先在components下分别创建侧边栏、顶部、布局等组件,用于全局配置:
CommonAside.vue
<template> <el-menu default-active="1-4-1" class="el-menu-vertical" @open="handleOpen" @close="handleClose" :collapse="isCollapse" background-color="#f5f5f5" text-color="#555555" active-text-color="#409eff" > <el-menu-item @click="clickItem(item)" v-for="item in noChildren" :key="item.name" :index="item.name" > <!-- 这里是字体图标,用模板字符串拼接,注意要动态绑定 --> <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.label" > <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.name">{{ subItem.label }}</el-menu-item> </el-menu-item-group> </el-submenu> </el-menu> </template> <style scoped lang="scss"> .el-menu-vertical:not(.el-menu--collapse) { width: 200px; min-height: 400px; } .el-menu { height: 100vh; border-right: none; } </style> <script> 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: { 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; }, // 获取菜单 MenuData() { return this.$store.state.basic.menu; }, }, }; </script>
CommonHeader.vue
<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
<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文件:
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直接进行渲染啦:
<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:
/** * 订单管理接口列表 */ 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文件:
<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:
<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>
看看效果:
查看详情: