ваше сообщение коммита
This commit is contained in:
@@ -1,28 +0,0 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
# Поддержка SPA (Single Page Application)
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
# Настройка кеширования для статических файлов
|
||||
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
|
||||
expires 30d;
|
||||
add_header Cache-Control "public, no-transform";
|
||||
}
|
||||
|
||||
# Настройка для API запросов на бэкенд
|
||||
location /api/ {
|
||||
proxy_pass http://backend:8000/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection 'upgrade';
|
||||
proxy_set_header Host $host;
|
||||
proxy_cache_bypass $http_upgrade;
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,8 @@ import HomeView from '../views/HomeView.vue';
|
||||
const SettingsAiView = () => import('../views/settings/AiSettingsView.vue');
|
||||
const SettingsBlockchainView = () => import('../views/settings/BlockchainSettingsView.vue');
|
||||
const SettingsSecurityView = () => import('../views/settings/SecuritySettingsView.vue');
|
||||
const SettingsInterfaceView = () => import('../views/settings/InterfaceSettingsView.vue');
|
||||
const SettingsInterfaceView = () => import('../views/settings/Interface/InterfaceSettingsView.vue');
|
||||
const SettingsInterfaceCloudflareDetailsView = () => import('../views/settings/Interface/CloudflareDetailsView.vue');
|
||||
import axios from 'axios';
|
||||
import { setToStorage } from '../utils/storage.js';
|
||||
|
||||
@@ -52,6 +53,11 @@ const routes = [
|
||||
name: 'settings-interface',
|
||||
component: SettingsInterfaceView,
|
||||
},
|
||||
{
|
||||
path: 'interface/cloudflare-details',
|
||||
name: 'settings-interface-cloudflare-details',
|
||||
component: SettingsInterfaceCloudflareDetailsView,
|
||||
},
|
||||
{
|
||||
path: 'telegram',
|
||||
name: 'settings-telegram',
|
||||
|
||||
307
frontend/src/views/settings/Interface/CloudflareDetailsView.vue
Normal file
307
frontend/src/views/settings/Interface/CloudflareDetailsView.vue
Normal file
@@ -0,0 +1,307 @@
|
||||
<template>
|
||||
<div class="cloudflare-details settings-panel">
|
||||
<button class="close-btn" @click="goBack">×</button>
|
||||
<h2>Настройка Cloudflare и подключение домена</h2>
|
||||
<ol class="instruction-block">
|
||||
<li>Зайдите в свой аккаунт <a href="https://dash.cloudflare.com/" target="_blank">Cloudflare</a> и добавьте ваш домен.</li>
|
||||
<li>Смените NS-записи у регистратора домена на те, что выдаст Cloudflare (см. <a href="https://developers.cloudflare.com/fundamentals/setup/add-site/ns/" target="_blank">инструкцию</a>).</li>
|
||||
<li>Дождитесь, когда домен будет обслуживаться Cloudflare (обычно 5-30 минут).</li>
|
||||
<li>Сгенерируйте <a href="https://dash.cloudflare.com/profile/api-tokens" target="_blank">API Token</a> с правами управления DNS и туннелями.</li>
|
||||
<li>Введите API Token и домен ниже для автоматической настройки туннеля и DNS.</li>
|
||||
<li><b>Один раз выполните в терминале WSL2:</b>
|
||||
<pre style="white-space:pre-line;font-size:0.95em;">curl -fsSL https://pkg.cloudflare.com/cloudflare-main.gpg | sudo tee /usr/share/keyrings/cloudflare-main.gpg >/dev/null
|
||||
echo 'deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/cloudflared noble main' | sudo tee /etc/apt/sources.list.d/cloudflared.list
|
||||
sudo apt update
|
||||
sudo apt install cloudflared</pre>
|
||||
</li>
|
||||
<li>Нажмите кнопку <b>Автоматически настроить и открыть приложение</b> и дождитесь появления ссылки.</li>
|
||||
</ol>
|
||||
<form class="form-block" @submit.prevent="saveToken">
|
||||
<label>Cloudflare API Token:</label>
|
||||
<input v-model="apiToken" type="text" class="form-control" placeholder="Введите API Token" />
|
||||
<button class="btn-primary" type="submit">Сохранить токен</button>
|
||||
</form>
|
||||
<div v-if="accounts.length" class="form-block">
|
||||
<label>Выберите аккаунт Cloudflare:</label>
|
||||
<select v-model="selectedAccountId" class="form-control">
|
||||
<option v-for="acc in accounts" :key="acc.id" :value="acc.id">
|
||||
{{ acc.name }} ({{ acc.id }})
|
||||
</option>
|
||||
</select>
|
||||
<button class="btn-primary" @click="saveAccountId">Сохранить аккаунт</button>
|
||||
<div v-if="accountStatusMsg" class="status-block">{{ accountStatusMsg }}</div>
|
||||
</div>
|
||||
<form class="form-block" @submit.prevent="connectDomain">
|
||||
<label>Домен для туннеля:</label>
|
||||
<input v-model="domain" type="text" class="form-control" placeholder="example.com" />
|
||||
<button class="btn-primary" type="submit">Проверить и подключить домен</button>
|
||||
</form>
|
||||
<div class="status-block">
|
||||
<b>Статус Cloudflared:</b> {{ tunnelStatus }}<br>
|
||||
<b>Статус домена:</b> {{ domainStatusMsg }}<br>
|
||||
<b>Статус туннеля:</b> {{ tunnelStatusMsg }}<br>
|
||||
<span v-if="statusMsg">{{ statusMsg }}</span>
|
||||
</div>
|
||||
<div v-if="appUrl" class="app-link-block">
|
||||
<a :href="appUrl" target="_blank" class="btn-primary open-app-btn">
|
||||
Открыть приложение
|
||||
</a>
|
||||
</div>
|
||||
<div v-if="autoSetupSteps.length" class="auto-setup-steps">
|
||||
<div v-for="step in autoSetupSteps" :key="step.step" :class="['auto-setup-step', step.status]">
|
||||
{{ step.message }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
const router = useRouter();
|
||||
const goBack = () => router.push('/settings/interface');
|
||||
|
||||
const apiToken = ref('');
|
||||
const domain = ref('');
|
||||
const statusMsg = ref('');
|
||||
const tunnelStatus = ref('');
|
||||
const domainStatusMsg = ref('');
|
||||
const tunnelStatusMsg = ref('');
|
||||
const appUrl = ref('');
|
||||
const autoSetupSteps = ref([]);
|
||||
const accounts = ref([]);
|
||||
const selectedAccountId = ref('');
|
||||
const accountStatusMsg = ref('');
|
||||
|
||||
async function loadSettings() {
|
||||
try {
|
||||
console.log('[CloudflareDetails] loadSettings: start');
|
||||
const res = await fetch('/api/cloudflare/settings');
|
||||
const data = await res.json();
|
||||
console.log('[CloudflareDetails] loadSettings: data', data);
|
||||
if (data.success && data.settings) {
|
||||
apiToken.value = data.settings.api_token || '';
|
||||
domain.value = data.settings.domain || '';
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[CloudflareDetails] loadSettings: error', e);
|
||||
}
|
||||
}
|
||||
|
||||
async function saveToken() {
|
||||
try {
|
||||
console.log('[CloudflareDetails] saveToken: start', apiToken.value);
|
||||
const res = await fetch('/api/cloudflare/token', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ token: apiToken.value })
|
||||
});
|
||||
const data = await res.json();
|
||||
console.log('[CloudflareDetails] saveToken: data', data);
|
||||
statusMsg.value = data.message || 'Токен сохранён!';
|
||||
// Получить список аккаунтов
|
||||
const accRes = await fetch('/api/cloudflare/accounts', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ api_token: apiToken.value })
|
||||
});
|
||||
const accData = await accRes.json();
|
||||
if (accData.success && accData.accounts) {
|
||||
accounts.value = accData.accounts;
|
||||
accountStatusMsg.value = 'Выберите аккаунт и сохраните.';
|
||||
} else {
|
||||
accountStatusMsg.value = accData.message || 'Ошибка получения аккаунтов';
|
||||
}
|
||||
getStatus();
|
||||
} catch (e) {
|
||||
console.error('[CloudflareDetails] saveToken: error', e);
|
||||
statusMsg.value = 'Ошибка при сохранении токена';
|
||||
accountStatusMsg.value = 'Ошибка получения аккаунтов';
|
||||
}
|
||||
}
|
||||
|
||||
async function saveAccountId() {
|
||||
try {
|
||||
const res = await fetch('/api/cloudflare/account-id', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ account_id: selectedAccountId.value })
|
||||
});
|
||||
const data = await res.json();
|
||||
accountStatusMsg.value = data.message || 'Account ID сохранён!';
|
||||
getStatus();
|
||||
} catch (e) {
|
||||
accountStatusMsg.value = 'Ошибка при сохранении Account ID';
|
||||
}
|
||||
}
|
||||
|
||||
async function connectDomain() {
|
||||
try {
|
||||
statusMsg.value = 'Выполняется автоматическая настройка...';
|
||||
appUrl.value = '';
|
||||
autoSetupSteps.value = [];
|
||||
const res = await fetch('/api/cloudflare/domain', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ domain: domain.value })
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
statusMsg.value = data.message || 'Готово!';
|
||||
appUrl.value = data.app_url || '';
|
||||
autoSetupSteps.value = data.steps || [];
|
||||
} else {
|
||||
statusMsg.value = data.error || 'Ошибка автоматической настройки';
|
||||
autoSetupSteps.value = data.steps || [];
|
||||
}
|
||||
getStatus();
|
||||
} catch (e) {
|
||||
statusMsg.value = 'Ошибка автоматической настройки: ' + e.message;
|
||||
}
|
||||
}
|
||||
|
||||
async function getStatus() {
|
||||
try {
|
||||
console.log('[CloudflareDetails] getStatus: start');
|
||||
const res = await fetch('/api/cloudflare/status');
|
||||
const data = await res.json();
|
||||
console.log('[CloudflareDetails] getStatus: data', data);
|
||||
tunnelStatus.value = data.status || '';
|
||||
domainStatusMsg.value = data.domainMsg || '';
|
||||
tunnelStatusMsg.value = data.tunnelMsg || '';
|
||||
} catch (e) {
|
||||
console.error('[CloudflareDetails] getStatus: error', e);
|
||||
tunnelStatus.value = 'Ошибка';
|
||||
domainStatusMsg.value = 'Ошибка';
|
||||
tunnelStatusMsg.value = 'Ошибка';
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadSettings();
|
||||
getStatus();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.cloudflare-details.settings-panel {
|
||||
padding: var(--block-padding);
|
||||
background-color: var(--color-light);
|
||||
border-radius: var(--radius-md);
|
||||
margin-top: var(--spacing-lg);
|
||||
animation: fadeIn var(--transition-normal);
|
||||
min-height: 200px;
|
||||
position: relative;
|
||||
}
|
||||
h2 {
|
||||
margin-bottom: var(--spacing-lg);
|
||||
border-bottom: 1px solid var(--color-grey-light);
|
||||
padding-bottom: var(--spacing-md);
|
||||
}
|
||||
.instruction-block {
|
||||
background: #f8f8f8;
|
||||
border-radius: 8px;
|
||||
padding: 1rem 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
.form-block {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
max-width: 400px;
|
||||
}
|
||||
.form-control {
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #ccc;
|
||||
font-size: 1rem;
|
||||
}
|
||||
.btn-primary {
|
||||
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;
|
||||
margin-top: 0.5rem;
|
||||
align-self: flex-start;
|
||||
}
|
||||
.btn-primary:hover {
|
||||
background: var(--color-primary-dark);
|
||||
}
|
||||
.btn-primary.install-btn {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
.status-block {
|
||||
margin: 1.5rem 0 0.5rem 0;
|
||||
font-size: 1.05rem;
|
||||
color: #555;
|
||||
}
|
||||
.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; }
|
||||
}
|
||||
.app-link-block {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
.open-app-btn {
|
||||
display: inline-block;
|
||||
padding: 0.7rem 2rem;
|
||||
font-size: 1.1rem;
|
||||
background: var(--color-success, #2cae4f);
|
||||
color: #fff;
|
||||
border-radius: 8px;
|
||||
text-decoration: none;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
.open-app-btn:hover {
|
||||
background: var(--color-success-dark, #1e7d32);
|
||||
}
|
||||
.auto-setup-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;
|
||||
display: block;
|
||||
}
|
||||
.auto-setup-steps {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
.auto-setup-step {
|
||||
margin-bottom: 0.5rem;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 6px;
|
||||
background: #f8f8f8;
|
||||
font-size: 1rem;
|
||||
}
|
||||
.auto-setup-step.ok {
|
||||
color: #2cae4f;
|
||||
}
|
||||
.auto-setup-step.error {
|
||||
color: #c62828;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,71 @@
|
||||
<template>
|
||||
<div class="interface-settings settings-panel" style="position:relative;min-height:120px">
|
||||
<button class="close-btn" @click="goBack">×</button>
|
||||
<h2>Подключить домен</h2>
|
||||
<div class="domain-connect-block">
|
||||
<p>Для подключения домена и настройки Cloudflare нажмите кнопку ниже.</p>
|
||||
<button class="btn-primary" @click="goToCloudflareDetails">Подробнее</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useRouter } from 'vue-router';
|
||||
const router = useRouter();
|
||||
const goBack = () => router.push('/settings');
|
||||
const goToCloudflareDetails = () => router.push('/settings/interface/cloudflare-details');
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.settings-panel {
|
||||
padding: var(--block-padding);
|
||||
background-color: var(--color-light);
|
||||
border-radius: var(--radius-md);
|
||||
margin-top: var(--spacing-lg);
|
||||
animation: fadeIn var(--transition-normal);
|
||||
}
|
||||
h2 {
|
||||
margin-bottom: var(--spacing-lg);
|
||||
border-bottom: 1px solid var(--color-grey-light);
|
||||
padding-bottom: var(--spacing-md);
|
||||
}
|
||||
.domain-connect-block {
|
||||
margin-top: 2rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.btn-primary {
|
||||
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;
|
||||
}
|
||||
.btn-primary:hover {
|
||||
background: var(--color-primary-dark);
|
||||
}
|
||||
.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; }
|
||||
}
|
||||
</style>
|
||||
@@ -1,114 +0,0 @@
|
||||
<template>
|
||||
<div class="interface-settings settings-panel" style="position:relative;min-height:120px">
|
||||
<button class="close-btn" @click="goBack">×</button>
|
||||
<h2>Настройки Интерфейса</h2>
|
||||
|
||||
<!-- Панель Язык -->
|
||||
<div class="sub-settings-panel">
|
||||
<h3>Настройки языка</h3>
|
||||
<div class="setting-form">
|
||||
<p>Выбор языка интерфейса</p>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Язык интерфейса:</label>
|
||||
<select v-model="selectedLanguage" class="form-control">
|
||||
<option value="ru">Русский</option>
|
||||
<option value="en">English</option>
|
||||
<!-- Добавить другие языки по необходимости -->
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DomainConnectBlock />
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
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 = () => {
|
||||
setToStorage('userLanguage', selectedLanguage.value);
|
||||
console.log(`[InterfaceSettingsView] Язык сохранен как: ${selectedLanguage.value}`);
|
||||
// TODO: Добавить реальную смену языка (i18n)
|
||||
// TODO: Возможно, отправить на сервер, если язык влияет на бэкенд
|
||||
// alert('Язык сохранен!'); // Пример уведомления
|
||||
};
|
||||
|
||||
// Можно убрать watch, если сохранение происходит только по кнопке
|
||||
// watch(selectedLanguage, (newLang) => {
|
||||
// setToStorage('userLanguage', newLang);
|
||||
// console.log(`[InterfaceSettingsView] Язык изменен на: ${newLang}`);
|
||||
// });
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.settings-panel {
|
||||
padding: var(--block-padding);
|
||||
background-color: var(--color-light);
|
||||
border-radius: var(--radius-md);
|
||||
margin-top: var(--spacing-lg);
|
||||
animation: fadeIn var(--transition-normal);
|
||||
}
|
||||
h2 {
|
||||
margin-bottom: var(--spacing-lg);
|
||||
border-bottom: 1px solid var(--color-grey-light);
|
||||
padding-bottom: var(--spacing-md);
|
||||
}
|
||||
h3 {
|
||||
margin-bottom: var(--spacing-md);
|
||||
color: var(--color-primary);
|
||||
}
|
||||
.sub-settings-panel {
|
||||
margin-bottom: var(--spacing-lg);
|
||||
padding-bottom: var(--spacing-lg);
|
||||
/* Убираем нижнюю границу, если это последняя панель */
|
||||
}
|
||||
.setting-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
.form-group {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.form-label {
|
||||
display: block; /* Можно оставить блочным */
|
||||
margin-bottom: var(--spacing-xs);
|
||||
}
|
||||
.form-control {
|
||||
max-width: 300px; /* Ограничим ширину select */
|
||||
}
|
||||
.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; }
|
||||
}
|
||||
</style>
|
||||
@@ -39,6 +39,7 @@ export default defineConfig({
|
||||
server: {
|
||||
port: 5173,
|
||||
host: '0.0.0.0',
|
||||
allowedHosts: ['dapp-frontend', 'localhost', '127.0.0.1', 'hb3-accelerator.com', 'dapp.hb3-accelerator.com'],
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://dapp-backend:8000',
|
||||
|
||||
Reference in New Issue
Block a user