@xfe-repo/web-components 1.6.2 → 1.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/package.json +6 -5
  2. package/skills/xfe-web-components/GUIDE.md +78 -0
  3. package/skills/xfe-web-components/SKILL.md +78 -0
  4. package/skills/xfe-web-components/TEMPLATE.md +170 -0
  5. package/skills/xfe-web-components/references/ApiService.md +136 -0
  6. package/skills/xfe-web-components/references/CInput.md +196 -0
  7. package/skills/xfe-web-components/references/CTable.md +274 -0
  8. package/skills/xfe-web-components/references/CVideo.md +94 -0
  9. package/skills/xfe-web-components/references/Clock.md +53 -0
  10. package/skills/xfe-web-components/references/ConfigProvider.md +198 -0
  11. package/skills/xfe-web-components/references/Countdown.md +109 -0
  12. package/skills/xfe-web-components/references/Counter.md +75 -0
  13. package/skills/xfe-web-components/references/Currency.md +112 -0
  14. package/skills/xfe-web-components/references/EffectAddressCascade.md +110 -0
  15. package/skills/xfe-web-components/references/EffectBrandSelect.md +231 -0
  16. package/skills/xfe-web-components/references/EffectBrandTransfer.md +143 -0
  17. package/skills/xfe-web-components/references/EffectCategoryCascade.md +227 -0
  18. package/skills/xfe-web-components/references/EffectFileUpload.md +348 -0
  19. package/skills/xfe-web-components/references/EffectLabelSelect.md +222 -0
  20. package/skills/xfe-web-components/references/EffectMerchantSelect.md +183 -0
  21. package/skills/xfe-web-components/references/EffectReservoirSelect.md +178 -0
  22. package/skills/xfe-web-components/references/EffectScopeSelect.md +220 -0
  23. package/skills/xfe-web-components/references/EffectSeriesSelect.md +204 -0
  24. package/skills/xfe-web-components/references/EffectSeriesSelectV2.md +154 -0
  25. package/skills/xfe-web-components/references/EffectSkuRecognize.md +149 -0
  26. package/skills/xfe-web-components/references/EffectSkuSelect.md +251 -0
  27. package/skills/xfe-web-components/references/EffectSkuTable.md +184 -0
  28. package/skills/xfe-web-components/references/EffectSpuSelect.md +189 -0
  29. package/skills/xfe-web-components/references/EffectStaffSelect.md +150 -0
  30. package/skills/xfe-web-components/references/EffectWarehouseSelect.md +183 -0
  31. package/skills/xfe-web-components/references/EffectWithFilePanel.md +205 -0
  32. package/skills/xfe-web-components/references/FileUpload.md +126 -0
  33. package/skills/xfe-web-components/references/Iconfont.md +31 -0
  34. package/skills/xfe-web-components/references/Loading.md +68 -0
  35. package/skills/xfe-web-components/references/MultiWindow.md +145 -0
  36. package/skills/xfe-web-components/references/OSSImage.md +230 -0
  37. package/skills/xfe-web-components/references/PrivacyField.md +90 -0
  38. package/skills/xfe-web-components/references/QRCode.md +148 -0
  39. package/skills/xfe-web-components/references/RichTextEditor.md +119 -0
  40. package/skills/xfe-web-components/references/SearchForm.md +270 -0
  41. package/skills/xfe-web-components/references/SearchList.md +128 -0
  42. package/skills/xfe-web-components/references/WithModal.md +328 -0
  43. package/skills/xfe-web-components/references/WithPanel.md +307 -0
  44. package/skills/xfe-web-components/references/commonFn.md +254 -0
