Blush Editorial / 暖粉编辑志

把文档变成会动、能改、能分享的 HTML 网页 PPT

Pretty HTML PPT 是一个面向 AI Agent 的演示型 Skill,用 12 套真实模板把文档、文章、图片、链接、截图、数据和作品集整理成可打开、可编辑、可发布的网页 PPT。

Why this exists

普通 AI PPT 最大的问题,是好看但不一定可用。

这套 skill 解决的是真实交付问题:中文清晰、结构可控、生成后还能继续修改和发布。

图片式生成

看起来像 PPT,但本质是图片。中文放大容易糊,文字和版式很难二次调整。

模板靠抽卡

每次都要赌结果,适合展示的很少,适合正式汇报的更少。

交付不连续

文档、PPT、网页、发布链接彼此割裂,内容改一遍,视觉还要再排一遍。

Workflow

从文档和素材到网页 PPT,中间只保留必要判断。

先判断受众、场景、内容密度和素材状态,再选模板、拆页面、生成 HTML,最后按 E 进入编辑或按 P 演讲。

文档 / 图片 / 截图 / 数据

把原始材料交给 AI Agent,不要求用户先手动改成 PPT 大纲。

场景与密度判断

区分演讲型、分享型、报告型、教程型,不把长文硬塞进一页。

套用真实模板

从 12 套模板复制开始,保留配色、版式、导航和动效。

打开、编辑、发布

本地浏览器打开;按 E 进入编辑,输入 E 不受影响,按 Esc 退出;按 P 进入演讲者模式。

Source assets

文档里的链接、图片和来源,不应该在生成 PPT 时丢掉。

Skill 会先抽取链接和素材,再判断它们应该变成按钮、来源标记、图片证据还是附录资料。

Image matching

图片按语义匹配

人物照、产品截图、数据图、旧 PPT 截图,会被放到能解释它们的页面里。

截图 对应观点
Source proof

来源和引用保留

飞书原文、视频、资料、引用文章,可以放成来源 chip 或附录链接。

Template map

12 套模板,分成两种使用场景。

一组负责更有记忆点的公开展示,一组负责更稳的职场、政务和产品汇报。

自媒体 / 个人展示 / 作品集 Creative

更适合文章分享、课程教程、个人项目、作品集和产品发布。

Pastel Blockfolio粉彩拼贴志
Blush Editorial暖粉编辑志
Mono Curve Slides墨线白稿
Ribbon Tab Brochure彩签页报
Blue Growth Deck蓝色增长稿
Garden Pop Landing花园跳色长页

行政政务 / 职场汇报 / 产品演讲 Report

更适合领导汇报、项目复盘、商务提案、答辩和正式材料。

One Dot Cinnabar一点丹红
Ivory Research Deck象牙研稿
Cobalt Executive Deck钴蓝商策
Coral Startup Deck珊瑚企简
Sapphire Defense Deck宝蓝答辩稿
Vermilion Civic Deck红色汇报稿
Install and prompt

用户只需要说清楚用途,Skill 负责判断模板。

安装后可以自动选模板,也可以指定 Cobalt、Blush、Mono 等某一套模板。适合 Codex、Claude Code、Cursor、Windsurf、Workbody 等 AI Agent 工作流。

InstallSkill
npx -y skills@latest add XshuiAi/Pretty-HTML-PPT \
  --skill pretty-html-ppt \
  --agent codex \
  --global
PromptDocument to deck
使用 $pretty-html-ppt,把这份文档和素材做成一份适合对外分享的 HTML 网页 PPT。
要求:
1. 先判断内容更适合哪一套模板;
2. 把长内容拆成封面、钩子、核心观点、案例、流程、总结;
3. 图片、截图、表格和旧 PPT 素材要转成页面里的视觉证据;
4. 保留文档里的重要链接,并让它们可以点击跳转;
5. 如果有数据,转成图表或可交互测算组件;
6. 保留可编辑模式、演讲者模式和网页发布能力。
Live presentation

