Files
workspace/code/fms/demo.html
T
2026-09-08 22:39:22 +08:00

130 lines
25 KiB
HTML

<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>FMS 权限中心 · 功能实例授权</title>
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = { theme: { extend: { fontFamily: { sans: ['Inter', 'Segoe UI', 'Microsoft YaHei', 'sans-serif'] }, colors: { ink: '#17212b', tealx: '#087f78', line: '#dfe7e9' } } } };
</script>
<style>
[v-cloak] { display: none }
::-webkit-scrollbar { width: 8px; height: 8px }
::-webkit-scrollbar-thumb { background: #c7d2d5; border-radius: 8px }
.mono { font-family: Consolas, Menlo, "Courier New", monospace }
</style>
</head>
<body class="bg-[#f4f7f7] font-sans text-[13px] text-slate-700 antialiased">
<div id="app" v-cloak>
<header class="sticky top-0 z-30 border-b border-line bg-white/95 backdrop-blur">
<div class="mx-auto flex h-14 max-w-[1500px] items-center gap-4 px-5">
<div class="flex items-center gap-3">
<span class="grid h-8 w-8 place-items-center rounded-lg bg-tealx text-sm font-bold text-white">F</span>
<div class="leading-tight"><div class="text-sm font-semibold text-ink">FMS 权限中心</div><div class="text-[11px] text-slate-400">功能实例授权 · 一客户一库</div></div>
</div>
<nav class="ml-5 flex rounded-lg bg-slate-100 p-1">
<button v-for="tab in tabs" :key="tab.key" @click="state.tab = tab.key" class="rounded-md px-4 py-1.5 text-[12px] transition" :class="state.tab === tab.key ? 'bg-white font-medium text-tealx shadow-sm' : 'text-slate-500 hover:text-slate-700'">{{ tab.name }}</button>
</nav>
<div class="ml-auto flex items-center gap-3 border-l border-line pl-4"><div class="text-right leading-tight"><div class="text-[12px] font-medium text-slate-700">系统管理员</div><div class="text-[11px] text-slate-400">DEMO-FWD</div></div><span class="grid h-8 w-8 place-items-center rounded-full bg-teal-50 text-[11px] font-bold text-tealx">IT</span></div>
</div>
</header>
<main v-if="state.tab === 'grant'" class="mx-auto max-w-[1500px] space-y-4 px-5 py-5">
<section class="flex flex-wrap items-center gap-3 rounded-xl border border-line bg-white px-4 py-3">
<div class="mr-2 text-[12px] font-medium text-slate-500">授权对象</div>
<button v-for="user in users" :key="user.id" @click="state.userId = user.id" class="flex items-center gap-2 rounded-lg border px-2.5 py-1.5 transition" :class="state.userId === user.id ? 'border-teal-300 bg-teal-50 text-tealx' : 'border-line bg-white text-slate-600 hover:bg-slate-50'">
<span class="grid h-6 w-6 place-items-center rounded-full text-[11px] font-bold" :class="state.userId === user.id ? 'bg-tealx text-white' : 'bg-slate-100 text-slate-500'">{{ user.name.slice(0, 1) }}</span><span>{{ user.name }}</span><span class="text-[11px] text-slate-400">{{ user.title }}</span><span v-if="grantCount(user.id)" class="rounded bg-amber-50 px-1 text-[10px] text-amber-700">{{ grantCount(user.id) }}</span>
</button>
<div class="ml-auto text-[11px] text-slate-400">未授权 = 拒绝 · 功能权限独立生效</div>
</section>
<div class="grid gap-4 xl:grid-cols-[270px_minmax(0,1fr)_330px]">
<aside class="rounded-xl border border-line bg-white">
<div class="border-b border-line px-4 py-3"><div class="font-semibold text-ink">菜单</div><div class="mt-0.5 text-[11px] text-slate-400">按客户实际导航选择功能</div></div>
<div class="border-b border-line p-3"><input v-model="state.search" class="h-8 w-full rounded-md border border-line px-2.5 text-[12px] outline-none focus:border-tealx" placeholder="搜索菜单或功能"></div>
<div class="max-h-[620px] overflow-auto p-2">
<div v-for="group in menuGroups" :key="group.code" class="mb-3">
<div class="px-3 pb-1.5 pt-1 text-[11px] font-semibold uppercase tracking-wide text-slate-400">{{ group.name }}</div>
<template v-for="code in group.items" :key="code"><button v-if="menuMatches(code)" @click="state.featureCode = code" class="mb-1 w-full rounded-lg px-3 py-2 text-left transition" :class="state.featureCode === code ? 'bg-teal-50 text-tealx' : 'text-slate-600 hover:bg-slate-50'">
<div class="flex items-center gap-2"><span class="h-1.5 w-1.5 rounded-full bg-tealx"></span><span class="truncate font-medium">{{ featureByCode(code).name }}</span><span class="ml-auto rounded bg-slate-100 px-1 text-[9px] text-slate-500">页面</span></div><div class="mono mt-1 truncate text-[10px] text-slate-400">{{ code }}</div>
</button></template>
</div>
<div v-if="!menuGroups.some(group => group.items.some(code => menuMatches(code)))" class="px-3 py-8 text-center text-[11px] text-slate-400">没有匹配的菜单</div>
</div>
</aside>
<section class="min-w-0 space-y-4">
<div class="rounded-xl border border-line bg-white">
<div class="flex items-start gap-3 border-b border-line px-5 py-4"><div><div class="flex items-center gap-2"><h1 class="text-[15px] font-semibold text-ink">{{ currentFeature.name }}</h1><span class="rounded bg-slate-100 px-1.5 py-0.5 text-[10px] text-slate-500">{{ typeName(currentFeature.type) }}</span></div><div class="mono mt-1 text-[11px] text-slate-400">{{ currentFeature.code }}</div></div><div class="ml-auto flex items-center gap-4"><label class="flex cursor-pointer items-center gap-2 rounded-md border px-2.5 py-1.5 text-[12px]" :class="hasPower(accessCode) ? 'border-teal-300 bg-teal-50 text-tealx' : 'border-line bg-white text-slate-500'"><input type="checkbox" class="h-4 w-4 accent-teal-600" :checked="hasPower(accessCode)" @change="togglePower(accessCode)"><span>{{ hasPower(accessCode) ? '启用' : '禁用' }}</span></label><div class="text-right text-[11px] text-slate-400"><div>{{ currentUser.name }} · 当前授权</div><div class="mt-0.5">模板:{{ currentFeature.template }}</div></div></div></div>
<div v-if="childFeatures.length" class="border-b border-line px-5 py-3"><div class="mb-2 text-[11px] font-medium text-slate-500">页面内功能</div><div class="flex flex-wrap gap-2"><button v-for="child in childFeatures" :key="child.code" @click="state.featureCode = child.code" class="rounded-md border px-2.5 py-1.5 text-left text-[11px] transition" :class="state.featureCode === child.code ? 'border-indigo-300 bg-indigo-50 text-indigo-700' : 'border-line text-slate-500 hover:bg-slate-50'"><span class="block font-medium">{{ child.name }}</span><span class="mono text-[10px] opacity-70">{{ child.code }}</span></button></div></div>
<div class="grid gap-3 border-b border-line bg-slate-50/60 px-5 py-3 sm:grid-cols-3"><div><div class="text-[11px] text-slate-400">使用模块</div><div class="mono mt-1 text-[11px] text-slate-600">{{ currentFeature.modules.join(' · ') || '无' }}</div></div><div><div class="text-[11px] text-slate-400">所属功能</div><div class="mono mt-1 text-[11px] text-slate-600">{{ currentFeature.parent || '顶层功能' }}</div></div><div><div class="text-[11px] text-slate-400">授权原则</div><div class="mt-1 text-[11px] text-slate-600">未勾选即不可用</div></div></div>
<div class="space-y-4 px-5 py-4">
<div><div class="mb-3 flex items-center justify-between"><div class="font-medium text-slate-700">模块默认能力</div><div class="text-[11px] text-slate-400">同一模块只配置一次,所有功能默认继承</div></div><div class="space-y-2"><div v-for="module in currentFeature.modules" :key="module" class="rounded-lg border border-line bg-slate-50/60 p-3"><div class="mb-2 flex items-center gap-2"><span class="mono text-[11px] font-medium text-tealx">{{ module }}</span><span class="rounded bg-white px-1.5 py-0.5 text-[10px] text-slate-400">全局默认</span></div><div class="grid gap-2 sm:grid-cols-2 lg:grid-cols-4"><label v-for="capability in moduleCapabilities(module)" :key="capability" class="flex cursor-pointer items-center gap-2 rounded-md border bg-white px-2.5 py-2 text-[11px]" :class="hasModulePower(module, capability) ? 'border-teal-300 text-tealx' : 'border-line text-slate-500'"><input type="checkbox" class="h-3.5 w-3.5 accent-teal-600" :checked="hasModulePower(module, capability)" @change="toggleModulePower(module, capability)"><span>{{ capabilityName(capability) }}</span></label></div></div><div v-if="!currentFeature.modules.length" class="text-[11px] text-slate-400">该功能不直接读取业务数据</div></div></div></div>
<div v-if="currentFeature.modules.length" class="border-t border-line pt-4"><div class="mb-3 flex items-center justify-between"><div class="font-medium text-slate-700">本功能例外</div><div class="text-[11px] text-slate-400">仅能收紧模块默认能力</div></div><div class="space-y-2"><div v-for="module in currentFeature.modules" :key="module" class="rounded-lg border border-amber-200 bg-amber-50/40 p-3"><div class="mb-2 flex items-center gap-2"><span class="mono text-[11px] font-medium text-amber-700">{{ module }}</span><span class="text-[10px] text-amber-700/70">禁止本场景操作</span></div><div class="grid gap-2 sm:grid-cols-2 lg:grid-cols-4"><label v-for="capability in moduleCapabilities(module)" :key="capability" class="flex cursor-pointer items-center gap-2 rounded-md border bg-white px-2.5 py-2 text-[11px]" :class="hasOverride(module, capability) ? 'border-amber-300 text-amber-700' : 'border-line text-slate-500'"><input type="checkbox" class="h-3.5 w-3.5 accent-amber-600" :checked="hasOverride(module, capability)" @change="toggleOverride(module, capability)"><span>{{ capabilityName(capability) }}</span></label></div></div></div></div></div>
<div v-if="featureActionPowers(currentFeature).length" class="border-t border-line pt-4"><div class="mb-3 flex items-center justify-between"><div class="font-medium text-slate-700">功能专属动作</div><div class="text-[11px] text-slate-400">审核、核销等动作单独授权</div></div><div class="grid gap-2 sm:grid-cols-2 lg:grid-cols-3"><button v-for="power in featureActionPowers(currentFeature)" :key="power.code" @click="togglePower(power.code)" class="flex min-h-[58px] items-center gap-3 rounded-lg border px-3 py-2 text-left transition" :class="hasPower(power.code) ? 'border-teal-300 bg-teal-50/70' : 'border-line bg-white hover:bg-slate-50'"><span class="grid h-7 w-7 shrink-0 place-items-center rounded-md text-[11px] font-bold" :class="hasPower(power.code) ? 'bg-tealx text-white' : 'bg-slate-100 text-slate-400'">{{ hasPower(power.code) ? '✓' : '—' }}</span><span class="min-w-0"><span class="block text-[12px] font-medium" :class="hasPower(power.code) ? 'text-tealx' : 'text-slate-600'">{{ capabilityName(power.capability) }}</span><span class="mono mt-0.5 block truncate text-[10px] text-slate-400">{{ power.code }}</span></span></button></div></div>
</div>
</div>
<div class="rounded-xl border border-line bg-white"><div class="border-b border-line px-5 py-3"><div class="font-medium text-slate-700">字段与数据范围</div><div class="mt-0.5 text-[11px] text-slate-400">仍按技术模块配置,个人设置只能收紧</div></div><div class="grid gap-3 p-4 sm:grid-cols-2"><div v-for="module in currentFeature.modules" :key="module" class="rounded-lg bg-slate-50 px-3 py-2.5"><div class="mono text-[11px] text-tealx">{{ module }}</div><div class="mt-1 text-[11px] text-slate-500">字段策略:跟随模块默认</div><div class="mt-0.5 text-[11px] text-slate-500">数据范围:全部数据</div></div><div v-if="!currentFeature.modules.length" class="text-[11px] text-slate-400">该功能不直接读取业务数据</div></div></div>
</section>
<aside class="space-y-4"><div class="rounded-xl border border-line bg-white"><div class="border-b border-line px-4 py-3"><div class="font-semibold text-ink">当前生效结果</div><div class="mt-0.5 text-[11px] text-slate-400">{{ currentUser.name }} · {{ currentFeature.name }}</div></div><div class="space-y-2 p-4"><div v-for="item in resultItems" :key="item.label" class="flex items-center justify-between rounded-lg bg-slate-50 px-3 py-2"><span class="text-[12px] text-slate-600">{{ item.label }}</span><span class="rounded px-1.5 py-0.5 text-[10px]" :class="item.on ? 'bg-emerald-50 text-emerald-700' : 'bg-slate-200 text-slate-400'">{{ item.on ? '已授权' : '未授权' }}</span></div></div></div><div class="rounded-xl border border-line bg-white"><div class="border-b border-line px-4 py-3"><div class="font-semibold text-ink">将写入的授权</div><div class="mt-0.5 text-[11px] text-slate-400">s_user_power · 使用业务权限编码</div></div><div class="max-h-72 overflow-auto p-3"><div v-for="power in activePowers" :key="power.code" class="mb-2 rounded-lg bg-slate-50 p-2.5 last:mb-0"><div class="mono break-all text-[10px] text-tealx">{{ power.code }}</div><div class="mt-1 text-[11px] text-slate-500">{{ currentUser.id }} · b_canuse = 1</div></div><div v-if="!activePowers.length" class="py-8 text-center text-[11px] text-slate-400">当前功能没有授权记录</div></div></div></aside>
</div>
</main>
<main v-else class="mx-auto max-w-[1500px] space-y-4 px-5 py-5"><section class="rounded-xl bg-[#17212b] px-5 py-4 text-white"><div class="flex flex-wrap items-end justify-between gap-4"><div><div class="text-[11px] font-semibold tracking-[.16em] text-teal-300">IT CONFIGURATION</div><h1 class="mt-1 text-lg font-semibold">功能实例与权限生成</h1><p class="mt-1 text-[12px] text-slate-300">技术人员维护业务功能、公共模板和模块依赖,客户只负责授权。</p></div><div class="grid grid-cols-3 gap-2 text-center"><div class="rounded-lg bg-white/10 px-4 py-2"><div class="text-[10px] text-slate-300">功能实例</div><div class="mt-0.5 text-xl font-semibold">{{ features.length }}</div></div><div class="rounded-lg bg-white/10 px-4 py-2"><div class="text-[10px] text-slate-300">公共模板</div><div class="mt-0.5 text-xl font-semibold">{{ templateCount }}</div></div><div class="rounded-lg bg-white/10 px-4 py-2"><div class="text-[10px] text-slate-300">权限点</div><div class="mt-0.5 text-xl font-semibold">{{ allPowers.length }}</div></div></div></div></section><div class="grid gap-4 lg:grid-cols-[minmax(0,1fr)_390px]"><section class="rounded-xl border border-line bg-white"><div class="flex items-center justify-between border-b border-line px-5 py-3"><div><div class="font-semibold text-ink">功能实例清单</div><div class="mt-0.5 text-[11px] text-slate-400">功能入口与模块默认能力分层维护</div></div><input v-model="state.search" class="h-8 w-52 rounded-md border border-line px-2.5 text-[12px] outline-none focus:border-tealx" placeholder="搜索"></div><div class="overflow-auto"><table class="w-full"><thead class="bg-slate-50 text-[11px] text-slate-500"><tr><th class="px-5 py-2.5 text-left font-medium">功能</th><th class="px-3 py-2.5 text-left font-medium">类型</th><th class="px-3 py-2.5 text-left font-medium">公共模板</th><th class="px-3 py-2.5 text-left font-medium">使用模块</th><th class="px-3 py-2.5 text-right font-medium">功能权限</th></tr></thead><tbody><tr v-for="feature in filteredFeatures" :key="feature.code" @click="state.configCode = feature.code" class="cursor-pointer border-t border-slate-100 hover:bg-slate-50"><td class="px-5 py-2.5"><div class="font-medium text-slate-700">{{ feature.name }}</div><div class="mono mt-0.5 text-[10px] text-slate-400">{{ feature.code }}</div></td><td class="px-3 py-2.5 text-[11px] text-slate-500">{{ typeName(feature.type) }}</td><td class="px-3 py-2.5 text-[11px] text-slate-500">{{ feature.template }}</td><td class="px-3 py-2.5"><span v-for="module in feature.modules" :key="module" class="mono mr-1 rounded bg-slate-100 px-1 text-[10px] text-slate-500">{{ module }}</span><span v-if="!feature.modules.length" class="text-[11px] text-slate-300">无</span></td><td class="px-3 py-2.5 text-right text-[11px] text-tealx">{{ powersFor(feature).length }}</td></tr></tbody></table></div></section><section class="rounded-xl border border-line bg-white"><div class="border-b border-line px-4 py-3"><div class="font-semibold text-ink">技术配置详情</div><div class="mono mt-1 text-[11px] text-slate-400">{{ configFeature.code }}</div></div><div class="space-y-3 p-4"><div><div class="text-[11px] text-slate-400">功能名称</div><div class="mt-1 text-[13px] font-medium text-slate-700">{{ configFeature.name }}</div></div><div><div class="text-[11px] text-slate-400">模板</div><div class="mono mt-1 rounded-md bg-slate-50 px-2.5 py-2 text-[11px] text-slate-600">{{ configFeature.template }}</div></div><div><div class="text-[11px] text-slate-400">模块白名单</div><div class="mt-1 space-y-1"><div v-for="module in configFeature.modules" :key="module" class="mono rounded-md bg-teal-50 px-2.5 py-2 text-[11px] text-tealx">{{ module }}</div><div v-if="!configFeature.modules.length" class="text-[11px] text-slate-400">无数据模块</div></div></div><div><div class="mb-1.5 text-[11px] text-slate-400">自动生成权限</div><div class="space-y-1.5"><div v-for="power in configPowers(configFeature)" :key="power.code" class="flex items-center justify-between rounded-md bg-slate-50 px-2.5 py-2"><span class="mono text-[10px] text-slate-600">{{ power.code }}</span><span class="text-[10px] text-slate-400">{{ capabilityName(power.capability) }}</span></div></div></div></div></section></div></main>
</div>
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
<script>
const { createApp, reactive, computed } = Vue;
const features = [
{ code: 'sea_detail', name: '海运详细', type: 'page', parent: '', template: 'DetailPage', modules: ['sea_main', 'cw_fee'], powers: ['access', 'audit'] },
{ code: 'sea_detail_container', name: '海运详细 · 装箱区域', type: 'panel', parent: 'sea_detail', template: 'DetailPanel', modules: ['container_main'], powers: ['access', 'read'] },
{ code: 'container_query', name: '装箱查询', type: 'page', parent: '', template: 'CommonListPage', modules: ['container_main'], powers: ['access', 'read', 'export'] },
{ code: 'customer_list', name: '客户档案', type: 'page', parent: '', template: 'CommonListPage', modules: ['customer'], powers: ['access'] },
{ code: 'finance_dashboard', name: '财务看板', type: 'page', parent: '', template: 'DashboardPage', modules: [], powers: ['access'] },
{ code: 'ar_aging_card', name: '应收账龄', type: 'report_card', parent: 'finance_dashboard', template: 'ReportCard', modules: ['finance_main'], powers: ['access', 'export'] },
{ code: 'gross_profit_card', name: '毛利分析', type: 'report_card', parent: 'finance_dashboard', template: 'ReportCard', modules: ['finance_main'], powers: ['access', 'export'] }
];
const users = [{ id: 'zhangsan', name: '张三', title: '费用会计' }, { id: 'lisi', name: '李四', title: '财务经理' }, { id: 'wangwu', name: '王五', title: '海运操作' }, { id: 'zhouba', name: '周八', title: '销售经理' }];
const menuGroups = [{ code: 'operation', name: '业务管理', items: ['sea_detail', 'container_query'] }, { code: 'basic', name: '基础资料', items: ['customer_list'] }, { code: 'finance', name: '财务中心', items: ['finance_dashboard'] }];
const moduleCaps = { sea_main: ['read', 'update'], cw_fee: ['read', 'create', 'update', 'export'], container_main: ['read', 'export'], customer: ['read', 'create', 'update', 'delete', 'export'], finance_main: ['read'] };
const grants = reactive({ zhangsan: ['feature.sea_detail.access', 'feature.sea_detail_container.access', 'feature.container_query.access', 'feature.cw_fee_list.access', 'module.sea_main.read', 'module.container_main.read', 'module.cw_fee.read', 'module.cw_fee.create', 'module.cw_fee.update'], lisi: ['feature.sea_detail.access', 'feature.sea_detail_container.access', 'feature.container_query.access', 'feature.cw_fee_list.access', 'feature.finance_dashboard.access', 'feature.ar_aging_card.access', 'module.sea_main.read', 'module.sea_main.update', 'module.container_main.read', 'module.container_main.export', 'module.cw_fee.read', 'module.cw_fee.create', 'module.cw_fee.update', 'module.cw_fee.export', 'module.customer.read', 'module.customer.create', 'module.customer.update', 'module.customer.delete', 'module.customer.export', 'module.finance_main.read', 'feature.sea_detail.audit'], wangwu: ['feature.sea_detail.access', 'feature.sea_detail_container.access', 'module.sea_main.read', 'module.container_main.read'], zhouba: [] });
const overrides = reactive({ zhangsan: [], lisi: ['sea_detail|cw_fee|create', 'sea_detail|cw_fee|update', 'sea_detail|cw_fee|export'], wangwu: [], zhouba: [] });
const capabilityNames = { access: '启用', read: '查看 / 查询', create: '新增', update: '修改', delete: '删除', export: '导出', audit: '审核' };
function powerFor(feature, capability) { return { code: 'feature.' + feature.code + '.' + capability, capability }; }
function modulePowerFor(module, capability) { return { code: 'module.' + module + '.' + capability, capability, module }; }
createApp({ setup() {
const state = reactive({ tab: 'grant', userId: 'zhangsan', featureCode: 'container_query', configCode: 'container_query', search: '' });
const tabs = [{ key: 'grant', name: '用户授权' }, { key: 'config', name: '功能配置' }];
const filteredFeatures = computed(() => { const q = state.search.trim().toLowerCase(); return q ? features.filter(x => (x.name + x.code + x.template).toLowerCase().includes(q)) : features; });
const currentFeature = computed(() => features.find(x => x.code === state.featureCode) || features[0]);
const accessCode = computed(() => 'feature.' + currentFeature.value.code + '.access');
const childFeatures = computed(() => features.filter(x => x.parent === currentFeature.value.code));
const configFeature = computed(() => features.find(x => x.code === state.configCode) || features[0]);
const currentUser = computed(() => users.find(x => x.id === state.userId) || users[0]);
const allPowers = computed(() => features.flatMap(f => powersFor(f)).concat(Object.keys(moduleCaps).flatMap(m => moduleCapabilities(m).map(c => modulePowerFor(m, c)))));
const templateCount = computed(() => new Set(features.map(x => x.template)).size);
function powersFor(feature) { return feature.powers.map(cap => powerFor(feature, cap)); }
function featureActionPowers(feature) { return powersFor(feature).filter(p => p.capability !== 'access'); }
function moduleCapabilities(module) { return moduleCaps[module] || []; }
function hasModulePower(module, capability) { return grants[state.userId].includes(modulePowerFor(module, capability).code); }
function toggleModulePower(module, capability) { togglePower(modulePowerFor(module, capability).code); }
function overrideKey(module, capability) { return currentFeature.value.code + '|' + module + '|' + capability; }
function hasOverride(module, capability) { return overrides[state.userId].includes(overrideKey(module, capability)); }
function toggleOverride(module, capability) { const list = overrides[state.userId]; const key = overrideKey(module, capability); const i = list.indexOf(key); if (i > -1) list.splice(i, 1); else list.push(key); }
function hasPower(code) { return grants[state.userId].includes(code); }
function togglePower(code) { const list = grants[state.userId]; const i = list.indexOf(code); if (i > -1) list.splice(i, 1); else list.push(code); }
function activePowers() { const feature = powersFor(currentFeature.value).filter(p => hasPower(p.code)); const modules = currentFeature.value.modules.flatMap(m => moduleCapabilities(m).map(c => modulePowerFor(m, c))).filter(p => hasModulePower(p.module, p.capability)); return feature.concat(modules); }
function grantCount(userId) { return grants[userId].length; }
function featureByCode(code) { return features.find(x => x.code === code) || features[0]; }
function menuMatches(code) { const q = state.search.trim().toLowerCase(); const f = featureByCode(code); return !q || (f.name + f.code).toLowerCase().includes(q); }
function typeName(type) { return type === 'page' ? '页面' : type === 'panel' ? '区域' : '报表卡片'; }
function capabilityName(cap) { return capabilityNames[cap] || cap; }
return { state, tabs, users, features, menuGroups, filteredFeatures, currentFeature, accessCode, childFeatures, configFeature, currentUser, allPowers, templateCount, powersFor, featureActionPowers, moduleCapabilities, hasModulePower, toggleModulePower, hasOverride, toggleOverride, hasPower, togglePower, activePowers, grantCount, featureByCode, menuMatches, typeName, capabilityName, resultItems: computed(() => { const f = currentFeature.value; const items = [{ label: '功能启用', on: hasPower('feature.' + f.code + '.access') }]; f.modules.forEach(m => moduleCapabilities(m).forEach(c => items.push({ label: m + ' · ' + capabilityName(c), on: hasModulePower(m, c) && !hasOverride(m, c) }))); return items; }) };
} }).mount('#app');
</script>
</body>
</html>