Files
OPC/Demo/assets/app.js
T
freedakandDevin 13cad398dc feat: OPC 平台功能增强 v1.4 · 8 项需求变更实施
CHG-006 抢单订单池:OPC 工作台加抢单池(REQ-128/129)
CHG-004 政策落地:OPC 加算力券申领/核销+贷款+租金,运营商加政策跟踪(REQ-124/125/126)
CHG-001 组队模块:新增 team.html 组队协同页(REQ-119/120/121)
CHG-003 活动管理:运营工作台加活动创建/发布/报名管理(REQ-122/123)
CHG-005 OPC 财务分析:OPC 工作台加财务看板(REQ-127)
CHG-008 平台撮合归集:运营工作台加录入入口(REQ-130)
CHG-002 入口跳转:确认已正确(OPC→opc.html)
CHG-007 服务中心定位:明确为运营商服务资源管理

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-14 08:47:29 +08:00

217 lines
9.4 KiB
JavaScript
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.
/**
* OPC 公共服务平台 Demo —— 角色登录态与权限导航
* 静态演示:角色存 localStorage,无真实后端鉴权。
*/
const ROLES = {
operator: { name: '社区运营商', home: 'operator.html' },
opc: { name: 'OPC/一人公司', home: 'opc.html' },
learner: { name: '培训学员', home: 'training.html' },
enterprise: { name: '企业客户', home: 'enterprise.html' },
personal: { name: '个人客户', home: 'enterprise.html' },
trainer: { name: '培训机构/导师', home: 'institution.html' },
service: { name: '服务机构', home: 'service.html' },
gov: { name: '政府主管部门', home: 'government.html' },
auditor: { name: '审计/评估机构', home: 'audit.html' },
finance: { name: '金融/园区/平台方', home: 'policy.html' },
};
/** 导航菜单:roles 为 '*' 表示所有角色可见;group 用于侧边栏分组 */
const MENUS = [
{ page: 'index', label: '门户首页', href: 'index.html', roles: '*', group: '公共' },
{ page: 'showcase', label: '社区大屏', href: 'showcase.html', roles: [], group: '公共' },
{ page: 'operator', label: '运营工作台', href: 'operator.html', roles: ['operator'], group: '运营管理' },
{ page: 'leads', label: '供需对接', href: 'leads.html', roles: ['operator', 'gov'], group: '运营管理' },
{ page: 'profile', label: '画像中心', href: 'profile.html', roles: ['operator', 'gov', 'enterprise'], group: '运营管理' },
{ page: 'opc', label: 'OPC 工作台', href: 'opc.html', roles: ['opc'], group: '我的工作台' },
{ page: 'team', label: '组队协同', href: 'team.html', roles: ['opc', 'operator'], group: '我的工作台' },
{ page: 'enterprise', label: '企业工作台', href: 'enterprise.html', roles: ['enterprise', 'personal'], labelMap: { personal: '个人工作台' }, group: '我的工作台' },
{ page: 'order', label: '订单与协同', href: 'order.html', roles: ['opc', 'enterprise', 'personal', 'operator'], group: '我的工作台' },
{ page: 'market', label: '橙果商城', href: 'market.html', roles: ['opc', 'enterprise', 'personal', 'learner', 'operator'], group: '我的工作台' },
{ page: 'service', label: '企业服务中心', href: 'service.html', roles: ['opc', 'enterprise', 'personal', 'service', 'operator'], group: '服务与政策' },
{ page: 'policy', label: '政策服务', href: 'policy.html', roles: ['opc', 'enterprise', 'personal', 'finance', 'learner', 'operator'], group: '服务与政策' },
{ page: 'training', label: '培训认证', href: 'training.html', roles: ['opc', 'trainer', 'learner', 'operator'], group: '培训与机构' },
{ page: 'institution', label: '机构管理', href: 'institution.html', roles: ['trainer', 'operator'], group: '培训与机构' },
{ page: 'government', label: '政府驾驶舱', href: 'government.html', roles: ['gov', 'auditor', 'operator'], group: '监管与审计' },
{ page: 'audit', label: '审计视图', href: 'audit.html', roles: ['auditor', 'operator'], group: '监管与审计' },
];
function currentRole() {
return localStorage.getItem('opc_role') || null;
}
function currentUser() {
return localStorage.getItem('opc_user') || '';
}
function canAccess(page, role) {
const menu = MENUS.find(m => m.page === page);
if (!menu) return true;
// 公开页面(index/showcase)任何人都可访问
if (['index', 'showcase'].includes(page)) return true;
return menu.roles === '*' || menu.roles.includes(role);
}
function logout() {
localStorage.removeItem('opc_role');
localStorage.removeItem('opc_user');
location.href = 'index.html';
}
function renderNav() {
const role = currentRole();
const page = document.body.dataset.page;
const host = document.getElementById('topnav');
if (!host || page === 'login') return;
// 门户首页和社区大屏公开可访问,不强制登录
const publicPages = ['index', 'showcase'];
if (!role && !publicPages.includes(page)) {
location.href = 'login.html';
return;
}
const user = currentUser();
const visibleMenus = role ? MENUS.filter(m => m.roles === '*' || m.roles.includes(role)) : [];
// 未登录访问大屏:只显示精简顶栏(品牌 + 返回门户),不显示侧边栏
if (!role && page === 'showcase') {
host.innerHTML = `
<a class="brand" href="index.html">OPC<span>公共服务平台</span></a>
<a href="index.html" class="muted" style="margin-left:auto;font-size:13px">← 返回门户</a>
`;
document.body.style.paddingLeft = '0';
return;
}
// 未登录(仅在门户首页):顶栏只显示品牌 + 帮助 + 登录按钮,不显示侧边栏
if (!role) {
host.innerHTML = `
<a class="brand" href="index.html">OPC<span>公共服务平台</span></a>
<a href="req-doc.html" class="muted" style="margin-left:auto;font-size:13px" target="_blank">帮助 / 需求文档</a>
<a href="login.html" class="btn sm">登录</a>
`;
document.body.style.paddingLeft = '0';
return;
}
// 按分组组织菜单
const groups = [];
const groupMap = {};
visibleMenus.forEach(m => {
if (!groupMap[m.group]) { groupMap[m.group] = []; groups.push(m.group); }
groupMap[m.group].push(m);
});
const sidebarLinks = groups.map(g => `
<div class="nav-group">
<div class="nav-group-title">${g}</div>
${groupMap[g].map(m => `<a href="${m.href}" class="${m.page === page ? 'active' : ''}">${(m.labelMap && m.labelMap[role]) || m.label}</a>`).join('')}
</div>`).join('');
// 顶栏:品牌 + 帮助(左)| 角色 + 重置 + 退出 + 菜单按钮(右)
host.innerHTML = `
<a class="brand" href="index.html">OPC<span>公共服务平台</span></a>
<a href="req-doc.html" class="muted" style="font-size:13px" target="_blank">帮助 / 需求文档</a>
<span style="margin-left:auto;display:flex;align-items:center;gap:12px">
<span class="role-badge">${ROLES[role].name}${user ? ' · ' + user : ''}</span>
<a href="javascript:resetDemo()" class="muted" style="font-size:12px" title="清除 localStorage 中的演示数据,恢复 mock.json 初始状态">重置数据</a>
<a href="javascript:logout()" class="muted" style="font-size:12px">退出</a>
</span>
<button class="nav-toggle" id="navToggle" aria-label="切换菜单">☰</button>
`;
// 侧边栏(底部不再放用户信息和退出,已移至顶栏右侧)
let sidebar = document.getElementById('sidebar');
if (!sidebar) {
sidebar = document.createElement('aside');
sidebar.id = 'sidebar';
sidebar.className = 'sidebar';
document.body.appendChild(sidebar);
}
sidebar.innerHTML = `
<div class="sidebar-head">
<a class="brand" href="index.html">OPC<span>公共服务平台</span></a>
<button class="nav-close" id="navClose" aria-label="关闭菜单">×</button>
</div>
<nav class="sidebar-nav">${sidebarLinks}</nav>
`;
// 移动端切换
const toggle = document.getElementById('navToggle');
const close = document.getElementById('navClose');
if (toggle) toggle.onclick = () => { sidebar.classList.add('open'); };
if (close) close.onclick = () => { sidebar.classList.remove('open'); };
if (!canAccess(page, role)) {
const main = document.querySelector('.container');
if (main) {
const tip = document.createElement('div');
tip.className = 'notice';
tip.style.marginBottom = '16px';
tip.textContent = `当前角色「${ROLES[role].name}」无本模块操作权限,以下为演示预览模式。`;
main.prepend(tip);
}
}
}
document.addEventListener('DOMContentLoaded', renderNav);
/* ===== 演示交互组件:Toast / Modal / Confirm ===== */
function toast(msg, type = 'ok') {
let host = document.getElementById('toasts');
if (!host) {
host = document.createElement('div');
host.id = 'toasts';
document.body.appendChild(host);
}
const el = document.createElement('div');
el.className = 'toast ' + type;
el.textContent = msg;
host.appendChild(el);
setTimeout(() => el.remove(), 3200);
}
function openModal({ title, body, actions }) {
const mask = document.createElement('div');
mask.className = 'modal-mask';
const btns = (actions || [{ label: '关闭' }])
.map((a, i) => `<button class="btn ${a.cls || ''}" data-i="${i}">${a.label}</button>`)
.join('');
mask.innerHTML = `
<div class="modal">
<span class="close-x" data-x="1">×</span>
<h3>${title}</h3>
<div class="modal-body">${body}</div>
<div class="modal-foot">${btns}</div>
</div>`;
mask.addEventListener('click', e => {
if (e.target === mask || e.target.dataset.x) mask.remove();
const i = e.target.dataset && e.target.dataset.i;
if (i !== undefined) {
const act = (actions || [{ label: '关闭' }])[+i];
if (act && act.onClick) act.onClick(mask);
else mask.remove();
}
});
document.body.appendChild(mask);
return mask;
}
function confirmDlg(title, msg, okLabel, onOk) {
openModal({
title,
body: `<p>${msg}</p>`,
actions: [
{ label: '取消', cls: 'ghost' },
{ label: okLabel || '确认', onClick: m => { m.remove(); onOk && onOk(); } },
],
});
}
/** 演示用:更新单元格为徽章 */
function setBadge(cell, text, cls) {
cell.innerHTML = `<span class="badge ${cls}">${text}</span>`;
}