dsh-ppt-studio 1.1.0
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/LICENSE +21 -0
- package/README.md +480 -0
- package/agent-presets/ppt/agent.cordis.yml +308 -0
- package/agent-presets/ppt/preset.yml +3 -0
- package/cordis.patch.yml +29 -0
- package/docs/00-/350/256/276/350/256/241/346/272/220.md +537 -0
- package/docs/01-/351/234/200/346/261/202/344/270/216/347/233/256/346/240/207.md +136 -0
- package/docs/02-/346/212/200/346/234/257/346/212/245/345/221/212.md +483 -0
- package/docs/03-/346/233/264/346/226/260/346/227/245/345/277/227.md +2898 -0
- package/docs/04-/350/267/257/347/272/277/345/233/276/344/270/216/351/207/214/347/250/213/347/242/221.md +113 -0
- package/docs/05-/350/277/255/344/273/243/346/265/201/347/250/213.md +210 -0
- package/docs/06-/350/257/204/345/256/241/344/270/216/346/265/213/350/257/225.md +296 -0
- package/docs/fidelity-report.md +225 -0
- package/examples/fx/fx-mini/deck.yaml +39 -0
- package/examples/fx/fx-mini/pages/01.yaml +6 -0
- package/examples/fx/fx-mini/pages/02.yaml +2 -0
- package/examples/fx/fx-mini/pages/03.yaml +6 -0
- package/examples/fx/fx-pro/deck.yaml +48 -0
- package/examples/fx/fx-pro/pages/01_cover.yaml +40 -0
- package/examples/fx/fx-pro/pages/02_arch.yaml +55 -0
- package/examples/fx/fx-pro/pages/03_table.yaml +20 -0
- package/examples/fx/fx-pro/pages/04_chart.yaml +40 -0
- package/examples/fx/fx-pro/pages/05_timeline.yaml +71 -0
- package/examples/fx/fx-pro/pages/06_longtext.yaml +13 -0
- package/examples/fx/fx-pro/pages/07_safearea.yaml +33 -0
- package/examples/fx/fx-pro/pages/08_contrast.yaml +22 -0
- package/examples/fx/fx-pro/pages/09_custgeom.yaml +32 -0
- package/examples/fx/fx-pro/pages/10_numbers.yaml +25 -0
- package/examples/fx/fx-pro/pages/11_section.yaml +20 -0
- package/examples/fx/fx-pro/pages/12_summary.yaml +57 -0
- package/examples/fx/fx-pro/preview/01_pages_01_cover.html +1 -0
- package/examples/fx/fx-pro/preview/02_pages_02_arch.html +3 -0
- package/examples/fx/fx-pro/preview/03_pages_03_table.html +1 -0
- package/examples/fx/fx-pro/preview/04_pages_04_chart.html +3 -0
- package/examples/fx/fx-pro/preview/05_pages_05_timeline.html +2 -0
- package/examples/fx/fx-pro/preview/06_pages_06_longtext.html +4 -0
- package/examples/fx/fx-pro/preview/07_pages_07_safearea.html +2 -0
- package/examples/fx/fx-pro/preview/08_pages_08_contrast.html +1 -0
- package/examples/fx/fx-pro/preview/09_pages_09_custgeom.html +1 -0
- package/examples/fx/fx-pro/preview/10_pages_10_numbers.html +1 -0
- package/examples/fx/fx-pro/preview/11_pages_11_section.html +1 -0
- package/examples/fx/fx-pro/preview/12_pages_12_summary.html +1 -0
- package/examples/fx/fx-pro/preview/deck.html +10 -0
- package/examples/fx/fx-pro/preview/layout.json +2325 -0
- package/examples/fx/fx-pro/preview/measured.json +814 -0
- package/examples/fx/fx-pro.pptx +0 -0
- package/examples/fx/ren-single/01.png +0 -0
- package/examples/fx/ren-spliced/01.png +0 -0
- package/examples/fx/ren-spliced/03.png +0 -0
- package/examples/fx/seed.pptx +0 -0
- package/examples/fx/single.pptx +0 -0
- package/examples/fx/spliced.pptx +0 -0
- package/examples/smoke/deck.yaml +26 -0
- package/examples/smoke/imported/deck.yaml +30 -0
- package/examples/smoke/imported/import-styles.json +126 -0
- package/examples/smoke/imported/pages/slide_01.yaml +31 -0
- package/examples/smoke/imported/pages/slide_02.yaml +38 -0
- package/examples/smoke/imported/pages/slide_03.yaml +55 -0
- package/examples/smoke/imported/preview-rt/01_pages_slide_01.html +1 -0
- package/examples/smoke/imported/preview-rt/02_pages_slide_02.html +1 -0
- package/examples/smoke/imported/preview-rt/03_pages_slide_03.html +1 -0
- package/examples/smoke/imported/preview-rt/deck.html +1 -0
- package/examples/smoke/imported/preview-rt/layout.json +434 -0
- package/examples/smoke/imported/reference/previews/01.png +0 -0
- package/examples/smoke/imported/reference/previews/02.png +0 -0
- package/examples/smoke/imported/reference/previews/03.png +0 -0
- package/examples/smoke/imported/source.pptx +0 -0
- package/examples/smoke/media-test.png +0 -0
- package/examples/smoke/out-smoke.pptx +0 -0
- package/examples/smoke/pages/01_cover.yaml +27 -0
- package/examples/smoke/pages/02_content.yaml +42 -0
- package/examples/smoke/pages/03_chart.yaml +35 -0
- package/examples/smoke/preview/01_pages_01_cover.html +1 -0
- package/examples/smoke/preview/02_pages_02_content.html +1 -0
- package/examples/smoke/preview/03_pages_03_chart.html +1 -0
- package/examples/smoke/preview/deck.html +1 -0
- package/examples/smoke/preview/layout.json +378 -0
- package/lib/autodeclare.js +40 -0
- package/lib/commands.js +161 -0
- package/lib/crosscheck.js +300 -0
- package/lib/imgmeta.js +55 -0
- package/lib/index.js +239 -0
- package/lib/measurement.js +209 -0
- package/lib/msrender.js +123 -0
- package/lib/pptd/export-pptx.js +686 -0
- package/lib/pptd/import-pptx.js +774 -0
- package/lib/pptd/layout.js +209 -0
- package/lib/pptd/render-html.js +254 -0
- package/lib/pptd/schema.js +518 -0
- package/lib/pptd/svgCharts.js +147 -0
- package/lib/pptxPy.js +205 -0
- package/lib/preset-delivery.js +195 -0
- package/lib/preview-server.js +164 -0
- package/lib/router.js +165 -0
- package/lib/scaffold.js +315 -0
- package/lib/skill.js +174 -0
- package/lib/splice.js +219 -0
- package/lib/state.js +88 -0
- package/lib/surgical.js +369 -0
- package/lib/templates.js +400 -0
- package/lib/tools.js +955 -0
- package/lib/verify.js +828 -0
- package/lib/xmljs.js +112 -0
- package/lib/zips.js +117 -0
- package/package.json +81 -0
- package/scripts/audit-manual-facts.mjs +294 -0
- package/scripts/audit-template-fidelity.mjs +162 -0
- package/scripts/bench-m2.mjs +48 -0
- package/scripts/build-templates.mjs +63 -0
- package/scripts/build.mjs +14 -0
- package/scripts/build.sh +7 -0
- package/scripts/check-lib-freshness.mjs +70 -0
- package/scripts/check-preset.mjs +189 -0
- package/scripts/check-tracked-lf.mjs +84 -0
- package/scripts/dbg-import.mjs +13 -0
- package/scripts/dbg-media.mjs +10 -0
- package/scripts/dbg-tpl-inventory.mjs +23 -0
- package/scripts/dump-workflow-baseline.mjs +38 -0
- package/scripts/e2e-1.0.mjs +129 -0
- package/scripts/eval-skills-blind.mjs +91 -0
- package/scripts/eval-skills-score.mjs +129 -0
- package/scripts/export-orig-templates.ps1 +17 -0
- package/scripts/fixtures/workflow-baseline.json +300 -0
- package/scripts/fixtures.mjs +501 -0
- package/scripts/import-user-templates.mjs +60 -0
- package/scripts/install-desktop.mjs +312 -0
- package/scripts/install.mjs +209 -0
- package/scripts/migrate-templates-dualtrack.mjs +71 -0
- package/scripts/preflight-1.0.mjs +198 -0
- package/scripts/probe-asset-freshness.mjs +57 -0
- package/scripts/probe-effect-semantics.mjs +69 -0
- package/scripts/probe-reparse-safety.mjs +32 -0
- package/scripts/rebuild-templates-c11.mjs +69 -0
- package/scripts/regression-real.mjs +84 -0
- package/scripts/release-sync.mjs +425 -0
- package/scripts/shot-templates.mjs +49 -0
- package/scripts/smoke.mjs +2512 -0
- package/scripts/verify-bundle-install.mjs +281 -0
- package/scripts/verify-fresh-install.mjs +260 -0
- package/skills/ppt-studio-copy/SKILL.md +139 -0
- package/skills/ppt-studio-craft/SKILL.md +171 -0
- package/skills/ppt-studio-data/SKILL.md +173 -0
- package/skills/ppt-studio-manual/SKILL.md +104 -0
- package/templates/academic-white/deck.yaml +24 -0
- package/templates/academic-white/pages/_chart.yaml +43 -0
- package/templates/academic-white/pages/_compare.yaml +37 -0
- package/templates/academic-white/pages/_content.yaml +35 -0
- package/templates/academic-white/pages/_cover.yaml +28 -0
- package/templates/academic-white/pages/_ending.yaml +28 -0
- package/templates/academic-white/pages/_flow.yaml +61 -0
- package/templates/academic-white/preview.png +0 -0
- package/templates/academic-white/template.yaml +14 -0
- package/templates/business-blue/deck.yaml +28 -0
- package/templates/business-blue/pages/_chart.yaml +36 -0
- package/templates/business-blue/pages/_compare.yaml +45 -0
- package/templates/business-blue/pages/_content.yaml +80 -0
- package/templates/business-blue/pages/_cover.yaml +39 -0
- package/templates/business-blue/pages/_ending.yaml +32 -0
- package/templates/business-blue/pages/_flow.yaml +87 -0
- package/templates/business-blue/preview.png +0 -0
- package/templates/business-blue/template.yaml +14 -0
- package/templates/pitch-bold/deck.yaml +23 -0
- package/templates/pitch-bold/pages/_chart.yaml +32 -0
- package/templates/pitch-bold/pages/_compare.yaml +36 -0
- package/templates/pitch-bold/pages/_content.yaml +58 -0
- package/templates/pitch-bold/pages/_cover.yaml +31 -0
- package/templates/pitch-bold/pages/_ending.yaml +27 -0
- package/templates/pitch-bold/pages/_flow.yaml +46 -0
- package/templates/pitch-bold/preview.png +0 -0
- package/templates/pitch-bold/template.yaml +14 -0
- package/templates/tech-dark/deck.yaml +24 -0
- package/templates/tech-dark/pages/_chart.yaml +27 -0
- package/templates/tech-dark/pages/_compare.yaml +38 -0
- package/templates/tech-dark/pages/_content.yaml +51 -0
- package/templates/tech-dark/pages/_cover.yaml +30 -0
- package/templates/tech-dark/pages/_ending.yaml +25 -0
- package/templates/tech-dark/pages/_flow.yaml +80 -0
- package/templates/tech-dark/preview.png +0 -0
- package/templates/tech-dark/template.yaml +14 -0
|
@@ -0,0 +1,501 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* 1.0.0 自包含测试夹具生成器(用户已清理历史工作区——此后测试不求外部资产):
|
|
4
|
+
* 生成 examples/fx/:
|
|
5
|
+
* fx-pro —— 高难度综合 12 页(声明制/安全区/对比度/渐变/alpha/custGeom/嵌套卡/表格/图表/跨页数字/来源标注)
|
|
6
|
+
* fx-mini —— 3 页迷你(单元素/空白页/极简)——快速路径
|
|
7
|
+
* 再导出 fx-pro 为 seed.pptx(splice/slice 的自产 source)。
|
|
8
|
+
*/
|
|
9
|
+
import { rm, mkdir, writeFile } from 'node:fs/promises'
|
|
10
|
+
import { join, dirname } from 'node:path'
|
|
11
|
+
import { fileURLToPath } from 'node:url'
|
|
12
|
+
import { exportPptx } from '../lib/pptd/export-pptx.js'
|
|
13
|
+
import { resolveDeck } from '../lib/pptd/schema.js'
|
|
14
|
+
|
|
15
|
+
const root = join(dirname(fileURLToPath(import.meta.url)), '..')
|
|
16
|
+
const fx = join(root, 'examples', 'fx')
|
|
17
|
+
|
|
18
|
+
const THEME = `theme:
|
|
19
|
+
colors:
|
|
20
|
+
ink: "#1F2937" # 主墨
|
|
21
|
+
primary: "#2563EB" # 主蓝
|
|
22
|
+
accent: "#F59E0B" # 强调橙
|
|
23
|
+
danger: "#C0392B" # 警示红
|
|
24
|
+
steel: "#64748B" # 辅助灰蓝
|
|
25
|
+
bg: "#F8FAFC" # 页面底
|
|
26
|
+
deep: "#0F172A" # 深底(白字承载)
|
|
27
|
+
# 扩展档(夹具调色:蓝/紫/绿阶 + 浅底)
|
|
28
|
+
blue-soft: "#DBEAFE"
|
|
29
|
+
blue-deep: "#1E3A8A"
|
|
30
|
+
blue-mid: "#3B82F6"
|
|
31
|
+
slate-deep: "#334155"
|
|
32
|
+
slate-soft: "#CBD5E1"
|
|
33
|
+
slate-lite: "#E2E8F0"
|
|
34
|
+
amber-soft: "#FEF3C7"
|
|
35
|
+
amber-deep: "#92400E"
|
|
36
|
+
amber-dark: "#78350F"
|
|
37
|
+
violet: "#8B5CF6"
|
|
38
|
+
green: "#10B981"
|
|
39
|
+
green-soft: "#D1FAE5"
|
|
40
|
+
green-deep: "#065F46"
|
|
41
|
+
textStyles:
|
|
42
|
+
title: {fontSize: 32, color: "$ink", bold: true, fontFamily: "微软雅黑"}
|
|
43
|
+
h1: {fontSize: 22, color: "$ink", bold: true, fontFamily: "微软雅黑"}
|
|
44
|
+
h2: {fontSize: 16, color: "$ink", bold: true, fontFamily: "微软雅黑"}
|
|
45
|
+
body: {fontSize: 14, color: "$ink", fontFamily: "微软雅黑"}
|
|
46
|
+
small: {fontSize: 12, color: "$steel", fontFamily: "微软雅黑"}
|
|
47
|
+
safeArea: {top: 34, bottom: 26, left: 40, right: 40}
|
|
48
|
+
minFontSize: 12
|
|
49
|
+
`
|
|
50
|
+
|
|
51
|
+
const P = (name, body) => [name, body].join('')
|
|
52
|
+
|
|
53
|
+
const pages = {
|
|
54
|
+
'pages/01_cover.yaml': P('pageType: cover\nbackground: "#F8FAFC"\nexpectedOutOfSafeArea: [band]\n', `elements:
|
|
55
|
+
- elementId: band
|
|
56
|
+
elementType: shape
|
|
57
|
+
kind: rect
|
|
58
|
+
bounds: [0, 0, 960, 540]
|
|
59
|
+
fill: {type: gradient, stops: [{pos: 0, color: "#0F172A"}, {pos: 100, color: "#2563EB"}], angle: 90}
|
|
60
|
+
- elementId: glow
|
|
61
|
+
elementType: shape
|
|
62
|
+
kind: ellipse
|
|
63
|
+
bounds: [660, 34, 260, 260]
|
|
64
|
+
fill: {color: "#FFFFFF", alpha: 18}
|
|
65
|
+
- elementId: title
|
|
66
|
+
elementType: text
|
|
67
|
+
bounds: [80, 150, 700, 90]
|
|
68
|
+
content: {text: "能力验证演示(自建夹具)", style: "$title", color: "#FFFFFF"}
|
|
69
|
+
- elementId: sub
|
|
70
|
+
elementType: text
|
|
71
|
+
bounds: [80, 260, 600, 50]
|
|
72
|
+
content: {text: "PPTD 中间层 · 声明制 · 双轨审阅", style: "$h1", color: "#CBD5E1"}
|
|
73
|
+
- elementId: badge
|
|
74
|
+
elementType: shape
|
|
75
|
+
kind: chevron
|
|
76
|
+
bounds: [80, 420, 200, 56]
|
|
77
|
+
fill: "#F59E0B"
|
|
78
|
+
- elementId: badge_t
|
|
79
|
+
elementType: text
|
|
80
|
+
bounds: [100, 430, 160, 40]
|
|
81
|
+
content: {text: "v1.0.0", style: "$h2", color: "#0F172A", align: center}
|
|
82
|
+
expectedOverlaps:
|
|
83
|
+
- {pair: [band, glow]}
|
|
84
|
+
- {pair: [band, title]}
|
|
85
|
+
- {pair: [band, sub]}
|
|
86
|
+
- {pair: [band, badge]}
|
|
87
|
+
- {pair: [band, badge_t]}
|
|
88
|
+
- {pair: [glow, title]}
|
|
89
|
+
- {pair: [glow, sub]}
|
|
90
|
+
- {pair: [badge, badge_t]}
|
|
91
|
+
`),
|
|
92
|
+
'pages/02_arch.yaml': P('pageType: content\nbackground: "#F8FAFC"\nsource: "夹具自造架构示意(三层嵌套 + 声明闭包)"\n', `elements:
|
|
93
|
+
- elementId: panel
|
|
94
|
+
elementType: shape
|
|
95
|
+
kind: roundRect
|
|
96
|
+
bounds: [60, 80, 840, 380]
|
|
97
|
+
fill: "#FFFFFF"
|
|
98
|
+
line: {color: "#CBD5E1", width: 1}
|
|
99
|
+
- elementId: box_a
|
|
100
|
+
elementType: shape
|
|
101
|
+
kind: rect
|
|
102
|
+
bounds: [100, 130, 350, 240]
|
|
103
|
+
fill: "#DBEAFE"
|
|
104
|
+
line: {color: "#2563EB", width: 1}
|
|
105
|
+
- elementId: box_a_t
|
|
106
|
+
elementType: text
|
|
107
|
+
bounds: [120, 150, 310, 60]
|
|
108
|
+
content: {text: "领域本体(知识层)", style: "$h2", color: "#1E3A8A"}
|
|
109
|
+
- elementId: box_a_b
|
|
110
|
+
elementType: text
|
|
111
|
+
bounds: [120, 230, 310, 120]
|
|
112
|
+
content: {text: "对象·属性·关系·约束\\n换场景 = 换实例,零重构", style: "$body", color: "#334155"}
|
|
113
|
+
- elementId: box_b
|
|
114
|
+
elementType: shape
|
|
115
|
+
kind: rect
|
|
116
|
+
bounds: [510, 130, 350, 240]
|
|
117
|
+
fill: "#FEF3C7"
|
|
118
|
+
line: {color: "#F59E0B", width: 1}
|
|
119
|
+
- elementId: box_b_t
|
|
120
|
+
elementType: text
|
|
121
|
+
bounds: [530, 150, 310, 60]
|
|
122
|
+
content: {text: "仿真内核(执行层)", style: "$h2", color: "#92400E"}
|
|
123
|
+
- elementId: box_b_b
|
|
124
|
+
elementType: text
|
|
125
|
+
bounds: [530, 230, 310, 120]
|
|
126
|
+
content: {text: "车间生产·负载评估·计划跟踪\\n结论可迁移(泛化性)", style: "$body", color: "#78350F"}
|
|
127
|
+
- elementId: arrow
|
|
128
|
+
elementType: line
|
|
129
|
+
points: [[450, 250], [508, 250]]
|
|
130
|
+
arrow: true
|
|
131
|
+
line: {color: "#2563EB", width: 2}
|
|
132
|
+
- elementId: foot
|
|
133
|
+
elementType: text
|
|
134
|
+
bounds: [100, 470, 760, 40]
|
|
135
|
+
content: {text: "知识层 → 映射 → 执行层:本体驱动仿真的泛化范式(A8 场景也在本页)", style: "$small"}
|
|
136
|
+
expectedOverlaps:
|
|
137
|
+
- {pair: [panel, box_a]}
|
|
138
|
+
- {pair: [panel, box_b]}
|
|
139
|
+
- {pair: [box_a, box_a_t]}
|
|
140
|
+
- {pair: [box_a, box_a_b]}
|
|
141
|
+
- {pair: [box_b, box_b_t]}
|
|
142
|
+
- {pair: [box_b, box_b_b]}
|
|
143
|
+
# 声明闭包:panel×box_a_t 等隔层对由包含关系传递自动通过,无需声明
|
|
144
|
+
`),
|
|
145
|
+
'pages/03_table.yaml': P('pageType: content\nbackground: "#F8FAFC"\nsource: "夹具自造表(数据为演示样本)"\n', `elements:
|
|
146
|
+
- elementId: t_head
|
|
147
|
+
elementType: text
|
|
148
|
+
bounds: [60, 60, 400, 40]
|
|
149
|
+
content: {text: "对策-指标对照表", style: "$h1"}
|
|
150
|
+
- elementId: tbl
|
|
151
|
+
elementType: table
|
|
152
|
+
bounds: [60, 120, 840, 300]
|
|
153
|
+
cols: [对策, 指标A, 指标B, 说明]
|
|
154
|
+
rows:
|
|
155
|
+
- [线表决策辅助, 决策响应-32%, 准确率+12%, 基线 45.6%]
|
|
156
|
+
- [排程优化, 交付周期-18%, 利用率+9%, 中型场景]
|
|
157
|
+
- [生产流程仿真, 节拍-15%, 一次合格率+8%, 泛化验证]
|
|
158
|
+
- elementId: t_note
|
|
159
|
+
elementType: text
|
|
160
|
+
bounds: [60, 450, 840, 40]
|
|
161
|
+
content: {text: "注:数据为演示样本(45.6% 为跨页对账演示数字)", style: "$small"}
|
|
162
|
+
`),
|
|
163
|
+
'pages/04_chart.yaml': P('pageType: content\nbackground: "#F8FAFC"\nsource: "夹具自造折线(演示)"\n', `elements:
|
|
164
|
+
- elementId: c_head
|
|
165
|
+
elementType: text
|
|
166
|
+
bounds: [60, 60, 400, 40]
|
|
167
|
+
content: {text: "仿真运行趋势", style: "$h1"}
|
|
168
|
+
- elementId: chart
|
|
169
|
+
elementType: chart
|
|
170
|
+
bounds: [60, 130, 480, 300]
|
|
171
|
+
chart:
|
|
172
|
+
type: line
|
|
173
|
+
data:
|
|
174
|
+
cols: [month, pass]
|
|
175
|
+
rows:
|
|
176
|
+
- ["1月", 92]
|
|
177
|
+
- ["2月", 94]
|
|
178
|
+
- ["3月", 95]
|
|
179
|
+
- ["4月", 96]
|
|
180
|
+
series:
|
|
181
|
+
- {name: 一次合格率, x: month, y: pass}
|
|
182
|
+
colors: ["$primary"]
|
|
183
|
+
- elementId: key
|
|
184
|
+
elementType: shape
|
|
185
|
+
kind: roundRect
|
|
186
|
+
bounds: [580, 130, 320, 300]
|
|
187
|
+
fill: "#FFFFFF"
|
|
188
|
+
line: {color: "#CBD5E1", width: 1}
|
|
189
|
+
- elementId: key_t
|
|
190
|
+
elementType: text
|
|
191
|
+
bounds: [610, 160, 260, 60]
|
|
192
|
+
content: {text: "关键指标", style: "$h2"}
|
|
193
|
+
- elementId: key_body
|
|
194
|
+
elementType: text
|
|
195
|
+
bounds: [610, 240, 260, 160]
|
|
196
|
+
content: {text: "一次合格率 96%(+8pt)\\n综合评 45.6% 提升\\n节拍 −15%", style: "$body"}
|
|
197
|
+
expectedOverlaps:
|
|
198
|
+
- {pair: [key, key_t]}
|
|
199
|
+
- {pair: [key, key_body]}
|
|
200
|
+
`),
|
|
201
|
+
'pages/05_timeline.yaml': P('pageType: content\nbackground: "#F8FAFC"\n', `elements:
|
|
202
|
+
- elementId: tl_head
|
|
203
|
+
elementType: text
|
|
204
|
+
bounds: [60, 60, 500, 40]
|
|
205
|
+
content: {text: "实施路线(五阶段)", style: "$h1"}
|
|
206
|
+
- elementId: tl_line
|
|
207
|
+
elementType: line
|
|
208
|
+
points: [[80, 200], [860, 200]]
|
|
209
|
+
line: {color: "#94A3B8", width: 2}
|
|
210
|
+
- elementId: st1
|
|
211
|
+
elementType: shape
|
|
212
|
+
kind: pentagon
|
|
213
|
+
bounds: [60, 140, 140, 60]
|
|
214
|
+
fill: "#2563EB"
|
|
215
|
+
- elementId: st1_t
|
|
216
|
+
elementType: text
|
|
217
|
+
bounds: [70, 148, 120, 44]
|
|
218
|
+
content: {text: "建模", style: "$h2", color: "#FFFFFF", align: center}
|
|
219
|
+
- elementId: st2
|
|
220
|
+
elementType: shape
|
|
221
|
+
kind: pentagon
|
|
222
|
+
bounds: [220, 140, 140, 60]
|
|
223
|
+
fill: "#3B82F6"
|
|
224
|
+
- elementId: st2_t
|
|
225
|
+
elementType: text
|
|
226
|
+
bounds: [230, 148, 120, 44]
|
|
227
|
+
content: {text: "映射", style: "$h2", color: "#FFFFFF", align: center}
|
|
228
|
+
- elementId: st3
|
|
229
|
+
elementType: shape
|
|
230
|
+
kind: pentagon
|
|
231
|
+
bounds: [380, 140, 140, 60]
|
|
232
|
+
fill: "#F59E0B"
|
|
233
|
+
- elementId: st3_t
|
|
234
|
+
elementType: text
|
|
235
|
+
bounds: [390, 148, 120, 44]
|
|
236
|
+
content: {text: "仿真", style: "$h2", color: "#FFFFFF", align: center}
|
|
237
|
+
- elementId: st4
|
|
238
|
+
elementType: shape
|
|
239
|
+
kind: pentagon
|
|
240
|
+
bounds: [540, 140, 140, 60]
|
|
241
|
+
fill: "#8B5CF6"
|
|
242
|
+
- elementId: st4_t
|
|
243
|
+
elementType: text
|
|
244
|
+
bounds: [550, 148, 120, 44]
|
|
245
|
+
content: {text: "验证", style: "$h2", color: "#FFFFFF", align: center}
|
|
246
|
+
- elementId: st5
|
|
247
|
+
elementType: shape
|
|
248
|
+
kind: pentagon
|
|
249
|
+
bounds: [700, 140, 140, 60]
|
|
250
|
+
fill: "#10B981"
|
|
251
|
+
- elementId: st5_t
|
|
252
|
+
elementType: text
|
|
253
|
+
bounds: [710, 148, 120, 44]
|
|
254
|
+
content: {text: "推广", style: "$h2", color: "#FFFFFF", align: center}
|
|
255
|
+
- elementId: tl_note
|
|
256
|
+
elementType: text
|
|
257
|
+
bounds: [80, 300, 800, 100]
|
|
258
|
+
content: {text: "单向推进 + 每阶段门禁:建模评审 → 映射校验 → 仿真对标 → 验证报告 → 批量复制\\n(时间线为示意图,非真实排期)", style: "$body"}
|
|
259
|
+
expectedOverlaps:
|
|
260
|
+
- {pair: [st1, tl_line]}
|
|
261
|
+
- {pair: [st2, tl_line]}
|
|
262
|
+
- {pair: [st3, tl_line]}
|
|
263
|
+
- {pair: [st4, tl_line]}
|
|
264
|
+
- {pair: [st5, tl_line]}
|
|
265
|
+
- {pair: [st1, st1_t]}
|
|
266
|
+
- {pair: [st2, st2_t]}
|
|
267
|
+
- {pair: [st3, st3_t]}
|
|
268
|
+
- {pair: [st4, st4_t]}
|
|
269
|
+
- {pair: [st5, st5_t]}
|
|
270
|
+
`),
|
|
271
|
+
'pages/06_longtext.yaml': P('pageType: content\nbackground: "#F8FAFC"\n', `elements:
|
|
272
|
+
- elementId: lt_head
|
|
273
|
+
elementType: text
|
|
274
|
+
bounds: [60, 60, 500, 40]
|
|
275
|
+
content: {text: "长文本容纳(保守估算 + 显式断行)", style: "$h1"}
|
|
276
|
+
- elementId: lt_body
|
|
277
|
+
elementType: text
|
|
278
|
+
bounds: [60, 130, 840, 340]
|
|
279
|
+
content:
|
|
280
|
+
text: "这一段用于验证长文本在保守估算下的处理:中文全角加粗按 1.06 系数、行高 1.2、标点禁则——\\n显式断行是长句的最佳实践,容器也必须给足高度。\\n本夹具故意把每行控制在 40 字以内,并预留 340px 高度(约 20 行×16.8px),确保门禁零错误,\\n同时验证保守估算不误报:若容器足够,estimate 与实测应一致。"
|
|
281
|
+
style: "$body"
|
|
282
|
+
`),
|
|
283
|
+
'pages/07_safearea.yaml': P('pageType: content\nbackground: "#F8FAFC"\nexpectedOutOfSafeArea: [logo, logo_t, footer, footer_t]\n', `elements:
|
|
284
|
+
- elementId: logo
|
|
285
|
+
elementType: shape
|
|
286
|
+
kind: ellipse
|
|
287
|
+
bounds: [895, 8, 40, 30]
|
|
288
|
+
fill: "#2563EB"
|
|
289
|
+
- elementId: logo_t
|
|
290
|
+
elementType: text
|
|
291
|
+
bounds: [897, 12, 36, 24]
|
|
292
|
+
content: {text: "S", style: "$body", color: "#FFFFFF", align: center}
|
|
293
|
+
- elementId: sa_title
|
|
294
|
+
elementType: text
|
|
295
|
+
bounds: [60, 70, 600, 40]
|
|
296
|
+
content: {text: "安全区与页脚带声明(logo 落在安全区外)", style: "$h1"}
|
|
297
|
+
- elementId: sa_body
|
|
298
|
+
elementType: text
|
|
299
|
+
bounds: [60, 140, 840, 200]
|
|
300
|
+
content: {text: "主题 safeArea 定义为顶/底/左/右 34/26/40/40px:内容不得侵入。\\n页脚带(y≥514)属于安全区外——如需放角标,用 expectedOutOfSafeArea 声明(已声明 logo)。", style: "$body"}
|
|
301
|
+
- elementId: footer
|
|
302
|
+
elementType: shape
|
|
303
|
+
kind: rect
|
|
304
|
+
bounds: [0, 514, 960, 26]
|
|
305
|
+
fill: "#E2E8F0"
|
|
306
|
+
- elementId: footer_t
|
|
307
|
+
elementType: text
|
|
308
|
+
bounds: [60, 518, 300, 20]
|
|
309
|
+
content: {text: "页脚带:夹具页脚 © v1.0.0", style: "$small"}
|
|
310
|
+
expectedOverlaps:
|
|
311
|
+
- {pair: [logo, logo_t]}
|
|
312
|
+
- {pair: [footer, footer_t]}
|
|
313
|
+
`),
|
|
314
|
+
'pages/08_contrast.yaml': P('pageType: content\nbackground: "#F8FAFC"\n', `elements:
|
|
315
|
+
- elementId: ct_head
|
|
316
|
+
elementType: text
|
|
317
|
+
bounds: [60, 60, 600, 40]
|
|
318
|
+
content: {text: "对比度承载面(渐变深底白字)", style: "$h1"}
|
|
319
|
+
- elementId: carrier
|
|
320
|
+
elementType: shape
|
|
321
|
+
kind: parallelogram
|
|
322
|
+
bounds: [60, 150, 400, 180]
|
|
323
|
+
fill: {type: gradient, stops: [{pos: 0, color: "#0F172A"}, {pos: 100, color: "#2563EB"}], angle: 90}
|
|
324
|
+
- elementId: carrier_t
|
|
325
|
+
elementType: text
|
|
326
|
+
bounds: [110, 230, 300, 60]
|
|
327
|
+
content: {text: "深底上的白字", style: "$h2", color: "#FFFFFF", align: center}
|
|
328
|
+
- elementId: ct_note
|
|
329
|
+
elementType: text
|
|
330
|
+
bounds: [520, 170, 380, 140]
|
|
331
|
+
content: {text: '对比度启发式以最近承载面(渐变中途 stop)为基准——本页不应报"白字 vs 页面背景"误报。', style: "$body"}
|
|
332
|
+
expectedOverlaps:
|
|
333
|
+
- {pair: [carrier, carrier_t]}
|
|
334
|
+
`),
|
|
335
|
+
'pages/09_custgeom.yaml': P('pageType: content\nbackground: "#F8FAFC"\nexpectedOutOfSafeArea: [arc]\n', `elements:
|
|
336
|
+
- elementId: cg_head
|
|
337
|
+
elementType: text
|
|
338
|
+
bounds: [60, 60, 600, 40]
|
|
339
|
+
content: {text: "custGeom 自定义几何(弧带装饰)", style: "$h1"}
|
|
340
|
+
- elementId: arc
|
|
341
|
+
elementType: shape
|
|
342
|
+
kind: custGeom
|
|
343
|
+
bounds: [600, 320, 320, 200]
|
|
344
|
+
path:
|
|
345
|
+
w: 320
|
|
346
|
+
h: 200
|
|
347
|
+
commands:
|
|
348
|
+
- {cmd: moveTo, pts: [[0, 200]]}
|
|
349
|
+
- {cmd: arcTo, pts: [[160, 100]], wR: 160, hR: 100, stAng: 0, swAng: 5400000}
|
|
350
|
+
- {cmd: lnTo, pts: [[200, 60]]}
|
|
351
|
+
- {cmd: cubicBezTo, pts: [[180, 40], [120, 20], [0, 20]]}
|
|
352
|
+
- {cmd: close}
|
|
353
|
+
fill: {color: "#2563EB", alpha: 35}
|
|
354
|
+
- elementId: cg_body
|
|
355
|
+
elementType: text
|
|
356
|
+
bounds: [60, 140, 500, 160]
|
|
357
|
+
content: {text: "弧带为装饰层(role: decoration 豁免重叠);几何:moveTo/arcTo/lnTo/cubicBezTo/close。", style: "$body"}
|
|
358
|
+
- elementId: deco
|
|
359
|
+
elementType: shape
|
|
360
|
+
kind: ellipse
|
|
361
|
+
bounds: [700, 60, 120, 120]
|
|
362
|
+
fill: {color: "#F59E0B", alpha: 30}
|
|
363
|
+
role: decoration
|
|
364
|
+
`),
|
|
365
|
+
'pages/10_numbers.yaml': P('pageType: content\nbackground: "#F8FAFC"\nsource: "夹具自造摘要(45.6% 与 04 页跨页)"\n', `elements:
|
|
366
|
+
- elementId: num_card
|
|
367
|
+
elementType: shape
|
|
368
|
+
kind: roundRect
|
|
369
|
+
bounds: [60, 90, 380, 220]
|
|
370
|
+
fill: "#FFFFFF"
|
|
371
|
+
line: {color: "#CBD5E1", width: 1}
|
|
372
|
+
- elementId: num_v
|
|
373
|
+
elementType: text
|
|
374
|
+
bounds: [100, 120, 300, 80]
|
|
375
|
+
content: {text: "45.6%", style: "$title", color: "#2563EB", align: center}
|
|
376
|
+
- elementId: num_l
|
|
377
|
+
elementType: text
|
|
378
|
+
bounds: [100, 220, 300, 40]
|
|
379
|
+
content: {text: "综合效率提升(与 04 页同值)", style: "$small", align: center}
|
|
380
|
+
- elementId: num_note
|
|
381
|
+
elementType: text
|
|
382
|
+
bounds: [480, 120, 420, 160]
|
|
383
|
+
content: {text: "跨页演示:本页 45.6% 与第 04 页 key_body 的 45.6% 应出现在 ppt_crosscheck 材料包。", style: "$body"}
|
|
384
|
+
expectedOverlaps:
|
|
385
|
+
- {pair: [num_card, num_v]}
|
|
386
|
+
- {pair: [num_card, num_l]}
|
|
387
|
+
`),
|
|
388
|
+
'pages/11_section.yaml': P('pageType: content\nbackground: "#F8FAFC"\nexpectedOutOfSafeArea: [sec_band]\n', `elements:
|
|
389
|
+
- elementId: sec_band
|
|
390
|
+
elementType: shape
|
|
391
|
+
kind: rect
|
|
392
|
+
bounds: [0, 0, 960, 540]
|
|
393
|
+
fill: "#0F172A"
|
|
394
|
+
- elementId: sec_num
|
|
395
|
+
elementType: text
|
|
396
|
+
bounds: [80, 180, 300, 120]
|
|
397
|
+
content: {text: "03", style: "$title", color: "#F59E0B"}
|
|
398
|
+
- elementId: sec_t
|
|
399
|
+
elementType: text
|
|
400
|
+
bounds: [400, 200, 480, 80]
|
|
401
|
+
content: {text: "验证与展望", style: "$title", color: "#FFFFFF"}
|
|
402
|
+
expectedOverlaps:
|
|
403
|
+
- {pair: [sec_band, sec_num]}
|
|
404
|
+
- {pair: [sec_band, sec_t]}
|
|
405
|
+
`),
|
|
406
|
+
'pages/12_summary.yaml': P('pageType: content\nbackground: "#F8FAFC"\n', `elements:
|
|
407
|
+
- elementId: sm_head
|
|
408
|
+
elementType: text
|
|
409
|
+
bounds: [60, 60, 600, 40]
|
|
410
|
+
content: {text: "总结:一条可验证的生产线", style: "$h1"}
|
|
411
|
+
- elementId: sm_a
|
|
412
|
+
elementType: shape
|
|
413
|
+
kind: roundRect
|
|
414
|
+
bounds: [60, 130, 260, 180]
|
|
415
|
+
fill: "#DBEAFE"
|
|
416
|
+
- elementId: sm_a_t
|
|
417
|
+
elementType: text
|
|
418
|
+
bounds: [80, 150, 220, 60]
|
|
419
|
+
content: {text: "声明制", style: "$h2", color: "#1E3A8A", align: center}
|
|
420
|
+
- elementId: sm_a_b
|
|
421
|
+
elementType: text
|
|
422
|
+
bounds: [80, 220, 220, 70]
|
|
423
|
+
content: {text: "冲突防不住?声明设计意图", style: "$body"}
|
|
424
|
+
- elementId: sm_b
|
|
425
|
+
elementType: shape
|
|
426
|
+
kind: roundRect
|
|
427
|
+
bounds: [350, 130, 260, 180]
|
|
428
|
+
fill: "#FEF3C7"
|
|
429
|
+
- elementId: sm_b_t
|
|
430
|
+
elementType: text
|
|
431
|
+
bounds: [370, 150, 220, 60]
|
|
432
|
+
content: {text: "双轨审阅", style: "$h2", color: "#92400E", align: center}
|
|
433
|
+
- elementId: sm_b_b
|
|
434
|
+
elementType: text
|
|
435
|
+
bounds: [370, 220, 220, 70]
|
|
436
|
+
content: {text: "数字门禁 + 视觉审阅 + 真渲染", style: "$body"}
|
|
437
|
+
- elementId: sm_c
|
|
438
|
+
elementType: shape
|
|
439
|
+
kind: roundRect
|
|
440
|
+
bounds: [640, 130, 260, 180]
|
|
441
|
+
fill: "#D1FAE5"
|
|
442
|
+
- elementId: sm_c_t
|
|
443
|
+
elementType: text
|
|
444
|
+
bounds: [660, 150, 220, 60]
|
|
445
|
+
content: {text: "保真交付", style: "$h2", color: "#065F46", align: center}
|
|
446
|
+
- elementId: sm_c_b
|
|
447
|
+
elementType: text
|
|
448
|
+
bounds: [660, 220, 220, 70]
|
|
449
|
+
content: {text: "splice / slice / patch", style: "$body"}
|
|
450
|
+
- elementId: sm_foot
|
|
451
|
+
elementType: text
|
|
452
|
+
bounds: [60, 360, 840, 90]
|
|
453
|
+
content: {text: "结论:元素区块冲突被机器防住(声明制),质量可验证(三轨),保真可一指令(splice)。", style: "$body"}
|
|
454
|
+
expectedOverlaps:
|
|
455
|
+
- {pair: [sm_a, sm_a_t]}
|
|
456
|
+
- {pair: [sm_a, sm_a_b]}
|
|
457
|
+
- {pair: [sm_b, sm_b_t]}
|
|
458
|
+
- {pair: [sm_b, sm_b_b]}
|
|
459
|
+
- {pair: [sm_c, sm_c_t]}
|
|
460
|
+
- {pair: [sm_c, sm_c_b]}
|
|
461
|
+
`),
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const miniPages = {
|
|
465
|
+
'pages/01.yaml': 'pageType: content\nelements:\n - elementId: only\n elementType: text\n bounds: [60, 60, 400, 60]\n content: {text: "唯一元素页", style: "$body"}\n',
|
|
466
|
+
'pages/02.yaml': 'pageType: content\n# 空页(仅背景)\n',
|
|
467
|
+
'pages/03.yaml': 'pageType: content\nelements:\n - elementId: t\n elementType: text\n bounds: [40, 40, 300, 40]\n content: {text: "极简页", style: "$body"}\n',
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
export async function genFixtures() {
|
|
471
|
+
await rm(fx, { recursive: true, force: true })
|
|
472
|
+
await mkdir(join(fx, 'fx-pro', 'pages'), { recursive: true })
|
|
473
|
+
await mkdir(join(fx, 'fx-mini', 'pages'), { recursive: true })
|
|
474
|
+
await writeFile(join(fx, 'fx-pro', 'deck.yaml'), `# 高难度综合夹具(v1.0.0 自包含测试案例;声明齐备 → verify 应 0 错误)
|
|
475
|
+
version: 1
|
|
476
|
+
title: "能力验证演示"
|
|
477
|
+
size: [960, 540]
|
|
478
|
+
${THEME}pages:
|
|
479
|
+
${Object.keys(pages).map((k) => ` - ${k}`).join('\n')}
|
|
480
|
+
`)
|
|
481
|
+
for (const [k, body] of Object.entries(pages)) await writeFile(join(fx, 'fx-pro', k), body)
|
|
482
|
+
await writeFile(join(fx, 'fx-mini', 'deck.yaml'), `# 迷你夹具(快速路径)
|
|
483
|
+
version: 1
|
|
484
|
+
title: "迷你演示"
|
|
485
|
+
size: [960, 540]
|
|
486
|
+
${THEME}pages:
|
|
487
|
+
- pages/01.yaml
|
|
488
|
+
- pages/02.yaml
|
|
489
|
+
- pages/03.yaml
|
|
490
|
+
`)
|
|
491
|
+
for (const [k, body] of Object.entries(miniPages)) await writeFile(join(fx, 'fx-mini', k), body)
|
|
492
|
+
// 自产 seed:fx-pro 导出(splice/slice 的自产 source——不依赖任何外部资产)
|
|
493
|
+
const ctx = await resolveDeck(join(fx, 'fx-pro'))
|
|
494
|
+
const r = await exportPptx(ctx, { out: join(fx, 'seed.pptx'), engine: 'pptd' })
|
|
495
|
+
return { fx, seed: r.file, pages: ctx.pages.length }
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
if (process.argv[1] && import.meta.url.replace(/\\/g, '/').endsWith(process.argv[1].replace(/\\/g, '/'))) {
|
|
499
|
+
const info = await genFixtures()
|
|
500
|
+
console.log('fixtures generated:', info.fx, '| seed:', info.seed, '| pages:', info.pages)
|
|
501
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 批量收纳用户模板(一次性/可复用):<目录>/*.pptx → ppt_import → registerTemplate → 清临时。
|
|
3
|
+
* 全部内容(页面/媒体/预览图)复制到模板库 templates/<id>/,完成后源目录可安全删除。
|
|
4
|
+
* 用法:node scripts/import-user-templates.mjs [目录](缺省桌面 ppt模板库)
|
|
5
|
+
*/
|
|
6
|
+
import { join, dirname, basename } from 'node:path'
|
|
7
|
+
import { fileURLToPath } from 'node:url'
|
|
8
|
+
import { rm, mkdir, readdir } from 'node:fs/promises'
|
|
9
|
+
import { importPptx } from '../lib/pptd/import-pptx.js'
|
|
10
|
+
import { registerTemplate, listTemplates, TEMPLATES_DIR } from '../lib/templates.js'
|
|
11
|
+
import { resolveDeck } from '../lib/pptd/schema.js'
|
|
12
|
+
import { renderDeck } from '../lib/pptd/render-html.js'
|
|
13
|
+
import { verifyDeck } from '../lib/verify.js'
|
|
14
|
+
|
|
15
|
+
const root = join(dirname(fileURLToPath(import.meta.url)), '..')
|
|
16
|
+
const src = process.argv[2] ?? 'C:/Users/11867/Desktop/ppt模板库'
|
|
17
|
+
const work = join(root, 'examples', 'tpl-import-work')
|
|
18
|
+
|
|
19
|
+
const files = (await readdir(src)).filter((f) => /\.pptx$/i.test(f))
|
|
20
|
+
if (!files.length) { console.log('无 .pptx'); process.exit(0) }
|
|
21
|
+
|
|
22
|
+
const results = []
|
|
23
|
+
for (const f of files) {
|
|
24
|
+
const pptx = join(src, f)
|
|
25
|
+
const ws = join(work, f.replace(/\.pptx$/i, ''))
|
|
26
|
+
console.log(`\n===== ${f} =====`)
|
|
27
|
+
try {
|
|
28
|
+
await rm(ws, { recursive: true, force: true })
|
|
29
|
+
await mkdir(ws, { recursive: true })
|
|
30
|
+
const imp = await importPptx(pptx, ws)
|
|
31
|
+
console.log(`import: ${imp.pages} 页 / ${imp.media.length} 媒体 / ${imp.warnings.join(';')}`)
|
|
32
|
+
const name = basename(f, '.pptx')
|
|
33
|
+
const style = /论文|答辩/.test(name) ? '学术答辩' : /总结|汇报/.test(name) ? '工作总结' : /商务/.test(name) ? '商务' : '用户模板'
|
|
34
|
+
const scene = /论文|答辩/.test(name) ? '毕业论文/项目答辩' : /总结|汇报/.test(name) ? '部门汇报/年终总结' : '商务演示'
|
|
35
|
+
const reg = await registerTemplate(ws, { id: slug(name), name, style, scene })
|
|
36
|
+
// 自验:模板工作区可校验 + 渲染(不强制 0 错误——外部导入模板为内容参照,可 autoDeclare 清理)
|
|
37
|
+
const ctx = await resolveDeck(reg.dir)
|
|
38
|
+
const r = await renderDeck(ctx, {})
|
|
39
|
+
const v = verifyDeck(r.layout)
|
|
40
|
+
const errs = v.text.split('\n').filter((l) => l.includes('[✗]')).length
|
|
41
|
+
results.push({ id: reg.id, name, pages: reg.pages, media: imp.media.length, preview: reg.preview ? '✓' : '—', errs })
|
|
42
|
+
console.log(`register: ${reg.id} → ${reg.dir}(${reg.pages} 页母版;verify 概要错误 ${errs} 条 [外部模板为内容参照,可 autoDeclare 清理])`)
|
|
43
|
+
} catch (e) {
|
|
44
|
+
results.push({ id: '-', name: f, error: e?.message })
|
|
45
|
+
console.log(`FAIL: ${e?.message}`)
|
|
46
|
+
} finally {
|
|
47
|
+
await rm(ws, { recursive: true, force: true })
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
await rm(work, { recursive: true, force: true })
|
|
51
|
+
|
|
52
|
+
console.log('\n===== 汇总 =====')
|
|
53
|
+
for (const r of results) console.log(`- ${r.id ?? 'FAIL'}|${r.name}|${r.pages ?? r.error ?? ''} ${r.preview ?? ''} ${r.errs !== undefined ? '|verify错误 ' + r.errs : ''}`)
|
|
54
|
+
const final = await listTemplates()
|
|
55
|
+
console.log(`\n模板库现有 ${final.length} 套:${final.map((t) => t.id).join(', ')}`)
|
|
56
|
+
console.log(`模板库目录:${TEMPLATES_DIR}(源目录 ${src} 现可删除)`)
|
|
57
|
+
|
|
58
|
+
function slug(s) {
|
|
59
|
+
return String(s).toLowerCase().replace(/[^\w\u4e00-\u9fff-]+/g, '-').replace(/^-+|-+$/g, '') || 'template'
|
|
60
|
+
}
|