@holyer-lib/ui 0.1.0 → 0.2.0

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.
package/dist/ui.umd.js CHANGED
@@ -16,25 +16,682 @@
16
16
  return css;
17
17
  }
18
18
 
19
+ function __$styleInject$3(css) {
20
+ if (!css) return;
21
+
22
+ if (typeof window == 'undefined') return;
23
+ var style = document.createElement('style');
24
+ style.setAttribute('media', 'screen');
25
+
26
+ style.innerHTML = css;
27
+ document.head.appendChild(style);
28
+ return css;
29
+ }
30
+
31
+ __$styleInject$3(".hi-expand-panel {\n position: relative;\n display: flex;\n transition: flex 0.3s ease;\n}\n.hi-expand-panel:hover .hi-expand-panel--control-trigger {\n display: flex;\n align-items: center;\n justify-content: center;\n}\n.hi-expand-panel--content {\n height: 100%;\n width: 100%;\n overflow: auto;\n padding: 16px;\n box-sizing: border-box;\n}\n.hi-expand-panel--control-draggable:hover {\n cursor: var(--control-draggable-cursor);\n}\n.hi-expand-panel--control-dragging {\n background-color: var(--control-dragging-bg-color) !important;\n}\n.hi-expand-panel--right .hi-expand-panel--control,\n.hi-expand-panel--left .hi-expand-panel--control {\n position: absolute;\n height: 100%;\n width: 1px;\n background-color: var(--td-gray-color-4);\n}\n.hi-expand-panel--right .hi-expand-panel--control-trigger,\n.hi-expand-panel--left .hi-expand-panel--control-trigger {\n position: absolute;\n top: 50%;\n transform: translateY(-50%);\n cursor: pointer;\n background-color: var(--td-font-white-1);\n border: 1px solid var(--td-gray-color-4);\n width: 12px;\n height: 56px;\n display: none;\n border-radius: 6px;\n}\n.hi-expand-panel--right {\n flex-direction: row;\n}\n.hi-expand-panel--right .hi-expand-panel--control {\n right: 0;\n}\n.hi-expand-panel--right .hi-expand-panel--control-trigger {\n right: -6px;\n}\n.hi-expand-panel--left {\n flex-direction: row-reverse;\n}\n.hi-expand-panel--left .hi-expand-panel--control {\n left: 0;\n}\n.hi-expand-panel--left .hi-expand-panel--control-trigger {\n left: -6px;\n}\n.hi-expand-panel--top .hi-expand-panel--control,\n.hi-expand-panel--bottom .hi-expand-panel--control {\n position: absolute;\n width: 100%;\n height: 1px;\n background-color: var(--td-gray-color-4);\n}\n.hi-expand-panel--top .hi-expand-panel--control-trigger,\n.hi-expand-panel--bottom .hi-expand-panel--control-trigger {\n position: absolute;\n left: 50%;\n transform: translateX(-50%);\n cursor: pointer;\n background-color: var(--td-font-white-1);\n border: 1px solid var(--td-gray-color-4);\n width: 56px;\n height: 12px;\n display: none;\n border-radius: 6px;\n}\n.hi-expand-panel--top {\n flex-direction: column-reverse;\n}\n.hi-expand-panel--top .hi-expand-panel--control {\n top: 0;\n}\n.hi-expand-panel--top .hi-expand-panel--control-trigger {\n top: -6px;\n}\n.hi-expand-panel--bottom {\n flex-direction: column;\n}\n.hi-expand-panel--bottom .hi-expand-panel--control {\n bottom: 0;\n}\n.hi-expand-panel--bottom .hi-expand-panel--control-trigger {\n bottom: -6px;\n}\n");
32
+
33
+ /**
34
+ * 类型判断工具
35
+ */
36
+
37
+ /**
38
+ * 将数值或字符串转换为合法的 CSS 长度值(用于 style 绑定)
39
+ * - 若为 number,自动追加 'px' 单位
40
+ * - 若为 string,原样返回(假定用户已提供合法 CSS 长度值)
41
+ * @param {number|string} value - 输入值(如 100, '100%', '50vh', 'auto')
42
+ * @returns {string} 合法的 CSS 长度字符串 默认值为 0
43
+ * @example
44
+ * formatSize(100) // '100px'
45
+ * formatSize('100%') // '100%'
46
+ * formatSize('50vh') // '50vh'
47
+ * formatSize(null) // 0
48
+ * formatSize(undefined) // 0
49
+ */
50
+ function formatSize$1(value) {
51
+ if (typeof value === 'number') {
52
+ return `${value}px`;
53
+ }
54
+ if (typeof value === 'string') {
55
+ if (value.trim() === '') {
56
+ return 0;
57
+ }
58
+ return value;
59
+ }
60
+ return 0;
61
+ }
62
+
19
63
  //
