ваше сообщение коммита
This commit is contained in:
@@ -53,8 +53,8 @@ function showDetails(contact) {
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 4px 32px rgba(0,0,0,0.12);
|
||||
padding: 32px 24px 24px 24px;
|
||||
max-width: 950px;
|
||||
margin: 40px auto;
|
||||
width: 100%;
|
||||
margin-top: 40px;
|
||||
position: relative;
|
||||
overflow-x: auto;
|
||||
}
|
||||
@@ -65,6 +65,9 @@ function showDetails(contact) {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.close-btn {
|
||||
position: absolute;
|
||||
top: 18px;
|
||||
right: 18px;
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 2rem;
|
||||
|
||||
@@ -357,8 +357,8 @@ function uninstallModule(module) {
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 4px 32px rgba(0,0,0,0.12);
|
||||
padding: 32px 24px 24px 24px;
|
||||
max-width: 950px;
|
||||
margin: 40px auto;
|
||||
width: 100%;
|
||||
margin-top: 40px;
|
||||
position: relative;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
47
frontend/src/components/MessagesTable.vue
Normal file
47
frontend/src/components/MessagesTable.vue
Normal file
@@ -0,0 +1,47 @@
|
||||
<template>
|
||||
<div class="messages-table-modal">
|
||||
<div class="messages-table-header">
|
||||
<h2>Новые сообщения</h2>
|
||||
</div>
|
||||
<div v-if="!messages.length" class="empty">Нет новых сообщений</div>
|
||||
<div v-else class="messages-list">
|
||||
<Message v-for="msg in messages" :key="msg.id" :message="msg" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { defineProps } from 'vue';
|
||||
import Message from './Message.vue';
|
||||
const props = defineProps({
|
||||
messages: { type: Array, required: true }
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.messages-table-modal {
|
||||
background: #fff;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 4px 32px rgba(0,0,0,0.12);
|
||||
padding: 32px 24px 24px 24px;
|
||||
width: 100%;
|
||||
margin-top: 40px;
|
||||
position: relative;
|
||||
overflow-x: auto;
|
||||
}
|
||||
.messages-table-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.empty {
|
||||
color: #888;
|
||||
text-align: center;
|
||||
margin: 20px 0;
|
||||
}
|
||||
.messages-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 18px;
|
||||
}
|
||||
</style>
|
||||
@@ -402,15 +402,15 @@ h3 {
|
||||
/* Медиа-запросы для адаптивности */
|
||||
@media screen and (min-width: 1200px) {
|
||||
.wallet-sidebar {
|
||||
width: 30%;
|
||||
max-width: 350px;
|
||||
width: 420px;
|
||||
max-width: 420px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 769px) and (max-width: 1199px) {
|
||||
.wallet-sidebar {
|
||||
width: 40%;
|
||||
max-width: 320px;
|
||||
width: 350px;
|
||||
max-width: 350px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
285
frontend/src/components/tables/TagsTableView.vue
Normal file
285
frontend/src/components/tables/TagsTableView.vue
Normal file
@@ -0,0 +1,285 @@
|
||||
<template>
|
||||
<div class="tags-table-wrapper">
|
||||
<div class="tableview-header-row">
|
||||
<button class="nav-btn" @click="goToTables">Таблицы</button>
|
||||
<button class="nav-btn" @click="goToCreate">Создать таблицу</button>
|
||||
<button class="close-btn" @click="closeTable">Закрыть</button>
|
||||
<button class="action-btn" disabled>Редактировать</button>
|
||||
<button class="danger-btn" disabled>Удалить</button>
|
||||
</div>
|
||||
<div class="tags-header-row">
|
||||
<h3>Теги</h3>
|
||||
</div>
|
||||
<table class="tags-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Название</th>
|
||||
<th>Описание</th>
|
||||
<th style="width:110px;">Действия</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="tag in tags" :key="tag.id">
|
||||
<td v-if="editId !== tag.id">{{ tag.name }}</td>
|
||||
<td v-else><input v-model="editName" class="edit-input" /></td>
|
||||
|
||||
<td v-if="editId !== tag.id">{{ tag.description || '—' }}</td>
|
||||
<td v-else><input v-model="editDescription" class="edit-input" /></td>
|
||||
|
||||
<td>
|
||||
<template v-if="editId === tag.id">
|
||||
<button class="save-btn" @click="saveEdit(tag)">Сохранить</button>
|
||||
<button class="cancel-btn" @click="cancelEdit">Отмена</button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<button class="edit-btn" @click="startEdit(tag)" title="Редактировать">✏️</button>
|
||||
<button class="delete-btn" @click="deleteTag(tag)" title="Удалить">🗑</button>
|
||||
</template>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="tags.length === 0">
|
||||
<td colspan="3" style="text-align:center; color:#888;">Нет тегов</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td style="text-align:center;">
|
||||
<button class="edit-btn" @click="showAddTagModal = true" title="Добавить тег">➕</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div v-if="showAddTagModal" class="modal-backdrop">
|
||||
<div class="modal">
|
||||
<h4>Добавить тег</h4>
|
||||
<input v-model="newTagName" class="edit-input" placeholder="Название" />
|
||||
<input v-model="newTagDescription" class="edit-input" placeholder="Описание" style="margin-top:0.7em;" />
|
||||
<div class="modal-actions">
|
||||
<button class="save-btn" @click="createTag">Создать</button>
|
||||
<button class="cancel-btn" @click="showAddTagModal = false">Отмена</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
const tags = ref([]);
|
||||
const editId = ref(null);
|
||||
const editName = ref('');
|
||||
const editDescription = ref('');
|
||||
const router = useRouter();
|
||||
|
||||
const showAddTagModal = ref(false);
|
||||
const newTagName = ref('');
|
||||
const newTagDescription = ref('');
|
||||
|
||||
function goToTables() {
|
||||
router.push({ name: 'tables-list' });
|
||||
}
|
||||
function goToCreate() {
|
||||
router.push({ name: 'create-table' });
|
||||
}
|
||||
function closeTable() {
|
||||
if (window.history.length > 1) {
|
||||
router.back();
|
||||
} else {
|
||||
router.push({ name: 'home' });
|
||||
}
|
||||
}
|
||||
|
||||
async function loadTags() {
|
||||
const res = await fetch('/api/tags');
|
||||
tags.value = await res.json();
|
||||
}
|
||||
|
||||
function startEdit(tag) {
|
||||
editId.value = tag.id;
|
||||
editName.value = tag.name;
|
||||
editDescription.value = tag.description || '';
|
||||
}
|
||||
|
||||
function cancelEdit() {
|
||||
editId.value = null;
|
||||
editName.value = '';
|
||||
editDescription.value = '';
|
||||
}
|
||||
|
||||
async function saveEdit(tag) {
|
||||
await fetch(`/api/tags/${tag.id}`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name: editName.value, description: editDescription.value })
|
||||
});
|
||||
await loadTags();
|
||||
cancelEdit();
|
||||
}
|
||||
|
||||
async function deleteTag(tag) {
|
||||
if (!confirm(`Удалить тег "${tag.name}"?`)) return;
|
||||
await fetch(`/api/tags/${tag.id}`, { method: 'DELETE' });
|
||||
await loadTags();
|
||||
}
|
||||
|
||||
async function createTag() {
|
||||
if (!newTagName.value.trim()) return;
|
||||
await fetch('/api/tags', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name: newTagName.value, description: newTagDescription.value })
|
||||
});
|
||||
newTagName.value = '';
|
||||
newTagDescription.value = '';
|
||||
showAddTagModal.value = false;
|
||||
await loadTags();
|
||||
}
|
||||
|
||||
onMounted(loadTags);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.tableview-header-row {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
margin: 1.2em 0 0.5em 0;
|
||||
}
|
||||
.tags-header-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
.add-plus-btn, .edit-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 1.1em;
|
||||
margin-right: 0.5em;
|
||||
color: #2ecc40;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
.add-plus-btn:hover, .edit-btn:hover, .save-btn:hover {
|
||||
color: #138496;
|
||||
}
|
||||
.close-btn {
|
||||
background: #ff4d4f;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 0.5em 1.2em;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
font-size: 1em;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
.close-btn:hover {
|
||||
background: #d9363e;
|
||||
}
|
||||
.action-btn {
|
||||
background: #2ecc40;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 0.5em 1.2em;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
font-size: 1em;
|
||||
margin-left: 0.7em;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
.action-btn:hover {
|
||||
background: #27ae38;
|
||||
}
|
||||
.danger-btn {
|
||||
background: #ff4d4f;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 0.5em 1.2em;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
font-size: 1em;
|
||||
margin-left: 0.7em;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
.danger-btn:hover {
|
||||
background: #d9363e;
|
||||
}
|
||||
.nav-btn {
|
||||
background: #eaeaea;
|
||||
color: #333;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 0.5em 1.2em;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
font-size: 1em;
|
||||
transition: background 0.2s;
|
||||
margin-right: 0.7em;
|
||||
}
|
||||
.nav-btn:hover {
|
||||
background: #d5d5d5;
|
||||
}
|
||||
.tags-table-wrapper {
|
||||
margin: 2em 0;
|
||||
background: #fff;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.04);
|
||||
padding: 1.5em 1em;
|
||||
}
|
||||
.tags-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
.tags-table th, .tags-table td {
|
||||
border: 1px solid #ececec;
|
||||
padding: 0.6em 1em;
|
||||
font-size: 1.05em;
|
||||
}
|
||||
.tags-table th {
|
||||
background: #f7f7f7;
|
||||
font-weight: 600;
|
||||
}
|
||||
.delete-btn {
|
||||
color: #dc3545;
|
||||
}
|
||||
.cancel-btn {
|
||||
color: #888;
|
||||
}
|
||||
.edit-input {
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 6px;
|
||||
padding: 4px 10px;
|
||||
font-size: 1em;
|
||||
min-width: 120px;
|
||||
}
|
||||
/* Модалка */
|
||||
.modal-backdrop {
|
||||
position: fixed;
|
||||
top: 0; left: 0; right: 0; bottom: 0;
|
||||
background: rgba(0,0,0,0.18);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
.modal {
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
padding: 2em 1.5em;
|
||||
box-shadow: 0 2px 16px rgba(0,0,0,0.13);
|
||||
min-width: 260px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.7em;
|
||||
}
|
||||
.modal-actions {
|
||||
display: flex;
|
||||
gap: 1em;
|
||||
margin-top: 1em;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
</style>
|
||||
@@ -1,63 +1,33 @@
|
||||
<template>
|
||||
<div class="tables-container">
|
||||
<header class="tables-header">
|
||||
<!-- <h2>Пользовательские таблицы</h2> -->
|
||||
<button class="create-btn" @click="createTable">Создать таблицу</button>
|
||||
</header>
|
||||
<div class="tables-list">
|
||||
<div
|
||||
v-for="table in tables"
|
||||
:key="table.id"
|
||||
class="table-card"
|
||||
:class="{ selected: table.id === props.selectedTableId }"
|
||||
@click="selectTable(table)"
|
||||
>
|
||||
<div class="table-info">
|
||||
<div class="table-title">{{ table.name }}</div>
|
||||
<div class="table-desc">{{ table.description }}</div>
|
||||
</div>
|
||||
<div class="table-actions">
|
||||
<button @click.stop="renameTable(table)">Переименовать</button>
|
||||
<button class="danger" @click.stop="confirmDelete(table)">Удалить</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!tables.length" class="empty-state">
|
||||
<ul class="tables-list-simple">
|
||||
<!-- Системная таблица tags -->
|
||||
<li>
|
||||
<button class="table-link" @click="goToTagsTable">Теги (tags)</button>
|
||||
</li>
|
||||
<!-- Пользовательские таблицы -->
|
||||
<li v-for="table in tables" :key="table.id">
|
||||
<button class="table-link" @click="selectTable(table)">{{ table.name }}</button>
|
||||
</li>
|
||||
<li v-if="!tables.length" class="empty-state">
|
||||
<span>Нет таблиц. Создайте первую!</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="showDeleteModal" class="modal-backdrop">
|
||||
<div class="modal">
|
||||
<p>Удалить таблицу <b>{{ selectedTable?.name }}</b>?</p>
|
||||
<div class="modal-actions">
|
||||
<button class="danger" @click="deleteTable(selectedTable)">Удалить</button>
|
||||
<button @click="showDeleteModal = false">Отмена</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<UserTableView
|
||||
v-if="props.selectedTableId"
|
||||
:table-id="props.selectedTableId"
|
||||
@close="emit('update:selected-table-id', null)"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, watch, nextTick } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import UserTableView from './UserTableView.vue';
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { useRouter, useRoute } from 'vue-router';
|
||||
import tablesService from '../../services/tablesService';
|
||||
|
||||
const props = defineProps({
|
||||
selectedTableId: Number
|
||||
});
|
||||
const emit = defineEmits(['update:selected-table-id']);
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
|
||||
const tables = ref([]);
|
||||
const showDeleteModal = ref(false);
|
||||
const selectedTable = ref(null);
|
||||
|
||||
async function fetchTables() {
|
||||
tables.value = await tablesService.getTables();
|
||||
@@ -70,38 +40,25 @@ function selectTable(table) {
|
||||
function createTable() {
|
||||
router.push({ name: 'create-table' });
|
||||
}
|
||||
|
||||
function renameTable(table) {
|
||||
const name = prompt('Новое имя', table.name);
|
||||
if (name && name !== table.name) {
|
||||
tablesService.updateTable(table.id, { name }).then(fetchTables);
|
||||
}
|
||||
}
|
||||
function confirmDelete(table) {
|
||||
selectedTable.value = table;
|
||||
showDeleteModal.value = true;
|
||||
}
|
||||
function deleteTable(table) {
|
||||
tablesService.deleteTable(table.id).then(() => {
|
||||
showDeleteModal.value = false;
|
||||
fetchTables();
|
||||
if (props.selectedTableId === table.id) emit('update:selected-table-id', null);
|
||||
});
|
||||
function goToTagsTable() {
|
||||
router.push({ name: 'tags-table-view' });
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.tables-container {
|
||||
max-width: 600px;
|
||||
margin: 2rem auto;
|
||||
background: #fff;
|
||||
border-radius: 18px;
|
||||
box-shadow: 0 2px 16px rgba(0,0,0,0.07);
|
||||
padding: 2rem 1.5rem;
|
||||
/* max-width: 600px; */
|
||||
/* margin: 2rem auto; */
|
||||
margin-top: 2rem;
|
||||
margin-left: 0;
|
||||
padding: 2rem 1.5rem 2rem 1.5rem;
|
||||
/* background: #fff; */
|
||||
/* border-radius: 18px; */
|
||||
/* box-shadow: 0 2px 16px rgba(0,0,0,0.07); */
|
||||
}
|
||||
.tables-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
@@ -118,62 +75,37 @@ function deleteTable(table) {
|
||||
.create-btn:hover {
|
||||
background: #27ae38;
|
||||
}
|
||||
.tables-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
.tables-list-simple {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
.table-card {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
background: #f8f9fa;
|
||||
border-radius: 10px;
|
||||
padding: 1rem;
|
||||
box-shadow: 0 1px 4px rgba(0,0,0,0.03);
|
||||
cursor: pointer;
|
||||
border: 2px solid transparent;
|
||||
transition: border 0.2s;
|
||||
.tables-list-simple li {
|
||||
margin-bottom: 0.5em;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
padding-bottom: 0.5em;
|
||||
}
|
||||
.table-card.selected {
|
||||
border: 2px solid #2ecc40;
|
||||
.tables-list-simple li:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
.table-info {
|
||||
flex: 1 1 200px;
|
||||
}
|
||||
.table-title {
|
||||
font-weight: 600;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
.table-desc {
|
||||
color: #888;
|
||||
font-size: 0.95em;
|
||||
margin-top: 0.2em;
|
||||
}
|
||||
.table-actions {
|
||||
display: flex;
|
||||
gap: 0.5em;
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
.table-actions button {
|
||||
background: #eaeaea;
|
||||
.table-link {
|
||||
background: none;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 0.4em 1em;
|
||||
color: #2ecc40;
|
||||
font-size: 1.1em;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
transition: background 0.2s;
|
||||
text-align: left;
|
||||
padding: 0.2em 0;
|
||||
transition: color 0.2s, background 0.2s;
|
||||
width: 100%;
|
||||
display: block;
|
||||
border-radius: 6px;
|
||||
}
|
||||
.table-actions button:hover {
|
||||
background: #d5d5d5;
|
||||
}
|
||||
.table-actions .danger {
|
||||
background: #ff4d4f;
|
||||
color: #fff;
|
||||
}
|
||||
.table-actions .danger:hover {
|
||||
background: #d9363e;
|
||||
.table-link:hover {
|
||||
color: #138496;
|
||||
background: #f5f7fa;
|
||||
text-decoration: none;
|
||||
}
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
@@ -181,49 +113,4 @@ function deleteTable(table) {
|
||||
margin: 2em 0;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
/* Модалка */
|
||||
.modal-backdrop {
|
||||
position: fixed;
|
||||
top: 0; left: 0; right: 0; bottom: 0;
|
||||
background: rgba(0,0,0,0.18);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
.modal {
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
padding: 2em 1.5em;
|
||||
box-shadow: 0 2px 16px rgba(0,0,0,0.13);
|
||||
min-width: 260px;
|
||||
}
|
||||
.modal-actions {
|
||||
display: flex;
|
||||
gap: 1em;
|
||||
margin-top: 1.5em;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
/* Адаптивность */
|
||||
@media (max-width: 600px) {
|
||||
.tables-container {
|
||||
padding: 1em 0.3em;
|
||||
}
|
||||
.table-card {
|
||||
flex-direction: column;
|
||||
gap: 0.7em;
|
||||
padding: 0.7em;
|
||||
}
|
||||
.tables-header {
|
||||
flex-direction: column;
|
||||
gap: 0.7em;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.table-actions {
|
||||
flex-wrap: wrap;
|
||||
gap: 0.4em;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user