@topmindspace/tms-skills 0.3.5 → 0.3.9
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 +88 -1
- package/README.en.md +4 -4
- package/README.md +4 -4
- package/bin/tms-skills.js +9 -1
- 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/assets/examples/2026-09-09-architecture-graphite-dark.html +43 -14
- package/top-ppt-html/assets/examples/2026-09-09-presentation-business-blue.html +43 -14
- package/top-ppt-html/assets/examples/2026-09-09-research-mckinsey.html +43 -14
- package/top-ppt-html/assets/examples/2026-09-26-topmind-tms-skills-showcase.html +43 -14
- package/top-ppt-html/assets/pptx-export.js +42 -13
- package/top-ppt-html/assets/style-gallery.html +1 -1
- package/top-ppt-html/assets/templates/architecture.html +45 -15
- package/top-ppt-html/assets/templates/presentation.html +45 -15
- package/top-ppt-html/assets/templates/research.html +45 -15
- package/top-ppt-html/scripts/audit_css.py +1 -1
- package/top-ppt-html/scripts/build_pptx.js +136 -25
- package/top-ppt-html/scripts/layout-constants.json +1 -1
- package/top-ppt-html/scripts/regression.py +3 -3
- package/top-ppt-html/scripts/render_compare.py +1 -1
- package/top-ppt-html/scripts/scaffold_report.py +1 -1
- package/top-ppt-html/scripts/sync_runtime.py +14 -14
- package/top-ppt-html/scripts/validate_report.py +4 -0
- package/topmind-cover/README.en.md +8 -9
- package/topmind-cover/README.md +8 -9
- package/topmind-cover/SKILL.md +28 -8
- package/topmind-cover/assets/examples/README.md +2 -2
- package/topmind-cover/assets/examples/big-type-wechat.png +0 -0
- package/topmind-cover/assets/examples/big-type.png +0 -0
- package/topmind-cover/assets/examples/brand-launch-wechat.png +0 -0
- package/topmind-cover/assets/examples/brand-launch.png +0 -0
- package/topmind-cover/assets/examples/gan-huo-wechat.png +0 -0
- package/topmind-cover/assets/examples/gan-huo.png +0 -0
- package/topmind-cover/assets/examples/ip-fun-wechat.png +0 -0
- package/topmind-cover/assets/examples/ip-fun.png +0 -0
- package/topmind-cover/assets/examples/magazine-wechat.png +0 -0
- package/topmind-cover/assets/examples/magazine.png +0 -0
- package/topmind-cover/assets/examples/minimal-wechat.png +0 -0
- package/topmind-cover/assets/examples/minimal.png +0 -0
- package/topmind-cover/assets/examples/news-flash-wechat.png +0 -0
- package/topmind-cover/assets/examples/news-flash.png +0 -0
- package/topmind-cover/assets/examples/overview.png +0 -0
- package/topmind-cover/assets/examples/tutorial-steps-wechat.png +0 -0
- package/topmind-cover/assets/examples/tutorial-steps.png +0 -0
- package/topmind-cover/references/cover-styles.md +160 -172
- package/topmind-cover/references/impact-language.md +91 -0
- package/topmind-cover/scripts/crop-cover.py +19 -13
- package/topmind-wechat-post/README.en.md +2 -2
- package/topmind-wechat-post/README.md +2 -2
- package/topmind-wechat-post/SKILL.md +1 -1
- package/topmind-wechat-post/scripts/md2wechat.py +12 -3
- package/topmind-x-article/README.en.md +1 -1
- package/topmind-x-article/README.md +1 -1
- package/topmind-x-article/scripts/md2x.py +7 -2
|
@@ -335,6 +335,13 @@ function fitRowH(avail, n, pref, floor) {
|
|
|
335
335
|
return Math.max(lo, Math.min(pref, avail / Math.max(1, n)));
|
|
336
336
|
}
|
|
337
337
|
|
|
338
|
+
/* Exhibit 徽标文本:纯编号补 "Exhibit " 前缀(HTML 为 `Exhibit {no}` + CSS uppercase,视觉同 "EXHIBIT 1");
|
|
339
|
+
调用方已带前缀的不重复 */
|
|
340
|
+
function exhibitBadge(no) {
|
|
341
|
+
const t = String(no == null ? '' : no).trim();
|
|
342
|
+
if (!t) return t;
|
|
343
|
+
return /^exhibit[\s\-:]*/i.test(t) ? t.toUpperCase() : ('EXHIBIT ' + t.toUpperCase());
|
|
344
|
+
}
|
|
338
345
|
/* ══════════ 内容模型 ══════════
|
|
339
346
|
* 从 --model=<file.json> 读取(推荐:由 extract_model.py 从 HTML 报告抽取,
|
|
340
347
|
* 保证 PPTX 与页面同源);未提供时使用下方内嵌 CONTENT 示例。 */
|
|
@@ -835,13 +842,30 @@ function dataTableRows(c) {
|
|
|
835
842
|
function dataTableText(c) { return dataTableRows(c).map(r => r.join(' | ')).join('\n'); }
|
|
836
843
|
|
|
837
844
|
/* 原生图表基础选项(位置由调用点附加) */
|
|
845
|
+
/* 数据标签小数位:按模型数值实际小数位(上限 2 位)生成 format code。
|
|
846
|
+
原 '0"单位"' 会把 1.2 显示成 1(像素级数值失真);无单位且无小数时保持 General。 */
|
|
847
|
+
function chartDecimals(c) {
|
|
848
|
+
let d = 0;
|
|
849
|
+
const scan = (v) => {
|
|
850
|
+
if (typeof v === 'number' && isFinite(v)) {
|
|
851
|
+
const s = String(v), i = s.indexOf('.');
|
|
852
|
+
if (i >= 0) d = Math.max(d, Math.min(2, s.length - i - 1));
|
|
853
|
+
}
|
|
854
|
+
};
|
|
855
|
+
(c.values || []).forEach(scan);
|
|
856
|
+
(c.series || []).forEach(se => (se.values || []).forEach(scan));
|
|
857
|
+
(c.points || []).forEach(p => { if (Array.isArray(p)) p.forEach(scan); else scan(p); });
|
|
858
|
+
(c.start || []).forEach(scan); (c.target || []).forEach(scan);
|
|
859
|
+
return d;
|
|
860
|
+
}
|
|
838
861
|
function chartBaseOpts(c, colors, multi, kind) {
|
|
839
862
|
const isLine = (kind === 'line' || kind === 'area');
|
|
863
|
+
const _dec = chartDecimals(c), _ds = _dec ? '.' + '0'.repeat(_dec) : '';
|
|
840
864
|
const opts = {
|
|
841
865
|
showValue: !isLine && !multi,
|
|
842
866
|
dataLabelPosition: 'outEnd',
|
|
843
867
|
dataLabelColor: STYLE.body, dataLabelFontSize: sz(11.5), dataLabelFontFace: STYLE.font,
|
|
844
|
-
dataLabelFormatCode: c.unit ? '0"' + c.unit + '"' : 'General',
|
|
868
|
+
dataLabelFormatCode: c.unit ? '0' + _ds + '"' + c.unit + '"' : (_dec ? '0' + _ds : 'General'),
|
|
845
869
|
catAxisLabelColor: STYLE.faint, catAxisLabelFontSize: sz(10.5), catAxisLabelFontFace: STYLE.font,
|
|
846
870
|
valAxisHidden: true, valAxisLineShow: false, catAxisLineShow: false,
|
|
847
871
|
catGridLine: { style: 'none' }, valGridLine: { style: 'none' },
|
|
@@ -919,7 +943,19 @@ function nativeChart(s, c, x, y, w, h, dcols) {
|
|
|
919
943
|
}
|
|
920
944
|
const ctype = kind === 'line' ? pptx.ChartType.line
|
|
921
945
|
: kind === 'area' ? pptx.ChartType.area : pptx.ChartType.bar;
|
|
922
|
-
|
|
946
|
+
let cdata = chartSeries(c);
|
|
947
|
+
if (t === 'hbar') {
|
|
948
|
+
/* 条形图(barDir=bar)类目轴自下而上绘制:反转数据使首项居顶,
|
|
949
|
+
与 HTML svg-hbar 及 A 通道 hbarRows 同序(标签-数值配对不变) */
|
|
950
|
+
cdata = cdata.map(se => ({ name: se.name,
|
|
951
|
+
labels: (se.labels || []).slice().reverse(), values: (se.values || []).slice().reverse() }));
|
|
952
|
+
/* 单系列时颜色按数据点分配:颜色跟随数据走,与 A 通道 dcols[j%5] 同归属 */
|
|
953
|
+
if (!multi && colors && colors.length) {
|
|
954
|
+
const _n = Math.max(1, (cdata[0].values || []).length);
|
|
955
|
+
opts.chartColors = (cdata[0].values || []).map((_, i) => colors[(_n - 1 - i) % colors.length]);
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
chartTry(s, ctype, cdata, opts, c);
|
|
923
959
|
}
|
|
924
960
|
/* 散点(X/Y 双系列)/ 气泡({x,y,w} 单系列);labels 解析为 x,values 为 y */
|
|
925
961
|
function xyChart(s, c, pos, kind, colors) {
|
|
@@ -1440,10 +1476,14 @@ function ribbonRects(s, pts, color) {
|
|
|
1440
1476
|
const ca = (a[1] + a[2]) / 2, cb = (b[1] + b[2]) / 2;
|
|
1441
1477
|
const dx = b[0] - a[0], dy = cb - ca;
|
|
1442
1478
|
const len = Math.sqrt(dx * dx + dy * dy);
|
|
1443
|
-
|
|
1479
|
+
/* 厚度取绝对值:sankey 按 [x,上,下]、streamgraph 按 [x,下,上] 传参(y 向下),
|
|
1480
|
+
符号不应影响厚度(原 (a[2]-a[1]) 在 streamgraph 为负 → th 恒 0.02,流带变细线) */
|
|
1481
|
+
const th = Math.max(0.02, Math.abs(((a[1] - a[2]) + (b[1] - b[2])) / 2));
|
|
1444
1482
|
const ang = Math.atan2(dy, dx) * 180 / Math.PI;
|
|
1445
|
-
|
|
1446
|
-
|
|
1483
|
+
/* 相邻 quad 沿走向微叠 0.02in:消除拼接处的抗锯齿发丝缝(streamgraph/sankey 像素级瑕疵) */
|
|
1484
|
+
const ov = 0.02;
|
|
1485
|
+
s.addShape('rect', { x: (a[0] + b[0]) / 2 - len / 2 - ov, y: (ca + cb) / 2 - th / 2,
|
|
1486
|
+
w: Math.max(0.02, len) + ov * 2, h: th, rotate: Math.round(ang * 10) / 10,
|
|
1447
1487
|
fill: { color: color }, line: { type: 'none' } });
|
|
1448
1488
|
}
|
|
1449
1489
|
}
|
|
@@ -1461,7 +1501,8 @@ function infoTableRows(sec) {
|
|
|
1461
1501
|
if (t === 'network') return [['节点', '名称']].concat((sec.nodes || []).map(n => [String(n[0]), String(n[1])]));
|
|
1462
1502
|
if (t === 'marimekko') {
|
|
1463
1503
|
const head = ['列', '总量' + u].concat((sec.legend || []).map(String));
|
|
1464
|
-
return [head].concat((sec.cols || []).map((c, i) => [String(c[0]), String(c[1])]
|
|
1504
|
+
return [head].concat((sec.cols || []).map((c, i) => [String(c[0]), String(c[1])]
|
|
1505
|
+
.concat(marimekkoSegs((sec.cells || [])[i], 99).map(sg => sg.label != null ? sg.label + ' ' + sg.value : String(sg.value)))));
|
|
1465
1506
|
}
|
|
1466
1507
|
if (t === 'streamgraph') {
|
|
1467
1508
|
return [['系列'].concat((sec.labels || []).map(String))]
|
|
@@ -1627,7 +1668,11 @@ function infoBoxplot(s, sec, y0, y1) {
|
|
|
1627
1668
|
fontFace: STYLE.font, fontSize: sz(10.5), color: STYLE.body });
|
|
1628
1669
|
s.addText(String(md) + (sec.unit || ''), { x: cx - step / 2, y: yFor(md) - 0.42, w: step, h: 0.26, align: 'center',
|
|
1629
1670
|
fontFace: STYLE.font, fontSize: sz(10), bold: true, color: STYLE.accent });
|
|
1630
|
-
|
|
1671
|
+
/* min 标签默认在须线帽下方;mn 贴轴时会被组标签带(P.y1-0.42 起)压住 →
|
|
1672
|
+
翻到帽上方,同时避开箱体下沿(几何铁律②:禁叠印) */
|
|
1673
|
+
let mnY = yFor(mn) + 0.03;
|
|
1674
|
+
if (mnY + 0.24 > P.y1 - 0.42) mnY = Math.min(yFor(mn) - 0.28, yFor(q1) - 0.30);
|
|
1675
|
+
s.addText(String(mn) + (sec.unit || ''), { x: cx - step / 2, y: mnY, w: step, h: 0.24, align: 'center',
|
|
1631
1676
|
fontFace: STYLE.font, fontSize: sz(9.5), color: STYLE.faint });
|
|
1632
1677
|
s.addText(String(mx) + (sec.unit || ''), { x: cx - step / 2, y: yFor(mx) - 0.28, w: step, h: 0.24, align: 'center',
|
|
1633
1678
|
fontFace: STYLE.font, fontSize: sz(9.5), color: STYLE.faint });
|
|
@@ -1670,6 +1715,15 @@ function infoNetwork(s, sec, y0, y1) {
|
|
|
1670
1715
|
align: outward ? 'left' : 'right', valign: 'middle', fontFace: STYLE.font, fontSize: sz(10), color: STYLE.body });
|
|
1671
1716
|
});
|
|
1672
1717
|
}
|
|
1718
|
+
/* marimekko 单元格归一化:裸数值 [70,25,5] 或 [标签,数值] 对 [["<50ms",70],…]
|
|
1719
|
+
(与 cols/flows/groups 的 [标签,值] 惯例一致);非数值 → 0,零值段过滤 */
|
|
1720
|
+
function marimekkoSegs(arr, maxSegs) {
|
|
1721
|
+
return ((arr || []).slice(0, maxSegs == null ? 99 : maxSegs)).map(el => {
|
|
1722
|
+
const pair = Array.isArray(el);
|
|
1723
|
+
const v = Number(pair ? el[1] : el);
|
|
1724
|
+
return { label: pair ? String(el[0]) : null, value: isFinite(v) ? v : 0 };
|
|
1725
|
+
}).filter(sg => sg.value > 0);
|
|
1726
|
+
}
|
|
1673
1727
|
/* 马赛克图:列宽按列总量、列高按 100% 构成的双重编码 */
|
|
1674
1728
|
function infoMarimekko(s, sec, y0, y1) {
|
|
1675
1729
|
const G = PT.marimekko;
|
|
@@ -1690,14 +1744,17 @@ function infoMarimekko(s, sec, y0, y1) {
|
|
|
1690
1744
|
let cx0 = MX;
|
|
1691
1745
|
cols.forEach((c, i) => {
|
|
1692
1746
|
const cw = Math.max(0.2, (Number(c[1]) || 0) / total * availW);
|
|
1693
|
-
const
|
|
1694
|
-
const sum =
|
|
1747
|
+
const segs = marimekkoSegs(cells[i], G.maxSegs);
|
|
1748
|
+
const sum = segs.reduce((a, sg) => a + sg.value, 0) || 1;
|
|
1695
1749
|
let cy0 = P.y0;
|
|
1696
|
-
|
|
1750
|
+
segs.forEach((sg, k) => {
|
|
1751
|
+
const v = sg.value;
|
|
1697
1752
|
const sh = Math.max(0.02, v / sum * plotH);
|
|
1698
1753
|
s.addShape('rect', { x: cx0, y: cy0, w: cw, h: sh,
|
|
1699
1754
|
fill: { color: palette[k % palette.length] }, line: { color: STYLE.bg, width: 0.75 } });
|
|
1700
|
-
|
|
1755
|
+
/* 有标签用标签("<50ms"),裸数值沿用旧行为(值 + 单位) */
|
|
1756
|
+
const segLabel = sg.label != null ? sg.label : (String(v) + (sec.unit || ''));
|
|
1757
|
+
s.addText(segLabel, { x: cx0, y: cy0 + sh / 2 - 0.13, w: cw, h: 0.26, align: 'center',
|
|
1701
1758
|
fontFace: STYLE.font, fontSize: sz(10), bold: true, fit: 'shrink',
|
|
1702
1759
|
color: k === 0 ? STYLE.onAccent : STYLE.ink });
|
|
1703
1760
|
cy0 += sh;
|
|
@@ -1792,9 +1849,41 @@ function addTable(s, tbl, x, y, w, availH, opts) {
|
|
|
1792
1849
|
const dense = rowH < 0.42;
|
|
1793
1850
|
const headSz = sz(dense ? 11 : 13), bodySz = sz(dense ? 10 : 12.5);
|
|
1794
1851
|
const hRowH = Math.min(headRowH, Math.max(hardFloor, rowH * 1.15));
|
|
1852
|
+
/* 内容高度预估:逐格用 estTextH 估算,超出行高则全表收字号 ——
|
|
1853
|
+
禁依赖 fit:'shrink' 或渲染引擎自动撑高(PowerPoint/LibreOffice 会按内容增高行,压住脚注) */
|
|
1854
|
+
const ncolsT = Math.max(head.length, 1, ...rowsIn.map(r => r.length));
|
|
1855
|
+
const effColW = (Array.isArray(tbl.colW) && tbl.colW.length >= ncolsT) ? tbl.colW.slice(0, ncolsT)
|
|
1856
|
+
: Array.from({ length: ncolsT }, () => w / ncolsT);
|
|
1857
|
+
const cellPadIn = (dense ? 2 : 5) / 72;
|
|
1858
|
+
const fitCellSz = (text, cw, availHh, maxSz) => fitFont([String(text == null ? '' : text)],
|
|
1859
|
+
Math.max(0.5, cw - cellPadIn * 2), Math.max(0.2, availHh - cellPadIn * 2),
|
|
1860
|
+
{ max: maxSz, maxShrinkSteps: 6 });
|
|
1861
|
+
let bodySzF = bodySz, headSzF = headSz;
|
|
1862
|
+
rowsIn.forEach(r => r.forEach((c, j) => {
|
|
1863
|
+
const f = fitCellSz(c, effColW[j] != null ? effColW[j] : effColW[0], rowH, bodySz);
|
|
1864
|
+
if (f < bodySzF) bodySzF = f;
|
|
1865
|
+
}));
|
|
1866
|
+
head.forEach((h, j) => {
|
|
1867
|
+
const f = fitCellSz(h, effColW[j] != null ? effColW[j] : effColW[0], hRowH, headSz);
|
|
1868
|
+
if (f < headSzF) headSzF = f;
|
|
1869
|
+
});
|
|
1870
|
+
/* 字号触底仍装不下 → 显式告警(内容超承载,建议拆页/精简;行高不再自动撑高) */
|
|
1871
|
+
const stillOver = (text, cw, availHh, f) =>
|
|
1872
|
+
estTextH(String(text == null ? '' : text), Math.max(0.5, cw - cellPadIn * 2), f, TM_LINE) >
|
|
1873
|
+
Math.max(0.2, availHh - cellPadIn * 2);
|
|
1874
|
+
let overCells = 0;
|
|
1875
|
+
rowsIn.forEach(r => r.forEach((c, j) => {
|
|
1876
|
+
if (stillOver(c, effColW[j] != null ? effColW[j] : effColW[0], rowH, bodySzF)) overCells++;
|
|
1877
|
+
}));
|
|
1878
|
+
head.forEach((h, j) => {
|
|
1879
|
+
if (stillOver(h, effColW[j] != null ? effColW[j] : effColW[0], hRowH, headSzF)) overCells++;
|
|
1880
|
+
});
|
|
1881
|
+
if (overCells > 0 && typeof console !== 'undefined')
|
|
1882
|
+
console.warn(`addTable: ${overCells} 个单元格在字号下限(正文 ${bodySzF}pt/表头 ${headSzF}pt)仍超出分配行高 —— ` +
|
|
1883
|
+
'内容超承载,建议拆页/精简文案(行高已锁定,防压脚注)');
|
|
1795
1884
|
const rows = [head.map(h => ({ text: h, options: { bold: true, color: STYLE.onAccent,
|
|
1796
|
-
fill: { color: STYLE.accent }, fontSize:
|
|
1797
|
-
.concat(rowsIn.map(r => r.map(c => ({ text: String(c), options: { fontSize:
|
|
1885
|
+
fill: { color: STYLE.accent }, fontSize: headSzF, fontFace: STYLE.font, valign: 'middle' } }))]
|
|
1886
|
+
.concat(rowsIn.map(r => r.map(c => ({ text: String(c), options: { fontSize: bodySzF,
|
|
1798
1887
|
color: STYLE.body, fontFace: STYLE.font, valign: 'middle', fill: { color: STYLE.bg } } }))));
|
|
1799
1888
|
/* rowH 数组:表头略高、正文均分剩余(pptxgenjs 支持逐行 rowH) */
|
|
1800
1889
|
const rowHeights = [hRowH].concat(rowsIn.map(() => rowH));
|
|
@@ -1885,7 +1974,7 @@ CONTENT.sections.forEach((sec) => {
|
|
|
1885
1974
|
|
|
1886
1975
|
/* 正文区起点 / 终点(含 lead 与 so-what / 来源行 / 待核实条占位) */
|
|
1887
1976
|
/* 非 exhibit 页型也可带 Exhibit 编号(帧头徽标;正文区下移让位,避免与主图重叠) */
|
|
1888
|
-
const exBadge = (type !== 'exhibit' && sec.exhibitNo) ?
|
|
1977
|
+
const exBadge = (type !== 'exhibit' && sec.exhibitNo) ? exhibitBadge(sec.exhibitNo) : null;
|
|
1889
1978
|
const bodyY0 = sec.lead ? PT.common.bodyYWithLead : CONTENT_TOP;
|
|
1890
1979
|
const bodyY = exBadge ? bodyY0 + 0.42 : bodyY0;
|
|
1891
1980
|
if (exBadge) {
|
|
@@ -1896,7 +1985,7 @@ CONTENT.sections.forEach((sec) => {
|
|
|
1896
1985
|
const _F = PT.flagbar || { hdH: 0.26, rowH: 0.24, maxRows: 3, gap: 0.12 };
|
|
1897
1986
|
const flagH = flagItems.length
|
|
1898
1987
|
? (_F.hdH + Math.min(_F.maxRows, flagItems.length) * _F.rowH + 0.06) : 0;
|
|
1899
|
-
let bodyBottom = chartBottom(!!sec.soWhat, !!sec.footnote);
|
|
1988
|
+
let bodyBottom = chartBottom(!!sec.soWhat, !!(sec.footnote || sec.note));
|
|
1900
1989
|
let flagY = 0;
|
|
1901
1990
|
if (flagH) {
|
|
1902
1991
|
/* 待核实条底对齐 contentBottom;与来源行 / so-what 同页时上移让位(不重叠) */
|
|
@@ -1943,7 +2032,7 @@ CONTENT.sections.forEach((sec) => {
|
|
|
1943
2032
|
/* R6:竖分隔线高度取原设计值与注释带感知下界的较小者——写死 3.1 会在
|
|
1944
2033
|
so-what/来源行并存时越过注释带顶(ANNOTATION_BAND_OVERLAP),而无注释时
|
|
1945
2034
|
不得比原设计更长(回归:smoke business-blue fixture slide 7)。 */
|
|
1946
|
-
const kpiBottom = chartBottom(!!sec.soWhat, !!sec.footnote) - 0.05;
|
|
2035
|
+
const kpiBottom = chartBottom(!!sec.soWhat, !!(sec.footnote || sec.note)) - 0.05;
|
|
1947
2036
|
s.addShape('rect', { x: kdivX, y: K.heroY + 0.15, w: 0.025,
|
|
1948
2037
|
h: Math.min(3.1, Math.max(0.6, kpiBottom - (K.heroY + 0.15))),
|
|
1949
2038
|
fill: { color: STYLE.line }, line: { type: 'none' } });
|
|
@@ -1952,9 +2041,15 @@ CONTENT.sections.forEach((sec) => {
|
|
|
1952
2041
|
{ x: kdivX + 0.35, y: K.metricY0, w: CW * (1 - K.dividerX) - 0.5, h: CONTENT_BOTTOM - K.metricY0 };
|
|
1953
2042
|
const kAvail = Math.max(0.8, kpiBottom - mreg.y);
|
|
1954
2043
|
const kmets = sec.metrics || [];
|
|
1955
|
-
|
|
2044
|
+
/* 容量按最小允许行高判断,行高按全部指标自适应压缩 ——
|
|
2045
|
+
原 kCap 用默认行高先截断,导致第 N+1 个指标被静默丢弃(禁静默丢数据) */
|
|
2046
|
+
const K_MINROWH = 0.42;
|
|
2047
|
+
const kCap = Math.max(1, Math.floor(kAvail / K_MINROWH));
|
|
1956
2048
|
const kRows = Math.min(kmets.length, kCap);
|
|
1957
|
-
|
|
2049
|
+
if (kRows < kmets.length)
|
|
2050
|
+
console.warn(`[build_pptx] 警告:KPI 页「${sec.title || ''}」支撑指标 ${kmets.length} 个超出容量(${kAvail.toFixed(2)}in),` +
|
|
2051
|
+
`仅渲染前 ${kRows} 个 —— 建议拆页`);
|
|
2052
|
+
const kRowH = fitRowH(kAvail, kRows, K.metricRowH, K_MINROWH);
|
|
1958
2053
|
kmets.slice(0, kRows).map(metricKV).forEach((m, i) => {
|
|
1959
2054
|
const ky = mreg.y + i * kRowH;
|
|
1960
2055
|
s.addText(m[0], { x: mreg.x, y: ky, w: mreg.w, h: Math.min(0.55, kRowH * 0.62),
|
|
@@ -1963,11 +2058,13 @@ CONTENT.sections.forEach((sec) => {
|
|
|
1963
2058
|
h: Math.max(0.24, kRowH - Math.min(0.55, kRowH * 0.62)), fontFace: STYLE.font,
|
|
1964
2059
|
fontSize: sz(10.5), color: STYLE.faint });
|
|
1965
2060
|
});
|
|
1966
|
-
/* 要点:指标行下方续排,k/v 双色(与 bar
|
|
2061
|
+
/* 要点:指标行下方续排,k/v 双色(与 bar 侧栏同口径);容量不足时显式告警(禁静默丢数据) */
|
|
1967
2062
|
const kpts = (sec.points || []).filter(Boolean);
|
|
1968
2063
|
const kpRowH = 0.5;
|
|
1969
2064
|
const kpY0 = mreg.y + kRows * kRowH + 0.12;
|
|
1970
2065
|
const kpCap = Math.max(0, Math.floor((kpiBottom - kpY0) / kpRowH));
|
|
2066
|
+
if (kpts.length > kpCap)
|
|
2067
|
+
console.warn(`[build_pptx] 警告:KPI 页「${sec.title || ''}」要点 ${kpts.length} 条超出容量,仅渲染前 ${kpCap} 条 —— 建议拆页`);
|
|
1971
2068
|
kpts.slice(0, kpCap).forEach((p, i) => {
|
|
1972
2069
|
const kv = pointKV(p), kk = kv[0], vv = kv[1];
|
|
1973
2070
|
const ky = kpY0 + i * kpRowH;
|
|
@@ -2225,10 +2322,24 @@ CONTENT.sections.forEach((sec) => {
|
|
|
2225
2322
|
s.addShape('roundRect', { x, y, w, h: rowH, rectRadius: 0.07,
|
|
2226
2323
|
fill: { color: acc ? STYLE.soft : STYLE.surface },
|
|
2227
2324
|
line: acc ? { type: 'none' } : { color: STYLE.line, width: 0.75 } });
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2325
|
+
/* 标题列宽按层内宽比例分配(labelFrac≈0.3);顶层窄时固定 3.2in 会把说明压成竖列叠印 →
|
|
2326
|
+
改为标题上、说明下的单框混排(与 HTML .pyr__t/.pyr__d 同语义) */
|
|
2327
|
+
const lwP = Math.min(P.labelW, (w - 0.44) * (P.labelFrac == null ? 0.32 : P.labelFrac));
|
|
2328
|
+
const descW = w - 0.44 - lwP;
|
|
2329
|
+
/* 窄层判定:说明列 < 1.2in,或标题在标题列内放不下单行(会中部换行)→
|
|
2330
|
+
标题上、说明下单框混排(与 HTML 窄屏 .pyr__lvl 单列同语义) */
|
|
2331
|
+
const narrowP = descW < 1.2 || (ld && estLines(lt, lwP, 13.5) > 1) || (!ld && estLines(lt, w - 0.44, 13.5) > 1);
|
|
2332
|
+
if (narrowP) {
|
|
2333
|
+
s.addText(lt, { x: x + 0.22, y: y + 0.03, w: w - 0.44, h: 0.30, valign: 'middle',
|
|
2334
|
+
fontFace: STYLE.font, fontSize: sz(12.5), bold: true, color: acc ? STYLE.accent : STYLE.ink, fit: 'shrink' });
|
|
2335
|
+
s.addText(ld, { x: x + 0.22, y: y + 0.33, w: w - 0.44, h: Math.max(0.2, rowH - 0.36), valign: 'middle',
|
|
2336
|
+
fontFace: STYLE.font, fontSize: sz(10.5), color: STYLE.faint, fit: 'shrink' });
|
|
2337
|
+
} else {
|
|
2338
|
+
s.addText(lt, { x: x + 0.22, y, w: lwP, h: rowH, valign: 'middle',
|
|
2339
|
+
fontFace: STYLE.font, fontSize: sz(13.5), bold: true, color: acc ? STYLE.accent : STYLE.ink, fit: 'shrink' });
|
|
2340
|
+
if (ld) s.addText(ld, { x: x + 0.22 + lwP, y, w: descW, h: rowH,
|
|
2341
|
+
valign: 'middle', fontFace: STYLE.font, fontSize: sz(11), color: STYLE.faint, fit: 'shrink' });
|
|
2342
|
+
}
|
|
2232
2343
|
});
|
|
2233
2344
|
} else if (type === 'steps') {
|
|
2234
2345
|
/* 步骤条:N 步横排 + 箭头;超 maxPerRow 折行;groups 分组标签 */
|
|
@@ -2644,12 +2755,12 @@ CONTENT.sections.forEach((sec) => {
|
|
|
2644
2755
|
const reg = REGIONS.regionOf('exhibit', 'primary', { withNote: !!(sec.soWhat || sec.footnote) })
|
|
2645
2756
|
|| { x: MX, y: PT.exhibit.chartY, w: CW, h: PT.exhibit.chartH };
|
|
2646
2757
|
if (sec.exhibitNo) {
|
|
2647
|
-
s.addText(
|
|
2758
|
+
s.addText(exhibitBadge(sec.exhibitNo), { x: MX, y: PT.exhibit.badgeY, w: CW, h: 0.35,
|
|
2648
2759
|
fontFace: STYLE.font, fontSize: sz(11.5), bold: true, color: STYLE.accent, charSpacing: 2 });
|
|
2649
2760
|
}
|
|
2650
2761
|
if (ec.labels && ec.values) {
|
|
2651
2762
|
const edcols = ec.colors || dataColors(styleArg);
|
|
2652
|
-
const h = Math.max(1.2, chartBottom(!!sec.soWhat, !!sec.footnote) - reg.y - 0.05);
|
|
2763
|
+
const h = Math.max(1.2, chartBottom(!!sec.soWhat, !!(sec.footnote || sec.note)) - reg.y - 0.05);
|
|
2653
2764
|
if (ec.type === 'hbar') {
|
|
2654
2765
|
chartBlock(s, ec, reg.x, reg.y, reg.w, h, edcols);
|
|
2655
2766
|
} else {
|
|
@@ -64,10 +64,10 @@ def main() -> int:
|
|
|
64
64
|
node = find_node()
|
|
65
65
|
node_path = find_node_modules()
|
|
66
66
|
if not node:
|
|
67
|
-
print('
|
|
67
|
+
print('错误: ' + NODE_HINT)
|
|
68
68
|
return 2
|
|
69
69
|
if not node_path:
|
|
70
|
-
print('
|
|
70
|
+
print('错误: ' + NODE_MODULES_HINT)
|
|
71
71
|
return 2
|
|
72
72
|
env = node_env(node_path)
|
|
73
73
|
print(f'Node: {node}')
|
|
@@ -82,7 +82,7 @@ def main() -> int:
|
|
|
82
82
|
if args.only:
|
|
83
83
|
examples = [h for h in examples if args.only in h.stem]
|
|
84
84
|
if not examples:
|
|
85
|
-
print('
|
|
85
|
+
print('错误: assets/examples/ 下没有可回归的示例。')
|
|
86
86
|
return 2
|
|
87
87
|
|
|
88
88
|
for html in examples:
|
|
@@ -211,7 +211,7 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
211
211
|
|
|
212
212
|
pptx = Path(args.pptx).resolve()
|
|
213
213
|
if not pptx.exists():
|
|
214
|
-
print(f'
|
|
214
|
+
print(f'错误: 找不到 {pptx}')
|
|
215
215
|
return 2
|
|
216
216
|
out = Path(args.out).resolve() if args.out else pptx.parent / 'render-compare'
|
|
217
217
|
out.mkdir(parents=True, exist_ok=True)
|
|
@@ -113,7 +113,7 @@ CHART_CYCLE = CHART_CORE # 轮换默认序列(骨架示范「图要选对」
|
|
|
113
113
|
_SCAFFOLD = (LC.get('charts') or {}).get('scaffold') or {}
|
|
114
114
|
if not _SCAFFOLD.get('viewBoxHeight') or not _SCAFFOLD.get('infoTypes'):
|
|
115
115
|
# 启动自检:单源缺键时立刻失败,防「静默退回默认值 180」的窗口期
|
|
116
|
-
print('
|
|
116
|
+
print('错误:layout-constants.json 缺 charts.scaffold.viewBoxHeight/infoTypes'
|
|
117
117
|
'(骨架尺寸单源不完整;补单源后先跑 sync_runtime.py)', file=sys.stderr)
|
|
118
118
|
sys.exit(2)
|
|
119
119
|
CHART_VIEWBOX = dict(_SCAFFOLD.get('viewBoxHeight') or {})
|
|
@@ -301,10 +301,10 @@ def inject(path: Path, pat: re.Pattern, repl: str, label: str) -> bool:
|
|
|
301
301
|
try:
|
|
302
302
|
src = path.read_text(encoding='utf-8')
|
|
303
303
|
except OSError as e:
|
|
304
|
-
print(f'
|
|
304
|
+
print(f'错误: 无法读取 {path.name}: {e}')
|
|
305
305
|
return False
|
|
306
306
|
if not pat.search(src):
|
|
307
|
-
print(f'
|
|
307
|
+
print(f'错误: {path.name} 缺少 {label} 标记块')
|
|
308
308
|
return False
|
|
309
309
|
path.write_text(pat.sub(lambda m: repl, src, count=1), encoding='utf-8')
|
|
310
310
|
return True
|
|
@@ -312,32 +312,32 @@ def inject(path: Path, pat: re.Pattern, repl: str, label: str) -> bool:
|
|
|
312
312
|
|
|
313
313
|
def main() -> int:
|
|
314
314
|
if not LC.exists():
|
|
315
|
-
print(f'
|
|
315
|
+
print(f'错误: 找不到 {LC}')
|
|
316
316
|
return 2
|
|
317
317
|
try:
|
|
318
318
|
lc = json.loads(LC.read_text(encoding='utf-8'))
|
|
319
319
|
except UnicodeDecodeError as e:
|
|
320
|
-
print(f'
|
|
320
|
+
print(f'错误: {LC.name} 不是合法 UTF-8: {e}')
|
|
321
321
|
return 2
|
|
322
322
|
except json.JSONDecodeError as e:
|
|
323
|
-
print(f'
|
|
323
|
+
print(f'错误: {LC.name} 不是合法 JSON: {e}')
|
|
324
324
|
return 2
|
|
325
325
|
except OSError as e:
|
|
326
|
-
print(f'
|
|
326
|
+
print(f'错误: 无法读取 {LC}: {e}')
|
|
327
327
|
return 2
|
|
328
328
|
if not MS.exists():
|
|
329
|
-
print(f'
|
|
329
|
+
print(f'错误: 找不到 {MS}(DSL schema 单源)')
|
|
330
330
|
return 2
|
|
331
331
|
try:
|
|
332
332
|
ms = json.loads(MS.read_text(encoding='utf-8'))
|
|
333
333
|
except UnicodeDecodeError as e:
|
|
334
|
-
print(f'
|
|
334
|
+
print(f'错误: {MS.name} 不是合法 UTF-8: {e}')
|
|
335
335
|
return 2
|
|
336
336
|
except json.JSONDecodeError as e:
|
|
337
|
-
print(f'
|
|
337
|
+
print(f'错误: {MS.name} 不是合法 JSON: {e}')
|
|
338
338
|
return 2
|
|
339
339
|
except OSError as e:
|
|
340
|
-
print(f'
|
|
340
|
+
print(f'错误: 无法读取 {MS}: {e}')
|
|
341
341
|
return 2
|
|
342
342
|
ok = True
|
|
343
343
|
|
|
@@ -345,13 +345,13 @@ def main() -> int:
|
|
|
345
345
|
try:
|
|
346
346
|
src = PE.read_text(encoding='utf-8')
|
|
347
347
|
except OSError as e:
|
|
348
|
-
print(f'
|
|
348
|
+
print(f'错误: 无法读取 {PE}: {e}')
|
|
349
349
|
return 1
|
|
350
350
|
if not CONST_PAT.search(src):
|
|
351
|
-
print('
|
|
351
|
+
print('错误: assets/pptx-export.js 缺少 __TOPPPT_CONSTANTS__ 标记块')
|
|
352
352
|
return 1
|
|
353
353
|
if not SCHEMA_PAT.search(src):
|
|
354
|
-
print('
|
|
354
|
+
print('错误: assets/pptx-export.js 缺少 __TOPPPT_SCHEMA__ 标记块')
|
|
355
355
|
return 1
|
|
356
356
|
block = build_const_block(lc)
|
|
357
357
|
PE.write_text(CONST_PAT.sub(lambda m: block, src), encoding='utf-8')
|
|
@@ -370,7 +370,7 @@ def main() -> int:
|
|
|
370
370
|
|
|
371
371
|
# ② 公共片段源
|
|
372
372
|
if not ENGINE.exists() or not UI.exists():
|
|
373
|
-
print(f'
|
|
373
|
+
print(f'错误: 缺少公共片段 {ENGINE} / {UI}')
|
|
374
374
|
return 1
|
|
375
375
|
engine_css = ENGINE.read_text(encoding='utf-8').rstrip('\n')
|
|
376
376
|
ui_js = UI.read_text(encoding='utf-8').rstrip('\n')
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
TopPPT HTML· HTML 报告质量校验(交付闭环)
|
|
5
5
|
用法:
|
|
6
6
|
python validate_report.py <报告.html> [--strict] [--layout-qa] [--json]
|
|
7
|
+
python validate_report.py --help
|
|
7
8
|
|
|
8
9
|
输出每项 PASS/FAIL,结尾给汇总与结论;任一 FAIL 时退出码为 1(--strict 时 WARN 也计失败)。
|
|
9
10
|
--json 以 JSON 输出全部检查结果(供脚本/流水线读取)。
|
|
@@ -1630,6 +1631,9 @@ def main():
|
|
|
1630
1631
|
if len(sys.argv) < 2:
|
|
1631
1632
|
print(__doc__)
|
|
1632
1633
|
return 2
|
|
1634
|
+
if sys.argv[1] in ('-h', '--help'):
|
|
1635
|
+
print(__doc__)
|
|
1636
|
+
return 0
|
|
1633
1637
|
path = Path(sys.argv[1])
|
|
1634
1638
|
strict = '--strict' in sys.argv
|
|
1635
1639
|
as_json = '--json' in sys.argv
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
A reusable skill for generating cover art for X long-form posts and WeChat articles. Goal: **striking, eye-catching, theme-focused**.
|
|
6
6
|
|
|
7
|
-
- Version: **v0.1.0** (same tag as `@topmindspace/tms-skills@0.3.
|
|
7
|
+
- Version: **v0.1.0** (same tag as `@topmindspace/tms-skills@0.3.9`)
|
|
8
8
|
|
|
9
9
|
## Style gallery
|
|
10
10
|
|
|
@@ -22,14 +22,13 @@ npx @topmindspace/tms-skills install topmind-cover
|
|
|
22
22
|
|
|
23
23
|
## Usage
|
|
24
24
|
|
|
25
|
-
0. **Pick a style
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
4. Crop and save:
|
|
25
|
+
0. **Pick a style**: choose 1 of 8 styles from `references/cover-styles.md` by topic → check the example image to confirm the visual language.
|
|
26
|
+
1. **Refine the title**: lock the title copy first (usually ≤ 10 chars) using that style's title rules; rewrite until it lands — no drawing yet.
|
|
27
|
+
2. **Composition brief**: one-sentence brief (theme / audience / mood / visual metaphor / palette direction) before composing the prompt.
|
|
28
|
+
3. Input: article title + 3 theme keywords + platform (x / wechat / both, default both).
|
|
29
|
+
4. Generate with the agent's image-generation capability (landscape 16:9, 8% margin on all sides).
|
|
30
|
+
5. **Impact self-check** (required): pass every item in the [impact self-check list](./SKILL.md#冲击力自检清单); on failure, tweak the prompt and regenerate, max 3 attempts.
|
|
31
|
+
6. Crop and save:
|
|
33
32
|
|
|
34
33
|
```bash
|
|
35
34
|
python3 scripts/crop-cover.py <main-image> --out-dir <package>/images/
|
package/topmind-cover/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
为 X 长文和公众号文章生成封面图的可复用技能。目标:**震撼、醒目、主题突出**。
|
|
6
6
|
|
|
7
|
-
- 版本:**v0.1.0**(与 `@topmindspace/tms-skills@0.3.
|
|
7
|
+
- 版本:**v0.1.0**(与 `@topmindspace/tms-skills@0.3.9` 同 tag)
|
|
8
8
|
|
|
9
9
|
## 风格样张
|
|
10
10
|
|
|
@@ -22,14 +22,13 @@ npx @topmindspace/tms-skills install topmind-cover
|
|
|
22
22
|
|
|
23
23
|
## 用法
|
|
24
24
|
|
|
25
|
-
0.
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
4. 裁剪落盘:
|
|
25
|
+
0. **选风格**:按题材从 `references/cover-styles.md` 选 1 种(8 选 1)→ 看示例图确认视觉语言。
|
|
26
|
+
1. **标题提炼**:先定标题文案(一般 ≤10 字),用该风格章节的标题写法规范;不满意重写,不先画图。
|
|
27
|
+
2. **构图简报**:一句话 brief(主题/受众/情绪/视觉隐喻/配色方向),组 prompt 前先写。
|
|
28
|
+
3. 输入文章标题 + 3 个主题关键词 + 平台(x / wechat / both,默认 both)。
|
|
29
|
+
4. 用 agent 的图片生成能力出图(横构图 16:9,四周留白 8%)。
|
|
30
|
+
5. **冲击力自检**(必做):按 [SKILL.md 冲击力自检清单](./SKILL.md#冲击力自检清单) 逐项过;不通过→调 prompt 重生成,最多 3 次。
|
|
31
|
+
6. 裁剪落盘:
|
|
33
32
|
|
|
34
33
|
```bash
|
|
35
34
|
python3 scripts/crop-cover.py <主图> --out-dir <包>/images/
|
package/topmind-cover/SKILL.md
CHANGED
|
@@ -31,7 +31,7 @@ updated: 2026-09-29
|
|
|
31
31
|
为 X 长文和公众号文章生成封面图。目标:**震撼、醒目、主题突出**。
|
|
32
32
|
|
|
33
33
|
```
|
|
34
|
-
|
|
34
|
+
标题提炼 → 构图简报 → 风格模板 → 生成 → 冲击力自检 → 裁剪双尺寸 → 落盘命名
|
|
35
35
|
```
|
|
36
36
|
|
|
37
37
|
## 风格索引
|
|
@@ -64,18 +64,38 @@ updated: 2026-09-29
|
|
|
64
64
|
|
|
65
65
|
## 工作流
|
|
66
66
|
|
|
67
|
-
0. **选风格(三步,必做)**:① 按题材从 `references/cover-styles.md` 选 1 种(8 选 1)→ ② 看 `assets/examples/<style>.png` 确认视觉语言 → ③
|
|
68
|
-
1.
|
|
69
|
-
2.
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
67
|
+
0. **选风格(三步,必做)**:① 按题材从 `references/cover-styles.md` 选 1 种(8 选 1)→ ② 看 `assets/examples/<style>.png` 确认视觉语言 → ③ 看该风格章节的标题写法规范,心里有标题雏形。
|
|
68
|
+
1. **标题提炼**(先定文案,不画图):用所选风格在 `cover-styles.md` 对应章节的标题写法规范提炼标题(一般 ≤10 字);不满意就重写,满意再进下一步。
|
|
69
|
+
- 坏:"关于 Manus 2.0 的一些思考与体验"(长、没钩子)→ 好:"Manus 2.0 实测"(短、有断言)
|
|
70
|
+
2. **构图简报**(一句话 brief,组 prompt 前先写):主题 + 受众 + 情绪 + 视觉隐喻 + 配色方向。
|
|
71
|
+
- 例:"给 AI 从业者看的 Manus 2.0 实测封面,情绪'震撼但可信',视觉隐喻是破壳而出的机械手,深空黑底 + 电光蓝强调。"
|
|
72
|
+
3. **输入与组 prompt**:用步骤 1 定下的标题 + 3 个主题关键词 + 平台(`x` / `wechat` / `both`,默认 both;平台只影响 prompt 侧重,裁剪落盘永远产出双尺寸)。按风格配方组装 prompt,标题逐字写明(防 AI 自造错别字)。
|
|
73
|
+
4. **生成**:`media.generate_image`,`name` 用 `<slug>-cover`。
|
|
74
|
+
5. **冲击力自检**(必做):必过下方"冲击力自检清单" 10 项;不通过→调整 prompt 重生成,最多 3 次;3 次仍不过→换一种风格,或回步骤 1 重写标题。
|
|
75
|
+
6. **裁剪落盘**:
|
|
73
76
|
```bash
|
|
74
77
|
python3 scripts/crop-cover.py <主图> --out-dir <包>/images/
|
|
75
78
|
```
|
|
76
79
|
产出 `00-封面.png`(1200×675,X/公众号共用主文件)与 `00-封面-公众号.png`(900×383)。
|
|
77
80
|
公众号包规约:正文引用 `images/00-封面.png`,`图片上传清单.md` 登记两版。
|
|
78
|
-
|
|
81
|
+
7. **系列感**:同一系列固定色系 + 版式语言,prompt 复用同一风格模板。
|
|
82
|
+
|
|
83
|
+
## 冲击力自检清单
|
|
84
|
+
|
|
85
|
+
**生成后必过**:不通过→调整 prompt 重生成,最多 3 次;3 次仍不过→换风格或重写标题。
|
|
86
|
+
|
|
87
|
+
| # | 检查项 | 不通过怎么改 |
|
|
88
|
+
|---|--------|--------------|
|
|
89
|
+
| 1 | 3 秒测试:缩到 120px 宽,3 秒内读出标题并说清"这是什么" | 放大标题,删小字 |
|
|
90
|
+
| 2 | 标题字高 ≥ 画面高度 1/4 | 放大标题,其他元素让路 |
|
|
91
|
+
| 3 | 对比果断:字色与底色明度差拉满(拒绝灰底白字式低对比) | 底色走极端(极深或极浅) |
|
|
92
|
+
| 4 | 记忆点:3 秒后能向人描述出一个具体的东西 | 加一个形象/数字/符号/质感记忆点 |
|
|
93
|
+
| 5 | 质感:背景有纹理、光影或描边,拒绝纯色死平 | 背景层加一层纸纹/噪点/光影 |
|
|
94
|
+
| 6 | 标题字逐字正确,无 AI 自造错别字 | 修正标题写法、缩短标题后重生成 |
|
|
95
|
+
| 7 | 标题与关键主体落在中央垂直 60% 安全区(900×383 裁剪不切字) | 把元素收拢进安全区 |
|
|
96
|
+
| 8 | `references/impact-language.md` 20 条原则快速勾选 | 按"保守陷阱清单"逐项纠正 |
|
|
97
|
+
| 9 | 中文逐字正确(装饰未导致错字、乱码、笔画粘连) | 有错字/粘连→减装饰或缩短标题后重生成 |
|
|
98
|
+
| 10 | 装饰手法 ≤ 该风格指定数量(多了即乱;minimal 只许 1 种) | 超量→删掉最弱的一种装饰再生成 |
|
|
79
99
|
|
|
80
100
|
## 设计铁律
|
|
81
101
|
|
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
|
|
16
16
|
本目录 16 张示例图均为**原创设计**(只演示抽象设计原则,不临摹任何第三方封面的
|
|
17
17
|
版式与配色;复用本库设计时亦不得与第三方封面构成实质相似——只学原则,不学版式)。
|
|
18
|
-
图中标题文案、数字、品牌名、署名均为**虚构演示内容**(如"
|
|
19
|
-
"
|
|
18
|
+
图中标题文案、数字、品牌名、署名均为**虚构演示内容**(如"破晓 6.0"、
|
|
19
|
+
"@老周带单")——直接复用前请换成自己的真实标题;凡封面上的数字必须在正文中有
|
|
20
20
|
出处,严禁编造数据。
|
|
21
21
|
|
|
22
22
|
## 复用声明
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|