From cfcfc3839c214890d89ebe2cee8c897cfcb21cbf Mon Sep 17 00:00:00 2001 From: Flik Date: Tue, 20 Jan 2026 22:41:29 +0800 Subject: [PATCH] =?UTF-8?q?refactor(ui):=20=E9=87=8D=E6=9E=84=E5=89=8D?= =?UTF-8?q?=E7=AB=AF=E7=95=8C=E9=9D=A2=E5=B8=83=E5=B1=80=E5=92=8C=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将App.vue改为侧边栏布局,添加折叠菜单功能 - 更新主题样式配置,统一视觉设计 - 重写ClientView.vue界面结构,优化用户体验 - 添加WebSocket类型支持到代理规则 - 实现响应式侧边栏和移动端适配 - 重构规则编辑为模态框形式,提升交互体验 - 整合插件管理和配置功能模块 - 优化客户端状态显示和操作按钮布局 - 统一错误处理和消息提示机制 - 简化代码逻辑,移除冗余功能实现 --- web/src/App.vue | 184 ++++- web/src/views/ClientView.vue | 1220 ++++++++++++++++------------------ 2 files changed, 715 insertions(+), 689 deletions(-) diff --git a/web/src/App.vue b/web/src/App.vue index 5908041..6d64738 100644 --- a/web/src/App.vue +++ b/web/src/App.vue @@ -1,8 +1,15 @@ + + diff --git a/web/src/views/ClientView.vue b/web/src/views/ClientView.vue index 5a8dbd2..b3e846f 100644 --- a/web/src/views/ClientView.vue +++ b/web/src/views/ClientView.vue @@ -3,13 +3,14 @@ import { ref, onMounted } from 'vue' import { useRoute, useRouter } from 'vue-router' import { NCard, NButton, NSpace, NTag, NTable, NEmpty, - NFormItem, NInput, NInputNumber, NSelect, NModal, NSwitch, - NIcon, useMessage, useDialog, NSpin, NAlert + NForm, NFormItem, NInput, NInputNumber, NSelect, NModal, NSwitch, + NIcon, useMessage, useDialog, NSpin, NGrid, NGridItem, + NStatistic, NDivider, NTooltip, NDropdown, type FormInst, type FormRules } from 'naive-ui' import { ArrowBackOutline, CreateOutline, TrashOutline, - PushOutline, PowerOutline, AddOutline, SaveOutline, CloseOutline, - SettingsOutline, StorefrontOutline, RefreshOutline, StopOutline, PlayOutline, DocumentTextOutline + PushOutline, AddOutline, StorefrontOutline, DocumentTextOutline, + ExtensionPuzzleOutline, SettingsOutline, OpenOutline } from '@vicons/ionicons5' import { getClient, updateClient, deleteClient, pushConfigToClient, disconnectClient, restartClient, @@ -25,35 +26,17 @@ const message = useMessage() const dialog = useDialog() const clientId = route.params.id as string +// Data const online = ref(false) const lastPing = ref('') const remoteAddr = ref('') const nickname = ref('') const rules = ref([]) const clientPlugins = ref([]) -const editing = ref(false) -const editRules = ref([]) +const loading = ref(false) -// 重命名相关 -const showRenameModal = ref(false) -const renameValue = ref('') - -// 内置类型 -const builtinTypes = [ - { label: 'TCP', value: 'tcp' }, - { label: 'UDP', value: 'udp' }, - { label: 'HTTP', value: 'http' }, - { label: 'HTTPS', value: 'https' }, - { label: 'SOCKS5', value: 'socks5' } -] - -// 规则类型选项(内置 + 插件) -const typeOptions = ref([...builtinTypes]) - -// 插件 RuleSchema 映射(包含内置类型和插件类型) +// Rule Schemas const pluginRuleSchemas = ref({}) - -// 加载规则配置模式 const loadRuleSchemas = async () => { try { const { data } = await getRuleSchemas() @@ -63,32 +46,194 @@ const loadRuleSchemas = async () => { } } -// 判断类型是否需要本地地址 +// Built-in Types (Added WebSocket) +const builtinTypes = [ + { label: 'TCP', value: 'tcp' }, + { label: 'UDP', value: 'udp' }, + { label: 'HTTP', value: 'http' }, + { label: 'HTTPS', value: 'https' }, + { label: 'SOCKS5', value: 'socks5' }, + { label: 'WebSocket', value: 'websocket' } +] + +// Modal Control for Rules +const showRuleModal = ref(false) +const ruleModalType = ref<'create' | 'edit'>('create') +const ruleFormRef = ref(null) +// Default Rule Model +const defaultRule = { + name: '', + local_ip: '127.0.0.1', + local_port: 80, + remote_port: 0, // 0 means unset/placeholder + type: 'tcp', + enabled: true, + plugin_config: {} as Record +} +const ruleForm = ref({ ...defaultRule }) + +// Helper: Check if type needs local addr const needsLocalAddr = (type: string) => { const schema = pluginRuleSchemas.value[type] - return schema?.needs_local_addr ?? true // 默认需要 + return schema?.needs_local_addr ?? true } -// 获取类型的额外字段 const getExtraFields = (type: string): ConfigField[] => { const schema = pluginRuleSchemas.value[type] return schema?.extra_fields || [] } -// 插件配置相关 -const showConfigModal = ref(false) -const configPluginName = ref('') -const configSchema = ref([]) -const configValues = ref>({}) -const configLoading = ref(false) +// Validation Rules +const ruleValidationRules: FormRules = { + name: { required: true, message: '请输入规则名称', trigger: 'blur' }, + type: { required: true, message: '请选择类型', trigger: ['blur', 'change'] }, + remote_port: [ + { required: true, type: 'number', message: '请输入远程端口', trigger: ['blur', 'change'] }, + { type: 'number', min: 1, max: 65535, message: '端口范围 1-65535', trigger: ['blur', 'change'] } + ], + local_ip: { + required: true, + validator(_rule, value) { + if (needsLocalAddr(ruleForm.value.type || 'tcp')) { + if (!value) return new Error('请输入本地IP') + } + return true + }, + trigger: 'blur' + }, + local_port: { + required: true, + validator(_rule, value) { + if (needsLocalAddr(ruleForm.value.type || 'tcp')) { + if (!value && value !== 0) return new Error('请输入本地端口') + if (typeof value === 'number' && (value < 1 || value > 65535)) return new Error('端口范围 1-65535') + } + return true + }, + trigger: ['blur', 'change'] + } +} -// 商店插件安装相关 +// Actions +const loadClient = async () => { + loading.value = true + try { + const { data } = await getClient(clientId) + online.value = data.online + lastPing.value = data.last_ping || '' + remoteAddr.value = data.remote_addr || '' + nickname.value = data.nickname || '' + rules.value = data.rules || [] + clientPlugins.value = data.plugins || [] + } catch (e) { + message.error('加载客户端信息失败') + console.error(e) + } finally { + loading.value = false + } +} + +// Client Rename +const showRenameModal = ref(false) +const renameValue = ref('') +const openRenameModal = () => { + renameValue.value = nickname.value + showRenameModal.value = true +} +const saveRename = async () => { + try { + await updateClient(clientId, { + id: clientId, + nickname: renameValue.value, + rules: rules.value + }) + nickname.value = renameValue.value + showRenameModal.value = false + message.success('重命名成功') + } catch (e) { + message.error('重命名失败') + } +} + +// Rule Management +const openCreateRule = () => { + ruleModalType.value = 'create' + ruleForm.value = { ...defaultRule, remote_port: 8080 } // Reset + 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 +} + +const handleDeleteRule = (rule: ProxyRule) => { + dialog.warning({ + title: '确认删除', + content: `确定要删除规则 "${rule.name}" 吗?`, + positiveText: '删除', + negativeText: '取消', + onPositiveClick: async () => { + const newRules = rules.value.filter(r => r.name !== rule.name) + await saveRules(newRules) + } + }) +} + +const saveRules = async (newRules: ProxyRule[]) => { + try { + await updateClient(clientId, { + id: clientId, + nickname: nickname.value, + rules: newRules + }) + rules.value = newRules + message.success('规则保存成功') + if (online.value) { + await pushConfigToClient(clientId) + message.success('配置已推送到客户端') + } + } catch (e: any) { + message.error('保存失败: ' + (e.response?.data || e.message)) + await loadClient() // Revert on failure + } +} + +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 + } + newRules.push({ ...ruleForm.value }) + } else { + const index = newRules.findIndex(r => r.name === ruleForm.value.name) + if (index > -1) { + newRules[index] = { ...ruleForm.value } + } + } + await saveRules(newRules) + showRuleModal.value = false + } else { + message.error('请检查表单填写') + } + }) +} + +// Store & Plugin Logic const showStoreModal = ref(false) const storePlugins = ref([]) const storeLoading = ref(false) -const storeInstalling = ref(null) // 正在安装的插件名称 - -// 安装配置模态框 +const storeInstalling = ref(null) const showInstallConfigModal = ref(false) const installPlugin = ref(null) const installRemotePort = ref(8080) @@ -96,41 +241,25 @@ const installAuthEnabled = ref(false) const installAuthUsername = ref('') const installAuthPassword = ref('') -// 日志查看相关 -const showLogViewer = ref(false) - -// 商店插件相关函数 const openStoreModal = async () => { showStoreModal.value = true storeLoading.value = true try { const { data } = await getStorePlugins() - storePlugins.value = (data.plugins || []).filter(p => p.download_url) + storePlugins.value = (data.plugins || []).filter((p: any) => p.download_url) } catch (e) { - console.error('Failed to load store plugins', e) - message.error('加载商店插件失败') + message.error('加载商店失败') } finally { storeLoading.value = false } } - -const handleInstallStorePlugin = async (plugin: StorePluginInfo) => { - if (!plugin.download_url) { - message.error('该插件没有下载地址') - return - } - // 打开配置模态框 +const handleInstallStorePlugin = (plugin: StorePluginInfo) => { installPlugin.value = plugin installRemotePort.value = 8080 - installAuthEnabled.value = false - installAuthUsername.value = '' - installAuthPassword.value = '' showInstallConfigModal.value = true } - const confirmInstallPlugin = async () => { if (!installPlugin.value) return - storeInstalling.value = installPlugin.value.name try { await installStorePlugin( @@ -156,193 +285,12 @@ const confirmInstallPlugin = async () => { } } -const loadClient = async () => { - try { - const { data } = await getClient(clientId) - online.value = data.online - lastPing.value = data.last_ping || '' - remoteAddr.value = data.remote_addr || '' - nickname.value = data.nickname || '' - rules.value = data.rules || [] - clientPlugins.value = data.plugins || [] - } catch (e) { - console.error('Failed to load client', e) - } -} - -onMounted(() => { - loadRuleSchemas() // 加载内置协议配置模式 - loadClient() -}) - -// 打开重命名弹窗 -const openRenameModal = () => { - renameValue.value = nickname.value - showRenameModal.value = true -} - -// 保存重命名 -const saveRename = async () => { - try { - await updateClient(clientId, { - id: clientId, - nickname: renameValue.value, - rules: rules.value - }) - nickname.value = renameValue.value - showRenameModal.value = false - message.success('重命名成功') - } catch (e) { - message.error('重命名失败') - } -} - -const startEdit = () => { - editRules.value = rules.value.map(rule => ({ - ...rule, - type: rule.type || 'tcp', - enabled: rule.enabled !== false - })) - editing.value = true -} - -const cancelEdit = () => { - editing.value = false -} - -const addRule = () => { - editRules.value.push({ - name: '', local_ip: '127.0.0.1', local_port: 80, remote_port: 8080, type: 'tcp', enabled: true - }) -} - -const removeRule = (index: number) => { - editRules.value.splice(index, 1) -} - -const saveEdit = async () => { - try { - // 合并插件管理的规则和编辑后的规则 - await updateClient(clientId, { id: clientId, nickname: nickname.value, rules: editRules.value }) - editing.value = false - message.success('保存成功') - await loadClient() - // 如果客户端在线,自动推送配置 - if (online.value) { - try { - await pushConfigToClient(clientId) - message.success('配置已自动推送到客户端') - } catch (e: any) { - message.warning('配置已保存,但推送失败: ' + (e.response?.data || '未知错误')) - } - } - } catch (e) { - message.error('保存失败') - } -} - -const confirmDelete = () => { - dialog.warning({ - title: '确认删除', - content: '确定要删除此客户端吗?', - positiveText: '删除', - negativeText: '取消', - onPositiveClick: async () => { - try { - await deleteClient(clientId) - message.success('删除成功') - router.push('/') - } catch (e) { - message.error('删除失败') - } - } - }) -} - -const pushConfig = async () => { - try { - await pushConfigToClient(clientId) - message.success('配置已推送') - } catch (e: any) { - message.error(e.response?.data || '推送失败') - } -} - -const disconnect = () => { - dialog.warning({ - title: '确认断开', - content: '确定要断开此客户端连接吗?', - positiveText: '断开', - negativeText: '取消', - onPositiveClick: async () => { - try { - await disconnectClient(clientId) - online.value = false - message.success('已断开连接') - } catch (e: any) { - message.error(e.response?.data || '断开失败') - } - } - }) -} - -// 重启客户端 -const handleRestartClient = () => { - dialog.warning({ - title: '确认重启', - content: '确定要重启此客户端吗?客户端将断开连接并自动重连。', - positiveText: '重启', - negativeText: '取消', - onPositiveClick: async () => { - try { - await restartClient(clientId) - message.success('重启命令已发送,客户端将自动重连') - setTimeout(() => loadClient(), 3000) - } catch (e: any) { - message.error(e.response?.data || '重启失败') - } - } - }) -} - -// 启动客户端插件 -const handleStartPlugin = async (plugin: ClientPlugin) => { - const rule = rules.value.find(r => r.type === plugin.name) - const ruleName = rule?.name || plugin.name - try { - await startClientPlugin(clientId, plugin.id, ruleName) - message.success(`已启动 ${plugin.name}`) - plugin.running = true - } catch (e: any) { - message.error(e.message || '启动失败') - } -} - -// 重启客户端插件 -const handleRestartPlugin = async (plugin: ClientPlugin) => { - // 找到使用此插件的规则 - const rule = rules.value.find(r => r.type === plugin.name) - const ruleName = rule?.name || plugin.name - try { - await restartClientPlugin(clientId, plugin.id, ruleName) - message.success(`已重启 ${plugin.name}`) - plugin.running = true - } catch (e: any) { - message.error(e.message || '重启失败') - } -} - -// 停止客户端插件 -const handleStopPlugin = async (plugin: ClientPlugin) => { - const rule = rules.value.find(r => r.type === plugin.name) - const ruleName = rule?.name || plugin.name - try { - await stopClientPlugin(clientId, plugin.id, ruleName) - message.success(`已停止 ${plugin.name}`) - plugin.running = false - } catch (e: any) { - message.error(e.message || '停止失败') - } +// Plugin Actions +const handleOpenPlugin = (plugin: ClientPlugin) => { + if (!plugin.remote_port) return + const hostname = window.location.hostname + const url = `http://${hostname}:${plugin.remote_port}` + window.open(url, '_blank') } const toggleClientPlugin = async (plugin: ClientPlugin) => { @@ -364,33 +312,35 @@ const toggleClientPlugin = async (plugin: ClientPlugin) => { } } -// 打开插件配置模态框 +// Plugin Config Modal +const showConfigModal = ref(false) +const configPluginName = ref('') +const configSchema = ref([]) +const configValues = ref>({}) +const configLoading = ref(false) const openConfigModal = async (plugin: ClientPlugin) => { configPluginName.value = plugin.name configLoading.value = true showConfigModal.value = true - try { const { data } = await getClientPluginConfig(clientId, plugin.name) configSchema.value = data.schema || [] configValues.value = { ...data.config } - // 填充默认值 - for (const field of configSchema.value) { - if (field.default && !configValues.value[field.key]) { - configValues.value[field.key] = field.default + // Fill defaults + configSchema.value.forEach(f => { + if (f.default && !configValues.value[f.key]) { + configValues.value[f.key] = f.default } - } - } catch (e: any) { - message.error(e.response?.data || '加载配置失败') + }) + } catch (e) { + message.error('加载配置失败') showConfigModal.value = false } finally { configLoading.value = false } } - -// 保存插件配置 const savePluginConfig = async () => { - try { + try { await updateClientPluginConfig(clientId, configPluginName.value, configValues.value) message.success('配置已保存') showConfigModal.value = false @@ -400,429 +350,387 @@ const savePluginConfig = async () => { } } -// 删除客户端插件 +// Standard Client Actions +const confirmDelete = () => { + dialog.warning({ + title: '确认删除', content: '确定要删除此客户端吗?', + positiveText: '删除', negativeText: '取消', + onPositiveClick: async () => { + await deleteClient(clientId); router.push('/') + } + }) +} +const disconnect = () => { + dialog.warning({ + title: '确认断开', content: '确定要断开连接吗?', + positiveText: '断开', negativeText: '取消', + onPositiveClick: async () => { + await disconnectClient(clientId); loadClient() + } + }) +} +const handleRestartClient = () => { + dialog.warning({ + title: '确认重启', content: '确定要重启客户端吗?', + positiveText: '重启', negativeText: '取消', + onPositiveClick: async () => { + await restartClient(clientId); message.success('重启命令已发送'); setTimeout(loadClient, 3000) + } + }) +} + +// Lifecycle +onMounted(() => { + loadRuleSchemas() + loadClient() +}) + +// Log Viewer +const showLogViewer = ref(false) + +// Plugin Status Actions +const handleStartPlugin = async (plugin: ClientPlugin) => { + const rule = rules.value.find(r => r.type === plugin.name) + const ruleName = rule?.name || plugin.name + try { await startClientPlugin(clientId, plugin.id, ruleName); message.success('已启动'); plugin.running = true } catch(e:any){ message.error(e.message) } +} +const handleRestartPlugin = async (plugin: ClientPlugin) => { + const rule = rules.value.find(r => r.type === plugin.name) + const ruleName = rule?.name || plugin.name + try { await restartClientPlugin(clientId, plugin.id, ruleName); message.success('已重启'); plugin.running = true } catch(e:any){ message.error(e.message)} +} +const handleStopPlugin = async (plugin: ClientPlugin) => { + const rule = rules.value.find(r => r.type === plugin.name) + const ruleName = rule?.name || plugin.name + try { await stopClientPlugin(clientId, plugin.id, ruleName); message.success('已停止'); plugin.running = false } catch(e:any){ message.error(e.message)} +} const handleDeletePlugin = (plugin: ClientPlugin) => { - dialog.warning({ - title: '确认删除', - content: `确定要删除插件 ${plugin.name} 吗?`, - positiveText: '删除', - negativeText: '取消', - onPositiveClick: async () => { - try { - await deleteClientPlugin(clientId, plugin.id) - message.success(`已删除 ${plugin.name}`) - await loadClient() - } catch (e: any) { - message.error(e.response?.data || '删除失败') - } - } - }) + dialog.warning({ + title: '确认删除', content: `确定要删除插件 ${plugin.name} 吗?`, + positiveText: '删除', negativeText: '取消', + onPositiveClick: async () => { + await deleteClientPlugin(clientId, plugin.id); message.success('已删除'); loadClient() + } + }) }