This commit is contained in:
oneao committed 2026-10-12 17:32:50 +08:00
1 parent 78eb811ff7
commit 97fe2ff306
83 files changed
+10083 -3662

No files matched your search

@@ -1,234 +0,0 @@
---
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,183 @@
---
title: Table 表格
description: 高性能虚拟滚动表格,支持排序、固定列、多选、行内编辑、列宽 / 列序拖拽与行拖拽排序
---
# Table 表格
`Table` 用于展示结构化数据,底层是 [`stk-table-vue`](https://ja-plus.github.io/stk-table-vue/),虚拟滚动 / 列宽拖拽 / 列序拖拽 / 区域选择 / 行拖拽排序都由它提供;组件负责**性能门控**、**内部列管理**与**行内编辑态**。
## 何时使用
- 数据量大(成百上千行)且需要滚动流畅时;
- 需要行内编辑、多选批量操作、列宽 / 列序可调时;
- 数据量很小、只要一张静态表时,原生 `<table>` 或 [Grid](/ui/general/grid) 更轻。
## 基础用法
给 `columns` 与 `dataSource` 两份数据即可。序号列(`seq`)默认开启,外框与圆角默认有(`bordered`)。
<demo vue="table/basic.vue" />
## 虚拟滚动与性能
组件内置两条门控,**自动**决定是否启用虚拟滚动,不需要业务手动切换:
| 方向 | 阈值 | 低于阈值时 | 原因 |
| --- | --- | --- | --- |
| 纵向 | 行数 > 100 | 全量渲染 | 库的虚拟滚动在「行数略多于单屏行数」时会把最后几行算出视口外,必须滚动才可见;行数少时全量渲染没有性能代价,却换来确定性正确的渲染 |
| 横向 | 列数 > 20 | 关掉横向虚拟 | 库在横向虚拟开启时会把每列宽度钉死为 `minWidth ?? width ?? 100`,未设 `width` 的列会失去「按比例铺满容器」的原生自适应 |
可以用 `virtual` / `virtualX` 关掉,但**不能**把它们强制打开到阈值以下。
<demo vue="table/virtual.vue" />
除了门控,还有三处性能相关设计:
- **无编辑行时不注入 `customCell`**:纯展示列与非编辑态的可编辑列走库的原生 `td` 渲染路径,滚动时每格少一个函数式组件实例;
- **尺寸变化 150ms 防抖**:折叠动画 / 窗口缩放期间 ResizeObserver 每帧都会通知,逐帧重算会强制 reflow 并重渲染可见行列;组件合并到尺寸稳定后只刷新一次,并区分「仅宽度变化」只重算横向;
- **选择列用原生 `<input type="checkbox">`**:视觉由 CSS 模拟成 [Checkbox](/ui/form/checkbox) 的外观,但每行不创建组件实例。
## 多选与批量操作
传 `rowSelection` 即在最前注入选择列;`getCheckboxProps` 可禁用个别行(禁用行不计入「全选」的分母)。
<demo vue="table/selection.vue" />
## 排序
列上写 `sorter: true` 即可点表头排序(组件内排序,不请求接口)。`sortType` 决定按数字还是字符串比较,默认字符串;需要自定义规则时 `sorter` 也可以传函数。
<demo vue="table/sorting.vue" />
## 行内编辑
列上加 `editor` 即可编辑:单击单元格进入,`Enter` 确认、`Esc` 取消。
| 编辑器类型 | 说明 |
| --- | --- |
| `text` / `number` / `textarea` | 用 [Input](/ui/form/input) 渲染 |
| `select` | 用 [Select](/ui/form/select) 渲染;`options` 可以是数组,也可以是接收当前行的函数 |
| `checkbox` | 点击即切换,无需先激活整行;约定 `1` = 选中、`0` = 未选中(与后端 tinyint 对齐) |
编辑器的输入值先写进内部草稿,**失焦才落库并抛出 `change`**。这样每敲一个字符不会触发父级重渲染(否则光标会丢、输入法会断)。用 `editable={false}` 可整体禁用编辑。
<demo vue="table/editable.vue" />
## 操作列
列上加 `actions` 渲染成一排小按钮;`type: 'more'` 的项会收进「更多」下拉。带 `confirm` 的操作先弹确认框,确认后才执行 `onClick`。
操作列建议显式写 `resizable: false`:它的宽度由内容决定,拖了不会落库,列一重建就丢。
<demo vue="table/actions.vue" />
## 列宽、列序与行拖拽
列宽 / 列序拖拽要求用 `v-model:columns` 绑定列定义——否则组件拖完无处回写(松手回弹),会直接关掉这两个功能。行拖拽排序用 `row-draggable`,顺序变化通过 `row-order-change` 回传。
<demo vue="table/resizable.vue" />
## API
### Props
| 名称 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| `columns` | `G3TableColumn<T>[]` | `[]` | 列定义,配合 `v-model:columns` 接收拖拽后的回写 |
| `dataSource` | `T[]` | `[]` | 数据源 |
| `rowKey` | `string \| (row) => string \| number` | `'id'` | 行唯一键 |
| `virtual` | `boolean` | `true` | 纵向虚拟滚动总开关(实际是否开启受行数门控) |
| `virtualX` | `boolean` | `true` | 横向虚拟滚动总开关(实际是否开启受列数门控) |
| `rowHeight` | `number` | `30` | 行高,同时驱动虚拟滚动计算 |
| `headerRowHeight` | `number` | `34` | 表头行高 |
| `bordered` | `boolean \| string` | `true` | 外框圆角描边 + 行分隔线;`false` 时只留行分隔线 |
| `colResizable` | `boolean \| object` | `true` | 列宽拖拽;未绑定 `v-model:columns` 时自动关闭。可用 `{ disabled: (col) => boolean }` 按列禁用 |
| `columnDraggable` | `boolean \| object` | `true` | 列序拖拽;同上要求绑定 `v-model:columns` |
| `rowDraggable` | `boolean` | `false` | 行拖拽排序 |
| `rowSelection` | `TableRowSelection \| null` | `null` | 多选配置,见下 |
| `seq` | `boolean \| { title?, width? }` | `true` | 序号列 |
| `activeRowKey` | `string \| number \| null` | `null` | 当前高亮行 |
| `editable` | `boolean` | `true` | 行内编辑总开关 |
| `noDataFull` | `boolean` | `true` | 空态铺满容器高度 |
| `showOverflow` | `boolean` | `true` | 内容溢出省略 |
| `autoFitWidth` | `boolean` | `false` | 非横向虚拟时铺满容器宽度(开启后关闭列宽拖拽) |
| `theme` | `'light' \| 'dark'` | — | 不传则跟随全局主题(`ConfigProvider` / `html[data-g3-theme]`) |
| `footerData` | `T[]` | `[]` | 表尾数据 |
| `footerConfig` | `object` | `` | 表尾配置 |
### 列定义 `G3TableColumn<T>`
在 `stk-table-vue` 的列类型基础上扩展;下面只列自有字段,其余(`width` / `minWidth` / `fixed` / `align` / `headerAlign` / `sorter` / `sortType` / `children` / `mergeCells` 等)原样透传。
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `dataIndex` | `keyof T & string` | 取值字段 |
| `title` | `string` | 表头文字 |
| `resizable` | `boolean` | 该列是否可拖宽;操作列 / 展开列建议设 `false` |
| `editor` | `TableEditor<T>` | 行内编辑器,见下 |
| `actions` | `TableAction<T>[]` | 操作列按钮 |
| `customCell` | `(props) => VNode \| string` | 自定义单元格渲染(逃生舱),传了就不再自动包装 |
### `editor`
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `type` | `'text' \| 'number' \| 'password' \| 'textarea' \| 'select' \| 'checkbox'` | 编辑器类型 |
| `options` | `TableSelectOption[] \| ((row) => TableSelectOption[])` | select 选项,可依赖当前行其它字段 |
| `disabled` | `boolean \| ((row) => boolean)` | 是否禁用该格编辑 |
| `allowClear` | `boolean` | select 是否可清空,默认 `true` |
| `emptyValue` | `unknown` | select 清空后落库的值,默认 `null` |
| `onChange` | `(value, row, col) => void` | 本列值变化回调(早于组件的 `change`) |
### `actions`
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `key` | `string \| number` | 唯一键;`type: 'more'` 的子项用它做下拉 key |
| `label` | `string` | 文案 |
| `type` | `'default' \| 'danger' \| 'delete' \| 'more'` | 语义类型 |
| `icon` | `Component` | 图标组件 |
| `visible` | `boolean \| ((row) => boolean)` | 是否显示 |
| `disabled` | `boolean \| ((row) => boolean)` | 是否禁用 |
| `confirm` | `string \| object` | 二次确认;字符串为标题,对象支持 `title` / `description` / `okText` / `cancelText` / `okType` / `width` / `maskClosable` |
| `onClick` | `(row) => void` | 点击回调(带 `confirm` 时在确认后执行) |
| `items` | `TableAction<T>[]` | `type: 'more'` 时的子项 |
### Events
| 名称 | 参数 | 说明 |
| --- | --- | --- |
| `update:columns` | `(columns)` | 列宽 / 列序拖拽后回写(内部列已剥离) |
| `update:selectedRowKeys` | `(keys)` | 勾选变化 |
| `change` | `(row, col, value)` | 行内编辑的值变化 |
| `row-order-change` | `(rows, sourceIndex, targetIndex)` | 行拖拽排序完成 |
| `area-selection-change` | `(ranges)` | 单元格选区变化(行内编辑依赖它) |
### Exposed
| 名称 | 类型 | 说明 |
| --- | --- | --- |
| `scrollToRow(rowKey)` | `Promise<void>` | 滚动到指定行 |
| `editRow(rowKey, focusCol?)` | `Promise<void>` | 滚动到指定行并进入编辑 |
| `initVirtualScroll()` / `refreshLayout()` | — | 手动重算虚拟滚动布局 |
| `getSelectedArea()` / `clearSelectedArea()` | — | 读取 / 清空单元格选区 |
| `setRowExpand(...)` | — | 展开 / 收起行(需配 `expand` 列) |
### Slots
| 名称 | 说明 |
| --- | --- |
| `empty` | 空态内容,默认显示「暂无数据」 |
| `expand` | 展开行内容(需列配置 `type: 'expand'`) |
## 实现说明
- **底层是 `stk-table-vue`**:虚拟滚动、列宽 / 列序拖拽、区域选择、行拖拽、二级表头、固定列、合并单元格都由它实现。组件不重复造这些轮子,只做门控与适配。
- **库样式已并入 `dist/style.css`**:库的 CSS 是独立文件(其 JS 里不含任何 `.css` import),组件 `@use` 了它,消费方不需要额外引一次第三方样式。
- 注意 SCSS 里**不能**写 `@use 'stk-table-vue/lib/style.css'`:带 `.css` 后缀时 Sass 不会内联,只原样输出一条 CSS `@import` 语句,产物里会留下一条浏览器解析不了的裸包路径。去掉后缀才会真正内联。
- **主题通过覆盖库的 CSS 变量实现**:库把 41 个主题值都做成了 `.stk-table` 作用域内的变量,组件把它们映射到 `--g3-*` 令牌(覆盖层在 `src/table/style/_overrides.scss`)。改变量而不是逐个选择器改样式,库升级时不容易失效。
- **区域选择按需自注册**:库的单元格区域选择是全局注册的(`registerFeature(useAreaSelection)`),不注册则 `area-selection-change` 永不触发、行内编辑失效。组件库不拥有 app 启动流程,因此**首次挂载时自动注册一次**;需要显式控制时可调用导出的 `registerTableFeatures()`。
- **`data-testid` 透传到内层**:组件 `inheritAttrs: false`,`$attrs` 落在内层 `StkTable` 上。外框 / 圆角在我们自己的根节点 `.g3-table` 上,写 E2E 时按类名定位根节点。
- **行编辑态存在 `WeakMap` 里**:按行对象存,不给业务数据打标记;行被回收时状态自动释放。行与行之间切换编辑**不翻转** `editingActive`,避免整表列重建。
- **RTL**:布局用逻辑属性,带符号位移走 `--g3-dir-sign`;分隔条拖拽与键盘方向由库的 `useDirection` 处理。