ваше сообщение коммита

This commit is contained in:
2025-07-15 14:14:53 +03:00
parent 584ff401ad
commit d952e89a26
10 changed files with 1109 additions and 124 deletions

View File

@@ -2,7 +2,13 @@
<template v-if="column.type === 'multiselect'">
<div v-if="!editing" @click="editing = true" class="tags-cell-view">
<span v-if="selectedMultiNames.length">{{ selectedMultiNames.join(', ') }}</span>
<span v-else style="color:#bbb"></span>
<span v-else class="cell-plus-icon" title="Добавить">
<svg width="18" height="18" viewBox="0 0 18 18" fill="none">
<circle cx="9" cy="9" r="8" fill="#f3f4f6" stroke="#b6c6e6"/>
<rect x="8" y="4" width="2" height="10" rx="1" fill="#4f8cff"/>
<rect x="4" y="8" width="10" height="2" rx="1" fill="#4f8cff"/>
</svg>
</span>
</div>
<div v-else class="tags-cell-edit">
<div class="tags-multiselect">
@@ -23,7 +29,13 @@
<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>
<span v-else class="cell-plus-icon" title="Добавить">
<svg width="18" height="18" viewBox="0 0 18 18" fill="none">
<circle cx="9" cy="9" r="8" fill="#f3f4f6" stroke="#b6c6e6"/>
<rect x="8" y="4" width="2" height="10" rx="1" fill="#4f8cff"/>
<rect x="4" y="8" width="10" height="2" rx="1" fill="#4f8cff"/>
</svg>
</span>
</div>
<div v-else class="tags-cell-edit">
<select v-model="editRelationValue" class="notion-input">
@@ -42,13 +54,19 @@
<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>
<span v-else class="cell-plus-icon" title="Добавить">
<svg width="18" height="18" viewBox="0 0 18 18" fill="none">
<circle cx="9" cy="9" r="8" fill="#f3f4f6" stroke="#b6c6e6"/>
<rect x="8" y="4" width="2" height="10" rx="1" fill="#4f8cff"/>
<rect x="4" y="8" width="10" height="2" rx="1" fill="#4f8cff"/>
</svg>
</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>
<label :for="'cell-multirel-' + option.id + '-' + rowId">{{ prettyDisplay(option.display, multiRelationOptions.value) }}</label>
<button class="delete-tag-btn" @click.prevent="deleteTag(option.id)" title="Удалить тег">×</button>
</div>
</div>
@@ -67,20 +85,33 @@
</div>
</template>
<template v-else>
<div v-if="!editing" class="cell-view-value" @click="editing = true">
<span v-if="isArrayString(localValue)">{{ parseArrayString(localValue).join(', ') }}</span>
<input
<span v-else-if="localValue">{{ localValue }}</span>
<span v-else class="cell-plus-icon" title="Добавить">
<svg width="18" height="18" viewBox="0 0 18 18" fill="none">
<circle cx="9" cy="9" r="8" fill="#f3f4f6" stroke="#b6c6e6"/>
<rect x="8" y="4" width="2" height="10" rx="1" fill="#4f8cff"/>
<rect x="4" y="8" width="10" height="2" rx="1" fill="#4f8cff"/>
</svg>
</span>
</div>
<textarea
v-else
v-model="localValue"
@blur="save"
@keyup.enter="save"
@blur="saveAndExit"
@keyup.enter="saveAndExit"
:placeholder="column.name"
class="cell-input"
autofocus
ref="textareaRef"
@input="autoResize"
/>
</template>
</template>
<script setup>
import { ref, watch, onMounted, computed } from 'vue';
import { ref, watch, onMounted, computed, nextTick } from 'vue';
import tablesService from '../../services/tablesService';
const props = defineProps(['rowId', 'column', 'cellValues']);
const emit = defineEmits(['update']);
@@ -110,6 +141,27 @@ const selectedMultiRelationNames = ref([]);
const showAddTagInput = ref(false);
const newTagName = ref('');
const textareaRef = ref(null);
function autoResize() {
const ta = textareaRef.value;
if (ta) {
ta.style.height = 'auto';
ta.style.height = ta.scrollHeight + 'px';
}
}
watch(editing, (val) => {
if (val) {
nextTick(() => {
if (textareaRef.value) {
autoResize();
setTimeout(() => autoResize(), 0);
}
});
}
});
// Добавляем watch для отслеживания изменений в мультисвязях
watch(editMultiRelationValues, (newValues, oldValues) => {
console.log('[editMultiRelationValues] changed from:', oldValues, 'to:', newValues);
@@ -472,6 +524,11 @@ function save() {
emit('update', localValue.value);
}
function saveAndExit() {
save();
editing.value = false;
}
function isArrayString(val) {
if (typeof val !== 'string') return false;
try {
@@ -482,36 +539,62 @@ function isArrayString(val) {
}
}
function parseArrayString(val) {
if (typeof val !== 'string') return [];
// Пробуем как JSON
try {
const arr = JSON.parse(val);
return Array.isArray(arr) ? arr : [val];
} catch {
return [val];
if (Array.isArray(arr)) return arr.map(String);
} catch {}
// Пробуем как PostgreSQL-массив
if (/^\{.*\}$/.test(val)) {
return val.replace(/[{}\s"]/g, '').split(',').filter(Boolean);
}
// Если просто строка
if (val.trim().length > 0) return [val.trim()];
return [];
}
function prettyDisplay(val) {
if (isArrayString(val)) {
return parseArrayString(val).join(', ');
function prettyDisplay(val, optionsArr) {
const arr = parseArrayString(val);
if (!arr.length) return '—';
if (optionsArr && Array.isArray(optionsArr)) {
// Для relation/multiselect-relation ищу display по id
return arr.map(id => {
const found = optionsArr.find(opt => String(opt.id) === String(id) || String(opt) === String(id));
return found ? (found.display || found) : id;
}).join(', ');
}
return val;
return arr.join(', ');
}
</script>
<style scoped>
.cell-input {
width: 100%;
border: 1px solid #e0e0e0;
border-radius: 5px;
padding: 0.3em 0.5em;
font-size: 1em;
background: #fff;
transition: border 0.2s;
border: none !important;
outline: none !important;
background: transparent !important;
box-shadow: none !important;
padding: 0 !important;
resize: none !important;
width: 100% !important;
min-height: 32px;
font: inherit;
color: inherit;
overflow: hidden;
}
.cell-input:focus {
border: 1.5px solid #2ecc40;
outline: none;
}
.tags-cell-view, .tags-cell-edit, .lookup-cell-view, .tag-option, .multi-relation-option, .add-multiselect-option, .add-tag-form, .multi-relation-options, .multi-relation-edit, .multi-relation-actions, .action-buttons {
white-space: normal !important;
word-break: break-word !important;
height: auto !important;
min-height: 1.7em;
align-items: flex-start !important;
vertical-align: top !important;
overflow: visible !important;
}
.tags-cell-view {
min-height: 1.7em;
cursor: pointer;
@@ -706,4 +789,30 @@ function prettyDisplay(val) {
.add-tag-block {
margin: 0.7em 0;
}
.cell-view-value {
display: block;
white-space: pre-wrap !important;
word-break: break-word !important;
overflow-wrap: anywhere !important;
width: 100%;
cursor: pointer;
transition: background 0.15s;
min-height: 32px;
}
.cell-view-value:hover {
background: #f3f4f6;
}
.cell-plus-icon {
color: #b6c6e6;
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 1.2em;
transition: color 0.15s;
vertical-align: middle;
}
.cell-plus-icon:hover {
color: #4f8cff;
}
</style>

View File

@@ -2,76 +2,110 @@
<div class="user-table-header" v-if="tableMeta">
<h2>{{ tableMeta.name }}</h2>
<div class="table-desc">{{ tableMeta.description }}</div>
<button v-if="isAdmin" class="rebuild-btn" @click="rebuildIndex" :disabled="rebuilding">
{{ rebuilding ? 'Пересборка...' : 'Пересобрать индекс' }}
</button>
<div class="table-header-actions" style="display: flex; align-items: center; gap: 12px; flex-wrap: wrap; margin-top: 8px; margin-bottom: 18px;">
<el-button type="danger" :disabled="!selectedRows.length" @click="deleteSelectedRows">Удалить выбранные</el-button>
<span v-if="selectedRows.length">Выбрано: {{ selectedRows.length }}</span>
<button v-if="isAdmin" class="rebuild-btn" @click="rebuildIndex" :disabled="rebuilding">
{{ rebuilding ? 'Пересборка...' : 'Пересобрать индекс' }}
</button>
<el-button @click="resetFilters" type="default" icon="el-icon-refresh">Сбросить фильтры</el-button>
<template v-for="def in relationFilterDefs" :key="def.col.id">
<el-select
v-model="relationFilters[def.filterKey]"
:multiple="def.isMulti"
filterable
clearable
:placeholder="def.col.name"
style="min-width: 180px;"
>
<el-option v-for="opt in def.options" :key="opt.id" :label="opt.display" :value="opt.id" />
</el-select>
</template>
</div>
<span v-if="rebuildStatus" :class="['rebuild-status', rebuildStatus.success ? 'success' : 'error']">
{{ rebuildStatus.message }}
</span>
</div>
<!-- Фильтры на Element Plus -->
<div class="table-filters-el" v-if="relationFilterDefs.length">
<!-- Только фильтры по multiselect-relation -->
<template v-for="def in relationFilterDefs" :key="def.col.id">
<el-select
v-model="relationFilters[def.filterKey]"
:multiple="def.isMulti"
filterable
clearable
:placeholder="def.col.name"
style="min-width: 180px;"
>
<el-option v-for="opt in def.options" :key="opt.id" :label="opt.display" :value="opt.id" />
</el-select>
</template>
<el-button @click="resetFilters" type="default" icon="el-icon-refresh">Сбросить фильтры</el-button>
</div>
<!-- Удаляю .table-filters-el -->
<div class="notion-table-wrapper">
<table class="notion-table">
<thead>
<tr>
<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" />
<el-table
:data="filteredRows"
border
style="width: 100%"
:header-cell-style="{ background: '#f3f4f6', fontWeight: 600 }"
:cell-style="{ whiteSpace: 'normal', wordBreak: 'break-word', minWidth: '80px' }"
:row-class-name="() => 'el-table-row-custom'"
row-key="id"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="48" fixed="left" />
<el-table-column
v-for="col in columns"
:key="col.id"
:prop="'col_' + col.id"
:label="col.name"
:resizable="true"
:min-width="120"
:show-overflow-tooltip="false"
>
<template #header="{ column }">
<template v-if="editingCol && editingCol.id === col.id">
<input v-model="colEditValue" class="notion-input" style="width: 90px; display: inline-block;" @keyup.enter="saveColEdit(col)" />
<button class="save-btn" @click="saveColEdit(col)">Сохранить</button>
<button class="cancel-btn" @click="cancelColEdit">Отмена</button>
</template>
<template v-else>
<span>{{ col.name }}</span>
<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="showAddColModal = true">+</button>
</th>
</tr>
</thead>
<tbody>
<tr v-for="row in filteredRows" :key="row.id">
<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, $event)"></button>
<!-- Меню строки -->
</template>
</template>
<template #default="{ row }">
<TableCell
:rowId="row.id"
:column="col"
:cellValues="cellValues"
@update="val => saveCellValue(row.id, col.id, val)"
/>
</template>
</el-table-column>
<!-- Было два столбца: один для плюса, один для ⋮. Теперь объединяем: -->
<el-table-column
label=""
width="48"
align="center"
fixed="right"
class-name="add-col-header"
:resizable="false"
>
<template #header>
<button class="add-col-btn" @click="addColumn" title="Добавить столбец">
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="11" cy="11" r="10" fill="#f3f4f6" stroke="#b6c6e6"/>
<rect x="10" y="5.5" width="2" height="11" rx="1" fill="#4f8cff"/>
<rect x="5.5" y="10" width="11" height="2" rx="1" fill="#4f8cff"/>
</svg>
</button>
</template>
<template #default="{ row }">
<button class="row-menu" @click.stop="openRowMenu(row, $event)"></button>
<teleport to="body">
<div v-if="openedRowMenuId === row.id" class="context-menu" :style="rowMenuStyle">
<button class="menu-item" @click="addRowAfter(row)">Добавить строку</button>
<button class="menu-item" @click="moveRowUp(row)" :disabled="rows.findIndex(r => r.id === row.id) === 0">Переместить вверх</button>
<button class="menu-item" @click="moveRowDown(row)" :disabled="rows.findIndex(r => r.id === row.id) === rows.length - 1">Переместить вниз</button>
<button class="menu-item danger" @click="deleteRow(row)">Удалить</button>
</div>
</td>
</tr>
<tr>
<td :colspan="columns.length + 1">
<button class="add-row" @click="addRow">+ Добавить строку</button>
</td>
</tr>
</tbody>
</table>
<!-- Оверлей для закрытия меню по клику вне -->
</teleport>
</template>
</el-table-column>
</el-table>
<teleport to="body">
<div v-if="openedColMenuId" class="context-menu" :style="colMenuStyle">
<button class="menu-item" @click="editColumn(columns.find(c => c.id === openedColMenuId))">Редактировать</button>
<button class="menu-item danger" @click="deleteColumn(columns.find(c => c.id === openedColMenuId))">Удалить</button>
<!-- <button class="menu-item" @click="addColumn">Добавить столбец</button> -->
</div>
</teleport>
<div v-if="openedColMenuId || openedRowMenuId" class="menu-overlay" @click="closeMenus"></div>
<!-- Модалка добавления столбца -->
<div v-if="showAddColModal" class="modal-backdrop">
@@ -136,6 +170,19 @@ const tableMeta = ref(null);
// const selectedProduct = ref('');
// const productOptions = ref([]);
const filteredRows = ref([]);
const selectedRows = ref([]);
function handleSelectionChange(val) {
selectedRows.value = val;
}
async function deleteSelectedRows() {
if (!selectedRows.value.length) return;
if (!confirm(`Удалить выбранные строки (${selectedRows.value.length})?`)) return;
for (const row of selectedRows.value) {
await tablesService.deleteRow(row.id);
}
selectedRows.value = [];
await fetchTable();
}
// Для модалки добавления столбца
const showAddColModal = ref(false);
@@ -244,6 +291,7 @@ async function handleAddColumn() {
closeAddColModal();
await fetchTable();
await updateRelationFilterDefs(); // Явно обновляем фильтры
window.dispatchEvent(new Event('placeholders-updated'));
}
async function deleteColumn(col) {
@@ -252,6 +300,7 @@ async function deleteColumn(col) {
await tablesService.deleteColumn(col.id);
await fetchTable();
await updateRelationFilterDefs(); // Явно обновляем фильтры
window.dispatchEvent(new Event('placeholders-updated'));
}
// Удаляю все переменные, функции и UI, связанные с tags, tagOptions, selectedTags, loadTags, updateFilterOptions с tags, и т.д.
@@ -411,6 +460,9 @@ function addColumn() {
function addRow() {
tablesService.addRow(props.tableId).then(fetchTable);
}
function addRowAfter(row) {
tablesService.addRow(props.tableId, row.id).then(fetchTable);
}
function openColMenu(col, event) {
openedColMenuId.value = col.id;
openedRowMenuId.value = null;
@@ -438,6 +490,33 @@ async function deleteRow(row) {
await fetchTable();
}
async function saveRowsOrder() {
// Сохраняем новый порядок строк на сервере
const orderArr = rows.value.map((row, idx) => ({ rowId: row.id, order: idx + 1 }));
await tablesService.updateRowsOrder(props.tableId, orderArr);
}
function moveRowUp(row) {
const idx = rows.value.findIndex(r => r.id === row.id);
if (idx > 0) {
const temp = rows.value[idx - 1];
rows.value[idx - 1] = rows.value[idx];
rows.value[idx] = temp;
saveRowsOrder();
fetchTable();
}
}
function moveRowDown(row) {
const idx = rows.value.findIndex(r => r.id === row.id);
if (idx < rows.value.length - 1) {
const temp = rows.value[idx + 1];
rows.value[idx + 1] = rows.value[idx];
rows.value[idx] = temp;
saveRowsOrder();
fetchTable();
}
}
async function rebuildIndex() {
rebuilding.value = true;
rebuildStatus.value = null;
@@ -458,12 +537,12 @@ async function rebuildIndex() {
<style scoped>
.user-table-header {
max-width: 1100px;
margin: 32px auto 0 auto;
padding: 32px 24px 18px 24px;
background: #fff;
border-radius: 18px;
box-shadow: 0 4px 24px rgba(0,0,0,0.08);
/* max-width: 1100px; */
margin: 0 auto 0 auto;
/* padding: 32px 24px 18px 24px; */
/* background: #fff; */
/* border-radius: 18px; */
/* box-shadow: 0 4px 24px rgba(0,0,0,0.08); */
display: flex;
flex-direction: column;
gap: 8px;
@@ -515,41 +594,24 @@ async function rebuildIndex() {
}
.notion-table-wrapper {
max-width: 1100px;
margin: 24px auto 0 auto;
background: #fff;
border-radius: 6px;
box-shadow: 0 1px 4px rgba(0,0,0,0.04);
padding: 12px 6px 18px 6px;
/* max-width: 1100px; */
margin: 0 auto 0 auto;
/* background: #fff; */
/* border-radius: 6px; */
/* box-shadow: 0 1px 4px rgba(0,0,0,0.04); */
/* padding: 12px 6px 18px 6px; */
}
.notion-table {
width: 100%;
border-collapse: collapse;
font-size: 0.98rem;
background: #fff;
}
.notion-table th, .notion-table td {
border: 1px solid #e5e7eb;
padding: 6px 10px;
text-align: left;
background: #fff;
font-size: 0.98rem;
.el-table__cell, .el-table th, .el-table td {
height: auto !important;
min-height: 36px;
white-space: normal !important;
word-break: break-word !important;
min-width: 80px;
max-width: 600px;
}
.notion-table th {
background: #f3f4f6;
font-weight: 600;
border-bottom: 2px solid #d1d5db;
border-top: 1px solid #e5e7eb;
padding-top: 7px;
padding-bottom: 7px;
}
.notion-table tr:hover td {
background: #f5f7fa;
.el-table-row-custom {
/* Можно добавить стили для высоты строк, если нужно */
}
.notion-input {
@@ -602,7 +664,7 @@ async function rebuildIndex() {
.context-menu {
position: absolute;
z-index: 10;
z-index: 2000;
min-width: 120px;
background: #fff;
border: 1px solid #e5e7eb;
@@ -691,6 +753,25 @@ async function rebuildIndex() {
font-style: italic;
}
.add-col-header .add-col-btn {
background: none;
border: none;
padding: 0;
margin: 0 auto;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: background 0.18s;
}
.add-col-header .add-col-btn:hover svg circle {
fill: #e5e7eb;
stroke: #4f8cff;
}
.add-col-header .add-col-btn:active svg circle {
fill: #dbeafe;
}
@media (max-width: 700px) {
.notion-table-wrapper, .table-filters-el {
padding: 4px 1vw;

View File

@@ -63,5 +63,10 @@ export default {
async rebuildIndex(tableId) {
const res = await api.post(`/tables/${tableId}/rebuild-index`);
return res.data;
},
async updateRowsOrder(tableId, orderArr) {
// orderArr: [{rowId, order}, ...]
const res = await api.patch(`/tables/${tableId}/rows/order`, { order: orderArr });
return res.data;
}
};

View File

@@ -2,13 +2,98 @@
<BaseLayout>
<div class="content-page-block">
<h2>Контент</h2>
<p>Здесь будет размещён контент.</p>
<form class="content-form" @submit.prevent>
<div class="form-group">
<label for="title">Заголовок страницы *</label>
<input v-model="form.title" id="title" type="text" required />
</div>
<div class="form-group">
<label for="summary">Краткое описание *</label>
<textarea v-model="form.summary" id="summary" required rows="2" />
</div>
<div class="form-group">
<label for="content">Основной контент *</label>
<textarea v-model="form.content" id="content" required rows="6" />
</div>
<div class="form-group">
<label for="image">Изображение/обложка</label>
<input v-model="form.image" id="image" type="text" placeholder="URL или имя файла" />
</div>
<div class="form-group">
<label for="tags">Теги</label>
<div class="tags-input">
<input
v-model="tagInput"
@keydown.enter.prevent="addTag"
@blur="addTag"
placeholder="Введите тег и нажмите Enter"
/>
<div class="tags-list">
<span v-for="(tag, idx) in form.tags" :key="tag" class="tag">
{{ tag }}
<button type="button" @click="removeTag(idx)">&times;</button>
</span>
</div>
</div>
</div>
<div class="form-group">
<label for="category">Категория</label>
<select v-model="form.category" id="category">
<option value="">Не выбрано</option>
<option value="О компании">О компании</option>
<option value="Продукты">Продукты</option>
<option value="Блог">Блог</option>
<option value="FAQ">FAQ</option>
</select>
</div>
<div class="form-group">
<label for="addToChat">Добавить в чат</label>
<select v-model="form.addToChat" id="addToChat">
<option value="yes">Да</option>
<option value="no">Нет</option>
</select>
</div>
<div class="form-group">
<label for="rag">Интегрировать с RAG</label>
<select v-model="form.rag" id="rag">
<option value="yes">Да</option>
<option value="no">Нет</option>
</select>
</div>
<button class="submit-btn" type="submit">Сохранить</button>
</form>
</div>
</BaseLayout>
</template>
<script setup>
import { ref } from 'vue';
import BaseLayout from '../components/BaseLayout.vue';
const form = ref({
title: '',
summary: '',
content: '',
image: '',
tags: [],
category: '',
addToChat: 'yes',
rag: 'yes',
});
const tagInput = ref('');
function addTag() {
const tag = tagInput.value.trim();
if (tag && !form.value.tags.includes(tag)) {
form.value.tags.push(tag);
}
tagInput.value = '';
}
function removeTag(idx) {
form.value.tags.splice(idx, 1);
}
</script>
<style scoped>
@@ -22,4 +107,62 @@ import BaseLayout from '../components/BaseLayout.vue';
position: relative;
overflow-x: auto;
}
.content-form {
display: flex;
flex-direction: column;
gap: 18px;
margin-top: 24px;
}
.form-group {
display: flex;
flex-direction: column;
gap: 6px;
}
input[type="text"], textarea, select {
border: 1px solid #d0d0d0;
border-radius: 6px;
padding: 8px 10px;
font-size: 1rem;
width: 100%;
}
.tags-input {
display: flex;
flex-direction: column;
gap: 6px;
}
.tags-list {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.tag {
background: #f0f0f0;
border-radius: 4px;
padding: 2px 8px;
display: flex;
align-items: center;
font-size: 0.95em;
}
.tag button {
background: none;
border: none;
color: #888;
margin-left: 4px;
cursor: pointer;
font-size: 1.1em;
}
.submit-btn {
background: #2d72d9;
color: #fff;
border: none;
border-radius: 6px;
padding: 10px 0;
font-size: 1.1em;
cursor: pointer;
margin-top: 12px;
transition: background 0.2s;
}
.submit-btn:hover {
background: #1a4e96;
}
</style>

View File

@@ -101,7 +101,7 @@
<script setup>
import BaseLayout from '@/components/BaseLayout.vue';
import { useRouter } from 'vue-router';
import { ref, onMounted, computed, watch } from 'vue';
import { ref, onMounted, computed, watch, onBeforeUnmount } from 'vue';
import axios from 'axios';
import RuleEditor from '@/components/ai-assistant/RuleEditor.vue';
import SystemMonitoring from '@/components/ai-assistant/SystemMonitoring.vue';
@@ -184,6 +184,12 @@ onMounted(() => {
loadLLMModels();
loadEmbeddingModels();
loadPlaceholders();
// Подписка на глобальное событие обновления плейсхолдеров
window.addEventListener('placeholders-updated', loadPlaceholders);
});
onBeforeUnmount(() => {
window.removeEventListener('placeholders-updated', loadPlaceholders);
});
async function saveSettings() {
await axios.put('/settings/ai-assistant', settings.value);