Allureluoli 二月FEBRUARY

区块链国赛笔记 12|Vue 与 Thymeleaf:比赛前端源码阅读

这份前端采用脚本方式组织 Vue 组件。按入口、主页面、登录页和公共组件阅读,比直接从长文件中逐行追踪更容易建立数据流。

本文由个人备赛旧笔记整理,整理日期为 2026-11-03。命令、输出和测试记录属于当时的实验环境;本文未重新搭建整套环境验证。

阅读前先确认

保留的是 Vue 2 风格源码分析,依赖模板、静态资源和后端接口,不能当作一个独立的 Vue 3 项目直接运行。

Spring-Thymeleaf

前端

Thymeleaf + Vue.js

Thymeleaf是Java的模板引擎

实例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<!-- 引入样式 -->
<link rel="shortcut icon" href="./static/images/favicon.ico">
<link rel="stylesheet" href="./static/lib/element.css">
<link rel="stylesheet" href="./static/style/index.css">
</head>

<body>
<div id="app">1111122222</div>
<!-- 引入组件库 -->
<script type="text/javascript" src="./static/lib/vue.min.js"></script>
<script type="text/javascript" src="./static/lib/element.js"></script>
<script type="text/javascript" src="./static/lib/axios.min.js"></script>

<!-- 引入 vue 实例及子组件 -->
<script src="./static/src/pages/components.js"></script>
<script src="./static/src/pages/login.js"></script>
<script src="./static/src/pages/main.js"></script>
<script src="./static/src/index.js"></script>
</body>

</html>

没什么好看的

Spring-vue

关于vue 组件

这里面的vue代码真的写的又臭又长,索性文件不多还是很好分析的,这也是里面占比只占10%的原因。

1
2
3
4
5
6
7
E:.
│ index.js
│
└─pages
components.js
login.js
main.js

我们直接上源码分析

index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
const vue = new Vue({
// 注册 Header、Login 子组件
components: {
Header,
Login,
},
// 实例绑定的元素
el: '#app',
data: {
currPage: 1, // 当前页
pageSize: 10, // 每页显示数据
total: 0, // 总数据
showComponent: Farm, // 当前展示的动态组件
login: false, // 登录状态
user: '', // 当前用户
},
template: `
<el-container>

<!-- 头部导航组件 -->
<el-header class="header"><Header :user="user" :login="login" @logout="(val) => {this.login = val;}" /></el-header>

<!-- 主要内容组件 -->
<el-container>
<el-main>

<!-- 登录组件 -->
<Login v-if="!login" @login="(val) => {this.login = true; this.showComponent = val.component; this.user = val.user;}" />


<!-- 动态组件,根据 showComponent 动态渲染当前用户 -->
<component v-else :is="showComponent"
@logout="(val) => {this.login = val;}"
@total="(val) => {this.total = val;}"
:curr-page.sync="currPage" :page-size.sync="pageSize">
</component>

<!-- 分页组件 -->
<el-pagination
style="text-align: center; margin-top: 10px;"
v-if="login && showComponent.name !== 'Consumer'"
background
:hide-on-single-page="false"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page.sync="currPage"
:page-sizes="[10, 20, 50]"
:page-size.sync="pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="total">
</el-pagination>
</el-main>
</el-container>
</el-container>
`,
methods: {
// 处理每页数据渲染数
handleSizeChange(val) {
this.pageSize = val;
},
// 处理翻页
handleCurrentChange(val) {
this.currPage = val;
},
}
})
  1. 未登录时 → 显示登录界面
  2. 登录后 → 显示功能页面(农场/消费者等)
  3. 头部导航始终显示用户信息
  4. 分页控件用于数据翻页

核心逻辑:

  • 登录成功:login = true,显示对应页面
  • 退出登录:login = false,回到登录界面

