vue2server 1.0.8 → 1.0.10

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.
@@ -1,5 +1,14 @@
1
- import { post } from '../utils/request'
1
+ import { post, get } from '../utils/request'
2
2
 
3
3
  export function getTrend(year) {
4
4
  return post('/mock/trend', { year })
5
5
  }
6
+
7
+ // 获取名字列表(分页)
8
+ // 后端接口:GET /api/mock/names
9
+ // 参数:
10
+ // - size:每页数量(最大200)
11
+ // - page:页码(从1开始)
12
+ export function getNames(params = {}) {
13
+ return get('/mock/names', params)
14
+ }
@@ -278,7 +278,7 @@ export default {
278
278
  },
279
279
  computed: {},
280
280
  methods: {
281
- async fetchData(allowRetry = true) {
281
+ async fetchData() {
282
282
  const params = {};
283
283
  const { belOrgNo, meetSubj, meetTypeCd, createDateRange } = this.filters;
284
284
  if (belOrgNo) params.belOrgNo = belOrgNo;
@@ -296,21 +296,8 @@ export default {
296
296
  }
297
297
  try {
298
298
  const res = await get("/meeting/list", params);
299
- if (res && res.list) {
300
- this.rows = Array.isArray(res.list) ? res.list : [];
301
- this.totalCount = Number.isFinite(res.total) ? res.total : this.rows.length;
302
- } else if (Array.isArray(res)) {
303
- this.rows = res;
304
- this.totalCount = res.length;
305
- } else {
306
- this.rows = [];
307
- this.totalCount = 0;
308
- }
309
- const maxPage = Math.max(1, Math.ceil(this.totalCount / this.pageSize));
310
- if (allowRetry && this.page > maxPage) {
311
- this.page = maxPage;
312
- await this.fetchData(false);
313
- }
299
+ this.rows = Array.isArray(res.list) ? res.list : [];
300
+ this.totalCount = Number.isFinite(res.total) ? res.total : this.rows.length;
314
301
  } catch (e) {
315
302
  this.$message.error("获取会议列表失败");
316
303
  }
@@ -0,0 +1,291 @@
1
+ <template>
2
+ <!--
3
+ 名字无缝滚动页面
4
+ 需求:
5
+ - 从后端获取名字列表
6
+ - 自动无缝向下滚动;鼠标移入暂停并允许滚轮上下滚动;移出恢复自动滚动
7
+ -->
8
+ <section class="page name-scroll-page">
9
+ <h1 class="page-title">名字滚动</h1>
10
+
11
+ <!-- 滚动容器:固定高度,溢出隐藏 -->
12
+ <div
13
+ class="scroll-wrapper"
14
+ ref="scrollWrapper"
15
+ @mouseenter="onMouseEnter"
16
+ @mouseleave="onMouseLeave"
17
+ @wheel.prevent="onWheel"
18
+ >
19
+ <!-- 内容容器:放置两份列表以实现无缝循环 -->
20
+ <div class="scroll-content" ref="scrollContent">
21
+ <ul class="name-list">
22
+ <li v-for="(n, idx) in names" :key="'a-' + idx" class="name-item">{{ n }}</li>
23
+ </ul>
24
+ <!-- 第二份同样的列表,用于衔接形成无缝滚动 -->
25
+ <ul class="name-list">
26
+ <li v-for="(n, idx) in names" :key="'b-' + idx" class="name-item">{{ n }}</li>
27
+ </ul>
28
+ </div>
29
+ </div>
30
+
31
+ <div class="toolbar">
32
+ <el-button size="small" @click="reload">重新加载</el-button>
33
+ <span class="tip">鼠标移入暂停自动滚动,滚轮可上下滚动;移出恢复</span>
34
+ </div>
35
+ <div class="last-visible">当前最后条显示的数据:<strong>{{ lastVisibleText || '-' }}</strong></div>
36
+ </section>
37
+ </template>
38
+
39
+ <script>
40
+ // 引入后端接口:获取名字列表
41
+ import { getNames } from '../api/mock'
42
+
43
+ export default {
44
+ name: 'NameScrollPage',
45
+ data() {
46
+ return {
47
+ // 名字列表数据(从后端获取)
48
+ names: [],
49
+ // 分页状态
50
+ page: 1,
51
+ pageSize: 50,
52
+ total: 0,
53
+ loadingNext: false,
54
+ // 是否正在自动滚动(true 表示开启)
55
+ autoRunning: false,
56
+ // 自动滚动速度:每帧增加的像素(可根据需要调整)
57
+ speed: 0.4,
58
+ // requestAnimationFrame 的任务 ID,用于取消动画
59
+ rafId: 0,
60
+ // 单份列表的高度(用于实现无缝循环)
61
+ listHeight: 0,
62
+ // 单条目高度(用于计算当前视口内的最后一条)
63
+ itemHeight: 0,
64
+ // 是否处于鼠标悬停状态(悬停时暂停自动滚动)
65
+ isHovering: false,
66
+ // 当前视口内最后一条数据的文案
67
+ lastVisibleText: '',
68
+ // 当前视口内最后一条数据的索引
69
+ lastVisibleIndex: 0
70
+ }
71
+ },
72
+ mounted() {
73
+ // 页面挂载后初始化:获取数据并启动自动滚动
74
+ this.init()
75
+ },
76
+ beforeDestroy() {
77
+ // 组件销毁前停止自动滚动,避免动画泄漏
78
+ this.stopAuto()
79
+ },
80
+ methods: {
81
+ /**
82
+ * 初始化页面:拉取名字并开始滚动
83
+ */
84
+ async init() {
85
+ await this.fetchNames()
86
+ this.$nextTick(() => {
87
+ this.measure()
88
+ this.startAuto()
89
+ })
90
+ },
91
+ /**
92
+ * 从后端获取名字列表
93
+ * 参数:size 每页数量(最大200),page 页码(从1开始)
94
+ * 返回:数组形式的名字列表
95
+ */
96
+ async fetchNames() {
97
+ const res = await getNames({ size: this.pageSize, page: this.page })
98
+ // 后端响应可能是 { list, total, page, pageSize } 或直接数组,做兼容处理
99
+ const list = (res && res.list) || res || []
100
+ this.names = Array.isArray(list) ? list : []
101
+ if (res && typeof res.total === 'number') this.total = res.total
102
+ if (res && typeof res.pageSize === 'number') this.pageSize = res.pageSize
103
+ if (res && typeof res.page === 'number') this.page = res.page
104
+ },
105
+ /**
106
+ * 重新测量单份列表高度
107
+ * 用于判断何时将 scrollTop 回跳,实现无缝循环
108
+ */
109
+ measure() {
110
+ const content = this.$refs.scrollContent
111
+ if (!content) return
112
+ const lists = content.getElementsByClassName('name-list')
113
+ if (lists && lists[0]) {
114
+ this.listHeight = lists[0].offsetHeight
115
+ }
116
+ const item = content.getElementsByClassName('name-item')[0]
117
+ if (item) {
118
+ this.itemHeight = item.offsetHeight
119
+ }
120
+ this.updateLastVisible()
121
+ },
122
+ /**
123
+ * 启动自动滚动(基于 requestAnimationFrame)
124
+ * 行为:每帧将容器 scrollTop 增加 speed,超出单份高度时回跳
125
+ */
126
+ startAuto() {
127
+ if (this.autoRunning) return
128
+ this.autoRunning = true
129
+ const step = () => {
130
+ if (!this.autoRunning) return
131
+ const wrapper = this.$refs.scrollWrapper
132
+ if (wrapper) {
133
+ wrapper.scrollTop += this.speed
134
+ // 超过单份列表高度则回跳,形成无缝循环
135
+ if (wrapper.scrollTop >= this.listHeight) {
136
+ wrapper.scrollTop -= this.listHeight
137
+ }
138
+ this.maybeLoadMore()
139
+ }
140
+ this.updateLastVisible()
141
+ this.rafId = window.requestAnimationFrame(step)
142
+ }
143
+ this.rafId = window.requestAnimationFrame(step)
144
+ },
145
+ /**
146
+ * 停止自动滚动:取消动画并置位
147
+ */
148
+ stopAuto() {
149
+ this.autoRunning = false
150
+ if (this.rafId) {
151
+ window.cancelAnimationFrame(this.rafId)
152
+ this.rafId = 0
153
+ }
154
+ },
155
+ /**
156
+ * 鼠标移入:暂停自动滚动,进入手动滚动模式
157
+ */
158
+ onMouseEnter() {
159
+ this.isHovering = true
160
+ this.stopAuto()
161
+ },
162
+ /**
163
+ * 鼠标移出:退出手动模式,恢复自动滚动
164
+ */
165
+ onMouseLeave() {
166
+ this.isHovering = false
167
+ this.startAuto()
168
+ },
169
+ /**
170
+ * 鼠标滚轮事件:根据滚轮方向上下滚动
171
+ * 同时保持无缝循环的边界处理
172
+ */
173
+ onWheel(e) {
174
+ const wrapper = this.$refs.scrollWrapper
175
+ if (!wrapper) return
176
+ const delta = e.deltaY || 0
177
+ wrapper.scrollTop += delta
178
+ // 下滚超过单份高度时回跳;上滚小于0时向上回跳
179
+ if (wrapper.scrollTop >= this.listHeight) {
180
+ wrapper.scrollTop -= this.listHeight
181
+ } else if (wrapper.scrollTop < 0) {
182
+ wrapper.scrollTop += this.listHeight
183
+ }
184
+ this.maybeLoadMore()
185
+ this.updateLastVisible()
186
+ },
187
+ /**
188
+ * 重新加载数据:重置滚动位置并恢复自动滚动
189
+ */
190
+ async reload() {
191
+ await this.fetchNames()
192
+ this.$nextTick(() => {
193
+ const wrapper = this.$refs.scrollWrapper
194
+ if (wrapper) wrapper.scrollTop = 0
195
+ this.measure()
196
+ if (!this.isHovering) this.startAuto()
197
+ })
198
+ },
199
+ hasMore() {
200
+ return this.page * this.pageSize < this.total
201
+ },
202
+ async loadNextPage() {
203
+ if (this.loadingNext || !this.hasMore()) return
204
+ this.loadingNext = true
205
+ const nextPage = this.page + 1
206
+ try {
207
+ const res = await getNames({ size: this.pageSize, page: nextPage })
208
+ const list = (res && res.list) || []
209
+ if (Array.isArray(list) && list.length) {
210
+ this.names = [...this.names, ...list]
211
+ if (res && typeof res.total === 'number') this.total = res.total
212
+ this.page = nextPage
213
+ this.$nextTick(() => {
214
+ this.measure()
215
+ })
216
+ }
217
+ } finally {
218
+ this.loadingNext = false
219
+ }
220
+ },
221
+ maybeLoadMore() {
222
+ if (!this.hasMore()) return
223
+ if (this.loadingNext) return
224
+ const thresholdIndex = Math.floor((this.names.length || 0) * 2 / 3)
225
+ if (this.lastVisibleIndex >= thresholdIndex) this.loadNextPage()
226
+ },
227
+ /**
228
+ * 计算并更新当前视口内最后一条可见数据
229
+ */
230
+ updateLastVisible() {
231
+ const wrapper = this.$refs.scrollWrapper
232
+ if (!wrapper || !Array.isArray(this.names) || this.names.length === 0) {
233
+ this.lastVisibleText = ''
234
+ return
235
+ }
236
+ const h = this.itemHeight || 1
237
+ const offset = wrapper.scrollTop || 0
238
+ const viewH = wrapper.clientHeight || 0
239
+ const endPos = offset + viewH
240
+ const relative = this.listHeight > 0 ? (endPos % this.listHeight) : endPos
241
+ let idx = Math.floor(relative / h) - 1
242
+ if (idx < 0) idx = 0
243
+ if (idx >= this.names.length) idx = this.names.length - 1
244
+ this.lastVisibleText = this.names[idx] || ''
245
+ this.lastVisibleIndex = idx
246
+ }
247
+ }
248
+ }
249
+ </script>
250
+
251
+ <style scoped>
252
+ .page.name-scroll-page {
253
+ padding: 16px;
254
+ }
255
+ .page-title {
256
+ font-size: 18px;
257
+ margin-bottom: 12px;
258
+ }
259
+ .scroll-wrapper {
260
+ height: 320px;
261
+ overflow: hidden;
262
+ border: 1px solid #ebeef5;
263
+ border-radius: 4px;
264
+ background: #fff;
265
+ }
266
+ .scroll-content {
267
+ /* 内容容器用于放置两份列表,实现无缝循环 */
268
+ }
269
+ .name-list {
270
+ list-style: none;
271
+ margin: 0;
272
+ padding: 0;
273
+ }
274
+ .name-item {
275
+ padding: 8px 12px;
276
+ border-bottom: 1px dashed #f0f0f0;
277
+ }
278
+ .toolbar {
279
+ margin-top: 12px;
280
+ display: flex;
281
+ align-items: center;
282
+ gap: 12px;
283
+ color: #606266;
284
+ }
285
+ .tip {
286
+ font-size: 12px;
287
+ }
288
+ .last-visible {
289
+ margin-top: 8px;
290
+ }
291
+ </style>
@@ -6,6 +6,7 @@
6
6
  import LineChartPage from '../pages/LineChartPage.vue'
7
7
  import MedalSetting from '../pages/MedalSetting.vue'
8
8
  import MeetingListPage from '../pages/MeetingListPage.vue'
9
+ import NameScrollPage from '../pages/NameScrollPage.vue'
9
10
 
10
11
  // routes 将在 router/index 中被 VueRouter 使用
11
12
  export const routes = [
@@ -49,5 +50,15 @@ export const routes = [
49
50
  icon: 'el-icon-date',
50
51
  showInMenu: true
51
52
  }
53
+ },
54
+ {
55
+ path: '/name-scroll',
56
+ name: 'NameScroll',
57
+ component: NameScrollPage,
58
+ meta: {
59
+ title: '名字滚动',
60
+ icon: 'el-icon-user',
61
+ showInMenu: true
62
+ }
52
63
  }
53
64
  ]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vue2server",
3
- "version": "1.0.8",
3
+ "version": "1.0.10",
4
4
  "description": "",
5
5
  "scripts": {
6
6
  "dev": "nodemon --watch src --ext ts --exec \"ts-node src/app.ts\"",
@@ -1,12 +1,37 @@
1
1
  import { Request, Response } from "express";
2
2
 
3
- import { buildMockUser, buildYearTrend } from "../services/mock.service";
3
+ import { buildMockUser, buildYearTrend, buildNameList } from "../services/mock.service";
4
+
5
+ function toInt(value: unknown, fallback: number): number {
6
+ if (typeof value === "number" && Number.isFinite(value)) return Math.floor(value);
7
+ if (typeof value === "string" && value.trim() !== "") {
8
+ const parsed = Number.parseInt(value, 10);
9
+ if (Number.isFinite(parsed)) return parsed;
10
+ }
11
+ return fallback;
12
+ }
13
+
4
14
  class MockController {
5
15
  getMockUser = (_req: Request, res: Response): void => {
6
16
  res.json({
7
17
  data: buildMockUser()
8
18
  });
9
19
  };
20
+ getNames = (req: Request, res: Response): void => {
21
+ const pageSize = Math.max(1, Math.min(200, toInt(req.query.size, 10)));
22
+ const page = Math.max(1, toInt(req.query.page, 1));
23
+ const start = (page - 1) * pageSize;
24
+ const list = buildNameList(pageSize, start);
25
+ const total = 100000;
26
+ res.json({
27
+ data: {
28
+ list,
29
+ total,
30
+ page,
31
+ pageSize
32
+ }
33
+ });
34
+ };
10
35
  getTrend = (req: Request, res: Response): void => {
11
36
  const rawYear = (req.body as { year?: unknown })?.year;
12
37
  const year = typeof rawYear === "string" ? Number.parseInt(rawYear, 10) : typeof rawYear === "number" ? rawYear : NaN;
@@ -5,5 +5,6 @@ const router = Router();
5
5
 
6
6
  router.get("/user", mockController.getMockUser);
7
7
  router.post("/trend", mockController.getTrend);
8
+ router.get("/names", mockController.getNames);
8
9
 
9
10
  export default router;
@@ -2,6 +2,7 @@ import { randomUUID } from "node:crypto";
2
2
 
3
3
  const FIRST_NAMES = ["Alex", "Sam", "Taylor", "Jordan", "Morgan", "Casey", "Riley", "Avery"];
4
4
  const LAST_NAMES = ["Li", "Wang", "Zhang", "Chen", "Liu", "Yang", "Zhao", "Huang"];
5
+ const DISPLAY_NAMES = ["张三", "李四", "王五", "赵六", "孙七", "周八", "吴九", "郑十"];
5
6
 
6
7
  export function buildMockUser(): { id: string; name: string; email: string; age: number; createdAt: string } {
7
8
  const firstName = pick(FIRST_NAMES);
@@ -20,6 +21,18 @@ export function buildMockUser(): { id: string; name: string; email: string; age:
20
21
  };
21
22
  }
22
23
 
24
+ export function buildNameList(count: number, offset = 0): string[] {
25
+ const list: string[] = [];
26
+ const base = DISPLAY_NAMES;
27
+ const baseLen = base.length || 1;
28
+ for (let i = 0; i < count; i += 1) {
29
+ const index = offset + i;
30
+ const name = base[index % baseLen];
31
+ list.push(`${name}${index + 1}`);
32
+ }
33
+ return list;
34
+ }
35
+
23
36
  export function buildYearTrend(year: number): Array<{ label: string; value: number | string }> {
24
37
  const result: Array<{ label: string; value: number | string }> = [];
25
38
  let current = new Date(Date.UTC(year, 0, 1));