@mrchar/cumquat 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,4135 @@
1
+ import { computed, getCurrentInstance, inject, isRef, nextTick, onBeforeUnmount, onMounted, onScopeDispose, provide, reactive, ref, shallowRef, toValue, useId as useId$1, watch } from "vue";
2
+ //#region src/core/useId.ts
3
+ let fallbackCount = 0;
4
+ /**
5
+ * SSR 安全的组件内唯一标识,用于 label/aria 关联。
6
+ *
7
+ * @param prefix 标识前缀,默认 `cq`
8
+ * @returns 形如 `cq-input-0` 的只读字符串引用
9
+ * @internal 消费组件:FormItem、Tooltip 及全部需要 aria 关联的控件
10
+ */
11
+ function useId(prefix = "cq") {
12
+ const raw = getCurrentInstance() ? useId$1() : `f${fallbackCount++}`;
13
+ return computed(() => `${prefix}-${raw.replace(/[^a-zA-Z0-9_-]/g, "")}`);
14
+ }
15
+ //#endregion
16
+ //#region src/core/useControllableModel.ts
17
+ /**
18
+ * 受控 / 非受控统一封装。
19
+ *
20
+ * - 传入 `modelValue` 时为受控:读外部值,写入时通过 onChange 回发事件;
21
+ * - 仅传入 `defaultValue` 时为非受控:内部自持状态,写入同样回发事件以便宿主监听。
22
+ *
23
+ * `v-model` 永远是纯同步的,本函数不做任何异步编排。
24
+ */
25
+ function useControllableModel(options = {}) {
26
+ const { modelValue, defaultValue, onChange, fallback, equals = Object.is } = options;
27
+ const controlledInput = typeof modelValue === "function" || isRef(modelValue);
28
+ const isControlled = ref(controlledInput || modelValue !== void 0);
29
+ const initial = toValue(modelValue);
30
+ const inner = ref(initial !== void 0 ? initial : defaultValue !== void 0 ? defaultValue : fallback);
31
+ const value = computed({
32
+ get: () => isControlled.value ? toValue(modelValue) : inner.value,
33
+ set: (next) => {
34
+ const current = isControlled.value ? toValue(modelValue) : inner.value;
35
+ if (current !== void 0 && equals(current, next)) return;
36
+ if (!isControlled.value) inner.value = next;
37
+ onChange?.(next);
38
+ }
39
+ });
40
+ const setValue = (next) => {
41
+ value.value = next;
42
+ };
43
+ return {
44
+ value,
45
+ setValue,
46
+ isControlled
47
+ };
48
+ }
49
+ /** 从标准 props 形态构造受控模型的便捷方法 */
50
+ function createControllableModel(props, fallback) {
51
+ return useControllableModel({
52
+ modelValue: props.modelValue,
53
+ defaultValue: props.defaultValue,
54
+ fallback,
55
+ onChange: props["onUpdate:modelValue"]
56
+ });
57
+ }
58
+ //#endregion
59
+ //#region src/core/useToggle.ts
60
+ /**
61
+ * 布尔状态及取反操作。
62
+ *
63
+ * @param initial 初始值,默认 false
64
+ * @internal 消费组件:Switch、Layout、FloatButton
65
+ */
66
+ function useToggle(initial = false) {
67
+ const value = ref(initial);
68
+ const toggle = (next) => {
69
+ value.value = typeof next === "boolean" ? next : !value.value;
70
+ };
71
+ const setTrue = () => {
72
+ value.value = true;
73
+ };
74
+ const setFalse = () => {
75
+ value.value = false;
76
+ };
77
+ return {
78
+ value,
79
+ toggle,
80
+ setTrue,
81
+ setFalse
82
+ };
83
+ }
84
+ //#endregion
85
+ //#region src/internal/dom.ts
86
+ /** 当前是否处于浏览器环境(惰性求值,保证 SSR 安全) */
87
+ const isClient = typeof window !== "undefined";
88
+ /** 将目标解析为真实 DOM 元素 */
89
+ function toElement(target) {
90
+ if (!target) return null;
91
+ return toValue(target) ?? null;
92
+ }
93
+ /** 可聚焦元素选择器(焦点陷阱 / roving focus 共用) */
94
+ const FOCUSABLE_SELECTOR = [
95
+ "a[href]",
96
+ "button:not([disabled])",
97
+ "textarea:not([disabled])",
98
+ "input:not([disabled])",
99
+ "select:not([disabled])",
100
+ "[tabindex]:not([tabindex=\"-1\"])",
101
+ "[contenteditable=\"true\"]"
102
+ ].join(",");
103
+ /**
104
+ * 获取容器内所有可聚焦元素(选择器已排除 disabled)。
105
+ * 不做 offsetParent 可见性过滤:无布局环境(jsdom)与 display 切换中的
106
+ * 元素仍需参与焦点管理,可见性由调用方保证。
107
+ */
108
+ function getFocusableList(container) {
109
+ if (!container || !isClient) return [];
110
+ return Array.from(container.querySelectorAll(FOCUSABLE_SELECTOR));
111
+ }
112
+ /** 事件目标是否包含节点(用于外部点击判定) */
113
+ function isEventInside(target, ...containers) {
114
+ if (!(target instanceof Node)) return false;
115
+ return containers.some((el) => !!el && el.contains(target));
116
+ }
117
+ /**
118
+ * 注册一个在作用域销毁时自动执行的清理函数;
119
+ * 无活动作用域时返回空操作注册器,避免 SSR 报错。
120
+ */
121
+ function tryOnScopeDispose(fn) {
122
+ try {
123
+ onScopeDispose(fn);
124
+ } catch {}
125
+ }
126
+ /** 下一帧执行,返回取消函数(SSR 下降级为 setTimeout) */
127
+ function rafThrottle(fn) {
128
+ if (!isClient || typeof requestAnimationFrame !== "function") {
129
+ const id = setTimeout(fn, 16);
130
+ return () => clearTimeout(id);
131
+ }
132
+ let scheduled = false;
133
+ const wrapped = () => {
134
+ scheduled = false;
135
+ fn();
136
+ };
137
+ return () => {
138
+ if (scheduled) return;
139
+ scheduled = true;
140
+ requestAnimationFrame(wrapped);
141
+ };
142
+ }
143
+ //#endregion
144
+ //#region src/core/useEventListener.ts
145
+ /**
146
+ * 绑定 DOM 事件并在作用域销毁时自动卸载;目标支持响应式引用,
147
+ * 目标变化(如 Teleport 后重挂载)时自动重新绑定。
148
+ *
149
+ * @param target 事件目标(元素 / window / document,可为 ref 或 getter)
150
+ * @param event 事件名
151
+ * @param listener 处理函数
152
+ * @param options addEventListener 选项
153
+ * @returns 手动解绑函数
154
+ */
155
+ function useEventListener(target, event, listener, options) {
156
+ let cleanup;
157
+ const bind = (el) => {
158
+ cleanup?.();
159
+ cleanup = void 0;
160
+ if (!isClient || !el) return;
161
+ el.addEventListener(event, listener, options);
162
+ cleanup = () => el.removeEventListener(event, listener, options);
163
+ };
164
+ watch(() => toValue(target), bind, { immediate: true });
165
+ const stop = () => cleanup?.();
166
+ tryOnScopeDispose(stop);
167
+ return stop;
168
+ }
169
+ //#endregion
170
+ //#region src/core/useResizeObserver.ts
171
+ /**
172
+ * 监听元素内容盒尺寸变化。SSR 环境下尺寸恒为 0 且不报错。
173
+ *
174
+ * @internal 消费方:useFloating;Watermark / Typography / VirtualList 组件内联复用
175
+ */
176
+ function useResizeObserver(target) {
177
+ const width = ref(0);
178
+ const height = ref(0);
179
+ let observer;
180
+ const apply = (el) => {
181
+ observer?.disconnect();
182
+ observer = void 0;
183
+ if (!isClient || !el) return;
184
+ if (typeof ResizeObserver === "undefined") {
185
+ width.value = el.offsetWidth;
186
+ height.value = el.offsetHeight;
187
+ return;
188
+ }
189
+ observer = new ResizeObserver((entries) => {
190
+ const rect = entries[0]?.contentRect;
191
+ if (rect) {
192
+ width.value = rect.width;
193
+ height.value = rect.height;
194
+ }
195
+ });
196
+ observer.observe(el);
197
+ };
198
+ watch(() => toElement(target), apply, { immediate: true });
199
+ const stop = () => observer?.disconnect();
200
+ tryOnScopeDispose(stop);
201
+ return {
202
+ width,
203
+ height,
204
+ stop
205
+ };
206
+ }
207
+ //#endregion
208
+ //#region src/core/useIntersectionObserver.ts
209
+ /**
210
+ * 监听元素进出视口状态。SSR 安全,不支持 IntersectionObserver 时按可见处理。
211
+ *
212
+ * @internal 消费方:useImageState 懒加载;宿主可独立使用
213
+ */
214
+ function useIntersectionObserver(target, options = {}) {
215
+ const isIntersecting = ref(false);
216
+ const ratio = ref(0);
217
+ let observer;
218
+ const stop = () => {
219
+ observer?.disconnect();
220
+ observer = void 0;
221
+ };
222
+ const apply = (el) => {
223
+ stop();
224
+ if (!isClient || !el) return;
225
+ if (typeof IntersectionObserver === "undefined") {
226
+ isIntersecting.value = true;
227
+ return;
228
+ }
229
+ observer = new IntersectionObserver((entries) => {
230
+ const entry = entries[0];
231
+ if (!entry) return;
232
+ isIntersecting.value = entry.isIntersecting;
233
+ ratio.value = entry.intersectionRatio;
234
+ if (entry.isIntersecting && options.once) stop();
235
+ }, {
236
+ root: toElement(options.root ?? null),
237
+ rootMargin: options.rootMargin,
238
+ threshold: options.threshold
239
+ });
240
+ observer.observe(el);
241
+ };
242
+ watch(() => toElement(target), apply, { immediate: true });
243
+ tryOnScopeDispose(stop);
244
+ return {
245
+ isIntersecting,
246
+ ratio,
247
+ stop
248
+ };
249
+ }
250
+ //#endregion
251
+ //#region src/core/useTimeoutFn.ts
252
+ /**
253
+ * 可随时停止 / 重启的一次性定时器,作用域销毁时自动清理。
254
+ *
255
+ * @param cb 到时回调
256
+ * @param ms 延迟毫秒;构造时传 null/undefined 表示不自动启动,需手动 start
257
+ * @internal 消费方:useOpen 延迟开合、useHoverIntent、useToastQueue、useTypeahead
258
+ */
259
+ function useTimeoutFn(cb, ms) {
260
+ const isPending = ref(false);
261
+ let timer;
262
+ const stop = () => {
263
+ if (timer !== void 0) {
264
+ clearTimeout(timer);
265
+ timer = void 0;
266
+ }
267
+ isPending.value = false;
268
+ };
269
+ const start = (nextMs) => {
270
+ if (!isClient) return;
271
+ stop();
272
+ isPending.value = true;
273
+ timer = setTimeout(() => {
274
+ timer = void 0;
275
+ isPending.value = false;
276
+ cb();
277
+ }, nextMs ?? ms ?? 0);
278
+ };
279
+ if (ms !== void 0) start(ms);
280
+ tryOnScopeDispose(stop);
281
+ return {
282
+ isPending,
283
+ start,
284
+ stop
285
+ };
286
+ }
287
+ //#endregion
288
+ //#region src/core/useIntervalFn.ts
289
+ /**
290
+ * 可暂停 / 恢复的周期定时器,作用域销毁时自动清理。
291
+ *
292
+ * @param cb 周期回调
293
+ * @param interval 间隔毫秒,可随 resume 动态变更
294
+ * @internal 消费方:Carousel 自动播放;宿主可独立使用
295
+ */
296
+ function useIntervalFn(cb, interval = 1e3, options = {}) {
297
+ const { immediate = true, immediateCallback = false } = options;
298
+ const isActive = ref(false);
299
+ let timer;
300
+ const pause = () => {
301
+ if (timer !== void 0) {
302
+ clearInterval(timer);
303
+ timer = void 0;
304
+ }
305
+ isActive.value = false;
306
+ };
307
+ const resume = (nextInterval = interval) => {
308
+ if (!isClient || timer !== void 0 || nextInterval <= 0) return;
309
+ isActive.value = true;
310
+ if (immediateCallback) cb();
311
+ timer = setInterval(cb, nextInterval);
312
+ };
313
+ const restart = () => {
314
+ pause();
315
+ resume();
316
+ };
317
+ if (immediate) resume();
318
+ tryOnScopeDispose(pause);
319
+ return {
320
+ isActive,
321
+ pause,
322
+ resume,
323
+ restart
324
+ };
325
+ }
326
+ //#endregion
327
+ //#region src/core/useMediaQuery.ts
328
+ /**
329
+ * 订阅 CSS 媒体查询结果。SSR 环境下恒为 false。
330
+ * 查询串支持响应式引用,变化时自动重新订阅。
331
+ *
332
+ * @internal 消费组件:Layout、Grid;断点语义由宿主基于本函数自行封装
333
+ */
334
+ function useMediaQuery(query) {
335
+ const matches = ref(false);
336
+ let mql;
337
+ const apply = (queryString) => {
338
+ if (!isClient || typeof window.matchMedia !== "function") {
339
+ matches.value = false;
340
+ return;
341
+ }
342
+ mql?.removeEventListener("change", onChange);
343
+ mql = window.matchMedia(queryString);
344
+ matches.value = mql.matches;
345
+ mql.addEventListener("change", onChange);
346
+ };
347
+ const onChange = (ev) => {
348
+ matches.value = ev.matches;
349
+ };
350
+ watch(() => toValue(query), apply, { immediate: true });
351
+ tryOnScopeDispose(() => mql?.removeEventListener("change", onChange));
352
+ return matches;
353
+ }
354
+ //#endregion
355
+ //#region src/core/useScroll.ts
356
+ /**
357
+ * 监听目标元素或窗口的滚动位置与方向,SSR 安全。
358
+ *
359
+ * @param target 滚动容器(window / document / 元素),可为响应式
360
+ * @internal 消费组件:Affix、Anchor、BackTop 内联复用;宿主可独立使用
361
+ */
362
+ function useScroll(target = () => isClient ? window : void 0, options = {}) {
363
+ const { capture = true, direction: withDirection = true, throttle = true } = options;
364
+ const x = ref(0);
365
+ const y = ref(0);
366
+ const directionX = ref("none");
367
+ const directionY = ref("none");
368
+ const readPosition = (el) => {
369
+ if (!el) return;
370
+ if (el === window) {
371
+ x.value = window.scrollX;
372
+ y.value = window.scrollY;
373
+ return;
374
+ }
375
+ const node = el;
376
+ if (typeof node.scrollLeft === "number") {
377
+ x.value = node.scrollLeft;
378
+ y.value = node.scrollTop;
379
+ }
380
+ };
381
+ const handle = (() => {
382
+ let prevX = 0;
383
+ let prevY = 0;
384
+ const update = () => {
385
+ const el = toValue(target) ?? void 0;
386
+ const nextX = x.value;
387
+ const nextY = y.value;
388
+ readPosition(el);
389
+ if (withDirection) {
390
+ if (x.value > prevX) directionX.value = "right";
391
+ else if (x.value < prevX) directionX.value = "left";
392
+ if (y.value > prevY) directionY.value = "down";
393
+ else if (y.value < prevY) directionY.value = "up";
394
+ }
395
+ prevX = nextX;
396
+ prevY = nextY;
397
+ };
398
+ return throttle ? rafThrottle(update) : update;
399
+ })();
400
+ watch(() => toValue(target), (raw) => {
401
+ const el = raw === document ? toElement(document.documentElement) ?? window : raw;
402
+ readPosition(el);
403
+ el?.addEventListener("scroll", handle, {
404
+ capture,
405
+ passive: true
406
+ });
407
+ }, { immediate: true });
408
+ tryOnScopeDispose(() => {
409
+ toValue(target)?.removeEventListener("scroll", handle, { capture });
410
+ });
411
+ return {
412
+ x,
413
+ y,
414
+ directionX,
415
+ directionY
416
+ };
417
+ }
418
+ //#endregion
419
+ //#region src/core/useScrollLock.ts
420
+ let lockCount = 0;
421
+ let previousOverflow = "";
422
+ let previousPaddingRight = "";
423
+ /**
424
+ * 打开浮层时锁定背景滚动。采用引用计数,支持多浮层叠加;
425
+ * 作用域销毁时自动释放一次锁定,SSR 安全。
426
+ *
427
+ * @param initial 初始是否锁定,默认 false
428
+ * @internal 消费方:useModal 及下拉浮层
429
+ */
430
+ function useScrollLock(initial = false) {
431
+ const isLocked = ref(false);
432
+ const lock = () => {
433
+ if (!isClient || isLocked.value) return;
434
+ if (lockCount === 0) {
435
+ const body = document.body;
436
+ previousOverflow = body.style.overflow;
437
+ previousPaddingRight = body.style.paddingRight;
438
+ const scrollbarWidth = window.innerWidth - body.clientWidth;
439
+ body.style.overflow = "hidden";
440
+ if (scrollbarWidth > 0) body.style.paddingRight = `${scrollbarWidth}px`;
441
+ }
442
+ lockCount += 1;
443
+ isLocked.value = true;
444
+ };
445
+ const unlock = () => {
446
+ if (!isClient || !isLocked.value) return;
447
+ lockCount = Math.max(0, lockCount - 1);
448
+ if (lockCount === 0) {
449
+ document.body.style.overflow = previousOverflow;
450
+ document.body.style.paddingRight = previousPaddingRight;
451
+ }
452
+ isLocked.value = false;
453
+ };
454
+ const toggle = (next) => {
455
+ if (next ?? !isLocked.value) lock();
456
+ else unlock();
457
+ };
458
+ if (initial) lock();
459
+ tryOnScopeDispose(unlock);
460
+ return {
461
+ isLocked,
462
+ lock,
463
+ unlock,
464
+ toggle
465
+ };
466
+ }
467
+ //#endregion
468
+ //#region src/core/useScrollIntoView.ts
469
+ /**
470
+ * 将目标元素平滑滚动至可视区。返回命令式方法,供 Tour/Anchor/Tabs 复用。
471
+ *
472
+ * @param defaultTarget 默认滚动目标(调用时可覆盖)
473
+ */
474
+ function useScrollIntoView(defaultTarget) {
475
+ return (target = defaultTarget, options = {}) => {
476
+ if (!isClient) return;
477
+ toElement(target ?? null)?.scrollIntoView({
478
+ behavior: options.behavior ?? "smooth",
479
+ block: options.block ?? "start",
480
+ inline: options.inline ?? "nearest"
481
+ });
482
+ };
483
+ }
484
+ //#endregion
485
+ //#region src/core/useClickOutside.ts
486
+ /**
487
+ * 点击目标元素外部时触发回调。SSR 安全,作用域销毁自动解绑。
488
+ *
489
+ * @internal 消费方:useDismiss 及各选择器组件
490
+ */
491
+ function useClickOutside(target, handler, options = {}) {
492
+ if (!isClient) return;
493
+ const { enabled = true, event = "pointerdown" } = options;
494
+ const listener = (ev) => {
495
+ if (!toValue(enabled)) return;
496
+ const main = toElement(target);
497
+ if (!main) return;
498
+ const extras = (toValue(options.ignore ?? []) ?? []).map((item) => toElement(item)).filter((el) => !!el);
499
+ if (isEventInside(ev.target, main, ...extras)) return;
500
+ handler(ev);
501
+ };
502
+ document.addEventListener(event, listener, true);
503
+ tryOnScopeDispose(() => {
504
+ document.removeEventListener(event, listener, true);
505
+ });
506
+ }
507
+ //#endregion
508
+ //#region src/core/useFocusTrap.ts
509
+ /**
510
+ * 焦点陷阱:激活期间 Tab 在容器内循环,Esc 不做处理(由 useDismiss 编排)。
511
+ *
512
+ * @param container 陷阱容器,可为响应式引用
513
+ * @internal 消费方:useModal
514
+ */
515
+ function useFocusTrap(container, options = {}) {
516
+ const { initialFocus, immediate = true } = options;
517
+ const isActive = ref(false);
518
+ const focusFirst = () => {
519
+ getFocusableList(toValue(container))[0]?.focus();
520
+ };
521
+ const focusLast = () => {
522
+ const list = getFocusableList(toValue(container));
523
+ list[list.length - 1]?.focus();
524
+ };
525
+ const onKeydown = (ev) => {
526
+ if (ev.key !== "Tab" || !isActive.value) return;
527
+ const el = toValue(container);
528
+ if (!el) return;
529
+ const list = getFocusableList(el);
530
+ if (list.length === 0) {
531
+ ev.preventDefault();
532
+ return;
533
+ }
534
+ const first = list[0];
535
+ const last = list[list.length - 1];
536
+ const active = document.activeElement;
537
+ if (ev.shiftKey && (active === first || !el.contains(active))) {
538
+ ev.preventDefault();
539
+ last.focus();
540
+ } else if (!ev.shiftKey && active === last) {
541
+ ev.preventDefault();
542
+ first.focus();
543
+ }
544
+ };
545
+ const trap = () => {
546
+ if (!isClient || isActive.value) return;
547
+ isActive.value = true;
548
+ document.addEventListener("keydown", onKeydown, true);
549
+ if (immediate) {
550
+ const el = toValue(container);
551
+ ((initialFocus ? el?.querySelector(initialFocus) : null) ?? getFocusableList(el)[0] ?? el)?.focus();
552
+ }
553
+ };
554
+ const release = () => {
555
+ if (!isActive.value) return;
556
+ isActive.value = false;
557
+ document.removeEventListener("keydown", onKeydown, true);
558
+ };
559
+ tryOnScopeDispose(release);
560
+ return {
561
+ isActive,
562
+ trap,
563
+ release,
564
+ focusFirst,
565
+ focusLast
566
+ };
567
+ }
568
+ //#endregion
569
+ //#region src/core/useFocusReturn.ts
570
+ /**
571
+ * 浮层关闭后将焦点归还至打开前的元素。
572
+ *
573
+ * @internal 消费方:useModal、usePopoverModel
574
+ */
575
+ function useFocusReturn(options = {}) {
576
+ const { autoRestore = true } = options;
577
+ let previous = null;
578
+ const record = () => {
579
+ if (!isClient) return;
580
+ previous = document.activeElement instanceof HTMLElement ? document.activeElement : null;
581
+ };
582
+ const restore = () => {
583
+ if (!isClient || !previous || !previous.isConnected) {
584
+ previous = null;
585
+ return;
586
+ }
587
+ previous.focus({ preventScroll: true });
588
+ previous = null;
589
+ };
590
+ if (autoRestore) tryOnScopeDispose(restore);
591
+ return {
592
+ record,
593
+ restore
594
+ };
595
+ }
596
+ //#endregion
597
+ //#region src/core/useRovingFocus.ts
598
+ /**
599
+ * 列表内 roving-tabindex 焦点管理:方向键 / Home / End 移动激活项,
600
+ * 自动跳过禁用项,可选循环。Tab 只落到当前激活项。
601
+ *
602
+ * @internal 消费组件:Select、Menu、Tabs、Segmented、Tree
603
+ */
604
+ function useRovingFocus(options) {
605
+ const { count, isItemDisabled = () => false, orientation = "vertical", loop = true, initialIndex = 0, autofocus = true, container } = options;
606
+ const activeIndex = ref(initialIndex);
607
+ const getCount = () => Math.max(0, toValue(count));
608
+ const isEnabledIndex = (index) => index >= 0 && index < getCount() && !isItemDisabled(index);
609
+ const findEnabled = (from, step) => {
610
+ const total = getCount();
611
+ let cursor = from;
612
+ for (let i = 0; i < total; i += 1) {
613
+ cursor += step;
614
+ if (cursor < 0 || cursor >= total) {
615
+ if (!loop) return from;
616
+ cursor = cursor < 0 ? total - 1 : 0;
617
+ }
618
+ if (!isItemDisabled(cursor)) return cursor;
619
+ }
620
+ return from;
621
+ };
622
+ const setActiveIndex = (index) => {
623
+ if (!isEnabledIndex(index)) return;
624
+ activeIndex.value = index;
625
+ if (autofocus && isClient && container) (toElement(container)?.querySelectorAll("[data-roving-index]"))?.[index]?.focus();
626
+ };
627
+ const move = (step) => {
628
+ const next = findEnabled(activeIndex.value, step);
629
+ if (next !== activeIndex.value) setActiveIndex(next);
630
+ };
631
+ const first = () => {
632
+ const next = Array.from({ length: getCount() }, (_, i) => i).find((i) => !isItemDisabled(i));
633
+ if (next !== void 0) setActiveIndex(next);
634
+ };
635
+ const last = () => {
636
+ for (let i = getCount() - 1; i >= 0; i -= 1) if (!isItemDisabled(i)) {
637
+ setActiveIndex(i);
638
+ return;
639
+ }
640
+ };
641
+ const onKeydown = (event) => {
642
+ const allowVertical = orientation !== "horizontal";
643
+ const allowHorizontal = orientation !== "vertical";
644
+ switch (event.key) {
645
+ case "ArrowDown":
646
+ if (allowVertical) {
647
+ event.preventDefault();
648
+ move(1);
649
+ }
650
+ break;
651
+ case "ArrowUp":
652
+ if (allowVertical) {
653
+ event.preventDefault();
654
+ move(-1);
655
+ }
656
+ break;
657
+ case "ArrowRight":
658
+ if (allowHorizontal) {
659
+ event.preventDefault();
660
+ move(1);
661
+ }
662
+ break;
663
+ case "ArrowLeft":
664
+ if (allowHorizontal) {
665
+ event.preventDefault();
666
+ move(-1);
667
+ }
668
+ break;
669
+ case "Home":
670
+ event.preventDefault();
671
+ first();
672
+ break;
673
+ case "End":
674
+ event.preventDefault();
675
+ last();
676
+ }
677
+ };
678
+ const getTabIndex = (index) => index === activeIndex.value ? 0 : -1;
679
+ const onItemFocus = (index) => () => {
680
+ activeIndex.value = index;
681
+ if (!autofocus && container) {
682
+ const el = toElement(container);
683
+ if (el) getFocusableList(el)[index]?.setAttribute("tabindex", "0");
684
+ }
685
+ };
686
+ return {
687
+ activeIndex,
688
+ setActiveIndex,
689
+ onKeydown,
690
+ getTabIndex,
691
+ onItemFocus
692
+ };
693
+ }
694
+ //#endregion
695
+ //#region src/core/useTypeahead.ts
696
+ /**
697
+ * 键盘输入匹配选项:在重置间隔内连续输入字符组成搜索串,
698
+ * 按前缀匹配选项标签并跳转,单字符重复按时序循环匹配。
699
+ *
700
+ * @internal 消费组件:Select、Menu、Tree
701
+ */
702
+ function useTypeahead(options) {
703
+ const { getLabels, onMatch, resetDelay = 500, loop = true } = options;
704
+ const startIndex = options.startIndex ?? (() => lastMatchIndex);
705
+ const query = ref("");
706
+ let lastKeyTime = 0;
707
+ let lastMatchIndex = -1;
708
+ const timer = useTimeoutFn(() => reset());
709
+ function reset() {
710
+ query.value = "";
711
+ lastMatchIndex = -1;
712
+ }
713
+ const onKeydown = (event) => {
714
+ if (event.ctrlKey || event.metaKey || event.altKey) return;
715
+ if (event.key.length !== 1) return;
716
+ const labels = getLabels();
717
+ if (labels.length === 0) return;
718
+ const now = Date.now();
719
+ if (now - lastKeyTime > resetDelay) reset();
720
+ lastKeyTime = now;
721
+ const char = event.key.toLowerCase();
722
+ const isRepeatSingle = query.value.length === 1 && query.value === char;
723
+ query.value = isRepeatSingle ? char : query.value + char;
724
+ const keyword = query.value.toLowerCase();
725
+ let matched = -1;
726
+ if (isRepeatSingle) for (let step = 1; step <= labels.length; step += 1) {
727
+ const idx = (lastMatchIndex + step) % labels.length;
728
+ if (!loop && idx <= lastMatchIndex) break;
729
+ if (labels[idx].toLowerCase().startsWith(char)) {
730
+ matched = idx;
731
+ break;
732
+ }
733
+ }
734
+ else {
735
+ const start = startIndex();
736
+ for (let step = 0; step < labels.length; step += 1) {
737
+ const idx = (start + 1 + step) % labels.length;
738
+ if (labels[idx].toLowerCase().startsWith(keyword)) {
739
+ matched = idx;
740
+ break;
741
+ }
742
+ }
743
+ }
744
+ timer.start();
745
+ if (matched !== -1) {
746
+ lastMatchIndex = matched;
747
+ onMatch?.(matched);
748
+ }
749
+ };
750
+ return {
751
+ query,
752
+ onKeydown,
753
+ reset
754
+ };
755
+ }
756
+ //#endregion
757
+ //#region src/core/useDraggable.ts
758
+ const clamp = (value, min = 0, max = 1) => Math.min(max, Math.max(min, value));
759
+ /**
760
+ * 基于 Pointer Events 的拖拽原语:在目标元素盒内测量归一化位置,
761
+ * 自动处理边界钳制、指针捕获与清理。Slider / ColorPicker 共用同一份几何计算。
762
+ */
763
+ function useDraggable(target, options = {}) {
764
+ const { handle, axis = "both", bounds, initialRatio = {
765
+ x: 0,
766
+ y: 0
767
+ }, disabled = false, onStart, onMove, onEnd } = options;
768
+ const dragging = ref(false);
769
+ const x = ref(0);
770
+ const y = ref(0);
771
+ const ratioX = ref(clamp(initialRatio.x));
772
+ const ratioY = ref(clamp(initialRatio.y));
773
+ let rect = null;
774
+ const currentPosition = () => ({
775
+ x: x.value,
776
+ y: y.value,
777
+ ratioX: ratioX.value,
778
+ ratioY: ratioY.value
779
+ });
780
+ const readPosition = (clientX, clientY) => {
781
+ const box = rect ?? toElement(bounds ?? target)?.getBoundingClientRect() ?? null;
782
+ if (!box || box.width === 0 || box.height === 0) return currentPosition();
783
+ const nextRatioX = clamp((clientX - box.left) / box.width);
784
+ const nextRatioY = clamp((clientY - box.top) / box.height);
785
+ if (axis === "x") ratioX.value = nextRatioX;
786
+ else if (axis === "y") ratioY.value = nextRatioY;
787
+ else {
788
+ ratioX.value = nextRatioX;
789
+ ratioY.value = nextRatioY;
790
+ }
791
+ x.value = ratioX.value * box.width;
792
+ y.value = ratioY.value * box.height;
793
+ return currentPosition();
794
+ };
795
+ const onPointerMove = (event) => {
796
+ if (!dragging.value) return;
797
+ const position = readPosition(event.clientX, event.clientY);
798
+ onMove?.(position, event);
799
+ };
800
+ const onPointerUp = (event) => {
801
+ if (!dragging.value) return;
802
+ dragging.value = false;
803
+ try {
804
+ toElement(handle ?? target)?.releasePointerCapture?.(event.pointerId);
805
+ } catch {}
806
+ document.removeEventListener("pointermove", onPointerMove);
807
+ document.removeEventListener("pointerup", onPointerUp);
808
+ onEnd?.(currentPosition(), event);
809
+ };
810
+ const onPointerDown = (event) => {
811
+ if (!isClient || toValue(disabled) || event.button !== 0) return;
812
+ rect = toElement(bounds ?? target)?.getBoundingClientRect() ?? null;
813
+ dragging.value = true;
814
+ try {
815
+ toElement(handle ?? target)?.setPointerCapture?.(event.pointerId);
816
+ } catch {}
817
+ document.addEventListener("pointermove", onPointerMove);
818
+ document.addEventListener("pointerup", onPointerUp);
819
+ const position = readPosition(event.clientX, event.clientY);
820
+ onStart?.(position, event);
821
+ };
822
+ watch(() => toElement(handle ?? target), (el, oldEl) => {
823
+ oldEl?.removeEventListener("pointerdown", onPointerDown);
824
+ el?.addEventListener("pointerdown", onPointerDown);
825
+ }, { immediate: true });
826
+ const moveToRatio = (nextX, nextY) => {
827
+ ratioX.value = axis === "y" ? ratioX.value : clamp(nextX);
828
+ ratioY.value = axis === "x" ? ratioY.value : clamp(nextY);
829
+ const box = toElement(bounds ?? target)?.getBoundingClientRect();
830
+ if (box) {
831
+ x.value = ratioX.value * box.width;
832
+ y.value = ratioY.value * box.height;
833
+ }
834
+ };
835
+ tryOnScopeDispose(() => {
836
+ document.removeEventListener("pointermove", onPointerMove);
837
+ document.removeEventListener("pointerup", onPointerUp);
838
+ });
839
+ return {
840
+ x,
841
+ y,
842
+ ratioX,
843
+ ratioY,
844
+ dragging,
845
+ moveToRatio
846
+ };
847
+ }
848
+ //#endregion
849
+ //#region src/core/useClipboard.ts
850
+ /**
851
+ * 复制文本到剪贴板,自动派生短暂的成功反馈;
852
+ * 不支持 Clipboard API 时降级为隐藏 textarea + execCommand。
853
+ *
854
+ * @internal 消费组件:Typography;宿主可独立使用
855
+ */
856
+ function useClipboard(options = {}) {
857
+ const { copiedDuration = 1500, legacy = true } = options;
858
+ const copied = ref(false);
859
+ const error = ref(null);
860
+ const timer = useTimeoutFn(() => {
861
+ copied.value = false;
862
+ });
863
+ const legacyCopy = (text) => {
864
+ if (!isClient) return false;
865
+ const textarea = document.createElement("textarea");
866
+ textarea.value = text;
867
+ textarea.setAttribute("readonly", "");
868
+ textarea.style.position = "fixed";
869
+ textarea.style.opacity = "0";
870
+ document.body.appendChild(textarea);
871
+ textarea.select();
872
+ try {
873
+ return document.execCommand("copy");
874
+ } finally {
875
+ document.body.removeChild(textarea);
876
+ }
877
+ };
878
+ const copy = async (text) => {
879
+ error.value = null;
880
+ let succeeded = false;
881
+ try {
882
+ if (isClient && navigator.clipboard?.writeText) try {
883
+ await navigator.clipboard.writeText(text);
884
+ succeeded = true;
885
+ } catch (apiErr) {
886
+ if (legacy && legacyCopy(text)) succeeded = true;
887
+ else throw apiErr;
888
+ }
889
+ else if (legacy) {
890
+ if (!legacyCopy(text)) throw new Error("clipboard unavailable");
891
+ succeeded = true;
892
+ } else throw new Error("clipboard unavailable");
893
+ } catch (err) {
894
+ error.value = err;
895
+ copied.value = false;
896
+ return false;
897
+ }
898
+ if (succeeded) {
899
+ copied.value = true;
900
+ timer.start(copiedDuration);
901
+ }
902
+ return succeeded;
903
+ };
904
+ return {
905
+ copied,
906
+ error,
907
+ copy
908
+ };
909
+ }
910
+ //#endregion
911
+ //#region src/core/useTransitionVisible.ts
912
+ /**
913
+ * 进出场过渡的挂载状态机:
914
+ * - 打开时先 rendered=true(挂载),下一帧 visible=true(触发进入过渡);
915
+ * - 关闭时 visible=false(触发离开过渡),after-leave 后 rendered=false(卸载)。
916
+ *
917
+ * 组件模板用法:
918
+ * `<Transition @after-leave="onAfterLeave"><div v-if="rendered" v-show="visible" />`
919
+ *
920
+ * @param isOpen 开合来源(ref / getter)
921
+ */
922
+ function useTransitionVisible(isOpen) {
923
+ const rendered = ref(toValue(isOpen));
924
+ const visible = ref(false);
925
+ watch(() => toValue(isOpen), (open) => {
926
+ if (open) {
927
+ rendered.value = true;
928
+ nextTick(() => {
929
+ visible.value = true;
930
+ });
931
+ } else visible.value = false;
932
+ }, {
933
+ immediate: true,
934
+ flush: "sync"
935
+ });
936
+ const onAfterLeave = () => {
937
+ rendered.value = false;
938
+ };
939
+ return {
940
+ rendered,
941
+ visible,
942
+ onAfterLeave
943
+ };
944
+ }
945
+ //#endregion
946
+ //#region src/core/useZIndex.ts
947
+ const BASE_Z_INDEX = 2e3;
948
+ let globalZIndex = BASE_Z_INDEX;
949
+ /**
950
+ * 全局浮层层级递增管理。Dialog/Drawer/Popover/Message/Notification
951
+ * 共用同一序列,组件卸载无需回收(仅单调递增)。
952
+ */
953
+ function useZIndex(initial) {
954
+ const currentZIndex = ref(initial ?? globalZIndex);
955
+ const nextZIndex = () => {
956
+ globalZIndex += 1;
957
+ currentZIndex.value = globalZIndex;
958
+ return globalZIndex;
959
+ };
960
+ return {
961
+ currentZIndex,
962
+ nextZIndex,
963
+ getGlobalZIndex: () => globalZIndex
964
+ };
965
+ }
966
+ /** 重置全局层级(仅供测试使用) */
967
+ function resetGlobalZIndex(value = BASE_Z_INDEX) {
968
+ globalZIndex = value;
969
+ }
970
+ //#endregion
971
+ //#region src/behavior/useOpen.ts
972
+ /**
973
+ * 开合状态机:支持受控(modelValue + update:modelValue)与非受控,
974
+ * 可选的打开 / 关闭延迟(由 useHoverIntent 之类场景复用)。
975
+ *
976
+ * @internal 消费组件:Dialog/Drawer(经 useModal)、Popover、Dropdown、Select、Tooltip
977
+ */
978
+ function useOpen(options = {}) {
979
+ const { openDelay = 0, closeDelay = 0, onChange, ...controlled } = options;
980
+ const { value } = useControllableModel({
981
+ modelValue: controlled.modelValue,
982
+ defaultValue: controlled.defaultValue,
983
+ fallback: false,
984
+ onChange: (next) => {
985
+ controlled["onUpdate:modelValue"]?.(next);
986
+ onChange?.(next);
987
+ }
988
+ });
989
+ const isOpen = value;
990
+ const openTimer = useTimeoutFn(() => {
991
+ isOpen.value = true;
992
+ });
993
+ const closeTimer = useTimeoutFn(() => {
994
+ isOpen.value = false;
995
+ });
996
+ const open = () => {
997
+ closeTimer.stop();
998
+ if (openDelay > 0) openTimer.start(openDelay);
999
+ else isOpen.value = true;
1000
+ };
1001
+ const close = () => {
1002
+ openTimer.stop();
1003
+ if (closeDelay > 0) closeTimer.start(closeDelay);
1004
+ else isOpen.value = false;
1005
+ };
1006
+ const toggle = (next) => {
1007
+ if (next ?? !isOpen.value) open();
1008
+ else close();
1009
+ };
1010
+ return {
1011
+ isOpen,
1012
+ open,
1013
+ close,
1014
+ toggle
1015
+ };
1016
+ }
1017
+ //#endregion
1018
+ //#region src/behavior/useDismiss.ts
1019
+ /**
1020
+ * ESC、外部点击、滚动失焦的统一关闭编排。仅在浮层打开时生效,
1021
+ * 监听器随作用域自动清理。
1022
+ *
1023
+ * @internal 消费组件:全部浮层(经 useModal / usePopoverModel 或直接使用)
1024
+ */
1025
+ function useDismiss(options) {
1026
+ const { isOpen, reference, floating, onDismiss, escapeKey = true, outsidePress = true, outsidePressEvent = "pointerdown", scroll = false, enabled = true } = options;
1027
+ const active = () => isClient && toValue(isOpen) && toValue(enabled);
1028
+ useEventListener(isClient ? document : null, "keydown", (event) => {
1029
+ if (!active() || !escapeKey || event.key !== "Escape") return;
1030
+ event.stopPropagation();
1031
+ onDismiss("escape-key");
1032
+ }, true);
1033
+ useEventListener(isClient ? document : null, outsidePressEvent, (event) => {
1034
+ if (!active() || !outsidePress) return;
1035
+ if (isEventInside(event.target, toElement(reference ?? null), toElement(floating ?? null))) return;
1036
+ onDismiss("outside-press");
1037
+ }, true);
1038
+ useEventListener(isClient ? window : null, "scroll", () => {
1039
+ if (!active() || !scroll) return;
1040
+ onDismiss("scroll");
1041
+ }, true);
1042
+ }
1043
+ //#endregion
1044
+ //#region src/behavior/useFloating.ts
1045
+ const OPPOSITE_SIDE = {
1046
+ top: "bottom",
1047
+ bottom: "top",
1048
+ left: "right",
1049
+ right: "left"
1050
+ };
1051
+ function splitPlacement(placement) {
1052
+ const [side, align] = placement.split("-");
1053
+ return [side, align ?? "middle"];
1054
+ }
1055
+ function joinPlacement(side, align) {
1056
+ return align === "middle" ? side : `${side}-${align}`;
1057
+ }
1058
+ /** 按方位与对齐计算未修正的基准坐标 */
1059
+ function computeBaseCoords(side, align, reference, floating, gap) {
1060
+ let x = 0;
1061
+ let y = 0;
1062
+ switch (side) {
1063
+ case "bottom":
1064
+ y = reference.bottom + gap;
1065
+ break;
1066
+ case "top":
1067
+ y = reference.top - floating.height - gap;
1068
+ break;
1069
+ case "right":
1070
+ x = reference.right + gap;
1071
+ break;
1072
+ case "left": x = reference.left - floating.width - gap;
1073
+ }
1074
+ if (side === "bottom" || side === "top") {
1075
+ if (align === "start") x = reference.left;
1076
+ else if (align === "end") x = reference.right - floating.width;
1077
+ else x = reference.left + (reference.width - floating.width) / 2;
1078
+ } else if (align === "start") y = reference.top;
1079
+ else if (align === "end") y = reference.bottom - floating.height;
1080
+ else y = reference.top + (reference.height - floating.height) / 2;
1081
+ return {
1082
+ x,
1083
+ y
1084
+ };
1085
+ }
1086
+ /** 判定给定方位是否有足够空间(含视口留白) */
1087
+ function sideHasSpace(side, reference, floating, gap, padding) {
1088
+ switch (side) {
1089
+ case "top": return reference.top - gap - floating.height >= padding;
1090
+ case "bottom": return window.innerHeight - reference.bottom - gap - floating.height >= padding;
1091
+ case "left": return reference.left - gap - floating.width >= padding;
1092
+ case "right": return window.innerWidth - reference.right - gap - floating.width >= padding;
1093
+ }
1094
+ }
1095
+ /**
1096
+ * 锚点浮层定位引擎:方位、自动翻转、视口移位、间距、箭头坐标;
1097
+ * 滚动、窗口尺寸、锚点与浮层尺寸变化时自动更新。零第三方依赖。
1098
+ *
1099
+ * @internal 消费组件:Tooltip/Popover/Dropdown/Select/Cascader/DatePicker/
1100
+ * TimePicker/ColorPicker/TreeSelect/AutoComplete/Mention/Popconfirm
1101
+ */
1102
+ function useFloating(options = {}) {
1103
+ const { placement = "bottom", strategy = "fixed", gap = 4, shiftPadding = 4, flip = true, shift = true, arrowSize, open, onPositioned } = options;
1104
+ const referenceRef = ref(null);
1105
+ const floatingRef = ref(null);
1106
+ const arrowRef = ref(null);
1107
+ const currentPlacement = ref(toValue(placement));
1108
+ const x = ref(0);
1109
+ const y = ref(0);
1110
+ const arrowX = ref(void 0);
1111
+ const arrowY = ref(void 0);
1112
+ const applyOffset = (coords) => {
1113
+ if (strategy !== "absolute") return coords;
1114
+ const offsetParent = floatingRef.value?.offsetParent;
1115
+ if (!offsetParent || offsetParent === document.body) return coords;
1116
+ const parent = offsetParent.getBoundingClientRect();
1117
+ return {
1118
+ x: coords.x - parent.left,
1119
+ y: coords.y - parent.top
1120
+ };
1121
+ };
1122
+ const update = () => {
1123
+ if (!isClient) return;
1124
+ const referenceEl = referenceRef.value;
1125
+ const floatingEl = floatingRef.value;
1126
+ if (!referenceEl || !floatingEl) return;
1127
+ const referenceRect = referenceEl.getBoundingClientRect();
1128
+ const floatingRect = floatingEl.getBoundingClientRect();
1129
+ const size = {
1130
+ width: floatingRect.width,
1131
+ height: floatingRect.height
1132
+ };
1133
+ let [side, align] = splitPlacement(toValue(placement));
1134
+ if (flip && !sideHasSpace(side, referenceRect, size, gap, shiftPadding)) {
1135
+ const opposite = OPPOSITE_SIDE[side];
1136
+ if (sideHasSpace(opposite, referenceRect, size, gap, shiftPadding)) side = opposite;
1137
+ }
1138
+ let coords = computeBaseCoords(side, align, referenceRect, size, gap);
1139
+ if (shift) {
1140
+ const minX = shiftPadding;
1141
+ const maxX = window.innerWidth - size.width - shiftPadding;
1142
+ const minY = shiftPadding;
1143
+ const maxY = window.innerHeight - size.height - shiftPadding;
1144
+ coords = {
1145
+ x: Math.min(Math.max(coords.x, minX), Math.max(minX, maxX)),
1146
+ y: Math.min(Math.max(coords.y, minY), Math.max(minY, maxY))
1147
+ };
1148
+ }
1149
+ coords = applyOffset(coords);
1150
+ x.value = coords.x;
1151
+ y.value = coords.y;
1152
+ currentPlacement.value = joinPlacement(side, align);
1153
+ const arrowEl = arrowRef.value;
1154
+ if (arrowEl) {
1155
+ const arrow = arrowSize ?? arrowEl.offsetWidth ?? 0;
1156
+ const referenceCenterX = (referenceRect.left + referenceRect.right) / 2 - coords.x;
1157
+ const referenceCenterY = (referenceRect.top + referenceRect.bottom) / 2 - coords.y;
1158
+ const min = shiftPadding;
1159
+ const maxX = Math.max(min, size.width - arrow - shiftPadding);
1160
+ const maxY = Math.max(min, size.height - arrow - shiftPadding);
1161
+ if (side === "top" || side === "bottom") {
1162
+ arrowX.value = Math.min(Math.max(referenceCenterX - arrow / 2, min), maxX);
1163
+ arrowY.value = void 0;
1164
+ } else {
1165
+ arrowY.value = Math.min(Math.max(referenceCenterY - arrow / 2, min), maxY);
1166
+ arrowX.value = void 0;
1167
+ }
1168
+ }
1169
+ onPositioned?.({
1170
+ placement: currentPlacement.value,
1171
+ x: coords.x,
1172
+ y: coords.y
1173
+ });
1174
+ };
1175
+ const floatingStyles = computed(() => ({
1176
+ position: strategy,
1177
+ left: 0,
1178
+ top: 0,
1179
+ transform: `translate(${Math.round(x.value)}px, ${Math.round(y.value)}px)`
1180
+ }));
1181
+ useResizeObserver(referenceRef);
1182
+ useResizeObserver(floatingRef);
1183
+ useEventListener(isClient ? window : null, "resize", update);
1184
+ useEventListener(isClient ? window : null, "scroll", update, {
1185
+ capture: true,
1186
+ passive: true
1187
+ });
1188
+ watch(() => [
1189
+ referenceRef.value,
1190
+ floatingRef.value,
1191
+ toValue(placement)
1192
+ ], () => nextTick(update), {
1193
+ immediate: true,
1194
+ flush: "post"
1195
+ });
1196
+ if (open !== void 0) watch(() => toValue(open), (isOpen) => {
1197
+ if (isOpen) nextTick(update);
1198
+ });
1199
+ watch(() => [floatingRef.value?.offsetWidth, floatingRef.value?.offsetHeight], () => update());
1200
+ return {
1201
+ referenceRef,
1202
+ floatingRef,
1203
+ arrowRef,
1204
+ placement: currentPlacement,
1205
+ x,
1206
+ y,
1207
+ arrowX,
1208
+ arrowY,
1209
+ floatingStyles,
1210
+ update
1211
+ };
1212
+ }
1213
+ //#endregion
1214
+ //#region src/behavior/useHoverIntent.ts
1215
+ /**
1216
+ * 悬停触发的开合延迟控制,防止抖动与误触。
1217
+ * 触发器与浮层应同时绑定返回的 enter/leave 处理器。
1218
+ *
1219
+ * @internal 消费组件:Tooltip、Dropdown(hover)、Menu
1220
+ */
1221
+ function useHoverIntent(options = {}) {
1222
+ const { showDelay = 100, hideDelay = 100, disabled = false } = options;
1223
+ const isHovered = ref(false);
1224
+ const showTimer = useTimeoutFn(() => {
1225
+ isHovered.value = true;
1226
+ });
1227
+ const hideTimer = useTimeoutFn(() => {
1228
+ isHovered.value = false;
1229
+ });
1230
+ const onMouseEnter = () => {
1231
+ if (disabled) return;
1232
+ hideTimer.stop();
1233
+ if (showDelay <= 0) isHovered.value = true;
1234
+ else showTimer.start(showDelay);
1235
+ };
1236
+ const onMouseLeave = () => {
1237
+ if (disabled) return;
1238
+ showTimer.stop();
1239
+ if (hideDelay <= 0) isHovered.value = false;
1240
+ else hideTimer.start(hideDelay);
1241
+ };
1242
+ const setOpen = (open) => {
1243
+ showTimer.stop();
1244
+ hideTimer.stop();
1245
+ isHovered.value = open;
1246
+ };
1247
+ return {
1248
+ isHovered,
1249
+ onMouseEnter,
1250
+ onMouseLeave,
1251
+ setOpen
1252
+ };
1253
+ }
1254
+ //#endregion
1255
+ //#region src/behavior/useListboxNavigation.ts
1256
+ /**
1257
+ * 选项列表键盘导航:上下移动、Home/End、回车选中、禁用项跳过、边界循环。
1258
+ * 与焦点管理解耦——本函数只维护逻辑激活索引。
1259
+ *
1260
+ * @internal 消费组件:Select、Dropdown、Menu、Cascader、AutoComplete、
1261
+ * Mention、Segmented、TreeSelect、TimePicker
1262
+ */
1263
+ function useListboxNavigation(options) {
1264
+ const { count, isItemDisabled = () => false, orientation = "vertical", loop = true, initialIndex = -1, onSelect, onActiveChange } = options;
1265
+ const activeIndex = ref(initialIndex);
1266
+ const getCount = () => Math.max(0, toValue(count));
1267
+ const setActiveIndex = (index) => {
1268
+ if (index !== -1 && (index < 0 || index >= getCount())) return;
1269
+ activeIndex.value = index;
1270
+ onActiveChange?.(index);
1271
+ };
1272
+ const findEnabled = (from, step) => {
1273
+ const total = getCount();
1274
+ if (total === 0) return -1;
1275
+ let cursor = from;
1276
+ for (let i = 0; i < total; i += 1) {
1277
+ cursor += step;
1278
+ if (cursor < 0 || cursor >= total) {
1279
+ if (!loop) return from;
1280
+ cursor = cursor < 0 ? total - 1 : 0;
1281
+ }
1282
+ if (!isItemDisabled(cursor)) return cursor;
1283
+ }
1284
+ return from;
1285
+ };
1286
+ const moveNext = () => {
1287
+ const next = findEnabled(activeIndex.value, 1);
1288
+ if (next !== activeIndex.value && next !== -1) setActiveIndex(next);
1289
+ };
1290
+ const movePrev = () => {
1291
+ const prev = findEnabled(activeIndex.value, -1);
1292
+ if (prev !== activeIndex.value && prev !== -1) setActiveIndex(prev);
1293
+ };
1294
+ const moveFirst = () => {
1295
+ const first = findEnabled(-1, 1);
1296
+ if (first !== -1) setActiveIndex(first);
1297
+ };
1298
+ const moveLast = () => {
1299
+ for (let i = getCount() - 1; i >= 0; i -= 1) if (!isItemDisabled(i)) {
1300
+ setActiveIndex(i);
1301
+ return;
1302
+ }
1303
+ };
1304
+ const selectActive = () => {
1305
+ const index = activeIndex.value;
1306
+ if (index >= 0 && !isItemDisabled(index)) onSelect?.(index);
1307
+ };
1308
+ const onKeydown = (event) => {
1309
+ const allowVertical = orientation !== "horizontal";
1310
+ const allowHorizontal = orientation !== "vertical";
1311
+ switch (event.key) {
1312
+ case "ArrowDown":
1313
+ if (allowVertical) {
1314
+ event.preventDefault();
1315
+ moveNext();
1316
+ }
1317
+ break;
1318
+ case "ArrowUp":
1319
+ if (allowVertical) {
1320
+ event.preventDefault();
1321
+ movePrev();
1322
+ }
1323
+ break;
1324
+ case "ArrowRight":
1325
+ if (allowHorizontal) {
1326
+ event.preventDefault();
1327
+ moveNext();
1328
+ }
1329
+ break;
1330
+ case "ArrowLeft":
1331
+ if (allowHorizontal) {
1332
+ event.preventDefault();
1333
+ movePrev();
1334
+ }
1335
+ break;
1336
+ case "Home":
1337
+ event.preventDefault();
1338
+ moveFirst();
1339
+ break;
1340
+ case "End":
1341
+ event.preventDefault();
1342
+ moveLast();
1343
+ break;
1344
+ case "Enter":
1345
+ case " ":
1346
+ event.preventDefault();
1347
+ selectActive();
1348
+ }
1349
+ };
1350
+ const reset = () => setActiveIndex(initialIndex);
1351
+ return {
1352
+ activeIndex,
1353
+ setActiveIndex,
1354
+ moveNext,
1355
+ movePrev,
1356
+ moveFirst,
1357
+ moveLast,
1358
+ onKeydown,
1359
+ reset
1360
+ };
1361
+ }
1362
+ //#endregion
1363
+ //#region src/behavior/useAsyncState.ts
1364
+ /**
1365
+ * 将外部异步函数包装为标准 AsyncStateLike:
1366
+ * 管理 pending/success/error 三态、重试与 stale 标记。
1367
+ * 函数自身不发起任何业务请求——请求函数由宿主提供。
1368
+ *
1369
+ * @param fetcher 返回 Promise 的数据获取函数
1370
+ * @internal 消费方:远程 Select/AutoComplete/Mention;Table 内联编排;宿主可独立使用
1371
+ */
1372
+ function useAsyncState(fetcher, options = {}) {
1373
+ const { initialData, immediate = true, immediateArgs, preserveDataOnRefresh = true, onSuccess, onError } = options;
1374
+ const state = ref({
1375
+ status: "pending",
1376
+ data: initialData
1377
+ });
1378
+ const loading = ref(true);
1379
+ const error = ref(void 0);
1380
+ let lastArgs = immediateArgs;
1381
+ let requestId = 0;
1382
+ const run = async (...args) => {
1383
+ const currentId = ++requestId;
1384
+ lastArgs = args;
1385
+ const hadSuccess = state.value.status === "success";
1386
+ state.value = {
1387
+ status: "pending",
1388
+ data: hadSuccess && preserveDataOnRefresh ? state.value.data : initialData,
1389
+ isStale: hadSuccess,
1390
+ retry: () => retry()
1391
+ };
1392
+ loading.value = true;
1393
+ error.value = void 0;
1394
+ try {
1395
+ const data = await fetcher(...args);
1396
+ if (currentId !== requestId) return void 0;
1397
+ state.value = {
1398
+ status: "success",
1399
+ data,
1400
+ isStale: false,
1401
+ retry: () => retry()
1402
+ };
1403
+ error.value = void 0;
1404
+ onSuccess?.(data, ...args);
1405
+ return data;
1406
+ } catch (err) {
1407
+ if (currentId !== requestId) return void 0;
1408
+ state.value = {
1409
+ status: "error",
1410
+ data: state.value.data,
1411
+ error: err,
1412
+ isStale: false,
1413
+ retry: () => retry()
1414
+ };
1415
+ error.value = err;
1416
+ onError?.(err, ...args);
1417
+ return;
1418
+ } finally {
1419
+ if (currentId === requestId) loading.value = false;
1420
+ }
1421
+ };
1422
+ const retry = () => run(...lastArgs ?? []);
1423
+ if (immediate) run(...immediateArgs ?? []);
1424
+ else {
1425
+ loading.value = false;
1426
+ state.value = {
1427
+ status: "success",
1428
+ data: initialData,
1429
+ retry: () => retry()
1430
+ };
1431
+ }
1432
+ return {
1433
+ state,
1434
+ run,
1435
+ retry,
1436
+ loading,
1437
+ error
1438
+ };
1439
+ }
1440
+ //#endregion
1441
+ //#region src/behavior/useActionState.ts
1442
+ /** 归一 action 返回值:resolve 值导出错误(status 为 error 或携带 error)时返回失败信息,否则 null */
1443
+ function extractActionFailure(result) {
1444
+ if (result !== null && typeof result === "object") {
1445
+ const { status, error } = result;
1446
+ if (status === "error" || error != null) return { error: error ?? result };
1447
+ }
1448
+ return null;
1449
+ }
1450
+ /** 把动作错误归一为可展示文案:优先 message,其次 error 字符串 / Error.message,最后回退文案 */
1451
+ function actionErrorText(error, fallback = "操作失败") {
1452
+ if (error && typeof error === "object") {
1453
+ const candidate = error;
1454
+ if (typeof candidate.message === "string" && candidate.message) return candidate.message;
1455
+ if (candidate.error != null) return actionErrorText(candidate.error, fallback);
1456
+ }
1457
+ if (typeof error === "string" && error) return error;
1458
+ if (error instanceof Error && error.message) return error.message;
1459
+ return fallback;
1460
+ }
1461
+ /**
1462
+ * 消费 ActionLike:执行期间自动派生 pending / disabled,错误归一存储。
1463
+ *
1464
+ * @internal 消费组件:Button、Switch、Popconfirm、Upload、Form 提交
1465
+ */
1466
+ function useActionState(action, options = {}) {
1467
+ const { disableOnPending = true, disabled, onSuccess, onError, onFinally } = options;
1468
+ const pending = ref(false);
1469
+ const error = ref(void 0);
1470
+ const disabledState = computed(() => disableOnPending && pending.value || disabled?.() === true);
1471
+ const run = async (...args) => {
1472
+ if (pending.value || disabled?.()) return void 0;
1473
+ pending.value = true;
1474
+ error.value = void 0;
1475
+ try {
1476
+ const result = await action?.(...args);
1477
+ const failure = extractActionFailure(result);
1478
+ if (failure) {
1479
+ error.value = failure.error;
1480
+ onError?.(failure.error);
1481
+ return result;
1482
+ }
1483
+ onSuccess?.(result);
1484
+ return result;
1485
+ } catch (err) {
1486
+ error.value = err;
1487
+ onError?.(err);
1488
+ return;
1489
+ } finally {
1490
+ pending.value = false;
1491
+ onFinally?.();
1492
+ }
1493
+ };
1494
+ return {
1495
+ pending,
1496
+ disabled: disabledState,
1497
+ error,
1498
+ run
1499
+ };
1500
+ }
1501
+ //#endregion
1502
+ //#region src/behavior/useActionValue.ts
1503
+ /**
1504
+ * 切换型组件的 action 消费:值变更先乐观应用,动作失败自动回退并记录错误。
1505
+ *
1506
+ * @internal 消费组件:Select、DatePicker、ColorPicker、Transfer、Rate、Segmented、Tabs 等即时变更组件
1507
+ */
1508
+ function useActionValue() {
1509
+ const pending = ref(false);
1510
+ const error = ref(void 0);
1511
+ const commit = async ({ execute, revert }) => {
1512
+ if (pending.value) return false;
1513
+ error.value = void 0;
1514
+ if (!execute) return true;
1515
+ pending.value = true;
1516
+ try {
1517
+ const failure = extractActionFailure(await execute());
1518
+ if (failure) {
1519
+ error.value = failure.error;
1520
+ revert?.();
1521
+ return false;
1522
+ }
1523
+ return true;
1524
+ } catch (err) {
1525
+ error.value = err;
1526
+ revert?.();
1527
+ return false;
1528
+ } finally {
1529
+ pending.value = false;
1530
+ }
1531
+ };
1532
+ return {
1533
+ pending,
1534
+ error,
1535
+ commit
1536
+ };
1537
+ }
1538
+ //#endregion
1539
+ //#region src/behavior/useToastQueue.ts
1540
+ let toastId = 0;
1541
+ /**
1542
+ * 命令式浮层队列:堆叠管理、去重、自动关闭、悬停暂停、手动销毁。
1543
+ * 只管理数据与计时,不渲染任何 DOM——渲染由 Message/Notification 组件承担。
1544
+ *
1545
+ * @internal 消费方:createMessage、createNotification
1546
+ */
1547
+ function useToastQueue(options = {}) {
1548
+ const { duration: defaultDuration = 3e3, max = 5, dedupe = true } = options;
1549
+ const items = ref([]);
1550
+ const timers = /* @__PURE__ */ new Map();
1551
+ const startedAt = /* @__PURE__ */ new Map();
1552
+ const clearTimer = (id) => {
1553
+ const timer = timers.get(id);
1554
+ if (timer !== void 0) {
1555
+ clearTimeout(timer);
1556
+ timers.delete(id);
1557
+ }
1558
+ };
1559
+ const remove = (id) => {
1560
+ clearTimer(id);
1561
+ startedAt.delete(id);
1562
+ items.value = items.value.filter((item) => item.id !== id);
1563
+ };
1564
+ const schedule = (item, wait = item.duration) => {
1565
+ if (wait <= 0) return;
1566
+ startedAt.set(item.id, Date.now());
1567
+ item.remaining = wait;
1568
+ timers.set(item.id, setTimeout(() => remove(item.id), wait));
1569
+ };
1570
+ const push = (type, content, pushOptions = {}) => {
1571
+ const duration = pushOptions.duration ?? defaultDuration;
1572
+ if (dedupe) {
1573
+ const existed = items.value.find((item) => item.type === type && item.content === content);
1574
+ if (existed) return existed;
1575
+ }
1576
+ const item = {
1577
+ id: ++toastId,
1578
+ type,
1579
+ content,
1580
+ title: pushOptions.title,
1581
+ duration,
1582
+ meta: pushOptions.meta,
1583
+ remaining: duration,
1584
+ closable: pushOptions.closable ?? true
1585
+ };
1586
+ items.value = [...items.value, item];
1587
+ schedule(item);
1588
+ if (items.value.length > max) items.value.slice(0, items.value.length - max).forEach((old) => remove(old.id));
1589
+ return item;
1590
+ };
1591
+ const clear = () => {
1592
+ Array.from(timers.keys()).forEach(clearTimer);
1593
+ items.value = [];
1594
+ };
1595
+ const pause = (id) => {
1596
+ const item = items.value.find((entry) => entry.id === id);
1597
+ const started = startedAt.get(id);
1598
+ if (!item || started === void 0) return;
1599
+ item.remaining = Math.max(0, item.remaining - (Date.now() - started));
1600
+ clearTimer(id);
1601
+ startedAt.delete(id);
1602
+ };
1603
+ const resume = (id) => {
1604
+ const item = items.value.find((entry) => entry.id === id);
1605
+ if (!item || timers.has(id)) return;
1606
+ schedule(item, item.remaining);
1607
+ };
1608
+ return {
1609
+ items,
1610
+ count: computed(() => items.value.length),
1611
+ push,
1612
+ remove,
1613
+ clear,
1614
+ pause,
1615
+ resume
1616
+ };
1617
+ }
1618
+ //#endregion
1619
+ //#region src/behavior/useOverlayContainer.ts
1620
+ /**
1621
+ * Teleport 目标容器的 SSR 安全解析。挂载后才访问 document,
1622
+ * 选择器不存在时按策略回退到 body。
1623
+ *
1624
+ * @internal 消费组件:全部 Teleport 浮层
1625
+ */
1626
+ function useOverlayContainer(options = {}) {
1627
+ const { to, fallbackToBody = true } = options;
1628
+ const container = ref(null);
1629
+ onMounted(() => {
1630
+ if (!isClient) return;
1631
+ if (!to) {
1632
+ container.value = document.body;
1633
+ return;
1634
+ }
1635
+ if (typeof to === "string") {
1636
+ const found = document.querySelector(to);
1637
+ container.value = found ?? (fallbackToBody ? document.body : null);
1638
+ } else container.value = to;
1639
+ });
1640
+ onBeforeUnmount(() => {
1641
+ container.value = null;
1642
+ });
1643
+ return { container };
1644
+ }
1645
+ //#endregion
1646
+ //#region src/behavior/useFormContext.ts
1647
+ const FORM_KEY = Symbol("cumquat-form");
1648
+ const FIELD_KEY = Symbol("cumquat-form-field");
1649
+ /** useForm 注册整表上下文 */
1650
+ function provideForm(context) {
1651
+ provide(FORM_KEY, context);
1652
+ }
1653
+ /** 表单控件 / FormItem 注入整表上下文,未处于表单中时返回 undefined */
1654
+ function injectForm() {
1655
+ return inject(FORM_KEY, void 0);
1656
+ }
1657
+ /** FormItem 注册字段上下文(向内部控件传递错误、id 等) */
1658
+ function provideFormField(context) {
1659
+ provide(FIELD_KEY, context);
1660
+ }
1661
+ /** 具体表单控件注入所属字段上下文 */
1662
+ function injectFormField() {
1663
+ return inject(FIELD_KEY, void 0);
1664
+ }
1665
+ //#endregion
1666
+ //#region src/form/form-types.ts
1667
+ /** 将单条或数组规则统一为数组 */
1668
+ function normalizeRules(rule) {
1669
+ if (!rule) return [];
1670
+ return Array.isArray(rule) ? rule : [rule];
1671
+ }
1672
+ /** 顺序执行规则,遇到首个失败即返回错误信息;全部通过返回 undefined */
1673
+ async function runRules(value, values, rules) {
1674
+ for (const rule of rules) {
1675
+ const result = await Promise.resolve().then(() => rule(value, values)).catch((err) => err instanceof Error ? err.message : String(err));
1676
+ if (result !== true) return result;
1677
+ }
1678
+ }
1679
+ //#endregion
1680
+ //#region src/form/useForm.ts
1681
+ /**
1682
+ * 无头表单模型:字段注册/注销、整表聚合值、同步/异步规则校验、
1683
+ * 提交、重置与脏检查。组件只负责布局与错误提示渲染。
1684
+ */
1685
+ function useForm(options = {}) {
1686
+ const { modelValue, defaultValue, rules = {}, disabled = false, labelPosition, onChange, onSubmit, onInvalid } = options;
1687
+ const initialSnapshot = () => ({ ...toValue(modelValue) ?? defaultValue ?? {} });
1688
+ const values = reactive({ ...initialSnapshot() });
1689
+ const errors = reactive({});
1690
+ const touched = reactive({});
1691
+ const fields = /* @__PURE__ */ new Map();
1692
+ const submitting = ref(false);
1693
+ const currentRules = () => toValue(rules);
1694
+ const setFieldValue = (name, value) => {
1695
+ const key = String(name);
1696
+ values[key] = value;
1697
+ };
1698
+ const registerField = (name, controller) => {
1699
+ const key = String(name);
1700
+ fields.set(key, controller);
1701
+ if (!(key in values) && controller.getValue) setFieldValue(key, controller.getValue());
1702
+ return () => {
1703
+ fields.delete(key);
1704
+ };
1705
+ };
1706
+ const validateField = async (name) => {
1707
+ const key = String(name);
1708
+ let error;
1709
+ const controller = fields.get(key);
1710
+ if (controller) {
1711
+ const result = await controller.validate();
1712
+ error = result === true ? void 0 : result;
1713
+ } else {
1714
+ const formRules = normalizeRules(currentRules()[key]);
1715
+ error = await runRules(values[key], values, formRules);
1716
+ }
1717
+ errors[key] = error;
1718
+ return error ?? true;
1719
+ };
1720
+ const validate = async () => {
1721
+ const names = /* @__PURE__ */ new Set([...fields.keys(), ...Object.keys(currentRules())]);
1722
+ return (await Promise.all(Array.from(names).map(async (name) => [name, await validateField(name)]))).every(([, result]) => result === true);
1723
+ };
1724
+ const clearValidate = (name) => {
1725
+ if (name === void 0) Object.keys(errors).forEach((key) => delete errors[key]);
1726
+ else delete errors[String(name)];
1727
+ };
1728
+ const resetField = (name) => {
1729
+ const key = String(name);
1730
+ if (key in initialSnapshot()) setFieldValue(key, initialSnapshot()[key]);
1731
+ delete errors[key];
1732
+ delete touched[key];
1733
+ fields.get(key)?.reset();
1734
+ };
1735
+ const reset = () => {
1736
+ const snapshot = initialSnapshot();
1737
+ Object.keys(values).forEach((key) => {
1738
+ if (!(key in snapshot)) delete values[key];
1739
+ });
1740
+ Object.assign(values, snapshot);
1741
+ Array.from(fields.values()).forEach((field) => field.reset());
1742
+ Object.keys(errors).forEach((key) => delete errors[key]);
1743
+ Object.keys(touched).forEach((key) => delete touched[key]);
1744
+ };
1745
+ const submit = async () => {
1746
+ if (submitting.value) return;
1747
+ if (!await validate()) {
1748
+ onInvalid?.({ ...errors });
1749
+ return;
1750
+ }
1751
+ submitting.value = true;
1752
+ try {
1753
+ await onSubmit?.({ ...values });
1754
+ } finally {
1755
+ submitting.value = false;
1756
+ }
1757
+ };
1758
+ watch(values, (next) => {
1759
+ onChange?.({ ...next });
1760
+ }, { deep: true });
1761
+ const validState = computed(() => Object.values(errors).every((error) => error === void 0));
1762
+ const dirty = computed(() => JSON.stringify(values) !== JSON.stringify(initialSnapshot()));
1763
+ const context = {
1764
+ values,
1765
+ errors,
1766
+ registerField,
1767
+ getFieldRules: (name) => currentRules()[String(name)],
1768
+ submit,
1769
+ reset,
1770
+ setFieldValue,
1771
+ disabled,
1772
+ labelPosition
1773
+ };
1774
+ provideForm(context);
1775
+ return {
1776
+ values,
1777
+ errors,
1778
+ touched,
1779
+ valid: validState,
1780
+ dirty,
1781
+ registerField,
1782
+ validateField,
1783
+ validate,
1784
+ clearValidate,
1785
+ setFieldValue,
1786
+ resetField,
1787
+ reset,
1788
+ submit,
1789
+ context
1790
+ };
1791
+ }
1792
+ //#endregion
1793
+ //#region src/form/useFormField.ts
1794
+ /**
1795
+ * 单字段模型:值的受控/非受控统一、校验信息、touched 状态、
1796
+ * aria 注入与表单上下文联动。所有表单控件共用。
1797
+ */
1798
+ function useFormField(options = {}) {
1799
+ const { name, modelValue, defaultValue, onChange, rules, validateOnBlur = true, validateOnChange = true, disabled, idPrefix = "cq-field" } = options;
1800
+ const form = injectForm();
1801
+ const fieldId = useId(idPrefix);
1802
+ const errorId = useId(`${idPrefix}-error`);
1803
+ const initialValue = toValue(modelValue) ?? defaultValue;
1804
+ const { value } = useControllableModel({
1805
+ modelValue,
1806
+ defaultValue,
1807
+ onChange: (next) => {
1808
+ onChange?.(next);
1809
+ if (form && name !== void 0) form.setFieldValue?.(name, next);
1810
+ }
1811
+ });
1812
+ const error = ref(void 0);
1813
+ const touched = ref(false);
1814
+ const mergedRules = () => {
1815
+ const own = normalizeRules(rules);
1816
+ return [...name !== void 0 ? normalizeRules(form?.getFieldRules?.(name)) : [], ...own];
1817
+ };
1818
+ const validate = async () => {
1819
+ const list = mergedRules();
1820
+ const result = list.length > 0 ? await runRules(value.value, form?.values ?? {}, list) : void 0;
1821
+ error.value = result;
1822
+ if (form && name !== void 0) form.errors[String(name)] = result;
1823
+ return result ?? true;
1824
+ };
1825
+ const clearValidate = () => {
1826
+ error.value = void 0;
1827
+ if (form && name !== void 0) delete form.errors[String(name)];
1828
+ };
1829
+ const reset = () => {
1830
+ value.value = initialValue;
1831
+ touched.value = false;
1832
+ clearValidate();
1833
+ };
1834
+ const handleChange = (next) => {
1835
+ value.value = next;
1836
+ if (validateOnChange && (touched.value || error.value !== void 0)) validate();
1837
+ };
1838
+ const handleBlur = () => {
1839
+ touched.value = true;
1840
+ if (form && name !== void 0) form.errors[String(name)] = error.value;
1841
+ if (validateOnBlur) validate();
1842
+ };
1843
+ if (form && name !== void 0) {
1844
+ form.setFieldValue?.(name, value.value);
1845
+ watch(value, (next) => form.setFieldValue?.(name, next));
1846
+ const unregister = form.registerField(name, {
1847
+ validate,
1848
+ reset,
1849
+ clearValidate,
1850
+ getValue: () => value.value,
1851
+ setValue: (next) => {
1852
+ value.value = next;
1853
+ }
1854
+ });
1855
+ onBeforeUnmount(unregister);
1856
+ error.value = form.errors[String(name)];
1857
+ }
1858
+ return {
1859
+ value,
1860
+ error,
1861
+ touched,
1862
+ valid: computed(() => error.value === void 0),
1863
+ disabled: computed(() => form?.disabled === true || toValue(disabled) === true),
1864
+ fieldId,
1865
+ errorId,
1866
+ aria: computed(() => ({
1867
+ id: fieldId.value,
1868
+ "aria-invalid": error.value ? true : void 0,
1869
+ "aria-describedby": error.value ? errorId.value : void 0
1870
+ })),
1871
+ handleChange,
1872
+ handleBlur,
1873
+ validate,
1874
+ reset,
1875
+ clearValidate
1876
+ };
1877
+ }
1878
+ //#endregion
1879
+ //#region src/form/useCheckableGroup.ts
1880
+ /**
1881
+ * 成组勾选值管理:多选数组(增删、全选、半选、数量约束)与单选互斥。
1882
+ *
1883
+ * @internal 消费组件:CheckboxGroup、RadioGroup
1884
+ */
1885
+ function useCheckableGroup(options = {}) {
1886
+ const { multiple = true, modelValue, defaultValue, onChange, options: getOptions, min = 0, max = Number.POSITIVE_INFINITY, isOptionDisabled = () => false } = options;
1887
+ const { value: model } = useControllableModel({
1888
+ modelValue,
1889
+ defaultValue,
1890
+ fallback: multiple ? [] : null,
1891
+ onChange
1892
+ });
1893
+ const selectedList = computed(() => {
1894
+ const current = model.value;
1895
+ if (Array.isArray(current)) return current;
1896
+ return current === null || current === void 0 ? [] : [current];
1897
+ });
1898
+ const isChecked = (value) => selectedList.value.some((item) => Object.is(item, value));
1899
+ const select = (value, checked) => {
1900
+ if (!multiple) {
1901
+ if (checked !== false) model.value = value;
1902
+ return;
1903
+ }
1904
+ const nextChecked = checked ?? !isChecked(value);
1905
+ const set = new Set(selectedList.value);
1906
+ if (nextChecked) {
1907
+ if (set.size >= max) return;
1908
+ set.add(value);
1909
+ } else {
1910
+ if (set.size <= min) return;
1911
+ set.delete(value);
1912
+ }
1913
+ model.value = Array.from(set);
1914
+ };
1915
+ const toggle = (value) => select(value);
1916
+ const enabledOptions = computed(() => (getOptions?.() ?? []).filter((value) => !isOptionDisabled(value)));
1917
+ const checkedCount = computed(() => selectedList.value.length);
1918
+ const allChecked = computed(() => {
1919
+ const options = enabledOptions.value;
1920
+ return options.length > 0 && options.every((value) => isChecked(value));
1921
+ });
1922
+ const indeterminate = computed(() => {
1923
+ const options = enabledOptions.value;
1924
+ const count = options.filter((value) => isChecked(value)).length;
1925
+ return count > 0 && count < options.length;
1926
+ });
1927
+ const toggleAll = (checked) => {
1928
+ if (!multiple) return;
1929
+ if (checked ?? !allChecked.value) {
1930
+ const merged = new Set(selectedList.value);
1931
+ for (const value of enabledOptions.value) {
1932
+ if (merged.size >= max) break;
1933
+ merged.add(value);
1934
+ }
1935
+ model.value = Array.from(merged);
1936
+ } else {
1937
+ const enabledSet = new Set(enabledOptions.value);
1938
+ model.value = selectedList.value.filter((value) => !enabledSet.has(value));
1939
+ }
1940
+ };
1941
+ return {
1942
+ model,
1943
+ isChecked,
1944
+ select,
1945
+ toggle,
1946
+ allChecked,
1947
+ indeterminate,
1948
+ toggleAll,
1949
+ checkedCount
1950
+ };
1951
+ }
1952
+ //#endregion
1953
+ //#region src/form/useSelectModel.ts
1954
+ /**
1955
+ * 选择器值模型:单选/多选值、选项标签回显、本地过滤、清除。
1956
+ * 不发请求、不管浮层开合(分别由 useAsyncState / useOpen 承担)。
1957
+ *
1958
+ * @internal 消费组件:Select、TreeSelect
1959
+ */
1960
+ function useSelectModel(options) {
1961
+ const { options: optionSource, modelValue, defaultValue, onChange, multiple = false, filterable = false, filterMethod, equals = Object.is } = options;
1962
+ const { value: model } = useControllableModel({
1963
+ modelValue,
1964
+ defaultValue,
1965
+ fallback: multiple ? [] : null,
1966
+ onChange
1967
+ });
1968
+ const keyword = ref("");
1969
+ const findOption = (value) => toValue(optionSource).find((option) => equals(option.value, value));
1970
+ /**
1971
+ * 已选快照:pick 时记下完整选项对象,独立于当前可见数据源。
1972
+ * 分页 / 服务端搜索后选项已不在当前页时,据此仍能回显已选标签;
1973
+ * 不从数据源中淘汰,仅随取消选中或清空移除。
1974
+ */
1975
+ const pickedOptions = ref([]);
1976
+ const findPicked = (value) => pickedOptions.value.find((option) => equals(option.value, value));
1977
+ const remember = (option) => {
1978
+ if (findPicked(option.value)) return;
1979
+ pickedOptions.value = [...pickedOptions.value, option];
1980
+ };
1981
+ const forget = (value) => {
1982
+ pickedOptions.value = pickedOptions.value.filter((option) => !equals(option.value, value));
1983
+ };
1984
+ const selectedOptions = computed(() => {
1985
+ const current = model.value;
1986
+ return (Array.isArray(current) ? current : current === null ? [] : [current]).map((value) => findPicked(value) ?? findOption(value)).filter((option) => !!option);
1987
+ });
1988
+ const selectedLabel = computed(() => selectedOptions.value[0]?.label);
1989
+ const defaultFilter = (option, word) => String(option.label).toLowerCase().includes(word.trim().toLowerCase());
1990
+ const filteredOptions = computed(() => {
1991
+ const all = toValue(optionSource);
1992
+ if (!filterable || !keyword.value) return all;
1993
+ const matcher = filterMethod ?? defaultFilter;
1994
+ return all.filter((option) => matcher(option, keyword.value));
1995
+ });
1996
+ const isSelected = (option) => {
1997
+ const current = model.value;
1998
+ if (Array.isArray(current)) return current.some((value) => equals(value, option.value));
1999
+ return current !== null && equals(current, option.value);
2000
+ };
2001
+ const selectOption = (option, checked) => {
2002
+ if (option.disabled) return false;
2003
+ if (multiple) {
2004
+ const set = new Set(Array.isArray(model.value) ? model.value : []);
2005
+ if (checked ?? !set.has(option.value)) {
2006
+ set.add(option.value);
2007
+ remember(option);
2008
+ } else {
2009
+ set.delete(option.value);
2010
+ forget(option.value);
2011
+ }
2012
+ model.value = Array.from(set);
2013
+ } else {
2014
+ if (checked === false) return false;
2015
+ if (model.value !== null && equals(model.value, option.value)) return false;
2016
+ model.value = option.value;
2017
+ remember(option);
2018
+ }
2019
+ keyword.value = "";
2020
+ return true;
2021
+ };
2022
+ const removeValue = (value) => {
2023
+ if (!multiple || !Array.isArray(model.value)) return;
2024
+ model.value = model.value.filter((item) => !equals(item, value));
2025
+ forget(value);
2026
+ };
2027
+ const clear = () => {
2028
+ model.value = multiple ? [] : null;
2029
+ pickedOptions.value = [];
2030
+ keyword.value = "";
2031
+ };
2032
+ const setKeyword = (next) => {
2033
+ keyword.value = next;
2034
+ };
2035
+ return {
2036
+ model,
2037
+ selectedOptions,
2038
+ selectedLabel,
2039
+ keyword,
2040
+ filteredOptions,
2041
+ isSelected,
2042
+ selectOption,
2043
+ removeValue,
2044
+ clear,
2045
+ setKeyword
2046
+ };
2047
+ }
2048
+ //#endregion
2049
+ //#region src/form/useTreeModel.ts
2050
+ const DEFAULT_FIELDS = {
2051
+ value: "value",
2052
+ label: "label",
2053
+ children: "children",
2054
+ disabled: "disabled",
2055
+ isLeaf: "isLeaf"
2056
+ };
2057
+ /**
2058
+ * 树形数据模型:展开集合、勾选集合(级联传播与半选推导)、
2059
+ * 可见节点扁平化。不渲染任何缩进 / 连接线(由组件内联)。
2060
+ *
2061
+ * @internal 消费组件:Tree、TreeSelect
2062
+ */
2063
+ function useTreeModel(options) {
2064
+ const { data, fieldNames: fieldOverrides, modelValue, defaultValue, onChange, expandedKeys: expandedProp, defaultExpandedKeys, onExpandedChange, cascade = true } = options;
2065
+ const fields = {
2066
+ ...DEFAULT_FIELDS,
2067
+ ...fieldOverrides
2068
+ };
2069
+ const flatNodes = computed(() => {
2070
+ const result = [];
2071
+ const walk = (list, depth, parentKey) => {
2072
+ for (const raw of list) {
2073
+ const key = raw[fields.value];
2074
+ const childList = raw[fields.children] ?? [];
2075
+ const childKeys = childList.map((child) => child[fields.value]);
2076
+ const leafKeys = [];
2077
+ if (childList.length === 0) leafKeys.push(key);
2078
+ else {
2079
+ const collect = (children) => {
2080
+ for (const child of children) {
2081
+ const grandchildren = child[fields.children] ?? [];
2082
+ if (grandchildren.length === 0) leafKeys.push(child[fields.value]);
2083
+ else collect(grandchildren);
2084
+ }
2085
+ };
2086
+ collect(childList);
2087
+ }
2088
+ const leafFlag = raw[fields.isLeaf];
2089
+ const isLeaf = leafFlag === void 0 ? childList.length === 0 : leafFlag === true;
2090
+ result.push({
2091
+ key,
2092
+ label: String(raw[fields.label] ?? ""),
2093
+ depth,
2094
+ disabled: Boolean(raw[fields.disabled]),
2095
+ isLeaf,
2096
+ parentKey,
2097
+ childKeys,
2098
+ leafKeys,
2099
+ raw
2100
+ });
2101
+ walk(childList, depth + 1, key);
2102
+ }
2103
+ };
2104
+ walk(toValue(data), 0, null);
2105
+ return result;
2106
+ });
2107
+ const getNode = (key) => flatNodes.value.find((node) => node.key === key);
2108
+ const expandedKeys = useControllableModel({
2109
+ modelValue: expandedProp,
2110
+ defaultValue: defaultExpandedKeys,
2111
+ fallback: [],
2112
+ onChange: onExpandedChange
2113
+ }).value;
2114
+ const isExpanded = (key) => expandedKeys.value.includes(key);
2115
+ const setExpanded = (key, expanded) => {
2116
+ const set = new Set(expandedKeys.value);
2117
+ if (expanded) set.add(key);
2118
+ else set.delete(key);
2119
+ expandedKeys.value = Array.from(set);
2120
+ };
2121
+ const toggleExpand = (key) => setExpanded(key, !isExpanded(key));
2122
+ const expandAll = () => {
2123
+ expandedKeys.value = flatNodes.value.filter((node) => !node.isLeaf).map((node) => node.key);
2124
+ };
2125
+ const collapseAll = () => {
2126
+ expandedKeys.value = [];
2127
+ };
2128
+ const visibleNodes = computed(() => {
2129
+ const all = flatNodes.value;
2130
+ if (all.length === 0) return [];
2131
+ const byParent = /* @__PURE__ */ new Map();
2132
+ for (const node of all) {
2133
+ const list = byParent.get(node.parentKey) ?? [];
2134
+ list.push(node);
2135
+ byParent.set(node.parentKey, list);
2136
+ }
2137
+ const visible = [];
2138
+ const pushLevel = (parentKey) => {
2139
+ for (const node of byParent.get(parentKey) ?? []) {
2140
+ visible.push(node);
2141
+ if (!node.isLeaf && isExpanded(node.key)) pushLevel(node.key);
2142
+ }
2143
+ };
2144
+ pushLevel(null);
2145
+ return visible;
2146
+ });
2147
+ const checkedKeys = useControllableModel({
2148
+ modelValue,
2149
+ defaultValue,
2150
+ fallback: [],
2151
+ onChange
2152
+ }).value;
2153
+ const checkedSet = computed(() => new Set(checkedKeys.value));
2154
+ const isChecked = (key) => {
2155
+ const node = getNode(key);
2156
+ if (!node) return false;
2157
+ if (!cascade) return checkedSet.value.has(key);
2158
+ return node.leafKeys.every((leaf) => checkedSet.value.has(leaf));
2159
+ };
2160
+ const isIndeterminate = (key) => {
2161
+ if (!cascade) return false;
2162
+ const node = getNode(key);
2163
+ if (!node || node.isLeaf) return false;
2164
+ const count = node.leafKeys.filter((leaf) => checkedSet.value.has(leaf)).length;
2165
+ return count > 0 && count < node.leafKeys.length;
2166
+ };
2167
+ const halfCheckedKeys = computed(() => flatNodes.value.filter((node) => !node.isLeaf && isIndeterminate(node.key)).map((node) => node.key));
2168
+ const check = (key, checked) => {
2169
+ const node = getNode(key);
2170
+ if (!node || node.disabled) return;
2171
+ const nextChecked = checked ?? !isChecked(key);
2172
+ const affected = (cascade ? node.leafKeys : [key]).filter((k) => !getNode(k)?.disabled);
2173
+ const set = new Set(checkedKeys.value);
2174
+ if (nextChecked) affected.forEach((leaf) => set.add(leaf));
2175
+ else affected.forEach((leaf) => set.delete(leaf));
2176
+ checkedKeys.value = Array.from(set);
2177
+ };
2178
+ const setCheckedKeys = (keys) => {
2179
+ checkedKeys.value = [...keys];
2180
+ };
2181
+ return {
2182
+ flatNodes,
2183
+ visibleNodes,
2184
+ checkedKeys,
2185
+ expandedKeys,
2186
+ getNode,
2187
+ isExpanded,
2188
+ setExpanded,
2189
+ toggleExpand,
2190
+ expandAll,
2191
+ collapseAll,
2192
+ isChecked,
2193
+ isIndeterminate,
2194
+ halfCheckedKeys,
2195
+ check,
2196
+ setCheckedKeys
2197
+ };
2198
+ }
2199
+ //#endregion
2200
+ //#region src/form/useAsyncSuggest.ts
2201
+ /**
2202
+ * 输入防抖 → 调用异步建议源 → loading/空态管理。
2203
+ * 竞态请求以最后一次为准,旧响应被丢弃。
2204
+ *
2205
+ * @internal 消费组件:AutoComplete、Mention
2206
+ */
2207
+ function useAsyncSuggest(options) {
2208
+ const { fetch, debounce = 300, immediate = false, minKeywordLength = 0, onSuccess, onError } = options;
2209
+ const suggestions = ref([]);
2210
+ const status = ref("idle");
2211
+ const loading = ref(false);
2212
+ const error = ref(void 0);
2213
+ const keyword = ref("");
2214
+ let requestId = 0;
2215
+ const run = async (nextKeyword = keyword.value) => {
2216
+ const currentId = ++requestId;
2217
+ status.value = "pending";
2218
+ loading.value = true;
2219
+ error.value = void 0;
2220
+ try {
2221
+ const result = await fetch(nextKeyword);
2222
+ if (currentId !== requestId) return;
2223
+ suggestions.value = result;
2224
+ status.value = "success";
2225
+ onSuccess?.(result, nextKeyword);
2226
+ } catch (err) {
2227
+ if (currentId !== requestId) return;
2228
+ error.value = err;
2229
+ status.value = "error";
2230
+ onError?.(err);
2231
+ } finally {
2232
+ if (currentId === requestId) loading.value = false;
2233
+ }
2234
+ };
2235
+ const timer = useTimeoutFn(() => {
2236
+ if (keyword.value.length < minKeywordLength) {
2237
+ suggestions.value = [];
2238
+ status.value = "idle";
2239
+ return;
2240
+ }
2241
+ run();
2242
+ });
2243
+ const setKeyword = (next) => {
2244
+ keyword.value = next;
2245
+ if (next.length < minKeywordLength) {
2246
+ timer.stop();
2247
+ suggestions.value = [];
2248
+ status.value = "idle";
2249
+ return;
2250
+ }
2251
+ timer.start(debounce);
2252
+ };
2253
+ const clear = () => {
2254
+ timer.stop();
2255
+ keyword.value = "";
2256
+ suggestions.value = [];
2257
+ status.value = "idle";
2258
+ error.value = void 0;
2259
+ };
2260
+ if (immediate) run("");
2261
+ return {
2262
+ suggestions,
2263
+ status,
2264
+ loading,
2265
+ error,
2266
+ keyword,
2267
+ setKeyword,
2268
+ run,
2269
+ clear
2270
+ };
2271
+ }
2272
+ //#endregion
2273
+ //#region src/form/date-utils.ts
2274
+ /**
2275
+ * 日期模型共享的纯函数工具(无响应式、无副作用)。
2276
+ */
2277
+ const pad2 = (value) => String(value).padStart(2, "0");
2278
+ /** 解析字符串(yyyy-MM-dd / yyyy-MM/yyyy)或时间值为本地日期;非法返回 null */
2279
+ function parseDate(value) {
2280
+ if (value === null || value === void 0 || value === "") return null;
2281
+ if (value instanceof Date) return Number.isNaN(value.getTime()) ? null : value;
2282
+ if (typeof value === "number") {
2283
+ const date = new Date(value);
2284
+ return Number.isNaN(date.getTime()) ? null : date;
2285
+ }
2286
+ if (typeof value !== "string") return null;
2287
+ const trimmed = value.trim();
2288
+ const match = /^(\d{4})(?:-(\d{1,2}))?(?:-(\d{1,2}))?/.exec(trimmed);
2289
+ if (match) {
2290
+ const [, year, month, day] = match;
2291
+ const date = new Date(Number(year), Number(month ?? 1) - 1, Number(day ?? 1));
2292
+ return Number.isNaN(date.getTime()) ? null : date;
2293
+ }
2294
+ const fallback = new Date(trimmed);
2295
+ return Number.isNaN(fallback.getTime()) ? null : fallback;
2296
+ }
2297
+ /** 按 pattern(yyyy / MM / dd)格式化日期 */
2298
+ function formatDate(date, pattern = "yyyy-MM-dd") {
2299
+ if (!date) return "";
2300
+ return pattern.replace("yyyy", String(date.getFullYear())).replace("MM", pad2(date.getMonth() + 1)).replace("dd", pad2(date.getDate()));
2301
+ }
2302
+ function startOfDay(date) {
2303
+ return new Date(date.getFullYear(), date.getMonth(), date.getDate());
2304
+ }
2305
+ function isSameDay(a, b) {
2306
+ if (!a || !b) return false;
2307
+ return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
2308
+ }
2309
+ function isSameMonth(a, b) {
2310
+ return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth();
2311
+ }
2312
+ function addDays(date, amount) {
2313
+ const next = new Date(date);
2314
+ next.setDate(next.getDate() + amount);
2315
+ return next;
2316
+ }
2317
+ function addMonths(date, amount) {
2318
+ const next = new Date(date);
2319
+ const day = next.getDate();
2320
+ next.setDate(1);
2321
+ next.setMonth(next.getMonth() + amount);
2322
+ const maxDay = getDaysInMonth(next.getFullYear(), next.getMonth());
2323
+ next.setDate(Math.min(day, maxDay));
2324
+ return next;
2325
+ }
2326
+ function addYears(date, amount) {
2327
+ return addMonths(date, amount * 12);
2328
+ }
2329
+ function getDaysInMonth(year, month) {
2330
+ return new Date(year, month + 1, 0).getDate();
2331
+ }
2332
+ /** 某天所属周的第一天(firstDayOfWeek: 0=周日 1=周一) */
2333
+ function startOfWeek(date, firstDayOfWeek = 1) {
2334
+ const diff = (date.getDay() - firstDayOfWeek + 7) % 7;
2335
+ return addDays(startOfDay(date), -diff);
2336
+ }
2337
+ /** 比较两个日期零点的先后:a < b → -1 */
2338
+ function compareDay(a, b) {
2339
+ const za = startOfDay(a).getTime();
2340
+ const zb = startOfDay(b).getTime();
2341
+ return Math.sign(za - zb);
2342
+ }
2343
+ /** 解析 HH:mm:ss / HH:mm 时间串 */
2344
+ function parseTime(value) {
2345
+ if (!value) return null;
2346
+ const match = /^(\d{1,2}):(\d{1,2})(?::(\d{1,2}))?$/.exec(value.trim());
2347
+ if (!match) return null;
2348
+ const [, hour, minute, second] = match;
2349
+ return {
2350
+ hour: Number(hour),
2351
+ minute: Number(minute),
2352
+ second: Number(second ?? 0)
2353
+ };
2354
+ }
2355
+ function formatTime(hour, minute, second, useSeconds = true) {
2356
+ const base = `${pad2(hour)}:${pad2(minute)}`;
2357
+ return useSeconds ? `${base}:${pad2(second)}` : base;
2358
+ }
2359
+ /** 将时间串换算为当天秒数,便于范围比较 */
2360
+ function timeToSeconds(value) {
2361
+ const parsed = parseTime(value);
2362
+ if (!parsed) return null;
2363
+ return parsed.hour * 3600 + parsed.minute * 60 + parsed.second;
2364
+ }
2365
+ //#endregion
2366
+ //#region src/form/useDateGrid.ts
2367
+ const MONTH_LABELS = [
2368
+ "1月",
2369
+ "2月",
2370
+ "3月",
2371
+ "4月",
2372
+ "5月",
2373
+ "6月",
2374
+ "7月",
2375
+ "8月",
2376
+ "9月",
2377
+ "10月",
2378
+ "11月",
2379
+ "12月"
2380
+ ];
2381
+ /**
2382
+ * 日历网格纯计算:月首偏移、当月天数、外部填充、周末/禁用/选中/范围标记。
2383
+ * 同一模型驱动 DatePicker 与 Calendar;多面板由组件内创建多个实例实现。
2384
+ */
2385
+ function useDateGrid(options = {}) {
2386
+ const { viewDate: injected, type = "date", firstDayOfWeek = 1, disabledDate = () => false, isSelected = () => false, isInRange = () => false, isRangeStart = () => false, isRangeEnd = () => false } = options;
2387
+ const viewDate = injected ?? ref(/* @__PURE__ */ new Date());
2388
+ const panelType = ref(type);
2389
+ const today = /* @__PURE__ */ new Date();
2390
+ const weekdayOrder = computed(() => {
2391
+ const order = [];
2392
+ for (let i = 0; i < 7; i += 1) order.push((firstDayOfWeek + i) % 7);
2393
+ return order;
2394
+ });
2395
+ const weeks = computed(() => {
2396
+ const first = startOfWeek(new Date(viewDate.value.getFullYear(), viewDate.value.getMonth(), 1), firstDayOfWeek);
2397
+ const rows = [];
2398
+ for (let row = 0; row < 6; row += 1) {
2399
+ const week = [];
2400
+ for (let col = 0; col < 7; col += 1) {
2401
+ const date = addDays(first, row * 7 + col);
2402
+ const base = {
2403
+ date,
2404
+ inMonth: isSameMonth(date, viewDate.value),
2405
+ isToday: isSameDay(date, today),
2406
+ disabled: disabledDate(date),
2407
+ isWeekend: date.getDay() === 0 || date.getDay() === 6
2408
+ };
2409
+ week.push({
2410
+ ...base,
2411
+ selected: isSelected(date),
2412
+ inRange: isInRange(date),
2413
+ rangeStart: isRangeStart(date),
2414
+ rangeEnd: isRangeEnd(date)
2415
+ });
2416
+ }
2417
+ rows.push(week);
2418
+ }
2419
+ return rows;
2420
+ });
2421
+ const months = computed(() => MONTH_LABELS.map((label, index) => {
2422
+ const date = new Date(viewDate.value.getFullYear(), index, 1);
2423
+ return {
2424
+ date,
2425
+ label,
2426
+ disabled: disabledDate(date),
2427
+ inPanel: true
2428
+ };
2429
+ }));
2430
+ const decadeStartYear = computed(() => Math.floor(viewDate.value.getFullYear() / 10) * 10);
2431
+ const years = computed(() => {
2432
+ const start = decadeStartYear.value - 1;
2433
+ return Array.from({ length: 12 }, (_, index) => {
2434
+ const year = start + index;
2435
+ const date = new Date(year, 0, 1);
2436
+ return {
2437
+ date,
2438
+ label: String(year),
2439
+ disabled: disabledDate(date),
2440
+ inPanel: year >= decadeStartYear.value && year < decadeStartYear.value + 10
2441
+ };
2442
+ });
2443
+ });
2444
+ const panelLabel = computed(() => {
2445
+ const year = viewDate.value.getFullYear();
2446
+ if (panelType.value === "date") return `${year}年 ${MONTH_LABELS[viewDate.value.getMonth()]}`;
2447
+ if (panelType.value === "month") return `${year}年`;
2448
+ return `${decadeStartYear.value} - ${decadeStartYear.value + 9}`;
2449
+ });
2450
+ const prevView = () => {
2451
+ if (panelType.value === "date") viewDate.value = addMonths(viewDate.value, -1);
2452
+ else if (panelType.value === "month") viewDate.value = addYears(viewDate.value, -1);
2453
+ else viewDate.value = addYears(viewDate.value, -10);
2454
+ };
2455
+ const nextView = () => {
2456
+ if (panelType.value === "date") viewDate.value = addMonths(viewDate.value, 1);
2457
+ else if (panelType.value === "month") viewDate.value = addYears(viewDate.value, 1);
2458
+ else viewDate.value = addYears(viewDate.value, 10);
2459
+ };
2460
+ const goToDate = (date) => {
2461
+ viewDate.value = new Date(date);
2462
+ };
2463
+ const setPanelType = (next) => {
2464
+ panelType.value = next;
2465
+ };
2466
+ return {
2467
+ viewDate,
2468
+ panelType,
2469
+ setPanelType,
2470
+ weekdayOrder,
2471
+ weeks,
2472
+ months,
2473
+ years,
2474
+ panelLabel,
2475
+ prevView,
2476
+ nextView,
2477
+ goToDate
2478
+ };
2479
+ }
2480
+ //#endregion
2481
+ //#region src/form/useDateModel.ts
2482
+ /**
2483
+ * 日期值模型:解析/格式化、粒度(日/月/年)、范围两段式选择、
2484
+ * 最小/最大与禁用限制、快捷选项。不负责网格计算(见 useDateGrid)。
2485
+ *
2486
+ * @internal 消费组件:DatePicker、Calendar
2487
+ */
2488
+ function useDateModel(options = {}) {
2489
+ const { modelValue, defaultValue, rangeModelValue, defaultRangeValue, onChange, range = false, granularity = "day", format: formatPattern = "yyyy-MM-dd", minDate, maxDate, disabledDate } = options;
2490
+ const pattern = granularity === "year" ? "yyyy" : granularity === "month" ? "yyyy-MM" : formatPattern;
2491
+ const min = computed(() => parseDate(minDate ?? null));
2492
+ const max = computed(() => parseDate(maxDate ?? null));
2493
+ const format = (date) => formatDate(date, pattern);
2494
+ const toStored = (value) => {
2495
+ const date = parseDate(value ?? null);
2496
+ return date ? format(date) : null;
2497
+ };
2498
+ const model = useControllableModel({
2499
+ modelValue,
2500
+ defaultValue,
2501
+ fallback: null
2502
+ }).value;
2503
+ const rawRangeModel = useControllableModel({
2504
+ modelValue: rangeModelValue,
2505
+ defaultValue: defaultRangeValue,
2506
+ fallback: null
2507
+ }).value;
2508
+ const rangeModel = computed({
2509
+ get: () => {
2510
+ const current = rawRangeModel.value;
2511
+ if (!current) return null;
2512
+ return [toStored(current[0]), toStored(current[1])];
2513
+ },
2514
+ set: (next) => {
2515
+ rawRangeModel.value = next;
2516
+ onChange?.(next);
2517
+ }
2518
+ });
2519
+ const selectedDate = computed(() => parseDate(model.value ?? null));
2520
+ const rangeDates = computed(() => {
2521
+ const current = rangeModel.value;
2522
+ if (!current) return [null, null];
2523
+ return [parseDate(current[0]), parseDate(current[1])];
2524
+ });
2525
+ const viewDate = ref(selectedDate.value ?? rangeDates.value[0] ?? /* @__PURE__ */ new Date());
2526
+ const pickingStart = ref(true);
2527
+ const hoverDate = ref(null);
2528
+ const syncView = (date) => {
2529
+ viewDate.value = new Date(date);
2530
+ };
2531
+ const isDisabled = (date) => {
2532
+ if (min.value && compareDay(date, min.value) < 0) return true;
2533
+ if (max.value && compareDay(date, max.value) > 0) return true;
2534
+ return disabledDate?.(date) === true;
2535
+ };
2536
+ const isSelected = (date) => {
2537
+ if (range) {
2538
+ const [start, end] = rangeDates.value;
2539
+ return isSameDay(date, start) || isSameDay(date, end);
2540
+ }
2541
+ return isSameDay(date, selectedDate.value);
2542
+ };
2543
+ const isRangeStart = (date) => isSameDay(date, rangeDates.value[0]);
2544
+ const isRangeEnd = (date) => isSameDay(date, rangeDates.value[1]);
2545
+ const isInRange = (date) => {
2546
+ if (!range) return false;
2547
+ const [start, end] = rangeDates.value;
2548
+ if (start && end) return compareDay(date, start) >= 0 && compareDay(date, end) <= 0;
2549
+ if (start && hoverDate.value && !pickingStart.value) {
2550
+ const [lo, hi] = compareDay(start, hoverDate.value) <= 0 ? [start, hoverDate.value] : [hoverDate.value, start];
2551
+ return compareDay(date, lo) >= 0 && compareDay(date, hi) <= 0;
2552
+ }
2553
+ return false;
2554
+ };
2555
+ const emitSingle = (date) => {
2556
+ const stored = toStored(date);
2557
+ model.value = stored;
2558
+ onChange?.(stored);
2559
+ };
2560
+ const selectDate = (date) => {
2561
+ if (isDisabled(date)) return;
2562
+ syncView(date);
2563
+ if (!range) {
2564
+ emitSingle(date);
2565
+ return;
2566
+ }
2567
+ if (pickingStart.value || !rangeModel.value?.[0]) {
2568
+ rangeModel.value = [toStored(date), null];
2569
+ pickingStart.value = false;
2570
+ hoverDate.value = null;
2571
+ return;
2572
+ }
2573
+ const start = rangeDates.value[0];
2574
+ if (start && compareDay(date, start) < 0) rangeModel.value = [toStored(date), toStored(start)];
2575
+ else rangeModel.value = [toStored(start), toStored(date)];
2576
+ pickingStart.value = true;
2577
+ hoverDate.value = null;
2578
+ };
2579
+ const setHoverDate = (date) => {
2580
+ hoverDate.value = date;
2581
+ };
2582
+ const selectShortcut = (shortcut) => {
2583
+ if (Array.isArray(shortcut.value)) {
2584
+ const [start, end] = shortcut.value;
2585
+ rangeModel.value = [toStored(start), toStored(end)];
2586
+ pickingStart.value = true;
2587
+ syncView(start);
2588
+ } else {
2589
+ emitSingle(shortcut.value);
2590
+ syncView(shortcut.value);
2591
+ }
2592
+ };
2593
+ const clear = () => {
2594
+ if (range) {
2595
+ rangeModel.value = null;
2596
+ pickingStart.value = true;
2597
+ } else emitSingle(null);
2598
+ hoverDate.value = null;
2599
+ };
2600
+ const prevMonth = () => {
2601
+ viewDate.value = addMonths(viewDate.value, -1);
2602
+ };
2603
+ const nextMonth = () => {
2604
+ viewDate.value = addMonths(viewDate.value, 1);
2605
+ };
2606
+ const goToDate = (date) => syncView(date);
2607
+ return {
2608
+ model,
2609
+ rangeModel,
2610
+ selectedDate,
2611
+ rangeDates,
2612
+ viewDate,
2613
+ hoverDate,
2614
+ displayText: computed(() => {
2615
+ if (range) {
2616
+ const [start, end] = rangeDates.value;
2617
+ const left = format(start);
2618
+ const right = format(end);
2619
+ if (!left && !right) return "";
2620
+ return `${left} ~ ${right}`;
2621
+ }
2622
+ return format(selectedDate.value);
2623
+ }),
2624
+ isSelected,
2625
+ isDisabled,
2626
+ isInRange,
2627
+ isRangeStart,
2628
+ isRangeEnd,
2629
+ selectDate,
2630
+ setHoverDate,
2631
+ selectShortcut,
2632
+ clear,
2633
+ prevMonth,
2634
+ nextMonth,
2635
+ goToDate,
2636
+ format
2637
+ };
2638
+ }
2639
+ //#endregion
2640
+ //#region src/form/useTimeModel.ts
2641
+ function buildStepValues(max, step) {
2642
+ const values = [];
2643
+ for (let value = 0; value <= max; value += 1) if (value % step === 0) values.push(value);
2644
+ return values;
2645
+ }
2646
+ /**
2647
+ * 时间选择模型:时/分/秒三列数据与步进、范围限制、回写与回显。
2648
+ * 滚动列渲染与确认按钮编排由 TimePicker 组件承担。
2649
+ *
2650
+ * @internal 消费组件:TimePicker、DatePicker(日期时间模式)
2651
+ */
2652
+ function useTimeModel(options = {}) {
2653
+ const { modelValue, defaultValue, onChange, hourStep = 1, minuteStep = 1, secondStep = 1, useSeconds = true, minTime, maxTime } = options;
2654
+ const { value: model } = useControllableModel({
2655
+ modelValue,
2656
+ defaultValue,
2657
+ fallback: null
2658
+ });
2659
+ const typedModel = model;
2660
+ const hour = ref(parseTime(typedModel.value)?.hour ?? null);
2661
+ const minute = ref(parseTime(typedModel.value)?.minute ?? null);
2662
+ const second = ref(useSeconds ? parseTime(typedModel.value)?.second ?? null : 0);
2663
+ const minSeconds = computed(() => timeToSeconds(minTime ?? null));
2664
+ const maxSeconds = computed(() => timeToSeconds(maxTime ?? null));
2665
+ const isItemDisabled = (unit, value) => {
2666
+ const h = hour.value ?? 0;
2667
+ const m = minute.value ?? 0;
2668
+ const s = second.value ?? 0;
2669
+ const candidate = unit === "hour" ? value * 3600 + m * 60 + s : unit === "minute" ? h * 3600 + value * 60 + s : h * 3600 + m * 60 + value;
2670
+ if (minSeconds.value !== null && candidate < minSeconds.value) return true;
2671
+ if (maxSeconds.value !== null && candidate > maxSeconds.value) return true;
2672
+ return false;
2673
+ };
2674
+ const hourColumn = computed(() => buildStepValues(23, hourStep).map((value) => ({
2675
+ value,
2676
+ label: pad2(value),
2677
+ disabled: isItemDisabled("hour", value)
2678
+ })));
2679
+ const minuteColumn = computed(() => buildStepValues(59, minuteStep).map((value) => ({
2680
+ value,
2681
+ label: pad2(value),
2682
+ disabled: isItemDisabled("minute", value)
2683
+ })));
2684
+ const secondColumn = computed(() => buildStepValues(59, secondStep).map((value) => ({
2685
+ value,
2686
+ label: pad2(value),
2687
+ disabled: isItemDisabled("second", value)
2688
+ })));
2689
+ const displayText = computed(() => {
2690
+ if (hour.value === null || minute.value === null) return "";
2691
+ return formatTime(hour.value, minute.value, second.value ?? 0, useSeconds);
2692
+ });
2693
+ const setHour = (value) => {
2694
+ hour.value = value;
2695
+ };
2696
+ const setMinute = (value) => {
2697
+ minute.value = value;
2698
+ };
2699
+ const setSecond = (value) => {
2700
+ second.value = value;
2701
+ };
2702
+ const setUnit = (unit, value) => {
2703
+ if (isItemDisabled(unit, value)) return;
2704
+ if (unit === "hour") setHour(value);
2705
+ else if (unit === "minute") setMinute(value);
2706
+ else setSecond(value);
2707
+ };
2708
+ const confirm = () => {
2709
+ if (hour.value === null || minute.value === null) {
2710
+ typedModel.value = null;
2711
+ return null;
2712
+ }
2713
+ const text = formatTime(hour.value, minute.value, second.value ?? 0, useSeconds);
2714
+ typedModel.value = text;
2715
+ onChange?.(text);
2716
+ return text;
2717
+ };
2718
+ const now = () => {
2719
+ const date = /* @__PURE__ */ new Date();
2720
+ hour.value = date.getHours();
2721
+ minute.value = date.getMinutes();
2722
+ second.value = date.getSeconds();
2723
+ confirm();
2724
+ };
2725
+ const clear = () => {
2726
+ hour.value = null;
2727
+ minute.value = null;
2728
+ second.value = useSeconds ? null : 0;
2729
+ typedModel.value = null;
2730
+ onChange?.(null);
2731
+ };
2732
+ const syncFromModel = (value) => {
2733
+ const parsed = parseTime(value);
2734
+ hour.value = parsed?.hour ?? null;
2735
+ minute.value = parsed?.minute ?? null;
2736
+ second.value = parsed ? parsed.second : useSeconds ? null : 0;
2737
+ };
2738
+ if (modelValue !== void 0) syncFromModel(modelValue);
2739
+ return {
2740
+ model: typedModel,
2741
+ hour,
2742
+ minute,
2743
+ second,
2744
+ hourColumn,
2745
+ minuteColumn,
2746
+ secondColumn,
2747
+ displayText,
2748
+ setHour,
2749
+ setMinute,
2750
+ setSecond,
2751
+ setUnit,
2752
+ now,
2753
+ clear,
2754
+ confirm
2755
+ };
2756
+ }
2757
+ //#endregion
2758
+ //#region src/overlay/useModal.ts
2759
+ /**
2760
+ * 模态开合编排:useOpen + 滚动锁定 + 焦点陷阱/归还 + ESC + 层级 +
2761
+ * 进出场过渡。Dialog/Drawer 共用,差异仅在方向与尺寸(组件内联)。
2762
+ */
2763
+ function useModal(options = {}) {
2764
+ const { maskClosable = true, escClosable = true, lockScroll: shouldLock = true, onOpen, onClose, onChange, ...controlled } = options;
2765
+ const { isOpen, open, close, toggle } = useOpen({
2766
+ ...controlled,
2767
+ onChange: (next) => {
2768
+ onChange?.(next);
2769
+ if (next) onOpen?.();
2770
+ else onClose?.();
2771
+ }
2772
+ });
2773
+ const { rendered, visible, onAfterLeave } = useTransitionVisible(isOpen);
2774
+ const { lock, unlock } = useScrollLock();
2775
+ const { currentZIndex, nextZIndex } = useZIndex();
2776
+ const { record: recordFocus, restore: restoreFocus } = useFocusReturn();
2777
+ const contentRef = ref(null);
2778
+ const { trap, release } = useFocusTrap(contentRef, { immediate: true });
2779
+ watch(isOpen, (openState) => {
2780
+ if (openState) {
2781
+ currentZIndex.value = nextZIndex();
2782
+ recordFocus();
2783
+ if (shouldLock) lock();
2784
+ trap();
2785
+ } else {
2786
+ release();
2787
+ if (shouldLock) unlock();
2788
+ restoreFocus();
2789
+ }
2790
+ });
2791
+ useDismiss({
2792
+ isOpen,
2793
+ floating: contentRef,
2794
+ onDismiss: (reason) => {
2795
+ if (reason === "escape-key" && escClosable) close();
2796
+ },
2797
+ outsidePress: false,
2798
+ scroll: false
2799
+ });
2800
+ const onMaskClick = () => {
2801
+ if (maskClosable) close();
2802
+ };
2803
+ return {
2804
+ isOpen,
2805
+ open,
2806
+ close,
2807
+ toggle,
2808
+ contentRef,
2809
+ rendered,
2810
+ visible,
2811
+ onAfterLeave,
2812
+ zIndex: currentZIndex,
2813
+ onMaskClick
2814
+ };
2815
+ }
2816
+ //#endregion
2817
+ //#region src/overlay/usePopoverModel.ts
2818
+ /**
2819
+ * 浮层触发模型:click / hover / focus / manual 四种触发方式与
2820
+ * 延迟开合、进出场过渡的组合编排。定位由 useFloating 独立承担。
2821
+ *
2822
+ * @internal 消费组件:Popover、Dropdown、Tooltip
2823
+ */
2824
+ function usePopoverModel(options = {}) {
2825
+ const { trigger = "click", openDelay = 100, closeDelay = 100, ...controlled } = options;
2826
+ const hover = useHoverIntent({
2827
+ showDelay: openDelay,
2828
+ hideDelay: closeDelay,
2829
+ disabled: trigger !== "hover"
2830
+ });
2831
+ const { isOpen, open, close, toggle } = useOpen({
2832
+ ...controlled,
2833
+ openDelay: 0,
2834
+ closeDelay: 0
2835
+ });
2836
+ if (trigger === "hover") watch(hover.isHovered, (hovered) => toggle(hovered));
2837
+ const { rendered, visible, onAfterLeave } = useTransitionVisible(isOpen);
2838
+ return {
2839
+ isOpen,
2840
+ open,
2841
+ close,
2842
+ toggle,
2843
+ rendered,
2844
+ visible,
2845
+ onAfterLeave,
2846
+ triggerProps: computed(() => {
2847
+ const props = {};
2848
+ if (trigger === "click") props.onClick = () => toggle();
2849
+ else if (trigger === "hover") {
2850
+ props.onMouseenter = hover.onMouseEnter;
2851
+ props.onMouseleave = hover.onMouseLeave;
2852
+ } else if (trigger === "focus") {
2853
+ props.onFocus = open;
2854
+ props.onBlur = close;
2855
+ }
2856
+ return props;
2857
+ }),
2858
+ floatingHoverProps: computed(() => trigger === "hover" ? {
2859
+ onMouseenter: hover.onMouseEnter,
2860
+ onMouseleave: hover.onMouseLeave
2861
+ } : {})
2862
+ };
2863
+ }
2864
+ //#endregion
2865
+ //#region src/overlay/createMessage.ts
2866
+ /**
2867
+ * 创建一套命令式消息服务。底层为共享响应式 toast 队列,
2868
+ * 渲染由 Message 组件承担;宿主也可在不引入组件的情况下调用。
2869
+ */
2870
+ function createMessage(options = {}) {
2871
+ const { duration = 3e3, max = 5 } = options;
2872
+ const queue = useToastQueue({
2873
+ duration,
2874
+ max
2875
+ });
2876
+ const push = (content, pushOptions = {}) => {
2877
+ return queue.push(pushOptions.type ?? "info", content, pushOptions).id;
2878
+ };
2879
+ return {
2880
+ items: queue.items,
2881
+ push,
2882
+ success: (content, time) => push(content, {
2883
+ type: "success",
2884
+ duration: time
2885
+ }),
2886
+ warning: (content, time) => push(content, {
2887
+ type: "warning",
2888
+ duration: time
2889
+ }),
2890
+ info: (content, time) => push(content, {
2891
+ type: "info",
2892
+ duration: time
2893
+ }),
2894
+ error: (content, time) => push(content, {
2895
+ type: "error",
2896
+ duration: time ?? 5e3
2897
+ }),
2898
+ loading: (content) => push(content, {
2899
+ type: "loading",
2900
+ duration: 0
2901
+ }),
2902
+ remove: queue.remove,
2903
+ clear: queue.clear,
2904
+ pause: queue.pause,
2905
+ resume: queue.resume
2906
+ };
2907
+ }
2908
+ /** 全局默认单例:所有 Message 组件与宿主共享同一队列 */
2909
+ const message = createMessage();
2910
+ //#endregion
2911
+ //#region src/overlay/createNotification.ts
2912
+ /**
2913
+ * 创建命令式通知服务。消息体携带标题 / 描述 / 动作数据,
2914
+ * 角落堆叠方向由渲染组件按 placement 分组。
2915
+ */
2916
+ function createNotification(options = {}) {
2917
+ const { duration = 4500, max = 7 } = options;
2918
+ const queue = useToastQueue({
2919
+ duration,
2920
+ max
2921
+ });
2922
+ const open = (type, title, description, time) => {
2923
+ return queue.push(type, title, {
2924
+ title,
2925
+ duration: time,
2926
+ meta: {
2927
+ title,
2928
+ description
2929
+ }
2930
+ }).id;
2931
+ };
2932
+ return {
2933
+ items: computed(() => queue.items.value.map((item) => ({
2934
+ id: item.id,
2935
+ type: item.type,
2936
+ title: item.meta.title,
2937
+ description: item.meta.description,
2938
+ duration: item.duration,
2939
+ meta: item.meta
2940
+ }))),
2941
+ open,
2942
+ success: (title, description) => open("success", title, description),
2943
+ warning: (title, description) => open("warning", title, description),
2944
+ info: (title, description) => open("info", title, description),
2945
+ error: (title, description) => open("error", title, description, 5e3),
2946
+ remove: queue.remove,
2947
+ clear: queue.clear
2948
+ };
2949
+ }
2950
+ /** 全局默认单例 */
2951
+ const notification = createNotification();
2952
+ //#endregion
2953
+ //#region src/overlay/useLoadingService.ts
2954
+ /** 创建一套隔离的计数式加载服务 */
2955
+ function createLoadingService() {
2956
+ const count = ref(0);
2957
+ const loading = computed(() => count.value > 0);
2958
+ const finish = () => {
2959
+ count.value = Math.max(0, count.value - 1);
2960
+ };
2961
+ const start = () => {
2962
+ count.value += 1;
2963
+ return finish;
2964
+ };
2965
+ const wrap = async (task) => {
2966
+ count.value += 1;
2967
+ try {
2968
+ return await task;
2969
+ } finally {
2970
+ finish();
2971
+ }
2972
+ };
2973
+ const clear = () => {
2974
+ count.value = 0;
2975
+ };
2976
+ return {
2977
+ loading,
2978
+ count,
2979
+ start,
2980
+ finish,
2981
+ wrap,
2982
+ clear
2983
+ };
2984
+ }
2985
+ const globalService = createLoadingService();
2986
+ /**
2987
+ * 局部 / 全屏加载的计数式服务(多请求合并显示一个加载态)。
2988
+ * 需要隔离实例时使用 createLoadingService。
2989
+ *
2990
+ * @internal 消费组件:Loading
2991
+ */
2992
+ function useLoadingService() {
2993
+ return globalService;
2994
+ }
2995
+ //#endregion
2996
+ //#region src/data/usePagination.ts
2997
+ const range = (start, end) => Array.from({ length: end - start + 1 }, (_, index) => start + index);
2998
+ /**
2999
+ * 分页模型:当前页 / 页大小、总页数、含省略号的页码序列、跳页与边界约束。
3000
+ * 组件自身不发请求,go/setPageSize 只负责状态与 onChange 通知。
3001
+ *
3002
+ * @internal 消费组件:Pagination;Table 内联复用;宿主列表可独立使用
3003
+ */
3004
+ function usePagination(options = {}) {
3005
+ const { currentPage, defaultCurrentPage = 1, onPageChange, pageSize: pageSizeProp, defaultPageSize = 10, onPageSizeChange, total: totalProp = 0, pageSizes = [
3006
+ 10,
3007
+ 20,
3008
+ 50,
3009
+ 100
3010
+ ], siblingCount = 1, boundaryCount = 1, onChange } = options;
3011
+ const total = ref(totalProp);
3012
+ const currentPageRef = useControllableModel({
3013
+ modelValue: currentPage,
3014
+ defaultValue: defaultCurrentPage,
3015
+ fallback: 1,
3016
+ onChange: (page) => {
3017
+ onPageChange?.(page);
3018
+ onChange?.({
3019
+ currentPage: page,
3020
+ pageSize: sizeModel.value.value
3021
+ });
3022
+ }
3023
+ }).value;
3024
+ const sizeModel = useControllableModel({
3025
+ modelValue: pageSizeProp,
3026
+ defaultValue: defaultPageSize,
3027
+ fallback: 10,
3028
+ onChange: (size) => {
3029
+ onPageSizeChange?.(size);
3030
+ onChange?.({
3031
+ currentPage: currentPageRef.value,
3032
+ pageSize: size
3033
+ });
3034
+ }
3035
+ });
3036
+ const pageSize = sizeModel.value;
3037
+ const totalPages = computed(() => Math.max(1, Math.ceil(total.value / pageSize.value)));
3038
+ watch(totalPages, (lastPage) => {
3039
+ if (currentPageRef.value > lastPage) currentPageRef.value = lastPage;
3040
+ });
3041
+ const pages = computed(() => {
3042
+ const lastPage = totalPages.value;
3043
+ if (lastPage <= boundaryCount * 2 + siblingCount * 2 + 3) return range(1, lastPage);
3044
+ const current = currentPageRef.value;
3045
+ const groups = [
3046
+ [
3047
+ 1,
3048
+ boundaryCount,
3049
+ "start-ellipsis"
3050
+ ],
3051
+ [
3052
+ Math.max(boundaryCount + 1, current - siblingCount),
3053
+ Math.min(lastPage - boundaryCount, current + siblingCount),
3054
+ "start-ellipsis"
3055
+ ],
3056
+ [
3057
+ lastPage - boundaryCount + 1,
3058
+ lastPage,
3059
+ "end-ellipsis"
3060
+ ]
3061
+ ];
3062
+ const result = [];
3063
+ let prevEnd = 0;
3064
+ groups.forEach(([start, end, token], groupIndex) => {
3065
+ if (start > end) return;
3066
+ if (prevEnd > 0) {
3067
+ const gap = start - prevEnd;
3068
+ if (gap === 1) {} else if (gap === 2) result.push(prevEnd + 1);
3069
+ else result.push(groupIndex === groups.length - 1 ? "end-ellipsis" : token);
3070
+ }
3071
+ result.push(...range(start, end));
3072
+ prevEnd = end;
3073
+ });
3074
+ return result;
3075
+ });
3076
+ const isFirst = computed(() => currentPageRef.value <= 1);
3077
+ const isLast = computed(() => currentPageRef.value >= totalPages.value);
3078
+ const canPrev = computed(() => !isFirst.value);
3079
+ const canNext = computed(() => !isLast.value);
3080
+ const rangeStart = computed(() => total.value === 0 ? 0 : (currentPageRef.value - 1) * pageSize.value + 1);
3081
+ const rangeEnd = computed(() => Math.min(total.value, currentPageRef.value * pageSize.value));
3082
+ const go = (page) => {
3083
+ const target = Math.min(Math.max(1, Math.trunc(page)), totalPages.value);
3084
+ if (target === currentPageRef.value) return;
3085
+ currentPageRef.value = target;
3086
+ };
3087
+ const prev = () => go(currentPageRef.value - 1);
3088
+ const next = () => go(currentPageRef.value + 1);
3089
+ const setPageSize = (size) => {
3090
+ if (size === pageSize.value) return;
3091
+ pageSize.value = size;
3092
+ go(1);
3093
+ };
3094
+ return {
3095
+ currentPage: currentPageRef,
3096
+ pageSize,
3097
+ total,
3098
+ totalPages,
3099
+ pageSizes,
3100
+ pages,
3101
+ isFirst,
3102
+ isLast,
3103
+ canPrev,
3104
+ canNext,
3105
+ rangeStart,
3106
+ rangeEnd,
3107
+ go,
3108
+ prev,
3109
+ next,
3110
+ setPageSize
3111
+ };
3112
+ }
3113
+ //#endregion
3114
+ //#region src/data/fetch.ts
3115
+ /** 判断是否为分页结果形态 */
3116
+ function isPagedResult(value) {
3117
+ return !!value && typeof value === "object" && Array.isArray(value.list) && typeof value.total === "number";
3118
+ }
3119
+ /** 归一化:数组 → 列表;{ list, total } → 分页;其他 → 单值 */
3120
+ function normalizeFetchResult(raw) {
3121
+ if (Array.isArray(raw)) return {
3122
+ data: raw,
3123
+ total: raw.length,
3124
+ raw
3125
+ };
3126
+ if (isPagedResult(raw)) return {
3127
+ data: raw.list,
3128
+ total: raw.total,
3129
+ raw
3130
+ };
3131
+ return {
3132
+ data: raw,
3133
+ raw
3134
+ };
3135
+ }
3136
+ /**
3137
+ * 创建资源请求控制器。纯工厂、无生命周期副作用,可在组件、组合函数或测试中使用。
3138
+ * 不主动发起请求,首次调用由消费方(组件资源钩子或宿主)触发。
3139
+ */
3140
+ function createFetchController(fetcher) {
3141
+ const state = ref({
3142
+ status: "pending",
3143
+ data: void 0
3144
+ });
3145
+ const loading = ref(false);
3146
+ const error = ref(void 0);
3147
+ let requestId = 0;
3148
+ let lastQuery = {};
3149
+ let abortController;
3150
+ const run = async (query) => {
3151
+ const currentId = ++requestId;
3152
+ lastQuery = query;
3153
+ abortController?.abort();
3154
+ abortController = typeof AbortController !== "undefined" ? new AbortController() : void 0;
3155
+ const hadSuccess = state.value.status === "success";
3156
+ state.value = {
3157
+ status: "pending",
3158
+ data: hadSuccess ? state.value.data : void 0,
3159
+ isStale: hadSuccess
3160
+ };
3161
+ loading.value = true;
3162
+ error.value = void 0;
3163
+ try {
3164
+ const raw = await fetcher({
3165
+ ...query,
3166
+ signal: abortController?.signal
3167
+ });
3168
+ if (currentId !== requestId) return void 0;
3169
+ const normalized = normalizeFetchResult(raw);
3170
+ state.value = {
3171
+ status: "success",
3172
+ data: normalized,
3173
+ isStale: false
3174
+ };
3175
+ error.value = void 0;
3176
+ return normalized;
3177
+ } catch (err) {
3178
+ if (currentId !== requestId) return void 0;
3179
+ state.value = {
3180
+ status: "error",
3181
+ data: state.value.data,
3182
+ error: err,
3183
+ isStale: false
3184
+ };
3185
+ error.value = err;
3186
+ return;
3187
+ } finally {
3188
+ if (currentId === requestId) loading.value = false;
3189
+ }
3190
+ };
3191
+ const refresh = () => run(lastQuery);
3192
+ const abort = () => {
3193
+ requestId++;
3194
+ abortController?.abort();
3195
+ };
3196
+ return {
3197
+ run,
3198
+ refresh,
3199
+ abort,
3200
+ state,
3201
+ loading,
3202
+ error
3203
+ };
3204
+ }
3205
+ /** useFetch 返回对象的品牌标记(内部识别用) */
3206
+ const FETCH_RESOURCE = Symbol("fetch-resource");
3207
+ /**
3208
+ * 宿主侧资源封装:把一个 Fetch 方法升级为带响应式状态的对象。
3209
+ * 不绑定组件时不会自动请求;组件接收其 `fetch` 后由组件机制驱动,
3210
+ * 状态在组件内外共享同一份。
3211
+ */
3212
+ function useFetch(fetcher) {
3213
+ const controller = createFetchController(fetcher);
3214
+ const total = ref(void 0);
3215
+ const state = computed(() => {
3216
+ const current = controller.state.value;
3217
+ return {
3218
+ status: current.status,
3219
+ data: current.data?.data,
3220
+ error: current.error,
3221
+ isStale: current.isStale,
3222
+ retry: () => void controller.refresh()
3223
+ };
3224
+ });
3225
+ watch(() => controller.state.value, (current) => {
3226
+ if (current.status === "success" && current.data) total.value = current.data.total;
3227
+ });
3228
+ const invoke = async (query = {}) => {
3229
+ return (await controller.run(query))?.raw;
3230
+ };
3231
+ return {
3232
+ fetch: invoke,
3233
+ state,
3234
+ status: computed(() => controller.state.value.status),
3235
+ data: computed(() => controller.state.value.data?.data),
3236
+ error: controller.error,
3237
+ loading: computed(() => controller.loading.value),
3238
+ isStale: computed(() => controller.state.value.isStale === true),
3239
+ total,
3240
+ refresh: (query) => query ? invoke(query) : controller.refresh().then((r) => r?.raw),
3241
+ [FETCH_RESOURCE]: controller
3242
+ };
3243
+ }
3244
+ /** 判断是否为 useFetch 返回对象 */
3245
+ function isUseFetchReturn(value) {
3246
+ return !!value && typeof value === "object" && FETCH_RESOURCE in value;
3247
+ }
3248
+ /**
3249
+ * 组件内部统一资源机制:
3250
+ * 挂载即请求;getQuery 的 deep 变化自动重取;防抖、竞态后到为准、stale 保留旧数据。
3251
+ * 所有 fetch 型组件共用此钩子,开发者只绑定 fetch 方法即可。
3252
+ */
3253
+ function useFetchResource(source, options) {
3254
+ const { getQuery, initialData, mapData, debounce = 0, merge } = options;
3255
+ const hasSource = ref(false);
3256
+ const total = ref(0);
3257
+ const lastData = ref(initialData);
3258
+ const controllerRef = shallowRef(void 0);
3259
+ let internalController;
3260
+ let timer;
3261
+ const map = (normalized) => {
3262
+ if (!normalized) return initialData;
3263
+ return mapData ? mapData(normalized) : normalized.data;
3264
+ };
3265
+ /** 最近一次命令式查询覆盖(懒加载等),retry 时保留 */
3266
+ let lastOverride;
3267
+ const execute = async (override) => {
3268
+ const controller = controllerRef.value;
3269
+ if (!controller) return;
3270
+ if (override !== void 0) lastOverride = override;
3271
+ const query = {
3272
+ ...getQuery(),
3273
+ ...lastOverride
3274
+ };
3275
+ if (merge && query.pagination && query.pagination.currentPage <= 1) lastData.value = initialData;
3276
+ const result = await controller.run(query);
3277
+ if (!result || !hasSource.value) return;
3278
+ const next = map(result);
3279
+ lastData.value = merge ? merge(lastData.value, next, query) : next;
3280
+ if (result.total !== void 0) total.value = result.total;
3281
+ };
3282
+ const state = computed(() => {
3283
+ const current = controllerRef.value?.state.value;
3284
+ if (!current || !controllerRef.value) return void 0;
3285
+ return {
3286
+ status: current.status,
3287
+ data: current.status === "success" ? lastData.value : map(current.data),
3288
+ error: current.error,
3289
+ isStale: current.isStale,
3290
+ retry: () => void execute()
3291
+ };
3292
+ });
3293
+ const schedule = (immediateRun, override) => {
3294
+ if (!controllerRef.value) return;
3295
+ if (timer) clearTimeout(timer);
3296
+ if (!immediateRun && debounce > 0) timer = setTimeout(() => void execute(override), debounce);
3297
+ else execute(override);
3298
+ };
3299
+ const queryReady = ref(false);
3300
+ let queryWatcherPrimed = false;
3301
+ watch(() => queryReady.value ? getQuery() : null, () => {
3302
+ if (!queryWatcherPrimed) {
3303
+ queryWatcherPrimed = true;
3304
+ return;
3305
+ }
3306
+ lastOverride = void 0;
3307
+ schedule(false);
3308
+ }, {
3309
+ deep: true,
3310
+ flush: "post"
3311
+ });
3312
+ let disposed = false;
3313
+ function bind(raw) {
3314
+ if (disposed) return;
3315
+ internalController?.abort();
3316
+ internalController = void 0;
3317
+ if (!raw) {
3318
+ controllerRef.value = void 0;
3319
+ hasSource.value = false;
3320
+ total.value = 0;
3321
+ lastData.value = initialData;
3322
+ lastOverride = void 0;
3323
+ return;
3324
+ }
3325
+ if (isUseFetchReturn(raw)) controllerRef.value = raw[FETCH_RESOURCE];
3326
+ else {
3327
+ internalController = createFetchController(raw);
3328
+ controllerRef.value = internalController;
3329
+ }
3330
+ hasSource.value = true;
3331
+ total.value = 0;
3332
+ lastData.value = initialData;
3333
+ lastOverride = void 0;
3334
+ queryReady.value = true;
3335
+ schedule(true);
3336
+ }
3337
+ watch(() => toValue(source), bind, { flush: "post" });
3338
+ queueMicrotask(() => bind(toValue(source)));
3339
+ onScopeDispose(() => {
3340
+ disposed = true;
3341
+ if (timer) clearTimeout(timer);
3342
+ internalController?.abort();
3343
+ });
3344
+ return {
3345
+ enabled: hasSource,
3346
+ state,
3347
+ loading: computed(() => controllerRef.value?.loading.value ?? false),
3348
+ error: computed(() => controllerRef.value?.error.value),
3349
+ total,
3350
+ refresh: async (override) => {
3351
+ await execute(override);
3352
+ }
3353
+ };
3354
+ }
3355
+ //#endregion
3356
+ //#region src/data/useSubscribe.ts
3357
+ /** 归一 subscribe 返回值:导出错误(status 为 error 或携带 error)时返回失败信息,否则 null */
3358
+ function extractSubscribeFailure(result) {
3359
+ if (result === null || typeof result !== "object") return null;
3360
+ const { status, error } = result;
3361
+ if (status === "error" || error != null) return { error: error ?? result };
3362
+ return null;
3363
+ }
3364
+ /**
3365
+ * 订阅型数据消费:挂载时调用处理方法,resolve 出数据后渲染,
3366
+ * 失败时记录错误并保留旧数据(与 fetch 失败策略一致),支持 refresh 重试。
3367
+ *
3368
+ * @internal 消费组件:Table、Statistic、Tabs、Progress、Image 等所有数据展示类组件
3369
+ */
3370
+ function useSubscribe(handler) {
3371
+ const state = ref({
3372
+ status: "pending",
3373
+ data: void 0
3374
+ });
3375
+ const data = ref(void 0);
3376
+ const loading = ref(false);
3377
+ const error = ref(void 0);
3378
+ const isStale = ref(false);
3379
+ async function run() {
3380
+ if (!handler) return;
3381
+ loading.value = true;
3382
+ isStale.value = !!data.value;
3383
+ state.value = {
3384
+ status: "pending",
3385
+ data: data.value,
3386
+ isStale: isStale.value,
3387
+ retry: run
3388
+ };
3389
+ try {
3390
+ const result = await handler();
3391
+ const failure = extractSubscribeFailure(result);
3392
+ if (failure) {
3393
+ error.value = failure.error;
3394
+ loading.value = false;
3395
+ isStale.value = !!data.value;
3396
+ state.value = {
3397
+ status: "error",
3398
+ data: data.value,
3399
+ error: failure.error,
3400
+ isStale: isStale.value,
3401
+ retry: run
3402
+ };
3403
+ return;
3404
+ }
3405
+ if (result?.status === "loading") return;
3406
+ data.value = result?.data;
3407
+ error.value = void 0;
3408
+ loading.value = false;
3409
+ isStale.value = false;
3410
+ state.value = {
3411
+ status: "success",
3412
+ data: data.value,
3413
+ isStale: false,
3414
+ retry: run
3415
+ };
3416
+ } catch (err) {
3417
+ error.value = err;
3418
+ loading.value = false;
3419
+ isStale.value = !!data.value;
3420
+ state.value = {
3421
+ status: "error",
3422
+ data: data.value,
3423
+ error: err,
3424
+ isStale: isStale.value,
3425
+ retry: run
3426
+ };
3427
+ }
3428
+ }
3429
+ queueMicrotask(run);
3430
+ return {
3431
+ state,
3432
+ data,
3433
+ loading,
3434
+ error,
3435
+ isStale,
3436
+ refresh: run
3437
+ };
3438
+ }
3439
+ //#endregion
3440
+ //#region src/data/useChat.ts
3441
+ const LOCAL_PREFIX = "__local_";
3442
+ /**
3443
+ * 聊天消息模型:fetch 管历史(向上翻页前插)、subscribe 管新消息(追加去重)、
3444
+ * send 管发送(乐观上屏 + 失败保留重试)。
3445
+ *
3446
+ * @internal 消费组件:Chat
3447
+ */
3448
+ function useChat(options) {
3449
+ const pageSize = options.pageSize ?? 20;
3450
+ const messages = ref([]);
3451
+ const sending = ref(false);
3452
+ const loadingOlder = ref(false);
3453
+ const hasMore = ref(false);
3454
+ const sendError = ref(void 0);
3455
+ const historyError = ref(void 0);
3456
+ let page = 0;
3457
+ let historyCount = 0;
3458
+ let seq = 0;
3459
+ const isLocal = (message) => String(message.id).startsWith(LOCAL_PREFIX);
3460
+ const subscription = useSubscribe(options.subscribe);
3461
+ watch(subscription.data, (incoming) => {
3462
+ if (incoming !== void 0 && incoming !== null) pushIncoming(incoming);
3463
+ }, { deep: true });
3464
+ function pushIncoming(incoming) {
3465
+ const list = Array.isArray(incoming) ? incoming : [incoming];
3466
+ for (const message of list) {
3467
+ if (!message || message.id === void 0 || message.id === null) continue;
3468
+ if (message.self) {
3469
+ const localIndex = messages.value.findIndex((item) => isLocal(item) && item.status === "sent" && item.content === message.content);
3470
+ if (localIndex >= 0) {
3471
+ messages.value.splice(localIndex, 1, message);
3472
+ continue;
3473
+ }
3474
+ }
3475
+ if (messages.value.some((item) => item.id === message.id)) continue;
3476
+ messages.value.push(message);
3477
+ }
3478
+ }
3479
+ function prepend(batch) {
3480
+ const older = batch.filter((message) => message && message.id !== void 0 && message.id !== null && !messages.value.some((item) => item.id === message.id));
3481
+ if (older.length) messages.value.unshift(...older);
3482
+ }
3483
+ async function loadOlder() {
3484
+ if (!options.fetch || loadingOlder.value) return;
3485
+ loadingOlder.value = true;
3486
+ historyError.value = void 0;
3487
+ try {
3488
+ page += 1;
3489
+ const result = await options.fetch({ pagination: {
3490
+ currentPage: page,
3491
+ pageSize
3492
+ } });
3493
+ if (Array.isArray(result)) {
3494
+ prepend(result);
3495
+ historyCount += result.length;
3496
+ hasMore.value = result.length >= pageSize;
3497
+ } else if (result && Array.isArray(result.list)) {
3498
+ const { list, total } = result;
3499
+ prepend(list);
3500
+ historyCount += list.length;
3501
+ hasMore.value = typeof total === "number" ? historyCount < total : list.length >= pageSize;
3502
+ } else hasMore.value = false;
3503
+ } catch (err) {
3504
+ page -= 1;
3505
+ historyError.value = err instanceof Error ? err.message : String(err);
3506
+ } finally {
3507
+ loadingOlder.value = false;
3508
+ }
3509
+ }
3510
+ async function send(content) {
3511
+ const text = content.trim();
3512
+ if (!text || !options.send || sending.value) return void 0;
3513
+ sendError.value = void 0;
3514
+ sending.value = true;
3515
+ messages.value.push({
3516
+ id: `${LOCAL_PREFIX}${++seq}`,
3517
+ content: text,
3518
+ self: true,
3519
+ time: Date.now(),
3520
+ status: "sending"
3521
+ });
3522
+ const temp = messages.value[messages.value.length - 1];
3523
+ try {
3524
+ const failure = extractActionFailure(await options.send(text));
3525
+ if (failure) {
3526
+ temp.status = "failed";
3527
+ sendError.value = actionErrorText(failure.error, "发送失败");
3528
+ return;
3529
+ }
3530
+ temp.status = "sent";
3531
+ return temp;
3532
+ } catch (err) {
3533
+ temp.status = "failed";
3534
+ sendError.value = actionErrorText(err, "发送失败");
3535
+ return;
3536
+ } finally {
3537
+ sending.value = false;
3538
+ }
3539
+ }
3540
+ async function retry(message) {
3541
+ if (!options.send || !isLocal(message)) return void 0;
3542
+ const index = messages.value.findIndex((item) => item.id === message.id);
3543
+ if (index >= 0) messages.value.splice(index, 1);
3544
+ return send(message.content);
3545
+ }
3546
+ if (options.fetch) queueMicrotask(() => void loadOlder());
3547
+ return {
3548
+ messages,
3549
+ sending,
3550
+ loadingOlder,
3551
+ hasMore,
3552
+ sendError,
3553
+ historyError,
3554
+ send,
3555
+ retry,
3556
+ loadOlder
3557
+ };
3558
+ }
3559
+ //#endregion
3560
+ //#region src/resources/create-fetch-adapter.ts
3561
+ /** 内置适配器的 HTTP 错误:携带状态码与解析后的响应体 */
3562
+ var ResourceHttpError = class extends Error {
3563
+ constructor(method, url, status, body) {
3564
+ super(`${method} ${url} 请求失败(${status})`);
3565
+ this.name = "ResourceHttpError";
3566
+ this.status = status;
3567
+ this.body = body;
3568
+ }
3569
+ };
3570
+ /** 拼装 query 参数:undefined 值整体省略 */
3571
+ function buildUrl(url, query) {
3572
+ if (!query) return url;
3573
+ const search = new URLSearchParams();
3574
+ for (const [key, value] of Object.entries(query)) {
3575
+ if (value === void 0) continue;
3576
+ search.append(key, String(value));
3577
+ }
3578
+ const qs = search.toString();
3579
+ if (!qs) return url;
3580
+ return `${url}${url.includes("?") ? "&" : "?"}${qs}`;
3581
+ }
3582
+ /**
3583
+ * 基于 fetch 的内置传输适配器。
3584
+ * GET 以 query 拼装 URL,写操作以 JSON 提交 body;非 2xx 状态拒绝为
3585
+ * ResourceHttpError(携带状态码与响应体);响应体自动 JSON 解析,空体为 undefined。
3586
+ */
3587
+ function createFetchAdapter() {
3588
+ return async (request) => {
3589
+ const headers = { ...request.headers };
3590
+ let body;
3591
+ if (request.body !== void 0) {
3592
+ headers["Content-Type"] ??= "application/json";
3593
+ body = typeof request.body === "string" ? request.body : JSON.stringify(request.body);
3594
+ }
3595
+ const response = await fetch(buildUrl(request.url, request.query), {
3596
+ method: request.method,
3597
+ headers,
3598
+ body,
3599
+ signal: request.signal
3600
+ });
3601
+ const text = await response.text();
3602
+ let parsed;
3603
+ try {
3604
+ parsed = text === "" ? void 0 : JSON.parse(text);
3605
+ } catch {
3606
+ parsed = text;
3607
+ }
3608
+ if (!response.ok) throw new ResourceHttpError(request.method, request.url, response.status, parsed);
3609
+ return parsed;
3610
+ };
3611
+ }
3612
+ //#endregion
3613
+ //#region src/resources/parse-open-api.ts
3614
+ /** 分页 / 关键词 / 排序参数的常见命名(识别统一查询片段,不作为筛选项) */
3615
+ const PAGE_NAME = /^(page|pageno|pageindex|pagenum|current)$/i;
3616
+ const PAGE_SIZE_NAME = /^(pagesize|size|limit|perpage|per_page)$/i;
3617
+ const KEYWORD_NAME = /^(keyword|kw|q|search)$/i;
3618
+ const SORTER_NAME = /^(sort|sorter|sortby|sort_by|order|orderby|order_by)$/i;
3619
+ const TOTAL_NAME = /^(total|totalcount|totalelements|total_items|count)$/i;
3620
+ const HTTP_METHODS = [
3621
+ "get",
3622
+ "post",
3623
+ "put",
3624
+ "patch",
3625
+ "delete"
3626
+ ];
3627
+ /** 按 $ref 指针(#/a/b/c)在文档中查找目标 */
3628
+ function lookupRef(doc, ref) {
3629
+ if (!ref.startsWith("#/")) return void 0;
3630
+ let current = doc;
3631
+ for (const segment of ref.slice(2).split("/")) {
3632
+ if (current === null || typeof current !== "object") return void 0;
3633
+ current = current[segment.replace(/~1/g, "/").replace(/~0/g, "~")];
3634
+ }
3635
+ return current;
3636
+ }
3637
+ /** 解引用(含引用链),带深度保护 */
3638
+ function resolve(doc, schema, depth = 0) {
3639
+ if (depth > 10 || schema === null || typeof schema !== "object") return void 0;
3640
+ const ref = schema.$ref;
3641
+ if (typeof ref === "string") {
3642
+ const target = lookupRef(doc, ref);
3643
+ return target ? resolve(doc, target, depth + 1) : void 0;
3644
+ }
3645
+ return schema;
3646
+ }
3647
+ /** 操作的 2xx 响应 schema:3.x 取 content,2.0 直接取 schema */
3648
+ function responseSchema(op) {
3649
+ const responses = op.responses;
3650
+ if (!responses) return void 0;
3651
+ const ok = responses["200"] ?? responses["201"] ?? responses.default;
3652
+ if (!ok) return void 0;
3653
+ const content = ok.content;
3654
+ if (content) return (content["application/json"] ?? Object.values(content)[0])?.schema;
3655
+ return ok.schema;
3656
+ }
3657
+ /** 操作的请求体 schema:3.x 取 requestBody,2.0 取 in:body 参数 */
3658
+ function requestBodySchema(doc, op) {
3659
+ const requestBody = op.requestBody;
3660
+ if (requestBody) {
3661
+ const content = (typeof requestBody.$ref === "string" ? lookupRef(doc, requestBody.$ref) : requestBody)?.content;
3662
+ if (!content) return void 0;
3663
+ return (content["application/json"] ?? Object.values(content)[0])?.schema;
3664
+ }
3665
+ return op.parameters?.find((param) => param.in === "body")?.schema;
3666
+ }
3667
+ /** schema 的元数据字段类型;object / array 等非原始类型返回 null(不进入元数据) */
3668
+ function fieldType(schema) {
3669
+ if (Array.isArray(schema.enum) && schema.enum.length > 0 && (schema.type === "string" || schema.type === "integer" || schema.type === "number")) return "enum";
3670
+ switch (schema.type) {
3671
+ case "integer":
3672
+ case "number": return "number";
3673
+ case "boolean": return "boolean";
3674
+ case "string":
3675
+ if (schema.format === "date-time") return "datetime";
3676
+ if (schema.format === "date") return "date";
3677
+ return "string";
3678
+ default: return null;
3679
+ }
3680
+ }
3681
+ function fieldOptions(schema) {
3682
+ if (!Array.isArray(schema.enum)) return void 0;
3683
+ return schema.enum.map((value) => ({
3684
+ label: String(value),
3685
+ value
3686
+ }));
3687
+ }
3688
+ /** 文档声明的内容格式:仅透传字符串形式的 format,其余忽略 */
3689
+ function fieldFormat(schema) {
3690
+ return typeof schema.format === "string" && schema.format !== "" ? schema.format : void 0;
3691
+ }
3692
+ /** 从对象 schema 的 properties 提取字段描述(仅原始类型) */
3693
+ function extractFields(doc, schema) {
3694
+ const resolved = resolve(doc, schema);
3695
+ const properties = resolved?.properties;
3696
+ if (!properties) return [];
3697
+ const requiredList = Array.isArray(resolved?.required) ? resolved.required : [];
3698
+ const fields = [];
3699
+ for (const [name, prop] of Object.entries(properties)) {
3700
+ const resolvedProp = resolve(doc, prop);
3701
+ if (!resolvedProp) continue;
3702
+ const type = fieldType(resolvedProp);
3703
+ if (!type) continue;
3704
+ fields.push({
3705
+ name,
3706
+ label: resolvedProp.description ?? resolvedProp.title ?? name,
3707
+ type,
3708
+ format: fieldFormat(resolvedProp),
3709
+ options: type === "enum" ? fieldOptions(resolvedProp) : void 0,
3710
+ required: requiredList.includes(name) || void 0
3711
+ });
3712
+ }
3713
+ return fields;
3714
+ }
3715
+ /** 识别列表响应形状:裸数组或「数组属性 + 总数属性」的信封结构 */
3716
+ function listShape(doc, schema) {
3717
+ const resolved = resolve(doc, schema);
3718
+ if (!resolved) return null;
3719
+ if (resolved.type === "array") return { itemSchema: resolved.items };
3720
+ const properties = resolved.properties;
3721
+ if (!properties) return null;
3722
+ let listPath;
3723
+ let totalPath;
3724
+ let itemSchema;
3725
+ for (const [name, prop] of Object.entries(properties)) {
3726
+ const resolvedProp = resolve(doc, prop);
3727
+ if (!resolvedProp) continue;
3728
+ if (resolvedProp.type === "array" && listPath === void 0) {
3729
+ listPath = name;
3730
+ itemSchema = resolvedProp.items;
3731
+ } else if (TOTAL_NAME.test(name) && resolvedProp.type === "integer" && totalPath === void 0) totalPath = name;
3732
+ }
3733
+ return listPath === void 0 ? null : {
3734
+ itemSchema,
3735
+ listPath,
3736
+ totalPath
3737
+ };
3738
+ }
3739
+ /** 把提取的字段合并进资源:已存在则追加场景标记,否则新建 */
3740
+ function mergeField(definition, partial, scopeKey) {
3741
+ const fields = definition.fields ??= [];
3742
+ let field = fields.find((item) => item.name === partial.name);
3743
+ if (!field) {
3744
+ field = {
3745
+ name: partial.name,
3746
+ label: partial.label,
3747
+ type: partial.type,
3748
+ scope: {}
3749
+ };
3750
+ if (partial.options) field.options = partial.options;
3751
+ if (partial.format) field.format = partial.format;
3752
+ fields.push(field);
3753
+ } else if (partial.format) field.format ??= partial.format;
3754
+ field.scope = {
3755
+ ...field.scope,
3756
+ [scopeKey]: true
3757
+ };
3758
+ if (partial.required) field.required = true;
3759
+ }
3760
+ /** 操作归类:路径占位符区分集合与单项 */
3761
+ function classify(method, hasPlaceholder) {
3762
+ switch (method) {
3763
+ case "get": return hasPlaceholder ? "detail" : "list";
3764
+ case "post": return hasPlaceholder ? null : "create";
3765
+ case "put":
3766
+ case "patch": return hasPlaceholder ? "update" : null;
3767
+ case "delete": return hasPlaceholder ? "remove" : null;
3768
+ default: return null;
3769
+ }
3770
+ }
3771
+ /** 资源名:首个 tag,回落路径首个非占位段 */
3772
+ function resourceName(op, path) {
3773
+ const tag = op.tags?.[0];
3774
+ if (tag) return tag;
3775
+ return path.split("/").find((part) => part !== "" && !part.startsWith("{")) ?? "root";
3776
+ }
3777
+ /** 从列表操作的查询参数识别统一查询片段命名 */
3778
+ function applyQueryNames(definition, params) {
3779
+ const queryNames = {};
3780
+ for (const param of params) {
3781
+ const name = param.name;
3782
+ if (!name || param.in !== "query") continue;
3783
+ if (PAGE_NAME.test(name)) queryNames.currentPage = name;
3784
+ else if (PAGE_SIZE_NAME.test(name)) queryNames.pageSize = name;
3785
+ else if (KEYWORD_NAME.test(name)) queryNames.keyword = name;
3786
+ else if (SORTER_NAME.test(name)) queryNames.sorter = name;
3787
+ }
3788
+ if (Object.keys(queryNames).length > 0) definition.queryNames = queryNames;
3789
+ }
3790
+ /** 非分页 / 关键词 / 排序的查询参数标记为筛选项;不存在的字段新建 */
3791
+ function applyFilterScopes(doc, definition, params) {
3792
+ const fields = definition.fields ??= [];
3793
+ for (const param of params) {
3794
+ const name = param.name;
3795
+ if (!name || param.in !== "query") continue;
3796
+ if (PAGE_NAME.test(name) || PAGE_SIZE_NAME.test(name) || KEYWORD_NAME.test(name) || SORTER_NAME.test(name)) continue;
3797
+ const schema = resolve(doc, param.schema ?? param);
3798
+ const type = (schema && fieldType(schema)) ?? "string";
3799
+ const existing = fields.find((field) => field.name === name);
3800
+ if (existing) existing.scope = {
3801
+ ...existing.scope,
3802
+ filter: true
3803
+ };
3804
+ else {
3805
+ const field = {
3806
+ name,
3807
+ label: param.description ?? param.summary ?? name,
3808
+ type,
3809
+ scope: { filter: true }
3810
+ };
3811
+ if (schema) {
3812
+ const format = fieldFormat(schema);
3813
+ if (format) field.format = format;
3814
+ if (fieldType(schema) === "enum") field.options = fieldOptions(schema);
3815
+ }
3816
+ fields.push(field);
3817
+ }
3818
+ }
3819
+ }
3820
+ /**
3821
+ * OpenAPI(Swagger)解析器。
3822
+ * 支持路径级 + 操作级参数合并(Swagger 2.0 常见)、$ref 引用、
3823
+ * 信封响应(数组属性 + 总数属性)与裸数组响应。
3824
+ */
3825
+ const parseOpenApi = (rawDoc) => {
3826
+ const doc = rawDoc;
3827
+ const paths = doc?.paths;
3828
+ if (!paths || typeof paths !== "object") throw new Error("无法解析元数据文档:缺少 paths 定义");
3829
+ const resources = {};
3830
+ const ensureResource = (name) => resources[name] ??= { fields: [] };
3831
+ for (const [path, rawItem] of Object.entries(paths)) {
3832
+ const item = rawItem;
3833
+ const sharedParams = item.parameters ?? [];
3834
+ for (const method of HTTP_METHODS) {
3835
+ const op = item[method];
3836
+ if (!op) continue;
3837
+ const key = classify(method, path.includes("{"));
3838
+ if (!key) continue;
3839
+ const definition = ensureResource(resourceName(op, path));
3840
+ definition[key] = {
3841
+ method: method.toUpperCase(),
3842
+ path
3843
+ };
3844
+ const params = [...sharedParams, ...op.parameters ?? []];
3845
+ if (key === "list") {
3846
+ applyQueryNames(definition, params);
3847
+ const shape = listShape(doc, responseSchema(op));
3848
+ if (shape) {
3849
+ if (shape.listPath) definition.response = {
3850
+ list: shape.listPath,
3851
+ total: shape.totalPath
3852
+ };
3853
+ for (const field of extractFields(doc, shape.itemSchema)) mergeField(definition, field, "list");
3854
+ applyFilterScopes(doc, definition, params);
3855
+ }
3856
+ } else if (key === "create" || key === "update") for (const field of extractFields(doc, requestBodySchema(doc, op))) mergeField(definition, field, key);
3857
+ }
3858
+ }
3859
+ return { resources };
3860
+ };
3861
+ //#endregion
3862
+ //#region src/resources/use-resources.ts
3863
+ const EMPTY_SCHEMA = { resources: {} };
3864
+ /** 拼接服务器根地址与路径;路径为绝对 URL 时直接使用 */
3865
+ function resolveUrl(server, path) {
3866
+ if (/^[a-z][a-z\d+\-.]*:\/\//i.test(path)) return path;
3867
+ return `${server.replace(/\/+$/, "")}/${path.replace(/^\/+/, "")}`;
3868
+ }
3869
+ /** 按点分路径从响应体中取值 */
3870
+ function pickPath(body, path) {
3871
+ let current = body;
3872
+ for (const segment of path.split(".")) {
3873
+ if (current === null || typeof current !== "object") return void 0;
3874
+ current = current[segment];
3875
+ }
3876
+ return current;
3877
+ }
3878
+ /** 统一查询片段 → 请求 query 参数(参数名按资源表映射,缺省回落默认名) */
3879
+ function buildListQuery(definition, query) {
3880
+ const names = {
3881
+ keyword: "keyword",
3882
+ currentPage: "page",
3883
+ pageSize: "pageSize",
3884
+ sorter: "sorter",
3885
+ ...definition.queryNames
3886
+ };
3887
+ const params = {};
3888
+ if (!query) return params;
3889
+ if (query.keyword !== void 0 && query.keyword !== null && query.keyword !== "") params[names.keyword] = query.keyword;
3890
+ if (query.pagination) {
3891
+ params[names.currentPage] = query.pagination.currentPage;
3892
+ params[names.pageSize] = query.pagination.pageSize;
3893
+ }
3894
+ if (query.sorter && query.sorter.order) params[names.sorter] = `${query.sorter.key},${query.sorter.order}`;
3895
+ if (query.filters && typeof query.filters === "object") {
3896
+ if (names.filters) params[names.filters] = JSON.stringify(query.filters);
3897
+ else for (const [key, value] of Object.entries(query.filters)) {
3898
+ if (value === void 0 || value === null || value === "") continue;
3899
+ params[key] = Array.isArray(value) ? value.map(String).join(",") : typeof value === "object" ? JSON.stringify(value) : value;
3900
+ }
3901
+ }
3902
+ return params;
3903
+ }
3904
+ /** 按资源表的列表响应映射归一化:信封取数组与总数,裸数组直传 */
3905
+ function normalizeListResult(definition, body) {
3906
+ const mapping = definition.response;
3907
+ if (!mapping?.list) return body;
3908
+ const list = pickPath(body, mapping.list);
3909
+ if (!Array.isArray(list)) return body;
3910
+ const total = mapping.total ? pickPath(body, mapping.total) : void 0;
3911
+ return total === void 0 ? list : {
3912
+ list,
3913
+ total: Number(total)
3914
+ };
3915
+ }
3916
+ /**
3917
+ * 元数据驱动的资源池:声明一次「服务器地址 + 元数据解析方式」,
3918
+ * 按资源名取客户端,请求方法与 fetch / action 协议同形,
3919
+ * 字段元数据(columns / filters / properties)按组件属性形状导出。
3920
+ *
3921
+ * 池初始化时开始加载元数据;client() 无需等待就绪,方法调用时内部自动等待。
3922
+ */
3923
+ function useResources(options) {
3924
+ const adapter = options.request ?? createFetchAdapter();
3925
+ const schemaRef = shallowRef(null);
3926
+ const statusRef = shallowRef("pending");
3927
+ const errorRef = shallowRef(void 0);
3928
+ let loadPromise = null;
3929
+ const resolveHeaders = async () => typeof options.headers === "function" ? options.headers() : options.headers;
3930
+ async function load() {
3931
+ statusRef.value = "pending";
3932
+ errorRef.value = void 0;
3933
+ try {
3934
+ let raw;
3935
+ if (options.schema !== void 0) raw = typeof options.schema === "function" ? await options.schema() : options.schema;
3936
+ else if (options.doc) {
3937
+ const doc = await adapter({
3938
+ method: "GET",
3939
+ url: resolveUrl(options.server, options.doc),
3940
+ headers: await resolveHeaders()
3941
+ });
3942
+ raw = await (options.parse ?? parseOpenApi)(doc, { server: options.server });
3943
+ } else throw new Error("useResources 需要提供 doc(元数据文档)或 schema(资源表)之一");
3944
+ schemaRef.value = options.refine ? await options.refine(raw) : raw;
3945
+ statusRef.value = "success";
3946
+ } catch (error) {
3947
+ errorRef.value = error;
3948
+ statusRef.value = "error";
3949
+ }
3950
+ }
3951
+ /** 等待资源表就绪;加载失败时抛出错误,由调用方决定如何呈现 */
3952
+ function ensureLoaded() {
3953
+ if (!loadPromise) loadPromise = load();
3954
+ return loadPromise.then(() => {
3955
+ if (statusRef.value === "error") throw errorRef.value;
3956
+ return schemaRef.value;
3957
+ });
3958
+ }
3959
+ ensureLoaded().catch(() => {});
3960
+ function client(name) {
3961
+ const loaded = schemaRef.value;
3962
+ if (loaded && !loaded.resources[name]) throw new Error(`资源池中不存在资源「${name}」`);
3963
+ const requireDefinition = async () => {
3964
+ const definition = (await ensureLoaded()).resources[name];
3965
+ if (!definition) throw new Error(`资源池中不存在资源「${name}」`);
3966
+ return definition;
3967
+ };
3968
+ const requireOp = (definition, key) => {
3969
+ const op = definition[key];
3970
+ if (!op) throw new Error(`资源「${name}」未声明 ${key} 操作`);
3971
+ return op;
3972
+ };
3973
+ const collectionUrl = (op) => resolveUrl(options.server, op.path);
3974
+ const itemUrl = (op, id) => resolveUrl(options.server, op.path).replace(/\{id\}/g, encodeURIComponent(String(id)));
3975
+ const fieldsOf = () => schemaRef.value?.resources[name]?.fields ?? [];
3976
+ const columnsOf = () => fieldsOf().filter((field) => field.scope?.list).map((field) => {
3977
+ const column = {
3978
+ key: field.name,
3979
+ title: field.label,
3980
+ type: field.type
3981
+ };
3982
+ if (field.format) column.format = field.format;
3983
+ if (field.type === "enum" && field.options && field.options.length > 0) column.filters = field.options;
3984
+ return column;
3985
+ });
3986
+ return {
3987
+ name,
3988
+ async list(query) {
3989
+ const definition = await requireDefinition();
3990
+ const op = requireOp(definition, "list");
3991
+ return normalizeListResult(definition, await adapter({
3992
+ method: op.method,
3993
+ url: collectionUrl(op),
3994
+ query: buildListQuery(definition, query),
3995
+ headers: await resolveHeaders(),
3996
+ signal: query?.signal
3997
+ }));
3998
+ },
3999
+ async detail(id) {
4000
+ const definition = await requireDefinition();
4001
+ const op = requireOp(definition, "detail");
4002
+ return await adapter({
4003
+ method: op.method,
4004
+ url: itemUrl(op, id),
4005
+ headers: await resolveHeaders()
4006
+ });
4007
+ },
4008
+ async create(payload) {
4009
+ const definition = await requireDefinition();
4010
+ const op = requireOp(definition, "create");
4011
+ return await adapter({
4012
+ method: op.method,
4013
+ url: collectionUrl(op),
4014
+ body: payload,
4015
+ headers: await resolveHeaders()
4016
+ });
4017
+ },
4018
+ async update(id, payload) {
4019
+ const definition = await requireDefinition();
4020
+ const op = requireOp(definition, "update");
4021
+ return await adapter({
4022
+ method: op.method,
4023
+ url: itemUrl(op, id),
4024
+ body: payload,
4025
+ headers: await resolveHeaders()
4026
+ });
4027
+ },
4028
+ async remove(id) {
4029
+ const definition = await requireDefinition();
4030
+ const op = requireOp(definition, "remove");
4031
+ return await adapter({
4032
+ method: op.method,
4033
+ url: itemUrl(op, id),
4034
+ headers: await resolveHeaders()
4035
+ });
4036
+ },
4037
+ get fields() {
4038
+ return fieldsOf();
4039
+ },
4040
+ get columns() {
4041
+ return columnsOf();
4042
+ },
4043
+ get filters() {
4044
+ return fieldsOf().filter((field) => field.scope?.filter);
4045
+ },
4046
+ get properties() {
4047
+ return fieldsOf().filter((field) => field.scope?.create === true || field.scope?.update === true);
4048
+ }
4049
+ };
4050
+ }
4051
+ return {
4052
+ state: {
4053
+ get status() {
4054
+ return statusRef.value;
4055
+ },
4056
+ get data() {
4057
+ return schemaRef.value ?? EMPTY_SCHEMA;
4058
+ },
4059
+ get error() {
4060
+ return errorRef.value;
4061
+ },
4062
+ retry: () => {
4063
+ loadPromise = null;
4064
+ ensureLoaded().catch(() => {});
4065
+ }
4066
+ },
4067
+ client,
4068
+ has: (target) => schemaRef.value?.resources[target] !== void 0,
4069
+ get names() {
4070
+ return Object.keys(schemaRef.value?.resources ?? {});
4071
+ }
4072
+ };
4073
+ }
4074
+ //#endregion
4075
+ //#region src/media/useImageState.ts
4076
+ /**
4077
+ * 图片加载状态机:loading / loaded / error,可选视口感知懒加载。
4078
+ * 不创建 DOM,组件把返回的事件绑定到 <img> 即可。
4079
+ *
4080
+ * @internal 消费组件:Image、Avatar
4081
+ */
4082
+ function useImageState(src, options = {}) {
4083
+ const { lazy = false, target, root, rootMargin, onLoad, onError } = options;
4084
+ const status = ref("idle");
4085
+ const shouldLoad = ref(!lazy);
4086
+ if (lazy && target) {
4087
+ const { isIntersecting } = useIntersectionObserver(target, {
4088
+ root,
4089
+ rootMargin,
4090
+ once: true
4091
+ });
4092
+ watch(isIntersecting, (visible) => {
4093
+ if (visible) shouldLoad.value = true;
4094
+ });
4095
+ } else if (lazy) shouldLoad.value = true;
4096
+ const isLoading = ref(false);
4097
+ const isLoaded = ref(false);
4098
+ const isError = ref(false);
4099
+ const setStatus = (next) => {
4100
+ status.value = next;
4101
+ isLoading.value = next === "loading";
4102
+ isLoaded.value = next === "loaded";
4103
+ isError.value = next === "error";
4104
+ };
4105
+ watch(() => [toValue(src), shouldLoad.value], ([nextSrc, allowed]) => {
4106
+ if (!allowed || !nextSrc) {
4107
+ setStatus(nextSrc ? "loading" : "idle");
4108
+ return;
4109
+ }
4110
+ setStatus("loading");
4111
+ }, { immediate: true });
4112
+ const handleLoad = (event) => {
4113
+ setStatus("loaded");
4114
+ onLoad?.(event);
4115
+ };
4116
+ const handleError = (event) => {
4117
+ setStatus("error");
4118
+ onError?.(event);
4119
+ };
4120
+ const reload = () => {
4121
+ setStatus("loading");
4122
+ };
4123
+ return {
4124
+ status,
4125
+ isLoading,
4126
+ isLoaded,
4127
+ isError,
4128
+ onLoad: handleLoad,
4129
+ onError: handleError,
4130
+ reload,
4131
+ shouldLoad
4132
+ };
4133
+ }
4134
+ //#endregion
4135
+ export { FETCH_RESOURCE, ResourceHttpError, actionErrorText, actionErrorText as subscribeErrorText, createControllableModel, createFetchAdapter, createFetchController, createLoadingService, createMessage, createNotification, extractActionFailure, extractSubscribeFailure, injectForm, injectFormField, isPagedResult, isUseFetchReturn, message, normalizeFetchResult, notification, parseOpenApi, provideForm, provideFormField, resetGlobalZIndex, useActionState, useActionValue, useAsyncState, useAsyncSuggest, useChat, useCheckableGroup, useClickOutside, useClipboard, useControllableModel, useDateGrid, useDateModel, useDismiss, useDraggable, useEventListener, useFetch, useFetchResource, useFloating, useFocusReturn, useFocusTrap, useForm, useFormField, useHoverIntent, useId, useImageState, useIntersectionObserver, useIntervalFn, useListboxNavigation, useLoadingService, useMediaQuery, useModal, useOpen, useOverlayContainer, usePagination, usePopoverModel, useResizeObserver, useResources, useRovingFocus, useScroll, useScrollIntoView, useScrollLock, useSelectModel, useSubscribe, useTimeModel, useTimeoutFn, useToastQueue, useToggle, useTransitionVisible, useTreeModel, useTypeahead, useZIndex };