Skip to content

Commit 78e4cb3

Browse files
committed
feat(web): redesign group ratio rules with collapsible grouped layout
Rewrite GroupGroupRatioRules and GroupSpecialUsableRules to group rules by user group in collapsible sections instead of a flat table. Default collapsed to reduce visual clutter when many rules exist. Fix i18n translations for ja, zh-TW with proper native text; add missing keys.
1 parent c734db3 commit 78e4cb3

File tree

8 files changed

+462
-1141
lines changed

8 files changed

+462
-1141
lines changed

web/src/i18n/locales/en.json

Lines changed: 9 additions & 121 deletions
Large diffs are not rendered by default.

web/src/i18n/locales/fr.json

Lines changed: 9 additions & 138 deletions
Large diffs are not rendered by default.

web/src/i18n/locales/ja.json

Lines changed: 10 additions & 139 deletions
Large diffs are not rendered by default.

web/src/i18n/locales/ru.json

Lines changed: 9 additions & 138 deletions
Large diffs are not rendered by default.

web/src/i18n/locales/vi.json

Lines changed: 9 additions & 137 deletions
Large diffs are not rendered by default.

web/src/i18n/locales/zh-TW.json

Lines changed: 9 additions & 217 deletions
Large diffs are not rendered by default.

web/src/pages/Setting/Ratio/components/GroupGroupRatioRules.jsx

Lines changed: 182 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,21 @@
11
import React, { useState, useCallback, useMemo } from 'react';
22
import {
33
Button,
4+
Collapsible,
5+
Input,
46
InputNumber,
57
Select,
8+
Tag,
69
Typography,
710
Popconfirm,
811
} from '@douyinfe/semi-ui';
9-
import { IconPlus, IconDelete } from '@douyinfe/semi-icons';
12+
import {
13+
IconPlus,
14+
IconDelete,
15+
IconChevronDown,
16+
IconChevronUp,
17+
} from '@douyinfe/semi-icons';
1018
import { useTranslation } from 'react-i18next';
11-
import CardTable from '../../../../components/common/ui/CardTable';
1219

1320
const { Text } = Typography;
1421

@@ -57,14 +64,106 @@ export function serializeGroupGroupRatio(rules) {
5764
: JSON.stringify(nested, null, 2);
5865
}
5966

