ваше сообщение коммита
This commit is contained in:
@@ -163,8 +163,8 @@ router.post('/verify', async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Создаем SIWE сообщение для проверки подписи
|
// Создаем SIWE сообщение для проверки подписи
|
||||||
const domain = 'localhost:5173'; // Используем тот же домен, что и на frontend
|
|
||||||
const origin = req.get('origin') || 'http://localhost:5173';
|
const origin = req.get('origin') || 'http://localhost:5173';
|
||||||
|
const domain = new URL(origin).host; // Извлекаем домен из origin
|
||||||
|
|
||||||
const { SiweMessage } = require('siwe');
|
const { SiweMessage } = require('siwe');
|
||||||
const message = new SiweMessage({
|
const message = new SiweMessage({
|
||||||
@@ -184,6 +184,9 @@ router.post('/verify', async (req, res) => {
|
|||||||
logger.info(`[verify] SIWE message for verification: ${messageToSign}`);
|
logger.info(`[verify] SIWE message for verification: ${messageToSign}`);
|
||||||
logger.info(`[verify] Domain: ${domain}, Origin: ${origin}`);
|
logger.info(`[verify] Domain: ${domain}, Origin: ${origin}`);
|
||||||
logger.info(`[verify] Normalized address: ${normalizedAddress}`);
|
logger.info(`[verify] Normalized address: ${normalizedAddress}`);
|
||||||
|
logger.info(`[verify] Request headers origin: ${req.get('origin')}`);
|
||||||
|
logger.info(`[verify] Request headers host: ${req.get('host')}`);
|
||||||
|
logger.info(`[verify] Request headers referer: ${req.get('referer')}`);
|
||||||
|
|
||||||
// Проверяем подпись
|
// Проверяем подпись
|
||||||
const isValid = await authService.verifySignature(messageToSign, signature, normalizedAddress);
|
const isValid = await authService.verifySignature(messageToSign, signature, normalizedAddress);
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ const router = express.Router();
|
|||||||
const { requireAdmin } = require('../middleware/auth');
|
const { requireAdmin } = require('../middleware/auth');
|
||||||
const logger = require('../utils/logger');
|
const logger = require('../utils/logger');
|
||||||
const { ethers } = require('ethers');
|
const { ethers } = require('ethers');
|
||||||
|
const db = require('../db');
|
||||||
const rpcProviderService = require('../services/rpcProviderService');
|
const rpcProviderService = require('../services/rpcProviderService');
|
||||||
const authTokenService = require('../services/authTokenService');
|
const authTokenService = require('../services/authTokenService');
|
||||||
const aiProviderSettingsService = require('../services/aiProviderSettingsService');
|
const aiProviderSettingsService = require('../services/aiProviderSettingsService');
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ const router = express.Router();
|
|||||||
const db = require('../db');
|
const db = require('../db');
|
||||||
const { requireAuth } = require('../middleware/auth');
|
const { requireAuth } = require('../middleware/auth');
|
||||||
const vectorSearchClient = require('../services/vectorSearchClient');
|
const vectorSearchClient = require('../services/vectorSearchClient');
|
||||||
|
const { broadcastTableUpdate, broadcastTableRelationsUpdate } = require('../wsHub');
|
||||||
|
|
||||||
router.use((req, res, next) => {
|
router.use((req, res, next) => {
|
||||||
console.log('Tables router received:', req.method, req.originalUrl);
|
console.log('Tables router received:', req.method, req.originalUrl);
|
||||||
@@ -32,7 +33,7 @@ router.get('/', async (req, res, next) => {
|
|||||||
let encryptionKey = 'default-key';
|
let encryptionKey = 'default-key';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
|
const keyPath = '/app/ssl/keys/full_db_encryption.key';
|
||||||
if (fs.existsSync(keyPath)) {
|
if (fs.existsSync(keyPath)) {
|
||||||
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
|
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
|
||||||
}
|
}
|
||||||
@@ -58,7 +59,7 @@ router.post('/', async (req, res, next) => {
|
|||||||
let encryptionKey = 'default-key';
|
let encryptionKey = 'default-key';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
|
const keyPath = '/app/ssl/keys/full_db_encryption.key';
|
||||||
if (fs.existsSync(keyPath)) {
|
if (fs.existsSync(keyPath)) {
|
||||||
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
|
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
|
||||||
}
|
}
|
||||||
@@ -85,7 +86,7 @@ router.get('/rag-sources', async (req, res, next) => {
|
|||||||
let encryptionKey = 'default-key';
|
let encryptionKey = 'default-key';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
|
const keyPath = '/app/ssl/keys/full_db_encryption.key';
|
||||||
if (fs.existsSync(keyPath)) {
|
if (fs.existsSync(keyPath)) {
|
||||||
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
|
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
|
||||||
}
|
}
|
||||||
@@ -115,7 +116,7 @@ router.get('/:id', async (req, res, next) => {
|
|||||||
let encryptionKey = 'default-key';
|
let encryptionKey = 'default-key';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
|
const keyPath = '/app/ssl/keys/full_db_encryption.key';
|
||||||
if (fs.existsSync(keyPath)) {
|
if (fs.existsSync(keyPath)) {
|
||||||
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
|
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
|
||||||
}
|
}
|
||||||
@@ -123,11 +124,26 @@ router.get('/:id', async (req, res, next) => {
|
|||||||
console.error('Error reading encryption key:', keyError);
|
console.error('Error reading encryption key:', keyError);
|
||||||
}
|
}
|
||||||
|
|
||||||
const tableMetaResult = await db.getQuery()('SELECT decrypt_text(name_encrypted, $2) as name, decrypt_text(description_encrypted, $2) as description FROM user_tables WHERE id = $1', [tableId, encryptionKey]);
|
// Выполняем все 4 запроса параллельно для ускорения
|
||||||
|
const [tableMetaResult, columnsResult, rowsResult, cellValuesResult] = await Promise.all([
|
||||||
|
// 1. Метаданные таблицы
|
||||||
|
db.getQuery()('SELECT decrypt_text(name_encrypted, $2) as name, decrypt_text(description_encrypted, $2) as description FROM user_tables WHERE id = $1', [tableId, encryptionKey]),
|
||||||
|
|
||||||
|
// 2. Столбцы
|
||||||
|
db.getQuery()('SELECT id, table_id, "order", created_at, updated_at, decrypt_text(name_encrypted, $2) as name, decrypt_text(type_encrypted, $2) as type, decrypt_text(placeholder_encrypted, $2) as placeholder_encrypted, options, placeholder FROM user_columns WHERE table_id = $1 ORDER BY "order" ASC, id ASC', [tableId, encryptionKey]),
|
||||||
|
|
||||||
|
// 3. Строки
|
||||||
|
db.getQuery()('SELECT * FROM user_rows WHERE table_id = $1 ORDER BY id', [tableId]),
|
||||||
|
|
||||||
|
// 4. Значения ячеек
|
||||||
|
db.getQuery()('SELECT id, row_id, column_id, created_at, updated_at, decrypt_text(value_encrypted, $2) as value FROM user_cell_values WHERE row_id IN (SELECT id FROM user_rows WHERE table_id = $1)', [tableId, encryptionKey])
|
||||||
|
]);
|
||||||
|
|
||||||
const tableMeta = tableMetaResult.rows[0] || { name: '', description: '' };
|
const tableMeta = tableMetaResult.rows[0] || { name: '', description: '' };
|
||||||
const columns = (await db.getQuery()('SELECT id, table_id, "order", created_at, updated_at, decrypt_text(name_encrypted, $2) as name, decrypt_text(type_encrypted, $2) as type, decrypt_text(placeholder_encrypted, $2) as placeholder_encrypted, placeholder FROM user_columns WHERE table_id = $1 ORDER BY "order" ASC, id ASC', [tableId, encryptionKey])).rows;
|
const columns = columnsResult.rows;
|
||||||
const rows = (await db.getQuery()('SELECT * FROM user_rows WHERE table_id = $1 ORDER BY id', [tableId])).rows;
|
const rows = rowsResult.rows;
|
||||||
const cellValues = (await db.getQuery()('SELECT id, row_id, column_id, created_at, updated_at, decrypt_text(value_encrypted, $2) as value FROM user_cell_values WHERE row_id IN (SELECT id FROM user_rows WHERE table_id = $1)', [tableId, encryptionKey])).rows;
|
const cellValues = cellValuesResult.rows;
|
||||||
|
|
||||||
res.json({ name: tableMeta.name, description: tableMeta.description, columns, rows, cellValues });
|
res.json({ name: tableMeta.name, description: tableMeta.description, columns, rows, cellValues });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
next(err);
|
next(err);
|
||||||
@@ -177,7 +193,7 @@ router.post('/:id/columns', async (req, res, next) => {
|
|||||||
let encryptionKey = 'default-key';
|
let encryptionKey = 'default-key';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
|
const keyPath = '/app/ssl/keys/full_db_encryption.key';
|
||||||
if (fs.existsSync(keyPath)) {
|
if (fs.existsSync(keyPath)) {
|
||||||
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
|
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
|
||||||
}
|
}
|
||||||
@@ -194,6 +210,7 @@ router.post('/:id/columns', async (req, res, next) => {
|
|||||||
[tableId, name, type, order || 0, placeholder, placeholder, encryptionKey]
|
[tableId, name, type, order || 0, placeholder, placeholder, encryptionKey]
|
||||||
);
|
);
|
||||||
res.json(result.rows[0]);
|
res.json(result.rows[0]);
|
||||||
|
broadcastTableUpdate(tableId);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
next(err);
|
next(err);
|
||||||
}
|
}
|
||||||
@@ -214,7 +231,7 @@ router.post('/:id/rows', async (req, res, next) => {
|
|||||||
let encryptionKey = 'default-key';
|
let encryptionKey = 'default-key';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
|
const keyPath = '/app/ssl/keys/full_db_encryption.key';
|
||||||
if (fs.existsSync(keyPath)) {
|
if (fs.existsSync(keyPath)) {
|
||||||
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
|
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
|
||||||
}
|
}
|
||||||
@@ -231,6 +248,7 @@ router.post('/:id/rows', async (req, res, next) => {
|
|||||||
}
|
}
|
||||||
console.log('[DEBUG][addRow] res.json:', result.rows[0]);
|
console.log('[DEBUG][addRow] res.json:', result.rows[0]);
|
||||||
res.json(result.rows[0]);
|
res.json(result.rows[0]);
|
||||||
|
broadcastTableUpdate(tableId);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
next(err);
|
next(err);
|
||||||
}
|
}
|
||||||
@@ -247,7 +265,7 @@ router.get('/:id/rows', async (req, res, next) => {
|
|||||||
let encryptionKey = 'default-key';
|
let encryptionKey = 'default-key';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
|
const keyPath = '/app/ssl/keys/full_db_encryption.key';
|
||||||
if (fs.existsSync(keyPath)) {
|
if (fs.existsSync(keyPath)) {
|
||||||
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
|
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
|
||||||
}
|
}
|
||||||
@@ -331,7 +349,7 @@ router.patch('/cell/:cellId', async (req, res, next) => {
|
|||||||
let encryptionKey = 'default-key';
|
let encryptionKey = 'default-key';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
|
const keyPath = '/app/ssl/keys/full_db_encryption.key';
|
||||||
if (fs.existsSync(keyPath)) {
|
if (fs.existsSync(keyPath)) {
|
||||||
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
|
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
|
||||||
}
|
}
|
||||||
@@ -362,6 +380,7 @@ router.patch('/cell/:cellId', async (req, res, next) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
res.json(result.rows[0]);
|
res.json(result.rows[0]);
|
||||||
|
broadcastTableUpdate(tableId);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
next(err);
|
next(err);
|
||||||
}
|
}
|
||||||
@@ -377,7 +396,7 @@ router.post('/cell', async (req, res, next) => {
|
|||||||
let encryptionKey = 'default-key';
|
let encryptionKey = 'default-key';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
|
const keyPath = '/app/ssl/keys/full_db_encryption.key';
|
||||||
if (fs.existsSync(keyPath)) {
|
if (fs.existsSync(keyPath)) {
|
||||||
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
|
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
|
||||||
}
|
}
|
||||||
@@ -391,10 +410,20 @@ router.post('/cell', async (req, res, next) => {
|
|||||||
RETURNING *`,
|
RETURNING *`,
|
||||||
[row_id, column_id, value, encryptionKey]
|
[row_id, column_id, value, encryptionKey]
|
||||||
);
|
);
|
||||||
// Получаем table_id
|
|
||||||
|
// Получаем table_id и проверяем, является ли это таблицей тегов
|
||||||
const table = (await db.getQuery()('SELECT table_id FROM user_rows WHERE id = $1', [row_id])).rows[0];
|
const table = (await db.getQuery()('SELECT table_id FROM user_rows WHERE id = $1', [row_id])).rows[0];
|
||||||
if (table) {
|
if (table) {
|
||||||
const tableId = table.table_id;
|
const tableId = table.table_id;
|
||||||
|
|
||||||
|
// Проверяем, является ли это таблицей "Теги клиентов" - ОТКЛЮЧАЕМ WebSocket
|
||||||
|
// const tableName = (await db.getQuery()('SELECT decrypt_text(name_encrypted, $2) as name FROM user_tables WHERE id = $1', [tableId, encryptionKey])).rows[0];
|
||||||
|
// if (tableName && tableName.name === 'Теги клиентов') {
|
||||||
|
// // Отправляем WebSocket уведомление об обновлении тегов
|
||||||
|
// const { broadcastTagsUpdate } = require('../wsHub');
|
||||||
|
// broadcastTagsUpdate();
|
||||||
|
// }
|
||||||
|
|
||||||
// Получаем всю строку для upsert
|
// Получаем всю строку для upsert
|
||||||
const rowData = (await db.getQuery()('SELECT r.id as row_id, decrypt_text(c.value_encrypted, $2) as text, decrypt_text(c2.value_encrypted, $2) as answer FROM user_rows r LEFT JOIN user_cell_values c ON c.row_id = r.id AND c.column_id = 1 LEFT JOIN user_cell_values c2 ON c2.row_id = r.id AND c2.column_id = 2 WHERE r.id = $1', [row_id, encryptionKey])).rows[0];
|
const rowData = (await db.getQuery()('SELECT r.id as row_id, decrypt_text(c.value_encrypted, $2) as text, decrypt_text(c2.value_encrypted, $2) as answer FROM user_rows r LEFT JOIN user_cell_values c ON c.row_id = r.id AND c.column_id = 1 LEFT JOIN user_cell_values c2 ON c2.row_id = r.id AND c2.column_id = 2 WHERE r.id = $1', [row_id, encryptionKey])).rows[0];
|
||||||
if (rowData) {
|
if (rowData) {
|
||||||
@@ -404,7 +433,12 @@ router.post('/cell', async (req, res, next) => {
|
|||||||
await vectorSearchClient.upsert(tableId, upsertRows);
|
await vectorSearchClient.upsert(tableId, upsertRows);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Отправляем WebSocket уведомление об обновлении таблицы
|
||||||
|
const { broadcastTableUpdate } = require('../wsHub');
|
||||||
|
broadcastTableUpdate(tableId);
|
||||||
}
|
}
|
||||||
|
|
||||||
res.json(result.rows[0]);
|
res.json(result.rows[0]);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
next(err);
|
next(err);
|
||||||
@@ -417,7 +451,29 @@ router.delete('/row/:rowId', async (req, res, next) => {
|
|||||||
const rowId = req.params.rowId;
|
const rowId = req.params.rowId;
|
||||||
// Получаем table_id
|
// Получаем table_id
|
||||||
const table = (await db.getQuery()('SELECT table_id FROM user_rows WHERE id = $1', [rowId])).rows[0];
|
const table = (await db.getQuery()('SELECT table_id FROM user_rows WHERE id = $1', [rowId])).rows[0];
|
||||||
|
|
||||||
|
// Проверяем, является ли это таблицей тегов перед удалением
|
||||||
|
let isTagsTable = false;
|
||||||
|
if (table) {
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
let encryptionKey = 'default-key';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const keyPath = '/app/ssl/keys/full_db_encryption.key';
|
||||||
|
if (fs.existsSync(keyPath)) {
|
||||||
|
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
|
||||||
|
}
|
||||||
|
} catch (keyError) {
|
||||||
|
console.error('Error reading encryption key:', keyError);
|
||||||
|
}
|
||||||
|
|
||||||
|
const tableName = (await db.getQuery()('SELECT decrypt_text(name_encrypted, $2) as name FROM user_tables WHERE id = $1', [table.table_id, encryptionKey])).rows[0];
|
||||||
|
isTagsTable = tableName && tableName.name === 'Теги клиентов';
|
||||||
|
}
|
||||||
|
|
||||||
await db.getQuery()('DELETE FROM user_rows WHERE id = $1', [rowId]);
|
await db.getQuery()('DELETE FROM user_rows WHERE id = $1', [rowId]);
|
||||||
|
|
||||||
if (table) {
|
if (table) {
|
||||||
const tableId = table.table_id;
|
const tableId = table.table_id;
|
||||||
// Получаем все строки для rebuild
|
// Получаем все строки для rebuild
|
||||||
@@ -427,7 +483,7 @@ router.delete('/row/:rowId', async (req, res, next) => {
|
|||||||
let encryptionKey = 'default-key';
|
let encryptionKey = 'default-key';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
|
const keyPath = '/app/ssl/keys/full_db_encryption.key';
|
||||||
if (fs.existsSync(keyPath)) {
|
if (fs.existsSync(keyPath)) {
|
||||||
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
|
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
|
||||||
}
|
}
|
||||||
@@ -442,6 +498,17 @@ router.delete('/row/:rowId', async (req, res, next) => {
|
|||||||
await vectorSearchClient.rebuild(tableId, rebuildRows);
|
await vectorSearchClient.rebuild(tableId, rebuildRows);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Отправляем WebSocket уведомление, если это была таблица тегов - ОТКЛЮЧАЕМ
|
||||||
|
// if (isTagsTable) {
|
||||||
|
// const { broadcastTagsUpdate } = require('../wsHub');
|
||||||
|
// broadcastTagsUpdate();
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Отправляем WebSocket уведомление об обновлении таблицы
|
||||||
|
const { broadcastTableUpdate } = require('../wsHub');
|
||||||
|
broadcastTableUpdate(tableId);
|
||||||
|
|
||||||
res.json({ success: true });
|
res.json({ success: true });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
next(err);
|
next(err);
|
||||||
@@ -471,7 +538,7 @@ router.patch('/column/:columnId', async (req, res, next) => {
|
|||||||
let encryptionKey = 'default-key';
|
let encryptionKey = 'default-key';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
|
const keyPath = '/app/ssl/keys/full_db_encryption.key';
|
||||||
if (fs.existsSync(keyPath)) {
|
if (fs.existsSync(keyPath)) {
|
||||||
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
|
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
|
||||||
}
|
}
|
||||||
@@ -523,6 +590,7 @@ router.patch('/column/:columnId', async (req, res, next) => {
|
|||||||
return res.status(404).json({ error: 'Column not found' });
|
return res.status(404).json({ error: 'Column not found' });
|
||||||
}
|
}
|
||||||
res.json(result.rows[0]);
|
res.json(result.rows[0]);
|
||||||
|
broadcastTableUpdate(colInfo.table_id);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
next(err);
|
next(err);
|
||||||
}
|
}
|
||||||
@@ -588,21 +656,45 @@ router.post('/:id/rebuild-index', requireAuth, async (req, res, next) => {
|
|||||||
if (!req.session.isAdmin) {
|
if (!req.session.isAdmin) {
|
||||||
return res.status(403).json({ error: 'Доступ только для администратора' });
|
return res.status(403).json({ error: 'Доступ только для администратора' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Получаем ключ шифрования
|
||||||
|
const fs = require('fs');
|
||||||
|
let encryptionKey = 'default-key';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const keyPath = '/app/ssl/keys/full_db_encryption.key';
|
||||||
|
if (fs.existsSync(keyPath)) {
|
||||||
|
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
|
||||||
|
}
|
||||||
|
} catch (keyError) {
|
||||||
|
console.error('Error reading encryption key:', keyError);
|
||||||
|
}
|
||||||
|
|
||||||
const tableId = req.params.id;
|
const tableId = req.params.id;
|
||||||
const { questionCol, answerCol } = await getQuestionAnswerColumnIds(tableId);
|
const { questionCol, answerCol } = await getQuestionAnswerColumnIds(tableId);
|
||||||
if (!questionCol || !answerCol) {
|
if (!questionCol || !answerCol) {
|
||||||
return res.status(400).json({ error: 'Не найдены колонки с вопросами и ответами' });
|
return res.status(400).json({ error: 'Не найдены колонки с вопросами и ответами' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const rows = (await db.getQuery()(
|
const rows = (await db.getQuery()(
|
||||||
`SELECT r.id as row_id, c.value as text, c2.value as answer
|
`SELECT r.id as row_id,
|
||||||
|
decrypt_text(c.value_encrypted, $4) as text,
|
||||||
|
decrypt_text(c2.value_encrypted, $4) as answer
|
||||||
FROM user_rows r
|
FROM user_rows r
|
||||||
LEFT JOIN user_cell_values c ON c.row_id = r.id AND c.column_id = $2
|
LEFT JOIN user_cell_values c ON c.row_id = r.id AND c.column_id = $2
|
||||||
LEFT JOIN user_cell_values c2 ON c2.row_id = r.id AND c2.column_id = $3
|
LEFT JOIN user_cell_values c2 ON c2.row_id = r.id AND c2.column_id = $3
|
||||||
WHERE r.table_id = $1`,
|
WHERE r.table_id = $1`,
|
||||||
[tableId, questionCol, answerCol]
|
[tableId, questionCol, answerCol, encryptionKey]
|
||||||
)).rows;
|
)).rows;
|
||||||
const rebuildRows = rows.filter(r => r.row_id && r.text).map(r => ({ row_id: r.row_id, text: r.text, metadata: { answer: r.answer } }));
|
|
||||||
|
const rebuildRows = rows.filter(r => r.row_id && r.text).map(r => ({
|
||||||
|
row_id: r.row_id,
|
||||||
|
text: r.text,
|
||||||
|
metadata: { answer: r.answer }
|
||||||
|
}));
|
||||||
|
|
||||||
console.log('[DEBUG][rebuildRows]', rebuildRows);
|
console.log('[DEBUG][rebuildRows]', rebuildRows);
|
||||||
|
|
||||||
if (rebuildRows.length > 0) {
|
if (rebuildRows.length > 0) {
|
||||||
await vectorSearchClient.rebuild(tableId, rebuildRows);
|
await vectorSearchClient.rebuild(tableId, rebuildRows);
|
||||||
res.json({ success: true, count: rebuildRows.length });
|
res.json({ success: true, count: rebuildRows.length });
|
||||||
@@ -708,6 +800,10 @@ router.post('/:tableId/row/:rowId/multirelations', async (req, res, next) => {
|
|||||||
[rowId, column_id, to_table_id, to_row_id]
|
[rowId, column_id, to_table_id, to_row_id]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Отправляем WebSocket уведомление об обновлении связей
|
||||||
|
broadcastTableRelationsUpdate(tableId, rowId);
|
||||||
|
|
||||||
res.json({ success: true });
|
res.json({ success: true });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
next(err);
|
next(err);
|
||||||
@@ -724,7 +820,7 @@ router.get('/:id/placeholders', async (req, res, next) => {
|
|||||||
let encryptionKey = 'default-key';
|
let encryptionKey = 'default-key';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
|
const keyPath = '/app/ssl/keys/full_db_encryption.key';
|
||||||
if (fs.existsSync(keyPath)) {
|
if (fs.existsSync(keyPath)) {
|
||||||
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
|
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -524,6 +524,11 @@ router.patch('/:id/tags', async (req, res) => {
|
|||||||
[userId, tagId]
|
[userId, tagId]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Отправляем WebSocket уведомление об обновлении тегов - ОТКЛЮЧАЕМ
|
||||||
|
// const { broadcastTagsUpdate } = require('../wsHub');
|
||||||
|
// broadcastTagsUpdate();
|
||||||
|
|
||||||
res.json({ success: true });
|
res.json({ success: true });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).json({ error: e.message });
|
res.status(500).json({ error: e.message });
|
||||||
@@ -553,6 +558,11 @@ router.delete('/:id/tags/:tagId', async (req, res) => {
|
|||||||
'DELETE FROM user_tag_links WHERE user_id = $1 AND tag_id = $2',
|
'DELETE FROM user_tag_links WHERE user_id = $1 AND tag_id = $2',
|
||||||
[userId, tagId]
|
[userId, tagId]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Отправляем WebSocket уведомление об обновлении тегов - ОТКЛЮЧАЕМ
|
||||||
|
// const { broadcastTagsUpdate } = require('../wsHub');
|
||||||
|
// broadcastTagsUpdate();
|
||||||
|
|
||||||
res.json({ success: true });
|
res.json({ success: true });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).json({ error: e.message });
|
res.status(500).json({ error: e.message });
|
||||||
|
|||||||
@@ -16,6 +16,10 @@ let wss = null;
|
|||||||
// Храним клиентов по userId для персонализированных уведомлений
|
// Храним клиентов по userId для персонализированных уведомлений
|
||||||
const wsClients = new Map(); // userId -> Set of WebSocket connections
|
const wsClients = new Map(); // userId -> Set of WebSocket connections
|
||||||
|
|
||||||
|
// Кэш для отслеживания изменений тегов
|
||||||
|
const tagsChangeCache = new Map();
|
||||||
|
const TAGS_CACHE_TTL = 5000; // 5 секунд
|
||||||
|
|
||||||
function initWSS(server) {
|
function initWSS(server) {
|
||||||
wss = new WebSocket.Server({ server, path: '/ws' });
|
wss = new WebSocket.Server({ server, path: '/ws' });
|
||||||
|
|
||||||
@@ -172,6 +176,96 @@ function broadcastConversationUpdate(conversationId, targetUserId = null) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function broadcastTableUpdate(tableId) {
|
||||||
|
console.log('📢 [WebSocket] Отправка обновления таблицы', tableId);
|
||||||
|
const payload = { type: 'table-updated', tableId };
|
||||||
|
for (const [userId, clients] of wsClients.entries()) {
|
||||||
|
for (const ws of clients) {
|
||||||
|
if (ws.readyState === WebSocket.OPEN) {
|
||||||
|
ws.send(JSON.stringify(payload));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function broadcastTableRelationsUpdate(tableId, rowId, targetUserId = null) {
|
||||||
|
console.log(`📢 [WebSocket] Отправка обновления связей таблицы`, {
|
||||||
|
tableId,
|
||||||
|
rowId,
|
||||||
|
targetUserId
|
||||||
|
});
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
type: 'table-relations-updated',
|
||||||
|
tableId,
|
||||||
|
rowId
|
||||||
|
};
|
||||||
|
|
||||||
|
if (targetUserId) {
|
||||||
|
// Отправляем конкретному пользователю
|
||||||
|
const userClients = wsClients.get(targetUserId.toString());
|
||||||
|
if (userClients) {
|
||||||
|
for (const ws of userClients) {
|
||||||
|
if (ws.readyState === WebSocket.OPEN) {
|
||||||
|
ws.send(JSON.stringify(payload));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Отправляем всем
|
||||||
|
for (const [userId, clients] of wsClients.entries()) {
|
||||||
|
for (const ws of clients) {
|
||||||
|
if (ws.readyState === WebSocket.OPEN) {
|
||||||
|
ws.send(JSON.stringify(payload));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function broadcastTagsUpdate(targetUserId = null) {
|
||||||
|
const now = Date.now();
|
||||||
|
const cacheKey = targetUserId || 'global';
|
||||||
|
|
||||||
|
// Проверяем, не отправляли ли мы недавно уведомление
|
||||||
|
const lastUpdate = tagsChangeCache.get(cacheKey);
|
||||||
|
if (lastUpdate && (now - lastUpdate) < TAGS_CACHE_TTL) {
|
||||||
|
console.log(`🏷️ [WebSocket] Пропускаем отправку уведомления о тегах (слишком часто)`, { targetUserId });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обновляем кэш
|
||||||
|
tagsChangeCache.set(cacheKey, now);
|
||||||
|
|
||||||
|
console.log(`🏷️ [WebSocket] Отправка обновления тегов`, { targetUserId });
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
type: 'tags-updated',
|
||||||
|
timestamp: now
|
||||||
|
};
|
||||||
|
|
||||||
|
if (targetUserId) {
|
||||||
|
// Отправляем конкретному пользователю
|
||||||
|
const userClients = wsClients.get(targetUserId.toString());
|
||||||
|
if (userClients) {
|
||||||
|
for (const ws of userClients) {
|
||||||
|
if (ws.readyState === WebSocket.OPEN) {
|
||||||
|
ws.send(JSON.stringify(payload));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Отправляем всем
|
||||||
|
for (const [userId, clients] of wsClients.entries()) {
|
||||||
|
for (const ws of clients) {
|
||||||
|
if (ws.readyState === WebSocket.OPEN) {
|
||||||
|
ws.send(JSON.stringify(payload));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function getConnectedUsers() {
|
function getConnectedUsers() {
|
||||||
const users = [];
|
const users = [];
|
||||||
for (const [userId, clients] of wsClients.entries()) {
|
for (const [userId, clients] of wsClients.entries()) {
|
||||||
@@ -210,6 +304,9 @@ module.exports = {
|
|||||||
broadcastMessagesUpdate,
|
broadcastMessagesUpdate,
|
||||||
broadcastChatMessage,
|
broadcastChatMessage,
|
||||||
broadcastConversationUpdate,
|
broadcastConversationUpdate,
|
||||||
|
broadcastTableUpdate,
|
||||||
|
broadcastTableRelationsUpdate,
|
||||||
|
broadcastTagsUpdate,
|
||||||
getConnectedUsers,
|
getConnectedUsers,
|
||||||
getStats
|
getStats
|
||||||
};
|
};
|
||||||
@@ -104,13 +104,14 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { defineProps, computed, ref, onMounted, watch } from 'vue';
|
import { defineProps, computed, ref, onMounted, watch, onUnmounted } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import { ElSelect, ElOption, ElForm, ElFormItem, ElInput, ElDatePicker, ElCheckbox, ElButton, ElMessageBox, ElMessage } from 'element-plus';
|
import { ElSelect, ElOption, ElForm, ElFormItem, ElInput, ElDatePicker, ElCheckbox, ElButton, ElMessageBox, ElMessage } from 'element-plus';
|
||||||
import ImportContactsModal from './ImportContactsModal.vue';
|
import ImportContactsModal from './ImportContactsModal.vue';
|
||||||
import BroadcastModal from './BroadcastModal.vue';
|
import BroadcastModal from './BroadcastModal.vue';
|
||||||
import tablesService from '../services/tablesService';
|
import tablesService from '../services/tablesService';
|
||||||
import messagesService from '../services/messagesService';
|
import messagesService from '../services/messagesService';
|
||||||
|
import { useTagsWebSocket } from '../composables/useTagsWebSocket';
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
contacts: { type: Array, default: () => [] },
|
contacts: { type: Array, default: () => [] },
|
||||||
newContacts: { type: Array, default: () => [] },
|
newContacts: { type: Array, default: () => [] },
|
||||||
@@ -141,38 +142,81 @@ const showBroadcastModal = ref(false);
|
|||||||
const selectedIds = ref([]);
|
const selectedIds = ref([]);
|
||||||
const selectAll = ref(false);
|
const selectAll = ref(false);
|
||||||
|
|
||||||
|
// WebSocket для тегов - ОТКЛЮЧАЕМ из-за циклических запросов
|
||||||
|
// const { onTagsUpdate } = useTagsWebSocket();
|
||||||
|
// let unsubscribeFromTags = null;
|
||||||
|
let lastTagsHash = ref(''); // Хеш последних загруженных тегов
|
||||||
|
let tagsUpdateInterval = null; // Интервал для периодического обновления тегов
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await fetchContacts();
|
await fetchContacts();
|
||||||
await loadAvailableTags();
|
// ВРЕМЕННО ОТКЛЮЧАЕМ - await loadAvailableTags();
|
||||||
|
|
||||||
|
// ВРЕМЕННО ОТКЛЮЧАЕМ - Вместо WebSocket используем периодическое обновление каждые 30 секунд
|
||||||
|
// tagsUpdateInterval = setInterval(async () => {
|
||||||
|
// console.log('[ContactTable] Периодическое обновление тегов');
|
||||||
|
// await loadAvailableTags();
|
||||||
|
// }, 30000); // 30 секунд
|
||||||
|
|
||||||
|
// Подписываемся на обновления тегов - ОТКЛЮЧАЕМ
|
||||||
|
// unsubscribeFromTags = onTagsUpdate(async () => {
|
||||||
|
// console.log('[ContactTable] Получено обновление тегов, проверяем необходимость перезагрузки');
|
||||||
|
// await loadAvailableTags();
|
||||||
|
// });
|
||||||
});
|
});
|
||||||
|
|
||||||
async function loadAvailableTags() {
|
onUnmounted(() => {
|
||||||
try {
|
// Отписываемся от WebSocket при размонтировании - ОТКЛЮЧАЕМ
|
||||||
// Получаем все пользовательские таблицы и ищем "Теги клиентов"
|
// if (unsubscribeFromTags) {
|
||||||
const tables = await tablesService.getTables();
|
// unsubscribeFromTags();
|
||||||
const tagsTable = tables.find(t => t.name === 'Теги клиентов');
|
// }
|
||||||
|
|
||||||
if (tagsTable) {
|
// Очищаем интервал
|
||||||
// Загружаем данные таблицы тегов
|
if (tagsUpdateInterval) {
|
||||||
const table = await tablesService.getTable(tagsTable.id);
|
clearInterval(tagsUpdateInterval);
|
||||||
const nameColumn = table.columns.find(col => col.name === 'Название') || table.columns[0];
|
tagsUpdateInterval = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
if (nameColumn) {
|
// ВРЕМЕННО ОТКЛЮЧАЕМ - async function loadAvailableTags() {
|
||||||
// Формируем список тегов
|
// try {
|
||||||
availableTags.value = table.rows.map(row => {
|
// // Получаем все пользовательские таблицы и ищем "Теги клиентов"
|
||||||
const nameCell = table.cellValues.find(c => c.row_id === row.id && c.column_id === nameColumn.id);
|
// const tables = await tablesService.getTables();
|
||||||
return {
|
// const tagsTable = tables.find(t => t.name === 'Теги клиентов');
|
||||||
id: row.id,
|
//
|
||||||
name: nameCell ? nameCell.value : `Тег ${row.id}`
|
// if (tagsTable) {
|
||||||
};
|
// // Загружаем данные таблицы тегов
|
||||||
}).filter(tag => tag.name.trim()); // Исключаем пустые названия
|
// const table = await tablesService.getTable(tagsTable.id);
|
||||||
}
|
// const nameColumn = table.columns.find(col => col.name === 'Название') || table.columns[0];
|
||||||
}
|
//
|
||||||
} catch (e) {
|
// if (nameColumn) {
|
||||||
console.error('Ошибка загрузки тегов:', e);
|
// // Формируем список тегов
|
||||||
availableTags.value = [];
|
// const newTags = table.rows.map(row => {
|
||||||
}
|
// const nameCell = table.cellValues.find(c => c.row_id === row.id && c.column_id === nameColumn.id);
|
||||||
}
|
// return {
|
||||||
|
// id: row.id,
|
||||||
|
// name: nameCell ? nameCell.value : `Тег ${row.id}`
|
||||||
|
// };
|
||||||
|
// }).filter(tag => tag.name.trim()); // Исключаем пустые названия
|
||||||
|
//
|
||||||
|
// // Создаем хеш для сравнения
|
||||||
|
// const newTagsHash = JSON.stringify(newTags.map(t => `${t.id}:${t.name}`).sort());
|
||||||
|
//
|
||||||
|
// // Обновляем только если данные действительно изменились
|
||||||
|
// if (newTagsHash !== lastTagsHash.value) {
|
||||||
|
// console.log('[ContactTable] Теги изменились, обновляем список');
|
||||||
|
// availableTags.value = newTags;
|
||||||
|
// lastTagsHash.value = newTagsHash;
|
||||||
|
// } else {
|
||||||
|
// console.log('[ContactTable] Теги не изменились, пропускаем обновление');
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// } catch (e) {
|
||||||
|
// console.error('Ошибка загрузки тегов:', e);
|
||||||
|
// availableTags.value = [];
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
function buildQuery() {
|
function buildQuery() {
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
|
|||||||
@@ -123,8 +123,11 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, watch, onMounted, computed, nextTick } from 'vue';
|
import { ref, watch, onMounted, computed, nextTick, onUnmounted } from 'vue';
|
||||||
import tablesService from '../../services/tablesService';
|
import tablesService from '../../services/tablesService';
|
||||||
|
import { useTablesWebSocket } from '../../composables/useTablesWebSocket';
|
||||||
|
import { useTagsWebSocket } from '../../composables/useTagsWebSocket';
|
||||||
|
import cacheService from '../../services/cacheService';
|
||||||
const props = defineProps(['rowId', 'column', 'cellValues']);
|
const props = defineProps(['rowId', 'column', 'cellValues']);
|
||||||
const emit = defineEmits(['update']);
|
const emit = defineEmits(['update']);
|
||||||
|
|
||||||
@@ -174,12 +177,54 @@ watch(editing, (val) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Добавляем watch для отслеживания изменений в мультисвязях
|
// Добавляем watch для отслеживания изменений в мультисвязях с дебаунсингом
|
||||||
|
let debounceTimer = null;
|
||||||
watch(editMultiRelationValues, (newValues, oldValues) => {
|
watch(editMultiRelationValues, (newValues, oldValues) => {
|
||||||
console.log('[editMultiRelationValues] changed from:', oldValues, 'to:', newValues);
|
console.log('[editMultiRelationValues] changed from:', oldValues, 'to:', newValues);
|
||||||
|
|
||||||
|
// Очищаем предыдущий таймер
|
||||||
|
if (debounceTimer) {
|
||||||
|
clearTimeout(debounceTimer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Устанавливаем новый таймер для предотвращения множественных обновлений
|
||||||
|
debounceTimer = setTimeout(() => {
|
||||||
|
// Здесь можно добавить дополнительную логику, если нужно
|
||||||
|
}, 100);
|
||||||
}, { deep: true });
|
}, { deep: true });
|
||||||
|
|
||||||
|
// Флаг для предотвращения циклической загрузки
|
||||||
|
const isLoadingMultiRelations = ref(false);
|
||||||
|
const lastLoadedValues = ref(new Map()); // Кэш последних загруженных значений
|
||||||
|
|
||||||
|
// WebSocket для обновлений таблиц
|
||||||
|
const { subscribeToTableRelationsUpdates } = useTablesWebSocket();
|
||||||
|
let unsubscribeFromWebSocket = null;
|
||||||
|
|
||||||
|
// Функция для очистки кэша
|
||||||
|
function clearCache() {
|
||||||
|
cacheService.clearAll();
|
||||||
|
console.log('[TableCell] Кэш очищен');
|
||||||
|
}
|
||||||
|
|
||||||
|
// WebSocket для тегов
|
||||||
|
const { onTagsUpdate } = useTagsWebSocket();
|
||||||
|
let unsubscribeFromTags = null;
|
||||||
|
|
||||||
|
// Удаляем локальные кэши
|
||||||
|
// const multiRelationOptionsCache = new Map();
|
||||||
|
// const multiRelationOptionsCacheTimeout = 30000; // 30 секунд
|
||||||
|
// const relationsCache = new Map();
|
||||||
|
// const relationsCacheTimeout = 10000; // 10 секунд
|
||||||
|
|
||||||
|
// Флаг для предотвращения повторных вызовов
|
||||||
|
let isInitialized = false;
|
||||||
|
let isMultiRelationValuesLoaded = false;
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
const startTime = Date.now();
|
||||||
|
console.log(`[TableCell] 🚀 Начало монтирования ячейки row:${props.rowId} col:${props.column.id} в ${startTime}`);
|
||||||
|
|
||||||
if (props.column.type === 'multiselect') {
|
if (props.column.type === 'multiselect') {
|
||||||
multiOptions.value = (props.column.options && props.column.options.options) || [];
|
multiOptions.value = (props.column.options && props.column.options.options) || [];
|
||||||
const cell = props.cellValues.find(
|
const cell = props.cellValues.find(
|
||||||
@@ -203,8 +248,46 @@ onMounted(async () => {
|
|||||||
} else if (props.column.type === 'lookup') {
|
} else if (props.column.type === 'lookup') {
|
||||||
await loadLookupValues();
|
await loadLookupValues();
|
||||||
} else if (props.column.type === 'multiselect-relation') {
|
} else if (props.column.type === 'multiselect-relation') {
|
||||||
|
// Загружаем опции только один раз
|
||||||
|
if (!isInitialized) {
|
||||||
|
console.log(`[TableCell] 📥 Загружаем опции для row:${props.rowId} col:${props.column.id}`);
|
||||||
|
await loadMultiRelationOptions();
|
||||||
|
isInitialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Загружаем relations только один раз для каждой комбинации rowId + columnId
|
||||||
|
if (!isMultiRelationValuesLoaded) {
|
||||||
|
console.log(`[TableCell] 📥 Загружаем relations для row:${props.rowId} col:${props.column.id}`);
|
||||||
|
await loadMultiRelationValues();
|
||||||
|
isMultiRelationValuesLoaded = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Подписываемся на обновления таблицы
|
||||||
|
if (props.column.type === 'multiselect-relation') {
|
||||||
|
unsubscribeFromWebSocket = subscribeToTableRelationsUpdates(props.column.table_id, async () => {
|
||||||
|
console.log('[TableCell] Получено обновление таблицы, перезагружаем relations');
|
||||||
|
// Сбрасываем флаг загрузки
|
||||||
|
isMultiRelationValuesLoaded = false;
|
||||||
|
// Очищаем кэш relations для текущей строки
|
||||||
|
cacheService.clearRelationsCache(props.rowId);
|
||||||
|
await loadMultiRelationValues();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Подписываемся на обновления тегов, если это связанная таблица тегов
|
||||||
|
if (props.column.options && props.column.options.relatedTableId) {
|
||||||
|
unsubscribeFromTags = onTagsUpdate(async () => {
|
||||||
|
console.log('[TableCell] Получено обновление тегов, перезагружаем опции');
|
||||||
|
// Сбрасываем флаги загрузки
|
||||||
|
isInitialized = false;
|
||||||
|
isMultiRelationValuesLoaded = false;
|
||||||
|
// Очищаем кэш таблицы тегов
|
||||||
|
cacheService.clearTableCache(props.column.options.relatedTableId);
|
||||||
await loadMultiRelationOptions();
|
await loadMultiRelationOptions();
|
||||||
await loadMultiRelationValues();
|
await loadMultiRelationValues();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Инициализация localValue для отображения массива, если нет имен
|
// Инициализация localValue для отображения массива, если нет имен
|
||||||
const cell = props.cellValues.find(
|
const cell = props.cellValues.find(
|
||||||
c => c.row_id === props.rowId && c.column_id === props.column.id
|
c => c.row_id === props.rowId && c.column_id === props.column.id
|
||||||
@@ -216,6 +299,28 @@ onMounted(async () => {
|
|||||||
);
|
);
|
||||||
localValue.value = cell ? cell.value : '';
|
localValue.value = cell ? cell.value : '';
|
||||||
}
|
}
|
||||||
|
const endTime = Date.now();
|
||||||
|
console.log(`[TableCell] ✅ Завершено монтирование ячейки row:${props.rowId} col:${props.column.id} за ${endTime - startTime}ms`);
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
// Отписываемся от WebSocket при размонтировании компонента
|
||||||
|
if (unsubscribeFromWebSocket) {
|
||||||
|
unsubscribeFromWebSocket();
|
||||||
|
unsubscribeFromWebSocket = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Отписываемся от обновлений тегов
|
||||||
|
if (unsubscribeFromTags) {
|
||||||
|
unsubscribeFromTags();
|
||||||
|
unsubscribeFromTags = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Очищаем таймер дебаунсинга
|
||||||
|
if (loadMultiRelationValuesTimer) {
|
||||||
|
clearTimeout(loadMultiRelationValuesTimer);
|
||||||
|
loadMultiRelationValuesTimer = null;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
@@ -244,7 +349,9 @@ watch(
|
|||||||
} else if (props.column.type === 'lookup') {
|
} else if (props.column.type === 'lookup') {
|
||||||
await loadLookupValues();
|
await loadLookupValues();
|
||||||
} else if (props.column.type === 'multiselect-relation') {
|
} else if (props.column.type === 'multiselect-relation') {
|
||||||
|
// Загружаем опции только один раз при изменении колонки
|
||||||
await loadMultiRelationOptions();
|
await loadMultiRelationOptions();
|
||||||
|
// Загружаем значения
|
||||||
await loadMultiRelationValues();
|
await loadMultiRelationValues();
|
||||||
// Инициализация localValue для отображения массива, если нет имен
|
// Инициализация localValue для отображения массива, если нет имен
|
||||||
const cell = props.cellValues.find(
|
const cell = props.cellValues.find(
|
||||||
@@ -378,49 +485,133 @@ async function loadLookupValues() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function loadMultiRelationOptions() {
|
async function loadMultiRelationOptions() {
|
||||||
|
// Проверяем, не загружены ли уже опции
|
||||||
|
if (multiRelationOptions.value.length > 0) {
|
||||||
|
console.log('[loadMultiRelationOptions] Опции уже загружены, пропускаем');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const rel = props.column.options || {};
|
const rel = props.column.options || {};
|
||||||
if (!rel.relatedTableId) return;
|
if (rel.relatedTableId && rel.relatedColumnId) {
|
||||||
const res = await fetch(`/api/tables/${rel.relatedTableId}`);
|
try {
|
||||||
const data = await res.json();
|
// Проверяем кэш для данных таблицы
|
||||||
// Далее используем data.columns, data.rows, data.cellValues
|
const cachedTableData = cacheService.getTableData(rel.relatedTableId, 'default');
|
||||||
const colId = rel.relatedColumnId || (data.columns[0] && data.columns[0].id);
|
let tableData;
|
||||||
|
|
||||||
|
if (cachedTableData) {
|
||||||
|
console.log(`[loadMultiRelationOptions] ✅ Используем предварительно загруженные данные таблицы ${rel.relatedTableId}`);
|
||||||
|
tableData = cachedTableData;
|
||||||
|
} else {
|
||||||
|
console.log(`[loadMultiRelationOptions] ⚠️ Данные таблицы ${rel.relatedTableId} не найдены в кэше, загружаем заново`);
|
||||||
|
const response = await fetch(`/api/tables/${rel.relatedTableId}`);
|
||||||
|
tableData = await response.json();
|
||||||
|
// Сохраняем в кэш
|
||||||
|
cacheService.setTableData(rel.relatedTableId, 'default', tableData);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Формируем опции из данных таблицы
|
||||||
|
const colId = rel.relatedColumnId || (tableData.columns[0] && tableData.columns[0].id);
|
||||||
const opts = [];
|
const opts = [];
|
||||||
for (const row of data.rows) {
|
for (const row of tableData.rows) {
|
||||||
const cell = data.cellValues.find(c => c.row_id === row.id && c.column_id === colId);
|
const cell = tableData.cellValues.find(c => c.row_id === row.id && c.column_id === colId);
|
||||||
opts.push({ id: row.id, display: cell ? cell.value : `ID ${row.id}` });
|
opts.push({ id: row.id, display: cell ? cell.value : `ID ${row.id}` });
|
||||||
}
|
}
|
||||||
multiRelationOptions.value = opts;
|
multiRelationOptions.value = opts;
|
||||||
|
console.log(`[loadMultiRelationOptions] Загружено ${opts.length} опций для таблицы ${rel.relatedTableId}`);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[loadMultiRelationOptions] Error:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Дебаунсинг для loadMultiRelationValues
|
||||||
|
let loadMultiRelationValuesTimer = null;
|
||||||
|
const LOAD_DEBOUNCE_DELAY = 100; // 100ms
|
||||||
|
|
||||||
async function loadMultiRelationValues() {
|
async function loadMultiRelationValues() {
|
||||||
|
// Проверяем, не загружены ли уже данные
|
||||||
|
if (isMultiRelationValuesLoaded) {
|
||||||
|
console.log('[loadMultiRelationValues] Данные уже загружены, пропускаем');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Очищаем предыдущий таймер
|
||||||
|
if (loadMultiRelationValuesTimer) {
|
||||||
|
clearTimeout(loadMultiRelationValuesTimer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Устанавливаем новый таймер
|
||||||
|
loadMultiRelationValuesTimer = setTimeout(async () => {
|
||||||
// Получаем связи для текущей строки
|
// Получаем связи для текущей строки
|
||||||
console.log('[loadMultiRelationValues] called for row:', props.rowId, 'column:', props.column.id);
|
console.log('[loadMultiRelationValues] called for row:', props.rowId, 'column:', props.column.id);
|
||||||
editMultiRelationValues.value = [];
|
|
||||||
selectedMultiRelationNames.value = [];
|
|
||||||
try {
|
try {
|
||||||
const rel = props.column.options || {};
|
const rel = props.column.options || {};
|
||||||
if (rel.relatedTableId && rel.relatedColumnId) {
|
if (rel.relatedTableId && rel.relatedColumnId) {
|
||||||
const url = `/api/tables/${props.column.table_id}/row/${props.rowId}/relations`;
|
// Проверяем кэш для relations
|
||||||
console.log('[loadMultiRelationValues] GET request to:', url);
|
let relations;
|
||||||
const res = await fetch(url);
|
let tableData;
|
||||||
const relations = await res.json();
|
|
||||||
console.log('[loadMultiRelationValues] API response status:', res.status, 'relations:', relations);
|
const cachedRelations = cacheService.getRelationsData(props.rowId, props.column.id);
|
||||||
|
if (cachedRelations) {
|
||||||
|
console.log('[loadMultiRelationValues] ✅ Используем предварительно загруженные relations для строки', props.rowId);
|
||||||
|
relations = cachedRelations;
|
||||||
|
} else {
|
||||||
|
console.log('[loadMultiRelationValues] ⚠️ Relations не найдены в кэше, загружаем заново для строки', props.rowId);
|
||||||
|
// Выполняем запросы параллельно
|
||||||
|
const [relationsRes, tableRes] = await Promise.all([
|
||||||
|
fetch(`/api/tables/${props.column.table_id}/row/${props.rowId}/relations`),
|
||||||
|
fetch(`/api/tables/${rel.relatedTableId}`)
|
||||||
|
]);
|
||||||
|
|
||||||
|
[relations, tableData] = await Promise.all([
|
||||||
|
relationsRes.json(),
|
||||||
|
tableRes.json()
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Сохраняем relations в кэш
|
||||||
|
cacheService.setRelationsData(props.rowId, props.column.id, relations);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[loadMultiRelationValues] API response status: 200 relations:', relations);
|
||||||
|
|
||||||
// Приводим все id к строке для корректного сравнения
|
// Приводим все id к строке для корректного сравнения
|
||||||
const relatedRowIds = relations
|
const relatedRowIds = relations
|
||||||
.filter(r => String(r.column_id) === String(props.column.id) && String(r.to_table_id) === String(rel.relatedTableId))
|
.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));
|
.map(r => String(r.to_row_id));
|
||||||
console.log('[loadMultiRelationValues] filtered related row ids:', relatedRowIds);
|
console.log('[loadMultiRelationValues] filtered related row ids:', relatedRowIds);
|
||||||
|
|
||||||
|
// Обновляем значения
|
||||||
editMultiRelationValues.value = relatedRowIds;
|
editMultiRelationValues.value = relatedRowIds;
|
||||||
|
|
||||||
|
// Если tableData не загружена, загружаем её отдельно
|
||||||
|
if (!tableData) {
|
||||||
|
const tableRes = await fetch(`/api/tables/${rel.relatedTableId}`);
|
||||||
|
tableData = await tableRes.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Формируем опции из загруженных данных таблицы
|
||||||
|
const colId = rel.relatedColumnId || (tableData.columns[0] && tableData.columns[0].id);
|
||||||
|
const opts = [];
|
||||||
|
for (const row of tableData.rows) {
|
||||||
|
const cell = tableData.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;
|
||||||
|
|
||||||
// Получаем display-значения
|
// Получаем display-значения
|
||||||
await loadMultiRelationOptions();
|
|
||||||
selectedMultiRelationNames.value = multiRelationOptions.value
|
selectedMultiRelationNames.value = multiRelationOptions.value
|
||||||
.filter(opt => relatedRowIds.includes(String(opt.id)))
|
.filter(opt => relatedRowIds.includes(String(opt.id)))
|
||||||
.map(opt => opt.display);
|
.map(opt => opt.display);
|
||||||
console.log('[loadMultiRelationValues] selectedMultiRelationNames:', selectedMultiRelationNames.value);
|
console.log('[loadMultiRelationValues] selectedMultiRelationNames:', selectedMultiRelationNames.value);
|
||||||
|
|
||||||
|
// Отмечаем, что данные загружены
|
||||||
|
isMultiRelationValuesLoaded = true;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('[loadMultiRelationValues] Error:', e);
|
console.error('[loadMultiRelationValues] Error:', e);
|
||||||
}
|
}
|
||||||
|
}, LOAD_DEBOUNCE_DELAY);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveMultiRelation() {
|
async function saveMultiRelation() {
|
||||||
@@ -484,11 +675,12 @@ async function addTag() {
|
|||||||
newTagName.value = '';
|
newTagName.value = '';
|
||||||
showAddTagInput.value = false;
|
showAddTagInput.value = false;
|
||||||
|
|
||||||
// Обновляем список опций
|
// Обновляем список опций и добавляем тег в выбранные параллельно
|
||||||
await loadMultiRelationOptions();
|
await Promise.all([
|
||||||
|
loadMultiRelationOptions(),
|
||||||
|
Promise.resolve(editMultiRelationValues.value.push(String(newRow.id)))
|
||||||
|
]);
|
||||||
|
|
||||||
// Автоматически добавляем новый тег в выбранные
|
|
||||||
editMultiRelationValues.value.push(String(newRow.id));
|
|
||||||
console.log('[addTag] Тег добавлен в выбранные:', editMultiRelationValues.value);
|
console.log('[addTag] Тег добавлен в выбранные:', editMultiRelationValues.value);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('[addTag] Ошибка при добавлении тега:', e);
|
console.error('[addTag] Ошибка при добавлении тега:', e);
|
||||||
|
|||||||
@@ -160,13 +160,17 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, computed, watch } from 'vue';
|
import { ref, onMounted, computed, watch, onUnmounted } from 'vue';
|
||||||
import tablesService from '../../services/tablesService';
|
import tablesService from '../../services/tablesService';
|
||||||
import TableCell from './TableCell.vue';
|
import TableCell from './TableCell.vue';
|
||||||
import { useAuthContext } from '@/composables/useAuth';
|
import { useAuthContext } from '@/composables/useAuth';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
// Импортируем компоненты Element Plus
|
// Импортируем компоненты Element Plus
|
||||||
import { ElSelect, ElOption, ElButton } from 'element-plus';
|
import { ElSelect, ElOption, ElButton } from 'element-plus';
|
||||||
|
import websocketService from '../../services/websocketService';
|
||||||
|
import cacheService from '../../services/cacheService';
|
||||||
|
let unsubscribeFromTableUpdate = null;
|
||||||
|
|
||||||
const { isAdmin } = useAuthContext();
|
const { isAdmin } = useAuthContext();
|
||||||
const rebuilding = ref(false);
|
const rebuilding = ref(false);
|
||||||
const rebuildStatus = ref(null);
|
const rebuildStatus = ref(null);
|
||||||
@@ -370,18 +374,131 @@ async function fetchFilteredRows() {
|
|||||||
|
|
||||||
// Основная загрузка таблицы
|
// Основная загрузка таблицы
|
||||||
async function fetchTable() {
|
async function fetchTable() {
|
||||||
|
const startTime = Date.now();
|
||||||
|
console.log(`[UserTableView] 🚀 Начало загрузки таблицы ${props.tableId} в ${startTime}`);
|
||||||
|
|
||||||
const data = await tablesService.getTable(props.tableId);
|
const data = await tablesService.getTable(props.tableId);
|
||||||
columns.value = data.columns;
|
columns.value = data.columns;
|
||||||
rows.value = data.rows;
|
rows.value = data.rows;
|
||||||
cellValues.value = data.cellValues;
|
cellValues.value = data.cellValues;
|
||||||
tableMeta.value = { name: data.name, description: data.description };
|
tableMeta.value = { name: data.name, description: data.description };
|
||||||
await updateRelationFilterDefs();
|
|
||||||
await fetchFilteredRows();
|
console.log(`[UserTableView] 📊 Загружено ${rows.value.length} строк, ${columns.value.length} столбцов`);
|
||||||
|
|
||||||
|
// Предварительно загружаем все relations для всех строк параллельно
|
||||||
|
const relationColumns = columns.value.filter(col => col.type === 'multiselect-relation');
|
||||||
|
if (relationColumns.length > 0) {
|
||||||
|
console.log(`[UserTableView] 🔄 Предварительно загружаем relations для ${relationColumns.length} столбцов`);
|
||||||
|
|
||||||
|
const relationPromises = [];
|
||||||
|
for (const row of rows.value) {
|
||||||
|
for (const col of relationColumns) {
|
||||||
|
const promise = fetch(`/api/tables/${col.table_id}/row/${row.id}/relations`)
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(relations => {
|
||||||
|
// Сохраняем в кэш
|
||||||
|
cacheService.setRelationsData(row.id, col.id, relations);
|
||||||
|
return { rowId: row.id, colId: col.id, relations };
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error(`[UserTableView] Ошибка загрузки relations для row:${row.id} col:${col.id}:`, error);
|
||||||
|
return { rowId: row.id, colId: col.id, relations: [] };
|
||||||
|
});
|
||||||
|
relationPromises.push(promise);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ждем загрузки всех relations
|
||||||
|
const results = await Promise.all(relationPromises);
|
||||||
|
console.log(`[UserTableView] ✅ Предварительно загружено ${results.length} relations`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Предварительно загружаем данные связанных таблиц для опций
|
||||||
|
const relatedTableIds = new Set();
|
||||||
|
for (const col of relationColumns) {
|
||||||
|
if (col.options && col.options.relatedTableId) {
|
||||||
|
relatedTableIds.add(col.options.relatedTableId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (relatedTableIds.size > 0) {
|
||||||
|
console.log(`[UserTableView] 🔄 Предварительно загружаем данные ${relatedTableIds.size} связанных таблиц для опций`);
|
||||||
|
|
||||||
|
const tablePromises = Array.from(relatedTableIds).map(tableId =>
|
||||||
|
fetch(`/api/tables/${tableId}`)
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(tableData => {
|
||||||
|
// Сохраняем в кэш с разными ключами для разных столбцов
|
||||||
|
cacheService.setTableData(tableId, 'default', tableData);
|
||||||
|
return { tableId, tableData };
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error(`[UserTableView] Ошибка загрузки таблицы ${tableId}:`, error);
|
||||||
|
return { tableId, tableData: null };
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const tableResults = await Promise.all(tablePromises);
|
||||||
|
console.log(`[UserTableView] ✅ Предварительно загружено ${tableResults.length} связанных таблиц`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Выполняем обновление фильтров и фильтрацию строк параллельно
|
||||||
|
await Promise.all([
|
||||||
|
updateRelationFilterDefs(),
|
||||||
|
fetchFilteredRows()
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Выводим статистику кэша для отладки
|
||||||
|
const cacheStats = cacheService.getStats();
|
||||||
|
console.log('[UserTableView] Статистика кэша после загрузки таблицы:', {
|
||||||
|
tableCacheSize: cacheStats.tableCacheSize,
|
||||||
|
relationsCacheSize: cacheStats.relationsCacheSize,
|
||||||
|
tableCacheKeys: cacheStats.tableCacheKeys,
|
||||||
|
relationsCacheKeys: cacheStats.relationsCacheKeys.slice(0, 5) // Показываем только первые 5 ключей
|
||||||
|
});
|
||||||
|
|
||||||
|
const endTime = Date.now();
|
||||||
|
console.log(`[UserTableView] ✅ Завершена загрузка таблицы ${props.tableId} за ${endTime - startTime}ms`);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateRelationFilterDefs() {
|
async function updateRelationFilterDefs() {
|
||||||
// Для каждого multiselect-relation-столбца формируем опции
|
|
||||||
const defs = [];
|
const defs = [];
|
||||||
|
const relatedTableMap = new Map();
|
||||||
|
|
||||||
|
// Сначала собираем все уникальные relatedTableId и создаем промисы для параллельной загрузки
|
||||||
|
for (const col of columns.value) {
|
||||||
|
if (col.type === 'multiselect-relation' && col.options && col.options.relatedTableId && col.options.relatedColumnId) {
|
||||||
|
const tableId = col.options.relatedTableId;
|
||||||
|
if (!relatedTableMap.has(tableId)) {
|
||||||
|
// Проверяем кэш
|
||||||
|
const cached = cacheService.getTableData(tableId);
|
||||||
|
if (cached) {
|
||||||
|
console.log(`[updateRelationFilterDefs] Используем кэшированные данные таблицы ${tableId}`);
|
||||||
|
relatedTableMap.set(tableId, Promise.resolve(cached));
|
||||||
|
} else {
|
||||||
|
relatedTableMap.set(tableId, tablesService.getTable(tableId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Загружаем все связанные таблицы параллельно
|
||||||
|
const relatedTables = await Promise.all(Array.from(relatedTableMap.values()));
|
||||||
|
|
||||||
|
// Создаем Map для быстрого доступа к загруженным таблицам
|
||||||
|
const tableMap = new Map();
|
||||||
|
let tableIndex = 0;
|
||||||
|
for (const tableId of relatedTableMap.keys()) {
|
||||||
|
const tableData = relatedTables[tableIndex++];
|
||||||
|
tableMap.set(tableId, tableData);
|
||||||
|
|
||||||
|
// Сохраняем в кэш, если это новые данные
|
||||||
|
if (!cacheService.getTableData(tableId)) {
|
||||||
|
cacheService.setTableData(tableId, 'default', tableData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Теперь формируем опции фильтров
|
||||||
for (const col of columns.value) {
|
for (const col of columns.value) {
|
||||||
if (col.type === 'multiselect-relation' && col.options && col.options.relatedTableId && col.options.relatedColumnId) {
|
if (col.type === 'multiselect-relation' && col.options && col.options.relatedTableId && col.options.relatedColumnId) {
|
||||||
// Собираем все уникальные id из этого столбца по всем строкам
|
// Собираем все уникальные id из этого столбца по всем строкам
|
||||||
@@ -391,8 +508,9 @@ async function updateRelationFilterDefs() {
|
|||||||
const arr = parseIfArray(cell ? cell.value : []);
|
const arr = parseIfArray(cell ? cell.value : []);
|
||||||
arr.forEach(val => idsSet.add(val));
|
arr.forEach(val => idsSet.add(val));
|
||||||
}
|
}
|
||||||
// Получаем значения из связанной таблицы
|
|
||||||
const relTable = await tablesService.getTable(col.options.relatedTableId);
|
// Получаем значения из связанной таблицы (уже загружена)
|
||||||
|
const relTable = tableMap.get(col.options.relatedTableId);
|
||||||
const opts = Array.from(idsSet).map(id => {
|
const opts = Array.from(idsSet).map(id => {
|
||||||
const relRow = relTable.rows.find(r => String(r.id) === String(id));
|
const relRow = relTable.rows.find(r => String(r.id) === String(id));
|
||||||
const cell = relTable.cellValues.find(c => c.row_id === (relRow ? relRow.id : id) && c.column_id === col.options.relatedColumnId);
|
const cell = relTable.cellValues.find(c => c.row_id === (relRow ? relRow.id : id) && c.column_id === col.options.relatedColumnId);
|
||||||
@@ -421,6 +539,19 @@ watch([relationFilters], fetchFilteredRows, { deep: true });
|
|||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
fetchTable();
|
fetchTable();
|
||||||
|
// Подписка на WebSocket обновления таблицы
|
||||||
|
unsubscribeFromTableUpdate = websocketService.onTableUpdate(props.tableId, () => {
|
||||||
|
console.log('[UserTableView] Получено событие table-updated, перезагружаем данные');
|
||||||
|
// Очищаем кэш текущей таблицы
|
||||||
|
cacheService.clearTableCache(props.tableId);
|
||||||
|
fetchTable();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (unsubscribeFromTableUpdate) {
|
||||||
|
unsubscribeFromTableUpdate();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Для редактирования ячеек
|
// Для редактирования ячеек
|
||||||
|
|||||||
@@ -14,7 +14,9 @@ import { ref, computed, watch, onMounted, onUnmounted } from 'vue';
|
|||||||
import api from '../api/axios';
|
import api from '../api/axios';
|
||||||
import { getFromStorage, setToStorage, removeFromStorage } from '../utils/storage';
|
import { getFromStorage, setToStorage, removeFromStorage } from '../utils/storage';
|
||||||
import { generateUniqueId } from '../utils/helpers';
|
import { generateUniqueId } from '../utils/helpers';
|
||||||
import websocketService from '../services/websocketService';
|
import websocketModule from '../services/websocketService';
|
||||||
|
|
||||||
|
const { websocketService } = websocketModule;
|
||||||
|
|
||||||
function initGuestId() {
|
function initGuestId() {
|
||||||
let id = getFromStorage('guestId', '');
|
let id = getFromStorage('guestId', '');
|
||||||
@@ -44,6 +46,15 @@ export function useChat(auth) {
|
|||||||
|
|
||||||
const guestId = ref(initGuestId());
|
const guestId = ref(initGuestId());
|
||||||
|
|
||||||
|
// Сохраняем ссылки на callback функции WebSocket для правильной отписки
|
||||||
|
const wsCallbacks = {
|
||||||
|
chatMessage: null,
|
||||||
|
conversationUpdated: null,
|
||||||
|
connected: null,
|
||||||
|
disconnected: null,
|
||||||
|
error: null
|
||||||
|
};
|
||||||
|
|
||||||
const shouldLoadHistory = computed(() => {
|
const shouldLoadHistory = computed(() => {
|
||||||
return auth.isAuthenticated.value || !!guestId.value;
|
return auth.isAuthenticated.value || !!guestId.value;
|
||||||
});
|
});
|
||||||
@@ -423,44 +434,69 @@ export function useChat(auth) {
|
|||||||
console.log('[useChat] Подключение к WebSocket для пользователя:', auth.user.value.id);
|
console.log('[useChat] Подключение к WebSocket для пользователя:', auth.user.value.id);
|
||||||
websocketService.connect(auth.user.value.id);
|
websocketService.connect(auth.user.value.id);
|
||||||
|
|
||||||
// Подписываемся на события
|
// Создаем и сохраняем callback функции
|
||||||
websocketService.on('chat-message', (message) => {
|
wsCallbacks.chatMessage = (message) => {
|
||||||
console.log('[useChat] Получено новое сообщение через WebSocket:', message);
|
console.log('[useChat] Получено новое сообщение через WebSocket:', message);
|
||||||
// Проверяем, что сообщение не дублируется
|
// Проверяем, что сообщение не дублируется
|
||||||
const existingMessage = messages.value.find(m => m.id === message.id);
|
const existingMessage = messages.value.find(m => m.id === message.id);
|
||||||
if (!existingMessage) {
|
if (!existingMessage) {
|
||||||
messages.value.push(message);
|
messages.value.push(message);
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
|
|
||||||
websocketService.on('conversation-updated', (conversationId) => {
|
wsCallbacks.conversationUpdated = (conversationId) => {
|
||||||
console.log('[useChat] Обновление диалога через WebSocket:', conversationId);
|
console.log('[useChat] Обновление диалога через WebSocket:', conversationId);
|
||||||
// Можно добавить логику обновления списка диалогов
|
// Можно добавить логику обновления списка диалогов
|
||||||
});
|
};
|
||||||
|
|
||||||
websocketService.on('connected', () => {
|
wsCallbacks.connected = () => {
|
||||||
console.log('[useChat] WebSocket подключен');
|
console.log('[useChat] WebSocket подключен');
|
||||||
});
|
};
|
||||||
|
|
||||||
websocketService.on('disconnected', () => {
|
wsCallbacks.disconnected = () => {
|
||||||
console.log('[useChat] WebSocket отключен');
|
console.log('[useChat] WebSocket отключен');
|
||||||
});
|
};
|
||||||
|
|
||||||
websocketService.on('error', (error) => {
|
wsCallbacks.error = (error) => {
|
||||||
console.error('[useChat] WebSocket ошибка:', error);
|
console.error('[useChat] WebSocket ошибка:', error);
|
||||||
});
|
};
|
||||||
|
|
||||||
|
// Подписываемся на события
|
||||||
|
websocketService.on('chat-message', wsCallbacks.chatMessage);
|
||||||
|
websocketService.on('conversation-updated', wsCallbacks.conversationUpdated);
|
||||||
|
websocketService.on('connected', wsCallbacks.connected);
|
||||||
|
websocketService.on('disconnected', wsCallbacks.disconnected);
|
||||||
|
websocketService.on('error', wsCallbacks.error);
|
||||||
} else {
|
} else {
|
||||||
console.log('[useChat] WebSocket не подключен: пользователь не аутентифицирован или данные не загружены');
|
console.log('[useChat] WebSocket не подключен: пользователь не аутентифицирован или данные не загружены');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function cleanupWebSocket() {
|
function cleanupWebSocket() {
|
||||||
websocketService.off('chat-message');
|
// Отписываемся от всех событий, передавая те же callback функции
|
||||||
websocketService.off('conversation-updated');
|
if (websocketService) {
|
||||||
websocketService.off('connected');
|
if (wsCallbacks.chatMessage) {
|
||||||
websocketService.off('disconnected');
|
websocketService.off('chat-message', wsCallbacks.chatMessage);
|
||||||
websocketService.off('error');
|
}
|
||||||
|
if (wsCallbacks.conversationUpdated) {
|
||||||
|
websocketService.off('conversation-updated', wsCallbacks.conversationUpdated);
|
||||||
|
}
|
||||||
|
if (wsCallbacks.connected) {
|
||||||
|
websocketService.off('connected', wsCallbacks.connected);
|
||||||
|
}
|
||||||
|
if (wsCallbacks.disconnected) {
|
||||||
|
websocketService.off('disconnected', wsCallbacks.disconnected);
|
||||||
|
}
|
||||||
|
if (wsCallbacks.error) {
|
||||||
|
websocketService.off('error', wsCallbacks.error);
|
||||||
|
}
|
||||||
websocketService.disconnect();
|
websocketService.disconnect();
|
||||||
|
|
||||||
|
// Очищаем ссылки на callback функции
|
||||||
|
Object.keys(wsCallbacks).forEach(key => {
|
||||||
|
wsCallbacks[key] = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Инициализация ---
|
// --- Инициализация ---
|
||||||
|
|||||||
141
frontend/src/composables/useTablesWebSocket.js
Normal file
141
frontend/src/composables/useTablesWebSocket.js
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2024-2025 Тарабанов Александр Викторович
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* This software is proprietary and confidential.
|
||||||
|
* Unauthorized copying, modification, or distribution is prohibited.
|
||||||
|
*
|
||||||
|
* For licensing inquiries: info@hb3-accelerator.com
|
||||||
|
* Website: https://hb3-accelerator.com
|
||||||
|
* GitHub: https://github.com/HB3-ACCELERATOR
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ref, onMounted, onUnmounted } from 'vue';
|
||||||
|
|
||||||
|
export function useTablesWebSocket() {
|
||||||
|
const ws = ref(null);
|
||||||
|
const isConnected = ref(false);
|
||||||
|
const tableUpdateCallbacks = ref(new Map()); // tableId -> callback
|
||||||
|
const tableRelationsUpdateCallbacks = ref(new Map()); // `${tableId}-${rowId}` -> callback
|
||||||
|
|
||||||
|
function connect() {
|
||||||
|
if (ws.value && ws.value.readyState === WebSocket.OPEN) {
|
||||||
|
return; // Уже подключены
|
||||||
|
}
|
||||||
|
|
||||||
|
const wsProtocol = window.location.protocol === 'https:' ? 'wss' : 'ws';
|
||||||
|
ws.value = new WebSocket(`${wsProtocol}://${window.location.host}/ws`);
|
||||||
|
|
||||||
|
ws.value.onopen = () => {
|
||||||
|
console.log('[TablesWebSocket] Соединение установлено');
|
||||||
|
isConnected.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.value.onmessage = (event) => {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(event.data);
|
||||||
|
console.log('[TablesWebSocket] Получено сообщение:', data);
|
||||||
|
|
||||||
|
if (data.type === 'table-updated') {
|
||||||
|
const callbacks = tableUpdateCallbacks.value.get(data.tableId);
|
||||||
|
if (callbacks) {
|
||||||
|
callbacks.forEach(callback => callback(data));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.type === 'table-relations-updated') {
|
||||||
|
const key = `${data.tableId}-${data.rowId}`;
|
||||||
|
const callbacks = tableRelationsUpdateCallbacks.value.get(key);
|
||||||
|
if (callbacks) {
|
||||||
|
callbacks.forEach(callback => callback(data));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[TablesWebSocket] Ошибка обработки сообщения:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.value.onclose = () => {
|
||||||
|
console.log('[TablesWebSocket] Соединение закрыто');
|
||||||
|
isConnected.value = false;
|
||||||
|
// Переподключение через 3 секунды
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!isConnected.value) {
|
||||||
|
connect();
|
||||||
|
}
|
||||||
|
}, 3000);
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.value.onerror = (error) => {
|
||||||
|
console.error('[TablesWebSocket] Ошибка соединения:', error);
|
||||||
|
isConnected.value = false;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function subscribeToTableUpdates(tableId, callback) {
|
||||||
|
if (!tableUpdateCallbacks.value.has(tableId)) {
|
||||||
|
tableUpdateCallbacks.value.set(tableId, []);
|
||||||
|
}
|
||||||
|
tableUpdateCallbacks.value.get(tableId).push(callback);
|
||||||
|
|
||||||
|
// Возвращаем функцию для отписки
|
||||||
|
return () => {
|
||||||
|
const callbacks = tableUpdateCallbacks.value.get(tableId);
|
||||||
|
if (callbacks) {
|
||||||
|
const index = callbacks.indexOf(callback);
|
||||||
|
if (index > -1) {
|
||||||
|
callbacks.splice(index, 1);
|
||||||
|
}
|
||||||
|
if (callbacks.length === 0) {
|
||||||
|
tableUpdateCallbacks.value.delete(tableId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function subscribeToTableRelationsUpdates(tableId, rowId, callback) {
|
||||||
|
const key = `${tableId}-${rowId}`;
|
||||||
|
if (!tableRelationsUpdateCallbacks.value.has(key)) {
|
||||||
|
tableRelationsUpdateCallbacks.value.set(key, []);
|
||||||
|
}
|
||||||
|
tableRelationsUpdateCallbacks.value.get(key).push(callback);
|
||||||
|
|
||||||
|
// Возвращаем функцию для отписки
|
||||||
|
return () => {
|
||||||
|
const callbacks = tableRelationsUpdateCallbacks.value.get(key);
|
||||||
|
if (callbacks) {
|
||||||
|
const index = callbacks.indexOf(callback);
|
||||||
|
if (index > -1) {
|
||||||
|
callbacks.splice(index, 1);
|
||||||
|
}
|
||||||
|
if (callbacks.length === 0) {
|
||||||
|
tableRelationsUpdateCallbacks.value.delete(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function disconnect() {
|
||||||
|
if (ws.value) {
|
||||||
|
ws.value.close();
|
||||||
|
ws.value = null;
|
||||||
|
}
|
||||||
|
isConnected.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
connect();
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
disconnect();
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
isConnected,
|
||||||
|
connect,
|
||||||
|
disconnect,
|
||||||
|
subscribeToTableUpdates,
|
||||||
|
subscribeToTableRelationsUpdates
|
||||||
|
};
|
||||||
|
}
|
||||||
70
frontend/src/composables/useTagsWebSocket.js
Normal file
70
frontend/src/composables/useTagsWebSocket.js
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2024-2025 Тарабанов Александр Викторович
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* This software is proprietary and confidential.
|
||||||
|
* Unauthorized copying, modification, or distribution is prohibited.
|
||||||
|
*
|
||||||
|
* For licensing inquiries: info@hb3-accelerator.com
|
||||||
|
* Website: https://hb3-accelerator.com
|
||||||
|
* GitHub: https://github.com/HB3-ACCELERATOR
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ref, onMounted, onUnmounted } from 'vue';
|
||||||
|
import websocketServiceModule from '../services/websocketService';
|
||||||
|
|
||||||
|
const { websocketService } = websocketServiceModule;
|
||||||
|
|
||||||
|
export function useTagsWebSocket() {
|
||||||
|
const tagsUpdateCallbacks = ref([]);
|
||||||
|
let debounceTimer = null;
|
||||||
|
const DEBOUNCE_DELAY = 1000; // 1 секунда
|
||||||
|
|
||||||
|
function onTagsUpdate(callback) {
|
||||||
|
tagsUpdateCallbacks.value.push(callback);
|
||||||
|
|
||||||
|
// Возвращаем функцию для отписки
|
||||||
|
return () => {
|
||||||
|
const index = tagsUpdateCallbacks.value.indexOf(callback);
|
||||||
|
if (index > -1) {
|
||||||
|
tagsUpdateCallbacks.value.splice(index, 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleTagsUpdate(data) {
|
||||||
|
console.log('🏷️ [useTagsWebSocket] Получено обновление тегов:', data);
|
||||||
|
|
||||||
|
// Очищаем предыдущий таймер
|
||||||
|
if (debounceTimer) {
|
||||||
|
clearTimeout(debounceTimer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Устанавливаем новый таймер для дебаунсинга
|
||||||
|
debounceTimer = setTimeout(() => {
|
||||||
|
console.log('🏷️ [useTagsWebSocket] Выполняем обновление тегов после дебаунсинга');
|
||||||
|
tagsUpdateCallbacks.value.forEach(callback => {
|
||||||
|
try {
|
||||||
|
callback(data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка в callback обновления тегов:', error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, DEBOUNCE_DELAY);
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
websocketService.on('tags-updated', handleTagsUpdate);
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (debounceTimer) {
|
||||||
|
clearTimeout(debounceTimer);
|
||||||
|
}
|
||||||
|
websocketService.off('tags-updated', handleTagsUpdate);
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
onTagsUpdate
|
||||||
|
};
|
||||||
|
}
|
||||||
117
frontend/src/services/cacheService.js
Normal file
117
frontend/src/services/cacheService.js
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
/**
|
||||||
|
* Глобальный сервис кэширования для оптимизации запросов
|
||||||
|
*/
|
||||||
|
|
||||||
|
class CacheService {
|
||||||
|
constructor() {
|
||||||
|
this.tableCache = new Map();
|
||||||
|
this.relationsCache = new Map();
|
||||||
|
this.tableCacheTimeout = 30000; // 30 секунд для таблиц
|
||||||
|
this.relationsCacheTimeout = 10000; // 10 секунд для relations
|
||||||
|
}
|
||||||
|
|
||||||
|
// Кэширование данных таблиц
|
||||||
|
getTableData(tableId, columnId = 'default') {
|
||||||
|
const cacheKey = `${tableId}_${columnId}`;
|
||||||
|
const cached = this.tableCache.get(cacheKey);
|
||||||
|
|
||||||
|
if (cached && Date.now() - cached.timestamp < this.tableCacheTimeout) {
|
||||||
|
console.log(`[CacheService] ✅ КЭШ ПОПАДАНИЕ для таблицы ${tableId} (${cacheKey})`);
|
||||||
|
return cached.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cached) {
|
||||||
|
console.log(`[CacheService] ⏰ Кэш истек для таблицы ${tableId} (${cacheKey})`);
|
||||||
|
} else {
|
||||||
|
console.log(`[CacheService] ❌ Кэш отсутствует для таблицы ${tableId} (${cacheKey})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
setTableData(tableId, columnId, data) {
|
||||||
|
const cacheKey = `${tableId}_${columnId}`;
|
||||||
|
this.tableCache.set(cacheKey, {
|
||||||
|
data,
|
||||||
|
timestamp: Date.now()
|
||||||
|
});
|
||||||
|
console.log(`[CacheService] Сохранены данные таблицы ${tableId} в кэш`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Кэширование relations
|
||||||
|
getRelationsData(rowId, columnId) {
|
||||||
|
const cacheKey = `${rowId}_${columnId}`;
|
||||||
|
const cached = this.relationsCache.get(cacheKey);
|
||||||
|
|
||||||
|
if (cached && Date.now() - cached.timestamp < this.relationsCacheTimeout) {
|
||||||
|
console.log(`[CacheService] Используем кэшированные relations для строки ${rowId}`);
|
||||||
|
return cached.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
setRelationsData(rowId, columnId, data) {
|
||||||
|
const cacheKey = `${rowId}_${columnId}`;
|
||||||
|
this.relationsCache.set(cacheKey, {
|
||||||
|
data,
|
||||||
|
timestamp: Date.now()
|
||||||
|
});
|
||||||
|
console.log(`[CacheService] Сохранены relations строки ${rowId} в кэш`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Очистка кэша
|
||||||
|
clearTableCache(tableId = null) {
|
||||||
|
if (tableId) {
|
||||||
|
// Очищаем кэш для конкретной таблицы
|
||||||
|
for (const [key] of this.tableCache) {
|
||||||
|
if (key.startsWith(`${tableId}_`)) {
|
||||||
|
this.tableCache.delete(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log(`[CacheService] Очищен кэш таблицы ${tableId}`);
|
||||||
|
} else {
|
||||||
|
// Очищаем весь кэш таблиц
|
||||||
|
this.tableCache.clear();
|
||||||
|
console.log('[CacheService] Очищен весь кэш таблиц');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clearRelationsCache(rowId = null) {
|
||||||
|
if (rowId) {
|
||||||
|
// Очищаем кэш для конкретной строки
|
||||||
|
for (const [key] of this.relationsCache) {
|
||||||
|
if (key.startsWith(`${rowId}_`)) {
|
||||||
|
this.relationsCache.delete(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log(`[CacheService] Очищен кэш relations строки ${rowId}`);
|
||||||
|
} else {
|
||||||
|
// Очищаем весь кэш relations
|
||||||
|
this.relationsCache.clear();
|
||||||
|
console.log('[CacheService] Очищен весь кэш relations');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Полная очистка всех кэшей
|
||||||
|
clearAll() {
|
||||||
|
this.tableCache.clear();
|
||||||
|
this.relationsCache.clear();
|
||||||
|
console.log('[CacheService] Очищены все кэши');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получение статистики кэша
|
||||||
|
getStats() {
|
||||||
|
return {
|
||||||
|
tableCacheSize: this.tableCache.size,
|
||||||
|
relationsCacheSize: this.relationsCache.size,
|
||||||
|
tableCacheKeys: Array.from(this.tableCache.keys()),
|
||||||
|
relationsCacheKeys: Array.from(this.relationsCache.keys())
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создаем единственный экземпляр
|
||||||
|
const cacheService = new CacheService();
|
||||||
|
|
||||||
|
export default cacheService;
|
||||||
@@ -126,6 +126,18 @@ class WebSocketService {
|
|||||||
this.emit('contacts-updated');
|
this.emit('contacts-updated');
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'tags-updated':
|
||||||
|
console.log('🏷️ [WebSocket] Обновление тегов клиентов');
|
||||||
|
this.emit('tags-updated');
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'table-updated':
|
||||||
|
console.log('[WebSocket] table-updated:', data.tableId);
|
||||||
|
if (tableUpdateSubscribers[data.tableId]) {
|
||||||
|
tableUpdateSubscribers[data.tableId].forEach(cb => cb(data));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
console.log('❓ [WebSocket] Неизвестный тип сообщения:', data.type);
|
console.log('❓ [WebSocket] Неизвестный тип сообщения:', data.type);
|
||||||
this.emit('unknown-message', data);
|
this.emit('unknown-message', data);
|
||||||
@@ -189,4 +201,21 @@ class WebSocketService {
|
|||||||
// Создаем единственный экземпляр
|
// Создаем единственный экземпляр
|
||||||
const websocketService = new WebSocketService();
|
const websocketService = new WebSocketService();
|
||||||
|
|
||||||
export default websocketService;
|
// Подписчики на обновления таблиц: tableId -> [callback]
|
||||||
|
const tableUpdateSubscribers = {};
|
||||||
|
|
||||||
|
function onTableUpdate(tableId, callback) {
|
||||||
|
if (!tableUpdateSubscribers[tableId]) {
|
||||||
|
tableUpdateSubscribers[tableId] = [];
|
||||||
|
}
|
||||||
|
tableUpdateSubscribers[tableId].push(callback);
|
||||||
|
// Возвращаем функцию для отписки
|
||||||
|
return () => {
|
||||||
|
tableUpdateSubscribers[tableId] = tableUpdateSubscribers[tableId].filter(cb => cb !== callback);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
websocketService,
|
||||||
|
onTableUpdate,
|
||||||
|
};
|
||||||
@@ -150,7 +150,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onMounted, watch } from 'vue';
|
import { ref, computed, onMounted, watch, onUnmounted } from 'vue';
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
import BaseLayout from '../../components/BaseLayout.vue';
|
import BaseLayout from '../../components/BaseLayout.vue';
|
||||||
import Message from '../../components/Message.vue';
|
import Message from '../../components/Message.vue';
|
||||||
@@ -160,6 +160,7 @@ import messagesService from '../../services/messagesService.js';
|
|||||||
import { useAuthContext } from '@/composables/useAuth';
|
import { useAuthContext } from '@/composables/useAuth';
|
||||||
import { ElMessageBox } from 'element-plus';
|
import { ElMessageBox } from 'element-plus';
|
||||||
import tablesService from '../../services/tablesService';
|
import tablesService from '../../services/tablesService';
|
||||||
|
import { useTagsWebSocket } from '../../composables/useTagsWebSocket';
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -187,10 +188,15 @@ const conversationId = ref(null);
|
|||||||
// id таблицы тегов (будет найден или создан)
|
// id таблицы тегов (будет найден или создан)
|
||||||
const tagsTableId = ref(null);
|
const tagsTableId = ref(null);
|
||||||
|
|
||||||
|
// WebSocket для тегов
|
||||||
|
const { onTagsUpdate } = useTagsWebSocket();
|
||||||
|
let unsubscribeFromTags = null;
|
||||||
|
|
||||||
async function ensureTagsTable() {
|
async function ensureTagsTable() {
|
||||||
// Получаем все пользовательские таблицы
|
// Получаем все пользовательские таблицы
|
||||||
const tables = await tablesService.getTables();
|
const tables = await tablesService.getTables();
|
||||||
let tagsTable = tables.find(t => t.name === 'Теги клиентов');
|
let tagsTable = tables.find(t => t.name === 'Теги клиентов');
|
||||||
|
|
||||||
if (!tagsTable) {
|
if (!tagsTable) {
|
||||||
// Если таблицы нет — создаём
|
// Если таблицы нет — создаём
|
||||||
tagsTable = await tablesService.createTable({
|
tagsTable = await tablesService.createTable({
|
||||||
@@ -198,17 +204,28 @@ async function ensureTagsTable() {
|
|||||||
description: 'Справочник тегов для контактов',
|
description: 'Справочник тегов для контактов',
|
||||||
isRagSourceId: 2 // не источник для RAG по умолчанию
|
isRagSourceId: 2 // не источник для RAG по умолчанию
|
||||||
});
|
});
|
||||||
// Добавляем столбцы
|
|
||||||
await tablesService.addColumn(tagsTable.id, { name: 'Название', type: 'text' });
|
// Добавляем столбцы параллельно
|
||||||
await tablesService.addColumn(tagsTable.id, { name: 'Описание', type: 'text' });
|
await Promise.all([
|
||||||
|
tablesService.addColumn(tagsTable.id, { name: 'Название', type: 'text' }),
|
||||||
|
tablesService.addColumn(tagsTable.id, { name: 'Описание', type: 'text' })
|
||||||
|
]);
|
||||||
} else {
|
} else {
|
||||||
// Проверяем, есть ли нужные столбцы, если таблица уже была создана
|
// Проверяем, есть ли нужные столбцы, если таблица уже была создана
|
||||||
const table = await tablesService.getTable(tagsTable.id);
|
const table = await tablesService.getTable(tagsTable.id);
|
||||||
const hasName = table.columns.some(col => col.name === 'Название');
|
const hasName = table.columns.some(col => col.name === 'Название');
|
||||||
const hasDesc = table.columns.some(col => col.name === 'Описание');
|
const hasDesc = table.columns.some(col => col.name === 'Описание');
|
||||||
if (!hasName) await tablesService.addColumn(tagsTable.id, { name: 'Название', type: 'text' });
|
|
||||||
if (!hasDesc) await tablesService.addColumn(tagsTable.id, { name: 'Описание', type: 'text' });
|
// Добавляем недостающие столбцы параллельно
|
||||||
|
const addColumnPromises = [];
|
||||||
|
if (!hasName) addColumnPromises.push(tablesService.addColumn(tagsTable.id, { name: 'Название', type: 'text' }));
|
||||||
|
if (!hasDesc) addColumnPromises.push(tablesService.addColumn(tagsTable.id, { name: 'Описание', type: 'text' }));
|
||||||
|
|
||||||
|
if (addColumnPromises.length > 0) {
|
||||||
|
await Promise.all(addColumnPromises);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
tagsTableId.value = tagsTable.id;
|
tagsTableId.value = tagsTable.id;
|
||||||
return tagsTable.id;
|
return tagsTable.id;
|
||||||
}
|
}
|
||||||
@@ -598,6 +615,20 @@ onMounted(async () => {
|
|||||||
await reloadContact();
|
await reloadContact();
|
||||||
await loadUserTags();
|
await loadUserTags();
|
||||||
await loadMessages();
|
await loadMessages();
|
||||||
|
|
||||||
|
// Подписываемся на обновления тегов
|
||||||
|
unsubscribeFromTags = onTagsUpdate(async () => {
|
||||||
|
console.log('[ContactDetailsView] Получено обновление тегов, перезагружаем списки тегов');
|
||||||
|
await loadAllTags();
|
||||||
|
await loadUserTags();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
// Отписываемся от WebSocket при размонтировании
|
||||||
|
if (unsubscribeFromTags) {
|
||||||
|
unsubscribeFromTags();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
watch(userId, async () => {
|
watch(userId, async () => {
|
||||||
await reloadContact();
|
await reloadContact();
|
||||||
|
|||||||
Reference in New Issue
Block a user