存档:首页 tab/黑边阶段改动 + 自适应规范文档
- home-page: 底部 tab 贴屏幕底、全新系列角标归位、画布铺满宽修左侧黑边 (首页仍是过程态,后续按「540容器 + 流式」最简方案重构) - AGENTS.md: 自适应规范(给 codex / 同事,三规则 + 三不要 + 验证法) - README.md(外层): 项目总览 + 启动 + 当前进度 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
70
README.md
Normal file
70
README.md
Normal file
@@ -0,0 +1,70 @@
|
||||
# VTuber Mini
|
||||
|
||||
540px 宽的移动端 H5 —— 单 VTuber 粉丝养成 app(养宠物、抽卡、纪念日提醒、商城兑换)。一套模板服务一个 VTuber,可换皮。
|
||||
|
||||
## 技术栈
|
||||
|
||||
React 19 + Vite 8 + Tailwind CSS 4 + TypeScript;Capacitor 8 打包 Android。包管理用 **pnpm**。
|
||||
|
||||
## 目录结构
|
||||
|
||||
```
|
||||
.
|
||||
├── vtubermini/ # 前端主工程(代码都在这)
|
||||
│ ├── src/ # 源码(pages / components / lib / services)
|
||||
│ ├── docs/ # 工程文档(Android 构建、接口契约等)
|
||||
│ ├── AGENTS.md # ⭐ 自适应布局规范(给 codex/AI 调页面用,改 UI 前必读)
|
||||
│ ├── android/ # Capacitor Android 工程
|
||||
│ └── package.json
|
||||
├── design/ # 设计稿
|
||||
├── assets/ # 素材
|
||||
├── Product-Spec.md # 产品规格
|
||||
├── DEV-PLAN.md # 开发计划
|
||||
├── *-Breakdown.md / *.mmd # 需求拆解 / UX 流程图
|
||||
└── .codex-logs/ # codex 运行日志
|
||||
```
|
||||
|
||||
## 快速启动(本地预览)
|
||||
|
||||
```bash
|
||||
cd vtubermini
|
||||
pnpm install # 首次需要
|
||||
pnpm dev # 启动,浏览器打开终端显示的 http://localhost:5173/
|
||||
```
|
||||
|
||||
- 打开后先是登录页 → 点 **「进入花园」** 进首页。
|
||||
- 看不同手机宽度:Chrome 按 `F12` → 设备工具栏 → 选 360 / 390 / 414。
|
||||
- 桌面不开设备模拟时,页面是一个 **540 宽的竖条居中**显示(已去掉手机外壳)。
|
||||
|
||||
## 构建 / Android 打包
|
||||
|
||||
```bash
|
||||
cd vtubermini
|
||||
pnpm build # 类型检查 + 产出 dist/
|
||||
pnpm cap:sync # 把 web 资源同步到 Android 工程
|
||||
```
|
||||
|
||||
Android 打 APK 需要 JDK 17 + Android Studio + `ANDROID_HOME`,详细步骤见 `vtubermini/docs/android-build.md`。
|
||||
|
||||
## 当前进度(自适应改造)
|
||||
|
||||
正在把全站从「固定比例画布 + 百分比绝对定位」改造为「540 宽标准移动布局」,解决**手机宽度变化导致布局错乱**的问题。
|
||||
|
||||
- ✅ **首页** `src/pages/home-page.tsx`:540 容器居中、去手机外壳、底部 tab 贴屏幕底
|
||||
- ⚠️ 首页 tab 样式仍在和需求对齐中(早期方案绕复杂了,正收敛到最简逻辑)
|
||||
- ✅ **自适应规范**已沉淀到 `vtubermini/AGENTS.md`(三条规则 + 两个亲测的坑 + 验证方法)——后续同事用 codex 调其他页照此执行,保证全站做法统一
|
||||
- ⬜ **其他页面**(anniversary / shop / my / album / pet …)待逐页按规范调整
|
||||
|
||||
核心规则(详见 `vtubermini/AGENTS.md`):
|
||||
1. 容器宽 `min(100vw, 540px)` 居中;
|
||||
2. **不用百分比绝对定位**(宽度一变就乱)——用 flex / 文档流 + 贴边;
|
||||
3. 导航 tab 贴屏幕底。
|
||||
|
||||
## 关键文档
|
||||
|
||||
| 文档 | 用途 |
|
||||
|---|---|
|
||||
| `vtubermini/AGENTS.md` | 自适应布局规范(改任何页面 UI 前必读) |
|
||||
| `vtubermini/docs/android-build.md` | Android 构建步骤 |
|
||||
| `vtubermini/docs/api-contract.md` | 前后端接口契约 |
|
||||
| `Product-Spec.md` / `DEV-PLAN.md` | 产品规格 / 开发计划 |
|
||||
43
vtubermini/AGENTS.md
Normal file
43
vtubermini/AGENTS.md
Normal file
@@ -0,0 +1,43 @@
|
||||
# AGENTS.md — VTuber Mini
|
||||
|
||||
> 给 codex / AI agent:在本项目调整页面**自适应布局**时,遵循本文件。
|
||||
> 目标:任何同事用 codex 微调任意页面,做法都统一、不再各搞一套。
|
||||
|
||||
## 这是什么项目
|
||||
540px 宽的移动端 H5(单 VTuber 粉丝养成 app)。React 19 + Vite + Tailwind 4 + Capacitor(Android)。
|
||||
在桌面浏览器里看 = 一个 540 宽的竖条居中显示。
|
||||
|
||||
## 自适应三条规则(就这么简单,别搞复杂)
|
||||
1. **容器**:页面根容器宽 = `min(100vw, 540px)`,水平居中,高 = `100dvh`。窄屏(<540)自动占满 100%。
|
||||
- 全局 `.device-stage` 已是这个规则,页面挂在它里面即可。
|
||||
2. **不要用百分比绝对定位**。元素位置别用 `style={{ left: 'X%', top: 'Y%' }}` 这种绝对定位——手机宽度一变就错乱(这是本项目布局乱的根因)。
|
||||
- 用正常 flex / 文档流;或贴边的 `absolute`(`bottom-0` / `top-0` / `inset-x-0`)。
|
||||
3. **导航 / tab 贴屏幕底**:`absolute inset-x-0 bottom-0` + 内部 flex 等分。背景美术用 `object-cover` 铺满容器。
|
||||
|
||||
## 两个必避的坑(亲测,栽过)
|
||||
1. **Tailwind 4 会静默丢弃自定义 CSS 类里的 `cqw / cqh / container-type`**(Lightning CSS 把整条规则删掉,不报错)。
|
||||
→ 关键尺寸**别写自定义 CSS 类,直接写 inline style,用 `vw/vh`**。例:
|
||||
```tsx
|
||||
<div style={{ width: 'min(100vw, 540px)', aspectRatio: '9 / 16' }} />
|
||||
```
|
||||
2. **`npm run build` 通过 ≠ 生效**。自定义 CSS 可能编译过但运行时根本没渲染(本项目曾让画布 `height` 塌成 0 却一直没发现,误以为成功)。
|
||||
→ 改完**必须实测元素几何**,不能只看编译过。
|
||||
|
||||
## 改完怎么验证(别跳过)
|
||||
```bash
|
||||
npm run build # 1) 先编译过
|
||||
CI=true npm run dev # 2) 跑起来(CI=true 避免后台进程因 stdin 退出)
|
||||
```
|
||||
然后在浏览器实测关键元素的 `getBoundingClientRect()`,确认:
|
||||
- 容器宽 = `min(屏宽, 540)`、`height ≠ 0`;
|
||||
- tab bar 的 `bottom` ≈ 屏幕高(真的贴底)、宽 = 屏幕宽;
|
||||
- 换几个宽度(360 / 400 / 540)布局都不乱。
|
||||
|
||||
> 提示:首页有登录 gate(`!isLoggedIn` → LoginPage,见 `App.tsx`),临时验证可把 `isLoggedIn` 初值改 `true`,**验证后还原**。
|
||||
|
||||
## 参考实现
|
||||
- `src/pages/home-page.tsx`:首页已按上面三条改过(540 容器、去手机外壳、底部 tab 贴屏幕底)——可作为其他页的样板,但**以本文件的规则为准,不要照搬其中的临时痕迹**。
|
||||
- 其他页(anniversary / shop / my / album / pet …)尚未逐页核对,按本规则调整即可。
|
||||
|
||||
## 已废弃
|
||||
`docs/adaptive-architecture.md` 是早期的复杂方案(9:16 固定画布 / scale 缩放 / 分层 / cqw),**已废弃、勿参考**,一切以本文件为准。
|
||||
@@ -454,7 +454,7 @@ export function HomePage({
|
||||
|
||||
return (
|
||||
<div className="relative h-full overflow-hidden bg-[#061124]">
|
||||
<div className="absolute overflow-hidden" style={{ top: 0, left: "50%", width: "min(100vw, 540px, calc(100vh * 9 / 16))", aspectRatio: "9 / 16", transform: "translateX(-50%)" }} onPointerDownCapture={resetHomeIdleTimer}>
|
||||
<div className="absolute overflow-hidden" style={{ top: 0, left: "50%", width: "min(100vw, 540px)", aspectRatio: "9 / 16", transform: "translateX(-50%)" }} onPointerDownCapture={resetHomeIdleTimer}>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="absolute inset-0 bg-[radial-gradient(circle_at_50%_18%,rgba(83,169,255,0.34),transparent_30%),linear-gradient(180deg,#18345f_0%,#071a33_48%,#030a17_100%)]"
|
||||
@@ -710,22 +710,8 @@ export function HomePage({
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<HomeLayoutBox
|
||||
className="z-30"
|
||||
elementId="homeMomentsNewBadge"
|
||||
isEditing={isHomeLayoutEditing}
|
||||
layout={homeLayout}
|
||||
onChange={onHomeLayoutChange}
|
||||
>
|
||||
<div className="home-new-badge-breath relative grid h-full w-full place-items-center text-center text-[clamp(6px,1.85vw,8.5px)] font-semibold leading-[1.05] tracking-[0.06em] text-[#5e3717]">
|
||||
<span className="home-new-badge-shape absolute inset-0 rounded-full border border-white/36 bg-[#f4d79b]/94 shadow-[0_4px_12px_rgba(0,0,0,0.25)]">
|
||||
<span className="absolute -bottom-[7%] left-[18%] h-[22%] w-[22%] rotate-45 border-b border-r border-white/30 bg-[#f4d79b]/94" />
|
||||
</span>
|
||||
<span className="relative">全新<br />系列</span>
|
||||
</div>
|
||||
</HomeLayoutBox>
|
||||
</div>
|
||||
<nav className="absolute inset-x-0 bottom-0 z-30 overflow-hidden flex items-stretch justify-around bg-gradient-to-t from-[#061124]/95 via-[#061124]/70 to-transparent pt-5" style={{ paddingBottom: "max(8px, env(safe-area-inset-bottom))" }}><img src={bottomIconBar} alt="" aria-hidden="true" draggable={false} className="pointer-events-none absolute inset-0 -z-10 h-full w-full object-cover scale-x-[1.12]" />{[{ label: "商城", route: "shop", icon: shopIcon }, { label: "心动时刻", route: "moments", icon: momentsIcon }, { label: "宠物", route: "pet", icon: petIcon }, { label: "心动档案", route: "album", icon: albumIcon }].map((tab) => (<button key={tab.route} type="button" aria-label={tab.label} onClick={() => onNavigate(tab.route as AppRoute)} className="flex flex-1 flex-col items-center gap-1 pb-1 active:scale-95"><img src={tab.icon} alt="" draggable={false} className="h-9 w-9 select-none object-contain drop-shadow-[0_0_10px_rgba(120,200,255,0.6)]" /><span className="text-[10px] font-medium leading-none text-white/90">{tab.label}</span></button>))}</nav>
|
||||
<div className="absolute inset-x-0 bottom-0 z-30 overflow-hidden" style={{ aspectRatio: "108 / 25" }}><img src={bottomIconBar} alt="" aria-hidden="true" draggable={false} className="pointer-events-none absolute inset-0 h-full w-full object-fill scale-x-[1.12]" />{[{ route: "shop", icon: shopIcon, left: "8.1%" }, { route: "moments", icon: momentsIcon, left: "33.7%" }, { route: "pet", icon: petIcon, left: "56.6%" }, { route: "album", icon: albumIcon, left: "80%" }].map((t) => (<button key={t.route} type="button" aria-label={t.route} onClick={() => onNavigate(t.route as AppRoute)} style={{ left: t.left, width: "13.3%" }} className="absolute top-1/2 flex -translate-y-1/2 items-center justify-center active:scale-95">{t.route === "moments" ? (<span className="pointer-events-none absolute -top-1 left-1/2 z-10 -translate-x-1/2 whitespace-nowrap rounded-full border border-white/40 bg-[#f4d79b] px-1.5 py-px text-[8px] font-bold leading-tight text-[#5e3717] shadow-[0_2px_6px_rgba(0,0,0,0.3)]">全新系列</span>) : null}<img src={t.icon} alt="" draggable={false} className="w-full select-none object-contain" /></button>))}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user