11import React , { useState , useCallback , useMemo } from 'react' ;
22import {
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' ;
1018import { useTranslation } from 'react-i18next' ;
11- import CardTable from '../../../../components/common/ui/CardTable' ;
1219
1320const { 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+
60159export 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