@@ -0,0 +1,328 @@
1
+ # WithModal
2
+
3
+ > 高阶弹窗组件,将触发元素和 Modal 弹窗封装为一体,点击触发元素自动打开弹窗,支持异步确认和关闭拦截。
4
+
5
+ ## 导入
6
+
7
+ ```tsx
8
+ import { WithModal } from '@xfe-repo/web-components'
9
+ import type { WithModalProps } from '@xfe-repo/web-components'
10
+ ```
11
+
12
+ ## 基本用法
13
+
14
+ ```tsx
15
+ import { WithModal } from '@xfe-repo/web-components'
16
+ import { Button } from 'antd'
17
+
18
+ function MyPage() {
19
+ return (
20
+ <WithModal
21
+ title="编辑信息"
22
+ handleElement={<Button type="primary">打开弹窗</Button>}
23
+ onOk={async () => {
24
+ await saveData()
25
+ // 返回 void 或 true 则关闭弹窗
26
+ }}
27
+ >
28
+ <p>弹窗内容</p>
29
+ </WithModal>
30
+ )
31
+ }
32
+ ```
33
+
34
+ ### 标准业务模式
35
+
36
+ 业务代码中最常用的模式 — modalProps 解构 + Form 三件套:
37
+
38
+ ```tsx
39
+ import { WithModal } from '@xfe-repo/web-components'
40
+ import { Form, Input, message } from 'antd'
41
+
42
+ function EditUserModal({ children, userId }) {
43
+ const [form] = Form.useForm()
44
+
45
+ const handleSave = async () => {
46
+ const values = await form.validateFields()
47
+ await updateUser(userId, values)
48
+ message.success('保存成功')
49
+ }
50
+
51
+ const handleClose = () => {
52
+ form.resetFields()
53
+ }
54
+
55
+ return (
56
+ <WithModal title="编辑用户" style={{ minWidth: 500 }} handleElement={children} onSave={handleSave} onClose={handleClose}>
57
+ <Form form={form} labelCol={{ span: 5 }}>
58
+ <Form.Item name="name" label="姓名" rules={[{ required: true }]}>
59
+ <Input />
60
+ </Form.Item>
61
+ <Form.Item name="email" label="邮箱">
62
+ <Input />
63
+ </Form.Item>
64
+ </Form>
65
+ </WithModal>
66
+ )
67
+ }
68
+
69
+ // 使用方式:children 作为触发元素
70
+ ;<EditUserModal userId={record.id}>
71
+ <Button type="link">编辑</Button>
72
+ </EditUserModal>
73
+ ```
74
+
75
+ > 💡 **三件套标准流程**:`form.validateFields()` 校验 → API 提交 → `form.resetFields()` 清理。`resetFields` 必须在 `onClose` 中调用,否则再次打开弹窗时会残留上次数据。
76
+
77
+ > 💡 **宽度设置**:88% 使用 `style={{ minWidth: N }}` 而非固定 `width`,常见值为 350/500/640/800/1280。使用 `minWidth` 允许内容撑开弹窗。
78
+
79
+ ## 进阶用法
80
+
81
+ ### 异步保存 + 阻止关闭
82
+
83
+ `onOk`/`onSave` 返回 `false` 可阻止弹窗关闭(如表单校验失败),异步执行期间自动显示 loading。
84
+
85
+ ```tsx
86
+ <WithModal
87
+ title="提交表单"
88
+ handleElement={<Button>提交</Button>}
89
+ onOk={async () => {
90
+ const valid = await form.validateFields().catch(() => false)
91
+ if (!valid) return false // 校验失败,阻止关闭
92
+
93
+ await submitForm()
94
+ message.success('提交成功')
95
+ // 返回 void,自动关闭弹窗
96
+ }}
97
+ onCancel={async () => {
98
+ if (hasUnsavedChanges) {
99
+ const confirmed = await confirmDiscard()
100
+ if (!confirmed) return false // 阻止关闭
101
+ }
102
+ // 返回 void,允许关闭
103
+ }}
104
+ >
105
+ <Form form={form}>...</Form>
106
+ </WithModal>
107
+ ```
108
+
109
+ ### 监听弹窗显隐
110
+
111
+ ```tsx
112
+ <WithModal
113
+ title="详情"
114
+ handleElement={<a>查看详情</a>}
115
+ onVisibleChange={(visible) => {
116
+ if (visible) {
117
+ // 弹窗打开时加载数据
118
+ fetchDetail()
119
+ }
120
+ }}
121
+ >
122
+ <DetailContent />
123
+ </WithModal>
124
+ ```
125
+
126
+ ### 隐藏底部按钮
127
+
128
+ ```tsx
129
+ <WithModal title="预览" handleElement={<Button>预览</Button>} showFooter={false}>
130
+ <ImagePreview />
131
+ </WithModal>
132
+ ```
133
+
134
+ ### 受控模式(外部控制显隐)
135
+
136
+ ```tsx
137
+ const [visible, setVisible] = useState(false)
138
+
139
+ <WithModal
140
+ title="弹窗"
141
+ visible={visible}
142
+ handleElement={<Button onClick={() => setVisible(true)}>打开</Button>}
143
+ onCancel={() => setVisible(false)}
144
+ >
145
+ <Content />
146
+ </WithModal>
147
+ ```
148
+
149
+ ### 触发元素的点击拦截
150
+
151
+ `handleElement` 的原始 `onClick` 若返回 `false`,则不会打开弹窗:
152
+
153
+ ```tsx
154
+ <WithModal
155
+ title="确认"
156
+ handleElement={
157
+ <Button
158
+ onClick={async () => {
159
+ const canOpen = await checkPermission()
160
+ if (!canOpen) return false // 阻止弹窗打开
161
+ }}
162
+ >
163
+ 需要权限
164
+ </Button>
165
+ }
166
+ >
167
+ <Content />
168
+ </WithModal>
169
+ ```
170
+
171
+ ### With{Action}Modal 封装范式
172
+
173
+ 推荐将每个弹窗操作封装为独立组件,统一接口为 `children + 业务props`:
174
+
175
+ ```tsx
176
+ // WithEditModal.tsx
177
+ interface WithEditModalProps {
178
+ children: ReactElement
179
+ recordId: string
180
+ onSuccess?: () => void
181
+ }
182
+
183
+ const WithEditModal = memo(({ children, recordId, onSuccess }: WithEditModalProps) => {
184
+ const [form] = Form.useForm()
185
+ const loading = useSelector(selectUpdateLoading)
186
+
187
+ const handleSave = async () => {
188
+ const values = await form.validateFields()
189
+ await dispatch(effectUpdate({ id: recordId, ...values }))
190
+ onSuccess?.()
191
+ }
192
+
193
+ return (
194
+ <WithModal
195
+ title="编辑"
196
+ handleElement={children}
197
+ onSave={handleSave}
198
+ onClose={() => form.resetFields()}
199
+ loading={loading}
200
+ style={{ minWidth: 500 }}
201
+ >
202
+ <Form form={form} labelCol={{ span: 5 }}>
203
+ {/* 表单内容 */}
204
+ </Form>
205
+ </WithModal>
206
+ )
207
+ })
208
+ ```
209
+
210
+ ### 纯展示弹窗
211
+
212
+ 仅展示数据、无保存操作的弹窗:
213
+
214
+ ```tsx
215
+ <WithModal title="订单详情" handleElement={<Button type="link">查看</Button>} showFooter={false}>
216
+ <Descriptions column={2}>
217
+ <Descriptions.Item label="订单号">{data.orderNo}</Descriptions.Item>
218
+ <Descriptions.Item label="金额">
219
+ <Currency>{data.amount}</Currency>
220
+ </Descriptions.Item>
221
+ </Descriptions>
222
+ </WithModal>
223
+ ```
224
+
225
+ ### 关于 destroyOnClose
226
+
227
+ WithModal 底层使用 antd Modal,默认 **不会** 在关闭时销毁子组件。如果忘记在 `onClose` 中调用 `form.resetFields()`,表单状态会泄漏到下次打开。
228
+
229
+ 建议:始终在 `onClose` 中调用 `form.resetFields()` 进行清理。
230
+
231
+ ## Props
232
+
233
+ ```typescript
234
+ export type WithModalProps = Omit<ModalProps, 'onOk' | 'onCancel' | 'confirmLoading'> & {
235
+ visible?: boolean
236
+ handleElement: ReactElement
237
+ activeClassName?: string
238
+ children?: ReactNode
239
+ loading?: boolean
240
+ showFooter?: boolean
241
+ onSave?: () => Promise<boolean | void> | boolean | void
242
+ onVisibleChange?: (visible?: boolean) => Promise<boolean | void> | boolean | void
243
+ onClose?: () => Promise<boolean | void> | boolean | void
244
+ onOk?: () => Promise<boolean | void> | boolean | void
245
+ onCancel?: () => Promise<boolean | void> | boolean | void
246
+ }
247
+ ```
248
+
249
+ | 属性 | 类型 | 必填 | 默认值 | 说明 |
250
+ | --------------- | --------------------------------------------------- | ---- | ------ | ------------------------------------------------------- |
251
+ | handleElement | `ReactElement` | 是 | - | 触发元素,点击后打开弹窗 |
252
+ | visible | `boolean` | 否 | - | 受控显隐状态(也接受 antd 的 `open`) |
253
+ | showFooter | `boolean` | 否 | `true` | 是否显示底部按钮栏 |
254
+ | loading | `boolean` | 否 | - | 外部控制确认按钮 loading 状态 |
255
+ | activeClassName | `string` | 否 | - | 弹窗打开时附加到 handleElement 的 className |
256
+ | onOk | `() => Promise<boolean \| void> \| boolean \| void` | 否 | - | 点击确认回调,返回 `false` 阻止关闭 |
257
+ | onSave | 同 onOk | 否 | - | `onOk` 的别名,两者都传时 `onOk` 优先 |
258
+ | onCancel | `() => Promise<boolean \| void> \| boolean \| void` | 否 | - | 点击取消回调,返回 `false` 阻止关闭 |
259
+ | onClose | 同 onCancel | 否 | - | `onCancel` 的别名,两者都传时 `onCancel` 优先 |
260
+ | onVisibleChange | `(visible?: boolean) => void` | 否 | - | 弹窗显隐变化回调 |
261
+ | children | `ReactNode` | 否 | - | 弹窗内容 |
262
+ | ...modalProps | `ModalProps` | 否 | - | 其他 antd Modal 属性(title、width、destroyOnClose 等) |
263
+
264
+ ### 回调优先级
265
+
266
+ | 场景 | 优先使用 | 备选 |
267
+ | -------- | ---------- | --------- |
268
+ | 确认操作 | `onOk` | `onSave` |
269
+ | 取消操作 | `onCancel` | `onClose` |
270
+
271
+ ## 常见陷阱
272
+
273
+ - ❌ 同时使用 `visible` 受控模式和依赖 handleElement 的内部状态管理:
274
+ ```tsx
275
+ // visible 外部状态和内部 show 状态冲突,弹窗行为不可预测
276
+ <WithModal visible={myVisible} handleElement={<Button>打开</Button>}>
277
+ ```
278
+ - ✅ 受控模式下在 `onCancel` 中同步更新外部状态:
279
+
280
+ ```tsx
281
+ <WithModal
282
+ visible={myVisible}
283
+ handleElement={<Button onClick={() => setMyVisible(true)}>打开</Button>}
284
+ onCancel={() => setMyVisible(false)}
285
+ >
286
+ ```
287
+
288
+ - ❌ `onOk` 回调中抛出异常但不处理 —— 组件会 catch 异常并阻止关闭(当作返回 `false`):
289
+ ```tsx
290
+ onOk={async () => {
291
+ await api.save() // 如果抛出异常,弹窗不会关闭
292
+ }}
293
+ ```
294
+ - ✅ 在回调中自行处理错误:
295
+
296
+ ```tsx
297
+ onOk={async () => {
298
+ try {
299
+ await api.save()
300
+ } catch (e) {
301
+ message.error('保存失败')
302
+ return false // 显式阻止关闭
303
+ }
304
+ }}
305
+ ```
306
+
307
+ - ❌ 关闭弹窗后不清理表单状态,再次打开时残留旧数据:
308
+ ```tsx
309
+ <WithModal onClose={() => { /* 什么都不做 */ }}>
310
+ ```
311
+ - ✅ 在 onClose 中调用 resetFields 清理:
312
+
313
+ ```tsx
314
+ <WithModal onClose={() => form.resetFields()}>
315
+ ```
316
+
317
+ - ❌ 把 `confirmLoading` 传给 WithModal —— 该属性已被 Omit,不生效
318
+ - ✅ 使用 `loading` 属性或让 onOk 的 async 自动管理 loading 状态
319
+
320
+ - 💡 复杂表单弹窗建议传 `maskClosable={false}`,防止用户误点遮罩关闭导致数据丢失。
321
+
322
+ ## 相关组件
323
+
324
+ | 场景 | 组件 | 说明 |
325
+ | -------------- | ----------------------- | ------------------------ |
326
+ | 弹窗内上传 | `EffectFileUpload` | 在弹窗中使用文件上传 |
327
+ | 弹窗内表单 | `EffectCategoryCascade` | 在弹窗中使用分类选择 |
328
+ | 弹窗内范围选择 | `EffectScopeSelect` | 在弹窗中使用商品范围选择 |
@@ -0,0 +1,307 @@
1
+ # WithPanel
2
+
3
+ > 高阶侧边栏组件,将触发元素和 Drawer 抽屉封装为一体,点击触发元素自动打开面板,支持异步确认和关闭拦截。与 WithModal 设计对称。
4
+
5
+ ## 导入
6
+
7
+ ```tsx
8
+ import { WithPanel } from '@xfe-repo/web-components'
9
+ import type { WithPanelProps } from '@xfe-repo/web-components'
10
+ ```
11
+
12
+ ## 基本用法
13
+
14
+ ```tsx
15
+ import { WithPanel } from '@xfe-repo/web-components'
16
+ import { Button } from 'antd'
17
+
18
+ function MyPage() {
19
+ return (
20
+ <WithPanel
21
+ title="编辑信息"
22
+ handleElement={<Button type="primary">打开面板</Button>}
23
+ onOk={async () => {
24
+ await saveData()
25
+ // 返回 void 或 true 则关闭面板
26
+ }}
27
+ >
28
+ <p>面板内容</p>
29
+ </WithPanel>
30
+ )
31
+ }
32
+ ```
33
+
34
+ ### 选择面板模式
35
+
36
+ 最常见的使用模式 — 构建一个带搜索和多选的数据选择面板:
37
+
38
+ ```tsx
39
+ import { WithPanel } from '@xfe-repo/web-components'
40
+ import { CTable } from '@xfe-repo/web-components'
41
+ import { Form, Input, Button } from 'antd'
42
+
43
+ function SelectProductPanel({ children, onSelect }) {
44
+ const [form] = Form.useForm()
45
+ const [selectedRows, setSelectedRows] = useState([])
46
+
47
+ const rowSelection = {
48
+ selectedRowKeys: selectedRows.map((r) => r.id),
49
+ onChange: (_, rows) => setSelectedRows(rows),
50
+ }
51
+
52
+ const handleConfirm = () => {
53
+ onSelect(selectedRows)
54
+ }
55
+
56
+ return (
57
+ <WithPanel title="选择商品" width="70%" handleElement={children} onSave={handleConfirm}>
58
+ <Form form={form} layout="inline" onFinish={handleSearch}>
59
+ <Form.Item name="keyword">
60
+ <Input placeholder="搜索商品" />
61
+ </Form.Item>
62
+ <Button htmlType="submit" type="primary">
63
+ 查询
64
+ </Button>
65
+ </Form>
66
+ <CTable dataSource={products} rowSelection={rowSelection} rowKey="id" columns={columns} pagination={false} />
67
+ </WithPanel>
68
+ )
69
+ }
70
+
71
+ // 使用
72
+ ;<SelectProductPanel onSelect={handleProductSelect}>
73
+ <Button>选择商品</Button>
74
+ </SelectProductPanel>
75
+ ```
76
+
77
+ ## 进阶用法
78
+
79
+ ### 异步保存 + 阻止关闭
80
+
81
+ `onOk`/`onSave` 返回 `false` 可阻止面板关闭(如表单校验失败),异步执行期间自动显示 loading。
82
+
83
+ ```tsx
84
+ <WithPanel
85
+ title="提交表单"
86
+ handleElement={<Button>提交</Button>}
87
+ onOk={async () => {
88
+ const valid = await form.validateFields().catch(() => false)
89
+ if (!valid) return false // 校验失败,阻止关闭
90
+
91
+ await submitForm()
92
+ message.success('提交成功')
93
+ // 返回 void,自动关闭面板
94
+ }}
95
+ onCancel={async () => {
96
+ if (hasUnsavedChanges) {
97
+ const confirmed = await confirmDiscard()
98
+ if (!confirmed) return false // 阻止关闭
99
+ }
100
+ // 返回 void,允许关闭
101
+ }}
102
+ >
103
+ <Form form={form}>...</Form>
104
+ </WithPanel>
105
+ ```
106
+
107
+ ### 隐藏底部按钮
108
+
109
+ ```tsx
110
+ <WithPanel title="预览" handleElement={<Button>预览</Button>} showFooter={false}>
111
+ <ImagePreview />
112
+ </WithPanel>
113
+ ```
114
+
115
+ ### 受控模式(外部控制显隐)
116
+
117
+ ```tsx
118
+ const [visible, setVisible] = useState(false)
119
+
120
+ <WithPanel
121
+ title="面板"
122
+ open={visible}
123
+ handleElement={<Button onClick={() => setVisible(true)}>打开</Button>}
124
+ onCancel={() => setVisible(false)}
125
+ >
126
+ <Content />
127
+ </WithPanel>
128
+ ```
129
+
130
+ ### 触发元素的点击拦截
131
+
132
+ `handleElement` 的原始 `onClick` 若返回 `false`,则不会打开面板:
133
+
134
+ ```tsx
135
+ <WithPanel
136
+ title="确认"
137
+ handleElement={
138
+ <Button
139
+ onClick={async () => {
140
+ const canOpen = await checkPermission()
141
+ if (!canOpen) return false // 阻止面板打开
142
+ }}
143
+ >
144
+ 需要权限
145
+ </Button>
146
+ }
147
+ >
148
+ <Content />
149
+ </WithPanel>
150
+ ```
151
+
152
+ ### 受控模式(cloneElement 注入)
153
+
154
+ 通过 cloneElement 将 onClick 注入到触发元素:
155
+
156
+ ```tsx
157
+ const [visible, setVisible] = useState(false)
158
+
159
+ <WithPanel
160
+ title="选择商品"
161
+ open={visible}
162
+ onClose={() => setVisible(false)}
163
+ handleElement={React.cloneElement(children, { onClick: () => setVisible(true) })}
164
+ >
165
+ {/* 面板内容 */}
166
+ </WithPanel>
167
+ ```
168
+
169
+ ### 旧版 API 与新版 API
170
+
171
+ 组件同时兼容旧版(`visible`/`onSave`/`onClose`/`submitText`)和新版(`open`/`onOk`/`onCancel`/`okText`)API。使用旧版 API 时,`width` 默认 `'50%'`,`maskClosable` 默认 `false`;使用新版 API 时,`width` 默认 `'100%'`,`maskClosable` 使用 Drawer 默认值。
172
+
173
+ ```tsx
174
+ // 旧版 API(兼容保留)
175
+ <WithPanel
176
+ visible={show}
177
+ submitText="保存"
178
+ handleElement={<Button>打开</Button>}
179
+ onSave={handleSave}
180
+ onClose={handleClose}
181
+ >
182
+ ...
183
+ </WithPanel>
184
+
185
+ // 新版 API(推荐)
186
+ <WithPanel
187
+ open={show}
188
+ okText="保存"
189
+ handleElement={<Button>打开</Button>}
190
+ onOk={handleSave}
191
+ onCancel={handleClose}
192
+ >
193
+ ...
194
+ </WithPanel>
195
+ ```
196
+
197
+ ## Props
198
+
199
+ ```typescript
200
+ export type WithPanelProps = Omit<DrawerProps, 'footer' | 'onClose'> & {
201
+ visible?: boolean
202
+ handleElement: ReactElement
203
+ activeClassName?: string
204
+ children?: ReactNode
205
+ submitText?: string
206
+ okText?: string
207
+ cancelText?: string
208
+ loading?: boolean
209
+ showFooter?: boolean
210
+ onSave?: () => Promise<boolean | void> | boolean | void
211
+ onClose?: () => Promise<boolean | void> | boolean | void
212
+ onOk?: () => Promise<boolean | void> | boolean | void
213
+ onCancel?: () => Promise<boolean | void> | boolean | void
214
+ }
215
+ ```
216
+
217
+ | 属性 | 类型 | 必填 | 默认值 | 说明 |
218
+ | --------------- | --------------------------------------------------- | ---- | -------------------------------- | ------------------------------------------------------------ |
219
+ | handleElement | `ReactElement` | 是 | - | 触发元素,点击后打开面板 |
220
+ | visible | `boolean` | 否 | - | 受控显隐状态(旧版 API,也可使用 `open`) |
221
+ | open | `boolean` | 否 | - | 受控显隐状态(新版 API,优先级高于 `visible`) |
222
+ | showFooter | `boolean` | 否 | `true` | 是否显示底部按钮栏 |
223
+ | loading | `boolean` | 否 | - | 外部控制确认按钮 loading 状态 |
224
+ | activeClassName | `string` | 否 | - | 面板打开时附加到 handleElement 的 className |
225
+ | okText | `string` | 否 | `'确定'` | 确认按钮文本(优先于 submitText) |
226
+ | submitText | `string` | 否 | - | 确认按钮文本(旧版 API,被 okText 覆盖) |
227
+ | cancelText | `string` | 否 | `'取消'` | 取消按钮文本 |
228
+ | width | `string \| number` | 否 | 旧版 `'50%'`,新版 `'100%'` | 面板宽度 |
229
+ | maskClosable | `boolean` | 否 | 旧版 `false`,新版 Drawer 默认值 | 点击遮罩是否关闭 |
230
+ | onOk | `() => Promise<boolean \| void> \| boolean \| void` | 否 | - | 点击确认回调,返回 `false` 阻止关闭 |
231
+ | onSave | 同 onOk | 否 | - | `onOk` 的别名,两者都传时 `onOk` 优先 |
232
+ | onCancel | `() => Promise<boolean \| void> \| boolean \| void` | 否 | - | 点击取消回调,返回 `false` 阻止关闭 |
233
+ | onClose | 同 onCancel | 否 | - | `onCancel` 的别名,两者都传时 `onCancel` 优先 |
234
+ | children | `ReactNode` | 否 | - | 面板内容 |
235
+ | ...drawerProps | `DrawerProps` | 否 | - | 其他 antd Drawer 属性(title、placement、destroyOnClose 等) |
236
+
237
+ ### 回调优先级
238
+
239
+ | 场景 | 优先使用 | 备选 |
240
+ | -------- | ---------- | ------------ |
241
+ | 确认操作 | `onOk` | `onSave` |
242
+ | 取消操作 | `onCancel` | `onClose` |
243
+ | 显隐控制 | `open` | `visible` |
244
+ | 按钮文本 | `okText` | `submitText` |
245
+
246
+ ### 旧版 vs 新版默认值
247
+
248
+ 当检测到使用了旧版 API(`onSave`、`onClose`、`visible` 或 `submitText` 中任一)时:
249
+
250
+ | 属性 | 旧版默认值 | 新版默认值 |
251
+ | ------------ | ---------- | ------------- |
252
+ | width | `'50%'` | `'100%'` |
253
+ | maskClosable | `false` | Drawer 默认值 |
254
+
255
+ ## 常见陷阱
256
+
257
+ - ❌ 同时使用 `visible` 受控模式和依赖 handleElement 的内部状态管理:
258
+ ```tsx
259
+ // visible/open 外部状态和内部 show 状态冲突,面板行为不可预测
260
+ <WithPanel visible={myVisible} handleElement={<Button>打开</Button>}>
261
+ ```
262
+ - ✅ 受控模式下在 `onCancel` 中同步更新外部状态:
263
+
264
+ ```tsx
265
+ <WithPanel
266
+ open={myVisible}
267
+ handleElement={<Button onClick={() => setMyVisible(true)}>打开</Button>}
268
+ onCancel={() => setMyVisible(false)}
269
+ >
270
+ ```
271
+
272
+ - ❌ `onOk` 回调中抛出异常但不处理——组件会 catch 异常并阻止关闭(当作返回 `false`):
273
+ ```tsx
274
+ onOk={async () => {
275
+ await api.save() // 如果抛出异常,面板不会关闭
276
+ }}
277
+ ```
278
+ - ✅ 在回调中自行处理错误:
279
+
280
+ ```tsx
281
+ onOk={async () => {
282
+ try {
283
+ await api.save()
284
+ } catch (e) {
285
+ message.error('保存失败')
286
+ return false // 显式阻止关闭
287
+ }
288
+ }}
289
+ ```
290
+
291
+ - ❌ 不传 `onOk`/`onSave` 时期望底部出现确认按钮——确认按钮仅在 `mergedOnOk`(即 `onOk` 或 `onSave`)存在时渲染
292
+ - ✅ 如果只需取消按钮,不传 `onOk` 即可;如果不需要任何底部按钮,使用 `showFooter={false}`
293
+
294
+ - ❌ 混用旧版 API 和新版 API 导致默认值不一致
295
+ - ✅ 统一使用新版 API(`open`/`onOk`/`onCancel`/`okText`)
296
+
297
+ > 💡 可以将 props 组装为对象后展开传入:`const panelProps = { title, width, onSave }; <WithPanel {...panelProps}>`
298
+
299
+ > 💡 常见 width 值:`500`、`800`(px)或 `"60%"`、`"70%"`(百分比)。
300
+
301
+ ## 相关组件
302
+
303
+ | 场景 | 组件 | 说明 |
304
+ | ---------- | ----------------------- | ------------------------------------ |
305
+ | 弹窗版本 | `WithModal` | 相同设计模式,使用 Modal 而非 Drawer |
306
+ | 面板内上传 | `EffectFileUpload` | 在面板中使用文件上传 |
307
+ | 面板内表单 | `EffectCategoryCascade` | 在面板中使用分类选择 |