iter: e02 新增 stepper 步骤条组件(三态流转 + active 信号双向绑定 + clickable 回跳)
This commit is contained in:
@@ -24,6 +24,7 @@ export { pagination } from './pagination.js';
|
||||
export { spinner } from './spinner.js';
|
||||
export { progress } from './progress.js';
|
||||
export { tabs } from './tabs.js';
|
||||
export { stepper } from './stepper.js';
|
||||
export { modal } from './modal.js';
|
||||
export { drawer } from './drawer.js';
|
||||
export { form, formItem } from './form.js';
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { effect, h } from '@ui-arch/frame';
|
||||
import type { StepperProps } from './types.js';
|
||||
|
||||
/*** 步骤条:done/current/upcoming 三态横向流程指示,clickable 时已完成步骤可点回跳并写回 active 信号 ***/
|
||||
export function stepper(props: StepperProps): HTMLElement {
|
||||
const active = props.active;
|
||||
const clickStep = (i: number): void => { if (!props.clickable || i >= active()) return; // 卫语句:不可点或未完成不回跳
|
||||
active(i); if (props.onChange) props.onChange(i); }; // 写回信号并回调
|
||||
const render = (): HTMLElement => {
|
||||
const cur = active();
|
||||
const items = props.steps.map((s, i) => {
|
||||
const state = i < cur ? 'done' : i === cur ? 'current' : 'upcoming'; // 三态:已完成 / 当前 / 未到
|
||||
const mark = state === 'done' ? '✓' : String(i + 1); // 完成打勾,其余显示序号
|
||||
const clickable = !!props.clickable && i < cur; // 仅已完成步骤可回跳
|
||||
const head = clickable
|
||||
? h('button', { class: 'ua-step-dot', type: 'button', onClick: () => clickStep(i) }, mark)
|
||||
: h('span', { class: 'ua-step-dot' }, mark);
|
||||
const body = h('div', { class: 'ua-step-text' }, h('span', { class: 'ua-step-label' }, s.label),
|
||||
s.description ? h('span', { class: 'ua-step-desc' }, s.description) : null);
|
||||
const li = h('li', { class: `ua-step ua-step-${state}`, 'aria-current': state === 'current' ? 'step' : null }, head, body);
|
||||
return li;
|
||||
});
|
||||
return h('ol', { class: 'ua-stepper' }, ...items);
|
||||
};
|
||||
let el!: HTMLElement; // 当前渲染产物(effect 首跑赋值)
|
||||
effect(() => { const next = render(); if (el) el.replaceWith(next); el = next; }); // active 变化整条重渲染(条目少,开销可忽略)
|
||||
return el;
|
||||
}
|
||||
@@ -175,6 +175,20 @@ export interface InputNumberProps {
|
||||
onChange?: (value: number) => void; // 值变化回调(已夹取到合法区间)
|
||||
}
|
||||
|
||||
/*** 步骤条条目 ***/
|
||||
export interface StepperItem {
|
||||
label: string; // 步骤标题
|
||||
description?: string; // 步骤补充说明;缺省不渲染
|
||||
}
|
||||
|
||||
/*** 步骤条入参 ***/
|
||||
export interface StepperProps {
|
||||
steps: StepperItem[]; // 步骤列表(按顺序渲染)
|
||||
active: Signal<number>; // 当前步索引(0 基),双向绑定(回跳写回、外部赋值同步)
|
||||
clickable?: boolean; // 已完成步骤是否可点击回跳;缺省 false
|
||||
onChange?: (index: number) => void; // 步骤切换回调(点击回跳触发)
|
||||
}
|
||||
|
||||
/*** 徽标入参 ***/
|
||||
export interface BadgeProps {
|
||||
label: string; // 徽标文案
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
"spinner(props):加载指示器,size 控制直径",
|
||||
"progress(props):进度条,value 信号驱动 0-100(满格切 success 色);不传 value 退化为不确定态无限往复",
|
||||
"tabs(props):标签页,active 信号双向绑定,滑动指示条 + 方向键环形导航,支持禁用页签",
|
||||
"stepper(props):步骤条,done/current/upcoming 三态,active 信号双向绑定,clickable 时已完成步骤可回跳",
|
||||
"modal(props, ...children):弹窗,open 信号驱动显隐,Esc / 遮罩 / × 关闭,size 三档宽度,footer 提供底部操作区",
|
||||
"drawer(props, ...children):抽屉,open 信号驱动侧滑面板,placement 左右可配、width 可调、footer 操作区,遮罩 / × / Esc 关闭",
|
||||
"form(props, items):表单容器,布局 + 聚合校验,全部通过才触发 onSubmit",
|
||||
@@ -978,6 +979,42 @@
|
||||
"键盘可访问:Tab 聚焦激活页签,←→ 在启用页签间环形移动"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "stepper",
|
||||
"title": "步骤条 Stepper",
|
||||
"demo": "sec-stepper",
|
||||
"props": [
|
||||
{
|
||||
"n": "steps",
|
||||
"t": "StepperItem[]",
|
||||
"r": 1,
|
||||
"d": "步骤列表,每项 { label, description? }"
|
||||
},
|
||||
{
|
||||
"n": "active",
|
||||
"t": "Signal<number>",
|
||||
"r": 1,
|
||||
"d": "当前步索引(0 基)信号,双向绑定:回跳写回、外部赋值同步"
|
||||
},
|
||||
{
|
||||
"n": "clickable",
|
||||
"t": "boolean",
|
||||
"d": "已完成步骤是否可点击回跳;缺省 false"
|
||||
},
|
||||
{
|
||||
"n": "onChange",
|
||||
"t": "(index: number) => void",
|
||||
"k": 1,
|
||||
"d": "步骤切换回调(点击回跳触发)"
|
||||
}
|
||||
],
|
||||
"example": "const step = signal(1); stepper({ steps: [{ label: '下单' }, { label: '支付' }, { label: '完成' }], active: step, clickable: true })",
|
||||
"steps": [
|
||||
"用 signal(0) 创建当前步索引,传入 active;索引之前的步骤显示 ✓ 已完成态",
|
||||
"流程推进时外部赋值即可:step(step() + 1),组件三态自动跟随",
|
||||
"clickable: true 时已完成步骤可点击回跳并写回信号,配合 onChange 同步业务状态"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "pagination",
|
||||
"title": "分页器 Pagination",
|
||||
|
||||
+82
-1
@@ -703,6 +703,86 @@
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
/* —— 步骤条 —— */
|
||||
.ua-stepper {
|
||||
display: flex;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
.ua-step {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: var(--ua-space-2);
|
||||
flex: 1;
|
||||
}
|
||||
.ua-step + .ua-step {
|
||||
padding-left: var(--ua-space-3);
|
||||
}
|
||||
.ua-step + .ua-step::before {
|
||||
content: ''; /* 连接线:与前一节点之间的横向短线 */
|
||||
width: 20px;
|
||||
height: 1px;
|
||||
margin-top: 13px;
|
||||
margin-right: var(--ua-space-2);
|
||||
background: var(--ua-color-border);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.ua-step-dot {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
border: 1px solid var(--ua-color-border);
|
||||
border-radius: 999px;
|
||||
background: var(--ua-color-surface);
|
||||
color: var(--ua-color-text-muted);
|
||||
font: inherit;
|
||||
font-size: var(--ua-font-size-sm);
|
||||
flex-shrink: 0;
|
||||
transition: border-color 0.15s, background 0.15s, color 0.15s, transform 0.12s, box-shadow 0.15s;
|
||||
}
|
||||
button.ua-step-dot {
|
||||
padding: 0; /* 可回跳步骤:重置原生按钮样式 */
|
||||
cursor: pointer;
|
||||
}
|
||||
button.ua-step-dot:hover {
|
||||
border-color: var(--ua-color-accent);
|
||||
color: var(--ua-color-accent);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
.ua-step-done .ua-step-dot {
|
||||
border-color: color-mix(in srgb, var(--ua-color-accent) 55%, transparent);
|
||||
background: color-mix(in srgb, var(--ua-color-accent) 10%, var(--ua-color-bg));
|
||||
color: var(--ua-color-accent);
|
||||
}
|
||||
.ua-step-current .ua-step-dot {
|
||||
border-color: var(--ua-color-accent);
|
||||
background: var(--ua-color-bg);
|
||||
color: var(--ua-color-accent);
|
||||
box-shadow: 0 0 0 3px color-mix(in srgb, var(--ua-color-accent) 15%, transparent);
|
||||
}
|
||||
.ua-step-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
min-width: 0;
|
||||
}
|
||||
.ua-step-label {
|
||||
font-size: var(--ua-font-size-md);
|
||||
line-height: 1.4;
|
||||
color: var(--ua-color-text-muted);
|
||||
}
|
||||
.ua-step-current .ua-step-label {
|
||||
color: var(--ua-color-text);
|
||||
font-weight: 600;
|
||||
}
|
||||
.ua-step-desc {
|
||||
font-size: var(--ua-font-size-sm);
|
||||
color: var(--ua-color-text-muted);
|
||||
}
|
||||
|
||||
/* —— 徽标 —— */
|
||||
.ua-badge {
|
||||
display: inline-block;
|
||||
@@ -1378,7 +1458,8 @@
|
||||
.ua-modal,
|
||||
.ua-modal-overlay,
|
||||
.ua-drawer,
|
||||
.ua-drawer-overlay {
|
||||
.ua-drawer-overlay,
|
||||
.ua-step-dot {
|
||||
animation: none !important;
|
||||
transition: none !important;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user