142 lines
10 KiB
Vue
142 lines
10 KiB
Vue
<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>
|