feat: add rule statistics and disable toggle

This commit is contained in:
Memory 2026-02-03 20:25:57 +08:00 committed by GitHub
parent 1c17cfb683
commit d2f700a0ef
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 131 additions and 15 deletions

View File

@ -85,6 +85,11 @@ export const mihomoRules = async (): Promise<IMihomoRulesInfo> => {
return await instance.get('/rules') return await instance.get('/rules')
} }
export const mihomoRulesDisable = async (rules: Record<string, boolean>): Promise<void> => {
const instance = await getAxios()
return await instance.patch('/rules/disable', rules)
}
export const mihomoProxies = async (): Promise<IMihomoProxies> => { export const mihomoProxies = async (): Promise<IMihomoProxies> => {
const instance = await getAxios() const instance = await getAxios()
const proxies = (await instance.get('/proxies')) as IMihomoProxies const proxies = (await instance.get('/proxies')) as IMihomoProxies

View File

@ -24,7 +24,8 @@ import {
mihomoVersion, mihomoVersion,
patchMihomoConfig, patchMihomoConfig,
mihomoSmartGroupWeights, mihomoSmartGroupWeights,
mihomoSmartFlushCache mihomoSmartFlushCache,
mihomoRulesDisable
} from '../core/mihomoApi' } from '../core/mihomoApi'
import { checkAutoRun, disableAutoRun, enableAutoRun } from '../sys/autoRun' import { checkAutoRun, disableAutoRun, enableAutoRun } from '../sys/autoRun'
import { import {
@ -206,6 +207,7 @@ const asyncHandlers: Record<string, AsyncFn> = {
mihomoCloseConnection, mihomoCloseConnection,
mihomoCloseAllConnections, mihomoCloseAllConnections,
mihomoRules, mihomoRules,
mihomoRulesDisable,
mihomoProxies, mihomoProxies,
mihomoGroups, mihomoGroups,
mihomoProxyProviders, mihomoProxyProviders,

View File

@ -7,6 +7,7 @@ const validInvokeChannels = [
'mihomoCloseConnection', 'mihomoCloseConnection',
'mihomoCloseAllConnections', 'mihomoCloseAllConnections',
'mihomoRules', 'mihomoRules',
'mihomoRulesDisable',
'mihomoProxies', 'mihomoProxies',
'mihomoGroups', 'mihomoGroups',
'mihomoProxyProviders', 'mihomoProxyProviders',

View File

@ -1,18 +1,95 @@
import { Card, CardBody } from '@heroui/react' import { Card, CardBody, Chip, Switch } from '@heroui/react'
import React from 'react' import React, { useState, useEffect } from 'react'
import { mihomoRulesDisable } from '@renderer/utils/ipc'
import { useTranslation } from 'react-i18next'
interface RuleItemProps extends IMihomoRulesDetail {
index: number
}
const RuleItem: React.FC<RuleItemProps> = (props) => {
const { t } = useTranslation()
const { type, payload, proxy, index: listIndex, extra } = props
const ruleIndex = props.index ?? listIndex
const { disabled, hitCount, hitAt, missCount, missAt } = extra
const [isEnabled, setIsEnabled] = useState(!disabled)
useEffect(() => {
setIsEnabled(!disabled)
}, [disabled])
const handleToggle = async (v: boolean): Promise<void> => {
setIsEnabled(v)
try {
await mihomoRulesDisable({ [ruleIndex]: !v })
} catch (error) {
console.error('Failed to toggle rule:', error)
setIsEnabled(!v)
}
}
const totalCount = hitCount + missCount
const hitRate = totalCount > 0 ? (hitCount / totalCount) * 100 : 0
const formatRelativeTime = (timestamp: string): string => {
const now = Date.now()
const time = new Date(timestamp).getTime()
const diff = Math.floor((now - time) / 1000)
if (diff < 60) return t('rules.hitAt.seconds')
if (diff < 3600) return t('rules.hitAt.minutes', { count: Math.floor(diff / 60) })
if (diff < 86400) return t('rules.hitAt.hours', { count: Math.floor(diff / 3600) })
return t('rules.hitAt.days', { count: Math.floor(diff / 86400) })
}
const hasStats = totalCount > 0
const RuleItem: React.FC<IMihomoRulesDetail & { index: number }> = (props) => {
const { type, payload, proxy, index } = props
return ( return (
<div className={`w-full px-2 pb-2 ${index === 0 ? 'pt-2' : ''}`}> <div className={`w-full px-2 pb-2 ${listIndex === 0 ? 'pt-2' : ''}`}>
<Card> <Card className={!isEnabled ? 'opacity-50' : ''}>
<CardBody className="w-full"> <CardBody className="py-3 px-4">
<div title={payload} className="text-ellipsis whitespace-nowrap overflow-hidden"> <div className="flex justify-between items-center gap-4">
{payload} {/* 左侧:规则信息 */}
</div> <div className="flex-1 min-w-0 flex items-center gap-3">
<div className="flex justify-start text-foreground-500"> {/* 规则内容 */}
<div>{type}</div> <div className="flex-1 min-w-0">
<div className="ml-2">{proxy}</div> <div title={payload} className="text-sm font-medium truncate mb-1.5">
{payload}
</div>
<div className="flex items-center gap-2">
<Chip size="sm" variant="flat" color="default" className="text-xs">
{type}
</Chip>
<Chip size="sm" variant="flat" color="default" className="text-xs">
{proxy}
</Chip>
</div>
</div>
{/* 统计信息 */}
{hasStats && (
<div className="flex items-center gap-3 text-xs shrink-0">
<span className="text-foreground-500 whitespace-nowrap">
{formatRelativeTime(hitAt || missAt)}
</span>
<span className="text-foreground-600 font-medium whitespace-nowrap">
{hitCount}/{totalCount}
</span>
<Chip size="sm" variant="flat" color="primary" className="text-xs">
{hitRate.toFixed(1)}%
</Chip>
</div>
)}
</div>
{/* 右侧开关 */}
<Switch
size="sm"
isSelected={isEnabled}
onValueChange={handleToggle}
aria-label="Toggle rule"
/>
</div> </div>
</CardBody> </CardBody>
</Card> </Card>

View File

@ -529,6 +529,10 @@
"outbound.modes.direct": "Direct", "outbound.modes.direct": "Direct",
"rules.title": "Rules", "rules.title": "Rules",
"rules.filter": "Filter Rules", "rules.filter": "Filter Rules",
"rules.hitAt.seconds": "Hit moments ago",
"rules.hitAt.minutes": "Hit {{count}} min ago",
"rules.hitAt.hours": "Hit {{count}} hrs ago",
"rules.hitAt.days": "Hit {{count}} days ago",
"override.title": "Override", "override.title": "Override",
"override.input.placeholder": "Enter override URL", "override.input.placeholder": "Enter override URL",
"override.import": "Import", "override.import": "Import",

View File

@ -498,6 +498,10 @@
"outbound.modes.direct": "مستقیم", "outbound.modes.direct": "مستقیم",
"rules.title": "قوانین", "rules.title": "قوانین",
"rules.filter": "فیلتر قوانین", "rules.filter": "فیلتر قوانین",
"rules.hitAt.seconds": "چند لحظه پیش",
"rules.hitAt.minutes": "{{count}} دقیقه پیش",
"rules.hitAt.hours": "{{count}} ساعت پیش",
"rules.hitAt.days": "{{count}} روز پیش",
"override.title": "جایگزینی", "override.title": "جایگزینی",
"override.input.placeholder": "وارد کردن URL جایگزین", "override.input.placeholder": "وارد کردن URL جایگزین",
"override.import": "وارد کردن", "override.import": "وارد کردن",

View File

@ -500,6 +500,10 @@
"outbound.modes.direct": "Прямой", "outbound.modes.direct": "Прямой",
"rules.title": "Правила", "rules.title": "Правила",
"rules.filter": "Фильтр правил", "rules.filter": "Фильтр правил",
"rules.hitAt.seconds": "Совпадение несколько секунд назад",
"rules.hitAt.minutes": "Совпадение {{count}} мин назад",
"rules.hitAt.hours": "Совпадение {{count}} ч назад",
"rules.hitAt.days": "Совпадение {{count}} дн назад",
"override.title": "Переопределение", "override.title": "Переопределение",
"override.input.placeholder": "Введите URL переопределения", "override.input.placeholder": "Введите URL переопределения",
"override.import": "Импорт", "override.import": "Импорт",

View File

@ -529,6 +529,10 @@
"outbound.modes.direct": "直连", "outbound.modes.direct": "直连",
"rules.title": "分流规则", "rules.title": "分流规则",
"rules.filter": "筛选过滤", "rules.filter": "筛选过滤",
"rules.hitAt.seconds": "最近命中于 几秒前",
"rules.hitAt.minutes": "最近命中于 {{count}} 分钟前",
"rules.hitAt.hours": "最近命中于 {{count}} 小时前",
"rules.hitAt.days": "最近命中于 {{count}} 天前",
"override.title": "覆写", "override.title": "覆写",
"override.input.placeholder": "输入覆写 URL", "override.input.placeholder": "输入覆写 URL",
"override.import": "导入", "override.import": "导入",

View File

@ -529,6 +529,10 @@
"outbound.modes.direct": "直連", "outbound.modes.direct": "直連",
"rules.title": "分流規則", "rules.title": "分流規則",
"rules.filter": "篩選過濾", "rules.filter": "篩選過濾",
"rules.hitAt.seconds": "最近命中於 幾秒前",
"rules.hitAt.minutes": "最近命中於 {{count}} 分鐘前",
"rules.hitAt.hours": "最近命中於 {{count}} 小時前",
"rules.hitAt.days": "最近命中於 {{count}} 天前",
"override.title": "覆寫", "override.title": "覆寫",
"override.input.placeholder": "輸入覆寫 URL", "override.input.placeholder": "輸入覆寫 URL",
"override.import": "匯入", "override.import": "匯入",

View File

@ -43,11 +43,12 @@ const Rules: React.FC = () => {
data={filteredRules} data={filteredRules}
itemContent={(i, rule) => ( itemContent={(i, rule) => (
<RuleItem <RuleItem
index={i} index={rule.index ?? i}
type={rule.type} type={rule.type}
payload={rule.payload} payload={rule.payload}
proxy={rule.proxy} proxy={rule.proxy}
size={rule.size} size={rule.size}
extra={rule.extra}
/> />
)} )}
/> />

View File

@ -19,6 +19,7 @@ interface IpcApi {
mihomoCloseConnection: (id: string) => Promise<void> mihomoCloseConnection: (id: string) => Promise<void>
mihomoCloseAllConnections: () => Promise<void> mihomoCloseAllConnections: () => Promise<void>
mihomoRules: () => Promise<IMihomoRulesInfo> mihomoRules: () => Promise<IMihomoRulesInfo>
mihomoRulesDisable: (rules: Record<string, boolean>) => Promise<void>
mihomoProxies: () => Promise<IMihomoProxies> mihomoProxies: () => Promise<IMihomoProxies>
mihomoGroups: () => Promise<IMihomoMixedGroup[]> mihomoGroups: () => Promise<IMihomoMixedGroup[]>
mihomoProxyProviders: () => Promise<IMihomoProxyProviders> mihomoProxyProviders: () => Promise<IMihomoProxyProviders>
@ -173,6 +174,7 @@ export const {
mihomoCloseConnection, mihomoCloseConnection,
mihomoCloseAllConnections, mihomoCloseAllConnections,
mihomoRules, mihomoRules,
mihomoRulesDisable,
mihomoProxies, mihomoProxies,
mihomoGroups, mihomoGroups,
mihomoProxyProviders, mihomoProxyProviders,

View File

@ -72,6 +72,14 @@ interface IMihomoRulesDetail {
payload: string payload: string
proxy: string proxy: string
size: number size: number
index: number
extra: {
disabled: boolean
hitCount: number
hitAt: string
missCount: number
missAt: string
}
} }
interface IMihomoConnectionsInfo { interface IMihomoConnectionsInfo {