@kne/react-modal 0.1.1 → 0.1.4

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.
package/README.md CHANGED
@@ -2,11 +2,11 @@
2
2
 
3
3
  ### 描述
4
4
 
5
- 基于 antd 的 React 弹窗组件,提供 Modal/useModal、Tabs 分栏布局与 renderModal 适配,支持 SimpleBar 滚动与移动端全屏。
5
+ 基于 antd 的 React 弹层组件,提供 Modal/Drawer、useModal/useDrawer/useConfirmModal、共用 TabsLayout/ScrollRegion 布局与 createModalRender/createDrawerRender,支持 SimpleBar 与移动端适配。
6
6
 
7
7
  ### 关键词
8
8
 
9
- react, modal, antd, dialog, simplebar, use-modal, footer-buttons, tabs-layout, mobile, form, component, i18n
9
+ react, modal, drawer, antd, dialog, simplebar, use-modal, use-drawer, use-confirm-modal, footer-buttons, tabs-layout, mobile, form, component, i18n
10
10
 
11
11
  ### 安装
12
12
 
@@ -18,20 +18,21 @@ npm i --save @kne/react-modal
18
18
 
19
19
  #### 概述
20
20
 
21
- `@kne/react-modal` 是基于 Ant Design Modal 的精简弹窗组件,保留声明式 `Modal` 与命令式 `useModal`,兼容 components-core Modal 常用调用(含 `footerButtons` 系统),并内置 SimpleBar 内容滚动与移动端全屏适配。
21
+ `@kne/react-modal` 是基于 Ant Design 的弹层组件库,提供声明式 / 命令式 **Modal** 与 **Drawer**,兼容 `footerButtons` 体系,内置 SimpleBar 滚动与移动端适配。
22
22
 
23
23
  #### 主要特性
24
24
 
25
- - 声明式 / 命令式同一套 props 与 UI
26
- - `footer` + `footerButtons`(`ButtonComponent` / `display` / `autoClose`)与既有用法对齐
25
+ - 声明式 / 命令式同一套 props 与 UI(Modal `useModal`;Drawer `useDrawer` + `DrawerContextHolder`;确认框 `useConfirmModal`)
26
+ - `footer` + `footerButtons`(`ButtonComponent` / `display` / `autoClose`)
27
27
  - title / footer 固定在滚动外,body 默认 SimpleBar
28
- - `--kne-modal-*` CSS 变量管理 body / content 高度与 padding
29
- - 移动端全屏(`@kne/responsive-utils`)与嵌套挂载处理
30
- - 布局组合:`ModalTabsLayout`、`ModalColumnsLayout`、`ModalScrollRegion`;`createModalRender` 对接 `renderModal` 宿主
28
+ - `--kne-modal-*` / `--kne-drawer-*` CSS 变量管理高度链
29
+ - Modal 移动端全屏;Drawer 移动端侧滑全宽
30
+ - **示例**:各场景示例顶部提供 **Modal / Drawer** 切换,同一套 props 与内容对比两种弹层
31
+ - **共用布局**:`TabsLayout`、`ColumnsLayout`、`ScrollRegion`;`createModalRender` / `createDrawerRender` 对接 renderModal 宿主
31
32
 
32
33
  #### 使用场景
33
34
 
34
- 需要在不打断当前流程的前提下打开浮层处理事务、表单确认或展示长内容时使用。Tabs 顶栏、N 列分栏、分步表单等可通过布局组件与 `createModalRender` 组合,无需业务侧手写 SCSS。
35
+ 列表快览、表单确认、侧滑详情、Tabs / 分栏复杂内容区均可通过布局组件组合,无需业务侧手写 SCSS。
35
36
 
36
37
 
37
38
  ### 示例
@@ -225,65 +226,99 @@ npm i --save @kne/react-modal
225
226
 
226
227
  #### 示例代码
227
228
 
228
- - 基础弹窗
229
- - 受控打开与异步 onConfirm(loading / 成功提示)
229
+ - 基础弹层
230
+ - Modal / Drawer 切换:受控打开与异步 onConfirm(loading / 成功提示)
230
231
  - _ReactModal(@kne/current-lib_react-modal)[import * as _ReactModal from "@kne/react-modal"],(@kne/current-lib_react-modal/dist/index.css),antd(antd)
231
232
 
232
233
  ```jsx
233
- const { default: Modal } = _ReactModal;
234
- const { Button, Space, message, App, Typography } = antd;
235
- const { useState } = React;
234
+ const { default: Modal, Drawer, DrawerContextHolder } = _ReactModal;
235
+ const { Button, Space, message, Typography, Radio, App } = antd;
236
+ const { useState, useEffect } = React;
236
237
 
237
238
  const { Text, Paragraph } = Typography;
238
239
 
239
240
  const BasicExample = () => {
240
241
  const [open, setOpen] = useState(false);
242
+ const [mode, setMode] = useState('modal');
243
+ const isDrawer = mode === 'drawer';
244
+
245
+ useEffect(() => {
246
+ if (open) {
247
+ setOpen(false);
248
+ }
249
+ }, [mode]);
250
+
251
+ const overlayProps = {
252
+ title: '保存评估备注',
253
+ open,
254
+ onClose: () => setOpen(false),
255
+ onConfirm: async () => {
256
+ await new Promise(resolve => setTimeout(resolve, 800));
257
+ message.success('备注已保存至候选人档案');
258
+ },
259
+ confirmText: '保存'
260
+ };
261
+
262
+ const content = (
263
+ <>
264
+ <Paragraph style={{ marginBottom: 8 }}>
265
+ 将把当前页面的筛选条件与评估摘要一并写入 <Text strong>陈思远</Text> 的档案备注。
266
+ </Paragraph>
267
+ <Paragraph type="secondary" style={{ marginBottom: 0 }}>
268
+ 保存后可在「候选人详情 → 操作记录」中查看历史版本。
269
+ </Paragraph>
270
+ </>
271
+ );
241
272
 
242
273
  return (
243
274
  <Space direction="vertical" style={{ width: '100%' }} size="middle">
275
+ <Space align="center" wrap>
276
+ <Text type="secondary">打开方式</Text>
277
+ <Radio.Group
278
+ value={mode}
279
+ optionType="button"
280
+ size="small"
281
+ options={[
282
+ { label: 'Modal', value: 'modal' },
283
+ { label: 'Drawer', value: 'drawer' }
284
+ ]}
285
+ onChange={e => setMode(e.target.value)}
286
+ />
287
+ </Space>
244
288
  <Button type="primary" onClick={() => setOpen(true)}>
245
289
  保存评估备注
246
290
  </Button>
247
- <Text type="secondary">最简受控弹窗:异步 onConfirm 带 loading,返回 false 可阻止关闭。</Text>
248
- <Modal
249
- title="保存评估备注"
250
- open={open}
251
- onClose={() => setOpen(false)}
252
- onConfirm={async () => {
253
- await new Promise(resolve => setTimeout(resolve, 800));
254
- message.success('备注已保存至候选人档案');
255
- }}
256
- confirmText="保存"
257
- >
258
- <Paragraph style={{ marginBottom: 8 }}>
259
- 将把当前页面的筛选条件与评估摘要一并写入 <Text strong>陈思远</Text> 的档案备注。
260
- </Paragraph>
261
- <Paragraph type="secondary" style={{ marginBottom: 0 }}>
262
- 保存后可在「候选人详情 → 操作记录」中查看历史版本。
263
- </Paragraph>
264
- </Modal>
291
+ <Text type="secondary">
292
+ 最简受控弹层:切换 Modal / Drawer 对比同一套 props;异步 onConfirm 带 loading。
293
+ </Text>
294
+ {isDrawer ? (
295
+ <Drawer {...overlayProps} size="default">
296
+ {content}
297
+ </Drawer>
298
+ ) : (
299
+ <Modal {...overlayProps}>{content}</Modal>
300
+ )}
265
301
  </Space>
266
302
  );
267
303
  };
268
304
 
269
- const BaseExample = () => (
305
+ render(
270
306
  <App>
307
+ <DrawerContextHolder />
271
308
  <BasicExample />
272
309
  </App>
273
310
  );
274
311
 
275
- render(<BaseExample />);
276
-
277
312
  ```
278
313
 
279
314
  - footerButtons 与尺寸
280
- - 岗位发布确认:size / footerButtons / 左侧 footer 插槽 / noPadding 预览
281
- - _ReactModal(@kne/current-lib_react-modal)[import * as _ReactModal from "@kne/react-modal"],(@kne/current-lib_react-modal/dist/index.css),antd(antd)
315
+ - Modal / Drawer 切换:size / footerButtons / 左侧 footer 插槽 / noPadding 预览
316
+ - _ReactModal(@kne/current-lib_react-modal)[import * as _ReactModal from "@kne/react-modal"],(@kne/current-lib_react-modal/dist/index.css),(@kne/current-lib_react-modal/doc/style.scss),antd(antd)
282
317
 
