iter(g103): 区分表格两种空状态

This commit is contained in:
HIsheR
2026-10-26 13:54:06 +08:00
parent f4701af984
commit e497c160f2
6 changed files with 42 additions and 11 deletions
+8
View File
@@ -1180,6 +1180,14 @@
"acceptance": "overlay.ts 新增 trapFocus(box, isOpen):Tab / Shift+Tab 首尾环形,有无可用焦点与焦点在容器外的两种情况都拦回;modal / drawer 删掉本地实现改为调用(行为不变);image 预览层补 tabindex=-1 + 打开即聚焦 + 全程锁焦点;npm run check 全绿", "acceptance": "overlay.ts 新增 trapFocus(box, isOpen):Tab / Shift+Tab 首尾环形,有无可用焦点与焦点在容器外的两种情况都拦回;modal / drawer 删掉本地实现改为调用(行为不变);image 预览层补 tabindex=-1 + 打开即聚焦 + 全程锁焦点;npm run check 全绿",
"notes": "自补:一致性与无障碍;陷阱实现单一来源,后续新增模态层直接复用", "notes": "自补:一致性与无障碍;陷阱实现单一来源,后续新增模态层直接复用",
"status": "done" "status": "done"
},
{
"id": "g103",
"title": "表格空状态区分「没数据」与「被筛掉」",
"motivation": "dataTable 不论有没有筛选词,0 行时一律显示「暂无匹配数据」:数据本身就空的时候提示「匹配」让人以为筛选还在生效,而筛选筛空的时候又不给任何出口,用户只能手动删掉输入框里的字才发现原来有数据",
"acceptance": "有筛选词时显示「没有符合「关键词」的行」并给「清除筛选」按钮(一次性清空关键字信号与内核筛选、回第 1 页);无筛选词时显示「暂无数据」且不出现按钮;关键字用受控信号持有,与筛选框同源;每行文件仍 ≤ 200 行;npm run check 全绿",
"notes": "自补:可用性;空状态行抽到 tableEmpty.ts,dataTable 顺手把 api 类型别名下沉到 types.ts 才没破 200 行",
"status": "open"
} }
] ]
} }
+1 -1
View File
@@ -1103,7 +1103,7 @@ const content = h(
section( section(
'sec-table', 'sec-table',
'数据表格', '数据表格',
'粘性表头、斑马纹、行选择(全选三态 + 已选计数,选中行低视觉比重)、点表头或 Tab 聚焦后按 Enter/空格 排序、定宽列表头右缘可拖拽或 ←→ 调列宽(双击复位)、工具栏筛选、底部分页;工具栏「导出 CSV」导的是筛选 + 排序后的全量行(不含勾选,与当前页无关)。', '粘性表头、斑马纹、行选择(全选三态 + 已选计数,选中行低视觉比重)、点表头或 Tab 聚焦后按 Enter/空格 排序、定宽列表头右缘可拖拽或 ←→ 调列宽(双击复位)、工具栏筛选(0 行时按「有没有筛选词」分别提示「暂无数据」或「没有符合…的行」并给一键清除)、底部分页;工具栏「导出 CSV」导的是筛选 + 排序后的全量行(不含勾选,与当前页无关)。',
card({ title: '用户表' }, dataTable({ card({ title: '用户表' }, dataTable({
data: users, data: users,
selectable: true, selectable: true,
+7 -9
View File
@@ -5,19 +5,21 @@ import {
selectionPlugin, selectionPlugin,
sortPlugin, sortPlugin,
} from '@ui-arch/table-core'; } 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 { computed, effect, h, renderList, signal } from '@ui-arch/frame';
import { attachResize } from './columnResize.js'; import { attachResize } from './columnResize.js';
import { exportButton } from './csv.js'; import { exportButton } from './csv.js';
import { button } from './button.js'; import { button } from './button.js';
import { skeleton } from './skeleton.js'; import { skeleton } from './skeleton.js';
import { textField } from './textField.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 持有,信号只做桥接渲染 ***/ /*** 数据表格:粘性表头 + 行选择 + 排序 + 筛选 + 分页 + 空状态;状态由 table-core 持有,信号只做桥接渲染 ***/
export function dataTable<T extends { id: RowKey }>(props: DataTableProps<T>): HTMLElement { export function dataTable<T extends { id: RowKey }>(props: DataTableProps<T>): HTMLElement {
// —— 状态集中声明 —— // —— 状态集中声明 ——
const selectable = props.selectable ?? false; // 行选择开关 const selectable = props.selectable ?? false; // 行选择开关
const keyword = signal(''); // 筛选框关键字:受控信号,空状态里的「清除筛选」靠它一键还原
const table = createTable({ const table = createTable({
data: props.data, data: props.data,
columns: props.columns, columns: props.columns,
@@ -25,10 +27,7 @@ export function dataTable<T extends { id: RowKey }>(props: DataTableProps<T>): H
plugins: [filterPlugin<T>(), sortPlugin<T>(), paginationPlugin<T>(), selectionPlugin<T>()], plugins: [filterPlugin<T>(), sortPlugin<T>(), paginationPlugin<T>(), selectionPlugin<T>()],
initialState: { pagination: { page: 1, pageSize: props.pageSize ?? 5 } }, initialState: { pagination: { page: 1, pageSize: props.pageSize ?? 5 } },
}); });
const api = table as unknown as TableInstance<T> & { const api = table as unknown as TableApi<T>; // 用到的两个取数方法在类型别名里声明
getPageInfo(): PageInfo;
selectAllVisible(): void;
};
const version = signal(0); // 桥接信号:table-core 订阅通知 → 递增触发信号系统 const version = signal(0); // 桥接信号:table-core 订阅通知 → 递增触发信号系统
table.subscribe(() => version(version() + 1)); table.subscribe(() => version(version() + 1));
@@ -142,9 +141,7 @@ export function dataTable<T extends { id: RowKey }>(props: DataTableProps<T>): H
if (busy()) return skeletonRows(); // 加载态优先:不看数据,避免闪出旧行 if (busy()) return skeletonRows(); // 加载态优先:不看数据,避免闪出旧行
const sel = state().selection; // 读入依赖:选择变化即整表重绘(行数 = pageSize,代价可控) const sel = state().selection; // 读入依赖:选择变化即整表重绘(行数 = pageSize,代价可控)
const rows = visible().rows; const rows = visible().rows;
if (!rows.length) { if (!rows.length) return [emptyRow(colspan, keyword, () => table.dispatch({ type: 'setFilter', key: filterKey, value: '' }))]; // 空状态:筛选词与内核筛选一起清
return [h('tr', { class: 'ua-empty-row' }, h('td', { colspan }, '暂无匹配数据'))];
}
return rows.map((row, i) => { return rows.map((row, i) => {
const key = visible().keys[i]; const key = visible().keys[i];
const selected = sel.includes(key); const selected = sel.includes(key);
@@ -170,6 +167,7 @@ export function dataTable<T extends { id: RowKey }>(props: DataTableProps<T>): H
const filterInput = textField({ const filterInput = textField({
placeholder: `筛选 ${filterKey}…`, placeholder: `筛选 ${filterKey}…`,
prefix: '🔍', prefix: '🔍',
value: keyword, // 受控信号:空状态里的「清除筛选」能同时清掉输入框内容
disabled: props.loading, // 加载中禁止筛选,避免与在途结果竞争 disabled: props.loading, // 加载中禁止筛选,避免与在途结果竞争
onInput: (value) => { onInput: (value) => {
table.dispatch({ type: 'setFilter', key: filterKey, value }); table.dispatch({ type: 'setFilter', key: filterKey, value });
+18
View File
@@ -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<string>, 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, // 清空信号与内核筛选
),
);
}
+4 -1
View File
@@ -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'; import type { Signal } from '@ui-arch/frame';
/*** 悬浮提示入参 ***/ /*** 悬浮提示入参 ***/
@@ -679,6 +679,9 @@ export interface SpaceProps {
block?: boolean; // 是否占满父容器宽度;缺省按内容收缩 block?: boolean; // 是否占满父容器宽度;缺省按内容收缩
} }
/*** 表格扩展接口:table-core 实例 + 皮肤层用到的两个取数方法 ***/
export type TableApi<T> = TableInstance<T> & { getPageInfo(): PageInfo; selectAllVisible(): void };
/*** 锚点导航项 ***/ /*** 锚点导航项 ***/
export interface AnchorItem { export interface AnchorItem {
href: string; // 目标区块地址(#id);对应元素须存在于页面上 href: string; // 目标区块地址(#id);对应元素须存在于页面上
+4
View File
@@ -195,6 +195,10 @@ body.ua-col-resizing {
color: var(--ua-color-text-muted); color: var(--ua-color-text-muted);
padding: 28px 12px; padding: 28px 12px;
} }
.ua-empty-tip {
display: inline-block;
margin-right: var(--ua-space-2);
}
.ua-table-scroll { .ua-table-scroll {
max-height: 420px; max-height: 420px;
overflow: auto; overflow: auto;