yanderong
2026-08-25 e959e9e29ebb74e54a4c73c13b4da8eb26d59ac5
feat: 用户/角色/菜单配置相关功能开发
1个文件已添加
4个文件已修改
405 ■■■■■ 已修改文件
src/api/roles.ts 148 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/users.ts 117 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/Account.vue 81 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/Role.vue 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/RoleForm.vue 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/roles.ts
New file
@@ -0,0 +1,148 @@
import { request, type ApiResponse } from './request'
export interface RolePermission {
  menuId: number
  menuName: string
}
export interface RolePermissionPage {
  menuId: number
  pageName: string
  permissions: RolePermission[]
}
export interface RolePermissionModule {
  menuId: number
  moduleName: string
  pages: RolePermissionPage[]
}
export interface CreateRoleInput {
  roleName: string
  roleKey: 'common'
  roleSort: 1
  status: '0'
  menuIds: number[]
  remark: string
}
export interface UpdateRoleInput {
  roleId: number
  roleName: string
  roleKey: 'common'
  roleSort: 1
  status: '0'
  menuIds: number[]
  remark: string
}
export interface RoleListParams {
  pageNum: number
  pageSize: number
  roleName?: string
}
export interface RoleListItem {
  id: number
  name: string
  accountCount: number
  createdAt: string
  remark: string
  builtIn: boolean
}
interface RoleDto {
  roleId: number
  roleName: string | null
  roleKey: string | null
  createTime: string | null
  remark: string | null
  userCount?: number | null
  accountCount?: number | null
}
interface RoleListResponse extends ApiResponse {
  total: number
  rows: RoleDto[]
}
interface RoleDetailDto {
  roleId: number
  roleName: string | null
  remark: string | null
  menuIds: number[] | null
}
interface RoleDetailResponse extends ApiResponse {
  data: RoleDetailDto
}
export interface RoleDetail {
  id: number
  name: string
  remark: string
  menuIds: number[]
}
export interface RoleListResult {
  total: number
  rows: RoleListItem[]
}
interface PermissionListResponse extends ApiResponse {
  data: RolePermissionModule[]
}
export async function getRolePermissionList(): Promise<RolePermissionModule[]> {
  const result = await request<PermissionListResponse>('/system/menu/permissionList')
  return Array.isArray(result.data) ? result.data : []
}
export async function getRoleList(params: RoleListParams): Promise<RoleListResult> {
  const searchParams = new URLSearchParams()
  Object.entries(params).forEach(([key, value]) => {
    if (value !== undefined && value !== '') searchParams.set(key, String(value))
  })
  const result = await request<RoleListResponse>(`/system/role/list?${searchParams.toString()}`)
  return {
    total: Number(result.total) || 0,
    rows: (result.rows || []).map(item => ({
      id: item.roleId,
      name: item.roleName || '-',
      accountCount: Number(item.userCount ?? item.accountCount) || 0,
      createdAt: item.createTime || '',
      remark: item.remark || '',
      builtIn: item.roleKey === 'admin',
    })),
  }
}
export async function getRole(roleId: number): Promise<RoleDetail> {
  const result = await request<RoleDetailResponse>(`/system/role/${roleId}`)
  return {
    id: result.data.roleId,
    name: result.data.roleName || '',
    remark: result.data.remark || '',
    menuIds: Array.isArray(result.data.menuIds) ? result.data.menuIds.map(Number) : [],
  }
}
export function createRole(input: CreateRoleInput): Promise<ApiResponse> {
  return request<ApiResponse>('/system/role', {
    method: 'POST',
    body: JSON.stringify(input),
  })
}
export function updateRole(input: UpdateRoleInput): Promise<ApiResponse> {
  return request<ApiResponse>('/system/role', {
    method: 'PUT',
    body: JSON.stringify(input),
  })
}
export async function deleteRoles(roleIds: number[]): Promise<void> {
  await request<ApiResponse>(`/system/role/${roleIds.join(',')}`, {
    method: 'DELETE',
  })
}
src/api/users.ts
@@ -4,6 +4,9 @@
  userId: number
  userName: string | null
  nickName: string | null
  phonenumber?: string | null
  roleId?: number | null
  createTime?: string | null
  status: string | null
}
@@ -16,6 +19,58 @@
  id: number
  name: string
  userName: string
}
export interface RoleOption {
  roleId: number
  roleName: string
}
interface RoleOptionDto {
  roleId: number
  roleName: string | null
}
interface RoleOptionResponse extends ApiResponse {
  data: RoleOptionDto[]
}
export interface CreateUserInput {
  nickName: string
  userName: string
  phonenumber: string
  password: string
  status: '0' | '1'
  roleIds: number[]
}
export interface UpdateUserInput extends Omit<CreateUserInput, 'password'> {
  userId: number
  password?: string
}
export interface UserListParams {
  pageNum: number
  pageSize: number
  nickName?: string
  phonenumber?: string
  roleId?: number
  status?: string
}
export interface UserListItem {
  id: number
  name: string
  username: string
  phone: string
  roleId: number
  status: '启用' | '禁用'
  createdAt: string
}
export interface UserListResult {
  total: number
  rows: UserListItem[]
}
export async function getEnabledUserOptions(): Promise<UserOption[]> {
@@ -33,3 +88,65 @@
      userName: item.userName?.trim() || '',
    }))
}
export async function getUserList(params: UserListParams): Promise<UserListResult> {
  const searchParams = new URLSearchParams()
  Object.entries(params).forEach(([key, value]) => {
    if (value !== undefined && value !== '') searchParams.set(key, String(value))
  })
  const result = await request<UserListResponse>(`/system/user/list?${searchParams.toString()}`)
  return {
    total: Number(result.total) || 0,
    rows: (result.rows || []).map(item => ({
      id: item.userId,
      name: item.nickName || '-',
      username: item.userName || '-',
      phone: item.phonenumber || '',
      roleId: Number(item.roleId) || 0,
      status: item.status === '0' ? '启用' : '禁用',
      createdAt: item.createTime || '',
    })),
  }
}
export async function getRoleOptions(): Promise<RoleOption[]> {
  const result = await request<RoleOptionResponse>('/system/role/optionselect')
  return (result.data || []).map(item => ({
    roleId: item.roleId,
    roleName: item.roleName || `角色${item.roleId}`,
  }))
}
export function createUser(input: CreateUserInput): Promise<ApiResponse> {
  return request<ApiResponse>('/system/user', {
    method: 'POST',
    body: JSON.stringify(input),
  })
}
export function updateUser(input: UpdateUserInput): Promise<ApiResponse> {
  return request<ApiResponse>('/system/user', {
    method: 'PUT',
    body: JSON.stringify(input),
  })
}
export function changeUserStatus(userId: number, status: '0' | '1'): Promise<ApiResponse> {
  return request<ApiResponse>('/system/user/changeStatus', {
    method: 'PUT',
    body: JSON.stringify({ userId, status }),
  })
}
export function resetUserPassword(userId: number, password: string): Promise<ApiResponse> {
  return request<ApiResponse>('/system/user/resetPwd', {
    method: 'PUT',
    body: JSON.stringify({ userId, password }),
  })
}
export async function deleteUsers(userIds: number[]): Promise<void> {
  await request<ApiResponse>(`/system/user/${userIds.join(',')}`, {
    method: 'DELETE',
  })
}
src/views/Account.vue
@@ -1,42 +1,42 @@
<script setup lang="ts">
import { computed, reactive, ref } from 'vue'
import { computed, onMounted, reactive, ref } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { useAccessControl, type AccountRecord, type AccountStatus } from '@/stores/accessControl'
import { changeUserStatus, createUser, deleteUsers, getRoleOptions, getUserList, resetUserPassword, updateUser, type RoleOption, type UserListItem } from '@/api/users'
import { useAccessControl, type AccountStatus } from '@/stores/accessControl'
interface Filters { name: string; phone: string; roleId: number | null; status: string }
const { accounts, roles, createAccount, updateAccount, toggleAccount, resetPassword, removeAccount } = useAccessControl()
const { accounts } = useAccessControl()
const filters = reactive<Filters>({ name: '', phone: '', roleId: null, status: '' })
const appliedFilters = ref<Filters>({ name: '', phone: '', roleId: null, status: '' })
const statuses: AccountStatus[] = ['启用', '禁用']
const records = ref<UserListItem[]>([])
const total = ref(0)
const currentPage = ref(1)
const pageSize = ref(10)
const loading = ref(false)
const accountDialogVisible = ref(false)
const editingId = ref<number | null>(null)
const roleOptions = ref<RoleOption[]>([])
const rolesLoading = ref(false)
const savingAccount = ref(false)
const accountForm = reactive({ name: '', username: '', phone: '', password: '', roleId: null as number | null, status: '启用' as AccountStatus })
const passwordDialogVisible = ref(false)
const passwordTarget = ref<AccountRecord | null>(null)
const passwordTarget = ref<UserListItem | null>(null)
const passwordForm = reactive({ password: '', confirmPassword: '' })
const roleMap = computed(() => new Map(roles.value.map(item => [item.id, item.name])))
const filteredAccounts = computed(() => {
  const query = appliedFilters.value
  return accounts.value
    .filter(item => !query.name || item.name.toLowerCase().includes(query.name.trim().toLowerCase()) || item.username.toLowerCase().includes(query.name.trim().toLowerCase()))
    .filter(item => !query.phone || item.phone.includes(query.phone.trim()))
    .filter(item => !query.roleId || item.roleId === query.roleId)
    .filter(item => !query.status || query.status === '全部' || item.status === query.status)
    .sort((a, b) => b.createdAt.localeCompare(a.createdAt))
})
const pagedAccounts = computed(() => filteredAccounts.value.slice((currentPage.value - 1) * pageSize.value, currentPage.value * pageSize.value))
const roleMap = computed(() => new Map(roleOptions.value.map(item => [item.roleId, item.roleName])))
function applySearch() { appliedFilters.value = { ...filters }; currentPage.value = 1; ElMessage.success(`已找到 ${filteredAccounts.value.length} 个账号`) }
function resetSearch() { Object.assign(filters, { name: '', phone: '', roleId: null, status: '' }); appliedFilters.value = { name: '', phone: '', roleId: null, status: '' }; currentPage.value = 1 }
function openCreate() { editingId.value = null; Object.assign(accountForm, { name: '', username: '', phone: '', password: '', roleId: null, status: '启用' }); accountDialogVisible.value = true }
function openEdit(item: AccountRecord) { editingId.value = item.id; Object.assign(accountForm, { name: item.name, username: item.username, phone: item.phone, password: '', roleId: item.roleId, status: item.status }); accountDialogVisible.value = true }
async function loadAccounts() { loading.value = true; try { const query = appliedFilters.value; const result = await getUserList({ pageNum: currentPage.value, pageSize: pageSize.value, nickName: query.name.trim() || undefined, phonenumber: query.phone.trim() || undefined, roleId: query.roleId || undefined, status: query.status || undefined }); records.value = result.rows; total.value = result.total } catch (error) { records.value = []; total.value = 0; ElMessage.error(error instanceof Error ? error.message : '账号列表加载失败') } finally { loading.value = false } }
function applySearch() { appliedFilters.value = { ...filters }; currentPage.value = 1; loadAccounts() }
function resetSearch() { Object.assign(filters, { name: '', phone: '', roleId: null, status: '' }); appliedFilters.value = { name: '', phone: '', roleId: null, status: '' }; currentPage.value = 1; loadAccounts() }
function handlePageChange(page: number) { currentPage.value = page; loadAccounts() }
function handleSizeChange(size: number) { pageSize.value = size; currentPage.value = 1; loadAccounts() }
async function loadRoleOptions() { rolesLoading.value = true; try { roleOptions.value = await getRoleOptions() } catch (error) { roleOptions.value = []; ElMessage.error(error instanceof Error ? error.message : '角色列表加载失败') } finally { rolesLoading.value = false } }
function openCreate() { editingId.value = null; Object.assign(accountForm, { name: '', username: '', phone: '', password: '', roleId: null, status: '启用' }); accountDialogVisible.value = true; loadRoleOptions() }
function openEdit(item: UserListItem) { editingId.value = item.id; Object.assign(accountForm, { name: item.name, username: item.username, phone: item.phone, password: '', roleId: item.roleId, status: item.status }); accountDialogVisible.value = true }
function isStrongPassword(value: string) { return value.length >= 12 && /[a-z]/.test(value) && /[A-Z]/.test(value) && /\d/.test(value) && /[^A-Za-z0-9]/.test(value) }
function saveAccount() {
async function saveAccount() {
  const name = accountForm.name.trim()
  const username = accountForm.username.trim()
  if (!name) { ElMessage.warning('请输入用户名称'); return }
@@ -44,35 +44,46 @@
  if (!editingId.value && !isStrongPassword(accountForm.password)) { ElMessage.warning('初始密码至少12位,并包含大小写字母、数字和特殊字符'); return }
  if (!accountForm.roleId) { ElMessage.warning('请选择所属角色'); return }
  if (accounts.value.some(item => item.id !== editingId.value && item.username.toLowerCase() === username.toLowerCase())) { ElMessage.warning('登录账号已存在'); return }
  if (editingId.value) updateAccount(editingId.value, { name, phone: accountForm.phone.trim(), roleId: accountForm.roleId, status: accountForm.status })
  else createAccount({ name, username, phone: accountForm.phone.trim(), password: accountForm.password, roleId: accountForm.roleId, status: accountForm.status })
  accountDialogVisible.value = false
  ElMessage.success(editingId.value ? '账号已更新' : '账号已新增')
  savingAccount.value = true
  try {
    if (editingId.value) await updateUser({ userId: editingId.value, nickName: name, userName: username, phonenumber: accountForm.phone.trim(), status: accountForm.status === '启用' ? '0' : '1', roleIds: [accountForm.roleId] })
    else await createUser({ nickName: name, userName: username, phonenumber: accountForm.phone.trim(), password: accountForm.password, status: accountForm.status === '启用' ? '0' : '1', roleIds: [accountForm.roleId] })
    accountDialogVisible.value = false
    ElMessage.success(editingId.value ? '账号已更新' : '账号已新增')
    await loadAccounts()
  } catch (error) {
    ElMessage.error(error instanceof Error ? error.message : '账号保存失败')
  } finally {
    savingAccount.value = false
  }
}
async function changeStatus(item: AccountRecord) {
async function changeStatus(item: UserListItem) {
  const next = item.status === '启用' ? '禁用' : '启用'
  try { await ElMessageBox.confirm(`确认${next}账号“${item.name}”?`, `${next}账号`, { confirmButtonText: next, cancelButtonText: '取消', type: 'warning' }); toggleAccount(item.id); ElMessage.success(`账号已${next}`) } catch { /* 用户取消 */ }
  try { await ElMessageBox.confirm(`确认${next}账号“${item.name}”?`, `${next}账号`, { confirmButtonText: next, cancelButtonText: '取消', type: 'warning' }) } catch { return }
  try { await changeUserStatus(item.id, next === '禁用' ? '1' : '0'); ElMessage.success(`账号已${next}`); await loadAccounts() } catch (error) { ElMessage.error(error instanceof Error ? error.message : `${next}账号失败`) }
}
function openReset(item: AccountRecord) { passwordTarget.value = item; Object.assign(passwordForm, { password: '', confirmPassword: '' }); passwordDialogVisible.value = true }
function savePassword() {
function openReset(item: UserListItem) { passwordTarget.value = item; Object.assign(passwordForm, { password: '', confirmPassword: '' }); passwordDialogVisible.value = true }
async function savePassword() {
  if (!passwordTarget.value) return
  if (!isStrongPassword(passwordForm.password)) { ElMessage.warning('新密码至少12位,并包含大小写字母、数字和特殊字符'); return }
  if (passwordForm.password !== passwordForm.confirmPassword) { ElMessage.warning('两次输入的密码不一致'); return }
  resetPassword(passwordTarget.value.id, passwordForm.password); passwordDialogVisible.value = false; ElMessage.success('密码已重置')
  try { await resetUserPassword(passwordTarget.value.id, passwordForm.password); passwordDialogVisible.value = false; ElMessage.success('密码已重置') } catch (error) { ElMessage.error(error instanceof Error ? error.message : '密码重置失败') }
}
async function deleteAccount(item: AccountRecord) {
async function deleteAccount(item: UserListItem) {
  if (item.username === 'Admin') { ElMessage.warning('系统管理员账号不能删除'); return }
  try { await ElMessageBox.confirm(`删除账号“${item.name}”后无法恢复,是否继续?`, '确认删除账号', { confirmButtonText: '删除', cancelButtonText: '取消', type: 'warning', confirmButtonClass: 'danger-confirm' }); removeAccount(item.id); currentPage.value = Math.min(currentPage.value, Math.max(1, Math.ceil(filteredAccounts.value.length / pageSize.value))); ElMessage.success('账号已删除') } catch { /* 用户取消 */ }
  try { await ElMessageBox.confirm(`删除账号“${item.name}”后无法恢复,是否继续?`, '确认删除账号', { confirmButtonText: '删除', cancelButtonText: '取消', type: 'warning', confirmButtonClass: 'danger-confirm' }) } catch { return }
  try { await deleteUsers([item.id]); ElMessage.success('账号已删除'); if (records.value.length === 1 && currentPage.value > 1) currentPage.value -= 1; await loadAccounts() } catch (error) { ElMessage.error(error instanceof Error ? error.message : '账号删除失败') }
}
onMounted(async () => { await Promise.all([loadRoleOptions(), loadAccounts()]) })
</script>
<template>
  <div class="account-page">
    <section class="filter-panel"><header class="section-heading"><div><i></i><strong>查询条件</strong><span>ACCOUNT FILTER CONDITIONS</span></div><small>共 {{ accounts.length }} 个账号</small></header><div class="filter-grid"><label><span>用户名称</span><el-input v-model="filters.name" clearable placeholder="输入名称或账号" @keyup.enter="applySearch" /></label><label><span>用户电话</span><el-input v-model="filters.phone" clearable placeholder="输入用户电话" @keyup.enter="applySearch" /></label><label><span>角色</span><el-select v-model="filters.roleId" clearable placeholder="全部角色"><el-option v-for="item in roles" :key="item.id" :label="item.name" :value="item.id" /></el-select></label><label><span>账号状态</span><el-select v-model="filters.status" clearable placeholder="全部状态"><el-option label="全部" value="全部" /><el-option v-for="item in statuses" :key="item" :label="item" :value="item" /></el-select></label><div class="filter-actions"><button class="primary-button" type="button" @click="applySearch"><el-icon><Search /></el-icon>查询</button><button class="secondary-button" type="button" @click="resetSearch"><el-icon><RefreshLeft /></el-icon>重置</button></div></div></section>
    <section class="result-panel"><div class="result-toolbar"><div><i></i><strong>账号列表</strong><span>{{ filteredAccounts.length }} RESULTS · 按创建时间倒序</span></div><button class="primary-button" type="button" @click="openCreate"><el-icon class="button-icon"><Plus /></el-icon>新增账号</button></div><div class="table-wrap"><table><thead><tr><th>序号</th><th>人员名称</th><th>登录账号</th><th>用户电话</th><th>角色</th><th>状态</th><th>创建时间</th><th class="action-column">操作</th></tr></thead><tbody><tr v-for="(item,index) in pagedAccounts" :key="item.id"><td class="id-cell">{{ (currentPage - 1) * pageSize + index + 1 }}</td><td><strong>{{ item.name }}</strong></td><td class="code-cell">{{ item.username }}</td><td>{{ item.phone || '-' }}</td><td>{{ roleMap.get(item.roleId) || '未分配' }}</td><td><span class="status-tag" :class="item.status === '启用' ? 'enabled' : 'disabled'"><i></i>{{ item.status }}</span></td><td>{{ item.createdAt }}</td><td class="action-column"><div class="row-actions"><button @click="openEdit(item)">编辑</button><button :class="item.status === '启用' ? 'danger-action' : 'enable-action'" @click="changeStatus(item)">{{ item.status === '启用' ? '禁用' : '启用' }}</button><button @click="openReset(item)">重置密码</button><button class="danger-action" @click="deleteAccount(item)">删除</button></div></td></tr><tr v-if="!pagedAccounts.length"><td colspan="8" class="empty-cell">暂无符合条件的账号</td></tr></tbody></table></div><footer class="pagination-bar"><span>显示 {{ pagedAccounts.length }} 条,共 {{ filteredAccounts.length }} 条</span><el-pagination v-model:current-page="currentPage" v-model:page-size="pageSize" :page-sizes="[10,20,50]" :total="filteredAccounts.length" layout="sizes, prev, pager, next" background /></footer></section>
    <section class="filter-panel"><header class="section-heading"><div><i></i><strong>查询条件</strong><span>ACCOUNT FILTER CONDITIONS</span></div><small>共 {{ total }} 个账号</small></header><div class="filter-grid"><label><span>用户名称</span><el-input v-model="filters.name" clearable placeholder="输入用户名称" @keyup.enter="applySearch" /></label><label><span>用户电话</span><el-input v-model="filters.phone" clearable placeholder="输入用户电话" @keyup.enter="applySearch" /></label><label><span>角色</span><el-select v-model="filters.roleId" :loading="rolesLoading" clearable placeholder="全部角色"><el-option v-for="item in roleOptions" :key="item.roleId" :label="item.roleName" :value="item.roleId" /></el-select></label><label><span>账号状态</span><el-select v-model="filters.status" clearable placeholder="全部状态"><el-option label="全部" value="" /><el-option label="启用" value="0" /><el-option label="禁用" value="1" /></el-select></label><div class="filter-actions"><button class="primary-button" type="button" :disabled="loading" @click="applySearch"><el-icon><Search /></el-icon>查询</button><button class="secondary-button" type="button" :disabled="loading" @click="resetSearch"><el-icon><RefreshLeft /></el-icon>重置</button></div></div></section>
    <section class="result-panel"><div class="result-toolbar"><div><i></i><strong>账号列表</strong><span>{{ total }} RESULTS</span></div><button class="primary-button" type="button" @click="openCreate"><el-icon class="button-icon"><Plus /></el-icon>新增账号</button></div><div v-loading="loading" class="table-wrap"><table><thead><tr><th>序号</th><th>人员名称</th><th>登录账号</th><th>用户电话</th><th>角色</th><th>状态</th><th>创建时间</th><th class="action-column">操作</th></tr></thead><tbody><tr v-for="(item,index) in records" :key="item.id"><td class="id-cell">{{ (currentPage - 1) * pageSize + index + 1 }}</td><td><strong>{{ item.name }}</strong></td><td class="code-cell">{{ item.username }}</td><td>{{ item.phone || '-' }}</td><td>{{ roleMap.get(item.roleId) || '未分配' }}</td><td><span class="status-tag" :class="item.status === '启用' ? 'enabled' : 'disabled'"><i></i>{{ item.status }}</span></td><td>{{ item.createdAt }}</td><td class="action-column"><div class="row-actions"><button @click="openEdit(item)">编辑</button><button :class="item.status === '启用' ? 'danger-action' : 'enable-action'" @click="changeStatus(item)">{{ item.status === '启用' ? '禁用' : '启用' }}</button><button @click="openReset(item)">重置密码</button><button class="danger-action" @click="deleteAccount(item)">删除</button></div></td></tr><tr v-if="!loading&&!records.length"><td colspan="8" class="empty-cell">暂无符合条件的账号</td></tr></tbody></table></div><footer class="pagination-bar"><span>显示 {{ records.length }} 条,共 {{ total }} 条</span><el-pagination :current-page="currentPage" :page-size="pageSize" :page-sizes="[10,20,50]" :total="total" layout="sizes, prev, pager, next" background @current-change="handlePageChange" @size-change="handleSizeChange" /></footer></section>
    <el-dialog v-model="accountDialogVisible" :title="editingId ? '编辑账号' : '新增账号'" width="720px" align-center destroy-on-close><div class="account-form"><label><span><b>*</b>用户名称</span><el-input v-model="accountForm.name" placeholder="请输入" /></label><label><span><b>*</b>登录账号</span><el-input v-model="accountForm.username" :disabled="Boolean(editingId)" placeholder="请输入" /></label><label><span>用户电话</span><el-input v-model="accountForm.phone" maxlength="11" placeholder="请输入" /></label><label v-if="!editingId"><span><b>*</b>初始密码</span><el-input v-model="accountForm.password" type="password" show-password placeholder="至少12位,包含大小写字母、数字和特殊字符" /></label><label><span><b>*</b>状态</span><el-switch v-model="accountForm.status" active-value="启用" inactive-value="禁用" /></label><label><span><b>*</b>所属角色</span><el-select v-model="accountForm.roleId" placeholder="请选择"><el-option v-for="item in roles" :key="item.id" :label="item.name" :value="item.id" /></el-select></label></div><template #footer><div class="dialog-footer-actions"><button class="secondary-button" @click="accountDialogVisible=false">取消</button><button class="primary-button" @click="saveAccount"><el-icon><Check /></el-icon>保存</button></div></template></el-dialog>
    <el-dialog v-model="accountDialogVisible" :title="editingId ? '编辑账号' : '新增账号'" width="720px" align-center destroy-on-close><div class="account-form"><label><span><b>*</b>用户名称</span><el-input v-model="accountForm.name" placeholder="请输入" /></label><label><span><b>*</b>登录账号</span><el-input v-model="accountForm.username" :disabled="Boolean(editingId)" placeholder="请输入" /></label><label><span>用户电话</span><el-input v-model="accountForm.phone" maxlength="11" placeholder="请输入" /></label><label v-if="!editingId"><span><b>*</b>初始密码</span><el-input v-model="accountForm.password" type="password" show-password placeholder="至少12位,包含大小写字母、数字和特殊字符" /></label><label><span><b>*</b>状态</span><el-switch v-model="accountForm.status" active-value="启用" inactive-value="禁用" /></label><label><span><b>*</b>所属角色</span><el-select v-model="accountForm.roleId" :loading="rolesLoading" placeholder="请选择"><el-option v-for="item in roleOptions" :key="item.roleId" :label="item.roleName" :value="item.roleId" /></el-select></label></div><template #footer><div class="dialog-footer-actions"><button class="secondary-button" :disabled="savingAccount" @click="accountDialogVisible=false">取消</button><button class="primary-button" :disabled="savingAccount||rolesLoading" @click="saveAccount"><el-icon><Check /></el-icon>{{ savingAccount?'保存中':'保存' }}</button></div></template></el-dialog>
    <el-dialog v-model="passwordDialogVisible" title="重置密码" width="520px" align-center destroy-on-close><div class="password-form"><label><span>用户</span><el-input :model-value="passwordTarget?.name" disabled /></label><label><span><b>*</b>新密码</span><el-input v-model="passwordForm.password" type="password" show-password placeholder="至少12位,包含大小写字母、数字和特殊字符" /></label><label><span><b>*</b>确认密码</span><el-input v-model="passwordForm.confirmPassword" type="password" show-password placeholder="请再次输入新密码" /></label></div><template #footer><div class="dialog-footer-actions"><button class="secondary-button" @click="passwordDialogVisible=false">取消</button><button class="primary-button" @click="savePassword"><el-icon><Check /></el-icon>保存</button></div></template></el-dialog>
  </div>
</template>
src/views/Role.vue
@@ -1,31 +1,37 @@
<script setup lang="ts">
import { computed, reactive, ref } from 'vue'
import { onMounted, reactive, ref } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { useRouter } from 'vue-router'
import { useAccessControl, type RoleRecord } from '@/stores/accessControl'
import { deleteRoles, getRoleList, type RoleListItem } from '@/api/roles'
const router = useRouter()
const { roles, accounts, removeRole } = useAccessControl()
const filters = reactive({ name: '' })
const appliedName = ref('')
const records = ref<RoleListItem[]>([])
const total = ref(0)
const currentPage = ref(1)
const pageSize = ref(10)
const filteredRoles = computed(() => roles.value.filter(item => !appliedName.value || item.name.toLowerCase().includes(appliedName.value.toLowerCase())).sort((a,b) => b.createdAt.localeCompare(a.createdAt)))
const pagedRoles = computed(() => filteredRoles.value.slice((currentPage.value - 1) * pageSize.value, currentPage.value * pageSize.value))
const accountCount = (roleId: number) => accounts.value.filter(item => item.roleId === roleId).length
function applySearch(){ appliedName.value = filters.name.trim(); currentPage.value = 1; ElMessage.success(`已找到 ${filteredRoles.value.length} 个角色`) }
function resetSearch(){ filters.name=''; appliedName.value=''; currentPage.value=1 }
const loading = ref(false)
const deletingRoleId = ref<number | null>(null)
async function loadRoles(){loading.value=true;try{const result=await getRoleList({pageNum:currentPage.value,pageSize:pageSize.value,roleName:appliedName.value||undefined});records.value=result.rows;total.value=result.total}catch(error){records.value=[];total.value=0;ElMessage.error(error instanceof Error?error.message:'角色列表加载失败')}finally{loading.value=false}}
function applySearch(){ appliedName.value = filters.name.trim(); currentPage.value = 1; loadRoles() }
function resetSearch(){ filters.name=''; appliedName.value=''; currentPage.value=1; loadRoles() }
function handlePageChange(page:number){currentPage.value=page;loadRoles()}
function handleSizeChange(size:number){pageSize.value=size;currentPage.value=1;loadRoles()}
function openCreate(){ router.push('/role/new') }
function openDetail(item:RoleRecord){ router.push(`/role/${item.id}`) }
function openEdit(item:RoleRecord){ router.push(`/role/${item.id}/edit`) }
async function deleteRole(item:RoleRecord){
function openDetail(item:RoleListItem){ router.push(`/role/${item.id}`) }
function openEdit(item:RoleListItem){ router.push(`/role/${item.id}/edit`) }
async function deleteRole(item:RoleListItem){
  if(item.builtIn){ ElMessage.warning('系统内置角色不能删除'); return }
  const count=accountCount(item.id)
  const count=item.accountCount
  if(count){ ElMessage.warning(`该角色仍关联 ${count} 个账号,请先调整账号角色`); return }
  try{ await ElMessageBox.confirm(`删除角色“${item.name}”后无法恢复,是否继续?`,'确认删除角色',{confirmButtonText:'删除',cancelButtonText:'取消',type:'warning',confirmButtonClass:'danger-confirm'}); removeRole(item.id); ElMessage.success('角色已删除') }catch{/* 用户取消 */}
  try{await ElMessageBox.confirm(`删除角色“${item.name}”后无法恢复,是否继续?`,'确认删除角色',{confirmButtonText:'删除',cancelButtonText:'取消',type:'warning',confirmButtonClass:'danger-confirm'})}catch{return}
  deletingRoleId.value=item.id
  try{await deleteRoles([item.id]);ElMessage.success('角色已删除');if(records.value.length===1&&currentPage.value>1)currentPage.value-=1;await loadRoles()}catch(error){ElMessage.error(error instanceof Error?error.message:'删除角色失败')}finally{deletingRoleId.value=null}
}
onMounted(loadRoles)
</script>
<template><div class="role-page"><section class="filter-panel"><header class="section-heading"><div><i></i><strong>查询条件</strong><span>ROLE FILTER CONDITIONS</span></div><small>共 {{ roles.length }} 个角色</small></header><div class="filter-grid"><label><span>角色名称</span><el-input v-model="filters.name" clearable placeholder="输入角色名称" @keyup.enter="applySearch" /></label><div class="filter-actions"><button class="primary-button" type="button" @click="applySearch"><el-icon><Search /></el-icon>查询</button><button class="secondary-button" type="button" @click="resetSearch"><el-icon><RefreshLeft /></el-icon>重置</button></div></div></section><section class="result-panel"><div class="result-toolbar"><div><i></i><strong>角色列表</strong><span>{{ filteredRoles.length }} RESULTS</span></div><button class="primary-button" type="button" @click="openCreate"><el-icon class="button-icon"><Plus /></el-icon>新增角色</button></div><div class="table-wrap"><table><thead><tr><th>序号</th><th>角色名称</th><th>关联账号数</th><th>创建时间</th><th>备注</th><th class="action-column">操作</th></tr></thead><tbody><tr v-for="(item,index) in pagedRoles" :key="item.id"><td class="id-cell">{{ (currentPage-1)*pageSize+index+1 }}</td><td><strong>{{ item.name }}</strong><span v-if="item.builtIn" class="built-in">内置</span></td><td>{{ accountCount(item.id) }}</td><td>{{ item.createdAt }}</td><td :title="item.remark">{{ item.remark || '-' }}</td><td class="action-column"><div class="row-actions"><button @click="openDetail(item)">详情</button><button @click="openEdit(item)">编辑</button><button class="delete-action" :disabled="item.builtIn" @click="deleteRole(item)">删除</button></div></td></tr><tr v-if="!pagedRoles.length"><td colspan="6" class="empty-cell">暂无符合条件的角色</td></tr></tbody></table></div><footer class="pagination-bar"><span>显示 {{ pagedRoles.length }} 条,共 {{ filteredRoles.length }} 条</span><el-pagination v-model:current-page="currentPage" v-model:page-size="pageSize" :page-sizes="[10,20,50]" :total="filteredRoles.length" layout="sizes, prev, pager, next" background /></footer></section></div></template>
<template><div class="role-page"><section class="filter-panel"><header class="section-heading"><div><i></i><strong>查询条件</strong><span>ROLE FILTER CONDITIONS</span></div><small>共 {{ total }} 个角色</small></header><div class="filter-grid"><label><span>角色名称</span><el-input v-model="filters.name" clearable placeholder="输入角色名称" @keyup.enter="applySearch" /></label><div class="filter-actions"><button class="primary-button" type="button" :disabled="loading" @click="applySearch"><el-icon><Search /></el-icon>查询</button><button class="secondary-button" type="button" :disabled="loading" @click="resetSearch"><el-icon><RefreshLeft /></el-icon>重置</button></div></div></section><section class="result-panel"><div class="result-toolbar"><div><i></i><strong>角色列表</strong><span>{{ total }} RESULTS</span></div><button class="primary-button" type="button" @click="openCreate"><el-icon class="button-icon"><Plus /></el-icon>新增角色</button></div><div v-loading="loading" class="table-wrap"><table><thead><tr><th>序号</th><th>角色名称</th><th>关联账号数</th><th>创建时间</th><th>备注</th><th class="action-column">操作</th></tr></thead><tbody><tr v-for="(item,index) in records" :key="item.id"><td class="id-cell">{{ (currentPage-1)*pageSize+index+1 }}</td><td><strong>{{ item.name }}</strong><span v-if="item.builtIn" class="built-in">内置</span></td><td>{{ item.accountCount }}</td><td>{{ item.createdAt }}</td><td :title="item.remark">{{ item.remark || '-' }}</td><td class="action-column"><div class="row-actions"><button @click="openDetail(item)">详情</button><button @click="openEdit(item)">编辑</button><button class="delete-action" :disabled="item.builtIn||deletingRoleId!==null" @click="deleteRole(item)">{{ deletingRoleId===item.id?'删除中':'删除' }}</button></div></td></tr><tr v-if="!loading&&!records.length"><td colspan="6" class="empty-cell">暂无符合条件的角色</td></tr></tbody></table></div><footer class="pagination-bar"><span>显示 {{ records.length }} 条,共 {{ total }} 条</span><el-pagination :current-page="currentPage" :page-size="pageSize" :page-sizes="[10,20,50]" :total="total" layout="sizes, prev, pager, next" background @current-change="handlePageChange" @size-change="handleSizeChange" /></footer></section></div></template>
<style scoped>
.role-page{width:100%}.primary-button,.secondary-button{height:34px;display:inline-flex;align-items:center;justify-content:center;gap:6px;padding:0 13px;border:1px solid transparent;border-radius:3px;font-size:13px;font-weight:500;cursor:pointer;white-space:nowrap}.primary-button{color:#fff;background:var(--blue);border-color:var(--blue)}.primary-button:hover{background:#176fcb;border-color:#176fcb}.secondary-button{color:#52606d;background:#fff;border-color:var(--line-strong)}.primary-button .button-icon{width:1em!important;height:1em!important;flex:0 0 1em;background:transparent!important}.filter-panel,.result-panel{background:#fff;border:1px solid #d8e1e9}.filter-panel{margin-bottom:16px}.section-heading,.result-toolbar{display:flex;align-items:center;justify-content:space-between;background:var(--surface-soft);border-bottom:1px solid var(--line)}.section-heading{height:44px;padding:0 16px}.section-heading>div,.result-toolbar>div{display:flex;align-items:center;gap:8px}.section-heading>div>i,.result-toolbar>div:first-child>i{width:3px;height:16px;background:#2b78a3}.section-heading strong,.result-toolbar strong{color:var(--ink);font-size:15px}.section-heading span,.result-toolbar span{color:#81909d;font-size:10px}.section-heading small{color:#647380;font-size:11px}.filter-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:18px;padding:16px}.filter-grid label{display:grid;grid-template-columns:72px minmax(0,1fr);align-items:center;gap:8px}.filter-grid label>span{color:#52606d;font-size:12px}.filter-actions{grid-column:4;display:flex;justify-content:flex-end;gap:8px}.result-toolbar{height:50px;padding:0 16px}.table-wrap{overflow-x:auto}table{width:100%;min-width:1050px;table-layout:fixed;border-collapse:collapse;color:#52606d;font-size:13px}th,td{height:50px;padding:0 12px;border-right:1px solid #dde7f0;border-bottom:1px solid #dde7f0;text-align:left;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}th{height:40px;background:#eaf4ff;border-color:#c7e0ff;font-size:12px}th:nth-child(1){width:70px}th:nth-child(2){width:210px}th:nth-child(3){width:130px}th:nth-child(4){width:180px}th:nth-child(5){width:330px}th:last-child{width:190px}.action-column{position:sticky;right:0;z-index:2;background:#fff;box-shadow:inset 1px 0 0 #b4c7d9,-8px 0 14px rgba(23,49,74,.12);text-align:center;overflow:visible}th.action-column{z-index:3;background:#eaf4ff}.row-actions{display:flex;justify-content:center;gap:16px}.row-actions button{border:0;color:var(--blue);background:transparent;font-size:12px;cursor:pointer}.row-actions .delete-action{color:#ad4a4a}.row-actions button:disabled{color:#aab4bd;cursor:not-allowed}.id-cell{color:#647380;font-family:'Cascadia Mono',Consolas,monospace}td strong{color:#26333f}.built-in{margin-left:8px;padding:2px 5px;border:1px solid #bdd6ed;color:#37688e;background:#edf5fb;font-size:10px}.empty-cell{height:160px;color:#8795a3;text-align:center}.pagination-bar{min-height:58px;display:flex;align-items:center;justify-content:space-between;padding:8px 14px}.pagination-bar>span{color:#7b8794;font-size:11px}@media(max-width:900px){.filter-grid{grid-template-columns:repeat(2,minmax(0,1fr))}.filter-actions{grid-column:2}}@media(max-width:600px){.filter-grid{grid-template-columns:1fr}.filter-actions{grid-column:1}.section-heading span,.result-toolbar span{display:none}.pagination-bar{align-items:flex-start;flex-direction:column;gap:10px}}
</style>
src/views/RoleForm.vue
@@ -1,24 +1,29 @@
<script setup lang="ts">
import { computed, reactive, ref } from 'vue'
import { computed, onMounted, reactive, ref } from 'vue'
import { ElMessage } from 'element-plus'
import { useRoute, useRouter } from 'vue-router'
import { permissionCatalog, permissionKey, useAccessControl } from '@/stores/accessControl'
import { createRole as createRoleRequest, getRole, getRolePermissionList, updateRole as updateRoleRequest, type RolePermissionModule } from '@/api/roles'
import { permissionKey, useAccessControl } from '@/stores/accessControl'
const route=useRoute(); const router=useRouter(); const {roles,createRole,updateRole}=useAccessControl()
const route=useRoute(); const router=useRouter(); const {roles}=useAccessControl()
const roleId=computed(()=>Number(route.params.id)||null)
const readonly=computed(()=>route.name==='role-detail')
const editingRole=computed(()=>roleId.value?roles.value.find(item=>item.id===roleId.value)||null:null)
const form=reactive({name:'',remark:''}); const selected=ref<string[]>([]); const saving=ref(false)
const modules=computed(()=>[...new Set(permissionCatalog.map(item=>item.module))].map(module=>({module,pages:permissionCatalog.filter(item=>item.module===module)})))
if(editingRole.value){ form.name=editingRole.value.name; form.remark=editingRole.value.remark; selected.value=[...editingRole.value.permissions] }
function keysForPage(page:string){const target=permissionCatalog.find(item=>item.page===page);return target?target.actions.map(action=>permissionKey(page,action)):[]}
function keysForModule(module:string){return permissionCatalog.filter(item=>item.module===module).flatMap(item=>item.actions.map(action=>permissionKey(item.page,action)))}
const modules=ref<RolePermissionModule[]>([]); const permissionsLoading=ref(false)
const detailMenuIds=ref(new Set<number>())
function keysForPage(page:RolePermissionModule['pages'][number]){return page.permissions.map(item=>permissionKey(page.pageName,item.menuName))}
function keysForModule(module:RolePermissionModule){return module.pages.flatMap(keysForPage)}
function allChecked(keys:string[]){return keys.length>0&&keys.every(key=>selected.value.includes(key))}
function someChecked(keys:string[]){return keys.some(key=>selected.value.includes(key))&&!allChecked(keys)}
function toggleKeys(keys:string[],checked:boolean){const next=new Set(selected.value);keys.forEach(key=>checked?next.add(key):next.delete(key));selected.value=[...next]}
async function saveRole(){const name=form.name.trim();if(!name){ElMessage.warning('请输入角色名称');return}if(roles.value.some(item=>item.id!==roleId.value&&item.name.toLowerCase()===name.toLowerCase())){ElMessage.warning('角色名称已存在');return}if(!selected.value.length){ElMessage.warning('请至少选择一项权限');return}saving.value=true;const payload={name,remark:form.remark.trim(),permissions:[...selected.value]};roleId.value?updateRole(roleId.value,payload):createRole(payload);saving.value=false;ElMessage.success(roleId.value?'角色已更新':'角色已新增');await router.push('/role')}
function selectedMenuIds(){const ids=new Set<number>();modules.value.forEach(module=>{let moduleSelected=false;module.pages.forEach(page=>{let pageSelected=false;page.permissions.forEach(permission=>{if(selected.value.includes(permissionKey(page.pageName,permission.menuName))){ids.add(permission.menuId);pageSelected=true;moduleSelected=true}});if(pageSelected)ids.add(page.menuId)});if(moduleSelected)ids.add(module.menuId)});return [...ids]}
function keysForMenuIds(menuIds:number[]){const ids=new Set(menuIds);return modules.value.flatMap(module=>module.pages.flatMap(page=>page.permissions.filter(permission=>ids.has(permission.menuId)).map(permission=>permissionKey(page.pageName,permission.menuName))))}
async function loadPermissions(){permissionsLoading.value=true;try{modules.value=await getRolePermissionList();if(roleId.value){const detail=await getRole(roleId.value);form.name=detail.name;form.remark=detail.remark;detailMenuIds.value=new Set(detail.menuIds);selected.value=keysForMenuIds(detail.menuIds)}else if(editingRole.value){form.name=editingRole.value.name;form.remark=editingRole.value.remark;selected.value=[...editingRole.value.permissions]}}catch(error){ElMessage.error(error instanceof Error?error.message:'角色详情加载失败')}finally{permissionsLoading.value=false}}
onMounted(loadPermissions)
async function saveRole(){const name=form.name.trim();if(!name){ElMessage.warning('请输入角色名称');return}if(roles.value.some(item=>item.id!==roleId.value&&item.name.toLowerCase()===name.toLowerCase())){ElMessage.warning('角色名称已存在');return}if(!selected.value.length){ElMessage.warning('请至少选择一项权限');return}saving.value=true;try{if(roleId.value){await updateRoleRequest({roleId:roleId.value,roleName:name,roleKey:'common',roleSort:1,status:'0',menuIds:selectedMenuIds(),remark:form.remark.trim()})}else{await createRoleRequest({roleName:name,roleKey:'common',roleSort:1,status:'0',menuIds:selectedMenuIds(),remark:form.remark.trim()})}ElMessage.success(roleId.value?'角色已更新':'角色已新增');await router.push('/role')}catch(error){ElMessage.error(error instanceof Error?error.message:'角色保存失败')}finally{saving.value=false}}
</script>
<template><div class="role-form-page"><div class="page-tools"><button class="back-button" @click="router.push('/role')"><el-icon><ArrowLeft /></el-icon>返回角色管理</button><span>{{ readonly?'ROLE DETAIL':roleId?'EDIT ROLE':'NEW ROLE' }}</span></div><section class="base-panel"><header class="panel-heading"><i></i><strong>{{ readonly?'角色详情':roleId?'编辑角色':'新增角色' }}</strong></header><div class="base-form"><label><span><b>*</b>角色名称</span><el-input v-model="form.name" :disabled="readonly" placeholder="请输入" /></label><label><span>备注</span><el-input v-model="form.remark" :disabled="readonly" placeholder="请输入" /></label></div></section><section class="permission-panel"><header class="panel-heading"><i></i><strong>操作权限</strong><small>按照模块、页面和具体操作配置角色能力</small></header><div class="permission-table-wrap"><table><thead><tr><th>模块</th><th>页面</th><th>权限</th></tr></thead><tbody v-for="group in modules" :key="group.module"><tr v-for="(page,index) in group.pages" :key="page.page"><td v-if="index===0" :rowspan="group.pages.length" class="module-cell"><el-checkbox :model-value="allChecked(keysForModule(group.module))" :indeterminate="someChecked(keysForModule(group.module))" :disabled="readonly" @change="toggleKeys(keysForModule(group.module),Boolean($event))">{{ group.module }}</el-checkbox></td><td><el-checkbox :model-value="allChecked(keysForPage(page.page))" :indeterminate="someChecked(keysForPage(page.page))" :disabled="readonly" @change="toggleKeys(keysForPage(page.page),Boolean($event))">{{ page.page }}</el-checkbox></td><td><el-checkbox-group v-model="selected" :disabled="readonly"><el-checkbox v-for="action in page.actions" :key="action" :value="permissionKey(page.page,action)">{{ action }}</el-checkbox></el-checkbox-group></td></tr></tbody></table></div></section><footer class="form-actions"><button class="secondary-button" @click="router.push('/role')">返回</button><button v-if="!readonly" class="primary-button" :disabled="saving" @click="saveRole"><el-icon><Check /></el-icon>{{ saving?'保存中':'保存' }}</button><button v-else class="primary-button" @click="router.push(`/role/${roleId}/edit`)"><el-icon><Edit /></el-icon>编辑角色</button></footer></div></template>
<template><div class="role-form-page"><div class="page-tools"><button class="back-button" @click="router.push('/role')"><el-icon><ArrowLeft /></el-icon>返回角色管理</button><span>{{ readonly?'ROLE DETAIL':roleId?'EDIT ROLE':'NEW ROLE' }}</span></div><section class="base-panel"><header class="panel-heading"><i></i><strong>{{ readonly?'角色详情':roleId?'编辑角色':'新增角色' }}</strong></header><div class="base-form"><label><span><b>*</b>角色名称</span><el-input v-model="form.name" :disabled="readonly" placeholder="请输入" /></label><label><span>备注</span><el-input v-model="form.remark" :disabled="readonly" placeholder="请输入" /></label></div></section><section class="permission-panel"><header class="panel-heading"><i></i><strong>操作权限</strong><small>按照模块、页面和具体操作配置角色能力</small></header><div v-loading="permissionsLoading" class="permission-table-wrap"><table><thead><tr><th>模块</th><th>页面</th><th>权限</th></tr></thead><tbody v-for="group in modules" :key="group.menuId"><tr v-for="(page,index) in group.pages" :key="page.menuId"><td v-if="index===0" :rowspan="group.pages.length" class="module-cell"><el-checkbox :model-value="readonly?detailMenuIds.has(group.menuId):allChecked(keysForModule(group))" :indeterminate="readonly?false:someChecked(keysForModule(group))" :disabled="readonly" @change="toggleKeys(keysForModule(group),Boolean($event))">{{ group.moduleName }}</el-checkbox></td><td><el-checkbox :model-value="readonly?detailMenuIds.has(page.menuId):allChecked(keysForPage(page))" :indeterminate="readonly?false:someChecked(keysForPage(page))" :disabled="readonly" @change="toggleKeys(keysForPage(page),Boolean($event))">{{ page.pageName }}</el-checkbox></td><td><el-checkbox-group v-model="selected" :disabled="readonly"><el-checkbox v-for="action in page.permissions" :key="action.menuId" :value="permissionKey(page.pageName,action.menuName)">{{ action.menuName }}</el-checkbox></el-checkbox-group></td></tr></tbody><tbody v-if="!permissionsLoading&&!modules.length"><tr><td colspan="3" class="empty-cell">暂无操作权限数据</td></tr></tbody></table></div></section><footer class="form-actions"><button class="secondary-button" @click="router.push('/role')">返回</button><button v-if="!readonly" class="primary-button" :disabled="saving||permissionsLoading" @click="saveRole"><el-icon><Check /></el-icon>{{ saving?'保存中':'保存' }}</button><button v-else class="primary-button" @click="router.push(`/role/${roleId}/edit`)"><el-icon><Edit /></el-icon>编辑角色</button></footer></div></template>
<style scoped>
.role-form-page{width:100%;padding-bottom:70px}.page-tools{height:34px;display:flex;align-items:center;justify-content:space-between}.page-tools>span{color:#7f8d99;font-size:10px}.back-button{display:inline-flex;align-items:center;gap:6px;border:0;color:#52606d;background:transparent;font-size:13px;cursor:pointer}.base-panel,.permission-panel{margin-bottom:16px;border:1px solid #d8e1e9;background:#fff}.panel-heading{height:48px;display:flex;align-items:center;gap:8px;padding:0 18px;border-bottom:1px solid var(--line);background:#fbfdff}.panel-heading i{width:3px;height:16px;background:#2b78a3}.panel-heading strong{color:#26333f;font-size:15px}.panel-heading small{margin-left:auto;color:#8795a3;font-size:11px}.base-form{display:grid;grid-template-columns:minmax(220px,1fr) minmax(320px,2fr);gap:28px;padding:20px 24px}.base-form label{display:grid;gap:8px}.base-form label>span{color:#52606d;font-size:12px}.base-form b{color:#b34f4f}.permission-table-wrap{overflow-x:auto;padding:18px 24px 24px}table{width:100%;min-width:900px;border-collapse:collapse;color:#52606d;font-size:12px}th,td{height:42px;padding:8px 14px;border:1px solid #cbd8e4;text-align:left}th{color:#fff;background:#3c6497;text-align:center;font-size:12px}th:nth-child(1){width:190px}th:nth-child(2){width:210px}.module-cell{background:#f2f7fb;vertical-align:middle}.permission-table-wrap :deep(.el-checkbox-group){display:flex;flex-wrap:wrap;gap:4px 24px}.permission-table-wrap :deep(.el-checkbox){margin-right:0}.primary-button,.secondary-button{height:34px;display:inline-flex;align-items:center;justify-content:center;gap:6px;padding:0 18px;border:1px solid transparent;border-radius:3px;font-size:13px;cursor:pointer}.primary-button{color:#fff;background:var(--blue);border-color:var(--blue)}.secondary-button{color:#52606d;background:#fff;border-color:var(--line-strong)}.form-actions{position:sticky;bottom:0;z-index:4;display:flex;justify-content:flex-end;gap:10px;padding:14px 0;border-top:1px solid #d8e1e9;background:rgba(244,248,251,.96)}@media(max-width:700px){.base-form{grid-template-columns:1fr}.panel-heading small{display:none}.permission-table-wrap{padding:12px}}
.role-form-page{width:100%;padding-bottom:70px}.page-tools{height:34px;display:flex;align-items:center;justify-content:space-between}.page-tools>span{color:#7f8d99;font-size:10px}.back-button{display:inline-flex;align-items:center;gap:6px;border:0;color:#52606d;background:transparent;font-size:13px;cursor:pointer}.base-panel,.permission-panel{margin-bottom:16px;border:1px solid #d8e1e9;background:#fff}.panel-heading{height:48px;display:flex;align-items:center;gap:8px;padding:0 18px;border-bottom:1px solid var(--line);background:#fbfdff}.panel-heading i{width:3px;height:16px;background:#2b78a3}.panel-heading strong{color:#26333f;font-size:15px}.panel-heading small{margin-left:auto;color:#8795a3;font-size:11px}.base-form{display:grid;grid-template-columns:minmax(220px,1fr) minmax(320px,2fr);gap:28px;padding:20px 24px}.base-form label{display:grid;gap:8px}.base-form label>span{color:#52606d;font-size:12px}.base-form b{color:#b34f4f}.permission-table-wrap{min-height:120px;overflow-x:auto;padding:18px 24px 24px}table{width:100%;min-width:900px;border-collapse:collapse;color:#52606d;font-size:12px}th,td{height:42px;padding:8px 14px;border:1px solid #cbd8e4;text-align:left}th{color:#fff;background:#3c6497;text-align:center;font-size:12px}th:nth-child(1){width:190px}th:nth-child(2){width:210px}.module-cell{background:#f2f7fb;vertical-align:middle}.empty-cell{height:80px;color:#8795a3;text-align:center}.permission-table-wrap :deep(.el-checkbox-group){display:flex;flex-wrap:wrap;gap:4px 24px}.permission-table-wrap :deep(.el-checkbox){margin-right:0}.primary-button,.secondary-button{height:34px;display:inline-flex;align-items:center;justify-content:center;gap:6px;padding:0 18px;border:1px solid transparent;border-radius:3px;font-size:13px;cursor:pointer}.primary-button{color:#fff;background:var(--blue);border-color:var(--blue)}.secondary-button{color:#52606d;background:#fff;border-color:var(--line-strong)}.form-actions{position:sticky;bottom:0;z-index:4;display:flex;justify-content:flex-end;gap:10px;padding:14px 0;border-top:1px solid #d8e1e9;background:rgba(244,248,251,.96)}@media(max-width:700px){.base-form{grid-template-columns:1fr}.panel-heading small{display:none}.permission-table-wrap{padding:12px}}
</style>