283
318
  ```jsx
284
- const { default: Modal } = _ReactModal;
285
- const { Button, Space, Radio, Tag, message, App, Typography } = antd;
286
- const { useState } = React;
319
+ const { default: Modal, Drawer, DrawerContextHolder } = _ReactModal;
320
+ const { Button, Space, Radio, Tag, message, Typography, App } = antd;
321
+ const { useState, useEffect } = React;
287
322
 
288
323
  const { Text, Title, Paragraph } = Typography;
289
324
 
@@ -291,11 +326,85 @@ const FooterButtonsExample = () => {
291
326
  const [open, setOpen] = useState(false);
292
327
  const [size, setSize] = useState('default');
293
328
  const [noPadding, setNoPadding] = useState(false);
329
+ const [mode, setMode] = useState('modal');
330
+ const isDrawer = mode === 'drawer';
331
+
332
+ useEffect(() => {
333
+ if (open) {
334
+ setOpen(false);
335
+ }
336
+ }, [mode]);
337
+
338
+ const overlayProps = {
339
+ title: '确认发布岗位',
340
+ size,
341
+ noPadding,
342
+ open,
343
+ onClose: () => setOpen(false),
344
+ footer: <Text type="secondary">发布后将同步至招聘官网与内推渠道</Text>,
345
+ footerButtons: [
346
+ {
347
+ children: '存草稿',
348
+ onClick: () => message.info('已保存草稿')
349
+ },
350
+ {
351
+ children: '预览',
352
+ display: () => size !== 'small',
353
+ onClick: () => message.info('打开预览页')
354
+ },
355
+ {
356
+ type: 'primary',
357
+ children: '立即发布',
358
+ onClick: async () => {
359
+ await new Promise(resolve => setTimeout(resolve, 600));
360
+ message.success('岗位已发布');
361
+ }
362
+ }
363
+ ]
364
+ };
365
+
366
+ const content = (
367
+ <div className="demo-job-preview">
368
+ <div className="demo-job-preview-header">
369
+ <Title level={5} style={{ margin: 0 }}>
370
+ 高级前端工程师
371
+ </Title>
372
+ <Space size={8} style={{ marginTop: 8 }}>
373
+ <Tag color="blue">上海</Tag>
374
+ <Tag>25K–40K · 15 薪</Tag>
375
+ <Tag color="green">急招</Tag>
376
+ </Space>
377
+ </div>
378
+ <div className="demo-job-preview-body">
379
+ <Paragraph style={{ marginTop: 0 }}>
380
+ 负责招聘中台、候选人评估等 B 端产品的前端交付;要求熟悉 React、工程化与组件库协作。
381
+ </Paragraph>
382
+ <Paragraph style={{ marginBottom: 0 }}>
383
+ {noPadding
384
+ ? 'noPadding=true:预览卡片应贴齐内容区边缘。'
385
+ : 'noPadding=false:预览卡片四周保留默认内边距。'}
386
+ </Paragraph>
387
+ </div>
388
+ </div>
389
+ );
294
390
 
295
391
  return (
296
392
  <Space direction="vertical" style={{ width: '100%' }} size="large">
393
+ <Space align="center" wrap>
394
+ <Text type="secondary">打开方式</Text>
395
+ <Radio.Group
396
+ value={mode}
397
+ optionType="button"
398
+ size="small"
399
+ options={[
400
+ { label: 'Modal', value: 'modal' },
401
+ { label: 'Drawer', value: 'drawer' }
402
+ ]}
403
+ onChange={e => setMode(e.target.value)}
404
+ />
405
+ </Space>
297
406
  <div>
298
- <Text type="secondary">切换尺寸与 noPadding,观察岗位预览卡片与 footer 按钮区布局。</Text>
407
+ <Text type="secondary">切换 Modal / Drawer、尺寸与 noPadding,观察预览卡片与 footer 按钮区布局。</Text>
299
408
  </div>
300
409
  <Radio.Group
301
410
  value={size}
@@ -315,86 +424,45 @@ const FooterButtonsExample = () => {
315
424
  noPadding={String(noPadding)}
316
425
  </Button>
317
426
  </Space>
318
- <Modal
319
- title="确认发布岗位"
320
- size={size}
321
- noPadding={noPadding}
322
- open={open}
323
- onClose={() => setOpen(false)}
324
- footer={<Text type="secondary">发布后将同步至招聘官网与内推渠道</Text>}
325
- footerButtons={[
326
- {
327
- children: '存草稿',
328
- onClick: () => message.info('已保存草稿')
329
- },
330
- {
331
- children: '预览',
332
- display: () => size !== 'small',
333
- onClick: () => message.info('打开预览页')
334
- },
335
- {
336
- type: 'primary',
337
- children: '立即发布',
338
- onClick: async () => {
339
- await new Promise(resolve => setTimeout(resolve, 600));
340
- message.success('岗位已发布');
341
- }
342
- }
343
- ]}
344
- >
345
- <div className="demo-job-preview">
346
- <div className="demo-job-preview-header">
347
- <Title level={5} style={{ margin: 0 }}>
348
- 高级前端工程师
349
- </Title>
350
- <Space size={8} style={{ marginTop: 8 }}>
351
- <Tag color="blue">上海</Tag>
352
- <Tag>25K–40K · 15 薪</Tag>
353
- <Tag color="green">急招</Tag>
354
- </Space>
355
- </div>
356
- <div className="demo-job-preview-body">
357
- <Paragraph style={{ marginTop: 0 }}>
358
- 负责招聘中台、候选人评估等 B 端产品的前端交付;要求熟悉 React、工程化与组件库协作。
359
- </Paragraph>
360
- <Paragraph style={{ marginBottom: 0 }}>
361
- {noPadding
362
- ? 'noPadding=true:预览卡片应贴齐弹窗内容区边缘。'
363
- : 'noPadding=false:预览卡片四周保留默认内边距。'}
364
- </Paragraph>
365
- </div>
366
- </div>
367
- </Modal>
427
+ {isDrawer ? (
428
+ <Drawer {...overlayProps}>{content}</Drawer>
429
+ ) : (
430
+ <Modal {...overlayProps}>{content}</Modal>
431
+ )}
368
432
  </Space>
369
433
  );
370
434
  };
371
435
 
372
- const BaseExample = () => (
436
+ render(
373
437
  <App>
438
+ <DrawerContextHolder />
374
439
  <FooterButtonsExample />
375
440
  </App>
376
441
  );
377
442
 
378
- render(<BaseExample />);
379
-
380
443
  ```
381
444
 
382
- - useModal 命令式
383
- - 命令式打开候选人快览,children 函数内可 close()
445
+ - 命令式打开(useModal / useDrawer)
446
+ - 切换 Modal / Drawer:命令式快览,children 函数内可 close()
384
447
  - _ReactModal(@kne/current-lib_react-modal)[import * as _ReactModal from "@kne/react-modal"],(@kne/current-lib_react-modal/dist/index.css),antd(antd)
385
448
 
386
449
  ```jsx
387
- const { useModal } = _ReactModal;
388
- const { Button, Space, message, App, Descriptions, Tag, Typography } = antd;
450
+ const { useModal, useDrawer, DrawerContextHolder } = _ReactModal;
451
+ const { Button, Space, message, Descriptions, Tag, Typography, Radio, App } = antd;
452
+ const { useState } = React;
389
453
 
390
454
  const { Text, Paragraph } = Typography;
391
455
 
392
456
  const CommandExample = () => {
393
457
  const modal = useModal();
458
+ const drawer = useDrawer();
459
+ const [mode, setMode] = useState('modal');
460
+ const isDrawer = mode === 'drawer';
394
461
 
395
462
  const openDetail = () => {
396
- modal({
397
- title: '候选人快览',
463
+ const api = isDrawer ? drawer : modal;
464
+ api({
465
+ title: isDrawer ? '候选人快览(侧滑)' : '候选人快览',
398
466
  size: 'small',
399
467
  confirmText: '加入待评估',
400
468
  children: ({ close }) => (
@@ -408,8 +476,8 @@ const CommandExample = () => {
408
476
  </Descriptions.Item>
409
477
  </Descriptions>
410
478
  <Paragraph type="secondary" style={{ marginBottom: 12 }}>
411
- 命令式弹窗适用于列表页「快速查看」场景;children 为函数时可调用 <Text code>close()</Text>{' '}
412
- 主动关闭。
479
+ 命令式 {isDrawer ? 'Drawer' : 'Modal'} 适用于列表页「快速查看」;children 为函数时可调用{' '}
480
+ <Text code>close()</Text> 主动关闭。
413
481
  </Paragraph>
414
482
  <Button size="small" onClick={() => close()}>
415
483
  关闭
@@ -424,10 +492,129 @@ const CommandExample = () => {
424
492
 
425
493
  return (
426
494
  <Space direction="vertical">
495
+ <Space align="center" wrap>
496
+ <Text type="secondary">打开方式</Text>
497
+ <Radio.Group
498
+ value={mode}
499
+ optionType="button"
500
+ size="small"
501
+ options={[
502
+ { label: 'Modal', value: 'modal' },
503
+ { label: 'Drawer', value: 'drawer' }
504
+ ]}
505
+ onChange={e => setMode(e.target.value)}
506
+ />
507
+ </Space>
427
508
  <Button type="primary" onClick={openDetail}>
428
509
  从列表打开候选人快览
429
510
  </Button>
430
- <Text type="secondary">需在 antd App 上下文中使用 useModal。</Text>
511
+ <Text type="secondary">
512
+ Drawer 模式需挂载 DrawerContextHolder;Modal 使用 antd App 内置 useModal。
513
+ </Text>
514
+ </Space>
515
+ );
516
+ };
517
+
518
+ render(
519
+ <App>
520
+ <DrawerContextHolder />
521
+ <CommandExample />
522
+ </App>
523
+ );
524
+
525
+ ```
526
+
527
+ - useConfirmModal 确认框
528
+ - confirm / info / success / warning / error 命令式确认与提示(仅 Modal)
529
+ - _ReactModal(@kne/current-lib_react-modal)[import * as _ReactModal from "@kne/react-modal"],(@kne/current-lib_react-modal/dist/index.css),antd(antd)
530
+
531
+ ```jsx
532
+ const { useConfirmModal } = _ReactModal;
533
+ const { Button, Space, App } = antd;
534
+
535
+ const CommandExample = () => {
536
+ const confirmModal = useConfirmModal();
537
+
538
+ return (
539
+ <Space wrap>
540
+ <Button
541
+ onClick={() => {
542
+ confirmModal({
543
+ danger: true,
544
+ type: 'confirm',
545
+ title: '确定要删除该记录吗?',
546
+ message: '此操作将永久删除该记录,相关数据将无法恢复。请确认是否继续删除操作?'
547
+ });
548
+ }}
549
+ >
550
+ confirm
551
+ </Button>
552
+ <Button
553
+ onClick={() => {
554
+ confirmModal({
555
+ type: 'confirm',
556
+ confirmType: 'warning',
557
+ title: '确定要编辑此内容吗?',
558
+ message: '编辑后需要重新提交审核,未保存的修改将丢失。请确认是否继续编辑?'
559
+ });
560
+ }}
561
+ >
562
+ confirm 警告
563
+ </Button>
564
+ <Button
565
+ onClick={() => {
566
+ confirmModal({
567
+ type: 'info',
568
+ title: '操作提示',
569
+ message: '该操作将更新系统配置,可能影响其他用户的使用。建议在非工作时间进行此操作。'
570
+ });
571
+ }}
572
+ >
573
+ info
574
+ </Button>
575
+ <Button
576
+ onClick={() => {
577
+ confirmModal({
578
+ type: 'info',
579
+ message: '数据已保存成功,系统将在后台进行同步处理,请稍候查看处理结果。'
580
+ });
581
+ }}
582
+ >
583
+ info 无标题
584
+ </Button>
585
+ <Button
586
+ onClick={() => {
587
+ confirmModal({
588
+ type: 'success',
589
+ title: '操作成功',
590
+ message: '恭喜!您的操作已成功完成。系统已发送通知邮件给相关团队成员。'
591
+ });
592
+ }}
593
+ >
594
+ success
595
+ </Button>
596
+ <Button
597
+ onClick={() => {
598
+ confirmModal({
599
+ type: 'warning',
600
+ title: '操作警告',
601
+ message: '检测到数据异常,继续操作可能导致数据不一致。建议先备份数据或联系技术支持。'
602
+ });
603
+ }}
604
+ >
605
+ warning
606
+ </Button>
607
+ <Button
608
+ onClick={() => {
609
+ confirmModal({
610
+ type: 'error',
611
+ title: '操作失败',
612
+ message: '系统处理出错,请检查网络连接或联系系统管理员。错误代码:ERR-500'
613
+ });
614
+ }}
615
+ >
616
+ error
617
+ </Button>
431
618
  </Space>
432
619
  );
433
620
  };
@@ -443,13 +630,13 @@ render(<BaseExample />);
443
630
  ```
444
631
 
445
632
  - 长内容滚动
446
- - 面试评估纪要:默认 SimpleBar vs bodyScroll=false 自管滚动
633
+ - Modal / Drawer 切换:SimpleBar vs bodyScroll=false 自管滚动
447
634
  - _ReactModal(@kne/current-lib_react-modal)[import * as _ReactModal from "@kne/react-modal"],(@kne/current-lib_react-modal/dist/index.css),antd(antd)