main.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
// 农场组件
const Farm = {
// 接受分页属性,用来控制翻页
props: ['currPage', 'pageSize'],
data() {
return {
popup: false, // 创建蔬菜弹窗
foodList: [], // 食品信息
}
},
computed: {
// 当前页渲染数据
renderList() {
return this.foodList.slice((this.currPage - 1) * this.pageSize, this.currPage * this.pageSize)
}
},
// 注册创建蔬菜子组件
components: {
CreateVegetable
},
template: `
<div>
<!-- 创建蔬菜按钮 -->
<el-button type="primary" @click="popup = true;">新建食品</el-button>

<!-- 退出按钮 -->
<el-button type="danger" @click="$emit('logout', false)" class="button-logout">退出</el-button>

<!-- 食品信息列表 -->
<el-table
:data="renderList"
style="width: 100%"
:default-sort = "{prop: 'date', order: 'descending'}"
>
<el-table-column
prop="traceNumber"
label="溯源码"
>
</el-table-column>
<el-table-column
prop="foodName"
label="食品名称"
>
</el-table-column>
<el-table-column
prop="traceName"
label="生产商"
>
</el-table-column>
<el-table-column
prop="date"
label="生产时间"
sortable
>
</el-table-column>
</el-table>

<!-- 创建蔬菜子组件 -->
<CreateVegetable :dialogFormVisible="popup" @popup="handlePopup" @confirmPopup="formSubmit" />
</div>
`,
mounted() {
// 挂载时获取农场数据
this.getData();
},
methods: {
// 处理数据请求
getData() {
axios({
method: 'get',
url: `/producing`
})
.then(ret => {
// 对数据进行处理
this.foodList = ret.data
.map(item => ({
...item,
date: dateTime(item.timestamp),
traceNumber: item.traceNumber,
traceName: item.from,
foodName: item.name
}))
.reverse();
this.$emit('total', this.foodList.length);
})
.catch(err => {
console.log(err)
})
},
// 添加蔬菜弹窗
handlePopup(val) {
this.popup = val;
},
// 添加蔬菜提交
formSubmit(val) {
axios({
method: 'post',
url: '/produce',
data: {
...val,
quality: parseInt(val.quality),
user: 0
}
})
.then(ret => {
if (ret.data.ret !== 1) {
// 已有溯源码校验
if (ret.data.ret === 0 && ret.data.msg === 'traceNumber already exist') {
this.$message({
message: '该溯源码已存在,请重新创建',
type: 'error',
center: true
});
} else {
this.$message({
message: '提交失败',
type: 'error',
center: true
});
}
return
}
this.$message({
message: '提交成功',
type: 'success',
center: true
});
// 提交成功后重新获取数据
this.getData();
})
.catch(err => {
console.log(err)
})
this.popup = false;
}
}
}

// 中间商组件
const Agent = {
// 和超市的职能差不多,放在 mixin 里面
mixins: [mixin],
data() {
return {
user: 1, // user: 1 为中间商
}
}
}

// 超市组件
const Mall = {
// 和中间商的职能差不多,放在 mixin 里面
mixins: [mixin],
data() {
return {
user: 2, // user: 2 为超市
}
}
}

