This commit is contained in:
2026-08-13 11:57:44 +08:00
parent 7beca3f985
commit 835ac0ad12
87 changed files with 3721 additions and 295 deletions
+2 -2
View File
@@ -186,12 +186,12 @@ export interface UpdatePlatformRoleRequest {
}
export interface CreatePlatformPermissionRequest {
type: 'MENU' | 'BUTTON'
type: 'MENU' | 'PAGE' | 'BUTTON' | 'API'
code: string
name: string
parentId: number | string
routePath: string | null
httpMethod: null
httpMethod: string | null
sortNo: number
}
+21 -18
View File
@@ -40,7 +40,7 @@ const filters = ref<PermissionFilters>({ keyword: '', type: '', status: '' })
const appliedFilters = ref<PermissionFilters>({ ...filters.value })
const queryFields: QueryField[] = [
{ key: 'keyword', type: 'input', label: '权限关键词', placeholder: '权限名称、编码或路径' },
{ key: 'type', type: 'select', label: '类型', placeholder: '全部类型', options: [{ label: '菜单', value: '菜单' }, { label: '按钮', value: '按钮' }] },
{ key: 'type', type: 'select', label: '类型', placeholder: '全部类型', options: [{ label: '菜单', value: '菜单' }, { label: '页面', value: '页面' }, { label: '按钮', value: '按钮' }, { label: '接口', value: '接口' }] },
{ key: 'status', type: 'select', label: '状态', placeholder: '全部状态', options: [{ label: '启用', value: '启用' }, { label: '停用', value: '停用' }] },
]
const expandedRowKeys = ref<string[]>([])
@@ -51,8 +51,9 @@ const formRules = computed<FormRules>(() => ({
name: [{ required: true, whitespace: true, message: '请输入权限名称', trigger: 'blur' }],
code: [{ required: true, whitespace: true, message: '请输入权限编码', trigger: 'blur' }],
type: [{ required: true, message: '请选择资源类型', trigger: 'change' }],
parentId: form.type === '按钮' ? [{ required: true, message: '请选择上级菜单', trigger: 'change' }] : [],
path: form.type === '菜单' ? [{ required: true, whitespace: true, message: '请输入菜单路由', trigger: 'blur' }] : [],
parentId: ['按钮', '页面', '接口'].includes(form.type) ? [{ required: true, message: '请选择上级菜单', trigger: 'change' }] : [],
path: ['菜单', '页面'].includes(form.type) ? [{ required: true, whitespace: true, message: '请输入菜单路由', trigger: 'blur' }] : [],
method: form.type === '接口' ? [{ required: true, message: '请选择 HTTP 方法', trigger: 'change' }] : [],
sort: [{ required: true, type: 'number', message: '请输入显示排序', trigger: 'change' }],
}))
const editorLabel = computed(() => form.id ? '编辑权限' : '新增权限')
@@ -64,7 +65,7 @@ const { captureBaseline } = useUnsavedWorkRegistration({
model: form,
requiredPermission,
})
const availablePermissionTypes: PermissionType[] = ['菜单', '按钮']
const availablePermissionTypes: PermissionType[] = ['菜单', '页面', '按钮', '接口']
const permissions = computed(() => serverPermissions.value)
const canCreate = computed(() => access.can('platform:permission:create'))
const canEdit = computed(() => access.can('platform:permission:edit'))
@@ -82,7 +83,7 @@ const blockedParentIds = computed(() => new Set(
form.id ? [form.id, ...getDescendantIds(permissions.value, form.id)] : [],
))
const parentOptions = computed(() => permissions.value.filter((item) => (
item.type === '菜单' && !blockedParentIds.value.has(item.id)
['目录', '菜单', '页面'].includes(item.type) && !blockedParentIds.value.has(item.id)
)))
const permissionColumns: DataTableColumn[] = [
{ key: 'actions', label: '操作', width: 280, fixed: 'left', slotName: 'actions' },
@@ -127,7 +128,7 @@ function permissionSummary(row: PermissionTreeNode) {
const descendants = collectDescendantIds(row.children ?? [])
.map((id) => permissions.value.find((item) => item.id === id))
.filter((item): item is Permission => Boolean(item))
const menuCount = descendants.filter((item) => item.type === '菜单').length
const menuCount = descendants.filter((item) => ['菜单', '页面'].includes(item.type)).length
const actionCount = descendants.filter((item) => ['按钮', '接口'].includes(item.type)).length
if (menuCount === 0 && actionCount === 0) return '无下级权限'
return [menuCount ? `${menuCount} 个菜单` : '', actionCount ? `${actionCount} 项操作` : ''].filter(Boolean).join(' · ')
@@ -157,8 +158,9 @@ function openEdit(row: Permission) {
nextTick(() => formRef.value?.clearValidate())
}
function handleTypeChange() {
if (form.type === '按钮') form.path = ''
nextTick(() => formRef.value?.clearValidate(['parentId', 'path']))
if (!['菜单', '页面'].includes(form.type)) form.path = ''
form.method = form.type === '接口' ? (form.method === '-' ? 'GET' : form.method) : '-'
nextTick(() => formRef.value?.clearValidate(['parentId', 'path', 'method']))
}
async function savePermission() {
const valid = await formRef.value?.validate().catch(() => false)
@@ -170,7 +172,7 @@ async function savePermission() {
const updated = await updatePlatformPermission(form.id, {
name: form.name.trim(),
parentId: form.parentId,
routePath: form.type === '菜单' ? form.path.trim() : null,
routePath: ['菜单', '页面'].includes(form.type) ? form.path.trim() : null,
httpMethod: form.method === '-' ? null : form.method,
sortNo: form.sort,
})
@@ -179,12 +181,12 @@ async function savePermission() {
ElMessage.success('权限资源已更新')
} else {
const created = await createPlatformPermission({
type: form.type === '按钮' ? 'BUTTON' : 'MENU',
type: ({ 目录: 'MENU', 菜单: 'MENU', 页面: 'PAGE', 按钮: 'BUTTON', 接口: 'API' } as const)[form.type],
code: form.code.trim(),
name: form.name.trim(),
parentId: form.parentId || '0',
routePath: form.type === '菜单' ? form.path.trim() : null,
httpMethod: null,
routePath: ['菜单', '页面'].includes(form.type) ? form.path.trim() : null,
httpMethod: form.type === '接口' ? form.method : null,
sortNo: form.sort,
})
serverPermissions.value.push(mapPlatformPermission(created))
@@ -229,10 +231,10 @@ async function removePermission(row: Permission) {
rowActionId.value = ''
}
}
function typeClass(type: PermissionType) { return { '目录': 'directory', '菜单': 'menu', '按钮': 'action', '接口': 'api' }[type] }
function typeClass(type: PermissionType) { return { '目录': 'directory', '菜单': 'menu', '页面': 'menu', '按钮': 'action', '接口': 'api' }[type] }
function canAddChild(row: Permission) {
return canCreate.value && row.type === '菜单'
return canCreate.value && ['菜单', '页面'].includes(row.type)
}
function canEditRow(row: Permission) {
return canEdit.value && !row.builtIn
@@ -292,7 +294,7 @@ onMounted(loadPermissions)
</div>
<span v-else class="secondary-text">仅查看</span>
</template>
<template #name="{ row }"><div class="permission-name"><span :class="`is-${typeClass((row as Permission).type)}`"><FolderTree v-if="(row as Permission).type === '目录' || (row as Permission).type === '菜单'" :size="16" /><ShieldCheck v-else-if="(row as Permission).type === '按钮'" :size="16" /><Braces v-else :size="16" /></span><div><strong>{{ (row as Permission).name }}</strong><small v-if="(row as PermissionTreeNode).children?.length">{{ permissionSummary(row as PermissionTreeNode) }}</small></div></div></template>
<template #name="{ row }"><div class="permission-name"><span :class="`is-${typeClass((row as Permission).type)}`"><FolderTree v-if="['目录', '菜单', '页面'].includes((row as Permission).type)" :size="16" /><ShieldCheck v-else-if="(row as Permission).type === '按钮'" :size="16" /><Braces v-else :size="16" /></span><div><strong>{{ (row as Permission).name }}</strong><small v-if="(row as PermissionTreeNode).children?.length">{{ permissionSummary(row as PermissionTreeNode) }}</small></div></div></template>
<template #code="{ row }"><code class="code-value">{{ (row as Permission).code }}</code></template>
<template #endpoint="{ row }"><div class="endpoint-cell"><span v-if="(row as Permission).method !== '-'">{{ (row as Permission).method }}</span><code>{{ (row as Permission).path || '-' }}</code></div></template>
<template #status="{ row }"><StatusBadge :status="(row as Permission).status" /></template>
@@ -308,12 +310,13 @@ onMounted(loadPermissions)
<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>
<el-form-item label="资源类型" prop="type" required><el-select v-model="form.type" :disabled="Boolean(form.id)" @change="handleTypeChange"><el-option v-for="item in availablePermissionTypes" :key="item" :label="item" :value="item" /></el-select></el-form-item>
<el-form-item label="上级资源" prop="parentId" :required="form.type === '按钮'"><el-select v-model="form.parentId" clearable placeholder="作为一级资源"><el-option v-for="item in parentOptions" :key="item.id" :label="`${item.name} (${item.code})`" :value="item.id" /></el-select></el-form-item>
<el-form-item v-if="form.type === '菜单'" label="路由路径" prop="path" required><el-input v-model="form.path" placeholder="如:/customers" /></el-form-item>
<el-form-item label="上级资源" prop="parentId" :required="['按钮', '页面', '接口'].includes(form.type)"><el-select v-model="form.parentId" clearable placeholder="作为一级资源"><el-option v-for="item in parentOptions" :key="item.id" :label="`${item.name} (${item.code})`" :value="item.id" /></el-select></el-form-item>
<el-form-item v-if="['菜单', '页面'].includes(form.type)" label="路由路径" prop="path" required><el-input v-model="form.path" placeholder="如:/customers" /></el-form-item>
<el-form-item v-if="form.type === '接口'" label="HTTP 方法" prop="method" required><el-select v-model="form.method"><el-option v-for="method in ['GET', 'POST', 'PUT', 'PATCH', 'DELETE']" :key="method" :label="method" :value="method" /></el-select></el-form-item>
<el-form-item label="显示排序" prop="sort" required><el-input-number v-model="form.sort" :min="0" :max="999" controls-position="right" /></el-form-item>
</div>
</FormSection>
<div class="form-callout"><Braces :size="18" /><div><strong>权限编码约定</strong><p>使用稳定的“业务域:资源:动作”编码;一期真实资源只创建菜单和按钮,接口鉴权由服务端实现。</p></div></div>
<div class="form-callout"><Braces :size="18" /><div><strong>权限编码约定</strong><p>使用稳定的“业务域:资源:动作”编码;资源类型与后端 MENU、PAGE、BUTTON、API 契约一致。</p></div></div>
</el-form>
</AppDrawer>
</AdminPage>
+1 -1
View File
@@ -1,6 +1,6 @@
export type RecordStatus = '正常' | '停用'
export type RoleStatus = '启用' | '停用'
export type PermissionType = '目录' | '菜单' | '按钮' | '接口'
export type PermissionType = '目录' | '菜单' | '页面' | '按钮' | '接口'
export type DataScope = '全部数据' | '本部门数据' | '本人数据' | '自定义'
export interface InternalUser {
+1 -1
View File
@@ -69,7 +69,7 @@ export function summarizePermissionSelection(permissions: Permission[], selected
const selectedPermissions = permissions.filter((permission) => selected.has(permission.id))
return {
menuCount: selectedPermissions.filter((permission) => permission.type === '菜单').length,
menuCount: selectedPermissions.filter((permission) => ['菜单', '页面'].includes(permission.type)).length,
actionCount: selectedPermissions.filter((permission) => ['按钮', '接口'].includes(permission.type)).length,
}
}
@@ -33,4 +33,18 @@ describe('mapPlatformPermission', () => {
status: 'ACTIVE',
})).toMatchObject({ id: '2082813911786024962', parentId: '2082813911786024961', type: '按钮' })
})
it.each([
['MENU', '菜单'],
['PAGE', '页面'],
['BUTTON', '按钮'],
['API', '接口'],
] as const)('maps backend permission type %s without collapsing it', (permissionType, type) => {
expect(mapPlatformPermission({
id: '1', tenantId: '0', appCode: 'PLATFORM', permissionType, parentId: '0',
permissionCode: `platform:test:${permissionType.toLowerCase()}`, permissionName: type,
routePath: null, httpMethod: permissionType === 'API' ? 'GET' : null,
sortNo: 10, status: 'ACTIVE',
})).toMatchObject({ type })
})
})
+1 -1
View File
@@ -8,7 +8,7 @@ export function mapPlatformPermission(item: PlatformPermissionResource): Permiss
parentId: parentId === '0' ? null : parentId,
name: item.permissionName,
code: item.permissionCode,
type: item.permissionType === 'BUTTON' ? '按钮' : '菜单',
type: ({ MENU: '菜单', PAGE: '页面', BUTTON: '按钮', API: '接口' } as const)[item.permissionType],
path: item.routePath ?? '',
method: item.httpMethod ?? '-',
status: item.status === 'ACTIVE' ? '启用' : '停用',