448
635
 
449
636
  ```jsx
450
- const { default: Modal } = _ReactModal;
451
- const { Button, Space, App, Typography, Divider } = antd;
452
- const { useState } = React;
637
+ const { default: Modal, Drawer, DrawerContextHolder } = _ReactModal;
638
+ const { Button, Space, Typography, Divider, Radio, App } = antd;
639
+ const { useState, useEffect } = React;
453
640
 
454
641
  const { Title, Paragraph, Text } = Typography;
455
642
 
@@ -508,11 +695,38 @@ const EvaluationContent = () => (
508
695
  const LongContentExample = () => {
509
696
  const [open, setOpen] = useState(false);
510
697
  const [openSelfScroll, setOpenSelfScroll] = useState(false);
698
+ const [mode, setMode] = useState('modal');
699
+ const isDrawer = mode === 'drawer';
700
+
701
+ useEffect(() => {
702
+ if (open) {
703
+ setOpen(false);
704
+ }
705
+ if (openSelfScroll) {
706
+ setOpenSelfScroll(false);
707
+ }
708
+ }, [mode]);
709
+
710
+ const contentHeightVar = isDrawer ? '--kne-drawer-content-height' : '--kne-modal-content-height';
711
+ const Overlay = isDrawer ? Drawer : Modal;
511
712
 
512
713
  return (
513
714
  <Space direction="vertical" style={{ width: '100%' }} size="middle">
715
+ <Space align="center" wrap>
716
+ <Text type="secondary">打开方式</Text>
717
+ <Radio.Group
718
+ value={mode}
719
+ optionType="button"
720
+ size="small"
721
+ options={[
722
+ { label: 'Modal', value: 'modal' },
723
+ { label: 'Drawer', value: 'drawer' }
724
+ ]}
725
+ onChange={e => setMode(e.target.value)}
726
+ />
727
+ </Space>
514
728
  <Paragraph type="secondary" style={{ margin: 0 }}>
515
- 对比默认 SimpleBar 与 bodyScroll=false 自管滚动两种长内容场景。
729
+ 对比默认 SimpleBar 与 bodyScroll=false 自管滚动;切换 Modal / Drawer 观察高度链一致。
516
730
  </Paragraph>
517
731
  <Space wrap>
518
732
  <Button type="primary" onClick={() => setOpen(true)}>
@@ -520,25 +734,27 @@ const LongContentExample = () => {
520
734
  </Button>
521
735
  <Button onClick={() => setOpenSelfScroll(true)}>自管滚动(bodyScroll=false)</Button>
522
736
  </Space>
523
- <Modal
737
+ <Overlay
524
738
  title="陈思远 · 面试评估纪要"
525
739
  open={open}
526
740
  onClose={() => setOpen(false)}
527
741
  onConfirm={() => {}}
528
742
  confirmText="保存纪要"
743
+ size={isDrawer ? 'default' : undefined}
529
744
  >
530
745
  <EvaluationContent />
531
- </Modal>
532
- <Modal
746
+ </Overlay>
747
+ <Overlay
533
748
  title="自管滚动示例"
534
749
  open={openSelfScroll}
535
750
  onClose={() => setOpenSelfScroll(false)}
536
751
  bodyScroll={false}
537
752
  footer={null}
753
+ size={isDrawer ? 'large' : undefined}
538
754
  >
539
755
  <div
540
756
  style={{
541
- height: 'var(--kne-modal-content-height)',
757
+ height: &#96;var(${contentHeightVar})&#96;,
542
758
  minHeight: 0,
543
759
  overflow: 'auto',
544
760
  boxSizing: 'border-box',
@@ -547,35 +763,36 @@ const LongContentExample = () => {
547
763
  >
548
764
  <div style={{ padding: 20 }}>
549
765
  <Paragraph style={{ marginTop: 0 }}>
550
- 内容区高度绑定 <Text code>--kne-modal-content-height</Text>,在 Tabs / 分栏场景同样适用。
766
+ 内容区高度绑定 <Text code>{contentHeightVar}</Text>,Tabs / 分栏场景同样适用。
551
767
  </Paragraph>
552
768
  <EvaluationContent />
553
769
  </div>
554
770
  </div>
555
- </Modal>
771
+ </Overlay>
556
772
  </Space>
557
773
  );
558
774
  };
559
775
 
560
- const BaseExample = () => (
776
+ render(
561
777
  <App>
778
+ <DrawerContextHolder />
562
779
  <LongContentExample />
563
780
  </App>
564
781
  );
565
782
 
566
- render(<BaseExample />);
567
-
568
783
  ```
569
784
 
570
785
  - 高度 CSS 变量
571
- - Switch 对比默认/自定义 CSS 变量;色块绑定 --kne-modal-content-height,探针显示变量与 clientHeight
786
+ - Modal / Drawer 切换:默认/自定义 CSS 变量;色块绑定 content-height,探针显示变量与 clientHeight
572
787
  - _ReactModal(@kne/current-lib_react-modal)[import * as _ReactModal from "@kne/react-modal"],(@kne/current-lib_react-modal/dist/index.css),antd(antd)
573
788
 
574
789
  ```jsx
575
- const { default: Modal } = _ReactModal;
576
- const { Button, Space, Switch, Tag, Descriptions, App } = antd;
790
+ const { default: Modal, Drawer, DrawerContextHolder } = _ReactModal;
791
+ const { Button, Space, Switch, Tag, Descriptions, Radio, Typography, App } = antd;
577
792
  const { useState, useEffect } = React;
578
793
 
794
+ const { Text } = Typography;
795
+
579
796
  const readCssVar = (el, name) => {
580
797
  if (!el) {
581
798
  return '-';
@@ -583,8 +800,22 @@ const readCssVar = (el, name) => {
583
800
  return getComputedStyle(el).getPropertyValue(name).trim() || '-';
584
801
  };
585
802
 
586
- const HeightProbe = ({ open, revision }) => {
803
+ const getOverlayChrome = mode => {
804
+ const isDrawer = mode === 'drawer';
805
+ return {
806
+ isDrawer,
807
+ testId: isDrawer ? 'react-drawer' : 'react-modal',
808
+ bodyClass: isDrawer ? 'drawer-body' : 'modal-body',
809
+ innerClass: isDrawer ? 'drawer-body-inner' : 'modal-body-inner',
810
+ contentHeightVar: isDrawer ? '--kne-drawer-content-height' : '--kne-modal-content-height',
811
+ bodyHeightVar: isDrawer ? '--kne-drawer-body-height' : '--kne-modal-body-height',
812
+ varPrefix: isDrawer ? '--kne-drawer' : '--kne-modal'
813
+ };
814
+ };
815
+
816
+ const HeightProbe = ({ open, revision, mode }) => {
587
817
  const [metrics, setMetrics] = useState(null);
818
+ const chrome = getOverlayChrome(mode);
588
819
 
589
820
  useEffect(() => {
590
821
  if (!open) {
@@ -592,34 +823,34 @@ const HeightProbe = ({ open, revision }) => {
592
823
  return undefined;
593
824
  }
594
825
  const timer = setTimeout(() => {
595
- const outer = document.querySelector('[data-testid="react-modal"]');
596
- const body = outer && outer.querySelector('.modal-body');
597
- const inner = outer && outer.querySelector('.modal-body-inner');
826
+ const outer = document.querySelector(&#96;[data-testid="${chrome.testId}"]&#96;);
827
+ const body = outer && outer.querySelector(&#96;.${chrome.bodyClass}&#96;);
828
+ const inner = outer && outer.querySelector(&#96;.${chrome.innerClass}&#96;);
598
829
  const fill = outer && outer.querySelector('[data-vars-fill]');
599
830
  setMetrics({
600
- gutter: readCssVar(outer, '--kne-modal-viewport-gutter'),
601
- bodyHeightVar: readCssVar(body || outer, '--kne-modal-body-height'),
602
- contentHeightVar: readCssVar(inner || body || outer, '--kne-modal-content-height'),
603
- bodyMin: readCssVar(outer, '--kne-modal-body-min-height'),
604
- paddingV: readCssVar(outer, '--kne-modal-body-padding-vertical'),
831
+ gutter: readCssVar(outer, &#96;${chrome.varPrefix}-viewport-gutter&#96;),
832
+ bodyHeightVar: readCssVar(body || outer, chrome.bodyHeightVar),
833
+ contentHeightVar: readCssVar(inner || body || outer, chrome.contentHeightVar),
834
+ bodyMin: readCssVar(outer, &#96;${chrome.varPrefix}-body-min-height&#96;),
835
+ paddingV: readCssVar(outer, &#96;${chrome.varPrefix}-body-padding-vertical&#96;),
605
836
  bodyClient: body ? &#96;${body.clientHeight}px&#96; : '-',
606
837
  fillClient: fill ? &#96;${fill.clientHeight}px&#96; : '-'
607
838
  });
608
839
  }, 80);
609
840
  return () => clearTimeout(timer);
610
- }, [open, revision]);
841
+ }, [open, revision, mode, chrome]);
611
842
 
612
843
  if (!metrics) {
613
- return <Tag>打开弹窗后显示变量与实测高度</Tag>;
844
+ return <Tag>打开弹层后显示变量与实测高度</Tag>;
614
845
  }
615
846
 
616
847
  return (
617
848
  <Descriptions size="small" bordered column={1} style={{ maxWidth: 640 }}>
618
- <Descriptions.Item label="--kne-modal-viewport-gutter">{metrics.gutter}</Descriptions.Item>
619
- <Descriptions.Item label="--kne-modal-body-min-height">{metrics.bodyMin}</Descriptions.Item>
620
- <Descriptions.Item label="--kne-modal-body-padding-vertical">{metrics.paddingV}</Descriptions.Item>
621
- <Descriptions.Item label="--kne-modal-body-height">{metrics.bodyHeightVar}</Descriptions.Item>
622
- <Descriptions.Item label="--kne-modal-content-height">{metrics.contentHeightVar}</Descriptions.Item>
849
+ <Descriptions.Item label={&#96;${chrome.varPrefix}-viewport-gutter&#96;}>{metrics.gutter}</Descriptions.Item>
850
+ <Descriptions.Item label={&#96;${chrome.varPrefix}-body-min-height&#96;}>{metrics.bodyMin}</Descriptions.Item>
851
+ <Descriptions.Item label={&#96;${chrome.varPrefix}-body-padding-vertical&#96;}>{metrics.paddingV}</Descriptions.Item>
852
+ <Descriptions.Item label={chrome.bodyHeightVar}>{metrics.bodyHeightVar}</Descriptions.Item>
853
+ <Descriptions.Item label={chrome.contentHeightVar}>{metrics.contentHeightVar}</Descriptions.Item>
623
854
  <Descriptions.Item label="body.clientHeight">{metrics.bodyClient}</Descriptions.Item>
624
855
  <Descriptions.Item label="色块 clientHeight(应≈ content-height)">{metrics.fillClient}</Descriptions.Item>
625
856
  </Descriptions>
@@ -631,18 +862,47 @@ const fillLines = Array.from({ length: 18 }, (_, i) => &#96;填充行 ${i + 1}
631
862
  const HeightVarsExample = () => {
632
863
  const [open, setOpen] = useState(false);
633
864
  const [customVars, setCustomVars] = useState(true);
634
- const revision = &#96;${customVars}|${open}&#96;;
865
+ const [mode, setMode] = useState('modal');
866
+ const isDrawer = mode === 'drawer';
635
867
 
636
- const modalStyle = customVars
637
- ? {
638
- '--kne-modal-viewport-gutter': '240px',
639
- '--kne-modal-body-min-height': '180px',
640
- '--kne-modal-body-padding-vertical': '64px'
641
- }
868
+ useEffect(() => {
869
+ if (open) {
870
+ setOpen(false);
871
+ }
872
+ }, [mode]);
873
+
874
+ const chrome = getOverlayChrome(mode);
875
+ const revision = &#96;${mode}|${customVars}|${open}&#96;;
876
+ const Overlay = isDrawer ? Drawer : Modal;
877
+
878
+ const overlayStyle = customVars
879
+ ? isDrawer
880
+ ? {
881
+ '--kne-drawer-body-min-height': '180px',
882
+ '--kne-drawer-body-padding-vertical': '64px'
883
+ }
884
+ : {
885
+ '--kne-modal-viewport-gutter': '240px',
886
+ '--kne-modal-body-min-height': '180px',
887
+ '--kne-modal-body-padding-vertical': '64px'
888
+ }
642
889
  : undefined;
643
890
 
644
891
  return (
645
892
  <Space direction="vertical" style={{ width: '100%' }} size="middle">
893
+ <Space align="center" wrap>
894
+ <Text type="secondary">打开方式</Text>
895
+ <Radio.Group
896
+ value={mode}
897
+ optionType="button"
898
+ size="small"
899
+ options={[
900
+ { label: 'Modal', value: 'modal' },
901
+ { label: 'Drawer', value: 'drawer' }
902
+ ]}
903
+ onChange={e => setMode(e.target.value)}
904
+ />
905
+ </Space>
646
906
  <Space wrap align="center">
647
907
  <Switch
648
908
  checked={customVars}
@@ -651,29 +911,29 @@ const HeightVarsExample = () => {
651
911
  unCheckedChildren="默认变量"
652
912
  />
653
913
  <Button type="primary" onClick={() => setOpen(true)}>
654
- 打开弹窗对比高度
914
+ 打开弹层对比高度
655
915
  </Button>
656
916
  <Tag color={customVars ? 'blue' : 'default'}>{customVars ? '已覆盖 CSS 变量' : '库内默认值'}</Tag>
657
917
  </Space>
658
918
  <div style={{ color: 'rgba(0,0,0,0.45)', maxWidth: 640 }}>
659
- 通过 Modal <code>style</code> 覆盖变量(挂在 <code>.modal</code> 根上)。自定义时加大{' '}
660
- <code>viewport-gutter</code>、<code>body-padding-vertical</code>,body 会明显变矮;下方色块高度绑定{' '}
661
- <code>--kne-modal-content-height</code>,应与实测 content 高度一致。
919
+ 通过 <code>style</code> 覆盖 {chrome.varPrefix}-* 变量。Drawer 无 viewport-gutter;Modal 可加大 gutter
920
+ 使 body 明显变矮。色块绑定 <code>{chrome.contentHeightVar}</code>。
662
921
  </div>
663
- <HeightProbe open={open} revision={revision} />
664
- <Modal
922
+ <HeightProbe open={open} revision={revision} mode={mode} />
923
+ <Overlay
665
924
  title={customVars ? '自定义高度 CSS 变量' : '默认高度 CSS 变量'}
666
925
  open={open}
667
926
  onClose={() => setOpen(false)}
668
927
  onConfirm={() => {}}
669
928
  bodyScroll={false}
670
929
  noPadding={false}
671
- style={modalStyle}
930
+ style={overlayStyle}
931
+ size={isDrawer ? 'default' : undefined}
672
932
  >
673
933
  <div
674
934
  data-vars-fill
675
935
  style={{
676
- height: 'var(--kne-modal-content-height)',
936
+ height: &#96;var(${chrome.contentHeightVar})&#96;,
677
937
  minHeight: 0,
678
938
  overflow: 'auto',
679
939
  boxSizing: 'border-box',
@@ -683,16 +943,8 @@ const HeightVarsExample = () => {
683
943
  >
684
944
  <div style={{ padding: 12 }}>
685
945
  <p style={{ marginTop: 0, fontWeight: 600 }}>
686
- 色块 height = var(--kne-modal-content-height)。切换「自定义/默认」后重新打开,对比色块高度与左侧探针数值。
946
+ 色块 height = var({chrome.contentHeightVar})。切换「自定义/默认」与 Modal/Drawer 后重新打开对比。
687
947
  </p>
688
- {customVars ? (
689
- <p style={{ margin: '0 0 8px' }}>
690
- 当前覆盖:gutter 240px(默认 120px)、padding-vertical 64px(默认 48px)、body-min-height
691
- 180px。
692
- </p>
693
- ) : (
694
- <p style={{ margin: '0 0 8px' }}>未传 style,使用库内 calc 与 size 默认 min-height。</p>
695
- )}
696
948
  {fillLines.map(text => (
697
949
  <p key={text} style={{ margin: '4px 0' }}>
698
950
  {text}
@@ -700,30 +952,31 @@ const HeightVarsExample = () => {
700
952
  ))}
701
953
  </div>
702
954
  </div>
703
- </Modal>
955
+ </Overlay>
704
956
  </Space>
705
957
  );
706
958
  };
707
959
 
708
- const BaseExample = () => (
960
+ render(
709
961
  <App>
962
+ <DrawerContextHolder />
710
963
  <HeightVarsExample />
711
964
  </App>
712
965
  );
713
966
 
714
- render(<BaseExample />);
715
-
716
967
  ```
