Улучшение фронта
This commit is contained in:
70
front_vue/src/Core/composables/useConfirm.js
Normal file
70
front_vue/src/Core/composables/useConfirm.js
Normal file
@@ -0,0 +1,70 @@
|
||||
const isOpen = ref(false)
|
||||
const config = ref({
|
||||
icon: 'fas fa-question-circle',
|
||||
iconColor: 'var(--accent-red)',
|
||||
title: '',
|
||||
message: '',
|
||||
buttons: [],
|
||||
})
|
||||
let resolvePromise = null
|
||||
|
||||
export function useConfirm() {
|
||||
const open = (options = {}) => {
|
||||
config.value = {
|
||||
icon: options.icon || 'fas fa-exclamation-triangle',
|
||||
iconColor: options.iconColor || 'var(--accent-red)',
|
||||
title: options.title || '',
|
||||
message: options.message || '',
|
||||
buttons: options.buttons || [],
|
||||
}
|
||||
isOpen.value = true
|
||||
}
|
||||
|
||||
const close = () => {
|
||||
isOpen.value = false
|
||||
if (resolvePromise) {
|
||||
resolvePromise(false)
|
||||
resolvePromise = null
|
||||
}
|
||||
}
|
||||
|
||||
const confirm = (options = {}) => {
|
||||
return new Promise((resolve) => {
|
||||
resolvePromise = resolve
|
||||
open({
|
||||
...options,
|
||||
buttons: (options.buttons || []).map(btn => ({
|
||||
...btn,
|
||||
action: () => {
|
||||
resolvePromise = null
|
||||
isOpen.value = false
|
||||
resolve(btn.value !== undefined ? btn.value : btn.label)
|
||||
if (btn.action) btn.action()
|
||||
},
|
||||
})),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const confirmDelete = (options = {}) => {
|
||||
return confirm({
|
||||
icon: options.icon || 'fas fa-trash-alt',
|
||||
iconColor: options.iconColor || 'var(--accent-red)',
|
||||
title: options.title || '',
|
||||
message: options.message || '',
|
||||
buttons: [
|
||||
{ label: options.cancelText || 'Отмена', variant: 'default', value: false },
|
||||
{ label: options.confirmText || 'Удалить', variant: 'danger', icon: 'fas fa-trash', value: true },
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
isOpen: readonly(isOpen),
|
||||
config: readonly(config),
|
||||
open,
|
||||
close,
|
||||
confirm,
|
||||
confirmDelete,
|
||||
}
|
||||
}
|
||||
@@ -43,3 +43,5 @@ export const STORAGE_KEYS = {
|
||||
}
|
||||
|
||||
export const AUTO_REFRESH_INTERVAL = 5000
|
||||
|
||||
export const APP_VERSION = __APP_VERSION__
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"app": {
|
||||
"title": "vServer Admin Panel",
|
||||
"logo": "vServer",
|
||||
"logo": "vServer v1.0",
|
||||
"footerAuthor": "Author: Sumaneev Roman",
|
||||
"loading": "Starting vServer..."
|
||||
},
|
||||
@@ -21,7 +21,9 @@
|
||||
"stop": "Stop",
|
||||
"starting": "Starting...",
|
||||
"stopping": "Stopping...",
|
||||
"wait": "Please wait..."
|
||||
"wait": "Please wait...",
|
||||
"stopConfirmTitle": "Stop server?",
|
||||
"stopConfirmMessage": "All services will be stopped"
|
||||
},
|
||||
"services": {
|
||||
"title": "Services Status",
|
||||
|
||||
@@ -21,7 +21,9 @@
|
||||
"stop": "Остановить",
|
||||
"starting": "Запускается...",
|
||||
"stopping": "Выключается...",
|
||||
"wait": "Ожидайте..."
|
||||
"wait": "Ожидайте...",
|
||||
"stopConfirmTitle": "Остановить сервер?",
|
||||
"stopConfirmMessage": "Все сервисы будут остановлены"
|
||||
},
|
||||
"services": {
|
||||
"title": "Статус сервисов",
|
||||
@@ -176,7 +178,6 @@
|
||||
"errorPrefix": "Ошибка"
|
||||
},
|
||||
"notify": {
|
||||
"settingsSaved": "Настройки сохранены и сервисы перезапущены!",
|
||||
"settingsSaved": "Настройки сохранены",
|
||||
"dataSaved": "Данные сохранены",
|
||||
"siteCreated": "Сайт успешно создан!",
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { api } from '@core/api/index.js'
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export const useCertsStore = defineStore('certs', {
|
||||
state: () => ({
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { api } from '@core/api/index.js'
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export const useConfigStore = defineStore('config', {
|
||||
state: () => ({
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { api } from '@core/api/index.js'
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export const useProxiesStore = defineStore('proxies', {
|
||||
state: () => ({
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { api } from '@core/api/index.js'
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export const useServicesStore = defineStore('services', {
|
||||
state: () => ({
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { api } from '@core/api/index.js'
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export const useSitesStore = defineStore('sites', {
|
||||
state: () => ({
|
||||
|
||||
@@ -1,100 +1,91 @@
|
||||
/* ============================================
|
||||
Общие стили таблиц
|
||||
Общие стили таблиц — Modern Clean
|
||||
============================================ */
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: var(--space-lg);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: var(--text-md);
|
||||
font-weight: var(--font-bold);
|
||||
color: var(--text-primary);
|
||||
margin: 0;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1.5px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-lg);
|
||||
}
|
||||
|
||||
.section-title::before {
|
||||
content: '';
|
||||
width: 4px;
|
||||
height: 16px;
|
||||
background: linear-gradient(180deg, var(--accent-purple), var(--accent-purple-light));
|
||||
border-radius: 2px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Table Container */
|
||||
.table-container {
|
||||
background: var(--glass-bg-light);
|
||||
backdrop-filter: var(--backdrop-blur);
|
||||
border: 1px solid var(--glass-border);
|
||||
border-radius: var(--radius);
|
||||
overflow: hidden;
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
/* Table */
|
||||
.data-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.data-table thead {
|
||||
background: var(--table-header-bg);
|
||||
border-collapse: separate;
|
||||
border-spacing: 0 8px;
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.data-table thead tr {
|
||||
border-bottom: 1px solid var(--table-border);
|
||||
border: none;
|
||||
}
|
||||
|
||||
.data-table th {
|
||||
padding: 18px 20px;
|
||||
padding: 8px 20px;
|
||||
text-align: left;
|
||||
font-size: var(--text-sm);
|
||||
font-weight: var(--font-bold);
|
||||
color: var(--text-secondary);
|
||||
font-size: 11px;
|
||||
font-weight: var(--font-semibold);
|
||||
color: var(--text-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1.2px;
|
||||
letter-spacing: 1.5px;
|
||||
border: none;
|
||||
background: none;
|
||||
}
|
||||
|
||||
.data-table th:last-child {
|
||||
width: 160px;
|
||||
text-align: center;
|
||||
.th-icon {
|
||||
opacity: 0.4;
|
||||
font-size: 10px;
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
.th-actions {
|
||||
text-align: right !important;
|
||||
padding-right: 20px !important;
|
||||
}
|
||||
|
||||
/* Rows */
|
||||
.data-table tbody tr {
|
||||
border-bottom: 1px solid var(--table-border);
|
||||
transition: all var(--transition-base);
|
||||
background: var(--glass-bg-light);
|
||||
}
|
||||
|
||||
.data-table tbody tr:hover {
|
||||
background: var(--table-hover-bg);
|
||||
background: rgba(var(--accent-rgb), 0.06);
|
||||
}
|
||||
|
||||
.data-table td {
|
||||
padding: 16px 20px;
|
||||
padding: 14px 20px;
|
||||
font-size: var(--text-base);
|
||||
color: var(--text-primary);
|
||||
border-top: 1px solid var(--glass-border);
|
||||
border-bottom: 1px solid var(--glass-border);
|
||||
}
|
||||
|
||||
.data-table td:first-child {
|
||||
border-left: 1px solid var(--glass-border);
|
||||
border-radius: var(--radius) 0 0 var(--radius);
|
||||
}
|
||||
|
||||
.data-table td:last-child {
|
||||
text-align: center;
|
||||
border-right: 1px solid var(--glass-border);
|
||||
border-radius: 0 var(--radius) var(--radius) 0;
|
||||
display: flex;
|
||||
gap: var(--space-xs);
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.data-table tbody tr:hover td {
|
||||
border-color: rgba(var(--accent-rgb), 0.15);
|
||||
}
|
||||
|
||||
/* Code */
|
||||
.data-table code {
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
|
||||
/* Links */
|
||||
.clickable-link {
|
||||
color: var(--link-color);
|
||||
cursor: pointer;
|
||||
@@ -117,59 +108,82 @@
|
||||
.cert-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
margin-right: 8px;
|
||||
border-radius: var(--radius);
|
||||
margin-right: 6px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.cert-valid {
|
||||
background: rgba(var(--success-rgb), 0.2);
|
||||
color: var(--accent-green);
|
||||
border: 1px solid rgba(var(--success-rgb), 0.4);
|
||||
}
|
||||
|
||||
.cert-expired {
|
||||
background: rgba(var(--danger-rgb), 0.2);
|
||||
color: var(--accent-red);
|
||||
border: 1px solid rgba(var(--danger-rgb), 0.4);
|
||||
}
|
||||
|
||||
.cert-none {
|
||||
background: rgba(var(--muted-rgb), 0.15);
|
||||
color: var(--text-muted);
|
||||
border: 1px solid rgba(var(--muted-rgb), 0.3);
|
||||
opacity: 0.5;
|
||||
opacity: 0.25;
|
||||
}
|
||||
|
||||
/* Icon buttons */
|
||||
.icon-btn {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
padding: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--btn-icon-bg);
|
||||
border: 1px solid var(--btn-icon-border);
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: var(--radius);
|
||||
color: var(--btn-icon-color);
|
||||
font-size: var(--text-md);
|
||||
color: var(--text-muted);
|
||||
font-size: var(--text-sm);
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-base);
|
||||
}
|
||||
|
||||
.icon-btn:hover {
|
||||
background: var(--btn-icon-hover-bg);
|
||||
border-color: var(--btn-icon-hover-border);
|
||||
transform: translateY(-1px);
|
||||
background: rgba(var(--accent-rgb), 0.1);
|
||||
color: var(--accent-purple-light);
|
||||
}
|
||||
|
||||
.icon-btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: wait;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.status-toggle {
|
||||
cursor: pointer;
|
||||
transition: opacity var(--transition-fast);
|
||||
}
|
||||
|
||||
.status-toggle:hover {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
/* Drag and drop */
|
||||
.drag-grip {
|
||||
color: var(--text-muted);
|
||||
opacity: 0.15;
|
||||
cursor: grab;
|
||||
font-size: 11px;
|
||||
margin-right: 8px;
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.data-table tbody tr:hover .drag-grip {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.drag-grip:hover {
|
||||
opacity: 1 !important;
|
||||
color: var(--accent-purple-light);
|
||||
}
|
||||
|
||||
.data-table tbody tr.dragging {
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.data-table tbody tr.drag-over td {
|
||||
border-top-color: var(--accent-purple) !important;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<script setup>
|
||||
import { api } from '@core/api/index.js'
|
||||
<script setup>
|
||||
|
||||
const { t, locale } = useI18n()
|
||||
const appStore = useAppStore()
|
||||
@@ -7,6 +6,7 @@ const servicesStore = useServicesStore()
|
||||
const { isDark, toggleTheme } = useTheme()
|
||||
|
||||
const isWails = typeof window !== 'undefined' && window?.runtime
|
||||
const { confirm } = useConfirm()
|
||||
const operating = ref(false)
|
||||
const statusLabel = ref('')
|
||||
|
||||
@@ -22,6 +22,18 @@ const serverToggle = async () => {
|
||||
if (operating.value) return
|
||||
|
||||
if (appStore.serverRunning) {
|
||||
const result = await confirm({
|
||||
icon: 'fas fa-power-off',
|
||||
iconColor: 'var(--accent-red)',
|
||||
title: t('server.stopConfirmTitle'),
|
||||
message: t('server.stopConfirmMessage'),
|
||||
buttons: [
|
||||
{ label: t('common.cancel'), variant: 'default', value: false },
|
||||
{ label: t('server.stop'), variant: 'danger', icon: 'fas fa-power-off', value: true },
|
||||
],
|
||||
})
|
||||
if (!result) return
|
||||
|
||||
operating.value = true
|
||||
statusLabel.value = t('server.stopping')
|
||||
servicesStore.setPending(t('server.stopping'))
|
||||
@@ -63,6 +75,7 @@ const windowClose = () => { if (isWails) window.runtime.Quit() }
|
||||
<div class="app-logo">
|
||||
<span class="logo-icon">🚀</span>
|
||||
<span class="logo-text">{{ t('app.logo') }}</span>
|
||||
<span class="logo-version">v{{ APP_VERSION }}</span>
|
||||
</div>
|
||||
<div class="server-status">
|
||||
<span class="status-indicator" :class="[operating ? 'status-pending' : (appStore.serverRunning ? 'status-online' : 'status-offline')]"></span>
|
||||
@@ -126,6 +139,12 @@ const windowClose = () => { if (isWails) window.runtime.Quit() }
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.logo-version {
|
||||
font-size: 10px;
|
||||
color: var(--text-muted);
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.server-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -1,9 +1,45 @@
|
||||
<script setup>
|
||||
<script setup>
|
||||
|
||||
const { t } = useI18n()
|
||||
const router = useRouter()
|
||||
const proxiesStore = useProxiesStore()
|
||||
const certsStore = useCertsStore()
|
||||
|
||||
const togglingStatus = ref('')
|
||||
const dragIdx = ref(null)
|
||||
const overIdx = ref(null)
|
||||
|
||||
const onDragStart = (i, e) => { dragIdx.value = i; e.dataTransfer.effectAllowed = 'move' }
|
||||
const onDragOver = (i, e) => { e.preventDefault(); overIdx.value = i }
|
||||
const onDragLeave = () => { overIdx.value = null }
|
||||
const onDragEnd = () => { dragIdx.value = null; overIdx.value = null }
|
||||
|
||||
const onDrop = async (i) => {
|
||||
if (dragIdx.value === null || dragIdx.value === i) { dragIdx.value = null; overIdx.value = null; return }
|
||||
const items = [...proxiesStore.list]
|
||||
const [moved] = items.splice(dragIdx.value, 1)
|
||||
items.splice(i, 0, moved)
|
||||
proxiesStore.list = items
|
||||
dragIdx.value = null
|
||||
overIdx.value = null
|
||||
const config = await api.getConfig()
|
||||
config.Proxy_Service = items.map(p => config.Proxy_Service.find(c => c.ExternalDomain === p.ExternalDomain)).filter(Boolean)
|
||||
await api.saveConfig(JSON.stringify(config))
|
||||
}
|
||||
|
||||
const toggleStatus = async (proxy) => {
|
||||
togglingStatus.value = proxy.ExternalDomain
|
||||
const config = await api.getConfig()
|
||||
const idx = config.Proxy_Service.findIndex(p => p.ExternalDomain === proxy.ExternalDomain)
|
||||
if (idx >= 0) {
|
||||
config.Proxy_Service[idx].Enable = !proxy.Enable
|
||||
await api.saveConfig(JSON.stringify(config))
|
||||
await api.reloadConfig()
|
||||
await proxiesStore.load()
|
||||
}
|
||||
togglingStatus.value = ''
|
||||
}
|
||||
|
||||
const openUrl = (host) => {
|
||||
if (window.runtime?.BrowserOpenURL) {
|
||||
window.runtime.BrowserOpenURL('http://' + host)
|
||||
@@ -41,27 +77,33 @@ const findCertForDomain = (domain) => {
|
||||
|
||||
<template>
|
||||
<section class="section">
|
||||
<div class="section-header">
|
||||
<h2 class="section-title">{{ t('proxies.title') }}</h2>
|
||||
<VButton variant="primary" icon="fas fa-plus" @click="router.push('/proxies/create')">
|
||||
{{ t('proxies.add') }}
|
||||
</VButton>
|
||||
</div>
|
||||
<VSectionHeader :title="t('proxies.title')" addable @add="router.push('/proxies/create')" />
|
||||
<div class="table-container">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ t('sites.name') }}</th>
|
||||
<th>{{ t('proxies.externalDomain') }}</th>
|
||||
<th>{{ t('proxies.localAddress') }}</th>
|
||||
<th>HTTPS</th>
|
||||
<th>{{ t('proxies.status') }}</th>
|
||||
<th>{{ t('proxies.actions') }}</th>
|
||||
<th><i class="fas fa-tag th-icon"></i> {{ t('sites.name') }}</th>
|
||||
<th><i class="fas fa-globe th-icon"></i> {{ t('proxies.externalDomain') }}</th>
|
||||
<th><i class="fas fa-server th-icon"></i> {{ t('proxies.localAddress') }}</th>
|
||||
<th><i class="fas fa-lock th-icon"></i> HTTPS</th>
|
||||
<th><i class="fas fa-circle-check th-icon"></i> {{ t('proxies.status') }}</th>
|
||||
<th class="th-actions">{{ t('proxies.actions') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="proxy in proxiesStore.list" :key="proxy.ExternalDomain">
|
||||
<tr
|
||||
v-for="(proxy, i) in proxiesStore.list"
|
||||
:key="proxy.ExternalDomain"
|
||||
draggable="true"
|
||||
:class="{ 'drag-over': overIdx === i, 'dragging': dragIdx === i }"
|
||||
@dragstart="onDragStart(i, $event)"
|
||||
@dragover="onDragOver(i, $event)"
|
||||
@dragleave="onDragLeave"
|
||||
@drop="onDrop(i)"
|
||||
@dragend="onDragEnd"
|
||||
>
|
||||
<td>
|
||||
<i class="fas fa-grip-vertical drag-grip"></i>
|
||||
<span class="cert-icon" :class="findCertForDomain(proxy.ExternalDomain) ? (findCertForDomain(proxy.ExternalDomain).is_expired ? 'cert-expired' : 'cert-valid') : 'cert-none'">
|
||||
<i class="fas fa-shield-alt"></i>
|
||||
</span>
|
||||
@@ -77,8 +119,12 @@ const findCertForDomain = (domain) => {
|
||||
</VBadge>
|
||||
</td>
|
||||
<td>
|
||||
<VBadge :variant="proxy.Enable ? 'online' : 'offline'">
|
||||
{{ proxy.Enable ? 'active' : 'disabled' }}
|
||||
<VBadge
|
||||
class="status-toggle"
|
||||
:variant="togglingStatus === proxy.ExternalDomain ? 'pending' : (proxy.Enable ? 'online' : 'offline')"
|
||||
@click="toggleStatus(proxy)"
|
||||
>
|
||||
{{ togglingStatus === proxy.ExternalDomain ? '...' : (proxy.Enable ? 'active' : 'disabled') }}
|
||||
</VBadge>
|
||||
</td>
|
||||
<td>
|
||||
|
||||
@@ -12,117 +12,80 @@ const serviceIcons = {
|
||||
PHP: 'fab fa-php',
|
||||
Proxy: 'fas fa-exchange-alt',
|
||||
}
|
||||
|
||||
const serviceInfoLabel = {
|
||||
HTTP: 'services.port',
|
||||
HTTPS: 'services.port',
|
||||
MySQL: 'services.port',
|
||||
PHP: 'services.ports',
|
||||
Proxy: 'services.rules',
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="service-card">
|
||||
<div class="service-header">
|
||||
<h3 class="service-name">
|
||||
<i :class="serviceIcons[service.name] || 'fas fa-server'"></i>
|
||||
{{ service.name }}
|
||||
</h3>
|
||||
<VBadge :variant="service.pending ? 'pending' : (service.status ? 'online' : 'offline')">
|
||||
{{ service.pending || (service.status ? t('common.enabled') : t('common.disabled')) }}
|
||||
</VBadge>
|
||||
</div>
|
||||
<div class="service-info">
|
||||
<div class="info-row">
|
||||
<span class="info-label">{{ t(serviceInfoLabel[service.name] || 'services.port') }}:</span>
|
||||
<span class="info-value">{{ service.port || service.info || '—' }}</span>
|
||||
</div>
|
||||
<div v-if="service.info && service.port" class="info-row">
|
||||
<span class="info-label">{{ t('services.rules') }}:</span>
|
||||
<span class="info-value">{{ service.info }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="service-item" :class="{ 'service-active': service.status, 'service-pending': service.pending }">
|
||||
<span class="service-dot"></span>
|
||||
<i :class="serviceIcons[service.name] || 'fas fa-server'" class="service-icon"></i>
|
||||
<span class="service-label">{{ service.name }}</span>
|
||||
<span v-if="service.pending" class="service-status-text pending">{{ service.pending }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.service-card {
|
||||
background: var(--glass-bg-light);
|
||||
backdrop-filter: var(--backdrop-blur);
|
||||
border: 1px solid var(--glass-border);
|
||||
.service-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 12px 18px;
|
||||
border-radius: var(--radius);
|
||||
padding: var(--space-lg);
|
||||
transition: all var(--transition-bounce);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--glass-border);
|
||||
background: var(--glass-bg-light);
|
||||
transition: all var(--transition-base);
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.service-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 3px;
|
||||
background: var(--service-card-gradient);
|
||||
opacity: 0;
|
||||
transition: opacity var(--transition-slow);
|
||||
}
|
||||
|
||||
.service-card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: var(--card-hover-shadow);
|
||||
.service-item:hover {
|
||||
border-color: var(--card-border-hover);
|
||||
}
|
||||
|
||||
.service-card:hover::before {
|
||||
opacity: 1;
|
||||
.service-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: var(--radius-full);
|
||||
background: var(--accent-red);
|
||||
flex-shrink: 0;
|
||||
transition: all var(--transition-base);
|
||||
}
|
||||
|
||||
.service-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 18px;
|
||||
.service-active .service-dot {
|
||||
background: var(--accent-green);
|
||||
box-shadow: 0 0 8px var(--accent-green);
|
||||
}
|
||||
|
||||
.service-name {
|
||||
.service-pending .service-dot {
|
||||
background: var(--accent-yellow, #f0ad4e);
|
||||
box-shadow: 0 0 8px var(--accent-yellow, #f0ad4e);
|
||||
animation: dot-blink 1s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.service-icon {
|
||||
font-size: var(--text-md);
|
||||
color: var(--text-muted);
|
||||
width: 18px;
|
||||
text-align: center;
|
||||
transition: color var(--transition-base);
|
||||
}
|
||||
|
||||
.service-active .service-icon {
|
||||
color: var(--accent-purple-light);
|
||||
}
|
||||
|
||||
.service-label {
|
||||
font-size: var(--text-md);
|
||||
font-weight: var(--font-semibold);
|
||||
color: var(--text-primary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-sm);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.service-name i {
|
||||
color: var(--accent-purple-light);
|
||||
font-size: var(--text-lg);
|
||||
}
|
||||
|
||||
.service-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.info-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
.service-status-text.pending {
|
||||
margin-left: auto;
|
||||
font-size: var(--text-sm);
|
||||
color: var(--text-secondary);
|
||||
font-weight: var(--font-medium);
|
||||
color: var(--accent-yellow, #f0ad4e);
|
||||
}
|
||||
|
||||
.info-value {
|
||||
font-size: 12px;
|
||||
color: var(--text-primary);
|
||||
font-weight: var(--font-semibold);
|
||||
@keyframes dot-blink {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.3; }
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -5,7 +5,7 @@ const servicesStore = useServicesStore()
|
||||
|
||||
<template>
|
||||
<section class="section">
|
||||
<div class="services-grid">
|
||||
<div class="services-row">
|
||||
<ServiceCard
|
||||
v-for="service in servicesStore.list"
|
||||
:key="service.name"
|
||||
@@ -16,21 +16,14 @@ const servicesStore = useServicesStore()
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.services-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
gap: var(--space-lg);
|
||||
.services-row {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.services-grid { grid-template-columns: repeat(3, 1fr); }
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.services-grid { grid-template-columns: repeat(2, 1fr); }
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.services-grid { grid-template-columns: 1fr; }
|
||||
.services-row > * {
|
||||
flex: 1;
|
||||
min-width: 140px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,12 +1,49 @@
|
||||
<script setup>
|
||||
<script setup>
|
||||
const { t } = useI18n()
|
||||
const router = useRouter()
|
||||
const sitesStore = useSitesStore()
|
||||
const certsStore = useCertsStore()
|
||||
const { success, error } = useNotification()
|
||||
const modal = useModal()
|
||||
const { confirmDelete: showConfirm } = useConfirm()
|
||||
|
||||
|
||||
const dragIdx = ref(null)
|
||||
const overIdx = ref(null)
|
||||
|
||||
const onDragStart = (i, e) => { dragIdx.value = i; e.dataTransfer.effectAllowed = 'move' }
|
||||
const onDragOver = (i, e) => { e.preventDefault(); overIdx.value = i }
|
||||
const onDragLeave = () => { overIdx.value = null }
|
||||
const onDragEnd = () => { dragIdx.value = null; overIdx.value = null }
|
||||
|
||||
const onDrop = async (i) => {
|
||||
if (dragIdx.value === null || dragIdx.value === i) { dragIdx.value = null; overIdx.value = null; return }
|
||||
const items = [...sitesStore.list]
|
||||
const [moved] = items.splice(dragIdx.value, 1)
|
||||
items.splice(i, 0, moved)
|
||||
sitesStore.list = items
|
||||
dragIdx.value = null
|
||||
overIdx.value = null
|
||||
const config = await api.getConfig()
|
||||
config.Site_www = items.map(s => config.Site_www.find(c => c.host === s.host)).filter(Boolean)
|
||||
await api.saveConfig(JSON.stringify(config))
|
||||
}
|
||||
|
||||
const openingFolder = ref('')
|
||||
const togglingStatus = ref('')
|
||||
|
||||
const toggleStatus = async (site) => {
|
||||
togglingStatus.value = site.host
|
||||
const newStatus = site.status === 'active' ? 'inactive' : 'active'
|
||||
const config = await api.getConfig()
|
||||
const idx = config.Site_www.findIndex(s => s.host === site.host)
|
||||
if (idx >= 0) {
|
||||
config.Site_www[idx].status = newStatus
|
||||
await api.saveConfig(JSON.stringify(config))
|
||||
await api.updateSiteCache()
|
||||
await sitesStore.load()
|
||||
}
|
||||
togglingStatus.value = ''
|
||||
}
|
||||
|
||||
const openUrl = (host) => {
|
||||
if (window.runtime?.BrowserOpenURL) {
|
||||
@@ -51,44 +88,48 @@ const findCertForDomain = (domain, aliases = []) => {
|
||||
return null
|
||||
}
|
||||
|
||||
const confirmDelete = (site) => {
|
||||
modal.open({
|
||||
const confirmDelete = async (site) => {
|
||||
const result = await showConfirm({
|
||||
title: t('sites.deleteTitle'),
|
||||
message: t('sites.deleteConfirm', { name: site.name, host: site.host }),
|
||||
warning: t('sites.deleteWarning'),
|
||||
onConfirm: async () => {
|
||||
const result = await sitesStore.remove(site.host)
|
||||
if (result && !String(result).startsWith('Error')) success(t('notify.siteDeleted'))
|
||||
else error(String(result))
|
||||
modal.close()
|
||||
},
|
||||
message: `${site.name} (${site.host})`,
|
||||
})
|
||||
if (result) {
|
||||
const res = await sitesStore.remove(site.host)
|
||||
if (res && !String(res).startsWith('Error')) success(t('notify.siteDeleted'))
|
||||
else error(String(res))
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="section">
|
||||
<div class="section-header">
|
||||
<h2 class="section-title">{{ t('sites.title') }}</h2>
|
||||
<VButton variant="primary" icon="fas fa-plus" @click="router.push('/sites/create')">
|
||||
{{ t('sites.add') }}
|
||||
</VButton>
|
||||
</div>
|
||||
<VSectionHeader :title="t('sites.title')" addable @add="router.push('/sites/create')" />
|
||||
<div class="table-container">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ t('sites.name') }}</th>
|
||||
<th>{{ t('sites.host') }}</th>
|
||||
<th>{{ t('sites.alias') }}</th>
|
||||
<th>{{ t('sites.status') }}</th>
|
||||
<th>{{ t('sites.rootFile') }}</th>
|
||||
<th>{{ t('sites.actions') }}</th>
|
||||
<th><i class="fas fa-tag th-icon"></i> {{ t('sites.name') }}</th>
|
||||
<th><i class="fas fa-globe th-icon"></i> {{ t('sites.host') }}</th>
|
||||
<th><i class="fas fa-link th-icon"></i> {{ t('sites.alias') }}</th>
|
||||
<th><i class="fas fa-circle-check th-icon"></i> {{ t('sites.status') }}</th>
|
||||
<th><i class="fas fa-file th-icon"></i> {{ t('sites.rootFile') }}</th>
|
||||
<th class="th-actions">{{ t('sites.actions') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="site in sitesStore.list" :key="site.host">
|
||||
<tr
|
||||
v-for="(site, i) in sitesStore.list"
|
||||
:key="site.host"
|
||||
draggable="true"
|
||||
:class="{ 'drag-over': overIdx === i, 'dragging': dragIdx === i }"
|
||||
@dragstart="onDragStart(i, $event)"
|
||||
@dragover="onDragOver(i, $event)"
|
||||
@dragleave="onDragLeave"
|
||||
@drop="onDrop(i)"
|
||||
@dragend="onDragEnd"
|
||||
>
|
||||
<td>
|
||||
<i class="fas fa-grip-vertical drag-grip"></i>
|
||||
<span class="cert-icon" :class="findCertForDomain(site.host, site.alias) ? (findCertForDomain(site.host, site.alias).is_expired ? 'cert-expired' : 'cert-valid') : 'cert-none'" :title="findCertForDomain(site.host, site.alias) ? (findCertForDomain(site.host, site.alias).is_expired ? 'SSL истёк' : `SSL (${findCertForDomain(site.host, site.alias).days_left} дн.)`) : 'Нет сертификата'">
|
||||
<i class="fas fa-shield-alt"></i>
|
||||
</span>
|
||||
@@ -99,8 +140,12 @@ const confirmDelete = (site) => {
|
||||
</td>
|
||||
<td><code>{{ site.alias?.join(', ') || '—' }}</code></td>
|
||||
<td>
|
||||
<VBadge :variant="site.status === 'active' ? 'online' : 'offline'">
|
||||
{{ site.status }}
|
||||
<VBadge
|
||||
class="status-toggle"
|
||||
:variant="togglingStatus === site.host ? 'pending' : (site.status === 'active' ? 'online' : 'offline')"
|
||||
@click="toggleStatus(site)"
|
||||
>
|
||||
{{ togglingStatus === site.host ? '...' : site.status }}
|
||||
</VBadge>
|
||||
</td>
|
||||
<td><code>{{ site.root_file }}</code></td>
|
||||
|
||||
142
front_vue/src/Design/components/ui/VConfirmDialog.vue
Normal file
142
front_vue/src/Design/components/ui/VConfirmDialog.vue
Normal file
@@ -0,0 +1,142 @@
|
||||
<script setup>
|
||||
|
||||
const { isOpen, config, close } = useConfirm()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<transition name="fade">
|
||||
<div v-if="isOpen" class="confirm-overlay" @click.self="close()">
|
||||
<transition name="scale">
|
||||
<div v-if="isOpen" class="confirm-dialog">
|
||||
<div class="confirm-icon" :style="{ color: config.iconColor }">
|
||||
<i :class="config.icon"></i>
|
||||
</div>
|
||||
<h3 class="confirm-title">{{ config.title }}</h3>
|
||||
<p v-if="config.message" class="confirm-message">{{ config.message }}</p>
|
||||
<div class="confirm-buttons">
|
||||
<button
|
||||
v-for="(btn, i) in config.buttons"
|
||||
:key="i"
|
||||
class="confirm-btn"
|
||||
:class="`confirm-btn--${btn.variant || 'default'}`"
|
||||
@click="btn.action?.()"
|
||||
>
|
||||
<i v-if="btn.icon" :class="btn.icon"></i>
|
||||
{{ btn.label }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</transition>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.confirm-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: var(--z-modal);
|
||||
backdrop-filter: blur(6px);
|
||||
}
|
||||
|
||||
.confirm-dialog {
|
||||
background: transparent;
|
||||
border: none;
|
||||
padding: 40px;
|
||||
max-width: 420px;
|
||||
width: 90%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.confirm-icon {
|
||||
font-size: 42px;
|
||||
margin-bottom: 16px;
|
||||
filter: drop-shadow(0 0 16px currentColor);
|
||||
}
|
||||
|
||||
.confirm-title {
|
||||
font-size: var(--text-xl);
|
||||
font-weight: var(--font-bold);
|
||||
color: var(--text-primary);
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
|
||||
.confirm-message {
|
||||
font-size: var(--text-md);
|
||||
color: var(--text-muted);
|
||||
margin: 0 0 28px 0;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.confirm-buttons {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.confirm-btn {
|
||||
width: 100%;
|
||||
padding: 12px 20px;
|
||||
border-radius: var(--radius);
|
||||
font-size: var(--text-md);
|
||||
font-weight: var(--font-semibold);
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-base);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.confirm-btn--default {
|
||||
background: rgba(var(--muted-rgb), 0.15);
|
||||
border-color: rgba(var(--muted-rgb), 0.3);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.confirm-btn--default:hover {
|
||||
background: rgba(var(--muted-rgb), 0.25);
|
||||
}
|
||||
|
||||
.confirm-btn--primary {
|
||||
background: rgba(var(--accent-rgb), 0.2);
|
||||
border-color: rgba(var(--accent-rgb), 0.4);
|
||||
color: var(--accent-purple-light);
|
||||
}
|
||||
|
||||
.confirm-btn--primary:hover {
|
||||
background: rgba(var(--accent-rgb), 0.3);
|
||||
}
|
||||
|
||||
.confirm-btn--danger {
|
||||
background: rgba(var(--danger-rgb), 0.2);
|
||||
border-color: rgba(var(--danger-rgb), 0.4);
|
||||
color: var(--accent-red);
|
||||
}
|
||||
|
||||
.confirm-btn--danger:hover {
|
||||
background: rgba(var(--danger-rgb), 0.3);
|
||||
}
|
||||
|
||||
.confirm-btn--success {
|
||||
background: rgba(var(--success-rgb), 0.2);
|
||||
border-color: rgba(var(--success-rgb), 0.4);
|
||||
color: var(--accent-green);
|
||||
}
|
||||
|
||||
.confirm-btn--success:hover {
|
||||
background: rgba(var(--success-rgb), 0.3);
|
||||
}
|
||||
|
||||
.scale-enter-active { transition: all 0.25s cubic-bezier(0.34, 1.56, 0.64, 1); }
|
||||
.scale-leave-active { transition: all 0.15s ease-in; }
|
||||
.scale-enter-from { opacity: 0; transform: scale(0.9); }
|
||||
.scale-leave-to { opacity: 0; transform: scale(0.95); }
|
||||
</style>
|
||||
72
front_vue/src/Design/components/ui/VSectionHeader.vue
Normal file
72
front_vue/src/Design/components/ui/VSectionHeader.vue
Normal file
@@ -0,0 +1,72 @@
|
||||
<script setup>
|
||||
defineProps({
|
||||
title: { type: String, required: true },
|
||||
addable: { type: Boolean, default: false },
|
||||
})
|
||||
|
||||
defineEmits(['add'])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="v-section-header">
|
||||
<span class="v-section-bar"></span>
|
||||
<h2 class="v-section-title">{{ title }}</h2>
|
||||
<button v-if="addable" class="v-section-add" @click="$emit('add')">
|
||||
<svg width="10" height="10" viewBox="0 0 10 10">
|
||||
<line x1="5" y1="0" x2="5" y2="10" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
||||
<line x1="0" y1="5" x2="10" y2="5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.v-section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: var(--space-md);
|
||||
}
|
||||
|
||||
.v-section-bar {
|
||||
width: 3px;
|
||||
height: 14px;
|
||||
background: var(--accent-purple);
|
||||
border-radius: 2px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.v-section-title {
|
||||
font-size: var(--text-sm);
|
||||
font-weight: var(--font-semibold);
|
||||
color: var(--text-muted);
|
||||
margin: 0;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1.5px;
|
||||
}
|
||||
|
||||
.v-section-add {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
background: rgba(var(--accent-rgb), 0.1);
|
||||
border: 1px solid rgba(var(--accent-rgb), 0.2);
|
||||
border-radius: var(--radius-full);
|
||||
color: var(--accent-purple-light);
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-base);
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.v-section-add:hover {
|
||||
background: rgba(var(--accent-rgb), 0.2);
|
||||
border-color: rgba(var(--accent-rgb), 0.4);
|
||||
}
|
||||
|
||||
.v-section-add svg {
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
@@ -47,6 +47,7 @@ onUnmounted(() => {
|
||||
</div>
|
||||
<VNotification />
|
||||
<VModal />
|
||||
<VConfirmDialog />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -56,7 +57,15 @@ onUnmounted(() => {
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
background: var(--bg-primary);
|
||||
|
||||
/* Красивый грандиент */
|
||||
background:
|
||||
/*
|
||||
radial-gradient(circle at 100% 30%, rgba(53, 55, 126, 0.089) 0%, transparent 50%),
|
||||
radial-gradient(circle at 45% 100%, rgba(126, 53, 98, 0.082) 0%, transparent 50%),
|
||||
radial-gradient(circle at 0% 100%, rgba(53, 55, 126, 0.089) 0%, transparent 50%),
|
||||
*/
|
||||
var(--bg-primary);
|
||||
}
|
||||
|
||||
.app-body {
|
||||
@@ -73,8 +82,9 @@ onUnmounted(() => {
|
||||
}
|
||||
|
||||
.main-content {
|
||||
--page-padding: 50px;
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 40px var(--space-3xl);
|
||||
padding: var(--page-padding) var(--page-padding) var(--page-padding) var(--page-padding);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -19,8 +19,10 @@ onMounted(async () => {
|
||||
<template>
|
||||
<div class="dashboard-view">
|
||||
<ServicesGrid />
|
||||
<SitesTable />
|
||||
<ProxiesTable />
|
||||
<div class="dashboard-tables">
|
||||
<SitesTable />
|
||||
<ProxiesTable />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -30,4 +32,22 @@ onMounted(async () => {
|
||||
flex-direction: column;
|
||||
gap: var(--space-xl);
|
||||
}
|
||||
|
||||
.dashboard-tables {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-xl);
|
||||
}
|
||||
|
||||
@media (min-width: 2200px) {
|
||||
.dashboard-tables {
|
||||
flex-direction: row;
|
||||
gap: var(--space-xl);
|
||||
}
|
||||
|
||||
.dashboard-tables > * {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<script setup>
|
||||
<script setup>
|
||||
const { t } = useI18n()
|
||||
const router = useRouter()
|
||||
const proxiesStore = useProxiesStore()
|
||||
const { success, error } = useNotification()
|
||||
const modal = useModal()
|
||||
const { confirmDelete: showConfirm } = useConfirm()
|
||||
|
||||
const props = defineProps({
|
||||
domain: { type: String, required: true },
|
||||
@@ -20,7 +20,6 @@ const form = reactive({
|
||||
autoSSL: false,
|
||||
})
|
||||
|
||||
import { api } from '@core/api/index.js'
|
||||
|
||||
const saving = ref(false)
|
||||
|
||||
@@ -66,21 +65,20 @@ const saveProxy = async () => {
|
||||
saving.value = false
|
||||
}
|
||||
|
||||
const confirmDelete = () => {
|
||||
modal.open({
|
||||
const confirmDelete = async () => {
|
||||
const result = await showConfirm({
|
||||
title: t('proxies.deleteTitle'),
|
||||
message: t('proxies.deleteConfirm', { domain: form.domain }),
|
||||
onConfirm: async () => {
|
||||
const result = await proxiesStore.remove(form.domain)
|
||||
if (result && !String(result).startsWith('Error')) {
|
||||
success(t('notify.proxyDeleted'))
|
||||
router.push('/')
|
||||
} else {
|
||||
error(String(result))
|
||||
}
|
||||
modal.close()
|
||||
},
|
||||
message: form.domain,
|
||||
})
|
||||
if (result) {
|
||||
const res = await proxiesStore.remove(form.domain)
|
||||
if (res && !String(res).startsWith('Error')) {
|
||||
success(t('notify.proxyDeleted'))
|
||||
router.push('/')
|
||||
} else {
|
||||
error(String(res))
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@ const toggleAcme = async () => {
|
||||
<template>
|
||||
<div class="settings-view">
|
||||
<div class="settings-header">
|
||||
<h2 class="section-title">{{ t('settings.title') }}</h2>
|
||||
<VSectionHeader :title="t('settings.title')" />
|
||||
<VButton variant="success" icon="fas fa-save" :loading="saving" @click="saveSettings">
|
||||
{{ saving ? t('settings.saving') : t('settings.save') }}
|
||||
</VButton>
|
||||
@@ -127,20 +127,25 @@ const toggleAcme = async () => {
|
||||
}
|
||||
|
||||
.settings-card {
|
||||
background: var(--glass-bg-light);
|
||||
backdrop-filter: var(--backdrop-blur);
|
||||
background: rgba(var(--accent-rgb), 0.02);
|
||||
border: 1px solid var(--glass-border);
|
||||
border-radius: var(--radius);
|
||||
padding: var(--space-lg);
|
||||
transition: all var(--transition-base);
|
||||
}
|
||||
|
||||
.settings-card:hover {
|
||||
background: rgba(var(--accent-rgb), 0.04);
|
||||
border-color: rgba(var(--accent-rgb), 0.2);
|
||||
}
|
||||
|
||||
.settings-card-title {
|
||||
font-size: var(--text-lg);
|
||||
font-size: var(--text-md);
|
||||
font-weight: var(--font-semibold);
|
||||
color: var(--text-primary);
|
||||
margin: 0 0 20px 0;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid var(--divider-subtle);
|
||||
border-bottom: 1px solid rgba(var(--accent-rgb), 0.1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-sm);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<script setup>
|
||||
<script setup>
|
||||
const { t } = useI18n()
|
||||
const router = useRouter()
|
||||
const sitesStore = useSitesStore()
|
||||
const { success, error } = useNotification()
|
||||
const modal = useModal()
|
||||
const { confirmDelete: showConfirm } = useConfirm()
|
||||
|
||||
const props = defineProps({
|
||||
host: { type: String, required: true },
|
||||
@@ -19,7 +19,6 @@ const form = reactive({
|
||||
autoSSL: false,
|
||||
})
|
||||
|
||||
import { api } from '@core/api/index.js'
|
||||
|
||||
const saving = ref(false)
|
||||
const aliasInput = ref('')
|
||||
@@ -81,22 +80,20 @@ const saveSite = async () => {
|
||||
saving.value = false
|
||||
}
|
||||
|
||||
const confirmDelete = () => {
|
||||
modal.open({
|
||||
const confirmDelete = async () => {
|
||||
const result = await showConfirm({
|
||||
title: t('sites.deleteTitle'),
|
||||
message: t('sites.deleteConfirm', { name: form.name, host: form.host }),
|
||||
warning: t('sites.deleteWarning'),
|
||||
onConfirm: async () => {
|
||||
const result = await sitesStore.remove(form.host)
|
||||
if (result && !String(result).startsWith('Error')) {
|
||||
success(t('notify.siteDeleted'))
|
||||
router.push('/')
|
||||
} else {
|
||||
error(String(result))
|
||||
}
|
||||
modal.close()
|
||||
},
|
||||
message: `${form.name} (${form.host})`,
|
||||
})
|
||||
if (result) {
|
||||
const res = await sitesStore.remove(form.host)
|
||||
if (res && !String(res).startsWith('Error')) {
|
||||
success(t('notify.siteDeleted'))
|
||||
router.push('/')
|
||||
} else {
|
||||
error(String(res))
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
<script setup>
|
||||
import { api } from '@core/api/index.js'
|
||||
import { useDraggable } from '@core/composables/useDraggable.js'
|
||||
<script setup>
|
||||
|
||||
const { t } = useI18n()
|
||||
const route = useRoute()
|
||||
|
||||
@@ -3,6 +3,9 @@ import vue from '@vitejs/plugin-vue'
|
||||
import AutoImport from 'unplugin-auto-import/vite'
|
||||
import Components from 'unplugin-vue-components/vite'
|
||||
import { resolve } from 'path'
|
||||
import { readFileSync } from 'fs'
|
||||
|
||||
const wailsConfig = JSON.parse(readFileSync(resolve(__dirname, '../wails.json'), 'utf-8'))
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
@@ -14,6 +17,13 @@ export default defineConfig({
|
||||
'vue-router',
|
||||
'vue-i18n',
|
||||
'pinia',
|
||||
{
|
||||
'@core/api/index.js': ['api'],
|
||||
'@core/constants.js': [
|
||||
'SERVICE_NAMES', 'SITE_STATUS', 'PROXY_STATUS', 'VACCESS_TYPE',
|
||||
'CERT_MODE', 'THEME', 'LOCALE', 'STORAGE_KEYS', 'AUTO_REFRESH_INTERVAL', 'APP_VERSION',
|
||||
],
|
||||
},
|
||||
],
|
||||
dirs: [
|
||||
'src/Core/composables',
|
||||
@@ -41,6 +51,10 @@ export default defineConfig({
|
||||
port: 4444,
|
||||
},
|
||||
|
||||
define: {
|
||||
__APP_VERSION__: JSON.stringify(wailsConfig.info.productVersion),
|
||||
},
|
||||
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': resolve(__dirname, 'src'),
|
||||
|
||||
Reference in New Issue
Block a user