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>
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
# OPC 公共服务平台 Demo
|
||||
|
||||
静态 HTML 原型,用于需求确认前的产品结构、角色权限与业务流程演示。
|
||||
数据统一来自 `assets/mock.json`,交互产生的变更存 `localStorage`(顶栏「重置数据」可还原)。
|
||||
|
||||
## 启动
|
||||
|
||||
```bash
|
||||
cd Demo
|
||||
python3 -m http.server 8321
|
||||
# 打开 http://localhost:8321/login.html
|
||||
```
|
||||
|
||||
必须通过 HTTP 访问(`fetch` 加载 mock.json),勿用 `file://` 直开。
|
||||
|
||||
## 角色—首页—菜单—功能矩阵
|
||||
|
||||
| 角色 | 登录后首页 | 可见菜单 | 核心功能 |
|
||||
|---|---|---|---|
|
||||
| 社区运营商 | 运营工作台 | 全部 15 个 | 专属工作台:审核队列(机构/课程/成果/采认/投诉)、争议仲裁、项目总览;加全业务入口 |
|
||||
| OPC/一人公司 | OPC 工作台 | 门户/OPC/订单/商城/服务中心/政策/培训/大屏 | 入驻五态、能力档案、L1-L4 认证与评分依据、订单池报价领取、交付质检、收入结算与提现、成果上架销售、购买外包服务、政策申报、学习认证 |
|
||||
| 培训学员 | 培训认证 | 门户/商城/政策/培训/大屏 | 学员档案(培训履历/认证/作品)、一键入驻成为 OPC(REQ-111/112)、浏览橙果商城 |
|
||||
| 企业客户 | 企业工作台 | 门户/企业/订单/商城/画像中心/服务中心/政策/大屏 | 发布需求+AI 诊断、小样签约、验收/争议、开票、成员协作、购买成果授权 |
|
||||
| 个人客户 | 个人工作台 | 门户/个人工作台/订单/商城/服务中心/政策/大屏 | 与企业客户共用 enterprise.html,菜单/标题/副标题按角色适配为"个人工作台";轻量认证通道(REQ-017),无成员管理 |
|
||||
| 培训机构/导师 | 机构管理 | 门户/培训认证/机构管理/大屏 | 资质入驻、课程/认证项目发布与审核、收费结算、证书颁发/吊销/复评、补贴绩效 |
|
||||
| 服务机构 | 服务中心 | 门户/服务中心/大屏 | 资质上架、服务包发布审核、工单受理/办结/转派/催补料、券核销、机构经营数据 |
|
||||
| 政府主管部门 | 政府驾驶舱 | 门户/供需对接/画像中心/驾驶舱/审计视图/大屏 | 指标下钻、里程碑拨付、整改跟踪、政策发布、供需转化监测、画像监管、审计监督 |
|
||||
| 审计/评估机构 | 审计视图 | 门户/驾驶舱/审计视图/大屏 | 授权范围只读:资金台账核对、抽样底稿、函证、异议登记(REQ-093) |
|
||||
| 金融/园区/平台方 | 政策服务 | 门户/政策服务/大屏 | 资源产品管理(券池/信用贷/工位)、核销明细、发布资源、政策匹配 |
|
||||
|
||||
无权限页面以「演示预览模式」只读展示;菜单由 `assets/app.js` 的 `MENUS` 表按角色过滤。
|
||||
「社区大屏」(showcase.html)为公开展览驾驶舱,所有角色可见(REQ-118)。
|
||||
|
||||
## 文件结构
|
||||
|
||||
```
|
||||
Demo/
|
||||
├── login.html 角色登录(localStorage 存角色)
|
||||
├── index.html 统一门户(三类行动入口)
|
||||
├── opc.html OPC 工作台
|
||||
├── enterprise.html 企业/个人客户工作台(按角色适配)
|
||||
├── order.html 订单协同、甘特图、三级质检
|
||||
├── operator.html 运营工作台(审核队列/仲裁/项目总览)
|
||||
├── market.html OPC橙果商城(OPC 上架 / 客户直购授权)
|
||||
├── leads.html 供需对接:线索→需求→订单漏斗
|
||||
├── profile.html 主题画像中心(OPC/企业/订单/学员 + AI 匹配演示)
|
||||
├── showcase.html OPC 社区驾驶舱 · 展览大屏(公开脱敏)
|
||||
├── service.html 企业服务中心(外包+机构超市)
|
||||
├── policy.html 政策服务与资源券
|
||||
├── training.html 培训认证(学员/OPC 双视角)
|
||||
├── institution.html 培训机构管理
|
||||
├── government.html 政府驾驶舱(暗色 Admin)
|
||||
├── audit.html 审计机构视图(只读授权)
|
||||
└── assets/
|
||||
├── mock.json 全部 Mock 数据(统一实体编码,14 个业务域)
|
||||
├── store.js 数据加载 + localStorage 持久化 + 渲染辅助
|
||||
├── app.js 角色登录态、权限导航、弹窗/Toast 组件
|
||||
└── style.css 共享样式(含甘特图/漏斗/大屏/打印)
|
||||
```
|
||||
|
||||
实体编码规范见 `pmdocs/0-req-OPCSP.md` 附 2;功能与需求映射见各页面 `REQ-xxx` 标注(118 条全覆盖)。
|
||||
@@ -0,0 +1,215 @@
|
||||
/**
|
||||
* 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>`;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* Demo 数据层:统一加载 assets/mock.json
|
||||
* 页面定义 window.renderPage(DB) 后由本文件回调渲染;
|
||||
* 交互函数直接修改 DB 并调用对应 renderPage 重渲染。
|
||||
* 交互产生的数据变更通过 localStorage 持久化,跨页面/刷新保留;
|
||||
* mock.json 结构升级时提升 SCHEMA_VER 使旧存档失效。
|
||||
*/
|
||||
|
||||
const SCHEMA_VER = 4;
|
||||
const DB_KEY = 'opc_demo_db';
|
||||
|
||||
let DB = null;
|
||||
|
||||
/** 徽章 */
|
||||
const B = (pair) => pair ? `<span class="badge ${pair[1]}">${pair[0]}</span>` : '—';
|
||||
/** 进度条 */
|
||||
const BAR = (w, cls = '') => `<div class="bar"><i class="${cls}" style="width:${w}%"></i></div>`;
|
||||
/** 步骤条 */
|
||||
const STEPS = (steps) => `<div class="steps">` + steps
|
||||
.map((s, i) => `${i ? '<span class="arrow">→</span>' : ''}<span class="step ${s[1]}">${s[0]}</span>`)
|
||||
.join('') + `</div>`;
|
||||
/** 标签 */
|
||||
const TAG = (t) => `<span class="tag">${t}</span>`;
|
||||
/** 列表行(key-value) */
|
||||
const LI = (l, r) => `<li><span>${l}</span><span>${r}</span></li>`;
|
||||
|
||||
/** 将当前 DB 持久化到 localStorage */
|
||||
function saveDB() {
|
||||
if (!DB) return;
|
||||
try {
|
||||
localStorage.setItem(DB_KEY, JSON.stringify({ v: SCHEMA_VER, data: DB }));
|
||||
} catch (e) { /* 存储满或隐私模式下静默降级 */ }
|
||||
}
|
||||
|
||||
/** 清除演示存档,恢复 mock.json 初始数据 */
|
||||
function resetDemo() {
|
||||
localStorage.removeItem(DB_KEY);
|
||||
location.reload();
|
||||
}
|
||||
|
||||
async function loadMock() {
|
||||
let fresh = {};
|
||||
try {
|
||||
const res = await fetch('assets/mock.json');
|
||||
fresh = await res.json();
|
||||
} catch (e) {
|
||||
console.error('mock.json 加载失败(请通过 http 服务访问,勿用 file:// 直开)', e);
|
||||
}
|
||||
let saved = null;
|
||||
try {
|
||||
const raw = localStorage.getItem(DB_KEY);
|
||||
if (raw) {
|
||||
const pack = JSON.parse(raw);
|
||||
if (pack.v === SCHEMA_VER) saved = pack.data;
|
||||
}
|
||||
} catch (e) { /* 存档损坏时回退到初始数据 */ }
|
||||
// 存档优先,但用 fresh 补齐新增加的业务域
|
||||
DB = saved ? Object.assign({}, fresh, saved) : fresh;
|
||||
window.addEventListener('beforeunload', saveDB);
|
||||
if (typeof window.renderPage === 'function') window.renderPage(DB);
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', loadMock);
|
||||
@@ -0,0 +1,364 @@
|
||||
:root {
|
||||
--primary: #f97316;
|
||||
--primary-dark: #ea580c;
|
||||
--primary-light: #fb923c;
|
||||
--primary-50: #fff7ed;
|
||||
--primary-100: #ffedd5;
|
||||
--primary-200: #fed7aa;
|
||||
--bg: #faf9f7;
|
||||
--bg-gradient: linear-gradient(135deg, #faf9f7 0%, #fff7ed 50%, #faf9f7 100%);
|
||||
--card: rgba(255, 255, 255, 0.82);
|
||||
--card-solid: #ffffff;
|
||||
--border: #f0ebe5;
|
||||
--border-strong: #e8e0d6;
|
||||
--text: #292524;
|
||||
--text-2: #78716c;
|
||||
--text-3: #a8a29e;
|
||||
--success: #059669;
|
||||
--warning: #d97706;
|
||||
--danger: #e11d48;
|
||||
--info: #0284c7;
|
||||
--radius: 0.75rem;
|
||||
--shadow: 0 4px 24px rgba(120, 90, 50, 0.08);
|
||||
--shadow-lg: 0 8px 32px rgba(120, 90, 50, 0.12);
|
||||
--glass-blur: blur(12px);
|
||||
}
|
||||
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
font-family: -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||
background: var(--bg-gradient);
|
||||
background-attachment: fixed;
|
||||
color: var(--text);
|
||||
line-height: 1.6;
|
||||
}
|
||||
a { color: var(--primary); text-decoration: none; }
|
||||
a:hover { color: var(--primary-dark); }
|
||||
|
||||
/* ===== 顶部精简栏 + 侧边导航 ===== */
|
||||
.topnav {
|
||||
position: sticky; top: 0; z-index: 100;
|
||||
height: 52px;
|
||||
background: rgba(255, 255, 255, 0.75);
|
||||
backdrop-filter: var(--glass-blur);
|
||||
-webkit-backdrop-filter: var(--glass-blur);
|
||||
border-bottom: 1px solid var(--border);
|
||||
display: flex; align-items: center;
|
||||
padding: 0 20px; gap: 16px;
|
||||
}
|
||||
.topnav .brand { font-weight: 700; font-size: 15px; color: var(--text); white-space: nowrap; }
|
||||
.topnav .brand span { color: var(--primary); }
|
||||
.topnav .role-badge {
|
||||
font-size: 12px; padding: 4px 10px;
|
||||
border-radius: 999px; background: var(--primary-100); color: var(--primary-dark);
|
||||
border: 1px solid var(--primary-200);
|
||||
}
|
||||
.topnav .muted { color: var(--text-2); text-decoration: none; }
|
||||
.topnav .muted:hover { color: var(--primary); }
|
||||
.topnav .nav-toggle {
|
||||
display: none; border: 1px solid var(--border); background: #fff;
|
||||
border-radius: 8px; padding: 4px 10px; font-size: 16px; cursor: pointer; color: var(--text);
|
||||
}
|
||||
|
||||
/* 侧边栏 */
|
||||
.sidebar {
|
||||
position: fixed; top: 0; left: 0; bottom: 0;
|
||||
width: 220px; z-index: 150;
|
||||
background: #fff8f0; border-right: 1px solid var(--border);
|
||||
display: flex; flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
.sidebar-head {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
padding: 14px 18px; border-bottom: 1px solid var(--border); height: 52px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.sidebar-head .brand { font-weight: 700; font-size: 15px; }
|
||||
.sidebar-head .brand span { color: var(--primary); }
|
||||
.sidebar-head .nav-close {
|
||||
display: none; border: none; background: none; font-size: 22px;
|
||||
cursor: pointer; color: var(--text-2); line-height: 1;
|
||||
}
|
||||
.sidebar-nav { flex: 1; padding: 8px 0; overflow-y: auto; }
|
||||
.nav-group { margin-bottom: 4px; }
|
||||
.nav-group-title {
|
||||
font-size: 11px; font-weight: 600; color: var(--text-3);
|
||||
padding: 10px 18px 4px; text-transform: uppercase; letter-spacing: .5px;
|
||||
}
|
||||
.sidebar-nav a {
|
||||
display: block; padding: 8px 18px; font-size: 13px;
|
||||
color: var(--text-2); border-left: 3px solid transparent;
|
||||
transition: all .15s;
|
||||
}
|
||||
.sidebar-nav a:hover { background: var(--primary-50); color: var(--primary-dark); }
|
||||
.sidebar-nav a.active { background: var(--primary-100); color: var(--primary-dark); border-left-color: var(--primary); font-weight: 600; }
|
||||
|
||||
/* body 留出侧边栏空间(login/showcase 无侧边栏除外) */
|
||||
body { padding-left: 220px; }
|
||||
body[data-page="login"], body.screen { padding-left: 0; }
|
||||
@media (max-width: 900px) {
|
||||
body { padding-left: 0; }
|
||||
.topnav .nav-toggle { display: inline-block; }
|
||||
.sidebar { transform: translateX(-100%); transition: transform .2s; box-shadow: 2px 0 12px rgba(0,0,0,.1); }
|
||||
.sidebar.open { transform: translateX(0); }
|
||||
.sidebar-head .nav-close { display: inline-block; }
|
||||
.topnav .brand { margin-right: auto; }
|
||||
}
|
||||
|
||||
/* ===== 布局 ===== */
|
||||
.container { max-width: 1200px; margin: 0 auto; padding: 24px 16px 48px; }
|
||||
.page-title { font-size: 22px; font-weight: 700; margin-bottom: 4px; }
|
||||
.page-sub { color: var(--text-2); font-size: 13px; margin-bottom: 20px; }
|
||||
.grid { display: grid; gap: 16px; }
|
||||
.grid-2 { grid-template-columns: repeat(2, 1fr); }
|
||||
.grid-3 { grid-template-columns: repeat(3, 1fr); }
|
||||
.grid-4 { grid-template-columns: repeat(4, 1fr); }
|
||||
@media (max-width: 900px) { .grid-3, .grid-4 { grid-template-columns: repeat(2,1fr); } }
|
||||
@media (max-width: 600px) { .grid-2, .grid-3, .grid-4 { grid-template-columns: 1fr; } }
|
||||
|
||||
/* ===== 卡片(毛玻璃) ===== */
|
||||
.card {
|
||||
background: var(--card);
|
||||
backdrop-filter: var(--glass-blur);
|
||||
-webkit-backdrop-filter: var(--glass-blur);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius); padding: 18px;
|
||||
box-shadow: var(--shadow);
|
||||
transition: box-shadow .2s;
|
||||
}
|
||||
.card:hover { box-shadow: var(--shadow-lg); }
|
||||
.card h3 { font-size: 15px; margin-bottom: 10px; display: flex; align-items: center; gap: 8px; }
|
||||
.card .sub { font-size: 12px; color: var(--text-2); margin-bottom: 10px; }
|
||||
.card .num { font-size: 26px; font-weight: 700; color: var(--primary-dark); }
|
||||
.card .num small { font-size: 12px; font-weight: 400; color: var(--text-2); }
|
||||
.section { margin-top: 24px; }
|
||||
.section > h2 { font-size: 17px; margin-bottom: 12px; display: flex; align-items: center; gap: 8px; }
|
||||
.section > h2 .req { font-size: 11px; font-weight: 400; color: var(--text-2); }
|
||||
|
||||
/* ===== 标签/徽章 ===== */
|
||||
.badge {
|
||||
display: inline-block; padding: 2px 8px; border-radius: 999px;
|
||||
font-size: 11px; font-weight: 500;
|
||||
}
|
||||
.badge.ok { background: #d1fae5; color: var(--success); }
|
||||
.badge.warn { background: #fef3c7; color: var(--warning); }
|
||||
.badge.err { background: #ffe4e6; color: var(--danger); }
|
||||
.badge.info { background: #e0f2fe; color: var(--info); }
|
||||
.badge.gray { background: #f5f5f4; color: var(--text-2); }
|
||||
.badge.pri { background: var(--primary-100); color: var(--primary-dark); }
|
||||
.tag {
|
||||
display: inline-block; padding: 2px 8px; margin: 2px 4px 2px 0;
|
||||
border: 1px solid var(--border-strong); border-radius: 6px;
|
||||
font-size: 11px; color: var(--text-2); background: #fff;
|
||||
}
|
||||
|
||||
/* ===== 表格 ===== */
|
||||
table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
||||
th, td { padding: 9px 10px; text-align: left; border-bottom: 1px solid var(--border); }
|
||||
th { color: var(--text-2); font-weight: 600; font-size: 12px; background: var(--primary-50); }
|
||||
tr:hover td { background: var(--primary-50); }
|
||||
|
||||
/* ===== 按钮 ===== */
|
||||
.btn {
|
||||
display: inline-block; padding: 8px 16px; border-radius: 8px;
|
||||
font-size: 13px; font-weight: 500; cursor: pointer; border: none;
|
||||
background: var(--primary); color: #fff;
|
||||
transition: all .15s;
|
||||
}
|
||||
.btn:hover { background: var(--primary-dark); transform: translateY(-1px); }
|
||||
.btn.ghost { background: rgba(255,255,255,0.7); color: var(--text); border: 1px solid var(--border-strong); }
|
||||
.btn.ghost:hover { background: var(--primary-50); border-color: var(--primary-200); color: var(--primary-dark); }
|
||||
.btn.sm { padding: 4px 10px; font-size: 12px; }
|
||||
|
||||
/* ===== 步骤/状态机 ===== */
|
||||
.steps { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; font-size: 12px; }
|
||||
.steps .step {
|
||||
padding: 5px 12px; border-radius: 999px; background: #f5f5f4; color: var(--text-2);
|
||||
}
|
||||
.steps .step.done { background: #d1fae5; color: var(--success); }
|
||||
.steps .step.cur { background: var(--primary); color: #fff; font-weight: 600; }
|
||||
.steps .arrow { color: var(--text-3); }
|
||||
|
||||
/* ===== 进度条 ===== */
|
||||
.bar { height: 8px; background: #f5f5f4; border-radius: 999px; overflow: hidden; }
|
||||
.bar i { display: block; height: 100%; background: var(--primary); border-radius: 999px; }
|
||||
.bar i.ok { background: var(--success); }
|
||||
.bar i.warn { background: var(--warning); }
|
||||
|
||||
/* ===== 门户 Hero ===== */
|
||||
.hero {
|
||||
background: linear-gradient(135deg, #f97316 0%, #ea580c 50%, #c2410c 100%);
|
||||
color: #fff; border-radius: var(--radius); padding: 40px 36px; margin-bottom: 24px;
|
||||
box-shadow: 0 8px 32px rgba(234, 88, 12, 0.2);
|
||||
}
|
||||
.hero h1 { font-size: 28px; margin-bottom: 8px; }
|
||||
.hero p { opacity: .92; font-size: 14px; max-width: 640px; }
|
||||
.hero .btn { background: rgba(255,255,255,0.9); color: var(--primary-dark); }
|
||||
.hero .btn:hover { background: #fff; }
|
||||
.hero .btn.ghost { background: rgba(255,255,255,0.15); color: #fff; border: 1px solid rgba(255,255,255,0.3); }
|
||||
.hero .btn.ghost:hover { background: rgba(255,255,255,0.25); }
|
||||
.entry-cards { display: grid; grid-template-columns: repeat(3,1fr); gap: 16px; margin-top: -20px; }
|
||||
@media (max-width: 800px) { .entry-cards { grid-template-columns: 1fr; } }
|
||||
.entry-card {
|
||||
background: var(--card);
|
||||
backdrop-filter: var(--glass-blur);
|
||||
-webkit-backdrop-filter: var(--glass-blur);
|
||||
border-radius: var(--radius); padding: 22px;
|
||||
border: 1px solid var(--border); box-shadow: var(--shadow-lg);
|
||||
}
|
||||
.entry-card h3 { font-size: 16px; margin-bottom: 6px; }
|
||||
.entry-card p { font-size: 12px; color: var(--text-2); margin-bottom: 12px; }
|
||||
|
||||
/* ===== 政府端暗色顶栏 + 侧边栏 ===== */
|
||||
.gov .topnav { background: #1c1410; border-bottom-color: #3a2a1e; }
|
||||
.gov .topnav .brand { color: #f5f5f4; }
|
||||
.gov .topnav .brand span { color: var(--primary-light); }
|
||||
.gov .topnav .role-badge { background: #422006; color: #fbbf24; border-color: #92400e; }
|
||||
.gov .topnav .muted { color: #a8a29e; }
|
||||
.gov .topnav .muted:hover { color: var(--primary-light); }
|
||||
.gov .sidebar { background: #1c1410; border-right-color: #3a2a1e; }
|
||||
.gov .sidebar-head { border-bottom-color: #3a2a1e; }
|
||||
.gov .sidebar-head .brand { color: #f5f5f4; }
|
||||
.gov .sidebar-head .brand span { color: var(--primary-light); }
|
||||
.gov .nav-group-title { color: #78716c; }
|
||||
.gov .sidebar-nav a { color: #d6d3d1; }
|
||||
.gov .sidebar-nav a:hover { background: #2a1f18; color: #fff; }
|
||||
.gov .sidebar-nav a.active { background: #2a1f18; color: var(--primary-light); border-left-color: var(--primary); }
|
||||
.gov-banner {
|
||||
background: #1c1410; color: #e7e5e4; border-radius: var(--radius);
|
||||
padding: 18px 22px; margin-bottom: 20px; font-size: 13px;
|
||||
display: flex; gap: 10px; align-items: center;
|
||||
}
|
||||
.gov-banner .badge { background: var(--primary); color: #fff; font-weight: 700; }
|
||||
|
||||
/* ===== 其他 ===== */
|
||||
.notice {
|
||||
border-left: 3px solid var(--primary); background: var(--primary-50);
|
||||
padding: 10px 14px; border-radius: 0 8px 8px 0; font-size: 12px; color: var(--primary-dark);
|
||||
}
|
||||
.muted { color: var(--text-2); font-size: 12px; }
|
||||
.list { list-style: none; }
|
||||
.list li { padding: 8px 0; border-bottom: 1px dashed var(--border); font-size: 13px; display: flex; justify-content: space-between; gap: 10px; }
|
||||
.list li:last-child { border-bottom: none; }
|
||||
.flow { font-size: 12px; color: var(--text-2); }
|
||||
.two-col { display: grid; grid-template-columns: 2fr 1fr; gap: 16px; }
|
||||
@media (max-width: 900px) { .two-col { grid-template-columns: 1fr; } }
|
||||
.footer { text-align: center; color: var(--text-3); font-size: 12px; padding: 24px 0; }
|
||||
|
||||
/* ===== 弹窗 ===== */
|
||||
.modal-mask {
|
||||
position: fixed; inset: 0; z-index: 200;
|
||||
background: rgba(28, 20, 16, 0.5);
|
||||
backdrop-filter: blur(4px);
|
||||
display: flex; align-items: center; justify-content: center; padding: 16px;
|
||||
}
|
||||
.modal {
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
backdrop-filter: var(--glass-blur);
|
||||
-webkit-backdrop-filter: var(--glass-blur);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
max-width: 560px; width: 100%; max-height: 85vh; overflow: auto;
|
||||
padding: 20px 22px; box-shadow: 0 20px 50px rgba(0,0,0,.25);
|
||||
}
|
||||
.modal h3 { font-size: 16px; margin-bottom: 12px; }
|
||||
.modal .modal-body { font-size: 13px; }
|
||||
.modal .modal-foot { display: flex; justify-content: flex-end; gap: 8px; margin-top: 16px; }
|
||||
.modal .close-x { float: right; cursor: pointer; color: var(--text-2); font-size: 18px; line-height: 1; }
|
||||
|
||||
/* ===== 表单 ===== */
|
||||
.field { margin-bottom: 12px; }
|
||||
.field label { display: block; font-size: 12px; color: var(--text-2); margin-bottom: 4px; }
|
||||
.field input, .field select, .field textarea {
|
||||
width: 100%; padding: 8px 10px; border: 1px solid var(--border-strong);
|
||||
border-radius: 8px; font-size: 13px; font-family: inherit; background: rgba(255,255,255,0.8);
|
||||
}
|
||||
.field input:focus, .field select:focus, .field textarea:focus {
|
||||
outline: none; border-color: var(--primary); box-shadow: 0 0 0 3px var(--primary-100);
|
||||
}
|
||||
.field textarea { min-height: 72px; resize: vertical; }
|
||||
.checks label { display: flex; gap: 8px; align-items: center; font-size: 13px; padding: 4px 0; }
|
||||
.field-row { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
|
||||
|
||||
/* ===== Toast ===== */
|
||||
#toasts { position: fixed; bottom: 20px; right: 20px; z-index: 300; display: flex; flex-direction: column; gap: 8px; }
|
||||
.toast {
|
||||
background: #1c1410; color: #fff; padding: 10px 16px; border-radius: 8px;
|
||||
font-size: 13px; box-shadow: 0 6px 18px rgba(0,0,0,.2);
|
||||
animation: tin .2s ease-out; max-width: 320px;
|
||||
}
|
||||
.toast.ok { border-left: 3px solid var(--success); }
|
||||
.toast.warn { border-left: 3px solid var(--warning); }
|
||||
.toast.err { border-left: 3px solid var(--danger); }
|
||||
.toast.info { border-left: 3px solid var(--info); }
|
||||
@keyframes tin { from { transform: translateY(8px); opacity: 0; } to { transform: none; opacity: 1; } }
|
||||
|
||||
.btn.danger { background: var(--danger); }
|
||||
.btn.danger:hover { background: #be123c; }
|
||||
.btn.ok { background: var(--success); }
|
||||
.btn.ok:hover { background: #047857; }
|
||||
|
||||
/* ===== 甘特图(REQ-036) ===== */
|
||||
.gantt { font-size: 12px; }
|
||||
.gantt-row { display: grid; grid-template-columns: 180px 1fr; gap: 10px; align-items: center; padding: 5px 0; }
|
||||
.gantt-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--text-2); }
|
||||
.gantt-axis { position: relative; height: 16px; margin-bottom: 2px; }
|
||||
.gantt-axis span { position: absolute; transform: translateX(-50%); font-size: 10px; color: var(--text-2); }
|
||||
.gantt-track {
|
||||
position: relative; height: 24px; border-radius: 4px;
|
||||
background: repeating-linear-gradient(90deg, #f5f5f4 0 1px, transparent 1px calc(100% / 20));
|
||||
}
|
||||
.gantt-bar {
|
||||
position: absolute; top: 4px; height: 16px; border-radius: 8px;
|
||||
background: var(--primary); color: #fff; font-size: 10px; line-height: 16px;
|
||||
padding: 0 8px; white-space: nowrap; overflow: hidden;
|
||||
}
|
||||
.gantt-bar.ok { background: var(--success); }
|
||||
.gantt-bar.warn { background: var(--warning); }
|
||||
.gantt-bar.err { background: var(--danger); }
|
||||
.gantt-today { position: absolute; top: -2px; bottom: -2px; width: 2px; background: var(--danger); opacity: .6; }
|
||||
|
||||
/* ===== 转化漏斗(REQ-097) ===== */
|
||||
.funnel-row { display: flex; align-items: center; gap: 12px; padding: 6px 0; font-size: 13px; }
|
||||
.funnel-stage { width: 90px; color: var(--text-2); flex-shrink: 0; }
|
||||
.funnel-bar {
|
||||
height: 28px; border-radius: 6px; background: var(--primary); color: #fff;
|
||||
display: flex; align-items: center; padding: 0 10px; font-size: 12px; min-width: 60px;
|
||||
}
|
||||
.funnel-conv { color: var(--text-2); font-size: 11px; margin-left: auto; }
|
||||
|
||||
/* ===== 展览大屏(REQ-118 · 暖橙暗色) ===== */
|
||||
.screen { background: #1a1410; color: #e7e5e4; }
|
||||
.screen .sidebar, .screen .topnav { display: none !important; }
|
||||
.screen { padding-left: 0 !important; }
|
||||
.screen .container { max-width: 1400px; }
|
||||
.screen .card {
|
||||
background: rgba(42, 31, 24, 0.6);
|
||||
backdrop-filter: var(--glass-blur);
|
||||
-webkit-backdrop-filter: var(--glass-blur);
|
||||
border-color: #3a2a1e; color: #e7e5e4;
|
||||
}
|
||||
.screen .card h3 { color: #a8a29e; }
|
||||
.screen .num { color: var(--primary-light); font-size: 32px; }
|
||||
.screen .muted, .screen .page-sub { color: #78716c; }
|
||||
.screen table th { background: #2a1f18; color: #a8a29e; }
|
||||
.screen td { border-bottom-color: #3a2a1e; }
|
||||
.screen tr:hover td { background: #2a1f18; }
|
||||
.screen .footer { color: #57534e; }
|
||||
.screen .bar { background: #3a2a1e; }
|
||||
.screen .bar i { background: var(--primary-light); }
|
||||
.screen-hero { text-align: center; padding: 20px 0 8px; }
|
||||
.screen-hero h1 { font-size: 26px; letter-spacing: 4px; color: var(--primary-light); }
|
||||
.screen-hero p { color: #78716c; font-size: 13px; }
|
||||
.ticker-wrap { overflow: hidden; background: #2a1f18; border: 1px solid #3a2a1e; border-radius: var(--radius); padding: 10px 0; }
|
||||
.ticker { display: flex; gap: 48px; white-space: nowrap; animation: tick 24s linear infinite; font-size: 13px; color: var(--primary-light); }
|
||||
@keyframes tick { from { transform: translateX(0); } to { transform: translateX(-50%); } }
|
||||
.screen .funnel-bar { background: var(--primary); }
|
||||
.screen .badge.pri { background: rgba(249, 115, 22, 0.2); color: var(--primary-light); }
|
||||
|
||||
/* ===== 打印(政府报告) ===== */
|
||||
@media print {
|
||||
.topnav, .sidebar, .btn, .footer, #toasts, .no-print { display: none !important; }
|
||||
body { padding-left: 0 !important; background: #fff; }
|
||||
.card { background: #fff; box-shadow: none; border: 1px solid #999; break-inside: avoid; backdrop-filter: none; }
|
||||
}
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>审计机构视图 · OPC 公共服务平台 Demo</title>
|
||||
<link rel="stylesheet" href="assets/style.css">
|
||||
</head>
|
||||
<body data-page="audit">
|
||||
<header class="topnav" id="topnav"></header>
|
||||
|
||||
<div class="container">
|
||||
<div class="notice" style="margin-bottom:16px">
|
||||
REQ-093:本视图为审计/评估机构授权只读视图,仅可查看委托范围内的绩效与资金台账,机构与个人明细未授权不可见。
|
||||
</div>
|
||||
|
||||
<div class="page-title">审计工作台</div>
|
||||
<div class="page-sub" id="au-sub"></div>
|
||||
|
||||
<!-- 授权范围 -->
|
||||
<div class="card">
|
||||
<h3>授权范围 <span class="req">REQ-093 · 按委托书限定</span></h3>
|
||||
<table id="au-scope"></table>
|
||||
</div>
|
||||
|
||||
<!-- 资金台账核对 -->
|
||||
<div class="section">
|
||||
<h2>资金台账核对 <span class="req">REQ-090/093</span></h2>
|
||||
<div class="card"><table id="au-ledger"></table></div>
|
||||
</div>
|
||||
|
||||
<!-- 抽样底稿 -->
|
||||
<div class="section">
|
||||
<div class="two-col">
|
||||
<div class="card">
|
||||
<h3>抽样核查底稿 <span class="req">SMP_ 统一编号</span></h3>
|
||||
<table id="au-smp"></table>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>审计动作</h3>
|
||||
<ul class="list" id="au-actions">
|
||||
<li><span>导出审计底稿包</span><button class="btn sm" onclick="toast('底稿包导出中:含台账快照、抽样记录、核对结论(演示)')">导出</button></li>
|
||||
<li><span>向运营商发起函证</span><button class="btn sm ghost" onclick="confirmReq()">申请</button></li>
|
||||
<li><span>登记核查异议</span><button class="btn sm ghost" onclick="objection()">登记</button></li>
|
||||
<li><span>查看平台操作日志(授权段)</span><button class="btn sm ghost" onclick="toast('已调取 09-01~09-11 政府侧操作日志 3 条、拨付留痕 2 条(演示)')">调取</button></li>
|
||||
</ul>
|
||||
<p class="muted" style="margin-top:10px">所有调阅与导出动作本身亦留痕,接受监管复核。</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="footer">OPC 公共服务平台 Demo · 审计机构视图</div>
|
||||
<script src="assets/app.js"></script>
|
||||
<script src="assets/store.js"></script>
|
||||
<script>
|
||||
function renderPage(DB) {
|
||||
const A = DB.audit;
|
||||
const role = currentRole();
|
||||
const isOperator = role === 'operator';
|
||||
document.getElementById('au-sub').textContent = isOperator
|
||||
? `${A.org} · ${A.engagement} · 运营商协调视角(配合审计核查与函证回复) · 数据截至 2026-09-11`
|
||||
: `${A.org} · ${A.engagement} · 数据截至 2026-09-11`;
|
||||
|
||||
// 运营商协调视角:隐藏审计专属操作(发起函证、登记异议、导出底稿),只保留查看日志
|
||||
if (isOperator) {
|
||||
document.getElementById('au-actions').innerHTML = `
|
||||
<li><span>查看平台操作日志(授权段)</span><button class="btn sm ghost" onclick="toast('已调取 09-01~09-11 政府侧操作日志 3 条、拨付留痕 2 条(演示)')">调取</button></li>
|
||||
<li><span>回复审计函证</span><button class="btn sm ghost" onclick="toast('暂无待回复函证(演示)')">查看</button></li>
|
||||
`;
|
||||
}
|
||||
|
||||
document.getElementById('au-scope').innerHTML =
|
||||
`<tr><th>数据域</th><th>范围</th><th>权限</th></tr>` +
|
||||
A.scope.map(s => `<tr><td>${s[0]}</td><td>${s[1]}</td><td>${s[2] === '只读' ? '<span class="badge ok">只读</span>' : '<span class="badge gray">未授权</span>'}</td></tr>`).join('');
|
||||
|
||||
document.getElementById('au-ledger').innerHTML =
|
||||
`<tr><th>台账编码</th><th>事项</th><th>金额</th><th>核对凭证</th><th>状态</th></tr>` +
|
||||
A.ledgers.map(l => `<tr><td>${l.code}</td><td>${l.item}</td><td>${l.amount}</td><td>${l.evidence}</td><td>${B(l.status)}</td></tr>`).join('');
|
||||
|
||||
document.getElementById('au-smp').innerHTML =
|
||||
`<tr><th>底稿号</th><th>核查对象</th><th>核查点</th><th>结论</th></tr>` +
|
||||
A.samplings.map((s, i) => `<tr><td>${s.code}</td><td>${s.target}</td><td>${s.check}</td><td>${B(s.result)}${
|
||||
s.result[0].includes('待补') ? ` <button class="btn sm ghost" onclick="supp(${i})">催补</button>` : ''
|
||||
}</td></tr>`).join('');
|
||||
}
|
||||
|
||||
function supp(i) {
|
||||
const s = DB.audit.samplings[i];
|
||||
confirmDlg('催补凭证', `就 ${s.code}(${s.target})向运营商发起补充凭证函证?`, '发送函证', () => {
|
||||
s.result = ['函证已发出', 'info'];
|
||||
renderPage(DB);
|
||||
toast('函证已发出并留痕,待运营商回复');
|
||||
});
|
||||
}
|
||||
function confirmReq() {
|
||||
openModal({
|
||||
title: '函证申请',
|
||||
body: `<div class="field"><label>函证事项</label><select><option>拨付凭证真实性</option><option>订单验收记录</option><option>券核销与订单关联</option></select></div>
|
||||
<div class="field"><label>说明</label><textarea placeholder="函证内容与回复期限"></textarea></div>`,
|
||||
actions: [
|
||||
{ label: '取消', cls: 'ghost' },
|
||||
{ label: '发送', onClick: m => { m.remove(); toast('函证申请已提交,运营商需在 5 个工作日内回复'); } },
|
||||
],
|
||||
});
|
||||
}
|
||||
function objection() {
|
||||
openModal({
|
||||
title: '登记核查异议',
|
||||
body: `<div class="field"><label>关联台账/底稿</label><input placeholder="如 FUND_0003 / SMP_20260901"></div>
|
||||
<div class="field"><label>异议内容</label><textarea placeholder="差异描述与依据"></textarea></div>`,
|
||||
actions: [
|
||||
{ label: '取消', cls: 'ghost' },
|
||||
{ label: '登记', cls: 'danger', onClick: m => { m.remove(); toast('异议已登记,进入三方核对流程', 'warn'); } },
|
||||
],
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,259 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>企业工作台 · OPC 公共服务平台 Demo</title>
|
||||
<link rel="stylesheet" href="assets/style.css">
|
||||
</head>
|
||||
<body data-page="enterprise">
|
||||
<header class="topnav" id="topnav"></header>
|
||||
|
||||
<div class="container">
|
||||
<div class="page-title" id="ent-title">企业工作台</div>
|
||||
<div class="page-sub" id="ent-sub"></div>
|
||||
|
||||
<!-- REQ-027/028 发布需求 + AI 需求诊断 -->
|
||||
<div class="card">
|
||||
<h3>发布需求 <span class="req">REQ-027/028</span></h3>
|
||||
<div class="grid grid-3" style="margin-bottom:12px">
|
||||
<div>
|
||||
<div class="field"><label>需求描述</label><textarea id="req-desc">Q4 上新 20 个 SKU,需要主图+详情页+A/B 广告素材,希望两周内交付,需要开发票。</textarea></div>
|
||||
<div class="field-row">
|
||||
<div class="field"><label>行业</label><select id="req-ind"><option>跨境电商</option><option>AI 漫剧/数字内容</option><option>企业智能体</option><option>数据标注</option><option>华强北硬件供应链</option><option>电池回收</option></select></div>
|
||||
<div class="field"><label>期望周期</label><select id="req-cycle"><option>7 天</option><option selected>14 天</option><option>30 天</option></select></div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p class="muted">AI 需求诊断(辅助,不替代运营商判断 REQ-028)</p>
|
||||
<div class="card" id="ai-diag" style="background:#eef2ff;padding:12px">
|
||||
建议拆为:<b>新品视觉包(询价订单)</b><br>
|
||||
· 主图 20 SKU × 6 张 · 详情页 ×20 套<br>
|
||||
· A/B 素材 ×60 张 · 预估 ¥38,000~52,000<br>
|
||||
· 建议验收标准:平台规范合规率 ≥99%
|
||||
</div>
|
||||
<button class="btn ghost sm" style="margin-top:8px" onclick="runDiag()">重新诊断</button>
|
||||
</div>
|
||||
<div>
|
||||
<div class="field"><label>数据等级</label><select id="req-dl"><option>L1 公开</option><option>L2 内部</option><option selected>L3 敏感</option><option>L4 高敏感</option></select></div>
|
||||
<div class="checks">
|
||||
<label><input type="checkbox" id="req-inv" checked> 需要发票</label>
|
||||
<label><input type="checkbox" id="req-sample" checked> 接受小样试做(REQ-032)</label>
|
||||
<label><input type="checkbox" id="req-ms"> 分阶段里程碑验收(REQ-047)</label>
|
||||
</div>
|
||||
<button class="btn" style="margin-top:8px" onclick="submitReq()">提交并进入询价</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- REQ-029/030 五类订单与状态机 -->
|
||||
<div class="section">
|
||||
<h2>我的订单 <span class="req">REQ-029/030 · 五类订单</span></h2>
|
||||
<div class="card">
|
||||
<table id="order-tbl"></table>
|
||||
<p class="muted" style="margin-top:10px">状态机:草稿 → 待审核 → 招募中 → 小样中 → 待签约 → 进行中 → 待验收 → 已完成 → 已结算 → 复购/归档</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- REQ-032 小样 + REQ-040/060 验收与争议 -->
|
||||
<div class="section">
|
||||
<div class="two-col">
|
||||
<div class="card">
|
||||
<h3>小样试做评审 <span class="req">REQ-032</span></h3>
|
||||
<table id="sample-tbl"></table>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>待验收与复购 <span class="req">REQ-040/045</span></h3>
|
||||
<ul class="list">
|
||||
<li><span>旗舰店视觉升级 · 里程碑 2</span><span id="accept-slot"><button class="btn sm" onclick="accept()">去验收</button></span></li>
|
||||
<li><span>近 90 天复购</span><span class="badge ok">第 3 单</span></li>
|
||||
<li><span>争议记录</span><span><span class="badge ok">0 起</span> <button class="btn sm ghost" onclick="dispute()">发起争议</button></span></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- REQ-041 双层合同 -->
|
||||
<div class="section">
|
||||
<div class="two-col">
|
||||
<div class="card">
|
||||
<h3>合同与结算 <span class="req">REQ-041~043 · 双层合同、分账核算</span></h3>
|
||||
<table id="contract-tbl"></table>
|
||||
<p class="muted" style="margin-top:8px">托管结算经第三方支付通道完成(REQ-042);订单保证金、延期预警、替补与争议机制保护交易双方(REQ-046)。</p>
|
||||
</div>
|
||||
<div class="card" id="member-card">
|
||||
<h3>成员与协作 <span class="req">REQ-014</span></h3>
|
||||
<ul class="list">
|
||||
<li><span>王采购 · 管理员</span><span class="badge ok">全部权限</span></li>
|
||||
<li><span>李运营 · 项目成员</span><span class="badge info">仅本项目</span></li>
|
||||
<li><span>外部协作人(设计院)</span><span class="badge gray">限期授权</span></li>
|
||||
</ul>
|
||||
<button class="btn sm" style="margin-top:10px" onclick="invite()">+ 邀请协作人</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="footer">OPC 公共服务平台 Demo · 企业工作台</div>
|
||||
<script src="assets/app.js"></script>
|
||||
<script src="assets/store.js"></script>
|
||||
<script>
|
||||
let orderSeq = 13;
|
||||
|
||||
function renderPage(DB) {
|
||||
const E = DB.enterprise;
|
||||
const role = currentRole();
|
||||
const isPersonal = role === 'personal';
|
||||
const isOperator = role === 'operator';
|
||||
if (isPersonal) {
|
||||
document.getElementById('ent-title').textContent = '个人工作台';
|
||||
document.title = '个人工作台 · OPC 公共服务平台 Demo';
|
||||
}
|
||||
document.getElementById('ent-sub').textContent = isPersonal
|
||||
? 'PER_00012 · 个人客户(发单方)· 轻量认证通道:实名+支付验证(REQ-017)'
|
||||
: isOperator
|
||||
? E.code + ' ' + E.name + ' · 运营商管理视角(查看企业客户订单与协作状态)'
|
||||
: E.code + ' ' + E.name + ' · 企业客户(发单方)';
|
||||
|
||||
// 个人发单方无企业成员管理需求,替换为轻量认证状态卡
|
||||
if (isPersonal) {
|
||||
document.getElementById('member-card').innerHTML = `
|
||||
<h3>个人认证 <span class="req">REQ-017</span></h3>
|
||||
<ul class="list">
|
||||
<li><span>实名核验</span><span class="badge ok">已通过</span></li>
|
||||
<li><span>支付验证</span><span class="badge ok">已绑定</span></li>
|
||||
<li><span>升级企业认证</span><button class="btn sm ghost" onclick="toast('企业认证通道:补充营业执照与统一社会信用代码核验')">升级</button></li>
|
||||
</ul>`;
|
||||
}
|
||||
|
||||
const acts = {
|
||||
progress: () => `<a class="btn sm" href="order.html">进度</a>`,
|
||||
collab: () => `<a class="btn sm" href="order.html">协同</a>`,
|
||||
rate: () => `<button class="btn sm ghost" onclick="rate()">查看评价</button>`,
|
||||
acceptDoc: () => `<button class="btn sm ghost" onclick="toast('验收单 PDF 已生成(演示):ACPT_2026060201.pdf')">验收单</button>`,
|
||||
};
|
||||
document.getElementById('order-tbl').innerHTML =
|
||||
`<tr><th>订单号</th><th>类型</th><th>内容</th><th>金额</th><th>状态</th><th>承接方</th><th>操作</th></tr>` +
|
||||
E.orders.map(o => `<tr><td>${o.id}</td><td>${B(o.type)}</td><td>${o.items}</td><td>${o.amount}</td><td>${B(o.status)}</td><td>${o.party}</td><td>${acts[o.action]()}</td></tr>`).join('');
|
||||
|
||||
document.getElementById('sample-tbl').innerHTML =
|
||||
`<tr><th>候选 OPC</th><th>小样评分</th><th>结果</th><th>操作</th></tr>` +
|
||||
E.samples.map(s => {
|
||||
const act = s.action === 'sign' ? `<button class="btn sm" onclick="sign(this,'${s.opc}')">签约</button>`
|
||||
: s.action === 'signAlt' ? `<button class="btn sm ghost" onclick="sign(this,'${s.opc}')">改选签约</button>` : '—';
|
||||
return `<tr><td>${s.opc}</td><td>${s.score}</td><td>${B(s.result)}</td><td>${act}</td></tr>`;
|
||||
}).join('');
|
||||
|
||||
document.getElementById('contract-tbl').innerHTML =
|
||||
`<tr><th>合同</th><th>层级</th><th>签署</th><th>结算节点</th><th>发票</th></tr>` +
|
||||
E.contracts.map(c => `<tr><td>${c.name}</td><td>${c.layer}</td><td>${B(c.sign)}</td><td>${c.node}</td><td>${
|
||||
c.invoice === 'pending' ? `<button class="btn sm" onclick="invoice(this)">申请开票</button>`
|
||||
: `<span class="badge ok">已开票</span> <button class="btn sm ghost" onclick="toast('发票 INV_2026072001.pdf 已下载(演示)')">下载</button>`
|
||||
}</td></tr>`).join('');
|
||||
}
|
||||
|
||||
let diagBusy = false;
|
||||
function runDiag() {
|
||||
if (diagBusy) return;
|
||||
diagBusy = true;
|
||||
const box = document.getElementById('ai-diag');
|
||||
box.innerHTML = 'AI 正在分析需求描述…';
|
||||
setTimeout(() => {
|
||||
box.innerHTML = '建议拆为:<b>新品视觉包(询价订单)</b><br>· 主图 20 SKU × 6 张 · 详情页 ×20 套<br>· A/B 素材 ×60 张 · 预估 ¥38,000~52,000<br>· 建议验收标准:平台规范合规率 ≥99%';
|
||||
toast('AI 诊断完成,建议仅供参考,最终以运营商诊断为准');
|
||||
diagBusy = false;
|
||||
}, 900);
|
||||
}
|
||||
function submitReq() {
|
||||
const desc = document.getElementById('req-desc').value.trim();
|
||||
if (!desc) { toast('请填写需求描述', 'warn'); return; }
|
||||
const dl = document.getElementById('req-dl').value;
|
||||
const ms = document.getElementById('req-ms').checked;
|
||||
const sample = document.getElementById('req-sample').checked;
|
||||
openModal({
|
||||
title: '确认提交需求',
|
||||
body: `<table>
|
||||
<tr><th>建议类型</th><td>询价订单(REQ-029)</td></tr>
|
||||
<tr><th>数据等级</th><td>${dl}</td></tr>
|
||||
<tr><th>里程碑验收</th><td>${ms ? '启用分阶段验收' : '一次性验收'}</td></tr>
|
||||
<tr><th>小样试做</th><td>${sample ? '接受' : '不接受'}</td></tr>
|
||||
<tr><th>幂等提交</th><td>Idempotency-Key 已生成,防重复下单(REQ-048)</td></tr>
|
||||
</table>`,
|
||||
actions: [
|
||||
{ label: '再想想', cls: 'ghost' },
|
||||
{ label: '确认提交', onClick: m => {
|
||||
m.remove();
|
||||
const id = 'ORDER_202609' + String(orderSeq).padStart(2, '0') + '01';
|
||||
DB.enterprise.orders.push({ id, type: ['询价订单', 'pri'], items: '新发布需求(按描述生成)', amount: '待报价', status: ['招募中', 'info'], party: '运营商匹配中', action: 'progress' });
|
||||
renderPage(DB);
|
||||
toast(`需求已提交:${id} 进入招募中,运营商开始匹配`);
|
||||
} },
|
||||
],
|
||||
});
|
||||
}
|
||||
function sign(btn, name) {
|
||||
confirmDlg('小样签约确认', `小样评审通过,确认与 ${name} 进入签约流程?将生成双层合同(企业↔运营商、运营商↔OPC)。`, '确认签约', () => {
|
||||
btn.outerHTML = '<span class="badge ok">待签约 · 合同生成中</span>';
|
||||
toast('已发起签约,电子签合同将推送双方签署(REQ-041)');
|
||||
});
|
||||
}
|
||||
function accept() {
|
||||
openModal({
|
||||
title: '验收 · 旗舰店视觉升级 里程碑 2',
|
||||
body: `
|
||||
<div class="field"><label>验收依据</label><input value="合同验收标准:平台规范合规率 ≥99%,交付物 8 项齐全" readonly></div>
|
||||
<div class="checks">
|
||||
<label><input type="radio" name="ac" value="pass" checked> 验收通过</label>
|
||||
<label><input type="radio" name="ac" value="rework"> 退回返工</label>
|
||||
<label><input type="radio" name="ac" value="dispute"> 存在争议(转运营商仲裁 REQ-060)</label>
|
||||
</div>
|
||||
<div class="field"><label>验收意见</label><textarea placeholder="结论、意见将归档可追溯(REQ-040)"></textarea></div>`,
|
||||
actions: [
|
||||
{ label: '取消', cls: 'ghost' },
|
||||
{ label: '提交验收结论', onClick: m => {
|
||||
const v = document.querySelector('input[name=ac]:checked').value;
|
||||
m.remove();
|
||||
if (v === 'pass') { document.getElementById('accept-slot').innerHTML = '<span class="badge ok">里程碑 2 已验收</span>'; toast('验收通过,进入里程碑 3;评价入口已开放'); }
|
||||
else if (v === 'rework') { toast('已退回返工,责任归因流程启动(REQ-057)', 'warn'); }
|
||||
else { toast('已转入争议处理流程,由运营商仲裁(REQ-060)', 'warn'); }
|
||||
} },
|
||||
],
|
||||
});
|
||||
}
|
||||
function dispute() {
|
||||
openModal({
|
||||
title: '发起争议',
|
||||
body: `<div class="field"><label>争议事项</label><textarea placeholder="描述争议内容与诉求"></textarea></div>
|
||||
<p class="muted">争议由运营商仲裁并记录结论(REQ-060),期间结算节点冻结。</p>`,
|
||||
actions: [
|
||||
{ label: '取消', cls: 'ghost' },
|
||||
{ label: '提交争议', cls: 'danger', onClick: m => { m.remove(); toast('争议单 DSP_20260912 已受理', 'warn'); } },
|
||||
],
|
||||
});
|
||||
}
|
||||
function invoice(btn) {
|
||||
confirmDlg('申请开票', '按里程碑 2 结算金额 ¥40,000 开具增值税普通发票?', '确认申请', () => {
|
||||
btn.outerHTML = '<span class="badge info">开票中</span>';
|
||||
toast('开票申请已提交财务(INV_2026091201),预计 1-3 个工作日');
|
||||
});
|
||||
}
|
||||
function invite() {
|
||||
openModal({
|
||||
title: '邀请协作人(REQ-014)',
|
||||
body: `<div class="field"><label>手机号/邮箱</label><input placeholder="协作人账号"></div>
|
||||
<div class="field-row">
|
||||
<div class="field"><label>角色</label><select><option>项目成员</option><option>外部协作人</option><option>只读查看</option></select></div>
|
||||
<div class="field"><label>授权范围</label><select><option>全部订单</option><option>仅 ORDER_2026082201</option></select></div>
|
||||
</div>`,
|
||||
actions: [
|
||||
{ label: '取消', cls: 'ghost' },
|
||||
{ label: '发送邀请', onClick: m => { m.remove(); toast('邀请已发送,对方确认后按授权范围生效'); } },
|
||||
],
|
||||
});
|
||||
}
|
||||
function rate() {
|
||||
openModal({ title: '我的评价 · ORDER_2026071501', body: '<p>★ 5.0 · "素材质量稳定,修改响应快,已列入长期供应商。"</p><p class="muted">评价已计入对方能力档案与信用记录(REQ-044)。</p>' });
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,176 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>政府驾驶舱 · OPC 公共服务平台 Demo</title>
|
||||
<link rel="stylesheet" href="assets/style.css">
|
||||
</head>
|
||||
<body data-page="government" class="gov">
|
||||
<header class="topnav" id="topnav"></header>
|
||||
|
||||
<div class="container">
|
||||
<div class="gov-banner">
|
||||
<span class="badge">ADMIN</span>
|
||||
<span id="gov-banner-text">政府主管部门视图 · 仅展示经授权、脱敏、汇总后的监管数据(REQ-087)</span>
|
||||
</div>
|
||||
|
||||
<div class="page-title">绩效驾驶舱
|
||||
<span style="float:right" class="no-print" id="gov-actions">
|
||||
<button class="btn sm ghost" onclick="publishPolicy()">发布政策</button>
|
||||
<button class="btn sm" onclick="window.print()">导出季度报告</button>
|
||||
</span>
|
||||
</div>
|
||||
<div class="page-sub" id="gov-sub">月度监测 · 季度评价 · 年度审计(REQ-091)· 数据截至 2026-09-11 · 点击指标卡可下钻(REQ-053)</div>
|
||||
|
||||
<!-- REQ-088 核心指标 + §8.2 年度目标对比 -->
|
||||
<div class="grid grid-4" id="gov-core"></div>
|
||||
|
||||
<!-- 就业 / 示范 / 培训 -->
|
||||
<div class="section grid grid-4" id="gov-secondary"></div>
|
||||
|
||||
<!-- REQ-090 财政资金 + REQ-076 资源券绩效 -->
|
||||
<div class="section">
|
||||
<div class="two-col">
|
||||
<div class="card">
|
||||
<h3>财政资金专账 <span class="req">REQ-090 · 里程碑拨付</span></h3>
|
||||
<table id="fund-tbl"></table>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>资源券绩效 <span class="req">REQ-076</span></h3>
|
||||
<ul class="list" id="coupon-list"></ul>
|
||||
<p class="muted" style="margin-top:8px">质量与交付数据由订单质检脱敏汇总同步(REQ-058);审计机构经授权可查台账(<a href="audit.html">审计视图</a>,REQ-093)。</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- REQ-109 财政补贴培训绩效 -->
|
||||
<div class="section">
|
||||
<div class="card">
|
||||
<h3>财政补贴培训项目绩效 <span class="req">REQ-109/110</span></h3>
|
||||
<table id="subsidy-tbl"></table>
|
||||
<p class="muted" style="margin-top:8px">收费与补贴并存项目已区分资金来源,无重复申报预警(REQ-110)。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- REQ-092 预警 + REQ-094 操作日志 -->
|
||||
<div class="section">
|
||||
<div class="two-col">
|
||||
<div class="card">
|
||||
<h3>风险与整改 <span class="req">REQ-092</span></h3>
|
||||
<ul class="list" id="risk-list"></ul>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>政府侧操作日志 <span class="req">REQ-094</span></h3>
|
||||
<ul class="list" id="log-list"></ul>
|
||||
<button class="btn sm ghost" style="margin-top:8px" onclick="toast('完整操作日志导出中…(演示)')">导出日志</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="footer">OPC 公共服务平台 Demo · 政府驾驶舱 · 数据脱敏汇总</div>
|
||||
<script src="assets/app.js"></script>
|
||||
<script src="assets/store.js"></script>
|
||||
<script>
|
||||
function renderPage(DB) {
|
||||
const G = DB.gov;
|
||||
const role = currentRole();
|
||||
const isAuditor = role === 'auditor';
|
||||
const isOperator = role === 'operator';
|
||||
|
||||
// auditor 和 operator 都是只读:隐藏发布政策按钮,保留导出
|
||||
if (isAuditor || isOperator) {
|
||||
document.getElementById('gov-actions').innerHTML = '<button class="btn sm" onclick="window.print()">导出季度报告</button>';
|
||||
}
|
||||
if (isAuditor) {
|
||||
document.getElementById('gov-banner-text').textContent = '审计机构授权只读视图 · 仅可查看经授权、脱敏、汇总后的监管数据(REQ-093)';
|
||||
document.getElementById('gov-sub').textContent = '授权范围内核查绩效与资金台账 · 数据截至 2026-09-11 · 点击指标卡可下钻(REQ-053)';
|
||||
} else if (isOperator) {
|
||||
document.getElementById('gov-banner-text').textContent = '运营商管理视图 · 全量监管数据汇总(REQ-087)· 政策发布与拨付由政府主管部门执行';
|
||||
document.getElementById('gov-sub').textContent = '运营商视角 · 月度监测 · 季度评价 · 年度审计(REQ-091)· 数据截至 2026-09-11 · 点击指标卡可下钻(REQ-053)';
|
||||
}
|
||||
|
||||
document.getElementById('gov-core').innerHTML = G.core.map(c => `
|
||||
<div class="card"${c.drill ? ` style="cursor:pointer" onclick="drill('${c.label}','${c.drill}')"` : ''}>
|
||||
<h3>${c.label}</h3><div class="num">${c.num}</div>
|
||||
${c.pct ? BAR(c.pct) : `<p class="muted">${c.sub || ''} ${c.subBadge ? B(c.subBadge) : ''}</p>`}
|
||||
</div>`).join('');
|
||||
|
||||
document.getElementById('gov-secondary').innerHTML = G.secondary.map(c =>
|
||||
`<div class="card"><h3>${c.label}</h3><div class="num">${c.num}</div>${c.sub ? `<p class="muted">${c.sub}</p>` : ''}</div>`).join('');
|
||||
|
||||
renderFunding();
|
||||
|
||||
document.getElementById('coupon-list').innerHTML = G.coupon.map(c =>
|
||||
LI(c[0], Array.isArray(c[1]) ? B(c[1]) : c[1])).join('');
|
||||
|
||||
document.getElementById('subsidy-tbl').innerHTML =
|
||||
`<tr><th>项目</th><th>承接机构</th><th>参训人数</th><th>取证率</th><th>训后接单转化率</th><th>补贴核算</th></tr>` +
|
||||
G.trainingSubsidy.map(s => `<tr><td>${s.project}</td><td>${s.org}</td><td>${s.people}</td><td>${s.certRate}</td><td>${s.conv}</td><td>${B(s.settle)}</td></tr>`).join('');
|
||||
|
||||
document.getElementById('risk-list').innerHTML = G.risks.map(r => `<li><span>${r.item}</span><span>${
|
||||
r.action === 'rectify' ? `<button class="btn sm" onclick="rectify()">整改跟踪</button>`
|
||||
: r.action === 'detail' ? `${r.text} <button class="btn sm ghost" onclick="toast('未办结 1 件:交付争议仲裁中(REQ-060)','warn')">详情</button>`
|
||||
: B(r.badge)
|
||||
}</span></li>`).join('');
|
||||
|
||||
document.getElementById('log-list').innerHTML = G.logs.map(l =>
|
||||
`<li><span>${l[0]}</span><span class="muted">${l[1]}</span></li>`).join('');
|
||||
}
|
||||
|
||||
function renderFunding() {
|
||||
const canPay = currentRole() === 'gov';
|
||||
document.getElementById('fund-tbl').innerHTML =
|
||||
`<tr><th>拨付节点</th><th>比例</th><th>金额</th><th>状态</th></tr>` +
|
||||
DB.gov.funding.map((f, i) => `<tr><td>${f.node}</td><td>${f.pct}</td><td>${f.amount}</td><td>${B(f.status)}${
|
||||
f.action === 'pay' && canPay ? ` <button class="btn sm" onclick="pay(${i})">拨付确认</button>` : ''
|
||||
}</td></tr>`).join('');
|
||||
}
|
||||
|
||||
function drill(title, detail) {
|
||||
openModal({ title: '指标下钻 · ' + title, body: `<p>${detail}</p><p class="muted" style="margin-top:8px">统一指标口径见 §8.1,计算逻辑可审计(REQ-089);支持按业务模式/项目/OPC 三维度下钻(REQ-053)。</p>` });
|
||||
}
|
||||
function pay(i) {
|
||||
const f = DB.gov.funding[i];
|
||||
confirmDlg('里程碑拨付确认', `确认「${f.node}」里程碑达成,拨付 ${f.pct}(${f.amount})?拨付后进入财政专账台账并留痕。`, '确认拨付', () => {
|
||||
f.status = ['已拨付', 'ok']; f.action = null;
|
||||
renderFunding();
|
||||
toast(`拨付完成:${f.amount}已记入财政资金专账(REQ-090)`);
|
||||
});
|
||||
}
|
||||
function rectify() {
|
||||
openModal({
|
||||
title: '整改跟踪 · 跨境电商视觉按时率',
|
||||
body: `<table>
|
||||
<tr><th>触发</th><td>按时交付率连续 2 月低于 95%(REQ-052/092)</td></tr>
|
||||
<tr><th>整改措施</th><td>① 新增替补产能 12 人 ② SOP 交付清单修订 ③ 相关人员复训</td></tr>
|
||||
<tr><th>状态</th><td><span class="badge warn">整改中 · 9 月底复核</span></td></tr>
|
||||
<tr><th>闭环</th><td>复核达标后自动解除预警并归档整改报告</td></tr>
|
||||
</table>`,
|
||||
actions: [
|
||||
{ label: '关闭', cls: 'ghost' },
|
||||
{ label: '催办整改', onClick: m => { m.remove(); toast('已向运营商发送整改催办通知'); } },
|
||||
],
|
||||
});
|
||||
}
|
||||
function publishPolicy() {
|
||||
openModal({
|
||||
title: '发布政策(REQ-071/094)',
|
||||
body: `
|
||||
<div class="field"><label>政策名称</label><input placeholder="如:软件工具券发放细则"></div>
|
||||
<div class="field-row">
|
||||
<div class="field"><label>发文机关</label><select><option>区经信局</option><option>区人社局</option><option>市科技局</option></select></div>
|
||||
<div class="field"><label>截止日期</label><input type="date"></div>
|
||||
</div>
|
||||
<div class="field"><label>适用对象与申报条件(结构化建模)</label><textarea placeholder="按发文机关、适用对象、申报条件、材料清单、补贴额度建模"></textarea></div>
|
||||
<p class="muted">入库后触发主体画像匹配重算并向适配主体推送(REQ-073);操作全程留痕(REQ-094)。</p>`,
|
||||
actions: [
|
||||
{ label: '取消', cls: 'ghost' },
|
||||
{ label: '发布并触发匹配', onClick: m => { m.remove(); toast('政策已入库,匹配重算完成:新增命中主体 214 个'); } },
|
||||
],
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
+164
@@ -0,0 +1,164 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>门户首页 · OPC 公共服务平台 Demo</title>
|
||||
<link rel="stylesheet" href="assets/style.css">
|
||||
</head>
|
||||
<body data-page="index">
|
||||
<header class="topnav" id="topnav"></header>
|
||||
|
||||
<div class="container">
|
||||
<!-- REQ-001/002 统一门户与三类行动入口 -->
|
||||
<div class="hero">
|
||||
<h1>一人公司 · 超级个体产业社区</h1>
|
||||
<p>以真实订单为牵引,连接政策服务、能力认证、企业需求、任务协同、质量交付、结算评价和绩效监管的产业公共服务基础设施。</p>
|
||||
<div style="margin-top:18px" id="hero-login">
|
||||
<a class="btn" href="login.html">登录 / 选择角色进入系统</a>
|
||||
<a class="btn ghost" href="showcase.html" target="_blank">查看社区大屏</a>
|
||||
<a class="btn ghost" href="req-doc.html" target="_blank">帮助 / 需求文档</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="entry-cards">
|
||||
<div class="entry-card">
|
||||
<h3>我要接订单</h3>
|
||||
<p>OPC 注册建档、能力认证、进入订单池承接任务</p>
|
||||
<a class="btn" href="login.html">登录进入 OPC 工作台</a>
|
||||
</div>
|
||||
<div class="entry-card">
|
||||
<h3>我要找服务</h3>
|
||||
<p>企业/个人发布需求、浏览服务产品、咨询询价</p>
|
||||
<a class="btn" href="login.html">登录发布需求</a>
|
||||
</div>
|
||||
<div class="entry-card">
|
||||
<h3>我要看成效</h3>
|
||||
<p>政府主管部门查看订单、就业、收入与资金绩效</p>
|
||||
<a class="btn" href="login.html">登录进入政府驾驶舱</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- REQ-009 服务目录(按服务结果组织) -->
|
||||
<div class="section">
|
||||
<h2>服务目录 <span class="req">REQ-009 · 按"服务结果"组织</span>
|
||||
<span style="margin-left:auto"><a class="btn sm ghost" href="market.html">OPC橙果商城 →</a></span>
|
||||
</h2>
|
||||
<div class="grid grid-4" id="products"></div>
|
||||
</div>
|
||||
|
||||
<!-- REQ-003/006 优秀案例与能力名片 -->
|
||||
<div class="section">
|
||||
<h2>优秀 OPC 与交付案例 <span class="req">REQ-003~007 · 经授权脱敏、运营商审核发布,AI 生成内容须标注</span></h2>
|
||||
<div class="grid grid-3" id="cases"></div>
|
||||
</div>
|
||||
|
||||
<!-- REQ-005 示范场景专区 -->
|
||||
<div class="section">
|
||||
<h2>示范场景专区 <span class="req">REQ-005</span></h2>
|
||||
<div class="card"><table id="scenes"></table></div>
|
||||
</div>
|
||||
|
||||
<!-- REQ-008/095 活动日历 + REQ-010 投诉建议 -->
|
||||
<div class="section">
|
||||
<div class="two-col">
|
||||
<div class="card">
|
||||
<h3>活动日历 <span class="req">REQ-008/095</span></h3>
|
||||
<ul class="list" id="acts"></ul>
|
||||
<p class="muted" style="margin-top:8px">每场活动关联后续线索与订单转化(REQ-096/097)。</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>帮助与投诉 <span class="req">REQ-010</span></h3>
|
||||
<ul class="list">
|
||||
<li><span>帮助中心 / 常见问题</span><a class="btn sm ghost" href="req-doc.html" target="_blank">查看</a></li>
|
||||
<li><span>投诉建议通道(工单可追溯)</span><button class="btn sm" onclick="complaint()">提交</button></li>
|
||||
<li><span>政策公告</span><a class="btn sm ghost" href="policy.html">查看</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="footer">OPC 公共服务平台 Demo · 静态原型 · 数据 assets/mock.json</div>
|
||||
<script src="assets/app.js"></script>
|
||||
<script src="assets/store.js"></script>
|
||||
<script>
|
||||
function renderPage(DB) {
|
||||
const P = DB.portal;
|
||||
// 已登录用户:hero 区登录按钮改为"进入工作台"
|
||||
const role = currentRole();
|
||||
if (role) {
|
||||
document.getElementById('hero-login').innerHTML =
|
||||
`<a class="btn" href="${ROLES[role].home}">进入 ${ROLES[role].name} 工作台</a>
|
||||
<a class="btn ghost" href="showcase.html" target="_blank">查看社区大屏</a>
|
||||
<a class="btn ghost" href="req-doc.html" target="_blank">帮助 / 需求文档</a>`;
|
||||
}
|
||||
document.getElementById('products').innerHTML = P.products.map(p => `
|
||||
<div class="card"><h3>${p.name}</h3><div class="sub">${p.cat}</div><p class="muted">${p.brief}</p>
|
||||
<button class="btn sm" onclick="productDetail('${p.name}')">查看详情</button>
|
||||
<button class="btn sm ghost" onclick="toast('已提交咨询,运营商将在 4 小时内联系您')">咨询询价</button></div>
|
||||
`).join('');
|
||||
|
||||
document.getElementById('cases').innerHTML = P.cases.map(c => `
|
||||
<div class="card"><h3>${c.name} <span class="badge pri">${c.level}</span></h3>
|
||||
<p class="muted">${c.tags}</p><p class="muted">${c.stats}</p>${B(c.status)}</div>
|
||||
`).join('');
|
||||
|
||||
document.getElementById('scenes').innerHTML =
|
||||
`<tr><th>示范场景</th><th>委托方</th><th>交付成果</th><th>验收结论</th><th>复制推广</th></tr>` +
|
||||
P.demoScenes.map(s => `<tr><td>${s.scene}</td><td>${s.client}</td><td>${s.result}</td><td>${B(s.accept)}</td><td>${s.spread}</td></tr>`).join('');
|
||||
|
||||
document.getElementById('acts').innerHTML = P.activities.map(a => `<li><span>${a.name}</span><span>${
|
||||
a.action === 'signup'
|
||||
? `<button class="btn sm" onclick="signup(this,'${a.name}')">报名</button>`
|
||||
: `<button class="btn sm ghost" onclick="toast('活动尚未开放报名,已为您设置提醒')">提醒</button>`
|
||||
}</span></li>`).join('');
|
||||
}
|
||||
|
||||
function productDetail(name) {
|
||||
const p = DB.portal.products.find(x => x.name === name);
|
||||
openModal({
|
||||
title: name,
|
||||
body: `<table>
|
||||
<tr><th>产品编码</th><td>${p.code}</td></tr>
|
||||
<tr><th>适用场景</th><td>${p.scene}</td></tr>
|
||||
<tr><th>交付物</th><td>${p.items}</td></tr>
|
||||
<tr><th>交付周期</th><td>${p.cycle}</td></tr>
|
||||
<tr><th>价格区间</th><td>${p.price}</td></tr>
|
||||
<tr><th>验收规则</th><td>${p.accept}</td></tr>
|
||||
<tr><th>修改次数</th><td>${p.revise}</td></tr>
|
||||
<tr><th>版权说明</th><td>交付即转移使用权,素材来源记录归档备查</td></tr>
|
||||
</table>`,
|
||||
actions: [
|
||||
{ label: '关闭', cls: 'ghost' },
|
||||
{ label: '发起采购', onClick: m => { m.remove(); toast('已转入采购流程:请登录企业工作台完善需求'); location.href = 'enterprise.html'; } },
|
||||
],
|
||||
});
|
||||
}
|
||||
function signup(btn, name) {
|
||||
confirmDlg('活动报名', `确认报名「${name}」?报名成功后将短信通知签到码。`, '确认报名', () => {
|
||||
btn.outerHTML = '<span class="badge ok">已报名</span>';
|
||||
toast('报名成功,签到码已发送');
|
||||
});
|
||||
}
|
||||
function complaint() {
|
||||
openModal({
|
||||
title: '投诉建议',
|
||||
body: `
|
||||
<div class="field"><label>类型</label><select id="cp-type"><option>服务质量投诉</option><option>交付争议</option><option>机构/课程投诉</option><option>功能建议</option></select></div>
|
||||
<div class="field"><label>关联单号(选填)</label><input id="cp-order" placeholder="ORDER_/SVC_/CERT_ 编号"></div>
|
||||
<div class="field"><label>问题描述</label><textarea id="cp-desc" placeholder="请描述问题经过与诉求"></textarea></div>`,
|
||||
actions: [
|
||||
{ label: '取消', cls: 'ghost' },
|
||||
{ label: '提交工单', onClick: m => {
|
||||
const d = document.getElementById('cp-desc').value.trim();
|
||||
if (!d) { toast('请填写问题描述', 'warn'); return; }
|
||||
m.remove();
|
||||
toast('工单 CP_2026091201 已提交,处理进度可在工单中心跟踪');
|
||||
} },
|
||||
],
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,294 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>培训机构管理 · OPC 公共服务平台 Demo</title>
|
||||
<link rel="stylesheet" href="assets/style.css">
|
||||
</head>
|
||||
<body data-page="institution">
|
||||
<header class="topnav" id="topnav"></header>
|
||||
|
||||
<div class="container">
|
||||
<div class="page-title">培训机构管理</div>
|
||||
<div class="page-sub" id="ins-sub"></div>
|
||||
|
||||
<!-- REQ-099 入驻资质 -->
|
||||
<div class="card">
|
||||
<h3>机构资质 <span class="req">REQ-099</span></h3>
|
||||
<div id="ins-steps"></div>
|
||||
<table style="margin-top:12px" id="qual-tbl"></table>
|
||||
</div>
|
||||
|
||||
<div class="section grid grid-4" id="ins-stats"></div>
|
||||
|
||||
<!-- REQ-100/101 课程发布 -->
|
||||
<div class="section">
|
||||
<h2>课程与实训项目 <span class="req">REQ-100/101 · 上架审核 + 收费交易</span></h2>
|
||||
<div class="card">
|
||||
<table id="course-tbl"></table>
|
||||
<p class="muted" style="margin-top:8px">收费项目经第三方支付通道结算,明示退费规则;财政补贴项目单独核算绩效(REQ-101/109/110)。
|
||||
<button class="btn sm" style="float:right" onclick="newCourse()">+ 发布课程/实训项目</button></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- REQ-104 认证项目 -->
|
||||
<div class="section">
|
||||
<div class="two-col">
|
||||
<div class="card">
|
||||
<h3>认证项目 <span class="req">REQ-104</span></h3>
|
||||
<table id="program-tbl"></table>
|
||||
<p class="muted" style="margin-top:8px">认证标准经运营商审核,与平台能力标签体系关联;通过结果计入 OPC 能力档案。
|
||||
<button class="btn sm" onclick="newCertProgram()">+ 新增认证项目</button></p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>教学质量与退出机制 <span class="req">REQ-102</span></h3>
|
||||
<ul class="list" id="ins-quality"></ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- REQ-105/108 证书颁发与查验 -->
|
||||
<div class="section">
|
||||
<h2>证书颁发与查验 <span class="req">REQ-105/108</span></h2>
|
||||
<div class="card">
|
||||
<table id="cert-tbl"></table>
|
||||
<p class="muted" style="margin-top:8px">对外查验页:输入证书编号可验证颁发主体、等级、能力标签、有效期与防伪标识;吊销/复评同步能力档案。
|
||||
<button class="btn sm" style="float:right" onclick="issueCert()">+ 颁发证书</button>
|
||||
<button class="btn sm ghost" style="float:right;margin-right:6px" onclick="verifyPortal()">查验页演示</button></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- REQ-103 共建 + REQ-109 绩效 -->
|
||||
<div class="section">
|
||||
<div class="two-col">
|
||||
<div class="card">
|
||||
<h3>共建实训基地 <span class="req">REQ-103 · 公共服务轨</span></h3>
|
||||
<ul class="list" id="cobuild-list"></ul>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>财政补贴绩效 <span class="req">REQ-109</span></h3>
|
||||
<ul class="list" id="subsidy-list"></ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="footer">OPC 公共服务平台 Demo · 培训机构管理</div>
|
||||
<script src="assets/app.js"></script>
|
||||
<script src="assets/store.js"></script>
|
||||
<script>
|
||||
let certSeq = 43;
|
||||
|
||||
function renderPage(DB) {
|
||||
const I = DB.institution;
|
||||
const role = currentRole();
|
||||
document.getElementById('ins-sub').textContent = role === 'operator'
|
||||
? I.code + ' ' + I.name + ' · 运营商管理视角(机构入驻审核与运营监管) · 市场化 + 公共服务双轨'
|
||||
: I.code + ' ' + I.name + ' · 入驻机构工作台 · 市场化 + 公共服务双轨';
|
||||
document.getElementById('ins-steps').innerHTML = STEPS(I.steps);
|
||||
|
||||
const qa = {
|
||||
license: `<button class="btn sm ghost" onclick="toast('办学许可证 No.教民xxxx · 有效期至 2028-12(演示)')">查看证照</button>`,
|
||||
teacher: `<button class="btn sm ghost" onclick="addTeacher()">新增讲师</button>`,
|
||||
dirs: `<button class="btn sm ghost" onclick="toast('已审方向:电商视觉 / AI 漫剧 / 设计基础')">方向明细</button>`,
|
||||
credit: `<button class="btn sm ghost" onclick="toast('近 12 个月无行政处罚与投诉成立记录')">信用报告</button>`,
|
||||
};
|
||||
document.getElementById('qual-tbl').innerHTML =
|
||||
`<tr><th>核验项</th><th>状态</th><th>操作</th></tr>` +
|
||||
I.quals.map(q => `<tr><td>${q.item}</td><td>${B(q.status)}</td><td>${qa[q.action]}</td></tr>`).join('');
|
||||
|
||||
document.getElementById('ins-stats').innerHTML = I.stats.map(s =>
|
||||
`<div class="card"><h3>${s.label}</h3><div class="num">${s.num}</div><p class="muted">${s.sub}</p></div>`).join('');
|
||||
|
||||
renderCourses(); renderPrograms(); renderCerts();
|
||||
|
||||
document.getElementById('ins-quality').innerHTML = I.quality.map(q =>
|
||||
LI(q[0], Array.isArray(q[1]) ? B(q[1]) : q[1])).join('');
|
||||
document.getElementById('cobuild-list').innerHTML = I.cobuild.map(c => LI(c[0], B(c[1]))).join('');
|
||||
document.getElementById('subsidy-list').innerHTML = I.subsidy.map(s =>
|
||||
LI(s[0], Array.isArray(s[1]) ? B(s[1]) : s[1])).join('');
|
||||
}
|
||||
|
||||
function renderCourses() {
|
||||
const acts = {
|
||||
edit: c => `<button class="btn sm ghost" onclick="editCourse('${c.name}')">编辑</button> <button class="btn sm ghost" onclick="offShelf(this)">下架</button>`,
|
||||
cobuild: c => `<button class="btn sm ghost" onclick="toast('共建课程由平台统一审核管理(REQ-103),修改需运营商复核')">共建管理</button>`,
|
||||
withdraw: c => `<button class="btn sm ghost" onclick="withdraw(this)">撤回</button>`,
|
||||
};
|
||||
document.getElementById('course-tbl').innerHTML =
|
||||
`<tr><th>项目</th><th>类型</th><th>属性</th><th>价格</th><th>报名</th><th>上架状态</th><th>操作</th></tr>` +
|
||||
DB.institution.courses.map(c => `<tr><td>${c.name} <span class="muted">${c.code}</span><br>${c.tags.map(TAG).join('')}</td><td>${c.type}</td><td>${B(c.attr)}</td><td>${c.price}</td><td>${c.enroll}</td><td>${B(c.status)}</td><td>${acts[c.action](c)}</td></tr>`).join('');
|
||||
}
|
||||
|
||||
function renderPrograms() {
|
||||
const pa = {
|
||||
exam: () => `<button class="btn sm ghost" onclick="toast('本期报名 42 人 · 已考 30 人 · 通过 23 人')">考情</button>`,
|
||||
review: () => `<button class="btn sm ghost" onclick="toast('评审中 8 人 · 待发证 5 人')">评审</button>`,
|
||||
};
|
||||
document.getElementById('program-tbl').innerHTML =
|
||||
`<tr><th>认证项目</th><th>方式</th><th>通过条件</th><th>关联标签</th><th>状态</th><th>操作</th></tr>` +
|
||||
DB.institution.certPrograms.map(p => `<tr><td>${p.name} <span class="muted">${p.code}</span></td><td>${p.method}</td><td>${p.cond}</td><td>${TAG(p.tag)}</td><td>${B(p.status)}</td><td>${pa[p.action]()}</td></tr>`).join('');
|
||||
}
|
||||
|
||||
function renderCerts() {
|
||||
const ca = {
|
||||
revoke: c => `<button class="btn sm ghost" onclick="revoke(this,'${c.no}')">吊销</button>`,
|
||||
reassess: c => `<button class="btn sm" onclick="reassess(this,'${c.no}')">安排复评</button>`,
|
||||
};
|
||||
document.getElementById('cert-tbl').innerHTML =
|
||||
`<tr><th>证书编号</th><th>持证人</th><th>认证项目</th><th>颁发日</th><th>有效期至</th><th>状态</th><th>操作</th></tr>` +
|
||||
DB.institution.certs.map(c => `<tr><td>${c.no}</td><td>${c.holder}</td><td>${c.program}</td><td>${c.issued}</td><td>${c.expire}</td><td>${B(c.status)}</td><td>${c.action ? ca[c.action](c) : '—'}</td></tr>`).join('');
|
||||
}
|
||||
|
||||
function newCourse() {
|
||||
openModal({
|
||||
title: '发布课程/实训项目(REQ-100)',
|
||||
body: `
|
||||
<div class="field"><label>项目名称</label><input id="nc-name" placeholder="如:AIGC 短视频实战班"></div>
|
||||
<div class="field-row">
|
||||
<div class="field"><label>类型</label><select id="nc-type"><option>课程</option><option>实训项目</option><option>认证项目</option></select></div>
|
||||
<div class="field"><label>属性</label><select id="nc-attr"><option>收费</option><option>公益·财政补贴</option><option>共建</option></select></div>
|
||||
</div>
|
||||
<div class="field-row">
|
||||
<div class="field"><label>价格(公益课填 0)</label><input id="nc-price" value="980"></div>
|
||||
<div class="field"><label>目标等级</label><select><option>→L1</option><option>→L2</option><option>→L3</option><option>→L4</option></select></div>
|
||||
</div>
|
||||
<div class="field"><label>退费规则(收费必填)</label><input placeholder="如:开课前全退,开课后按课时比例"></div>`,
|
||||
actions: [
|
||||
{ label: '取消', cls: 'ghost' },
|
||||
{ label: '提交审核', onClick: m => {
|
||||
const n = document.getElementById('nc-name').value.trim();
|
||||
if (!n) { toast('请填写项目名称', 'warn'); return; }
|
||||
const type = document.getElementById('nc-type').value;
|
||||
const attr = document.getElementById('nc-attr').value;
|
||||
const price = document.getElementById('nc-price').value;
|
||||
m.remove();
|
||||
DB.institution.courses.push({ name: n, tags: ['待标注'], type, attr: [attr, 'pri'], price: '¥' + price, enroll: '—', status: ['运营商审核中', 'info'], action: 'withdraw' });
|
||||
renderCourses();
|
||||
toast('已提交,运营商审核通过后上架(REQ-100)');
|
||||
} },
|
||||
],
|
||||
});
|
||||
}
|
||||
function editCourse(name) {
|
||||
openModal({
|
||||
title: '编辑 · ' + name,
|
||||
body: `<div class="field"><label>价格</label><input value="¥1,280"></div>
|
||||
<div class="field"><label>退费规则</label><input value="开课前全退,开课后按课时比例退"></div>
|
||||
<p class="muted">价格/退费规则变更需运营商复核后生效。</p>`,
|
||||
actions: [
|
||||
{ label: '取消', cls: 'ghost' },
|
||||
{ label: '保存并送审', onClick: m => { m.remove(); toast('修改已提交复核'); } },
|
||||
],
|
||||
});
|
||||
}
|
||||
function offShelf(btn) {
|
||||
confirmDlg('下架确认', '下架后停止新报名,在读学员不受影响。确认下架?', '确认下架', () => {
|
||||
const row = btn.closest('tr');
|
||||
setBadge(row.cells[5], '已下架', 'gray');
|
||||
btn.remove();
|
||||
toast('课程已下架');
|
||||
});
|
||||
}
|
||||
function withdraw(btn) {
|
||||
confirmDlg('撤回送审', '撤回后可修改内容重新提交审核。', '确认撤回', () => {
|
||||
const name = btn.closest('tr').cells[0].textContent.trim();
|
||||
DB.institution.courses = DB.institution.courses.filter(c => !name.startsWith(c.name));
|
||||
renderCourses();
|
||||
toast('已撤回送审项目');
|
||||
});
|
||||
}
|
||||
function addTeacher() {
|
||||
openModal({
|
||||
title: '新增讲师备案',
|
||||
body: `<div class="field"><label>姓名</label><input></div>
|
||||
<div class="field"><label>资质证明(职称/作品/行业认证)</label><input type="file"></div>`,
|
||||
actions: [
|
||||
{ label: '取消', cls: 'ghost' },
|
||||
{ label: '提交备案', onClick: m => { m.remove(); toast('讲师备案已提交,计入师资力量核验'); } },
|
||||
],
|
||||
});
|
||||
}
|
||||
function newCertProgram() {
|
||||
openModal({
|
||||
title: '新增认证项目(REQ-104)',
|
||||
body: `
|
||||
<div class="field"><label>认证名称</label><input id="np-name" placeholder="如:AI 视觉质检师认证"></div>
|
||||
<div class="field"><label>认证方式</label><select id="np-method"><option>考试+作品评审</option><option>实训评审</option><option>仅考试</option></select></div>
|
||||
<div class="field"><label>通过条件</label><input id="np-cond" placeholder="如:笔试 ≥80 + 作品评审通过"></div>
|
||||
<div class="field"><label>关联能力标签</label><input id="np-tag" placeholder="行业 / 服务 / 工具"></div>`,
|
||||
actions: [
|
||||
{ label: '取消', cls: 'ghost' },
|
||||
{ label: '提交审核', onClick: m => {
|
||||
const n = document.getElementById('np-name').value.trim();
|
||||
if (!n) { toast('请填写认证名称', 'warn'); return; }
|
||||
DB.institution.certPrograms.push({ name: n, method: document.getElementById('np-method').value, cond: document.getElementById('np-cond').value, tag: document.getElementById('np-tag').value || '待关联', status: ['运营商审核中', 'info'], action: 'review' });
|
||||
m.remove(); renderPrograms();
|
||||
toast('认证项目已提交运营商审核,通过后与能力标签体系关联');
|
||||
} },
|
||||
],
|
||||
});
|
||||
}
|
||||
function issueCert() {
|
||||
certSeq++;
|
||||
const no = 'CERT_2026' + String(certSeq).padStart(4, '0');
|
||||
openModal({
|
||||
title: '颁发证书(REQ-105)',
|
||||
body: `
|
||||
<div class="field"><label>持证人</label><input placeholder="OPC_编号"></div>
|
||||
<div class="field"><label>认证项目</label><select><option>电商视觉设计师认证(中级)</option><option>AI 漫剧制作认证</option></select></div>
|
||||
<div class="field"><label>有效期</label><select><option>2 年</option><option>3 年</option></select></div>
|
||||
<p class="muted">将生成证书编号 ${no},附防伪标识并开放对外查验。</p>`,
|
||||
actions: [
|
||||
{ label: '取消', cls: 'ghost' },
|
||||
{ label: '确认颁发', onClick: m => {
|
||||
m.remove();
|
||||
DB.institution.certs.push({ no, holder: 'OPC_新学员', program: '电商视觉中级', issued: '2026-09-12', expire: '2028-09-12', status: ['有效 · 可对外查验', 'ok'], action: 'revoke' });
|
||||
renderCerts();
|
||||
toast(`证书 ${no} 已颁发并计入持证人能力档案`);
|
||||
} },
|
||||
],
|
||||
});
|
||||
}
|
||||
function revoke(btn, no) {
|
||||
confirmDlg('吊销证书', `确认吊销 ${no}?吊销原因将留痕并同步持证人能力档案(REQ-108)。`, '确认吊销', () => {
|
||||
const c = DB.institution.certs.find(x => x.no === no);
|
||||
if (c) { c.status = ['已吊销', 'err']; c.action = null; }
|
||||
renderCerts();
|
||||
toast(`${no} 已吊销,能力档案同步更新`, 'warn');
|
||||
});
|
||||
}
|
||||
function reassess(btn, no) {
|
||||
confirmDlg('安排复评', `为 ${no} 持证人安排到期复评?复评不通过将降级或吊销。`, '确认安排', () => {
|
||||
const c = DB.institution.certs.find(x => x.no === no);
|
||||
if (c) { c.status = ['复评安排中', 'info']; c.action = null; }
|
||||
renderCerts();
|
||||
toast('复评已排期,通知已发送持证人');
|
||||
});
|
||||
}
|
||||
function verifyPortal() {
|
||||
openModal({
|
||||
title: '证书查验页(对外公开演示)',
|
||||
body: `<div class="field"><label>证书编号</label><input id="vp-no" value="CERT_20260042"></div>`,
|
||||
actions: [
|
||||
{ label: '取消', cls: 'ghost' },
|
||||
{ label: '查验', onClick: m => {
|
||||
const no = document.getElementById('vp-no').value.trim();
|
||||
m.remove();
|
||||
const c = DB.institution.certs.find(x => x.no === no);
|
||||
if (!c) { toast('未查询到该证书,请核对编号(REQ-105)', 'err'); return; }
|
||||
openModal({ title: '查验结果 · ' + no, body: `<table>
|
||||
<tr><th>颁发主体</th><td>${DB.institution.code} ${DB.institution.name}(平台入驻机构)</td></tr>
|
||||
<tr><th>持证人</th><td>${c.holder}(脱敏)</td></tr>
|
||||
<tr><th>认证项目</th><td>${c.program}</td></tr>
|
||||
<tr><th>有效期</th><td>${c.issued} ~ ${c.expire}</td></tr>
|
||||
<tr><th>状态</th><td>${B(c.status)}</td></tr>
|
||||
</table>` });
|
||||
} },
|
||||
],
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>供需对接 · OPC 公共服务平台 Demo</title>
|
||||
<link rel="stylesheet" href="assets/style.css">
|
||||
</head>
|
||||
<body data-page="leads">
|
||||
<header class="topnav" id="topnav"></header>
|
||||
|
||||
<div class="container">
|
||||
<div class="page-title">供需对接工作台</div>
|
||||
<div class="page-sub" id="ld-sub">线索登记 → 跟进 → 需求确认 → 订单转化,全链路可追溯(REQ-095~098)</div>
|
||||
|
||||
<div class="grid grid-4" id="ld-stats"></div>
|
||||
|
||||
<!-- REQ-097 线索转化漏斗 -->
|
||||
<div class="section">
|
||||
<div class="two-col">
|
||||
<div class="card">
|
||||
<h3>转化漏斗 <span class="req">REQ-097</span></h3>
|
||||
<div class="funnel" id="ld-funnel"></div>
|
||||
<p class="muted" style="margin-top:10px">每场活动自动归集线索并追踪转化贡献(REQ-096)。</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>运营商跟单动作 <span class="req">REQ-098</span></h3>
|
||||
<ul class="list">
|
||||
<li><span>活动后 48h 内完成首次触达</span><span class="badge ok">SOP 执行中</span></li>
|
||||
<li><span>超 7 天未跟进线索标黄</span><span class="badge warn">4 条待处理</span></li>
|
||||
<li><span>需求确认后转企业工作台发单</span><span class="badge info">一键转需求</span></li>
|
||||
<li><span>转化结果回写活动绩效</span><span class="badge pri">自动归集</span></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 线索台账 -->
|
||||
<div class="section">
|
||||
<h2>线索台账 <span class="req">REQ-097 · LEAD_ 统一编号</span>
|
||||
<span style="margin-left:auto"><button class="btn sm" onclick="newLead()">+ 登记线索</button></span>
|
||||
</h2>
|
||||
<div class="card"><table id="ld-tbl"></table></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="footer">OPC 公共服务平台 Demo · 供需对接</div>
|
||||
<script src="assets/app.js"></script>
|
||||
<script src="assets/store.js"></script>
|
||||
<script>
|
||||
let leadSeq = 97;
|
||||
|
||||
function renderPage(DB) {
|
||||
const L = DB.leads;
|
||||
const role = currentRole();
|
||||
document.getElementById('ld-sub').textContent = role === 'gov'
|
||||
? '政府监测视角 · 供需转化成效与漏斗监测(REQ-095~098)'
|
||||
: '线索登记 → 跟进 → 需求确认 → 订单转化,全链路可追溯(REQ-095~098)';
|
||||
document.getElementById('ld-stats').innerHTML = L.stats.map(s =>
|
||||
`<div class="card"><h3>${s.label}</h3><div class="num">${s.num}</div><p class="muted">${s.sub}</p></div>`).join('');
|
||||
|
||||
document.getElementById('ld-funnel').innerHTML = L.funnel.map((f, i) => `
|
||||
<div class="funnel-row">
|
||||
<span class="funnel-stage">${f.stage}</span>
|
||||
<div class="funnel-bar" style="width:${f.w}%">${f.num}</div>
|
||||
${i ? `<span class="funnel-conv">转化 ${(f.num / L.funnel[i-1].num * 100).toFixed(0)}%</span>` : ''}
|
||||
</div>`).join('');
|
||||
|
||||
renderLeads();
|
||||
}
|
||||
|
||||
function leadAction(l, i) {
|
||||
if (l.action === 'follow') return `<button class="btn sm" onclick="follow(${i})">跟进</button>`;
|
||||
if (l.action === 'toReq') return `<button class="btn sm" onclick="toReq(${i})">转需求</button>`;
|
||||
if (l.action === 'order') return `<button class="btn sm ghost" onclick="toast('已关联订单 ${l.order},可在订单与协同查看')">关联订单</button>`;
|
||||
return '—';
|
||||
}
|
||||
function renderLeads() {
|
||||
document.getElementById('ld-tbl').innerHTML =
|
||||
`<tr><th>线索编号</th><th>主体</th><th>来源</th><th>意向需求</th><th>阶段</th><th>跟进人</th><th>操作</th></tr>` +
|
||||
DB.leads.list.map((l, i) => `<tr><td>${l.code}</td><td>${l.party}</td><td>${l.source}</td><td>${l.need}</td><td>${B(l.stage)}</td><td>${l.owner}</td><td>${leadAction(l, i)}</td></tr>`).join('');
|
||||
}
|
||||
|
||||
function newLead() {
|
||||
openModal({
|
||||
title: '登记线索(REQ-097)',
|
||||
body: `
|
||||
<div class="field"><label>主体名称</label><input id="nl-p" placeholder="企业/个人/OPC"></div>
|
||||
<div class="field-row">
|
||||
<div class="field"><label>来源</label><select id="nl-s"><option>门户咨询</option><option>ACT_0001 供需对接会</option><option>政策宣讲</option><option>转介绍</option></select></div>
|
||||
<div class="field"><label>跟进人</label><select><option>运营·王</option><option>运营·李</option></select></div>
|
||||
</div>
|
||||
<div class="field"><label>意向需求</label><textarea id="nl-n" placeholder="一句话描述需求或诉求"></textarea></div>`,
|
||||
actions: [
|
||||
{ label: '取消', cls: 'ghost' },
|
||||
{ label: '登记', onClick: m => {
|
||||
const p = document.getElementById('nl-p').value.trim();
|
||||
if (!p) { toast('请填写主体名称', 'warn'); return; }
|
||||
m.remove();
|
||||
const code = 'LEAD_' + String(leadSeq++).padStart(4, '0');
|
||||
DB.leads.list.unshift({ code, party: p, source: document.getElementById('nl-s').value, need: document.getElementById('nl-n').value || '待沟通', stage: ['跟进中', 'info'], owner: '运营·王', action: 'follow' });
|
||||
DB.leads.funnel[0].num++; DB.leads.funnel[1].num++;
|
||||
renderPage(DB);
|
||||
toast(`线索 ${code} 已登记,进入跟进队列`);
|
||||
} },
|
||||
],
|
||||
});
|
||||
}
|
||||
function follow(i) {
|
||||
const l = DB.leads.list[i];
|
||||
openModal({
|
||||
title: '跟进记录 · ' + l.code,
|
||||
body: `<div class="field"><label>跟进方式</label><select><option>电话回访</option><option>上门拜访</option><option>线上会议</option></select></div>
|
||||
<div class="field"><label>跟进纪要</label><textarea placeholder="将写入线索流转留痕"></textarea></div>`,
|
||||
actions: [
|
||||
{ label: '取消', cls: 'ghost' },
|
||||
{ label: '保存并推进', onClick: m => {
|
||||
m.remove();
|
||||
l.stage = ['需求确认', 'warn']; l.action = 'toReq';
|
||||
DB.leads.funnel[2].num++;
|
||||
renderPage(DB);
|
||||
toast('跟进纪要已记录,线索推进至「需求确认」');
|
||||
} },
|
||||
],
|
||||
});
|
||||
}
|
||||
function toReq(i) {
|
||||
const l = DB.leads.list[i];
|
||||
confirmDlg('转需求确认', `将线索 ${l.code}(${l.party})转为正式需求单?将在企业工作台生成需求并进入询价流程。`, '确认转需求', () => {
|
||||
l.stage = ['已转化', 'ok']; l.action = 'order'; l.order = 'ORDER_2026091301';
|
||||
DB.leads.funnel[3].num++;
|
||||
renderPage(DB);
|
||||
toast(`已转化:关联订单 ORDER_2026091301,转化记录回写来源活动`);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
+174
@@ -0,0 +1,174 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>登录 · OPC 公共服务平台 Demo</title>
|
||||
<link rel="stylesheet" href="assets/style.css">
|
||||
<style>
|
||||
.login-wrap { max-width: 1080px; margin: 48px auto; padding: 0 16px; }
|
||||
.login-head { text-align: center; margin-bottom: 28px; }
|
||||
.login-head h1 { font-size: 24px; }
|
||||
.login-head p { color: var(--text-2); font-size: 13px; margin-top: 6px; }
|
||||
.role-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px; }
|
||||
@media (max-width: 800px) { .role-grid { grid-template-columns: repeat(2,1fr); } }
|
||||
@media (max-width: 520px) { .role-grid { grid-template-columns: 1fr; } }
|
||||
.role-card {
|
||||
background: rgba(255,255,255,0.82); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px);
|
||||
border: 1px solid var(--border); border-radius: var(--radius);
|
||||
padding: 18px; cursor: pointer; transition: all .2s;
|
||||
box-shadow: 0 4px 24px rgba(120, 90, 50, 0.08);
|
||||
}
|
||||
.role-card:hover { border-color: var(--primary); box-shadow: 0 8px 32px rgba(249, 115, 22, 0.15); transform: translateY(-2px); }
|
||||
.role-card h3 { font-size: 15px; margin-bottom: 4px; }
|
||||
.role-card p { font-size: 12px; color: var(--text-2); }
|
||||
.role-card .go { font-size: 12px; color: var(--primary); margin-top: 10px; display: inline-block; }
|
||||
</style>
|
||||
</head>
|
||||
<body data-page="login">
|
||||
<div class="login-wrap">
|
||||
<div class="login-head">
|
||||
<h1>OPC 公共服务平台 <span style="color:var(--primary)">Demo</span></h1>
|
||||
<p>一人公司 / 超级个体产业社区 · 选择角色进入对应工作台(演示环境,无需密码)</p>
|
||||
</div>
|
||||
<div class="role-grid" id="roleGrid"></div>
|
||||
<p class="footer">静态原型演示 · 角色权限参照 pmdocs/0-req-OPCSP.md §3</p>
|
||||
|
||||
<!-- 设计与使用说明 -->
|
||||
<div class="section" id="guide" style="text-align:left;margin-top:36px">
|
||||
<h2 style="font-size:20px;margin-bottom:16px">设计与使用说明 <span class="req">Demo v1.3 · REQ-001~118 全覆盖</span></h2>
|
||||
|
||||
<!-- 平台定位 -->
|
||||
<div class="card" style="margin-bottom:16px">
|
||||
<h3>平台定位</h3>
|
||||
<p>本 Demo 是 OPC 社区运营公共服务平台的可交互原型,验证产品结构、角色权限、业务流程、培训机构/认证管理、成果交易和政府绩效监管等核心能力。长期目标是形成"一个入口、一个身份、一个能力档案、一个订单池、一套交付标准、一套政府绩效台账"的产业公共服务基础设施。</p>
|
||||
<p class="muted" style="margin-top:8px">技术形态:纯静态 HTML + Mock 数据(assets/mock.json)+ localStorage 持久化,无后端依赖。数据通过 fetch 加载,须通过 HTTP 访问(不能 file:// 直开)。</p>
|
||||
</div>
|
||||
|
||||
<!-- 角色与入口 -->
|
||||
<div class="card" style="margin-bottom:16px">
|
||||
<h3>角色与入口 <span class="req">REQ-011~018 · 10 个角色</span></h3>
|
||||
<table>
|
||||
<tr><th>角色</th><th>首页</th><th>核心功能</th></tr>
|
||||
<tr><td>社区运营商</td><td>运营工作台</td><td>全业务运营、审核仲裁、质检规则、拨付确认</td></tr>
|
||||
<tr><td>OPC/一人公司</td><td>OPC 工作台</td><td>建档、能力认证、订单池接单、交付质检、收入结算、上架成果</td></tr>
|
||||
<tr><td>培训学员</td><td>培训认证</td><td>学员档案、培训履历、作品沉淀、申请入驻成为 OPC</td></tr>
|
||||
<tr><td>企业客户</td><td>企业工作台</td><td>发布需求、AI 诊断、小样签约、验收、争议、开票</td></tr>
|
||||
<tr><td>个人客户</td><td>个人工作台</td><td>同企业流程,轻量认证(实名+支付验证)</td></tr>
|
||||
<tr><td>培训机构/导师</td><td>机构管理</td><td>资质入驻、课程/实训/认证项目、证书颁发/吊销/复评、补贴绩效</td></tr>
|
||||
<tr><td>服务机构</td><td>企业服务中心</td><td>资质上架、服务包、机构工单办理、券核销、评价退出</td></tr>
|
||||
<tr><td>政府主管部门</td><td>政府驾驶舱</td><td>指标下钻、政策发布、财政拨付、整改跟踪、供需监测</td></tr>
|
||||
<tr><td>审计/评估机构</td><td>审计视图</td><td>授权范围内只读核查绩效、资金台账、抽样底稿、函证异议</td></tr>
|
||||
<tr><td>金融/园区/平台方</td><td>政策服务</td><td>资源券、信用贷、工位等资源产品管理和核销</td></tr>
|
||||
</table>
|
||||
<p class="muted" style="margin-top:8px">上方选择角色卡片即可进入对应工作台;角色写入 localStorage,各页面按角色动态渲染导航菜单和可见功能。</p>
|
||||
</div>
|
||||
|
||||
<!-- 核心业务流 -->
|
||||
<div class="card" style="margin-bottom:16px">
|
||||
<h3>核心业务流 <span class="req">REQ-027~048 · 全链路单据化</span></h3>
|
||||
<div class="steps" style="margin:8px 0 14px">
|
||||
<span class="step done">需求发布</span><span class="arrow">→</span>
|
||||
<span class="step done">订单生成</span><span class="arrow">→</span>
|
||||
<span class="step done">小样试做</span><span class="arrow">→</span>
|
||||
<span class="step done">签约</span><span class="arrow">→</span>
|
||||
<span class="step cur">任务拆解</span><span class="arrow">→</span>
|
||||
<span class="step">交付批次</span><span class="arrow">→</span>
|
||||
<span class="step">三级质检</span><span class="arrow">→</span>
|
||||
<span class="step">验收</span><span class="arrow">→</span>
|
||||
<span class="step">托管结算</span><span class="arrow">→</span>
|
||||
<span class="step">评价归档</span>
|
||||
</div>
|
||||
<table>
|
||||
<tr><th>业务域</th><th>核心页面</th><th>关键单据</th><th>对应 REQ</th></tr>
|
||||
<tr><td>订单交付</td><td>order.html</td><td>ORDER_ → TASK_ → DLV_ → ACPT_ → SETL_</td><td>REQ-027~048</td></tr>
|
||||
<tr><td>能力认证</td><td>opc.html / training.html</td><td>OPC_ → CERT_ → EVAL_</td><td>REQ-019~026, 081~086</td></tr>
|
||||
<tr><td>培训管理</td><td>institution.html</td><td>TRN_ → CRS_ → ACR_ → CERT_</td><td>REQ-099~110</td></tr>
|
||||
<tr><td>政策资金</td><td>policy.html</td><td>POLICY_ → APLY_ → VCH_ → FUND_</td><td>REQ-071~080</td></tr>
|
||||
<tr><td>供需对接</td><td>leads.html</td><td>ACT_ → LEAD_ → ORDER_</td><td>REQ-095~098</td></tr>
|
||||
<tr><td>成果交易</td><td>market.html</td><td>WORK_ → ORDER_ → LICENSE_</td><td>REQ-111~115</td></tr>
|
||||
<tr><td>政府监管</td><td>government.html</td><td>FUND_ → VCH_ → 绩效台账</td><td>REQ-087~094</td></tr>
|
||||
<tr><td>审计核查</td><td>audit.html</td><td>SMP_ · 函证 · 异议</td><td>REQ-093</td></tr>
|
||||
<tr><td>AI 匹配</td><td>profile.html</td><td>主题画像 → score/evidence/confidence</td><td>REQ-116/117</td></tr>
|
||||
<tr><td>展览大屏</td><td>showcase.html</td><td>脱敏汇总 KPI</td><td>REQ-118</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- 使用指南 -->
|
||||
<div class="card" style="margin-bottom:16px">
|
||||
<h3>使用指南</h3>
|
||||
<ol style="padding-left:20px">
|
||||
<li><b>访问入口</b>:在上方选择角色卡片进入对应工作台(演示环境无需密码)。</li>
|
||||
<li><b>切换角色</b>:进入工作台后,右上角点击「退出」可回到本页重新选择角色。</li>
|
||||
<li><b>推荐体验路径</b>:
|
||||
<ul style="margin:6px 0">
|
||||
<li>企业客户 → 发布需求(AI 诊断)→ 小样签约 → 订单协同 → 三级质检 → 验收</li>
|
||||
<li>OPC → 能力档案 → 订单池报价/领取 → 提交交付物 → 收入结算 → 上架成果到橙果商城</li>
|
||||
<li>培训机构 → 发布课程 → 颁发证书 → 吊销/复评 → 查验页演示</li>
|
||||
<li>政府 → 指标下钻 → 拨付确认 → 整改跟踪 → 发布政策</li>
|
||||
<li>运营商 → 待办审核队列 → 通过/驳回/仲裁 → 跨域数据联动演示</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><b>数据持久化</b>:交互产生的变更(新订单、新证书、状态流转)自动保存到 localStorage,刷新不丢失;点击右上角「重置数据」可恢复 mock.json 初始状态。</li>
|
||||
<li><b>跨页联动</b>:运营商工作台通过审核后,对应课程/成果会同步到机构管理页和橙果商城——演示"一个数据底座"。</li>
|
||||
<li><b>打印导出</b>:政府驾驶舱「导出季度报告」按钮触发浏览器打印(已配 @media print 样式)。</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<!-- 统一编码与文档 -->
|
||||
<div class="card">
|
||||
<h3>统一编码与配套文档</h3>
|
||||
<div class="two-col">
|
||||
<div>
|
||||
<p><b>实体编码规范</b>(附 2)</p>
|
||||
<ul style="font-size:13px">
|
||||
<li>主体/机构:5 位顺序号(OPC_00017、TRN_00001)</li>
|
||||
<li>业务单据:日期+2 位序号(ORDER_2026091201)</li>
|
||||
<li>证书:年份+4 位序号(CERT_20260042)</li>
|
||||
<li>编号不可改、不复用,外键用业务编号关联</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<p><b>配套文档</b></p>
|
||||
<ul style="font-size:13px">
|
||||
<li><a href="../pmdocs/0-req-OPCSP.html">需求与目标文档 v1.3(汇报版 HTML)</a></li>
|
||||
<li><a href="README.md">Demo README(角色—菜单矩阵)</a></li>
|
||||
<li>需求文档:pmdocs/0-req-OPCSP.md(118 条 REQ)</li>
|
||||
<li>Mock 数据:assets/mock.json(14 个业务域)</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="assets/app.js"></script>
|
||||
<script>
|
||||
const DESCS = {
|
||||
operator: '全业务运营、项目管理、质检、结算、机构准入',
|
||||
opc: '建档、学习、认证、接单、交付、购买外包服务、上架成果',
|
||||
learner: '学习实训、沉淀履历与作品、申请入驻成为 OPC',
|
||||
enterprise: '发布需求、选方案、验收、评价',
|
||||
personal: '发布需求、下单、验收、评价(轻量认证)',
|
||||
trainer: '机构入驻、课程/实训/认证项目发布、证书颁发',
|
||||
service: '财税、法务、知识产权、融资等服务上架',
|
||||
gov: '绩效驾驶舱、政策发布、供需对接成效、审计监督',
|
||||
auditor: '授权台账只读核查、抽样底稿、函证与异议登记',
|
||||
finance: '授信、空间、渠道和资源券',
|
||||
};
|
||||
const grid = document.getElementById('roleGrid');
|
||||
grid.innerHTML = Object.entries(ROLES).map(([k, r]) => `
|
||||
<div class="role-card" onclick="loginAs('${k}')">
|
||||
<h3>${r.name}</h3>
|
||||
<p>${DESCS[k] || ''}</p>
|
||||
<span class="go">以该角色进入 →</span>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
function loginAs(role) {
|
||||
localStorage.setItem('opc_role', role);
|
||||
localStorage.setItem('opc_user', '演示账号');
|
||||
location.href = ROLES[role].home;
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,138 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>OPC橙果商城 · OPC 公共服务平台 Demo</title>
|
||||
<link rel="stylesheet" href="assets/style.css">
|
||||
</head>
|
||||
<body data-page="market">
|
||||
<header class="topnav" id="topnav"></header>
|
||||
|
||||
<div class="container">
|
||||
<div class="page-title">OPC橙果商城</div>
|
||||
<div class="page-sub">OPC 上架可复用成果,客户直接购买授权 · 标准化授权与版权管理(REQ-113~115)
|
||||
<span style="float:right" id="mk-actions"></span>
|
||||
</div>
|
||||
|
||||
<!-- REQ-113 成果商品 -->
|
||||
<div class="grid grid-4" id="mk-grid"></div>
|
||||
|
||||
<!-- 我的购买(买方角色) -->
|
||||
<div class="section">
|
||||
<h2>我的购买 <span class="req">REQ-115 · 授权书随单生成</span></h2>
|
||||
<div class="card"><table id="mk-orders"></table></div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<div class="notice">REQ-114:成果须经运营商审核并明示授权类型(个人授权/企业授权/买断)与版权归属;交付含版权来源记录,超范围使用或二次销售进入争议仲裁(REQ-060)。</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="footer">OPC 公共服务平台 Demo · OPC橙果商城</div>
|
||||
<script src="assets/app.js"></script>
|
||||
<script src="assets/store.js"></script>
|
||||
<script>
|
||||
let workSeq = 61;
|
||||
|
||||
function renderPage(DB) {
|
||||
const M = DB.market;
|
||||
const role = currentRole();
|
||||
const canSell = ['opc', 'operator'].includes(role);
|
||||
const canBuy = ['opc', 'enterprise', 'personal', 'learner'].includes(role);
|
||||
|
||||
document.getElementById('mk-actions').innerHTML =
|
||||
(canSell ? `<button class="btn sm" onclick="sellWork()">+ 上架成果</button>` : '');
|
||||
|
||||
document.getElementById('mk-grid').innerHTML = M.items.map(w => `
|
||||
<div class="card">
|
||||
<h3>${w.name}</h3>
|
||||
<div class="sub">${w.code} · ${w.cat}</div>
|
||||
<p class="muted">出品 ${w.seller} · 销量 ${w.sales} · ★${w.rating}</p>
|
||||
<div class="num" style="font-size:18px">${w.price}</div>
|
||||
<p style="margin:6px 0">${B(w.status)}</p>
|
||||
<button class="btn sm" onclick="detail('${w.code}')">查看详情</button>
|
||||
${canBuy && w.action === 'buy' ? `<button class="btn sm ghost" onclick="buyWork('${w.code}')">直接购买</button>` : ''}
|
||||
${role === 'operator' && w.action === 'audit' ? `<button class="btn sm ghost" onclick="auditWork('${w.code}')">审核</button>` : ''}
|
||||
</div>`).join('');
|
||||
|
||||
document.getElementById('mk-orders').innerHTML =
|
||||
`<tr><th>订单号</th><th>成果</th><th>授权类型</th><th>金额</th><th>状态</th></tr>` +
|
||||
M.purchases.map(p => `<tr><td>${p.order}</td><td>${p.item}</td><td>${p.license}</td><td>${p.amount}</td><td>${B(p.status)} <button class="btn sm ghost" onclick="toast('授权书 LICENSE_${p.order.slice(6)}.pdf 已下载(演示)')">授权书</button></td></tr>`).join('');
|
||||
}
|
||||
|
||||
function detail(code) {
|
||||
const w = DB.market.items.find(x => x.code === code);
|
||||
openModal({
|
||||
title: w.name,
|
||||
body: `<table>
|
||||
<tr><th>成果编码</th><td>${w.code}</td></tr>
|
||||
<tr><th>出品方</th><td>${w.seller}</td></tr>
|
||||
<tr><th>授权类型</th><td>${w.licenses.map(l => `${l[0]} ${l[1]}`).join(' · ')}</td></tr>
|
||||
<tr><th>版权归属</th><td>著作权归出品 OPC 所有;按授权类型授予使用权,买断转移全部财产权</td></tr>
|
||||
<tr><th>来源记录</th><td><span class="badge ok">素材来源已备案 · 无侵权风险</span></td></tr>
|
||||
<tr><th>交付内容</th><td>源文件 + 授权书 + 使用说明</td></tr>
|
||||
</table>`,
|
||||
actions: [
|
||||
{ label: '关闭', cls: 'ghost' },
|
||||
{ label: '购买', onClick: m => { m.remove(); buyWork(code); } },
|
||||
],
|
||||
});
|
||||
}
|
||||
function buyWork(code) {
|
||||
const w = DB.market.items.find(x => x.code === code);
|
||||
openModal({
|
||||
title: '购买成果 · ' + w.name,
|
||||
body: `
|
||||
<div class="field"><label>授权类型</label><select id="bw-l">${w.licenses.map(l => `<option>${l[0]}(${l[1]})</option>`).join('')}</select></div>
|
||||
<div class="checks"><label><input type="checkbox" id="bw-a" checked> 同意授权协议(授权范围、禁止事项、违约责任)</label></div>
|
||||
<p class="muted">支付经第三方通道完成,授权书随订单自动生成(REQ-114)。</p>`,
|
||||
actions: [
|
||||
{ label: '取消', cls: 'ghost' },
|
||||
{ label: '支付并下单', onClick: m => {
|
||||
if (!document.getElementById('bw-a').checked) { toast('请先同意授权协议', 'warn'); return; }
|
||||
m.remove();
|
||||
const lic = document.getElementById('bw-l').value.split('(')[0];
|
||||
const order = 'ORDER_2026091401';
|
||||
DB.market.purchases.unshift({ order, item: `${w.code} ${w.name}`, license: lic, amount: w.licenses.find(l => l[0] === lic)[1], status: ['已交付 · 授权书已生成', 'ok'] });
|
||||
w.sales++;
|
||||
renderPage(DB);
|
||||
toast(`购买成功:订单 ${order} 已生成,授权书与源文件已交付`);
|
||||
} },
|
||||
],
|
||||
});
|
||||
}
|
||||
function sellWork() {
|
||||
openModal({
|
||||
title: '上架成果(OPC 角色 · REQ-113)',
|
||||
body: `
|
||||
<div class="field"><label>成果名称</label><input id="sw-n" placeholder="如:双 11 Banner 模板包"></div>
|
||||
<div class="field-row">
|
||||
<div class="field"><label>类目</label><select><option>电商视觉模板</option><option>素材包</option><option>AI 漫剧</option><option>企业智能体</option></select></div>
|
||||
<div class="field"><label>个人授权价</label><input placeholder="¥"></div>
|
||||
</div>
|
||||
<div class="checks"><label><input type="checkbox" checked> 声明原创或已获完整授权,版权来源记录随件提交(REQ-114)</label></div>`,
|
||||
actions: [
|
||||
{ label: '取消', cls: 'ghost' },
|
||||
{ label: '提交审核', onClick: m => {
|
||||
const n = document.getElementById('sw-n').value.trim();
|
||||
if (!n) { toast('请填写成果名称', 'warn'); return; }
|
||||
m.remove();
|
||||
DB.market.items.push({ code: 'WORK_00' + workSeq++, name: n, seller: 'OPC_00017 张设计', cat: '电商视觉模板', price: '待定价', licenses: [['个人授权', '待定价']], sales: 0, rating: '—', status: ['审核中', 'info'], action: 'audit' });
|
||||
renderPage(DB);
|
||||
toast('成果已提交运营商审核,审核通过后自动上架并公示授权类型');
|
||||
} },
|
||||
],
|
||||
});
|
||||
}
|
||||
function auditWork(code) {
|
||||
const w = DB.market.items.find(x => x.code === code);
|
||||
confirmDlg('上架审核', `审核「${w.name}」:原创声明 ✓ 版权来源记录 ✓ 授权类型明示 ✓。确认通过上架?`, '通过上架', () => {
|
||||
w.status = ['已上架', 'ok']; w.action = 'buy';
|
||||
renderPage(DB);
|
||||
toast('审核通过,成果已上架');
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
+257
@@ -0,0 +1,257 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>OPC 工作台 · OPC 公共服务平台 Demo</title>
|
||||
<link rel="stylesheet" href="assets/style.css">
|
||||
</head>
|
||||
<body data-page="opc">
|
||||
<header class="topnav" id="topnav"></header>
|
||||
|
||||
<div class="container">
|
||||
<div class="page-title">OPC 工作台</div>
|
||||
<div class="page-sub" id="opc-sub"></div>
|
||||
|
||||
<!-- REQ-013 五态入驻状态 -->
|
||||
<div class="card">
|
||||
<h3>入驻状态 <span class="req">REQ-013</span></h3>
|
||||
<div id="opc-steps"></div>
|
||||
<p class="muted" style="margin-top:10px">距离"优质供应商":复购率需 ≥50%(当前 62% ✓)、累计交付 ≥50 单(当前 38 单)、连续 90 天零投诉 ✓</p>
|
||||
<p class="muted">支持自然人/个体户/一人公司/机构等主体类型(REQ-011);注销保留法定审计数据并匿名化个人信息(REQ-018)。</p>
|
||||
</div>
|
||||
|
||||
<div class="section grid grid-4" id="opc-stats"></div>
|
||||
|
||||
<!-- REQ-019~021 能力档案与认证 -->
|
||||
<div class="section">
|
||||
<div class="two-col">
|
||||
<div class="card">
|
||||
<h3>能力档案 <span class="req">REQ-019/020</span></h3>
|
||||
<div id="opc-profile"></div>
|
||||
<button class="btn sm" style="margin-top:10px" onclick="uploadWork()">上传作品</button>
|
||||
<button class="btn sm ghost" style="margin-top:10px" onclick="toast('能力名片已生成: opc.card/OPC_00017 (演示链接)')">生成对外能力名片</button>
|
||||
<button class="btn sm ghost" style="margin-top:10px" onclick="identity()">实名认证信息</button>
|
||||
<p class="muted" style="margin-top:6px">REQ-006 对外能力名片 · REQ-012/016/017 实名核验</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>能力认证 <span class="req">REQ-021/022/024/025</span></h3>
|
||||
<div id="opc-cert"></div>
|
||||
<a class="btn ghost sm" href="training.html" style="margin-top:10px">进入培训认证</a>
|
||||
<button class="btn sm ghost" style="margin-top:10px" onclick="verifyCert()">查验证书</button>
|
||||
<button class="btn sm ghost" style="margin-top:10px" onclick="scoreBasis()">评分依据</button>
|
||||
<button class="btn sm ghost" style="margin-top:10px" onclick="toast('复评申请已登记,运营商将在到期前 30 天安排复评')">预约复评</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- REQ-031 订单池与智能推荐 -->
|
||||
<div class="section">
|
||||
<h2>订单池 · 推荐任务 <span class="req">REQ-026/031 · 能力档案作为派单/资源券/信用输入,按标签/等级/表现匹配</span></h2>
|
||||
<div class="card"><table id="pool-tbl"></table></div>
|
||||
</div>
|
||||
|
||||
<!-- REQ-043/046 收入与结算 -->
|
||||
<div class="section">
|
||||
<h2>收入与结算 <span class="req">REQ-043/046 · 分账核算 + 保证金托管</span></h2>
|
||||
<div class="two-col">
|
||||
<div class="card">
|
||||
<h3>结算总览</h3>
|
||||
<ul class="list" id="stl-summary"></ul>
|
||||
<button class="btn sm" style="margin-top:10px" onclick="withdraw()">申请提现</button>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>结算单 <span class="req">SETL_ 编号</span></h3>
|
||||
<table id="stl-tbl"></table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- REQ-023 质量预警 + REQ-044 评价 -->
|
||||
<div class="section">
|
||||
<div class="two-col">
|
||||
<div class="card">
|
||||
<h3>质量与信用 <span class="req">REQ-023/050</span></h3>
|
||||
<ul class="list" id="opc-quality"></ul>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>最新客户评价 <span class="req">REQ-044</span></h3>
|
||||
<ul class="list" id="opc-reviews"></ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="footer">OPC 公共服务平台 Demo · OPC 工作台</div>
|
||||
<script src="assets/app.js"></script>
|
||||
<script src="assets/store.js"></script>
|
||||
<script>
|
||||
let avail = { h: '40h', cap: 25 };
|
||||
|
||||
function renderPage(DB) {
|
||||
const O = DB.opc;
|
||||
const role = currentRole();
|
||||
document.getElementById('opc-sub').textContent = role === 'operator'
|
||||
? `${O.id} · ${O.name} · 运营商管理视角(查看 OPC 档案与运营状态)`
|
||||
: `${O.id} · ${O.name} · 入驻五态与能力档案一览`;
|
||||
document.getElementById('opc-steps').innerHTML = STEPS(O.steps);
|
||||
document.getElementById('opc-stats').innerHTML = O.stats.map(s =>
|
||||
`<div class="card"><h3>${s.label}</h3><div class="num">${s.num}</div><p class="muted">${s.sub}</p></div>`).join('');
|
||||
|
||||
const p = O.profile;
|
||||
document.getElementById('opc-profile').innerHTML = `
|
||||
<p class="muted">五层标签:${p.tags}</p>
|
||||
<div>${p.skills.map(TAG).join('')}</div>
|
||||
<table style="margin-top:12px">
|
||||
<tr><th>档案项</th><th>内容</th></tr>
|
||||
${p.rows.map(r => `<tr><td>${r[0]}</td><td>${r[1]}</td></tr>`).join('')}
|
||||
<tr><td>可接单时间</td><td id="avail-cell"></td></tr>
|
||||
<tr><td>保密/合规</td><td><span class="badge ok">NDA 已签</span> <span class="badge ok">合规培训通过</span></td></tr>
|
||||
</table>`;
|
||||
renderAvail();
|
||||
|
||||
const c = O.cert;
|
||||
document.getElementById('opc-cert').innerHTML = `
|
||||
<div class="num">${c.level} <small>${c.desc}</small></div>
|
||||
<p class="muted" style="margin:8px 0">证书 ${c.no} · 有效期至 ${c.expire}</p>
|
||||
${BAR(c.progress, 'ok')}
|
||||
<p class="muted" style="margin-top:6px">${c.next}</p>`;
|
||||
|
||||
renderPool();
|
||||
document.getElementById('stl-summary').innerHTML = O.settlement.summary.map(s => LI(s[0], s[1])).join('');
|
||||
document.getElementById('stl-tbl').innerHTML =
|
||||
`<tr><th>结算单</th><th>关联订单</th><th>金额</th><th>状态</th></tr>` +
|
||||
O.settlement.rows.map(r => `<tr><td>${r.code}</td><td>${r.order}</td><td>${r.amount}</td><td>${B(r.status)}</td></tr>`).join('');
|
||||
|
||||
document.getElementById('opc-quality').innerHTML = O.quality.map(q => LI(q[0], B(q[1]))).join('');
|
||||
document.getElementById('opc-reviews').innerHTML = O.reviews.map(r =>
|
||||
`<li><span>${r[0]}</span><span class="badge ok">${r[1]}</span></li>`).join('');
|
||||
}
|
||||
|
||||
function renderAvail() {
|
||||
document.getElementById('avail-cell').innerHTML =
|
||||
`每周 ${avail.h} · 月产能约 ${avail.cap} 任务件 <button class="btn sm ghost" onclick="editAvail()">调整</button>`;
|
||||
}
|
||||
|
||||
function renderPool() {
|
||||
const acts = {
|
||||
quote: o => `<button class="btn sm" onclick="quote(this,'${o.id}')">参与报价</button>`,
|
||||
claim: o => `<button class="btn sm" onclick="claim(this,'${o.id}')">领取任务</button>`,
|
||||
view: o => `<a class="btn sm ghost" href="order.html">查看</a>`,
|
||||
};
|
||||
document.getElementById('pool-tbl').innerHTML =
|
||||
`<tr><th>订单</th><th>类型</th><th>交付物</th><th>预算</th><th>截止</th><th>匹配度</th><th>操作</th></tr>` +
|
||||
DB.opc.orderPool.map(o => `<tr><td>${o.id}</td><td>${B(o.type)}</td><td>${o.items}</td><td>${o.budget}</td><td>${o.ddl}</td><td>${B(o.match)}</td><td>${acts[o.action](o)}</td></tr>`).join('');
|
||||
}
|
||||
|
||||
function claim(btn, id) {
|
||||
const o = DB.opc.orderPool.find(x => x.id === id);
|
||||
confirmDlg('领取派单任务', `${o.items} · ${o.budget} · 截止 ${o.ddl}。领取后将按 SOP 提交清单执行并计入产能。`, '确认领取', () => {
|
||||
btn.outerHTML = '<span class="badge ok">已领取 · 进入任务协同</span>';
|
||||
toast('任务已领取,可在「订单与协同」查看 SOP 与提交清单');
|
||||
});
|
||||
}
|
||||
function quote(btn, id) {
|
||||
const o = DB.opc.orderPool.find(x => x.id === id);
|
||||
openModal({
|
||||
title: '参与询价报价 · ' + id,
|
||||
body: `
|
||||
<div class="field"><label>报价金额(客户预算 ${o.budget})</label><input id="q-price" value="22500"></div>
|
||||
<div class="field"><label>交付周期</label><select><option>10 天</option><option>14 天</option><option>20 天</option></select></div>
|
||||
<div class="field"><label>方案说明</label><textarea placeholder="交付物拆分、样例、排产说明"></textarea></div>
|
||||
<p class="muted">小样试做:本单要求小样,报价后进入小样环节(REQ-032)。</p>`,
|
||||
actions: [
|
||||
{ label: '取消', cls: 'ghost' },
|
||||
{ label: '提交报价', onClick: m => {
|
||||
m.remove();
|
||||
btn.outerHTML = '<span class="badge info">已报价 ¥22,500</span>';
|
||||
toast('报价已提交,等待运营商组织评审');
|
||||
} },
|
||||
],
|
||||
});
|
||||
}
|
||||
function uploadWork() {
|
||||
openModal({
|
||||
title: '上传作品',
|
||||
body: `
|
||||
<div class="field"><label>作品名称</label><input placeholder="如:夏季新品主图系列"></div>
|
||||
<div class="field"><label>关联能力标签</label><input value="跨境电商 / 视觉设计 / 商品场景图"></div>
|
||||
<div class="field"><label>客户授权说明(REQ-004)</label><select><option>已获客户授权展示</option><option>仅展示脱敏能力描述</option></select></div>
|
||||
<div class="field"><label>文件</label><input type="file"></div>`,
|
||||
actions: [
|
||||
{ label: '取消', cls: 'ghost' },
|
||||
{ label: '提交审核', onClick: m => { m.remove(); toast('作品已提交,运营商审核通过后进入对外名片'); } },
|
||||
],
|
||||
});
|
||||
}
|
||||
function editAvail() {
|
||||
openModal({
|
||||
title: '调整可接单时间',
|
||||
body: `<div class="field-row">
|
||||
<div class="field"><label>每周可投入</label><select id="a-h"><option>20h</option><option selected>40h</option><option>60h</option></select></div>
|
||||
<div class="field"><label>月产能(任务件)</label><input id="a-cap" value="${avail.cap}"></div></div>`,
|
||||
actions: [
|
||||
{ label: '取消', cls: 'ghost' },
|
||||
{ label: '保存', onClick: m => {
|
||||
avail.h = document.getElementById('a-h').value;
|
||||
avail.cap = document.getElementById('a-cap').value;
|
||||
m.remove(); renderAvail();
|
||||
toast('可接单时间已更新,派单匹配将按新产能计算');
|
||||
} },
|
||||
],
|
||||
});
|
||||
}
|
||||
function identity() {
|
||||
openModal({
|
||||
title: '实名认证信息(REQ-012/017)',
|
||||
body: `<table>
|
||||
<tr><th>主体类型</th><td>个体工商户(个人通道已完成实名+支付验证)</td></tr>
|
||||
<tr><th>手机核验</th><td><span class="badge ok">已通过</span> 138****6688</td></tr>
|
||||
<tr><th>身份证核验</th><td><span class="badge ok">已通过</span> 3301**********1234</td></tr>
|
||||
<tr><th>营业执照</th><td><span class="badge ok">已核验</span> 统一社会信用代码 92****XU</td></tr>
|
||||
<tr><th>法人/操作人/收款主体</th><td>三者一致 · 已关联</td></tr>
|
||||
<tr><th>变更触发</th><td>法人、收款账户变更将自动触发重新核验(REQ-016)</td></tr>
|
||||
</table>`,
|
||||
actions: [
|
||||
{ label: '关闭', cls: 'ghost' },
|
||||
{ label: '模拟收款账户变更', cls: 'danger', onClick: m => { m.remove(); toast('检测到收款主体变更:已触发重新核验流程,期间接单权限临时锁定(REQ-016)', 'warn'); } },
|
||||
],
|
||||
});
|
||||
}
|
||||
function scoreBasis() {
|
||||
openModal({
|
||||
title: '认证评分依据 · L3(REQ-022/024)',
|
||||
body: `<p class="muted">评分由系统自动计算,全部依据可追溯至业务单据;不以缴费培训作为认证条件。</p>
|
||||
<table>
|
||||
<tr><th>依据来源</th><th>指标</th><th>得分贡献</th></tr>
|
||||
<tr><td>ORDER_2026071501 验收记录</td><td>一次验收通过率 95%</td><td>+28</td></tr>
|
||||
<tr><td>DLV_ 质检批次 ×14</td><td>三级质检通过率 / 返工率 4%</td><td>+24</td></tr>
|
||||
<tr><td>TRP_20260802 实训评审</td><td>真实单实训 95 分</td><td>+18</td></tr>
|
||||
<tr><td>EVAL_ 客户评价 ×23</td><td>均分 4.9 / 复购率 62%</td><td>+20</td></tr>
|
||||
<tr><td>CERT_20260042 机构认证</td><td>中级能力附加</td><td>+10</td></tr>
|
||||
<tr><th>综合</th><th>L3 阈值 ≥85</th><th>96 · 达标</th></tr>
|
||||
</table>`,
|
||||
});
|
||||
}
|
||||
function withdraw() {
|
||||
confirmDlg('申请提现', '可提现余额 ¥46,800,提至已核验收款账户(REQ-012 实名关联)。预计 T+1 到账。', '确认提现', () => {
|
||||
toast('提现申请已提交财务通道,到账后将生成结算回单');
|
||||
});
|
||||
}
|
||||
function verifyCert() {
|
||||
const c = DB.opc.cert;
|
||||
openModal({
|
||||
title: '证书查验 · ' + c.no,
|
||||
body: `<table>
|
||||
<tr><th>证书编号</th><td>${c.no}</td></tr>
|
||||
<tr><th>颁发主体</th><td>OPC 公共服务平台</td></tr>
|
||||
<tr><th>认证等级</th><td>${c.level} · ${c.desc}</td></tr>
|
||||
<tr><th>能力标签</th><td>跨境电商 / 视觉设计 / 商品场景图</td></tr>
|
||||
<tr><th>有效期</th><td>2026-04-01 ~ ${c.expire}</td></tr>
|
||||
<tr><th>状态</th><td><span class="badge ok">有效 · 防伪校验通过</span></td></tr>
|
||||
</table>`,
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,155 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>运营工作台 · OPC 公共服务平台 Demo</title>
|
||||
<link rel="stylesheet" href="assets/style.css">
|
||||
</head>
|
||||
<body data-page="operator">
|
||||
<header class="topnav" id="topnav"></header>
|
||||
|
||||
<div class="container">
|
||||
<div class="page-title">运营工作台</div>
|
||||
<div class="page-sub">运营商待办与全业务运营入口 · 审核、仲裁、派单、结算批次</div>
|
||||
|
||||
<div class="grid grid-4" id="op-stats"></div>
|
||||
|
||||
<!-- 业务方向选择与培育(§1.5 步骤①②) -->
|
||||
<div class="section">
|
||||
<h2>业务方向选择与培育 <span class="req">§1.5 ①② · 选方向 → 育人才</span></h2>
|
||||
<div class="card"><table id="op-biz"></table></div>
|
||||
<p class="muted" style="margin-top:8px">运营五步闭环:选方向 → 育人才 → 建平台 → 立 SOP → 聚产业。每个方向有对应 SOP 标准体系与质量指标集,支撑蜂群式交付。</p>
|
||||
</div>
|
||||
|
||||
<!-- 待办审核队列 -->
|
||||
<div class="section">
|
||||
<h2>待办审核队列 <span class="req">REQ-099/100/114/106/060/010 · ADT_ 统一编号</span></h2>
|
||||
<div class="card"><table id="op-audits"></table></div>
|
||||
</div>
|
||||
|
||||
<!-- 项目总览 -->
|
||||
<div class="section">
|
||||
<h2>进行中项目 <span class="req">REQ-031/036 · 运营商统一拆单与质检</span></h2>
|
||||
<div class="card"><table id="op-proj"></table></div>
|
||||
</div>
|
||||
|
||||
<!-- 快捷入口 -->
|
||||
<div class="section">
|
||||
<div class="card">
|
||||
<h3>快捷入口</h3>
|
||||
<p>
|
||||
<a class="btn sm" href="leads.html">供需对接</a>
|
||||
<a class="btn sm" href="order.html">订单协同</a>
|
||||
<a class="btn sm" href="institution.html">机构管理</a>
|
||||
<a class="btn sm" href="market.html">橙果商城审核</a>
|
||||
<a class="btn sm ghost" href="profile.html">画像中心</a>
|
||||
<a class="btn sm ghost" href="showcase.html">社区大屏</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="footer">OPC 公共服务平台 Demo · 运营工作台</div>
|
||||
<script src="assets/app.js"></script>
|
||||
<script src="assets/store.js"></script>
|
||||
<script>
|
||||
function renderPage(DB) {
|
||||
const O = DB.operator;
|
||||
document.getElementById('op-stats').innerHTML = O.stats.map(s =>
|
||||
`<div class="card"><h3>${s.label}</h3><div class="num">${s.num}</div><p class="muted">${s.sub}</p></div>`).join('');
|
||||
renderBizDirections();
|
||||
renderAudits();
|
||||
document.getElementById('op-proj').innerHTML =
|
||||
`<tr><th>项目</th><th>客户</th><th>承接</th><th>进度</th><th>状态</th><th>操作</th></tr>` +
|
||||
O.projects.map(p => `<tr><td>${p.code} ${p.name}</td><td>${p.client}</td><td>${p.opc}</td><td>${BAR(p.pct)}</td><td>${B(p.status)}</td><td><a class="btn sm ghost" href="order.html">进入协同</a></td></tr>`).join('');
|
||||
}
|
||||
|
||||
function renderBizDirections() {
|
||||
const dirs = DB.operator.bizDirections || [];
|
||||
document.getElementById('op-biz').innerHTML =
|
||||
`<tr><th>业务方向</th><th>阶段</th><th>认证 OPC</th><th>在训学员</th><th>累计订单</th><th>收入</th><th>SOP 标准</th><th>状态</th></tr>` +
|
||||
dirs.map(d => `<tr>
|
||||
<td><b>${d.name}</b></td>
|
||||
<td>${d.stage}</td>
|
||||
<td>${d.opcs} 人</td>
|
||||
<td>${d.trainees} 人</td>
|
||||
<td>${d.orders} 单</td>
|
||||
<td>${d.revenue}</td>
|
||||
<td><code>${d.sop}</code></td>
|
||||
<td>${B(d.status)}</td>
|
||||
</tr>`).join('');
|
||||
}
|
||||
|
||||
function renderAudits() {
|
||||
document.getElementById('op-audits').innerHTML =
|
||||
`<tr><th>待办号</th><th>类型</th><th>对象</th><th>要点</th><th>操作</th></tr>` +
|
||||
DB.operator.audits.map((a, i) => `<tr><td>${a.code}</td><td><span class="badge pri">${a.type}</span></td><td>${a.target}</td><td class="muted">${a.detail}</td><td>${
|
||||
a.action === 'arbitrate'
|
||||
? `<button class="btn sm" onclick="arbitrate(${i})">仲裁</button>`
|
||||
: `<button class="btn sm" onclick="approve(${i})">通过</button> <button class="btn sm ghost" onclick="reject(${i})">驳回</button>`
|
||||
}</td></tr>`).join('');
|
||||
}
|
||||
|
||||
function applyAuditResult(a, pass) {
|
||||
// 联动更新对应业务域,演示数据一致性
|
||||
if (a.domain === 'course') {
|
||||
const c = DB.institution.courses.find(x => x.code === 'CRS_0104');
|
||||
if (c) { c.status = pass ? ['已上架', 'ok'] : ['已驳回', 'err']; c.action = pass ? 'edit' : 'withdraw'; }
|
||||
}
|
||||
if (a.domain === 'work') {
|
||||
const w = DB.market.items.find(x => x.code === 'WORK_0060');
|
||||
if (w) { w.status = pass ? ['已上架', 'ok'] : ['已驳回', 'err']; w.action = pass ? 'buy' : 'audit'; }
|
||||
}
|
||||
}
|
||||
|
||||
function approve(i) {
|
||||
const a = DB.operator.audits[i];
|
||||
confirmDlg('审核通过确认', `确认通过「${a.type} · ${a.target}」?结果将写入对应业务台账并留痕。`, '确认通过', () => {
|
||||
applyAuditResult(a, true);
|
||||
DB.operator.audits.splice(i, 1);
|
||||
renderAudits();
|
||||
toast(`已通过:${a.target}${a.domain === 'course' ? '(课程已在机构端同步上架)' : a.domain === 'work' ? '(成果已上架商城)' : ''}`);
|
||||
});
|
||||
}
|
||||
function reject(i) {
|
||||
const a = DB.operator.audits[i];
|
||||
openModal({
|
||||
title: '驳回 · ' + a.target,
|
||||
body: `<div class="field"><label>驳回原因(将反馈给提交方)</label><textarea placeholder="如:材料不全 / 资质不符 / 内容违规"></textarea></div>`,
|
||||
actions: [
|
||||
{ label: '取消', cls: 'ghost' },
|
||||
{ label: '确认驳回', cls: 'danger', onClick: m => {
|
||||
m.remove();
|
||||
applyAuditResult(a, false);
|
||||
DB.operator.audits.splice(i, 1);
|
||||
renderAudits();
|
||||
toast('已驳回并通知提交方', 'warn');
|
||||
} },
|
||||
],
|
||||
});
|
||||
}
|
||||
function arbitrate(i) {
|
||||
const a = DB.operator.audits[i];
|
||||
openModal({
|
||||
title: '争议仲裁 · ' + a.target,
|
||||
body: `<p class="muted">争议点:详情页返工责任归属。双方提交材料已归档。</p>
|
||||
<div class="checks">
|
||||
<label><input type="radio" name="ab" checked> 判定为客户责任(需求变更),返工费用客户承担</label>
|
||||
<label><input type="radio" name="ab"> 判定为 OPC 责任,计入返工率并免费返工</label>
|
||||
<label><input type="radio" name="ab"> 双方分摊(50/50)</label>
|
||||
</div>`,
|
||||
actions: [
|
||||
{ label: '取消', cls: 'ghost' },
|
||||
{ label: '下达仲裁结论', onClick: m => {
|
||||
m.remove();
|
||||
DB.operator.audits.splice(i, 1);
|
||||
renderAudits();
|
||||
toast('仲裁结论已下达并留痕(REQ-060),结算节点已解冻');
|
||||
} },
|
||||
],
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
+285
@@ -0,0 +1,285 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>订单协同与质检 · OPC 公共服务平台 Demo</title>
|
||||
<link rel="stylesheet" href="assets/style.css">
|
||||
</head>
|
||||
<body data-page="order">
|
||||
<header class="topnav" id="topnav"></header>
|
||||
|
||||
<div class="container">
|
||||
<div class="page-title" id="od-title"></div>
|
||||
<div class="page-sub" id="od-sub"></div>
|
||||
|
||||
<!-- REQ-030 订单状态机 -->
|
||||
<div class="card">
|
||||
<h3>订单状态 <span class="req">REQ-030</span>
|
||||
<span style="margin-left:auto">
|
||||
<button class="btn sm" onclick="submitDeliver()">提交交付物</button>
|
||||
<button class="btn sm ghost" onclick="toast('流转留痕:09-01 签约 → 09-03 开工 → 09-08 里程碑1验收通过(演示)')">流转留痕</button>
|
||||
</span>
|
||||
</h3>
|
||||
<div id="od-steps"></div>
|
||||
</div>
|
||||
|
||||
<!-- REQ-033/034 任务拆解与协作组 -->
|
||||
<div class="section">
|
||||
<h2>任务拆解 <span class="req">REQ-033/034 · 协作组 3 人</span></h2>
|
||||
<div class="card">
|
||||
<table id="task-tbl"></table>
|
||||
<div class="notice" style="margin-top:12px" id="risk-notice"></div>
|
||||
<p class="muted" style="margin-top:8px">订单保证金 ¥12,000 已托管 · 延期预警与替补机制生效中(REQ-046);任务支持模板/依赖/暂停/转派(REQ-034)。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 蜂群式交付(§1.5 步骤④) -->
|
||||
<div class="section">
|
||||
<h2>蜂群式交付 <span class="req">§1.5 ④ · SOP 标准体系 · 单兵可替 · 产能可扩</span></h2>
|
||||
<div class="card">
|
||||
<p style="margin-bottom:12px">基于 <code>SOP_电商视觉_V3.2</code> 标准体系,多个 OPC 按统一标准并行交付同一项目;每个交付单元独立可替换,单个 OPC 退出不影响整体产能。</p>
|
||||
<table id="swarm-tbl"></table>
|
||||
<p class="muted" style="margin-top:10px">蜂群交付核心:① 统一 SOP 标准保证质量一致性 ② 任务粒度可拆可替 ③ 产能随 OPC 数量线性扩展 ④ 项目负责人复检 + 运营商终检兜底</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- REQ-036 进度甘特图 -->
|
||||
<div class="section">
|
||||
<div class="card">
|
||||
<h3>进度甘特图 <span class="req">REQ-036 · 红线为今日 09-11</span></h3>
|
||||
<div class="gantt" id="gantt"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- REQ-049 三级质检 -->
|
||||
<div class="section">
|
||||
<div class="two-col">
|
||||
<div class="card">
|
||||
<h3>三级质检 <span class="req">REQ-049/039/054/055</span></h3>
|
||||
<table id="qc-tbl"></table>
|
||||
<p class="muted" style="margin-top:8px">版权与合规审查为内容类订单强制终检项(REQ-054);AI 生成内容须经事实/肖像/商标/有害信息审核(REQ-055)。</p>
|
||||
<p class="muted">返工归因(REQ-057):详情页批次 A → 需求变更(客户新增合规文案要求),不计入 OPC 返工率。</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>质量指标 · 跨境电商视觉 <span class="req">REQ-051/056/058/059</span>
|
||||
<span style="margin-left:auto"><button class="btn sm ghost" onclick="qcRules()">抽检规则</button></span>
|
||||
</h3>
|
||||
<ul class="list" id="qm-list"></ul>
|
||||
<p class="muted" style="margin-top:8px">REQ-052:按时交付率接近阈值,已触发预警观察;质量数据脱敏同步政府驾驶舱与 OPC 能力档案(REQ-058),指标集版本 QI_0002/V3 可扩展(REQ-059)。</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- REQ-035/038 协作与安全 -->
|
||||
<div class="section">
|
||||
<div class="two-col">
|
||||
<div class="card">
|
||||
<h3>文件与沟通 <span class="req">REQ-035</span></h3>
|
||||
<ul class="list" id="file-list"></ul>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>数据安全 <span class="req">REQ-038 · L3 敏感</span></h3>
|
||||
<ul class="list" id="sec-list"></ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="footer">OPC 公共服务平台 Demo · 订单协同与质检</div>
|
||||
<script src="assets/app.js"></script>
|
||||
<script src="assets/store.js"></script>
|
||||
<script>
|
||||
function renderPage(DB) {
|
||||
const D = DB.orderDetail;
|
||||
const role = currentRole();
|
||||
const subMap = {
|
||||
opc: 'OPC 交付方视角 · 提交交付物、三级质检、替补协作',
|
||||
enterprise: '企业发单方视角 · 跟踪进度、验收与争议',
|
||||
personal: '个人发单方视角 · 跟踪进度、验收与争议',
|
||||
operator: '运营商管理视角 · 全链路协同与质检监管',
|
||||
};
|
||||
document.getElementById('od-title').textContent = '订单协同 · ' + D.id;
|
||||
document.getElementById('od-sub').textContent = D.title + (subMap[role] ? ' · ' + subMap[role] : '');
|
||||
document.getElementById('od-steps').innerHTML = STEPS(D.steps);
|
||||
renderTasks();
|
||||
renderSwarm();
|
||||
renderQc();
|
||||
|
||||
document.getElementById('qm-list').innerHTML = D.qualityMetrics.map(q => LI(q[0], B(q[1]))).join('');
|
||||
renderGantt();
|
||||
document.getElementById('file-list').innerHTML = D.files.map(f =>
|
||||
`<li><span>${f[0]}</span><span class="muted">${f[1]}</span></li>`).join('');
|
||||
document.getElementById('sec-list').innerHTML =
|
||||
D.security.map(s => LI(s[0], B(s[1]))).join('') +
|
||||
`<li><span>下载审计日志</span><span><span class="muted">近 7 天 14 次</span> <button class="btn sm ghost" onclick="auditLog()">查看</button></span></li>`;
|
||||
}
|
||||
|
||||
const DAYS = 20, TODAY = 11;
|
||||
function renderGantt() {
|
||||
const D = DB.orderDetail;
|
||||
const ticks = [1, 5, 10, 15, 20].map(d =>
|
||||
`<span style="left:${(d - 0.5) / DAYS * 100}%">09-${String(d).padStart(2, '0')}</span>`).join('');
|
||||
document.getElementById('gantt').innerHTML =
|
||||
`<div class="gantt-row"><span class="gantt-name"></span><div class="gantt-axis">${ticks}</div></div>` +
|
||||
D.tasks.map(t => `
|
||||
<div class="gantt-row">
|
||||
<span class="gantt-name">${t.name}</span>
|
||||
<div class="gantt-track">
|
||||
<div class="gantt-bar ${t.risk ? 'err' : (t.cls || '')}" style="left:${(t.s - 1) / DAYS * 100}%;width:${(t.e - t.s + 1) / DAYS * 100}%">${t.ddl} 止 · ${t.pct}%</div>
|
||||
<div class="gantt-today" style="left:${(TODAY - 0.5) / DAYS * 100}%"></div>
|
||||
</div>
|
||||
</div>`).join('');
|
||||
}
|
||||
|
||||
function qcRules() {
|
||||
openModal({
|
||||
title: '抽检规则配置(REQ-056)',
|
||||
body: `
|
||||
<div class="field-row">
|
||||
<div class="field"><label>抽检比例</label><select><option>10%</option><option selected>30%</option><option>100%(高敏项目默认)</option></select></div>
|
||||
<div class="field"><label>合格阈值</label><select><option>≥95%</option><option selected>≥98%</option><option>≥99%</option></select></div>
|
||||
</div>
|
||||
<div class="field"><label>抽检人</label><select><option>运营商质检组</option><option>交叉质检(其他 L3+ OPC)</option></select></div>
|
||||
<p class="muted">本项目数据等级 L3 敏感,建议全检;规则变更留痕并影响后续批次。</p>`,
|
||||
actions: [
|
||||
{ label: '取消', cls: 'ghost' },
|
||||
{ label: '保存规则', onClick: m => { m.remove(); toast('抽检规则已更新:30% → 全检(L3 高敏默认),新批次生效'); } },
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
function renderSwarm() {
|
||||
const S = DB.orderDetail.swarm || [];
|
||||
document.getElementById('swarm-tbl').innerHTML =
|
||||
`<tr><th>交付单元</th><th>承接 OPC</th><th>等级</th><th>SOP 标准</th><th>状态</th><th>可替换</th></tr>` +
|
||||
S.map(s => `<tr>
|
||||
<td>${s.unit}</td>
|
||||
<td>${s.opc}</td>
|
||||
<td><span class="badge pri">${s.level}</span></td>
|
||||
<td><code>${s.sop}</code></td>
|
||||
<td>${B(s.status)}</td>
|
||||
<td>${s.replaceable ? '<span class="badge ok">可替</span>' : '<span class="badge gray">—</span>'}</td>
|
||||
</tr>`).join('');
|
||||
}
|
||||
|
||||
function renderTasks() {
|
||||
const D = DB.orderDetail;
|
||||
document.getElementById('task-tbl').innerHTML =
|
||||
`<tr><th>任务</th><th>负责人</th><th>前置依赖</th><th>截止</th><th>状态</th><th>进度</th></tr>` +
|
||||
D.tasks.map((t, i) => `<tr${t.risk ? ' id="task-risk"' : ''}><td>${t.name}</td><td>${t.owner}</td><td>${t.dep}</td><td>${t.ddl}</td><td>${B(t.status)}</td><td>${BAR(t.pct, t.cls)}</td></tr>`).join('');
|
||||
renderRiskNotice();
|
||||
}
|
||||
|
||||
function renderRiskNotice() {
|
||||
const t = DB.orderDetail.tasks.find(x => x.risk);
|
||||
const n = document.getElementById('risk-notice');
|
||||
if (!t) { n.style.display = 'none'; return; }
|
||||
n.style.display = '';
|
||||
n.innerHTML = t.taken
|
||||
? `替补已接管:OPC_00061 刘工 接手商品场景图任务,新交期 09-21,已同步客户与项目负责人。`
|
||||
: `REQ-037 预警:${t.name}任务预计逾期 2 天,已通知项目负责人与运营商,替补 OPC_00061 刘工 可接管。
|
||||
<button class="btn sm" style="margin-left:8px" onclick="takeover()">启用替补接管</button>
|
||||
<button class="btn sm ghost" onclick="reassign()">转派</button>`;
|
||||
}
|
||||
|
||||
function renderQc() {
|
||||
const D = DB.orderDetail;
|
||||
document.getElementById('qc-tbl').innerHTML =
|
||||
`<tr><th>交付批次</th><th>① OPC 自检</th><th>② 项目复检</th><th>③ 交付终检</th><th>操作</th></tr>` +
|
||||
D.qc.map((q, i) => `<tr${q.id ? ` id="${q.id}"` : ''}><td>${q.batch}</td><td>${B(q.s1)}</td><td>${B(q.s2)}</td><td>${q.s3 ? B(q.s3) : '—'}</td><td>${qcAction(q, i)}</td></tr>`).join('');
|
||||
}
|
||||
function qcAction(q, i) {
|
||||
if (q.action === 'rework') return `<button class="btn sm ghost" onclick="reworkDetail()">归因</button>`;
|
||||
if (q.action === 'review') return `<button class="btn sm" onclick="reviewPass(${i})">复检通过</button> <button class="btn sm ghost" onclick="reviewFail(${i})">退回</button>`;
|
||||
if (q.action === 'final') return `<button class="btn sm" onclick="finalPass(${i})">终检通过</button>`;
|
||||
return '—';
|
||||
}
|
||||
|
||||
function submitDeliver() {
|
||||
openModal({
|
||||
title: '提交交付物 · SOP 提交清单校验(REQ-039)',
|
||||
body: `
|
||||
<div class="field"><label>所属任务</label><select><option>首页+详情页改版 ×8</option><option>活动 Banner ×12</option></select></div>
|
||||
<div class="checks">
|
||||
<label><input type="checkbox" id="d1" checked> 交付文件齐全(源文件+导出图)</label>
|
||||
<label><input type="checkbox" id="d2" checked> 命名规范符合 SOP_电商视觉_V3.2</label>
|
||||
<label><input type="checkbox" id="d3"> 素材来源记录已填写(版权审查必备)</label>
|
||||
<label><input type="checkbox" id="d4" checked> 自检问题清单已附</label>
|
||||
</div>
|
||||
<div class="field"><label>文件</label><input type="file" multiple></div>`,
|
||||
actions: [
|
||||
{ label: '取消', cls: 'ghost' },
|
||||
{ label: '提交质检', onClick: m => {
|
||||
if (!document.getElementById('d3').checked) { toast('清单不完整:素材来源记录为强制项,无法进入质检', 'err'); return; }
|
||||
m.remove();
|
||||
DB.orderDetail.qc.push({ batch: 'DLV_2026091101 · 详情页批次 B(新提交)', s1: ['自检通过', 'ok'], s2: ['待复检', 'info'], s3: null, action: null });
|
||||
renderQc();
|
||||
toast('清单校验通过,交付物进入①自检→②复检队列');
|
||||
} },
|
||||
],
|
||||
});
|
||||
}
|
||||
function takeover() {
|
||||
confirmDlg('启用替补接管', '将「商品场景图 ×40」移交替补 OPC_00061 刘工?已交付部分与文件版本一并移交,逾期责任按 REQ-057 归因。', '确认接管', () => {
|
||||
const t = DB.orderDetail.tasks.find(x => x.risk);
|
||||
t.owner = 'OPC_00061 刘工(替补)'; t.status = ['已接管 · 排产中', 'info']; t.taken = true;
|
||||
renderTasks();
|
||||
toast('替补接管完成,任务与权限已转移');
|
||||
});
|
||||
}
|
||||
function reassign() {
|
||||
openModal({
|
||||
title: '任务转派',
|
||||
body: `<div class="field"><label>转派给</label><select><option>OPC_00061 刘工(替补 · L2 · 匹配 88%)</option><option>OPC_00045 陈视觉(L2 · 当前负载 80%)</option></select></div>
|
||||
<div class="field"><label>转派原因</label><textarea placeholder="将记录于任务流转留痕"></textarea></div>`,
|
||||
actions: [
|
||||
{ label: '取消', cls: 'ghost' },
|
||||
{ label: '确认转派', onClick: m => { m.remove(); toast('转派申请已提交项目负责人审批'); } },
|
||||
],
|
||||
});
|
||||
}
|
||||
function reviewPass(i) {
|
||||
const q = DB.orderDetail.qc[i];
|
||||
q.s2 = ['通过', 'ok']; q.s3 = ['待终检', 'info']; q.action = 'final';
|
||||
renderQc();
|
||||
toast('②复检通过,进入③交付终检');
|
||||
}
|
||||
function reviewFail(i) {
|
||||
const q = DB.orderDetail.qc[i];
|
||||
q.s2 = ['返工', 'err']; q.action = 'rework';
|
||||
renderQc();
|
||||
toast('已退回返工,请填写责任归因(REQ-057)', 'warn');
|
||||
}
|
||||
function finalPass(i) {
|
||||
const q = DB.orderDetail.qc[i];
|
||||
q.s3 = ['通过', 'ok']; q.action = null;
|
||||
renderQc();
|
||||
toast('③终检通过,批次可提交客户验收');
|
||||
}
|
||||
function reworkDetail() {
|
||||
openModal({
|
||||
title: '返工归因 · 详情页批次 A(REQ-057)',
|
||||
body: `<div class="checks">
|
||||
<label><input type="radio" name="rw" checked> 需求变更(客户新增合规文案)</label>
|
||||
<label><input type="radio" name="rw"> OPC 执行问题</label>
|
||||
<label><input type="radio" name="rw"> 质检遗漏</label>
|
||||
<label><input type="radio" name="rw"> 客户其他原因</label>
|
||||
</div><p class="muted">归因结果用于费用与信用结算:本次不计入 OPC 返工率。</p>`,
|
||||
actions: [{ label: '确认归因', onClick: m => { m.remove(); toast('归因已记录:需求变更 · 客户承担返工费用'); } }],
|
||||
});
|
||||
}
|
||||
function auditLog() {
|
||||
openModal({
|
||||
title: '下载审计日志(近 7 天)',
|
||||
body: `<table>
|
||||
<tr><th>时间</th><th>操作人</th><th>动作</th></tr>
|
||||
<tr><td>09-11 14:02</td><td>OPC_00017 张设计</td><td>下载 首页改版_v2.psd</td></tr>
|
||||
<tr><td>09-10 09:31</td><td>项目负责人</td><td>导出 交付清单</td></tr>
|
||||
<tr><td>09-09 16:47</td><td>运营商质检员</td><td>下载 DLV_2026090901 源文件</td></tr>
|
||||
</table>`,
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,225 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>政策服务 · OPC 公共服务平台 Demo</title>
|
||||
<link rel="stylesheet" href="assets/style.css">
|
||||
</head>
|
||||
<body data-page="policy">
|
||||
<header class="topnav" id="topnav"></header>
|
||||
|
||||
<div class="container">
|
||||
<div class="page-title">政策服务</div>
|
||||
<div class="page-sub">政策库 · 自动匹配 · 申报闭环 · 资源券核销</div>
|
||||
|
||||
<!-- REQ-072/077 AI 自动匹配 -->
|
||||
<div class="card" style="background:#eef2ff;border-color:#c7d2fe">
|
||||
<h3>主体画像自动匹配 <span class="req">REQ-072/077</span></h3>
|
||||
<p class="muted" id="pol-profile"></p>
|
||||
<table id="match-tbl"></table>
|
||||
<p class="muted" style="margin-top:8px">confidence < 0.6 的匹配结果提示人工核对,不编造政策条款(REQ-077)。</p>
|
||||
</div>
|
||||
|
||||
<!-- 金融/园区方资源产品(finance/operator 角色) -->
|
||||
<div class="section" id="fin-section" style="display:none">
|
||||
<h2>我的资源产品 <span class="req">REQ-076 · 资源供给方视图</span></h2>
|
||||
<div class="grid grid-3" id="fin-grid"></div>
|
||||
</div>
|
||||
|
||||
<!-- REQ-071/075 政策库 -->
|
||||
<div class="section">
|
||||
<h2>政策库 <span class="req">REQ-071/075</span></h2>
|
||||
<div class="card">
|
||||
<table id="lib-tbl"></table>
|
||||
<p class="muted" style="margin-top:8px">失效政策标注并保留历史版本,已申报记录保留政策快照(REQ-080)。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- REQ-074/076 申报流程 + 资源券闭环 -->
|
||||
<div class="section">
|
||||
<div class="two-col">
|
||||
<div class="card">
|
||||
<h3>我的申报 <span class="req">REQ-074</span></h3>
|
||||
<ul class="list" id="apply-list"></ul>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>资源券台账 <span class="req">REQ-076 · 发放—使用—核销—绩效</span></h3>
|
||||
<ul class="list" id="coupon-list"></ul>
|
||||
<button class="btn sm" style="margin-top:8px" onclick="useCoupon()">券核销支付(演示 ¥500)</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- REQ-079 防套利 -->
|
||||
<div class="section">
|
||||
<div class="notice">REQ-079:重复申报、虚假申报自动预警——申报结果计入主体信用与绩效台账,与实名、合同、验收、收款、发票交叉核验。</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="footer">OPC 公共服务平台 Demo · 政策服务</div>
|
||||
<script src="assets/app.js"></script>
|
||||
<script src="assets/store.js"></script>
|
||||
<script>
|
||||
function renderPage(DB) {
|
||||
const P = DB.policy;
|
||||
const role = currentRole();
|
||||
const isFinance = ['finance', 'operator'].includes(role);
|
||||
document.getElementById('pol-profile').textContent = isFinance
|
||||
? '供给方视图:' + P.finance.org + ' · 资源产品核销与主体匹配情况'
|
||||
: '画像:' + P.profile;
|
||||
|
||||
// 金融/园区资源产品区
|
||||
const finSec = document.getElementById('fin-section');
|
||||
finSec.style.display = isFinance ? '' : 'none';
|
||||
if (isFinance) {
|
||||
document.getElementById('fin-grid').innerHTML = P.finance.products.map((p, i) => `
|
||||
<div class="card"><h3>${p.name}</h3><div class="sub">${p.code}</div>
|
||||
<p class="muted">额度 ${p.quota} · ${p.used}</p>${B(p.status)}
|
||||
<div style="margin-top:10px"><button class="btn sm ghost" onclick="finDetail(${i})">核销明细</button>
|
||||
<button class="btn sm ghost" onclick="finPublish()">发布资源</button></div></div>`).join('');
|
||||
}
|
||||
|
||||
const acts = {
|
||||
apply: p => `<button class="btn sm" onclick="apply(this,'${p.name}')">一键申报</button>`,
|
||||
coupon: p => `<button class="btn sm" onclick="coupon(this)">申领</button>`,
|
||||
agent: p => `<button class="btn sm ghost" onclick="agent(this)">咨询代办</button>`,
|
||||
};
|
||||
document.getElementById('match-tbl').innerHTML =
|
||||
`<tr><th>命中政策</th><th>命中依据</th><th>缺失条件</th><th>置信度</th><th>操作</th></tr>` +
|
||||
P.matched.map(p => `<tr><td>${p.name}</td><td>${p.basis}</td><td>${p.missing}</td><td>${B(p.conf)}</td><td>${acts[p.action](p)}</td></tr>`).join('');
|
||||
|
||||
document.getElementById('lib-tbl').innerHTML =
|
||||
`<tr><th>政策</th><th>发文机关</th><th>适用对象</th><th>补贴额度</th><th>截止</th><th>状态</th></tr>` +
|
||||
P.library.map((p, i) => `<tr><td>${p.name}</td><td>${p.org}</td><td>${p.target}</td><td>${p.quota}</td><td>${p.ddl}</td><td>${B(p.status)} ${
|
||||
p.snapshot
|
||||
? `<button class="btn sm ghost" onclick="toast('已加载政策快照 V2025:该版本为历史申报记录存档(REQ-080)')">快照</button>`
|
||||
: `<button class="btn sm ghost" onclick="policyDetail(${i})">详情</button>`
|
||||
}</td></tr>`).join('');
|
||||
|
||||
renderApplications();
|
||||
renderCoupon();
|
||||
}
|
||||
|
||||
function finDetail(i) {
|
||||
const p = DB.policy.finance.products[i];
|
||||
openModal({
|
||||
title: '核销明细 · ' + p.code,
|
||||
body: `<table>
|
||||
<tr><th>产品</th><td>${p.name}</td></tr>
|
||||
<tr><th>总额度</th><td>${p.quota}</td></tr>
|
||||
<tr><th>使用进度</th><td>${p.used}</td></tr>
|
||||
<tr><th>近 30 天核销</th><td>VCH_ 关联有效订单 96 单 · 按产出核销</td></tr>
|
||||
<tr><th>异常预警</th><td><span class="badge ok">无重复申报/套利预警(REQ-079)</span></td></tr>
|
||||
</table>`,
|
||||
});
|
||||
}
|
||||
function finPublish() {
|
||||
openModal({
|
||||
title: '发布资源产品(金融/园区方)',
|
||||
body: `<div class="field"><label>资源名称</label><input placeholder="如:模型券池 / 共享工位"></div>
|
||||
<div class="field-row">
|
||||
<div class="field"><label>额度/数量</label><input placeholder="¥ 或 工位数"></div>
|
||||
<div class="field"><label>适用对象</label><select><option>L2 以上认证 OPC</option><option>全部 OPC</option><option>入驻企业</option></select></div>
|
||||
</div>`,
|
||||
actions: [
|
||||
{ label: '取消', cls: 'ghost' },
|
||||
{ label: '提交发布', onClick: m => { m.remove(); toast('资源产品已提交,运营商审核后进入政策库与券台账'); } },
|
||||
],
|
||||
});
|
||||
}
|
||||
function renderApplications() {
|
||||
document.getElementById('apply-list').innerHTML = DB.policy.applications.map(a =>
|
||||
`<li><span>${a.name}</span><span>${
|
||||
a.action === 'upload' ? `<button class="btn sm" onclick="uploadMat(this)">上传发票</button>` : B(a.badge)
|
||||
}</span></li>`).join('');
|
||||
}
|
||||
|
||||
function renderCoupon() {
|
||||
const c = DB.policy.coupon;
|
||||
document.getElementById('coupon-list').innerHTML =
|
||||
LI('算力券余额', `<span id="coupon-bal">¥${c.balance.toLocaleString()} / ¥${c.total.toLocaleString()}</span>`) +
|
||||
LI('已核销产出', `<span class="muted">${c.linked}</span>`) +
|
||||
LI('绩效口径', B(['按使用量+实际产出', 'pri']));
|
||||
}
|
||||
|
||||
function apply(btn, name) {
|
||||
const p = DB.policy.matched.find(x => x.name === name);
|
||||
openModal({
|
||||
title: '一键申报 · ' + name,
|
||||
body: `<p class="muted">申报清单(REQ-074),材料模板可在线填写:</p>
|
||||
<div class="checks">${p.materials.map(x => `<label><input type="checkbox" class="mat" checked> ${x}(已预填,可补充)</label>`).join('')}</div>
|
||||
<div class="field"><label>补充材料</label><input type="file"></div>`,
|
||||
actions: [
|
||||
{ label: '取消', cls: 'ghost' },
|
||||
{ label: '提交申报', onClick: m => {
|
||||
m.remove();
|
||||
btn.outerHTML = '<span class="badge warn">审核中(人社)</span>';
|
||||
DB.policy.applications.unshift({ code: 'APLY_2026091201', name, badge: ['审核中(人社)', 'warn'] });
|
||||
renderApplications();
|
||||
toast('申报已提交,进度与结果将归档(REQ-074)');
|
||||
} },
|
||||
],
|
||||
});
|
||||
}
|
||||
function coupon(btn) {
|
||||
confirmDlg('申领算力券', '按细则可申领 ≤¥10,000/年。申领后进入「发放—使用—核销—绩效」闭环,按实际产出核销(REQ-076)。', '确认申领', () => {
|
||||
btn.outerHTML = '<span class="badge warn">发放审核中</span>';
|
||||
DB.policy.applications.unshift({ code: 'APLY_2026091202', name: '算力券申领(第 5 批)', badge: ['发放审核中', 'warn'] });
|
||||
renderApplications();
|
||||
toast('申领已提交,经信局审核后发放至券台账');
|
||||
});
|
||||
}
|
||||
function agent(btn) {
|
||||
openModal({
|
||||
title: '政策落实代办(REQ-078)',
|
||||
body: `<p>该政策置信度 0.55 需人工核对。可由运营商或合作机构协助申报,代办过程留痕。</p>
|
||||
<div class="field"><label>代办事项</label><textarea>小微企业创业担保贷款 · 担保材料整理与申报</textarea></div>`,
|
||||
actions: [
|
||||
{ label: '取消', cls: 'ghost' },
|
||||
{ label: '申请代办', onClick: m => { m.remove(); btn.outerHTML = '<span class="badge info">代办中</span>'; toast('代办工单已创建,专员将在 1 个工作日内联系'); } },
|
||||
],
|
||||
});
|
||||
}
|
||||
function policyDetail(i) {
|
||||
const p = DB.policy.library[i];
|
||||
openModal({
|
||||
title: p.name,
|
||||
body: `<table>
|
||||
<tr><th>发文机关</th><td>${p.org}</td></tr>
|
||||
<tr><th>申报条件</th><td>${p.conds.map(c => '· ' + c).join('<br>')}</td></tr>
|
||||
<tr><th>材料清单</th><td>${p.mats.map(c => '· ' + c).join('<br>')}</td></tr>
|
||||
<tr><th>政策快照</th><td>V2026-09 · 条款随申报记录存档(REQ-080)</td></tr>
|
||||
</table>`,
|
||||
actions: [
|
||||
{ label: '关闭', cls: 'ghost' },
|
||||
{ label: '检测匹配', onClick: m => { m.remove(); toast('主体画像匹配重算完成:命中条件已更新(REQ-073)'); } },
|
||||
],
|
||||
});
|
||||
}
|
||||
function uploadMat(btn) {
|
||||
openModal({
|
||||
title: '上传补充材料 · 算力券申领',
|
||||
body: `<p class="muted">缺失材料:近 3 个月算力消费发票</p><div class="field"><label>发票文件</label><input type="file"></div>`,
|
||||
actions: [
|
||||
{ label: '取消', cls: 'ghost' },
|
||||
{ label: '提交', onClick: m => {
|
||||
m.remove();
|
||||
const a = DB.policy.applications.find(x => x.action === 'upload');
|
||||
if (a) { delete a.action; a.badge = ['材料已齐 · 审核中', 'warn']; }
|
||||
renderApplications();
|
||||
toast('材料 4/4 已齐,进入审核');
|
||||
} },
|
||||
],
|
||||
});
|
||||
}
|
||||
function useCoupon() {
|
||||
confirmDlg('算力券核销', '本次算力消费 ¥500 使用算力券支付?核销将关联订单产出计入绩效。', '确认核销', () => {
|
||||
DB.policy.coupon.balance -= 500;
|
||||
renderCoupon();
|
||||
toast('核销成功:已关联 ORDER_2026091001 交付产出');
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,72 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>画像中心 · OPC 公共服务平台 Demo</title>
|
||||
<link rel="stylesheet" href="assets/style.css">
|
||||
</head>
|
||||
<body data-page="profile">
|
||||
<header class="topnav" id="topnav"></header>
|
||||
|
||||
<div class="container">
|
||||
<div class="page-title">主题画像中心</div>
|
||||
<div class="page-sub" id="pf-sub">OPC / 企业 / 订单 / 学员 核心主题统一画像 · AI 自动匹配的数据底座(REQ-116)</div>
|
||||
|
||||
<!-- REQ-116 主题画像 -->
|
||||
<div class="grid grid-2" id="pf-grid"></div>
|
||||
|
||||
<!-- REQ-117 AI 匹配演示 -->
|
||||
<div class="section">
|
||||
<h2>AI 自动匹配演示 <span class="req">REQ-117 · score + evidence + confidence</span></h2>
|
||||
<div class="card">
|
||||
<p class="muted" id="md-req"></p>
|
||||
<table id="md-tbl"></table>
|
||||
<p class="muted" style="margin-top:8px">confidence < 0.6 提示人工核对;匹配依据可追溯,不编造(REQ-117)。画像维度随业务行为自动更新(REQ-116)。</p>
|
||||
<button class="btn sm" onclick="runMatch()">重新计算匹配</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="footer">OPC 公共服务平台 Demo · 主题画像中心</div>
|
||||
<script src="assets/app.js"></script>
|
||||
<script src="assets/store.js"></script>
|
||||
<script>
|
||||
function renderPage(DB) {
|
||||
const P = DB.profiles;
|
||||
const role = currentRole();
|
||||
const subMap = {
|
||||
gov: '监管视角 · 脱敏主题画像与 AI 匹配监测(REQ-116/117)',
|
||||
enterprise: '需求方视角 · 查看候选 OPC 画像与 AI 匹配结果(REQ-117)',
|
||||
operator: '运营管理视角 · 全量主题画像与 AI 匹配数据底座(REQ-116/117)',
|
||||
};
|
||||
if (subMap[role]) document.getElementById('pf-sub').textContent = subMap[role];
|
||||
document.getElementById('pf-grid').innerHTML = P.entities.map(e => `
|
||||
<div class="card">
|
||||
<h3>${e.type} · ${e.name} <span class="req">${e.code}</span></h3>
|
||||
<ul class="list">
|
||||
${e.dims.map(d => `<li><span>${d[0]}</span><span class="muted">${d[1]}</span></li>`).join('')}
|
||||
</ul>
|
||||
</div>`).join('');
|
||||
|
||||
document.getElementById('md-req').textContent = '输入 · ' + P.matchDemo.req;
|
||||
renderMatch();
|
||||
}
|
||||
|
||||
function renderMatch() {
|
||||
document.getElementById('md-tbl').innerHTML =
|
||||
`<tr><th>候选 OPC</th><th>匹配分</th><th>依据(evidence)</th><th>置信度</th></tr>` +
|
||||
DB.profiles.matchDemo.results.map(r => `<tr><td>${r.target}</td><td><b>${r.score}</b></td><td>${r.evidence}</td><td>${
|
||||
r.conf === 'high' ? '<span class="badge ok">高 · 可直接派单</span>' : '<span class="badge warn">中 · 建议人工复核</span>'
|
||||
}</td></tr>`).join('');
|
||||
}
|
||||
let matching = false;
|
||||
function runMatch() {
|
||||
if (matching) return;
|
||||
matching = true;
|
||||
document.getElementById('md-tbl').innerHTML = '<tr><td colspan="4">AI 正在基于主题画像计算匹配…</td></tr>';
|
||||
setTimeout(() => { renderMatch(); toast('匹配重算完成:TOP1 OPC_00017(96 分,可直接派单)'); matching = false; }, 900);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,202 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>企业服务中心 · OPC 公共服务平台 Demo</title>
|
||||
<link rel="stylesheet" href="assets/style.css">
|
||||
</head>
|
||||
<body data-page="service">
|
||||
<header class="topnav" id="topnav"></header>
|
||||
|
||||
<div class="container">
|
||||
<div class="page-title">企业服务中心</div>
|
||||
<div class="page-sub" id="svc-sub"></div>
|
||||
|
||||
<!-- REQ-061/062 外包服务目录 -->
|
||||
<div class="section">
|
||||
<h2>外包服务目录 <span class="req">REQ-061/062 · 标准化服务包</span></h2>
|
||||
<div class="grid grid-3" id="svc-catalog"></div>
|
||||
</div>
|
||||
|
||||
<!-- REQ-063 服务工单 -->
|
||||
<div class="section">
|
||||
<h2>我的服务工单 <span class="req">REQ-063</span></h2>
|
||||
<div class="card">
|
||||
<table id="svc-tbl"></table>
|
||||
<p class="muted" style="margin-top:8px">工单状态:受理 → 办理中 → 待补充材料 → 已完成 / 异常(REQ-063);凭证电子化归档(REQ-069);外包服务质量计入经营档案(REQ-064);服务费可与订单收入联动抵扣/代扣(REQ-068)</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- REQ-065/066 机构超市 -->
|
||||
<div class="section">
|
||||
<h2>服务机构超市 <span class="req">REQ-065/066 · 资质审核入驻</span></h2>
|
||||
<div class="card">
|
||||
<table id="org-tbl"></table>
|
||||
<p class="muted" style="margin-top:8px">机构经资质审核后方可上架,平台实施评价与退出管理,不做未经审查的广告展示(REQ-066)。
|
||||
<button class="btn sm" style="float:right" onclick="newSvc()">+ 上架新服务(机构角色)</button></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 服务机构工作台(service/operator 角色) -->
|
||||
<div class="section" id="vendor-section" style="display:none">
|
||||
<h2>机构工单处理 <span class="req">REQ-063/064 · 服务机构侧</span></h2>
|
||||
<div class="grid grid-4" id="vendor-stats" style="margin-bottom:16px"></div>
|
||||
<div class="card"><table id="vendor-tbl"></table></div>
|
||||
</div>
|
||||
|
||||
<!-- REQ-067 数据安全提示 -->
|
||||
<div class="section">
|
||||
<div class="notice">REQ-067:涉及财务、人事等敏感经营数据的外包服务,须签署数据处理协议并按 L3/L4 级数据管控。</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="footer">OPC 公共服务平台 Demo · 企业服务中心</div>
|
||||
<script src="assets/app.js"></script>
|
||||
<script src="assets/store.js"></script>
|
||||
<script>
|
||||
let svcSeq = 13;
|
||||
|
||||
function renderPage(DB) {
|
||||
const S = DB.service;
|
||||
const role = currentRole();
|
||||
document.getElementById('svc-sub').textContent =
|
||||
role === 'service' ? '服务机构视图 · 资质上架、工单办理、评价与退出管理'
|
||||
: role === 'opc' ? '自营外包服务 + 第三方服务机构超市 · 为 OPC 提供经营支撑'
|
||||
: '自营外包服务 + 第三方服务机构超市 · 标准化服务包';
|
||||
document.getElementById('svc-catalog').innerHTML = S.catalog.map(c => `
|
||||
<div class="card"><h3>${c.name} ${B(c.badge)}</h3><p class="muted">${c.desc}</p>
|
||||
${c.action === 'insPlan'
|
||||
? `<button class="btn sm" onclick="insPlan()">查看方案</button>`
|
||||
: `<button class="btn sm" onclick="buy('${c.svcName || c.name}','${c.vendor}','${c.price}')">购买服务包</button>`}
|
||||
</div>`).join('');
|
||||
|
||||
renderWorkorders();
|
||||
renderVendor(role);
|
||||
|
||||
document.getElementById('org-tbl').innerHTML =
|
||||
`<tr><th>机构</th><th>类目</th><th>资质状态</th><th>服务评价</th><th>在架服务</th><th>操作</th></tr>` +
|
||||
S.orgs.map(o => `<tr><td>${o.name}</td><td>${o.cat}</td><td>${B(o.qual)}</td><td>${o.rating}</td><td>${o.svcs}</td><td>${orgAction(o)}</td></tr>`).join('');
|
||||
}
|
||||
|
||||
function orgAction(o) {
|
||||
if (o.action === 'detail') return `<button class="btn sm ghost" onclick="orgDetail('${o.name}','${o.license}','${o.score}','${o.complaint}')">资质详情</button>`;
|
||||
if (o.action === 'coupon') return `<button class="btn sm ghost" onclick="toast('支持算力券/模型券核销,接口对接已完成(REQ-076)')">券核销</button>`;
|
||||
if (o.action === 'urge') return `<button class="btn sm ghost" onclick="toast('已向机构发送年审提醒,逾期将自动下架服务','warn')">催审</button>`;
|
||||
return '—';
|
||||
}
|
||||
|
||||
function renderVendor(role) {
|
||||
const show = ['service', 'operator'].includes(role);
|
||||
document.getElementById('vendor-section').style.display = show ? '' : 'none';
|
||||
if (!show) return;
|
||||
const V = DB.service.myVendor;
|
||||
document.getElementById('vendor-stats').innerHTML = V.stats.map(s =>
|
||||
`<div class="card"><h3>${s.label}</h3><div class="num">${s.num}</div><p class="muted">${s.sub}</p></div>`).join('');
|
||||
document.getElementById('vendor-tbl').innerHTML =
|
||||
`<tr><th>工单号</th><th>事项</th><th>客户/需求</th><th>状态</th><th>操作</th></tr>` +
|
||||
V.tickets.map((t, i) => `<tr><td>${t.code}</td><td>${t.item}</td><td>${t.need}</td><td>${B(t.status)}</td><td>${vendorAction(t, i)}</td></tr>`).join('');
|
||||
}
|
||||
function vendorAction(t, i) {
|
||||
if (t.action === 'accept') return `<button class="btn sm" onclick="ticketAct(${i},'accept')">受理</button> <button class="btn sm ghost" onclick="ticketAct(${i},'reject')">转派</button>`;
|
||||
if (t.action === 'finish') return `<button class="btn sm" onclick="ticketAct(${i},'finish')">办结</button>`;
|
||||
if (t.action === 'urge') return `<button class="btn sm ghost" onclick="toast('已向客户发送补料提醒')">催补料</button>`;
|
||||
return '—';
|
||||
}
|
||||
function ticketAct(i, act) {
|
||||
const t = DB.service.myVendor.tickets[i];
|
||||
if (act === 'reject') { confirmDlg('工单转派', `将 ${t.code} 转派给其他机构/自营?将通知客户并留痕。`, '确认转派', () => { t.status = ['已转派', 'gray']; t.action = null; renderVendor(currentRole()); toast('工单已转派'); }); return; }
|
||||
const label = act === 'accept' ? '受理' : '办结';
|
||||
confirmDlg(`工单${label}`, `确认${label} ${t.code}(${t.item})?`, '确认', () => {
|
||||
t.status = act === 'accept' ? ['办理中', 'warn'] : ['已完成', 'ok'];
|
||||
t.action = act === 'accept' ? 'finish' : null;
|
||||
renderVendor(currentRole());
|
||||
toast(`工单已${label},服务记录计入经营档案(REQ-064)`);
|
||||
});
|
||||
}
|
||||
|
||||
function renderWorkorders() {
|
||||
document.getElementById('svc-tbl').innerHTML =
|
||||
`<tr><th>工单号</th><th>服务</th><th>服务商</th><th>状态</th><th>凭证归档</th><th>评价</th></tr>` +
|
||||
DB.service.workorders.map(w => `<tr><td>${w.id}</td><td>${w.svc}</td><td>${w.vendor}</td><td>${B(w.status)}</td><td>${
|
||||
w.doc === 'policy' ? `<button class="btn sm ghost" onclick="toast('保单 INS_2026080101.pdf 已下载(演示)')">保单已归档</button>` : w.doc
|
||||
}</td><td>${
|
||||
w.rate === 'supply' ? `<button class="btn sm" onclick="supply(this)">补材料</button>` : w.rate
|
||||
}</td></tr>`).join('');
|
||||
}
|
||||
|
||||
function buy(name, provider, price) {
|
||||
openModal({
|
||||
title: '购买服务包 · ' + name,
|
||||
body: `<table>
|
||||
<tr><th>服务商</th><td>${provider}</td></tr>
|
||||
<tr><th>价格</th><td>${price}</td></tr>
|
||||
<tr><th>SLA</th><td>申报/办理及时率 100%;差错率 ≤0.5%;投诉率 ≤1%</td></tr>
|
||||
<tr><th>交付物</th><td>服务结果 + 电子化凭证归档(REQ-069)</td></tr>
|
||||
</table>
|
||||
<div class="checks"><label><input type="checkbox" id="dpa"> 同意签署数据处理协议(涉及敏感经营数据时必选 REQ-067)</label></div>`,
|
||||
actions: [
|
||||
{ label: '取消', cls: 'ghost' },
|
||||
{ label: '确认下单', onClick: m => {
|
||||
m.remove();
|
||||
const id = 'SVC_202609' + String(svcSeq++).padStart(2, '0') + '01';
|
||||
DB.service.workorders.push({ id, svc: name, vendor: provider, status: ['受理', 'info'], doc: '—', rate: '—' });
|
||||
renderWorkorders();
|
||||
toast(`服务工单已创建:${id},状态可在工单列表跟踪`);
|
||||
} },
|
||||
],
|
||||
});
|
||||
}
|
||||
function insPlan() {
|
||||
const rows = DB.service.insPlans.map(p =>
|
||||
`<tr><td>${p.plan}</td><td>${p.for}</td><td>${p.cover}</td><td>${p.price}</td></tr>`).join('');
|
||||
openModal({
|
||||
title: '商保方案推荐 · 按 OPC 等级差异化(REQ-070)',
|
||||
body: `<table><tr><th>方案</th><th>适用</th><th>保障</th><th>保费</th></tr>${rows}</table>`,
|
||||
actions: [
|
||||
{ label: '关闭', cls: 'ghost' },
|
||||
{ label: '投保进阶版', onClick: m => { m.remove(); toast('投保申请已提交,承保进度可在工单跟踪'); } },
|
||||
],
|
||||
});
|
||||
}
|
||||
function orgDetail(name, license, score, complaint) {
|
||||
openModal({
|
||||
title: '机构资质 · ' + name,
|
||||
body: `<table>
|
||||
<tr><th>资质证照</th><td>${license}(已核验)</td></tr>
|
||||
<tr><th>服务评价</th><td>${score} / 5.0 · 投诉率 ${complaint}</td></tr>
|
||||
<tr><th>入驻审核</th><td>2026-03 通过 · 年审周期 12 个月</td></tr>
|
||||
<tr><th>退出条款</th><td>投诉率连续超标或资质失效即下架(REQ-066)</td></tr>
|
||||
</table>`,
|
||||
});
|
||||
}
|
||||
function supply(btn) {
|
||||
openModal({
|
||||
title: '补充材料 · SVC_2026071201',
|
||||
body: `<p class="muted">代理机构要求补充:商标图样高清版(JPG ≥2000px)</p>
|
||||
<div class="field"><label>上传材料</label><input type="file"></div>`,
|
||||
actions: [
|
||||
{ label: '取消', cls: 'ghost' },
|
||||
{ label: '提交', onClick: m => { m.remove(); btn.outerHTML = '<span class="badge warn">办理中</span>'; toast('材料已提交,工单恢复办理中'); } },
|
||||
],
|
||||
});
|
||||
}
|
||||
function newSvc() {
|
||||
openModal({
|
||||
title: '上架新服务(服务机构角色)',
|
||||
body: `
|
||||
<div class="field"><label>服务名称</label><input placeholder="如:高新认定辅导"></div>
|
||||
<div class="field-row">
|
||||
<div class="field"><label>类目</label><select><option>财税</option><option>法务</option><option>知识产权</option><option>人力</option><option>融资</option></select></div>
|
||||
<div class="field"><label>价格</label><input placeholder="¥ / 计价方式"></div>
|
||||
</div>
|
||||
<div class="field"><label>SLA 与交付物</label><textarea placeholder="服务时效、交付标准、投诉通道"></textarea></div>`,
|
||||
actions: [
|
||||
{ label: '取消', cls: 'ghost' },
|
||||
{ label: '提交审核', onClick: m => { m.remove(); toast('服务已提交,运营商资质+内容审核通过后上架'); } },
|
||||
],
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,130 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>OPC 社区驾驶舱 · 展览大屏</title>
|
||||
<link rel="stylesheet" href="assets/style.css">
|
||||
</head>
|
||||
<body data-page="showcase" class="gov screen">
|
||||
<header class="topnav" id="topnav"></header>
|
||||
|
||||
<div class="container">
|
||||
<div class="screen-hero">
|
||||
<h1>OPC 社区驾驶舱</h1>
|
||||
<p>一人公司 · 超级个体产业社区 · 展览区公开展示 · 全部数据脱敏汇总(REQ-118)</p>
|
||||
</div>
|
||||
|
||||
<!-- 实时动态 -->
|
||||
<div class="ticker-wrap"><div class="ticker" id="sc-ticker"></div></div>
|
||||
|
||||
<!-- 核心指标 -->
|
||||
<div class="section grid grid-3" id="sc-kpis"></div>
|
||||
|
||||
<div class="section">
|
||||
<div class="two-col">
|
||||
<!-- 业务模式分布 -->
|
||||
<div class="card">
|
||||
<h3>业务模式分布 · 按订单额</h3>
|
||||
<div id="sc-mix"></div>
|
||||
</div>
|
||||
<!-- 供需转化 -->
|
||||
<div class="card">
|
||||
<h3>供需转化(本季度)</h3>
|
||||
<div class="funnel" id="sc-funnel"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<div class="two-col">
|
||||
<!-- 明星 OPC -->
|
||||
<div class="card">
|
||||
<h3>明星 OPC 榜 <span class="req">脱敏展示</span></h3>
|
||||
<table id="sc-top"></table>
|
||||
</div>
|
||||
<!-- 橙果商城热榜 -->
|
||||
<div class="card">
|
||||
<h3>OPC橙果商城 · 热销成果</h3>
|
||||
<ul class="list" id="sc-market"></ul>
|
||||
<a class="btn sm ghost" style="margin-top:10px" href="market.html">进入商城 →</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 产业集聚效应(§1.5 步骤⑤) -->
|
||||
<div class="section">
|
||||
<h2 style="color:#e2e8f0">产业集聚效应 <span class="req">§1.5 ⑤ · 标杆 + 上下游 + 复购飞轮</span></h2>
|
||||
<div class="two-col">
|
||||
<div class="card">
|
||||
<h3>上游生态</h3>
|
||||
<table id="sc-upstream"></table>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>下游产能</h3>
|
||||
<table id="sc-downstream"></table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card" style="margin-top:14px">
|
||||
<h3>产业链条 · 按业务方向</h3>
|
||||
<table id="sc-chain"></table>
|
||||
</div>
|
||||
<div class="card" style="margin-top:14px">
|
||||
<h3>标杆带动 <span class="req">REQ-005</span></h3>
|
||||
<p id="sc-benchmark" style="font-size:13px;line-height:1.8"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="footer">OPC 公共服务平台 Demo · 展览区大屏 · 数据每 5 分钟刷新(演示为静态)</div>
|
||||
<script src="assets/app.js"></script>
|
||||
<script src="assets/store.js"></script>
|
||||
<script>
|
||||
function renderPage(DB) {
|
||||
const S = DB.showcase;
|
||||
|
||||
// 滚动条:内容复制一遍实现无缝滚动
|
||||
const items = S.ticker.map(t => `<span>◆ ${t}</span>`).join('');
|
||||
document.getElementById('sc-ticker').innerHTML = items + items;
|
||||
|
||||
document.getElementById('sc-kpis').innerHTML = S.kpis.map(k =>
|
||||
`<div class="card"><h3>${k.label}</h3><div class="num">${k.num}</div></div>`).join('');
|
||||
|
||||
document.getElementById('sc-mix').innerHTML = S.bizMix.map(m => `
|
||||
<div style="margin-bottom:12px">
|
||||
<div style="display:flex;justify-content:space-between;font-size:13px"><span>${m[0]}</span><span>${m[1]}%</span></div>
|
||||
${BAR(m[1])}
|
||||
</div>`).join('');
|
||||
|
||||
document.getElementById('sc-funnel').innerHTML = S.funnel.map((f, i) => `
|
||||
<div class="funnel-row">
|
||||
<span class="funnel-stage" style="color:#94a3b8">${f[0]}</span>
|
||||
<div class="funnel-bar" style="width:${100 - i * 22}%;background:#0ea5e9">${f[1]}</div>
|
||||
${i ? `<span class="funnel-conv">转化 ${(f[1] / S.funnel[i-1][1] * 100).toFixed(0)}%</span>` : ''}
|
||||
</div>`).join('');
|
||||
|
||||
document.getElementById('sc-top').innerHTML =
|
||||
`<tr><th>OPC</th><th>等级</th><th>关键指标</th></tr>` +
|
||||
S.topOpc.map(o => `<tr><td>${o.code} ${o.name}</td><td><span class="badge pri">${o.level}</span></td><td>${o.metric}</td></tr>`).join('');
|
||||
|
||||
document.getElementById('sc-market').innerHTML = S.marketTop.map(m =>
|
||||
`<li><span>${m[0]}</span><span class="muted">${m[1]}</span></li>`).join('');
|
||||
|
||||
// 产业集聚效应(§1.5 步骤⑤)
|
||||
const C = S.cluster || {};
|
||||
if (C.upstream) {
|
||||
document.getElementById('sc-upstream').innerHTML =
|
||||
`<tr><th>类型</th><th>数量</th><th>代表</th></tr>` +
|
||||
C.upstream.map(u => `<tr><td>${u.type}</td><td><b>${u.count}</b></td><td class="muted" style="font-size:12px">${u.top}</td></tr>`).join('');
|
||||
document.getElementById('sc-downstream').innerHTML =
|
||||
`<tr><th>类型</th><th>数量</th><th>构成</th></tr>` +
|
||||
C.downstream.map(d => `<tr><td>${d.type}</td><td><b>${d.count}</b></td><td class="muted" style="font-size:12px">${d.top}</td></tr>`).join('');
|
||||
document.getElementById('sc-chain').innerHTML =
|
||||
`<tr><th>业务方向</th><th>上游</th><th>核心产能</th><th>下游效果</th></tr>` +
|
||||
C.chain.map(c => `<tr><td><b>${c.dir}</b></td><td style="font-size:12px">${c.upstream}</td><td style="font-size:12px">${c.core}</td><td style="font-size:12px">${c.downstream}</td></tr>`).join('');
|
||||
document.getElementById('sc-benchmark').textContent = C.benchmark || '';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,223 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>培训认证 · OPC 公共服务平台 Demo</title>
|
||||
<link rel="stylesheet" href="assets/style.css">
|
||||
</head>
|
||||
<body data-page="training">
|
||||
<header class="topnav" id="topnav"></header>
|
||||
|
||||
<div class="container">
|
||||
<div class="page-title">培训认证</div>
|
||||
<div class="page-sub" id="tr-sub"></div>
|
||||
|
||||
<div id="learner-banner"></div>
|
||||
|
||||
<!-- REQ-081 闭环进度 -->
|
||||
<div class="card">
|
||||
<h3 id="tr-path-title">我的成长路径 <span class="req">REQ-081 · 学习—实训—认证—接单闭环</span></h3>
|
||||
<div id="tr-steps"></div>
|
||||
</div>
|
||||
|
||||
<div class="section grid grid-4" id="tr-stats"></div>
|
||||
|
||||
<!-- REQ-082/084 课程 -->
|
||||
<div class="section">
|
||||
<h2>课程中心 <span class="req">REQ-082/084 · 工具之外的职业素养</span></h2>
|
||||
<div class="card">
|
||||
<table id="course-tbl"></table>
|
||||
<button class="btn sm" style="margin-top:10px" onclick="enroll()">+ 报名新课程</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- REQ-083 实训 -->
|
||||
<div class="section">
|
||||
<div class="two-col">
|
||||
<div class="card">
|
||||
<h3>项目实训记录 <span class="req">REQ-083/086</span></h3>
|
||||
<table id="prac-tbl"></table>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>我的证书 <span class="req">REQ-105/106</span></h3>
|
||||
<ul class="list" id="cert-list"></ul>
|
||||
<p class="muted" style="margin-top:8px">机构认证与第三方证书作为 L 级认证输入,不替代 L 级(REQ-107)。</p>
|
||||
<button class="btn sm" style="margin-top:8px" onclick="verifyInput()">证书查验</button>
|
||||
<button class="btn sm ghost" style="margin-top:8px" onclick="regThird()">登记第三方证书</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- REQ-004/113 作品集(学员与 OPC 均可沉淀作品) -->
|
||||
<div class="section">
|
||||
<h2>我的作品 <span class="req">REQ-111/113 · 作品可上架橙果商城</span></h2>
|
||||
<div class="card">
|
||||
<table id="work-tbl"></table>
|
||||
<button class="btn sm" style="margin-top:10px" onclick="addWork()">+ 上传作品</button>
|
||||
<a class="btn sm ghost" style="margin-top:10px" href="market.html">去橙果商城看看</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- REQ-085 认证→订单池 -->
|
||||
<div class="section">
|
||||
<div class="notice" id="join-notice"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="footer">OPC 公共服务平台 Demo · 培训认证</div>
|
||||
<script src="assets/app.js"></script>
|
||||
<script src="assets/store.js"></script>
|
||||
<script>
|
||||
function renderPage(DB) {
|
||||
const T = DB.training;
|
||||
const role = currentRole();
|
||||
const isLearner = role === 'learner';
|
||||
document.getElementById('tr-sub').textContent = isLearner
|
||||
? `${T.learner.id} ${T.learner.name} · 学员档案:培训履历、认证与作品(REQ-111)`
|
||||
: role === 'trainer'
|
||||
? '学员侧课程与证书展示视图 · 机构管理请进入「机构管理」工作台(REQ-081)'
|
||||
: '学习 — 练习 — 实训 — 认证 — 接单 闭环(REQ-081)';
|
||||
|
||||
// 学员转化横幅(REQ-112)
|
||||
document.getElementById('learner-banner').innerHTML = isLearner ? `
|
||||
<div class="notice" style="margin-bottom:16px;border-left-color:var(--primary);background:#eef2ff;color:var(--primary)">
|
||||
您当前是培训学员,尚未入驻 OPC。<b>${T.learner.goal}</b><br>
|
||||
<span style="font-size:12px">政策适配:可申请创业培训补贴 POLICY_0001(≤¥2,000/人,训后 90 天接单即达核销条件)</span>
|
||||
<button class="btn sm" style="margin-left:10px" onclick="becomeOpc()">申请入驻成为 OPC</button>
|
||||
</div>` : '';
|
||||
|
||||
document.getElementById('tr-path-title').innerHTML = isLearner
|
||||
? '我的学员档案 <span class="req">REQ-111 · 履历—认证—作品</span>'
|
||||
: '我的成长路径 <span class="req">REQ-081 · 学习—实训—认证—接单闭环</span>';
|
||||
document.getElementById('tr-steps').innerHTML = STEPS(isLearner ? T.learnerSteps : T.steps);
|
||||
document.getElementById('tr-stats').innerHTML = (isLearner
|
||||
? T.learner.readiness.map(r => ({ label: r[0], num: '', badge: r[1], sub: '' }))
|
||||
: T.stats.map(s => ({ label: s.label, num: s.num, sub: s.sub }))
|
||||
).map(s => `<div class="card"><h3>${s.label}</h3>${s.badge ? B(s.badge) : `<div class="num">${s.num}</div>`}<p class="muted">${s.sub || ''}</p></div>`).join('');
|
||||
|
||||
renderCourses();
|
||||
document.getElementById('prac-tbl').innerHTML =
|
||||
`<tr><th>实训项目</th><th>验收标准</th><th>成绩</th><th>导师点评</th></tr>` +
|
||||
T.practices.map(p => `<tr><td>${p.name}</td><td>${p.std}</td><td>${B(p.score)}</td><td>${p.comment}</td></tr>`).join('');
|
||||
document.getElementById('cert-list').innerHTML = T.certs.map(c => LI(c.name, B(c.badge))).join('');
|
||||
document.getElementById('work-tbl').innerHTML =
|
||||
`<tr><th>作品编码</th><th>作品</th><th>能力标签</th><th>状态</th></tr>` +
|
||||
T.works.map(w => `<tr><td>${w.code}</td><td>${w.name}</td><td>${w.tag}</td><td>${B(w.status)}</td></tr>`).join('');
|
||||
document.getElementById('join-notice').textContent = isLearner
|
||||
? 'REQ-112:入驻申请通过后,学员档案(培训履历、认证记录、作品)将自动带入 OPC 能力档案,进入五态入驻流程(REQ-013)。'
|
||||
: 'REQ-085:通过认证后自动纳入订单池并按等级开放接单范围——L3 当前可承接:中型项目、客户沟通和协同管理类任务。';
|
||||
}
|
||||
|
||||
function becomeOpc() {
|
||||
confirmDlg('入驻成为 OPC(REQ-112)',
|
||||
'将学员档案(培训履历、认证记录、4 件作品)带入 OPC 能力档案并发起入驻申请?入驻后经五态流程即可进入订单池接单。',
|
||||
'发起入驻申请', () => {
|
||||
DB.training.learnerSteps[4] = ['入驻成为 OPC', 'cur'];
|
||||
renderPage(DB);
|
||||
toast('入驻申请已提交:学员档案已带入能力档案,运营商审核中(REQ-013 五态入驻)');
|
||||
});
|
||||
}
|
||||
let workSeq = 35;
|
||||
function addWork() {
|
||||
openModal({
|
||||
title: '上传作品(REQ-111)',
|
||||
body: `
|
||||
<div class="field"><label>作品名称</label><input id="aw-n" placeholder="如:国庆海报模板包"></div>
|
||||
<div class="field"><label>能力标签</label><input value="跨境电商视觉"></div>
|
||||
<div class="checks"><label><input type="checkbox" checked> 原创声明 / 客户授权展示(REQ-004)</label></div>`,
|
||||
actions: [
|
||||
{ label: '取消', cls: 'ghost' },
|
||||
{ label: '提交', onClick: m => {
|
||||
const n = document.getElementById('aw-n').value.trim();
|
||||
if (!n) { toast('请填写作品名称', 'warn'); return; }
|
||||
m.remove();
|
||||
DB.training.works.push({ code: 'WORK_00' + workSeq++, name: n, tag: '跨境电商视觉', status: ['已入档案', 'ok'] });
|
||||
renderPage(DB);
|
||||
toast('作品已入学员/能力档案,可进一步上架OPC橙果商城(REQ-113)');
|
||||
} },
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
function renderCourses() {
|
||||
document.getElementById('course-tbl').innerHTML =
|
||||
`<tr><th>课程</th><th>提供方</th><th>类型</th><th>价格</th><th>进度</th><th>操作</th></tr>` +
|
||||
DB.training.courses.map((c, i) => `<tr><td>${c.name}</td><td>${c.provider}</td><td>${B(c.type)}</td><td>${c.price}</td><td>${BAR(c.pct, c.pct >= 100 ? 'ok' : '')}</td><td>${courseAction(c, i)}</td></tr>`).join('');
|
||||
}
|
||||
function courseAction(c, i) {
|
||||
if (c.action === 'study') return `<button class="btn sm" onclick="study(${i})">继续学习</button>`;
|
||||
if (c.action === 'certDone') return `<button class="btn sm ghost" onclick="toast('结业证书已同步至能力档案')">结业证书</button>`;
|
||||
if (c.action === 'record') return `<button class="btn sm ghost" onclick="toast('合规培训记录已同步至主体状态(REQ-015)')">培训记录</button>`;
|
||||
if (c.action === 'enrolled') return `<span class="badge info">已报名待开课</span>`;
|
||||
if (c.action === 'done') return `<span class="badge ok">已完课 · 待考试</span>`;
|
||||
return '—';
|
||||
}
|
||||
|
||||
function study(i) {
|
||||
const c = DB.training.courses[i];
|
||||
c.pct = Math.min(100, c.pct + 10);
|
||||
if (c.pct >= 100) c.action = 'done';
|
||||
renderCourses();
|
||||
toast(c.pct >= 100 ? `「${c.name}」已完课,可预约认证考试` : `「${c.name}」学习进度 ${c.pct}%`);
|
||||
}
|
||||
function enroll() {
|
||||
openModal({
|
||||
title: '报名课程',
|
||||
body: `<div class="field"><label>选择课程</label><select id="en-c">
|
||||
<option>CRS_0102 AI 漫剧制作实训营 · TRN_00001 星火设计学院 · ¥2,600</option>
|
||||
<option>CRS_0104 Midjourney 高阶技法 · TRN_00001 星火设计学院 · ¥980(审核中)</option>
|
||||
<option>CRS_0103 设计基础公益课 · 平台共建 · 免费</option>
|
||||
</select></div>
|
||||
<p class="muted">收费课经第三方支付通道结算,明示退费规则(REQ-101);补贴课计入财政补贴绩效(REQ-109)。</p>`,
|
||||
actions: [
|
||||
{ label: '取消', cls: 'ghost' },
|
||||
{ label: '去支付/报名', onClick: m => {
|
||||
m.remove();
|
||||
DB.training.courses.push({ code: 'CRS_0102', name: 'AI 漫剧制作实训营', provider: 'TRN_00001 星火设计学院', type: ['收费课', 'pri'], price: '¥2,600', pct: 0, action: 'enrolled' });
|
||||
renderCourses();
|
||||
toast('支付成功(第三方通道),已报名实训营');
|
||||
} },
|
||||
],
|
||||
});
|
||||
}
|
||||
function verifyInput() {
|
||||
openModal({
|
||||
title: '证书查验(对外查验页演示)',
|
||||
body: `<div class="field"><label>输入证书编号</label><input id="vc" placeholder="如 CERT_20260087 / CERT_20260042" value="CERT_20260042"></div>`,
|
||||
actions: [
|
||||
{ label: '取消', cls: 'ghost' },
|
||||
{ label: '查验', onClick: m => {
|
||||
const no = document.getElementById('vc').value.trim();
|
||||
m.remove();
|
||||
const inst = DB.institution.certs.find(c => c.no === no);
|
||||
const isPlatform = no === DB.opc.cert.no;
|
||||
if (inst || isPlatform) {
|
||||
openModal({ title: '查验结果 · ' + no, body: `<table>
|
||||
<tr><th>颁发主体</th><td>${inst ? DB.institution.code + ' ' + DB.institution.name : 'OPC 公共服务平台'}</td></tr>
|
||||
<tr><th>持证人</th><td>${inst ? inst.holder : 'OPC_00017 张设计'}(脱敏)</td></tr>
|
||||
<tr><th>认证项目</th><td>${inst ? inst.program : 'L3 能力认证'}</td></tr>
|
||||
<tr><th>有效期</th><td>${inst ? inst.issued + ' ~ ' + inst.expire : '2026-04-01 ~ 2027-03-31'}</td></tr>
|
||||
<tr><th>状态</th><td>${inst ? B(inst.status) : '<span class="badge ok">有效 · 防伪校验通过</span>'}</td></tr>
|
||||
</table>` });
|
||||
} else toast('未查询到该证书,请核对编号(REQ-105)', 'err');
|
||||
} },
|
||||
],
|
||||
});
|
||||
}
|
||||
function regThird() {
|
||||
openModal({
|
||||
title: '第三方证书采认登记(REQ-106)',
|
||||
body: `
|
||||
<div class="field"><label>证书类型</label><select><option>人社职业技能等级证书</option><option>行业/厂商认证</option></select></div>
|
||||
<div class="field"><label>证书名称与编号</label><input placeholder="如:电子商务师三级 / 2203xxxxxx"></div>
|
||||
<div class="field"><label>证明材料</label><input type="file"></div>`,
|
||||
actions: [
|
||||
{ label: '取消', cls: 'ghost' },
|
||||
{ label: '提交核验', onClick: m => { m.remove(); toast('已提交核验,通过后计入能力档案并作为 L 级认证输入'); } },
|
||||
],
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user