iter(g160): 滑块支持双滑块区间

This commit is contained in:
HIsheR
2026-10-26 16:03:55 +08:00
parent 084ffcd980
commit ef564fc72b
7 changed files with 133 additions and 33 deletions
+41 -27
View File
@@ -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()}`),
);
}