// 消费者组件
const Consumer = {
// 溯源详情子组件注册
components: {
TraceDetail
},
data() {
return {
form: {
traceNumber: '', // 输入的溯源码
},
foodDetail: '', // 溯源详细信息
onSearch: false, // 搜索情况
}
},
template:`
<el-row style="height: 100%;" class="consumer">
<!-- 左边为溯源查询 -->
<el-col :span="7" style="height: 100%;">
<div class="grid-content bg-purple">
<el-form :model="form" ref="form">
<h4>溯源码搜索</h4>
<el-divider></el-divider>
<el-form-item
label="请输入您想要查询的溯源码"
prop="traceNumber"
>
<el-input v-model.number="form.traceNumber" type="textarea" :rows="3" autocomplete="off" @clear="onSearch = false;"></el-input>
</el-form-item>
<el-button type="primary" @click="onSubmit">查询</el-button>
</el-form>
</div>
</el-col>

<!-- 右边为溯源查询结果 -->
<el-col :span="16">
<div class="grid-content bg-purple-light">
<div v-if ="!onSearch" class="consumer-tip">请在左侧查询栏中输入溯源码进行查询</div>
<div v-if ="onSearch && foodDetail.length === 0" class="consumer-tip">该溯源码无对应信息,请确认后重新查询</div>

<!-- 溯源查询成功后才展示 -->
<div v-if ="onSearch && foodDetail.length">
<!-- 溯源详情组件, user: 3 为消费者 -->
<TraceDetail :food-detail="foodDetail" :user="3" />
</div>
</div>
</el-col>
<el-col :span="1">
<!--退出按钮 -->
<el-button type="danger" @click="$emit('logout', false)" class="button-logout">退出</el-button>
</el-col>
</el-row>
`,
methods: {
// 处理按溯源码查询
onSubmit() {
if (!this.form.traceNumber) {
this.$message.error('请输入溯源码');
return;
}
this.onSearch = true;
axios({
method: 'get',
url: `/trace?traceNumber=${this.form.traceNumber}`
})
.then(ret => {
this.foodDetail = ret.data || []
})
.catch(err => {
console.log(err);
})
}
}
}

Farm(农场)

  • 功能:生产食品,显示食品列表
  • 特点:可以新建食品,有分页表格

Agent(中间商)和 Mall(超市)

  • 功能:基本相同(使用mixin共享代码)
  • 区别:user值不同(1=中间商,2=超市)

Consumer(消费者)

  • 功能:查询食品溯源信息
  • 特点:输入溯源码查看食品来源

核心区别:

  • 农场:生产食品
  • 中间商/超市:流通食品
  • 消费者:查询食品信息

就像食品产业链:生产→流通→消费!

login.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
// 登录组件
const Login = {
data() {
return {
// 用户身份
users: [
{
name: '生产商',
userName: 'producer',
component: Farm,
},
{
name: '中间商',
userName: 'distributor',
component: Agent,
},
{
name: '超市',
userName: 'retailer',
component: Mall,
},
{
name: '消费者',
userName: 'consumer',
component: Consumer,
},
],
currentUser: null, // 当前用户
address: '', // 角色地址
countdown: 5, // 倒计时时间
loginItem: '', // 登录用户信息
timer: null, // 处理倒计时的定时器
}
},
template: `
<div class="login">
<!-- 角色选择 -->
<h3 v-if="currentUser === null">请选择您的角色</h3>
<el-row :gutter="80" v-if="currentUser === null">
<el-col :span="6" v-for="(item, index) in users" :key="index">
<div @click="handleClick(item)">{{ item.name }}</div>
</el-col>
</el-row>

<!-- 角色登录 -->
<div v-else class="is-login">
<h3>登录中......(倒计时:{{ countdown }} 秒)</h3>
<div>角色:
<span>{{ currentUser }}</span>
</div>

<!-- 非消费者则显示角色地址 -->
<div v-if="currentUser !== '消费者'">角色地址:
<span>{{ address }}</span>
</div>

<!-- 直接登录按钮 -->
<el-button type="primary" @click="clearTimer">直接登录</el-button>
</div>
</div>
`,
methods: {
// 登录时有个5秒的倒计时,这里是在点击直接登录时,清楚倒计时,直接跳到相关页面
clearTimer() {
clearInterval(this.timer);
this.$emit('login', {
component: this.loginItem.component,
user: this.loginItem.name,
});
},
// 倒计时
countdownInterval({ component, name: user }) {
this.timer = setInterval(() => {
if(this.countdown <= 0){
this.clearTimer();
}
this.countdown -= 1;
}, 1000);
},
// 点击用户登录,获取用户地址
handleClick(item) {
this.loginItem = item;
// 处理消费者角色,其他三个角色都有一个角色地址
if (item.userName !== 'consumer') {
axios({
method: 'get',
url: `/userinfo?userName=${item.userName}`,
})
.then(ret => {
this.address = ret.data.address;
this.currentUser = item.name;
this.countdownInterval(item);
})
.catch(err => {
console.log(err)
})
} else {
this.currentUser = item.name;
this.countdownInterval(item);
}
}
}
}

