Compare commits

...

2 Commits

Author SHA1 Message Date
ezequielnick
4f5af4ee30 1.8.8 Released 2025-10-06 12:53:33 +08:00
Memory
1ff165d61b
migrate: move WebUI from global settings to mihomo page 2025-10-06 12:40:01 +08:00
6 changed files with 337 additions and 366 deletions

View File

@ -7,7 +7,7 @@
- Webdav增加Cron定时备份
- 连接卡片纯数字显示样式
- 支持修改点击任务栏的窗口触发行为
- 在设置里增加 WebUI 快捷打开方式
- 内核设置下增加 WebUI 快捷打开方式
### 修复 (Fix)
- MacOS 首次启动时的 ENOENT: no such file or directory(config.yaml)

View File

@ -1,356 +0,0 @@
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 (
<p className="text-sm text-default-500 break-all">
{parts.map((part, index) => {
if (part === '%host' || part === '%port' || part === '%secret') {
return (
<span key={index} className="bg-warning-200 text-warning-800 px-1 rounded">
{part}
</span>
)
}
return part
})}
</p>
)
}
// 可点击的变量标签组件
const ClickableVariableTag: React.FC<{
variable: string;
onClick: (variable: string) => void
}> = ({ variable, onClick }) => {
return (
<span
className="bg-warning-200 text-warning-800 px-1 rounded ml-1 cursor-pointer hover:bg-warning-300"
onClick={() => onClick(variable)}
>
{variable}
</span>
)
}
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<WebUIPanel[]>([])
const [editingPanel, setEditingPanel] = useState<WebUIPanel | null>(null)
const [newPanelName, setNewPanelName] = useState('')
const [newPanelUrl, setNewPanelUrl] = useState('')
const urlInputRef = useRef<HTMLInputElement>(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 (
<SettingCard>
<SettingItem title={t('settings.webui.title')} divider>
<div className="flex gap-2">
<Button
size="sm"
color="primary"
onPress={() => setIsModalOpen(true)}
>
{t('settings.webui.manage')}
</Button>
</div>
</SettingItem>
<SettingItem title={t('settings.webui.currentConfig')}>
<div className="text-sm text-default-500">
<p>{t('settings.webui.host')}: {host}</p>
<p>{t('settings.webui.port')}: {port}</p>
</div>
</SettingItem>
{/* 面板管理模态框 */}
<Modal
isOpen={isModalOpen}
onOpenChange={setIsModalOpen}
size="5xl"
scrollBehavior="inside"
backdrop="blur"
classNames={{ backdrop: 'top-[48px]' }}
hideCloseButton
>
<ModalContent className="h-full w-[calc(100%-100px)]">
<ModalHeader className="flex pb-0 app-drag">
{t('settings.webui.manage')}
</ModalHeader>
<ModalBody className="flex flex-col h-full">
<div className="flex flex-col h-full">
{/* 添加/编辑面板表单 */}
<div className="flex flex-col gap-2 p-3 bg-default-100 rounded-lg flex-shrink-0">
<Input
label={t('settings.webui.panelName')}
placeholder={t('settings.webui.panelNamePlaceholder')}
value={newPanelName}
onValueChange={setNewPanelName}
/>
<Input
ref={urlInputRef}
label={t('settings.webui.panelUrl')}
placeholder={t('settings.webui.panelUrlPlaceholder')}
value={newPanelUrl}
onValueChange={setNewPanelUrl}
/>
<div className="text-xs text-default-500">
{t('settings.webui.variableHint')}:
<ClickableVariableTag variable="%host" onClick={insertVariableAtCursor} />
<ClickableVariableTag variable="%port" onClick={insertVariableAtCursor} />
<ClickableVariableTag variable="%secret" onClick={insertVariableAtCursor} />
</div>
<div className="flex gap-2">
{editingPanel ? (
<>
<Button
size="sm"
color="primary"
onPress={updatePanel}
isDisabled={!newPanelName || !newPanelUrl}
>
{t('common.save')}
</Button>
<Button
size="sm"
color="default"
variant="bordered"
onPress={cancelEditing}
>
{t('common.cancel')}
</Button>
</>
) : (
<Button
size="sm"
color="primary"
onPress={addNewPanel}
isDisabled={!newPanelName || !newPanelUrl}
>
{t('settings.webui.addPanel')}
</Button>
)}
<Button
size="sm"
color="warning"
variant="bordered"
onPress={restoreDefaultPanels}
>
{t('settings.webui.restoreDefaults')}
</Button>
</div>
</div>
{/* 面板列表 */}
<div className="flex flex-col gap-2 mt-2 overflow-y-auto flex-grow">
<h3 className="text-lg font-semibold">{t('settings.webui.panels')}</h3>
{allPanels.map(panel => (
<div key={panel.id} className="flex items-start justify-between p-3 bg-default-50 rounded-lg flex-shrink-0">
<div className="flex-1 mr-2">
<p className="font-medium">{panel.name}</p>
<HighlightedUrl url={panel.url} />
</div>
<div className="flex gap-2">
<Button
isIconOnly
size="sm"
color="primary"
onPress={() => openWebUI(panel)}
>
<MdOpenInNew />
</Button>
<Button
isIconOnly
size="sm"
color="warning"
onPress={() => startEditing(panel)}
>
<MdEdit />
</Button>
<Button
isIconOnly
size="sm"
color="danger"
onPress={() => deletePanel(panel.id)}
>
<MdDelete />
</Button>
</div>
</div>
))}
</div>
</div>
</ModalBody>
<ModalFooter className="pt-0">
<Button
color="primary"
onPress={() => setIsModalOpen(false)}
>
{t('common.close')}
</Button>
</ModalFooter>
</ModalContent>
</Modal>
</SettingCard>
)
}
export default WebUIConfig

