Files
OPC/Demo/assets/app.js
T
freedakandDevin 6027898fbe feat: 新增智能体服务订阅功能(REQ-131~134)
企业、个人用户和 OPC 均可订阅运营商提供的智能体服务:
- 智能体服务目录(6 个智能体:电商视觉/AI漫剧/企业智能体/数据标注/硬件供应链/跨境文案)
- 订阅管理(月度/季度/年度套餐、用量跟踪、续费/取消)
- 调用记录与账单(按月计费、算力券抵扣)
- 新增 agent.html 页面,opc/enterprise/personal/operator 角色可见

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

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

231 lines
10 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: '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>`;
}