2026-07-30 17:59:43 +08:00
|
|
|
|
<script setup lang="ts">
|
2026-08-01 12:17:33 +08:00
|
|
|
|
import { computed, nextTick, onMounted, reactive, ref } from 'vue'
|
|
|
|
|
|
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus'
|
|
|
|
|
|
import { Braces, FolderTree, Pencil, Plus, ShieldCheck, Trash2 } from '@lucide/vue'
|
2026-08-03 18:49:18 +08:00
|
|
|
|
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'
|
2026-08-01 12:17:33 +08:00
|
|
|
|
import {
|
|
|
|
|
|
changePlatformPermissionStatus,
|
|
|
|
|
|
createPlatformPermission,
|
|
|
|
|
|
deletePlatformPermission,
|
|
|
|
|
|
listPlatformPermissions,
|
|
|
|
|
|
updatePlatformPermission,
|
2026-08-03 18:49:18 +08:00
|
|
|
|
} from '@/modules/iam/api/platformIam'
|
|
|
|
|
|
import { getApiErrorDetails, getApiErrorMessage, type ApiErrorDetails } from '@/shared/api/http'
|
|
|
|
|
|
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
|
|
|
|
|
|
import { useUnsavedWorkRegistration } from '@/core/unsaved-work/useUnsavedWorkRegistration'
|
|
|
|
|
|
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
|
|
|
|
|
import type { Permission, PermissionType } from '@/modules/iam/types'
|
|
|
|
|
|
import { buildPermissionTree, getDescendantIds, type PermissionTreeNode } from '@/modules/iam/utils/permissionTree'
|
|
|
|
|
|
import { mapPlatformPermission } from '@/modules/iam/utils/platformPermission'
|
2026-07-30 17:59:43 +08:00
|
|
|
|
|
|
|
|
|
|
const access = useAccessPreviewStore()
|
|
|
|
|
|
const editorOpen = ref(false)
|
2026-08-01 12:17:33 +08:00
|
|
|
|
const formRef = ref<FormInstance>()
|
2026-07-30 17:59:43 +08:00
|
|
|
|
const serverPermissions = ref<Permission[]>([])
|
|
|
|
|
|
const loading = ref(false)
|
|
|
|
|
|
const saving = ref(false)
|
2026-08-01 12:17:33 +08:00
|
|
|
|
const rowActionId = ref('')
|
2026-07-30 17:59:43 +08:00
|
|
|
|
const loadError = ref<ApiErrorDetails | null>(null)
|
2026-08-01 12:17:33 +08:00
|
|
|
|
const page = ref(1)
|
|
|
|
|
|
const pageSize = ref(10)
|
|
|
|
|
|
type PermissionFilters = QueryModel & { keyword: string; type: string; status: string }
|
|
|
|
|
|
const filters = ref<PermissionFilters>({ keyword: '', type: '', status: '' })
|
|
|
|
|
|
const appliedFilters = ref<PermissionFilters>({ ...filters.value })
|
|
|
|
|
|
const queryFields: QueryField[] = [
|
|
|
|
|
|
{ key: 'keyword', type: 'input', placeholder: '权限名称、编码或路径' },
|
|
|
|
|
|
{ key: 'type', type: 'select', placeholder: '全部类型', options: [{ label: '菜单', value: '菜单' }, { label: '按钮', value: '按钮' }] },
|
|
|
|
|
|
{ key: 'status', type: 'select', placeholder: '全部状态', options: [{ label: '启用', value: '启用' }, { label: '停用', value: '停用' }] },
|
|
|
|
|
|
]
|
|
|
|
|
|
const expandedRowKeys = ref<string[]>([])
|
|
|
|
|
|
const tableRenderKey = ref(0)
|
2026-07-30 17:59:43 +08:00
|
|
|
|
const emptyForm = (): Permission => ({ id: '', parentId: null, name: '', code: '', type: '菜单', path: '', method: '-', status: '启用', sort: 10 })
|
|
|
|
|
|
const form = reactive(emptyForm())
|
2026-08-01 12:17:33 +08:00
|
|
|
|
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' }] : [],
|
|
|
|
|
|
sort: [{ required: true, type: 'number', message: '请输入显示排序', trigger: 'change' }],
|
|
|
|
|
|
}))
|
2026-07-30 17:59:43 +08:00
|
|
|
|
const editorLabel = computed(() => form.id ? '编辑权限' : '新增权限')
|
2026-08-01 12:17:33 +08:00
|
|
|
|
const requiredPermission = computed(() => form.id ? 'platform:permission:edit' : 'platform:permission:create')
|
2026-07-30 17:59:43 +08:00
|
|
|
|
const { captureBaseline } = useUnsavedWorkRegistration({
|
|
|
|
|
|
key: 'permission-editor',
|
|
|
|
|
|
label: editorLabel,
|
|
|
|
|
|
isOpen: editorOpen,
|
|
|
|
|
|
model: form,
|
|
|
|
|
|
requiredPermission,
|
|
|
|
|
|
})
|
2026-07-31 15:34:35 +08:00
|
|
|
|
const availablePermissionTypes: PermissionType[] = ['菜单', '按钮']
|
|
|
|
|
|
const permissions = computed(() => serverPermissions.value)
|
2026-07-30 17:59:43 +08:00
|
|
|
|
const canCreate = computed(() => access.can('platform:permission:create'))
|
2026-08-01 12:17:33 +08:00
|
|
|
|
const canEdit = computed(() => access.can('platform:permission:edit'))
|
|
|
|
|
|
const canChangeStatus = computed(() => access.can('platform:permission:status'))
|
|
|
|
|
|
const canDelete = computed(() => access.can('platform:permission:delete'))
|
2026-07-30 17:59:43 +08:00
|
|
|
|
|
|
|
|
|
|
function matches(item: Permission) {
|
2026-08-01 12:17:33 +08:00
|
|
|
|
const keyword = appliedFilters.value.keyword.trim().toLowerCase()
|
|
|
|
|
|
return (!keyword || [item.name, item.code, item.path].some((value) => value.toLowerCase().includes(keyword))) && (!appliedFilters.value.type || item.type === appliedFilters.value.type) && (!appliedFilters.value.status || item.status === appliedFilters.value.status)
|
2026-07-30 17:59:43 +08:00
|
|
|
|
}
|
|
|
|
|
|
const treeRows = computed(() => buildPermissionTree(permissions.value, matches))
|
2026-08-01 12:17:33 +08:00
|
|
|
|
const pagedTreeRows = computed(() => treeRows.value.slice((page.value - 1) * pageSize.value, page.value * pageSize.value))
|
|
|
|
|
|
const rootRowKeys = computed(() => treeRows.value.map((item) => item.id))
|
2026-07-30 17:59:43 +08:00
|
|
|
|
const blockedParentIds = computed(() => new Set(
|
|
|
|
|
|
form.id ? [form.id, ...getDescendantIds(permissions.value, form.id)] : [],
|
|
|
|
|
|
))
|
|
|
|
|
|
const parentOptions = computed(() => permissions.value.filter((item) => (
|
2026-07-31 15:34:35 +08:00
|
|
|
|
item.type === '菜单' && !blockedParentIds.value.has(item.id)
|
2026-07-30 17:59:43 +08:00
|
|
|
|
)))
|
2026-08-01 12:17:33 +08:00
|
|
|
|
const permissionColumns: DataTableColumn[] = [
|
|
|
|
|
|
{ key: 'actions', label: '操作', width: 280, fixed: 'left', slotName: 'actions' },
|
|
|
|
|
|
{ key: 'name', label: '权限名称', minWidth: 260, slotName: 'name' },
|
|
|
|
|
|
{ key: 'code', label: '权限编码', minWidth: 220, slotName: 'code' },
|
|
|
|
|
|
{ prop: 'type', label: '类型', width: 80 },
|
|
|
|
|
|
{ key: 'endpoint', label: '路由 / 接口', minWidth: 260, slotName: 'endpoint' },
|
|
|
|
|
|
{ prop: 'sort', label: '排序', width: 72, align: 'right' },
|
|
|
|
|
|
{ key: 'status', label: '状态', width: 88, slotName: 'status' },
|
|
|
|
|
|
]
|
2026-07-30 17:59:43 +08:00
|
|
|
|
|
|
|
|
|
|
async function loadPermissions() {
|
|
|
|
|
|
loading.value = true
|
|
|
|
|
|
loadError.value = null
|
|
|
|
|
|
serverPermissions.value = []
|
|
|
|
|
|
try {
|
|
|
|
|
|
serverPermissions.value = (await listPlatformPermissions()).map(mapPlatformPermission)
|
2026-08-01 12:17:33 +08:00
|
|
|
|
expandedRowKeys.value = rootRowKeys.value
|
|
|
|
|
|
tableRenderKey.value += 1
|
2026-07-30 17:59:43 +08:00
|
|
|
|
} catch (error) {
|
|
|
|
|
|
loadError.value = getApiErrorDetails(error, '权限资源加载失败,请稍后重试')
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
loading.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-01 12:17:33 +08:00
|
|
|
|
function applyFilters() {
|
|
|
|
|
|
appliedFilters.value = { ...filters.value }
|
|
|
|
|
|
page.value = 1
|
|
|
|
|
|
void loadPermissions()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function collectDescendantIds(nodes: PermissionTreeNode[], ids: string[] = []) {
|
|
|
|
|
|
nodes.forEach((node) => {
|
|
|
|
|
|
ids.push(node.id)
|
|
|
|
|
|
if (node.children) collectDescendantIds(node.children, ids)
|
|
|
|
|
|
})
|
|
|
|
|
|
return ids
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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 actionCount = descendants.filter((item) => ['按钮', '接口'].includes(item.type)).length
|
|
|
|
|
|
if (menuCount === 0 && actionCount === 0) return '无下级权限'
|
|
|
|
|
|
return [menuCount ? `${menuCount} 个菜单` : '', actionCount ? `${actionCount} 项操作` : ''].filter(Boolean).join(' · ')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function expandAllRows() {
|
|
|
|
|
|
expandedRowKeys.value = collectDescendantIds(treeRows.value)
|
|
|
|
|
|
tableRenderKey.value += 1
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function collapseActionRows() {
|
|
|
|
|
|
expandedRowKeys.value = rootRowKeys.value
|
|
|
|
|
|
tableRenderKey.value += 1
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function openCreate(parentId: string | null = null) {
|
|
|
|
|
|
Object.assign(form, emptyForm(), { parentId })
|
|
|
|
|
|
captureBaseline()
|
|
|
|
|
|
editorOpen.value = true
|
|
|
|
|
|
nextTick(() => formRef.value?.clearValidate())
|
|
|
|
|
|
}
|
|
|
|
|
|
function openEdit(row: Permission) {
|
|
|
|
|
|
if (!canEdit.value || row.builtIn) return
|
|
|
|
|
|
Object.assign(form, { ...row })
|
|
|
|
|
|
captureBaseline()
|
|
|
|
|
|
editorOpen.value = true
|
|
|
|
|
|
nextTick(() => formRef.value?.clearValidate())
|
|
|
|
|
|
}
|
|
|
|
|
|
function handleTypeChange() {
|
|
|
|
|
|
if (form.type === '按钮') form.path = ''
|
|
|
|
|
|
nextTick(() => formRef.value?.clearValidate(['parentId', 'path']))
|
|
|
|
|
|
}
|
2026-07-30 17:59:43 +08:00
|
|
|
|
async function savePermission() {
|
2026-08-01 12:17:33 +08:00
|
|
|
|
const valid = await formRef.value?.validate().catch(() => false)
|
|
|
|
|
|
if (!valid) return
|
2026-07-30 17:59:43 +08:00
|
|
|
|
if (permissions.value.some((item) => item.code.toLowerCase() === form.code.toLowerCase() && item.id !== form.id)) { ElMessage.error('权限编码已经存在'); return }
|
2026-07-31 15:34:35 +08:00
|
|
|
|
saving.value = true
|
|
|
|
|
|
try {
|
2026-08-01 12:17:33 +08:00
|
|
|
|
if (form.id) {
|
|
|
|
|
|
const updated = await updatePlatformPermission(form.id, {
|
|
|
|
|
|
name: form.name.trim(),
|
|
|
|
|
|
parentId: form.parentId,
|
|
|
|
|
|
routePath: form.type === '菜单' ? form.path.trim() : null,
|
|
|
|
|
|
httpMethod: form.method === '-' ? null : form.method,
|
|
|
|
|
|
sortNo: form.sort,
|
|
|
|
|
|
})
|
|
|
|
|
|
const index = serverPermissions.value.findIndex((item) => item.id === form.id)
|
|
|
|
|
|
if (index >= 0) serverPermissions.value[index] = mapPlatformPermission(updated)
|
|
|
|
|
|
ElMessage.success('权限资源已更新')
|
|
|
|
|
|
} else {
|
2026-07-30 17:59:43 +08:00
|
|
|
|
const created = await createPlatformPermission({
|
|
|
|
|
|
type: form.type === '按钮' ? 'BUTTON' : 'MENU',
|
|
|
|
|
|
code: form.code.trim(),
|
|
|
|
|
|
name: form.name.trim(),
|
2026-07-30 21:23:39 +08:00
|
|
|
|
parentId: form.parentId || '0',
|
2026-07-30 17:59:43 +08:00
|
|
|
|
routePath: form.type === '菜单' ? form.path.trim() : null,
|
|
|
|
|
|
httpMethod: null,
|
|
|
|
|
|
sortNo: form.sort,
|
|
|
|
|
|
})
|
|
|
|
|
|
serverPermissions.value.push(mapPlatformPermission(created))
|
|
|
|
|
|
ElMessage.success('权限资源已创建')
|
2026-08-01 12:17:33 +08:00
|
|
|
|
}
|
|
|
|
|
|
editorOpen.value = false
|
2026-07-31 15:34:35 +08:00
|
|
|
|
} catch (error) {
|
2026-08-01 12:17:33 +08:00
|
|
|
|
ElMessage.error(getApiErrorMessage(error, form.id ? '权限资源更新失败,请稍后重试' : '权限资源创建失败,请稍后重试'))
|
2026-07-31 15:34:35 +08:00
|
|
|
|
} finally {
|
|
|
|
|
|
saving.value = false
|
2026-07-30 17:59:43 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-08-01 12:17:33 +08:00
|
|
|
|
async function toggleStatus(row: Permission) {
|
|
|
|
|
|
if (row.builtIn) return
|
|
|
|
|
|
const target = row.status === '启用' ? '停用' : '启用'
|
|
|
|
|
|
try {
|
|
|
|
|
|
await ElMessageBox.confirm(`确认${target}权限“${row.name}”吗?`, '权限状态', { confirmButtonText: '确认', cancelButtonText: '取消' })
|
|
|
|
|
|
rowActionId.value = row.id
|
|
|
|
|
|
const updated = await changePlatformPermissionStatus(row.id, target === '启用' ? 'ACTIVE' : 'DISABLED')
|
|
|
|
|
|
Object.assign(row, mapPlatformPermission(updated))
|
|
|
|
|
|
ElMessage.success(`权限已${target}`)
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
if (error === 'cancel' || error === 'close') return
|
|
|
|
|
|
ElMessage.error(getApiErrorMessage(error, '权限状态更新失败,请稍后重试'))
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
rowActionId.value = ''
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function removePermission(row: Permission) {
|
|
|
|
|
|
if (row.builtIn) return
|
|
|
|
|
|
try {
|
|
|
|
|
|
await ElMessageBox.confirm(`确认删除权限“${row.name}”吗?删除前需确保没有下级资源且未分配给角色。`, '删除权限', { confirmButtonText: '删除', cancelButtonText: '取消', type: 'warning' })
|
|
|
|
|
|
rowActionId.value = row.id
|
|
|
|
|
|
await deletePlatformPermission(row.id)
|
|
|
|
|
|
await loadPermissions()
|
|
|
|
|
|
ElMessage.success('权限资源已删除')
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
if (error === 'cancel' || error === 'close') return
|
|
|
|
|
|
ElMessage.error(getApiErrorMessage(error, '权限资源删除失败,请稍后重试'))
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
rowActionId.value = ''
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-07-30 17:59:43 +08:00
|
|
|
|
function typeClass(type: PermissionType) { return { '目录': 'directory', '菜单': 'menu', '按钮': 'action', '接口': 'api' }[type] }
|
|
|
|
|
|
|
|
|
|
|
|
onMounted(loadPermissions)
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<template>
|
|
|
|
|
|
<div class="business-page">
|
|
|
|
|
|
<el-alert v-if="loadError" type="error" :closable="false" show-icon role="alert"><template #title><div class="api-error-alert-title"><span>{{ loadError.message }}</span><ApiErrorMeta :details="loadError" /></div><el-button link type="danger" @click="loadPermissions">重新加载</el-button></template></el-alert>
|
2026-08-03 11:45:45 +08:00
|
|
|
|
<QueryToolbar v-model="filters" class="surface-panel" ariaLabel="权限筛选" :fields="queryFields" @search="applyFilters" />
|
2026-07-30 17:59:43 +08:00
|
|
|
|
|
|
|
|
|
|
<section class="surface-panel table-panel">
|
2026-08-03 11:45:45 +08:00
|
|
|
|
<header class="panel-heading"><div><h2>权限资源树 <span class="secondary-text">共 {{ permissions.length }} 项</span></h2><p>默认展示目录与菜单,展开菜单后查看按钮权限;接口权限将在服务端契约确定后补充。</p></div><div class="permission-tree-heading-actions"><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></div><div class="tree-view-actions"><el-button link type="primary" @click="expandAllRows">展开全部</el-button><el-button link @click="collapseActionRows">收起操作权限</el-button></div></div><el-button v-if="canCreate" type="primary" @click="openCreate(null)"><Plus :size="16" />新增权限</el-button></div></header>
|
2026-08-01 12:17:33 +08:00
|
|
|
|
<DataTable :key="tableRenderKey" v-model:page="page" v-model:page-size="pageSize" :data="pagedTreeRows" :columns="permissionColumns" :loading="loading" :total="treeRows.length" row-key="id" :expand-row-keys="expandedRowKeys" :tree-props="{ children: 'children' }" empty-text="没有符合条件的权限资源">
|
|
|
|
|
|
<template #actions="{ row }"><div v-if="canCreate || canEdit || canChangeStatus || canDelete" class="table-actions"><el-button v-if="canCreate && (row as Permission).type === '菜单'" link type="primary" @click="openCreate((row as Permission).id)"><Plus :size="15" />添加下级</el-button><el-button v-if="canEdit" link type="primary" :disabled="(row as Permission).builtIn || rowActionId === (row as Permission).id" @click="openEdit(row as Permission)"><Pencil :size="15" />编辑</el-button><el-button v-if="canChangeStatus" link :type="(row as Permission).status === '启用' ? 'danger' : 'success'" :loading="rowActionId === (row as Permission).id" :disabled="(row as Permission).builtIn" @click="toggleStatus(row as Permission)">{{ (row as Permission).status === '启用' ? '停用' : '启用' }}</el-button><el-button v-if="canDelete" link type="danger" :disabled="(row as Permission).builtIn || rowActionId === (row as Permission).id" @click="removePermission(row as Permission)"><Trash2 :size="15" />删除</el-button><span v-if="(row as Permission).builtIn" class="secondary-text">系统内置</span></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 #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>
|
|
|
|
|
|
</DataTable>
|
2026-07-30 17:59:43 +08:00
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
|
|
<el-drawer v-model="editorOpen" :title="form.id ? '编辑权限资源' : '新增权限资源'" size="600px" class="business-drawer editor-drawer" :modal="false" :close-on-click-modal="false">
|
2026-08-01 12:17:33 +08:00
|
|
|
|
<el-form ref="formRef" :model="form" :rules="formRules" label-position="top" class="drawer-form">
|
2026-07-30 17:59:43 +08:00
|
|
|
|
<div class="form-section">
|
|
|
|
|
|
<h3>资源定义</h3>
|
|
|
|
|
|
<div class="form-grid two-cols">
|
2026-08-01 12:17:33 +08:00
|
|
|
|
<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="sort" required><el-input-number v-model="form.sort" :min="0" :max="999" controls-position="right" /></el-form-item>
|
2026-07-30 17:59:43 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="form-callout"><Braces :size="18" /><div><strong>权限编码约定</strong><p>使用稳定的“业务域:资源:动作”编码;一期真实资源只创建菜单和按钮,接口鉴权由服务端实现。</p></div></div>
|
|
|
|
|
|
</el-form>
|
2026-08-01 12:17:33 +08:00
|
|
|
|
<template #footer><div class="drawer-footer"><el-button :disabled="saving" @click="editorOpen = false">取消</el-button><el-button v-if="form.id ? canEdit : canCreate" type="primary" :loading="saving" @click="savePermission">{{ form.id ? '保存修改' : '保存权限' }}</el-button></div></template>
|
2026-07-30 17:59:43 +08:00
|
|
|
|
</el-drawer>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
2026-08-01 12:17:33 +08:00
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
|
.permission-tree-tools { display: flex; align-items: center; gap: 16px; }
|
2026-08-03 11:45:45 +08:00
|
|
|
|
.permission-tree-heading-actions { display: flex; align-items: center; gap: 12px; }
|
2026-08-01 12:17:33 +08:00
|
|
|
|
.tree-view-actions { display: flex; align-items: center; gap: 4px; 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; }
|
|
|
|
|
|
|
|
|
|
|
|
@media (max-width: 720px) {
|
2026-08-03 11:45:45 +08:00
|
|
|
|
.permission-tree-heading-actions { width: 100%; align-items: flex-start; flex-direction: column; gap: 8px; }
|
2026-08-01 12:17:33 +08:00
|
|
|
|
.permission-tree-tools { width: 100%; align-items: flex-start; flex-direction: column; gap: 8px; }
|
2026-08-03 11:45:45 +08:00
|
|
|
|
.permission-tree-heading-actions > .el-button { min-height: 40px; }
|
2026-08-01 12:17:33 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@media (max-width: 420px) {
|
|
|
|
|
|
.tree-view-actions { width: 100%; justify-content: flex-start; }
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|