build_voc_workshop_ppt.js 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469
  1. const pptxgen = require("pptxgenjs");
  2. const path = require("path");
  3. const OUT = path.resolve(__dirname, "../VOC工作坊讲师版.pptx");
  4. const pptx = new pptxgen();
  5. pptx.defineLayout({ name: "WIDE", width: 13.333, height: 7.5 });
  6. pptx.layout = "WIDE";
  7. pptx.author = "VOC.market";
  8. pptx.company = "VOC.market";
  9. pptx.subject = "VOC工作坊讲师版";
  10. pptx.title = "VOC工作坊讲师版";
  11. pptx.lang = "zh-CN";
  12. pptx.theme = {
  13. headFontFace: "Microsoft YaHei",
  14. bodyFontFace: "Microsoft YaHei",
  15. lang: "zh-CN",
  16. };
  17. const C = {
  18. ink: "10141A",
  19. paper: "FBFAF7",
  20. muted: "66707B",
  21. line: "D7DCE2",
  22. cyan: "00F0FF",
  23. red: "FF5252",
  24. amber: "F6A623",
  25. green: "12A66A",
  26. blue: "315DF4",
  27. pale: "F3F7FA",
  28. white: "FFFFFF",
  29. dark: "0F141B",
  30. };
  31. let slideNo = 0;
  32. function addVocLogo(slide, x, y, size, darkText = true) {
  33. slide.addShape(pptx.ShapeType.roundRect, {
  34. x, y, w: size, h: size,
  35. rectRadius: 0.06,
  36. fill: { color: C.cyan },
  37. line: { transparency: 100 },
  38. });
  39. slide.addShape(pptx.ShapeType.rect, {
  40. x: x + size * 0.48, y, w: size * 0.52, h: size,
  41. fill: { color: C.red, transparency: 5 },
  42. line: { transparency: 100 },
  43. });
  44. slide.addText("V", {
  45. x, y: y + size * 0.15, w: size, h: size * 0.72,
  46. fontSize: size * 34,
  47. fontFace: "Microsoft YaHei",
  48. bold: true,
  49. color: "000000",
  50. align: "center",
  51. margin: 0,
  52. });
  53. slide.addText("VOC.MARKET", {
  54. x: x + size + 0.1, y: y + 0.01, w: 1.1, h: 0.14,
  55. fontSize: 6.3,
  56. bold: true,
  57. color: darkText ? C.ink : C.white,
  58. margin: 0,
  59. });
  60. slide.addText("商协云", {
  61. x: x + size + 0.1, y: y + 0.17, w: 0.7, h: 0.12,
  62. fontSize: 5,
  63. color: darkText ? C.muted : "CAD3DE",
  64. margin: 0,
  65. });
  66. }
  67. function addFooter(slide) {
  68. addVocLogo(slide, 0.42, 0.23, 0.18, true);
  69. slide.addText("VOC WORKSHOP / USER VOICE TO ACTION", {
  70. x: 0.9, y: 0.27, w: 3.1, h: 0.12,
  71. fontSize: 5.5,
  72. color: C.muted,
  73. margin: 0,
  74. });
  75. slide.addText(String(slideNo).padStart(2, "0"), {
  76. x: 12.35, y: 0.24, w: 0.35, h: 0.15,
  77. fontSize: 6,
  78. color: C.muted,
  79. align: "right",
  80. margin: 0,
  81. });
  82. slide.addShape(pptx.ShapeType.line, {
  83. x: 0.42, y: 0.55, w: 12.5, h: 0,
  84. line: { color: "E5E8EC", width: 0.5 },
  85. });
  86. }
  87. function newSlide(kicker, title, subtitle) {
  88. slideNo += 1;
  89. const slide = pptx.addSlide();
  90. slide.background = { color: C.paper };
  91. addFooter(slide);
  92. slide.addText(kicker, {
  93. x: 0.72, y: 0.82, w: 2.5, h: 0.16,
  94. fontSize: 7,
  95. bold: true,
  96. color: C.cyan,
  97. margin: 0,
  98. charSpace: 0.6,
  99. });
  100. slide.addText(title, {
  101. x: 0.72, y: 1.08, w: 9.0, h: 0.5,
  102. fontSize: 23,
  103. bold: true,
  104. color: C.ink,
  105. margin: 0,
  106. fit: "shrink",
  107. });
  108. if (subtitle) {
  109. slide.addText(subtitle, {
  110. x: 0.74, y: 1.68, w: 10.7, h: 0.28,
  111. fontSize: 9,
  112. color: C.muted,
  113. margin: 0,
  114. fit: "shrink",
  115. });
  116. }
  117. slide.addShape(pptx.ShapeType.rect, { x: 0.72, y: 2.08, w: 0.42, h: 0.05, fill: { color: C.cyan }, line: { transparency: 100 } });
  118. slide.addShape(pptx.ShapeType.rect, { x: 1.2, y: 2.08, w: 0.16, h: 0.05, fill: { color: C.red }, line: { transparency: 100 } });
  119. slide.addShape(pptx.ShapeType.line, { x: 1.45, y: 2.105, w: 10.8, h: 0, line: { color: C.line, width: 0.7 } });
  120. return slide;
  121. }
  122. function bulletList(slide, items, x, y, w, opts = {}) {
  123. const fontSize = opts.fontSize || 12;
  124. const gap = opts.gap || 0.38;
  125. const color = opts.color || C.cyan;
  126. items.forEach((text, i) => {
  127. slide.addShape(pptx.ShapeType.ellipse, {
  128. x, y: y + i * gap + 0.06, w: 0.08, h: 0.08,
  129. fill: { color },
  130. line: { transparency: 100 },
  131. });
  132. slide.addText(text, {
  133. x: x + 0.18, y: y + i * gap, w, h: 0.22,
  134. fontSize,
  135. color: C.ink,
  136. margin: 0,
  137. fit: "shrink",
  138. });
  139. });
  140. }
  141. function note(slide, text, x, y, w, color = C.cyan) {
  142. slide.addShape(pptx.ShapeType.rect, {
  143. x, y, w, h: 0.48,
  144. fill: { color: "EAFBFD" },
  145. line: { transparency: 100 },
  146. });
  147. slide.addShape(pptx.ShapeType.rect, {
  148. x, y, w: 0.08, h: 0.48,
  149. fill: { color },
  150. line: { transparency: 100 },
  151. });
  152. slide.addText(text, {
  153. x: x + 0.22, y: y + 0.14, w: w - 0.35, h: 0.16,
  154. fontSize: 8.8,
  155. bold: true,
  156. color: C.ink,
  157. margin: 0,
  158. fit: "shrink",
  159. });
  160. }
  161. function codeBox(slide, text, x, y, w, h, fontSize = 8.2) {
  162. slide.addShape(pptx.ShapeType.roundRect, {
  163. x, y, w, h,
  164. rectRadius: 0.04,
  165. fill: { color: C.dark },
  166. line: { color: "2B3440", width: 0.7 },
  167. });
  168. slide.addText(text, {
  169. x: x + 0.18, y: y + 0.16, w: w - 0.36, h: h - 0.28,
  170. fontFace: "SimHei",
  171. fontSize,
  172. color: C.white,
  173. margin: 0,
  174. fit: "shrink",
  175. breakLine: false,
  176. });
  177. }
  178. function table(slide, rows, x, y, w, colWs, opts = {}) {
  179. const rowH = opts.rowH || 0.36;
  180. const fontSize = opts.fontSize || 7.4;
  181. slide.addShape(pptx.ShapeType.rect, {
  182. x, y, w, h: rowH,
  183. fill: { color: C.ink },
  184. line: { transparency: 100 },
  185. });
  186. rows.forEach((row, r) => {
  187. let cx = x;
  188. row.forEach((cell, c) => {
  189. const cw = colWs[c] * w;
  190. if (r > 0) {
  191. slide.addShape(pptx.ShapeType.rect, {
  192. x: cx, y: y + r * rowH, w: cw, h: rowH,
  193. fill: { color: r % 2 ? C.white : C.pale },
  194. line: { color: C.line, width: 0.35 },
  195. });
  196. }
  197. slide.addText(cell, {
  198. x: cx + 0.05, y: y + r * rowH + 0.105, w: cw - 0.1, h: 0.16,
  199. fontSize: r === 0 ? fontSize + 0.4 : fontSize,
  200. bold: r === 0,
  201. color: r === 0 ? C.white : C.ink,
  202. margin: 0,
  203. fit: "shrink",
  204. });
  205. cx += cw;
  206. });
  207. });
  208. }
  209. function card(slide, head, body, x, y, w, h, color) {
  210. slide.addShape(pptx.ShapeType.rect, {
  211. x, y, w, h,
  212. fill: { color: C.white },
  213. line: { color: C.line, width: 0.6 },
  214. });
  215. slide.addShape(pptx.ShapeType.rect, {
  216. x, y, w: 0.08, h,
  217. fill: { color },
  218. line: { transparency: 100 },
  219. });
  220. slide.addText(head, {
  221. x: x + 0.22, y: y + 0.18, w: w - 0.35, h: 0.2,
  222. fontSize: 12.5,
  223. bold: true,
  224. color: C.ink,
  225. margin: 0,
  226. });
  227. slide.addText(body, {
  228. x: x + 0.22, y: y + 0.5, w: w - 0.35, h: h - 0.55,
  229. fontSize: 8.2,
  230. color: C.muted,
  231. margin: 0,
  232. fit: "shrink",
  233. });
  234. }
  235. function pill(slide, text, x, y, w, color) {
  236. slide.addShape(pptx.ShapeType.roundRect, {
  237. x, y, w, h: 0.32,
  238. rectRadius: 0.06,
  239. fill: { color },
  240. line: { transparency: 100 },
  241. });
  242. slide.addText(text, {
  243. x, y: y + 0.08, w, h: 0.12,
  244. fontSize: 7.6,
  245. bold: true,
  246. color: C.white,
  247. align: "center",
  248. margin: 0,
  249. fit: "shrink",
  250. });
  251. }
  252. // 01 cover
  253. {
  254. slideNo += 1;
  255. const slide = pptx.addSlide();
  256. slide.background = { color: C.dark };
  257. for (let x = 0; x < 13.4; x += 0.55) slide.addShape(pptx.ShapeType.line, { x, y: 0, w: 0, h: 7.5, line: { color: "25303A", width: 0.3 } });
  258. for (let y = 0; y < 7.6; y += 0.55) slide.addShape(pptx.ShapeType.line, { x: 0, y, w: 13.4, h: 0, line: { color: "25303A", width: 0.3 } });
  259. slide.addShape(pptx.ShapeType.rect, { x: 0, y: 0, w: 0.35, h: 1.35, fill: { color: C.cyan }, line: { transparency: 100 } });
  260. slide.addShape(pptx.ShapeType.rect, { x: 12.55, y: 5.55, w: 0.78, h: 1.95, fill: { color: C.red }, line: { transparency: 100 } });
  261. addVocLogo(slide, 0.75, 0.45, 0.38, false);
  262. slide.addText("VOC.MARKET / WORKSHOP DECK", { x: 0.78, y: 1.76, w: 2.7, h: 0.16, fontSize: 7, bold: true, color: C.cyan, margin: 0 });
  263. slide.addText("VOC 工作坊\n讲师版", { x: 0.78, y: 2.08, w: 5.0, h: 1.25, fontSize: 34, bold: true, color: C.white, margin: 0, fit: "shrink" });
  264. slide.addText("主线:发现真实用户问题,判断优先级,转成可验证经营动作。", { x: 0.8, y: 3.55, w: 6.2, h: 0.25, fontSize: 12, color: "CAD3DE", margin: 0 });
  265. table(slide, [
  266. ["时间", "5 月 28 日 14:00-17:00;5 月 29 日 9:00-12:00 / 14:00-17:00"],
  267. ["对象", "线下门店老板、品牌负责人、运营负责人"],
  268. ["目标", "跑通 VOC 技能,输出问题地图、优先级与一周复盘计划"],
  269. ], 0.78, 4.12, 6.7, [0.18, 0.82], { rowH: 0.38, fontSize: 7.1 });
  270. slide.addShape(pptx.ShapeType.roundRect, { x: 7.7, y: 4.5, w: 4.95, h: 1.08, rectRadius: 0.05, fill: { color: "202833" }, line: { transparency: 100 } });
  271. ["用户原声", "问题地图", "优先级", "经营动作"].forEach((t, i) => pill(slide, t, 8.0 + i * 1.05, 4.9, 0.86, [C.cyan, C.red, C.amber, C.green][i]));
  272. }
  273. // 02 - 24
  274. {
  275. const s = newSlide("POSITION", "这次不是口播课,是 VOC 问题发现课", "口播、同城内容可以做,但必须排在 VOC 证据和问题判断之后。");
  276. bulletList(s, ["主线一:把用户真实声音采出来", "主线二:把声音归类成真实经营问题", "主线三:判断哪些问题本周最该解决", "主线四:把 P0 问题转成一周内可验证动作"], 0.95, 2.5, 5.8, { fontSize: 14, gap: 0.5 });
  277. table(s, [["少讲", "多讲"], ["技术参数", "自然语言怎么启动 VOC"], ["口播脚本", "用户原声如何证明问题"], ["泛趋势", "问题优先级和经营动作"]], 7.1, 2.45, 4.9, [0.35, 0.65]);
  278. }
  279. {
  280. const s = newSlide("AGENDA", "课程路线图", "三段时间对应三个教学任务:跑通、看懂、转动作。");
  281. table(s, [["时间", "模块", "产出"], ["5/28 14:00-17:00", "工具跑通 + 首轮演示", "sample 报告 / 会看待校准问题"], ["5/29 9:00-12:00", "VOC 实战 + 用户证据", "问题地图 / 用户原声证据"], ["5/29 14:00-17:00", "优先级 + 经营动作", "P0 问题 / 一周复盘计划"]], 0.95, 2.45, 11.1, [0.22, 0.34, 0.44], { rowH: 0.46, fontSize: 8 });
  282. note(s, "讲师口径:不要让学员觉得自己要学代码,他们只需要学会用业务语言提出好问题。", 1.0, 5.0, 10.7);
  283. }
  284. {
  285. const s = newSlide("BUSINESS FLOW", "按老板的经营问题来讲", "不讲工具菜单,讲每一轮能拿到什么经营交付物。");
  286. table(s, [
  287. ["老板想解决的问题", "课堂模块", "最终交付物"],
  288. ["顾客最近在关心什么", "看市场", "用户洞察趋势清单"],
  289. ["别人为什么吸引顾客", "看竞品", "竞品图谱与错位机会"],
  290. ["顾客到底在吐槽什么", "看问题", "VOC 问题清单与优先级"],
  291. ["老板明天先改什么", "找动作", "单点问题 7 天行动计划"],
  292. ["下周账号发什么", "做内容", "7 天视频选题与口播脚本"],
  293. ], 0.9, 2.05, 11.35, [0.34, 0.22, 0.44], { rowH: 0.47, fontSize: 8.2 });
  294. note(s, "现场要反复强调:先看证据,再做判断,再定动作。口播脚本要来自 VOC,不做脱离用户问题的通用爆款课。", 0.95, 5.45, 10.9, C.green);
  295. }
  296. {
  297. const s = newSlide("DEMO CHAIN", "现场演示调用链路", "每一轮都用自然语言启动,Claude Code 负责选择技能和工具。");
  298. table(s, [
  299. ["学员说法", "触发能力", "拿到结果"],
  300. ["帮我看顾客最近关心什么", "抖音/小红书趋势情报", "第一轮样本观察与待确认问题"],
  301. ["帮我看看竞品都在做什么", "VOC 竞品图谱", "竞品候选、被夸点、被吐槽点"],
  302. ["顾客到底在吐槽什么", "VOC 问题池", "问题优先级与 Top 1 深挖入口"],
  303. ["这个问题老板怎么解决", "单点 VOC 深挖", "7 天动作和验证指标"],
  304. ["下周账号发什么", "VOC 内容选题", "7 天选题、口播、镜头和评论区引导"],
  305. ], 0.75, 2.0, 11.75, [0.38, 0.28, 0.34], { rowH: 0.47, fontSize: 7.9 });
  306. note(s, "不要要求学员记 skill 名称。老师只需要让他们说经营问题,再展示 Claude Code 如何把问题转成交付物。", 0.95, 5.45, 10.9, C.amber);
  307. }
  308. {
  309. const s = newSlide("WHY VOC", "VOC 对老板的价值:把感觉变成证据", "老板每天都在判断,但很多判断缺少用户证据。");
  310. card(s, "原来的判断方式", "凭经验、看同行、看爆款、听员工反馈。问题是:容易被个别案例和主观经验带偏。", 0.9, 2.45, 5.3, 1.35, C.red);
  311. card(s, "VOC 的判断方式", "从评论、笔记、视频、私信、门店反馈里提取高频用户声音,再转成可验证假设。", 7.0, 2.45, 5.3, 1.35, C.cyan);
  312. bulletList(s, ["该不该上新品", "顾客为什么觉得贵", "差评是偶发还是系统问题", "什么问题影响进店和复购"], 1.1, 4.35, 10.5, { fontSize: 13, gap: 0.38, color: C.amber });
  313. }
  314. {
  315. const s = newSlide("OUTPUTS", "这场工作坊最终带走什么", "交付物从内容选题改为 VOC 经营判断。");
  316. table(s, [["交付物", "用途"], ["VOC 情报报告", "看清本轮样本提示了哪些问题"], ["用户原声证据", "让判断有证据,不只靠感觉"], ["用户问题地图", "按产品、价格、服务、环境、传播归类"], ["问题优先级表", "判断 P0/P1/P2,决定先修什么"], ["门店改进动作表", "把问题变成负责人、时间和验证标准"], ["课后一周复盘表", "让 VOC 变成持续机制"]], 0.9, 2.25, 11.2, [0.28, 0.72], { rowH: 0.42, fontSize: 7.8 });
  317. }
  318. {
  319. const s = newSlide("SETUP", "环境配置只做到能用", "课堂不讲技术原理,只保证能打开、能执行、能跑出 sample。");
  320. bulletList(s, ["VS Code 能打开课程工作区", "Claude Code 能正常对话", "Node / npx 有版本号", "VOC Intelligence workspace smoke 通过"], 0.95, 2.45, 4.5, { fontSize: 13.2, gap: 0.46 });
  321. codeBox(s, "node --version\nnpx --version\n\nnpx @gangvy/claude-code-voc-intelligence@latest workspace --smoke", 6.3, 2.45, 5.7, 1.9);
  322. note(s, "通过标准:能返回报告正文,而不是只返回文件路径。", 0.95, 5.0, 10.8, C.green);
  323. }
  324. {
  325. const s = newSlide("FIRST RUN", "第一次运行:先用演示样例", "先确认报告结构,不要一上来就卡在真实采集。");
  326. codeBox(s, "帮我做一份本地餐饮行业的小红书趋势情报。\n先用演示样例跑通流程,不要真实采集。\n在聊天里给我第一轮样本观察和待确认问题。", 0.95, 2.4, 5.6, 1.35);
  327. codeBox(s, "帮我做一份本地餐饮老板能用的抖音趋势情报。\n重点看同城顾客为什么进店、为什么吐槽、哪些问题值得优先解决。\n先用演示样例跑通流程,不要真实采集。", 6.8, 2.4, 5.6, 1.55);
  328. bulletList(s, ["第一轮是待校准假设", "必须有证据样本", "必须有下一轮问题"], 1.05, 4.6, 10.2, { fontSize: 13, gap: 0.42, color: C.red });
  329. }
  330. {
  331. const s = newSlide("PROFILE", "餐饮老板怎么提问题", "Claude Code 不需要技术参数,需要的是门店业务上下文。");
  332. table(s, [["项目", "我的答案"], ["门店类型", ""], ["所在商圈", ""], ["人均消费", ""], ["招牌菜/主推品", ""], ["主要客群", ""], ["目前最想解决的问题", ""]], 0.9, 2.35, 5.1, [0.42, 0.58], { rowH: 0.36 });
  333. codeBox(s, "我的门店是地方菜,人均 80 元,主要客群是家庭聚餐和朋友小聚。\n我想知道近期顾客最在意什么问题,以及本周应该优先改什么。", 6.55, 2.45, 5.6, 1.25);
  334. note(s, "讲师提示:先让学员把“门店问题”讲清楚,再决定采集关键词。", 6.55, 4.15, 5.6, C.amber);
  335. }
  336. {
  337. const s = newSlide("KEYWORDS", "关键词设计:别只搜行业大词", "关键词越接近用户表达,越容易发现真实问题。");
  338. table(s, [["类型", "关键词示例"], ["品类词", "本地美食、地方菜、烧烤、火锅、粉面、小龙虾"], ["场景词", "朋友聚餐、家庭聚餐、情侣约会、生日聚会、宵夜"], ["需求词", "性价比、好拍、出片、不踩雷、排队、停车、服务"], ["菜品词", "辣、下饭、锅气、招牌菜、新品、套餐、团购"], ["情绪词", "值得、后悔、踩雷、惊喜、一般、还会来"]], 0.9, 2.35, 11.2, [0.22, 0.78], { rowH: 0.42, fontSize: 7.8 });
  339. }
  340. {
  341. const s = newSlide("READING", "第一轮报告怎么读", "第一轮报告不是答案,是带证据的假设。");
  342. table(s, [["报告部分", "老板要看什么"], ["数据口径", "平台、关键词、样本数量是否清楚"], ["证据样本", "用户原声像不像真实客户"], ["用户顾虑", "担心点是否影响进店、复购或推荐"], ["校准问题", "能不能让老板继续反馈"]], 0.95, 2.35, 5.5, [0.34, 0.66], { rowH: 0.42 });
  343. bulletList(s, ["先看样本来自哪里", "再看用户反复说什么", "再看机会假设", "最后看待校准问题"], 7.1, 2.6, 4.5, { fontSize: 13.5, gap: 0.5, color: C.green });
  344. }
  345. {
  346. const s = newSlide("PROBLEM STACK", "VOC 核心:先发现问题,再谈机会", "一个门店的问题通常能分成五层。");
  347. table(s, [["层级", "用户会怎么说", "老板要判断什么"], ["产品", "不好吃、分量少、味道不稳定、招牌不突出", "单品问题,还是出品体系问题"], ["价格", "贵、不值、套餐不划算、团购坑", "价格高,还是价值表达不足"], ["服务", "慢、冷淡、没人管、体验不好", "员工问题,还是流程问题"], ["环境", "吵、挤、不好停车、不出片", "硬件问题,还是动线/指引问题"], ["传播", "不知道点什么、怕踩雷、没记忆点", "内容问题,还是定位问题"]], 0.78, 2.28, 11.8, [0.18, 0.44, 0.38], { rowH: 0.41, fontSize: 7.4 });
  348. }
  349. {
  350. const s = newSlide("TRUE PROBLEM", "判断真问题的 4 个标准", "不是每条评论都值得改,先过滤噪声。");
  351. card(s, "重复出现", "多条评论、多位顾客、多平台都提到。", 0.9, 2.5, 5.1, 1.0, C.cyan);
  352. card(s, "具体可定位", "能定位到菜品、场景、服务环节或门店体验。", 6.8, 2.5, 5.1, 1.0, C.red);
  353. card(s, "影响决策", "会影响进店、复购、推荐、客单价。", 0.9, 3.95, 5.1, 1.0, C.amber);
  354. card(s, "可被验证", "一周内能用动作或数据验证是否改善。", 6.8, 3.95, 5.1, 1.0, C.green);
  355. }
  356. {
  357. const s = newSlide("WORKSHEET 1", "练习一:用户问题地图", "把 VOC 报告里的用户原声放进表格,先把问题说准确。");
  358. table(s, [["用户原声", "属于哪类问题", "是否重复出现", "是否影响决策", "是否可验证"], ["", "", "", "", ""], ["", "", "", "", ""], ["", "", "", "", ""], ["", "", "", "", ""]], 0.75, 2.35, 11.8, [0.32, 0.2, 0.16, 0.18, 0.14], { rowH: 0.43 });
  359. note(s, "讲师提示:这页不要急着给建议,先训练学员把问题归类准确。", 0.95, 5.25, 10.8);
  360. }
  361. {
  362. const s = newSlide("WORKSHEET 2", "练习二:从评论里找真实需求", "用户原话不是摆设,要转成“他在意什么”和“门店意味着什么”。");
  363. table(s, [["用户原话", "他在意什么", "对门店意味着什么", "可做动作"], ["", "", "", ""], ["", "", "", ""], ["", "", "", ""], ["", "", "", ""]], 0.78, 2.35, 11.6, [0.33, 0.22, 0.27, 0.18], { rowH: 0.44 });
  364. bulletList(s, ["夸菜品:可能是招牌强化机会", "吐槽贵:可能是价值表达不清", "说排队:可能要优化等位体验"], 1.0, 5.25, 10.2, { fontSize: 11.3, gap: 0.32, color: C.amber });
  365. }
  366. {
  367. const s = newSlide("PRIORITY", "练习三:做问题优先级排序", "不是所有问题都马上解决,先判断先修哪个。");
  368. table(s, [["问题", "出现频次", "影响营收", "解决难度", "优先级"], ["", "高/中/低", "高/中/低", "高/中/低", "P0/P1/P2"], ["", "高/中/低", "高/中/低", "高/中/低", "P0/P1/P2"], ["", "高/中/低", "高/中/低", "高/中/低", "P0/P1/P2"]], 0.8, 2.35, 7.1, [0.28, 0.18, 0.18, 0.18, 0.18]);
  369. bulletList(s, ["P0:高频、高影响、低难度,本周就要改", "P1:高频或高影响,但需要资源协调", "P2:有讨论价值,但暂时不影响核心经营"], 8.35, 2.65, 3.8, { fontSize: 12.2, gap: 0.58, color: C.green });
  370. }
  371. {
  372. const s = newSlide("OPPORTUNITY", "练习四:从问题里找新品/套餐机会", "新品机会不是老板灵感,而是用户需求、门店能力、传播理由的交集。");
  373. table(s, [["候选方向", "用户证据", "门店是否能做", "是否容易传播", "优先级"], ["", "", "", "", ""], ["", "", "", "", ""], ["", "", "", "", ""]], 0.78, 2.35, 11.7, [0.23, 0.32, 0.18, 0.17, 0.10], { rowH: 0.44 });
  374. note(s, "注意:这页仍然属于 VOC 后的经营判断,不是先拍脑袋想新品。", 0.95, 4.9, 10.8, C.green);
  375. }
  376. {
  377. const s = newSlide("ACTION", "练习五:把差评变成门店动作", "差评不是用来解释的,是用来找系统问题的。");
  378. table(s, [["差评/吐槽", "归因", "影响程度", "本周动作", "负责人"], ["排队久", "动线/排班/等位体验", "高/中/低", "", ""], ["菜慢", "后厨/高峰/菜品结构", "高/中/低", "", ""], ["觉得贵", "菜单/套餐/价值表达", "高/中/低", "", ""], ["服务冷淡", "培训/激励/流程", "高/中/低", "", ""]], 0.78, 2.35, 11.7, [0.20, 0.32, 0.16, 0.20, 0.12], { rowH: 0.42 });
  379. }
  380. {
  381. const s = newSlide("MECHANISM", "练习六:把 VOC 变成每周复盘机制", "一次报告有用,持续复盘才会变成经营能力。");
  382. table(s, [["本周观察", "用户证据", "老板判断", "下周动作"], ["高频问题", "", "", ""], ["新增问题", "", "", ""], ["已改善问题", "", "", ""], ["需要继续验证的问题", "", "", ""]], 0.78, 2.35, 11.7, [0.22, 0.32, 0.24, 0.22], { rowH: 0.44 });
  383. note(s, "目标:让员工每周能提交用户证据,让老板每周能做一次问题优先级判断。", 0.95, 5.25, 10.8);
  384. }
  385. {
  386. const s = newSlide("LIVE MODE", "真实采集前先检查 token 和余额", "没有 token 或余额不足时,给充值/开通链接,不要只抛 403。");
  387. codeBox(s, "请切换真实采集。\n行业:本地餐饮\n重点看:用户为什么进店、为什么吐槽、哪些问题值得优先解决。\n平台:小红书和抖音。\n请先检查 token 和余额,再开始采集。", 0.95, 2.35, 6.0, 1.85);
  388. bulletList(s, ["没有 token:给开通/充值链接", "余额不足:给充值链接和 sample 方案", "服务不可用:保留参数,恢复后补跑"], 7.45, 2.65, 4.5, { fontSize: 13, gap: 0.5, color: C.red });
  389. }
  390. {
  391. const s = newSlide("TEAM TASK", "小组实战任务", "每组交付的不是一篇空泛报告,而是一张能执行的问题地图。");
  392. table(s, [["任务", "交付物"], ["建立门店档案", "品类、人均、商圈、招牌菜、目标客群、竞品"], ["跑第一轮 VOC", "样本观察、用户顾虑、证据样本、校准问题"], ["转成问题地图", "产品、价格、服务、环境、传播五类问题"], ["做优先级排序", "P0/P1/P2 问题排序"], ["输出经营动作", "3 个本周可验证动作"], ["复盘计划", "课后一周 VOC 复盘表"]], 0.9, 2.2, 10.8, [0.28, 0.72], { rowH: 0.39 });
  393. }
  394. {
  395. const s = newSlide("REPORT OUT", "小组汇报模板", "汇报 5 分钟,重点说证据、问题和动作。");
  396. codeBox(s, "我们的门店/品类:\n\n最重要的用户信号:\n1.\n2.\n3.\n\n最值得优先验证的问题:\n\n证据来自:\n\n本周行动:\n1.\n2.\n3.", 0.9, 2.35, 5.8, 2.9);
  397. bulletList(s, ["不要讲太多背景", "不要只讲趋势词", "必须引用用户原声", "必须给出一周行动"], 7.3, 2.65, 4.5, { fontSize: 13.2, gap: 0.5, color: C.cyan });
  398. }
  399. {
  400. const s = newSlide("OPTIONAL", "口播和同城内容:放在最后作为延展", "只有当 VOC 问题足够清楚,内容才不会变成空泛宣传。");
  401. bulletList(s, ["本次主线:VOC 问题发现、证据判断、经营动作", "可选延展:把真实用户问题变成同城选题", "下次工作坊可深化:VOC 转老板 IP / 口播 / 短视频脚本"], 0.95, 2.45, 6.0, { fontSize: 13.5, gap: 0.55 });
  402. codeBox(s, "内容公式:\n用户真实问题 + 门店证据 + 到店理由 = 一条可拍内容", 7.1, 2.65, 4.8, 1.1, 9);
  403. }
  404. {
  405. const s = newSlide("CHECKLIST", "一份好 VOC 报告的验收标准", "看它能不能指导动作,不要只看它像不像咨询报告。");
  406. table(s, [["验收项", "合格标准", "不合格表现"], ["数据口径", "说明平台、关键词、样本数量", "不知道样本从哪来"], ["证据样本", "有标题、作者、互动、原文或评论", "只有观点没有证据"], ["问题地图", "能看清产品、价格、服务、环境、传播问题", "只写抽象趋势"], ["校准问题", "能让老板继续反馈", "没有下一轮问题"], ["行动建议", "有优先级和验证标准", "没有时间、负责人、验证方式"]], 0.75, 2.28, 11.8, [0.18, 0.46, 0.36], { rowH: 0.4, fontSize: 7.1 });
  407. }
  408. {
  409. const s = newSlide("FAQ", "现场常见问题处理", "课堂不要被工具问题打断业务主线。");
  410. table(s, [["问题", "处理方式"], ["Claude Code 看不到技能", "重启 Claude Code 会话,确认 .claude/skills 已生成"], ["npx 很慢", "换网络,或用讲师提供的已安装工作区"], ["live 真实采集失败", "先看 token/余额;课堂回到 sample 完成方法练习"], ["第一轮报告不准", "正常,继续用真实业务反馈做校准"], ["员工不会用", "老板先沉淀每周趋势、差评复盘、优先级排序 3 个 Prompt"]], 0.85, 2.3, 11.0, [0.28, 0.72], { rowH: 0.42 });
  411. }
  412. {
  413. const s = newSlide("7 DAYS", "课后一周执行清单", "每天只做一个小动作,让 VOC 变成门店机制。");
  414. table(s, [["日期", "动作", "输出"], ["第 1 天", "跑一次 sample 或 live VOC", "第一轮样本观察"], ["第 2 天", "补充真实业务反馈", "校准后的问题方向"], ["第 3 天", "做问题优先级排序", "P0/P1/P2 问题表"], ["第 4 天", "选 1 个 P0 问题执行改进", "动作记录"], ["第 5 天", "收集改进后的顾客反馈", "反馈摘录"], ["第 6 天", "汇总评论/咨询/到店反馈", "用户反馈摘录"], ["第 7 天", "复盘并更新关键词", "下周 VOC 任务"]], 0.85, 2.15, 11.2, [0.18, 0.48, 0.34], { rowH: 0.37 });
  415. }
  416. {
  417. const s = newSlide("CLOSE", "结业行动卡", "结束前,每位老板写下一个本周必须验证的问题。");
  418. codeBox(s, "我这两天最有价值的发现是:\n\nVOC 证据提示顾客最在意:\n\n我本周要立刻验证的问题是:\n\n我准备采取的动作是:\n\n我希望一周后看到的验证结果是:", 1.0, 2.35, 6.2, 2.5);
  419. bulletList(s, ["不要追求一次看透所有问题", "先验证一个 P0 问题", "让 VOC 成为每周固定动作"], 8.0, 2.75, 3.8, { fontSize: 14, gap: 0.62, color: C.green });
  420. }
  421. (async () => {
  422. await pptx.writeFile({ fileName: OUT });
  423. console.log(OUT);
  424. })().catch((error) => {
  425. console.error(error);
  426. process.exit(1);
  427. });