Files
OPC/Demo/assets/app.js
T
freedakandDevin f307e877c9 feat: OPC 社区运营公共服务平台 Demo v1.3
一人公司/超级个体产业社区公共服务基础设施 Demo,包含:
- 18 个角色页面(运营商/OPC/企业/个人/学员/培训机构/服务机构/政府/审计/金融)
- 暖橙活力主题 UI(渐变毛玻璃 + 暖色侧栏)
- 五步运营闭环(选方向→育人才→建平台→立SOP→聚产业)
- 华强北硬件供应链接入
- 蜂群式交付 + 产业集聚效应展示
- 需求文档(REQ-001~118 全覆盖)

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

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

216 lines
9.3 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: '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>`;
}