app-store-screenshots

app-store-screenshots

熱門

當需要建置 App Store 或 Google Play 截圖頁面、產生可匯出的 iOS 及/或 Android 應用程式宣傳截圖,或是使用 Next.js 快速搭建截圖編輯器時使用。觸發詞包含:app store、play store、screenshots(截圖)、marketing assets(行銷素材)、html-to-image、phone mockup(手機樣機)、android screenshots、feature graphic(宣傳圖)。

6024星標
0分支
更新於 2026/7/11
SKILL.md
唯讀
名稱
app-store-screenshots
描述

當需要建置 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 展示圖)。該編輯器包辦了所有繁重的工作:

  • 按照畫布真實解析度的即時連動預覽(縮放至合適大小)
  • 拖曳重新排序畫面、單元內文字編輯、各畫面獨立的版面切換器
  • 跨畫面 Mockup:手機/裝置外框、標題文案與分層元素可跨越相鄰畫面移動,並匯出為裁切後的成果
  • 支援拖放上傳的截圖選擇器(檔案 → 儲存至 public/screenshots/uploaded/<hash>.png
  • 自動儲存至專案根目錄的 app-store-screenshots.json(可納入 git 版本控制)+ localStorage 鏡像同步
  • 輕鬆切換 iOS ↔ Android 平台 — 分開的卡片集(Slide Decks)可並排運作
  • 透過 html-to-image 一鍵批次匯出符合 Apple/Google 所有指定解析度的 PNG 檔
  • 單張投影片可切換淺色/深色模式、主題預設集、語系選擇
  • 針對此 Skill 建立的舊版專案提供引導式原地轉移(Migration);被動與顯式轉移會保持舊版卡片集隔離,直到使用者主動選擇啟用連動畫布(Connected Canvas)

開箱即用的支援裝置:

  • iPhone(直向)— Apple App Store
  • iPad(直向)— Apple App Store
  • Android 手機(直向)— Google Play
  • Android 7 吋平板(直向 + 橫向)— Google Play
  • Android 10 吋平板(直向 + 橫向)— Google Play
  • Feature Graphic(1024×500 橫幅)— Google Play 應用程式頁面頂部宣傳圖

核心原則

截圖是廣告宣傳,不是說明文件。 每張截圖都只賣一個核心理念。如果你只是展示 UI,那就做錯了 — 你賣的是一種感覺、一種成果,或是擊中使用者痛點的解方。請善用此 Skill 的互動式編輯器來快速迭代文案與排版;切勿從零開始手動打造頁面。

此 Skill 的工作流程

  1. 複製預先建置好的範本:從 template/(與此 SKILL.md 位於同目錄)複製檔案至使用者的工作目錄。
  2. 使用使用者的套件管理器安裝相依套件。
  3. 將使用者的截圖放進 public/screenshots/...,並將 App 圖示放進 public/
  4. (可選)預先填充 app-store-screenshots.json,寫入使用者的 App 名稱、初始文案、截圖與連動畫布偏好設定,讓首次預覽就能看到實際效果。
  5. 啟動開發伺服器(dev server),並提示使用者在瀏覽器中開啟編輯器。

不應該手寫 page.tsx、裝置外框(device frames)或匯出邏輯。這些都已經包含在範本中了。

步驟 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 但缺少 schemaVersionschemaVersion < 2,或缺少 connectedCanvas 欄位。
  • 存在 src/components/editor/screenshot-editor.tsx 但編輯器中未引用 DeckCanvasconnectedCanvas
  • src/app/page.tsx 包含舊版的全功能單一檔案產生器(如 html-to-imagetoPngEXPORT_SIZESPHONE_SCREEN、硬編碼的投影片陣列/主題)。
  • 專案庫中包含舊版的截圖資產佈局(public/mockup.pngpublic/screenshots...)以及截圖產生器套件設定。

如果偵測到舊版實作,在進行任何其他操作前,請只詢問一個問題:

在此處偵測到舊版的 App Store 截圖專案。請問是否要將現有專案轉移(Migrate)至新的連動畫布(connected-canvas)編輯器?

  1. 是 — 將現有專案轉移至新版編輯器
  2. 否 — 以其他方式設定或修改專案

如果使用者選擇 ,請不要詢問步驟 1 的問卷。請使用專案庫中現有的檔案執行下方的轉移流程。如果使用者選擇 ,請繼續進行步驟 1。

轉移流程(當使用者選擇「是」時)

目標是進行原地 UI/範本升級,而不是重新設計。只要現有檔案中已存在使用者的 App 名稱、文案、截圖路徑、App 圖示、已上傳資產、語系和裝置卡片集,就必須完整保留。用當前範本替換舊的 UI 實作。除非專案已經明確啟用連動畫布,否則請保持舊版卡片集於獨立匯出模式。

轉移規則:

  1. 切勿詢問更多產品/設計問題。 使用者已經有現成專案。請從現有檔案中進行推斷,並在最後報告任何不影響運作的缺失資訊。
  2. 絕不刪除使用者資產。 務必保留 public/screenshots/public/app-icon.png、已上傳的截圖以及任何現有的 app-store-screenshots.json
  3. 保持可復原性。 若工作區(worktree)不乾淨,請勿復原無關的變更。在覆寫範本檔案前,將被替換的專案狀態/資產/程式碼快照複製到專案外的臨時備份目錄(例如 /tmp/app-store-screenshots-migration-<timestamp>/),並在最終回覆中註明該路徑。
  4. 優先使用結構化轉移。 請使用 JSON 工具讀取與寫入 app-store-screenshots.json。請勿使用 Regex 編輯 JSON。
  5. 設定 schemaVersion: 2 並維護舊版 connectedCanvas 的安全性。 如果現有專案已有明確的布林值 connectedCanvas,請予以保留。如果專案是 v2 之前的版本或缺少此標記,請寫入 "connectedCanvas": false,以防螢幕外/裁切的舊版 Mockup 溢出到相鄰的匯出圖中。新專案預設仍為連動畫布。
  6. 保持截圖路徑指向現有檔案。 除非舊專案原本就依賴數字命名且轉移過程需要它們,否則請勿重新命名截圖檔案。保留現有的靜態路徑即可。
  7. 自動處理自訂主題(無須詢問)。 如果舊專案引用了自訂的 themeId,且能在舊檔案中找到對應的主題物件,請將該主題物件合併到全新的 src/lib/constants.ts 中。如果無法復原,請在專案 JSON 中保留該 themeId;編輯器會自動降級回 clean-light 並發出警告,同時你應提醒使用者需要手動復原自訂主題。
  8. 盡可能合併 package 元數據。 截圖編輯器必須以範本的 dependencies 與 scripts 為準,但應保留無衝突的現有 dependenciesdevDependencies 和實用指令,除非直接發生衝突。
  9. 請勿將範本範例卡片集匯入實際轉移中。 如果舊專案已有卡片集或截圖,請僅將範本用於 UI/程式碼。轉移後的專案中不要包含範本的範例截圖/卡片集,以免使用者的 App 繼承無關的範例內容。
  10. 使用一次性副本進行 Dogfooding 測試。 如果使用者要求測試或審查轉移過程而非直接轉移專案,請將 App 複製到臨時目錄或工作區並在該處執行轉移。只有在使用明確要求實際轉移並回答 時,才可動用真實的程式碼庫。

推薦的轉移步驟命令序列:

# 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。若已存在專案檔案,請原地修正(coerce)。若不存在專案檔案但舊的投影片資料嵌入在 src/lib/defaults.tssrc/app/page.tsx 中,請盡力將其擷取至範本的專案 JSON 中,若失敗再降級使用預設投影片。請優先尋找名為 slidesscreensfeaturesdefaultSlidesappNametaglinetheme 和截圖路徑的舊陣列或物件。若舊實作中僅有圖片檔案,請依路徑排序 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