This commit is contained in:
谢宇宁
2026-09-15 16:25:45 +07:00
parent da39ab9a2a
commit 2d63d37930
449 changed files with 25632 additions and 24085 deletions
@@ -0,0 +1,519 @@
# 黄果短剧落地页(电脑版 5 变体)实施计划
> **For agentic workers:** 用 superpowers:executing-plans 或 subagent-driven-development 按 Task 执行。步骤用 `- [ ]` 勾选。
>
> **用户规则覆盖(最高优先级):**
> - 不强制 TDD:本计划不写"先写测试"步骤。
> - 收尾只做静态检查(`pnpm exec tsc --noEmit` / `pnpm build`),不跑单测/E2E。
> - 不自动 commit / push / 建 PR:除非用户明确说"提交"。
**Goal:** 把 repo 改造为黄果短剧落地页:1 个共用电脑模板 `PcLanding.vue` + 5 变体素材(背景大图 + 底部封面),进站 5 路随机分流 A~E,并更新 index.html 为简体黄果 SEO。
**Architecture:** Vue 3 `<script setup lang="ts">` + Vite + SCSS`zoom` 缩放(基于 1920×1080),绝对定位 + 图片叠加,`<picture>` webp+png。版面只写一次,`variants.ts` 映射每变体的 `bg`/`covers`。共用元素(logo/主文案/二维码文字烤成图,导航程式画)全变体复用。二维码复用 `DesktopQrcode`
**Tech Stack:** Vue 3 + Vite + SCSS + `qrcode.vue`Figma MCP `get_screenshot`(导出节点 PNG+ `curl` 下载;`pnpm webp`sharp q80)生成 webp。
**规格来源:** `docs/superpowers/specs/2026-05-29-huangguo-landing-design.md`
---
## 关键设计参数(Figma `yD7lloED2Okdp1FJTYvhe3`,变体 1 `73:53`5 变体共用版面)
画布 1920×1080,全部绝对坐标。
| 元素 | 位置 (x,y) | 尺寸 | 说明 |
|---|---|---|---|
| 背景大图 | 0,0 | 1920×1080 | 每变体不同,含底部深色渐变 |
| 顶部 pill 条 | 360,0 | 1200×70 | 半透明圆角,水平居中(CSS 画) |
| logo 锁定组 | 420,15 | 154×40 | 烤图(icon+黄果短剧+标语) |
| 联系方式 容器 | 1094,23 | 406×24 | 3 项 + 圆点分隔(程式画) |
| 主文案 | 204,308 | 630×381 | 烤图 |
| 二维码本体 | 204,750 | 180×180 | 白底圆角,内 150×150 动态二维码 |
| 二维码文字块 | 408,757 | 502×166 | 烤图 |
| 底部封面行 | 1184,778 | 606×152 | 每变体不同,烤成长条图 |
联系方式 3 项(相对 406×24 容器,容器位于顶部内容 (734,8)):
- 官方群组:item @x=0icon 24×24 + 文字 @x=30
- 圆点分隔 @x=124(竖排 4 点,2×2,间距 4px)
- 商务合作:item @x=156
- 圆点分隔 @x=280
- 渠道合作:item @x=312
导出节点 ID
| 变体(版本) | 背景 frame | 轮动 cover row |
|---|---|---|
| 1 (A) | `73:54` | `81:766` |
| 2 (B) | `73:112` | `81:735` |
| 3 (C) | `73:169` | `81:704` |
| 4 (D) | `73:225` | `81:673` |
| 5 (E) | `73:281` | `73:356` |
共用元素(从变体 1 导出):logo `73:76`、主文案 `73:62`、二维码文字 `73:69`、官方群组 icon `73:83`、商务合作 icon `73:93`、渠道合作 icon `73:104`
---
## File Structure
```
src/
├── pages/index.vue # 改:设备检测 + 5 路随机 A~E
├── components/landing/PcLanding.vue # 新:电脑共用模板
├── config/variants.ts # 新:变体素材映射 + 导航链接常量
├── components/desktop/qrcode.vue # 复用(必要时微调 .qr-inner 尺寸)
└── assets/image/hg/
├── common/ logo / theme / qr-text / icon-tg / icon-biz / icon-channel (.png+.webp)
└── pc/ bg1..bg5 / covers1..covers5 (.png+.webp)
index.html # 改:简体黄果 SEO
```
---
# Tasks
## Task 1:从 Figma 导出全部素材 PNG
**Files:**
- Create: `src/assets/image/hg/common/{logo,theme,qr-text,icon-tg,icon-biz,icon-channel}.png`
- Create: `src/assets/image/hg/pc/{bg1..bg5,covers1..covers5}.png`
导出方式:对每个节点调用 `mcp__figma__get_screenshot`fileKey `yD7lloED2Okdp1FJTYvhe3`),拿到 `image_url` 后用 `curl -s -o <目标路径> "<url>"` 下载。背景用默认整合渲染;其余叠加元素用 `contentsOnly: true`(透明底)。
- [ ] **Step 1.1:建目录**
```bash
mkdir -p src/assets/image/hg/common src/assets/image/hg/pc
```
- [ ] **Step 1.2:导出 5 张背景**`maxDimension: 1920`,默认模式)
- 73:54 → `src/assets/image/hg/pc/bg1.png`
- 73:112 → `bg2.png`
- 73:169 → `bg3.png`
- 73:225 → `bg4.png`
- 73:281 → `bg5.png`
- [ ] **Step 1.3:导出 5 张封面长条**`contentsOnly: true``maxDimension: 1212` ≈2×)
- 81:766 → `covers1.png`81:735 → `covers2.png`81:704 → `covers3.png`81:673 → `covers4.png`73:356 → `covers5.png`
- [ ] **Step 1.4:导出共用元素**`contentsOnly: true`
- 73:76 → `common/logo.png``maxDimension: 308`
- 73:62 → `common/theme.png``maxDimension: 1260`
- 73:69 → `common/qr-text.png``maxDimension: 1004`
- 73:83 → `common/icon-tg.png``maxDimension: 72`
- 73:93 → `common/icon-biz.png``maxDimension: 72`
- 73:104 → `common/icon-channel.png``maxDimension: 72`
- [ ] **Step 1.5:核对**
```bash
ls -la src/assets/image/hg/common src/assets/image/hg/pc
```
Expected14 个 png,皆非 0 字节。用 Read 抽看 `theme.png`/`qr-text.png`/`logo.png` 确认是透明底、无背景大图穿透;若穿透,改该节点为 `contentsOnly: true` 重导,或退化为 CSS 文字渲染(见 Task 3 备注)。
- [ ] **Step 1.6:生成 webp**
```bash
pnpm webp
```
Expected:上述 png 同目录生成同名 `.webp`
---
## Task 2:建立 `src/config/variants.ts`
**Files:**
- Create: `src/config/variants.ts`
- [ ] **Step 2.1:写入完整内容**
```ts
import bg1 from '~/assets/image/hg/pc/bg1.png'
import bg1W from '~/assets/image/hg/pc/bg1.webp'
import bg2 from '~/assets/image/hg/pc/bg2.png'
import bg2W from '~/assets/image/hg/pc/bg2.webp'
import bg3 from '~/assets/image/hg/pc/bg3.png'
import bg3W from '~/assets/image/hg/pc/bg3.webp'
import bg4 from '~/assets/image/hg/pc/bg4.png'
import bg4W from '~/assets/image/hg/pc/bg4.webp'
import bg5 from '~/assets/image/hg/pc/bg5.png'
import bg5W from '~/assets/image/hg/pc/bg5.webp'
import c1 from '~/assets/image/hg/pc/covers1.png'
import c1W from '~/assets/image/hg/pc/covers1.webp'
import c2 from '~/assets/image/hg/pc/covers2.png'
import c2W from '~/assets/image/hg/pc/covers2.webp'
import c3 from '~/assets/image/hg/pc/covers3.png'
import c3W from '~/assets/image/hg/pc/covers3.webp'
import c4 from '~/assets/image/hg/pc/covers4.png'
import c4W from '~/assets/image/hg/pc/covers4.webp'
import c5 from '~/assets/image/hg/pc/covers5.png'
import c5W from '~/assets/image/hg/pc/covers5.webp'
export interface PcVariant {
bg: string
bgW: string
covers: string
coversW: string
}
export const PC_VARIANTS: PcVariant[] = [
{ bg: bg1, bgW: bg1W, covers: c1, coversW: c1W },
{ bg: bg2, bgW: bg2W, covers: c2, coversW: c2W },
{ bg: bg3, bgW: bg3W, covers: c3, coversW: c3W },
{ bg: bg4, bgW: bg4W, covers: c4, coversW: c4W },
{ bg: bg5, bgW: bg5W, covers: c5, coversW: c5W },
]
export const VERSION_LETTERS = ['A', 'B', 'C', 'D', 'E'] as const
// 导航链接:官方群组已定,其余为占位,给到网址改这里即可
export const NAV_LINKS = {
group: 'https://t.me/huangguoshortdrama',
biz: '#TODO-business',
channel: '#TODO-channel',
}
```
---
## Task 3:建立 `src/components/landing/PcLanding.vue`
**Files:**
- Create: `src/components/landing/PcLanding.vue`
- [ ] **Step 3.1:写入完整 SFC**
```vue
<template>
<div class="hg-pc">
<!-- 背景大图变体 -->
<div class="bg">
<picture>
<source type="image/webp" :srcset="v.bgW" />
<img :src="v.bg" alt="" />
</picture>
</div>
<!-- 顶部 pill 导航条 -->
<div class="top">
<div class="logo">
<picture>
<source type="image/webp" :srcset="logoW" />
<img :src="logo" alt="黄果短剧" />
</picture>
</div>
<div class="nav">
<a class="item" :href="NAV_LINKS.group">
<picture>
<source type="image/webp" :srcset="iconTgW" />
<img :src="iconTg" alt="官方群组" />
</picture>
<span>官方群組</span>
</a>
<i class="dot" />
<a class="item" :href="NAV_LINKS.biz">
<picture>
<source type="image/webp" :srcset="iconBizW" />
<img :src="iconBiz" alt="商务合作" />
</picture>
<span>商務合作</span>
</a>
<i class="dot" />
<a class="item" :href="NAV_LINKS.channel">
<picture>
<source type="image/webp" :srcset="iconChW" />
<img :src="iconCh" alt="渠道合作" />
</picture>
<span>渠道合作</span>
</a>
</div>
</div>
<!-- 主文案 -->
<div class="theme">
<picture>
<source type="image/webp" :srcset="themeW" />
<img :src="theme" alt="每一帧都能解你的色" />
</picture>
</div>
<!-- 二维码区 -->
<div class="qr">
<div class="qr-box">
<DesktopQrcode />
</div>
<div class="qr-text">
<picture>
<source type="image/webp" :srcset="qrTextW" />
<img :src="qrText" alt="" />
</picture>
</div>
</div>
<!-- 底部封面行变体 -->
<div class="covers">
<picture>
<source type="image/webp" :srcset="v.coversW" />
<img :src="v.covers" alt="" />
</picture>
</div>
</div>
</template>
<script lang="ts" setup>
import { computed, onMounted, onBeforeUnmount } from 'vue'
import DesktopQrcode from '~/components/desktop/qrcode.vue'
import { PC_VARIANTS, NAV_LINKS } from '~/config/variants'
import logo from '~/assets/image/hg/common/logo.png'
import logoW from '~/assets/image/hg/common/logo.webp'
import theme from '~/assets/image/hg/common/theme.png'
import themeW from '~/assets/image/hg/common/theme.webp'
import qrText from '~/assets/image/hg/common/qr-text.png'
import qrTextW from '~/assets/image/hg/common/qr-text.webp'
import iconTg from '~/assets/image/hg/common/icon-tg.png'
import iconTgW from '~/assets/image/hg/common/icon-tg.webp'
import iconBiz from '~/assets/image/hg/common/icon-biz.png'
import iconBizW from '~/assets/image/hg/common/icon-biz.webp'
import iconCh from '~/assets/image/hg/common/icon-channel.png'
import iconChW from '~/assets/image/hg/common/icon-channel.webp'
const props = defineProps<{ variant: number }>()
const v = computed(() => PC_VARIANTS[props.variant] ?? PC_VARIANTS[0])
function resizeBox() {
const w = window.innerWidth
const h = window.innerHeight
const ratio = w / h
const pct = ratio >= 1920 / 1080 ? (h / 1080) * 100 : (w / 1920) * 100
document.body.style.cssText = 'zoom:' + pct + '%'
}
onMounted(() => {
resizeBox()
window.addEventListener('resize', resizeBox)
})
onBeforeUnmount(() => window.removeEventListener('resize', resizeBox))
</script>
<style lang="scss" scoped>
img { -webkit-user-drag: none; }
.hg-pc {
user-select: none;
width: 100%;
height: 100%;
position: relative;
overflow: hidden;
.bg {
position: absolute;
inset: 0;
z-index: 0;
img { width: 100%; height: 100%; object-fit: cover; }
}
.top {
position: absolute;
left: 360px;
top: 0;
width: 1200px;
height: 70px;
z-index: 3;
display: flex;
align-items: center;
background: rgba(255, 255, 255, 0.08);
backdrop-filter: blur(10px);
border-radius: 0 0 20px 20px;
.logo {
position: absolute;
left: 60px;
top: 15px;
width: 154px;
height: 40px;
img { width: 100%; height: 100%; object-fit: contain; }
}
.nav {
position: absolute;
left: 734px;
top: 23px;
height: 24px;
display: flex;
align-items: center;
gap: 32px;
.item {
display: flex;
align-items: center;
gap: 6px;
cursor: pointer;
text-decoration: none;
img { width: 24px; height: 24px; display: block; }
span { color: #fff; font-size: 16px; line-height: 1; white-space: nowrap; }
}
.dot {
width: 2px;
height: 14px;
background-image: radial-gradient(rgba(255,255,255,0.6) 1px, transparent 1px);
background-size: 2px 4px;
}
}
}
.theme {
position: absolute;
left: 204px;
top: 308px;
width: 630px;
height: 381px;
z-index: 2;
pointer-events: none;
img { width: 100%; height: 100%; object-fit: contain; }
}
.qr {
position: absolute;
left: 204px;
top: 750px;
width: 706px;
height: 180px;
z-index: 2;
.qr-box {
position: absolute;
left: 0;
top: 0;
width: 180px;
height: 180px;
background: #fff;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
}
.qr-text {
position: absolute;
left: 204px;
top: 7px;
width: 502px;
height: 166px;
img { width: 100%; height: 100%; object-fit: contain; }
}
}
.covers {
position: absolute;
left: 1184px;
top: 778px;
width: 606px;
height: 152px;
z-index: 2;
pointer-events: none;
img { width: 100%; height: 100%; object-fit: contain; }
}
}
</style>
```
> **备注(若 Task 1.5 发现 theme/qr-text/logo 透明导出失败):** 把对应 `<picture>` 换成 CSS 文字。logo 文字「黃果短劇」18px bold + 标语 12px;主文案标题约 60px bold + 副标 18px;二维码文字三行(24px / 18px semibold / 13px regular)。字体用系统 CJK 栈 `"PingFang SC","Microsoft YaHei",sans-serif`。
---
## Task 4:改造 `src/pages/index.vue` 为 5 路随机分流
**Files:**
- Modify: `src/pages/index.vue`
- [ ] **Step 4.1:替换为以下完整内容**
```vue
<script setup>
import PcLanding from '~/components/landing/PcLanding.vue'
import { computed, onBeforeMount, onBeforeUnmount, ref } from 'vue'
import { useEventTracker, getSessionTraceId } from '~/utils/useEventTracker'
import { VERSION_LETTERS } from '~/config/variants'
const isMobile = computed(() =>
/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)
)
const variantIndex = ref(0)
const version = computed(() => VERSION_LETTERS[variantIndex.value])
const { trackLandingPageView, trackLandingPageClick, generateSessionId } = useEventTracker()
const handleClick = (e) => {
const rawText = e?.target?.alt || e.target.innerText || ''
const tabKey = rawText.replace(/[^A-Za-z0-9_一-鿿]/g, '_').replace(/_{2,}/g, '_').replace(/^_+|_+$/g, '') || ''
const isDownload = tabKey.toLowerCase() === 'download'
trackLandingPageClick({
landing_page_id: `JHA-126${version.value}`,
tab_key: tabKey,
tab_name: rawText,
click_coordinates_x: e.clientX,
click_coordinates_y: e.clientY,
click_x_percent: Math.round((e.clientX / window.innerWidth) * 100),
click_y_percent: Math.round((e.clientY / window.innerHeight) * 100),
screen_width: window.innerWidth,
screen_height: window.innerHeight,
trace_id: isDownload ? getSessionTraceId() : generateSessionId(),
})
}
onBeforeMount(() => {
variantIndex.value = Math.floor(Math.random() * 5)
trackLandingPageView({ landing_page_id: `JHA-126${version.value}` })
window.addEventListener('click', handleClick)
})
onBeforeUnmount(() => {
window.removeEventListener('click', handleClick)
})
</script>
<template>
<!-- 手机版黄果变体待做暂时桌面/手机都先用 PcLanding 占位 -->
<PcLanding :variant="variantIndex" />
</template>
<style scoped></style>
```
> **手机过渡说明:** 黄果手机版未做,此处先全部走 `PcLanding`。手机 Figma 到位后新增 `MobileLanding.vue`,模板改为 `isMobile ? MobileLanding : PcLanding`。`isMobile` 已保留备用。
---
## Task 5:更新 `index.html` 为简体黄果 SEO
**Files:**
- Modify: `index.html`
- [ ] **Step 5.1:替换 `<title>` 与各 meta**
- `<title>``黄果短剧官网 - 竖屏短剧APP下载 | 黄果`
- `description``黄果短剧官网 —— 竖屏短剧新世代。强情节、快节奏、随时随地,把最上瘾的好故事装进你的每一个碎片时间。支持安卓/iOS下载,海量都市情感、甜宠恋爱、古装传奇、悬疑反转短剧,免费在线看。`
- `keywords`:用户提供整串清单(逗号分隔)
- `og:title``黄果短剧 | 竖屏短剧APP下载官方入口``og:site_name``黄果短剧``og:url``https://huangguo.com``og:description` 同 description
- `twitter:title/description/site` 同步为黄果
- `canonical``https://huangguo.com`
- 删除 bili 专用 `rating`adult / RTA)两行
- `favicon` / `og:image`:暂留现有(开放项)
---
## Task 6:静态检查 + 浏览器目检
> 不跑测试。先静态检查,再起 dev server 目检。
- [ ] **Step 6.1:类型检查** `pnpm exec tsc --noEmit` → 无错误
- [ ] **Step 6.2:构建** `pnpm build` → 成功输出 dist/
- [ ] **Step 6.3dev 目检** `pnpm dev`,开 `http://localhost:3000/`,多刷几次确认随机命中不同变体;核对:背景大图、顶部 logo+3 导航、主文案、二维码(扫码=当前页)、底部封面行位置正确;点官方群组跳 `https://t.me/huangguoshortdrama`;缩放窗口无横向滚动条。
---
## Self-Review
- ✅ Spec §2 架构 → Task 2/3/4。✅ §3 坐标 → Task 3 样式。✅ §4 烤图/程式画 → Task 1+3。✅ §5 分流埋点 → Task 4。✅ §6 导航链接常量 → Task 2。✅ §7 二维码现状 → 复用 DesktopQrcode。✅ §8 SEO → Task 5。✅ §9 压缩 → Task 1.6 + `<picture>` + vsharp。
- Placeholderbiz/channel 链接为有意占位(规格 §10 已记),非计划缺漏。
- 类型一致:`PC_VARIANTS`/`NAV_LINKS`/`VERSION_LETTERS` 在 Task 2 定义,Task 3/4 引用一致;`PcVariant` 字段 `bg/bgW/covers/coversW` 与模板用法一致。
- 风险:Figma 文字节点透明导出可能穿背景 → Task 1.5 已加核对 + Task 3 CSS 退化备注。