feat: add compliant data connectors and lineage
- add authorized nine-source crawler connectors with security controls and packaged schemas - add the school internship contract to the vendor data standard center - add bidirectional campus data-to-report lineage with gap ownership and remediation Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
parent
92515928d9
commit
8dff9847e3
@@ -24,6 +24,8 @@ python3 -m http.server 8080
|
||||
- 改进任务筛选、搜索、创建、阶段进展和报告建议转任务
|
||||
- 数据源初次导入、增量更新、质量处置、版本管理、授权与审计
|
||||
- 厂商基础数据包、校级数据、企业数据分类及授权状态筛选
|
||||
- 覆盖 6 个数据源和 42 份报告的“数据 → 指标/分析主题 → 报告”双向依赖图谱,支持只看缺口
|
||||
- 报告缺失字段、当前完整率、影响范围、业务索取人、技术接口人和要求完成日期一体化追踪
|
||||
- 校级管理者、校级技术管理员、学院负责人、专业负责人、就业老师、企业导师、厂商超级管理员七种差异化工作台
|
||||
- 厂商平台级数据包导入、学校租户、账号权限、备份恢复和审计管理
|
||||
- 校级、学院、专业、产业专题与数据质量报告中心
|
||||
@@ -41,6 +43,25 @@ python3 -m http.server 8080
|
||||
- **校级数据**:来自学校实习、就业、教务等业务系统,数据权属归学校。
|
||||
- **企业数据**:由合作企业确认提交,按照合作协议限定使用范围。
|
||||
|
||||
## 数据与报告依赖图谱
|
||||
|
||||
“数据资产”页提供两种追溯方向:
|
||||
|
||||
- **从报告看数据**:选择任一校级、学院、专业、产业专题或数据质量报告,查看必需数据、可选增强数据、分析主题、当前完备度和缺口。
|
||||
- **从数据看报告**:选择任一数据源,查看可形成的分析主题、支撑的报告类型和具体报告数量。
|
||||
- **只看缺口**:仅保留存在质量或字段问题的依赖关系,快速定位不能稳定支撑报告的数据。
|
||||
|
||||
当前样板将 42 份报告映射到 6 个数据源,并以版本化依赖模板区分必需与可选关系。每个缺口展示:
|
||||
|
||||
- 缺失或待核验字段;
|
||||
- 当前记录影响量、完整率和处理状态;
|
||||
- 业务索取人及所属部门;
|
||||
- 技术接口人及所属技术部门;
|
||||
- 要求完成日期和受影响报告数;
|
||||
- 可直接进入现有质量处置抽屉的操作入口。
|
||||
|
||||
依赖元数据用于跨部门补数协同,可以在数据运维角色下查看;报告正文仍按现有角色与数据范围授权,查看依赖关系不自动获得报告阅读权限。静态样板中的依赖、完整率和责任状态均为演示数据,正式系统应将 `ReportDataDependency`、`AnalysisTopic` 和 `DataRequest` 作为租户隔离、版本化、可审计的后端实体。
|
||||
|
||||
## 角色视角
|
||||
|
||||
- **校级管理者**:全校决策总览、专业对比、校企协同、改进任务、数据资产与报告。
|
||||
@@ -67,6 +88,7 @@ python3 -m http.server 8080
|
||||
- 厂商超级管理员在“平台运营中心 → 数据包管理”完成基础数据包导入、Schema 与质量校验、租户授权和版本发布。
|
||||
- 校级技术管理员登录后默认进入“学校数据运维中心”,管理学校自有数据和企业授权数据。
|
||||
- 每个数据源的“资产详情”包含业务截止日期、入库日期、下次更新、更新内容、主键、必填字段、格式、Schema、质量阈值、责任人与审计事件。
|
||||
- “数据与报告双向依赖图谱”同时回答“这份报告还需要什么数据”和“这项数据可以形成哪些报告”,并把缺口直接分派到业务索取人与技术接口人。
|
||||
- “接收与使用反馈”展示租户、包号、版本、完整 SHA、交付包记录数、独立分析快照、支撑报告/项目/任务、业务可用确认和下一周期反馈。
|
||||
- 更新采用“暂存 → 校验 → 审批 → 发布”流程。失败数据进入隔离区,不覆盖当前稳定版本。
|
||||
|
||||
@@ -93,3 +115,7 @@ python3 -m http.server 8080
|
||||
|
||||
- `preview-campus-closed-loop.png`
|
||||
- `preview-campus-closed-loop-mobile.png`
|
||||
|
||||
数据依赖图谱预览:
|
||||
|
||||
- `preview-data-report-lineage.png`
|
||||
|
||||
+543
-38
@@ -231,6 +231,7 @@ const tasks = [
|
||||
|
||||
const sources = [
|
||||
{
|
||||
id: "regional-jobs",
|
||||
name: "区域招聘岗位基础包",
|
||||
type: "互联网岗位 · 标准数据包",
|
||||
category: "vendor",
|
||||
@@ -243,6 +244,7 @@ const sources = [
|
||||
status: "服务有效",
|
||||
records: "41,236",
|
||||
quality: "93.4%",
|
||||
analysisCompleteness: 98.8,
|
||||
updated: "2026-03-18",
|
||||
dataAsOf: "2026-03-17",
|
||||
nextUpdate: "2026-04-01",
|
||||
@@ -255,10 +257,13 @@ const sources = [
|
||||
threshold: "综合质量 ≥ 90;主键唯一率 100%",
|
||||
changeSummary: "新增 12,430 条,修改 846 条,失效 312 条;新增 AI 应用岗位分类",
|
||||
steward: "平台数据产品部",
|
||||
stewardContact: "林岚 · 数据产品经理",
|
||||
technicalOwner: "平台数据运维组",
|
||||
technicalContact: "平台数据包运维值班",
|
||||
action: "检查更新",
|
||||
},
|
||||
{
|
||||
id: "salary-company",
|
||||
name: "薪资与企业画像基础包",
|
||||
type: "互联网数据 · 增值数据包",
|
||||
category: "vendor",
|
||||
@@ -271,6 +276,7 @@ const sources = [
|
||||
status: "服务有效",
|
||||
records: "36,284",
|
||||
quality: "91.8%",
|
||||
analysisCompleteness: 100,
|
||||
updated: "2026-03-18",
|
||||
dataAsOf: "2026-03-17",
|
||||
nextUpdate: "2026-04-01",
|
||||
@@ -283,10 +289,13 @@ const sources = [
|
||||
threshold: "综合质量 ≥ 90;薪资合法率 ≥ 98%",
|
||||
changeSummary: "更新 2,186 条薪资样本,补充 326 家企业规模与融资阶段标签",
|
||||
steward: "平台数据产品部",
|
||||
stewardContact: "林岚 · 数据产品经理",
|
||||
technicalOwner: "平台数据运维组",
|
||||
technicalContact: "平台数据包运维值班",
|
||||
action: "检查更新",
|
||||
},
|
||||
{
|
||||
id: "internship",
|
||||
name: "实习管理平台",
|
||||
type: "校内系统 · API",
|
||||
category: "school",
|
||||
@@ -299,6 +308,7 @@ const sources = [
|
||||
status: "正常",
|
||||
records: "32,418",
|
||||
quality: "96.2%",
|
||||
analysisCompleteness: 100,
|
||||
updated: "今天 08:12",
|
||||
dataAsOf: "2026-03-21",
|
||||
nextUpdate: "明天 08:00",
|
||||
@@ -311,10 +321,13 @@ const sources = [
|
||||
threshold: "必填完整率 ≥ 95%;跨表关联率 ≥ 98%",
|
||||
changeSummary: "新增 186 条,修改 42 条;更新 31 家实习企业岗位信息",
|
||||
steward: "就业处",
|
||||
stewardContact: "赵晴 · 实习管理专员",
|
||||
technicalOwner: "学校信息中心",
|
||||
technicalContact: "王博 · 数据接口岗",
|
||||
action: "立即同步",
|
||||
},
|
||||
{
|
||||
id: "employment",
|
||||
name: "就业管理系统",
|
||||
type: "校内系统 · 数据库视图",
|
||||
category: "school",
|
||||
@@ -327,6 +340,7 @@ const sources = [
|
||||
status: "字段缺失",
|
||||
records: "8,926",
|
||||
quality: "87.6%",
|
||||
analysisCompleteness: 91.2,
|
||||
updated: "今天 07:50",
|
||||
dataAsOf: "2026-03-21",
|
||||
nextUpdate: "异常处理后重试",
|
||||
@@ -339,11 +353,14 @@ const sources = [
|
||||
threshold: "专业代码合法率 100%;必填完整率 ≥ 95%",
|
||||
changeSummary: "新增 126 条,修改 43 条;86 条薪资记录进入隔离区,未覆盖当前生产版本",
|
||||
steward: "就业处",
|
||||
stewardContact: "赵晴 · 就业数据专员",
|
||||
technicalOwner: "学校信息中心",
|
||||
technicalContact: "王博 · 数据接口岗",
|
||||
action: "处理异常",
|
||||
warning: true,
|
||||
},
|
||||
{
|
||||
id: "curriculum",
|
||||
name: "课程标准库",
|
||||
type: "校内文件 · 结构化导入",
|
||||
category: "school",
|
||||
@@ -356,6 +373,7 @@ const sources = [
|
||||
status: "正常",
|
||||
records: "1,428",
|
||||
quality: "95.1%",
|
||||
analysisCompleteness: 100,
|
||||
updated: "昨天 17:20",
|
||||
dataAsOf: "2026-03-20",
|
||||
nextUpdate: "2026-08-25",
|
||||
@@ -368,10 +386,13 @@ const sources = [
|
||||
threshold: "课程代码唯一率 100%;技能映射覆盖率 ≥ 80%",
|
||||
changeSummary: "导入 2026 春季课程快照;新增 38 门课程,修订 126 份课程标准",
|
||||
steward: "教务处",
|
||||
stewardContact: "陈璐 · 教务数据专员",
|
||||
technicalOwner: "学校信息中心",
|
||||
technicalContact: "王博 · 数据接口岗",
|
||||
action: "立即同步",
|
||||
},
|
||||
{
|
||||
id: "enterprise-demand",
|
||||
name: "企业确认需求库",
|
||||
type: "企业提交 · 问卷/工作台",
|
||||
category: "enterprise",
|
||||
@@ -384,6 +405,7 @@ const sources = [
|
||||
status: "授权有效",
|
||||
records: "8,350",
|
||||
quality: "97.3%",
|
||||
analysisCompleteness: 98.5,
|
||||
updated: "昨天 20:08",
|
||||
dataAsOf: "2026-03-21",
|
||||
nextUpdate: "企业提交后实时入队",
|
||||
@@ -396,7 +418,9 @@ const sources = [
|
||||
threshold: "企业身份与授权关联率 100%;必填完整率 ≥ 95%",
|
||||
changeSummary: "新增 18 家企业确认、54 项岗位需求;12 项过期需求自动停用",
|
||||
steward: "产教融合中心",
|
||||
stewardContact: "王健 · 企业合作专员",
|
||||
technicalOwner: "学校信息中心",
|
||||
technicalContact: "王博 · 数据接口岗",
|
||||
action: "查看回执",
|
||||
},
|
||||
];
|
||||
@@ -827,6 +851,7 @@ function syncClosedLoopCollections() {
|
||||
function saveAndRenderClosedLoop(message = "") {
|
||||
saveClosedLoopState();
|
||||
syncClosedLoopCollections();
|
||||
renderDataReportLineage();
|
||||
renderClosedLoop();
|
||||
renderTasks();
|
||||
renderProjects();
|
||||
@@ -1502,6 +1527,7 @@ const reports = [
|
||||
{
|
||||
id: "salary-2026-03",
|
||||
type: "industry",
|
||||
dataProfile: "salary",
|
||||
typeLabel: "产业专题",
|
||||
title: "2026届实习就业薪资竞争力分析",
|
||||
scope: "全校2026届学生",
|
||||
@@ -1824,6 +1850,150 @@ professionReportCatalog.forEach(([name, college, score, focus], index) => {
|
||||
});
|
||||
});
|
||||
|
||||
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: '<path d="M4 5h16v14H4V5Zm4 0V3h8v2M8 10h8m-8 4h5"/>',
|
||||
lab: '<path d="M9 3h6m-5 0v6l-5 9a2 2 0 0 0 2 3h10a2 2 0 0 0 2-3l-5-9V3M8 15h8"/>',
|
||||
@@ -1983,6 +2153,7 @@ function applyRole(role, options = {}) {
|
||||
]);
|
||||
}
|
||||
|
||||
renderDataReportLineage();
|
||||
renderClosedLoop();
|
||||
if (document.getElementById("professionSelect")) {
|
||||
renderProfessionProfile(document.getElementById("professionSelect").value);
|
||||
@@ -2522,6 +2693,359 @@ function renderTasks(filter = "all", search = "") {
|
||||
}
|
||||
}
|
||||
|
||||
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 `<div class="lineage-empty-node">${svgIcon("database")}<strong>${message}</strong><small>关闭“只看缺口”可查看完整依赖。</small></div>`;
|
||||
}
|
||||
|
||||
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 `
|
||||
<button class="lineage-node lineage-source-node ${hasGap ? "has-gap" : ""} ${selected ? "selected" : ""}" data-source-detail="${source.name}">
|
||||
<span class="lineage-node-top">
|
||||
<em class="${requirement}">${requirement === "required" ? "必需数据" : "可选增强"}</em>
|
||||
<i class="${hasGap ? "gap" : "ready"}">${hasGap ? gaps[0].levelLabel : "可用"}</i>
|
||||
</span>
|
||||
<strong>${source.name}</strong>
|
||||
<small>${dependency?.purpose || source.type}</small>
|
||||
<span class="lineage-node-meta">
|
||||
<span><b>${completeness.toFixed(1)}%</b> 完整</span>
|
||||
<span>${source.steward}</span>
|
||||
</span>
|
||||
${hasGap ? `<span class="lineage-node-gap">${gaps.map((gap) => gap.fields.slice(0, 2).join("、")).join(";")}</span>` : ""}
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderLineageTopicNode(topic, index) {
|
||||
return `
|
||||
<div class="lineage-topic-node">
|
||||
<span>${String(index + 1).padStart(2, "0")}</span>
|
||||
<strong>${topic}</strong>
|
||||
<small>统一口径计算 · 保留证据定位</small>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderLineageReportNode(report, readableIds = readableReportIdSet()) {
|
||||
const gaps = reportGaps(report);
|
||||
const readiness = reportReadiness(report);
|
||||
const canRead = readableIds.has(report.id);
|
||||
return `
|
||||
<button class="lineage-node lineage-report-node ${gaps.length ? "has-gap" : ""}" data-lineage-report="${report.id}">
|
||||
<span class="lineage-node-top">
|
||||
<em>${report.typeLabel}</em>
|
||||
<i class="${gaps.length ? "gap" : "ready"}">${gaps.length ? `${gaps.length}项缺口` : "数据齐备"}</i>
|
||||
</span>
|
||||
<strong>${report.title}</strong>
|
||||
<small>${report.scope}</small>
|
||||
<span class="lineage-node-meta">
|
||||
<span><b>${readiness.toFixed(1)}%</b> 完备</span>
|
||||
<span>${canRead ? report.version : "仅依赖元数据"}</span>
|
||||
</span>
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderLineageReportGroup(group, readableIds) {
|
||||
const affectedCount = group.reports.filter((report) => reportGaps(report).length).length;
|
||||
return `
|
||||
<details class="lineage-report-group" ${group.reports.length <= 5 ? "open" : ""}>
|
||||
<summary>
|
||||
<span><strong>${group.label}</strong><small>${affectedCount ? `${affectedCount} 份存在数据缺口` : "当前数据关系可用"}</small></span>
|
||||
<em>${group.reports.length} 份</em>
|
||||
</summary>
|
||||
<div class="lineage-report-group-list">
|
||||
${group.reports.map((report) => renderLineageReportNode(report, readableIds)).join("")}
|
||||
</div>
|
||||
</details>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderLineageStage(title, countText, className, content) {
|
||||
return `
|
||||
<section class="lineage-stage ${className}">
|
||||
<div class="lineage-stage-head"><span>${title}</span><strong>${countText}</strong></div>
|
||||
<div class="lineage-stage-list">${content}</div>
|
||||
</section>
|
||||
`;
|
||||
}
|
||||
|
||||
function lineageConnector(label) {
|
||||
return `
|
||||
<div class="lineage-connector" aria-hidden="true">
|
||||
<span>${label}</span>
|
||||
<i><b></b></i>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
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 `
|
||||
<div class="lineage-stage-grid report-direction">
|
||||
${renderLineageStage("数据资产", `${visibleDependencies.length} / ${profile.dependencies.length} 项`, "sources", sourceNodes)}
|
||||
${lineageConnector("形成指标")}
|
||||
${renderLineageStage("指标 / 分析主题", `${profile.topics.length} 类`, "topics", topicNodes)}
|
||||
${lineageConnector("生成报告")}
|
||||
${renderLineageStage("目标报告", "1 份", "reports", renderLineageReportNode(report, readableIds))}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
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 `
|
||||
<div class="lineage-stage-grid source-direction">
|
||||
${renderLineageStage("选中数据", "1 项", "sources", renderLineageSourceNode(source, null, true))}
|
||||
${lineageConnector("支撑分析")}
|
||||
${renderLineageStage("可形成的分析", `${topics.length} 类`, "topics", topics.map(renderLineageTopicNode).join(""))}
|
||||
${lineageConnector("支撑报告")}
|
||||
${renderLineageStage("可形成的报告", `${visibleReports.length} 份`, "reports", reportNodes)}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function lineageSummaryCard(label, value, note, className = "") {
|
||||
return `<article class="${className}"><span>${label}</span><strong>${value}</strong><small>${note}</small></article>`;
|
||||
}
|
||||
|
||||
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 `
|
||||
<article class="lineage-gap-card ${gap.level}">
|
||||
<div class="lineage-gap-card-head">
|
||||
<span><i></i>${gap.levelLabel}</span>
|
||||
<em>${gap.status}</em>
|
||||
</div>
|
||||
<strong>${gap.title}</strong>
|
||||
<small>${source.name} · ${gap.amount}</small>
|
||||
<div class="lineage-gap-fields">${gap.fields.map((field) => `<span>${field}</span>`).join("")}</div>
|
||||
<p>${gap.request}</p>
|
||||
<dl>
|
||||
<div><dt>业务索取人</dt><dd>${source.stewardContact}</dd></div>
|
||||
<div><dt>技术接口人</dt><dd>${source.technicalContact}</dd></div>
|
||||
<div><dt>要求完成</dt><dd>${gap.due}</dd></div>
|
||||
<div><dt>影响范围</dt><dd>${selectedReport ? "当前报告" : `${impactCount} 份报告`}</dd></div>
|
||||
</dl>
|
||||
<button data-lineage-gap-action="${gap.id}">进入补数与质量处置</button>
|
||||
</article>
|
||||
`;
|
||||
}
|
||||
|
||||
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("")
|
||||
: `
|
||||
<div class="lineage-gap-empty">
|
||||
${svgIcon("database")}
|
||||
<span><strong>当前关系没有待补数据</strong><small>数据版本、质量和授权均达到报告生成要求。</small></span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
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 `<option value="${item.id}">${item.typeLabel} · ${item.title}${canRead ? "" : " · 仅依赖元数据"}</option>`;
|
||||
})
|
||||
.join("")
|
||||
: sources
|
||||
.map((item) => `<option value="${item.id}">${item.categoryLabel} · ${item.name}</option>`)
|
||||
.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,
|
||||
@@ -2529,33 +3053,6 @@ const dataOpsState = {
|
||||
issue: null,
|
||||
};
|
||||
|
||||
const dataIssueCatalog = {
|
||||
"salary-missing": {
|
||||
title: "就业系统薪资字段缺失",
|
||||
level: "阻断发布",
|
||||
sourceName: "就业管理系统",
|
||||
affected: "86 条记录 · 电子商务专业 41 条、现代服务类专业 45 条",
|
||||
reason: "employment_salary 为空,未达到必填完整率 95% 的发布阈值。",
|
||||
action: "由就业处补齐薪资或填写缺失原因,修复文件按 student_id + cohort 回写后重新校验。",
|
||||
},
|
||||
"job-normalization": {
|
||||
title: "新增岗位名称待归一",
|
||||
level: "需要复核",
|
||||
sourceName: "区域招聘岗位基础包",
|
||||
affected: "218 个岗位别名 · 影响 1,426 条招聘记录",
|
||||
reason: "新增“具身智能应用工程师”等岗位未映射到当前岗位分类词典 V3.2。",
|
||||
action: "优先审核需求量前 50 的岗位名称,确认标准岗位、技能标签及别名规则。",
|
||||
},
|
||||
"enterprise-duplicate": {
|
||||
title: "疑似重复企业记录",
|
||||
level: "一般事项",
|
||||
sourceName: "企业确认需求库",
|
||||
affected: "12 家企业 · 涉及 37 项岗位需求",
|
||||
reason: "企业简称不同,但统一社会信用代码或工商地址高度相似。",
|
||||
action: "由产教融合中心确认主企业记录,合并需求与授权回执并保留审计映射。",
|
||||
},
|
||||
};
|
||||
|
||||
function currentDataSource() {
|
||||
return sources.find((source) => source.name === dataOpsState.sourceName) || sources[0];
|
||||
}
|
||||
@@ -2582,8 +3079,8 @@ function renderDataOpsOverview(source) {
|
||||
<h3>更新与责任</h3>
|
||||
<dl>
|
||||
<div><dt>数据类别</dt><dd>${source.categoryLabel} · ${source.commercial}</dd></div>
|
||||
<div><dt>业务责任人</dt><dd>${source.steward}</dd></div>
|
||||
<div><dt>技术责任人</dt><dd>${source.technicalOwner}</dd></div>
|
||||
<div><dt>业务责任人</dt><dd>${source.stewardContact} · ${source.steward}</dd></div>
|
||||
<div><dt>技术责任人</dt><dd>${source.technicalContact} · ${source.technicalOwner}</dd></div>
|
||||
<div><dt>更新模式</dt><dd>${source.updateMode}</dd></div>
|
||||
<div><dt>当前状态</dt><dd><span class="data-ops-state ${source.warning ? "warning" : ""}">${source.status}</span></dd></div>
|
||||
</dl>
|
||||
@@ -2674,7 +3171,7 @@ function renderDataOpsAudit(source) {
|
||||
return `
|
||||
<div class="data-authorization-card">
|
||||
<span>权属与授权</span><strong>${source.rights}</strong><p>${authorization}</p>
|
||||
<div><span>业务责任人<strong>${source.steward}</strong></span><span>技术责任人<strong>${source.technicalOwner}</strong></span><span>授权状态<strong>${source.status}</strong></span></div>
|
||||
<div><span>业务责任人<strong>${source.stewardContact} · ${source.steward}</strong></span><span>技术责任人<strong>${source.technicalContact} · ${source.technicalOwner}</strong></span><span>授权状态<strong>${source.status}</strong></span></div>
|
||||
</div>
|
||||
<article class="data-ops-card">
|
||||
<h3>最近审计事件</h3>
|
||||
@@ -2721,17 +3218,23 @@ function renderDataOpsUsage() {
|
||||
</article>`;
|
||||
}
|
||||
|
||||
function renderDataIssue(issueKey) {
|
||||
const issue = dataIssueCatalog[issueKey];
|
||||
function renderDataIssue(issueId) {
|
||||
const issue = dataGapById(issueId);
|
||||
const source = currentDataSource();
|
||||
const impactCount = reportsAffectedByGap(issue).length;
|
||||
return `
|
||||
<div class="data-issue-hero">
|
||||
<span>${issue.level}</span><h3>${issue.title}</h3><p>${issue.affected}</p>
|
||||
<span>${issue.drawerLevel}</span><h3>${issue.drawerTitle}</h3><p>${issue.affected}</p>
|
||||
</div>
|
||||
<article class="data-ops-card"><h3>规则命中原因</h3><p>${issue.reason}</p></article>
|
||||
<article class="data-ops-card"><h3>建议处置</h3><p>${issue.action}</p></article>
|
||||
<article class="data-ops-card">
|
||||
<h3>受影响分析</h3>
|
||||
<div class="data-impact-grid"><span>当前生产版本<strong>未覆盖</strong></span><span>分析快照<strong>沿用稳定版本</strong></span><span>责任部门<strong>${currentDataSource().steward}</strong></span></div>
|
||||
<h3>受影响分析与补数责任</h3>
|
||||
<div class="data-impact-grid"><span>当前生产版本<strong>未覆盖</strong></span><span>影响报告<strong>${impactCount} 份</strong></span><span>分析快照<strong>沿用稳定版本</strong></span></div>
|
||||
<dl class="data-issue-owner-list">
|
||||
<div><dt>业务索取人</dt><dd>${source.stewardContact} · ${source.steward}</dd></div>
|
||||
<div><dt>技术接口人</dt><dd>${source.technicalContact} · ${source.technicalOwner}</dd></div>
|
||||
</dl>
|
||||
</article>
|
||||
`;
|
||||
}
|
||||
@@ -2841,11 +3344,11 @@ function updateDataOpsDrawer() {
|
||||
}
|
||||
|
||||
if (dataOpsState.context === "issue") {
|
||||
const issue = dataIssueCatalog[dataOpsState.issue];
|
||||
const issueSource = sources.find((item) => item.name === issue.sourceName);
|
||||
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.title;
|
||||
document.getElementById("dataOpsTitle").textContent = issue.drawerTitle;
|
||||
document.getElementById("dataOpsDescription").textContent = "查看受影响数据、规则命中原因和可审计的处理流程。";
|
||||
content.innerHTML = renderDataIssue(dataOpsState.issue);
|
||||
footer.innerHTML = `<button class="button secondary" data-data-ops-result="错误明细文件已准备,可交由责任部门修复">下载错误明细</button><button class="button primary" data-data-ops-result="已创建质量整改任务,并通知责任部门">创建整改任务</button>`;
|
||||
@@ -4707,6 +5210,7 @@ function init() {
|
||||
renderPartners();
|
||||
renderTasks();
|
||||
renderSources();
|
||||
renderDataReportLineage();
|
||||
setupGenericInteractions();
|
||||
setupRoleExperience();
|
||||
setupPlatformActions();
|
||||
@@ -4716,6 +5220,7 @@ function init() {
|
||||
setupForms();
|
||||
setupFilters();
|
||||
setupSourceFilters();
|
||||
setupDataReportLineage();
|
||||
setupDataOperations();
|
||||
setupWorkspaceDetails();
|
||||
setupProductExperience();
|
||||
|
||||
+49
-1
@@ -870,6 +870,54 @@
|
||||
<button id="dataAuthorizationButton">查看授权说明</button>
|
||||
</div>
|
||||
|
||||
<article class="panel data-lineage-panel" id="dataReportLineage">
|
||||
<div class="panel-header data-lineage-header">
|
||||
<div>
|
||||
<span class="panel-kicker">DATA → ANALYSIS → REPORT</span>
|
||||
<h2>数据与报告双向依赖图谱</h2>
|
||||
<p>从报告反查必需数据,也可从数据查看能形成的报告;缺口直接定位业务索取人和技术接口人。</p>
|
||||
</div>
|
||||
<span class="lineage-snapshot-badge"><i></i>分析快照 V2026.03</span>
|
||||
</div>
|
||||
|
||||
<div class="data-lineage-toolbar">
|
||||
<div class="lineage-mode-switch" id="dataLineageModeSwitch" role="group" aria-label="选择图谱查看方向">
|
||||
<button class="active" data-lineage-mode="report" aria-pressed="true">从报告看数据</button>
|
||||
<button data-lineage-mode="source" aria-pressed="false">从数据看报告</button>
|
||||
</div>
|
||||
<label class="lineage-selector">
|
||||
<span id="dataLineageSelectorLabel">选择报告</span>
|
||||
<select id="dataLineageSelector" aria-label="选择要追溯的报告"></select>
|
||||
</label>
|
||||
<label class="lineage-gap-toggle">
|
||||
<input type="checkbox" id="dataLineageGapToggle">
|
||||
<span><i></i>只看缺口</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="lineage-summary-grid" id="dataLineageSummary" aria-live="polite"></div>
|
||||
|
||||
<div class="lineage-legend" aria-label="图谱图例">
|
||||
<span><i class="required"></i>必需数据</span>
|
||||
<span><i class="optional"></i>可选增强</span>
|
||||
<span><i class="gap"></i>存在缺口</span>
|
||||
<small>点击数据节点查看资产要求,点击报告节点进入在线报告。</small>
|
||||
</div>
|
||||
|
||||
<div class="data-lineage-canvas" id="dataLineageCanvas"></div>
|
||||
|
||||
<div class="lineage-gap-section">
|
||||
<div class="lineage-gap-heading">
|
||||
<div>
|
||||
<span>DATA REQUEST DESK</span>
|
||||
<h3>缺口与补数责任</h3>
|
||||
</div>
|
||||
<strong id="dataLineageGapCount">0 项待处理</strong>
|
||||
</div>
|
||||
<div class="lineage-gap-list" id="dataLineageGapList"></div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="panel source-panel">
|
||||
<div class="panel-header">
|
||||
<div>
|
||||
@@ -918,7 +966,7 @@
|
||||
<span class="panel-kicker">质量事项</span>
|
||||
<h2>待处理问题</h2>
|
||||
</div>
|
||||
<span class="panel-count amber-count">4 项</span>
|
||||
<span class="panel-count amber-count">3 项</span>
|
||||
</div>
|
||||
<div class="issue-list">
|
||||
<button data-data-issue="salary-missing">
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 330 KiB |
+924
@@ -3085,6 +3085,840 @@ svg {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.data-lineage-panel {
|
||||
margin-bottom: 14px;
|
||||
overflow: hidden;
|
||||
background:
|
||||
radial-gradient(circle at 98% 1%, rgba(198, 230, 111, 0.08), transparent 21%),
|
||||
white;
|
||||
}
|
||||
|
||||
.data-lineage-header {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.data-lineage-header h2 {
|
||||
margin-bottom: 7px;
|
||||
}
|
||||
|
||||
.data-lineage-header p {
|
||||
max-width: 720px;
|
||||
margin: 0;
|
||||
color: var(--muted);
|
||||
line-height: 1.65;
|
||||
}
|
||||
|
||||
.lineage-snapshot-badge {
|
||||
display: inline-flex;
|
||||
flex: 0 0 auto;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
padding: 8px 11px;
|
||||
color: var(--green);
|
||||
background: var(--green-soft);
|
||||
border: 1px solid #d2e8db;
|
||||
border-radius: 9px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.lineage-snapshot-badge i {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
background: var(--green);
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 0 4px rgba(35, 135, 101, 0.11);
|
||||
}
|
||||
|
||||
.data-lineage-toolbar {
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(280px, 1fr) auto;
|
||||
align-items: end;
|
||||
gap: 12px;
|
||||
margin-top: 18px;
|
||||
padding: 12px;
|
||||
background: #f4f7f4;
|
||||
border: 1px solid #e5ebe6;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.lineage-mode-switch {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
padding: 4px;
|
||||
background: #e8ede9;
|
||||
border-radius: 9px;
|
||||
}
|
||||
|
||||
.lineage-mode-switch button {
|
||||
min-height: 38px;
|
||||
padding: 0 13px;
|
||||
color: var(--muted);
|
||||
background: transparent;
|
||||
border-radius: 7px;
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.lineage-mode-switch button.active {
|
||||
color: white;
|
||||
background: var(--green);
|
||||
box-shadow: 0 4px 12px rgba(23, 107, 79, 0.18);
|
||||
}
|
||||
|
||||
.lineage-selector {
|
||||
display: grid;
|
||||
min-width: 0;
|
||||
gap: 5px;
|
||||
color: var(--muted);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.lineage-selector select {
|
||||
width: 100%;
|
||||
min-height: 38px;
|
||||
padding: 0 34px 0 11px;
|
||||
overflow: hidden;
|
||||
color: var(--ink);
|
||||
background: white;
|
||||
border: 1px solid #dfe6e1;
|
||||
border-radius: 8px;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.lineage-gap-toggle {
|
||||
display: flex;
|
||||
min-height: 38px;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.lineage-gap-toggle input {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
overflow: hidden;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.lineage-gap-toggle > span {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: var(--muted);
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.lineage-gap-toggle > span i {
|
||||
position: relative;
|
||||
width: 38px;
|
||||
height: 22px;
|
||||
background: #cbd4ce;
|
||||
border-radius: 12px;
|
||||
transition: background 0.18s ease;
|
||||
}
|
||||
|
||||
.lineage-gap-toggle > span i::after {
|
||||
position: absolute;
|
||||
top: 3px;
|
||||
left: 3px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background: white;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 2px 5px rgba(21, 54, 42, 0.2);
|
||||
content: "";
|
||||
transition: transform 0.18s ease;
|
||||
}
|
||||
|
||||
.lineage-gap-toggle input:checked + span {
|
||||
color: #9a6515;
|
||||
}
|
||||
|
||||
.lineage-gap-toggle input:checked + span i {
|
||||
background: var(--amber);
|
||||
}
|
||||
|
||||
.lineage-gap-toggle input:checked + span i::after {
|
||||
transform: translateX(16px);
|
||||
}
|
||||
|
||||
.lineage-gap-toggle input:focus-visible + span i {
|
||||
outline: 3px solid rgba(35, 135, 101, 0.2);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.lineage-summary-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 9px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.lineage-summary-grid article {
|
||||
min-width: 0;
|
||||
padding: 13px 14px;
|
||||
background: #fafbf9;
|
||||
border: 1px solid var(--line-soft);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.lineage-summary-grid article.warning {
|
||||
background: #fff9ed;
|
||||
border-color: #efdcae;
|
||||
}
|
||||
|
||||
.lineage-summary-grid article.ready {
|
||||
background: #f1f8f4;
|
||||
border-color: #d8e9de;
|
||||
}
|
||||
|
||||
.lineage-summary-grid span,
|
||||
.lineage-summary-grid strong,
|
||||
.lineage-summary-grid small {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.lineage-summary-grid span {
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.lineage-summary-grid strong {
|
||||
margin: 7px 0 4px;
|
||||
color: var(--ink);
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.lineage-summary-grid article.warning strong {
|
||||
color: #9a6515;
|
||||
}
|
||||
|
||||
.lineage-summary-grid article.ready strong {
|
||||
color: var(--green);
|
||||
}
|
||||
|
||||
.lineage-summary-grid small {
|
||||
color: var(--muted);
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.lineage-legend {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
margin: 18px 0 9px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.lineage-legend > span {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.lineage-legend i {
|
||||
width: 11px;
|
||||
height: 11px;
|
||||
border: 2px solid var(--green);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.lineage-legend i.optional {
|
||||
border-color: #6d88a1;
|
||||
border-style: dashed;
|
||||
}
|
||||
|
||||
.lineage-legend i.gap {
|
||||
background: var(--amber-soft);
|
||||
border-color: var(--amber);
|
||||
}
|
||||
|
||||
.lineage-legend small {
|
||||
margin-left: auto;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.data-lineage-canvas {
|
||||
padding: 15px;
|
||||
background:
|
||||
linear-gradient(rgba(34, 105, 79, 0.035) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(34, 105, 79, 0.035) 1px, transparent 1px),
|
||||
#f7f9f7;
|
||||
background-size: 24px 24px;
|
||||
border: 1px solid #e1e8e3;
|
||||
border-radius: 14px;
|
||||
}
|
||||
|
||||
.lineage-stage-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(250px, 1.08fr) 48px minmax(210px, 0.82fr) 48px minmax(250px, 1fr);
|
||||
align-items: start;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.lineage-stage {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
background: rgba(255, 255, 255, 0.94);
|
||||
border: 1px solid #dfe7e1;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 8px 24px rgba(25, 58, 45, 0.06);
|
||||
}
|
||||
|
||||
.lineage-stage-head {
|
||||
display: flex;
|
||||
min-height: 45px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
padding: 10px 12px;
|
||||
color: var(--green);
|
||||
background: #edf6f1;
|
||||
border-bottom: 1px solid #dce9e1;
|
||||
}
|
||||
|
||||
.lineage-stage.topics .lineage-stage-head {
|
||||
color: #376991;
|
||||
background: #eef4f9;
|
||||
border-color: #dae6ef;
|
||||
}
|
||||
|
||||
.lineage-stage.reports .lineage-stage-head {
|
||||
color: #8b621f;
|
||||
background: #fff8ea;
|
||||
border-color: #eee0bd;
|
||||
}
|
||||
|
||||
.lineage-stage-head span {
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.lineage-stage-head strong {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.lineage-stage-list {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
padding: 9px;
|
||||
}
|
||||
|
||||
.lineage-node {
|
||||
display: block;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
padding: 11px;
|
||||
color: var(--ink);
|
||||
background: white;
|
||||
border: 1px solid #dfe7e1;
|
||||
border-radius: 10px;
|
||||
text-align: left;
|
||||
transition:
|
||||
transform 0.18s ease,
|
||||
border-color 0.18s ease,
|
||||
box-shadow 0.18s ease;
|
||||
}
|
||||
|
||||
.lineage-node:hover,
|
||||
.lineage-node:focus-visible {
|
||||
transform: translateY(-1px);
|
||||
border-color: #8fc2a9;
|
||||
box-shadow: 0 8px 20px rgba(29, 83, 62, 0.11);
|
||||
}
|
||||
|
||||
.lineage-node.selected {
|
||||
background: #f3f9f6;
|
||||
border-color: #78b597;
|
||||
box-shadow: inset 3px 0 var(--green);
|
||||
}
|
||||
|
||||
.lineage-node.has-gap {
|
||||
background: #fffcf5;
|
||||
border-color: #e8d29f;
|
||||
}
|
||||
|
||||
.lineage-node-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.lineage-node-top em,
|
||||
.lineage-node-top i {
|
||||
padding: 4px 6px;
|
||||
border-radius: 6px;
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.lineage-node-top em {
|
||||
color: var(--green);
|
||||
background: var(--green-soft);
|
||||
}
|
||||
|
||||
.lineage-node-top em.optional {
|
||||
color: #4e718e;
|
||||
background: var(--blue-soft);
|
||||
}
|
||||
|
||||
.lineage-node-top i.ready {
|
||||
color: var(--green);
|
||||
background: #eff7f2;
|
||||
}
|
||||
|
||||
.lineage-node-top i.gap {
|
||||
color: #966214;
|
||||
background: var(--amber-soft);
|
||||
}
|
||||
|
||||
.lineage-node > strong,
|
||||
.lineage-node > small {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.lineage-node > strong {
|
||||
overflow: hidden;
|
||||
font-size: 14px;
|
||||
line-height: 1.45;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.lineage-node > small {
|
||||
min-height: 38px;
|
||||
margin-top: 5px;
|
||||
overflow: hidden;
|
||||
color: var(--muted);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.lineage-node-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
margin-top: 8px;
|
||||
padding-top: 8px;
|
||||
color: var(--muted);
|
||||
border-top: 1px solid #edf0ed;
|
||||
}
|
||||
|
||||
.lineage-node-meta b {
|
||||
color: var(--green);
|
||||
}
|
||||
|
||||
.lineage-node-gap {
|
||||
display: block;
|
||||
margin-top: 7px;
|
||||
padding: 6px 7px;
|
||||
overflow: hidden;
|
||||
color: #8c621b;
|
||||
background: #fff5db;
|
||||
border-radius: 6px;
|
||||
line-height: 1.45;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.lineage-report-node > strong {
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.lineage-report-group {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
background: white;
|
||||
border: 1px solid #e2e7e3;
|
||||
border-radius: 9px;
|
||||
}
|
||||
|
||||
.lineage-report-group > summary {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto 12px;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px;
|
||||
color: var(--ink);
|
||||
background: #fafbf9;
|
||||
cursor: pointer;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.lineage-report-group > summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.lineage-report-group > summary::after {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-right: 2px solid #819088;
|
||||
border-bottom: 2px solid #819088;
|
||||
content: "";
|
||||
transform: rotate(45deg) translateY(-2px);
|
||||
transition: transform 0.16s ease;
|
||||
}
|
||||
|
||||
.lineage-report-group[open] > summary::after {
|
||||
transform: rotate(225deg) translate(-2px, -1px);
|
||||
}
|
||||
|
||||
.lineage-report-group > summary span,
|
||||
.lineage-report-group > summary strong,
|
||||
.lineage-report-group > summary small {
|
||||
display: block;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.lineage-report-group > summary small {
|
||||
margin-top: 3px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.lineage-report-group > summary em {
|
||||
padding: 4px 6px;
|
||||
color: #805b16;
|
||||
background: var(--amber-soft);
|
||||
border-radius: 6px;
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.lineage-report-group-list {
|
||||
display: grid;
|
||||
max-height: 360px;
|
||||
gap: 7px;
|
||||
padding: 8px;
|
||||
overflow-y: auto;
|
||||
border-top: 1px solid #e8ede9;
|
||||
}
|
||||
|
||||
.lineage-topic-node {
|
||||
display: grid;
|
||||
grid-template-columns: 30px minmax(0, 1fr);
|
||||
gap: 3px 9px;
|
||||
padding: 10px;
|
||||
background: #f8fbfd;
|
||||
border: 1px solid #dfe8ef;
|
||||
border-radius: 9px;
|
||||
}
|
||||
|
||||
.lineage-topic-node > span {
|
||||
display: grid;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
grid-row: 1 / 3;
|
||||
place-items: center;
|
||||
color: #467594;
|
||||
background: #e5f0f7;
|
||||
border-radius: 7px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.lineage-topic-node strong {
|
||||
align-self: end;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.lineage-topic-node small {
|
||||
color: var(--muted);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.lineage-connector {
|
||||
display: grid;
|
||||
min-height: 120px;
|
||||
align-content: center;
|
||||
justify-items: center;
|
||||
gap: 9px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.lineage-connector span {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.lineage-connector i {
|
||||
position: relative;
|
||||
display: block;
|
||||
width: 38px;
|
||||
height: 2px;
|
||||
background: #9fb2a8;
|
||||
}
|
||||
|
||||
.lineage-connector i::after {
|
||||
position: absolute;
|
||||
top: -4px;
|
||||
right: -1px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-top: 2px solid #789286;
|
||||
border-right: 2px solid #789286;
|
||||
content: "";
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
.lineage-connector b {
|
||||
position: absolute;
|
||||
top: -3px;
|
||||
left: 4px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: var(--lime);
|
||||
border: 2px solid white;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 0 1px #9fb2a8;
|
||||
}
|
||||
|
||||
.lineage-empty-node {
|
||||
display: grid;
|
||||
min-height: 150px;
|
||||
place-items: center;
|
||||
align-content: center;
|
||||
gap: 7px;
|
||||
padding: 20px;
|
||||
color: var(--muted);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.lineage-empty-node svg {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
color: #91a49a;
|
||||
}
|
||||
|
||||
.lineage-empty-node strong,
|
||||
.lineage-empty-node small {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.lineage-gap-section {
|
||||
margin-top: 16px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid var(--line-soft);
|
||||
}
|
||||
|
||||
.lineage-gap-heading {
|
||||
display: flex;
|
||||
align-items: end;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.lineage-gap-heading span {
|
||||
color: #ad741d;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
|
||||
.lineage-gap-heading h3 {
|
||||
margin: 4px 0 0;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.lineage-gap-heading > strong {
|
||||
padding: 6px 9px;
|
||||
color: #956313;
|
||||
background: var(--amber-soft);
|
||||
border-radius: 7px;
|
||||
}
|
||||
|
||||
.lineage-gap-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 9px;
|
||||
}
|
||||
|
||||
.lineage-gap-card {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
flex-direction: column;
|
||||
padding: 13px;
|
||||
background: #fffcf5;
|
||||
border: 1px solid #ead8ad;
|
||||
border-radius: 11px;
|
||||
}
|
||||
|
||||
.lineage-gap-card.blocking {
|
||||
background: #fff8f4;
|
||||
border-color: #ebc5b5;
|
||||
}
|
||||
|
||||
.lineage-gap-card-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.lineage-gap-card-head span {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: #a06611;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.lineage-gap-card.blocking .lineage-gap-card-head span {
|
||||
color: #a34f31;
|
||||
}
|
||||
|
||||
.lineage-gap-card-head span i {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
background: var(--amber);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.lineage-gap-card.blocking .lineage-gap-card-head span i {
|
||||
background: #c96643;
|
||||
}
|
||||
|
||||
.lineage-gap-card-head em {
|
||||
color: var(--muted);
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
.lineage-gap-card > strong,
|
||||
.lineage-gap-card > small {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.lineage-gap-card > strong {
|
||||
margin-top: 10px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.lineage-gap-card > small {
|
||||
margin-top: 4px;
|
||||
color: var(--muted);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.lineage-gap-fields {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 5px;
|
||||
margin-top: 9px;
|
||||
}
|
||||
|
||||
.lineage-gap-fields span {
|
||||
padding: 4px 6px;
|
||||
color: #81591a;
|
||||
background: #fff1cb;
|
||||
border-radius: 5px;
|
||||
font-family: "SFMono-Regular", Consolas, monospace;
|
||||
}
|
||||
|
||||
.lineage-gap-card.blocking .lineage-gap-fields span {
|
||||
color: #944a30;
|
||||
background: #fee9df;
|
||||
}
|
||||
|
||||
.lineage-gap-card p {
|
||||
margin: 10px 0;
|
||||
color: var(--ink-soft);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.lineage-gap-card dl {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
margin: auto 0 10px;
|
||||
padding: 9px;
|
||||
background: rgba(255, 255, 255, 0.75);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.lineage-gap-card dl div {
|
||||
display: grid;
|
||||
grid-template-columns: 74px minmax(0, 1fr);
|
||||
gap: 7px;
|
||||
}
|
||||
|
||||
.lineage-gap-card dt {
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.lineage-gap-card dd {
|
||||
min-width: 0;
|
||||
margin: 0;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.lineage-gap-card > button {
|
||||
width: 100%;
|
||||
min-height: 36px;
|
||||
color: #8c5d12;
|
||||
background: white;
|
||||
border: 1px solid #e3cd9c;
|
||||
border-radius: 7px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.lineage-gap-card.blocking > button {
|
||||
color: #9a4d32;
|
||||
border-color: #e6b8a5;
|
||||
}
|
||||
|
||||
.lineage-gap-card > button:hover {
|
||||
background: #fff7e5;
|
||||
}
|
||||
|
||||
.lineage-gap-empty {
|
||||
display: flex;
|
||||
grid-column: 1 / -1;
|
||||
min-height: 90px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
padding: 18px;
|
||||
color: var(--muted);
|
||||
background: #f4f8f5;
|
||||
border: 1px dashed #cbded2;
|
||||
border-radius: 11px;
|
||||
}
|
||||
|
||||
.lineage-gap-empty svg {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
color: var(--green);
|
||||
}
|
||||
|
||||
.lineage-gap-empty strong,
|
||||
.lineage-gap-empty small {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.lineage-gap-empty small {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.data-issue-owner-list {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 8px;
|
||||
margin: 12px 0 0;
|
||||
}
|
||||
|
||||
.data-issue-owner-list div {
|
||||
padding: 10px;
|
||||
background: #f5f8f5;
|
||||
border: 1px solid var(--line-soft);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.data-issue-owner-list dt {
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.data-issue-owner-list dd {
|
||||
margin: 5px 0 0;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.source-filter-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -6397,6 +7231,30 @@ svg {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.lineage-stage-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.lineage-stage-list {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.report-direction .lineage-stage.reports .lineage-stage-list {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.lineage-connector {
|
||||
min-height: 36px;
|
||||
}
|
||||
|
||||
.lineage-connector i {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.lineage-gap-list {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.platform-metric-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
@@ -6487,6 +7345,24 @@ svg {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.data-lineage-toolbar {
|
||||
grid-template-columns: 1fr;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.lineage-mode-switch {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
|
||||
.lineage-summary-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.lineage-gap-list {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.tenant-card-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
@@ -6977,6 +7853,54 @@ svg {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.data-lineage-header,
|
||||
.lineage-gap-heading {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.lineage-snapshot-badge {
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.lineage-mode-switch button {
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.lineage-summary-grid,
|
||||
.lineage-stage-list {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.lineage-legend {
|
||||
align-items: flex-start;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px 12px;
|
||||
}
|
||||
|
||||
.lineage-legend small {
|
||||
width: 100%;
|
||||
margin-left: 0;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.data-lineage-canvas {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.lineage-gap-toggle {
|
||||
min-height: 34px;
|
||||
}
|
||||
|
||||
.lineage-gap-card dl div {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.data-issue-owner-list {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.source-license-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user