dingdanquanliucheng/frontend/web-admin/src/views/SystemPage.vue
taiyi dd060e88ce fix: SystemPage.vue 补全缺失的 template 闭合标签
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-07 14:58:24 +08:00

1425 lines
49 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!--
SystemPage.vue - 系统管理页面
职责统一管理系统用户角色和菜单三大模块支持查询新增编辑
启用/停用重置密码和角色菜单授权三个模块通过顶部 Tab 切换展示
-->
<template>
<section class="page admin-page" :id="pageId">
<div class="page-header">
<div>
<h2>系统管理</h2>
<p class="subline">本页继续按 02-API 细化设计推进已支持用户角色菜单的真实查询新增编辑重置密码与角色授权联调</p>
</div>
</div>
<div class="module-switcher">
<button
v-for="tab in tabs"
:key="tab.key"
:class="{ active: activeTab === tab.key }"
@click="switchTab(tab.key)"
>
{{ tab.label }}
</button>
</div>
<section class="overview-grid">
<article v-for="module in systemModules" :key="module.title" class="overview-card">
<h3>{{ module.title }}</h3>
<ul>
<li v-for="item in module.items" :key="item">{{ item }}</li>
</ul>
</article>
</section>
<section v-if="activeTab === 'users'" class="module-panel">
<div class="panel-header">
<h3>用户管理</h3>
<span>对齐 `/api/system/users`、`/api/system/users/{user_id}`、`/reset-password`</span>
</div>
<section class="toolbar toolbar-4">
<label>
<span>账号</span>
<input v-model.trim="userFilters.username" type="text" placeholder="请输入账号" />
</label>
<label>
<span>姓名</span>
<input v-model.trim="userFilters.real_name" type="text" placeholder="请输入姓名" />
</label>
<label>
<span>手机号</span>
<input v-model.trim="userFilters.mobile" type="text" placeholder="请输入手机号" />
</label>
<label>
<span>状态</span>
<select v-model="userFilters.status">
<option value="">全部状态</option>
<option value="1">启用</option>
<option value="0">停用</option>
</select>
</label>
<div class="toolbar-actions">
<button class="primary-btn" :disabled="loadingUsers" @click="handleSearchUsers">
{{ loadingUsers ? "查询中..." : "查询用户" }}
</button>
<button class="ghost-btn" :disabled="loadingUsers" @click="handleResetUsers">重置</button>
<button class="ghost-btn" @click="toggleCreateUserForm">
{{ showCreateUserForm ? "收起新增" : "新增用户" }}
</button>
</div>
</section>
<div v-if="showCreateUserForm" class="modal-mask" @click.self="cancelCreateUser">
<section class="modal-panel">
<div class="modal-header">
<div>
<h4>新增用户</h4>
<span>调用真实 `/api/system/users`</span>
</div>
<button class="modal-close" type="button" @click="cancelCreateUser">×</button>
</div>
<div class="form-grid">
<label>
<span>账号</span>
<input v-model.trim="userCreateForm.username" type="text" placeholder="请输入账号" />
</label>
<label>
<span>初始密码</span>
<input v-model.trim="userCreateForm.password" type="password" placeholder="至少 6 位" />
</label>
<label>
<span>姓名</span>
<input v-model.trim="userCreateForm.real_name" type="text" placeholder="请输入姓名" />
</label>
<label>
<span>手机号</span>
<input v-model.trim="userCreateForm.mobile" type="text" placeholder="请输入手机号" />
</label>
<label>
<span>角色</span>
<select v-model="userCreateForm.role_id">
<option value="">请选择角色</option>
<option v-for="item in roleOptions" :key="item.value" :value="String(item.value)">
{{ item.label }}
</option>
</select>
</label>
<label>
<span>状态</span>
<select v-model.number="userCreateForm.status">
<option :value="1">启用</option>
<option :value="0">停用</option>
</select>
</label>
</div>
<div class="action-row split">
<button class="ghost-btn" @click="cancelCreateUser">取消新增</button>
<button class="primary-btn" :disabled="creatingUser" @click="handleCreateUser">
{{ creatingUser ? "创建中..." : "提交创建" }}
</button>
</div>
</section>
</div>
<div v-if="editingUserId" class="modal-mask" @click.self="cancelEditUser">
<section class="modal-panel">
<div class="modal-header">
<div>
<h4>编辑用户</h4>
<span>调用真实 `/api/system/users/{user_id}`</span>
</div>
<button class="modal-close" type="button" @click="cancelEditUser">×</button>
</div>
<div class="form-grid">
<label>
<span>账号</span>
<input :value="userEditForm.username" type="text" disabled />
</label>
<label>
<span>姓名</span>
<input v-model.trim="userEditForm.real_name" type="text" placeholder="请输入姓名" />
</label>
<label>
<span>手机号</span>
<input v-model.trim="userEditForm.mobile" type="text" placeholder="请输入手机号" />
</label>
<label>
<span>角色</span>
<select v-model="userEditForm.role_id">
<option value="">请选择角色</option>
<option v-for="item in roleOptions" :key="item.value" :value="String(item.value)">
{{ item.label }}
</option>
</select>
</label>
<label>
<span>状态</span>
<select v-model.number="userEditForm.status">
<option :value="1">启用</option>
<option :value="0">停用</option>
</select>
</label>
<label>
<span>重置密码</span>
<input v-model.trim="userPasswordForm.new_password" type="password" placeholder="如需重置请输入新密码" />
</label>
</div>
<div class="action-row split">
<button class="ghost-btn" @click="cancelEditUser">取消编辑</button>
<div class="inline-actions">
<button class="ghost-btn" :disabled="resettingUserPassword" @click="handleResetUserPassword">
{{ resettingUserPassword ? "重置中..." : "重置密码" }}
</button>
<button class="primary-btn" :disabled="updatingUser" @click="handleUpdateUser">
{{ updatingUser ? "保存中..." : "保存用户" }}
</button>
</div>
</div>
</section>
</div>
<p v-if="loadingUsers" class="loading">正在加载用户列表...</p>
<div v-else-if="!userRows.length" class="empty-state">当前没有用户数据</div>
<div v-else class="table-wrap">
<table>
<thead>
<tr>
<th><input type="checkbox" :checked="selectedUserIds.length === userRows.length && userRows.length > 0" @change="toggleAllUsers($event)" /></th>
<th>账号</th>
<th>姓名</th>
<th>手机号</th>
<th>角色</th>
<th>状态</th>
<th>更新时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="row in userRows" :key="row.userId || row.username">
<td><input type="checkbox" :value="row.userId" v-model="selectedUserIds" /></td>
<td>{{ row.username }}</td>
<td>{{ row.realName }}</td>
<td>{{ row.mobile }}</td>
<td>{{ row.roleName }}</td>
<td><span class="status status-success">{{ row.status }}</span></td>
<td>{{ row.updatedAt }}</td>
<td>
<button class="link-btn" @click="startEditUser(row)">编辑</button>
<button class="link-btn" :class="{ 'danger-link': row.status === '启用' }" @click="handleToggleUserStatus(row)">{{ row.status === '启用' ? '停用' : '启用' }}</button>
<button class="link-btn danger-link" @click="handleDeleteUser(row)">删除</button>
</td>
</tr>
</tbody>
</table>
<div v-if="selectedUserIds.length" style="margin-top:8px;">
<button class="link-btn danger-link" @click="handleBatchDeleteUsers">批量删除 ({{ selectedUserIds.length }})</button>
</div>
<PaginationBar v-model:currentPage="userPagination.pageNo" v-model:pageSize="userPagination.pageSize" :total="userPagination.total" />
</div>
</section>
<section v-if="activeTab === 'roles'" class="module-panel">
<div class="panel-header">
<h3>角色管理</h3>
<span>对齐 `/api/system/roles`、`/api/system/roles/{role_id}`、`/api/system/roles/{role_id}/menus`</span>
</div>
<section class="toolbar toolbar-3">
<label>
<span>角色名称</span>
<input v-model.trim="roleFilters.role_name" type="text" placeholder="请输入角色名称" />
</label>
<label>
<span>角色编码</span>
<input v-model.trim="roleFilters.role_code" type="text" placeholder="请输入角色编码" />
</label>
<label>
<span>状态</span>
<select v-model="roleFilters.status">
<option value="">全部状态</option>
<option value="1">启用</option>
<option value="0">停用</option>
</select>
</label>
<div class="toolbar-actions">
<button class="primary-btn" :disabled="loadingRoles" @click="handleSearchRoles">
{{ loadingRoles ? "查询中..." : "查询角色" }}
</button>
<button class="ghost-btn" :disabled="loadingRoles" @click="handleResetRoles">重置</button>
<button class="ghost-btn" @click="toggleCreateRoleForm">
{{ showCreateRoleForm ? "收起新增" : "新增角色" }}
</button>
</div>
</section>
<div v-if="showCreateRoleForm" class="modal-mask" @click.self="cancelCreateRole">
<section class="modal-panel">
<div class="modal-header">
<div>
<h4>新增角色</h4>
<span>调用真实 `/api/system/roles`</span>
</div>
<button class="modal-close" type="button" @click="cancelCreateRole">×</button>
</div>
<div class="form-grid">
<label>
<span>角色名称</span>
<input v-model.trim="roleCreateForm.role_name" type="text" placeholder="请输入角色名称" />
</label>
<label>
<span>角色编码</span>
<input v-model.trim="roleCreateForm.role_code" type="text" placeholder="请输入角色编码" />
</label>
<label>
<span>状态</span>
<select v-model.number="roleCreateForm.status">
<option :value="1">启用</option>
<option :value="0">停用</option>
</select>
</label>
<label class="full-width">
<span>备注</span>
<textarea v-model.trim="roleCreateForm.remark" rows="3" placeholder="请输入备注"></textarea>
</label>
</div>
<div class="action-row split">
<button class="ghost-btn" @click="cancelCreateRole">取消新增</button>
<button class="primary-btn" :disabled="creatingRole" @click="handleCreateRole">
{{ creatingRole ? "创建中..." : "提交创建" }}
</button>
</div>
</section>
</div>
<div v-if="editingRoleId" class="modal-mask" @click.self="cancelEditRole">
<section class="modal-panel">
<div class="modal-header">
<div>
<h4>编辑角色</h4>
<span>调用真实 `/api/system/roles/{role_id}`</span>
</div>
<button class="modal-close" type="button" @click="cancelEditRole">×</button>
</div>
<div class="form-grid">
<label>
<span>角色名称</span>
<input v-model.trim="roleEditForm.role_name" type="text" placeholder="请输入角色名称" />
</label>
<label>
<span>角色编码</span>
<input :value="roleEditForm.role_code" type="text" disabled />
</label>
<label>
<span>状态</span>
<select v-model.number="roleEditForm.status">
<option :value="1">启用</option>
<option :value="0">停用</option>
</select>
</label>
<label class="full-width">
<span>备注</span>
<textarea v-model.trim="roleEditForm.remark" rows="3" placeholder="请输入备注"></textarea>
</label>
</div>
<div class="action-row split">
<button class="ghost-btn" @click="cancelEditRole">取消编辑</button>
<button class="primary-btn" :disabled="updatingRole" @click="handleUpdateRole">
{{ updatingRole ? "保存中..." : "保存角色" }}
</button>
</div>
</section>
</div>
<div v-if="authorizingRoleId" class="modal-mask" @click.self="cancelAssignRoleMenus">
<section class="modal-panel modal-panel-wide">
<div class="modal-header">
<div>
<h4>角色授权</h4>
<span>{{ roleMenuForm.role_name }}</span>
</div>
<button class="modal-close" type="button" @click="cancelAssignRoleMenus">×</button>
</div>
<div class="permission-grid">
<label v-for="row in menuRows" :key="`assign-${row.menuId}`" class="permission-item">
<input v-model="roleMenuForm.menu_ids" type="checkbox" :value="String(row.menuId)" />
<span>{{ `${" ".repeat(row.level || 0)}${row.menuName}` }}</span>
<small>{{ row.permissionCode }}</small>
</label>
</div>
<div class="action-row split">
<button class="ghost-btn" @click="cancelAssignRoleMenus">取消授权</button>
<button class="primary-btn" :disabled="assigningRoleMenus" @click="handleAssignRoleMenus">
{{ assigningRoleMenus ? "保存中..." : "保存授权" }}
</button>
</div>
</section>
</div>
<p v-if="loadingRoles" class="loading">正在加载角色列表...</p>
<div v-else-if="!roleRows.length" class="empty-state">当前没有角色数据</div>
<div v-else class="table-wrap">
<table>
<thead>
<tr>
<th><input type="checkbox" :checked="selectedRoleIds.length === roleRows.length && roleRows.length > 0" @change="toggleAllRoles($event)" /></th>
<th>角色名称</th>
<th>角色编码</th>
<th>状态</th>
<th>备注</th>
<th>创建时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="row in roleRows" :key="row.roleId || row.roleCode">
<td><input type="checkbox" :value="row.roleId" v-model="selectedRoleIds" /></td>
<td>{{ row.roleName }}</td>
<td>{{ row.roleCode }}</td>
<td><span class="status status-pending">{{ row.status }}</span></td>
<td>{{ row.remark }}</td>
<td>{{ row.createdAt }}</td>
<td>
<button class="link-btn" @click="startEditRole(row)">编辑</button>
<button class="link-btn" @click="startAssignRoleMenus(row)">授权</button>
<button class="link-btn danger-link" @click="handleDeleteRole(row)">删除</button>
</td>
</tr>
</tbody>
</table>
<div v-if="selectedRoleIds.length" style="margin-top:8px;">
<button class="link-btn danger-link" @click="handleBatchDeleteRoles">批量删除 ({{ selectedRoleIds.length }})</button>
</div>
<PaginationBar v-model:currentPage="rolePagination.pageNo" v-model:pageSize="rolePagination.pageSize" :total="rolePagination.total" />
</div>
</section>
<section v-if="activeTab === 'menus'" class="module-panel">
<div class="panel-header">
<h3>菜单管理</h3>
<span>对齐 `/api/system/menus`、`/api/system/menus/{menu_id}`</span>
</div>
<section class="toolbar toolbar-3">
<label>
<span>菜单类型</span>
<select v-model="menuFilters.menu_type">
<option value="">全部类型</option>
<option value="catalog">目录</option>
<option value="page">页面</option>
<option value="button">按钮</option>
</select>
</label>
<label>
<span>状态</span>
<select v-model="menuFilters.status">
<option value="">全部状态</option>
<option value="1">启用</option>
<option value="0">停用</option>
</select>
</label>
<div class="toolbar-actions">
<button class="primary-btn" :disabled="loadingMenus" @click="handleSearchMenus">
{{ loadingMenus ? "查询中..." : "查询菜单" }}
</button>
<button class="ghost-btn" :disabled="loadingMenus" @click="handleResetMenus">重置</button>
<button class="ghost-btn" @click="toggleCreateMenuForm">
{{ showCreateMenuForm ? "收起新增" : "新增菜单" }}
</button>
</div>
</section>
<div v-if="showCreateMenuForm" class="modal-mask" @click.self="cancelCreateMenu">
<section class="modal-panel">
<div class="modal-header">
<div>
<h4>新增菜单</h4>
<span>调用真实 `/api/system/menus`</span>
</div>
<button class="modal-close" type="button" @click="cancelCreateMenu">×</button>
</div>
<div class="form-grid">
<label>
<span>父级菜单 ID</span>
<input v-model.number="menuCreateForm.parent_id" type="number" min="0" />
</label>
<label>
<span>菜单名称</span>
<input v-model.trim="menuCreateForm.menu_name" type="text" placeholder="请输入菜单名称" />
</label>
<label>
<span>菜单路径</span>
<input v-model.trim="menuCreateForm.menu_path" type="text" placeholder="例如 /system/users" />
</label>
<label>
<span>菜单类型</span>
<select v-model="menuCreateForm.menu_type">
<option value="catalog">目录</option>
<option value="page">页面</option>
<option value="button">按钮</option>
</select>
</label>
<label>
<span>权限编码</span>
<input v-model.trim="menuCreateForm.permission_code" type="text" placeholder="例如 system:user:list" />
</label>
<label>
<span>图标</span>
<input v-model.trim="menuCreateForm.icon" type="text" placeholder="可选" />
</label>
<label>
<span>排序</span>
<input v-model.number="menuCreateForm.sort_no" type="number" min="0" />
</label>
<label>
<span>状态</span>
<select v-model.number="menuCreateForm.status">
<option :value="1">启用</option>
<option :value="0">停用</option>
</select>
</label>
</div>
<div class="action-row split">
<button class="ghost-btn" @click="cancelCreateMenu">取消新增</button>
<button class="primary-btn" :disabled="creatingMenu" @click="handleCreateMenu">
{{ creatingMenu ? "创建中..." : "提交创建" }}
</button>
</div>
</section>
</div>
<div v-if="editingMenuId" class="modal-mask" @click.self="cancelEditMenu">
<section class="modal-panel">
<div class="modal-header">
<div>
<h4>编辑菜单</h4>
<span>调用真实 `/api/system/menus/{menu_id}`</span>
</div>
<button class="modal-close" type="button" @click="cancelEditMenu">×</button>
</div>
<div class="form-grid">
<label>
<span>父级菜单 ID</span>
<input v-model.number="menuEditForm.parent_id" type="number" min="0" />
</label>
<label>
<span>菜单名称</span>
<input v-model.trim="menuEditForm.menu_name" type="text" placeholder="请输入菜单名称" />
</label>
<label>
<span>菜单路径</span>
<input v-model.trim="menuEditForm.menu_path" type="text" placeholder="例如 /system/users" />
</label>
<label>
<span>菜单类型</span>
<select v-model="menuEditForm.menu_type">
<option value="catalog">目录</option>
<option value="page">页面</option>
<option value="button">按钮</option>
</select>
</label>
<label>
<span>权限编码</span>
<input v-model.trim="menuEditForm.permission_code" type="text" placeholder="例如 system:user:list" />
</label>
<label>
<span>图标</span>
<input v-model.trim="menuEditForm.icon" type="text" placeholder="可选" />
</label>
<label>
<span>排序</span>
<input v-model.number="menuEditForm.sort_no" type="number" min="0" />
</label>
<label>
<span>状态</span>
<select v-model.number="menuEditForm.status">
<option :value="1">启用</option>
<option :value="0">停用</option>
</select>
</label>
</div>
<div class="action-row split">
<button class="ghost-btn" @click="cancelEditMenu">取消编辑</button>
<button class="primary-btn" :disabled="updatingMenu" @click="handleUpdateMenu">
{{ updatingMenu ? "保存中..." : "保存菜单" }}
</button>
</div>
</section>
</div>
<p v-if="loadingMenus" class="loading">正在加载菜单列表...</p>
<div v-else-if="!menuRows.length" class="empty-state">当前没有菜单数据</div>
<div v-else class="table-wrap">
<table>
<thead>
<tr>
<th>菜单名称</th>
<th>菜单路径</th>
<th>类型</th>
<th>权限编码</th>
<th>排序</th>
<th>状态</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="row in menuRows" :key="row.menuId || row.permissionCode">
<td>{{ `${" ".repeat(row.level || 0)}${row.menuName}` }}</td>
<td>{{ row.menuPath }}</td>
<td>{{ mapMenuType(row.menuType) }}</td>
<td>{{ row.permissionCode }}</td>
<td>{{ row.sortNo }}</td>
<td><span class="status status-gray">{{ row.status }}</span></td>
<td>
<button class="link-btn" @click="startEditMenu(row)">编辑</button>
</td>
</tr>
</tbody>
</table>
<PaginationBar v-model:currentPage="menuPagination.pageNo" v-model:pageSize="menuPagination.pageSize" :total="menuPagination.total" />
</div>
</section>
</section>
</template>
<script setup>
/**
* SystemPage.vue - 系统管理页面脚本
* 管理用户 CRUD 和密码重置、角色 CRUD 和菜单授权、菜单 CRUD。
* 三个模块共享 Tab 切换逻辑,挂载时根据 URL hash 自动定位到对应模块。
*/
import { onMounted, reactive, ref, watch } from "vue";
import PaginationBar from '../components/PaginationBar.vue';
import {
assignRoleMenus,
changeUserStatus,
createSystemMenu,
createSystemRole,
createSystemUser,
deleteUser,
permanentDeleteUser,
deleteRole,
permanentDeleteRole,
batchDeleteUser,
batchDeleteRole,
fetchRoleMenuAssignment,
fetchSystemMenus,
fetchSystemRoles,
fetchSystemUsers,
resetSystemUserPassword,
updateSystemMenu,
updateSystemRole,
updateSystemUser,
} from "../mockApi";
import { useToast } from '../composables/useToast';
const toast = useToast();
/** 页面唯一标识 */
const pageId = "system-page";
/** Tab 切换配置(用户管理/角色管理/菜单管理) */
const tabs = [
{ key: "users", label: "用户管理" },
{ key: "roles", label: "角色管理" },
{ key: "menus", label: "菜单管理" },
];
/** 当前激活的 Tab key */
const activeTab = ref("users");
/** 系统模块概览数据 */
const systemModules = ref([]);
// ========== 用户管理模块状态 ==========
/** 用户列表加载中标识 */
const loadingUsers = ref(true);
/** 新增用户操作进行中标识 */
const creatingUser = ref(false);
/** 编辑用户操作进行中标识 */
const updatingUser = ref(false);
/** 重置密码操作进行中标识 */
const resettingUserPassword = ref(false);
/** 批量选中的用户 ID 列表 */
const selectedUserIds = ref([]);
/** 新增用户弹窗是否显示 */
const showCreateUserForm = ref(false);
/** 当前编辑的用户 IDnull 表示未编辑) */
const editingUserId = ref(null);
const userPagination = reactive({ pageNo: 1, pageSize: 20, total: 0 });
const userRows = ref([]);
const roleOptions = ref([]);
const userFilters = reactive({
username: "",
real_name: "",
mobile: "",
status: "",
});
const userCreateForm = reactive({
username: "",
password: "",
real_name: "",
mobile: "",
role_id: "",
status: 1,
});
const userEditForm = reactive({
username: "",
real_name: "",
mobile: "",
role_id: "",
status: 1,
});
const userPasswordForm = reactive({
new_password: "",
});
// ========== 角色管理模块状态 ==========
/** 角色列表加载中标识 */
const loadingRoles = ref(true);
/** 新增角色操作进行中标识 */
const creatingRole = ref(false);
/** 编辑角色操作进行中标识 */
const updatingRole = ref(false);
/** 角色授权操作进行中标识 */
const assigningRoleMenus = ref(false);
/** 批量选中的角色 ID 列表 */
const selectedRoleIds = ref([]);
/** 新增角色弹窗是否显示 */
const showCreateRoleForm = ref(false);
/** 当前编辑的角色 ID */
const editingRoleId = ref(null);
/** 当前正在授权的角色 ID */
const authorizingRoleId = ref(null);
const rolePagination = reactive({ pageNo: 1, pageSize: 20, total: 0 });
const roleRows = ref([]);
const roleFilters = reactive({
role_name: "",
role_code: "",
status: "",
});
const roleCreateForm = reactive({
role_name: "",
role_code: "",
status: 1,
remark: "",
});
const roleEditForm = reactive({
role_name: "",
role_code: "",
status: 1,
remark: "",
});
const roleMenuForm = reactive({
role_name: "",
menu_ids: [],
});
// ========== 菜单管理模块状态 ==========
/** 菜单列表加载中标识 */
const loadingMenus = ref(true);
/** 新增菜单操作进行中标识 */
const creatingMenu = ref(false);
/** 编辑菜单操作进行中标识 */
const updatingMenu = ref(false);
/** 新增菜单弹窗是否显示 */
const showCreateMenuForm = ref(false);
/** 当前编辑的菜单 ID */
const editingMenuId = ref(null);
const menuPagination = reactive({ pageNo: 1, pageSize: 20, total: 0 });
const menuRows = ref([]);
const menuFilters = reactive({
menu_type: "",
status: "",
});
const menuCreateForm = reactive({
parent_id: 0,
menu_name: "",
menu_path: "",
menu_type: "page",
permission_code: "",
icon: "",
sort_no: 0,
status: 1,
});
const menuEditForm = reactive({
parent_id: 0,
menu_name: "",
menu_path: "",
menu_type: "page",
permission_code: "",
icon: "",
sort_no: 0,
status: 1,
});
function mapMenuType(value) {
const map = {
catalog: "目录",
page: "页面",
button: "按钮",
};
return map[value] || value || "-";
}
function resetUserCreateForm() {
userCreateForm.username = "";
userCreateForm.password = "";
userCreateForm.real_name = "";
userCreateForm.mobile = "";
userCreateForm.role_id = "";
userCreateForm.status = 1;
}
function resetUserEditForm() {
userEditForm.username = "";
userEditForm.real_name = "";
userEditForm.mobile = "";
userEditForm.role_id = "";
userEditForm.status = 1;
userPasswordForm.new_password = "";
editingUserId.value = null;
}
function resetRoleCreateForm() {
roleCreateForm.role_name = "";
roleCreateForm.role_code = "";
roleCreateForm.status = 1;
roleCreateForm.remark = "";
}
function resetRoleEditForm() {
roleEditForm.role_name = "";
roleEditForm.role_code = "";
roleEditForm.status = 1;
roleEditForm.remark = "";
editingRoleId.value = null;
}
function resetRoleMenuForm() {
roleMenuForm.role_name = "";
roleMenuForm.menu_ids = [];
authorizingRoleId.value = null;
}
function resetMenuCreateForm() {
menuCreateForm.parent_id = 0;
menuCreateForm.menu_name = "";
menuCreateForm.menu_path = "";
menuCreateForm.menu_type = "page";
menuCreateForm.permission_code = "";
menuCreateForm.icon = "";
menuCreateForm.sort_no = 0;
menuCreateForm.status = 1;
}
function resetMenuEditForm() {
menuEditForm.parent_id = 0;
menuEditForm.menu_name = "";
menuEditForm.menu_path = "";
menuEditForm.menu_type = "page";
menuEditForm.permission_code = "";
menuEditForm.icon = "";
menuEditForm.sort_no = 0;
menuEditForm.status = 1;
editingMenuId.value = null;
}
function toggleCreateUserForm() {
showCreateUserForm.value = !showCreateUserForm.value;
if (showCreateUserForm.value) {
resetUserEditForm();
}
}
function cancelCreateUser() {
showCreateUserForm.value = false;
resetUserCreateForm();
}
function toggleCreateRoleForm() {
showCreateRoleForm.value = !showCreateRoleForm.value;
if (showCreateRoleForm.value) {
resetRoleEditForm();
resetRoleMenuForm();
}
}
function cancelCreateRole() {
showCreateRoleForm.value = false;
resetRoleCreateForm();
}
function toggleCreateMenuForm() {
showCreateMenuForm.value = !showCreateMenuForm.value;
if (showCreateMenuForm.value) {
resetMenuEditForm();
}
}
function cancelCreateMenu() {
showCreateMenuForm.value = false;
resetMenuCreateForm();
}
function startEditUser(row) {
editingUserId.value = row.userId;
showCreateUserForm.value = false;
userEditForm.username = row.username;
userEditForm.real_name = row.realName;
userEditForm.mobile = row.mobile === "-" ? "" : row.mobile;
userEditForm.role_id = String(row.roleId || "");
userEditForm.status = row.status === "启用" ? 1 : 0;
userPasswordForm.new_password = "";
}
function cancelEditUser() {
resetUserEditForm();
}
function startEditRole(row) {
editingRoleId.value = row.roleId;
showCreateRoleForm.value = false;
resetRoleMenuForm();
roleEditForm.role_name = row.roleName;
roleEditForm.role_code = row.roleCode;
roleEditForm.status = row.status === "启用" ? 1 : 0;
roleEditForm.remark = row.remark === "-" ? "" : row.remark;
}
function cancelEditRole() {
resetRoleEditForm();
}
async function startAssignRoleMenus(row) {
authorizingRoleId.value = row.roleId;
editingRoleId.value = null;
showCreateRoleForm.value = false;
roleMenuForm.role_name = row.roleName;
roleMenuForm.menu_ids = [];
toast.success("");
try {
if (!menuRows.value.length) {
await handleSearchMenus();
}
const data = await fetchRoleMenuAssignment(row.roleId);
roleMenuForm.menu_ids = (data.menu_ids || []).map((item) => String(item));
} catch (error) {
toast.error(error.message || "加载角色授权失败");
}
}
function cancelAssignRoleMenus() {
resetRoleMenuForm();
}
function startEditMenu(row) {
editingMenuId.value = row.menuId;
showCreateMenuForm.value = false;
menuEditForm.parent_id = row.parentId || 0;
menuEditForm.menu_name = row.menuName;
menuEditForm.menu_path = row.menuPath === "-" ? "" : row.menuPath;
menuEditForm.menu_type = row.menuType || "page";
menuEditForm.permission_code = row.permissionCode === "-" ? "" : row.permissionCode;
menuEditForm.icon = row.icon === "-" ? "" : row.icon;
menuEditForm.sort_no = row.sortNo || 0;
menuEditForm.status = row.status === "启用" ? 1 : 0;
}
function cancelEditMenu() {
resetMenuEditForm();
}
async function loadUsers() {
const data = await fetchSystemUsers({ ...userFilters, page_no: userPagination.pageNo, page_size: userPagination.pageSize });
userRows.value = data.rows;
userPagination.total = data.total || data.rows?.length || 0;
return data;
}
async function loadRoles() {
const data = await fetchSystemRoles({ ...roleFilters, page_no: rolePagination.pageNo, page_size: rolePagination.pageSize });
roleRows.value = data.rows;
rolePagination.total = data.total || data.rows?.length || 0;
roleOptions.value = data.rows.map((item) => ({
value: item.roleId,
label: `${item.roleName} / ${item.roleCode}`,
}));
return data;
}
async function loadMenus() {
const data = await fetchSystemMenus({ ...menuFilters, page_no: menuPagination.pageNo, page_size: menuPagination.pageSize });
menuRows.value = data.rows;
menuPagination.total = data.total || data.rows?.length || 0;
return data;
}
async function handleSearchUsers() {
loadingUsers.value = true;
toast.success("");
try {
const data = await loadUsers();
} catch (error) {
toast.error(error.message || "加载用户列表失败");
} finally {
loadingUsers.value = false;
}
}
async function handleToggleUserStatus(row) {
const newStatus = row.status === "启用" ? 0 : 1;
const action = newStatus === 0 ? "停用" : "启用";
try {
await changeUserStatus(row.userId, newStatus);
await handleSearchUsers();
toast.success(`用户 ${row.username}${action}`);
} catch (error) {
toast.error(error.message || `${action}用户失败`);
}
}
async function handleResetUsers() {
userFilters.username = "";
userFilters.real_name = "";
userFilters.mobile = "";
userFilters.status = "";
userPagination.pageNo = 1;
await handleSearchUsers();
}
async function handleCreateUser() {
if (!userCreateForm.username.trim()) {
toast.error("请输入账号");
return;
}
if (!userCreateForm.password.trim()) {
toast.error("请输入初始密码");
return;
}
if (!userCreateForm.real_name.trim()) {
toast.error("请输入姓名");
return;
}
if (!userCreateForm.role_id) {
toast.error("请选择角色");
return;
}
creatingUser.value = true;
toast.success("");
try {
const result = await createSystemUser({
...userCreateForm,
role_id: Number(userCreateForm.role_id),
});
showCreateUserForm.value = false;
resetUserCreateForm();
await handleSearchUsers();
toast.success(`用户 ${result.username} 创建成功。`);
} catch (error) {
toast.error(error.message || "创建用户失败");
} finally {
creatingUser.value = false;
}
}
async function handleUpdateUser() {
if (!editingUserId.value) {
return;
}
if (!userEditForm.real_name.trim()) {
toast.error("请输入姓名");
return;
}
if (!userEditForm.role_id) {
toast.error("请选择角色");
return;
}
updatingUser.value = true;
toast.success("");
try {
await updateSystemUser(editingUserId.value, {
real_name: userEditForm.real_name,
mobile: userEditForm.mobile,
role_id: Number(userEditForm.role_id),
status: userEditForm.status,
});
await handleSearchUsers();
resetUserEditForm();
toast.success("用户信息已更新。");
} catch (error) {
toast.error(error.message || "更新用户失败");
} finally {
updatingUser.value = false;
}
}
async function handleResetUserPassword() {
if (!editingUserId.value) {
return;
}
if (!userPasswordForm.new_password.trim()) {
toast.error("请输入新密码");
return;
}
resettingUserPassword.value = true;
toast.success("");
try {
await resetSystemUserPassword(editingUserId.value, {
new_password: userPasswordForm.new_password,
});
userPasswordForm.new_password = "";
toast.success("密码已重置。");
} catch (error) {
toast.error(error.message || "重置密码失败");
} finally {
resettingUserPassword.value = false;
}
}
async function handleSearchRoles() {
loadingRoles.value = true;
toast.success("");
try {
const data = await loadRoles();
} catch (error) {
toast.error(error.message || "加载角色列表失败");
} finally {
loadingRoles.value = false;
}
}
async function handleResetRoles() {
roleFilters.role_name = "";
roleFilters.role_code = "";
roleFilters.status = "";
rolePagination.pageNo = 1;
await handleSearchRoles();
}
async function handleCreateRole() {
if (!roleCreateForm.role_name.trim()) {
toast.error("请输入角色名称");
return;
}
if (!roleCreateForm.role_code.trim()) {
toast.error("请输入角色编码");
return;
}
creatingRole.value = true;
toast.success("");
try {
const result = await createSystemRole({ ...roleCreateForm });
showCreateRoleForm.value = false;
resetRoleCreateForm();
await handleSearchRoles();
toast.success(`角色 ${result.role_name} 创建成功。`);
} catch (error) {
toast.error(error.message || "创建角色失败");
} finally {
creatingRole.value = false;
}
}
async function handleUpdateRole() {
if (!editingRoleId.value) {
return;
}
if (!roleEditForm.role_name.trim()) {
toast.error("请输入角色名称");
return;
}
updatingRole.value = true;
toast.success("");
try {
await updateSystemRole(editingRoleId.value, {
role_name: roleEditForm.role_name,
status: roleEditForm.status,
remark: roleEditForm.remark,
});
await handleSearchRoles();
resetRoleEditForm();
toast.success("角色信息已更新。");
} catch (error) {
toast.error(error.message || "更新角色失败");
} finally {
updatingRole.value = false;
}
}
async function handleAssignRoleMenus() {
if (!authorizingRoleId.value) {
return;
}
assigningRoleMenus.value = true;
toast.success("");
try {
const selectedMenuIds = roleMenuForm.menu_ids.map((item) => Number(item));
const selectedMenus = menuRows.value.filter((item) => selectedMenuIds.includes(item.menuId));
await assignRoleMenus(authorizingRoleId.value, {
menu_ids: selectedMenuIds,
permission_codes: selectedMenus.map((item) => item.permissionCode).filter((item) => item && item !== "-"),
});
resetRoleMenuForm();
toast.success("角色授权已更新。");
} catch (error) {
toast.error(error.message || "保存角色授权失败");
} finally {
assigningRoleMenus.value = false;
}
}
async function handleSearchMenus() {
loadingMenus.value = true;
toast.success("");
try {
const data = await loadMenus();
} catch (error) {
toast.error(error.message || "加载菜单列表失败");
} finally {
loadingMenus.value = false;
}
}
async function handleResetMenus() {
menuFilters.menu_type = "";
menuFilters.status = "";
menuPagination.pageNo = 1;
await handleSearchMenus();
}
async function handleCreateMenu() {
if (!menuCreateForm.menu_name.trim()) {
toast.error("请输入菜单名称");
return;
}
if (!menuCreateForm.permission_code.trim()) {
toast.error("请输入权限编码");
return;
}
creatingMenu.value = true;
toast.success("");
try {
const result = await createSystemMenu({ ...menuCreateForm });
showCreateMenuForm.value = false;
resetMenuCreateForm();
await handleSearchMenus();
toast.success(`菜单 ${result.menu_name} 创建成功。`);
} catch (error) {
toast.error(error.message || "创建菜单失败");
} finally {
creatingMenu.value = false;
}
}
async function handleUpdateMenu() {
if (!editingMenuId.value) {
return;
}
if (!menuEditForm.menu_name.trim()) {
toast.error("请输入菜单名称");
return;
}
if (!menuEditForm.permission_code.trim()) {
toast.error("请输入权限编码");
return;
}
updatingMenu.value = true;
toast.success("");
try {
await updateSystemMenu(editingMenuId.value, { ...menuEditForm });
await handleSearchMenus();
resetMenuEditForm();
toast.success("菜单信息已更新。");
} catch (error) {
toast.error(error.message || "更新菜单失败");
} finally {
updatingMenu.value = false;
}
}
async function switchTab(tabKey) {
activeTab.value = tabKey;
toast.success("");
const hashMap = {
users: "#users",
roles: "#roles",
menus: "#menus",
};
window.history.replaceState({}, "", `/system${hashMap[tabKey] || ""}`);
if (tabKey === "users" && !userRows.value.length) {
handleSearchUsers();
}
if (tabKey === "roles" && !roleRows.value.length) {
handleSearchRoles();
}
if (tabKey === "menus" && !menuRows.value.length) {
handleSearchMenus();
}
}
function toggleAllUsers(e) {
selectedUserIds.value = e.target.checked ? userRows.value.map(r => r.userId) : [];
}
function toggleAllRoles(e) {
selectedRoleIds.value = e.target.checked ? roleRows.value.map(r => r.roleId) : [];
}
async function handleBatchDeleteUsers() {
if (!confirm(`确认批量删除 ${selectedUserIds.value.length} 个用户?`)) return;
try {
await batchDeleteUser(selectedUserIds.value);
toast.success("批量删除成功");
selectedUserIds.value = [];
await handleSearchUsers();
} catch (e) { toast.error(e.message); }
}
async function handleBatchDeleteRoles() {
if (!confirm(`确认批量删除 ${selectedRoleIds.value.length} 个角色?`)) return;
try {
await batchDeleteRole(selectedRoleIds.value);
toast.success("批量删除成功");
selectedRoleIds.value = [];
await handleSearchRoles();
} catch (e) { toast.error(e.message); }
}
async function handleDeleteUser(row) {
if (!confirm(`确认删除用户「${row.realName || row.username}」?\n\n点击"确定"执行软删除(可恢复),点击"取消"后可选择永久删除。`)) {
if (!confirm(`确认永久删除用户「${row.realName || row.username}」?\n\n此操作不可恢复`)) return;
try {
await permanentDeleteUser(row.userId);
toast.success("永久删除成功");
await handleSearchUsers();
} catch (e) { toast.error(e.message); }
return;
}
try {
await deleteUser(row.userId);
toast.success("已停用");
await handleSearchUsers();
} catch (e) { toast.error(e.message); }
}
async function handleDeleteRole(row) {
if (!confirm(`确认删除角色「${row.roleName}」?\n\n点击"确定"执行软删除(可恢复),点击"取消"后可选择永久删除。`)) {
if (!confirm(`确认永久删除角色「${row.roleName}」?\n\n此操作不可恢复如果该角色下有用户将无法删除。`)) return;
try {
await permanentDeleteRole(row.roleId);
toast.success("永久删除成功");
await handleSearchRoles();
} catch (e) { toast.error(e.message); }
return;
}
try {
await deleteRole(row.roleId);
toast.success("已停用");
await handleSearchRoles();
} catch (e) { toast.error(e.message); }
}
watch(() => userPagination.pageNo, () => { handleSearchUsers(); });
watch(() => userPagination.pageSize, () => { userPagination.pageNo = 1; handleSearchUsers(); });
watch(() => rolePagination.pageNo, () => { handleSearchRoles(); });
watch(() => rolePagination.pageSize, () => { rolePagination.pageNo = 1; handleSearchRoles(); });
watch(() => menuPagination.pageNo, () => { handleSearchMenus(); });
watch(() => menuPagination.pageSize, () => { menuPagination.pageNo = 1; handleSearchMenus(); });
onMounted(async () => {
const section = window.location.hash.replace('#', '');
if (section === 'roles') activeTab.value = 'roles';
if (section === 'menus') activeTab.value = 'menus';
await Promise.all([handleSearchRoles(), handleSearchUsers(), handleSearchMenus()]);
});
</script>
<style scoped>
.page { padding: 0; }
.page-header { display: flex; justify-content: space-between; align-items: center; gap: 12px; margin-bottom: 18px; }
h2 { margin: 0 0 6px; font-size: 24px; }
.subline { margin: 0; color: #64748b; }
.module-switcher { display: flex; flex-wrap: wrap; gap: 10px; margin-bottom: 16px; }
.module-switcher button { border: 1px solid #cbd5e1; border-radius: 999px; padding: 10px 16px; background: #fff; }
.module-switcher .active { background: #1d4ed8; color: #fff; border-color: #1d4ed8; }
.overview-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 12px; margin-bottom: 20px; }
.overview-card { border: 1px solid #dbe3ef; border-radius: 16px; padding: 16px; background: #f8fbff; }
.overview-card h3 { margin: 0 0 10px; font-size: 16px; }
.overview-card ul { margin: 0; padding-left: 18px; color: #475569; }
.module-panel, .form-panel { margin-top: 20px; border: 1px solid #dbe3ef; border-radius: 18px; padding: 16px; background: #fff; }
.panel-header { display: flex; justify-content: space-between; align-items: center; gap: 12px; margin-bottom: 12px; }
.panel-header.compact { margin-bottom: 8px; }
.panel-header h3, .panel-header h4 { margin: 0; }
.panel-header span { color: #64748b; font-size: 13px; }
.toolbar { display: grid; gap: 12px; margin-bottom: 20px; align-items: end; }
.toolbar-4 { grid-template-columns: repeat(4, minmax(0, 1fr)) auto; }
.toolbar-3 { grid-template-columns: repeat(3, minmax(0, 1fr)) auto; }
.toolbar label, .form-grid label { display: grid; gap: 6px; }
.toolbar span, .form-grid span { color: #475569; font-size: 13px; font-weight: 600; }
.toolbar input,
.toolbar select,
.form-grid input,
.form-grid select,
textarea,
.ghost-btn,
.link-btn,
.primary-btn,
.modal-close {
border-radius: 12px;
padding: 10px 12px;
min-width: 0;
}
.toolbar input,
.toolbar select,
.form-grid input,
.form-grid select,
textarea,
.ghost-btn,
.link-btn {
border: 1px solid #cbd5e1;
background: #fff;
}
.primary-btn { border: 1px solid #2563eb; background: #2563eb; color: #fff; }
.ghost-btn { background: #f8fafc; color: #334155; }
.toolbar-actions { display: flex; gap: 10px; align-items: end; flex-wrap: wrap; }
.loading { margin: 0; color: #6b7280; }
.empty-state { padding: 24px; border: 1px dashed #cbd5e1; border-radius: 16px; color: #64748b; text-align: center; background: #fff; }
.table-wrap { overflow-x: auto; }
table { width: 100%; border-collapse: collapse; min-width: 960px; }
th, td { text-align: left; padding: 12px 10px; border-bottom: 1px solid #e5e7eb; vertical-align: top; }
th { color: #334155; font-size: 13px; background: #f8fafc; }
.form-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }
.full-width { grid-column: 1 / -1; }
.action-row { margin-top: 16px; display: flex; justify-content: flex-end; }
.action-row.split { justify-content: space-between; align-items: center; }
.inline-actions { display: flex; gap: 10px; flex-wrap: wrap; }
.modal-mask { position: fixed; inset: 0; z-index: 50; display: flex; align-items: center; justify-content: center; padding: 24px; background: rgba(15, 23, 42, 0.48); backdrop-filter: blur(2px); }
.modal-panel { width: min(760px, 100%); max-height: min(88vh, 860px); overflow: auto; border-radius: 20px; border: 1px solid #dbe3ef; background: #fff; padding: 24px; box-shadow: 0 24px 80px rgba(15, 23, 42, 0.24); }
.modal-panel-wide { width: min(980px, 100%); }
.detail-modal { width: min(860px, 100%); }
.modal-header { display: flex; justify-content: space-between; align-items: flex-start; gap: 12px; margin-bottom: 16px; }
.modal-header h4 { margin: 0 0 6px; font-size: 18px; }
.modal-header span { color: #64748b; font-size: 13px; }
.modal-close { width: 36px; height: 36px; border: 1px solid #d1d5db; background: #f8fafc; font-size: 22px; line-height: 1; color: #334155; }
.permission-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }
.permission-item { display: grid; gap: 4px; padding: 12px; border: 1px solid #dbe3ef; border-radius: 14px; background: #f8fbff; }
.permission-item small { color: #64748b; }
.status { display: inline-flex; padding: 4px 10px; border-radius: 999px; }
.status-success { background: #eff6ff; color: #1d4ed8; }
.status-pending { background: #fef3c7; color: #92400e; }
.status-gray { background: #e2e8f0; color: #475569; }
.link-btn { color: #2563eb; cursor: pointer; padding: 6px 10px; }
.danger-link { color: #dc2626 !important; }
@media (max-width: 1200px) {
.toolbar-4, .toolbar-3 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.toolbar-actions { grid-column: 1 / -1; }
}
@media (max-width: 768px) {
.page-header, .panel-header, .action-row.split { flex-direction: column; align-items: stretch; }
.toolbar, .form-grid, .permission-grid { grid-template-columns: 1fr; }
table { min-width: 780px; }
}
</style>