两步登录流程:

  1. 选择身份:点击生产商/中间商/超市/消费者
  2. 自动登录:5秒倒计时后自动进入对应页面

特殊处理:

  • 非消费者:显示区块链地址
  • 消费者:直接登录(无地址)
  • 可点击”直接登录”跳过倒计时

登录结果:

  • 进入对应角色页面(农场/中间商/超市/消费者)

components.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
// 日期时间格式处理,把日期统一转化成 YYYY-MM-DD hh:mm:ss 的格式
const dateTime = time => {
let newDate = new Date(time);
let dateCollection = { y: newDate.getFullYear(), M: newDate.getMonth() + 1, d: newDate.getDate(), h: newDate.getHours(), m: newDate.getMinutes(), s: newDate.getSeconds() };
for(let i in dateCollection) {
if (dateCollection[i] < 10) dateCollection[i] = `0${dateCollection[i]}`;
}
let { y, M, d, h, m, s } = dateCollection;
return `${y}-${M}-${d} ${h}:${m}:${s}`;
}

// 头部组件
const Header = {
// 接受传入的登录状态、用户信息
props: ['login', 'user'],
template: `
<div class="header">
<img src="./static/images/logo.png" />
<h3>食品溯源平台</h3>
<span v-if="login" class="user-name">{{ user }}</span>
</div>
`
}

// 食品详情展示
const FoodDetail = {
// 接受传入的食品详情信息
props: ['food'],
computed: {
// 日期转换
date() {
if (this.food.timestamp) return dateTime(this.food.timestamp)
}
},
template: `
<div class="fooddetail">
<h4>食品基本信息</h4>
<div class="content trace-content">
<div>
<span>生产商地址</span>
<span>{{ food.from_address || food.address }}</span>
</div>
<div>
<span>溯源码</span>
<span>{{ food.traceNumber }}</span>
</div>
<div>
<span>食品名称</span>
<span>{{ food.name }}</span>
</div>
<div>
<span>生产商</span>
<span>{{ food.from || food.produce }}</span>
</div>
<div>
<span>生产时间</span>
<span>{{ date }}</span>
</div>
</div>
</div>
`,
}

