init: @ui-arch 私人组件库 monorepo
- core/table-core: Headless 表格内核(排序/筛选/分页/选择插件 + 冒烟 9 项) - core/frame: signal/computed/effect + hyperscript 微框架(零依赖) - skin/ui: 16 个 DOM 组件(含自绘 select、滑动指示条 tabs、行选择 dataTable、form 表单校验) - design/tokens: --ua-* 设计令牌(light/dark) - tools/cli: ua 命令行(index/list/info/docs/style/mcp)+ MCP 注册表服务 - apps: demo 演示站(esbuild 单文件)+ docs 双轨文档(人类版 + llms-full.txt)
This commit is contained in:
@@ -0,0 +1,578 @@
|
||||
{
|
||||
"name": "@ui-arch/ui",
|
||||
"type": "skin-dom",
|
||||
"description": "DOM 皮肤组件库:button / textField / textarea / checkbox / switch / select / radioGroup / card / badge / alert / spinner / progress / tabs / modal / form / formItem / dataTable,构建在 @ui-arch/frame 与 @ui-arch/table-core 之上,样式只消费 --ua-* 令牌。",
|
||||
"tags": [
|
||||
"components",
|
||||
"dom",
|
||||
"skin",
|
||||
"table",
|
||||
"form",
|
||||
"feedback"
|
||||
],
|
||||
"api": {
|
||||
"entry": "src/index.ts",
|
||||
"exports": [
|
||||
"button(props):按钮,variant = primary | ghost | danger",
|
||||
"textField(props):文本输入,value 信号双向绑定",
|
||||
"textarea(props):多行输入,value 信号双向绑定",
|
||||
"checkbox(props):复选框,checked 信号双向绑定",
|
||||
"switchButton(props):开关,checked 信号双向绑定",
|
||||
"select(props):自绘下拉选择(非原生 select),value 信号双向绑定,点外部 / Esc 关闭,选项弹层带微动画",
|
||||
"radioGroup(props):单选组,options + value 信号双向绑定",
|
||||
"card(props, ...children):卡片容器",
|
||||
"badge(props):徽标,variant = default | success | warning | danger",
|
||||
"alert(props):提示条,variant = info | success | warning | danger",
|
||||
"spinner(props):加载指示器,size 控制直径",
|
||||
"progress(props):进度条,轨道 + 渐变流动填充,value 信号驱动 0-100,满格切 success 色",
|
||||
"tabs(props):标签页,active 信号双向绑定,滑动指示条 + 方向键环形导航,支持禁用页签",
|
||||
"modal(props, ...children):弹窗,open 信号驱动显隐,Esc / 遮罩 / × 关闭",
|
||||
"form(props, items):表单容器,布局 + 聚合校验,全部通过才触发 onSubmit",
|
||||
"formItem(props, control):表单字段,label + 必填红星 + 规则校验 + 错误区,返回 { el, validate } 句柄",
|
||||
"dataTable(props):数据表格,粘性表头 / 行选择(全选三态)/ 排序 / 筛选 / 分页 / 空状态,内核为 table-core"
|
||||
],
|
||||
"keyTypes": [
|
||||
"ButtonProps",
|
||||
"TextFieldProps",
|
||||
"TextareaProps",
|
||||
"CheckboxProps",
|
||||
"SwitchProps",
|
||||
"SelectProps",
|
||||
"RadioGroupProps",
|
||||
"OptionItem",
|
||||
"CardProps",
|
||||
"BadgeProps",
|
||||
"AlertProps",
|
||||
"SpinnerProps",
|
||||
"ProgressProps",
|
||||
"TabsProps",
|
||||
"ModalProps",
|
||||
"FormRule",
|
||||
"FormItemProps",
|
||||
"FormItemHandle",
|
||||
"FormProps",
|
||||
"DataTableProps"
|
||||
]
|
||||
},
|
||||
"usage": "const checked = signal(false); checkbox({ label: '同意', checked }); const open = signal(false); modal({ title: '确认', open }, '内容')",
|
||||
"conventions": [
|
||||
"皮肤不持有业务状态,一律委托 headless 内核或调用方信号",
|
||||
"表单类组件统一约定:*Props.value/checked/active 为 Signal 双向绑定,onChange/onInput 为回调",
|
||||
"语义色 variant 四档:default|info / success / warning / danger,一律来自令牌,禁止硬编码颜色"
|
||||
],
|
||||
"docs": {
|
||||
"intro": "16 个 DOM 组件,统一用法:组件是普通函数,传入 props 返回 DOM 节点;需要状态时先创建 signal 再双向绑定。引入 CSS:import '@ui-arch/ui/ui.css'(内部已消费 tokens)。",
|
||||
"steps": [
|
||||
"安装依赖:pnpm add @ui-arch/ui @ui-arch/frame(workspace 内自动链接)",
|
||||
"引入样式:在应用入口 import '@ui-arch/ui/ui.css',暗色主题在根元素设 data-theme='dark'",
|
||||
"创建状态:import { signal } from '@ui-arch/frame',如 const name = signal('')",
|
||||
"调用组件:const el = textField({ value: name }),把 el 挂到你的容器或配合 h() 组合",
|
||||
"读写状态:name() 读、name('张三') 写,界面自动同步,无需手动操作 DOM"
|
||||
],
|
||||
"components": [
|
||||
{
|
||||
"id": "button",
|
||||
"title": "按钮 Button",
|
||||
"demo": "sec-counter",
|
||||
"props": [
|
||||
{
|
||||
"n": "label",
|
||||
"t": "string",
|
||||
"r": 1,
|
||||
"d": "按钮文案"
|
||||
},
|
||||
{
|
||||
"n": "variant",
|
||||
"t": "'primary' | 'ghost' | 'danger'",
|
||||
"d": "视觉强度;缺省 primary"
|
||||
},
|
||||
{
|
||||
"n": "onClick",
|
||||
"t": "() => void",
|
||||
"k": 1,
|
||||
"d": "点击回调"
|
||||
}
|
||||
],
|
||||
"example": "button({ label: '提交', variant: 'primary', onClick: () => console.log('clicked') })",
|
||||
"steps": [
|
||||
"确定语义强度:主操作用 primary(默认),次要操作用 ghost,危险操作用 danger",
|
||||
"绑定 onClick 回调处理点击逻辑",
|
||||
"需要禁用态时自行在回调首行卫语句判断,或外层包条件渲染"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "textField",
|
||||
"title": "文本输入 TextField",
|
||||
"demo": "sec-bind",
|
||||
"props": [
|
||||
{
|
||||
"n": "value",
|
||||
"t": "Signal<string>",
|
||||
"d": "双向绑定信号:输入写回信号,信号变化写回输入框"
|
||||
},
|
||||
{
|
||||
"n": "placeholder",
|
||||
"t": "string",
|
||||
"d": "占位文案"
|
||||
},
|
||||
{
|
||||
"n": "onInput",
|
||||
"t": "(value: string) => void",
|
||||
"k": 1,
|
||||
"d": "输入回调,每次按键触发"
|
||||
}
|
||||
],
|
||||
"example": "const name = signal(''); const el = textField({ value: name, placeholder: '输入名字…', onInput: (v) => console.log(v) })",
|
||||
"steps": [
|
||||
"先用 signal('') 创建字符串信号",
|
||||
"传入 value 完成双向绑定:用户输入自动写回,外部赋值自动回显",
|
||||
"在任意响应式位置用 name() 读取最新值,如 () => `你好,${name()}`"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "textarea",
|
||||
"title": "多行输入 Textarea",
|
||||
"demo": "sec-form",
|
||||
"props": [
|
||||
{
|
||||
"n": "value",
|
||||
"t": "Signal<string>",
|
||||
"d": "双向绑定信号"
|
||||
},
|
||||
{
|
||||
"n": "rows",
|
||||
"t": "number",
|
||||
"d": "初始行数;缺省 3"
|
||||
},
|
||||
{
|
||||
"n": "placeholder",
|
||||
"t": "string",
|
||||
"d": "占位文案"
|
||||
},
|
||||
{
|
||||
"n": "onInput",
|
||||
"t": "(value: string) => void",
|
||||
"k": 1,
|
||||
"d": "输入回调"
|
||||
}
|
||||
],
|
||||
"example": "const bio = signal(''); textarea({ value: bio, rows: 4, placeholder: '一句话介绍…' })",
|
||||
"steps": [
|
||||
"创建信号并传入 value",
|
||||
"用 rows 控制初始高度,用户可纵向拉伸",
|
||||
"字数统计等派生逻辑直接在响应式文本里读 bio().length"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "checkbox",
|
||||
"title": "复选框 Checkbox",
|
||||
"demo": "sec-form",
|
||||
"props": [
|
||||
{
|
||||
"n": "checked",
|
||||
"t": "Signal<boolean>",
|
||||
"d": "勾选状态信号,双向绑定"
|
||||
},
|
||||
{
|
||||
"n": "label",
|
||||
"t": "string",
|
||||
"d": "右侧文案;缺省只渲染勾选框"
|
||||
},
|
||||
{
|
||||
"n": "onChange",
|
||||
"t": "(checked: boolean) => void",
|
||||
"k": 1,
|
||||
"d": "变更回调"
|
||||
}
|
||||
],
|
||||
"example": "const agreed = signal(false); checkbox({ label: '同意条款', checked: agreed, onChange: (v) => console.log(v) })",
|
||||
"steps": [
|
||||
"创建布尔信号 agreed = signal(false)",
|
||||
"传入 checked 与 label,勾选即写回信号",
|
||||
"表单提交前用 agreed() 判断是否勾选"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "switchButton",
|
||||
"title": "开关 Switch",
|
||||
"demo": "sec-form",
|
||||
"props": [
|
||||
{
|
||||
"n": "checked",
|
||||
"t": "Signal<boolean>",
|
||||
"d": "开关状态信号,双向绑定"
|
||||
},
|
||||
{
|
||||
"n": "label",
|
||||
"t": "string",
|
||||
"d": "左侧文案"
|
||||
},
|
||||
{
|
||||
"n": "onChange",
|
||||
"t": "(checked: boolean) => void",
|
||||
"k": 1,
|
||||
"d": "变更回调"
|
||||
}
|
||||
],
|
||||
"example": "const night = signal(true); switchButton({ label: '夜间模式', checked: night })",
|
||||
"steps": [
|
||||
"创建布尔信号并传入 checked",
|
||||
"轨道与滑块的动画由 CSS 完成,无需干预",
|
||||
"状态变化在 onChange 里做副作用(如切换主题)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "select",
|
||||
"title": "下拉选择 Select(自绘弹层)",
|
||||
"demo": "sec-form",
|
||||
"props": [
|
||||
{
|
||||
"n": "options",
|
||||
"t": "OptionItem[]",
|
||||
"r": 1,
|
||||
"d": "选项列表,每项 { value, label }"
|
||||
},
|
||||
{
|
||||
"n": "value",
|
||||
"t": "Signal<string>",
|
||||
"d": "选中值信号,双向绑定"
|
||||
},
|
||||
{
|
||||
"n": "placeholder",
|
||||
"t": "string",
|
||||
"d": "未选中时触发器显示的占位文案"
|
||||
},
|
||||
{
|
||||
"n": "onChange",
|
||||
"t": "(value: string) => void",
|
||||
"k": 1,
|
||||
"d": "选中回调"
|
||||
}
|
||||
],
|
||||
"example": "const city = signal(''); select({ options: [{ value: 'bj', label: '北京' }], value: city, placeholder: '选择城市…' })",
|
||||
"steps": [
|
||||
"准备 options 数组,value 是提交用值、label 是展示文案",
|
||||
"创建信号并传入 value;未传时组件内部自持状态",
|
||||
"点击触发器展开弹层,点选项写回信号并收起;点外部或按 Esc 也会收起",
|
||||
"选中项在弹层中用淡色弱化标记,不抢视觉焦点"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "radioGroup",
|
||||
"title": "单选组 RadioGroup",
|
||||
"demo": "sec-form",
|
||||
"props": [
|
||||
{
|
||||
"n": "options",
|
||||
"t": "OptionItem[]",
|
||||
"r": 1,
|
||||
"d": "选项列表"
|
||||
},
|
||||
{
|
||||
"n": "value",
|
||||
"t": "Signal<string>",
|
||||
"d": "选中值信号,双向绑定"
|
||||
},
|
||||
{
|
||||
"n": "onChange",
|
||||
"t": "(value: string) => void",
|
||||
"k": 1,
|
||||
"d": "变更回调"
|
||||
}
|
||||
],
|
||||
"example": "const size = signal('m'); radioGroup({ options: [{ value: 's', label: '小' }, { value: 'm', label: '中' }], value: size })",
|
||||
"steps": [
|
||||
"创建字符串信号,初值设为默认选项的 value",
|
||||
"传入 options 与 value,组内自动互斥",
|
||||
"读取 size() 获取当前选中值"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "card",
|
||||
"title": "卡片 Card",
|
||||
"demo": "sec-card",
|
||||
"props": [
|
||||
{
|
||||
"n": "title",
|
||||
"t": "string",
|
||||
"d": "标题;缺省不渲染标题行"
|
||||
},
|
||||
{
|
||||
"n": "...children",
|
||||
"t": "unknown[]",
|
||||
"d": "任意子节点:字符串 / DOM / 响应式函数"
|
||||
}
|
||||
],
|
||||
"example": "card({ title: '用户表' }, dataTable({ data, columns }))",
|
||||
"steps": [
|
||||
"card 是容器组件:第一个参数是 props,其余参数是任意子节点",
|
||||
"传 title 显示标题行,否则只有内容区",
|
||||
"子节点可以混合字符串、DOM 节点与响应式函数"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "badge",
|
||||
"title": "徽标 Badge",
|
||||
"demo": "sec-feedback",
|
||||
"props": [
|
||||
{
|
||||
"n": "label",
|
||||
"t": "string",
|
||||
"r": 1,
|
||||
"d": "徽标文案"
|
||||
},
|
||||
{
|
||||
"n": "variant",
|
||||
"t": "'default' | 'success' | 'warning' | 'danger'",
|
||||
"d": "语义色;缺省 default"
|
||||
}
|
||||
],
|
||||
"example": "badge({ label: '成功', variant: 'success' })",
|
||||
"steps": [
|
||||
"选择语义色:default 中性、success 成功、warning 警告、danger 危险",
|
||||
"传入 label 即可,纯展示组件无状态"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "alert",
|
||||
"title": "提示条 Alert",
|
||||
"demo": "sec-feedback",
|
||||
"props": [
|
||||
{
|
||||
"n": "message",
|
||||
"t": "string",
|
||||
"r": 1,
|
||||
"d": "提示内容"
|
||||
},
|
||||
{
|
||||
"n": "variant",
|
||||
"t": "'info' | 'success' | 'warning' | 'danger'",
|
||||
"d": "语义色;缺省 info"
|
||||
}
|
||||
],
|
||||
"example": "alert({ message: '操作可能存在风险', variant: 'warning' })",
|
||||
"steps": [
|
||||
"按信息级别选 variant:info 提示、success 成功、warning 警告、danger 错误",
|
||||
"传入 message 即可,入场自带下滑动画"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "spinner",
|
||||
"title": "加载指示器 Spinner",
|
||||
"demo": "sec-feedback",
|
||||
"props": [
|
||||
{
|
||||
"n": "size",
|
||||
"t": "number",
|
||||
"d": "直径像素;缺省 18"
|
||||
}
|
||||
],
|
||||
"example": "spinner(); spinner({ size: 26 })",
|
||||
"steps": [
|
||||
"数据加载时插入 spinner(),加载完成移除",
|
||||
"用 size 匹配上下文尺寸(按钮内用小号,整页 loading 用大号)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "progress",
|
||||
"title": "进度条 Progress",
|
||||
"demo": "sec-feedback",
|
||||
"props": [
|
||||
{
|
||||
"n": "value",
|
||||
"t": "Signal<number>",
|
||||
"r": 1,
|
||||
"d": "进度 0-100 信号,变化自动平滑过渡"
|
||||
},
|
||||
{
|
||||
"n": "showText",
|
||||
"t": "boolean",
|
||||
"d": "是否显示百分比文字;缺省 true"
|
||||
}
|
||||
],
|
||||
"example": "const pct = signal(0); progress({ value: pct }); setInterval(() => pct((pct() + 5) % 105), 400)",
|
||||
"steps": [
|
||||
"创建数值信号 pct = signal(0),范围 0-100(越界自动钳制)",
|
||||
"传入 value,填充宽度与百分比文字随信号平滑过渡",
|
||||
"进行中填充带流动条纹,到 100% 自动变 success 色并停止条纹",
|
||||
"不需要百分比文字时传 showText: false"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "tabs",
|
||||
"title": "标签页 Tabs",
|
||||
"demo": "sec-tabs-modal",
|
||||
"props": [
|
||||
{
|
||||
"n": "tabs",
|
||||
"t": "OptionItem[]",
|
||||
"r": 1,
|
||||
"d": "页签列表,每项 { value, label }"
|
||||
},
|
||||
{
|
||||
"n": "active",
|
||||
"t": "Signal<string>",
|
||||
"d": "当前激活页签 value 信号,双向绑定"
|
||||
},
|
||||
{
|
||||
"n": "onChange",
|
||||
"t": "(value: string) => void",
|
||||
"k": 1,
|
||||
"d": "切换回调"
|
||||
}
|
||||
],
|
||||
"example": "const tab = signal('a'); tabs({ tabs: [{ value: 'a', label: '概览' }, { value: 'b', label: '明细', disabled: true }], active: tab })",
|
||||
"steps": [
|
||||
"创建信号 tab = signal('a'),初值为默认页签",
|
||||
"tabs 只渲染页签头;页签内容用响应式函数按信号切换,如 () => tab() === 'a' ? panelA : panelB",
|
||||
"点击或方向键(←→ 环形)切换,滑动指示条自动跟随;disabled: true 的页签置灰不可点",
|
||||
"键盘可访问:Tab 聚焦激活页签,←→ 在启用页签间环形移动"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "modal",
|
||||
"title": "弹窗 Modal",
|
||||
"demo": "sec-tabs-modal",
|
||||
"props": [
|
||||
{
|
||||
"n": "open",
|
||||
"t": "Signal<boolean>",
|
||||
"r": 1,
|
||||
"d": "开关信号:true 显示 / false 隐藏"
|
||||
},
|
||||
{
|
||||
"n": "title",
|
||||
"t": "string",
|
||||
"r": 1,
|
||||
"d": "标题"
|
||||
},
|
||||
{
|
||||
"n": "onClose",
|
||||
"t": "() => void",
|
||||
"k": 1,
|
||||
"d": "关闭回调(点遮罩、× 或 Esc 触发)"
|
||||
},
|
||||
{
|
||||
"n": "...children",
|
||||
"t": "unknown[]",
|
||||
"d": "弹窗内容子节点"
|
||||
}
|
||||
],
|
||||
"example": "const open = signal(false); modal({ title: '确认操作', open, onClose: () => open(false) }, h('p', null, '内容'))",
|
||||
"steps": [
|
||||
"创建布尔信号 open = signal(false),弹窗初始隐藏",
|
||||
"把 open 传给 modal,其余参数为弹窗内容子节点",
|
||||
"打开:open(true);关闭:open(false),点遮罩或 × 会自动写回 false",
|
||||
"入场自带遮罩淡入 + 弹窗上浮动画"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "form",
|
||||
"title": "表单 Form / 表单字段 FormItem",
|
||||
"demo": "sec-formlab",
|
||||
"props": [
|
||||
{
|
||||
"n": "layout",
|
||||
"t": "'vertical' | 'horizontal'",
|
||||
"d": "布局方向;缺省 vertical(horizontal 为标签左置)"
|
||||
},
|
||||
{
|
||||
"n": "submitText",
|
||||
"t": "string",
|
||||
"d": "提交按钮文案;缺省「提交」"
|
||||
},
|
||||
{
|
||||
"n": "resetText",
|
||||
"t": "string",
|
||||
"d": "重置按钮文案;缺省不渲染"
|
||||
},
|
||||
{
|
||||
"n": "onSubmit",
|
||||
"t": "() => void",
|
||||
"k": 1,
|
||||
"d": "全部字段校验通过后的提交回调"
|
||||
},
|
||||
{
|
||||
"n": "(formItem)label",
|
||||
"t": "string",
|
||||
"r": 1,
|
||||
"d": "字段标签"
|
||||
},
|
||||
{
|
||||
"n": "(formItem)value",
|
||||
"t": "Signal<string>",
|
||||
"r": 1,
|
||||
"d": "字段值信号,校验数据源"
|
||||
},
|
||||
{
|
||||
"n": "(formItem)required",
|
||||
"t": "boolean",
|
||||
"d": "必填:显示红星并自动追加非空规则"
|
||||
},
|
||||
{
|
||||
"n": "(formItem)rules",
|
||||
"t": "FormRule[]",
|
||||
"d": "校验规则:required / min / max / pattern / validator,按序执行取首个失败"
|
||||
},
|
||||
{
|
||||
"n": "(formItem)validateOn",
|
||||
"t": "'input' | 'blur' | 'submit'",
|
||||
"d": "校验时机;缺省 blur(输入时自动清已报错误)"
|
||||
}
|
||||
],
|
||||
"example": "const name = signal(''); const f = form({ onSubmit: submit }, [formItem({ label: '用户名', required: true, value: name, rules: [{ min: 2, max: 12 }] }, textField({ value: name }))])",
|
||||
"steps": [
|
||||
"为每个字段创建 signal 与控件,如 const name = signal(''); textField({ value: name })",
|
||||
"用 formItem 包住控件:formItem({ label, required, value, rules }, control),返回 { el, validate } 句柄",
|
||||
"把句柄数组交给 form:form({ onSubmit }, items),form 渲染全部字段 + 提交/重置按钮",
|
||||
"提交时逐项校验:全过触发 onSubmit;有错则标红控件、显示错误文案并聚焦首个出错项",
|
||||
"规则语义:required 非空(trim)、min/max 长度、pattern 正则、validator 自定义(true 通过 / 字符串为错误)",
|
||||
"非必填字段为空时跳过其余规则;Enter 键触发表单原生 submit(已拦截走校验流程)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "dataTable",
|
||||
"title": "数据表格 DataTable",
|
||||
"demo": "sec-table",
|
||||
"props": [
|
||||
{
|
||||
"n": "data",
|
||||
"t": "T[]",
|
||||
"r": 1,
|
||||
"d": "全量行数据,每行须含 id 字段作 rowKey"
|
||||
},
|
||||
{
|
||||
"n": "columns",
|
||||
"t": "ColumnDef<T>[]",
|
||||
"r": 1,
|
||||
"d": "列定义:{ key, header?, sortable? }"
|
||||
},
|
||||
{
|
||||
"n": "filterKey",
|
||||
"t": "string",
|
||||
"d": "工具栏筛选的列;缺省取第一列"
|
||||
},
|
||||
{
|
||||
"n": "pageSize",
|
||||
"t": "number",
|
||||
"d": "每页行数;缺省 5"
|
||||
},
|
||||
{
|
||||
"n": "selectable",
|
||||
"t": "boolean",
|
||||
"d": "启用行选择列:全选三态(含半选)、行 checkbox、已选计数;缺省 false"
|
||||
}
|
||||
],
|
||||
"example": "dataTable({ data: users, selectable: true, columns: [{ key: 'name', header: '姓名', sortable: true }], pageSize: 5 })",
|
||||
"steps": [
|
||||
"准备行数据,确保每行有唯一 id(跨页选择与刷新不丢行靠它)",
|
||||
"定义 columns:sortable: true 的列表头可点击排序(升 → 降 → 取消循环),标记箭头 ↑↓↕",
|
||||
"selectable: true 开启行选择:表头全选框带 indeterminate 半选态,工具栏显示已选计数",
|
||||
"选中行用低视觉比重样式(淡色底 + 弱化文字),选择状态存 rowKey,跨页不丢失",
|
||||
"工具栏输入框即筛选,输入即时过滤并回第 1 页;无匹配时显示空状态行",
|
||||
"表头粘性固定(滚动容器 max-height 420px),斑马纹 + 悬停高亮由 CSS 提供"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user