1425 lines
49 KiB
Vue
1425 lines
49 KiB
Vue
<!--
|
||
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);
|
||
/** 当前编辑的用户 ID(null 表示未编辑) */
|
||
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>
|