Crunchy
2025-01-13 01d423865ae5eddf91f35c8526f5683c3b430870
密码强校验添加
已修改3个文件
1591 ■■■■ 文件已修改
src/const/crud/admin/user.js 320 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/admin/user/index.vue 768 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/admin/user/info.vue 503 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/const/crud/admin/user.js
@@ -14,148 +14,190 @@
 * this software without specific prior written permission.
 * Author: ztt
 */
import {getDetails} from '@/api/admin/user'
import { getDetails } from '@/api/admin/user'
var validateUsername = (rule, value, callback) => {
  getDetails(value).then(response => {
    if (window.boxType === 'edit') callback()
    const result = response.data.data
    if (result !== null) {
      callback(new Error('用户名已经存在'))
    } else {
      callback()
    }
  })
    getDetails(value).then((response) => {
        if (window.boxType === 'edit') callback()
        const result = response.data.data
        if (result !== null) {
            callback(new Error('用户名已经存在'))
        } else {
            callback()
        }
    })
}
export const tableOption = {
  border: true,
  index: true,
  indexLabel: '序号',
  stripe: true,
  menuAlign: 'center',
  searchMenuSpan:6,
  editBtn: false,
  delBtn: false,
  align: 'center',
  addBtn: false,
  column: [{
    fixed: true,
    label: 'id',
    prop: 'userId',
    span: 24,
    hide: true,
    editDisabled: true,
    addDisplay: false
  }, {
    fixed: true,
    label: '用户名',
    prop: 'username',
    editDisabled: true,
    slot: true,
    search: true,
    span: 24,
    rules: [{
      required: true,
      message: '请输入用户名'
    },
      {
        min: 3,
        max: 20,
        message: '长度在 3 到 20 个字符',
        trigger: 'blur'
      },
      {validator: validateUsername, trigger: 'blur'}
    border: true,
    index: true,
    indexLabel: '序号',
    stripe: true,
    menuAlign: 'center',
    searchMenuSpan: 6,
    editBtn: false,
    delBtn: false,
    align: 'center',
    addBtn: false,
    column: [
        {
            fixed: true,
            label: 'id',
            prop: 'userId',
            span: 24,
            hide: true,
            editDisabled: true,
            addDisplay: false
        },
        {
            fixed: true,
            label: '用户名',
            prop: 'username',
            editDisabled: true,
            slot: true,
            search: true,
            span: 24,
            rules: [
                {
                    required: true,
                    message: '请输入用户名'
                },
                {
                    min: 3,
                    max: 20,
                    message: '长度在 3 到 20 个字符',
                    trigger: 'blur'
                },
                { validator: validateUsername, trigger: 'blur' }
            ]
        },
        {
            label: '密码',
            prop: 'password',
            type: 'password',
            hide: true,
            slot: true,
            formslot: true,
            span: 24,
            rules: [
                {
                    required: true,
                    message: '请输入密码',
                    trigger: 'blur'
                },
                {
                    validator: (rule, value, callback) => {
                        let strength = 0
                        if (value.length >= 8) strength++
                        if (/\d/.test(value)) strength++
                        if (/[a-z]/.test(value)) strength++
                        if (/[A-Z]/.test(value)) strength++
                        if (/[!@#$%^&*]/.test(value)) strength++
                        if (strength < 4) {
                            callback(new Error('密码强度不够,请确保密码包含:大小写字母、数字和特殊字符,且长度不少于8位'))
                        } else {
                            callback()
                        }
                    },
                    trigger: 'blur'
                }
            ]
        },
        {
            label: '所属分组',
            prop: 'deptId',
            formslot: true,
            slot: true,
            span: 24,
            hide: true,
            dataType: 'number',
            rules: [
                {
                    required: true,
                    message: '请选择部门',
                    trigger: 'change'
                }
            ]
        },
        {
            label: '员工',
            prop: 'staffInfo',
            formslot: true,
            span: 24,
            rules: [
                {
                    required: true,
                    message: '请选择员工',
                    trigger: 'change'
                }
            ]
        },
        {
            label: '手机号',
            prop: 'phone',
            type: 'tel',
            value: '',
            span: 24,
            rules: [
                {
                    min: 11,
                    max: 11,
                    message: '长度在 11 个字符',
                    trigger: 'blur'
                }
            ]
        },
        {
            label: '角色',
            prop: 'role',
            formslot: true,
            slot: true,
            overHidden: true,
            span: 24,
            rules: [
                {
                    required: true,
                    message: '请选择角色',
                    trigger: 'blur'
                }
            ]
        },
        {
            label: '状态',
            prop: 'lockFlag',
            type: 'radio',
            slot: true,
            border: true,
            span: 24,
            rules: [
                {
                    required: true,
                    message: '请选择状态',
                    trigger: 'blur'
                }
            ],
            dicData: [
                {
                    label: '有效',
                    value: '0'
                },
                {
                    label: '锁定',
                    value: '9'
                }
            ]
        },
        {
            width: 180,
            label: '创建时间',
            prop: 'createTime',
            type: 'datetime',
            format: 'yyyy-MM-dd HH:mm',
            valueFormat: 'yyyy-MM-dd HH:mm:ss',
            editDisabled: true,
            addDisplay: false,
            span: 24
        }
    ]
  }, {
    label: '密码',
    prop: 'password',
    type: 'password',
    value: '',
    hide: true,
    span: 24,
    rules: [{
      min: 6,
      max: 20,
      required: true,
      message: '长度在 6 到 20 个字符',
      trigger: 'blur'
    }]
  }, {
    label: '所属分组',
    prop: 'deptId',
    formslot: true,
    slot: true,
    span: 24,
    hide: true,
    dataType:"number",
    rules: [{
      required: true,
      message: '请选择部门',
      trigger: 'change'
    }]
  }, {
    label: '员工',
    prop: 'staffInfo',
    formslot: true,
    span: 24,
    rules: [{
      required: true,
      message: '请选择员工',
      trigger: 'change'
    }]
  }, {
    label: '手机号',
    prop: 'phone',
    type: 'tel',
    value: '',
    span: 24,
    rules: [{
      min: 11,
      max: 11,
      message: '长度在 11 个字符',
      trigger: 'blur'
    }]
  }, {
    label: '角色',
    prop: 'role',
    formslot: true,
    slot: true,
    overHidden: true,
    span: 24,
    rules: [{
      required: true,
      message: '请选择角色',
      trigger: 'change'
    }]
  }, {
    label: '状态',
    prop: 'lockFlag',
    type: 'radio',
    slot: true,
    border:true,
    span: 24,
    rules: [{
      required: true,
      message: '请选择状态',
      trigger: 'change'
    }],
    dicData: [{
      label: '有效',
      value: '0'
    }, {
      label: '锁定',
      value: '9'
    }]
  }, {
    width: 180,
    label: '创建时间',
    prop: 'createTime',
    type: 'datetime',
    format: 'yyyy-MM-dd HH:mm',
    valueFormat: 'yyyy-MM-dd HH:mm:ss',
    editDisabled: true,
    addDisplay: false,
    span: 24
  }]
}
src/views/admin/user/index.vue
@@ -16,186 +16,69 @@
  -->
<template>
  <div class="user">
    <basic-container>
      <el-row :span="24">
        <el-col :xs="24" :sm="24" :md="5" class="user__tree">
          <avue-tree
            :option="treeOption"
            :data="treeData"
            @node-click="nodeClick"
          >
    <div class="user">
        <basic-container>
            <el-row :span="24">
                <el-col :xs="24" :sm="24" :md="5" class="user__tree">
                    <avue-tree :option="treeOption" :data="treeData" @node-click="nodeClick">
            <span class="el-tree-node__label" slot-scope="{ node, data }">
              <el-tooltip
                class="item"
                effect="dark"
                content="无数据权限"
                placement="right-start"
                v-if="data.isLock"
              >
              <el-tooltip class="item" effect="dark" content="无数据权限" placement="right-start" v-if="data.isLock">
                <span>{{ node.label }} <i class="el-icon-lock"></i></span>
              </el-tooltip>
              <span v-if="!data.isLock">{{ node.label }}</span>
            </span>
          </avue-tree>
        </el-col>
        <el-col :xs="24" :sm="24" :md="19" class="user__main">
          <avue-crud
            ref="crud"
            :option="option"
            v-model="form"
            :page="page"
            :table-loading="listLoading"
            :before-open="handleOpenBefore"
            :data="list"
            @on-load="getList"
            @search-change="searchChange"
            @refresh-change="refreshChange"
            @size-change="sizeChange"
            @current-change="currentChange"
            @row-update="update"
            @row-save="create"
          >
            <template slot="menuLeft">
              <el-button
                v-if="sys_user_add"
                class="filter-item"
                type="primary"
                size="small"
                icon="el-icon-edit"
                @click="$refs.crud.rowAdd()"
                >添加
              </el-button>
              <!-- <el-button
                v-if="sys_user_add"
                class="filter-item"
                type="primary"
                size="small"
                icon="el-icon-upload"
                @click="importDialogVisible=true"
                >导入
              </el-button> -->
            </template>
            <template slot="username" slot-scope="scope">
              <span>{{ scope.row.username }}</span>
            </template>
            <template slot="role" slot-scope="scope">
                    </avue-tree>
                </el-col>
                <el-col :xs="24" :sm="24" :md="19" class="user__main">
                    <avue-crud ref="crud" :option="option" v-model="form" :page="page" :table-loading="listLoading" :before-open="handleOpenBefore" :data="list" @on-load="getList" @search-change="searchChange" @refresh-change="refreshChange" @size-change="sizeChange" @current-change="currentChange" @row-update="update" @row-save="create">
                        <template slot="menuLeft">
                            <el-button v-if="sys_user_add" class="filter-item" type="primary" size="small" icon="el-icon-edit" @click="$refs.crud.rowAdd()">添加 </el-button>
                        </template>
                        <template slot="username" slot-scope="scope">
                            <span>{{ scope.row.username }}</span>
                        </template>
                        <template slot="role" slot-scope="scope">
              <span v-for="(role, index) in scope.row.roleList" :key="index">
                <el-tag>{{ role.roleName }} </el-tag>&nbsp;&nbsp;
              </span>
            </template>
            <template slot="deptId" slot-scope="scope">
              {{ scope.row.deptName }}
            </template>
            <template slot="lockFlag" slot-scope="scope">
              <el-tag>{{ scope.label }}</el-tag>
            </template>
            <template slot="menu" slot-scope="scope">
              <el-button
                v-if="sys_user_edit"
                type="text"
                size="small"
                icon="el-icon-edit"
                @click="handleUpdate(scope.row, scope.index)"
                >编辑
              </el-button>
              <el-button
                v-if="sys_user_del"
                type="text"
                size="small"
                icon="el-icon-delete"
                @click="deletes(scope.row, scope.index)"
                >删除
              </el-button>
              <el-button
                v-if="sys_user_lock"
                type="text"
                size="small"
                icon="el-icon-unlock"
                @click="unlock(scope.row, scope.index)"
                >解锁
              </el-button>
            </template>
            <template slot="deptIdForm" slot-scope="scope">
              <avue-input-tree
                v-model="form.deptId"
                :node-click="getNodeData"
                :dic="treeDeptData"
                :props="defaultProps"
                placeholder="请选择所属分组"
              />
            </template>
            <template slot="staffInfoForm" slot-scope="scope">
              <el-input
                @focus="showStaff = true"
                v-model="form.staffInfo"
                readonly
              >
                <i
                  class="el-icon-arrow-down el-input__icon"
                  slot="suffix"
                  @click="showStaff = true"
                ></i>
              </el-input>
            </template>
            <template slot="roleForm" slot-scope="scope">
              <avue-select
                v-model="role"
                :dic="rolesOptions"
                :props="roleProps"
                multiple
                placeholder="请选择角色"
              />
            </template>
          </avue-crud>
        </el-col>
      </el-row>
    </basic-container>
                        </template>
                        <template slot="deptId" slot-scope="scope">
                            {{ scope.row.deptName }}
                        </template>
                        <template slot="lockFlag" slot-scope="scope">
                            <el-tag>{{ scope.label }}</el-tag>
                        </template>
                        <template slot="menu" slot-scope="scope">
                            <el-button v-if="sys_user_edit" type="text" size="small" icon="el-icon-edit" @click="handleUpdate(scope.row, scope.index)">编辑 </el-button>
                            <el-button v-if="sys_user_del" type="text" size="small" icon="el-icon-delete" @click="deletes(scope.row, scope.index)">删除 </el-button>
                            <el-button v-if="sys_user_lock" type="text" size="small" icon="el-icon-unlock" @click="unlock(scope.row, scope.index)">解锁 </el-button>
                        </template>
                        <template slot="deptIdForm" slot-scope="scope">
                            <avue-input-tree v-model="form.deptId" :node-click="getNodeData" :dic="treeDeptData" :props="defaultProps" placeholder="请选择所属分组" />
                        </template>
                        <template slot="staffInfoForm" slot-scope="scope">
                            <el-input @focus="showStaff = true" v-model="form.staffInfo" readonly>
                                <i class="el-icon-arrow-down el-input__icon" slot="suffix" @click="showStaff = true"></i>
                            </el-input>
                        </template>
                        <template slot="roleForm" slot-scope="scope">
                            <avue-select v-model="role" :dic="rolesOptions" :props="roleProps" multiple placeholder="请选择角色" />
                        </template>
                        <template slot="passwordForm" slot-scope="scope">
                            <div class="password-input-container">
                                <el-input v-model="form.password" type="password" @input="checkPasswordStrength" placeholder="请输入密码" show-password style="width: 100%"> </el-input>
                                <div v-if="form.password" class="password-strength-indicator">
                                    密码强度: <span :class="passwordLevelClass">{{ passwordLevel }}</span>
                                </div>
                            </div>
                        </template>
                    </avue-crud>
                </el-col>
            </el-row>
        </basic-container>
    <staffDialog
      :currshowlist.sync="showStaff"
      @listenToStaffEvent="selectStaff"
    />
    <el-dialog title="导入" :visible.sync="importDialogVisible" width="30%">
      <span>
        <div>
          <div>
            <el-upload
              style="margin-left:8px;display: inline;"
              class="upload-demo"
              drag
              :headers="headers"
              :action="uploadInfo.url"
              :beforeUpload="beforeAvatarUpload"
              :limit="1"
              :show-file-list="false"
              :file-list="fileList"
              :on-success="fileSuccessUploadScan"
              :on-error="handleError"
              accept=".xlsx,.xls,.csv, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.ms-excel"
              :auto-upload="true"
              ref="uploadScan"
              multiple
            >
              <i class="el-icon-upload"></i>
              <div class="el-upload__text"><em>点击导入数据</em></div>
              <div class="el-upload__tip" slot="tip" >
                只能上传xlsx/xls文件,且不超过10M<el-button
                  type="text"
                  style="font-size:12px;"
                  @click="downDataTemplate(uploadInfo.fileName)"
                  v-if="uploadInfo.Download"
                  >下载模板</el-button
                >
              </div>
            </el-upload>
          </div>
          <div></div>
        </div>
      </span>
      <span slot="footer" class="dialog-footer"> </span>
    </el-dialog>
  </div>
        <staffDialog :currshowlist.sync="showStaff" @listenToStaffEvent="selectStaff" />
    </div>
</template>
<script>
import staffDialog from '@/views/common/staff.vue'
@@ -206,298 +89,285 @@
import { mapGetters } from 'vuex'
import Template from '../../quality/parts/template'
import { getObj } from '@/api/basic/staff'
import { getStore } from '@/util/store.js'
import { uploadTemplate } from '@/api/basic/template'
export default {
  name: 'SysUser',
  components: { Template, staffDialog },
  data() {
    return {
      fileList: [], // 上传文件列表
      // 上传头信息
      headers: {
        Authorization: 'Bearer ' + getStore({ name: 'access_token' })
      },
      uploadInfo: {
        // 是否展示上传EXCEL以及对应的url
        isShow: true,
        url: '/mes/user/upload',
        download: true,
        fileName: '用户模板'
      },
      importDialogVisible: false,
      showStaff: false,
      searchForm: {},
      treeOption: {
        nodeKey: 'id',
        addBtn: false,
        menu: false,
        props: {
          label: 'name',
          value: 'id'
    name: 'SysUser',
    components: { Template, staffDialog },
    data() {
        return {
            showStaff: false,
            searchForm: {},
            treeOption: {
                nodeKey: 'id',
                addBtn: false,
                menu: false,
                props: {
                    label: 'name',
                    value: 'id'
                }
            },
            treeData: [],
            option: tableOption,
            treeDeptData: [],
            checkedKeys: [],
            roleProps: {
                label: 'roleName',
                value: 'roleId'
            },
            defaultProps: {
                label: 'name',
                value: 'id'
            },
            page: {
                total: 0, // 总页数
                currentPage: 1, // 当前页数
                pageSize: 20, // 每页显示多少条,
                isAsc: false // 是否倒序
            },
            list: [],
            listLoading: true,
            role: [],
            form: {},
            rolesOptions: [],
            passwordLevel: '', // 密码强度级别
            passwordLevelClass: '' // 密码强度样式类
        }
      },
      treeData: [],
      option: tableOption,
      treeDeptData: [],
      checkedKeys: [],
      roleProps: {
        label: 'roleName',
        value: 'roleId'
      },
      defaultProps: {
        label: 'name',
        value: 'id'
      },
      page: {
        total: 0, // 总页数
        currentPage: 1, // 当前页数
        pageSize: 20, // 每页显示多少条,
        isAsc: false // 是否倒序
      },
      list: [],
      listLoading: true,
      role: [],
      form: {},
      rolesOptions: []
    }
  },
  computed: {
    ...mapGetters(['permissions'])
  },
  watch: {
    role() {
      this.form.role = this.role
    }
  },
  created() {
    this.sys_user_add = this.permissions.sys_user_add
    this.sys_user_edit = this.permissions.sys_user_edit
    this.sys_user_del = this.permissions.sys_user_del
    this.sys_user_lock = this.permissions.sys_user_lock
    this.init()
  },
  methods: {
    // 限制文件上传大小,目前限制为10M(另可以加类型限制)
    beforeAvatarUpload(file) {
      const fileName = file.name
      const fileType = fileName.substring(fileName.lastIndexOf('.') + 1)
      const isLt10M = file.size / 1024 / 1024 < 10
      if (fileType !== 'xlsx' && fileType !== 'xls') {
        this.$message.error('文件格式只能为xlsx或xls,请删除后重新上传')
      }
      if (!isLt10M) {
        this.$message({
          message: '文件大小',
          type: 'warning'
        })
      }
      return isLt10M
    },
    // 文件上传成功回调事件
    fileSuccessUploadScan(response, file, fileList) {
      if (response.code != '0') {
        this.$message.warning(response.msg)
      } else {
      if(response.data!=""&&response.data!=[]&&response.data!=null){
        this.$message({
          message: response.data,
          type: 'success',
          dangerouslyUseHTMLString: true,
        })
      }else{
        this.$message({
          message: '上传成功',
          type: 'success'
        })
      }
        this.importDialogVisible=false
        this.fileList=[]
    computed: {
        ...mapGetters(['permissions'])
    },
    watch: {
        role() {
            this.form.role = this.role
        }
    },
    created() {
        this.sys_user_add = this.permissions.sys_user_add
        this.sys_user_edit = this.permissions.sys_user_edit
        this.sys_user_del = this.permissions.sys_user_del
        this.sys_user_lock = this.permissions.sys_user_lock
        this.init()
        // this.getDataList()
      }
      this.$refs.uploadScan.clearFiles()
    },
    // 上传失败
    handleError(err, file, fileList) {
      const error = JSON.parse(err.message)
      if (error.msg) {
        this.$message.error(error.msg)
      } else {
        this.$message.error('上传失败')
      }
    },
    // 下载数据模板
    downDataTemplate() {
      uploadTemplate("user").then((response) => {
        const blob = new Blob([response.data], {
          type: 'application/force-download'
        })
        let fileName="模板文件";
        if(this.uploadInfo.fileName!=undefined&&this.uploadInfo.fileName!=''&&this.uploadInfo.fileName!=null){
            fileName=this.uploadInfo.fileName
        }
        const filename = decodeURI(fileName+'.xlsx')
        // 创建一个超链接,将文件流赋进去,然后实现这个超链接的单击事件
        const elink = document.createElement('a')
        elink.download = filename
        elink.style.display = 'none'
        elink.href = URL.createObjectURL(blob)
        document.body.appendChild(elink)
        elink.click()
        URL.revokeObjectURL(elink.href) // 释放URL 对象
        document.body.removeChild(elink)
      })
    },
    selectStaff(staff) {
      this.form.staffInfo = staff.staffName + '-' + staff.staffNo
      this.form.phone = staff.phone
      this.form.staffId = staff.id
    },
    init() {
      fetchTree().then((response) => {
        this.treeData = response.data.data
      })
    },
    nodeClick(data) {
      this.page.page = 1
      this.getList(this.page, { deptId: data.id })
    },
    getList(page, params) {
      this.listLoading = true
      fetchList(
        Object.assign(
          {
            current: page.currentPage,
            size: page.pageSize
          },
          params,
          this.searchForm
        )
      ).then((response) => {
        this.list = response.data.data.records
        this.page.total = response.data.data.total
        this.listLoading = false
      })
    },
    getNodeData() {
      deptRoleList().then((response) => {
        this.rolesOptions = response.data.data
      })
    },
    searchChange(param, done) {
      this.searchForm = param
      this.page.currentPage = 1
      this.getList(this.page, param)
      done()
    },
    sizeChange(pageSize) {
      this.page.pageSize = pageSize
    },
    currentChange(current) {
      this.page.currentPage = current
    },
    refreshChange() {
      this.getList(this.page)
    },
    handleOpenBefore(show, type) {
      window.boxType = type
      // 查询部门树
      fetchTree().then((response) => {
        this.treeDeptData = response.data.data
      })
      // 查询角色列表
      deptRoleList().then((response) => {
        this.rolesOptions = response.data.data
      })
    methods: {
        selectStaff(staff) {
            this.form.staffInfo = staff.staffName + '-' + staff.staffNo
            this.form.phone = staff.phone
            this.form.staffId = staff.id
        },
        init() {
            fetchTree().then((response) => {
                this.treeData = response.data.data
            })
        },
        nodeClick(data) {
            this.page.page = 1
            this.getList(this.page, { deptId: data.id })
        },
        getList(page, params) {
            this.listLoading = true
            fetchList(
                Object.assign(
                    {
                        current: page.currentPage,
                        size: page.pageSize
                    },
                    params,
                    this.searchForm
                )
            ).then((response) => {
                this.list = response.data.data.records
                this.page.total = response.data.data.total
                this.listLoading = false
            })
        },
        getNodeData() {
            deptRoleList().then((response) => {
                this.rolesOptions = response.data.data
            })
        },
        searchChange(param, done) {
            this.searchForm = param
            this.page.currentPage = 1
            this.getList(this.page, param)
            done()
        },
        sizeChange(pageSize) {
            this.page.pageSize = pageSize
        },
        currentChange(current) {
            this.page.currentPage = current
        },
        refreshChange() {
            this.getList(this.page)
        },
        handleOpenBefore(show, type) {
            window.boxType = type
            // 查询部门树
            fetchTree().then((response) => {
                this.treeDeptData = response.data.data
            })
            // 查询角色列表
            deptRoleList().then((response) => {
                this.rolesOptions = response.data.data
            })
      if (['edit', 'views'].includes(type)) {
        this.role = []
        for (let i = 0; i < this.form.roleList.length; i++) {
          this.role[i] = this.form.roleList[i].roleId
        }
      } else if (type === 'add') {
        this.role = []
      }
      show()
    },
    handleUpdate(row, index) {
      this.$refs.crud.rowEdit(row, index)
      this.form.password = undefined
    },
            if (['edit', 'views'].includes(type)) {
                this.role = []
                for (let i = 0; i < this.form.roleList.length; i++) {
                    this.role[i] = this.form.roleList[i].roleId
                }
            } else if (type === 'add') {
                this.role = []
            }
            show()
        },
        handleUpdate(row, index) {
            this.$refs.crud.rowEdit(row, index)
            this.form.password = undefined
        },
    create(row, done, loading) {
      if (this.form.phone.indexOf('*') > 0) {
        this.form.phone = undefined
      }
      addObj(this.form)
        .then(() => {
          this.getList(this.page)
          done()
          this.$notify.success('创建成功')
        })
        .catch(() => {
          loading()
        })
    },
    update(row, index, done, loading) {
      if (this.form.phone && this.form.phone.indexOf('*') > 0) {
        this.form.phone = undefined
      }
      putObj(this.form)
        .then(() => {
          this.getList(this.page)
          done()
          this.$notify.success('修改成功')
        })
        .catch(() => {
          loading()
        })
    },
    deletes(row, index) {
      this.$confirm(
        '此操作将永久删除该用户(用户名:' + row.username + '), 是否继续?',
        '提示',
        {
          confirmButtonText: '确定',
          cancelButtonText: '取消',
          closeOnClickModal: false,
          type: 'warning'
        create(row, done, loading) {
            // 检查密码强度
            if (this.passwordLevel !== '强') {
                this.$message.warning('密码强度不够,请确保密码包含:大小写字母、数字和特殊字符,且长度不少于8位')
                loading()
                return
            }
            if (this.form.phone.indexOf('*') > 0) {
                this.form.phone = undefined
            }
            addObj(this.form)
                .then(() => {
                    this.getList(this.page)
                    done()
                    this.$notify.success('创建成功')
                })
                .catch(() => {
                    loading()
                })
        },
        update(row, index, done, loading) {
            // 如果修改了密码,也要检查密码强度
            if (this.form.password && this.passwordLevel !== '强') {
                this.$message.warning('密码强度不够,请确保密码包含:大小写字母、数字和特殊字符,且长度不少于8位')
                loading()
                return
            }
            if (this.form.phone && this.form.phone.indexOf('*') > 0) {
                this.form.phone = undefined
            }
            putObj(this.form)
                .then(() => {
                    this.getList(this.page)
                    done()
                    this.$notify.success('修改成功')
                })
                .catch(() => {
                    loading()
                })
        },
        deletes(row, index) {
            this.$confirm('此操作将永久删除该用户(用户名:' + row.username + '), 是否继续?', '提示', {
                confirmButtonText: '确定',
                cancelButtonText: '取消',
                closeOnClickModal: false,
                type: 'warning'
            }).then(() => {
                delObj(row.userId)
                    .then(() => {
                        this.list.splice(index, 1)
                        this.$notify.success('删除成功')
                    })
                    .catch(() => {
                        this.$notify.error('删除失败')
                    })
            })
        },
        unlock(row, index) {
            unlock({ id: row.userId }).then((repsonse) => {})
        },
        // 检查密码强度
        checkPasswordStrength(password) {
            let strength = 0
            // 检查长度
            if (password.length >= 8) strength++
            // 检查是否包含数字
            if (/\d/.test(password)) strength++
            // 检查是否包含小写字母
            if (/[a-z]/.test(password)) strength++
            // 检查是否包含大写字母
            if (/[A-Z]/.test(password)) strength++
            // 检查是否包含特殊字符
            if (/[!@#$%^&*]/.test(password)) strength++
            switch (strength) {
                case 0:
                case 1:
                    this.passwordLevel = '弱'
                    this.passwordLevelClass = 'password-weak'
                    break
                case 2:
                case 3:
                    this.passwordLevel = '中'
                    this.passwordLevelClass = 'password-medium'
                    break
                case 4:
                case 5:
                    this.passwordLevel = '强'
                    this.passwordLevelClass = 'password-strong'
                    break
            }
        }
      ).then(() => {
        delObj(row.userId)
          .then(() => {
            this.list.splice(index, 1)
            this.$notify.success('删除成功')
          })
          .catch(() => {
            this.$notify.error('删除失败')
          })
      })
    },
    unlock(row, index) {
      unlock({ id: row.userId }).then((repsonse) => {
        if(repsonse.data.data){
          this.$message.success("该用户已解锁")
        }
      })
    }
  }
}
</script>
<style lang="scss">
.user {
  height: 100%;
    height: 100%;
  &__tree {
    padding-top: 3px;
    padding-right: 20px;
  }
  &__main {
    .el-card__body {
      padding-top: 0;
    &__tree {
        padding-top: 3px;
        padding-right: 20px;
    }
  }
    &__main {
        .el-card__body {
            padding-top: 0;
        }
    }
}
.password-input-container {
    width: 100%;
    .el-input {
        width: 100%;
    }
}
.password-strength-indicator {
    margin-top: 5px;
    font-size: 12px;
    color: #606266;
}
.password-weak {
    color: #f56c6c;
}
.password-medium {
    color: #e6a23c;
}
.password-strong {
    color: #67c23a;
}
</style>
src/views/admin/user/info.vue
@@ -16,269 +16,314 @@
  -->
<template>
  <div class="app-container calendar-list-container">
    <basic-container>
      <template>
        <el-tabs v-model="switchStatus" @tab-click="switchTab" style="padding-left:20px">
          <el-tab-pane label="信息管理" name="userManager"/>
          <el-tab-pane label="密码管理" name="passwordManager"/>
        </el-tabs>
      </template>
      <el-row>
        <el-col :span="12">
          <div class="grid-content bg-purple">
            <el-form
              v-if="switchStatus==='userManager'"
              ref="ruleForm2"
              :model="ruleForm2"
              :rules="rules2"
              label-width="100px"
              class="demo-ruleForm">
              <el-form-item
                label="用户名"
                prop="username">
                <el-input
                  v-model="ruleForm2.username"
                  type="text"
                  disabled/>
              </el-form-item>
              <el-form-item label="手机号" prop="phone">
                <el-input v-model="ruleForm2.phone" placeholder="验证码登录使用"/>
              </el-form-item>
              <el-form-item label="头像">
                <el-upload
                  :headers="headers"
                  :show-file-list="false"
                  :on-success="handleAvatarSuccess"
                  class="avatar-uploader"
                  action="/admin/sys-file/upload">
                  <img v-if="ruleForm2.avatar" id="avatar" :src="avatarUrl" class="avatar">
                  <i v-else class="el-icon-plus avatar-uploader-icon"/>
                </el-upload>
              </el-form-item>
              <!-- <el-form-item
                label="社交登录"
                prop="social">
                <a
                  href="#"
                  style="color: blue"
                  @click="handleClick('wechat')">绑定微信</a>|
                <a
                  href="#"
                  style="color: blue"
                  @click="handleClick('gitee')">绑定码云</a> |
                <a
                  href="#"
                  style="color: blue"
                  @click="handleClick('osc')">开源中国</a>
              </el-form-item> -->
              <el-form-item>
                <el-button
                  type="primary"
                  @click="submitForm('ruleForm2')">提交
                </el-button>
                <el-button @click="resetForm('ruleForm2')">重置</el-button>
              </el-form-item>
            </el-form>
            <el-form
              v-if="switchStatus==='passwordManager'"
              ref="ruleForm2"
              :model="ruleForm2"
              :rules="rules2"
              label-width="100px"
              class="demo-ruleForm">
              <el-form-item
                label="原密码"
                prop="password">
                <el-input
                  v-model="ruleForm2.password"
                  type="password"
                  auto-complete="off"/>
              </el-form-item>
              <el-form-item
                label="密码"
                prop="newpassword1">
                <el-input
                  v-model="ruleForm2.newpassword1"
                  type="password"
                  auto-complete="off"/>
              </el-form-item>
              <el-form-item
                label="确认密码"
                prop="newpassword2">
                <el-input
                  v-model="ruleForm2.newpassword2"
                  type="password"
                  auto-complete="off"/>
              </el-form-item>
              <el-form-item>
                <el-button
                  type="primary"
                  @click="submitForm('ruleForm2')">提交
                </el-button>
                <el-button @click="resetForm('ruleForm2')">重置</el-button>
              </el-form-item>
            </el-form>
          </div>
        </el-col>
      </el-row>
    </basic-container>
  </div>
    <div class="app-container calendar-list-container">
        <basic-container>
            <template>
                <el-tabs @tab-click="switchTab">
                    <el-tab-pane label="信息管理" name="userManager" />
                    <el-tab-pane label="密码管理" name="passwordManager" />
                </el-tabs>
            </template>
            <el-row>
                <el-col :span="12">
                    <div class="grid-content bg-purple">
                        <el-form v-if="switchStatus === 'userManager'" ref="ruleForm2" :model="ruleForm2" :rules="rules2" label-width="100px" class="demo-ruleForm">
                            <el-form-item label="用户名" prop="username">
                                <el-input v-model="ruleForm2.username" type="text" disabled />
                            </el-form-item>
                            <el-form-item label="手机号" prop="phone">
                                <el-input v-model="ruleForm2.phone" placeholder="验证码登录使用" />
                            </el-form-item>
                            <el-form-item label="头像">
                                <el-upload :headers="headers" :show-file-list="false" :on-success="handleAvatarSuccess" class="avatar-uploader" action="/admin/sys-file/upload">
                                    <img v-if="ruleForm2.avatar" id="avatar" :src="avatarUrl" class="avatar" />
                                    <i v-else class="el-icon-plus avatar-uploader-icon" />
                                </el-upload>
                            </el-form-item>
                            <el-form-item label="社交登录" prop="social">
                                <a href="#" style="color: blue" @click="handleClick('wechat')">绑定微信</a>| <a href="#" style="color: blue" @click="handleClick('gitee')">绑定码云</a> |
                                <a href="#" style="color: blue" @click="handleClick('osc')">开源中国</a>
                            </el-form-item>
                            <el-form-item>
                                <el-button type="primary" @click="submitForm('ruleForm2')">提交 </el-button>
                                <el-button @click="resetForm('ruleForm2')">重置</el-button>
                            </el-form-item>
                        </el-form>
                        <el-form v-if="switchStatus === 'passwordManager'" ref="ruleForm2" :model="ruleForm2" :rules="rules2" label-width="100px" class="demo-ruleForm">
                            <el-form-item label="原密码" prop="password">
                                <el-input v-model="ruleForm2.password" type="password" auto-complete="off" />
                            </el-form-item>
                            <el-form-item label="密码" prop="newpassword1">
                                <div class="password-input-container">
                                    <el-input v-model="ruleForm2.newpassword1" type="password" @input="checkPasswordStrength" show-password auto-complete="off" />
                                    <div v-if="ruleForm2.newpassword1" class="password-strength-indicator">
                                        密码强度: <span :class="passwordLevelClass">{{ passwordLevel }}</span>
                                    </div>
                                </div>
                            </el-form-item>
                            <el-form-item label="确认密码" prop="newpassword2">
                                <el-input v-model="ruleForm2.newpassword2" type="password" auto-complete="off" />
                            </el-form-item>
                            <el-form-item>
                                <el-button type="primary" @click="submitForm('ruleForm2')">提交 </el-button>
                                <el-button @click="resetForm('ruleForm2')">重置</el-button>
                            </el-form-item>
                        </el-form>
                    </div>
                </el-col>
            </el-row>
        </basic-container>
    </div>
</template>
<script>
  import {handleImg, openWindow} from '@/util/util'
  import {mapState} from 'vuex'
  import store from '@/store'
  import {getStore, setStore} from '@/util/store'
  import {editInfo} from '@/api/admin/user'
import { handleImg, openWindow } from '@/util/util'
import { mapState } from 'vuex'
import store from '@/store'
import { getStore, setStore } from '@/util/store'
import { editInfo } from '@/api/admin/user'
  export default {
export default {
    data() {
      var validatePass = (rule, value, callback) => {
        if (this.ruleForm2.password !== '') {
          if (value !== this.ruleForm2.newpassword1) {
            callback(new Error('两次输入密码不一致!'))
          } else {
            callback()
          }
        } else {
          callback()
        // 密码强度校验
        const validatePasswordStrength = (rule, value, callback) => {
            let strength = 0
            if (value.length >= 8) strength++
            if (/\d/.test(value)) strength++
            if (/[a-z]/.test(value)) strength++
            if (/[A-Z]/.test(value)) strength++
            if (/[!@#$%^&*]/.test(value)) strength++
            if (strength < 4) {
                callback(new Error('密码强度不够,请确保密码包含:大小写字母、数字和特殊字符,且长度不少于8位'))
            } else {
                callback()
            }
        }
      }
      return {
        switchStatus: 'userManager',
        avatarUrl: '',
        show: false,
        headers: {
          'Authorization': 'Bearer ' + store.getters.access_token,
        },
        ruleForm2: {
          username: '',
          password: '',
          newpassword1: '',
          newpassword2: '',
          avatar: '',
          phone: ''
        },
        rules2: {
          password: [{required: true, min: 6, message: '原密码不能为空且不少于6位', trigger: 'change'}],
          newpassword1: [{required: false, min: 6, message: '不少于6位', trigger: 'change'}],
          newpassword2: [{required: false, validator: validatePass, trigger: 'blur'}]
        // 确认密码校验
        var validatePass = (rule, value, callback) => {
            if (this.ruleForm2.password !== '') {
                if (value !== this.ruleForm2.newpassword1) {
                    callback(new Error('两次输入密码不一致!'))
                } else {
                    callback()
                }
            } else {
                callback()
            }
        }
      }
        return {
            switchStatus: 'userManager',
            avatarUrl: '',
            show: false,
            headers: {
                Authorization: 'Bearer ' + store.getters.access_token
            },
            ruleForm2: {
                username: '',
                password: '',
                newpassword1: '',
                newpassword2: '',
                avatar: '',
                phone: ''
            },
            passwordLevel: '', // 密码强度级别
            passwordLevelClass: '', // 密码强度样式类
            rules2: {
                password: [
                    {
                        required: true,
                        min: 8,
                        message: '原密码不能为空且不少于8位',
                        trigger: 'change'
                    }
                ],
                newpassword1: [{ required: true, message: '请输入新密码', trigger: 'blur' }, { validator: validatePasswordStrength, trigger: 'blur' }],
                newpassword2: [
                    {
                        required: true,
                        validator: validatePass,
                        trigger: 'blur'
                    }
                ]
            }
        }
    },
    created() {
      this.resetForm()
        this.resetForm()
    },
    computed: {
      ...mapState({
        userInfo: state => state.user.userInfo
      })
        ...mapState({
            userInfo: (state) => state.user.userInfo
        })
    },
    methods: {
      switchTab(tab) {
        if (tab.name === 'userManager') {
          handleImg(this.ruleForm2.avatar, 'avatar')
        }
        this.switchStatus = tab.name
      },
      submitForm(formName) {
        this.$refs[formName].validate(valid => {
          if (!valid) {
            return false
          }
          editInfo(this.ruleForm2).then(response => {
            this.handleLocalData(this.ruleForm2)
            this.$notify.success('修改成功')
            // 修改密码之后强制重新登录
            if (this.switchStatus === 'passwordManager') {
              this.$store.dispatch('LogOut').then(() => {
                location.reload() // 为了重新实例化vue-router对象 避免bug
              })
        switchTab(tab) {
            if (tab.name === 'userManager') {
                handleImg(this.ruleForm2.avatar, 'avatar')
            }
          })
        })
      },
      resetForm() {
        this.ruleForm2.password = undefined
        this.ruleForm2.newpassword1 = undefined
        this.ruleForm2.newpassword2 = undefined
        this.ruleForm2.username = this.userInfo.username
        this.ruleForm2.phone = this.userInfo.phone
        this.ruleForm2.avatar = this.userInfo.avatar
        handleImg(this.userInfo.avatar, 'avatar')
        //判断是否选择了租户ID
        const TENANT_ID = getStore({name: 'tenantId'})
        if (TENANT_ID) {
          this.headers['TENANT-ID'] = TENANT_ID // 租户ID
        }
      },
      handleClick(thirdpart) {
        let appid, client_id, redirect_uri, url
        redirect_uri = encodeURIComponent(window.location.origin + '/#/authredirect')
        if (thirdpart === 'wechat') {
          appid = 'wxd1678d3f83b1d83a'
          url = 'https://open.weixin.qq.com/connect/qrconnect?appid=' + appid + '&redirect_uri=' + redirect_uri + '&state=WX-BIND&response_type=code&scope=snsapi_login#wechat_redirect'
        } else if (thirdpart === 'tencent') {
          client_id = '101322838'
          url = 'https://graph.qq.com/oauth2.0/authorize?response_type=code&state=QQ-BIND&client_id=' + client_id + '&redirect_uri=' + redirect_uri
        } else if (thirdpart === 'gitee') {
          client_id = '235ce26bbc59565b82c989aa3a407ce844cf59a7c5e0f9caa9bb3bf32cee5952'
          url = 'https://gitee.com/oauth/authorize?response_type=code&state=GITEE-BIND&client_id=' + client_id + '&redirect_uri=' + redirect_uri
        } else if (thirdpart === 'osc') {
          client_id = 'neIIqlwGsjsfsA6uxNqD'
          url = 'https://www.oschina.net/action/oauth2/authorize?response_type=code&client_id=' + client_id + '&state=OSC-BIND&redirect_uri=' + redirect_uri
        }
        openWindow(url, thirdpart, 540, 540)
      },
      handleAvatarSuccess(res, file) {
        this.avatarUrl = URL.createObjectURL(file.raw)
        this.ruleForm2.avatar = res.data.url
      },
      // 处理本地数据,避免刷新不同步
      handleLocalData(form) {
        let userInfo = getStore({name: 'userInfo'})
            this.switchStatus = tab.name
        },
        submitForm(formName) {
            this.$refs[formName].validate((valid) => {
                if (!valid) {
                    return false
                }
                editInfo(this.ruleForm2).then((response) => {
                    this.handleLocalData(this.ruleForm2)
                    this.$notify.success('修改成功')
                    // 修改密码之后强制重新登录
                    if (this.switchStatus === 'passwordManager') {
                        this.$store.dispatch('LogOut').then(() => {
                            location.reload() // 为了重新实例化vue-router对象 避免bug
                        })
                    }
                })
            })
        },
        resetForm() {
            this.ruleForm2.password = undefined
            this.ruleForm2.newpassword1 = undefined
            this.ruleForm2.newpassword2 = undefined
            this.ruleForm2.username = this.userInfo.username
            this.ruleForm2.phone = this.userInfo.phone
            this.ruleForm2.avatar = this.userInfo.avatar
            handleImg(this.userInfo.avatar, 'avatar')
            //判断是否选择了租户ID
            const TENANT_ID = getStore({ name: 'tenantId' })
            if (TENANT_ID) {
                this.headers['TENANT-ID'] = TENANT_ID // 租户ID
            }
        },
        handleClick(thirdpart) {
            let appid, client_id, redirect_uri, url
            redirect_uri = encodeURIComponent(window.location.origin + '/#/authredirect')
            if (thirdpart === 'wechat') {
                appid = 'wxd1678d3f83b1d83a'
                url = 'https://open.weixin.qq.com/connect/qrconnect?appid=' + appid + '&redirect_uri=' + redirect_uri + '&state=WX-BIND&response_type=code&scope=snsapi_login#wechat_redirect'
            } else if (thirdpart === 'tencent') {
                client_id = '101322838'
                url = 'https://graph.qq.com/oauth2.0/authorize?response_type=code&state=QQ-BIND&client_id=' + client_id + '&redirect_uri=' + redirect_uri
            } else if (thirdpart === 'gitee') {
                client_id = '235ce26bbc59565b82c989aa3a407ce844cf59a7c5e0f9caa9bb3bf32cee5952'
                url = 'https://gitee.com/oauth/authorize?response_type=code&state=GITEE-BIND&client_id=' + client_id + '&redirect_uri=' + redirect_uri
            } else if (thirdpart === 'osc') {
                client_id = 'neIIqlwGsjsfsA6uxNqD'
                url = 'https://www.oschina.net/action/oauth2/authorize?response_type=code&client_id=' + client_id + '&state=OSC-BIND&redirect_uri=' + redirect_uri
            }
            openWindow(url, thirdpart, 540, 540)
        },
        handleAvatarSuccess(res, file) {
            this.avatarUrl = URL.createObjectURL(file.raw)
            this.ruleForm2.avatar = res.data.url
        },
        // 处理本地数据,避免刷新不同步
        handleLocalData(form) {
            let userInfo = getStore({ name: 'userInfo' })
        if (userInfo) {
          userInfo.avatar = form.avatar
          userInfo.phone = form.phone
          setStore({
            name: 'userInfo',
            content: userInfo,
            type: 'session'
          })
            if (userInfo) {
                userInfo.avatar = form.avatar
                userInfo.phone = form.phone
                setStore({
                    name: 'userInfo',
                    content: userInfo,
                    type: 'session'
                })
            }
        },
        // 检查密码强度
        checkPasswordStrength(password) {
            let strength = 0
            // 检查长度
            if (password.length >= 8) strength++
            // 检查是否包含数字
            if (/\d/.test(password)) strength++
            // 检查是否包含小写字母
            if (/[a-z]/.test(password)) strength++
            // 检查是否包含大写字母
            if (/[A-Z]/.test(password)) strength++
            // 检查是否包含特殊字符
            if (/[!@#$%^&*]/.test(password)) strength++
            switch (strength) {
                case 0:
                case 1:
                    this.passwordLevel = '弱'
                    this.passwordLevelClass = 'password-weak'
                    break
                case 2:
                case 3:
                    this.passwordLevel = '中'
                    this.passwordLevelClass = 'password-medium'
                    break
                case 4:
                case 5:
                    this.passwordLevel = '强'
                    this.passwordLevelClass = 'password-strong'
                    break
            }
        }
      }
    }
  }
}
</script>
<style>
  .avatar-uploader .el-upload {
.avatar-uploader .el-upload {
    border: 1px dashed #d9d9d9;
    border-radius: 6px;
    cursor: pointer;
    position: relative;
    overflow: hidden;
  }
}
  .avatar-uploader .el-upload:hover {
    border-color: #409EFF;
  }
.avatar-uploader .el-upload:hover {
    border-color: #409eff;
}
  .avatar-uploader-icon {
.avatar-uploader-icon {
    font-size: 28px !important;
    color: #8c939d !important;
    width: 178px !important;
    height: 178px !important;
    line-height: 178px !important;
    text-align: center !important;
  }
}
  .avatar {
.avatar {
    width: 178px;
    height: 178px;
    display: block;
  }
}
.password-input-container {
    width: 100%;
    .el-input {
        width: 100%;
    }
}
.password-strength-indicator {
    margin-top: 5px;
    font-size: 12px;
    color: #606266;
}
.password-weak {
    color: #f56c6c;
}
.password-medium {
    color: #e6a23c;
}
.password-strong {
    color: #67c23a;
}
</style>