@teamix-evo/ui 0.1.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.
- package/LICENSE +21 -0
- package/README.md +336 -0
- package/_data.json +12 -0
- package/manifest.json +1688 -0
- package/package.json +90 -0
- package/src/components/accordion/accordion.meta.md +87 -0
- package/src/components/accordion/accordion.stories.tsx +67 -0
- package/src/components/accordion/accordion.tsx +58 -0
- package/src/components/affix/affix.meta.md +80 -0
- package/src/components/affix/affix.stories.tsx +57 -0
- package/src/components/affix/affix.tsx +97 -0
- package/src/components/alert/alert.meta.md +101 -0
- package/src/components/alert/alert.stories.tsx +93 -0
- package/src/components/alert/alert.tsx +132 -0
- package/src/components/alert-dialog/alert-dialog.meta.md +107 -0
- package/src/components/alert-dialog/alert-dialog.stories.tsx +81 -0
- package/src/components/alert-dialog/alert-dialog.tsx +136 -0
- package/src/components/anchor/anchor.meta.md +87 -0
- package/src/components/anchor/anchor.stories.tsx +74 -0
- package/src/components/anchor/anchor.tsx +130 -0
- package/src/components/app/app.meta.md +86 -0
- package/src/components/app/app.stories.tsx +62 -0
- package/src/components/app/app.tsx +58 -0
- package/src/components/aspect-ratio/aspect-ratio.meta.md +81 -0
- package/src/components/aspect-ratio/aspect-ratio.stories.tsx +59 -0
- package/src/components/aspect-ratio/aspect-ratio.tsx +22 -0
- package/src/components/auto-complete/auto-complete.meta.md +102 -0
- package/src/components/auto-complete/auto-complete.stories.tsx +93 -0
- package/src/components/auto-complete/auto-complete.tsx +205 -0
- package/src/components/avatar/avatar.meta.md +94 -0
- package/src/components/avatar/avatar.stories.tsx +80 -0
- package/src/components/avatar/avatar.tsx +126 -0
- package/src/components/badge/badge.meta.md +119 -0
- package/src/components/badge/badge.stories.tsx +153 -0
- package/src/components/badge/badge.tsx +210 -0
- package/src/components/breadcrumb/breadcrumb.meta.md +107 -0
- package/src/components/breadcrumb/breadcrumb.stories.tsx +84 -0
- package/src/components/breadcrumb/breadcrumb.tsx +122 -0
- package/src/components/button/button.meta.md +98 -0
- package/src/components/button/button.stories.tsx +235 -0
- package/src/components/button/button.tsx +160 -0
- package/src/components/button-group/button-group.meta.md +92 -0
- package/src/components/button-group/button-group.stories.tsx +90 -0
- package/src/components/button-group/button-group.tsx +75 -0
- package/src/components/calendar/calendar.meta.md +118 -0
- package/src/components/calendar/calendar.stories.tsx +68 -0
- package/src/components/calendar/calendar.tsx +107 -0
- package/src/components/card/card.meta.md +117 -0
- package/src/components/card/card.stories.tsx +112 -0
- package/src/components/card/card.tsx +222 -0
- package/src/components/carousel/carousel.meta.md +117 -0
- package/src/components/carousel/carousel.stories.tsx +84 -0
- package/src/components/carousel/carousel.tsx +224 -0
- package/src/components/cascader/cascader.meta.md +110 -0
- package/src/components/cascader/cascader.stories.tsx +108 -0
- package/src/components/cascader/cascader.tsx +198 -0
- package/src/components/checkbox/checkbox.meta.md +99 -0
- package/src/components/checkbox/checkbox.stories.tsx +130 -0
- package/src/components/checkbox/checkbox.tsx +125 -0
- package/src/components/collapsible/collapsible.meta.md +80 -0
- package/src/components/collapsible/collapsible.stories.tsx +35 -0
- package/src/components/collapsible/collapsible.tsx +18 -0
- package/src/components/color-picker/color-picker.meta.md +84 -0
- package/src/components/color-picker/color-picker.stories.tsx +80 -0
- package/src/components/color-picker/color-picker.tsx +160 -0
- package/src/components/combobox/combobox.meta.md +93 -0
- package/src/components/combobox/combobox.stories.tsx +55 -0
- package/src/components/combobox/combobox.tsx +130 -0
- package/src/components/command/command.meta.md +104 -0
- package/src/components/command/command.stories.tsx +59 -0
- package/src/components/command/command.tsx +147 -0
- package/src/components/context-menu/context-menu.meta.md +90 -0
- package/src/components/context-menu/context-menu.stories.tsx +46 -0
- package/src/components/context-menu/context-menu.tsx +191 -0
- package/src/components/data-table/data-table.meta.md +149 -0
- package/src/components/data-table/data-table.stories.tsx +125 -0
- package/src/components/data-table/data-table.tsx +185 -0
- package/src/components/date-picker/date-picker.meta.md +106 -0
- package/src/components/date-picker/date-picker.stories.tsx +58 -0
- package/src/components/date-picker/date-picker.tsx +156 -0
- package/src/components/descriptions/descriptions.meta.md +78 -0
- package/src/components/descriptions/descriptions.stories.tsx +60 -0
- package/src/components/descriptions/descriptions.tsx +129 -0
- package/src/components/dialog/dialog.meta.md +105 -0
- package/src/components/dialog/dialog.stories.tsx +93 -0
- package/src/components/dialog/dialog.tsx +128 -0
- package/src/components/drawer/drawer.meta.md +96 -0
- package/src/components/drawer/drawer.stories.tsx +54 -0
- package/src/components/drawer/drawer.tsx +114 -0
- package/src/components/dropdown-menu/dropdown-menu.meta.md +103 -0
- package/src/components/dropdown-menu/dropdown-menu.stories.tsx +112 -0
- package/src/components/dropdown-menu/dropdown-menu.tsx +195 -0
- package/src/components/empty/empty.meta.md +81 -0
- package/src/components/empty/empty.stories.tsx +46 -0
- package/src/components/empty/empty.tsx +47 -0
- package/src/components/field/field.meta.md +116 -0
- package/src/components/field/field.stories.tsx +117 -0
- package/src/components/field/field.tsx +164 -0
- package/src/components/flex/flex.meta.md +94 -0
- package/src/components/flex/flex.stories.tsx +112 -0
- package/src/components/flex/flex.tsx +122 -0
- package/src/components/float-button/float-button.meta.md +87 -0
- package/src/components/float-button/float-button.stories.tsx +78 -0
- package/src/components/float-button/float-button.tsx +143 -0
- package/src/components/form/form.meta.md +131 -0
- package/src/components/form/form.stories.tsx +122 -0
- package/src/components/form/form.tsx +194 -0
- package/src/components/grid/grid.meta.md +87 -0
- package/src/components/grid/grid.stories.tsx +99 -0
- package/src/components/grid/grid.tsx +130 -0
- package/src/components/hover-card/hover-card.meta.md +92 -0
- package/src/components/hover-card/hover-card.stories.tsx +68 -0
- package/src/components/hover-card/hover-card.tsx +29 -0
- package/src/components/image/image.meta.md +94 -0
- package/src/components/image/image.stories.tsx +55 -0
- package/src/components/image/image.tsx +138 -0
- package/src/components/input/input.meta.md +109 -0
- package/src/components/input/input.stories.tsx +117 -0
- package/src/components/input/input.tsx +213 -0
- package/src/components/input-group/input-group.meta.md +92 -0
- package/src/components/input-group/input-group.stories.tsx +88 -0
- package/src/components/input-group/input-group.tsx +107 -0
- package/src/components/input-number/input-number.meta.md +91 -0
- package/src/components/input-number/input-number.stories.tsx +87 -0
- package/src/components/input-number/input-number.tsx +210 -0
- package/src/components/input-otp/input-otp.meta.md +105 -0
- package/src/components/input-otp/input-otp.stories.tsx +65 -0
- package/src/components/input-otp/input-otp.tsx +97 -0
- package/src/components/item/item.meta.md +116 -0
- package/src/components/item/item.stories.tsx +113 -0
- package/src/components/item/item.tsx +171 -0
- package/src/components/kbd/kbd.meta.md +85 -0
- package/src/components/kbd/kbd.stories.tsx +70 -0
- package/src/components/kbd/kbd.tsx +81 -0
- package/src/components/label/label.meta.md +91 -0
- package/src/components/label/label.stories.tsx +87 -0
- package/src/components/label/label.tsx +66 -0
- package/src/components/masonry/masonry.meta.md +85 -0
- package/src/components/masonry/masonry.stories.tsx +66 -0
- package/src/components/masonry/masonry.tsx +59 -0
- package/src/components/mentions/mentions.meta.md +89 -0
- package/src/components/mentions/mentions.stories.tsx +75 -0
- package/src/components/mentions/mentions.tsx +237 -0
- package/src/components/menubar/menubar.meta.md +100 -0
- package/src/components/menubar/menubar.stories.tsx +81 -0
- package/src/components/menubar/menubar.tsx +232 -0
- package/src/components/native-select/native-select.meta.md +88 -0
- package/src/components/native-select/native-select.stories.tsx +80 -0
- package/src/components/native-select/native-select.tsx +54 -0
- package/src/components/navigation-menu/navigation-menu.meta.md +108 -0
- package/src/components/navigation-menu/navigation-menu.stories.tsx +112 -0
- package/src/components/navigation-menu/navigation-menu.tsx +125 -0
- package/src/components/notification/notification.meta.md +91 -0
- package/src/components/notification/notification.stories.tsx +96 -0
- package/src/components/notification/notification.tsx +84 -0
- package/src/components/pagination/pagination.meta.md +127 -0
- package/src/components/pagination/pagination.stories.tsx +62 -0
- package/src/components/pagination/pagination.tsx +285 -0
- package/src/components/popconfirm/popconfirm.meta.md +109 -0
- package/src/components/popconfirm/popconfirm.stories.tsx +76 -0
- package/src/components/popconfirm/popconfirm.tsx +134 -0
- package/src/components/popover/popover.meta.md +97 -0
- package/src/components/popover/popover.stories.tsx +82 -0
- package/src/components/popover/popover.tsx +55 -0
- package/src/components/progress/progress.meta.md +86 -0
- package/src/components/progress/progress.stories.tsx +75 -0
- package/src/components/progress/progress.tsx +195 -0
- package/src/components/radio-group/radio-group.meta.md +103 -0
- package/src/components/radio-group/radio-group.stories.tsx +77 -0
- package/src/components/radio-group/radio-group.tsx +78 -0
- package/src/components/rate/rate.meta.md +87 -0
- package/src/components/rate/rate.stories.tsx +81 -0
- package/src/components/rate/rate.tsx +153 -0
- package/src/components/resizable/resizable.meta.md +92 -0
- package/src/components/resizable/resizable.stories.tsx +104 -0
- package/src/components/resizable/resizable.tsx +56 -0
- package/src/components/result/result.meta.md +90 -0
- package/src/components/result/result.stories.tsx +71 -0
- package/src/components/result/result.tsx +91 -0
- package/src/components/scroll-area/scroll-area.meta.md +84 -0
- package/src/components/scroll-area/scroll-area.stories.tsx +41 -0
- package/src/components/scroll-area/scroll-area.tsx +51 -0
- package/src/components/segmented/segmented.meta.md +103 -0
- package/src/components/segmented/segmented.stories.tsx +101 -0
- package/src/components/segmented/segmented.tsx +138 -0
- package/src/components/select/select.meta.md +110 -0
- package/src/components/select/select.stories.tsx +100 -0
- package/src/components/select/select.tsx +188 -0
- package/src/components/separator/separator.meta.md +74 -0
- package/src/components/separator/separator.stories.tsx +71 -0
- package/src/components/separator/separator.tsx +104 -0
- package/src/components/sheet/sheet.meta.md +97 -0
- package/src/components/sheet/sheet.stories.tsx +82 -0
- package/src/components/sheet/sheet.tsx +139 -0
- package/src/components/sidebar/sidebar.meta.md +131 -0
- package/src/components/sidebar/sidebar.stories.tsx +82 -0
- package/src/components/sidebar/sidebar.tsx +351 -0
- package/src/components/skeleton/skeleton.meta.md +95 -0
- package/src/components/skeleton/skeleton.stories.tsx +79 -0
- package/src/components/skeleton/skeleton.tsx +144 -0
- package/src/components/slider/slider.meta.md +94 -0
- package/src/components/slider/slider.stories.tsx +69 -0
- package/src/components/slider/slider.tsx +86 -0
- package/src/components/sonner/sonner.meta.md +96 -0
- package/src/components/sonner/sonner.stories.tsx +91 -0
- package/src/components/sonner/sonner.tsx +40 -0
- package/src/components/space/space.meta.md +94 -0
- package/src/components/space/space.stories.tsx +94 -0
- package/src/components/space/space.tsx +106 -0
- package/src/components/spinner/spinner.meta.md +76 -0
- package/src/components/spinner/spinner.stories.tsx +71 -0
- package/src/components/spinner/spinner.tsx +64 -0
- package/src/components/statistic/statistic.meta.md +99 -0
- package/src/components/statistic/statistic.stories.tsx +71 -0
- package/src/components/statistic/statistic.tsx +197 -0
- package/src/components/steps/steps.meta.md +102 -0
- package/src/components/steps/steps.stories.tsx +75 -0
- package/src/components/steps/steps.tsx +170 -0
- package/src/components/switch/switch.meta.md +92 -0
- package/src/components/switch/switch.stories.tsx +75 -0
- package/src/components/switch/switch.tsx +101 -0
- package/src/components/table/table.meta.md +95 -0
- package/src/components/table/table.stories.tsx +75 -0
- package/src/components/table/table.tsx +122 -0
- package/src/components/tabs/tabs.meta.md +98 -0
- package/src/components/tabs/tabs.stories.tsx +70 -0
- package/src/components/tabs/tabs.tsx +119 -0
- package/src/components/tag/tag.meta.md +94 -0
- package/src/components/tag/tag.stories.tsx +77 -0
- package/src/components/tag/tag.tsx +185 -0
- package/src/components/textarea/textarea.meta.md +83 -0
- package/src/components/textarea/textarea.stories.tsx +63 -0
- package/src/components/textarea/textarea.tsx +113 -0
- package/src/components/time-picker/time-picker.meta.md +83 -0
- package/src/components/time-picker/time-picker.stories.tsx +59 -0
- package/src/components/time-picker/time-picker.tsx +94 -0
- package/src/components/timeline/timeline.meta.md +102 -0
- package/src/components/timeline/timeline.stories.tsx +104 -0
- package/src/components/timeline/timeline.tsx +147 -0
- package/src/components/toggle/toggle.meta.md +88 -0
- package/src/components/toggle/toggle.stories.tsx +66 -0
- package/src/components/toggle/toggle.tsx +53 -0
- package/src/components/toggle-group/toggle-group.meta.md +90 -0
- package/src/components/toggle-group/toggle-group.stories.tsx +83 -0
- package/src/components/toggle-group/toggle-group.tsx +78 -0
- package/src/components/tooltip/tooltip.meta.md +99 -0
- package/src/components/tooltip/tooltip.stories.tsx +71 -0
- package/src/components/tooltip/tooltip.tsx +93 -0
- package/src/components/tour/tour.meta.md +116 -0
- package/src/components/tour/tour.stories.tsx +66 -0
- package/src/components/tour/tour.tsx +242 -0
- package/src/components/transfer/transfer.meta.md +90 -0
- package/src/components/transfer/transfer.stories.tsx +68 -0
- package/src/components/transfer/transfer.tsx +251 -0
- package/src/components/tree/tree.meta.md +111 -0
- package/src/components/tree/tree.stories.tsx +109 -0
- package/src/components/tree/tree.tsx +367 -0
- package/src/components/tree-select/tree-select.meta.md +100 -0
- package/src/components/tree-select/tree-select.stories.tsx +80 -0
- package/src/components/tree-select/tree-select.tsx +171 -0
- package/src/components/typography/typography.meta.md +102 -0
- package/src/components/typography/typography.stories.tsx +115 -0
- package/src/components/typography/typography.tsx +245 -0
- package/src/components/upload/upload.meta.md +111 -0
- package/src/components/upload/upload.stories.tsx +75 -0
- package/src/components/upload/upload.tsx +265 -0
- package/src/components/watermark/watermark.meta.md +95 -0
- package/src/components/watermark/watermark.stories.tsx +78 -0
- package/src/components/watermark/watermark.tsx +165 -0
- package/src/utils/cn.ts +6 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: upload
|
|
3
|
+
name: Upload
|
|
4
|
+
type: component
|
|
5
|
+
category: form
|
|
6
|
+
since: 0.1.0
|
|
7
|
+
package: "@teamix-evo/ui"
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Upload
|
|
11
|
+
|
|
12
|
+
文件上传 — antd 独有补足。支持**点击 + 拖拽**双模式(`dragger`)、多文件、accept 过滤、`maxCount` / `maxSize` 校验、内置文件列表(显示文件名 / 进度 / 移除按钮)。**只负责前端选择与列表呈现 — 实际上传请求由消费方在 `onChange` 中发起**(组件不内置 XHR)。
|
|
13
|
+
|
|
14
|
+
## When to use
|
|
15
|
+
|
|
16
|
+
- 表单中的附件上传(简历、合同、头像、产品图)
|
|
17
|
+
- 拖拽区域批量导入(数据文件、图片)
|
|
18
|
+
- 需要展示上传进度时,通过受控 `fileList` + `status="uploading"` + `percent` 渲染
|
|
19
|
+
|
|
20
|
+
## When NOT to use
|
|
21
|
+
|
|
22
|
+
- 仅需读取本地图片用于客户端预览 → 直接用 `<input type="file">`
|
|
23
|
+
- 服务端推送式同步 → 不在本组件职责内
|
|
24
|
+
|
|
25
|
+
<!-- auto:props:begin -->
|
|
26
|
+
| 名称 | 类型 | 默认值 | 必填 | 说明 |
|
|
27
|
+
| --- | --- | --- | --- | --- |
|
|
28
|
+
| `fileList` | `UploadFileItem[]` | – | – | 已上传文件列表(受控)— 不传则组件内部维护本地列表(限制少,通常配 onChange)。 |
|
|
29
|
+
| `accept` | `string` | – | – | 文件接受类型(等价 input.accept) — e.g. `image/*`, `.pdf,.docx`。 |
|
|
30
|
+
| `multiple` | `boolean` | `false` | – | 是否支持多文件选择(antd `multiple` 并集)。 |
|
|
31
|
+
| `maxCount` | `number` | – | – | 最大文件数(超出则忽略,触发 `onExceed`)。 |
|
|
32
|
+
| `maxSize` | `number` | – | – | 单文件最大字节数(超出则忽略,触发 `onExceed`)。 |
|
|
33
|
+
| `dragger` | `boolean` | `false` | – | 启用拖拽上传(antd `Dragger` 并集) — 显示大块虚线区,可点击亦可拖拽。 |
|
|
34
|
+
| `disabled` | `boolean` | – | – | 禁用上传(已选文件不可删除)。 |
|
|
35
|
+
| `showFileList` | `boolean` | `true` | – | 是否显示文件列表(antd `showUploadList` 并集)。 |
|
|
36
|
+
| `onChange` | `(next: UploadFileItem[]) => void` | – | – | 文件被添加(用户选择 / 拖入)时的回调 — 返回更新后的列表。 |
|
|
37
|
+
| `onRemove` | `(file: UploadFileItem) => void` | – | – | 文件被移除时的回调。 |
|
|
38
|
+
| `onExceed` | `(rejected: File[], reason: 'count' \| 'size') => void` | – | – | 超出 maxCount / maxSize 时的回调。 |
|
|
39
|
+
| `children` | `React.ReactNode` | – | – | 触发器自定义内容(非 dragger 模式下渲染于 Button 内)。 |
|
|
40
|
+
| `className` | `string` | – | – | – |
|
|
41
|
+
<!-- auto:props:end -->
|
|
42
|
+
|
|
43
|
+
<!-- auto:deps:begin -->
|
|
44
|
+
### 同库依赖
|
|
45
|
+
|
|
46
|
+
> `teamix-evo ui add upload` 时,以下 entry 会被自动连带安装(无需手动 add)。
|
|
47
|
+
|
|
48
|
+
| Entry | 类型 | 描述 |
|
|
49
|
+
| --- | --- | --- |
|
|
50
|
+
| `cn` | util | Tailwind className 合并工具(clsx + tailwind-merge) |
|
|
51
|
+
| `button` | component | 通用按钮 — shadcn 实现 + antd 功能扩展(loading / icon / shape / block / dashed variant) |
|
|
52
|
+
| `progress` | component | 进度条 — Radix 线性 + antd 的 status / showInfo / size 并集 + 配套 ProgressCircle 环形 |
|
|
53
|
+
|
|
54
|
+
### npm 依赖
|
|
55
|
+
|
|
56
|
+
> 业务侧需要先 `pnpm add` / `npm install` 这些包。CLI 在 `ui add` 完成后会列出此提示。
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
pnpm add lucide-react@^0.460.0
|
|
60
|
+
```
|
|
61
|
+
<!-- auto:deps:end -->
|
|
62
|
+
|
|
63
|
+
## AI 生成纪律
|
|
64
|
+
|
|
65
|
+
- **不要在组件内做真上传** — 拿到 `onChange(files)` 后由消费方负责 fetch / axios,组件只管 UI
|
|
66
|
+
- **进度场景**:必须用**受控** `fileList`,在 `onChange` 拿到 file 后启动上传,期间通过 `setState` 更新对应 item 的 `status="uploading"` + `percent`
|
|
67
|
+
- **`maxCount` / `maxSize`**:校验失败会触发 `onExceed(rejected, reason)`,业务侧用 toast 提示;**不要**在组件外手动二次校验
|
|
68
|
+
- **`accept`**:浏览器层过滤,**不能**当作安全校验 — 服务端仍需独立校验文件类型
|
|
69
|
+
- **`dragger` 与 `children`**:dragger=true 时 children 替换默认提示文案;dragger=false 时 children 替换 Button 文本
|
|
70
|
+
- **不要给 input 加 `name`** — Upload 不参与原生 form submit,数据由 `onChange` 取走
|
|
71
|
+
|
|
72
|
+
## Examples
|
|
73
|
+
|
|
74
|
+
```tsx
|
|
75
|
+
import { Upload, type UploadFileItem } from '@/components/ui/upload';
|
|
76
|
+
import { toast } from 'sonner';
|
|
77
|
+
import * as React from 'react';
|
|
78
|
+
|
|
79
|
+
// 点击上传
|
|
80
|
+
<Upload
|
|
81
|
+
accept="image/*"
|
|
82
|
+
multiple
|
|
83
|
+
maxCount={3}
|
|
84
|
+
maxSize={5 * 1024 * 1024}
|
|
85
|
+
onExceed={(_, reason) =>
|
|
86
|
+
toast.error(reason === 'count' ? '最多 3 个文件' : '单文件不能超过 5MB')
|
|
87
|
+
}
|
|
88
|
+
onChange={(list) => console.log(list)}
|
|
89
|
+
/>
|
|
90
|
+
|
|
91
|
+
// 拖拽上传(Dragger)
|
|
92
|
+
<Upload dragger multiple accept=".csv,.xlsx" onChange={(list) => uploadAll(list)} />
|
|
93
|
+
|
|
94
|
+
// 带进度(受控)
|
|
95
|
+
const [files, setFiles] = React.useState<UploadFileItem[]>([]);
|
|
96
|
+
<Upload
|
|
97
|
+
fileList={files}
|
|
98
|
+
multiple
|
|
99
|
+
onChange={(next) => {
|
|
100
|
+
setFiles(next);
|
|
101
|
+
const fresh = next.filter((f) => f.status === 'done' && f.file);
|
|
102
|
+
fresh.forEach((item) => {
|
|
103
|
+
uploadXhr(item.file!, (percent) => {
|
|
104
|
+
setFiles((cur) =>
|
|
105
|
+
cur.map((f) => (f.uid === item.uid ? { ...f, status: 'uploading', percent } : f)),
|
|
106
|
+
);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
}}
|
|
110
|
+
/>
|
|
111
|
+
```
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
3
|
+
import { Upload, type UploadFileItem } from './upload';
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof Upload> = {
|
|
6
|
+
title: '表单与输入 · Form/Upload',
|
|
7
|
+
component: Upload,
|
|
8
|
+
tags: ['autodocs'],
|
|
9
|
+
parameters: {
|
|
10
|
+
docs: {
|
|
11
|
+
description: {
|
|
12
|
+
component:
|
|
13
|
+
'文件上传 — 支持点击 + 拖拽双模式(Dragger),内置 maxCount / maxSize 校验、文件列表、进度条。组件只负责前端选择与列表呈现,真实上传请求由消费方在 onChange 中发起。视觉走 OpenTrek tokens,等价 antd `Upload` + `Upload.Dragger`,所有样式来自 `@teamix-evo/design`,无 mock。',
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
argTypes: {
|
|
18
|
+
multiple: { control: 'boolean' },
|
|
19
|
+
dragger: { control: 'boolean' },
|
|
20
|
+
disabled: { control: 'boolean' },
|
|
21
|
+
accept: { control: 'text' },
|
|
22
|
+
},
|
|
23
|
+
args: { multiple: false, dragger: false, disabled: false },
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export default meta;
|
|
27
|
+
type Story = StoryObj<typeof Upload>;
|
|
28
|
+
|
|
29
|
+
export const Playground: Story = {};
|
|
30
|
+
|
|
31
|
+
export const Dragger: Story = {
|
|
32
|
+
parameters: { controls: { disable: true } },
|
|
33
|
+
render: () => (
|
|
34
|
+
<Upload dragger multiple accept=".csv,.xlsx,.json" />
|
|
35
|
+
),
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export const WithLimits: Story = {
|
|
39
|
+
parameters: { controls: { disable: true } },
|
|
40
|
+
render: () => (
|
|
41
|
+
<Upload
|
|
42
|
+
multiple
|
|
43
|
+
maxCount={3}
|
|
44
|
+
maxSize={2 * 1024 * 1024}
|
|
45
|
+
onExceed={(rej, reason) =>
|
|
46
|
+
// eslint-disable-next-line no-alert
|
|
47
|
+
alert(`${reason === 'count' ? '超出最大文件数' : '超出最大文件大小'}: ${rej.map((f) => f.name).join(', ')}`)
|
|
48
|
+
}
|
|
49
|
+
/>
|
|
50
|
+
),
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export const ControlledProgress: Story = {
|
|
54
|
+
parameters: { controls: { disable: true } },
|
|
55
|
+
render: () => {
|
|
56
|
+
const [files, setFiles] = React.useState<UploadFileItem[]>([
|
|
57
|
+
{ uid: 'demo-1', name: 'report.pdf', status: 'uploading', percent: 45 },
|
|
58
|
+
{ uid: 'demo-2', name: 'cover.png', status: 'done' },
|
|
59
|
+
{ uid: 'demo-3', name: 'broken.zip', status: 'error' },
|
|
60
|
+
]);
|
|
61
|
+
return (
|
|
62
|
+
<Upload
|
|
63
|
+
dragger
|
|
64
|
+
multiple
|
|
65
|
+
fileList={files}
|
|
66
|
+
onChange={setFiles}
|
|
67
|
+
/>
|
|
68
|
+
);
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export const Disabled: Story = {
|
|
73
|
+
parameters: { controls: { disable: true } },
|
|
74
|
+
render: () => <Upload dragger disabled />,
|
|
75
|
+
};
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { File, Upload as UploadIcon, X } from 'lucide-react';
|
|
3
|
+
|
|
4
|
+
import { cn } from '@/utils/cn';
|
|
5
|
+
import { Button } from '@/components/button/button';
|
|
6
|
+
import { Progress } from '@/components/progress/progress';
|
|
7
|
+
|
|
8
|
+
export interface UploadFileItem {
|
|
9
|
+
/** 稳定 ID(由消费方维护)。 */
|
|
10
|
+
uid: string;
|
|
11
|
+
/** 显示文件名。 */
|
|
12
|
+
name: string;
|
|
13
|
+
/** 文件原对象(可选,服务端已上传完成的回填可不传)。 */
|
|
14
|
+
file?: File;
|
|
15
|
+
/**
|
|
16
|
+
* 上传状态(antd `status` 并集) — uploading 显示进度条;error 红色文案;done 不显示进度。
|
|
17
|
+
*/
|
|
18
|
+
status?: 'uploading' | 'done' | 'error' | 'removed';
|
|
19
|
+
/** 上传百分比(0~100),仅 `status="uploading"` 时显示。 */
|
|
20
|
+
percent?: number;
|
|
21
|
+
/** 上传完成后的远程 URL(可选)。 */
|
|
22
|
+
url?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface UploadProps {
|
|
26
|
+
/**
|
|
27
|
+
* 已上传文件列表(受控)— 不传则组件内部维护本地列表(限制少,通常配 onChange)。
|
|
28
|
+
*/
|
|
29
|
+
fileList?: UploadFileItem[];
|
|
30
|
+
/**
|
|
31
|
+
* 文件接受类型(等价 input.accept) — e.g. `image/*`, `.pdf,.docx`。
|
|
32
|
+
*/
|
|
33
|
+
accept?: string;
|
|
34
|
+
/**
|
|
35
|
+
* 是否支持多文件选择(antd `multiple` 并集)。
|
|
36
|
+
* @default false
|
|
37
|
+
*/
|
|
38
|
+
multiple?: boolean;
|
|
39
|
+
/**
|
|
40
|
+
* 最大文件数(超出则忽略,触发 `onExceed`)。
|
|
41
|
+
*/
|
|
42
|
+
maxCount?: number;
|
|
43
|
+
/**
|
|
44
|
+
* 单文件最大字节数(超出则忽略,触发 `onExceed`)。
|
|
45
|
+
*/
|
|
46
|
+
maxSize?: number;
|
|
47
|
+
/**
|
|
48
|
+
* 启用拖拽上传(antd `Dragger` 并集) — 显示大块虚线区,可点击亦可拖拽。
|
|
49
|
+
* @default false
|
|
50
|
+
*/
|
|
51
|
+
dragger?: boolean;
|
|
52
|
+
/** 禁用上传(已选文件不可删除)。 */
|
|
53
|
+
disabled?: boolean;
|
|
54
|
+
/**
|
|
55
|
+
* 是否显示文件列表(antd `showUploadList` 并集)。
|
|
56
|
+
* @default true
|
|
57
|
+
*/
|
|
58
|
+
showFileList?: boolean;
|
|
59
|
+
/** 文件被添加(用户选择 / 拖入)时的回调 — 返回更新后的列表。 */
|
|
60
|
+
onChange?: (next: UploadFileItem[]) => void;
|
|
61
|
+
/** 文件被移除时的回调。 */
|
|
62
|
+
onRemove?: (file: UploadFileItem) => void;
|
|
63
|
+
/** 超出 maxCount / maxSize 时的回调。 */
|
|
64
|
+
onExceed?: (rejected: File[], reason: 'count' | 'size') => void;
|
|
65
|
+
/** 触发器自定义内容(非 dragger 模式下渲染于 Button 内)。 */
|
|
66
|
+
children?: React.ReactNode;
|
|
67
|
+
className?: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function makeUid(): string {
|
|
71
|
+
return Math.random().toString(36).slice(2, 10) + Date.now().toString(36);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const Upload = React.forwardRef<HTMLDivElement, UploadProps>(
|
|
75
|
+
(
|
|
76
|
+
{
|
|
77
|
+
fileList,
|
|
78
|
+
accept,
|
|
79
|
+
multiple = false,
|
|
80
|
+
maxCount,
|
|
81
|
+
maxSize,
|
|
82
|
+
dragger = false,
|
|
83
|
+
disabled = false,
|
|
84
|
+
showFileList = true,
|
|
85
|
+
onChange,
|
|
86
|
+
onRemove,
|
|
87
|
+
onExceed,
|
|
88
|
+
children,
|
|
89
|
+
className,
|
|
90
|
+
},
|
|
91
|
+
ref,
|
|
92
|
+
) => {
|
|
93
|
+
const inputRef = React.useRef<HTMLInputElement>(null);
|
|
94
|
+
const [internal, setInternal] = React.useState<UploadFileItem[]>([]);
|
|
95
|
+
const isControlled = fileList !== undefined;
|
|
96
|
+
const current = isControlled ? fileList! : internal;
|
|
97
|
+
|
|
98
|
+
const [dragOver, setDragOver] = React.useState(false);
|
|
99
|
+
|
|
100
|
+
const setList = (next: UploadFileItem[]) => {
|
|
101
|
+
if (!isControlled) setInternal(next);
|
|
102
|
+
onChange?.(next);
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const addFiles = (files: File[]) => {
|
|
106
|
+
if (disabled || files.length === 0) return;
|
|
107
|
+
let candidate = files;
|
|
108
|
+
|
|
109
|
+
if (typeof maxSize === 'number') {
|
|
110
|
+
const tooBig = candidate.filter((f) => f.size > maxSize);
|
|
111
|
+
if (tooBig.length) onExceed?.(tooBig, 'size');
|
|
112
|
+
candidate = candidate.filter((f) => f.size <= maxSize);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const remaining =
|
|
116
|
+
typeof maxCount === 'number'
|
|
117
|
+
? Math.max(0, maxCount - current.length)
|
|
118
|
+
: candidate.length;
|
|
119
|
+
if (typeof maxCount === 'number' && candidate.length > remaining) {
|
|
120
|
+
onExceed?.(candidate.slice(remaining), 'count');
|
|
121
|
+
candidate = candidate.slice(0, remaining);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const items: UploadFileItem[] = candidate.map((f) => ({
|
|
125
|
+
uid: makeUid(),
|
|
126
|
+
name: f.name,
|
|
127
|
+
file: f,
|
|
128
|
+
status: 'done',
|
|
129
|
+
}));
|
|
130
|
+
setList([...current, ...items]);
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const handleSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
134
|
+
const list = e.target.files ? Array.from(e.target.files) : [];
|
|
135
|
+
addFiles(list);
|
|
136
|
+
e.target.value = '';
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const remove = (uid: string) => {
|
|
140
|
+
const target = current.find((f) => f.uid === uid);
|
|
141
|
+
if (!target) return;
|
|
142
|
+
setList(current.filter((f) => f.uid !== uid));
|
|
143
|
+
onRemove?.(target);
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const openPicker = () => {
|
|
147
|
+
if (disabled) return;
|
|
148
|
+
inputRef.current?.click();
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const onDrop = (e: React.DragEvent) => {
|
|
152
|
+
e.preventDefault();
|
|
153
|
+
setDragOver(false);
|
|
154
|
+
if (disabled) return;
|
|
155
|
+
const files = Array.from(e.dataTransfer.files);
|
|
156
|
+
addFiles(files);
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const trigger = dragger ? (
|
|
160
|
+
<div
|
|
161
|
+
role="button"
|
|
162
|
+
tabIndex={disabled ? -1 : 0}
|
|
163
|
+
aria-disabled={disabled}
|
|
164
|
+
onClick={openPicker}
|
|
165
|
+
onKeyDown={(e) => {
|
|
166
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
167
|
+
e.preventDefault();
|
|
168
|
+
openPicker();
|
|
169
|
+
}
|
|
170
|
+
}}
|
|
171
|
+
onDragOver={(e) => {
|
|
172
|
+
e.preventDefault();
|
|
173
|
+
if (!disabled) setDragOver(true);
|
|
174
|
+
}}
|
|
175
|
+
onDragLeave={() => setDragOver(false)}
|
|
176
|
+
onDrop={onDrop}
|
|
177
|
+
className={cn(
|
|
178
|
+
'flex cursor-pointer flex-col items-center justify-center gap-2 rounded-md border border-dashed bg-muted/30 px-6 py-10 text-center text-sm text-muted-foreground transition-colors',
|
|
179
|
+
'hover:border-primary/50 hover:bg-muted/50',
|
|
180
|
+
'focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring',
|
|
181
|
+
dragOver && 'border-primary bg-primary/5',
|
|
182
|
+
disabled && 'cursor-not-allowed opacity-60',
|
|
183
|
+
)}
|
|
184
|
+
>
|
|
185
|
+
{children ?? (
|
|
186
|
+
<>
|
|
187
|
+
<UploadIcon className="size-8 text-primary/70" />
|
|
188
|
+
<div className="font-medium text-foreground">点击或拖拽文件到此处上传</div>
|
|
189
|
+
<div className="text-xs">
|
|
190
|
+
{multiple ? '可选择多个文件' : '仅支持单个文件'}
|
|
191
|
+
{accept ? ` · 限 ${accept}` : null}
|
|
192
|
+
</div>
|
|
193
|
+
</>
|
|
194
|
+
)}
|
|
195
|
+
</div>
|
|
196
|
+
) : (
|
|
197
|
+
<Button
|
|
198
|
+
type="button"
|
|
199
|
+
variant="outline"
|
|
200
|
+
onClick={openPicker}
|
|
201
|
+
disabled={disabled}
|
|
202
|
+
icon={<UploadIcon />}
|
|
203
|
+
>
|
|
204
|
+
{children ?? '选择文件'}
|
|
205
|
+
</Button>
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
return (
|
|
209
|
+
<div ref={ref} className={cn('flex flex-col gap-3', className)}>
|
|
210
|
+
<input
|
|
211
|
+
ref={inputRef}
|
|
212
|
+
type="file"
|
|
213
|
+
accept={accept}
|
|
214
|
+
multiple={multiple}
|
|
215
|
+
disabled={disabled}
|
|
216
|
+
onChange={handleSelect}
|
|
217
|
+
className="hidden"
|
|
218
|
+
/>
|
|
219
|
+
{trigger}
|
|
220
|
+
{showFileList && current.length > 0 ? (
|
|
221
|
+
<ul className="flex flex-col gap-2">
|
|
222
|
+
{current.map((f) => (
|
|
223
|
+
<li
|
|
224
|
+
key={f.uid}
|
|
225
|
+
className={cn(
|
|
226
|
+
'flex items-center gap-3 rounded-md border bg-card px-3 py-2 text-sm',
|
|
227
|
+
f.status === 'error' && 'border-destructive/40',
|
|
228
|
+
)}
|
|
229
|
+
>
|
|
230
|
+
<File className="size-4 shrink-0 text-muted-foreground" />
|
|
231
|
+
<div className="flex min-w-0 flex-1 flex-col gap-1">
|
|
232
|
+
<span
|
|
233
|
+
className={cn(
|
|
234
|
+
'truncate',
|
|
235
|
+
f.status === 'error' && 'text-destructive',
|
|
236
|
+
)}
|
|
237
|
+
title={f.name}
|
|
238
|
+
>
|
|
239
|
+
{f.name}
|
|
240
|
+
</span>
|
|
241
|
+
{f.status === 'uploading' ? (
|
|
242
|
+
<Progress value={f.percent ?? 0} size="sm" />
|
|
243
|
+
) : null}
|
|
244
|
+
</div>
|
|
245
|
+
{!disabled ? (
|
|
246
|
+
<button
|
|
247
|
+
type="button"
|
|
248
|
+
aria-label={`移除 ${f.name}`}
|
|
249
|
+
onClick={() => remove(f.uid)}
|
|
250
|
+
className="rounded-sm p-1 text-muted-foreground transition-colors hover:bg-accent hover:text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
|
251
|
+
>
|
|
252
|
+
<X className="size-4" />
|
|
253
|
+
</button>
|
|
254
|
+
) : null}
|
|
255
|
+
</li>
|
|
256
|
+
))}
|
|
257
|
+
</ul>
|
|
258
|
+
) : null}
|
|
259
|
+
</div>
|
|
260
|
+
);
|
|
261
|
+
},
|
|
262
|
+
);
|
|
263
|
+
Upload.displayName = 'Upload';
|
|
264
|
+
|
|
265
|
+
export { Upload };
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: watermark
|
|
3
|
+
name: Watermark
|
|
4
|
+
type: component
|
|
5
|
+
category: feedback
|
|
6
|
+
since: 0.1.0
|
|
7
|
+
package: "@teamix-evo/ui"
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Watermark
|
|
11
|
+
|
|
12
|
+
水印 — antd 独有补足。**等价 antd `Watermark`**(v5.1+)。在容器内部生成重复平铺的 SVG 水印(文字 / 图片),用于敏感页面 / 内部文档 / 演示场景。
|
|
13
|
+
|
|
14
|
+
## When to use
|
|
15
|
+
|
|
16
|
+
- 内部管理后台敏感页面(显示用户名 + 工号,起威慑作用)
|
|
17
|
+
- 文档预览(防止截图被盗用)
|
|
18
|
+
- 设计稿 / 评审页(标注"内部" / "Draft")
|
|
19
|
+
- demo 录屏(标注产品名 / 版本)
|
|
20
|
+
|
|
21
|
+
## When NOT to use
|
|
22
|
+
|
|
23
|
+
- 真正的版权保护 → **服务端水印** + 数字签名;前端水印可被开发者删除
|
|
24
|
+
- 通知 / 提示语 → `Alert` / `Tag`(语义不一样)
|
|
25
|
+
- 装饰背景图 → CSS `background-image`
|
|
26
|
+
|
|
27
|
+
<!-- auto:props:begin -->
|
|
28
|
+
| 名称 | 类型 | 默认值 | 必填 | 说明 |
|
|
29
|
+
| --- | --- | --- | --- | --- |
|
|
30
|
+
| `content` | `string \| string[]` | – | – | 水印文字(antd `content` 并集) — 单行字符串或多行字符串数组。 |
|
|
31
|
+
| `image` | `string` | – | – | 水印图片(antd `image` 并集) — 优先级高于 `content`。建议透明 PNG / SVG。 |
|
|
32
|
+
| `width` | `number` | `120` | – | 单块水印宽度(px)。 |
|
|
33
|
+
| `height` | `number` | `64` | – | 单块水印高度(px)。 |
|
|
34
|
+
| `rotate` | `number` | `-22` | – | 水印旋转角度(deg)。 |
|
|
35
|
+
| `fontSize` | `number` | `14` | – | 字体大小(px)。 |
|
|
36
|
+
| `fontColor` | `string` | `"rgba(0,0,0,0.15)"` | – | 字体颜色(支持 rgba 设置透明度)。 |
|
|
37
|
+
| `fontWeight` | `'normal' \| 'medium' \| 'bold'` | `"normal"` | – | 字体粗细。 |
|
|
38
|
+
| `gapX` | `number` | `80` | – | 水印块之间的水平间距(px)。 |
|
|
39
|
+
| `gapY` | `number` | `80` | – | 水印块之间的垂直间距(px)。 |
|
|
40
|
+
<!-- auto:props:end -->
|
|
41
|
+
|
|
42
|
+
<!-- auto:deps:begin -->
|
|
43
|
+
### 同库依赖
|
|
44
|
+
|
|
45
|
+
> `teamix-evo ui add watermark` 时,以下 entry 会被自动连带安装(无需手动 add)。
|
|
46
|
+
|
|
47
|
+
| Entry | 类型 | 描述 |
|
|
48
|
+
| --- | --- | --- |
|
|
49
|
+
| `cn` | util | Tailwind className 合并工具(clsx + tailwind-merge) |
|
|
50
|
+
|
|
51
|
+
### npm 依赖
|
|
52
|
+
|
|
53
|
+
_无 — 本组件不依赖任何 npm 包。_
|
|
54
|
+
<!-- auto:deps:end -->
|
|
55
|
+
|
|
56
|
+
## AI 生成纪律
|
|
57
|
+
|
|
58
|
+
- **不可作为安全防护**:Watermark 是**前端视觉效果**,F12 可删 — 真正合规需要**服务端**水印(后端给图片打水印 / 给 PDF 嵌不可见数字签名)
|
|
59
|
+
- **`content` 短文本**(用户名 + 工号 / 公司名):太长会被截断,可改用多行字符串数组
|
|
60
|
+
- **`fontColor` 必须带 alpha**(默认 `rgba(0,0,0,0.15)`):不透明水印会遮挡内容
|
|
61
|
+
- **`image`** 优先级高于 content;**透明 PNG / SVG** 否则会出现矩形背景
|
|
62
|
+
- **`rotate=-22`** 是 antd 默认角度 — 防止水平 / 垂直地平等被截图工具一键去除
|
|
63
|
+
- **不要嵌套 Watermark** — 双重水印视觉混乱
|
|
64
|
+
|
|
65
|
+
## Examples
|
|
66
|
+
|
|
67
|
+
```tsx
|
|
68
|
+
import { Watermark } from '@/components/ui/watermark';
|
|
69
|
+
|
|
70
|
+
// 单行文本
|
|
71
|
+
<Watermark content="Acme Corp · CONFIDENTIAL">
|
|
72
|
+
<article className="prose">...内部文档内容...</article>
|
|
73
|
+
</Watermark>
|
|
74
|
+
|
|
75
|
+
// 多行(用户名 + 工号)
|
|
76
|
+
<Watermark content={['Alice Wong', 'EMP-2026-001']}>
|
|
77
|
+
<DashboardPage />
|
|
78
|
+
</Watermark>
|
|
79
|
+
|
|
80
|
+
// 图片水印
|
|
81
|
+
<Watermark image="/brand-mark.svg" width={80} height={40}>
|
|
82
|
+
...
|
|
83
|
+
</Watermark>
|
|
84
|
+
|
|
85
|
+
// 调整密度与角度
|
|
86
|
+
<Watermark
|
|
87
|
+
content="内部资料"
|
|
88
|
+
gapX={120}
|
|
89
|
+
gapY={120}
|
|
90
|
+
rotate={-30}
|
|
91
|
+
fontColor="rgba(255,0,0,0.2)"
|
|
92
|
+
>
|
|
93
|
+
...
|
|
94
|
+
</Watermark>
|
|
95
|
+
```
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import { Watermark } from './watermark';
|
|
3
|
+
|
|
4
|
+
const meta: Meta<typeof Watermark> = {
|
|
5
|
+
title: '反馈与浮层 · Feedback/Watermark',
|
|
6
|
+
component: Watermark,
|
|
7
|
+
tags: ['autodocs'],
|
|
8
|
+
parameters: {
|
|
9
|
+
docs: {
|
|
10
|
+
description: {
|
|
11
|
+
component:
|
|
12
|
+
'水印 — 容器内重复平铺的 SVG 文字 / 图片水印(内部敏感页面 / 文档预览 / demo 录屏)。前端视觉效果,不作为安全防护。等价 antd `Watermark`(v5.1+)。视觉走 OpenTrek tokens,所有样式来自 `@teamix-evo/design`,无 mock。',
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
argTypes: {
|
|
17
|
+
content: { control: 'text' },
|
|
18
|
+
rotate: { control: { type: 'number', min: -90, max: 90 } },
|
|
19
|
+
fontSize: { control: { type: 'number', min: 8, max: 48 } },
|
|
20
|
+
gapX: { control: 'number' },
|
|
21
|
+
gapY: { control: 'number' },
|
|
22
|
+
},
|
|
23
|
+
args: {
|
|
24
|
+
content: 'Acme Corp · CONFIDENTIAL',
|
|
25
|
+
rotate: -22,
|
|
26
|
+
fontSize: 14,
|
|
27
|
+
gapX: 80,
|
|
28
|
+
gapY: 80,
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export default meta;
|
|
33
|
+
type Story = StoryObj<typeof Watermark>;
|
|
34
|
+
|
|
35
|
+
export const Playground: Story = {
|
|
36
|
+
render: (args) => (
|
|
37
|
+
<Watermark {...args} className="h-72 w-full rounded-md border bg-card p-6">
|
|
38
|
+
<h3 className="mb-3 text-lg font-semibold">内部文档</h3>
|
|
39
|
+
<p className="text-sm text-muted-foreground">
|
|
40
|
+
这是一段示例文档内容。水印铺在底层,内容可正常交互。
|
|
41
|
+
本组件适合内部管理后台、敏感页面、设计稿评审等场景。
|
|
42
|
+
</p>
|
|
43
|
+
</Watermark>
|
|
44
|
+
),
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export const MultilineUser: Story = {
|
|
48
|
+
parameters: { controls: { disable: true } },
|
|
49
|
+
render: () => (
|
|
50
|
+
<Watermark
|
|
51
|
+
content={['Alice Wong', 'EMP-2026-001']}
|
|
52
|
+
className="h-72 w-full rounded-md border bg-card p-6"
|
|
53
|
+
>
|
|
54
|
+
<h3 className="mb-3 text-lg font-semibold">用户信息水印</h3>
|
|
55
|
+
<p className="text-sm text-muted-foreground">
|
|
56
|
+
多行水印通常用于"用户名 + 工号",起到威慑截图泄密的作用。
|
|
57
|
+
</p>
|
|
58
|
+
</Watermark>
|
|
59
|
+
),
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export const ColoredAndDense: Story = {
|
|
63
|
+
parameters: { controls: { disable: true } },
|
|
64
|
+
render: () => (
|
|
65
|
+
<Watermark
|
|
66
|
+
content="内部资料"
|
|
67
|
+
fontColor="rgba(220,38,38,0.18)"
|
|
68
|
+
fontWeight="bold"
|
|
69
|
+
gapX={60}
|
|
70
|
+
gapY={60}
|
|
71
|
+
rotate={-30}
|
|
72
|
+
className="h-72 w-full rounded-md border bg-card p-6"
|
|
73
|
+
>
|
|
74
|
+
<h3 className="mb-3 text-lg font-semibold">红色密集水印</h3>
|
|
75
|
+
<p className="text-sm text-muted-foreground">配合 fontWeight / gap / rotate 调整视觉强度。</p>
|
|
76
|
+
</Watermark>
|
|
77
|
+
),
|
|
78
|
+
};
|