const pptxgen = require("pptxgenjs"); const path = require("path"); const OUT = path.resolve(__dirname, "../VOC工作坊讲师版.pptx"); const pptx = new pptxgen(); pptx.defineLayout({ name: "WIDE", width: 13.333, height: 7.5 }); pptx.layout = "WIDE"; pptx.author = "VOC.market"; pptx.company = "VOC.market"; pptx.subject = "VOC工作坊讲师版"; pptx.title = "VOC工作坊讲师版"; pptx.lang = "zh-CN"; pptx.theme = { headFontFace: "Microsoft YaHei", bodyFontFace: "Microsoft YaHei", lang: "zh-CN", }; const C = { ink: "10141A", paper: "FBFAF7", muted: "66707B", line: "D7DCE2", cyan: "00F0FF", red: "FF5252", amber: "F6A623", green: "12A66A", blue: "315DF4", pale: "F3F7FA", white: "FFFFFF", dark: "0F141B", }; let slideNo = 0; function addVocLogo(slide, x, y, size, darkText = true) { slide.addShape(pptx.ShapeType.roundRect, { x, y, w: size, h: size, rectRadius: 0.06, fill: { color: C.cyan }, line: { transparency: 100 }, }); slide.addShape(pptx.ShapeType.rect, { x: x + size * 0.48, y, w: size * 0.52, h: size, fill: { color: C.red, transparency: 5 }, line: { transparency: 100 }, }); slide.addText("V", { x, y: y + size * 0.15, w: size, h: size * 0.72, fontSize: size * 34, fontFace: "Microsoft YaHei", bold: true, color: "000000", align: "center", margin: 0, }); slide.addText("VOC.MARKET", { x: x + size + 0.1, y: y + 0.01, w: 1.1, h: 0.14, fontSize: 6.3, bold: true, color: darkText ? C.ink : C.white, margin: 0, }); slide.addText("商协云", { x: x + size + 0.1, y: y + 0.17, w: 0.7, h: 0.12, fontSize: 5, color: darkText ? C.muted : "CAD3DE", margin: 0, }); } function addFooter(slide) { addVocLogo(slide, 0.42, 0.23, 0.18, true); slide.addText("VOC WORKSHOP / USER VOICE TO ACTION", { x: 0.9, y: 0.27, w: 3.1, h: 0.12, fontSize: 5.5, color: C.muted, margin: 0, }); slide.addText(String(slideNo).padStart(2, "0"), { x: 12.35, y: 0.24, w: 0.35, h: 0.15, fontSize: 6, color: C.muted, align: "right", margin: 0, }); slide.addShape(pptx.ShapeType.line, { x: 0.42, y: 0.55, w: 12.5, h: 0, line: { color: "E5E8EC", width: 0.5 }, }); } function newSlide(kicker, title, subtitle) { slideNo += 1; const slide = pptx.addSlide(); slide.background = { color: C.paper }; addFooter(slide); slide.addText(kicker, { x: 0.72, y: 0.82, w: 2.5, h: 0.16, fontSize: 7, bold: true, color: C.cyan, margin: 0, charSpace: 0.6, }); slide.addText(title, { x: 0.72, y: 1.08, w: 9.0, h: 0.5, fontSize: 23, bold: true, color: C.ink, margin: 0, fit: "shrink", }); if (subtitle) { slide.addText(subtitle, { x: 0.74, y: 1.68, w: 10.7, h: 0.28, fontSize: 9, color: C.muted, margin: 0, fit: "shrink", }); } slide.addShape(pptx.ShapeType.rect, { x: 0.72, y: 2.08, w: 0.42, h: 0.05, fill: { color: C.cyan }, line: { transparency: 100 } }); slide.addShape(pptx.ShapeType.rect, { x: 1.2, y: 2.08, w: 0.16, h: 0.05, fill: { color: C.red }, line: { transparency: 100 } }); slide.addShape(pptx.ShapeType.line, { x: 1.45, y: 2.105, w: 10.8, h: 0, line: { color: C.line, width: 0.7 } }); return slide; } function bulletList(slide, items, x, y, w, opts = {}) { const fontSize = opts.fontSize || 12; const gap = opts.gap || 0.38; const color = opts.color || C.cyan; items.forEach((text, i) => { slide.addShape(pptx.ShapeType.ellipse, { x, y: y + i * gap + 0.06, w: 0.08, h: 0.08, fill: { color }, line: { transparency: 100 }, }); slide.addText(text, { x: x + 0.18, y: y + i * gap, w, h: 0.22, fontSize, color: C.ink, margin: 0, fit: "shrink", }); }); } function note(slide, text, x, y, w, color = C.cyan) { slide.addShape(pptx.ShapeType.rect, { x, y, w, h: 0.48, fill: { color: "EAFBFD" }, line: { transparency: 100 }, }); slide.addShape(pptx.ShapeType.rect, { x, y, w: 0.08, h: 0.48, fill: { color }, line: { transparency: 100 }, }); slide.addText(text, { x: x + 0.22, y: y + 0.14, w: w - 0.35, h: 0.16, fontSize: 8.8, bold: true, color: C.ink, margin: 0, fit: "shrink", }); } function codeBox(slide, text, x, y, w, h, fontSize = 8.2) { slide.addShape(pptx.ShapeType.roundRect, { x, y, w, h, rectRadius: 0.04, fill: { color: C.dark }, line: { color: "2B3440", width: 0.7 }, }); slide.addText(text, { x: x + 0.18, y: y + 0.16, w: w - 0.36, h: h - 0.28, fontFace: "SimHei", fontSize, color: C.white, margin: 0, fit: "shrink", breakLine: false, }); } function table(slide, rows, x, y, w, colWs, opts = {}) { const rowH = opts.rowH || 0.36; const fontSize = opts.fontSize || 7.4; slide.addShape(pptx.ShapeType.rect, { x, y, w, h: rowH, fill: { color: C.ink }, line: { transparency: 100 }, }); rows.forEach((row, r) => { let cx = x; row.forEach((cell, c) => { const cw = colWs[c] * w; if (r > 0) { slide.addShape(pptx.ShapeType.rect, { x: cx, y: y + r * rowH, w: cw, h: rowH, fill: { color: r % 2 ? C.white : C.pale }, line: { color: C.line, width: 0.35 }, }); } slide.addText(cell, { x: cx + 0.05, y: y + r * rowH + 0.105, w: cw - 0.1, h: 0.16, fontSize: r === 0 ? fontSize + 0.4 : fontSize, bold: r === 0, color: r === 0 ? C.white : C.ink, margin: 0, fit: "shrink", }); cx += cw; }); }); } function card(slide, head, body, x, y, w, h, color) { slide.addShape(pptx.ShapeType.rect, { x, y, w, h, fill: { color: C.white }, line: { color: C.line, width: 0.6 }, }); slide.addShape(pptx.ShapeType.rect, { x, y, w: 0.08, h, fill: { color }, line: { transparency: 100 }, }); slide.addText(head, { x: x + 0.22, y: y + 0.18, w: w - 0.35, h: 0.2, fontSize: 12.5, bold: true, color: C.ink, margin: 0, }); slide.addText(body, { x: x + 0.22, y: y + 0.5, w: w - 0.35, h: h - 0.55, fontSize: 8.2, color: C.muted, margin: 0, fit: "shrink", }); } function pill(slide, text, x, y, w, color) { slide.addShape(pptx.ShapeType.roundRect, { x, y, w, h: 0.32, rectRadius: 0.06, fill: { color }, line: { transparency: 100 }, }); slide.addText(text, { x, y: y + 0.08, w, h: 0.12, fontSize: 7.6, bold: true, color: C.white, align: "center", margin: 0, fit: "shrink", }); } // 01 cover { slideNo += 1; const slide = pptx.addSlide(); slide.background = { color: C.dark }; 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 } }); 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 } }); slide.addShape(pptx.ShapeType.rect, { x: 0, y: 0, w: 0.35, h: 1.35, fill: { color: C.cyan }, line: { transparency: 100 } }); slide.addShape(pptx.ShapeType.rect, { x: 12.55, y: 5.55, w: 0.78, h: 1.95, fill: { color: C.red }, line: { transparency: 100 } }); addVocLogo(slide, 0.75, 0.45, 0.38, false); 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 }); 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" }); slide.addText("主线:发现真实用户问题,判断优先级,转成可验证经营动作。", { x: 0.8, y: 3.55, w: 6.2, h: 0.25, fontSize: 12, color: "CAD3DE", margin: 0 }); table(slide, [ ["时间", "5 月 28 日 14:00-17:00;5 月 29 日 9:00-12:00 / 14:00-17:00"], ["对象", "线下门店老板、品牌负责人、运营负责人"], ["目标", "跑通 VOC 技能,输出问题地图、优先级与一周复盘计划"], ], 0.78, 4.12, 6.7, [0.18, 0.82], { rowH: 0.38, fontSize: 7.1 }); 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 } }); ["用户原声", "问题地图", "优先级", "经营动作"].forEach((t, i) => pill(slide, t, 8.0 + i * 1.05, 4.9, 0.86, [C.cyan, C.red, C.amber, C.green][i])); } // 02 - 24 { const s = newSlide("POSITION", "这次不是口播课,是 VOC 问题发现课", "口播、同城内容可以做,但必须排在 VOC 证据和问题判断之后。"); bulletList(s, ["主线一:把用户真实声音采出来", "主线二:把声音归类成真实经营问题", "主线三:判断哪些问题本周最该解决", "主线四:把 P0 问题转成一周内可验证动作"], 0.95, 2.5, 5.8, { fontSize: 14, gap: 0.5 }); table(s, [["少讲", "多讲"], ["技术参数", "自然语言怎么启动 VOC"], ["口播脚本", "用户原声如何证明问题"], ["泛趋势", "问题优先级和经营动作"]], 7.1, 2.45, 4.9, [0.35, 0.65]); } { const s = newSlide("AGENDA", "课程路线图", "三段时间对应三个教学任务:跑通、看懂、转动作。"); 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 }); note(s, "讲师口径:不要让学员觉得自己要学代码,他们只需要学会用业务语言提出好问题。", 1.0, 5.0, 10.7); } { const s = newSlide("BUSINESS FLOW", "按老板的经营问题来讲", "不讲工具菜单,讲每一轮能拿到什么经营交付物。"); table(s, [ ["老板想解决的问题", "课堂模块", "最终交付物"], ["顾客最近在关心什么", "看市场", "用户洞察趋势清单"], ["别人为什么吸引顾客", "看竞品", "竞品图谱与错位机会"], ["顾客到底在吐槽什么", "看问题", "VOC 问题清单与优先级"], ["老板明天先改什么", "找动作", "单点问题 7 天行动计划"], ["下周账号发什么", "做内容", "7 天视频选题与口播脚本"], ], 0.9, 2.05, 11.35, [0.34, 0.22, 0.44], { rowH: 0.47, fontSize: 8.2 }); note(s, "现场要反复强调:先看证据,再做判断,再定动作。口播脚本要来自 VOC,不做脱离用户问题的通用爆款课。", 0.95, 5.45, 10.9, C.green); } { const s = newSlide("DEMO CHAIN", "现场演示调用链路", "每一轮都用自然语言启动,Claude Code 负责选择技能和工具。"); table(s, [ ["学员说法", "触发能力", "拿到结果"], ["帮我看顾客最近关心什么", "抖音/小红书趋势情报", "第一轮样本观察与待确认问题"], ["帮我看看竞品都在做什么", "VOC 竞品图谱", "竞品候选、被夸点、被吐槽点"], ["顾客到底在吐槽什么", "VOC 问题池", "问题优先级与 Top 1 深挖入口"], ["这个问题老板怎么解决", "单点 VOC 深挖", "7 天动作和验证指标"], ["下周账号发什么", "VOC 内容选题", "7 天选题、口播、镜头和评论区引导"], ], 0.75, 2.0, 11.75, [0.38, 0.28, 0.34], { rowH: 0.47, fontSize: 7.9 }); note(s, "不要要求学员记 skill 名称。老师只需要让他们说经营问题,再展示 Claude Code 如何把问题转成交付物。", 0.95, 5.45, 10.9, C.amber); } { const s = newSlide("WHY VOC", "VOC 对老板的价值:把感觉变成证据", "老板每天都在判断,但很多判断缺少用户证据。"); card(s, "原来的判断方式", "凭经验、看同行、看爆款、听员工反馈。问题是:容易被个别案例和主观经验带偏。", 0.9, 2.45, 5.3, 1.35, C.red); card(s, "VOC 的判断方式", "从评论、笔记、视频、私信、门店反馈里提取高频用户声音,再转成可验证假设。", 7.0, 2.45, 5.3, 1.35, C.cyan); bulletList(s, ["该不该上新品", "顾客为什么觉得贵", "差评是偶发还是系统问题", "什么问题影响进店和复购"], 1.1, 4.35, 10.5, { fontSize: 13, gap: 0.38, color: C.amber }); } { const s = newSlide("OUTPUTS", "这场工作坊最终带走什么", "交付物从内容选题改为 VOC 经营判断。"); table(s, [["交付物", "用途"], ["VOC 情报报告", "看清本轮样本提示了哪些问题"], ["用户原声证据", "让判断有证据,不只靠感觉"], ["用户问题地图", "按产品、价格、服务、环境、传播归类"], ["问题优先级表", "判断 P0/P1/P2,决定先修什么"], ["门店改进动作表", "把问题变成负责人、时间和验证标准"], ["课后一周复盘表", "让 VOC 变成持续机制"]], 0.9, 2.25, 11.2, [0.28, 0.72], { rowH: 0.42, fontSize: 7.8 }); } { const s = newSlide("SETUP", "环境配置只做到能用", "课堂不讲技术原理,只保证能打开、能执行、能跑出 sample。"); bulletList(s, ["VS Code 能打开课程工作区", "Claude Code 能正常对话", "Node / npx 有版本号", "VOC Intelligence workspace smoke 通过"], 0.95, 2.45, 4.5, { fontSize: 13.2, gap: 0.46 }); codeBox(s, "node --version\nnpx --version\n\nnpx @gangvy/claude-code-voc-intelligence@latest workspace --smoke", 6.3, 2.45, 5.7, 1.9); note(s, "通过标准:能返回报告正文,而不是只返回文件路径。", 0.95, 5.0, 10.8, C.green); } { const s = newSlide("FIRST RUN", "第一次运行:先用演示样例", "先确认报告结构,不要一上来就卡在真实采集。"); codeBox(s, "帮我做一份本地餐饮行业的小红书趋势情报。\n先用演示样例跑通流程,不要真实采集。\n在聊天里给我第一轮样本观察和待确认问题。", 0.95, 2.4, 5.6, 1.35); codeBox(s, "帮我做一份本地餐饮老板能用的抖音趋势情报。\n重点看同城顾客为什么进店、为什么吐槽、哪些问题值得优先解决。\n先用演示样例跑通流程,不要真实采集。", 6.8, 2.4, 5.6, 1.55); bulletList(s, ["第一轮是待校准假设", "必须有证据样本", "必须有下一轮问题"], 1.05, 4.6, 10.2, { fontSize: 13, gap: 0.42, color: C.red }); } { const s = newSlide("PROFILE", "餐饮老板怎么提问题", "Claude Code 不需要技术参数,需要的是门店业务上下文。"); table(s, [["项目", "我的答案"], ["门店类型", ""], ["所在商圈", ""], ["人均消费", ""], ["招牌菜/主推品", ""], ["主要客群", ""], ["目前最想解决的问题", ""]], 0.9, 2.35, 5.1, [0.42, 0.58], { rowH: 0.36 }); codeBox(s, "我的门店是地方菜,人均 80 元,主要客群是家庭聚餐和朋友小聚。\n我想知道近期顾客最在意什么问题,以及本周应该优先改什么。", 6.55, 2.45, 5.6, 1.25); note(s, "讲师提示:先让学员把“门店问题”讲清楚,再决定采集关键词。", 6.55, 4.15, 5.6, C.amber); } { const s = newSlide("KEYWORDS", "关键词设计:别只搜行业大词", "关键词越接近用户表达,越容易发现真实问题。"); table(s, [["类型", "关键词示例"], ["品类词", "本地美食、地方菜、烧烤、火锅、粉面、小龙虾"], ["场景词", "朋友聚餐、家庭聚餐、情侣约会、生日聚会、宵夜"], ["需求词", "性价比、好拍、出片、不踩雷、排队、停车、服务"], ["菜品词", "辣、下饭、锅气、招牌菜、新品、套餐、团购"], ["情绪词", "值得、后悔、踩雷、惊喜、一般、还会来"]], 0.9, 2.35, 11.2, [0.22, 0.78], { rowH: 0.42, fontSize: 7.8 }); } { const s = newSlide("READING", "第一轮报告怎么读", "第一轮报告不是答案,是带证据的假设。"); table(s, [["报告部分", "老板要看什么"], ["数据口径", "平台、关键词、样本数量是否清楚"], ["证据样本", "用户原声像不像真实客户"], ["用户顾虑", "担心点是否影响进店、复购或推荐"], ["校准问题", "能不能让老板继续反馈"]], 0.95, 2.35, 5.5, [0.34, 0.66], { rowH: 0.42 }); bulletList(s, ["先看样本来自哪里", "再看用户反复说什么", "再看机会假设", "最后看待校准问题"], 7.1, 2.6, 4.5, { fontSize: 13.5, gap: 0.5, color: C.green }); } { const s = newSlide("PROBLEM STACK", "VOC 核心:先发现问题,再谈机会", "一个门店的问题通常能分成五层。"); table(s, [["层级", "用户会怎么说", "老板要判断什么"], ["产品", "不好吃、分量少、味道不稳定、招牌不突出", "单品问题,还是出品体系问题"], ["价格", "贵、不值、套餐不划算、团购坑", "价格高,还是价值表达不足"], ["服务", "慢、冷淡、没人管、体验不好", "员工问题,还是流程问题"], ["环境", "吵、挤、不好停车、不出片", "硬件问题,还是动线/指引问题"], ["传播", "不知道点什么、怕踩雷、没记忆点", "内容问题,还是定位问题"]], 0.78, 2.28, 11.8, [0.18, 0.44, 0.38], { rowH: 0.41, fontSize: 7.4 }); } { const s = newSlide("TRUE PROBLEM", "判断真问题的 4 个标准", "不是每条评论都值得改,先过滤噪声。"); card(s, "重复出现", "多条评论、多位顾客、多平台都提到。", 0.9, 2.5, 5.1, 1.0, C.cyan); card(s, "具体可定位", "能定位到菜品、场景、服务环节或门店体验。", 6.8, 2.5, 5.1, 1.0, C.red); card(s, "影响决策", "会影响进店、复购、推荐、客单价。", 0.9, 3.95, 5.1, 1.0, C.amber); card(s, "可被验证", "一周内能用动作或数据验证是否改善。", 6.8, 3.95, 5.1, 1.0, C.green); } { const s = newSlide("WORKSHEET 1", "练习一:用户问题地图", "把 VOC 报告里的用户原声放进表格,先把问题说准确。"); table(s, [["用户原声", "属于哪类问题", "是否重复出现", "是否影响决策", "是否可验证"], ["", "", "", "", ""], ["", "", "", "", ""], ["", "", "", "", ""], ["", "", "", "", ""]], 0.75, 2.35, 11.8, [0.32, 0.2, 0.16, 0.18, 0.14], { rowH: 0.43 }); note(s, "讲师提示:这页不要急着给建议,先训练学员把问题归类准确。", 0.95, 5.25, 10.8); } { const s = newSlide("WORKSHEET 2", "练习二:从评论里找真实需求", "用户原话不是摆设,要转成“他在意什么”和“门店意味着什么”。"); table(s, [["用户原话", "他在意什么", "对门店意味着什么", "可做动作"], ["", "", "", ""], ["", "", "", ""], ["", "", "", ""], ["", "", "", ""]], 0.78, 2.35, 11.6, [0.33, 0.22, 0.27, 0.18], { rowH: 0.44 }); bulletList(s, ["夸菜品:可能是招牌强化机会", "吐槽贵:可能是价值表达不清", "说排队:可能要优化等位体验"], 1.0, 5.25, 10.2, { fontSize: 11.3, gap: 0.32, color: C.amber }); } { const s = newSlide("PRIORITY", "练习三:做问题优先级排序", "不是所有问题都马上解决,先判断先修哪个。"); 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]); bulletList(s, ["P0:高频、高影响、低难度,本周就要改", "P1:高频或高影响,但需要资源协调", "P2:有讨论价值,但暂时不影响核心经营"], 8.35, 2.65, 3.8, { fontSize: 12.2, gap: 0.58, color: C.green }); } { const s = newSlide("OPPORTUNITY", "练习四:从问题里找新品/套餐机会", "新品机会不是老板灵感,而是用户需求、门店能力、传播理由的交集。"); table(s, [["候选方向", "用户证据", "门店是否能做", "是否容易传播", "优先级"], ["", "", "", "", ""], ["", "", "", "", ""], ["", "", "", "", ""]], 0.78, 2.35, 11.7, [0.23, 0.32, 0.18, 0.17, 0.10], { rowH: 0.44 }); note(s, "注意:这页仍然属于 VOC 后的经营判断,不是先拍脑袋想新品。", 0.95, 4.9, 10.8, C.green); } { const s = newSlide("ACTION", "练习五:把差评变成门店动作", "差评不是用来解释的,是用来找系统问题的。"); table(s, [["差评/吐槽", "归因", "影响程度", "本周动作", "负责人"], ["排队久", "动线/排班/等位体验", "高/中/低", "", ""], ["菜慢", "后厨/高峰/菜品结构", "高/中/低", "", ""], ["觉得贵", "菜单/套餐/价值表达", "高/中/低", "", ""], ["服务冷淡", "培训/激励/流程", "高/中/低", "", ""]], 0.78, 2.35, 11.7, [0.20, 0.32, 0.16, 0.20, 0.12], { rowH: 0.42 }); } { const s = newSlide("MECHANISM", "练习六:把 VOC 变成每周复盘机制", "一次报告有用,持续复盘才会变成经营能力。"); table(s, [["本周观察", "用户证据", "老板判断", "下周动作"], ["高频问题", "", "", ""], ["新增问题", "", "", ""], ["已改善问题", "", "", ""], ["需要继续验证的问题", "", "", ""]], 0.78, 2.35, 11.7, [0.22, 0.32, 0.24, 0.22], { rowH: 0.44 }); note(s, "目标:让员工每周能提交用户证据,让老板每周能做一次问题优先级判断。", 0.95, 5.25, 10.8); } { const s = newSlide("LIVE MODE", "真实采集前先检查 token 和余额", "没有 token 或余额不足时,给充值/开通链接,不要只抛 403。"); codeBox(s, "请切换真实采集。\n行业:本地餐饮\n重点看:用户为什么进店、为什么吐槽、哪些问题值得优先解决。\n平台:小红书和抖音。\n请先检查 token 和余额,再开始采集。", 0.95, 2.35, 6.0, 1.85); bulletList(s, ["没有 token:给开通/充值链接", "余额不足:给充值链接和 sample 方案", "服务不可用:保留参数,恢复后补跑"], 7.45, 2.65, 4.5, { fontSize: 13, gap: 0.5, color: C.red }); } { const s = newSlide("TEAM TASK", "小组实战任务", "每组交付的不是一篇空泛报告,而是一张能执行的问题地图。"); table(s, [["任务", "交付物"], ["建立门店档案", "品类、人均、商圈、招牌菜、目标客群、竞品"], ["跑第一轮 VOC", "样本观察、用户顾虑、证据样本、校准问题"], ["转成问题地图", "产品、价格、服务、环境、传播五类问题"], ["做优先级排序", "P0/P1/P2 问题排序"], ["输出经营动作", "3 个本周可验证动作"], ["复盘计划", "课后一周 VOC 复盘表"]], 0.9, 2.2, 10.8, [0.28, 0.72], { rowH: 0.39 }); } { const s = newSlide("REPORT OUT", "小组汇报模板", "汇报 5 分钟,重点说证据、问题和动作。"); codeBox(s, "我们的门店/品类:\n\n最重要的用户信号:\n1.\n2.\n3.\n\n最值得优先验证的问题:\n\n证据来自:\n\n本周行动:\n1.\n2.\n3.", 0.9, 2.35, 5.8, 2.9); bulletList(s, ["不要讲太多背景", "不要只讲趋势词", "必须引用用户原声", "必须给出一周行动"], 7.3, 2.65, 4.5, { fontSize: 13.2, gap: 0.5, color: C.cyan }); } { const s = newSlide("OPTIONAL", "口播和同城内容:放在最后作为延展", "只有当 VOC 问题足够清楚,内容才不会变成空泛宣传。"); bulletList(s, ["本次主线:VOC 问题发现、证据判断、经营动作", "可选延展:把真实用户问题变成同城选题", "下次工作坊可深化:VOC 转老板 IP / 口播 / 短视频脚本"], 0.95, 2.45, 6.0, { fontSize: 13.5, gap: 0.55 }); codeBox(s, "内容公式:\n用户真实问题 + 门店证据 + 到店理由 = 一条可拍内容", 7.1, 2.65, 4.8, 1.1, 9); } { const s = newSlide("CHECKLIST", "一份好 VOC 报告的验收标准", "看它能不能指导动作,不要只看它像不像咨询报告。"); table(s, [["验收项", "合格标准", "不合格表现"], ["数据口径", "说明平台、关键词、样本数量", "不知道样本从哪来"], ["证据样本", "有标题、作者、互动、原文或评论", "只有观点没有证据"], ["问题地图", "能看清产品、价格、服务、环境、传播问题", "只写抽象趋势"], ["校准问题", "能让老板继续反馈", "没有下一轮问题"], ["行动建议", "有优先级和验证标准", "没有时间、负责人、验证方式"]], 0.75, 2.28, 11.8, [0.18, 0.46, 0.36], { rowH: 0.4, fontSize: 7.1 }); } { const s = newSlide("FAQ", "现场常见问题处理", "课堂不要被工具问题打断业务主线。"); 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 }); } { const s = newSlide("7 DAYS", "课后一周执行清单", "每天只做一个小动作,让 VOC 变成门店机制。"); 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 }); } { const s = newSlide("CLOSE", "结业行动卡", "结束前,每位老板写下一个本周必须验证的问题。"); codeBox(s, "我这两天最有价值的发现是:\n\nVOC 证据提示顾客最在意:\n\n我本周要立刻验证的问题是:\n\n我准备采取的动作是:\n\n我希望一周后看到的验证结果是:", 1.0, 2.35, 6.2, 2.5); bulletList(s, ["不要追求一次看透所有问题", "先验证一个 P0 问题", "让 VOC 成为每周固定动作"], 8.0, 2.75, 3.8, { fontSize: 14, gap: 0.62, color: C.green }); } (async () => { await pptx.writeFile({ fileName: OUT }); console.log(OUT); })().catch((error) => { console.error(error); process.exit(1); });