@holyer-lib/ui 0.0.9 → 0.0.11
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.cjs.js +760 -9
- package/dist/ui.esm.js +760 -9
- package/dist/ui.umd.js +760 -9
- package/package.json +1 -7
package/dist/ui.umd.js
CHANGED
|
@@ -16,7 +16,319 @@
|
|
|
16
16
|
return css;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
function __$styleInject(css) {
|
|
19
|
+
function __$styleInject$2(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$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");
|
|
32
|
+
|
|
33
|
+
//
|
|
34
|
+
|
|
35
|
+
var script$2 = {
|
|
36
|
+
name: 'HiExpandText',
|
|
37
|
+
props: {
|
|
38
|
+
content: {
|
|
39
|
+
type: String,
|
|
40
|
+
default: ''
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* [0]: 展开时显示的文本,[1]: 收起时显示的文本
|
|
45
|
+
*/
|
|
46
|
+
label: {
|
|
47
|
+
type: Array,
|
|
48
|
+
default: () => ['展开', '收起'],
|
|
49
|
+
validator: arr => {
|
|
50
|
+
return arr.length === 2 && arr.every(s => typeof s === 'string');
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
lineClamp: {
|
|
55
|
+
type: Number,
|
|
56
|
+
default: 2
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
data() {
|
|
60
|
+
return {
|
|
61
|
+
showToggle: false,
|
|
62
|
+
isExpanded: false,
|
|
63
|
+
resizeTimer: null,
|
|
64
|
+
resizeObserver: null
|
|
65
|
+
};
|
|
66
|
+
},
|
|
67
|
+
computed: {
|
|
68
|
+
expandText() {
|
|
69
|
+
return this.isExpanded ? this.label[1] : this.label[0];
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
textClass() {
|
|
73
|
+
return {
|
|
74
|
+
'hi-expand-text--content': true,
|
|
75
|
+
'hi-expand-text--content__show-toggle': this.showToggle,
|
|
76
|
+
'hi-expand-text--content__expanded': this.isExpanded
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
watch: {
|
|
82
|
+
content() {
|
|
83
|
+
this.$nextTick(this.checkEllipsis);
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
mounted() {
|
|
87
|
+
this.checkEllipsis();
|
|
88
|
+
window.addEventListener('resize', this.handleResize);
|
|
89
|
+
if (window.ResizeObserver && this.$el) {
|
|
90
|
+
this.resizeObserver = new ResizeObserver(() => {
|
|
91
|
+
this.handleResize();
|
|
92
|
+
});
|
|
93
|
+
this.resizeObserver.observe(this.$el);
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
beforeDestroy() {
|
|
97
|
+
window.removeEventListener('resize', this.handleResize);
|
|
98
|
+
if (this.resizeObserver) this.resizeObserver.disconnect();
|
|
99
|
+
if (this.resizeTimer) clearTimeout(this.resizeTimer);
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
methods: {
|
|
103
|
+
handleResize() {
|
|
104
|
+
if (this.resizeTimer) clearTimeout(this.resizeTimer);
|
|
105
|
+
this.resizeTimer = setTimeout(() => {
|
|
106
|
+
this.checkEllipsis();
|
|
107
|
+
}, 100);
|
|
108
|
+
},
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* @Description 检查是否存在溢出情况,兼容展开和收起两种状态
|
|
112
|
+
* @Author holyer
|
|
113
|
+
* @Date 2026/02/08 17:01:51
|
|
114
|
+
*/
|
|
115
|
+
checkEllipsis() {
|
|
116
|
+
const textEl = this.$refs.textRef;
|
|
117
|
+
if (!textEl || !textEl.offsetParent) {
|
|
118
|
+
this.showToggle = false;
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (this.isExpanded) {
|
|
123
|
+
// 展开状态下:模拟收起状态,检测是否需要 toggle
|
|
124
|
+
this.showToggle = this.wouldOverflowIfCollapsed(textEl);
|
|
125
|
+
} else {
|
|
126
|
+
// 收起状态下:直接检测是否溢出
|
|
127
|
+
this.showToggle = textEl.scrollHeight - textEl.clientHeight > 2;
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* 模拟收起状态,检测内容是否会溢出
|
|
133
|
+
*/
|
|
134
|
+
wouldOverflowIfCollapsed(el) {
|
|
135
|
+
// 1. 保存原始状态
|
|
136
|
+
const originalDisplay = el.style.display;
|
|
137
|
+
const originalWebkitLineClamp = el.style.webkitLineClamp;
|
|
138
|
+
const originalClassList = el.className;
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
// 2. 临时应用“收起”样式,移除 --expanded 和 --show-toggle
|
|
142
|
+
el.className = 'hi-expand-text--content';
|
|
143
|
+
el.style.display = '-webkit-box';
|
|
144
|
+
el.style.webkitBoxOrient = 'vertical';
|
|
145
|
+
el.style.overflow = 'hidden';
|
|
146
|
+
el.style.webkitLineClamp = this.lineClamp;
|
|
147
|
+
|
|
148
|
+
// 3. 强制 reflow(触发 layout)
|
|
149
|
+
const { scrollHeight, clientHeight } = el;
|
|
150
|
+
|
|
151
|
+
// 4. 判断是否溢出
|
|
152
|
+
return scrollHeight - clientHeight > 2;
|
|
153
|
+
} finally {
|
|
154
|
+
// 5. 恢复原始状态(确保无副作用)
|
|
155
|
+
el.className = originalClassList;
|
|
156
|
+
el.style.display = originalDisplay;
|
|
157
|
+
el.style.webkitLineClamp = originalWebkitLineClamp;
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
|
|
161
|
+
handleToggle() {
|
|
162
|
+
this.isExpanded = !this.isExpanded;
|
|
163
|
+
this.$emit('toggle', this.isExpanded);
|
|
164
|
+
},
|
|
165
|
+
|
|
166
|
+
// 供外部手动更新
|
|
167
|
+
update() {
|
|
168
|
+
this.$nextTick(this.checkEllipsis);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
function normalizeComponent$2(template, style, script, scopeId, isFunctionalTemplate, moduleIdentifier /* server only */, shadowMode, createInjector, createInjectorSSR, createInjectorShadow) {
|
|
174
|
+
if (typeof shadowMode !== 'boolean') {
|
|
175
|
+
createInjectorSSR = createInjector;
|
|
176
|
+
createInjector = shadowMode;
|
|
177
|
+
shadowMode = false;
|
|
178
|
+
}
|
|
179
|
+
// Vue.extend constructor export interop.
|
|
180
|
+
const options = typeof script === 'function' ? script.options : script;
|
|
181
|
+
// render functions
|
|
182
|
+
if (template && template.render) {
|
|
183
|
+
options.render = template.render;
|
|
184
|
+
options.staticRenderFns = template.staticRenderFns;
|
|
185
|
+
options._compiled = true;
|
|
186
|
+
// functional template
|
|
187
|
+
if (isFunctionalTemplate) {
|
|
188
|
+
options.functional = true;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
// scopedId
|
|
192
|
+
if (scopeId) {
|
|
193
|
+
options._scopeId = scopeId;
|
|
194
|
+
}
|
|
195
|
+
let hook;
|
|
196
|
+
if (moduleIdentifier) {
|
|
197
|
+
// server build
|
|
198
|
+
hook = function (context) {
|
|
199
|
+
// 2.3 injection
|
|
200
|
+
context =
|
|
201
|
+
context || // cached call
|
|
202
|
+
(this.$vnode && this.$vnode.ssrContext) || // stateful
|
|
203
|
+
(this.parent && this.parent.$vnode && this.parent.$vnode.ssrContext); // functional
|
|
204
|
+
// 2.2 with runInNewContext: true
|
|
205
|
+
if (!context && typeof __VUE_SSR_CONTEXT__ !== 'undefined') {
|
|
206
|
+
context = __VUE_SSR_CONTEXT__;
|
|
207
|
+
}
|
|
208
|
+
// inject component styles
|
|
209
|
+
if (style) {
|
|
210
|
+
style.call(this, createInjectorSSR(context));
|
|
211
|
+
}
|
|
212
|
+
// register component module identifier for async chunk inference
|
|
213
|
+
if (context && context._registeredComponents) {
|
|
214
|
+
context._registeredComponents.add(moduleIdentifier);
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
// used by ssr in case component is cached and beforeCreate
|
|
218
|
+
// never gets called
|
|
219
|
+
options._ssrRegister = hook;
|
|
220
|
+
}
|
|
221
|
+
else if (style) {
|
|
222
|
+
hook = shadowMode
|
|
223
|
+
? function (context) {
|
|
224
|
+
style.call(this, createInjectorShadow(context, this.$root.$options.shadowRoot));
|
|
225
|
+
}
|
|
226
|
+
: function (context) {
|
|
227
|
+
style.call(this, createInjector(context));
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
if (hook) {
|
|
231
|
+
if (options.functional) {
|
|
232
|
+
// register for functional component in vue file
|
|
233
|
+
const originalRender = options.render;
|
|
234
|
+
options.render = function renderWithStyleInjection(h, context) {
|
|
235
|
+
hook.call(context);
|
|
236
|
+
return originalRender(h, context);
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
else {
|
|
240
|
+
// inject component registration as beforeCreate hook
|
|
241
|
+
const existing = options.beforeCreate;
|
|
242
|
+
options.beforeCreate = existing ? [].concat(existing, hook) : [hook];
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return script;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/* script */
|
|
249
|
+
const __vue_script__$2 = script$2;
|
|
250
|
+
|
|
251
|
+
/* template */
|
|
252
|
+
var __vue_render__$2 = function () {
|
|
253
|
+
var _vm = this;
|
|
254
|
+
var _h = _vm.$createElement;
|
|
255
|
+
var _c = _vm._self._c || _h;
|
|
256
|
+
return _c(
|
|
257
|
+
"div",
|
|
258
|
+
{
|
|
259
|
+
staticClass: "hi-expand-text",
|
|
260
|
+
style: { "--hi-expand-text-line-clamp": _vm.lineClamp },
|
|
261
|
+
},
|
|
262
|
+
[
|
|
263
|
+
_c(
|
|
264
|
+
"div",
|
|
265
|
+
{ ref: "textRef", class: _vm.textClass },
|
|
266
|
+
[
|
|
267
|
+
_vm.showToggle
|
|
268
|
+
? _c(
|
|
269
|
+
"div",
|
|
270
|
+
{
|
|
271
|
+
staticClass: "hi-expand-text--toggle",
|
|
272
|
+
on: { click: _vm.handleToggle },
|
|
273
|
+
},
|
|
274
|
+
[
|
|
275
|
+
_vm._t("toggleText", function () {
|
|
276
|
+
return [_vm._v(_vm._s(_vm.expandText))]
|
|
277
|
+
}),
|
|
278
|
+
],
|
|
279
|
+
2
|
|
280
|
+
)
|
|
281
|
+
: _vm._e(),
|
|
282
|
+
_vm._v(" "),
|
|
283
|
+
_vm._t("default", function () {
|
|
284
|
+
return [_vm._v(_vm._s(_vm.content))]
|
|
285
|
+
}),
|
|
286
|
+
],
|
|
287
|
+
2
|
|
288
|
+
),
|
|
289
|
+
]
|
|
290
|
+
)
|
|
291
|
+
};
|
|
292
|
+
var __vue_staticRenderFns__$2 = [];
|
|
293
|
+
__vue_render__$2._withStripped = true;
|
|
294
|
+
|
|
295
|
+
/* style */
|
|
296
|
+
const __vue_inject_styles__$2 = undefined;
|
|
297
|
+
/* scoped */
|
|
298
|
+
const __vue_scope_id__$2 = undefined;
|
|
299
|
+
/* module identifier */
|
|
300
|
+
const __vue_module_identifier__$2 = undefined;
|
|
301
|
+
/* functional template */
|
|
302
|
+
const __vue_is_functional_template__$2 = false;
|
|
303
|
+
/* style inject */
|
|
304
|
+
|
|
305
|
+
/* style inject SSR */
|
|
306
|
+
|
|
307
|
+
/* style inject shadow dom */
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
const __vue_component__$2 = /*#__PURE__*/normalizeComponent$2(
|
|
312
|
+
{ render: __vue_render__$2, staticRenderFns: __vue_staticRenderFns__$2 },
|
|
313
|
+
__vue_inject_styles__$2,
|
|
314
|
+
__vue_script__$2,
|
|
315
|
+
__vue_scope_id__$2,
|
|
316
|
+
__vue_is_functional_template__$2,
|
|
317
|
+
__vue_module_identifier__$2,
|
|
318
|
+
false,
|
|
319
|
+
undefined,
|
|
320
|
+
undefined,
|
|
321
|
+
undefined
|
|
322
|
+
);
|
|
323
|
+
|
|
324
|
+
__vue_component__$2.name = 'HiExpandText';
|
|
325
|
+
|
|
326
|
+
// 添加 install
|
|
327
|
+
__vue_component__$2.install = Vue => {
|
|
328
|
+
Vue.component(__vue_component__$2.name, __vue_component__$2);
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
function __$styleInject$1(css) {
|
|
20
332
|
if (!css) return;
|
|
21
333
|
|
|
22
334
|
if (typeof window == 'undefined') return;
|
|
@@ -28,7 +340,7 @@
|
|
|
28
340
|
return css;
|
|
29
341
|
}
|
|
30
342
|
|
|
31
|
-
__$styleInject(".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");
|
|
343
|
+
__$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");
|
|
32
344
|
|
|
33
345
|
//
|
|
34
346
|
const TITILE_SIZE_MAP = {
|
|
@@ -36,7 +348,7 @@
|
|
|
36
348
|
medium: '16px',
|
|
37
349
|
large: '18px'
|
|
38
350
|
};
|
|
39
|
-
var script = {
|
|
351
|
+
var script$1 = {
|
|
40
352
|
name: 'HiTitle',
|
|
41
353
|
inheritAttrs: false,
|
|
42
354
|
props: {
|
|
@@ -115,7 +427,7 @@
|
|
|
115
427
|
}
|
|
116
428
|
};
|
|
117
429
|
|
|
118
|
-
function normalizeComponent(template, style, script, scopeId, isFunctionalTemplate, moduleIdentifier /* server only */, shadowMode, createInjector, createInjectorSSR, createInjectorShadow) {
|
|
430
|
+
function normalizeComponent$1(template, style, script, scopeId, isFunctionalTemplate, moduleIdentifier /* server only */, shadowMode, createInjector, createInjectorSSR, createInjectorShadow) {
|
|
119
431
|
if (typeof shadowMode !== 'boolean') {
|
|
120
432
|
createInjectorSSR = createInjector;
|
|
121
433
|
createInjector = shadowMode;
|
|
@@ -191,10 +503,10 @@
|
|
|
191
503
|
}
|
|
192
504
|
|
|
193
505
|
/* script */
|
|
194
|
-
const __vue_script__ = script;
|
|
506
|
+
const __vue_script__$1 = script$1;
|
|
195
507
|
|
|
196
508
|
/* template */
|
|
197
|
-
var __vue_render__ = function () {
|
|
509
|
+
var __vue_render__$1 = function () {
|
|
198
510
|
var _vm = this;
|
|
199
511
|
var _h = _vm.$createElement;
|
|
200
512
|
var _c = _vm._self._c || _h;
|
|
@@ -260,6 +572,435 @@
|
|
|
260
572
|
]
|
|
261
573
|
)
|
|
262
574
|
};
|
|
575
|
+
var __vue_staticRenderFns__$1 = [];
|
|
576
|
+
__vue_render__$1._withStripped = true;
|
|
577
|
+
|
|
578
|
+
/* style */
|
|
579
|
+
const __vue_inject_styles__$1 = undefined;
|
|
580
|
+
/* scoped */
|
|
581
|
+
const __vue_scope_id__$1 = undefined;
|
|
582
|
+
/* module identifier */
|
|
583
|
+
const __vue_module_identifier__$1 = undefined;
|
|
584
|
+
/* functional template */
|
|
585
|
+
const __vue_is_functional_template__$1 = false;
|
|
586
|
+
/* style inject */
|
|
587
|
+
|
|
588
|
+
/* style inject SSR */
|
|
589
|
+
|
|
590
|
+
/* style inject shadow dom */
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
const __vue_component__$1 = /*#__PURE__*/normalizeComponent$1(
|
|
595
|
+
{ render: __vue_render__$1, staticRenderFns: __vue_staticRenderFns__$1 },
|
|
596
|
+
__vue_inject_styles__$1,
|
|
597
|
+
__vue_script__$1,
|
|
598
|
+
__vue_scope_id__$1,
|
|
599
|
+
__vue_is_functional_template__$1,
|
|
600
|
+
__vue_module_identifier__$1,
|
|
601
|
+
false,
|
|
602
|
+
undefined,
|
|
603
|
+
undefined,
|
|
604
|
+
undefined
|
|
605
|
+
);
|
|
606
|
+
|
|
607
|
+
// 显式设置 name(避免 .vue 丢失)
|
|
608
|
+
__vue_component__$1.name = 'HiTitle';
|
|
609
|
+
|
|
610
|
+
// 添加 install
|
|
611
|
+
__vue_component__$1.install = Vue => {
|
|
612
|
+
Vue.component(__vue_component__$1.name, __vue_component__$1);
|
|
613
|
+
};
|
|
614
|
+
|
|
615
|
+
function __$styleInject(css) {
|
|
616
|
+
if (!css) return;
|
|
617
|
+
|
|
618
|
+
if (typeof window == 'undefined') return;
|
|
619
|
+
var style = document.createElement('style');
|
|
620
|
+
style.setAttribute('media', 'screen');
|
|
621
|
+
|
|
622
|
+
style.innerHTML = css;
|
|
623
|
+
document.head.appendChild(style);
|
|
624
|
+
return css;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
__$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");
|
|
628
|
+
|
|
629
|
+
/**
|
|
630
|
+
* 类型判断工具
|
|
631
|
+
*/
|
|
632
|
+
|
|
633
|
+
/**
|
|
634
|
+
* 将数值或字符串转换为合法的 CSS 长度值(用于 style 绑定)
|
|
635
|
+
* - 若为 number,自动追加 'px' 单位
|
|
636
|
+
* - 若为 string,原样返回(假定用户已提供合法 CSS 长度值)
|
|
637
|
+
* @param {number|string} value - 输入值(如 100, '100%', '50vh', 'auto')
|
|
638
|
+
* @returns {string} 合法的 CSS 长度字符串 默认值为 0
|
|
639
|
+
* @example
|
|
640
|
+
* formatSize(100) // '100px'
|
|
641
|
+
* formatSize('100%') // '100%'
|
|
642
|
+
* formatSize('50vh') // '50vh'
|
|
643
|
+
* formatSize(null) // 0
|
|
644
|
+
* formatSize(undefined) // 0
|
|
645
|
+
*/
|
|
646
|
+
function formatSize(value) {
|
|
647
|
+
if (typeof value === 'number') {
|
|
648
|
+
return `${value}px`;
|
|
649
|
+
}
|
|
650
|
+
if (typeof value === 'string') {
|
|
651
|
+
if (value.trim() === '') {
|
|
652
|
+
return 0;
|
|
653
|
+
}
|
|
654
|
+
return value;
|
|
655
|
+
}
|
|
656
|
+
return 0;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
//
|
|
660
|
+
|
|
661
|
+
var script = {
|
|
662
|
+
name: 'HiVirtualList',
|
|
663
|
+
props: {
|
|
664
|
+
items: {
|
|
665
|
+
type: Array,
|
|
666
|
+
required: true,
|
|
667
|
+
default: () => []
|
|
668
|
+
},
|
|
669
|
+
itemHeight: {
|
|
670
|
+
type: Number,
|
|
671
|
+
required: true,
|
|
672
|
+
validator(value) {
|
|
673
|
+
if (value <= 0) {
|
|
674
|
+
// eslint-disable-next-line no-console
|
|
675
|
+
console.error('[HiVirtualList] itemHeight must be a positive number.');
|
|
676
|
+
return false;
|
|
677
|
+
}
|
|
678
|
+
return true;
|
|
679
|
+
}
|
|
680
|
+
},
|
|
681
|
+
height: {
|
|
682
|
+
type: [Number, String],
|
|
683
|
+
required: true
|
|
684
|
+
},
|
|
685
|
+
buffer: {
|
|
686
|
+
type: Number,
|
|
687
|
+
default: 50
|
|
688
|
+
},
|
|
689
|
+
nodeKey: {
|
|
690
|
+
type: String,
|
|
691
|
+
default: undefined
|
|
692
|
+
}
|
|
693
|
+
},
|
|
694
|
+
data() {
|
|
695
|
+
return {
|
|
696
|
+
scrollTop: 0,
|
|
697
|
+
clientHeight: 0,
|
|
698
|
+
resizeObserver: null,
|
|
699
|
+
resizeTimer: null
|
|
700
|
+
};
|
|
701
|
+
},
|
|
702
|
+
computed: {
|
|
703
|
+
styles() {
|
|
704
|
+
return {
|
|
705
|
+
height: formatSize(this.height)
|
|
706
|
+
};
|
|
707
|
+
},
|
|
708
|
+
renderItemHeight() {
|
|
709
|
+
return formatSize(this.itemHeight);
|
|
710
|
+
},
|
|
711
|
+
total() {
|
|
712
|
+
return this.items.length;
|
|
713
|
+
},
|
|
714
|
+
visibleCount() {
|
|
715
|
+
if (!this.clientHeight || !this.itemHeight) return 20;
|
|
716
|
+
return Math.ceil(this.clientHeight / this.itemHeight) + this.buffer * 2;
|
|
717
|
+
},
|
|
718
|
+
startIndex() {
|
|
719
|
+
return Math.max(0, Math.floor(this.scrollTop / this.itemHeight));
|
|
720
|
+
},
|
|
721
|
+
endIndex() {
|
|
722
|
+
return Math.min(this.total, this.startIndex + this.visibleCount);
|
|
723
|
+
},
|
|
724
|
+
visibleItems() {
|
|
725
|
+
return this.items.slice(this.startIndex, this.endIndex);
|
|
726
|
+
},
|
|
727
|
+
topPlaceholderHeight() {
|
|
728
|
+
return formatSize(this.startIndex * this.itemHeight);
|
|
729
|
+
},
|
|
730
|
+
bottomPlaceholderHeight() {
|
|
731
|
+
return formatSize((this.total - this.endIndex) * this.itemHeight);
|
|
732
|
+
}
|
|
733
|
+
},
|
|
734
|
+
mounted() {
|
|
735
|
+
this.updateClientHeight();
|
|
736
|
+
this.setupResizeListener();
|
|
737
|
+
},
|
|
738
|
+
beforeDestroy() {
|
|
739
|
+
this.cleanupResizeListener();
|
|
740
|
+
},
|
|
741
|
+
methods: {
|
|
742
|
+
/**
|
|
743
|
+
* 判断是否应使用 nodeKey 字段
|
|
744
|
+
*/
|
|
745
|
+
_shouldUseNodeKey() {
|
|
746
|
+
return this.nodeKey && typeof this.nodeKey === 'string' && this.nodeKey.trim() !== '';
|
|
747
|
+
},
|
|
748
|
+
|
|
749
|
+
/**
|
|
750
|
+
* 获取 item 的唯一 key(用于 v-for)
|
|
751
|
+
*/
|
|
752
|
+
getNodeKey(item, index) {
|
|
753
|
+
if (this._shouldUseNodeKey() && item != null && typeof item === 'object') {
|
|
754
|
+
const key = item[this.nodeKey];
|
|
755
|
+
if (key != null) {
|
|
756
|
+
return key;
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
return index;
|
|
760
|
+
},
|
|
761
|
+
|
|
762
|
+
updateClientHeight() {
|
|
763
|
+
if (this.$refs.rootRef) {
|
|
764
|
+
this.clientHeight = this.$refs.rootRef.clientHeight;
|
|
765
|
+
}
|
|
766
|
+
},
|
|
767
|
+
|
|
768
|
+
setupResizeListener() {
|
|
769
|
+
if (typeof ResizeObserver !== 'undefined') {
|
|
770
|
+
this.resizeObserver = new ResizeObserver(() => {
|
|
771
|
+
this.updateClientHeight();
|
|
772
|
+
});
|
|
773
|
+
this.resizeObserver.observe(this.$refs.rootRef);
|
|
774
|
+
} else {
|
|
775
|
+
window.addEventListener('resize', this.handleWindowResize);
|
|
776
|
+
}
|
|
777
|
+
},
|
|
778
|
+
|
|
779
|
+
cleanupResizeListener() {
|
|
780
|
+
if (this.resizeObserver) {
|
|
781
|
+
this.resizeObserver.disconnect();
|
|
782
|
+
this.resizeObserver = null;
|
|
783
|
+
}
|
|
784
|
+
if (this.resizeTimer) {
|
|
785
|
+
clearTimeout(this.resizeTimer);
|
|
786
|
+
this.resizeTimer = null;
|
|
787
|
+
}
|
|
788
|
+
window.removeEventListener('resize', this.handleWindowResize);
|
|
789
|
+
},
|
|
790
|
+
|
|
791
|
+
handleWindowResize() {
|
|
792
|
+
if (this.resizeTimer) {
|
|
793
|
+
clearTimeout(this.resizeTimer);
|
|
794
|
+
}
|
|
795
|
+
this.resizeTimer = setTimeout(() => {
|
|
796
|
+
this.updateClientHeight();
|
|
797
|
+
}, 100);
|
|
798
|
+
},
|
|
799
|
+
|
|
800
|
+
handleScroll(e) {
|
|
801
|
+
this.scrollTop = e.target.scrollTop;
|
|
802
|
+
this.$emit('scroll', e);
|
|
803
|
+
|
|
804
|
+
const { scrollHeight, clientHeight, scrollTop } = e.target;
|
|
805
|
+
if (scrollTop === 0) this.$emit('reach-top');
|
|
806
|
+
if (scrollHeight - clientHeight - scrollTop < 100) this.$emit('reach-bottom');
|
|
807
|
+
this.$emit('visible-change', { startIndex: this.startIndex, endIndex: this.endIndex });
|
|
808
|
+
},
|
|
809
|
+
|
|
810
|
+
refresh() {
|
|
811
|
+
this.updateClientHeight();
|
|
812
|
+
},
|
|
813
|
+
|
|
814
|
+
/**
|
|
815
|
+
* 内部滚动方法(仅设置 DOM scrollTop,状态由 handleScroll 同步)
|
|
816
|
+
*/
|
|
817
|
+
_setScrollTop(scrollTop) {
|
|
818
|
+
if (this.$refs.rootRef) {
|
|
819
|
+
this.$refs.rootRef.scrollTop = scrollTop;
|
|
820
|
+
}
|
|
821
|
+
},
|
|
822
|
+
|
|
823
|
+
/**
|
|
824
|
+
* 滚动到指定目标
|
|
825
|
+
* - 若启用了有效的 nodeKey,则 target 视为 keyValue
|
|
826
|
+
* - 否则,target 必须是 number(index)
|
|
827
|
+
*/
|
|
828
|
+
scrollTo(target) {
|
|
829
|
+
if (target == null) {
|
|
830
|
+
// eslint-disable-next-line no-console
|
|
831
|
+
console.warn('[HiVirtualList] scrollTo: target cannot be null or undefined');
|
|
832
|
+
return;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
let index = -1;
|
|
836
|
+
|
|
837
|
+
// 使用 getNodeKey 生成的 key 进行匹配(严格对齐)
|
|
838
|
+
if (this._shouldUseNodeKey()) {
|
|
839
|
+
index = this.items.findIndex((item, i) => {
|
|
840
|
+
return this.getNodeKey(item, i) === target;
|
|
841
|
+
});
|
|
842
|
+
} else {
|
|
843
|
+
// 降级到 index 模式
|
|
844
|
+
if (typeof target === 'number' && target >= 0 && target < this.total) {
|
|
845
|
+
index = target;
|
|
846
|
+
} else {
|
|
847
|
+
// eslint-disable-next-line no-console
|
|
848
|
+
console.warn(
|
|
849
|
+
'[HiVirtualList] scrollTo: when nodeKey is not provided, target must be a valid index (number).'
|
|
850
|
+
);
|
|
851
|
+
return;
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
if (index < 0 || index >= this.total) {
|
|
856
|
+
if (this._shouldUseNodeKey()) {
|
|
857
|
+
// eslint-disable-next-line no-console
|
|
858
|
+
console.warn(`[HiVirtualList] scrollTo: item with key "${target}" not found`);
|
|
859
|
+
} else {
|
|
860
|
+
// eslint-disable-next-line no-console
|
|
861
|
+
console.warn(`[HiVirtualList] scrollTo: index ${target} is out of range [0, ${this.total})`);
|
|
862
|
+
}
|
|
863
|
+
return;
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
this._setScrollTop(index * this.itemHeight);
|
|
867
|
+
},
|
|
868
|
+
|
|
869
|
+
scrollToTop() {
|
|
870
|
+
this._setScrollTop(0);
|
|
871
|
+
},
|
|
872
|
+
|
|
873
|
+
scrollToBottom() {
|
|
874
|
+
this._setScrollTop(this.total * this.itemHeight);
|
|
875
|
+
},
|
|
876
|
+
|
|
877
|
+
getVisibleRange() {
|
|
878
|
+
return {
|
|
879
|
+
startIndex: this.startIndex,
|
|
880
|
+
endIndex: this.endIndex
|
|
881
|
+
};
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
};
|
|
885
|
+
|
|
886
|
+
function normalizeComponent(template, style, script, scopeId, isFunctionalTemplate, moduleIdentifier /* server only */, shadowMode, createInjector, createInjectorSSR, createInjectorShadow) {
|
|
887
|
+
if (typeof shadowMode !== 'boolean') {
|
|
888
|
+
createInjectorSSR = createInjector;
|
|
889
|
+
createInjector = shadowMode;
|
|
890
|
+
shadowMode = false;
|
|
891
|
+
}
|
|
892
|
+
// Vue.extend constructor export interop.
|
|
893
|
+
const options = typeof script === 'function' ? script.options : script;
|
|
894
|
+
// render functions
|
|
895
|
+
if (template && template.render) {
|
|
896
|
+
options.render = template.render;
|
|
897
|
+
options.staticRenderFns = template.staticRenderFns;
|
|
898
|
+
options._compiled = true;
|
|
899
|
+
// functional template
|
|
900
|
+
if (isFunctionalTemplate) {
|
|
901
|
+
options.functional = true;
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
// scopedId
|
|
905
|
+
if (scopeId) {
|
|
906
|
+
options._scopeId = scopeId;
|
|
907
|
+
}
|
|
908
|
+
let hook;
|
|
909
|
+
if (moduleIdentifier) {
|
|
910
|
+
// server build
|
|
911
|
+
hook = function (context) {
|
|
912
|
+
// 2.3 injection
|
|
913
|
+
context =
|
|
914
|
+
context || // cached call
|
|
915
|
+
(this.$vnode && this.$vnode.ssrContext) || // stateful
|
|
916
|
+
(this.parent && this.parent.$vnode && this.parent.$vnode.ssrContext); // functional
|
|
917
|
+
// 2.2 with runInNewContext: true
|
|
918
|
+
if (!context && typeof __VUE_SSR_CONTEXT__ !== 'undefined') {
|
|
919
|
+
context = __VUE_SSR_CONTEXT__;
|
|
920
|
+
}
|
|
921
|
+
// inject component styles
|
|
922
|
+
if (style) {
|
|
923
|
+
style.call(this, createInjectorSSR(context));
|
|
924
|
+
}
|
|
925
|
+
// register component module identifier for async chunk inference
|
|
926
|
+
if (context && context._registeredComponents) {
|
|
927
|
+
context._registeredComponents.add(moduleIdentifier);
|
|
928
|
+
}
|
|
929
|
+
};
|
|
930
|
+
// used by ssr in case component is cached and beforeCreate
|
|
931
|
+
// never gets called
|
|
932
|
+
options._ssrRegister = hook;
|
|
933
|
+
}
|
|
934
|
+
else if (style) {
|
|
935
|
+
hook = shadowMode
|
|
936
|
+
? function (context) {
|
|
937
|
+
style.call(this, createInjectorShadow(context, this.$root.$options.shadowRoot));
|
|
938
|
+
}
|
|
939
|
+
: function (context) {
|
|
940
|
+
style.call(this, createInjector(context));
|
|
941
|
+
};
|
|
942
|
+
}
|
|
943
|
+
if (hook) {
|
|
944
|
+
if (options.functional) {
|
|
945
|
+
// register for functional component in vue file
|
|
946
|
+
const originalRender = options.render;
|
|
947
|
+
options.render = function renderWithStyleInjection(h, context) {
|
|
948
|
+
hook.call(context);
|
|
949
|
+
return originalRender(h, context);
|
|
950
|
+
};
|
|
951
|
+
}
|
|
952
|
+
else {
|
|
953
|
+
// inject component registration as beforeCreate hook
|
|
954
|
+
const existing = options.beforeCreate;
|
|
955
|
+
options.beforeCreate = existing ? [].concat(existing, hook) : [hook];
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
return script;
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
/* script */
|
|
962
|
+
const __vue_script__ = script;
|
|
963
|
+
|
|
964
|
+
/* template */
|
|
965
|
+
var __vue_render__ = function () {
|
|
966
|
+
var _vm = this;
|
|
967
|
+
var _h = _vm.$createElement;
|
|
968
|
+
var _c = _vm._self._c || _h;
|
|
969
|
+
return _c(
|
|
970
|
+
"div",
|
|
971
|
+
{
|
|
972
|
+
ref: "rootRef",
|
|
973
|
+
staticClass: "hi-virtual-list",
|
|
974
|
+
style: _vm.styles,
|
|
975
|
+
on: { scroll: _vm.handleScroll },
|
|
976
|
+
},
|
|
977
|
+
[
|
|
978
|
+
_c("div", {
|
|
979
|
+
staticClass: "hi-virtual-list--placeholder",
|
|
980
|
+
style: { height: _vm.topPlaceholderHeight },
|
|
981
|
+
}),
|
|
982
|
+
_vm._v(" "),
|
|
983
|
+
_vm._l(_vm.visibleItems, function (item, i) {
|
|
984
|
+
return _c(
|
|
985
|
+
"div",
|
|
986
|
+
{
|
|
987
|
+
key: _vm.getNodeKey(item, _vm.startIndex + i),
|
|
988
|
+
staticClass: "hi-virtual-list--item-wrapper",
|
|
989
|
+
style: { height: _vm.renderItemHeight },
|
|
990
|
+
},
|
|
991
|
+
[_vm._t("default", null, { item: item, index: _vm.startIndex + i })],
|
|
992
|
+
2
|
|
993
|
+
)
|
|
994
|
+
}),
|
|
995
|
+
_vm._v(" "),
|
|
996
|
+
_c("div", {
|
|
997
|
+
staticClass: "hi-virtual-list--placeholder",
|
|
998
|
+
style: { height: _vm.bottomPlaceholderHeight },
|
|
999
|
+
}),
|
|
1000
|
+
],
|
|
1001
|
+
2
|
|
1002
|
+
)
|
|
1003
|
+
};
|
|
263
1004
|
var __vue_staticRenderFns__ = [];
|
|
264
1005
|
__vue_render__._withStripped = true;
|
|
265
1006
|
|
|
@@ -293,14 +1034,19 @@
|
|
|
293
1034
|
);
|
|
294
1035
|
|
|
295
1036
|
// 显式设置 name(避免 .vue 丢失)
|
|
296
|
-
__vue_component__.name = '
|
|
1037
|
+
__vue_component__.name = 'HiVirtualList';
|
|
297
1038
|
|
|
298
1039
|
// 添加 install
|
|
299
1040
|
__vue_component__.install = Vue => {
|
|
300
1041
|
Vue.component(__vue_component__.name, __vue_component__);
|
|
301
1042
|
};
|
|
302
1043
|
|
|
303
|
-
|
|
1044
|
+
// eslint-disable-next-line prettier/prettier
|
|
1045
|
+
const components = [
|
|
1046
|
+
__vue_component__$2,
|
|
1047
|
+
__vue_component__$1,
|
|
1048
|
+
__vue_component__
|
|
1049
|
+
];
|
|
304
1050
|
|
|
305
1051
|
const install = function (Vue) {
|
|
306
1052
|
components.forEach(component => {
|
|
@@ -308,7 +1054,12 @@
|
|
|
308
1054
|
});
|
|
309
1055
|
};
|
|
310
1056
|
|
|
311
|
-
var index = {
|
|
1057
|
+
var index = {
|
|
1058
|
+
install,
|
|
1059
|
+
HiExpandText: __vue_component__$2,
|
|
1060
|
+
HiTitle: __vue_component__$1,
|
|
1061
|
+
HiVirtualList: __vue_component__
|
|
1062
|
+
};
|
|
312
1063
|
|
|
313
1064
|
return index;
|
|
314
1065
|
|