puzhibing
2026-08-28 e92cd56bb56b111c1afe9922191f9897215ff6b0
frontend-admin/src/views/UsersView.vue
@@ -1,11 +1,12 @@
<template>
  <div>
  <div class="page">
    <h2 class="page-title">平台管理员</h2>
    <el-form :inline="true" class="search-form" @submit.prevent="doSearch">
      <el-form-item label="姓名">
        <el-input v-model="filters.realName" placeholder="模糊查询" clearable style="width: 140px" />
        <el-input v-model="filters.realName" placeholder="请输入姓名" clearable style="width: 140px" />
      </el-form-item>
      <el-form-item label="用户电话">
        <el-input v-model="filters.mobile" placeholder="模糊查询" clearable style="width: 140px" />
        <el-input v-model="filters.mobile" placeholder="请输入用户电话" clearable style="width: 140px" />
      </el-form-item>
      <el-form-item label="角色">
        <el-select
@@ -17,7 +18,7 @@
          <el-option
            v-for="r in roles"
            :key="r.id"
            :label="`${r.roleName} (${r.roleCode})`"
            :label="r.roleName"
            :value="r.id"
          />
        </el-select>
@@ -33,11 +34,12 @@
        <el-button @click="resetSearch">重置</el-button>
      </el-form-item>
      <el-form-item class="push-right">
        <el-button v-permission="'sys:users:create'" type="success" @click="openCreate">新增用户</el-button>
        <el-button v-permission="'sys:users:create'" type="primary" @click="openCreate">新增管理员</el-button>
      </el-form-item>
    </el-form>
    <el-table :data="rows" v-loading="loading" stripe>
    <div class="table-wrap">
      <el-table :data="rows" v-loading="loading" stripe height="100%">
      <el-table-column label="序号" width="64" :index="indexMethod" type="index" />
      <el-table-column prop="realName" label="姓名" width="100" show-overflow-tooltip />
      <el-table-column prop="username" label="登录账号" width="120" show-overflow-tooltip />
@@ -58,48 +60,89 @@
      <el-table-column label="操作" width="300" fixed="right">
        <template #default="{ row }">
          <div class="table-ops">
            <el-button v-permission="'sys:users:update'" type="primary" link @click="openEdit(row)">编辑</el-button>
            <el-button
              v-if="!isSuperAdminRow(row)"
              v-permission="'sys:users:update'"
              type="primary"
              link
              @click="openEdit(row)"
            >
              编辑
            </el-button>
            <el-button
              v-permission="'sys:users:status'"
              v-if="row.status === 1"
              type="danger"
              v-if="row.status === 1 && !isSuperAdminRow(row)"
              type="primary"
              link
              @click="confirmDisable(row)"
            >
              禁用
            </el-button>
            <el-button v-else v-permission="'sys:users:status'" type="success" link @click="enableUser(row)">启用</el-button>
            <el-button v-permission="'sys:users:password'" type="primary" link @click="openResetPwd(row)">重置密码</el-button>
            <el-button v-permission="'sys:users:delete'" type="danger" link @click="confirmDelete(row)">删除</el-button>
            <el-button
              v-else-if="!isSuperAdminRow(row)"
              v-permission="'sys:users:status'"
              type="primary"
              link
              @click="enableUser(row)"
            >
              启用
            </el-button>
            <el-button
              v-if="!isSuperAdminRow(row)"
              v-permission="'sys:users:password'"
              type="primary"
              link
              @click="openResetPwd(row)"
            >
              重置密码
            </el-button>
            <el-button
              v-if="!isSuperAdminRow(row)"
              v-permission="'sys:users:delete'"
              type="primary"
              link
              @click="confirmDelete(row)"
            >
              删除
            </el-button>
          </div>
        </template>
      </el-table-column>
    </el-table>
      </el-table>
    </div>
    <el-pagination
      class="pager"
      class="main-pager"
      background
      layout="prev, pager, next"
      layout="sizes, prev, pager, next"
      :total="total"
      :page-size="size"
      :page-sizes="[10, 20, 50, 100]"
      v-model:current-page="page"
      @current-change="load"
      @size-change="onSizeChange"
    />
    <!-- 新增 -->
    <el-dialog v-model="dlgCreate" title="新增用户" width="520px" destroy-on-close @closed="resetCreateForm">
    <el-dialog v-model="dlgCreate" title="新增管理员" width="520px" destroy-on-close @closed="resetCreateForm">
      <el-form :model="createForm" label-width="100px">
        <el-form-item label="登录名" required>
          <el-input v-model="createForm.username" autocomplete="off" />
        <el-form-item label="用户名称" required>
          <el-input v-model="createForm.realName" placeholder="请输入用户名称" />
        </el-form-item>
        <el-form-item label="密码" required>
          <el-input v-model="createForm.password" type="password" show-password autocomplete="new-password" />
        <el-form-item label="联系电话" required>
          <el-input v-model="createForm.mobile" placeholder="请输入联系电话" />
        </el-form-item>
        <el-form-item label="姓名">
          <el-input v-model="createForm.realName" />
        <el-form-item label="登录账号" required>
          <el-input v-model="createForm.username" autocomplete="off" placeholder="请输入登录账号" />
        </el-form-item>
        <el-form-item label="手机">
          <el-input v-model="createForm.mobile" />
        <el-form-item label="初始密码" required>
          <el-input
            v-model="createForm.password"
            type="password"
            show-password
            autocomplete="new-password"
            placeholder="请输入至少12位含大小写字母、特殊字符的密码"
          />
        </el-form-item>
        <el-form-item label="头像">
          <div class="avatar-upload-row">
