Files
freedakandDevin 63dda2fb4c fix: UI 优化 · 门户无侧边栏 + 政策落地表格紧凑 + 结算/质量用表格布局
- index.html 已登录也不显示侧边栏(公开展示页)
- 政策落地三栏表格精简为 3 列,加 word-break: keep-all 防止中文竖排
- 结算总览和质量与信用从 list 改为表格布局,更紧凑
- 智能体服务菜单移到「服务与政策」分组
- 门户"我要接订单"恢复为 login.html(先选角色)

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

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

231 lines
10 KiB
JavaScript
Raw Permalink 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: 'agent', label: '智能体服务', href: 'agent.html', roles: ['opc', 'enterprise', 'personal', '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;
}
// 门户首页:已登录也不显示侧边栏(公开展示页),顶栏显示品牌 + 帮助 + 角色徽章 + 退出
if (page === 'index') {
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>
<span class="role-badge">${ROLES[role].name}</span>
<button class="btn sm ghost" onclick="resetDemo()">重置数据</button>
<button class="btn sm" onclick="logout()">退出</button>
`;
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>`;
}