mirror of
https://gh.catmak.name/https://github.com/mihomo-party-org/mihomo-party
synced 2025-12-27 13:10:30 +08:00
perf: rule edit page
This commit is contained in:
parent
e1b8c9960a
commit
f953dca228
@ -12,9 +12,10 @@ import {
|
|||||||
Autocomplete,
|
Autocomplete,
|
||||||
AutocompleteItem,
|
AutocompleteItem,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
Divider
|
Divider,
|
||||||
|
Spinner
|
||||||
} from '@heroui/react'
|
} from '@heroui/react'
|
||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useState, useMemo, useCallback, startTransition, memo, useDeferredValue } from 'react'
|
||||||
import { getProfileStr, setRuleStr, getRuleStr } from '@renderer/utils/ipc'
|
import { getProfileStr, setRuleStr, getRuleStr } from '@renderer/utils/ipc'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import yaml from 'js-yaml'
|
import yaml from 'js-yaml'
|
||||||
@ -278,24 +279,152 @@ const isAddRuleDisabled = (newRule: RuleItem, validateRulePayload: (ruleType: st
|
|||||||
(newRule.type !== 'MATCH' && newRule.payload.trim() !== '' && !validateRulePayload(newRule.type, newRule.payload));
|
(newRule.type !== 'MATCH' && newRule.payload.trim() !== '' && !validateRulePayload(newRule.type, newRule.payload));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 避免整个列表重新渲染
|
||||||
|
interface RuleListItemProps {
|
||||||
|
rule: RuleItem;
|
||||||
|
originalIndex: number;
|
||||||
|
isDeleted: boolean;
|
||||||
|
isPrependOrAppend: boolean;
|
||||||
|
rulesLength: number;
|
||||||
|
onMoveUp: (index: number) => void;
|
||||||
|
onMoveDown: (index: number) => void;
|
||||||
|
onRemove: (index: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const RuleListItem = memo<RuleListItemProps>(({
|
||||||
|
rule,
|
||||||
|
originalIndex,
|
||||||
|
isDeleted,
|
||||||
|
isPrependOrAppend,
|
||||||
|
rulesLength,
|
||||||
|
onMoveUp,
|
||||||
|
onMoveDown,
|
||||||
|
onRemove
|
||||||
|
}) => {
|
||||||
|
let bgColorClass = 'bg-content2';
|
||||||
|
let textStyleClass = '';
|
||||||
|
|
||||||
|
if (isDeleted) {
|
||||||
|
bgColorClass = 'bg-danger-50 opacity-70';
|
||||||
|
textStyleClass = 'line-through text-foreground-500';
|
||||||
|
} else if (isPrependOrAppend) {
|
||||||
|
bgColorClass = 'bg-success-50';
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`flex items-center gap-2 p-2 rounded-lg ${bgColorClass}`}>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<Chip size="sm" variant="flat">
|
||||||
|
{rule.type}
|
||||||
|
</Chip>
|
||||||
|
{/* 显示附加参数 */}
|
||||||
|
<div className="flex gap-1">
|
||||||
|
{rule.additionalParams && rule.additionalParams.length > 0 && (
|
||||||
|
rule.additionalParams.map((param, idx) => (
|
||||||
|
<Chip key={idx} size="sm" variant="flat" color="secondary">{param}</Chip>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className={`font-medium truncate ${textStyleClass}`}>
|
||||||
|
{rule.type === 'MATCH' ? rule.proxy : rule.payload}
|
||||||
|
</div>
|
||||||
|
{rule.proxy && rule.type !== 'MATCH' && (
|
||||||
|
<div className={`text-sm text-foreground-500 truncate ${textStyleClass}`}>{rule.proxy}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-1">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="light"
|
||||||
|
onPress={() => originalIndex !== -1 && onMoveUp(originalIndex)}
|
||||||
|
isIconOnly
|
||||||
|
isDisabled={originalIndex === -1 || originalIndex === 0 || isDeleted}
|
||||||
|
>
|
||||||
|
<IoMdArrowUp className="text-lg" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="light"
|
||||||
|
onPress={() => originalIndex !== -1 && onMoveDown(originalIndex)}
|
||||||
|
isIconOnly
|
||||||
|
isDisabled={originalIndex === -1 || originalIndex === rulesLength - 1 || isDeleted}
|
||||||
|
>
|
||||||
|
<IoMdArrowDown className="text-lg" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
color={originalIndex !== -1 && isDeleted ? "success" : "danger"}
|
||||||
|
variant="light"
|
||||||
|
onPress={() => originalIndex !== -1 && onRemove(originalIndex)}
|
||||||
|
isIconOnly
|
||||||
|
>
|
||||||
|
{originalIndex !== -1 && isDeleted ? <IoMdUndo className="text-lg" /> : <IoMdTrash className="text-lg" />}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}, (prevProps, nextProps) => {
|
||||||
|
return (
|
||||||
|
prevProps.rule === nextProps.rule &&
|
||||||
|
prevProps.originalIndex === nextProps.originalIndex &&
|
||||||
|
prevProps.isDeleted === nextProps.isDeleted &&
|
||||||
|
prevProps.isPrependOrAppend === nextProps.isPrependOrAppend &&
|
||||||
|
prevProps.rulesLength === nextProps.rulesLength
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
const EditRulesModal: React.FC<Props> = (props) => {
|
const EditRulesModal: React.FC<Props> = (props) => {
|
||||||
const { id, onClose } = props
|
const { id, onClose } = props
|
||||||
const [rules, setRules] = useState<RuleItem[]>([])
|
const [rules, setRules] = useState<RuleItem[]>([])
|
||||||
const [filteredRules, setFilteredRules] = useState<RuleItem[]>([])
|
|
||||||
const [, setProfileContent] = useState('')
|
const [, setProfileContent] = useState('')
|
||||||
const [newRule, setNewRule] = useState<RuleItem>({ type: 'DOMAIN', payload: '', proxy: 'DIRECT', additionalParams: [] })
|
const [newRule, setNewRule] = useState<RuleItem>({ type: 'DOMAIN', payload: '', proxy: 'DIRECT', additionalParams: [] })
|
||||||
const [searchTerm, setSearchTerm] = useState('')
|
const [searchTerm, setSearchTerm] = useState('')
|
||||||
|
const [deferredSearchTerm, setDeferredSearchTerm] = useState('')
|
||||||
const [proxyGroups, setProxyGroups] = useState<string[]>([])
|
const [proxyGroups, setProxyGroups] = useState<string[]>([])
|
||||||
const [deletedRules, setDeletedRules] = useState<Set<number>>(new Set())
|
const [deletedRules, setDeletedRules] = useState<Set<number>>(new Set())
|
||||||
const [prependRules, setPrependRules] = useState<Set<number>>(new Set())
|
const [prependRules, setPrependRules] = useState<Set<number>>(new Set())
|
||||||
const [appendRules, setAppendRules] = useState<Set<number>>(new Set())
|
const [appendRules, setAppendRules] = useState<Set<number>>(new Set())
|
||||||
|
const [isLoading, setIsLoading] = useState(true)
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const ruleIndexMap = useMemo(() => {
|
||||||
|
const map = new Map<RuleItem, number>()
|
||||||
|
rules.forEach((rule, index) => {
|
||||||
|
map.set(rule, index)
|
||||||
|
})
|
||||||
|
return map
|
||||||
|
}, [rules])
|
||||||
|
|
||||||
|
const filteredRules = useMemo(() => {
|
||||||
|
if (deferredSearchTerm === '') return rules
|
||||||
|
|
||||||
|
const lowerSearch = deferredSearchTerm.toLowerCase()
|
||||||
|
return rules.filter(rule =>
|
||||||
|
rule.type.toLowerCase().includes(lowerSearch) ||
|
||||||
|
rule.payload.toLowerCase().includes(lowerSearch) ||
|
||||||
|
(rule.proxy && rule.proxy.toLowerCase().includes(lowerSearch)) ||
|
||||||
|
(rule.additionalParams && rule.additionalParams.some(param => param.toLowerCase().includes(lowerSearch)))
|
||||||
|
)
|
||||||
|
}, [deferredSearchTerm, rules])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
startTransition(() => {
|
||||||
|
setDeferredSearchTerm(searchTerm)
|
||||||
|
})
|
||||||
|
}, [searchTerm])
|
||||||
|
|
||||||
|
const deferredFilteredRules = useDeferredValue(filteredRules)
|
||||||
|
|
||||||
const getContent = async (): Promise<void> => {
|
const getContent = async (): Promise<void> => {
|
||||||
|
setIsLoading(true)
|
||||||
|
try {
|
||||||
const content = await getProfileStr(id)
|
const content = await getProfileStr(id)
|
||||||
setProfileContent(content)
|
setProfileContent(content)
|
||||||
|
|
||||||
try {
|
|
||||||
const parsed = yaml.load(content) as any
|
const parsed = yaml.load(content) as any
|
||||||
let initialRules: RuleItem[] = [];
|
let initialRules: RuleItem[] = [];
|
||||||
|
|
||||||
@ -427,11 +556,9 @@ const EditRulesModal: React.FC<Props> = (props) => {
|
|||||||
|
|
||||||
// 设置规则列表
|
// 设置规则列表
|
||||||
setRules(allRules);
|
setRules(allRules);
|
||||||
setFilteredRules(allRules);
|
|
||||||
} else {
|
} else {
|
||||||
// 使用初始规则
|
// 使用初始规则
|
||||||
setRules(initialRules);
|
setRules(initialRules);
|
||||||
setFilteredRules(initialRules);
|
|
||||||
// 清空规则标记
|
// 清空规则标记
|
||||||
setPrependRules(new Set());
|
setPrependRules(new Set());
|
||||||
setAppendRules(new Set());
|
setAppendRules(new Set());
|
||||||
@ -441,7 +568,6 @@ const EditRulesModal: React.FC<Props> = (props) => {
|
|||||||
// 规则文件读取失败
|
// 规则文件读取失败
|
||||||
console.debug('规则文件读取失败:', ruleError);
|
console.debug('规则文件读取失败:', ruleError);
|
||||||
setRules(initialRules);
|
setRules(initialRules);
|
||||||
setFilteredRules(initialRules);
|
|
||||||
// 清空规则标记
|
// 清空规则标记
|
||||||
setPrependRules(new Set());
|
setPrependRules(new Set());
|
||||||
setAppendRules(new Set());
|
setAppendRules(new Set());
|
||||||
@ -449,6 +575,8 @@ const EditRulesModal: React.FC<Props> = (props) => {
|
|||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Failed to parse profile content', e)
|
console.error('Failed to parse profile content', e)
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -456,21 +584,28 @@ const EditRulesModal: React.FC<Props> = (props) => {
|
|||||||
getContent()
|
getContent()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
const validateRulePayload = useCallback((ruleType: string, payload: string): boolean => {
|
||||||
if (searchTerm === '') {
|
if (ruleType === 'MATCH') {
|
||||||
setFilteredRules(rules)
|
return true;
|
||||||
} else {
|
|
||||||
const filtered = rules.filter(rule =>
|
|
||||||
rule.type.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
||||||
rule.payload.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
||||||
(rule.proxy && rule.proxy.toLowerCase().includes(searchTerm.toLowerCase())) ||
|
|
||||||
(rule.additionalParams && rule.additionalParams.some(param => param.toLowerCase().includes(searchTerm.toLowerCase())))
|
|
||||||
)
|
|
||||||
setFilteredRules(filtered)
|
|
||||||
}
|
}
|
||||||
}, [searchTerm, rules])
|
|
||||||
|
|
||||||
const handleSave = async (): Promise<void> => {
|
const rule = ruleDefinitionsMap.get(ruleType);
|
||||||
|
const validator = rule?.validator;
|
||||||
|
if (!validator) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return validator(payload);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const isPayloadValid = useMemo(() => {
|
||||||
|
if (newRule.type === 'MATCH' || !newRule.payload) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return validateRulePayload(newRule.type, newRule.payload);
|
||||||
|
}, [newRule.type, newRule.payload, validateRulePayload]);
|
||||||
|
|
||||||
|
const handleSave = useCallback(async (): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
// 保存规则到文件
|
// 保存规则到文件
|
||||||
const prependRuleStrings = Array.from(prependRules)
|
const prependRuleStrings = Array.from(prependRules)
|
||||||
@ -509,7 +644,7 @@ const EditRulesModal: React.FC<Props> = (props) => {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
alert(t('profiles.editRules.saveError') + ': ' + (e instanceof Error ? e.message : String(e)));
|
alert(t('profiles.editRules.saveError') + ': ' + (e instanceof Error ? e.message : String(e)));
|
||||||
}
|
}
|
||||||
}
|
}, [prependRules, deletedRules, rules, appendRules, id, onClose, t])
|
||||||
|
|
||||||
const handleRuleTypeChange = (selected: string): void => {
|
const handleRuleTypeChange = (selected: string): void => {
|
||||||
const noResolveSupported = isRuleSupportsNoResolve(selected);
|
const noResolveSupported = isRuleSupportsNoResolve(selected);
|
||||||
@ -547,14 +682,19 @@ const EditRulesModal: React.FC<Props> = (props) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAddRule = (position: 'prepend' | 'append' = 'append'): void => {
|
const handleAddRule = useCallback((position: 'prepend' | 'append' = 'append'): void => {
|
||||||
if (newRule.type === 'MATCH' || newRule.payload.trim() !== '') {
|
if (!(newRule.type === 'MATCH' || newRule.payload.trim() !== '')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (newRule.type !== 'MATCH' && newRule.payload.trim() !== '' && !validateRulePayload(newRule.type, newRule.payload)) {
|
if (newRule.type !== 'MATCH' && newRule.payload.trim() !== '' && !validateRulePayload(newRule.type, newRule.payload)) {
|
||||||
alert(t('profiles.editRules.invalidPayload') + ': ' + getRuleExample(newRule.type));
|
alert(t('profiles.editRules.invalidPayload') + ': ' + getRuleExample(newRule.type));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const newRuleItem = { ...newRule };
|
const newRuleItem = { ...newRule };
|
||||||
|
|
||||||
|
startTransition(() => {
|
||||||
let updatedRules: RuleItem[];
|
let updatedRules: RuleItem[];
|
||||||
|
|
||||||
if (position === 'prepend') {
|
if (position === 'prepend') {
|
||||||
@ -568,7 +708,7 @@ const EditRulesModal: React.FC<Props> = (props) => {
|
|||||||
// 更新规则索引
|
// 更新规则索引
|
||||||
const { newPrependRules, newAppendRules, newDeletedRules } = updateAllRuleIndicesAfterInsertion(prependRules, appendRules, deletedRules, insertPosition, true);
|
const { newPrependRules, newAppendRules, newDeletedRules } = updateAllRuleIndicesAfterInsertion(prependRules, appendRules, deletedRules, insertPosition, true);
|
||||||
|
|
||||||
// 更新状态
|
// 批量更新状态
|
||||||
setPrependRules(newPrependRules);
|
setPrependRules(newPrependRules);
|
||||||
setAppendRules(newAppendRules);
|
setAppendRules(newAppendRules);
|
||||||
setDeletedRules(newDeletedRules);
|
setDeletedRules(newDeletedRules);
|
||||||
@ -584,7 +724,7 @@ const EditRulesModal: React.FC<Props> = (props) => {
|
|||||||
// 更新规则索引
|
// 更新规则索引
|
||||||
const { newPrependRules, newAppendRules, newDeletedRules } = updateAllRuleIndicesAfterInsertion(prependRules, appendRules, deletedRules, insertPosition, false, true);
|
const { newPrependRules, newAppendRules, newDeletedRules } = updateAllRuleIndicesAfterInsertion(prependRules, appendRules, deletedRules, insertPosition, false, true);
|
||||||
|
|
||||||
// 更新状态
|
// 批量更新状态
|
||||||
setPrependRules(newPrependRules);
|
setPrependRules(newPrependRules);
|
||||||
setAppendRules(newAppendRules);
|
setAppendRules(newAppendRules);
|
||||||
setDeletedRules(newDeletedRules);
|
setDeletedRules(newDeletedRules);
|
||||||
@ -592,12 +732,11 @@ const EditRulesModal: React.FC<Props> = (props) => {
|
|||||||
|
|
||||||
// 更新规则列表
|
// 更新规则列表
|
||||||
setRules(updatedRules);
|
setRules(updatedRules);
|
||||||
setFilteredRules(updatedRules);
|
});
|
||||||
setNewRule({ type: 'DOMAIN', payload: '', proxy: 'DIRECT', additionalParams: [] });
|
setNewRule({ type: 'DOMAIN', payload: '', proxy: 'DIRECT', additionalParams: [] });
|
||||||
}
|
}, [newRule, rules, prependRules, appendRules, deletedRules, validateRulePayload, t])
|
||||||
}
|
|
||||||
|
|
||||||
const handleRemoveRule = (index: number): void => {
|
const handleRemoveRule = useCallback((index: number): void => {
|
||||||
setDeletedRules(prev => {
|
setDeletedRules(prev => {
|
||||||
const newSet = new Set(prev);
|
const newSet = new Set(prev);
|
||||||
if (newSet.has(index)) {
|
if (newSet.has(index)) {
|
||||||
@ -607,10 +746,11 @@ const EditRulesModal: React.FC<Props> = (props) => {
|
|||||||
}
|
}
|
||||||
return newSet;
|
return newSet;
|
||||||
});
|
});
|
||||||
}
|
}, [])
|
||||||
|
|
||||||
const handleMoveRuleUp = (index: number): void => {
|
const handleMoveRuleUp = useCallback((index: number): void => {
|
||||||
if (index <= 0) return;
|
if (index <= 0) return;
|
||||||
|
startTransition(() => {
|
||||||
const updatedRules = [...rules];
|
const updatedRules = [...rules];
|
||||||
const temp = updatedRules[index];
|
const temp = updatedRules[index];
|
||||||
updatedRules[index] = updatedRules[index - 1];
|
updatedRules[index] = updatedRules[index - 1];
|
||||||
@ -632,22 +772,17 @@ const EditRulesModal: React.FC<Props> = (props) => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// 首先更新规则数组
|
// 批量更新状态
|
||||||
setRules(updatedRules);
|
setRules(updatedRules);
|
||||||
setFilteredRules(updatedRules);
|
|
||||||
|
|
||||||
// 更新删除规则索引
|
|
||||||
setDeletedRules(prev => updateRuleIndices(prev, index, index - 1));
|
setDeletedRules(prev => updateRuleIndices(prev, index, index - 1));
|
||||||
|
|
||||||
// 更新前置规则索引
|
|
||||||
setPrependRules(prev => updateRuleIndices(prev, index, index - 1));
|
setPrependRules(prev => updateRuleIndices(prev, index, index - 1));
|
||||||
|
|
||||||
// 更新后置规则索引
|
|
||||||
setAppendRules(prev => updateRuleIndices(prev, index, index - 1));
|
setAppendRules(prev => updateRuleIndices(prev, index, index - 1));
|
||||||
}
|
});
|
||||||
|
}, [rules, prependRules, appendRules])
|
||||||
|
|
||||||
const handleMoveRuleDown = (index: number): void => {
|
const handleMoveRuleDown = useCallback((index: number): void => {
|
||||||
if (index >= rules.length - 1) return;
|
if (index >= rules.length - 1) return;
|
||||||
|
startTransition(() => {
|
||||||
const updatedRules = [...rules];
|
const updatedRules = [...rules];
|
||||||
const temp = updatedRules[index];
|
const temp = updatedRules[index];
|
||||||
updatedRules[index] = updatedRules[index + 1];
|
updatedRules[index] = updatedRules[index + 1];
|
||||||
@ -669,37 +804,13 @@ const EditRulesModal: React.FC<Props> = (props) => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// 首先更新规则数组
|
// 批量更新状态
|
||||||
setRules(updatedRules);
|
setRules(updatedRules);
|
||||||
setFilteredRules(updatedRules);
|
|
||||||
|
|
||||||
// 更新删除规则索引
|
|
||||||
setDeletedRules(prev => updateRuleIndices(prev, index, index + 1));
|
setDeletedRules(prev => updateRuleIndices(prev, index, index + 1));
|
||||||
|
|
||||||
// 更新前置规则索引
|
|
||||||
setPrependRules(prev => updateRuleIndices(prev, index, index + 1));
|
setPrependRules(prev => updateRuleIndices(prev, index, index + 1));
|
||||||
|
|
||||||
// 更新后置规则索引
|
|
||||||
setAppendRules(prev => updateRuleIndices(prev, index, index + 1));
|
setAppendRules(prev => updateRuleIndices(prev, index, index + 1));
|
||||||
}
|
});
|
||||||
|
}, [rules, prependRules, appendRules])
|
||||||
const validateRulePayload = (ruleType: string, payload: string): boolean => {
|
|
||||||
if (ruleType === 'MATCH') {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const validator = getRuleValidator(ruleType);
|
|
||||||
if (!validator) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return validator(payload);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getRuleValidator = (ruleType: string): ((value: string) => boolean) | undefined => {
|
|
||||||
const rule = ruleDefinitionsMap.get(ruleType);
|
|
||||||
return rule?.validator;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 解析规则字符串
|
// 解析规则字符串
|
||||||
const parseRuleString = (ruleStr: string): RuleItem => {
|
const parseRuleString = (ruleStr: string): RuleItem => {
|
||||||
@ -911,7 +1022,7 @@ const EditRulesModal: React.FC<Props> = (props) => {
|
|||||||
value={newRule.payload}
|
value={newRule.payload}
|
||||||
onValueChange={(value) => setNewRule({ ...newRule, payload: value })}
|
onValueChange={(value) => setNewRule({ ...newRule, payload: value })}
|
||||||
isDisabled={newRule.type === 'MATCH'}
|
isDisabled={newRule.type === 'MATCH'}
|
||||||
color={newRule.payload && newRule.type !== 'MATCH' && !validateRulePayload(newRule.type, newRule.payload) ? "danger" : "default"}
|
color={newRule.payload && newRule.type !== 'MATCH' && !isPayloadValid ? "danger" : "default"}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Autocomplete
|
<Autocomplete
|
||||||
@ -998,7 +1109,11 @@ const EditRulesModal: React.FC<Props> = (props) => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-2 max-h-[calc(100vh-200px)] overflow-y-auto flex-1">
|
<div className="flex flex-col gap-2 max-h-[calc(100vh-200px)] overflow-y-auto flex-1">
|
||||||
{filteredRules.length === 0 ? (
|
{isLoading ? (
|
||||||
|
<div className="flex items-center justify-center h-full py-8">
|
||||||
|
<Spinner size="lg" label={t('common.loading') || 'Loading...'} />
|
||||||
|
</div>
|
||||||
|
) : filteredRules.length === 0 ? (
|
||||||
<div className="text-center text-foreground-500 py-4">
|
<div className="text-center text-foreground-500 py-4">
|
||||||
{rules.length === 0
|
{rules.length === 0
|
||||||
? t('profiles.editRules.noRules')
|
? t('profiles.editRules.noRules')
|
||||||
@ -1007,72 +1122,23 @@ const EditRulesModal: React.FC<Props> = (props) => {
|
|||||||
: t('profiles.editRules.noRules')}
|
: t('profiles.editRules.noRules')}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
filteredRules.map((rule, index) => {
|
deferredFilteredRules.map((rule, index) => {
|
||||||
const originalIndex = rules.indexOf(rule);
|
const originalIndex = ruleIndexMap.get(rule) ?? -1;
|
||||||
let bgColorClass = 'bg-content2';
|
const isDeleted = deletedRules.has(originalIndex);
|
||||||
let textStyleClass = '';
|
const isPrependOrAppend = prependRules.has(originalIndex) || appendRules.has(originalIndex);
|
||||||
if (deletedRules.has(originalIndex)) {
|
|
||||||
bgColorClass = 'bg-danger-50 opacity-70';
|
|
||||||
textStyleClass = 'line-through text-foreground-500';
|
|
||||||
} else if (prependRules.has(originalIndex) || appendRules.has(originalIndex)) {
|
|
||||||
bgColorClass = 'bg-success-50';
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={`${originalIndex}-${index}`} className={`flex items-center gap-2 p-2 rounded-lg ${bgColorClass}`}>
|
<RuleListItem
|
||||||
<div className="flex flex-col">
|
key={`${originalIndex}-${index}`}
|
||||||
<div className="flex items-center gap-1">
|
rule={rule}
|
||||||
<Chip size="sm" variant="flat">
|
originalIndex={originalIndex}
|
||||||
{rule.type}
|
isDeleted={isDeleted}
|
||||||
</Chip>
|
isPrependOrAppend={isPrependOrAppend}
|
||||||
{/* 显示附加参数 */}
|
rulesLength={rules.length}
|
||||||
<div className="flex gap-1">
|
onMoveUp={handleMoveRuleUp}
|
||||||
{rule.additionalParams && rule.additionalParams.length > 0 && (
|
onMoveDown={handleMoveRuleDown}
|
||||||
rule.additionalParams.map((param, idx) => (
|
onRemove={handleRemoveRule}
|
||||||
<Chip key={idx} size="sm" variant="flat" color="secondary">{param}</Chip>
|
/>
|
||||||
))
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex-1 min-w-0">
|
|
||||||
<div className={`font-medium truncate ${textStyleClass}`}>
|
|
||||||
{rule.type === 'MATCH' ? rule.proxy : rule.payload}
|
|
||||||
</div>
|
|
||||||
{rule.proxy && rule.type !== 'MATCH' && (
|
|
||||||
<div className={`text-sm text-foreground-500 truncate ${textStyleClass}`}>{rule.proxy}</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-1">
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="light"
|
|
||||||
onPress={() => originalIndex !== -1 && handleMoveRuleUp(originalIndex)}
|
|
||||||
isIconOnly
|
|
||||||
isDisabled={originalIndex === -1 || originalIndex === 0 || deletedRules.has(originalIndex)}
|
|
||||||
>
|
|
||||||
<IoMdArrowUp className="text-lg" />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="light"
|
|
||||||
onPress={() => originalIndex !== -1 && handleMoveRuleDown(originalIndex)}
|
|
||||||
isIconOnly
|
|
||||||
isDisabled={originalIndex === -1 || originalIndex === rules.length - 1 || deletedRules.has(originalIndex)}
|
|
||||||
>
|
|
||||||
<IoMdArrowDown className="text-lg" />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
color={originalIndex !== -1 && deletedRules.has(originalIndex) ? "success" : "danger"}
|
|
||||||
variant="light"
|
|
||||||
onPress={() => originalIndex !== -1 && handleRemoveRule(originalIndex)}
|
|
||||||
isIconOnly
|
|
||||||
>
|
|
||||||
{originalIndex !== -1 && deletedRules.has(originalIndex) ? <IoMdUndo className="text-lg" /> : <IoMdTrash className="text-lg" />}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
)}
|
)}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user