// 溯源详情展示
const TraceDetail = {
// 食品详情子组件,FoodDetail 展示的是农场部分的信息
components: {
FoodDetail
},
// 接受传入的溯源详情信息,及用户信息;foodDetail为数组,第一个数组元素为农场溯源信息、第二个数组元素为中间商溯源信息、第三个数组元素为超市溯源信息
props: ['foodDetail', 'user'],
template:`
<div>
<h3 class="trace-title">以下是溯源码为{{foodDetail[0].traceNumber}}的溯源信息</h3>
<div class="tracedetail">
<!-- 农场部分的溯源详情 -->
<!-- 农场部分的溯源详情 -->
<FoodDetail :food="foodDetail[0]" />

<!-- 根据 foodDetail 的溯源细节,决定是否展示相关阶段的溯源信息, user: 2 为 超市, user: 3 为消费者 -->
<el-divider v-if="foodDetail[1] && (user === 2 || user === 3)"></el-divider>
<div v-if="foodDetail[1] && (user === 2 || user === 3)">
<h4>食品流通信息</h4>
<div class="trace-timeline trace-content">
<el-timeline>
<el-timeline-item class="el-timeline-no-node el-timeline-no-tail">
<div>
<span>中间商地址</span>
<span>{{ foodDetail[1].to_address }}</span>
</div>
</el-timeline-item>
<el-timeline-item>
<div>
<span>入库时间</span>
<span>{{ dateTime(foodDetail[1].timestamp) }}</span>
</div>
</el-timeline-item>
<el-timeline-item>
<div>
<span>质检情况</span>
<span>{{ foodDetail[1].quality === 0 ? '优质' : foodDetail[1].quality === 1 ? '合格' : '不合格' }}</span>
</div>
</el-timeline-item>
<el-timeline-item>
<div>
<span>发货单位</span>
<span>{{ foodDetail[1].from }}</span>
</div>
</el-timeline-item>
<el-timeline-item style="height:60px;" class="el-timeline-no-tail">
<div>
<span>收货单位</span>
<span>{{ foodDetail[1].to }}</span>
</div>
</el-timeline-item>

<!-- 根据 foodDetail 的溯源细节,决定是否展示相关阶段的溯源信息, user: 3 为消费者 -->
<el-timeline-item class="el-timeline-no-node el-timeline-no-tail" v-if="foodDetail[2] && user === 3">
<div>
<span>超市地址</span>
<span>{{ foodDetail[2].to_address }}</span>
</div>
</el-timeline-item>
<el-timeline-item v-if="foodDetail[2] && user === 3">
<div>
<span>入库时间</span>
<span>{{ dateTime(foodDetail[2].timestamp) }}</span>
</div>
</el-timeline-item>
<el-timeline-item v-if="foodDetail[2] && user === 3">
<div>
<span>质检情况</span>
<span>{{ foodDetail[2].quality === 0 ? '优质' : foodDetail[2].quality === 1 ? '合格' : '不合格' }}</span>
</div>
</el-timeline-item>
<el-timeline-item v-if="foodDetail[2] && user === 3">
<div>
<span>发货单位</span>
<span>{{ foodDetail[2].from }}</span>
</div>
</el-timeline-item>
<el-timeline-item v-if="foodDetail[2] && user === 3">
<div>
<span>收货单位</span>
<span>{{ foodDetail[2].to }}</span>
</div>
</el-timeline-item>
</el-timeline>
</div>
</div>
</div>
</div>
`,
methods: {
// 时间格式转化
dateTime(val) {
return dateTime(val)
}
}
}

// 添加蔬菜组件
const AddVegetable = {
// 溯源详情组件注册
components: {
TraceDetail
},
data() {
return {
form: {
traceName: '', // 收货单位
quality: '', // 质检情况
},
formLabelWidth: '180px', // 弹窗宽度
}
},
// 接受传入的食品详情、可视化、用户信息
props: ['dialogFormVisible', 'food', 'user'],
template:`
<div>
<el-dialog title="添加食品信息" :visible.sync="dialogFormVisible" :show-close="false" width="40%" :center="true">
<el-form :model="form" ref="form" label-position="left" :label-width="formLabelWidth">
<el-form-item
label="质检情况"
prop="quality"
:rules="[
{ required: true, message: '请选择质检情况', trigger: 'change' }
]">
<el-radio-group v-model="form.quality">
<el-radio label="0">优质</el-radio>
<el-radio label="1">合格</el-radio>
<el-radio label="2">不合格</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item
label="收货单位(本单位)"
prop="traceName"
:rules="[
{ required: true, message: '请输入收货单位' }
]">
<el-input v-model="form.traceName" autocomplete="off"></el-input>
</el-form-item>
</el-form>
<div>
<el-button @click="$emit('popup', false)">取 消</el-button>
<el-button type="primary" @click="submitForm('form')">确 定</el-button>
</div>
<el-divider></el-divider>

<!-- 食品详情信息 -->
<TraceDetail :food-detail="food" :user="user" />
</el-dialog>
</div>
`,
methods: {
// 处理添加蔬菜提交, $emit 到 父组件处理
submitForm(formName) {
// 参数校验
this.$refs[formName].validate((valid) => {
if (valid) {
this.$emit('confirmPopup', this.form);
} else {
return false;
}
});
},
}
}