717
968
 
718
969
  - title / footer / noPadding 高度探针
719
- - 切换 title 空/有、footer=null、footerButtons=[]、noPadding auto/true/false、bodyScroll,实测 body 与 content 高度变量与 clientHeight
970
+ - Modal / Drawer 切换:title、footer、noPadding、bodyScroll 组合实测高度变量
720
971
  - _ReactModal(@kne/current-lib_react-modal)[import * as _ReactModal from "@kne/react-modal"],(@kne/current-lib_react-modal/dist/index.css),antd(antd)
721
972
 
722
973
  ```jsx
723
- const { default: Modal } = _ReactModal;
724
- const { Button, Space, Switch, Radio, Tag, Descriptions, App } = antd;
974
+ const { default: Modal, Drawer, DrawerContextHolder } = _ReactModal;
975
+ const { Button, Space, Switch, Radio, Tag, Descriptions, Typography, App } = antd;
725
976
  const { useState, useEffect } = React;
726
977
 
978
+ const { Text } = Typography;
979
+
727
980
  const readCssVar = (el, name) => {
728
981
  if (!el) {
729
982
  return '-';
@@ -731,8 +984,22 @@ const readCssVar = (el, name) => {
731
984
  return getComputedStyle(el).getPropertyValue(name).trim() || '-';
732
985
  };
733
986
 
734
- const HeightProbe = ({ open, revision }) => {
987
+ const getOverlayChrome = mode => {
988
+ const isDrawer = mode === 'drawer';
989
+ return {
990
+ isDrawer,
991
+ testId: isDrawer ? 'react-drawer' : 'react-modal',
992
+ bodyClass: isDrawer ? 'drawer-body' : 'modal-body',
993
+ innerClass: isDrawer ? 'drawer-body-inner' : 'modal-body-inner',
994
+ contentHeightVar: isDrawer ? '--kne-drawer-content-height' : '--kne-modal-content-height',
995
+ bodyHeightVar: isDrawer ? '--kne-drawer-body-height' : '--kne-modal-body-height',
996
+ varPrefix: isDrawer ? '--kne-drawer' : '--kne-modal'
997
+ };
998
+ };
999
+
1000
+ const HeightProbe = ({ open, revision, mode }) => {
735
1001
  const [metrics, setMetrics] = useState(null);
1002
+ const chrome = getOverlayChrome(mode);
736
1003
 
737
1004
  useEffect(() => {
738
1005
  if (!open) {
@@ -740,39 +1007,39 @@ const HeightProbe = ({ open, revision }) => {
740
1007
  return undefined;
741
1008
  }
742
1009
  const timer = setTimeout(() => {
743
- const outer = document.querySelector('[data-testid="react-modal"]');
744
- const body = outer && outer.querySelector('.modal-body');
745
- const inner = outer && outer.querySelector('.modal-body-inner');
1010
+ const outer = document.querySelector(&#96;[data-testid="${chrome.testId}"]&#96;);
1011
+ const body = outer && outer.querySelector(&#96;.${chrome.bodyClass}&#96;);
1012
+ const inner = outer && outer.querySelector(&#96;.${chrome.innerClass}&#96;);
746
1013
  const scrollHost = outer && outer.querySelector('[data-height-scroll-host]');
747
1014
  setMetrics({
748
1015
  outerClass: outer ? outer.className : '-',
749
- bodyHeightVar: readCssVar(body || outer, '--kne-modal-body-height'),
750
- contentHeightVar: readCssVar(inner || body || outer, '--kne-modal-content-height'),
751
- paddingV: readCssVar(outer, '--kne-modal-body-padding-vertical'),
752
- paddingH: readCssVar(outer, '--kne-modal-body-padding-horizontal'),
753
- titleH: readCssVar(outer, '--kne-modal-title-height'),
754
- footerH: readCssVar(outer, '--kne-modal-footer-height'),
1016
+ bodyHeightVar: readCssVar(body || outer, chrome.bodyHeightVar),
1017
+ contentHeightVar: readCssVar(inner || body || outer, chrome.contentHeightVar),
1018
+ paddingV: readCssVar(outer, &#96;${chrome.varPrefix}-body-padding-vertical&#96;),
1019
+ paddingH: readCssVar(outer, &#96;${chrome.varPrefix}-body-padding-horizontal&#96;),
1020
+ titleH: readCssVar(outer, &#96;${chrome.varPrefix}-title-height&#96;),
1021
+ footerH: readCssVar(outer, &#96;${chrome.varPrefix}-footer-height&#96;),
755
1022
  bodyClient: body ? &#96;${body.clientHeight}px&#96; : '-',
756
1023
  innerClient: inner ? &#96;${inner.clientHeight}px&#96; : '-',
757
1024
  scrollClient: scrollHost ? &#96;${scrollHost.clientHeight}px&#96; : '-'
758
1025
  });
759
1026
  }, 80);
760
1027
  return () => clearTimeout(timer);
761
- }, [open, revision]);
1028
+ }, [open, revision, mode, chrome]);
762
1029
 
763
1030
  if (!metrics) {
764
- return <Tag>打开弹窗后显示实测高度</Tag>;
1031
+ return <Tag>打开弹层后显示实测高度</Tag>;
765
1032
  }
766
1033
 
767
1034
  return (
768
1035
  <Descriptions size="small" bordered column={1} style={{ maxWidth: 640 }}>
769
1036
  <Descriptions.Item label="outer class">{metrics.outerClass}</Descriptions.Item>
770
- <Descriptions.Item label="--kne-modal-title-height">{metrics.titleH}</Descriptions.Item>
771
- <Descriptions.Item label="--kne-modal-footer-height">{metrics.footerH}</Descriptions.Item>
772
- <Descriptions.Item label="--kne-modal-body-padding-vertical">{metrics.paddingV}</Descriptions.Item>
773
- <Descriptions.Item label="--kne-modal-body-padding-horizontal">{metrics.paddingH}</Descriptions.Item>
774
- <Descriptions.Item label="--kne-modal-body-height">{metrics.bodyHeightVar}</Descriptions.Item>
775
- <Descriptions.Item label="--kne-modal-content-height">{metrics.contentHeightVar}</Descriptions.Item>
1037
+ <Descriptions.Item label={&#96;${chrome.varPrefix}-title-height&#96;}>{metrics.titleH}</Descriptions.Item>
1038
+ <Descriptions.Item label={&#96;${chrome.varPrefix}-footer-height&#96;}>{metrics.footerH}</Descriptions.Item>
1039
+ <Descriptions.Item label={&#96;${chrome.varPrefix}-body-padding-vertical&#96;}>{metrics.paddingV}</Descriptions.Item>
1040
+ <Descriptions.Item label={&#96;${chrome.varPrefix}-body-padding-horizontal&#96;}>{metrics.paddingH}</Descriptions.Item>
1041
+ <Descriptions.Item label={chrome.bodyHeightVar}>{metrics.bodyHeightVar}</Descriptions.Item>
1042
+ <Descriptions.Item label={chrome.contentHeightVar}>{metrics.contentHeightVar}</Descriptions.Item>
776
1043
  <Descriptions.Item label="body.clientHeight">{metrics.bodyClient}</Descriptions.Item>
777
1044
  <Descriptions.Item label="body-inner.clientHeight">{metrics.innerClient}</Descriptions.Item>
778
1045
  <Descriptions.Item label="scrollHost.clientHeight">{metrics.scrollClient}</Descriptions.Item>
@@ -783,39 +1050,59 @@ const HeightProbe = ({ open, revision }) => {
783
1050
  const ChromeHeightExample = () => {
784
1051
  const [open, setOpen] = useState(false);
785
1052
  const [hasTitle, setHasTitle] = useState(true);
786
- const [footerMode, setFooterMode] = useState('default'); // default | null | emptyButtons
1053
+ const [footerMode, setFooterMode] = useState('default');
787
1054
  const [bodyScroll, setBodyScroll] = useState(false);
788
- const [noPaddingMode, setNoPaddingMode] = useState('auto'); // auto | true | false
789
- const revision = [hasTitle, footerMode, bodyScroll, noPaddingMode, open].join('|');
1055
+ const [noPaddingMode, setNoPaddingMode] = useState('auto');
1056
+ const [mode, setMode] = useState('modal');
1057
+ const isDrawer = mode === 'drawer';
790
1058
 
791
- const noPaddingProp =
792
- noPaddingMode === 'auto' ? undefined : noPaddingMode === 'true';
1059
+ useEffect(() => {
1060
+ if (open) {
1061
+ setOpen(false);
1062
+ }
1063
+ }, [mode]);
1064
+
1065
+ const chrome = getOverlayChrome(mode);
1066
+ const Overlay = isDrawer ? Drawer : Modal;
1067
+ const revision = [mode, hasTitle, footerMode, bodyScroll, noPaddingMode, open].join('|');
793
1068
 
794
- const modalProps = {
1069
+ const noPaddingProp = noPaddingMode === 'auto' ? undefined : noPaddingMode === 'true';
1070
+
1071
+ const overlayProps = {
795
1072
  open,
796
1073
  onClose: () => setOpen(false),
797
1074
  bodyScroll,
798
1075
  ...(hasTitle ? { title: '候选人评估 · 高度调试' } : {}),
799
- ...(noPaddingProp === undefined ? {} : { noPadding: noPaddingProp })
1076
+ ...(noPaddingProp === undefined ? {} : { noPadding: noPaddingProp }),
1077
+ size: isDrawer ? 'default' : undefined
800
1078
  };
801
1079
 
802
1080
  if (footerMode === 'null') {
803
- modalProps.footer = null;
1081
+ overlayProps.footer = null;
804
1082
  } else if (footerMode === 'emptyButtons') {
805
- modalProps.footer = <span>仅左侧 footer,按钮为空数组</span>;
806
- modalProps.footerButtons = [];
1083
+ overlayProps.footer = <span>仅左侧 footer,按钮为空数组</span>;
1084
+ overlayProps.footerButtons = [];
807
1085
  } else {
808
- modalProps.onConfirm = () => {};
1086
+ overlayProps.onConfirm = () => {};
809
1087
  }
810
1088
 
811
- const openModal = () => {
812
- setOpen(true);
813
- };
814
-
815
1089
  const lines = Array.from({ length: 24 }, (_, i) => &#96;行 ${i + 1} · 用于观察滚动与高度是否贴齐&#96;);
816
1090
 
817
1091
  return (
818
1092
  <Space direction="vertical" style={{ width: '100%' }} size="middle">
1093
+ <Space align="center" wrap>
1094
+ <Text type="secondary">打开方式</Text>
1095
+ <Radio.Group
1096
+ value={mode}
1097
+ optionType="button"
1098
+ size="small"
1099
+ options={[
1100
+ { label: 'Modal', value: 'modal' },
1101
+ { label: 'Drawer', value: 'drawer' }
1102
+ ]}
1103
+ onChange={e => setMode(e.target.value)}
1104
+ />
1105
+ </Space>
819
1106
  <Space wrap align="center">
820
1107
  <span>title</span>
821
1108
  <Switch checked={hasTitle} onChange={setHasTitle} checkedChildren="有" unCheckedChildren="空" />
@@ -852,29 +1139,21 @@ const ChromeHeightExample = () => {
852
1139
  ]}
853
1140
  onChange={e => setNoPaddingMode(e.target.value)}
854
1141
  />
855
- <div style={{ marginTop: 8, color: 'rgba(0,0,0,0.45)' }}>
856
- bodyScroll=false 且未传 noPadding 时默认去掉内边距;显式 false 可保留 padding 观察差异。
857
- </div>
858
1142
  </div>
859
1143
  <Space wrap>
860
- <Button type="primary" onClick={openModal}>
861
- 打开评估弹窗并测量
1144
+ <Button type="primary" onClick={() => setOpen(true)}>
1145
+ 打开并测量
862
1146
  </Button>
863
- <Tag>hasTitle={String(hasTitle)}</Tag>
1147
+ <Tag>mode={mode}</Tag>
864
1148
  <Tag>footerMode={footerMode}</Tag>
865
- <Tag>bodyScroll={String(bodyScroll)}</Tag>
866
- <Tag>
867
- noPadding=
868
- {noPaddingProp === undefined ? 'undefined(auto)' : String(noPaddingProp)}
869
- </Tag>
870
1149
  </Space>
871
- <HeightProbe open={open} revision={revision} />
872
- <Modal {...modalProps}>
1150
+ <HeightProbe open={open} revision={revision} mode={mode} />
1151
+ <Overlay {...overlayProps}>
873
1152
  {bodyScroll === false ? (
874
1153
  <div
875
1154
  data-height-scroll-host
876
1155
  style={{
877
- height: 'var(--kne-modal-content-height)',
1156
+ height: &#96;var(${chrome.contentHeightVar})&#96;,
878
1157
  minHeight: 0,
879
1158
  overflow: 'auto',
880
1159
  boxSizing: 'border-box',
@@ -884,8 +1163,7 @@ const ChromeHeightExample = () => {
884
1163
  >
885
1164
  <div style={{ padding: 12 }}>
886
1165
  <p style={{ marginTop: 0, fontWeight: 600 }}>
887
- 滚动宿主使用 height: var(--kne-modal-content-height)。蓝/橙底应与 body
888
- 内容区同高;footer=null 时 footer 变量应为 0。
1166
+ 滚动宿主 height: var({chrome.contentHeightVar})。footer=null 时 footer 变量应为 0。
889
1167
  </p>
890
1168
  {lines.map(text => (
891
1169
  <p key={text}>{text}</p>
@@ -909,31 +1187,32 @@ const ChromeHeightExample = () => {
909
1187
  </div>
910
1188
  </div>
911
1189
  )}
912
- </Modal>
1190
+ </Overlay>
913
1191
  </Space>
914
1192
  );
915
1193
  };
916
1194
 
917
- const BaseExample = () => (
1195
+ render(
918
1196
  <App>
1197
+ <DrawerContextHolder />
919
1198
  <ChromeHeightExample />
920
1199
  </App>
921
1200
  );
922
1201
 
923
- render(<BaseExample />);
924
-
925
1202
  ```
