iter(g103): 区分表格两种空状态
This commit is contained in:
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
+1
-1
@@ -1103,7 +1103,7 @@ const content = h(
|
||||
section(
|
||||
'sec-table',
|
||||
'数据表格',
|
||||
'粘性表头、斑马纹、行选择(全选三态 + 已选计数,选中行低视觉比重)、点表头或 Tab 聚焦后按 Enter/空格 排序、定宽列表头右缘可拖拽或 ←→ 调列宽(双击复位)、工具栏筛选、底部分页;工具栏「导出 CSV」导的是筛选 + 排序后的全量行(不含勾选,与当前页无关)。',
|
||||
'粘性表头、斑马纹、行选择(全选三态 + 已选计数,选中行低视觉比重)、点表头或 Tab 聚焦后按 Enter/空格 排序、定宽列表头右缘可拖拽或 ←→ 调列宽(双击复位)、工具栏筛选(0 行时按「有没有筛选词」分别提示「暂无数据」或「没有符合…的行」并给一键清除)、底部分页;工具栏「导出 CSV」导的是筛选 + 排序后的全量行(不含勾选,与当前页无关)。',
|
||||
card({ title: '用户表' }, dataTable({
|
||||
data: users,
|
||||
selectable: true,
|
||||
|
||||
@@ -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<T extends { id: RowKey }>(props: DataTableProps<T>): 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<T extends { id: RowKey }>(props: DataTableProps<T>): H
|
||||
plugins: [filterPlugin<T>(), sortPlugin<T>(), paginationPlugin<T>(), selectionPlugin<T>()],
|
||||
initialState: { pagination: { page: 1, pageSize: props.pageSize ?? 5 } },
|
||||
});
|
||||
const api = table as unknown as TableInstance<T> & {
|
||||
getPageInfo(): PageInfo;
|
||||
selectAllVisible(): void;
|
||||
};
|
||||
const api = table as unknown as TableApi<T>; // 用到的两个取数方法在类型别名里声明
|
||||
const version = signal(0); // 桥接信号:table-core 订阅通知 → 递增触发信号系统
|
||||
table.subscribe(() => version(version() + 1));
|
||||
|
||||
@@ -142,9 +141,7 @@ export function dataTable<T extends { id: RowKey }>(props: DataTableProps<T>): 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<T extends { id: RowKey }>(props: DataTableProps<T>): H
|
||||
const filterInput = textField({
|
||||
placeholder: `筛选 ${filterKey}…`,
|
||||
prefix: '🔍',
|
||||
value: keyword, // 受控信号:空状态里的「清除筛选」能同时清掉输入框内容
|
||||
disabled: props.loading, // 加载中禁止筛选,避免与在途结果竞争
|
||||
onInput: (value) => {
|
||||
table.dispatch({ type: 'setFilter', key: filterKey, value });
|
||||
|
||||
@@ -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, // 清空信号与内核筛选
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -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<T> = TableInstance<T> & { getPageInfo(): PageInfo; selectAllVisible(): void };
|
||||
|
||||
/*** 锚点导航项 ***/
|
||||
export interface AnchorItem {
|
||||
href: string; // 目标区块地址(#id);对应元素须存在于页面上
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user