iter(g160): 滑块支持双滑块区间
This commit is contained in:
+20
-2
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"$schema": "ui-arch-registry-index/0.1",
|
"$schema": "ui-arch-registry-index/0.1",
|
||||||
"generatedAt": "2026-10-26T08:02:08.033Z",
|
"generatedAt": "2026-10-26T08:03:40.841Z",
|
||||||
"repoRoot": ".",
|
"repoRoot": ".",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
@@ -573,6 +573,23 @@
|
|||||||
"t": "{ value: number; label?: string }[]",
|
"t": "{ value: number; label?: string }[]",
|
||||||
"d": "刻度标注:按值定位在轨道下方,label 缺省显示数值;纯装饰不进读屏(数值由原生 input 播报)"
|
"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",
|
"n": "onChange",
|
||||||
"t": "(value: number) => void",
|
"t": "(value: number) => void",
|
||||||
@@ -587,7 +604,8 @@
|
|||||||
"拖动或按 ←→ 键改变值:原生 range 自带键盘与触屏交互,无需额外处理",
|
"拖动或按 ←→ 键改变值:原生 range 自带键盘与触屏交互,无需额外处理",
|
||||||
"只想看拖动条不显示数字时传 showValue: false;需要自定义展示可自行读 signal",
|
"只想看拖动条不显示数字时传 showValue: false;需要自定义展示可自行读 signal",
|
||||||
"禁用场景传 disabled(布尔或信号),置灰且不可拖",
|
"禁用场景传 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 volume = signal(40);
|
||||||
|
const priceLo = signal(200); // 区间滑块下限
|
||||||
|
const priceHi = signal(800); // 区间滑块上限
|
||||||
|
|
||||||
// —— 数字输入:购买数量演示,与禁用态实例共用同一信号 ——
|
// —— 数字输入:购买数量演示,与禁用态实例共用同一信号 ——
|
||||||
const qty = signal(1);
|
const qty = signal(1);
|
||||||
@@ -417,13 +419,14 @@ const content = h(
|
|||||||
section(
|
section(
|
||||||
'sec-slider',
|
'sec-slider',
|
||||||
'滑块',
|
'滑块',
|
||||||
'slider:原生 range 承载交互(含键盘 ←→),value 信号双向绑定,右侧可显示当前值。',
|
'slider:原生 range 承载交互(含键盘 ←→),value 信号双向绑定,右侧可显示当前值;range 模式叠两条原生轨做双滑块区间。',
|
||||||
h(
|
h(
|
||||||
'div',
|
'div',
|
||||||
{ class: 'row' },
|
{ class: 'row' },
|
||||||
slider({ value: volume, min: 0, max: 100, step: 5, onChange: (v) => toast.info(`音量 ${v}`) }),
|
slider({ value: volume, min: 0, max: 100, step: 5, onChange: (v) => toast.info(`音量 ${v}`) }),
|
||||||
slider({ value: volume, disabled: locked }),
|
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: 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(
|
section(
|
||||||
|
|||||||
+5
-1
@@ -1,6 +1,6 @@
|
|||||||
# @ui-arch 组件库 Agent 文档(无图形版)
|
# @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)。
|
> 组件 = 普通函数,传 props 返回 DOM;状态用 signal 双向绑定:读 signal() 写 signal(v)。
|
||||||
|
|
||||||
## 快速上手
|
## 快速上手
|
||||||
@@ -197,6 +197,9 @@ const bio = signal(''); textarea({ value: bio, rows: 4, placeholder: '一句话
|
|||||||
- disabled:boolean | Signal<boolean> —— 禁用态:布尔直接置位,传信号则动态同步
|
- disabled:boolean | Signal<boolean> —— 禁用态:布尔直接置位,传信号则动态同步
|
||||||
- showValue(默认 true):boolean —— 是否右侧显示当前值;缺省 true
|
- showValue(默认 true):boolean —— 是否右侧显示当前值;缺省 true
|
||||||
- marks:{ value: number; label?: string }[] —— 刻度标注:按值定位在轨道下方,label 缺省显示数值;纯装饰不进读屏(数值由原生 input 播报)
|
- 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 —— 拖动回调
|
- 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
|
4. 只想看拖动条不显示数字时传 showValue: false;需要自定义展示可自行读 signal
|
||||||
5. 禁用场景传 disabled(布尔或信号),置灰且不可拖
|
5. 禁用场景传 disabled(布尔或信号),置灰且不可拖
|
||||||
6. 需要在轨道下方标刻度(如 0 / 50 / 100 的档位含义)传 marks:[{ value, label }],按 min-max 换算百分比定位,容器宽度变化时自动跟随
|
6. 需要在轨道下方标刻度(如 0 / 50 / 100 的档位含义)传 marks:[{ value, label }],按 min-max 换算百分比定位,容器宽度变化时自动跟随
|
||||||
|
7. 要选数值区间就传 range: true 并给 end 信号:两条原生 range 叠在同一轨道,只有拇指可拖;拖下限越过上限会自动压平,回调走 onRangeChange(low, high)
|
||||||
|
|
||||||
现场演示:apps/docs/demo.html#sec-slider
|
现场演示: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';
|
import type { SliderProps } from './types.js';
|
||||||
|
|
||||||
/*** 滑块:原生 range 承载交互与键盘,value 信号双向绑定;marks 在轨道下方标出刻度文案 ***/
|
/*** 滑块:原生 range 承载交互与键盘,value 信号双向绑定;range 模式叠两条原生轨取双滑块(值域夹取保证 low ≤ high),marks 在轨道下方标出刻度文案 ***/
|
||||||
export function slider(props: SliderProps): HTMLElement {
|
export function slider(props: SliderProps): HTMLElement {
|
||||||
|
// —— 状态集中声明 ——
|
||||||
const value = props.value; // 必需信号:组件不持有数值,一律委托调用方
|
const value = props.value; // 必需信号:组件不持有数值,一律委托调用方
|
||||||
const min = props.min ?? 0; // 与 input 的缺省保持一致,供刻度换算百分比
|
const min = props.min ?? 0; // 与 input 的缺省保持一致,供刻度换算百分比
|
||||||
const max = props.max ?? 100;
|
const max = props.max ?? 100;
|
||||||
const input = h('input', {
|
const step = props.step ?? 1;
|
||||||
type: 'range',
|
const high = props.end ?? signal(max); // 区间上限:不传则由组件内部维护
|
||||||
class: 'ua-slider-input',
|
const disabled = props.disabled; // 布尔或信号禁用,h() 直接透传
|
||||||
min,
|
const pct = (v: number): number => ((v - min) / (max - min || 1)) * 100; // 分母兜底,避免 min=max 时除零
|
||||||
max,
|
|
||||||
step: props.step ?? 1,
|
|
||||||
disabled: props.disabled, // 布尔或信号禁用,h() 直接透传
|
|
||||||
}) as HTMLInputElement;
|
|
||||||
|
|
||||||
input.addEventListener('input', () => {
|
/*** 建一条原生 range:区间模式靠 CSS 叠在同一格,只有拇指可命中 ***/
|
||||||
const next = Number(input.value);
|
const track = (sig: typeof value, onInput: (n: number) => void): HTMLInputElement => {
|
||||||
value(next); // 用户拖动写回信号
|
const input = h('input', { type: 'range', class: 'ua-slider-input', min, max, step, disabled }) as HTMLInputElement;
|
||||||
props.onChange?.(next);
|
input.addEventListener('input', () => onInput(Number(input.value)));
|
||||||
});
|
|
||||||
effect(() => {
|
effect(() => {
|
||||||
const next = String(value());
|
const next = String(sig());
|
||||||
if (input.value !== next) input.value = next; // 外部赋值同步滑块位置(等值不重写,避免打断拖动)
|
if (input.value !== next) input.value = next; // 外部赋值同步位置(等值不重写,避免打断拖动)
|
||||||
});
|
});
|
||||||
|
return input;
|
||||||
|
};
|
||||||
|
|
||||||
// 刻度:按值算百分比定位,纯装饰不进读屏(数值本身由原生 input 播报)
|
// 刻度:按值算百分比定位,纯装饰不进读屏(数值本身由原生 input 播报)
|
||||||
const marks = props.marks?.length
|
const marks = props.marks?.length
|
||||||
? h('div', { class: 'ua-slider-marks', 'aria-hidden': 'true' },
|
? h('div', { class: 'ua-slider-marks', 'aria-hidden': 'true' },
|
||||||
...props.marks.map((mark) => h('span', {
|
...props.marks.map((mark) => h('span', { class: 'ua-slider-mark', style: `left:${pct(mark.value)}%` }, mark.label ?? String(mark.value))))
|
||||||
class: 'ua-slider-mark',
|
|
||||||
style: `left:${((mark.value - min) / (max - min || 1)) * 100}%`, // 分母兜底,避免 min=max 时除零
|
|
||||||
}, mark.label ?? String(mark.value))))
|
|
||||||
: null;
|
: 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(
|
return h(
|
||||||
'div',
|
'div',
|
||||||
{ class: 'ua-slider' },
|
{ class: 'ua-slider' },
|
||||||
h('div', { class: 'ua-slider-track' }, input, marks), // 轨道包一层:刻度只跟滑块对齐,不影响右侧数值
|
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' }, () => String(value())),
|
props.showValue === false ? null : h('span', { class: 'ua-slider-value' }, () => `${value()} ~ ${high()}`),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -182,6 +182,9 @@ export interface SliderProps {
|
|||||||
disabled?: boolean | Signal<boolean>; // 禁用态:布尔直接置位,传信号则动态同步
|
disabled?: boolean | Signal<boolean>; // 禁用态:布尔直接置位,传信号则动态同步
|
||||||
showValue?: boolean; // 是否右侧显示当前值;缺省 true
|
showValue?: boolean; // 是否右侧显示当前值;缺省 true
|
||||||
marks?: { value: number; label?: string }[]; // 刻度标注:按值定位在轨道下方,label 缺省显示数值;纯装饰不进读屏
|
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; // 拖动回调
|
onChange?: (value: number) => void; // 拖动回调
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -444,6 +444,23 @@
|
|||||||
"t": "{ value: number; label?: string }[]",
|
"t": "{ value: number; label?: string }[]",
|
||||||
"d": "刻度标注:按值定位在轨道下方,label 缺省显示数值;纯装饰不进读屏(数值由原生 input 播报)"
|
"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",
|
"n": "onChange",
|
||||||
"t": "(value: number) => void",
|
"t": "(value: number) => void",
|
||||||
@@ -458,7 +475,8 @@
|
|||||||
"拖动或按 ←→ 键改变值:原生 range 自带键盘与触屏交互,无需额外处理",
|
"拖动或按 ←→ 键改变值:原生 range 自带键盘与触屏交互,无需额外处理",
|
||||||
"只想看拖动条不显示数字时传 showValue: false;需要自定义展示可自行读 signal",
|
"只想看拖动条不显示数字时传 showValue: false;需要自定义展示可自行读 signal",
|
||||||
"禁用场景传 disabled(布尔或信号),置灰且不可拖",
|
"禁用场景传 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;
|
font-variant-numeric: tabular-nums;
|
||||||
text-align: right;
|
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 {
|
.ua-textarea {
|
||||||
|
|||||||
Reference in New Issue
Block a user