
Retro China 80s Ppt
- 1 installs
- Updated June 11, 2026
- yabaa233/skill_study
Generates .pptx presentations in a 1980s-China nostalgic visual style with four templates: old textbook, award certificate, quotation book, and calendar.
About
Turns a title and slide outline into a real .pptx file styled with retro Chinese 1980s aesthetics (enamel-red/beige/green palette, SimSun font, seal titles). A developer uses it for nostalgic-themed decks like annual reviews, birthday certificates, or retro-aesthetic slides.
- Four templates: textbook, award, quotation, calendar
- Outputs a real .pptx openable in PowerPoint/WPS/Keynote
Retro China 80s Ppt by the numbers
- 1 all-time installs (skills.sh)
- Ranked #565 of 688 Office & Documents skills by installs in the Skillselion catalog
- Data as of Jul 8, 2026 (Skillselion catalog sync)
npx skills add https://github.com/yabaa233/skill_study --skill retro-china-80s-pptAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1 |
|---|---|
| Last updated | June 11, 2026 |
| Repository | yabaa233/skill_study ↗ |
What it does
Generates .pptx presentations in a 1980s-China nostalgic visual style with four templates: old textbook, award certificate, quotation book, and calendar.
Files
Retro China 80s PPT — 中国 80 年代怀旧风格 PPT 📜
一个把普通大纲变成年代感十足的怀旧 PPT 的工具。
使用场景
何时应调用本工具:用户明确要求"做 PPT / 生成演示文稿 / 写汇报"并且带有以下关键词:
- 80 年代、怀旧、复古、老课本、大字报、奖状、喜报、红双喜、挂历、语录
- 年代感、复古风、民国风(可用老课本模板近似)
不该调用的情况:用户只想聊天、只想要大纲文字、或要求 "现代简约 PPT"。
示例对话
1. 明确风格
- 用户:
"帮我做个80年代风格的 PPT,主题是我们年会回顾" - 调用:
{
"title": "1988 我们的年会",
"template": "textbook",
"slides": [
{ "title": "开篇寄语", "bullets": ["光阴似箭", "岁月如歌", "回望来时路"] },
{ "title": "今年成就", "bullets": ["营收翻一番", "三项专利", "新开两个分厂"] }
]
}2. 奖状模板
- 用户:
"给我闺女做张生日奖状 PPT,叫她年度最佳女儿" - 调用:template =
"award",slides 含表彰词。
3. 语录本
- 用户:
"做一页红色语录本的 PPT,写几条程序员名言" - 调用:template =
"quotation"。
4. 挂历
- 用户:
"做一个挂历风的年度回顾 PPT" - 调用:template =
"calendar"。
LLM 使用指南(重要)
1. 内容组织是 LLM 的工作:用户通常只给主题 / 方向,LLM 需要:
- 构思 4-8 张幻灯片的结构(开篇 → 正文 → 总结)
- 每页标题 4-10 字为宜(和老课本视觉匹配)
- 每页 3-6 条 bullets,每条不超过 20 字(避免挤爆版面)
2. 模板选择:
- 正式汇报 / 课堂分享 →
textbook - 庆祝 / 表彰 / 祝福 →
award - 名言 / 格言 / 语录集 →
quotation - 年度回顾 / 日期相关 →
calendar
3. 文件输出后:
- 工具返回
filePath后,主动告诉用户文件已保存到哪里 - 建议用户用 PowerPoint / WPS / Keynote 打开
- 可附加一句怀旧风打趣话
注意事项
- 输出真实的
.pptx二进制文件,可直接用 PowerPoint 打开 - 幻灯片数量建议 3-12 张,过多会让文件臃肿
- 所有文字使用宋体(SimSun / Songti SC),英文系统也能降级显示
- 文件默认存到 Skill 的
dataDir;如需指定路径,传outputPath - 生成属于 MODERATE 风险(写磁盘文件),需用户确认
模板视觉说明
| 模板 | 配色 | 关键元素 |
|---|---|---|
textbook | 宣纸米黄 + 墨黑 + 朱砂红 | 课本边框、页码、方格背景、红色批注印章 |
award | 奖状金 + 搪瓷红 | 红双喜 / 奖状波浪边、烫金标题、大红印章 |
quotation | 正红底 + 黄字 | 语录本大红封面、毛体风标题、落款 |
calendar | 米黄底 + 墨绿 + 红 | 挂历撕页感、日期方块、节气提示 |
# 依赖 & 构建产物
node_modules/
# 注意:scripts/*.bundle.cjs 必须提交(运行时入口),不要加进来
# 测试输出
out/
/**
* Build script — bundle src/retro-ppt.cjs 为自包含的 scripts/retro-ppt.bundle.cjs
*
* 使用:
* npm install
* node build.js
*/
const { buildSync } = require('esbuild');
const path = require('path');
buildSync({
entryPoints: [path.join(__dirname, 'src', 'retro-ppt.cjs')],
bundle: true,
platform: 'node',
format: 'cjs',
target: 'node16',
minify: true,
outfile: path.join(__dirname, 'scripts', 'retro-ppt.bundle.cjs'),
logLevel: 'info',
});
console.log('[retro-china-80s-ppt] ✅ Build done → scripts/retro-ppt.bundle.cjs');
{
"name": "retro-china-80s-ppt",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "retro-china-80s-ppt",
"version": "1.0.0",
"dependencies": {
"pptxgenjs": "^3.12.0"
},
"devDependencies": {
"esbuild": "^0.20.0"
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz",
"integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"aix"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz",
"integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz",
"integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz",
"integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz",
"integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz",
"integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz",
"integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz",
"integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz",
"integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz",
"integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz",
"integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz",
"integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz",
"integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==",
"cpu": [
"mips64el"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz",
"integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz",
"integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz",
"integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==",
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz",
"integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz",
"integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz",
"integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz",
"integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz",
"integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz",
"integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz",
"integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@types/node": {
"version": "18.19.130",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz",
"integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==",
"license": "MIT",
"dependencies": {
"undici-types": "~5.26.4"
}
},
"node_modules/core-util-is": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
"license": "MIT"
},
"node_modules/esbuild": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz",
"integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=12"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.20.2",
"@esbuild/android-arm": "0.20.2",
"@esbuild/android-arm64": "0.20.2",
"@esbuild/android-x64": "0.20.2",
"@esbuild/darwin-arm64": "0.20.2",
"@esbuild/darwin-x64": "0.20.2",
"@esbuild/freebsd-arm64": "0.20.2",
"@esbuild/freebsd-x64": "0.20.2",
"@esbuild/linux-arm": "0.20.2",
"@esbuild/linux-arm64": "0.20.2",
"@esbuild/linux-ia32": "0.20.2",
"@esbuild/linux-loong64": "0.20.2",
"@esbuild/linux-mips64el": "0.20.2",
"@esbuild/linux-ppc64": "0.20.2",
"@esbuild/linux-riscv64": "0.20.2",
"@esbuild/linux-s390x": "0.20.2",
"@esbuild/linux-x64": "0.20.2",
"@esbuild/netbsd-x64": "0.20.2",
"@esbuild/openbsd-x64": "0.20.2",
"@esbuild/sunos-x64": "0.20.2",
"@esbuild/win32-arm64": "0.20.2",
"@esbuild/win32-ia32": "0.20.2",
"@esbuild/win32-x64": "0.20.2"
}
},
"node_modules/https": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/https/-/https-1.0.0.tgz",
"integrity": "sha512-4EC57ddXrkaF0x83Oj8sM6SLQHAWXw90Skqu2M4AEWENZ3F02dFJE/GARA8igO79tcgYqGrD7ae4f5L3um2lgg==",
"license": "ISC"
},
"node_modules/image-size": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/image-size/-/image-size-1.2.1.tgz",
"integrity": "sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==",
"license": "MIT",
"dependencies": {
"queue": "6.0.2"
},
"bin": {
"image-size": "bin/image-size.js"
},
"engines": {
"node": ">=16.x"
}
},
"node_modules/immediate": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
"license": "MIT"
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"license": "ISC"
},
"node_modules/isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
"license": "MIT"
},
"node_modules/jszip": {
"version": "3.10.1",
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
"integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==",
"license": "(MIT OR GPL-3.0-or-later)",
"dependencies": {
"lie": "~3.3.0",
"pako": "~1.0.2",
"readable-stream": "~2.3.6",
"setimmediate": "^1.0.5"
}
},
"node_modules/lie": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
"integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
"license": "MIT",
"dependencies": {
"immediate": "~3.0.5"
}
},
"node_modules/pako": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
"license": "(MIT AND Zlib)"
},
"node_modules/pptxgenjs": {
"version": "3.12.0",
"resolved": "https://registry.npmjs.org/pptxgenjs/-/pptxgenjs-3.12.0.tgz",
"integrity": "sha512-ZozkYKWb1MoPR4ucw3/aFYlHkVIJxo9czikEclcUVnS4Iw/M+r+TEwdlB3fyAWO9JY1USxJDt0Y0/r15IR/RUA==",
"license": "MIT",
"dependencies": {
"@types/node": "^18.7.3",
"https": "^1.0.0",
"image-size": "^1.0.0",
"jszip": "^3.7.1"
}
},
"node_modules/process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
"license": "MIT"
},
"node_modules/queue": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz",
"integrity": "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==",
"license": "MIT",
"dependencies": {
"inherits": "~2.0.3"
}
},
"node_modules/readable-stream": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
"license": "MIT",
"dependencies": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
}
},
"node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"license": "MIT"
},
"node_modules/setimmediate": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
"integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==",
"license": "MIT"
},
"node_modules/string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"license": "MIT",
"dependencies": {
"safe-buffer": "~5.1.0"
}
},
"node_modules/undici-types": {
"version": "5.26.5",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
"license": "MIT"
},
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"license": "MIT"
}
}
}
{
"name": "retro-china-80s-ppt",
"version": "1.0.0",
"private": true,
"description": "Retro China 80s PPT generator skill",
"scripts": {
"build": "node build.js",
"test": "node test.cjs"
},
"dependencies": {
"pptxgenjs": "^3.12.0"
},
"devDependencies": {
"esbuild": "^0.20.0"
}
}
📜 retro-china-80s-ppt
中国 80 年代怀旧风格 PPT 生成器 — ClawPet Skill(v1.0.0)。
把普通大纲变成带有年代感的 .pptx 演示文稿。四种模板任选:
| 模板 | 效果 | 适用 |
|---|---|---|
textbook 老课本 | 宣纸米黄 + 墨黑 + 朱砂红,双线边框 + 方格底纹 + 印章 | 正式汇报、课堂分享 |
award 奖状喜报 | 金底红字 + 红双喜 + 波浪边框 + 大红印 | 表彰、祝贺、生日奖状 |
quotation 语录本 | 正红底 + 黄字 + 五角星 + 金边 | 名言、格言、语录集 |
calendar 挂历 | 米黄 + 墨绿大日期块 + 挂绳 | 年度回顾、日期相关 |
---
目录结构(位于仓库 skills/retro-china-80s-ppt/)
skills/retro-china-80s-ppt/
├── SKILL.md # 元数据 + tools 声明 + LLM 使用指南
├── package.json # npm 依赖声明(pptxgenjs + esbuild)
├── build.js # esbuild 打包脚本
├── src/
│ └── retro-ppt.cjs # 源文件(开发时编辑)
├── scripts/
│ └── retro-ppt.bundle.cjs # 构建产物(运行时入口,提交前生成)
├── test.cjs # 验证脚本(生成 4 份 demo PPT)
└── README.md # 本文件---
本地构建
cd skills/retro-china-80s-ppt
npm install
node build.js # 生成 scripts/retro-ppt.bundle.cjs
node test.cjs # 运行验证脚本(会在 out/ 下生成 4 份 demo PPT)注意:scripts/retro-ppt.bundle.cjs 是必须提交到仓库的构建产物,因为客户端环境没有 node_modules,运行时直接加载 bundle。
---
工具参数
工具名:create_retro_ppt
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
title | string | ✅ | 主标题 |
subtitle | string | – | 副标题 |
author | string | – | 作者 / 单位,默认 ClawPet |
template | textbook\ | award\ | quotation\ |
slides | {title, bullets[], note?}[] | ✅ | 至少 1 页 |
outputPath | string | – | 输出 .pptx 绝对路径,默认存到 dataDir |
返回:{ success, filePath, slideCount, template, message } 或 { success: false, error }。
---
LLM 使用示例
{
"title": "一九八八年度工作报告",
"subtitle": "回首过往 · 再启新程",
"author": "某某厂劳资科",
"template": "textbook",
"slides": [
{ "title": "开篇寄语", "bullets": ["光阴似箭", "岁月如歌", "望同志们批评指正"] },
{ "title": "今年成就", "bullets": ["完成指标 112%", "获得三项专利", "新开两条生产线"] },
{ "title": "明年展望", "bullets": ["技术革新", "质量管理", "争创一流"] }
]
}---
技术要求
- OS: win32 / darwin / linux
- 依赖:
pptxgenjs(已 bundle,运行时无需安装) - 运行时: Node.js >= 16
- 输出: 标准
.pptx(13.33 × 7.5 英寸宽屏)
License
MIT
/**
* ============================================================
* 📜 Retro China 80s PPT — 中国 80 年代怀旧风格 PPT 生成器
* ============================================================
*
* 依赖:pptxgenjs (通过 esbuild bundle 打包为自包含文件)
*
* 模板:
* - textbook 老课本:宣纸米黄 + 墨黑 + 朱砂红
* - award 奖状喜报:金底红字 + 红双喜边框
* - quotation 语录本:红底黄字
* - calendar 挂历台历:米黄 + 墨绿 + 红点缀
*/
const path = require('path');
const fs = require('fs');
const PptxGenJS = require('pptxgenjs');
// ─── 配色与字体常量 ─────────────────────────────────
const PALETTE = {
textbook: {
bg: 'FAF3E0', // 宣纸暖白
ink: '1A1A1A', // 墨黑
accent: 'C9302C', // 朱砂红
grid: 'D4C9A8', // 方格线
frame: '8B5A2B', // 深棕边框
subtle: '6B5B3E', // 淡墨
},
award: {
bg: 'F5E6C8', // 奖状米金
ink: '5C3A21', // 深棕
accent: 'C9302C', // 搪瓷红
gold: 'B8860B', // 暗金
frame: 'C9302C',
subtle: '8B5A2B',
},
quotation: {
bg: 'A6281F', // 正红
ink: 'F5E6C8', // 米黄字
accent: 'F0C24B', // 黄
frame: 'F0C24B',
subtle: 'E8D49A',
},
calendar: {
bg: 'FAF3E0',
ink: '1A1A1A',
accent: 'C9302C',
green: '2E5D3A', // 墨绿
frame: '2E5D3A',
subtle: '6B5B3E',
},
};
const FONT_CN = '宋体';
const FONT_BOLD = '黑体';
// ─── 工具函数 ───────────────────────────────────────
function safeFilename(title) {
return String(title || 'retro-ppt')
.replace(/[<>:"/\\|?*\x00-\x1F]/g, '_')
.slice(0, 60)
.trim() || 'retro-ppt';
}
function nowStamp() {
const d = new Date();
const p = (n) => String(n).padStart(2, '0');
return `${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}-${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}`;
}
function ensureDir(dir) {
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
}
// ─── 装饰绘制 ───────────────────────────────────────
/**
* 为课本/挂历模板绘制方格底纹(装饰用,稀疏一点)
*/
function drawGridBackground(slide, color) {
const cols = 10;
const rows = 6;
const cellW = 13.33 / cols;
const cellH = 7.5 / rows;
for (let i = 0; i <= cols; i++) {
slide.addShape('line', {
x: i * cellW, y: 0, w: 0, h: 7.5,
line: { color, width: 0.5, dashType: 'dash' },
});
}
for (let j = 0; j <= rows; j++) {
slide.addShape('line', {
x: 0, y: j * cellH, w: 13.33, h: 0,
line: { color, width: 0.5, dashType: 'dash' },
});
}
}
/**
* 绘制双线装饰边框(课本风)
*/
function drawDoubleBorder(slide, color) {
slide.addShape('rect', {
x: 0.3, y: 0.3, w: 12.73, h: 6.9,
line: { color, width: 2.5 }, fill: { type: 'none' },
});
slide.addShape('rect', {
x: 0.5, y: 0.5, w: 12.33, h: 6.5,
line: { color, width: 0.75 }, fill: { type: 'none' },
});
}
/**
* 绘制奖状波浪边(近似用多个小三角/菱形)
*/
function drawAwardBorder(slide, color) {
// 外层粗红框
slide.addShape('rect', {
x: 0.25, y: 0.25, w: 12.83, h: 7.0,
line: { color, width: 4 }, fill: { type: 'none' },
});
// 四角菱形装饰
const corners = [
{ x: 0.15, y: 0.15 },
{ x: 12.88, y: 0.15 },
{ x: 0.15, y: 7.15 },
{ x: 12.88, y: 7.15 },
];
for (const c of corners) {
slide.addShape('diamond', {
x: c.x, y: c.y, w: 0.3, h: 0.3,
fill: { color }, line: { color: 'FFFFFF', width: 0.5 },
});
}
// 内层细金线框
slide.addShape('rect', {
x: 0.6, y: 0.6, w: 12.13, h: 6.3,
line: { color: 'B8860B', width: 1.25, dashType: 'solid' },
fill: { type: 'none' },
});
}
/**
* 绘制印章(红色圆章 + 白字)
*/
function drawStamp(slide, text, x, y, color) {
slide.addShape('ellipse', {
x, y, w: 1.1, h: 1.1,
fill: { color }, line: { color, width: 2 },
});
slide.addShape('ellipse', {
x: x + 0.08, y: y + 0.08, w: 0.94, h: 0.94,
line: { color: 'FFFFFF', width: 1 }, fill: { type: 'none' },
});
slide.addText(text, {
x, y, w: 1.1, h: 1.1,
fontFace: FONT_BOLD, fontSize: 10, color: 'FFFFFF',
bold: true, align: 'center', valign: 'middle',
});
}
/**
* 绘制红双喜字符(用文字模拟,省去导入图片)
*/
function drawDoubleHappy(slide, x, y) {
slide.addText('囍', {
x, y, w: 0.9, h: 0.9,
fontFace: FONT_BOLD, fontSize: 44, color: 'C9302C',
bold: true, align: 'center', valign: 'middle',
});
}
// ─── 首页渲染 ───────────────────────────────────────
function renderCoverTextbook(slide, { title, subtitle, author }, pal) {
slide.background = { color: pal.bg };
drawGridBackground(slide, pal.grid);
drawDoubleBorder(slide, pal.frame);
// 顶部小标:语文 / 第N课 风格
slide.addText('——— 怀旧演示 · 壹玖捌捌 ———', {
x: 1, y: 0.9, w: 11.33, h: 0.5,
fontFace: FONT_CN, fontSize: 16, color: pal.subtle,
align: 'center', italic: true,
});
// 大标题(仿老课本封面手写风)
slide.addText(title, {
x: 1, y: 2.0, w: 11.33, h: 1.8,
fontFace: FONT_BOLD, fontSize: 60, color: pal.ink,
bold: true, align: 'center', valign: 'middle',
shadow: { type: 'outer', color: pal.accent, blur: 0, offset: 3, angle: 45, opacity: 0.35 },
});
// 朱砂红副标题
if (subtitle) {
slide.addText(subtitle, {
x: 1, y: 4.0, w: 11.33, h: 0.8,
fontFace: FONT_CN, fontSize: 24, color: pal.accent,
align: 'center', italic: true,
});
}
// 底部落款
slide.addText(`—— ${author} 敬启 ——`, {
x: 1, y: 6.2, w: 11.33, h: 0.5,
fontFace: FONT_CN, fontSize: 14, color: pal.subtle,
align: 'center',
});
// 右下印章
drawStamp(slide, '敬呈', 11.4, 5.9, pal.accent);
}
function renderCoverAward(slide, { title, subtitle, author }, pal) {
slide.background = { color: pal.bg };
drawAwardBorder(slide, pal.frame);
// 左右红双喜
drawDoubleHappy(slide, 1.0, 0.9);
drawDoubleHappy(slide, 11.4, 0.9);
// 奖状顶部横批
slide.addText('喜 报', {
x: 1, y: 1.0, w: 11.33, h: 0.9,
fontFace: FONT_BOLD, fontSize: 44, color: pal.accent,
bold: true, align: 'center', letterSpacing: 20,
});
// 主标题
slide.addText(title, {
x: 0.8, y: 2.4, w: 11.73, h: 1.6,
fontFace: FONT_BOLD, fontSize: 54, color: pal.accent,
bold: true, align: 'center', valign: 'middle',
});
if (subtitle) {
slide.addText(subtitle, {
x: 1, y: 4.3, w: 11.33, h: 0.8,
fontFace: FONT_CN, fontSize: 22, color: pal.ink,
align: 'center', italic: true,
});
}
// 落款
slide.addText(`颁发单位:${author}`, {
x: 7, y: 5.8, w: 5.5, h: 0.4,
fontFace: FONT_CN, fontSize: 14, color: pal.ink, align: 'left',
});
slide.addText(`公元 ${new Date().getFullYear()} 年`, {
x: 7, y: 6.2, w: 5.5, h: 0.4,
fontFace: FONT_CN, fontSize: 14, color: pal.ink, align: 'left',
});
// 大红印章
drawStamp(slide, '荣誉之印', 10.5, 5.8, pal.accent);
}
function renderCoverQuotation(slide, { title, subtitle, author }, pal) {
slide.background = { color: pal.bg };
// 金色粗边框
slide.addShape('rect', {
x: 0.4, y: 0.4, w: 12.53, h: 6.7,
line: { color: pal.frame, width: 3 }, fill: { type: 'none' },
});
slide.addShape('rect', {
x: 0.7, y: 0.7, w: 11.93, h: 6.1,
line: { color: pal.frame, width: 1 }, fill: { type: 'none' },
});
// 顶部五角星
slide.addText('★', {
x: 6.16, y: 0.8, w: 1, h: 1,
fontFace: FONT_BOLD, fontSize: 60, color: pal.accent,
bold: true, align: 'center',
});
// 大标题(毛体风用黑体加粗近似)
slide.addText(title, {
x: 1, y: 2.4, w: 11.33, h: 1.8,
fontFace: FONT_BOLD, fontSize: 64, color: pal.ink,
bold: true, align: 'center', valign: 'middle',
});
if (subtitle) {
slide.addText(subtitle, {
x: 1, y: 4.4, w: 11.33, h: 0.8,
fontFace: FONT_CN, fontSize: 22, color: pal.subtle,
align: 'center',
});
}
slide.addText(`—— ${author} 编 ——`, {
x: 1, y: 6.2, w: 11.33, h: 0.5,
fontFace: FONT_CN, fontSize: 14, color: pal.ink, align: 'center',
});
}
function renderCoverCalendar(slide, { title, subtitle, author }, pal) {
slide.background = { color: pal.bg };
drawDoubleBorder(slide, pal.frame);
// 顶部挂绳(模拟挂历悬挂)
slide.addShape('line', {
x: 6.0, y: 0.1, w: 1.33, h: 0.3,
line: { color: pal.frame, width: 2 },
});
slide.addShape('ellipse', {
x: 6.56, y: 0.3, w: 0.2, h: 0.2,
fill: { color: pal.accent }, line: { color: pal.accent, width: 1 },
});
// 大号年份(挂历感)
const year = new Date().getFullYear();
slide.addText(String(year), {
x: 1, y: 0.9, w: 11.33, h: 1.5,
fontFace: FONT_BOLD, fontSize: 88, color: pal.green,
bold: true, align: 'center', valign: 'middle',
});
// 主标题
slide.addText(title, {
x: 1, y: 2.8, w: 11.33, h: 1.5,
fontFace: FONT_BOLD, fontSize: 42, color: pal.ink,
bold: true, align: 'center', valign: 'middle',
});
if (subtitle) {
slide.addText(subtitle, {
x: 1, y: 4.5, w: 11.33, h: 0.8,
fontFace: FONT_CN, fontSize: 20, color: pal.accent,
align: 'center',
});
}
slide.addText(`制作:${author}`, {
x: 1, y: 6.3, w: 11.33, h: 0.4,
fontFace: FONT_CN, fontSize: 14, color: pal.subtle, align: 'center',
});
}
// ─── 内容页渲染 ───────────────────────────────────────
function renderContentTextbook(slide, { title, bullets, note }, pal, pageNo) {
slide.background = { color: pal.bg };
drawGridBackground(slide, pal.grid);
drawDoubleBorder(slide, pal.frame);
// 页码 / 课次(左上)
slide.addText(`第 ${pageNo} 课`, {
x: 0.7, y: 0.6, w: 2, h: 0.4,
fontFace: FONT_CN, fontSize: 14, color: pal.accent, italic: true,
});
// 标题下加红色下划线
slide.addText(title, {
x: 0.8, y: 1.0, w: 11.73, h: 0.9,
fontFace: FONT_BOLD, fontSize: 34, color: pal.ink,
bold: true, align: 'left', valign: 'middle',
});
slide.addShape('line', {
x: 0.8, y: 1.95, w: 11.73, h: 0,
line: { color: pal.accent, width: 2 },
});
// Bullets
const bulletText = (bullets || []).map((b) => ({
text: b,
options: {
bullet: { code: '25CF' },
breakLine: true,
},
}));
slide.addText(bulletText, {
x: 1.2, y: 2.2, w: 11.0, h: 4.2,
fontFace: FONT_CN, fontSize: 22, color: pal.ink,
paraSpaceAfter: 14, valign: 'top',
});
// 页脚
slide.addText(note || `—— 第 ${pageNo} 页 ——`, {
x: 0.8, y: 6.7, w: 11.73, h: 0.4,
fontFace: FONT_CN, fontSize: 12, color: pal.subtle,
align: 'center', italic: true,
});
}
function renderContentAward(slide, { title, bullets, note }, pal, pageNo) {
slide.background = { color: pal.bg };
drawAwardBorder(slide, pal.frame);
slide.addText(title, {
x: 0.8, y: 0.9, w: 11.73, h: 0.9,
fontFace: FONT_BOLD, fontSize: 34, color: pal.accent,
bold: true, align: 'center', valign: 'middle',
});
const bulletText = (bullets || []).map((b) => ({
text: b,
options: {
bullet: { code: '2605' }, // ★
breakLine: true,
},
}));
slide.addText(bulletText, {
x: 1.5, y: 2.2, w: 10.33, h: 4.0,
fontFace: FONT_CN, fontSize: 22, color: pal.ink,
paraSpaceAfter: 16, valign: 'top',
});
// 页脚
slide.addText(note || `—— 第 ${pageNo} 篇 ——`, {
x: 0.8, y: 6.7, w: 11.73, h: 0.4,
fontFace: FONT_CN, fontSize: 12, color: pal.subtle, align: 'center',
});
}
function renderContentQuotation(slide, { title, bullets, note }, pal, pageNo) {
slide.background = { color: pal.bg };
// 金色边框
slide.addShape('rect', {
x: 0.4, y: 0.4, w: 12.53, h: 6.7,
line: { color: pal.frame, width: 2.5 }, fill: { type: 'none' },
});
// 标题
slide.addText(title, {
x: 0.8, y: 0.8, w: 11.73, h: 1.0,
fontFace: FONT_BOLD, fontSize: 32, color: pal.accent,
bold: true, align: 'center', valign: 'middle',
});
// 分隔线
slide.addShape('line', {
x: 3, y: 1.9, w: 7.33, h: 0,
line: { color: pal.accent, width: 1.5 },
});
const bulletText = (bullets || []).map((b) => ({
text: `「 ${b} 」`,
options: {
breakLine: true,
},
}));
slide.addText(bulletText, {
x: 1.2, y: 2.2, w: 10.93, h: 4.2,
fontFace: FONT_CN, fontSize: 24, color: pal.ink,
paraSpaceAfter: 18, align: 'center', valign: 'top',
});
slide.addText(note || `—— 语录 · 第 ${pageNo} 则 ——`, {
x: 0.8, y: 6.7, w: 11.73, h: 0.4,
fontFace: FONT_CN, fontSize: 12, color: pal.subtle, align: 'center',
});
}
function renderContentCalendar(slide, { title, bullets, note }, pal, pageNo) {
slide.background = { color: pal.bg };
drawDoubleBorder(slide, pal.frame);
// 左侧大号"日"字块(模拟挂历日期格)
slide.addShape('rect', {
x: 0.8, y: 1.0, w: 2.6, h: 5.4,
fill: { color: pal.green }, line: { color: pal.green, width: 0 },
});
slide.addText(String(pageNo).padStart(2, '0'), {
x: 0.8, y: 1.4, w: 2.6, h: 2.0,
fontFace: FONT_BOLD, fontSize: 96, color: 'FFFFFF',
bold: true, align: 'center', valign: 'middle',
});
slide.addText('第 ' + pageNo + ' 页', {
x: 0.8, y: 3.6, w: 2.6, h: 0.5,
fontFace: FONT_CN, fontSize: 14, color: 'FFFFFF', align: 'center',
});
slide.addShape('line', {
x: 1.1, y: 4.2, w: 2.0, h: 0,
line: { color: pal.accent, width: 2 },
});
slide.addText('CLAWPET', {
x: 0.8, y: 4.4, w: 2.6, h: 0.5,
fontFace: FONT_BOLD, fontSize: 14, color: 'FFFFFF', bold: true, align: 'center',
});
// 右侧标题 + 内容
slide.addText(title, {
x: 3.8, y: 1.0, w: 8.73, h: 0.9,
fontFace: FONT_BOLD, fontSize: 30, color: pal.green,
bold: true, align: 'left', valign: 'middle',
});
slide.addShape('line', {
x: 3.8, y: 1.95, w: 8.73, h: 0,
line: { color: pal.accent, width: 1.5 },
});
const bulletText = (bullets || []).map((b) => ({
text: b,
options: {
bullet: { code: '25A0' }, // ■
breakLine: true,
},
}));
slide.addText(bulletText, {
x: 4.0, y: 2.2, w: 8.5, h: 4.2,
fontFace: FONT_CN, fontSize: 20, color: pal.ink,
paraSpaceAfter: 14, valign: 'top',
});
slide.addText(note || `—— ${new Date().getFullYear()} 挂历 ——`, {
x: 3.8, y: 6.7, w: 8.73, h: 0.4,
fontFace: FONT_CN, fontSize: 12, color: pal.subtle, align: 'center',
});
}
// ─── 模板分派 ───────────────────────────────────────
const COVER_RENDERERS = {
textbook: renderCoverTextbook,
award: renderCoverAward,
quotation: renderCoverQuotation,
calendar: renderCoverCalendar,
};
const CONTENT_RENDERERS = {
textbook: renderContentTextbook,
award: renderContentAward,
quotation: renderContentQuotation,
calendar: renderContentCalendar,
};
// ─── 主生成函数 ───────────────────────────────────────
async function generatePpt(options) {
const {
title,
subtitle = '',
author = 'ClawPet',
template = 'textbook',
slides = [],
outputPath,
} = options || {};
if (!title || typeof title !== 'string') {
throw new Error('必须提供 title(演示文稿主标题)');
}
if (!Array.isArray(slides) || slides.length === 0) {
throw new Error('必须提供 slides 数组,且至少包含 1 页');
}
if (!COVER_RENDERERS[template]) {
throw new Error(`未知模板:${template},可选:textbook/award/quotation/calendar`);
}
const pal = PALETTE[template];
const pptx = new PptxGenJS();
pptx.layout = 'LAYOUT_WIDE'; // 13.33 x 7.5 英寸
pptx.title = title;
pptx.author = author;
pptx.company = author;
pptx.subject = subtitle || title;
// 封面
const cover = pptx.addSlide();
COVER_RENDERERS[template](cover, { title, subtitle, author }, pal);
// 内容页
slides.forEach((s, idx) => {
const slide = pptx.addSlide();
CONTENT_RENDERERS[template](
slide,
{
title: s.title || `第 ${idx + 1} 章`,
bullets: Array.isArray(s.bullets) ? s.bullets : [],
note: s.note,
},
pal,
idx + 1
);
});
return { pptx, slideCount: slides.length + 1 };
}
// ─── 工具入口:execute(input, context) ────────────────
async function execute(input, context) {
try {
const { pptx, slideCount } = await generatePpt(input);
// 决定输出路径
let outPath = input?.outputPath;
if (!outPath) {
const baseDir = (context && context.dataDir) || process.cwd();
ensureDir(baseDir);
const fname = `${safeFilename(input?.title)}_${nowStamp()}.pptx`;
outPath = path.join(baseDir, fname);
} else {
ensureDir(path.dirname(outPath));
}
await pptx.writeFile({ fileName: outPath });
return {
success: true,
filePath: outPath,
slideCount,
template: input?.template || 'textbook',
message: `已生成 ${slideCount} 页 80 年代风格 PPT,保存到:${outPath}`,
};
} catch (err) {
return {
success: false,
error: err && err.message ? err.message : String(err),
};
}
}
// ─── 导出 ───────────────────────────────────────────
module.exports = {
execute,
generatePpt, // 便于测试
PALETTE,
};
#!/usr/bin/env node
/**
* retro-china-80s-ppt 功能验证脚本
*
* 依赖构建产物:请先执行 `npm install && node build.js`
*
* 本测试会生成 4 份不同模板的 demo PPT,输出到当前目录 out/ 文件夹。
*/
const path = require('path');
const fs = require('fs');
// 优先使用 bundle;若未构建则回退到 src(开发时用)
let mod;
const bundlePath = path.join(__dirname, 'scripts', 'retro-ppt.bundle.cjs');
if (fs.existsSync(bundlePath)) {
mod = require(bundlePath);
console.log('[test] 使用 bundle:', bundlePath);
} else {
mod = require(path.join(__dirname, 'src', 'retro-ppt.cjs'));
console.warn('[test] ⚠️ 未发现 bundle,使用源文件(生产前请执行 node build.js)');
}
const { execute } = mod;
const OUT_DIR = path.join(__dirname, 'out');
fs.mkdirSync(OUT_DIR, { recursive: true });
const mockContext = {
toolName: 'create_retro_ppt',
dataDir: OUT_DIR,
skillDir: __dirname,
};
// ─── 四种模板的示例内容 ───────────────────────────────
const cases = [
{
name: 'textbook',
input: {
title: '一九八八年度工作报告',
subtitle: '回首过往 · 再启新程',
author: '某某厂劳资科',
template: 'textbook',
slides: [
{ title: '开篇寄语', bullets: ['光阴似箭,岁月如歌', '值此岁末,谨作汇报', '望同志们批评指正'] },
{ title: '今年成就', bullets: ['完成指标 112%', '获得三项专利', '新开两条生产线', '培训工人 86 名'] },
{ title: '明年展望', bullets: ['继续技术革新', '加强质量管理', '扩大对外交流', '争创一流单位'], note: '——一九八八年腊月' },
],
},
},
{
name: 'award',
input: {
title: '年度最佳女儿',
subtitle: '兹授予小明同志此殊荣',
author: '亲爱的爸爸',
template: 'award',
slides: [
{ title: '主要事迹', bullets: ['学习勤奋,成绩优秀', '尊老爱幼,品德端正', '兴趣广泛,才艺出众', '开朗大方,深得大家喜爱'] },
{ title: '特此表彰', bullets: ['以资鼓励', '再接再厉', '前程似锦', '阖家安康'], note: '—— 颁发于家庭年会 ——' },
],
},
},
{
name: 'quotation',
input: {
title: '程序员语录',
subtitle: '代码江湖 三十六计',
author: '老张编',
template: 'quotation',
slides: [
{ title: '论调试', bullets: ['能跑就别动它', '改一行 bug 冒三行', '遇事不决 console.log', '祖传代码 勿动为上'] },
{ title: '论加班', bullets: ['天行健 程序员以自强不息', '不是在改 bug 就是在产生 bug 的路上', '摸鱼乃立业之本'] },
],
},
},
{
name: 'calendar',
input: {
title: '年度大事回顾',
subtitle: '日子 · 一页一页翻过',
author: 'ClawPet 宠物台历',
template: 'calendar',
slides: [
{ title: '春 · 万物复苏', bullets: ['大年初一吃饺子', '三月植树', '清明踏青'] },
{ title: '夏 · 烈日炎炎', bullets: ['端午龙舟', '暑期纳凉', '七夕情长'] },
{ title: '秋 · 硕果累累', bullets: ['中秋赏月', '国庆同乐', '重阳登高'] },
{ title: '冬 · 岁末团圆', bullets: ['冬至饺子', '腊八喝粥', '除夕守岁'], note: '—— 祈来年顺遂 ——' },
],
},
},
];
async function run() {
console.log('═'.repeat(60));
console.log(' 📜 Retro China 80s PPT — 功能验证');
console.log('═'.repeat(60));
let pass = 0;
let fail = 0;
for (const c of cases) {
const outPath = path.join(OUT_DIR, `demo-${c.name}.pptx`);
try {
const result = await execute({ ...c.input, outputPath: outPath }, mockContext);
if (result.success && fs.existsSync(outPath)) {
const size = fs.statSync(outPath).size;
console.log(` ✅ [${c.name}] 生成成功 (${size} bytes) → ${outPath}`);
pass++;
} else {
console.log(` ❌ [${c.name}] 失败: ${result.error || '文件未生成'}`);
fail++;
}
} catch (err) {
console.log(` ❌ [${c.name}] 异常: ${err.message}`);
fail++;
}
}
// 边界测试
console.log('\n—— 边界测试 ——');
const missingTitle = await execute({ slides: [{ title: 'x', bullets: [] }] }, mockContext);
if (!missingTitle.success && /title/.test(missingTitle.error || '')) {
console.log(' ✅ 缺少 title 正确报错');
pass++;
} else {
console.log(' ❌ 缺少 title 未报错');
fail++;
}
const emptySlides = await execute({ title: 'x', slides: [] }, mockContext);
if (!emptySlides.success && /slides/.test(emptySlides.error || '')) {
console.log(' ✅ 空 slides 正确报错');
pass++;
} else {
console.log(' ❌ 空 slides 未报错');
fail++;
}
const badTemplate = await execute({
title: 'x',
template: 'disco',
slides: [{ title: 'a', bullets: ['b'] }],
}, mockContext);
if (!badTemplate.success && /模板|template/.test(badTemplate.error || '')) {
console.log(' ✅ 未知模板正确报错');
pass++;
} else {
console.log(' ❌ 未知模板未报错');
fail++;
}
console.log('\n' + '═'.repeat(60));
console.log(` 结果: ${pass} 通过 / ${fail} 失败`);
console.log(` 输出目录: ${OUT_DIR}`);
console.log('═'.repeat(60));
process.exit(fail > 0 ? 1 : 0);
}
run().catch((err) => {
console.error('💥 测试异常:', err.stack || err.message);
process.exit(1);
});