九、员工管理
1. 页面设计
<template>
<div>
<div style="display: flex; justify-content: space-between">
<div>
<el-input
placeholder="请输入员工名进行搜索"
style="width: 300px; margin-right: 10px"
size="small"
></el-input>
<el-button type="primary" icon="el-icon-search" size="small">
搜索
</el-button>
<el-button type="primary" size="small">
<i class="fa fa-angle-double-down"></i>
高级搜索
</el-button>
</div>
<div>
<el-button type="success" size="small">
<i class="fa fa-level-up"></i>
导入数据
</el-button>
<el-button type="success" size="small">
<i class="fa fa-level-down"></i>
导出数据
</el-button>
<el-button type="primary" icon="el-icon-plus" size="small"
>添加员工</el-button
>
</div>
</div>
<div style="margin-top: 10px;">
<el-table :data="emps" stripe border style="width: 100%"
v-loading="loading" element-loading-text="拼命加载中"
element-loading-spinner="el-icon-loading"
element-loading-background="rgba(f, f, f, 0.8)">
<el-table-column type="selection" width="55"> </el-table-column>
<el-table-column prop="name" label="姓名" align="left" width="90" fixed="left">
</el-table-column>
<el-table-column prop="workId" label="工号" align="left" width="90">
</el-table-column>
<el-table-column prop="gender" label="性别" align="left" width="50">
</el-table-column>
<el-table-column prop="birthday" label="生日日期" align="left" width="100">
</el-table-column>
<el-table-column prop="idCard" label="身份证号码" align="left" width="160">
</el-table-column>
<el-table-column prop="wedlock" label="婚姻状态" align="left" width="85">
</el-table-column>
<el-table-column prop="nation.name" label="民族" align="left" width="80">
</el-table-column>
<el-table-column prop="nativePlace" label="籍贯" align="left" width="80">
</el-table-column>
<el-table-column prop="politicsStatus.name" label="政治面貌" align="left" width="75">
</el-table-column>
<el-table-column prop="email" label="电子邮件" align="left" width="180">
</el-table-column>
<el-table-column prop="phone" label="电话号码" align="left" width="110">
</el-table-column>
<el-table-column prop="address" label="联系地址" align="left" width="230">
</el-table-column>
<el-table-column prop="department.name" label="所属部门" align="left" width="100">
</el-table-column>
<el-table-column prop="joblevel.name" label="职称" width="50">
</el-table-column>
<el-table-column prop="position.name" label="职位" width="100">
</el-table-column>
<el-table-column prop="engageForm" label="聘用形式" align="left" width="100">
</el-table-column>
<el-table-column prop="tiptopDegree" label="最高学历" align="left" width="80">
</el-table-column>
<el-table-column prop="school" label="毕业院校" align="left" width="150">
</el-table-column>
<el-table-column prop="specialty" label="专业" align="left" width="150">
</el-table-column>
<el-table-column prop="workState" label="在职状态" align="left" width="80">
</el-table-column>
<el-table-column prop="beginDate" label="入职日期" align="left" width="100">
</el-table-column>
<el-table-column prop="conversionTime" label="转正日期" align="left" width="100">
</el-table-column>
<el-table-column prop="beginContract" label="合同起始日期" align="left" width="100">
</el-table-column>
<el-table-column prop="endContract" label="合同截止日期" align="left" width="100">
</el-table-column>
<el-table-column label="合同期限" align="left" width="100">
<template slot-scope="scope">
<el-tag>{{scope.row.contractTerm}}</el-tag>年
</template>
</el-table-column>
<el-table-column label="操作" width="200" fixed="right">
<template slot-scope="scope">
<el-button style="padding: 3px;" size="mini" @click="edit(scope.row)">编辑</el-button>
<el-button style="padding: 3px;" size="mini">查看高级资料</el-button>
<el-button style="padding: 3px;" size="mini" type="danger">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
</div>
</template>
<script>
export default {
name: "EmpBasic",
data() {
return {
emps: [],
loading: false
};
},
methods: {
initEmps() {
this.loading = true;
this.getRequest("/employee/basic/").then((resp) => {
if (resp) {
this.loading = false;
this.emps = resp.obj.data;
}
});
},
},
mounted() {
this.initEmps();
},
};
</script>
<style scoped>
</style>
2. 实现分页
<template>
<div>
<div style="display: flex; justify-content: space-between">
...
</div>
<div style="margin-top: 10px;">
...
<div style="display: flex; justify-content: flex-end; margin-top: 10px;">
<el-pagination
background :total="total"
layout="sizes, prev, pager, next, jumper, ->, total"
@size-change="sizeChange" @current-change="currentChange">
</el-pagination>
</div>
</div>
</div>
</template>
<script>
export default {
name: "EmpBasic",
data() {
return {
...
total: 0,
page: 1,
size: 10
};
},
methods: {
...
// 页面改变
currentChange(currentPage){
this.page = currentPage;
this.initEmps();
},
sizeChange(size){
this.size = size;
this.initEmps();
}
},
mounted() {
this.initEmps();
},
};
</script>
...
3. 常规搜索
<template>
<div>
<div style="display: flex; justify-content: space-between">
<div>
<el-input
placeholder="请输入员工名进行搜索"
style="width: 300px; margin-right: 10px" size="small"
v-model="empName" @keydown.enter.native="initEmps"
clearable @clear="initEmps"
></el-input>
<el-button type="primary" icon="el-icon-search" size="small" @click="initEmps">
搜索
</el-button>
...
</div>
...
</div>
...
</div>
</template>
<script>
export default {
name: "EmpBasic",
data() {
return {
...
empName: '' // 模糊搜索的name
};
},
methods: {
initEmps() {
this.loading = true;
this.getRequest("/employee/basic/?currentPage="+this.page + '&size='+this.size
+ '&name='+this.empName).then((resp) => {
this.loading = false;
if (resp) {
this.emps = resp.obj.data;
this.total = resp.obj.total;
}
});
},
}
...
};
</script>
...
4. 添加员工界面
<el-dialog
title="添加员工"
:visible.sync="dialogVisible"
width="80%">
<div>
<el-form ref="empForm" :model="emp">
<el-row>
<el-col :span="6">
<el-form-item label="姓名:" prop="name">
<el-input v-model="emp.name" placeholder="请输入员工姓名" size="mini" style="width: 150px;"></el-input>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="性别:" prop="gender">
<el-radio-group v-model="emp.gender" style="margin-top: 13px;">
<el-radio label="男">男</el-radio>
<el-radio label="女">女</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="出生日期:" prop="birthday">
<el-date-picker
v-model="emp.birthday"
type="date"
size="mini"
style="width: 150px"
value-format="yyyy-MM-dd"
placeholder="出生日期"
></el-date-picker>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="政治面貌:" prop="politicId">
<el-select v-model="emp.politicId" size="mini" style="width: 200px" placeholder="政治面貌">
<el-option v-for="item in options" :key="item" :label="item.label" :value="item.value"></el-option>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="6">
<el-form-item label="民族:" prop="nationId">
<el-select v-model="emp.nationId" size="mini" style="width: 150px;" placeholder="民族">
<el-option v-for="item in options" :key="item" :label="item.label" :value="item.value"></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="籍贯:" prop="nativePlace">
<el-input v-model="emp.nativePlace" placeholder="请输入籍贯" size="mini" style="width: 150px;"></el-input>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="电子邮箱:" prop="email">
<el-input v-model="emp.email" placeholder="请输入电子邮箱" size="mini" style="width: 150px;"></el-input>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="联系地址:" prop="address">
<el-input v-model="emp.address" placeholder="请输入联系地址" size="mini" style="width: 200px;"></el-input>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="6">
<el-form-item label="职位:" prop="position">
<el-select v-model="emp.position" size="mini" style="width: 150px;" placeholder="职位">
<el-option v-for="item in options" :key="item" :label="item.label" :value="item.value"></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="职称:" prop="joblevel">
<el-select v-model="emp.joblevel" size="mini" style="width: 150px;" placeholder="职称">
<el-option v-for="item in options" :key="item" :label="item.label" :value="item.value"></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="所属部门:" prop="department">
<el-input v-model="emp.department" placeholder="请输入联系地址" size="mini" style="width: 150px;"></el-input>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="电话号码:" prop="phone">
<el-input v-model="emp.phone" placeholder="请输入电话号码" size="mini" style="width: 200px;"></el-input>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="6">
<el-form-item label="工号:" prop="workId">
<el-input v-model="emp.workId" placeholder="请输入工号" size="mini" style="width: 150px;"></el-input>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="学历:" prop="tiptopDegree">
<el-select v-model="emp.tiptopDegree" size="mini" style="width: 150px;" placeholder="学历">
<el-option v-for="item in options" :key="item" :label="item.label" :value="item.value"></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="毕业院校:" prop="school">
<el-input v-model="emp.school" placeholder="请输入学校" size="mini" style="width: 150px;"></el-input>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="专业名称:" prop="specialty">
<el-input v-model="emp.specialty" placeholder="请输入专业名称" size="mini" style="width: 200px;"></el-input>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="6">
<el-form-item label="入职日期:" prop="contractTerm">
<el-date-picker
v-model="emp.contractTerm"
type="date"
size="mini"
style="width: 120px"
value-format="yyyy-MM-dd"
placeholder="入职日期"
></el-date-picker>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="转正日期:" prop="conversionTime">
<el-date-picker
v-model="emp.conversionTime"
type="date"
size="mini"
style="width: 120px"
value-format="yyyy-MM-dd"
placeholder="转正日期"
></el-date-picker>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="合同起始日期:" prop="beginContract">
<el-date-picker
v-model="emp.beginContract"
type="date"
size="mini"
style="width: 120px"
value-format="yyyy-MM-dd"
placeholder="合同起始日期"
></el-date-picker>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="合同截止日期:" prop="endContract">
<el-date-picker
v-model="emp.endContract"
type="date"
size="mini"
style="width: 170px"
value-format="yyyy-MM-dd"
placeholder="合同截止日期"
></el-date-picker>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="8">
<el-form-item label="身份证号码:" prop="idCard">
<el-input v-model="emp.idCard" placeholder="请输入身份证号码" size="mini" style="width: 200px;"></el-input>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="聘用形式:" prop="engageForm">
<el-radio-group v-model="emp.engageForm" style="margin-top: 13px;">
<el-radio label="劳动合同">劳动合同</el-radio>
<el-radio label="劳务合同">劳务合同</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="婚姻状况:" prop="wedlock">
<el-radio-group v-model="emp.wedlock" style="margin-top: 13px;">
<el-radio label="已婚">已婚</el-radio>
<el-radio label="未婚">未婚</el-radio>
<el-radio label="离异">离异</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
</el-row>
</el-form>
</div>
<span slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false">取 消</el-button>
<el-button type="primary" @click="dialogVisible = false">确 定</el-button>
</span>
</el-dialog>
5. 下拉框数据处理
思路分析:因为大部分下拉框的数据都是变化不大的数据,一般不会发生改变;这里的民族、政治面貌、职称都存在sessionStorage中,也可以像不变的学历直接存在data中;工号则直接存入这个即将添加的emp对象中;对于变化较大的职位,这里采取每次打开对话框时初始化;
<template>
<div>
...
<el-dialog
title="添加员工"
:visible.sync="dialogVisible"
width="80%">
<div>
<el-form ref="empForm" :model="emp">
<el-row>
...
<el-col :span="6">
<el-form-item label="政治面貌:" prop="politicId">
<el-select v-model="emp.politicId" size="mini" style="width: 200px" placeholder="政治面貌">
<el-option v-for="item in politicsStatus" :key="item.id" :label="item.name" :value="item.id"></el-option>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="6">
<el-form-item label="民族:" prop="nationId">
<el-select v-model="emp.nationId" size="mini" style="width: 150px;" placeholder="民族">
<el-option v-for="item in nations" :key="item.id" :label="item.name" :value="item.id"></el-option>
</el-select>
</el-form-item>
</el-col>
...
</el-row>
<el-row>
<el-col :span="6">
<el-form-item label="职位:" prop="position">
<el-select v-model="emp.position" size="mini" style="width: 150px;" placeholder="职位">
<el-option v-for="item in positions" :key="item.id" :label="item.name" :value="item.id"></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="职称:" prop="joblevel">
<el-select v-model="emp.joblevel" size="mini" style="width: 150px;" placeholder="职称">
<el-option v-for="item in joblevels" :key="item.id" :label="item.name" :value="item.id"></el-option>
</el-select>
</el-form-item>
</el-col>
...
</el-row>
<el-row>
<el-col :span="6">
<el-form-item label="工号:" prop="workId">
<el-input v-model="emp.workId" placeholder="请输入工号" size="mini" style="width: 150px;" disabled></el-input>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="学历:" prop="tiptopDegree">
<el-select v-model="emp.tiptopDegree" size="mini" style="width: 150px;" placeholder="学历">
<el-option v-for="item in tiptopDegree" :key="item" :label="item" :value="item"></el-option>
</el-select>
</el-form-item>
</el-col>
...
</el-row>
...
</el-form>
</div>
...
</el-dialog>
</div>
</template>
<script>
export default {
name: "EmpBasic",
data() {
return {
...
emp: { //添加员工的对象
workId: '',
...
},
nations: [],
joblevels: [],
positions: [],
politicsStatus: [],
tiptopDegree: ['博士', '硕士', '本科', '大专', '高中', '初中', '小学', '其他']
};
},
methods: {
...
//因为职位变动大,所以另外写,并且在打开添加对话框的时候调用初始化
initPositions(){
this.getRequest('/employee/basic/positions').then(resp=>{
if(resp){
this.positions = resp.obj;
}
})
},
// 初始化添加员工的死数据
initData(){
if(!window.sessionStorage.getItem('nations')){
this.getRequest('/employee/basic/nations').then(resp=>{
if(resp){
this.nations = resp.obj;
window.sessionStorage.setItem('nations', JSON.stringify(this.nations))
}
})
}else{
this.nations = JSON.parse(window.sessionStorage.getItem('nations'))
}
if(!window.sessionStorage.getItem('joblevels')){
this.getRequest('/employee/basic/joblevels').then(resp=>{
if(resp){
this.joblevels = resp.obj;
window.sessionStorage.setItem('joblevels', JSON.stringify(this.joblevels))
}
})
}else{
this.joblevels = JSON.parse(window.sessionStorage.getItem('joblevels'))
}
if(!window.sessionStorage.getItem('politicsStatus')){
this.getRequest('/employee/basic/politicsStatus').then(resp=>{
if(resp){
this.politicsStatus = resp.obj;
window.sessionStorage.setItem('politicsStatus', JSON.stringify(this.politicsStatus))
}
})
}else{
this.politicsStatus = JSON.parse(window.sessionStorage.getItem('politicsStatus'))
}
},
getMaxWorkId(){
this.getRequest('/employee/basic/maxWorkId').then(resp=>{
if(resp){
this.emp.workId = resp.obj;
}
})
},
showAddEmpView(){
this.dialogVisible = true;
this.initPositions();
this.getMaxWorkId();
},
...
},
mounted() {
this.initEmps();
this.initData();
},
};
</script>
...
6. 选择部门
...
<el-form-item label="所属部门:" prop="department">
<el-popover placement="bottom" title="请选择部门"
width="200" trigger="manual" v-model="visible">
<el-tree :data="allDeps" :props="defaultProps"
@node-click="handleNodeClick" default-expand-all></el-tree>
<div style="width:150px; height:24px; display:inline-flex;
border:1px solid #dedede; border-radius:5px;cursor:pointer;
align-items:center; box-sizing:border-box;
padding-left: 8px; font-size: 14px;"
@click="showDepView" slot="reference">{{inputDepName}}</div>
</el-popover>
</el-form-item>
...
<script>
export default {
name: "EmpBasic",
data() {
return {
...
visible: false, //部门弹出框是否可见
defaultProps: { //树形控件的子节点
children: 'children',
label: 'name'
},
allDeps: [], //所有部门
inputDepName: '' //部门回显名
};
},
methods: {
...
// 初始化添加员工的死数据
initData(){
...
if(!window.sessionStorage.getItem('allDeps')){
this.getRequest('/employee/basic/departments').then(resp=>{
if(resp){
this.allDeps = resp.obj;
window.sessionStorage.setItem('allDeps', JSON.stringify(this.allDeps))
}
})
}else{
this.allDeps = JSON.parse(window.sessionStorage.getItem('allDeps'))
}
},
...
//打开关闭部门框
showDepView(){
this.visible = !this.visible;
},
handleNodeClick(data){
this.inputDepName = data.name;
this.emp.departmentId = data.id;
this.visible = !this.visible;
}
},
...
};
</script>
7. 校验并添加员工
...
<el-dialog ...>
<div>
<el-form ref="empForm" :model="emp" :rules="rules">
...
</el-form>
</div>
<span slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false">取 消</el-button>
<el-button type="primary" @click="doAddEmp">确 定</el-button>
</span>
</el-dialog>
...
<script>
export default{
...
data() {
return {
...
rules: { //提交前的表单验证规则
name: [{required: true, message: '请输入员工姓名', trigger: 'blur'}],
gender: [{required: true, message: '请输入员工性别', trigger: 'blur'}],
birthday: [{required: true, message: '请输入出生日期', trigger: 'blur'}],
idCard: [{required: true, message: '请输入身份证号码', trigger: 'blur'},
{pattern: /^[1-9]\d{5}(18|19|20)\d{2}((0[1-9])|(1[0-2]))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$/, message: '身份证号码格式不正确', trigger: 'blur'}
],
wedlock: [{required: true, message: '请输入婚姻状况', trigger: 'blur'}],
nationId: [{required: true, message: '请输入民族', trigger: 'blur'}],
nativePlace: [{required: true, message: '请输入籍贯', trigger: 'blur'}],
politicsStatus: [{required: true, message: '请输入政治面貌', trigger: 'blur'}],
email: [{required: true, message: '请输入邮箱地址', trigger: 'blur'},
{type:'email', message:'邮箱地址格式不正确', trigger: 'blur'}
],
phone: [{required: true, message: '请输入电话号码', trigger: 'blur'}],
address: [{required: true, message: '请输入员工地址', trigger: 'blur'}],
departmentId: [{required: true, message: '请输入所属部门', trigger: 'blur'}],
jobLevelId: [{required: true, message: '请输入职称', trigger: 'blur'}],
posId: [{required: true, message: '请输入职位', trigger: 'blur'}],
engageForm: [{required: true, message: '请输入聘用形式', trigger: 'blur'}],
tiptopDegree: [{required: true, message: '请输入学历', trigger: 'blur'}],
specialty: [{required: true, message: '请输入专业', trigger: 'blur'}],
school: [{required: true, message: '请输入毕业院校', trigger: 'blur'}],
beginDate: [{required: true, message: '请输入入职日期', trigger: 'blur'}],
workState: [{required: true, message: '请输入工作状态', trigger: 'blur'}],
workId: [{required: true, message: '请输入工号', trigger: 'blur'}],
contractTerm: [{required: true, message: '请输入合同期限', trigger: 'blur'}],
conversionTime: [{required: true, message: '请输入转正日期', trigger: 'blur'}],
notWorkDate: [{required: true, message: '请输入离职日期', trigger: 'blur'}],
beginContract: [{required: true, message: '请输入合同起始日期', trigger: 'blur'}],
endContract: [{required: true, message: '请输入合同结束日期', trigger: 'blur'}],
workAge: [{required: true, message: '请输入工龄', trigger: 'blur'}]
}
};
},
methods: {
...
//执行添加员工
doAddEmp(){
this.$refs.empForm.validate(valid=>{
if(valid){
this.postRequest('/employee/basic/', this.emp).then(resp=>{
if(resp){
dialogVisible = false;
this.initEmps();
}
})
}
})
}
}
}
...
</script>
8. 删除/编辑员工
思路分析:传递id删除员工;复用添加员工实现编辑员工;
<template>
...
<el-button ... @click="deleteEmp(scope.row)">删除</el-button>
...
</template>
<script>
export default {
name: "EmpBasic",
data() {
return {
...
emp: { // 添加/修改员工的对象
...
},
...
dialogTitle: ''
};
},
methods: {
...
showAddEmpView(){
this.dialogTitle = '添加员工'
this.emp = { //初始化对象
...
}
this.inputDepName = '';
this.dialogVisible = true;
this.initPositions();
this.getMaxWorkId();
},
...
//执行添加/修改员工
doAddEmp(){
if(this.emp.id){
//修改员工
this.$refs.empForm.validate(valid=>{
if(valid){
this.putRequest('/employee/basic/', this.emp).then(resp=>{
if(resp){
this.dialogVisible = false;
this.initEmps();
}
})
}
})
}else{
//添加员工
this.$refs.empForm.validate(valid=>{
if(valid){
this.postRequest('/employee/basic/', this.emp).then(resp=>{
if(resp){
this.dialogVisible = false;
this.initEmps();
}
})
}
})
}
},
//删除员工
deleteEmp(data){
this.$confirm('此操作将永久删除[ '+data.name+' ]该员工,是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.deleteRequest('/employee/basic/'+data.id).then(resp=>{
if(resp){
this.initEmps();
}
})
this.$message({
type: 'success',
message: '删除成功!'
});
}).catch(() => {
this.$message({
type: 'info',
message: '已取消删除'
});
});
},
//编辑员工
showEditEmpView(data){
this.dialogTitle = '编辑员工';
Object.assign(this.emp, data)
this.inputDepName = data.department.name;
this.initPositions();
this.dialogVisible = true;
}
},
...
};
</script>
...
9. 导出员工数据
① 安装js-file-download
npm install js-file-download
② 封装download.js
// utils/download.js
import axios from 'axios'
const service = axios.create({
responseType: 'arraybuffer'
})
service.interceptors.request.use(config=>{
config.headers['Authorization'] = window.sessionStorage.getItem('tokenStr');
return config;
}, error=>{
console.log(error);
})
service.interceptors.response.use(resp=>{
const headers = resp.headers;
//判断是否为application/json格式
let reg = RegExp(/application\/json/);
if(headers['content-type'].match(reg)){
//如果是application/json
resp.data = unitToString(resp.data)
}else{
//如果是stream流
let fileDownload = require('js-file-download'); //js-file-download
let fileName = headers['content-disposition'].split(';')[1].split('filename=')[1]; //文件名
let contentType = headers['content-type']; //文件类型
//解码:防止中文乱码
fileName = decodeURIComponent(fileName);
fileDownload(resp.data, fileName, contentType)
}
}, error=>{
console.log(error);
})
function unitToString(unitArray){
let encodedString = String.fromCharCode.apply(null, new Uint8Array(unitArray));
let decodedString = decodeURIComponent(escape(encodedString));
return JSON.parse(decodedString);
}
let baseUrl = '';
export const downloadRequest = (url, params)=>{
return service({
methods: 'GET',
url: `${baseUrl}${url}`,
data: params
})
}
export default service;
// main.js
...
import { downloadRequest } from './utils/download';
...
Vue.prototype.downloadRequest = downloadRequest;
...
③ 使用
<template>
...
<el-button type="success" size="small" @click="exportData">
<i class="fa fa-level-down"></i>
导出数据
</el-button>
</template>
<script>
export default {
...
methods: {
exportData(){
this.downloadRequest('/employee/basic/export');
}
}
...
}
</script>
...
10. 导入数据
<template>
...
<el-upload
style="display: inline-flex;margin-right: 10px;"
:show-file-list="false" :before-upload="beforeUpload"
:on-success="onSuccess" :on-error="onError"
:disabled="importDataDisabled"
:headers="headers"
action="/employee/basic/import">
<el-button type="success" size="small" :icon="importDataBtnIcon"
:disabled="importDataDisabled">
{{importDataBtnText}}
</el-button>
</el-upload>
...
</template>
<script>
export default {
data(){
return{
...
//导入数据的图标和文本
importDataBtnText: '导入数据',
importDataBtnIcon: 'el-icon-upload2',
importDataDisabled: false,
//令牌
headers: {
Authorization: window.sessionStorage.getItem('tokenStr')
}
},
methods: {
...
//上传文件成功之前
beforeUpload(){
this.importDataBtnText = '正在上传';
this.importDataBtnIcon = 'el-icon-loading';
this.importDataDisabled = true;
},
//上传成功
onSuccess(){
this.importDataBtnText = '导入数据';
this.importDataBtnIcon = 'el-icon-upload';
this.importDataDisabled = false;
this.initEmps();
},
//上传失败
onError(){
this.importDataBtnText = '导入数据';
this.importDataBtnIcon = 'el-icon-upload';
this.importDataDisabled = false;
}
}
}
</script>
十、工资账套管理
1. 基本展示
<template>
<div>
<div style="display: flex; justify-content: space-between;">
<el-button type="primary" icon="el-icon-plus">添加工资账套</el-button>
<el-button type="success" icon="el-icon-refresh"></el-button>
</div>
<div style="margin-top: 10px;">
<el-table :data="slaries" border stripe>
<el-table-column type="selection" width="40"></el-table-column>
<el-table-column prop="name" label="账套名称" width="120"></el-table-column>
<el-table-column prop="basicSalary" label="基本工资" width="80"></el-table-column>
<el-table-column prop="trafficSalary" label="交通补助" width="80"></el-table-column>
<el-table-column prop="lunchSalary" label="午餐补助" width="80"></el-table-column>
<el-table-column prop="bonus" label="奖金" width="80"></el-table-column>
<el-table-column prop="createDate" label="启用时间" width="100"></el-table-column>
<el-table-column label="养老金" align="center">
<el-table-column prop="pensionPer" label="比率" width="80"></el-table-column>
<el-table-column prop="pensionBase" label="基数" width="120"></el-table-column>
</el-table-column>
<el-table-column label="医疗保险" align="center">
<el-table-column prop="medicalPer" label="比率" width="80"></el-table-column>
<el-table-column prop="medicalBase" label="基数" width="120"></el-table-column>
</el-table-column>
<el-table-column label="公积金" align="center">
<el-table-column prop="accumulationFundPer" label="比率" width="80"></el-table-column>
<el-table-column prop="accumulationFundBase" label="基数" width="120"></el-table-column>
</el-table-column>
<el-table-column label="操作">
<el-button type="primary">编辑</el-button>
<el-button type="danger">删除</el-button>
</el-table-column>
</el-table>
</div>
</div>
</template>
<script>
export default {
name: 'SalSob',
data(){
return{
slaries: {}
}
},
methods: {
initSalaries(){
this.getRequest('/salary/sob/').then(resp=>{
if(resp){
this.slaries = resp.obj;
}
})
}
},
mounted(){
this.initSalaries();
}
}
</script>
<style scoped>
</style>
2. 添加工资账套界面
<template>
<div>
...
<el-dialog
title="添加工资账套"
:visible.sync="dialogVisible"
width="50%">
<div style="display: flex;justify-content: space-around;align-items: center;">
<el-steps direction="vertical" :active="activeItemIndex">
<el-step :title="itemName" v-for="(itemName, index) in salaryItemName" :key="index"></el-step>
</el-steps>
<el-input v-for="(itemName, index) in salaryItemName" :key="index"
:placeholder="'请输入'+itemName+'...'"
v-show="index == activeItemIndex" style="width: 200px;"></el-input>
</div>
<span slot="footer" class="dialog-footer">
<el-button @click="preStep">{{activeItemIndex==10?'取 消':'上一步'}}</el-button>
<el-button type="primary" @click="nextStep">{{activeItemIndex==10?'完 成':'下一步'}}</el-button>
</span>
</el-dialog>
</div>
</template>
<script>
export default {
name: 'SalSob',
data(){
return{
...
salaryItemName: ['账套名称', '基本工资', '交通补助', '午餐补助', '奖金', '养老金比率', '养老金基数', '医疗保险比率', '医疗保险基数', '公积金比率', '公积金基数'],
activeItemIndex: 0
}
},
methods: {
...
showAddSalaryView(){
this.activeItemIndex = 0;
this.dialogVisible = true;
},
preStep(){
if(this.activeItemIndex == 0){
return;
}else if(this.activeItemIndex == 10){
this.dialogVisible = false; //取消
return;
}
this.activeItemIndex--;
},
nextStep(){
if(this.activeItemIndex == 10){
alert('OK'); //完成
return;
}
this.activeItemIndex++;
}
},
...
}
</script>
...
3. 添加账套请求
<template>
<div>
...
<el-dialog
title="添加工资账套"
:visible.sync="dialogVisible"
width="50%">
<div style="display: flex;justify-content: space-around;align-items: center;">
<el-steps direction="vertical" :active="activeItemIndex">
<el-step :title="itemName" v-for="(itemName, index) in salaryItemName" :key="index"></el-step>
</el-steps>
<el-input v-model="salary[title]" v-for="(value, title, index) in salary"
:key="index" :placeholder="'请输入'+salaryItemName[index]+'...'"
v-show="index == activeItemIndex" style="width: 200px;"></el-input>
</div>
<span slot="footer" class="dialog-footer">
<el-button @click="preStep">{{activeItemIndex==10?'取 消':'上一步'}}</el-button>
<el-button type="primary" @click="nextStep">{{activeItemIndex==10?'完 成':'下一步'}}</el-button>
</span>
</el-dialog>
</div>
</template>
<script>
export default {
name: 'SalSob',
data(){
return{
salaries: {},
dialogVisible: false,
salaryItemName: ['账套名称', '基本工资', '交通补助', '午餐补助', '奖金', '养老金比率', '养老金基数', '医疗保险比率', '医疗保险基数', '公积金比率', '公积金基数'],
activeItemIndex: 0,
salary: { //添加账套对象
"name": "工资账套",
"basicSalary": 0,
"trafficSalary": 0,
"lunchSalary": 0,
"bonus": 0,
"pensionPer": 0,
"pensionBase": 0,
"medicalPer": 0,
"medicalBase": 0,
"accumulationFundPer": 0,
"accumulationFundBase": 0
}
}
},
methods: {
...
showAddSalaryView(){
this.salary = { //初始化账套对象
"name": "",
"basicSalary": 0,
"trafficSalary": 0,
"lunchSalary": 0,
"bonus": 0,
"pensionPer": 0,
"pensionBase": 0,
"medicalPer": 0,
"medicalBase": 0,
"accumulationFundPer": 0,
"accumulationFundBase": 0
}
this.activeItemIndex = 0;
this.dialogVisible = true;
},
...
nextStep(){
if(this.activeItemIndex == 10){
//完成
this.postRequest('/salary/sob/', this.salary).then(resp=>{
if(resp){
this.initSalaries();
this.dialogVisible = false;
}
})
return;
}
this.activeItemIndex++;
}
},
...
}
</script>
...
4. 编辑和删除
<template>
<div>
<div style="display: flex; justify-content: space-between;">
<el-button type="primary" icon="el-icon-plus" @click="showAddSalaryView">添加工资账套</el-button>
<el-button type="success" icon="el-icon-refresh" @click="initSalaries"></el-button>
</div>
<div style="margin-top: 10px;">
<el-table :data="salaries" border stripe>
...
<el-table-column label="操作">
<template slot-scope="scope">
<el-button type="primary" @click="showEditSalaryView(scope.row)">编辑</el-button>
<el-button type="danger" @click="deleteSalary(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
<el-dialog
:title="dialogTitle"
:visible.sync="dialogVisible"
width="50%">
...
</el-dialog>
</div>
</template>
<script>
export default {
name: 'SalSob',
data(){
return{
dialogVisible: false,
...
activeItemIndex: 0,
salary: { //添加账套对象
...
},
//复用弹出框
dialogTitle: ''
}
},
methods: {
...
showAddSalaryView(){
this.dialogTitle = '添加工资账套'
this.salary = { //初始化账套对象
...
}
this.activeItemIndex = 0;
this.dialogVisible = true;
},
...
nextStep(){
if(this.activeItemIndex == 10){
if(this.salary.id){
//更新账套
this.putRequest('/salary/sob/', this.salary).then(resp=>{
if(resp){
this.initSalaries();
this.dialogVisible = false;
}
})
}else{
//添加账套
//最后一步,完成
this.postRequest('/salary/sob/', this.salary).then(resp=>{
if(resp){
this.initSalaries();
this.dialogVisible = false;
}
})
}
return;
}
this.activeItemIndex++;
},
// 删除
deleteSalary(data){
console.log(data);
this.$confirm('此操作将永久删除[ '+data.name+' ]该工资账套,是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.deleteRequest('/salary/sob/'+data.id).then(resp=>{
if(resp){
this.initSalaries();
}
})
this.$message({
type: 'success',
message: '删除成功!'
});
}).catch(() => {
this.$message({
type: 'info',
message: '已取消删除'
});
});
},
// 编辑
showEditSalaryView(data){
this.dialogVisible = true;
this.activeItemIndex = 0;
this.dialogTitle = '编辑工资账套';
this.salary = { //账套对象
"name": data.name,
"basicSalary": data.basicSalary,
"trafficSalary": data.trafficSalary,
"lunchSalary": data.lunchSalary,
"bonus": data.bonus,
"pensionPer": data.pensionPer,
"pensionBase": data.pensionBase,
"medicalPer": data.medicalPer,
"medicalBase": data.medicalBase,
"accumulationFundPer": data.accumulationFundPer,
"accumulationFundBase": data.accumulationFundBase,
"id": data.id
};
this.postRequest
}
},
mounted(){
this.initSalaries();
}
}
</script>
...
十一、在线聊天功能
1. 添加页面按钮、路由
...
import FriendChat from '../views/chat/FriendChat'
...
const routes = [
...
{
path: '/home',
name: 'Home',
component: Home,
children: [
{
path: '/chat',
name: '在线聊天',
component: FriendChat
}
]
}
]
const router = new VueRouter({
routes
})
export default router
<template>
<div>
<el-container>
<el-header class="homeHeader">
...
<div>
<el-button icon="el-icon-bell" type="text"
style="font-size: 20px; color: #fff; margin-right: 12px;"
@click="goChat">
</el-button>
...
</div>
</el-header>
<el-container>
...
</el-container>
</div>
</template>
<script>
export default {
...
methods: {
...
goChat(){
this.$router.push('/chat')
}
}
};
</script>
<style scoped>
...
</style>
2. 引入vue-chat
- store/index
// store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex);
const now = new Date();
const store = new Vuex.Store({
state: {
routes: [],
sessions:[{
id:1,
user:{
name:'示例介绍',
img:'../src/assets/images/2.png'
},
messages:[{
content:'Hello,这是一个基于Vue + Vuex + Webpack构建的简单chat示例,聊天记录保存在localStorge, 有什么问题可以通过Github Issue问我。',
date:now
},{
content:'项目地址(原作者): https://github.com/coffcer/vue-chat',
date:now
},{
content:'本项目地址(重构): https://github.com/is-liyiwei',
date:now
}]
},{
id:2,
user:{
name:'webpack',
img:'../src/assets/images/3.jpg'
},
messages:[{
content:'Hi,我是webpack哦',
date:now
}]
}],
currentSessionId:1,
filterKey:''
},
mutations: {
initRoutes(state, payload){
state.routes = payload;
},
changeCurrentSessionId (state,id) {
state.currentSessionId = id;
},
addMessage (state,msg) {
state.sessions[state.currentSessionId-1].messages.push({
content:msg,
date: new Date(),
self:true
})
},
INIT_DATA (state) {
let data = localStorage.getItem('vue-chat-session');
//console.log(data)
if (data) {
state.sessions = JSON.parse(data);
}
}
},
actions: {
initData (context) {
context.commit('INIT_DATA')
}
}
});
store.watch(function (state) {
return state.sessions
},function (val) {
console.log('CHANGE: ', val);
localStorage.setItem('vue-chat-session', JSON.stringify(val));
},{
deep:true/*这个貌似是开启watch监测的判断,官方说明也比较模糊*/
})
export default store;
- FriendChat.vue
<template>
<div id="app">
<div class="sidebar">
<card></card>
<list></list>
</div>
<div class="main">
<message></message>
<usertext></usertext>
</div>
</div>
</template>
<script>
import card from '../../components/chat/card.vue'
import list from '../../components/chat/list.vue'
import message from '../../components/chat/message.vue'
import usertext from '../../components/chat/usertext.vue'
export default {
name: 'FriendChat',
data () {
return {
}
},
mounted:function() {
this.$store.dispatch('initData');
},
components:{
card,
list,
message,
usertext
}
}
</script>
<style lang="scss" scoped>
#app {
margin: 20px auto;
width: 800px;
height: 600px;
overflow: hidden;
border-radius: 10px;
.sidebar, .main {
height: 100%;
}
.sidebar {
float: left;
color: #f4f4f4;
background-color: #2e3238;
width: 200px;
}
.main {
position: relative;
overflow: hidden;
background-color: #eee;
}
}
</style>
- 引入自定义组件
- 安装sass依赖
npm install sass-loader --save-dev
npm install node-sass --save-dev
- 基本效果
3. 动态用户列表展示
- store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
import {getRequest} from '../utils/api'
Vue.use(Vuex);
const now = new Date();
const store = new Vuex.Store({
state: {
routes: [],
sessions: [...],
admins: [],
currentSessionId: -1,
filterKey:''
},
mutations: {
...
INIT_ADMINS(state, data){
console.log(111);
state.admins = data;
}
},
actions: {
initData (context) {
getRequest('/chat/').then(resp=>{
if(resp){
context.commit('INIT_ADMINS', resp.obj)
}
})
}
}
});
store.watch(function (state) {
return state.sessions
},function (val) {
console.log('CHANGE: ', val);
localStorage.setItem('vue-chat-session', JSON.stringify(val));
},{
deep:true/*这个貌似是开启watch监测的判断,官方说明也比较模糊*/
})
export default store;
- card.vue
<template>
<div id="card">
<header>
<img class="avatar" v-bind:src="user.userFace" v-bind:alt="user.remark">
<p class="name">{{user.remark}}</p>
</header>
<footer>
<input class="search" type="text" v-model="$store.state.filterKey"
placeholder="search user...">
</footer>
</div>
</template>
<script>
export default {
name: 'card',
data () {
return {
user: JSON.parse(window.sessionStorage.getItem('user'))
}
}
}
</script>
....
- list.vue
<template>
<div id="list">
<ul style="padding-left: 0;">
<li v-for="item in admins" :class="{ active: item.id === currentSessionId }"
v-on:click="changeCurrentSessionId(item.id)" :key="item.id">
<img class="avatar" :src="item.userFace" :alt="item.remark">
<p class="name">{{item.remark}}</p>
</li>
</ul>
</div>
</template>
<script>
import {mapState} from 'vuex'
export default {
...
computed: mapState([
'admins',
'currentSessionId'
]),
methods:{
changeCurrentSessionId:function (id) {
this.$store.commit('changeCurrentSessionId',id)
}
}
}
</script>
...
4. WebSocket
-
简介
WebSocket是HTML5新增的网络协议,它可以在单个TCP连接上提供全双工通讯,让客户端和服务端之间数据交换更简单,它允许服务端向客户端主动推送数据;使用WebSocket,客户端和服务端之间只需要进行一次握手就可以建立持久性的、双向的连接。
-
安装
npm i sockjs-client
npm i stompjs
npm i net --save
- 请求转发
// vue.config.js
const { defineConfig } = require('@vue/cli-service')
// 请求经过nodejs时候,会通过这个代理对象,转发到8081端口
let proxyObj = {}
proxyObj['/'] = { //所有要代理的路径是/
// websocket
ws: false,
// 代理到哪里去,目标地址
target: 'http://localhost:8081',
// 表示发生请求头host会被设置为target
changeOrigin: true,
// 假如后端有前端路径,这里会不重写请求路径
pathReWrite: {
'^/': '/'
}
}
// websocket
proxyObj['/wsyeb'] = {
ws: true,
target: 'wsyeb://localhost:8081'
}
module.exports = defineConfig({
transpileDependencies: true,
// 转发到8081端口
devServer: {
host: 'localhost',
port: 8080,
proxy: proxyObj
}
})
5. 实现基本的WebSocket连接
思路分析:在状态管理对象中定义一个连接方法(在连接socket时要将JWT令牌传给Auth-Token),在初始化页面(这里选择在初始化列表时)调用该方法,实现WebSocket的消息订阅;订阅成功后,在usertext组件中调用addMessage方法,模拟消息发送(这里固定向wang管理员发送消息);
- store/index.js
...
import SockJS from 'sockjs-client'
import Stomp from 'stompjs'
...
const store = new Vuex.Store({
state: {
...
stomp: null
},
mutations: {
...
},
actions: {
...
connect(context){
context.state.stomp = Stomp.over(new SockJS('/wsyeb/ep'));
// 因为使用JWT令牌,所以要身份验证
let token = window.sessionStorage.getItem('tokenStr')
context.state.stomp.connect({'Auth-Token': token}, success=>{
//订阅消息
context.state.stomp.subscribe('/user/queue/chat', msg=>{
console.log(msg.body);
})
},error=>{
console.log(error);
})
}
}
});
...
export default store;
- menu.js
...
export const initMenu = (router, store)=>{
...
// 发送请求并处理
getRequest('/system/cfg/menu').then(data=>{
if(data){
...
// 连接Socket
store.dispatch('connect')
}
})
}
...
- 模拟消息发送
<template>
<div id="uesrtext">
<textarea placeholder="按 Ctrl + Enter 发送" v-model="content" v-on:keyup="addMessage"></textarea>
</div>
</template>
<script>
import {mapState} from 'vuex'
export default {
name: 'uesrtext',
data () {
return {
content:''
}
},
methods: {
addMessage (e) {
if (e.ctrlKey && e.keyCode ===13 && this.content.length) {
let msgObj = new Object();
msgObj.to = 'wang'
msgObj.content = this.content
this.$store.state.stomp.send('/wsyeb/chat', {}, JSON.stringify(msgObj))
}
}
}
}
</script>
...
6. 消息的发送和接收存储
思路分析: 点击对象时切换到点击的会话对象;发送消息后,将消息存储在store.state.sessions对象中,而对象的键为类似于admin#wang(admin对wang的聊天记录)的格式,值为一条条消息对象;
sessions: {
admin#wang: [
{content:'哈哈哈', date:'2022-8-3',self: false},
{content:'嘿嘿嘿', date:'2022-8-3',self: false},
...
],
wang#yu: [
{content:'111', date:'2022-8-3',self: false},
{content:'222', date:'2022-8-3',self: false},
...
],
...
}
- 切换会话对象
<template>
<div id="list">
<ul style="padding-left: 0;">
<li v-for="item in admins"
:class="{ active: currentSession?item.username === currentSession.username:false }"
@click="changeCurrentSession(item)" :key="item.id">
<img class="avatar" :src="item.userFace" :alt="item.remark">
<p class="name">{{item.remark}}</p>
</li>
</ul>
</div>
</template>
<script>
import {mapState} from 'vuex'
export default {
...
computed: mapState([
'admins',
'currentSession'
]),
methods:{
changeCurrentSession:function (currentSession) {
this.$store.commit('changeCurrentSession',currentSession)
}
}
}
</script>
...
- 发送消息
// usertext.vue
...
export default {
...
computed: mapState([
'currentSession'
]),
methods: {
addMessage (e) {
if (e.ctrlKey && e.keyCode ===13 && this.content.length) {
let msgObj = new Object();
msgObj.to = this.currentSession.username; //当前选中的聊天对象
msgObj.content = this.content;
this.$store.state.stomp.send('/wsyeb/chat', {}, JSON.stringify(msgObj))
this.$store.commit('addMessage',msgObj);
this.content='';
}
}
}
}
...
- 接收消息
// store/index.js
...
const store = new Vuex.Store({
state: {
...
sessions:{},
admins: [], //列表
currentAdmin: JSON.parse(window.sessionStorage.getItem('user')), //当前用户
currentSession: null, //当前会话对象
filterKey:'', //筛选用户的关键字
stomp: null //连接对象
},
mutations: {
...
changeCurrentSession (state,currentSession) {
state.currentSession = currentSession;
},
addMessage (state,msg) {
let mss = state.sessions[state.currentAdmin.username+'#'+msg.to];
if(!mss){
// 不存在这个会话时,初始化一个数组
state.sessions[state.currentAdmin.username+'#'+msg.to] = []
}// 存在,push消息进这个数组
state.sessions[state.currentAdmin.username+'#'+msg.to].push({
content:msg.content,
date: new Date(),
self: !msg.notSelf //防止 admin#admin 的情况
})
},
...
},
actions: {
...
connect(context){
context.state.stomp = Stomp.over(new SockJS('/wsyeb/ep'));
// 因为使用JWT令牌,所以要身份验证
let token = window.sessionStorage.getItem('tokenStr')
context.state.stomp.connect({'Auth-Token': token}, success=>{
//订阅消息
context.state.stomp.subscribe('/user/queue/chat', msg=>{
// console.log(msg.body);
let receiveMsg = JSON.parse(msg.body);
receiveMsg.notSelf = true;
//admin发给wang,wang接收消息并存入sessions:wang#admin,不会出现wang#wang
receiveMsg.to = receiveMsg.from;
context.commit('addMessage', receiveMsg)
})
},error=>{
console.log(error);
})
}
}
});
...
7. 聊天数据的展示
思路分析:因为消息原来存储在Vuex中,但是为了实现数据的持久化,应该将聊天记录存放在localStorage中,除非主动删除,数据会一直存在;而这个存储的过程实现为:在Vuex中写了一个监听sessions变化的函数,当sessions变化时,会主动去调用INIT_DATA这个同步方法,将sessions存入localStorage的vue-chat-session中;而消息展示的实现,只需要把存放在localStorage中对应会话的内容拿出来遍历,并通过每一条消息对象的self来判断是否为登录的用户,实现差异化;
- store/index.js
...
const store = new Vuex.Store({
state: {
...
sessions:{},
...
},
mutations: {
...
INIT_DATA (state) {
// 浏览器本地的历史聊天记录,数据持久化
let data = localStorage.getItem('vue-chat-session');
//console.log(data)
if (data) {
state.sessions = JSON.parse(data);
}
},
...
},
...
store.watch(function (state) {
return state.sessions
},function (val) {
console.log('CHANGE: ', val);
localStorage.setItem('vue-chat-session', JSON.stringify(val));
},{
deep:true/*这个貌似是开启watch监测的判断,官方说明也比较模糊*/
})
export default store;
- Message.vue
<template>
<div id="message" v-scroll-bottom="sessions">
<ul v-if="currentSession">
<li v-for="entry in sessions[user.username+'#'+currentSession.username]"
:key="entry.id">
<p class="time">
<span>{{entry.date | time}}</span>
</p>
<div class="main" :class="{self:entry.self}">
<img class="avatar" :src="entry.self ? user.userFace : currentSession.userFace">
<p class="text">{{entry.content}}</p>
</div>
</li>
</ul>
</div>
</template>
<script>
import {mapState} from 'vuex'
export default {
name: 'message',
data () {
return {
user: JSON.parse(window.sessionStorage.getItem('user'))
}
},
computed:mapState([
'sessions',
'currentSession'
]),
filters:{
time (date) {
if (date) {
date = new Date(date);
}
return `${date.getHours()}:${date.getMinutes()}`;
}
},
directives: {/*这个是vue的自定义指令,官方文档有详细说明*/
// 发送消息后滚动到底部,这里无法使用原作者的方法,也未找到合理的方法解决,暂用setTimeout的方法模拟
'scroll-bottom' (el) {
//console.log(el.scrollTop);
setTimeout(function () {
el.scrollTop+=9999;
}, 1)
}
}
}
</script>
<style lang="scss" scoped>
#message {
padding: 15px;
max-height: 68%;
overflow-y: scroll;
ul {
list-style-type: none;
padding-left: 0;
li {
margin-bottom: 15px;
}
}
.time {
text-align: center;
margin: 7px 0;
> span {
display: inline-block;
padding: 0 18px;
font-size: 12px;
color: #FFF;
background-color: #dcdcdc;
border-radius: 2px;
}
}
.main {
.avatar {
float: left;
margin: 0 10px 0 0;
border-radius: 3px;
width: 30px;
height: 30px;
}
.text {
display: inline-block;
padding: 0 10px;
max-width: 80%;
background-color: #fafafa;
border-radius: 4px;
line-height: 30px;
}
}
.self {
text-align: right;
.avatar {
float: right;
margin: 0 0 0 10px;
border-radius: 3px;
width: 30px;
height: 30px;
}
.text {
display: inline-block;
padding: 0 10px;
max-width: 80%;
background-color: #b2e281;
border-radius: 4px;
line-height: 30px;
}
}
}
</style>
8.消息提示
思路分析:在状态管理中设置一个isDot对象,这个对象的数据结构设计仿照聊天信息,根据例如admin#wang=true的格式,判断是否显示消息提示点。当接收到消息时,若不在相应会话框则显示提示点和消息提示;这里为了防止刷新后未查看的消息消失,仿照聊天记录进行数据持久化,其实也可以将这个提示点属性直接添加到聊天消息对象中!注意的是:在js文件中直接使用ElementUI不能使用this.$notify,需要单独导入Notification对象!
- store/index.js
...
import { Notification } from 'element-ui';
...
const store = new Vuex.Store({
state: {
...
isDot: {}
},
mutations: {
...
changeCurrentSession (state, currentSession) {
// console.log(currentSession);
state.currentSession = currentSession;
Vue.set(state.isDot, state.currentAdmin.username+'#'+state.currentSession.username, false)
},
...
INIT_ISDOT (state) {
// 将红点数据持久化
let data = localStorage.getItem('vue-chat-session-dot');
//console.log(data)
if (data) {
state.isDot = JSON.parse(data);
}
},
},
actions: {
initData (context) {
context.commit('INIT_DATA');
context.commit('INIT_ISDOT');
...
},
connect(context){
...
context.state.stomp.connect({'Auth-Token': token}, success=>{
//订阅消息
context.state.stomp.subscribe('/user/queue/chat', msg=>{
...
if(!context.state.currentSession || receiveMsg.from != context.state.currentSession.username){
// 通知弹框
Notification({
title: `管理员 ${receiveMsg.fromNickName} 发来消息`,
message: receiveMsg.content.length>10?receiveMsg.content.substr(0,10):receiveMsg.content,
position: 'bottom-right'
});
Vue.set(context.state.isDot, context.state.currentAdmin.username+'#'+receiveMsg.from, true)
}
...
})
},error=>{
console.log(error);
})
}
}
});
...
// 监听红点变化
store.watch((state)=>{
return state.isDot;
}, (val)=>{
localStorage.setItem('vue-chat-session-dot',JSON.stringify(val))
},{
deep: true
})
export default store;
- list.vue
<template>
<div id="list">
<ul style="padding-left: 0;">
<li v-for="item in admins"
:class="{ active: currentSession?item.username === currentSession.username:false }"
@click="changeCurrentSession(item)" :key="item.id">
<el-badge :is-dot="isDot[user.username+'#'+item.username]" type="primary">
<img class="avatar" :src="item.userFace" :alt="item.remark">
</el-badge>
<p class="name">{{item.remark}}</p>
</li>
</ul>
</div>
</template>
<script>
import {mapState} from 'vuex'
export default {
name: 'list',
data () {
return {
user: JSON.parse(window.sessionStorage.getItem('user'))
}
},
computed: mapState([
'isDot',
'admins',
'currentSession'
]),
methods:{
changeCurrentSession(currentSession) {
this.$store.commit('changeCurrentSession',currentSession)
}
}
}
</script>
...
十二、个人中心
<template>
<div style="display: flex;justify-content: center;">
<el-card class="box-card" style="width: 60%;">
<div slot="header" class="clearfix">
<span>{{admin.remark}}</span>
</div>
<div>
<div style="display: flex; justify-content: center;">
<el-upload
action="/admin/userFace"
:headers="headers"
:data="admin"
:show-file-list="false"
:on-success="onSuccess"
>
<img :src="admin.userFace" alt="" title="点击修改用户头像" style="width: 100px; height: 100px; border-radius: 50px;">
</el-upload>
</div>
<div style="display: flex; justify-content: space-between;margin-top:40px;margin-bottom: 20px;">
<div>账号名称:<el-tag>{{admin.name}}</el-tag></div>
<div>电话号码:<el-tag>{{admin.telphone}}</el-tag></div>
<div>手机号码:<el-tag>{{admin.phone}}</el-tag></div>
</div>
<div style="display: flex; justify-content: space-between;margin-bottom: 20px;">
<div>居住地址:<el-tag>{{admin.address}}</el-tag></div>
</div>
<div style="margin-bottom: 40px;">用户标签:<el-tag v-for="(r,index) in admin.roles" :key="index" style="margin-right:5px;" type="success">{{r.remark}}</el-tag></div>
<div style="display: flex; justify-content: center;">
<el-button type="primary" @click="showEditView">修改信息</el-button>
<el-button type="danger" @click="showEditPasswordView">修改密码</el-button>
</div>
</div>
</el-card>
<el-dialog
title="修改用户信息"
:visible.sync="dialogVisible"
width="40%">
<div>
<table>
<tr>
<td>用户昵称:</td>
<td><el-input v-model="editAdmin.remark"></el-input></td>
</tr>
<tr>
<td>电话号码:</td>
<td><el-input v-model="editAdmin.telphone"></el-input></td>
</tr>
<tr>
<td>手机号码:</td>
<td><el-input v-model="editAdmin.phone"></el-input></td>
</tr>
<tr>
<td>用户地址:</td>
<td><el-input v-model="editAdmin.address"></el-input></td>
</tr>
</table>
</div>
<span slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false">取 消</el-button>
<el-button type="primary" @click="updateAdminInfo">确 定</el-button>
</span>
</el-dialog>
<el-dialog
title="修改用户密码"
:visible.sync="psdDialogVisible"
width="40%">
<div>
<el-form :model="ruleForm" status-icon :rules="rules" ref="ruleForm" label-width="100px" class="demo-ruleForm">
<el-form-item label="请输入旧密码" prop="oldPass">
<el-input type="password" v-model="ruleForm.oldPass" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="请输入新密码" prop="pass">
<el-input type="password" v-model="ruleForm.pass" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="请确认新密码" prop="checkPass">
<el-input type="password" v-model="ruleForm.checkPass" autocomplete="off"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submitForm('ruleForm')">提交</el-button>
<el-button @click="resetForm('ruleForm')">重置</el-button>
</el-form-item>
</el-form>
</div>
</el-dialog>
</div>
</template>
<script>
export default {
name: 'AdminInfo',
data(){
var validatePass = (rule, value, callback) => {
if (value === '') {
callback(new Error('请输入密码'));
} else {
if (this.ruleForm.checkPass !== '') {
this.$refs.ruleForm.validateField('checkPass');
}
callback();
}
};
var validatePass2 = (rule, value, callback) => {
if (value === '') {
callback(new Error('请再次输入密码'));
} else if (value !== this.ruleForm.pass) {
callback(new Error('两次输入密码不一致!'));
} else {
callback();
}
};
var validateOldPass = (rule, value, callback) => {
if (value === '') {
callback(new Error('请输入旧密码'));
} else {
if (this.ruleForm.checkPass !== '') {
this.$refs.ruleForm.validateField('oldPass');
}
callback();
}
};
return {
admin: {},
editAdmin: {},
dialogVisible: false,
psdDialogVisible: false,
ruleForm: {
oldPass: '',
pass: '',
checkPass: ''
},
rules: {
pass: [
{ validator: validatePass, trigger: 'blur' }
],
checkPass: [
{ validator: validatePass2, trigger: 'blur' }
],
oldPass: [
{ validator: validateOldPass, trigger: 'blur' }
]
},
headers: {
Authorization: window.sessionStorage.getItem('tokenStr')
}
};
},
mounted(){
this.initAdmin();
},
methods: {
initAdmin(){
this.getRequest('/login/getUserInfo').then(resp=>{
if(resp){
this.admin = resp.obj;
this.editAdmin = Object.assign({}, this.admin)
window.sessionStorage.setItem('user', JSON.stringify(resp.obj))
this.$store.commit('INIT_ADMIN',resp.obj)
}
})
},
showEditView(){
this.dialogVisible = true;
},
updateAdminInfo(){
this.putRequest('/system/admin/',this.editAdmin).then(resp=>{
if(resp){
this.dialogVisible = false;
this.initAdmin();
}
})
},
showEditPasswordView(){
this.psdDialogVisible = true;
},
submitForm(formName) {
this.$refs[formName].validate((valid) => {
if (valid) {
this.ruleForm.adminId = this.admin.id;
this.putRequest('/admin/pass', this.ruleForm).then(resp=>{
if(resp){
// 更新密码成功后
this.postRequest('/login/doLogout');
window.sessionStorage.removeItem('user');
window.sessionStorage.removeItem('tokenStr');
this.$store.commit('initRoutes',[]);
this.$router.replace('/')
}
})
} else {
console.log('error submit!!');
return false;
}
});
},
resetForm(formName) {
this.$refs[formName].resetFields();
},
onSuccess(){
console.log(111);
this.initAdmin();
}
}
}
</script>
<style scoped>
</style>