zouyu
2026-05-07 b0d4df5f39525ae7fe252e8ee65d85fd71dca721
src/views/system/user/index.vue
@@ -47,7 +47,12 @@
            </div>
          </div>
          <el-col>
            <el-table v-loading="loading" :data="userList" :header-cell-style="{ background: '#f8f8f9', color: '#515a6e' }" border>
            <el-table ref="dragTable" v-loading="loading" row-key="userId" :data="userList" :header-cell-style="{ background: '#f8f8f9', color: '#515a6e' }" border>
              <el-table-column label="拖拽" align="center" width="60">
                <template slot-scope="scope">
                  <i class="el-icon-rank drag-handle" :data-user-id="scope.row.userId"></i>
                </template>
              </el-table-column>
              <el-table-column label="序号" align="center" type="index" />
              <el-table-column label="姓名" align="center" key="nickName" prop="nickName" :show-overflow-tooltip="true" />
              <el-table-column label="账号" align="center" key="userName" prop="userName" :show-overflow-tooltip="true" />
@@ -75,6 +80,7 @@
              </el-table-column>
            </el-table>
            <pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNum"
                        :page-sizes="[20,50,100,200,500]"
              :limit.sync="queryParams.pageSize" @pagination="getList" />
          </el-col>
        </pane>
@@ -114,48 +120,61 @@
        </el-row>
        <el-row>
          <el-col :span="12">
            <el-form-item label="岗位">
              <el-select style="width:100%" v-model="form.postIds" multiple placeholder="请选择">
                <el-option
                  v-for="item in postOptions"
                  :key="item.postId"
                  :label="item.postName"
                  :value="item.postId"
                  :disabled="item.status == 1"
                ></el-option>
              </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="角色" prop="roleIds">
              <el-select v-model="form.roleIds" multiple placeholder="请选择角色" clearable>
              <el-select style="width:100%" v-model="form.roleIds" multiple placeholder="请选择角色" clearable>
                <el-option v-for="item in roleOptions" :key="item.roleId" :label="item.roleName" :value="item.roleId"
                  :disabled="item.status == 1"></el-option>
              </el-select>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row>
          <el-col :span="12">
            <el-form-item label="密码" prop="password">
              <el-input v-model="form.password" placeholder="请输入用户密码" type="password" maxlength="20" show-password />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row>
          <el-col :span="12">
            <el-form-item label="姓名EN" prop="nameEn">
              <el-input v-model="form.nameEn" placeholder="请输入姓名EN" maxlength="50" />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row>
          <el-col :span="12">
            <el-form-item label="邮箱" prop="email">
              <el-input v-model="form.email" placeholder="请输入内容"></el-input>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row>
          <el-col :span="12">
            <el-form-item label="单位" prop="company">
              <el-select v-model="form.company" placeholder="请选择单位" style="width: 100%" clearable>
                <el-option v-for="item in postOptions" :key="item.id" :label="item.company"
                <el-option v-for="item in companyOptions" :key="item.id" :label="item.company"
                  :value="item.id"></el-option>
              </el-select>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row>
          <el-col :span="12">
            <el-form-item label="归属部门" prop="deptId">
              <treeselect v-model="form.deptId" :options="enabledDeptOptions" :show-count="true"
                placeholder="请选择归属部门" />
                          placeholder="请选择归属部门" />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row>
          <el-col :span="12">
            <el-form-item label="签名">
              <el-upload class="avatar-uploader" :action="uploadAction" :show-file-list="false"
@@ -166,11 +185,13 @@
              </el-upload>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row>
          <el-col :span="12">
            <el-form-item label="个人照片">
              <el-upload class="avatar-uploader" :action="uploadAction" :show-file-list="false"
                :headers="upload.headers" accept=".png, .jpg, .jpeg, .gif" :on-error="handleUploadError1"
                :on-success="handleUploadSuccess1" :before-upload="handleBeforeUpload1">
                         :headers="upload.headers" accept=".png, .jpg, .jpeg, .gif" :on-error="handleUploadError1"
                         :on-success="handleUploadSuccess1" :before-upload="handleBeforeUpload1">
                <img v-if="form.pictureUrl" :src="javaApi + '/img/' + form.pictureUrl" class="avatar" alt="">
                <i v-else class="el-icon-plus avatar-uploader-icon"></i>
              </el-upload>
