ваше сообщение коммита
This commit is contained in:
@@ -1,22 +1,75 @@
|
||||
<template>
|
||||
<template v-if="column.type === 'tags'">
|
||||
<template v-if="column.type === 'multiselect'">
|
||||
<div v-if="!editing" @click="editing = true" class="tags-cell-view">
|
||||
<span v-if="selectedTagNames.length">{{ selectedTagNames.join(', ') }}</span>
|
||||
<span v-if="selectedMultiNames.length">{{ selectedMultiNames.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 v-for="option in multiOptions" :key="option" class="tag-option">
|
||||
<input type="checkbox" :id="'cell-multi-' + option + '-' + rowId" :value="option" v-model="editMultiValues" />
|
||||
<label :for="'cell-multi-' + option + '-' + rowId">{{ option }}</label>
|
||||
<span class="remove-option" @click.stop="removeMultiOption(option)">✕</span>
|
||||
</div>
|
||||
</div>
|
||||
<button class="save-btn" @click="saveTags">Сохранить</button>
|
||||
<button class="cancel-btn" @click="cancelTags">Отмена</button>
|
||||
<div class="add-multiselect-option">
|
||||
<input v-model="newMultiOption" @keyup.enter="addMultiOption" placeholder="Новое значение" />
|
||||
<button class="add-btn" @click="addMultiOption">+</button>
|
||||
</div>
|
||||
<button class="save-btn" @click="saveMulti">Сохранить</button>
|
||||
<button class="cancel-btn" @click="cancelMulti">Отмена</button>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="column.type === 'relation'">
|
||||
<div v-if="!editing" @click="editing = true" class="tags-cell-view">
|
||||
<span v-if="selectedRelationName">{{ selectedRelationName }}</span>
|
||||
<span v-else style="color:#bbb">—</span>
|
||||
</div>
|
||||
<div v-else class="tags-cell-edit">
|
||||
<select v-model="editRelationValue" class="notion-input">
|
||||
<option v-for="opt in relationOptions" :key="opt.id" :value="opt.id">{{ opt.display }}</option>
|
||||
</select>
|
||||
<button class="save-btn" @click="saveRelation">Сохранить</button>
|
||||
<button class="cancel-btn" @click="cancelRelation">Отмена</button>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="column.type === 'lookup'">
|
||||
<div class="lookup-cell-view">
|
||||
<span v-if="lookupValues.length">{{ lookupValues.join(', ') }}</span>
|
||||
<span v-else style="color:#bbb">—</span>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="column.type === 'multiselect-relation'">
|
||||
<div v-if="!editing" @click="editing = true" class="tags-cell-view">
|
||||
<span v-if="selectedMultiRelationNames.length">{{ selectedMultiRelationNames.map(prettyDisplay).join(', ') }}</span>
|
||||
<span v-else>{{ prettyDisplay(localValue) }}</span>
|
||||
</div>
|
||||
<div v-else class="tags-cell-edit">
|
||||
<div class="tags-multiselect">
|
||||
<div v-for="option in multiRelationOptions" :key="option.id" class="tag-option">
|
||||
<input type="checkbox" :id="'cell-multirel-' + option.id + '-' + rowId" :value="String(option.id)" v-model="editMultiRelationValues" />
|
||||
<label :for="'cell-multirel-' + option.id + '-' + rowId">{{ prettyDisplay(option.display) }}</label>
|
||||
<button class="delete-tag-btn" @click.prevent="deleteTag(option.id)" title="Удалить тег">×</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="add-tag-block">
|
||||
<button v-if="!showAddTagInput" class="add-tag-btn" @click="showAddTagInput = true">+ Новый тег</button>
|
||||
<div v-else class="add-tag-form">
|
||||
<input v-model="newTagName" @keyup.enter="addTag" placeholder="Название тега" />
|
||||
<button class="add-tag-confirm" @click="addTag">Добавить</button>
|
||||
<button class="add-tag-cancel" @click="showAddTagInput = false; newTagName = ''">×</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="action-buttons">
|
||||
<button class="save-btn" @click="saveMultiRelation">Сохранить</button>
|
||||
<button class="cancel-btn" @click="cancelMultiRelation">Отмена</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span v-if="isArrayString(localValue)">{{ parseArrayString(localValue).join(', ') }}</span>
|
||||
<input
|
||||
v-else
|
||||
v-model="localValue"
|
||||
@blur="save"
|
||||
@keyup.enter="save"
|
||||
@@ -27,46 +80,113 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch, onMounted } from 'vue';
|
||||
import { ref, watch, onMounted, computed } from 'vue';
|
||||
import tablesService from '../../services/tablesService';
|
||||
const props = defineProps(['rowId', 'column', 'cellValues']);
|
||||
const emit = defineEmits(['update']);
|
||||
|
||||
const localValue = ref('');
|
||||
const editing = ref(false);
|
||||
const allTags = ref([]); // Все теги из /api/tags
|
||||
// const allTags = ref([]); // Все теги из /api/tags
|
||||
const editTagIds = ref([]); // id выбранных тегов в режиме редактирования
|
||||
|
||||
// Для отображения выбранных тегов
|
||||
const selectedTagNames = ref([]);
|
||||
|
||||
const multiOptions = ref([]);
|
||||
const editMultiValues = ref([]);
|
||||
const selectedMultiNames = ref([]);
|
||||
const newMultiOption = ref('');
|
||||
// relation/lookup
|
||||
const relationOptions = ref([]);
|
||||
const editRelationValue = ref(null);
|
||||
const selectedRelationName = ref('');
|
||||
const lookupValues = ref([]);
|
||||
|
||||
const multiRelationOptions = ref([]);
|
||||
const editMultiRelationValues = ref([]);
|
||||
const selectedMultiRelationNames = ref([]);
|
||||
|
||||
const showAddTagInput = ref(false);
|
||||
const newTagName = ref('');
|
||||
|
||||
// Добавляем watch для отслеживания изменений в мультисвязях
|
||||
watch(editMultiRelationValues, (newValues, oldValues) => {
|
||||
console.log('[editMultiRelationValues] changed from:', oldValues, 'to:', newValues);
|
||||
}, { deep: true });
|
||||
|
||||
onMounted(async () => {
|
||||
if (props.column.type === 'tags') {
|
||||
await loadTags();
|
||||
updateSelectedTagNames();
|
||||
if (props.column.type === 'multiselect') {
|
||||
multiOptions.value = (props.column.options && props.column.options.options) || [];
|
||||
const cell = props.cellValues.find(
|
||||
c => c.row_id === props.rowId && c.column_id === props.column.id
|
||||
);
|
||||
let values = [];
|
||||
if (cell && cell.value) {
|
||||
try {
|
||||
values = JSON.parse(cell.value);
|
||||
} catch {}
|
||||
}
|
||||
editMultiValues.value = Array.isArray(values) ? values : [];
|
||||
selectedMultiNames.value = multiOptions.value.filter(opt => editMultiValues.value.includes(opt));
|
||||
} else if (props.column.type === 'relation') {
|
||||
await loadRelationOptions();
|
||||
const cell = props.cellValues.find(
|
||||
c => c.row_id === props.rowId && c.column_id === props.column.id
|
||||
);
|
||||
editRelationValue.value = cell ? cell.value : null;
|
||||
selectedRelationName.value = relationOptions.value.find(opt => String(opt.id) === String(editRelationValue.value))?.display || '';
|
||||
} else if (props.column.type === 'lookup') {
|
||||
await loadLookupValues();
|
||||
} else if (props.column.type === 'multiselect-relation') {
|
||||
await loadMultiRelationOptions();
|
||||
await loadMultiRelationValues();
|
||||
// Инициализация localValue для отображения массива, если нет имен
|
||||
const cell = props.cellValues.find(
|
||||
c => c.row_id === props.rowId && c.column_id === props.column.id
|
||||
);
|
||||
localValue.value = cell ? cell.value : '';
|
||||
} else {
|
||||
const cell = props.cellValues.find(
|
||||
c => c.row_id === props.rowId && c.column_id === props.column.id
|
||||
);
|
||||
localValue.value = cell ? cell.value : '';
|
||||
}
|
||||
});
|
||||
|
||||
async function loadTags() {
|
||||
const res = await fetch('/api/tags');
|
||||
allTags.value = await res.json();
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [props.rowId, props.column.id, props.cellValues],
|
||||
() => {
|
||||
if (props.column.type === 'tags') {
|
||||
// Значение ячейки — строка с JSON-массивом id тегов
|
||||
async () => {
|
||||
if (props.column.type === 'multiselect') {
|
||||
multiOptions.value = (props.column.options && props.column.options.options) || [];
|
||||
const cell = props.cellValues.find(
|
||||
c => c.row_id === props.rowId && c.column_id === props.column.id
|
||||
);
|
||||
let ids = [];
|
||||
let values = [];
|
||||
if (cell && cell.value) {
|
||||
try {
|
||||
ids = JSON.parse(cell.value);
|
||||
values = JSON.parse(cell.value);
|
||||
} catch {}
|
||||
}
|
||||
editTagIds.value = Array.isArray(ids) ? ids : [];
|
||||
updateSelectedTagNames();
|
||||
editMultiValues.value = Array.isArray(values) ? values : [];
|
||||
selectedMultiNames.value = multiOptions.value.filter(opt => editMultiValues.value.includes(opt));
|
||||
} else if (props.column.type === 'relation') {
|
||||
await loadRelationOptions();
|
||||
const cell = props.cellValues.find(
|
||||
c => c.row_id === props.rowId && c.column_id === props.column.id
|
||||
);
|
||||
editRelationValue.value = cell ? cell.value : null;
|
||||
selectedRelationName.value = relationOptions.value.find(opt => String(opt.id) === String(editRelationValue.value))?.display || '';
|
||||
} else if (props.column.type === 'lookup') {
|
||||
await loadLookupValues();
|
||||
} else if (props.column.type === 'multiselect-relation') {
|
||||
await loadMultiRelationOptions();
|
||||
await loadMultiRelationValues();
|
||||
// Инициализация localValue для отображения массива, если нет имен
|
||||
const cell = props.cellValues.find(
|
||||
c => c.row_id === props.rowId && c.column_id === props.column.id
|
||||
);
|
||||
localValue.value = cell ? cell.value : '';
|
||||
} else {
|
||||
const cell = props.cellValues.find(
|
||||
c => c.row_id === props.rowId && c.column_id === props.column.id
|
||||
@@ -77,26 +197,305 @@ watch(
|
||||
{ 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 saveMulti() {
|
||||
emit('update', JSON.stringify(editMultiValues.value));
|
||||
editing.value = false;
|
||||
}
|
||||
function cancelMulti() {
|
||||
editing.value = false;
|
||||
selectedMultiNames.value = multiOptions.value.filter(opt => editMultiValues.value.includes(opt));
|
||||
}
|
||||
|
||||
async function addMultiOption() {
|
||||
const val = newMultiOption.value.trim();
|
||||
if (!val) return;
|
||||
// Если multiselect связан с relation-таблицей (например, Теги клиентов)
|
||||
if (props.column.options && props.column.options.relatedTableId && props.column.options.relatedColumnId) {
|
||||
// 1. Создаём строку в relation-таблице
|
||||
const newRow = await tablesService.addRow(props.column.options.relatedTableId);
|
||||
// 2. Сохраняем значение в нужную ячейку (название тега)
|
||||
await tablesService.saveCell({
|
||||
table_id: props.column.options.relatedTableId,
|
||||
row_id: newRow.id,
|
||||
column_id: props.column.options.relatedColumnId,
|
||||
value: val
|
||||
});
|
||||
// 3. Обновляем multiOptions (заново загружаем из relation-таблицы)
|
||||
const relTable = await tablesService.getTable(props.column.options.relatedTableId);
|
||||
const colId = props.column.options.relatedColumnId;
|
||||
multiOptions.value = relTable.rows.map(row => {
|
||||
const cell = relTable.cellValues.find(c => c.row_id === row.id && c.column_id === colId);
|
||||
return cell ? cell.value : `ID ${row.id}`;
|
||||
});
|
||||
// 4. Добавляем новый тег в выбранные
|
||||
editMultiValues.value.push(val);
|
||||
newMultiOption.value = '';
|
||||
return;
|
||||
}
|
||||
// Обычный multiselect (старый вариант)
|
||||
if (multiOptions.value.includes(val)) return;
|
||||
const updatedOptions = [...multiOptions.value, val];
|
||||
await updateMultiOptionsOnServer(updatedOptions);
|
||||
multiOptions.value = updatedOptions;
|
||||
newMultiOption.value = '';
|
||||
}
|
||||
async function removeMultiOption(option) {
|
||||
const updatedOptions = multiOptions.value.filter(o => o !== option);
|
||||
await updateMultiOptionsOnServer(updatedOptions);
|
||||
multiOptions.value = updatedOptions;
|
||||
// Если удалили выбранное — убираем из выбранных
|
||||
editMultiValues.value = editMultiValues.value.filter(v => v !== option);
|
||||
}
|
||||
async function updateMultiOptionsOnServer(optionsArr) {
|
||||
// PATCH /api/tables/column/:columnId
|
||||
const body = {
|
||||
options: {
|
||||
...(props.column.options || {}),
|
||||
options: optionsArr
|
||||
}
|
||||
};
|
||||
await fetch(`/api/tables/column/${props.column.id}`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
}
|
||||
|
||||
async function loadRelationOptions() {
|
||||
// Получаем данные из связанной таблицы (id и display)
|
||||
const opts = [];
|
||||
try {
|
||||
const rel = props.column.options || {};
|
||||
if (rel.relatedTableId) {
|
||||
const res = await fetch(`/api/tables/${rel.relatedTableId}`);
|
||||
const data = await res.json();
|
||||
const colId = rel.relatedColumnId || (data.columns[0] && data.columns[0].id);
|
||||
for (const row of data.rows) {
|
||||
const cell = data.cellValues.find(c => c.row_id === row.id && c.column_id === colId);
|
||||
opts.push({ id: row.id, display: cell ? cell.value : `ID ${row.id}` });
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
relationOptions.value = opts;
|
||||
}
|
||||
function saveRelation() {
|
||||
emit('update', editRelationValue.value);
|
||||
editing.value = false;
|
||||
selectedRelationName.value = relationOptions.value.find(opt => String(opt.id) === String(editRelationValue.value))?.display || '';
|
||||
}
|
||||
function cancelRelation() {
|
||||
editing.value = false;
|
||||
}
|
||||
|
||||
async function loadLookupValues() {
|
||||
// Получаем связанные rowId через relation-таблицу
|
||||
lookupValues.value = [];
|
||||
try {
|
||||
const rel = props.column.options || {};
|
||||
if (rel.relatedTableId && rel.relatedColumnId) {
|
||||
// Получаем связи для текущей строки
|
||||
const res = await fetch(`/api/tables/${props.column.table_id}/row/${props.rowId}/relations`);
|
||||
const relations = await res.json();
|
||||
// Фильтруем по нужному столбцу relation
|
||||
const relatedRowIds = relations
|
||||
.filter(r => String(r.column_id) === String(props.column.id) && String(r.to_table_id) === String(rel.relatedTableId))
|
||||
.map(r => r.to_row_id);
|
||||
if (relatedRowIds.length) {
|
||||
// Получаем значения из связанной таблицы
|
||||
const relTable = await fetch(`/api/tables/${rel.relatedTableId}`);
|
||||
const relData = await relTable.json();
|
||||
lookupValues.value = relatedRowIds.map(rowId => {
|
||||
const cell = relData.cellValues.find(c => c.row_id === rowId && c.column_id === rel.relatedColumnId);
|
||||
return cell ? cell.value : `ID ${rowId}`;
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
async function loadMultiRelationOptions() {
|
||||
const rel = props.column.options || {};
|
||||
if (!rel.relatedTableId) return;
|
||||
const res = await fetch(`/api/tables/${rel.relatedTableId}`);
|
||||
const data = await res.json();
|
||||
// Далее используем data.columns, data.rows, data.cellValues
|
||||
const colId = rel.relatedColumnId || (data.columns[0] && data.columns[0].id);
|
||||
const opts = [];
|
||||
for (const row of data.rows) {
|
||||
const cell = data.cellValues.find(c => c.row_id === row.id && c.column_id === colId);
|
||||
opts.push({ id: row.id, display: cell ? cell.value : `ID ${row.id}` });
|
||||
}
|
||||
multiRelationOptions.value = opts;
|
||||
}
|
||||
|
||||
async function loadMultiRelationValues() {
|
||||
// Получаем связи для текущей строки
|
||||
console.log('[loadMultiRelationValues] called for row:', props.rowId, 'column:', props.column.id);
|
||||
editMultiRelationValues.value = [];
|
||||
selectedMultiRelationNames.value = [];
|
||||
try {
|
||||
const rel = props.column.options || {};
|
||||
if (rel.relatedTableId && rel.relatedColumnId) {
|
||||
const url = `/api/tables/${props.column.table_id}/row/${props.rowId}/relations`;
|
||||
console.log('[loadMultiRelationValues] GET request to:', url);
|
||||
const res = await fetch(url);
|
||||
const relations = await res.json();
|
||||
console.log('[loadMultiRelationValues] API response status:', res.status, 'relations:', relations);
|
||||
// Приводим все id к строке для корректного сравнения
|
||||
const relatedRowIds = relations
|
||||
.filter(r => String(r.column_id) === String(props.column.id) && String(r.to_table_id) === String(rel.relatedTableId))
|
||||
.map(r => String(r.to_row_id));
|
||||
console.log('[loadMultiRelationValues] filtered related row ids:', relatedRowIds);
|
||||
editMultiRelationValues.value = relatedRowIds;
|
||||
// Получаем display-значения
|
||||
await loadMultiRelationOptions();
|
||||
selectedMultiRelationNames.value = multiRelationOptions.value
|
||||
.filter(opt => relatedRowIds.includes(String(opt.id)))
|
||||
.map(opt => opt.display);
|
||||
console.log('[loadMultiRelationValues] selectedMultiRelationNames:', selectedMultiRelationNames.value);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[loadMultiRelationValues] Error:', e);
|
||||
}
|
||||
}
|
||||
|
||||
function saveTags() {
|
||||
emit('update', JSON.stringify(editTagIds.value));
|
||||
editing.value = false;
|
||||
async function saveMultiRelation() {
|
||||
console.log('[saveMultiRelation] called');
|
||||
const rel = props.column.options || {};
|
||||
console.log('[saveMultiRelation] editMultiRelationValues:', editMultiRelationValues.value);
|
||||
try {
|
||||
const payload = {
|
||||
column_id: props.column.id,
|
||||
to_table_id: rel.relatedTableId,
|
||||
to_row_ids: editMultiRelationValues.value
|
||||
};
|
||||
console.log('[saveMultiRelation] POST payload:', payload);
|
||||
const response = await fetch(`/api/tables/${props.column.table_id}/row/${props.rowId}/multirelations`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
const result = await response.json().catch(() => ({}));
|
||||
console.log('[saveMultiRelation] API response status:', response.status, 'result:', result);
|
||||
editing.value = false;
|
||||
await loadMultiRelationValues();
|
||||
console.log('[saveMultiRelation] emitting update with:', editMultiRelationValues.value);
|
||||
emit('update', editMultiRelationValues.value);
|
||||
} catch (e) {
|
||||
console.error('[saveMultiRelation] Ошибка при сохранении мультисвязи:', e);
|
||||
}
|
||||
}
|
||||
function cancelTags() {
|
||||
|
||||
async function addTag() {
|
||||
if (!newTagName.value.trim()) return;
|
||||
const rel = props.column.options || {};
|
||||
|
||||
try {
|
||||
console.log('[addTag] Добавляем новый тег:', newTagName.value);
|
||||
|
||||
// 1. Создаем новую пустую строку в связанной таблице
|
||||
const rowResponse = await fetch(`/api/tables/${rel.relatedTableId}/rows`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
const newRow = await rowResponse.json();
|
||||
|
||||
console.log('[addTag] Новая строка создана:', newRow);
|
||||
|
||||
// 2. Добавляем значение в ячейку через POST /cell
|
||||
const cellResponse = await fetch(`/api/tables/cell`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
row_id: newRow.id,
|
||||
column_id: rel.relatedColumnId,
|
||||
value: newTagName.value
|
||||
})
|
||||
});
|
||||
const cellResult = await cellResponse.json();
|
||||
|
||||
console.log('[addTag] Значение ячейки сохранено:', cellResult);
|
||||
|
||||
// Очищаем форму
|
||||
newTagName.value = '';
|
||||
showAddTagInput.value = false;
|
||||
|
||||
// Обновляем список опций
|
||||
await loadMultiRelationOptions();
|
||||
|
||||
// Автоматически добавляем новый тег в выбранные
|
||||
editMultiRelationValues.value.push(String(newRow.id));
|
||||
console.log('[addTag] Тег добавлен в выбранные:', editMultiRelationValues.value);
|
||||
} catch (e) {
|
||||
console.error('[addTag] Ошибка при добавлении тега:', e);
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteTag(tagId) {
|
||||
const rel = props.column.options || {};
|
||||
if (!confirm('Удалить этот тег?')) return;
|
||||
|
||||
try {
|
||||
console.log('[deleteTag] Удаляем тег с ID:', tagId);
|
||||
|
||||
// Удаляем тег из связанной таблицы
|
||||
const response = await fetch(`/api/tables/row/${tagId}`, { method: 'DELETE' });
|
||||
const result = await response.json();
|
||||
|
||||
console.log('[deleteTag] Ответ сервера:', response.status, result);
|
||||
|
||||
// Убираем тег из выбранных значений, если он был выбран
|
||||
editMultiRelationValues.value = editMultiRelationValues.value.filter(id => String(id) !== String(tagId));
|
||||
|
||||
// Обновляем список опций
|
||||
await loadMultiRelationOptions();
|
||||
|
||||
console.log('[deleteTag] Тег удален:', tagId);
|
||||
} catch (e) {
|
||||
console.error('[deleteTag] Ошибка при удалении тега:', e);
|
||||
}
|
||||
}
|
||||
|
||||
function cancelMultiRelation() {
|
||||
// Сбрасываем форму добавления тега
|
||||
showAddTagInput.value = false;
|
||||
newTagName.value = '';
|
||||
|
||||
// Закрываем режим редактирования
|
||||
editing.value = false;
|
||||
updateSelectedTagNames();
|
||||
|
||||
// Перезагружаем исходные значения
|
||||
loadMultiRelationValues();
|
||||
}
|
||||
|
||||
function save() {
|
||||
emit('update', localValue.value);
|
||||
}
|
||||
|
||||
function isArrayString(val) {
|
||||
if (typeof val !== 'string') return false;
|
||||
try {
|
||||
const arr = JSON.parse(val);
|
||||
return Array.isArray(arr);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
function parseArrayString(val) {
|
||||
try {
|
||||
const arr = JSON.parse(val);
|
||||
return Array.isArray(arr) ? arr : [val];
|
||||
} catch {
|
||||
return [val];
|
||||
}
|
||||
}
|
||||
|
||||
function prettyDisplay(val) {
|
||||
if (isArrayString(val)) {
|
||||
return parseArrayString(val).join(', ');
|
||||
}
|
||||
return val;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -161,4 +560,150 @@ function save() {
|
||||
.cancel-btn:hover {
|
||||
background: #d5d5d5;
|
||||
}
|
||||
.add-multiselect-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5em;
|
||||
margin-bottom: 0.7em;
|
||||
}
|
||||
.add-multiselect-option input {
|
||||
flex: 1;
|
||||
padding: 0.2em 0.5em;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 5px;
|
||||
font-size: 1em;
|
||||
}
|
||||
.add-btn {
|
||||
background: #2ecc40;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 0.3em 1em;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
.add-btn:hover {
|
||||
background: #27ae38;
|
||||
}
|
||||
.remove-option {
|
||||
color: #e74c3c;
|
||||
font-size: 1.1em;
|
||||
margin-left: 0.4em;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
.remove-option:hover {
|
||||
color: #c0392b;
|
||||
}
|
||||
.lookup-cell-view {
|
||||
min-height: 1.7em;
|
||||
padding: 0.2em 0.1em;
|
||||
color: #222;
|
||||
}
|
||||
.multi-relation-edit {
|
||||
padding: 8px 0;
|
||||
}
|
||||
.multi-relation-options {
|
||||
max-height: 180px;
|
||||
overflow-y: auto;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.multi-relation-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
padding: 2px 0;
|
||||
}
|
||||
.delete-tag-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #e53e3e;
|
||||
font-size: 1.1em;
|
||||
cursor: pointer;
|
||||
padding: 0 4px;
|
||||
}
|
||||
.multi-relation-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.save-btn {
|
||||
background: #4f8cff;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
padding: 4px 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.cancel-btn {
|
||||
background: #f3f4f6;
|
||||
color: #333;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
padding: 4px 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.add-tag-block {
|
||||
margin-top: 8px;
|
||||
}
|
||||
.add-tag-btn {
|
||||
background: #f3f4f6;
|
||||
color: #4f8cff;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
padding: 4px 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.add-tag-form {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
}
|
||||
.add-tag-form input {
|
||||
padding: 3px 8px;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.add-tag-confirm {
|
||||
background: #4f8cff;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
padding: 3px 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.add-tag-cancel {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #e53e3e;
|
||||
font-size: 1.1em;
|
||||
cursor: pointer;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: 0.5em;
|
||||
margin-top: 0.7em;
|
||||
}
|
||||
|
||||
.delete-tag-btn:hover {
|
||||
color: #c0392b;
|
||||
}
|
||||
|
||||
.add-tag-btn:hover {
|
||||
background: #e2e8f0;
|
||||
color: #3182ce;
|
||||
}
|
||||
|
||||
.add-tag-confirm:hover {
|
||||
background: #3182ce;
|
||||
}
|
||||
|
||||
.add-tag-block {
|
||||
margin: 0.7em 0;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user