feat: новая функция
This commit is contained in:
@@ -8,8 +8,17 @@ echo "🔧 Настройка nginx с параметрами:"
|
||||
echo " DOMAIN: $DOMAIN"
|
||||
echo " BACKEND_CONTAINER: $BACKEND_CONTAINER"
|
||||
|
||||
# Выбор конфигурации в зависимости от домена
|
||||
if echo "$DOMAIN" | grep -qE '^localhost(:[0-9]+)?$'; then
|
||||
echo " Режим: ЛОКАЛЬНАЯ РАЗРАБОТКА (без SSL)"
|
||||
TEMPLATE_FILE="/etc/nginx/nginx-local.conf.template"
|
||||
else
|
||||
echo " Режим: ПРОДАКШН (с SSL)"
|
||||
TEMPLATE_FILE="/etc/nginx/nginx-ssl.conf.template"
|
||||
fi
|
||||
|
||||
# Обработка переменных окружения для nginx конфигурации
|
||||
envsubst '${DOMAIN} ${BACKEND_CONTAINER}' < /etc/nginx/nginx.conf.template > /etc/nginx/nginx.conf
|
||||
envsubst '${DOMAIN} ${BACKEND_CONTAINER}' < $TEMPLATE_FILE > /etc/nginx/nginx.conf
|
||||
|
||||
# Проверка синтаксиса nginx конфигурации
|
||||
echo "🔍 Проверка синтаксиса nginx конфигурации..."
|
||||
|
||||
86
frontend/nginx-local.conf
Normal file
86
frontend/nginx-local.conf
Normal file
@@ -0,0 +1,86 @@
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
# Rate limiting для защиты от DDoS
|
||||
limit_req_zone $binary_remote_addr zone=req_limit_per_ip:10m rate=10r/s;
|
||||
limit_req_zone $binary_remote_addr zone=api_limit_per_ip:10m rate=5r/s;
|
||||
|
||||
# HTTP сервер для локальной разработки (БЕЗ SSL)
|
||||
server {
|
||||
listen 80;
|
||||
server_name ${DOMAIN};
|
||||
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
# Healthcheck endpoint
|
||||
location /health {
|
||||
access_log off;
|
||||
return 200 "healthy\n";
|
||||
add_header Content-Type text/plain;
|
||||
}
|
||||
|
||||
# Основной location
|
||||
location / {
|
||||
# Rate limiting для основных страниц
|
||||
limit_req zone=req_limit_per_ip burst=20 nodelay;
|
||||
|
||||
try_files $uri $uri/ /index.html;
|
||||
|
||||
# Базовые заголовки безопасности
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header X-XSS-Protection "1; mode=block" always;
|
||||
}
|
||||
|
||||
# Статические файлы
|
||||
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
add_header Vary Accept-Encoding;
|
||||
|
||||
# Заголовки безопасности для статических файлов
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
}
|
||||
|
||||
# API
|
||||
location /api/ {
|
||||
# Rate limiting для API (более строгое)
|
||||
limit_req zone=api_limit_per_ip burst=10 nodelay;
|
||||
|
||||
proxy_pass http://${BACKEND_CONTAINER}:8000/api/;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
# Заголовки безопасности для API
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header X-XSS-Protection "1; mode=block" always;
|
||||
}
|
||||
|
||||
# WebSocket поддержка
|
||||
location /ws {
|
||||
proxy_pass http://${BACKEND_CONTAINER}:8000/ws;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto http;
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
proxy_set_header X-Forwarded-Port $server_port;
|
||||
}
|
||||
|
||||
# Скрытие информации о сервере
|
||||
server_tokens off;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,8 @@ RUN apk add --no-cache curl
|
||||
COPY --from=frontend-builder /app/dist/ /usr/share/nginx/html/
|
||||
|
||||
# Копируем конфигурацию nginx
|
||||
COPY nginx-simple.conf /etc/nginx/nginx.conf.template
|
||||
COPY nginx-simple.conf /etc/nginx/nginx-ssl.conf.template
|
||||
COPY nginx-local.conf /etc/nginx/nginx-local.conf.template
|
||||
|
||||
# Копируем скрипт запуска
|
||||
COPY docker-entrypoint.sh /docker-entrypoint.sh
|
||||
|
||||
@@ -279,11 +279,14 @@ export function useChat(auth) {
|
||||
}
|
||||
|
||||
// Добавляем ответ ИИ, если есть
|
||||
if (response.data.aiMessage) {
|
||||
if (response.data.aiResponse) {
|
||||
messages.value.push({
|
||||
...response.data.aiMessage,
|
||||
sender_type: 'assistant', // Убедимся, что тип правильный
|
||||
id: `ai_${Date.now()}`,
|
||||
content: response.data.aiResponse.response || response.data.aiResponse,
|
||||
sender_type: 'assistant',
|
||||
role: 'assistant',
|
||||
timestamp: new Date().toISOString(),
|
||||
isLocal: false
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -276,6 +276,11 @@ const routes = [
|
||||
name: 'vds-mock',
|
||||
component: () => import('../views/VdsMockView.vue')
|
||||
},
|
||||
{
|
||||
path: '/connect-wallet',
|
||||
name: 'connect-wallet',
|
||||
component: () => import('../views/ConnectWalletView.vue')
|
||||
},
|
||||
];
|
||||
|
||||
const router = createRouter({
|
||||
|
||||
@@ -15,7 +15,11 @@
|
||||
* @returns {string} - Уникальный ID
|
||||
*/
|
||||
export const generateUniqueId = () => {
|
||||
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
// Генерируем в формате guest_* для совместимости с UniversalGuestService
|
||||
const array = new Uint8Array(16);
|
||||
crypto.getRandomValues(array);
|
||||
const hex = Array.from(array, byte => byte.toString(16).padStart(2, '0')).join('');
|
||||
return `guest_${hex}`;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
369
frontend/src/views/ConnectWalletView.vue
Normal file
369
frontend/src/views/ConnectWalletView.vue
Normal file
@@ -0,0 +1,369 @@
|
||||
<template>
|
||||
<div class="connect-wallet-container">
|
||||
<div class="connect-wallet-card">
|
||||
<!-- Loading состояние -->
|
||||
<div v-if="loading" class="loading-state">
|
||||
<div class="spinner"></div>
|
||||
<p>Проверка токена...</p>
|
||||
</div>
|
||||
|
||||
<!-- Токен валиден -->
|
||||
<div v-else-if="tokenValid && !connected" class="connect-state">
|
||||
<div class="icon">🔗</div>
|
||||
<h1>Подключение кошелька</h1>
|
||||
|
||||
<div class="info-block">
|
||||
<p class="provider-info">
|
||||
Вы переходите из:
|
||||
<strong>{{ providerName }}</strong>
|
||||
</p>
|
||||
<p class="description">
|
||||
Подключите Web3 кошелек для сохранения истории сообщений и полного доступа к системе
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
@click="connectWallet"
|
||||
:disabled="connecting"
|
||||
class="connect-button"
|
||||
>
|
||||
<span v-if="!connecting">Подключить MetaMask</span>
|
||||
<span v-else>Подключение...</span>
|
||||
</button>
|
||||
|
||||
<div v-if="error" class="error-message">
|
||||
{{ error }}
|
||||
</div>
|
||||
|
||||
<div class="expires-info">
|
||||
⏱ Ссылка истекает: {{ expiresAt }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Токен истек или недействителен -->
|
||||
<div v-else-if="!tokenValid" class="expired-state">
|
||||
<div class="icon">⏰</div>
|
||||
<h1>Ссылка истекла</h1>
|
||||
<p>Эта ссылка больше недействительна</p>
|
||||
<p class="hint">
|
||||
Запросите новую ссылку в боте, отправив команду
|
||||
<code>/connect</code>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Успешно подключено -->
|
||||
<div v-else-if="connected" class="success-state">
|
||||
<div class="icon">✅</div>
|
||||
<h1>Кошелек подключен!</h1>
|
||||
<p>История сообщений перенесена</p>
|
||||
<p class="stats" v-if="migrationStats">
|
||||
Перенесено сообщений: {{ migrationStats.migrated }}
|
||||
</p>
|
||||
<button @click="goToChat" class="go-chat-button">
|
||||
Перейти к чату
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'ConnectWalletView',
|
||||
|
||||
data() {
|
||||
return {
|
||||
loading: true,
|
||||
tokenValid: false,
|
||||
connected: false,
|
||||
connecting: false,
|
||||
error: null,
|
||||
provider: null,
|
||||
expiresAt: null,
|
||||
migrationStats: null
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
providerName() {
|
||||
const names = {
|
||||
telegram: 'Telegram',
|
||||
email: 'Email'
|
||||
};
|
||||
return names[this.provider] || this.provider;
|
||||
}
|
||||
},
|
||||
|
||||
async mounted() {
|
||||
const token = this.$route.query.token;
|
||||
if (!token) {
|
||||
this.loading = false;
|
||||
this.tokenValid = false;
|
||||
return;
|
||||
}
|
||||
|
||||
await this.checkToken(token);
|
||||
},
|
||||
|
||||
methods: {
|
||||
async checkToken(token) {
|
||||
try {
|
||||
const response = await fetch(`/api/identity/link-status/${token}`);
|
||||
const data = await response.json();
|
||||
|
||||
this.tokenValid = data.valid;
|
||||
this.provider = data.provider;
|
||||
|
||||
if (data.expiresAt) {
|
||||
const expiresDate = new Date(data.expiresAt);
|
||||
this.expiresAt = expiresDate.toLocaleString('ru-RU');
|
||||
}
|
||||
|
||||
this.loading = false;
|
||||
|
||||
} catch (error) {
|
||||
console.error('Ошибка проверки токена:', error);
|
||||
this.error = 'Ошибка проверки токена';
|
||||
this.loading = false;
|
||||
this.tokenValid = false;
|
||||
}
|
||||
},
|
||||
|
||||
async connectWallet() {
|
||||
try {
|
||||
this.connecting = true;
|
||||
this.error = null;
|
||||
|
||||
// Проверяем наличие MetaMask
|
||||
if (!window.ethereum) {
|
||||
this.error = 'MetaMask не установлен. Установите расширение MetaMask.';
|
||||
this.connecting = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// 1. Запрос аккаунтов
|
||||
const accounts = await window.ethereum.request({
|
||||
method: 'eth_requestAccounts'
|
||||
});
|
||||
|
||||
if (!accounts || accounts.length === 0) {
|
||||
this.error = 'Не удалось получить адрес кошелька';
|
||||
this.connecting = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const address = accounts[0];
|
||||
|
||||
// 2. Получить подпись
|
||||
const message = `Подключение кошелька к системе\nАдрес: ${address}\nВремя: ${new Date().toISOString()}`;
|
||||
|
||||
const signature = await window.ethereum.request({
|
||||
method: 'personal_sign',
|
||||
params: [message, address]
|
||||
});
|
||||
|
||||
// 3. Отправить на сервер
|
||||
const token = this.$route.query.token;
|
||||
const response = await fetch('/api/auth/wallet-with-link', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({
|
||||
address,
|
||||
signature,
|
||||
message,
|
||||
token
|
||||
})
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
this.connected = true;
|
||||
this.migrationStats = {
|
||||
migrated: result.migratedMessages
|
||||
};
|
||||
|
||||
// Через 2 секунды переходим в чат
|
||||
setTimeout(() => {
|
||||
this.goToChat();
|
||||
}, 2000);
|
||||
|
||||
} else {
|
||||
this.error = result.error || 'Ошибка подключения кошелька';
|
||||
this.connecting = false;
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Ошибка подключения кошелька:', error);
|
||||
|
||||
if (error.code === 4001) {
|
||||
this.error = 'Вы отклонили запрос подписи';
|
||||
} else {
|
||||
this.error = 'Ошибка подключения кошелька. Попробуйте снова.';
|
||||
}
|
||||
|
||||
this.connecting = false;
|
||||
}
|
||||
},
|
||||
|
||||
goToChat() {
|
||||
this.$router.push('/chat');
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.connect-wallet-container {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.connect-wallet-card {
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||||
padding: 40px;
|
||||
max-width: 500px;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: 64px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
color: #2c3e50;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.info-block {
|
||||
margin: 24px 0;
|
||||
}
|
||||
|
||||
.provider-info {
|
||||
font-size: 16px;
|
||||
color: #666;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.provider-info strong {
|
||||
color: #667eea;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: 14px;
|
||||
color: #888;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.connect-button,
|
||||
.go-chat-button {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 14px 32px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
width: 100%;
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.connect-button:hover:not(:disabled),
|
||||
.go-chat-button:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 20px rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
|
||||
.connect-button:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
background: #fee;
|
||||
color: #c33;
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
margin-top: 16px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.expires-info {
|
||||
margin-top: 20px;
|
||||
font-size: 13px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.loading-state {
|
||||
padding: 40px 20px;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
border: 4px solid #f3f3f3;
|
||||
border-top: 4px solid #667eea;
|
||||
border-radius: 50%;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
animation: spin 1s linear infinite;
|
||||
margin: 0 auto 20px;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.loading-state p {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.expired-state,
|
||||
.success-state {
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.hint {
|
||||
background: #f5f5f5;
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
margin-top: 20px;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.hint code {
|
||||
background: #e0e0e0;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-family: 'Courier New', monospace;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.stats {
|
||||
background: #f0f9ff;
|
||||
color: #0369a1;
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
margin-top: 16px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -273,12 +273,22 @@ async function loadSettings() {
|
||||
}
|
||||
}
|
||||
async function loadTelegramBots() {
|
||||
const { data } = await axios.get('/settings/telegram-settings/list');
|
||||
telegramBots.value = data.items || [];
|
||||
try {
|
||||
const { data } = await axios.get('/settings/telegram-settings/list');
|
||||
telegramBots.value = data.items || [];
|
||||
} catch (error) {
|
||||
console.error('[AiAssistantSettings] Ошибка загрузки telegram bots:', error);
|
||||
telegramBots.value = [];
|
||||
}
|
||||
}
|
||||
async function loadEmailList() {
|
||||
const { data } = await axios.get('/settings/email-settings/list');
|
||||
emailList.value = data.items || [];
|
||||
try {
|
||||
const { data } = await axios.get('/settings/email-settings/list');
|
||||
emailList.value = data.items || [];
|
||||
} catch (error) {
|
||||
console.error('[AiAssistantSettings] Ошибка загрузки email list:', error);
|
||||
emailList.value = [];
|
||||
}
|
||||
}
|
||||
async function loadLLMModels() {
|
||||
const { data } = await axios.get('/settings/llm-models');
|
||||
@@ -306,15 +316,15 @@ async function savePlaceholderEdit() {
|
||||
await loadPlaceholders();
|
||||
closeEditPlaceholder();
|
||||
}
|
||||
onMounted(() => {
|
||||
loadSettings();
|
||||
loadUserTables();
|
||||
loadRules();
|
||||
loadTelegramBots();
|
||||
loadEmailList();
|
||||
loadLLMModels();
|
||||
loadEmbeddingModels();
|
||||
loadPlaceholders();
|
||||
onMounted(async () => {
|
||||
await loadSettings();
|
||||
await loadUserTables();
|
||||
await loadRules();
|
||||
await loadTelegramBots();
|
||||
await loadEmailList();
|
||||
await loadLLMModels();
|
||||
await loadEmbeddingModels();
|
||||
await loadPlaceholders();
|
||||
// Подписка на глобальное событие обновления плейсхолдеров
|
||||
window.addEventListener('placeholders-updated', loadPlaceholders);
|
||||
});
|
||||
|
||||
@@ -76,8 +76,9 @@
|
||||
<script setup>
|
||||
import BaseLayout from '@/components/BaseLayout.vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { reactive, ref, onMounted } from 'vue';
|
||||
import { reactive, ref, onMounted, watch } from 'vue';
|
||||
import api from '@/api/axios';
|
||||
import { useAuthContext } from '@/composables/useAuth';
|
||||
|
||||
const router = useRouter();
|
||||
const goBack = () => router.push('/settings/ai');
|
||||
@@ -96,7 +97,15 @@ const form = reactive({
|
||||
const original = reactive({});
|
||||
const editMode = ref(false);
|
||||
|
||||
const auth = useAuthContext();
|
||||
|
||||
const loadEmailSettings = async () => {
|
||||
// Не загружаем если не авторизован
|
||||
if (!auth.isAuthenticated.value) {
|
||||
console.log('[EmailSettings] Пропуск загрузки - пользователь не авторизован');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await api.get('/settings/email-settings');
|
||||
if (res.data.success) {
|
||||
@@ -113,12 +122,18 @@ const loadEmailSettings = async () => {
|
||||
Object.assign(original, JSON.parse(JSON.stringify(form)));
|
||||
}
|
||||
} catch (e) {
|
||||
// обработка ошибки
|
||||
console.error('[EmailSettings] Ошибка загрузки:', e);
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
await loadEmailSettings();
|
||||
// Отслеживаем изменение авторизации
|
||||
watch(() => auth.isAuthenticated.value, async (isAuth) => {
|
||||
if (isAuth) {
|
||||
await loadEmailSettings();
|
||||
}
|
||||
}, { immediate: true }); // immediate: true - вызовется сразу при монтировании
|
||||
|
||||
onMounted(() => {
|
||||
editMode.value = false;
|
||||
});
|
||||
|
||||
|
||||
@@ -42,8 +42,9 @@
|
||||
<script setup>
|
||||
import BaseLayout from '@/components/BaseLayout.vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { reactive, ref, onMounted } from 'vue';
|
||||
import { reactive, ref, onMounted, watch } from 'vue';
|
||||
import api from '@/api/axios';
|
||||
import { useAuthContext } from '@/composables/useAuth';
|
||||
|
||||
const router = useRouter();
|
||||
const goBack = () => router.push('/settings/ai');
|
||||
@@ -55,7 +56,15 @@ const form = reactive({
|
||||
const original = reactive({});
|
||||
const editMode = ref(false);
|
||||
|
||||
const auth = useAuthContext();
|
||||
|
||||
const loadTelegramSettings = async () => {
|
||||
// Не загружаем если не авторизован
|
||||
if (!auth.isAuthenticated.value) {
|
||||
console.log('[TelegramSettings] Пропуск загрузки - пользователь не авторизован');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await api.get('/settings/telegram-settings');
|
||||
if (res.data.success) {
|
||||
@@ -65,12 +74,18 @@ const loadTelegramSettings = async () => {
|
||||
Object.assign(original, JSON.parse(JSON.stringify(form)));
|
||||
}
|
||||
} catch (e) {
|
||||
// обработка ошибки
|
||||
console.error('[TelegramSettings] Ошибка загрузки:', e);
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
await loadTelegramSettings();
|
||||
// Отслеживаем изменение авторизации
|
||||
watch(() => auth.isAuthenticated.value, async (isAuth) => {
|
||||
if (isAuth) {
|
||||
await loadTelegramSettings();
|
||||
}
|
||||
}, { immediate: true }); // immediate: true - вызовется сразу при монтировании
|
||||
|
||||
onMounted(() => {
|
||||
editMode.value = false;
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user