/* Copyright (C) 2025 QuantumNous This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ import React, { useState, useCallback, useMemo } from 'react'; import { Button, Collapsible, Input, Select, Tag, Typography, Popconfirm, } from '@douyinfe/semi-ui'; import { IconPlus, IconDelete, IconChevronDown, IconChevronUp, } from '@douyinfe/semi-icons'; import { useTranslation } from 'react-i18next'; const { Text } = Typography; let _idCounter = 0; const uid = () => `gsu_${++_idCounter}`; const OP_ADD = 'add'; const OP_REMOVE = 'remove'; const OP_APPEND = 'append'; function parsePrefix(rawKey) { if (rawKey.startsWith('+:')) return { op: OP_ADD, groupName: rawKey.slice(2) }; if (rawKey.startsWith('-:')) return { op: OP_REMOVE, groupName: rawKey.slice(2) }; return { op: OP_APPEND, groupName: rawKey }; } function toRawKey(op, groupName) { if (op === OP_ADD) return `+:${groupName}`; if (op === OP_REMOVE) return `-:${groupName}`; return groupName; } function parseJSON(str) { if (!str || !str.trim()) return {}; try { return JSON.parse(str); } catch { return {}; } } function flattenRules(nested) { const rules = []; for (const [userGroup, inner] of Object.entries(nested)) { if (typeof inner !== 'object' || inner === null) continue; for (const [rawKey, desc] of Object.entries(inner)) { const { op, groupName } = parsePrefix(rawKey); rules.push({ _id: uid(), userGroup, op, targetGroup: groupName, description: op === OP_REMOVE ? 'remove' : (typeof desc === 'string' ? desc : ''), }); } } return rules; } function nestRules(rules) { const result = {}; rules.forEach(({ userGroup, op, targetGroup, description }) => { if (!userGroup || !targetGroup) return; if (!result[userGroup]) result[userGroup] = {}; result[userGroup][toRawKey(op, targetGroup)] = description; }); return result; } export function serializeGroupSpecialUsable(rules) { const nested = nestRules(rules); return Object.keys(nested).length === 0 ? '' : JSON.stringify(nested, null, 2); } const OP_TAG_MAP = { [OP_ADD]: { color: 'green', label: '添加 (+:)' }, [OP_REMOVE]: { color: 'red', label: '移除 (-:)' }, [OP_APPEND]: { color: 'blue', label: '追加' }, }; function UsableGroupSection({ groupName, items, opOptions, onUpdate, onRemove, onAdd, t }) { const [open, setOpen] = useState(false); return (
setOpen(!open)} >
{open ? : } {groupName} {items.length} {t('条规则')}
e.stopPropagation()}>
{items.map((rule) => (
onUpdate(rule._id, 'targetGroup', v)} style={{ flex: 1 }} /> {rule.op !== OP_REMOVE ? ( onUpdate(rule._id, 'description', v)} style={{ flex: 1 }} /> ) : (
-
)} onRemove(rule._id)} position='left' >
))}
); } export default function GroupSpecialUsableRules({ value, groupNames = [], onChange, }) { const { t } = useTranslation(); const [rules, setRules] = useState(() => flattenRules(parseJSON(value))); const [newGroupName, setNewGroupName] = useState(''); const emitChange = useCallback( (newRules) => { setRules(newRules); onChange?.(serializeGroupSpecialUsable(newRules)); }, [onChange], ); const updateRule = useCallback( (id, field, val) => { emitChange( rules.map((r) => { if (r._id !== id) return r; const updated = { ...r, [field]: val }; if (field === 'op' && val === OP_REMOVE) updated.description = 'remove'; else if (field === 'op' && r.op === OP_REMOVE && val !== OP_REMOVE) { if (updated.description === 'remove') updated.description = ''; } return updated; }), ); }, [rules, emitChange], ); const removeRule = useCallback( (id) => emitChange(rules.filter((r) => r._id !== id)), [rules, emitChange], ); const addRuleToGroup = useCallback( (groupName) => { emitChange([ ...rules, { _id: uid(), userGroup: groupName, op: OP_APPEND, targetGroup: '', description: '' }, ]); }, [rules, emitChange], ); const addNewGroup = useCallback(() => { const name = newGroupName.trim(); if (!name) return; emitChange([ ...rules, { _id: uid(), userGroup: name, op: OP_APPEND, targetGroup: '', description: '' }, ]); setNewGroupName(''); }, [rules, emitChange, newGroupName]); const groupOptions = useMemo( () => groupNames.map((n) => ({ value: n, label: n })), [groupNames], ); const opOptions = useMemo( () => [ { value: OP_ADD, label: t('添加 (+:)') }, { value: OP_REMOVE, label: t('移除 (-:)') }, { value: OP_APPEND, label: t('追加') }, ], [t], ); const grouped = useMemo(() => { const map = {}; const order = []; rules.forEach((r) => { if (!r.userGroup) return; if (!map[r.userGroup]) { map[r.userGroup] = []; order.push(r.userGroup); } map[r.userGroup].push(r); }); return order.map((name) => ({ name, items: map[name] })); }, [rules]); if (grouped.length === 0 && rules.length === 0) { return (
{t('暂无规则,点击下方按钮添加')}
); }