ваше сообщение коммита
This commit is contained in:
@@ -4,17 +4,52 @@
|
||||
<h2>Контакты</h2>
|
||||
<button class="close-btn" @click="$emit('close')">×</button>
|
||||
</div>
|
||||
<div class="filters-panel">
|
||||
<input v-model="filterName" placeholder="Имя" />
|
||||
<input v-model="filterEmail" placeholder="Email" />
|
||||
<input v-model="filterTelegram" placeholder="Telegram" />
|
||||
<input v-model="filterWallet" placeholder="Кошелек" />
|
||||
<input v-model="filterDateFrom" type="date" placeholder="Дата от" />
|
||||
<input v-model="filterDateTo" type="date" placeholder="Дата до" />
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" v-model="filterOnlyNewMessages" /> Только с новыми сообщениями
|
||||
</label>
|
||||
</div>
|
||||
<el-form :inline="true" class="filters-form" label-position="top">
|
||||
<el-form-item label="Поиск">
|
||||
<el-input v-model="filterSearch" placeholder="Поиск по имени, email, telegram, кошельку" clearable @input="onAnyFilterChange" />
|
||||
</el-form-item>
|
||||
<el-form-item label="Тип контакта">
|
||||
<el-select v-model="filterContactType" placeholder="Все" style="min-width:120px;" @change="onAnyFilterChange">
|
||||
<el-option label="Все" value="all" />
|
||||
<el-option label="Email" value="email" />
|
||||
<el-option label="Telegram" value="telegram" />
|
||||
<el-option label="Кошелек" value="wallet" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="Дата от">
|
||||
<el-date-picker v-model="filterDateFrom" type="date" placeholder="Дата от" clearable style="width: 100%;" @change="onAnyFilterChange" />
|
||||
</el-form-item>
|
||||
<el-form-item label="Дата до">
|
||||
<el-date-picker v-model="filterDateTo" type="date" placeholder="Дата до" clearable style="width: 100%;" @change="onAnyFilterChange" />
|
||||
</el-form-item>
|
||||
<el-form-item label="Только с новыми сообщениями">
|
||||
<el-select v-model="filterNewMessages" placeholder="Нет" style="min-width:110px;" @change="onAnyFilterChange">
|
||||
<el-option label="Нет" :value="''" />
|
||||
<el-option label="Да" value="yes" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="Теги">
|
||||
<el-select
|
||||
v-model="selectedTagIds"
|
||||
multiple
|
||||
filterable
|
||||
collapse-tags
|
||||
placeholder="Выберите теги"
|
||||
style="min-width:180px;"
|
||||
@change="onAnyFilterChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="tag in allTags"
|
||||
:key="tag.id"
|
||||
:label="tag.name"
|
||||
:value="tag.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button @click="resetFilters" type="default">Сбросить фильтры</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<table class="contact-table">
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -27,7 +62,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="contact in filteredContactsArray" :key="contact.id" :class="{ 'new-contact-row': newIds.includes(contact.id) }">
|
||||
<tr v-for="contact in contactsArray" :key="contact.id" :class="{ 'new-contact-row': newIds.includes(contact.id) }">
|
||||
<td>{{ contact.name || '-' }}</td>
|
||||
<td>{{ contact.email || '-' }}</td>
|
||||
<td>{{ contact.telegram || '-' }}</td>
|
||||
@@ -44,8 +79,9 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { defineProps, computed, ref } from 'vue';
|
||||
import { defineProps, computed, ref, onMounted, watch } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { ElSelect, ElOption, ElForm, ElFormItem, ElInput, ElDatePicker, ElCheckbox, ElButton } from 'element-plus';
|
||||
const props = defineProps({
|
||||
contacts: { type: Array, default: () => [] },
|
||||
newContacts: { type: Array, default: () => [] },
|
||||
@@ -53,38 +89,79 @@ const props = defineProps({
|
||||
markMessagesAsReadForUser: { type: Function, default: null },
|
||||
markContactAsRead: { type: Function, default: null }
|
||||
});
|
||||
const contactsArray = computed(() => Array.from(props.contacts || []));
|
||||
const contactsArray = ref([]); // теперь управляем вручную
|
||||
const newIds = computed(() => props.newContacts.map(c => c.id));
|
||||
const newMsgUserIds = computed(() => props.newMessages.map(m => String(m.user_id)));
|
||||
const router = useRouter();
|
||||
|
||||
// Фильтры
|
||||
const filterName = ref('');
|
||||
const filterEmail = ref('');
|
||||
const filterTelegram = ref('');
|
||||
const filterWallet = ref('');
|
||||
const filterSearch = ref('');
|
||||
const filterContactType = ref('all');
|
||||
const filterDateFrom = ref('');
|
||||
const filterDateTo = ref('');
|
||||
const filterOnlyNewMessages = ref(false);
|
||||
const filterNewMessages = ref('');
|
||||
|
||||
const filteredContactsArray = computed(() => {
|
||||
return contactsArray.value.filter(contact => {
|
||||
const nameMatch = !filterName.value || (contact.name || '').toLowerCase().includes(filterName.value.toLowerCase());
|
||||
const emailMatch = !filterEmail.value || (contact.email || '').toLowerCase().includes(filterEmail.value.toLowerCase());
|
||||
const telegramMatch = !filterTelegram.value || (contact.telegram || '').toLowerCase().includes(filterTelegram.value.toLowerCase());
|
||||
const walletMatch = !filterWallet.value || (contact.wallet || '').toLowerCase().includes(filterWallet.value.toLowerCase());
|
||||
let dateFromMatch = true, dateToMatch = true;
|
||||
if (filterDateFrom.value && contact.created_at) {
|
||||
dateFromMatch = new Date(contact.created_at) >= new Date(filterDateFrom.value);
|
||||
}
|
||||
if (filterDateTo.value && contact.created_at) {
|
||||
dateToMatch = new Date(contact.created_at) <= new Date(filterDateTo.value);
|
||||
}
|
||||
const newMsgMatch = !filterOnlyNewMessages.value || newMsgUserIds.value.includes(String(contact.id));
|
||||
return nameMatch && emailMatch && telegramMatch && walletMatch && dateFromMatch && dateToMatch && newMsgMatch;
|
||||
});
|
||||
// Теги
|
||||
const allTags = ref([]);
|
||||
const selectedTagIds = ref([]);
|
||||
|
||||
onMounted(async () => {
|
||||
await loadTags();
|
||||
await fetchContacts();
|
||||
});
|
||||
|
||||
async function loadTags() {
|
||||
const res = await fetch('/api/tags');
|
||||
allTags.value = await res.json();
|
||||
}
|
||||
|
||||
function buildQuery() {
|
||||
const params = new URLSearchParams();
|
||||
if (selectedTagIds.value.length > 0) params.append('tagIds', selectedTagIds.value.join(','));
|
||||
if (filterDateFrom.value) params.append('dateFrom', formatDateOnly(filterDateFrom.value));
|
||||
if (filterDateTo.value) params.append('dateTo', formatDateOnly(filterDateTo.value));
|
||||
if (filterContactType.value && filterContactType.value !== 'all') params.append('contactType', filterContactType.value);
|
||||
if (filterSearch.value) params.append('search', filterSearch.value);
|
||||
if (filterNewMessages.value) params.append('newMessages', filterNewMessages.value);
|
||||
return params.toString();
|
||||
}
|
||||
|
||||
async function fetchContacts() {
|
||||
let url = '/api/users';
|
||||
const query = buildQuery();
|
||||
if (query) url += '?' + query;
|
||||
const res = await fetch(url);
|
||||
const data = await res.json();
|
||||
contactsArray.value = data.contacts || [];
|
||||
}
|
||||
|
||||
function onTagsFilterChange() {
|
||||
onAnyFilterChange();
|
||||
}
|
||||
|
||||
function onAnyFilterChange() {
|
||||
fetchContacts();
|
||||
}
|
||||
|
||||
function resetFilters() {
|
||||
filterSearch.value = '';
|
||||
filterContactType.value = 'all';
|
||||
filterDateFrom.value = '';
|
||||
filterDateTo.value = '';
|
||||
filterNewMessages.value = '';
|
||||
selectedTagIds.value = [];
|
||||
fetchContacts();
|
||||
}
|
||||
|
||||
function formatDateOnly(date) {
|
||||
if (!date) return '';
|
||||
const d = new Date(date);
|
||||
const year = d.getFullYear();
|
||||
const month = String(d.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(d.getDate()).padStart(2, '0');
|
||||
return `${year}-${month}-${day}`;
|
||||
}
|
||||
|
||||
function formatDate(date) {
|
||||
if (!date) return '-';
|
||||
return new Date(date).toLocaleString();
|
||||
@@ -201,27 +278,21 @@ async function showDetails(contact) {
|
||||
background: #e6ffe6 !important;
|
||||
transition: background 0.3s;
|
||||
}
|
||||
.filters-panel {
|
||||
.filters-form {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 18px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 1.2em 1.5em;
|
||||
align-items: flex-end;
|
||||
background: #f7f9fa;
|
||||
border-radius: 12px;
|
||||
padding: 1.2em 1em 0.7em 1em;
|
||||
margin-bottom: 1.2em;
|
||||
}
|
||||
.filters-panel input {
|
||||
padding: 6px 10px;
|
||||
border: 1px solid #d0d7de;
|
||||
border-radius: 6px;
|
||||
font-size: 1em;
|
||||
min-width: 110px;
|
||||
}
|
||||
.filters-panel input[type="checkbox"] {
|
||||
margin-right: 4px;
|
||||
}
|
||||
.checkbox-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 0.98em;
|
||||
user-select: none;
|
||||
@media (max-width: 900px) {
|
||||
.filters-form {
|
||||
flex-direction: column;
|
||||
gap: 0.7em 0;
|
||||
}
|
||||
}
|
||||
.new-msg-icon {
|
||||
color: #ff9800;
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
<template>
|
||||
<td>
|
||||
<template v-if="column.type === 'tags'">
|
||||
<div v-if="!editing" @click="editing = true" class="tags-cell-view">
|
||||
<span v-if="selectedTagNames.length">{{ selectedTagNames.join(', ') }}</span>
|
||||
<span v-else style="color:#bbb">—</span>
|
||||
</div>
|
||||
<div v-else class="tags-cell-edit">
|
||||
<div class="tags-multiselect">
|
||||
<div v-for="tag in allTags" :key="tag.id" class="tag-option">
|
||||
<input type="checkbox" :id="'cell-tag-' + tag.id + '-' + rowId" :value="tag.id" v-model="editTagIds" />
|
||||
<label :for="'cell-tag-' + tag.id + '-' + rowId">{{ tag.name }}</label>
|
||||
</div>
|
||||
</div>
|
||||
<button class="save-btn" @click="saveTags">Сохранить</button>
|
||||
<button class="cancel-btn" @click="cancelTags">Отмена</button>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<input
|
||||
v-model="localValue"
|
||||
@blur="save"
|
||||
@@ -7,26 +23,77 @@
|
||||
:placeholder="column.name"
|
||||
class="cell-input"
|
||||
/>
|
||||
</td>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue';
|
||||
import { ref, watch, onMounted } from 'vue';
|
||||
const props = defineProps(['rowId', 'column', 'cellValues']);
|
||||
const emit = defineEmits(['update']);
|
||||
|
||||
const localValue = ref('');
|
||||
const editing = ref(false);
|
||||
const allTags = ref([]); // Все теги из /api/tags
|
||||
const editTagIds = ref([]); // id выбранных тегов в режиме редактирования
|
||||
|
||||
// Для отображения выбранных тегов
|
||||
const selectedTagNames = ref([]);
|
||||
|
||||
onMounted(async () => {
|
||||
if (props.column.type === 'tags') {
|
||||
await loadTags();
|
||||
updateSelectedTagNames();
|
||||
}
|
||||
});
|
||||
|
||||
async function loadTags() {
|
||||
const res = await fetch('/api/tags');
|
||||
allTags.value = await res.json();
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [props.rowId, props.column.id, props.cellValues],
|
||||
() => {
|
||||
const cell = props.cellValues.find(
|
||||
c => c.row_id === props.rowId && c.column_id === props.column.id
|
||||
);
|
||||
localValue.value = cell ? cell.value : '';
|
||||
if (props.column.type === 'tags') {
|
||||
// Значение ячейки — строка с JSON-массивом id тегов
|
||||
const cell = props.cellValues.find(
|
||||
c => c.row_id === props.rowId && c.column_id === props.column.id
|
||||
);
|
||||
let ids = [];
|
||||
if (cell && cell.value) {
|
||||
try {
|
||||
ids = JSON.parse(cell.value);
|
||||
} catch {}
|
||||
}
|
||||
editTagIds.value = Array.isArray(ids) ? ids : [];
|
||||
updateSelectedTagNames();
|
||||
} else {
|
||||
const cell = props.cellValues.find(
|
||||
c => c.row_id === props.rowId && c.column_id === props.column.id
|
||||
);
|
||||
localValue.value = cell ? cell.value : '';
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
function updateSelectedTagNames() {
|
||||
if (props.column.type === 'tags') {
|
||||
selectedTagNames.value = allTags.value
|
||||
.filter(tag => editTagIds.value.includes(tag.id))
|
||||
.map(tag => tag.name);
|
||||
}
|
||||
}
|
||||
|
||||
function saveTags() {
|
||||
emit('update', JSON.stringify(editTagIds.value));
|
||||
editing.value = false;
|
||||
}
|
||||
function cancelTags() {
|
||||
editing.value = false;
|
||||
updateSelectedTagNames();
|
||||
}
|
||||
|
||||
function save() {
|
||||
emit('update', localValue.value);
|
||||
}
|
||||
@@ -46,4 +113,52 @@ function save() {
|
||||
border: 1.5px solid #2ecc40;
|
||||
outline: none;
|
||||
}
|
||||
.tags-cell-view {
|
||||
min-height: 1.7em;
|
||||
cursor: pointer;
|
||||
padding: 0.2em 0.1em;
|
||||
}
|
||||
.tags-cell-edit {
|
||||
background: #f8f8f8;
|
||||
border-radius: 6px;
|
||||
padding: 0.3em 0.2em 0.5em 0.2em;
|
||||
}
|
||||
.tags-multiselect {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5em 1.2em;
|
||||
margin-bottom: 0.7em;
|
||||
}
|
||||
.tag-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.3em;
|
||||
}
|
||||
.save-btn {
|
||||
background: #2ecc40;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 0.3em 1em;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
margin-right: 0.7em;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
.save-btn:hover {
|
||||
background: #27ae38;
|
||||
}
|
||||
.cancel-btn {
|
||||
background: #eaeaea;
|
||||
color: #333;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 0.3em 1em;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
.cancel-btn:hover {
|
||||
background: #d5d5d5;
|
||||
}
|
||||
</style>
|
||||
@@ -7,32 +7,38 @@
|
||||
<table class="notion-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th v-for="col in columns" :key="col.id" @dblclick="editColumn(col)">
|
||||
<th v-for="col in columns" :key="col.id" @dblclick="editColumn(col)" class="th-col">
|
||||
<span v-if="!editingCol || editingCol.id !== col.id">{{ col.name }}</span>
|
||||
<input v-else v-model="colEditValue" @blur="saveColEdit(col)" @keyup.enter="saveColEdit(col)" @keyup.esc="cancelColEdit" class="notion-input" />
|
||||
<button class="col-menu" @click.stop="openColMenu(col)">⋮</button>
|
||||
<button class="col-menu" @click.stop="openColMenu(col, $event)">⋮</button>
|
||||
<!-- Меню столбца -->
|
||||
<div v-if="openedColMenuId === col.id" class="context-menu" :style="colMenuStyle">
|
||||
<button class="menu-item" @click="startRenameCol(col)">Переименовать</button>
|
||||
<button class="menu-item" @click="startChangeTypeCol(col)">Изменить тип</button>
|
||||
<button class="menu-item danger" @click="deleteColumn(col)">Удалить</button>
|
||||
</div>
|
||||
</th>
|
||||
<th>
|
||||
<button class="add-col" @click="addColumn">+</button>
|
||||
<button class="add-col" @click="showAddColModal = true">+</button>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="row in rows" :key="row.id">
|
||||
<td v-for="col in columns" :key="col.id" @click="startEdit(row, col)">
|
||||
<span v-if="!isEditing(row, col)">{{ getCellValue(row, col) || '—' }}</span>
|
||||
<input
|
||||
v-else
|
||||
v-model="editValue"
|
||||
@blur="saveEdit(row, col)"
|
||||
@keyup.enter="saveEdit(row, col)"
|
||||
@keyup.esc="cancelEdit"
|
||||
class="notion-input"
|
||||
autofocus
|
||||
<td v-for="col in columns" :key="col.id">
|
||||
<TableCell
|
||||
:rowId="row.id"
|
||||
:column="col"
|
||||
:cellValues="cellValues"
|
||||
@update="val => saveCellValue(row.id, col.id, val)"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<button class="row-menu" @click.stop="openRowMenu(row)">⋮</button>
|
||||
<button class="row-menu" @click.stop="openRowMenu(row, $event)">⋮</button>
|
||||
<!-- Меню строки -->
|
||||
<div v-if="openedRowMenuId === row.id" class="context-menu" :style="rowMenuStyle">
|
||||
<button class="menu-item danger" @click="deleteRow(row)">Удалить</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@@ -42,13 +48,55 @@
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<!-- Модалки и меню можно реализовать через отдельные компоненты или простые div -->
|
||||
<!-- Оверлей для закрытия меню по клику вне -->
|
||||
<div v-if="openedColMenuId || openedRowMenuId" class="menu-overlay" @click="closeMenus"></div>
|
||||
<!-- Модалка добавления столбца -->
|
||||
<div v-if="showAddColModal" class="modal-backdrop">
|
||||
<div class="modal add-col-modal">
|
||||
<h4>Добавить столбец</h4>
|
||||
<label>Название</label>
|
||||
<input v-model="newColName" class="notion-input" placeholder="Название столбца" />
|
||||
<label>Тип</label>
|
||||
<select v-model="newColType" class="notion-input">
|
||||
<option value="text">Текст</option>
|
||||
<option value="number">Число</option>
|
||||
<option value="tags">Теги</option>
|
||||
</select>
|
||||
<label>Назначение столбца</label>
|
||||
<select v-model="newColPurpose" class="notion-input">
|
||||
<option value="">— Не выбрано —</option>
|
||||
<option value="question">Это столбец с вопросами</option>
|
||||
<option value="answer">Это столбец с ответами</option>
|
||||
<option value="clarifyingAnswer">Ответ с уточняющим вопросом</option>
|
||||
<option value="objectionAnswer">Ответ на возражение</option>
|
||||
<option value="userTags">Это столбец с тегами пользователей</option>
|
||||
<option value="context">Это столбец с дополнительным контекстом</option>
|
||||
<option value="product">Это столбец с продуктом/услугой</option>
|
||||
<option value="priority">Это столбец с приоритетом</option>
|
||||
<option value="date">Это столбец с датой</option>
|
||||
</select>
|
||||
<div v-if="newColType === 'tags'">
|
||||
<label>Выберите теги</label>
|
||||
<div class="tags-multiselect">
|
||||
<div v-for="tag in tags" :key="tag.id" class="tag-option">
|
||||
<input type="checkbox" :id="'tag-' + tag.id" :value="tag.id" v-model="selectedTagIds" />
|
||||
<label :for="'tag-' + tag.id">{{ tag.name }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<button class="save-btn" @click="handleAddColumn">Добавить</button>
|
||||
<button class="cancel-btn" @click="closeAddColModal">Отмена</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { ref, onMounted } from 'vue';
|
||||
import tablesService from '../../services/tablesService';
|
||||
import TableCell from './TableCell.vue';
|
||||
|
||||
const props = defineProps({ tableId: Number });
|
||||
const columns = ref([]);
|
||||
@@ -56,6 +104,52 @@ const rows = ref([]);
|
||||
const cellValues = ref([]);
|
||||
const tableMeta = ref(null);
|
||||
|
||||
// Для модалки добавления столбца
|
||||
const showAddColModal = ref(false);
|
||||
const newColName = ref('');
|
||||
const newColType = ref('text');
|
||||
const tags = ref([]);
|
||||
const selectedTagIds = ref([]);
|
||||
const newColPurpose = ref("");
|
||||
|
||||
// Меню столбца
|
||||
const openedColMenuId = ref(null);
|
||||
const openedRowMenuId = ref(null);
|
||||
const colMenuStyle = ref('');
|
||||
const rowMenuStyle = ref('');
|
||||
|
||||
function closeAddColModal() {
|
||||
showAddColModal.value = false;
|
||||
newColName.value = '';
|
||||
newColType.value = 'text';
|
||||
selectedTagIds.value = [];
|
||||
newColPurpose.value = '';
|
||||
}
|
||||
|
||||
async function handleAddColumn() {
|
||||
if (!newColName.value) return;
|
||||
const data = { name: newColName.value, type: newColType.value };
|
||||
if (newColType.value === 'tags') {
|
||||
data.tagIds = selectedTagIds.value;
|
||||
}
|
||||
if (newColPurpose.value) {
|
||||
data.purpose = newColPurpose.value;
|
||||
}
|
||||
await tablesService.addColumn(props.tableId, data);
|
||||
closeAddColModal();
|
||||
fetchTable();
|
||||
}
|
||||
|
||||
async function loadTags() {
|
||||
const res = await fetch('/api/tags');
|
||||
tags.value = await res.json();
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchTable();
|
||||
loadTags();
|
||||
});
|
||||
|
||||
// Для редактирования ячеек
|
||||
const editing = ref({ rowId: null, colId: null });
|
||||
const editValue = ref('');
|
||||
@@ -95,13 +189,40 @@ function cancelColEdit() {
|
||||
|
||||
// Добавление/удаление
|
||||
function addColumn() {
|
||||
tablesService.addColumn(props.tableId, { name: 'Новый столбец', type: 'text' }).then(fetchTable);
|
||||
showAddColModal.value = true;
|
||||
}
|
||||
function addRow() {
|
||||
tablesService.addRow(props.tableId).then(fetchTable);
|
||||
}
|
||||
function openColMenu(col) { /* TODO: контекстное меню */ }
|
||||
function openRowMenu(row) { /* TODO: контекстное меню */ }
|
||||
function openColMenu(col, event) {
|
||||
openedColMenuId.value = col.id;
|
||||
openedRowMenuId.value = null;
|
||||
setMenuPosition(event, colMenuStyle);
|
||||
}
|
||||
function openRowMenu(row, event) {
|
||||
openedRowMenuId.value = row.id;
|
||||
openedColMenuId.value = null;
|
||||
setMenuPosition(event, rowMenuStyle);
|
||||
}
|
||||
function closeMenus() {
|
||||
openedColMenuId.value = null;
|
||||
openedRowMenuId.value = null;
|
||||
}
|
||||
function setMenuPosition(event, styleRef) {
|
||||
// Позиционируем меню под кнопкой
|
||||
const rect = event.target.getBoundingClientRect();
|
||||
styleRef.value = `position:fixed;top:${rect.bottom + 4}px;left:${rect.left}px;z-index:2000;`;
|
||||
}
|
||||
// Действия меню столбца
|
||||
function startRenameCol(col) {
|
||||
closeMenus();
|
||||
editColumn(col);
|
||||
}
|
||||
function startChangeTypeCol(col) {
|
||||
closeMenus();
|
||||
// TODO: реализовать смену типа столбца (можно открыть модалку выбора типа)
|
||||
alert('Изменение типа столбца пока не реализовано');
|
||||
}
|
||||
|
||||
// Загрузка данных
|
||||
async function fetchTable() {
|
||||
@@ -111,7 +232,21 @@ async function fetchTable() {
|
||||
cellValues.value = data.cellValues;
|
||||
tableMeta.value = { name: data.name, description: data.description };
|
||||
}
|
||||
fetchTable();
|
||||
|
||||
function saveCellValue(rowId, columnId, value) {
|
||||
tablesService.saveCell({ row_id: rowId, column_id: columnId, value }).then(fetchTable);
|
||||
}
|
||||
|
||||
function deleteRow(row) {
|
||||
if (confirm('Удалить эту строку?')) {
|
||||
tablesService.deleteRow(row.id).then(fetchTable);
|
||||
}
|
||||
}
|
||||
function deleteColumn(col) {
|
||||
if (confirm('Удалить этот столбец?')) {
|
||||
tablesService.deleteColumn(col.id).then(fetchTable);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -174,4 +309,138 @@ fetchTable();
|
||||
color: #888;
|
||||
font-size: 1em;
|
||||
}
|
||||
.modal-backdrop {
|
||||
position: fixed;
|
||||
top: 0; left: 0; right: 0; bottom: 0;
|
||||
background: rgba(0,0,0,0.18);
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.modal {
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 16px rgba(0,0,0,0.13);
|
||||
padding: 2em 2em 1.5em 2em;
|
||||
min-width: 320px;
|
||||
max-width: 95vw;
|
||||
}
|
||||
.add-col-modal label {
|
||||
font-weight: 500;
|
||||
margin-top: 0.7em;
|
||||
display: block;
|
||||
}
|
||||
.add-col-modal input,
|
||||
.add-col-modal select {
|
||||
width: 100%;
|
||||
border: 1px solid #ececec;
|
||||
border-radius: 7px;
|
||||
padding: 0.5em 0.8em;
|
||||
font-size: 1em;
|
||||
background: #fafbfc;
|
||||
margin-bottom: 0.7em;
|
||||
}
|
||||
.tags-multiselect {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5em 1.2em;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
.tag-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.3em;
|
||||
}
|
||||
.save-btn {
|
||||
background: #2ecc40;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 0.5em 1.2em;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
margin-right: 0.7em;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
.save-btn:hover {
|
||||
background: #27ae38;
|
||||
}
|
||||
.cancel-btn {
|
||||
background: #eaeaea;
|
||||
color: #333;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 0.5em 1.2em;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
.cancel-btn:hover {
|
||||
background: #d5d5d5;
|
||||
}
|
||||
.th-col {
|
||||
position: relative;
|
||||
}
|
||||
.delete-col-btn {
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
right: 6px;
|
||||
background: none;
|
||||
border: none;
|
||||
color: #ff4d4f;
|
||||
font-size: 1.1em;
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
.th-col:hover .delete-col-btn {
|
||||
opacity: 1;
|
||||
}
|
||||
.delete-row-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #ff4d4f;
|
||||
font-size: 1.1em;
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
tr:hover .delete-row-btn {
|
||||
opacity: 1;
|
||||
}
|
||||
.context-menu {
|
||||
background: #fff;
|
||||
border: 1px solid #ececec;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 12px rgba(0,0,0,0.13);
|
||||
min-width: 150px;
|
||||
padding: 0.3em 0.2em;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: fixed;
|
||||
z-index: 2001;
|
||||
}
|
||||
.menu-item {
|
||||
background: none;
|
||||
border: none;
|
||||
text-align: left;
|
||||
padding: 0.6em 1.1em;
|
||||
font-size: 1em;
|
||||
color: #222;
|
||||
cursor: pointer;
|
||||
border-radius: 6px;
|
||||
transition: background 0.18s;
|
||||
}
|
||||
.menu-item:hover {
|
||||
background: #f2f8f4;
|
||||
}
|
||||
.menu-item.danger {
|
||||
color: #ff4d4f;
|
||||
}
|
||||
.menu-overlay {
|
||||
position: fixed;
|
||||
top: 0; left: 0; right: 0; bottom: 0;
|
||||
z-index: 1999;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user