20
- //
21
- //
22
- //
23
- //
64
+
65
+ const validateSize = val => {
66
+ if (typeof val === 'number') {
67
+ return val >= 0;
68
+ }
69
+ return true;
70
+ };
71
+
72
+ var script$3 = {
73
+ name: 'HiExpandPanel',
74
+ model: {
75
+ prop: 'expanded',
76
+ event: 'update:expanded'
77
+ },
78
+ props: {
79
+ // 支持受控和非受控两种模式
80
+ expanded: {
81
+ type: Boolean,
82
+ default: undefined
83
+ },
84
+
85
+ placement: {
86
+ type: String,
87
+ default: 'right',
88
+ validator: v => ['left', 'right', 'top', 'bottom'].includes(v)
89
+ },
90
+
91
+ size: {
92
+ type: [Number, String],
93
+ default: 280,
94
+ validator: validateSize
95
+ },
96
+
97
+ minSize: {
98
+ type: [Number, String],
99
+ default: 240,
100
+ validator: validateSize
101
+ },
102
+
103
+ maxSize: {
104
+ type: [Number, String],
105
+ default: 480,
106
+ validator: validateSize
107
+ },
108
+
109
+ collapsedSize: {
110
+ type: [String, Number],
111
+ default: 24,
112
+ validator: validateSize
113
+ },
114
+
115
+ draggable: {
116
+ type: Boolean,
117
+ default: true
118
+ },
119
+
120
+ showTrigger: {
121
+ type: Boolean,
122
+ default: true
123
+ },
124
+
125
+ cacheKey: {
126
+ type: String,
127
+ default: ''
128
+ },
129
+
130
+ cacheVersion: {
131
+ type: String,
132
+ default: ''
133
+ },
134
+
135
+ draggingBgColor: {
136
+ type: String,
137
+ default: 'var(--td-gray-color-6)'
138
+ }
139
+ },
140
+ data() {
141
+ let cachedData = {};
142
+ if (this.cacheKey) {
143
+ try {
144
+ const stored = localStorage.getItem(`${this.cacheKey}${this.cacheVersion}`);
145
+ if (stored) {
146
+ cachedData = JSON.parse(stored);
147
+ }
148
+ } catch (e) {
149
+ // eslint-disable-next-line no-console
150
+ console.warn(`HiExpandPanel: Failed to parse cache for key "${this.cacheKey}"`, e);
151
+ cachedData = {};
152
+ }
153
+ }
154
+
155
+ // 核心逻辑:有外部控制就用外部控制,否则用缓存
156
+ const useExternal = this.expanded !== undefined;
157
+ const initialExpanded = useExternal
158
+ ? this.expanded
159
+ : cachedData.cachedExpanded !== undefined
160
+ ? cachedData.cachedExpanded
161
+ : true;
162
+ const initialSize = useExternal
163
+ ? this.size
164
+ : cachedData.cachedSize !== undefined
165
+ ? cachedData.cachedSize
166
+ : this.size;
167
+
168
+ return {
169
+ innerExpanded: initialExpanded,
170
+ clientSize: initialSize,
171
+ isDragging: false,
172
+ startX: 0,
173
+ startY: 0,
174
+ startWidth: 0,
175
+ startHeight: 0,
176
+ hasExternalControl: useExternal
177
+ };
178
+ },
179
+ computed: {
180
+ isHorizontal() {
181
+ return ['left', 'right'].includes(this.placement);
182
+ },
183
+
184
+ // 根据展开状态和方向动态计算面板尺寸限制
185
+ getContainerStyles() {
186
+ // 如果未展开,直接返回空对象,不设置min/max尺寸
187
+ if (!this.innerExpanded) return {};
188
+ // 展开时根据方向设置对应的min/max尺寸
189
+ return this.isHorizontal
190
+ ? {
191
+ minWidth: formatSize$1(this.minSize),
192
+ maxWidth: formatSize$1(this.maxSize)
193
+ }
194
+ : {
195
+ minHeight: formatSize$1(this.minSize),
196
+ maxHeight: formatSize$1(this.maxSize)
197
+ };
198
+ },
199
+ panelStyle() {
200
+ const getWidth = () => {
201
+ if (this.isHorizontal) {
202
+ return this.innerExpanded ? formatSize$1(this.clientSize) : formatSize$1(this.collapsedSize) || 0;
203
+ }
204
+ return '100%';
205
+ };
206
+
207
+ const getHeight = () => {
208
+ if (this.isHorizontal) {
209
+ return '100%';
210
+ }
211
+ return this.innerExpanded ? formatSize$1(this.clientSize) : formatSize$1(this.collapsedSize) || 0;
212
+ };
213
+
214
+ return {
215
+ width: getWidth(),
216
+ height: getHeight(),
217
+ ...this.getContainerStyles,
218
+ transition: this.isDragging ? 'none' : 'flex 0.3s ease',
219
+ '--control-draggable-cursor': this.isHorizontal ? 'col-resize' : 'row-resize',
220
+ '--control-dragging-bg-color': this.draggingBgColor
221
+ };
222
+ }
223
+ },
224
+ watch: {
225
+ expanded(newVal) {
226
+ // 只有在组件是受控时才更新状态
227
+ if (this.hasExternalControl) {
228
+ if (newVal !== this.innerExpanded) {
229
+ this.innerExpanded = newVal;
230
+ }
231
+ }
232
+ },
233
+
234
+ innerExpanded(newVal) {
235
+ this.$emit('update:expanded', newVal);
236
+ this.$emit('expand-change', newVal);
237
+ if (this.cacheKey) {
238
+ this.handleSaveCache();
239
+ }
240
+ }
241
+ },
242
+ methods: {
243
+ handleToggle() {
244
+ this.innerExpanded = !this.innerExpanded;
245
+ },
246
+
247
+ handleSaveCache() {
248
+ if (this.cacheKey) {
249
+ localStorage.setItem(
250
+ `${this.cacheKey}${this.cacheVersion}`,
251
+ JSON.stringify({
252
+ cachedSize: this.clientSize,
253
+ cachedExpanded: this.innerExpanded
254
+ })
255
+ );
256
+ }
257
+ },
258
+
259
+ handleDragMousedown(e) {
260
+ if (!this.draggable) return;
261
+ e.preventDefault();
262
+ this.isDragging = true;
263
+ this.startX = e.clientX;
264
+ this.startY = e.clientY;
265
+
266
+ const panelRect = this.$refs.hiExpandPanelRef.getBoundingClientRect();
267
+ this.startWidth = panelRect.width;
268
+ this.startHeight = panelRect.height;
269
+
270
+ this.handleCreateMousemoveListener();
271
+ // 添加全局样式防止选中文本
272
+ document.body.style.userSelect = 'none';
273
+ },
274
+
275
+ // 核心逻辑:根据鼠标移动计算新的尺寸
276
+ handleDragMousemove(e) {
277
+ if (!this.isDragging) return;
278
+ // 只在需要阻止默认行为时调用 preventDefault
279
+ e.preventDefault();
280
+
281
+ let newSize;
282
+ if (this.isHorizontal) {
283
+ // 根据放置位置决定宽度变化的方向
284
+ const moveWidth = this.placement === 'left' ? this.startX - e.clientX : e.clientX - this.startX;
285
+ newSize = this.startWidth + moveWidth;
286
+ } else {
287
+ // 根据放置位置决定高度变化的方向
288
+ const moveHeight = this.placement === 'top' ? this.startY - e.clientY : e.clientY - this.startY;
289
+ newSize = this.startHeight + moveHeight;
290
+ }
291
+
292
+ // minSize 和 maxSize 已经限制了最大宽高,直接取 newSize 设置 clientSize 即可
293
+ this.clientSize = newSize;
294
+ },
295
+
296
+ // 鼠标松开结束拖动
297
+ handleDragMouseup() {
298
+ if (!this.isDragging) return;
299
+ this.isDragging = false;
300
+ this.handleSaveCache();
301
+ // 清理事件监听
302
+ this.handleClearMousemoveListener();
303
+ this.$emit('drag-end', {
304
+ size: this.clientSize,
305
+ expanded: this.innerExpanded
306
+ });
307
+
308
+ // 恢复全局样式
309
+ document.body.style.userSelect = '';
310
+ },
311
+
312
+ // 添加拖拽事件监听
313
+ handleCreateMousemoveListener() {
314
+ document.addEventListener('mousemove', this.handleDragMousemove);
315
+ document.addEventListener('mouseup', this.handleDragMouseup);
316
+ },
317
+
318
+ // 移除拖拽事件监听
319
+ handleClearMousemoveListener() {
320
+ document.removeEventListener('mousemove', this.handleDragMousemove);
321
+ document.removeEventListener('mouseup', this.handleDragMouseup);
322
+ }
323
+ }
324
+ };
325
+
326
+ function normalizeComponent$3(template, style, script, scopeId, isFunctionalTemplate, moduleIdentifier /* server only */, shadowMode, createInjector, createInjectorSSR, createInjectorShadow) {
327
+ if (typeof shadowMode !== 'boolean') {
328
+ createInjectorSSR = createInjector;
329
+ createInjector = shadowMode;
330
+ shadowMode = false;
331
+ }
332
+ // Vue.extend constructor export interop.
333
+ const options = typeof script === 'function' ? script.options : script;
334
+ // render functions
335
+ if (template && template.render) {
336
+ options.render = template.render;
337
+ options.staticRenderFns = template.staticRenderFns;
338
+ options._compiled = true;
339
+ // functional template
340
+ if (isFunctionalTemplate) {
341
+ options.functional = true;
342
+ }
343
+ }
344
+ // scopedId
345
+ if (scopeId) {
346
+ options._scopeId = scopeId;
347
+ }
348
+ let hook;
349
+ if (moduleIdentifier) {
350
+ // server build
351
+ hook = function (context) {
352
+ // 2.3 injection
353
+ context =
354
+ context || // cached call
355
+ (this.$vnode && this.$vnode.ssrContext) || // stateful
356
+ (this.parent && this.parent.$vnode && this.parent.$vnode.ssrContext); // functional
357
+ // 2.2 with runInNewContext: true
358
+ if (!context && typeof __VUE_SSR_CONTEXT__ !== 'undefined') {
359
+ context = __VUE_SSR_CONTEXT__;
360
+ }
361
+ // inject component styles
362
+ if (style) {
363
+ style.call(this, createInjectorSSR(context));
364
+ }
365
+ // register component module identifier for async chunk inference
366
+ if (context && context._registeredComponents) {
367
+ context._registeredComponents.add(moduleIdentifier);
368
+ }
369
+ };
370
+ // used by ssr in case component is cached and beforeCreate
371
+ // never gets called
372
+ options._ssrRegister = hook;
373
+ }
374
+ else if (style) {
375
+ hook = shadowMode
376
+ ? function (context) {
377
+ style.call(this, createInjectorShadow(context, this.$root.$options.shadowRoot));
378
+ }
379
+ : function (context) {
380
+ style.call(this, createInjector(context));
381
+ };
382
+ }
383
+ if (hook) {
384
+ if (options.functional) {
385
+ // register for functional component in vue file
386
+ const originalRender = options.render;
387
+ options.render = function renderWithStyleInjection(h, context) {
388
+ hook.call(context);
389
+ return originalRender(h, context);
390
+ };
391
+ }
392
+ else {
393
+ // inject component registration as beforeCreate hook
394
+ const existing = options.beforeCreate;
395
+ options.beforeCreate = existing ? [].concat(existing, hook) : [hook];
396
+ }
397
+ }
398
+ return script;
399
+ }
400
+
401
+ /* script */
402
+ const __vue_script__$3 = script$3;
403
+
404
+ /* template */
405
+ var __vue_render__$3 = function () {
406
+ var _vm = this;
407
+ var _h = _vm.$createElement;
408
+ var _c = _vm._self._c || _h;
409
+ return _c(
410
+ "div",
411
+ {
412
+ ref: "hiExpandPanelRef",
413
+ class: ["hi-expand-panel", "hi-expand-panel--" + _vm.placement],
414
+ style: _vm.panelStyle,
415
+ },
416
+ [
417
+ _c(
418
+ "div",
419
+ {
420
+ directives: [
421
+ {
422
+ name: "show",
423
+ rawName: "v-show",
424
+ value: _vm.innerExpanded,
425
+ expression: "innerExpanded",
426
+ },
427
+ ],
428
+ staticClass: "hi-expand-panel--content",
429
+ },
430
+ [_vm._t("default")],
431
+ 2
432
+ ),
433
+ _vm._v(" "),
434
+ _c(
435
+ "div",
436
+ {
437
+ class: [
438
+ "hi-expand-panel--control",
439
+ {
440
+ "hi-expand-panel--control-draggable":
441
+ _vm.draggable && _vm.innerExpanded,
442
+ "hi-expand-panel--control-dragging":
443
+ _vm.isDragging && _vm.innerExpanded,
444
+ },
445
+ ],
446
+ on: {
447
+ mousedown: _vm.handleDragMousedown,
448
+ mousemove: _vm.handleDragMousemove,
449
+ mouseup: _vm.handleDragMouseup,
450
+ },
451
+ },
452
+ [
453
+ _vm.$slots.trigger || _vm.showTrigger
454
+ ? _c(
455
+ "div",
456
+ {
457
+ staticClass: "hi-expand-panel--control-trigger",
458
+ on: {
459
+ click: function ($event) {
460
+ $event.stopPropagation();
461
+ return _vm.handleToggle.apply(null, arguments)
462
+ },
463
+ mousedown: function ($event) {
464
+ $event.stopPropagation();
465
+ },
466
+ },
467
+ },
468
+ [
469
+ _vm._t("trigger", function () {
470
+ return [
471
+ _vm.isHorizontal
472
+ ? [
473
+ _vm.placement === "right"
474
+ ? _c("span", [
475
+ _vm._v(_vm._s(_vm.innerExpanded ? "◂" : "▸")),
476
+ ])
477
+ : _c("span", [
478
+ _vm._v(_vm._s(_vm.innerExpanded ? "▸" : "◂")),
479
+ ]),
480
+ ]
481
+ : [
482
+ _vm.placement === "top"
483
+ ? _c("span", [
484
+ _vm._v(_vm._s(_vm.innerExpanded ? "▾" : "▴")),
485
+ ])
486
+ : _c("span", [
487
+ _vm._v(_vm._s(_vm.innerExpanded ? "▴" : "▾")),
488
+ ]),
489
+ ],
490
+ ]
491
+ }),
492
+ ],
493
+ 2
494
+ )
495
+ : _vm._e(),
496
+ ]
497
+ ),
498
+ ]
499
+ )
500
+ };
501
+ var __vue_staticRenderFns__$3 = [];
502
+ __vue_render__$3._withStripped = true;
503
+
504
+ /* style */
505
+ const __vue_inject_styles__$3 = undefined;
506
+ /* scoped */
507
+ const __vue_scope_id__$3 = undefined;
508
+ /* module identifier */
509
+ const __vue_module_identifier__$3 = undefined;
510
+ /* functional template */
511
+ const __vue_is_functional_template__$3 = false;
512
+ /* style inject */
513
+
514
+ /* style inject SSR */
515
+
516
+ /* style inject shadow dom */
517
+
518
+
519
+
520
+ const __vue_component__$3 = /*#__PURE__*/normalizeComponent$3(
521
+ { render: __vue_render__$3, staticRenderFns: __vue_staticRenderFns__$3 },
522
+ __vue_inject_styles__$3,
523
+ __vue_script__$3,
524
+ __vue_scope_id__$3,
525
+ __vue_is_functional_template__$3,
526
+ __vue_module_identifier__$3,
527
+ false,
528
+ undefined,
529
+ undefined,
530
+ undefined
531
+ );
532
+
533
+ __vue_component__$3.name = 'HiExpandPanel';
534
+
535
+ // 添加 install
536
+ __vue_component__$3.install = Vue => {
537
+ Vue.component(__vue_component__$3.name, __vue_component__$3);
538
+ };
539
+
540
+ function __$styleInject$2(css) {
541
+ if (!css) return;
542
+
543
+ if (typeof window == 'undefined') return;
544
+ var style = document.createElement('style');
545
+ style.setAttribute('media', 'screen');
546
+
547
+ style.innerHTML = css;
548
+ document.head.appendChild(style);
549
+ return css;
550
+ }
551
+
552
+ __$styleInject$2(".hi-expand-text {\n display: flex;\n width: 100%;\n line-height: 1.5;\n}\n.hi-expand-text--content {\n display: -webkit-box;\n -webkit-box-orient: vertical;\n overflow: hidden;\n text-overflow: ellipsis;\n word-break: break-word;\n -webkit-line-clamp: var(--hi-expand-text-line-clamp);\n line-clamp: var(--hi-expand-text-line-clamp);\n}\n.hi-expand-text--content.hi-expand-text--content__show-toggle::before {\n content: '';\n float: right;\n height: 100%;\n margin-bottom: calc(-1em * 1.5);\n}\n.hi-expand-text--content__expanded {\n display: block;\n overflow: visible;\n -webkit-line-clamp: unset;\n line-clamp: unset;\n}\n.hi-expand-text--toggle {\n color: var(--td-brand-color);\n float: right;\n clear: both;\n cursor: pointer;\n}\n");
553
+
24
554
  //
