vercerl-express-test 1.0.0

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.
@@ -0,0 +1,571 @@
1
+ <template>
2
+ <section class="medal-settings-page">
3
+ <h1>勋章设置</h1>
4
+ <p class="desc">展示勋章基础信息,可查看启用状态与基本配置。</p>
5
+
6
+ <div class="toolbar">
7
+ <el-button type="primary" size="mini" @click="onOpenCreate">
8
+ 新增
9
+ </el-button>
10
+ </div>
11
+
12
+ <el-dialog
13
+ title="创建"
14
+ :visible.sync="dialogVisible"
15
+ width="720px"
16
+ :close-on-click-modal="false"
17
+ >
18
+ <el-form :model="formMedal" label-width="90px" size="small" class="create-form">
19
+ <el-form-item label="授予主体">
20
+ <el-select v-model="formMedal.confeSubKno" placeholder="请选择授予主体">
21
+ <el-option
22
+ v-for="opt in subjectOptions"
23
+ :key="opt.value"
24
+ :label="opt.label"
25
+ :value="opt.value"
26
+ />
27
+ </el-select>
28
+ </el-form-item>
29
+ <el-form-item label="勋章名称">
30
+ <el-input v-model="formMedal.medalName" placeholder="请输入勋章名称" />
31
+ </el-form-item>
32
+ <el-form-item label="授予方式">
33
+ <el-input :value="formatConfeMode('4')" disabled />
34
+ </el-form-item>
35
+ <el-form-item label="是否限时">
36
+ <el-select v-model="formMedal.limitTmFlag" placeholder="请选择">
37
+ <el-option
38
+ v-for="opt in limitTimeOptions"
39
+ :key="opt.value"
40
+ :label="opt.label"
41
+ :value="opt.value"
42
+ />
43
+ </el-select>
44
+ </el-form-item>
45
+ <el-form-item v-if="formMedal.limitTmFlag === 'Y'" label="限时时长">
46
+ <div class="inline-group">
47
+ <el-input-number v-model="formMedal.wearDurat" :min="1" />
48
+ <el-select v-model="formMedal.tmUnitType" style="margin-left: 8px; width: 120px">
49
+ <el-option
50
+ v-for="opt in tmUnitOptions"
51
+ :key="opt.value"
52
+ :label="opt.label"
53
+ :value="opt.value"
54
+ />
55
+ </el-select>
56
+ </div>
57
+ </el-form-item>
58
+ <el-form-item label="是否限量">
59
+ <el-select v-model="formMedal.limitQtyFlag" placeholder="请选择">
60
+ <el-option
61
+ v-for="opt in limitQtyOptions"
62
+ :key="opt.value"
63
+ :label="opt.label"
64
+ :value="opt.value"
65
+ />
66
+ </el-select>
67
+ </el-form-item>
68
+ <el-form-item v-if="formMedal.limitQtyFlag === 'Y'" label="勋章数量">
69
+ <el-input-number v-model="formMedal.medalQty" :min="1" />
70
+ </el-form-item>
71
+ <el-form-item label="勋章描述">
72
+ <el-input
73
+ type="textarea"
74
+ v-model="formMedal.medalDesc"
75
+ :rows="3"
76
+ maxlength="150"
77
+ show-word-limit
78
+ placeholder="请输入勋章描述"
79
+ />
80
+ </el-form-item>
81
+ <div class="upload-block">
82
+ <div class="upload-label">
83
+ 静态勋章
84
+ </div>
85
+ <div class="upload-content">
86
+ <el-upload
87
+ class="avatar-uploader"
88
+ action="#"
89
+ :auto-upload="false"
90
+ :show-file-list="false"
91
+ :before-upload="onBeforeUpload"
92
+ >
93
+ <el-button size="small" type="primary">选择图片</el-button>
94
+ <div slot="tip" class="upload-tip">
95
+ 支持上传 PNG、JPG、JPEG 格式文件,文件大小小于 10M
96
+ </div>
97
+ </el-upload>
98
+ </div>
99
+ </div>
100
+ </el-form>
101
+ <span slot="footer" class="dialog-footer">
102
+ <el-button size="small" @click="dialogVisible = false">取消</el-button>
103
+ <el-button size="small" type="primary" @click="onSave">
104
+ 保存
105
+ </el-button>
106
+ </span>
107
+ </el-dialog>
108
+
109
+ <MedalIssueDialog
110
+ :visible.sync="issueDialogVisible"
111
+ :medal="issueCurrentMedal"
112
+ :limit-count="issueCurrentMedal && issueCurrentMedal.limitQtyFlag === 'Y' ? Number(issueCurrentMedal.medalQty || 0) : -1"
113
+ @confirm="onIssueConfirm"
114
+ />
115
+
116
+ <el-table
117
+ ref="medalTable"
118
+ :key="tableKey"
119
+ :data="rows"
120
+ border
121
+ stripe
122
+ size="small"
123
+ style="width: 100%"
124
+ >
125
+ <el-table-column
126
+ v-for="col in columns"
127
+ :key="col.key || col.prop"
128
+ :prop="col.prop"
129
+ :label="col.label"
130
+ :width="col.width"
131
+ :min-width="col.minWidth"
132
+ :align="col.align || 'left'"
133
+ :header-align="col.headerAlign || col.align || 'left'"
134
+ :fixed="col.fixed"
135
+ >
136
+ <template slot="header">
137
+ <div class="header-wrapper header-draggable">
138
+ <span class="header-label">{{ col.label }}</span>
139
+ </div>
140
+ </template>
141
+ <template slot-scope="scope">
142
+ <span v-if="col.type === 'index'">
143
+ {{ scope.$index + 1 }}
144
+ </span>
145
+ <div v-else-if="col.type === 'icon'" class="medal-icon">
146
+ <img
147
+ v-if="scope.row.filePicUrl"
148
+ :src="scope.row.filePicUrl"
149
+ alt=""
150
+ />
151
+ </div>
152
+ <span v-else-if="col.type === 'confeMode'">
153
+ {{ formatConfeMode(scope.row.confeMode) }}
154
+ </span>
155
+ <span v-else-if="col.type === 'limitTmFlag'">
156
+ {{ scope.row.limitTmFlag === "Y" ? "限时" : "不限时" }}
157
+ </span>
158
+ <span v-else-if="col.type === 'wearDurat'">
159
+ <span v-if="scope.row.limitTmFlag === 'Y'">
160
+ {{ scope.row.wearDurat }}{{ formatTmUnit(scope.row.tmUnitType) }}
161
+ </span>
162
+ <span v-else>
163
+ 不限期
164
+ </span>
165
+ </span>
166
+ <span v-else-if="col.type === 'medalQty'">
167
+ <span v-if="scope.row.limitQtyFlag === 'Y'">
168
+ {{ scope.row.medalQty }}
169
+ </span>
170
+ <span v-else>
171
+ 不限量
172
+ </span>
173
+ </span>
174
+ <span v-else-if="col.type === 'createDate'">
175
+ {{ formatDate(scope.row.createTm) }}
176
+ </span>
177
+ <span v-else-if="col.type === 'updateDate'">
178
+ {{ formatDate(scope.row.updateTm) }}
179
+ </span>
180
+ <span v-else-if="col.type === 'switch'">
181
+ <el-switch
182
+ v-model="scope.row.activaFlag"
183
+ active-value="Y"
184
+ inactive-value="N"
185
+ active-text="启用"
186
+ inactive-text="停用"
187
+ @change="onToggleActive(scope.row)"
188
+ />
189
+ </span>
190
+ <span v-else-if="col.type === 'actions'">
191
+ <el-button type="text" size="mini" @click="onPreview(scope.row)">
192
+ 预览
193
+ </el-button>
194
+ <el-button
195
+ type="text"
196
+ size="mini"
197
+ :disabled="scope.row.activaFlag !== 'Y'"
198
+ @click="onOpenIssue(scope.row)"
199
+ >
200
+ 发放
201
+ </el-button>
202
+ <el-button type="text" size="mini" @click="onMore(scope.row)">
203
+ 更多
204
+ </el-button>
205
+ </span>
206
+ <span v-else>
207
+ {{ scope.row[col.prop] }}
208
+ </span>
209
+ </template>
210
+ </el-table-column>
211
+ </el-table>
212
+ </section>
213
+ </template>
214
+
215
+ <script>
216
+ import Sortable from "sortablejs";
217
+ import { get } from "../utils/request";
218
+ import MedalIssueDialog from "./MedalIssueDialog.vue";
219
+
220
+ export default {
221
+ name: "MedalSettingsView",
222
+ components: {
223
+ MedalIssueDialog
224
+ },
225
+ data() {
226
+ return {
227
+ rows: [],
228
+ columns: [
229
+ { key: "index", label: "序号", width: 60, align: "center", type: "index" },
230
+ { key: "medalNo", prop: "medalNo", label: "勋章编号", minWidth: 180 },
231
+ { key: "icon", label: "勋章图标", width: 90, align: "center", type: "icon" },
232
+ { key: "medalName", prop: "medalName", label: "勋章名称", minWidth: 140 },
233
+ { key: "medalObjName", prop: "medalObjName", label: "勋章对象", minWidth: 140 },
234
+ { key: "confeMode", prop: "confeMode", label: "发放方式", width: 120, align: "center", type: "confeMode" },
235
+ { key: "limitTmFlag", prop: "limitTmFlag", label: "是否限时", width: 100, align: "center", type: "limitTmFlag" },
236
+ { key: "wearDurat", prop: "wearDurat", label: "限时时长", width: 120, align: "center", type: "wearDurat" },
237
+ { key: "medalQty", prop: "medalQty", label: "发放数量", width: 100, align: "center", type: "medalQty" },
238
+ { key: "issuedQty", prop: "issuedQty", label: "已发数量", width: 100, align: "center" },
239
+ { key: "createTm", prop: "createTm", label: "创建日期", width: 160, align: "center", type: "createDate" },
240
+ { key: "updateTm", prop: "updateTm", label: "更新日期", width: 160, align: "center", type: "updateDate" },
241
+ { key: "activaFlag", prop: "activaFlag", label: "启用/停用", width: 120, align: "center", type: "switch" },
242
+ { key: "actions", label: "操作", width: 180, align: "center", fixed: "right", type: "actions" }
243
+ ],
244
+ headerSortable: null,
245
+ tableKey: 0,
246
+ isDestroyed: false,
247
+ dialogVisible: false,
248
+ issueDialogVisible: false,
249
+ issueCurrentMedal: null,
250
+ issueUserPool: [],
251
+ issueMedalUserMap: {},
252
+ issueSelectedUsers: [],
253
+ issueLeftKeyword: "",
254
+ issueRightKeyword: "",
255
+ issueLeftPage: 1,
256
+ issueRightPage: 1,
257
+ issueLeftPageSize: 10,
258
+ issueRightPageSize: 10,
259
+ issueLeftSelected: [],
260
+ issueRightSelected: [],
261
+ formMedal: {
262
+ confeSubKno: "CU",
263
+ medalName: "",
264
+ limitTmFlag: "N",
265
+ limitQtyFlag: "N",
266
+ wearDurat: 12,
267
+ tmUnitType: "D",
268
+ medalQty: 0,
269
+ medalDesc: "",
270
+ filePicUrl: ""
271
+ },
272
+ subjectOptions: [
273
+ { label: "客户经理", value: "CU" }
274
+ ],
275
+ limitTimeOptions: [
276
+ { label: "不限时", value: "N" },
277
+ { label: "限时", value: "Y" }
278
+ ],
279
+ limitQtyOptions: [
280
+ { label: "不限量", value: "N" },
281
+ { label: "限量", value: "Y" }
282
+ ],
283
+ tmUnitOptions: [
284
+ { label: "日", value: "D" },
285
+ { label: "月", value: "M" },
286
+ { label: "年", value: "Y" }
287
+ ]
288
+ };
289
+ },
290
+ created() {
291
+ this.fetchData();
292
+ },
293
+ computed: {
294
+ issueLeftTotal() {
295
+ return this.issueLeftFiltered.length;
296
+ },
297
+ issueRightTotal() {
298
+ return this.issueRightFiltered.length;
299
+ },
300
+ issueLeftFiltered() {
301
+ const selectedSet = new Set((this.issueSelectedUsers || []).map(u => u.userNo));
302
+ const keyword = (this.issueLeftKeyword || "").trim();
303
+ const pool = Array.isArray(this.issueUserPool) ? this.issueUserPool : [];
304
+ const list = pool.filter(u => !selectedSet.has(u.userNo));
305
+ if (!keyword) return list;
306
+ return list.filter(u => this.formatUserLabel(u).includes(keyword));
307
+ },
308
+ issueRightFiltered() {
309
+ const keyword = (this.issueRightKeyword || "").trim();
310
+ const list = Array.isArray(this.issueSelectedUsers) ? this.issueSelectedUsers : [];
311
+ if (!keyword) return list;
312
+ return list.filter(u => this.formatUserLabel(u).includes(keyword));
313
+ },
314
+ issueLeftPageRows() {
315
+ const start = (this.issueLeftPage - 1) * this.issueLeftPageSize;
316
+ return this.issueLeftFiltered.slice(start, start + this.issueLeftPageSize);
317
+ },
318
+ issueRightPageRows() {
319
+ const start = (this.issueRightPage - 1) * this.issueRightPageSize;
320
+ return this.issueRightFiltered.slice(start, start + this.issueRightPageSize);
321
+ }
322
+ },
323
+ mounted() {
324
+ this.$nextTick(() => {
325
+ this.initColumnDrag();
326
+ });
327
+ },
328
+ beforeDestroy() {
329
+ this.isDestroyed = true;
330
+ if (this.headerSortable && this.headerSortable.destroy) {
331
+ this.headerSortable.destroy();
332
+ this.headerSortable = null;
333
+ }
334
+ },
335
+ methods: {
336
+ fetchData() {
337
+ get('/medal/list').then(res => {
338
+ if (res && res.data) {
339
+ this.rows = res.data;
340
+ } else if (Array.isArray(res)) {
341
+ this.rows = res;
342
+ }
343
+ }).catch(err => {
344
+ console.error(err);
345
+ this.$message.error('获取勋章列表失败');
346
+ });
347
+ },
348
+ onOpenCreate() {
349
+ this.resetForm();
350
+ this.dialogVisible = true;
351
+ },
352
+ onOpenIssue(row) {
353
+ if (!row) return;
354
+ if (row.activaFlag !== "Y") {
355
+ this.$message({
356
+ type: "warning",
357
+ message: "该勋章已停用,无法发放"
358
+ });
359
+ return;
360
+ }
361
+ this.issueCurrentMedal = row;
362
+ this.issueDialogVisible = true;
363
+ },
364
+ onIssueConfirm(data) {
365
+ const { medal, users } = data;
366
+ if (!medal) return;
367
+ const total = users.length;
368
+
369
+ const key = medal.medalNo || String(medal.id || "");
370
+ const nextRows = this.rows.map(r => {
371
+ if ((r.medalNo || String(r.id || "")) !== key) return r;
372
+ return Object.assign({}, r, { issuedQty: total, updateTm: new Date().toISOString() });
373
+ });
374
+ this.rows = nextRows;
375
+ this.issueDialogVisible = false;
376
+ this.$message({
377
+ type: "success",
378
+ message: `已发放勋章「${medal.medalName || medal.medalNo}」,共 ${total} 人`
379
+ });
380
+ },
381
+ resetForm() {
382
+ this.formMedal = {
383
+ confeSubKno: "CU",
384
+ medalName: "",
385
+ limitTmFlag: "N",
386
+ limitQtyFlag: "N",
387
+ wearDurat: 12,
388
+ tmUnitType: "D",
389
+ medalQty: 0,
390
+ medalDesc: "",
391
+ filePicUrl: ""
392
+ };
393
+ },
394
+ onBeforeUpload() {
395
+ this.formMedal.filePicUrl = "";
396
+ return false;
397
+ },
398
+ onSave() {
399
+ const now = new Date().toISOString();
400
+ const subject = this.subjectOptions.find(item => item.value === this.formMedal.confeSubKno);
401
+ const params = {
402
+ activaFlag: "Y",
403
+ confeMode: "4",
404
+ confeSubKno: this.formMedal.confeSubKno,
405
+ createTm: now,
406
+ creatorNo: "046083",
407
+ fileGifName: "",
408
+ fileGifSize: "",
409
+ fileGifUrl: "",
410
+ filePicName: "",
411
+ filePicSize: "",
412
+ filePicUrl: this.formMedal.filePicUrl || "",
413
+ limitQtyFlag: this.formMedal.limitQtyFlag,
414
+ limitTmFlag: this.formMedal.limitTmFlag,
415
+ medalDesc: this.formMedal.medalDesc,
416
+ medalName: this.formMedal.medalName,
417
+ tmUnitType: this.formMedal.tmUnitType,
418
+ updateStaffNo: "046083",
419
+ updateTm: now,
420
+ medalQty: this.formMedal.limitQtyFlag === "Y" ? this.formMedal.medalQty : 0
421
+ };
422
+ console.log("params:", params);
423
+ const row = {
424
+ id: Date.now(),
425
+ ...params,
426
+ medalObjName: subject ? `授予${subject.label}` : "",
427
+ issuedQty: 0,
428
+ wearDurat: this.formMedal.limitTmFlag === "Y" ? this.formMedal.wearDurat : 0
429
+ };
430
+ this.rows = this.rows.concat(row);
431
+ this.dialogVisible = false;
432
+ this.$message({
433
+ type: "success",
434
+ message: "保存成功"
435
+ });
436
+ },
437
+ initColumnDrag() {
438
+ if (this.isDestroyed) return;
439
+ if (this.headerSortable && this.headerSortable.destroy) {
440
+ this.headerSortable.destroy();
441
+ this.headerSortable = null;
442
+ }
443
+ const table = this.$refs.medalTable;
444
+ if (!table || !table.$el) return;
445
+ const headerRow = table.$el.querySelector(".el-table__header-wrapper thead tr");
446
+ if (!headerRow) return;
447
+ this.headerSortable = Sortable.create(headerRow, {
448
+ animation: 150,
449
+ handle: ".header-draggable",
450
+ draggable: "th",
451
+ onEnd: (evt) => {
452
+ const { oldIndex, newIndex } = evt;
453
+ if (oldIndex == null || newIndex == null) return;
454
+ if (oldIndex === newIndex) return;
455
+ const from = oldIndex;
456
+ const to = newIndex;
457
+ if (from < 0 || to < 0 || from >= this.columns.length || to >= this.columns.length) return;
458
+ const list = this.columns.slice();
459
+ [list[from], list[to]] = [list[to], list[from]];
460
+ this.columns = list;
461
+ this.tableKey += 1;
462
+ this.$nextTick(() => {
463
+ this.initColumnDrag();
464
+ });
465
+ }
466
+ });
467
+ },
468
+ formatConfeMode(value) {
469
+ if (value === "4") return "手动发放";
470
+ return value || "";
471
+ },
472
+ formatTmUnit(value) {
473
+ if (value === "D") return "日";
474
+ if (value === "M") return "月";
475
+ if (value === "Y") return "年";
476
+ return "";
477
+ },
478
+ formatDate(value) {
479
+ if (!value) return "";
480
+ const d = new Date(value);
481
+ if (Number.isNaN(d.getTime())) return "";
482
+ const yyyy = d.getFullYear();
483
+ const mm = String(d.getMonth() + 1).padStart(2, "0");
484
+ const dd = String(d.getDate()).padStart(2, "0");
485
+ return `${yyyy}-${mm}-${dd}`;
486
+ },
487
+ onToggleActive(row) {
488
+ const flag = row.activaFlag === "Y" ? "启用" : "停用";
489
+ this.$message({
490
+ type: "success",
491
+ message: `已${flag}勋章「${row.medalName || row.medalNo}」`
492
+ });
493
+ },
494
+ onPreview(row) {
495
+ this.$alert(
496
+ `勋章名称:${row.medalName}\n勋章编号:${row.medalNo}`,
497
+ "勋章预览",
498
+ {
499
+ confirmButtonText: "确定"
500
+ }
501
+ );
502
+ },
503
+ onMore(row) {
504
+ this.$message({
505
+ type: "info",
506
+ message: `更多操作:${row.medalName || row.medalNo}`
507
+ });
508
+ }
509
+ }
510
+ };
511
+ </script>
512
+
513
+ <style scoped>
514
+ .medal-settings-page {
515
+ padding: 24px;
516
+ }
517
+ .desc {
518
+ margin-bottom: 12px;
519
+ color: #666;
520
+ }
521
+ .toolbar {
522
+ margin-bottom: 12px;
523
+ }
524
+ .create-form {
525
+ max-height: 520px;
526
+ overflow-y: auto;
527
+ }
528
+ .inline-group {
529
+ display: flex;
530
+ align-items: center;
531
+ }
532
+ .upload-block {
533
+ display: flex;
534
+ align-items: flex-start;
535
+ margin-top: 12px;
536
+ }
537
+ .upload-label {
538
+ width: 90px;
539
+ flex-shrink: 0;
540
+ padding-top: 4px;
541
+ color: #606266;
542
+ }
543
+ .upload-content {
544
+ flex: 1;
545
+ }
546
+ .upload-tip {
547
+ margin-top: 6px;
548
+ font-size: 12px;
549
+ color: #909399;
550
+ }
551
+ .header-wrapper {
552
+ display: inline-flex;
553
+ align-items: center;
554
+ }
555
+ .header-draggable {
556
+ cursor: move;
557
+ }
558
+ .header-label {
559
+ white-space: nowrap;
560
+ }
561
+ .medal-icon {
562
+ display: flex;
563
+ justify-content: center;
564
+ align-items: center;
565
+ }
566
+ .medal-icon img {
567
+ width: 40px;
568
+ height: 40px;
569
+ border-radius: 4px;
570
+ }
571
+ </style>
@@ -0,0 +1,12 @@
1
+ import Vue from 'vue'
2
+ import Router from 'vue-router'
3
+
4
+ import { routes } from './routes'
5
+
6
+ Vue.use(Router)
7
+
8
+ export default new Router({
9
+ mode: 'hash',
10
+ routes
11
+ })
12
+
@@ -0,0 +1,29 @@
1
+ import LineChartPage from '../pages/LineChartPage.vue'
2
+ import MedalSetting from '../pages/MedalSetting.vue'
3
+
4
+ export const routes = [
5
+ {
6
+ path: '/',
7
+ redirect: '/line-chart'
8
+ },
9
+ {
10
+ path: '/line-chart',
11
+ name: 'LineChart',
12
+ component: LineChartPage,
13
+ meta: {
14
+ title: '折线图',
15
+ icon: 'el-icon-data-line',
16
+ showInMenu: true
17
+ }
18
+ },
19
+ {
20
+ path: '/medal-setting',
21
+ name: 'MedalSetting',
22
+ component: MedalSetting,
23
+ meta: {
24
+ title: '勋章设置',
25
+ icon: 'el-icon-medal',
26
+ showInMenu: true
27
+ }
28
+ }
29
+ ]
@@ -0,0 +1,28 @@
1
+ import axios from 'axios'
2
+
3
+ const service = axios.create({
4
+ baseURL: process.env.VUE_APP_API_BASE || 'http://localhost:3000/api',
5
+ timeout: 15000
6
+ })
7
+
8
+ service.interceptors.request.use(
9
+ config => config,
10
+ error => Promise.reject(error)
11
+ )
12
+
13
+ service.interceptors.response.use(
14
+ response => {
15
+ const res = response.data
16
+ if (res && typeof res === 'object' && 'code' in res) {
17
+ if (res.code === 0) return res.data
18
+ return Promise.reject(res)
19
+ }
20
+ return res
21
+ },
22
+ error => Promise.reject(error)
23
+ )
24
+
25
+ export const get = (url, params = {}, config = {}) => service.get(url, { params, ...config })
26
+ export const post = (url, data = {}, config = {}) => service.post(url, data, config)
27
+
28
+ export default service
@@ -0,0 +1,8 @@
1
+ module.exports = {
2
+ devServer: {
3
+ port: 8080, // 本地开发服务器端口
4
+ proxy: null // 代理配置,如果需要代理API请求,可以在这里配置
5
+ },
6
+ outputDir: 'dist', // 构建输出目录
7
+ assetsDir: 'static' // 静态资源目录
8
+ }