它不是静态页面,而是一套可以拿去讲的演示作品。

按 P 打开讲稿备注、下一页预览和计时器;按 E 进入编辑,编辑时 E 正常输入,按 Esc 退出。

P

演讲者模式

展示当前页标题、摘要、下一页标题、讲稿备注和演讲计时。

E

浏览器编辑

生成后可直接点文字修改,再导出一份改好的 HTML 文件。

HTML

发布链接

静态文件可以放到 GitHub Pages、Cloudflare Workers 或任意静态托管。

Image layout

插入图片之后,不应该只丢在页面底部。

编辑模式里的图片会变成可拖动的画面块,可以吸附到左侧、居中、右侧或置底,也可以快速切换尺寸。

图片变成可以摆放的内容块

适合人物照片、产品截图、案例证据、课程封面和数据图。用户上传后可以拖动,靠近对齐点会自动吸附。

拖动 / 吸附 / 调尺寸
Data widget

数据不只能做成表格,也可以变成可调参数的演示组件。

适合现金流测算、预算规划、增长率假设、KPI 变化和产品方案对比。拖动参数,结果会实时变化。

Example calculator

现金流测算示例

--

拖动右侧参数,查看不同投入和年份下的估算结果。

示例测算,不构成投资或财务建议。
Use cases

它可以做的不只是 PPT,而是更容易传播的内容展示。

同一套能力可以服务公开分享、个人作品集、课程产品、数据汇报和正式提案。

01 Creator share

自媒体分享

把文章、脚本和教程变成一套有节奏的网页 PPT,适合公开发布和二次传播。

02 Portfolio

个人作品集

把项目经历、作品截图和个人介绍做成可以演示也可以发链接的作品页。

03 Product pitch

产品路演

把产品介绍、功能截图、案例和方案整理成可以讲给客户或团队看的展示稿。

04 Work report

职场汇报

把数据表、项目复盘和工作总结转成结构清楚、能现场讲的报告页。

Final message

这不是把内容装进模板,而是把内容变成可以被讲出来的作品。

Pretty HTML PPT 的重点,是让文档、演示、编辑和发布变成同一条链路。

