iter(g55): 新增 checkboxGroup 多选组组件

This commit is contained in:
HIsheR
2026-10-26 12:19:23 +08:00
parent 73b37b1cdf
commit e54fdb6f22
12 changed files with 270 additions and 10 deletions
+60
View File
@@ -0,0 +1,60 @@
import { effect, h, signal } from '@ui-arch/frame';
import type { Signal } from '@ui-arch/frame';
import type { CheckboxGroupProps } from './types.js';
/*** 多选组:value 数组信号双向绑定,min/max 用「禁用边界」而非报错,可选全选行(含半选) ***/
export function checkboxGroup(props: CheckboxGroupProps): HTMLElement {
// —— 状态集中声明 ——
const value: Signal<string[]> = props.value ?? signal<string[]>([]); // 选中值
const inputs: HTMLInputElement[] = []; // 各选项勾选框
const dir = props.direction ?? 'horizontal'; // 排列方向
const usable = props.options.filter((o) => !o.disabled); // 可勾选项(全选基数)
/*** 整组禁用:布尔直接取,信号在此读取以纳入 effect 依赖 ***/
const groupOff = (): boolean => (typeof props.disabled === 'function' ? props.disabled() : props.disabled === true);
/*** 变更落库:写信号 + 回调,边界不在此处拒绝——由 sync 提前禁用越界选项 ***/
const update = (next: string[]): void => {
value(next);
props.onChange?.([...next]);
};
const all = h('input', { type: 'checkbox', class: 'ua-checkbox', 'aria-label': '全选' }) as HTMLInputElement;
all.addEventListener('change', () => {
update(all.checked ? usable.map((o) => o.value) : []); // 全选只覆盖未禁用项
});
const rows = props.options.map((opt) => {
const input = h('input', { type: 'checkbox', class: 'ua-checkbox' }) as HTMLInputElement;
input.value = opt.value;
input.addEventListener('change', () => {
const cur = new Set(value());
input.checked ? cur.add(opt.value) : cur.delete(opt.value); // 用户操作写回
update([...cur]);
});
inputs.push(input);
return h('label', { class: 'ua-check-row' }, input, opt.label);
});
/*** 同步勾选与可见可用性:到上限禁用未选项、到下限禁用已选项(不改值只锁操作) ***/
const sync = (): void => {
const cur = value();
const off = groupOff();
const atMax = props.max != null && cur.length >= props.max;
const atMin = props.min != null && cur.length <= props.min;
props.options.forEach((opt, i) => {
const input = inputs[i];
const on = cur.includes(opt.value);
input.checked = on;
input.disabled = off || opt.disabled === true || (on ? atMin : atMax); // 边界项锁死,避免越界
});
const picked = usable.filter((o) => cur.includes(o.value)).length;
all.checked = picked > 0 && picked === usable.length;
all.indeterminate = picked > 0 && picked < usable.length; // 半选:部分已选
all.disabled = off || (picked === usable.length && atMin); // 全选行同样受边界约束
};
effect(sync);
const body = h('div', { class: `ua-check-group ua-check-group-${dir}` }, ...(props.checkAll ? [h('label', { class: 'ua-check-row' }, all, '全选')] : []), ...rows);
return h('div', { class: 'ua-checkbox-group', role: 'group' }, body) as HTMLElement;
}
+1
View File
@@ -5,6 +5,7 @@ export { inputNumber } from './inputNumber.js';
export { textarea } from './textarea.js';
export { slider } from './slider.js';
export { checkbox } from './checkbox.js';
export { checkboxGroup } from './checkboxGroup.js';
export { switchButton } from './switch.js';
export { select } from './select.js';
export { menu } from './menu.js';
+19
View File
@@ -600,4 +600,23 @@ export interface WatermarkProps {
color?: string; // 颜色(CSS 串或令牌名);缺省取 --ua-color-text-muted 的实色
}
/*** 多选组选项 ***/
export interface CheckOption {
value: string; // 选项值
label: string; // 显示文案
disabled?: boolean; // 单项禁用:不可勾选,也不计入全选
}
/*** 多选组入参 ***/
export interface CheckboxGroupProps {
options: CheckOption[]; // 选项集合
value?: Signal<string[]>; // 选中值数组信号;不传则内部维护
min?: number; // 最少选几项:到下限后已选项禁用,防止取消
max?: number; // 最多选几项:到上限后未选项禁用
checkAll?: boolean; // 是否显示全选行(含半选);缺省 false
direction?: 'horizontal' | 'vertical'; // 排列方向;缺省 horizontal
disabled?: boolean | Signal<boolean>; // 整组禁用
onChange?: (values: string[]) => void; // 变更回调(传副本)
}
export type { ColumnDef, RowKey }; // 再导出常用内核类型,方便调用方
+57 -1
View File
@@ -67,7 +67,8 @@
"backTop(props):返回顶部悬浮按钮,越过阈值才浮现,外环 conic-gradient 显示已滚比例,点击平滑回顶",
"colorPicker(props):颜色选择,预设色板(缺省语义令牌色)+ 原生取色器 + 手输 CSS 颜色串,value 为颜色串信号",
"tour(props):引导漫游,返回 { el, start, close } 句柄;聚光框按 target 选择器定位,卡片自动选方位,Esc / 跳过走 onClose",
"watermark(props, ...children):内容容器 + 平铺水印覆盖层,canvas 生成单元、颜色取自令牌,覆盖层不挡交互"
"watermark(props, ...children):内容容器 + 平铺水印覆盖层,canvas 生成单元、颜色取自令牌,覆盖层不挡交互",
"checkboxGroup(props):多选组,value 为数组信号,min/max 用禁用边界项实现(不抛错),checkAll 支持半选且只统计未禁用项"
],
"keyTypes": [
"ButtonProps",
@@ -2332,6 +2333,61 @@
"color 传 var(--ua-*) 时会先解析成计算值(canvas 不认 CSS 变量),所以暗色主题下颜色会自动跟随",
"覆盖层为 pointer-events:none,下方按钮/输入框照常可点;watermark 根节点自带圆角与裁剪"
]
},
{
"id": "checkboxGroup",
"title": "多选组 CheckboxGroup",
"demo": "sec-checkgroup",
"props": [
{
"n": "options",
"t": "CheckOption[]",
"r": 1,
"d": "选项集合:value / label / 可选 disabled"
},
{
"n": "value",
"t": "Signal<string[]>",
"d": "选中值数组信号;不传则内部维护"
},
{
"n": "min",
"t": "number",
"d": "最少选几项:到下限后已选项禁用,防止取消"
},
{
"n": "max",
"t": "number",
"d": "最多选几项:到上限后未选项禁用"
},
{
"n": "checkAll",
"t": "boolean",
"d": "是否显示全选行(含半选);缺省 false"
},
{
"n": "direction",
"t": "'horizontal' | 'vertical'",
"d": "排列方向;缺省 horizontal"
},
{
"n": "disabled",
"t": "boolean | Signal<boolean>",
"d": "整组禁用"
},
{
"n": "onChange",
"t": "(values: string[]) => void",
"d": "变更回调(传副本)"
}
],
"example": "checkboxGroup({ value: permPick, min: 1, max: 3, checkAll: true, options: [{ value: 'read', label: '读取' }, { value: 'write', label: '写入' }] })",
"steps": [
"options 顺序即渲染顺序;勾选后把整份数组写回 value 信号并触发 onChange",
"max 生效时未选项自动禁用,min 生效时已选项自动禁用——用禁用表达边界,不抛错也不静默回滚",
"checkAll 行的选中/半选只统计未禁用项,禁用项不计入基数",
"direction 传 vertical 改为纵向排列;disabled 支持信号,整组响应式禁用"
]
}
]
}
+11
View File
@@ -3239,3 +3239,14 @@ button.ua-step-dot:hover {
background-repeat: repeat;
background-position: 0 0;
}
.ua-check-group {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: var(--ua-space-2);
}
.ua-check-group-vertical {
flex-direction: column;
align-items: flex-start;
gap: var(--ua-space-1);
}