@@ -278,13 +299,22 @@
  resetUserPwd,
  changeUserStatus,
  deptTreeSelect,
  selectCompaniesList, selectSimpleList, addPersonUser, uploadFile, selectRoleList, selectCustomEnum, addDepartment
  selectCompaniesList,
  selectSimpleList,
  addPersonUser,
  uploadFile,
  selectRoleList,
  selectCustomEnum,
  addDepartment,
  updateUserSort
} from "@/api/system/user";
import {optionSelect} from '@/api/system/post'
import { getToken } from "@/utils/auth";
import Treeselect from "@riophae/vue-treeselect";
import "@riophae/vue-treeselect/dist/vue-treeselect.css";
import { Splitpanes, Pane } from "splitpanes";
import "splitpanes/dist/splitpanes.css";
import Sortable from "sortablejs";
export default {
  nickName: "User",
@@ -323,6 +353,8 @@
      dateRange: [],
      // 岗位选项
      postOptions: [],
      //单位选项
      companyOptions:[],
      // 角色选项
      roleOptions: [],
      // 表单参数
@@ -354,7 +386,7 @@
      // 查询参数
      queryParams: {
        pageNum: 1,
        pageSize: 10,
        pageSize: 20,
        nickName: undefined,
        phonenumber: undefined,
        status: undefined,
@@ -387,9 +419,35 @@
        roleIds: [
          { required: true, message: "请选择角色", trigger: "change" }
        ],
        // password: [
        //   { required: true, message: "密码不能为空", trigger: "blur" },
        // ],
        password: [
          { required: false, message: "密码不能为空", trigger: "blur" },
          { min: 8, max: 20, message: "密码长度必须在8-20个字符之间", trigger: "blur" },
          {
            validator: (rule, value, callback) => {
              if (!value) {
                callback();
                return;
              }
              // 检查是否包含大写字母
              const hasUpperCase = /[A-Z]/.test(value);
              // 检查是否包含小写字母
              const hasLowerCase = /[a-z]/.test(value);
              // 检查是否包含特殊符号
              const hasSpecialChar = /[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/.test(value);
              if (!hasUpperCase) {
                callback(new Error('密码必须包含至少一个大写字母'));
              } else if (!hasLowerCase) {
                callback(new Error('密码必须包含至少一个小写字母'));
              } else if (!hasSpecialChar) {
                callback(new Error('密码必须包含至少一个特殊符号'));
              } else {
                callback();
              }
            },
            trigger: "blur"
          }
        ],
        phonenumber: [
          {
            required: true,
@@ -417,6 +475,8 @@
        fatherId: 10001,
        nickName: '',
      },
      sortTable: null,
      sortSaving: false,
    };
  },
  watch: {
@@ -432,16 +492,103 @@
      this.initPassword = response.msg;
    });
  },
  beforeDestroy() {
    this.destroyDrag()
  },
  methods: {
    // 表格行拖拽排序
    destroyDrag() {
      if (this.sortTable) {
        this.sortTable.destroy()
        this.sortTable = null
      }
    },
    syncDragDisabledState() {
      if (this.sortTable) {
        this.sortTable.option('disabled', this.loading || this.sortSaving)
      }
    },
    initDrag() {
      this.destroyDrag()
      if (!this.$refs.dragTable || !this.userList || this.userList.length === 0) {
        return
      }
      // 获取 el-table 的 tbody 元素(拖拽的目标容器)
      const tbody = this.$refs.dragTable.$el.querySelector(
        '.el-table__body-wrapper tbody'
      )
      if (!tbody) {
        return
      }
      // 初始化 Sortable
      this.sortTable = Sortable.create(tbody, {
        animation: 150, // 拖拽动画过渡时长
        ghostClass: 'sortable-ghost', // 拖拽占位符样式
        chosenClass: 'sortable-chosen', // 选中行样式
        dragClass: 'sortable-drag', // 拖拽元素样式
        handle: '.drag-handle',
        disabled: this.loading || this.sortSaving,
        // 拖拽结束触发(核心逻辑)
        onEnd: async({ oldIndex, newIndex }) => {
          if (
            this.loading ||
            this.sortSaving ||
            oldIndex === newIndex ||
            oldIndex === undefined ||
            newIndex === undefined
          ) {
            return
          }
          const previousList = [...this.userList]
          const row = this.userList.splice(oldIndex, 1)[0]
          if (!row) {
            this.userList = previousList
            return
          }
          this.userList.splice(newIndex, 0, row)
          const pageOffset =
            (this.queryParams.pageNum - 1) * this.queryParams.pageSize
          const data = this.userList.map((item, index) => ({
            id: item.userId,
            sort: pageOffset + index + 1
          }))
          this.sortSaving = true
          this.syncDragDisabledState()
          try {
            await updateUserSort(data)
            this.$message.success('更新排序成功')
          } catch (error) {
            this.userList = previousList
            this.$message.error('更新排序失败')
          } finally {
            this.sortSaving = false
            this.$nextTick(() => {
              this.syncDragDisabledState()
            })
          }
        }
      })
    },
    /** 查询用户列表 */
    getList() {
      this.loading = true;
      this.loading = true
      this.syncDragDisabledState()
      listUser(this.addDateRange(this.queryParams, this.dateRange)).then(response => {
        this.userList = response.rows;
        this.total = response.total;
        this.loading = false;
      }
      );
        this.userList = response.rows
        this.total = response.total
        this.loading = false
        this.$nextTick(() => {
          this.initDrag()
        })
      }).catch(() => {
        this.loading = false
        this.destroyDrag()
      })
    },
    // 打开添加架构弹框
    addSchema() {
@@ -677,10 +824,11 @@
      this.reset();
      this.open = true;
      selectCustomEnum().then(res => {
        this.postOptions = res.data;
        this.companyOptions = res.data;
      })
      getUser().then(response => {
        this.roleOptions = response.roles;
        this.postOptions = response.posts
        this.title = "添加用户";
      });
    },
@@ -688,7 +836,7 @@
    handleUpdate(row) {
      this.reset();
      selectCustomEnum().then(res => {
        this.postOptions = res.data;
        this.companyOptions = res.data;
      })
      const userId = row.userId || this.ids;
      getUser(userId).then(response => {
@@ -696,6 +844,8 @@
        this.form.password = ''
        this.roleOptions = response.roles;
        this.$set(this.form, "roleIds", response.roleIds);
        this.postOptions = response.posts
        this.$set(this.form, "postIds", response.postIds);
        this.open = true;
        this.title = "修改用户";
      });
@@ -856,6 +1006,28 @@
</script>
<style scoped lang="scss">
:deep(.drag-handle) {
  cursor: grab;
  color: #909399;
  font-size: 16px;
  display: inline-block;
  user-select: none;
}
:deep(.drag-handle:hover) {
  color: #409EFF;
  cursor: grab;
}
:deep(.drag-handle:active) {
  cursor: grabbing;
}
:deep(.sortable-ghost) {
  opacity: 0.8;
  background: #f0f9eb;
}
:deep(.sortable-chosen) {
  cursor: move;
  background: #e1f3d8;
}
.search_form {
  display: flex;
  justify-content: space-between;