ваше сообщение коммита
This commit is contained in:
@@ -129,11 +129,16 @@ function closeAddColModal() {
|
||||
async function handleAddColumn() {
|
||||
if (!newColName.value) return;
|
||||
const data = { name: newColName.value, type: newColType.value };
|
||||
const options = {};
|
||||
if (newColType.value === 'tags') {
|
||||
data.tagIds = selectedTagIds.value;
|
||||
options.tagIds = selectedTagIds.value;
|
||||
}
|
||||
if (newColPurpose.value) {
|
||||
data.purpose = newColPurpose.value;
|
||||
options.purpose = newColPurpose.value;
|
||||
}
|
||||
if (Object.keys(options).length > 0) {
|
||||
data.options = options;
|
||||
}
|
||||
await tablesService.addColumn(props.tableId, data);
|
||||
closeAddColModal();
|
||||
|
||||
@@ -27,6 +27,11 @@ const routes = [
|
||||
component: () => import('../views/SettingsView.vue'),
|
||||
// Добавляем дочерние маршруты
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'settings-index',
|
||||
component: () => import('@/views/settings/SettingsIndexView.vue'),
|
||||
},
|
||||
{
|
||||
path: 'ai',
|
||||
name: 'settings-ai',
|
||||
@@ -50,21 +55,35 @@ const routes = [
|
||||
{
|
||||
path: 'telegram',
|
||||
name: 'settings-telegram',
|
||||
component: () => import('../views/settings/TelegramSettingsView.vue'),
|
||||
component: () => import('../views/settings/AI/TelegramSettingsView.vue'),
|
||||
},
|
||||
{
|
||||
path: 'email',
|
||||
name: 'settings-email',
|
||||
component: () => import('../views/settings/EmailSettingsView.vue'),
|
||||
component: () => import('../views/settings/AI/EmailSettingsView.vue'),
|
||||
},
|
||||
// Опционально: перенаправление со /settings на первую подстраницу
|
||||
{
|
||||
path: '',
|
||||
name: 'settings-index',
|
||||
redirect: { name: 'settings-ai' }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/settings/ai/openai',
|
||||
name: 'openai-settings',
|
||||
component: () => import('@/views/settings/AI/OpenAISettingsView.vue'),
|
||||
},
|
||||
{
|
||||
path: '/settings/ai/ollama',
|
||||
name: 'ollama-settings',
|
||||
component: () => import('@/views/settings/AI/OllamaSettingsView.vue'),
|
||||
},
|
||||
{
|
||||
path: '/settings/ai/database',
|
||||
name: 'database-settings',
|
||||
component: () => import('@/views/settings/AI/DatabaseSettingsView.vue'),
|
||||
},
|
||||
{
|
||||
path: '/settings/ai/assistant',
|
||||
name: 'ai-assistant-settings',
|
||||
component: () => import('@/views/settings/AI/AiAssistantSettings.vue'),
|
||||
},
|
||||
{
|
||||
path: '/tables',
|
||||
name: 'tables-list',
|
||||
@@ -120,6 +139,16 @@ const routes = [
|
||||
name: 'dle-management',
|
||||
component: () => import('../views/DleManagementView.vue')
|
||||
},
|
||||
{
|
||||
path: '/settings/ai/telegram',
|
||||
name: 'telegram-settings',
|
||||
component: () => import('@/views/settings/AI/TelegramSettingsView.vue'),
|
||||
},
|
||||
{
|
||||
path: '/settings/ai/email',
|
||||
name: 'email-settings',
|
||||
component: () => import('@/views/settings/AI/EmailSettingsView.vue'),
|
||||
},
|
||||
];
|
||||
|
||||
const router = createRouter({
|
||||
|
||||
@@ -8,23 +8,12 @@
|
||||
>
|
||||
<div class="settings-view-container">
|
||||
<h1>Настройки</h1>
|
||||
|
||||
<div v-if="isLoading">Загрузка данных пользователя...</div>
|
||||
<div v-else-if="!auth.isAuthenticated.value">
|
||||
<p>Для доступа к настройкам необходимо <button @click="goToHomeAndShowSidebar">войти</button>.</p>
|
||||
</div>
|
||||
<div v-else class="settings-navigation-buttons">
|
||||
<div class="buttons-grid">
|
||||
<router-link :to="{ name: 'settings-ai' }" class="btn btn-secondary">ИИ</router-link>
|
||||
<router-link :to="{ name: 'settings-blockchain' }" class="btn btn-secondary">Блокчейн</router-link>
|
||||
<router-link :to="{ name: 'settings-security' }" class="btn btn-secondary">Безопасность</router-link>
|
||||
<router-link :to="{ name: 'settings-interface' }" class="btn btn-secondary">Интерфейс</router-link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Router view для отображения дочерних компонентов настроек -->
|
||||
<router-view></router-view>
|
||||
|
||||
</div>
|
||||
</BaseLayout>
|
||||
</template>
|
||||
@@ -32,7 +21,7 @@
|
||||
<script setup>
|
||||
import { ref, onMounted, watch, onBeforeUnmount, computed, defineProps, defineEmits } from 'vue';
|
||||
import { useAuthContext } from '../composables/useAuth';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useRouter, useRoute } from 'vue-router';
|
||||
import { getFromStorage, setToStorage } from '../utils/storage';
|
||||
import BaseLayout from '../components/BaseLayout.vue';
|
||||
import eventBus from '../utils/eventBus';
|
||||
@@ -50,6 +39,7 @@ const emit = defineEmits(['auth-action-completed']);
|
||||
|
||||
const auth = useAuthContext();
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const isLoading = ref(true);
|
||||
|
||||
// Обработчик события изменения авторизации
|
||||
@@ -130,23 +120,7 @@ strong {
|
||||
}
|
||||
|
||||
/* Новые стили для кнопок навигации */
|
||||
.settings-navigation-buttons {
|
||||
margin-top: var(--spacing-lg);
|
||||
padding-top: var(--spacing-lg);
|
||||
border-top: 1px solid var(--color-grey-light);
|
||||
}
|
||||
|
||||
.buttons-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
.buttons-grid .btn {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
justify-content: center;
|
||||
}
|
||||
/* Удалено: .settings-navigation-buttons, .buttons-grid, .buttons-grid .btn */
|
||||
|
||||
/* Анимации */
|
||||
@keyframes fadeIn {
|
||||
|
||||
276
frontend/src/views/settings/AI/AiAssistantSettings.vue
Normal file
276
frontend/src/views/settings/AI/AiAssistantSettings.vue
Normal file
@@ -0,0 +1,276 @@
|
||||
<template>
|
||||
<BaseLayout>
|
||||
<div class="ai-assistant-settings-block">
|
||||
<button class="close-btn" @click="goBack">×</button>
|
||||
<h2>ИИ-ассистент: интеграция и настройки</h2>
|
||||
<div class="ai-assistant-settings settings-panel">
|
||||
<form @submit.prevent="saveSettings">
|
||||
<label>Системный промт</label>
|
||||
<textarea v-model="settings.system_prompt" rows="3" />
|
||||
<label>LLM-модель</label>
|
||||
<select v-if="llmModels.length" v-model="settings.model">
|
||||
<option v-for="m in llmModels" :key="m.id" :value="m.id">{{ m.id }} ({{ m.provider }})</option>
|
||||
</select>
|
||||
<input v-else v-model="settings.model" placeholder="qwen2.5" />
|
||||
<label>Embedding-модель</label>
|
||||
<select v-if="filteredEmbeddingModels.length" v-model="settings.embedding_model">
|
||||
<option v-for="m in filteredEmbeddingModels" :key="m.id" :value="m.id">{{ m.id }} ({{ m.provider }})</option>
|
||||
</select>
|
||||
<input v-else v-model="settings.embedding_model" placeholder="bge-base-zh" />
|
||||
<label>Выбранные RAG-таблицы</label>
|
||||
<select v-model="settings.selected_rag_tables" :multiple="false">
|
||||
<option v-for="table in ragTables" :key="table.id" :value="table.id">{{ table.name }} (id: {{ table.id }})</option>
|
||||
</select>
|
||||
<label>Набор правил</label>
|
||||
<div class="rules-row">
|
||||
<select v-model="settings.rules_id">
|
||||
<option v-for="rule in rulesList" :key="rule.id" :value="rule.id">
|
||||
{{ rule.name }}
|
||||
</option>
|
||||
</select>
|
||||
<button type="button" @click="openRuleEditor()">Создать</button>
|
||||
<button type="button" :disabled="!settings.rules_id" @click="openRuleEditor(settings.rules_id)">Редактировать</button>
|
||||
<button type="button" :disabled="!settings.rules_id" @click="deleteRule(settings.rules_id)">Удалить</button>
|
||||
</div>
|
||||
<div v-if="selectedRule">
|
||||
<p><b>Описание:</b> {{ selectedRule.description }}</p>
|
||||
<pre class="rules-json">{{ JSON.stringify(selectedRule.rules, null, 2) }}</pre>
|
||||
</div>
|
||||
<label>Telegram-бот</label>
|
||||
<select v-model="settings.telegram_settings_id">
|
||||
<option v-for="tg in telegramBots" :key="tg.id" :value="tg.id">
|
||||
{{ tg.bot_username }}
|
||||
</option>
|
||||
</select>
|
||||
<label>Email для связи</label>
|
||||
<select v-model="settings.email_settings_id">
|
||||
<option v-for="em in emailList" :key="em.id" :value="em.id">
|
||||
{{ em.from_email }}
|
||||
</option>
|
||||
</select>
|
||||
<div class="actions">
|
||||
<button type="submit">Сохранить</button>
|
||||
<button type="button" @click="goBack">Отмена</button>
|
||||
</div>
|
||||
</form>
|
||||
<RuleEditor v-if="showRuleEditor" :rule="editingRule" @close="onRuleEditorClose" />
|
||||
</div>
|
||||
</div>
|
||||
</BaseLayout>
|
||||
</template>
|
||||
<script setup>
|
||||
import BaseLayout from '@/components/BaseLayout.vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { ref, onMounted, computed, watch } from 'vue';
|
||||
import axios from 'axios';
|
||||
import RuleEditor from '@/components/ai-assistant/RuleEditor.vue';
|
||||
const router = useRouter();
|
||||
const goBack = () => router.push('/settings/ai');
|
||||
const settings = ref({ system_prompt: '', model: '', selected_rag_tables: [], rules_id: null });
|
||||
const userTables = ref([]);
|
||||
const ragTables = computed(() => userTables.value.filter(t => t.is_rag_source_id === 1));
|
||||
const rulesList = ref([]);
|
||||
const showRuleEditor = ref(false);
|
||||
const editingRule = ref(null);
|
||||
const telegramBots = ref([]);
|
||||
const emailList = ref([]);
|
||||
const llmModels = ref([]);
|
||||
const embeddingModels = ref([]);
|
||||
const selectedRule = computed(() => rulesList.value.find(r => r.id === settings.value.rules_id) || null);
|
||||
const selectedLLM = computed(() => llmModels.value.find(m => m.id === settings.value.model));
|
||||
const filteredEmbeddingModels = computed(() => {
|
||||
if (!selectedLLM.value) return embeddingModels.value;
|
||||
return embeddingModels.value.filter(m => m.provider === selectedLLM.value.provider);
|
||||
});
|
||||
async function loadUserTables() {
|
||||
const { data } = await axios.get('/api/tables');
|
||||
userTables.value = Array.isArray(data) ? data : [];
|
||||
}
|
||||
async function loadRules() {
|
||||
const { data } = await axios.get('/api/settings/ai-assistant-rules');
|
||||
rulesList.value = data.rules || [];
|
||||
}
|
||||
async function loadSettings() {
|
||||
const { data } = await axios.get('/api/settings/ai-assistant');
|
||||
if (data.success && data.settings) {
|
||||
settings.value = data.settings;
|
||||
}
|
||||
}
|
||||
async function loadTelegramBots() {
|
||||
const { data } = await axios.get('/api/settings/telegram-settings/list');
|
||||
telegramBots.value = data.items || [];
|
||||
}
|
||||
async function loadEmailList() {
|
||||
const { data } = await axios.get('/api/settings/email-settings/list');
|
||||
emailList.value = data.items || [];
|
||||
}
|
||||
async function loadLLMModels() {
|
||||
const { data } = await axios.get('/api/settings/llm-models');
|
||||
llmModels.value = data.models || [];
|
||||
}
|
||||
async function loadEmbeddingModels() {
|
||||
const { data } = await axios.get('/api/settings/embedding-models');
|
||||
embeddingModels.value = data.models || [];
|
||||
}
|
||||
onMounted(() => {
|
||||
loadSettings();
|
||||
loadUserTables();
|
||||
loadRules();
|
||||
loadTelegramBots();
|
||||
loadEmailList();
|
||||
loadLLMModels();
|
||||
loadEmbeddingModels();
|
||||
});
|
||||
async function saveSettings() {
|
||||
await axios.put('/api/settings/ai-assistant', settings.value);
|
||||
goBack();
|
||||
}
|
||||
function openRuleEditor(ruleId = null) {
|
||||
if (ruleId) {
|
||||
editingRule.value = rulesList.value.find(r => r.id === ruleId) || null;
|
||||
} else {
|
||||
editingRule.value = null;
|
||||
}
|
||||
showRuleEditor.value = true;
|
||||
}
|
||||
async function deleteRule(ruleId) {
|
||||
if (!confirm('Удалить этот набор правил?')) return;
|
||||
await axios.delete(`/api/settings/ai-assistant-rules/${ruleId}`);
|
||||
await loadRules();
|
||||
if (settings.value.rules_id === ruleId) settings.value.rules_id = null;
|
||||
}
|
||||
async function onRuleEditorClose(updated) {
|
||||
showRuleEditor.value = false;
|
||||
editingRule.value = null;
|
||||
if (updated) await loadRules();
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.ai-assistant-settings-block {
|
||||
background: #fff;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 4px 32px rgba(0,0,0,0.12);
|
||||
padding: 32px 24px 24px 24px;
|
||||
width: 100vw;
|
||||
max-width: none;
|
||||
margin: 0;
|
||||
position: relative;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
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;
|
||||
}
|
||||
h2 {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.ai-assistant-settings.settings-panel {
|
||||
background: none !important;
|
||||
box-shadow: none !important;
|
||||
border-radius: 0 !important;
|
||||
margin-top: 0 !important;
|
||||
max-width: 100% !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
label {
|
||||
display: block;
|
||||
margin-top: 1rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
textarea, input, select {
|
||||
width: 100%;
|
||||
margin-top: 0.5rem;
|
||||
padding: 0.5rem;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #ddd;
|
||||
font-size: 1rem;
|
||||
}
|
||||
select[multiple] {
|
||||
min-height: 80px;
|
||||
}
|
||||
.rules-row {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.rules-json {
|
||||
background: #f7f7f7;
|
||||
border-radius: 6px;
|
||||
padding: 0.5rem;
|
||||
font-size: 0.95em;
|
||||
margin-top: 0.5rem;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
button[type="submit"], .actions button {
|
||||
background: var(--color-primary);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 0.5rem 1.5rem;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
}
|
||||
button[type="button"] {
|
||||
background: #eee;
|
||||
color: #333;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 0.5rem 1.5rem;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
}
|
||||
.modal-bg {
|
||||
position: fixed;
|
||||
top: 0; left: 0; right: 0; bottom: 0;
|
||||
background: rgba(0,0,0,0.25);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
.modal {
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 16px rgba(0,0,0,0.12);
|
||||
padding: 2rem;
|
||||
min-width: 320px;
|
||||
max-width: 420px;
|
||||
}
|
||||
.error {
|
||||
color: #c00;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
.rag-table-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0 0 1em 0;
|
||||
}
|
||||
.rag-table-link {
|
||||
color: #2ecc40;
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
}
|
||||
.rag-table-link:hover {
|
||||
color: #27ae38;
|
||||
}
|
||||
</style>
|
||||
210
frontend/src/views/settings/AI/DatabaseSettingsView.vue
Normal file
210
frontend/src/views/settings/AI/DatabaseSettingsView.vue
Normal file
@@ -0,0 +1,210 @@
|
||||
<template>
|
||||
<BaseLayout>
|
||||
<div class="db-settings-block">
|
||||
<button class="close-btn" @click="goBack">×</button>
|
||||
<h2>База данных: интеграция и настройки</h2>
|
||||
<div class="db-settings settings-panel">
|
||||
<form v-if="editMode" @submit.prevent="saveDbSettings" class="settings-form">
|
||||
<div class="form-group">
|
||||
<label for="dbHost">Host</label>
|
||||
<input id="dbHost" v-model="form.dbHost" type="text" required />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="dbPort">Port</label>
|
||||
<input id="dbPort" v-model.number="form.dbPort" type="number" required />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="dbName">Database</label>
|
||||
<input id="dbName" v-model="form.dbName" type="text" required />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="dbUser">User</label>
|
||||
<input id="dbUser" v-model="form.dbUser" type="text" required />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="dbPassword">Password</label>
|
||||
<input id="dbPassword" v-model="form.dbPassword" type="password" :placeholder="form.dbPassword ? 'Изменить пароль' : 'Введите пароль'" />
|
||||
</div>
|
||||
<button type="submit" class="save-btn">Сохранить</button>
|
||||
<button type="button" class="cancel-btn" @click="cancelEdit">Отмена</button>
|
||||
</form>
|
||||
<div v-else class="settings-view">
|
||||
<div class="view-row"><span>Host:</span> <b>{{ form.dbHost }}</b></div>
|
||||
<div class="view-row"><span>Port:</span> <b>{{ form.dbPort }}</b></div>
|
||||
<div class="view-row"><span>Database:</span> <b>{{ form.dbName }}</b></div>
|
||||
<div class="view-row"><span>User:</span> <b>{{ form.dbUser }}</b></div>
|
||||
<div class="view-row"><span>Password:</span> <b>••••••••••••••••••••••••••••••••</b></div>
|
||||
<button type="button" class="edit-btn" @click="editMode = true">Изменить</button>
|
||||
<button type="button" class="cancel-btn" @click="goBack">Закрыть</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BaseLayout>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import BaseLayout from '@/components/BaseLayout.vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { reactive, ref, onMounted } from 'vue';
|
||||
import api from '@/api/axios';
|
||||
|
||||
const router = useRouter();
|
||||
const goBack = () => router.push('/settings/ai');
|
||||
|
||||
const form = reactive({
|
||||
dbHost: '',
|
||||
dbPort: 5432,
|
||||
dbName: '',
|
||||
dbUser: '',
|
||||
dbPassword: ''
|
||||
});
|
||||
const original = reactive({});
|
||||
const editMode = ref(false);
|
||||
|
||||
const loadDbSettings = async () => {
|
||||
try {
|
||||
const res = await api.get('/api/settings/db-settings');
|
||||
if (res.data.success) {
|
||||
const s = res.data.settings;
|
||||
form.dbHost = s.db_host;
|
||||
form.dbPort = s.db_port;
|
||||
form.dbName = s.db_name;
|
||||
form.dbUser = s.db_user;
|
||||
form.dbPassword = '';
|
||||
Object.assign(original, JSON.parse(JSON.stringify(form)));
|
||||
}
|
||||
} catch (e) {
|
||||
// обработка ошибки
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
await loadDbSettings();
|
||||
editMode.value = false;
|
||||
});
|
||||
|
||||
const saveDbSettings = async () => {
|
||||
try {
|
||||
await api.put('/api/settings/db-settings', {
|
||||
db_host: form.dbHost,
|
||||
db_port: form.dbPort,
|
||||
db_name: form.dbName,
|
||||
db_user: form.dbUser,
|
||||
db_password: form.dbPassword || undefined
|
||||
});
|
||||
alert('Настройки базы данных сохранены');
|
||||
form.dbPassword = '';
|
||||
Object.assign(original, JSON.parse(JSON.stringify(form)));
|
||||
editMode.value = false;
|
||||
} catch (e) {
|
||||
alert('Ошибка сохранения настроек базы данных');
|
||||
}
|
||||
};
|
||||
|
||||
const cancelEdit = () => {
|
||||
Object.assign(form, JSON.parse(JSON.stringify(original)));
|
||||
form.dbPassword = '';
|
||||
editMode.value = false;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.db-settings-block {
|
||||
background: #fff;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 4px 32px rgba(0,0,0,0.12);
|
||||
padding: 32px 24px 24px 24px;
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
margin: 40px auto 0 auto;
|
||||
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;
|
||||
}
|
||||
h2 {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.db-settings.settings-panel {
|
||||
background: none !important;
|
||||
box-shadow: none !important;
|
||||
border-radius: 0 !important;
|
||||
margin-top: 0 !important;
|
||||
max-width: 100% !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
.settings-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.save-btn {
|
||||
background: var(--color-primary);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 0.5rem 1.5rem;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
.save-btn:hover {
|
||||
background: var(--color-primary-dark);
|
||||
}
|
||||
.cancel-btn {
|
||||
background: #eee;
|
||||
color: #333;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 0.5rem 1.5rem;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
margin-left: 1rem;
|
||||
}
|
||||
.settings-view {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.2rem;
|
||||
}
|
||||
.view-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 1rem;
|
||||
background: #f8f8f8;
|
||||
border-radius: 4px;
|
||||
padding: 0.5rem 1rem;
|
||||
}
|
||||
.edit-btn {
|
||||
background: var(--color-primary);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 0.5rem 1.5rem;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
align-self: flex-end;
|
||||
margin-top: 1.5rem;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
.edit-btn:hover {
|
||||
background: var(--color-primary-dark);
|
||||
}
|
||||
</style>
|
||||
225
frontend/src/views/settings/AI/EmailSettingsView.vue
Normal file
225
frontend/src/views/settings/AI/EmailSettingsView.vue
Normal file
@@ -0,0 +1,225 @@
|
||||
<template>
|
||||
<BaseLayout>
|
||||
<div class="email-settings-block">
|
||||
<button class="close-btn" @click="goBack">×</button>
|
||||
<h2>Email: интеграция и настройки</h2>
|
||||
<div class="email-settings settings-panel">
|
||||
<form v-if="editMode" @submit.prevent="saveEmailSettings" class="settings-form">
|
||||
<div class="form-group">
|
||||
<label for="smtpHost">SMTP Host</label>
|
||||
<input id="smtpHost" v-model="form.smtpHost" type="text" required />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="smtpPort">SMTP Port</label>
|
||||
<input id="smtpPort" v-model.number="form.smtpPort" type="number" required />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="smtpUser">SMTP User</label>
|
||||
<input id="smtpUser" v-model="form.smtpUser" type="text" required />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="smtpPassword">SMTP Password</label>
|
||||
<input id="smtpPassword" v-model="form.smtpPassword" type="password" :placeholder="form.smtpPassword ? 'Изменить пароль' : 'Введите пароль'" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="imapHost">IMAP Host</label>
|
||||
<input id="imapHost" v-model="form.imapHost" type="text" required />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="imapPort">IMAP Port</label>
|
||||
<input id="imapPort" v-model.number="form.imapPort" type="number" required />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="fromEmail">From Email</label>
|
||||
<input id="fromEmail" v-model="form.fromEmail" type="email" required />
|
||||
</div>
|
||||
<button type="submit" class="save-btn">Сохранить</button>
|
||||
<button type="button" class="cancel-btn" @click="cancelEdit">Отмена</button>
|
||||
</form>
|
||||
<div v-else class="settings-view">
|
||||
<div class="view-row"><span>SMTP Host:</span> <b>{{ form.smtpHost }}</b></div>
|
||||
<div class="view-row"><span>SMTP Port:</span> <b>{{ form.smtpPort }}</b></div>
|
||||
<div class="view-row"><span>SMTP User:</span> <b>{{ form.smtpUser }}</b></div>
|
||||
<div class="view-row"><span>IMAP Host:</span> <b>{{ form.imapHost }}</b></div>
|
||||
<div class="view-row"><span>IMAP Port:</span> <b>{{ form.imapPort }}</b></div>
|
||||
<div class="view-row"><span>From Email:</span> <b>{{ form.fromEmail }}</b></div>
|
||||
<button type="button" class="edit-btn" @click="editMode = true">Изменить</button>
|
||||
<button type="button" class="cancel-btn" @click="goBack">Закрыть</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BaseLayout>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import BaseLayout from '@/components/BaseLayout.vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { reactive, ref, onMounted } from 'vue';
|
||||
import api from '@/api/axios';
|
||||
|
||||
const router = useRouter();
|
||||
const goBack = () => router.push('/settings/ai');
|
||||
|
||||
const form = reactive({
|
||||
smtpHost: '',
|
||||
smtpPort: 465,
|
||||
smtpUser: '',
|
||||
smtpPassword: '',
|
||||
imapHost: '',
|
||||
imapPort: 993,
|
||||
fromEmail: ''
|
||||
});
|
||||
const original = reactive({});
|
||||
const editMode = ref(false);
|
||||
|
||||
const loadEmailSettings = async () => {
|
||||
try {
|
||||
const res = await api.get('/api/settings/email-settings');
|
||||
if (res.data.success) {
|
||||
const s = res.data.settings;
|
||||
form.smtpHost = s.smtp_host;
|
||||
form.smtpPort = s.smtp_port;
|
||||
form.smtpUser = s.smtp_user;
|
||||
form.imapHost = s.imap_host || '';
|
||||
form.imapPort = s.imap_port || 993;
|
||||
form.fromEmail = s.from_email;
|
||||
form.smtpPassword = '';
|
||||
Object.assign(original, JSON.parse(JSON.stringify(form)));
|
||||
}
|
||||
} catch (e) {
|
||||
// обработка ошибки
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
await loadEmailSettings();
|
||||
editMode.value = false;
|
||||
});
|
||||
|
||||
const saveEmailSettings = async () => {
|
||||
try {
|
||||
await api.put('/api/settings/email-settings', {
|
||||
smtp_host: form.smtpHost,
|
||||
smtp_port: form.smtpPort,
|
||||
smtp_user: form.smtpUser,
|
||||
smtp_password: form.smtpPassword || undefined,
|
||||
imap_host: form.imapHost,
|
||||
imap_port: form.imapPort,
|
||||
from_email: form.fromEmail
|
||||
});
|
||||
alert('Настройки Email сохранены');
|
||||
form.smtpPassword = '';
|
||||
Object.assign(original, JSON.parse(JSON.stringify(form)));
|
||||
editMode.value = false;
|
||||
} catch (e) {
|
||||
alert('Ошибка сохранения email-настроек');
|
||||
}
|
||||
};
|
||||
|
||||
const cancelEdit = () => {
|
||||
Object.assign(form, JSON.parse(JSON.stringify(original)));
|
||||
form.smtpPassword = '';
|
||||
editMode.value = false;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.email-settings-block {
|
||||
background: #fff;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 4px 32px rgba(0,0,0,0.12);
|
||||
padding: 32px 24px 24px 24px;
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
margin: 40px auto 0 auto;
|
||||
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;
|
||||
}
|
||||
h2 {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.email-settings.settings-panel {
|
||||
background: none !important;
|
||||
box-shadow: none !important;
|
||||
border-radius: 0 !important;
|
||||
margin-top: 0 !important;
|
||||
max-width: 100% !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
.settings-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.save-btn {
|
||||
background: var(--color-primary);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 0.5rem 1.5rem;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
.save-btn:hover {
|
||||
background: var(--color-primary-dark);
|
||||
}
|
||||
.cancel-btn {
|
||||
background: #eee;
|
||||
color: #333;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 0.5rem 1.5rem;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
margin-left: 1rem;
|
||||
}
|
||||
.settings-view {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.2rem;
|
||||
}
|
||||
.view-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 1rem;
|
||||
background: #f8f8f8;
|
||||
border-radius: 4px;
|
||||
padding: 0.5rem 1rem;
|
||||
}
|
||||
.edit-btn {
|
||||
background: var(--color-primary);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 0.5rem 1.5rem;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
align-self: flex-end;
|
||||
margin-top: 1.5rem;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
.edit-btn:hover {
|
||||
background: var(--color-primary-dark);
|
||||
}
|
||||
</style>
|
||||
65
frontend/src/views/settings/AI/OllamaSettingsView.vue
Normal file
65
frontend/src/views/settings/AI/OllamaSettingsView.vue
Normal file
@@ -0,0 +1,65 @@
|
||||
<template>
|
||||
<BaseLayout>
|
||||
<div class="ollama-settings-block">
|
||||
<button class="close-btn" @click="goBack">×</button>
|
||||
<h2>Ollama: интеграция и настройки</h2>
|
||||
<AIProviderSettings
|
||||
provider="ollama"
|
||||
label="Ollama (локальные модели)"
|
||||
description="Настройка Ollama для локальных open-source моделей. Ключ не требуется."
|
||||
apiKeyPlaceholder=""
|
||||
baseUrlPlaceholder="http://localhost:11434"
|
||||
:showApiKey="false"
|
||||
:showBaseUrl="true"
|
||||
/>
|
||||
</div>
|
||||
</BaseLayout>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import BaseLayout from '@/components/BaseLayout.vue';
|
||||
import AIProviderSettings from '@/views/settings/AIProviderSettings.vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
const router = useRouter();
|
||||
const goBack = () => router.push('/settings/ai');
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.ollama-settings-block {
|
||||
background: #fff;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 4px 32px rgba(0,0,0,0.12);
|
||||
padding: 32px 24px 24px 24px;
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
margin: 40px auto 0 auto;
|
||||
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;
|
||||
}
|
||||
h2 {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.ai-provider-settings.settings-panel {
|
||||
background: none !important;
|
||||
box-shadow: none !important;
|
||||
border-radius: 0 !important;
|
||||
margin-top: 0 !important;
|
||||
max-width: 100% !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
</style>
|
||||
65
frontend/src/views/settings/AI/OpenAISettingsView.vue
Normal file
65
frontend/src/views/settings/AI/OpenAISettingsView.vue
Normal file
@@ -0,0 +1,65 @@
|
||||
<template>
|
||||
<BaseLayout>
|
||||
<div class="openai-settings-block">
|
||||
<button class="close-btn" @click="goBack">×</button>
|
||||
<h2>OpenAI: интеграция и настройки</h2>
|
||||
<AIProviderSettings
|
||||
provider="openai"
|
||||
label="OpenAI API Key"
|
||||
description="Введите OpenAI API Key и (опционально) Base URL для кастомных endpoint."
|
||||
apiKeyPlaceholder="sk-..."
|
||||
baseUrlPlaceholder="https://api.openai.com/v1"
|
||||
:showApiKey="true"
|
||||
:showBaseUrl="true"
|
||||
/>
|
||||
</div>
|
||||
</BaseLayout>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import BaseLayout from '@/components/BaseLayout.vue';
|
||||
import AIProviderSettings from '@/views/settings/AIProviderSettings.vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
const router = useRouter();
|
||||
const goBack = () => router.push('/settings/ai');
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.openai-settings-block {
|
||||
background: #fff;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 4px 32px rgba(0,0,0,0.12);
|
||||
padding: 32px 24px 24px 24px;
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
margin: 40px auto 0 auto;
|
||||
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;
|
||||
}
|
||||
h2 {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.ai-provider-settings.settings-panel {
|
||||
background: none !important;
|
||||
box-shadow: none !important;
|
||||
border-radius: 0 !important;
|
||||
margin-top: 0 !important;
|
||||
max-width: 100% !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
</style>
|
||||
186
frontend/src/views/settings/AI/TelegramSettingsView.vue
Normal file
186
frontend/src/views/settings/AI/TelegramSettingsView.vue
Normal file
@@ -0,0 +1,186 @@
|
||||
<template>
|
||||
<BaseLayout>
|
||||
<div class="telegram-settings-block">
|
||||
<button class="close-btn" @click="goBack">×</button>
|
||||
<h2>Telegram: интеграция и настройки</h2>
|
||||
<div class="telegram-settings settings-panel">
|
||||
<form v-if="editMode" @submit.prevent="saveTelegramSettings" class="settings-form">
|
||||
<div class="form-group">
|
||||
<label for="botToken">Bot Token</label>
|
||||
<input id="botToken" v-model="form.botToken" type="text" required />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="botUsername">Bot Username</label>
|
||||
<input id="botUsername" v-model="form.botUsername" type="text" required />
|
||||
</div>
|
||||
<button type="submit" class="save-btn">Сохранить</button>
|
||||
<button type="button" class="cancel-btn" @click="cancelEdit">Отмена</button>
|
||||
</form>
|
||||
<div v-else class="settings-view">
|
||||
<div class="view-row"><span>Bot Token:</span> <b>••••••••••••••••••••••••••••••••••••••••</b></div>
|
||||
<div class="view-row"><span>Bot Username:</span> <b>{{ form.botUsername }}</b></div>
|
||||
<button type="button" class="edit-btn" @click="editMode = true">Изменить</button>
|
||||
<button type="button" class="cancel-btn" @click="goBack">Закрыть</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BaseLayout>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import BaseLayout from '@/components/BaseLayout.vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { reactive, ref, onMounted } from 'vue';
|
||||
import api from '@/api/axios';
|
||||
|
||||
const router = useRouter();
|
||||
const goBack = () => router.push('/settings/ai');
|
||||
|
||||
const form = reactive({
|
||||
botToken: '',
|
||||
botUsername: ''
|
||||
});
|
||||
const original = reactive({});
|
||||
const editMode = ref(false);
|
||||
|
||||
const loadTelegramSettings = async () => {
|
||||
try {
|
||||
const res = await api.get('/api/settings/telegram-settings');
|
||||
if (res.data.success) {
|
||||
const s = res.data.settings;
|
||||
form.botToken = s.bot_token || '';
|
||||
form.botUsername = s.bot_username;
|
||||
Object.assign(original, JSON.parse(JSON.stringify(form)));
|
||||
}
|
||||
} catch (e) {
|
||||
// обработка ошибки
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
await loadTelegramSettings();
|
||||
editMode.value = false;
|
||||
});
|
||||
|
||||
const saveTelegramSettings = async () => {
|
||||
try {
|
||||
await api.put('/api/telegram-settings', {
|
||||
bot_token: form.botToken,
|
||||
bot_username: form.botUsername
|
||||
});
|
||||
alert('Настройки Telegram сохранены');
|
||||
form.botToken = '';
|
||||
Object.assign(original, JSON.parse(JSON.stringify(form)));
|
||||
editMode.value = false;
|
||||
} catch (e) {
|
||||
alert('Ошибка сохранения telegram-настроек');
|
||||
}
|
||||
};
|
||||
|
||||
const cancelEdit = () => {
|
||||
Object.assign(form, JSON.parse(JSON.stringify(original)));
|
||||
form.botToken = '';
|
||||
editMode.value = false;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.telegram-settings-block {
|
||||
background: #fff;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 4px 32px rgba(0,0,0,0.12);
|
||||
padding: 32px 24px 24px 24px;
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
margin: 40px auto 0 auto;
|
||||
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;
|
||||
}
|
||||
h2 {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.telegram-settings.settings-panel {
|
||||
background: none !important;
|
||||
box-shadow: none !important;
|
||||
border-radius: 0 !important;
|
||||
margin-top: 0 !important;
|
||||
max-width: 100% !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
.settings-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.save-btn {
|
||||
background: var(--color-primary);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 0.5rem 1.5rem;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
.save-btn:hover {
|
||||
background: var(--color-primary-dark);
|
||||
}
|
||||
.cancel-btn {
|
||||
background: #eee;
|
||||
color: #333;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 0.5rem 1.5rem;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
margin-left: 1rem;
|
||||
}
|
||||
.settings-view {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.2rem;
|
||||
}
|
||||
.view-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 1rem;
|
||||
background: #f8f8f8;
|
||||
border-radius: 4px;
|
||||
padding: 0.5rem 1rem;
|
||||
}
|
||||
.edit-btn {
|
||||
background: var(--color-primary);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 0.5rem 1.5rem;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
align-self: flex-end;
|
||||
margin-top: 1.5rem;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
.edit-btn:hover {
|
||||
background: var(--color-primary-dark);
|
||||
}
|
||||
</style>
|
||||
@@ -15,10 +15,18 @@
|
||||
<input type="text" v-model="baseUrl" :placeholder="baseUrlPlaceholder" />
|
||||
</div>
|
||||
<div v-if="models.length">
|
||||
<label>Модель:</label>
|
||||
<label>Модель (LLM):</label>
|
||||
<select v-model="selectedModel">
|
||||
<option v-for="model in models" :key="model.id || model" :value="model.id || model">
|
||||
{{ model.id || model }}
|
||||
<option v-for="model in models" :key="model.id || model.name || model" :value="model.id || model.name || model">
|
||||
{{ model.id || model.name || model }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div v-if="embeddingModels.length">
|
||||
<label>Embeddings-модель:</label>
|
||||
<select v-model="selectedEmbeddingModel">
|
||||
<option v-for="model in embeddingModels" :key="model.id || model.name || model" :value="model.id || model.name || model">
|
||||
{{ model.id || model.name || model }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
@@ -49,7 +57,9 @@ const props = defineProps({
|
||||
const apiKey = ref('');
|
||||
const baseUrl = ref('');
|
||||
const selectedModel = ref('');
|
||||
const selectedEmbeddingModel = ref('');
|
||||
const models = ref([]);
|
||||
const embeddingModels = ref([]);
|
||||
const hasSettings = ref(false);
|
||||
const verifying = ref(false);
|
||||
const verifyStatus = ref(null);
|
||||
@@ -65,9 +75,11 @@ async function loadSettings() {
|
||||
apiKey.value = data.settings.api_key || '';
|
||||
baseUrl.value = data.settings.base_url || '';
|
||||
selectedModel.value = data.settings.selected_model || '';
|
||||
selectedEmbeddingModel.value = data.settings.embedding_model || '';
|
||||
hasSettings.value = true;
|
||||
if (apiKey.value || props.provider === 'ollama') {
|
||||
await loadModels();
|
||||
await loadEmbeddingModels();
|
||||
}
|
||||
} else {
|
||||
hasSettings.value = false;
|
||||
@@ -82,13 +94,30 @@ async function loadModels() {
|
||||
const { data } = await axios.get(`/api/settings/ai-settings/${props.provider}/models`);
|
||||
models.value = data.models || [];
|
||||
if (!selectedModel.value && models.value.length) {
|
||||
selectedModel.value = models.value[0].id || models.value[0];
|
||||
const first = models.value[0];
|
||||
selectedModel.value = first.id || first.name || first;
|
||||
}
|
||||
} catch (e) {
|
||||
models.value = [];
|
||||
}
|
||||
}
|
||||
|
||||
async function loadEmbeddingModels() {
|
||||
try {
|
||||
const { data } = await axios.get(`/api/settings/ai-settings/${props.provider}/models`);
|
||||
embeddingModels.value = (data.models || []).filter(m => {
|
||||
const name = m.id || m.name || m;
|
||||
return name && name.toLowerCase().includes('embed');
|
||||
});
|
||||
if (!selectedEmbeddingModel.value && embeddingModels.value.length) {
|
||||
const first = embeddingModels.value[0];
|
||||
selectedEmbeddingModel.value = first.id || first.name || first;
|
||||
}
|
||||
} catch (e) {
|
||||
embeddingModels.value = [];
|
||||
}
|
||||
}
|
||||
|
||||
async function onVerify() {
|
||||
verifying.value = true;
|
||||
verifyStatus.value = null;
|
||||
@@ -119,6 +148,7 @@ async function onSave() {
|
||||
api_key: apiKey.value,
|
||||
base_url: baseUrl.value,
|
||||
selected_model: selectedModel.value,
|
||||
embedding_model: selectedEmbeddingModel.value,
|
||||
});
|
||||
saveStatus.value = true;
|
||||
hasSettings.value = true;
|
||||
@@ -135,7 +165,9 @@ async function onDelete() {
|
||||
apiKey.value = '';
|
||||
baseUrl.value = '';
|
||||
selectedModel.value = '';
|
||||
selectedEmbeddingModel.value = '';
|
||||
models.value = [];
|
||||
embeddingModels.value = [];
|
||||
hasSettings.value = false;
|
||||
}
|
||||
|
||||
@@ -151,8 +183,9 @@ watch([apiKey, baseUrl], () => {
|
||||
<style scoped>
|
||||
.ai-provider-settings.settings-panel {
|
||||
padding: var(--block-padding);
|
||||
background-color: var(--color-light);
|
||||
border-radius: var(--radius-md);
|
||||
/* background-color: var(--color-light); */
|
||||
/* border-radius: var(--radius-md); */
|
||||
/* box-shadow: 0 2px 8px rgba(0,0,0,0.08); */
|
||||
margin-top: var(--spacing-lg);
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
@@ -1,228 +0,0 @@
|
||||
<template>
|
||||
<div class="ai-assistant-settings-modal">
|
||||
<h2>Настройки ИИ-ассистента</h2>
|
||||
<form @submit.prevent="saveSettings">
|
||||
<label>Системный промт</label>
|
||||
<textarea v-model="settings.system_prompt" rows="3" />
|
||||
<label>Языки</label>
|
||||
<input v-model="languagesInput" placeholder="ru, en, es" />
|
||||
<label>Модель</label>
|
||||
<input v-model="settings.model" placeholder="qwen2.5" />
|
||||
<label>Выбранные RAG-таблицы</label>
|
||||
<ul class="rag-table-list">
|
||||
<li v-for="table in ragTables" :key="table.id">
|
||||
<router-link :to="{ name: 'user-table-view', params: { id: table.id } }" class="rag-table-link">
|
||||
{{ table.name }}
|
||||
</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
<label>Набор правил</label>
|
||||
<div class="rules-row">
|
||||
<select v-model="settings.rules_id">
|
||||
<option v-for="rule in rulesList" :key="rule.id" :value="rule.id">
|
||||
{{ rule.name }}
|
||||
</option>
|
||||
</select>
|
||||
<button type="button" @click="openRuleEditor()">Создать</button>
|
||||
<button type="button" :disabled="!settings.rules_id" @click="openRuleEditor(settings.rules_id)">Редактировать</button>
|
||||
<button type="button" :disabled="!settings.rules_id" @click="deleteRule(settings.rules_id)">Удалить</button>
|
||||
</div>
|
||||
<div v-if="selectedRule">
|
||||
<p><b>Описание:</b> {{ selectedRule.description }}</p>
|
||||
<pre class="rules-json">{{ JSON.stringify(selectedRule.rules, null, 2) }}</pre>
|
||||
</div>
|
||||
<label>Telegram-бот</label>
|
||||
<select v-model="settings.telegram_settings_id">
|
||||
<option v-for="tg in telegramBots" :key="tg.id" :value="tg.id">
|
||||
{{ tg.bot_username }}
|
||||
</option>
|
||||
</select>
|
||||
<label>Email для связи</label>
|
||||
<select v-model="settings.email_settings_id">
|
||||
<option v-for="em in emailList" :key="em.id" :value="em.id">
|
||||
{{ em.from_email }}
|
||||
</option>
|
||||
</select>
|
||||
<div class="actions">
|
||||
<button type="submit">Сохранить</button>
|
||||
<button type="button" @click="emit('cancel')">Отмена</button>
|
||||
</div>
|
||||
</form>
|
||||
<RuleEditor v-if="showRuleEditor" :rule="editingRule" @close="onRuleEditorClose" />
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { ref, onMounted, computed } from 'vue';
|
||||
import axios from 'axios';
|
||||
import RuleEditor from '../../components/ai-assistant/RuleEditor.vue';
|
||||
const emit = defineEmits(['cancel']);
|
||||
const settings = ref({ system_prompt: '', model: '', selected_rag_tables: [], languages: [], rules_id: null });
|
||||
const languagesInput = ref('');
|
||||
const userTables = ref([]);
|
||||
const ragTables = computed(() => userTables.value.filter(t => t.is_rag_source_id === 1));
|
||||
const rulesList = ref([]);
|
||||
const showRuleEditor = ref(false);
|
||||
const editingRule = ref(null);
|
||||
const telegramBots = ref([]);
|
||||
const emailList = ref([]);
|
||||
|
||||
const selectedRule = computed(() => rulesList.value.find(r => r.id === settings.value.rules_id) || null);
|
||||
|
||||
async function loadUserTables() {
|
||||
const { data } = await axios.get('/api/tables');
|
||||
userTables.value = Array.isArray(data) ? data : [];
|
||||
}
|
||||
async function loadRules() {
|
||||
const { data } = await axios.get('/api/settings/ai-assistant-rules');
|
||||
rulesList.value = data.rules || [];
|
||||
}
|
||||
async function loadSettings() {
|
||||
const { data } = await axios.get('/api/settings/ai-assistant');
|
||||
if (data.success && data.settings) {
|
||||
settings.value = data.settings;
|
||||
languagesInput.value = (data.settings.languages || []).join(', ');
|
||||
}
|
||||
}
|
||||
async function loadTelegramBots() {
|
||||
const { data } = await axios.get('/api/settings/telegram-settings');
|
||||
telegramBots.value = data.items || [];
|
||||
}
|
||||
async function loadEmailList() {
|
||||
const { data } = await axios.get('/api/settings/email-settings');
|
||||
emailList.value = data.items || [];
|
||||
}
|
||||
onMounted(() => {
|
||||
loadSettings();
|
||||
loadUserTables();
|
||||
loadRules();
|
||||
loadTelegramBots();
|
||||
loadEmailList();
|
||||
});
|
||||
|
||||
async function saveSettings() {
|
||||
settings.value.languages = languagesInput.value.split(',').map(s => s.trim()).filter(Boolean);
|
||||
await axios.put('/api/settings/ai-assistant', settings.value);
|
||||
emit('cancel');
|
||||
}
|
||||
|
||||
function openRuleEditor(ruleId = null) {
|
||||
if (ruleId) {
|
||||
editingRule.value = rulesList.value.find(r => r.id === ruleId) || null;
|
||||
} else {
|
||||
editingRule.value = null;
|
||||
}
|
||||
showRuleEditor.value = true;
|
||||
}
|
||||
|
||||
async function deleteRule(ruleId) {
|
||||
if (!confirm('Удалить этот набор правил?')) return;
|
||||
await axios.delete(`/api/settings/ai-assistant-rules/${ruleId}`);
|
||||
await loadRules();
|
||||
if (settings.value.rules_id === ruleId) settings.value.rules_id = null;
|
||||
}
|
||||
|
||||
async function onRuleEditorClose(updated) {
|
||||
showRuleEditor.value = false;
|
||||
editingRule.value = null;
|
||||
if (updated) await loadRules();
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.ai-assistant-settings-modal {
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
|
||||
padding: 2rem;
|
||||
max-width: 540px;
|
||||
margin: 2rem auto;
|
||||
}
|
||||
label {
|
||||
display: block;
|
||||
margin-top: 1rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
textarea, input, select {
|
||||
width: 100%;
|
||||
margin-top: 0.5rem;
|
||||
padding: 0.5rem;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #ddd;
|
||||
font-size: 1rem;
|
||||
}
|
||||
select[multiple] {
|
||||
min-height: 80px;
|
||||
}
|
||||
.rules-row {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.rules-json {
|
||||
background: #f7f7f7;
|
||||
border-radius: 6px;
|
||||
padding: 0.5rem;
|
||||
font-size: 0.95em;
|
||||
margin-top: 0.5rem;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
button[type="submit"], .actions button {
|
||||
background: var(--color-primary);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 0.5rem 1.5rem;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
}
|
||||
button[type="button"] {
|
||||
background: #eee;
|
||||
color: #333;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 0.5rem 1.5rem;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
}
|
||||
.modal-bg {
|
||||
position: fixed;
|
||||
top: 0; left: 0; right: 0; bottom: 0;
|
||||
background: rgba(0,0,0,0.25);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
.modal {
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 16px rgba(0,0,0,0.12);
|
||||
padding: 2rem;
|
||||
min-width: 320px;
|
||||
max-width: 420px;
|
||||
}
|
||||
.error {
|
||||
color: #c00;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
.rag-table-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0 0 1em 0;
|
||||
}
|
||||
.rag-table-link {
|
||||
color: #2ecc40;
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
}
|
||||
.rag-table-link:hover {
|
||||
color: #27ae38;
|
||||
}
|
||||
</style>
|
||||
@@ -1,46 +1,37 @@
|
||||
<template>
|
||||
<div class="ai-settings settings-panel">
|
||||
<div class="ai-settings settings-panel" style="position:relative">
|
||||
<button class="close-btn" @click="$router.push('/settings')">×</button>
|
||||
<h2>Интеграции</h2>
|
||||
<div class="integration-blocks" v-if="!showProvider && !showEmailSettings && !showTelegramSettings && !showDbSettings">
|
||||
<div class="integration-block">
|
||||
<h3>OpenAI</h3>
|
||||
<p>Интеграция с OpenAI (GPT-4, GPT-3.5 и др.).</p>
|
||||
<button class="details-btn" @click="showProvider = 'openai'">Подробнее</button>
|
||||
</div>
|
||||
<div class="integration-block">
|
||||
<h3>Anthropic</h3>
|
||||
<p>Интеграция с Anthropic Claude (Claude 3 и др.).</p>
|
||||
<button class="details-btn" @click="showProvider = 'anthropic'">Подробнее</button>
|
||||
</div>
|
||||
<div class="integration-block">
|
||||
<h3>Google Gemini</h3>
|
||||
<p>Интеграция с Google Gemini (Gemini 1.5, 1.0 и др.).</p>
|
||||
<button class="details-btn" @click="showProvider = 'google'">Подробнее</button>
|
||||
<button class="details-btn" @click="$router.push('/settings/ai/openai')">Подробнее</button>
|
||||
</div>
|
||||
<div class="integration-block">
|
||||
<h3>Ollama</h3>
|
||||
<p>Локальные open-source модели через Ollama.</p>
|
||||
<button class="details-btn" @click="showProvider = 'ollama'">Подробнее</button>
|
||||
<button class="details-btn" @click="$router.push('/settings/ai/ollama')">Подробнее</button>
|
||||
</div>
|
||||
<div class="integration-block">
|
||||
<h3>Telegram</h3>
|
||||
<p>Интеграция с Telegram-ботом для уведомлений и авторизации.</p>
|
||||
<button class="details-btn" @click="showTelegramSettings = true">Подробнее</button>
|
||||
<button class="details-btn" @click="$router.push('/settings/ai/telegram')">Подробнее</button>
|
||||
</div>
|
||||
<div class="integration-block">
|
||||
<h3>Email</h3>
|
||||
<p>Интеграция с Email для отправки писем и уведомлений.</p>
|
||||
<button class="details-btn" @click="showEmailSettings = true">Подробнее</button>
|
||||
<button class="details-btn" @click="$router.push('/settings/ai/email')">Подробнее</button>
|
||||
</div>
|
||||
<div class="integration-block">
|
||||
<h3>База данных</h3>
|
||||
<p>Интеграция с PostgreSQL для хранения данных приложения и управления настройками.</p>
|
||||
<button class="details-btn" @click="showDbSettings = true">Подробнее</button>
|
||||
<button class="details-btn" @click="$router.push('/settings/ai/database')">Подробнее</button>
|
||||
</div>
|
||||
<div class="integration-block">
|
||||
<h3>ИИ-ассистент</h3>
|
||||
<p>Настройки поведения, языков, моделей и правил работы ассистента.</p>
|
||||
<button class="details-btn" @click="showAiAssistantSettings = true">Подробнее</button>
|
||||
<button class="details-btn" @click="$router.push('/settings/ai/assistant')">Подробнее</button>
|
||||
</div>
|
||||
</div>
|
||||
<AIProviderSettings
|
||||
@@ -54,20 +45,12 @@
|
||||
:showBaseUrl="providerLabels[showProvider].showBaseUrl"
|
||||
@cancel="showProvider = null"
|
||||
/>
|
||||
<TelegramSettingsView v-if="showTelegramSettings" @cancel="showTelegramSettings = false" />
|
||||
<EmailSettingsView v-if="showEmailSettings" @cancel="showEmailSettings = false" />
|
||||
<DatabaseSettingsView v-if="showDbSettings" @cancel="showDbSettings = false" />
|
||||
<AiAssistantSettings v-if="showAiAssistantSettings" @cancel="showAiAssistantSettings = false" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import AIProviderSettings from './AIProviderSettings.vue';
|
||||
import TelegramSettingsView from './TelegramSettingsView.vue';
|
||||
import EmailSettingsView from './EmailSettingsView.vue';
|
||||
import DatabaseSettingsView from './DatabaseSettingsView.vue';
|
||||
import AiAssistantSettings from './AiAssistantSettings.vue';
|
||||
|
||||
const showProvider = ref(null);
|
||||
const showTelegramSettings = ref(false);
|
||||
@@ -153,4 +136,19 @@ const providerLabels = {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
.close-btn {
|
||||
position: absolute;
|
||||
top: 18px;
|
||||
right: 18px;
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 2rem;
|
||||
cursor: pointer;
|
||||
color: #bbb;
|
||||
transition: color 0.2s;
|
||||
z-index: 10;
|
||||
}
|
||||
.close-btn:hover {
|
||||
color: #333;
|
||||
}
|
||||
</style>
|
||||
@@ -1,5 +1,6 @@
|
||||
<template>
|
||||
<div class="blockchain-settings settings-panel">
|
||||
<div class="blockchain-settings settings-panel" style="position:relative">
|
||||
<button class="close-btn" @click="goBack">×</button>
|
||||
|
||||
<!-- Панель Создать новое DLE (Digital Legal Entity) -->
|
||||
<div class="sub-settings-panel">
|
||||
@@ -320,6 +321,7 @@ import axios from 'axios'; // Предполагаем, что axios досту
|
||||
import { useAuthContext } from '@/composables/useAuth'; // Импортируем composable useAuth
|
||||
import dleService from '@/services/dleService';
|
||||
import useBlockchainNetworks from '@/composables/useBlockchainNetworks'; // Импортируем composable для работы с сетями
|
||||
import { useRouter } from 'vue-router';
|
||||
// TODO: Импортировать API
|
||||
|
||||
const { address, isAdmin, auth, user } = useAuthContext(); // Получаем объект адреса и статус админа
|
||||
@@ -340,6 +342,8 @@ const {
|
||||
loadingNetworks
|
||||
} = useBlockchainNetworks();
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
// Добавляем настройки безопасности и подключения
|
||||
const securitySettings = reactive({
|
||||
rpcConfigs: [], // Массив для хранения { networkId: string, rpcUrl: string, chainId: number }
|
||||
@@ -995,6 +999,8 @@ const testRpcHandler = async (rpc) => {
|
||||
}
|
||||
};
|
||||
|
||||
const goBack = () => router.push('/settings');
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -1287,4 +1293,20 @@ h3 {
|
||||
background-color: #a0d2dc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
position: absolute;
|
||||
top: 18px;
|
||||
right: 18px;
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 2rem;
|
||||
cursor: pointer;
|
||||
color: #bbb;
|
||||
transition: color 0.2s;
|
||||
z-index: 10;
|
||||
}
|
||||
.close-btn:hover {
|
||||
color: #333;
|
||||
}
|
||||
</style>
|
||||
@@ -1,171 +0,0 @@
|
||||
<template>
|
||||
<div class="db-settings settings-panel">
|
||||
<h2>Настройки базы данных</h2>
|
||||
<form v-if="editMode" @submit.prevent="saveDbSettings" class="settings-form">
|
||||
<div class="form-group">
|
||||
<label for="dbHost">Host</label>
|
||||
<input id="dbHost" v-model="form.dbHost" type="text" required />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="dbPort">Port</label>
|
||||
<input id="dbPort" v-model.number="form.dbPort" type="number" required />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="dbName">Database</label>
|
||||
<input id="dbName" v-model="form.dbName" type="text" required />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="dbUser">User</label>
|
||||
<input id="dbUser" v-model="form.dbUser" type="text" required />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="dbPassword">Password</label>
|
||||
<input id="dbPassword" v-model="form.dbPassword" type="password" :placeholder="form.dbPassword ? 'Изменить пароль' : 'Введите пароль'" />
|
||||
</div>
|
||||
<button type="submit" class="save-btn">Сохранить</button>
|
||||
<button type="button" class="cancel-btn" @click="cancelEdit">Отмена</button>
|
||||
</form>
|
||||
<div v-else class="settings-view">
|
||||
<div class="view-row"><span>Host:</span> <b>{{ form.dbHost }}</b></div>
|
||||
<div class="view-row"><span>Port:</span> <b>{{ form.dbPort }}</b></div>
|
||||
<div class="view-row"><span>Database:</span> <b>{{ form.dbName }}</b></div>
|
||||
<div class="view-row"><span>User:</span> <b>{{ form.dbUser }}</b></div>
|
||||
<div class="view-row"><span>Password:</span> <b>••••••••••••••••••••••••••••••••</b></div>
|
||||
<button type="button" class="edit-btn" @click="editMode = true">Изменить</button>
|
||||
<button type="button" class="cancel-btn" @click="$emit('cancel')">Закрыть</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { reactive, ref, onMounted } from 'vue';
|
||||
import api from '@/api/axios';
|
||||
|
||||
const form = reactive({
|
||||
dbHost: '',
|
||||
dbPort: 5432,
|
||||
dbName: '',
|
||||
dbUser: '',
|
||||
dbPassword: ''
|
||||
});
|
||||
const original = reactive({});
|
||||
const editMode = ref(false);
|
||||
|
||||
const loadDbSettings = async () => {
|
||||
try {
|
||||
const res = await api.get('/api/db-settings');
|
||||
if (res.data.success) {
|
||||
const s = res.data.settings;
|
||||
form.dbHost = s.db_host;
|
||||
form.dbPort = s.db_port;
|
||||
form.dbName = s.db_name;
|
||||
form.dbUser = s.db_user;
|
||||
form.dbPassword = '';
|
||||
Object.assign(original, JSON.parse(JSON.stringify(form)));
|
||||
}
|
||||
} catch (e) {
|
||||
// обработка ошибки
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
await loadDbSettings();
|
||||
editMode.value = false;
|
||||
});
|
||||
|
||||
const saveDbSettings = async () => {
|
||||
try {
|
||||
await api.put('/api/db-settings', {
|
||||
db_host: form.dbHost,
|
||||
db_port: form.dbPort,
|
||||
db_name: form.dbName,
|
||||
db_user: form.dbUser,
|
||||
db_password: form.dbPassword || undefined
|
||||
});
|
||||
alert('Настройки базы данных сохранены');
|
||||
form.dbPassword = '';
|
||||
Object.assign(original, JSON.parse(JSON.stringify(form)));
|
||||
editMode.value = false;
|
||||
} catch (e) {
|
||||
alert('Ошибка сохранения настроек базы данных');
|
||||
}
|
||||
};
|
||||
|
||||
const cancelEdit = () => {
|
||||
Object.assign(form, JSON.parse(JSON.stringify(original)));
|
||||
form.dbPassword = '';
|
||||
editMode.value = false;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.settings-panel {
|
||||
padding: var(--block-padding);
|
||||
background-color: var(--color-light);
|
||||
border-radius: var(--radius-md);
|
||||
margin-top: var(--spacing-lg);
|
||||
max-width: 500px;
|
||||
}
|
||||
.settings-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.save-btn {
|
||||
background: var(--color-primary);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 0.5rem 1.5rem;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
.save-btn:hover {
|
||||
background: var(--color-primary-dark);
|
||||
}
|
||||
.cancel-btn {
|
||||
background: #eee;
|
||||
color: #333;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 0.5rem 1.5rem;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
margin-left: 1rem;
|
||||
}
|
||||
.settings-view {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.2rem;
|
||||
}
|
||||
.view-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 1rem;
|
||||
background: #f8f8f8;
|
||||
border-radius: 4px;
|
||||
padding: 0.5rem 1rem;
|
||||
}
|
||||
.edit-btn {
|
||||
background: var(--color-primary);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 0.5rem 1.5rem;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
align-self: flex-end;
|
||||
margin-top: 1.5rem;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
.edit-btn:hover {
|
||||
background: var(--color-primary-dark);
|
||||
}
|
||||
</style>
|
||||
@@ -1,186 +0,0 @@
|
||||
<template>
|
||||
<div class="email-settings settings-panel">
|
||||
<h2>Настройки Email</h2>
|
||||
<form v-if="editMode" @submit.prevent="saveEmailSettings" class="settings-form">
|
||||
<div class="form-group">
|
||||
<label for="smtpHost">SMTP Host</label>
|
||||
<input id="smtpHost" v-model="form.smtpHost" type="text" required />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="smtpPort">SMTP Port</label>
|
||||
<input id="smtpPort" v-model.number="form.smtpPort" type="number" required />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="smtpUser">SMTP User</label>
|
||||
<input id="smtpUser" v-model="form.smtpUser" type="text" required />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="smtpPassword">SMTP Password</label>
|
||||
<input id="smtpPassword" v-model="form.smtpPassword" type="password" :placeholder="form.smtpPassword ? 'Изменить пароль' : 'Введите пароль'" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="imapHost">IMAP Host</label>
|
||||
<input id="imapHost" v-model="form.imapHost" type="text" required />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="imapPort">IMAP Port</label>
|
||||
<input id="imapPort" v-model.number="form.imapPort" type="number" required />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="fromEmail">From Email</label>
|
||||
<input id="fromEmail" v-model="form.fromEmail" type="email" required />
|
||||
</div>
|
||||
<button type="submit" class="save-btn">Сохранить</button>
|
||||
<button type="button" class="cancel-btn" @click="cancelEdit">Отмена</button>
|
||||
</form>
|
||||
<div v-else class="settings-view">
|
||||
<div class="view-row"><span>SMTP Host:</span> <b>{{ form.smtpHost }}</b></div>
|
||||
<div class="view-row"><span>SMTP Port:</span> <b>{{ form.smtpPort }}</b></div>
|
||||
<div class="view-row"><span>SMTP User:</span> <b>{{ form.smtpUser }}</b></div>
|
||||
<div class="view-row"><span>IMAP Host:</span> <b>{{ form.imapHost }}</b></div>
|
||||
<div class="view-row"><span>IMAP Port:</span> <b>{{ form.imapPort }}</b></div>
|
||||
<div class="view-row"><span>From Email:</span> <b>{{ form.fromEmail }}</b></div>
|
||||
<button type="button" class="edit-btn" @click="editMode = true">Изменить</button>
|
||||
<button type="button" class="cancel-btn" @click="$emit('cancel')">Закрыть</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { reactive, ref, onMounted } from 'vue';
|
||||
import api from '@/api/axios';
|
||||
|
||||
const form = reactive({
|
||||
smtpHost: '',
|
||||
smtpPort: 465,
|
||||
smtpUser: '',
|
||||
smtpPassword: '',
|
||||
imapHost: '',
|
||||
imapPort: 993,
|
||||
fromEmail: ''
|
||||
});
|
||||
const original = reactive({});
|
||||
const editMode = ref(false);
|
||||
|
||||
const loadEmailSettings = async () => {
|
||||
try {
|
||||
const res = await api.get('/api/email-settings');
|
||||
if (res.data.success) {
|
||||
const s = res.data.settings;
|
||||
form.smtpHost = s.smtp_host;
|
||||
form.smtpPort = s.smtp_port;
|
||||
form.smtpUser = s.smtp_user;
|
||||
form.imapHost = s.imap_host || '';
|
||||
form.imapPort = s.imap_port || 993;
|
||||
form.fromEmail = s.from_email;
|
||||
form.smtpPassword = '';
|
||||
Object.assign(original, JSON.parse(JSON.stringify(form)));
|
||||
}
|
||||
} catch (e) {
|
||||
// обработка ошибки
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
await loadEmailSettings();
|
||||
editMode.value = false;
|
||||
});
|
||||
|
||||
const saveEmailSettings = async () => {
|
||||
try {
|
||||
await api.put('/api/email-settings', {
|
||||
smtp_host: form.smtpHost,
|
||||
smtp_port: form.smtpPort,
|
||||
smtp_user: form.smtpUser,
|
||||
smtp_password: form.smtpPassword || undefined,
|
||||
imap_host: form.imapHost,
|
||||
imap_port: form.imapPort,
|
||||
from_email: form.fromEmail
|
||||
});
|
||||
alert('Настройки Email сохранены');
|
||||
form.smtpPassword = '';
|
||||
Object.assign(original, JSON.parse(JSON.stringify(form)));
|
||||
editMode.value = false;
|
||||
} catch (e) {
|
||||
alert('Ошибка сохранения email-настроек');
|
||||
}
|
||||
};
|
||||
|
||||
const cancelEdit = () => {
|
||||
Object.assign(form, JSON.parse(JSON.stringify(original)));
|
||||
form.smtpPassword = '';
|
||||
editMode.value = false;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.settings-panel {
|
||||
padding: var(--block-padding);
|
||||
background-color: var(--color-light);
|
||||
border-radius: var(--radius-md);
|
||||
margin-top: var(--spacing-lg);
|
||||
max-width: 500px;
|
||||
}
|
||||
.settings-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.save-btn {
|
||||
background: var(--color-primary);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 0.5rem 1.5rem;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
.save-btn:hover {
|
||||
background: var(--color-primary-dark);
|
||||
}
|
||||
.cancel-btn {
|
||||
background: #eee;
|
||||
color: #333;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 0.5rem 1.5rem;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
margin-left: 1rem;
|
||||
}
|
||||
.settings-view {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.2rem;
|
||||
}
|
||||
.view-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 1rem;
|
||||
background: #f8f8f8;
|
||||
border-radius: 4px;
|
||||
padding: 0.5rem 1rem;
|
||||
}
|
||||
.edit-btn {
|
||||
background: var(--color-primary);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 0.5rem 1.5rem;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
align-self: flex-end;
|
||||
margin-top: 1.5rem;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
.edit-btn:hover {
|
||||
background: var(--color-primary-dark);
|
||||
}
|
||||
</style>
|
||||
@@ -1,5 +1,6 @@
|
||||
<template>
|
||||
<div class="interface-settings settings-panel">
|
||||
<div class="interface-settings settings-panel" style="position:relative;min-height:120px">
|
||||
<button class="close-btn" @click="goBack">×</button>
|
||||
<h2>Настройки Интерфейса</h2>
|
||||
|
||||
<!-- Панель Язык -->
|
||||
@@ -27,9 +28,13 @@
|
||||
import { ref } from 'vue';
|
||||
import { getFromStorage, setToStorage } from '../../utils/storage'; // Путь к utils может отличаться
|
||||
import DomainConnectBlock from './DomainConnectBlock.vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
// TODO: Импортировать API для сохранения, если нужно
|
||||
|
||||
const selectedLanguage = ref(getFromStorage('userLanguage', 'ru'));
|
||||
const router = useRouter();
|
||||
|
||||
const goBack = () => router.push('/settings');
|
||||
|
||||
// Функция сохранения
|
||||
const saveLanguageSetting = () => {
|
||||
@@ -87,6 +92,21 @@ h3 {
|
||||
.btn-primary {
|
||||
align-self: flex-start;
|
||||
}
|
||||
.close-btn {
|
||||
position: absolute;
|
||||
top: 18px;
|
||||
right: 18px;
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 2rem;
|
||||
cursor: pointer;
|
||||
color: #bbb;
|
||||
transition: color 0.2s;
|
||||
z-index: 10;
|
||||
}
|
||||
.close-btn:hover {
|
||||
color: #333;
|
||||
}
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
<template>
|
||||
<div class="ollama-settings settings-panel">
|
||||
<h2>Настройки Ollama</h2>
|
||||
<div class="current-model-block">
|
||||
<span>Текущая модель:</span>
|
||||
<b>{{ currentModel }}</b>
|
||||
</div>
|
||||
<div class="select-model-block">
|
||||
<label for="ollamaModel">Доступные модели для загрузки:</label>
|
||||
<select id="ollamaModel" v-model="selectedModel">
|
||||
<option v-for="model in availableModels" :key="model" :value="model">{{ model }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<button class="close-btn" @click="$emit('cancel')">Закрыть</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
// TODO: заменить на реальный API Ollama
|
||||
const currentModel = ref('qwen2.5');
|
||||
const availableModels = ref(['qwen2.5', 'llama3', 'mistral', 'phi3', 'gemma']);
|
||||
const selectedModel = ref(currentModel.value);
|
||||
|
||||
onMounted(() => {
|
||||
// Здесь будет запрос к Ollama для получения списка моделей и текущей
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.ollama-settings.settings-panel {
|
||||
padding: var(--block-padding);
|
||||
background-color: var(--color-light);
|
||||
border-radius: var(--radius-md);
|
||||
margin-top: var(--spacing-lg);
|
||||
max-width: 500px;
|
||||
}
|
||||
.current-model-block {
|
||||
margin-bottom: 1.5rem;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
.select-model-block {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
select {
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #ccc;
|
||||
font-size: 1rem;
|
||||
}
|
||||
.close-btn {
|
||||
background: #eee;
|
||||
color: #333;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 0.5rem 1.5rem;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
}
|
||||
.close-btn:hover {
|
||||
background: #ddd;
|
||||
}
|
||||
</style>
|
||||
@@ -1 +0,0 @@
|
||||
<!-- Компонент удалён как неактуальный -->
|
||||
@@ -1,5 +1,6 @@
|
||||
<template>
|
||||
<div class="security-settings settings-panel">
|
||||
<button class="close-btn" @click="goBack">×</button>
|
||||
<h2>Настройки безопасности и подключения к блокчейну</h2>
|
||||
|
||||
<!-- Индикатор загрузки -->
|
||||
@@ -59,6 +60,7 @@ import useBlockchainNetworks from '@/composables/useBlockchainNetworks';
|
||||
import eventBus from '@/utils/eventBus';
|
||||
import RpcProvidersSettings from './RpcProvidersSettings.vue';
|
||||
import AuthTokensSettings from './AuthTokensSettings.vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
// Состояние для отображения/скрытия дополнительных настроек
|
||||
const showRpcSettings = ref(false);
|
||||
@@ -288,6 +290,9 @@ provide('removeAuthToken', removeAuthToken);
|
||||
provide('addAuthToken', addAuthToken);
|
||||
provide('newAuthToken', newAuthToken);
|
||||
provide('networks', networks);
|
||||
|
||||
const router = useRouter();
|
||||
const goBack = () => router.push('/settings');
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -573,4 +578,20 @@ small {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
position: absolute;
|
||||
top: 18px;
|
||||
right: 18px;
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 2rem;
|
||||
cursor: pointer;
|
||||
color: #bbb;
|
||||
transition: color 0.2s;
|
||||
z-index: 10;
|
||||
}
|
||||
.close-btn:hover {
|
||||
color: #333;
|
||||
}
|
||||
</style>
|
||||
65
frontend/src/views/settings/SettingsIndexView.vue
Normal file
65
frontend/src/views/settings/SettingsIndexView.vue
Normal file
@@ -0,0 +1,65 @@
|
||||
<template>
|
||||
<div class="main-blocks">
|
||||
<div class="main-block">
|
||||
<h3>ИИ</h3>
|
||||
<p>Настройки интеграций, моделей, ассистента и RAG.</p>
|
||||
<button class="details-btn" @click="$router.push('/settings/ai')">Подробнее</button>
|
||||
</div>
|
||||
<div class="main-block">
|
||||
<h3>Блокчейн</h3>
|
||||
<p>Интеграция с блокчейн-сетями, RPC, токены и смарт-контракты.</p>
|
||||
<button class="details-btn" @click="$router.push('/settings/blockchain')">Подробнее</button>
|
||||
</div>
|
||||
<div class="main-block">
|
||||
<h3>Безопасность</h3>
|
||||
<p>Управление доступом, токенами, аутентификацией и правами.</p>
|
||||
<button class="details-btn" @click="$router.push('/settings/security')">Подробнее</button>
|
||||
</div>
|
||||
<div class="main-block">
|
||||
<h3>Интерфейс</h3>
|
||||
<p>Настройки внешнего вида, локализации и пользовательского опыта.</p>
|
||||
<button class="details-btn" @click="$router.push('/settings/interface')">Подробнее</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
// пусто
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.main-blocks {
|
||||
display: flex;
|
||||
gap: 2rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.main-block {
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.04);
|
||||
padding: 2rem;
|
||||
min-width: 260px;
|
||||
flex: 1 1 300px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.details-btn {
|
||||
margin-top: 1.5rem;
|
||||
background: var(--color-primary);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 0.5rem 1.5rem;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
.details-btn:hover {
|
||||
background: var(--color-primary-dark);
|
||||
}
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
</style>
|
||||
@@ -1,147 +0,0 @@
|
||||
<template>
|
||||
<div class="telegram-settings settings-panel">
|
||||
<h2>Настройки Telegram</h2>
|
||||
<form v-if="editMode" @submit.prevent="saveTelegramSettings" class="settings-form">
|
||||
<div class="form-group">
|
||||
<label for="botToken">Bot Token</label>
|
||||
<input id="botToken" v-model="form.botToken" type="text" required />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="botUsername">Bot Username</label>
|
||||
<input id="botUsername" v-model="form.botUsername" type="text" required />
|
||||
</div>
|
||||
<button type="submit" class="save-btn">Сохранить</button>
|
||||
<button type="button" class="cancel-btn" @click="cancelEdit">Отмена</button>
|
||||
</form>
|
||||
<div v-else class="settings-view">
|
||||
<div class="view-row"><span>Bot Token:</span> <b>••••••••••••••••••••••••••••••••</b></div>
|
||||
<div class="view-row"><span>Bot Username:</span> <b>{{ form.botUsername }}</b></div>
|
||||
<button type="button" class="edit-btn" @click="editMode = true">Изменить</button>
|
||||
<button type="button" class="cancel-btn" @click="$emit('cancel')">Закрыть</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { reactive, ref, onMounted } from 'vue';
|
||||
import api from '@/api/axios';
|
||||
|
||||
const form = reactive({
|
||||
botToken: '',
|
||||
botUsername: ''
|
||||
});
|
||||
const original = reactive({});
|
||||
const editMode = ref(false);
|
||||
|
||||
const loadTelegramSettings = async () => {
|
||||
try {
|
||||
const res = await api.get('/api/telegram-settings');
|
||||
if (res.data.success) {
|
||||
const s = res.data.settings;
|
||||
form.botToken = '';
|
||||
form.botUsername = s.bot_username;
|
||||
Object.assign(original, JSON.parse(JSON.stringify(form)));
|
||||
}
|
||||
} catch (e) {
|
||||
// обработка ошибки
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
await loadTelegramSettings();
|
||||
editMode.value = false;
|
||||
});
|
||||
|
||||
const saveTelegramSettings = async () => {
|
||||
try {
|
||||
await api.put('/api/telegram-settings', {
|
||||
bot_token: form.botToken,
|
||||
bot_username: form.botUsername
|
||||
});
|
||||
alert('Настройки Telegram сохранены');
|
||||
form.botToken = '';
|
||||
Object.assign(original, JSON.parse(JSON.stringify(form)));
|
||||
editMode.value = false;
|
||||
} catch (e) {
|
||||
alert('Ошибка сохранения telegram-настроек');
|
||||
}
|
||||
};
|
||||
|
||||
const cancelEdit = () => {
|
||||
Object.assign(form, JSON.parse(JSON.stringify(original)));
|
||||
form.botToken = '';
|
||||
editMode.value = false;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.settings-panel {
|
||||
padding: var(--block-padding);
|
||||
background-color: var(--color-light);
|
||||
border-radius: var(--radius-md);
|
||||
margin-top: var(--spacing-lg);
|
||||
max-width: 500px;
|
||||
}
|
||||
.settings-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.save-btn {
|
||||
background: var(--color-primary);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 0.5rem 1.5rem;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
.save-btn:hover {
|
||||
background: var(--color-primary-dark);
|
||||
}
|
||||
.cancel-btn {
|
||||
background: #eee;
|
||||
color: #333;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 0.5rem 1.5rem;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
margin-left: 1rem;
|
||||
}
|
||||
.settings-view {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.2rem;
|
||||
}
|
||||
.view-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 1rem;
|
||||
background: #f8f8f8;
|
||||
border-radius: 4px;
|
||||
padding: 0.5rem 1rem;
|
||||
}
|
||||
.edit-btn {
|
||||
background: var(--color-primary);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 0.5rem 1.5rem;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
align-self: flex-end;
|
||||
margin-top: 1.5rem;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
.edit-btn:hover {
|
||||
background: var(--color-primary-dark);
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user