diff --git a/docs/superpowers/plans/2026-04-19-puro-ai-landing-auth.md b/docs/superpowers/plans/2026-04-19-puro-ai-landing-auth.md new file mode 100644 index 00000000..265d2a14 --- /dev/null +++ b/docs/superpowers/plans/2026-04-19-puro-ai-landing-auth.md @@ -0,0 +1,1746 @@ +# PURO AI · Landing + Auth + Docs Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** 在 `ai.puro.im` 上线 PURO AI 品牌化的 Landing、Login、Register、Docs 四个页面,落地 `puro.css` 作为全站新设计系统,保持所有后台页面和现有 Auth 逻辑不变。 + +**Architecture:** 复用 Claude Design 产出的 `docs/design-drafts/v2/` 静态 HTML,翻译成 Vue 3 SFC 组件,CSS 通过全局引入 `puro.css`(含 tokens + primitives),路由改造让 `/` 根据登录态动态展示 Landing 或 Dashboard。所有 hardcoded 中文通过 vue-i18n 的 `zh.ts` 集中管理。 + +**Tech Stack:** Vue 3.4+ (Composition API, TypeScript), Vue Router 4, vue-i18n, Pinia, Tailwind CSS(保留 legacy primary 色板不动),Vite 5, pnpm 10, vitest(router 用)。 + +**Spec:** `docs/superpowers/specs/2026-04-19-puro-ai-landing-auth-design.md`(第 3/4/4.5/6 节为本计划权威依据) + +**Branch:** `feat/design-landing-auth`(已创建 · 当前 HEAD `3a16b3ec`) + +--- + +## 文件结构 + +``` +frontend/ +├── index.html ← MODIFY 加 Google Fonts preconnect + link +├── src/ +│ ├── assets/ +│ │ └── puro.css ← NEW 复制 docs/design-drafts/v2/puro.css +│ ├── main.ts ← MODIFY import './assets/puro.css' +│ ├── router/ +│ │ └── index.ts ← MODIFY `/` auth-aware + `/docs` public +│ ├── views/ +│ │ ├── landing/ +│ │ │ └── LandingView.vue ← NEW 6 sections, dashboard mockup 静态 +│ │ ├── docs/ +│ │ │ └── DocsView.vue ← NEW 6 subsections, curl/codex/claude-code 示例 +│ │ └── auth/ +│ │ ├── LoginView.vue ← MODIFY 套新 narrative slot + 小幅改文案 +│ │ └── RegisterView.vue ← MODIFY 同上 +│ ├── components/ +│ │ └── layout/ +│ │ └── AuthLayout.vue ← MODIFY 加 `narrative` 命名 slot(条件渲染) +│ └── i18n/ +│ └── locales/ +│ └── zh.ts ← MODIFY 新增 `landing.*` / `docs.*` / 扩 `auth.*` +└── tailwind.config.js ← MODIFY extend color 加 puro.* 家族(给 Vue 用) +``` + +**不动的文件**:所有 `views/admin/**`、`views/user/**`、`views/setup/**`、`App.vue`、`AppHeader/AppLayout/AppSidebar.vue`、backend 全部。 + +**设计决策**: +- `puro.css` 里已有的 primitives class(`.btn-primary`, `.card`, `.input` 等)直接用 class 引用,**不抽 Vue 组件**(减少 scope 膨胀) +- Tailwind 现有 `primary`(teal #14b8a6)**不改**——admin 页还用。新加 `puro.cyan` / `puro.purple` / `puro.amber` 等命名空间,新页面用 `text-puro-cyan` 类 +- i18n 只补 `zh.ts`(本期默认中文);`en.ts` 键留空或复用 `zh` 值 + +--- + +## Task 1: 引入 puro.css 设计系统 + Google Fonts + +**Files:** +- Create: `frontend/src/assets/puro.css` +- Modify: `frontend/src/main.ts:7` +- Modify: `frontend/index.html` + +- [ ] **Step 1: 复制 puro.css 到 assets** + +```bash +cp /Users/mini/Work/dev/sub2api/docs/design-drafts/v2/puro.css \ + /Users/mini/Work/dev/sub2api/frontend/src/assets/puro.css +``` + +- [ ] **Step 2: 在 main.ts 里 import(紧接 style.css)** + +Modify `frontend/src/main.ts`,把第 7 行: + +```ts +import './style.css' +``` + +改为: + +```ts +import './style.css' +import './assets/puro.css' +``` + +- [ ] **Step 3: 在 index.html `` 里加 Google Fonts preconnect + link** + +Modify `frontend/index.html`,在 `` 里(`` 之前)插入: + +```html +<link rel="preconnect" href="https://fonts.googleapis.com"> +<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> +<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet"> +``` + +- [ ] **Step 4: 启动 dev server 验证 CSS 变量可用** + +Run: +```bash +cd /Users/mini/Work/dev/sub2api/frontend && pnpm run dev +``` + +Expected: 启动成功(端口默认 5173),无 console error。 + +浏览器打开 http://localhost:5173,DevTools Console 执行: +```js +getComputedStyle(document.documentElement).getPropertyValue('--cyan') +``` +Expected: `" #22d3ee"` + +**停掉 dev server**(保留终端)。 + +- [ ] **Step 5: 扩展 tailwind.config.js 加 puro 色板** + +Modify `frontend/tailwind.config.js`,在 `theme.extend.colors` 里(`dark` 键后)加: + +```js + // PURO AI 设计系统色板(给新页面 Landing/Auth/Docs 用,不影响 admin) + puro: { + cyan: '#22d3ee', + 'cyan-2': '#67e8f9', + purple: '#a855f7', + amber: '#fbbf24', + green: '#34d399', + red: '#f87171', + // 平台品牌点色 + claude: '#d97757', + gpt: '#10a37f', + gemini: '#4285f4', + codex: '#f0a030' + } +``` + +- [ ] **Step 6: 验证 Tailwind class 生效** + +Run: +```bash +cd /Users/mini/Work/dev/sub2api/frontend && pnpm run dev +``` + +DevTools Console: +```js +// 临时在页面里插入 div +document.body.insertAdjacentHTML('afterbegin', '<div class="bg-puro-cyan" id="test-puro" style="padding:20px">test</div>') +// 读取实际背景色 +getComputedStyle(document.getElementById('test-puro')).backgroundColor +``` +Expected: `"rgb(34, 211, 238)"` + +删除测试元素,停掉 dev server。 + +- [ ] **Step 7: 提交** + +```bash +cd /Users/mini/Work/dev/sub2api +git add frontend/src/assets/puro.css frontend/src/main.ts frontend/index.html frontend/tailwind.config.js +git commit -m "$(cat <<'EOF' +feat(design): scaffold PURO AI design system + +- Add puro.css (tokens + primitives) as global stylesheet +- Load Inter + JetBrains Mono via Google Fonts +- Extend tailwind.config with puro.* color namespace (no conflict with legacy primary/accent palettes) + +Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> +EOF +)" +``` + +--- + +## Task 2: LandingView — 骨架 + Nav + Hero + +**Files:** +- Create: `frontend/src/views/landing/LandingView.vue` + +- [ ] **Step 1: 建 LandingView.vue 骨架** + +Create `frontend/src/views/landing/LandingView.vue`: + +```vue +<template> + <div class="puro-page"> + <div class="bg-glow"></div> + <div class="grain"></div> + + <!-- NAV --> + <nav class="nav"> + <div class="container nav-inner"> + <a href="/" class="brand"> + <span class="hex">⬢</span> + <span>PURO AI</span> + </a> + <div class="nav-links"> + <a href="#features">产品</a> + <a href="/docs">文档</a> + </div> + <div class="nav-cta"> + <router-link to="/login" class="btn btn-ghost">登录</router-link> + <router-link to="/register" class="btn btn-primary">免费试用 →</router-link> + </div> + </div> + </nav> + + <!-- HERO --> + <section class="hero container"> + <div class="hero-eyebrow"> + <span class="pill">ChatGPT Plus · Claude Pro · Codex · Gemini</span> + </div> + <h1 class="hero-title"> + 你的 AI 订阅,<br> + <span class="text-puro-cyan">已经付过钱了。</span> + </h1> + <p class="hero-sub"> + Claude Pro · ChatGPT Plus · Codex · Gemini 订阅<br> + 聚合成统一 API,零改动接入 OpenAI / Anthropic SDK + </p> + <div class="hero-cta"> + <router-link to="/login" class="btn btn-primary btn-lg">登录 →</router-link> + <a href="mailto:admin@puro.im" class="btn btn-ghost btn-lg">联系咨询</a> + </div> + <div class="hero-micro"> + 已验证可用 Codex CLI · Claude Code · curl · 服务器出口新加坡 + </div> + </section> + </div> +</template> + +<script setup lang="ts"> +// LandingView — public marketing landing page for PURO AI +// Rendered at `/` when user is unauthenticated (see router/index.ts) +</script> + +<style scoped> +.puro-page { + min-height: 100vh; + background: var(--bg-0); + color: var(--text-0); + font-family: var(--font-sans); + position: relative; + overflow-x: hidden; +} + +.hero { + text-align: center; + padding: 100px 24px 80px; + position: relative; + z-index: 2; +} +.hero-eyebrow { margin-bottom: 24px; } +.hero-title { + font-size: clamp(36px, 5.5vw, 64px); + font-weight: 800; + line-height: 1.1; + letter-spacing: -0.03em; + margin-bottom: 20px; +} +.hero-sub { + font-size: 18px; + color: var(--text-2); + line-height: 1.6; + max-width: 640px; + margin: 0 auto 36px; +} +.hero-cta { + display: flex; + gap: 12px; + justify-content: center; + flex-wrap: wrap; + margin-bottom: 20px; +} +.hero-micro { + font-size: 13px; + color: var(--text-3); + font-family: var(--font-mono); +} +</style> +``` + +- [ ] **Step 2: 临时加 /landing-preview 路由方便预览** + +Modify `frontend/src/router/index.ts`,在 `// ==================== Public Routes ====================` 注释后(靠近 `/home` 定义那里)插入: + +```ts + { + path: '/landing-preview', + name: 'LandingPreview', + component: () => import('@/views/landing/LandingView.vue'), + meta: { + requiresAuth: false, + title: 'PURO AI' + } + }, +``` + +- [ ] **Step 3: 启动 dev server 预览** + +Run: +```bash +cd /Users/mini/Work/dev/sub2api/frontend && pnpm run dev +``` + +浏览器开 http://localhost:5173/landing-preview + +Expected: +- 暗黑底 +- "⬢ PURO AI" + "登录" + "免费试用 →" 顶栏 +- 中央 "你的 AI 订阅,已经付过钱了。"("已经付过钱了" 是 cyan 色) +- "登录 →" + "联系咨询" 两个按钮 +- 底部 cyan/purple radial 光晕 + +**停掉 dev server**。 + +- [ ] **Step 4: 提交** + +```bash +cd /Users/mini/Work/dev/sub2api +git add frontend/src/views/landing/LandingView.vue frontend/src/router/index.ts +git commit -m "feat(landing): LandingView scaffold with Nav + Hero + +Temporary route /landing-preview added for dev iteration. Will flip / to +this view once all 6 sections are in place. + +Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>" +``` + +--- + +## Task 3: LandingView — ② 模型墙 + ③ 三大特性 + +**Files:** +- Modify: `frontend/src/views/landing/LandingView.vue` + +- [ ] **Step 1: 在 `<section class="hero">` 结束标签之后、script 之前,插入 ② + ③** + +```vue + <!-- ② 模型墙 --> + <section class="block container" id="models"> + <div class="section-header"> + <div class="section-kicker">支持的 AI 平台</div> + <h2 class="section-title">通过 OAuth 直接复用你的订阅</h2> + <p class="section-sub">无需申请官方 API key,也无需切换账号</p> + </div> + <div class="model-wall"> + <div class="model-card"> + <div class="model-dot" style="background: var(--p-claude)"></div> + <div class="model-name">Claude Pro / Max</div> + <div class="model-meta">Anthropic OAuth</div> + </div> + <div class="model-card"> + <div class="model-dot" style="background: var(--p-gpt)"></div> + <div class="model-name">ChatGPT Plus / Pro</div> + <div class="model-meta">OpenAI OAuth</div> + </div> + <div class="model-card"> + <div class="model-dot" style="background: var(--p-codex)"></div> + <div class="model-name">Codex CLI</div> + <div class="model-meta">OpenAI OAuth</div> + </div> + <div class="model-card"> + <div class="model-dot" style="background: var(--p-gemini)"></div> + <div class="model-name">Gemini Code Assist</div> + <div class="model-meta">Google OAuth</div> + </div> + <div class="model-card is-muted"> + <div class="model-dot" style="background: var(--text-3)"></div> + <div class="model-name">更多</div> + <div class="model-meta">规划中</div> + </div> + </div> + </section> + + <!-- ③ 三特性 --> + <section class="block container" id="features"> + <div class="section-header"> + <div class="section-kicker">核心特性</div> + <h2 class="section-title">一套 key,三件武器</h2> + </div> + <div class="features"> + <div class="feature card"> + <div class="feature-icon">⚡</div> + <h3>一个 key 接所有模型</h3> + <p>不再为每个 provider 申请 API key、配置 base_url。统一 <code class="mono">sk-</code> 走 Claude / GPT / Gemini,按 model 自动路由到对应账号池。</p> + </div> + <div class="feature card"> + <div class="feature-icon">🔄</div> + <h3>账号池高可用</h3> + <p>支持多账号自动调度与 failover。某个上游触发限流 / 冷却时,流量切到下一个健康账号,token 刷新全自动。</p> + </div> + <div class="feature card"> + <div class="feature-icon">📊</div> + <h3>用量看板</h3> + <p>每条请求的 tokens、费用、上游账号、延迟全可视化。模型分布饼图 + 趋势曲线 + Top 排行。</p> + </div> + </div> + </section> +``` + +- [ ] **Step 2: 在 `<style scoped>` 里追加样式** + +```css +.container { + max-width: 1120px; + margin: 0 auto; + padding: 0 24px; + position: relative; + z-index: 2; +} +.block { padding: 80px 24px; } +.section-header { text-align: center; margin-bottom: 40px; } +.section-kicker { + font-size: 12px; + font-weight: 600; + color: var(--cyan); + text-transform: uppercase; + letter-spacing: 0.12em; + margin-bottom: 12px; + font-family: var(--font-mono); +} +.section-title { + font-size: clamp(28px, 3.5vw, 40px); + font-weight: 700; + letter-spacing: -0.02em; + margin-bottom: 12px; +} +.section-sub { color: var(--text-2); font-size: 15px; } + +/* model wall */ +.model-wall { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 12px; +} +.model-card { + padding: 20px; + border: 1px solid var(--border); + border-radius: var(--r-lg); + background: var(--bg-1); + display: flex; + align-items: center; + gap: 12px; +} +.model-dot { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; } +.model-name { font-weight: 600; font-size: 14px; } +.model-meta { font-size: 11px; color: var(--text-3); font-family: var(--font-mono); margin-top: 2px; } +.model-card.is-muted { opacity: 0.5; } +.model-card.is-muted .model-name { color: var(--text-2); } + +/* features */ +.features { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + gap: 16px; +} +.feature { + padding: 28px 24px; + border: 1px solid var(--border); + border-radius: var(--r-lg); + background: var(--bg-1); +} +.feature-icon { font-size: 28px; margin-bottom: 14px; } +.feature h3 { font-size: 18px; font-weight: 700; margin-bottom: 10px; } +.feature p { color: var(--text-2); font-size: 14px; line-height: 1.6; } +.feature code { color: var(--cyan); font-size: 13px; } +``` + +- [ ] **Step 3: 验证** + +Run: +```bash +cd /Users/mini/Work/dev/sub2api/frontend && pnpm run dev +``` + +浏览器开 http://localhost:5173/landing-preview + +Expected: +- Hero 下方出现"支持的 AI 平台"section,5 张模型卡片横排 +- 再下方"一套 key,三件武器"section,3 张特性卡片 + +停掉 dev server。 + +- [ ] **Step 4: 提交** + +```bash +cd /Users/mini/Work/dev/sub2api +git add frontend/src/views/landing/LandingView.vue +git commit -m "feat(landing): add Models wall + Features sections + +Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>" +``` + +--- + +## Task 4: LandingView — ④ Code Demo + ⑤ Dashboard mockup + +**Files:** +- Modify: `frontend/src/views/landing/LandingView.vue` + +- [ ] **Step 1: 在 features section 之后插入 Code Demo + Dashboard mockup** + +```vue + <!-- ④ Code Demo --> + <section class="block container" id="code"> + <div class="section-header"> + <div class="section-kicker">快速接入</div> + <h2 class="section-title">把 base_url 一改,就能用</h2> + <p class="section-sub">兼容 OpenAI / Anthropic / Gemini SDK,<span class="text-puro-cyan">零代码改动</span></p> + </div> + <div class="code-demo"> + <div class="code-block"> + <div class="code-title mono">~/.codex/config.toml</div> + <pre class="mono"><code><span class="cm">[model_providers.OpenAI]</span> +base_url = <span class="str">"https://ai.puro.im"</span> +wire_api = <span class="str">"responses"</span> +requires_openai_auth = <span class="kw">true</span></code></pre> + </div> + <div class="code-block"> + <div class="code-title mono">curl</div> + <pre class="mono"><code><span class="cm">$</span> curl https://ai.puro.im/responses \ + -H <span class="str">"Authorization: Bearer sk-xxx"</span> \ + -d <span class="str">'{"model":"gpt-5.4","input":"hello"}'</span></code></pre> + </div> + </div> + <div class="code-foot">支持 OpenAI Responses API · Anthropic Messages API · Gemini generateContent · 流式 SSE & WebSocket</div> + </section> + + <!-- ⑤ Dashboard mockup --> + <section class="block container" id="dashboard"> + <div class="section-header"> + <div class="section-kicker">用量透明</div> + <h2 class="section-title">每条请求都看得见</h2> + <p class="section-sub">不像第三方 API 池子那种"扣了多少不告诉你"——扣哪个账号、跑哪个模型、用了多少 tokens、上游响应几秒,一目了然。</p> + </div> + <div class="dash-mock"> + <div class="dash-header"> + <span class="dash-title">Dashboard · 预览</span> + <div class="dash-dots"><span></span><span></span><span></span></div> + </div> + <div class="dash-body"> + <div class="stat-row"> + <div class="stat"><div class="stat-label">今日请求</div><div class="stat-value">1,842</div><div class="stat-delta">+12.3%</div></div> + <div class="stat"><div class="stat-label">输入 Tokens</div><div class="stat-value">2.1M</div><div class="stat-delta">+8.1%</div></div> + <div class="stat"><div class="stat-label">输出 Tokens</div><div class="stat-value">485K</div><div class="stat-delta">+15.6%</div></div> + <div class="stat"><div class="stat-label">今日费用</div><div class="stat-value">$1.23</div><div class="stat-delta down">-4.2%</div></div> + </div> + <div class="chart-card"> + <div class="chart-title">近 30 天用量趋势</div> + <svg viewBox="0 0 600 120" class="chart-svg"> + <polyline points="0,90 40,80 80,70 120,65 160,60 200,50 240,55 280,45 320,40 360,35 400,30 440,25 480,20 520,25 560,15 600,10" + fill="none" stroke="#22d3ee" stroke-width="2"/> + <polyline points="0,100 40,95 80,90 120,88 160,85 200,82 240,80 280,78 320,75 360,73 400,70 440,68 480,65 520,63 560,60 600,58" + fill="none" stroke="#a855f7" stroke-width="2" stroke-dasharray="4 4"/> + </svg> + </div> + <table class="log-table mono"> + <thead> + <tr><th>时间</th><th>模型</th><th>上游</th><th>状态</th><th>用量</th></tr> + </thead> + <tbody> + <tr><td>12:34:07</td><td>gpt-5.4</td><td><span class="provider gpt"><span class="dot"></span>ChatGPT #1</span></td><td class="status-200">200</td><td>2,341</td></tr> + <tr><td>12:34:02</td><td>claude-opus-4-7</td><td><span class="provider claude"><span class="dot"></span>Claude #2</span></td><td class="status-200">200</td><td>5,102</td></tr> + <tr><td>12:33:58</td><td>gemini-2.5-pro</td><td><span class="provider gemini"><span class="dot"></span>Gemini #1</span></td><td class="status-200">200</td><td>843</td></tr> + <tr><td>12:33:41</td><td>gpt-5.4</td><td><span class="provider gpt"><span class="dot"></span>ChatGPT #2</span></td><td class="status-429">429</td><td>—</td></tr> + </tbody> + </table> + </div> + </div> + </section> +``` + +- [ ] **Step 2: 追加样式到 `<style scoped>`** + +```css +/* code demo */ +.code-demo { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 16px; +} +@media (max-width: 820px) { .code-demo { grid-template-columns: 1fr; } } +.code-block { + border: 1px solid var(--border); + border-radius: var(--r-lg); + background: var(--bg-code); + overflow: hidden; +} +.code-title { + padding: 10px 16px; + background: var(--bg-1); + font-size: 11px; + color: var(--text-3); + border-bottom: 1px solid var(--border); +} +.code-block pre { + padding: 16px; + font-size: 13px; + line-height: 1.6; + color: var(--text-1); + overflow-x: auto; +} +.cm { color: var(--text-3); } +.str { color: var(--cyan); } +.kw { color: var(--amber); } +.code-foot { + margin-top: 20px; + text-align: center; + font-size: 12px; + color: var(--text-3); + font-family: var(--font-mono); +} + +/* dash mockup */ +.dash-mock { + border: 1px solid var(--border); + border-radius: var(--r-xl); + background: var(--bg-1); + overflow: hidden; + box-shadow: 0 40px 80px -40px rgba(0,0,0,0.8); +} +.dash-header { + padding: 12px 16px; + border-bottom: 1px solid var(--border); + display: flex; + align-items: center; + justify-content: space-between; +} +.dash-title { font-size: 12px; color: var(--text-2); font-family: var(--font-mono); } +.dash-dots { display: flex; gap: 6px; } +.dash-dots span { width: 10px; height: 10px; border-radius: 50%; background: var(--border-2); } +.dash-body { padding: 20px; } +.stat-row { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; margin-bottom: 20px; } +@media (max-width: 720px) { .stat-row { grid-template-columns: repeat(2, 1fr); } } +.stat { + padding: 14px; + border: 1px solid var(--border); + border-radius: var(--r-md); + background: rgba(15,23,42,0.6); +} +.stat-label { font-size: 10px; color: var(--text-3); text-transform: uppercase; letter-spacing: 0.08em; margin-bottom: 6px; } +.stat-value { font-size: 22px; font-weight: 700; letter-spacing: -0.02em; font-family: var(--font-mono); } +.stat-delta { font-size: 11px; color: var(--green, #34d399); margin-top: 4px; font-family: var(--font-mono); } +.stat-delta.down { color: var(--red, #f87171); } + +.chart-card { + border: 1px solid var(--border); + border-radius: var(--r-md); + background: rgba(15,23,42,0.6); + padding: 16px; + margin-bottom: 20px; +} +.chart-title { font-size: 12px; color: var(--text-2); margin-bottom: 12px; } +.chart-svg { width: 100%; height: 120px; display: block; } + +.log-table { width: 100%; font-size: 12px; border-collapse: collapse; } +.log-table th { + text-align: left; color: var(--text-3); font-weight: 500; + padding: 10px 12px; border-bottom: 1px solid var(--border); + text-transform: uppercase; font-size: 10px; letter-spacing: 0.08em; +} +.log-table td { padding: 10px 12px; border-bottom: 1px solid rgba(30,41,59,0.5); color: var(--text-1); } +.log-table tr:last-child td { border-bottom: none; } +.log-table .status-200 { color: var(--green, #34d399); } +.log-table .status-429 { color: var(--amber); } +.log-table .provider { display: inline-flex; align-items: center; gap: 6px; } +.log-table .provider .dot { width: 6px; height: 6px; border-radius: 50%; } +.provider.claude .dot { background: var(--p-claude); } +.provider.gpt .dot { background: var(--p-gpt); } +.provider.gemini .dot { background: var(--p-gemini); } +``` + +- [ ] **Step 3: 验证** + +Run: +```bash +cd /Users/mini/Work/dev/sub2api/frontend && pnpm run dev +``` + +http://localhost:5173/landing-preview + +Expected: +- Code Demo 两个代码块并排(移动端堆叠) +- Dashboard mockup:4 格 stats + 折线 SVG + 4 行 log table + +停掉 dev server。 + +- [ ] **Step 4: 提交** + +```bash +cd /Users/mini/Work/dev/sub2api +git add frontend/src/views/landing/LandingView.vue +git commit -m "feat(landing): add Code Demo + Dashboard mockup sections + +Dashboard mockup uses static data (stats, SVG chart, log table) — no +backend dependency. Visually demonstrates the product's usage transparency. + +Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>" +``` + +--- + +## Task 5: LandingView — ⑥ Footer + 顶部 Nav 样式 + +**Files:** +- Modify: `frontend/src/views/landing/LandingView.vue` + +- [ ] **Step 1: 在 `⑤ Dashboard` section 后、`</template>` 前加 Footer** + +```vue + <!-- ⑥ Footer --> + <footer class="puro-footer"> + <div class="container footer-grid"> + <div class="footer-brand"> + <div class="brand"><span class="hex">⬢</span><span>PURO AI</span></div> + <p class="footer-tagline">Self-hosted on puro.im</p> + <p class="footer-meta">© 2026 puro.im · MIT License<br>fork of Wei-Shaw/sub2api</p> + </div> + <div class="footer-col"> + <div class="footer-col-title">产品</div> + <a href="/docs">文档</a> + <a href="https://git.puro.im/purovps/sub2api/releases" target="_blank" rel="noopener">更新日志</a> + </div> + <div class="footer-col"> + <div class="footer-col-title">资源</div> + <a href="https://git.puro.im/purovps/sub2api" target="_blank" rel="noopener">GitHub</a> + <a href="/docs#codex">Codex 配置示例</a> + <a href="https://status.puro.im" target="_blank" rel="noopener">API 状态</a> + </div> + <div class="footer-col"> + <div class="footer-col-title">联系</div> + <a href="mailto:admin@puro.im">admin@puro.im</a> + <a href="https://git.puro.im" target="_blank" rel="noopener">git.puro.im</a> + </div> + </div> + </footer> +``` + +- [ ] **Step 2: 追加 Nav 和 Footer 样式到 `<style scoped>`** + +```css +.nav { + position: sticky; + top: 0; + z-index: 10; + background: rgba(10,14,26,0.75); + backdrop-filter: blur(12px); + border-bottom: 1px solid var(--border); +} +.nav-inner { + display: flex; + align-items: center; + gap: 28px; + padding: 16px 24px; + max-width: 1120px; + margin: 0 auto; +} +.brand { + display: flex; + align-items: center; + gap: 8px; + font-weight: 700; + font-size: 16px; +} +.brand .hex { + color: var(--cyan); + font-size: 20px; +} +.nav-links { + display: flex; + gap: 20px; + font-size: 14px; + color: var(--text-2); +} +.nav-links a:hover { color: var(--text-0); } +.nav-cta { + display: flex; + gap: 10px; + margin-left: auto; +} +@media (max-width: 640px) { + .nav-links { display: none; } +} + +/* footer */ +.puro-footer { + margin-top: 80px; + padding: 60px 24px 40px; + border-top: 1px solid var(--border); +} +.footer-grid { + display: grid; + grid-template-columns: 2fr 1fr 1fr 1fr; + gap: 36px; +} +@media (max-width: 720px) { .footer-grid { grid-template-columns: 1fr 1fr; } } +.footer-brand .brand { margin-bottom: 12px; } +.footer-tagline { color: var(--text-2); font-size: 13px; margin-bottom: 8px; } +.footer-meta { color: var(--text-3); font-size: 12px; line-height: 1.7; } +.footer-col-title { + color: var(--text-0); + font-size: 13px; + font-weight: 600; + margin-bottom: 12px; +} +.footer-col a { + display: block; + color: var(--text-2); + font-size: 13px; + padding: 4px 0; +} +.footer-col a:hover { color: var(--cyan); } + +/* pill */ +.pill { + display: inline-block; + padding: 6px 14px; + border: 1px solid var(--border-2); + border-radius: 999px; + font-size: 12px; + color: var(--text-2); + background: rgba(15,23,42,0.6); +} +``` + +- [ ] **Step 3: 验证整页 Landing** + +Run: +```bash +cd /Users/mini/Work/dev/sub2api/frontend && pnpm run dev +``` + +http://localhost:5173/landing-preview + +Expected 整页滚动可见顺序: +1. Nav(sticky,滚动时背景半透明 blur) +2. Hero +3. 模型墙 +4. 三特性 +5. Code Demo +6. Dashboard mockup +7. Footer 4 列 + +移动端(窗口缩到 <640px):Nav 菜单折叠、footer 变 2 列、stats 变 2 列、code demo 堆叠。 + +停掉 dev server。 + +- [ ] **Step 4: 提交** + +```bash +cd /Users/mini/Work/dev/sub2api +git add frontend/src/views/landing/LandingView.vue +git commit -m "feat(landing): Footer + Nav styles (sticky blur + responsive) + +Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>" +``` + +--- + +## Task 6: 路由 `/` auth-aware + +**Files:** +- Modify: `frontend/src/router/index.ts` +- Test: `frontend/src/router/__tests__/guards.spec.ts`(若有现成的 / guard 测试,补上 Landing 分支;没有就新加) + +- [ ] **Step 1: 查看当前 `/` 的 redirect 逻辑** + +Run: +```bash +grep -n "path: '/'" /Users/mini/Work/dev/sub2api/frontend/src/router/index.ts +grep -n "path: '/landing-preview'" /Users/mini/Work/dev/sub2api/frontend/src/router/index.ts +``` +Expected: `path: '/'` 在一行(redirect 到 `/home`),`/landing-preview` 在另一行。 + +- [ ] **Step 2: 把 `/landing-preview` 改名成 `/` 的实际挂载** + +Modify `frontend/src/router/index.ts`: + +1. 删除 `/landing-preview` 那 10 行临时 route +2. 把原来的: +```ts + { + path: '/', + redirect: '/home' + }, +``` +替换为: +```ts + { + path: '/', + name: 'Landing', + component: () => import('@/views/landing/LandingView.vue'), + meta: { + requiresAuth: false, + title: 'PURO AI — 你的 AI 订阅,已经付过钱了', + redirectIfAuth: '/dashboard' // authenticated users bounce to dashboard + } + }, +``` + +- [ ] **Step 3: 在 guard 里加 redirectIfAuth 分支** + +Navigation guard 在 `frontend/src/router/index.ts:524` 的 `router.beforeEach((to, _from, next) => { ... })`。auth store 暴露 `isAuthenticated` computed(定义见 `stores/auth.ts:31`)。 + +在 `authStore.checkAuth()` 那行(line ~532)后、所有其它逻辑之前,插入: + +```ts + // Auth-aware redirects for public pages (e.g. '/' shows Landing to anons, bounces authed users) + const redirectIfAuth = to.meta.redirectIfAuth as string | undefined + if (redirectIfAuth && authStore.isAuthenticated) { + return next(redirectIfAuth) + } +``` + +- [ ] **Step 4: TypeScript meta 字段声明** + +Modify `frontend/src/router/meta.d.ts`(若存在)或 `router/index.ts` 顶部 module augmentation,为 `RouteMeta` 增加 `redirectIfAuth?: string`: + +```ts +import 'vue-router' +declare module 'vue-router' { + interface RouteMeta { + redirectIfAuth?: string + } +} +``` + +(若 `meta.d.ts` 已有 module augmentation,追加字段即可。) + +- [ ] **Step 5: 验证未登录 → Landing、登录后 → Dashboard** + +Run: +```bash +cd /Users/mini/Work/dev/sub2api/frontend && pnpm run dev +``` + +场景 1(匿名):DevTools → Application → Clear storage → 全清 localStorage/sessionStorage +- http://localhost:5173/ → **LandingView** 呈现 +- 点 Hero "登录 →" → 跳 /login + +场景 2(登录态): +- 在 /login 输 `admin@puro.im` / `fJni5YDmEY242owh` 登录(见 spec §5 "当前状态 snapshot") +- 登录后自动跳到 /dashboard(既有行为) +- 手动再访问 http://localhost:5173/ → 被 guard 跳到 /dashboard(新加的 redirectIfAuth 生效) + +停掉 dev server。 + +- [ ] **Step 6: 提交** + +```bash +cd /Users/mini/Work/dev/sub2api +git add frontend/src/router/index.ts +git commit -m "feat(router): mount Landing at / with auth-aware redirect + +Anonymous visitors see PURO AI landing page. Authenticated users are +redirected to /dashboard via meta.redirectIfAuth guard extension. + +Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>" +``` + +--- + +## Task 7: AuthLayout 改造(加 narrative slot) + +**Files:** +- Modify: `frontend/src/components/layout/AuthLayout.vue` + +- [ ] **Step 1: 读现有 AuthLayout 的 slot 结构** + +Run: +```bash +wc -l /Users/mini/Work/dev/sub2api/frontend/src/components/layout/AuthLayout.vue +``` +Expected: ~90-120 行。读它确认 slot 有默认 + `#footer`。 + +- [ ] **Step 2: 改 AuthLayout.vue template 为可选左右分栏** + +把整个 `<template>` 换成: + +```vue +<template> + <div class="auth-shell" :class="{ 'auth-shell-split': hasNarrative }"> + <div class="bg-glow soft"></div> + + <!-- LEFT: Narrative (split mode only, hidden on mobile) --> + <aside v-if="hasNarrative" class="auth-narrative"> + <slot name="narrative"></slot> + </aside> + + <!-- RIGHT: Form --> + <main class="auth-main"> + <div class="auth-main-inner"> + <div class="mb-8 text-center" v-if="!hasNarrative"> + <!-- 兼容旧调用:无 narrative slot 时退回居中品牌头 --> + <template v-if="settingsLoaded"> + <div class="mb-4 inline-flex h-14 w-14 items-center justify-center overflow-hidden rounded-2xl"> + <img :src="siteLogo || '/logo.png'" alt="Logo" class="h-full w-full object-contain" /> + </div> + <h1 class="text-2xl font-bold">{{ siteName }}</h1> + </template> + </div> + + <slot /> + + <div class="mt-6 text-center text-sm"> + <slot name="footer" /> + </div> + </div> + </main> + </div> +</template> +``` + +- [ ] **Step 3: 在 `<script setup>` 里加 `useSlots` 检测 narrative** + +在 `<script setup lang="ts">` 顶部追加: + +```ts +import { useSlots } from 'vue' +const slots = useSlots() +const hasNarrative = computed(() => !!slots.narrative) +``` + +(`computed` 如果还没 import 就加上) + +- [ ] **Step 4: 在 `<style scoped>` 里加 split layout 样式** + +把现有 `<style scoped>`(或在文件末新增)补入: + +```css +.auth-shell { + min-height: 100vh; + background: var(--bg-0); + color: var(--text-0); + font-family: var(--font-sans); + display: flex; + position: relative; + overflow: hidden; +} +.auth-shell-split { display: grid; grid-template-columns: 1fr 1fr; } +@media (max-width: 900px) { + .auth-shell-split { grid-template-columns: 1fr; } + .auth-narrative { display: none; } +} +.auth-narrative { + position: relative; + padding: 48px 56px; + background: linear-gradient(135deg, #0a0e1a 0%, #1e1b4b 100%); + border-right: 1px solid var(--border); + display: flex; + flex-direction: column; + justify-content: space-between; +} +.auth-main { + display: flex; + align-items: center; + justify-content: center; + padding: 40px 24px; + position: relative; + z-index: 2; +} +.auth-main-inner { width: 100%; max-width: 420px; } +``` + +- [ ] **Step 5: 验证无 narrative 场景(旧行为兼容)** + +Run: +```bash +cd /Users/mini/Work/dev/sub2api/frontend && pnpm run dev +``` + +http://localhost:5173/forgot-password(这个页面用 AuthLayout 但不会传 narrative) + +Expected: 居中品牌卡片,看起来像以前。 + +停掉 dev server。 + +- [ ] **Step 6: 提交** + +```bash +cd /Users/mini/Work/dev/sub2api +git add frontend/src/components/layout/AuthLayout.vue +git commit -m "refactor(auth): AuthLayout supports optional narrative slot (split mode) + +Backward-compatible: without narrative slot, falls back to centered brand +card. With narrative slot, renders 50/50 split on desktop, collapses to +single column on mobile. + +Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>" +``` + +--- + +## Task 8: LoginView 套 narrative slot + 文案微调 + +**Files:** +- Modify: `frontend/src/views/auth/LoginView.vue` + +- [ ] **Step 1: 在 `<AuthLayout>` 开标签后插入 `<template #narrative>`** + +找到 `<AuthLayout>`(第 2 行附近)后,紧跟着加: + +```vue + <template #narrative> + <div class="auth-narrative-inner"> + <div class="brand"><span class="hex">⬢</span><span>PURO AI</span></div> + <div class="auth-narrative-hero"> + <div class="auth-narrative-headline"> + <span class="num-5">5</span> 个订阅<br> + → <span class="num-1">1</span> 个 key + </div> + <p class="auth-narrative-sub"> + 省去切换账号的繁琐,<br> + 省去为多个高昂订阅重复买单。<br> + <span class="auth-narrative-tagline">PURO(纯粹)—— 让 AI 调用回归本质。</span> + </p> + </div> + <div class="auth-narrative-foot mono">Claude · ChatGPT · Codex · Gemini</div> + </div> + </template> +``` + +- [ ] **Step 2: 调登录表单的 heading 文案** + +把现有 `<h2>{{ t('auth.welcomeBack') }}</h2>` 对应的那行 heading 改为: + +```vue + <h2 class="text-2xl font-bold text-gray-900 dark:text-white">登录</h2> + <p class="mt-2 text-sm text-gray-500 dark:text-dark-400">用你的 PURO AI 账户继续</p> +``` + +(直接 hardcode 中文,避开改 i18n;Task 11 会统一补进 zh.ts。) + +- [ ] **Step 3: 在 LoginView 的 `<style scoped>` 里(如果没有就加一个)追加 narrative 内部样式** + +```vue +<style scoped> +.auth-narrative-inner { + position: relative; + z-index: 2; + height: 100%; + display: flex; + flex-direction: column; + justify-content: space-between; + color: var(--text-0); +} +.brand { + display: flex; + align-items: center; + gap: 8px; + font-weight: 700; + font-size: 18px; +} +.brand .hex { color: var(--cyan); font-size: 24px; } + +.auth-narrative-hero { } +.auth-narrative-headline { + font-size: clamp(40px, 5vw, 64px); + font-weight: 800; + line-height: 1.05; + letter-spacing: -0.03em; + margin-bottom: 24px; +} +.auth-narrative-headline .num-5 { color: var(--amber); } +.auth-narrative-headline .num-1 { color: var(--cyan); } +.auth-narrative-sub { + font-size: 15px; + line-height: 1.7; + color: var(--text-1); +} +.auth-narrative-tagline { + display: block; + margin-top: 12px; + font-size: 12px; + color: var(--text-3); +} +.auth-narrative-foot { + font-size: 12px; + color: var(--text-3); +} +</style> +``` + +- [ ] **Step 4: 验证** + +Run: +```bash +cd /Users/mini/Work/dev/sub2api/frontend && pnpm run dev +``` + +http://localhost:5173/login + +Expected(桌面): +- 左侧:PURO AI 品牌 + "5 个订阅 → 1 个 key"(5 橙色,1 青色)+ 三句排比副文 + 底部小字 +- 右侧:原有登录表单(邮箱/密码/CTA/OAuth),heading 改成"登录" +- 移动端(缩到 <900px):narrative 隐藏,表单居中 + +登录流程一定要验: +- 用已有账号登录一次,看是否正常跳 Dashboard(OAuth/Turnstile/2FA 逻辑未动过,应都保留) + +停掉 dev server。 + +- [ ] **Step 5: 提交** + +```bash +cd /Users/mini/Work/dev/sub2api +git add frontend/src/views/auth/LoginView.vue +git commit -m "feat(auth): LoginView with PURO narrative split layout + +- Left: ⬢ PURO AI brand + '5→1' headline + value props +- Right: unchanged form (OAuth / Turnstile / 2FA all preserved) +- Heading copy updated to Chinese (i18n key consolidation in later task) + +Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>" +``` + +--- + +## Task 9: RegisterView 套同样的 narrative + +**Files:** +- Modify: `frontend/src/views/auth/RegisterView.vue` + +- [ ] **Step 1: 读 RegisterView 当前 `<AuthLayout>` 块** + +Run: +```bash +grep -n "AuthLayout\|welcomeBack\|createAccount" /Users/mini/Work/dev/sub2api/frontend/src/views/auth/RegisterView.vue | head -10 +``` + +- [ ] **Step 2: 在 `<AuthLayout>` 开标签后插入同款 narrative slot** + +(与 LoginView 完全相同的 `<template #narrative>` 块——**复制粘贴** LoginView 的 narrative template。) + +- [ ] **Step 3: 调注册表单 heading** + +把现有 `<h2>` heading 那行替换为: + +```vue + <h2 class="text-2xl font-bold text-gray-900 dark:text-white">创建账户</h2> + <p class="mt-2 text-sm text-gray-500 dark:text-dark-400">5 分钟开始用 PURO AI</p> +``` + +- [ ] **Step 4: 把 LoginView 的 narrative `<style scoped>` 复制到 RegisterView** + +(整段 `.auth-narrative-inner / .brand / .auth-narrative-*` 的 CSS 复制一份) + +- [ ] **Step 5: 验证** + +Run: +```bash +cd /Users/mini/Work/dev/sub2api/frontend && pnpm run dev +``` + +http://localhost:5173/register + +Expected: 同 Login 的 split layout,右侧表单是注册表单(邮箱 + 密码 + 确认密码 + Turnstile + CTA),heading "创建账户"。 + +停掉 dev server。 + +- [ ] **Step 6: 提交** + +```bash +cd /Users/mini/Work/dev/sub2api +git add frontend/src/views/auth/RegisterView.vue +git commit -m "feat(auth): RegisterView with PURO narrative split layout + +Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>" +``` + +--- + +## Task 10: DocsView + +**Files:** +- Create: `frontend/src/views/docs/DocsView.vue` +- Modify: `frontend/src/router/index.ts` + +- [ ] **Step 1: 建 DocsView.vue** + +Create `frontend/src/views/docs/DocsView.vue`: + +```vue +<template> + <div class="puro-page"> + <div class="bg-glow soft"></div> + + <nav class="nav"> + <div class="container nav-inner"> + <router-link to="/" class="brand"><span class="hex">⬢</span><span>PURO AI</span></router-link> + <div class="nav-links"> + <router-link to="/">首页</router-link> + <a href="#codex">Codex</a> + <a href="#claude-code">Claude Code</a> + <a href="#curl">curl</a> + </div> + <div class="nav-cta"> + <router-link to="/login" class="btn btn-primary">登录 →</router-link> + </div> + </div> + </nav> + + <section class="docs-hero container"> + <h1>快速接入 PURO AI</h1> + <p class="subtitle">三步走:拿 key → 配 base_url → 发请求</p> + </section> + + <div class="container docs-body"> + <section id="get-key" class="docs-section"> + <h2>1. 获取 API key</h2> + <p>当前 PURO AI 不开放自助注册付费。联系管理员获取:</p> + <div class="callout"> + <a href="mailto:admin@puro.im">admin@puro.im</a> + </div> + <p class="note">未来通过 iShare 入口开放订阅购买。</p> + </section> + + <section id="codex" class="docs-section"> + <h2>2. Codex CLI 接入</h2> + <p>修改 <code class="mono">~/.codex/config.toml</code>:</p> + <pre class="mono"><code>model_provider = <span class="str">"OpenAI"</span> +model = <span class="str">"gpt-5.4"</span> +wire_api = <span class="str">"responses"</span> + +[model_providers.OpenAI] +name = <span class="str">"OpenAI"</span> +base_url = <span class="str">"https://ai.puro.im"</span> +wire_api = <span class="str">"responses"</span> +requires_openai_auth = <span class="kw">true</span></code></pre> + <p>然后 <code class="mono">~/.codex/auth.json</code>:</p> + <pre class="mono"><code>{ + <span class="str">"OPENAI_API_KEY"</span>: <span class="str">"sk-xxxxxxxxxxxxxxxx"</span> +}</code></pre> + <p>验证:</p> + <pre class="mono"><code><span class="cm">$</span> codex exec --sandbox read-only <span class="str">"say hi"</span></code></pre> + </section> + + <section id="claude-code" class="docs-section"> + <h2>3. Claude Code 接入</h2> + <p>修改 <code class="mono">~/.claude/settings.json</code>:</p> + <pre class="mono"><code>{ + <span class="str">"base_url"</span>: <span class="str">"https://ai.puro.im"</span>, + <span class="str">"api_key"</span>: <span class="str">"sk-xxxxxxxxxxxxxxxx"</span> +}</code></pre> + <p class="note">Claude Code 通过 <code class="mono">/v1/messages</code> endpoint 调用 Anthropic 兼容 API。</p> + </section> + + <section id="curl" class="docs-section"> + <h2>4. curl 直连测试</h2> + <p>OpenAI Responses API:</p> + <pre class="mono"><code><span class="cm">$</span> curl https://ai.puro.im/responses \ + -H <span class="str">"Authorization: Bearer sk-xxx"</span> \ + -H <span class="str">"Content-Type: application/json"</span> \ + -d <span class="str">'{"model":"gpt-5.4","input":"hello"}'</span></code></pre> + <p>Anthropic Messages API:</p> + <pre class="mono"><code><span class="cm">$</span> curl https://ai.puro.im/v1/messages \ + -H <span class="str">"Authorization: Bearer sk-xxx"</span> \ + -H <span class="str">"Content-Type: application/json"</span> \ + -H <span class="str">"anthropic-version: 2023-06-01"</span> \ + -d <span class="str">'{"model":"claude-opus-4-7","max_tokens":100,"messages":[{"role":"user","content":"hi"}]}'</span></code></pre> + </section> + + <section id="models" class="docs-section"> + <h2>5. 支持的模型</h2> + <ul class="model-list"> + <li><code class="mono">gpt-5.4</code> · OpenAI(via ChatGPT Plus / Codex OAuth)</li> + <li><code class="mono">gpt-5.4-codex</code> · OpenAI Codex 专用</li> + <li><code class="mono">claude-opus-4-7</code> · Anthropic(via Claude Pro / Max OAuth)</li> + <li><code class="mono">claude-sonnet-4-6</code> · Anthropic</li> + <li><code class="mono">gemini-2.5-pro</code> · Google(via Code Assist OAuth)</li> + <li><code class="mono">gemini-2.5-flash</code> · Google</li> + </ul> + <p class="note">后端 pricing 表实时跟进 <code class="mono">model-price-repo</code>,完整清单登录后在 <router-link to="/dashboard">控制台</router-link> 查看。</p> + </section> + + <section id="feedback" class="docs-section"> + <h2>6. 问题反馈</h2> + <p>遇到问题或希望补接某个平台:</p> + <div class="callout"> + <a href="mailto:admin@puro.im">admin@puro.im</a> + </div> + </section> + </div> + </div> +</template> + +<script setup lang="ts"></script> + +<style scoped> +.puro-page { + min-height: 100vh; + background: var(--bg-0); + color: var(--text-0); + font-family: var(--font-sans); + position: relative; +} +.container { max-width: 860px; margin: 0 auto; padding: 0 24px; position: relative; z-index: 2; } +.nav { + position: sticky; + top: 0; + z-index: 10; + background: rgba(10,14,26,0.75); + backdrop-filter: blur(12px); + border-bottom: 1px solid var(--border); +} +.nav-inner { display: flex; align-items: center; gap: 28px; padding: 16px 24px; max-width: 1120px; margin: 0 auto; } +.brand { display: flex; align-items: center; gap: 8px; font-weight: 700; font-size: 16px; } +.brand .hex { color: var(--cyan); font-size: 20px; } +.nav-links { display: flex; gap: 20px; font-size: 14px; color: var(--text-2); } +.nav-links a:hover { color: var(--text-0); } +.nav-cta { margin-left: auto; } + +.docs-hero { padding: 80px 24px 40px; text-align: center; } +.docs-hero h1 { font-size: clamp(32px, 4vw, 48px); font-weight: 800; letter-spacing: -0.03em; margin-bottom: 12px; } +.docs-hero .subtitle { color: var(--text-2); font-size: 16px; } + +.docs-body { padding-bottom: 80px; } +.docs-section { margin: 48px 0; scroll-margin-top: 80px; } +.docs-section h2 { + font-size: 22px; + font-weight: 700; + margin-bottom: 14px; + color: var(--text-0); + border-bottom: 1px solid var(--border); + padding-bottom: 8px; +} +.docs-section p { color: var(--text-1); font-size: 14px; line-height: 1.8; margin: 12px 0; } +.docs-section .note { color: var(--text-3); font-size: 13px; } +.docs-section code.mono { background: var(--bg-1); padding: 2px 6px; border-radius: var(--r-sm); font-size: 13px; color: var(--cyan); } +.docs-section pre.mono { + background: var(--bg-code); + border: 1px solid var(--border); + border-radius: var(--r-md); + padding: 16px; + font-size: 13px; + line-height: 1.6; + color: var(--text-1); + overflow-x: auto; + margin: 12px 0; +} +.docs-section pre .str { color: var(--cyan); } +.docs-section pre .kw { color: var(--amber); } +.docs-section pre .cm { color: var(--text-3); } + +.callout { + padding: 16px 20px; + background: var(--bg-1); + border: 1px solid var(--border-2); + border-left: 3px solid var(--cyan); + border-radius: var(--r-md); + margin: 12px 0; +} +.callout a { color: var(--cyan); font-family: var(--font-mono); font-size: 14px; } + +.model-list { list-style: none; padding: 0; } +.model-list li { padding: 8px 0; color: var(--text-1); font-size: 14px; border-bottom: 1px dashed var(--border); } +.model-list li:last-child { border-bottom: none; } +</style> +``` + +- [ ] **Step 2: 在 router/index.ts 注册 `/docs` public route** + +插入(在 `/home` 附近的 Public Routes 区): + +```ts + { + path: '/docs', + name: 'Docs', + component: () => import('@/views/docs/DocsView.vue'), + meta: { + requiresAuth: false, + title: 'PURO AI · 文档' + } + }, +``` + +- [ ] **Step 3: 验证** + +Run: +```bash +cd /Users/mini/Work/dev/sub2api/frontend && pnpm run dev +``` + +http://localhost:5173/docs + +Expected: +- 顶部 sticky nav(⬢ PURO AI / 首页 / Codex / Claude Code / curl / 登录) +- Hero:"快速接入 PURO AI" +- 6 个 numbered sections +- 代码块深色高亮 + +点 Nav 里的 "Codex" / "Claude Code" / "curl" 锚点会跳到对应段。 + +停掉 dev server。 + +- [ ] **Step 4: 提交** + +```bash +cd /Users/mini/Work/dev/sub2api +git add frontend/src/views/docs/DocsView.vue frontend/src/router/index.ts +git commit -m "feat(docs): public DocsView with Codex/Claude Code/curl quickstart + +Public route /docs, no auth required. Six sections: get key, codex CLI +config, claude code settings, curl smoke test, supported models, feedback. + +Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>" +``` + +--- + +## Task 11: i18n 整理(Login/Register heading) + +**Files:** +- Modify: `frontend/src/i18n/locales/zh.ts` +- Modify: `frontend/src/views/auth/LoginView.vue` +- Modify: `frontend/src/views/auth/RegisterView.vue` + +**Scope**:本期**只把 Login/Register 的 heading 文案**纳入 i18n(Landing 和 Docs 整页内容量大、语言只中文,暂不 i18n 化,后续英文版再说)。 + +- [ ] **Step 1: zh.ts 里查看现有 auth namespace** + +Run: +```bash +grep -n "auth:" /Users/mini/Work/dev/sub2api/frontend/src/i18n/locales/zh.ts | head -3 +``` + +- [ ] **Step 2: 在 `auth` namespace 里加 PURO 专用 keys** + +Modify `zh.ts`,在 `auth: { ... }` 里面追加(注意保留现有键): + +```ts + // PURO AI redesign + puroLoginTitle: '登录', + puroLoginSub: '用你的 PURO AI 账户继续', + puroRegisterTitle: '创建账户', + puroRegisterSub: '5 分钟开始用 PURO AI', +``` + +- [ ] **Step 3: 把 LoginView / RegisterView 里 hardcoded 的两行 heading 换成 t() 调用** + +LoginView: +```vue +<h2 class="text-2xl font-bold text-gray-900 dark:text-white">{{ t('auth.puroLoginTitle') }}</h2> +<p class="mt-2 text-sm text-gray-500 dark:text-dark-400">{{ t('auth.puroLoginSub') }}</p> +``` + +RegisterView 同理用 `auth.puroRegisterTitle` / `auth.puroRegisterSub`。 + +- [ ] **Step 4: 验证 zh.ts 没语法错误** + +Run: +```bash +cd /Users/mini/Work/dev/sub2api/frontend && pnpm run type-check 2>&1 | tail -5 +``` +Expected: 无 TypeScript 错误。 + +- [ ] **Step 5: dev server 确认 heading 显示正确** + +```bash +cd /Users/mini/Work/dev/sub2api/frontend && pnpm run dev +``` + +http://localhost:5173/login → heading "登录" +http://localhost:5173/register → heading "创建账户" + +停掉 dev server。 + +- [ ] **Step 6: 提交** + +```bash +cd /Users/mini/Work/dev/sub2api +git add frontend/src/i18n/locales/zh.ts frontend/src/views/auth/LoginView.vue frontend/src/views/auth/RegisterView.vue +git commit -m "chore(i18n): consolidate PURO auth heading keys into zh.ts + +Landing and Docs text remain hardcoded Chinese this cycle (see spec §6 +Note 5). Only Login/Register heading strings are moved into i18n. + +Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>" +``` + +--- + +## Task 12: Build check + manual smoke test + +**Files:** +- 无代码改动 + +- [ ] **Step 1: TypeScript + Vue 类型检查** + +Run: +```bash +cd /Users/mini/Work/dev/sub2api/frontend && pnpm run type-check 2>&1 | tail -20 +``` +Expected: No errors. + +- [ ] **Step 2: Lint check(如果有)** + +Run: +```bash +cd /Users/mini/Work/dev/sub2api/frontend && pnpm run lint 2>&1 | tail -20 +``` +Expected: No errors, 或最多 warnings(可接受)。 + +- [ ] **Step 3: 产品构建** + +Run: +```bash +cd /Users/mini/Work/dev/sub2api/frontend && pnpm run build 2>&1 | tail -20 +``` +Expected: `✓ built in Xs` · 产物写到 `../backend/internal/web/dist/`。 + +- [ ] **Step 4: 本地预览 prod bundle** + +Run: +```bash +cd /Users/mini/Work/dev/sub2api/frontend && pnpm run preview 2>&1 | head -5 +``` +Expected: preview server 启动(默认 4173)。 + +开浏览器做**验收清单走一遍**(对齐 spec §7): +- [ ] http://localhost:4173/ — Landing 6 section 全在 +- [ ] 窗口缩到 <640px — Nav 菜单隐藏、Footer 2 列、stats 2 列 +- [ ] http://localhost:4173/login — 左右分栏,narrative 左,表单右 +- [ ] 窗口缩到 <900px — narrative 隐藏,表单独占 +- [ ] http://localhost:4173/register — 同 login 布局 +- [ ] http://localhost:4173/docs — 6 sections,锚点跳转工作 +- [ ] http://localhost:4173/dashboard — 后台页面**外观不变**(重要!) +- [ ] http://localhost:4173/accounts(管理员)— 外观不变 + +停掉 preview。 + +- [ ] **Step 5: 记录验收结果在 spec checklist** + +Modify `docs/superpowers/specs/2026-04-19-puro-ai-landing-auth-design.md`,在第 7 节验收清单里,把跑通的项目 `[ ]` 改成 `[x]`。 + +- [ ] **Step 6: 提交验收清单更新** + +```bash +cd /Users/mini/Work/dev/sub2api +git add docs/superpowers/specs/2026-04-19-puro-ai-landing-auth-design.md +git commit -m "docs: check off local acceptance items [CI SKIP] + +Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>" +``` + +--- + +## Task 13: 合并到 main + CI 部署 + ai.puro.im 实机验证 + +**Files:** +- 无代码改动 + +- [ ] **Step 1: push 最新到 gitea** + +```bash +cd /Users/mini/Work/dev/sub2api && git push gitea feat/design-landing-auth +``` + +- [ ] **Step 2: 创建 PR** + +```bash +curl -s -u 'purovps:Qweewqzzx1' -X POST https://git.puro.im/api/v1/repos/purovps/sub2api/pulls \ + -H 'Content-Type: application/json' \ + -d '{ + "title": "feat: PURO AI landing + auth + docs redesign", + "body": "Implements docs/superpowers/specs/2026-04-19-puro-ai-landing-auth-design.md\n\nSee plan: docs/superpowers/plans/2026-04-19-puro-ai-landing-auth.md\n\nScope (path B):\n- Landing page at / (anon)\n- AuthLayout split mode\n- LoginView / RegisterView with PURO narrative\n- Public /docs page\n- puro.css design system\n\nNot touched: admin pages, backend, auth logic.", + "head": "feat/design-landing-auth", + "base": "main" + }' | head -c 300 +``` +Expected: JSON with `"number": N` = PR 号。 + +- [ ] **Step 3: 合并 PR(manual via Gitea UI 或 API)** + +Via Gitea UI:https://git.puro.im/purovps/sub2api/pulls/<N>/merge,点 Merge。 + +或 API(把 N 替换成上一步 PR 号): +```bash +curl -s -u 'purovps:Qweewqzzx1' -X POST https://git.puro.im/api/v1/repos/purovps/sub2api/pulls/<N>/merge \ + -H 'Content-Type: application/json' \ + -d '{"Do":"merge"}' +``` + +- [ ] **Step 4: 跟 CI 构建(预计 90-180 秒)** + +Run: +```bash +ssh vps "for i in \$(seq 1 20); do + running=\$(docker ps --filter 'name=^drone-' --format '{{.Names}} | {{.Image}}' | head -1) + if [ -z \"\$running\" ]; then echo '(build done)'; break; fi + echo \"\$running\" + sleep 10 +done +echo '--- container age ---' +docker ps --filter 'name=^sub2api$' --format '{{.Names}} {{.Status}}'" +``` + +Expected: 最后 `sub2api` 容器 status 显示 `Up (N) seconds` 的很新时间戳。 + +- [ ] **Step 5: 线上实机验证(对齐 spec §7 完整 checklist)** + +分别访问(用匿名浏览器 / 无痕窗口): + +``` +https://ai.puro.im/ → PURO AI Landing +https://ai.puro.im/docs → 精简文档 +https://ai.puro.im/login → split layout 登录 +https://ai.puro.im/register → split layout 注册 +``` + +登录后访问: +``` +https://ai.puro.im/ → 被 guard 跳到 /dashboard +https://ai.puro.im/dashboard → 后台外观不变 +``` + +发一个 AI 请求验证中转没坏: +```bash +curl -sS -X POST https://ai.puro.im/responses \ + -H 'Authorization: Bearer sk-d2132de2f0b4c1ab64ef7241a16d254cab483f1f8afd47ad4a89e39cf6e2345a' \ + -H 'Content-Type: application/json' \ + -d '{"model":"gpt-5.4","input":"say: redesign-live","stream":false}' \ + -w '\n-- HTTP %{http_code} / %{time_total}s --\n' | head -c 500 +``` +Expected: HTTP 200 + "redesign-live" 响应。 + +- [ ] **Step 6: 打 tag 标记里程碑** + +```bash +cd /Users/mini/Work/dev/sub2api +git checkout main && git pull gitea main +git tag -a v0.2.0-puro-redesign -m "feat: PURO AI landing + auth + docs redesign" +git push gitea v0.2.0-puro-redesign +``` + +--- + +## 收尾 checklist + +本计划执行完,spec §7 的 11 个验收项应全部打勾: + +- [ ] puro.css 全局引入 +- [ ] / (匿名) → Landing +- [ ] / (登录) → Dashboard +- [ ] Landing 6 section 齐 +- [ ] Dashboard mockup 纯静态 +- [ ] /login split 布局 +- [ ] /register split 布局 +- [ ] /docs 公开可访问含配置示例 +- [ ] OAuth/Turnstile/2FA 正常 +- [ ] 后台外观不变 +- [ ] CI 通过,ai.puro.im 加载正常