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

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

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

295 lines
15 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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>