ваше сообщение коммита
This commit is contained in:
203
frontend/src/views/settings/AIProviderSettings.vue
Normal file
203
frontend/src/views/settings/AIProviderSettings.vue
Normal file
@@ -0,0 +1,203 @@
|
||||
<template>
|
||||
<div class="ai-provider-settings settings-panel">
|
||||
<h2>{{ label }}</h2>
|
||||
<p class="desc">{{ description }}</p>
|
||||
<form @submit.prevent="onSave">
|
||||
<div v-if="showApiKey">
|
||||
<label>API Key:</label>
|
||||
<input type="password" v-model="apiKey" :placeholder="apiKeyPlaceholder" />
|
||||
<button type="button" class="verify-btn" @click="onVerify" :disabled="verifying">Verify</button>
|
||||
<span v-if="verifyStatus === true" class="ok">✔️</span>
|
||||
<span v-if="verifyStatus === false" class="error">Ошибка: {{ verifyError }}</span>
|
||||
</div>
|
||||
<div v-if="showBaseUrl">
|
||||
<label>Base URL:</label>
|
||||
<input type="text" v-model="baseUrl" :placeholder="baseUrlPlaceholder" />
|
||||
</div>
|
||||
<div v-if="models.length">
|
||||
<label>Модель:</label>
|
||||
<select v-model="selectedModel">
|
||||
<option v-for="model in models" :key="model.id || model" :value="model.id || model">
|
||||
{{ model.id || model }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button type="submit" :disabled="saving">Сохранить</button>
|
||||
<button type="button" @click="onDelete" v-if="hasSettings">Удалить ключ</button>
|
||||
<button type="button" @click="$emit('cancel')">Закрыть</button>
|
||||
</div>
|
||||
<div v-if="saveStatus === true" class="ok">Сохранено!</div>
|
||||
<div v-if="saveStatus === false" class="error">Ошибка: {{ saveError }}</div>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, watch } from 'vue';
|
||||
import axios from 'axios';
|
||||
const props = defineProps({
|
||||
provider: { type: String, required: true },
|
||||
label: { type: String, required: true },
|
||||
description: { type: String, default: '' },
|
||||
showApiKey: { type: Boolean, default: true },
|
||||
showBaseUrl: { type: Boolean, default: true },
|
||||
apiKeyPlaceholder: { type: String, default: '' },
|
||||
baseUrlPlaceholder: { type: String, default: '' },
|
||||
});
|
||||
|
||||
const apiKey = ref('');
|
||||
const baseUrl = ref('');
|
||||
const selectedModel = ref('');
|
||||
const models = ref([]);
|
||||
const hasSettings = ref(false);
|
||||
const verifying = ref(false);
|
||||
const verifyStatus = ref(null);
|
||||
const verifyError = ref('');
|
||||
const saving = ref(false);
|
||||
const saveStatus = ref(null);
|
||||
const saveError = ref('');
|
||||
|
||||
async function loadSettings() {
|
||||
try {
|
||||
const { data } = await axios.get(`/api/settings/ai-settings/${props.provider}`);
|
||||
if (data.settings) {
|
||||
apiKey.value = data.settings.api_key || '';
|
||||
baseUrl.value = data.settings.base_url || '';
|
||||
selectedModel.value = data.settings.selected_model || '';
|
||||
hasSettings.value = true;
|
||||
if (apiKey.value || props.provider === 'ollama') {
|
||||
await loadModels();
|
||||
}
|
||||
} else {
|
||||
hasSettings.value = false;
|
||||
}
|
||||
} catch (e) {
|
||||
hasSettings.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadModels() {
|
||||
try {
|
||||
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];
|
||||
}
|
||||
} catch (e) {
|
||||
models.value = [];
|
||||
}
|
||||
}
|
||||
|
||||
async function onVerify() {
|
||||
verifying.value = true;
|
||||
verifyStatus.value = null;
|
||||
verifyError.value = '';
|
||||
try {
|
||||
const { data } = await axios.post(`/api/settings/ai-settings/${props.provider}/verify`, {
|
||||
api_key: apiKey.value,
|
||||
base_url: baseUrl.value,
|
||||
});
|
||||
verifyStatus.value = data.success;
|
||||
if (data.success) {
|
||||
await loadModels();
|
||||
}
|
||||
} catch (e) {
|
||||
verifyStatus.value = false;
|
||||
verifyError.value = e.response?.data?.error || e.message;
|
||||
} finally {
|
||||
verifying.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function onSave() {
|
||||
saving.value = true;
|
||||
saveStatus.value = null;
|
||||
saveError.value = '';
|
||||
try {
|
||||
await axios.put(`/api/settings/ai-settings/${props.provider}`, {
|
||||
api_key: apiKey.value,
|
||||
base_url: baseUrl.value,
|
||||
selected_model: selectedModel.value,
|
||||
});
|
||||
saveStatus.value = true;
|
||||
hasSettings.value = true;
|
||||
} catch (e) {
|
||||
saveStatus.value = false;
|
||||
saveError.value = e.response?.data?.error || e.message;
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function onDelete() {
|
||||
await axios.delete(`/api/settings/ai-settings/${props.provider}`);
|
||||
apiKey.value = '';
|
||||
baseUrl.value = '';
|
||||
selectedModel.value = '';
|
||||
models.value = [];
|
||||
hasSettings.value = false;
|
||||
}
|
||||
|
||||
onMounted(loadSettings);
|
||||
watch([apiKey, baseUrl], () => {
|
||||
verifyStatus.value = null;
|
||||
verifyError.value = '';
|
||||
saveStatus.value = null;
|
||||
saveError.value = '';
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.ai-provider-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;
|
||||
}
|
||||
.desc {
|
||||
color: #666;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 0.3rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
input[type="password"], input[type="text"], select {
|
||||
width: 100%;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #ccc;
|
||||
font-size: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.verify-btn {
|
||||
margin-left: 0.5rem;
|
||||
background: var(--color-primary);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 0.4rem 1.2rem;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
.verify-btn:hover {
|
||||
background: var(--color-primary-dark);
|
||||
}
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
.ok {
|
||||
color: #2cae4f;
|
||||
margin-left: 1rem;
|
||||
}
|
||||
.error {
|
||||
color: #d32f2f;
|
||||
margin-left: 1rem;
|
||||
}
|
||||
</style>
|
||||
@@ -1,26 +1,97 @@
|
||||
<template>
|
||||
<div class="ai-settings settings-panel">
|
||||
<h2>Интеграции</h2>
|
||||
<div class="integration-blocks">
|
||||
<div class="integration-blocks" v-if="!showProvider && !showEmailSettings && !showTelegramSettings">
|
||||
<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>
|
||||
</div>
|
||||
<div class="integration-block">
|
||||
<h3>Ollama</h3>
|
||||
<p>Локальные open-source модели через Ollama.</p>
|
||||
<button class="details-btn" @click="showProvider = 'ollama'">Подробнее</button>
|
||||
</div>
|
||||
<div class="integration-block">
|
||||
<h3>Telegram</h3>
|
||||
<p>Интеграция с Telegram-ботом для уведомлений и авторизации.</p>
|
||||
<button class="details-btn" @click="goToTelegram">Подробнее</button>
|
||||
<button class="details-btn" @click="showTelegramSettings = true">Подробнее</button>
|
||||
</div>
|
||||
<div class="integration-block">
|
||||
<h3>Email</h3>
|
||||
<p>Интеграция с Email для отправки писем и уведомлений.</p>
|
||||
<button class="details-btn" @click="goToEmail">Подробнее</button>
|
||||
<button class="details-btn" @click="showEmailSettings = true">Подробнее</button>
|
||||
</div>
|
||||
</div>
|
||||
<AIProviderSettings
|
||||
v-if="showProvider"
|
||||
:provider="showProvider"
|
||||
:label="providerLabels[showProvider].label"
|
||||
:description="providerLabels[showProvider].description"
|
||||
:apiKeyPlaceholder="providerLabels[showProvider].apiKeyPlaceholder"
|
||||
:baseUrlPlaceholder="providerLabels[showProvider].baseUrlPlaceholder"
|
||||
:showApiKey="providerLabels[showProvider].showApiKey"
|
||||
:showBaseUrl="providerLabels[showProvider].showBaseUrl"
|
||||
@cancel="showProvider = null"
|
||||
/>
|
||||
<TelegramSettingsView v-if="showTelegramSettings" @cancel="showTelegramSettings = false" />
|
||||
<EmailSettingsView v-if="showEmailSettings" @cancel="showEmailSettings = false" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useRouter } from 'vue-router';
|
||||
const router = useRouter();
|
||||
const goToTelegram = () => router.push({ name: 'settings-telegram' });
|
||||
const goToEmail = () => router.push({ name: 'settings-email' });
|
||||
import { ref } from 'vue';
|
||||
import AIProviderSettings from './AIProviderSettings.vue';
|
||||
import TelegramSettingsView from './TelegramSettingsView.vue';
|
||||
import EmailSettingsView from './EmailSettingsView.vue';
|
||||
const showProvider = ref(null);
|
||||
const showTelegramSettings = ref(false);
|
||||
const showEmailSettings = ref(false);
|
||||
|
||||
const providerLabels = {
|
||||
openai: {
|
||||
label: 'OpenAI API Key',
|
||||
description: 'Введите OpenAI API Key и (опционально) Base URL для кастомных endpoint.',
|
||||
apiKeyPlaceholder: 'sk-...',
|
||||
baseUrlPlaceholder: 'https://api.openai.com/v1',
|
||||
showApiKey: true,
|
||||
showBaseUrl: true,
|
||||
},
|
||||
anthropic: {
|
||||
label: 'Anthropic API Key',
|
||||
description: 'Введите Anthropic API Key и (опционально) Base URL.',
|
||||
apiKeyPlaceholder: '...',
|
||||
baseUrlPlaceholder: 'https://api.anthropic.com/v1',
|
||||
showApiKey: true,
|
||||
showBaseUrl: true,
|
||||
},
|
||||
google: {
|
||||
label: 'Google Gemini API Key',
|
||||
description: 'Введите Google Gemini API Key и (опционально) Base URL.',
|
||||
apiKeyPlaceholder: '...',
|
||||
baseUrlPlaceholder: 'https://generativelanguage.googleapis.com/v1beta',
|
||||
showApiKey: true,
|
||||
showBaseUrl: true,
|
||||
},
|
||||
ollama: {
|
||||
label: 'Ollama (локальные модели)',
|
||||
description: 'Настройка Ollama для локальных open-source моделей. Ключ не требуется.',
|
||||
apiKeyPlaceholder: '',
|
||||
baseUrlPlaceholder: 'http://localhost:11434',
|
||||
showApiKey: false,
|
||||
showBaseUrl: true,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
171
frontend/src/views/settings/DatabaseSettingsView.vue
Normal file
171
frontend/src/views/settings/DatabaseSettingsView.vue
Normal file
@@ -0,0 +1,171 @@
|
||||
<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,40 +1,115 @@
|
||||
<template>
|
||||
<div class="email-settings settings-panel">
|
||||
<h2>Настройки Email</h2>
|
||||
<form @submit.prevent="saveEmailSettings" class="settings-form">
|
||||
<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" required />
|
||||
<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 } from 'vue';
|
||||
// TODO: Импортировать API для сохранения
|
||||
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 () => {
|
||||
// TODO: Реализовать вызов API для сохранения
|
||||
alert('Настройки Email сохранены (заглушка)');
|
||||
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>
|
||||
|
||||
@@ -69,4 +144,43 @@ const saveEmailSettings = async () => {
|
||||
.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>
|
||||
63
frontend/src/views/settings/OllamaSettingsView.vue
Normal file
63
frontend/src/views/settings/OllamaSettingsView.vue
Normal file
@@ -0,0 +1,63 @@
|
||||
<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,7 +1,7 @@
|
||||
<template>
|
||||
<div class="telegram-settings settings-panel">
|
||||
<h2>Настройки Telegram</h2>
|
||||
<form @submit.prevent="saveTelegramSettings" class="settings-form">
|
||||
<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 />
|
||||
@@ -11,20 +11,66 @@
|
||||
<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 } from 'vue';
|
||||
// TODO: Импортировать API для сохранения
|
||||
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 () => {
|
||||
// TODO: Реализовать вызов API для сохранения
|
||||
alert('Настройки Telegram сохранены (заглушка)');
|
||||
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>
|
||||
|
||||
@@ -59,4 +105,43 @@ const saveTelegramSettings = async () => {
|
||||
.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