67+
function GroupSection({ groupName, items, groupOptions, onUpdate, onRemove, onAdd, t }) {
68+
const [open, setOpen] = useState(false);
69+
70+
return (
71+
<div
72+
style={{
73+
border: '1px solid var(--semi-color-border)',
74+
borderRadius: 8,
75+
overflow: 'hidden',
76+
}}
77+
>
78+
<div
79+
className='flex items-center justify-between cursor-pointer'
80+
style={{
81+
padding: '8px 12px',
82+
background: 'var(--semi-color-fill-0)',
83+
}}
84+
onClick={() => setOpen(!open)}
85+
>
86+
<div className='flex items-center gap-2'>
87+
{open ? <IconChevronUp size='small' /> : <IconChevronDown size='small' />}
88+
<Text strong>{groupName}</Text>
89+
<Tag size='small' color='blue'>{items.length} {t('条规则')}</Tag>
90+
</div>
91+
<div className='flex items-center gap-1' onClick={(e) => e.stopPropagation()}>
92+
<Button
93+
icon={<IconPlus />}
94+
size='small'
95+
theme='borderless'
96+
onClick={() => onAdd(groupName)}
97+
/>
98+
<Popconfirm
99+
title={t('确认删除该分组的所有规则?')}
100+
onConfirm={() => items.forEach((item) => onRemove(item._id))}
101+
position='left'
102+
>
103+
<Button
104+
icon={<IconDelete />}
105+
size='small'
106+
type='danger'
107+
theme='borderless'
108+
/>
109+
</Popconfirm>
110+
</div>
111+
</div>
112+
<Collapsible isOpen={open} keepDOM>
113+
<div style={{ padding: '8px 12px' }}>
114+
{items.map((rule) => (
115+
<div
116+
key={rule._id}
117+
className='flex items-center gap-2'
118+
style={{ marginBottom: 6 }}
119+
>
120+
<Select
121+
size='small'
122+
filter
123+
value={rule.usingGroup || undefined}
124+
placeholder={t('选择使用分组')}
125+
optionList={groupOptions}
126+
onChange={(v) => onUpdate(rule._id, 'usingGroup', v)}
127+
style={{ flex: 1 }}
128+
allowCreate
129+
position='bottomLeft'
130+
/>
131+
<InputNumber
132+
size='small'
133+
min={0}
134+
step={0.1}
135+
value={rule.ratio}
136+
style={{ width: 100 }}
137+
onChange={(v) => onUpdate(rule._id, 'ratio', v ?? 0)}
138+
/>
139+
<Popconfirm
140+
title={t('确认删除该规则?')}
141+
onConfirm={() => onRemove(rule._id)}
142+
position='left'
143+
>
144+
<Button
145+
icon={<IconDelete />}
146+
type='danger'
147+
theme='borderless'
148+
size='small'
149+
/>
150+
</Popconfirm>
151+
</div>
152+
))}
153+
</div>
154+
</Collapsible>
155+
</div>
156+
);
157+
}
158+
60159
export default function GroupGroupRatioRules({
61160
value,
62161
groupNames = [],
63162
onChange,
64163
}) {
65164
const { t } = useTranslation();
66-
67165
const [rules, setRules] = useState(() => flattenRules(parseJSON(value)));
166+
const [newGroupName, setNewGroupName] = useState('');
68167

69168
const emitChange = useCallback(
70169
(newRules) => {
@@ -76,129 +175,111 @@ export default function GroupGroupRatioRules({
76175

77176
const updateRule = useCallback(
78177
(id, field, val) => {
79-
const next = rules.map((r) =>
80-
r._id === id ? { ...r, [field]: val } : r,
81-
);
82-
emitChange(next);
178+
emitChange(rules.map((r) => (r._id === id ? { ...r, [field]: val } : r)));
83179
},
84180
[rules, emitChange],
85181
);
86182

87-
const addRule = useCallback(() => {
88-
emitChange([
89-
...rules,
90-
{ _id: uid(), userGroup: '', usingGroup: '', ratio: 1 },
91-
]);
92-
}, [rules, emitChange]);
93-
94183
const removeRule = useCallback(
95184
(id) => {
96185
emitChange(rules.filter((r) => r._id !== id));
97186
},
98187
[rules, emitChange],
99188
);
100189

190+
const addRuleToGroup = useCallback(
191+
(groupName) => {
192+
emitChange([
193+
...rules,
194+
{ _id: uid(), userGroup: groupName, usingGroup: '', ratio: 1 },
195+
]);
196+
},
197+
[rules, emitChange],
198+
);
199+
200+
const addNewGroup = useCallback(() => {
201+
const name = newGroupName.trim();
202+
if (!name) return;
203+
emitChange([
204+
...rules,
205+
{ _id: uid(), userGroup: name, usingGroup: '', ratio: 1 },
206+
]);
207+
setNewGroupName('');
208+
}, [rules, emitChange, newGroupName]);
209+
101210
const groupOptions = useMemo(
102211
() => groupNames.map((n) => ({ value: n, label: n })),
103212
[groupNames],
104213
);
105214

106-
const columns = useMemo(
107-
() => [
108-
{
109-
title: t('用户分组'),
110-
dataIndex: 'userGroup',
111-
key: 'userGroup',
112-
width: 200,
113-
render: (_, record) => (
215+
const grouped = useMemo(() => {
216+
const map = {};
217+
const order = [];
218+
rules.forEach((r) => {
219+
if (!r.userGroup) return;
220+
if (!map[r.userGroup]) {
221+
map[r.userGroup] = [];
222+
order.push(r.userGroup);
223+
}
224+
map[r.userGroup].push(r);
225+
});
226+
return order.map((name) => ({ name, items: map[name] }));
227+
}, [rules]);
228+
229+
if (grouped.length === 0 && rules.length === 0) {
230+
return (
231+
<div>
232+
<Text type='tertiary' className='block text-center py-4'>
233+
{t('暂无规则,点击下方按钮添加')}
234+
</Text>
235+
<div className='mt-2 flex justify-center gap-2'>
114236
<Select
115237
size='small'
116238
filter
117-
value={record.userGroup || undefined}
118-
placeholder={t('选择用户分组')}
119-
optionList={groupOptions}
120-
onChange={(v) => updateRule(record._id, 'userGroup', v)}
121-
style={{ width: '100%' }}
122239
allowCreate
123-
position='bottomLeft'
124-
/>
125-
),
126-
},
127-
{
128-
title: t('使用分组'),
129-
dataIndex: 'usingGroup',
130-
key: 'usingGroup',
131-
width: 200,
132-
render: (_, record) => (
133-
<Select
134-
size='small'
135-
filter
136-
value={record.usingGroup || undefined}
137-
placeholder={t('选择使用分组')}
240+
placeholder={t('选择用户分组')}
138241
optionList={groupOptions}
139-
onChange={(v) => updateRule(record._id, 'usingGroup', v)}
140-
style={{ width: '100%' }}
141-
allowCreate
242+
value={newGroupName || undefined}
243+
onChange={setNewGroupName}
244+
style={{ width: 200 }}
142245
position='bottomLeft'
143246
/>
144-
),
145-
},
146-
{
147-
title: t('倍率'),
148-
dataIndex: 'ratio',
149-
key: 'ratio',
150-
width: 140,
151-
render: (_, record) => (
152-
<InputNumber
153-
size='small'
154-
min={0}
155-
step={0.1}
156-
value={record.ratio}
157-
style={{ width: '100%' }}
158-
onChange={(v) => updateRule(record._id, 'ratio', v ?? 0)}
159-
/>
160-
),
161-
},
162-
{
163-
title: '',
164-
key: 'actions',
165-
width: 50,
166-
render: (_, record) => (
167-
<Popconfirm
168-
title={t('确认删除该规则?')}
169-
onConfirm={() => removeRule(record._id)}
170-
position='left'
171-
>
172-
<Button
173-
icon={<IconDelete />}
174-
type='danger'
175-
theme='borderless'
176-
size='small'
177-
/>
178-
</Popconfirm>
179-
),
180-
},
181-
],
182-
[t, groupOptions, updateRule, removeRule],
183-
);
247+
<Button icon={<IconPlus />} theme='outline' onClick={addNewGroup}>
248+
{t('添加分组规则')}
249+
</Button>
250+
</div>
251+
</div>
252+
);
253+
}
184254

185255
return (
186-
<div>
187-
<CardTable
188-
columns={columns}
189-
dataSource={rules}
190-
rowKey='_id'
191-
hidePagination
192-
size='small'
193-
empty={
194-
<Text type='tertiary'>
195-
{t('暂无规则,点击下方按钮添加')}
196-
</Text>
197-
}
198-
/>
199-
<div className='mt-3 flex justify-center'>
200-
<Button icon={<IconPlus />} theme='outline' onClick={addRule}>
201-
{t('添加规则')}
256+
<div className='space-y-2'>
257+
{grouped.map((group) => (
258+
<GroupSection
259+
key={group.name}
260+
groupName={group.name}
261+
items={group.items}
262+
groupOptions={groupOptions}
263+
onUpdate={updateRule}
264+
onRemove={removeRule}
265+
onAdd={addRuleToGroup}
266+
t={t}
267+
/>
268+
))}
269+
<div className='mt-3 flex justify-center gap-2'>
270+
<Select
271+
size='small'
272+
filter
273+
allowCreate
274+
placeholder={t('选择用户分组')}
275+
optionList={groupOptions}
276+
value={newGroupName || undefined}
277+
onChange={setNewGroupName}
278+
style={{ width: 200 }}
279+
position='bottomLeft'
280+
/>
281+
<Button icon={<IconPlus />} theme='outline' onClick={addNewGroup}>
282+
{t('添加分组规则')}
202283
</Button>
203284
</div>
204285
</div>

0 commit comments

Comments
 (0)