云办公开发笔记(下)

149 阅读5分钟

九、员工管理

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>

image.png

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>
...

image.png

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>
...

image.png

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>

image.png

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>
...

image.png

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>

image.png

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>

image.png

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>
...

image.png image.png

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>

image.png

十、工资账套管理

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>

image.png

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>
...

image.png

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>
...

image.png image.png

十一、在线聊天功能

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>

image.png

2. 引入vue-chat

项目地址:github.com/is-liyiwei/…

  • 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>
  • 引入自定义组件 image.png
  • 安装sass依赖
npm install sass-loader --save-dev
npm install node-sass --save-dev
  • 基本效果 image.png

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>
...

image.png

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>
...

image.png image.png

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>

image.png image.png

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>
...

image.png image.png image.png

十二、个人中心

<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>

image.png image.png image.png