Files
huangguo_landpage/docs/superpowers/plans/2026-05-29-huangguo-pc-landing.md
2026-09-15 16:25:45 +07:00

520 lines
18 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 黄果短剧落地页(电脑版 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 退化备注。