ваше сообщение коммита
This commit is contained in:
@@ -163,7 +163,7 @@ app.use((req, res, next) => {
|
|||||||
app.use('/api/tables', tablesRoutes); // ДОЛЖНО БЫТЬ ВЫШЕ!
|
app.use('/api/tables', tablesRoutes); // ДОЛЖНО БЫТЬ ВЫШЕ!
|
||||||
// app.use('/api', identitiesRoutes);
|
// app.use('/api', identitiesRoutes);
|
||||||
app.use('/api/auth', authRoutes);
|
app.use('/api/auth', authRoutes);
|
||||||
app.use('/api/users/:userId/tags', userTagsRoutes);
|
app.use('/api/users', userTagsRoutes);
|
||||||
app.use('/api/users', usersRoutes);
|
app.use('/api/users', usersRoutes);
|
||||||
app.use('/api/chat', chatRoutes);
|
app.use('/api/chat', chatRoutes);
|
||||||
app.use('/api/admin', adminRoutes);
|
app.use('/api/admin', adminRoutes);
|
||||||
|
|||||||
@@ -5,15 +5,23 @@ const db = require('../db');
|
|||||||
// GET /api/messages?userId=123
|
// GET /api/messages?userId=123
|
||||||
router.get('/', async (req, res) => {
|
router.get('/', async (req, res) => {
|
||||||
const userId = req.query.userId;
|
const userId = req.query.userId;
|
||||||
if (!userId) return res.status(400).json({ error: 'userId required' });
|
|
||||||
try {
|
try {
|
||||||
const result = await db.getQuery()(
|
let result;
|
||||||
|
if (userId) {
|
||||||
|
result = await db.getQuery()(
|
||||||
`SELECT id, user_id, sender_type, content, channel, role, direction, created_at, attachment_filename, attachment_mimetype, attachment_size, attachment_data, metadata
|
`SELECT id, user_id, sender_type, content, channel, role, direction, created_at, attachment_filename, attachment_mimetype, attachment_size, attachment_data, metadata
|
||||||
FROM messages
|
FROM messages
|
||||||
WHERE user_id = $1
|
WHERE user_id = $1
|
||||||
ORDER BY created_at ASC`,
|
ORDER BY created_at ASC`,
|
||||||
[userId]
|
[userId]
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
result = await db.getQuery()(
|
||||||
|
`SELECT id, user_id, sender_type, content, channel, role, direction, created_at, attachment_filename, attachment_mimetype, attachment_size, attachment_data, metadata
|
||||||
|
FROM messages
|
||||||
|
ORDER BY created_at ASC`
|
||||||
|
);
|
||||||
|
}
|
||||||
res.json(result.rows);
|
res.json(result.rows);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).json({ error: 'DB error', details: e.message });
|
res.status(500).json({ error: 'DB error', details: e.message });
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ const { app, nonceStore } = require('./app');
|
|||||||
const http = require('http');
|
const http = require('http');
|
||||||
const { initWSS } = require('./wsHub');
|
const { initWSS } = require('./wsHub');
|
||||||
const logger = require('./utils/logger');
|
const logger = require('./utils/logger');
|
||||||
|
const { getBot } = require('./services/telegramBot');
|
||||||
|
const EmailBotService = require('./services/emailBot');
|
||||||
|
|
||||||
const PORT = process.env.PORT || 8000;
|
const PORT = process.env.PORT || 8000;
|
||||||
|
|
||||||
@@ -14,9 +16,26 @@ console.log('Используемый порт:', process.env.PORT || 8000);
|
|||||||
async function initServices() {
|
async function initServices() {
|
||||||
try {
|
try {
|
||||||
console.log('Инициализация сервисов...');
|
console.log('Инициализация сервисов...');
|
||||||
// Здесь может быть инициализация ботов, email-сервисов и т.д.
|
console.log('[initServices] Запуск Email-бота...');
|
||||||
// ...
|
console.log('[initServices] Создаю экземпляр EmailBotService...');
|
||||||
console.log('Все сервисы успешно инициализированы');
|
let emailBot;
|
||||||
|
try {
|
||||||
|
emailBot = new EmailBotService();
|
||||||
|
console.log('[initServices] Экземпляр EmailBotService создан');
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[initServices] Ошибка при создании экземпляра EmailBotService:', err);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
console.log('[initServices] Перед вызовом emailBot.start()');
|
||||||
|
try {
|
||||||
|
await emailBot.start();
|
||||||
|
console.log('[initServices] Email-бот успешно запущен');
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[initServices] Ошибка при запуске emailBot:', err);
|
||||||
|
}
|
||||||
|
console.log('[initServices] Запуск Telegram-бота...');
|
||||||
|
await getBot();
|
||||||
|
console.log('[initServices] Telegram-бот успешно запущен');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Ошибка при инициализации сервисов:', error);
|
console.error('Ошибка при инициализации сервисов:', error);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
console.log('[ai-assistant] loaded');
|
||||||
|
|
||||||
const { ChatOllama } = require('@langchain/ollama');
|
const { ChatOllama } = require('@langchain/ollama');
|
||||||
const { HNSWLib } = require('@langchain/community/vectorstores/hnswlib');
|
const { HNSWLib } = require('@langchain/community/vectorstores/hnswlib');
|
||||||
const { OpenAIEmbeddings } = require('@langchain/openai');
|
const { OpenAIEmbeddings } = require('@langchain/openai');
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
const db = require('../db');
|
const db = require('../db');
|
||||||
const OpenAI = require('openai');
|
const OpenAI = require('openai');
|
||||||
const Anthropic = require('@anthropic-ai/sdk');
|
const Anthropic = require('@anthropic-ai/sdk');
|
||||||
const { GoogleGenAI } = require('@google/genai');
|
|
||||||
|
|
||||||
const TABLE = 'ai_providers_settings';
|
const TABLE = 'ai_providers_settings';
|
||||||
|
|
||||||
@@ -48,6 +47,7 @@ async function getProviderModels(provider, { api_key, base_url } = {}) {
|
|||||||
return res.data ? res.data.map(m => ({ id: m.id, ...m })) : [];
|
return res.data ? res.data.map(m => ({ id: m.id, ...m })) : [];
|
||||||
}
|
}
|
||||||
if (provider === 'google') {
|
if (provider === 'google') {
|
||||||
|
const { GoogleGenAI } = await import('@google/genai');
|
||||||
const ai = new GoogleGenAI({ apiKey: api_key, baseUrl: base_url });
|
const ai = new GoogleGenAI({ apiKey: api_key, baseUrl: base_url });
|
||||||
const pager = await ai.models.list();
|
const pager = await ai.models.list();
|
||||||
const models = [];
|
const models = [];
|
||||||
@@ -79,6 +79,7 @@ async function verifyProviderKey(provider, { api_key, base_url } = {}) {
|
|||||||
return { success: true };
|
return { success: true };
|
||||||
}
|
}
|
||||||
if (provider === 'google') {
|
if (provider === 'google') {
|
||||||
|
const { GoogleGenAI } = await import('@google/genai');
|
||||||
const ai = new GoogleGenAI({ apiKey: api_key, baseUrl: base_url });
|
const ai = new GoogleGenAI({ apiKey: api_key, baseUrl: base_url });
|
||||||
const pager = await ai.models.list();
|
const pager = await ai.models.list();
|
||||||
for await (const _ of pager) {
|
for await (const _ of pager) {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
console.log('[EmailBot] emailBot.js loaded');
|
||||||
const db = require('../db');
|
const db = require('../db');
|
||||||
const nodemailer = require('nodemailer');
|
const nodemailer = require('nodemailer');
|
||||||
const Imap = require('imap');
|
const Imap = require('imap');
|
||||||
@@ -9,6 +10,10 @@ const identityService = require('./identity-service');
|
|||||||
const aiAssistant = require('./ai-assistant');
|
const aiAssistant = require('./ai-assistant');
|
||||||
|
|
||||||
class EmailBotService {
|
class EmailBotService {
|
||||||
|
constructor() {
|
||||||
|
console.log('[EmailBot] constructor called');
|
||||||
|
}
|
||||||
|
|
||||||
async getSettingsFromDb() {
|
async getSettingsFromDb() {
|
||||||
const { rows } = await db.getQuery()('SELECT * FROM email_settings ORDER BY id LIMIT 1');
|
const { rows } = await db.getQuery()('SELECT * FROM email_settings ORDER BY id LIMIT 1');
|
||||||
if (!rows.length) throw new Error('Email settings not found in DB');
|
if (!rows.length) throw new Error('Email settings not found in DB');
|
||||||
@@ -46,6 +51,7 @@ class EmailBotService {
|
|||||||
idleInterval: 300000,
|
idleInterval: 300000,
|
||||||
forceNoop: true,
|
forceNoop: true,
|
||||||
},
|
},
|
||||||
|
connTimeout: 30000, // 30 секунд
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -228,6 +234,8 @@ class EmailBotService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async start() {
|
async start() {
|
||||||
|
try {
|
||||||
|
console.log('[EmailBot] start() called');
|
||||||
logger.info('[EmailBot] start() called');
|
logger.info('[EmailBot] start() called');
|
||||||
const imapConfig = await this.getImapConfig();
|
const imapConfig = await this.getImapConfig();
|
||||||
// Логируем IMAP-конфиг (без пароля)
|
// Логируем IMAP-конфиг (без пароля)
|
||||||
@@ -276,7 +284,13 @@ class EmailBotService {
|
|||||||
this.imap.connect();
|
this.imap.connect();
|
||||||
};
|
};
|
||||||
tryConnect();
|
tryConnect();
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[EmailBot] Ошибка при старте:', err);
|
||||||
|
logger.error('[EmailBot] Ошибка при старте:', err);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('[EmailBot] module.exports = EmailBotService');
|
||||||
module.exports = EmailBotService;
|
module.exports = EmailBotService;
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
|
console.log('[identity-service] loaded');
|
||||||
|
|
||||||
const db = require('../db');
|
const db = require('../db');
|
||||||
const logger = require('../utils/logger');
|
const logger = require('../utils/logger');
|
||||||
const { getLinkedWallet } = require('./wallet-service');
|
const { getLinkedWallet } = require('./wallet-service');
|
||||||
const { checkAdminRole } = require('./admin-role');
|
const { checkAdminRole } = require('./admin-role');
|
||||||
|
const { broadcastContactsUpdate } = require('../wsHub');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Сервис для работы с идентификаторами пользователей
|
* Сервис для работы с идентификаторами пользователей
|
||||||
@@ -541,6 +544,7 @@ class IdentityService {
|
|||||||
await this.saveIdentity(userId, provider, providerId, true);
|
await this.saveIdentity(userId, provider, providerId, true);
|
||||||
user = { id: userId, role: 'user' };
|
user = { id: userId, role: 'user' };
|
||||||
isNew = true;
|
isNew = true;
|
||||||
|
broadcastContactsUpdate();
|
||||||
}
|
}
|
||||||
// Проверяем связь с кошельком
|
// Проверяем связь с кошельком
|
||||||
const wallet = await getLinkedWallet(user.id);
|
const wallet = await getLinkedWallet(user.id);
|
||||||
|
|||||||
@@ -332,8 +332,9 @@ async function getBot() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Запускаем бота
|
// Запуск бота
|
||||||
await botInstance.launch();
|
await botInstance.launch();
|
||||||
|
logger.info('[TelegramBot] Бот запущен');
|
||||||
}
|
}
|
||||||
|
|
||||||
return botInstance;
|
return botInstance;
|
||||||
|
|||||||
@@ -53,8 +53,8 @@ function showDetails(contact) {
|
|||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
box-shadow: 0 4px 32px rgba(0,0,0,0.12);
|
box-shadow: 0 4px 32px rgba(0,0,0,0.12);
|
||||||
padding: 32px 24px 24px 24px;
|
padding: 32px 24px 24px 24px;
|
||||||
max-width: 950px;
|
width: 100%;
|
||||||
margin: 40px auto;
|
margin-top: 40px;
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
}
|
}
|
||||||
@@ -65,6 +65,9 @@ function showDetails(contact) {
|
|||||||
margin-bottom: 24px;
|
margin-bottom: 24px;
|
||||||
}
|
}
|
||||||
.close-btn {
|
.close-btn {
|
||||||
|
position: absolute;
|
||||||
|
top: 18px;
|
||||||
|
right: 18px;
|
||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
font-size: 2rem;
|
font-size: 2rem;
|
||||||
|
|||||||
@@ -357,8 +357,8 @@ function uninstallModule(module) {
|
|||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
box-shadow: 0 4px 32px rgba(0,0,0,0.12);
|
box-shadow: 0 4px 32px rgba(0,0,0,0.12);
|
||||||
padding: 32px 24px 24px 24px;
|
padding: 32px 24px 24px 24px;
|
||||||
max-width: 950px;
|
width: 100%;
|
||||||
margin: 40px auto;
|
margin-top: 40px;
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
}
|
}
|
||||||
|
|||||||
47
frontend/src/components/MessagesTable.vue
Normal file
47
frontend/src/components/MessagesTable.vue
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
<template>
|
||||||
|
<div class="messages-table-modal">
|
||||||
|
<div class="messages-table-header">
|
||||||
|
<h2>Новые сообщения</h2>
|
||||||
|
</div>
|
||||||
|
<div v-if="!messages.length" class="empty">Нет новых сообщений</div>
|
||||||
|
<div v-else class="messages-list">
|
||||||
|
<Message v-for="msg in messages" :key="msg.id" :message="msg" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { defineProps } from 'vue';
|
||||||
|
import Message from './Message.vue';
|
||||||
|
const props = defineProps({
|
||||||
|
messages: { type: Array, required: true }
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.messages-table-modal {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 16px;
|
||||||
|
box-shadow: 0 4px 32px rgba(0,0,0,0.12);
|
||||||
|
padding: 32px 24px 24px 24px;
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 40px;
|
||||||
|
position: relative;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
.messages-table-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
.empty {
|
||||||
|
color: #888;
|
||||||
|
text-align: center;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
.messages-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 18px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -402,15 +402,15 @@ h3 {
|
|||||||
/* Медиа-запросы для адаптивности */
|
/* Медиа-запросы для адаптивности */
|
||||||
@media screen and (min-width: 1200px) {
|
@media screen and (min-width: 1200px) {
|
||||||
.wallet-sidebar {
|
.wallet-sidebar {
|
||||||
width: 30%;
|
width: 420px;
|
||||||
max-width: 350px;
|
max-width: 420px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (min-width: 769px) and (max-width: 1199px) {
|
@media screen and (min-width: 769px) and (max-width: 1199px) {
|
||||||
.wallet-sidebar {
|
.wallet-sidebar {
|
||||||
width: 40%;
|
width: 350px;
|
||||||
max-width: 320px;
|
max-width: 350px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
285
frontend/src/components/tables/TagsTableView.vue
Normal file
285
frontend/src/components/tables/TagsTableView.vue
Normal file
@@ -0,0 +1,285 @@
|
|||||||
|
<template>
|
||||||
|
<div class="tags-table-wrapper">
|
||||||
|
<div class="tableview-header-row">
|
||||||
|
<button class="nav-btn" @click="goToTables">Таблицы</button>
|
||||||
|
<button class="nav-btn" @click="goToCreate">Создать таблицу</button>
|
||||||
|
<button class="close-btn" @click="closeTable">Закрыть</button>
|
||||||
|
<button class="action-btn" disabled>Редактировать</button>
|
||||||
|
<button class="danger-btn" disabled>Удалить</button>
|
||||||
|
</div>
|
||||||
|
<div class="tags-header-row">
|
||||||
|
<h3>Теги</h3>
|
||||||
|
</div>
|
||||||
|
<table class="tags-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Название</th>
|
||||||
|
<th>Описание</th>
|
||||||
|
<th style="width:110px;">Действия</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="tag in tags" :key="tag.id">
|
||||||
|
<td v-if="editId !== tag.id">{{ tag.name }}</td>
|
||||||
|
<td v-else><input v-model="editName" class="edit-input" /></td>
|
||||||
|
|
||||||
|
<td v-if="editId !== tag.id">{{ tag.description || '—' }}</td>
|
||||||
|
<td v-else><input v-model="editDescription" class="edit-input" /></td>
|
||||||
|
|
||||||
|
<td>
|
||||||
|
<template v-if="editId === tag.id">
|
||||||
|
<button class="save-btn" @click="saveEdit(tag)">Сохранить</button>
|
||||||
|
<button class="cancel-btn" @click="cancelEdit">Отмена</button>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<button class="edit-btn" @click="startEdit(tag)" title="Редактировать">✏️</button>
|
||||||
|
<button class="delete-btn" @click="deleteTag(tag)" title="Удалить">🗑</button>
|
||||||
|
</template>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-if="tags.length === 0">
|
||||||
|
<td colspan="3" style="text-align:center; color:#888;">Нет тегов</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td></td>
|
||||||
|
<td></td>
|
||||||
|
<td style="text-align:center;">
|
||||||
|
<button class="edit-btn" @click="showAddTagModal = true" title="Добавить тег">➕</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div v-if="showAddTagModal" class="modal-backdrop">
|
||||||
|
<div class="modal">
|
||||||
|
<h4>Добавить тег</h4>
|
||||||
|
<input v-model="newTagName" class="edit-input" placeholder="Название" />
|
||||||
|
<input v-model="newTagDescription" class="edit-input" placeholder="Описание" style="margin-top:0.7em;" />
|
||||||
|
<div class="modal-actions">
|
||||||
|
<button class="save-btn" @click="createTag">Создать</button>
|
||||||
|
<button class="cancel-btn" @click="showAddTagModal = false">Отмена</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted } from 'vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
const tags = ref([]);
|
||||||
|
const editId = ref(null);
|
||||||
|
const editName = ref('');
|
||||||
|
const editDescription = ref('');
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const showAddTagModal = ref(false);
|
||||||
|
const newTagName = ref('');
|
||||||
|
const newTagDescription = ref('');
|
||||||
|
|
||||||
|
function goToTables() {
|
||||||
|
router.push({ name: 'tables-list' });
|
||||||
|
}
|
||||||
|
function goToCreate() {
|
||||||
|
router.push({ name: 'create-table' });
|
||||||
|
}
|
||||||
|
function closeTable() {
|
||||||
|
if (window.history.length > 1) {
|
||||||
|
router.back();
|
||||||
|
} else {
|
||||||
|
router.push({ name: 'home' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadTags() {
|
||||||
|
const res = await fetch('/api/tags');
|
||||||
|
tags.value = await res.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
function startEdit(tag) {
|
||||||
|
editId.value = tag.id;
|
||||||
|
editName.value = tag.name;
|
||||||
|
editDescription.value = tag.description || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancelEdit() {
|
||||||
|
editId.value = null;
|
||||||
|
editName.value = '';
|
||||||
|
editDescription.value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveEdit(tag) {
|
||||||
|
await fetch(`/api/tags/${tag.id}`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ name: editName.value, description: editDescription.value })
|
||||||
|
});
|
||||||
|
await loadTags();
|
||||||
|
cancelEdit();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteTag(tag) {
|
||||||
|
if (!confirm(`Удалить тег "${tag.name}"?`)) return;
|
||||||
|
await fetch(`/api/tags/${tag.id}`, { method: 'DELETE' });
|
||||||
|
await loadTags();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createTag() {
|
||||||
|
if (!newTagName.value.trim()) return;
|
||||||
|
await fetch('/api/tags', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ name: newTagName.value, description: newTagDescription.value })
|
||||||
|
});
|
||||||
|
newTagName.value = '';
|
||||||
|
newTagDescription.value = '';
|
||||||
|
showAddTagModal.value = false;
|
||||||
|
await loadTags();
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(loadTags);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.tableview-header-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-items: center;
|
||||||
|
margin: 1.2em 0 0.5em 0;
|
||||||
|
}
|
||||||
|
.tags-header-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 1em;
|
||||||
|
}
|
||||||
|
.add-plus-btn, .edit-btn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1.1em;
|
||||||
|
margin-right: 0.5em;
|
||||||
|
color: #2ecc40;
|
||||||
|
transition: color 0.2s;
|
||||||
|
}
|
||||||
|
.add-plus-btn:hover, .edit-btn:hover, .save-btn:hover {
|
||||||
|
color: #138496;
|
||||||
|
}
|
||||||
|
.close-btn {
|
||||||
|
background: #ff4d4f;
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 0.5em 1.2em;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1em;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
.close-btn:hover {
|
||||||
|
background: #d9363e;
|
||||||
|
}
|
||||||
|
.action-btn {
|
||||||
|
background: #2ecc40;
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 0.5em 1.2em;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1em;
|
||||||
|
margin-left: 0.7em;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
.action-btn:hover {
|
||||||
|
background: #27ae38;
|
||||||
|
}
|
||||||
|
.danger-btn {
|
||||||
|
background: #ff4d4f;
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 0.5em 1.2em;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1em;
|
||||||
|
margin-left: 0.7em;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
.danger-btn:hover {
|
||||||
|
background: #d9363e;
|
||||||
|
}
|
||||||
|
.nav-btn {
|
||||||
|
background: #eaeaea;
|
||||||
|
color: #333;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 0.5em 1.2em;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1em;
|
||||||
|
transition: background 0.2s;
|
||||||
|
margin-right: 0.7em;
|
||||||
|
}
|
||||||
|
.nav-btn:hover {
|
||||||
|
background: #d5d5d5;
|
||||||
|
}
|
||||||
|
.tags-table-wrapper {
|
||||||
|
margin: 2em 0;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.04);
|
||||||
|
padding: 1.5em 1em;
|
||||||
|
}
|
||||||
|
.tags-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
.tags-table th, .tags-table td {
|
||||||
|
border: 1px solid #ececec;
|
||||||
|
padding: 0.6em 1em;
|
||||||
|
font-size: 1.05em;
|
||||||
|
}
|
||||||
|
.tags-table th {
|
||||||
|
background: #f7f7f7;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.delete-btn {
|
||||||
|
color: #dc3545;
|
||||||
|
}
|
||||||
|
.cancel-btn {
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
.edit-input {
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 4px 10px;
|
||||||
|
font-size: 1em;
|
||||||
|
min-width: 120px;
|
||||||
|
}
|
||||||
|
/* Модалка */
|
||||||
|
.modal-backdrop {
|
||||||
|
position: fixed;
|
||||||
|
top: 0; left: 0; right: 0; bottom: 0;
|
||||||
|
background: rgba(0,0,0,0.18);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
.modal {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 2em 1.5em;
|
||||||
|
box-shadow: 0 2px 16px rgba(0,0,0,0.13);
|
||||||
|
min-width: 260px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.7em;
|
||||||
|
}
|
||||||
|
.modal-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 1em;
|
||||||
|
margin-top: 1em;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,63 +1,33 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="tables-container">
|
<div class="tables-container">
|
||||||
<header class="tables-header">
|
<header class="tables-header">
|
||||||
<!-- <h2>Пользовательские таблицы</h2> -->
|
|
||||||
<button class="create-btn" @click="createTable">Создать таблицу</button>
|
<button class="create-btn" @click="createTable">Создать таблицу</button>
|
||||||
</header>
|
</header>
|
||||||
<div class="tables-list">
|
<ul class="tables-list-simple">
|
||||||
<div
|
<!-- Системная таблица tags -->
|
||||||
v-for="table in tables"
|
<li>
|
||||||
:key="table.id"
|
<button class="table-link" @click="goToTagsTable">Теги (tags)</button>
|
||||||
class="table-card"
|
</li>
|
||||||
:class="{ selected: table.id === props.selectedTableId }"
|
<!-- Пользовательские таблицы -->
|
||||||
@click="selectTable(table)"
|
<li v-for="table in tables" :key="table.id">
|
||||||
>
|
<button class="table-link" @click="selectTable(table)">{{ table.name }}</button>
|
||||||
<div class="table-info">
|
</li>
|
||||||
<div class="table-title">{{ table.name }}</div>
|
<li v-if="!tables.length" class="empty-state">
|
||||||
<div class="table-desc">{{ table.description }}</div>
|
|
||||||
</div>
|
|
||||||
<div class="table-actions">
|
|
||||||
<button @click.stop="renameTable(table)">Переименовать</button>
|
|
||||||
<button class="danger" @click.stop="confirmDelete(table)">Удалить</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-if="!tables.length" class="empty-state">
|
|
||||||
<span>Нет таблиц. Создайте первую!</span>
|
<span>Нет таблиц. Создайте первую!</span>
|
||||||
</div>
|
</li>
|
||||||
</div>
|
</ul>
|
||||||
<div v-if="showDeleteModal" class="modal-backdrop">
|
|
||||||
<div class="modal">
|
|
||||||
<p>Удалить таблицу <b>{{ selectedTable?.name }}</b>?</p>
|
|
||||||
<div class="modal-actions">
|
|
||||||
<button class="danger" @click="deleteTable(selectedTable)">Удалить</button>
|
|
||||||
<button @click="showDeleteModal = false">Отмена</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<UserTableView
|
|
||||||
v-if="props.selectedTableId"
|
|
||||||
:table-id="props.selectedTableId"
|
|
||||||
@close="emit('update:selected-table-id', null)"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, watch, nextTick } from 'vue';
|
import { ref, onMounted } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter, useRoute } from 'vue-router';
|
||||||
import UserTableView from './UserTableView.vue';
|
|
||||||
import tablesService from '../../services/tablesService';
|
import tablesService from '../../services/tablesService';
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
selectedTableId: Number
|
|
||||||
});
|
|
||||||
const emit = defineEmits(['update:selected-table-id']);
|
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const route = useRoute();
|
||||||
|
|
||||||
const tables = ref([]);
|
const tables = ref([]);
|
||||||
const showDeleteModal = ref(false);
|
|
||||||
const selectedTable = ref(null);
|
|
||||||
|
|
||||||
async function fetchTables() {
|
async function fetchTables() {
|
||||||
tables.value = await tablesService.getTables();
|
tables.value = await tablesService.getTables();
|
||||||
@@ -70,38 +40,25 @@ function selectTable(table) {
|
|||||||
function createTable() {
|
function createTable() {
|
||||||
router.push({ name: 'create-table' });
|
router.push({ name: 'create-table' });
|
||||||
}
|
}
|
||||||
|
function goToTagsTable() {
|
||||||
function renameTable(table) {
|
router.push({ name: 'tags-table-view' });
|
||||||
const name = prompt('Новое имя', table.name);
|
|
||||||
if (name && name !== table.name) {
|
|
||||||
tablesService.updateTable(table.id, { name }).then(fetchTables);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function confirmDelete(table) {
|
|
||||||
selectedTable.value = table;
|
|
||||||
showDeleteModal.value = true;
|
|
||||||
}
|
|
||||||
function deleteTable(table) {
|
|
||||||
tablesService.deleteTable(table.id).then(() => {
|
|
||||||
showDeleteModal.value = false;
|
|
||||||
fetchTables();
|
|
||||||
if (props.selectedTableId === table.id) emit('update:selected-table-id', null);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.tables-container {
|
.tables-container {
|
||||||
max-width: 600px;
|
/* max-width: 600px; */
|
||||||
margin: 2rem auto;
|
/* margin: 2rem auto; */
|
||||||
background: #fff;
|
margin-top: 2rem;
|
||||||
border-radius: 18px;
|
margin-left: 0;
|
||||||
box-shadow: 0 2px 16px rgba(0,0,0,0.07);
|
padding: 2rem 1.5rem 2rem 1.5rem;
|
||||||
padding: 2rem 1.5rem;
|
/* background: #fff; */
|
||||||
|
/* border-radius: 18px; */
|
||||||
|
/* box-shadow: 0 2px 16px rgba(0,0,0,0.07); */
|
||||||
}
|
}
|
||||||
.tables-header {
|
.tables-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: flex-end;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 1.5rem;
|
margin-bottom: 1.5rem;
|
||||||
}
|
}
|
||||||
@@ -118,62 +75,37 @@ function deleteTable(table) {
|
|||||||
.create-btn:hover {
|
.create-btn:hover {
|
||||||
background: #27ae38;
|
background: #27ae38;
|
||||||
}
|
}
|
||||||
.tables-list {
|
.tables-list-simple {
|
||||||
display: flex;
|
list-style: none;
|
||||||
flex-direction: column;
|
padding: 0;
|
||||||
gap: 1rem;
|
margin: 0;
|
||||||
}
|
}
|
||||||
.table-card {
|
.tables-list-simple li {
|
||||||
display: flex;
|
margin-bottom: 0.5em;
|
||||||
flex-wrap: wrap;
|
border-bottom: 1px solid #f0f0f0;
|
||||||
justify-content: space-between;
|
padding-bottom: 0.5em;
|
||||||
align-items: flex-start;
|
|
||||||
background: #f8f9fa;
|
|
||||||
border-radius: 10px;
|
|
||||||
padding: 1rem;
|
|
||||||
box-shadow: 0 1px 4px rgba(0,0,0,0.03);
|
|
||||||
cursor: pointer;
|
|
||||||
border: 2px solid transparent;
|
|
||||||
transition: border 0.2s;
|
|
||||||
}
|
}
|
||||||
.table-card.selected {
|
.tables-list-simple li:last-child {
|
||||||
border: 2px solid #2ecc40;
|
border-bottom: none;
|
||||||
}
|
}
|
||||||
.table-info {
|
.table-link {
|
||||||
flex: 1 1 200px;
|
background: none;
|
||||||
}
|
|
||||||
.table-title {
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 1.1em;
|
|
||||||
}
|
|
||||||
.table-desc {
|
|
||||||
color: #888;
|
|
||||||
font-size: 0.95em;
|
|
||||||
margin-top: 0.2em;
|
|
||||||
}
|
|
||||||
.table-actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 0.5em;
|
|
||||||
margin-top: 0.5em;
|
|
||||||
}
|
|
||||||
.table-actions button {
|
|
||||||
background: #eaeaea;
|
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 6px;
|
color: #2ecc40;
|
||||||
padding: 0.4em 1em;
|
font-size: 1.1em;
|
||||||
|
font-weight: 600;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-weight: 500;
|
text-align: left;
|
||||||
transition: background 0.2s;
|
padding: 0.2em 0;
|
||||||
|
transition: color 0.2s, background 0.2s;
|
||||||
|
width: 100%;
|
||||||
|
display: block;
|
||||||
|
border-radius: 6px;
|
||||||
}
|
}
|
||||||
.table-actions button:hover {
|
.table-link:hover {
|
||||||
background: #d5d5d5;
|
color: #138496;
|
||||||
}
|
background: #f5f7fa;
|
||||||
.table-actions .danger {
|
text-decoration: none;
|
||||||
background: #ff4d4f;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
.table-actions .danger:hover {
|
|
||||||
background: #d9363e;
|
|
||||||
}
|
}
|
||||||
.empty-state {
|
.empty-state {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
@@ -181,49 +113,4 @@ function deleteTable(table) {
|
|||||||
margin: 2em 0;
|
margin: 2em 0;
|
||||||
font-size: 1.1em;
|
font-size: 1.1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Модалка */
|
|
||||||
.modal-backdrop {
|
|
||||||
position: fixed;
|
|
||||||
top: 0; left: 0; right: 0; bottom: 0;
|
|
||||||
background: rgba(0,0,0,0.18);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
z-index: 1000;
|
|
||||||
}
|
|
||||||
.modal {
|
|
||||||
background: #fff;
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: 2em 1.5em;
|
|
||||||
box-shadow: 0 2px 16px rgba(0,0,0,0.13);
|
|
||||||
min-width: 260px;
|
|
||||||
}
|
|
||||||
.modal-actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 1em;
|
|
||||||
margin-top: 1.5em;
|
|
||||||
justify-content: flex-end;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Адаптивность */
|
|
||||||
@media (max-width: 600px) {
|
|
||||||
.tables-container {
|
|
||||||
padding: 1em 0.3em;
|
|
||||||
}
|
|
||||||
.table-card {
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.7em;
|
|
||||||
padding: 0.7em;
|
|
||||||
}
|
|
||||||
.tables-header {
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.7em;
|
|
||||||
align-items: flex-start;
|
|
||||||
}
|
|
||||||
.table-actions {
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 0.4em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
55
frontend/src/composables/useContactsWebSocket.js
Normal file
55
frontend/src/composables/useContactsWebSocket.js
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import { ref, onMounted, onUnmounted } from 'vue';
|
||||||
|
import { getContacts } from '../services/contactsService';
|
||||||
|
import { getAllMessages } from '../services/messagesService';
|
||||||
|
|
||||||
|
export function useContactsAndMessagesWebSocket() {
|
||||||
|
const contacts = ref([]);
|
||||||
|
const messages = ref([]);
|
||||||
|
const newContacts = ref([]);
|
||||||
|
const newMessages = ref([]);
|
||||||
|
let ws = null;
|
||||||
|
let lastContactId = null;
|
||||||
|
let lastMessageId = null;
|
||||||
|
|
||||||
|
async function fetchContacts() {
|
||||||
|
const all = await getContacts();
|
||||||
|
contacts.value = all;
|
||||||
|
if (lastContactId) {
|
||||||
|
newContacts.value = all.filter(c => c.id > lastContactId);
|
||||||
|
} else {
|
||||||
|
newContacts.value = [];
|
||||||
|
}
|
||||||
|
if (all.length) lastContactId = Math.max(...all.map(c => c.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchMessages() {
|
||||||
|
const all = await getAllMessages();
|
||||||
|
messages.value = all;
|
||||||
|
if (lastMessageId) {
|
||||||
|
newMessages.value = all.filter(m => m.id > lastMessageId);
|
||||||
|
} else {
|
||||||
|
newMessages.value = [];
|
||||||
|
}
|
||||||
|
if (all.length) lastMessageId = Math.max(...all.map(m => m.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchContacts();
|
||||||
|
fetchMessages();
|
||||||
|
ws = new WebSocket('ws://localhost:8000');
|
||||||
|
ws.onmessage = (event) => {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(event.data);
|
||||||
|
if (data.type === 'contacts-updated') fetchContacts();
|
||||||
|
if (data.type === 'messages-updated') fetchMessages();
|
||||||
|
} catch (e) {}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => { if (ws) ws.close(); });
|
||||||
|
|
||||||
|
function markContactsAsRead() { newContacts.value = []; }
|
||||||
|
function markMessagesAsRead() { newMessages.value = []; }
|
||||||
|
|
||||||
|
return { contacts, messages, newContacts, newMessages, markContactsAsRead, markMessagesAsRead };
|
||||||
|
}
|
||||||
@@ -92,6 +92,11 @@ const routes = [
|
|||||||
component: () => import('../views/tables/DeleteTableView.vue'),
|
component: () => import('../views/tables/DeleteTableView.vue'),
|
||||||
props: true
|
props: true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/tables/tags',
|
||||||
|
name: 'tags-table-view',
|
||||||
|
component: () => import('../views/tables/TagsTableViewPage.vue')
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/contacts/:id',
|
path: '/contacts/:id',
|
||||||
name: 'contact-details',
|
name: 'contact-details',
|
||||||
@@ -104,6 +109,16 @@ const routes = [
|
|||||||
component: () => import('../views/contacts/ContactDeleteConfirm.vue'),
|
component: () => import('../views/contacts/ContactDeleteConfirm.vue'),
|
||||||
props: true
|
props: true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/contacts-list',
|
||||||
|
name: 'contacts-list',
|
||||||
|
component: () => import('../views/ContactsView.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/dle-management',
|
||||||
|
name: 'dle-management',
|
||||||
|
component: () => import('../views/DleManagementView.vue')
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
|
|||||||
@@ -30,3 +30,12 @@ export default {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export async function getContacts() {
|
||||||
|
const res = await fetch('/api/users');
|
||||||
|
const data = await res.json();
|
||||||
|
if (data && data.success) {
|
||||||
|
return data.contacts;
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
@@ -7,3 +7,8 @@ export default {
|
|||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export async function getAllMessages() {
|
||||||
|
const { data } = await axios.get('/api/messages');
|
||||||
|
return data;
|
||||||
|
}
|
||||||
74
frontend/src/views/ContactsView.vue
Normal file
74
frontend/src/views/ContactsView.vue
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
<template>
|
||||||
|
<BaseLayout>
|
||||||
|
<div class="contacts-tabs">
|
||||||
|
<button :class="{active: tab==='all'}" @click="tab='all'">Все контакты</button>
|
||||||
|
<button :class="{active: tab==='newContacts'}" @click="tab='newContacts'; markContactsAsRead()">
|
||||||
|
Новые контакты
|
||||||
|
<span v-if="newContacts.length" class="badge">{{ newContacts.length }}</span>
|
||||||
|
</button>
|
||||||
|
<button :class="{active: tab==='newMessages'}" @click="tab='newMessages'; markMessagesAsRead()">
|
||||||
|
Новые сообщения
|
||||||
|
<span v-if="newMessages.length" class="badge">{{ newMessages.length }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<ContactTable v-if="tab==='all'" :contacts="contacts" />
|
||||||
|
<ContactTable v-if="tab==='newContacts'" :contacts="newContacts" />
|
||||||
|
<MessagesTable v-if="tab==='newMessages'" :messages="newMessages" />
|
||||||
|
</BaseLayout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import BaseLayout from '../components/BaseLayout.vue';
|
||||||
|
import ContactTable from '../components/ContactTable.vue';
|
||||||
|
import MessagesTable from '../components/MessagesTable.vue';
|
||||||
|
import { useContactsAndMessagesWebSocket } from '../composables/useContactsWebSocket';
|
||||||
|
|
||||||
|
const tab = ref('all');
|
||||||
|
const {
|
||||||
|
contacts, newContacts, newMessages,
|
||||||
|
markContactsAsRead, markMessagesAsRead
|
||||||
|
} = useContactsAndMessagesWebSocket();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
function goBack() {
|
||||||
|
if (window.history.length > 1) {
|
||||||
|
router.back();
|
||||||
|
} else {
|
||||||
|
router.push({ name: 'crm' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.contacts-tabs {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
.contacts-tabs button {
|
||||||
|
background: #f5f7fa;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px 8px 0 0;
|
||||||
|
padding: 10px 22px;
|
||||||
|
font-size: 1.08rem;
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
transition: background 0.18s, color 0.18s;
|
||||||
|
}
|
||||||
|
.contacts-tabs button.active {
|
||||||
|
background: #fff;
|
||||||
|
color: #17a2b8;
|
||||||
|
font-weight: 600;
|
||||||
|
box-shadow: 0 -2px 8px rgba(0,0,0,0.04);
|
||||||
|
}
|
||||||
|
.badge {
|
||||||
|
background: #dc3545;
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 2px 8px;
|
||||||
|
font-size: 0.95em;
|
||||||
|
margin-left: 7px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -9,18 +9,16 @@
|
|||||||
<div class="crm-view-container">
|
<div class="crm-view-container">
|
||||||
<div class="dle-management-block">
|
<div class="dle-management-block">
|
||||||
<h2>Управление DLE</h2>
|
<h2>Управление DLE</h2>
|
||||||
<button class="btn btn-info" @click="showDleManagement = true">
|
<button class="btn btn-info" @click="goToDleManagement">
|
||||||
<i class="fas fa-cogs"></i> Подробнее
|
<i class="fas fa-cogs"></i> Подробнее
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<DleManagement v-if="showDleManagement" :dle-list="dleList" :selected-dle-index="selectedDleIndex" @close="showDleManagement = false" />
|
|
||||||
<div class="crm-contacts-block">
|
<div class="crm-contacts-block">
|
||||||
<h2>Контакты</h2>
|
<h2>Контакты</h2>
|
||||||
<button class="btn btn-info" @click="showContacts = true">
|
<button class="btn btn-info" @click="goToContactsList">
|
||||||
<i class="fas fa-address-book"></i> Подробнее
|
<i class="fas fa-address-book"></i> Подробнее
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<ContactTable v-if="showContacts" :contacts="contacts" @close="showContacts = false" @show-details="openContactDetails" />
|
|
||||||
<div class="crm-tables-block">
|
<div class="crm-tables-block">
|
||||||
<h2>Таблицы</h2>
|
<h2>Таблицы</h2>
|
||||||
<button class="btn btn-info" @click="goToTables">
|
<button class="btn btn-info" @click="goToTables">
|
||||||
@@ -189,6 +187,14 @@ function onContactDeleted() {
|
|||||||
function goToTables() {
|
function goToTables() {
|
||||||
router.push({ name: 'tables-list' });
|
router.push({ name: 'tables-list' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function goToDleManagement() {
|
||||||
|
router.push({ name: 'dle-management' });
|
||||||
|
}
|
||||||
|
|
||||||
|
function goToContactsList() {
|
||||||
|
router.push({ name: 'contacts-list' });
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
47
frontend/src/views/DleManagementView.vue
Normal file
47
frontend/src/views/DleManagementView.vue
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
<template>
|
||||||
|
<BaseLayout>
|
||||||
|
<DleManagement
|
||||||
|
:dle-list="dleList"
|
||||||
|
:selected-dle-index="selectedDleIndex"
|
||||||
|
@close="goBack"
|
||||||
|
class="dle-management-root"
|
||||||
|
/>
|
||||||
|
</BaseLayout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted } from 'vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import BaseLayout from '../components/BaseLayout.vue';
|
||||||
|
import DleManagement from '../components/DleManagement.vue';
|
||||||
|
import dleService from '../services/dleService';
|
||||||
|
|
||||||
|
const dleList = ref([]);
|
||||||
|
const selectedDleIndex = ref(0);
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
function goBack() {
|
||||||
|
if (window.history.length > 1) {
|
||||||
|
router.back();
|
||||||
|
} else {
|
||||||
|
router.push({ name: 'crm' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
dleList.value = await dleService.getAllDLEs() || [];
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.dle-management-root {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 16px;
|
||||||
|
box-shadow: 0 4px 32px rgba(0,0,0,0.12);
|
||||||
|
padding: 32px 24px 24px 24px;
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 40px;
|
||||||
|
position: relative;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
|
<BaseLayout>
|
||||||
<div class="contact-details-page">
|
<div class="contact-details-page">
|
||||||
<Header :isSidebarOpen="isSidebarOpen" @toggle-sidebar="toggleSidebar" />
|
|
||||||
<div v-if="isLoading">Загрузка...</div>
|
<div v-if="isLoading">Загрузка...</div>
|
||||||
<div v-else-if="!contact">Контакт не найден</div>
|
<div v-else-if="!contact">Контакт не найден</div>
|
||||||
<div v-else class="contact-details-content">
|
<div v-else class="contact-details-content">
|
||||||
<div class="contact-details-header">
|
<div class="contact-details-header">
|
||||||
<h2>Детали контакта</h2>
|
<h2>Детали контакта</h2>
|
||||||
<router-link class="back-btn" :to="{ name: 'crm' }">← Назад к списку</router-link>
|
<button class="close-btn" @click="goBack">×</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="contact-info-block">
|
<div class="contact-info-block">
|
||||||
<div>
|
<div>
|
||||||
@@ -98,12 +98,13 @@
|
|||||||
</el-dialog>
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</BaseLayout>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onMounted, watch } from 'vue';
|
import { ref, computed, onMounted, watch } from 'vue';
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
import Header from '../../components/Header.vue';
|
import BaseLayout from '../../components/BaseLayout.vue';
|
||||||
import Message from '../../components/Message.vue';
|
import Message from '../../components/Message.vue';
|
||||||
import contactsService from '../../services/contactsService.js';
|
import contactsService from '../../services/contactsService.js';
|
||||||
import messagesService from '../../services/messagesService.js';
|
import messagesService from '../../services/messagesService.js';
|
||||||
@@ -125,7 +126,6 @@ const showTagModal = ref(false);
|
|||||||
const newTagName = ref('');
|
const newTagName = ref('');
|
||||||
const newTagDescription = ref('');
|
const newTagDescription = ref('');
|
||||||
const messages = ref([]);
|
const messages = ref([]);
|
||||||
const isSidebarOpen = ref(false);
|
|
||||||
|
|
||||||
function toggleSidebar() {
|
function toggleSidebar() {
|
||||||
isSidebarOpen.value = !isSidebarOpen.value;
|
isSidebarOpen.value = !isSidebarOpen.value;
|
||||||
@@ -288,6 +288,14 @@ async function reloadContact() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function goBack() {
|
||||||
|
if (window.history.length > 1) {
|
||||||
|
router.back();
|
||||||
|
} else {
|
||||||
|
router.push({ name: 'crm' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await reloadContact();
|
await reloadContact();
|
||||||
await loadMessages();
|
await loadMessages();
|
||||||
@@ -302,19 +310,17 @@ watch(userId, async () => {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.contact-details-page {
|
.contact-details-page {
|
||||||
max-width: 900px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 32px 0;
|
padding: 32px 0;
|
||||||
}
|
}
|
||||||
.contact-details-content {
|
.contact-details-content {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 16px;
|
||||||
|
box-shadow: 0 4px 32px rgba(0,0,0,0.12);
|
||||||
padding: 32px 24px 24px 24px;
|
padding: 32px 24px 24px 24px;
|
||||||
max-width: 700px;
|
width: 100%;
|
||||||
margin: 40px auto;
|
margin-top: 40px;
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
background: none;
|
|
||||||
border-radius: 0;
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
}
|
||||||
.contact-details-header {
|
.contact-details-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -322,17 +328,16 @@ watch(userId, async () => {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 18px;
|
margin-bottom: 18px;
|
||||||
}
|
}
|
||||||
.back-btn {
|
.close-btn {
|
||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
color: #17a2b8;
|
font-size: 2rem;
|
||||||
font-size: 1.1rem;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
text-decoration: underline;
|
color: #bbb;
|
||||||
padding: 0;
|
transition: color 0.2s;
|
||||||
}
|
}
|
||||||
.back-btn:hover {
|
.close-btn:hover {
|
||||||
color: #138496;
|
color: #333;
|
||||||
}
|
}
|
||||||
.contact-info-block {
|
.contact-info-block {
|
||||||
margin-bottom: 18px;
|
margin-bottom: 18px;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<BaseLayout>
|
<BaseLayout>
|
||||||
|
<div class="table-block-wrapper">
|
||||||
<div class="tableview-header-row">
|
<div class="tableview-header-row">
|
||||||
<button class="nav-btn" @click="goToTables">Таблицы</button>
|
<button class="nav-btn" @click="goToTables">Таблицы</button>
|
||||||
<button class="nav-btn" @click="goToCreate">Создать таблицу</button>
|
<button class="nav-btn" @click="goToCreate">Создать таблицу</button>
|
||||||
@@ -8,6 +9,7 @@
|
|||||||
<button class="danger-btn" @click="goToDelete">Удалить</button>
|
<button class="danger-btn" @click="goToDelete">Удалить</button>
|
||||||
</div>
|
</div>
|
||||||
<UserTableView :table-id="Number($route.params.id)" />
|
<UserTableView :table-id="Number($route.params.id)" />
|
||||||
|
</div>
|
||||||
</BaseLayout>
|
</BaseLayout>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -44,6 +46,16 @@ function goToCreate() {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
.table-block-wrapper {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 16px;
|
||||||
|
box-shadow: 0 4px 32px rgba(0,0,0,0.12);
|
||||||
|
padding: 32px 24px 24px 24px;
|
||||||
|
max-width: 950px;
|
||||||
|
margin: 40px auto;
|
||||||
|
position: relative;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
.tableview-header-row {
|
.tableview-header-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
|
|||||||
@@ -1,11 +1,51 @@
|
|||||||
<template>
|
<template>
|
||||||
<BaseLayout>
|
<BaseLayout>
|
||||||
|
<div class="tables-list-block">
|
||||||
|
<button class="close-btn" @click="goBack">×</button>
|
||||||
<h2>Список таблиц</h2>
|
<h2>Список таблиц</h2>
|
||||||
<UserTablesList />
|
<UserTablesList />
|
||||||
|
</div>
|
||||||
</BaseLayout>
|
</BaseLayout>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import BaseLayout from '../../components/BaseLayout.vue';
|
import BaseLayout from '../../components/BaseLayout.vue';
|
||||||
import UserTablesList from '../../components/tables/UserTablesList.vue';
|
import UserTablesList from '../../components/tables/UserTablesList.vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
// import TagsTableView from '../../components/tables/TagsTableView.vue'; // больше не используется
|
||||||
|
const router = useRouter();
|
||||||
|
function goBack() {
|
||||||
|
if (window.history.length > 1) {
|
||||||
|
router.back();
|
||||||
|
} else {
|
||||||
|
router.push({ name: 'crm' });
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.tables-list-block {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 16px;
|
||||||
|
box-shadow: 0 4px 32px rgba(0,0,0,0.12);
|
||||||
|
padding: 32px 24px 24px 24px;
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 40px;
|
||||||
|
position: relative;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
.close-btn {
|
||||||
|
position: absolute;
|
||||||
|
top: 18px;
|
||||||
|
right: 18px;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
font-size: 2rem;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #bbb;
|
||||||
|
transition: color 0.2s;
|
||||||
|
}
|
||||||
|
.close-btn:hover {
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
10
frontend/src/views/tables/TagsTableViewPage.vue
Normal file
10
frontend/src/views/tables/TagsTableViewPage.vue
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<template>
|
||||||
|
<BaseLayout>
|
||||||
|
<TagsTableView />
|
||||||
|
</BaseLayout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import BaseLayout from '../../components/BaseLayout.vue';
|
||||||
|
import TagsTableView from '../../components/tables/TagsTableView.vue';
|
||||||
|
</script>
|
||||||
Reference in New Issue
Block a user