This commit is contained in:
2026-09-15 09:58:15 +08:00
parent 3f998bdb88
commit 4bf66d9571
90 changed files with 1599 additions and 1388 deletions
+8
View File
@@ -0,0 +1,8 @@
/* IAM-only layout; the shared stylesheet owns forms, dialogs and row buttons. */
.iam-density .permission-tree { margin-top: 0; padding: 0; border-top: 0; }
.iam-density .permission-tree .el-tree-node__content { height: auto; min-height: var(--admin-row-height); }
.iam-density .permission-node { align-items: flex-start; gap: var(--admin-space-3); padding-right: var(--admin-space-5); }
.iam-density .permission-node span { white-space: normal; overflow-wrap: anywhere; }
.iam-density :is(.permission-node code, .code-value, .endpoint-cell code) { font-size: var(--admin-font-caption); }
.iam-density .form-callout { gap: var(--admin-space-4); margin-top: var(--admin-space-5); padding: var(--admin-space-5); border-radius: var(--admin-radius); }
.iam-density .form-callout strong, .iam-density .form-callout p { font-size: var(--admin-font-caption); }
@@ -79,11 +79,11 @@ enableAutoUnmount(afterEach)
afterAll(() => vi.unstubAllGlobals())
describe('InternalUsersView platform internal user management', () => {
it('renders row business actions as colored outlined buttons with prototype icons', () => {
expect(internalUsersSource).toContain('size="small" plain type="primary" @click="openEdit')
expect(internalUsersSource).toContain('size="small" plain :type="(row as PlatformManagedUser).status')
expect(internalUsersSource).toContain('<Pencil :size="15" />编辑')
expect(internalUsersSource).toContain('<Power :size="15" />')
it('renders row business actions as shared outlined text buttons', () => {
expect(internalUsersSource).toContain('size="small" data-tone="neutral" @click="openEdit')
expect(internalUsersSource).toContain('size="small" :data-tone="(row as PlatformManagedUser).status')
expect(internalUsersSource).toMatch(/>\s*编辑\s*<\/el-button>/)
expect(internalUsersSource).toContain(':data-tone=')
expect(internalUsersSource).not.toContain('v-if="canChangeStatus" link')
})
+16 -14
View File
@@ -1,7 +1,9 @@
<script setup lang="ts">
import '@/styles/adminDensity.css'
import '../iamDensity.css'
import { computed, nextTick, onMounted, reactive, ref, watch } from 'vue'
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus'
import { Pencil, Plus, Power } from '@lucide/vue'
import { Plus } from '@lucide/vue'
import {
changePlatformManagedUserStatus,
createPlatformManagedUser,
@@ -301,7 +303,7 @@ async function toggleStatus(row: PlatformManagedUser) {
const targetStatus = row.status === 'ACTIVE' ? 'DISABLED' : 'ACTIVE'
const targetLabel = targetStatus === 'ACTIVE' ? '启用' : '禁用'
try {
await ElMessageBox.confirm(`确认${targetLabel}用户“${row.displayName}”吗?`, `${targetLabel}用户`, {
await ElMessageBox.confirm(`确认${targetLabel}用户“${row.displayName}”吗?`, `${targetLabel}用户`, { customClass: 'admin-density-confirm',
confirmButtonText: `确认${targetLabel}`,
cancelButtonText: '取消',
type: targetStatus === 'DISABLED' ? 'warning' : 'info',
@@ -342,7 +344,7 @@ onMounted(loadInitialData)
</script>
<template>
<AdminPage as="div">
<AdminPage class="admin-density iam-density" as="div">
<ListPageLayout aria-label="内部用户管理">
<template #toolbar>
<QueryToolbar
@@ -365,16 +367,16 @@ onMounted(loadInitialData)
:loading="loading" :total="total" aria-label="内部用户列表" row-key="membershipId" empty-text="没有符合筛选条件的内部用户">
<template #toolbar-actions><el-button v-if="canCreate" type="primary" @click="openCreate"><Plus :size="16" />新增用户</el-button></template>
<template #actions="{ row }">
<div class="table-actions">
<el-button v-if="canEdit || canChangeStatus || canResetPassword" size="small" plain type="primary" @click="openEdit(row as PlatformManagedUser)">
<Pencil :size="15" />编辑
<div class="table-actions density-actions">
<el-button class="density-action" v-if="canEdit || canChangeStatus || canResetPassword" size="small" data-tone="neutral" @click="openEdit(row as PlatformManagedUser)">
编辑
</el-button>
<el-button v-if="canChangeStatus" size="small" plain :type="(row as PlatformManagedUser).status === 'ACTIVE' ? 'danger' : 'success'"
<el-button class="density-action" v-if="canChangeStatus" size="small" :data-tone="(row as PlatformManagedUser).status === 'ACTIVE' ? 'danger' : 'success'"
:loading="rowActionId === String((row as PlatformManagedUser).membershipId)"
:disabled="cannotDisable(row as PlatformManagedUser)"
:title="(row as PlatformManagedUser).protectedAccount ? '系统管理员不能被禁用' : isCurrentUser(row as PlatformManagedUser) ? '不能禁用当前登录用户' : undefined"
@click="toggleStatus(row as PlatformManagedUser)">
<Power :size="15" />{{ (row as PlatformManagedUser).status === 'ACTIVE' ? '禁用' : '启用' }}
{{ (row as PlatformManagedUser).status === 'ACTIVE' ? '禁用' : '启用' }}
</el-button>
</div>
</template>
@@ -391,9 +393,9 @@ onMounted(loadInitialData)
</section>
</ListPageLayout>
<AppDialog v-model="editorOpen" :title="editorLabel" width="min(650px, calc(100vw - 28px))" class="internal-user-editor-dialog" destroy-on-close :confirm-loading="saving" :confirm-text="editingUser ? '保存修改' : '确认添加'" @confirm="saveUser">
<el-form ref="formRef" :model="form" :rules="formRules" label-position="top">
<FormSection title="用户资料" description="维护登录身份、联系方式与平台角色。">
<AppDialog v-model="editorOpen" :title="editorLabel" width="min(650px, calc(100vw - 28px))" class="internal-user-editor-dialog admin-density-dialog iam-density" destroy-on-close :confirm-loading="saving" :confirm-text="editingUser ? '保存修改' : '确认添加'" @confirm="saveUser">
<el-form class="density-form" ref="formRef" :model="form" :rules="formRules" label-position="top">
<FormSection class="density-form-section" title="用户资料" description="维护登录身份、联系方式与平台角色。">
<div class="form-grid two-cols">
<el-form-item label="用户名" prop="username" required>
<el-input v-model="form.username" :disabled="Boolean(editingUser)" placeholder="登录用户名" maxlength="64" />
@@ -439,7 +441,7 @@ onMounted(loadInitialData)
</div>
</FormSection>
<FormSection v-if="!editingUser || canResetPassword" title="登录密码" description="密码为 8-64 位,并同时包含字母和数字。">
<FormSection class="density-form-section" v-if="!editingUser || canResetPassword" title="登录密码" description="密码为 8-64 位,并同时包含字母和数字。">
<el-checkbox v-if="editingUser" v-model="form.resetPassword">同时重置密码</el-checkbox>
<div v-if="!editingUser || form.resetPassword" class="form-grid two-cols password-fields">
<el-form-item :label="editingUser ? '新密码' : '初始密码'" prop="password" required>
@@ -456,8 +458,8 @@ onMounted(loadInitialData)
</template>
<style scoped>
.password-fields { margin-top: 16px; }
.field-help, .field-error { margin: 6px 0 0; font-size: 12px; line-height: 1.5; }
.password-fields { margin-top: var(--admin-space-6); }
.field-help, .field-error { margin: 6px 0 0; font-size: var(--admin-font-caption); line-height: 1.5; }
.field-help { color: var(--cc-text-secondary); }
.field-error { color: var(--el-color-danger); }
:global(.internal-user-editor-dialog) { max-height: min(760px, calc(100vh - 32px)); display: flex; flex-direction: column; }
@@ -0,0 +1,69 @@
import ElementPlus, { type FormInstance } from 'element-plus'
import { createPinia, setActivePinia } from 'pinia'
import { enableAutoUnmount, flushPromises, mount } from '@vue/test-utils'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { useAuthStore } from '@/core/auth/auth'
import AppDialog from '@/shared/components/AppDialog.vue'
import { createPlatformPermission, listPlatformPermissions, updatePlatformPermission } from '../api/platformIam'
import PermissionsView from './PermissionsView.vue'
vi.mock('../api/platformIam', () => ({
listPlatformPermissions: vi.fn(), createPlatformPermission: vi.fn(), updatePlatformPermission: vi.fn(),
changePlatformPermissionStatus: vi.fn(), deletePlatformPermission: vi.fn(),
}))
enableAutoUnmount(afterEach)
afterEach(() => { document.body.innerHTML = ''; vi.unstubAllGlobals(); vi.clearAllMocks() })
describe('permission editor modal', () => {
it.each(['create', 'edit'])('keeps the %s form in the centered dialog and preserves failed writes', async mode => {
vi.stubGlobal('ResizeObserver', class { observe() {} unobserve() {} disconnect() {} })
const pinia = createPinia()
setActivePinia(pinia)
const auth = useAuthStore()
auth.isServerSession = true
auth.permissions = ['platform:permission:list', 'platform:permission:create', 'platform:permission:edit']
vi.mocked(listPlatformPermissions).mockResolvedValue([{
id: '90071992547409931', tenantId: 0, appCode: 'PLATFORM', permissionType: 'MENU', parentId: 0,
permissionCode: 'platform:sample:list', permissionName: '测试菜单', routePath: '/sample',
httpMethod: null, sortNo: 10, status: 'ACTIVE',
}])
vi.mocked(createPlatformPermission).mockRejectedValue(new Error('保存失败'))
vi.mocked(updatePlatformPermission).mockRejectedValue(new Error('保存失败'))
const wrapper = mount(PermissionsView, { attachTo: document.body, global: { plugins: [pinia, ElementPlus] } })
await flushPromises()
await wrapper.findAll(mode === 'create' ? 'button' : '.el-table__body-wrapper button').find(button => button.text().trim() === (mode === 'create' ? '新增权限' : '编辑'))!.trigger('click')
await flushPromises()
const dialog = document.querySelector<HTMLElement>('.el-dialog.permission-editor-dialog.admin-density-dialog')!
expect(dialog).not.toBeNull()
expect(dialog.style.getPropertyValue('--el-dialog-width')).toBe('min(720px, calc(100vw - 28px))')
expect(document.querySelector('.el-drawer')).toBeNull()
expect(dialog.querySelectorAll('.el-form-item')).toHaveLength(6)
const codeInput = dialog.querySelector<HTMLInputElement>('input[placeholder="如:platform:customer:list"]')!
expect(codeInput.disabled).toBe(mode === 'edit')
// Vitest externalizes Element Plus: its CJS validator interop differs from the Vite browser bundle.
// Exercise the dialog's validation gate explicitly, without changing production rules.
const form = wrapper.findComponent({ name: 'ElForm' })
expect(form.props('rules').name).toEqual(expect.arrayContaining([expect.objectContaining({ required: true })]))
const validate = vi.spyOn(form.vm.$.exposed as FormInstance, 'validate').mockResolvedValue(true)
if (mode === 'create') {
validate.mockRejectedValueOnce({ name: [{ message: '请输入权限名称' }] })
wrapper.getComponent(AppDialog).vm.$emit('confirm')
await flushPromises()
expect(createPlatformPermission).not.toHaveBeenCalled()
for (const [placeholder, value] of [['用户可理解的中文名称', '新增菜单'], ['如:platform:customer:list', 'platform:new:list'], ['如:/customers', '/new']]) {
const input = dialog.querySelector<HTMLInputElement>(`input[placeholder="${placeholder}"]`)!
input.value = value!
input.dispatchEvent(new Event('input', { bubbles: true }))
}
}
await flushPromises()
wrapper.getComponent(AppDialog).vm.$emit('confirm')
await flushPromises()
if (mode === 'create') expect(createPlatformPermission).toHaveBeenCalledWith({ type: 'MENU', code: 'platform:new:list', name: '新增菜单', parentId: '0', routePath: '/new', httpMethod: null, sortNo: 10 })
else expect(updatePlatformPermission).toHaveBeenCalledWith('90071992547409931', { name: '测试菜单', parentId: null, routePath: '/sample', httpMethod: null, sortNo: 10 })
expect(wrapper.getComponent(AppDialog).props('modelValue')).toBe(true)
expect(codeInput.value).toBe(mode === 'create' ? 'platform:new:list' : 'platform:sample:list')
})
})
+11 -11
View File
@@ -53,7 +53,7 @@ function mountServerView(permissionCodes = ['platform:permission:list', 'platfor
AdminPage: { template: '<main><slot /></main>' },
ListPageLayout: { template: '<section><slot name="toolbar" /><slot name="state" /><slot /></section>' },
FormSection: { template: '<section><slot /></section>' },
AppDrawer: { template: '<section><slot /><slot name="footer" /></section>' },
AppDialog: { template: '<section><slot /><slot name="footer" /></section>' },
LoadErrorState: { props: ['message'], template: '<div role="alert">{{ message }}<slot name="details" /></div>' },
DataTable: {
props: ['data'],
@@ -85,16 +85,16 @@ describe('PermissionsView IAM mode', () => {
expect(permissionsSource).toContain("(row as Permission).status || '—'")
})
it('uses outlined business action buttons while keeping tree view helpers as links', () => {
expect(permissionsSource).toContain('size="small" plain type="primary" @click="openCreate((row as Permission).id)"')
expect(permissionsSource).toContain('size="small" plain type="primary" :disabled="rowActionId')
expect(permissionsSource).toContain('size="small" plain :type="(row as Permission).status')
expect(permissionsSource).toContain('size="small" plain type="danger"')
expect(permissionsSource).toContain('size="small" plain :aria-label="moreActionsLabel(row as Permission)"')
expect(permissionsSource).toContain('<Plus :size="15" />添加下级')
expect(permissionsSource).toContain('<Pencil :size="15" />编辑')
expect(permissionsSource).toContain('<Power :size="15" />')
expect(permissionsSource).toContain('<Trash2 :size="15" />删除')
expect(permissionsSource).toContain('<MoreHorizontal :size="16" />')
expect(permissionsSource).toContain('size="small" data-tone="neutral" @click="openCreate((row as Permission).id)"')
expect(permissionsSource).toContain('size="small" data-tone="neutral" :disabled="rowActionId')
expect(permissionsSource).toContain('size="small" :data-tone="(row as Permission).status')
expect(permissionsSource).toContain('size="small" data-tone="danger"')
expect(permissionsSource).toContain('size="small" :aria-label="moreActionsLabel(row as Permission)"')
expect(permissionsSource).toContain('>添加下级</el-button>')
expect(permissionsSource).toContain('>编辑</el-button>')
expect(permissionsSource).toContain(':data-tone=')
expect(permissionsSource).toContain('>删除</el-button>')
expect(permissionsSource).toContain('<MoreHorizontal aria-hidden="true" :size="16" />')
expect(permissionsSource).toContain('<el-button link type="primary" @click="expandAllRows">展开全部</el-button>')
expect(permissionsSource).not.toContain('canAddChild(row as Permission)" link')
expect(permissionsSource).not.toContain('canEditRow(row as Permission)" link')
+21 -19
View File
@@ -1,12 +1,14 @@
<script setup lang="ts">
import '@/styles/adminDensity.css'
import '../iamDensity.css'
import { computed, nextTick, onMounted, reactive, ref } from 'vue'
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus'
import { Braces, FolderTree, MoreHorizontal, Pencil, Plus, Power, ShieldCheck, Trash2 } from '@lucide/vue'
import { Braces, FolderTree, MoreHorizontal, Plus, ShieldCheck } from '@lucide/vue'
import StatusBadge from '@/shared/components/StatusBadge.vue'
import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue'
import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue'
import AdminPage from '@/shared/components/AdminPage.vue'
import AppDrawer from '@/shared/components/AppDrawer.vue'
import AppDialog from '@/shared/components/AppDialog.vue'
import FormSection from '@/shared/components/FormSection.vue'
import ListPageLayout from '@/shared/components/ListPageLayout.vue'
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
@@ -203,7 +205,7 @@ async function toggleStatus(row: Permission) {
if (row.builtIn) return
const target = row.status === '启用' ? '停用' : '启用'
try {
await ElMessageBox.confirm(`确认${target}权限“${row.name}”吗?`, '权限状态', { confirmButtonText: '确认', cancelButtonText: '取消' })
await ElMessageBox.confirm(`确认${target}权限“${row.name}”吗?`, '权限状态', { customClass: 'admin-density-confirm', confirmButtonText: '确认', cancelButtonText: '取消' })
rowActionId.value = row.id
const updated = await changePlatformPermissionStatus(row.id, target === '启用' ? 'ACTIVE' : 'DISABLED')
Object.assign(row, mapPlatformPermission(updated))
@@ -219,7 +221,7 @@ async function toggleStatus(row: Permission) {
async function removePermission(row: Permission) {
if (row.builtIn) return
try {
await ElMessageBox.confirm(`确认删除权限“${row.name}”吗?删除前需确保没有下级资源且未分配给角色。`, '删除权限', { confirmButtonText: '删除', cancelButtonText: '取消', type: 'warning' })
await ElMessageBox.confirm(`确认删除权限“${row.name}”吗?删除前需确保没有下级资源且未分配给角色。`, '删除权限', { customClass: 'admin-density-confirm', confirmButtonText: '删除', cancelButtonText: '取消', type: 'warning' })
rowActionId.value = row.id
await deletePlatformPermission(row.id)
await loadPermissions()
@@ -268,7 +270,7 @@ onMounted(loadPermissions)
</script>
<template>
<AdminPage as="div">
<AdminPage class="admin-density iam-density" as="div">
<ListPageLayout aria-label="平台权限资源管理">
<template #toolbar><QueryToolbar v-model="filters" class="surface-panel" ariaLabel="权限筛选" :fields="queryFields" preference-key="permissions-query" @search="applyFilters" /></template>
<template v-if="loadError" #state>
@@ -282,14 +284,14 @@ onMounted(loadPermissions)
<template #toolbar-start><div class="permission-tree-tools"><div class="legend-list"><span><i class="is-menu" />目录 / 菜单</span><span><i class="is-action" />操作</span><span><i class="is-api" />接口预留</span><span class="permission-protection-hint">受保护资源不可编辑、停用或删除</span></div><div class="tree-view-actions"><el-button link type="primary" @click="expandAllRows">展开全部</el-button><el-button link @click="collapseActionRows">收起操作权限</el-button></div></div></template>
<template #toolbar-actions><el-button v-if="canCreate" type="primary" @click="openCreate(null)"><Plus :size="16" />新增权限</el-button></template>
<template #actions="{ row }">
<div v-if="permissionActionCount(row as Permission) > 0 || (row as Permission).builtIn" class="table-actions">
<el-button v-if="canAddChild(row as Permission)" size="small" plain type="primary" @click="openCreate((row as Permission).id)"><Plus :size="15" />添加下级</el-button>
<el-button v-if="canEditRow(row as Permission)" size="small" plain type="primary" :disabled="rowActionId === (row as Permission).id" @click="openEdit(row as Permission)"><Pencil :size="15" />编辑</el-button>
<el-button v-if="canChangeStatusRow(row as Permission)" size="small" plain :type="(row as Permission).status === '启用' ? 'danger' : 'success'" :loading="rowActionId === (row as Permission).id" @click="toggleStatus(row as Permission)"><Power :size="15" />{{ (row as Permission).status === '启用' ? '停用' : '启用' }}</el-button>
<el-button v-if="showDeleteDirect(row as Permission)" size="small" plain type="danger" :disabled="rowActionId === (row as Permission).id" @click="removePermission(row as Permission)"><Trash2 :size="15" />删除</el-button>
<div v-if="permissionActionCount(row as Permission) > 0 || (row as Permission).builtIn" class="table-actions density-actions">
<el-button class="density-action" v-if="canAddChild(row as Permission)" size="small" data-tone="neutral" @click="openCreate((row as Permission).id)">添加下级</el-button>
<el-button class="density-action" v-if="canEditRow(row as Permission)" size="small" data-tone="neutral" :disabled="rowActionId === (row as Permission).id" @click="openEdit(row as Permission)">编辑</el-button>
<el-button class="density-action" v-if="canChangeStatusRow(row as Permission)" size="small" :data-tone="(row as Permission).status === '启用' ? 'danger' : 'success'" :loading="rowActionId === (row as Permission).id" @click="toggleStatus(row as Permission)">{{ (row as Permission).status === '启用' ? '停用' : '启用' }}</el-button>
<el-button class="density-action" v-if="showDeleteDirect(row as Permission)" size="small" data-tone="danger" :disabled="rowActionId === (row as Permission).id" @click="removePermission(row as Permission)">删除</el-button>
<el-dropdown v-if="showDeleteMore(row as Permission)" trigger="click" @command="(command: string) => command === 'delete' && removePermission(row as Permission)">
<el-button size="small" plain :aria-label="moreActionsLabel(row as Permission)" :title="moreActionsLabel(row as Permission)"><MoreHorizontal :size="16" /></el-button>
<template #dropdown><el-dropdown-menu><el-dropdown-item command="delete" :disabled="rowActionId === (row as Permission).id" class="danger-menu-item"><Trash2 :size="15" />删除</el-dropdown-item></el-dropdown-menu></template>
<el-button data-tone="neutral" class="density-action" size="small" :aria-label="moreActionsLabel(row as Permission)" :title="moreActionsLabel(row as Permission)"><MoreHorizontal aria-hidden="true" :size="16" /></el-button>
<template #dropdown><el-dropdown-menu><el-dropdown-item command="delete" :disabled="rowActionId === (row as Permission).id" class="danger-menu-item">删除</el-dropdown-item></el-dropdown-menu></template>
</el-dropdown>
</div>
<span v-else class="secondary-text">仅查看</span>
@@ -303,9 +305,9 @@ onMounted(loadPermissions)
</section>
</ListPageLayout>
<AppDrawer v-model="editorOpen" :title="form.id ? '编辑权限资源' : '新增权限资源'" size="600px" class="business-drawer editor-drawer" :modal="false" :confirm-loading="saving" :confirm-text="form.id ? '保存修改' : '保存权限'" :show-confirm="form.id ? canEdit : canCreate" @confirm="savePermission">
<el-form ref="formRef" :model="form" :rules="formRules" label-position="top" class="drawer-form">
<FormSection title="资源定义" description="定义权限名称、编码、类型和层级关系。">
<AppDialog v-model="editorOpen" :title="form.id ? '编辑权限资源' : '新增权限资源'" width="min(720px, calc(100vw - 28px))" class="permission-editor-dialog admin-density-dialog iam-density" :confirm-loading="saving" :confirm-text="form.id ? '保存修改' : '保存权限'" :show-confirm="form.id ? canEdit : canCreate" @confirm="savePermission">
<el-form ref="formRef" :model="form" :rules="formRules" label-position="top" class="density-form">
<FormSection class="density-form-section" title="资源定义" description="定义权限名称、编码、类型和层级关系。">
<div class="form-grid two-cols">
<el-form-item label="权限名称" prop="name" required><el-input v-model="form.name" placeholder="用户可理解的中文名称" maxlength="64" /></el-form-item>
<el-form-item label="权限编码" prop="code" required><el-input v-model="form.code" :disabled="Boolean(form.id)" placeholder="如:platform:customer:list" maxlength="128" /></el-form-item>
@@ -318,17 +320,17 @@ onMounted(loadPermissions)
</FormSection>
<div class="form-callout"><Braces :size="18" /><div><strong>权限编码约定</strong><p>使用稳定的“业务域:资源:动作”编码;资源类型与后端 MENU、PAGE、BUTTON、API 契约一致。</p></div></div>
</el-form>
</AppDrawer>
</AppDialog>
</AdminPage>
</template>
<style scoped>
.permission-tree-tools { display: flex; align-items: center; gap: 16px; }
.permission-tree-tools { display: flex; align-items: center; gap: var(--admin-space-6); }
.permission-protection-hint { color: var(--cc-muted); }
.tree-view-actions { display: flex; align-items: center; gap: 4px; white-space: nowrap; }
.tree-view-actions { display: flex; align-items: center; gap: var(--admin-space-1); white-space: nowrap; }
.tree-view-actions .el-button + .el-button { margin-left: 0; }
.permission-name > div { display: flex; min-width: 0; flex-direction: column; gap: 2px; }
.permission-name small { color: var(--cc-muted); font-size: 11px; line-height: 1.35; }
.permission-name small { color: var(--cc-muted); font-size: var(--admin-font-caption); line-height: 1.35; }
:deep(.danger-menu-item:not(.is-disabled)) { color: var(--el-color-danger); }
@media (max-width: 720px) {
+6 -6
View File
@@ -77,12 +77,12 @@ describe('RolesView', () => {
})
it('uses bordered striped tables and semantic role operation buttons', () => {
expect(rolesSource).toContain('border stripe')
expect(rolesSource).toContain('size="small" plain type="primary"')
expect(rolesSource).toContain('size="small" plain :type=')
expect(rolesSource).toContain('size="small" plain type="danger"')
expect(rolesSource).toContain('<Pencil :size="15" />')
expect(rolesSource).toContain('<Power :size="15" />')
expect(rolesSource).toContain('<Trash2 :size="15" />')
expect(rolesSource).toContain('size="small" data-tone="neutral"')
expect(rolesSource).toContain('size="small" :data-tone=')
expect(rolesSource).toContain('size="small" data-tone="danger"')
expect(rolesSource).toContain('>配置</el-button>')
expect(rolesSource).toContain(':data-tone=')
expect(rolesSource).toContain('>删除</el-button>')
expect(rolesSource).toContain('@media (max-width: 720px)')
})
beforeEach(() => {
+15 -12
View File
@@ -1,7 +1,9 @@
<script setup lang="ts">
import '@/styles/adminDensity.css'
import '../iamDensity.css'
import { computed, onMounted, reactive, ref } from 'vue'
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus'
import { Pencil, Plus, Power, ShieldCheck, Trash2 } from '@lucide/vue'
import { Plus, ShieldCheck } from '@lucide/vue'
import {
changePlatformRoleStatus,
createPlatformRole,
@@ -195,7 +197,7 @@ async function removeRole(row: Role) {
if (row.builtIn) { ElMessage.warning('系统内置角色不能删除'); return }
if (row.userCount > 0) { ElMessage.warning('请先移除该角色下的用户'); return }
try {
await ElMessageBox.confirm(`确认删除角色“${row.name}”吗?`, '删除角色', { confirmButtonText: '删除', cancelButtonText: '取消', type: 'warning' })
await ElMessageBox.confirm(`确认删除角色“${row.name}”吗?`, '删除角色', { customClass: 'admin-density-confirm', confirmButtonText: '删除', cancelButtonText: '取消', type: 'warning' })
rowActionId.value = row.id
await deletePlatformRole(row.id)
serverRoles.value = serverRoles.value.filter((item) => item.id !== row.id)
@@ -212,7 +214,7 @@ async function toggleStatus(row: Role) {
if (row.builtIn) { ElMessage.warning('系统内置角色不能停用'); return }
const target = row.status === '启用' ? '停用' : '启用'
try {
await ElMessageBox.confirm(`确认${target}角色“${row.name}”吗?`, '角色状态', { confirmButtonText: '确认', cancelButtonText: '取消' })
await ElMessageBox.confirm(`确认${target}角色“${row.name}”吗?`, '角色状态', { customClass: 'admin-density-confirm', confirmButtonText: '确认', cancelButtonText: '取消' })
rowActionId.value = row.id
const updated = await changePlatformRoleStatus(row.id, target === '启用' ? 'ACTIVE' : 'DISABLED')
Object.assign(row, mapPlatformRole(updated))
@@ -234,7 +236,7 @@ onMounted(loadRoles)
</script>
<template>
<AdminPage as="div">
<AdminPage class="admin-density iam-density" as="div">
<ListPageLayout aria-label="平台角色管理">
<template #toolbar><QueryToolbar v-model="filters" class="surface-panel" ariaLabel="角色筛选" :fields="queryFields" preference-key="roles-query" @search="applyFilters" /></template>
<template v-if="loadError" #state>
@@ -246,7 +248,7 @@ onMounted(loadRoles)
<section v-if="!loadError" class="surface-panel table-panel">
<DataTable v-model:page="page" v-model:page-size="pageSize" :data="pagedRows" :columns="roleColumns" preference-key="roles-table" :loading="loading" :total="filteredRows.length" row-key="id" border stripe aria-label="平台角色列表" empty-text="没有符合条件的角色">
<template #toolbar-actions><el-button v-if="canCreate" type="primary" @click="openCreate"><Plus :size="16" />新增角色</el-button></template>
<template #actions="{ row }"><div v-if="canEdit || canGrant || access.can('platform:role:status') || access.can('platform:role:delete')" class="table-actions"><el-button v-if="canEdit || canGrant" size="small" plain type="primary" :disabled="rowActionId === (row as Role).id || (row as Role).builtIn" @click="openEdit(row as Role)"><Pencil :size="15" />配置</el-button><el-button v-if="access.can('platform:role:status')" size="small" plain :type="(row as Role).status === '启用' ? 'danger' : 'success'" :loading="rowActionId === (row as Role).id" :disabled="(row as Role).builtIn" @click="toggleStatus(row as Role)"><Power :size="15" />{{ (row as Role).status === '启用' ? '停用' : '启用' }}</el-button><el-button v-if="access.can('platform:role:delete')" size="small" plain type="danger" :disabled="(row as Role).builtIn || (row as Role).userCount > 0 || rowActionId === (row as Role).id" @click="removeRole(row as Role)"><Trash2 :size="15" />删除</el-button></div><span v-else class="secondary-text">仅查看</span></template>
<template #actions="{ row }"><div v-if="canEdit || canGrant || access.can('platform:role:status') || access.can('platform:role:delete')" class="table-actions density-actions"><el-button class="density-action" v-if="canEdit || canGrant" size="small" data-tone="neutral" :disabled="rowActionId === (row as Role).id || (row as Role).builtIn" @click="openEdit(row as Role)">配置</el-button><el-button class="density-action" v-if="access.can('platform:role:status')" size="small" :data-tone="(row as Role).status === '启用' ? 'danger' : 'success'" :loading="rowActionId === (row as Role).id" :disabled="(row as Role).builtIn" @click="toggleStatus(row as Role)">{{ (row as Role).status === '启用' ? '停用' : '启用' }}</el-button><el-button class="density-action" v-if="access.can('platform:role:delete')" size="small" data-tone="danger" :disabled="(row as Role).builtIn || (row as Role).userCount > 0 || rowActionId === (row as Role).id" @click="removeRole(row as Role)">删除</el-button></div><span v-else class="secondary-text">仅查看</span></template>
<template #role="{ row }"><div class="role-cell"><span><ShieldCheck :size="18" /></span><div><strong>{{ (row as Role).name }}</strong></div></div></template>
<template #permissionSummary="{ row }"><div class="primary-cell"><strong>{{ permissionSummary(row as Role) }}</strong><span>平台 IAM 权限</span></div></template>
<template #roleType="{ row }"><el-tag :type="(row as Role).builtIn ? 'warning' : 'info'" effect="plain" size="small">{{ (row as Role).builtIn ? '系统内置' : '自定义' }}</el-tag></template>
@@ -256,15 +258,15 @@ onMounted(loadRoles)
</section>
</ListPageLayout>
<AppDialog v-model="editorOpen" :title="form.id ? '配置角色' : '新增角色'" width="min(860px, calc(100vw - 28px))" class="role-editor-dialog" :confirm-loading="saving" confirm-text="保存角色" :show-confirm="form.id ? canEdit || canGrant : canCreate" destroy-on-close @opened="initializePermissionSelection" @confirm="saveRole">
<el-form ref="formRef" :model="form" :rules="formRules" label-position="top" class="role-editor-form">
<AppDialog v-model="editorOpen" :title="form.id ? '配置角色' : '新增角色'" width="min(860px, calc(100vw - 28px))" class="role-editor-dialog admin-density-dialog iam-density" :confirm-loading="saving" confirm-text="保存角色" :show-confirm="form.id ? canEdit || canGrant : canCreate" destroy-on-close @opened="initializePermissionSelection" @confirm="saveRole">
<el-form ref="formRef" :model="form" :rules="formRules" label-position="top" class="role-editor-form density-form">
<div class="role-editor-layout">
<FormSection title="基本信息" description="维护角色名称、编码和职责说明。" class="role-editor-basic">
<FormSection title="基本信息" description="维护角色名称、编码和职责说明。" class="role-editor-basic density-form-section">
<el-form-item label="角色名称" prop="name" required><el-input v-model="form.name" :disabled="Boolean(form.id) && !canEdit" placeholder="如:订单运营专员" maxlength="64" /></el-form-item>
<el-form-item label="角色编码" prop="code" required><el-input v-model="form.code" :disabled="Boolean(form.id)" placeholder="如:ORDER_OPERATOR" maxlength="64" /></el-form-item>
<el-form-item label="角色说明"><el-input v-model="form.description" :disabled="Boolean(form.id) && !canEdit" type="textarea" :rows="3" placeholder="说明该角色的工作范围" maxlength="200" show-word-limit /></el-form-item>
</FormSection>
<FormSection v-if="canGrant" title="菜单与操作权限" :description="`按当前后台菜单分组;勾选上级可全选下级,部分选中时显示半选,当前 ${form.permissionIds.length} 项授权。`" class="role-editor-permissions"><el-tree ref="treeRef" class="permission-tree" :data="permissionTree" node-key="id" show-checkbox :default-expanded-keys="permissionGroupIds" :props="{ label: 'name', children: 'children' }" @check="syncPermissionIds"><template #default="{ data }"><div class="permission-node" :class="{ 'is-navigation-group': !data.permissionId }"><span>{{ data.name }}</span><code v-if="data.code">{{ data.code }}</code></div></template></el-tree></FormSection>
<FormSection v-if="canGrant" title="菜单与操作权限" :description="`按当前后台菜单分组;勾选上级可全选下级,部分选中时显示半选,当前 ${form.permissionIds.length} 项授权。`" class="role-editor-permissions density-form-section"><el-tree ref="treeRef" class="permission-tree" :data="permissionTree" node-key="id" show-checkbox :default-expanded-keys="permissionGroupIds" :props="{ label: 'name', children: 'children' }" @check="syncPermissionIds"><template #default="{ data }"><div class="permission-node" :class="{ 'is-navigation-group': !data.permissionId }"><span>{{ data.name }}</span><code v-if="data.code">{{ data.code }}</code></div></template></el-tree></FormSection>
</div>
</el-form>
</AppDialog>
@@ -272,8 +274,9 @@ onMounted(loadRoles)
</template>
<style scoped>
.role-editor-layout { display: grid; grid-template-columns: minmax(0, 0.9fr) minmax(0, 1.3fr); gap: 20px; }
.role-editor-permissions { min-width: 0; padding-left: 20px; border-left: 1px solid var(--cc-border); }
.role-editor-layout { display: grid; grid-template-columns: minmax(0, 0.9fr) minmax(0, 1.3fr); gap: var(--admin-space-5); }
.role-editor-layout:not(:has(.role-editor-permissions)) { grid-template-columns: minmax(0, 1fr); }
.role-editor-layout > .role-editor-permissions.density-form-section { min-width: 0; padding: 0 0 0 var(--admin-space-5); border-left: 1px solid var(--cc-border); }
.permission-tree { max-height: 380px; overflow-x: hidden; overflow-y: auto; }
.permission-node span { min-width: 0; }
.permission-node code { max-width: 58%; white-space: normal; text-align: right; }
@@ -283,6 +286,6 @@ onMounted(loadRoles)
:global(.role-editor-dialog .el-dialog__footer) { flex: 0 0 auto; }
@media (max-width: 720px) {
.role-editor-layout { grid-template-columns: minmax(0, 1fr); }
.role-editor-permissions { padding-top: 18px; padding-left: 0; border-top: 1px solid var(--cc-border); border-left: 0; }
.role-editor-layout > .role-editor-permissions.density-form-section { padding-top: 18px; padding-left: 0; border-top: 1px solid var(--cc-border); border-left: 0; }
}
</style>