View File

@ -72,7 +72,7 @@
"settings.hideConnectionCardWave": "Connection Card Pure Number Display",
"settings.showDockIcon": "Show Dock Icon",
"settings.useWindowFrame": "Use System Title Bar",
"settings.triggerMainWindowBehavior": "Window Trigger Behavior",
"settings.triggerMainWindowBehavior": "Tray Window Trigger Behavior",
"settings.triggerMainWindowBehaviorShow": "Always Show",
"settings.triggerMainWindowBehaviorToggle": "Toggle Show/Hide",
"settings.disableHardwareAcceleration": "Disable Hardware Acceleration",
@ -93,7 +93,6 @@
"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",

View File

@ -72,7 +72,7 @@
"settings.hideConnectionCardWave": "连接卡片纯数字显示",
"settings.showDockIcon": "显示 Dock 图标",
"settings.useWindowFrame": "使用系统标题栏",
"settings.triggerMainWindowBehavior": "窗口触发行为",
"settings.triggerMainWindowBehavior": "托盘窗口触发行为",
"settings.triggerMainWindowBehaviorShow": "总是显示",
"settings.triggerMainWindowBehaviorToggle": "切换显示/隐藏",
"settings.disableHardwareAcceleration": "禁用硬件加速",
@ -93,7 +93,6 @@
"settings.title": "应用设置",
"settings.webui.title": "WebUI 管理面板",
"settings.webui.manage": "管理面板",
"settings.webui.currentConfig": "当前配置信息",
"settings.webui.host": "主机",
"settings.webui.port": "端口",
"settings.webui.secret": "密钥",

View File

