Files
admin-web/src/views/PermissionsView.vue
T
2026-07-31 15:34:35 +08:00

142 lines
10 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { computed, onMounted, reactive, ref } from 'vue'
import { ElMessage } from 'element-plus'
import { Braces, FolderTree, Plus, Search, ShieldCheck } from '@lucide/vue'
import MetricStrip from '@/components/MetricStrip.vue'
import StatusBadge from '@/components/StatusBadge.vue'
import { createPlatformPermission, listPlatformPermissions } from '@/api/platformIam'
import { getApiErrorDetails, getApiErrorMessage, type ApiErrorDetails } from '@/api/http'
import ApiErrorMeta from '@/components/ApiErrorMeta.vue'
import { useUnsavedWorkRegistration } from '@/composables/useUnsavedWorkRegistration'
import { useAccessPreviewStore } from '@/stores/accessPreview'
import type { Permission, PermissionType } from '@/types/admin'
import { buildPermissionTree, getDescendantIds } from '@/utils/permissionTree'
import { mapPlatformPermission } from '@/utils/platformPermission'
const access = useAccessPreviewStore()
const editorOpen = ref(false)
const serverPermissions = ref<Permission[]>([])
const loading = ref(false)
const saving = ref(false)
const loadError = ref<ApiErrorDetails | null>(null)
const filters = reactive({ keyword: '', type: '', status: '' })
const emptyForm = (): Permission => ({ id: '', parentId: null, name: '', code: '', type: '菜单', path: '', method: '-', status: '启用', sort: 10 })
const form = reactive(emptyForm())
const editorLabel = computed(() => form.id ? '编辑权限' : '新增权限')
const requiredPermission = computed(() => 'platform:permission:create')
const { captureBaseline } = useUnsavedWorkRegistration({
key: 'permission-editor',
label: editorLabel,
isOpen: editorOpen,
model: form,
requiredPermission,
})
const availablePermissionTypes: PermissionType[] = ['菜单', '按钮']
const permissions = computed(() => serverPermissions.value)
const canCreate = computed(() => access.can('platform:permission:create'))
function matches(item: Permission) {
const keyword = filters.keyword.trim().toLowerCase()
return (!keyword || [item.name, item.code, item.path].some((value) => value.toLowerCase().includes(keyword))) && (!filters.type || item.type === filters.type) && (!filters.status || item.status === filters.status)
}
const treeRows = computed(() => buildPermissionTree(permissions.value, matches))
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)
)))
const metrics = computed(() => [
{ label: '权限资源', value: permissions.value.length, note: 'IAM 返回的资源' },
{ label: '导航资源', value: permissions.value.filter((item) => ['目录','菜单'].includes(item.type)).length, note: '目录与页面入口' },
{ label: '操作权限', value: permissions.value.filter((item) => item.type === '按钮').length, note: '控制页面操作' },
{ label: '停用资源', value: permissions.value.filter((item) => item.status === '停用').length, note: '当前不可分配' },
])
async function loadPermissions() {
loading.value = true
loadError.value = null
serverPermissions.value = []
try {
serverPermissions.value = (await listPlatformPermissions()).map(mapPlatformPermission)
} catch (error) {
loadError.value = getApiErrorDetails(error, '权限资源加载失败,请稍后重试')
} finally {
loading.value = false
}
}
function openCreate(parentId: string | null = null) { Object.assign(form, emptyForm(), { parentId }); captureBaseline(); editorOpen.value = true }
async function savePermission() {
if (!form.name.trim() || !form.code.trim()) { ElMessage.warning('请填写权限名称和权限编码'); return }
if (permissions.value.some((item) => item.code.toLowerCase() === form.code.toLowerCase() && item.id !== form.id)) { ElMessage.error('权限编码已经存在'); return }
if (form.type === '菜单' && !form.path.trim()) { ElMessage.warning('请填写菜单路由'); return }
if (form.type === '按钮' && !form.parentId) { ElMessage.warning('按钮权限必须选择上级菜单'); return }
saving.value = true
try {
const created = await createPlatformPermission({
type: form.type === '按钮' ? 'BUTTON' : 'MENU',
code: form.code.trim(),
name: form.name.trim(),
parentId: form.parentId || '0',
routePath: form.type === '菜单' ? form.path.trim() : null,
httpMethod: null,
sortNo: form.sort,
})
serverPermissions.value.push(mapPlatformPermission(created))
editorOpen.value = false
ElMessage.success('权限资源已创建')
} catch (error) {
ElMessage.error(getApiErrorMessage(error, '权限资源创建失败,请稍后重试'))
} finally {
saving.value = false
}
}
function typeClass(type: PermissionType) { return { '目录': 'directory', '菜单': 'menu', '按钮': 'action', '接口': 'api' }[type] }
onMounted(loadPermissions)
</script>
<template>
<div class="business-page">
<MetricStrip :items="metrics" />
<el-alert type="warning" :closable="false" show-icon title="权限资源列表和新增已接入 IAM;一期真实资源仅使用菜单和按钮类型,编辑与启停接口尚未提供。" />
<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>
<section class="surface-panel filter-panel" aria-label="权限筛选">
<div class="filter-fields"><el-input v-model="filters.keyword" clearable placeholder="权限名称、编码或路径" aria-label="权限关键词"><template #prefix><Search :size="16" /></template></el-input><el-select v-model="filters.type" clearable placeholder="全部类型" aria-label="权限类型"><el-option v-for="item in availablePermissionTypes" :key="item" :label="item" :value="item" /></el-select><el-select v-model="filters.status" clearable placeholder="全部状态" aria-label="权限状态"><el-option label="启用" value="启用" /><el-option label="停用" value="停用" /></el-select></div>
<div class="filter-actions"><el-button v-if="canCreate" type="primary" @click="openCreate(null)"><Plus :size="16" />新增权限</el-button></div>
</section>
<section class="surface-panel table-panel">
<header class="panel-heading"><div><h2>权限资源树</h2><p>目录组织菜单,按钮控制页面操作;接口权限将在服务端契约确定后补充。</p></div><div class="legend-list"><span><i class="is-menu" />目录 / 菜单</span><span><i class="is-action" />操作</span><span><i class="is-api" />接口预留</span></div></header>
<el-table v-loading="loading" :data="treeRows" row-key="id" default-expand-all :tree-props="{ children: 'children' }" empty-text="没有符合条件的权限资源">
<el-table-column label="操作" width="235" fixed="left"><template #default="{ row }"><div v-if="canCreate" class="table-actions"><el-button v-if="row.type === '菜单'" link type="primary" @click="openCreate(row.id)"><Plus :size="15" />添加下级</el-button><span class="secondary-text">编辑接口待补齐</span></div><span v-else class="secondary-text">仅查看</span></template></el-table-column>
<el-table-column label="权限名称" min-width="260"><template #default="{ row }"><div class="permission-name"><span :class="`is-${typeClass(row.type)}`"><FolderTree v-if="row.type === '目录' || row.type === '菜单'" :size="16" /><ShieldCheck v-else-if="row.type === '按钮'" :size="16" /><Braces v-else :size="16" /></span><strong>{{ row.name }}</strong></div></template></el-table-column>
<el-table-column label="权限编码" min-width="220"><template #default="{ row }"><code class="code-value">{{ row.code }}</code></template></el-table-column>
<el-table-column prop="type" label="类型" width="80" />
<el-table-column label="路由 / 接口" min-width="260"><template #default="{ row }"><div class="endpoint-cell"><span v-if="row.method !== '-'">{{ row.method }}</span><code>{{ row.path || '-' }}</code></div></template></el-table-column>
<el-table-column prop="sort" label="排序" width="72" align="right" />
<el-table-column label="状态" width="88"><template #default="{ row }"><StatusBadge :status="row.status" /></template></el-table-column>
</el-table>
</section>
<el-drawer v-model="editorOpen" :title="form.id ? '编辑权限资源' : '新增权限资源'" size="600px" class="business-drawer editor-drawer" :modal="false" :close-on-click-modal="false">
<el-form label-position="top" class="drawer-form">
<div class="form-section">
<h3>资源定义</h3>
<div class="form-grid two-cols">
<el-form-item label="权限名称" required><el-input v-model="form.name" placeholder="用户可理解的中文名称" maxlength="64" /></el-form-item>
<el-form-item label="权限编码" required><el-input v-model="form.code" placeholder="如 platform:customer:list" maxlength="128" /></el-form-item>
<el-form-item label="资源类型"><el-select v-model="form.type"><el-option v-for="item in availablePermissionTypes" :key="item" :label="item" :value="item" /></el-select></el-form-item>
<el-form-item label="上级资源"><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="路由路径"><el-input v-model="form.path" placeholder="例如 /customers" /></el-form-item>
<el-form-item label="显示排序"><el-input-number v-model="form.sort" :min="0" :max="999" controls-position="right" /></el-form-item>
</div>
</div>
<div class="form-callout"><Braces :size="18" /><div><strong>权限编码约定</strong><p>使用稳定的“业务域:资源:动作”编码;一期真实资源只创建菜单和按钮,接口鉴权由服务端实现。</p></div></div>
</el-form>
<template #footer><div class="drawer-footer"><el-button @click="editorOpen = false">取消</el-button><el-button v-if="canCreate" type="primary" :loading="saving" @click="savePermission">保存权限</el-button></div></template>
</el-drawer>
</div>
</template>