u
This commit is contained in:
1 parent
32d423b759
commit
78eb811ff7
102 files changed
+12067
-209
No files matched your search
@@ -0,0 +1,234 @@
|
||||
---
|
||||
title: Attachment 附件管理器
|
||||
description: 分类 / 列表(可选预览)的附件管理面板,上传(选择、拖拽、粘贴)、批量下载删除与分页
|
||||
---
|
||||
|
||||
# Attachment 附件管理器
|
||||
|
||||
`Attachment` 是「一个业务对象的附件集合」管理器:左栏分类、中栏文件(卡片 / 列表双视图),支持选择、拖拽、粘贴上传,多选批量下载 / 删除与分页。右栏预览默认关闭,需要时 `show-preview` 打开。
|
||||
|
||||
它**不请求任何接口**,也不认识「模块 / 业务主键」这类概念:数据从 `files` / `categories` 进来,用户意图以事件出去;需要组件代跑请求时,传 `uploadRequest` / `removeRequest` 即可。
|
||||
|
||||
## 何时使用
|
||||
|
||||
- 需要管理某个业务单据 / 项目的附件集合(上传、分类、下载、删除);
|
||||
- 文件可能有很多,需要分类过滤、分页、批量操作;
|
||||
- 只需要一个简单的「选择文件」入口时,用 [Input](/ui/form/input) 或原生 `<input type="file">` 更轻。
|
||||
|
||||
## 基础用法
|
||||
|
||||
给 `files`(附件)和 `categories`(分类,可选)两份数据,上传与删除交给业务:
|
||||
|
||||
<demo vue="attachment/basic.vue" />
|
||||
|
||||
## 多选与批量操作
|
||||
|
||||
没勾选时工具栏只有「全选」;一旦勾选,工具栏浮出「已选 N 项 + 下载 + 删除」。示例关掉了 `builtin-download`,下载只上报事件:
|
||||
|
||||
<demo vue="attachment/select.vue" />
|
||||
|
||||
## 只读
|
||||
|
||||
`readonly` 会隐藏上传 / 删除入口,并让拖拽、粘贴失效,适合审批查看场景:
|
||||
|
||||
<demo vue="attachment/readonly.vue" />
|
||||
|
||||
## 弹窗用法
|
||||
|
||||
附件组件本身不是弹窗,需要弹窗时用 [Modal](/ui/overlay/modal) 包一层即可(高度用 `height` 给):
|
||||
|
||||
<demo vue="attachment/modal.vue" />
|
||||
|
||||
## 上传与校验
|
||||
|
||||
三种入队方式:点底栏「选择」、把文件拖进列表区、`paste-upload` 打开后在页面任意处 `Ctrl/Cmd + V`。
|
||||
|
||||
入队前按 `accept` / `maxSize` / `maxCount` 校验,被拒的文件通过 `invalid` 事件抛出(`info.message` 是组件按当前语言包生成的提示原文,可直接 toast):
|
||||
|
||||
```ts
|
||||
function onInvalid(info: AttachmentInvalidInfo) {
|
||||
// reason: 'accept' | 'max-size' | 'max-count'
|
||||
message.warning(info.message)
|
||||
}
|
||||
```
|
||||
|
||||
队列里的文件先渲染在列表最前面(带「待上传」标记),点「上传」后组件把它们交出去:
|
||||
|
||||
- **没传 `upload-request`**:emit `upload(files, ctx)`,并**立即清空队列**(文件所有权移交业务);
|
||||
- **传了 `upload-request`**:组件托管请求,期间「上传」按钮 loading;成功后 emit `upload-success(files, result)`,`result` 是数组时自动追加进 `files`;失败 emit `upload-error(err, files)` 且**保留队列**(可重试)。
|
||||
|
||||
## 数据契约
|
||||
|
||||
列表项的字段是中性的,不含任何业务字段名:
|
||||
|
||||
```ts
|
||||
const files = ref<AttachmentItem[]>([
|
||||
{ id: 12, name: '合同.pdf', size: 512_000, categoryId: 'contract', url: '/files/12.pdf' },
|
||||
])
|
||||
```
|
||||
|
||||
业务侧字段名不同也没关系 —— 组件的所有事件都把原始数据放在 `item.raw` 里原样回传,字段映射写在业务侧的一个 adapter 就够:
|
||||
|
||||
```ts
|
||||
// 后端行 → 组件项
|
||||
const toItem = (row: BfFilesRow): AttachmentItem => ({
|
||||
id: row.subid,
|
||||
name: row.mx_filename,
|
||||
size: Number(row.mx_filesize),
|
||||
ext: row.mx_fileext,
|
||||
url: row.mx_mapfilename,
|
||||
categoryId: row.mx_cate_id,
|
||||
raw: row,
|
||||
})
|
||||
```
|
||||
|
||||
## 分页
|
||||
|
||||
两种模式,由 `total` 区分:
|
||||
|
||||
| 模式 | 传参 | 行为 |
|
||||
| --- | --- | --- |
|
||||
| 客户端分页 | 只给 `files` | `files` 是完整列表,组件按 `page` / `pageSize` 自行切片 |
|
||||
| 服务端分页 | 再给 `total` | `files` 已经是当前页数据,组件只画不切,翻页靠 `update:page` |
|
||||
|
||||
`page` / `pageSize` 是**非受控**的:不传时组件内部维护(默认 1 / 20),传了就完全听你的(`v-model:page-size` 等)。
|
||||
|
||||
## API
|
||||
|
||||
### Props
|
||||
|
||||
| 名称 | 类型 | 默认值 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| `files` | `AttachmentItem[]` | `[]` | 附件列表,配合 `v-model:files` |
|
||||
| `categories` | `AttachmentCategory[]` | `[]` | 分类数据;为空则不渲染左栏 |
|
||||
| `categoryKey` | `string \| number` | `''` | 选中分类,`''` 表示「全部」,配合 `v-model:categoryKey` |
|
||||
| `showCategory` | `boolean` | `true` | 是否显示左栏(无 `categories` 时自动隐藏) |
|
||||
| `showPreview` | `boolean` | `false` | 是否显示右栏预览;关掉时 `preview` 事件照常抛出,可自行开弹窗预览 |
|
||||
| `readonly` | `boolean` | `false` | 只读:隐藏上传 / 删除,拖拽与粘贴失效 |
|
||||
| `loading` | `boolean` | `false` | 列表加载中 |
|
||||
| `categoryLoading` | `boolean` | `false` | 分类加载中 |
|
||||
| `selectable` | `boolean` | `true` | 是否可勾选 |
|
||||
| `multiple` | `boolean` | `true` | 是否允许多选 |
|
||||
| `accept` | `string` | — | 接受的文件类型,同原生 `accept`(支持 `.pdf` / `image/*` / `application/pdf`) |
|
||||
| `maxSize` | `number` | `0` | 单文件大小上限(字节),`0` 表示不限制 |
|
||||
| `maxCount` | `number` | `0` | 待上传队列上限,`0` 表示不限制 |
|
||||
| `pasteUpload` | `boolean` | `false` | 监听 `document` 的粘贴上传(多实例场景请只开一个) |
|
||||
| `confirmRemove` | `boolean` | `true` | 删除前二次确认(内建 Modal) |
|
||||
| `builtinDownload` | `boolean` | `true` | 依据 `item.url` 内建触发下载;关掉后只上报 `download` |
|
||||
| `pagination` | `boolean` | `true` | 是否显示分页(数据不足一页时自动隐藏) |
|
||||
| `page` | `number` | — | 当前页,非受控(内部默认 `1`) |
|
||||
| `pageSize` | `number` | — | 每页条数,非受控(内部默认 `20`) |
|
||||
| `total` | `number` | — | 总条数;**传了即为服务端分页**,`files` 视为当前页 |
|
||||
| `pageSizeOptions` | `Array<number \| string>` | `[10, 20, 50, 100]` | 每页条数选项 |
|
||||
| `showSizeChanger` | `boolean` | `false` | 显示每页条数切换器 |
|
||||
| `height` | `number \| string` | — | 根容器高度,数字按 px;默认撑满父容器(父级无高度时保底 320px) |
|
||||
| `uploadRequest` | `(files: File[], ctx: AttachmentUploadContext) => Promise<AttachmentItem[] \| void> \| AttachmentItem[] \| void` | — | 托管上传 |
|
||||
| `removeRequest` | `(items: AttachmentItem[]) => Promise<void> \| void` | — | 托管删除 |
|
||||
|
||||
### Events
|
||||
|
||||
| 名称 | 参数 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `update:files` | `(files: AttachmentItem[])` | 删除成功 / 托管上传回填后更新列表 |
|
||||
| `update:categoryKey` | `(key: string \| number)` | 分类切换 |
|
||||
| `update:page` | `(page: number)` | 翻页 |
|
||||
| `update:pageSize` | `(size: number)` | 每页条数变化 |
|
||||
| `change` | `(files: AttachmentItem[])` | 列表增删(上传成功 / 删除成功) |
|
||||
| `select` | `(items: AttachmentItem[])` | 勾选变化 |
|
||||
| `categoryChange` | `(key: string \| number, category: AttachmentCategory \| null)` | 分类切换,带原始分类对象 |
|
||||
| `preview` | `(item: AttachmentItem \| null)` | 点击某项预览(翻页同样触发) |
|
||||
| `download` | `(items: AttachmentItem[])` | 下载 |
|
||||
| `remove` | `(items: AttachmentItem[])` | 用户确认删除且请求成功 |
|
||||
| `removeError` | `(error: unknown, items: AttachmentItem[])` | 删除失败 |
|
||||
| `upload` | `(files: File[], ctx: AttachmentUploadContext)` | 未传 `uploadRequest` 时把待上传文件交给业务 |
|
||||
| `uploadSuccess` | `(files: File[], result: AttachmentItem[] \| void)` | 托管上传成功 |
|
||||
| `uploadError` | `(error: unknown, files: File[])` | 托管上传失败(队列保留) |
|
||||
| `invalid` | `(info: AttachmentInvalidInfo)` | 文件被 `accept` / `maxSize` / `maxCount` 拒绝 |
|
||||
|
||||
### Exposed(通过 ref 调用)
|
||||
|
||||
| 名称 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `selectedItems` | `AttachmentItem[]` | 当前勾选的附件 |
|
||||
| `pendingFiles` | `AttachmentPendingFile[]` | 待上传队列 |
|
||||
| `openFileDialog()` | — | 打开系统文件选择框 |
|
||||
| `clearSelection()` | — | 清空勾选 |
|
||||
| `selectAll()` | — | 全选当前页 |
|
||||
| `addFiles(files)` | `File[] \| FileList => File[]` | 入队(走 `accept` / `maxSize` / `maxCount` 校验),返回被接受的文件 |
|
||||
| `clearPending()` | — | 清空待上传队列(业务自行接管上传时调用) |
|
||||
|
||||
### 类型定义
|
||||
|
||||
```ts
|
||||
interface AttachmentItem {
|
||||
id: string | number
|
||||
name: string
|
||||
size?: number
|
||||
ext?: string
|
||||
url?: string
|
||||
thumbnailUrl?: string
|
||||
categoryId?: string | number
|
||||
status?: 'ready' | 'uploading' | 'error'
|
||||
percent?: number
|
||||
raw?: unknown
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
interface AttachmentCategory {
|
||||
id: string | number
|
||||
name: string
|
||||
count?: number
|
||||
disabled?: boolean
|
||||
children?: AttachmentCategory[]
|
||||
}
|
||||
|
||||
interface AttachmentPendingFile {
|
||||
file: File
|
||||
name: string
|
||||
size: number
|
||||
ext: string
|
||||
url?: string
|
||||
}
|
||||
|
||||
interface AttachmentInvalidInfo {
|
||||
reason: 'accept' | 'max-size' | 'max-count'
|
||||
files: File[]
|
||||
message: string
|
||||
}
|
||||
|
||||
interface AttachmentUploadContext {
|
||||
categoryId?: string | number
|
||||
}
|
||||
```
|
||||
|
||||
### 样式变量
|
||||
|
||||
类型色(图标 + 扩展名那一小块)按文件族取色,全部可覆盖;`--g3-color-*` 未加载时组件自带兜底值:
|
||||
|
||||
| 变量 | 说明 |
|
||||
| --- | --- |
|
||||
| `--g3-attachment-type-image` | 图片:默认 `--g3-color-primary` |
|
||||
| `--g3-attachment-type-doc` | 文档(pdf / doc / txt …):默认 `--g3-color-danger` |
|
||||
| `--g3-attachment-type-sheet` | 表格:默认 `--g3-color-success` |
|
||||
| `--g3-attachment-type-slide` | 演示:默认 `--g3-color-warning` |
|
||||
| `--g3-attachment-type-archive` | 压缩包:默认 `#d98e04`(暗色 `#e0a63c`) |
|
||||
| `--g3-attachment-type-media` | 音视频:默认 `#1f9ea8`(暗色 `#38bcc6`) |
|
||||
| `--g3-attachment-type-other` | 未知类型:默认 `--g3-text-3` |
|
||||
|
||||
其余尺寸 / 间距 / 圆角 / 过渡全部走令牌(`--g3-control-height`、`--g3-space-*`、`--g3-radius*`、`--g3-duration*`),换肤时不需要动组件。
|
||||
|
||||
## 实现说明
|
||||
|
||||
- **默认两栏,预览按需开**:布局用 [Splitter](/ui/data/splitter)——分类栏固定 200px(150–320 可拖),列表栏自适应(≥280);`show-preview` 打开后追加 360px(≥280)的预览栏。`showCategory` / `showPreview` 为假时对应面板直接不渲染,只剩两栏或一栏。预览栏关闭时 `preview` 事件照常抛出,业务可以接自己的弹窗 / 抽屉。
|
||||
- **面板之间有常显的分割线**:关掉了 Splitter 的 `barOnHover`(默认只在 hover 时显示),并把线宽收成 1px——否则分类栏与卡片区之间看不出边界。拖拽命中区仍是 10px,只是视觉线更细。
|
||||
- **工具栏整行同一档规格**:全选(复选框文案)、已选计数、批量按钮统一到 **12px 字 / 24px 控件**(按钮 `size="small"`),高度**写死** `24px + 上下各 8px`——必须用 `height` 而不是 `min-height`:默认 `content-box` 下 `min-height` 只管内容盒,实际高度 = 内容盒 + 上下 padding,勾选前后仍会变。行内元素一律 `flex: none`(不收缩)+ `flex-wrap: nowrap` + `overflow: hidden`。勾选后浮出「| 已选 N 项 · 下载 · 删除」,用 1px 竖线把「选择」与「操作」分组——字号一致、控件等高,才不会看着"歪"或空荡。删除与下载用 [Button](/ui/general/button) 的 `danger-outline` / `outline` 变体,在工具栏里有明确边界(`ghost` 的纯文字按钮会像裸文本飘着)。**窄面板**走容器查询(按面板宽度、不是视口宽度):先收掉按钮文案只留图标,再窄一点连「全选」文案也收起,保证一行永远放得下。
|
||||
- **操作藏在 hover 里**:卡片右下角的「下载 / 删除」平时不显示,hover 或键盘聚焦(`:focus-within`)才浮现,`@media (hover: none)` 的触屏设备下常显。工具栏同理——没勾选任何文件时不渲染「下载 / 删除」,只留全选;勾选后浮出「已选 N 项 + 操作」。
|
||||
- **空态就是投放区**:虚线框铺满列表区,hover 转主色;文案两行(「暂无文件」+「拖拽文件到此处,或点击选择」),并把 `accept` / `maxSize` / `maxCount` 的限制拼成第三行提示,省得用户上传后才被拒。
|
||||
- **底栏是"脏"状态指示**:待上传队列非空时,底栏整块染主色淡底、计数变主色;上传与分页合成同一块(只有一条顶边),不会出现两条并排的分隔线。
|
||||
- **分类树用 [Tree](/ui/data/tree)**:`categories` 之上合成了一个「全部」根节点(对外用 `categoryKey === ''` 表示),因此可以给层级数据;`categoryChange` 会把原始分类对象一起带出来。
|
||||
- **删除确认是内建 Modal**:不走 `Modal.confirm()` 命令式(那会让组件依赖全局挂载容器)。`confirmRemove` 关掉后直接进请求。
|
||||
- **`update:files` 的语义**:受控组件不擅自改数据。删除成功、或托管上传返回数组时,组件会 emit 过滤 / 追加后的新数组,方便 `v-model:files` 一行接上;完全自管数据时忽略它即可。
|
||||
- **blob URL 的回收**:待上传图片的本地预览地址由组件创建,在出队、清空、切换分类、卸载时释放,业务无需关心。
|
||||
- **上传进度**:`files` 里某项 `status: 'uploading'` 时,卡片封面底边出现 2px 进度条、列表视图在大小列显示细轨 + 百分比(进度值读 `percent`)。
|
||||
- **粘贴上传默认关闭**:它监听 `document`,一个页面多个附件组件同时开会互相抢,所以做成显式 opt-in。
|
||||
- **RTL**:布局全部用逻辑属性(`inset-inline-start` / `inset-block-end` / `text-align: start` 等),宿主 `<html dir="rtl">` 下自动镜像,分隔条拖拽与键盘方向由 Splitter 的 `useDirection` 处理。
|
||||
@@ -0,0 +1,173 @@
|
||||
---
|
||||
title: RichText 富文本编辑器
|
||||
description: 基于 Tiptap 的富文本编辑,支持字体、字号、对齐、文字与背景色、上下标、图片视频上传与表格
|
||||
---
|
||||
|
||||
# RichText 富文本编辑器
|
||||
|
||||
`RichText` 是基于 [Tiptap](https://tiptap.dev) 封装的富文本编辑器,`v-model` 输出干净的 HTML 字符串,可直接作为邮件正文、公告正文等场景使用。
|
||||
|
||||
## 何时使用
|
||||
|
||||
- 录入需要富文本格式的正文(公告、邮件、通知、帮助文档);
|
||||
- 需要字体 / 字号 / 对齐 / 文字颜色 / 背景颜色 / 上标 / 下标等排版能力时;
|
||||
- 需要在正文中嵌入图片 / 视频、插入表格时。
|
||||
|
||||
> 纯文本或短文本请用 [Input](/ui/form/input)(`type="textarea"`),不要用富文本承载简单字段。
|
||||
|
||||
## 基础用法
|
||||
|
||||
`v-model` 双向绑定 HTML 字符串,工具条默认置顶,内置字体 / 字号 / 对齐 / 颜色 / 上下标 / 表格 / 撤销重做。
|
||||
|
||||
<demo vue="rich-text/basic.vue" />
|
||||
|
||||
## 工具条
|
||||
|
||||
工具条按「一天点几次」而不是「功能重不重要」排布,**始终一行、高度恒定 48px**:
|
||||
|
||||
- **常驻**(顺序:历史 → 正文格式 → 行内格式 → 对齐 → 颜色 → 列表与插入):撤销、重做 | 段落格式(正文 / 标题 1-3)| 加粗、斜体、下划线 | 对齐(左 / 中 / 右)| 字体颜色、背景颜色 | 无序列表、有序列表、链接、图片、插入表格;
|
||||
- **收进 `⋯ 更多`**:`字体` / `字号`(二级菜单,选中态跟随光标)、`格式` 组(删除线 / 上标 / 下标 / 引用)、`插入` 组(视频)。菜单项带分组标题与快捷键提示(对着 Tiptap 的真实键位写的,如删除线 `Ctrl+Shift+S`)。
|
||||
|
||||
图标 16px、按钮 32px(等于 `--g3-control-height`)、组内间隙 4px、内边距 8px / 10px(左右 10px 让首列图标与正文的 12px 内边距对齐);图标前景用 `text-1`(88% 黑)而不是 ghost 按钮默认的 `text-2`(65%),描边保持 lucide 原生粗细 —— 实测「发虚」的主因是浅灰前景而不是线宽,所以只加深颜色、不加重描边。整条工具条自然宽度约 **744px**:760px 以上的容器都是一行,再窄才折行,且折行以「组」为单位,不会把分隔线甩到行首/行尾。表格插入按钮留在工具条上而不是进 `⋯`:它要先选行列,收起后承载不了那个面板。
|
||||
|
||||
## 对齐与颜色
|
||||
|
||||
工具条提供三组排版能力,均作用于选区所在的块(正文段落 / 标题,表格单元格里的内容同样是段落):
|
||||
|
||||
- **对齐**(左 / 居中 / 右):写 `text-align` 到块元素上;点当前档位即取消对齐,不需要单独的「取消」按钮;
|
||||
- **字体颜色**(`A` 图标 + 当前色条):写 `color`;
|
||||
- **背景颜色**(荧光笔图标 + 当前色条):写 `background-color`。
|
||||
|
||||
两个取色按钮都是「图标 + 当前色条」的形式,色条显示选区当前的颜色(未设色时是空位)。点开后是预设色板面板:常用色一屏排满,`无颜色` 清除该属性,右侧彩虹色块打开系统取色器选任意颜色。选完色面板自动收起。
|
||||
|
||||
## 图片 / 视频上传
|
||||
|
||||
图片和视频通过 `upload-image` / `upload-video` 两个 prop 注入上传函数。上传函数接收 `File`,返回 `{ fileUrl, ... }` 元数据;上传完成后组件会触发 `upload-inline` 事件,把内联资源元数据上报给外层(对齐邮件「inline 附件」模型)。
|
||||
|
||||
<demo vue="rich-text/upload.vue" />
|
||||
|
||||
```ts
|
||||
// 上传函数约定
|
||||
type Upload = (file: File) => Promise<{
|
||||
fileUrl: string // 正文 img / video 的 src
|
||||
fileId?: string // 后端文件 id
|
||||
cid?: string // 内联资源 cid(邮件场景)
|
||||
fileName?: string
|
||||
fileSize?: number
|
||||
}>
|
||||
```
|
||||
|
||||
## 只读 / 禁用
|
||||
|
||||
`readonly` 可聚焦可复制但不可编辑;`disabled` 整体灰化不可交互。
|
||||
|
||||
<demo vue="rich-text/readonly.vue" />
|
||||
|
||||
## 表格
|
||||
|
||||
工具条「插入表格」按钮弹出行 / 列选择面板(支持关闭表头行);光标进入表格后,表格下方会浮出就近操作栏:
|
||||
|
||||
- **行**:上方插入行、下方插入行、删除行;
|
||||
- **列**:左侧插入列、右侧插入列、删除列;
|
||||
- **单元格**:合并单元格、拆分单元格(两个独立按钮,不可用时各自置灰);
|
||||
- **表头**:切换表头列、切换表头单元格;
|
||||
- **删除**:整表删除(与增删行列用分隔线隔开,防误点)。
|
||||
|
||||
单元格内文字的对齐、颜色在顶部工具条里设置(单元格内容也是普通段落),操作栏不再重复提供。
|
||||
|
||||
列宽拖拽:悬停列分隔线出现 `col-resize` 光标,左右拖动调整列宽。拖拽采用 `table-layout: fixed`,**只动被拖列、其余列纹丝不动**(Excel 语义);列宽以 `colwidth` 属性持久化进 HTML,最小列宽 10px。拖宽后表格总宽超过容器时,在表格容器内横向滚动,不撑破编辑器内容区。`Tab` / `Shift+Tab` 在单元格间跳转,末格 `Tab` 自动追加行。
|
||||
|
||||
> 行列数限制为 1–20,超出会在插入时自动夹回,避免一次性生成超大表格卡死编辑器。
|
||||
|
||||
## 获取纯文本
|
||||
|
||||
通过 `ref` 调用 `getText()` 拿到纯文本(邮件 `text/plain` 备选),`getHTML()` 拿 HTML,`clear()` 清空。
|
||||
|
||||
<demo vue="rich-text/methods.vue" />
|
||||
|
||||
## API
|
||||
|
||||
### Props
|
||||
|
||||
| 名称 | 类型 | 默认值 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| `modelValue` | `string` | `''` | 双向绑定的 HTML 字符串 |
|
||||
| `placeholder` | `string` | — | 占位文案 |
|
||||
| `disabled` | `boolean` | `false` | 禁用态 |
|
||||
| `readonly` | `boolean` | `false` | 只读态:可聚焦、可复制,但不可编辑 |
|
||||
| `toolbar` | `'top' \| 'bottom' \| false` | `'top'` | 工具条位置;`false` 隐藏工具条 |
|
||||
| `minHeight` | `number \| string` | `200` | 编辑区最小高度 |
|
||||
| `maxHeight` | `number \| string` | — | 编辑区最大高度(超出滚动) |
|
||||
| `uploadImage` | `(file: File) => Promise<RichTextUploadResult>` | — | 图片上传函数,返回完整元数据 |
|
||||
| `uploadVideo` | `(file: File) => Promise<RichTextUploadResult>` | — | 视频上传函数,返回完整元数据 |
|
||||
| `fonts` | `RichTextFontOption[]` | 内置字体表 | 「更多」菜单里字体子菜单的选项 |
|
||||
| `fontSizes` | `RichTextFontSizeOption[]` | 内置字号表 | 「更多」菜单里字号子菜单的选项 |
|
||||
|
||||
### Events
|
||||
|
||||
| 名称 | 参数 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `update:modelValue` | `(html: string)` | 内容变化时触发 |
|
||||
| `focus` | `(event: FocusEvent)` | 获得焦点 |
|
||||
| `blur` | `(event: FocusEvent)` | 失去焦点 |
|
||||
| `upload-inline` | `(result: RichTextUploadResult, kind: 'image' \| 'video')` | 内联资源上传完成,上报给外层收集 |
|
||||
| `upload-error` | `(error: Error, kind: 'image' \| 'video')` | 上传失败 |
|
||||
|
||||
### Exposed(通过 ref 调用)
|
||||
|
||||
| 名称 | 签名 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `editor` | `Editor` | 底层 Tiptap 实例(进阶用法) |
|
||||
| `getHTML` | `() => string` | 当前 HTML |
|
||||
| `getText` | `() => string` | 纯文本(邮件 text/plain) |
|
||||
| `focus` | `() => void` | 聚焦 |
|
||||
| `clear` | `() => void` | 清空内容 |
|
||||
|
||||
### 类型定义
|
||||
|
||||
```ts
|
||||
export interface RichTextUploadResult {
|
||||
fileUrl: string
|
||||
fileId?: string
|
||||
cid?: string
|
||||
fileName?: string
|
||||
fileSize?: number
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
export interface RichTextFontOption {
|
||||
label: string
|
||||
value: string
|
||||
}
|
||||
|
||||
export interface RichTextFontSizeOption {
|
||||
label: string
|
||||
value: string
|
||||
}
|
||||
```
|
||||
|
||||
### 样式变量
|
||||
|
||||
| 变量 | 默认值 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `--g3-border-color` | `#d9d9d9` | 容器 / 表格边框 |
|
||||
| `--g3-radius` | `6px` | 容器圆角 |
|
||||
| `--g3-fill` | `rgba(0,0,0,.04)` | 工具条背景、表头背景 |
|
||||
| `--g3-color-primary` | `#18181b` | 激活态、链接、引用竖线颜色、聚焦时正文区的 2px 描边 |
|
||||
| `--g3-color-primary-soft` | 主色 12% | 按钮激活底、选中单元格底 |
|
||||
| `--g3-rich-text-toolbar-shadow-near` | `rgba(34,47,62,.1)` | 工具条投影近层(`0 2px 2px -2px`,`toolbar="bottom"` 时翻到上方) |
|
||||
| `--g3-rich-text-toolbar-shadow-far` | `rgba(34,47,62,.07)` | 工具条投影远层(`0 8px 8px -4px` 弥散) |
|
||||
|
||||
## 实现说明
|
||||
|
||||
- **选型 Tiptap**:MIT 协议、Vue 3 官方绑定(`@tiptap/vue-3`)、headless 架构便于贴合 G3 设计体系;相比 HugeRTE(TinyMCE 的 MIT fork)在 Vue 集成与 UI 定制上更契合组件库封装;
|
||||
- **不含代码块**:从 `starter-kit` 剔除了 Code / CodeBlock(ERP 场景不需要);
|
||||
- **字体 / 字号 / 颜色 / 背景色**:用官方 `TextStyleKit`(内置 `setFontFamily` / `setFontSize` / `setColor` / `setBackgroundColor` 命令),行高保持关闭;颜色只写 `textStyle` 记号(`<span style="color: …">`),不引入额外的 mark;
|
||||
- **对齐**:官方 `@tiptap/extension-text-align`,只挂到 `heading` / `paragraph`,`alignments` 限左中右;面板里是 `toggleTextAlign`,点当前档位即取消;
|
||||
- **内容区即编辑区**:`minHeight` / `maxHeight` 作用在内容区上,可编辑区(`.tiptap`)用 `flex: 1 0 auto` 撑满 —— 内容短时整块都能点(点空白即聚焦、光标落到正文末尾),内容长时按内容高度撑开、由内容区滚动且不裁切。注意 EditorContent 会在内容区与可编辑区之间再插一层包裹 DOM,所以 `.tiptap` 上的 `min-height: inherit` 取的是那一层的值(恒为 0),撑不高可编辑区;
|
||||
- **聚焦高亮只给正文区**:编辑器获得焦点时(根节点 `:focus-within`),正文区四周亮起 2px 主色内描边,工具条不参与 —— 谁可编辑谁亮。用 `inset box-shadow` 而不是 `border`(不参与布局,聚焦不挤动正文),也不用 TinyMCE 那种 `::after` 覆盖层(正文区就是滚动容器,覆盖层会跟着正文滚走,而滚动容器的阴影不随内容滚动,天然钉在内容盒上);
|
||||
- **工具条密度与「更多」**:按钮 32px(等于 `--g3-control-height`)+ 组内 4px 间隙,字体 / 字号与低频功能收进 `⋯` 菜单,自然宽度从 928px 降到 744px;`⋯` 用 `G3Dropdown` 现成的 `type: 'label'`(分组标题)/ `children`(子菜单)/ `shortcut` / `selected` 能力拼出来,与工具条共用同一份 `ToolButton` 定义,激活态与禁用态一致。菜单内容约 295px,靠 Dropdown 默认的视口上限(`calc(100vh - 48px)`)保证面板内部不出滚动条,不需要额外传 `menuMaxHeight`;
|
||||
- **图标分量**:16px 图标 + lucide 原生描边(16px 下约 1.33px),只把前景从 ghost 默认的 `text-2`(65%)提到 `text-1`(88%)。试过描边 2.5 与放大到 24px,都偏重;实测「发虚」的主因是浅灰前景,动颜色这一项性价比最高;
|
||||
- **视频自写 node**:Tiptap 官方无视频扩展,`VideoNode.ts` 定义了轻量 `video` block node,输出 `<video controls>` 标签;邮件场景由业务层决定是否降级为「封面图 + 附件」(编辑器不绑定邮件语义);
|
||||
- **内联资源上报**:图片 / 视频上传后 `emit('upload-inline', result, kind)`,外层把 `result` 收集进 `inlines` 数组,与邮件「inline 附件」模型对齐(参考 Email-Vue 的 `@upload-image`);
|
||||
- **依赖策略**:Tiptap 核心走 `peerDependencies`,扩展走 `dependencies`,不把编辑器全家桶强塞给所有 `@g3soft/ui` 使用者。
|
||||
@@ -0,0 +1,124 @@
|
||||
---
|
||||
title: SliderCaptcha 滑块验证码
|
||||
description: 按住滑块拖到行内结束端完成验证,支持键盘操作与 RTL
|
||||
---
|
||||
|
||||
# SliderCaptcha 滑块验证码
|
||||
|
||||
`SliderCaptcha` 是一个「按住滑块拖到最右端」的验证控件,`v-model` 绑 `boolean`,`true` 表示**已验证通过**。
|
||||
|
||||
## 何时使用
|
||||
|
||||
- 登录 / 注册 / 短信验证码等需要「一次人为停顿」的入口;
|
||||
- 轻量混淆脚本、降低误触强度的场景;
|
||||
- **不要**把它当安全边界:拖拽与键盘都能被脚本模拟,它能挡住的只是「顺手连点」,真正的风控要靠服务端。
|
||||
|
||||
## 基础用法
|
||||
|
||||
`v-model` 绑 `boolean`;`success` 在通过的那一刻触发一次,方便埋点或联动下一步。
|
||||
|
||||
<demo vue="captcha/basic.vue" />
|
||||
|
||||
`text` / `successText` 可覆盖语言包文案(默认取 `captcha` 语言包分组)。
|
||||
|
||||
## 尺寸
|
||||
|
||||
`size` 取 `small` / `medium` / `large`,高度分别跟随 `--g3-control-height-sm` / `--g3-control-height` / `--g3-control-height-lg`;手柄是正方形,宽度自动等于高度。
|
||||
|
||||
<demo vue="captcha/sizes.vue" />
|
||||
|
||||
## 与表单一起使用
|
||||
|
||||
放进 `G3FormItem` 时,label 与校验交给表单,滑块只负责提供值。验证是有状态的,**每轮提交都要复位**:清掉绑定值后调 `resume()`,让用户重新拖一次。
|
||||
|
||||
<demo vue="captcha/in-form.vue" />
|
||||
|
||||
## 键盘与无障碍
|
||||
|
||||
手柄是 `role="slider"`,带 `aria-valuemin` / `aria-valuemax` / `aria-valuenow`(0~100 的完成度)与 `aria-valuetext`(当前提示文案,通过后是成功文案),读屏可直接播报状态。
|
||||
|
||||
| 按键 | 行为 |
|
||||
| --- | --- |
|
||||
| `Tab` | 聚焦手柄(未通过时才可聚焦,通过后 `tabindex="-1"`) |
|
||||
| `→` / `↑` | 沿屏幕上「向右」推进一档(行程的 1/10,最少 20px) |
|
||||
| `←` / `↓` | 回退一档 |
|
||||
| `Home` | 回到起点(不复位通过状态) |
|
||||
| `End` | 直达终点并通过验证 |
|
||||
|
||||
`keyboard={false}` 可关掉键盘通道(只保留指针拖拽)。
|
||||
|
||||
## RTL
|
||||
|
||||
位置用逻辑属性 `inset-inline-start` 表达,镜像由 `dir` 自动完成:RTL 下手柄从**右侧**起步、向**左**拖到终点。指针位移按行内方向折算(`useDirection`),方向键的左右也随方向翻转 —— 对用户而言始终是「朝屏幕上那个方向推到底」。
|
||||
|
||||
## API
|
||||
|
||||
### Props
|
||||
|
||||
| 名称 | 类型 | 默认值 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| `modelValue` | `boolean` | `false` | 双向绑定:是否已验证通过 |
|
||||
| `text` | `string` | 语言包 | 未通过时的提示文案 |
|
||||
| `successText` | `string` | 语言包 | 通过后的文案 |
|
||||
| `disabled` | `boolean` | `false` | 禁用态:不响应拖拽与键盘(样式不作整体半透明) |
|
||||
| `size` | `'small' \| 'medium' \| 'large'` | `'medium'` | 尺寸 |
|
||||
| `keyboard` | `boolean` | `true` | 是否支持键盘操作 |
|
||||
|
||||
### Events
|
||||
|
||||
| 名称 | 参数 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `update:modelValue` | `(value: boolean)` | 验证状态变化 |
|
||||
| `success` | `({ isPassing: true })` | 通过时触发一次 |
|
||||
| `start` | `(event: PointerEvent)` | 开始拖拽 |
|
||||
| `move` | `({ event, moveDistance, offset })` | 拖拽过程中触发(键盘操作不触发) |
|
||||
| `end` | `(event: PointerEvent)` | 拖拽结束(含取消) |
|
||||
|
||||
### Slots
|
||||
|
||||
| 名称 | 参数 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `text` | `{ isPassing }` | 覆盖提示文案 |
|
||||
| `action` | `{ isPassing }` | 覆盖手柄里的图标(默认 `ChevronsRight` / 通过后 `Check`,RTL 下由全库的图标镜像规则翻面) |
|
||||
|
||||
### 实例方法
|
||||
|
||||
| 名称 | 签名 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `resume` | `() => void` | 复位到未通过状态(清绑定值 + 回弹动画),校验失败后调用 |
|
||||
|
||||
### 类型定义
|
||||
|
||||
```ts
|
||||
export type SliderCaptchaSize = 'small' | 'medium' | 'large'
|
||||
|
||||
export interface SliderCaptchaProps {
|
||||
modelValue?: boolean
|
||||
text?: string
|
||||
successText?: string
|
||||
disabled?: boolean
|
||||
size?: SliderCaptchaSize
|
||||
keyboard?: boolean
|
||||
}
|
||||
```
|
||||
|
||||
### 样式变量
|
||||
|
||||
| 变量 | 默认值 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `--g3-control-height` | `32px` | 默认高度(手柄宽 = 高度) |
|
||||
| `--g3-control-height-sm` | `24px` | `size="small"` 的高度 |
|
||||
| `--g3-control-height-lg` | `40px` | `size="large"` 的高度 |
|
||||
| `--g3-fill` | `rgba(0, 0, 0, 0.04)` | 轨道底色 |
|
||||
| `--g3-border-color` | `#d9d9d9` | 轨道描边 |
|
||||
| `--g3-color-success` | `hsl(144 57% 58%)` | 进度条与通过后的手柄图标色 |
|
||||
| `--g3-bg-container` | `#fff` | 手柄底色 |
|
||||
|
||||
## 实现说明
|
||||
|
||||
- **拖拽走 Pointer Events + `setPointerCapture`**(与 `bindDrag` / `bindResize` 同一套):指针移出组件、触屏、手写笔都能正确收尾,不需要往 `document` 上挂全局监听;捕获失败的老环境会退化成「移出即松手」,不会卡住。
|
||||
- **跟手不加过渡**:拖拽期间进度条与手柄必须实时跟随指针,只有回弹 / `Home` / `End` 跳转才启用 300ms 过渡(`is-returning`),否则会有「拖不动」的手感。
|
||||
- **行程 = 容器宽 - 手柄宽**:每次交互开始时量一次 `getBoundingClientRect()`,因此容器宽度自适应、字体缩放、`size` 变化都不需要监听 resize。
|
||||
- **只有从「已通过」回退时才 `emit('update:modelValue', false)`**:中途松手的回弹不该对外重复报一次 `false`。
|
||||
- **文案不硬编码**:默认值来自语言包 `captcha.slide` / `captcha.verified` / `captcha.sliderLabel`。
|
||||
- 初始 `modelValue = true`(回填已通过状态)时,手柄在挂载后一帧落到行程末端,不会停在起点。
|
||||
@@ -81,6 +81,7 @@ description: 点击 / 悬停 / 右键触发的操作列表,支持子菜单与
|
||||
| `mouseLeaveDelay` | `number` | `200` | hover 关闭延迟(ms)。有子菜单时适当调大 |
|
||||
| `offset` | `number` | `4` | 与触发器的间距(px) |
|
||||
| `unwrapped` | `boolean` | `true` | 零包裹模式,触发器不额外包 `<span>` |
|
||||
| `menuMaxHeight` | `boolean \| number \| string` | — | 菜单面板最大高度(超出才滚动)。不传 / `true` 用默认上限(`--g3-dropdown-max-height`,兜底 `calc(100vh - 48px)`);`false` 不限制(面板随内容撑开,菜单很短时才用);数字按 px;字符串原样(如 `'320px'` / `'min(480px, 70vh)'`)。面板 Teleport 到 `body`,调用方没法用 CSS 局部覆盖那个变量,所以按实例调整用这个 prop。子菜单面板沿用同一个值 |
|
||||
|
||||
### Events
|
||||
|
||||
@@ -138,7 +139,7 @@ export interface DropdownClickPayload {
|
||||
| 变量 | 默认值 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `--g3-dropdown-min-width` | `180px` | 菜单最小宽度 |
|
||||
| `--g3-dropdown-max-height` | `256px` | 菜单最大高度(超出滚动) |
|
||||
| `--g3-dropdown-max-height` | `calc(100vh - 48px)` | 菜单最大高度(超出滚动);按视口算,菜单长到屏幕放不下才滚 |
|
||||
| `--g3-dropdown-item-padding-y` / `-x` | `5px` / `8px` | 菜单项内边距 |
|
||||
| `--g3-dropdown-item-radius` | `4px` | 菜单项圆角 |
|
||||
| `--g3-dropdown-divider-margin` | `4px 0` | 分隔线外边距 |
|
||||
|
||||
@@ -67,6 +67,7 @@ description: 浮层基座:12 方位定位、自动翻转、外部点击与 Esc
|
||||
| `followTriggerWidth` | `boolean` | `false` | 浮层宽度以触发器宽度为下限 |
|
||||
| `unwrapped` | `boolean` | `false` | 零包裹模式:把事件注入触发器单个根节点,不再包一层 `<span>` |
|
||||
| `popupContainer` | `string \| HTMLElement \| (() => …)` | ConfigProvider 配置 | 浮层挂载容器,默认 `body` |
|
||||
| `popupScope` | `string` | — | 「同一条浮层链」标识:父面板与各级子浮层传同一个值,点击链内任意面板都不算点击外部(Dropdown 的子菜单靠它才能用鼠标点子项) |
|
||||
|
||||
### Events
|
||||
|
||||
|
||||
Reference in new issue
Block a user