926
1203
 
927
1204
  - 批量候选人评估(Tabs + 分栏)
928
- - HR 批量面试评估:左列表右详情、搜索筛选、维度评分;固定分栏 / Splitter / 批次概览
1205
+ - Modal / Drawer 切换:Tabs 分栏 / Splitter / 批次概览
929
1206
  - _ReactModal(@kne/current-lib_react-modal)[import * as _ReactModal from "@kne/react-modal"],(@kne/current-lib_react-modal/dist/index.css),antd(antd),(@kne/current-lib_react-modal/doc/style.scss)
930
1207
 
931
1208
  ```jsx
932
1209
  const {
933
1210
  default: Modal,
934
- ModalTabsLayout,
935
- ModalColumnsLayout,
936
- ModalScrollRegion,
1211
+ Drawer,
1212
+ DrawerContextHolder,
1213
+ TabsLayout,
1214
+ ColumnsLayout,
1215
+ ScrollRegion,
937
1216
  modalClassNames
938
1217
  } = _ReactModal;
939
1218
  const {
@@ -948,9 +1227,10 @@ const {
948
1227
  Typography,
949
1228
  Divider,
950
1229
  App,
951
- message
1230
+ message,
1231
+ Radio
952
1232
  } = antd;
953
- const { useState, useMemo } = React;
1233
+ const { useState, useMemo, useEffect } = React;
954
1234
 
955
1235
  const { Text, Title, Paragraph } = Typography;
956
1236
 
@@ -1234,8 +1514,8 @@ const ColumnsPane = () => {
1234
1514
  const { setActive, search, setSearch, filtered, current } = useCandidatePanel();
1235
1515
 
1236
1516
  return (
1237
- <ModalColumnsLayout widths={['34%', '1fr']}>
1238
- <ModalScrollRegion>
1517
+ <ColumnsLayout widths={['34%', '1fr']}>
1518
+ <ScrollRegion>
1239
1519
  <CandidateList
1240
1520
  items={filtered}
1241
1521
  activeKey={current?.key}
@@ -1243,11 +1523,11 @@ const ColumnsPane = () => {
1243
1523
  search={search}
1244
1524
  onSearchChange={setSearch}
1245
1525
  />
1246
- </ModalScrollRegion>
1247
- <ModalScrollRegion inset>
1526
+ </ScrollRegion>
1527
+ <ScrollRegion inset>
1248
1528
  <CandidateDetail candidate={current} />
1249
- </ModalScrollRegion>
1250
- </ModalColumnsLayout>
1529
+ </ScrollRegion>
1530
+ </ColumnsLayout>
1251
1531
  );
1252
1532
  };
1253
1533
 
@@ -1265,7 +1545,7 @@ const SplitterPane = () => {
1265
1545
  max="52%"
1266
1546
  >
1267
1547
  <Splitter.Panel>
1268
- <ModalScrollRegion>
1548
+ <ScrollRegion>
1269
1549
  <CandidateList
1270
1550
  items={filtered}
1271
1551
  activeKey={current?.key}
@@ -1273,12 +1553,12 @@ const SplitterPane = () => {
1273
1553
  search={search}
1274
1554
  onSearchChange={setSearch}
1275
1555
  />
1276
- </ModalScrollRegion>
1556
+ </ScrollRegion>
1277
1557
  </Splitter.Panel>
1278
1558
  <Splitter.Panel>
1279
- <ModalScrollRegion inset>
1559
+ <ScrollRegion inset>
1280
1560
  <CandidateDetail candidate={current} />
1281
- </ModalScrollRegion>
1561
+ </ScrollRegion>
1282
1562
  </Splitter.Panel>
1283
1563
  </Splitter>
1284
1564
  </div>
@@ -1302,7 +1582,7 @@ const OverviewPane = () => {
1302
1582
  ];
1303
1583
 
1304
1584
  return (
1305
- <ModalScrollRegion>
1585
+ <ScrollRegion>
1306
1586
  <div className="candidate-overview-stats">
1307
1587
  {[
1308
1588
  { label: '本批人数', value: stats.total },
@@ -1344,7 +1624,7 @@ const OverviewPane = () => {
1344
1624
  })}
1345
1625
  <Divider />
1346
1626
  <Paragraph type="secondary" style={{ marginBottom: 0 }}>
1347
- 概览 Tab 同样使用 ModalScrollRegion:批次统计与日程较长时在本面板内滚动,不影响 Tabs 顶栏与底部操作区。
1627
+ 概览 Tab 同样使用 ScrollRegion:批次统计与日程较长时在本面板内滚动,不影响 Tabs 顶栏与底部操作区。
1348
1628
  </Paragraph>
1349
1629
  {Array.from({ length: 6 }, (_, i) => (
1350
1630
  <Paragraph key={i} style={{ marginBottom: 8, color: 'rgba(0,0,0,0.45)' }}>
@@ -1352,25 +1632,60 @@ const OverviewPane = () => {
1352
1632
  </Paragraph>
1353
1633
  ))}
1354
1634
  </div>
1355
- </ModalScrollRegion>
1635
+ </ScrollRegion>
1356
1636
  );
1357
1637
  };
1358
1638
 
1359
1639
  const ExtendLayoutExample = () => {
1360
1640
  const [open, setOpen] = useState(false);
1361
1641
  const [tab, setTab] = useState('columns');
1642
+ const [mode, setMode] = useState('modal');
1643
+ const isDrawer = mode === 'drawer';
1644
+
1645
+ useEffect(() => {
1646
+ if (open) {
1647
+ setOpen(false);
1648
+ }
1649
+ }, [mode]);
1650
+
1651
+ const Overlay = isDrawer ? Drawer : Modal;
1652
+
1653
+ const layout = (
1654
+ <TabsLayout
1655
+ activeKey={tab}
1656
+ onChange={setTab}
1657
+ items={[
1658
+ { key: 'columns', label: '候选人', children: <ColumnsPane /> },
1659
+ { key: 'split', label: '可调分栏', children: <SplitterPane /> },
1660
+ { key: 'overview', label: '批次概览', children: <OverviewPane /> }
1661
+ ]}
1662
+ />
1663
+ );
1362
1664
 
1363
1665
  return (
1364
1666
  <Space direction="vertical" style={{ width: '100%' }} size="middle">
1667
+ <Space align="center" wrap>
1668
+ <Text type="secondary">打开方式</Text>
1669
+ <Radio.Group
1670
+ value={mode}
1671
+ optionType="button"
1672
+ size="small"
1673
+ options={[
1674
+ { label: 'Modal', value: 'modal' },
1675
+ { label: 'Drawer', value: 'drawer' }
1676
+ ]}
1677
+ onChange={e => setMode(e.target.value)}
1678
+ />
1679
+ </Space>
1365
1680
  <div>
1366
1681
  <Button type="primary" onClick={() => setOpen(true)}>
1367
- 打开批量评估弹窗
1682
+ 打开批量评估弹层
1368
1683
  </Button>
1369
1684
  <Paragraph type="secondary" style={{ margin: '8px 0 0' }}>
1370
- 模拟 HR 批量面试评估:Tabs 占 title 位,左列表右详情,支持固定分栏 / 可调 Splitter / 批次概览。
1685
+ 模拟 HR 批量面试评估:Tabs 占 title 位,左列表右详情;切换 Modal / Drawer 对比布局高度链。
1371
1686
  </Paragraph>
1372
1687
  </div>
1373
- <Modal
1688
+ <Overlay
1374
1689
  open={open}
1375
1690
  onClose={() => setOpen(false)}
1376
1691
  bodyScroll={false}
@@ -1382,39 +1697,30 @@ const ExtendLayoutExample = () => {
1382
1697
  confirmText="保存本批评估"
1383
1698
  cancelText="稍后处理"
1384
1699
  >
1385
- <ModalTabsLayout
1386
- activeKey={tab}
1387
- onChange={setTab}
1388
- items={[
1389
- { key: 'columns', label: '候选人', children: <ColumnsPane /> },
1390
- { key: 'split', label: '可调分栏', children: <SplitterPane /> },
1391
- { key: 'overview', label: '批次概览', children: <OverviewPane /> }
1392
- ]}
1393
- />
1394
- </Modal>
1700
+ {layout}
1701
+ </Overlay>
1395
1702
  </Space>
1396
1703
  );
1397
1704
  };
1398
1705
 
1399
- const BaseExample = () => (
1706
+ render(
1400
1707
  <App>
1708
+ <DrawerContextHolder />
1401
1709
  <ExtendLayoutExample />
1402
1710
  </App>
1403
1711
  );
1404
1712
 
1405
- render(<BaseExample />);
1406
-
1407
1713
  ```