25
555
 
26
- var script = {
27
- name: 'HiTitle',
556
+ var script$2 = {
557
+ name: 'HiExpandText',
28
558
  props: {
29
559
  content: {
30
560
  type: String,
31
- default: '标题'
561
+ default: ''
562
+ },
563
+
564
+ /**
565
+ * [0]: 展开时显示的文本,[1]: 收起时显示的文本
566
+ */
567
+ label: {
568
+ type: Array,
569
+ default: () => ['展开', '收起'],
570
+ validator: arr => {
571
+ return arr.length === 2 && arr.every(s => typeof s === 'string');
572
+ }
573
+ },
574
+
575
+ lineClamp: {
576
+ type: Number,
577
+ default: 2
578
+ }
579
+ },
580
+ data() {
581
+ return {
582
+ showToggle: false,
583
+ isExpanded: false,
584
+ resizeTimer: null,
585
+ resizeObserver: null
586
+ };
587
+ },
588
+ computed: {
589
+ expandText() {
590
+ return this.isExpanded ? this.label[1] : this.label[0];
591
+ },
592
+
593
+ textClass() {
594
+ return {
595
+ 'hi-expand-text--content': true,
596
+ 'hi-expand-text--content__show-toggle': this.showToggle,
597
+ 'hi-expand-text--content__expanded': this.isExpanded
598
+ };
599
+ }
600
+ },
601
+
602
+ watch: {
603
+ content() {
604
+ this.$nextTick(this.checkEllipsis);
32
605
  }
33
606
  },
34
- data() {}
607
+ mounted() {
608
+ this.checkEllipsis();
609
+ window.addEventListener('resize', this.handleResize);
610
+ if (window.ResizeObserver && this.$el) {
611
+ this.resizeObserver = new ResizeObserver(() => {
612
+ this.handleResize();
613
+ });
614
+ this.resizeObserver.observe(this.$el);
615
+ }
616
+ },
617
+ beforeDestroy() {
618
+ window.removeEventListener('resize', this.handleResize);
619
+ if (this.resizeObserver) this.resizeObserver.disconnect();
620
+ if (this.resizeTimer) clearTimeout(this.resizeTimer);
621
+ },
622
+
623
+ methods: {
624
+ handleResize() {
625
+ if (this.resizeTimer) clearTimeout(this.resizeTimer);
626
+ this.resizeTimer = setTimeout(() => {
627
+ this.checkEllipsis();
628
+ }, 100);
629
+ },
630
+
631
+ /**
632
+ * @Description 检查是否存在溢出情况,兼容展开和收起两种状态
633
+ * @Author holyer
634
+ * @Date 2026/02/08 17:01:51
635
+ */
636
+ checkEllipsis() {
637
+ const textEl = this.$refs.textRef;
638
+ if (!textEl || !textEl.offsetParent) {
639
+ this.showToggle = false;
640
+ return;
641
+ }
642
+
643
+ if (this.isExpanded) {
644
+ // 展开状态下:模拟收起状态,检测是否需要 toggle
645
+ this.showToggle = this.wouldOverflowIfCollapsed(textEl);
646
+ } else {
647
+ // 收起状态下:直接检测是否溢出
648
+ this.showToggle = textEl.scrollHeight - textEl.clientHeight > 2;
649
+ }
650
+ },
651
+
652
+ /**
653
+ * 模拟收起状态,检测内容是否会溢出
654
+ */
655
+ wouldOverflowIfCollapsed(el) {
656
+ // 1. 保存原始状态
657
+ const originalDisplay = el.style.display;
658
+ const originalWebkitLineClamp = el.style.webkitLineClamp;
659
+ const originalClassList = el.className;
660
+
661
+ try {
662
+ // 2. 临时应用“收起”样式,移除 --expanded 和 --show-toggle
663
+ el.className = 'hi-expand-text--content';
664
+ el.style.display = '-webkit-box';
665
+ el.style.webkitBoxOrient = 'vertical';
666
+ el.style.overflow = 'hidden';
667
+ el.style.webkitLineClamp = this.lineClamp;
668
+
669
+ // 3. 强制 reflow(触发 layout)
670
+ const { scrollHeight, clientHeight } = el;
671
+
672
+ // 4. 判断是否溢出
673
+ return scrollHeight - clientHeight > 2;
674
+ } finally {
675
+ // 5. 恢复原始状态(确保无副作用)
676
+ el.className = originalClassList;
677
+ el.style.display = originalDisplay;
678
+ el.style.webkitLineClamp = originalWebkitLineClamp;
679
+ }
680
+ },
681
+
682
+ handleToggle() {
683
+ this.isExpanded = !this.isExpanded;
684
+ this.$emit('toggle', this.isExpanded);
685
+ },
686
+
687
+ // 供外部手动更新
688
+ update() {
689
+ this.$nextTick(this.checkEllipsis);
690
+ }
691
+ }
35
692
  };
36
693
 
37
- function normalizeComponent(template, style, script, scopeId, isFunctionalTemplate, moduleIdentifier /* server only */, shadowMode, createInjector, createInjectorSSR, createInjectorShadow) {
694
+ function normalizeComponent$2(template, style, script, scopeId, isFunctionalTemplate, moduleIdentifier /* server only */, shadowMode, createInjector, createInjectorSSR, createInjectorShadow) {
38
695
  if (typeof shadowMode !== 'boolean') {
39
696
  createInjectorSSR = createInjector;
40
697
  createInjector = shadowMode;
@@ -109,57 +766,717 @@
109
766
  return script;
110
767
  }
111
768
 
112
- const isOldIE = typeof navigator !== 'undefined' &&
113
- /msie [6-9]\\b/.test(navigator.userAgent.toLowerCase());
114
- function createInjector(context) {
115
- return (id, style) => addStyle(id, style);
116
- }
117
- let HEAD;
118
- const styles = {};
119
- function addStyle(id, css) {
120
- const group = isOldIE ? css.media || 'default' : id;
121
- const style = styles[group] || (styles[group] = { ids: new Set(), styles: [] });
122
- if (!style.ids.has(id)) {
123
- style.ids.add(id);
124
- let code = css.source;
125
- if (css.map) {
126
- // https://developer.chrome.com/devtools/docs/javascript-debugging
127
- // this makes source maps inside style tags work properly in Chrome
128
- code += '\n/*# sourceURL=' + css.map.sources[0] + ' */';
129
- // http://stackoverflow.com/a/26603875
130
- code +=
131
- '\n/*# sourceMappingURL=data:application/json;base64,' +
132
- btoa(unescape(encodeURIComponent(JSON.stringify(css.map)))) +
133
- ' */';
769
+ /* script */
770
+ const __vue_script__$2 = script$2;
771
+
772
+ /* template */
773
+ var __vue_render__$2 = function () {
774
+ var _vm = this;
775
+ var _h = _vm.$createElement;
776
+ var _c = _vm._self._c || _h;
777
+ return _c(
778
+ "div",
779
+ {
780
+ staticClass: "hi-expand-text",
781
+ style: { "--hi-expand-text-line-clamp": _vm.lineClamp },
782
+ },
783
+ [
784
+ _c(
785
+ "div",
786
+ { ref: "textRef", class: _vm.textClass },
787
+ [
788
+ _vm.showToggle
789
+ ? _c(
790
+ "div",
791
+ {
792
+ staticClass: "hi-expand-text--toggle",
793
+ on: { click: _vm.handleToggle },
794
+ },
795
+ [
796
+ _vm._t("toggleText", function () {
797
+ return [_vm._v(_vm._s(_vm.expandText))]
798
+ }),
799
+ ],
800
+ 2
801
+ )
802
+ : _vm._e(),
803
+ _vm._v(" "),
804
+ _vm._t("default", function () {
805
+ return [_vm._v(_vm._s(_vm.content))]
806
+ }),
807
+ ],
808
+ 2
809
+ ),
810
+ ]
811
+ )
812
+ };
813
+ var __vue_staticRenderFns__$2 = [];
814
+ __vue_render__$2._withStripped = true;
815
+
816
+ /* style */
817
+ const __vue_inject_styles__$2 = undefined;
818
+ /* scoped */
819
+ const __vue_scope_id__$2 = undefined;
820
+ /* module identifier */
821
+ const __vue_module_identifier__$2 = undefined;
822
+ /* functional template */
823
+ const __vue_is_functional_template__$2 = false;
824
+ /* style inject */
825
+
826
+ /* style inject SSR */
827
+
828
+ /* style inject shadow dom */
829
+
830
+
831
+
832
+ const __vue_component__$2 = /*#__PURE__*/normalizeComponent$2(
833
+ { render: __vue_render__$2, staticRenderFns: __vue_staticRenderFns__$2 },
834
+ __vue_inject_styles__$2,
835
+ __vue_script__$2,
836
+ __vue_scope_id__$2,
837
+ __vue_is_functional_template__$2,
838
+ __vue_module_identifier__$2,
839
+ false,
840
+ undefined,
841
+ undefined,
842
+ undefined
843
+ );
844
+
845
+ __vue_component__$2.name = 'HiExpandText';
846
+
847
+ // 添加 install
848
+ __vue_component__$2.install = Vue => {
849
+ Vue.component(__vue_component__$2.name, __vue_component__$2);
850
+ };
851
+
852
+ function __$styleInject$1(css) {
853
+ if (!css) return;
854
+
855
+ if (typeof window == 'undefined') return;
856
+ var style = document.createElement('style');
857
+ style.setAttribute('media', 'screen');
858
+
859
+ style.innerHTML = css;
860
+ document.head.appendChild(style);
861
+ return css;
862
+ }
863
+
864
+ __$styleInject$1(".hi-title {\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n.hi-title--small {\n gap: 2px;\n}\n.hi-title--large {\n gap: 6px;\n}\n.hi-title__header {\n display: flex;\n align-items: center;\n gap: 12px;\n}\n.hi-title__prefix {\n flex-shrink: 0;\n display: flex;\n align-items: center;\n}\n.hi-title__bar {\n width: 4px;\n background-color: var(--td-brand-color);\n flex-shrink: 0;\n}\n.hi-title__icon {\n flex-shrink: 0;\n line-height: 1;\n}\n.hi-title__text {\n margin: 0;\n font-weight: 600;\n color: var(--td-text-color-primary);\n}\n.hi-title__description {\n margin: 0;\n font-size: 12px;\n color: var(--td-text-color-secondary);\n}\n");
865
+
866
+ //
867
+ const TITILE_SIZE_MAP = {
868
+ small: '14px',
869
+ medium: '16px',
870
+ large: '18px'
871
+ };
872
+ var script$1 = {
873
+ name: 'HiTitle',
874
+ inheritAttrs: false,
875
+ props: {
876
+ // 自定义图标类名
877
+ iconClass: {
878
+ type: String,
879
+ default: ''
880
+ },
881
+ // 主标题文本(优先级低于 default slot)
882
+ content: {
883
+ type: String,
884
+ default: ''
885
+ },
886
+ // 描述文本(优先级低于 description slot)
887
+ description: {
888
+ type: String,
889
+ default: ''
890
+ },
891
+ // 尺寸:控制整体大小(影响文字、图标、bar 高度)
892
+ size: {
893
+ type: String,
894
+ default: 'medium',
895
+ validator: val => ['small', 'medium', 'large'].includes(val)
896
+ },
897
+ // 主标题颜色(支持自定义)
898
+ color: {
899
+ type: String,
900
+ default: ''
901
+ },
902
+ // 自定义前缀图标组件
903
+ prefixIcon: {
904
+ type: [Object, Function],
905
+ default: null
906
+ },
907
+ // 自定义 bar 类名(用于覆盖样式)
908
+ barClass: {
909
+ type: String,
910
+ default: ''
911
+ },
912
+ // 自定义标题文字类名
913
+ textClass: {
914
+ type: String,
915
+ default: ''
916
+ },
917
+ // 自定义描述文字类名
918
+ descClass: {
919
+ type: String,
920
+ default: ''
921
+ }
922
+ },
923
+ computed: {
924
+ titleClass() {
925
+ return ['hi-title', `hi-title--${this.size}`, { 'hi-title--has-desc': this.hasDescription }];
926
+ },
927
+ // 根据 size 计算文字大小
928
+ textSize() {
929
+ return TITILE_SIZE_MAP[this.size] || TITILE_SIZE_MAP.medium;
930
+ },
931
+ // 图标大小 = 文字大小(保持视觉一致)
932
+ iconSize() {
933
+ return this.textSize;
934
+ },
935
+ // 装饰条高度 = 文字行高 ≈ 文字大小 * 1.2~1.5
936
+ barHeight() {
937
+ const base = parseFloat(TITILE_SIZE_MAP[this.size] || '16');
938
+ return `${base * 1.2}px`;
939
+ },
940
+ // 主标题颜色(props 优先,否则继承)
941
+ textColor() {
942
+ return this.color || 'inherit';
943
+ },
944
+ // 是否存在描述内容(用于控制布局)
945
+ hasDescription() {
946
+ return this.description || this.$slots.description;
947
+ }
948
+ }
949
+ };
950
+
951
+ function normalizeComponent$1(template, style, script, scopeId, isFunctionalTemplate, moduleIdentifier /* server only */, shadowMode, createInjector, createInjectorSSR, createInjectorShadow) {
952
+ if (typeof shadowMode !== 'boolean') {
953
+ createInjectorSSR = createInjector;
954
+ createInjector = shadowMode;
955
+ shadowMode = false;
956
+ }
957
+ // Vue.extend constructor export interop.
958
+ const options = typeof script === 'function' ? script.options : script;
959
+ // render functions
960
+ if (template && template.render) {
961
+ options.render = template.render;
962
+ options.staticRenderFns = template.staticRenderFns;
963
+ options._compiled = true;
964
+ // functional template
965
+ if (isFunctionalTemplate) {
966
+ options.functional = true;
967
+ }
968
+ }
969
+ // scopedId
970
+ if (scopeId) {
971
+ options._scopeId = scopeId;
972
+ }
973
+ let hook;
974
+ if (moduleIdentifier) {
975
+ // server build
976
+ hook = function (context) {
977
+ // 2.3 injection
978
+ context =
979
+ context || // cached call
980
+ (this.$vnode && this.$vnode.ssrContext) || // stateful
981
+ (this.parent && this.parent.$vnode && this.parent.$vnode.ssrContext); // functional
982
+ // 2.2 with runInNewContext: true
983
+ if (!context && typeof __VUE_SSR_CONTEXT__ !== 'undefined') {
984
+ context = __VUE_SSR_CONTEXT__;
985
+ }
986
+ // inject component styles
987
+ if (style) {
988
+ style.call(this, createInjectorSSR(context));
989
+ }
990
+ // register component module identifier for async chunk inference
991
+ if (context && context._registeredComponents) {
992
+ context._registeredComponents.add(moduleIdentifier);
993
+ }
994
+ };
995
+ // used by ssr in case component is cached and beforeCreate
996
+ // never gets called
997
+ options._ssrRegister = hook;
998
+ }
999
+ else if (style) {
1000
+ hook = shadowMode
1001
+ ? function (context) {
1002
+ style.call(this, createInjectorShadow(context, this.$root.$options.shadowRoot));
1003
+ }
1004
+ : function (context) {
1005
+ style.call(this, createInjector(context));
1006
+ };
1007
+ }
1008
+ if (hook) {
1009
+ if (options.functional) {
1010
+ // register for functional component in vue file
1011
+ const originalRender = options.render;
1012
+ options.render = function renderWithStyleInjection(h, context) {
1013
+ hook.call(context);
1014
+ return originalRender(h, context);
1015
+ };
134
1016
  }
135
- if (!style.element) {
136
- style.element = document.createElement('style');
137
- style.element.type = 'text/css';
138
- if (css.media)
139
- style.element.setAttribute('media', css.media);
140
- if (HEAD === undefined) {
141
- HEAD = document.head || document.getElementsByTagName('head')[0];
142
- }
143
- HEAD.appendChild(style.element);
1017
+ else {
1018
+ // inject component registration as beforeCreate hook
1019
+ const existing = options.beforeCreate;
1020
+ options.beforeCreate = existing ? [].concat(existing, hook) : [hook];
1021
+ }
1022
+ }
1023
+ return script;
1024
+ }
1025
+
1026
+ /* script */
1027
+ const __vue_script__$1 = script$1;
1028
+
1029
+ /* template */
1030
+ var __vue_render__$1 = function () {
1031
+ var _vm = this;
1032
+ var _h = _vm.$createElement;
1033
+ var _c = _vm._self._c || _h;
1034
+ return _c(
1035
+ "div",
1036
+ _vm._b({ class: _vm.titleClass }, "div", _vm.$attrs, false),
1037
+ [
1038
+ _c("div", { staticClass: "hi-title__header" }, [
1039
+ _c(
1040
+ "div",
1041
+ { staticClass: "hi-title__prefix" },
1042
+ [
1043
+ _vm._t("prefix", function () {
1044
+ return [
1045
+ _vm.prefixIcon
1046
+ ? _c(_vm.prefixIcon, {
1047
+ tag: "component",
1048
+ class: ["hi-title__icon", _vm.iconClass],
1049
+ style: { fontSize: _vm.iconSize },
1050
+ })
1051
+ : _c("div", {
1052
+ class: ["hi-title__bar", _vm.barClass],
1053
+ style: { height: _vm.barHeight },
1054
+ }),
1055
+ ]
1056
+ }),
1057
+ ],
1058
+ 2
1059
+ ),
1060
+ _vm._v(" "),
1061
+ _vm.content || _vm.$slots.default
1062
+ ? _c(
1063
+ "div",
1064
+ {
1065
+ class: ["hi-title__text", _vm.textClass],
1066
+ style: {
1067
+ color: _vm.textColor,
1068
+ fontSize: _vm.textSize,
1069
+ },
1070
+ },
1071
+ [
1072
+ _vm._t("default", function () {
1073
+ return [_vm._v(_vm._s(_vm.content))]
1074
+ }),
1075
+ ],
1076
+ 2
1077
+ )
1078
+ : _vm._e(),
1079
+ ]),
1080
+ _vm._v(" "),
1081
+ _vm.hasDescription
1082
+ ? _c(
1083
+ "p",
1084
+ { class: ["hi-title__description", _vm.descClass] },
1085
+ [
1086
+ _vm._t("description", function () {
1087
+ return [_vm._v(_vm._s(_vm.description))]
1088
+ }),
1089
+ ],
1090
+ 2
1091
+ )
1092
+ : _vm._e(),
1093
+ ]
1094
+ )
1095
+ };
1096
+ var __vue_staticRenderFns__$1 = [];
1097
+ __vue_render__$1._withStripped = true;
1098
+
1099
+ /* style */
1100
+ const __vue_inject_styles__$1 = undefined;
1101
+ /* scoped */
1102
+ const __vue_scope_id__$1 = undefined;
1103
+ /* module identifier */
1104
+ const __vue_module_identifier__$1 = undefined;
1105
+ /* functional template */
1106
+ const __vue_is_functional_template__$1 = false;
1107
+ /* style inject */
1108
+
1109
+ /* style inject SSR */
1110
+
1111
+ /* style inject shadow dom */
1112
+
1113
+
1114
+
1115
+ const __vue_component__$1 = /*#__PURE__*/normalizeComponent$1(
1116
+ { render: __vue_render__$1, staticRenderFns: __vue_staticRenderFns__$1 },
1117
+ __vue_inject_styles__$1,
1118
+ __vue_script__$1,
1119
+ __vue_scope_id__$1,
1120
+ __vue_is_functional_template__$1,
1121
+ __vue_module_identifier__$1,
1122
+ false,
1123
+ undefined,
1124
+ undefined,
1125
+ undefined
1126
+ );
1127
+
1128
+ // 显式设置 name(避免 .vue 丢失)
1129
+ __vue_component__$1.name = 'HiTitle';
1130
+
1131
+ // 添加 install
1132
+ __vue_component__$1.install = Vue => {
1133
+ Vue.component(__vue_component__$1.name, __vue_component__$1);
1134
+ };
1135
+
1136
+ function __$styleInject(css) {
1137
+ if (!css) return;
1138
+
1139
+ if (typeof window == 'undefined') return;
1140
+ var style = document.createElement('style');
1141
+ style.setAttribute('media', 'screen');
1142
+
1143
+ style.innerHTML = css;
1144
+ document.head.appendChild(style);
1145
+ return css;
1146
+ }
1147
+
1148
+ __$styleInject(".hi-virtual-list {\n overflow-y: auto;\n overflow-x: hidden;\n position: relative;\n will-change: scroll-position;\n}\n.hi-virtual-list--placeholder {\n pointer-events: none;\n}\n.hi-virtual-list--item-wrapper {\n box-sizing: border-box;\n}\n");
1149
+
1150
+ /**
1151
+ * 类型判断工具
1152
+ */
1153
+
1154
+ /**
1155
+ * 将数值或字符串转换为合法的 CSS 长度值(用于 style 绑定)
1156
+ * - 若为 number,自动追加 'px' 单位
1157
+ * - 若为 string,原样返回(假定用户已提供合法 CSS 长度值)
1158
+ * @param {number|string} value - 输入值(如 100, '100%', '50vh', 'auto')
1159
+ * @returns {string} 合法的 CSS 长度字符串 默认值为 0
1160
+ * @example
1161
+ * formatSize(100) // '100px'
1162
+ * formatSize('100%') // '100%'
1163
+ * formatSize('50vh') // '50vh'
1164
+ * formatSize(null) // 0
1165
+ * formatSize(undefined) // 0
1166
+ */
1167
+ function formatSize(value) {
1168
+ if (typeof value === 'number') {
1169
+ return `${value}px`;
1170
+ }
1171
+ if (typeof value === 'string') {
1172
+ if (value.trim() === '') {
1173
+ return 0;
1174
+ }
1175
+ return value;
1176
+ }
1177
+ return 0;
1178
+ }
1179
+
1180
+ //
1181
+
1182
+ var script = {
1183
+ name: 'HiVirtualList',
1184
+ props: {
1185
+ items: {
1186
+ type: Array,
1187
+ required: true,
1188
+ default: () => []
1189
+ },
1190
+ itemHeight: {
1191
+ type: Number,
1192
+ required: true,
1193
+ validator(value) {
1194
+ if (value <= 0) {
1195
+ // eslint-disable-next-line no-console
1196
+ console.error('[HiVirtualList] itemHeight must be a positive number.');
1197
+ return false;
1198
+ }
1199
+ return true;
1200
+ }
1201
+ },
1202
+ height: {
1203
+ type: [Number, String],
1204
+ required: true
1205
+ },
1206
+ buffer: {
1207
+ type: Number,
1208
+ default: 50
1209
+ },
1210
+ nodeKey: {
1211
+ type: String,
1212
+ default: undefined
1213
+ }
1214
+ },
1215
+ data() {
1216
+ return {
1217
+ scrollTop: 0,
1218
+ clientHeight: 0,
1219
+ resizeObserver: null,
1220
+ resizeTimer: null
1221
+ };
1222
+ },
1223
+ computed: {
1224
+ styles() {
1225
+ return {
1226
+ height: formatSize(this.height)
1227
+ };
1228
+ },
1229
+ renderItemHeight() {
1230
+ return formatSize(this.itemHeight);
1231
+ },
1232
+ total() {
1233
+ return this.items.length;
1234
+ },
1235
+ visibleCount() {
1236
+ if (!this.clientHeight || !this.itemHeight) return 20;
1237
+ return Math.ceil(this.clientHeight / this.itemHeight) + this.buffer * 2;
1238
+ },
1239
+ startIndex() {
1240
+ return Math.max(0, Math.floor(this.scrollTop / this.itemHeight));
1241
+ },
1242
+ endIndex() {
1243
+ return Math.min(this.total, this.startIndex + this.visibleCount);
1244
+ },
1245
+ visibleItems() {
1246
+ return this.items.slice(this.startIndex, this.endIndex);
1247
+ },
1248
+ topPlaceholderHeight() {
1249
+ return formatSize(this.startIndex * this.itemHeight);
1250
+ },
1251
+ bottomPlaceholderHeight() {
1252
+ return formatSize((this.total - this.endIndex) * this.itemHeight);
1253
+ }
1254
+ },
1255
+ mounted() {
1256
+ this.updateClientHeight();
1257
+ this.setupResizeListener();
1258
+ },
1259
+ beforeDestroy() {
1260
+ this.cleanupResizeListener();
1261
+ },
1262
+ methods: {
1263
+ /**
1264
+ * 判断是否应使用 nodeKey 字段
1265
+ */
1266
+ _shouldUseNodeKey() {
1267
+ return this.nodeKey && typeof this.nodeKey === 'string' && this.nodeKey.trim() !== '';
1268
+ },
1269
+
1270
+ /**
1271
+ * 获取 item 的唯一 key(用于 v-for)
1272
+ */
1273
+ getNodeKey(item, index) {
1274
+ if (this._shouldUseNodeKey() && item != null && typeof item === 'object') {
1275
+ const key = item[this.nodeKey];
1276
+ if (key != null) {
1277
+ return key;
1278
+ }
1279
+ }
1280
+ return index;
1281
+ },
1282
+
1283
+ updateClientHeight() {
1284
+ if (this.$refs.rootRef) {
1285
+ this.clientHeight = this.$refs.rootRef.clientHeight;
1286
+ }
1287
+ },
1288
+
1289
+ setupResizeListener() {
1290
+ if (typeof ResizeObserver !== 'undefined') {
1291
+ this.resizeObserver = new ResizeObserver(() => {
1292
+ this.updateClientHeight();
1293
+ });
1294
+ this.resizeObserver.observe(this.$refs.rootRef);
1295
+ } else {
1296
+ window.addEventListener('resize', this.handleWindowResize);
1297
+ }
1298
+ },
1299
+
1300
+ cleanupResizeListener() {
1301
+ if (this.resizeObserver) {
1302
+ this.resizeObserver.disconnect();
1303
+ this.resizeObserver = null;
1304
+ }
1305
+ if (this.resizeTimer) {
1306
+ clearTimeout(this.resizeTimer);
1307
+ this.resizeTimer = null;
1308
+ }
1309
+ window.removeEventListener('resize', this.handleWindowResize);
1310
+ },
1311
+
1312
+ handleWindowResize() {
1313
+ if (this.resizeTimer) {
1314
+ clearTimeout(this.resizeTimer);
1315
+ }
1316
+ this.resizeTimer = setTimeout(() => {
1317
+ this.updateClientHeight();
1318
+ }, 100);
1319
+ },
1320
+
1321
+ handleScroll(e) {
1322
+ this.scrollTop = e.target.scrollTop;
1323
+ this.$emit('scroll', e);
1324
+
1325
+ const { scrollHeight, clientHeight, scrollTop } = e.target;
1326
+ if (scrollTop === 0) this.$emit('reach-top');
1327
+ if (scrollHeight - clientHeight - scrollTop < 100) this.$emit('reach-bottom');
1328
+ this.$emit('visible-change', { startIndex: this.startIndex, endIndex: this.endIndex });
1329
+ },
1330
+
1331
+ refresh() {
1332
+ this.updateClientHeight();
1333
+ },
1334
+
1335
+ /**
1336
+ * 内部滚动方法(仅设置 DOM scrollTop,状态由 handleScroll 同步)
1337
+ */
1338
+ _setScrollTop(scrollTop) {
1339
+ if (this.$refs.rootRef) {
1340
+ this.$refs.rootRef.scrollTop = scrollTop;
1341
+ }
1342
+ },
1343
+
1344
+ /**
1345
+ * 滚动到指定目标
1346
+ * - 若启用了有效的 nodeKey,则 target 视为 keyValue
1347
+ * - 否则,target 必须是 number(index)
1348
+ */
1349
+ scrollTo(target) {
1350
+ if (target == null) {
1351
+ // eslint-disable-next-line no-console
1352
+ console.warn('[HiVirtualList] scrollTo: target cannot be null or undefined');
1353
+ return;
1354
+ }
1355
+
1356
+ let index = -1;
1357
+
1358
+ // 使用 getNodeKey 生成的 key 进行匹配(严格对齐)
1359
+ if (this._shouldUseNodeKey()) {
1360
+ index = this.items.findIndex((item, i) => {
1361
+ return this.getNodeKey(item, i) === target;
1362
+ });
1363
+ } else {
1364
+ // 降级到 index 模式
1365
+ if (typeof target === 'number' && target >= 0 && target < this.total) {
1366
+ index = target;
1367
+ } else {
1368
+ // eslint-disable-next-line no-console
1369
+ console.warn(
1370
+ '[HiVirtualList] scrollTo: when nodeKey is not provided, target must be a valid index (number).'
1371
+ );
1372
+ return;
1373
+ }
1374
+ }
1375
+
1376
+ if (index < 0 || index >= this.total) {
1377
+ if (this._shouldUseNodeKey()) {
1378
+ // eslint-disable-next-line no-console
1379
+ console.warn(`[HiVirtualList] scrollTo: item with key "${target}" not found`);
1380
+ } else {
1381
+ // eslint-disable-next-line no-console
1382
+ console.warn(`[HiVirtualList] scrollTo: index ${target} is out of range [0, ${this.total})`);
1383
+ }
1384
+ return;
1385
+ }
1386
+
1387
+ this._setScrollTop(index * this.itemHeight);
1388
+ },
1389
+
1390
+ scrollToTop() {
1391
+ this._setScrollTop(0);
1392
+ },
1393
+
1394
+ scrollToBottom() {
1395
+ this._setScrollTop(this.total * this.itemHeight);
1396
+ },
1397
+
1398
+ getVisibleRange() {
1399
+ return {
1400
+ startIndex: this.startIndex,
1401
+ endIndex: this.endIndex
1402
+ };
1403
+ }
1404
+ }
1405
+ };
1406
+
1407
+ function normalizeComponent(template, style, script, scopeId, isFunctionalTemplate, moduleIdentifier /* server only */, shadowMode, createInjector, createInjectorSSR, createInjectorShadow) {
1408
+ if (typeof shadowMode !== 'boolean') {
1409
+ createInjectorSSR = createInjector;
1410
+ createInjector = shadowMode;
1411
+ shadowMode = false;
1412
+ }
1413
+ // Vue.extend constructor export interop.
1414
+ const options = typeof script === 'function' ? script.options : script;
1415
+ // render functions
1416
+ if (template && template.render) {
1417
+ options.render = template.render;
1418
+ options.staticRenderFns = template.staticRenderFns;
1419
+ options._compiled = true;
1420
+ // functional template
1421
+ if (isFunctionalTemplate) {
1422
+ options.functional = true;
144
1423
  }
145
- if ('styleSheet' in style.element) {
146
- style.styles.push(code);
147
- style.element.styleSheet.cssText = style.styles
148
- .filter(Boolean)
149
- .join('\n');
1424
+ }
1425
+ // scopedId
1426
+ if (scopeId) {
1427
+ options._scopeId = scopeId;
1428
+ }
1429
+ let hook;
1430
+ if (moduleIdentifier) {
1431
+ // server build
1432
+ hook = function (context) {
1433
+ // 2.3 injection
1434
+ context =
1435
+ context || // cached call
1436
+ (this.$vnode && this.$vnode.ssrContext) || // stateful
1437
+ (this.parent && this.parent.$vnode && this.parent.$vnode.ssrContext); // functional
1438
+ // 2.2 with runInNewContext: true
1439
+ if (!context && typeof __VUE_SSR_CONTEXT__ !== 'undefined') {
1440
+ context = __VUE_SSR_CONTEXT__;
1441
+ }
1442
+ // inject component styles
1443
+ if (style) {
1444
+ style.call(this, createInjectorSSR(context));
1445
+ }
1446
+ // register component module identifier for async chunk inference
1447
+ if (context && context._registeredComponents) {
1448
+ context._registeredComponents.add(moduleIdentifier);
1449
+ }
1450
+ };
1451
+ // used by ssr in case component is cached and beforeCreate
1452
+ // never gets called
1453
+ options._ssrRegister = hook;
1454
+ }
1455
+ else if (style) {
1456
+ hook = shadowMode
1457
+ ? function (context) {
1458
+ style.call(this, createInjectorShadow(context, this.$root.$options.shadowRoot));
1459
+ }
1460
+ : function (context) {
1461
+ style.call(this, createInjector(context));
1462
+ };
1463
+ }
1464
+ if (hook) {
1465
+ if (options.functional) {
1466
+ // register for functional component in vue file
1467
+ const originalRender = options.render;
1468
+ options.render = function renderWithStyleInjection(h, context) {
1469
+ hook.call(context);
1470
+ return originalRender(h, context);
1471
+ };
150
1472
  }
151
1473
  else {
152
- const index = style.ids.size - 1;
153
- const textNode = document.createTextNode(code);
154
- const nodes = style.element.childNodes;
155
- if (nodes[index])
156
- style.element.removeChild(nodes[index]);
157
- if (nodes.length)
158
- style.element.insertBefore(textNode, nodes[index]);
159
- else
160
- style.element.appendChild(textNode);
1474
+ // inject component registration as beforeCreate hook
1475
+ const existing = options.beforeCreate;
1476
+ options.beforeCreate = existing ? [].concat(existing, hook) : [hook];
161
1477
  }
162
1478
  }
1479
+ return script;
163
1480
  }
164
1481
 
165
1482
  /* script */
@@ -170,25 +1487,54 @@
170
1487
  var _vm = this;
171
1488
  var _h = _vm.$createElement;
172
1489
  var _c = _vm._self._c || _h;
173
- return _c("div", { staticClass: "hi-title" }, [
174
- _vm._v("\n " + _vm._s(_vm.content) + "\n"),
175
- ])
1490
+ return _c(
1491
+ "div",
1492
+ {
1493
+ ref: "rootRef",
1494
+ staticClass: "hi-virtual-list",
1495
+ style: _vm.styles,
1496
+ on: { scroll: _vm.handleScroll },
1497
+ },
1498
+ [
1499
+ _c("div", {
1500
+ staticClass: "hi-virtual-list--placeholder",
1501
+ style: { height: _vm.topPlaceholderHeight },
1502
+ }),
1503
+ _vm._v(" "),
1504
+ _vm._l(_vm.visibleItems, function (item, i) {
1505
+ return _c(
1506
+ "div",
1507
+ {
1508
+ key: _vm.getNodeKey(item, _vm.startIndex + i),
1509
+ staticClass: "hi-virtual-list--item-wrapper",
1510
+ style: { height: _vm.renderItemHeight },
1511
+ },
1512
+ [_vm._t("default", null, { item: item, index: _vm.startIndex + i })],
1513
+ 2
1514
+ )
1515
+ }),
1516
+ _vm._v(" "),
1517
+ _c("div", {
1518
+ staticClass: "hi-virtual-list--placeholder",
1519
+ style: { height: _vm.bottomPlaceholderHeight },
1520
+ }),
1521
+ ],
1522
+ 2
1523
+ )
176
1524
  };
177
1525
  var __vue_staticRenderFns__ = [];
178
1526
  __vue_render__._withStripped = true;
179
1527
 
180
1528
  /* style */
181
- const __vue_inject_styles__ = function (inject) {
182
- if (!inject) return
183
- inject("data-v-03a3ffda_0", { source: "\n.title[data-v-03a3ffda] {\n text-align: center;\n margin: 20px 0;\n}\n", map: {"version":3,"sources":["/home/runner/work/holyer-lib/holyer-lib/packages/ui/title/src/index.vue"],"names":[],"mappings":";AAoBA;EACA,kBAAA;EACA,cAAA;AACA","file":"index.vue","sourcesContent":["<template>\n <div class=\"hi-title\">\n {{ content }}\n </div>\n</template>\n\n<script>\nexport default {\n name: 'HiTitle',\n props: {\n content: {\n type: String,\n default: '标题'\n }\n },\n data() {}\n};\n</script>\n\n<style scoped>\n.title {\n text-align: center;\n margin: 20px 0;\n}\n</style>\n"]}, media: undefined });
184
-
185
- };
1529
+ const __vue_inject_styles__ = undefined;
186
1530
  /* scoped */
187
- const __vue_scope_id__ = "data-v-03a3ffda";
1531
+ const __vue_scope_id__ = undefined;
188
1532
  /* module identifier */
189
1533
  const __vue_module_identifier__ = undefined;
190
1534
  /* functional template */
191
1535
  const __vue_is_functional_template__ = false;
1536
+ /* style inject */
1537
+
192
1538
  /* style inject SSR */
193
1539
 
194
1540
  /* style inject shadow dom */
@@ -203,12 +1549,26 @@
203
1549
  __vue_is_functional_template__,
204
1550
  __vue_module_identifier__,
205
1551
  false,
206
- createInjector,
1552
+ undefined,
207
1553
  undefined,
208
1554
  undefined
209
1555
  );
210
1556
 
211
- const components = [__vue_component__];
1557
+ // 显式设置 name(避免 .vue 丢失)
1558
+ __vue_component__.name = 'HiVirtualList';
1559
+
1560
+ // 添加 install
1561
+ __vue_component__.install = Vue => {
1562
+ Vue.component(__vue_component__.name, __vue_component__);
1563
+ };
1564
+
1565
+ // eslint-disable-next-line prettier/prettier
1566
+ const components = [
1567
+ __vue_component__$3,
1568
+ __vue_component__$2,
1569
+ __vue_component__$1,
1570
+ __vue_component__
1571
+ ];
212
1572
 
213
1573
  const install = function (Vue) {
214
1574
  components.forEach(component => {
@@ -216,7 +1576,13 @@
216
1576
  });
217
1577
  };
218
1578
 
219
- var index = { install, HiTitle: __vue_component__ };
1579
+ var index = {
1580
+ install,
1581
+ HiExpandPanel: __vue_component__$3,
1582
+ HiExpandText: __vue_component__$2,
1583
+ HiTitle: __vue_component__$1,
1584
+ HiVirtualList: __vue_component__
1585
+ };
220
1586
 
221
1587
  return index;
222
1588