iter(g160): 滑块支持双滑块区间
This commit is contained in:
+20
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "ui-arch-registry-index/0.1",
|
||||
"generatedAt": "2026-10-26T08:02:08.033Z",
|
||||
"generatedAt": "2026-10-26T08:03:40.841Z",
|
||||
"repoRoot": ".",
|
||||
"packages": [
|
||||
{
|
||||
@@ -573,6 +573,23 @@
|
||||
"t": "{ value: number; label?: string }[]",
|
||||
"d": "刻度标注:按值定位在轨道下方,label 缺省显示数值;纯装饰不进读屏(数值由原生 input 播报)"
|
||||
},
|
||||
{
|
||||
"n": "range",
|
||||
"t": "boolean",
|
||||
"df": "false",
|
||||
"d": "双滑块区间模式:value 当下限、end 当上限,两拇指之间用强调色连成已选段"
|
||||
},
|
||||
{
|
||||
"n": "end",
|
||||
"t": "Signal<number>",
|
||||
"d": "区间上限信号;仅 range 模式使用,不传则组件内部维护"
|
||||
},
|
||||
{
|
||||
"n": "onRangeChange",
|
||||
"t": "(low: number, high: number) => void",
|
||||
"k": 1,
|
||||
"d": "区间两端变更回调,参数已保证 low ≤ high(越界那一端会被压平)"
|
||||
},
|
||||
{
|
||||
"n": "onChange",
|
||||
"t": "(value: number) => void",
|
||||
@@ -587,7 +604,8 @@
|
||||
"拖动或按 ←→ 键改变值:原生 range 自带键盘与触屏交互,无需额外处理",
|
||||
"只想看拖动条不显示数字时传 showValue: false;需要自定义展示可自行读 signal",
|
||||
"禁用场景传 disabled(布尔或信号),置灰且不可拖",
|
||||
"需要在轨道下方标刻度(如 0 / 50 / 100 的档位含义)传 marks:[{ value, label }],按 min-max 换算百分比定位,容器宽度变化时自动跟随"
|
||||
"需要在轨道下方标刻度(如 0 / 50 / 100 的档位含义)传 marks:[{ value, label }],按 min-max 换算百分比定位,容器宽度变化时自动跟随",
|
||||
"要选数值区间就传 range: true 并给 end 信号:两条原生 range 叠在同一轨道,只有拇指可拖;拖下限越过上限会自动压平,回调走 onRangeChange(low, high)"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
+4
-1
@@ -200,6 +200,8 @@ const chipOn = { 全部: signal(true), 进行中: signal(false), 已完成: sign
|
||||
|
||||
// —— 滑块:音量与禁用态演示 ——
|
||||
const volume = signal(40);
|
||||
const priceLo = signal(200); // 区间滑块下限
|
||||
const priceHi = signal(800); // 区间滑块上限
|
||||
|
||||
// —— 数字输入:购买数量演示,与禁用态实例共用同一信号 ——
|
||||
const qty = signal(1);
|
||||
@@ -417,13 +419,14 @@ const content = h(
|
||||
section(
|
||||
'sec-slider',
|
||||
'滑块',
|
||||
'slider:原生 range 承载交互(含键盘 ←→),value 信号双向绑定,右侧可显示当前值。',
|
||||
'slider:原生 range 承载交互(含键盘 ←→),value 信号双向绑定,右侧可显示当前值;range 模式叠两条原生轨做双滑块区间。',
|
||||
h(
|
||||
'div',
|
||||
{ class: 'row' },
|
||||
slider({ value: volume, min: 0, max: 100, step: 5, onChange: (v) => toast.info(`音量 ${v}`) }),
|
||||
slider({ value: volume, disabled: locked }),
|
||||
slider({ value: volume, marks: [{ value: 0, label: '静音' }, { value: 50, label: '中' }, { value: 100, label: '最大' }], onChange: (v) => toast.info(`音量 ${v}`) }),
|
||||
slider({ value: priceLo, end: priceHi, range: true, min: 0, max: 1000, step: 50, onRangeChange: (a, b) => toast.info(`价格 ${a} ~ ${b}`) }),
|
||||
),
|
||||
),
|
||||
section(
|
||||
|
||||
+5
-1
@@ -1,6 +1,6 @@
|
||||
# @ui-arch 组件库 Agent 文档(无图形版)
|
||||
|
||||
> 自动生成于 2026-10-26T08:02:08.033Z,数据源:各包 ua.manifest.json,勿手改。人类版文档站:apps/docs/index.html。
|
||||
> 自动生成于 2026-10-26T08:03:40.841Z,数据源:各包 ua.manifest.json,勿手改。人类版文档站:apps/docs/index.html。
|
||||
> 组件 = 普通函数,传 props 返回 DOM;状态用 signal 双向绑定:读 signal() 写 signal(v)。
|
||||
|
||||
## 快速上手
|
||||
@@ -197,6 +197,9 @@ const bio = signal(''); textarea({ value: bio, rows: 4, placeholder: '一句话
|
||||
- disabled:boolean | Signal<boolean> —— 禁用态:布尔直接置位,传信号则动态同步
|
||||
- showValue(默认 true):boolean —— 是否右侧显示当前值;缺省 true
|
||||
- marks:{ value: number; label?: string }[] —— 刻度标注:按值定位在轨道下方,label 缺省显示数值;纯装饰不进读屏(数值由原生 input 播报)
|
||||
- range(默认 false):boolean —— 双滑块区间模式:value 当下限、end 当上限,两拇指之间用强调色连成已选段
|
||||
- end:Signal<number> —— 区间上限信号;仅 range 模式使用,不传则组件内部维护
|
||||
- onRangeChange(回调):(low: number, high: number) => void —— 区间两端变更回调,参数已保证 low ≤ high(越界那一端会被压平)
|
||||
- onChange(回调):(value: number) => void —— 拖动回调
|
||||
|
||||
示例:
|
||||
@@ -213,6 +216,7 @@ const vol = signal(40); slider({ value: vol, min: 0, max: 100, step: 5, onChange
|
||||
4. 只想看拖动条不显示数字时传 showValue: false;需要自定义展示可自行读 signal
|
||||
5. 禁用场景传 disabled(布尔或信号),置灰且不可拖
|
||||
6. 需要在轨道下方标刻度(如 0 / 50 / 100 的档位含义)传 marks:[{ value, label }],按 min-max 换算百分比定位,容器宽度变化时自动跟随
|
||||
7. 要选数值区间就传 range: true 并给 end 信号:两条原生 range 叠在同一轨道,只有拇指可拖;拖下限越过上限会自动压平,回调走 onRangeChange(low, high)
|
||||
|
||||
现场演示:apps/docs/demo.html#sec-slider
|
||||
|
||||
|
||||
@@ -1,43 +1,57 @@
|
||||
import { effect, h } from '@ui-arch/frame';
|
||||
import { effect, h, signal } from '@ui-arch/frame';
|
||||
import type { SliderProps } from './types.js';
|
||||
|
||||
/*** 滑块:原生 range 承载交互与键盘,value 信号双向绑定;marks 在轨道下方标出刻度文案 ***/
|
||||
/*** 滑块:原生 range 承载交互与键盘,value 信号双向绑定;range 模式叠两条原生轨取双滑块(值域夹取保证 low ≤ high),marks 在轨道下方标出刻度文案 ***/
|
||||
export function slider(props: SliderProps): HTMLElement {
|
||||
const value = props.value; // 必需信号:组件不持有数值,一律委托调用方
|
||||
const min = props.min ?? 0; // 与 input 的缺省保持一致,供刻度换算百分比
|
||||
// —— 状态集中声明 ——
|
||||
const value = props.value; // 必需信号:组件不持有数值,一律委托调用方
|
||||
const min = props.min ?? 0; // 与 input 的缺省保持一致,供刻度换算百分比
|
||||
const max = props.max ?? 100;
|
||||
const input = h('input', {
|
||||
type: 'range',
|
||||
class: 'ua-slider-input',
|
||||
min,
|
||||
max,
|
||||
step: props.step ?? 1,
|
||||
disabled: props.disabled, // 布尔或信号禁用,h() 直接透传
|
||||
}) as HTMLInputElement;
|
||||
const step = props.step ?? 1;
|
||||
const high = props.end ?? signal(max); // 区间上限:不传则由组件内部维护
|
||||
const disabled = props.disabled; // 布尔或信号禁用,h() 直接透传
|
||||
const pct = (v: number): number => ((v - min) / (max - min || 1)) * 100; // 分母兜底,避免 min=max 时除零
|
||||
|
||||
input.addEventListener('input', () => {
|
||||
const next = Number(input.value);
|
||||
value(next); // 用户拖动写回信号
|
||||
props.onChange?.(next);
|
||||
});
|
||||
effect(() => {
|
||||
const next = String(value());
|
||||
if (input.value !== next) input.value = next; // 外部赋值同步滑块位置(等值不重写,避免打断拖动)
|
||||
});
|
||||
/*** 建一条原生 range:区间模式靠 CSS 叠在同一格,只有拇指可命中 ***/
|
||||
const track = (sig: typeof value, onInput: (n: number) => void): HTMLInputElement => {
|
||||
const input = h('input', { type: 'range', class: 'ua-slider-input', min, max, step, disabled }) as HTMLInputElement;
|
||||
input.addEventListener('input', () => onInput(Number(input.value)));
|
||||
effect(() => {
|
||||
const next = String(sig());
|
||||
if (input.value !== next) input.value = next; // 外部赋值同步位置(等值不重写,避免打断拖动)
|
||||
});
|
||||
return input;
|
||||
};
|
||||
|
||||
// 刻度:按值算百分比定位,纯装饰不进读屏(数值本身由原生 input 播报)
|
||||
const marks = props.marks?.length
|
||||
? h('div', { class: 'ua-slider-marks', 'aria-hidden': 'true' },
|
||||
...props.marks.map((mark) => h('span', {
|
||||
class: 'ua-slider-mark',
|
||||
style: `left:${((mark.value - min) / (max - min || 1)) * 100}%`, // 分母兜底,避免 min=max 时除零
|
||||
}, mark.label ?? String(mark.value))))
|
||||
...props.marks.map((mark) => h('span', { class: 'ua-slider-mark', style: `left:${pct(mark.value)}%` }, mark.label ?? String(mark.value))))
|
||||
: null;
|
||||
|
||||
if (!props.range) { // —— 单值:一条原生轨,行为与旧版完全一致 ——
|
||||
const input = track(value, (next) => { value(next); props.onChange?.(next); });
|
||||
return h(
|
||||
'div',
|
||||
{ class: 'ua-slider' },
|
||||
h('div', { class: 'ua-slider-track' }, input, marks),
|
||||
props.showValue === false ? null : h('span', { class: 'ua-slider-value' }, () => String(value())),
|
||||
);
|
||||
}
|
||||
|
||||
// —— 区间:两条原生轨叠放,rail 打底、seg 画出两拇指之间的已选段 ——
|
||||
const rail = h('div', { class: 'ua-slider-rail', 'aria-hidden': 'true' });
|
||||
const seg = h('div', { class: 'ua-slider-seg', 'aria-hidden': 'true' }) as HTMLElement;
|
||||
const lowIn = track(value, (next) => { value(Math.min(next, high())); props.onRangeChange?.(value(), high()); }); // 下限越过上限就压平
|
||||
const highIn = track(high, (next) => { high(Math.max(next, value())); props.onRangeChange?.(value(), high()); }); // 上限低于下限就压平
|
||||
lowIn.classList.add('ua-slider-input-low'); // 供 CSS 区分上下层与拇指命中区
|
||||
highIn.classList.add('ua-slider-input-high');
|
||||
effect(() => { seg.style.left = `${pct(value())}%`; seg.style.width = `${pct(high()) - pct(value())}%`; }); // 已选段随两端重算
|
||||
|
||||
return h(
|
||||
'div',
|
||||
{ class: 'ua-slider' },
|
||||
h('div', { class: 'ua-slider-track' }, input, marks), // 轨道包一层:刻度只跟滑块对齐,不影响右侧数值
|
||||
props.showValue === false ? null : h('span', { class: 'ua-slider-value' }, () => String(value())),
|
||||
h('div', { class: 'ua-slider-track' }, h('div', { class: 'ua-slider-range' }, rail, seg, lowIn, highIn), marks),
|
||||
props.showValue === false ? null : h('span', { class: 'ua-slider-value' }, () => `${value()} ~ ${high()}`),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -182,6 +182,9 @@ export interface SliderProps {
|
||||
disabled?: boolean | Signal<boolean>; // 禁用态:布尔直接置位,传信号则动态同步
|
||||
showValue?: boolean; // 是否右侧显示当前值;缺省 true
|
||||
marks?: { value: number; label?: string }[]; // 刻度标注:按值定位在轨道下方,label 缺省显示数值;纯装饰不进读屏
|
||||
range?: boolean; // 双滑块区间模式:value 是下限、end 是上限;缺省 false
|
||||
end?: Signal<number>; // 区间上限信号;仅 range 模式使用,不传则内部维护
|
||||
onRangeChange?: (low: number, high: number) => void; // 区间两端变更回调(已保证 low ≤ high)
|
||||
onChange?: (value: number) => void; // 拖动回调
|
||||
}
|
||||
|
||||
|
||||
@@ -444,6 +444,23 @@
|
||||
"t": "{ value: number; label?: string }[]",
|
||||
"d": "刻度标注:按值定位在轨道下方,label 缺省显示数值;纯装饰不进读屏(数值由原生 input 播报)"
|
||||
},
|
||||
{
|
||||
"n": "range",
|
||||
"t": "boolean",
|
||||
"df": "false",
|
||||
"d": "双滑块区间模式:value 当下限、end 当上限,两拇指之间用强调色连成已选段"
|
||||
},
|
||||
{
|
||||
"n": "end",
|
||||
"t": "Signal<number>",
|
||||
"d": "区间上限信号;仅 range 模式使用,不传则组件内部维护"
|
||||
},
|
||||
{
|
||||
"n": "onRangeChange",
|
||||
"t": "(low: number, high: number) => void",
|
||||
"k": 1,
|
||||
"d": "区间两端变更回调,参数已保证 low ≤ high(越界那一端会被压平)"
|
||||
},
|
||||
{
|
||||
"n": "onChange",
|
||||
"t": "(value: number) => void",
|
||||
@@ -458,7 +475,8 @@
|
||||
"拖动或按 ←→ 键改变值:原生 range 自带键盘与触屏交互,无需额外处理",
|
||||
"只想看拖动条不显示数字时传 showValue: false;需要自定义展示可自行读 signal",
|
||||
"禁用场景传 disabled(布尔或信号),置灰且不可拖",
|
||||
"需要在轨道下方标刻度(如 0 / 50 / 100 的档位含义)传 marks:[{ value, label }],按 min-max 换算百分比定位,容器宽度变化时自动跟随"
|
||||
"需要在轨道下方标刻度(如 0 / 50 / 100 的档位含义)传 marks:[{ value, label }],按 min-max 换算百分比定位,容器宽度变化时自动跟随",
|
||||
"要选数值区间就传 range: true 并给 end 信号:两条原生 range 叠在同一轨道,只有拇指可拖;拖下限越过上限会自动压平,回调走 onRangeChange(low, high)"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -826,6 +826,46 @@ body.ua-col-resizing {
|
||||
font-variant-numeric: tabular-nums;
|
||||
text-align: right;
|
||||
}
|
||||
.ua-slider-range {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 20px;
|
||||
}
|
||||
.ua-slider-rail {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 4px;
|
||||
border-radius: var(--ua-radius-sm);
|
||||
background: color-mix(in srgb, var(--ua-color-border) 70%, transparent);
|
||||
}
|
||||
.ua-slider-seg {
|
||||
position: absolute;
|
||||
height: 4px;
|
||||
border-radius: var(--ua-radius-sm);
|
||||
background: var(--ua-color-accent);
|
||||
transition: left 0.15s, width 0.15s;
|
||||
}
|
||||
.ua-slider-range .ua-slider-input {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
background: none;
|
||||
pointer-events: none; /* 轨道不吃事件,只有拇指可拖,避免上下两条互相遮挡 */
|
||||
}
|
||||
.ua-slider-range .ua-slider-input::-webkit-slider-thumb {
|
||||
pointer-events: auto;
|
||||
}
|
||||
.ua-slider-range .ua-slider-input::-moz-range-thumb {
|
||||
pointer-events: auto;
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.ua-slider-seg { transition: none; }
|
||||
}
|
||||
|
||||
/* —— 多行输入 —— */
|
||||
.ua-textarea {
|
||||
|
||||
Reference in New Issue
Block a user