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')
}
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> => {
const instance = await getAxios()
const proxies = (await instance.get('/proxies')) as IMihomoProxies

View File

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

View File

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

View File

@ -1,18 +1,95 @@
import { Card, CardBody } from '@heroui/react'
import React from 'react'
import { Card, CardBody, Chip, Switch } from '@heroui/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 (
<div className={`w-full px-2 pb-2 ${index === 0 ? 'pt-2' : ''}`}>
<Card>
<CardBody className="w-full">
<div title={payload} className="text-ellipsis whitespace-nowrap overflow-hidden">
<div className={`w-full px-2 pb-2 ${listIndex === 0 ? 'pt-2' : ''}`}>
<Card className={!isEnabled ? 'opacity-50' : ''}>
<CardBody className="py-3 px-4">
<div className="flex justify-between items-center gap-4">
{/* 左侧:规则信息 */}
<div className="flex-1 min-w-0 flex items-center gap-3">
{/* 规则内容 */}
<div className="flex-1 min-w-0">
<div title={payload} className="text-sm font-medium truncate mb-1.5">
{payload}
</div>
<div className="flex justify-start text-foreground-500">
<div>{type}</div>
<div className="ml-2">{proxy}</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>
</CardBody>
</Card>

View File

@ -529,6 +529,10 @@
"outbound.modes.direct": "Direct",
"rules.title": "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.input.placeholder": "Enter override URL",
"override.import": "Import",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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