// mixin, 这里主要处理中间商和超市的耦合业务
const mixin = {
// 注册添加蔬菜信息、食品详情子组件
components: {
AddVegetable,
FoodDetail
},
// 接受传入的分页信息
props: ['currPage', 'pageSize'],
data() {
return {
popup: false, // 控制添加蔬菜信息弹窗
traceNumber: '', // 溯源码
foodDetail: '', // 溯源食品信息
foodList: [], // 食品信息列表
popoverData: {}, // 弹窗食品信息数据
}
},
computed: {
// 当前页的数据渲染
renderList() {
return this.foodList.slice((this.currPage - 1) * this.pageSize, this.currPage * this.pageSize)
}
},
template: `
<div>
<el-input
placeholder="请输入溯源码"
v-model.number="traceNumber"
clearable>
</el-input>
<el-button type="primary" @click="addVegetable">添加食品信息</el-button>
<el-button type="danger" @click="$emit('logout', false)" class="button-logout">退出</el-button>

<!-- 食品流转信息列表 -->
<el-table
:data="renderList"
style="width: 100%"
:default-sort = "{prop: 'date', order: 'descending'}"
>
<el-table-column
prop="traceNumber"
label="溯源码"
>
<template slot-scope="scope">

<!-- 鼠标点击食品细节弹窗信息 -->
<el-popover trigger="click" placement="top" @show="handlePopover(scope.row)" :width="400">
<FoodDetail :food="popoverData" />
<div slot="reference">
<span style="color: blue; cursor: pointer;">{{ scope.row.traceNumber }}</span>
</div>
</el-popover>
</template>
</el-table-column>
<el-table-column
prop="name"
label="食品名称"
>
</el-table-column>
<el-table-column
prop="date"
label="上架时间"
sortable
>
</el-table-column>
<el-table-column
prop="quality"
label="质检情况"
>
</el-table-column>
<el-table-column
prop="from"
label="发货单位"
>
</el-table-column>
<el-table-column
prop="to"
label="收获单位"
>
</el-table-column>
</el-table>

<!-- 添加蔬菜信息组件 -->
<AddVegetable :dialogFormVisible="popup" :user="user" :food="foodDetail" @popup="handlePopup" @confirmPopup="formSubmit" />

</div>
`,
mounted() {
// 获取数据
this.getData();
},
methods: {
// 数据获取,根据 user 用户类型来分别获取中间商和超市的数据
getData() {
let pathname = this.user === 1 ? 'distributing' : 'retailing';
axios({
method: 'get',
url: `/${pathname}`
})
.then(ret => {
this.foodList = ret.data
.map(item => {
let quality = item.quality === 0 ? '优质' : item.quality === 1 ? '合格' : '不合格'
return ({
...item,
date: dateTime(item.timestamp),
quality,
})
})
.reverse();
this.$emit('total', this.foodList.length);
})
.catch(err => {
console.log(err)
})
},
// 表格中食品详情弹窗
handlePopover(data) {
// 中间商已有的信息足有支持显示,不需要进行额外 http 请求
if (this.user === 1) {
this.popoverData = data;
return;
}
// 超市则需要额外发送 http 请求获取更多细节
axios({
method: 'get',
url: `/food?traceNumber=${data.traceNumber}`
})
.then(ret => {
this.popoverData = ret.data;
})
.catch(err => {
console.log(err)
})
},
// 处理添加蔬菜信息
addVegetable() {
if (!this.traceNumber) {
this.$message.error('请输入溯源码');
return;
}
axios({
method: 'get',
url: `/trace?traceNumber=${this.traceNumber}`
})
.then(ret => {
this.foodDetail = ret.data || []
this.popup = true;
})
.catch(err => {
console.log(err)
})
},
// 添加蔬菜信息弹窗
handlePopup(val) {
this.popup = val;
},
// 添加蔬菜信息提交,根据 user 进行不同的请求
formSubmit(val) {
let pathname = this.user === 1 ? 'adddistribution' : 'addretail';
axios({
method: 'post',
url: `/${pathname}`,
data: {
...val,
quality: parseInt(val.quality),
traceNumber: this.foodDetail[0].traceNumber
}
})
.then(ret => {
if (ret.data.ret !== 1) {
this.$message({
message: '提交失败',
type: 'error',
center: true
});
return
}
this.$message({
message: '提交成功',
type: 'success',
center: true
});
this.getData();
})
.catch(err => {
console.log(err)
})
this.popup = false;
}
}
}

