const professionData = {
software: {
name: "软件技术",
college: "信息工程学院",
graduates: 186,
score: 84.2,
level: "高匹配",
demand: "2,864",
salary: "1.08",
coverage: "76%",
companyCount: 12,
skills: [
{ name: "Java / Spring", category: "后端开发", market: 92, course: 86 },
{ name: "Python", category: "应用开发", market: 84, course: 71 },
{ name: "Docker / K8s", category: "云原生", market: 81, course: 32 },
{ name: "数据库", category: "数据技术", market: 78, course: 74 },
{ name: "前端工程化", category: "Web开发", market: 72, course: 65 },
{ name: "AI辅助开发", category: "智能编程", market: 69, course: 28 },
],
jobs: [
{ name: "Java开发工程师", count: 826, growth: 18, width: 100 },
{ name: "前端开发工程师", count: 594, growth: 12, width: 72 },
{ name: "测试开发工程师", count: 463, growth: 9, width: 56 },
{ name: "云平台运维", count: 382, growth: 31, width: 46 },
{ name: "数据开发工程师", count: 311, growth: 24, width: 38 },
],
recommendations: [
{
priority: "高优先级",
title: "强化“云原生与容器化”实训模块",
description:
"建议在综合实训中增加 Docker、Kubernetes 和 CI/CD 流水线,并把企业部署任务用于课堂练习。",
evidence: "2,864条岗位样本中,相关技能出现率由42%升至78%,当前课程覆盖仅32%",
},
{
priority: "中优先级",
title: "将AI辅助开发纳入软件工程实践",
description:
"增加代码生成、测试辅助与安全审查内容,同时明确人工复核和代码责任边界。",
evidence: "69%的重点企业已在JD中明确要求具备AI编程工具使用经验",
},
],
companies: [
{ short: "海康", name: "海康威视", focus: "边缘计算 · 软件开发", match: 94 },
{ short: "网易", name: "网易杭州", focus: "云服务 · 测试开发", match: 91 },
{ short: "数梦", name: "数梦工场", focus: "数据平台 · 后端开发", match: 89 },
],
},
mechatronics: {
name: "机电一体化技术",
college: "智能制造学院",
graduates: 154,
score: 81.7,
level: "高匹配",
demand: "2,146",
salary: "1.05",
coverage: "72%",
companyCount: 9,
skills: [
{ name: "PLC编程", category: "自动控制", market: 94, course: 88 },
{ name: "工业机器人", category: "智能装备", market: 89, course: 71 },
{ name: "机器视觉", category: "工业视觉", market: 82, course: 38 },
{ name: "设备数字孪生", category: "数字工厂", market: 73, course: 26 },
{ name: "电气制图", category: "工程设计", market: 68, course: 82 },
{ name: "精益生产", category: "生产管理", market: 64, course: 52 },
],
jobs: [
{ name: "自动化工程师", count: 742, growth: 21, width: 100 },
{ name: "机器人工程师", count: 526, growth: 27, width: 71 },
{ name: "设备调试工程师", count: 398, growth: 13, width: 54 },
{ name: "机器视觉工程师", count: 287, growth: 38, width: 39 },
{ name: "电气工程师", count: 193, growth: 8, width: 26 },
],
recommendations: [
{
priority: "高优先级",
title: "新增工业视觉综合实训项目",
description:
"与装备企业共同设计缺陷检测任务,练习相机标定、图像处理、产线集成和质量评价。",
evidence: "机器视觉岗位近90天增长38%,课程覆盖度仅38%,技能缺口44个百分点",
},
{
priority: "中优先级",
title: "引入设备数字孪生教学案例",
description:
"把现有 PLC 和生产线实训对应到数字孪生模型,并安排设备数据采集与仿真任务。",
evidence: "73%的龙头制造企业已把数字孪生列入智能工厂相关岗位要求",
},
],
companies: [
{ short: "吉利", name: "吉利汽车", focus: "智能产线 · 自动化", match: 95 },
{ short: "大华", name: "大华股份", focus: "机器视觉 · 智能装备", match: 92 },
{ short: "西奥", name: "西奥电梯", focus: "设备调试 · 电气", match: 88 },
],
},
ecommerce: {
name: "电子商务",
college: "商贸旅游学院",
graduates: 212,
score: 74.8,
level: "待关注",
demand: "1,932",
salary: "0.96",
coverage: "68%",
companyCount: 15,
skills: [
{ name: "内容运营", category: "新媒体", market: 91, course: 76 },
{ name: "数据分析", category: "经营分析", market: 86, course: 54 },
{ name: "跨境平台运营", category: "跨境电商", market: 83, course: 43 },
{ name: "直播投放", category: "营销增长", market: 78, course: 69 },
{ name: "AI内容生产", category: "智能营销", market: 75, course: 31 },
{ name: "供应链管理", category: "履约运营", market: 68, course: 59 },
],
jobs: [
{ name: "电商运营", count: 618, growth: 13, width: 100 },
{ name: "内容运营", count: 496, growth: 22, width: 80 },
{ name: "跨境电商运营", count: 374, growth: 34, width: 61 },
{ name: "直播投放", count: 283, growth: 18, width: 46 },
{ name: "用户增长", count: 161, growth: 27, width: 26 },
],
recommendations: [
{
priority: "高优先级",
title: "重构跨境电商平台运营课程",
description:
"增加企业店铺运营、选品定价、海外投放和合规履约任务,并用企业运营数据评价学习过程。",
evidence: "跨境运营岗位增长34%,当前课程技能覆盖43%,企业反馈实操能力不足",
},
{
priority: "中优先级",
title: "增加AI营销内容生产工作流",
description:
"将AI文案、图片与短视频辅助生产纳入内容运营课程,保留品牌规范与人工审核。",
evidence: "重点企业JD中AI内容工具要求出现率75%,现有覆盖仅31%",
},
],
companies: [
{ short: "遥望", name: "遥望科技", focus: "直播电商 · 内容运营", match: 93 },
{ short: "连连", name: "连连数字", focus: "跨境支付 · 商家运营", match: 90 },
{ short: "有赞", name: "有赞科技", focus: "SaaS电商 · 用户运营", match: 87 },
],
},
};
const professions = [
{ key: "software", icon: "软", name: "软件技术", college: "信息工程学院", score: 84.2, demand: 2864, gaps: 3, trend: 4.8 },
{ key: "mechatronics", icon: "机", name: "机电一体化技术", college: "智能制造学院", score: 81.7, demand: 2146, gaps: 4, trend: 3.6 },
{ key: "ecommerce", icon: "商", name: "电子商务", college: "商贸旅游学院", score: 74.8, demand: 1932, gaps: 6, trend: -1.2 },
{ key: "iot", icon: "物", name: "物联网应用技术", college: "信息工程学院", score: 79.5, demand: 1648, gaps: 4, trend: 6.1 },
{ key: "mold", icon: "模", name: "模具设计与制造", college: "智能制造学院", score: 68.4, demand: 738, gaps: 7, trend: -2.4 },
];
const industries = [
{ name: "工业视觉与智能检测", growth: 38, width: 100 },
{ name: "跨境电商运营", growth: 34, width: 89 },
{ name: "云平台与DevOps", growth: 31, width: 82 },
{ name: "工业机器人应用", growth: 27, width: 71 },
{ name: "AI营销内容生产", growth: 24, width: 63 },
];
const trendSets = {
term: {
score: 78.6,
labels: ["9月", "10月", "11月", "12月", "1月", "3月"],
school: [69, 71.5, 72.2, 75.8, 76.4, 78.6],
benchmark: [68, 69, 69.8, 70.7, 71.2, 72.1],
},
year: {
score: 78.6,
labels: ["一季度", "二季度", "三季度", "四季度", "本期"],
school: [67.5, 71.2, 73.8, 76.1, 78.6],
benchmark: [68.4, 69.2, 70.1, 71.3, 72.1],
},
three: {
score: 78.6,
labels: ["2023", "2024", "2025", "2026"],
school: [64.8, 69.7, 74.4, 78.6],
benchmark: [66.2, 68.4, 70.3, 72.1],
},
};
const projects = [
{ type: "course", tone: "green", title: "云原生应用开发课程共建", meta: "信息工程学院 × 数梦工场", progress: 72, partners: ["李", "王", "企"] },
{ type: "lab", tone: "amber", title: "工业视觉缺陷检测实训", meta: "智能制造学院 × 大华股份", progress: 48, partners: ["陈", "赵", "企"] },
{ type: "practice", tone: "blue", title: "跨境电商店铺运营", meta: "商贸旅游学院 × 连连数字", progress: 86, partners: ["徐", "周", "企"] },
{ type: "course", tone: "green", title: "数字文旅内容生产工作坊", meta: "艺术设计学院 × 宋城集团", progress: 35, partners: ["许", "孙", "企"] },
];
const enterpriseDemands = [
{
logo: "吉",
company: "吉利汽车",
industry: "智能制造",
title: "新能源汽车产线数字化人才共育",
description: "希望联合开发设备数据采集和产线运维实训,首期可接收30名学生。",
due: "3天后截止",
},
{
logo: "遥",
company: "遥望科技",
industry: "数字商贸",
title: "直播电商运营实战项目",
description: "提供品牌运营任务,需要课程团队共同设计过程评价标准。",
due: "本周五截止",
},
{
logo: "海",
company: "海康威视",
industry: "数字技术",
title: "边缘计算软件测试人才需求",
description: "邀请专业负责人确认技能清单,并讨论联合实训基地合作。",
due: "7天后截止",
},
];
const partners = [
{ logo: "吉利", name: "吉利汽车", industry: "汽车制造 · 龙头企业", reason: "近一年发布高匹配岗位186个,与机电专业技能重合度高。", tags: ["智能产线", "企业导师"], match: 95 },
{ logo: "数梦", name: "数梦工场", industry: "数据智能 · 专精特新", reason: "已确认参与云原生课程共建,并提供项目案例。", tags: ["云平台", "课程共建"], match: 94 },
{ logo: "遥望", name: "遥望科技", industry: "直播电商 · 上市企业", reason: "内容运营和直播投放需求稳定,愿意提供实训项目。", tags: ["直播电商", "项目实训"], match: 93 },
{ logo: "大华", name: "大华股份", industry: "智能物联 · 龙头企业", reason: "工业视觉岗位增长显著,与学校智能制造方向互补。", tags: ["机器视觉", "实训基地"], match: 92 },
];
const tasks = [
{ title: "强化云原生与容器化实训内容", source: "技能缺口分析 · AI建议已审核", profession: "软件技术", owner: "李雯", due: "2026-04-30", progress: 72, status: "progress", statusText: "进行中" },
{ title: "建设工业视觉缺陷检测实训项目", source: "企业需求 · 大华股份确认", profession: "机电一体化", owner: "陈昊", due: "2026-05-15", progress: 48, status: "progress", statusText: "进行中" },
{ title: "完成跨境电商课程标准修订", source: "专业供需分析 · 教研室提交", profession: "电子商务", owner: "徐莉", due: "2026-03-28", progress: 90, status: "review", statusText: "待确认" },
{ title: "引入3名企业导师参与毕业设计", source: "校企合作计划", profession: "物联网应用", owner: "王健", due: "2026-03-20", progress: 100, status: "done", statusText: "已完成" },
{ title: "开展模具专业企业需求复核调研", source: "低匹配专业预警", profession: "模具设计", owner: "赵明", due: "2026-03-12", progress: 35, status: "risk", statusText: "存在风险" },
{ title: "更新AI辅助开发课程使用规范", source: "教学质量委员会决议", profession: "软件技术", owner: "周宁", due: "2026-04-18", progress: 100, status: "done", statusText: "已完成" },
];
const sources = [
{
id: "regional-jobs",
name: "区域招聘岗位基础包",
type: "互联网岗位 · 标准数据包",
category: "vendor",
categoryLabel: "基础数据包",
commercial: "付费订阅",
provider: "平台厂商导入",
rights: "项目期内授权使用",
coverage: "杭州及长三角",
frequency: "每月更新",
status: "服务有效",
records: "41,236",
quality: "93.4%",
analysisCompleteness: 98.8,
updated: "2026-03-18",
dataAsOf: "2026-03-17",
nextUpdate: "2026-04-01",
updateMode: "全量首包 / 月度增量",
version: "V2026.03",
schema: "JOB-PACK 3.2",
format: "Parquet / CSV UTF-8",
primaryKey: "package_id + source_record_id",
requiredFields: "岗位名称、企业标识、地区代码、发布时间、薪资单位、来源地址",
threshold: "综合质量 ≥ 90;主键唯一率 100%",
changeSummary: "新增 12,430 条,修改 846 条,失效 312 条;新增 AI 应用岗位分类",
steward: "平台数据产品部",
stewardContact: "林岚 · 数据产品经理",
technicalOwner: "平台数据运维组",
technicalContact: "平台数据包运维值班",
action: "检查更新",
},
{
id: "salary-company",
name: "薪资与企业画像基础包",
type: "互联网数据 · 增值数据包",
category: "vendor",
categoryLabel: "基础数据包",
commercial: "付费订阅",
provider: "平台厂商导入",
rights: "项目期内授权使用",
coverage: "区域重点产业",
frequency: "每月更新",
status: "服务有效",
records: "36,284",
quality: "91.8%",
analysisCompleteness: 100,
updated: "2026-03-18",
dataAsOf: "2026-03-17",
nextUpdate: "2026-04-01",
updateMode: "全量首包 / 月度增量",
version: "V2026.Q1.3",
schema: "SALARY-COMPANY 2.4",
format: "Parquet / CSV UTF-8",
primaryKey: "package_id + enterprise_id + position_code",
requiredFields: "企业标识、岗位代码、薪资区间、统计周期、企业行业与规模",
threshold: "综合质量 ≥ 90;薪资合法率 ≥ 98%",
changeSummary: "更新 2,186 条薪资样本,补充 326 家企业规模与融资阶段标签",
steward: "平台数据产品部",
stewardContact: "林岚 · 数据产品经理",
technicalOwner: "平台数据运维组",
technicalContact: "平台数据包运维值班",
action: "检查更新",
},
{
id: "internship",
name: "实习管理平台",
type: "校内系统 · API",
category: "school",
categoryLabel: "校级数据",
commercial: "校方自有",
provider: "学校实习系统",
rights: "校方数据资产",
coverage: "全校实习学生",
frequency: "每日同步",
status: "正常",
records: "32,418",
quality: "96.2%",
analysisCompleteness: 100,
updated: "今天 08:12",
dataAsOf: "2026-03-21",
nextUpdate: "明天 08:00",
updateMode: "初次全量 / 每日增量",
version: "SCHOOL-20260322-01",
schema: "INTERNSHIP 2.1",
format: "API JSON",
primaryKey: "tenant_id + internship_id",
requiredFields: "学生标识、专业代码、企业标识、岗位、开始/结束日期、状态、更新时间",
threshold: "必填完整率 ≥ 95%;跨表关联率 ≥ 98%",
changeSummary: "新增 186 条,修改 42 条;更新 31 家实习企业岗位信息",
steward: "就业处",
stewardContact: "赵晴 · 实习管理专员",
technicalOwner: "学校信息中心",
technicalContact: "王博 · 数据接口岗",
action: "立即同步",
},
{
id: "employment",
name: "就业管理系统",
type: "校内系统 · 数据库视图",
category: "school",
categoryLabel: "校级数据",
commercial: "校方自有",
provider: "学校就业系统",
rights: "校方数据资产",
coverage: "近三届毕业生",
frequency: "每日同步",
status: "字段缺失",
records: "8,926",
quality: "87.6%",
analysisCompleteness: 91.2,
updated: "今天 07:50",
dataAsOf: "2026-03-21",
nextUpdate: "异常处理后重试",
updateMode: "初次全量 / 每日增量",
version: "SCHOOL-20260322-02",
schema: "EMPLOYMENT 2.1",
format: "数据库视图",
primaryKey: "tenant_id + student_id + cohort",
requiredFields: "学生标识、专业代码、届次、就业状态、岗位、企业、业务更新时间",
threshold: "专业代码合法率 100%;必填完整率 ≥ 95%",
changeSummary: "新增 126 条,修改 43 条;86 条薪资记录进入隔离区,未覆盖当前生产版本",
steward: "就业处",
stewardContact: "赵晴 · 就业数据专员",
technicalOwner: "学校信息中心",
technicalContact: "王博 · 数据接口岗",
action: "处理异常",
warning: true,
},
{
id: "curriculum",
name: "课程标准库",
type: "校内文件 · 结构化导入",
category: "school",
categoryLabel: "校级数据",
commercial: "校方自有",
provider: "学校教务处",
rights: "校方数据资产",
coverage: "32个专业",
frequency: "学期更新",
status: "正常",
records: "1,428",
quality: "95.1%",
analysisCompleteness: 100,
updated: "昨天 17:20",
dataAsOf: "2026-03-20",
nextUpdate: "2026-08-25",
updateMode: "初次全量 / 学期快照",
version: "CURRICULUM-2026S1-03",
schema: "CURRICULUM 1.8",
format: "XLSX / CSV UTF-8",
primaryKey: "tenant_id + course_code + curriculum_version",
requiredFields: "课程代码、专业代码、课程名称、学时、课程标准、技能标签、版本",
threshold: "课程代码唯一率 100%;技能映射覆盖率 ≥ 80%",
changeSummary: "导入 2026 春季课程快照;新增 38 门课程,修订 126 份课程标准",
steward: "教务处",
stewardContact: "陈璐 · 教务数据专员",
technicalOwner: "学校信息中心",
technicalContact: "王博 · 数据接口岗",
action: "立即同步",
},
{
id: "enterprise-demand",
name: "企业确认需求库",
type: "企业提交 · 问卷/工作台",
category: "enterprise",
categoryLabel: "企业数据",
commercial: "合作授权",
provider: "126家合作企业",
rights: "只限约定合作用途",
coverage: "岗位与技能需求",
frequency: "按需更新",
status: "授权有效",
records: "8,350",
quality: "97.3%",
analysisCompleteness: 98.5,
updated: "昨天 20:08",
dataAsOf: "2026-03-21",
nextUpdate: "企业提交后实时入队",
updateMode: "初次导入 / 事件增量",
version: "ENTERPRISE-20260321-06",
schema: "ENTERPRISE-DEMAND 1.6",
format: "工作台表单 / API JSON / XLSX",
primaryKey: "tenant_id + enterprise_credit_code + demand_id",
requiredFields: "企业标识、岗位、需求人数、技能、有效期、提交人、确认时间、授权编号",
threshold: "企业身份与授权关联率 100%;必填完整率 ≥ 95%",
changeSummary: "新增 18 家企业确认、54 项岗位需求;12 项过期需求自动停用",
steward: "产教融合中心",
stewardContact: "王健 · 企业合作专员",
technicalOwner: "学校信息中心",
technicalContact: "王博 · 数据接口岗",
action: "查看回执",
},
];
const roleProfiles = {
校级管理者: {
user: "周明远",
avatar: "周",
organization: "杭州科技职业技术学院",
organizationLabel: "当前组织",
emblem: "杭",
root: "产教融合中心",
context: "2025—2026 学年 · 第 2 学期",
workspaceLabel: "学校工作空间",
menus: ["dashboard", "loop", "profession", "collaboration", "actions", "data", "report", "screen"],
defaultView: "dashboard",
},
校级技术管理员: {
user: "王博",
avatar: "王",
organization: "杭州科技职业技术学院 · 信息中心",
organizationLabel: "当前数据域",
emblem: "数",
root: "学校数据运维中心",
context: "校级技术管理员 · 6 个数据源 · 2 个待处理任务",
workspaceLabel: "数据运维",
menus: ["loop", "data", "actions", "report"],
defaultView: "data",
dataAdministrator: true,
},
学院负责人: {
user: "李雯",
avatar: "李",
organization: "信息工程学院",
organizationLabel: "当前学院",
emblem: "信",
root: "学院工作台",
context: "信息工程学院 · 6 个专业 · 2026届",
workspaceLabel: "学院工作空间",
menus: ["loop", "profession", "collaboration", "actions", "report"],
defaultView: "profession",
},
专业负责人: {
user: "李雯",
avatar: "李",
organization: "信息工程学院 · 软件技术专业",
organizationLabel: "当前专业",
emblem: "软",
root: "专业建设工作台",
context: "软件技术专业 · 2026 春季流程",
workspaceLabel: "专业工作空间",
menus: ["loop", "profession", "collaboration", "actions", "report"],
defaultView: "loop",
},
就业老师: {
user: "赵晴",
avatar: "赵",
organization: "杭州科技职业技术学院 · 就业处",
organizationLabel: "当前业务域",
emblem: "就",
root: "就业服务工作台",
context: "就业处 · 2026届 · 数据使用反馈",
workspaceLabel: "就业工作空间",
menus: ["loop", "data", "actions", "report"],
defaultView: "loop",
},
企业导师: {
user: "陈凯",
avatar: "陈",
organization: "海康威视 · 智能物联事业群",
organizationLabel: "当前企业",
emblem: "企",
root: "校企共育空间",
context: "企业导师 · 2026 春季合作项目",
workspaceLabel: "企业工作空间",
menus: ["loop", "collaboration", "actions"],
defaultView: "collaboration",
},
厂商超级管理员: {
user: "陈致远",
avatar: "陈",
organization: "产教融智平台运营中心",
organizationLabel: "平台运营方",
emblem: "融",
root: "平台运营中心",
context: "生产环境 · 华东区域 · 12 家学校租户",
workspaceLabel: "平台运营",
menus: [],
defaultView: "platform",
platform: true,
},
};
const CLOSED_LOOP_SCHEMA_VERSION = 2;
const CLOSED_LOOP_LIMITS = {
auditEvents: 200,
evidences: 100,
usageFeedback: 50,
customCollection: 50,
entityHistory: 100,
};
const CLOSED_LOOP_STORAGE_KEY = "eduIndustry.campusClosedLoop.TENANT-HZVTC.v1";
const CLOSED_LOOP_SHARED = {
tenantId: "TENANT-HZVTC",
packageId: "EDU-PKG-20260823-01",
packageVersion: "V2026.08.23",
packageRecords: 2846320,
packageSha256: "4f8d91a47e2b6d82b2aaad136d344d61c83f9cd472185d2543725c91870e9e32",
receiptId: "RCT-HZVTC-20260823-01",
};
function closedLoopSeed() {
return {
schemaVersion: CLOSED_LOOP_SCHEMA_VERSION,
seededAt: "2026-08-23T09:00:00+08:00",
shared: { ...CLOSED_LOOP_SHARED },
entities: {
demand: {
id: "DEM-HKVS-20260823-01",
status: "已提交",
company: "海康威视",
enterpriseId: "ENT-HKVS",
title: "边缘计算软件测试人才共育需求",
position: "边缘计算测试工程师",
skills: "Linux、Python、自动化测试、边缘设备调试",
headcount: 24,
authorizationNo: "AUTH-HKVS-20260823-01",
validUntil: "2027-08-22",
submitter: "陈凯 · 海康威视",
submittedAt: "2026-08-23 09:10",
cycle: "2026C2",
description: "面向软件技术专业共建边缘计算测试实训,企业导师参与中期评审与成果评价。",
},
snapshot: {
id: "SNP-HZVTC-20260824-01",
status: "待接收",
receivedAt: "",
acceptedAt: "",
operator: "",
packageId: CLOSED_LOOP_SHARED.packageId,
packageVersion: CLOSED_LOOP_SHARED.packageVersion,
sha256: CLOSED_LOOP_SHARED.packageSha256,
expectedRecords: 128642,
checkedRecords: 0,
},
recommendation: {
id: "REC-SOFTWARE-20260823-01",
status: "待决策",
title: "强化“云原生与容器化”实训模块",
reason: "",
operator: "",
decidedAt: "",
snapshotId: "SNP-HZVTC-20260824-01",
demandId: "DEM-HKVS-20260823-01",
},
task: null,
project: null,
evidences: [],
evaluation: null,
report: null,
usageFeedback: [],
review: null,
receipt: {
id: CLOSED_LOOP_SHARED.receiptId,
status: "待技术确认",
tenantId: CLOSED_LOOP_SHARED.tenantId,
packageId: CLOSED_LOOP_SHARED.packageId,
packageVersion: CLOSED_LOOP_SHARED.packageVersion,
expectedRecords: CLOSED_LOOP_SHARED.packageRecords,
expectedSha256: CLOSED_LOOP_SHARED.packageSha256,
receivedRecords: 0,
receivedSha256: "",
verification: null,
note: "",
technicalConfirmedBy: "",
technicalConfirmedAt: "",
businessUsable: false,
},
},
sequences: { cycle: 1, task: 0, project: 0, evaluation: 0, acceptance: 0, report: 0, review: 0, evidence: 0, feedback: 0 },
custom: { demands: [], tasks: [], projects: [], reports: [] },
auditEvents: [
{
id: "AUD-20260823-0001",
action: "SEED_CREATED",
objectId: "DEM-HKVS-20260823-01",
operator: "样板数据初始化",
role: "系统",
at: "2026-08-23 09:00",
detail: "创建可重置的校园业务流程样板。",
},
],
};
}
function isPlainObject(value) {
return Boolean(value && typeof value === "object" && !Array.isArray(value));
}
function trimClosedLoopCollections(state) {
state.auditEvents = state.auditEvents.slice(0, CLOSED_LOOP_LIMITS.auditEvents);
state.entities.evidences = state.entities.evidences.slice(-CLOSED_LOOP_LIMITS.evidences);
state.entities.usageFeedback = state.entities.usageFeedback.slice(-CLOSED_LOOP_LIMITS.usageFeedback);
Object.keys(state.custom).forEach((key) => {
state.custom[key] = state.custom[key].slice(0, CLOSED_LOOP_LIMITS.customCollection);
});
if (state.entities.task) {
state.entities.task.progressLogs = state.entities.task.progressLogs.slice(-CLOSED_LOOP_LIMITS.entityHistory);
state.entities.task.acceptanceHistory = state.entities.task.acceptanceHistory.slice(-CLOSED_LOOP_LIMITS.entityHistory);
}
if (state.entities.report) state.entities.report.history = state.entities.report.history.slice(-CLOSED_LOOP_LIMITS.entityHistory);
return state;
}
function resetInvalidClosedLoopState(seed, rawVersion, reason) {
seed.auditEvents.unshift({
id: `AUD-RESET-${Date.now()}`,
action: "SECURITY_SCHEMA_RESET",
objectId: CLOSED_LOOP_SHARED.receiptId,
operator: "流程状态迁移器",
role: "系统",
at: nowText(),
detail: `Schema V${rawVersion ?? "未知"} 状态已按安全策略失效并重建 V${CLOSED_LOOP_SCHEMA_VERSION} 种子:${reason}`,
});
return seed;
}
function migrateClosedLoopState(raw) {
const seed = closedLoopSeed();
if (!raw || typeof raw !== "object") return seed;
if (raw.schemaVersion !== CLOSED_LOOP_SCHEMA_VERSION) {
return resetInvalidClosedLoopState(seed, raw.schemaVersion, "旧版本未执行完整 SHA 强校验");
}
const rawEntities = isPlainObject(raw.entities) ? raw.entities : {};
const rawCustom = isPlainObject(raw.custom) ? raw.custom : {};
const allowedCustom = Object.fromEntries(Object.entries(rawCustom).filter(([key]) => Object.hasOwn(seed.custom, key)));
const mergeEntity = (key) => {
const seedEntity = seed.entities[key];
const rawEntity = rawEntities[key];
if (isPlainObject(seedEntity) && isPlainObject(rawEntity)) {
return { ...seedEntity, ...rawEntity };
}
return rawEntity ?? seedEntity;
};
const state = {
...seed,
...raw,
schemaVersion: CLOSED_LOOP_SCHEMA_VERSION,
shared: { ...seed.shared, ...(isPlainObject(raw.shared) ? raw.shared : {}) },
entities: {
...seed.entities,
...rawEntities,
demand: mergeEntity("demand"),
snapshot: mergeEntity("snapshot"),
recommendation: mergeEntity("recommendation"),
receipt: mergeEntity("receipt"),
},
sequences: { ...seed.sequences, ...(isPlainObject(raw.sequences) ? raw.sequences : {}) },
custom: { ...seed.custom, ...allowedCustom },
auditEvents: Array.isArray(raw.auditEvents) ? raw.auditEvents : seed.auditEvents,
};
const validEntityArrays = Array.isArray(state.entities.evidences) && Array.isArray(state.entities.usageFeedback);
const validCustomCollections = Object.keys(seed.custom).every((key) => Array.isArray(state.custom[key]));
const validCoreEntities = ["demand", "snapshot", "recommendation", "receipt"].every((key) => isPlainObject(state.entities[key]));
const validCoreIds =
validCoreEntities &&
["demand", "snapshot", "recommendation", "receipt"].every(
(key) => typeof state.entities[key].id === "string" && state.entities[key].id.length > 0 && state.entities[key].id.length <= 96,
);
const validTask =
state.entities.task === null ||
(isPlainObject(state.entities.task) && Array.isArray(state.entities.task.progressLogs) && Array.isArray(state.entities.task.acceptanceHistory));
const validReport = state.entities.report === null || (isPlainObject(state.entities.report) && Array.isArray(state.entities.report.history));
const validOptionalEntities = ["project", "evaluation", "review"].every((key) => state.entities[key] === null || isPlainObject(state.entities[key]));
if (!validEntityArrays || !validCustomCollections || !validCoreEntities || !validCoreIds || !validTask || !validReport || !validOptionalEntities) {
return resetInvalidClosedLoopState(seed, raw.schemaVersion, "实体结构或历史数组不满足 V2 不变量");
}
if (state.entities.task) {
state.entities.task.acceptanceHistory = state.entities.task.acceptanceHistory.map((item, index) =>
isPlainObject(item) && !item.id
? { ...item, id: `ACC-HZVTC-${String(index + 1).padStart(2, "0")}` }
: item,
);
}
const cycleFromDemandId = state.entities.demand.id.match(/-(\d{4}C[12])-/)?.[1];
state.entities.demand.cycle = state.entities.demand.cycle || cycleFromDemandId || "2026C2";
state.sequences.cycle = Math.max(
Number.isInteger(state.sequences.cycle) ? state.sequences.cycle : 0,
state.entities.demand.previousVersionId ? 2 : 1,
);
["task", "project", "evaluation", "report", "review"].forEach((key) => {
state.sequences[key] = Math.max(Number.isInteger(state.sequences[key]) ? state.sequences[key] : 0, state.entities[key] ? 1 : 0);
});
state.sequences.acceptance = Math.max(
Number.isInteger(state.sequences.acceptance) ? state.sequences.acceptance : 0,
state.entities.task?.acceptanceHistory.length || 0,
);
if (state.entities.evaluation && ["进行中", "待评价"].includes(state.entities.task?.status)) {
const staleEvaluationId = state.entities.evaluation.id || "EVL-旧记录";
state.entities.evaluation = null;
state.auditEvents.unshift({
id: closedLoopToken("AUD"),
action: "STALE_EVALUATION_MIGRATED",
objectId: staleEvaluationId,
operator: "流程状态迁移器",
role: "系统",
at: nowText(),
detail: "检测到补证后的待评价任务仍引用旧企业评价,已清除当前评价引用并要求重新评价。",
});
}
if (state.entities.review?.decision === "进入下一周期" && !state.entities.demand.previousVersionId) {
state.pendingNextCycleMigration = { ...state.entities.review };
}
state.sequences.evidence = Math.max(Number.isInteger(state.sequences.evidence) ? state.sequences.evidence : 0, state.entities.evidences.length);
state.sequences.feedback = Math.max(Number.isInteger(state.sequences.feedback) ? state.sequences.feedback : 0, state.entities.usageFeedback.length);
return trimClosedLoopCollections(state);
}
function loadClosedLoopState() {
try {
return migrateClosedLoopState(JSON.parse(localStorage.getItem(CLOSED_LOOP_STORAGE_KEY)));
} catch {
return closedLoopSeed();
}
}
let closedLoopState = loadClosedLoopState();
function saveClosedLoopState() {
closedLoopState.schemaVersion = CLOSED_LOOP_SCHEMA_VERSION;
trimClosedLoopCollections(closedLoopState);
try {
localStorage.setItem(CLOSED_LOOP_STORAGE_KEY, JSON.stringify(closedLoopState));
return true;
} catch {
showToast("浏览器阻止了本地存储,本次流程状态仅在当前页面有效。", "状态未持久化");
return false;
}
}
function currentRoleName() {
return document.getElementById("roleSelect")?.value || "校级管理者";
}
function currentRoleUser() {
const role = currentRoleName();
return `${roleProfiles[role]?.user || "样板用户"} · ${role}`;
}
function nowText() {
return new Intl.DateTimeFormat("zh-CN", {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
hour12: false,
})
.format(new Date())
.replaceAll("/", "-");
}
function safeText(value) {
const element = document.createElement("span");
element.textContent = String(value ?? "");
return element.innerHTML;
}
function closedLoopToken(prefix) {
return `${prefix}-${Date.now()}-${Math.random().toString(16).slice(2, 7).toUpperCase()}`;
}
function addClosedLoopAudit(action, objectId, detail, operator = currentRoleUser(), role = currentRoleName()) {
closedLoopState.auditEvents.unshift({
id: closedLoopToken("AUD"),
action,
objectId,
operator,
role,
at: nowText(),
detail,
});
}
function loopStatusClass(status = "") {
if (["已发布", "已完成", "已验收", "已采纳", "通过", "已接收", "已确认", "关闭"].includes(status)) return "done";
if (["已退回", "需整改", "驳回", "恶化"].includes(status)) return "risk";
if (["暂缓", "待审核", "待验收", "待评价", "进入下一周期"].includes(status)) return "review";
return "progress";
}
function syncClosedLoopCollections() {
const e = closedLoopState.entities;
const removeSynced = (list) => {
for (let index = list.length - 1; index >= 0; index -= 1) {
if (list[index]._closedLoop || list[index]._customPersisted) list.splice(index, 1);
}
};
removeSynced(enterpriseDemands);
removeSynced(tasks);
removeSynced(projects);
enterpriseDemands.unshift(
{
_closedLoop: true,
id: e.demand.id,
logo: "海",
company: e.demand.company,
industry: "智能物联",
title: e.demand.title,
description: `${e.demand.position} · ${e.demand.headcount}人 · ${e.demand.skills}`,
due: `${e.demand.validUntil} 有效`,
status: e.demand.status,
},
...closedLoopState.custom.demands.map((item) => ({ ...item, _customPersisted: true })),
);
if (e.task) {
tasks.unshift({
_closedLoop: true,
id: e.task.id,
title: e.task.title,
source: `流程建议 ${e.task.recommendationId} · 需求 ${e.task.demandId}`,
profession: e.task.profession,
owner: e.task.owner,
due: e.task.due,
progress: e.task.progress,
status: e.task.status === "已完成" ? "done" : e.task.status === "待验收" ? "review" : e.task.status === "已退回" ? "risk" : "progress",
statusText: e.task.status,
});
}
tasks.unshift(...closedLoopState.custom.tasks.map((item) => ({ ...item, _customPersisted: true })));
if (e.project) {
projects.unshift({
_closedLoop: true,
id: e.project.id,
type: "practice",
tone: "green",
title: e.project.title,
meta: `${e.project.profession} × ${e.demand.company} · ${e.project.id}`,
progress: e.project.progress,
partners: ["李", "陈", "企"],
status: e.project.status,
demandId: e.project.demandId,
recommendationId: e.project.recommendationId,
taskId: e.project.taskId,
});
}
projects.unshift(...closedLoopState.custom.projects.map((item) => ({ ...item, _customPersisted: true })));
for (let index = reports.length - 1; index >= 0; index -= 1) {
if (reports[index]._closedLoop || reports[index]._customPersisted) reports.splice(index, 1);
}
if (e.report) {
const base = reports.find((item) => item.type === "school") || reports[0];
reports.unshift({
...base,
_closedLoop: true,
id: e.report.id,
title: e.report.title,
version: e.report.version || CLOSED_LOOP_SHARED.packageVersion,
status: e.report.status,
statusClass: loopStatusClass(e.report.status),
updated: e.report.updatedAt || "刚刚",
featured: true,
roles: ["校级管理者", "校级技术管理员", "学院负责人", "专业负责人", "就业老师"],
});
}
reports.unshift(...closedLoopState.custom.reports.map((item) => ({ ...item, _customPersisted: true })));
}
function saveAndRenderClosedLoop(message = "") {
saveClosedLoopState();
syncClosedLoopCollections();
renderDataReportLineage();
renderClosedLoop();
renderTasks();
renderProjects();
renderDemands();
renderReportCenter();
if (message) showToast(message, "流程状态已保存");
}
const closedLoopActionCatalog = {
demand: {
title: "提交企业需求",
queueLabel: "提交 / 更新本企业需求",
nodeKey: "demand",
description: "仅企业导师可维护本企业需求;岗位、技能、人数、授权号、有效期和提交人均为必填。",
roles: ["企业导师"],
fields: [
["position", "岗位名称", "text", "边缘计算测试工程师"],
["skills", "技能要求", "textarea", "Linux、Python、自动化测试、边缘设备调试"],
["headcount", "计划人数", "number", "24"],
["authorizationNo", "授权号", "text", "AUTH-HKVS-20260823-01"],
["validUntil", "授权有效期", "date", "2027-08-22"],
["submitter", "提交人", "text", "陈凯 · 海康威视"],
],
},
receive: {
title: "确认接收并生成快照",
queueLabel: "确认包与需求接收",
nodeKey: "snapshot",
description: "校级技术管理员强校验租户、包号、版本、完整 SHA-256 和记录数;全部一致后确认接收,并生成独立分析快照。",
roles: ["校级技术管理员"],
fields: [
["tenantId", "租户标识", "text", CLOSED_LOOP_SHARED.tenantId],
["packageId", "数据包编号", "text", CLOSED_LOOP_SHARED.packageId],
["packageVersion", "数据包版本", "text", CLOSED_LOOP_SHARED.packageVersion],
["checkedRecords", "交付包记录数", "number", String(CLOSED_LOOP_SHARED.packageRecords)],
["sha256", "完整 SHA-256 摘要", "text", CLOSED_LOOP_SHARED.packageSha256],
["note", "技术接收说明", "textarea", "包版本、记录数与SHA校验一致,授权关联率100%,允许进入分析快照。"],
],
},
decision: {
title: "专业建议决策",
queueLabel: "决策专业建议",
nodeKey: "recommendation",
description: "采纳、暂缓或驳回都必须填写理由;采纳后自动创建关联任务。",
roles: ["专业负责人"],
fields: [
["decision", "决策", "select", "采纳|暂缓|驳回"],
["reason", "决策理由", "textarea", "企业需求与岗位样本相互印证,现有课程覆盖不足,纳入本期实训调整。"],
["owner", "任务负责人", "text", "李雯"],
["due", "任务截止日期", "date", "2026-12-20"],
],
},
project: {
title: "创建关联项目",
queueLabel: "创建关联项目",
nodeKey: "project",
description: "项目自动绑定 demandId、recommendationId 与 taskId。",
roles: ["专业负责人"],
fields: [
["title", "项目名称", "text", "边缘计算测试项目进课堂"],
["profession", "牵头专业", "text", "软件技术"],
["owner", "项目负责人", "text", "李雯"],
],
},
evidence: {
title: "提交阶段进展与证据",
queueLabel: "提交进展与证据",
nodeKey: "evidence",
description: "保存证据名称、类型、说明、提交人、时间和文件名/链接;提交后自动更新任务和项目进度。",
roles: ["专业负责人", "学院负责人"],
fields: [
["name", "证据名称", "text", "边缘计算测试实训方案"],
["type", "证据类型", "select", "课程方案|阶段成果|评审纪要|学生作品"],
["description", "证据说明", "textarea", "已完成企业任务拆解、课程映射和中期评价量规。"],
["submitter", "提交人", "text", "李雯 · 软件技术专业"],
["fileRef", "文件名或链接", "text", "边缘计算测试实训方案-v1.0.pdf"],
["progress", "本次完成进度", "number", "80"],
],
},
evaluation: {
title: "提交企业评价",
queueLabel: "提交企业评价",
nodeKey: "evaluation",
description: "企业导师只能评价本企业关联对象;通过或需整改均须填写意见。",
roles: ["企业导师"],
fields: [
["result", "评价结论", "select", "通过|需整改"],
["comment", "评价意见", "textarea", "任务覆盖企业设备测试流程,验收量规清晰,同意进入校级验收。"],
],
},
acceptance: {
title: "校级任务验收",
queueLabel: "校级验收 / 退回",
nodeKey: "acceptance",
description: "校级管理者在企业评价后验收或退回任务,并记录意见。",
roles: ["校级管理者"],
fields: [
["result", "验收结论", "select", "验收通过|退回整改"],
["comment", "验收意见", "textarea", "企业评价、课程方案和过程证据齐全,同意完成。"],
],
},
reportCreate: {
title: "生成业务报告草稿",
queueLabel: "生成业务报告草稿",
nodeKey: "report",
description: "报告关联需求、快照、任务和全部证据,生成后进入校内审批流程。",
roles: ["专业负责人", "学院负责人", "校级管理者"],
fields: [["title", "报告名称", "text", "边缘计算测试人才共育成效报告"]],
},
reportSubmit: {
title: "提交报告审核",
queueLabel: "提交报告审核",
nodeKey: "report",
description: "草稿提交后进入待审核状态。",
roles: ["专业负责人", "学院负责人", "校级管理者"],
fields: [["comment", "提交说明", "textarea", "流程实体与证据已核对,请校级管理者审核。"]],
},
reportReview: {
title: "报告审批",
queueLabel: "批准 / 退回报告",
nodeKey: "report",
description: "只有校级管理者可批准或退回业务报告;审批人必须与本次提交人分离。",
roles: ["校级管理者"],
fields: [
["result", "审批结论", "select", "批准|退回"],
["approver", "独立审批人", "select", "刘嘉禾 · 校级管理者|周明远 · 校级管理者"],
["comment", "审批意见", "textarea", "实体关联、数据口径和验收记录完整,同意批准。"],
],
},
reportPublish: {
title: "正式发布报告",
queueLabel: "正式发布报告",
nodeKey: "report",
description: "只有已批准报告可正式发布;发布后下载和导出才标识为正式版。",
roles: ["校级管理者"],
fields: [["comment", "发布说明", "textarea", "作为本期校园业务流程正式报告发布。"]],
},
feedback: {
title: "记录就业侧使用反馈",
queueLabel: "登记就业使用反馈",
nodeKey: "feedback",
description: "就业老师确认数据对就业业务是否可用,并登记下一周期关注项。",
roles: ["就业老师"],
fields: [
["usable", "业务确认", "select", "可用|需补充"],
["department", "使用部门", "text", "就业处"],
["comment", "使用反馈", "textarea", "岗位技能标签可用于就业指导课程和重点企业岗位推荐。"],
["nextCycle", "下一周期关注", "textarea", "补充学生项目作品与就业岗位匹配率。"],
],
},
review: {
title: "下一周期复核",
queueLabel: "下一周期复核",
nodeKey: "review",
description: "保存前后指标与改善结论,并决定关闭或进入下一周期。",
roles: ["专业负责人"],
fields: [
["before", "前周期指标", "number", "32"],
["after", "本周期指标", "number", "68"],
["conclusion", "复核结论", "select", "改善|无变化|恶化"],
["decision", "周期决定", "select", "关闭|进入下一周期"],
["comment", "复核说明", "textarea", "云原生与边缘测试课程覆盖率明显提升,企业评价通过。"],
],
},
};
function closedLoopActionState(action) {
const e = closedLoopState.entities;
const checks = {
demand: [
e.snapshot.status === "待接收" && !e.task && !e.report,
e.snapshot.status !== "待接收" ? "技术接收完成后不能修改当前需求;如需变更,请创建新版本并重新接收。" : "已有下游任务或报告,当前需求版本不能直接修改。",
],
receive: [e.demand.status === "已提交" && e.snapshot.status !== "已验收", e.snapshot.status === "已验收" ? "技术接收与快照验收已完成。" : "需先由企业提交完整需求。"],
decision: [e.snapshot.status === "已验收" && e.recommendation.status !== "已采纳", e.recommendation.status === "已采纳" ? "建议已采纳并创建关联任务。" : "需先完成数据接收并生成已验收快照。"],
project: [Boolean(e.task && !e.project), e.project ? "关联项目已创建。" : "需先采纳建议并自动创建任务。"],
evidence: [
Boolean(e.task && e.project && ["进行中", "需整改", "已退回"].includes(e.task.status) && !e.report),
e.report ? "报告已生成,当前任务不能再追加证据;新增证据请放到下一周期或新版本。" : e.task?.status === "已完成" ? "任务已完成校级验收,不能继续追加当前版本证据。" : "需先创建处于执行或整改状态的关联任务和项目。",
],
evaluation: [
e.evidences.length > 0 &&
e.task?.progress === 100 &&
e.project?.progress === 100 &&
e.demand.enterpriseId === "ENT-HKVS" &&
e.task?.status === "待评价" &&
!e.evaluation,
e.task?.status === "已完成"
? "任务已完成校级验收,无需重复评价。"
: ["需整改", "已退回"].includes(e.task?.status)
? "必须先补充新的执行证据,原评价不能直接改写。"
: e.evaluation?.result === "通过"
? "企业评价已通过,等待校级验收。"
: e.evidences.length === 0
? "需先有本企业项目执行证据。"
: "阶段进度必须达到100%后才能提交企业评价。",
],
acceptance: [e.evaluation?.result === "通过" && e.task?.status === "待验收", e.evaluation?.result === "需整改" ? "企业评价为需整改,必须先补充证据并重新评价。" : "未有企业通过评价时任务不能申请校级验收。"],
reportCreate: [e.task?.status === "已完成" && !e.report, e.report ? "业务报告已生成。" : "需先通过校级任务验收。"],
reportSubmit: [e.report?.status === "草稿" || e.report?.status === "已退回", "仅草稿或已退回报告可提交审核。"],
reportReview: [e.report?.status === "待审核", "仅待审核报告可审批。"],
reportPublish: [
e.report?.status === "已批准" && Boolean(e.report.approvedBy) && e.report.approvedBy !== e.report.submittedBy,
"正式发布前必须由不同于提交人的校级管理者批准报告。",
],
feedback: [e.report?.status === "已发布" && !e.review, e.review ? "本周期已完成成效复核,不能继续追加使用反馈。" : "需先正式发布报告。"],
review: [
e.usageFeedback.at(-1)?.usable === "可用" && !e.review,
e.review ? "本周期成效复核已完成。" : e.usageFeedback.length ? "最新业务使用反馈仍为需补充,确认可用后才能复核。" : "需先由就业老师登记使用反馈。",
],
};
const [enabled, reason] = checks[action] || [false, "动作不可用。"];
return { enabled, reason: enabled ? "前置条件已满足。" : reason };
}
function actionPrerequisite(action) {
const role = currentRoleName();
const allowed = closedLoopActionCatalog[action]?.roles.includes(role);
if (!allowed) return { enabled: false, reason: "当前角色无此动作权限。" };
return closedLoopActionState(action);
}
const CLOSED_LOOP_NODE_POSITIONS = {
demand: [60, 35],
snapshot: [415, 35],
recommendation: [770, 35],
task: [20, 220],
project: [215, 220],
evidence: [410, 220],
evaluation: [605, 220],
acceptance: [800, 220],
report: [170, 420],
feedback: [415, 420],
review: [660, 420],
};
function closedLoopCssPixelValue(styles, property) {
const value = parseFloat(styles.getPropertyValue(property));
if (!Number.isFinite(value) || value <= 0) throw new Error(`流程布局变量 ${property} 无效。`);
return value;
}
function closedLoopCanvasStyles() {
const canvas = document.querySelector(".loop-flow-canvas");
if (!canvas) throw new Error("流程画布尚未挂载。");
return getComputedStyle(canvas);
}
function closedLoopCanvasSize() {
const styles = closedLoopCanvasStyles();
return {
width: closedLoopCssPixelValue(styles, "--loop-canvas-width"),
height: closedLoopCssPixelValue(styles, "--loop-canvas-height"),
};
}
function closedLoopNodeSize() {
const styles = closedLoopCanvasStyles();
return {
width: closedLoopCssPixelValue(styles, "--loop-node-width"),
height: closedLoopCssPixelValue(styles, "--loop-node-height"),
};
}
function closedLoopNodeGeometry(key, size = closedLoopNodeSize()) {
const [left, top] = CLOSED_LOOP_NODE_POSITIONS[key];
const { width, height } = size;
return {
left,
right: left + width,
top,
bottom: top + height,
centerX: left + width / 2,
centerY: top + height / 2,
};
}
function closedLoopMainEdgeDefinitions() {
const size = closedLoopNodeSize();
const demand = closedLoopNodeGeometry("demand", size);
const snapshot = closedLoopNodeGeometry("snapshot", size);
const recommendation = closedLoopNodeGeometry("recommendation", size);
const task = closedLoopNodeGeometry("task", size);
const project = closedLoopNodeGeometry("project", size);
const evidence = closedLoopNodeGeometry("evidence", size);
const evaluation = closedLoopNodeGeometry("evaluation", size);
const acceptance = closedLoopNodeGeometry("acceptance", size);
const report = closedLoopNodeGeometry("report", size);
const feedback = closedLoopNodeGeometry("feedback", size);
const review = closedLoopNodeGeometry("review", size);
const recommendationElbowY = task.top - 40;
const acceptanceElbowY = report.top - 35;
return [
{ from: "demand", to: "snapshot", path: `M${demand.right} ${demand.centerY} H${snapshot.left}` },
{ from: "snapshot", to: "recommendation", path: `M${snapshot.right} ${snapshot.centerY} H${recommendation.left}` },
{
from: "recommendation",
to: "task",
path: `M${recommendation.centerX} ${recommendation.bottom} V${recommendationElbowY} H${task.centerX} V${task.top}`,
label: "采纳后创建任务",
labelPosition: [(recommendation.centerX + task.centerX) / 2, recommendationElbowY - 9],
},
{ from: "task", to: "project", path: `M${task.right} ${task.centerY} H${project.left}` },
{ from: "project", to: "evidence", path: `M${project.right} ${project.centerY} H${evidence.left}` },
{ from: "evidence", to: "evaluation", path: `M${evidence.right} ${evidence.centerY} H${evaluation.left}` },
{ from: "evaluation", to: "acceptance", path: `M${evaluation.right} ${evaluation.centerY} H${acceptance.left}` },
{
from: "acceptance",
to: "report",
path: `M${acceptance.centerX} ${acceptance.bottom} V${acceptanceElbowY} H${report.centerX} V${report.top}`,
label: "验收通过",
labelPosition: [(acceptance.centerX + report.centerX) / 2 + 15, acceptanceElbowY - 9],
},
{ from: "report", to: "feedback", path: `M${report.right} ${report.centerY} H${feedback.left}` },
{ from: "feedback", to: "review", path: `M${feedback.right} ${feedback.centerY} H${review.left}` },
];
}
const closedLoopUiState = {
selectedNodeKey: null,
};
function closedLoopNodeDefinitions() {
const e = closedLoopState.entities;
const evidence = e.evidences.at(-1);
const feedback = e.usageFeedback.at(-1);
const acceptance = e.task?.acceptanceHistory?.at(-1);
const rawNodes = [
{
key: "demand",
title: "企业需求",
phase: "需求提出",
id: e.demand.id,
status: e.demand.status,
owner: "企业导师",
description: "企业提交岗位、技能、人数与授权信息,学校据此开展分析并安排共育项目。",
input: "岗位、技能、计划人数、授权编号",
output: "带编号的企业需求版本",
complete: e.demand.status === "已提交",
risk: false,
relations: [e.demand.enterpriseId, e.demand.previousVersionId, e.recommendation.demandId],
facts: [
["岗位", e.demand.position],
["计划人数", `${e.demand.headcount} 人`],
["技能要求", e.demand.skills],
["授权有效期", e.demand.validUntil],
],
},
{
key: "snapshot",
title: "接收与分析快照",
phase: "技术校验",
id: e.snapshot.id,
status: e.snapshot.status,
owner: "校级技术管理员",
description: "强校验租户、包号、版本、记录数和完整 SHA-256,通过后生成独立分析快照。",
input: "厂商数据包、Manifest、租户授权",
output: "已验收分析快照",
complete: e.snapshot.status === "已验收",
risk: false,
relations: [e.receipt.id, e.snapshot.packageId, e.snapshot.packageVersion],
facts: [
["交付记录", `${e.receipt.receivedRecords.toLocaleString("zh-CN")} / ${e.receipt.expectedRecords.toLocaleString("zh-CN")} 条`],
["分析样本", `${e.snapshot.checkedRecords.toLocaleString("zh-CN")} / ${e.snapshot.expectedRecords.toLocaleString("zh-CN")} 条`],
["接收状态", e.receipt.status],
["技术接收人", e.snapshot.operator || "待确认"],
],
},
{
key: "recommendation",
title: "专业建议决策",
phase: "业务决策",
id: e.recommendation.id,
status: e.recommendation.status,
owner: "专业负责人",
description: "专业负责人查看数据快照和企业需求后采纳、暂缓或驳回建议,采纳后自动创建任务。",
input: "分析结论、企业需求、课程差距",
output: "带理由的专业决策",
complete: e.recommendation.status === "已采纳",
risk: ["暂缓", "驳回"].includes(e.recommendation.status),
relations: [e.recommendation.snapshotId, e.recommendation.demandId],
facts: [
["建议", e.recommendation.title],
["决策理由", e.recommendation.reason || "待填写"],
["决策人", e.recommendation.operator || "待专业负责人决策"],
["决策时间", e.recommendation.decidedAt || "待决策"],
],
},
{
key: "task",
title: "改进任务",
phase: "任务生成",
id: e.task?.id || "TSK-待创建",
status: e.task?.status || "未创建",
owner: e.task?.owner || "专业负责人",
description: "采纳专业建议后自动生成改进任务,保存负责人、期限、进度和验收历史。",
input: "已采纳建议与任务负责人",
output: "有负责人、期限和验收要求的任务",
reached: e.recommendation.status === "已采纳",
complete: Boolean(e.task),
risk: ["需整改", "已退回"].includes(e.task?.status),
relations: [e.task?.demandId, e.task?.recommendationId],
facts: [
["任务名称", e.task?.title || "待创建"],
["负责人", e.task?.owner || "待指定"],
["截止日期", e.task?.due || "待确定"],
["执行进度", `${e.task?.progress || 0}%`],
],
},
{
key: "project",
title: "关联共育项目",
phase: "项目承接",
id: e.project?.id || "PRJ-待创建",
status: e.project?.status || "未创建",
owner: e.project?.owner || "专业负责人",
description: "把企业需求、专业建议和改进任务绑定为同一个校企共育项目,避免任务与项目脱节。",
input: "需求 ID、建议 ID、任务 ID",
output: "三类实体强绑定的项目",
complete: Boolean(e.project),
risk: ["需整改", "已退回"].includes(e.project?.status),
relations: [e.project?.demandId, e.project?.recommendationId, e.project?.taskId],
facts: [
["项目名称", e.project?.title || "待创建"],
["牵头专业", e.project?.profession || "待确定"],
["负责人", e.project?.owner || "待指定"],
["项目进度", `${e.project?.progress || 0}%`],
],
},
{
key: "evidence",
title: "执行证据",
phase: "过程执行",
id: evidence?.id || "EVD-待提交",
status: e.evidences.length ? `已提交 ${e.evidences.length} 项 · 进度 ${e.task?.progress || 0}%` : "未提交",
owner: "专业 / 学院负责人",
description: "提交课程方案、阶段成果、评审纪要或学生作品,并同步更新任务与项目进度。",
input: "过程产物、提交人、文件引用",
output: "带时间和对象关联的验收证据",
complete:
e.evidences.length > 0 &&
e.task?.progress === 100 &&
e.project?.progress === 100 &&
!["需整改", "已退回"].includes(e.task?.status),
risk: ["需整改", "已退回"].includes(e.task?.status),
relations: [evidence?.taskId, evidence?.projectId],
facts: [
["最新证据", evidence?.name || "待提交"],
["证据类型", evidence?.type || "待选择"],
["文件引用", evidence?.fileRef || "待上传"],
["提交人", evidence?.submitter || "待提交"],
],
},
{
key: "evaluation",
title: "企业评价",
phase: "企业复核",
id: e.evaluation?.id || "EVL-待提交",
status: e.evaluation?.result || "未评价",
owner: "企业导师",
description: "企业导师只评价本企业关联对象;需整改会回流到执行证据节点,通过后进入校级验收。",
input: "100% 阶段进度与执行证据",
output: "通过或需整改的企业意见",
complete: Boolean(e.evaluation),
risk: e.evaluation?.result === "需整改",
relations: [e.evaluation?.taskId, e.evaluation?.projectId, e.evaluation?.enterpriseId],
facts: [
["评价结论", e.evaluation?.result || "待评价"],
["评价意见", e.evaluation?.comment || "待填写"],
["评价人", e.evaluation?.operator || "待企业导师"],
["评价时间", e.evaluation?.at || "待评价"],
],
},
{
key: "acceptance",
title: "校级验收",
phase: "学校验收",
id: e.task?.status === "待验收" ? "ACC-待验收" : acceptance?.id || "ACC-待验收",
status:
e.task?.status === "待验收"
? "待验收"
: e.task?.status === "已完成"
? "验收通过"
: acceptance?.result || "未到达",
owner: "校级管理者",
description: "校级管理者核对企业评价、任务进度与证据后验收;退回时重新进入证据补充。",
input: "企业通过评价、任务和项目证据",
output: "校级验收结论",
complete: e.task?.status === "已完成",
risk: e.task?.status === "已退回",
relations: [e.task?.id, e.evaluation?.id, e.project?.id],
facts: [
["验收结论", acceptance?.result || "待验收"],
["验收意见", acceptance?.comment || "待填写"],
["验收人", acceptance?.operator || "待校级管理者"],
["任务状态", e.task?.status || "未创建"],
],
},
{
key: "report",
title: "报告审批发布",
phase: "正式交付",
id: e.report?.id || "RPT-待生成",
status: e.report?.status || "未生成",
owner: "编制人 / 独立审批人",
description: "报告关联需求、快照、任务和证据,依次经过草稿、提交、独立审批和正式发布。",
input: "验收通过的任务与报告生成时登记的证据(不可追加)",
output: "可归档的正式报告版本",
complete: e.report?.status === "已发布",
risk: e.report?.status === "已退回",
relations: [e.report?.snapshotId, e.report?.demandId, e.report?.taskId],
facts: [
["报告名称", e.report?.title || "待生成"],
["当前状态", e.report?.status || "未生成"],
["提交人", e.report?.submittedBy || "待提交"],
["批准人", e.report?.approvedBy || "待独立审批"],
],
},
{
key: "feedback",
title: "业务使用反馈",
phase: "成果使用",
id: feedback?.id || "FBK-待登记",
status: feedback?.usable || "未反馈",
owner: "就业老师",
description: "就业老师确认报告和数据是否可用于就业业务,并记录下一周期需要继续补充的事项。",
input: "正式报告与业务使用结果",
output: "可用性确认和下一周期关注项",
complete: e.usageFeedback.length > 0 && feedback?.usable === "可用",
risk: feedback?.usable === "需补充",
relations: [feedback?.reportId, feedback?.snapshotId],
facts: [
["使用部门", feedback?.department || "待登记"],
["可用性", feedback?.usable || "待确认"],
["使用反馈", feedback?.comment || "待填写"],
["下一周期", feedback?.nextCycle || "待登记"],
],
},
{
key: "review",
title: "成效复核",
phase: "周期复盘",
id: e.review?.id || "RVW-待复核",
status: e.review?.decision || "未复核",
owner: "专业负责人",
description: "对比前后周期指标,判断改善、无变化或恶化,并决定关闭或进入下一周期。",
input: "前后指标、使用反馈和改进证据",
output: "关闭结论或下一周期起点",
complete: e.review?.decision === "关闭",
risk: e.review?.conclusion === "恶化",
relations: [e.review?.id, e.report?.id, feedback?.id],
facts: [
["前周期指标", e.review ? String(e.review.before) : "待记录"],
["本周期指标", e.review ? String(e.review.after) : "待记录"],
["复核结论", e.review?.conclusion || "待判断"],
["周期决定", e.review?.decision || "待决定"],
],
},
];
const actionsByNode = Object.entries(closedLoopActionCatalog).reduce((grouped, [action, config]) => {
const actions = grouped.get(config.nodeKey) || [];
actions.push(action);
grouped.set(config.nodeKey, actions);
return grouped;
}, new Map());
let currentAssigned = false;
return rawNodes.map((node, index) => {
const actions = actionsByNode.get(node.key) || [];
const reached = node.reached ?? actions.some((action) => closedLoopActionState(action).enabled);
let state = "locked";
if (node.complete) {
state = "complete";
} else if (!currentAssigned && node.risk) {
state = "risk";
currentAssigned = true;
} else if (!currentAssigned && reached) {
state = "current";
currentAssigned = true;
}
const [x, y] = CLOSED_LOOP_NODE_POSITIONS[node.key];
return { ...node, actions, reached, index, state, x, y };
});
}
function closedLoopCurrentNode(nodes) {
return nodes.find((node) => ["current", "risk"].includes(node.state)) || null;
}
function closedLoopNodeStatusClass(node) {
if (node.risk) return "risk";
if (node.state === "complete") return "done";
if (node.state === "risk") return "risk";
if (node.state === "current") return "review";
return "progress";
}
function renderClosedLoopNode(node, selectedKey) {
const selected = node.key === selectedKey;
return `
`;
}
function closedLoopBranchDefinitions(nodes) {
const e = closedLoopState.entities;
const byKey = new Map(nodes.map((node) => [node.key, node]));
const size = closedLoopNodeSize();
const demand = closedLoopNodeGeometry("demand", size);
const recommendation = closedLoopNodeGeometry("recommendation", size);
const evidence = closedLoopNodeGeometry("evidence", size);
const evaluation = closedLoopNodeGeometry("evaluation", size);
const acceptance = closedLoopNodeGeometry("acceptance", size);
const review = closedLoopNodeGeometry("review", size);
const remediationElbowY = evaluation.top - 32;
const acceptanceReturnY = acceptance.bottom + 45;
const cycleRailX = closedLoopCanvasSize().width - 35;
const cycleRailY = demand.top - 17;
return [
{
from: "evaluation",
fromLabel: "企业评价",
to: "evidence",
toLabel: "执行证据",
label: "需整改",
description: "需整改时补充证据并重新评价",
tone: "risk",
active: e.evaluation?.result === "需整改",
state: e.evaluation?.result === "需整改" ? "risk" : "branch",
path: `M${evaluation.centerX} ${evaluation.top} V${remediationElbowY} H${evidence.centerX} V${evidence.top}`,
labelPosition: [(evaluation.centerX + evidence.centerX) / 2, remediationElbowY - 8],
},
{
from: "acceptance",
fromLabel: "校级验收",
to: "evidence",
toLabel: "执行证据",
label: "退回补证",
description: "退回时补证并重新进入企业评价",
tone: "risk",
active: e.task?.status === "已退回",
state: e.task?.status === "已退回" ? "risk" : "branch",
path: `M${acceptance.centerX} ${acceptance.bottom} V${acceptanceReturnY} H${evidence.centerX} V${evidence.bottom}`,
labelPosition: [(acceptance.centerX + evidence.centerX) / 2, acceptanceReturnY - 8],
},
{
from: "review",
fromLabel: "成效复核",
to: "demand",
toLabel: "企业需求",
label: "进入下一周期",
description: "进入下一周期时创建新需求版本",
tone: "cycle",
active: Boolean(e.demand.previousVersionId),
state: e.demand.previousVersionId ? (byKey.get("demand").state === "current" ? "current" : "complete") : "branch",
path: `M${review.right} ${review.centerY} H${cycleRailX} V${cycleRailY} H${demand.centerX} V${demand.top}`,
labelPosition: [cycleRailX - 90, recommendation.bottom + 23],
},
];
}
function closedLoopEdgeState(sourceNode, targetNode, branchState = "") {
if (branchState) return branchState;
if (targetNode.state === "complete") return "complete";
if (targetNode.state === "risk") return "risk";
if (targetNode.state === "current") return "current";
return "locked";
}
function renderClosedLoopEdges(nodes) {
const byKey = new Map(nodes.map((node) => [node.key, node]));
const edges = [...closedLoopMainEdgeDefinitions(), ...closedLoopBranchDefinitions(nodes)];
const definitions = `
${["complete", "current", "risk", "locked", "branch"]
.map(
(state) =>
``,
)
.join("")}
`;
return (
definitions +
edges
.map(({ from, to, path, label = "", labelPosition = [], state: branchState = "" }) => {
const state = closedLoopEdgeState(byKey.get(from), byKey.get(to), branchState);
const [labelX, labelY] = labelPosition;
return `
${label ? `${label}` : ""}
`;
})
.join("")
);
}
function renderClosedLoopMobileBranches(nodes) {
return closedLoopBranchDefinitions(nodes)
.map(
(branch) => `
`,
)
.join("");
}
function closedLoopNodeAction(node) {
const role = currentRoleName();
const roleActions = node.actions.filter((action) => closedLoopActionCatalog[action].roles.includes(role));
if (!roleActions.length) return null;
return roleActions.find((action) => actionPrerequisite(action).enabled) || roleActions[0];
}
function renderClosedLoopNodeDetail(node) {
document.getElementById("loopNodeKicker").textContent = `${String(node.index + 1).padStart(2, "0")} · ${node.phase.toUpperCase()}`;
document.getElementById("loopNodeTitle").textContent = node.title;
const status = document.getElementById("loopNodeStatus");
status.className = `status-badge ${closedLoopNodeStatusClass(node)}`;
status.textContent = node.status;
const action = closedLoopNodeAction(node);
const availability = action ? actionPrerequisite(action) : null;
const relations = node.relations.filter(Boolean);
const detailState = node.risk ? "risk" : node.state;
const actionMarkup = action
? `
${closedLoopActionCatalog[action].queueLabel}${availability.reason}
`
: `
当前角色只读该节点由${safeText(node.owner)}负责;切换到对应角色后,可办理满足前置条件的动作。
`;
document.getElementById("loopNodeDetail").innerHTML = `
${String(node.index + 1).padStart(2, "0")}
责任角色${safeText(node.owner)}${safeText(node.description)}
${node.risk ? "需要处理" : node.state === "complete" ? "节点已完成" : node.state === "current" ? "当前待办" : "等待前置节点"}
节点输入${safeText(node.input)}
节点输出${safeText(node.output)}
${node.facts.map(([label, value]) => `${label}${safeText(value)}`).join("")}
关联实体
${relations.map((relation) => ``).join("") || "前置实体完成后自动建立关联"}
${actionMarkup}`;
}
function selectClosedLoopNode(nodeKey, options = {}) {
const nodes = closedLoopNodeDefinitions();
const node = nodes.find((item) => item.key === nodeKey);
if (!node) return null;
closedLoopUiState.selectedNodeKey = node.key;
document.querySelectorAll("[data-loop-node]").forEach((button) => {
const selected = button.dataset.loopNode === node.key;
button.classList.toggle("selected", selected);
button.setAttribute("aria-pressed", String(selected));
});
renderClosedLoopNodeDetail(node);
const selectedButton = document.querySelector(`[data-loop-node="${node.key}"]`);
if (options.focus) selectedButton?.focus({ preventScroll: true });
return { node, selectedButton };
}
function renderClosedLoopAction(action, nodes) {
const availability = actionPrerequisite(action);
const node = nodes.find((item) => item.key === closedLoopActionCatalog[action].nodeKey);
return `
`;
}
function renderClosedLoop() {
const root = document.getElementById("loopChain");
if (!root) return;
const nodes = closedLoopNodeDefinitions();
const currentNode = closedLoopCurrentNode(nodes);
if (!closedLoopUiState.selectedNodeKey || !nodes.some((node) => node.key === closedLoopUiState.selectedNodeKey)) {
closedLoopUiState.selectedNodeKey = currentNode?.key || nodes.at(-1).key;
}
const selectedNode = nodes.find((node) => node.key === closedLoopUiState.selectedNodeKey) || currentNode || nodes.at(-1);
const completedCount = nodes.filter((node) => node.state === "complete").length;
const progress = Math.round((completedCount / nodes.length) * 100);
document.getElementById("loopSchemaBadge").textContent = `SCHEMA V${closedLoopState.schemaVersion}`;
document.getElementById("loopFocusCurrent").disabled = !currentNode;
document.getElementById("loopIdentityStrip").innerHTML = `
共享业务标识对照
租户 ${closedLoopState.shared.tenantId}
厂商包 ${closedLoopState.shared.packageId}
版本 ${closedLoopState.shared.packageVersion}
学校回执 ${closedLoopState.shared.receiptId}
静态原型仅用相同标识核对业务链,不代表厂商端与校园端页面实时同步。`;
document.getElementById("loopFlowSummary").innerHTML = `
流程完成度${progress}%
已完成节点${completedCount} / ${nodes.length}
当前待办${safeText(currentNode?.title || "流程已关闭")}
责任角色${safeText(currentNode?.owner || "无待办")}`;
root.innerHTML = nodes.map((node) => renderClosedLoopNode(node, selectedNode.key)).join("");
const flowSvg = document.getElementById("loopFlowEdges");
const canvasSize = closedLoopCanvasSize();
flowSvg.setAttribute("viewBox", `0 0 ${canvasSize.width} ${canvasSize.height}`);
flowSvg.innerHTML = renderClosedLoopEdges(nodes);
document.getElementById("loopMobileBranches").innerHTML = renderClosedLoopMobileBranches(nodes);
renderClosedLoopNodeDetail(selectedNode);
const role = currentRoleName();
const roleActions = Object.entries(closedLoopActionCatalog)
.filter(([, config]) => config.roles.includes(role))
.map(([action]) => action);
const boundaryNote = {
校级管理者: "不代替技术接收或专业决策。",
校级技术管理员: "技术质量职责不包含业务验收或报告批准。",
学院负责人: "校级验收、审批和发布节点不可用。",
专业负责人: "校级验收、批准和发布仍由校级管理者办理。",
就业老师: "不能进入校级报告审批节点。",
企业导师: "仅限本企业授权对象,不能操作学校内部审批。",
厂商超级管理员: "校园流程只读;共享标识用于业务对照,不构成实时同步。",
};
const capabilityText = roleActions.length
? `可办理:${roleActions.map((action) => closedLoopActionCatalog[action].queueLabel).join("、")}。`
: "本流程无可办理动作。";
document.getElementById("loopRoleActionTitle").textContent = `${role}可办理节点`;
document.getElementById("loopRoleBoundary").innerHTML = `权限边界${capabilityText}${boundaryNote[role]}
`;
document.getElementById("loopActionList").innerHTML =
roleActions.map((action) => renderClosedLoopAction(action, nodes)).join("") ||
`当前为只读视角切换校园业务角色后,右侧会显示该角色可办理的流程节点。
`;
document.getElementById("loopAuditCount").textContent = `${closedLoopState.auditEvents.length} 条`;
document.getElementById("loopAuditList").innerHTML = closedLoopState.auditEvents
.slice(0, 18)
.map(
(event) =>
`${safeText(event.action)} · ${safeText(event.objectId)}${safeText(event.operator)} · ${safeText(event.at)}${safeText(event.detail)}`,
)
.join("");
}
function openClosedLoopAction(action) {
const config = closedLoopActionCatalog[action];
const availability = actionPrerequisite(action);
if (!config || !availability.enabled) {
showToast(availability.reason, "前置条件未满足");
return;
}
selectClosedLoopNode(closedLoopActionCatalog[action].nodeKey || closedLoopUiState.selectedNodeKey);
const e = closedLoopState.entities;
const receiveDefaults = {
tenantId: e.receipt.tenantId,
packageId: e.receipt.packageId,
packageVersion: e.receipt.packageVersion,
checkedRecords: e.receipt.receivedRecords || e.receipt.expectedRecords,
sha256: e.receipt.receivedSha256 || e.receipt.expectedSha256,
};
const existingByAction = {
demand: e.demand,
receive: e.receipt,
decision: e.recommendation,
project: e.project || {},
evidence: {},
evaluation: e.evaluation || {},
acceptance: {},
reportCreate: e.report || {},
reportSubmit: {},
reportReview: {},
reportPublish: {},
feedback: {},
review: e.review || {},
}[action];
document.getElementById("closedLoopModalKicker").textContent = `WORKFLOW NODE · ${action.toUpperCase()}`;
document.getElementById("closedLoopModalTitle").textContent = config.title;
document.getElementById("closedLoopModalDescription").textContent = config.description;
document.getElementById("closedLoopForm").dataset.action = action;
document.getElementById("closedLoopFields").innerHTML = config.fields
.map(([name, label, type, fallback]) => {
const existingValue = action === "receive" ? receiveDefaults[name] : existingByAction?.[name];
const value = existingValue ?? fallback;
if (type === "textarea") return ``;
if (type === "select") {
return ``;
}
return ``;
})
.join("");
openModal(document.getElementById("closedLoopModal"));
}
function nextClosedLoopCycle(currentCycle) {
const match = /^(\d{4})C([12])$/.exec(currentCycle || "");
if (!match) return "2027C1";
const year = Number(match[1]);
return match[2] === "1" ? `${year}C2` : `${year + 1}C1`;
}
function closedLoopCycleEntityId(prefix, owner, cycle, sequence) {
return `${prefix}-${owner}-${cycle}-${String(sequence).padStart(2, "0")}`;
}
const CLOSED_LOOP_LEGACY_ENTITY_IDS = {
task: "TSK-SOFTWARE-20260823-01",
project: "PRJ-HKVS-20260823-01",
evaluation: "EVL-HKVS-20260823-01",
acceptance: "ACC-HZVTC-20260823-01",
report: "RPT-HZVTC-20260823-01",
review: "RVW-SOFTWARE-2027C1-01",
};
function nextClosedLoopEntityId(kind, prefix, owner) {
const sequence = ++closedLoopState.sequences[kind];
const cycle = closedLoopState.entities.demand.cycle || "2026C2";
if (cycle === "2026C2" && sequence === 1) return CLOSED_LOOP_LEGACY_ENTITY_IDS[kind];
return closedLoopCycleEntityId(prefix, owner, cycle, sequence);
}
function startClosedLoopNextCycle(review, at) {
const e = closedLoopState.entities;
const previousCycle = e.demand.cycle || "2026C2";
const previous = {
demandId: e.demand.id,
snapshotId: e.snapshot.id,
reportId: e.report?.id || "",
feedbackId: e.usageFeedback.at(-1)?.id || "",
reviewId: review.id,
};
if (e.report) {
const reportTemplate =
reports.find((report) => report.type === "school" && !report._closedLoop && !report._customPersisted) ||
reports[0];
closedLoopState.custom.reports.unshift({
...reportTemplate,
...e.report,
evidenceIds: Array.isArray(e.report.evidenceIds) ? [...e.report.evidenceIds] : [],
evidences: e.evidences.map((item) => ({ ...item })),
history: e.report.history.map((item) => ({ ...item })),
status: "已归档",
statusClass: "review",
updated: at,
archivedAt: at,
archivedCycle: previousCycle,
featured: false,
roles: ["校级管理者", "校级技术管理员", "学院负责人", "专业负责人", "就业老师"],
});
}
const nextSequence = ++closedLoopState.sequences.cycle;
const nextCycle = nextClosedLoopCycle(previousCycle);
const nextDemandId = closedLoopCycleEntityId("DEM", "HKVS", nextCycle, nextSequence);
const nextSnapshotId = closedLoopCycleEntityId("SNP", "HZVTC", nextCycle, nextSequence);
const nextReceiptId = closedLoopCycleEntityId("RCT", "HZVTC", nextCycle, nextSequence);
e.demand = {
...e.demand,
id: nextDemandId,
status: "草稿",
submittedAt: "",
previousVersionId: previous.demandId,
cycle: nextCycle,
};
e.snapshot = {
...e.snapshot,
id: nextSnapshotId,
status: "待接收",
receivedAt: "",
acceptedAt: "",
operator: "",
checkedRecords: 0,
previousSnapshotId: previous.snapshotId,
};
e.recommendation = {
...e.recommendation,
id: closedLoopCycleEntityId("REC", "SOFTWARE", nextCycle, nextSequence),
status: "待决策",
reason: "",
operator: "",
decidedAt: "",
snapshotId: nextSnapshotId,
demandId: nextDemandId,
};
Object.assign(e, {
task: null,
project: null,
evidences: [],
evaluation: null,
report: null,
usageFeedback: [],
review: null,
receipt: {
...e.receipt,
id: nextReceiptId,
status: "待技术确认",
receivedRecords: 0,
receivedSha256: "",
verification: null,
note: "",
technicalConfirmedBy: "",
technicalConfirmedAt: "",
businessUsable: false,
},
});
closedLoopState.shared.receiptId = nextReceiptId;
addClosedLoopAudit(
"NEXT_CYCLE_STARTED",
nextDemandId,
`由 ${previous.reviewId} 启动 ${nextCycle};上一周期 ${previous.demandId} / ${previous.snapshotId} / ${previous.reportId || "无报告"} / ${previous.feedbackId || "无反馈"} 已归档留痕。`,
);
}
function applyClosedLoopAction(action, values) {
const e = closedLoopState.entities;
const at = nowText();
if (action === "demand") {
Object.assign(e.demand, values, { headcount: Number(values.headcount), status: "已提交", submittedAt: at });
addClosedLoopAudit("DEMAND_SUBMITTED", e.demand.id, `${values.position} · ${values.headcount}人 · ${values.authorizationNo}`);
} else if (action === "receive") {
Object.assign(e.receipt, {
status: "已技术确认",
receivedRecords: Number(values.checkedRecords),
receivedSha256: values.sha256,
note: values.note,
verification: {
tenantMatched: true,
packageMatched: true,
versionMatched: true,
recordCountMatched: true,
sha256Matched: true,
verifiedAt: at,
},
technicalConfirmedBy: currentRoleUser(),
technicalConfirmedAt: at,
});
Object.assign(e.snapshot, {
packageId: values.packageId,
packageVersion: values.packageVersion,
sha256: values.sha256,
sourcePackageRecords: Number(values.checkedRecords),
checkedRecords: e.snapshot.expectedRecords,
status: "已验收",
receivedAt: at,
acceptedAt: at,
operator: currentRoleUser(),
});
addClosedLoopAudit(
"PACKAGE_RECEIVED_SNAPSHOT_ACCEPTED",
e.snapshot.id,
`${values.tenantId} · ${values.packageId} · ${values.packageVersion} · 交付 ${Number(values.checkedRecords).toLocaleString("zh-CN")} 条 · 分析快照 ${e.snapshot.checkedRecords.toLocaleString("zh-CN")} 条 · SHA ${values.sha256}`,
);
} else if (action === "decision") {
Object.assign(e.recommendation, {
status: values.decision === "采纳" ? "已采纳" : values.decision,
reason: values.reason,
operator: currentRoleUser(),
decidedAt: at,
});
addClosedLoopAudit("RECOMMENDATION_DECIDED", e.recommendation.id, `${values.decision} · ${values.reason}`);
if (values.decision === "采纳" && !e.task) {
e.task = {
id: nextClosedLoopEntityId("task", "TSK", "SOFTWARE"),
status: "进行中",
title: e.recommendation.title,
profession: "软件技术",
owner: values.owner,
due: values.due,
progress: 10,
demandId: e.demand.id,
recommendationId: e.recommendation.id,
progressLogs: [{ at, operator: currentRoleUser(), progress: 10, detail: "建议采纳,任务自动创建。" }],
acceptanceHistory: [],
};
addClosedLoopAudit("TASK_AUTO_CREATED", e.task.id, `由建议 ${e.recommendation.id} 自动创建。`);
}
} else if (action === "project") {
e.project = {
id: nextClosedLoopEntityId("project", "PRJ", "HKVS"),
status: "进行中",
title: values.title,
profession: values.profession,
owner: values.owner,
progress: e.task?.progress || 10,
demandId: e.demand.id,
recommendationId: e.recommendation.id,
taskId: e.task.id,
createdAt: at,
};
addClosedLoopAudit("PROJECT_CREATED", e.project.id, `绑定 ${e.demand.id} / ${e.recommendation.id} / ${e.task.id}`);
} else if (action === "evidence") {
const reopeningEvaluation = ["需整改", "已退回"].includes(e.task.status);
const evidence = {
id: `EVD-HKVS-${String(++closedLoopState.sequences.evidence).padStart(2, "0")}`,
name: values.name,
type: values.type,
description: values.description,
submitter: values.submitter,
submittedAt: at,
fileRef: values.fileRef,
taskId: e.task.id,
projectId: e.project.id,
};
e.evidences.push(evidence);
e.task.progress = Math.min(100, Number(values.progress));
e.task.status = e.task.progress >= 100 ? "待评价" : "进行中";
e.task.progressLogs.push({ at, operator: currentRoleUser(), progress: e.task.progress, detail: values.description });
e.project.progress = e.task.progress;
e.project.status = e.task.progress >= 100 ? "待企业评价" : "进行中";
if (reopeningEvaluation) {
e.evaluation = null;
addClosedLoopAudit("ENTERPRISE_REEVALUATION_REQUIRED", e.task.id, "整改证据已补充,原企业评价不再作为当前验收依据。");
}
addClosedLoopAudit("EVIDENCE_SUBMITTED", evidence.id, `${values.name} · ${values.fileRef} · 进度${e.task.progress}%`);
} else if (action === "evaluation") {
e.evaluation = {
id: nextClosedLoopEntityId("evaluation", "EVL", "HKVS"),
result: values.result,
comment: values.comment,
enterpriseId: "ENT-HKVS",
taskId: e.task.id,
projectId: e.project.id,
operator: currentRoleUser(),
at,
};
e.task.status = values.result === "通过" ? "待验收" : "需整改";
e.project.status = values.result === "通过" ? "待校级验收" : "需整改";
addClosedLoopAudit("ENTERPRISE_EVALUATION_SUBMITTED", e.evaluation.id, `${values.result} · ${values.comment}`);
} else if (action === "acceptance") {
const passed = values.result === "验收通过";
e.task.status = passed ? "已完成" : "已退回";
e.project.status = passed ? "已验收" : "已退回";
e.task.progress = passed ? 100 : Math.min(e.task.progress, 90);
e.project.progress = e.task.progress;
e.task.acceptanceHistory.push({
id: nextClosedLoopEntityId("acceptance", "ACC", "HZVTC"),
result: values.result,
comment: values.comment,
operator: currentRoleUser(),
at,
});
addClosedLoopAudit("SCHOOL_ACCEPTANCE_RECORDED", e.task.id, `${values.result} · ${values.comment}`);
} else if (action === "reportCreate") {
e.report = {
id: nextClosedLoopEntityId("report", "RPT", "HZVTC"),
title: values.title,
version: CLOSED_LOOP_SHARED.packageVersion,
status: "草稿",
demandId: e.demand.id,
snapshotId: e.snapshot.id,
taskId: e.task.id,
evidenceIds: e.evidences.map((item) => item.id),
createdBy: currentRoleUser(),
submittedBy: "",
approvedBy: "",
updatedAt: at,
history: [{ status: "草稿", comment: "关联流程实体生成。", operator: currentRoleUser(), at }],
};
addClosedLoopAudit("REPORT_DRAFT_CREATED", e.report.id, `关联 ${e.snapshot.id} / ${e.demand.id} / ${e.task.id} / ${e.evidences.length}项证据`);
} else if (action === "reportSubmit") {
e.report.status = "待审核";
e.report.submittedBy = currentRoleUser();
e.report.approvedBy = "";
e.report.updatedAt = at;
e.report.history.push({ status: "待审核", comment: values.comment, operator: currentRoleUser(), at });
addClosedLoopAudit("REPORT_SUBMITTED", e.report.id, values.comment);
} else if (action === "reportReview") {
e.report.status = values.result === "批准" ? "已批准" : "已退回";
e.report.approvedBy = values.result === "批准" ? values.approver : "";
e.report.updatedAt = at;
e.report.history.push({ status: e.report.status, comment: values.comment, operator: values.approver, at });
addClosedLoopAudit("REPORT_REVIEWED", e.report.id, `${values.result} · ${values.approver} · ${values.comment}`, values.approver, "校级管理者");
} else if (action === "reportPublish") {
e.report.status = "已发布";
e.report.updatedAt = at;
e.report.history.push({ status: "已发布", comment: values.comment, operator: currentRoleUser(), at });
addClosedLoopAudit("REPORT_PUBLISHED", e.report.id, values.comment);
} else if (action === "feedback") {
const feedback = {
id: `FBK-EMP-${String(++closedLoopState.sequences.feedback).padStart(2, "0")}`,
...values,
operator: currentRoleUser(),
at,
reportId: e.report.id,
snapshotId: e.snapshot.id,
};
e.usageFeedback.push(feedback);
e.receipt.businessUsable = values.usable === "可用";
e.receipt.status = values.usable === "可用" ? "业务确认可用" : "业务要求补充";
addClosedLoopAudit("DATA_USAGE_FEEDBACK_ADDED", feedback.id, `${values.department} · ${values.usable} · 下一周期:${values.nextCycle}`);
} else if (action === "review") {
const review = {
id: nextClosedLoopEntityId("review", "RVW", "SOFTWARE"),
before: Number(values.before),
after: Number(values.after),
conclusion: values.conclusion,
decision: values.decision,
comment: values.comment,
operator: currentRoleUser(),
at,
};
e.review = review;
addClosedLoopAudit("NEXT_CYCLE_REVIEW_RECORDED", review.id, `${values.before}→${values.after} · ${values.conclusion} · ${values.decision}`);
if (values.decision === "进入下一周期") startClosedLoopNextCycle(review, at);
}
closedLoopUiState.selectedNodeKey = null;
saveAndRenderClosedLoop(`${closedLoopActionCatalog[action].title}已完成`);
}
const reports = [
{
id: "school-2026-03",
type: "school",
typeLabel: "校级报告",
title: "2026年春季全校产教供需分析总报告",
scope: "全校 32 个专业",
period: "2026年3月",
periodKey: "current",
pages: 42,
version: "V2026.03",
status: "已发布",
statusClass: "done",
updated: "今天 09:30",
author: "教务处 · 就业处",
roles: ["校级管理者"],
featured: true,
summary:
"本期全校供需匹配度为78.6分,较上一分析周期提升4.2分。智能制造、数字商贸和云平台方向需求增长明显,但云原生、工业视觉、跨境运营等技能仍存在结构性缺口。",
metrics: [
{ label: "供需匹配度", value: "78.6", note: "较上期 +4.2" },
{ label: "产业岗位样本", value: "18,420", note: "近90天 +12.8%" },
{ label: "重点技能缺口", value: "23", note: "8项高优先级" },
{ label: "改进完成率", value: "68%", note: "21/31项已完成" },
],
findings: [
{ title: "云平台与DevOps岗位快速增长", detail: "近90天需求增长31%,软件技术和云计算方向课程覆盖仍不足。", value: "+31%" },
{ title: "工业视觉成为智能制造新缺口", detail: "机器视觉相关需求增长38%,涉及机电、工业机器人等4个专业。", value: "+38%" },
{ title: "跨境电商人才需求由运营转向复合能力", detail: "企业更关注数据分析、内容生产和海外合规履约能力。", value: "6项缺口" },
],
bars: [
{ label: "信息技术类", value: 86 },
{ label: "智能制造类", value: 82 },
{ label: "数字商贸类", value: 75 },
{ label: "文化创意类", value: 72 },
{ label: "现代服务类", value: 69 },
],
actions: [
{ priority: "P0", title: "推动11个专业处理高优技能缺口", detail: "由教务处牵头,4月底前提交课程调整或实训项目方案。" },
{ priority: "P1", title: "建设工业视觉与云原生两类校企共享实训", detail: "优先对接大华、数梦工场等已表达合作意向的企业。" },
{ priority: "P1", title: "每季度请企业确认一次需求", detail: "把互联网招聘信息与合作企业确认的需求分开,避免混用。" },
],
},
{
id: "software-2026-03",
type: "profession",
typeLabel: "专业报告",
title: "软件技术专业人才需求分析报告",
scope: "信息工程学院 · 软件技术",
period: "2026年3月",
periodKey: "current",
pages: 28,
version: "V2026.03",
status: "已发布",
statusClass: "done",
updated: "今天 09:18",
author: "信息工程学院",
roles: ["校级管理者", "学院负责人"],
featured: true,
summary:
"软件技术专业供需匹配度84.2分,整体处于高匹配区间。Java后端和测试开发培养基础较好,云原生部署、AI辅助开发和数据工程能力是下一阶段课程建设重点。",
metrics: [
{ label: "专业匹配度", value: "84.2", note: "全校排名第3" },
{ label: "对口岗位需求", value: "2,864", note: "近90天 +18.6%" },
{ label: "课程技能覆盖", value: "76%", note: "31/41项已覆盖" },
{ label: "潜力合作企业", value: "12", note: "4家高意向" },
],
findings: [
{ title: "Java/Spring能力供需匹配良好", detail: "市场需求强度92%,课程覆盖86%,可继续保持项目化教学。", value: "差值6" },
{ title: "Docker/K8s技能缺口明显", detail: "市场需求81%,课程覆盖仅32%,相差49个百分点。", value: "-49" },
{ title: "AI辅助开发进入企业基础要求", detail: "69%的重点岗位提出相关要求,当前课程覆盖仅28%。", value: "-41" },
],
bars: [
{ label: "Java / Spring", value: 92 },
{ label: "Python应用", value: 84 },
{ label: "Docker / K8s", value: 81 },
{ label: "数据库技术", value: 78 },
{ label: "AI辅助开发", value: 69 },
],
actions: [
{ priority: "P0", title: "强化云原生与容器化实训模块", detail: "在综合实训中覆盖Docker、Kubernetes、CI/CD和可观测性。" },
{ priority: "P1", title: "将AI辅助开发纳入软件工程实践", detail: "增加代码生成、测试辅助、安全审查和人工复核规范。" },
{ priority: "P1", title: "联合企业建设交付项目", detail: "引入数梦工场、海康威视的工程任务和企业导师评价。" },
],
},
{
id: "college-info-2026-03",
type: "college",
typeLabel: "学院报告",
title: "信息工程学院供需分析与专业建设报告",
scope: "信息工程学院 · 6个专业",
period: "2026年3月",
periodKey: "current",
pages: 36,
version: "V2026.03",
status: "待审核",
statusClass: "review",
updated: "今天 08:55",
author: "信息工程学院",
roles: ["校级管理者", "学院负责人"],
summary:
"学院6个专业平均匹配度80.4分,软件技术、物联网应用技术表现较好。云平台、数据工程和 AI 应用课程可由多个专业共用。",
metrics: [
{ label: "学院匹配度", value: "80.4", note: "较上期 +5.1" },
{ label: "覆盖专业", value: "6", note: "2个高匹配专业" },
{ label: "相关岗位", value: "6,120", note: "近90天样本" },
{ label: "合作企业", value: "38", note: "本学期新增6家" },
],
findings: [
{ title: "基础技术课程重复开设", detail: "数据库、Linux 和软件工程内容在多个专业重复,可由学院统一开设共用模块。", value: "6专业" },
{ title: "5个专业都缺云平台课程内容", detail: "云部署、容器和运维内容在5个专业中均覆盖不足。", value: "5专业" },
{ title: "企业项目集中度偏高", detail: "前5家企业承担了61%的项目,需要扩展中型技术企业伙伴。", value: "61%" },
],
bars: [
{ label: "软件技术", value: 84 },
{ label: "物联网应用", value: 80 },
{ label: "大数据技术", value: 79 },
{ label: "人工智能技术", value: 78 },
{ label: "计算机网络", value: 75 },
],
actions: [
{ priority: "P0", title: "开设学院共用工程实践课程", detail: "云平台、数据库和 DevOps 基础内容由学院共用,各专业再安排不同项目。" },
{ priority: "P1", title: "扩展8家中型技术企业合作", detail: "减少项目对少数企业的依赖,为不同专业提供更多企业任务。" },
{ priority: "P1", title: "建立跨专业学生项目团队", detail: "围绕智能物联场景组织软件、数据和网络方向联合实训。" },
],
},
{
id: "gap-2026-03",
type: "industry",
typeLabel: "产业专题",
title: "杭州重点产业人才缺口专题报告",
scope: "杭州重点产业 · 全校专业",
period: "2026年3月",
periodKey: "current",
pages: 31,
version: "V2026.03",
status: "已发布",
statusClass: "done",
updated: "昨天 17:40",
author: "产教融合中心",
roles: ["校级管理者"],
summary:
"杭州区域人才需求持续向智能制造、云计算、跨境电商和数字内容方向集中。全校有23项重点技能缺口,其中8项需要在本学期安排课程或项目调整。",
metrics: [
{ label: "重点缺口", value: "23", note: "8项高优" },
{ label: "关联专业", value: "11", note: "覆盖5个学院" },
{ label: "相关企业", value: "326", note: "126家已确认" },
{ label: "岗位增长", value: "24.8%", note: "重点方向均值" },
],
findings: [
{ title: "工业视觉缺口最为紧迫", detail: "相关岗位增长38%,学校课程平均覆盖不足40%。", value: "+38%" },
{ title: "跨境运营强调数据与合规", detail: "单一平台运营能力已经不能满足企业复合岗位要求。", value: "83%需求" },
{ title: "云平台运维需求外溢到传统专业", detail: "智能制造和物联网岗位也开始要求设备上云与运维能力。", value: "7专业" },
],
bars: [
{ label: "工业视觉", value: 92 },
{ label: "云平台运维", value: 88 },
{ label: "跨境电商", value: 83 },
{ label: "数据工程", value: 79 },
{ label: "AI内容生产", value: 75 },
],
actions: [
{ priority: "P0", title: "针对8项高优缺口建立校级响应清单", detail: "明确对应学院、专业负责人、企业伙伴和阶段性交付物。" },
{ priority: "P1", title: "将企业确认数据纳入缺口复核", detail: "避免只依赖互联网招聘信息推断实际人才缺口。" },
{ priority: "P1", title: "每季度更新重点产业技能词典", detail: "由行业专家和专业负责人共同审核新增技能及映射关系。" },
],
},
{
id: "salary-2026-03",
type: "industry",
dataProfile: "salary",
typeLabel: "产业专题",
title: "2026届实习就业薪资竞争力分析",
scope: "全校2026届学生",
period: "2026年3月",
periodKey: "current",
pages: 24,
version: "V2026.03",
status: "已发布",
statusClass: "done",
updated: "昨天 16:20",
author: "就业处",
roles: ["校级管理者"],
summary:
"2026届学生就业薪资中位数为8,640元,达到区域同岗位中位数的98%。信息技术类专业高于区域基准,现代服务和文化创意类仍有提升空间。",
metrics: [
{ label: "就业薪资中位数", value: "8,640", note: "区域基准98%" },
{ label: "实习月薪中位数", value: "3,820", note: "同比 +8.4%" },
{ label: "高于市场专业", value: "12", note: "占全部37.5%" },
{ label: "有效薪资样本", value: "6,284", note: "完整度91.2%" },
],
findings: [
{ title: "软件技术薪资竞争力领先", detail: "应届岗位中位数达到区域同岗位的108%。", value: "1.08" },
{ title: "技能证据显著影响起薪", detail: "具备企业项目或高质量作品集的学生起薪平均高14%。", value: "+14%" },
{ title: "服务类专业薪资离散度较大", detail: "企业规模和岗位类型对薪资影响明显,需加强预期指导。", value: "差异32%" },
],
bars: [
{ label: "信息技术类", value: 108 },
{ label: "智能制造类", value: 102 },
{ label: "数字商贸类", value: 96 },
{ label: "文化创意类", value: 91 },
{ label: "现代服务类", value: 88 },
],
actions: [
{ priority: "P1", title: "将企业项目成果纳入就业推荐材料", detail: "用学生作品和企业评价说明其岗位能力。" },
{ priority: "P1", title: "建立分专业薪资预期指导", detail: "使用分位数而非平均数,为学生提供地域和岗位差异说明。" },
{ priority: "P2", title: "持续核验企业薪资数据完整性", detail: "对缺失和区间过宽的薪资记录降低统计权重。" },
],
},
{
id: "quality-2026-03",
type: "quality",
typeLabel: "数据质量",
title: "2026年3月数据资产质量与口径报告",
scope: "3类数据资产 · 6个数据源",
period: "2026年3月",
periodKey: "current",
pages: 19,
version: "V2026.03",
status: "已发布",
statusClass: "done",
updated: "昨天 15:05",
author: "数据管理中心",
roles: ["校级管理者", "校级技术管理员"],
summary:
"本期分析快照包含128,642条有效记录,总体质量评分92.6。互联网基础数据包、校级数据和企业数据均保留独立权属与授权标识。",
metrics: [
{ label: "质量评分", value: "92.6", note: "达到分析阈值" },
{ label: "有效记录", value: "128,642", note: "去重后快照" },
{ label: "归一覆盖率", value: "94.8%", note: "218项待复核" },
{ label: "数据源", value: "6", note: "三类资产" },
],
findings: [
{ title: "就业薪资字段存在缺失", detail: "涉及86条记录,已在薪资结论中降低相关权重。", value: "86条" },
{ title: "岗位归一覆盖率达到分析要求", detail: "剩余218个新岗位名称进入人工审核队列。", value: "94.8%" },
{ title: "企业数据授权记录完整", detail: "126家合作企业均保留提交时间和授权场景。", value: "100%" },
],
bars: [
{ label: "完整性", value: 95 },
{ label: "准确性", value: 91 },
{ label: "及时性", value: 97 },
{ label: "一致性", value: 93 },
{ label: "归一覆盖", value: 95 },
],
actions: [
{ priority: "P0", title: "补齐就业薪资缺失记录", detail: "由就业处在本期正式材料发布前完成字段确认。" },
{ priority: "P1", title: "审核218个新增岗位名称", detail: "先处理需求量前50的岗位,并把确认后的别名加入规则表。" },
{ priority: "P2", title: "增加数据包服务期自动提醒", detail: "基础数据包到期前30天通知厂商和学校管理员。" },
],
},
{
id: "mechatronics-2026-03",
type: "profession",
typeLabel: "专业报告",
title: "机电一体化技术专业人才需求分析报告",
scope: "智能制造学院 · 机电一体化",
period: "2026年3月",
periodKey: "current",
pages: 27,
version: "V2026.03",
status: "已发布",
statusClass: "done",
updated: "2026-03-20",
author: "智能制造学院",
roles: ["校级管理者"],
summary: "机电一体化专业匹配度81.7分,PLC和设备调试基础扎实,工业视觉与设备数字孪生是主要技能缺口。",
metrics: [
{ label: "专业匹配度", value: "81.7", note: "较上期 +3.6" },
{ label: "对口岗位", value: "2,146", note: "近90天 +21%" },
{ label: "技能覆盖", value: "72%", note: "工业视觉不足" },
{ label: "潜力企业", value: "9", note: "3家高意向" },
],
findings: [
{ title: "PLC能力覆盖充足", detail: "课程覆盖88%,与企业岗位要求保持一致。", value: "88%" },
{ title: "工业视觉缺口扩大", detail: "岗位增长38%,课程覆盖仅38%。", value: "-44" },
{ title: "数字孪生进入设备岗位要求", detail: "龙头企业正在将相关能力纳入工程师岗位。", value: "+29%" },
],
bars: [
{ label: "PLC编程", value: 94 },
{ label: "工业机器人", value: 89 },
{ label: "机器视觉", value: 82 },
{ label: "数字孪生", value: 73 },
{ label: "电气制图", value: 68 },
],
actions: [
{ priority: "P0", title: "新增工业视觉综合实训", detail: "联合装备企业建设缺陷检测与产线集成任务。" },
{ priority: "P1", title: "引入数字孪生教学案例", detail: "将现有PLC实训映射到设备数据与仿真场景。" },
],
},
{
id: "ecommerce-2026-03",
type: "profession",
typeLabel: "专业报告",
title: "电子商务专业人才需求分析报告",
scope: "商贸旅游学院 · 电子商务",
period: "2026年3月",
periodKey: "current",
pages: 26,
version: "V2026.03",
status: "待审核",
statusClass: "review",
updated: "2026-03-19",
author: "商贸旅游学院",
roles: ["校级管理者"],
summary: "电子商务专业匹配度74.8分,内容运营基础较好,跨境运营、数据分析和AI内容生产需要重点强化。",
metrics: [
{ label: "专业匹配度", value: "74.8", note: "待关注" },
{ label: "对口岗位", value: "1,932", note: "近90天 +13%" },
{ label: "技能覆盖", value: "68%", note: "6项明显缺口" },
{ label: "潜力企业", value: "15", note: "4家高意向" },
],
findings: [
{ title: "内容运营保持较高需求", detail: "相关岗位需求强度91%,课程覆盖76%。", value: "91%" },
{ title: "跨境平台运营覆盖不足", detail: "岗位增长34%,课程覆盖仅43%。", value: "-40" },
{ title: "AI内容生产成为新要求", detail: "75%的重点企业提出相关工具能力。", value: "75%" },
],
bars: [
{ label: "内容运营", value: 91 },
{ label: "数据分析", value: 86 },
{ label: "跨境运营", value: 83 },
{ label: "直播投放", value: 78 },
{ label: "AI内容生产", value: 75 },
],
actions: [
{ priority: "P0", title: "重构跨境电商平台运营课程", detail: "增加企业店铺运营、海外投放和合规履约任务。" },
{ priority: "P1", title: "增加AI营销内容生产工作流", detail: "纳入AI文案、图片、视频辅助生产与人工审核。" },
],
},
{
id: "school-2025",
type: "school",
typeLabel: "校级报告",
title: "2025年度全校产教供需分析总报告",
scope: "全校 32 个专业",
period: "2025年度",
periodKey: "previous",
pages: 40,
version: "V2025.12",
status: "已归档",
statusClass: "review",
updated: "2025-12-28",
author: "教务处 · 就业处",
roles: ["校级管理者"],
summary: "2025年度全校供需匹配度为74.4分,初步识别云平台、工业视觉和跨境运营等重点建设方向。",
metrics: [
{ label: "供需匹配度", value: "74.4", note: "年度提升3.2" },
{ label: "岗位样本", value: "62,840", note: "全年累计" },
{ label: "技能缺口", value: "28", note: "12项高优" },
{ label: "合作企业", value: "108", note: "新增23家" },
],
findings: [
{ title: "数字技术需求增长最快", detail: "软件、数据与云平台岗位全年增长22%。", value: "+22%" },
{ title: "专业之间响应速度差异明显", detail: "已有9个专业启动课程调整,5个专业尚未响应。", value: "9/14" },
],
bars: [
{ label: "信息技术类", value: 82 },
{ label: "智能制造类", value: 77 },
{ label: "数字商贸类", value: 70 },
{ label: "文化创意类", value: 68 },
],
actions: [
{ priority: "P0", title: "每季度更新一次分析", detail: "用季度数据快照和任务进展替代一年一次的人工报告。" },
{ priority: "P1", title: "完善岗位分类与技能词典", detail: "提高跨数据源岗位匹配和趋势比较的稳定性。" },
],
},
];
const professionReportCatalog = [
["物联网应用技术", "信息工程学院", 79.5, "边缘计算与物联网集成"],
["大数据技术", "信息工程学院", 78.9, "数据工程与实时计算"],
["人工智能技术应用", "信息工程学院", 77.8, "模型应用与数据标注"],
["计算机网络技术", "信息工程学院", 75.6, "云网络与安全运维"],
["工业机器人技术", "智能制造学院", 82.3, "机器人集成与机器视觉"],
["电气自动化技术", "智能制造学院", 80.8, "智能控制与设备联网"],
["机械制造及自动化", "智能制造学院", 76.4, "数字化制造与精益生产"],
["模具设计与制造", "智能制造学院", 68.4, "数字化设计与智能检测"],
["新能源汽车技术", "智能制造学院", 83.1, "三电检测与智能诊断"],
["汽车检测与维修技术", "智能制造学院", 74.9, "新能源汽车售后与诊断"],
["跨境电子商务", "商贸旅游学院", 81.2, "海外投放与合规履约"],
["国际商务", "商贸旅游学院", 73.6, "数字贸易与供应链协作"],
["市场营销", "商贸旅游学院", 72.8, "数据营销与用户增长"],
["现代物流管理", "商贸旅游学院", 78.1, "智慧物流与仓配优化"],
["酒店管理与数字化运营", "商贸旅游学院", 71.5, "收益管理与数字化服务"],
["旅游管理", "商贸旅游学院", 70.9, "文旅内容与目的地运营"],
["会展策划与管理", "商贸旅游学院", 72.1, "数字会展与活动运营"],
["建筑工程技术", "城市建设学院", 76.8, "BIM应用与智慧建造"],
["工程造价", "城市建设学院", 75.2, "全过程造价与数字算量"],
["市政工程技术", "城市建设学院", 73.7, "城市更新与工程数字化"],
["环境艺术设计", "艺术设计学院", 74.6, "空间数字表达与项目交付"],
["广告艺术设计", "艺术设计学院", 76.2, "品牌内容与AI视觉生产"],
["动漫设计", "艺术设计学院", 78.4, "三维内容与实时渲染"],
["数字媒体艺术设计", "艺术设计学院", 80.1, "交互设计与数字内容生产"],
["学前教育", "教育学院", 77.2, "幼儿发展评估与数字资源"],
["早期教育", "教育学院", 73.4, "家庭指导与托育服务"],
["婴幼儿托育服务与管理", "教育学院", 79.3, "托育运营与健康照护"],
["应用电子技术", "智能制造学院", 75.9, "嵌入式系统与智能硬件"],
["云计算技术应用", "信息工程学院", 85.1, "云平台运维与DevOps"],
];
professionReportCatalog.forEach(([name, college, score, focus], index) => {
const demand = 820 + ((index * 173) % 1780);
const coverage = Math.max(58, Math.min(88, Math.round(score - 5 + (index % 5))));
const gap = Math.max(8, Math.round(92 - coverage));
const companyCount = 6 + (index % 10);
reports.push({
id: `profession-auto-${index + 1}`,
type: "profession",
typeLabel: "专业报告",
title: `${name}专业人才需求分析报告`,
scope: `${college} · ${name}`,
period: "2026年3月",
periodKey: "current",
pages: 24 + (index % 6),
version: "V2026.03",
status: index % 9 === 0 ? "待审核" : "已发布",
statusClass: index % 9 === 0 ? "review" : "done",
updated: `2026-03-${String(18 - (index % 8)).padStart(2, "0")}`,
author: college,
roles: ["校级管理者"],
summary: `${name}专业供需匹配度${score}分。当前课程基础总体稳定,下一阶段应优先补充${focus}相关课程和企业项目。`,
metrics: [
{ label: "专业匹配度", value: String(score), note: score >= 80 ? "高匹配" : score >= 74 ? "保持关注" : "需要改进" },
{ label: "对口岗位", value: demand.toLocaleString("zh-CN"), note: "近90天有效样本" },
{ label: "技能覆盖", value: `${coverage}%`, note: `${gap}个百分点缺口` },
{ label: "潜力企业", value: String(companyCount), note: `${2 + (index % 4)}家高意向` },
],
findings: [
{ title: `${focus}需求持续增长`, detail: `重点企业正在将相关能力纳入岗位要求,近90天需求信号保持上升。`, value: `+${16 + (index % 23)}%` },
{ title: "课程实践与企业任务仍有差距", detail: "现有课程已覆盖基础知识,但企业场景、工具链和交付评价练习仍然不足。", value: `-${gap}` },
{ title: "可以联系更多合作企业", detail: `已有${companyCount}家企业与该专业较匹配,可讨论项目实训和企业导师安排。`, value: `${companyCount}家` },
],
bars: [
{ label: "专业基础能力", value: Math.min(95, coverage + 8) },
{ label: focus.slice(0, 8), value: Math.min(94, coverage + 14) },
{ label: "工程实践", value: Math.max(55, coverage - 4) },
{ label: "数据与AI应用", value: Math.max(52, coverage - 9) },
{ label: "企业项目交付", value: Math.max(50, coverage - 12) },
],
actions: [
{ priority: "P0", title: `强化${focus}教学模块`, detail: "引入企业任务,明确使用的工具、过程产物和验收标准。" },
{ priority: "P1", title: "邀请企业导师参与课程评审", detail: `优先对接已识别的${companyCount}家潜力企业,核验岗位技能与课程映射。` },
{ priority: "P1", title: "整理学生项目材料", detail: "把项目作品、企业评价和技能考核结果放入就业推荐材料。" },
],
});
});
[
["智能制造学院", "6个专业", "工业视觉、数字孪生与设备联网"],
["商贸旅游学院", "7个专业", "跨境运营、数字营销与智慧服务"],
["城市建设学院", "3个专业", "BIM应用、智慧建造与城市更新"],
["艺术设计与教育联合板块", "7个专业", "数字内容、AI创作与特色人才培养"],
].forEach(([name, scope, focus], index) => {
reports.push({
id: `college-auto-${index + 1}`,
type: "college",
typeLabel: "学院报告",
title: `${name}供需分析与专业建设报告`,
scope: `${name} · ${scope}`,
period: "2026年3月",
periodKey: "current",
pages: 32 + index,
version: "V2026.03",
status: index === 3 ? "待审核" : "已发布",
statusClass: index === 3 ? "review" : "done",
updated: `2026-03-${18 - index}`,
author: name,
roles: ["校级管理者"],
summary: `${name}整体供需表现稳定。多个专业都需要${focus}相关课程,可由学院统一开设部分内容。`,
metrics: [
{ label: "学院匹配度", value: String(76.8 + index * 1.3), note: "较上期持续提升" },
{ label: "覆盖专业", value: scope.replace("个专业", ""), note: "全部纳入分析" },
{ label: "技能缺口", value: String(8 + index * 2), note: "跨专业统筹响应" },
{ label: "合作企业", value: String(26 + index * 7), note: "持续扩展" },
],
findings: [
{ title: `多个专业都需要${focus}`, detail: "多个专业的技能缺口相近,适合共用学院课程或实训环境。", value: "高关联" },
{ title: "企业项目分布仍需优化", detail: "建议降低头部企业集中度,引入更多中型和专精特新企业。", value: "待改善" },
{ title: "建议可以开始执行", detail: "重点事项已有负责人和期限,可加入学院任务清单继续跟进。", value: "可执行" },
],
bars: [
{ label: "专业匹配", value: 80 + index },
{ label: "技能覆盖", value: 73 + index * 2 },
{ label: "企业合作", value: 76 + index },
{ label: "项目实践", value: 70 + index * 2 },
],
actions: [
{ priority: "P0", title: `建设${focus}共享模块`, detail: "列出学院共用内容,以及各专业需要单独设置的练习。" },
{ priority: "P1", title: "按合作类型整理企业名单", detail: "分别列出可参与课程共建、项目实训、实习就业和企业导师工作的企业。" },
{ priority: "P1", title: "按月检查专业改进任务", detail: "校级和学院共同跟进逾期、未响应或缺少材料的任务。" },
],
});
});
const reportDataProfiles = {
school: {
label: "校级综合分析",
topics: ["区域岗位趋势", "课程技能覆盖", "实习就业成效", "企业需求验证"],
dependencies: [
{ sourceId: "regional-jobs", requirement: "required", purpose: "岗位规模、技能需求与区域趋势" },
{ sourceId: "salary-company", requirement: "optional", purpose: "区域薪资基准与企业画像补充" },
{ sourceId: "internship", requirement: "required", purpose: "学生实习岗位与过程成效" },
{ sourceId: "employment", requirement: "required", purpose: "毕业去向、岗位与就业薪资" },
{ sourceId: "curriculum", requirement: "required", purpose: "专业课程与技能培养供给" },
{ sourceId: "enterprise-demand", requirement: "required", purpose: "合作企业正式确认的人才需求" },
],
},
college: {
label: "学院专业建设分析",
topics: ["专业结构对比", "共用课程分析", "培养结果检查", "企业合作机会"],
dependencies: [
{ sourceId: "regional-jobs", requirement: "required", purpose: "学院相关岗位与技能趋势" },
{ sourceId: "salary-company", requirement: "optional", purpose: "专业群薪资和企业结构参考" },
{ sourceId: "internship", requirement: "optional", purpose: "学院实习岗位与实践反馈" },
{ sourceId: "employment", requirement: "optional", purpose: "学院毕业去向与岗位匹配验证" },
{ sourceId: "curriculum", requirement: "required", purpose: "跨专业课程与共享技能供给" },
{ sourceId: "enterprise-demand", requirement: "required", purpose: "学院合作企业需求与项目机会" },
],
},
profession: {
label: "专业人才需求分析",
topics: ["岗位技能需求", "课程技能对应", "学生学习结果", "企业需求复核"],
dependencies: [
{ sourceId: "regional-jobs", requirement: "required", purpose: "对口岗位、技能频次与需求变化" },
{ sourceId: "salary-company", requirement: "optional", purpose: "区域同岗位薪资竞争力" },
{ sourceId: "internship", requirement: "optional", purpose: "学生实践岗位与企业评价" },
{ sourceId: "employment", requirement: "optional", purpose: "毕业去向和岗位匹配验证" },
{ sourceId: "curriculum", requirement: "required", purpose: "课程标准、学时与技能覆盖" },
{ sourceId: "enterprise-demand", requirement: "required", purpose: "企业确认的人才需求与合作意向" },
],
},
industry: {
label: "产业人才缺口分析",
topics: ["产业岗位趋势", "岗位技能归一", "培养供给对照", "企业确认校验"],
dependencies: [
{ sourceId: "regional-jobs", requirement: "required", purpose: "产业岗位规模、增速与技能信号" },
{ sourceId: "salary-company", requirement: "optional", purpose: "重点产业薪资与企业结构" },
{ sourceId: "curriculum", requirement: "required", purpose: "全校专业与课程技能供给" },
{ sourceId: "enterprise-demand", requirement: "required", purpose: "合作企业需求交叉验证" },
],
},
salary: {
label: "实习就业薪资分析",
topics: ["区域薪资基准", "实习薪资分布", "就业薪资对比", "专业竞争力"],
dependencies: [
{ sourceId: "regional-jobs", requirement: "optional", purpose: "区域岗位热度与样本辅助筛选" },
{ sourceId: "salary-company", requirement: "required", purpose: "区域同岗位薪资基准" },
{ sourceId: "internship", requirement: "required", purpose: "学生实习薪资与岗位信息" },
{ sourceId: "employment", requirement: "required", purpose: "毕业生就业薪资与去向" },
],
},
quality: {
label: "数据质量与口径分析",
topics: ["完整性与准确性", "版本与时效", "授权与权属", "加工血缘"],
dependencies: [
{ sourceId: "regional-jobs", requirement: "required", purpose: "岗位数据质量与归一覆盖" },
{ sourceId: "salary-company", requirement: "required", purpose: "薪资和企业画像质量" },
{ sourceId: "internship", requirement: "required", purpose: "实习数据完整性与时效" },
{ sourceId: "employment", requirement: "required", purpose: "就业数据完整性与异常记录" },
{ sourceId: "curriculum", requirement: "required", purpose: "课程数据版本与技能映射" },
{ sourceId: "enterprise-demand", requirement: "required", purpose: "企业身份、授权与需求质量" },
],
},
};
const dataGapCatalog = [
{
id: "job-normalization",
sourceId: "regional-jobs",
level: "attention",
levelLabel: "待复核",
title: "新增岗位缺少标准岗位代码与技能映射",
drawerTitle: "新增岗位名称待归一",
drawerLevel: "需要复核",
amount: "218 个岗位名称 · 影响 1,426 条记录",
affected: "218 个岗位别名 · 影响 1,426 条招聘记录",
fields: ["standard_position_code", "normalized_job_name", "skill_tags"],
affectedProfileKeys: ["school", "college", "profession", "industry", "quality"],
reason: "新增“具身智能应用工程师”等岗位未映射到当前岗位分类词典 V3.2。",
action: "优先审核需求量前 50 的岗位名称,确认标准岗位、技能标签及别名规则。",
status: "平台侧复核中",
due: "2026-03-24",
request: "请平台数据产品部确认标准岗位及技能标签,由平台数据运维组发布别名规则增量。",
},
{
id: "salary-missing",
sourceId: "employment",
level: "blocking",
levelLabel: "需补数",
title: "就业薪资与缺失原因未完整提供",
drawerTitle: "就业系统薪资字段缺失",
drawerLevel: "当前不能发布",
amount: "86 条记录 · 当前字段完整率 91.2%",
affected: "86 条记录 · 电子商务专业 41 条、现代服务类专业 45 条",
fields: ["employment_salary", "salary_period", "missing_reason"],
affectedReportIds: ["school-2026-03", "salary-2026-03", "quality-2026-03", "ecommerce-2026-03"],
reason: "employment_salary 为空,未达到必填完整率 95% 的发布阈值。",
action: "由就业处补齐薪资或填写缺失原因,修复文件按 student_id + cohort 回写后重新校验。",
status: "待就业处补充",
due: "2026-03-25",
request: "请就业处补齐薪资或登记缺失原因,由学校信息中心回写只读视图并重新执行质量校验。",
},
{
id: "enterprise-duplicate",
sourceId: "enterprise-demand",
level: "attention",
levelLabel: "待核验",
title: "企业主标识存在疑似重复",
drawerTitle: "疑似重复企业记录",
drawerLevel: "一般事项",
amount: "12 家企业 · 涉及 37 项确认需求",
affected: "12 家企业 · 涉及 37 项岗位需求",
fields: ["enterprise_credit_code", "master_enterprise_id"],
affectedProfileKeys: ["school", "college", "profession", "industry", "quality"],
reason: "企业简称不同,但统一社会信用代码或工商地址高度相似。",
action: "由产教融合中心确认主企业记录,合并需求与授权回执并保留审计映射。",
status: "待产教融合中心确认",
due: "2026-03-26",
request: "请产教融合中心确认企业主记录,由学校信息中心合并需求与授权回执并保留审计映射。",
},
];
const sourceAnalysisTopics = {
"regional-jobs": ["岗位需求趋势", "岗位技能归一", "产业增长信号"],
"salary-company": ["区域薪资基准", "企业规模画像", "岗位竞争力"],
internship: ["实习岗位分布", "实践过程成效", "企业评价"],
employment: ["毕业去向", "就业薪资", "岗位匹配成效"],
curriculum: ["课程技能覆盖", "专业培养供给", "课程版本"],
"enterprise-demand": ["企业需求确认", "校企合作机会", "授权有效性"],
};
const dataLineageState = {
mode: "report",
selectedReportId: "school-2026-03",
selectedSourceId: "employment",
gapsOnly: false,
};
const iconPaths = {
course: '',
lab: '',
practice: '',
database: '',
cloud: '',
file: '',
package: '',
school: '',
building: '',
};
function svgIcon(name, className = "") {
return ``;
}
function formatNumber(value) {
return Number(value).toLocaleString("zh-CN");
}
function showToast(message, title = "操作成功") {
const region = document.getElementById("toastRegion");
const toast = document.createElement("div");
toast.className = "toast";
toast.innerHTML = `
${title}${message}
`;
region.appendChild(toast);
setTimeout(() => {
toast.classList.add("out");
setTimeout(() => toast.remove(), 240);
}, 2600);
}
function openModal(modal) {
modal.classList.add("open");
modal.setAttribute("aria-hidden", "false");
document.body.style.overflow = "hidden";
setTimeout(() => {
const focusable = modal.querySelector("button, input, select, textarea");
focusable?.focus();
}, 100);
}
function closeModal(modal) {
modal.classList.remove("open");
modal.setAttribute("aria-hidden", "true");
if (!document.querySelector(".modal-backdrop.open")) {
document.body.style.overflow = "";
}
}
function switchView(viewName) {
const view = document.getElementById(`view-${viewName}`);
if (!view) return;
document.querySelectorAll(".view").forEach((item) => item.classList.remove("active"));
view.classList.add("active");
document.querySelectorAll(".nav-item[data-view]").forEach((item) => {
item.classList.toggle("active", item.dataset.view === viewName);
});
if (viewName !== "platform") {
document.querySelectorAll("[data-platform-target]").forEach((item) => item.classList.remove("active"));
}
document.getElementById("breadcrumbCurrent").textContent = view.dataset.title;
if (viewName === "reports") renderReportCenter();
if (viewName === "loop") renderClosedLoop();
document.body.classList.remove("sidebar-open");
window.scrollTo({ top: 0, behavior: "smooth" });
}
function applyRole(role, options = {}) {
const { notify = true } = options;
const profile = roleProfiles[role] || roleProfiles.校级管理者;
const isPlatformRole = Boolean(profile.platform);
const isDataAdministrator = Boolean(profile.dataAdministrator);
document.getElementById("roleName").textContent = role;
document.getElementById("roleUserName").textContent = profile.user;
document.getElementById("roleAvatar").textContent = profile.avatar;
document.getElementById("organizationName").textContent = profile.organization;
document.getElementById("organizationLabel").textContent = profile.organizationLabel;
document.getElementById("organizationEmblem").textContent = profile.emblem;
document.getElementById("breadcrumbRoot").textContent = profile.root;
document.getElementById("topbarContext").textContent = profile.context;
document.getElementById("workspaceNavLabel").textContent = profile.workspaceLabel;
document.getElementById("dataNavText").textContent = isDataAdministrator ? "数据运维中心" : "数据资产";
document.getElementById("dataPageTitle").textContent = isDataAdministrator ? "学校数据运维中心" : "数据资产与质量";
document.getElementById("dataPageDescription").textContent = isDataAdministrator
? "管理学校数据源、初次导入、增量更新、字段契约、质量问题、版本与审计。"
: "区分厂商基础数据包、学校自有数据与企业授权数据,每项分析结论都注明数据来源。";
document.getElementById("syncAllButtonText").textContent = isDataAdministrator ? "新建更新任务" : "同步全部数据";
document.getElementById("schoolNav").hidden = isPlatformRole;
document.getElementById("platformNav").hidden = !isPlatformRole;
document.getElementById("sidebarFooter").hidden = isPlatformRole || !profile.menus.includes("report");
document.querySelectorAll("[data-menu-key]").forEach((item) => {
item.hidden = !profile.menus.includes(item.dataset.menuKey);
});
const hasDeliveryMenu = profile.menus.includes("report") || profile.menus.includes("screen");
document.getElementById("deliveryNavLabel").hidden = !hasDeliveryMenu;
reportState.type = "all";
reportState.period = "all";
reportState.search = "";
reportState.limit = 6;
const collaborationHeading = document.getElementById("collaborationHeading");
const collaborationDescription = document.getElementById("collaborationDescription");
const demandButton = document.getElementById("enterpriseDemandButton");
const projectButton = document.getElementById("newProjectButton");
const collaborationStats = document.querySelector(".collab-stats");
const renderCollaborationStats = (items) => {
collaborationStats.innerHTML = items
.map(
(item) =>
`${item.label}${item.value}${item.unit}${item.note}
`,
)
.join("");
};
if (role === "企业导师") {
collaborationHeading.textContent = "企业合作工作台";
collaborationDescription.textContent = "提交岗位与技能需求,参与课程共建并评价学生项目成果。";
demandButton.innerHTML = `${svgIcon("file")}提交企业需求`;
projectButton.innerHTML = `${svgIcon("course")}发起共建项目`;
renderCollaborationStats([
{ label: "已发布人才需求", value: "6", unit: "项", note: "2 项等待学校响应" },
{ label: "共建项目", value: "3", unit: "个", note: "全部按计划推进" },
{ label: "企业导师", value: "12", unit: "人", note: "本月授课 28 学时" },
{ label: "参与学生", value: "86", unit: "人", note: "企业评价 4.7 / 5" },
]);
} else if (role === "学院负责人" || role === "专业负责人") {
collaborationHeading.textContent = "学院校企合作";
collaborationDescription.textContent = "围绕本院专业对接企业需求,推进课程共建、实训与企业导师协作。";
demandButton.innerHTML = `${svgIcon("file")}邀请企业调研`;
projectButton.innerHTML = `${svgIcon("course")}新建合作项目`;
renderCollaborationStats([
{ label: "本院合作企业", value: "38", unit: "家", note: "本学期新增 6 家" },
{ label: "本院合作项目", value: "8", unit: "个", note: "3 个进行中" },
{ label: "企业导师", value: "21", unit: "人", note: "覆盖 6 个专业" },
{ label: "学生参与", value: "386", unit: "人次", note: "满意度 94%" },
]);
} else {
collaborationHeading.textContent = "校企合作";
collaborationDescription.textContent = "根据企业需求安排课程共建、合作项目和人才培养计划。";
demandButton.innerHTML = `${svgIcon("file")}发起需求调研`;
projectButton.innerHTML = `${svgIcon("course")}新建合作项目`;
renderCollaborationStats([
{ label: "在库合作企业", value: "126", unit: "家", note: "本学期新增 18 家" },
{ label: "本学期合作项目", value: "24", unit: "个", note: "8 个进行中" },
{ label: "企业导师", value: "68", unit: "人", note: "覆盖 17 个专业" },
{ label: "学生参与", value: "1,284", unit: "人次", note: "满意度 92%" },
]);
}
renderDataReportLineage();
renderClosedLoop();
if (document.getElementById("professionSelect")) {
renderProfessionProfile(document.getElementById("professionSelect").value);
}
switchView(profile.defaultView);
if (isPlatformRole) {
document.querySelectorAll("#platformNav [data-platform-target]").forEach((item) => item.classList.remove("active"));
const firstPlatformItem = document.querySelector("#platformNav [data-platform-target]");
firstPlatformItem?.classList.add("active");
document.getElementById("breadcrumbCurrent").textContent = "平台总览";
}
if (notify) showToast(`工作台与数据权限已切换为“${role}”`, "角色已切换");
}
function renderSparkline(elementId, points, color = "#238765") {
const element = document.getElementById(elementId);
if (!element) return;
const width = 78;
const height = 28;
const max = Math.max(...points);
const min = Math.min(...points);
const range = max - min || 1;
const coords = points.map((point, index) => {
const x = (index / (points.length - 1)) * width;
const y = height - ((point - min) / range) * (height - 5) - 2;
return [x, y];
});
const line = coords.map(([x, y]) => `${x},${y}`).join(" ");
const area = `M ${coords[0][0]} ${height} L ${coords.map(([x, y]) => `${x} ${y}`).join(" L ")} L ${coords.at(-1)[0]} ${height} Z`;
element.innerHTML = `
`;
}
function chartCoordinates(values, width, height, padding) {
const min = 60;
const max = 84;
return values.map((value, index) => {
const x = padding + (index / (values.length - 1)) * (width - padding * 2);
const y = height - padding - ((value - min) / (max - min)) * (height - padding * 2);
return [x, y];
});
}
function renderTrendChart(period = "term") {
const data = trendSets[period];
const svg = document.getElementById("lineChart");
const width = 620;
const height = 220;
const padding = 18;
const schoolPoints = chartCoordinates(data.school, width, height, padding);
const benchmarkPoints = chartCoordinates(data.benchmark, width, height, padding);
const schoolLine = schoolPoints.map(([x, y]) => `${x},${y}`).join(" ");
const benchmarkLine = benchmarkPoints.map(([x, y]) => `${x},${y}`).join(" ");
const area = `M ${schoolPoints[0][0]} ${height - padding} L ${schoolPoints
.map(([x, y]) => `${x} ${y}`)
.join(" L ")} L ${schoolPoints.at(-1)[0]} ${height - padding} Z`;
svg.innerHTML = `
${[35, 80, 125, 170].map((y) => ``).join("")}
${schoolPoints.map(([x, y], index) => ``).join("")}
${data.score}
`;
document.getElementById("chartLabels").innerHTML = data.labels.map((label) => `${label}`).join("");
document.getElementById("dialScore").textContent = data.score.toFixed(1);
document.getElementById("metricMatch").textContent = data.score.toFixed(1);
document.getElementById("dialValue").style.strokeDashoffset = `${389.6 * (1 - data.score / 100)}`;
}
function renderProfessionTable(filter = "") {
const table = document.getElementById("professionTable");
const keyword = filter.trim().toLowerCase();
const filtered = professions.filter(
(item) => item.name.toLowerCase().includes(keyword) || item.college.toLowerCase().includes(keyword),
);
table.innerHTML = filtered
.map((item) => {
const level = item.score >= 80 ? "high" : item.score >= 72 ? "mid" : "low";
const gapLevel = item.gaps <= 3 ? "low" : item.gaps <= 5 ? "mid" : "high";
const gapText = item.gaps <= 3 ? "轻微" : item.gaps <= 5 ? "关注" : "预警";
return `
| ${item.icon}${item.name} |
${item.college} |
${item.score} |
${formatNumber(item.demand)} 个 |
${gapText} · ${item.gaps}项 |
${item.trend >= 0 ? "↑" : "↓"} ${Math.abs(item.trend)}% |
|
`;
})
.join("");
table.querySelectorAll(".profession-row-action").forEach((button) => {
button.addEventListener("click", () => {
const key = professionData[button.dataset.profession] ? button.dataset.profession : "software";
document.getElementById("professionSelect").value = key;
renderProfessionProfile(key);
switchView("profession");
});
});
}
function renderIndustryList() {
document.getElementById("industryList").innerHTML = industries
.map(
(item, index) => `
${String(index + 1).padStart(2, "0")}
${item.name}
+${item.growth}%
`,
)
.join("");
}
function skillGapClass(gap) {
if (gap > 30) return "high";
if (gap > 12) return "mid";
return "low";
}
function renderProfessionProfile(key) {
const data = professionData[key] || professionData.software;
document.getElementById("professionTitle").textContent = data.name;
document.getElementById("professionCollege").textContent = data.college;
document.getElementById("graduateCount").textContent = data.graduates;
document.getElementById("profileScore").textContent = data.score.toFixed(1);
document.getElementById("profileLevel").textContent = data.level;
document.getElementById("profileDemand").textContent = data.demand;
document.getElementById("profileSalary").textContent = data.salary;
document.getElementById("profileCoverage").textContent = data.coverage;
document.getElementById("profileCompanies").textContent = data.companyCount;
document.getElementById("skillChart").innerHTML = data.skills
.map((skill) => {
const gap = skill.market - skill.course;
const state = gap > 30 ? "明显缺口" : gap > 12 ? "部分覆盖" : "覆盖良好";
return `
${skill.name}${skill.category}
${gap > 0 ? `-${gap}` : `+${Math.abs(gap)}`}${state}
`;
})
.join("");
document.getElementById("jobDemandList").innerHTML = data.jobs
.map(
(job) => `
${job.name}${job.count}个 +${job.growth}%
`,
)
.join("");
document.getElementById("courseRecommendations").innerHTML = data.recommendations
.map(
(recommendation, index) => `
${recommendation.priority}
${recommendation.title}
${key === "software" && index === 0 ? `${closedLoopState.entities.recommendation.id} · ${closedLoopState.entities.recommendation.status} · 快照 ${closedLoopState.entities.snapshot.id}` : ""}
${recommendation.description}
数据依据:${recommendation.evidence}
`,
)
.join("");
document.querySelectorAll(".recommendation-actions button").forEach((button) => {
button.addEventListener("click", () => {
const card = button.closest(".recommendation-card");
const action = button.dataset.action;
if (key === "software" && Number(card.dataset.recommendation) === 0) {
if (currentRoleName() !== "专业负责人") {
showToast("仅专业负责人可保存专业建议决策,当前角色保持只读。", "权限不足");
return;
}
openClosedLoopAction("decision");
const decisionSelect = document.querySelector('#closedLoopFields select[name="decision"]');
if (decisionSelect) decisionSelect.value = action === "accept" ? "采纳" : action === "hold" ? "暂缓" : "驳回";
return;
}
if (action === "accept") {
const recommendation = data.recommendations[Number(card.dataset.recommendation)];
const existingTask = tasks.some((task) => task.title === recommendation.title);
if (!existingTask) {
const task = {
id: `TSK-REC-${Date.now()}`,
title: recommendation.title,
source: `专业建议 · ${data.name} · 已关联数据证据`,
profession: data.name,
owner: "待确认",
due: "2026-06-30",
progress: 0,
status: "review",
statusText: "待确认",
};
closedLoopState.custom.tasks.unshift(task);
addClosedLoopAudit("RECOMMENDATION_ACCEPTED_TASK_CREATED", task.id, `${data.name} · ${recommendation.title}`);
saveClosedLoopState();
syncClosedLoopCollections();
renderTasks();
}
card.classList.add("accepted");
button.textContent = "已采纳";
button.disabled = true;
showToast(existingTask ? "该建议已在改进任务中,可前往任务中心查看" : "建议已加入改进任务,并关联专业分析证据");
} else if (action === "hold") {
showToast("建议已标记为暂缓,下期分析时将再次提醒", "状态已更新");
} else {
card.style.opacity = "0.52";
showToast("建议已记录为不采纳,可在历史记录中恢复", "状态已更新");
}
});
});
document.getElementById("companyShortlist").innerHTML = data.companies
.map(
(company) => `
${company.short}
${company.name}${company.focus}
${company.match}%
`,
)
.join("");
if (professionTabState !== "skills") renderProfessionDetailView(professionTabState);
}
let professionTabState = "skills";
function professionEvidenceNote(code, text) {
return ``;
}
function renderProfessionJobsView(data) {
const totalJobs = data.jobs.reduce((sum, item) => sum + item.count, 0);
const regions = [
{ name: "杭州主城区", value: 46, count: Math.round(totalJobs * 0.46) },
{ name: "杭州区县与都市圈", value: 24, count: Math.round(totalJobs * 0.24) },
{ name: "长三角其他城市", value: 19, count: Math.round(totalJobs * 0.19) },
{ name: "其他地区", value: 11, count: Math.round(totalJobs * 0.11) },
];
return `
${data.name}${data.graduates}2026届毕业生
${data.jobs.map((job, index) => ``).join("")}
${data.jobs
.map(
(job, index) =>
`${String(index + 1).padStart(2, "0")}${job.name}${job.count}个 · 增长 ${job.growth}%`,
)
.join("")}
${regions.map((region) => `${region.name}${region.count}个 · ${region.value}%`).join("")}
龙头企业34% · 976个岗位
专精特新与成长型企业42% · 1,203个岗位
其他有效企业24% · 685个岗位
${professionEvidenceNote("证据 PRO-JOB-301", `岗位流向使用区域招聘基础包中的去重岗位、学校近三届就业去向及合作企业确认记录计算。互联网岗位反映需求信号,不等于实际录用人数。`)}
`;
}
function renderProfessionSalaryView(data) {
const salaryByKey = {
软件技术: { school: 8640, region: 8010, peer: 8230, p25: 7200, p75: 10800, samples: 628 },
机电一体化技术: { school: 7920, region: 7540, peer: 7680, p25: 6800, p75: 9600, samples: 512 },
电子商务: { school: 6980, region: 7280, peer: 7160, p25: 5800, p75: 8500, samples: 706 },
};
const salary = salaryByKey[data.name];
const max = 12000;
const comparisons = [
["本专业就业中位数", salary.school, "school"],
["区域同岗位中位数", salary.region, "region"],
["同类院校专业中位数", salary.peer, "peer"],
];
return `
${comparisons
.map(
([label, value, tone]) =>
`${label}¥ ${value.toLocaleString("zh-CN")}`,
)
.join("")}
P25¥ ${salary.p25.toLocaleString("zh-CN")}
P75¥ ${salary.p75.toLocaleString("zh-CN")}
${salary.samples}条完整就业薪资记录缺失、异常区间和无法匹配岗位的记录已剔除或降权。
+14%企业项目经历
+9%高质量作品集
+7%企业导师评价
${professionEvidenceNote("证据 PRO-SAL-302", `薪资对标使用${salary.samples}条完整校内就业回执与区域同岗位样本,按中位数比较并控制地区、岗位层级和企业规模差异。`)}
`;
}
function renderProfessionCoursesView(data) {
const courseNames = {
软件技术: ["Java企业级开发", "Web前端工程实践", "云原生综合实训", "数据库应用", "软件工程项目"],
机电一体化技术: ["PLC控制技术", "工业机器人应用", "机器视觉实训", "设备数字孪生", "电气设计"],
电子商务: ["电商运营实务", "内容营销", "跨境平台运营", "直播投放", "数据化经营"],
};
const courses = courseNames[data.name];
return `
| 课程 / 技能 | ${data.skills.map((skill) => `${skill.name} | `).join("")}调整建议 |
${courses
.map((course, row) => {
const cells = data.skills
.map((skill, column) => {
const score = Math.max(18, Math.min(96, skill.course + ((row * 17 + column * 11) % 35) - 18));
const level = score >= 72 ? "full" : score >= 45 ? "partial" : "gap";
return `${score} | `;
})
.join("");
return `| ${course}${row < 2 ? "专业核心课" : row === 2 ? "综合实训" : "专业方向课"} | ${cells} |
`;
})
.join("")}
${data.coverage}课程技能覆盖率课程标准、实训任务和考核要求均参与映射。
${data.skills
.map((skill) => ({ ...skill, gap: skill.market - skill.course }))
.sort((a, b) => b.gap - a.gap)
.slice(0, 3)
.map((skill, index) => `${String(index + 1).padStart(2, "0")}${skill.name}需求 ${skill.market} · 覆盖 ${skill.course} · 缺口 ${skill.gap}`)
.join("")}
${professionEvidenceNote("证据 PRO-CRS-303", `矩阵由课程标准、教学任务、实训产物和考核要求映射至版本化技能词典。文字出现不等于学生达成,仍需结合项目作品和企业评价。`)}
`;
}
function renderProfessionDetailView(tab) {
const key = document.getElementById("professionSelect").value;
const data = professionData[key] || professionData.software;
const renderers = {
jobs: renderProfessionJobsView,
salary: renderProfessionSalaryView,
courses: renderProfessionCoursesView,
};
document.getElementById("professionDetailView").innerHTML = renderers[tab](data);
}
function renderProjects(filter = "all") {
const toneClass = { green: "", amber: "amber", blue: "blue" };
const filteredProjects =
filter === "all"
? projects
: projects.filter((project) => (filter === "practice" ? project.type === "practice" || project.type === "lab" : project.type === filter));
document.getElementById("projectList").innerHTML = filteredProjects
.map(
(project) => `
${svgIcon(project.type)}
${project.title}${project.id ? `${project.id} · ${project.status || "进行中"} · ` : ""}${project.meta}
${project.partners.map((partner) => `${partner}`).join("")}
项目进度 ${project.progress}%
`,
)
.join("");
if (!filteredProjects.length) {
document.getElementById("projectList").innerHTML = `当前分类暂无项目可以新建项目或切换其他分类。
`;
}
}
function renderDemands() {
document.getElementById("enterpriseDemandList").innerHTML = enterpriseDemands
.map(
(demand) => `
${demand.logo}${demand.company}${demand.industry}
${demand.status || "待响应"}
${demand.title}
${demand.id ? `
${demand.id}` : ""}
${demand.description}
${demand.due}
`,
)
.join("");
document.querySelectorAll("[data-demand]").forEach((button) => {
button.addEventListener("click", () => {
openWorkspaceDetail("demand", button.dataset.demandId || button.dataset.demand);
});
});
}
function renderPartners(filter = "all") {
const categoryByPartner = {
吉利汽车: "智能制造",
大华股份: "智能制造",
数梦工场: "数字技术",
遥望科技: "现代服务",
};
const filteredPartners = filter === "all" ? partners : partners.filter((partner) => categoryByPartner[partner.name] === filter);
document.getElementById("partnerGrid").innerHTML = filteredPartners
.map(
(partner) => `
${partner.logo}
${partner.name}${partner.industry}
${partner.reason}
${partner.tags.map((tag) => `${tag}`).join("")}
`,
)
.join("");
document.querySelectorAll("[data-connect]").forEach((button) => {
button.addEventListener("click", () => {
openWorkspaceDetail("partner", button.dataset.connect);
});
});
}
function renderTasks(filter = "all", search = "") {
const keyword = search.trim().toLowerCase();
const filtered = tasks.filter((task) => {
const matchesFilter =
filter === "all" ||
task.status === filter ||
(filter === "progress" && task.status === "risk");
const matchesSearch = `${task.title}${task.profession}${task.owner}`.toLowerCase().includes(keyword);
return matchesFilter && matchesSearch;
});
document.getElementById("taskTable").innerHTML = filtered
.map(
(task) => `
| ${task.title}${task.id ? `${task.id} · ` : ""}${task.source} |
${task.profession} |
${task.owner.slice(0, 1)}${task.owner} |
${task.due} |
${task.progress}% |
${task.statusText} |
|
`,
)
.join("");
if (!filtered.length) {
document.getElementById("taskTable").innerHTML = `| 没有找到符合条件的任务 |
`;
}
}
function reportDataProfileKey(report) {
return report.dataProfile || report.type;
}
function reportDataProfile(report) {
return reportDataProfiles[reportDataProfileKey(report)] || reportDataProfiles.profession;
}
function sourceById(sourceId) {
return sources.find((source) => source.id === sourceId);
}
function gapsForSource(sourceId) {
return dataGapCatalog.filter((gap) => gap.sourceId === sourceId);
}
function dataGapById(gapId) {
return dataGapCatalog.find((gap) => gap.id === gapId);
}
function dependencyForReport(report, sourceId) {
return reportDataProfile(report).dependencies.find((dependency) => dependency.sourceId === sourceId);
}
function reportsUsingSource(sourceId) {
return reports.filter((report) => Boolean(dependencyForReport(report, sourceId)));
}
function gapAffectsReport(gap, report) {
if (!dependencyForReport(report, gap.sourceId)) return false;
if (gap.affectedReportIds) return gap.affectedReportIds.includes(report.id);
if (report.periodKey !== "current") return false;
return gap.affectedProfileKeys?.includes(reportDataProfileKey(report)) ?? false;
}
function reportsAffectedByGap(gap) {
return reports.filter((report) => gapAffectsReport(gap, report));
}
function reportReadiness(report) {
const requiredDependencies = reportDataProfile(report).dependencies.filter(
(dependency) => dependency.requirement === "required",
);
if (!requiredDependencies.length) return 100;
return (
requiredDependencies.reduce(
(total, dependency) => total + sourceById(dependency.sourceId).analysisCompleteness,
0,
) /
requiredDependencies.length
);
}
function reportGaps(report) {
return dataGapCatalog.filter((gap) => gapAffectsReport(gap, report));
}
function reportsGroupedByType(reportItems) {
return reportItems.reduce((groups, report) => {
const group = groups.get(report.type) || {
type: report.type,
label: report.typeLabel,
reports: [],
};
group.reports.push(report);
groups.set(report.type, group);
return groups;
}, new Map());
}
function readableReportIdSet() {
return new Set(reportsForCurrentRole().map((report) => report.id));
}
function lineageEmptyNode(message) {
return `${svgIcon("database")}${message}关闭“只看缺口”可查看完整依赖。
`;
}
function renderLineageSourceNode(source, dependency, selected = false, relevantGaps = gapsForSource(source.id)) {
const gaps = relevantGaps.filter((gap) => gap.sourceId === source.id);
const hasGap = gaps.length > 0;
const requirement = dependency?.requirement || "required";
const completeness = source.analysisCompleteness;
return `
`;
}
function renderLineageTopicNode(topic, index) {
return `
${String(index + 1).padStart(2, "0")}
${topic}
按同一统计口径计算 · 可查看数据来源
`;
}
function renderLineageReportNode(report, readableIds = readableReportIdSet()) {
const gaps = reportGaps(report);
const readiness = reportReadiness(report);
const canRead = readableIds.has(report.id);
return `
`;
}
function renderLineageReportGroup(group, readableIds) {
const affectedCount = group.reports.filter((report) => reportGaps(report).length).length;
return `
${group.label}${affectedCount ? `${affectedCount} 份存在数据缺口` : "当前数据关系可用"}
${group.reports.length} 份
${group.reports.map((report) => renderLineageReportNode(report, readableIds)).join("")}
`;
}
function renderLineageStage(title, countText, className, content) {
return `
${title}${countText}
${content}
`;
}
function lineageConnector(label) {
return `
${label}
`;
}
function renderReportLineageCanvas(report) {
const profile = reportDataProfile(report);
const gaps = reportGaps(report);
const readableIds = readableReportIdSet();
const gapSourceIds = new Set(gaps.map((gap) => gap.sourceId));
const visibleDependencies = dataLineageState.gapsOnly
? profile.dependencies.filter((dependency) => gapSourceIds.has(dependency.sourceId))
: profile.dependencies;
const sourceNodes =
visibleDependencies
.map((dependency) => renderLineageSourceNode(sourceById(dependency.sourceId), dependency, false, gaps))
.join("") || lineageEmptyNode("当前报告没有数据缺口");
const topicNodes = profile.topics.map(renderLineageTopicNode).join("");
return `
${renderLineageStage("数据资产", `${visibleDependencies.length} / ${profile.dependencies.length} 项`, "sources", sourceNodes)}
${lineageConnector("计算指标")}
${renderLineageStage("指标 / 分析主题", `${profile.topics.length} 类`, "topics", topicNodes)}
${lineageConnector("生成报告")}
${renderLineageStage("目标报告", "1 份", "reports", renderLineageReportNode(report, readableIds))}
`;
}
function renderSourceLineageCanvas(source) {
const sourceReports = reportsUsingSource(source.id);
const affectedReportIds = new Set(
gapsForSource(source.id).flatMap((gap) => reportsAffectedByGap(gap).map((report) => report.id)),
);
const visibleReports = dataLineageState.gapsOnly
? sourceReports.filter((report) => affectedReportIds.has(report.id))
: sourceReports;
const reportGroups = [...reportsGroupedByType(visibleReports).values()];
const readableIds = readableReportIdSet();
const reportNodes =
reportGroups
.map((group) => renderLineageReportGroup(group, readableIds))
.join("") || lineageEmptyNode("当前数据来源没有待处理的报告关系");
const topics = sourceAnalysisTopics[source.id] || ["基础指标", "专题分析", "质量校验"];
return `
${renderLineageStage("选中数据", "1 项", "sources", renderLineageSourceNode(source, null, true))}
${lineageConnector("用于分析")}
${renderLineageStage("可做的分析", `${topics.length} 类`, "topics", topics.map(renderLineageTopicNode).join(""))}
${lineageConnector("用于报告")}
${renderLineageStage("使用该数据的报告", `${visibleReports.length} 份`, "reports", reportNodes)}
`;
}
function lineageSummaryCard(label, value, note, className = "") {
return `${label}${value}${note}`;
}
function renderLineageSummary(report, source) {
const summary = document.getElementById("dataLineageSummary");
if (dataLineageState.mode === "report") {
const profile = reportDataProfile(report);
const requiredCount = profile.dependencies.filter((dependency) => dependency.requirement === "required").length;
const gaps = reportGaps(report);
summary.innerHTML = [
lineageSummaryCard("当前报告", report.typeLabel, report.version),
lineageSummaryCard("依赖数据", `${requiredCount} 必需 · ${profile.dependencies.length - requiredCount} 可选`, "依赖关系已登记"),
lineageSummaryCard("数据完备度", `${reportReadiness(report).toFixed(1)}%`, gaps.length ? "使用稳定版本,可继续分析" : "达到生成条件"),
lineageSummaryCard("待补数据", `${gaps.length} 项`, gaps.length ? "已定位责任人与时限" : "当前无数据缺口", gaps.length ? "warning" : "ready"),
].join("");
return;
}
const sourceReports = reportsUsingSource(source.id);
const gaps = gapsForSource(source.id);
summary.innerHTML = [
lineageSummaryCard("当前数据", source.categoryLabel, source.version),
lineageSummaryCard("使用该数据的报告", `${sourceReports.length} 份`, `${reportsGroupedByType(sourceReports).size} 类报告`),
lineageSummaryCard("当前质量", source.quality, `${source.records} 条有效记录`),
lineageSummaryCard("数据缺口", `${gaps.length} 项`, gaps.length ? "已定位责任人与时限" : "当前无数据缺口", gaps.length ? "warning" : "ready"),
].join("");
}
function renderLineageGapCard(gap, selectedReport) {
const source = sourceById(gap.sourceId);
const impactCount = selectedReport ? 1 : reportsAffectedByGap(gap).length;
return `
${gap.levelLabel}
${gap.status}
${gap.title}
${source.name} · ${gap.amount}
${gap.fields.map((field) => `${field}`).join("")}
${gap.request}
- 业务索取人
- ${source.stewardContact}
- 技术接口人
- ${source.technicalContact}
- 要求完成
- ${gap.due}
- 影响范围
- ${selectedReport ? "当前报告" : `${impactCount} 份报告`}
`;
}
function renderLineageGaps(report, source) {
const gaps = dataLineageState.mode === "report" ? reportGaps(report) : gapsForSource(source.id);
document.getElementById("dataLineageGapCount").textContent = `${gaps.length} 项待处理`;
document.getElementById("dataLineageGapList").innerHTML = gaps.length
? gaps.map((gap) => renderLineageGapCard(gap, dataLineageState.mode === "report" ? report : null)).join("")
: `
${svgIcon("database")}
当前关系没有待补数据数据版本、质量和授权均达到报告生成要求。
`;
}
function renderDataReportLineage() {
const selector = document.getElementById("dataLineageSelector");
if (!selector) return;
const mode = dataLineageState.mode;
const readableReportIds = new Set(reportsForCurrentRole().map((report) => report.id));
if (!reports.some((report) => report.id === dataLineageState.selectedReportId)) {
dataLineageState.selectedReportId = "school-2026-03";
}
if (!sources.some((source) => source.id === dataLineageState.selectedSourceId)) {
dataLineageState.selectedSourceId = sources[0].id;
}
const report = reports.find((item) => item.id === dataLineageState.selectedReportId) || reports[0];
const source = sourceById(dataLineageState.selectedSourceId) || sources[0];
document.querySelectorAll("[data-lineage-mode]").forEach((button) => {
const selected = button.dataset.lineageMode === mode;
button.classList.toggle("active", selected);
button.setAttribute("aria-pressed", String(selected));
});
document.getElementById("dataLineageSelectorLabel").textContent = mode === "report" ? "选择报告" : "选择数据";
selector.setAttribute("aria-label", mode === "report" ? "选择报告查看所需数据" : "选择数据查看使用它的报告");
selector.innerHTML =
mode === "report"
? reports
.map((item) => {
const canRead = readableReportIds.has(item.id);
return ``;
})
.join("")
: sources
.map((item) => ``)
.join("");
selector.value = mode === "report" ? report.id : source.id;
document.getElementById("dataLineageGapToggle").checked = dataLineageState.gapsOnly;
document.getElementById("dataLineageCanvas").innerHTML =
mode === "report" ? renderReportLineageCanvas(report) : renderSourceLineageCanvas(source);
renderLineageSummary(report, source);
renderLineageGaps(report, source);
}
function setupDataReportLineage() {
const lineage = document.getElementById("dataReportLineage");
lineage.addEventListener("click", (event) => {
const modeButton = event.target.closest("[data-lineage-mode]");
if (modeButton) {
dataLineageState.mode = modeButton.dataset.lineageMode;
renderDataReportLineage();
return;
}
const reportButton = event.target.closest("[data-lineage-report]");
if (reportButton) {
const reportId = reportButton.dataset.lineageReport;
if (!reportsForCurrentRole().some((report) => report.id === reportId)) {
showToast("依赖元数据可见,但当前角色没有该报告正文的阅读权限。", "权限范围提示");
return;
}
openReportReader(reportId);
return;
}
const gapButton = event.target.closest("[data-lineage-gap-action]");
if (gapButton) openDataOpsDrawer("issue", gapButton.dataset.lineageGapAction);
});
document.getElementById("dataLineageSelector").addEventListener("change", (event) => {
if (dataLineageState.mode === "report") dataLineageState.selectedReportId = event.target.value;
else dataLineageState.selectedSourceId = event.target.value;
renderDataReportLineage();
});
document.getElementById("dataLineageGapToggle").addEventListener("change", (event) => {
dataLineageState.gapsOnly = event.target.checked;
renderDataReportLineage();
});
}
const dataOpsState = {
context: "source",
sourceName: sources[0].name,
tab: "overview",
issue: null,
};
function currentDataSource() {
return sources.find((source) => source.name === dataOpsState.sourceName) || sources[0];
}
function dataOpsStat(label, value, note = "") {
return `${label}${value}${note ? `${note}` : ""}`;
}
function renderDataOpsOverview(source) {
return `
${dataOpsStat("当前版本", source.version, source.updateMode)}
${dataOpsStat("业务截止日期", source.dataAsOf, `入库于 ${source.updated}`)}
${dataOpsStat("质量评分", source.quality, `发布阈值 ${source.threshold.split(";")[0]}`)}
${dataOpsStat("下次更新", source.nextUpdate, source.frequency)}
本次更新内容
${source.changeSummary}
更新数据先放入暂存区检查;不满足最低质量要求的数据进入隔离区,不会覆盖当前已发布版本。
更新与责任
- 数据类别
- ${source.categoryLabel} · ${source.commercial}
- 业务责任人
- ${source.stewardContact} · ${source.steward}
- 技术责任人
- ${source.technicalContact} · ${source.technicalOwner}
- 更新模式
- ${source.updateMode}
- 当前状态
- ${source.status}
质量与可用性
${source.quality}
- 主键、必填字段和代码合法性已执行自动校验
- 授权缺失或已过期记录不得进入分析快照
- 失败任务保留错误明细、操作者和追踪编号
`;
}
function renderDataOpsRequirements(source) {
const fields = source.requiredFields.split("、");
return `
数据契约${source.schema}${source.name}
支持格式${source.format}日期使用 ISO-8601 · 字符编码 UTF-8
主键要求${source.primaryKey}主键唯一率必须达到 100%
初次导入与增量更新要求
01准备文件或连接支持标准模板导入,也可按更新频率配置 API、数据库视图或 SFTP。
02字段映射与样本预检确认主键、必填字段、代码字典和敏感字段处理规则。
03暂存与质量校验先写入暂存区,发现严重错误时不更新生产版本。
04审批发布与留痕数据管理员确认质量和授权后,生成不可变版本并进入分析。
必填字段
${fields.map((field) => `${field}`).join("")}
验收门槛
| 校验项 | 门槛 | 失败处理 |
| 主键唯一性 | 100% | 整批不发布,可下载冲突明细 |
| 必填完整性 | ≥ 95% | 进入隔离区,修复后重试 |
| 代码与日期合法性 | ≥ 98% | 错误记录不进入分析 |
| 综合质量 | ${source.threshold} | 低于阈值不得发布 |
`;
}
function renderDataOpsHistory(source) {
return `
${source.warning ? "最近一次增量任务需要处理" : "最近一次更新任务已完成"}
${source.warning ? "QUALITY_CHECK · 78%" : "PUBLISHED · 100%"}
${source.version}${source.updated} · ${source.updateMode}${source.changeSummary}
追踪编号 TRACE-${source.category.toUpperCase()}-20260322-01
上一稳定版本2026-03-17 · 已发布当前分析快照仍引用该稳定版本,可随时回滚。
保留 180 天 · 完整性校验通过
初次导入2026-02-28 · FULL完成字段映射、主键确认、授权登记与全量质量基线。
审批人:学校数据管理员 / 平台数据产品负责人
失败处理策略
网络失败从检查点自动重试 3 次;数据错误进入隔离区;出现主键冲突、授权缺失或 Schema 不兼容时,整批数据不发布。历史已发布版本不直接覆盖。
`;
}
function renderDataOpsAudit(source) {
const authorization =
source.category === "vendor"
? "按学校租户、产品版本、用途和服务期限授权"
: source.category === "enterprise"
? "绑定企业授权编号、合作用途、专业范围和有效期"
: "数据权属归学校,仅限本租户获授权角色使用";
return `
权属与授权${source.rights}${authorization}
业务责任人${source.stewardContact} · ${source.steward}技术责任人${source.technicalContact} · ${source.technicalOwner}授权状态${source.status}
最近审计事件
版本校验完成${source.updated} · 系统质量引擎 · TRACE-8F2A
授权范围复核2026-03-18 16:22 · 数据管理员 · 审批通过
字段映射版本更新2026-03-17 14:08 · ${source.technicalOwner} · ${source.schema}
访问与留存策略
敏感字段按角色脱敏,导入、导出、报告下载和权限变更均保留操作者、时间戳、前后版本哈希及追踪编号。授权撤回后立即停止后续分析使用。
`;
}
function renderDataOpsUsage() {
const e = closedLoopState.entities;
return `
接收回执${e.receipt.id} · ${e.receipt.status}
${e.snapshot.packageId} / ${e.snapshot.packageVersion} 已接收状态:${e.snapshot.status}。共享业务标识仅用于静态原型对照,不表示两页面实时同步。
SHA校验${safeText(e.receipt.receivedSha256 || e.receipt.expectedSha256)}交付包记录数${e.receipt.receivedRecords.toLocaleString("zh-CN")} / ${e.receipt.expectedRecords.toLocaleString("zh-CN")}分析快照${e.snapshot.id} · ${e.snapshot.checkedRecords.toLocaleString("zh-CN")}条
业务使用情况
报告${e.report?.id || "待生成"}
项目${e.project?.id || "待创建"}
任务${e.task?.id || "待创建"}
业务部门确认与下一周期反馈
${e.usageFeedback
.map((item) => `${safeText(item.department)} · ${safeText(item.usable)} · ${safeText(item.id)}${safeText(item.operator)} · ${safeText(item.at)}${safeText(item.comment)};下一周期:${safeText(item.nextCycle)}`)
.join("") || `尚无业务使用反馈就业老师可在报告发布后登记。`}
${currentRoleName() === "就业老师" ? `` : ""}
${currentRoleName() === "校级技术管理员" && e.snapshot.status !== "已验收" ? `` : ""}
`;
}
function renderDataIssue(issueId) {
const issue = dataGapById(issueId);
const source = currentDataSource();
const impactCount = reportsAffectedByGap(issue).length;
return `
${issue.drawerLevel}${issue.drawerTitle}
${issue.affected}
规则命中原因
${issue.reason}
建议处置
${issue.action}
受影响分析与补数责任
当前生产版本未覆盖影响报告${impactCount} 份分析快照沿用稳定版本
- 业务索取人
- ${source.stewardContact} · ${source.steward}
- 技术接口人
- ${source.technicalContact} · ${source.technicalOwner}
`;
}
function renderAddDataSource() {
return `
1选择方式2字段映射3质量规则4审批发布
新建学校数据源
初次导入交付清单
- 标准模板或只读连接信息
- 字段说明、主键、代码字典和样例数据
- 业务截止日期、覆盖范围和责任人
- 敏感字段清单、授权范围和留存期限
`;
}
function renderAuthorizationOverview() {
return `
${dataOpsStat("厂商基础包", "2 项授权", "服务期内可用")}
${dataOpsStat("学校自有数据", "3 个数据源", "权属归学校")}
${dataOpsStat("企业授权", "126 家", "12 项 30 日内到期")}
授权使用矩阵
| 数据类别 | 允许用途 | 可见角色 | 期限与退出 |
| 厂商基础数据包 | 产业趋势、岗位与薪资分析 | 校级管理者、学院负责人 | 随订阅服务期,到期停止新分析 |
| 学校自有数据 | 专业诊断、课程和就业分析 | 按校内组织与最小权限 | 学校自主留存与删除 |
| 企业授权数据 | 人才需求确认、课程与项目共建 | 约定学院、专业和项目成员 | 撤权即时生效,到期自动停用 |
安全控制
租户隔离已启用敏感字段脱敏18 项规则导出审计完整留痕授权到期提醒提前 30 天
`;
}
function updateDataOpsDrawer() {
const source = currentDataSource();
const content = document.getElementById("dataOpsContent");
const tabs = document.getElementById("dataOpsTabs");
const footer = document.getElementById("dataOpsFooter");
tabs.hidden = dataOpsState.context !== "source";
if (dataOpsState.context === "batch") {
document.getElementById("dataOpsKicker").textContent = "DATA UPDATE CENTER";
document.getElementById("dataOpsTitle").textContent = "数据更新任务中心";
document.getElementById("dataOpsDescription").textContent = "先确认业务截止日期、变更内容和最低质量要求,再创建带编号的更新任务。";
content.innerHTML = `
${dataOpsStat("待更新", "2 个来源", "1个自动调度 · 1个人工导入")}${dataOpsStat("运行正常", "4 个来源", "最近24小时无失败")}${dataOpsStat("隔离记录", "86 条", "未进入生产版本")}
数据源更新计划
${sources
.map(
(item) => ``,
)
.join("")}
执行策略
`;
footer.innerHTML = ``;
return;
}
if (dataOpsState.context === "add") {
document.getElementById("dataOpsKicker").textContent = "DATA SOURCE ONBOARDING";
document.getElementById("dataOpsTitle").textContent = "新增数据源";
document.getElementById("dataOpsDescription").textContent = "支持标准模板直接导入,并可按业务需要配置自动增量连接。";
content.innerHTML = renderAddDataSource();
footer.innerHTML = ``;
return;
}
if (dataOpsState.context === "authorization") {
document.getElementById("dataOpsKicker").textContent = "DATA AUTHORIZATION";
document.getElementById("dataOpsTitle").textContent = "数据权属与授权中心";
document.getElementById("dataOpsDescription").textContent = "设置数据用途、可见范围、服务期限、撤权方式和操作记录要求。";
content.innerHTML = renderAuthorizationOverview();
footer.innerHTML = ``;
return;
}
if (dataOpsState.context === "issue") {
const issue = dataGapById(dataOpsState.issue);
const issueSource = sourceById(issue.sourceId);
if (issueSource) dataOpsState.sourceName = issueSource.name;
document.getElementById("dataOpsKicker").textContent = "DATA QUALITY WORKBENCH";
document.getElementById("dataOpsTitle").textContent = issue.drawerTitle;
document.getElementById("dataOpsDescription").textContent = "查看受影响数据、规则命中原因和可审计的处理流程。";
content.innerHTML = renderDataIssue(dataOpsState.issue);
footer.innerHTML = ``;
return;
}
document.getElementById("dataOpsKicker").textContent = `${source.categoryLabel.toUpperCase()} · ${source.version}`;
document.getElementById("dataOpsTitle").textContent = source.name;
document.getElementById("dataOpsDescription").textContent = `${source.provider} · ${source.coverage} · ${source.frequency}`;
const renderers = {
overview: renderDataOpsOverview,
requirements: renderDataOpsRequirements,
history: renderDataOpsHistory,
audit: renderDataOpsAudit,
usage: renderDataOpsUsage,
};
content.innerHTML = renderers[dataOpsState.tab](source);
tabs.querySelectorAll("button").forEach((button) => {
button.classList.toggle("active", button.dataset.dataOpsTab === dataOpsState.tab);
});
if (dataOpsState.tab === "usage") {
footer.innerHTML = ``;
return;
}
const primaryLabel =
source.category === "vendor" ? "检查并验收新版本" : source.warning ? "进入异常处置" : "创建更新任务";
footer.innerHTML = ``;
}
function openDataOpsDrawer(context, value) {
dataOpsState.context = context;
dataOpsState.tab = "overview";
if (context === "source") dataOpsState.sourceName = value;
if (context === "issue") dataOpsState.issue = value;
updateDataOpsDrawer();
const drawer = document.getElementById("dataOpsDrawer");
drawer.classList.add("open");
drawer.setAttribute("aria-hidden", "false");
document.body.style.overflow = "hidden";
}
function closeDataOpsDrawer() {
const drawer = document.getElementById("dataOpsDrawer");
drawer.classList.remove("open");
drawer.setAttribute("aria-hidden", "true");
document.body.style.overflow = "";
}
function workspaceMetric(label, value, note) {
return `${label}${value}${note}`;
}
function openWorkspaceDetail(kind, value = "") {
const configurations = {
project: () => {
const project = projects.find((item) => item.id === value || item.title === value) || projects[0];
const loopProject = project._closedLoop ? closedLoopState.entities.project : null;
return {
kicker: "COLLABORATION PROJECT",
title: project.title,
description: project.meta,
content: `
${workspaceMetric("项目进度", `${project.progress}%`, "按计划推进")}${workspaceMetric("参与单位", "3 方", "学校、学院、企业")}${workspaceMetric("学生参与", "42 人", "跨专业组队")}
里程碑与交付物
企业任务书与验收标准确认已完成 · 2026-02-28课程任务和学生分组已完成 · 2026-03-08企业中期评审进行中 · 2026-04-12成果验收与能力评价计划 · 2026-05-30
${loopProject ? `实体关联
需求${loopProject.demandId}建议${loopProject.recommendationId}任务${loopProject.taskId}
执行证据
${closedLoopState.entities.evidences.map((item) => `${safeText(item.name)} · ${safeText(item.type)}${safeText(item.submitter)} · ${safeText(item.submittedAt)} · ${safeText(item.fileRef)}`).join("") || "尚未提交证据提交后同步更新项目进度"}
` : `验收证据
企业任务书已归档学生过程产物86 份企业导师评价待中期评审
`}`,
primary: loopProject ? "关闭详情" : "更新项目进度",
};
},
task: () => {
const task = tasks.find((item) => item.id === value || item.title === value) || tasks[0];
const loopTask = task._closedLoop ? closedLoopState.entities.task : null;
return {
kicker: "IMPROVEMENT TASK",
title: task.title,
description: `${task.profession} · 来源:${task.source}`,
content: `
${workspaceMetric("负责人", task.owner, "主责")}${workspaceMetric("截止日期", task.due, task.statusText)}${workspaceMetric("当前进度", `${task.progress}%`, "自动汇总")}
任务目标
根据分析建议调整课程、项目或企业合作安排,并用正式方案、企业确认和学生考核结果验收。
进展记录
${loopTask ? loopTask.progressLogs.map((item) => `${safeText(item.detail)}${safeText(item.operator)} · ${safeText(item.at)} · 进度 ${item.progress}%`).join("") : `完成现状与差距复核李雯 · 2026-03-18 16:20企业导师已确认技能清单陈凯 · 2026-03-20 10:42课程调整方案进入教研室评审系统 · 今天 08:30`}
验收要求
课程或项目方案${loopTask && closedLoopState.entities.evidences.length ? "已提交" : "必需"}企业确认回执${closedLoopState.entities.evaluation ? closedLoopState.entities.evaluation.result : "必需"}校级验收${loopTask?.status || "待执行"}
${loopTask ? `` : ""}`,
primary: loopTask ? "关闭详情" : "提交阶段进展",
};
},
demand: () => {
const demand = enterpriseDemands.find((item) => item.id === value || item.company === value) || enterpriseDemands[0];
const loopDemand = demand._closedLoop ? closedLoopState.entities.demand : null;
return {
kicker: "ENTERPRISE DEMAND",
title: demand.title,
description: `${demand.company} · ${demand.industry} · ${demand.due}`,
content: `
需求说明
${demand.description}
${workspaceMetric("计划人数", `${loopDemand?.headcount || 24} 人`, "2026届")}${workspaceMetric("技能要求", loopDemand?.skills || "8 项", loopDemand ? loopDemand.authorizationNo : "3 项重点")}${workspaceMetric("有效期", loopDemand?.validUntil || "实训 + 实习", loopDemand?.submitter || "企业导师参与")}
学校响应方案
`,
primary: "提交响应方案",
};
},
partner: () => {
const partner = partners.find((item) => item.name === value) || partners[0];
return {
kicker: "PARTNER ENGAGEMENT",
title: `发起与${partner.name}的合作对接`,
description: `${partner.industry} · 综合匹配度 ${partner.match}%`,
content: `
推荐依据
${partner.reason}
${partner.tags.map((tag) => `${tag}`).join("")}
对接安排
`,
primary: "创建对接事项",
};
},
notifications: () => ({
kicker: "NOTIFICATION CENTER",
title: "待办与通知",
description: "聚合需要当前角色处理的业务事项。",
content: ``,
primary: "全部标记为已读",
}),
organizations: () => ({
kicker: "ORGANIZATION SCOPE",
title: "组织与数据范围",
description: "根据账号角色切换组织,数据权限同步变化。",
content: `权限说明
切换组织只改变可查看的业务范围,不改变租户隔离。平台运营人员默认不能查看学校明细数据。
`,
primary: "确认当前范围",
}),
templates: () => ({
kicker: "REPORT TEMPLATE LIBRARY",
title: "报告模板库",
description: "按报告用途设置章节、数据说明和审批步骤。",
content: `校级报告全校产教供需分析总报告学校数据、区域基准、专业结构、任务清单42页 · 已启用学院报告学院专业建设分析报告专业对比、共用课程、企业合作与任务跟踪36页 · 已启用专业报告专业人才需求分析报告岗位、技能、薪资、课程对应和企业推荐24—30页 · 已启用数据质量数据资产质量与口径报告来源、权属、版本、质量规则、限制和审计19页 · 已启用 `,
primary: "新建模板版本",
}),
compare: () => ({
kicker: "PROGRAM COMPARISON",
title: "专业对比篮",
description: "最多选择4个专业,比较供需、技能、薪资与改进优先级。",
content: `| 专业 | 匹配度 | 岗位需求 | 薪资竞争力 | 技能覆盖 | 建议 |
|---|
| ${document.getElementById("professionTitle").textContent} | 84.2 | 2,864 | 1.08 | 76% | 保持优势 |
| 机电一体化技术 | 81.7 | 2,146 | 1.05 | 72% | 补强工业视觉 |
| 电子商务 | 74.8 | 1,932 | 0.96 | 68% | 优先调整 |
对比结论
软件技术需求和薪资竞争力领先;机电专业需补强机器视觉;电子商务应优先响应跨境运营和数据分析能力缺口。
`,
primary: "生成专业对比报告",
}),
industry: () => ({
kicker: "INDUSTRY TREND",
title: "重点产业需求趋势",
description: "查看近90天岗位增速、样本和关联专业。",
content: `${industries.map((item, index) => `${String(index + 1).padStart(2, "0")}${item.name}+${item.growth}%${820 + index * 173}条样本`).join("")}
口径说明
岗位增速来自近90天去重招聘信号,并由合作企业需求确认校验;只用于识别趋势,不直接解释为实际录用人数。
`,
primary: "生成产业专题报告",
}),
course: () => ({
kicker: "COURSE STANDARD",
title: "课程标准与技能映射",
description: "查看课程目标、项目任务、考核证据和调整记录。",
content: `映射依据
课程技能覆盖同时读取课程标准、实训任务、考核要求和学生项目产物,不因正文中出现技能名称就判定为充分覆盖。
${workspaceMetric("课程目标", "6 项", "已结构化")}${workspaceMetric("实训任务", "4 项", "2项企业任务")}${workspaceMetric("技能标签", "12 项", "版本 V3.2")}
最近调整
新增企业项目验收要求教研室 · 2026-03-18更新云平台技能映射课程负责人 · 2026-03-12
`,
primary: "进入课程标准修订",
}),
};
const factory = configurations[kind] || configurations.task;
const config = factory();
document.getElementById("workspaceDetailKicker").textContent = config.kicker;
document.getElementById("workspaceDetailTitle").textContent = config.title;
document.getElementById("workspaceDetailDescription").textContent = config.description;
document.getElementById("workspaceDetailContent").innerHTML = config.content;
document.getElementById("workspaceDetailFooter").innerHTML = ``;
const drawer = document.getElementById("workspaceDetailDrawer");
drawer.classList.add("open");
drawer.setAttribute("aria-hidden", "false");
document.body.style.overflow = "hidden";
}
function closeWorkspaceDetail() {
const drawer = document.getElementById("workspaceDetailDrawer");
drawer.classList.remove("open");
drawer.setAttribute("aria-hidden", "true");
document.body.style.overflow = "";
}
function renderSources(filter = "all") {
const filteredSources = filter === "all" ? sources : sources.filter((source) => source.category === filter);
const iconByCategory = {
vendor: "package",
school: "school",
enterprise: "building",
};
const commercialClass = {
vendor: "paid",
school: "owned",
enterprise: "authorized",
};
document.getElementById("sourceGrid").innerHTML = filteredSources
.map(
(source) => `
${source.categoryLabel}
${source.commercial}
${svgIcon(iconByCategory[source.category])}
${source.name}${source.type}
${source.status}
数据提供方${source.provider}
权属与授权${source.rights}
覆盖范围${source.coverage}
更新频率${source.frequency}
有效记录${source.records}
质量评分${source.quality}
`,
)
.join("");
document.getElementById("sourceFilterResult").textContent = `当前展示 ${filteredSources.length} 个数据源`;
document.querySelectorAll("[data-sync-source]").forEach((button) => {
button.addEventListener("click", () => {
const source = sources.find((item) => item.name === button.dataset.syncSource);
dataOpsState.sourceName = source.name;
dataOpsState.tab = source.warning ? "overview" : "history";
openDataOpsDrawer("source", source.name);
dataOpsState.tab = source.warning ? "overview" : "history";
updateDataOpsDrawer();
});
});
}
const reportState = {
type: "all",
period: "all",
search: "",
limit: 6,
};
function reportsForCurrentRole() {
const role = document.getElementById("roleSelect")?.value || "校级管理者";
return reports.filter((report) => {
if (report.roles.includes(role)) return true;
if (role === "专业负责人") return report.roles.includes("学院负责人") && report.type === "profession";
if (role === "就业老师") return report.roles.includes("校级管理者") && ["school", "quality"].includes(report.type);
return false;
});
}
function reportIcon(type) {
const icons = {
school: "school",
college: "building",
profession: "file",
industry: "cloud",
quality: "database",
};
return svgIcon(icons[type] || "file");
}
function visibleReports() {
const keyword = reportState.search.trim().toLowerCase();
return reportsForCurrentRole().filter((report) => {
const matchesType = reportState.type === "all" || report.type === reportState.type;
const matchesPeriod = reportState.period === "all" || report.periodKey === reportState.period;
const searchable = `${report.title}${report.scope}${report.author}`.toLowerCase();
return matchesType && matchesPeriod && searchable.includes(keyword);
});
}
function renderFeaturedReports(roleReports) {
const featuredReports = roleReports.filter((report) => report.featured).slice(0, 2);
const fallbackReports = roleReports.slice(0, 2);
const selected = featuredReports.length >= 2 ? featuredReports : fallbackReports;
document.getElementById("featuredReports").innerHTML = selected
.map(
(report, index) => `
${index === 0 ? "本期重点报告" : "推荐阅读"}${report.status}
${report.title}
${report.summary}
${report.scope}${report.pages}页${report.updated}
${report.typeLabel.toUpperCase()}
${report.title}
${report.bars
.slice(0, 5)
.map((bar, barIndex) => ``)
.join("")}
${report.version} · ${report.period}
`,
)
.join("");
}
function renderReportList() {
const filtered = visibleReports();
const visible = filtered.slice(0, reportState.limit);
const list = document.getElementById("reportList");
list.innerHTML = visible
.map(
(report) => `
`,
)
.join("");
if (!visible.length) {
list.innerHTML = `
${svgIcon("file")}
没有找到符合条件的报告
可以调整报告类型、周期或搜索关键词。
`;
}
document.getElementById("reportListSummary").textContent = `共 ${filtered.length} 份报告,当前显示 ${visible.length} 份`;
const loadMore = document.getElementById("loadMoreReports");
loadMore.hidden = visible.length >= filtered.length;
}
function renderReportCenter() {
const roleReports = reportsForCurrentRole();
const role = document.getElementById("roleSelect")?.value || "校级管理者";
const descriptions = {
校级管理者: "查看和管理校级、学院、专业、产业与数据质量报告,并保留各版本。",
学院负责人: "查看本院、所属专业及相关数据质量报告,并根据分析结论安排专业建设任务。",
专业负责人: "查看本专业报告、提交业务流程报告审核并追踪审批历史;校级批准与发布由校级管理者执行。",
就业老师: "查看校级与数据质量报告,登记就业侧使用反馈;不能审批校级业务报告。",
校级技术管理员: "查看数据质量、版本、口径和授权报告,并检查每个分析版本的数据来源和处理记录。",
};
document.getElementById("reportCenterDescription").textContent =
descriptions[role] || "查看和管理分析报告,并保留各版本。";
document.getElementById("reportTotalCount").textContent = roleReports.length;
document.getElementById("reportNewCount").textContent = roleReports.filter((item) => item.periodKey === "current").length;
document.getElementById("reportReviewCount").textContent = roleReports.filter((item) => item.status === "待审核").length;
document.getElementById("reportSearch").value = reportState.search;
document.getElementById("reportPeriodFilter").value = reportState.period;
document.querySelectorAll("#reportTypeFilters [data-report-filter]").forEach((button) => {
button.classList.toggle("active", button.dataset.reportFilter === reportState.type);
});
renderFeaturedReports(roleReports);
renderReportList();
}
function reportSectionHeading(index, english, title) {
return `
${String(index).padStart(2, "0")}
${english}
${title}
`;
}
function renderReportDocument(report, options = {}) {
const interactive = options.interactive !== false;
const evidence = window.ReportVisuals.buildEvidence(report);
const findingEvidenceCodes = [
evidence.evidenceCodes.trend,
evidence.evidenceCodes.matrix,
evidence.evidenceCodes.quadrant,
];
return `
产教融智 · INDUSTRY EDUCATION
${report.typeLabel} · ${report.period}
${report.title}
${report.scope}。报告使用同一数据快照生成,每项结论都注明来源、样本量和统计口径。
STANDARD REPORT · 可查看数据来源、统计口径和结论依据
${reportSectionHeading(1, "EXECUTIVE SUMMARY", "执行摘要")}
${report.summary}
${report.metrics
.map(
(metric) => `
${metric.label}${metric.value}${metric.note}
`,
)
.join("")}
本校 / 本专业${evidence.score.toFixed(1)}
区域院校均值${evidence.benchmark.regional.toFixed(1)}${(evidence.score - evidence.benchmark.regional).toFixed(1)}分
同类院校均值${evidence.benchmark.peer.toFixed(1)}${(evidence.score - evidence.benchmark.peer).toFixed(1)}分
区域可比排名${evidence.benchmark.rank}/${evidence.benchmark.schoolCount}样本口径一致
${window.ReportVisuals.renderExecutiveCharts(report, { interactive })}
综合解读
本期${evidence.scoreLabel}为${evidence.score.toFixed(1)}分,高于区域可比均值${(evidence.score - evidence.benchmark.regional).toFixed(1)}分,高于同类院校均值${(evidence.score - evidence.benchmark.peer).toFixed(1)}分。计算使用${evidence.benchmark.sampleText}和同一数据快照;低样本指标已降低权重,互联网招聘数量不直接当作人才缺口人数。
${reportSectionHeading(2, "INSTITUTION PROFILE", report.type === "profession" ? "专业与学校现状" : report.type === "college" ? "学院与学校现状" : "学校现状")}
${window.ReportVisuals.renderProfileEvidence(report, { interactive })}
学校现状判断
学校现状数据来自学籍、教务、人事、实习就业和校企合作台账。当前全校专业、课程和企业合作规模能够满足常规人才培养需要,但企业项目实训、企业导师评价和课程技能对应关系仍不完整。每项指标都可查看来源系统、更新时间、责任部门和计算口径。
${reportSectionHeading(3, "KEY FINDINGS", "关键发现与证据定位")}
${report.findings
.map(
(finding, index) => `
${String(index + 1).padStart(2, "0")}
${finding.title}
${finding.detail}
证据定位:${findingEvidenceCodes[index % findingEvidenceCodes.length]} · ${evidence.sources[index % evidence.sources.length].code} · 有效样本 ${evidence.sources[index % evidence.sources.length].sample}
${finding.value}
`,
)
.join("")}
结论交叉验证规则
外部岗位变化+校内培养数据+企业确认需求→业务负责人复核
${reportSectionHeading(4, "SUPPLY & DEMAND", "供需结构与技能缺口分析")}
${window.ReportVisuals.renderSupplyVisuals(report, { interactive })}
表4-1 核心维度可比数据
| 分析维度 | 本校/本专业 | 区域均值 | 同类均值 | 较区域 | 需求强度 | 课程覆盖 | 证据状态 |
${evidence.dimensions
.map(
(item) => `
| ${item.name} |
${item.school.toFixed(1)} |
${item.regional.toFixed(1)} |
${item.peer.toFixed(1)} |
${item.school >= item.regional ? "+" : ""}${(item.school - item.regional).toFixed(1)} |
${item.demand.toFixed(0)} |
${item.coverage.toFixed(0)} |
已交叉验证 |
`,
)
.join("")}
图4-1至图4-4使用同一分析快照。需求强度、课程覆盖、学生达成和企业确认均为0—100标准分,用于同口径横向比较,不代表绝对岗位人数或学生人数。三维图用于同时观察四类证据,并非独立的3D业务流程。
结构性结论
高需求但课程覆盖不足的能力位于象限图左上区域,应优先加入专业建设清单;高需求且覆盖充分的能力需要检查学生掌握情况和企业评价。趋势图显示课程调整仍慢于岗位需求变化,建议增加企业项目和企业验收,不要只增加课程名称。
${reportSectionHeading(5, "DATA EVIDENCE", "样本与数据来源")}
${window.ReportVisuals.renderEvidenceLedger(report)}
${reportSectionHeading(6, "METHODOLOGY", "指标口径、分析方法与限制")}
01数据采集采集近90天岗位、校内主数据、课程标准、实习就业回执和合作企业确认记录,保留权属与授权标签。
02清洗归一按岗位ID、企业、发布时间去重;专业依据教育部专业代码归一;技能使用版本化词典映射。
03选择对比样本只纳入同层次、同区域且有效样本达到100条的院校;同类院校按专业结构、规模和产业方向选择。
04相互核对外部岗位、校内数据和企业确认中至少两类结果一致,才列为较可靠结论;AI草拟文字必须人工审核。
| 指标 | 计算口径 | 样本与置信规则 | 使用限制 |
| 供需匹配度 | 岗位方向40% + 技能覆盖35% + 薪资竞争力15% + 企业确认10% | 四类分项均有样本;缺项按规则降权,不直接补均值 | 用于识别建设方向,不用于直接决定招生规模 |
| 技能缺口 | 需求强度-课程覆盖-学生达成修正值 | 岗位样本≥100且课程映射覆盖≥80%为中高置信 | 标准分差值不能解释为缺口人数 |
| 岗位需求 | 近90天有效招聘记录去重并按企业、时效和重复发布降权 | 77,520条岗位记录;126家企业确认用于校验 | 招聘信号不等于实际录用人数 |
| 课程技能覆盖 | 课程标准、实训任务和考核要求与技能词典映射后的加权覆盖 | 1,286门次课程;映射覆盖率94.8% | 有文本描述不等于学生实际达成 |
| 学生达成 | 课程考核、项目产物、企业评价和证书证据的综合达成度 | 缺少企业评价的项目仅计校内证据,不做强外推 | 不同专业考核难度不可直接比较绝对分数 |
| 薪资竞争力 | 校内就业薪资中位数 / 区域同岗位中位数 | 完整薪资样本6,284条;异常和缺失记录已剔除或降权 | 受地区、岗位层级和企业规模影响 |
本报告用于讨论专业建设和校企合作。AI只整理数据和草拟文字,不修改原始数据,也不删除相反结果。业务负责人仍需结合政策、办学定位和资源情况审核;报告不能替代招生、专业撤并或课程调整的正式决策程序。
${reportSectionHeading(7, "ACTION PLAN", "行动建议与验收证据")}
${report.actions
.map(
(action, index) => `
${action.priority}
${action.title}
${action.detail}
建议依据:${findingEvidenceCodes[index % findingEvidenceCodes.length]} · 验收要求:课程/项目方案、企业确认回执、学生达成记录至少两项
`,
)
.join("")}
责任确认5个工作日内
方案与资源提交可按验收要求检查的成果
阶段复核按月更新证据
效果评估下一周期回看指标
`;
}
function renderReportApprovalToolbar(report) {
const toolbar = document.getElementById("reportApprovalToolbar");
const loopReport = closedLoopState.entities.report;
if (!loopReport || report.id !== loopReport.id) {
toolbar.innerHTML = `${safeText(report.status)}历史报告按原有发布状态展示。`;
return;
}
const official = loopReport.status === "已发布";
const actions = [];
if (["草稿", "已退回"].includes(loopReport.status) && ["专业负责人", "学院负责人", "校级管理者"].includes(currentRoleName())) {
actions.push(``);
}
if (loopReport.status === "待审核" && currentRoleName() === "校级管理者") {
actions.push(``);
}
if (loopReport.status === "已批准" && currentRoleName() === "校级管理者") {
actions.push(``);
}
toolbar.innerHTML = `
${loopReport.status} · ${official ? "正式版" : "非正式版"}${loopReport.id} · 发布前打印、下载和导出均标识非正式版
关联 ${loopReport.snapshotId} / ${loopReport.demandId} / ${loopReport.taskId} / ${loopReport.evidenceIds.length}项证据
审批历史 ${loopReport.history.length} 条
${loopReport.history
.map((item) => `${safeText(item.status)}${safeText(item.operator)} · ${safeText(item.at)}${safeText(item.comment)}`)
.join("")}
${actions.join("")}
`;
}
function openReportReader(reportId) {
const report = reports.find((item) => item.id === reportId);
if (!report) return;
const reader = document.getElementById("reportReader");
const tocItems = [
["cover", "封面"],
["summary", "执行摘要"],
["profile", "学校现状"],
["findings", "关键发现"],
["analysis", "供需结构"],
["evidence", "数据来源"],
["method", "指标口径"],
["actions", "行动建议"],
];
window.ReportVisuals.dispose(document.getElementById("reportDocument"));
reader.dataset.reportId = report.id;
document.getElementById("reportReaderTitle").textContent = report.title;
document.getElementById("reportReaderVersion").textContent = report.version;
renderReportApprovalToolbar(report);
document.getElementById("reportDocument").innerHTML = renderReportDocument(report);
document.getElementById("reportToc").innerHTML = tocItems
.map(
([key, label], index) =>
``,
)
.join("");
reader.classList.add("open");
reader.setAttribute("aria-hidden", "false");
document.body.style.overflow = "hidden";
document.querySelector(".report-document-wrap").scrollTop = 0;
requestAnimationFrame(() => window.ReportVisuals.init(report, document.getElementById("reportDocument")));
}
function closeReportReader() {
const reader = document.getElementById("reportReader");
window.ReportVisuals.dispose(document.getElementById("reportDocument"));
reader.classList.remove("open");
reader.setAttribute("aria-hidden", "true");
document.body.style.overflow = "";
}
function reportStandaloneStyles() {
return `
:root{--green:#176b4f;--green2:#2f8f6b;--lime:#c6e66f;--ink:#27362f;--muted:#718078;--line:#dfe7e1;--soft:#f4f7f4}
*{box-sizing:border-box}body{margin:0;background:#eef1ed;color:var(--ink);font-family:"PingFang SC","Microsoft YaHei",sans-serif;font-size:14px;line-height:1.7}
.report-document{max-width:1040px;margin:30px auto;background:white;box-shadow:0 16px 50px #20362b1a}
.report-cover-page{min-height:570px;padding:68px 70px 56px;color:white;background:radial-gradient(circle at 90% 100%,#244f3e 0 18%,transparent 19%),linear-gradient(155deg,#184638,#0f2c24);display:flex;flex-direction:column;justify-content:space-between}
.report-cover-brand{display:flex;align-items:center;gap:12px;color:var(--lime);font-weight:700;letter-spacing:.1em}.report-cover-brand i{display:inline-block;width:34px;height:4px;background:var(--lime)}
.report-cover-main span{color:#9bb4a9;font-size:14px}.report-cover-main h1{max-width:700px;margin:18px 0 0;font-family:"Songti SC","STSong",serif;font-size:42px;line-height:1.35}.report-cover-main p{max-width:620px;color:#aac0b7;font-size:16px}
.report-standard-stamp{display:inline-flex;margin-top:16px;padding:7px 10px;color:#dce8b9;background:#c6e66f14;border:1px solid #c6e66f40;border-radius:7px;font-size:13px;font-weight:700}
.report-cover-foot{display:grid;grid-template-columns:1fr 1fr;gap:16px;padding-top:24px;border-top:1px solid #ffffff22}.report-cover-foot span,.report-cover-foot strong{display:block}.report-cover-foot span{color:#829f93}.report-cover-foot strong{margin-top:6px;color:white}
.report-section{padding:50px 60px;border-bottom:1px solid #e5eae6}.report-section-heading{display:flex;gap:16px;margin-bottom:24px}.report-section-heading>span{display:grid;width:36px;height:36px;flex:0 0 auto;place-items:center;color:var(--green);background:#e8f4ee;border-radius:10px;font-weight:800}.report-section-heading small{color:var(--green2);font-size:11px;font-weight:700;letter-spacing:.1em}.report-section-heading h2{margin:2px 0 0;font-size:24px}
.report-summary-callout{padding:20px;background:#eff7f2;border-left:4px solid var(--green);border-radius:0 12px 12px 0;font-size:15px}
.report-metric-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:10px;margin-top:20px}.report-metric-grid div{padding:16px;background:#f8faf8;border:1px solid #e5eae6;border-radius:11px}.report-metric-grid span,.report-metric-grid strong,.report-metric-grid small{display:block}.report-metric-grid span{color:var(--muted);font-size:11px}.report-metric-grid strong{margin:8px 0 5px;font-size:23px}.report-metric-grid small{color:var(--green2);font-size:11px}
.report-benchmark-strip{display:grid;grid-template-columns:repeat(4,1fr);margin-top:12px;overflow:hidden;color:white;background:#153d31;border-radius:12px}.report-benchmark-strip>span{padding:15px;border-right:1px solid #ffffff1a}.report-benchmark-strip small,.report-benchmark-strip strong,.report-benchmark-strip em{display:block}.report-benchmark-strip small{color:#a9beb5;font-size:11px}.report-benchmark-strip strong{margin:7px 0 4px;font-size:22px}.report-benchmark-strip em{color:var(--lime);font-size:11px;font-style:normal}
.report-chart-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14px;margin-top:22px}.report-chart-card{min-width:0;margin:0;overflow:hidden;border:1px solid var(--line);border-radius:14px;box-shadow:0 7px 24px #1e3c2f0f}.report-chart-head{display:flex;align-items:center;gap:9px;padding:14px 15px 11px;border-bottom:1px solid #edf1ee}.report-chart-head>span{padding:4px 6px;color:var(--green);background:#e8f4ee;border-radius:5px;font-size:11px;font-weight:800}.report-chart-head strong{font-size:13px}.report-chart-stage{height:310px;overflow:hidden;background:white}.report-chart-fallback{display:grid;width:100%;height:100%;place-items:center;padding:6px}.report-chart-fallback svg{display:block;width:100%;height:100%}.report-chart-card figcaption{min-height:83px;padding:12px 15px;color:#65736c;background:#f8faf8;border-top:1px solid #edf1ee;font-size:11px}.report-evidence-code{display:inline-flex;margin-right:5px;padding:3px 6px;color:var(--green);background:#e8f4ee;border-radius:5px;font-size:11px;font-weight:800}
.report-interpretation{display:grid;grid-template-columns:84px 1fr;gap:15px;margin-top:16px;padding:17px 18px;background:linear-gradient(100deg,#edf7f1,#f8fbf7);border:1px solid #dbeadf;border-radius:12px}.report-interpretation>span{align-self:start;padding:5px 8px;color:white;background:var(--green);border-radius:6px;font-size:11px;font-weight:800;text-align:center}.report-interpretation p{margin:0;font-size:13px}.report-interpretation strong{color:var(--green)}
.report-profile-banner{display:grid;grid-template-columns:1.2fr .8fr;gap:20px;padding:22px;color:#e7f0ec;background:radial-gradient(circle at 90% 0,#c6e66f24,transparent 35%),linear-gradient(135deg,#173f33,#102e26);border-radius:15px}.report-profile-kicker{color:var(--lime);font-size:11px;font-weight:800}.report-profile-banner h3{margin:9px 0 8px;color:white;font-size:19px}.report-profile-banner p{margin:0;color:#a9bdb5;font-size:12px}.report-profile-attributes{display:grid;grid-template-columns:1fr 1fr;gap:8px}.report-profile-attributes span{padding:10px;color:#8da79c;background:#ffffff0e;border:1px solid #ffffff12;border-radius:9px;font-size:11px}.report-profile-attributes strong{display:block;margin-top:5px;color:#eaf2ee;font-size:11px}
.report-profile-stat-grid{display:grid;grid-template-columns:repeat(4,1fr);align-items:start;gap:10px;margin-top:13px}.report-profile-stat-grid article{padding:14px;background:#fafbf9;border:1px solid #e5eae6;border-radius:11px}.report-profile-code,.report-profile-label,.report-profile-stat-grid article>strong,.report-profile-stat-grid article>em{display:block}.report-profile-code{color:#84928a;font-size:11px;font-weight:700}.report-profile-label{margin-top:9px;color:var(--muted);font-size:11px}.report-profile-stat-grid article>strong{margin:5px 0 4px;font-size:23px}.report-profile-stat-grid article>strong small{margin-left:3px;color:var(--muted);font-size:11px}.report-profile-stat-grid article>em{min-height:32px;color:var(--green2);font-size:11px;font-style:normal}.report-profile-stat-grid details{margin-top:9px;padding-top:8px;border-top:1px solid #e8ede9}.report-profile-stat-grid summary{color:var(--green);font-size:11px;font-weight:700;cursor:pointer}.report-profile-stat-grid details>div{margin-top:8px;padding:9px;color:var(--muted);background:white;border:1px solid #e8ede9;border-radius:8px;font-size:11px}.report-profile-stat-grid details span,.report-profile-stat-grid details strong{display:block}.report-profile-stat-grid details span+span{margin-top:6px}.report-profile-stat-grid details strong{color:var(--ink)}.report-profile-stat-grid details p{padding-top:8px;border-top:1px dashed var(--line)}
.report-finding-list,.report-action-list{display:flex;flex-direction:column;gap:11px}.report-finding,.report-action-card{display:grid;grid-template-columns:36px 1fr auto;align-items:start;gap:13px;padding:15px;background:#fafbf9;border:1px solid #e5eae6;border-radius:11px}.report-finding>span:first-child,.report-action-priority{display:grid;width:36px;height:36px;place-items:center;color:var(--green);background:#e8f4ee;border-radius:9px;font-weight:800}.report-finding strong,.report-finding small,.report-action-card strong,.report-action-card small{display:block}.report-finding small,.report-action-card small{color:var(--muted);font-size:12px}.report-finding-proof,.report-action-evidence{margin-top:7px;color:var(--green2)!important;font-size:11px!important;font-weight:600}.report-finding em{color:var(--green);font-style:normal;font-weight:800}.report-finding-validation{display:flex;align-items:center;justify-content:center;gap:8px;flex-wrap:wrap;margin-top:15px;padding:13px;background:#f5f8f5;border:1px dashed #d4ded7;border-radius:10px;font-size:11px}.report-finding-validation span{padding:5px 7px;color:var(--green);background:white;border:1px solid #dbe6df;border-radius:6px;font-weight:700}
.report-table-wrap{max-width:100%;overflow-x:auto;border:1px solid #e5eae6;border-radius:10px}.report-table{width:100%;border-collapse:collapse}.report-table th{padding:11px 10px;color:var(--muted);background:#f4f7f4;border-bottom:1px solid var(--line);font-size:12px;text-align:left}.report-table td{padding:13px 10px;border-bottom:1px solid #e5eae6;font-size:13px}.report-comparison-table{min-width:760px}.report-evidence-table{min-width:860px}.report-source-tag,.report-confidence{display:inline-flex;padding:4px 7px;color:var(--green);background:#e8f4ee;border-radius:6px;font-size:11px;font-weight:700}.report-evidence-table td:nth-child(2) small{display:block;margin-top:5px;color:var(--muted);font-size:11px}.report-positive{color:var(--green);font-weight:700}.report-negative{color:#b45249;font-weight:700}.report-subsection-title{margin:28px 0 12px;font-size:15px}
.report-evidence-summary{display:grid;grid-template-columns:repeat(4,1fr);gap:10px;margin-bottom:14px}.report-evidence-summary span{padding:13px;background:#f7faf7;border:1px solid #e5eae6;border-radius:10px}.report-evidence-summary small,.report-evidence-summary strong{display:block}.report-evidence-summary small{color:var(--muted);font-size:11px}.report-evidence-summary strong{margin-top:6px;font-size:14px}.report-proof-chain{display:grid;grid-template-columns:1fr 24px 1fr 24px 1fr 24px 1fr;align-items:center;gap:4px;margin-bottom:15px;padding:14px;background:#173e32;border-radius:12px}.report-proof-chain>span{text-align:center}.report-proof-chain i,.report-proof-chain strong,.report-proof-chain small{display:block}.report-proof-chain i{width:27px;height:27px;margin:0 auto 6px;padding-top:5px;color:#173e32;background:var(--lime);border-radius:8px;font-size:11px;font-style:normal;font-weight:800}.report-proof-chain strong{color:white;font-size:11px}.report-proof-chain small{color:#92aaa0;font-size:11px}.report-proof-chain>b{height:1px;background:#668078}.report-evidence-notice{margin-top:14px;padding:15px 17px;color:#6f5220;background:#fff9ed;border:1px solid #eee0bd;border-radius:10px}.report-evidence-notice strong{display:block}.report-evidence-notice p{margin:5px 0 0;font-size:11px}
.report-method-steps{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-bottom:16px}.report-method-steps article{display:grid;grid-template-columns:34px 1fr;gap:3px 11px;padding:14px;background:#fafbf9;border:1px solid #e5eae6;border-radius:10px}.report-method-steps article>span{display:grid;width:34px;height:34px;grid-row:1/3;place-items:center;color:var(--green);background:#e8f4ee;border-radius:9px;font-size:11px;font-weight:800}.report-method-steps strong{font-size:12px}.report-method-steps p{margin:2px 0 0;color:var(--muted);font-size:11px}.report-method-note{margin-top:18px;padding:15px;background:#f7f8f5;border:1px dashed #ced8d1;border-radius:10px;font-size:12px}
.report-action-priority{color:#a14238;background:#fff0ed}.report-action-card button{display:none}.report-action-cycle{display:grid;grid-template-columns:repeat(7,auto);align-items:center;justify-content:center;gap:10px;margin-top:17px;padding:15px;color:#dcebe4;background:#173e32;border-radius:11px}.report-action-cycle span,.report-action-cycle strong,.report-action-cycle small{display:block}.report-action-cycle strong{font-size:11px}.report-action-cycle small{color:#91aaa0;font-size:11px}.report-action-cycle i{width:28px;height:1px;background:var(--lime)}
.report-file-note{max-width:1040px;margin:0 auto 30px;color:var(--muted);text-align:center;font-size:13px}
.report-document small,.report-document summary,.report-document .report-source-tag,.report-document .report-confidence,.report-document .report-evidence-code,.report-document .report-profile-code,.report-document .report-profile-label,.report-document .report-action-priority,.report-document .report-standard-stamp{font-size:13px!important}
.report-document p,.report-document li,.report-document td,.report-document figcaption,.report-document .report-finding small,.report-document .report-action-card small{font-size:14px!important}
.report-document th,.report-document svg text{font-size:13px!important}
@media(max-width:700px){body{background:white}.report-document{margin:0;box-shadow:none}.report-cover-page{min-height:480px;padding:44px 25px}.report-cover-main h1{font-size:30px}.report-section{padding:32px 20px}.report-metric-grid,.report-benchmark-strip,.report-evidence-summary,.report-profile-stat-grid{grid-template-columns:1fr 1fr}.report-chart-grid,.report-profile-banner,.report-method-steps{grid-template-columns:1fr}.report-profile-attributes{grid-template-columns:1fr 1fr}.report-cover-foot{grid-template-columns:1fr}.report-finding,.report-action-card{grid-template-columns:36px 1fr}.report-finding em{grid-column:2}.report-interpretation{grid-template-columns:1fr}.report-proof-chain,.report-action-cycle{grid-template-columns:1fr}.report-proof-chain>b,.report-action-cycle i{width:1px;height:14px;justify-self:center}.report-chart-card figcaption{min-height:0}}
@media print{body{background:white}.report-document{max-width:none;margin:0;box-shadow:none}.report-cover-page{break-after:page}.report-section{break-before:page}.report-chart-card,.report-profile-stat-grid article,.report-finding,.report-action-card{break-inside:avoid}.report-file-note{display:none}}
`;
}
function downloadReportSample(reportId) {
const report = reports.find((item) => item.id === reportId);
if (!report) return;
const isClosedLoopReport = closedLoopState.entities.report?.id === reportId;
const official = !isClosedLoopReport || closedLoopState.entities.report.status === "已发布";
const editionLabel = official ? "正式发布版" : "非正式版 · 仅供审核";
const content = `
${report.title}
${editionLabel}${isClosedLoopReport ? ` · ${report.id} · ${closedLoopState.entities.report.status}` : ""}
${renderReportDocument(report, { interactive: false })}
本文件由产教融智报告中心生成 · 数据快照 ${report.version} · ${report.period}
`;
const blob = new Blob([content], { type: "text/html;charset=utf-8" });
const url = URL.createObjectURL(blob);
const anchor = document.createElement("a");
anchor.href = url;
anchor.download = `${report.title.replace(/[\\/:*?"<>|]/g, "-")}-${official ? "正式版" : "非正式版"}.html`;
anchor.click();
setTimeout(() => URL.revokeObjectURL(url), 500);
if (isClosedLoopReport) {
addClosedLoopAudit("REPORT_DOWNLOADED", report.id, `${editionLabel}离线HTML`);
saveClosedLoopState();
renderClosedLoop();
}
showToast(`离线HTML报告已下载,当前标识:${editionLabel}`, "下载已开始");
}
function setupSourceFilters() {
const filterButtons = [...document.querySelectorAll("[data-source-filter]")];
const summaryCards = [...document.querySelectorAll("[data-source-summary-filter]")];
const applyFilter = (filter) => {
filterButtons.forEach((button) => button.classList.toggle("active", button.dataset.sourceFilter === filter));
summaryCards.forEach((card) => card.classList.toggle("active", card.dataset.sourceSummaryFilter === filter));
renderSources(filter);
document.querySelector(".source-panel").scrollIntoView({ behavior: "smooth", block: "start" });
};
filterButtons.forEach((button) => {
button.addEventListener("click", () => applyFilter(button.dataset.sourceFilter));
});
summaryCards.forEach((card) => {
card.addEventListener("click", () => applyFilter(card.dataset.sourceSummaryFilter));
});
}
function setupDataOperations() {
document.getElementById("addDataSourceButton").addEventListener("click", () => openDataOpsDrawer("add"));
document.getElementById("dataAuthorizationButton").addEventListener("click", () => openDataOpsDrawer("authorization"));
document.getElementById("syncAllButton").addEventListener("click", () => openDataOpsDrawer("batch"));
document.querySelectorAll("[data-data-issue]").forEach((button) => {
button.addEventListener("click", () => openDataOpsDrawer("issue", button.dataset.dataIssue));
});
document.getElementById("dataOpsTabs").addEventListener("click", (event) => {
const button = event.target.closest("[data-data-ops-tab]");
if (!button) return;
dataOpsState.tab = button.dataset.dataOpsTab;
updateDataOpsDrawer();
});
document.getElementById("dataOpsDrawer").addEventListener("click", (event) => {
if (event.target === event.currentTarget) closeDataOpsDrawer();
});
document.getElementById("closeDataOpsDrawer").addEventListener("click", closeDataOpsDrawer);
document.getElementById("dataOpsFooter").addEventListener("click", (event) => {
const closeButton = event.target.closest("[data-data-ops-close]");
if (closeButton) {
closeDataOpsDrawer();
return;
}
const tabButton = event.target.closest("[data-data-ops-tab-jump]");
if (tabButton) {
dataOpsState.tab = tabButton.dataset.dataOpsTabJump;
updateDataOpsDrawer();
document.querySelector(".data-ops-drawer").scrollTop = 0;
return;
}
const resultButton = event.target.closest("[data-data-ops-result]");
if (resultButton) {
const message = resultButton.dataset.dataOpsResult;
closeDataOpsDrawer();
showToast(message, "数据运维任务已记录");
}
});
document.getElementById("dataOpsContent").addEventListener("click", (event) => {
const loopAction = event.target.closest("[data-loop-action]");
if (loopAction) {
closeDataOpsDrawer();
openClosedLoopAction(loopAction.dataset.loopAction);
return;
}
const method = event.target.closest(".data-source-methods button");
if (method) {
method.parentElement.querySelectorAll("button").forEach((item) => item.classList.toggle("active", item === method));
}
});
document.getElementById("dataOpsFooter").addEventListener("click", (event) => {
if (event.target.closest("[data-view-loop]")) {
closeDataOpsDrawer();
switchView("loop");
}
});
document.addEventListener("click", (event) => {
const sourceButton = event.target.closest("[data-source-detail]");
if (sourceButton) openDataOpsDrawer("source", sourceButton.dataset.sourceDetail);
});
}
function setupWorkspaceDetails() {
document.getElementById("organizationSwitcher").addEventListener("click", () => openWorkspaceDetail("organizations"));
document.getElementById("notificationButton").addEventListener("click", () => openWorkspaceDetail("notifications"));
document.getElementById("industryTrendMore").addEventListener("click", () => openWorkspaceDetail("industry"));
document.getElementById("compareProfession").addEventListener("click", () => openWorkspaceDetail("compare"));
document.getElementById("reportTemplateButton").addEventListener("click", () => openWorkspaceDetail("templates"));
document.getElementById("closeWorkspaceDetail").addEventListener("click", closeWorkspaceDetail);
document.getElementById("workspaceDetailDrawer").addEventListener("click", (event) => {
if (event.target === event.currentTarget) closeWorkspaceDetail();
});
document.getElementById("workspaceDetailFooter").addEventListener("click", (event) => {
if (event.target.closest("[data-workspace-close]")) {
closeWorkspaceDetail();
return;
}
if (event.target.closest("[data-workspace-confirm]")) {
const action = event.target.closest("[data-workspace-confirm]").textContent;
closeWorkspaceDetail();
showToast(`${action}已保存,并写入业务记录`, "业务操作已完成");
}
});
document.addEventListener("click", (event) => {
const project = event.target.closest("[data-project-detail]");
if (project) {
openWorkspaceDetail("project", project.dataset.projectId || project.dataset.projectDetail);
return;
}
const task = event.target.closest("[data-task-detail]");
if (task) {
openWorkspaceDetail("task", task.dataset.taskId || task.dataset.taskDetail);
return;
}
const course = event.target.closest("[data-workspace-detail^='course-']");
if (course) {
openWorkspaceDetail("course", course.dataset.workspaceDetail);
return;
}
});
document.getElementById("workspaceDetailContent").addEventListener("click", (event) => {
const loopAction = event.target.closest("[data-loop-action]");
if (loopAction) {
closeWorkspaceDetail();
openClosedLoopAction(loopAction.dataset.loopAction);
return;
}
const viewButton = event.target.closest("[data-view-jump]");
if (viewButton) {
closeWorkspaceDetail();
switchView(viewButton.dataset.viewJump);
return;
}
const issueButton = event.target.closest("[data-data-issue]");
if (issueButton) {
closeWorkspaceDetail();
openDataOpsDrawer("issue", issueButton.dataset.dataIssue);
}
});
document.querySelectorAll("[data-project-filter]").forEach((button) => {
button.addEventListener("click", () => {
button.parentElement.querySelectorAll("button").forEach((item) => item.classList.toggle("active", item === button));
renderProjects(button.dataset.projectFilter);
});
});
document.querySelectorAll("[data-partner-filter]").forEach((button) => {
button.addEventListener("click", () => {
button.parentElement.querySelectorAll("button").forEach((item) => item.classList.toggle("active", item === button));
renderPartners(button.dataset.partnerFilter);
});
});
}
const productTourSteps = [
{ view: "dashboard", selector: ".quality-strip", title: "先核对数据", description: "先看数据来自哪里、何时更新、是否通过检查;分析只使用已验收版本。" },
{ view: "dashboard", selector: ".ai-panel", title: "查看关键变化", description: "系统整理岗位、技能和专业变化,每条结论都可打开查看数据来源。" },
{ view: "profession", selector: ".profile-banner", title: "定位专业差距", description: "从岗位、技能、薪资和课程映射判断专业优势与结构性缺口。" },
{ view: "collaboration", selector: ".collab-stats", title: "安排企业合作", description: "把企业需求写入课程共建、企业项目、企业导师和实习就业计划。" },
{ view: "actions", selector: ".action-overview", title: "跟踪改进成效", description: "每项建议都有负责人、截止日期、进展证据和下一周期效果复核。" },
{ view: "data", selector: ".source-summary-grid", title: "管理数据资产", description: "厂商、学校和企业三类数据分权维护,支持初次导入、增量更新、版本和审计。" },
{ view: "reports", selector: ".featured-report-grid", title: "完成正式交付", description: "在线报告、材料包和展示大屏使用同一数据快照,适用于决策、汇报与报送。" },
];
let productTourStep = 0;
function closeProductTour() {
const tour = document.getElementById("productTour");
tour.classList.remove("open");
tour.setAttribute("aria-hidden", "true");
if (document.getElementById("productTourCoach").hidden) document.body.style.overflow = "";
}
function closeTourCoach() {
document.getElementById("productTourCoach").hidden = true;
document.querySelectorAll(".tour-target").forEach((item) => item.classList.remove("tour-target"));
document.body.classList.remove("tour-running");
}
function renderProductTourStep() {
const step = productTourSteps[productTourStep];
document.querySelectorAll(".tour-target").forEach((item) => item.classList.remove("tour-target"));
switchView(step.view);
const target = document.querySelector(`#view-${step.view} ${step.selector}`);
if (target) {
target.classList.add("tour-target");
setTimeout(() => target.scrollIntoView({ behavior: "smooth", block: "center" }), 80);
}
document.getElementById("tourStepLabel").textContent = `${String(productTourStep + 1).padStart(2, "0")} / ${String(productTourSteps.length).padStart(2, "0")}`;
document.getElementById("tourStepTitle").textContent = step.title;
document.getElementById("tourStepDescription").textContent = step.description;
document.getElementById("tourPrevious").disabled = productTourStep === 0;
document.getElementById("tourNext").textContent = productTourStep === productTourSteps.length - 1 ? "进入展示大屏" : "下一步";
}
function openShowcaseScreen() {
closeTourCoach();
const screen = document.getElementById("showcaseScreen");
screen.classList.add("open");
screen.setAttribute("aria-hidden", "false");
document.body.style.overflow = "hidden";
}
function closeShowcaseScreen() {
const screen = document.getElementById("showcaseScreen");
screen.classList.remove("open");
screen.setAttribute("aria-hidden", "true");
document.body.style.overflow = "";
}
function setupProductExperience() {
document.getElementById("productTourButton").addEventListener("click", () => {
const tour = document.getElementById("productTour");
tour.classList.add("open");
tour.setAttribute("aria-hidden", "false");
document.body.style.overflow = "hidden";
});
document.getElementById("closeProductTour").addEventListener("click", closeProductTour);
document.getElementById("productTour").addEventListener("click", (event) => {
if (event.target === event.currentTarget) closeProductTour();
});
document.getElementById("startProductTour").addEventListener("click", () => {
const roleSelect = document.getElementById("roleSelect");
roleSelect.value = "校级管理者";
applyRole("校级管理者", { notify: false });
closeProductTour();
productTourStep = 0;
document.getElementById("productTourCoach").hidden = false;
document.body.classList.add("tour-running");
renderProductTourStep();
});
document.getElementById("tourPlatformButton").addEventListener("click", () => {
document.getElementById("roleSelect").value = "厂商超级管理员";
applyRole("厂商超级管理员", { notify: false });
closeProductTour();
});
document.getElementById("tourReportButton").addEventListener("click", () => {
document.getElementById("roleSelect").value = "校级管理者";
applyRole("校级管理者", { notify: false });
closeProductTour();
switchView("reports");
setTimeout(() => openReportReader("school-2026-03"), 120);
});
document.getElementById("tourPrevious").addEventListener("click", () => {
if (productTourStep > 0) {
productTourStep -= 1;
renderProductTourStep();
}
});
document.getElementById("tourNext").addEventListener("click", () => {
if (productTourStep === productTourSteps.length - 1) {
openShowcaseScreen();
return;
}
productTourStep += 1;
renderProductTourStep();
});
document.getElementById("closeTourCoach").addEventListener("click", closeTourCoach);
document.querySelectorAll("[data-showcase-open]").forEach((button) => button.addEventListener("click", openShowcaseScreen));
document.getElementById("closeShowcaseScreen").addEventListener("click", closeShowcaseScreen);
}
function setupGenericInteractions() {
document.querySelectorAll(".nav-item[data-view]").forEach((button) => {
button.addEventListener("click", () => switchView(button.dataset.view));
});
document.querySelectorAll("[data-view-jump]").forEach((button) => {
button.addEventListener("click", () => switchView(button.dataset.viewJump));
});
document.querySelectorAll("[data-toast]").forEach((button) => {
button.addEventListener("click", () => showToast(button.dataset.toast, "操作已记录"));
});
document.querySelectorAll("[data-close-modal]").forEach((button) => {
button.addEventListener("click", () => closeModal(button.closest(".modal-backdrop")));
});
document.querySelectorAll(".modal-backdrop").forEach((backdrop) => {
backdrop.addEventListener("click", (event) => {
if (event.target === backdrop && backdrop.id !== "analysisModal") closeModal(backdrop);
});
});
document.addEventListener("keydown", (event) => {
if (event.key === "Escape") {
if (document.getElementById("showcaseScreen").classList.contains("open")) {
closeShowcaseScreen();
return;
}
if (!document.getElementById("productTourCoach").hidden) {
closeTourCoach();
return;
}
if (document.getElementById("productTour").classList.contains("open")) {
closeProductTour();
return;
}
if (document.getElementById("dataOpsDrawer").classList.contains("open")) {
closeDataOpsDrawer();
return;
}
if (document.getElementById("workspaceDetailDrawer").classList.contains("open")) {
closeWorkspaceDetail();
return;
}
if (document.getElementById("reportReader").classList.contains("open")) {
closeReportReader();
return;
}
const open = document.querySelector(".modal-backdrop.open");
if (open && open.id !== "analysisModal") closeModal(open);
document.body.classList.remove("sidebar-open");
}
});
const mobileMenu = document.getElementById("mobileMenu");
const sidebarScrim = document.getElementById("sidebarScrim");
mobileMenu.addEventListener("click", () => document.body.classList.toggle("sidebar-open"));
sidebarScrim.addEventListener("click", () => document.body.classList.remove("sidebar-open"));
const tooltip = document.getElementById("tooltip");
document.querySelectorAll("[data-tip]").forEach((button) => {
const show = () => {
tooltip.textContent = button.dataset.tip;
const rect = button.getBoundingClientRect();
tooltip.style.left = `${Math.min(rect.left, window.innerWidth - 245)}px`;
tooltip.style.top = `${rect.bottom + 7}px`;
tooltip.classList.add("show");
};
const hide = () => tooltip.classList.remove("show");
button.addEventListener("mouseenter", show);
button.addEventListener("mouseleave", hide);
button.addEventListener("focus", show);
button.addEventListener("blur", hide);
});
document.querySelectorAll("[data-report-generate]").forEach((button) => {
button.addEventListener("click", () => openReportGeneration(button.dataset.reportGenerate));
});
}
function openReportGeneration(context = "center") {
const modal = document.getElementById("reportModal");
const title = document.getElementById("reportModalTitle");
const description = document.getElementById("reportModalDescription");
const options = document.getElementById("reportOptions");
const currentProfession = document.getElementById("professionTitle")?.textContent || "软件技术";
const configurations = {
package: {
title: "生成供需会材料包",
description: "所有报告将使用同一分析快照,确保统计口径一致。",
options: [
["全校供需分析总报告", "约42页 · PDF / Word", true],
["32个专业人才需求报告", "独立文件 · 自动生成目录", true],
["关键指标一页纸", "适用于会议汇报与报送", true],
["数据口径与质量附件", "指标定义、样本量及数据来源", false],
],
},
profession: {
title: `生成${currentProfession}专业报告`,
description: "生成包含岗位、技能、薪资、企业与课程建议的专业分析报告。",
options: [
["执行摘要与专业画像", "核心指标、趋势与结论", true],
["技能供需与课程映射", "技能缺口及课程覆盖依据", true],
["岗位、薪资与企业分析", "岗位需求、薪资分位数和合作企业", true],
["行动建议与数据附件", "建议责任、口径及数据来源", true],
],
},
center: {
title: "生成新分析报告",
description: "选择报告范围和内容,生成后将自动进入报告中心。",
options: [
["校级供需分析报告", "全校专业和产业供需总览", true],
["学院或专业报告", "按当前账号数据范围生成", true],
["产业人才缺口专题", "重点方向、企业和增长趋势", false],
["数据质量与口径报告", "数据资产、质量与使用限制", false],
],
},
};
const configuration = configurations[context] || configurations.center;
modal.dataset.context = context;
title.textContent = configuration.title;
description.textContent = configuration.description;
options.innerHTML = `
${configuration.options
.map(
([name, note, checked]) =>
``,
)
.join("")}
`;
openModal(modal);
}
function setupReportCenterInteractions() {
const typeFilters = document.getElementById("reportTypeFilters");
const search = document.getElementById("reportSearch");
const period = document.getElementById("reportPeriodFilter");
typeFilters.addEventListener("click", (event) => {
const button = event.target.closest("[data-report-filter]");
if (!button) return;
reportState.type = button.dataset.reportFilter;
reportState.limit = 6;
typeFilters.querySelectorAll("button").forEach((item) => item.classList.toggle("active", item === button));
renderReportList();
});
search.addEventListener("input", () => {
reportState.search = search.value;
reportState.limit = 6;
renderReportList();
});
period.addEventListener("change", () => {
reportState.period = period.value;
reportState.limit = 6;
renderReportList();
});
document.getElementById("loadMoreReports").addEventListener("click", () => {
reportState.limit += 4;
renderReportList();
});
document.addEventListener("click", (event) => {
const openButton = event.target.closest("[data-report-open]");
if (openButton) {
openReportReader(openButton.dataset.reportOpen);
return;
}
const downloadButton = event.target.closest("[data-report-download]");
if (downloadButton) {
downloadReportSample(downloadButton.dataset.reportDownload);
return;
}
const actionButton = event.target.closest("[data-report-action-create]");
if (actionButton) {
const title = actionButton.closest(".report-action-card").querySelector("strong").textContent.trim();
if (!tasks.some((task) => task.title === title)) {
const report = reports.find((item) => item.id === document.getElementById("reportReader").dataset.reportId);
const task = {
id: `TSK-REPORT-${Date.now()}`,
title,
source: `报告建议 · ${report?.version || "当前版本"} · 已关联数据来源`,
profession: report?.type === "profession" ? report.scope.split("·").pop().trim() : "全校专业",
owner: "待确认",
due: "2026-04-30",
progress: 0,
status: "review",
statusText: "待确认",
};
closedLoopState.custom.tasks.unshift(task);
addClosedLoopAudit("TASK_CREATED_FROM_REPORT", task.id, `报告 ${report?.id || "未知"} · ${title}`);
saveClosedLoopState();
syncClosedLoopCollections();
renderTasks();
}
actionButton.textContent = "已加入任务";
actionButton.disabled = true;
showToast("报告建议已创建改进任务,并关联本报告的数据证据与版本");
}
});
document.getElementById("closeReportReader").addEventListener("click", closeReportReader);
document.getElementById("reportToc").addEventListener("click", (event) => {
const button = event.target.closest("[data-report-section]");
if (!button) return;
document.querySelectorAll("#reportToc button").forEach((item) => item.classList.toggle("active", item === button));
const section = document.getElementById(`report-section-${button.dataset.reportSection}`);
section?.scrollIntoView({ behavior: "smooth", block: "start" });
});
document.getElementById("reportPrintButton").addEventListener("click", () => {
const reportId = document.getElementById("reportReader").dataset.reportId;
if (closedLoopState.entities.report?.id === reportId) {
const official = closedLoopState.entities.report.status === "已发布";
addClosedLoopAudit("REPORT_PRINTED", reportId, official ? "正式版打印" : "非正式版打印");
saveClosedLoopState();
showToast(official ? "当前打印为正式发布版。" : "报告尚未发布,打印输出将作为非正式版使用。", official ? "正式版" : "非正式版提示");
}
window.print();
});
document.getElementById("reportDownloadButton").addEventListener("click", () => {
downloadReportSample(document.getElementById("reportReader").dataset.reportId);
});
}
function setupRoleExperience() {
const roleSelect = document.getElementById("roleSelect");
roleSelect.addEventListener("change", () => applyRole(roleSelect.value));
document.querySelectorAll("[data-platform-target]").forEach((button) => {
button.addEventListener("click", () => {
const targetId = button.dataset.platformTarget;
switchView("platform");
document.querySelectorAll("#platformNav [data-platform-target]").forEach((item) => {
item.classList.toggle("active", item.dataset.platformTarget === targetId);
});
const labels = {
platformOverview: "平台总览",
packageManagement: "数据包管理",
tenantManagement: "机构与租户",
accountManagement: "账号与权限",
backupManagement: "备份与恢复",
auditManagement: "审计与监控",
};
document.getElementById("breadcrumbCurrent").textContent = labels[targetId] || "平台总览";
setTimeout(() => {
document.getElementById(targetId)?.scrollIntoView({ behavior: "smooth", block: "start" });
}, 80);
document.body.classList.remove("sidebar-open");
});
});
}
const platformActionTemplates = {
import: {
kicker: "DATA PACKAGE IMPORT",
title: "导入基础数据包",
description: "校验厂商数据包版本,并选择本次发布的学校租户范围。",
confirm: "开始导入",
progress: true,
content: `
`,
},
backup: {
kicker: "PLATFORM BACKUP",
title: "创建平台备份",
description: "本操作仅创建新的加密备份,不影响当前生产数据。",
confirm: "开始备份",
progress: true,
content: `
`,
},
restore: {
kicker: "RECOVERY DRILL",
title: "发起恢复演练",
description: "在隔离环境中验证备份可恢复性,不覆盖生产数据。",
confirm: "开始演练",
progress: true,
content: `
`,
},
tenant: {
kicker: "TENANT ONBOARDING",
title: "新增学校租户",
description: "创建学校独立工作空间并配置初始数据服务。",
confirm: "创建租户",
content: `
`,
},
packageDetail: {
kicker: "DATA PACKAGE DETAIL",
title: "数据包服务详情",
description: "查看版本、质量校验和学校授权情况。",
confirm: "完成",
content: `
当前生产版本 V2026.032026-03-18 发布 · 86.4万条有效记录 · 质量评分 93.4
授权与服务12家学校已订阅,服务协议全部有效,下次更新计划为 2026-04-01。
`,
},
tenantDetail: {
kicker: "TENANT DETAIL",
title: "学校租户详情",
description: "查看租户版本、服务状态和资源用量。",
confirm: "进入租户",
content: `
租户运行正常旗舰版 · 386名用户 · 6个数据源 · 本月生成报告74份
资源使用数据存储 38.2GB / 100GB,AI分析额度使用 64%,服务有效至 2027-02-28。
`,
},
service: {
kicker: "SERVICE MONITOR",
title: "平台服务监控",
description: "查看服务可用性、任务吞吐和最近异常。",
confirm: "完成",
content: `最近24小时运行正常共处理428个任务,成功425个,3个任务已自动重试成功,无生产事故。
`,
},
account: {
kicker: "PLATFORM ACCOUNTS",
title: "平台管理员账号",
description: "管理厂商运营、实施与技术支持账号。",
confirm: "进入管理",
content: `8 个平台级账号超级管理员2人,平台运营3人,数据运维2人,安全审计1人,全部启用MFA。
`,
},
permission: {
kicker: "ROLE PERMISSION",
title: "角色与权限模板",
description: "分别设置学校、学院、企业和平台运营人员的权限。",
confirm: "进入配置",
content: `7 套权限模板采用租户隔离、角色权限和数据范围三层控制,最近修改已通过审计复核。
`,
},
security: {
kicker: "SECURITY POLICY",
title: "平台安全策略",
description: "查看认证、访问控制和安全告警策略。",
confirm: "完成",
content: `当前无高危安全风险MFA覆盖率100%,异常登录拦截3次,密钥轮换将在12天后执行。
`,
},
audit: {
kicker: "AUDIT LOG",
title: "平台审计日志",
description: "查看数据导入、权限变更、租户操作和系统任务记录。",
confirm: "查看完整日志",
content: `今日记录 286 条关键操作数据操作68条,权限操作12条,系统任务201条,安全事件5条,全部保留操作者和时间戳。
`,
},
};
function setupPlatformActions() {
const modal = document.getElementById("platformActionModal");
const confirmButton = document.getElementById("platformActionConfirm");
document.querySelectorAll("[data-platform-action]").forEach((button) => {
button.addEventListener("click", () => {
const action = button.dataset.platformAction;
const template = platformActionTemplates[action] || platformActionTemplates.service;
modal.dataset.action = action;
document.getElementById("platformActionKicker").textContent = template.kicker;
document.getElementById("platformActionTitle").textContent = template.title;
document.getElementById("platformActionDescription").textContent = template.description;
document.getElementById("platformActionContent").innerHTML = template.content;
document.getElementById("platformActionProgress").hidden = true;
document.getElementById("platformActionProgressBar").style.width = "0";
confirmButton.textContent = template.confirm;
confirmButton.disabled = false;
openModal(modal);
});
});
confirmButton.addEventListener("click", () => {
const action = modal.dataset.action;
const template = platformActionTemplates[action] || platformActionTemplates.service;
if (!template.progress) {
closeModal(modal);
showToast(`${template.title}操作已记录`, "平台操作");
return;
}
const progress = document.getElementById("platformActionProgress");
const bar = document.getElementById("platformActionProgressBar");
const value = document.getElementById("platformActionProgressValue");
const text = document.getElementById("platformActionProgressText");
progress.hidden = false;
confirmButton.disabled = true;
let current = 0;
const messages = {
import: ["正在校验数据包签名…", "正在检查字段与质量规则…", "正在写入版本库并发布…"],
backup: ["正在创建一致性快照…", "正在加密并写入备份存储…", "正在校验备份完整性…"],
restore: ["正在创建隔离环境…", "正在恢复备份数据…", "正在验证数据完整性…"],
}[action];
const timer = setInterval(() => {
current = Math.min(current + 10, 100);
bar.style.width = `${current}%`;
value.textContent = `${current}%`;
text.textContent = messages[Math.min(Math.floor(current / 35), 2)];
if (current === 100) {
clearInterval(timer);
setTimeout(() => {
closeModal(modal);
confirmButton.disabled = false;
const successMessages = {
import: "基础数据包已通过校验并发布到所选学校",
backup: "平台备份已创建并完成完整性校验",
restore: "隔离恢复演练成功,生产数据未受影响",
};
showToast(successMessages[action], "平台操作完成");
}, 350);
}
}, 90);
});
}
function setupAnalysis() {
document.getElementById("runAnalysis").addEventListener("click", () => {
const modal = document.getElementById("analysisModal");
const progress = document.getElementById("analysisProgress");
const status = document.getElementById("analysisStatus");
const steps = [...document.querySelectorAll(".analysis-steps span")];
const statuses = [
"读取本期分析快照…",
"归一并匹配产业岗位…",
"计算专业技能覆盖差距…",
"整理分析结果和建议…",
];
openModal(modal);
progress.style.width = "0";
steps.forEach((step, index) => step.classList.toggle("active", index === 0));
let value = 0;
const timer = setInterval(() => {
value += 10;
progress.style.width = `${value}%`;
const stage = Math.min(Math.floor(value / 26), 3);
status.textContent = statuses[stage];
steps.forEach((step, index) => step.classList.toggle("active", index <= stage));
if (value >= 100) {
clearInterval(timer);
setTimeout(() => {
closeModal(modal);
showToast("供需分析 V2026.03 已生成,3项关键变化需要关注");
}, 350);
}
}, 120);
});
}
function setupReportGeneration() {
const button = document.getElementById("generateReportButton");
button.addEventListener("click", () => {
const progressWrap = document.getElementById("reportProgress");
const progressBar = document.getElementById("reportProgressBar");
const progressValue = document.getElementById("reportProgressValue");
const progressText = document.getElementById("reportProgressText");
progressWrap.hidden = false;
button.disabled = true;
button.textContent = "正在生成…";
let value = 0;
const timer = setInterval(() => {
value += 8;
if (value > 100) value = 100;
progressBar.style.width = `${value}%`;
progressValue.textContent = `${value}%`;
if (value < 34) progressText.textContent = "正在准备报告模板…";
else if (value < 70) progressText.textContent = "正在渲染32个专业图表…";
else progressText.textContent = "正在校验口径并打包…";
if (value === 100) {
clearInterval(timer);
setTimeout(() => {
const baseReport = reportsForCurrentRole()[0] || reports[0];
const generatedReport = {
...baseReport,
id: `generated-${Date.now()}`,
version: `${baseReport.version}-R${reports.filter((item) => item.id.startsWith("generated-")).length + 1}`,
status: "待审核",
statusClass: "review",
updated: "刚刚",
featured: false,
};
closedLoopState.custom.reports.unshift(generatedReport);
addClosedLoopAudit("REPORT_CREATED", generatedReport.id, `${generatedReport.title} · ${generatedReport.status}`);
saveClosedLoopState();
syncClosedLoopCollections();
closeModal(document.getElementById("reportModal"));
showToast("报告已生成并进入报告中心,可继续在线阅读或下载");
progressWrap.hidden = true;
progressBar.style.width = "0";
button.disabled = false;
button.innerHTML = `${svgIcon("file")}开始生成`;
const role = document.getElementById("roleSelect").value;
if (roleProfiles[role]?.menus.includes("report")) {
switchView("reports");
renderReportCenter();
}
}, 450);
}
}, 100);
});
}
function configureFormModal(type) {
const currentRole = document.getElementById("roleSelect").value;
const configs = {
task: {
kicker: "ACTION FORM",
title: "创建改进任务",
description: "把分析结果写成有负责人、截止日期和验收要求的任务。",
label: "任务名称",
value: "强化云原生与容器化实训内容",
scopeLabel: "关联专业",
scopeOptions: ["软件技术", "机电一体化技术", "电子商务"],
priorityLabel: "优先级",
priorityOptions: ["高优先级", "中优先级", "低优先级"],
goalLabel: "目标与说明",
goal: "依据近90天岗位数据,将 Docker、Kubernetes 与 CI/CD 实践纳入实训课程,并邀请企业导师参与课程评审。",
ownerLabel: "负责人",
owner: "李雯 · 信息工程学院",
dueLabel: "计划完成时间",
due: "2026-06-30",
},
project: {
kicker: "COLLABORATION PROJECT",
title: "新建合作项目",
description: "连接专业团队与企业伙伴,共同推进人才培养项目。",
label: "项目名称",
value: "企业项目进课堂",
scopeLabel: "牵头专业",
scopeOptions: ["软件技术", "机电一体化技术", "电子商务"],
priorityLabel: "项目类型",
priorityOptions: ["企业项目进课堂", "课程共建", "实训基地", "企业导师"],
goalLabel: "共建目标与验收方式",
goal: "围绕企业任务共同设计教学项目、过程评价和成果验收,并保存企业任务书与导师评价。",
ownerLabel: "校内负责人",
owner: "王健 · 产教融合中心",
dueLabel: "计划启动时间",
due: "2026-04-15",
},
demand: {
kicker: "ENTERPRISE RESEARCH",
title: "发起企业需求调研",
description: "向合作企业收集岗位、技能和人才培养需求。",
label: "调研主题",
value: "2026年度重点产业人才需求调研",
scopeLabel: "调研范围",
scopeOptions: ["126家合作企业", "重点产业企业", "当前专业合作企业"],
priorityLabel: "调研类型",
priorityOptions: ["岗位与技能需求", "课程共建意愿", "实习就业需求"],
goalLabel: "调研说明",
goal: "收集未来12个月岗位、技能、人才数量和校企合作意愿,企业提交结果需确认授权用途与有效期。",
ownerLabel: "发起部门",
owner: "产教融合中心",
dueLabel: "反馈截止时间",
due: "2026-04-30",
},
};
if (currentRole === "企业导师" && type === "demand") {
configs.demand = {
kicker: "ENTERPRISE DEMAND",
title: "提交企业人才需求",
description: "提交岗位、技能与共育需求,供学校专业团队确认是否承接。",
label: "需求名称",
value: "边缘计算软件测试人才共育需求",
scopeLabel: "关联专业",
scopeOptions: ["软件技术", "物联网应用技术", "计算机网络技术"],
priorityLabel: "合作方式",
priorityOptions: ["人才共育", "企业项目", "课程共建", "实习就业"],
goalLabel: "企业需求说明",
goal: "填写岗位、技能、计划人数和合作方式,并由企业联系人确认数据用途与有效期。",
ownerLabel: "企业联系人",
owner: "陈凯 · 企业人才发展部",
dueLabel: "期望响应时间",
due: "2026-04-20",
};
}
if (currentRole === "企业导师" && type === "project") {
configs.project = {
kicker: "CO-CREATION PROJECT",
title: "发起校企共建项目",
description: "邀请学校共同设计企业项目、课程内容和评价标准。",
label: "项目名称",
value: "智能物联边缘计算综合实训",
scopeLabel: "面向专业",
scopeOptions: ["物联网应用技术", "软件技术", "计算机网络技术"],
priorityLabel: "项目类型",
priorityOptions: ["企业项目", "课程共建", "企业导师", "实训基地"],
goalLabel: "企业任务与验收要求",
goal: "按边缘计算测试场景设计实训任务,由企业导师参加中期评审,并检查测试报告、代码质量和协作过程。",
ownerLabel: "企业负责人",
owner: "陈凯 · 企业人才发展部",
dueLabel: "建议启动时间",
due: "2026-04-20",
};
}
const config = configs[type];
document.getElementById("formModalKicker").textContent = config.kicker;
document.getElementById("formModalTitle").textContent = config.title;
document.getElementById("formModalDescription").textContent = config.description;
document.getElementById("formNameLabel").textContent = config.label;
document.getElementById("formName").value = config.value;
document.getElementById("formScopeLabel").textContent = config.scopeLabel;
document.getElementById("formScope").innerHTML = config.scopeOptions.map((option) => ``).join("");
document.getElementById("formPriorityLabel").textContent = config.priorityLabel;
document.getElementById("formPriority").innerHTML = config.priorityOptions.map((option) => ``).join("");
document.getElementById("formGoalLabel").textContent = config.goalLabel;
document.getElementById("formGoal").value = config.goal;
document.getElementById("formOwnerLabel").textContent = config.ownerLabel;
document.getElementById("formOwner").value = config.owner;
document.getElementById("formDueLabel").textContent = config.dueLabel;
document.getElementById("formDue").value = config.due;
document.getElementById("actionForm").dataset.type = type;
openModal(document.getElementById("formModal"));
}
function setupForms() {
document.getElementById("newTaskButton").addEventListener("click", () => configureFormModal("task"));
document.getElementById("newProjectButton").addEventListener("click", () => {
if (currentRoleName() === "专业负责人") openClosedLoopAction("project");
else configureFormModal("project");
});
document.getElementById("enterpriseDemandButton").addEventListener("click", () => {
if (currentRoleName() === "企业导师") openClosedLoopAction("demand");
else configureFormModal("demand");
});
document.getElementById("actionForm").addEventListener("submit", (event) => {
event.preventDefault();
const type = event.currentTarget.dataset.type;
const title = document.getElementById("formName").value.trim();
const scope = document.getElementById("formScope").value;
const classification = document.getElementById("formPriority").value;
const goal = document.getElementById("formGoal").value.trim();
const owner = document.getElementById("formOwner").value.trim();
const due = document.getElementById("formDue").value;
if (type === "task") {
const task = {
id: `TSK-CUSTOM-${Date.now()}`,
title,
source: `手动创建 · ${classification}`,
profession: scope,
owner: owner.split("·")[0].trim(),
due,
progress: 0,
status: "progress",
statusText: "进行中",
};
closedLoopState.custom.tasks.unshift(task);
addClosedLoopAudit("TASK_CREATED", task.id, `${title} · ${scope} · ${owner}`);
saveClosedLoopState();
syncClosedLoopCollections();
renderTasks();
switchView("actions");
} else if (type === "project") {
const project = {
id: `PRJ-CUSTOM-${Date.now()}`,
type: classification.includes("课程") ? "course" : "practice",
tone: "green",
title,
meta: `${scope} × 合作伙伴待确认`,
progress: 0,
partners: [owner.slice(0, 1), "企"],
};
closedLoopState.custom.projects.unshift(project);
addClosedLoopAudit("PROJECT_CREATED", project.id, `${title} · ${scope} · ${owner}`);
saveClosedLoopState();
syncClosedLoopCollections();
renderProjects();
switchView("collaboration");
} else if (type === "demand") {
const demand = {
id: `DEM-CUSTOM-${Date.now()}`,
logo: document.getElementById("roleSelect").value === "企业导师" ? "企" : "调",
company: document.getElementById("roleSelect").value === "企业导师" ? "企业人才发展部" : "校企需求调研",
industry: classification,
title,
description: goal,
due: due ? `${due} 截止` : "待确认",
status: "已提交",
};
closedLoopState.custom.demands.unshift(demand);
addClosedLoopAudit("DEMAND_CREATED", demand.id, `${title} · ${classification} · ${owner}`);
saveClosedLoopState();
syncClosedLoopCollections();
renderDemands();
switchView("collaboration");
}
closeModal(document.getElementById("formModal"));
const messages = {
task: "改进任务已创建,并通知专业负责人",
project: "合作项目草案已创建,可继续邀请企业伙伴",
demand: "企业需求调研已创建,共有126家在库企业可发送",
};
showToast(messages[type] || "内容已保存");
});
}
function setupFilters() {
document.getElementById("professionSearch").addEventListener("input", (event) => renderProfessionTable(event.target.value));
document.getElementById("professionSelect").addEventListener("change", (event) => renderProfessionProfile(event.target.value));
document.querySelectorAll("#periodSwitch button").forEach((button) => {
button.addEventListener("click", () => {
document.querySelectorAll("#periodSwitch button").forEach((item) => item.classList.remove("active"));
button.classList.add("active");
renderTrendChart(button.dataset.period);
});
});
document.querySelectorAll("#professionTabs button").forEach((button) => {
button.addEventListener("click", () => {
document.querySelectorAll("#professionTabs button").forEach((item) => item.classList.remove("active"));
button.classList.add("active");
professionTabState = button.dataset.tab;
const skillsView = document.getElementById("professionSkillsView");
const detailView = document.getElementById("professionDetailView");
const showSkills = professionTabState === "skills";
skillsView.hidden = !showSkills;
detailView.hidden = showSkills;
if (!showSkills) renderProfessionDetailView(professionTabState);
});
});
let taskFilter = "all";
const taskSearch = document.getElementById("taskSearch");
document.querySelectorAll("#taskFilters button").forEach((button) => {
button.addEventListener("click", () => {
taskFilter = button.dataset.filter;
document.querySelectorAll("#taskFilters button").forEach((item) => item.classList.remove("active"));
button.classList.add("active");
renderTasks(taskFilter, taskSearch.value);
});
});
taskSearch.addEventListener("input", () => renderTasks(taskFilter, taskSearch.value));
}
function validateClosedLoopSubmission(action, values) {
const e = closedLoopState.entities;
if (action === "receive") {
values.tenantId = values.tenantId.trim();
values.packageId = values.packageId.trim();
values.packageVersion = values.packageVersion.trim();
values.sha256 = values.sha256.trim().toLowerCase();
const checkedRecords = Number(values.checkedRecords);
const checks = [
[values.tenantId === e.receipt.tenantId, `租户标识必须为 ${e.receipt.tenantId}`],
[values.packageId === e.receipt.packageId, `数据包编号必须为 ${e.receipt.packageId}`],
[values.packageVersion === e.receipt.packageVersion, `数据包版本必须为 ${e.receipt.packageVersion}`],
[Number.isInteger(checkedRecords) && checkedRecords === e.receipt.expectedRecords, `交付包记录数必须为 ${e.receipt.expectedRecords.toLocaleString("zh-CN")} 条`],
[/^[a-f0-9]{64}$/.test(values.sha256), "SHA-256 必须是完整的64位十六进制摘要"],
[values.sha256 === e.receipt.expectedSha256, "SHA-256 与厂商发布 Manifest 的数据文件摘要不一致"],
];
const failure = checks.find(([matched]) => !matched);
if (failure) {
return {
valid: false,
title: "技术接收校验失败",
message: `${failure[1]};回执与分析快照状态均未前进。`,
auditAction: "PACKAGE_RECEIPT_VALIDATION_FAILED",
auditDetail: `${values.tenantId || "空租户"} · ${values.packageId || "空包号"} · ${values.packageVersion || "空版本"} · ${Number.isFinite(checkedRecords) ? checkedRecords : "无效记录数"} 条 · SHA ${values.sha256 || "空"}`,
};
}
values.checkedRecords = String(checkedRecords);
}
if (action === "evidence") {
const progress = Number(values.progress);
if (!Number.isFinite(progress) || progress < 0 || progress > 100) {
return { valid: false, title: "进度值无效", message: "阶段进度必须是0到100之间的数字。" };
}
}
if (action === "reportReview" && values.approver === e.report?.submittedBy) {
return {
valid: false,
title: "报告审批职责未分离",
message: "本次报告提交人不能批准或退回自己的提交,请选择另一位校级管理者。",
auditAction: "REPORT_REVIEW_BLOCKED_SELF_APPROVAL",
auditDetail: `${e.report.id} · 提交人 ${e.report.submittedBy} · 拟审批人 ${values.approver}`,
};
}
return { valid: true };
}
function setupClosedLoop() {
document.getElementById("view-loop").addEventListener("click", (event) => {
const actionButton = event.target.closest("[data-loop-action]");
if (actionButton) {
openClosedLoopAction(actionButton.dataset.loopAction);
return;
}
const nodeButton = event.target.closest("[data-loop-node]");
if (nodeButton) {
selectClosedLoopNode(nodeButton.dataset.loopNode, { focus: true });
document.getElementById("loopNodeDetail").scrollIntoView({ behavior: "smooth", block: "nearest" });
return;
}
const branchButton = event.target.closest("[data-loop-branch-target]");
if (branchButton) {
const selected = selectClosedLoopNode(branchButton.dataset.loopBranchTarget, { focus: true });
selected?.selectedButton?.scrollIntoView({ behavior: "smooth", block: "nearest" });
return;
}
const relationButton = event.target.closest("[data-loop-relation]");
if (relationButton) {
showToast(`${relationButton.dataset.loopRelation} 已在当前节点关联实体中定位。`, "实体关系");
return;
}
if (event.target.closest("#loopFocusCurrent")) {
const nodes = closedLoopNodeDefinitions();
const currentNode = closedLoopCurrentNode(nodes);
if (!currentNode) {
showToast("所有流程节点均已完成,当前没有待办节点。", "流程已关闭");
return;
}
const selected = selectClosedLoopNode(currentNode.key, { focus: true });
selected?.selectedButton?.scrollIntoView({
behavior: "smooth",
block: "nearest",
inline: "center",
});
}
});
document.getElementById("reportApprovalToolbar").addEventListener("click", (event) => {
const button = event.target.closest("[data-loop-action]");
if (button) openClosedLoopAction(button.dataset.loopAction);
});
document.getElementById("closedLoopForm").addEventListener("submit", (event) => {
event.preventDefault();
const action = event.currentTarget.dataset.action;
const values = Object.fromEntries(new FormData(event.currentTarget).entries());
const availability = actionPrerequisite(action);
if (!availability.enabled) {
showToast(availability.reason, "权限或前置条件已变化");
return;
}
const validation = validateClosedLoopSubmission(action, values);
if (!validation.valid) {
if (validation.auditAction) {
addClosedLoopAudit(validation.auditAction, action === "receive" ? closedLoopState.entities.receipt.id : closedLoopState.entities.report?.id || action, validation.auditDetail);
saveClosedLoopState();
renderClosedLoop();
}
showToast(validation.message, validation.title);
return;
}
closeModal(document.getElementById("closedLoopModal"));
applyClosedLoopAction(action, values);
const reader = document.getElementById("reportReader");
if (reader.classList.contains("open")) {
const report = reports.find((item) => item.id === reader.dataset.reportId);
if (report) renderReportApprovalToolbar(report);
document.body.style.overflow = "hidden";
}
});
document.getElementById("resetClosedLoop").addEventListener("click", () => {
if (!window.confirm("确认重置校园业务流程样板?本地持久化的流程实体、自定义业务记录和审计事件将恢复为种子状态。")) return;
localStorage.removeItem(CLOSED_LOOP_STORAGE_KEY);
closedLoopState = closedLoopSeed();
closedLoopUiState.selectedNodeKey = null;
closedLoopState.auditEvents.unshift({
id: `AUD-${Date.now()}`,
action: "SAMPLE_RESET",
objectId: CLOSED_LOOP_SHARED.tenantId,
operator: currentRoleUser(),
role: currentRoleName(),
at: nowText(),
detail: `按 schemaVersion ${CLOSED_LOOP_SCHEMA_VERSION} 重置样板。`,
});
saveAndRenderClosedLoop("校园业务流程样板已重置");
renderProfessionProfile(document.getElementById("professionSelect").value);
});
}
function init() {
if (closedLoopState.pendingNextCycleMigration) {
const legacyReview = closedLoopState.pendingNextCycleMigration;
delete closedLoopState.pendingNextCycleMigration;
startClosedLoopNextCycle(legacyReview, legacyReview.at || nowText());
addClosedLoopAudit(
"LEGACY_NEXT_CYCLE_MIGRATED",
closedLoopState.entities.demand.id,
`旧版复核 ${legacyReview.id || "RVW-旧记录"} 已转换为可继续办理的新周期记录。`,
"流程状态迁移器",
"系统",
);
}
syncClosedLoopCollections();
saveClosedLoopState();
renderSparkline("sparkMatch", [65, 68, 67, 72, 74, 78.6]);
renderSparkline("sparkJobs", [38, 42, 41, 50, 57, 64], "#4978a8");
renderSparkline("sparkGaps", [34, 31, 32, 29, 26, 23], "#d89527");
renderTrendChart();
renderProfessionTable();
renderIndustryList();
renderProfessionProfile("software");
renderProjects();
renderDemands();
renderPartners();
renderTasks();
renderSources();
renderDataReportLineage();
setupGenericInteractions();
setupRoleExperience();
setupPlatformActions();
setupAnalysis();
setupReportGeneration();
setupReportCenterInteractions();
setupForms();
setupFilters();
setupSourceFilters();
setupDataReportLineage();
setupDataOperations();
setupWorkspaceDetails();
setupProductExperience();
setupClosedLoop();
renderClosedLoop();
}
document.addEventListener("DOMContentLoaded", init);