diff --git a/src/main/core/mihomoApi.ts b/src/main/core/mihomoApi.ts index 4c9aeb5..49b9722 100644 --- a/src/main/core/mihomoApi.ts +++ b/src/main/core/mihomoApi.ts @@ -85,6 +85,11 @@ export const mihomoRules = async (): Promise => { return await instance.get('/rules') } +export const mihomoRulesDisable = async (rules: Record): Promise => { + const instance = await getAxios() + return await instance.patch('/rules/disable', rules) +} + export const mihomoProxies = async (): Promise => { const instance = await getAxios() const proxies = (await instance.get('/proxies')) as IMihomoProxies diff --git a/src/main/utils/ipc.ts b/src/main/utils/ipc.ts index b9cc651..b8d538c 100644 --- a/src/main/utils/ipc.ts +++ b/src/main/utils/ipc.ts @@ -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 = { mihomoCloseConnection, mihomoCloseAllConnections, mihomoRules, + mihomoRulesDisable, mihomoProxies, mihomoGroups, mihomoProxyProviders, diff --git a/src/preload/index.ts b/src/preload/index.ts index d596905..a2644cc 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -7,6 +7,7 @@ const validInvokeChannels = [ 'mihomoCloseConnection', 'mihomoCloseAllConnections', 'mihomoRules', + 'mihomoRulesDisable', 'mihomoProxies', 'mihomoGroups', 'mihomoProxyProviders', diff --git a/src/renderer/src/components/rules/rule-item.tsx b/src/renderer/src/components/rules/rule-item.tsx index e188860..1aa433a 100644 --- a/src/renderer/src/components/rules/rule-item.tsx +++ b/src/renderer/src/components/rules/rule-item.tsx @@ -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 = (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 => { + 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 = (props) => { - const { type, payload, proxy, index } = props return ( -
- - -
- {payload} -
-
-
{type}
-
{proxy}
+
+ + +
+ {/* 左侧:规则信息 */} +
+ {/* 规则内容 */} +
+
+ {payload} +
+
+ + {type} + + + {proxy} + +
+
+ + {/* 统计信息 */} + {hasStats && ( +
+ + {formatRelativeTime(hitAt || missAt)} + + + {hitCount}/{totalCount} + + + {hitRate.toFixed(1)}% + +
+ )} +
+ + {/* 右侧开关 */} +
diff --git a/src/renderer/src/locales/en-US.json b/src/renderer/src/locales/en-US.json index abbdf4c..ca7f127 100644 --- a/src/renderer/src/locales/en-US.json +++ b/src/renderer/src/locales/en-US.json @@ -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", diff --git a/src/renderer/src/locales/fa-IR.json b/src/renderer/src/locales/fa-IR.json index 585edb3..40a4f23 100644 --- a/src/renderer/src/locales/fa-IR.json +++ b/src/renderer/src/locales/fa-IR.json @@ -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": "وارد کردن", diff --git a/src/renderer/src/locales/ru-RU.json b/src/renderer/src/locales/ru-RU.json index bcbaed5..e59fe84 100644 --- a/src/renderer/src/locales/ru-RU.json +++ b/src/renderer/src/locales/ru-RU.json @@ -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": "Импорт", diff --git a/src/renderer/src/locales/zh-CN.json b/src/renderer/src/locales/zh-CN.json index 1cf130c..0e932df 100644 --- a/src/renderer/src/locales/zh-CN.json +++ b/src/renderer/src/locales/zh-CN.json @@ -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": "导入", diff --git a/src/renderer/src/locales/zh-TW.json b/src/renderer/src/locales/zh-TW.json index 69647f0..3c95ea3 100644 --- a/src/renderer/src/locales/zh-TW.json +++ b/src/renderer/src/locales/zh-TW.json @@ -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": "匯入", diff --git a/src/renderer/src/pages/rules.tsx b/src/renderer/src/pages/rules.tsx index 3b5dcbf..0dc3beb 100644 --- a/src/renderer/src/pages/rules.tsx +++ b/src/renderer/src/pages/rules.tsx @@ -43,11 +43,12 @@ const Rules: React.FC = () => { data={filteredRules} itemContent={(i, rule) => ( )} /> diff --git a/src/renderer/src/utils/ipc.ts b/src/renderer/src/utils/ipc.ts index c42592e..9beb749 100644 --- a/src/renderer/src/utils/ipc.ts +++ b/src/renderer/src/utils/ipc.ts @@ -19,6 +19,7 @@ interface IpcApi { mihomoCloseConnection: (id: string) => Promise mihomoCloseAllConnections: () => Promise mihomoRules: () => Promise + mihomoRulesDisable: (rules: Record) => Promise mihomoProxies: () => Promise mihomoGroups: () => Promise mihomoProxyProviders: () => Promise @@ -173,6 +174,7 @@ export const { mihomoCloseConnection, mihomoCloseAllConnections, mihomoRules, + mihomoRulesDisable, mihomoProxies, mihomoGroups, mihomoProxyProviders, diff --git a/src/shared/types.d.ts b/src/shared/types.d.ts index da520f0..ec9b69a 100644 --- a/src/shared/types.d.ts +++ b/src/shared/types.d.ts @@ -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 {