后台管理系统组件
# 后台管理系统组件
# 页面主容器
<template>
<div>
<!-- 使用 Element UI 的容器布局组件,设置全屏高度 -->
<el-container style="height: 100vh;">
<!-- 左侧侧边栏 -->
<el-aside :width="isCollapse ? '64px' : '210px'">
<!-- Sidebar 组件,负责显示左侧菜单 -->
<sidebar
:menuData="menuData"
:activeIndex="activeIndex"
:isCollapse="isCollapse"
:logo="logo"
:title="title">
</sidebar>
</el-aside>
<!-- 主体部分 -->
<el-container>
<!-- 顶部导航栏 -->
<el-header style="height: 84px;">
<!-- NavBar 组件,包含了面包屑导航、用户菜单、全屏按钮等功能 -->
<nav-bar @toggle-collapse="clickCollapse"></nav-bar>
</el-header>
<!-- 主要内容区域,使用 Element UI 的 el-main 组件 -->
<el-main style="background-color: white;" class="scrollable-main">
<!-- 路由视图,显示对应路由的页面内容 -->
<router-view></router-view>
</el-main>
</el-container>
</el-container>
</div>
</template>
<script>
import logo from '@/assets/logo.png'; // 引入系统 Logo 图片
import Sidebar from '@/components/laout/Sidebar.vue'; // 引入侧边栏组件
import NavBar from "@/components/laout/NavBar.vue"; // 引入导航栏组件
import axios from 'axios'; // 引入 axios 用于发送 HTTP 请求
export default {
name: 'Home', // 组件名称
components: {
NavBar, // 注册 NavBar 组件
Sidebar // 注册 Sidebar 组件
},
data() {
return {
logo: logo, // 系统 Logo
title: '在线后台管理系统', // 系统标题
activeIndex: '/main/home', // 当前活跃的菜单项
isCollapse: false, // 侧边栏折叠状态
menuData: [], // 动态菜单数据
};
},
created() {
// 在组件创建时发送请求,获取侧边栏菜单数据
this.getMenuData('/api/menuData');
},
methods: {
// 控制侧边栏的折叠与展开
clickCollapse() {
this.isCollapse = !this.isCollapse;
},
// 获取侧边栏菜单数据
async getMenuData(url) {
try {
// 发送 HTTP GET 请求获取菜单数据
const response = await axios.get(url);
// 将获取的数据存入 menuData
this.menuData = response.data;
} catch (error) {
// 如果请求失败,显示错误信息
this.$message.error('菜单数据获取失败');
console.error('请求失败:', error);
}
},
}
};
</script>
<style>
/* 全局样式:重置 HTML 和 body 的默认样式 */
body,
html {
margin: 0;
padding: 0;
height: 100%;
}
</style>
<style scoped>
/* 滚动区域样式:确保内容区域可以滚动,并设置背景色 */
.scrollable-main {
overflow-y: auto; /* 启用垂直滚动 */
height: calc(100vh - 84px); /* 内容区域高度为视口高度减去导航栏高度 */
background-color: #1b70c5; /* 设置背景色 */
}
/* 顶部导航栏样式 */
.el-header {
padding: 0 0; /* 移除内边距 */
box-sizing: border-box; /* 让 padding 和 border 包含在元素的宽高内 */
flex-shrink: 0; /* 禁止导航栏收缩 */
}
</style>
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
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
# 侧边导航栏组件
<template>
<div class="sidebar-content">
<!-- 顶部图标区域 -->
<div v-if="logo || title" class="grid-content logo-container" :class="{ 'logo-collapsed': isCollapse }">
<img v-if="logo" :src="logo" alt="网站logo" class="logo">
<span v-if="title && !isCollapse" class="logo-text">{{ title }}</span>
</div>
<!-- 菜单导航栏 -->
<el-menu :default-active="$store.state.activeIndex"
class="el-menu-vertical-demo"
mode="vertical"
@open="handleOpen"
@close="handleClose"
:collapse="isCollapse"
:router="true"
@select="selection"
:collapse-transition="false">
<!-- 遍历菜单数据,根据是否有子菜单进行不同的渲染 -->
<template v-for="item in menuData">
<!-- 无子菜单项 -->
<el-menu-item v-if="!item.children" :key="item.name" :index="item.path">
<i :class="`el-icon-${item.icon}`"></i>
<span slot="title">{{ item.label }}</span>
</el-menu-item>
<!-- 有子菜单项 -->
<el-submenu v-else :key="item.name" :index="item.name">
<template slot="title">
<i :class="`el-icon-${item.icon}`"></i>
<span slot="title">{{ item.label }}</span>
</template>
<el-menu-item v-for="child in item.children" :key="child.name" :index="child.path">
<i :class="`el-icon-${child.icon}`"></i>
{{ child.label }}
</el-menu-item>
</el-submenu>
</template>
</el-menu>
</div>
</template>
<script>
export default {
props: {
menuData: {
type: Array,
required: true
},
activeIndex: {
type: String,
required: false
},
isCollapse: {
type: Boolean,
required: false,
default: false
},
logo: {
type: String,
required: false
},
title: {
type: String,
required: false
}
},
data() {
return {};
},
methods: {
handleOpen(key, keyPath) {
console.log('展开的子菜单:', key, keyPath);
},
handleClose(key, keyPath) {
console.log('收起的子菜单:', key, keyPath);
},
selection(index, indexPath) {
console.log(index);
this.$store.commit('EditActiveIndex', index);
},
},
created() {
const currentPath = this.$route.path;
this.$store.commit('EditActiveIndex', currentPath); // 组件刚激活时,设置当前路径为激活状态
},
watch: {
// 监听路由的变化
'$route.path'(newVal) {
console.log('Route changed:', newVal);
this.$store.commit('EditActiveIndex', newVal); // 根据新的路径更新 Vuex 中的 activeIndex
}
}
};
</script>
<style>
/* 确保侧边栏的背景色一致 */
.scrollable-aside,
.sidebar-content,
.logo-container {
background-color: #242b3e !important;
flex-shrink: 0; /* 确保侧边栏不收缩 */
}
/* 设置侧边栏背景色 */
.scrollable-aside {
background-color: #242b3e !important;
}
/* 设置滚动区域和滚动条样式 */
.sidebar-content {
height: 100vh;
display: flex;
flex-direction: column;
overflow-x: hidden; /* 隐藏水平滚动条 */
overflow-y: hidden; /* 仅在需要时显示垂直滚动条 */
}
/* 顶部图标区域样式 */
.logo-container {
height: 60px;
display: flex;
align-items: center;
justify-content: flex-start; /* 使内容与左侧对齐 */
padding-left: 20px; /* 与菜单项的 padding 对齐 */
background-color: #222b40;
z-index: 2;
position: relative;
transition: width 0.3s, height 0.3s;
overflow: hidden; /* 保证在收起状态下内容不溢出 */
min-width: 60px; /* 确保在收起时宽度不写死,但也不影响页面布局 */
flex-shrink: 0; /* 避免收缩 */
}
.logo-collapsed {
justify-content: center;
background-color: #222b40 !important;
width: 60px; /* 固定宽度防止引起布局问题 */
height: 60px; /* 保证高度一致 */
flex-shrink: 0;
padding-left: 0; /* 去掉收起时的 padding */
}
/* 保证 logo 在收起时的尺寸合适 */
.logo {
height: 30px;
width: 30px; /* 保证logo在收起状态下的大小更小 */
margin-right: 0; /* 去掉右边距 */
transition: height 0.3s, width 0.3s;
}
.logo-collapsed .logo {
height: 20px; /* 收起时的logo高度 */
width: 20px; /* 收起时的logo宽度 */
}
/* 保证文本仅在展开时显示 */
.logo-text {
color: white;
font-size: 15px;
font-weight: bold;
margin-left: 10px; /* 确保文本与图标间有适当的间距 */
display: none; /* 默认隐藏文本 */
}
.logo-container:not(.logo-collapsed) .logo-text {
display: block; /* 仅在展开状态下显示文本 */
}
/* 菜单的样式 */
.el-menu-vertical-demo:not(.el-menu--collapse) {
min-height: 500px;
height: calc(100vh - 60px);
/* 计算高度,减去顶部图标区域的高度 */
width: auto; /* 避免固定宽度 */
transition: all 0.3s; /* 添加平滑过渡效果 */
}
.el-menu-vertical-demo.el-menu--collapse {
width: 60px; /* 收起时固定宽度 */
}
/* 菜单项样式 */
.el-menu {
border-right: solid 0px #e6e6e6 !important;
height: 100%;
background-color: #222b40;
flex-shrink: 0; /* 确保不缩放 */
transition: all 0.3s; /* 添加平滑过渡效果 */
}
.el-submenu__title {
color: #ccc;
height: 50px;
line-height: 50px;
transition: all 0.3s; /* 添加平滑过渡效果 */
}
.el-menu-item {
color: #ccc;
height: 50px;
line-height: 50px;
padding-left: 20px; /* 与顶部 logo-container 保持一致 */
background-color: #222b40 !important;
/* 必须加上这个背景色,否则鼠标离开会出现白色 */
transition: all 0.3s; /* 添加平滑过渡效果 */
}
.el-menu-item.is-active {
color: #3399ff !important;
}
.el-menu--inline .el-menu-item {
padding-left: 50px !important;
}
.el-submenu__title:hover {
background-color: #222 !important;
}
.el-menu-item:not(.is-active):hover {
background-color: #222 !important;
}
.el-menu-item:hover {
background-color: #222 !important;
}
.el-submenu__icon-arrow {
margin-top: -5px;
}
</style>
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
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
# 顶部导航栏组件
NavBar.vue
<template>
<!-- 顶部导航栏,使用 Element UI 的 el-header 组件 -->
<el-header style="height: 100%;">
<div class="bannerbox">
<el-col :span="20">
<div class="manager-header-center">
<div style="display: flex; align-items: center">
<!-- 侧边栏折叠按钮 -->
<el-button icon="el-icon-menu" size="mini" @click="clickCollapse" class="collapseButton"></el-button>
<!-- 使用 Element UI 的面包屑组件显示导航路径 -->
<el-breadcrumb separator-class="el-icon-arrow-right">
<!-- 首页固定为导航的第一个项 -->
<el-breadcrumb-item :to="{ path: '/main'}">首页</el-breadcrumb-item>
<i class="el-icon-youjiantou"></i>
<!-- 当前路由的名称,自动根据 $route.path 显示 -->
<el-breadcrumb-item :to="{ path: $route.path }">{{ $route.meta.name }}</el-breadcrumb-item>
</el-breadcrumb>
</div>
</div>
</el-col>
<el-col :span="4">
<div class="header-actions">
<!-- 全屏按钮 -->
<div class="screen-full" @click="fullScreen">
<svg-icon icon-name="full-screen"></svg-icon>
</div>
<!-- 用户下拉菜单,显示用户头像 -->
<div>
<user-dropdown v-model="activeIndex" :avatar-url="avatarUrl"></user-dropdown>
</div>
</div>
</el-col>
</div>
<!-- 标签页 -->
<div class="tabs-view-container">
<div class="tabs-wrapper">
<!-- 遍历显示标签页 -->
<span
:class="isActive(tab)"
v-for="tab in tabList"
:key="tab.path"
@click="goTo(tab)"
>
{{ tab.name }}
<!-- 关闭标签页按钮,防止关闭首页标签 -->
<i
class="el-icon-close"
v-if="tab.path != '/main/home'"
@click.stop="removeTab(tab)"
/>
</span>
</div>
<div class="tabs-close-item" @click="closeAllTab">
全部关闭
</div>
</div>
</el-header>
</template>
<script>
import { mapState, mapMutations } from 'vuex'; // Vuex 用于管理全局状态
import userDropdown from "@/components/UserDropdown.vue"; // 引入用户下拉菜单组件
export default {
name: 'NavBar',
components: {
userDropdown // 注册子组件
},
data() {
return {
fullscreen: false, // 标记当前是否为全屏状态
activeIndex: '/main/home', // 当前活跃的标签索引
breadcrumbList: [], // 面包屑导航数据
};
},
computed: {
// 绑定 Vuex 的 state 中的 user 和 tabList 数据
...mapState(['user', 'tabList']),
avatarUrl() {
return this.user.avatar; // 从 Vuex 中获取用户头像 URL
}
},
created() {
// 初始化面包屑导航和标签页状态
this.updateBreadcrumb();
this.saveTab(this.$route); // 保存当前路由为标签页
},
watch: {
// 监听路由的变化,更新面包屑导航和标签页
'$route'() {
this.updateBreadcrumb();
this.saveTab(this.$route);
}
},
mounted() {
// 监听全屏状态的变化
document.addEventListener('fullscreenchange', this.onFullScreenChange);
},
beforeDestroy() {
// 组件销毁前移除全屏状态监听器
document.removeEventListener('fullscreenchange', this.onFullScreenChange);
},
methods: {
...mapMutations(['saveTab', 'removeTab', 'resetTab']), // 使用 Vuex 的 mutation 方法来操作状态
fullScreen() {
try {
let element = document.documentElement; // 获取 HTML 根元素
if (this.fullscreen) { // 如果当前是全屏状态,执行退出全屏操作
if (document.fullscreenElement) { // 判断是否处于全屏状态
if (document.exitFullscreen) {
document.exitFullscreen();
} else if (document.webkitExitFullscreen) {
document.webkitExitFullscreen();
} else if (document.mozCancelFullScreen) {
document.mozCancelFullScreen();
} else if (document.msExitFullscreen) {
document.msExitFullscreen();
}
}
} else { // 否则,进入全屏模式
if (element.requestFullscreen) {
element.requestFullscreen();
} else if (element.webkitRequestFullscreen) {
element.webkitRequestFullscreen();
} else if (element.mozRequestFullScreen) {
element.mozRequestFullScreen();
} else if (element.msRequestFullscreen) {
element.msRequestFullscreen();
}
}
} catch (error) {
console.error('Failed to toggle full screen mode:', error);
}
},
onFullScreenChange() {
this.fullscreen = !!document.fullscreenElement; // 根据是否有 fullscreenElement 来更新全屏状态
},
clickCollapse() {
this.$emit('toggle-collapse'); // 触发父组件的事件,控制侧边栏的折叠与展开
},
goTo(tab) {
this.$router.push(tab.path); // 跳转到指定标签页的路由
},
removeTab(tab) {
this.$store.commit('removeTab', tab); // 调用 Vuex 的 mutation 方法移除标签
if (tab.path === this.$route.path) {
const lastTab = this.tabList[this.tabList.length - 1]; // 获取最后一个标签
this.$router.push(lastTab.path); // 跳转到最后一个标签的路由
}
},
closeAllTab() {
this.resetTab(); // 调用 Vuex 的 mutation 方法重置标签页
this.$router.push('/main/home'); // 跳转到首页
},
updateBreadcrumb() {
let matched = this.$route.matched.filter((item) => item.name); // 获取当前路由的匹配信息
const first = matched[0];
if (first && first.name !== '系统首页') {
matched = [{ path: '/main/home', name: '系统首页' }].concat(matched); // 如果当前不是首页,手动添加首页到面包屑导航
}
this.breadcrumbList = matched; // 更新面包屑导航的数据
},
isActive(tab) {
return tab.path === this.$route.path ? 'tabs-view-item-active' : 'tabs-view-item'; // 判断标签是否为当前活跃标签
}
}
};
</script>
<style scoped>
/* 样式部分定义了各个 UI 元素的布局和视觉效果 */
.collapseButton {
margin-left: 13px;
margin-right: 8px;
}
.bannerbox {
display: flex;
align-items: center;
justify-content: center;
}
.header-actions {
display: flex;
justify-content: flex-end;
align-content: center;
}
.screen-full {
display: flex;
align-content: center;
font-size: 40px;
cursor: pointer;
margin-right: 4px;
}
.tabs-view-container {
display: flex;
position: relative;
padding-left: 10px;
padding-right: 10px;
height: 33px;
background: #fff;
border-bottom: 1px solid #d8dce5;
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.12), 0 0 3px 0 rgba(0, 0, 0, 0.04);
}
.tabs-wrapper {
overflow-x: auto;
overflow-y: hidden;
white-space: nowrap;
width: 95%;
}
.tabs-view-item {
display: inline-block;
cursor: pointer;
height: 25px;
line-height: 25px;
border: 1px solid #d8dce5;
color: #495060;
background: #fff;
padding: 0 8px;
font-size: 12px;
margin-top: 4px;
margin-left: 5px;
}
.tabs-close-item {
position: absolute;
right: 10px;
display: inline-block;
cursor: pointer;
height: 25px;
line-height: 25px;
border: 1px solid #d8dce5;
color: #495060;
background: #fff;
padding: 0 8px;
font-size: 12px;
margin-top: 4px;
margin-left: 5px;
}
.tabs-view-item-active {
display: inline-block;
cursor
: pointer;
height: 26px;
line-height: 26px;
padding: 0 8px;
font-size: 12px;
margin-top: 4px;
margin-left: 5px;
background-color: #42b983;
color: #fff;
border-color: #42b983;
}
.tabs-view-item-active:before {
content: "";
background: #fff;
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
position: relative;
margin-right: 2px;
}
.el-icon-close {
padding: 0.1rem;
}
.el-icon-close:hover {
border-radius: 50%;
background: #b4bccc;
transition-duration: 0.3s;
}
</style>
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
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
重要API和参数说明:
Vuex 的
mapState
和mapMutations
:mapState
:用于将 Vuex 的 state 映射到组件的计算属性中,以便在组件中直接访问全局状态。mapMutations
:用于将 Vuex 的 mutation 映射到组件的方法中,以便在组件中直接调用全局的 mutation 操作状态。
$route
和this.$router.push()
:$route
:包含当前激活的路由对象信息,包括路径、参数和 meta 数据等。this.$router.push()
:用于编程式导航到指定的路由路径,类似于<router-link>
的功能。
document.fullscreenElement
:- 用于检查当前是否有元素处于全屏状态,返回处于全屏状态的元素,如果没有处于全屏状态则返回
null
。
- 用于检查当前是否有元素处于全屏状态,返回处于全屏状态的元素,如果没有处于全屏状态则返回
document.requestFullscreen()
和document.exitFullscreen()
:requestFullscreen()
:用于使当前元素进入全屏模式。exitFullscreen()
:用于退出全屏模式。
复用组件时需要修改的部分:
标签页逻辑:
- 如果需要自定义标签页的管理逻辑,例如保存标签页、关闭标签页、重置标签页等操作,需要根据业务需求修改
removeTab
和closeAllTab
方法,并确保对应的 Vuex mutation 处理逻辑正确。
- 如果需要自定义标签页的管理逻辑,例如保存标签页、关闭标签页、重置标签页等操作,需要根据业务需求修改
面包屑导航:
- 如果项目中路由结构与现有逻辑不同,可能需要修改
updateBreadcrumb
方法中的匹配逻辑,以生成正确的面包屑导航。
- 如果项目中路由结构与现有逻辑不同,可能需要修改
样式定制:
- 样式部分可以根据项目的 UI 规范进行调整,比如修改
tabs-view-item-active
的样式以符合不同的配色方案。
- 样式部分可以根据项目的 UI 规范进行调整,比如修改
全屏功能:
- 如果需要在全屏时展示额外的内容或触发额外的逻辑,可以在
fullScreen
和onFullScreenChange
方法中添加自定义逻辑。
- 如果需要在全屏时展示额外的内容或触发额外的逻辑,可以在
用户下拉菜单:
userDropdown
组件根据项目需求可能需要调整或者替换为其他自定义的用户管理组件。确保 avatarUrl 的获取和展示符合实际需求。
# 头像下拉框组件
<template>
<div style="display: flex; align-items: center; justify-content: flex-end;">
<el-dropdown @command="handleCommand">
<div style="display: flex; align-items: center; justify-content: center;">
<span class="el-dropdown-link">
<img :src="avatarUrl" class="avatar" alt="用户头像">
</span>
<span style="cursor: pointer;">{{ username }}</span>
</div>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item icon="el-icon-user-solid" command="/main/user">个人中心</el-dropdown-item>
<el-dropdown-item icon="el-icon-s-tools" command="logout">退出登录</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</div>
</template>
<script>
export default {
data() {
return {
defaultOpeneds:[]
}
},
props: {
avatarUrl: {
type: String,
// default: 'https://web-183.oss-cn-beijing.aliyuncs.com/typora/202408080905128.jpg' // 默认头像地址
},
username: {
type: String,
default: '管理员' // 默认用户名
},
value: {
type:String,
require:false
}
},
methods: {
handleCommand(command) {
if (command === '/main/user') {
// if (this.$route.path === command) {
// return false;
// }
this.$store.commit('EditActiveIndex', command);
this.$router.push('/main/user'); // 跳转到个人中心页面
} else if (command === 'logout') {
this.$router.push('/'); // 执行退出登录操作
localStorage.removeItem("xm-user");
sessionStorage.removeItem("xm-user");
this.$message.info('退出登录');
// 或者你可以调用一个方法来处理登出逻辑
}
}
}
};
</script>
<style scoped>
.avatar {
width: 40px;
height: 40px;
border-radius: 50%;
cursor: pointer;
margin-right: 10px;
}
</style>
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
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
# vuex共享数据管理
// 引入 Vue 核心库
import Vue from 'vue';
// 引入 Vuex
import Vuex from 'vuex';
// 应用 Vuex 插件
Vue.use(Vuex);
// 同步用户信息到存储的辅助函数
function syncUserToStorage(user, storageType) {
const storage = storageType === 'local' ? localStorage : sessionStorage;
storage.setItem('xm-user', JSON.stringify(user));
}
// 准备 state 对象——保存具体的数据
const state = {
activeIndex: '/home', // 当前激活的菜单索引
tabList: [{ name: "首页", path: "/main/home" }], // 保存当前打开的标签页列表
user: {
// 你之前的用户信息相关字段在这里
},
storageType: 'local', // 默认使用 localStorage
collapse: false // 控制导航栏的折叠状态
};
// 准备 mutations 对象——修改 state 中的数据
const mutations = {
EditActiveIndex(state, val) {
state.activeIndex = val; // 修改激活的菜单索引
},
EditUser(state, val) {
state.user = { ...state.user, ...val }; // 合并更新用户信息,保留未修改的数据
// 根据 storageType 来确定修改后的用户信息同步到local还是session
syncUserToStorage(state.user, state.storageType);
},
SetStorageType(state, storageType) {
state.storageType = storageType; // 设置存储类型为 localStorage 或 sessionStorage
},
saveTab(state, tab) {
// 如果当前标签页不在 tabList 中,则添加到 tabList
if (!state.tabList.find(item => item.path === tab.path)) {
state.tabList.push({ name: tab.meta.name, path: tab.path });
}
},
removeTab(state, tab) {
// 根据标签名找到对应的索引并删除
const index = state.tabList.findIndex(item => item.path === tab.path);
state.tabList.splice(index, 1);
},
resetTab(state) {
// 重置标签页,只保留首页
state.tabList = [{ name: "首页", path: "/main/home" }];
},
trigger(state) {
// 切换导航栏的折叠状态
state.collapse = !state.collapse;
}
};
// 创建并导出 store
const store = new Vuex.Store({
state,
mutations
});
// 刷新页面的时候,vuex数据丢失,我们会按以下步骤重新加载数据
// 首先从 localStorage 获取中的用户信息,并设置存储类型为local
let savedUser = JSON.parse(localStorage.getItem('xm-user') || '{}');
let storageType = 'local';
// 如果localStorage中没有获取到,我们再从sessionStorage中获取,并设置存储类型为session
if (Object.keys(savedUser).length === 0) {
savedUser = JSON.parse(sessionStorage.getItem('xm-user') || '{}');
storageType = 'session';
}
// 如果用户信息存在,更新 Vuex 的状态
if (Object.keys(savedUser).length > 0) {
store.commit('SetStorageType', storageType);
store.commit('EditUser', savedUser);
}
export default store;
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
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
# 路由router
// 导入 Vue 框架
import Vue from 'vue';
// 导入 vue-router
import VueRouter from 'vue-router';
import NProgress from 'nprogress';
import {Message} from 'element-ui'
// 使用 vue-router 插件
Vue.use(VueRouter);
// 保存 VueRouter 原始的 push 方法
const originalPush = VueRouter.prototype.push;
// 重写 VueRouter 的 push 方法
VueRouter.prototype.push = function push(location) {
// 调用原始的 push 方法,并捕获返回的 Promise 的错误
return originalPush.call(this, location).catch(err => err);
};
// 定义路由配置
const routes = [
// 定义路由规则,示例代码如下
{
path:'/',
redirect: '/login',
},
{
path: '/main',
redirect: '/main/home',
},
{
path: '/login',
component: () => import('@/view/Login.vue'),
},
{
path: '/register',
component: () => import('@/view/Register.vue'),
},
{
path: '/main',
component: () => import('@/components/laout/Container.vue'),
children:[
{
path: 'home',
component: () => import('@/view/Home.vue'),
meta:{
name:'系统首页'
}
},
{
path:'about',
component:() => import('@/view/About.vue'),
meta:{
name:'表格数据'
}
},
{
path:'report',
component: () => import('@/view/Report.vue'),
meta: {
name: '报告管理'
}
},
{
path:'user',
component:() => import('@/view/User.vue'),
meta: {
name: '个人中心'
}
},
{
path:'chat',
component:() => import('@/view/Chat.vue'),
meta: {
name: '单人聊天'
}
},
{
path:'layout',
component:() => import('@/view/GridLayout.vue'),
meta: {
name: '网格布局'
}
},
{
path:'image',
component:() => import('@/view/CarouselImage.vue'),
meta: {
name: '轮播大图'
}
},
{
path:'echarts',
component:() => import('@/view/EChart.vue'),
meta: {
name: '图表展示'
}
}
]
},
{
path: '/gitlogincalback',
component: () => import('@/view/GiteeCallBack.vue')
},
{
path: '/githublogincalback',
component: () => import('@/view/GithubCallBack.vue')
},
{
path: '*',
component: () => import('@/view/404.vue') // 捕获所有未匹配的路由
}
];
// 创建路由实例
const router = new VueRouter({
mode: 'history',
routes // 注入路由配置
});
// 全局前置守卫
router.beforeEach((to, from, next) => {
NProgress.start(); // 开始进度条
// 检查目标路由是否需要身份验证
if (to.path === '/login' || to.path === '/register'
|| to.path === '/gitlogincalback' || to.path === '/githublogincalback') {
next(); // 不需要身份验证,放行
} else {
const token = localStorage.getItem('xm-user') || sessionStorage.getItem('xm-user');
if (token) {
next(); // 用户已登录,放行
} else {
Message.error('token已失效,请重新登录');
next('/login'); // 跳转到登录页
}
}
});
router.afterEach(() => {
NProgress.done(); // 结束进度条
});
// resetRouter 用于清空和重置 Vue Router 实例中的路由表。
// 通过重新设置路由的匹配器 (matcher),可以在某些场景下动态地更新应用的路由配置,比如在用户权限变化、用户登录/注销后,重新加载新的路由配置。
export function resetRouter() {
const newRouter = router;
router.matcher = newRouter.matcher;
}
// 导出路由实例
export default router;
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
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
# 请求响应拦截器
import axios from 'axios'
import router from "@/router";
import { Message } from 'element-ui'; // 引入 Element UI 的 Message 模块
// 创建一个新的 axios 实例
const request = axios.create({
baseURL: '/api', // 基础URL前缀
timeout: 30000 // 请求超时时间设置为30秒
})
// request 拦截器
// 可以在请求发送前对请求做一些处理,例如统一添加 token 或对请求参数进行加密
request.interceptors.request.use(config => {
config.headers['Content-Type'] = 'application/json;charset=utf-8'; // 设置请求头的内容类型为 JSON
// 尝试从 localStorage 获取用户信息
let user = JSON.parse(localStorage.getItem("xm-user") || '{}');
// 如果 localStorage 没有用户信息,再尝试从 sessionStorage 获取
if (Object.keys(user).length === 0) {
user = JSON.parse(sessionStorage.getItem("xm-user") || '{}');
}
// 设置 Authorization 请求头
if (user != null && user.token) {
config.headers['token'] = `Bearer ${user.token}`; // 将 token 以 "Bearer " 前缀形式添加到请求头中
}
return config // 返回修改后的请求配置
}, error => {
console.error('request error: ' + error) // 打印请求错误信息
return Promise.reject(error) // 返回拒绝的 Promise
});
// response 拦截器
// 可以在接口响应后统一处理结果
request.interceptors.response.use(
response => {
let res = response.data; // 获取响应数据
console.log(response.data);
// 兼容服务端返回的字符串数据
if (typeof res === 'string') {
try {
res = JSON.parse(res); // 安全解析为 JSON 对象
} catch (error) {
console.error('JSON 解析错误:', error);
Message.error('响应数据格式错误'); // 添加错误提示
return Promise.reject(new Error('响应数据格式错误')); // 如果解析失败,返回拒绝的 Promise
}
}
// 处理 401 未授权错误
if (res.code === 401 || res.code === '401') { // 检查响应状态码是否为 401(未授权)
Message.error('登录已过期,请重新登录'); // 添加未授权提示
router.push('/login'); // 重定向到登录页面
}
return res; // 返回处理后的响应数据
},
error => {
// 检查 error.response 是否存在
if (error.response) {
const status = error.response.status;
if (status === 401) {
Message.error('登录已过期,请重新登录'); // 添加未授权提示
// 如果是 401 错误,跳转到登录页面
router.push('/login');
} else if (status === 403) {
Message.error('权限不足,访问被拒绝'); // 添加 403 错误提示
} else if (status === 404) {
Message.error('请求资源未找到'); // 添加 404 错误提示
} else {
// 可以在这里处理其他状态码错误,比如 500 等
Message.error(`请求错误,状态码: ${status}`); // 添加其他错误提示
console.error(`HTTP 错误: ${status}`);
}
} else {
// 处理网络错误或其他无法预料的错误
Message.error('网络错误或未知错误,请稍后重试'); // 添加网络错误提示
console.error('网络错误或未知错误:', error);
}
return Promise.reject(error); // 返回拒绝的 Promise
}
);
export default request // 导出 axios 实例
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
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
# MOCK生成动态菜单数据
import Mock from 'mockjs';
const menuData =[
{
path: '/main/home',
name: 'dashboard',
label: '主页',
icon: 's-home',
url: 'Home/home'
},
{
path: '/main/report',
name: 'reports',
label: '报告管理',
icon: 'document',
url: 'Reports/Reports'
},
{
label: '设置',
icon: 'setting',
name: 'setting',
children: [
{
path: '/main/user',
name: 'profile',
label: '个人中心',
icon: 'user',
url: 'Settings/Profile'
},
{
path: '/main/chat',
name: 'brief',
label: '单人聊天',
icon: 'edit-outline',
url: 'Chat/chat'
},
]
},
{
path: '/main/about',
name: 'about',
label: '表格数据',
icon: 'info',
url: 'About/About'
},
{
path: '/main/layout',
name: 'layout',
label: '网格布局',
icon: 's-grid',
url: 'Layout/layout'
},
{
path: '/main/image',
name: 'image',
label: '轮播大图',
icon: 'picture',
url: 'Image/image'
},
{
path: '/main/echarts',
name: 'echarts',
label: '图表展示',
icon: 'picture',
url: 'ECharts/echarts'
}
]
Mock.mock('/api/menuData', 'get', menuData);
export default menuData
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
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
# 表格curd非组件版本
<template>
<div>
<!-- 表格操作按钮区域 -->
<el-row style="padding-bottom: 5px;">
<el-col :span="4">
<!-- 删除选中行按钮 -->
<el-button type="danger" plain @click="deleteSelectedRows" :disabled="selectedRows.length === 0">
批量删除
</el-button>
<!-- 新增按钮 -->
<el-button type="primary" plain @click="handleAdd">新增</el-button>
</el-col>
<el-col :span="20">
<div>
<!-- 查询输入框 -->
<el-input v-model="search" placeholder="请输入关键字查询" style="width: 200px;"></el-input>
<el-button type="info" plain @click="fetchFilteredData" style="margin-left: 10px">查询</el-button>
<el-button type="warning" plain @click="reset" style="margin-left: 10px">重置</el-button>
</div>
</el-col>
</el-row>
<!-- 表格组件 -->
<el-table :data="tableData" border stripe style="width: 100%" @selection-change="handleSelectionChange" ref="multipleTable">
<el-table-column type="selection" width="55"></el-table-column>
<el-table-column prop="id" label="ID" width="80"></el-table-column>
<el-table-column prop="title" label="标题"></el-table-column>
<el-table-column prop="content" label="正文"></el-table-column>
<el-table-column prop="type" label="事件">
<template v-slot="scope">
<!-- 映射type值到el-tag -->
<el-tag :type="getTagType(scope.row.type)">
{{ getTagName(scope.row.type) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="time" label="创建时间"></el-table-column>
<el-table-column prop="user" label="创建人"></el-table-column>
<!-- 操作列 -->
<el-table-column label="操作" width="200">
<template slot-scope="scope">
<el-button size="mini" @click="handleEdit(scope.row)">编辑</el-button>
<el-button size="mini" type="danger" @click="handleDelete(scope.row.id)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页组件 -->
<el-pagination
:current-page.sync="currentPage"
:page-size.sync="pageSize"
:total="total"
background
layout="->,total,sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange">
</el-pagination>
<!-- 编辑/新增对话框 -->
<el-dialog :title="dialogTitle" :visible.sync="fromVisible" width="500px">
<el-form :model="form" label-width="100px">
<!-- 标题表单项 -->
<el-form-item label="标题">
<el-input v-model="form.title"></el-input>
</el-form-item>
<!-- 正文表单项 -->
<el-form-item label="正文">
<el-input type="textarea" v-model="form.content"></el-input>
</el-form-item>
<!-- 事件类型选择 -->
<el-form-item label="事件类型">
<el-select v-model="form.type" placeholder="请选择事件类型">
<el-option label="信息事件" value="info"></el-option>
<el-option label="警告事件" value="warning"></el-option>
<el-option label="错误事件" value="error"></el-option>
</el-select>
</el-form-item>
<!-- 创建时间 -->
<el-form-item label="创建时间">
<el-date-picker v-model="form.time" type="datetime" placeholder="选择日期时间"></el-date-picker>
</el-form-item>
<!-- 创建人 -->
<el-form-item label="创建人">
<el-input v-model="form.user" :disabled="true"></el-input>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="fromVisible = false">取消</el-button>
<el-button type="primary" @click="saveData">保存</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import axios from 'axios';
import AvatarUploader from '@/components/AvatarUploader.vue';
export default {
components: { AvatarUploader },
data() {
return {
search: '', // 用于存储搜索条件
tableData: [], // 存储表格数据
currentPage: 1, // 当前页码
pageSize: 10, // 每页显示条目个数
total: 0, // 总条目数
fromVisible: false, // 控制对话框显示
dialogTitle: '', // 动态对话框标题
form: { // 表单数据(用于新增和编辑)
id: '',
name: '',
age: '',
address: '',
avatar: '' // 头像 URL
},
selectedRows: [] // 用于存储当前选中的多行数据
};
},
created() {
this.loadData(); // 组件创建时加载数据
},
methods: {
// 根据后端传递的type值获取el-tag的类型
getTagType(type) {
const typeMap = {
info: 'success', // 这里可以调整为Element UI中的tag类型,如'success', 'warning', 'danger'
warning: 'warning',
error: 'danger'
};
return typeMap[type] || 'info'; // 默认返回'info'
},
// 根据后端传递的type值获取显示名称
getTagName(type) {
const nameMap = {
info: '信息事件',
warning: '警告事件',
error: '错误事件'
};
return nameMap[type] || '未知事件';
},
handleAdd() { // 新增数据
this.form = {}; // 清空表单数据
this.form.user = this.$store.state.user.username;
this.dialogTitle = '新增信息'; // 设置对话框标题为“新增”
this.fromVisible = true; // 打开对话框
},
handleEdit(row) { // 编辑数据
this.form = {...row}; // 将当前行的数据赋值给表单
this.dialogTitle = '编辑信息'; // 设置对话框标题为“编辑”
this.fromVisible = true; // 打开对话框
},
saveData() { // 保存数据
if (this.form.id) {
// 如果表单中有 ID,说明是编辑操作
axios.put(`/api/save/${this.form.id}`, this.form)
.then(() => {
this.$message.success('编辑成功');
this.fromVisible = false; // 关闭对话框
this.loadData(); // 重新加载数据
})
.catch(error => {
console.error('保存编辑失败:', error);
this.$message.error('保存失败');
});
} else {
// 如果表单中没有 ID,说明是新增操作
axios.post('/api/add', this.form)
.then(() => {
this.$message.success('新增成功');
this.fromVisible = false; // 关闭对话框
this.loadData(); // 重新加载数据
})
.catch(error => {
console.error('新增失败:', error);
this.$message.error('新增失败');
});
}
},
fetchFilteredData() { // 按关键词查询数据
if (this.search === '' || this.search === null) {
return false;
}
// 发起 GET 请求,请求参数为搜索条件
axios.get('/api/search', {
params: {
name: this.search
}
})
.then(response => {
// 更新表格数据为后端返回的查询结果
this.tableData = response.data;
console.log('查询结果:', this.tableData);
})
.catch(error => {
// 处理请求错误
this.$message.error('查询失败');
console.error('查询失败:', error);
});
},
reset() {
this.search = null;
this.loadData();
},
// 处理选择变化事件
handleSelectionChange(selection) {
this.selectedRows = selection; // 更新选中的行数据
},
// 删除选中的行
async deleteSelectedRows() {
try {
// 先进行确认操作
await this.$confirm('此操作将永久删除选中的记录, 是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
});
// 获取选中行的ID数组
const ids = this.selectedRows.map(row => row.id);
// 发送删除请求到后端
await axios.post('/api/deleteRows', {ids});
// 删除成功后,重新加载数据和更新分页信息
this.loadData(); // 重新加载数据,包括更新总条目数
// 清空表格的选择状态
this.$refs.multipleTable.clearSelection();
this.selectedRows = []; // 清空选中的行数据
// 显示成功消息
this.$message.success('删除成功');
} catch (error) {
// 判断是用户取消操作,还是请求失败
if (error === 'cancel') {
this.$message.info('已取消删除');
} else {
this.$message.error('删除失败');
}
}
},
// 初始化加载数据
loadData() {
const params = {
page: this.currentPage,
size: this.pageSize
};
// 使用 axios 发送 GET 请求,查询数据
axios.get('/api/data', {params})
.then(response => {
this.tableData = response.data.data; // 将返回的数据赋值给表格
this.total = response.data.total; // 设置总条目数
})
.catch(error => {
this.$message.error('数据加载失败');
console.error('加载数据失败:', error);
});
},
// 处理每页显示条数改变
handleSizeChange(newSize) {
this.pageSize = newSize;
this.loadData(); // 重新加载数据
},
// 处理当前页码改变
handleCurrentChange(newPage) {
this.currentPage = newPage;
this.loadData(); // 重新加载数据
},
// 处理删除操作
handleDelete(id) {
this.$confirm('此操作将永久删除该记录, 是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
axios.delete(`/api/delete/${id}`)
.then(() => {
this.$message.success('删除成功');
this.loadData(); // 重新加载数据
})
.catch(error => {
console.error('删除失败:', error);
this.$message.error('删除失败');
});
}).catch(() => {
this.$message.info('已取消删除');
});
}
}
};
</script>
<style scoped>
/* 表单项布局美化 */
.el-form-item {
margin-bottom: 20px; /* 增加表单项之间的间距 */
}
.dialog-footer {
text-align: right; /* 使对话框底部的按钮右对齐 */
}
.el-button {
margin-left: 10px; /* 增加按钮之间的间距 */
}
</style>
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
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
# 表格curd组件版本
<template>
<div>
<!-- 表格操作按钮区域 -->
<el-row style="padding-bottom: 5px;">
<el-col :span="4">
<!-- 删除选中行按钮 -->
<el-button type="danger" plain @click="deleteSelectedRows" :disabled="selectedRows.length === 0">
批量删除
</el-button>
<!-- 新增按钮 -->
<el-button type="primary" plain @click="handleAdd">新增</el-button>
</el-col>
<el-col :span="20">
<div>
<!-- 查询输入框 -->
<el-input v-model="search" placeholder="请输入关键字查询" style="width: 200px;" @keydown.enter.native="fetchFilteredData"></el-input>
<el-button type="info" plain @click="fetchFilteredData" style="margin-left: 10px">查询</el-button>
<el-button type="warning" plain @click="reset" style="margin-left: 10px">重置</el-button>
</div>
</el-col>
</el-row>
<!-- 表格组件 -->
<el-table :data="tableData" border stripe style="width: 100%" @selection-change="handleSelectionChange"
ref="multipleTable">
<el-table-column type="selection" width="55"></el-table-column>
<!-- 动态列渲染,根据传入的 columns 参数生成表格列 -->
<el-table-column v-for="column in columns" :key="column.prop" :prop="column.prop" :label="column.label"
:width="column.width">
<template v-if="column.type === 'custom'" v-slot="scope">
<!-- 自定义渲染列 -->
<slot :name="column.slotName" :row="scope.row"></slot>
</template>
</el-table-column>
<!-- 操作列 -->
<el-table-column label="操作" width="200">
<template slot-scope="scope">
<el-button size="mini" @click="handleEdit(scope.row)">编辑</el-button>
<el-button size="mini" type="danger" @click="handleDelete(scope.row.id)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页组件 -->
<el-pagination :current-page.sync="currentPage" :page-size.sync="pageSize" :total="total" background
layout="->,total,sizes, prev, pager, next, jumper" @size-change="handleSizeChange"
@current-change="handleCurrentChange">
</el-pagination>
<!-- 编辑/新增对话框 -->
<el-dialog :title="dialogTitle" :visible.sync="fromVisible" width="500px" lock-scroll>
<el-form :model="form" label-width="100px">
<!-- 动态生成表单项 -->
<el-form-item v-for="field in formFields" :key="field.prop" :label="field.label">
<!-- 使用动态组件渲染表单项 -->
<component :is="field.component" v-model="form[field.prop]" v-bind="field.attrs"></component>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="fromVisible = false">取消</el-button>
<el-button type="primary" @click="saveData">保存</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import request from "@/request";
export default {
props: {
columns: {
type: Array,
required: true,
},
formFields: {
type: Array,
required: true,
},
apiBaseUrl: {
type: String,
required: false,
},
},
data() {
return {
search: '', // 搜索条件
tableData: [], // 表格数据
currentPage: 1, // 当前页码
pageSize: 10, // 每页显示的条目数
total: 0, // 总条目数
fromVisible: false, // 控制对话框的显示状态
dialogTitle: '', // 对话框标题(根据新增/编辑切换)
form: {}, // 表单数据
selectedRows: [] // 存储当前选中的行
};
},
methods: {
// 初始化加载数据
loadData() {
const params = {
page: this.currentPage,
size: this.pageSize
};
// 使用 /user 发送 GET 请求,查询数据
request.get(`/user/data`, { params })
.then(response => {
this.tableData = response.data.data; // 将返回的数据赋值给表格
this.total = response.data.total; // 设置总条目数
})
.catch(error => {
this.$message.error('数据加载失败');
console.error('加载数据失败:', error);
});
},
handleAdd() { // 新增操作
this.form = {}; // 清空表单数据
this.dialogTitle = '新增信息'; // 设置对话框标题为“新增”
this.fromVisible = true; // 打开对话框
},
handleEdit(row) { // 编辑操作
this.form = { ...row }; // 将选中行的数据复制到表单
this.dialogTitle = '编辑信息'; // 设置对话框标题为“编辑”
this.fromVisible = true; // 打开对话框
},
saveData() { // 保存数据(新增/编辑)
if (this.form.id) { // 在表单中,id 可以是一个隐藏字段,用户不需要看到它,但系统在提交表单时会包含这个字段。
// 编辑操作
request.put(`/user/save/${this.form.id}`, this.form)
.then(() => {
this.$message.success('编辑成功');
this.fromVisible = false; // 关闭对话框
this.loadData(); // 重新加载数据
})
.catch(error => {
console.error('保存编辑失败:', error);
this.$message.error('保存失败');
});
} else {
// 新增操作
request.post(`/user/add`, this.form)
.then((res) => {
if(res && res.code === 200) {
this.$message.success('新增成功');
this.fromVisible = false; // 关闭对话框
this.loadData(); // 重新加载数据
}else {
this.$message.error(res.message);
}
})
.catch(error => {
console.error('新增失败:', error);
this.$message.error('新增失败');
});
}
},
fetchFilteredData() { // 按关键词查询数据
if (this.search === '' || this.search === null) {
return false;
}
// 发起 GET 请求,请求参数为搜索条件
request.get(`/user/search`, {
params: {
name: this.search, // 关键词
page: this.currentPage, // 当前页码
size: this.pageSize // 每页显示的条数
}
})
.then(response => {
// 更新表格数据为后端返回的查询结果
this.tableData = response.data.data;
this.total = response.data.total;
console.log('查询结果:', this.tableData);
})
.catch(error => {
// 处理请求错误
this.$message.error('查询失败');
console.error('查询失败:', error);
});
},
reset() {
this.search = null;
this.loadData();
},
handleSelectionChange(selection) {
this.selectedRows = selection; // 更新选中的行数据
},
// 批量删除
async deleteSelectedRows() { // 删除选中的行
try {
// 先进行确认操作
await this.$confirm('此操作将永久删除选中的记录, 是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
});
// 获取选中行的ID数组
const ids = this.selectedRows.map(row => row.id);
// 发送删除请求到后端
const response = await request.post(`/user/deleteRows`, ids);
if(response && response.code === 200) {
// 删除成功后,重新加载数据和更新分页信息
this.loadData(); // 重新加载数据,包括更新总条目数
// 清空表格的选择状态
this.$refs.multipleTable.clearSelection();
this.selectedRows = []; // 清空选中的行数据
// 显示成功消息
this.$message.success('删除成功');
}else {
this.$message.error(response.message);
}
} catch (error) {
// 判断是用户取消操作,还是请求失败
if (error === 'cancel') {
this.$message.info('已取消删除');
} else {
this.$message.error('删除失败');
}
}
},
handleSizeChange(newSize) {
this.pageSize = newSize;
this.loadData(); // 重新加载数据
},
handleCurrentChange(newPage) {
this.currentPage = newPage;
this.loadData(); // 重新加载数据
},
handleDelete(id) { // 删除单个记录
this.$confirm('此操作将永久删除该记录, 是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
request.delete(`/user/delete/${id}`)
.then(() => {
this.$message.success('删除成功');
this.loadData(); // 重新加载数据
})
.catch(error => {
console.error('删除失败:', error);
this.$message.error('删除失败');
});
}).catch(() => {
this.$message.info('已取消删除');
});
}
},
created() {
this.loadData(); // 组件创建时加载数据
}
};
</script>
<style scoped>
/* 表单项布局美化 */
.el-form-item {
margin-bottom: 20px;
/* 增加表单项之间的间距 */
}
.dialog-footer {
text-align: right;
/* 使对话框底部的按钮右对齐 */
}
.el-button {
margin-left: 10px;
/* 增加按钮之间的间距 */
}
</style>
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
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
# 表格curd组件的使用
<template>
<div class='About'>
<table-crud :columns="columns" :formFields="formFields" >
<!-- 定义头像列的自定义插槽 -->
<template v-slot:avatarSlot="{ row }">
<el-image :src="row.avatar" alt="头像" fit="contain" style="width: 50px; height: 50px; border-radius: 50%;"></el-image>
</template>
</table-crud>
</div>
</template>
<script>
import TableCrud from '@/components/TableCrud.vue';
export default {
name: 'About',
components: { TableCrud },
data() {
return {
// 定义表格列配置
columns: [
{ prop: 'id', label: 'ID', width: '80' },
{ prop: 'username', label: '名称' },
{ prop: 'email', label: '邮箱' },
{ prop: 'avatar', label: '头像', width: '100', type: 'custom', slotName: 'avatarSlot' },
{ prop: 'createTime', label: '创建时间' }
],
// 定义表单字段配置
formFields: [
{ prop: 'username', label: '名称', component: 'el-input', attrs: { placeholder: '请输入名称'} },
{ prop: 'email', label: '邮箱', component: 'el-input', attrs: { placeholder: '请输入邮箱' } },
{ prop: 'avatar', label: '头像', component: 'AvatarUploader', attrs: { action: '/api/upload/uploadAvatar' } },
{ prop: 'createTime', label: '创建时间', component: 'el-input', attrs: { placeholder: '请输入创建时间' } }
]
};
}
}
</script>
<style scoped>
/* 样式设置(可以根据需要修改为 less 或 sass 等预处理器) */
</style>
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
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
# 头像上传组件
<template>
<div>
<el-upload class="avatar-uploader" :action="action" :headers="headers" :show-file-list="false"
:on-success="handleAvatarSuccess" :on-error="handleAvatarError" :before-upload="beforeAvatarUpload"
:on-progress="handleProgress" drag>
<div v-if="uploading" class="progress-wrapper">
<el-progress type="circle" :percentage="uploadPercentage"></el-progress>
<div class="progress-text">{{ uploadPercentage }}%</div>
</div>
<div v-else-if="imageUrl" class="avatar-container">
<img :src="imageUrl" class="avatar" />
</div>
<div v-else class="upload-placeholder">
<i class="el-icon-plus avatar-uploader-icon"></i>
<div class="el-upload__text">拖拽或点击上传</div>
</div>
</el-upload>
</div>
</template>
<script>
export default {
name: 'AvatarUploader',
props: {
action: {
type: String,
required: true
},
headers: {
type: Object,
default: () => ({})
},
value: {
type: String,
default: ''
}
},
data() {
return {
imageUrl: this.value, // 用于存储上传成功后的图片URL
uploading: false, // 上传状态
uploadPercentage: 0 // 上传进度百分比
};
},
watch: {
value(newVal) {
this.imageUrl = newVal;
}
},
methods: {
// 上传成功后的处理函数
handleAvatarSuccess(response, file) {
if(response && response.code === 200) {
this.uploading = false; // 结束上传状态
this.imageUrl = response.data; // 使用服务器返回的URL
this.$emit('input', this.imageUrl);
}else {
this.uploading = false; // 结束上传状态
this.$message.error('上传失败');
}
},
// 上传失败后的处理函数
handleAvatarError(err, file) {
this.uploading = false; // 结束上传状态
this.$message.error('上传失败');
// this.imageUrl = 'https://web-183.oss-cn-beijing.aliyuncs.com/typora/202408080812350.jpg';
},
// 上传前的检查函数,限制文件格式和大小
beforeAvatarUpload(file) {
const isJPG = file.type === 'image/jpeg' || file.type === 'image/png';
const isLt500K = file.size / 1024 / 1024 < 0.5;
if (!isJPG) {
this.$message.error('上传头像图片只能是 JPG 或 PNG 格式!');
return false;
}
if (!isLt500K) {
this.$message.error('上传头像图片大小不能超过 500KB!');
return false;
}
this.uploading = true; // 开始上传状态
this.uploadPercentage = 0; // 重置进度条
return isJPG && isLt500K;
},
// 处理上传进度
handleProgress(event, file, fileList) {
this.uploadPercentage = Math.round((event.loaded / event.total) * 100);
}
}
};
</script>
<style scoped>
/* 上传组件的主要容器样式 */
.avatar-uploader {
position: relative;
/* 定位方式为相对定位,以便内部元素定位 */
width: 140px;
/* 固定宽度 */
height: 140px;
/* 固定高度 */
border: 1px dashed #d9d9d9;
/* 边框样式为虚线 */
border-radius: 6px;
/* 边框圆角 */
cursor: pointer;
/* 鼠标指针样式为手形 */
overflow: hidden;
/* 隐藏溢出内容 */
display: flex;
/* 使用弹性布局 */
align-items: center;
/* 垂直居中对齐 */
justify-content: center;
/* 水平居中对齐 */
}
/* 包含上传图片的容器样式 */
.avatar-container {
width: 100%;
/* 宽度为父元素的100% */
height: 100%;
/* 高度为父元素的100% */
display: flex;
/* 使用弹性布局 */
align-items: center;
/* 垂直居中对齐 */
justify-content: center;
/* 水平居中对齐 */
border-radius: 6px;
/* 边框圆角 */
overflow: hidden;
/* 隐藏溢出内容 */
}
/* 上传图标样式 */
.avatar-uploader-icon {
font-size: 28px;
/* 字体大小 */
color: #8c939d;
/* 字体颜色 */
}
/* 上传的图片样式 */
.avatar {
max-width: 100%;
/* 最大宽度为父元素的100% */
max-height: 100%;
/* 最大高度为父元素的100% */
object-fit: contain;
/* 保持图像的长宽比 */
}
/* 上传占位符样式 */
.upload-placeholder {
display: flex;
/* 使用弹性布局 */
flex-direction: column;
/* 垂直排列 */
align-items: center;
/* 垂直居中对齐 */
justify-content: center;
/* 水平居中对齐 */
width: 100%;
/* 宽度为父元素的100% */
height: 100%;
/* 高度为父元素的100% */
}
/* 进度条容器样式 */
.progress-wrapper {
position: absolute;
/* 定位方式为绝对定位 */
top: 50%;
/* 垂直居中 */
left: 50%;
/* 水平居中 */
transform: translate(-50%, -50%);
/* 移动到中心位置 */
width: 60px;
/* 固定宽度 */
height: 60px;
/* 固定高度 */
display: flex;
/* 使用弹性布局 */
flex-direction: column;
/* 垂直排列 */
align-items: center;
/* 垂直居中对齐 */
justify-content: center;
/* 水平居中对齐 */
}
/* 进度条文本样式 */
.progress-text {
margin-top: 8px;
/* 顶部外边距 */
font-size: 14px;
/* 字体大小 */
color: #606266;
/* 字体颜色 */
}
</style>
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
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
# Echarts图表组件
<template>
<!-- 创建图表容器,设置图表的宽度和高度 -->
<div ref="chart" :style="{ width: width, height: height }"></div>
</template>
<script>
import * as echarts from 'echarts';
export default {
props: {
options: {
type: Object,
required: true // 接收图表的配置项,必须传入
},
width: {
type: String,
default: '600px' // 图表宽度,默认为600px
},
height: {
type: String,
default: '400px' // 图表高度,默认为400px
},
onChartClick: {
type: Function,
default: null // 点击事件处理函数,可选
}
},
mounted() {
this.initChart(); // 组件挂载后初始化图表
},
methods: {
/**
* 初始化 ECharts 图表实例
*/
initChart() {
const chart = echarts.init(this.$refs.chart); // 初始化 ECharts 实例
chart.setOption(this.options); // 设置图表配置项
if (this.onChartClick) {
chart.on('click', this.onChartClick); // 如果传入了 onChartClick 函数,注册点击事件
}
this.chart = chart; // 保存实例,便于后续操作
},
/**
* 更新图表配置项
* @param {Object} newOptions 新的配置项
*/
updateChart(newOptions) {
if (this.chart) {
this.chart.setOption(newOptions, true); // 更新图表配置项
}
},
/**
* 销毁 ECharts 实例,避免内存泄漏
*/
destroyChart() {
if (this.chart) {
this.chart.dispose(); // 销毁图表实例
}
}
},
watch: {
options: {
deep: true,
handler(newOptions) {
this.updateChart(newOptions); // 监听 options 变化,更新图表
}
}
},
beforeDestroy() {
this.destroyChart(); // 组件销毁前,销毁图表实例
}
};
</script>
<style scoped>
/* 设置图表容器的样式,确保宽度和高度 */
div[ref="chart"] {
width: 100%;
height: 100%;
}
</style>
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
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
# 轮播图组件
<template>
<el-carousel ref="carousel" :interval="5000" arrow="always" height="400px" :autoplay="true"
class="carousel-container">
<!-- 循环渲染 Carousel 项 -->
<el-carousel-item v-for="(item, index) in images" :key="index">
<img :src="item.src" :alt="item.alt" class="carousel-image" @click="handleImageClick(item)"
@error="handleImageError" />
</el-carousel-item>
</el-carousel>
</template>
<script>
export default {
name: 'ImageCarousel',
props: {
images: {
type: Array,
required: true,
default: () => []
},
interval: {
type: Number,
default: 5000
},
arrow: {
type: String,
default: 'always'
},
height: {
type: String,
default: '400px'
},
autoplay: {
type: Boolean,
default: true
},
fallbackImage: {
type: String,
default: 'https://via.placeholder.com/800x400?text=Image+Not+Available'
}
},
methods: {
// 处理图片点击事件,使用 vue-router 进行页面跳转
handleImageClick(image) {
if (image && image.link) {
this.$router.push(image.link).catch(err => {
console.error('Navigation error:', err); // 捕获并记录导航错误
});
}
},
// 处理图片加载错误
handleImageError(event) {
event.target.src = this.fallbackImage;
}
}
};
</script>
<style scoped>
/* 为整个 Carousel 容器添加圆角和隐藏溢出内容 */
.carousel-container {
border-radius: 12px;
/* 圆角 */
overflow: hidden;
/* 隐藏溢出内容 */
}
/* 设置轮播图内图片的样式 */
.carousel-image {
width: 100%;
/* 图片宽度占满容器 */
height: 100%;
/* 高度占满容器 */
object-fit: cover;
/* 保持图片比例,裁剪多余部分 */
cursor: pointer;
/* 鼠标悬浮时显示小手状态 */
transition: transform 0.3s ease, opacity 0.3s ease;
/* 增加平滑的过渡效果 */
}
.carousel-image:hover {
transform: scale(1.05);
/* 鼠标悬浮时放大 */
opacity: 0.9;
/* 悬浮时略微降低不透明度 */
}
/* 自定义 Carousel 切换箭头的样式 */
.el-carousel__arrow {
background-color: rgba(0, 0, 0, 0.5);
/* 半透明黑色背景 */
transition: background-color 0.3s ease, transform 0.3s ease;
/* 增加箭头过渡效果 */
border-radius: 50%;
/* 箭头背景圆角 */
}
.el-carousel__arrow:hover {
background-color: rgba(0, 0, 0, 0.7);
/* 悬浮时背景颜色变深 */
transform: scale(1.2);
/* 悬浮时放大箭头 */
}
/* 自定义指示器样式 */
.el-carousel__indicator {
background-color: rgba(255, 255, 255, 0.5);
/* 半透明白色指示器 */
border-radius: 50%;
/* 圆角 */
width: 12px;
height: 12px;
margin: 0 5px;
/* 控制指示器之间的间距 */
transition: background-color 0.3s ease, transform 0.3s ease;
/* 增加过渡效果 */
}
.el-carousel__indicator:hover {
transform: scale(1.2);
/* 悬浮时放大指示器 */
background-color: rgba(255, 255, 255, 0.8);
/* 悬浮时颜色加深 */
}
.el-carousel__indicator--active {
background-color: rgba(255, 255, 255, 1);
/* 活跃指示器为不透明 */
transform: scale(1.5);
/* 活跃的指示器稍大 */
}
</style>
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
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
# 登录页面组件
<template>
<div class="login-container">
<el-form :model="loginForm" :rules="loginRules" ref="loginForm" label-width="70px" class="loginForm">
<div style="display: flex;align-items: center; justify-content: center;">
<h2>欢迎登录</h2>
<!-- <span style="margin: 12px 0;font-size: 20px;font-weight: 400;">欢迎登录</span> -->
</div>
<el-form-item label="用户名" prop="username">
<el-input prefix-icon="el-icon-user" v-model="loginForm.username" placeholder="请输入用户名"></el-input>
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input prefix-icon="el-icon-lock" type="password" v-model="loginForm.password" placeholder="请输入密码"
show-password @keydown.enter.native="handleLogin"></el-input>
</el-form-item>
<!-- 验证码 -->
<el-form-item label="验证码" prop="vercode">
<div>
<el-row style="display: flex;">
<el-col :span="15">
<el-input v-model="loginForm.vercode" prefix-icon="el-icon-lock" style=" margin-right: 10px"
placeholder="请输入验证码" @keydown.enter.native="handleLogin"></el-input>
</el-col>
<el-col :span="4" style="margin-left: 3px; cursor: pointer" >
<img :src='captchaImg' height="100%" alt="验证码" @click="refresh()">
</el-col>
</el-row>
</div>
</el-form-item>
<el-form-item>
<el-checkbox v-model="loginForm.rememberMe">记住我</el-checkbox>
<el-row>
<div style="display: flex;justify-content: space-between;">
<el-button type="text" @click="changePassword">忘记密码?</el-button>
<el-button type="text" @click="goToRegister">没有账号?</el-button>
</div>
</el-row>
</el-form-item>
<el-form-item>
<div style="display: flex">
<el-button type="primary" @click="handleLogin">登录</el-button>
<el-button @click="handleReset">重置</el-button>
<div style="display: flex;justify-content: center;align-items: center;margin-left: 10px">
<svg-icon icon-name="github" style="font-size: 25px;cursor: pointer" @click.native="githubLogin"></svg-icon>
<svg-icon icon-name="gitee" style="font-size: 26px; margin-left: 10px;cursor: pointer" @click.native="giteeLogin"></svg-icon>
</div>
</div>
</el-form-item>
</el-form>
<el-dialog :visible.sync="dialogFormVisible" title="修改密码" width="500px">
<el-form :model="editForm" :rules="editRules" ref="editForm" label-width="100px">
<el-form-item label="用户名" prop="username">
<el-input prefix-icon="el-icon-user" v-model="editForm.username" placeholder="请输入用户名"></el-input>
</el-form-item>
<el-form-item label="邮箱" prop="email">
<el-input prefix-icon="el-icon-message" v-model="editForm.email" placeholder="请输入邮箱"></el-input>
</el-form-item>
<el-form-item label="验证码" prop="code">
<el-row>
<el-col :span="16">
<el-input v-model="editForm.code" placeholder="请输入验证码" ></el-input>
</el-col>
<el-col :span="7">
<el-button type="primary" plain @click="sendCode" style="margin-left: 10px;">发送验证码</el-button>
</el-col>
</el-row>
</el-form-item>
<el-form-item label="新密码" prop="newPassword">
<el-input type="password" prefix-icon="el-icon-lock" v-model="editForm.newPassword" placeholder="请输入新密码"
show-password></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleChangePassword">提交</el-button>
<el-button @click="dialogFormVisible = false">取消</el-button>
</el-form-item>
</el-form>
</el-dialog>
</div>
</template>
<script>
import request from "@/request";
export default {
data() {
return {
dialogFormVisible: false, // 控制 Dialog 显示与隐藏的状态
captchaImg:'', // 后端返回的图片验证码
loginForm: {
username: '',
password: '',
rememberMe: false,
vercode:'', // 前端输入的图片验证码
captchaKey:'' //图片验证码的唯一key
},
editForm: {
username: '',
email: '',
code: '',
newPassword: ''
},
loginRules: {
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' },
{ min: 3, max: 15, message: '长度在 3 到 15 个字符', trigger: 'blur' }
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 6, message: '密码长度不能少于 6 个字符', trigger: 'blur' }
],
vercode:[
{ required: true, message: '请输入验证码', trigger: 'blur' },
]
},
editRules: {
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' }
],
email: [
{ required: true, message: '请输入邮箱', trigger: 'blur' },
{ type: 'email', message: '请输入正确的邮箱地址', trigger: 'blur' }
],
code: [
{ required: true, message: '请输入验证码', trigger: 'blur' }
],
newPassword: [
{ required: true, message: '请输入新密码', trigger: 'blur' },
{ min: 6, message: '密码长度不能少于 6 个字符', trigger: 'blur' }
]
}
};
},
methods: {
githubLogin() {
// 获取当前页面的回调地址,如果为空则默认使用首页地址
const callbackUrl = this.$route.query.redirectUrl || '/main';
request.get('/web/login/github', {
params: {
callbackUrl:callbackUrl
}
}).then(res => {
if(res && res.code ===200) {
location.href = res.data;
}
}
)
},
giteeLogin() {
// 获取当前页面的回调地址,如果为空则默认使用首页地址
const callbackUrl = this.$route.query.redirectUrl || '/main';
request.get('/web/login/gitee', {
params: {
callbackUrl:callbackUrl
}
}).then(res => {
if(res && res.code ===200) {
location.href = res.data;
}
}
)
},
handleLogin() {
this.$refs.loginForm.validate((valid) => {
if (valid) {
request.post('/web/login', this.loginForm)
.then(response => {
if(response && response.code === 200) {
const user = response.data; // 服务器返回的完整用户信息
// 根据用户是否选择“记住我”,确定存储类型
const storageType = this.loginForm.rememberMe ? 'local' : 'session';
// 将存储类型设置到 Vuex
this.$store.commit('SetStorageType', storageType);
// 将用户信息提交到 Vuex,并同步到指定的存储(localStorage 或 sessionStorage)
this.$store.commit('EditUser', user);
// 登录成功后跳转到主页面
this.$router.push('/main');
this.$message.success('登录成功');
}else {
this.$message.error(response.message);
}
})
.catch(error => {
this.$message.error('登录失败:'+error);
console.error(error);
});
// this.$message.success('登录成功');
// this.$router.push('/main');
} else {
this.$message.error('请完善表单信息');
return false;
}
});
},
handleReset() {
this.$refs.loginForm.resetFields();
},
goToRegister() {
this.$router.push({ path: '/register' });
},
changePassword() {
this.dialogFormVisible = true;
},
sendCode() {
// 发送验证码逻辑
// request.post('/send-code', {
// username: this.editForm.username,
// email: this.editForm.email
// })
// .then(response => {
// this.$message.success('验证码已发送');
// })
// .catch(error => {
// this.$message.error('发送验证码失败');
// console.error(error);
// });
},
handleChangePassword() {
// this.$refs.editForm.validate((valid) => {
// if (valid) {
// axios.post('/api/change-password', {
// username: this.editForm.username,
// email: this.editForm.email,
// code: this.editForm.code,
// newPassword: this.editForm.newPassword
// })
// .then(response => {
// this.$message.success('密码修改成功');
// this.dialogFormVisible = false;
// })
// .catch(error => {
// this.$message.error('修改密码失败');
// console.error(error);
// });
// } else {
// this.$message.error('请完善表单信息');
// return false;
// }
// });
},
// 获取图片验证码请求
refresh() {
request.post("/web/getcode").then(res => {
if (res&& res.code === 200) {
// 设置图片验证码
this.captchaImg = res.data.captchaImg;
// 将后端返回的该图片验证码的唯一key存进当前表单里面,用于提交到后端去获取redis里面的验证码
this.loginForm.captchaKey = res.data.captchaKey;
}else {
this.$message.warning('获取图片验证码失败');
}
})
}
},
mounted() {
this.refresh(); // 页面加载的时候刷新验证码
},
};
</script>
<style scoped>
.login-container {
display: flex;
align-items: center;
justify-content: center;
height: 100vh;
background: linear-gradient(135deg, #d0d2ea, #d3d4e1);
/* 渐变背景色 */
/* background-image: url('/path/to/your/background.jpg'); */
/* 背景图像 */
background-size: cover;
/* 保证图片覆盖整个背景 */
background-repeat: no-repeat;
background-position: center;
}
.loginForm {
width: 400px;
padding: 20px;
background-color: rgba(255, 255, 255, 0.9);
/* 半透明白色背景 */
border: 1px solid #ebeef5;
border-radius: 5px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
/deep/.el-form-item__content {
line-height: 20px !important;
position: relative;
font-size: 14px;
}
/deep/.el-form-item {
margin-bottom: 12px;
}
</style>
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
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
# 注册页面组件
<template>
<div style="display: flex;align-items: center; height: 100vh;">
<el-form :model="registerForm" :rules="registerRules" ref="registerForm" label-width="100px">
<div style="display: flex;justify-content: center;">
<h2>欢迎注册</h2>
</div>
<el-form-item label="用户名" prop="username">
<el-input prefix-icon="el-icon-user" v-model="registerForm.username" placeholder="请输入用户名"></el-input>
</el-form-item>
<el-form-item prefix-icon="el-icon-lock" label="密码" prop="password" >
<el-input type="password" v-model="registerForm.password" show-password placeholder="请输入密码"></el-input>
</el-form-item>
<el-form-item label="确认密码" prop="confirmPassword">
<el-input prefix-icon="el-icon-lock" type="password" show-password v-model="registerForm.confirmPassword"
placeholder="请再次输入密码"></el-input>
</el-form-item>
<el-form-item label="邮箱" prop="email">
<el-input prefix-icon="el-icon-box" v-model="registerForm.email" placeholder="请输入邮箱"></el-input>
</el-form-item>
<el-form-item>
<div>
<el-button type="primary" @click="handleRegister" style="width: 120px;">注册</el-button>
<!-- <el-button @click="handleReset">重置</el-button> -->
<el-button type="text" @click="gotologin">前往登录?</el-button>
</div>
</el-form-item>
</el-form>
</div>
</template>
<script>
import request from "@/request";
export default {
data() {
return {
registerForm: {
username: '',
password: '',
confirmPassword: '',
email: ''
},
registerRules: {
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' },
{ min: 3, max: 15, message: '长度在 3 到 15 个字符', trigger: 'blur' }
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 6, message: '密码长度不能少于 6 个字符', trigger: 'blur' }
],
confirmPassword: [
{ required: true, message: '请再次输入密码', trigger: 'blur' },
{
validator: (rule, value, callback) => {
if (value !== this.registerForm.password) {
callback(new Error('两次输入的密码不一致'));
} else {
callback();
}
}, trigger: 'blur'
}
],
email: [
{ required: true, message: '请输入邮箱', trigger: 'blur' },
{ type: 'email', message: '请输入正确的邮箱地址', trigger: 'blur' }
]
}
};
},
methods: {
handleRegister() {
this.$refs.registerForm.validate((valid) => {
if (valid) {
request.post('/web/register',this.registerForm).then(res => {
if(res && res.code === 200) {
this.$message.success('注册成功');
this.registerForm = {};
this.$router.push('/login');
}else {
this.$message.error(res.message);
}
}).catch(
error => {
this.$message.error(error);
}
)
} else {
this.$message.error('请完善表单信息');
return false;
}
});
},
handleReset() {
this.$refs.registerForm.resetFields();
},
gotologin() {
this.$router.push('/login');
}
}
};
</script>
<style scoped>
.el-form {
width: 400px;
margin: 100px auto;
padding: 20px;
border: 1px solid #ebeef5;
border-radius: 5px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
</style>
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
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
# 个人信息页面组件
<template>
<div class='User'>
<el-tabs v-model="activeTab" >
<el-tab-pane label="修改信息" name="first">
<div style="width: 100%; display: flex; align-items: center; padding: 100px 0 0 200px">
<div>
<AvatarUploader :action="action" v-model="form.avatar"></AvatarUploader>
</div>
<div>
<div style="width: 300px; padding-left: 20px;">
<el-form :model="form" :rules="rules" ref="form" >
<el-form-item label="用户名" prop="username">
<el-input v-model="form.username" disabled></el-input>
</el-form-item>
<el-form-item label="邮箱" prop="email">
<el-input type="email" v-model="form.email"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click.native="submitForm">提交</el-button>
<el-button @click="resetForm">重置</el-button>
</el-form-item>
</el-form>
</div>
</div>
</div>
</el-tab-pane>
<el-tab-pane label="修改密码" name="second">
<div style="width: 300px;">
<el-form :model="editPasswodForm" ref="editForm" :rules="rules" label-position="left" label-width="100px">
<el-form-item label="旧密码" prop="oldPassword">
<el-input type="password" v-model="editPasswodForm.oldPassword" show-password></el-input>
</el-form-item>
<el-form-item label="新密码" prop="newPassword">
<el-input type="password" v-model="editPasswodForm.newPassword" show-password></el-input>
</el-form-item>
<el-form-item label="确认密码" prop="confirmPassword">
<el-input type="password" v-model="editPasswodForm.confirmPassword" show-password></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click.native="submitPassword">提交</el-button>
</el-form-item>
</el-form>
</div>
</el-tab-pane>
</el-tabs>
</div>
</template>
<script>
import AvatarUploader from '../components/AvatarUploader'
import request from "@/request";
export default {
name: 'User',
components: {
AvatarUploader,},
data() {
return {
action:'/api/upload/uploadAvatar',
activeTab: 'first', // 当前激活的选项卡
form: {
username: this.$store.state.user.username, // 表单用户名字段
email: this.$store.state.user.email, // 表单邮箱字段
avatar:this.$store.state.user.avatar,
},
rules: {
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' }
],
email: [
{ required: true, message: '请输入邮箱', trigger: 'blur' },
{ type: 'email', message: '请输入正确的邮箱地址', trigger: 'blur' }
],
oldPassword:[
{ required: true, message: '请输入密码', trigger: 'blur' }, // 必填项校验
{ min: 6, message: '密码长度不能少于 6 个字符', trigger: 'blur' }, // 长度校验
],
newPassword:[
{ required: true, message: '请输入密码', trigger: 'blur' }, // 必填项校验
{ min: 6, message: '密码长度不能少于 6 个字符', trigger: 'blur' }, // 长度校验
],
confirmPassword: [
{ required: true, message: '请确认密码', trigger: 'blur' }, // 必填项校验
{ validator: this.validatePasswordMatch, trigger: 'blur' } // 自定义校验函数
]
},
editPasswodForm:{
oldPassword:'',
newPassword:'',
confirmPassword:''
}
};
},
methods: {
validatePasswordMatch(rule, value, callback) {
// 校验两次输入的密码是否一致
if (value !== this.editPasswodForm.newPassword) {
callback(new Error('两次输入的密码不一致'));
} else {
callback();
}
},
submitForm() {
this.$refs.form.validate((valid) => {
if(valid) {
request.put(`/user/save/${this.$store.state.user.id}`,this.form).then((response) => {
if(response && response.code === 200) {
// 更新 Vuex 中的用户信息
this.$store.commit("EditUser", this.form);
this.$message.success('保存成功');
}
})
}
});
},
resetForm() {
this.form = {};
this.$message.success('重置成功');
},
submitPassword() {
this.$refs.editForm.validate((valid) => {
if(valid) {
request.put(`/user/updatePassword/${this.$store.state.user.id}`,this.editPasswodForm).then((response) => {
if(response && response.code === 200) {
this.$message.success('修改成功');
}else {
this.$message.error(response.message);
}
})
}
})
}
}
}
</script>
<style scoped>
/* 样式设置(可以根据需要修改为 less 或 sass 等预处理器) */
</style>
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
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
# 第三方登录回调页面组件
<template>
<div class="auth-callback">
<!-- 页面背景 -->
<div class="background"></div>
<!-- 主内容区 -->
<div class="content">
<i v-if="isLoading" class="el-icon-loading loading-icon"></i>
</div>
</div>
</template>
<script>
import request from "@/request";
export default {
data() {
return {
isLoading: true, // 控制加载动画的显示
};
},
created() {
this.handleAuthCallback();
},
methods: {
async handleAuthCallback() {
const { code, state } = this.$route.query;
if (!code || !state) {
this.handleError("缺少必要的授权参数");
return;
}
try {
const result = await request.get('/web/callback/gitee', {
params: { code, state }
});
if (result && result.code === 200) {
this.handleSuccess(result.data);
} else {
this.handleError(result.message || "登录失败,请重试");
}
} catch (error) {
this.handleError("处理授权回调失败,请稍后再试");
console.error("处理授权回调失败:", error);
}
},
handleSuccess(data) {
const user = data;
this.$store.commit("EditUser",user);
// 设置为本地localStorage存储
this.$store.commit("SetStorageType",'local');
let redirectUrl = data.callbackUrl || '/main';
if (redirectUrl === '/login') {
redirectUrl = '/main';
}
this.$router.push(redirectUrl);
},
handleError(message) {
this.$message.error(message); // 显示错误信息
this.$router.push('/login'); // 立即跳转到登录页面
}
}
};
</script>
<style scoped>
.auth-callback {
position: relative;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
background-color: #f0f2f5;
}
.background {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(135deg, #6a11cb 0%, #2575fc 100%);
z-index: -1;
}
.content {
text-align: center;
padding: 20px;
background: rgba(255, 255, 255, 0.9);
border-radius: 8px;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
}
.loading-icon {
font-size: 48px;
color: #409eff;
}
</style>
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
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
# 404页面组件
<template>
<div class="not-found">
<el-card class="card" shadow="hover">
<div class="sign">
<p>肥肠抱歉,你要找的页面不见了</p>
</div>
</el-card>
<div class="additional-content">
<el-button type="primary" @click="goBack">返回上一页</el-button>
<!-- <el-button type="text" @click="goHome">回到首页</el-button> -->
</div>
</div>
</template>
<script>
export default {
methods: {
goBack() {
this.$router.go(-1);
},
goHome() {
this.$router.push('/');
}
}
};
</script>
<style scoped>
.not-found {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 100vh;
background-color: #f0f2f5;
padding: 20px;
}
.card {
width: 600px;
/* 增加宽度 */
height: 350px;
/* 增加高度 */
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 40px;
/* 增加底部间距 */
transition: transform 0.3s ease;
text-align: center;
background-color: #fff;
border-radius: 12px;
}
.card:hover .sign {
transform: scale(1.1);
}
.sign {
width: 350px;
/* 增加宽度 */
height: 100px;
/* 增加高度 */
background-color: #e6f7ff;
color: #409eff;
font-size: 22px;
/* 增加字体大小 */
font-weight: bold;
display: flex;
justify-content: center;
align-items: center;
border: 2px solid #409eff;
border-radius: 8px;
position: relative;
text-align: center;
transition: transform 0.3s ease;
}
.sign::before,
.sign::after {
content: "";
width: 12px;
/* 增加宽度 */
height: 60px;
/* 增加高度 */
background-color: #409eff;
position: absolute;
top: -60px;
/* 调整位置 */
}
.sign::before {
left: 60px;
}
.sign::after {
right: 60px;
}
.sign p {
margin: 0;
transform: rotate(-5deg);
}
.additional-content {
display: flex;
gap: 20px;
/* 增加按钮间距 */
}
.el-button {
padding: 15px 30px;
/* 增加按钮尺寸 */
border-radius: 8px;
/* 调整按钮圆角 */
font-size: 16px;
/* 增加字体大小 */
}
.el-button.text {
color: #409eff;
}
.el-button.text:hover {
color: #66b1ff;
}
</style>
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
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
编辑此页 (opens new window)
上次更新: 2024/12/28, 18:32:08