从零开始使用vue2+element搭建后台管理系统(框架布局+面包屑+内容页加载实现)
先在components下分别创建侧边栏、顶部、布局等组件,用于全局配置:
CommonAside.vue
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 | <template> <el-menu class = "el-menu-vertical" background-color= "#282e38" text-color= "#f4f4f5" active-text-color= "#f57d2d" : default -active= "activeMenu" :router= "true" :collapse= "isCollapse" @open= "handleOpen" @close= "handleClose" > <!-- :background-color= "global.menuBg" --> <!-- :text-color= "global.menuText" --> <!-- :active-text-color= "global.menuActiveText" --> <el-menu-item @click= "clickItem(item)" v- for = "item in noChildren" :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- for = "item in hasChildren" :key= "item.label" :index= "item.path" > <template slot= "title" > <i : class = "`el-icon-${item.icon}`" ></i> <span slot= "title" >{{ item.label }}</span> </template> <el-menu-item-group v- for = "subItem in item.children" :key= "subItem.name" > <el-menu-item @click= "clickItem(subItem)" :index= "subItem.path" >{{ subItem.label }}</el-menu-item> </el-menu-item-group> </el-submenu> </el-menu> </template> <style scoped lang= "scss" > @ import "@/assets/css/global.scss" ; .el-menu-vertical:not(.el-menu--collapse) { width: 210px; min-height: 400px; } .el-menu { height: 100vh; border-right: none; } .el-menu-item:focus, .el-menu-item:hover { outline: 0; background-color: $menuHover !important; } </style> <script> import global from "@/assets/css/global.scss" ; export default { data() { return {}; }, methods: { handleOpen(key, keyPath) { console.log(key, keyPath); }, handleClose(key, keyPath) { console.log(key, keyPath); }, clickItem(item) { // 防止自己跳自己的报错 if ( this .$route.path !== item.path && !( this .$route.path === "/home" && item.path === "/" ) ) { this .$router.push(item.path); } // 面包屑 this .$store.commit( "basic/SelectMenu" , item); }, }, computed: { // 获取菜单 MenuData() { console.log( this .$store.state.basic.menu, "xxxx" ); return this .$store.state.basic.menu; }, noChildren() { // 如果没有children则返回true,会被过滤器留下 return this .MenuData.filter((item) => !item.children); }, hasChildren() { return this .MenuData.filter((item) => item.children); }, // 要放到计算属性,自动计算 isCollapse() { return this .$store.state.basic.isCollapse; }, // 当前页面 activeMenu() { console.log( this .$route.path, "-----------this.$route.path" ); return this .$route.path; }, // 通用样式配置 global() { return global; }, }, }; </script> |
顺手贴一个公用样式文件:
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 | $blue: #324157; $light-blue: #3a71a8; $red: #c03639; $pink: #e65d6e; $green: #30b08f; $tiffany: #4ab7bd; $yellow: #fec171; $panGreen: #30b08f; // $menuText: #f4f4f5; $menuText: #cecece; $menuActiveText: #f57d2d; $subMenuActiveText: #f4f4f5; $menuBg: #282e38; $menuHover: #3e434c; $subMenuBg: #c2c2c2; $subMenuHover: #b6b6b6; : export { menuText: $menuText; menuActiveText: $menuActiveText; subMenuActiveText: $subMenuActiveText; menuBg: $menuBg; menuHover: $menuHover; subMenuBg: $subMenuBg; subMenuHover: $subMenuHover; } |
导出后,在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 | export function getInfo(mobile) { console.log(mobile, "mobile" ); return { code: 10000, msg: "成功" , data: { name: "WUN" , avatar: "https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png" , menuList: [ { path: "/account/account" , name: "user" , label: "烛月账号管理" , icon: "s-custom" , url: "account/AccountView" , }, { path: "/order/order" , name: "order" , label: "订单管理" , icon: "s-marketing" , url: "order/OrderView" , }, { path: "/auth/auth" , name: "auth" , label: "权限管理" , icon: "s-tools" , url: "auth/AuthView" , }, ], }, }; } |
然后登录成功后,获取用户信息时进行存储:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | // 获取用户信息 async getUserInfo(mobile) { try { this .loading = true ; const res = await getInfo(mobile); if (res.code !== 10000) this .$message.error(res.msg || "获取用户信息失败!" ); if (res.code === 10000 && res.data) { this .$store.commit( "user/SET_NAME" , res.data.name); this .$store.commit( "user/SET_AVATAR" , res.data.avatar); this .$store.commit( "basic/setMenu" , res.data.menuList); setTimeout(() => { this .$router.push( "/home" ); }); } } finally { this .loading = false ; } }, |
这里因为是假接口,所以直接在方法中进行了处理,实际上是在store的actions中进行请求及数据管理的。接下来是store文件夹下basic.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 | import Vue from "vue" ; import Vuex from "vuex" ; import Cookie from "js-cookies" ; import { getMenu } from "@/utils/auth" ; Vue.use(Vuex); const getDefaultState = () => { return { // 导航栏是否折叠 isCollapse: false , // 页签数据 tabList: [ { path: "/" , name: "home" , label: "首页" , icon: "s-home" , url: "Home/Home" , }, ], // 菜单 menu: getMenu(), }; }; const state = getDefaultState(); const mutations = { // 修改导航栏展开和收起的方法 CollapseMenu(state) { state.isCollapse = !state.isCollapse; }, // 更新页签数据 SelectMenu(state, item) { // 如果点击的不在页签数据中,则添加 const index = state.tabList.findIndex((val) => val.name === item.name); if (index === -1) { state.tabList.push(item); } }, // 删除tag:删除tabList中对应的item closeTag(state, item) { const index = state.tabList.findIndex((val) => val.name === item.name); state.tabList.splice(index, 1); }, // 设置不同用户的菜单 setMenu(state, val) { state.menu = val; return Cookie.setItem( "menu" , JSON.stringify(val)); }, // 动态添加路由 addMenu(state, router) { // 判断Cookie if (!Cookie.getItem( "menu" )) return ; const menu = JSON.parse(Cookie.getItem( "menu" )); state.menu = menu; const menuArray = []; // 组装路由 menu.forEach((item) => { // 判断是否有子路由 if (item.children) { item.children = item.children.map((child) => { child.component = () => import (`../../views/${child.url}`); return child; }); menuArray.push(...item.children); } else { item.component = () => import (`../../views/${item.url}`); menuArray.push(item); } }); menuArray.forEach((item) => { router.addRoute( "Main" , item); }); }, }; const actions = {}; export default { namespaced: true , state, mutations, actions, }; |
其中包括获取cookie中缓存的菜单数组,方法在utils/auth下:
1 2 3 4 5 6 7 8 | export function getMenu() { const menuStr = Cookie.getItem( "menu" ); if (menuStr) { return JSON.parse(menuStr); } else { return []; } } |
因为cookie缓存是转字符串的,因此做一层格式处理。简单的侧边栏就完成咯。
CommonHeader.vue
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 | <template> <div class = "header-container" > <div class = "l-content" > <i v- if = "isCollapse" class = "el-icon-s-unfold" @click= "handleMenu" ></i> <i v- else class = "el-icon-s-fold" @click= "handleMenu" ></i> <img class = "header-icon" src= "../assets/logo.png" alt= "" /> <h3> {{ isCollapse ? "后台" : "后台管理系统" }} </h3> <!-- 面包屑 --> <el-breadcrumb class = "app-breadcrumb" separator= "/" > <transition-group name= "breadcrumb" > <el-breadcrumb-item v- for = "(item, index) in levelList" :key= "item.path" > <span v- if = " item.redirect === 'noRedirect' || index == levelList.length - 1 " class = "no-redirect" >{{ item.meta.title }}</span > <a v- else @click.prevent= "handleLink(item)" >{{ item.meta.title }}</a> </el-breadcrumb-item> </transition-group> </el-breadcrumb> </div> <div class = "r-content" > <el-dropdown @command= "handleClick" > <span class = "el-dropdown-link" > <img class = "user" src= "../assets/logo.png" alt= "" /> </span> <el-dropdown-menu slot= "dropdown" > <el-dropdown-item>个人信息</el-dropdown-item> <el-dropdown-item command= "logout" >退出</el-dropdown-item> </el-dropdown-menu> </el-dropdown> </div> </div> </template> <script> export default { data() { return { levelList: null , }; }, watch: { $route() { this .getBreadcrumb(); }, }, created() { this .getBreadcrumb(); }, methods: { // 切换菜单伸缩 handleMenu() { this .$store.commit( "basic/CollapseMenu" ); }, // 退出登录 handleClick(command) { if (command === "logout" ) { this .$store.dispatch( "user/logout" ).then(() => { this .$router.push({ name: "/login" , }); }); } }, // 获取面包屑 getBreadcrumb() { // this.$route.matched匹配到一个路由数组 let matched = this .$route.matched.filter( (item) => item.meta && item.meta.title ); const first = matched[0]; if (! this .isHome(first)) { matched = [ { path: "/home" , meta: { title: "首页" , redirect: "/home" , path: "/home" }, }, ].concat(matched); } this .levelList = matched.filter( (item) => item.meta && item.meta.title && item.meta.breadcrumb !== false ); }, // 判断是否为主页 isHome(route) { const name = route && route.name; if (!name) { return false ; } // 对路由进行大小写处理 return name.trim().toLocaleLowerCase() === "home" .toLocaleLowerCase(); }, // 转译路由 pathCompile(path) { // 将字符串转化为正则表达式的方法 var pathToRegexp = require( "path-to-regexp" ); // 参阅:https://github.com/PanJiaChen/vue-element-admin/issues/561 const { params } = this .$route; const toPath = pathToRegexp.compile(path); return toPath(params); }, // 跳转 handleLink(item) { const { redirect, path } = item; if (redirect) { this .$router.push(redirect); return ; } this .$router.push( this .pathCompile(path)); }, }, computed: { isCollapse() { return this .$store.state.basic.isCollapse; }, }, }; </script> <style lang= "scss" scoped> .header-container { box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08); height: 60px; display: flex; justify-content: space-between; align-items: center; padding: 0 20px; .el-dropdown-link { cursor: pointer; color: #409eff; .user { width: 40px; height: 40px; border-radius: 50%; } } .header-icon { margin: 0 16px; height: 48px; } h3 { text-align: center; line-height: 48px; color: #ffffff; font-size: 18px; font-weight: 600; color: #454545; } } .l-content { display: flex; align-items: center; cursor: pointer; .el-breadcrumb { margin-left: 15px; .el-breadcrumb__item { .el-breadcrumb__inner { &.is-link { color: #666666; } } &:last-child { .el-breadcrumb__inner { color: #ffffff; } } } } } </style> |
注意头部因为有面包屑,因此需要导入path-to-regexp插件用来处理 url 中地址与参数:yarn add path-to-regexp
LayoutView.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 | <template> <el-container> <el-aside width= "auto" > </el-aside> <el-container> <el-header> <common-header /> </el-header> <!-- <common-tags /> --> <el-main> <div class = "page-container-wn" > <router-view></router-view> </div> </el-main> </el-container> </el-container> </template> <script> import CommonAside from "../components/CommonAside.vue" ; import CommonHeader from "../components/CommonHeader.vue" ; export default { data() { return {}; }, components: { CommonAside, CommonHeader, // CommonTags, }, }; </script> <style lang= "scss" scoped> .el-header { padding: 0; } </style> |
就可以对应上一篇博客的router文件夹下的index文件中的路由配置中的父级component
同时,我们需要在页面中间的内容部分做一个全局加载的功能,避免element框架loading功能的全屏加载不够友好。在utils文件夹下新建loading.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 | import Vue from "vue" ; // loading框设置局部刷新,且所有请求完成后关闭loading框 let loading; let needLoadingRequestCount = 0; // 声明一个对象用于存储请求个数 function startLoading() { loading = Vue.prototype.$loading({ lock: true , text: "努力加载中(っ•̀ω•́)っ⁾⁾" , background: "rgba(255,255,255,.4)" , target: document.querySelector( ".page-container-wn" ), // 设置加载动画区域 }); } function endLoading() { loading.close(); } export function showPageLoading(target) { if (needLoadingRequestCount === 0) { startLoading(target); } needLoadingRequestCount++; } export function hidePageLoading() { if (needLoadingRequestCount <= 0) return ; needLoadingRequestCount--; if (needLoadingRequestCount === 0) { endLoading(); } } export default { showPageLoading, hidePageLoading, }; |
然后写一个简单的动态渲染的表单组件,在components文件夹下新增CustomForm.vue,这样就可以用后端返回的表单配置json直接进行渲染啦:
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 | <template> <div class = "filterPanel" > <!--是否行内表单--> <el-form class = "form" :inline= "inline" :model= "form" :rules= "rules" ref= "form" > <!--标签显示名称--> <div class = "labelGroup" > <slot></slot> <el-form-item v- for = "item in formLabel" :key= "item.model" :label= "item.label" :prop= "item.model" > <!--根据type来显示是什么标签--> <!-- 一般输入框 --> <el-input v-model= "form[item.model]" :placeholder= "item.placeholder || '请输入' + item.label" v- if = "item.type === 'input'" > </el-input> <!-- 数字输入框 --> <el-input v-model= "form[item.model]" :min= "0" type= "number" :placeholder= "item.placeholder || '请输入' + item.label" v- if = "item.type === 'number'" > </el-input> <el-autocomplete class = "inline-input" v-model= "form[item.model]" v- if = "item.type === 'searchInput'" :fetch-suggestions= " (queryString, cb) => { searchOptionName(queryString, cb, item.opts); } " :placeholder= "item.placeholder || '请输入' + item.label" :trigger-on-focus= "false" ></el-autocomplete> <el-select v-model= "form[item.model]" :placeholder= "item.placeholder || '请选择' + item.label" v- if = "item.type === 'select'" > <!--如果是select或者checkbox 、Radio,还需要选项信息--> <el-option v- for = "item in item.opts" :key= "item.value" v-show= "item.label" :label= "item.label" :value= "item.value" ></el-option> </el-select> <!-- 开关 --> <el- switch v-model= "form[item.model]" v- if = "item.type === 'switch'" ></el- switch > <!-- 单个日期选择器 --> <el-date-picker v-model= "form[item.model]" type= "date" placeholder= "选择日期" v- if = "item.type === 'date'" value-format= "yyyy-MM-dd" > </el-date-picker> <!-- 日期范围选择器 --> <el-date-picker v-model= "form[item.model]" range-separator= "至" start-placeholder= "开始日期" end-placeholder= "结束日期" type= "datetimerange" placeholder= "选择日期" v- if = "item.type === 'dateRange'" value-format= "yyyy-MM-dd" > </el-date-picker> <!-- 日期时间选择器 --> <el-date-picker v-model= "form[item.model]" type= "datetimerange" range-separator= "至" v- if = "item.type === 'dateTimeRange'" start-placeholder= "开始日期" end-placeholder= "结束日期" value-format= "yyyy-MM-dd HH:mm:ss" > </el-date-picker> </el-form-item> </div> <div class = "btnGroup" > <el-form-item> <el-button type= "primary" @click= "search" >搜索</el-button> <el-button @click= "reset" >重置</el-button> </el-form-item> </div> </el-form> </div> </template> <script> // import Bus from "./formEventBus"; export default { name: "CustomForm" , //inline 属性可以让表单域变为行内的表单域 //form 表单数据 formLabel 是标签数据 props: { inline: { type: Boolean, default : true , }, formLabel: Array, rules: Object, }, watch: { formLabel: { handler(newVal) { if (newVal) { newVal.forEach((item) => { this .$set( this .form, item.model, item. default || "" ); }); } }, immediate: true , }, }, data() { return { form: {}, }; }, mounted() { let obj = {}; this .formLabel.forEach(async (item, index) => { if (item.optsConfig) { //获取动态下拉选项 let val = await this .getOpts(item); this .$set( this .formLabel[index], "opts" , val); obj[item.model] = val; this .$emit( "getSelect" , obj); } }); }, methods: { searchOptionName(queryString, cb, data) { var restaurants = data; var results = queryString ? restaurants.filter( this .createFilter(queryString)) : restaurants; cb(results); }, createFilter(queryString) { return (restaurant) => { return ( restaurant.value.toLowerCase().indexOf(queryString.toLowerCase()) != -1 ); }; }, reset() { this .form.pageNum = 1; this .$refs[ "form" ].resetFields(); this .$emit( "confirm" , this .form); // Bus.$emit('getParam', this.form);//给Table传查询参数 }, search() { this .form.pageNum = 1; this .$emit( "confirm" , this .form); // Bus.$emit('getParam', this.form);//给Table传查询参数 }, async getOpts(oData) { let { api, param, labelKey, valueKey } = oData.optsConfig; let opts = []; const res = await api(param); if (res.code === 1) { opts = res.data.map((item) => { if (oData.model === "goodsSku" && item[ "goodsSku" ] != "" ) { //SKU特殊处理 const itemObj = JSON.parse(item.goodsSku); return { label: itemObj[labelKey].join( "-" ), value: itemObj[valueKey], ...item, }; } else { return { label: item[labelKey], value: item[valueKey], ...item, }; } }); } return opts; }, }, }; </script> <style lang= "scss" > .filterPanel { margin-bottom: 20px; padding: 20px 20px 0 20px; .form { display: flex; justify-content: space-between; .btnGroup { min-width: 150px; display: flex; flex-direction: row; flex-wrap: nowrap; } } .el-input__inner { background-color: #fff; height: 33px; line-height: 33px; } .filterPanel { width: 100%; border-radius: 4px; background: #f7f8fa; padding-top: 20px; padding-right: 20px; } .btnContainer { margin-bottom: 20px; } .el-button { height: 32px !important; padding: 0 16px; } .el-date-editor .el-range__icon, .el-range-separator { line-height: 26px; } } </style> |
接下来整一个页面看看效果。首先在api文件夹下新增此页面的api文件,我称之为order.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 | /** * 订单管理接口列表 */ export function orderInit() { return { code: 10000, msg: "请求成功" , data: { searchList: [ { label: "" , model: "time" , type: "dateTimeRange" , }, { label: "" , placeholder: "请选择游戏" , model: "game" , type: "select" , opts: [ { label: "游戏1" , value: "game1" , }, { label: "游戏2" , value: "game2" , }, ], }, { label: "" , placeholder: "全部订单" , model: "order" , type: "select" , opts: [ { label: "订单1" , value: "order1" , }, { label: "订单2" , value: "order2" , }, ], }, { label: "" , placeholder: "订单类型" , model: "orderType" , type: "select" , opts: [ { label: "平台订单号" , value: "orderId" , }, { label: "游戏订单号" , value: "gameOrderId" , }, ], }, { label: "" , placeholder: "输入文本" , model: "text" , type: "input" , }, ], searchRules: {}, tableHeader: [ { id: "orderId" , name: "平台订单号" }, { id: "gameName" , name: "游戏名称" }, { id: "userName" , name: "用户名" }, { id: "avaName" , name: "区服/角色名" }, { id: "sum" , name: "金额" }, { id: "createTime" , name: "创建时间" }, { id: "payStatus" , name: "支付状态" }, { id: "noticeStatus" , name: "通知状态" }, ], }, }; } export function orderList(params) { console.log(params, "params" ); return { code: 10000, msg: "请求成功" , data: { tableData: [ { id: "1212121" , orderId: "xxxx" , gameName: "华夏回事路" , userName: "138xxxx2323" , avaName: "xxxxxxxxx" , sum: "648" , createTime: "2023-05-26 00:00:00" , payStatus: "成功" , noticeStatus: "成功" , }, { id: "12121221" , orderId: "xxxx" , gameName: "华夏回事路2" , userName: "138xxxx2323" , avaName: "xxxxxxxxx" , sum: "648" , createTime: "2023-05-26 00:00:00" , payStatus: "成功" , noticeStatus: "成功" , }, { id: "12121321" , orderId: "xxxx" , gameName: "华夏回3事路" , userName: "138xxxx2323" , avaName: "xxxxxxxxx" , sum: "648" , createTime: "2023-05-26 00:00:00" , payStatus: "成功" , noticeStatus: "成功" , }, ], page: { pageSize: 10, current: 1, total: 200, }, }, }; } |
当然接口是假的,接下来写页面,在views文件夹下新增order文件夹,新建index.vue文件:
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 | <template> <div> <el-card class = "box-card search-card" > <CustomForm :formLabel= "searchList" :rules= "searchRules" @confirm= "handleSearch" /> </el-card> <el-card class = "box-card" > <el-table :data= "tableData" border v-loading= "tableLoading" style= "width: 100%; margin-bottom: 16px" > <el-table-column v- for = "item in tableHeader || []" :key= "item.id" :prop= "item.id" :label= "item.name" /> <el-table-column fixed= "right" label= "操作" width= "120" > <template slot-scope= "scope" > <el-button @click= "handleDetail(scope.row)" type= "text" >详情</el-button > <el-button type= "text" disabled>补单</el-button> </template> </el-table-column> </el-table> <el-pagination @size-change= "handleSizeChange" @current-change= "handleCurrentChange" :current-page= "tablePage.current || 1" :page-size= "tablePage.current || 10" layout= "total, sizes, prev, pager, next" :total= "tablePage.total || total" > <!-- :page-sizes= "[10, 20, 50, 100]" --> </el-pagination> </el-card> <DataDetail :rowInfo= "rowInfo" :visible= "drawerVisible" @updateVisible= "updateVisible" /> </div> </template> <script> import CustomForm from "@/components/CustomForm.vue" ; import DataDetail from "./DataDetail" ; import { orderInit, orderList } from "@/api/order" ; import { showPageLoading, hidePageLoading } from "@/utils/loading" ; export default { name: "OrderView" , data() { return { formInline: { user: "" , region: "" , }, tableHeader: [], tableData: [], searchList: [], searchRules: {}, tablePage: { pageSize: 10, current: 1, total: 400, }, searchData: {}, rowInfo: {}, drawerVisible: false , tableLoading: false , }; }, mounted() { this .init(); this .handleSearch(); }, methods: { // 初始化 async init() { try { showPageLoading(); // 开启 const res = await orderInit(); if (res.code !== 10000) this .$message.error(res.msg); if (res.code === 10000 && res.data) { this .searchList = res.data.searchList; this .searchRules = res.data.searchRules; this .tableHeader = res.data.tableHeader; } } finally { setTimeout(() => { hidePageLoading(); }, 2000); } }, // 查询列表数据 async handleSearch(data) { try { const tableParams = { ... this .tablePage, ...(data || {}) }; this .searchData = { ...tableParams }; this .tableLoading = true ; const res = await orderList(tableParams); if (res.code !== 10000) this .$message.error(res.msg); if (res.code === 10000 && res.data) { this .tableData = res.data.tableData; this .tablePage = { ...res.data.page }; } } finally { this .tableLoading = false ; } }, // 查看详情 handleDetail(data) { this .rowInfo = data; this .drawerVisible = true ; }, // 切换每页展示条数 handleSizeChange(val) { this .tablePage = { ... this .tablePage, pageSize: val }; this .handleSearch({ ... this .searchData, ... this .tablePage, pageSize: val, }); }, // 切换页码 handleCurrentChange(val) { this .tablePage = { ... this .tablePage, current: val }; this .handleSearch({ ... this .searchData, ... this .tablePage, current: val, }); }, updateVisible() { this .drawerVisible = ! this .drawerVisible; }, }, components: { CustomForm, DataDetail, }, }; </script> <style lang= "scss" scoped> .search-card { margin-bottom: 16px; } </style> |
可以看到引入了@/utils/loading中导出的两个方法,并在初始化接口中进行了使用(写了个2秒的setTimeOut看效果)
然后新建详情抽屉组件,依旧在order文件夹下,新建DataDetail.vue:
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 | <template> <el-drawer :visible.sync= "showDrawer" size= "45%" title= "详情" > <!-- @tab-click= "handleCheckTab" --> <el-tabs v-model= "activeName" style= "margin: 24px" > <el-tab-pane label= "订单详情" name= "first" > <el-descriptions :column= "2" :labelStyle= "{ fontWeight: 'bold' }" > <el-descriptions-item v- for = "item in data.first || []" :key= "item.key" :label= "item.label" >{{ item.value }}</el-descriptions-item > </el-descriptions> </el-tab-pane> <el-tab-pane label= "支付回调" name= "second" > <el-table :data= "data.second?.tableData || []" border style= "width: 100%; margin-bottom: 16px" > <el-table-column v- for = "item in data.second?.tableHeader || []" :key= "item.id" :prop= "item.id" :label= "item.name" > </el-table-column> </el-table> </el-tab-pane> <el-tab-pane label= "通知游戏" name= "third" > <el-table :data= "data.third?.tableData || []" border style= "width: 100%; margin-bottom: 16px" > <el-table-column v- for = "item in data.third?.tableHeader || []" :key= "item.id" :prop= "item.id" :label= "item.name" > </el-table-column> </el-table> </el-tab-pane> </el-tabs> </el-drawer> </template> <script> export default { name: "DataDetail" , props: { visible: { type: Boolean, default : false , }, rowInfo: Object, }, data() { return { activeName: "first" , data: {}, }; }, computed: { showDrawer: { get() { return this .visible; }, set(val) { this .$emit( "updateVisible" , val); }, }, }, mounted() { this .data = { first: [ { key: "orderId" , label: "平台订单号" , value: "xxxxx" }, { key: "orderId2" , label: "支付消息" , value: "xxxxx" }, { key: "orderId3" , label: "支付方式" , value: "支付宝" }, { key: "orderId4" , label: "游戏名称" , value: "xxxxx" }, { key: "orderId5" , label: "支付渠道单号" , value: "xxxxxxxxxxxxxxx" }, { key: "orderId6" , label: "订单金额" , value: "648.00" }, { key: "orderId7" , label: "支付状态" , value: "成功" }, { key: "orderId8" , label: "通知游戏状态" , value: "成功" }, { key: "orderId9" , label: "游戏区服" , value: "xxx1服" }, { key: "orderId10" , label: "游戏角色名" , value: "ABB" }, { key: "orderId11" , label: "游戏订单号" , value: "xxxxxxxxxx" }, { key: "orderId12" , label: "商品名称" , value: "648礼包" }, { key: "orderId13" , label: "回调地址" , value: "https://xxxx.ccc.com" }, ], second: { tableHeader: [ { id: "xxTime" , name: "回调时间" }, { id: "xxStatus" , name: "回调状态" }, ], tableData: [ { id: "12121212" , xxTime: "2022-05-21 23-33" , xxStatus: "成功" }, { id: "12134321212" , xxTime: "2022-05-21 13-33" , xxStatus: "失败" }, { id: "1212221212" , xxTime: "2022-05-21 23-23" , xxStatus: "成功" }, ], }, third: { tableHeader: [ { id: "xxTime" , name: "通知时间" }, { id: "xxStatus" , name: "通知状态" }, ], tableData: [ { id: "12121212" , xxTime: "2022-05-21 23-33" , xxStatus: "成功" }, { id: "12121212" , xxTime: "2022-05-21 13-33" , xxStatus: "失败" }, { id: "12121212" , xxTime: "2022-05-21 23-23" , xxStatus: "成功" }, ], }, }; }, methods: { // handleCheckTab(val) { // console.log(val); // }, }, }; </script> |
看看效果:
查看详情:
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具