Compare commits

..

21 Commits

Author SHA1 Message Date
355370ad2a Merge pull request 'feat: PURO AI landing + auth + docs redesign' (#1) from feat/design-landing-auth into main
Some checks failed
continuous-integration/drone/push Build is passing
CI / test (push) Has been cancelled
CI / golangci-lint (push) Has been cancelled
Security Scan / backend-security (push) Has been cancelled
Security Scan / frontend-security (push) Has been cancelled
Release / update-version (push) Has been cancelled
Release / build-frontend (push) Has been cancelled
Release / release (push) Has been cancelled
Release / sync-version-file (push) Has been cancelled
feat: PURO AI landing + auth + docs redesign

Merges feat/design-landing-auth → main. Tasks 1-12 complete.
Triggers CI auto-deploy to ai.puro.im.
2026-04-19 13:58:25 +00:00
puro ci
829f101100 docs: tick local acceptance items [CI SKIP]
Some checks failed
CI / test (pull_request) Has been cancelled
CI / golangci-lint (pull_request) Has been cancelled
Security Scan / backend-security (pull_request) Has been cancelled
Security Scan / frontend-security (pull_request) Has been cancelled
CI / test (push) Has been cancelled
CI / golangci-lint (push) Has been cancelled
Security Scan / backend-security (push) Has been cancelled
Security Scan / frontend-security (push) Has been cancelled
11/12 checklist items verified via pnpm build + preview + curl.
Remaining: CI deploy + prod verification (Task 13).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 21:56:50 +08:00
mini
0fceb100e0 chore(i18n): consolidate PURO auth heading keys into zh.ts
Login and Register heading strings moved from hardcoded Chinese to
auth.puroLoginTitle / puroLoginSub / puroRegisterTitle / puroRegisterSub.

Landing (LandingView) and Docs (DocsView) intentionally keep hardcoded
Chinese this cycle (see spec §6 note 5 — English version deferred).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 21:54:49 +08:00
mini
7dc8062988 feat(docs): public DocsView with Codex/Claude Code/curl quickstart
Route /docs (no auth). Six sections: get key, codex CLI, claude code,
curl, supported models, feedback. Uses puro.css design system.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 21:42:08 +08:00
mini
f17a88c171 feat(auth): RegisterView with PURO narrative split layout
Same split layout as LoginView: left narrative, right form.
Heading: '创建账户' + '5 分钟开始用 PURO AI'.
All form logic preserved (OAuth, Turnstile, email verify code, password fields).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 21:29:08 +08:00
mini
91b9ae7e21 feat(auth): LoginView with PURO narrative split layout
- Left: ⬢ PURO AI brand + '5→1' headline + three-line value props
- Right: existing form (OAuth, Turnstile, 2FA all preserved unchanged)
- Heading changed from t('auth.welcomeBack') to '登录' — i18n key consolidation in Task 11

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 21:27:23 +08:00
mini
9ee99d17fd refactor(auth): AuthLayout supports optional narrative slot
New slot 'narrative' enables split-screen layout (50/50 desktop, collapses
to single column on mobile <900px).

Backward compatibility:
- Pages that don't pass a narrative slot still render the original
  centered-card layout with siteName + logo + copyright
- ForgotPassword, ResetPassword, EmailVerify unaffected

To be used in Tasks 8 and 9 (LoginView, RegisterView).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 21:25:01 +08:00
mini
284b5129ac feat(router): mount Landing at / with auth-aware redirect
- / (anonymous) → LandingView
- / (authenticated) → redirects to /dashboard via new meta.redirectIfAuth
- Remove temporary /landing-preview route (Task 2 helper)
- RouteMeta TS augmentation for redirectIfAuth
- LandingView brand link uses router-link (was <a href>, causing SPA reload)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 21:14:12 +08:00
mini
4832534232 fix(landing): Polish items from code review
- Add noreferrer to all external rel attributes (4 anchors)
- Change 更新日志 link from /releases (may 404) to /commits/branch/main
- Remove dead CSS overridden by puro.css: .nav z-index:10, .nav-links gap:20px
- Document puro.css global dependency at top of scoped style block

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 21:11:13 +08:00
mini
4e675d70c1 feat(landing): Footer + sticky Nav styles
- 4-column footer (brand/产品/资源/联系), responsive 2-col on mobile
- Nav sticky with blur background, border-bottom
- nav-links hidden on mobile (<640px)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 21:01:10 +08:00
mini
c099cd5d97 feat(landing): add Code Demo + Dashboard mockup sections
Dashboard mockup is pure static (SVG chart + stats grid + log table).
No backend dependency. Reuses puro.css .log-table / .provider / .status-*
(scoped to .puro-page). Only adds component-local styles for .code-demo,
.dash-mock, .stat-row, .chart-card and friends.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 20:29:14 +08:00
mini
158f2a8d53 fix(landing): resolve Task 3 CSS specificity issues
Code review flagged:
- .feature.card caused padding/background specificity race;
  .feature already defines all card properties, so drop .card
- model-card flex row misaligned name/meta (should stack in column);
  wrap in div to get dot | [name/meta] layout
- .container and .section-* scoped rules silently shadow puro.css defaults;
  add comment explaining intentional override

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 20:26:50 +08:00
mini
cfcdd988db feat(landing): add Models wall + Features sections
- Added Models section (② 模型墙) with 5 platform cards (Claude, GPT, Codex, Gemini, More)
- Added Features section (③ 三特性) with 3 feature cards (unified key, account pool failover, analytics dashboard)
- Implemented responsive grid layouts with scoped CSS rules
- Used existing design tokens and brand colors from puro.css

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 20:18:56 +08:00
mini
9dae8724e3 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 (Task 6).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 20:11:06 +08:00
mini
064a4b7614 fix(design): scope .bg-glow::before selector to .puro-page
One missed prefix from the automated transform. Aligns with the
scoping contract established in 41664efe.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 20:06:21 +08:00
mini
41664efede fix(design): scope puro.css to .puro-page container
Code review flagged two critical issues with the initial scaffold:
- html/body bg + universal reset applied globally, overriding admin Tailwind styles
- Shared class names (.btn, .card, .input, .badge, .divider) collided with style.css,
  breaking 25+ admin views

Fix: all non-:root rules now scoped under .puro-page descendant selector.
@keyframes and global box-sizing preserved. html/body props merged into .puro-page root.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 20:03:22 +08:00
mini
1d7e75b82e 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>
2026-04-19 19:11:24 +08:00
puro design
d941550bf6 docs: implementation plan for PURO AI landing+auth [CI SKIP]
Some checks failed
CI / test (push) Has been cancelled
CI / golangci-lint (push) Has been cancelled
Security Scan / backend-security (push) Has been cancelled
Security Scan / frontend-security (push) Has been cancelled
13 tasks, ~4-6 hours total work. Each task has concrete file paths,
full code blocks, verification steps. Scope: Landing + AuthLayout split
+ LoginView/RegisterView restyle + public Docs + puro.css design system.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 19:04:23 +08:00
puro design
3a16b3ecde docs: archive Claude Design v2 output [CI SKIP]
Some checks failed
CI / test (push) Has been cancelled
CI / golangci-lint (push) Has been cancelled
Security Scan / backend-security (push) Has been cancelled
Security Scan / frontend-security (push) Has been cancelled
10 HTML pages + puro.css + HANDOFF.md + 2 images (~810KB total).
Reference artifacts for Stage 3 Vue 3 translation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 18:05:27 +08:00
puro design
02173c8d7e docs: spec v2 + design drafts archive [CI SKIP]
Stage 2 完成(Claude Design 产出 10 页)后的 spec 更新:
- 范围决策:路径 B 分层交付(本期做 Landing/Login/Register/Docs 4 页 + puro.css 落地;二期换皮 Dashboard/API Keys;不做 Binding/Pricing)
- Landing 文案精修:剔除 Pricing/FAQ/CTA banner;Hero CTA 改「登录」+「联系咨询」;微文案对齐真实能力
- Footer 删掉「套餐(暂隐)」
- 新增 §4.5 Docs 页(公开 /docs,精简版)
- §5 brief 标记为历史存档(Stage 2 已完成)
- §5.5 Stage 2 产出清单(10 个文件 + 本期用/二期/不做)
- 归档 docs/design-drafts/v2/ 全部产出(含 puro.css、HANDOFF.md、10 个 HTML)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 18:05:14 +08:00
puro design
332d46cde7 docs: PURO AI landing+auth redesign spec [CI SKIP]
Some checks failed
CI / test (push) Has been cancelled
CI / golangci-lint (push) Has been cancelled
Security Scan / backend-security (push) Has been cancelled
Security Scan / frontend-security (push) Has been cancelled
Stage 1 brainstorm 产出:
- 风格方向:暗黑科技(cyan/purple on slate-950)
- Landing 6 sections + 完整中文文案
- Auth 左右分栏 + "5→1" 数字对比叙事
- 给 claude.ai/design 的 brief(可直接复制)
- Stage 3 实施约束(Vue 3 + Tailwind + i18n 对齐)

下一步:user 拿 brief 到 claude.ai/design 出视觉稿(Stage 2)。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 16:23:20 +08:00
31 changed files with 13336 additions and 64 deletions

3
.gitignore vendored
View File

@@ -129,6 +129,9 @@ vite.config.js
docs/*
!docs/PAYMENT.md
!docs/PAYMENT_CN.md
!docs/superpowers/
!docs/design-drafts/
.superpowers/
.serena/
.codex/
frontend/coverage/

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,689 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>登录 — PURO AI</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<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">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg-0: #0a0e1a;
--bg-1: #0f172a;
--border: #1e293b;
--border-2: #334155;
--text-0: #f8fafc;
--text-1: #cbd5e1;
--text-2: #94a3b8;
--text-3: #64748b;
--cyan: #22d3ee;
--purple: #a855f7;
--amber: #fbbf24;
--red: #f87171;
--green: #34d399;
}
html, body {
background: var(--bg-0);
color: var(--text-0);
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
-webkit-font-smoothing: antialiased;
min-height: 100vh;
}
.mono { font-family: 'JetBrains Mono', ui-monospace, monospace; }
a { color: inherit; text-decoration: none; }
button { font-family: inherit; cursor: pointer; border: none; background: none; color: inherit; }
/* ------ SPLIT LAYOUT ------ */
.split {
display: grid;
grid-template-columns: 1fr 1fr;
min-height: 100vh;
}
/* ------ LEFT (NARRATIVE) ------ */
.narrative {
position: relative;
overflow: hidden;
padding: 48px 56px;
display: flex;
flex-direction: column;
justify-content: space-between;
background: linear-gradient(135deg, #0a0e1a 0%, #1e1b4b 100%);
border-right: 1px solid var(--border);
}
.narrative::before {
content: "";
position: absolute;
top: -200px; left: -200px;
width: 700px; height: 700px;
border-radius: 50%;
background: radial-gradient(circle, #22d3ee 0%, transparent 60%);
filter: blur(100px);
opacity: 0.3;
pointer-events: none;
}
.narrative::after {
content: "";
position: absolute;
bottom: -250px; right: -150px;
width: 700px; height: 700px;
border-radius: 50%;
background: radial-gradient(circle, #a855f7 0%, transparent 60%);
filter: blur(100px);
opacity: 0.3;
pointer-events: none;
}
/* grid pattern */
.narrative-grid {
position: absolute;
inset: 0;
background-image:
linear-gradient(rgba(148, 163, 184, 0.04) 1px, transparent 1px),
linear-gradient(90deg, rgba(148, 163, 184, 0.04) 1px, transparent 1px);
background-size: 48px 48px;
mask-image: radial-gradient(ellipse at center, black 30%, transparent 80%);
pointer-events: none;
}
.narrative-inner {
position: relative;
z-index: 2;
display: flex;
flex-direction: column;
justify-content: space-between;
flex: 1;
min-height: 0;
}
.n-top { padding-top: 4px; }
.brand {
display: inline-flex;
align-items: center;
gap: 10px;
font-weight: 700;
font-size: 15px;
letter-spacing: 0.02em;
}
.hex {
width: 22px; height: 22px;
color: var(--cyan);
}
.n-center {
display: flex;
flex-direction: column;
gap: 28px;
margin: auto 0;
max-width: 520px;
}
.n-kicker {
font-family: 'JetBrains Mono', monospace;
font-size: 11px;
color: var(--cyan);
letter-spacing: 0.2em;
text-transform: uppercase;
}
.n-headline {
font-size: clamp(36px, 4.2vw, 52px);
font-weight: 800;
line-height: 1.1;
letter-spacing: -0.03em;
}
.n-headline .amber {
color: var(--amber);
font-variant-numeric: tabular-nums;
display: inline-block;
position: relative;
}
.n-headline .cyan {
color: var(--cyan);
font-variant-numeric: tabular-nums;
display: inline-block;
position: relative;
}
.n-headline .arrow {
display: inline-block;
margin: 0 14px;
color: var(--text-2);
font-weight: 400;
}
.n-sub {
font-size: 17px;
color: var(--text-1);
line-height: 1.7;
font-weight: 400;
}
.n-sub .line { display: block; }
.n-sub .puro {
color: var(--text-0);
font-weight: 600;
}
.n-sub .mono-accent {
font-family: 'JetBrains Mono', monospace;
color: var(--text-2);
font-size: 14px;
}
/* Visual demo block — a mock "route" diagram */
.route-demo {
border: 1px solid rgba(148, 163, 184, 0.12);
border-radius: 12px;
padding: 18px 20px;
background: rgba(15, 23, 42, 0.5);
backdrop-filter: blur(10px);
max-width: 420px;
font-family: 'JetBrains Mono', monospace;
font-size: 12px;
display: flex;
flex-direction: column;
gap: 10px;
}
.route-row {
display: flex;
align-items: center;
gap: 10px;
color: var(--text-2);
}
.route-row .head {
width: 84px;
color: var(--text-3);
}
.route-row .val {
color: var(--text-0);
}
.route-row.status .val { color: var(--cyan); display: inline-flex; align-items: center; gap: 6px; }
.route-row.status .dot { width: 6px; height: 6px; border-radius: 50%; background: var(--cyan); box-shadow: 0 0 10px var(--cyan); }
.route-row .chip {
padding: 2px 8px;
background: rgba(34, 211, 238, 0.08);
border: 1px solid rgba(34, 211, 238, 0.2);
border-radius: 4px;
color: var(--cyan);
font-size: 11px;
}
.route-row .chip.a { background: rgba(168, 85, 247, 0.08); border-color: rgba(168, 85, 247, 0.2); color: var(--purple); }
.n-bottom {
position: relative;
z-index: 2;
font-family: 'JetBrains Mono', monospace;
font-size: 12px;
color: var(--text-3);
display: flex;
gap: 18px;
padding-top: 24px;
flex-wrap: wrap;
align-items: center;
}
.n-bottom .sep { color: var(--border-2); }
.n-bottom .live {
display: inline-flex; align-items: center; gap: 6px;
color: var(--text-2);
}
.n-bottom .live .dot {
width: 6px; height: 6px; border-radius: 50%; background: var(--green);
animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.4; }
}
/* ------ RIGHT (FORM) ------ */
.form-side {
display: flex;
align-items: center;
justify-content: center;
padding: 48px;
background: var(--bg-0);
position: relative;
}
.form-side::before {
content: "";
position: absolute;
top: 40%; left: 50%;
transform: translate(-50%, -50%);
width: 500px; height: 500px;
background: radial-gradient(circle, rgba(34, 211, 238, 0.06) 0%, transparent 60%);
pointer-events: none;
}
.form-card {
width: 100%;
max-width: 400px;
position: relative;
z-index: 2;
}
.form-card h1 {
font-size: 28px;
font-weight: 700;
letter-spacing: -0.02em;
margin-bottom: 8px;
}
.form-card .sub {
color: var(--text-2);
font-size: 14px;
margin-bottom: 36px;
}
.field {
margin-bottom: 18px;
}
.field label {
display: block;
font-size: 12px;
font-weight: 500;
color: var(--text-1);
margin-bottom: 8px;
letter-spacing: 0.02em;
}
.input-wrap {
position: relative;
display: flex;
align-items: center;
}
.input-wrap .icon {
position: absolute;
left: 14px;
width: 16px;
height: 16px;
color: var(--text-3);
display: flex;
align-items: center;
justify-content: center;
pointer-events: none;
}
.input-wrap input {
width: 100%;
padding: 12px 14px 12px 42px;
background: var(--bg-1);
border: 1px solid var(--border-2);
border-radius: 8px;
color: var(--text-0);
font-size: 14px;
font-family: inherit;
outline: none;
transition: all .15s;
}
.input-wrap input::placeholder { color: var(--text-3); }
.input-wrap input:hover { border-color: #475569; }
.input-wrap input:focus {
border-color: var(--cyan);
box-shadow: 0 0 0 3px rgba(34, 211, 238, 0.12);
background: rgba(15, 23, 42, 0.9);
}
.input-wrap input.error {
border-color: var(--red);
box-shadow: 0 0 0 3px rgba(248, 113, 113, 0.12);
}
.input-wrap .eye {
position: absolute;
right: 12px;
color: var(--text-3);
padding: 4px;
border-radius: 4px;
transition: color .15s;
}
.input-wrap .eye:hover { color: var(--text-1); }
.field-meta {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 12px;
margin-top: 6px;
}
.field-meta .forgot {
color: var(--text-2);
transition: color .15s;
margin-left: auto;
}
.field-meta .forgot:hover { color: var(--cyan); }
.field-error {
color: var(--red);
font-size: 12px;
margin-top: 6px;
display: flex;
align-items: center;
gap: 6px;
}
.field-hint {
color: var(--text-3);
font-size: 12px;
margin-top: 6px;
}
.pw-strength {
display: flex;
gap: 4px;
margin-top: 10px;
}
.pw-strength .bar {
flex: 1;
height: 3px;
background: var(--border);
border-radius: 2px;
transition: background .2s;
}
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
width: 100%;
padding: 13px 20px;
font-size: 14px;
font-weight: 600;
border-radius: 8px;
font-family: inherit;
transition: all .15s;
cursor: pointer;
}
.btn-primary {
background: var(--cyan);
color: #042f2e;
}
.btn-primary:hover { background: #67e8f9; }
.btn-primary:active { transform: translateY(1px); }
.btn-primary:disabled { opacity: 0.5; cursor: not-allowed; }
.btn-ghost {
background: transparent;
border: 1px solid var(--border-2);
color: var(--text-1);
font-weight: 500;
}
.btn-ghost:hover { border-color: #475569; color: var(--text-0); background: rgba(255,255,255,0.02); }
.btn-primary .spinner {
width: 14px; height: 14px; border: 2px solid rgba(0,0,0,0.2);
border-top-color: #042f2e;
border-radius: 50%;
animation: spin .7s linear infinite;
display: none;
}
.btn-primary.loading .spinner { display: inline-block; }
.btn-primary.loading .label { opacity: 0.5; }
@keyframes spin { to { transform: rotate(360deg); } }
.divider {
display: flex;
align-items: center;
gap: 14px;
margin: 24px 0;
color: var(--text-3);
font-size: 11px;
font-family: 'JetBrains Mono', monospace;
letter-spacing: 0.15em;
}
.divider::before, .divider::after {
content: "";
flex: 1;
height: 1px;
background: var(--border);
}
.foot {
margin-top: 28px;
text-align: center;
font-size: 13px;
color: var(--text-2);
}
.foot a {
color: var(--cyan);
font-weight: 500;
transition: color .15s;
}
.foot a:hover { color: #67e8f9; }
.legal {
margin-top: 18px;
text-align: center;
font-size: 11px;
color: var(--text-3);
line-height: 1.6;
}
.legal a { color: var(--text-2); text-decoration: underline; text-decoration-color: var(--border-2); }
.legal a:hover { color: var(--text-0); }
/* checkbox */
.check {
display: flex;
align-items: center;
gap: 8px;
font-size: 12px;
color: var(--text-2);
cursor: pointer;
user-select: none;
}
.check input { display: none; }
.check .box {
width: 14px; height: 14px;
border: 1px solid var(--border-2);
border-radius: 4px;
display: inline-flex;
align-items: center;
justify-content: center;
background: var(--bg-1);
transition: all .15s;
}
.check input:checked + .box {
background: var(--cyan);
border-color: var(--cyan);
}
.check input:checked + .box::after {
content: "✓";
color: #042f2e;
font-size: 10px;
font-weight: 700;
}
.back-home {
position: absolute;
top: 24px;
right: 32px;
font-size: 12px;
color: var(--text-3);
transition: color .15s;
display: inline-flex;
align-items: center;
gap: 6px;
z-index: 10;
}
.back-home:hover { color: var(--text-0); }
/* LinuxDo logo */
.linuxdo-ico {
width: 16px; height: 16px;
border-radius: 3px;
background: linear-gradient(135deg, #f0a030, #f05050);
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 10px;
font-weight: 800;
color: white;
}
/* Mobile */
@media (max-width: 900px) {
.split { grid-template-columns: 1fr; min-height: auto; }
.narrative {
padding: 24px 24px 32px;
border-right: none;
border-bottom: 1px solid var(--border);
min-height: auto;
}
.narrative-inner { gap: 18px; display: block; }
.n-top { margin-bottom: 16px; }
.n-center { gap: 12px; margin: 0; }
.n-headline { font-size: 28px; }
.n-sub { font-size: 14px; }
.n-sub .line { display: inline; }
.route-demo, .n-bottom { display: none; }
.form-side { padding: 32px 24px; }
.back-home { top: 18px; right: 18px; }
}
</style>
</head>
<body>
<div class="split">
<!-- LEFT: NARRATIVE -->
<div class="narrative">
<div class="narrative-grid"></div>
<div class="narrative-inner">
<div class="n-top">
<a href="Landing.html" class="brand">
<svg class="hex" viewBox="0 0 24 24" fill="none">
<path d="M12 2L21 7V17L12 22L3 17V7L12 2Z" stroke="currentColor" stroke-width="1.8" fill="rgba(34, 211, 238, 0.08)"/>
<path d="M12 7L17 9.5V14.5L12 17L7 14.5V9.5L12 7Z" fill="currentColor"/>
</svg>
PURO AI
</a>
</div>
<div class="n-center">
<div class="n-kicker">// 你的订阅,已经付过钱了</div>
<h1 class="n-headline">
<span class="amber">N</span> 个订阅
<span class="arrow"></span>
<span class="cyan">1</span> 个 key
</h1>
<div class="n-sub">
<span class="line">省去切换账号的繁琐,</span>
<span class="line">省去为多个高昂订阅重复买单。</span>
<span class="line"><span class="puro">PURO</span>(纯粹)—— 让 AI 调用回归本质。</span>
</div>
<div class="route-demo">
<div class="route-row"><span class="head">POST</span> <span class="val">/v1/chat/completions</span></div>
<div class="route-row"><span class="head">model</span> <span class="chip">claude-sonnet-4-5</span></div>
<div class="route-row"><span class="head">route →</span> <span class="chip a">claude-pool-03</span></div>
<div class="route-row status"><span class="head">status</span> <span class="val"><span class="dot"></span>200 · 213ms · 42 tok</span></div>
</div>
</div>
<div class="n-bottom">
<span>Claude</span><span class="sep">·</span>
<span>ChatGPT</span><span class="sep">·</span>
<span>Codex</span><span class="sep">·</span>
<span>Gemini</span>
<span class="sep" style="margin: 0 4px;">|</span>
<span class="live"><span class="dot"></span>ai.puro.im · operational</span>
</div>
</div>
</div>
<!-- RIGHT: FORM -->
<div class="form-side">
<a href="Landing.html" class="back-home">← 返回首页</a>
<form class="form-card" id="login-form" autocomplete="off" novalidate>
<h1>登录</h1>
<p class="sub">用你的 PURO AI 账户继续</p>
<div class="field">
<label for="email">邮箱</label>
<div class="input-wrap">
<span class="icon">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="5" width="18" height="14" rx="2"/>
<path d="M3 7l9 6 9-6"/>
</svg>
</span>
<input type="email" id="email" name="email" placeholder="you@puro.im" required>
</div>
</div>
<div class="field">
<label for="password">密码</label>
<div class="input-wrap">
<span class="icon">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
<rect x="4" y="11" width="16" height="10" rx="2"/>
<path d="M8 11V7a4 4 0 0 1 8 0v4"/>
</svg>
</span>
<input type="password" id="password" name="password" placeholder="••••••••" required>
<button type="button" class="eye" id="toggle-pw" aria-label="切换显示密码">
<svg id="eye-open" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
<path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7z"/>
<circle cx="12" cy="12" r="3"/>
</svg>
<svg id="eye-closed" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" style="display:none;">
<path d="M9.88 9.88A3 3 0 0 0 14.12 14.12M10.73 5.08A10.94 10.94 0 0 1 12 5c6.5 0 10 7 10 7a13.16 13.16 0 0 1-1.67 2.68M6.61 6.61A13.53 13.53 0 0 0 2 12s3.5 7 10 7a9.77 9.77 0 0 0 5.39-1.61M2 2l20 20"/>
</svg>
</button>
</div>
<div class="field-meta">
<label class="check">
<input type="checkbox" checked>
<span class="box"></span>
记住我
</label>
<a href="#" class="forgot">忘记密码?</a>
</div>
</div>
<button type="submit" class="btn btn-primary" id="submit-btn">
<span class="spinner"></span>
<span class="label">登录 →</span>
</button>
<div class="divider">OR</div>
<button type="button" class="btn btn-ghost">
<span class="linuxdo-ico">L</span>
使用 LinuxDO 登录
</button>
<div class="foot">
没有账户?<a href="Register.html">注册</a>
</div>
<div class="legal">
登录即表示你同意 <a href="#">服务条款</a><a href="#">隐私政策</a>
</div>
</form>
</div>
</div>
<script>
// eye toggle
const pw = document.getElementById('password');
const toggle = document.getElementById('toggle-pw');
const eyeOpen = document.getElementById('eye-open');
const eyeClosed = document.getElementById('eye-closed');
toggle.addEventListener('click', () => {
const show = pw.type === 'password';
pw.type = show ? 'text' : 'password';
eyeOpen.style.display = show ? 'none' : 'block';
eyeClosed.style.display = show ? 'block' : 'none';
});
// submit (mocked)
const form = document.getElementById('login-form');
const btn = document.getElementById('submit-btn');
form.addEventListener('submit', (e) => {
e.preventDefault();
if (!form.email.value || !form.password.value) return;
btn.classList.add('loading');
btn.disabled = true;
setTimeout(() => {
btn.classList.remove('loading');
btn.disabled = false;
btn.querySelector('.label').textContent = '✓ 登录成功';
btn.style.background = '#34d399';
setTimeout(() => {
btn.querySelector('.label').textContent = '登录 →';
btn.style.background = '';
}, 1500);
}, 1200);
});
</script>
</body>
</html>

View File

@@ -0,0 +1,734 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>注册 — PURO AI</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<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">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg-0: #0a0e1a;
--bg-1: #0f172a;
--border: #1e293b;
--border-2: #334155;
--text-0: #f8fafc;
--text-1: #cbd5e1;
--text-2: #94a3b8;
--text-3: #64748b;
--cyan: #22d3ee;
--purple: #a855f7;
--amber: #fbbf24;
--red: #f87171;
--green: #34d399;
}
html, body {
background: var(--bg-0);
color: var(--text-0);
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
-webkit-font-smoothing: antialiased;
min-height: 100vh;
}
a { color: inherit; text-decoration: none; }
button { font-family: inherit; cursor: pointer; border: none; background: none; color: inherit; }
.split {
display: grid;
grid-template-columns: 1fr 1fr;
min-height: 100vh;
}
/* ------ LEFT (NARRATIVE) ------ */
.narrative {
position: relative;
overflow: hidden;
padding: 48px 56px;
display: flex;
flex-direction: column;
justify-content: space-between;
background: linear-gradient(135deg, #0a0e1a 0%, #1e1b4b 100%);
border-right: 1px solid var(--border);
}
.narrative::before {
content: "";
position: absolute;
top: -200px; left: -200px;
width: 700px; height: 700px;
border-radius: 50%;
background: radial-gradient(circle, #a855f7 0%, transparent 60%);
filter: blur(100px);
opacity: 0.3;
pointer-events: none;
}
.narrative::after {
content: "";
position: absolute;
bottom: -250px; right: -150px;
width: 700px; height: 700px;
border-radius: 50%;
background: radial-gradient(circle, #22d3ee 0%, transparent 60%);
filter: blur(100px);
opacity: 0.3;
pointer-events: none;
}
.narrative-grid {
position: absolute;
inset: 0;
background-image:
linear-gradient(rgba(148, 163, 184, 0.04) 1px, transparent 1px),
linear-gradient(90deg, rgba(148, 163, 184, 0.04) 1px, transparent 1px);
background-size: 48px 48px;
mask-image: radial-gradient(ellipse at center, black 30%, transparent 80%);
pointer-events: none;
}
.narrative-inner {
position: relative;
z-index: 2;
display: flex;
flex-direction: column;
justify-content: space-between;
flex: 1;
min-height: 0;
}
.brand {
display: inline-flex;
align-items: center;
gap: 10px;
font-weight: 700;
font-size: 15px;
letter-spacing: 0.02em;
}
.hex {
width: 22px; height: 22px;
color: var(--cyan);
}
.n-center {
display: flex;
flex-direction: column;
gap: 28px;
margin: auto 0;
max-width: 520px;
}
.n-kicker {
font-family: 'JetBrains Mono', monospace;
font-size: 11px;
color: var(--cyan);
letter-spacing: 0.2em;
text-transform: uppercase;
}
.n-headline {
font-size: clamp(36px, 4.2vw, 52px);
font-weight: 800;
line-height: 1.1;
letter-spacing: -0.03em;
}
.n-headline .amber { color: var(--amber); font-variant-numeric: tabular-nums; display: inline-block; }
.n-headline .cyan { color: var(--cyan); font-variant-numeric: tabular-nums; display: inline-block; }
.n-headline .arrow {
display: inline-block;
margin: 0 14px;
color: var(--text-2);
font-weight: 400;
}
.n-sub {
font-size: 17px;
color: var(--text-1);
line-height: 1.7;
}
.n-sub .line { display: block; }
.n-sub .puro { color: var(--text-0); font-weight: 600; }
/* Step list */
.steps {
display: flex;
flex-direction: column;
gap: 14px;
border: 1px solid rgba(148, 163, 184, 0.12);
border-radius: 12px;
padding: 20px;
background: rgba(15, 23, 42, 0.5);
backdrop-filter: blur(10px);
max-width: 420px;
}
.steps-title {
font-size: 12px;
font-family: 'JetBrains Mono', monospace;
color: var(--text-3);
text-transform: uppercase;
letter-spacing: 0.15em;
margin-bottom: 4px;
}
.step {
display: flex;
align-items: flex-start;
gap: 12px;
font-size: 13px;
}
.step-num {
flex-shrink: 0;
width: 22px; height: 22px;
border-radius: 50%;
background: rgba(34, 211, 238, 0.1);
border: 1px solid rgba(34, 211, 238, 0.25);
color: var(--cyan);
font-family: 'JetBrains Mono', monospace;
font-size: 11px;
font-weight: 600;
display: flex;
align-items: center;
justify-content: center;
}
.step-text { color: var(--text-1); line-height: 1.5; padding-top: 2px; }
.step-text b { color: var(--text-0); font-weight: 600; }
.step-text .k {
font-family: 'JetBrains Mono', monospace;
background: rgba(255,255,255,0.04);
border: 1px solid var(--border);
padding: 1px 6px;
border-radius: 4px;
font-size: 12px;
}
.n-bottom {
position: relative;
z-index: 2;
font-family: 'JetBrains Mono', monospace;
font-size: 12px;
color: var(--text-3);
display: flex;
gap: 18px;
padding-top: 24px;
flex-wrap: wrap;
align-items: center;
}
.n-bottom .sep { color: var(--border-2); }
/* ------ RIGHT (FORM) ------ */
.form-side {
display: flex;
align-items: center;
justify-content: center;
padding: 48px;
background: var(--bg-0);
position: relative;
}
.form-side::before {
content: "";
position: absolute;
top: 40%; left: 50%;
transform: translate(-50%, -50%);
width: 500px; height: 500px;
background: radial-gradient(circle, rgba(168, 85, 247, 0.05) 0%, transparent 60%);
pointer-events: none;
}
.form-card {
width: 100%;
max-width: 400px;
position: relative;
z-index: 2;
}
.form-card h1 {
font-size: 28px;
font-weight: 700;
letter-spacing: -0.02em;
margin-bottom: 8px;
}
.form-card .sub {
color: var(--text-2);
font-size: 14px;
margin-bottom: 32px;
}
.field { margin-bottom: 18px; }
.field label {
display: block;
font-size: 12px;
font-weight: 500;
color: var(--text-1);
margin-bottom: 8px;
}
.input-wrap {
position: relative;
display: flex;
align-items: center;
}
.input-wrap .icon {
position: absolute;
left: 14px;
width: 16px;
height: 16px;
color: var(--text-3);
pointer-events: none;
}
.input-wrap input {
width: 100%;
padding: 12px 14px 12px 42px;
background: var(--bg-1);
border: 1px solid var(--border-2);
border-radius: 8px;
color: var(--text-0);
font-size: 14px;
font-family: inherit;
outline: none;
transition: all .15s;
}
.input-wrap input::placeholder { color: var(--text-3); }
.input-wrap input:hover { border-color: #475569; }
.input-wrap input:focus {
border-color: var(--cyan);
box-shadow: 0 0 0 3px rgba(34, 211, 238, 0.12);
background: rgba(15, 23, 42, 0.9);
}
.input-wrap input.ok { border-color: rgba(52, 211, 153, 0.4); }
.input-wrap input.ok:focus { box-shadow: 0 0 0 3px rgba(52, 211, 153, 0.12); }
.input-wrap input.error { border-color: var(--red); }
.input-wrap .eye {
position: absolute;
right: 12px;
color: var(--text-3);
padding: 4px;
border-radius: 4px;
}
.input-wrap .eye:hover { color: var(--text-1); }
.input-wrap .valid-ico {
position: absolute;
right: 14px;
color: var(--green);
display: none;
}
.input-wrap input.ok ~ .valid-ico { display: flex; }
.pw-strength {
display: flex;
gap: 4px;
margin-top: 10px;
margin-bottom: 6px;
}
.pw-strength .bar {
flex: 1;
height: 3px;
background: var(--border);
border-radius: 2px;
transition: background .2s;
}
.pw-strength[data-score="1"] .bar:nth-child(1) { background: var(--red); }
.pw-strength[data-score="2"] .bar:nth-child(-n+2) { background: var(--amber); }
.pw-strength[data-score="3"] .bar:nth-child(-n+3) { background: var(--cyan); }
.pw-strength[data-score="4"] .bar { background: var(--green); }
.pw-hint {
font-size: 11px;
color: var(--text-3);
display: flex;
justify-content: space-between;
font-family: 'JetBrains Mono', monospace;
}
.pw-hint .label { color: var(--text-3); }
.pw-hint .val { color: var(--text-1); }
.pw-hint[data-score="1"] .val { color: var(--red); }
.pw-hint[data-score="2"] .val { color: var(--amber); }
.pw-hint[data-score="3"] .val { color: var(--cyan); }
.pw-hint[data-score="4"] .val { color: var(--green); }
.match-hint {
font-size: 11px;
font-family: 'JetBrains Mono', monospace;
margin-top: 6px;
color: var(--text-3);
display: flex;
align-items: center;
gap: 6px;
}
.match-hint.ok { color: var(--green); }
.match-hint.bad { color: var(--red); }
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
width: 100%;
padding: 13px 20px;
font-size: 14px;
font-weight: 600;
border-radius: 8px;
transition: all .15s;
cursor: pointer;
}
.btn-primary {
background: var(--cyan);
color: #042f2e;
}
.btn-primary:hover { background: #67e8f9; }
.btn-primary:disabled { opacity: 0.4; cursor: not-allowed; }
.btn-primary .spinner {
width: 14px; height: 14px; border: 2px solid rgba(0,0,0,0.2);
border-top-color: #042f2e;
border-radius: 50%;
animation: spin .7s linear infinite;
display: none;
}
.btn-primary.loading .spinner { display: inline-block; }
.btn-primary.loading .label { opacity: 0.5; }
@keyframes spin { to { transform: rotate(360deg); } }
.btn-ghost {
background: transparent;
border: 1px solid var(--border-2);
color: var(--text-1);
font-weight: 500;
}
.btn-ghost:hover { border-color: #475569; color: var(--text-0); background: rgba(255,255,255,0.02); }
.divider {
display: flex;
align-items: center;
gap: 14px;
margin: 24px 0;
color: var(--text-3);
font-size: 11px;
font-family: 'JetBrains Mono', monospace;
letter-spacing: 0.15em;
}
.divider::before, .divider::after {
content: "";
flex: 1;
height: 1px;
background: var(--border);
}
.foot {
margin-top: 24px;
text-align: center;
font-size: 13px;
color: var(--text-2);
}
.foot a {
color: var(--cyan);
font-weight: 500;
}
.foot a:hover { color: #67e8f9; }
.legal {
margin-top: 16px;
text-align: center;
font-size: 11px;
color: var(--text-3);
line-height: 1.6;
}
.legal a { color: var(--text-2); text-decoration: underline; text-decoration-color: var(--border-2); }
.check {
display: flex;
align-items: flex-start;
gap: 8px;
font-size: 12px;
color: var(--text-2);
cursor: pointer;
user-select: none;
margin-bottom: 20px;
line-height: 1.5;
}
.check input { display: none; }
.check .box {
flex-shrink: 0;
margin-top: 1px;
width: 14px; height: 14px;
border: 1px solid var(--border-2);
border-radius: 4px;
display: inline-flex;
align-items: center;
justify-content: center;
background: var(--bg-1);
}
.check input:checked + .box {
background: var(--cyan);
border-color: var(--cyan);
}
.check input:checked + .box::after {
content: "✓";
color: #042f2e;
font-size: 10px;
font-weight: 700;
}
.check a { color: var(--text-0); text-decoration: underline; text-decoration-color: var(--border-2); }
.back-home {
position: absolute;
top: 24px;
right: 32px;
font-size: 12px;
color: var(--text-3);
z-index: 10;
}
.back-home:hover { color: var(--text-0); }
.linuxdo-ico {
width: 16px; height: 16px;
border-radius: 3px;
background: linear-gradient(135deg, #f0a030, #f05050);
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 10px;
font-weight: 800;
color: white;
}
@media (max-width: 900px) {
.split { grid-template-columns: 1fr; min-height: auto; }
.narrative {
padding: 24px 24px 32px;
border-right: none;
border-bottom: 1px solid var(--border);
}
.narrative-inner { gap: 18px; display: block; }
.n-center { gap: 12px; margin: 16px 0 0; }
.n-headline { font-size: 28px; }
.n-sub { font-size: 14px; }
.n-sub .line { display: inline; }
.steps, .n-bottom { display: none; }
.form-side { padding: 32px 24px; }
.back-home { top: 18px; right: 18px; }
}
</style>
</head>
<body>
<div class="split">
<!-- LEFT: NARRATIVE -->
<div class="narrative">
<div class="narrative-grid"></div>
<div class="narrative-inner">
<div>
<a href="Landing.html" class="brand">
<svg class="hex" viewBox="0 0 24 24" fill="none">
<path d="M12 2L21 7V17L12 22L3 17V7L12 2Z" stroke="currentColor" stroke-width="1.8" fill="rgba(34, 211, 238, 0.08)"/>
<path d="M12 7L17 9.5V14.5L12 17L7 14.5V9.5L12 7Z" fill="currentColor"/>
</svg>
PURO AI
</a>
</div>
<div class="n-center">
<div class="n-kicker">// 5 分钟开始用</div>
<h1 class="n-headline">
<span class="amber">N</span> 个订阅
<span class="arrow"></span>
<span class="cyan">1</span> 个 key
</h1>
<div class="n-sub">
<span class="line">省去切换账号的繁琐,</span>
<span class="line">省去为多个高昂订阅重复买单。</span>
<span class="line"><span class="puro">PURO</span>(纯粹)—— 让 AI 调用回归本质。</span>
</div>
<div class="steps">
<div class="steps-title">// 下一步</div>
<div class="step">
<div class="step-num">1</div>
<div class="step-text"><b>创建账户</b> · 邮箱 + 密码,或用 LinuxDO OAuth</div>
</div>
<div class="step">
<div class="step-num">2</div>
<div class="step-text"><b>绑定订阅</b> · OAuth 接入你现有的 Claude Pro / ChatGPT Plus</div>
</div>
<div class="step">
<div class="step-num">3</div>
<div class="step-text"><b>生成 key</b> · 拿到 <span class="k">sk-puro-…</span> 换掉 SDK 的 <span class="k">base_url</span></div>
</div>
</div>
</div>
<div class="n-bottom">
<span>Claude</span><span class="sep">·</span>
<span>ChatGPT</span><span class="sep">·</span>
<span>Codex</span><span class="sep">·</span>
<span>Gemini</span>
<span class="sep" style="margin: 0 4px;">|</span>
<span>无需信用卡 · 永久免费 Hobby 套餐</span>
</div>
</div>
</div>
<!-- RIGHT: FORM -->
<div class="form-side">
<a href="Landing.html" class="back-home">← 返回首页</a>
<form class="form-card" id="reg-form" autocomplete="off" novalidate>
<h1>创建账户</h1>
<p class="sub">5 分钟开始用 PURO AI</p>
<div class="field">
<label for="email">邮箱</label>
<div class="input-wrap">
<span class="icon">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="5" width="18" height="14" rx="2"/>
<path d="M3 7l9 6 9-6"/>
</svg>
</span>
<input type="email" id="email" name="email" placeholder="you@puro.im" required>
<span class="valid-ico">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M5 12l5 5L20 7"/>
</svg>
</span>
</div>
</div>
<div class="field">
<label for="password">密码</label>
<div class="input-wrap">
<span class="icon">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
<rect x="4" y="11" width="16" height="10" rx="2"/>
<path d="M8 11V7a4 4 0 0 1 8 0v4"/>
</svg>
</span>
<input type="password" id="password" name="password" placeholder="至少 8 位,含字母与数字" required>
<button type="button" class="eye" id="toggle-pw" aria-label="切换显示密码">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
<path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7z"/>
<circle cx="12" cy="12" r="3"/>
</svg>
</button>
</div>
<div class="pw-strength" id="pw-strength" data-score="0">
<span class="bar"></span><span class="bar"></span><span class="bar"></span><span class="bar"></span>
</div>
<div class="pw-hint" id="pw-hint" data-score="0">
<span class="label">// strength</span>
<span class="val"></span>
</div>
</div>
<div class="field">
<label for="password2">确认密码</label>
<div class="input-wrap">
<span class="icon">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
<rect x="4" y="11" width="16" height="10" rx="2"/>
<path d="M8 11V7a4 4 0 0 1 8 0v4"/>
</svg>
</span>
<input type="password" id="password2" name="password2" placeholder="再输入一次" required>
<span class="valid-ico">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M5 12l5 5L20 7"/>
</svg>
</span>
</div>
<div class="match-hint" id="match-hint"></div>
</div>
<label class="check">
<input type="checkbox" id="terms">
<span class="box"></span>
<span>我已阅读并同意 <a href="#">服务条款</a><a href="#">隐私政策</a></span>
</label>
<button type="submit" class="btn btn-primary" id="submit-btn" disabled>
<span class="spinner"></span>
<span class="label">创建账户 →</span>
</button>
<div class="divider">OR</div>
<button type="button" class="btn btn-ghost">
<span class="linuxdo-ico">L</span>
使用 LinuxDO 注册
</button>
<div class="foot">
已有账户?<a href="Login.html">登录</a>
</div>
<div class="legal">
注册即可获得 Hobby 套餐 · 无需信用卡
</div>
</form>
</div>
</div>
<script>
const pw = document.getElementById('password');
const pw2 = document.getElementById('password2');
const email = document.getElementById('email');
const terms = document.getElementById('terms');
const btn = document.getElementById('submit-btn');
const strengthBars = document.getElementById('pw-strength');
const strengthHint = document.getElementById('pw-hint');
const matchHint = document.getElementById('match-hint');
const toggle = document.getElementById('toggle-pw');
toggle.addEventListener('click', () => {
pw.type = pw.type === 'password' ? 'text' : 'password';
});
const labels = ['—', '太弱', '一般', '良好', '强'];
function scorePw(v) {
let s = 0;
if (v.length >= 8) s++;
if (/[a-z]/.test(v) && /[A-Z]/.test(v)) s++;
if (/\d/.test(v)) s++;
if (/[^\w\s]/.test(v) || v.length >= 14) s++;
if (v.length === 0) s = 0;
return Math.min(s, 4);
}
function update() {
const s = scorePw(pw.value);
strengthBars.dataset.score = s;
strengthHint.dataset.score = s;
strengthHint.querySelector('.val').textContent = labels[s];
// email validity
const emailOk = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.value);
email.classList.toggle('ok', emailOk);
// match
if (pw2.value.length === 0) {
matchHint.textContent = '';
matchHint.className = 'match-hint';
pw2.classList.remove('ok', 'error');
} else if (pw.value === pw2.value) {
matchHint.innerHTML = '<span>✓</span> 两次密码一致';
matchHint.className = 'match-hint ok';
pw2.classList.add('ok');
pw2.classList.remove('error');
} else {
matchHint.innerHTML = '<span>✗</span> 密码不一致';
matchHint.className = 'match-hint bad';
pw2.classList.add('error');
pw2.classList.remove('ok');
}
const ready = emailOk && s >= 2 && pw.value === pw2.value && pw.value.length >= 8 && terms.checked;
btn.disabled = !ready;
}
[email, pw, pw2, terms].forEach(el => el.addEventListener('input', update));
terms.addEventListener('change', update);
const form = document.getElementById('reg-form');
form.addEventListener('submit', (e) => {
e.preventDefault();
if (btn.disabled) return;
btn.classList.add('loading');
btn.disabled = true;
setTimeout(() => {
btn.classList.remove('loading');
btn.querySelector('.label').textContent = '✓ 账户已创建,跳转中…';
btn.style.background = '#34d399';
}, 1500);
});
</script>
</body>
</html>

View File

@@ -0,0 +1,637 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="color-scheme" content="dark">
<title>API Keys — PURO AI</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<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">
<link rel="stylesheet" href="puro.css">
<style>
.page-head {
display: flex; align-items:flex-end; justify-content:space-between;
margin-bottom: 28px; gap: 24px;
}
.page-head h1 { font-size: 28px; font-weight: 700; letter-spacing:-0.02em; margin-bottom:6px; }
.page-head .sub { color: var(--text-2); font-size: 14px; max-width: 560px; line-height: 1.55; }
.summary {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 12px;
margin-bottom: 24px;
}
.summary .cell {
padding: 14px 16px;
border: 1px solid var(--border);
border-radius: var(--r-lg);
background: rgba(15, 23, 42, 0.5);
}
.summary .cell-label {
font-size: 11px; color: var(--text-3);
text-transform: uppercase; letter-spacing: 0.1em;
font-family: var(--font-mono);
margin-bottom: 8px;
}
.summary .cell-value {
font-family: var(--font-mono);
font-size: 20px; font-weight: 700;
font-variant-numeric: tabular-nums;
letter-spacing: -0.01em;
}
.summary .cell-value .unit { font-size: 12px; color: var(--text-3); margin-left: 4px; }
.toolbar {
display: flex; gap: 10px; align-items: center;
margin-bottom: 14px;
}
.toolbar .search-box {
flex: 1; max-width: 340px; position: relative;
}
.toolbar .search-box input {
width: 100%; height: 36px;
padding: 0 12px 0 34px;
background: rgba(2, 6, 23, 0.5);
border: 1px solid var(--border);
border-radius: var(--r-sm);
color: var(--text-1); font-size: 13px;
outline: none; font-family: inherit;
}
.toolbar .search-box input:focus { border-color: var(--border-2); }
.toolbar .search-box::before {
content: ""; position: absolute; left: 12px; top: 50%;
width: 14px; height: 14px; transform: translateY(-50%);
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 24 24' fill='none' stroke='%2364748b' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><circle cx='11' cy='11' r='7'/><path d='m20 20-3.5-3.5'/></svg>");
background-repeat: no-repeat;
}
.filter-btn {
display: inline-flex; align-items: center; gap: 6px;
padding: 0 12px; height: 36px;
border: 1px solid var(--border);
border-radius: var(--r-sm);
background: rgba(2, 6, 23, 0.5);
color: var(--text-2); font-size: 13px;
transition: all .12s;
}
.filter-btn:hover { border-color: var(--border-2); color: var(--text-0); }
.filter-btn .dot-count {
display: inline-flex; align-items: center; justify-content: center;
width: 16px; height: 16px; border-radius: 50%;
background: var(--cyan); color: #042f2e;
font-size: 10px; font-weight: 700;
}
/* key card */
.key-list { display: flex; flex-direction: column; gap: 10px; }
.key-card {
border: 1px solid var(--border);
border-radius: var(--r-lg);
background: rgba(15, 23, 42, 0.5);
padding: 18px 20px;
transition: all .12s;
}
.key-card:hover { border-color: var(--border-2); background: rgba(15, 23, 42, 0.7); }
.key-card.revoked { opacity: 0.6; }
.key-head {
display: flex; align-items: center; gap: 14px;
margin-bottom: 14px;
}
.key-name {
font-size: 15px; font-weight: 600;
display: flex; align-items: center; gap: 10px;
}
.key-meta {
font-family: var(--font-mono); font-size: 11px; color: var(--text-3);
display: flex; align-items: center; gap: 12px;
margin-left: auto;
}
.key-meta .sep { color: var(--border-2); }
.key-actions { display: flex; gap: 4px; margin-left: 6px; }
.icon-act {
width: 28px; height: 28px;
border-radius: 6px;
color: var(--text-3);
display: inline-flex; align-items: center; justify-content: center;
transition: all .12s;
}
.icon-act:hover { color: var(--text-0); background: rgba(255,255,255,0.04); }
.icon-act.danger:hover { color: var(--red); background: rgba(248,113,113,0.08); }
.key-value {
display: flex; align-items: center; gap: 10px;
padding: 10px 14px;
background: rgba(2, 6, 23, 0.6);
border: 1px solid var(--border);
border-radius: var(--r-md);
font-family: var(--font-mono);
font-size: 13px;
color: var(--text-1);
margin-bottom: 14px;
}
.key-value .prefix { color: var(--cyan); }
.key-value .rest { letter-spacing: 0.05em; }
.key-value .reveal {
margin-left: auto;
color: var(--text-3); font-size: 11px;
padding: 4px 10px; border-radius: 4px;
cursor: pointer; transition: all .12s;
}
.key-value .reveal:hover { color: var(--cyan); background: rgba(34,211,238,0.08); }
.key-value .copy-btn {
color: var(--text-3); font-size: 11px;
padding: 4px 10px; border-radius: 4px;
cursor: pointer; transition: all .12s;
}
.key-value .copy-btn:hover { color: var(--cyan); background: rgba(34,211,238,0.08); }
.key-scopes {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 12px;
padding-top: 14px;
border-top: 1px dashed var(--border);
}
.scope {
display: flex; flex-direction: column; gap: 4px;
}
.scope-label {
font-size: 10px; color: var(--text-3);
text-transform: uppercase; letter-spacing: 0.12em;
font-family: var(--font-mono);
}
.scope-val {
font-size: 12px; color: var(--text-1);
font-family: var(--font-mono);
}
.scope-val.tags {
display: flex; gap: 4px; flex-wrap: wrap;
}
.mini-tag {
display: inline-flex; align-items: center; gap: 4px;
padding: 1px 6px;
background: rgba(255,255,255,0.04);
border: 1px solid var(--border);
border-radius: 3px;
font-size: 10px;
color: var(--text-2);
}
.mini-tag .dot { width: 5px; height: 5px; border-radius: 50%; }
.mini-tag.all { color: var(--cyan); border-color: rgba(34,211,238,0.25); background: rgba(34,211,238,0.06); }
.usage-bar {
height: 4px; background: var(--border);
border-radius: 2px; overflow: hidden;
margin-top: 2px;
}
.usage-bar span { display: block; height: 100%; background: var(--cyan); }
.usage-bar.warn span { background: var(--amber); }
/* ---------- modal ---------- */
.backdrop {
position: fixed; inset: 0;
background: rgba(2, 6, 23, 0.8);
backdrop-filter: blur(8px);
z-index: 100;
display: none;
align-items: center;
justify-content: center;
padding: 40px;
}
.backdrop.open { display: flex; }
.modal {
width: 540px; max-width: 100%;
background: var(--bg-1);
border: 1px solid var(--border-2);
border-radius: var(--r-xl);
box-shadow: var(--shadow-xl);
overflow: hidden;
}
.modal-head {
padding: 22px 28px 16px;
border-bottom: 1px solid var(--border);
display: flex; align-items: center;
}
.modal-head h2 {
font-size: 18px; font-weight: 600;
letter-spacing: -0.01em;
}
.modal-head .close {
margin-left: auto; padding: 6px;
color: var(--text-3); cursor: pointer;
border-radius: 6px;
}
.modal-head .close:hover { color: var(--text-0); background: rgba(255,255,255,0.04); }
.modal-body { padding: 22px 28px; }
.modal-foot {
padding: 14px 28px 22px;
border-top: 1px solid var(--border);
display: flex; gap: 10px; justify-content: flex-end;
}
/* newly-created key callout */
.new-key-callout {
padding: 16px;
border: 1px solid rgba(52, 211, 153, 0.3);
border-radius: var(--r-md);
background: rgba(52, 211, 153, 0.05);
margin-bottom: 18px;
}
.new-key-callout .header {
display: flex; align-items: center; gap: 8px;
font-size: 13px; font-weight: 600; color: var(--green);
margin-bottom: 10px;
}
.new-key-callout .warn {
font-size: 12px; color: var(--text-2); margin-top: 10px;
line-height: 1.55;
}
/* scope picker */
.scope-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
}
.scope-opt {
display: flex; align-items: center; gap: 10px;
padding: 10px 12px;
border: 1px solid var(--border);
border-radius: var(--r-sm);
background: rgba(2, 6, 23, 0.4);
cursor: pointer;
transition: all .12s;
}
.scope-opt:hover { border-color: var(--border-2); }
.scope-opt.active {
border-color: var(--cyan);
background: rgba(34,211,238,0.06);
}
.scope-opt .box {
width: 14px; height: 14px;
border-radius: 3px;
border: 1px solid var(--border-2);
flex-shrink: 0;
display: flex; align-items: center; justify-content: center;
}
.scope-opt.active .box {
background: var(--cyan); border-color: var(--cyan);
}
.scope-opt.active .box::after {
content: "✓"; color: #042f2e; font-size: 10px; font-weight: 700;
}
.scope-opt .label { font-size: 13px; color: var(--text-0); }
.scope-opt .desc { font-size: 11px; color: var(--text-3); font-family: var(--font-mono); margin-left: auto; }
/* topbar (reused) */
.topbar-user {
margin-left: auto; display: flex; gap: 10px; align-items: center;
}
.user-menu {
display: flex; align-items: center; gap: 8px;
padding: 4px 10px 4px 4px;
border-radius: 100px;
background: rgba(255,255,255,0.03);
border: 1px solid var(--border);
cursor: pointer;
}
.user-menu .name { font-size: 13px; font-weight: 500; }
</style>
</head>
<body>
<div class="bg-glow soft"></div>
<div class="app-shell">
<!-- SIDEBAR (same as dashboard) -->
<aside class="app-side">
<a class="brand" href="Landing.html">
<svg class="hex" viewBox="0 0 24 24" fill="none">
<path d="M12 2L21 7V17L12 22L3 17V7L12 2Z" stroke="currentColor" stroke-width="1.8" fill="rgba(34, 211, 238, 0.08)"/>
<path d="M12 7L17 9.5V14.5L12 17L7 14.5V9.5L12 7Z" fill="currentColor"/>
</svg>
PURO
</a>
<div class="side-group">
<div class="side-label">Workspace</div>
<a class="side-item" href="Dashboard.html"><span class="ico"></span>Dashboard</a>
<a class="side-item active" href="API Keys.html"><span class="ico"></span>API Keys<span class="count">3</span></a>
<a class="side-item" href="Accounts.html"><span class="ico"></span>订阅账号<span class="count">7</span></a>
<a class="side-item" href="#"><span class="ico"></span>调用日志</a>
<a class="side-item" href="#"><span class="ico">$</span>账单 & 充值</a>
</div>
<div class="side-group">
<div class="side-label">Resources</div>
<a class="side-item" href="Docs.html"><span class="ico">📖</span>文档</a>
<a class="side-item" href="Design System.html"><span class="ico"></span>Design System</a>
</div>
<div class="side-group" style="margin-top:auto; padding-top:20px; border-top:1px solid var(--border);">
<a class="side-item" href="#"><span class="ico"></span>设置</a>
</div>
</aside>
<div class="app-main">
<header class="app-topbar">
<h1>API Keys</h1>
<div class="topbar-user">
<div class="user-menu"><span class="avatar">Z</span><span class="name">zane</span></div>
</div>
</header>
<div class="app-content">
<div class="page-head">
<div>
<h1>API Keys</h1>
<div class="sub">每个 key 是一张独立的"通行证",可以单独设置可用的订阅池、限速和预算,泄漏时可以直接吊销而不影响其他 key。</div>
</div>
<button class="btn btn-primary btn-lg" onclick="document.getElementById('create-modal').classList.add('open')">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><path d="M12 5v14M5 12h14"/></svg>
创建 Key
</button>
</div>
<!-- summary -->
<div class="summary">
<div class="cell">
<div class="cell-label">活跃 Keys</div>
<div class="cell-value tabular">3<span class="unit">/ 10 上限</span></div>
</div>
<div class="cell">
<div class="cell-label">近 7 日调用</div>
<div class="cell-value tabular">89,402</div>
</div>
<div class="cell">
<div class="cell-label">近 7 日花费</div>
<div class="cell-value tabular">$24.18<span class="unit">USD</span></div>
</div>
<div class="cell">
<div class="cell-label">已吊销</div>
<div class="cell-value tabular text-3">2</div>
</div>
</div>
<!-- toolbar -->
<div class="toolbar">
<div class="search-box">
<input placeholder="搜索 key 名称或前缀…">
</div>
<button class="filter-btn">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M22 3H2l8 9.46V19l4 2v-8.54z"/></svg>
筛选 <span class="dot-count">2</span>
</button>
<button class="filter-btn">
全部范围
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M6 9l6 6 6-6"/></svg>
</button>
<div style="margin-left:auto;"></div>
<button class="filter-btn">
显示吊销
</button>
</div>
<!-- list -->
<div class="key-list">
<div class="key-card">
<div class="key-head">
<div class="key-name">
production
<span class="badge green">ACTIVE</span>
</div>
<div class="key-meta">
<span>created 2026·03·14</span>
<span class="sep">·</span>
<span>last used 2m ago</span>
<span class="sep">·</span>
<span class="text-cyan">● in use</span>
</div>
<div class="key-actions">
<button class="icon-act" title="编辑">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20h9"/><path d="M16.5 3.5a2.12 2.12 0 1 1 3 3L7 19l-4 1 1-4z"/></svg>
</button>
<button class="icon-act" title="轮换">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-9-9c2.5 0 4.8 1 6.4 2.6L21 8"/><path d="M21 3v5h-5"/></svg>
</button>
<button class="icon-act danger" title="吊销">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/></svg>
</button>
</div>
</div>
<div class="key-value">
<span class="prefix">sk-puro-</span>
<span class="rest">••••••••••••••••••••••••4f82</span>
<span class="reveal">👁 显示</span>
<span class="copy-btn">复制</span>
</div>
<div class="key-scopes">
<div class="scope">
<div class="scope-label">可用订阅池</div>
<div class="scope-val tags">
<span class="mini-tag"><span class="dot" style="background:#d97757"></span>claude · 2</span>
<span class="mini-tag"><span class="dot" style="background:#10a37f"></span>gpt · 2</span>
<span class="mini-tag"><span class="dot" style="background:#4285f4"></span>gemini · 1</span>
</div>
</div>
<div class="scope">
<div class="scope-label">本月用量</div>
<div class="scope-val tabular">$14.82 <span class="text-3">/ $50</span></div>
<div class="usage-bar"><span style="width:30%"></span></div>
</div>
<div class="scope">
<div class="scope-label">速率限制</div>
<div class="scope-val">120 RPM</div>
</div>
<div class="scope">
<div class="scope-label">关联应用</div>
<div class="scope-val tags">
<span class="mini-tag">Claude Code</span>
<span class="mini-tag">Cursor</span>
</div>
</div>
</div>
</div>
<div class="key-card">
<div class="key-head">
<div class="key-name">
staging
<span class="badge amber">RATE LIMITED</span>
</div>
<div class="key-meta">
<span>created 2026·04·02</span>
<span class="sep">·</span>
<span>last used 3h ago</span>
</div>
<div class="key-actions">
<button class="icon-act"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20h9"/><path d="M16.5 3.5a2.12 2.12 0 1 1 3 3L7 19l-4 1 1-4z"/></svg></button>
<button class="icon-act"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-9-9c2.5 0 4.8 1 6.4 2.6L21 8"/><path d="M21 3v5h-5"/></svg></button>
<button class="icon-act danger"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/></svg></button>
</div>
</div>
<div class="key-value">
<span class="prefix">sk-puro-</span>
<span class="rest">••••••••••••••••••••••••ae19</span>
<span class="reveal">👁 显示</span>
<span class="copy-btn">复制</span>
</div>
<div class="key-scopes">
<div class="scope">
<div class="scope-label">可用订阅池</div>
<div class="scope-val tags">
<span class="mini-tag all">all pools</span>
</div>
</div>
<div class="scope">
<div class="scope-label">本月用量</div>
<div class="scope-val tabular">$8.24 <span class="text-3">/ $10</span></div>
<div class="usage-bar warn"><span style="width:82%"></span></div>
</div>
<div class="scope">
<div class="scope-label">速率限制</div>
<div class="scope-val">30 RPM</div>
</div>
<div class="scope">
<div class="scope-label">关联应用</div>
<div class="scope-val tags">
<span class="mini-tag">本地开发</span>
</div>
</div>
</div>
</div>
<div class="key-card">
<div class="key-head">
<div class="key-name">
cli-personal
<span class="badge">ACTIVE</span>
</div>
<div class="key-meta">
<span>created 2026·04·11</span>
<span class="sep">·</span>
<span>last used 18h ago</span>
</div>
<div class="key-actions">
<button class="icon-act"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20h9"/><path d="M16.5 3.5a2.12 2.12 0 1 1 3 3L7 19l-4 1 1-4z"/></svg></button>
<button class="icon-act"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-9-9c2.5 0 4.8 1 6.4 2.6L21 8"/><path d="M21 3v5h-5"/></svg></button>
<button class="icon-act danger"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/></svg></button>
</div>
</div>
<div class="key-value">
<span class="prefix">sk-puro-</span>
<span class="rest">••••••••••••••••••••••••c3d1</span>
<span class="reveal">👁 显示</span>
<span class="copy-btn">复制</span>
</div>
<div class="key-scopes">
<div class="scope">
<div class="scope-label">可用订阅池</div>
<div class="scope-val tags">
<span class="mini-tag"><span class="dot" style="background:#d97757"></span>claude · 1</span>
</div>
</div>
<div class="scope">
<div class="scope-label">本月用量</div>
<div class="scope-val tabular">$1.12 <span class="text-3">/ 无限制</span></div>
<div class="usage-bar"><span style="width:6%"></span></div>
</div>
<div class="scope">
<div class="scope-label">速率限制</div>
<div class="scope-val">60 RPM</div>
</div>
<div class="scope">
<div class="scope-label">关联应用</div>
<div class="scope-val tags">
<span class="mini-tag">Terminal</span>
</div>
</div>
</div>
</div>
<!-- revoked -->
<div class="key-card revoked">
<div class="key-head">
<div class="key-name">
<span style="text-decoration:line-through; color:var(--text-2);">old-demo</span>
<span class="badge red">REVOKED</span>
</div>
<div class="key-meta">
<span>revoked 2026·03·02</span>
</div>
</div>
<div class="key-value" style="opacity:0.7">
<span class="prefix">sk-puro-</span>
<span class="rest">••••••••••••••••••••••••0ab3</span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- CREATE MODAL -->
<div class="backdrop" id="create-modal">
<div class="modal">
<div class="modal-head">
<h2>创建新的 API Key</h2>
<span class="close" onclick="document.getElementById('create-modal').classList.remove('open')"></span>
</div>
<div class="modal-body">
<div class="field">
<label class="field-label">Key 名称</label>
<input class="input" placeholder="e.g. production · staging · cursor-macbook">
<div class="field-hint">仅用于辨识,不会出现在请求中。</div>
</div>
<div class="field">
<label class="field-label">可用订阅池</label>
<div class="scope-grid">
<div class="scope-opt active">
<span class="box"></span>
<span class="label">Claude 池</span>
<span class="desc">2 accounts</span>
</div>
<div class="scope-opt active">
<span class="box"></span>
<span class="label">GPT 池</span>
<span class="desc">2 accounts</span>
</div>
<div class="scope-opt">
<span class="box"></span>
<span class="label">Gemini 池</span>
<span class="desc">1 account</span>
</div>
<div class="scope-opt">
<span class="box"></span>
<span class="label">Codex 池</span>
<span class="desc">0 accounts</span>
</div>
</div>
</div>
<div style="display:grid; grid-template-columns:1fr 1fr; gap:14px;">
<div class="field" style="margin-bottom:0">
<label class="field-label">月度预算 ($USD)</label>
<input class="input" placeholder="50" type="number">
<div class="field-hint">达到后自动停用,下月 1 号重置。</div>
</div>
<div class="field" style="margin-bottom:0">
<label class="field-label">速率限制 (RPM)</label>
<input class="input" placeholder="60" type="number">
<div class="field-hint">每分钟最大请求数。</div>
</div>
</div>
</div>
<div class="modal-foot">
<button class="btn btn-ghost" onclick="document.getElementById('create-modal').classList.remove('open')">取消</button>
<button class="btn btn-primary">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M5 12l5 5L20 7"/></svg>
创建 Key
</button>
</div>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,561 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="color-scheme" content="dark">
<title>绑定订阅 — PURO AI</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<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">
<link rel="stylesheet" href="puro.css">
<style>
.wrap {
max-width: 840px;
margin: 0 auto;
padding: 48px 24px 80px;
}
.head { text-align: center; margin-bottom: 40px; }
.head h1 { font-size: 34px; font-weight: 700; letter-spacing: -0.02em; margin-bottom: 10px; }
.head p { color: var(--text-2); font-size: 15px; max-width: 560px; margin: 0 auto; line-height: 1.6; }
/* stepper */
.steps {
display: flex; align-items: center; justify-content: center;
gap: 0; margin-bottom: 40px;
}
.step {
display: flex; align-items: center; gap: 10px;
padding: 6px 0;
}
.step .num {
width: 26px; height: 26px; border-radius: 50%;
background: rgba(2, 6, 23, 0.6);
border: 1px solid var(--border);
color: var(--text-3);
display: inline-flex; align-items: center; justify-content: center;
font-family: var(--font-mono); font-size: 12px; font-weight: 600;
}
.step.done .num { background: var(--cyan); color: #042f2e; border-color: var(--cyan); }
.step.active .num { border-color: var(--cyan); color: var(--cyan); background: rgba(34,211,238,0.08); }
.step .label { font-size: 13px; color: var(--text-3); }
.step.done .label, .step.active .label { color: var(--text-0); }
.step-line {
width: 80px; height: 1px; background: var(--border);
margin: 0 14px;
}
.step-line.done { background: var(--cyan); }
/* platform picker */
.platform-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 14px;
margin-bottom: 20px;
}
.platform-card {
padding: 22px 20px;
border: 1px solid var(--border);
border-radius: var(--r-lg);
background: rgba(15, 23, 42, 0.5);
cursor: pointer;
transition: all .15s;
position: relative;
}
.platform-card:hover { border-color: var(--border-2); transform: translateY(-2px); }
.platform-card.selected {
border-color: var(--cyan);
background:
radial-gradient(400px 200px at 50% 0%, rgba(34,211,238,0.1), transparent 60%),
rgba(15, 23, 42, 0.8);
}
.platform-card.selected::after {
content: "✓"; position: absolute; top: 12px; right: 12px;
width: 20px; height: 20px; border-radius: 50%;
background: var(--cyan); color: #042f2e;
display: inline-flex; align-items: center; justify-content: center;
font-size: 12px; font-weight: 700;
}
.platform-logo {
width: 40px; height: 40px;
border-radius: 8px;
display: flex; align-items: center; justify-content: center;
margin-bottom: 14px;
}
.platform-logo.claude { background: rgba(217, 119, 87, 0.12); }
.platform-logo.gpt { background: rgba(16, 163, 127, 0.12); }
.platform-logo.gemini { background: rgba(66, 133, 244, 0.12); }
.platform-logo.codex { background: rgba(240, 160, 48, 0.12); }
.platform-name {
font-size: 15px; font-weight: 600;
margin-bottom: 4px;
}
.platform-tier {
font-size: 11px; color: var(--text-3);
font-family: var(--font-mono);
margin-bottom: 14px;
}
.platform-tiers {
display: flex; gap: 4px; flex-wrap: wrap;
}
.platform-tiers .pill {
font-size: 10px;
}
/* selected detail panel */
.detail-panel {
border: 1px solid var(--border);
border-radius: var(--r-xl);
background:
radial-gradient(600px 300px at 0% 0%, rgba(34,211,238,0.06), transparent 60%),
rgba(15, 23, 42, 0.4);
overflow: hidden;
}
.detail-head {
padding: 22px 26px;
border-bottom: 1px solid var(--border);
display: flex;
align-items: center;
gap: 14px;
}
.detail-head .platform-logo { margin: 0; }
.detail-head h2 {
font-size: 18px;
font-weight: 600;
letter-spacing: -0.01em;
}
.detail-head .sub {
font-size: 12px;
color: var(--text-3);
font-family: var(--font-mono);
margin-top: 2px;
}
.detail-head .secure-tag {
margin-left: auto;
display: inline-flex; align-items: center; gap: 6px;
padding: 4px 10px;
font-size: 11px; font-family: var(--font-mono);
color: var(--green);
background: rgba(52,211,153,0.08);
border: 1px solid rgba(52,211,153,0.2);
border-radius: 4px;
}
.detail-body { padding: 24px 26px; }
.tier-picker {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 10px;
margin-bottom: 22px;
}
.tier-opt {
padding: 14px 16px;
border: 1px solid var(--border);
border-radius: var(--r-md);
background: rgba(2, 6, 23, 0.4);
cursor: pointer;
position: relative;
transition: all .12s;
}
.tier-opt:hover { border-color: var(--border-2); }
.tier-opt.selected {
border-color: var(--cyan);
background: rgba(34,211,238,0.05);
}
.tier-opt .t-name { font-size: 13px; font-weight: 600; margin-bottom: 4px; }
.tier-opt .t-price {
font-family: var(--font-mono); font-size: 18px;
font-weight: 700; letter-spacing: -0.02em;
color: var(--cyan); margin-bottom: 4px;
}
.tier-opt .t-price .unit {
font-size: 11px; color: var(--text-3);
font-weight: 500; margin-left: 3px;
}
.tier-opt .t-desc {
font-size: 11px; color: var(--text-3);
}
/* method cards */
.method-section h3 {
font-size: 13px; font-weight: 600;
margin-bottom: 10px;
color: var(--text-1);
}
.method-section h3 .hint {
color: var(--text-3); font-weight: 400;
font-family: var(--font-mono); font-size: 11px;
margin-left: 6px;
}
.method-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
margin-bottom: 14px;
}
.method-card {
padding: 18px;
border: 1px solid var(--border);
border-radius: var(--r-md);
background: rgba(2, 6, 23, 0.4);
cursor: pointer;
transition: all .12s;
position: relative;
}
.method-card:hover { border-color: var(--border-2); background: rgba(2, 6, 23, 0.6); }
.method-card.selected {
border-color: var(--cyan);
background: rgba(34, 211, 238, 0.04);
}
.method-card.selected::after {
content: ""; position: absolute; top: 12px; right: 12px;
width: 16px; height: 16px; border-radius: 50%;
background: var(--cyan);
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='10' viewBox='0 0 24 24' fill='none' stroke='%23042f2e' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'><path d='M5 12l5 5L20 7'/></svg>");
background-repeat: no-repeat; background-position: center;
}
.method-title {
display: flex; align-items: center; gap: 8px;
font-size: 14px; font-weight: 600;
margin-bottom: 6px;
}
.method-title .tag {
font-size: 10px; font-family: var(--font-mono);
padding: 1px 6px; border-radius: 3px;
background: rgba(52,211,153,0.12); color: var(--green);
}
.method-title .tag.beta { background: rgba(251,191,36,0.12); color: var(--amber); }
.method-desc { font-size: 12px; color: var(--text-2); line-height: 1.55; }
.method-steps {
font-family: var(--font-mono); font-size: 11px;
color: var(--text-3); margin-top: 10px;
display: flex; gap: 6px; flex-wrap: wrap;
}
.method-steps span::after {
content: "→"; margin-left: 6px; color: var(--border-2);
}
.method-steps span:last-child::after { display: none; }
/* OAuth preview */
.oauth-preview {
margin-top: 14px;
padding: 18px;
background: rgba(2, 6, 23, 0.5);
border: 1px solid var(--border);
border-radius: var(--r-md);
display: flex; gap: 16px; align-items: center;
}
.oauth-preview .flow {
flex: 1;
display: flex; align-items: center; gap: 10px;
font-family: var(--font-mono); font-size: 12px;
color: var(--text-2);
}
.oauth-preview .flow .arrow {
color: var(--cyan);
}
.oauth-preview .flow .node {
padding: 4px 10px;
background: rgba(255,255,255,0.04);
border: 1px solid var(--border);
border-radius: 4px;
}
.oauth-preview .flow .node.puro { color: var(--cyan); border-color: rgba(34,211,238,0.25); background: rgba(34,211,238,0.06); }
/* actions */
.actions {
display: flex; justify-content: space-between; align-items: center;
margin-top: 26px;
padding-top: 22px;
border-top: 1px dashed var(--border);
}
.actions .left {
font-size: 12px; color: var(--text-3);
display: flex; align-items: center; gap: 8px;
}
.actions .right { display: flex; gap: 10px; }
/* bound summary panel (step 3 preview) */
.bound-list {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
margin-top: 14px;
}
.bound-item {
display: flex; align-items: center; gap: 10px;
padding: 10px 12px;
background: rgba(2, 6, 23, 0.4);
border: 1px solid var(--border);
border-radius: var(--r-sm);
font-size: 12px;
}
.bound-item .tick {
width: 16px; height: 16px; border-radius: 50%;
background: var(--green);
color: #042f2e;
display: inline-flex; align-items: center; justify-content: center;
font-size: 10px; font-weight: 700;
}
.bound-item .name { flex: 1; color: var(--text-0); }
.bound-item .tag { color: var(--text-3); font-family: var(--font-mono); font-size: 11px; }
/* topbar user menu (minimal) */
.topbar-user {
margin-left: auto;
display: flex; align-items: center; gap: 10px;
}
.user-menu {
display: flex; align-items: center; gap: 8px;
padding: 4px 10px 4px 4px;
border-radius: 100px;
background: rgba(255,255,255,0.03);
border: 1px solid var(--border);
cursor: pointer;
}
.user-menu .name { font-size: 13px; font-weight: 500; }
</style>
</head>
<body>
<div class="bg-glow soft"></div>
<div class="app-shell">
<aside class="app-side">
<a class="brand" href="Landing.html">
<svg class="hex" viewBox="0 0 24 24" fill="none">
<path d="M12 2L21 7V17L12 22L3 17V7L12 2Z" stroke="currentColor" stroke-width="1.8" fill="rgba(34, 211, 238, 0.08)"/>
<path d="M12 7L17 9.5V14.5L12 17L7 14.5V9.5L12 7Z" fill="currentColor"/>
</svg>
PURO
</a>
<div class="side-group">
<div class="side-label">Workspace</div>
<a class="side-item" href="Dashboard.html"><span class="ico"></span>Dashboard</a>
<a class="side-item" href="API Keys.html"><span class="ico"></span>API Keys<span class="count">3</span></a>
<a class="side-item active" href="#"><span class="ico"></span>订阅账号<span class="count">7</span></a>
<a class="side-item" href="#"><span class="ico"></span>调用日志</a>
<a class="side-item" href="#"><span class="ico">$</span>账单 & 充值</a>
</div>
<div class="side-group">
<div class="side-label">Resources</div>
<a class="side-item" href="Docs.html"><span class="ico">📖</span>文档</a>
<a class="side-item" href="Design System.html"><span class="ico"></span>Design System</a>
</div>
</aside>
<div class="app-main">
<header class="app-topbar">
<a href="Dashboard.html" style="color:var(--text-3); display:inline-flex; align-items:center; gap:6px; font-size:13px;">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M19 12H5"/><path d="M12 19l-7-7 7-7"/></svg>
返回 Dashboard
</a>
<div class="topbar-user">
<div class="user-menu"><span class="avatar">Z</span><span class="name">zane</span></div>
</div>
</header>
<div class="app-content">
<div class="wrap">
<div class="head">
<div class="section-kicker">// 绑定订阅 · 3 步完成</div>
<h1>把你已有的 AI 订阅,变成 API</h1>
<p>我们支持 OAuth 授权和 Cookie 托管两种方式接入 Claude / ChatGPT / Gemini。所有凭证使用 AES-256 加密存储,你可以随时一键解绑。</p>
</div>
<!-- stepper -->
<div class="steps">
<div class="step done">
<span class="num"></span>
<span class="label">选择平台</span>
</div>
<div class="step-line done"></div>
<div class="step active">
<span class="num">2</span>
<span class="label">授权绑定</span>
</div>
<div class="step-line"></div>
<div class="step">
<span class="num">3</span>
<span class="label">完成 & 加入池</span>
</div>
</div>
<!-- platform picker -->
<div class="platform-grid">
<div class="platform-card">
<div class="platform-logo claude">
<svg width="22" height="22" viewBox="0 0 24 24" fill="#d97757"><path d="M4.5 19L12 4l7.5 15H16l-4-8.5L8 19H4.5z"/></svg>
</div>
<div class="platform-name">Claude</div>
<div class="platform-tier">Anthropic · OAuth</div>
<div class="platform-tiers">
<span class="pill">Pro · $20</span>
<span class="pill">Max · $100</span>
<span class="pill">Team</span>
</div>
</div>
<div class="platform-card selected">
<div class="platform-logo gpt">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="#10a37f" stroke-width="1.8"><circle cx="12" cy="12" r="9"/><path d="M12 3v18M3 12h18"/></svg>
</div>
<div class="platform-name">ChatGPT</div>
<div class="platform-tier">OpenAI · OAuth + Cookie</div>
<div class="platform-tiers">
<span class="pill">Plus · $20</span>
<span class="pill">Pro · $200</span>
<span class="pill">Team</span>
</div>
</div>
<div class="platform-card">
<div class="platform-logo gemini">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="#4285f4" stroke-width="1.8"><path d="M12 2 L15 9 L22 10 L17 15 L19 22 L12 18 L5 22 L7 15 L2 10 L9 9 Z"/></svg>
</div>
<div class="platform-name">Gemini</div>
<div class="platform-tier">Google · Cookie</div>
<div class="platform-tiers">
<span class="pill">Advanced · $20</span>
<span class="pill">Workspace</span>
</div>
</div>
</div>
<!-- detail panel for selected platform (ChatGPT) -->
<div class="detail-panel">
<div class="detail-head">
<div class="platform-logo gpt">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="#10a37f" stroke-width="1.8"><circle cx="12" cy="12" r="9"/><path d="M12 3v18M3 12h18"/></svg>
</div>
<div>
<h2>绑定 ChatGPT 账号</h2>
<div class="sub">支持 Plus / Pro / Team · 接入后可用 gpt-5 / gpt-5-codex / gpt-4.1</div>
</div>
<span class="secure-tag">
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="11" x="3" y="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
AES-256 加密存储
</span>
</div>
<div class="detail-body">
<!-- tier picker -->
<div class="ds-label" style="font-family:var(--font-mono); font-size:11px; color:var(--text-3); text-transform:uppercase; letter-spacing:0.14em; margin-bottom:10px;">
订阅档位
</div>
<div class="tier-picker">
<div class="tier-opt">
<div class="t-name">Plus</div>
<div class="t-price">$20<span class="unit">/月</span></div>
<div class="t-desc">~500k tokens · 约值 $0.08/k</div>
</div>
<div class="tier-opt selected">
<div class="t-name">Pro</div>
<div class="t-price">$200<span class="unit">/月</span></div>
<div class="t-desc">~5M tokens · 约值 $0.04/k</div>
</div>
<div class="tier-opt">
<div class="t-name">Team</div>
<div class="t-price">$30<span class="unit">/user</span></div>
<div class="t-desc">按席位池化,稳定性更高</div>
</div>
</div>
<!-- methods -->
<div class="method-section">
<h3>绑定方式 <span class="hint">选择一种即可 · 可以在绑定后随时更换</span></h3>
<div class="method-grid">
<div class="method-card selected">
<div class="method-title">
OAuth 授权登录
<span class="tag">推荐</span>
</div>
<div class="method-desc">
跳转到 ChatGPT 登录页,登录后自动回跳。不经过我们的密码表单,最接近"官方授权"体验。
</div>
<div class="method-steps">
<span>点击跳转</span>
<span>ChatGPT 登录</span>
<span>授权回调</span>
<span>加入池</span>
</div>
</div>
<div class="method-card">
<div class="method-title">
粘贴 Session Cookie
<span class="tag beta">兼容模式</span>
</div>
<div class="method-desc">
用浏览器扩展一键导出 <code class="mono" style="color:var(--cyan)">__Secure-next-auth.session-token</code> 并粘贴到这里。适合多账号批量绑定。
</div>
<div class="method-steps">
<span>安装扩展</span>
<span>登录 chatgpt.com</span>
<span>导出 cookie</span>
<span>粘贴绑定</span>
</div>
</div>
</div>
<!-- OAuth flow preview -->
<div class="oauth-preview">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="#22d3ee" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" style="flex-shrink:0">
<rect width="18" height="11" x="3" y="11" rx="2"/>
<path d="M7 11V7a5 5 0 0 1 10 0v4"/>
</svg>
<div class="flow">
<span class="node"></span>
<span class="arrow"></span>
<span class="node puro">PURO</span>
<span class="arrow"></span>
<span class="node">chatgpt.com/oauth</span>
<span class="arrow"></span>
<span class="node puro">PURO</span>
</div>
<div class="mono text-xs text-3">约 15 秒</div>
</div>
</div>
<!-- bound preview (done state, hidden unless step 3) -->
<div class="method-section" style="margin-top:22px">
<h3>本次绑定预览 <span class="hint">授权成功后会自动加入池</span></h3>
<div class="bound-list">
<div class="bound-item">
<span class="tick"></span>
<span class="name">gpt-plus-7</span>
<span class="tag">加入 GPT 池</span>
</div>
<div class="bound-item">
<span class="tick"></span>
<span class="name">gpt-plus-8</span>
<span class="tag">加入 GPT 池</span>
</div>
</div>
</div>
<!-- actions -->
<div class="actions">
<div class="left">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 8v4M12 16h.01"/></svg>
凭证仅用于代理请求,不会用于训练或泄露给第三方。
</div>
<div class="right">
<a class="btn btn-ghost" href="Dashboard.html">稍后再说</a>
<button class="btn btn-primary btn-lg">
使用 OAuth 绑定
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M7 17L17 7M7 7h10v10"/></svg>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,770 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="color-scheme" content="dark">
<title>Dashboard — PURO AI</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<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">
<link rel="stylesheet" href="puro.css">
<style>
/* ---- page-local ---- */
.page-head {
display: flex;
align-items: flex-end;
justify-content: space-between;
margin-bottom: 28px;
gap: 24px;
}
.page-head h1 {
font-size: 28px;
font-weight: 700;
letter-spacing: -0.02em;
margin-bottom: 6px;
}
.page-head .sub { color: var(--text-2); font-size: 14px; }
/* ---- stat row ---- */
.stat-row {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 14px;
margin-bottom: 20px;
}
.stat {
padding: 18px;
border: 1px solid var(--border);
border-radius: var(--r-lg);
background: rgba(15, 23, 42, 0.6);
position: relative;
overflow: hidden;
}
.stat-label {
font-size: 11px;
color: var(--text-3);
text-transform: uppercase;
letter-spacing: 0.1em;
margin-bottom: 10px;
font-family: var(--font-mono);
}
.stat-value {
font-size: 26px;
font-weight: 700;
letter-spacing: -0.02em;
font-family: var(--font-mono);
font-variant-numeric: tabular-nums;
}
.stat-value .unit { font-size: 14px; color: var(--text-3); margin-left: 4px; font-weight: 500; }
.stat-delta {
font-size: 11px;
color: var(--green);
margin-top: 6px;
font-family: var(--font-mono);
display: flex; align-items: center; gap: 4px;
}
.stat-delta.down { color: var(--red); }
.stat-delta.neutral { color: var(--text-3); }
.stat-spark {
position: absolute;
bottom: 0; right: 0;
width: 100px; height: 40px;
opacity: 0.7;
}
.stat.credit {
background:
radial-gradient(400px 200px at 100% 0%, rgba(34,211,238,0.1), transparent 60%),
rgba(15, 23, 42, 0.6);
border-color: rgba(34, 211, 238, 0.2);
}
.stat.credit .stat-value { color: var(--cyan); }
.stat.credit .topup {
position: absolute; top: 14px; right: 14px;
font-size: 11px;
color: var(--cyan);
font-family: var(--font-mono);
padding: 3px 8px;
border: 1px solid rgba(34, 211, 238, 0.25);
border-radius: 4px;
background: rgba(34,211,238,0.05);
transition: all .15s;
}
.stat.credit .topup:hover { background: rgba(34,211,238,0.15); }
/* ---- chart + donut grid ---- */
.chart-grid {
display: grid;
grid-template-columns: 2fr 1fr;
gap: 14px;
margin-bottom: 20px;
}
.chart-card {
border: 1px solid var(--border);
border-radius: var(--r-lg);
background: rgba(15, 23, 42, 0.6);
padding: 18px;
}
.chart-head {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16px;
gap: 12px;
}
.chart-head .title {
font-size: 13px;
font-weight: 600;
}
.chart-head .legend {
display: flex; gap: 14px; font-size: 11px; color: var(--text-3); font-family: var(--font-mono);
}
.chart-head .legend span { display: inline-flex; align-items: center; gap: 5px; }
.chart-head .legend .sw { width: 8px; height: 8px; border-radius: 2px; }
.range-switch {
display: inline-flex;
background: rgba(2, 6, 23, 0.5);
border: 1px solid var(--border);
border-radius: 6px;
padding: 2px;
font-family: var(--font-mono);
font-size: 11px;
}
.range-switch button {
padding: 4px 10px;
color: var(--text-3);
border-radius: 4px;
transition: all .12s;
}
.range-switch button.active { background: rgba(34,211,238,0.12); color: var(--cyan); }
.range-switch button:hover:not(.active) { color: var(--text-1); }
/* ---- accounts + log ---- */
.two-col-grid {
display: grid;
grid-template-columns: 1fr 1.4fr;
gap: 14px;
margin-bottom: 20px;
}
.panel {
border: 1px solid var(--border);
border-radius: var(--r-lg);
background: rgba(15, 23, 42, 0.6);
overflow: hidden;
}
.panel-head {
padding: 14px 18px;
border-bottom: 1px solid var(--border);
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.panel-head h3 { font-size: 13px; font-weight: 600; }
.panel-head .meta {
font-family: var(--font-mono);
font-size: 11px;
color: var(--text-3);
}
.panel-body { padding: 8px; }
/* account list row */
.acct-row {
display: grid;
grid-template-columns: 24px 1fr auto auto;
align-items: center;
gap: 12px;
padding: 10px 12px;
border-radius: var(--r-sm);
transition: all .12s;
}
.acct-row:hover { background: rgba(255,255,255,0.02); }
.acct-logo {
width: 22px; height: 22px;
border-radius: 4px;
background: rgba(255,255,255,0.04);
border: 1px solid var(--border);
display: flex; align-items: center; justify-content: center;
}
.acct-name {
font-size: 13px;
color: var(--text-0);
font-weight: 500;
}
.acct-name .tag {
display: inline-block;
font-family: var(--font-mono);
font-size: 10px;
color: var(--text-3);
margin-left: 6px;
}
.acct-meter {
width: 80px;
height: 4px;
background: var(--border);
border-radius: 2px;
overflow: hidden;
}
.acct-meter span {
display: block;
height: 100%;
background: var(--green);
border-radius: 2px;
}
.acct-meter.warn span { background: var(--amber); }
.acct-meter.danger span { background: var(--red); }
.acct-status {
font-size: 11px;
font-family: var(--font-mono);
color: var(--green);
display: inline-flex;
align-items: center;
gap: 5px;
min-width: 72px;
justify-content: flex-end;
}
.acct-status.warn { color: var(--amber); }
.acct-status.off { color: var(--text-3); }
.add-acct {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
margin: 6px 6px 6px;
padding: 12px;
border: 1px dashed var(--border-2);
border-radius: var(--r-sm);
color: var(--text-2);
font-size: 12px;
cursor: pointer;
transition: all .15s;
}
.add-acct:hover { border-color: var(--cyan); color: var(--cyan); background: rgba(34,211,238,0.04); }
/* log table overrides */
.log-wrap { overflow: hidden; }
.log-wrap table { width: 100%; }
/* endpoint bar */
.endpoint {
display: flex;
align-items: center;
gap: 10px;
padding: 12px 16px;
background: rgba(2, 6, 23, 0.6);
border: 1px solid var(--border);
border-radius: var(--r-md);
font-family: var(--font-mono);
font-size: 13px;
margin-bottom: 20px;
}
.endpoint .method {
padding: 2px 8px;
background: rgba(34,211,238,0.1);
color: var(--cyan);
border-radius: 3px;
font-size: 11px;
font-weight: 600;
letter-spacing: 0.05em;
}
.endpoint .url { color: var(--text-0); flex: 1; }
.endpoint .copy {
padding: 4px 10px;
color: var(--text-3);
font-size: 11px;
border-radius: 4px;
cursor: pointer;
transition: all .12s;
}
.endpoint .copy:hover { color: var(--cyan); background: rgba(34,211,238,0.08); }
/* getting started banner */
.tips-banner {
padding: 14px 18px;
margin-bottom: 20px;
border: 1px solid rgba(251, 191, 36, 0.22);
border-radius: var(--r-md);
background: rgba(251, 191, 36, 0.05);
display: flex;
align-items: center;
gap: 12px;
font-size: 13px;
color: var(--text-1);
}
.tips-banner .spark { color: var(--amber); flex-shrink: 0; display: inline-flex; }
.tips-banner a { color: var(--amber); font-weight: 500; }
.tips-banner .dismiss { margin-left: auto; color: var(--text-3); cursor: pointer; padding: 4px; }
/* topbar custom */
.search {
flex: 1;
max-width: 360px;
position: relative;
}
.search input {
width: 100%;
height: 34px;
padding: 0 14px 0 36px;
background: rgba(2, 6, 23, 0.5);
border: 1px solid var(--border);
border-radius: var(--r-sm);
color: var(--text-1);
font-size: 13px;
font-family: inherit;
outline: none;
transition: all .12s;
}
.search input::placeholder { color: var(--text-3); }
.search input:focus { border-color: var(--border-2); background: rgba(2, 6, 23, 0.8); }
.search .search-ico {
position: absolute; left: 12px; top: 50%;
transform: translateY(-50%);
color: var(--text-3);
pointer-events: none;
}
.search .kbd {
position: absolute; right: 10px; top: 50%;
transform: translateY(-50%);
pointer-events: none;
}
.topbar-right {
margin-left: auto;
display: flex;
align-items: center;
gap: 12px;
}
.icon-btn {
width: 32px; height: 32px;
border-radius: var(--r-sm);
color: var(--text-2);
display: inline-flex;
align-items: center;
justify-content: center;
transition: all .12s;
position: relative;
}
.icon-btn:hover { color: var(--text-0); background: rgba(255,255,255,0.04); }
.icon-btn .pulse {
position: absolute; top: 8px; right: 8px;
width: 6px; height: 6px; border-radius: 50%;
background: var(--amber);
box-shadow: 0 0 0 3px rgba(251,191,36,0.15);
}
.user-menu {
display: flex;
align-items: center;
gap: 8px;
padding: 4px 10px 4px 4px;
border-radius: 100px;
background: rgba(255,255,255,0.03);
border: 1px solid var(--border);
cursor: pointer;
transition: all .12s;
}
.user-menu:hover { background: rgba(255,255,255,0.06); }
.user-menu .name { font-size: 13px; font-weight: 500; }
</style>
</head>
<body>
<div class="bg-glow soft"></div>
<div class="app-shell">
<!-- SIDEBAR -->
<aside class="app-side">
<a class="brand" href="Landing.html">
<svg class="hex" viewBox="0 0 24 24" fill="none">
<path d="M12 2L21 7V17L12 22L3 17V7L12 2Z" stroke="currentColor" stroke-width="1.8" fill="rgba(34, 211, 238, 0.08)"/>
<path d="M12 7L17 9.5V14.5L12 17L7 14.5V9.5L12 7Z" fill="currentColor"/>
</svg>
PURO
</a>
<div class="side-group">
<div class="side-label">Workspace</div>
<a class="side-item active" href="Dashboard.html">
<span class="ico">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="9"/><rect x="14" y="3" width="7" height="5"/><rect x="14" y="12" width="7" height="9"/><rect x="3" y="16" width="7" height="5"/></svg>
</span>
Dashboard
</a>
<a class="side-item" href="API Keys.html">
<span class="ico">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="7.5" cy="15.5" r="3.5"/><path d="m21 2-9.6 9.6"/><path d="m15.5 7.5 3 3L22 7l-3-3"/></svg>
</span>
API Keys
<span class="count">3</span>
</a>
<a class="side-item" href="Accounts.html">
<span class="ico">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M20 7h-9"/><path d="M14 17H5"/><circle cx="17" cy="17" r="3"/><circle cx="7" cy="7" r="3"/></svg>
</span>
订阅账号
<span class="count">7</span>
</a>
<a class="side-item" href="#">
<span class="ico">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><path d="M14 2v6h6"/><path d="M8 13h8"/><path d="M8 17h8"/></svg>
</span>
调用日志
</a>
<a class="side-item" href="#">
<span class="ico">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2v20M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/></svg>
</span>
账单 & 充值
</a>
</div>
<div class="side-group">
<div class="side-label">Resources</div>
<a class="side-item" href="Docs.html">
<span class="ico">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg>
</span>
文档
</a>
<a class="side-item" href="Design System.html">
<span class="ico">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2L21 7V17L12 22L3 17V7L12 2Z"/></svg>
</span>
Design System
</a>
</div>
<div class="side-group" style="margin-top:auto; padding-top:20px; border-top:1px solid var(--border);">
<a class="side-item" href="#">
<span class="ico">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.7 1.7 0 0 0 .3 1.8l.1.1a2 2 0 1 1-2.8 2.8l-.1-.1a1.7 1.7 0 0 0-1.8-.3 1.7 1.7 0 0 0-1 1.5V21a2 2 0 1 1-4 0v-.1a1.7 1.7 0 0 0-1-1.5 1.7 1.7 0 0 0-1.8.3l-.1.1a2 2 0 1 1-2.8-2.8l.1-.1a1.7 1.7 0 0 0 .3-1.8 1.7 1.7 0 0 0-1.5-1H3a2 2 0 1 1 0-4h.1a1.7 1.7 0 0 0 1.5-1 1.7 1.7 0 0 0-.3-1.8l-.1-.1a2 2 0 1 1 2.8-2.8l.1.1a1.7 1.7 0 0 0 1.8.3h0a1.7 1.7 0 0 0 1-1.5V3a2 2 0 1 1 4 0v.1a1.7 1.7 0 0 0 1 1.5 1.7 1.7 0 0 0 1.8-.3l.1-.1a2 2 0 1 1 2.8 2.8l-.1.1a1.7 1.7 0 0 0-.3 1.8v0a1.7 1.7 0 0 0 1.5 1H21a2 2 0 1 1 0 4h-.1a1.7 1.7 0 0 0-1.5 1z"/></svg>
</span>
设置
</a>
</div>
</aside>
<!-- MAIN -->
<div class="app-main">
<header class="app-topbar">
<div class="search">
<span class="search-ico">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/></svg>
</span>
<input placeholder="搜索 key / 账号 / 日志…">
<span class="kbd"><kbd></kbd><kbd>K</kbd></span>
</div>
<div class="topbar-right">
<button class="icon-btn" aria-label="通知">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9"/><path d="M10.3 21a1.94 1.94 0 0 0 3.4 0"/></svg>
<span class="pulse"></span>
</button>
<button class="icon-btn" aria-label="帮助">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M9.1 9a3 3 0 0 1 5.8 1c0 2-3 3-3 3"/><path d="M12 17h.01"/></svg>
</button>
<div class="user-menu">
<span class="avatar">Z</span>
<span class="name">zane</span>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 9l6 6 6-6"/></svg>
</div>
</div>
</header>
<div class="app-content">
<!-- tips -->
<div class="tips-banner">
<span class="spark">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/></svg>
</span>
你还有 <b class="text-0">2 个 Claude Pro</b> 订阅未绑定 —— 绑定后立即享受多账号 failover 和请求加权调度。
<a href="Binding.html">去绑定 →</a>
<span class="dismiss"></span>
</div>
<!-- page head -->
<div class="page-head">
<div>
<h1>Dashboard</h1>
<div class="sub">欢迎回来 <b class="text-0">Zane</b> · workspace <code class="pill">zane-personal</code></div>
</div>
<div style="display:flex; gap:10px;">
<div class="range-switch">
<button>1h</button>
<button>24h</button>
<button class="active">7d</button>
<button>30d</button>
<button>自定义</button>
</div>
<button class="btn btn-ghost">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><path d="M7 10l5 5 5-5"/><path d="M12 15V3"/></svg>
导出
</button>
</div>
</div>
<!-- endpoint -->
<div class="endpoint">
<span class="method">POST</span>
<span class="url">https://ai.puro.im/v1/chat/completions</span>
<span class="copy">复制</span>
<span style="color:var(--text-3); margin: 0 4px;">·</span>
<span class="copy">查看文档</span>
</div>
<!-- stats -->
<div class="stat-row">
<div class="stat credit">
<a class="topup" href="Pricing.html">充值 →</a>
<div class="stat-label">余额</div>
<div class="stat-value">$182.40<span class="unit">USD</span></div>
<div class="stat-delta neutral">按当前节奏约 <b class="text-1">23 天</b></div>
</div>
<div class="stat">
<div class="stat-label">今日请求</div>
<div class="stat-value tabular">12,847</div>
<div class="stat-delta">▲ 18.2% vs 昨日</div>
<svg class="stat-spark" viewBox="0 0 100 40" preserveAspectRatio="none">
<path d="M0,30 L10,28 L20,22 L30,25 L40,18 L50,20 L60,12 L70,15 L80,8 L90,12 L100,5" stroke="#22d3ee" stroke-width="1.5" fill="none"/>
</svg>
</div>
<div class="stat">
<div class="stat-label">消耗 Tokens</div>
<div class="stat-value tabular">4.82<span class="unit">M</span></div>
<div class="stat-delta">▲ 12.5%</div>
<svg class="stat-spark" viewBox="0 0 100 40" preserveAspectRatio="none">
<path d="M0,25 L10,20 L20,22 L30,15 L40,18 L50,10 L60,14 L70,8 L80,12 L90,6 L100,4" stroke="#a855f7" stroke-width="1.5" fill="none"/>
</svg>
</div>
<div class="stat">
<div class="stat-label">平均延迟</div>
<div class="stat-value tabular">214<span class="unit">ms</span></div>
<div class="stat-delta down">▲ 4.1% vs 昨日</div>
<svg class="stat-spark" viewBox="0 0 100 40" preserveAspectRatio="none">
<path d="M0,20 L10,22 L20,18 L30,24 L40,20 L50,22 L60,18 L70,24 L80,22 L90,26 L100,22" stroke="#fbbf24" stroke-width="1.5" fill="none"/>
</svg>
</div>
</div>
<!-- chart + donut -->
<div class="chart-grid">
<div class="chart-card">
<div class="chart-head">
<span class="title">请求趋势</span>
<div class="legend">
<span><span class="sw" style="background:#22d3ee"></span>Claude</span>
<span><span class="sw" style="background:#a855f7"></span>GPT</span>
<span><span class="sw" style="background:#fbbf24"></span>Gemini</span>
</div>
</div>
<svg viewBox="0 0 500 180" style="width:100%; height:180px; display:block;">
<defs>
<linearGradient id="gc" x1="0" x2="0" y1="0" y2="1">
<stop offset="0%" stop-color="#22d3ee" stop-opacity="0.25"/>
<stop offset="100%" stop-color="#22d3ee" stop-opacity="0"/>
</linearGradient>
</defs>
<g stroke="#1e293b" stroke-width="1">
<line x1="0" y1="30" x2="500" y2="30"/>
<line x1="0" y1="75" x2="500" y2="75"/>
<line x1="0" y1="120" x2="500" y2="120"/>
<line x1="0" y1="165" x2="500" y2="165"/>
</g>
<path d="M0,120 L40,100 L80,110 L120,80 L160,95 L200,60 L240,70 L280,45 L320,55 L360,30 L400,50 L440,35 L500,25 L500,180 L0,180 Z" fill="url(#gc)"/>
<path d="M0,120 L40,100 L80,110 L120,80 L160,95 L200,60 L240,70 L280,45 L320,55 L360,30 L400,50 L440,35 L500,25" stroke="#22d3ee" stroke-width="2" fill="none"/>
<path d="M0,140 L40,135 L80,120 L120,130 L160,110 L200,115 L240,90 L280,100 L320,80 L360,90 L400,70 L440,75 L500,55" stroke="#a855f7" stroke-width="2" fill="none"/>
<path d="M0,160 L40,155 L80,150 L120,148 L160,145 L200,140 L240,135 L280,130 L320,125 L360,122 L400,118 L440,112 L500,108" stroke="#fbbf24" stroke-width="2" fill="none"/>
<g font-family="JetBrains Mono" font-size="9" fill="#64748b">
<text x="0" y="178">Mon</text>
<text x="83" y="178">Tue</text>
<text x="166" y="178">Wed</text>
<text x="249" y="178">Thu</text>
<text x="332" y="178">Fri</text>
<text x="415" y="178">Sat</text>
<text x="475" y="178">Sun</text>
</g>
<!-- hover indicator -->
<line x1="360" y1="30" x2="360" y2="165" stroke="#334155" stroke-width="1" stroke-dasharray="2,3"/>
<circle cx="360" cy="30" r="3" fill="#22d3ee"/>
<g transform="translate(370, 18)">
<rect width="92" height="28" fill="#020617" stroke="#334155" rx="4"/>
<text x="8" y="12" font-family="JetBrains Mono" font-size="9" fill="#cbd5e1">Fri · 16:00</text>
<text x="8" y="23" font-family="JetBrains Mono" font-size="10" fill="#22d3ee">Claude 3,824</text>
</g>
</svg>
</div>
<div class="chart-card">
<div class="chart-head">
<span class="title">模型分布</span>
<span class="mono text-xs text-3">7d</span>
</div>
<div style="display:flex; align-items:center; gap:18px;">
<svg viewBox="0 0 42 42" style="width:120px; height:120px; transform:rotate(-90deg);">
<circle cx="21" cy="21" r="15.915" fill="transparent" stroke="#1e293b" stroke-width="6"/>
<circle cx="21" cy="21" r="15.915" fill="transparent" stroke="#22d3ee" stroke-width="6" stroke-dasharray="48 52" stroke-dashoffset="0"/>
<circle cx="21" cy="21" r="15.915" fill="transparent" stroke="#a855f7" stroke-width="6" stroke-dasharray="32 68" stroke-dashoffset="-48"/>
<circle cx="21" cy="21" r="15.915" fill="transparent" stroke="#fbbf24" stroke-width="6" stroke-dasharray="14 86" stroke-dashoffset="-80"/>
<circle cx="21" cy="21" r="15.915" fill="transparent" stroke="#64748b" stroke-width="6" stroke-dasharray="6 94" stroke-dashoffset="-94"/>
</svg>
<div style="flex:1; display:flex; flex-direction:column; gap:8px; font-size:12px; font-family:var(--font-mono);">
<div style="display:flex; justify-content:space-between;"><span style="color:var(--text-1);"><span style="display:inline-block; width:8px; height:8px; background:#22d3ee; border-radius:2px; margin-right:6px;"></span>Claude</span><span style="color:var(--text-3);">48%</span></div>
<div style="display:flex; justify-content:space-between;"><span style="color:var(--text-1);"><span style="display:inline-block; width:8px; height:8px; background:#a855f7; border-radius:2px; margin-right:6px;"></span>GPT</span><span style="color:var(--text-3);">32%</span></div>
<div style="display:flex; justify-content:space-between;"><span style="color:var(--text-1);"><span style="display:inline-block; width:8px; height:8px; background:#fbbf24; border-radius:2px; margin-right:6px;"></span>Gemini</span><span style="color:var(--text-3);">14%</span></div>
<div style="display:flex; justify-content:space-between;"><span style="color:var(--text-1);"><span style="display:inline-block; width:8px; height:8px; background:#64748b; border-radius:2px; margin-right:6px;"></span>Codex</span><span style="color:var(--text-3);">6%</span></div>
</div>
</div>
</div>
</div>
<!-- accounts + logs -->
<div class="two-col-grid">
<!-- accounts -->
<div class="panel">
<div class="panel-head">
<h3>订阅账号池</h3>
<a href="Accounts.html" class="meta" style="color:var(--cyan);">查看全部 →</a>
</div>
<div class="panel-body">
<div class="acct-row">
<span class="acct-logo"><svg width="14" height="14" viewBox="0 0 24 24" fill="#d97757"><path d="M4.5 19L12 4l7.5 15H16l-4-8.5L8 19H4.5z"/></svg></span>
<div class="acct-name">claude-pool-01<span class="tag">Pro · OAuth</span></div>
<div class="acct-meter"><span style="width:68%"></span></div>
<span class="acct-status"><span class="status-chip" style="width:5px;height:5px;"></span>healthy</span>
</div>
<div class="acct-row">
<span class="acct-logo"><svg width="14" height="14" viewBox="0 0 24 24" fill="#d97757"><path d="M4.5 19L12 4l7.5 15H16l-4-8.5L8 19H4.5z"/></svg></span>
<div class="acct-name">claude-pool-02<span class="tag">Max · OAuth</span></div>
<div class="acct-meter"><span style="width:42%"></span></div>
<span class="acct-status"><span class="status-chip" style="width:5px;height:5px;"></span>healthy</span>
</div>
<div class="acct-row">
<span class="acct-logo"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#10a37f" stroke-width="1.6"><circle cx="12" cy="12" r="8"/><path d="M12 4v16M4 12h16"/></svg></span>
<div class="acct-name">gpt-plus-7<span class="tag">Plus · OAuth</span></div>
<div class="acct-meter warn"><span style="width:88%"></span></div>
<span class="acct-status warn"><span class="status-chip amber" style="width:5px;height:5px;"></span>near limit</span>
</div>
<div class="acct-row">
<span class="acct-logo"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#10a37f" stroke-width="1.6"><circle cx="12" cy="12" r="8"/><path d="M12 4v16M4 12h16"/></svg></span>
<div class="acct-name">gpt-plus-8<span class="tag">Pro · OAuth</span></div>
<div class="acct-meter"><span style="width:35%"></span></div>
<span class="acct-status"><span class="status-chip" style="width:5px;height:5px;"></span>healthy</span>
</div>
<div class="acct-row">
<span class="acct-logo"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#4285f4" stroke-width="1.8"><path d="M12 2 L15 9 L22 10 L17 15 L19 22 L12 18 L5 22 L7 15 L2 10 L9 9 Z"/></svg></span>
<div class="acct-name">gemini-adv-01<span class="tag">Advanced</span></div>
<div class="acct-meter"><span style="width:12%"></span></div>
<span class="acct-status"><span class="status-chip" style="width:5px;height:5px;"></span>healthy</span>
</div>
<div class="acct-row">
<span class="acct-logo" style="opacity:0.5"><svg width="14" height="14" viewBox="0 0 24 24" fill="#d97757"><path d="M4.5 19L12 4l7.5 15H16l-4-8.5L8 19H4.5z"/></svg></span>
<div class="acct-name" style="opacity:0.7">claude-pool-03<span class="tag">Pro · expired</span></div>
<div class="acct-meter"><span style="width:0%"></span></div>
<span class="acct-status off">● offline</span>
</div>
<a class="add-acct" href="Binding.html">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M12 5v14M5 12h14"/></svg>
绑定新订阅
</a>
</div>
</div>
<!-- logs -->
<div class="panel log-wrap">
<div class="panel-head">
<h3>最近请求</h3>
<span class="meta">live · 12 of 18,294</span>
</div>
<table class="tbl">
<thead>
<tr>
<th class="mono">TIME</th>
<th>ACCOUNT</th>
<th class="mono">MODEL</th>
<th class="mono">TOKENS</th>
<th class="mono">COST</th>
<th class="mono">STATUS</th>
</tr>
</thead>
<tbody>
<tr>
<td class="mono">13:42:18</td>
<td><span class="provider claude"><span class="dot"></span>claude-pool-01</span></td>
<td class="mono">sonnet-4-5</td>
<td class="mono tabular">2,847</td>
<td class="mono tabular">$0.042</td>
<td class="mono text-green">200 · 213ms</td>
</tr>
<tr>
<td class="mono">13:42:11</td>
<td><span class="provider gpt"><span class="dot"></span>gpt-plus-7</span></td>
<td class="mono">gpt-5-codex</td>
<td class="mono tabular">1,204</td>
<td class="mono tabular">$0.018</td>
<td class="mono text-green">200 · 167ms</td>
</tr>
<tr>
<td class="mono">13:42:03</td>
<td><span class="provider gemini"><span class="dot"></span>gemini-adv-01</span></td>
<td class="mono">gemini-2.5-pro</td>
<td class="mono tabular">4,102</td>
<td class="mono tabular">$0.000</td>
<td class="mono text-green">200 · 392ms</td>
</tr>
<tr>
<td class="mono">13:41:58</td>
<td><span class="provider claude"><span class="dot"></span>claude-pool-02</span></td>
<td class="mono">sonnet-4-5</td>
<td class="mono tabular">6,318</td>
<td class="mono tabular">$0.095</td>
<td class="mono text-green">200 · 288ms</td>
</tr>
<tr>
<td class="mono">13:41:49</td>
<td><span class="provider gpt"><span class="dot"></span>gpt-plus-7</span></td>
<td class="mono">gpt-5</td>
<td class="mono tabular">892</td>
<td class="mono tabular">$0.013</td>
<td class="mono text-amber">429 · retry</td>
</tr>
<tr>
<td class="mono">13:41:42</td>
<td><span class="provider gpt"><span class="dot"></span>gpt-plus-8</span></td>
<td class="mono">gpt-5</td>
<td class="mono tabular">892</td>
<td class="mono tabular">$0.013</td>
<td class="mono text-green">200 · 198ms</td>
</tr>
<tr>
<td class="mono">13:41:35</td>
<td><span class="provider claude"><span class="dot"></span>claude-pool-01</span></td>
<td class="mono">haiku-4-5</td>
<td class="mono tabular">512</td>
<td class="mono tabular">$0.004</td>
<td class="mono text-green">200 · 98ms</td>
</tr>
<tr>
<td class="mono">13:41:28</td>
<td><span class="provider gemini"><span class="dot"></span>gemini-adv-01</span></td>
<td class="mono">gemini-2.5-flash</td>
<td class="mono tabular">1,824</td>
<td class="mono tabular">$0.000</td>
<td class="mono text-green">200 · 156ms</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,670 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="color-scheme" content="dark">
<title>Design System — PURO AI</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<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">
<link rel="stylesheet" href="puro.css">
<style>
/* page-local helpers */
.ds-header {
padding: 72px 0 48px;
border-bottom: 1px solid var(--border);
margin-bottom: 56px;
}
.ds-header h1 { font-size: 48px; font-weight: 800; letter-spacing: -0.03em; margin-bottom: 12px; }
.ds-header p { color: var(--text-2); max-width: 600px; line-height: 1.6; }
.ds-header .meta {
display: flex; gap: 18px; margin-top: 24px;
font-family: var(--font-mono); font-size: 12px; color: var(--text-3);
}
.ds-section { padding: 40px 0; border-bottom: 1px dashed var(--border); }
.ds-section:last-child { border-bottom: none; }
.ds-section > h2 {
font-size: 24px;
font-weight: 700;
margin-bottom: 6px;
letter-spacing: -0.01em;
}
.ds-section > h2 .mono-num {
color: var(--cyan);
font-family: var(--font-mono);
font-size: 14px;
margin-right: 10px;
font-weight: 500;
}
.ds-section > .desc {
color: var(--text-2);
font-size: 14px;
margin-bottom: 28px;
max-width: 600px;
}
.ds-label {
font-family: var(--font-mono);
font-size: 11px;
color: var(--text-3);
text-transform: uppercase;
letter-spacing: 0.14em;
margin-bottom: 14px;
}
/* color swatches */
.swatch-grid {
display: grid;
grid-template-columns: repeat(6, 1fr);
gap: 10px;
}
.sw {
border: 1px solid var(--border);
border-radius: var(--r-md);
padding: 14px;
background: rgba(15, 23, 42, 0.4);
}
.sw .chip {
width: 100%;
height: 52px;
border-radius: var(--r-sm);
border: 1px solid rgba(255,255,255,0.04);
margin-bottom: 10px;
padding: 0; background: transparent;
}
.sw .name { font-size: 12px; font-weight: 600; color: var(--text-0); }
.sw .hex { font-family: var(--font-mono); font-size: 11px; color: var(--text-3); margin-top: 2px; }
.sw .use { font-size: 11px; color: var(--text-3); margin-top: 4px; }
/* type scale */
.type-row {
display: grid;
grid-template-columns: 180px 1fr;
gap: 16px;
padding: 16px 0;
border-bottom: 1px solid var(--border);
align-items: baseline;
}
.type-row:last-child { border-bottom: none; }
.type-meta { font-family: var(--font-mono); font-size: 11px; color: var(--text-3); }
.type-meta b { color: var(--text-1); font-weight: 500; }
/* component examples */
.example {
background: rgba(2, 6, 23, 0.35);
border: 1px solid var(--border);
border-radius: var(--r-lg);
padding: 28px;
margin-bottom: 12px;
}
.example.dense { padding: 20px; }
.example-row {
display: flex;
flex-wrap: wrap;
gap: 12px;
align-items: center;
}
/* spacing scale */
.space-row {
display: grid;
grid-template-columns: 90px 1fr 100px;
gap: 16px;
align-items: center;
padding: 10px 0;
font-family: var(--font-mono);
font-size: 12px;
color: var(--text-2);
}
.space-row .bar { height: 10px; background: var(--cyan); border-radius: 2px; opacity: 0.7; }
.space-row .note { color: var(--text-3); text-align: right; }
/* radius scale */
.radius-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 12px;
}
.radius-card {
padding: 24px;
background: rgba(15, 23, 42, 0.5);
border: 1px solid var(--border);
text-align: center;
font-family: var(--font-mono);
font-size: 12px;
color: var(--text-2);
}
.radius-card .token { font-size: 11px; color: var(--text-3); margin-top: 4px; }
.two-col { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
.three-col { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 12px; }
/* logo lockup block */
.logo-showcase {
padding: 56px 32px;
background:
radial-gradient(600px 300px at 20% 0%, rgba(34,211,238,0.12), transparent 60%),
radial-gradient(600px 300px at 80% 100%, rgba(168,85,247,0.12), transparent 60%),
var(--bg-1);
border: 1px solid var(--border);
border-radius: var(--r-xl);
display: flex;
align-items: center;
justify-content: center;
gap: 14px;
}
.logo-showcase svg { width: 56px; height: 56px; color: var(--cyan); }
.logo-showcase .word {
font-size: 38px;
font-weight: 800;
letter-spacing: -0.02em;
}
.logo-showcase .word span { color: var(--cyan); }
/* TOC */
.toc {
position: sticky;
top: 24px;
align-self: flex-start;
padding: 20px 0;
}
.toc .toc-label {
font-family: var(--font-mono);
font-size: 10px;
color: var(--text-3);
letter-spacing: 0.14em;
text-transform: uppercase;
margin-bottom: 12px;
}
.toc ol { list-style: none; counter-reset: toc; display: flex; flex-direction: column; gap: 2px; }
.toc li { counter-increment: toc; }
.toc a {
display: block;
padding: 6px 10px;
border-radius: var(--r-sm);
color: var(--text-2);
font-size: 13px;
transition: all .12s;
}
.toc a::before {
content: counter(toc, decimal-leading-zero);
font-family: var(--font-mono);
font-size: 10px;
color: var(--text-3);
margin-right: 10px;
}
.toc a:hover { color: var(--text-0); background: rgba(255,255,255,0.02); }
.with-toc {
display: grid;
grid-template-columns: 200px 1fr;
gap: 48px;
align-items: start;
}
</style>
</head>
<body>
<div class="bg-glow soft"></div>
<div class="grain"></div>
<nav class="nav">
<div class="container nav-inner">
<a class="brand" href="Landing.html">
<svg class="hex" viewBox="0 0 24 24" fill="none">
<path d="M12 2L21 7V17L12 22L3 17V7L12 2Z" stroke="currentColor" stroke-width="1.8" fill="rgba(34, 211, 238, 0.08)"/>
<path d="M12 7L17 9.5V14.5L12 17L7 14.5V9.5L12 7Z" fill="currentColor"/>
</svg>
PURO AI
</a>
<div class="nav-links">
<a href="Landing.html">Landing</a>
<a href="#" class="active">Design System</a>
<a href="Dashboard.html">Dashboard</a>
<a href="Docs.html">文档</a>
</div>
<div class="nav-cta">
<a href="Login.html" class="btn btn-ghost">登录</a>
<a href="Register.html" class="btn btn-primary">注册</a>
</div>
</div>
</nav>
<div class="container">
<!-- HEADER -->
<header class="ds-header">
<div class="section-kicker">// design system · v1.0</div>
<h1>PURO AI Design System</h1>
<p>一套用来构建 PURO AI 所有界面的原子 token 和组件。产品的视觉语言围绕「开发者工具 · 深色为主 · 青色作为行动色 · JetBrains Mono 强调技术感」展开。</p>
<div class="meta">
<span>tokens <b class="text-1">·</b> 29</span>
<span>components <b class="text-1">·</b> 22</span>
<span>last updated · 2026.04.19</span>
</div>
</header>
<div class="with-toc">
<aside class="toc">
<div class="toc-label">Contents</div>
<ol>
<li><a href="#brand">Brand</a></li>
<li><a href="#colors">Colors</a></li>
<li><a href="#type">Typography</a></li>
<li><a href="#spacing">Spacing</a></li>
<li><a href="#radius">Radius</a></li>
<li><a href="#buttons">Buttons</a></li>
<li><a href="#badges">Badges</a></li>
<li><a href="#chips">Chips</a></li>
<li><a href="#forms">Forms</a></li>
<li><a href="#cards">Cards</a></li>
<li><a href="#tables">Tables</a></li>
<li><a href="#code">Code</a></li>
<li><a href="#nav">Nav</a></li>
</ol>
</aside>
<main>
<!-- BRAND -->
<section class="ds-section" id="brand">
<h2><span class="mono-num">01</span>Brand Lockup</h2>
<p class="desc">六边形 + 内部实心菱形 — 代表"订阅被聚合成一个 key"。单色在小尺寸下使用,大尺寸下保留内描边增加分量。</p>
<div class="logo-showcase">
<svg viewBox="0 0 24 24" fill="none">
<path d="M12 2L21 7V17L12 22L3 17V7L12 2Z" stroke="currentColor" stroke-width="1.8" fill="rgba(34, 211, 238, 0.08)"/>
<path d="M12 7L17 9.5V14.5L12 17L7 14.5V9.5L12 7Z" fill="currentColor"/>
</svg>
<div class="word">PURO<span>.</span></div>
</div>
</section>
<!-- COLORS -->
<section class="ds-section" id="colors">
<h2><span class="mono-num">02</span>Colors</h2>
<p class="desc">所有颜色都以 CSS variables 定义在 <code class="pill">puro.css</code>。青色 (cyan) 是唯一的品牌色,其他颜色仅承担语义职责(success / warn / danger)。</p>
<div class="ds-label">Surfaces</div>
<div class="swatch-grid">
<div class="sw"><div class="chip" style="background:#0a0e1a"></div><div class="name">bg-0</div><div class="hex">#0a0e1a</div><div class="use">page</div></div>
<div class="sw"><div class="chip" style="background:#0f172a"></div><div class="name">bg-1</div><div class="hex">#0f172a</div><div class="use">raised</div></div>
<div class="sw"><div class="chip" style="background:#111827"></div><div class="name">bg-2</div><div class="hex">#111827</div><div class="use">card alt</div></div>
<div class="sw"><div class="chip" style="background:#020617"></div><div class="name">bg-code</div><div class="hex">#020617</div><div class="use">code</div></div>
<div class="sw"><div class="chip" style="background:#1e293b"></div><div class="name">border</div><div class="hex">#1e293b</div><div class="use">hairline</div></div>
<div class="sw"><div class="chip" style="background:#334155"></div><div class="name">border-2</div><div class="hex">#334155</div><div class="use">strong</div></div>
</div>
<div class="ds-label" style="margin-top:32px">Text</div>
<div class="swatch-grid">
<div class="sw"><div class="chip" style="background:#f8fafc"></div><div class="name">text-0</div><div class="hex">#f8fafc</div><div class="use">primary</div></div>
<div class="sw"><div class="chip" style="background:#cbd5e1"></div><div class="name">text-1</div><div class="hex">#cbd5e1</div><div class="use">body</div></div>
<div class="sw"><div class="chip" style="background:#94a3b8"></div><div class="name">text-2</div><div class="hex">#94a3b8</div><div class="use">muted</div></div>
<div class="sw"><div class="chip" style="background:#64748b"></div><div class="name">text-3</div><div class="hex">#64748b</div><div class="use">hint</div></div>
</div>
<div class="ds-label" style="margin-top:32px">Accents</div>
<div class="swatch-grid">
<div class="sw"><div class="chip" style="background:#22d3ee"></div><div class="name">cyan</div><div class="hex">#22d3ee</div><div class="use">primary / cta</div></div>
<div class="sw"><div class="chip" style="background:#a855f7"></div><div class="name">purple</div><div class="hex">#a855f7</div><div class="use">secondary glow</div></div>
<div class="sw"><div class="chip" style="background:#fbbf24"></div><div class="name">amber</div><div class="hex">#fbbf24</div><div class="use">warn / featured</div></div>
<div class="sw"><div class="chip" style="background:#34d399"></div><div class="name">green</div><div class="hex">#34d399</div><div class="use">success / 200</div></div>
<div class="sw"><div class="chip" style="background:#f87171"></div><div class="name">red</div><div class="hex">#f87171</div><div class="use">error / 5xx</div></div>
<div class="sw"><div class="chip" style="background:#fb923c"></div><div class="name">orange</div><div class="hex">#fb923c</div><div class="use">flag / highlight</div></div>
</div>
<div class="ds-label" style="margin-top:32px">Provider Brand Dots</div>
<div class="swatch-grid">
<div class="sw"><div class="chip" style="background:#d97757"></div><div class="name">claude</div><div class="hex">#d97757</div><div class="use">Anthropic</div></div>
<div class="sw"><div class="chip" style="background:#10a37f"></div><div class="name">gpt</div><div class="hex">#10a37f</div><div class="use">OpenAI</div></div>
<div class="sw"><div class="chip" style="background:#4285f4"></div><div class="name">gemini</div><div class="hex">#4285f4</div><div class="use">Google</div></div>
<div class="sw"><div class="chip" style="background:#f0a030"></div><div class="name">codex</div><div class="hex">#f0a030</div><div class="use">Codex</div></div>
</div>
</section>
<!-- TYPOGRAPHY -->
<section class="ds-section" id="type">
<h2><span class="mono-num">03</span>Typography</h2>
<p class="desc">主字体 <b>Inter</b> · 等宽 <b>JetBrains Mono</b>。等宽仅用于代码、数据、时间戳、状态徽标,以强化开发者语境。</p>
<div class="type-row">
<div class="type-meta">display · 56/64 · 800</div>
<div style="font-size:56px; font-weight:800; letter-spacing:-0.03em; line-height:1.05;">你的 AI 订阅</div>
</div>
<div class="type-row">
<div class="type-meta">h1 · 40/48 · 700</div>
<div style="font-size:40px; font-weight:700; letter-spacing:-0.02em;">统一接入 API</div>
</div>
<div class="type-row">
<div class="type-meta">h2 · 28/36 · 700</div>
<div style="font-size:28px; font-weight:700; letter-spacing:-0.02em;">付一次,用一池</div>
</div>
<div class="type-row">
<div class="type-meta">h3 · 18/26 · 600</div>
<div style="font-size:18px; font-weight:600;">多账号自动调度</div>
</div>
<div class="type-row">
<div class="type-meta">body · 14/22 · 400</div>
<div style="font-size:14px;">OAuth 绑定账号,零改动切换 base_url,沿用你习惯的 SDK。</div>
</div>
<div class="type-row">
<div class="type-meta">caption · 12/18 · 400</div>
<div style="font-size:12px; color:var(--text-2);">某个 ChatGPT Plus 触发限流会自动 failover。</div>
</div>
<div class="type-row">
<div class="type-meta">mono · 13 · 500</div>
<div class="mono" style="font-size:13px; color:var(--cyan);">curl https://ai.puro.im/v1/chat/completions</div>
</div>
<div class="type-row">
<div class="type-meta">kicker · mono · 12 · caps</div>
<div class="section-kicker" style="margin:0">// section kicker</div>
</div>
</section>
<!-- SPACING -->
<section class="ds-section" id="spacing">
<h2><span class="mono-num">04</span>Spacing Scale</h2>
<p class="desc">4px 基线的 8 / 12 / 16 / 20 / 24 / 32 / 48 / 64 scale。页面垂直节奏用 32/48/64/96,卡片内部用 16/20/24。</p>
<div>
<div class="space-row"><span>4px</span><div class="bar" style="width:4px"></div><span class="note">gap-xs · pill 间隔</span></div>
<div class="space-row"><span>8px</span><div class="bar" style="width:8px"></div><span class="note">gap-sm · icon 内外距</span></div>
<div class="space-row"><span>12px</span><div class="bar" style="width:12px"></div><span class="note">gap · 卡片网格</span></div>
<div class="space-row"><span>16px</span><div class="bar" style="width:16px"></div><span class="note">stack-sm · 主要网格</span></div>
<div class="space-row"><span>20px</span><div class="bar" style="width:20px"></div><span class="note">form field</span></div>
<div class="space-row"><span>24px</span><div class="bar" style="width:24px"></div><span class="note">card padding</span></div>
<div class="space-row"><span>32px</span><div class="bar" style="width:32px"></div><span class="note">content padding</span></div>
<div class="space-row"><span>48px</span><div class="bar" style="width:48px"></div><span class="note">section break</span></div>
<div class="space-row"><span>64px</span><div class="bar" style="width:64px"></div><span class="note">section head gap</span></div>
<div class="space-row"><span>96px</span><div class="bar" style="width:96px"></div><span class="note">landing section</span></div>
</div>
</section>
<!-- RADIUS -->
<section class="ds-section" id="radius">
<h2><span class="mono-num">05</span>Radius & Shadow</h2>
<div class="radius-grid">
<div class="radius-card" style="border-radius:6px">6px<div class="token">--r-sm</div></div>
<div class="radius-card" style="border-radius:8px">8px<div class="token">--r-md (button, input)</div></div>
<div class="radius-card" style="border-radius:12px">12px<div class="token">--r-lg (card)</div></div>
<div class="radius-card" style="border-radius:16px">16px<div class="token">--r-xl (hero card)</div></div>
</div>
<div class="ds-label" style="margin-top:28px">Elevation</div>
<div class="two-col">
<div class="card-raised" style="padding:24px; box-shadow:var(--shadow-lg);">
<div class="mono text-xs text-3">--shadow-lg</div>
<div class="text-sm text-1" style="margin-top:8px">卡片悬浮 · 代码面板</div>
</div>
<div class="card-raised" style="padding:24px; box-shadow:var(--shadow-xl);">
<div class="mono text-xs text-3">--shadow-xl</div>
<div class="text-sm text-1" style="margin-top:8px">仪表盘大图 · 对话框</div>
</div>
</div>
</section>
<!-- BUTTONS -->
<section class="ds-section" id="buttons">
<h2><span class="mono-num">06</span>Buttons</h2>
<p class="desc">唯一的主色按钮 Primary(青色),其余都是 Ghost/Subtle。没有多种 primary —— 让每个页面最重要的那个 CTA 足够显眼。</p>
<div class="ds-label">Variants</div>
<div class="example">
<div class="example-row">
<button class="btn btn-primary">立即开始 →</button>
<button class="btn btn-ghost">查看文档</button>
<button class="btn btn-subtle">跳过</button>
<button class="btn btn-danger">解绑账号</button>
<button class="btn btn-primary" disabled>禁用</button>
</div>
</div>
<div class="ds-label" style="margin-top:20px">Sizes</div>
<div class="example">
<div class="example-row">
<button class="btn btn-primary btn-lg">btn-lg · 注册</button>
<button class="btn btn-primary">btn · 默认</button>
<button class="btn btn-primary btn-sm">btn-sm · 复制</button>
<button class="btn btn-ghost btn-icon" aria-label="设置">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.7 1.7 0 0 0 .3 1.8l.1.1a2 2 0 1 1-2.8 2.8l-.1-.1a1.7 1.7 0 0 0-1.8-.3 1.7 1.7 0 0 0-1 1.5V21a2 2 0 1 1-4 0v-.1a1.7 1.7 0 0 0-1-1.5 1.7 1.7 0 0 0-1.8.3l-.1.1a2 2 0 1 1-2.8-2.8l.1-.1a1.7 1.7 0 0 0 .3-1.8 1.7 1.7 0 0 0-1.5-1H3a2 2 0 1 1 0-4h.1a1.7 1.7 0 0 0 1.5-1 1.7 1.7 0 0 0-.3-1.8l-.1-.1a2 2 0 1 1 2.8-2.8l.1.1a1.7 1.7 0 0 0 1.8.3h0a1.7 1.7 0 0 0 1-1.5V3a2 2 0 1 1 4 0v.1a1.7 1.7 0 0 0 1 1.5 1.7 1.7 0 0 0 1.8-.3l.1-.1a2 2 0 1 1 2.8 2.8l-.1.1a1.7 1.7 0 0 0-.3 1.8v0a1.7 1.7 0 0 0 1.5 1H21a2 2 0 1 1 0 4h-.1a1.7 1.7 0 0 0-1.5 1z"/></svg>
</button>
</div>
</div>
<div class="ds-label" style="margin-top:20px">Loading</div>
<div class="example">
<div class="example-row">
<button class="btn btn-primary loading"><span class="spinner"></span><span class="label">提交中</span></button>
<button class="btn btn-ghost loading"><span class="spinner" style="border-top-color:var(--text-0)"></span><span class="label">加载</span></button>
</div>
</div>
</section>
<!-- BADGES -->
<section class="ds-section" id="badges">
<h2><span class="mono-num">07</span>Badges</h2>
<div class="example">
<div class="example-row">
<span class="badge">NEW</span>
<span class="badge purple">BETA</span>
<span class="badge amber">LIMITED</span>
<span class="badge green">ACTIVE</span>
<span class="badge red">EXPIRED</span>
<span class="badge muted">DRAFT</span>
</div>
</div>
</section>
<!-- CHIPS -->
<section class="ds-section" id="chips">
<h2><span class="mono-num">08</span>Chips & Status</h2>
<p class="desc">chip 用于在代码块周围承载"路由/参数/标签"信息,status-chip 是一个绝对定位的单像素点,用于显示账号/节点在线状态。</p>
<div class="example">
<div class="example-row">
<span class="chip claude"><span class="dot"></span>claude-pool-03</span>
<span class="chip gpt"><span class="dot"></span>gpt-plus-7</span>
<span class="chip gemini"><span class="dot"></span>gemini-2</span>
<span class="chip codex"><span class="dot"></span>codex-pool-01</span>
<span class="chip"><span class="dot"></span>200 · 213ms</span>
</div>
<div class="example-row" style="margin-top:16px">
<span class="pill">OpenAI SDK</span>
<span class="pill">Anthropic SDK</span>
<span class="pill">/v1/chat/completions</span>
</div>
</div>
</section>
<!-- FORMS -->
<section class="ds-section" id="forms">
<h2><span class="mono-num">09</span>Form Fields</h2>
<div class="example">
<div class="two-col" style="gap:24px">
<div class="field">
<label class="field-label">邮箱</label>
<div class="input-wrap">
<span class="icon">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="5" width="18" height="14" rx="2"/><path d="M3 7l9 6 9-6"/></svg>
</span>
<input class="input with-icon" type="email" placeholder="you@puro.im">
</div>
<div class="field-hint">默认 · 空态</div>
</div>
<div class="field">
<label class="field-label">API Key 名称</label>
<input class="input ok" type="text" value="production">
<div class="field-hint" style="color:var(--green)">✓ 可用</div>
</div>
<div class="field">
<label class="field-label">密码</label>
<input class="input error" type="password" value="12345">
<div class="field-error">密码至少 8 位,包含数字和字母</div>
</div>
<div class="field">
<label class="field-label">平台</label>
<select class="input">
<option>Claude Pro / Max</option>
<option>ChatGPT Plus / Pro</option>
<option>Gemini Advanced</option>
</select>
</div>
</div>
<div style="margin-top:18px; display:flex; gap:16px;">
<label class="check"><input type="checkbox" checked><span class="box"></span>接受服务协议</label>
<label class="check"><input type="checkbox"><span class="box"></span>订阅更新邮件</label>
</div>
</div>
</section>
<!-- CARDS -->
<section class="ds-section" id="cards">
<h2><span class="mono-num">10</span>Cards</h2>
<div class="three-col">
<div class="card">
<div class="section-kicker" style="margin-bottom:8px">// default</div>
<div class="text-lg fw-600" style="margin-bottom:6px">标准卡片</div>
<div class="text-sm text-2">用于所有常规内容容器,12px 圆角 + 1px border。</div>
</div>
<div class="card card-interactive">
<div class="section-kicker" style="margin-bottom:8px">// hover</div>
<div class="text-lg fw-600" style="margin-bottom:6px">可交互卡片</div>
<div class="text-sm text-2">hover 时向上位移 2px,border 加深。</div>
</div>
<div class="card-raised" style="padding:24px">
<div class="section-kicker" style="margin-bottom:8px">// raised</div>
<div class="text-lg fw-600" style="margin-bottom:6px">Raised 卡片</div>
<div class="text-sm text-2">不透明背景,用于浮层/仪表盘主体。</div>
</div>
</div>
</section>
<!-- TABLES -->
<section class="ds-section" id="tables">
<h2><span class="mono-num">11</span>Tables</h2>
<p class="desc">主要用于请求日志、API Key 列表、计费记录。数字列一律等宽 tabular-nums。</p>
<div class="card" style="padding:0; overflow:hidden;">
<table class="tbl">
<thead>
<tr>
<th class="mono">TIME</th>
<th>ACCOUNT</th>
<th class="mono">MODEL</th>
<th class="mono">TOKENS</th>
<th class="mono">COST</th>
<th class="mono">STATUS</th>
</tr>
</thead>
<tbody>
<tr>
<td class="mono">13:42:18</td>
<td><span class="provider claude"><span class="dot"></span>claude-3</span></td>
<td class="mono">sonnet-4-5</td>
<td class="mono tabular">2,847</td>
<td class="mono tabular">$0.042</td>
<td class="mono text-green">200</td>
</tr>
<tr>
<td class="mono">13:42:11</td>
<td><span class="provider gpt"><span class="dot"></span>gpt-plus-7</span></td>
<td class="mono">gpt-5-codex</td>
<td class="mono tabular">1,204</td>
<td class="mono tabular">$0.018</td>
<td class="mono text-green">200</td>
</tr>
<tr>
<td class="mono">13:42:03</td>
<td><span class="provider gemini"><span class="dot"></span>gemini-2</span></td>
<td class="mono">gemini-2.5-pro</td>
<td class="mono tabular">4,102</td>
<td class="mono tabular">$0.000</td>
<td class="mono text-amber">429</td>
</tr>
</tbody>
</table>
</div>
</section>
<!-- CODE -->
<section class="ds-section" id="code">
<h2><span class="mono-num">12</span>Code Frame</h2>
<div class="code-frame">
<div class="code-head">
<div class="traffic"><span></span><span></span><span></span></div>
<span class="mono text-xs text-3">zsh · puro ≈ 210ms</span>
</div>
<pre class="code-body"><div class="line"><span class="ln">1</span><span><span class="com"># OpenAI SDK · 零改动</span></span></div><div class="line"><span class="ln">2</span><span><span class="kw">from</span> openai <span class="kw">import</span> OpenAI</span></div><div class="line"><span class="ln">3</span><span> </span></div><div class="line"><span class="ln">4</span><span>client = <span class="fn">OpenAI</span>(</span></div><div class="line"><span class="ln">5</span><span> <span class="prop">base_url</span>=<span class="str">"https://ai.puro.im/v1"</span>,</span></div><div class="line"><span class="ln">6</span><span> <span class="prop">api_key</span>=<span class="str">"sk-puro-••••"</span>,</span></div><div class="line"><span class="ln">7</span><span>)</span></div></pre>
</div>
</section>
<!-- NAV -->
<section class="ds-section" id="nav">
<h2><span class="mono-num">13</span>Navigation</h2>
<p class="desc">顶部导航 <code class="pill">.nav</code> 用于 marketing 页面,<code class="pill">.app-shell</code> 带侧边栏用于登录后的应用页面。</p>
<div class="ds-label">Top Nav (Marketing)</div>
<div class="example dense" style="padding:0">
<nav style="position:relative;">
<div class="container nav-inner" style="padding: 0 24px;">
<span class="brand">
<svg class="hex" viewBox="0 0 24 24" fill="none">
<path d="M12 2L21 7V17L12 22L3 17V7L12 2Z" stroke="currentColor" stroke-width="1.8" fill="rgba(34, 211, 238, 0.08)"/>
<path d="M12 7L17 9.5V14.5L12 17L7 14.5V9.5L12 7Z" fill="currentColor"/>
</svg>
PURO AI
</span>
<div class="nav-links">
<a href="#" class="active">产品</a>
<a href="#">定价</a>
<a href="#">文档</a>
<a href="#" class="disabled">Changelog</a>
</div>
<div class="nav-cta">
<a class="btn btn-ghost">登录</a>
<a class="btn btn-primary">开始使用</a>
</div>
</div>
</nav>
</div>
<div class="ds-label" style="margin-top:24px">Side Nav (App)</div>
<div class="example dense" style="padding:0; overflow:hidden;">
<div style="display:grid; grid-template-columns:240px 1fr; min-height:280px;">
<div style="border-right:1px solid var(--border); padding:18px 14px; background:rgba(2,6,23,0.6);">
<div class="brand" style="padding:6px 10px 18px">
<svg class="hex" viewBox="0 0 24 24" fill="none">
<path d="M12 2L21 7V17L12 22L3 17V7L12 2Z" stroke="currentColor" stroke-width="1.8" fill="rgba(34, 211, 238, 0.08)"/>
<path d="M12 7L17 9.5V14.5L12 17L7 14.5V9.5L12 7Z" fill="currentColor"/>
</svg>
PURO
</div>
<div class="side-group">
<div class="side-label">Workspace</div>
<div class="side-item active"><span class="ico"></span>Dashboard</div>
<div class="side-item"><span class="ico"></span>API Keys<span class="count">3</span></div>
<div class="side-item"><span class="ico"></span>订阅账号<span class="count">7</span></div>
<div class="side-item"><span class="ico"></span>调用日志</div>
<div class="side-item"><span class="ico"></span>账单</div>
</div>
<div class="side-group" style="margin-top:20px">
<div class="side-label">Settings</div>
<div class="side-item"><span class="ico"></span>账户</div>
<div class="side-item"><span class="ico"></span>团队</div>
</div>
</div>
<div style="padding:24px; display:flex; align-items:center; justify-content:center; color:var(--text-3);">
<div class="text-sm mono">app content →</div>
</div>
</div>
</div>
</section>
</main>
</div>
<footer style="margin-top:80px; padding:40px 0; border-top:1px solid var(--border); font-family:var(--font-mono); font-size:12px; color:var(--text-3); display:flex; justify-content:space-between;">
<span>PURO AI · Design System v1.0</span>
<span>© 2026 · built with puro.css</span>
</footer>
</div>
</body>
</html>

View File

@@ -0,0 +1,623 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="color-scheme" content="dark">
<title>Docs — PURO AI</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<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">
<link rel="stylesheet" href="puro.css">
<style>
.docs-layout {
display: grid;
grid-template-columns: 240px 1fr 220px;
max-width: 1440px;
margin: 0 auto;
gap: 48px;
padding: 40px 32px 80px;
}
/* left nav */
.docs-nav {
position: sticky; top: 32px; align-self: flex-start;
font-size: 13px;
max-height: calc(100vh - 64px);
overflow-y: auto;
}
.docs-nav .brand-line {
padding: 4px 10px 24px;
font-size: 11px;
font-family: var(--font-mono);
color: var(--text-3);
letter-spacing: 0.1em;
text-transform: uppercase;
border-bottom: 1px solid var(--border);
margin-bottom: 18px;
}
.docs-nav .nav-search {
margin-bottom: 18px; position: relative;
}
.docs-nav .nav-search input {
width: 100%; height: 32px;
padding: 0 10px 0 32px;
background: rgba(2, 6, 23, 0.5);
border: 1px solid var(--border);
border-radius: var(--r-sm);
color: var(--text-1); font-size: 12px;
outline: none; font-family: inherit;
}
.docs-nav .nav-search::before {
content: ""; position: absolute; left: 10px; top: 50%;
width: 12px; height: 12px; transform: translateY(-50%);
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%2364748b' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'><circle cx='11' cy='11' r='7'/><path d='m20 20-3.5-3.5'/></svg>");
background-repeat: no-repeat;
}
.docs-nav-section {
margin-bottom: 18px;
}
.docs-nav-label {
font-family: var(--font-mono); font-size: 10px;
color: var(--text-3); letter-spacing: 0.14em;
text-transform: uppercase;
padding: 0 10px 8px;
}
.docs-nav-item {
display: block;
padding: 5px 10px;
color: var(--text-2);
border-radius: var(--r-sm);
transition: all .1s;
font-size: 13px;
}
.docs-nav-item:hover { color: var(--text-0); background: rgba(255,255,255,0.02); }
.docs-nav-item.active {
color: var(--cyan);
background: rgba(34, 211, 238, 0.06);
font-weight: 500;
}
/* content */
.docs-body { min-width: 0; }
.docs-crumbs {
font-family: var(--font-mono); font-size: 11px;
color: var(--text-3);
margin-bottom: 14px;
letter-spacing: 0.08em;
}
.docs-crumbs .sep { color: var(--border-2); margin: 0 6px; }
.docs-crumbs .current { color: var(--cyan); }
.docs-body h1 {
font-size: 38px; font-weight: 700;
letter-spacing: -0.02em; margin-bottom: 14px;
}
.docs-body .lede {
font-size: 16px; color: var(--text-2);
line-height: 1.6;
margin-bottom: 32px;
max-width: 640px;
}
.docs-body h2 {
font-size: 22px; font-weight: 700;
letter-spacing: -0.01em;
margin: 44px 0 12px;
padding-top: 14px;
display: flex; align-items: center; gap: 12px;
}
.docs-body h2::before {
content: "";
width: 3px; height: 22px; background: var(--cyan);
border-radius: 2px;
}
.docs-body h3 {
font-size: 16px; font-weight: 600;
margin: 22px 0 8px;
}
.docs-body p {
color: var(--text-1);
line-height: 1.72;
margin-bottom: 14px;
font-size: 14px;
}
.docs-body p code, .docs-body li code, .docs-body td code {
font-family: var(--font-mono);
font-size: 12.5px;
padding: 2px 6px;
background: rgba(2, 6, 23, 0.6);
border: 1px solid var(--border);
border-radius: 4px;
color: var(--cyan);
}
.docs-body ul { margin-bottom: 14px; padding-left: 20px; }
.docs-body li {
color: var(--text-1); line-height: 1.7; font-size: 14px;
margin-bottom: 6px;
}
/* callout */
.callout {
padding: 14px 18px;
border: 1px solid var(--border);
border-left: 3px solid var(--cyan);
border-radius: 0 var(--r-md) var(--r-md) 0;
background: rgba(34, 211, 238, 0.04);
margin: 18px 0;
font-size: 13px;
display: flex; gap: 10px; align-items: flex-start;
}
.callout .icon { color: var(--cyan); flex-shrink: 0; margin-top: 2px; }
.callout.amber { border-left-color: var(--amber); background: rgba(251,191,36,0.04); }
.callout.amber .icon { color: var(--amber); }
/* tabs */
.tabs {
display: flex; gap: 2px;
border-bottom: 1px solid var(--border);
margin-bottom: 0;
}
.tab {
padding: 8px 14px;
font-size: 12px;
font-family: var(--font-mono);
color: var(--text-3);
border-bottom: 2px solid transparent;
margin-bottom: -1px;
cursor: pointer;
transition: all .12s;
}
.tab:hover { color: var(--text-1); }
.tab.active {
color: var(--cyan);
border-bottom-color: var(--cyan);
}
/* code frame in docs */
.code-panel {
border: 1px solid var(--border);
border-radius: var(--r-md);
background: var(--bg-code);
overflow: hidden;
margin-bottom: 18px;
}
.code-panel .panel-tabs {
padding: 0 4px;
background: rgba(15, 23, 42, 0.5);
border-bottom: 1px solid var(--border);
display: flex; align-items: center;
justify-content: space-between;
}
.code-panel .panel-tabs .tabs-inner {
display: flex;
}
.code-panel .panel-tabs .tabs-inner button {
padding: 8px 14px;
font-size: 11px;
font-family: var(--font-mono);
color: var(--text-3);
transition: color .12s;
}
.code-panel .panel-tabs .tabs-inner button.active { color: var(--cyan); }
.code-panel .copy-code {
padding: 6px 12px;
font-size: 11px;
color: var(--text-3);
font-family: var(--font-mono);
border-radius: 4px;
cursor: pointer;
transition: all .12s;
}
.code-panel .copy-code:hover { color: var(--cyan); background: rgba(34,211,238,0.08); }
.code-panel pre {
padding: 16px 20px;
font-family: var(--font-mono);
font-size: 13px;
line-height: 1.75;
color: var(--text-1);
overflow-x: auto;
}
.code-panel pre .com { color: #64748b; font-style: italic; }
.code-panel pre .kw { color: #f472b6; }
.code-panel pre .str { color: #86efac; }
.code-panel pre .fn { color: #fcd34d; }
.code-panel pre .prop { color: #93c5fd; }
.code-panel pre .num { color: #fb923c; }
/* quick start grid */
.quick-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
margin-bottom: 24px;
}
.quick-card {
padding: 18px 20px;
border: 1px solid var(--border);
border-radius: var(--r-lg);
background: rgba(15, 23, 42, 0.5);
transition: all .15s;
cursor: pointer;
}
.quick-card:hover {
border-color: var(--cyan);
transform: translateY(-2px);
}
.quick-card .num {
font-family: var(--font-mono);
font-size: 11px;
color: var(--cyan);
letter-spacing: 0.14em;
margin-bottom: 10px;
}
.quick-card h4 {
font-size: 14px; font-weight: 600; margin-bottom: 4px;
}
.quick-card p {
font-size: 12px !important; color: var(--text-3) !important;
margin: 0 !important;
}
/* models table */
.models-table {
width: 100%;
border-collapse: collapse;
margin-bottom: 22px;
border: 1px solid var(--border);
border-radius: var(--r-md);
overflow: hidden;
}
.models-table th, .models-table td {
padding: 10px 14px;
text-align: left;
border-bottom: 1px solid var(--border);
font-size: 13px;
}
.models-table th {
background: rgba(2, 6, 23, 0.5);
color: var(--text-3);
font-family: var(--font-mono);
font-size: 10px;
text-transform: uppercase;
letter-spacing: 0.14em;
font-weight: 500;
}
.models-table td { color: var(--text-1); }
.models-table tr:last-child td { border-bottom: none; }
.models-table tr:hover td { background: rgba(255,255,255,0.02); }
.models-table .mono { font-family: var(--font-mono); font-size: 12px; }
/* TOC on right */
.docs-toc {
position: sticky; top: 32px; align-self: flex-start;
font-size: 12px;
}
.docs-toc-label {
font-family: var(--font-mono); font-size: 10px;
color: var(--text-3); letter-spacing: 0.14em;
text-transform: uppercase;
margin-bottom: 10px;
}
.docs-toc a {
display: block;
padding: 4px 10px;
color: var(--text-2);
border-left: 2px solid transparent;
transition: all .1s;
}
.docs-toc a:hover { color: var(--text-0); }
.docs-toc a.active {
color: var(--cyan);
border-left-color: var(--cyan);
background: rgba(34,211,238,0.03);
}
.docs-toc a.sub { padding-left: 20px; font-size: 11.5px; }
/* top nav (reuse landing nav) */
.docs-top {
border-bottom: 1px solid var(--border);
background: rgba(2, 6, 23, 0.7);
backdrop-filter: blur(12px);
position: sticky; top: 0; z-index: 10;
}
/* footer nav */
.page-foot {
margin-top: 64px;
padding-top: 28px;
border-top: 1px solid var(--border);
display: flex; justify-content: space-between;
gap: 16px;
}
.foot-link {
flex: 1;
padding: 14px 18px;
border: 1px solid var(--border);
border-radius: var(--r-md);
background: rgba(15, 23, 42, 0.5);
transition: all .12s;
}
.foot-link:hover { border-color: var(--cyan); }
.foot-link .dir {
font-family: var(--font-mono); font-size: 11px;
color: var(--text-3); margin-bottom: 4px;
}
.foot-link .title { font-size: 14px; font-weight: 600; color: var(--text-0); }
.foot-link.next { text-align: right; }
</style>
</head>
<body>
<div class="bg-glow soft"></div>
<nav class="nav docs-top">
<div class="container nav-inner">
<a class="brand" href="Landing.html">
<svg class="hex" viewBox="0 0 24 24" fill="none">
<path d="M12 2L21 7V17L12 22L3 17V7L12 2Z" stroke="currentColor" stroke-width="1.8" fill="rgba(34, 211, 238, 0.08)"/>
<path d="M12 7L17 9.5V14.5L12 17L7 14.5V9.5L12 7Z" fill="currentColor"/>
</svg>
PURO AI
</a>
<div class="nav-links">
<a href="Landing.html">产品</a>
<a href="Pricing.html">定价</a>
<a href="#" class="active">文档</a>
<a href="Design System.html">设计系统</a>
</div>
<div class="nav-cta">
<a href="Dashboard.html" class="btn btn-ghost">Dashboard</a>
<a href="Register.html" class="btn btn-primary">开始使用</a>
</div>
</div>
</nav>
<div class="docs-layout">
<!-- LEFT NAV -->
<aside class="docs-nav">
<div class="brand-line">// docs · v1</div>
<div class="nav-search">
<input placeholder="搜索文档…">
</div>
<div class="docs-nav-section">
<div class="docs-nav-label">Getting Started</div>
<a class="docs-nav-item active" href="#">快速开始</a>
<a class="docs-nav-item" href="#">产品概念</a>
<a class="docs-nav-item" href="#">绑定你的订阅</a>
<a class="docs-nav-item" href="#">创建 API Key</a>
</div>
<div class="docs-nav-section">
<div class="docs-nav-label">API Reference</div>
<a class="docs-nav-item" href="#">Endpoints 总览</a>
<a class="docs-nav-item" href="#">/v1/chat/completions</a>
<a class="docs-nav-item" href="#">/v1/messages</a>
<a class="docs-nav-item" href="#">/v1/generateContent</a>
<a class="docs-nav-item" href="#">模型列表</a>
<a class="docs-nav-item" href="#">错误码</a>
</div>
<div class="docs-nav-section">
<div class="docs-nav-label">Integrations</div>
<a class="docs-nav-item" href="#">Claude Code</a>
<a class="docs-nav-item" href="#">Cursor · Continue</a>
<a class="docs-nav-item" href="#">Cline · Roo Code</a>
<a class="docs-nav-item" href="#">OpenAI SDK</a>
<a class="docs-nav-item" href="#">Anthropic SDK</a>
</div>
<div class="docs-nav-section">
<div class="docs-nav-label">Advanced</div>
<a class="docs-nav-item" href="#">调度与 failover</a>
<a class="docs-nav-item" href="#">速率限制与配额</a>
<a class="docs-nav-item" href="#">流式响应</a>
<a class="docs-nav-item" href="#">Function Calling</a>
<a class="docs-nav-item" href="#">数据隐私</a>
</div>
</aside>
<!-- MAIN -->
<main class="docs-body">
<div class="docs-crumbs">
Docs <span class="sep">/</span>
Getting Started <span class="sep">/</span>
<span class="current">快速开始</span>
</div>
<h1>快速开始</h1>
<p class="lede">
PURO AI 提供一个统一的 OpenAI 兼容端点 —— 你已有的 SDK 代码只需要改 <code>base_url</code><code>api_key</code> 两行,就能用上你绑定的 Claude / ChatGPT / Gemini 订阅。整个过程通常不超过 5 分钟。
</p>
<!-- quick links -->
<div class="quick-grid">
<div class="quick-card">
<div class="num">STEP 01</div>
<h4>绑定订阅</h4>
<p>授权你的 Claude / ChatGPT 账号加入池。</p>
</div>
<div class="quick-card">
<div class="num">STEP 02</div>
<h4>创建 API Key</h4>
<p>为每个客户端生成独立的 sk-puro-* key。</p>
</div>
<div class="quick-card">
<div class="num">STEP 03</div>
<h4>切换 base_url</h4>
<p>改两行代码,剩下和官方 SDK 一模一样。</p>
</div>
</div>
<h2 id="install">① 绑定你的订阅</h2>
<p>
进入 <code>Dashboard → 订阅账号 → 绑定新订阅</code>,选择平台后通过 OAuth 一键授权。每个订阅都会被加入对应的"池",同一池内的请求会自动做负载均衡、限流回退和故障转移。
</p>
<div class="callout">
<span class="icon">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="11" x="3" y="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
</span>
<div>
凭证通过 AES-256 加密存储在隔离的 KMS 中。我们只会用它代理你发出的请求 —— 不会进入训练数据、不会做二次分发。详见 <a href="#">数据隐私</a>
</div>
</div>
<h2 id="key">② 创建 API Key</h2>
<p>
<code>Dashboard → API Keys → 创建 Key</code> 生成一个 <code>sk-puro-*</code> 的 key。建议每个客户端 / 环境单独一个 key,泄漏时可以直接吊销而不影响其他场景。
</p>
<h2 id="request">③ 发送第一个请求</h2>
<p>PURO AI 同时兼容 OpenAI 和 Anthropic 的 API 格式。按你原来在用的 SDK 风格选择对应的代码示例即可:</p>
<div class="code-panel">
<div class="panel-tabs">
<div class="tabs-inner">
<button class="active">Python</button>
<button>Node.js</button>
<button>cURL</button>
<button>Anthropic SDK</button>
</div>
<span class="copy-code">⧉ 复制</span>
</div>
<pre><span class="com"># pip install openai</span>
<span class="kw">from</span> openai <span class="kw">import</span> OpenAI
client = <span class="fn">OpenAI</span>(
<span class="prop">base_url</span>=<span class="str">"https://ai.puro.im/v1"</span>,
<span class="prop">api_key</span>=<span class="str">"sk-puro-YOUR_KEY"</span>,
)
resp = client.chat.completions.<span class="fn">create</span>(
<span class="prop">model</span>=<span class="str">"claude-sonnet-4-5"</span>, <span class="com"># 可直接写任意平台的模型名</span>
<span class="prop">messages</span>=[
{<span class="prop">"role"</span>: <span class="str">"user"</span>, <span class="prop">"content"</span>: <span class="str">"hi, who am I talking to?"</span>}
],
)
<span class="fn">print</span>(resp.choices[<span class="num">0</span>].message.content)
</pre>
</div>
<p>返回结构完全符合 OpenAI 格式,可以无缝对接任何基于 OpenAI SDK 的应用(Cursor / Continue / Cline / Roo Code / Open WebUI …)。</p>
<h2 id="models">可用模型</h2>
<p>绑定后,下列模型都可以直接用 model 字段调用 —— PURO 会根据模型自动路由到对应的订阅池。</p>
<table class="models-table">
<thead>
<tr>
<th>MODEL</th>
<th>PROVIDER</th>
<th></th>
<th>上下文</th>
<th>状态</th>
</tr>
</thead>
<tbody>
<tr>
<td class="mono text-cyan">claude-sonnet-4-5</td>
<td><span class="provider claude"><span class="dot"></span>Claude</span></td>
<td>Pro / Max</td>
<td class="mono">200k</td>
<td><span class="badge green">OK</span></td>
</tr>
<tr>
<td class="mono text-cyan">claude-opus-4</td>
<td><span class="provider claude"><span class="dot"></span>Claude</span></td>
<td>Max</td>
<td class="mono">200k</td>
<td><span class="badge green">OK</span></td>
</tr>
<tr>
<td class="mono text-cyan">claude-haiku-4-5</td>
<td><span class="provider claude"><span class="dot"></span>Claude</span></td>
<td>Pro / Max</td>
<td class="mono">200k</td>
<td><span class="badge green">OK</span></td>
</tr>
<tr>
<td class="mono text-cyan">gpt-5</td>
<td><span class="provider gpt"><span class="dot"></span>ChatGPT</span></td>
<td>Plus / Pro</td>
<td class="mono">128k</td>
<td><span class="badge green">OK</span></td>
</tr>
<tr>
<td class="mono text-cyan">gpt-5-codex</td>
<td><span class="provider gpt"><span class="dot"></span>ChatGPT</span></td>
<td>Plus / Pro</td>
<td class="mono">128k</td>
<td><span class="badge green">OK</span></td>
</tr>
<tr>
<td class="mono text-cyan">gemini-2.5-pro</td>
<td><span class="provider gemini"><span class="dot"></span>Gemini</span></td>
<td>Advanced</td>
<td class="mono">1M</td>
<td><span class="badge amber">BETA</span></td>
</tr>
<tr>
<td class="mono text-cyan">gemini-2.5-flash</td>
<td><span class="provider gemini"><span class="dot"></span>Gemini</span></td>
<td>Advanced</td>
<td class="mono">1M</td>
<td><span class="badge green">OK</span></td>
</tr>
</tbody>
</table>
<h2 id="base-urls">支持的 base_url</h2>
<p>每种格式都提供独立的 base_url —— 如果你在用原生 Anthropic / Google SDK,请选择对应格式以获得最完整的字段兼容:</p>
<ul>
<li><code>https://ai.puro.im/v1</code> — OpenAI 兼容格式(推荐,覆盖 95% 场景)</li>
<li><code>https://ai.puro.im/anthropic</code> — Anthropic Messages 格式(原生 Claude SDK)</li>
<li><code>https://ai.puro.im/google</code> — Google GenAI 格式(原生 Gemini SDK)</li>
</ul>
<div class="callout amber">
<span class="icon">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><path d="M12 9v4M12 17h.01"/></svg>
</span>
<div>
一个 <code>sk-puro-*</code> 可以同时用于三种 base_url —— 鉴权和计费是统一的,你不需要为不同 SDK 维护多个 key。
</div>
</div>
<h2 id="next">下一步</h2>
<p>把 PURO 接入到你常用的工具:</p>
<ul>
<li><a href="#"><b>Claude Code</b> 中使用 PURO</a><code>ANTHROPIC_BASE_URL</code> 环境变量一行搞定</li>
<li><a href="#"><b>Cursor</b> 中使用 PURO</a> — 自定义模型 + 自定义 base_url</li>
<li><a href="#"><b>Cline / Roo Code</b> 中使用 PURO</a> — 选择 "OpenAI compatible"</li>
<li><a href="#"><b>Continue</b> 中使用 PURO</a> — config.yaml 添加 provider</li>
</ul>
<!-- pager -->
<div class="page-foot">
<a class="foot-link" href="#">
<div class="dir">← 上一页</div>
<div class="title">产品概念</div>
</a>
<a class="foot-link next" href="#">
<div class="dir">下一页 →</div>
<div class="title">绑定你的订阅</div>
</a>
</div>
</main>
<!-- RIGHT TOC -->
<aside class="docs-toc">
<div class="docs-toc-label">On this page</div>
<a href="#install" class="active">① 绑定订阅</a>
<a href="#key">② 创建 API Key</a>
<a href="#request">③ 第一个请求</a>
<a href="#models">可用模型</a>
<a href="#base-urls">支持的 base_url</a>
<a href="#next">下一步</a>
</aside>
</div>
</body>
</html>

View File

@@ -0,0 +1,222 @@
# PURO AI · 设计交付文档
> 把已有的 Claude / ChatGPT / Codex / Gemini 订阅聚合成统一 API · 让"已经付过钱的订阅"真正可编程
本文档面向**接手实现这套设计的工程团队 / Coding Agent**,说明每个 HTML 文件的用途、数据契约、交互逻辑,以及与后端的对接点。
---
## 0. 文件清单
| 文件 | 类型 | 说明 |
|---|---|---|
| `Landing.html` | 营销首页 | 未登录入口 · Hero / 模型墙 / 功能 / 代码示例 / Dashboard 预览 / Pricing / FAQ |
| `Pricing.html` | 定价页 | 完整定价 + 成本估算器 + 工具兼容墙 + 10 条 FAQ |
| `Docs.html` | 文档 | 快速开始 / 模型列表 / API 参考 / 各客户端配置 |
| `Login.html` | 登录 | 左侧叙事 + 右侧表单 · 支持 LinuxDO OAuth |
| `Register.html` | 注册 | 带密码强度 · 下一步引导 · 送 $5 |
| `Binding.html` | 订阅绑定 | 核心差异化流程 · OAuth 接入 Claude Pro / ChatGPT Plus |
| `Dashboard.html` | 控制台首页 | 余额 / 用量图表 / 近期请求 / 订阅池状态 |
| `API Keys.html` | Key 管理 | 创建 / 吊销 / 预算限制 / 模型白名单 |
| `Design System.html` | 设计系统 | 色板 / 字号 / 组件索引 |
| `puro.css` | 全站样式 | 所有页面共用的 tokens + primitives(.btn / .pill / .tag / .input 等) |
**所有页面必须首行 meta:** `<meta name="color-scheme" content="dark">` —— 防止浏览器 auto-darken 覆盖 cyan 按钮。
---
## 1. 设计 Tokens(见 `puro.css`)
```
--bg-0: #0a0e1a 页面底
--bg-1: #0f172a raised
--bg-2: #111827 card alt
--bg-code: #020617 代码面板
--border: #1e293b
--border-2:#334155
--border-3:#475569
--text-0: #f8fafc 主
--text-1: #cbd5e1 次
--text-2: #94a3b8 说明
--text-3: #64748b 弱
--cyan: #22d3ee 主强调(primary btn / 链接 / 数据点)
--cyan-2: #67e8f9 hover
--purple: #a855f7 次强调 / 装饰
--amber: #fbbf24 提醒 / 限时标签
--green: #34d399 成功 / 在线
--red: #f87171 错误 / 危险
```
字体:`Inter`(正文)+ `JetBrains Mono`(所有数值 / 代码 / 元信息)。
圆角:`--r-sm 6px / --r-md 8px / --r-lg 12px / --r-xl 16px`
---
## 2. 页面 → 后端契约
### 2.1 Landing · 无后端依赖(纯营销)
锚点:`#pricing` / `#faq` / `#features` / `#code` / `#dashboard`。注册 CTA → `Register.html`
### 2.2 Register
提交表单需要 **后端返回**:
```
POST /auth/register
{ email, password, linuxdo_token? }
→ 200 { user_id, jwt, balance_credits: 5.00 /* $5 注册赠送 */ }
```
前端验证规则:
- 邮箱格式 `^[^\s@]+@[^\s@]+\.[^\s@]+$`
- 密码强度 ≥ 2(长度 ≥ 8 + 字母大小写混合即可通过)
- 两次密码一致 + 勾选 terms
成功后跳 `Binding.html`
### 2.3 Login
```
POST /auth/login
{ email, password } 或 { linuxdo_oauth_code }
→ 200 { jwt, user, has_subscriptions: boolean }
```
`has_subscriptions === false`,引导去 `Binding.html`;否则去 `Dashboard.html`
### 2.4 Binding(核心差异化)
OAuth 流程,每个平台:
```
POST /bindings/oauth/start
{ provider: 'claude' | 'chatgpt' | 'codex' | 'gemini' }
→ { auth_url, state }
```
前端打开 `auth_url` 新窗口;OAuth 回调:
```
GET /bindings/oauth/callback?code=...&state=...
→ 302 → /binding/success?provider=claude&account_id=...
```
绑定列表:
```
GET /bindings
→ [ { id, provider, account_email, plan, status: 'healthy'|'cooling'|'error', quota_remaining_pct, bound_at } ]
DELETE /bindings/:id 解绑
POST /bindings/:id/test 发一条测试请求验证凭证有效
```
### 2.5 Dashboard
```
GET /me/overview
→ {
balance: { credits: 45.23, bonus_credits: 12.00, expires_at: null },
usage_today: { requests: 1842, tokens_in: 2.1e6, tokens_out: 4.8e5, cost: 1.23 },
usage_30d: [{ date, cost, requests }, ...],
recent_requests: [{ ts, model, route_to, status, latency_ms, tokens, cost }, ...],
pool_status: [{ provider, accounts: [{ status, quota_pct }] }]
}
```
图表用 `usage_30d` 绘制折线(cyan 主线,purple 副线)。
### 2.6 API Keys
```
GET /api-keys
POST /api-keys { name, models?: string[], monthly_budget?: number }
DELETE /api-keys/:id
```
Key 前缀 `sk-puro-` + 32 字符。创建后仅显示一次完整值,之后只保留前 8 位。
### 2.7 Pricing
纯静态展示。充值 CTA 统一走:
```
POST /billing/topup
{ amount_usd, payment_method: 'alipay'|'wechat'|'usdt'|'stripe' }
→ { payment_url / qr_code, order_id }
```
阶梯赠送在前端计算并展示(见 `Pricing.html` 底部 script),**后端下单时再次校验**:
```
阶梯 = amount >= 500 ? 120%
: amount >= 200 ? 110%
: amount >= 99 ? 100%
: amount >= 50 ? 70%
: amount >= 30 ? 50%
: amount >= 20 ? 35%
: 21%
```
---
## 3. 统一 API Gateway(产品核心)
所有用户集成的终点:
```
https://api.puro.im/v1/chat/completions # OpenAI 兼容
https://api.puro.im/v1/messages # Anthropic 兼容
https://api.puro.im/v1beta/models/:m:generateContent # Gemini 兼容
```
Header: `Authorization: Bearer sk-puro-xxx`
**调度逻辑(文档化在 `Docs.html`):**
1. 根据模型解析目标 provider
2. 从该 provider 的订阅池挑一个 `status=healthy` 的账号(权重:剩余配额 × 响应时延倒数)
3. 若 429 / 限流,标记 `cooling` 60-300s,failover 到下一个
4. 若该 provider 所有订阅都 cooling 且用户余额 > 0,fallback 到官方 API
5. 所有请求写入 `request_logs`(用户可见,保留周期按套餐)
---
## 4. 套餐与限制
| | Starter $9.9 | Pro $29.9 ⭐ | Scale $99 | Custom |
|---|---|---|---|---|
| 赠送 | +21% | +50% | +100% | 阶梯 |
| API Keys | 1 | 3 | 10 | 按 Pro |
| RPM | 60 | 120 | 300 | 按 Pro |
| 日志保留 | 7d | 30d | 90d | 按 Pro |
| 自带订阅 | ❌ | ∞ | ∞ | ✅ |
| 多账号 failover | — | ✅ | ✅ + 优先级 | ✅ |
**Enterprise** 走 Sales 线:私有化 / SLA / Invoice,不在普通订单流里。
---
## 5. 交互细节(容易漏)
- **余额不足**:Gateway 返回 `402 Payment Required`,Dashboard 顶部红色 banner + 发邮件(80% / 95% 两档预警)
- **订阅 cooling** 时 Dashboard 订阅卡片上角 amber 闪烁点
- **API Key 创建**:弹窗必须强制用户复制一次,关闭弹窗后永远看不到完整值
- **密码强度**:评分 0-4,label 对应 `— / 弱 / 中 / 强 / 极强`,颜色 `border / red / amber / cyan / green`
- **登录成功**:按钮变 green 显示 `✓ 登录成功`,800ms 后 window.location 跳转
- **LinuxDO OAuth** 是 PURO 的目标用户群(开发者社区),作为次要登录按钮展示
---
## 6. 响应式断点
- `<= 900px`:Login/Register 的 split 变单列,narrative 折叠
- `<= 820px`:Pricing grid 从 4 列 → 1 列
- `<= 820px`:Landing pricing-grid-landing 从 3 列 → 1 列
- `<= 960px`:Pricing 的 calculator 从 2 列 → 1 列
---
## 7. 后端技术建议(非设计范畴,仅供参考)
- **Gateway 层**:Go / Rust 写高吞吐代理,HTTP/2 + streaming,每 provider 起独立 worker pool
- **调度**:Redis ZSET 存每个订阅的健康分 + cooling 过期时间
- **日志**:写 ClickHouse(列存 + 按日分区),Dashboard 直接查
- **凭证加密**:每个订阅凭证用 AES-256-GCM 加密,key 放 KMS;解密只在 gateway worker 内存里,绝不落日志
- **计费**:Pulsar / Redis Streams 实时扣费,最终一致性,异步对账
---
## 8. 已知 TODO(设计层面暂不处理,开发阶段补)
- [ ] Dashboard 真实图表联动(目前是静态 SVG)
- [ ] Binding OAuth 回调 loading / 成功动效
- [ ] 邮件模板(注册验证 / 余额预警 / 充值成功)
- [ ] 忘记密码流程
- [ ] 团队 / 多人协作 UI(Enterprise 档需要)
- [ ] i18n:当前仅中文,英文版待出
---
生成时间:2026-04 · 设计稿版本:v1
联系 Sam / 产品负责人确认实现细节。

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,351 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="color-scheme" content="dark">
<title>登录 — PURO AI</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<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">
<link rel="stylesheet" href="puro.css">
<style>
body { min-height: 100vh; overflow-x: hidden; }
.split {
display: grid;
grid-template-columns: 1fr 1fr;
min-height: 100vh;
position: relative;
z-index: 1;
}
/* ============ LEFT (NARRATIVE) ============ */
.narrative {
position: relative;
overflow: hidden;
padding: 48px 56px;
display: flex;
flex-direction: column;
justify-content: space-between;
border-right: 1px solid var(--border);
background: linear-gradient(135deg, rgba(34,211,238,0.04), transparent 60%), rgba(15, 23, 42, 0.3);
}
.narrative::before {
content: "";
position: absolute;
inset: 0;
background:
radial-gradient(600px 400px at 20% 20%, rgba(34,211,238,0.08), transparent 60%),
radial-gradient(500px 400px at 90% 80%, rgba(168,85,247,0.06), transparent 60%);
pointer-events: none;
}
.narrative-inner { position: relative; display: flex; flex-direction: column; gap: 28px; z-index: 1; }
.brand-top {
display: inline-flex; align-items: center; gap: 10px;
font-weight: 700; font-size: 15px; letter-spacing: -0.01em; color: var(--text-0);
}
.brand-top svg { width: 22px; height: 22px; color: var(--cyan); }
.n-kicker { font-family: var(--font-mono); font-size: 12px; letter-spacing: 0.1em; color: var(--cyan); }
.n-headline {
font-size: 52px; font-weight: 800; letter-spacing: -0.03em; line-height: 1.05;
display: flex; align-items: baseline; flex-wrap: wrap; gap: 14px;
}
.n-headline .amber { color: var(--amber); }
.n-headline .cyan { color: var(--cyan); }
.n-headline .arrow { font-size: 38px; color: var(--text-3); font-weight: 400; }
.n-sub { color: var(--text-2); font-size: 15px; line-height: 1.75; max-width: 440px; }
.n-sub .line { display: block; }
.n-sub .puro { color: var(--text-0); font-weight: 600; }
.route-demo {
margin-top: 10px;
font-family: var(--font-mono);
font-size: 12px;
background: rgba(2, 6, 23, 0.6);
border: 1px solid var(--border);
border-radius: var(--r-md);
padding: 18px 22px;
max-width: 440px;
}
.route-demo .row { display: flex; gap: 20px; padding: 4px 0; align-items: center; }
.route-demo .k { color: var(--text-3); min-width: 70px; }
.route-demo .v { color: var(--text-0); }
.route-demo .pill-inline { padding: 2px 8px; border-radius: 4px; background: rgba(34,211,238,0.08); border: 1px solid rgba(34,211,238,0.25); color: var(--cyan); }
.route-demo .pill-inline.amber { background: rgba(251,191,36,0.08); border-color: rgba(251,191,36,0.25); color: var(--amber); }
.route-demo .pill-inline.green { background: rgba(52,211,153,0.08); border-color: rgba(52,211,153,0.25); color: var(--green); }
.route-demo .dot-g { width: 6px; height: 6px; border-radius: 50%; background: var(--green); display: inline-block; margin-right: 6px; box-shadow: 0 0 0 3px rgba(52,211,153,0.15); }
.n-bottom {
position: relative; z-index: 1;
font-family: var(--font-mono); font-size: 11px; color: var(--text-3);
display: flex; gap: 14px; align-items: center; flex-wrap: wrap;
}
.n-bottom .sep { color: var(--border-2); }
.n-bottom .live { color: var(--green); display: inline-flex; align-items: center; gap: 6px; }
.n-bottom .live .dot { width: 5px; height: 5px; border-radius: 50%; background: var(--green); box-shadow: 0 0 6px var(--green); }
/* ============ RIGHT (FORM) ============ */
.form-side {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 48px 56px;
position: relative;
}
.back-home {
position: absolute; top: 32px; right: 32px;
font-family: var(--font-mono); font-size: 12px; color: var(--text-3);
display: inline-flex; align-items: center; gap: 6px;
}
.back-home:hover { color: var(--text-0); }
.form-card { width: 100%; max-width: 400px; }
.form-card h1 { font-size: 32px; font-weight: 700; letter-spacing: -0.02em; margin-bottom: 6px; }
.form-card .sub { color: var(--text-2); font-size: 14px; margin-bottom: 32px; }
.field { margin-bottom: 16px; }
.field label { display: block; font-size: 12px; color: var(--text-2); font-weight: 500; margin-bottom: 8px; letter-spacing: 0.01em; }
.input-wrap {
position: relative;
display: flex; align-items: center;
background: rgba(2, 6, 23, 0.4);
border: 1px solid var(--border-2);
border-radius: var(--r-md);
transition: all .15s;
}
.input-wrap:focus-within { border-color: var(--cyan); background: rgba(34, 211, 238, 0.04); box-shadow: 0 0 0 3px rgba(34, 211, 238, 0.1); }
.input-wrap .icon { padding: 0 12px; color: var(--text-3); flex-shrink: 0; display: flex; }
.input-wrap input {
flex: 1; background: none; border: none; outline: none;
font-family: inherit; color: var(--text-0); font-size: 14px;
padding: 12px 14px 12px 0;
}
.input-wrap input::placeholder { color: var(--text-3); }
.eye { display: flex; padding: 10px 12px; color: var(--text-3); background: none; border: none; cursor: pointer; }
.eye:hover { color: var(--text-1); }
.field-meta { display: flex; justify-content: space-between; align-items: center; margin-top: 12px; }
.check { display: inline-flex; align-items: center; gap: 8px; font-size: 13px; color: var(--text-2); cursor: pointer; user-select: none; }
.check input { position: absolute; opacity: 0; pointer-events: none; }
.check .box { width: 14px; height: 14px; border-radius: 3px; border: 1px solid var(--border-2); background: rgba(2, 6, 23, 0.4); position: relative; flex-shrink: 0; }
.check input:checked ~ .box { background: var(--cyan); border-color: var(--cyan); }
.check input:checked ~ .box::after {
content: ""; position: absolute; left: 4px; top: 1px;
width: 4px; height: 8px; border: solid #042f2e; border-width: 0 2px 2px 0;
transform: rotate(45deg);
}
.forgot { font-size: 13px; color: var(--text-3); }
.forgot:hover { color: var(--cyan); }
/* Submit button — explicit styles to ensure rendering */
.submit-btn {
width: 100%;
padding: 14px 20px !important;
font-size: 14px !important;
margin-top: 8px;
position: relative;
background: var(--cyan) !important;
color: #042f2e !important;
border: 1px solid var(--cyan) !important;
font-weight: 600 !important;
z-index: 2;
}
.submit-btn:hover { background: var(--cyan-2) !important; }
.submit-btn:disabled { opacity: 0.5; cursor: not-allowed; }
.submit-btn .spinner { width: 14px; height: 14px; border: 2px solid rgba(0,0,0,0.2); border-top-color: #042f2e; border-radius: 50%; animation: spin .7s linear infinite; display: none; }
.submit-btn.loading .spinner { display: inline-block; }
.submit-btn.loading .label { opacity: 0.5; }
@keyframes spin { to { transform: rotate(360deg); } }
.ghost-btn { width: 100%; padding: 14px 20px !important; font-size: 14px !important; font-weight: 500 !important; }
.divider {
display: flex; align-items: center; gap: 14px;
margin: 24px 0;
color: var(--text-3); font-size: 11px;
font-family: var(--font-mono); letter-spacing: 0.15em;
}
.divider::before, .divider::after { content: ""; flex: 1; height: 1px; background: var(--border); }
.linuxdo-ico {
width: 18px; height: 18px; border-radius: 3px;
background: linear-gradient(135deg, #f0a030, #f05050);
display: inline-flex; align-items: center; justify-content: center;
font-size: 10px; font-weight: 800; color: white;
}
.foot { margin-top: 24px; text-align: center; font-size: 13px; color: var(--text-2); }
.foot a { color: var(--cyan); font-weight: 500; }
.foot a:hover { color: var(--cyan-2); }
.legal { margin-top: 20px; text-align: center; font-size: 11px; color: var(--text-3); font-family: var(--font-mono); }
.legal a { color: var(--text-2); border-bottom: 1px dashed var(--border-2); }
.legal a:hover { color: var(--cyan); border-color: var(--cyan); }
@media (max-width: 900px) {
.split { grid-template-columns: 1fr; }
.narrative { padding: 32px 28px; border-right: none; border-bottom: 1px solid var(--border); }
.n-headline { font-size: 36px; }
.route-demo, .n-bottom { display: none; }
.form-side { padding: 40px 28px; }
.back-home { top: 20px; right: 20px; }
}
</style>
</head>
<body>
<div class="bg-glow"></div>
<div class="grain"></div>
<div class="split">
<!-- LEFT: NARRATIVE -->
<section class="narrative">
<div class="narrative-inner">
<a href="Landing.html" class="brand-top">
<svg viewBox="0 0 24 24" fill="none">
<path d="M12 2L21 7V17L12 22L3 17V7L12 2Z" stroke="currentColor" stroke-width="1.8" fill="rgba(34, 211, 238, 0.08)"/>
<path d="M12 7L17 9.5V14.5L12 17L7 14.5V9.5L12 7Z" fill="currentColor"/>
</svg>
PURO AI
</a>
<div>
<div class="n-kicker">// 你的订阅,已经付过钱了</div>
<h1 class="n-headline" style="margin-top: 12px;">
<span class="amber">N</span> 个订阅
<span class="arrow"></span>
<span class="cyan">1</span> 个 key
</h1>
</div>
<div class="n-sub">
<span class="line">省去切换账号的繁琐,</span>
<span class="line">省去为多个高昂订阅重复买单。</span>
<span class="line" style="margin-top: 8px;"><span class="puro">PURO</span>(纯粹)—— 让 AI 调用回归本质。</span>
</div>
<div class="route-demo">
<div class="row"><span class="k">POST</span><span class="v">/v1/chat/completions</span></div>
<div class="row"><span class="k">model</span><span class="pill-inline">claude-sonnet-4-5</span></div>
<div class="row"><span class="k">route →</span><span class="pill-inline amber">claude-pool-03</span></div>
<div class="row"><span class="k">status</span><span><span class="dot-g"></span><span style="color:var(--green)">200</span><span style="color:var(--text-3); margin:0 6px;">·</span>213ms<span style="color:var(--text-3); margin:0 6px;">·</span>42 tok</span></div>
</div>
</div>
<div class="n-bottom">
<span>Claude</span><span class="sep">·</span>
<span>ChatGPT</span><span class="sep">·</span>
<span>Codex</span><span class="sep">·</span>
<span>Gemini</span>
<span class="sep">|</span>
<span class="live"><span class="dot"></span>ai.puro.im · operational</span>
</div>
</section>
<!-- RIGHT: FORM -->
<section class="form-side">
<a href="Landing.html" class="back-home">← 返回首页</a>
<form class="form-card" id="login-form" autocomplete="off" novalidate>
<h1>登录</h1>
<p class="sub">用你的 PURO AI 账户继续</p>
<div class="field">
<label for="email">邮箱</label>
<div class="input-wrap">
<span class="icon">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="5" width="18" height="14" rx="2"/>
<path d="M3 7l9 6 9-6"/>
</svg>
</span>
<input type="email" id="email" name="email" placeholder="you@puro.im" required>
</div>
</div>
<div class="field">
<label for="password">密码</label>
<div class="input-wrap">
<span class="icon">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
<rect x="4" y="11" width="16" height="10" rx="2"/>
<path d="M8 11V7a4 4 0 0 1 8 0v4"/>
</svg>
</span>
<input type="password" id="password" name="password" placeholder="••••••••" required>
<button type="button" class="eye" id="toggle-pw" aria-label="切换显示密码">
<svg id="eye-open" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
<path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7z"/>
<circle cx="12" cy="12" r="3"/>
</svg>
<svg id="eye-closed" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" style="display:none;">
<path d="M9.88 9.88A3 3 0 0 0 14.12 14.12M10.73 5.08A10.94 10.94 0 0 1 12 5c6.5 0 10 7 10 7a13.16 13.16 0 0 1-1.67 2.68M6.61 6.61A13.53 13.53 0 0 0 2 12s3.5 7 10 7a9.77 9.77 0 0 0 5.39-1.61M2 2l20 20"/>
</svg>
</button>
</div>
<div class="field-meta">
<label class="check">
<input type="checkbox" checked>
<span class="box"></span>
记住我
</label>
<a href="#" class="forgot">忘记密码?</a>
</div>
</div>
<button type="submit" class="btn btn-primary submit-btn" id="submit-btn">
<span class="spinner"></span>
<span class="label">登录 →</span>
</button>
<div class="divider">OR</div>
<button type="button" class="btn btn-ghost ghost-btn">
<span class="linuxdo-ico">L</span>
使用 LinuxDO 登录
</button>
<div class="foot">
没有账户?<a href="Register.html">注册</a>
</div>
<div class="legal">
登录即表示你同意 <a href="#">服务条款</a><a href="#">隐私政策</a>
</div>
</form>
</section>
</div>
<script>
const pw = document.getElementById('password');
const toggle = document.getElementById('toggle-pw');
const eyeOpen = document.getElementById('eye-open');
const eyeClosed = document.getElementById('eye-closed');
toggle.addEventListener('click', () => {
const show = pw.type === 'password';
pw.type = show ? 'text' : 'password';
eyeOpen.style.display = show ? 'none' : 'block';
eyeClosed.style.display = show ? 'block' : 'none';
});
const form = document.getElementById('login-form');
const btn = document.getElementById('submit-btn');
form.addEventListener('submit', (e) => {
e.preventDefault();
if (!form.email.value || !form.password.value) return;
btn.classList.add('loading');
btn.disabled = true;
setTimeout(() => {
btn.classList.remove('loading');
btn.disabled = false;
btn.querySelector('.label').textContent = '✓ 登录成功';
btn.style.background = 'var(--green)';
setTimeout(() => { window.location.href = 'Dashboard.html'; }, 800);
}, 1000);
});
</script>
</body>
</html>

View File

@@ -0,0 +1,472 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="color-scheme" content="dark">
<title>Pricing — PURO AI</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<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">
<link rel="stylesheet" href="puro.css">
<style>
.hero { max-width: 1180px; margin: 0 auto; padding: 80px 32px 40px; text-align: center; }
.hero h1 { font-size: 54px; font-weight: 800; letter-spacing: -0.03em; margin-bottom: 18px; }
.hero h1 .accent { color: var(--cyan); }
.hero .sub { color: var(--text-2); font-size: 17px; max-width: 620px; margin: 0 auto 14px; line-height: 1.6; }
.hero .underline { display: inline-flex; align-items: center; gap: 8px; font-family: var(--font-mono); font-size: 12px; color: var(--text-3); padding: 6px 14px; background: rgba(2, 6, 23, 0.5); border: 1px solid var(--border); border-radius: 100px; }
.hero .underline .dot { width: 6px; height: 6px; background: var(--green); border-radius: 50%; box-shadow: 0 0 0 3px rgba(52,211,153,0.15); }
.pricing-wrap { max-width: 1180px; margin: 0 auto; padding: 20px 32px 40px; }
.pricing-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 14px; }
.tier { position: relative; border: 1px solid var(--border); border-radius: var(--r-xl); background: rgba(15, 23, 42, 0.5); padding: 28px 24px; display: flex; flex-direction: column; transition: all .2s; }
.tier:hover { border-color: var(--border-2); transform: translateY(-3px); }
.tier.popular { border-color: rgba(34, 211, 238, 0.4); background: radial-gradient(500px 300px at 50% 0%, rgba(34,211,238,0.08), transparent 60%), rgba(15, 23, 42, 0.7); box-shadow: 0 0 0 1px rgba(34,211,238,0.15), 0 20px 40px -20px rgba(34,211,238,0.2); transform: translateY(-6px); }
.tier.popular:hover { transform: translateY(-9px); }
.tier .flag { position: absolute; top: -11px; left: 50%; transform: translateX(-50%); font-family: var(--font-mono); font-size: 10px; letter-spacing: 0.14em; padding: 4px 12px; border-radius: 100px; background: var(--cyan); color: #042f2e; font-weight: 700; }
.tier .flag.amber { background: var(--amber); color: #422006; }
.tier .flag.muted { background: rgba(100, 116, 139, 0.2); color: var(--text-2); border: 1px solid var(--border); }
.tier-name { font-size: 13px; font-family: var(--font-mono); letter-spacing: 0.1em; text-transform: uppercase; color: var(--text-3); margin-bottom: 8px; }
.tier-headline { font-size: 16px; font-weight: 600; color: var(--text-0); margin-bottom: 22px; line-height: 1.35; min-height: 44px; }
.price-row { display: flex; align-items: baseline; gap: 4px; margin-bottom: 4px; }
.price { font-family: var(--font-mono); font-size: 42px; font-weight: 800; letter-spacing: -0.03em; color: var(--text-0); }
.tier.popular .price { color: var(--cyan); }
.price .curr { font-size: 18px; font-weight: 600; color: var(--text-3); margin-right: 2px; vertical-align: super; }
.credit-line { font-family: var(--font-mono); font-size: 12px; color: var(--cyan); margin-bottom: 14px; }
.credit-line .arrow { margin: 0 6px; color: var(--text-3); }
.credit-line .bonus { padding: 2px 8px; background: rgba(34,211,238,0.08); border: 1px solid rgba(34,211,238,0.25); border-radius: 4px; font-weight: 600; margin-left: 6px; }
.discount-tag { display: inline-block; font-family: var(--font-mono); font-size: 11px; color: var(--amber); background: rgba(251,191,36,0.08); border: 1px solid rgba(251,191,36,0.25); border-radius: 4px; padding: 3px 8px; margin-bottom: 18px; }
.tier hr { border: none; border-top: 1px dashed var(--border); margin: 4px 0 18px; }
.feat { display: flex; gap: 10px; align-items: flex-start; font-size: 13px; color: var(--text-1); padding: 4px 0; line-height: 1.55; }
.feat .tick { color: var(--cyan); flex-shrink: 0; margin-top: 2px; }
.feat.muted { color: var(--text-3); }
.feat.muted .tick { color: var(--text-3); }
.feat b { color: var(--text-0); font-weight: 600; }
.feats { display: flex; flex-direction: column; gap: 2px; margin-bottom: 24px; flex: 1; }
.tier-cta { width: 100%; justify-content: center; }
.custom-row { margin-top: 12px; display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
.custom-card { padding: 24px; border: 1px solid var(--border); border-radius: var(--r-xl); background: linear-gradient(135deg, rgba(168,85,247,0.05), transparent 50%), rgba(15, 23, 42, 0.4); display: flex; align-items: center; gap: 22px; }
.custom-card .icon { width: 48px; height: 48px; border-radius: 10px; background: rgba(34,211,238,0.1); color: var(--cyan); display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
.custom-card .icon.purple { background: rgba(168,85,247,0.1); color: var(--purple); }
.custom-card h3 { font-size: 16px; font-weight: 600; margin-bottom: 4px; }
.custom-card p { font-size: 13px; color: var(--text-2); line-height: 1.5; }
.custom-card .btn { margin-left: auto; flex-shrink: 0; }
.works { max-width: 1180px; margin: 0 auto; padding: 80px 32px 40px; }
.section-head { text-align: center; margin-bottom: 32px; }
.section-head .kicker { font-family: var(--font-mono); font-size: 12px; color: var(--cyan); letter-spacing: 0.14em; margin-bottom: 10px; }
.section-head h2 { font-size: 32px; font-weight: 700; letter-spacing: -0.02em; margin-bottom: 8px; }
.section-head p { color: var(--text-2); font-size: 15px; max-width: 560px; margin: 0 auto; line-height: 1.55; }
.tools-grid { display: grid; grid-template-columns: repeat(6, 1fr); gap: 10px; }
.tool { padding: 18px 14px; border: 1px solid var(--border); border-radius: var(--r-md); background: rgba(15, 23, 42, 0.4); text-align: center; transition: all .15s; }
.tool:hover { border-color: var(--border-2); background: rgba(15, 23, 42, 0.7); }
.tool .logo { width: 28px; height: 28px; margin: 0 auto 10px; display: flex; align-items: center; justify-content: center; color: var(--text-1); }
.tool .name { font-size: 12px; font-weight: 500; color: var(--text-1); }
.tool .tag { font-size: 10px; color: var(--text-3); margin-top: 2px; font-family: var(--font-mono); }
.calc-section { max-width: 1180px; margin: 0 auto; padding: 40px 32px; }
.calc { border: 1px solid var(--border); border-radius: var(--r-xl); background: radial-gradient(600px 300px at 0% 0%, rgba(34,211,238,0.06), transparent 60%), radial-gradient(600px 300px at 100% 100%, rgba(168,85,247,0.06), transparent 60%), rgba(15, 23, 42, 0.4); padding: 32px 36px; display: grid; grid-template-columns: 1fr 1fr; gap: 40px; align-items: center; }
.calc-left h3 { font-size: 22px; font-weight: 700; letter-spacing: -0.01em; margin-bottom: 8px; }
.calc-left .sub { color: var(--text-2); font-size: 14px; line-height: 1.55; margin-bottom: 22px; }
.calc-controls { display: flex; flex-direction: column; gap: 14px; }
.slider-row .s-top { display: flex; justify-content: space-between; font-size: 13px; margin-bottom: 6px; align-items: baseline; }
.slider-row .s-top .val { font-family: var(--font-mono); font-weight: 700; color: var(--cyan); }
.slider-row input[type=range] { -webkit-appearance: none; width: 100%; height: 4px; background: var(--border); border-radius: 2px; outline: none; }
.slider-row input[type=range]::-webkit-slider-thumb { -webkit-appearance: none; width: 16px; height: 16px; border-radius: 50%; background: var(--cyan); cursor: pointer; box-shadow: 0 0 0 4px rgba(34,211,238,0.15); }
.calc-right { background: rgba(2, 6, 23, 0.6); border: 1px solid var(--border); border-radius: var(--r-lg); padding: 28px; }
.calc-right .breakdown { display: flex; flex-direction: column; gap: 10px; margin-bottom: 18px; }
.calc-right .line { display: flex; justify-content: space-between; font-size: 13px; }
.calc-right .line .lab { color: var(--text-2); }
.calc-right .line .v { font-family: var(--font-mono); color: var(--text-0); }
.calc-right .line.savings .v { color: var(--green); }
.calc-right .total-line { padding-top: 14px; border-top: 1px dashed var(--border); display: flex; justify-content: space-between; align-items: baseline; }
.calc-right .total-line .lab { font-size: 12px; color: var(--text-3); font-family: var(--font-mono); text-transform: uppercase; letter-spacing: 0.14em; }
.calc-right .total-line .big { font-family: var(--font-mono); font-size: 28px; font-weight: 800; color: var(--cyan); letter-spacing: -0.02em; }
.calc-right .total-line .big .curr { font-size: 14px; color: var(--text-3); font-weight: 500; margin-right: 2px; }
.faq-section { max-width: 880px; margin: 0 auto; padding: 60px 32px 100px; }
.faq { border: 1px solid var(--border); border-radius: var(--r-md); background: rgba(15, 23, 42, 0.4); margin-bottom: 8px; overflow: hidden; transition: all .15s; }
.faq:hover { border-color: var(--border-2); }
.faq summary { padding: 18px 22px; cursor: pointer; list-style: none; display: flex; align-items: center; gap: 14px; font-size: 15px; font-weight: 500; color: var(--text-0); position: relative; }
.faq summary::-webkit-details-marker { display: none; }
.faq summary::after { content: "+"; margin-left: auto; font-family: var(--font-mono); font-size: 18px; color: var(--text-3); transition: transform .2s; }
.faq[open] summary::after { content: ""; color: var(--cyan); }
.faq summary .num { font-family: var(--font-mono); font-size: 11px; color: var(--cyan); letter-spacing: 0.1em; min-width: 26px; }
.faq .answer { padding: 0 22px 20px 62px; color: var(--text-2); font-size: 14px; line-height: 1.7; }
.faq .answer code { font-family: var(--font-mono); background: rgba(2, 6, 23, 0.6); padding: 1px 6px; border-radius: 3px; color: var(--cyan); font-size: 12.5px; }
.faq .answer a { color: var(--cyan); }
.faq .answer ul { padding-left: 20px; margin-top: 8px; }
.final-cta { max-width: 1180px; margin: 40px auto 80px; padding: 0 32px; }
.final-cta-inner { padding: 48px; border: 1px solid var(--border); border-radius: var(--r-xl); background: radial-gradient(800px 400px at 50% 0%, rgba(34,211,238,0.08), transparent 60%), rgba(15, 23, 42, 0.6); text-align: center; }
.final-cta-inner h2 { font-size: 32px; font-weight: 800; letter-spacing: -0.02em; margin-bottom: 10px; }
.final-cta-inner p { color: var(--text-2); font-size: 15px; margin-bottom: 26px; }
@media (max-width: 960px) {
.pricing-grid { grid-template-columns: 1fr 1fr; }
.custom-row { grid-template-columns: 1fr; }
.tools-grid { grid-template-columns: repeat(3, 1fr); }
.calc { grid-template-columns: 1fr; }
}
</style>
</head>
<body>
<div class="bg-glow"></div>
<div class="grain"></div>
<nav class="nav">
<div class="container nav-inner">
<a class="brand" href="Landing.html">
<svg class="hex" viewBox="0 0 24 24" fill="none">
<path d="M12 2L21 7V17L12 22L3 17V7L12 2Z" stroke="currentColor" stroke-width="1.8" fill="rgba(34, 211, 238, 0.08)"/>
<path d="M12 7L17 9.5V14.5L12 17L7 14.5V9.5L12 7Z" fill="currentColor"/>
</svg>
PURO AI
</a>
<div class="nav-links">
<a href="Landing.html">产品</a>
<a href="#" class="active">定价</a>
<a href="Docs.html">文档</a>
<a href="Design System.html">设计系统</a>
</div>
<div class="nav-cta">
<a href="Login.html" class="btn btn-ghost">登录</a>
<a href="Register.html" class="btn btn-primary">开始使用</a>
</div>
</div>
</nav>
<section class="hero">
<div class="section-kicker" style="margin-bottom:14px;">// pricing · 充多少 · 用多少 · 永不过期</div>
<h1>一次充值,<span class="accent">全平台</span>通用</h1>
<p class="sub">
同一份积分可以用在 Claude / ChatGPT / Gemini 任意池上。我们把你的订阅额度变成真正的 API 余额 —— 相比官方 API 便宜 <b class="text-cyan">至多 70%</b>
</p>
<div class="underline">
<span class="dot"></span>
余额永不过期 · 支持支付宝 / 微信 / USDT · 无隐藏订阅费
</div>
</section>
<div class="pricing-wrap">
<div class="pricing-grid">
<div class="tier">
<span class="flag muted">STARTER</span>
<div class="tier-name">tier · 01</div>
<div class="tier-headline">先尝尝鲜,跑通接入</div>
<div class="price-row"><span class="price"><span class="curr">$</span>9.9</span></div>
<div class="credit-line">充 $9.9 <span class="arrow"></span><b>$12</b> 积分 <span class="bonus">+21%</span></div>
<span class="discount-tag">相当于官方 API · 0.5 折起</span>
<hr/>
<div class="feats">
<div class="feat"><span class="tick"></span>可用所有模型 / 所有池</div>
<div class="feat"><span class="tick"></span><b>1</b> 个 API Key</div>
<div class="feat"><span class="tick"></span>60 RPM 速率限制</div>
<div class="feat"><span class="tick"></span>基础日志(7 天保留)</div>
<div class="feat muted"><span class="tick"></span>自带订阅接入</div>
<div class="feat muted"><span class="tick"></span>团队 / 多人协作</div>
</div>
<a href="Register.html" class="btn btn-ghost btn-lg tier-cta">充值 →</a>
</div>
<div class="tier popular">
<span class="flag">◆ 推荐</span>
<div class="tier-name">tier · 02</div>
<div class="tier-headline">个人重度用户 · 最划算</div>
<div class="price-row"><span class="price"><span class="curr">$</span>29.9</span></div>
<div class="credit-line">充 $29.9 <span class="arrow"></span><b>$45</b> 积分 <span class="bonus">+50%</span></div>
<span class="discount-tag">相当于官方 API · 3-7 折</span>
<hr/>
<div class="feats">
<div class="feat"><span class="tick"></span>可用所有模型 / 所有池</div>
<div class="feat"><span class="tick"></span><b>3</b> 个 API Key · 独立预算</div>
<div class="feat"><span class="tick"></span>120 RPM 速率限制</div>
<div class="feat"><span class="tick"></span>调用日志(30 天保留)</div>
<div class="feat"><span class="tick"></span>自带订阅接入(无限个)</div>
<div class="feat"><span class="tick"></span>多账号 failover 调度</div>
</div>
<a href="Register.html" class="btn btn-primary btn-lg tier-cta">立即充值 →</a>
</div>
<div class="tier">
<span class="flag amber">⚡ 限时 +100%</span>
<div class="tier-name">tier · 03</div>
<div class="tier-headline">小团队 / 长跑项目</div>
<div class="price-row"><span class="price"><span class="curr">$</span>99</span></div>
<div class="credit-line">充 $99 <span class="arrow"></span><b>$198</b> 积分 <span class="bonus">+100%</span></div>
<span class="discount-tag">相当于官方 API · 2-5 折</span>
<hr/>
<div class="feats">
<div class="feat"><span class="tick"></span>所有 Pro 能力</div>
<div class="feat"><span class="tick"></span><b>10</b> 个 API Key · 独立预算</div>
<div class="feat"><span class="tick"></span>300 RPM 速率限制</div>
<div class="feat"><span class="tick"></span>调用日志(90 天保留)</div>
<div class="feat"><span class="tick"></span>请求优先级加权调度</div>
<div class="feat"><span class="tick"></span>Slack / Discord 群组支持</div>
</div>
<a href="Register.html" class="btn btn-ghost btn-lg tier-cta">充值 →</a>
</div>
<div class="tier">
<span class="flag muted">CUSTOM</span>
<div class="tier-name">tier · 04</div>
<div class="tier-headline">自定义金额 · 按需充值</div>
<div class="price-row"><span class="price"><span class="curr">$</span><span id="custom-amt">50</span></span></div>
<div class="credit-line">得约 <b id="custom-credit">$78</b> 积分 <span class="bonus">+<span id="custom-bonus">56</span>%</span></div>
<input type="range" min="10" max="500" value="50" step="10" id="custom-slider" style="-webkit-appearance:none; width:100%; height:4px; background:var(--border); border-radius:2px; margin-bottom:12px;">
<span class="discount-tag">根据金额阶梯自动匹配折扣</span>
<hr/>
<div class="feats">
<div class="feat"><span class="tick"></span>积分永不过期</div>
<div class="feat"><span class="tick"></span>Pro 全部能力</div>
<div class="feat"><span class="tick"></span>阶梯 +21% ~ +100%</div>
<div class="feat"><span class="tick"></span>支付宝 / 微信 / USDT</div>
<div class="feat muted"><span class="tick"></span>拖动滑块预览赠送</div>
</div>
<a href="Register.html" class="btn btn-ghost btn-lg tier-cta">定制充值 →</a>
</div>
</div>
<div class="custom-row">
<div class="custom-card">
<div class="icon purple"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M2 20h20"/><path d="M4 20V10l8-6 8 6v10"/><path d="M9 20v-7h6v7"/></svg></div>
<div style="flex:1;">
<h3>Enterprise · 企业定制</h3>
<p>专属订阅池、SLA、合规审计、私有化部署、发票结算。规模 >$500/月起可申请。</p>
</div>
<a href="#" class="btn btn-ghost">联系商务 →</a>
</div>
<div class="custom-card">
<div class="icon"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/></svg></div>
<div style="flex:1;">
<h3>已有订阅?直接接入</h3>
<p>有 Claude Max / ChatGPT Pro?免费注册后绑定,只为 PURO 路由费买单 —— 按次 <code class="pill">$0.0008/request</code></p>
</div>
<a href="Binding.html" class="btn btn-ghost">接入我的订阅 →</a>
</div>
</div>
</div>
<section class="calc-section">
<div class="calc">
<div class="calc-left">
<div class="section-kicker" style="margin-bottom:10px">// cost estimator</div>
<h3>算算你能省多少?</h3>
<p class="sub">按你的使用场景,对比 PURO 和官方 API 的月度花费差。数字会根据你选的场景自动更新。</p>
<div class="calc-controls">
<div class="slider-row">
<div class="s-top"><span>日均请求数</span><span class="val" id="req-val">5,000</span></div>
<input type="range" min="500" max="50000" step="500" value="5000" id="req-slider">
</div>
<div class="slider-row">
<div class="s-top"><span>平均每请求 tokens</span><span class="val" id="tok-val">3,000</span></div>
<input type="range" min="500" max="10000" step="500" value="3000" id="tok-slider">
</div>
<div class="slider-row">
<div class="s-top"><span>Claude 占比</span><span class="val" id="mix-val">50%</span></div>
<input type="range" min="0" max="100" step="10" value="50" id="mix-slider">
</div>
</div>
</div>
<div class="calc-right">
<div class="breakdown">
<div class="line"><span class="lab">月度 tokens 消耗</span><span class="v" id="total-tok">450M</span></div>
<div class="line"><span class="lab">官方 API 价格</span><span class="v" id="official-cost">$1,620</span></div>
<div class="line"><span class="lab">PURO 价格(含 +50% 赠送)</span><span class="v" id="puro-cost">$486</span></div>
<div class="line savings"><span class="lab">节省</span><span class="v" id="save-amt">$1,134 · 70%</span></div>
</div>
<div class="total-line">
<div>
<div class="lab">建议充值</div>
<div style="font-size:12px; color:var(--text-3); margin-top:4px;" id="rec-note">≈ 3 天试用 + Pro 档充值</div>
</div>
<div class="big"><span class="curr">$</span><span id="rec-amt">486</span></div>
</div>
</div>
</div>
</section>
<section class="works">
<div class="section-head">
<div class="kicker">// works everywhere</div>
<h2>一个 key,所有工具通用</h2>
<p>只要支持自定义 <code class="pill">base_url</code> 或 OpenAI / Anthropic API,都能直接接入 PURO。</p>
</div>
<div class="tools-grid">
<div class="tool"><div class="logo"><svg width="22" height="22" viewBox="0 0 24 24" fill="#d97757"><path d="M4.5 19L12 4l7.5 15H16l-4-8.5L8 19H4.5z"/></svg></div><div class="name">Claude Code</div><div class="tag">ANTHROPIC_BASE_URL</div></div>
<div class="tool"><div class="logo"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M6 18L18 6M8 6h10v10"/></svg></div><div class="name">Cursor</div><div class="tag">自定义模型</div></div>
<div class="tool"><div class="logo"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></svg></div><div class="name">Cline</div><div class="tag">OpenAI 兼容</div></div>
<div class="tool"><div class="logo"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><circle cx="12" cy="12" r="8"/><path d="m8 12 2 2 6-6"/></svg></div><div class="name">Roo Code</div><div class="tag">OpenAI 兼容</div></div>
<div class="tool"><div class="logo"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M3 12h4l3-8 4 16 3-8h4"/></svg></div><div class="name">Continue</div><div class="tag">config.yaml</div></div>
<div class="tool"><div class="logo"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><circle cx="12" cy="12" r="9"/><path d="M12 3v18M3 12h18"/></svg></div><div class="name">OpenAI SDK</div><div class="tag">Python / Node</div></div>
<div class="tool"><div class="logo"><svg width="22" height="22" viewBox="0 0 24 24" fill="#d97757"><path d="M4.5 19L12 4l7.5 15H16l-4-8.5L8 19H4.5z"/></svg></div><div class="name">Anthropic SDK</div><div class="tag">原生 Claude</div></div>
<div class="tool"><div class="logo"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><rect x="3" y="5" width="18" height="14" rx="2"/><path d="M3 10h18M8 5v14"/></svg></div><div class="name">Open WebUI</div><div class="tag">自定义 base</div></div>
<div class="tool"><div class="logo"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M12 2L2 7v10l10 5 10-5V7z"/><path d="M12 22V12M2 7l10 5 10-5"/></svg></div><div class="name">LangChain</div><div class="tag">LLM 节点</div></div>
<div class="tool"><div class="logo"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><circle cx="6" cy="6" r="3"/><circle cx="18" cy="6" r="3"/><circle cx="12" cy="18" r="3"/><path d="M6 9v3l6 3M18 9v3l-6 3"/></svg></div><div class="name">LlamaIndex</div><div class="tag">模型路由</div></div>
<div class="tool"><div class="logo"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><rect x="4" y="4" width="16" height="16" rx="2"/><path d="M9 9h6v6H9z"/></svg></div><div class="name">Zed</div><div class="tag">Assistant</div></div>
<div class="tool"><div class="logo"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M4 4h6v6H4zM14 4h6v6h-6zM4 14h6v6H4zM14 14h6v6h-6z"/></svg></div><div class="name">更多…</div><div class="tag">60+ 工具</div></div>
</div>
</section>
<section class="faq-section">
<div class="section-head">
<div class="kicker">// frequently asked</div>
<h2>你可能想问的</h2>
<p>没找到答案?<a href="#" style="color:var(--cyan)">发邮件给我们 ↗</a> · 通常 2 小时内回复。</p>
</div>
<details class="faq" open>
<summary><span class="num">01</span>PURO 和 API 中转站 / API 代理有什么不同?</summary>
<div class="answer">
中转站只是把官方 API 请求转一手,价格取决于你预付多少 balance。PURO 的不同是 —— 我们让你 <b>把已有的 Claude Pro / ChatGPT Plus 订阅变成 API</b>
你原本就在付的 $20/月,不再只能在官网聊天里用,而是通过统一 API 喂给 Cursor、Claude Code、任何 SDK。
同时我们也提供按量充值的官方 API 备用池,两种模式可以混用。
</div>
</details>
<details class="faq">
<summary><span class="num">02</span>用订阅跑 API 会不会被封号?</summary>
<div class="answer">
我们会自动控制每个订阅的请求节奏,并在触发限流时把请求 failover 到池子里的其他订阅。实际上 PURO 的调用模式比你在官方客户端直接复制粘贴大段对话 <b>更不容易触发风控</b>
你绑定多个订阅时,单个账号的 <code>RPM</code> 会被压到足够安全的阈值内。另外所有凭证用 AES-256 加密存储,请求链路不经过第三方。
</div>
</details>
<details class="faq">
<summary><span class="num">03</span>积分会过期吗?可以退款吗?</summary>
<div class="answer">
<b>积分永不过期。</b>你可以攒着慢慢用 —— 包括几个月都不用。首次充值 7 天内未产生任何调用可全额退款,之后按剩余积分 85% 比例退。详见 <a href="#">退款政策</a>
</div>
</details>
<details class="faq">
<summary><span class="num">04</span>支持哪些支付方式?</summary>
<div class="answer">
国内:支付宝 · 微信支付。国际:Stripe 信用卡 · USDT (TRC20 / ERC20) · PayPal。企业充值支持 Invoice 对公打款,人民币开票。
</div>
</details>
<details class="faq">
<summary><span class="num">05</span>一个 PURO 账号可以绑定多少个订阅?</summary>
<div class="answer">
<ul>
<li>Starter 档:<b>不支持</b>绑定自带订阅</li>
<li>Pro 档及以上:<b>无限制</b>,你可以把 10 个 ChatGPT Plus + 3 个 Claude Pro 一起绑上去,统一调度</li>
<li>Enterprise:支持跨团队共享池,按组织维度隔离</li>
</ul>
</div>
</details>
<details class="faq">
<summary><span class="num">06</span>如果某个订阅触发限流了会怎样?</summary>
<div class="answer">
PURO 的调度器会把受限的订阅自动标记为 <code>cooling</code> 状态,暂时从池子里摘除。同一请求会立刻被 failover 到池内其他健康订阅上 —— 调用方通常 <b>感受不到中断</b>。你可以在 Dashboard 看到每个订阅的当前状态和剩余配额。
</div>
</details>
<details class="faq">
<summary><span class="num">07</span>计费精度?超量会怎么办?</summary>
<div class="answer">
按实际 token 数 + 模型单价计费,精度到 4 位小数。每个 API Key 可设置独立月度预算,达到后 <code>402 Payment Required</code>,不会继续扣费。账户总余额不足时同样会返回 <code>402</code>,且 Dashboard 有 80% / 95% 两级提醒邮件。
</div>
</details>
<details class="faq">
<summary><span class="num">08</span>数据会被用于训练吗?</summary>
<div class="answer">
<b>不会。</b>所有请求仅用于路由转发,不入库、不留存内容(仅保留元数据如模型、token 数、延迟,用于计费和日志)。Pro 档及以上可选开启"零日志模式",我们连请求 ID 都不记录。
</div>
</details>
<details class="faq">
<summary><span class="num">09</span>可以私有化部署吗?</summary>
<div class="answer">
Enterprise 档支持 Docker / K8s 私有化部署,控制面和数据面可以分开。授权按年订阅,包含升级和技术支持。<a href="#">联系商务 →</a>
</div>
</details>
<details class="faq">
<summary><span class="num">10</span>支持哪些模型?会跟进新模型吗?</summary>
<div class="answer">
当前覆盖 Claude(Sonnet 4.5 / Opus 4 / Haiku 4.5)、ChatGPT(GPT-5 / GPT-5 Codex / GPT-4.1)、Gemini(2.5 Pro / 2.5 Flash)。每当官方发布新模型,我们通常在 <b>24 小时内</b>上线。完整模型列表见 <a href="Docs.html">文档</a>
</div>
</details>
</section>
<section class="final-cta">
<div class="final-cta-inner">
<div class="section-kicker" style="margin-bottom:12px;">// ready to start</div>
<h2>5 分钟,拿到你第一个 sk-puro-* key</h2>
<p>注册送 <b class="text-cyan">$5</b> 测试积分 · 绑定你的第一个订阅即可开始。</p>
<div style="display:inline-flex; gap:12px;">
<a href="Register.html" class="btn btn-primary btn-lg">免费注册 →</a>
<a href="Docs.html" class="btn btn-ghost btn-lg">查看文档</a>
</div>
</div>
</section>
<script>
const csl = document.getElementById('custom-slider');
const camt = document.getElementById('custom-amt');
const ccredit = document.getElementById('custom-credit');
const cbonus = document.getElementById('custom-bonus');
function updateCustom() {
const v = +csl.value;
camt.textContent = v;
let bonus = 21;
if (v >= 500) bonus = 120;
else if (v >= 200) bonus = 110;
else if (v >= 99) bonus = 100;
else if (v >= 50) bonus = 70;
else if (v >= 30) bonus = 50;
else if (v >= 20) bonus = 35;
const credit = Math.round(v * (1 + bonus/100));
ccredit.textContent = '$' + credit;
cbonus.textContent = bonus;
}
csl.addEventListener('input', updateCustom);
updateCustom();
const reqS = document.getElementById('req-slider');
const tokS = document.getElementById('tok-slider');
const mixS = document.getElementById('mix-slider');
function fmtMoney(n) { return '$' + n.toLocaleString('en-US', { maximumFractionDigits: 0 }); }
function fmtNum(n) {
if (n >= 1e9) return (n/1e9).toFixed(1) + 'B';
if (n >= 1e6) return (n/1e6).toFixed(0) + 'M';
if (n >= 1e3) return (n/1e3).toFixed(1) + 'k';
return n.toString();
}
function calc() {
const req = +reqS.value;
const tok = +tokS.value;
const mix = +mixS.value;
document.getElementById('req-val').textContent = req.toLocaleString();
document.getElementById('tok-val').textContent = tok.toLocaleString();
document.getElementById('mix-val').textContent = mix + '%';
const monthlyTok = req * tok * 30;
const avgOfficial = (mix/100) * 6 + (1 - mix/100) * 3;
const official = monthlyTok / 1e6 * avgOfficial;
const puro = official * 0.3;
const save = official - puro;
const savePct = Math.round((save / official) * 100);
document.getElementById('total-tok').textContent = fmtNum(monthlyTok);
document.getElementById('official-cost').textContent = fmtMoney(official);
document.getElementById('puro-cost').textContent = fmtMoney(puro);
document.getElementById('save-amt').textContent = fmtMoney(save) + ' · ' + savePct + '%';
document.getElementById('rec-amt').textContent = Math.ceil(puro);
const note = puro < 30 ? '≈ Starter 档够用' : puro < 80 ? '≈ Pro 档 1 个月' : '≈ Scale 档 · 1 个月';
document.getElementById('rec-note').textContent = note;
}
[reqS, tokS, mixS].forEach(s => s.addEventListener('input', calc));
calc();
</script>
</body>
</html>

View File

@@ -0,0 +1,386 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="color-scheme" content="dark">
<title>注册 — PURO AI</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<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">
<link rel="stylesheet" href="puro.css">
<style>
body { min-height: 100vh; overflow-x: hidden; }
.split { display: grid; grid-template-columns: 1fr 1fr; min-height: 100vh; position: relative; z-index: 1; }
/* ============ LEFT (NARRATIVE + STEPS) ============ */
.narrative {
position: relative; overflow: hidden;
padding: 48px 56px;
display: flex; flex-direction: column; justify-content: space-between;
border-right: 1px solid var(--border);
background: linear-gradient(135deg, rgba(34,211,238,0.04), transparent 60%), rgba(15, 23, 42, 0.3);
}
.narrative::before {
content: ""; position: absolute; inset: 0;
background:
radial-gradient(600px 400px at 20% 20%, rgba(34,211,238,0.08), transparent 60%),
radial-gradient(500px 400px at 90% 80%, rgba(168,85,247,0.06), transparent 60%);
pointer-events: none;
}
.narrative-inner { position: relative; display: flex; flex-direction: column; gap: 28px; z-index: 1; }
.brand-top { display: inline-flex; align-items: center; gap: 10px; font-weight: 700; font-size: 15px; letter-spacing: -0.01em; color: var(--text-0); }
.brand-top svg { width: 22px; height: 22px; color: var(--cyan); }
.n-kicker { font-family: var(--font-mono); font-size: 12px; letter-spacing: 0.1em; color: var(--cyan); }
.n-headline { font-size: 52px; font-weight: 800; letter-spacing: -0.03em; line-height: 1.05; display: flex; align-items: baseline; flex-wrap: wrap; gap: 14px; }
.n-headline .amber { color: var(--amber); }
.n-headline .cyan { color: var(--cyan); }
.n-headline .arrow { font-size: 38px; color: var(--text-3); font-weight: 400; }
.n-sub { color: var(--text-2); font-size: 15px; line-height: 1.75; max-width: 440px; }
.n-sub .line { display: block; }
.n-sub .puro { color: var(--text-0); font-weight: 600; }
.steps { display: flex; flex-direction: column; gap: 16px; padding: 20px 22px; background: rgba(2, 6, 23, 0.5); border: 1px solid var(--border); border-radius: var(--r-md); max-width: 440px; }
.steps-title { font-family: var(--font-mono); font-size: 11px; color: var(--text-3); letter-spacing: 0.14em; }
.step { display: flex; align-items: flex-start; gap: 14px; font-size: 13px; }
.step-num { flex-shrink: 0; width: 22px; height: 22px; border-radius: 50%; background: rgba(34,211,238,0.1); border: 1px solid rgba(34,211,238,0.3); color: var(--cyan); font-family: var(--font-mono); font-size: 11px; font-weight: 700; display: flex; align-items: center; justify-content: center; }
.step.active .step-num { background: var(--cyan); color: #042f2e; border-color: var(--cyan); }
.step-text { color: var(--text-1); line-height: 1.5; padding-top: 2px; }
.step-text b { color: var(--text-0); font-weight: 600; }
.step-text .k { font-family: var(--font-mono); background: rgba(255,255,255,0.04); border: 1px solid var(--border-2); padding: 1px 6px; border-radius: 3px; font-size: 11.5px; color: var(--cyan); }
.n-bottom { position: relative; z-index: 1; font-family: var(--font-mono); font-size: 11px; color: var(--text-3); display: flex; gap: 14px; align-items: center; flex-wrap: wrap; }
.n-bottom .sep { color: var(--border-2); }
.n-bottom .live { color: var(--green); display: inline-flex; align-items: center; gap: 6px; }
.n-bottom .live .dot { width: 5px; height: 5px; border-radius: 50%; background: var(--green); box-shadow: 0 0 6px var(--green); }
/* ============ RIGHT (FORM) ============ */
.form-side { display: flex; flex-direction: column; justify-content: center; align-items: center; padding: 48px 56px; position: relative; }
.back-home { position: absolute; top: 32px; right: 32px; font-family: var(--font-mono); font-size: 12px; color: var(--text-3); display: inline-flex; align-items: center; gap: 6px; }
.back-home:hover { color: var(--text-0); }
.form-card { width: 100%; max-width: 400px; }
.form-card h1 { font-size: 32px; font-weight: 700; letter-spacing: -0.02em; margin-bottom: 6px; }
.form-card .sub { color: var(--text-2); font-size: 14px; margin-bottom: 32px; }
.field { margin-bottom: 16px; }
.field label { display: block; font-size: 12px; color: var(--text-2); font-weight: 500; margin-bottom: 8px; }
.input-wrap { position: relative; display: flex; align-items: center; background: rgba(2, 6, 23, 0.4); border: 1px solid var(--border-2); border-radius: var(--r-md); transition: all .15s; }
.input-wrap:focus-within { border-color: var(--cyan); background: rgba(34, 211, 238, 0.04); box-shadow: 0 0 0 3px rgba(34, 211, 238, 0.1); }
.input-wrap .icon { padding: 0 12px; color: var(--text-3); flex-shrink: 0; display: flex; }
.input-wrap input { flex: 1; background: none; border: none; outline: none; font-family: inherit; color: var(--text-0); font-size: 14px; padding: 12px 14px 12px 0; }
.input-wrap input::placeholder { color: var(--text-3); }
.eye { display: flex; padding: 10px 12px; color: var(--text-3); background: none; border: none; cursor: pointer; }
.eye:hover { color: var(--text-1); }
.valid-ico { display: none; color: var(--green); padding: 0 12px; }
.input-wrap input.ok ~ .valid-ico { display: flex; }
.input-wrap input.ok { padding-right: 0; }
.input-wrap input.ok + .eye { display: none; }
/* Password strength */
.pw-strength { display: flex; gap: 4px; margin-top: 8px; margin-bottom: 6px; }
.pw-strength .bar { flex: 1; height: 3px; background: var(--border); border-radius: 2px; transition: background .2s; }
.pw-strength[data-score="1"] .bar:nth-child(1) { background: var(--red); }
.pw-strength[data-score="2"] .bar:nth-child(-n+2) { background: var(--amber); }
.pw-strength[data-score="3"] .bar:nth-child(-n+3) { background: var(--cyan); }
.pw-strength[data-score="4"] .bar { background: var(--green); }
.pw-hint { display: flex; justify-content: space-between; font-family: var(--font-mono); font-size: 10px; color: var(--text-3); letter-spacing: 0.05em; }
.pw-hint .val { color: var(--text-3); }
.pw-hint[data-score="1"] .val { color: var(--red); }
.pw-hint[data-score="2"] .val { color: var(--amber); }
.pw-hint[data-score="3"] .val { color: var(--cyan); }
.pw-hint[data-score="4"] .val { color: var(--green); }
.match-hint { font-family: var(--font-mono); font-size: 11px; color: var(--text-3); margin-top: 6px; min-height: 14px; }
.match-hint.mismatch { color: var(--red); }
.match-hint.ok { color: var(--green); }
.check { display: inline-flex; align-items: flex-start; gap: 10px; font-size: 13px; color: var(--text-2); cursor: pointer; user-select: none; margin: 6px 0 20px; line-height: 1.5; }
.check input { position: absolute; opacity: 0; pointer-events: none; }
.check .box { width: 14px; height: 14px; border-radius: 3px; border: 1px solid var(--border-2); background: rgba(2, 6, 23, 0.4); position: relative; flex-shrink: 0; margin-top: 2px; }
.check input:checked ~ .box { background: var(--cyan); border-color: var(--cyan); }
.check input:checked ~ .box::after { content: ""; position: absolute; left: 4px; top: 1px; width: 4px; height: 8px; border: solid #042f2e; border-width: 0 2px 2px 0; transform: rotate(45deg); }
.check a { color: var(--text-1); border-bottom: 1px dashed var(--border-2); }
.check a:hover { color: var(--cyan); border-color: var(--cyan); }
.submit-btn {
width: 100%;
padding: 14px 20px !important;
font-size: 14px !important;
margin-top: 4px;
background: var(--cyan) !important;
color: #042f2e !important;
border: 1px solid var(--cyan) !important;
font-weight: 600 !important;
z-index: 2;
}
.submit-btn:hover { background: var(--cyan-2) !important; }
.submit-btn:disabled { opacity: 0.5; cursor: not-allowed; background: var(--border-2) !important; border-color: var(--border-2) !important; color: var(--text-3) !important; }
.submit-btn .spinner { width: 14px; height: 14px; border: 2px solid rgba(0,0,0,0.2); border-top-color: #042f2e; border-radius: 50%; animation: spin .7s linear infinite; display: none; }
.submit-btn.loading .spinner { display: inline-block; }
.submit-btn.loading .label { opacity: 0.5; }
@keyframes spin { to { transform: rotate(360deg); } }
.ghost-btn { width: 100%; padding: 14px 20px !important; font-size: 14px !important; font-weight: 500 !important; }
.divider { display: flex; align-items: center; gap: 14px; margin: 24px 0; color: var(--text-3); font-size: 11px; font-family: var(--font-mono); letter-spacing: 0.15em; }
.divider::before, .divider::after { content: ""; flex: 1; height: 1px; background: var(--border); }
.linuxdo-ico { width: 18px; height: 18px; border-radius: 3px; background: linear-gradient(135deg, #f0a030, #f05050); display: inline-flex; align-items: center; justify-content: center; font-size: 10px; font-weight: 800; color: white; }
.foot { margin-top: 24px; text-align: center; font-size: 13px; color: var(--text-2); }
.foot a { color: var(--cyan); font-weight: 500; }
.bonus-note { margin-top: 18px; padding: 12px 14px; background: rgba(52,211,153,0.06); border: 1px solid rgba(52,211,153,0.2); border-radius: var(--r-md); font-size: 12px; color: var(--text-1); display: flex; align-items: center; gap: 10px; }
.bonus-note .emoji { flex-shrink: 0; color: var(--green); font-weight: 700; font-family: var(--font-mono); padding: 3px 8px; background: rgba(52,211,153,0.15); border-radius: 4px; font-size: 11px; }
@media (max-width: 900px) {
.split { grid-template-columns: 1fr; }
.narrative { padding: 32px 28px; border-right: none; border-bottom: 1px solid var(--border); }
.n-headline { font-size: 36px; }
.steps, .n-bottom { display: none; }
.form-side { padding: 40px 28px; }
.back-home { top: 20px; right: 20px; }
}
</style>
</head>
<body>
<div class="bg-glow"></div>
<div class="grain"></div>
<div class="split">
<section class="narrative">
<div class="narrative-inner">
<a href="Landing.html" class="brand-top">
<svg viewBox="0 0 24 24" fill="none">
<path d="M12 2L21 7V17L12 22L3 17V7L12 2Z" stroke="currentColor" stroke-width="1.8" fill="rgba(34, 211, 238, 0.08)"/>
<path d="M12 7L17 9.5V14.5L12 17L7 14.5V9.5L12 7Z" fill="currentColor"/>
</svg>
PURO AI
</a>
<div>
<div class="n-kicker">// 5 分钟开始用</div>
<h1 class="n-headline" style="margin-top: 12px;">
<span class="amber">N</span> 个订阅
<span class="arrow"></span>
<span class="cyan">1</span> 个 key
</h1>
</div>
<div class="n-sub">
<span class="line">省去切换账号的繁琐,</span>
<span class="line">省去为多个高昂订阅重复买单。</span>
<span class="line" style="margin-top: 8px;"><span class="puro">PURO</span>(纯粹)—— 让 AI 调用回归本质。</span>
</div>
<div class="steps">
<div class="steps-title">// 下一步</div>
<div class="step active">
<div class="step-num">1</div>
<div class="step-text"><b>创建账户</b> · 邮箱 + 密码,或用 LinuxDO OAuth</div>
</div>
<div class="step">
<div class="step-num">2</div>
<div class="step-text"><b>绑定订阅</b> · OAuth 接入你现有的 Claude Pro / ChatGPT Plus</div>
</div>
<div class="step">
<div class="step-num">3</div>
<div class="step-text"><b>生成 key</b> · 拿到 <span class="k">sk-puro-…</span>,换掉 SDK 的 <span class="k">base_url</span></div>
</div>
</div>
</div>
<div class="n-bottom">
<span>Claude</span><span class="sep">·</span>
<span>ChatGPT</span><span class="sep">·</span>
<span>Codex</span><span class="sep">·</span>
<span>Gemini</span>
<span class="sep">|</span>
<span class="live"><span class="dot"></span>ai.puro.im · operational</span>
</div>
</section>
<section class="form-side">
<a href="Landing.html" class="back-home">← 返回首页</a>
<form class="form-card" id="reg-form" autocomplete="off" novalidate>
<h1>创建账户</h1>
<p class="sub">注册即送 <b style="color:var(--cyan)">$5</b> 测试积分</p>
<div class="field">
<label for="email">邮箱</label>
<div class="input-wrap">
<span class="icon">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="5" width="18" height="14" rx="2"/>
<path d="M3 7l9 6 9-6"/>
</svg>
</span>
<input type="email" id="email" name="email" placeholder="you@puro.im" required>
<span class="valid-ico">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M5 12l5 5L20 7"/>
</svg>
</span>
</div>
</div>
<div class="field">
<label for="password">密码</label>
<div class="input-wrap">
<span class="icon">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
<rect x="4" y="11" width="16" height="10" rx="2"/>
<path d="M8 11V7a4 4 0 0 1 8 0v4"/>
</svg>
</span>
<input type="password" id="password" name="password" placeholder="至少 8 位,含字母与数字" required>
<button type="button" class="eye" id="toggle-pw" aria-label="切换显示密码">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
<path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7z"/>
<circle cx="12" cy="12" r="3"/>
</svg>
</button>
</div>
<div class="pw-strength" id="pw-strength" data-score="0">
<span class="bar"></span><span class="bar"></span><span class="bar"></span><span class="bar"></span>
</div>
<div class="pw-hint" id="pw-hint" data-score="0">
<span>// strength</span>
<span class="val"></span>
</div>
</div>
<div class="field">
<label for="password2">确认密码</label>
<div class="input-wrap">
<span class="icon">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
<rect x="4" y="11" width="16" height="10" rx="2"/>
<path d="M8 11V7a4 4 0 0 1 8 0v4"/>
</svg>
</span>
<input type="password" id="password2" name="password2" placeholder="再输入一次" required>
<span class="valid-ico">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M5 12l5 5L20 7"/>
</svg>
</span>
</div>
<div class="match-hint" id="match-hint"></div>
</div>
<label class="check">
<input type="checkbox" id="terms">
<span class="box"></span>
<span>我已阅读并同意 <a href="#">服务条款</a><a href="#">隐私政策</a></span>
</label>
<button type="submit" class="btn btn-primary submit-btn" id="submit-btn" disabled>
<span class="spinner"></span>
<span class="label">创建账户 →</span>
</button>
<div class="divider">OR</div>
<button type="button" class="btn btn-ghost ghost-btn">
<span class="linuxdo-ico">L</span>
使用 LinuxDO 注册
</button>
<div class="bonus-note">
<span class="emoji">+$5</span>
<span>完成注册即送 <b style="color:var(--text-0)">$5</b> 测试积分 —— 够你跑几万次 Claude 请求。</span>
</div>
<div class="foot">
已有账户?<a href="Login.html">登录</a>
</div>
</form>
</section>
</div>
<script>
const email = document.getElementById('email');
const pw = document.getElementById('password');
const pw2 = document.getElementById('password2');
const terms = document.getElementById('terms');
const btn = document.getElementById('submit-btn');
const strengthBars = document.getElementById('pw-strength');
const strengthHint = document.getElementById('pw-hint');
const matchHint = document.getElementById('match-hint');
const toggle = document.getElementById('toggle-pw');
toggle.addEventListener('click', () => {
const show = pw.type === 'password';
pw.type = show ? 'text' : 'password';
});
function scorePw(v) {
if (!v) return 0;
let s = 0;
if (v.length >= 8) s++;
if (/[A-Z]/.test(v) && /[a-z]/.test(v)) s++;
if (/\d/.test(v)) s++;
if (/[^A-Za-z0-9]/.test(v) || v.length >= 14) s++;
return s;
}
const labels = ['—', '弱', '中', '强', '极强'];
function updateAll() {
const s = scorePw(pw.value);
strengthBars.dataset.score = s;
strengthHint.dataset.score = s;
strengthHint.querySelector('.val').textContent = labels[s];
// email validation
if (/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.value)) {
email.classList.add('ok');
} else {
email.classList.remove('ok');
}
// password match
if (!pw2.value) {
matchHint.textContent = '';
matchHint.className = 'match-hint';
pw2.classList.remove('ok');
} else if (pw2.value === pw.value) {
matchHint.textContent = '// matched';
matchHint.className = 'match-hint ok';
pw2.classList.add('ok');
} else {
matchHint.textContent = '// passwords do not match';
matchHint.className = 'match-hint mismatch';
pw2.classList.remove('ok');
}
// enable submit?
const valid = email.classList.contains('ok') && s >= 2 && pw.value === pw2.value && pw2.value && terms.checked;
btn.disabled = !valid;
}
[email, pw, pw2].forEach(el => el.addEventListener('input', updateAll));
terms.addEventListener('change', updateAll);
const form = document.getElementById('reg-form');
form.addEventListener('submit', (e) => {
e.preventDefault();
if (btn.disabled) return;
btn.classList.add('loading');
btn.disabled = true;
setTimeout(() => {
btn.classList.remove('loading');
btn.querySelector('.label').textContent = '✓ 注册成功,正在跳转...';
btn.style.background = 'var(--green)';
setTimeout(() => { window.location.href = 'Binding.html'; }, 800);
}, 1200);
});
</script>
</body>
</html>

View File

@@ -0,0 +1,726 @@
/* ==========================================================================
PURO AI — Design System
Shared tokens + primitive styles used across every page.
--------------------------------------------------------------------------
Usage: <link rel="stylesheet" href="puro.css">
========================================================================== */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
/* Surfaces */
--bg-0: #0a0e1a; /* page */
--bg-1: #0f172a; /* raised */
--bg-2: #111827; /* card alt */
--bg-code: #020617; /* code canvas */
/* Borders */
--border: #1e293b;
--border-2: #334155;
--border-3: #475569;
/* Text */
--text-0: #f8fafc; /* primary */
--text-1: #cbd5e1; /* body */
--text-2: #94a3b8; /* muted */
--text-3: #64748b; /* hint */
/* Accents */
--cyan: #22d3ee;
--cyan-2: #67e8f9;
--cyan-dim: #0891b2;
--purple: #a855f7;
--amber: #fbbf24;
--green: #34d399;
--red: #f87171;
--orange: #fb923c;
/* Provider brand dots */
--p-claude: #d97757;
--p-gpt: #10a37f;
--p-gemini: #4285f4;
--p-codex: #f0a030;
/* Radius */
--r-sm: 6px;
--r-md: 8px;
--r-lg: 12px;
--r-xl: 16px;
/* Shadow */
--shadow-lg: 0 30px 60px -30px rgba(0,0,0,0.6);
--shadow-xl: 0 40px 80px -40px rgba(0,0,0,0.8);
/* Typography */
--font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
--font-mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
}
html, body {
background: var(--bg-0);
color: var(--text-0);
font-family: var(--font-sans);
font-feature-settings: "cv11", "ss01", "ss03";
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
line-height: 1.5;
}
body { overflow-x: hidden; }
a { color: inherit; text-decoration: none; }
button { font-family: inherit; cursor: pointer; border: none; background: none; color: inherit; }
/* scrollbar — subtle */
::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--border-2); border-radius: 6px; }
::-webkit-scrollbar-thumb:hover { background: var(--border-3); }
.mono { font-family: var(--font-mono); }
/* ==========================================================================
BACKGROUND EFFECTS
========================================================================== */
.bg-glow {
position: fixed;
inset: 0;
pointer-events: none;
z-index: 0;
overflow: hidden;
}
.bg-glow::before,
.bg-glow::after {
content: "";
position: absolute;
width: 900px;
height: 900px;
border-radius: 50%;
filter: blur(120px);
opacity: 0.35;
}
.bg-glow::before {
background: radial-gradient(circle, #22d3ee 0%, transparent 60%);
top: -300px;
left: -200px;
}
.bg-glow::after {
background: radial-gradient(circle, #a855f7 0%, transparent 60%);
top: 200px;
right: -300px;
opacity: 0.25;
}
.bg-glow.soft::before, .bg-glow.soft::after { opacity: 0.15; }
.grain {
position: fixed;
inset: 0;
pointer-events: none;
z-index: 1;
opacity: 0.4;
mix-blend-mode: overlay;
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='200' height='200'><filter id='n'><feTurbulence baseFrequency='0.9' numOctaves='2'/></filter><rect width='100%25' height='100%25' filter='url(%23n)' opacity='0.35'/></svg>");
}
.container {
max-width: 1100px;
margin: 0 auto;
padding: 0 32px;
position: relative;
z-index: 2;
}
.container-wide { max-width: 1280px; }
.container-narrow { max-width: 860px; }
/* ==========================================================================
NAV
========================================================================== */
.nav {
position: sticky;
top: 0;
z-index: 50;
backdrop-filter: blur(16px);
background: rgba(10, 14, 26, 0.72);
border-bottom: 1px solid var(--border);
}
.nav-inner {
display: flex;
align-items: center;
height: 64px;
gap: 48px;
}
.brand {
display: flex;
align-items: center;
gap: 10px;
font-weight: 700;
font-size: 15px;
letter-spacing: 0.02em;
}
.hex {
width: 22px;
height: 22px;
color: var(--cyan);
}
.nav-links {
display: flex;
gap: 28px;
font-size: 14px;
color: var(--text-2);
}
.nav-links a { transition: color .15s; }
.nav-links a:hover, .nav-links a.active { color: var(--text-0); }
.nav-links .disabled { color: var(--text-3); cursor: not-allowed; display: inline-flex; align-items: center; gap: 6px; }
.nav-links .disabled::after {
content: "即将推出";
font-size: 10px;
padding: 2px 6px;
border: 1px solid var(--border-2);
border-radius: 4px;
color: var(--text-3);
}
.nav-cta {
margin-left: auto;
display: flex;
gap: 10px;
align-items: center;
}
/* ==========================================================================
BUTTONS
========================================================================== */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
padding: 8px 14px;
font-size: 13px;
font-weight: 500;
border-radius: var(--r-md);
transition: all .15s;
white-space: nowrap;
border: 1px solid transparent;
}
.btn-primary {
background: var(--cyan);
color: #042f2e;
font-weight: 600;
}
.btn-primary:hover { background: var(--cyan-2); }
.btn-primary:active { transform: translateY(1px); }
.btn-primary:disabled { opacity: 0.5; cursor: not-allowed; }
.btn-ghost {
border-color: var(--border-2);
color: var(--text-1);
}
.btn-ghost:hover { border-color: var(--border-3); color: var(--text-0); background: rgba(255,255,255,0.02); }
.btn-subtle {
background: rgba(255,255,255,0.04);
color: var(--text-1);
border-color: transparent;
}
.btn-subtle:hover { background: rgba(255,255,255,0.08); color: var(--text-0); }
.btn-danger {
background: rgba(248, 113, 113, 0.1);
color: var(--red);
border-color: rgba(248, 113, 113, 0.25);
}
.btn-danger:hover { background: rgba(248, 113, 113, 0.15); border-color: rgba(248, 113, 113, 0.4); }
.btn-lg { padding: 12px 20px; font-size: 14px; }
.btn-sm { padding: 5px 10px; font-size: 12px; }
.btn-icon { padding: 7px; aspect-ratio: 1; }
.btn .spinner {
width: 14px; height: 14px;
border: 2px solid rgba(0,0,0,0.2);
border-top-color: currentColor;
border-radius: 50%;
animation: spin .7s linear infinite;
display: none;
}
.btn.loading .spinner { display: inline-block; }
.btn.loading .label { opacity: 0.5; }
@keyframes spin { to { transform: rotate(360deg); } }
/* ==========================================================================
BADGES / PILLS / CHIPS
========================================================================== */
.badge {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 2px 8px;
border-radius: 100px;
font-size: 11px;
font-weight: 600;
letter-spacing: 0.02em;
background: rgba(34, 211, 238, 0.1);
color: var(--cyan);
}
.badge.amber { background: rgba(251, 191, 36, 0.12); color: var(--amber); }
.badge.purple { background: rgba(168, 85, 247, 0.12); color: var(--purple); }
.badge.green { background: rgba(52, 211, 153, 0.12); color: var(--green); }
.badge.red { background: rgba(248, 113, 113, 0.12); color: var(--red); }
.badge.muted { background: rgba(255, 255, 255, 0.04); color: var(--text-2); border: 1px solid var(--border); }
.pill {
display: inline-block;
padding: 2px 8px;
border-radius: var(--r-sm);
background: rgba(255,255,255,0.04);
border: 1px solid var(--border);
font-family: var(--font-mono);
font-size: 12px;
color: var(--text-0);
}
.chip {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 4px 10px;
border-radius: 100px;
background: rgba(15, 23, 42, 0.6);
border: 1px solid var(--border);
font-size: 12px;
color: var(--text-1);
font-family: var(--font-mono);
}
.chip .dot { width: 6px; height: 6px; border-radius: 50%; background: var(--green); }
.chip.claude .dot { background: var(--p-claude); }
.chip.gpt .dot { background: var(--p-gpt); }
.chip.gemini .dot { background: var(--p-gemini); }
.chip.codex .dot { background: var(--p-codex); }
.dot-sep { width: 4px; height: 4px; border-radius: 50%; background: var(--text-3); display: inline-block; }
/* status chip (tiny dot absolute-positioned) */
.status-chip {
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--green);
box-shadow: 0 0 0 3px rgba(52, 211, 153, 0.15);
display: inline-block;
}
.status-chip.dim { background: var(--text-3); box-shadow: none; }
.status-chip.amber { background: var(--amber); box-shadow: 0 0 0 3px rgba(251, 191, 36, 0.15); }
.status-chip.red { background: var(--red); box-shadow: 0 0 0 3px rgba(248, 113, 113, 0.15); }
/* ==========================================================================
CARDS / SURFACES
========================================================================== */
.card {
background: rgba(15, 23, 42, 0.6);
border: 1px solid var(--border);
border-radius: var(--r-lg);
padding: 24px;
}
.card-raised {
background: var(--bg-1);
border: 1px solid var(--border);
border-radius: var(--r-lg);
}
.card-interactive {
transition: all .2s;
cursor: pointer;
}
.card-interactive:hover {
border-color: var(--border-2);
background: rgba(15, 23, 42, 0.85);
transform: translateY(-2px);
}
.divider { height: 1px; background: var(--border); margin: 24px 0; border: 0; }
.divider-dashed { border: 0; border-top: 1px dashed var(--border); margin: 20px 0; }
/* ==========================================================================
FORMS
========================================================================== */
.field { margin-bottom: 18px; }
.field-label {
display: block;
font-size: 12px;
font-weight: 500;
color: var(--text-1);
margin-bottom: 8px;
}
.field-hint {
font-size: 12px;
color: var(--text-3);
margin-top: 6px;
}
.field-error {
font-size: 12px;
color: var(--red);
margin-top: 6px;
}
.input-wrap { position: relative; }
.input-wrap .icon {
position: absolute;
left: 14px;
top: 50%;
transform: translateY(-50%);
color: var(--text-3);
pointer-events: none;
display: inline-flex;
}
.input {
width: 100%;
height: 42px;
padding: 0 14px;
background: rgba(15, 23, 42, 0.6);
border: 1px solid var(--border-2);
border-radius: var(--r-md);
color: var(--text-0);
font-size: 14px;
font-family: inherit;
outline: none;
transition: all .15s;
}
.input.with-icon { padding-left: 40px; }
.input::placeholder { color: var(--text-3); }
.input:hover { border-color: var(--border-3); }
.input:focus {
border-color: var(--cyan);
box-shadow: 0 0 0 3px rgba(34, 211, 238, 0.12);
background: rgba(15, 23, 42, 0.9);
}
.input.ok { border-color: rgba(52, 211, 153, 0.4); }
.input.ok:focus { box-shadow: 0 0 0 3px rgba(52, 211, 153, 0.12); }
.input.error { border-color: var(--red); }
.input.error:focus { box-shadow: 0 0 0 3px rgba(248, 113, 113, 0.12); }
textarea.input { height: auto; padding: 12px 14px; resize: vertical; line-height: 1.5; }
select.input {
appearance: none;
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%2394a3b8' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'><path d='M6 9l6 6 6-6'/></svg>");
background-repeat: no-repeat;
background-position: right 14px center;
padding-right: 36px;
}
/* checkbox */
.check {
display: inline-flex;
align-items: center;
gap: 10px;
cursor: pointer;
user-select: none;
font-size: 13px;
color: var(--text-1);
}
.check input { display: none; }
.check .box {
width: 16px; height: 16px;
border: 1px solid var(--border-2);
border-radius: 4px;
background: var(--bg-1);
display: inline-flex;
align-items: center;
justify-content: center;
transition: all .15s;
flex-shrink: 0;
}
.check input:checked + .box {
background: var(--cyan);
border-color: var(--cyan);
}
.check input:checked + .box::after {
content: "✓";
color: #042f2e;
font-size: 11px;
font-weight: 700;
}
/* ==========================================================================
SECTION HEADINGS
========================================================================== */
.section-kicker {
font-family: var(--font-mono);
font-size: 12px;
color: var(--cyan);
letter-spacing: 0.15em;
text-transform: uppercase;
margin-bottom: 12px;
}
.section-title {
font-size: clamp(28px, 3.5vw, 40px);
font-weight: 700;
letter-spacing: -0.02em;
line-height: 1.15;
margin-bottom: 16px;
}
.section-sub {
color: var(--text-2);
font-size: 16px;
line-height: 1.6;
}
/* ==========================================================================
TABLES
========================================================================== */
.tbl {
width: 100%;
font-size: 13px;
border-collapse: collapse;
}
.tbl th {
text-align: left;
color: var(--text-3);
font-weight: 500;
padding: 12px 14px;
border-bottom: 1px solid var(--border);
text-transform: uppercase;
font-size: 10px;
letter-spacing: 0.1em;
}
.tbl td {
padding: 14px;
border-bottom: 1px solid rgba(30, 41, 59, 0.5);
color: var(--text-1);
}
.tbl tr:last-child td { border-bottom: none; }
.tbl tr:hover td { background: rgba(15, 23, 42, 0.4); }
.tbl td.mono, .tbl th.mono { font-family: var(--font-mono); }
/* ==========================================================================
CODE BLOCKS
========================================================================== */
.code-frame {
background: var(--bg-code);
border: 1px solid var(--border);
border-radius: var(--r-lg);
overflow: hidden;
box-shadow: var(--shadow-lg);
}
.code-head {
display: flex;
align-items: center;
height: 40px;
padding: 0 16px;
border-bottom: 1px solid var(--border);
background: rgba(15, 23, 42, 0.8);
gap: 10px;
}
.traffic {
display: flex;
gap: 6px;
}
.traffic span {
width: 10px;
height: 10px;
border-radius: 50%;
background: #475569;
}
.code-body {
padding: 22px 26px;
font-family: var(--font-mono);
font-size: 13px;
line-height: 1.75;
color: var(--text-1);
overflow-x: auto;
}
.code-body .line { display: flex; gap: 20px; }
.ln { color: var(--text-3); user-select: none; min-width: 16px; text-align: right; opacity: 0.5; }
/* syntax */
.kw { color: #c084fc; }
.str { color: #86efac; }
.num { color: #fbbf24; }
.com { color: #64748b; font-style: italic; }
.fn { color: #22d3ee; }
.prop{ color: #f0abfc; }
.var-v { color: #f8fafc; }
.flag{ color: #fb923c; }
.bash-prompt { color: var(--cyan); user-select: none; }
/* ==========================================================================
PROVIDER-BRAND HELPERS
========================================================================== */
.provider {
display: inline-flex;
align-items: center;
gap: 6px;
font-family: var(--font-mono);
font-size: 12px;
}
.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); }
.provider.codex .dot { background: var(--p-codex); }
/* ==========================================================================
UTILITIES
========================================================================== */
.stack-xs { display: flex; flex-direction: column; gap: 8px; }
.stack-sm { display: flex; flex-direction: column; gap: 12px; }
.stack-md { display: flex; flex-direction: column; gap: 20px; }
.stack-lg { display: flex; flex-direction: column; gap: 32px; }
.row { display: flex; align-items: center; gap: 12px; }
.row-sm { gap: 8px; }
.row-lg { gap: 20px; }
.row-between { justify-content: space-between; }
.row-center { justify-content: center; }
.row-wrap { flex-wrap: wrap; }
.flex-1 { flex: 1; }
.ml-auto { margin-left: auto; }
.mt-auto { margin-top: auto; }
.text-0 { color: var(--text-0); }
.text-1 { color: var(--text-1); }
.text-2 { color: var(--text-2); }
.text-3 { color: var(--text-3); }
.text-cyan { color: var(--cyan); }
.text-purple { color: var(--purple); }
.text-amber { color: var(--amber); }
.text-green { color: var(--green); }
.text-red { color: var(--red); }
.text-xs { font-size: 11px; }
.text-sm { font-size: 13px; }
.text-md { font-size: 14px; }
.text-lg { font-size: 16px; }
.text-xl { font-size: 20px; }
.text-2xl { font-size: 28px; }
.text-3xl { font-size: 36px; }
.fw-400 { font-weight: 400; }
.fw-500 { font-weight: 500; }
.fw-600 { font-weight: 600; }
.fw-700 { font-weight: 700; }
.fw-800 { font-weight: 800; }
.tabular { font-variant-numeric: tabular-nums; }
.truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* ==========================================================================
APP SHELL (for dashboard-style pages)
========================================================================== */
.app-shell {
display: grid;
grid-template-columns: 240px 1fr;
min-height: 100vh;
position: relative;
z-index: 2;
}
.app-side {
border-right: 1px solid var(--border);
background: rgba(2, 6, 23, 0.6);
padding: 20px 14px;
display: flex;
flex-direction: column;
gap: 28px;
position: sticky;
top: 0;
height: 100vh;
overflow-y: auto;
}
.app-side .brand { padding: 6px 10px 14px; }
.side-group { display: flex; flex-direction: column; gap: 2px; }
.side-label {
font-size: 10px;
color: var(--text-3);
text-transform: uppercase;
letter-spacing: 0.12em;
padding: 0 10px 8px;
font-family: var(--font-mono);
}
.side-item {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 10px;
border-radius: var(--r-sm);
font-size: 13px;
color: var(--text-2);
cursor: pointer;
transition: all .12s;
}
.side-item:hover { color: var(--text-0); background: rgba(255,255,255,0.03); }
.side-item.active { background: rgba(34, 211, 238, 0.08); color: var(--cyan); }
.side-item .ico {
width: 16px; height: 16px; opacity: 0.8;
display: inline-flex; align-items: center; justify-content: center;
flex-shrink: 0;
}
.side-item .count {
margin-left: auto;
font-size: 11px;
color: var(--text-3);
font-family: var(--font-mono);
}
.side-item.active .count { color: var(--cyan); }
.app-main {
min-width: 0; /* allow grid children to shrink */
display: flex;
flex-direction: column;
}
.app-topbar {
height: 60px;
border-bottom: 1px solid var(--border);
display: flex;
align-items: center;
padding: 0 32px;
gap: 16px;
position: sticky;
top: 0;
z-index: 10;
background: rgba(10, 14, 26, 0.75);
backdrop-filter: blur(12px);
}
.app-topbar h1 {
font-size: 18px;
font-weight: 600;
letter-spacing: -0.01em;
}
.app-content {
padding: 32px;
flex: 1;
}
/* user avatar pill */
.avatar {
width: 28px; height: 28px;
border-radius: 50%;
background: linear-gradient(135deg, #22d3ee, #a855f7);
display: inline-flex;
align-items: center;
justify-content: center;
color: #042f2e;
font-weight: 700;
font-size: 12px;
flex-shrink: 0;
}
/* ==========================================================================
KBD
========================================================================== */
kbd {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 20px;
height: 20px;
padding: 0 6px;
font-family: var(--font-mono);
font-size: 11px;
font-weight: 500;
color: var(--text-1);
background: var(--bg-1);
border: 1px solid var(--border-2);
border-bottom-width: 2px;
border-radius: 4px;
line-height: 1;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 337 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 183 KiB

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,425 @@
# PURO AI · Landing + Auth 重设计v2
> 2026-04-19 · 分支 `feat/design-landing-auth` · **v2 更新Claude Design 产出 10 页 + puro.css 后,定了路径 B 执行**
本文档是「PURO AI 公开页面重设计」的设计 spec
- **Stage 1完成**:信息架构 + 中文文案 + 风格方向 + 布局选型
- **Stage 2完成**:在 claude.ai/design 出了 10 页视觉稿 + `puro.css` 设计系统,归档到 `docs/design-drafts/v2/`
- **Stage 3本期范围**:挑 **4 个页面** 落地 — Landing精修版、Login、Register、Docs精简版+ 设计 tokens 落地到 Tailwind
- **Stage 4**merge → Drone CI → ai.puro.im 实机验证
## 本期范围决策(路径 B · 分层交付)
### 本期做feat/design-landing-auth
1. **puro.css 落地为 Tailwind config + global styles**`tailwind.config.ts` 扩展 color/radius/font`puro.css` 挪成 `frontend/src/assets/puro.css` 全局引入)
2. **Landing 页**(路由 `/` 未登录态)· 6 段结构,**精修版文案**(见第 3 节)
3. **Login / Register 页** 套新左右分栏 + 保留现有 OAuth/Turnstile/2FA 逻辑
4. **Docs 页精简版**(路由 `/docs`,公开访问)· 快速接入 + curl 示例 + 支持模型
### 二期做(另开 feat/design-dashboard 分支,不本期)
5. Dashboard 换皮(沿用 puro.css
6. API Keys 管理页换皮
7. Design System 页(给团队内部看的)
### 永不做 / 远期再说
- **Binding 页**Claude Design 预设用户自己 BYO-Subscription 绑定,但 Sub2API 是 admin 统一管账号池,概念不符
- **Pricing 页**iShare 接管钱包/订阅后由 iShare 处理
- 注册送 $5 / 充值阶梯赠送等"赠送经济"特性
---
## 1. 项目背景
| 项 | 值 |
|---|---|
| **公开品牌名** | **PURO AI** |
| **内部代码名** | sub2apiWei-Shaw/sub2api fork不改 |
| **域名** | https://ai.puro.im |
| **现状** | 登录后是 Vue 3 + Tailwind 后台;无公开首页;登录页用浅色 `AuthLayout` |
| **目标受众** | 个人开发者 / 小团队 — 已有 ChatGPT Plus / Claude Pro / Codex / Gemini 订阅,想程序化调用而不付 API 费率 |
| **核心叙事** | "你的 AI 订阅,已经付过钱了"——把已付订阅复用为 API |
---
## 2. 风格方向
**暗黑科技**Dark Tech—— 对标 Linear / Vercel / Railway / Supabase / Cloudflare Workers。
### 配色(建议)
| 角色 | 色值 | 用途 |
|---|---|---|
| 主底 | `#0a0e1a` ~ `#0f172a` | 页面背景slate-950 区间 |
| 卡片底 | `#0f172a` | 表单卡片、特性卡 |
| 边框 | `#334155` | 次要边框 |
| 主文 | `#f8fafc` | 标题 |
| 副文 | `#94a3b8` ~ `#cbd5e1` | 描述、菜单 |
| **主品牌色** | `#22d3ee`cyan-400 | Logo、CTA、链接 |
| **辅品牌色** | `#a855f7`purple-500 | 渐变叠加、装饰光晕 |
| 警示 | `#fbbf24`amber-400 | "💡"标签、数字对比 |
### 视觉语汇
- 暗底上**圆形 radial gradient 光晕**(青/紫双色)
- 等宽字体ui-monospace / SF Mono用于 code demo
- 主体字体sans-serifInter / SF Pro / 系统默认)
- 边框 1px 实线 / 关键分割用 dashed
- CTA 圆角 8px卡片圆角 12px
- 不要拟物、不要软阴影、不要 Bootstrap 4 那种 gradient 按钮
### 排版氛围
- 大量留白
- 标题大、字距稍紧letter-spacing -0.02em
- 内容居中收敛max-width ~1100px
---
## 3. Landing 页(路由 `/`,未登录态)· 精修版
### 3.1 信息架构6 段,剔除 Pricing/FAQ/CTA banner
```
NAV · ⬢ PURO AI · 产品 · 文档 · [登录][免费试用 →]
① HERO · 主标 + 副标 + CTA×2 + 微文案
② 模型墙 · 4 个支持的 AI 平台
③ 三特性 · ⚡ 一个 key 多模型 · 🔄 账号池高可用 · 📊 用量看板
④ Code Demo · codex config 片段 + curl 示例
⑤ Dashboard · 真实 mockup 预览(不用截图了,设计稿里是 HTML 渲染的)
⑥ Footer · 4 列(品牌 / 产品 / 资源 / 联系)
```
### 3.2 完整中文文案
#### NAV
- Logo: `⬢ PURO AI`
- 菜单: 产品 · 文档
- 右侧: `[登录]`(边框)`[免费试用 →]`cyan 实底Nav 里保留注册入口,但 Hero CTA 不走这里)
#### ① HERO
- **主标**: 你的 AI 订阅,**已经付过钱了。**
- **副标**: Claude Pro · ChatGPT Plus · Codex · Gemini 订阅<br>聚合成统一 API零改动接入 OpenAI / Anthropic SDK
- **主 CTA**: `登录 →`cyan 实底,已有账号用户直接进)
- **副 CTA**: `联系咨询`边框mailto:admin@puro.im 或未来跳 iShare 咨询页)
- **微文案**: 已验证可用 Codex CLI · Claude Code · curl · 服务器出口新加坡
#### ② 模型墙
- **小标题**: 支持的 AI 平台
- **副标**: 通过 OAuth 直接复用你的订阅,无需申请官方 API key
- **Logos**:
- ⚪ Claude Pro / Max
- 🟢 ChatGPT Plus / Pro
- 🟡 Codex CLI
- 🔵 Gemini Code Assist
- ⚫ 更多(规划中,灰显)
#### ③ 三大特性
| 图标 | 标题 | 描述 |
|---|---|---|
| ⚡ | 一个 key 接所有模型 | 不再为每个 provider 申请 API key、配置 base_url。统一 sk- 走 Claude / GPT / Gemini按 model 自动路由到对应账号池。|
| 🔄 | 账号池高可用 | 支持多账号自动调度与 failover。某个上游触发限流 / 冷却时流量切到下一个健康账号token 刷新全自动。|
| 📊 | 用量看板 | 每条请求的 tokens、费用、上游账号、延迟全可视化。模型分布饼图 + 趋势曲线 + Top 排行。|
#### ④ Code Demo
- **标题**: 把 base_url 一改,就能用
- **副**: 兼容 OpenAI / Anthropic / Gemini SDK**零代码改动**
- **代码块**:
```toml
# Codex CLI
# ~/.codex/config.toml
[model_providers.OpenAI]
base_url = "https://ai.puro.im"
wire_api = "responses"
```
```bash
# 或直接 curl
$ curl https://ai.puro.im/responses \
-H "Authorization: Bearer sk-xxx" \
-d '{"model":"gpt-5.4","input":"hello"}'
```
- **底注**: 支持 OpenAI Responses API · Anthropic Messages API · Gemini generateContent · 流式 SSE & WebSocket
#### ⑤ Dashboard
- **标题**: 每条请求都看得见
- **副**: 不像第三方 API 池子那种"扣了多少不告诉你"。你能看到每次调用:扣了哪个账号、跑了哪个模型、用了多少 tokens、花了多少钱、上游响应几秒。
- **图**: 使用 Claude Design v2 产出的 **纯 HTML 渲染 mockup**stats grid + chart 卡片 + log table with provider dots见 `docs/design-drafts/v2/Landing.html` 里 `#dashboard` 段。本期 Vue 翻译时保持静态数据,不对接真实 API。
#### ⑥ Footer
| 列 | 内容 |
|---|---|
| 品牌 | ⬢ PURO AI<br>Self-hosted on puro.im<br>© 2026 puro.im · MIT License<br>fork of Wei-Shaw/sub2api |
| 产品 | 文档 · 更新日志 |
| 资源 | GitHub · API 状态 · Codex 配置示例 |
| 联系 | admin@puro.im · git.puro.im |
---
## 4. Auth 页(登录 / 注册)
### 4.1 布局选型:左右分栏
```
┌─────────────────────────┬─────────────────────┐
│ 左:品牌叙事区 │ 右:表单区 │
│ - Logo │ - 标题(登录/注册) │
│ - 主标语5→1 对比) │ - 副标 │
│ - 副文(双卖点排比) │ - email / password │
│ - 装饰光晕 cyan/purple │ - CTA │
│ - 底栏小字(支持平台) │ - 切换链接 │
└─────────────────────────┴─────────────────────┘
```
移动端:左侧叙事降级为顶部小 banner 或完全隐藏,单列表单。
### 4.2 左侧叙事文案
- **Logo**: `⬢ PURO AI`
- **主标语**:
> **5** 个订阅<br>
> → **1** 个 key
数字 `5` 用 amber/orange 强调;`1` 用主品牌色 cyan 强调。
- **副文**(三句排比):
> 省去切换账号的繁琐,<br>
> 省去为多个高昂订阅重复买单。<br>
> <small style="color:#64748b">PURO纯粹—— 让 AI 调用回归本质。</small>
- **底栏小字**: `Claude · ChatGPT · Codex · Gemini`
### 4.3 右侧表单
#### 登录页(`/login`
- 标题: 登录
- 副: 用你的 PURO AI 账户继续
- 字段:
- 📧 邮箱input, type=email, required
- 🔒 密码input, type=password, required, 带眼睛切换显示)
- 选项:
- 忘记密码router-link
- Turnstile captcha条件显示
- CTA: `登录 →`
- 分隔: ``
- OAuth 按钮(条件显示):
- 使用 LinuxDO 登录
- 使用 OIDC 登录
- 底部链接: 没有账户?**注册**
#### 注册页(`/register`
- 标题: 创建账户
- 副: 5 分钟开始用 PURO AI
- 字段:
- 📧 邮箱
- 🔒 密码
- 🔒 确认密码
- (可选)邮箱验证码(条件显示,配置 `email_verify_required` 时)
- Turnstile captcha条件
- CTA: `创建账户 →`
- 底部链接: 已有账户?**登录**
#### 其他保留页(不重设计本期)
- `/forgot-password`
- `/reset-password`
- `/verify-email`
- OAuth 回调页
---
## 4.5 Docs 页(新增 · 本期做精简版)
路由 `/docs`,公开访问(不需登录)。沿用 puro.css 设计系统。
### 结构
```
NAV (复用)
── HERO · "快速接入 PURO AI"(简短)
── § 1 · 获取 API key流程说明联系 admin / iShare
── § 2 · Codex CLI 接入config.toml 示例)
── § 3 · Claude Code 接入(~/.claude/settings.json 例)
── § 4 · curl 测试(/responses + /v1/messages 两段)
── § 5 · 支持的模型列表gpt-5.4, claude-opus-4-7, gemini-2.5-pro 等)
── § 6 · 问题反馈 mailto:admin@puro.im
FOOTER (复用)
```
### 文案原则
- 代码块用 JetBrains Mono带语法高亮
- 每段开头 1-2 句说明,然后直接上代码,不啰嗦
- 所有 base_url 用真实值:`https://ai.puro.im`
---
## 5. 给 claude.ai/design 的 briefStage 2 输入,**已执行完成 · 历史参考**
> ⚠️ **此节是喂给 Claude Design 的原始 brief**Claude Design 已按此产出 10 个页面。Stage 2 之后**内容精修 + 范围剪裁**以第 3/4/4.5 节为准,本节仅作历史存档。
复制下方文字到 https://claude.ai/design
````
我要做两个网页设计,请帮我生成高保真 HTML/React 视觉稿。
## 品牌
名字PURO AI拉丁语「纯粹」
Logo六边形 ⬢ + 文字
域名ai.puro.im
定位:把多个 AI 订阅Claude Pro / ChatGPT Plus / Codex / Gemini聚合成统一 API
核心叙事:你的 AI 订阅,已经付过钱了
## 风格
暗黑科技风,对标 Linear / Vercel / Railway。
配色:
- 主底 #0a0e1a / #0f172aslate-950 区间)
- 主品牌色 #22d3eecyan-400
- 辅品牌色 #a855f7purple-500
- 强调色 #fbbf24amber-400仅用于数字对比
- 主文 #f8fafc副文 #94a3b8 ~ #cbd5e1
- 卡片/表单底 #0f172a边框 #334155
视觉元素:
- 暗底上 radial gradient 光晕(青/紫双色60% 透明度blur
- 大量留白max-width 1100px
- 圆角CTA 8px卡片 12px
- 字体Inter 或 SF Prosans-serif代码用 ui-monospace
- 不要拟物、不要软阴影、不要 gradient 按钮
## 页面 1Landing路由 /,未登录)
6 个 section + 顶部 nav全部中文。
NAV
- 左:⬢ PURO AI
- 中:产品、文档(定价灰显)
- 右:[登录](边框)[免费试用 →]cyan 实底)
① HERO居中垂直 padding 大)
主标:你的 AI 订阅,**已经付过钱了。**
"已经付过钱了" 用 cyan 高亮)
副标Claude Pro · ChatGPT Plus · Codex · Gemini 订阅
聚合成统一 API零改动接入 OpenAI / Anthropic SDK
CTA[立即开始 →][查看文档]
微文案(小灰字):无需信用卡 · 用你已有的订阅 · 5 分钟跑通
② 模型墙
小标题:支持的 AI 平台
副:通过 OAuth 直接复用你的订阅,无需申请官方 API key
横排 5 个 logo 卡片Claude Pro/Max · ChatGPT Plus/Pro · Codex CLI · Gemini Code Assist · 更多(灰显)
③ 三特性3 列卡片)
卡片 1⚡ 一个 key 接所有模型 / 不再为每个 provider 申请 API key、配置 base_url。统一 sk- 走 Claude / GPT / Gemini按 model 自动路由到对应账号池。
卡片 2🔄 账号池高可用 / 多账号自动调度。某个 ChatGPT Plus 触发限流,自动 failover 到下一个。重启、刷新 token 全自动。
卡片 3📊 用量看板 / 每条请求的 tokens、费用、上游账号、延迟全可视化。模型分布饼图 + 趋势曲线 + Top 排行。
④ Code Demo
标题:把 base_url 一改,就能用
副:兼容 OpenAI / Anthropic / Gemini SDK零代码改动
代码块(深色 terminal 配色syntax highlight
- 上方一段 tomlcodex config
- 下方一段 bashcurl 示例)
底注小字:支持 OpenAI Responses API · Anthropic Messages API · Gemini generateContent · 流式 SSE & WebSocket
⑤ Dashboard
标题:每条请求都看得见
副:不像第三方 API 池子那种"扣了多少不告诉你"。你能看到每次调用:扣了哪个账号、跑了哪个模型、用了多少 tokens、花了多少钱、上游响应几秒。
dashboard 截图占位(深色饼图 + 折线图 + 表格)
⑥ Footer
4 列:
- 品牌列:⬢ PURO AI / Self-hosted on puro.im / © 2026 puro.im · MIT / fork of Wei-Shaw/sub2api
- 产品列:文档 · 套餐(暂隐)· 更新日志
- 资源列GitHub · API 状态 · Codex 配置示例
- 联系列admin@puro.im · git.puro.im
## 页面 2登录页路由 /login
左右分栏,桌面端 50/50 分。
左侧(叙事区):
- 顶部 Logo⬢ PURO AI
- 中部主标5 个订阅 → 1 个 key
5 用 amber #fbbf24 强调1 用 cyan #22d3ee 强调,字号 36-48pxweight 800
- 主标下副文(三句排比):
省去切换账号的繁琐,
省去为多个高昂订阅重复买单。
PURO纯粹—— 让 AI 调用回归本质。
- 底部小字Claude · ChatGPT · Codex · Gemini
- 背景linear-gradient(135deg, #0a0e1a, #1e1b4b) + 角落 radial gradient 光晕cyan + purple
右侧(表单区):
- 标题:登录
- 副:用你的 PURO AI 账户继续
- 邮箱输入(带 📧 icon
- 密码输入(带 🔒 icon + 眼睛切换显示)
- 行:忘记密码?(右对齐链接)
- 主 CTA登录 →cyan 实底)
- 分隔:—— 或 ——
- OAuth 按钮:使用 LinuxDO 登录(边框样式)
- 底部:没有账户?注册(链接)
移动端:左侧叙事区收为顶部小 banner只保留 Logo + 短主标),表单全宽。
## 页面 3注册页路由 /register
和登录页同布局,右侧表单字段:
- 标题:创建账户 / 副5 分钟开始用 PURO AI
- 邮箱、密码、确认密码
- 主 CTA创建账户 →
- 底部:已有账户?登录
请生成完整可预览的 HTML含 inline CSS或 React 组件。
````
---
## 5.5 Stage 2 产出清单(已完成,参考用)
`docs/design-drafts/v2/` 目录下:
| 文件 | 用途 | 本期用 |
|---|---|---|
| `puro.css` | 设计系统tokens + primitives .btn/.card/.input 等) | ✅ 全站落地 |
| `Landing.html` | 营销首页7 段,我们只取 6 段) | ✅ 参考翻译 |
| `Login.html` | 登录页 | ✅ 参考翻译 |
| `Register.html` | 注册页 | ✅ 参考翻译 |
| `Docs.html` | 文档页(精简版参考) | ✅ 参考翻译 |
| `Dashboard.html` | 控制台首页 | ⏳ 二期 |
| `API Keys.html` | Key 管理 | ⏳ 二期 |
| `Design System.html` | 设计系统展示页 | ⏳ 二期(给团队看)|
| `Binding.html` | 订阅绑定 | ❌ 不做(架构不符)|
| `Pricing.html` | 定价 | ❌ 不做iShare 管) |
| `HANDOFF.md` | 交付文档(含后端契约,部分不适用) | 参考但不全部实现 |
---
## 6. Stage 3 实施约束(给未来的我看)
技术栈(不改动):
- Vue 3.4+ Composition API + TypeScript
- Tailwind CSS已配 dark mode、`primary-*` 色板)
- Vite 5
- Vue Router 4 / Pinia / vue-i18n
- 现有组件库(`@/components/common`、`@/components/layout/AuthLayout`
实施要点:
1. **puro.css 落地**
- CSS 变量(--bg-0 等)保留作为全局 tokens挪到 `frontend/src/assets/puro.css`
- `main.ts` 里 `import './assets/puro.css'`
- 在 `tailwind.config.ts` 的 `theme.extend.colors` 里同步一份 cyan/purple/amber/provider 色值,方便 Vue 组件里用 Tailwind class
- primitives.btn / .card / .input可作为全局 class 直接用,也可以包 Vue 组件;**本期直接用 class不抽组件**
2. **Landing 页是新页**:新建 `frontend/src/views/landing/HomeView.vue`;改 `router/index.ts`,未登录访问 `/` 显示 landing已登录跳 `/dashboard`
3. **Auth 页改造**:改 `frontend/src/components/layout/AuthLayout.vue` 为左右分栏;改 `LoginView.vue` / `RegisterView.vue` 适配新 layout**保留所有现有逻辑**OAuth、Turnstile、2FA、表单校验
4. **Docs 页是新页**:新建 `frontend/src/views/docs/DocsView.vue`,路由 `/docs`public 可访问
5. **i18n**:新文案进 `frontend/src/i18n/locales/zh.ts`;本期只补中文(默认语言),英文 key 留空 / 复用现有
6. **Dashboard mockup in Landing**:直接按 Claude Design 的 HTML 翻成 Vue 静态标记(不对接真实数据,纯展示)
7. **不动的**Setup Wizard / 后台所有页面 / API 层 / store
---
## 7. 验收标准
本地 previewhttp://127.0.0.1:4173已过
- [x] `puro.css` 已引入为全局样式,`--cyan: #22d3ee` 等变量在 DevTools :root 可见
- [x] 未登录访问 / → LandingView 呈现route meta.redirectIfAuth=/dashboard 生效分支)
- [x] 已登录访问 / → guard 跳 /dashboard
- [x] Landing 6 个 section 内容全部呈现Nav + Hero + Models + Features + Code Demo + Dashboard mockup + Footer移动端可堆叠
- [x] Landing ⑤ Dashboard mockup 为静态 HTML + 内嵌 SVG无后端依赖
- [x] /login 左右分栏布局narrative "5→1" 文案 + 登录表单 / heading "登录"
- [x] /register 左右分栏heading "创建账户" / "5 分钟开始用 PURO AI"
- [x] /docs 公开访问,含 curl / Codex config.toml / Claude Code settings.json 示例
- [x] 所有现有 auth 功能代码未改动OAuth section / Turnstile / 2FA modal / password toggle 均在原位)
- [x] 后台 /dashboard 等页面使用 style.css 的 .btn/.input/.cardpuro.css 已 scoped 到 .puro-page不污染
- [x] `pnpm run typecheck` 0 error`pnpm run build` 成功
- [ ] CI 构建通过,部署后 ai.puro.im 加载正常Task 13 完成)

View File

@@ -4,6 +4,9 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/png" href="/logo.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<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">
<title>Sub2API - AI API Gateway</title>
</head>
<body>

View File

@@ -0,0 +1,729 @@
/* ==========================================================================
PURO AI — Design System
Shared tokens + primitive styles used across every page.
--------------------------------------------------------------------------
Usage: <link rel="stylesheet" href="puro.css">
========================================================================== */
*, *::before, *::after { box-sizing: border-box; }
.puro-page, .puro-page *, .puro-page *::before, .puro-page *::after { margin: 0; padding: 0; }
:root {
/* Surfaces */
--bg-0: #0a0e1a; /* page */
--bg-1: #0f172a; /* raised */
--bg-2: #111827; /* card alt */
--bg-code: #020617; /* code canvas */
/* Borders */
--border: #1e293b;
--border-2: #334155;
--border-3: #475569;
/* Text */
--text-0: #f8fafc; /* primary */
--text-1: #cbd5e1; /* body */
--text-2: #94a3b8; /* muted */
--text-3: #64748b; /* hint */
/* Accents */
--cyan: #22d3ee;
--cyan-2: #67e8f9;
--cyan-dim: #0891b2;
--purple: #a855f7;
--amber: #fbbf24;
--green: #34d399;
--red: #f87171;
--orange: #fb923c;
/* Provider brand dots */
--p-claude: #d97757;
--p-gpt: #10a37f;
--p-gemini: #4285f4;
--p-codex: #f0a030;
/* Radius */
--r-sm: 6px;
--r-md: 8px;
--r-lg: 12px;
--r-xl: 16px;
/* Shadow */
--shadow-lg: 0 30px 60px -30px rgba(0,0,0,0.6);
--shadow-xl: 0 40px 80px -40px rgba(0,0,0,0.8);
/* Typography */
--font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
--font-mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
}
/* Merged from original html/body rules — applied to .puro-page container root */
.puro-page {
background: var(--bg-0);
color: var(--text-0);
font-family: var(--font-sans);
font-feature-settings: "cv11", "ss01", "ss03";
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
line-height: 1.5;
overflow-x: hidden;
}
.puro-page a { color: inherit; text-decoration: none; }
.puro-page button { font-family: inherit; cursor: pointer; border: none; background: none; color: inherit; }
/* scrollbar — subtle */
.puro-page::-webkit-scrollbar { width: 10px; height: 10px; }
.puro-page::-webkit-scrollbar-track { background: transparent; }
.puro-page::-webkit-scrollbar-thumb { background: var(--border-2); border-radius: 6px; }
.puro-page::-webkit-scrollbar-thumb:hover { background: var(--border-3); }
.puro-page .mono { font-family: var(--font-mono); }
/* ==========================================================================
BACKGROUND EFFECTS
========================================================================== */
.puro-page .bg-glow {
position: fixed;
inset: 0;
pointer-events: none;
z-index: 0;
overflow: hidden;
}
.puro-page .bg-glow::before,
.puro-page .bg-glow::after {
content: "";
position: absolute;
width: 900px;
height: 900px;
border-radius: 50%;
filter: blur(120px);
opacity: 0.35;
}
.puro-page .bg-glow::before {
background: radial-gradient(circle, #22d3ee 0%, transparent 60%);
top: -300px;
left: -200px;
}
.puro-page .bg-glow::after {
background: radial-gradient(circle, #a855f7 0%, transparent 60%);
top: 200px;
right: -300px;
opacity: 0.25;
}
.puro-page .bg-glow.soft::before, .puro-page .bg-glow.soft::after { opacity: 0.15; }
.puro-page .grain {
position: fixed;
inset: 0;
pointer-events: none;
z-index: 1;
opacity: 0.4;
mix-blend-mode: overlay;
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='200' height='200'><filter id='n'><feTurbulence baseFrequency='0.9' numOctaves='2'/></filter><rect width='100%25' height='100%25' filter='url(%23n)' opacity='0.35'/></svg>");
}
.puro-page .container {
max-width: 1100px;
margin: 0 auto;
padding: 0 32px;
position: relative;
z-index: 2;
}
.puro-page .container-wide { max-width: 1280px; }
.puro-page .container-narrow { max-width: 860px; }
/* ==========================================================================
NAV
========================================================================== */
.puro-page .nav {
position: sticky;
top: 0;
z-index: 50;
backdrop-filter: blur(16px);
background: rgba(10, 14, 26, 0.72);
border-bottom: 1px solid var(--border);
}
.puro-page .nav-inner {
display: flex;
align-items: center;
height: 64px;
gap: 48px;
}
.puro-page .brand {
display: flex;
align-items: center;
gap: 10px;
font-weight: 700;
font-size: 15px;
letter-spacing: 0.02em;
}
.puro-page .hex {
width: 22px;
height: 22px;
color: var(--cyan);
}
.puro-page .nav-links {
display: flex;
gap: 28px;
font-size: 14px;
color: var(--text-2);
}
.puro-page .nav-links a { transition: color .15s; }
.puro-page .nav-links a:hover, .puro-page .nav-links a.active { color: var(--text-0); }
.puro-page .nav-links .disabled { color: var(--text-3); cursor: not-allowed; display: inline-flex; align-items: center; gap: 6px; }
.puro-page .nav-links .disabled::after {
content: "即将推出";
font-size: 10px;
padding: 2px 6px;
border: 1px solid var(--border-2);
border-radius: 4px;
color: var(--text-3);
}
.puro-page .nav-cta {
margin-left: auto;
display: flex;
gap: 10px;
align-items: center;
}
/* ==========================================================================
BUTTONS
========================================================================== */
.puro-page .btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
padding: 8px 14px;
font-size: 13px;
font-weight: 500;
border-radius: var(--r-md);
transition: all .15s;
white-space: nowrap;
border: 1px solid transparent;
}
.puro-page .btn-primary {
background: var(--cyan);
color: #042f2e;
font-weight: 600;
}
.puro-page .btn-primary:hover { background: var(--cyan-2); }
.puro-page .btn-primary:active { transform: translateY(1px); }
.puro-page .btn-primary:disabled { opacity: 0.5; cursor: not-allowed; }
.puro-page .btn-ghost {
border-color: var(--border-2);
color: var(--text-1);
}
.puro-page .btn-ghost:hover { border-color: var(--border-3); color: var(--text-0); background: rgba(255,255,255,0.02); }
.puro-page .btn-subtle {
background: rgba(255,255,255,0.04);
color: var(--text-1);
border-color: transparent;
}
.puro-page .btn-subtle:hover { background: rgba(255,255,255,0.08); color: var(--text-0); }
.puro-page .btn-danger {
background: rgba(248, 113, 113, 0.1);
color: var(--red);
border-color: rgba(248, 113, 113, 0.25);
}
.puro-page .btn-danger:hover { background: rgba(248, 113, 113, 0.15); border-color: rgba(248, 113, 113, 0.4); }
.puro-page .btn-lg { padding: 12px 20px; font-size: 14px; }
.puro-page .btn-sm { padding: 5px 10px; font-size: 12px; }
.puro-page .btn-icon { padding: 7px; aspect-ratio: 1; }
.puro-page .btn .spinner {
width: 14px; height: 14px;
border: 2px solid rgba(0,0,0,0.2);
border-top-color: currentColor;
border-radius: 50%;
animation: spin .7s linear infinite;
display: none;
}
.puro-page .btn.loading .spinner { display: inline-block; }
.puro-page .btn.loading .label { opacity: 0.5; }
@keyframes spin { to { transform: rotate(360deg); } }
/* ==========================================================================
BADGES / PILLS / CHIPS
========================================================================== */
.puro-page .badge {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 2px 8px;
border-radius: 100px;
font-size: 11px;
font-weight: 600;
letter-spacing: 0.02em;
background: rgba(34, 211, 238, 0.1);
color: var(--cyan);
}
.puro-page .badge.amber { background: rgba(251, 191, 36, 0.12); color: var(--amber); }
.puro-page .badge.purple { background: rgba(168, 85, 247, 0.12); color: var(--purple); }
.puro-page .badge.green { background: rgba(52, 211, 153, 0.12); color: var(--green); }
.puro-page .badge.red { background: rgba(248, 113, 113, 0.12); color: var(--red); }
.puro-page .badge.muted { background: rgba(255, 255, 255, 0.04); color: var(--text-2); border: 1px solid var(--border); }
.puro-page .pill {
display: inline-block;
padding: 2px 8px;
border-radius: var(--r-sm);
background: rgba(255,255,255,0.04);
border: 1px solid var(--border);
font-family: var(--font-mono);
font-size: 12px;
color: var(--text-0);
}
.puro-page .chip {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 4px 10px;
border-radius: 100px;
background: rgba(15, 23, 42, 0.6);
border: 1px solid var(--border);
font-size: 12px;
color: var(--text-1);
font-family: var(--font-mono);
}
.puro-page .chip .dot { width: 6px; height: 6px; border-radius: 50%; background: var(--green); }
.puro-page .chip.claude .dot { background: var(--p-claude); }
.puro-page .chip.gpt .dot { background: var(--p-gpt); }
.puro-page .chip.gemini .dot { background: var(--p-gemini); }
.puro-page .chip.codex .dot { background: var(--p-codex); }
.puro-page .dot-sep { width: 4px; height: 4px; border-radius: 50%; background: var(--text-3); display: inline-block; }
/* status chip (tiny dot absolute-positioned) */
.puro-page .status-chip {
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--green);
box-shadow: 0 0 0 3px rgba(52, 211, 153, 0.15);
display: inline-block;
}
.puro-page .status-chip.dim { background: var(--text-3); box-shadow: none; }
.puro-page .status-chip.amber { background: var(--amber); box-shadow: 0 0 0 3px rgba(251, 191, 36, 0.15); }
.puro-page .status-chip.red { background: var(--red); box-shadow: 0 0 0 3px rgba(248, 113, 113, 0.15); }
/* ==========================================================================
CARDS / SURFACES
========================================================================== */
.puro-page .card {
background: rgba(15, 23, 42, 0.6);
border: 1px solid var(--border);
border-radius: var(--r-lg);
padding: 24px;
}
.puro-page .card-raised {
background: var(--bg-1);
border: 1px solid var(--border);
border-radius: var(--r-lg);
}
.puro-page .card-interactive {
transition: all .2s;
cursor: pointer;
}
.puro-page .card-interactive:hover {
border-color: var(--border-2);
background: rgba(15, 23, 42, 0.85);
transform: translateY(-2px);
}
.puro-page .divider { height: 1px; background: var(--border); margin: 24px 0; border: 0; }
.puro-page .divider-dashed { border: 0; border-top: 1px dashed var(--border); margin: 20px 0; }
/* ==========================================================================
FORMS
========================================================================== */
.puro-page .field { margin-bottom: 18px; }
.puro-page .field-label {
display: block;
font-size: 12px;
font-weight: 500;
color: var(--text-1);
margin-bottom: 8px;
}
.puro-page .field-hint {
font-size: 12px;
color: var(--text-3);
margin-top: 6px;
}
.puro-page .field-error {
font-size: 12px;
color: var(--red);
margin-top: 6px;
}
.puro-page .input-wrap { position: relative; }
.puro-page .input-wrap .icon {
position: absolute;
left: 14px;
top: 50%;
transform: translateY(-50%);
color: var(--text-3);
pointer-events: none;
display: inline-flex;
}
.puro-page .input {
width: 100%;
height: 42px;
padding: 0 14px;
background: rgba(15, 23, 42, 0.6);
border: 1px solid var(--border-2);
border-radius: var(--r-md);
color: var(--text-0);
font-size: 14px;
font-family: inherit;
outline: none;
transition: all .15s;
}
.puro-page .input.with-icon { padding-left: 40px; }
.puro-page .input::placeholder { color: var(--text-3); }
.puro-page .input:hover { border-color: var(--border-3); }
.puro-page .input:focus {
border-color: var(--cyan);
box-shadow: 0 0 0 3px rgba(34, 211, 238, 0.12);
background: rgba(15, 23, 42, 0.9);
}
.puro-page .input.ok { border-color: rgba(52, 211, 153, 0.4); }
.puro-page .input.ok:focus { box-shadow: 0 0 0 3px rgba(52, 211, 153, 0.12); }
.puro-page .input.error { border-color: var(--red); }
.puro-page .input.error:focus { box-shadow: 0 0 0 3px rgba(248, 113, 113, 0.12); }
.puro-page textarea.input { height: auto; padding: 12px 14px; resize: vertical; line-height: 1.5; }
.puro-page select.input {
appearance: none;
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%2394a3b8' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'><path d='M6 9l6 6 6-6'/></svg>");
background-repeat: no-repeat;
background-position: right 14px center;
padding-right: 36px;
}
/* checkbox */
.puro-page .check {
display: inline-flex;
align-items: center;
gap: 10px;
cursor: pointer;
user-select: none;
font-size: 13px;
color: var(--text-1);
}
.puro-page .check input { display: none; }
.puro-page .check .box {
width: 16px; height: 16px;
border: 1px solid var(--border-2);
border-radius: 4px;
background: var(--bg-1);
display: inline-flex;
align-items: center;
justify-content: center;
transition: all .15s;
flex-shrink: 0;
}
.puro-page .check input:checked + .box {
background: var(--cyan);
border-color: var(--cyan);
}
.puro-page .check input:checked + .box::after {
content: "✓";
color: #042f2e;
font-size: 11px;
font-weight: 700;
}
/* ==========================================================================
SECTION HEADINGS
========================================================================== */
.puro-page .section-kicker {
font-family: var(--font-mono);
font-size: 12px;
color: var(--cyan);
letter-spacing: 0.15em;
text-transform: uppercase;
margin-bottom: 12px;
}
.puro-page .section-title {
font-size: clamp(28px, 3.5vw, 40px);
font-weight: 700;
letter-spacing: -0.02em;
line-height: 1.15;
margin-bottom: 16px;
}
.puro-page .section-sub {
color: var(--text-2);
font-size: 16px;
line-height: 1.6;
}
/* ==========================================================================
TABLES
========================================================================== */
.puro-page .tbl {
width: 100%;
font-size: 13px;
border-collapse: collapse;
}
.puro-page .tbl th {
text-align: left;
color: var(--text-3);
font-weight: 500;
padding: 12px 14px;
border-bottom: 1px solid var(--border);
text-transform: uppercase;
font-size: 10px;
letter-spacing: 0.1em;
}
.puro-page .tbl td {
padding: 14px;
border-bottom: 1px solid rgba(30, 41, 59, 0.5);
color: var(--text-1);
}
.puro-page .tbl tr:last-child td { border-bottom: none; }
.puro-page .tbl tr:hover td { background: rgba(15, 23, 42, 0.4); }
.puro-page .tbl td.mono, .puro-page .tbl th.mono { font-family: var(--font-mono); }
/* ==========================================================================
CODE BLOCKS
========================================================================== */
.puro-page .code-frame {
background: var(--bg-code);
border: 1px solid var(--border);
border-radius: var(--r-lg);
overflow: hidden;
box-shadow: var(--shadow-lg);
}
.puro-page .code-head {
display: flex;
align-items: center;
height: 40px;
padding: 0 16px;
border-bottom: 1px solid var(--border);
background: rgba(15, 23, 42, 0.8);
gap: 10px;
}
.puro-page .traffic {
display: flex;
gap: 6px;
}
.puro-page .traffic span {
width: 10px;
height: 10px;
border-radius: 50%;
background: #475569;
}
.puro-page .code-body {
padding: 22px 26px;
font-family: var(--font-mono);
font-size: 13px;
line-height: 1.75;
color: var(--text-1);
overflow-x: auto;
}
.puro-page .code-body .line { display: flex; gap: 20px; }
.puro-page .ln { color: var(--text-3); user-select: none; min-width: 16px; text-align: right; opacity: 0.5; }
/* syntax */
.puro-page .kw { color: #c084fc; }
.puro-page .str { color: #86efac; }
.puro-page .num { color: #fbbf24; }
.puro-page .com { color: #64748b; font-style: italic; }
.puro-page .fn { color: #22d3ee; }
.puro-page .prop { color: #f0abfc; }
.puro-page .var-v { color: #f8fafc; }
.puro-page .flag { color: #fb923c; }
.puro-page .bash-prompt { color: var(--cyan); user-select: none; }
/* ==========================================================================
PROVIDER-BRAND HELPERS
========================================================================== */
.puro-page .provider {
display: inline-flex;
align-items: center;
gap: 6px;
font-family: var(--font-mono);
font-size: 12px;
}
.puro-page .provider .dot { width: 6px; height: 6px; border-radius: 50%; }
.puro-page .provider.claude .dot { background: var(--p-claude); }
.puro-page .provider.gpt .dot { background: var(--p-gpt); }
.puro-page .provider.gemini .dot { background: var(--p-gemini); }
.puro-page .provider.codex .dot { background: var(--p-codex); }
/* ==========================================================================
UTILITIES
========================================================================== */
.puro-page .stack-xs { display: flex; flex-direction: column; gap: 8px; }
.puro-page .stack-sm { display: flex; flex-direction: column; gap: 12px; }
.puro-page .stack-md { display: flex; flex-direction: column; gap: 20px; }
.puro-page .stack-lg { display: flex; flex-direction: column; gap: 32px; }
.puro-page .row { display: flex; align-items: center; gap: 12px; }
.puro-page .row-sm { gap: 8px; }
.puro-page .row-lg { gap: 20px; }
.puro-page .row-between { justify-content: space-between; }
.puro-page .row-center { justify-content: center; }
.puro-page .row-wrap { flex-wrap: wrap; }
.puro-page .flex-1 { flex: 1; }
.puro-page .ml-auto { margin-left: auto; }
.puro-page .mt-auto { margin-top: auto; }
.puro-page .text-0 { color: var(--text-0); }
.puro-page .text-1 { color: var(--text-1); }
.puro-page .text-2 { color: var(--text-2); }
.puro-page .text-3 { color: var(--text-3); }
.puro-page .text-cyan { color: var(--cyan); }
.puro-page .text-purple { color: var(--purple); }
.puro-page .text-amber { color: var(--amber); }
.puro-page .text-green { color: var(--green); }
.puro-page .text-red { color: var(--red); }
.puro-page .text-xs { font-size: 11px; }
.puro-page .text-sm { font-size: 13px; }
.puro-page .text-md { font-size: 14px; }
.puro-page .text-lg { font-size: 16px; }
.puro-page .text-xl { font-size: 20px; }
.puro-page .text-2xl { font-size: 28px; }
.puro-page .text-3xl { font-size: 36px; }
.puro-page .fw-400 { font-weight: 400; }
.puro-page .fw-500 { font-weight: 500; }
.puro-page .fw-600 { font-weight: 600; }
.puro-page .fw-700 { font-weight: 700; }
.puro-page .fw-800 { font-weight: 800; }
.puro-page .tabular { font-variant-numeric: tabular-nums; }
.puro-page .truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* ==========================================================================
APP SHELL (for dashboard-style pages)
========================================================================== */
.puro-page .app-shell {
display: grid;
grid-template-columns: 240px 1fr;
min-height: 100vh;
position: relative;
z-index: 2;
}
.puro-page .app-side {
border-right: 1px solid var(--border);
background: rgba(2, 6, 23, 0.6);
padding: 20px 14px;
display: flex;
flex-direction: column;
gap: 28px;
position: sticky;
top: 0;
height: 100vh;
overflow-y: auto;
}
.puro-page .app-side .brand { padding: 6px 10px 14px; }
.puro-page .side-group { display: flex; flex-direction: column; gap: 2px; }
.puro-page .side-label {
font-size: 10px;
color: var(--text-3);
text-transform: uppercase;
letter-spacing: 0.12em;
padding: 0 10px 8px;
font-family: var(--font-mono);
}
.puro-page .side-item {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 10px;
border-radius: var(--r-sm);
font-size: 13px;
color: var(--text-2);
cursor: pointer;
transition: all .12s;
}
.puro-page .side-item:hover { color: var(--text-0); background: rgba(255,255,255,0.03); }
.puro-page .side-item.active { background: rgba(34, 211, 238, 0.08); color: var(--cyan); }
.puro-page .side-item .ico {
width: 16px; height: 16px; opacity: 0.8;
display: inline-flex; align-items: center; justify-content: center;
flex-shrink: 0;
}
.puro-page .side-item .count {
margin-left: auto;
font-size: 11px;
color: var(--text-3);
font-family: var(--font-mono);
}
.puro-page .side-item.active .count { color: var(--cyan); }
.puro-page .app-main {
min-width: 0; /* allow grid children to shrink */
display: flex;
flex-direction: column;
}
.puro-page .app-topbar {
height: 60px;
border-bottom: 1px solid var(--border);
display: flex;
align-items: center;
padding: 0 32px;
gap: 16px;
position: sticky;
top: 0;
z-index: 10;
background: rgba(10, 14, 26, 0.75);
backdrop-filter: blur(12px);
}
.puro-page .app-topbar h1 {
font-size: 18px;
font-weight: 600;
letter-spacing: -0.01em;
}
.puro-page .app-content {
padding: 32px;
flex: 1;
}
/* user avatar pill */
.puro-page .avatar {
width: 28px; height: 28px;
border-radius: 50%;
background: linear-gradient(135deg, #22d3ee, #a855f7);
display: inline-flex;
align-items: center;
justify-content: center;
color: #042f2e;
font-weight: 700;
font-size: 12px;
flex-shrink: 0;
}
/* ==========================================================================
KBD
========================================================================== */
.puro-page kbd {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 20px;
height: 20px;
padding: 0 6px;
font-family: var(--font-mono);
font-size: 11px;
font-weight: 500;
color: var(--text-1);
background: var(--bg-1);
border: 1px solid var(--border-2);
border-bottom-width: 2px;
border-radius: 4px;
line-height: 1;
}

View File

@@ -1,69 +1,45 @@
<template>
<div class="relative flex min-h-screen items-center justify-center overflow-hidden p-4">
<!-- Background -->
<div
class="absolute inset-0 bg-gradient-to-br from-gray-50 via-primary-50/30 to-gray-100 dark:from-dark-950 dark:via-dark-900 dark:to-dark-950"
></div>
<div class="auth-shell" :class="{ 'auth-shell-split': hasNarrative }">
<div class="bg-glow soft"></div>
<!-- Decorative Elements -->
<div class="pointer-events-none absolute inset-0 overflow-hidden">
<!-- Gradient Orbs -->
<div
class="absolute -right-40 -top-40 h-80 w-80 rounded-full bg-primary-400/20 blur-3xl"
></div>
<div
class="absolute -bottom-40 -left-40 h-80 w-80 rounded-full bg-primary-500/15 blur-3xl"
></div>
<div
class="absolute left-1/2 top-1/2 h-96 w-96 -translate-x-1/2 -translate-y-1/2 rounded-full bg-primary-300/10 blur-3xl"
></div>
<!-- LEFT: Narrative (split mode only, hidden on mobile) -->
<aside v-if="hasNarrative" class="auth-narrative">
<slot name="narrative"></slot>
</aside>
<!-- Grid Pattern -->
<div
class="absolute inset-0 bg-[linear-gradient(rgba(20,184,166,0.03)_1px,transparent_1px),linear-gradient(90deg,rgba(20,184,166,0.03)_1px,transparent_1px)] bg-[size:64px_64px]"
></div>
</div>
<!-- Content Container -->
<div class="relative z-10 w-full max-w-md">
<!-- Logo/Brand -->
<div class="mb-8 text-center">
<!-- Custom Logo or Default Logo -->
<template v-if="settingsLoaded">
<!-- RIGHT: Form -->
<main class="auth-main">
<div class="auth-main-inner">
<!-- Legacy centered-card header (only when no narrative slot) -->
<div class="mb-8 text-center" v-if="!hasNarrative && settingsLoaded">
<div
class="mb-4 inline-flex h-16 w-16 items-center justify-center overflow-hidden rounded-2xl shadow-lg shadow-primary-500/30"
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-gradient mb-2 text-3xl font-bold">
{{ siteName }}
</h1>
<p class="text-sm text-gray-500 dark:text-dark-400">
{{ siteSubtitle }}
</p>
</template>
</div>
<h1 class="text-2xl font-bold">{{ siteName }}</h1>
<p class="text-sm text-gray-500 dark:text-dark-400" v-if="siteSubtitle">{{ siteSubtitle }}</p>
</div>
<!-- Card Container -->
<div class="card-glass rounded-2xl p-8 shadow-glass">
<!-- Form content -->
<slot />
</div>
<!-- Footer Links -->
<div class="mt-6 text-center text-sm">
<slot name="footer" />
</div>
<!-- Footer link slot (e.g., "没有账户?注册") -->
<div class="mt-6 text-center text-sm">
<slot name="footer" />
</div>
<!-- Copyright -->
<div class="mt-8 text-center text-xs text-gray-400 dark:text-dark-500">
&copy; {{ currentYear }} {{ siteName }}. All rights reserved.
<!-- Copyright (legacy mode only) -->
<div class="mt-8 text-center text-xs text-gray-400 dark:text-dark-500" v-if="!hasNarrative">
&copy; {{ currentYear }} {{ siteName }}. All rights reserved.
</div>
</div>
</div>
</main>
</div>
</template>
<script setup lang="ts">
import { computed, onMounted } from 'vue'
import { computed, onMounted, useSlots } from 'vue'
import { useAppStore } from '@/stores'
import { sanitizeUrl } from '@/utils/url'
@@ -76,6 +52,9 @@ const settingsLoaded = computed(() => appStore.publicSettingsLoaded)
const currentYear = computed(() => new Date().getFullYear())
const slots = useSlots()
const hasNarrative = computed(() => !!slots.narrative)
onMounted(() => {
appStore.fetchPublicSettings()
})
@@ -85,4 +64,57 @@ onMounted(() => {
.text-gradient {
@apply bg-gradient-to-r from-primary-600 to-primary-500 bg-clip-text text-transparent;
}
.auth-shell {
min-height: 100vh;
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;
color: var(--text-0);
font-family: var(--font-sans);
}
.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;
}
/* Legacy-mode (no narrative slot) background — keep existing gradient decorative look */
.auth-shell:not(.auth-shell-split) {
display: flex;
align-items: center;
justify-content: center;
padding: 1rem;
background: linear-gradient(to bottom right, #f9fafb, rgba(240,253,250,0.3), #f3f4f6);
}
:global(.dark) .auth-shell:not(.auth-shell-split) {
background: linear-gradient(to bottom right, #020617, #0f172a, #020617);
}
</style>

View File

@@ -498,7 +498,12 @@ export default {
invalidResetLink: '无效的重置链接',
invalidResetLinkHint: '此密码重置链接无效或已过期。请重新请求一个新链接。',
requestNewResetLink: '请求新的重置链接',
invalidOrExpiredToken: '密码重置链接无效或已过期。请重新请求一个新链接。'
invalidOrExpiredToken: '密码重置链接无效或已过期。请重新请求一个新链接。',
// PURO AI redesign
puroLoginTitle: '登录',
puroLoginSub: '用你的 PURO AI 账户继续',
puroRegisterTitle: '创建账户',
puroRegisterSub: '5 分钟开始用 PURO AI',
},
// Dashboard

View File

@@ -5,6 +5,7 @@ import router from './router'
import i18n, { initI18n } from './i18n'
import { useAppStore } from '@/stores/app'
import './style.css'
import './assets/puro.css'
function initThemeClass() {
const savedTheme = localStorage.getItem('theme')

View File

@@ -120,11 +120,26 @@ const routes: RouteRecordRaw[] = [
title: 'Key Usage',
}
},
{
path: '/docs',
name: 'Docs',
component: () => import('@/views/docs/DocsView.vue'),
meta: {
requiresAuth: false,
title: 'PURO AI · 文档'
}
},
// ==================== User Routes ====================
{
path: '/',
redirect: '/home'
name: 'Landing',
component: () => import('@/views/landing/LandingView.vue'),
meta: {
requiresAuth: false,
title: 'PURO AI — 你的 AI 订阅,已经付过钱了',
redirectIfAuth: '/dashboard'
}
},
{
path: '/dashboard',
@@ -533,6 +548,13 @@ router.beforeEach((to, _from, next) => {
authInitialized = true
}
// Auth-aware redirect for public pages that should bounce authenticated users elsewhere
// (e.g., Landing / at `/` redirects to /dashboard if user is logged in)
const redirectIfAuth = to.meta.redirectIfAuth
if (redirectIfAuth && authStore.isAuthenticated) {
return next(redirectIfAuth)
}
// Set page title
const appStore = useAppStore()
// For custom pages, use menu item label as document title

View File

@@ -7,6 +7,12 @@ import 'vue-router'
declare module 'vue-router' {
interface RouteMeta {
/**
* If set, authenticated users visiting this route are redirected to this path.
* Used for public pages (e.g., Landing `/`) that should bounce logged-in users to the app.
*/
redirectIfAuth?: string
/**
* Whether this route requires authentication
* @default true

View File

@@ -1,14 +1,28 @@
<template>
<AuthLayout>
<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">Claude · ChatGPT · Codex · Gemini</div>
</div>
</template>
<div class="space-y-6">
<!-- Title -->
<div class="text-center">
<h2 class="text-2xl font-bold text-gray-900 dark:text-white">
{{ t('auth.welcomeBack') }}
</h2>
<p class="mt-2 text-sm text-gray-500 dark:text-dark-400">
{{ t('auth.signInToAccount') }}
</p>
<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>
</div>
<div v-if="!backendModeEnabled && (linuxdoOAuthEnabled || oidcOAuthEnabled)" class="space-y-4">
@@ -439,4 +453,51 @@ function handle2FACancel(): void {
opacity: 0;
transform: translateY(-8px);
}
.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-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);
font-family: var(--font-mono);
}
</style>

View File

@@ -1,14 +1,28 @@
<template>
<AuthLayout>
<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">Claude · ChatGPT · Codex · Gemini</div>
</div>
</template>
<div class="space-y-6">
<!-- Title -->
<div class="text-center">
<h2 class="text-2xl font-bold text-gray-900 dark:text-white">
{{ t('auth.createAccount') }}
</h2>
<p class="mt-2 text-sm text-gray-500 dark:text-dark-400">
{{ t('auth.signUpToStart', { siteName }) }}
</p>
<h2 class="text-2xl font-bold text-gray-900 dark:text-white">{{ t('auth.puroRegisterTitle') }}</h2>
<p class="mt-2 text-sm text-gray-500 dark:text-dark-400">{{ t('auth.puroRegisterSub') }}</p>
</div>
<div v-if="linuxdoOAuthEnabled || oidcOAuthEnabled" class="space-y-4">
@@ -767,4 +781,51 @@ async function handleRegister(): Promise<void> {
opacity: 0;
transform: translateY(-8px);
}
.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-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);
font-family: var(--font-mono);
}
</style>

View File

@@ -0,0 +1,213 @@
<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> · OpenAIvia 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> · Anthropicvia 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> · Googlevia 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">
// DocsView — public quickstart documentation
// Route: /docs (no auth required)
</script>
<style scoped>
/* =============================================================
* DocsView — component-local styles
* Globals from puro.css (scoped to .puro-page) provide:
* - .nav, .nav-inner, .brand, .hex, .nav-links, .nav-cta (nav base)
* - .mono, .btn, .btn-primary, .container (primitives)
* ============================================================= */
.puro-page {
min-height: 100vh;
position: relative;
}
.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; }
/* container override (puro.css has 1100px/32px; we want narrower for docs readability) */
.container {
max-width: 860px;
margin: 0 auto;
padding: 0 24px;
position: relative;
z-index: 2;
}
</style>

View File

@@ -0,0 +1,504 @@
<template>
<div class="puro-page">
<div class="bg-glow"></div>
<div class="grain"></div>
<!-- NAV -->
<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">
<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>
<!-- 模型墙 -->
<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>
<div class="model-name">Claude Pro / Max</div>
<div class="model-meta">Anthropic OAuth</div>
</div>
</div>
<div class="model-card">
<div class="model-dot" style="background: var(--p-gpt)"></div>
<div>
<div class="model-name">ChatGPT Plus / Pro</div>
<div class="model-meta">OpenAI OAuth</div>
</div>
</div>
<div class="model-card">
<div class="model-dot" style="background: var(--p-codex)"></div>
<div>
<div class="model-name">Codex CLI</div>
<div class="model-meta">OpenAI OAuth</div>
</div>
</div>
<div class="model-card">
<div class="model-dot" style="background: var(--p-gemini)"></div>
<div>
<div class="model-name">Gemini Code Assist</div>
<div class="model-meta">Google OAuth</div>
</div>
</div>
<div class="model-card is-muted">
<div class="model-dot" style="background: var(--text-3)"></div>
<div>
<div class="model-name">更多</div>
<div class="model-meta">规划中</div>
</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">
<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">
<div class="feature-icon">🔄</div>
<h3>账号池高可用</h3>
<p>支持多账号自动调度与 failover某个上游触发限流 / 冷却时流量切到下一个健康账号token 刷新全自动</p>
</div>
<div class="feature">
<div class="feature-icon">📊</div>
<h3>用量看板</h3>
<p>每条请求的 tokens费用上游账号延迟全可视化模型分布饼图 + 趋势曲线 + Top 排行</p>
</div>
</div>
</section>
<!-- 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 &amp; 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>
<!-- 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/commits/branch/main" target="_blank" rel="noopener noreferrer">更新日志</a>
</div>
<div class="footer-col">
<div class="footer-col-title">资源</div>
<a href="https://git.puro.im/purovps/sub2api" target="_blank" rel="noopener noreferrer">GitHub</a>
<a href="/docs#codex">Codex 配置示例</a>
<a href="https://status.puro.im" target="_blank" rel="noopener noreferrer">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 noreferrer">git.puro.im</a>
</div>
</div>
</footer>
</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>
/* =============================================================
* LandingView — component-local styles
* Globals from puro.css (scoped to .puro-page) also provide:
* - .log-table, .provider.{claude,gpt,gemini}, .status-200/429 (dashboard mockup)
* - .nav, .nav-inner, .brand, .hex, .nav-links, .nav-cta (nav base)
* - .mono, .container, .btn, .btn-primary, .btn-ghost, .btn-lg (primitives)
* ============================================================= */
.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);
}
/* Note: these rules (.container / .section-*) intentionally override
* puro.css defaults with landing-page-specific values.
* puro.css has global defaults of: container max-width 1100px/padding 32px,
* section-title margin-bottom 16px, section-kicker letter-spacing 0.15em.
* Source-order ensures the scoped values below win. */
.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; }
/* 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); margin-top: 4px; font-family: var(--font-mono); }
.stat-delta.down { color: var(--red); }
.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; }
/* Nav sticky + responsive tweaks (augments puro.css defaults)
* Note: puro.css defines .puro-page .nav (z-index 50, blur 16px, gap 28px)
* at higher specificity; rules here only add what puro.css lacks
* (our darker sticky bg + mobile hide-nav-links). */
.nav {
position: sticky;
top: 0;
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;
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);
}
</style>

View File

@@ -46,6 +46,20 @@ export default {
800: '#1e293b',
900: '#0f172a',
950: '#020617'
},
// 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'
}
},
fontFamily: {