From 4500f48d4ca0ad311a678f6784197fd1d04070b3 Mon Sep 17 00:00:00 2001 From: Flik Date: Thu, 22 Jan 2026 16:16:17 +0800 Subject: [PATCH] =?UTF-8?q?refactor(ClientView):=20=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E5=AE=A2=E6=88=B7=E7=AB=AF=E8=A7=86=E5=9B=BE=E7=95=8C=E9=9D=A2?= =?UTF-8?q?=E6=A0=B7=E5=BC=8F=E5=92=8C=E7=BB=84=E4=BB=B6=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 替换 naive-ui 组件为自定义玻璃态设计组件 - 添加粒子动画背景效果 - 优化页面布局结构和响应式设计 - 移除未使用的 NCard、NTable、NStatistic 等组件导入 - 重构规则表格和插件列表的展示方式 - 更新模态框标题和简化配置表单 - 调整头部导航和底部栏样式 - 优化卡片组件的视觉效果和交互反馈 --- web/src/App.vue | 10 +- web/src/views/ClientView.vue | 941 +++++++++++++++++++++++---------- web/src/views/LoginView.vue | 296 +++++++++-- web/src/views/PluginsView.vue | 719 ++++++++++++++----------- web/src/views/SettingsView.vue | 357 +++++++++---- 5 files changed, 1595 insertions(+), 728 deletions(-) diff --git a/web/src/App.vue b/web/src/App.vue index 7d170b3..8720347 100644 --- a/web/src/App.vue +++ b/web/src/App.vue @@ -175,11 +175,13 @@ const themeOverrides: GlobalThemeOverrides = { .header { height: 60px; - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + background: rgba(30, 27, 75, 0.95); + backdrop-filter: blur(20px); + -webkit-backdrop-filter: blur(20px); display: flex; align-items: center; padding: 0 24px; - box-shadow: 0 2px 8px rgba(102, 126, 234, 0.15); + border-bottom: 1px solid rgba(255, 255, 255, 0.1); } .header-content { @@ -226,13 +228,13 @@ const themeOverrides: GlobalThemeOverrides = { .main-content { flex: 1; padding: 0; - background-color: transparent; + background: linear-gradient(135deg, #1e1b4b 0%, #312e81 30%, #4c1d95 60%, #581c87 100%); overflow-y: auto; } .footer { height: 48px; - background: rgba(255, 255, 255, 0.05); + background: rgba(30, 27, 75, 0.9); backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px); border-top: 1px solid rgba(255, 255, 255, 0.1); diff --git a/web/src/views/ClientView.vue b/web/src/views/ClientView.vue index 027c872..f9276b4 100644 --- a/web/src/views/ClientView.vue +++ b/web/src/views/ClientView.vue @@ -2,15 +2,15 @@ import { ref, onMounted } from 'vue' import { useRoute, useRouter } from 'vue-router' import { - NCard, NButton, NSpace, NTag, NTable, NEmpty, + NButton, NSpace, NTag, NEmpty, NForm, NFormItem, NInput, NInputNumber, NSelect, NModal, NSwitch, NIcon, useMessage, useDialog, NSpin, NGrid, NGridItem, - NStatistic, NDivider, NTooltip, NDropdown, type FormInst, type FormRules + NTooltip, NDropdown, type FormInst, type FormRules } from 'naive-ui' import { ArrowBackOutline, CreateOutline, TrashOutline, PushOutline, AddOutline, StorefrontOutline, DocumentTextOutline, - ExtensionPuzzleOutline, SettingsOutline, OpenOutline, CloudDownloadOutline, RefreshOutline + ExtensionPuzzleOutline, SettingsOutline, CloudDownloadOutline, RefreshOutline } from '@vicons/ionicons5' import { getClient, updateClient, deleteClient, pushConfigToClient, disconnectClient, restartClient, @@ -73,7 +73,7 @@ const defaultRule = { name: '', local_ip: '127.0.0.1', local_port: 80, - remote_port: 0, // 0 means unset/placeholder + remote_port: 0, type: 'tcp', enabled: true, plugin_config: {} as Record @@ -219,14 +219,13 @@ const saveRename = async () => { // Rule Management const openCreateRule = () => { ruleModalType.value = 'create' - ruleForm.value = { ...defaultRule, remote_port: 8080 } // Reset + ruleForm.value = { ...defaultRule, remote_port: 8080 } showRuleModal.value = true } const openEditRule = (rule: ProxyRule) => { if (rule.plugin_managed) return ruleModalType.value = 'edit' - // Deep copy to avoid modifying original until saved ruleForm.value = JSON.parse(JSON.stringify(rule)) showRuleModal.value = true } @@ -259,7 +258,7 @@ const saveRules = async (newRules: ProxyRule[]) => { } } catch (e: any) { message.error('保存失败: ' + (e.response?.data || e.message)) - await loadClient() // Revert on failure + await loadClient() } } @@ -267,10 +266,8 @@ const handleRuleSubmit = (e: MouseEvent) => { e.preventDefault() ruleFormRef.value?.validate(async (errors) => { if (!errors) { - // Logic to merge rule let newRules = [...rules.value] if (ruleModalType.value === 'create') { - // Check duplicate name if (newRules.some(r => r.name === ruleForm.value.name)) { message.error('规则名称已存在') return @@ -387,7 +384,6 @@ const openConfigModal = async (plugin: ClientPlugin) => { const { data } = await getClientPluginConfig(clientId, plugin.name) configSchema.value = data.schema || [] configValues.value = { ...data.config } - // Fill defaults configSchema.value.forEach(f => { if (f.default && !configValues.value[f.key]) { configValues.value[f.key] = f.default @@ -477,224 +473,223 @@ const handleDeletePlugin = (plugin: ClientPlugin) => { + +.status-tag { + padding: 4px 12px; + border-radius: 20px; + font-size: 12px; + font-weight: 500; + background: rgba(239, 68, 68, 0.2); + color: #fca5a5; +} + +.status-tag.online { + background: rgba(52, 211, 153, 0.2); + color: #34d399; +} + +.header-actions { + display: flex; + gap: 8px; +} + +/* Glass Button */ +.glass-btn { + background: rgba(255, 255, 255, 0.1); + border: 1px solid rgba(255, 255, 255, 0.15); + border-radius: 8px; + padding: 8px 16px; + color: white; + font-size: 13px; + cursor: pointer; + transition: all 0.2s; + display: flex; + align-items: center; + gap: 6px; +} + +.glass-btn:hover:not(:disabled) { + background: rgba(255, 255, 255, 0.2); +} + +.glass-btn:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.glass-btn.primary { + background: linear-gradient(135deg, #60a5fa 0%, #a78bfa 100%); + border: none; +} + +.glass-btn.danger { + background: rgba(239, 68, 68, 0.2); + border-color: rgba(239, 68, 68, 0.3); + color: #fca5a5; +} + +.glass-btn.warning { + background: rgba(251, 191, 36, 0.2); + border-color: rgba(251, 191, 36, 0.3); + color: #fcd34d; +} + +.glass-btn.small { padding: 6px 12px; font-size: 12px; } +.glass-btn.tiny { padding: 4px 8px; font-size: 11px; } +.glass-btn.full { width: 100%; justify-content: center; } + +/* Main Grid */ +.main-grid { + display: grid; + grid-template-columns: 300px 1fr; + gap: 24px; +} + +@media (max-width: 900px) { + .main-grid { grid-template-columns: 1fr; } +} + +.left-column, .right-column { + display: flex; + flex-direction: column; + gap: 20px; +} + +/* Glass Card */ +.glass-card { + background: rgba(255, 255, 255, 0.08); + backdrop-filter: blur(20px); + -webkit-backdrop-filter: blur(20px); + border-radius: 16px; + border: 1px solid rgba(255, 255, 255, 0.12); + overflow: hidden; +} + +.card-header { + padding: 16px 20px; + border-bottom: 1px solid rgba(255, 255, 255, 0.08); + display: flex; + justify-content: space-between; + align-items: center; +} + +.card-header h3 { + margin: 0; + font-size: 15px; + font-weight: 600; + color: white; +} + +.card-body { padding: 20px; } +.card-actions { + padding: 16px 20px; + border-top: 1px solid rgba(255, 255, 255, 0.08); + display: flex; + gap: 8px; +} + +/* Stat Items */ +.stat-item { + display: flex; + justify-content: space-between; + padding: 8px 0; + border-bottom: 1px solid rgba(255, 255, 255, 0.05); +} + +.stat-item:last-child { border-bottom: none; } + +.stat-label { + color: rgba(255, 255, 255, 0.6); + font-size: 13px; +} + +.stat-value { + color: white; + font-size: 13px; +} + +.stat-value.mono { + font-family: monospace; + font-size: 12px; +} + +/* Mini Stats */ +.stats-row { + display: flex; + justify-content: space-around; +} + +.mini-stat { + text-align: center; +} + +.mini-stat-value { + display: block; + font-size: 28px; + font-weight: 700; + color: white; +} + +.mini-stat-label { + font-size: 12px; + color: rgba(255, 255, 255, 0.5); +} + +/* Update Card */ +.platform-info { + font-size: 12px; + color: rgba(255, 255, 255, 0.5); + margin-bottom: 8px; +} + +.empty-hint { + color: rgba(255, 255, 255, 0.4); + font-size: 13px; + text-align: center; + padding: 16px 0; +} + +.update-available p { + margin: 0 0 8px 0; + color: #34d399; + font-size: 13px; +} + +/* Rules Table */ +.empty-state { + text-align: center; + padding: 32px; + color: rgba(255, 255, 255, 0.4); +} + +.rules-table { + overflow-x: auto; +} + +.table-header, .table-row { + display: grid; + grid-template-columns: 1fr 80px 1.5fr 60px 100px; + gap: 12px; + padding: 10px 0; + align-items: center; +} + +.table-header { + border-bottom: 1px solid rgba(255, 255, 255, 0.1); + color: rgba(255, 255, 255, 0.5); + font-size: 12px; + font-weight: 500; +} + +.table-row { + border-bottom: 1px solid rgba(255, 255, 255, 0.05); + color: rgba(255, 255, 255, 0.8); + font-size: 13px; +} + +.rule-name { font-weight: 500; color: white; } +.rule-mapping { font-family: monospace; font-size: 12px; } +.rule-actions { display: flex; gap: 6px; justify-content: flex-end; } + +/* Icon Button */ +.icon-btn { + background: rgba(255, 255, 255, 0.1); + border: none; + border-radius: 6px; + padding: 4px 10px; + color: rgba(255, 255, 255, 0.8); + font-size: 12px; + cursor: pointer; + transition: all 0.2s; +} + +.icon-btn:hover:not(:disabled) { + background: rgba(255, 255, 255, 0.2); + color: white; +} + +.icon-btn:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.icon-btn.danger { + color: #fca5a5; +} + +.icon-btn.danger:hover:not(:disabled) { + background: rgba(239, 68, 68, 0.2); +} + +.icon-btn.success { + color: #34d399; +} + +.icon-btn.success:hover:not(:disabled) { + background: rgba(52, 211, 153, 0.2); +} + +/* Plugins List */ +.plugins-list { + display: flex; + flex-direction: column; + gap: 12px; +} + +.plugin-item { + background: rgba(255, 255, 255, 0.05); + border-radius: 12px; + padding: 16px; + display: flex; + flex-direction: column; + gap: 12px; +} + +.plugin-info { + display: flex; + align-items: center; + gap: 10px; +} + +.plugin-name { + font-weight: 600; + color: white; + font-size: 14px; +} + +.plugin-version { + font-size: 12px; + color: rgba(255, 255, 255, 0.5); +} + +.plugin-meta { + display: flex; + align-items: center; + gap: 12px; + font-size: 12px; + color: rgba(255, 255, 255, 0.6); +} + +.plugin-actions { + display: flex; + gap: 8px; + margin-top: 4px; +} + +/* Store Plugin Card */ +.store-plugin-card { + background: rgba(255, 255, 255, 0.05); + border-radius: 12px; + padding: 16px; + border: 1px solid rgba(255, 255, 255, 0.08); +} + +.store-plugin-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 8px; +} + +.store-plugin-name { + font-weight: 600; + color: white; + font-size: 14px; +} + +.store-plugin-desc { + color: rgba(255, 255, 255, 0.6); + font-size: 12px; + margin: 0 0 12px 0; + line-height: 1.5; +} + \ No newline at end of file diff --git a/web/src/views/LoginView.vue b/web/src/views/LoginView.vue index ec198cb..feedd6c 100644 --- a/web/src/views/LoginView.vue +++ b/web/src/views/LoginView.vue @@ -1,7 +1,6 @@ diff --git a/web/src/views/SettingsView.vue b/web/src/views/SettingsView.vue index 96fa38c..06e06bb 100644 --- a/web/src/views/SettingsView.vue +++ b/web/src/views/SettingsView.vue @@ -1,8 +1,7 @@