270 lines
33 KiB
HTML
270 lines
33 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 src="https://unpkg.com/lucide@latest"></script>
|
|
<script>
|
|
tailwind.config = {
|
|
theme: {
|
|
extend: {
|
|
fontFamily: { sans: ['Inter', 'Segoe UI', 'Microsoft YaHei', 'sans-serif'] },
|
|
colors: { ink: '#17212b', paper: '#f6f7f8', line: '#dfe5e8', brand: '#2463eb', tealx: '#0f766e' }
|
|
}
|
|
}
|
|
};
|
|
</script>
|
|
<style>
|
|
:root { color-scheme: light; }
|
|
* { box-sizing: border-box; }
|
|
body { margin: 0; background: #f6f7f8; color: #334155; font-family: Inter, "Segoe UI", "Microsoft YaHei", sans-serif; }
|
|
button, input, select { font: inherit; }
|
|
button:focus-visible, input:focus-visible, select:focus-visible { outline: 3px solid rgba(37, 99, 235, .22); outline-offset: 2px; }
|
|
.scrollbar::-webkit-scrollbar { width: 8px; height: 8px; }
|
|
.scrollbar::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 8px; }
|
|
.segmented button { min-width: 58px; }
|
|
.segmented button + button { border-left: 1px solid #e2e8f0; }
|
|
.menu-row:hover { background: #fbfdff; }
|
|
.menu-row.selected { background: #eff6ff; }
|
|
.toast { animation: toast-in 180ms ease-out; }
|
|
@keyframes toast-in { from { transform: translate(-50%, 8px); opacity: 0 } to { transform: translate(-50%, 0); opacity: 1 } }
|
|
@media (max-width: 900px) {
|
|
.app-grid { grid-template-columns: 1fr !important; }
|
|
.role-panel { min-height: auto !important; }
|
|
.preview-panel { position: static !important; }
|
|
.menu-head, .menu-row > [data-select-menu] { grid-template-columns: 1fr !important; }
|
|
.menu-head > div:nth-child(2), .menu-head > div:nth-child(3) { display: none; }
|
|
.menu-row > [data-select-menu] > div:nth-child(2), .menu-row > [data-select-menu] > div:nth-child(3) { padding-left: 44px; text-align: left; }
|
|
.menu-row > [data-select-menu] > div:nth-child(3) .flex { justify-content: flex-start; }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="app" class="min-h-screen">
|
|
<header class="border-b border-line bg-white">
|
|
<div class="mx-auto flex min-h-[68px] max-w-[1580px] items-center gap-4 px-5 py-3">
|
|
<div class="flex items-center gap-3">
|
|
<div class="grid h-9 w-9 place-items-center rounded-lg bg-ink text-sm font-bold text-white">F</div>
|
|
<div class="leading-tight">
|
|
<div class="text-[15px] font-semibold tracking-[-.01em] text-ink">FMS 权限中心</div>
|
|
<div class="mt-0.5 text-[11px] text-slate-400">岗位授权 · 菜单化配置,模块化执行</div>
|
|
</div>
|
|
</div>
|
|
<div class="ml-5 hidden h-7 w-px bg-line md:block"></div>
|
|
<div class="hidden items-center gap-2 text-[12px] text-slate-500 md:flex">
|
|
<span>系统设置</span><i data-lucide="chevron-right" class="h-3.5 w-3.5 text-slate-300"></i><span class="font-medium text-slate-700">权限分配</span>
|
|
</div>
|
|
<div class="ml-auto flex items-center gap-3">
|
|
<button id="previewUserBtn" class="hidden items-center gap-2 rounded-md border border-line bg-white px-3 py-2 text-[12px] text-slate-600 transition hover:border-slate-300 hover:bg-slate-50 sm:flex">
|
|
<i data-lucide="user-round-search" class="h-4 w-4 text-slate-400"></i>预览用户
|
|
</button>
|
|
<button id="saveBtn" class="flex items-center gap-2 rounded-md bg-brand px-3.5 py-2 text-[12px] font-medium text-white shadow-sm transition hover:bg-blue-700">
|
|
<i data-lucide="save" class="h-4 w-4"></i>保存变更
|
|
</button>
|
|
<div class="hidden h-8 w-px bg-line sm:block"></div>
|
|
<div class="flex items-center gap-2">
|
|
<div class="grid h-8 w-8 place-items-center rounded-full bg-slate-100 text-[11px] font-bold text-slate-600">IT</div>
|
|
<div class="hidden leading-tight sm:block"><div class="text-[12px] font-medium text-slate-700">系统管理员</div><div class="text-[10px] text-slate-400">DEMO-FWD</div></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
<main class="mx-auto max-w-[1580px] px-5 py-5">
|
|
<section class="mb-4 flex flex-wrap items-end justify-between gap-4">
|
|
<div>
|
|
<div class="mb-1 flex items-center gap-2 text-[11px] font-semibold uppercase tracking-[.13em] text-brand"><span class="h-1.5 w-1.5 rounded-full bg-brand"></span>授权工作台</div>
|
|
<h1 class="text-[22px] font-semibold tracking-[-.02em] text-ink">配置岗位能使用哪些业务功能</h1>
|
|
<p class="mt-1 text-[13px] text-slate-500">先按业务菜单选择权限等级,系统会自动展开到对应模块。共享模块只配置一次。</p>
|
|
</div>
|
|
<div class="flex items-center gap-2 rounded-md border border-amber-200 bg-amber-50 px-3 py-2 text-[11px] text-amber-800">
|
|
<i data-lucide="shield-check" class="h-4 w-4 text-amber-600"></i>
|
|
<span>未授权 = 拒绝 · 个人设置不能扩大岗位权限</span>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="mb-4 grid grid-cols-2 gap-3 sm:grid-cols-4">
|
|
<div class="border border-line bg-white px-4 py-3"><div class="text-[11px] text-slate-400">岗位角色</div><div id="roleCount" class="mt-1 text-[21px] font-semibold text-ink">5</div></div>
|
|
<div class="border border-line bg-white px-4 py-3"><div class="text-[11px] text-slate-400">当前岗位成员</div><div id="memberCount" class="mt-1 text-[21px] font-semibold text-ink">12</div></div>
|
|
<div class="border border-line bg-white px-4 py-3"><div class="text-[11px] text-slate-400">已配置业务菜单</div><div id="configuredCount" class="mt-1 text-[21px] font-semibold text-ink">6</div></div>
|
|
<div class="border border-line bg-white px-4 py-3"><div class="text-[11px] text-slate-400">待保存变更</div><div id="changeCount" class="mt-1 text-[21px] font-semibold text-amber-600">0</div></div>
|
|
</section>
|
|
|
|
<div class="app-grid grid items-start gap-4 xl:grid-cols-[250px_minmax(0,1fr)_330px]">
|
|
<aside class="role-panel min-h-[720px] border border-line bg-white">
|
|
<div class="border-b border-line px-4 py-4">
|
|
<div class="flex items-center justify-between"><div class="font-semibold text-ink">岗位角色</div><button id="addRoleBtn" title="新增岗位" class="grid h-7 w-7 place-items-center rounded-md border border-line text-slate-500 transition hover:border-brand hover:text-brand"><i data-lucide="plus" class="h-4 w-4"></i></button></div>
|
|
<div class="mt-1 text-[11px] text-slate-400">新员工先绑定岗位,少量人员再做例外</div>
|
|
<div class="relative mt-3"><i data-lucide="search" class="pointer-events-none absolute left-2.5 top-2 h-4 w-4 text-slate-400"></i><input id="roleSearch" placeholder="搜索岗位" class="h-8 w-full rounded-md border border-line bg-white pl-8 pr-2 text-[12px] text-slate-700 outline-none focus:border-brand" /></div>
|
|
</div>
|
|
<div id="roleList" class="scrollbar max-h-[620px] overflow-auto p-2"></div>
|
|
<div class="border-t border-line px-4 py-3"><button id="exceptionBtn" class="flex w-full items-center justify-between rounded-md px-2 py-2 text-left text-[11px] text-slate-500 transition hover:bg-slate-50"><span class="flex items-center gap-2"><i data-lucide="user-cog" class="h-4 w-4 text-slate-400"></i>用户例外</span><i data-lucide="arrow-up-right" class="h-3.5 w-3.5 text-slate-400"></i></button></div>
|
|
</aside>
|
|
|
|
<section class="min-w-0 border border-line bg-white">
|
|
<div class="border-b border-line px-5 py-4">
|
|
<div class="flex flex-wrap items-start justify-between gap-3">
|
|
<div>
|
|
<div class="flex items-center gap-2"><h2 id="currentRoleName" class="text-[16px] font-semibold text-ink">费用会计</h2><span id="currentRoleStatus" class="rounded bg-emerald-50 px-2 py-1 text-[10px] font-medium text-emerald-700">已启用</span></div>
|
|
<div class="mt-1 flex flex-wrap items-center gap-x-3 gap-y-1 text-[11px] text-slate-400"><span id="currentRoleDesc">负责费用录入、核对和结算</span><span class="text-slate-300">·</span><span id="currentRoleMembers">12 名成员</span><span class="text-slate-300">·</span><span>主岗位</span></div>
|
|
</div>
|
|
<div class="flex items-center gap-2"><button id="resetBtn" class="flex items-center gap-2 rounded-md border border-line px-3 py-2 text-[11px] text-slate-600 transition hover:bg-slate-50"><i data-lucide="rotate-ccw" class="h-3.5 w-3.5"></i>恢复岗位模板</button><button id="memberBtn" class="flex items-center gap-2 rounded-md border border-line px-3 py-2 text-[11px] text-slate-600 transition hover:bg-slate-50"><i data-lucide="users" class="h-3.5 w-3.5"></i>查看成员</button></div>
|
|
</div>
|
|
</div>
|
|
<div class="border-b border-line bg-slate-50/70 px-5 py-3">
|
|
<div class="flex flex-wrap items-center justify-between gap-2"><div><div class="text-[12px] font-semibold text-slate-700">业务菜单权限</div><div class="mt-0.5 text-[11px] text-slate-400">权限等级会自动展开为模块动作;“自定义”仅在特殊场景使用</div></div><div class="flex items-center gap-2"><div class="relative"><i data-lucide="search" class="pointer-events-none absolute left-2.5 top-1.5 h-3.5 w-3.5 text-slate-400"></i><input id="menuSearch" placeholder="搜索业务菜单" class="h-7 w-40 rounded-md border border-line bg-white pl-7 pr-2 text-[11px] outline-none focus:border-brand" /></div><button id="expandBtn" class="flex h-7 items-center gap-1.5 rounded-md border border-line bg-white px-2.5 text-[11px] text-slate-600 hover:bg-slate-50"><i data-lucide="list-plus" class="h-3.5 w-3.5"></i><span>展开全部</span></button></div></div>
|
|
</div>
|
|
<div class="menu-head grid grid-cols-[minmax(210px,1fr)_minmax(300px,1.3fr)_120px] gap-3 border-b border-line px-5 py-2.5 text-[10px] font-semibold uppercase tracking-[.08em] text-slate-400"><div>业务页面</div><div>岗位权限等级</div><div class="text-right">使用模块</div></div>
|
|
<div id="menuList" class="scrollbar max-h-[560px] overflow-auto"></div>
|
|
<div id="detailPanel" class="border-t border-line bg-[#fbfcfd]"></div>
|
|
</section>
|
|
|
|
<aside class="preview-panel sticky top-4 space-y-4">
|
|
<section class="border border-line bg-white">
|
|
<div class="border-b border-line px-4 py-4"><div class="flex items-center justify-between"><div><div class="font-semibold text-ink">生效结果</div><div class="mt-1 text-[11px] text-slate-400">系统按模块合并后的真实权限</div></div><span class="grid h-8 w-8 place-items-center rounded-md bg-blue-50 text-brand"><i data-lucide="scan-eye" class="h-4 w-4"></i></span></div></div>
|
|
<div class="grid grid-cols-2 gap-px bg-line"><div class="bg-white px-4 py-3"><div class="text-[10px] text-slate-400">可进入菜单</div><div id="previewMenus" class="mt-1 text-[20px] font-semibold text-ink">6</div></div><div class="bg-white px-4 py-3"><div class="text-[10px] text-slate-400">可用模块</div><div id="previewModules" class="mt-1 text-[20px] font-semibold text-ink">8</div></div><div class="bg-white px-4 py-3"><div class="text-[10px] text-slate-400">经办动作</div><div id="previewActions" class="mt-1 text-[20px] font-semibold text-ink">14</div></div><div class="bg-white px-4 py-3"><div class="text-[10px] text-slate-400">敏感字段</div><div id="previewSensitive" class="mt-1 text-[20px] font-semibold text-ink">3</div></div></div>
|
|
<div class="border-t border-line px-4 py-3"><div class="mb-2 flex items-center justify-between"><span class="text-[11px] font-medium text-slate-600">模块权限来源</span><span class="text-[10px] text-slate-400">共享模块自动合并</span></div><div id="moduleSummary" class="space-y-2"></div></div>
|
|
</section>
|
|
<section class="border border-line bg-white">
|
|
<div class="border-b border-line px-4 py-3"><div class="flex items-center justify-between"><div class="font-semibold text-ink">统一数据范围</div><button id="scopeBtn" title="编辑数据范围" class="grid h-7 w-7 place-items-center rounded-md border border-line text-slate-500 hover:border-brand hover:text-brand"><i data-lucide="sliders-horizontal" class="h-3.5 w-3.5"></i></button></div><div class="mt-1 text-[11px] text-slate-400">默认作用于岗位下的业务模块</div></div>
|
|
<div class="space-y-2 px-4 py-3"><div class="flex items-center justify-between rounded-md bg-slate-50 px-3 py-2"><span class="text-[11px] text-slate-500">查看范围</span><span class="flex items-center gap-1.5 text-[11px] font-medium text-slate-700"><span class="h-1.5 w-1.5 rounded-full bg-tealx"></span><span id="readScope">本部门及下级</span></span></div><div class="flex items-center justify-between rounded-md bg-slate-50 px-3 py-2"><span class="text-[11px] text-slate-500">操作范围</span><span class="flex items-center gap-1.5 text-[11px] font-medium text-slate-700"><span class="h-1.5 w-1.5 rounded-full bg-amber-500"></span><span id="writeScope">本部门</span></span></div></div>
|
|
</section>
|
|
<section class="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-1 text-[11px] text-slate-400">只展示需要关注的字段组</div></div><div id="fieldSummary" class="space-y-2 px-4 py-3"></div>
|
|
</section>
|
|
<div class="flex items-start gap-2 border border-blue-100 bg-blue-50/70 px-3 py-3 text-[11px] leading-5 text-blue-800"><i data-lucide="info" class="mt-0.5 h-4 w-4 shrink-0 text-brand"></i><span>菜单是配置入口,模块是执行主体。修改共享模块时,系统会提示受影响的其他业务页面。</span></div>
|
|
</aside>
|
|
</div>
|
|
</main>
|
|
<div id="toast" class="pointer-events-none fixed bottom-5 left-1/2 z-50 hidden -translate-x-1/2"></div>
|
|
</div>
|
|
|
|
<script>
|
|
const levels = [
|
|
{ key: 'none', label: '无权', actions: 0 },
|
|
{ key: 'view', label: '查看', actions: 1 },
|
|
{ key: 'operate', label: '经办', actions: 3 },
|
|
{ key: 'manage', label: '主管', actions: 5 },
|
|
{ key: 'custom', label: '自定义', actions: 0 }
|
|
];
|
|
const menus = [
|
|
{ id: 'sea', group: '海运业务', name: '海运业务', desc: '海运订单、箱量与操作节点', icon: 'ship', modules: [{ id: 'shipment', name: '海运订单', usage: '主模块' }, { id: 'container', name: '装箱管理', usage: '业务模块' }], shared: false },
|
|
{ id: 'air', group: '空运业务', name: '空运业务', desc: '空运订单、航班与操作节点', icon: 'plane', modules: [{ id: 'air_order', name: '空运订单', usage: '主模块' }, { id: 'air_charge', name: '空运费用', usage: '引用模块' }], shared: false },
|
|
{ id: 'fee', group: '财务管理', name: '费用管理', desc: '费用录入、审核与结算', icon: 'receipt-text', modules: [{ id: 'cw_fee', name: '费用模块', usage: '主模块' }, { id: 'customer', name: '客户档案', usage: '引用模块' }], shared: true },
|
|
{ id: 'invoice', group: '财务管理', name: '发票管理', desc: '开票、收票与核销', icon: 'file-text', modules: [{ id: 'invoice', name: '发票模块', usage: '主模块' }, { id: 'cw_fee', name: '费用模块', usage: '共享模块' }], shared: true },
|
|
{ id: 'customer', group: '基础资料', name: '客户档案', desc: '客户资料和联系人维护', icon: 'contact', modules: [{ id: 'customer', name: '客户档案', usage: '主模块' }], shared: true },
|
|
{ id: 'report', group: '分析报表', name: '经营报表', desc: '收入、成本与毛利分析', icon: 'chart-no-axes-combined', modules: [{ id: 'report', name: '报表数据集', usage: '报表模块' }], shared: false }
|
|
];
|
|
const roles = [
|
|
{ id: 'operator', name: '海运操作', desc: '订单录入与节点跟进', members: 18, color: 'bg-blue-100 text-blue-700', levels: { sea: 'operate', air: 'none', fee: 'view', invoice: 'none', customer: 'view', report: 'none' }, scopes: ['本部门及下级', '本部门'], fields: [['内部成本', '隐藏'], ['毛利', '隐藏'], ['客户联系人', '可见']] },
|
|
{ id: 'service', name: '客服专员', desc: '客户沟通与业务查询', members: 9, color: 'bg-violet-100 text-violet-700', levels: { sea: 'operate', air: 'operate', fee: 'view', invoice: 'view', customer: 'view', report: 'view' }, scopes: ['全部业务', '本部门'], fields: [['内部成本', '隐藏'], ['毛利', '隐藏'], ['客户联系人', '可编辑']] },
|
|
{ id: 'accountant', name: '费用会计', desc: '负责费用录入、核对和结算', members: 12, color: 'bg-emerald-100 text-emerald-700', levels: { sea: 'view', air: 'view', fee: 'manage', invoice: 'operate', customer: 'view', report: 'view' }, scopes: ['本部门及下级', '本部门'], fields: [['内部成本', '可见'], ['毛利', '只读'], ['客户联系人', '可见']] },
|
|
{ id: 'finance', name: '财务经理', desc: '财务审核、核销与经营分析', members: 4, color: 'bg-amber-100 text-amber-700', levels: { sea: 'view', air: 'view', fee: 'manage', invoice: 'manage', customer: 'view', report: 'manage' }, scopes: ['全部业务', '全部业务'], fields: [['内部成本', '可见'], ['毛利', '可编辑'], ['客户联系人', '可见']] },
|
|
{ id: 'readonly', name: '只读查询', desc: '跨业务查询,不允许修改', members: 27, color: 'bg-slate-100 text-slate-600', levels: { sea: 'view', air: 'view', fee: 'view', invoice: 'view', customer: 'view', report: 'view' }, scopes: ['全部业务', '无'], fields: [['内部成本', '隐藏'], ['毛利', '隐藏'], ['客户联系人', '可见']] }
|
|
];
|
|
const appState = { roleId: 'accountant', roleSearch: '', menuSearch: '', selectedMenu: 'fee', expanded: new Set(['fee']), changed: 0, toastTimer: null };
|
|
const roleById = id => roles.find(role => role.id === id);
|
|
const menuById = id => menus.find(menu => menu.id === id);
|
|
const levelByKey = key => levels.find(level => level.key === key) || levels[0];
|
|
function icon(name, cls = 'h-4 w-4') { return `<i data-lucide="${name}" class="${cls}"></i>`; }
|
|
function refreshIcons() { if (window.lucide) lucide.createIcons({ attrs: { 'stroke-width': 1.8 } }); }
|
|
function showToast(message, tone = 'dark') {
|
|
const toast = document.getElementById('toast');
|
|
toast.className = 'toast pointer-events-none fixed bottom-5 left-1/2 z-50 -translate-x-1/2';
|
|
toast.innerHTML = `<div class="flex items-center gap-2 rounded-md px-3.5 py-2.5 text-[12px] shadow-lg ${tone === 'success' ? 'bg-emerald-700 text-white' : 'bg-ink text-white'}">${icon(tone === 'success' ? 'check-circle-2' : 'info', 'h-4 w-4')}<span>${message}</span></div>`;
|
|
refreshIcons();
|
|
clearTimeout(appState.toastTimer);
|
|
appState.toastTimer = setTimeout(() => { toast.className = 'pointer-events-none fixed bottom-5 left-1/2 z-50 hidden -translate-x-1/2'; }, 2300);
|
|
}
|
|
function levelClass(key) { return ({ none: 'bg-slate-100 font-medium text-slate-500', view: 'bg-blue-50 font-medium text-blue-700', operate: 'bg-teal-50 font-medium text-teal-700', manage: 'bg-amber-50 font-medium text-amber-700', custom: 'bg-violet-50 font-medium text-violet-700' })[key]; }
|
|
function levelControl(menu, role) {
|
|
const current = role.levels[menu.id] || 'none';
|
|
return `<div class="segmented flex w-fit overflow-hidden rounded-md border border-line bg-white">${levels.map(level => `<button data-level="${level.key}" data-menu="${menu.id}" class="h-8 px-2.5 text-[11px] transition ${current === level.key ? levelClass(level.key) : 'text-slate-500 hover:bg-slate-50'}">${level.label}</button>`).join('')}</div>`;
|
|
}
|
|
function renderRoles() {
|
|
const query = appState.roleSearch.trim().toLowerCase();
|
|
const list = roles.filter(role => !query || `${role.name}${role.desc}`.toLowerCase().includes(query));
|
|
document.getElementById('roleList').innerHTML = list.map(role => `<button data-role="${role.id}" class="mb-1.5 flex w-full items-center gap-3 rounded-md px-3 py-3 text-left transition ${appState.roleId === role.id ? 'bg-blue-50 ring-1 ring-inset ring-blue-100' : 'hover:bg-slate-50'}"><span class="grid h-8 w-8 shrink-0 place-items-center rounded-md text-[11px] font-semibold ${role.color}">${role.name.slice(0, 1)}</span><span class="min-w-0 flex-1"><span class="flex items-center justify-between gap-2"><span class="truncate text-[12px] font-medium ${appState.roleId === role.id ? 'text-blue-800' : 'text-slate-700'}">${role.name}</span><span class="text-[10px] text-slate-400">${role.members}人</span></span><span class="mt-0.5 block truncate text-[10px] text-slate-400">${role.desc}</span></span></button>`).join('') || '<div class="px-3 py-10 text-center text-[11px] text-slate-400">没有匹配岗位</div>';
|
|
document.querySelectorAll('[data-role]').forEach(el => el.addEventListener('click', () => { appState.roleId = el.dataset.role; appState.selectedMenu = 'fee'; appState.expanded = new Set(['fee']); render(); }));
|
|
}
|
|
function renderMenus() {
|
|
const role = roleById(appState.roleId);
|
|
const query = appState.menuSearch.trim().toLowerCase();
|
|
const filtered = menus.filter(menu => !query || `${menu.name}${menu.desc}${menu.group}`.toLowerCase().includes(query));
|
|
let previousGroup = '';
|
|
let html = '';
|
|
filtered.forEach(menu => {
|
|
if (menu.group !== previousGroup) { html += `<div class="border-b border-line bg-slate-50/90 px-5 py-2 text-[10px] font-semibold tracking-wide text-slate-400">${menu.group}</div>`; previousGroup = menu.group; }
|
|
const active = appState.selectedMenu === menu.id;
|
|
const expanded = appState.expanded.has(menu.id);
|
|
const current = levelByKey(role.levels[menu.id]);
|
|
html += `<div class="menu-row ${active ? 'selected' : ''} border-b border-line transition"><div data-select-menu="${menu.id}" class="grid cursor-pointer grid-cols-[minmax(210px,1fr)_minmax(300px,1.3fr)_120px] items-center gap-3 px-5 py-3"><div class="flex min-w-0 items-center gap-3"><button data-expand-menu="${menu.id}" title="展开${menu.name}" class="grid h-7 w-7 shrink-0 place-items-center rounded-md border border-line bg-white text-slate-400 transition hover:border-brand hover:text-brand">${icon(expanded ? 'chevron-down' : 'chevron-right', 'h-3.5 w-3.5')}</button><span class="grid h-8 w-8 shrink-0 place-items-center rounded-md ${active ? 'bg-blue-100 text-brand' : 'bg-slate-100 text-slate-500'}">${icon(menu.icon, 'h-4 w-4')}</span><span class="min-w-0"><span class="flex items-center gap-2"><span class="truncate text-[13px] font-medium ${active ? 'text-blue-800' : 'text-slate-700'}">${menu.name}</span>${menu.shared ? '<span class="shrink-0 rounded bg-slate-100 px-1.5 py-0.5 text-[9px] text-slate-500">含共享模块</span>' : ''}</span><span class="mt-0.5 block truncate text-[10px] text-slate-400">${menu.desc}</span></span></div><div class="min-w-0" onclick="event.stopPropagation()">${levelControl(menu, role)}<div class="mt-1.5 flex items-center gap-1.5 text-[10px] text-slate-400"><span class="h-1.5 w-1.5 rounded-full ${current.key === 'none' ? 'bg-slate-300' : current.key === 'custom' ? 'bg-violet-500' : 'bg-emerald-500'}"></span>${current.key === 'none' ? '未开放' : current.key === 'custom' ? '已配置特殊动作' : `包含 ${current.actions} 项基础能力`}</div></div><div class="text-right"><div class="flex flex-wrap justify-end gap-1">${menu.modules.slice(0, 2).map(module => `<span class="rounded bg-white px-1.5 py-1 text-[10px] text-slate-500 ring-1 ring-inset ring-slate-200">${module.name}</span>`).join('')}</div><div class="mt-1 text-[10px] text-slate-400">${menu.modules.length} 个模块</div></div></div>`;
|
|
if (expanded && active) html += `<div class="border-t border-blue-100 bg-blue-50/40 px-5 py-3"><div class="mb-2 flex items-center justify-between"><div class="flex items-center gap-2 text-[11px] font-medium text-blue-900">${icon('layers-3', 'h-4 w-4 text-brand')}页面使用的模块</div><span class="text-[10px] text-blue-700/70">IT 已配置能力上限,岗位权限会与之取交集</span></div><div class="grid gap-2 sm:grid-cols-2">${menu.modules.map(module => `<div class="flex items-center justify-between rounded-md border border-blue-100 bg-white px-3 py-2.5"><span class="min-w-0"><span class="block truncate text-[11px] font-medium text-slate-700">${module.name}</span><span class="mono mt-0.5 block text-[9px] text-slate-400">${module.id} · ${module.usage}</span></span>${module.id === 'cw_fee' ? '<span class="ml-2 flex shrink-0 items-center gap-1 text-[10px] text-amber-700"><span class="h-1.5 w-1.5 rounded-full bg-amber-500"></span>共享</span>' : '<span class="ml-2 text-[10px] text-slate-400">随页面生效</span>'}</div>`).join('')}</div>${menu.shared ? `<div class="mt-3 flex items-start gap-2 border-t border-blue-100 pt-3 text-[10px] leading-5 text-blue-800">${icon('link-2', 'mt-0.5 h-3.5 w-3.5 shrink-0 text-brand')}<span>该页面引用了共享模块。修改模块权限时,系统会同步影响使用同一模块的其他页面。</span></div>` : ''}</div>`;
|
|
});
|
|
document.getElementById('menuList').innerHTML = html || '<div class="px-5 py-12 text-center text-[11px] text-slate-400">没有匹配的业务菜单</div>';
|
|
document.querySelectorAll('[data-level]').forEach(el => el.addEventListener('click', () => setLevel(el.dataset.menu, el.dataset.level)));
|
|
document.querySelectorAll('[data-select-menu]').forEach(el => el.addEventListener('click', () => { appState.selectedMenu = el.dataset.selectMenu; appState.expanded.add(el.dataset.selectMenu); render(); }));
|
|
document.querySelectorAll('[data-expand-menu]').forEach(el => el.addEventListener('click', event => { event.stopPropagation(); const id = el.dataset.expandMenu; appState.expanded.has(id) ? appState.expanded.delete(id) : appState.expanded.add(id); render(); }));
|
|
renderDetail(role, menuById(appState.selectedMenu));
|
|
}
|
|
function renderDetail(role, menu) {
|
|
if (!menu) return;
|
|
const level = levelByKey(role.levels[menu.id]);
|
|
const actionNames = level.key === 'manage' ? ['查看', '新增', '修改', '导出', '审核'] : level.key === 'operate' ? ['查看', '新增', '修改'] : level.key === 'view' ? ['查看'] : level.key === 'custom' ? ['查看', '修改', '导出'] : [];
|
|
document.getElementById('detailPanel').innerHTML = `<div class="flex flex-wrap items-start justify-between gap-3 px-5 py-4"><div><div class="flex items-center gap-2"><span class="grid h-7 w-7 place-items-center rounded-md bg-white text-brand ring-1 ring-inset ring-blue-100">${icon(menu.icon, 'h-3.5 w-3.5')}</span><div><div class="text-[12px] font-semibold text-slate-700">${menu.name} · 高级设置</div><div class="mt-0.5 text-[10px] text-slate-400">只有需要特殊收紧时才调整这里</div></div></div></div><button id="detailScopeBtn" class="flex items-center gap-1.5 rounded-md border border-line bg-white px-2.5 py-1.5 text-[10px] text-slate-600 hover:bg-slate-50">${icon('settings-2', 'h-3.5 w-3.5')}配置模块细节</button></div><div class="grid gap-3 border-t border-line px-5 py-4 md:grid-cols-2"><div><div class="mb-2 flex items-center justify-between"><span class="text-[11px] font-medium text-slate-600">本页面动作</span><span class="text-[10px] text-slate-400">${level.label}级</span></div><div class="flex flex-wrap gap-1.5">${actionNames.length ? actionNames.map(name => `<span class="flex items-center gap-1 rounded bg-white px-2 py-1.5 text-[10px] text-slate-600 ring-1 ring-inset ring-slate-200"><span class="h-1.5 w-1.5 rounded-full bg-emerald-500"></span>${name}</span>`).join('') : '<span class="text-[10px] text-slate-400">当前页面未开放操作</span>'}</div></div><div><div class="mb-2 flex items-center justify-between"><span class="text-[11px] font-medium text-slate-600">页面说明</span><span class="text-[10px] text-slate-400">菜单只做入口和能力上限</span></div><div class="rounded-md border border-dashed border-slate-200 bg-white px-3 py-2.5 text-[10px] leading-5 text-slate-500">字段权限和数据范围统一从右侧模块策略进入,不会在每个菜单下重复维护。</div></div></div>`;
|
|
document.getElementById('detailScopeBtn').addEventListener('click', () => showToast('已打开模块策略面板(原型)'));
|
|
}
|
|
function renderPreview() {
|
|
const role = roleById(appState.roleId);
|
|
const activeMenus = menus.filter(menu => role.levels[menu.id] !== 'none');
|
|
const activeModules = new Set();
|
|
let actionTotal = 0;
|
|
activeMenus.forEach(menu => { menu.modules.forEach(module => activeModules.add(module.id)); actionTotal += levelByKey(role.levels[menu.id]).actions; });
|
|
document.getElementById('previewMenus').textContent = activeMenus.length;
|
|
document.getElementById('previewModules').textContent = activeModules.size;
|
|
document.getElementById('previewActions').textContent = actionTotal;
|
|
document.getElementById('previewSensitive').textContent = role.fields.filter(field => field[1] !== '可见').length;
|
|
document.getElementById('moduleSummary').innerHTML = [...activeModules].slice(0, 5).map(id => { const module = menus.flatMap(menu => menu.modules).find(module => module.id === id); const sources = menus.filter(menu => role.levels[menu.id] !== 'none' && menu.modules.some(item => item.id === id)).length; return `<div class="flex items-center justify-between gap-2"><span class="flex min-w-0 items-center gap-2"><span class="h-1.5 w-1.5 shrink-0 rounded-full ${sources > 1 ? 'bg-amber-500' : 'bg-tealx'}"></span><span class="truncate text-[11px] text-slate-600">${module.name}</span></span><span class="shrink-0 text-[10px] text-slate-400">${sources > 1 ? `共享 · ${sources}页` : '随页面'}</span></div>`; }).join('') + (activeModules.size > 5 ? `<div class="pt-1 text-[10px] text-brand">还有 ${activeModules.size - 5} 个模块</div>` : '');
|
|
document.getElementById('readScope').textContent = role.scopes[0];
|
|
document.getElementById('writeScope').textContent = role.scopes[1];
|
|
document.getElementById('fieldSummary').innerHTML = role.fields.map(field => `<div class="flex items-center justify-between rounded-md bg-slate-50 px-3 py-2"><span class="text-[11px] text-slate-600">${field[0]}</span><span class="rounded px-1.5 py-1 text-[10px] ${field[1] === '隐藏' ? 'bg-slate-200 text-slate-500' : field[1] === '只读' ? 'bg-amber-50 text-amber-700' : 'bg-emerald-50 text-emerald-700'}">${field[1]}</span></div>`).join('');
|
|
}
|
|
function renderHeader() {
|
|
const role = roleById(appState.roleId);
|
|
document.getElementById('currentRoleName').textContent = role.name;
|
|
document.getElementById('currentRoleDesc').textContent = role.desc;
|
|
document.getElementById('currentRoleMembers').textContent = `${role.members} 名成员`;
|
|
document.getElementById('memberCount').textContent = role.members;
|
|
document.getElementById('configuredCount').textContent = menus.filter(menu => role.levels[menu.id] !== 'none').length;
|
|
document.getElementById('changeCount').textContent = appState.changed;
|
|
document.getElementById('roleCount').textContent = roles.length;
|
|
}
|
|
function render() { renderHeader(); renderRoles(); renderMenus(); renderPreview(); refreshIcons(); }
|
|
function setLevel(menuId, level) {
|
|
const role = roleById(appState.roleId);
|
|
role.levels[menuId] = level;
|
|
appState.selectedMenu = menuId;
|
|
appState.expanded.add(menuId);
|
|
appState.changed += 1;
|
|
render();
|
|
const menu = menuById(menuId);
|
|
if (level === 'custom') showToast(`${menu.name} 已切换为自定义,请检查页面动作`);
|
|
else showToast(`${menu.name}:已设置为${levelByKey(level).label}`, 'success');
|
|
}
|
|
document.getElementById('roleSearch').addEventListener('input', event => { appState.roleSearch = event.target.value; renderRoles(); refreshIcons(); });
|
|
document.getElementById('menuSearch').addEventListener('input', event => { appState.menuSearch = event.target.value; renderMenus(); refreshIcons(); });
|
|
document.getElementById('expandBtn').addEventListener('click', () => { const all = menus.filter(menu => !appState.menuSearch || `${menu.name}${menu.desc}${menu.group}`.toLowerCase().includes(appState.menuSearch.trim().toLowerCase())).map(menu => menu.id); const open = all.every(id => appState.expanded.has(id)); appState.expanded = new Set(open ? [] : all); renderMenus(); refreshIcons(); });
|
|
document.getElementById('saveBtn').addEventListener('click', () => { if (!appState.changed) { showToast('当前没有待保存的变更'); return; } appState.changed = 0; renderHeader(); showToast('岗位权限已保存,模块权限已重新计算', 'success'); });
|
|
document.getElementById('resetBtn').addEventListener('click', () => showToast('已恢复为“费用会计”岗位模板(原型)'));
|
|
document.getElementById('scopeBtn').addEventListener('click', () => showToast('已打开统一数据范围设置(原型)'));
|
|
document.getElementById('previewUserBtn').addEventListener('click', () => showToast('用户预览:张三 · 费用会计'));
|
|
document.getElementById('memberBtn').addEventListener('click', () => showToast('成员列表:12 人继承此岗位'));
|
|
document.getElementById('exceptionBtn').addEventListener('click', () => showToast('用户例外用于少量特殊人员,建议谨慎使用'));
|
|
document.getElementById('addRoleBtn').addEventListener('click', () => showToast('新增岗位流程已打开(原型)'));
|
|
render();
|
|
</script>
|
|
</body>
|
|
</html>
|