@topmindspace/tms-skills 0.3.9 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +79 -0
- package/README.en.md +11 -8
- package/README.md +11 -8
- package/bin/tms-skills.js +144 -12
- package/package.json +1 -1
- package/top-ppt-html/README.en.md +2 -2
- package/top-ppt-html/README.md +1 -1
- package/top-ppt-html/SKILL.md +8 -7
- package/top-ppt-html/assets/examples/2026-09-09-architecture-graphite-dark.html +16 -171
- package/top-ppt-html/assets/examples/2026-09-09-architecture-graphite-dark.model.json +13 -3
- package/top-ppt-html/assets/examples/2026-09-09-presentation-business-blue.html +16 -324
- package/top-ppt-html/assets/examples/2026-09-09-presentation-business-blue.model.json +15 -1
- package/top-ppt-html/assets/examples/2026-09-09-research-mckinsey.html +16 -917
- package/top-ppt-html/assets/examples/2026-09-09-research-mckinsey.model.json +15 -1
- package/top-ppt-html/assets/examples/2026-09-26-topmind-tms-skills-showcase.html +99 -18
- package/top-ppt-html/assets/icons/index.json +557 -0
- package/top-ppt-html/assets/icons//344/270/212/344/274/240.png +0 -0
- package/top-ppt-html/assets/icons//344/270/212/344/274/240.svg +1 -0
- package/top-ppt-html/assets/icons//344/270/213/350/275/275.png +0 -0
- package/top-ppt-html/assets/icons//344/270/213/350/275/275.svg +1 -0
- package/top-ppt-html/assets/icons//344/270/213/351/231/215.png +0 -0
- package/top-ppt-html/assets/icons//344/270/213/351/231/215.svg +1 -0
- package/top-ppt-html/assets/icons//344/271/246/347/255/276.png +0 -0
- package/top-ppt-html/assets/icons//344/271/246/347/255/276.svg +1 -0
- package/top-ppt-html/assets/icons//344/272/221.png +0 -0
- package/top-ppt-html/assets/icons//344/272/221.svg +1 -0
- package/top-ppt-html/assets/icons//344/275/215/347/275/256.png +0 -0
- package/top-ppt-html/assets/icons//344/275/215/347/275/256.svg +1 -0
- package/top-ppt-html/assets/icons//344/277/241/346/201/257.png +0 -0
- package/top-ppt-html/assets/icons//344/277/241/346/201/257.svg +1 -0
- package/top-ppt-html/assets/icons//345/210/206/344/272/253.png +0 -0
- package/top-ppt-html/assets/icons//345/210/206/344/272/253.svg +1 -0
- package/top-ppt-html/assets/icons//345/213/276/351/200/211.png +0 -0
- package/top-ppt-html/assets/icons//345/213/276/351/200/211.svg +1 -0
- package/top-ppt-html/assets/icons//345/215/240/346/257/224.png +0 -0
- package/top-ppt-html/assets/icons//345/215/240/346/257/224.svg +1 -0
- package/top-ppt-html/assets/icons//345/233/242/351/230/237.png +0 -0
- package/top-ppt-html/assets/icons//345/233/242/351/230/237.svg +1 -0
- package/top-ppt-html/assets/icons//345/233/276/350/241/250.png +0 -0
- package/top-ppt-html/assets/icons//345/233/276/350/241/250.svg +1 -0
- package/top-ppt-html/assets/icons//345/242/236/351/225/277.png +0 -0
- package/top-ppt-html/assets/icons//345/242/236/351/225/277.svg +1 -0
- package/top-ppt-html/assets/icons//345/256/211/345/205/250.png +0 -0
- package/top-ppt-html/assets/icons//345/256/211/345/205/250.svg +1 -0
- package/top-ppt-html/assets/icons//345/257/271/350/257/235.png +0 -0
- package/top-ppt-html/assets/icons//345/257/271/350/257/235.svg +1 -0
- package/top-ppt-html/assets/icons//345/267/245/345/205/267.png +0 -0
- package/top-ppt-html/assets/icons//345/267/245/345/205/267.svg +1 -0
- package/top-ppt-html/assets/icons//345/276/252/347/216/257.png +0 -0
- package/top-ppt-html/assets/icons//345/276/252/347/216/257.svg +1 -0
- package/top-ppt-html/assets/icons//346/210/220/346/236/234.png +0 -0
- package/top-ppt-html/assets/icons//346/210/220/346/236/234.svg +1 -0
- package/top-ppt-html/assets/icons//346/211/213/346/234/272.png +0 -0
- package/top-ppt-html/assets/icons//346/211/213/346/234/272.svg +1 -0
- package/top-ppt-html/assets/icons//346/225/210/347/216/207.png +0 -0
- package/top-ppt-html/assets/icons//346/225/210/347/216/207.svg +1 -0
- package/top-ppt-html/assets/icons//346/225/260/346/215/256.png +0 -0
- package/top-ppt-html/assets/icons//346/225/260/346/215/256.svg +1 -0
- package/top-ppt-html/assets/icons//346/226/207/344/273/266/345/244/271.png +0 -0
- package/top-ppt-html/assets/icons//346/226/207/344/273/266/345/244/271.svg +1 -0
- package/top-ppt-html/assets/icons//346/226/207/346/241/243.png +0 -0
- package/top-ppt-html/assets/icons//346/226/207/346/241/243.svg +1 -0
- package/top-ppt-html/assets/icons//346/227/227/345/270/234.png +0 -0
- package/top-ppt-html/assets/icons//346/227/227/345/270/234.svg +1 -0
- package/top-ppt-html/assets/icons//346/227/266/351/227/264.png +0 -0
- package/top-ppt-html/assets/icons//346/227/266/351/227/264.svg +1 -0
- package/top-ppt-html/assets/icons//346/230/237/346/240/207.png +0 -0
- package/top-ppt-html/assets/icons//346/230/237/346/240/207.svg +1 -0
- package/top-ppt-html/assets/icons//346/231/272/350/203/275.png +0 -0
- package/top-ppt-html/assets/icons//346/231/272/350/203/275.svg +1 -0
- package/top-ppt-html/assets/icons//346/235/203/351/231/220.png +0 -0
- package/top-ppt-html/assets/icons//346/235/203/351/231/220.svg +1 -0
- package/top-ppt-html/assets/icons//346/243/200/346/237/245.png +0 -0
- package/top-ppt-html/assets/icons//346/243/200/346/237/245.svg +1 -0
- package/top-ppt-html/assets/icons//346/264/236/345/257/237.png +0 -0
- package/top-ppt-html/assets/icons//346/264/236/345/257/237.svg +1 -0
- package/top-ppt-html/assets/icons//346/265/201/347/250/213.png +0 -0
- package/top-ppt-html/assets/icons//346/265/201/347/250/213.svg +1 -0
- package/top-ppt-html/assets/icons//346/270/205/345/215/225.png +0 -0
- package/top-ppt-html/assets/icons//346/270/205/345/215/225.svg +1 -0
- package/top-ppt-html/assets/icons//347/201/253/347/256/255.png +0 -0
- package/top-ppt-html/assets/icons//347/201/253/347/256/255.svg +1 -0
- package/top-ppt-html/assets/icons//347/201/257/346/263/241.png +0 -0
- package/top-ppt-html/assets/icons//347/201/257/346/263/241.svg +1 -0
- package/top-ppt-html/assets/icons//347/224/250/346/210/267.png +0 -0
- package/top-ppt-html/assets/icons//347/224/250/346/210/267.svg +1 -0
- package/top-ppt-html/assets/icons//347/233/256/346/240/207.png +0 -0
- package/top-ppt-html/assets/icons//347/233/256/346/240/207.svg +1 -0
- package/top-ppt-html/assets/icons//347/256/255/345/244/264.png +0 -0
- package/top-ppt-html/assets/icons//347/256/255/345/244/264.svg +1 -0
- package/top-ppt-html/assets/icons//347/273/204/347/273/207.png +0 -0
- package/top-ppt-html/assets/icons//347/273/204/347/273/207.svg +1 -0
- package/top-ppt-html/assets/icons//347/275/227/347/233/230.png +0 -0
- package/top-ppt-html/assets/icons//347/275/227/347/233/230.svg +1 -0
- package/top-ppt-html/assets/icons//350/241/250/346/240/274.png +0 -0
- package/top-ppt-html/assets/icons//350/241/250/346/240/274.svg +1 -0
- package/top-ppt-html/assets/icons//350/256/241/345/210/222.png +0 -0
- package/top-ppt-html/assets/icons//350/256/241/345/210/222.svg +1 -0
- package/top-ppt-html/assets/icons//350/256/276/347/275/256.png +0 -0
- package/top-ppt-html/assets/icons//350/256/276/347/275/256.svg +1 -0
- package/top-ppt-html/assets/icons//350/266/213/345/212/277.png +0 -0
- package/top-ppt-html/assets/icons//350/266/213/345/212/277.svg +1 -0
- package/top-ppt-html/assets/icons//351/202/256/344/273/266.png +0 -0
- package/top-ppt-html/assets/icons//351/202/256/344/273/266.svg +1 -0
- package/top-ppt-html/assets/icons//351/207/221/351/222/261.png +0 -0
- package/top-ppt-html/assets/icons//351/207/221/351/222/261.svg +1 -0
- package/top-ppt-html/assets/icons//351/223/276/346/216/245.png +0 -0
- package/top-ppt-html/assets/icons//351/223/276/346/216/245.svg +1 -0
- package/top-ppt-html/assets/icons//351/227/252/347/224/265.png +0 -0
- package/top-ppt-html/assets/icons//351/227/252/347/224/265.svg +1 -0
- package/top-ppt-html/assets/icons//351/243/216/351/231/251.png +0 -0
- package/top-ppt-html/assets/icons//351/243/216/351/231/251.svg +1 -0
- package/top-ppt-html/assets/pptx-export.js +2 -2
- package/top-ppt-html/assets/style-gallery.html +1 -1
- package/top-ppt-html/assets/templates/architecture.html +15 -3
- package/top-ppt-html/assets/templates/engine.css +12 -0
- package/top-ppt-html/assets/templates/presentation.html +15 -3
- package/top-ppt-html/assets/templates/research.html +15 -3
- package/top-ppt-html/package-lock.json +2 -2
- package/top-ppt-html/package.json +2 -2
- package/top-ppt-html/references/icons.md +22 -19
- package/top-ppt-html/references/layout-variants.md +69 -0
- package/top-ppt-html/references/style-pack.md +40 -0
- package/top-ppt-html/scripts/build_pptx.js +93 -26
- package/top-ppt-html/scripts/check_icons.py +119 -0
- package/top-ppt-html/scripts/hydrate_charts.py +60 -5
- package/top-ppt-html/scripts/icon_lib.js +66 -37
- package/top-ppt-html/scripts/layout-constants.json +91 -4
- package/top-ppt-html/scripts/make_icon_pack.py +172 -0
- package/top-ppt-html/scripts/model-schema.json +4 -1
- package/top-ppt-html/scripts/render_from_model.py +116 -55
- package/top-ppt-html/scripts/validate_pptx.py +1 -0
- package/top-ppt-html/scripts/validate_report.py +153 -0
- package/topmind-cover/README.en.md +4 -4
- package/topmind-cover/README.md +4 -4
- package/topmind-cover/SKILL.md +36 -18
- package/topmind-cover/assets/examples/README.md +13 -7
- package/topmind-cover/assets/examples/bg-blur-alt.png +0 -0
- package/topmind-cover/assets/examples/bg-blur-wechat.png +0 -0
- package/topmind-cover/assets/examples/bg-blur.png +0 -0
- package/topmind-cover/assets/examples/overview.png +0 -0
- package/topmind-cover/assets/examples/paper-collage-alt.png +0 -0
- package/topmind-cover/assets/examples/paper-collage-wechat.png +0 -0
- package/topmind-cover/assets/examples/paper-collage.png +0 -0
- package/topmind-cover/assets/examples/white-clean-alt-wechat.png +0 -0
- package/topmind-cover/assets/examples/white-clean-alt.png +0 -0
- package/topmind-cover/assets/examples/white-clean-wechat.png +0 -0
- package/topmind-cover/assets/examples/white-clean.png +0 -0
- package/topmind-cover/package.json +2 -2
- package/topmind-cover/references/cover-styles.md +146 -14
- package/topmind-cover/references/impact-language.md +9 -6
- package/topmind-cover/references/ref-white-clean-1.png +0 -0
- package/topmind-cover/references/ref-white-clean-2.png +0 -0
- package/topmind-wechat-post/README.en.md +1 -1
- package/topmind-wechat-post/README.md +1 -1
- package/topmind-wechat-post/scripts/md2wechat.py +130 -27
- package/topmind-wechat-post/scripts/negative_tests.py +74 -0
- package/topmind-x-article/README.en.md +1 -1
- package/topmind-x-article/README.md +1 -1
- package/topmind-x-article/scripts/md2x.py +18 -3
- package/topmind-x-article/scripts/negative_tests.py +17 -0
|
@@ -1408,7 +1408,35 @@
|
|
|
1408
1408
|
"preferContentFirst": true,
|
|
1409
1409
|
"warnBelow": 9.5
|
|
1410
1410
|
},
|
|
1411
|
-
"continuousTextRule": "语义连续的一句话 / so-what 主句 / 结论句 / 表格单元格正文不得拆成多个独立文本框(避免异常空格、断句、基线漂移);局部加粗高亮必须用同一文本框内富文本。"
|
|
1411
|
+
"continuousTextRule": "语义连续的一句话 / so-what 主句 / 结论句 / 表格单元格正文不得拆成多个独立文本框(避免异常空格、断句、基线漂移);局部加粗高亮必须用同一文本框内富文本。",
|
|
1412
|
+
"adaptiveText": {
|
|
1413
|
+
"$comment": "弹性文本单源(0.2.0):长标题/长列表/长表格单元格的降级规则。build_pptx.js 只读本表(AT),禁止在调用处散落 magic number。tier 语义:字符数按 CJK=1、拉丁/数字≈0.5 折算(与 estLines 同口径)。",
|
|
1414
|
+
"title": {
|
|
1415
|
+
"base": 30,
|
|
1416
|
+
"step1": 24,
|
|
1417
|
+
"longChars": 28,
|
|
1418
|
+
"longMax": 24,
|
|
1419
|
+
"xlongChars": 44,
|
|
1420
|
+
"xlongMax": 20
|
|
1421
|
+
},
|
|
1422
|
+
"list": {
|
|
1423
|
+
"denseItems": 6,
|
|
1424
|
+
"gapFactor": 0.5,
|
|
1425
|
+
"denseGapFactor": 0.25,
|
|
1426
|
+
"lineFactor": 1.45,
|
|
1427
|
+
"denseLineFactor": 1.3,
|
|
1428
|
+
"max": 14,
|
|
1429
|
+
"denseMax": 12
|
|
1430
|
+
},
|
|
1431
|
+
"table": {
|
|
1432
|
+
"cellMax": 12.5,
|
|
1433
|
+
"cellFloor": 9,
|
|
1434
|
+
"headMax": 13,
|
|
1435
|
+
"longCellChars": 24,
|
|
1436
|
+
"rowFloorIn": 0.22,
|
|
1437
|
+
"cellPadIn": 0.07
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1412
1440
|
},
|
|
1413
1441
|
"anchorTolerance": {
|
|
1414
1442
|
"$comment": "空间锚点容差(深度模式)。centerPct = 中心点偏差上限(占参考框局部宽高比例);keyPx = 关键锚点(图标/标签/箭头端点)偏差上限(px,按 1280×720 参考图计);exactPx = 用户要求 1:1 时的上限。",
|
|
@@ -1964,7 +1992,7 @@
|
|
|
1964
1992
|
}
|
|
1965
1993
|
},
|
|
1966
1994
|
"qualityGates": {
|
|
1967
|
-
"$comment": "v9+ 硬门禁。tagLeakPatterns / titleOnly / underfill / roundtripMin / chartSkew / chartOversize 同前。antiTruncation:截断迹象与溢出未拆页;chromeDriftIn:跨页 chrome y 容差(英寸)。",
|
|
1995
|
+
"$comment": "v9+ 硬门禁。tagLeakPatterns / titleOnly / underfill / roundtripMin / chartSkew / chartOversize 同前。antiTruncation:截断迹象与溢出未拆页;chromeDriftIn:跨页 chrome y 容差(英寸)。 contentCoverage:模型文本→渲染文本覆盖率下限(validate_report 只 WARN)。",
|
|
1968
1996
|
"tagLeakPatterns": [
|
|
1969
1997
|
"<a ",
|
|
1970
1998
|
"</a>",
|
|
@@ -2033,7 +2061,8 @@
|
|
|
2033
2061
|
"minCards": 3,
|
|
2034
2062
|
"shortPlainChars": 12,
|
|
2035
2063
|
"maxShortRatio": 0.5
|
|
2036
|
-
}
|
|
2064
|
+
},
|
|
2065
|
+
"contentCoverage": 0.95
|
|
2037
2066
|
},
|
|
2038
2067
|
"imageAdmission": {
|
|
2039
2068
|
"$comment": "图片资产准入门。forbidden = 禁止图片化的区域(主信息层与简单图表必须原生重建);allowed = 可保留为图片的复杂视觉资产;maxPageAreaPct = 单页图片覆盖面积上限(超过须逐项说明);fullSlideRiskPct = 按整页背景风险处理阈值(内容页默认失败);placeholderIsNative = 配图占位由原生形状渲染(不是图片,不触发 pictures 门禁)。",
|
|
@@ -3525,5 +3554,63 @@
|
|
|
3525
3554
|
]
|
|
3526
3555
|
}
|
|
3527
3556
|
}
|
|
3557
|
+
},
|
|
3558
|
+
"layoutVariants": {
|
|
3559
|
+
"$comment": "版式变体注册表(0.2.0 新增):复用既有页型/组件的组合版式,不新增 DSL 页型。variant 写在 section.variant;HTML 侧 render_from_model 按 base 页型渲染 + 变体类名;PPTX 侧 build_pptx 同名映射。新增变体:在此登记 + references/layout-variants.md 写骨架文档;禁止为变体另起渲染器。",
|
|
3560
|
+
"version": "0.1",
|
|
3561
|
+
"variants": {
|
|
3562
|
+
"bento-grid": {
|
|
3563
|
+
"name": "Bento 拼贴卡",
|
|
3564
|
+
"base": "cards",
|
|
3565
|
+
"html": ".grid.g-bento(复用 .card;首卡 .card--lead 跨 2 列)",
|
|
3566
|
+
"components": [
|
|
3567
|
+
"card",
|
|
3568
|
+
"card__ico",
|
|
3569
|
+
"pts_list"
|
|
3570
|
+
],
|
|
3571
|
+
"sectionAttr": "data-variant=\"bento-grid\"",
|
|
3572
|
+
"when": "4–6 张卡、其中 1 张是主结论/总览时;信息密度高于等权卡片",
|
|
3573
|
+
"budget": {
|
|
3574
|
+
"maxCards": 6,
|
|
3575
|
+
"recommendCards": "4–5",
|
|
3576
|
+
"leadMaxPoints": 4,
|
|
3577
|
+
"otherMaxPoints": 3
|
|
3578
|
+
},
|
|
3579
|
+
"pptx": "cards 分支:首卡跨 2 列,其余顺排(build_pptx.js variant 分支)"
|
|
3580
|
+
},
|
|
3581
|
+
"timeline": {
|
|
3582
|
+
"name": "图标时间线",
|
|
3583
|
+
"base": "timeline",
|
|
3584
|
+
"html": "复用 .tl;phase 字典加 icon 键 → 节点图标 .tl__ico[data-icon]",
|
|
3585
|
+
"components": [
|
|
3586
|
+
"tl",
|
|
3587
|
+
"icon:<name>"
|
|
3588
|
+
],
|
|
3589
|
+
"sectionAttr": "data-variant=\"timeline\"",
|
|
3590
|
+
"when": "阶段有明确类型语义(里程碑/风险/交付)且图标能加速扫读时;无语义用基础 timeline",
|
|
3591
|
+
"budget": {
|
|
3592
|
+
"maxPhases": 6,
|
|
3593
|
+
"iconPerPhase": 1,
|
|
3594
|
+
"descMaxChars": 60
|
|
3595
|
+
},
|
|
3596
|
+
"pptx": "timeline 分支:节点圆点旁贴 icon: 图标(build_pptx.js phaseKV 第 5 元)"
|
|
3597
|
+
},
|
|
3598
|
+
"2-col-feature": {
|
|
3599
|
+
"name": "双栏特性",
|
|
3600
|
+
"base": "twocol",
|
|
3601
|
+
"html": ".cols-2;左栏 .col--feature(复用 .card 语义:加粗标题 + accent 左线)",
|
|
3602
|
+
"components": [
|
|
3603
|
+
"cols-2",
|
|
3604
|
+
"card"
|
|
3605
|
+
],
|
|
3606
|
+
"sectionAttr": "data-variant=\"2-col-feature\"",
|
|
3607
|
+
"when": "左栏是核心主张/特性、右栏是支撑细节时;两栏对等论证用基础 twocol",
|
|
3608
|
+
"budget": {
|
|
3609
|
+
"maxParas": 6,
|
|
3610
|
+
"featureMaxParas": 2
|
|
3611
|
+
},
|
|
3612
|
+
"pptx": "twocol 分支:首栏加 accent 左线 + 标题放大(build_pptx.js variant 分支)"
|
|
3613
|
+
}
|
|
3614
|
+
}
|
|
3528
3615
|
}
|
|
3529
|
-
}
|
|
3616
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""TopPPT HTML · 内置图标包生成器(单源)
|
|
4
|
+
|
|
5
|
+
产出 assets/icons/:
|
|
6
|
+
- <name>.svg 24×24 手绘线性图标(stroke=currentColor, 1.8, 圆角),HTML 内联用
|
|
7
|
+
- <name>.png 512px 预渲染(cairosvg),PPTX 通道用(python-pptx 不认 SVG,见报告)
|
|
8
|
+
- index.json 关键词→图标映射 + 默认图标表
|
|
9
|
+
|
|
10
|
+
用法:
|
|
11
|
+
python scripts/make_icon_pack.py # 写 SVG + index.json
|
|
12
|
+
python scripts/make_icon_pack.py --png # 另渲染 512px PNG(需 cairosvg)
|
|
13
|
+
|
|
14
|
+
图标全部手绘原创(24 网格),不复制第三方图标库。
|
|
15
|
+
其中 19 个沿用既有 icon_lib.js 的成熟路径(字节一致,现有渲染不变);
|
|
16
|
+
流程 采用 references/icons.md 文档版(git-branch 式,与文档对齐)。
|
|
17
|
+
"""
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import json
|
|
21
|
+
import sys
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
ROOT = Path(__file__).resolve().parent.parent
|
|
25
|
+
OUT = ROOT / 'assets' / 'icons'
|
|
26
|
+
|
|
27
|
+
SVG_OPEN = ('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" '
|
|
28
|
+
'stroke="currentColor" stroke-width="1.8" stroke-linecap="round" '
|
|
29
|
+
'stroke-linejoin="round">')
|
|
30
|
+
|
|
31
|
+
# name -> (inner_svg, label, keywords)
|
|
32
|
+
ICONS: dict[str, tuple[str, str, list[str]]] = {
|
|
33
|
+
# ── 既有 19(与 icon_lib.js 现行字节一致) ──
|
|
34
|
+
'增长': ('<path d="M22 7l-8.5 8.5-5-5L2 17"/><path d="M16 7h6v6"/>',
|
|
35
|
+
'增长/上升', ['增长', '上升', '提升', '增加', 'up']),
|
|
36
|
+
'下降': ('<path d="M22 17l-8.5-8.5-5 5L2 7"/><path d="M16 17h6v-6"/>',
|
|
37
|
+
'下降/回落', ['下降', '回落', '减少', '降低', 'down']),
|
|
38
|
+
'数据': ('<ellipse cx="12" cy="5" rx="9" ry="3"/><path d="M3 5v14c0 1.66 4.03 3 9 3s9-1.34 9-3V5"/><path d="M3 12c0 1.66 4.03 3 9 3s9-1.34 9-3"/>',
|
|
39
|
+
'数据库/底座', ['数据', '底座', 'database']),
|
|
40
|
+
'图表': ('<path d="M3 3v18h18"/><rect x="7" y="12" width="3" height="6" rx="1"/><rect x="12" y="8" width="3" height="10" rx="1"/><rect x="17" y="4" width="3" height="14" rx="1"/>',
|
|
41
|
+
'柱状图', ['图表', '柱状', 'bar']),
|
|
42
|
+
'趋势': ('<path d="M3 3v18h18"/><path d="M7 14l4-4 3 3 5-6"/><path d="M15 7h4v4"/>',
|
|
43
|
+
'趋势/折线', ['趋势', '折线', 'line']),
|
|
44
|
+
'占比': ('<path d="M21.2 15.9A10 10 0 1 1 8 2.8"/><path d="M22 12A10 10 0 0 0 12 2v10z"/>',
|
|
45
|
+
'环形/占比', ['占比', '环形', '份额', 'donut']),
|
|
46
|
+
'表格': ('<rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 9h18M3 15h18M9 3v18M15 3v18"/>',
|
|
47
|
+
'表格/网格', ['表格', '矩阵', 'table']),
|
|
48
|
+
'效率': ('<path d="M12 15l3.5-5.5"/><path d="M20.2 15a8.5 8.5 0 1 0-16.4 0"/>',
|
|
49
|
+
'仪表盘/效率', ['效率', '仪表盘', '性能', 'gauge']),
|
|
50
|
+
'成果': ('<circle cx="12" cy="8" r="6"/><path d="M15.5 13 17 22l-5-3-5 3 1.5-9"/>',
|
|
51
|
+
'奖章/成果', ['成果', '奖章', '奖项', 'medal']),
|
|
52
|
+
'安全': ('<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/><path d="M9 12l2 2 4-4"/>',
|
|
53
|
+
'盾牌/安全', ['安全', '盾牌', 'shield']),
|
|
54
|
+
'权限': ('<rect x="3" y="11" width="18" height="10" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/>',
|
|
55
|
+
'锁/权限', ['权限', '锁', 'lock']),
|
|
56
|
+
'检查': ('<circle cx="12" cy="12" r="10"/><path d="M8 12.5l2.5 2.5L16 9.5"/>',
|
|
57
|
+
'检查/对勾', ['检查', '对勾', '校验', 'check']),
|
|
58
|
+
'风险': ('<path d="M10.3 3.9 1.8 18a2 2 0 0 0 1.7 3h17a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0z"/><path d="M12 9v4M12 17h.01"/>',
|
|
59
|
+
'警告/风险', ['风险', '警告', 'warning']),
|
|
60
|
+
'团队': ('<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/>',
|
|
61
|
+
'用户/团队', ['团队', '人力', 'team']),
|
|
62
|
+
'计划': ('<rect x="3" y="4" width="18" height="18" rx="2"/><path d="M16 2v4M8 2v4M3 10h18"/>',
|
|
63
|
+
'日历/计划', ['计划', '日历', '日程', 'calendar']),
|
|
64
|
+
'智能': ('<rect x="4" y="4" width="16" height="16" rx="2"/><path d="M9 9h6v6H9zM9 1v3M15 1v3M9 20v3M15 20v3M1 9h3M1 15h3M20 9h3M20 15h3"/>',
|
|
65
|
+
'芯片/AI', ['智能', '芯片', 'AI', 'chip']),
|
|
66
|
+
'洞察': ('<circle cx="11" cy="11" r="8"/><path d="M21 21l-4.35-4.35"/>',
|
|
67
|
+
'搜索/洞察', ['洞察', '搜索', '分析', 'search']),
|
|
68
|
+
'工具': ('<path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/>',
|
|
69
|
+
'扳手/工具', ['工具', '扳手', 'tool']),
|
|
70
|
+
'清单': ('<path d="M8 6h13M8 12h13M8 18h13M3 6h.01M3 12h.01M3 18h.01"/>',
|
|
71
|
+
'清单/条目', ['清单', '条目', 'checklist']),
|
|
72
|
+
# ── 流程:采用 references/icons.md 文档版(git-branch 式) ──
|
|
73
|
+
'流程': ('<circle cx="6" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><circle cx="18" cy="8" r="3"/><path d="M6 9v6M18 11a9 9 0 0 1-9 4"/>',
|
|
74
|
+
'分支/流程', ['流程', '分支', 'pipeline']),
|
|
75
|
+
# ── 新增 28(手绘原创) ──
|
|
76
|
+
'箭头': ('<path d="M4 12h15"/><path d="M13 6l6 6-6 6"/>',
|
|
77
|
+
'右箭头', ['箭头', '指向', 'arrow']),
|
|
78
|
+
'下载': ('<path d="M12 4v11"/><path d="M7 10.5l5 5 5-5"/><path d="M4 16v4h16v-4"/>',
|
|
79
|
+
'下载', ['下载', 'download']),
|
|
80
|
+
'上传': ('<path d="M12 20V9"/><path d="M7 13.5l5-5 5 5"/><path d="M4 16v4h16v-4"/>',
|
|
81
|
+
'上传', ['上传', 'upload']),
|
|
82
|
+
'分享': ('<circle cx="6" cy="12" r="2.5"/><circle cx="17.5" cy="5.5" r="2.5"/><circle cx="17.5" cy="18.5" r="2.5"/><path d="M8.3 10.9l6.9-4.2M8.3 13.1l6.9 4.2"/>',
|
|
83
|
+
'分享', ['分享', 'share']),
|
|
84
|
+
'循环': ('<path d="M20.5 15a8.5 8.5 0 1 1-2-8.9L21 8.5"/><path d="M21 3.5v5h-5"/>',
|
|
85
|
+
'循环/刷新', ['循环', '刷新', '迭代', '飞轮', 'refresh']),
|
|
86
|
+
'目标': ('<circle cx="12" cy="12" r="9"/><circle cx="12" cy="12" r="5"/><path d="M12 12h.01"/>',
|
|
87
|
+
'同心圆靶心', ['目标', 'target']),
|
|
88
|
+
'闪电': ('<path d="M13 2 3 14h7l-1 8 10-12h-7l1-8z"/>',
|
|
89
|
+
'闪电/高效', ['闪电', '高效', '快速', '快捷']),
|
|
90
|
+
'勾选': ('<path d="M4 12.5l5 5L20 6.5"/>',
|
|
91
|
+
'对勾', ['勾选', '完成', 'done']),
|
|
92
|
+
'用户': ('<circle cx="12" cy="8" r="4"/><path d="M4 21c0-3.5 3.6-5.5 8-5.5s8 2 8 5.5"/>',
|
|
93
|
+
'单人/用户', ['用户', '客户', 'user']),
|
|
94
|
+
'组织': ('<rect x="9" y="2" width="6" height="6" rx="1"/><rect x="2" y="16" width="6" height="6" rx="1"/><rect x="16" y="16" width="6" height="6" rx="1"/><path d="M12 8v4M5 12h14M5 12v4M19 12v4"/>',
|
|
95
|
+
'组织架构/层级', ['组织', '层级', '架构', 'org']),
|
|
96
|
+
'灯泡': ('<path d="M12 3a6 6 0 0 0-3.4 11c.7.5 1.4 1.3 1.4 2.5h4c0-1.2.7-2 1.4-2.5A6 6 0 0 0 12 3z"/><path d="M10 19.5h4"/><path d="M10.8 22h2.4"/>',
|
|
97
|
+
'灯泡/创意', ['灯泡', '创意', '想法', 'idea']),
|
|
98
|
+
'文档': ('<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8l-6-6z"/><path d="M14 2v6h6"/><path d="M9 13h6M9 17h6"/>',
|
|
99
|
+
'文档/报告', ['文档', '报告', '文件', 'doc']),
|
|
100
|
+
'云': ('<path d="M7 18.5a4 4 0 0 1-.4-7.98A5.5 5.5 0 0 1 17.3 11 3.9 3.9 0 0 1 17 18.5H7z"/>',
|
|
101
|
+
'云/平台', ['云', '平台', 'cloud']),
|
|
102
|
+
'邮件': ('<rect x="2" y="4" width="20" height="16" rx="2"/><path d="M2.5 7.5 12 13l9.5-5.5"/>',
|
|
103
|
+
'信封/邮件', ['邮件', '邮箱', 'mail']),
|
|
104
|
+
'手机': ('<rect x="7" y="2" width="10" height="20" rx="2.5"/><path d="M11 18.5h2"/>',
|
|
105
|
+
'手机', ['手机', '移动', 'phone']),
|
|
106
|
+
'对话': ('<rect x="3" y="4" width="18" height="12" rx="2"/><path d="M8 16v4l4.5-4"/>',
|
|
107
|
+
'对话框', ['对话', '沟通', 'chat']),
|
|
108
|
+
'位置': ('<path d="M12 21.5s-7-5.3-7-11a7 7 0 0 1 14 0c0 5.7-7 11-7 11z"/><circle cx="12" cy="10.5" r="2.5"/>',
|
|
109
|
+
'定位/地点', ['位置', '地点', 'pin']),
|
|
110
|
+
'时间': ('<circle cx="12" cy="12" r="9"/><path d="M12 7v5l3.5 2"/>',
|
|
111
|
+
'时钟/时间', ['时间', '时钟', 'clock']),
|
|
112
|
+
'金钱': ('<rect x="2" y="6" width="20" height="12" rx="2"/><circle cx="12" cy="12" r="2.8"/><path d="M5.5 12h.01M18.5 12h.01"/>',
|
|
113
|
+
'纸币/财务', ['金钱', '成本', '财务', 'money']),
|
|
114
|
+
'链接': ('<path d="M10 14a4.5 4.5 0 0 0 6.4.4l2.8-2.8a4.5 4.5 0 0 0-6.4-6.4l-1.6 1.6"/><path d="M14 10a4.5 4.5 0 0 0-6.4-.4l-2.8 2.8a4.5 4.5 0 0 0 6.4 6.4l1.6-1.6"/>',
|
|
115
|
+
'链条/链接', ['链接', 'link']),
|
|
116
|
+
'设置': ('<circle cx="12" cy="12" r="3.2"/><path d="M12 2.5v2.8M12 18.7v2.8M2.5 12h2.8M18.7 12h2.8M5.2 5.2l2 2M16.8 16.8l2 2M18.8 5.2l-2 2M7.2 16.8l-2 2"/>',
|
|
117
|
+
'齿轮/设置', ['设置', '齿轮', '配置', 'setting']),
|
|
118
|
+
'信息': ('<circle cx="12" cy="12" r="9"/><path d="M12 11v5"/><path d="M12 7.5h.01"/>',
|
|
119
|
+
'信息/提示', ['信息', '提示', 'info']),
|
|
120
|
+
'星标': ('<path d="m12 2.8 2.8 5.8 6.4.9-4.6 4.5 1.1 6.3-5.7-3-5.7 3 1.1-6.3L2.8 9.5l6.4-.9z"/>',
|
|
121
|
+
'五角星', ['星标', '收藏', 'star']),
|
|
122
|
+
'书签': ('<path d="M6.5 3h11V21l-5.5-3.8L6.5 21z"/>',
|
|
123
|
+
'书签', ['书签', 'bookmark']),
|
|
124
|
+
'文件夹': ('<path d="M3 7a2 2 0 0 1 2-2h4l2 2.5h8a2 2 0 0 1 2 2V18a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>',
|
|
125
|
+
'文件夹/资产', ['文件夹', '资产', 'folder']),
|
|
126
|
+
'火箭': ('<path d="M12 2.5c2.6 2.2 4 5.6 4 9.2l-4 2.8-4-2.8c0-3.6 1.4-7 4-9.2z"/><circle cx="12" cy="9" r="1.6"/><path d="M8.3 12.5 5 16.5l3.6-.6M15.7 12.5l3.3 4-3.6-.6"/><path d="M10.8 16.8c0 1.6.5 2.6 1.2 3.7.7-1.1 1.2-2.1 1.2-3.7"/>',
|
|
127
|
+
'火箭/发射', ['火箭', '发射', 'rocket']),
|
|
128
|
+
'旗帜': ('<path d="M5 21.5V4"/><path d="M5 4.5h12.5l-2.8 3.8 2.8 3.7H5"/>',
|
|
129
|
+
'旗帜/里程碑', ['旗帜', '里程碑', 'flag']),
|
|
130
|
+
'罗盘': ('<circle cx="12" cy="12" r="9"/><path d="m15.5 8.5-2.2 4.8-4.8 2.2 2.2-4.8z"/>',
|
|
131
|
+
'罗盘/方向', ['罗盘', '方向', '导航', 'compass']),
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
DEFAULTS = ['信息', '检查', '数据', '图表', '目标', '团队', '工具']
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def main() -> int:
|
|
138
|
+
OUT.mkdir(parents=True, exist_ok=True)
|
|
139
|
+
assert len(ICONS) == 48, f'图标数应为 48,实际 {len(ICONS)}'
|
|
140
|
+
assert all(d in ICONS for d in DEFAULTS), '默认图标必须在包内'
|
|
141
|
+
index = {'version': '0.2.0', 'count': len(ICONS),
|
|
142
|
+
'defaults': DEFAULTS, 'icons': {}}
|
|
143
|
+
for name, (inner, label, keywords) in ICONS.items():
|
|
144
|
+
svg = SVG_OPEN + inner + '</svg>'
|
|
145
|
+
(OUT / f'{name}.svg').write_text(svg, encoding='utf-8')
|
|
146
|
+
index['icons'][name] = {
|
|
147
|
+
'file': f'{name}.svg',
|
|
148
|
+
'png': f'{name}.png',
|
|
149
|
+
'label': label,
|
|
150
|
+
'keywords': keywords,
|
|
151
|
+
'default': name in DEFAULTS,
|
|
152
|
+
}
|
|
153
|
+
(OUT / 'index.json').write_text(
|
|
154
|
+
json.dumps(index, ensure_ascii=False, indent=2) + '\n', encoding='utf-8')
|
|
155
|
+
print(f'[make_icon_pack] OK: {len(ICONS)} svg + index.json → {OUT}')
|
|
156
|
+
if '--png' in sys.argv:
|
|
157
|
+
try:
|
|
158
|
+
import cairosvg
|
|
159
|
+
except ImportError:
|
|
160
|
+
print('[make_icon_pack] FAIL: 需 cairosvg(pip install cairosvg)才能渲染 PNG')
|
|
161
|
+
return 1
|
|
162
|
+
for name, (inner, _label, _kw) in ICONS.items():
|
|
163
|
+
svg = SVG_OPEN.replace('currentColor', '#1a56a8') + inner + '</svg>'
|
|
164
|
+
cairosvg.svg2png(bytestring=svg.encode('utf-8'),
|
|
165
|
+
write_to=str(OUT / f'{name}.png'),
|
|
166
|
+
output_width=512, output_height=512)
|
|
167
|
+
print(f'[make_icon_pack] OK: {len(ICONS)} png(512px) → {OUT}')
|
|
168
|
+
return 0
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
if __name__ == '__main__':
|
|
172
|
+
raise SystemExit(main())
|
|
@@ -39,10 +39,13 @@
|
|
|
39
39
|
"lead",
|
|
40
40
|
"soWhat",
|
|
41
41
|
"footnote",
|
|
42
|
+
"note",
|
|
42
43
|
"flags",
|
|
43
44
|
"image",
|
|
44
45
|
"exhibitNo",
|
|
45
|
-
"layoutPreset"
|
|
46
|
+
"layoutPreset",
|
|
47
|
+
"variant",
|
|
48
|
+
"anchor"
|
|
46
49
|
],
|
|
47
50
|
"mutualExclusionHint": "verdict 与 soWhat 互斥:二者共用 annotation 结论条槽位,同页双填必叠印。comparison 页填 verdict 时不要再填 soWhat(渲染器已自动二选一,但模型侧应只给一个)。",
|
|
48
51
|
"flagsHint": "flags = 待核实/待二次修改条目字符串数组(如 '2027 增速 xx%(口径未定)');渲染为 accent 强调色标注行,提示用户核对。",
|
|
@@ -95,9 +95,18 @@ def kp(k, v) -> str:
|
|
|
95
95
|
return f'<li><strong>{k}。</strong>{v}</li>' if k else f'<li>{v}</li>'
|
|
96
96
|
|
|
97
97
|
|
|
98
|
-
def
|
|
98
|
+
def sec_anchor(i: int, sec: dict) -> str:
|
|
99
|
+
"""页锚点 id:模型 anchor 优先,否则 s{i}(agenda 链接与 open_sec 同源,禁各写一份)。"""
|
|
100
|
+
a = str((sec or {}).get('anchor') or '').strip()
|
|
101
|
+
if re.fullmatch(r'[A-Za-z][A-Za-z0-9_-]*', a):
|
|
102
|
+
return a
|
|
103
|
+
return f's{i}'
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def open_sec(i: int, ptype: str, extra: str = '', sec: dict = None) -> str:
|
|
99
107
|
skel = PAGE_TO_PRESET.get(ptype, 'P4')
|
|
100
|
-
|
|
108
|
+
sid = sec_anchor(i, sec)
|
|
109
|
+
return (f'<section class="band" id="{sid}" data-skel="{skel}" '
|
|
101
110
|
f'data-page-type="{ptype}"{extra}>')
|
|
102
111
|
|
|
103
112
|
|
|
@@ -118,10 +127,17 @@ def sowhat(sec: dict) -> str:
|
|
|
118
127
|
|
|
119
128
|
|
|
120
129
|
def footnote(sec: dict) -> str:
|
|
130
|
+
# footnote 与 note 共用来源行(与 build_pptx 注释带同口径:两者都不丢)
|
|
121
131
|
t = sec.get('footnote')
|
|
122
|
-
|
|
132
|
+
n = sec.get('note')
|
|
133
|
+
parts = []
|
|
134
|
+
if t:
|
|
135
|
+
parts.append(cite(t))
|
|
136
|
+
if n and n != t:
|
|
137
|
+
parts.append(f'注:{cite(n)}')
|
|
138
|
+
if not parts:
|
|
123
139
|
return ''
|
|
124
|
-
return f' <div class="footnote">{
|
|
140
|
+
return f' <div class="footnote">{" ".join(parts)}</div>\n'
|
|
125
141
|
|
|
126
142
|
|
|
127
143
|
def flags_block(sec: dict) -> str:
|
|
@@ -134,55 +150,49 @@ def flags_block(sec: dict) -> str:
|
|
|
134
150
|
|
|
135
151
|
|
|
136
152
|
def wrap(i: int, ptype: str, body: str, sec: dict, extra: str = '') -> str:
|
|
137
|
-
return (f'{open_sec(i, ptype, extra)}\n <div class="wrap">\n'
|
|
153
|
+
return (f'{open_sec(i, ptype, extra, sec)}\n <div class="wrap">\n'
|
|
138
154
|
f'{shead(sec)}\n{body}{sowhat(sec)}{footnote(sec)}{flags_block(sec)}'
|
|
139
155
|
f' </div>\n</section>')
|
|
140
156
|
|
|
141
157
|
|
|
142
|
-
# ──
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
'智能': _SVG_OPEN + '<rect x="4" y="4" width="16" height="16" rx="2"/><path d="M9 9h6v6H9zM9 1v3M15 1v3M9 20v3M15 20v3M1 9h3M1 15h3M20 9h3M20 15h3"/></svg>',
|
|
163
|
-
'洞察': _SVG_OPEN + '<circle cx="11" cy="11" r="8"/><path d="M21 21l-4.35-4.35"/></svg>',
|
|
164
|
-
'工具': _SVG_OPEN + '<path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/></svg>',
|
|
165
|
-
'清单': _SVG_OPEN + '<path d="M8 6h13M8 12h13M8 18h13M3 6h.01M3 12h.01M3 18h.01"/></svg>',
|
|
166
|
-
}
|
|
158
|
+
# ── 语义图标库(单源:assets/icons/ + index.json;与 icon_lib.js / PPTX 真导出同源)──
|
|
159
|
+
_ICON_DIR = Path(__file__).resolve().parent.parent / 'assets' / 'icons'
|
|
160
|
+
_ICON_INDEX: dict = {}
|
|
161
|
+
try:
|
|
162
|
+
_ICON_INDEX = json.loads((_ICON_DIR / 'index.json').read_text(encoding='utf-8'))
|
|
163
|
+
except OSError:
|
|
164
|
+
_ICON_INDEX = {}
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _load_icons() -> 'dict[str, str]':
|
|
168
|
+
out: dict[str, str] = {}
|
|
169
|
+
for name in (_ICON_INDEX.get('icons') or {}):
|
|
170
|
+
try:
|
|
171
|
+
out[name] = (_ICON_DIR / f'{name}.svg').read_text(encoding='utf-8').strip()
|
|
172
|
+
except OSError:
|
|
173
|
+
continue
|
|
174
|
+
return out
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
ICONS: dict[str, str] = _load_icons()
|
|
167
178
|
_ICONS_ORDER = list(ICONS.keys())
|
|
179
|
+
_ICON_DEFAULTS = [n for n in (_ICON_INDEX.get('defaults') or []) if n in ICONS] or _ICONS_ORDER
|
|
168
180
|
|
|
169
181
|
|
|
170
182
|
def pick_icon(title: str = '', idx: int = 0) -> str:
|
|
171
|
-
"""
|
|
172
|
-
|
|
173
|
-
for key, svg in ICONS.items():
|
|
174
|
-
if key in t:
|
|
175
|
-
return svg
|
|
176
|
-
return ICONS[_ICONS_ORDER[idx % len(_ICONS_ORDER)]]
|
|
183
|
+
"""按标题语义挑图标 SVG;未命中则在默认表内轮换(选不出用默认,不盲目轮换全量)。"""
|
|
184
|
+
return ICONS[pick_icon_name(title, idx)]
|
|
177
185
|
|
|
178
186
|
|
|
179
187
|
def pick_icon_name(title: str = '', idx: int = 0) -> str:
|
|
180
|
-
"""图标名(与 icon_lib.js / PPTX
|
|
188
|
+
"""图标名(与 icon_lib.js / PPTX 真导出同名;关键词来自 index.json)。"""
|
|
181
189
|
t = str(title or '')
|
|
182
|
-
|
|
183
|
-
|
|
190
|
+
meta = _ICON_INDEX.get('icons') or {}
|
|
191
|
+
for key in _ICONS_ORDER:
|
|
192
|
+
kws = (meta.get(key) or {}).get('keywords') or [key]
|
|
193
|
+
if any(kw and kw in t for kw in kws):
|
|
184
194
|
return key
|
|
185
|
-
return
|
|
195
|
+
return _ICON_DEFAULTS[idx % len(_ICON_DEFAULTS)]
|
|
186
196
|
|
|
187
197
|
|
|
188
198
|
def card_head(title: str, idx: int = 0, icon_name: str | None = None) -> str:
|
|
@@ -253,16 +263,23 @@ def r_metrics(i, sec):
|
|
|
253
263
|
|
|
254
264
|
|
|
255
265
|
def r_kpi(i, sec):
|
|
266
|
+
# 大数指标页:hero 大数字 + 支撑指标行(metrics,可选)+ 要点;三者缺一不丢其余
|
|
256
267
|
hero = sec.get('hero') or []
|
|
257
268
|
val = hero[0] if hero else ''
|
|
258
269
|
lab = hero[1] if len(hero) > 1 else ''
|
|
259
270
|
delta = hero[2] if len(hero) > 2 else ''
|
|
271
|
+
left = ''
|
|
272
|
+
if val or lab:
|
|
273
|
+
left = (
|
|
274
|
+
' <div class="stack gap-3"><div class="t-metric" style="color:var(--accent)">'
|
|
275
|
+
f'{esc(val)}</div>'
|
|
276
|
+
f'<div class="t-h3">{esc(lab)}</div>'
|
|
277
|
+
+ (f'<div class="t-sm" style="color:var(--accent);font-weight:600">{esc(delta)}</div>' if delta else '')
|
|
278
|
+
+ '</div>\n')
|
|
279
|
+
mets = metrics_row(sec.get('metrics')) if sec.get('metrics') else ''
|
|
260
280
|
body = (
|
|
261
281
|
' <div class="grid g-hero rv a-c">\n'
|
|
262
|
-
|
|
263
|
-
f'<div class="t-h3">{esc(lab)}</div>'
|
|
264
|
-
+ (f'<div class="t-sm" style="color:var(--accent);font-weight:600">{esc(delta)}</div>' if delta else '')
|
|
265
|
-
+ '</div>\n'
|
|
282
|
+
+ left + mets
|
|
266
283
|
+ ' <div class="grid g-2">\n'
|
|
267
284
|
+ pts_list(sec.get('points') or [['支撑', '一句话。']])
|
|
268
285
|
+ '\n </div>\n </div>\n')
|
|
@@ -339,14 +356,20 @@ def r_exhibit(i, sec):
|
|
|
339
356
|
|
|
340
357
|
def r_twocol(i, sec):
|
|
341
358
|
paras = sec.get('paragraphs') or []
|
|
359
|
+
variant = sec.get('variant') or ''
|
|
342
360
|
cols = []
|
|
343
|
-
for p in paras:
|
|
361
|
+
for pi, p in enumerate(paras):
|
|
344
362
|
if isinstance(p, (list, tuple)) and len(p) >= 2:
|
|
345
363
|
cols.append(f' <p class="t-body"><strong>{esc(p[0])}。</strong>{cite(p[1])}</p>')
|
|
346
364
|
else:
|
|
347
365
|
cols.append(f' <p class="t-body">{cite(p)}</p>')
|
|
366
|
+
# 变体 2-col-feature:首栏为特性栏(复用 .card 语义:加粗标题 + accent 左线)
|
|
367
|
+
extra = ''
|
|
368
|
+
if variant == '2-col-feature' and cols:
|
|
369
|
+
cols[0] = cols[0].replace('class="t-body"', 'class="t-body col--feature"', 1)
|
|
370
|
+
extra = ' data-variant="2-col-feature"'
|
|
348
371
|
body = ' <div class="cols-2 rv">\n' + '\n'.join(cols) + '\n </div>\n'
|
|
349
|
-
return wrap(i, 'twocol', body, sec)
|
|
372
|
+
return wrap(i, 'twocol', body, sec, extra)
|
|
350
373
|
|
|
351
374
|
|
|
352
375
|
def r_threecol(i, sec):
|
|
@@ -365,11 +388,16 @@ def r_threecol(i, sec):
|
|
|
365
388
|
|
|
366
389
|
def r_cards(i, sec):
|
|
367
390
|
cards = sec.get('cards') or []
|
|
391
|
+
variant = sec.get('variant') or ''
|
|
368
392
|
n = int(sec.get('columns') or min(3, max(1, len(cards))))
|
|
369
393
|
cls = {1: 'g-2', 2: 'g-2', 3: 'g-3', 4: 'g-4'}.get(n, 'g-3')
|
|
370
394
|
# 纯卡片栅格强制等高(g-2--equal / g-3--equal),与 CSS / layout-grammar「同行卡片 stretch」一致
|
|
371
395
|
if cls in ('g-2', 'g-3', 'g-4'):
|
|
372
396
|
cls = cls + '--equal'
|
|
397
|
+
extra = ''
|
|
398
|
+
if variant == 'bento-grid':
|
|
399
|
+
# 变体 bento-grid:首卡跨 2 列(.card--lead),复用 .card 组件
|
|
400
|
+
cls, extra = 'g-bento', ' data-variant="bento-grid"'
|
|
373
401
|
blocks = []
|
|
374
402
|
for ci, cd in enumerate(cards):
|
|
375
403
|
if isinstance(cd, dict):
|
|
@@ -380,11 +408,12 @@ def r_cards(i, sec):
|
|
|
380
408
|
title, points, icon_name = cd[0], [['', cd[1]]], None
|
|
381
409
|
else:
|
|
382
410
|
title, points, icon_name = (cd or ''), [], None
|
|
411
|
+
lead_cls = ' card--lead' if (variant == 'bento-grid' and ci == 0) else ''
|
|
383
412
|
blocks.append(
|
|
384
|
-
f' <div class="card">{card_head(str(title), ci, icon_name)}\n'
|
|
413
|
+
f' <div class="card{lead_cls}">{card_head(str(title), ci, icon_name)}\n'
|
|
385
414
|
f' {pts_list(points)}</div>')
|
|
386
415
|
body = f' <div class="grid {cls} rv">\n' + '\n'.join(blocks) + '\n </div>\n'
|
|
387
|
-
return wrap(i, 'cards', body, sec)
|
|
416
|
+
return wrap(i, 'cards', body, sec, extra)
|
|
388
417
|
|
|
389
418
|
|
|
390
419
|
def r_split(i, sec):
|
|
@@ -519,6 +548,9 @@ def r_image(i, sec):
|
|
|
519
548
|
else:
|
|
520
549
|
grid_cls = f'media-grid media-grid--{min(4, max(2, n))}'
|
|
521
550
|
media = f' <div class="{grid_cls} rv">\n' + '\n'.join(cells) + '\n </div>\n'
|
|
551
|
+
# 组 caption(图编号):多图版式同样渲染,与 PPTX img.caption 同口径(此前仅单图分支渲染)
|
|
552
|
+
if cap:
|
|
553
|
+
media += f' <p class="media__src">{cite(cap)}</p>\n'
|
|
522
554
|
else:
|
|
523
555
|
src = im.get('src') or ''
|
|
524
556
|
ph = bool(im.get('placeholder')) or not src
|
|
@@ -589,23 +621,32 @@ def r_lane(i, sec):
|
|
|
589
621
|
|
|
590
622
|
def r_timeline(i, sec):
|
|
591
623
|
items = []
|
|
624
|
+
variant = sec.get('variant') or ''
|
|
592
625
|
for ph in (sec.get('phases') or []):
|
|
593
626
|
if isinstance(ph, (list, tuple)):
|
|
594
627
|
lab, name, desc = (list(ph) + ['', '', ''])[:3]
|
|
595
628
|
state = ph[3] if len(ph) > 3 else ''
|
|
629
|
+
icon_name = ph[4] if len(ph) > 4 else ''
|
|
596
630
|
else:
|
|
597
631
|
lab, name, desc, state = ph.get('label', ''), ph.get('name', ''), ph.get('d', ''), ph.get('s', '')
|
|
632
|
+
icon_name = ph.get('icon', '')
|
|
598
633
|
cls = 'tl__i'
|
|
599
634
|
if state == 'done':
|
|
600
635
|
cls += ' tl__i--done'
|
|
601
636
|
elif state == 'now':
|
|
602
637
|
cls += ' tl__i--now'
|
|
638
|
+
# 变体 timeline:节点图标(icon:<name>,无图标回落圆点)
|
|
639
|
+
dot = '<div class="tl__d"></div>'
|
|
640
|
+
if variant == 'timeline' and icon_name and icon_name in ICONS:
|
|
641
|
+
dot = (f'<div class="tl__ico" data-icon="{esc(icon_name)}">'
|
|
642
|
+
f'{ICONS[icon_name]}</div>')
|
|
603
643
|
items.append(
|
|
604
|
-
f' <div class="{cls}"
|
|
644
|
+
f' <div class="{cls}">{dot}'
|
|
605
645
|
f'<div class="tl__l">{esc(lab)}</div><div class="tl__t">{esc(name)}</div>'
|
|
606
646
|
f'<p class="t-body">{cite(desc)}</p></div>')
|
|
607
647
|
body = ' <div class="fig rv"><div class="tl">\n' + '\n'.join(items) + '\n </div></div>\n'
|
|
608
|
-
|
|
648
|
+
extra = ' data-variant="timeline"' if variant == 'timeline' else ''
|
|
649
|
+
return wrap(i, 'timeline', body, sec, extra)
|
|
609
650
|
|
|
610
651
|
|
|
611
652
|
def r_steps(i, sec):
|
|
@@ -675,18 +716,34 @@ def r_pyramid(i, sec):
|
|
|
675
716
|
|
|
676
717
|
|
|
677
718
|
def r_halftable(i, sec):
|
|
678
|
-
"""半表半图:g-half 左表右图(校验 TYPE_FEATURE.halftable)。
|
|
719
|
+
"""半表半图:g-half 左表右图(校验 TYPE_FEATURE.halftable)。
|
|
720
|
+
research 模式带 exhibitNo 时:右图按 Exhibit 规范加编号徽标与来源行,表侧不丢
|
|
721
|
+
(此前 research 统一走 r_exhibit 会静默丢弃 table——内容覆盖率门禁捕获)。"""
|
|
679
722
|
tbl = sec.get('table') or {}
|
|
680
723
|
ch = sec.get('chart') or {}
|
|
681
724
|
head = tbl.get('head') or []
|
|
682
725
|
rows = tbl.get('rows') or []
|
|
683
726
|
th = ''.join(f'<th>{esc(h)}</th>' for h in head)
|
|
684
727
|
trs = ''.join('<tr>' + ''.join(f'<td>{cite(c)}</td>' for c in row) + '</tr>' for row in rows)
|
|
728
|
+
no = sec.get('exhibitNo')
|
|
729
|
+
if no:
|
|
730
|
+
fig = (
|
|
731
|
+
' <div class="fig"><div class="exhibit rv">\n'
|
|
732
|
+
f' <div class="exhibit__hd"><span class="exhibit__no">Exhibit {esc(no)}</span>'
|
|
733
|
+
f'<span class="exhibit__t">{esc(sec.get("title") or "")}</span></div>\n'
|
|
734
|
+
f'{chart_svg(ch, i)}'
|
|
735
|
+
f' <div class="exhibit__src">{cite(sec.get("footnote") or "来源:待补")}</div>\n'
|
|
736
|
+
' </div></div>\n')
|
|
737
|
+
sec = dict(sec)
|
|
738
|
+
sec.pop('footnote', None) # 已进来源行,避免 wrap 再渲染一次
|
|
739
|
+
else:
|
|
740
|
+
fig = (
|
|
741
|
+
f' <div class="fig"><div class="fig__cap">{esc(ch.get("cap") or "互证图")}</div>\n'
|
|
742
|
+
f'{chart_svg(ch, i)} </div>\n')
|
|
685
743
|
body = (
|
|
686
744
|
' <div class="grid g-half g-2 rv a-start">\n'
|
|
687
745
|
f' <div class="tbl-wrap"><table><thead><tr>{th}</tr></thead><tbody>{trs}</tbody></table></div>\n'
|
|
688
|
-
f'
|
|
689
|
-
f'{chart_svg(ch, i)} </div>\n'
|
|
746
|
+
f'{fig}'
|
|
690
747
|
' </div>\n')
|
|
691
748
|
return wrap(i, 'halftable', body, sec)
|
|
692
749
|
|
|
@@ -800,8 +857,10 @@ def render_body(model: dict) -> str:
|
|
|
800
857
|
title = ag[1] if isinstance(ag, (list, tuple)) and len(ag) > 1 else ''
|
|
801
858
|
desc = ag[2] if isinstance(ag, (list, tuple)) and len(ag) > 2 else ''
|
|
802
859
|
href_i = chapter_first.get(str(num).zfill(2)) or chapter_first.get(str(num)) or gi
|
|
860
|
+
# 锚点与 open_sec 同源:该页有 anchor 则链到命名锚点
|
|
861
|
+
href_id = sec_anchor(href_i, secs[href_i - 1] if 0 < href_i <= len(secs) else {})
|
|
803
862
|
lis.append(
|
|
804
|
-
f' <li class="agenda__i"><a class="agenda__a" href="#
|
|
863
|
+
f' <li class="agenda__i"><a class="agenda__a" href="#{href_id}">'
|
|
805
864
|
f'<span class="agenda__n">{esc(num)}</span>'
|
|
806
865
|
f'<span><span class="agenda__t">{esc(title)}</span>'
|
|
807
866
|
f'<div class="agenda__d">{esc(desc)}</div></span></a></li>')
|
|
@@ -822,7 +881,9 @@ def render_body(model: dict) -> str:
|
|
|
822
881
|
sec = dict(sec)
|
|
823
882
|
sec['exhibitNo'] = ex_n
|
|
824
883
|
fn = RENDERERS.get(t, r_points)
|
|
825
|
-
if is_research_exhibit:
|
|
884
|
+
if is_research_exhibit and t != 'halftable':
|
|
885
|
+
# halftable 在 research 下仍走 r_halftable(表+图),仅借 exhibitNo 加 Exhibit 徽标;
|
|
886
|
+
# 统一走 r_exhibit 会静默丢弃 table(内容覆盖率门禁捕获,0.2.0 修)。
|
|
826
887
|
fn = r_exhibit
|
|
827
888
|
try:
|
|
828
889
|
parts.append(fn(i, sec))
|