@ -18,9 +18,9 @@ import {
installSpecificMihomoCore,
clearMihomoVersionCache
} from '@renderer/utils/ipc'
import React, { useState, useEffect } from 'react'
import React, { useState, useEffect, useRef } from 'react'
import InterfaceModal from '@renderer/components/mihomo/interface-modal'
import { MdDeleteForever } from 'react-icons/md'
import { MdDeleteForever, MdEdit, MdDelete, MdOpenInNew } from 'react-icons/md'
import { useTranslation } from 'react-i18next'
const CoreMap = {
@ -48,10 +48,18 @@ const Mihomo: React.FC = () => {
disableSystemCA,
skipSafePathCheck } = appConfig || {}
const { controledMihomoConfig, patchControledMihomoConfig } = useControledMihomoConfig()
interface WebUIPanel {
id: string
name: string
url: string
isDefault?: boolean
}
const {
ipv6,
'external-controller': externalController = '',
secret,
secret = '',
authentication = [],
'skip-auth-prefixes': skipAuthPrefixes = ['127.0.0.1/32', '::1/128'],
'log-level': logLevel = 'info',
@ -91,6 +99,187 @@ const Mihomo: React.FC = () => {
const [searchTerm, setSearchTerm] = useState('')
const [refreshing, setRefreshing] = useState(false)
// WebUI管理状态
const [isWebUIModalOpen, setIsWebUIModalOpen] = useState(false)
const [allPanels, setAllPanels] = useState<WebUIPanel[]>([])
const [editingPanel, setEditingPanel] = useState<WebUIPanel | null>(null)
const [newPanelName, setNewPanelName] = useState('')
const [newPanelUrl, setNewPanelUrl] = useState('')
const urlInputRef = useRef<HTMLInputElement>(null)
// 解析主机和端口
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
}
]
// 初始化面板列表
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)
}
// 用于高亮显示URL中的变量
const HighlightedUrl: React.FC<{ url: string }> = ({ url }) => {
const parts = url.split(/(%host|%port|%secret)/g)
return (
<p className="text-sm text-default-500 break-all">
{parts.map((part, index) => {
if (part === '%host' || part === '%port' || part === '%secret') {
return (
<span key={index} className="bg-warning-200 text-warning-800 px-1 rounded">
{part}
</span>
)
}
return part
})}
</p>
)
}
// 可点击的变量标签组件
const ClickableVariableTag: React.FC<{
variable: string;
onClick: (variable: string) => void
}> = ({ variable, onClick }) => {
return (
<span
className="bg-warning-200 text-warning-800 px-1 rounded ml-1 cursor-pointer hover:bg-warning-300"
onClick={() => onClick(variable)}
>
{variable}
</span>
)
}
const onChangeNeedRestart = async (patch: Partial<IMihomoConfig>): Promise<void> => {
await patchControledMihomoConfig(patch)
await restartCore()
@ -626,6 +815,18 @@ const Mihomo: React.FC = () => {
/>
</div>
</SettingItem>
<SettingItem title={t('settings.webui.title')} divider>
<div className="flex gap-2">
<Button
size="sm"
color="primary"
isDisabled={!externalController || externalController.trim() === ''}
onPress={() => setIsWebUIModalOpen(true)}
>
{t('settings.webui.manage')}
</Button>
</div>
</SettingItem>
<SettingItem title={t('mihomo.ipv6')} divider>
<Switch
size="sm"
@ -1014,6 +1215,136 @@ const Mihomo: React.FC = () => {
</SettingItem>
</SettingCard>
</BasePage>
{/* WebUI 管理模态框 */}
<Modal
isOpen={isWebUIModalOpen}
onOpenChange={setIsWebUIModalOpen}
size="5xl"
scrollBehavior="inside"
backdrop="blur"
classNames={{ backdrop: 'top-[48px]' }}
hideCloseButton
>
<ModalContent className="h-full w-[calc(100%-100px)]">
<ModalHeader className="flex pb-0 app-drag">
{t('settings.webui.manage')}
</ModalHeader>
<ModalBody className="flex flex-col h-full">
<div className="flex flex-col h-full">
{/* 添加/编辑面板表单 */}
<div className="flex flex-col gap-2 p-3 bg-default-100 rounded-lg flex-shrink-0">
<Input
label={t('settings.webui.panelName')}
placeholder={t('settings.webui.panelNamePlaceholder')}
value={newPanelName}
onValueChange={setNewPanelName}
/>
<Input
ref={urlInputRef}
label={t('settings.webui.panelUrl')}
placeholder={t('settings.webui.panelUrlPlaceholder')}
value={newPanelUrl}
onValueChange={setNewPanelUrl}
/>
<div className="text-xs text-default-500">
{t('settings.webui.variableHint')}:
<ClickableVariableTag variable="%host" onClick={insertVariableAtCursor} />
<ClickableVariableTag variable="%port" onClick={insertVariableAtCursor} />
<ClickableVariableTag variable="%secret" onClick={insertVariableAtCursor} />
</div>
<div className="flex gap-2">
{editingPanel ? (
<>
<Button
size="sm"
color="primary"
onPress={updatePanel}
isDisabled={!newPanelName || !newPanelUrl}
>
{t('common.save')}
</Button>
<Button
size="sm"
color="default"
variant="bordered"
onPress={cancelEditing}
>
{t('common.cancel')}
</Button>
</>
) : (
<Button
size="sm"
color="primary"
onPress={addNewPanel}
isDisabled={!newPanelName || !newPanelUrl}
>
{t('settings.webui.addPanel')}
</Button>
)}
<Button
size="sm"
color="warning"
variant="bordered"
onPress={restoreDefaultPanels}
>
{t('settings.webui.restoreDefaults')}
</Button>
</div>
</div>
{/* 面板列表 */}
<div className="flex flex-col gap-2 mt-2 overflow-y-auto flex-grow">
<h3 className="text-lg font-semibold">{t('settings.webui.panels')}</h3>
{allPanels.map(panel => (
<div key={panel.id} className="flex items-start justify-between p-3 bg-default-50 rounded-lg flex-shrink-0">
<div className="flex-1 mr-2">
<p className="font-medium">{panel.name}</p>
<HighlightedUrl url={panel.url} />
</div>
<div className="flex gap-2">
<Button
isIconOnly
size="sm"
color="primary"
onPress={() => openWebUI(panel)}
>
<MdOpenInNew />
</Button>
<Button
isIconOnly
size="sm"
color="warning"
onPress={() => startEditing(panel)}
>
<MdEdit />
</Button>
<Button
isIconOnly
size="sm"
color="danger"
onPress={() => deletePanel(panel.id)}
>
<MdDelete />
</Button>
</div>
</div>
))}
</div>
</div>
</ModalBody>
<ModalFooter className="pt-0">
<Button
color="primary"
onPress={() => setIsWebUIModalOpen(false)}
>
{t('common.close')}
</Button>
</ModalFooter>
</ModalContent>
</Modal>
{/* 自定义版本选择模态框 */}
<Modal
isOpen={isOpen}

View File

@ -10,7 +10,6 @@ import ShortcutConfig from '@renderer/components/settings/shortcut-config'
import { FaTelegramPlane } from 'react-icons/fa'
import SiderConfig from '@renderer/components/settings/sider-config'
import SubStoreConfig from '@renderer/components/settings/substore-config'
import WebUIConfig from '@renderer/components/settings/webui-config'
import { useTranslation } from 'react-i18next'
const Settings: React.FC = () => {
@ -61,7 +60,6 @@ const Settings: React.FC = () => {
}
>
<GeneralConfig />
<WebUIConfig />
<SubStoreConfig />
<SiderConfig />
<WebdavConfig />