@@ -118,15 +161,24 @@
            </div>
          </div>
        </el-form-item>
        <el-form-item label="角色" required>
          <el-select v-model="createForm.roleIds" multiple placeholder="必选,可多选" style="width: 100%">
        <el-form-item label="所属角色" required>
          <el-select v-model="createForm.roleIds" multiple placeholder="请选择所属角色(可多选)" style="width: 100%">
            <el-option
              v-for="r in roles"
              :key="r.id"
              :label="`${r.roleName} (${r.roleCode})`"
              :label="r.roleName"
              :value="r.id"
            />
          </el-select>
        </el-form-item>
        <el-form-item label="状态">
          <el-switch
            v-model="createForm.statusEnabled"
            inline-prompt
            active-text="启用"
            inactive-text="禁用"
            :disabled="isAdminUsername(createForm.username)"
          />
        </el-form-item>
      </el-form>
      <template #footer>
@@ -165,14 +217,23 @@
          </div>
        </el-form-item>
        <el-form-item label="角色" required>
          <el-select v-model="editForm.roleIds" multiple placeholder="必选" style="width: 100%">
          <el-select v-model="editForm.roleIds" multiple placeholder="请选择所属角色" style="width: 100%">
            <el-option
              v-for="r in roles"
              :key="r.id"
              :label="`${r.roleName} (${r.roleCode})`"
              :label="r.roleName"
              :value="r.id"
            />
          </el-select>
        </el-form-item>
        <el-form-item label="状态">
          <el-switch
            v-model="editForm.statusEnabled"
            inline-prompt
            active-text="启用"
            inactive-text="禁用"
            :disabled="isAdminUsername(editForm.username)"
          />
        </el-form-item>
      </el-form>
      <template #footer>
@@ -182,13 +243,28 @@
    </el-dialog>
    <!-- 重置密码 -->
    <el-dialog v-model="dlgPwd" title="重置密码" width="440px" destroy-on-close @closed="resetPwdForm">
    <el-dialog v-model="dlgPwd" title="重置密码" width="560px" destroy-on-close @closed="resetPwdForm">
      <el-form :model="pwdForm" label-width="120px">
        <el-form-item label="账号名称">
          <el-input v-model="pwdUserName" disabled />
        </el-form-item>
        <el-form-item label="新密码" required>
          <el-input v-model="pwdForm.newPassword" type="password" show-password autocomplete="new-password" />
          <el-input
            v-model="pwdForm.newPassword"
            type="password"
            show-password
            autocomplete="new-password"
            placeholder="请输入至少12位含大小写字母、特殊字符的密码"
          />
        </el-form-item>
        <el-form-item label="确认新密码" required>
          <el-input v-model="pwdForm.newPasswordConfirm" type="password" show-password autocomplete="new-password" />
          <el-input
            v-model="pwdForm.newPasswordConfirm"
            type="password"
            show-password
            autocomplete="new-password"
            placeholder="请输入至少12位含大小写字母、特殊字符的密码"
          />
        </el-form-item>
      </el-form>
      <template #footer>
