优化
This commit is contained in:
@@ -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')
|
||||
})
|
||||
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
@@ -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')
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user