arms-app 1.0.68 → 1.0.70
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/package.json +1 -1
- package/view/{5.vue → index.vue} +18 -47
- package/view/{4.vue → index2.vue} +29 -20
- package/view/1.js +0 -23
- package/view/1.vue +0 -117
- package/view/111.js +0 -35
- package/view/2.js +0 -90
- package/view/2.vue +0 -129
- package/view/3.js +0 -289
- package/view/3.vue +0 -289
- package/view/555.vue +0 -196
- package/view/CallRecordDetail.vue +0 -101
- package/view/ListedCompaniesView copy.vue +0 -238
- package/view/ListedCompaniesView.vue +0 -224
- package/view/ListedEnterprisesView.vue +0 -206
- package/view/ListedIncrementalEnterprisesView.vue +0 -195
- package/view/index.html +0 -51
- package/view//345/205/250/345/261/217.png +0 -0
- package/view//351/200/200/345/207/272/345/205/250/345/261/217.png +0 -0
package/view/3.js
DELETED
|
@@ -1,289 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 在树形结构中通过ID查找对象
|
|
3
|
-
* @param {Array} treeData - 树形结构数组
|
|
4
|
-
* @param {string|number} targetId - 要查找的目标ID
|
|
5
|
-
* @returns {Object|null} 找到的对象或null
|
|
6
|
-
*/
|
|
7
|
-
function findNodeById(treeData, targetId) {
|
|
8
|
-
for (let node of treeData) {
|
|
9
|
-
// 检查当前节点是否匹配目标ID
|
|
10
|
-
if (node.id === targetId) {
|
|
11
|
-
return node;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
// 只有当children存在且为非空数组时才递归查找
|
|
15
|
-
if (node.children && Array.isArray(node.children) && node.children.length > 0) {
|
|
16
|
-
const found = findNodeById(node.children, targetId);
|
|
17
|
-
if (found) {
|
|
18
|
-
return found;
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
return null;
|
|
23
|
-
}
|
|
24
|
-
// 复杂的电商系统权限树 - 模拟真实业务场景
|
|
25
|
-
const complexBusinessData = [
|
|
26
|
-
{
|
|
27
|
-
id: 1,
|
|
28
|
-
name: "系统总览",
|
|
29
|
-
code: "dashboard",
|
|
30
|
-
type: "menu",
|
|
31
|
-
icon: "dashboard",
|
|
32
|
-
children: []
|
|
33
|
-
},
|
|
34
|
-
|
|
35
|
-
{
|
|
36
|
-
id: 2,
|
|
37
|
-
name: "商品管理",
|
|
38
|
-
code: "product",
|
|
39
|
-
type: "module",
|
|
40
|
-
icon: "shopping-bag",
|
|
41
|
-
children: [
|
|
42
|
-
{
|
|
43
|
-
id: 3,
|
|
44
|
-
name: "商品基础信息",
|
|
45
|
-
code: "product_base",
|
|
46
|
-
type: "menu",
|
|
47
|
-
children: [
|
|
48
|
-
{
|
|
49
|
-
id: 4,
|
|
50
|
-
name: "商品列表",
|
|
51
|
-
code: "product_list",
|
|
52
|
-
type: "page",
|
|
53
|
-
children: [
|
|
54
|
-
{ id: 5, name: "查看商品", code: "view_product", type: "permission", action: ["read"] },
|
|
55
|
-
{ id: 6, name: "添加商品", code: "add_product", type: "permission", action: ["create"] },
|
|
56
|
-
{ id: 7, name: "编辑商品", code: "edit_product", type: "permission", action: ["update"] },
|
|
57
|
-
{ id: 8, name: "删除商品", code: "delete_product", type: "permission", action: ["delete"] }
|
|
58
|
-
]
|
|
59
|
-
},
|
|
60
|
-
{
|
|
61
|
-
id: 9,
|
|
62
|
-
name: "商品分类",
|
|
63
|
-
code: "product_category",
|
|
64
|
-
type: "page",
|
|
65
|
-
children: [
|
|
66
|
-
{ id: 10, name: "查看分类", code: "view_category", type: "permission", action: ["read"] },
|
|
67
|
-
{ id: 11, name: "管理分类", code: "manage_category", type: "permission", action: ["create", "update", "delete"] }
|
|
68
|
-
]
|
|
69
|
-
},
|
|
70
|
-
{
|
|
71
|
-
id: 12,
|
|
72
|
-
name: "品牌管理",
|
|
73
|
-
code: "brand_management",
|
|
74
|
-
type: "page",
|
|
75
|
-
children: [
|
|
76
|
-
{ id: 13, name: "查看品牌", code: "view_brand", type: "permission" },
|
|
77
|
-
{ id: 14, name: "品牌审核", code: "audit_brand", type: "permission" }
|
|
78
|
-
]
|
|
79
|
-
}
|
|
80
|
-
]
|
|
81
|
-
},
|
|
82
|
-
|
|
83
|
-
{
|
|
84
|
-
id: 15,
|
|
85
|
-
name: "库存管理",
|
|
86
|
-
code: "inventory",
|
|
87
|
-
type: "menu",
|
|
88
|
-
children: [
|
|
89
|
-
{
|
|
90
|
-
id: 16,
|
|
91
|
-
name: "仓库管理",
|
|
92
|
-
code: "warehouse",
|
|
93
|
-
type: "page",
|
|
94
|
-
children: [
|
|
95
|
-
{ id: 17, name: "查看仓库", code: "view_warehouse", type: "permission" },
|
|
96
|
-
{ id: 18, name: "管理仓库", code: "manage_warehouse", type: "permission" }
|
|
97
|
-
]
|
|
98
|
-
},
|
|
99
|
-
{
|
|
100
|
-
id: 19,
|
|
101
|
-
name: "库存调拨",
|
|
102
|
-
code: "stock_transfer",
|
|
103
|
-
type: "page",
|
|
104
|
-
children: [
|
|
105
|
-
{ id: 20, name: "发起调拨", code: "initiate_transfer", type: "permission" },
|
|
106
|
-
{ id: 21, name: "审批调拨", code: "approve_transfer", type: "permission" },
|
|
107
|
-
{ id: 22, name: "执行调拨", code: "execute_transfer", type: "permission" }
|
|
108
|
-
]
|
|
109
|
-
},
|
|
110
|
-
{
|
|
111
|
-
id: 23,
|
|
112
|
-
name: "盘点管理",
|
|
113
|
-
code: "inventory_count",
|
|
114
|
-
type: "page",
|
|
115
|
-
children: [
|
|
116
|
-
{ id: 24, name: "创建盘点", code: "create_count", type: "permission" },
|
|
117
|
-
{ id: 25, name: "盘点审核", code: "audit_count", type: "permission" },
|
|
118
|
-
{ id: 26, name: "盘点报表", code: "count_report", type: "permission" }
|
|
119
|
-
]
|
|
120
|
-
}
|
|
121
|
-
]
|
|
122
|
-
},
|
|
123
|
-
|
|
124
|
-
{
|
|
125
|
-
id: 27,
|
|
126
|
-
name: "价格策略",
|
|
127
|
-
code: "pricing",
|
|
128
|
-
type: "menu",
|
|
129
|
-
children: [
|
|
130
|
-
{
|
|
131
|
-
id: 28,
|
|
132
|
-
name: "促销活动",
|
|
133
|
-
code: "promotion",
|
|
134
|
-
type: "page",
|
|
135
|
-
children: [
|
|
136
|
-
{ id: 29, name: "限时折扣", code: "flash_sale", type: "permission" },
|
|
137
|
-
{ id: 30, name: "满减活动", code: "full_reduction", type: "permission" },
|
|
138
|
-
{ id: 31, name: "优惠券", code: "coupon", type: "permission" }
|
|
139
|
-
]
|
|
140
|
-
}
|
|
141
|
-
]
|
|
142
|
-
}
|
|
143
|
-
]
|
|
144
|
-
},
|
|
145
|
-
|
|
146
|
-
{
|
|
147
|
-
id: 32,
|
|
148
|
-
name: "订单管理",
|
|
149
|
-
code: "order",
|
|
150
|
-
type: "module",
|
|
151
|
-
children: [
|
|
152
|
-
{
|
|
153
|
-
id: 33,
|
|
154
|
-
name: "订单处理",
|
|
155
|
-
code: "order_processing",
|
|
156
|
-
type: "menu",
|
|
157
|
-
children: [
|
|
158
|
-
{
|
|
159
|
-
id: 34,
|
|
160
|
-
name: "订单查询",
|
|
161
|
-
code: "order_query",
|
|
162
|
-
type: "page",
|
|
163
|
-
children: [
|
|
164
|
-
{ id: 35, name: "今日订单", code: "today_order", type: "permission" },
|
|
165
|
-
{ id: 36, name: "历史订单", code: "history_order", type: "permission" },
|
|
166
|
-
{ id: 37, name: "异常订单", code: "abnormal_order", type: "permission" }
|
|
167
|
-
]
|
|
168
|
-
},
|
|
169
|
-
{
|
|
170
|
-
id: 38,
|
|
171
|
-
name: "订单操作",
|
|
172
|
-
code: "order_operation",
|
|
173
|
-
type: "page",
|
|
174
|
-
children: [
|
|
175
|
-
{ id: 39, name: "确认订单", code: "confirm_order", type: "permission" },
|
|
176
|
-
{ id: 40, name: "取消订单", code: "cancel_order", type: "permission" },
|
|
177
|
-
{ id: 41, name: "退款处理", code: "refund_order", type: "permission" }
|
|
178
|
-
]
|
|
179
|
-
}
|
|
180
|
-
]
|
|
181
|
-
},
|
|
182
|
-
|
|
183
|
-
{
|
|
184
|
-
id: 42,
|
|
185
|
-
name: "物流配送",
|
|
186
|
-
code: "logistics",
|
|
187
|
-
type: "menu",
|
|
188
|
-
children: [
|
|
189
|
-
{
|
|
190
|
-
id: 43,
|
|
191
|
-
name: "配送管理",
|
|
192
|
-
code: "delivery",
|
|
193
|
-
type: "page",
|
|
194
|
-
children: [
|
|
195
|
-
{ id: 44, name: "发货管理", code: "shipping", type: "permission" },
|
|
196
|
-
{ id: 45, name: "快递跟踪", code: "tracking", type: "permission" },
|
|
197
|
-
{ id: 46, name: "签收确认", code: "sign_confirm", type: "permission" }
|
|
198
|
-
]
|
|
199
|
-
}
|
|
200
|
-
]
|
|
201
|
-
}
|
|
202
|
-
]
|
|
203
|
-
},
|
|
204
|
-
|
|
205
|
-
{
|
|
206
|
-
id: 47,
|
|
207
|
-
name: "用户中心",
|
|
208
|
-
code: "user",
|
|
209
|
-
type: "module",
|
|
210
|
-
children: [
|
|
211
|
-
{
|
|
212
|
-
id: 48,
|
|
213
|
-
name: "会员管理",
|
|
214
|
-
code: "member",
|
|
215
|
-
type: "menu",
|
|
216
|
-
children: [
|
|
217
|
-
{
|
|
218
|
-
id: 49,
|
|
219
|
-
name: "会员信息",
|
|
220
|
-
code: "member_info",
|
|
221
|
-
type: "page",
|
|
222
|
-
children: [
|
|
223
|
-
{ id: 50, name: "会员列表", code: "member_list", type: "permission" },
|
|
224
|
-
{ id: 51, name: "会员等级", code: "member_level", type: "permission" },
|
|
225
|
-
{ id: 52, name: "积分管理", code: "points_manage", type: "permission" }
|
|
226
|
-
]
|
|
227
|
-
},
|
|
228
|
-
{
|
|
229
|
-
id: 53,
|
|
230
|
-
name: "用户行为",
|
|
231
|
-
code: "user_behavior",
|
|
232
|
-
type: "page",
|
|
233
|
-
children: [
|
|
234
|
-
{ id: 54, name: "浏览记录", code: "browse_history", type: "permission" },
|
|
235
|
-
{ id: 55, name: "购买偏好", code: "purchase_preference", type: "permission" }
|
|
236
|
-
]
|
|
237
|
-
}
|
|
238
|
-
]
|
|
239
|
-
}
|
|
240
|
-
]
|
|
241
|
-
},
|
|
242
|
-
|
|
243
|
-
{
|
|
244
|
-
id: 56,
|
|
245
|
-
name: "数据中心",
|
|
246
|
-
code: "analytics",
|
|
247
|
-
type: "module",
|
|
248
|
-
children: [
|
|
249
|
-
{
|
|
250
|
-
id: 57,
|
|
251
|
-
name: "报表统计",
|
|
252
|
-
code: "reports",
|
|
253
|
-
type: "menu",
|
|
254
|
-
children: [
|
|
255
|
-
{
|
|
256
|
-
id: 58,
|
|
257
|
-
name: "销售报表",
|
|
258
|
-
code: "sales_report",
|
|
259
|
-
type: "page",
|
|
260
|
-
children: [
|
|
261
|
-
{ id: 59, name: "日销售报表", code: "daily_sales", type: "permission" },
|
|
262
|
-
{ id: 60, name: "月销售报表", code: "monthly_sales", type: "permission" },
|
|
263
|
-
{ id: 61, name: "年销售报表", code: "yearly_sales", type: "permission" }
|
|
264
|
-
]
|
|
265
|
-
},
|
|
266
|
-
{
|
|
267
|
-
id: 62,
|
|
268
|
-
name: "运营报表",
|
|
269
|
-
code: "operation_report",
|
|
270
|
-
type: "page",
|
|
271
|
-
children: [
|
|
272
|
-
{ id: 63, name: "流量分析", code: "traffic_analysis", type: "permission" },
|
|
273
|
-
{ id: 64, name: "转化分析", code: "conversion_analysis", type: "permission" },
|
|
274
|
-
{ id: 63, name: "用户留存", code: "user_retention", type: "permission" } // 故意重复的ID用于测试
|
|
275
|
-
]
|
|
276
|
-
}
|
|
277
|
-
]
|
|
278
|
-
}
|
|
279
|
-
]
|
|
280
|
-
}
|
|
281
|
-
];
|
|
282
|
-
|
|
283
|
-
console.log(findNodeById(complexBusinessData, 32));
|
|
284
|
-
console.log('================')
|
|
285
|
-
console.log(findNodeById(complexBusinessData, 33));
|
|
286
|
-
console.log('================')
|
|
287
|
-
console.log(findNodeById(complexBusinessData, 34));
|
|
288
|
-
console.log('================')
|
|
289
|
-
console.log(findNodeById(complexBusinessData, 35));
|
package/view/3.vue
DELETED
|
@@ -1,289 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<section class="page">
|
|
3
|
-
<h3>虚拟滚动(DOM 池 + translateY)</h3>
|
|
4
|
-
|
|
5
|
-
<div
|
|
6
|
-
class="viewport"
|
|
7
|
-
:style="{ height: viewportHeight + 'px' }"
|
|
8
|
-
@wheel.prevent="onWheel"
|
|
9
|
-
@mouseenter="hover = true"
|
|
10
|
-
@mouseleave="hover = false"
|
|
11
|
-
>
|
|
12
|
-
<!-- 真实渲染的少量行(可视行 + buffer),DOM 数量不会随全量数据增长 -->
|
|
13
|
-
<div class="track" :style="{ transform: `translateY(${-offset}px)` }">
|
|
14
|
-
<div
|
|
15
|
-
v-for="row in pool"
|
|
16
|
-
:key="row.__k"
|
|
17
|
-
class="row"
|
|
18
|
-
:style="{ height: rowHeight + 'px', lineHeight: rowHeight + 'px' }"
|
|
19
|
-
>
|
|
20
|
-
<span class="cell">{{ row.name }}</span>
|
|
21
|
-
<span class="cell">{{ row.score }}</span>
|
|
22
|
-
</div>
|
|
23
|
-
</div>
|
|
24
|
-
</div>
|
|
25
|
-
</section>
|
|
26
|
-
</template>
|
|
27
|
-
|
|
28
|
-
<script>
|
|
29
|
-
// 虚拟滚动示例:通过「固定数量的 DOM 池 + translateY」实现高性能长列表展示
|
|
30
|
-
export default {
|
|
31
|
-
// 组件名称,用于路由懒加载和调试
|
|
32
|
-
name: "VirtualPoolScrollView",
|
|
33
|
-
props: {
|
|
34
|
-
// 备用的全量数据(演示时可通过 props 直接传入),实际项目通常来自接口
|
|
35
|
-
list: {
|
|
36
|
-
type: Array,
|
|
37
|
-
default: () => [],
|
|
38
|
-
},
|
|
39
|
-
// 单行高度(像素),用于计算视口高度和滚动归一化
|
|
40
|
-
rowHeight: { type: Number, default: 40 },
|
|
41
|
-
// 视口中一次性可见的行数
|
|
42
|
-
visible: { type: Number, default: 3 },
|
|
43
|
-
// 自动滚动速度:px / 秒
|
|
44
|
-
speed: { type: Number, default: 30 },
|
|
45
|
-
// DOM 池额外渲染几行,避免边缘抖动
|
|
46
|
-
buffer: { type: Number, default: 3 },
|
|
47
|
-
// 用户滚轮后暂停自动滚动多久(毫秒)
|
|
48
|
-
resumeDelay: { type: Number, default: 800 },
|
|
49
|
-
},
|
|
50
|
-
|
|
51
|
-
data() {
|
|
52
|
-
return {
|
|
53
|
-
// 当前偏移量(单位:px),始终保持在 [0, rowHeight) 区间内循环
|
|
54
|
-
offset: 0,
|
|
55
|
-
// DOM 池中的可见数据,仅维护少量行(可视行 + buffer)
|
|
56
|
-
pool: [],
|
|
57
|
-
// 当前池子第一条数据在全量列表中的索引
|
|
58
|
-
headIndex: 0,
|
|
59
|
-
// 当前 requestAnimationFrame 的 ID,用于停止动画
|
|
60
|
-
rafId: null,
|
|
61
|
-
// 上一次 requestAnimationFrame 回调的时间戳(毫秒),用于计算时间增量
|
|
62
|
-
lastTs: 0,
|
|
63
|
-
// 鼠标是否悬停在列表上,悬停时会暂停自动滚动
|
|
64
|
-
hover: false,
|
|
65
|
-
// 用户最近一次滚轮操作后,自动滚动暂停到的时间点(时间戳)
|
|
66
|
-
userPausedUntil: 0,
|
|
67
|
-
// 递增的 key 种子,用于确保 v-for 渲染的每一行有稳定的唯一 key
|
|
68
|
-
keySeed: 0,
|
|
69
|
-
// 从接口拉取到的全量数据列表,优先作为虚拟滚动的数据源
|
|
70
|
-
fetchedList: [],
|
|
71
|
-
};
|
|
72
|
-
},
|
|
73
|
-
|
|
74
|
-
computed: {
|
|
75
|
-
// 实际参与虚拟滚动的全量列表:优先使用接口返回的 fetchedList,其次使用 props.list
|
|
76
|
-
effectiveList() {
|
|
77
|
-
if (Array.isArray(this.fetchedList) && this.fetchedList.length) {
|
|
78
|
-
return this.fetchedList;
|
|
79
|
-
}
|
|
80
|
-
return Array.isArray(this.list) ? this.list : [];
|
|
81
|
-
},
|
|
82
|
-
// 视口高度 = 单行高度 * 可视行数
|
|
83
|
-
viewportHeight() {
|
|
84
|
-
return this.rowHeight * this.visible;
|
|
85
|
-
},
|
|
86
|
-
// DOM 池大小:可视行 + 预渲染 buffer,避免边缘抖动
|
|
87
|
-
poolSize() {
|
|
88
|
-
return this.visible + this.buffer;
|
|
89
|
-
},
|
|
90
|
-
// 是否可以启动自动滚动(数据量需要大于可视行数)
|
|
91
|
-
canRun() {
|
|
92
|
-
return Array.isArray(this.effectiveList) && this.effectiveList.length > this.visible;
|
|
93
|
-
},
|
|
94
|
-
},
|
|
95
|
-
|
|
96
|
-
watch: {
|
|
97
|
-
// 当外部通过 props.list 传入的新数据变更时,重置池子以适配新数据
|
|
98
|
-
list: {
|
|
99
|
-
immediate: true,
|
|
100
|
-
handler() {
|
|
101
|
-
this.resetPool();
|
|
102
|
-
},
|
|
103
|
-
},
|
|
104
|
-
},
|
|
105
|
-
|
|
106
|
-
mounted() {
|
|
107
|
-
// 组件挂载后先拉取接口数据,拉取结束后再启动自动滚动
|
|
108
|
-
this.loadListFromApi().finally(() => {
|
|
109
|
-
this.start();
|
|
110
|
-
});
|
|
111
|
-
},
|
|
112
|
-
|
|
113
|
-
beforeDestroy() {
|
|
114
|
-
this.stop();
|
|
115
|
-
},
|
|
116
|
-
|
|
117
|
-
methods: {
|
|
118
|
-
// 从接口响应中提取列表数据,统一兼容结构
|
|
119
|
-
extractListFromResponse(json) {
|
|
120
|
-
if (!json) return [];
|
|
121
|
-
if (Array.isArray(json.data)) return json.data;
|
|
122
|
-
if (Array.isArray(json.list)) return json.list;
|
|
123
|
-
if (Array.isArray(json)) return json;
|
|
124
|
-
return [];
|
|
125
|
-
},
|
|
126
|
-
|
|
127
|
-
// 将接口返回的单条记录映射为虚拟列表行数据
|
|
128
|
-
mapItemToRow(item, index) {
|
|
129
|
-
const name = item && item.name != null ? item.name : "用户 " + (index + 1);
|
|
130
|
-
const score = item && item.description != null ? item.description : "";
|
|
131
|
-
return { name, score };
|
|
132
|
-
},
|
|
133
|
-
|
|
134
|
-
// 一次性从接口拉取全量列表数据
|
|
135
|
-
async loadListFromApi() {
|
|
136
|
-
try {
|
|
137
|
-
// 通过 devServer 代理转发到后端 http://localhost:3000/api/list?page=1
|
|
138
|
-
const response = await fetch(`/api/list?page=1`);
|
|
139
|
-
if (!response.ok) {
|
|
140
|
-
throw new Error("Network response was not ok");
|
|
141
|
-
}
|
|
142
|
-
// 解析 JSON 响应体
|
|
143
|
-
const json = await response.json();
|
|
144
|
-
// 统一提取出用于渲染的基础数组
|
|
145
|
-
const list = this.extractListFromResponse(json);
|
|
146
|
-
// 将后端结构映射为虚拟列表行结构
|
|
147
|
-
this.fetchedList = list.map((item, index) => this.mapItemToRow(item, index));
|
|
148
|
-
this.resetPool();
|
|
149
|
-
} catch (e) {
|
|
150
|
-
console.error("加载列表数据失败", e);
|
|
151
|
-
this.resetPool();
|
|
152
|
-
}
|
|
153
|
-
},
|
|
154
|
-
// 重置 DOM 池状态,并根据当前有效列表重新填充池子
|
|
155
|
-
resetPool() {
|
|
156
|
-
this.offset = 0;
|
|
157
|
-
this.lastTs = 0;
|
|
158
|
-
this.headIndex = 0;
|
|
159
|
-
this.keySeed = 0;
|
|
160
|
-
|
|
161
|
-
const source = this.effectiveList;
|
|
162
|
-
const n = Array.isArray(source) ? source.length : 0;
|
|
163
|
-
if (!n) {
|
|
164
|
-
this.pool = [];
|
|
165
|
-
return;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
const size = Math.min(this.poolSize, n);
|
|
169
|
-
this.pool = Array.from({ length: size }, (_, i) => {
|
|
170
|
-
const item = source[(this.headIndex + i) % n];
|
|
171
|
-
return { ...item, __k: ++this.keySeed }; // __k 只用于 v-for key
|
|
172
|
-
});
|
|
173
|
-
},
|
|
174
|
-
|
|
175
|
-
// 启动基于 requestAnimationFrame 的自动滚动循环
|
|
176
|
-
start() {
|
|
177
|
-
const tick = (ts) => {
|
|
178
|
-
this.rafId = requestAnimationFrame(tick);
|
|
179
|
-
if (!this.canRun) return;
|
|
180
|
-
|
|
181
|
-
const now = Date.now();
|
|
182
|
-
if (now < this.userPausedUntil) return;
|
|
183
|
-
if (this.hover) return;
|
|
184
|
-
|
|
185
|
-
if (!this.lastTs) this.lastTs = ts;
|
|
186
|
-
const dt = (ts - this.lastTs) / 1000;
|
|
187
|
-
this.lastTs = ts;
|
|
188
|
-
|
|
189
|
-
// 自动滚动:offset 增加
|
|
190
|
-
this.offset += this.speed * dt;
|
|
191
|
-
this.normalizeForward();
|
|
192
|
-
};
|
|
193
|
-
|
|
194
|
-
this.rafId = requestAnimationFrame(tick);
|
|
195
|
-
},
|
|
196
|
-
|
|
197
|
-
// 停止自动滚动
|
|
198
|
-
stop() {
|
|
199
|
-
if (this.rafId) cancelAnimationFrame(this.rafId);
|
|
200
|
-
this.rafId = null;
|
|
201
|
-
},
|
|
202
|
-
|
|
203
|
-
// 向下滚动(内容向上移动):offset 累积到一行高度后,将第一条移到末尾,实现 DOM 循环复用
|
|
204
|
-
normalizeForward() {
|
|
205
|
-
const source = this.effectiveList;
|
|
206
|
-
const n = Array.isArray(source) ? source.length : 0;
|
|
207
|
-
if (!n) return;
|
|
208
|
-
|
|
209
|
-
while (this.offset >= this.rowHeight) {
|
|
210
|
-
this.offset -= this.rowHeight;
|
|
211
|
-
// headIndex 前进一条
|
|
212
|
-
this.headIndex = (this.headIndex + 1) % n;
|
|
213
|
-
|
|
214
|
-
// pool 复用:shift 掉第一条,push 新的一条
|
|
215
|
-
this.pool.shift();
|
|
216
|
-
const tailIdx = (this.headIndex + this.pool.length) % n;
|
|
217
|
-
const newItem = source[tailIdx];
|
|
218
|
-
this.pool.push({ ...newItem, __k: ++this.keySeed });
|
|
219
|
-
}
|
|
220
|
-
},
|
|
221
|
-
|
|
222
|
-
// 向上滚动:offset 为负时,将最后一条移到头部,实现 DOM 循环复用
|
|
223
|
-
normalizeBackward() {
|
|
224
|
-
const source = this.effectiveList;
|
|
225
|
-
const n = Array.isArray(source) ? source.length : 0;
|
|
226
|
-
if (!n) return;
|
|
227
|
-
|
|
228
|
-
while (this.offset < 0) {
|
|
229
|
-
this.offset += this.rowHeight;
|
|
230
|
-
// headIndex 后退一条
|
|
231
|
-
this.headIndex = (this.headIndex - 1 + n) % n;
|
|
232
|
-
|
|
233
|
-
// pool 复用:pop 掉最后一条,unshift 新的一条
|
|
234
|
-
this.pool.pop();
|
|
235
|
-
const newHead = source[this.headIndex];
|
|
236
|
-
this.pool.unshift({ ...newHead, __k: ++this.keySeed });
|
|
237
|
-
}
|
|
238
|
-
},
|
|
239
|
-
|
|
240
|
-
// 处理用户滚轮交互:手动调整 offset,并在一段时间内暂停自动滚动
|
|
241
|
-
onWheel(e) {
|
|
242
|
-
const source = this.effectiveList;
|
|
243
|
-
const n = Array.isArray(source) ? source.length : 0;
|
|
244
|
-
if (!n) return;
|
|
245
|
-
|
|
246
|
-
// wheel:deltaY > 0 代表向下滚(内容向上走)
|
|
247
|
-
const delta = e.deltaY;
|
|
248
|
-
|
|
249
|
-
// 交互暂停自动滚动一会儿(否则会“抢滚动”)
|
|
250
|
-
this.userPausedUntil = Date.now() + this.resumeDelay;
|
|
251
|
-
|
|
252
|
-
this.offset += delta;
|
|
253
|
-
this.normalizeForward();
|
|
254
|
-
this.normalizeBackward();
|
|
255
|
-
},
|
|
256
|
-
},
|
|
257
|
-
};
|
|
258
|
-
</script>
|
|
259
|
-
|
|
260
|
-
<style scoped>
|
|
261
|
-
.page {
|
|
262
|
-
padding: 20px;
|
|
263
|
-
font-family: Arial, Helvetica, sans-serif;
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
.viewport {
|
|
267
|
-
overflow: hidden;
|
|
268
|
-
position: relative;
|
|
269
|
-
border: 1px solid #eee;
|
|
270
|
-
user-select: none;
|
|
271
|
-
width: 340px;
|
|
272
|
-
background: #fff;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
.track {
|
|
276
|
-
will-change: transform;
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
.row {
|
|
280
|
-
display: flex;
|
|
281
|
-
padding: 0 12px;
|
|
282
|
-
border-bottom: 1px solid #f3f3f3;
|
|
283
|
-
box-sizing: border-box;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
.cell {
|
|
287
|
-
width: 140px;
|
|
288
|
-
}
|
|
289
|
-
</style>
|