用于搭建 App Store 或 Google Play 截图生成页面、生成可导出的 iOS/Android 应用宣传营销截图,或使用 Next.js 脚手架快速构建截图编辑器。触发关键词:app store、play store、screenshots、marketing assets、html-to-image、phone mockup、android screenshots、feature graphic。
App Store & Google Play 截图生成器
概述
快速搭建预置的 Next.js + ShadCN 编辑器,帮助用户将 App Store 与 Google Play 截图打造成广告宣传图(而非单纯的 UI 展示图)。编辑器已处理好所有底层繁重工作:
- 连贯画布实时预览:按画布真实分辨率渲染(等比缩放适配屏幕)
- 支持拖拽调整页面顺序、内联文本编辑、单页布局切换
- 跨页样机设计:手机/设备边框、文案标题及图层元素可跨相邻页面拖拽移动,并自动裁剪导出
- 拖拽上传截图(文件保存至
public/screenshots/uploaded/<hash>.png) - 自动保存至项目根目录的
app-store-screenshots.json(可提交至 Git 追踪)+localStorage镜像备份 - 便捷切换 iOS ↔ Android 平台 — 两套幻灯片画布独立并存
- 基于
html-to-image实现一键批量导出符合 Apple/Google 官方分辨率要求的 PNG 格式图片 - 单页支持浅色/深色主题切换、预置主题色板及多语言选择
- 针对旧版项目的引导式原位迁移:在用户主动开启“连贯画布(connected canvas)”前,迁移流程会将旧版画布保持隔离导出,确保兼容性
开箱即用的支持设备:
- iPhone(竖屏)— Apple App Store
- iPad(竖屏)— Apple App Store
- Android Phone(竖屏)— Google Play
- Android Tablet 7"(竖屏 + 横屏)— Google Play
- Android Tablet 10"(竖屏 + 横屏)— Google Play
- Feature Graphic(1024×500 宣传置顶图)— Google Play 商店页横幅
核心理念
应用截图是广告,而不是产品说明书。 每张截图只需推销一个核心卖点。如果你只是平铺直叙地展示 UI,那就做错了 — 你应该卖的是情感体验、最终效果,或者是解决痛点。请直接利用本 Skill 提供的交互式编辑器快速迭代文案与布局,切勿从零手写页面。
本 Skill 的作用
- 将与
SKILL.md同目录的template/预置模板复制到用户的当前工作目录中。 - 使用用户的包管理器安装依赖项。
- 将用户的应用截图放入
public/screenshots/...,应用图标放入public/。 - (可选)在
app-store-screenshots.json中预填应用名称、初始文案、截图路径以及连贯画布偏好设置,确保首次预览即有实际内容展示。 - 启动开发服务器,并提示用户在浏览器中打开编辑器。
请不要手动编写 page.tsx、设备样机边框或导出逻辑,这些代码均已内置于模板中。
步骤 0:检测已有的截图项目
在步骤 1 询问新建项目问题之前,务必先检查当前工作目录是否存在已有的 app-store-screenshots 项目实现。
执行以下轻量级探测命令:
test -f package.json && sed -n '1,220p' package.json
test -f app-store-screenshots.json && sed -n '1,120p' app-store-screenshots.json
rg -n "app-store-screenshots|html-to-image|toPng|ScreenshotEditor|DeckCanvas|connectedCanvas|EXPORT_SIZES|mockup.png|PHONE_SCREEN" package.json src app public 2>/dev/null
find public -maxdepth 4 \( -path "*/screenshots*" -o -name "mockup.png" -o -name "app-icon.png" \) -print 2>/dev/null
当满足以下任一条件时,即视为旧版实现项目:
- 存在
app-store-screenshots.json但缺失schemaVersion``、schemaVersion < 2,或未包含connectedCanvas。 - 存在
src/components/editor/screenshot-editor.tsx但编辑器中未引用DeckCanvas或connectedCanvas。 src/app/page.tsx包含旧版的单文件生成器代码(包含html-to-image、toPng、EXPORT_SIZES、PHONE_SCREEN、硬编码的幻灯片数组/主题等)。- 仓库包含旧版的截图资源目录结构(
public/mockup.png、public/screenshots...)以及截图生成器依赖包配置。
若检测到旧版实现,在执行任何其它操作之前,只需且必须询问用户这一个问题:
我在此处检测到了旧版的 App Store 截图项目。是否需要将该项目迁移至全新的连贯画布(connected-canvas)编辑器?
- 是 — 将现有项目迁移至新版编辑器
- 否 — 采用其它方式配置或修改项目
如果用户选择 是,则不要询问步骤 1 的问卷,直接使用仓库中现有的文件执行下方迁移流程;如果用户选择 否,则继续执行步骤 1。
迁移路径(用户选择“是”时)
目标是完成 UI/模板的原位升级,而非重新设计。保留用户已有的应用名称、文案、截图路径、应用图标、已上传资源、多语言配置及设备画布组合。将旧版 UI 实现替换为当前模板,且除非项目之前已明确开启连贯画布,否则将旧版画布保持在独立导出模式(isolated export mode)。
迁移规则:
- 无需再询问产品/设计细节。 用户已有现有项目,直接从已有文件中推断相关信息,并在末尾汇总汇报不影响运行的遗漏点即可。
- 切勿删除用户资源。 妥善保留
public/screenshots/、public/app-icon.png、已上传的截图以及现有的app-store-screenshots.json。 - 确保可恢复性。 若工作区(worktree)不干净,不要还原无关修改。在覆盖模板文件之前,请将替换掉的项目状态/资源/代码快照复制保存到仓库外的临时备份目录(例如
/tmp/app-store-screenshots-migration-<timestamp>/),并在最终回复中告知该备份路径。 - 优先使用结构化迁移工具。 使用 JSON 工具读取和写入
app-store-screenshots.json,切勿使用正则表达式编辑 JSON。 - 设置
schemaVersion: 2并妥善处理旧版connectedCanvas。 若现有项目已显式包含布尔值connectedCanvas,请予以保留;若项目为 v2 之前版本或缺失该标志,请写入"connectedCanvas": false,防止旧版超屏/裁剪的样机泄漏溢出到相邻页面导出图中。新建项目仍默认使用连贯画布。 - 保持截图文件路径不变。 除非旧项目原本就依赖数字命名且迁移必须用到,否则不要重命名截图文件,使用现有静态路径即可。
- 静默处理自定义主题。 若旧项目引用了自定义
themeId,且能在代码中找到匹配的主题对象,请将其合并到新的src/lib/constants.ts中。若无法恢复,可在项目 JSON 中保留该themeId,编辑器会回退至clean-light主题并给出警告,此时应提示用户需手动恢复自定义主题。 - 尽可能合并 package 元数据。 截图编辑器所需的模板依赖项和脚本优先级最高,但应保留无关的已有
dependencies、devDependencies及有用脚本,除非存在直接冲突。 - 真实迁移中不要导入模板示例画布。 若旧项目已有画布或截图,模板仅用于提供 UI/代码。勿将模板自带的示例截图/画布混入迁移后的项目中,避免用户的应用继承无关示例内容。
- 内测/试运行时使用临时副本。 若用户要求测试或审查迁移过程而非直接迁移项目,请将应用复制到临时目录或 worktree 中进行迁移验证。只有在用户明确要求真实迁移并回答 是 时,才操作真实项目目录。
推荐的迁移步骤:
# 1. 将有用旧文件快照备份到仓库外部
STAMP=$(date +%Y%m%d-%H%M%S)
BACKUP_DIR="/tmp/app-store-screenshots-migration-$STAMP"
mkdir -p "$BACKUP_DIR"
cp -R app-store-screenshots.json public src package.json tailwind.config.ts next.config.mjs "$BACKUP_DIR/" 2>/dev/null || true
# 2. 保留复制模板时必须保留的项目状态与资源
PRESERVE_DIR="$BACKUP_DIR/preserve"
mkdir -p "$PRESERVE_DIR"
cp app-store-screenshots.json "$PRESERVE_DIR/" 2>/dev/null || true
cp -R public/screenshots "$PRESERVE_DIR/screenshots" 2>/dev/null || true
cp public/app-icon.png "$PRESERVE_DIR/app-icon.png" 2>/dev/null || true
# 3. 将当前模板覆盖到旧版 UI 实现上
cp -R "<SKILL_DIR>/template/." "$PWD/"
cp app-store-screenshots.json "$BACKUP_DIR/template-app-store-screenshots.json" 2>/dev/null || true
# 4. 将保留的用户状态/资源还原并覆盖模板示例文件
cp "$PRESERVE_DIR/app-store-screenshots.json" app-store-screenshots.json 2>/dev/null || true
mkdir -p public
if [ -d "$PRESERVE_DIR/screenshots" ]; then
mkdir -p "$BACKUP_DIR/template-samples/public"
mv public/screenshots "$BACKUP_DIR/template-samples/public/screenshots" 2>/dev/null || true
cp -R "$PRESERVE_DIR/screenshots" public/screenshots
else
mkdir -p public/screenshots
fi
cp "$PRESERVE_DIR/app-icon.png" public/app-icon.png 2>/dev/null || true
复制完成后,升级或新建 app-store-screenshots.json。如果已存在项目配置文件,则进行就地适配。如果不存在项目文件,但旧幻灯片数据嵌套在 src/lib/defaults.ts 或 src/app/page.tsx 中,请尽最大努力将其提取并填入模板的项目 JSON 中,然后再考虑回退至初始默认页面。提取时优先识别名为 slides、screens、features、defaultSlides、appName、tagline、theme 的旧版数组或对象以及截图路径。如果旧实现中仅有图片文件,请将 public/screenshots/** 按照路径排序,并据此生成初始幻灯片数据。
使用如下小型 JSON 脚本完成最终的项目状态兼容修正:
BACKUP_DIR="$BACKUP_DIR" node <<'NODE'
const fs = require("fs");
const path = require("path");
const PROJECT_FILE = "app-store-screenshots.json";
const DEFAULT_LOCALE = "en";
const DEVICE_KEYS = ["iphone", "ipad", "android", "android-7", "android-10", "feature-graphic"];
const LAYOUTS = ["hero", "device-bottom", "device-top", "two-devices", "no-device", "split-landscape", "feature-graphic"];
function readJson(file) {
try {
return JSON.parse(fs.readFileSync(file, "utf8"));
} catch {
return null;
}
}
const templateState =
readJson(path.join(process.env.BACKUP_DIR || "", "template-app-store-screenshots.json")) ||
readJson(PROJECT_FILE) ||
{};
const existingState = readJson(PROJECT_FILE) || {};
const hasExplicitConnectedCanvas = typeof existingState.connectedCanvas === "boolean";
const existingDecks =
existingState.slidesByDevice && typeof existingState.slidesByDevice === "object"
? existingState.slidesByDevice
: {};
const hasExistingDecks = Object.keys(existingDecks).length > 0;
const state = {
...templateState,
...existingState,
slidesByDevice: hasExistingDecks ? existingDecks : templateState.slidesByDevice || {},
};
const legacySlides =
Array.isArray(existingState.slides) ? existingState.slides :
Array.isArray(existingState.screens) ? existingState.screens :
Array.isArray(existingState.features) ? existingState.features :
null;
if (legacySlides && !hasExistingDecks) {
state.slidesByDevice = {
iphone: legacySlides,
};
}
function localized(value) {
if (typeof value === "string") return { [DEFAULT_LOCALE]: value };
if (value && typeof value === "object") return value;
return {};
}
function cleanTransform(value) {
if (!value || typeof value !== "object") return undefined;
const { x, y, width, height, rotation, zIndex } = value;
if (![x, y, width, height].every((n) => typeof n === "number" && Number.isFinite(n))) return undefined;
return {
x,
y,
width: Math.max(1, width),
height: Math.max(1, height),
...(typeof rotation === "number" && Number.isFinite(rotation) ? { rotation } : {}),
...(typeof zIndex === "number" && Number.isFinite(zIndex) ? { zIndex } : {}),
};
}
function firstString(...values) {
return values.find((value) => typ
<!-- truncated for translation batch; full body continues in source -->






