From a7de9b2588e133de00a4ffd176f4b1b1915698c1 Mon Sep 17 00:00:00 2001
From: Memory <134070804+Memory2314@users.noreply.github.com>
Date: Fri, 3 Oct 2025 22:49:41 +0800
Subject: [PATCH] refactor: Redesign and migrate WebUI to global settings
---
.../src/components/settings/webui-config.tsx | 356 ++++++++++++++++++
src/renderer/src/locales/en-US.json | 21 +-
src/renderer/src/locales/zh-CN.json | 21 +-
src/renderer/src/pages/profiles.tsx | 77 +---
src/renderer/src/pages/settings.tsx | 2 +
5 files changed, 389 insertions(+), 88 deletions(-)
create mode 100644 src/renderer/src/components/settings/webui-config.tsx
diff --git a/src/renderer/src/components/settings/webui-config.tsx b/src/renderer/src/components/settings/webui-config.tsx
new file mode 100644
index 0000000..37ad16d
--- /dev/null
+++ b/src/renderer/src/components/settings/webui-config.tsx
@@ -0,0 +1,356 @@
+import React, { useState, useEffect, useRef } from 'react'
+import SettingCard from '../base/base-setting-card'
+import SettingItem from '../base/base-setting-item'
+import { Button, Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Input } from '@heroui/react'
+import { useControledMihomoConfig } from '@renderer/hooks/use-controled-mihomo-config'
+import { useTranslation } from 'react-i18next'
+import { MdEdit, MdDelete, MdOpenInNew } from 'react-icons/md'
+
+interface WebUIPanel {
+ id: string
+ name: string
+ url: string
+ isDefault?: boolean
+}
+
+// 用于高亮显示URL中的变量
+const HighlightedUrl: React.FC<{ url: string }> = ({ url }) => {
+ const parts = url.split(/(%host|%port|%secret)/g)
+
+ return (
+
+ {parts.map((part, index) => {
+ if (part === '%host' || part === '%port' || part === '%secret') {
+ return (
+
+ {part}
+
+ )
+ }
+ return part
+ })}
+
+ )
+}
+
+// 可点击的变量标签组件
+const ClickableVariableTag: React.FC<{
+ variable: string;
+ onClick: (variable: string) => void
+}> = ({ variable, onClick }) => {
+ return (
+ onClick(variable)}
+ >
+ {variable}
+
+ )
+}
+
+const WebUIConfig: React.FC = () => {
+ const { t } = useTranslation()
+ const { controledMihomoConfig } = useControledMihomoConfig()
+
+ const externalController = controledMihomoConfig?.['external-controller'] || ''
+ const secret = controledMihomoConfig?.secret || ''
+
+ // 解析主机和端口
+ const parseController = () => {
+ if (externalController) {
+ const [host, port] = externalController.split(':')
+ return { host: host.replace('0.0.0.0', '127.0.0.1'), port }
+ }
+ return { host: '127.0.0.1', port: '9090' }
+ }
+
+ const { host, port } = parseController()
+
+ // 默认WebUI面板选项
+ const defaultWebUIPanels: WebUIPanel[] = [
+ {
+ id: 'metacubexd',
+ name: 'MetaCubeXD',
+ url: 'https://metacubex.github.io/metacubexd/#/setup?http=true&hostname=%host&port=%port&secret=%secret',
+ isDefault: true
+ },
+ {
+ id: 'yacd',
+ name: 'YACD',
+ url: 'https://yacd.metacubex.one/?hostname=%host&port=%port&secret=%secret',
+ isDefault: true
+ },
+ {
+ id: 'zashboard',
+ name: 'Zashboard',
+ url: 'https://board.zash.run.place/#/setup?http=true&hostname=%host&port=%port&secret=%secret',
+ isDefault: true
+ }
+ ]
+
+ const [isModalOpen, setIsModalOpen] = useState(false)
+ const [allPanels, setAllPanels] = useState([])
+ const [editingPanel, setEditingPanel] = useState(null)
+ const [newPanelName, setNewPanelName] = useState('')
+ const [newPanelUrl, setNewPanelUrl] = useState('')
+
+
+ const urlInputRef = useRef(null)
+
+ // 初始化面板列表
+ useEffect(() => {
+ const savedPanels = localStorage.getItem('webui-panels')
+ if (savedPanels) {
+ setAllPanels(JSON.parse(savedPanels))
+ } else {
+ setAllPanels(defaultWebUIPanels)
+ }
+ }, [])
+
+ // 保存面板列表到localStorage
+ useEffect(() => {
+ if (allPanels.length > 0) {
+ localStorage.setItem('webui-panels', JSON.stringify(allPanels))
+ }
+ }, [allPanels])
+
+ // 在URL输入框光标处插入或替换变量
+ const insertVariableAtCursor = (variable: string) => {
+ if (!urlInputRef.current) return
+
+ const input = urlInputRef.current
+ const start = input.selectionStart || 0
+ const end = input.selectionEnd || 0
+ const currentValue = newPanelUrl || ''
+
+ // 如果有选中文本,则替换选中的文本
+ const newValue = currentValue.substring(0, start) + variable + currentValue.substring(end)
+
+ setNewPanelUrl(newValue)
+
+ // 设置光标位置到插入变量之后
+ setTimeout(() => {
+ if (urlInputRef.current) {
+ const newCursorPos = start + variable.length
+ urlInputRef.current.setSelectionRange(newCursorPos, newCursorPos)
+ urlInputRef.current.focus()
+ }
+ }, 0)
+ }
+
+ // 打开WebUI面板
+ const openWebUI = (panel: WebUIPanel) => {
+ const url = panel.url
+ .replace('%host', host)
+ .replace('%port', port)
+ .replace('%secret', secret)
+ window.open(url, '_blank')
+ }
+
+ // 添加新面板
+ const addNewPanel = () => {
+ if (newPanelName && newPanelUrl) {
+ const newPanel: WebUIPanel = {
+ id: Date.now().toString(),
+ name: newPanelName,
+ url: newPanelUrl
+ }
+ setAllPanels([...allPanels, newPanel])
+ setNewPanelName('')
+ setNewPanelUrl('')
+ setEditingPanel(null)
+ }
+ }
+
+ // 更新面板
+ const updatePanel = () => {
+ if (editingPanel && newPanelName && newPanelUrl) {
+ const updatedPanels = allPanels.map(panel =>
+ panel.id === editingPanel.id
+ ? { ...panel, name: newPanelName, url: newPanelUrl }
+ : panel
+ )
+ setAllPanels(updatedPanels)
+ setEditingPanel(null)
+ setNewPanelName('')
+ setNewPanelUrl('')
+ }
+ }
+
+ // 删除面板
+ const deletePanel = (id: string) => {
+ setAllPanels(allPanels.filter(panel => panel.id !== id))
+ }
+
+ // 开始编辑面板
+ const startEditing = (panel: WebUIPanel) => {
+ setEditingPanel(panel)
+ setNewPanelName(panel.name)
+ setNewPanelUrl(panel.url)
+ }
+
+ // 取消编辑
+ const cancelEditing = () => {
+ setEditingPanel(null)
+ setNewPanelName('')
+ setNewPanelUrl('')
+ }
+
+ // 恢复默认面板
+ const restoreDefaultPanels = () => {
+ setAllPanels(defaultWebUIPanels)
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
{t('settings.webui.host')}: {host}
+
{t('settings.webui.port')}: {port}
+
+
+
+ {/* 面板管理模态框 */}
+
+
+
+ {t('settings.webui.manage')}
+
+
+
+ {/* 添加/编辑面板表单 */}
+
+
+
+
+ {t('settings.webui.variableHint')}:
+
+
+
+
+
+ {editingPanel ? (
+ <>
+
+
+ >
+ ) : (
+
+ )}
+
+
+
+
+ {/* 面板列表 */}
+
+
{t('settings.webui.panels')}
+ {allPanels.map(panel => (
+
+
+
+
+
+
+
+
+ ))}
+
+
+
+
+
+
+
+
+
+ )
+}
+
+export default WebUIConfig
\ No newline at end of file
diff --git a/src/renderer/src/locales/en-US.json b/src/renderer/src/locales/en-US.json
index d6147cb..e0e458c 100644
--- a/src/renderer/src/locales/en-US.json
+++ b/src/renderer/src/locales/en-US.json
@@ -91,6 +91,21 @@
"settings.links.github": "GitHub Repository",
"settings.links.telegram": "Telegram Group",
"settings.title": "Application Settings",
+ "settings.webui.title": "WebUI Management Panel",
+ "settings.webui.manage": "Manage Panels",
+ "settings.webui.currentConfig": "Current Configuration",
+ "settings.webui.host": "Host",
+ "settings.webui.port": "Port",
+ "settings.webui.secret": "Secret",
+ "settings.webui.noSecret": "No Secret",
+ "settings.webui.panelName": "Panel Name",
+ "settings.webui.panelUrl": "Panel URL",
+ "settings.webui.panelNamePlaceholder": "Enter panel name",
+ "settings.webui.panelUrlPlaceholder": "Enter panel URL",
+ "settings.webui.variableHint": "Available Variables",
+ "settings.webui.addPanel": "Add Panel",
+ "settings.webui.panels": "Panel List",
+ "settings.webui.restoreDefaults": "Restore Defaults",
"mihomo.userAgent": "Subscription User Agent",
"mihomo.userAgentPlaceholder": "Default: mihomo.party/v{{version}} (clash.meta)",
"mihomo.delayTest.url": "Delay Test URL",
@@ -405,12 +420,6 @@
"profiles.remote": "Remote",
"profiles.local": "Local",
"profiles.trafficUsage": "Traffic Usage Progress",
- "profiles.openWebUI.title": "WebUI Management Panel",
- "profiles.openWebUI.description": "Select a WebUI panel to open",
- "profiles.openWebUI.local": "Local WebUI",
- "profiles.updateWebUI.button": "Update Panel",
- "profiles.updateWebUI.success": "WebUI panel updated successfully",
- "profiles.updateWebUI.failed": "WebUI panel update failed: {{error}}",
"profiles.editInfo.title": "Edit Information",
"profiles.editInfo.name": "Name",
"profiles.editInfo.url": "Subscription URL",
diff --git a/src/renderer/src/locales/zh-CN.json b/src/renderer/src/locales/zh-CN.json
index 40d6f47..9949373 100644
--- a/src/renderer/src/locales/zh-CN.json
+++ b/src/renderer/src/locales/zh-CN.json
@@ -91,6 +91,21 @@
"settings.links.github": "GitHub 仓库",
"settings.links.telegram": "Telegram 群组",
"settings.title": "应用设置",
+ "settings.webui.title": "WebUI 管理面板",
+ "settings.webui.manage": "管理面板",
+ "settings.webui.currentConfig": "当前配置信息",
+ "settings.webui.host": "主机",
+ "settings.webui.port": "端口",
+ "settings.webui.secret": "密钥",
+ "settings.webui.noSecret": "无密钥",
+ "settings.webui.panelName": "面板名称",
+ "settings.webui.panelUrl": "面板URL",
+ "settings.webui.panelNamePlaceholder": "输入面板名称",
+ "settings.webui.panelUrlPlaceholder": "输入面板URL",
+ "settings.webui.variableHint": "可用变量",
+ "settings.webui.addPanel": "添加面板",
+ "settings.webui.panels": "面板列表",
+ "settings.webui.restoreDefaults": "恢复默认",
"mihomo.title": "内核设置",
"mihomo.restart": "重启内核",
"mihomo.memory": "内存使用",
@@ -410,12 +425,6 @@
"profiles.traffic.expired": "已过期",
"profiles.traffic.remainingDays": "剩余 {{days}} 天",
"profiles.traffic.lastUpdate": "最后更新:{{time}}",
- "profiles.openWebUI.title": "WebUI 管理面板",
- "profiles.openWebUI.description": "选择一个 WebUI 面板打开",
- "profiles.openWebUI.local": "本地 WebUI",
- "profiles.updateWebUI.button": "更新面板",
- "profiles.updateWebUI.success": "WebUI 面板更新成功",
- "profiles.updateWebUI.failed": "WebUI 面板更新失败: {{error}}",
"profiles.editInfo.title": "编辑信息",
"profiles.editInfo.name": "名称",
"profiles.editInfo.url": "订阅地址",
diff --git a/src/renderer/src/pages/profiles.tsx b/src/renderer/src/pages/profiles.tsx
index b5be2e1..50e38cd 100644
--- a/src/renderer/src/pages/profiles.tsx
+++ b/src/renderer/src/pages/profiles.tsx
@@ -7,16 +7,12 @@ import {
DropdownItem,
DropdownMenu,
DropdownTrigger,
- Input,
- Card,
- CardBody,
- CardHeader
+ Input
} from '@heroui/react'
import BasePage from '@renderer/components/base/base-page'
import ProfileItem from '@renderer/components/profiles/profile-item'
import { useProfileConfig } from '@renderer/hooks/use-profile-config'
import { useAppConfig } from '@renderer/hooks/use-app-config'
-import { useControledMihomoConfig } from '@renderer/hooks/use-controled-mihomo-config'
import { getFilePath, readTextFile, subStoreCollections, subStoreSubs } from '@renderer/utils/ipc'
import type { KeyboardEvent } from 'react'
import { ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react'
@@ -36,7 +32,6 @@ import SubStoreIcon from '@renderer/components/base/substore-icon'
import useSWR from 'swr'
import { useNavigate } from 'react-router-dom'
import { useTranslation } from 'react-i18next'
-import { mihomoUpgradeUI } from '@renderer/utils/ipc'
const Profiles: React.FC = () => {
const { t } = useTranslation()
@@ -50,9 +45,6 @@ const Profiles: React.FC = () => {
mutateProfileConfig
} = useProfileConfig()
const { appConfig } = useAppConfig()
- const { controledMihomoConfig } = useControledMihomoConfig()
- const externalController = controledMihomoConfig?.['external-controller'] || ''
- const externalUI = (controledMihomoConfig as any)?.['external-ui']
const { useSubStore = true, useCustomSubStore = false, customSubStoreUrl = '' } = appConfig || {}
const { current, items = [] } = profileConfig || {}
const navigate = useNavigate()
@@ -203,26 +195,6 @@ const Profiles: React.FC = () => {
setSortedItems(items)
}, [items])
- // 获取本地WebUI的URL
- const getLocalWebUIUrl = (): string => {
- if (externalController) {
- // 将地址转换为WebUI URL
- // 例如: 127.0.0.1:9090 -> http://127.0.0.1:9090/ui
- const controller = externalController.replace('0.0.0.0', '127.0.0.1')
- // 如果配置了external-ui,使用/ui路径,否则可能需要使用不同的路径
- const uiPath = externalUI ? '/ui' : '/ui' // 默认使用/ui路径
- return `http://${controller}${uiPath}`
- }
- // 默认URL
- return 'http://127.0.0.1:9090/ui'
- }
-
- // 检查本地WebUI是否可用
- const isLocalWebUIAvailable = (): boolean => {
- // 如果有配置的external-controller,则认为本地WebUI可用
- return !!externalController
- }
-
return (
{
- {/* WebUI Card with Multiple Options */}
-
-
-
-
-
{t('profiles.openWebUI.title')}
-
{t('profiles.openWebUI.description')}
-
-
-
-
-
-
- {isLocalWebUIAvailable() && (
- <>
-
-
- >
- )}
-
-
-
-
{
@@ -60,6 +61,7 @@ const Settings: React.FC = () => {
}
>
+