arms-app 1.0.62 → 1.0.64

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arms-app",
3
- "version": "1.0.62",
3
+ "version": "1.0.64",
4
4
  "description": "一个基于 Express 的 Web 应用1",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/view/1.vue CHANGED
@@ -50,12 +50,21 @@ export default {
50
50
  },
51
51
  },
52
52
  mounted() {
53
- this.list = Array.from({ length: 15 }, (_, i) => `列表项 ${i + 1}`);
54
- this.$nextTick(() => {
55
- if (this.$refs.ss && this.$refs.ss.reset) this.$refs.ss.reset();
56
- });
53
+ this.fetchData();
57
54
  },
58
55
  methods: {
56
+ async fetchData() {
57
+ // 模拟接口请求
58
+ console.log('开始获取数据...');
59
+ await new Promise((resolve) => setTimeout(resolve, 1000));
60
+ const data = Array.from({ length: 5000 }, (_, i) => `列表项 ${i + 1}`);
61
+ // 使用 Object.freeze 冻结数据,避免 Vue 深度监听,显著提升大数据量下的渲染性能
62
+ this.list = Object.freeze(data);
63
+ console.log('数据获取完成,共', this.list.length, '条');
64
+ this.$nextTick(() => {
65
+ if (this.$refs.ss && this.$refs.ss.reset) this.$refs.ss.reset();
66
+ });
67
+ },
59
68
  onWheel(e) {
60
69
  const ss = this.$refs.ss;
61
70
  if (!ss) return;
package/view/2.vue ADDED
@@ -0,0 +1,129 @@
1
+ <template>
2
+ <el-dialog
3
+ :visible.sync="visibleInner"
4
+ title="列表"
5
+ width="800px"
6
+ :close-on-click-modal="false"
7
+ @open="onOpen"
8
+ @closed="onClosed"
9
+ >
10
+ <!-- 表格只渲染当前页 rows(10条) -->
11
+ <el-table
12
+ :data="rows"
13
+ height="400"
14
+ border
15
+ row-key="id"
16
+ >
17
+ <el-table-column prop="id" label="ID" width="120" />
18
+ <el-table-column prop="name" label="名称" />
19
+ <el-table-column prop="status" label="状态" width="120" />
20
+ </el-table>
21
+
22
+ <div style="margin-top: 12px; text-align: right;">
23
+ <el-pagination
24
+ background
25
+ layout="total, prev, pager, next, sizes"
26
+ :total="total"
27
+ :current-page.sync="page"
28
+ :page-size.sync="pageSize"
29
+ :page-sizes="[10, 20, 50, 100]"
30
+ @current-change="refreshPage"
31
+ @size-change="onSizeChange"
32
+ />
33
+ </div>
34
+
35
+ <span slot="footer" class="dialog-footer">
36
+ <el-button @click="close">关闭</el-button>
37
+ </span>
38
+ </el-dialog>
39
+ </template>
40
+
41
+ <script>
42
+ export default {
43
+ name: "ChildDialog",
44
+ props: {
45
+ // 只控制显隐,不要用 props 传 5000 条
46
+ visible: { type: Boolean, default: false }
47
+ },
48
+ data() {
49
+ return {
50
+ // 内部 visible(避免直接改 props)
51
+ visibleInner: false,
52
+
53
+ // 只存 UI 需要的(轻量)
54
+ page: 1,
55
+ pageSize: 10,
56
+ total: 0,
57
+ rows: []
58
+ };
59
+ },
60
+
61
+ watch: {
62
+ // 父组件控制打开/关闭
63
+ visible: {
64
+ immediate: true,
65
+ handler(v) {
66
+ this.visibleInner = v;
67
+ }
68
+ },
69
+ // 同步回父组件(:visible.sync)
70
+ visibleInner(v) {
71
+ this.$emit("update:visible", v);
72
+ }
73
+ },
74
+
75
+ created() {
76
+ // 非响应式大数据容器(不要放 data 里)
77
+ this._rawList = [];
78
+ this._hasData = false;
79
+ },
80
+
81
+ methods: {
82
+ /** 父组件 open 后调用:this.$refs.dlg.setData(bigList) */
83
+ setData(list) {
84
+ // 关键:不放 data,不做深拷贝,不 deep watch
85
+ this._rawList = Array.isArray(list) ? list : [];
86
+ this._hasData = true;
87
+
88
+ // 初始化分页
89
+ this.page = 1;
90
+ this.total = this._rawList.length;
91
+
92
+ // 如果 dialog 已经打开,立刻刷新;否则等 onOpen 再刷
93
+ if (this.visibleInner) this.refreshPage();
94
+ },
95
+
96
+ onOpen() {
97
+ // dialog 打开后,如果数据已设置,刷新一次(渲染10条)
98
+ if (this._hasData) this.refreshPage();
99
+ },
100
+
101
+ onClosed() {
102
+ // 关闭时只清理轻量状态,避免销毁/重建造成卡顿
103
+ this.rows = [];
104
+ this.page = 1;
105
+ // 如果你希望下次打开仍复用数据,就别清 _rawList
106
+ // this._rawList = [];
107
+ // this._hasData = false;
108
+ },
109
+
110
+ refreshPage() {
111
+ const start = (this.page - 1) * this.pageSize;
112
+ const end = start + this.pageSize;
113
+
114
+ // 只 slice 当前页,rows 才是响应式的
115
+ this.rows = this._rawList.slice(start, end);
116
+ this.total = this._rawList.length;
117
+ },
118
+
119
+ onSizeChange() {
120
+ this.page = 1;
121
+ this.refreshPage();
122
+ },
123
+
124
+ close() {
125
+ this.visibleInner = false;
126
+ }
127
+ }
128
+ };
129
+ </script>
package/view/3.vue ADDED
@@ -0,0 +1,289 @@
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>
package/view/4.vue ADDED
@@ -0,0 +1,74 @@
1
+ <template>
2
+ <section class="person-table-page">
3
+ <h1>人员信息表</h1>
4
+ <el-table
5
+ :data="rows"
6
+ border
7
+ stripe
8
+ size="small"
9
+ style="width: 100%"
10
+ >
11
+ <el-table-column type="index" label="序号" width="60" align="center" />
12
+ <el-table-column prop="name" label="人员姓名" min-width="100" />
13
+ <el-table-column label="性别" width="80">
14
+ <template slot-scope="scope">
15
+ {{ formatSex(scope.row.sex) }}
16
+ </template>
17
+ </el-table-column>
18
+ <el-table-column prop="postName" label="岗位" min-width="80" />
19
+ <el-table-column prop="happenPerio" label="入行日期" min-width="110" />
20
+ <el-table-column prop="telNo" label="联系方式" min-width="130" />
21
+ <el-table-column prop="dtlAddr" label="现居住地" min-width="120" />
22
+ <el-table-column prop="emergContactPersonName" label="紧急联系人" min-width="100" />
23
+ <el-table-column prop="emergContactPhoneNo" label="紧急联系方式" min-width="130" />
24
+ <el-table-column prop="birthDt" label="出生日期" min-width="110" />
25
+ <el-table-column prop="highestEduDegreeDesc" label="最高学历" min-width="90" />
26
+ <el-table-column prop="createTm" label="创建日期" min-width="110" />
27
+ <el-table-column prop="rmk" label="备注" min-width="120" />
28
+ </el-table>
29
+ </section>
30
+ </template>
31
+
32
+ <script>
33
+ export default {
34
+ name: "PersonTableView",
35
+ data() {
36
+ return {
37
+ rows: [
38
+ {
39
+ id: 3286648151643648,
40
+ name: "李白",
41
+ sex: "1",
42
+ postName: "保洁",
43
+ happenPerio: "2002-02-02",
44
+ telNo: "18966541247",
45
+ dtlAddr: "北京",
46
+ emergContactPersonName: "王安石",
47
+ emergContactPhoneNo: "15466257489",
48
+ birthDt: "2000-02-02",
49
+ highestEduDegreeDesc: "硕士",
50
+ createTm: "2025-12-25",
51
+ rmk: "好人",
52
+ beloOrgNO: "000291",
53
+ creatorNo: "022171",
54
+ delFlag: "0",
55
+ },
56
+ ],
57
+ };
58
+ },
59
+ methods: {
60
+ formatSex(v) {
61
+ if (v === "1" || v === 1) return "男";
62
+ if (v === "2" || v === 2) return "女";
63
+ return "";
64
+ },
65
+ },
66
+ };
67
+ </script>
68
+
69
+ <style scoped>
70
+ .person-table-page {
71
+ padding: 24px;
72
+ }
73
+ </style>
74
+