| New file |
| | |
| | | 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', |
| | | }) |
| | | } |
| | |
| | | userId: number |
| | | userName: string | null |
| | | nickName: string | null |
| | | phonenumber?: string | null |
| | | roleId?: number | null |
| | | createTime?: string | null |
| | | status: string | null |
| | | } |
| | | |
| | |
| | | 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[]> { |
| | |
| | | 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', |
| | | }) |
| | | } |
| | |
| | | <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 } |
| | |
| | | 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> |
| | |
| | | <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&¤tPage.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> |
| | |
| | | <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> |