12 templates Editable HTML Presenter Mode Shareable Deck
(() => { const slides = [...document.querySelectorAll("[data-slide]")]; const navLinks = [...document.querySelectorAll(".nav a[href^='#']")]; const dotNav = document.querySelector(".demo-dots"); let activeIndex = 0; document.documentElement.classList.add("demo-enhanced"); const progress = document.querySelector("[data-demo-progress] span"); function updateProgress() { const max = Math.max(1, document.documentElement.scrollHeight - window.innerHeight); const percent = Math.max(0, Math.min(100, window.scrollY / max * 100)); if (progress) progress.style.setProperty("--progress", percent + "%"); } document.addEventListener("pointermove", (event) => { if (isEditing()) return; document.documentElement.style.setProperty("--mx", event.clientX + "px"); document.documentElement.style.setProperty("--my", event.clientY + "px"); }, { passive: true }); window.addEventListener("scroll", updateProgress, { passive: true }); updateProgress(); if (dotNav && !dotNav.children.length) { dotNav.innerHTML = slides.map((slide, index) => ( `${String(index + 1).padStart(2, "0")}` )).join(""); } const dotLinks = [...document.querySelectorAll("[data-demo-dot]")]; function isEditing() { return document.body.classList.contains("xs-editing"); } function updateUI(index) { activeIndex = Math.max(0, Math.min(slides.length - 1, index)); const current = slides[activeIndex]; navLinks.forEach((link) => { link.classList.toggle("is-active", link.getAttribute("href") === "#" + current.id); }); dotLinks.forEach((link, linkIndex) => { link.classList.toggle("is-active", linkIndex === activeIndex); }); } function goTo(index) { if (!slides.length) return; const next = Math.max(0, Math.min(slides.length - 1, index)); slides[next].scrollIntoView({ behavior: "smooth", block: "start" }); updateUI(next); } function go(delta) { goTo(activeIndex + delta); } document.querySelector("[data-demo-next]")?.addEventListener("click", () => go(1)); dotLinks.forEach((link, index) => { link.addEventListener("click", (event) => { event.preventDefault(); goTo(index); }); }); document.addEventListener("keydown", (event) => { const tag = document.activeElement?.tagName?.toLowerCase(); if (isEditing() || tag === "input" || tag === "textarea" || tag === "select") return; if (event.key === "ArrowRight" || event.key === "PageDown") { event.preventDefault(); go(1); } if (event.key === "ArrowLeft" || event.key === "PageUp") { event.preventDefault(); go(-1); } }); const revealItems = [ ...slides, ...document.querySelectorAll(".card, .step, .module, .code-card, .feature-card, .roadmap-card, .source-card, .data-result, .data-controls, .layout-controls button, .layout-preview") ]; revealItems.forEach((item) => item.classList.add("demo-reveal")); slides[0]?.classList.add("is-visible"); const motionCards = [...document.querySelectorAll(".card, .step, .module, .code-card, .feature-card, .roadmap-card, .source-card")]; motionCards.forEach((card) => { card.dataset.tilt = "true"; card.addEventListener("pointermove", (event) => { if (isEditing()) return; const rect = card.getBoundingClientRect(); const x = (event.clientX - rect.left) / rect.width - .5; const y = (event.clientY - rect.top) / rect.height - .5; card.style.setProperty("--tilt-x", (x * 5).toFixed(2) + "deg"); card.style.setProperty("--tilt-y", (-y * 5).toFixed(2) + "deg"); }); card.addEventListener("pointerleave", () => { card.style.setProperty("--tilt-x", "0deg"); card.style.setProperty("--tilt-y", "0deg"); }); card.addEventListener("click", (event) => { if (isEditing() || card.querySelector(".demo-ripple")) return; const rect = card.getBoundingClientRect(); const ripple = document.createElement("span"); ripple.className = "demo-ripple"; ripple.style.left = (event.clientX - rect.left) + "px"; ripple.style.top = (event.clientY - rect.top) + "px"; card.appendChild(ripple); ripple.addEventListener("animationend", () => ripple.remove(), { once: true }); }); }); document.querySelectorAll(".workflow-grid, .feature-grid, .roadmap-grid").forEach((grid) => { grid.addEventListener("pointerover", (event) => { const item = event.target.closest(".step, .feature-card, .roadmap-card"); if (!item || isEditing()) return; [...grid.children].forEach(child => child.classList.toggle("is-hot", child === item)); }); grid.addEventListener("pointerleave", () => { [...grid.children].forEach(child => child.classList.remove("is-hot")); }); }); if ("IntersectionObserver" in window) { const revealObserver = new IntersectionObserver((entries, observer) => { entries.forEach((entry) => { if (!entry.isIntersecting) return; entry.target.classList.add("is-visible"); observer.unobserve(entry.target); }); }, { threshold: .16 }); revealItems.forEach((item) => revealObserver.observe(item)); const slideObserver = new IntersectionObserver((entries) => { entries.forEach((entry) => { if (!entry.isIntersecting) return; updateUI(slides.indexOf(entry.target)); }); }, { threshold: .45 }); slides.forEach((slide) => slideObserver.observe(slide)); } else { revealItems.forEach((item) => item.classList.add("is-visible")); } document.querySelectorAll("[data-detail]").forEach((card) => { const detail = document.createElement("div"); detail.className = "demo-detail"; detail.textContent = card.dataset.detail; card.appendChild(detail); card.setAttribute("tabindex", "0"); card.setAttribute("role", "button"); card.setAttribute("aria-expanded", "false"); const toggle = () => { if (isEditing()) return; const open = card.classList.toggle("is-expanded"); card.setAttribute("aria-expanded", String(open)); }; card.addEventListener("click", toggle); card.addEventListener("keydown", (event) => { if (event.key === "Enter" || event.key === " ") { event.preventDefault(); toggle(); } }); }); const layoutPreview = document.querySelector("[data-layout-preview]"); const layoutTitle = document.querySelector("[data-layout-title]"); const layoutText = document.querySelector("[data-layout-text]"); const layoutNote = document.querySelector("[data-layout-note]"); const layoutCopy = { right: { title: "右图左文,适合讲产品和人物", text: "文字先把观点讲清楚,图片作为证据放在右侧。适合产品截图、案例图和人物介绍。", note: "右侧吸附" }, left: { title: "左图右文,适合做案例拆解", text: "图片先进入视线,再用右侧文字解释重点。适合作品集、界面截图和前后对比。", note: "左侧吸附" }, center: { title: "居中大图,适合视觉主场", text: "让图片成为这一页的主角,文字退到下方。适合封面图、作品预览和课程海报。", note: "居中吸附" }, bottom: { title: "置底横图,适合保留讲述空间", text: "上方继续讲逻辑,下方放截图、流程或数据图。适合汇报页和教程页。", note: "底部吸附" } }; document.querySelectorAll("[data-layout-choice]").forEach((button) => { button.addEventListener("click", () => { if (isEditing() || !layoutPreview) return; const choice = button.dataset.layoutChoice || "right"; document.querySelectorAll("[data-layout-choice]").forEach(item => { item.classList.toggle("is-active", item === button); }); layoutPreview.className = "layout-preview layout-" + choice; layoutTitle.textContent = layoutCopy[choice].title; layoutText.textContent = layoutCopy[choice].text; layoutNote.textContent = layoutCopy[choice].note; }); }); const savingsWidget = document.querySelector("[data-savings-widget]"); if (savingsWidget) { const formatter = new Intl.NumberFormat("zh-CN", { maximumFractionDigits: 0 }); const money = (value) => "¥" + formatter.format(Math.round(value)); const input = (name) => savingsWidget.querySelector(`[data-input="${name}"]`); const value = (name) => savingsWidget.querySelector(`[data-value="${name}"]`); const result = savingsWidget.querySelector("[data-savings-result]"); const summary = savingsWidget.querySelector("[data-savings-summary]"); const bars = { principal: savingsWidget.querySelector('[data-bar="principal"]'), growth: savingsWidget.querySelector('[data-bar="growth"]'), total: savingsWidget.querySelector('[data-bar="total"]') }; function updateSavings() { const initial = Number(input("initial").value); const monthly = Number(input("monthly").value); const rate = Number(input("rate").value) / 100; const years = Number(input("years").value); const months = years * 12; const monthlyRate = rate / 12; let total = initial; for (let index = 0; index < months; index += 1) { total = total * (1 + monthlyRate) + monthly; } const principal = initial + monthly * months; const growth = Math.max(0, total - principal); const max = Math.max(total, principal, 1); value("initial").textContent = formatter.format(initial); value("monthly").textContent = formatter.format(monthly); value("rate").textContent = (rate * 100).toFixed(rate * 100 % 1 ? 1 : 0) + "%"; value("years").textContent = years + " 年"; result.textContent = money(total); summary.textContent = `${years} 年后估算约 ${money(total)},其中本金 ${money(principal)},假设增长 ${money(growth)}。`; bars.principal.style.setProperty("--bar", Math.max(18, principal / max * 82) + "%"); bars.growth.style.setProperty("--bar", Math.max(18, growth / max * 82) + "%"); bars.total.style.setProperty("--bar", Math.max(18, total / max * 96) + "%"); } savingsWidget.querySelectorAll("[data-input]").forEach((control) => { control.addEventListener("input", updateSavings); }); updateSavings(); } updateUI(0); })();