vercerl-express-test 1.0.0 → 1.0.3

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/README.md CHANGED
@@ -1,6 +1,6 @@
1
- ## 命名规范
2
- 例子:如创建一个auth模块;
3
- - 需要创建一个controllers/auth.controller.ts文件,控制器逻辑都写这里,包含接口的jsDoc注释
4
- - 需要创建一个services/auth.service.ts文件,业务逻辑都写这里
5
- - 需要创建一个models/auth.model.ts文件,数据库操作都写这里
6
- - 需要创建一个routes/auth.route.ts文件,路由都写这里
1
+ ## Naming Convention
2
+ Example: Creating an auth module;
3
+ - Create a controllers/auth.controller.ts file, write controller logic here, including jsDoc comments for interfaces
4
+ - Create a services/auth.service.ts file, write business logic here
5
+ - Create a models/auth.model.ts file, write database operations here
6
+ - Create a routes/auth.route.ts file, write routing here
@@ -0,0 +1,542 @@
1
+ <template>
2
+ <!--
3
+ 会议列表页面
4
+ 功能:
5
+ 1. 顶部查询条件:所属机构、会议主题、会议类型、创建日期范围
6
+ 2. 工具栏:新建按钮(预留后续接入创建弹窗)
7
+ 3. 中间表格:
8
+ - 支持列拖拽(除操作列外)
9
+ - 展示会议基本信息
10
+ 4. 底部分页:基于筛选结果做前端分页
11
+ -->
12
+ <section class="page meeting-page">
13
+ <h1 class="meeting-title">会议列表</h1>
14
+
15
+ <!-- 查询条件区域:用于筛选会议列表 -->
16
+ <div class="meeting-search-card">
17
+ <el-form :inline="true" :model="filters" size="small" class="meeting-search-form">
18
+ <!-- 所属机构下拉框 -->
19
+ <el-form-item label="所属机构">
20
+ <el-select
21
+ v-model="filters.belOrgNo"
22
+ placeholder="请选择"
23
+ clearable
24
+ style="width: 200px"
25
+ >
26
+ <el-option
27
+ v-for="opt in orgOptions"
28
+ :key="opt.value"
29
+ :label="opt.label"
30
+ :value="opt.value"
31
+ />
32
+ </el-select>
33
+ </el-form-item>
34
+ <!-- 会议主题输入框(模糊匹配) -->
35
+ <el-form-item label="会议主题">
36
+ <el-input
37
+ v-model="filters.meetSubj"
38
+ placeholder="请输入"
39
+ clearable
40
+ style="width: 220px"
41
+ />
42
+ </el-form-item>
43
+ <!-- 会议类型下拉框 -->
44
+ <el-form-item label="会议类型">
45
+ <el-select
46
+ v-model="filters.meetTypeCd"
47
+ placeholder="全部"
48
+ clearable
49
+ style="width: 160px"
50
+ >
51
+ <el-option
52
+ v-for="opt in typeOptions"
53
+ :key="opt.value"
54
+ :label="opt.label"
55
+ :value="opt.value"
56
+ />
57
+ </el-select>
58
+ </el-form-item>
59
+ <!-- 创建日期范围选择 -->
60
+ <el-form-item label="创建日期">
61
+ <el-date-picker
62
+ v-model="filters.createDateRange"
63
+ type="daterange"
64
+ range-separator="至"
65
+ start-placeholder="开始日期"
66
+ end-placeholder="结束日期"
67
+ format="yyyy-MM-dd"
68
+ value-format="yyyy-MM-dd"
69
+ unlink-panels
70
+ />
71
+ </el-form-item>
72
+ <!-- 查询 / 重置按钮 -->
73
+ <el-form-item>
74
+ <el-button type="primary" size="small" @click="onSearch">
75
+ 查询
76
+ </el-button>
77
+ <el-button size="small" @click="onReset">
78
+ 重置
79
+ </el-button>
80
+ </el-form-item>
81
+ </el-form>
82
+ </div>
83
+
84
+ <!-- 工具栏:后续可扩展更多操作按钮 -->
85
+ <div class="meeting-toolbar">
86
+ <el-button type="primary" size="small" icon="el-icon-plus">
87
+ 新建
88
+ </el-button>
89
+ </div>
90
+
91
+ <!--
92
+ 主表格:
93
+ - ref/:key:配合拖拽重建表头
94
+ - index 列固定在最左侧
95
+ - 中间列通过 columns 配置,支持拖拽调整顺序
96
+ - 操作列固定在最右侧且不可拖拽
97
+ -->
98
+ <el-table
99
+ ref="meetingTable"
100
+ :key="tableKey"
101
+ :data="pagedRows"
102
+ border
103
+ stripe
104
+ size="small"
105
+ style="width: 100%"
106
+ :default-sort="{ prop: 'createTm', order: 'descending' }"
107
+ @sort-change="onSortChange"
108
+ >
109
+ <!-- 序号列:仅显示索引,不参与拖拽 -->
110
+ <el-table-column
111
+ type="index"
112
+ label="序号"
113
+ width="60"
114
+ align="center"
115
+ />
116
+ <!--
117
+ 中间业务列:
118
+ 由 columns 数组驱动,通过 header 上的 header-draggable 类来实现拖拽
119
+ -->
120
+ <el-table-column
121
+ v-for="col in columns"
122
+ :key="col.key"
123
+ :prop="col.prop"
124
+ :label="col.label"
125
+ :width="col.width"
126
+ :min-width="col.minWidth"
127
+ :align="col.align || 'left'"
128
+ :header-align="col.headerAlign || col.align || 'left'"
129
+ :sortable="col.sortable"
130
+ :label-class-name="'meeting-header-' + col.key"
131
+ >
132
+ <!-- 表头内容:包一层 div 以作为拖拽的 handle -->
133
+ <template slot="header">
134
+ <div class="header-wrapper header-draggable">
135
+ <span class="header-label">{{ col.label }}</span>
136
+ </div>
137
+ </template>
138
+ <!-- 根据列类型渲染不同的单元格内容 -->
139
+ <template slot-scope="scope">
140
+ <span v-if="col.type === 'type'">
141
+ <el-tag
142
+ :type="getTypeTagType(scope.row.meetTypeCd)"
143
+ size="mini"
144
+ >
145
+ {{ formatMeetType(scope.row.meetTypeCd) }}
146
+ </el-tag>
147
+ </span>
148
+ <div v-else-if="col.type === 'content'">
149
+ <div class="meeting-subject">
150
+ {{ scope.row.meetSubj }}
151
+ </div>
152
+ <div
153
+ v-if="scope.row.plainContent"
154
+ class="meeting-content"
155
+ >
156
+ {{ scope.row.plainContent }}
157
+ </div>
158
+ </div>
159
+ <span v-else-if="col.type === 'createDate'">
160
+ {{ formatDate(scope.row.createTm) }}
161
+ </span>
162
+ <span v-else-if="col.type === 'updateDate'">
163
+ {{ formatDateTime(scope.row.updateTm) }}
164
+ </span>
165
+ <span v-else>
166
+ {{ scope.row[col.prop] }}
167
+ </span>
168
+ </template>
169
+ </el-table-column>
170
+ <!-- 操作列:固定在右侧,不参与拖拽(通过 label-class-name 区分) -->
171
+ <el-table-column
172
+ label="操作"
173
+ width="140"
174
+ align="center"
175
+ fixed="right"
176
+ label-class-name="meeting-header-action"
177
+ >
178
+ <template slot-scope="scope">
179
+ <el-button
180
+ type="text"
181
+ size="mini"
182
+ @click="onView(scope.row)"
183
+ >
184
+ 详情
185
+ </el-button>
186
+ <el-button
187
+ type="text"
188
+ size="mini"
189
+ @click="onDelete(scope.row)"
190
+ >
191
+ 删除
192
+ </el-button>
193
+ </template>
194
+ </el-table-column>
195
+ </el-table>
196
+
197
+ <div class="meeting-pagination">
198
+ <el-pagination
199
+ layout="total, prev, pager, next"
200
+ :current-page.sync="page"
201
+ :page-size="pageSize"
202
+ :total="filteredRows.length"
203
+ @current-change="onPageChange"
204
+ />
205
+ </div>
206
+ </section>
207
+ </template>
208
+
209
+ <script>
210
+ import Sortable from "sortablejs";
211
+
212
+ export default {
213
+ name: "MeetingListPage",
214
+ data() {
215
+ return {
216
+ // 可拖拽的业务列配置(不包含序号列与操作列)
217
+ columns: [
218
+ { key: "type", prop: "meetTypeCd", label: "类型", width: 120, align: "center", type: "type" },
219
+ { key: "content", prop: "meetSubj", label: "内容", minWidth: 260, type: "content" },
220
+ { key: "organizationName", prop: "organizationName", label: "所属机构", minWidth: 160 },
221
+ { key: "displayName", prop: "displayName", label: "创建人", width: 120 },
222
+ { key: "createDate", prop: "createTm", label: "创建日期", width: 160, align: "center", type: "createDate", sortable: "custom" },
223
+ { key: "updateDate", prop: "updateTm", label: "最近编辑时间", width: 190, align: "center", type: "updateDate", sortable: "custom" }
224
+ ],
225
+ // 示例数据:模拟接口返回的会议列表,用于本地展示
226
+ rows: [
227
+ {
228
+ id: 33029032315325800,
229
+ belOrgNo: "31021",
230
+ organizationName: "上海浦东支行",
231
+ meetSubj: "安全测试",
232
+ meetConte: "<p>1231</p>",
233
+ plainContent: "1231",
234
+ meetDt: "2026-01-07",
235
+ meetNo: "ORTHER2026-01-16 17:02:05",
236
+ meetTypeCd: "TRAINING",
237
+ displayName: "刘晓鸣",
238
+ createTm: "2026-01-16",
239
+ updateTm: "2026-01-16 17:02:05"
240
+ },
241
+ {
242
+ id: 33029032315325801,
243
+ belOrgNo: "11001",
244
+ organizationName: "北京西城支行",
245
+ meetSubj: "二化管理体系例会",
246
+ meetConte: "<p>1.各部门汇报日常工作以及问题</p><p>2.当前阶段化重点说明</p>",
247
+ plainContent: "各部门汇报日常工作以及问题;当前阶段化重点说明",
248
+ meetDt: "2026-01-15",
249
+ meetNo: "MORNING2026-01-15 19:56:24",
250
+ meetTypeCd: "MORNING",
251
+ displayName: "黄丽丽",
252
+ createTm: "2026-01-15",
253
+ updateTm: "2026-01-15 19:56:24"
254
+ }
255
+ ],
256
+ // 查询条件
257
+ filters: {
258
+ belOrgNo: "",
259
+ meetSubj: "",
260
+ meetTypeCd: "",
261
+ createDateRange: []
262
+ },
263
+ // 所属机构下拉选项
264
+ orgOptions: [
265
+ { label: "全部", value: "" },
266
+ { label: "上海浦东支行", value: "31021" },
267
+ { label: "北京西城支行", value: "11001" }
268
+ ],
269
+ // 会议类型下拉选项
270
+ typeOptions: [
271
+ { label: "培训", value: "TRAINING" },
272
+ { label: "晨夕会", value: "MORNING" }
273
+ ],
274
+ // 当前页码
275
+ page: 1,
276
+ // 每页条数
277
+ pageSize: 10,
278
+ // 预留:如果后续接接口,可以用来缓存上次查询 key
279
+ lastSearchKey: "",
280
+ // 当前远程排序字段(与 el-table-column 的 prop 对应)
281
+ sortField: "createTm",
282
+ // 当前远程排序方向:ascending / descending
283
+ sortOrder: "descending",
284
+ // 表头拖拽实例
285
+ headerSortable: null,
286
+ // 强制刷新表头用的 key(列顺序改变后重建表格)
287
+ tableKey: 0,
288
+ // 标记组件是否已销毁,避免销毁后仍操作 DOM
289
+ isDestroyed: false
290
+ };
291
+ },
292
+ mounted() {
293
+ // 等 DOM 渲染完成后初始化列拖拽
294
+ this.$nextTick(() => {
295
+ this.initColumnDrag();
296
+ });
297
+ },
298
+ beforeDestroy() {
299
+ this.isDestroyed = true;
300
+ if (this.headerSortable && this.headerSortable.destroy) {
301
+ this.headerSortable.destroy();
302
+ this.headerSortable = null;
303
+ }
304
+ },
305
+ computed: {
306
+ // 根据查询条件得到过滤后的数据列表
307
+ filteredRows() {
308
+ const subj = (this.filters.meetSubj || "").trim();
309
+ const belOrgNo = this.filters.belOrgNo || "";
310
+ const meetTypeCd = this.filters.meetTypeCd || "";
311
+ const [start, end] = Array.isArray(this.filters.createDateRange) ? this.filters.createDateRange : [];
312
+
313
+ const list = this.rows.filter(row => {
314
+ if (belOrgNo && row.belOrgNo !== belOrgNo) return false;
315
+ if (meetTypeCd && row.meetTypeCd !== meetTypeCd) return false;
316
+ if (subj && !row.meetSubj.includes(subj)) return false;
317
+ if (start && end) {
318
+ const d = row.createTm;
319
+ if (!d || d < start || d > end) return false;
320
+ }
321
+ return true;
322
+ });
323
+
324
+ if (this.sortField && this.sortOrder) {
325
+ const field = this.sortField;
326
+ const order = this.sortOrder === "ascending" ? 1 : -1;
327
+ list.sort((a, b) => {
328
+ const av = this.getTimeValue(a[field]);
329
+ const bv = this.getTimeValue(b[field]);
330
+ if (av === bv) return 0;
331
+ return av > bv ? order : -order;
332
+ });
333
+ }
334
+
335
+ return list;
336
+ },
337
+ // 当前页应显示的数据(在 filteredRows 基础上做前端分页)
338
+ pagedRows() {
339
+ const start = (this.page - 1) * this.pageSize;
340
+ return this.filteredRows.slice(start, start + this.pageSize);
341
+ }
342
+ },
343
+ watch: {
344
+ // 当过滤后的数据量变化时,自动纠正当前页,避免超出最大页码
345
+ filteredRows() {
346
+ const maxPage = Math.max(1, Math.ceil(this.filteredRows.length / this.pageSize) || 1);
347
+ if (this.page > maxPage) this.page = maxPage;
348
+ }
349
+ },
350
+ methods: {
351
+ /**
352
+ * 初始化表头拖拽
353
+ * 说明:
354
+ * - 使用 SortableJS 绑定在 thead 的 tr 元素上
355
+ * - handle: 仅允许拖拽 header-draggable 区域
356
+ * - draggable: 排除操作列表头(.meeting-header-action)
357
+ * - 序号列位于最左,其 th 索引为 0,不在 columns 数组中,因此需要 -1 做映射
358
+ */
359
+ initColumnDrag() {
360
+ if (this.isDestroyed) return;
361
+ if (this.headerSortable && this.headerSortable.destroy) {
362
+ this.headerSortable.destroy();
363
+ this.headerSortable = null;
364
+ }
365
+ const table = this.$refs.meetingTable;
366
+ if (!table || !table.$el) return;
367
+ const headerRow = table.$el.querySelector(".el-table__header-wrapper thead tr");
368
+ if (!headerRow) return;
369
+ this.headerSortable = Sortable.create(headerRow, {
370
+ animation: 150,
371
+ handle: ".header-draggable",
372
+ draggable: "th:not(.meeting-header-action)",
373
+ onEnd: evt => {
374
+ const { oldIndex, newIndex } = evt;
375
+ if (oldIndex == null || newIndex == null) return;
376
+ // 索引 0 对应序号列,从 1 开始才对应 columns 数组
377
+ const from = oldIndex - 1;
378
+ const to = newIndex - 1;
379
+ if (from < 0 || to < 0 || from >= this.columns.length || to >= this.columns.length) return;
380
+ const list = this.columns.slice();
381
+ const moved = list.splice(from, 1)[0];
382
+ list.splice(to, 0, moved);
383
+ this.columns = list;
384
+ this.tableKey += 1;
385
+ this.$nextTick(() => {
386
+ this.initColumnDrag();
387
+ });
388
+ }
389
+ });
390
+ },
391
+ /**
392
+ * 处理表格远程排序事件
393
+ * Element UI 会传入:{ column, prop, order }
394
+ * - prop:对应列的 prop,如 createTm / updateTm
395
+ * - order:ascending / descending / null
396
+ */
397
+ onSortChange({ prop, order }) {
398
+ this.sortField = prop || "";
399
+ this.sortOrder = order || "";
400
+ this.page = 1;
401
+ },
402
+ // 点击“查询”:仅重置到第一页,筛选逻辑在 computed 中统一处理
403
+ onSearch() {
404
+ this.page = 1;
405
+ },
406
+ // 点击“重置”:清空所有查询条件并回到第一页
407
+ onReset() {
408
+ this.filters = {
409
+ belOrgNo: "",
410
+ meetSubj: "",
411
+ meetTypeCd: "",
412
+ createDateRange: []
413
+ };
414
+ this.page = 1;
415
+ },
416
+ // 分页器切换页码
417
+ onPageChange(p) {
418
+ this.page = p;
419
+ },
420
+ // 将会议类型编码转换为中文文案
421
+ formatMeetType(code) {
422
+ if (code === "TRAINING") return "培训";
423
+ if (code === "MORNING") return "晨夕会";
424
+ return code || "";
425
+ },
426
+ // 根据会议类型返回不同颜色的 Tag 类型
427
+ getTypeTagType(code) {
428
+ if (code === "TRAINING") return "primary";
429
+ if (code === "MORNING") return "warning";
430
+ return "info";
431
+ },
432
+ // 格式化日期为 yyyy-MM-dd
433
+ formatDate(value) {
434
+ if (!value) return "";
435
+ if (value.length === 10) return value;
436
+ const d = new Date(value);
437
+ if (Number.isNaN(d.getTime())) return "";
438
+ const yyyy = d.getFullYear();
439
+ const mm = String(d.getMonth() + 1).padStart(2, "0");
440
+ const dd = String(d.getDate()).padStart(2, "0");
441
+ return `${yyyy}-${mm}-${dd}`;
442
+ },
443
+ // 格式化时间为 yyyy-MM-dd HH:mm:ss
444
+ formatDateTime(value) {
445
+ if (!value) return "";
446
+ if (value.length >= 16 && value.includes(" ")) return value;
447
+ const d = new Date(value);
448
+ if (Number.isNaN(d.getTime())) return "";
449
+ const yyyy = d.getFullYear();
450
+ const mm = String(d.getMonth() + 1).padStart(2, "0");
451
+ const dd = String(d.getDate() + 0).padStart(2, "0");
452
+ const hh = String(d.getHours()).padStart(2, "0");
453
+ const mi = String(d.getMinutes()).padStart(2, "0");
454
+ const ss = String(d.getSeconds()).padStart(2, "0");
455
+ return `${yyyy}-${mm}-${dd} ${hh}:${mi}:${ss}`;
456
+ },
457
+ // 将各种形式的日期值转换为时间戳,用于排序
458
+ getTimeValue(value) {
459
+ if (!value) return 0;
460
+ const d = new Date(value);
461
+ if (Number.isNaN(d.getTime())) return 0;
462
+ return d.getTime();
463
+ },
464
+ // 点击“详情”:弹出对话框展示会议详细信息
465
+ onView(row) {
466
+ const lines = [
467
+ `会议主题:${row.meetSubj || ""}`,
468
+ `内容:${row.plainContent || ""}`,
469
+ `所属机构:${row.organizationName || ""}`,
470
+ `会议类型:${this.formatMeetType(row.meetTypeCd)}`,
471
+ `创建人:${row.displayName || ""}`,
472
+ `创建日期:${this.formatDate(row.createTm)}`,
473
+ `最近编辑时间:${this.formatDateTime(row.updateTm)}`
474
+ ];
475
+ this.$alert(lines.join("\n"), "会议详情", {
476
+ confirmButtonText: "确定"
477
+ });
478
+ },
479
+ // 点击“删除”:二次确认后从当前 rows 中移除
480
+ onDelete(row) {
481
+ this.$confirm(`确定删除会议「${row.meetSubj || ""}」吗?`, "提示", {
482
+ type: "warning",
483
+ confirmButtonText: "确定",
484
+ cancelButtonText: "取消"
485
+ })
486
+ .then(() => {
487
+ this.rows = this.rows.filter(item => item.id !== row.id);
488
+ this.$message({
489
+ type: "success",
490
+ message: "删除成功"
491
+ });
492
+ })
493
+ .catch(() => {});
494
+ }
495
+ }
496
+ };
497
+ </script>
498
+
499
+ <style scoped>
500
+ .meeting-page {
501
+ padding: 24px;
502
+ }
503
+ .meeting-title {
504
+ margin-bottom: 16px;
505
+ }
506
+ .meeting-search-card {
507
+ padding: 12px 16px 4px;
508
+ background: #fafafa;
509
+ border-radius: 4px;
510
+ border: 1px solid #ebeef5;
511
+ margin-bottom: 12px;
512
+ }
513
+ .meeting-search-form {
514
+ display: flex;
515
+ flex-wrap: wrap;
516
+ }
517
+ .meeting-toolbar {
518
+ margin-bottom: 12px;
519
+ }
520
+ .meeting-subject {
521
+ font-weight: 500;
522
+ margin-bottom: 4px;
523
+ }
524
+ .meeting-content {
525
+ font-size: 12px;
526
+ color: #909399;
527
+ }
528
+ .meeting-pagination {
529
+ margin-top: 12px;
530
+ text-align: right;
531
+ }
532
+ .header-wrapper {
533
+ display: inline-flex;
534
+ align-items: center;
535
+ }
536
+ .header-draggable {
537
+ cursor: move;
538
+ }
539
+ .header-label {
540
+ white-space: nowrap;
541
+ }
542
+ </style>
@@ -1,5 +1,6 @@
1
1
  import LineChartPage from '../pages/LineChartPage.vue'