1408
1714
 
1409
- - useModal 内再开声明式 Modal
1410
- - 导出评估报告:外层 useModal + 内层字段选择 Modal(嵌套 hoist)
1715
+ - 嵌套弹层(外层命令式 + 内层 Modal)
1716
+ - Modal / Drawer 切换外层;内层声明式 Modal 嵌套 hoist
1411
1717
  - _ReactModal(@kne/current-lib_react-modal)[import * as _ReactModal from "@kne/react-modal"],(@kne/current-lib_react-modal/dist/index.css),antd(antd)
1412
1718
 
1413
1719
  ```jsx
1414
- const { default: Modal, useModal } = _ReactModal;
1415
- const { Button, Space, Typography, Tag, App, message, Checkbox } = antd;
1720
+ const { default: Modal, useModal, useDrawer, DrawerContextHolder } = _ReactModal;
1721
+ const { Button, Space, Typography, Tag, message, Checkbox, Radio, App } = antd;
1416
1722
  const { useState, useEffect } = React;
1417
- const { Text, Paragraph, Title } = Typography;
1723
+ const { Text, Paragraph } = Typography;
1418
1724
 
1419
1725
  const EXPORT_FIELDS = [
1420
1726
  { label: '基本信息', value: 'basic' },
@@ -1423,7 +1729,7 @@ const EXPORT_FIELDS = [
1423
1729
  { label: '附件简历', value: 'resume' }
1424
1730
  ];
1425
1731
 
1426
- const NestProbe = ({ open }) => {
1732
+ const NestProbe = ({ open, isDrawer }) => {
1427
1733
  const [info, setInfo] = useState(null);
1428
1734
 
1429
1735
  useEffect(() => {
@@ -1432,46 +1738,40 @@ const NestProbe = ({ open }) => {
1432
1738
  return;
1433
1739
  }
1434
1740
  const id = window.setTimeout(() => {
1435
- const titles = Array.from(document.querySelectorAll('.ant-modal-root .modal-title'));
1436
- const outerTitle = titles.find(el => el.textContent === '导出评估报告');
1741
+ const outerRootClass = isDrawer ? '.ant-drawer-root' : '.ant-modal-root';
1742
+ const titleClass = isDrawer ? '.drawer-title' : '.modal-title';
1743
+ const titles = Array.from(document.querySelectorAll(&#96;${outerRootClass} ${titleClass}&#96;));
1744
+ const outerTitle = titles.find(el => el.textContent === (isDrawer ? '导出评估报告(侧滑)' : '导出评估报告'));
1437
1745
  const innerTitle = titles.find(el => el.textContent === '选择导出字段');
1438
- const outerRoot = outerTitle?.closest('.ant-modal-root');
1746
+ const outerRoot = outerTitle?.closest(outerRootClass.slice(1));
1439
1747
  const innerRoot = innerTitle?.closest('.ant-modal-root');
1440
- const outerBody = outerRoot?.querySelector('.ant-modal-body');
1441
- const outerZ = Number(outerRoot?.querySelector('.ant-modal-wrap')?.style?.zIndex || 0);
1442
- const innerZ = Number(innerRoot?.querySelector('.ant-modal-wrap')?.style?.zIndex || 0);
1748
+ const outerBody = outerRoot?.querySelector(isDrawer ? '.ant-drawer-body' : '.ant-modal-body');
1443
1749
  const hoisted = !!(innerRoot && outerBody && !outerBody.contains(innerRoot));
1444
- setInfo({ hoisted, outerZ, innerZ });
1750
+ setInfo({ hoisted });
1445
1751
  }, 50);
1446
1752
  return () => window.clearTimeout(id);
1447
- }, [open]);
1753
+ }, [open, isDrawer]);
1448
1754
 
1449
1755
  if (!info) {
1450
1756
  return <Text type="secondary">打开内层后显示挂载探针</Text>;
1451
1757
  }
1452
1758
 
1453
1759
  return (
1454
- <Space direction="vertical" size={4}>
1455
- <span>
1456
- 内层挂载位置:
1457
- <Tag color={info.hoisted ? 'success' : 'error'} style={{ marginLeft: 8 }}>
1458
- {info.hoisted ? '外层 modal-root 外侧' : '异常'}
1459
- </Tag>
1460
- </span>
1461
- <Text type="secondary">zIndex 外层 {info.outerZ} / 内层 {info.innerZ}(antd 管理)</Text>
1462
- </Space>
1760
+ <Tag color={info.hoisted ? 'success' : 'error'}>
1761
+ 内层 Modal {info.hoisted ? '已 hoist 到外层外侧' : '挂载异常'}
1762
+ </Tag>
1463
1763
  );
1464
1764
  };
1465
1765
 
1466
- const ExportFieldPicker = () => {
1766
+ const ExportFieldPicker = ({ isDrawer }) => {
1467
1767
  const [innerOpen, setInnerOpen] = useState(false);
1468
1768
  const [checked, setChecked] = useState(['basic', 'scores', 'notes']);
1469
1769
 
1470
1770
  return (
1471
1771
  <div>
1472
1772
  <Paragraph>
1473
- 已选择本批 <Text strong>8</Text> 位候选人。导出前可勾选需要写入 PDF 的字段;内层声明式{' '}
1474
- <Text code>Modal</Text> 会自动 hoist,无需手动指定容器。
1773
+ 已选择本批 <Text strong>8</Text> 位候选人。内层始终为声明式 Modal;外层当前为{' '}
1774
+ <Text code>{isDrawer ? 'Drawer' : 'Modal'}</Text>。
1475
1775
  </Paragraph>
1476
1776
  <Space>
1477
1777
  <Button type="primary" onClick={() => setInnerOpen(true)}>
@@ -1480,7 +1780,7 @@ const ExportFieldPicker = () => {
1480
1780
  <Text type="secondary">已选 {checked.length} 项</Text>
1481
1781
  </Space>
1482
1782
  <div style={{ marginTop: 12 }}>
1483
- <NestProbe open={innerOpen} />
1783
+ <NestProbe open={innerOpen} isDrawer={isDrawer} />
1484
1784
  </div>
1485
1785
  <Modal
1486
1786
  title="选择导出字段"
@@ -1506,47 +1806,63 @@ const ExportFieldPicker = () => {
1506
1806
 
1507
1807
  const NestedModalExample = () => {
1508
1808
  const modal = useModal();
1809
+ const drawer = useDrawer();
1810
+ const [mode, setMode] = useState('modal');
1811
+ const isDrawer = mode === 'drawer';
1509
1812
 
1510
1813
  return (
1511
1814
  <Space direction="vertical" style={{ width: '100%' }} size="middle">
1815
+ <Space align="center" wrap>
1816
+ <Text type="secondary">打开方式</Text>
1817
+ <Radio.Group
1818
+ value={mode}
1819
+ optionType="button"
1820
+ size="small"
1821
+ options={[
1822
+ { label: 'Modal', value: 'modal' },
1823
+ { label: 'Drawer', value: 'drawer' }
1824
+ ]}
1825
+ onChange={e => setMode(e.target.value)}
1826
+ />
1827
+ </Space>
1512
1828
  <Button
1513
1829
  type="primary"
1514
1830
  onClick={() => {
1515
- modal({
1516
- title: '导出评估报告',
1831
+ const api = isDrawer ? drawer : modal;
1832
+ api({
1833
+ title: isDrawer ? '导出评估报告(侧滑)' : '导出评估报告',
1517
1834
  size: 'default',
1518
- children: <ExportFieldPicker />,
1835
+ children: <ExportFieldPicker isDrawer={isDrawer} />,
1519
1836
  onConfirm: () => message.success('报告已进入生成队列'),
1520
1837
  confirmText: '开始导出'
1521
1838
  });
1522
1839
  }}
1523
1840
  >
1524
- 导出本批评估(嵌套弹窗)
1841
+ 导出本批评估(嵌套弹层)
1525
1842
  </Button>
1526
- <Text type="secondary">外层 useModal + 内层声明式 Modal:关闭内层不影响外层。</Text>
1843
+ <Text type="secondary">外层 Modal / Drawer 命令式 + 内层声明式 Modal 嵌套 hoist。</Text>
1527
1844
  </Space>
1528
1845
  );
1529
1846
  };
1530
1847
 
1531
- const BaseExample = () => (
1848
+ render(
1532
1849
  <App>
1850
+ <DrawerContextHolder />
1533
1851
  <NestedModalExample />
1534
1852
  </App>
1535
1853
  );
1536
1854
 
1537
- render(<BaseExample />);
1538
-
1539
1855
  ```
