办公系统前端开发
本系统采用vue框架进行搭建,主要ui是Elementui,主要功能实现了一个公司的员工资料、人事管理、薪资管理、统计管理、系统管理等功能。
1登陆页面设计
如图所示,使用Elementui进行简单搭建,验证码由后端提供,主要代码如下,
本代码主要运用了elemenui的Loading加载、form验证表单
以及附属的相关事件,样式由后期调整得。
事件的主要功能:将axios从后端请求得到的token数据存入window.
SessionStorage.setItem()中,为了使axios做下一次请求时获取token认证,
登陆即完成了
<template> <div> <el-form v-loading="loading" element-loading-text="正在登录..." element-loading-spinner="el-icon-loading" element-loading-background="rgba(0, 0, 0, 0.8)" :rules="rules" ref="loginForm" :model="loginForm" class="loginContainer"> <h3 class="loginTitle">系统登陆</h3> <el-form-item prop="username"> <el-input type="text" v-model="loginForm.username" auto-complete="false" placeholder="请输入用户名"></el-input> </el-form-item> <el-form-item prop="password"> <el-input type="password" v-model="loginForm.password" auto-complete="false" placeholder="请输入密码"></el-input> </el-form-item> <el-form-item prop="code"> <el-input style="width:200px ; margin-right: 5px" type="text" v-model="loginForm.code" auto-complete="false" placeholder="点击更换图片"> </el-input> <img :src="captchaUrl" @click="updateCaptcha" class="el-form-item__content"> </el-form-item> <el-checkbox v-model="checked" class="loginRemember">记住我</el-checkbox> <el-button type="primary" style="width: 100%" @click="submitLogin">登录</el-button> </el-form> </div> </template> <script> export default { name: "Login", data(){ return{ captchaUrl:'/captcha?time='+new Date(), loginForm:{ username:'admin', password:'123', code:'' }, checked:true, loading:false, rules:{ username: [{required:true,message:'请输入用户名',trigger:'blur'}], password: [{required:true,message: '请输入密码',trigger:'blur'}], code: [{required:true,message:'请输入验证码',trigger:'blur'}] } } }, methods: { updateCaptcha(){ this.captchaUrl='/captcha?time='+new Date(); }, submitLogin() { this.$refs.loginForm.validate((valid) =>{ if (valid) { this.loading=true; this.postRequest('/login',this.loginForm).then(resp=>{ if (resp){ this.loading=false; const tokenStr=resp.obj.tokenHead+resp.obj.token; //存进去 跳转页面后获取 window.sessionStorage.setItem('tokenStr',tokenStr); //跳转 /* this.$router.replace('/home');*/ /*此处解决自己输入属地址的行为*/ let path = this.$route.query.redirect; this.$router.replace((path=='/'||path==undefined)?'/home' : path); } this.loading=false; }); } else { this.$message.error('请填写完整!!!'); return false; } }); } } } </script> <style> .el-form-item__content{ display: flex; align-items: center } .loginContainer{ border-radius: 15px; background-clip: padding-box;/*背景裁剪到内边框*/ margin: 180px auto; width: 350px; padding: 15px 35px 15px 35px;/*拉大间隔*/ background: #fff; border: 1px solid #eaeaea; } .loginTitle{ margin: 0px auto 40px auto; text-align: center; } .loginRemember{ text-align: left; margin: 0px 0px 15px 0px; } </style>
2主页设计
如图所示,主要将上面所属的系统功能一一展示在左上角,主要运用的是
Elementui的container容器布局第四个,展示的数据菜单由路由模式从
后端获取,实现自动展示相应的菜单,代码如下。
<template> <div> <el-container> <el-header class="homeHeader"> <div class="title"> 云e办</div> <!--下拉菜单 指令事件--> <el-dropdown class="userInfo" @command="commandHandler"> <span class="el-dropdown-link"> {{user.name}}<i><img :src="user.userFace"/></i> </span> <el-dropdown-menu slot="dropdown"> <el-dropdown-item command="userinf">个人中心</el-dropdown-item> <el-dropdown-item command="setting">设置</el-dropdown-item> <el-dropdown-item command="logout">注销登录</el-dropdown-item> </el-dropdown-menu> </el-dropdown> </el-header> <el-container> <el-aside width="200px"> <el-menu router unique-opened> <el-submenu :index="index+''" v-for="(item,index) in routes" :key="index" v-if="!item.hidden"> <template slot="title"> <i :class="item.iconCls" style="color: darkcyan;margin-right: 5px"></i> <span>{{item.name}}</span> </template> <el-menu-item v-for="(children,index) in item.children" :index="children.path"> {{children.name}} </el-menu-item> </el-submenu> </el-menu> </el-aside> <el-main> <el-breadcrumb v-if="this.$router.currentRoute.path !='/home'" separator-class="el-icon-arrow-right"> <el-breadcrumb-item :to="{ path: '/home' }">首页</el-breadcrumb-item> <el-breadcrumb-item>{{this.$router.currentRoute.name}}</el-breadcrumb-item> </el-breadcrumb> <div class="homeWelcome" v-if="this.$router.currentRoute.path=='/home'"> 欢迎来到云e办系统 </div> <router-view class="homeRouterView" /> </el-main> </el-container> </el-container> </div> </template> <script> export default { name: "Home", computed:{ routes(){ return this.$store.state.routes; } }, data(){ return{ user:JSON.parse(window.sessionStorage.getItem('user')) } }, methods:{ commandHandler(command){ if (command=='logout'){ { this.$confirm('此操作将注销登录, 是否继续?', '提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }).then(() => { this.postRequest('/logout'); window.sessionStorage.removeItem('tokenStr'); window.sessionStorage.removeItem('user'); //清空菜单 this.$store.commit('initRoutes',[]); this.$router.replace('/'); }).catch(() => { this.$message({ type: 'info', message: '已取消注销' }); }); } } } } } </script> <style> .homeHeader{ background: #409eff; display: flex;/*文本生效*/ align-items: center; justify-content: space-between;/*将最后元素末尾对齐*/ padding: 0 15px; box-sizing: border-box; } .homeHeader .title{ font-size: 30px; font-family: 华文楷体; color: white; } .homeHeader .userInfo{ cursor:pointer; } .el-dropdown-link img{ width: 48px; height: 48px; border-radius: 24px; } .homeWelcome{ text-align: center; font-size: 30px; font-family: 华文楷体; color: #409eff; padding-top: 50px; } .homeRouterView{ margin-top: 10px; } </style>
3Vuex及路由存储对象
安装vuex后,使用vuex构建存储对象,代码如下
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex);
export default new Vuex.Store({
state:{
routes:[]
},
//同步
mutations:{
initRoutes(state,data){
state.routes=data;
}
},
//异步
actions:{
}
})
4菜单初始化工具类
因为我们刚刚使用vuex构建了路由储存对象,需要使用菜单初始化工具将菜单存入routes[],
具体工具类代码如下
import {getRequest} from './api' export const initMenu = (router,store)=>{ if(store.state.routes.length>0){ return; } getRequest('/system/cfg/menu').then(data=>{ if (data){ let fmtRoutes = formatRoutes(data); router.addRoutes(fmtRoutes); store.commit('initRoutes',fmtRoutes);/*核心*/ } }) } export const formatRoutes = (routes)=>{ let fmtRoutes=[]; routes.forEach(router=>{ let{ path, component, name, iconCls, children, }= router; if (children&&children instanceof Array){ //递归 children=formatRoutes(children); } let fmRouter = { path:path, name, iconCls, children, component(resolve){ if (component.startsWith('Home')){ require(['../views/'+component+'.vue'],resolve); }else if (component.startsWith('Emp')){ require(['../views/emp/'+component+'.vue'],resolve); }else if(component.startsWith('Per')){ require(['../views/per/'+component+'.vue'],resolve); }else if(component.startsWith('Sal')){ require(['../views/sal/'+component+'.vue'],resolve); }else if(component.startsWith('Sta')){ require(['../views/sta/'+component+'.vue'],resolve); }else if(component.startsWith('Sys')){ require(['../views/sys/'+component+'.vue'],resolve); } } } fmtRoutes.push(fmRouter); }); return fmtRoutes; }
5请求响应处理工具类
此工具类主要是过滤非法请求以及处理响应时发生的错误,将他们的信息用弹窗显示出来如图所示,具体代码如下
import axios from 'axios' import {Message} from "element-ui"; //请求拦截器 axios.interceptors.request.use(config=>{ if (window.sessionStorage.getItem('tokenStr')){ //请求时自动带入token config.headers['Authorization'] = window.sessionStorage.getItem('tokenStr') } return config; },error => { console.log(error); }) axios.interceptors.response.use(success=>{ //业务逻辑错误 if (success.status||success.data.code==200){ if (success.data.code==500||success.data.code==401||success.data.code==403){ Message.error({message:success.data.message}); return; } if (success.data.message){ Message.success({message:success.data.message}) } } return success.data; },error=>{ //未能调取接口错误 if (error.response.data.code==504||error.response.data.code==404){ Message.error({message:'服务器被吃了'}) }else if(error.response.data.code==403){ Message.error({message:'权限不足'}) }else if(error.response.data.code==401){ Message.error({message:'尚未登陆'}) router.replace('/') } else { if (error.response.data.message) { Message.error({message: error.response.data.message}); } else { Message.error({message: '未知错误'}) } } return; }); let base=''; //传送json请求格式的post请求 export const postRequest=(url,params)=>{ return axios({ method:'post', url:`${base}${url}`, data:params }) } export const putRequest=(url,params)=>{ return axios({ method:'put', url:`${base}${url}`, data:params }) } export const getRequest=(url,params)=>{ return axios({ method:'get', url:`${base}${url}`, data:params }) } export const deleteRequest=(url,params)=>{ return axios({ method:'delete', url:`${base}${url}`, data:params }) }
6暴露路由
使用路由模式菜单,需要将路由包装暴露出来,以便公共使用
import Vue from 'vue' import VueRouter from 'vue-router' import Login from '../views/Login.vue' Vue.use(VueRouter) const routes = [ { path: '/', name: 'Login', component: Login, hidden:true }, ] const router = new VueRouter({ routes }) export default router
7创建菜单组件
我们需要将菜单组件分组创建好,然后通过工具类初始化存入路由数组,以便实现点击跳转,代码后面发布
8创建系统管理SysBasic的子组件及基础信息设置代码
基础信息设置 代码,采用了elementui的Tabs标签效果,内容名字是它的子组件
<template> <div> <el-tabs v-model="activeName" type="card"> <el-tab-pane label="部门管理" name="DepMana"><DepMana></DepMana></el-tab-pane> <el-tab-pane label="职位管理" name="PosMana"><PosMana/></el-tab-pane> <el-tab-pane label="职称管理" name="JoblevelMana"><JoblevelMana/></el-tab-pane> <el-tab-pane label="奖惩规则" name="EcMana"><EcMana/></el-tab-pane> <el-tab-pane label="权限组" name="PermissMana"><PermissMana/></el-tab-pane> </el-tabs> </div> </template> <script> import DepMana from '../../components/sys/basic/DepMana' import EcMana from "../../components/sys/basic/EcMana"; import JoblevelMana from "../../components/sys/basic/JoblevelMana"; import PermissMana from "../../components/sys/basic/PermissMana"; import PosMana from "../../components/sys/basic/PosMana"; export default { name: "SysBasic", data(){ return{ activeName:'JoblevelMana' /*默认开启PosMana*/ } }, components:{ DepMana, EcMana, JoblevelMana, PermissMana, PosMana } } </script> <style scoped> </style>
9基础信息设置之职位管理
采用Elementui的input输入框、Table表格多选,含有添加、编辑、删除、批量删除事件
<template> <div> <div> <el-input suffix-icon="el-icon-plus" class="addPosInput" size="small" placeholder="添加职位" v-model="pos.name" @keydown.enter.native="addPosition" > </el-input> <el-button type="primary" icon="el-icon-plus" size="small" @click="addPosition">添加</el-button> </div> <div> <el-table border size="small" stripe :data="positions" @selection-change="handleSelectionChange" style="width: 70%"> <el-table-column type="selection" width="55"> </el-table-column> <el-table-column prop="id" label="编号" width="55"> </el-table-column> <el-table-column prop="name" label="职位" width="120"> </el-table-column> <el-table-column prop="createDate" label="创建时间" width="200"> </el-table-column> <el-table-column prop="enabled" label="是否启用" width="120"> </el-table-column> <el-table-column label="操作"> <template scope="scope"> <el-button size="small" @click="showEditView(scope.$index, scope.row)">编辑</el-button> <el-button size="small" type="danger" @click="handleDelete(scope.$index, scope.row)">删除</el-button> </template> </el-table-column> </el-table> </div> <el-button @click="deleteMany" type="danger" size="small" style="margin-top: 8px" :disabled="this.multipleSelection.length==8">批量删除</el-button> <el-dialog title="编辑职位" :visible.sync="dialogVisible" size="tiny" > <div> <el-tag>职位名称</el-tag> <el-input v-model="updatePos.name" class="updatePosInput"></el-input> </div> <span slot="footer" class="dialog-footer"> <el-button @click="dialogVisible = false" size="small">取 消</el-button> <el-button type="primary" @click="doUpdate" size="small">确 定</el-button> </span> </el-dialog> </div> </template> <script> export default { name: "PosMana", data(){ return{ pos:{ name:'' }, positions:[], dialogVisible:false, updatePos:{ name:"" }, multipleSelection:[] } }, mounted() { this.initPositions(); }, methods:{ initPositions(){ this.getRequest('/system/basic/pos/').then(resp=>{ if (resp){ this.positions = resp; } }) }, doUpdate(){ this.putRequest('/system/basic/pos/',this.updatePos).then(resp=>{ if (resp){ this.initPositions(); this.dialogVisible=false; } }) }, addPosition(){ if (this.pos.name){ this.postRequest('/system/basic/pos/',this.pos).then(resp=>{ if (resp){ this.initPositions(); } }) }else{ this.$message.error('职位名称不能为空!'); } }, showEditView(index, date) { Object.assign(this.updatePos,date);/*date拷贝给updatePos*/ this.updatePos.createDate=""; this.dialogVisible=true; }, handleDelete(index, data) { this.$confirm('此操作将永久删除['+data.name+'], 是否继续?', '提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }).then(() => { this.deleteRequest('/system/basic/pos/'+data.id).then(resp=>{ if (resp){ this.initPositions(); } }); }).catch(() => { this.$message({ type: 'info', message: '已取消删除' }); }); }, handleSelectionChange(val){ this.multipleSelection=val; }, deleteMany(){ this.$confirm('此操作将永久删除['+this.multipleSelection.length+']条职位, 是否继续?', '提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }).then(() => { let ids ='?'; this.multipleSelection.forEach(item=>{ ids += 'ids=' + item.id+'&'; }); this.deleteRequest('/system/basic/pos/'+ids).then(resp=>{ if (resp){ this.initPositions(); } }); }).catch(() => { this.$message({ type: 'info', message: '已取消删除' }); }); } } } </script> <style> .addPosInput{ width:300px; margin-right: 8px; } /*.posMainMain{ margin-top: 10px; }*/ .updatePosInput{ width: 200px; margin-left: 8px; } </style>
10基础信息设置之权限组
权限组 代码,采用了elementui的折叠面板手风琴模式,Tree树形控件
<template> <div> <div class="PermissManaTool"> <el-input size="small" placeholder="请输入角色英文名" v-model="role.name"> <template slot="prepend">ROLE_</template> </el-input> <el-input size="small" placeholder="请输入角色中文名" v-model="role.nameZh"> </el-input> <el-button size="small" type="primary" icon="el-icon-plus" @click="doAddRole">添加角色</el-button> </div> <div class="permissManaMain"> <!--折叠面板 手风琴模式 change自带事件会传:name的值 通过 accordion 属性来设置是否以手风琴模式显示。--> <el-collapse v-model="activeName" accordion @change="change"> <el-collapse-item :title="r.nameZh" :name='r.id' v-for="(r,index) in roles" :key="index"> <!--卡片--> <el-card class="box-card"> <div slot="header" class="clearfix"> <span>可访问资源</span> <!--删除按钮--> <el-button style="float: right; padding: 3px 0;color: red" type="text" icon="el-icon-delete" @click="doDeleteRole(r)" /> </div> <div> <!--树形控件 defaultProps展示子类和名字 node-key="id"事件通过id获取数据--> <el-tree :data="allMenus" ref="tree" :key="index" :props="defaultProps" show-checkbox :default-checked-keys="selectedMenus" node-key="id" ></el-tree> <!-- 样式 容器布局 向右对齐--> <div style="display: flex; justify-content: flex-end"> <el-button size="mini">取消修改</el-button> <el-button size="mini" type="primary" @click="doUpdate(r.id,index)">确认修改</el-button> <!--primary为主要按钮样式--> </div> </div> </el-card> </el-collapse-item> </el-collapse> </div> </div> </template> <script> export default { name: "PermissMana", data(){ return{ role:{ name:'', nameZh:'' }, roles:[], allMenus:[], defaultProps: { children: 'children', label: 'name' }, selectedMenus:[], activeName:-1 } }, mounted(){ this.initRoles(); }, methods:{ doAddRole(){ if (this.role.name && this.role.nameZh){ this.postRequest('/system/basic/permiss/role',this.role).then(resp=>{ if (resp){ this.initRoles(); this.role.name=''; this.role.nameZh=''; } }); }else{ this.$message.error('所有字段不能为空!'); } }, doUpdate(rid,index){ let tree = this.$refs.tree[index]; let selectedKeys = tree.getCheckedKeys(true);/*传true只打印叶子节点*/ let url='/system/basic/permiss/role?rid='+rid; let mids =''; selectedKeys.forEach(key=>{ mids += '&mids='+key; }); url+=mids; this.putRequest(url).then(resp=>{ if (resp){ this.initRoles(); this.activeName=-1; } }); }, doDeleteRole(role){ this.$confirm('此操作将永久删除['+role.nameZh+'], 是否继续?', '提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }).then(() => { this.deleteRequest('/system/basic/permiss/role/'+role.id).then(resp=>{ if (resp){ this.initRoles(); } }); }).catch(() => { this.$message({ type: 'info', message: '已取消删除' }); }); }, initSelectedMenus(rid){ this.getRequest('/system/basic/permiss/mid/'+rid).then(resp=>{ if (resp){ this.selectedMenus=resp; } }); }, change(rid){ if (rid){ this.initAllMenus(); this.initSelectedMenus(rid); this.shuaxin(rid); } }, shuaxin(rid){ this.initAllMenus(); this.initSelectedMenus(rid); }, initAllMenus(){ this.getRequest('/system/basic/permiss/menus').then(resp=>{ if (resp){ this.allMenus=resp; } }); }, initRoles(){ this.getRequest('/system/basic/permiss/').then(resp=>{ if (resp){ this.roles=resp; } }); } } } </script> <style > .PermissManaTool{ display: flex;/*使用容器布局 下面左对齐生效*/ justify-content: flex-start;/*左对齐*/ } .PermissManaTool .el-input{ width: 300px; margin-right: 6px; } .permissManaMain{ margin-top: 10px; width: 700px; } </style>