@@ -204,6 +280,7 @@
import { ElMessage, ElMessageBox } from "element-plus";
import type { UploadRequestOptions } from "element-plus";
import http from "../api/http";
import { normalizeFormData } from "../utils/formHelper";
type RoleRow = { id: number; roleCode: string; roleName: string };
@@ -240,6 +317,7 @@
const editingId = ref<number | null>(null);
const pwdUserId = ref<number | null>(null);
const pwdUserUuid = ref("");
const pwdUserName = ref("");
const createForm = reactive({
  username: "",
@@ -248,6 +326,7 @@
  mobile: "",
  avatarUrl: "",
  roleIds: [] as number[],
  statusEnabled: true,
});
const editForm = reactive({
@@ -257,12 +336,14 @@
  mobile: "",
  avatarUrl: "",
  roleIds: [] as number[],
  statusEnabled: true,
});
const pwdForm = reactive({
  newPassword: "",
  newPasswordConfirm: "",
});
const SUPER_ADMIN_USERNAME = "admin";
function indexMethod(idx: number) {
  return (page.value - 1) * size.value + idx + 1;
@@ -317,6 +398,12 @@
  load();
}
function onSizeChange(nextSize: number) {
  size.value = nextSize;
  page.value = 1;
  load();
}
function resetCreateForm() {
  createForm.username = "";
  createForm.password = "";
@@ -324,6 +411,7 @@
  createForm.mobile = "";
  createForm.avatarUrl = "";
  createForm.roleIds = [];
  createForm.statusEnabled = true;
}
function openCreate() {
@@ -332,28 +420,43 @@
}
async function submitCreate() {
  if (!createForm.username.trim() || !createForm.password) {
    ElMessage.warning("请填写登录名与密码");
  if (!createForm.realName.trim()) {
    ElMessage.warning("请填写用户名称");
    return;
  }
  if (createForm.password.length < 6) {
    ElMessage.warning("密码至少 6 位");
  if (!createForm.mobile.trim()) {
    ElMessage.warning("请填写联系电话");
    return;
  }
  if (!createForm.username.trim()) {
    ElMessage.warning("请填写登录账号");
    return;
  }
  if (!createForm.password) {
    ElMessage.warning("请填写初始密码");
    return;
  }
  if (!isStrongPassword(createForm.password)) {
    ElMessage.warning("请输入至少12位含大小写字母、特殊字符的密码");
    return;
  }
  if (!createForm.roleIds.length) {
    ElMessage.warning("请至少选择一个角色");
    return;
  }
  const nextStatus = isAdminUsername(createForm.username) ? 1 : (createForm.statusEnabled ? 1 : 0);
  saving.value = true;
  try {
    await http.post("/sys/users", {
    const payload = normalizeFormData({
      username: createForm.username.trim(),
      password: createForm.password,
      realName: createForm.realName || undefined,
      mobile: createForm.mobile || undefined,
      avatarUrl: createForm.avatarUrl || undefined,
      roleIds: createForm.roleIds,
      status: nextStatus,
    });
    await http.post("/sys/users", payload);
    ElMessage.success("已创建");
    dlgCreate.value = false;
    load();
@@ -376,6 +479,7 @@
    editForm.mobile = u.mobile || "";
    editForm.avatarUrl = u.avatarUrl || "";
    editForm.roleIds = [...(u.roleIds || [])];
    editForm.statusEnabled = Number(u.status) !== 0;
    dlgEdit.value = true;
  } catch (e: unknown) {
    ElMessage.error(e instanceof Error ? e.message : "加载详情失败");
@@ -388,15 +492,18 @@
    ElMessage.warning("请至少选择一个角色");
    return;
  }
  const nextStatus = isAdminUsername(editForm.username) ? 1 : (editForm.statusEnabled ? 1 : 0);
  saving.value = true;
  try {
    await http.put(`/sys/users/${editingId.value}`, {
    const payload = normalizeFormData({
      uuid: editForm.uuid,
      realName: editForm.realName || undefined,
      mobile: editForm.mobile || undefined,
      avatarUrl: editForm.avatarUrl || undefined,
      roleIds: editForm.roleIds,
      status: nextStatus,
    });
    await http.put(`/sys/users/${editingId.value}`, payload);
    ElMessage.success("已保存");
    dlgEdit.value = false;
    load();
@@ -435,6 +542,7 @@
function openResetPwd(row: UserRow) {
  pwdUserId.value = row.id;
  pwdUserUuid.value = row.uuid;
  pwdUserName.value = row.username || "";
  pwdForm.newPassword = "";
  pwdForm.newPasswordConfirm = "";
  dlgPwd.value = true;
@@ -445,12 +553,13 @@
  pwdForm.newPasswordConfirm = "";
  pwdUserId.value = null;
  pwdUserUuid.value = "";
  pwdUserName.value = "";
}
async function submitPwd() {
  if (pwdUserId.value == null) return;
  if (pwdForm.newPassword.length < 6) {
    ElMessage.warning("密码至少 6 位");
  if (!isStrongPassword(pwdForm.newPassword)) {
    ElMessage.warning("请输入至少12位含大小写字母、特殊字符的密码");
    return;
  }
  if (pwdForm.newPassword !== pwdForm.newPasswordConfirm) {
@@ -459,11 +568,12 @@
  }
  saving.value = true;
  try {
    await http.post(`/sys/users/${pwdUserId.value}/password`, {
    const payload = normalizeFormData({
      uuid: pwdUserUuid.value,
      newPassword: pwdForm.newPassword,
      newPasswordConfirm: pwdForm.newPasswordConfirm,
    });
    await http.post(`/sys/users/${pwdUserId.value}/password`, payload);
    ElMessage.success("密码已重置");
    dlgPwd.value = false;
  } catch (e: unknown) {
@@ -473,7 +583,29 @@
  }
}
function isStrongPassword(value: string) {
  return (
    value.length >= 12
    && value.length <= 64
    && /[a-z]/.test(value)
    && /[A-Z]/.test(value)
    && /[^A-Za-z0-9]/.test(value)
  );
}
function isSuperAdminRow(row: UserRow) {
  return (row.username || "").trim().toLowerCase() === SUPER_ADMIN_USERNAME;
}
function isAdminUsername(username?: string) {
  return (username || "").trim().toLowerCase() === SUPER_ADMIN_USERNAME;
}
function confirmDisable(row: UserRow) {
  if (isSuperAdminRow(row)) {
    ElMessage.warning("超级管理员账号不可禁用");
    return;
  }
  ElMessageBox.confirm("禁用后该用户将无法登录管理后台,是否继续?", "确认禁用", {
    confirmButtonText: "禁用",
    cancelButtonText: "取消",
@@ -502,6 +634,10 @@
}
function confirmDelete(row: UserRow) {
  if (isSuperAdminRow(row)) {
    ElMessage.warning("超级管理员账号不可删除");
    return;
  }
  ElMessageBox.confirm("删除为逻辑删除,且将解除该用户角色关联,是否继续?", "确认删除", {
    confirmButtonText: "删除",
    cancelButtonText: "取消",
@@ -530,19 +666,39 @@
</script>
<style scoped>
.page-title {
  margin: 0 0 12px;
  color: #2e446e;
  font-size: 18px;
  font-weight: 700;
}
.page {
  display: flex;
  flex-direction: column;
  height: calc(100vh - 190px);
  min-height: 420px;
}
.search-form {
  margin-bottom: 12px;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 4px 0;
  flex: none;
}
.push-right {
  margin-left: auto;
}
.pager {
.table-wrap {
  flex: 1;
  min-height: 0;
}
.main-pager {
  margin-top: 12px;
  justify-content: flex-end;
  flex: none;
}
.avatar-upload-row {