1540
1856
 
1541
- - FormInfo FormModal + renderModal
1542
- - 超长表单:用 @kne/react-modal 重写 renderModal,验收 title/footer 固定与 body SimpleBar 滚动(可关 bodyScroll 对比)
1857
+ - FormModal + renderModal
1858
+ - Modal / Drawer 切换:超长表单,createModalRender / createDrawerRender
1543
1859
  - _ReactModal(@kne/current-lib_react-modal)[import * as _ReactModal from "@kne/react-modal"],(@kne/current-lib_react-modal/dist/index.css),_FormInfo(@kne/form-info)[import * as _FormInfo from "@kne/form-info"],(@kne/form-info/dist/index.css),antd(antd)
1544
1860
 
1545
1861
  ```jsx
1546
- const { createModalRender } = _ReactModal;
1862
+ const { createModalRender, createDrawerRender, DrawerContextHolder } = _ReactModal;
1547
1863
  const { default: FormInfo, FormModal, Input, TextArea } = _FormInfo;
1548
- const { Button, Space, Typography, App, message, Switch, Flex } = antd;
1549
- const { useState, useMemo } = React;
1864
+ const { Button, Space, Typography, App, message, Switch, Flex, Radio } = antd;
1865
+ const { useState, useMemo, useEffect } = React;
1550
1866
  const { Text, Paragraph } = Typography;
1551
1867
 
1552
1868
  const SECTION_DEFS = [
@@ -1668,13 +1984,19 @@ const LongFormFields = () => (
1668
1984
  const FormInfoModalExample = () => {
1669
1985
  const [open, setOpen] = useState(false);
1670
1986
  const [bodyScroll, setBodyScroll] = useState(true);
1987
+ const [mode, setMode] = useState('modal');
1988
+ const isDrawer = mode === 'drawer';
1671
1989
  const initialData = useMemo(() => buildInitialData(), []);
1672
1990
 
1673
- const renderModalBase = createModalRender({
1674
- footerButtons: [],
1675
- bodyScroll: true,
1676
- size: 'large'
1677
- });
1991
+ useEffect(() => {
1992
+ if (open) {
1993
+ setOpen(false);
1994
+ }
1995
+ }, [mode]);
1996
+
1997
+ const renderModalBase = isDrawer
1998
+ ? createDrawerRender({ placement: 'right', footerButtons: [], bodyScroll: true, size: 'large' })
1999
+ : createModalRender({ footerButtons: [], bodyScroll: true, size: 'large' });
1678
2000
 
1679
2001
  const renderModal = ({
1680
2002
  formProps,
@@ -1697,6 +2019,19 @@ const FormInfoModalExample = () => {
1697
2019
 
1698
2020
  return (
1699
2021
  <Space direction="vertical" style={{ width: '100%' }}>
2022
+ <Space align="center" wrap>
2023
+ <Text type="secondary">打开方式</Text>
2024
+ <Radio.Group
2025
+ value={mode}
2026
+ optionType="button"
2027
+ size="small"
2028
+ options={[
2029
+ { label: 'Modal', value: 'modal' },
2030
+ { label: 'Drawer', value: 'drawer' }
2031
+ ]}
2032
+ onChange={e => setMode(e.target.value)}
2033
+ />
2034
+ </Space>
1700
2035
  <Space wrap>
1701
2036
  <Button type="primary" onClick={() => setOpen(true)}>
1702
2037
  打开深度评估表单(FormModal)
@@ -1707,11 +2042,11 @@ const FormInfoModalExample = () => {
1707
2042
  </Space>
1708
2043
  </Space>
1709
2044
  <Text type="secondary">
1710
- createModalRender 注入 Modal 默认 props;form-info 宿主字段与 onCancel→onClose 在 renderModal 内映射。
2045
+ createModalRender / createDrawerRender 注入默认 props;切换 Modal / Drawer 对比 form-info 宿主集成。
1711
2046
  </Text>
1712
2047
 
1713
2048
  <FormModal
1714
- title="候选人深度评估(超长表单)"
2049
+ title={isDrawer ? '候选人深度评估(侧滑)' : '候选人深度评估(超长表单)'}
1715
2050
  open={open}
1716
2051
  onCancel={() => setOpen(false)}
1717
2052
  renderModal={renderModal}
@@ -1732,35 +2067,43 @@ const FormInfoModalExample = () => {
1732
2067
  );
1733
2068
  };
1734
2069
 
1735
- const BaseExample = () => (
2070
+ render(
1736
2071
  <App>
2072
+ <DrawerContextHolder />
1737
2073
  <FormInfoModalExample />
1738
2074
  </App>
1739
2075
  );
1740
2076
 
1741
- render(<BaseExample />);
1742
-
1743
2077
  ```
1744
2078
 
1745
2079
  - FormStepsModal + renderModal
1746
- - 分步表单:FormStepsModal 在 modalProps 中传 renderModal;步骤内 FormInfo gap + Flex 垂直间距
2080
+ - Modal / Drawer 切换:分步表单,步骤内 FormInfo gap + Flex 垂直间距
1747
2081
  - _ReactModal(@kne/current-lib_react-modal)[import * as _ReactModal from "@kne/react-modal"],(@kne/current-lib_react-modal/dist/index.css),_FormInfo(@kne/form-info)[import * as _FormInfo from "@kne/form-info"],(@kne/form-info/dist/index.css),antd(antd)
1748
2082
 
