From e497c160f21019e5b14a578065aff45fcd4c1799 Mon Sep 17 00:00:00 2001 From: HIsheR Date: Mon, 26 Oct 2026 13:54:06 +0800 Subject: [PATCH] =?UTF-8?q?iter(g103):=20=E5=8C=BA=E5=88=86=E8=A1=A8?= =?UTF-8?q?=E6=A0=BC=E4=B8=A4=E7=A7=8D=E7=A9=BA=E7=8A=B6=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .ua/backlog.json | 8 ++++++++ apps/demo/main.ts | 2 +- packages/skin/ui/src/dataTable.ts | 16 +++++++--------- packages/skin/ui/src/tableEmpty.ts | 18 ++++++++++++++++++ packages/skin/ui/src/types.ts | 5 ++++- packages/skin/ui/ui.css | 4 ++++ 6 files changed, 42 insertions(+), 11 deletions(-) create mode 100644 packages/skin/ui/src/tableEmpty.ts diff --git a/.ua/backlog.json b/.ua/backlog.json index 840a954..48de044 100644 --- a/.ua/backlog.json +++ b/.ua/backlog.json @@ -1180,6 +1180,14 @@ "acceptance": "overlay.ts 新增 trapFocus(box, isOpen):Tab / Shift+Tab 首尾环形,有无可用焦点与焦点在容器外的两种情况都拦回;modal / drawer 删掉本地实现改为调用(行为不变);image 预览层补 tabindex=-1 + 打开即聚焦 + 全程锁焦点;npm run check 全绿", "notes": "自补:一致性与无障碍;陷阱实现单一来源,后续新增模态层直接复用", "status": "done" + }, + { + "id": "g103", + "title": "表格空状态区分「没数据」与「被筛掉」", + "motivation": "dataTable 不论有没有筛选词,0 行时一律显示「暂无匹配数据」:数据本身就空的时候提示「匹配」让人以为筛选还在生效,而筛选筛空的时候又不给任何出口,用户只能手动删掉输入框里的字才发现原来有数据", + "acceptance": "有筛选词时显示「没有符合「关键词」的行」并给「清除筛选」按钮(一次性清空关键字信号与内核筛选、回第 1 页);无筛选词时显示「暂无数据」且不出现按钮;关键字用受控信号持有,与筛选框同源;每行文件仍 ≤ 200 行;npm run check 全绿", + "notes": "自补:可用性;空状态行抽到 tableEmpty.ts,dataTable 顺手把 api 类型别名下沉到 types.ts 才没破 200 行", + "status": "open" } ] } diff --git a/apps/demo/main.ts b/apps/demo/main.ts index a7ac9c6..eda929a 100644 --- a/apps/demo/main.ts +++ b/apps/demo/main.ts @@ -1103,7 +1103,7 @@ const content = h( section( 'sec-table', '数据表格', - '粘性表头、斑马纹、行选择(全选三态 + 已选计数,选中行低视觉比重)、点表头或 Tab 聚焦后按 Enter/空格 排序、定宽列表头右缘可拖拽或 ←→ 调列宽(双击复位)、工具栏筛选、底部分页;工具栏「导出 CSV」导的是筛选 + 排序后的全量行(不含勾选,与当前页无关)。', + '粘性表头、斑马纹、行选择(全选三态 + 已选计数,选中行低视觉比重)、点表头或 Tab 聚焦后按 Enter/空格 排序、定宽列表头右缘可拖拽或 ←→ 调列宽(双击复位)、工具栏筛选(0 行时按「有没有筛选词」分别提示「暂无数据」或「没有符合…的行」并给一键清除)、底部分页;工具栏「导出 CSV」导的是筛选 + 排序后的全量行(不含勾选,与当前页无关)。', card({ title: '用户表' }, dataTable({ data: users, selectable: true, diff --git a/packages/skin/ui/src/dataTable.ts b/packages/skin/ui/src/dataTable.ts index bb95f2a..9cc2d87 100644 --- a/packages/skin/ui/src/dataTable.ts +++ b/packages/skin/ui/src/dataTable.ts @@ -5,19 +5,21 @@ import { selectionPlugin, sortPlugin, } from '@ui-arch/table-core'; -import type { PageInfo, RowKey, TableInstance } from '@ui-arch/table-core'; +import type { RowKey } from '@ui-arch/table-core'; import { computed, effect, h, renderList, signal } from '@ui-arch/frame'; import { attachResize } from './columnResize.js'; import { exportButton } from './csv.js'; import { button } from './button.js'; import { skeleton } from './skeleton.js'; import { textField } from './textField.js'; -import type { DataTableProps } from './types.js'; +import { emptyRow } from './tableEmpty.js'; +import type { DataTableProps, TableApi } from './types.js'; /*** 数据表格:粘性表头 + 行选择 + 排序 + 筛选 + 分页 + 空状态;状态由 table-core 持有,信号只做桥接渲染 ***/ export function dataTable(props: DataTableProps): HTMLElement { // —— 状态集中声明 —— const selectable = props.selectable ?? false; // 行选择开关 + const keyword = signal(''); // 筛选框关键字:受控信号,空状态里的「清除筛选」靠它一键还原 const table = createTable({ data: props.data, columns: props.columns, @@ -25,10 +27,7 @@ export function dataTable(props: DataTableProps): H plugins: [filterPlugin(), sortPlugin(), paginationPlugin(), selectionPlugin()], initialState: { pagination: { page: 1, pageSize: props.pageSize ?? 5 } }, }); - const api = table as unknown as TableInstance & { - getPageInfo(): PageInfo; - selectAllVisible(): void; - }; + const api = table as unknown as TableApi; // 用到的两个取数方法在类型别名里声明 const version = signal(0); // 桥接信号:table-core 订阅通知 → 递增触发信号系统 table.subscribe(() => version(version() + 1)); @@ -142,9 +141,7 @@ export function dataTable(props: DataTableProps): H if (busy()) return skeletonRows(); // 加载态优先:不看数据,避免闪出旧行 const sel = state().selection; // 读入依赖:选择变化即整表重绘(行数 = pageSize,代价可控) const rows = visible().rows; - if (!rows.length) { - return [h('tr', { class: 'ua-empty-row' }, h('td', { colspan }, '暂无匹配数据'))]; - } + if (!rows.length) return [emptyRow(colspan, keyword, () => table.dispatch({ type: 'setFilter', key: filterKey, value: '' }))]; // 空状态:筛选词与内核筛选一起清 return rows.map((row, i) => { const key = visible().keys[i]; const selected = sel.includes(key); @@ -170,6 +167,7 @@ export function dataTable(props: DataTableProps): H const filterInput = textField({ placeholder: `筛选 ${filterKey}…`, prefix: '🔍', + value: keyword, // 受控信号:空状态里的「清除筛选」能同时清掉输入框内容 disabled: props.loading, // 加载中禁止筛选,避免与在途结果竞争 onInput: (value) => { table.dispatch({ type: 'setFilter', key: filterKey, value }); diff --git a/packages/skin/ui/src/tableEmpty.ts b/packages/skin/ui/src/tableEmpty.ts new file mode 100644 index 0000000..9ca634d --- /dev/null +++ b/packages/skin/ui/src/tableEmpty.ts @@ -0,0 +1,18 @@ +import { h } from '@ui-arch/frame'; +import type { Signal } from '@ui-arch/frame'; +import { button } from './button.js'; + +/*** 表格空状态行:有筛选词说明是筛掉的并给一键清除,没有筛选词就是真没数据;两种语义不该混成一句话 ***/ +export function emptyRow(colspan: number, keyword: Signal, onClear: () => void): HTMLElement { + const q = keyword().trim(); // 读信号:筛选词变化时整表重绘会带上新文案 + return h( + 'tr', + { class: 'ua-empty-row' }, + h( + 'td', + { colspan }, + h('span', { class: 'ua-empty-tip' }, q ? `没有符合「${q}」的行` : '暂无数据'), + q ? button({ label: '清除筛选', variant: 'ghost', onClick: () => { keyword(''); onClear(); } }) : null, // 清空信号与内核筛选 + ), + ); +} diff --git a/packages/skin/ui/src/types.ts b/packages/skin/ui/src/types.ts index 41bdda9..07ef531 100644 --- a/packages/skin/ui/src/types.ts +++ b/packages/skin/ui/src/types.ts @@ -1,4 +1,4 @@ -import type { ColumnDef, RowKey } from '@ui-arch/table-core'; +import type { ColumnDef, PageInfo, RowKey, TableInstance } from '@ui-arch/table-core'; import type { Signal } from '@ui-arch/frame'; /*** 悬浮提示入参 ***/ @@ -679,6 +679,9 @@ export interface SpaceProps { block?: boolean; // 是否占满父容器宽度;缺省按内容收缩 } +/*** 表格扩展接口:table-core 实例 + 皮肤层用到的两个取数方法 ***/ +export type TableApi = TableInstance & { getPageInfo(): PageInfo; selectAllVisible(): void }; + /*** 锚点导航项 ***/ export interface AnchorItem { href: string; // 目标区块地址(#id);对应元素须存在于页面上 diff --git a/packages/skin/ui/ui.css b/packages/skin/ui/ui.css index 2fa261a..30b2f63 100644 --- a/packages/skin/ui/ui.css +++ b/packages/skin/ui/ui.css @@ -195,6 +195,10 @@ body.ua-col-resizing { color: var(--ua-color-text-muted); padding: 28px 12px; } +.ua-empty-tip { + display: inline-block; + margin-right: var(--ua-space-2); +} .ua-table-scroll { max-height: 420px; overflow: auto;