// 创建蔬菜组件
const CreateVegetable = {
data() {
return {
form: {
foodName: '', // 食品名称
traceName: '', // 生产商名称
traceNumber: '', // 溯源码
quality: '', // 质检情况
},
formLabelWidth: '120px'
}
},
// 接受传入的可视化参数
props: ['dialogFormVisible'],
template:`
<div>
<el-dialog title="新建食品" :visible.sync="dialogFormVisible" :show-close="false" width="35%" :center="true">
<el-form :model="form" ref="form" label-position="left" :label-width="formLabelWidth">
<el-form-item
label="溯源码"
prop="traceNumber"
:rules="[
{ required: true, message: '请输入溯源码' }
]"
>
<el-input v-model.number="form.traceNumber" autocomplete="off"></el-input>
</el-form-item>
<el-form-item
label="食品名称"
prop="foodName"
:rules="[
{ required: true, message: '请输入食品名称' }
]"
>
<el-input v-model="form.foodName" autocomplete="off"></el-input>
</el-form-item>
<el-form-item
label="生产商"
prop="traceName"
:rules="[
{ required: true, message: '请输入生产商' }
]"
>
<el-input v-model="form.traceName" autocomplete="off"></el-input>
</el-form-item>
<el-form-item
label="质检情况"
prop="quality"
:rules="[
{ required: true, message: '请选择质检情况', trigger: 'change' }
]"
>
<el-radio-group v-model="form.quality">
<el-radio label="0">优质</el-radio>
<el-radio label="1">合格</el-radio>
<el-radio label="2">不合格</el-radio>
</el-radio-group>
</el-form-item>

</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="$emit('popup', false)">取 消</el-button>
<el-button type="primary" @click="submitForm('form')">确 定</el-button>
</div>
</el-dialog>
</div>
`,
methods: {
// 提交信息
submitForm(formName) {
// 参数校验
this.$refs[formName].validate((valid) => {
if (valid) {
this.$emit('confirmPopup', this.form)
} else {
return false;
}
});
},
}
}

Header(头部组件)

  • 显示logo和平台名称
  • 登录后显示当前用户身份

FoodDetail(食品详情)

  • 显示食品基本信息
  • 格式化显示时间

TraceDetail(溯源详情)

  • 显示完整食品流转信息
  • 根据用户权限显示不同内容:
    • 消费者:看到全流程(农场→中间商→超市)
    • 超市:看到农场→中间商
    • 中间商:只看到农场

AddVegetable(添加食品)

  • 中间商/超市使用:接收食品并质检
  • 需要输入质检结果和收货单位

CreateVegetable(创建食品)

  • 农场使用:创建新食品
  • 需要输入溯源码、食品名称等

核心功能:

  • 时间统一格式化显示
  • 权限控制显示不同信息
  • 质检状态:0=优质,1=合格,2=不合格

复现时的自查

  • 记录自己实际使用的版本、配置和缺失依赖。
  • 对照本篇关键步骤,保存命令或调用结果,并解释结果代表什么。
  • 对失败或与原记录不一致的情况保留错误信息,不把历史输出当成自己的验证结果。

返回系列导读 · 上一篇 · 下一篇

评论COMMENTS