1749
2083
  ```jsx
1750
- const { createModalRender, modalClassNames } = _ReactModal;
2084
+ const { createModalRender, createDrawerRender, modalClassNames, DrawerContextHolder } = _ReactModal;
1751
2085
  const { default: FormInfo, FormStepsModal, List, Input, TextArea } = _FormInfo;
1752
- const { Button, Space, Typography, App, message, Flex } = antd;
1753
- const { useState } = React;
2086
+ const { Button, Space, Typography, App, message, Flex, Radio } = antd;
2087
+ const { useState, useEffect } = React;
1754
2088
  const { Text } = Typography;
1755
2089
 
1756
- const renderStepsModalBase = createModalRender({
1757
- footerButtons: [],
1758
- bodyScroll: true,
1759
- size: 'default',
1760
- className: modalClassNames.stepsForm
1761
- });
2090
+ const renderStepsModalBase = isDrawer =>
2091
+ isDrawer
2092
+ ? createDrawerRender({
2093
+ placement: 'right',
2094
+ footerButtons: [],
2095
+ bodyScroll: true,
2096
+ size: 'default',
2097
+ className: modalClassNames.stepsForm
2098
+ })
2099
+ : createModalRender({
2100
+ footerButtons: [],
2101
+ bodyScroll: true,
2102
+ size: 'default',
2103
+ className: modalClassNames.stepsForm
2104
+ });
1762
2105
 
1763
- const renderStepsModal = ({
2106
+ const renderStepsModal = isDrawer => ({
1764
2107
  formProps,
1765
2108
  saveText,
1766
2109
  autoClose,
@@ -1771,7 +2114,7 @@ const renderStepsModal = ({
1771
2114
  className,
1772
2115
  ...props
1773
2116
  }) =>
1774
- renderStepsModalBase({
2117
+ renderStepsModalBase(isDrawer)({
1775
2118
  ...props,
1776
2119
  className,
1777
2120
  onClose: onCancel,
@@ -1805,15 +2148,34 @@ const STEP_DATA = {
1805
2148
 
1806
2149
  const FormInfoStepsModalExample = () => {
1807
2150
  const [open, setOpen] = useState(false);
2151
+ const [mode, setMode] = useState('modal');
2152
+ const isDrawer = mode === 'drawer';
2153
+
2154
+ useEffect(() => {
2155
+ if (open) {
2156
+ setOpen(false);
2157
+ }
2158
+ }, [mode]);
1808
2159
 
1809
2160
  return (
1810
2161
  <Space direction="vertical" style={{ width: '100%' }}>
2162
+ <Space align="center" wrap>
2163
+ <Text type="secondary">打开方式</Text>
2164
+ <Radio.Group
2165
+ value={mode}
2166
+ optionType="button"
2167
+ size="small"
2168
+ options={[
2169
+ { label: 'Modal', value: 'modal' },
2170
+ { label: 'Drawer', value: 'drawer' }
2171
+ ]}
2172
+ onChange={e => setMode(e.target.value)}
2173
+ />
2174
+ </Space>
1811
2175
  <Button type="primary" onClick={() => setOpen(true)}>
1812
2176
  打开分步评估(FormStepsModal)
1813
2177
  </Button>
1814
- <Text type="secondary">
1815
- 三步完成候选人评估:基本信息 → 维度评分 → 经历与结论。
1816
- </Text>
2178
+ <Text type="secondary">三步完成候选人评估;切换 Modal / Drawer 对比分步表单高度链。</Text>
1817
2179
 
1818
2180
  <FormStepsModal
1819
2181
  autoStep
@@ -1825,10 +2187,10 @@ const FormInfoStepsModalExample = () => {
1825
2187
  }}
1826
2188
  modalProps={{
1827
2189
  open,
1828
- title: '候选人评估(分步)',
2190
+ title: isDrawer ? '候选人评估(侧滑分步)' : '候选人评估(分步)',
1829
2191
  width: 900,
1830
2192
  onCancel: () => setOpen(false),
1831
- renderModal: renderStepsModal
2193
+ renderModal: renderStepsModal(isDrawer)
1832
2194
  }}
1833
2195
  items={[
1834
2196
  {
@@ -1920,14 +2282,13 @@ const FormInfoStepsModalExample = () => {
1920
2282
  );
1921
2283
  };
1922
2284
 
1923
- const BaseExample = () => (
2285
+ render(
1924
2286
  <App>
2287
+ <DrawerContextHolder />
1925
2288
  <FormInfoStepsModalExample />
1926
2289
  </App>
1927
2290
  );
1928
2291
 
1929
- render(<BaseExample />);
1930
-
1931
2292
  ```
1932
2293
 
1933
2294
  ### API
@@ -1986,6 +2347,39 @@ render(<BaseExample />);
1986
2347
  |------|------|------|
1987
2348
  | modal | function | 调用后弹出 Modal;返回 `{ close }`;默认 `zIndex` 为 1100 |
1988
2349
 
2350
+ #### useConfirmModal
2351
+
2352
+ 命令式确认 / 提示弹窗,底层走 antd `App.modal` 的 `confirm` / `info` / `success` / `warning` / `error`。需在 antd `App` 上下文中使用。
2353
+
2354
+ ##### 参数
2355
+
2356
+ | 属性 | 类型 | 默认值 | 描述 |
2357
+ |------|------|--------|------|
2358
+ | type | `confirm` \| `info` \| `success` \| `warning` \| `error` | `confirm` | 调用 `modal[type]` |
2359
+ | title | ReactNode | - | 标题 |
2360
+ | message | ReactNode | - | 正文 |
2361
+ | danger | boolean | false | 为 true 时展示语义图标,确认钮危险色 |
2362
+ | confirmType | `info` \| `warning` \| `error` \| `success` | `info` | `type=confirm` 时图标语义 |
2363
+ | icon | ReactNode | - | 自定义图标,覆盖默认 |
2364
+ | onConfirm | function | - | 映射 antd `onOk` |
2365
+ | onCancel | function | - | 映射 antd `onCancel` |
2366
+ | confirmText | ReactNode | - | 映射 `okText` |
2367
+ | cancelText | ReactNode | - | 映射 `cancelText` |
2368
+ | onClose | function | - | 调用 `close()` 时触发 |
2369
+ | maskClosable | boolean | false | 默认不可点蒙层关闭 |
2370
+ | getContainer | HTMLElement \| function | - | 嵌套时挂到外层 modal 外侧 |
2371
+ | afterClose / zIndex / wrapClassName | - | - | 透传;默认 `zIndex` 1100 |
2372
+
2373
+ 其余未列出参数按 antd Modal.confirm 习惯透传。
2374
+
2375
+ ##### 返回值
2376
+
2377
+ | 属性 | 类型 | 描述 |
2378
+ |------|------|------|
2379
+ | confirmModal | function | 调用后弹出确认框;返回 `{ close }` |
2380
+
2381
+ 与 `useModal` 对比:专用于短文案确认 / 提示,桌面居中窄宽(约 400px),无 SimpleBar body。
2382
+
1989
2383
  #### CSS 变量
1990
2384
 
1991
2385
  挂在 `.modal` / `.modal-outer` 上,可业务覆盖。
@@ -2005,33 +2399,33 @@ render(<BaseExample />);
2005
2399
  | `--kne-modal-content-min-height` | content 侧 min |
2006
2400
  | `--kne-modal-content-width` | 内容宽度契约(扣 horizontal padding) |
2007
2401
 
2008
- #### 布局组合
2402
+ #### 布局组合(Modal / Drawer 共用)
2009
2403
 
2010
- Tabs / N 列分栏等复杂内容区须 **`bodyScroll={false}`**,由 `ModalScrollRegion`(SimpleBar)分区滚动,避免与 Modal body 双层 SimpleBar。
2404
+ Tabs / N 列分栏等复杂内容区须 **`bodyScroll={false}`**,由 `ScrollRegion`(SimpleBar)分区滚动,避免与弹层 body 双层 SimpleBar。
2011
2405
 
2012
- ##### ModalTabsLayout
2406
+ ##### TabsLayout
2013
2407
 
2014
- antd `Tabs` 封装:顶栏占 title 位(配合无 `title` 的 Modal)、antd6 高度链、`destroyOnHidden` 默认 true。其余 props 透传 Tabs。
2408
+ antd `Tabs` 封装:顶栏占 title 位(配合无 `title` 的弹层)、antd6 高度链、`destroyOnHidden` 默认 true。其余 props 透传 Tabs。
2015
2409
 
2016
- ##### ModalColumnsLayout
2410
+ ##### ColumnsLayout
2017
2411
 
2018
2412
  固定 N 列 flex 分栏。`widths` 与 `children` 等长(如 `['36%', '1fr']`、`['200px', '1fr', '280px']`);`'1fr'` 占剩余宽度。
2019
2413
 
2020
- ##### ModalScrollRegion
2414
+ ##### ScrollRegion
2021
2415
 
2022
2416
  单块 SimpleBar 滚动区,用于 Tab 面板或每一列。首列/末列背景由父级 `:first-child` / `:last-child` 选中。
2023
2417
 
2024
2418
  - `inset`(默认 `false`):为 `true` 时内容区增加 `16px 20px` 内边距(适合详情、概览等单列内容)
2025
2419
  - 分栏列表左列通常保持 `inset={false}`,由列表项自行控制间距
2026
2420
 
2027
- Tabs / 分栏弹窗须 **`bodyScroll={false}`**,此时 Modal 默认 **noPadding**(内容贴边),以便分栏铺满 body;需要外层留白时可传 **`noPadding={false}`**。
2421
+ Tabs / 分栏弹层须 **`bodyScroll={false}`**,此时默认 **noPadding**(内容贴边),以便分栏铺满 body;需要外层留白时可传 **`noPadding={false}`**。
2028
2422
 
2029
2423
  ##### modalClassNames
2030
2424
 
2031
2425
  | 常量 | 值 | 用途 |
2032
2426
  |------|-----|------|
2033
- | `stepsForm` | `react-modal-steps-form` | 分步弹窗挂 Modal `className`,去横向溢出 |
2034
- | `splitter` | `react-modal-splitter` | antd Splitter 在 Modal 内的高度链 |
2427
+ | `stepsForm` | `react-modal-steps-form` | 分步弹窗挂 `className`,去横向溢出 |
2428
+ | `splitter` | `react-modal-splitter` | antd Splitter 在弹层内的高度链 |
2035
2429
 
2036
2430
  ##### createModalRender
2037
2431
 
@@ -2048,3 +2442,38 @@ createModalRender(modalDefaults) => (hostProps) => Modal
2048
2442
  | 子内容自带 Footer | `{ footerButtons: [] }` |
2049
2443
  | 长内容 | `{ footerButtons: [], bodyScroll: true, size: 'large' }` |
2050
2444
  | 分步弹窗 | `{ footerButtons: [], bodyScroll: true, size: 'default', className: modalClassNames.stepsForm }` |
2445
+
2446
+ #### Drawer
2447
+
2448
+ 声明式侧滑层。基于 antd Drawer,关闭请使用 `onClose`(内部映射 antd `onClose`)。API 与 Modal 对齐处不再重复;差异如下。
2449
+
2450
+ ##### 属性(差异与补充)
2451
+
2452
+ | 属性 | 类型 | 默认值 | 描述 |
2453
+ |------|------|--------|------|
2454
+ | placement | `left` \| `right` \| `top` \| `bottom` | `right` | 滑出方向;`size` 映射 width(left/right)或 height(top/bottom) |
2455
+ | size | `small` \| `default` \| `large` | `default` | 600 / 1000 / min(vw−64, 1500) px |
2456
+ | 移动端 | - | 侧滑全宽 | left/right → 100vw;top/bottom → 100vh(非 Modal 式全屏居中) |
2457
+
2458
+ 其余 `open` / `onClose` / `title` / `children` / `footer` / `footerButtons` / `bodyScroll` / `noPadding` / `closable` / `maskClosable` / `getContainer` 等与 Modal 相同。
2459
+
2460
+ **FormModal**:传入的 `modalRender` 会在 Drawer 内注入 `ModalForm`,包裹整块 chrome(title / body / footer,与 Modal 的 panel 语义一致);footer 内 Submit/Cancel 需在 Form 上下文内。
2461
+
2462
+ 嵌套时默认挂到外层 `.ant-drawer-root` 外侧。
2463
+
2464
+ #### useDrawer
2465
+
2466
+ 命令式打开 Drawer,参数同 Drawer。须在 antd `App` 内挂载 **`<DrawerContextHolder />`**(对标 antd 内置 `ModalContextHolder`),签名与 `useModal` 相同:`const drawer = useDrawer(); drawer(props) → { close }`。
2467
+
2468
+ #### Drawer CSS 变量
2469
+
2470
+ 挂在 `.drawer` / `.drawer-outer` 上;命名 `--kne-drawer-*`,语义与 Modal 对齐(无 viewport gutter,body 占满面板高度链)。`.drawer-outer` 内 bridge `--kne-modal-content-height` 等,供共用 `ScrollRegion` / `TabsLayout` 高度链。
2471
+
2472
+ #### createDrawerRender
2473
+
2474
+ ```ts
2475
+ createDrawerRender(drawerDefaults) => (hostProps) => Drawer
2476
+ ```
2477
+
2478
+ 单参数合并渲染;宿主字段映射由 `renderModal` 回调内完成(同 `createModalRender`)。
2479
+