2
2
  import MedalSetting from '../pages/MedalSetting.vue'
3
+ import MeetingListPage from '../pages/MeetingListPage.vue'
3
4
 
4
5
  export const routes = [
5
6
  {
@@ -25,5 +26,15 @@ export const routes = [
25
26
  icon: 'el-icon-medal',
26
27
  showInMenu: true
27
28
  }
29
+ },
30
+ {
31
+ path: '/meeting-list',
32
+ name: 'MeetingList',
33
+ component: MeetingListPage,
34
+ meta: {
35
+ title: '会议列表',
36
+ icon: 'el-icon-date',
37
+ showInMenu: true
38
+ }
28
39
  }
29
40
  ]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vercerl-express-test",
3
- "version": "1.0.0",
3
+ "version": "1.0.3",
4
4
  "description": "",
5
5
  "scripts": {
6
6
  "dev": "nodemon --watch src --ext ts --exec \"ts-node src/app.ts\"",
@@ -9,6 +9,7 @@
9
9
  "typecheck": "tsc --noEmit",
10
10
  "lint": "eslint .",
11
11
  "test": "npm run build && node --test",
12
+ "front-dev": "cd frontEnd && npm run serve",
12
13
  "front-build": "cd frontEnd && npm run build",
13
14
  "copy-frontend": "rm -rf dist/public && mkdir -p dist/public && cp -R frontEnd/dist/* dist/public/",
14
15
  "build:all": "npm run build && npm run front-build && npm run copy-frontend",
@@ -19,7 +20,7 @@
19
20
  "登录": "npm login ",
20
21
  "一键发布": "npm version patch && npm publish"
21
22
  },
22
- "keywords": [
23
+ "keywords": [
23
24
  "express",
24
25
  "web",
25
26
  "application"