Compare commits
30 Commits
ora_main
...
e843a7aef8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e843a7aef8 | ||
|
|
4cf6840479 | ||
|
|
9f78b70a87 | ||
|
|
046765632d | ||
|
|
93481b8c45 | ||
|
|
6291dc40d0 | ||
| 355370ad2a | |||
|
|
829f101100 | ||
|
|
0fceb100e0 | ||
|
|
7dc8062988 | ||
|
|
f17a88c171 | ||
|
|
91b9ae7e21 | ||
|
|
9ee99d17fd | ||
|
|
284b5129ac | ||
|
|
4832534232 | ||
|
|
4e675d70c1 | ||
|
|
c099cd5d97 | ||
|
|
158f2a8d53 | ||
|
|
cfcdd988db | ||
|
|
9dae8724e3 | ||
|
|
064a4b7614 | ||
|
|
41664efede | ||
|
|
1d7e75b82e | ||
|
|
d941550bf6 | ||
|
|
3a16b3ecde | ||
|
|
02173c8d7e | ||
|
|
332d46cde7 | ||
|
|
ac3417a964 | ||
|
|
9c34e9619c | ||
|
|
f431b2e2ff |
5
.ci/Dockerfile
Normal file
5
.ci/Dockerfile
Normal file
@@ -0,0 +1,5 @@
|
||||
FROM gcr.io/distroless/static-debian12:nonroot
|
||||
WORKDIR /app
|
||||
COPY sub2api-linux /app/sub2api
|
||||
EXPOSE 8080
|
||||
ENTRYPOINT ["/app/sub2api"]
|
||||
13
.ci/README.md
Normal file
13
.ci/README.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# ai.puro.im CI artifacts
|
||||
|
||||
Drone CI (see `.drone.yml`) builds a statically-linked `sub2api-linux` binary and bakes it into this distroless image.
|
||||
|
||||
Host-side state (NOT in repo):
|
||||
- `/opt/sub2api/docker-compose.yml` — sub2api + sub2api-pg + sub2api-redis services + PG password
|
||||
- `/opt/sub2api/app-data/config.yaml` — wizard-generated runtime config
|
||||
- `/opt/sub2api/{pg-data,redis-data,app-data,logs}` — persistent volumes
|
||||
|
||||
Deploy flow:
|
||||
1. Drone builds frontend (pnpm) + backend (go, linux/amd64)
|
||||
2. CI copies `backend/sub2api-linux` + `.ci/Dockerfile` to `/opt/sub2api/`
|
||||
3. CI runs `docker compose up -d --build sub2api` — rebuilds only sub2api service, leaves PG/Redis untouched
|
||||
75
.drone.yml
Normal file
75
.drone.yml
Normal file
@@ -0,0 +1,75 @@
|
||||
kind: pipeline
|
||||
type: docker
|
||||
name: default
|
||||
|
||||
trigger:
|
||||
branch: [main]
|
||||
event: [push]
|
||||
|
||||
steps:
|
||||
- name: build-frontend
|
||||
image: node:18-alpine
|
||||
commands:
|
||||
- corepack enable
|
||||
- corepack prepare pnpm@10.33.0 --activate
|
||||
- cd frontend
|
||||
- pnpm install --frozen-lockfile
|
||||
- pnpm run build
|
||||
volumes:
|
||||
- name: pnpm-store
|
||||
path: /root/.local/share/pnpm/store
|
||||
|
||||
- name: build-backend
|
||||
image: golang:1.23-alpine
|
||||
environment:
|
||||
CGO_ENABLED: "0"
|
||||
GOOS: linux
|
||||
GOARCH: amd64
|
||||
GOTOOLCHAIN: auto
|
||||
GOFLAGS: "-buildvcs=false"
|
||||
commands:
|
||||
- apk add --no-cache git
|
||||
- cd backend
|
||||
- go build -tags embed -ldflags='-s -w' -o sub2api-linux ./cmd/server
|
||||
volumes:
|
||||
- name: go-cache
|
||||
path: /root/.cache/go-build
|
||||
- name: go-mod
|
||||
path: /go/pkg/mod
|
||||
depends_on:
|
||||
- build-frontend
|
||||
|
||||
- name: deploy
|
||||
image: docker:cli
|
||||
commands:
|
||||
- cp backend/sub2api-linux /opt/sub2api/sub2api-linux
|
||||
- cp .ci/Dockerfile /opt/sub2api/Dockerfile
|
||||
- cd /opt/sub2api && docker compose up -d --build sub2api
|
||||
- sleep 8
|
||||
- docker ps --filter 'name=^sub2api$' --filter 'status=running' --format '{{.Names}}' | grep -qx sub2api
|
||||
- docker inspect sub2api --format='{{.State.Health.Status}} {{.State.Status}}' 2>/dev/null || true
|
||||
- echo "deploy ok"
|
||||
volumes:
|
||||
- name: docker-sock
|
||||
path: /var/run/docker.sock
|
||||
- name: opt-sub2api
|
||||
path: /opt/sub2api
|
||||
depends_on:
|
||||
- build-backend
|
||||
|
||||
volumes:
|
||||
- name: pnpm-store
|
||||
host:
|
||||
path: /opt/drone/cache/pnpm-store
|
||||
- name: go-cache
|
||||
host:
|
||||
path: /opt/drone/cache/go-build
|
||||
- name: go-mod
|
||||
host:
|
||||
path: /opt/drone/cache/go-mod
|
||||
- name: docker-sock
|
||||
host:
|
||||
path: /var/run/docker.sock
|
||||
- name: opt-sub2api
|
||||
host:
|
||||
path: /opt/sub2api
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -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/
|
||||
|
||||
541
VPS_DEPLOYMENT_NOTES.md
Normal file
541
VPS_DEPLOYMENT_NOTES.md
Normal file
@@ -0,0 +1,541 @@
|
||||
# Sub2API VPS 部署 + CI/CD 记录
|
||||
|
||||
> 2026-04-19 @ `ai.puro.im` · 217.216.32.230 · Ubuntu 22.04 x86_64
|
||||
|
||||
本文档对应 `LOCAL_SETUP_NOTES.md`(Mac 本地开发环境)的线上版本,覆盖:从零到站点可访问、接入 Codex CLI、建立 Drone CI 自动部署的完整流程。
|
||||
|
||||
---
|
||||
|
||||
## 概览
|
||||
|
||||
```
|
||||
外网
|
||||
│ HTTPS (Let's Encrypt, Caddy auto-TLS)
|
||||
▼
|
||||
ai.puro.im :443
|
||||
│
|
||||
┌──────┴──────┐
|
||||
│ Caddy │ host, /etc/caddy/conf.d/sub2api.conf
|
||||
│ reverse_ │ → 127.0.0.1:8081
|
||||
│ proxy │
|
||||
└──────┬──────┘
|
||||
│
|
||||
docker port map 127.0.0.1:8081 → 容器 8080
|
||||
│
|
||||
┌────────▼──────────────┐
|
||||
│ sub2api-net (compose) │
|
||||
│ sub2api (Go) │ /opt/sub2api/ (持久化)
|
||||
│ sub2api-pg (PG15) │ ├─ app-data/ (config, .installed, pricing, logs)
|
||||
│ sub2api-redis │ ├─ pg-data/
|
||||
│ (redis:7-alpine) │ └─ redis-data/
|
||||
└───────────────────────┘
|
||||
│ VPS 出口
|
||||
▼
|
||||
api.anthropic.com / chatgpt.com / api.openai.com
|
||||
```
|
||||
|
||||
### CI 流程
|
||||
|
||||
```
|
||||
Mac (本地仓库) ─ git push gitea main ─▶ Gitea (git.puro.im) ──webhook──▶ Drone (devops.puro.im)
|
||||
│
|
||||
┌───────────┴────────────┐
|
||||
│ build-frontend (pnpm) │
|
||||
│ build-backend (go) │
|
||||
│ deploy │
|
||||
│ - cp bin + Dockerfile│
|
||||
│ - docker compose up │
|
||||
│ --build sub2api │
|
||||
└────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 一、VPS 环境
|
||||
|
||||
| 项 | 值 | 备注 |
|
||||
|---|---|---|
|
||||
| 主机 | 217.216.32.230(新加坡) | `ssh vps` |
|
||||
| OS | Ubuntu 22.04 LTS x86_64 | |
|
||||
| Caddy | host 安装,`/etc/caddy/conf.d/*.conf` auto-include | 已跑着 puro.im / git.puro.im / devops.puro.im / erp.puro.im / ... |
|
||||
| Docker | host 装 + `devops-net` 网络 | 各应用容器都在 devops-net 里 |
|
||||
| Gitea | `git.puro.im:2222` (SSH), 3000 (HTTP) | purovps/sub2api 仓库 |
|
||||
| Drone | `devops.puro.im`(OAuth via Gitea) | drone-server + drone-runner-docker |
|
||||
| Sub2API 专用 docker-compose | `sub2api-net` 独立 bridge 网络(**不加 devops-net**) | 隔离 PG/Redis 生命周期 |
|
||||
| DNS | Cloudflare **DNS only**(非代理模式) | `ai.puro.im` A 记录 → VPS IP |
|
||||
| 端口冲突 | 宿主 `:8080` 被 drone-server 占 | Sub2API 映射到宿主 `127.0.0.1:8081` |
|
||||
|
||||
---
|
||||
|
||||
## 二、初次部署流程(手工路径,首次跑通)
|
||||
|
||||
> CI 跑通后这段主要作为"首次引导 / 灾难恢复参考"。日常改代码走 CI(下一节)。
|
||||
|
||||
### 1. 本机交叉编译 linux/amd64 二进制
|
||||
|
||||
```bash
|
||||
cd /Users/mini/Work/dev/sub2api/backend
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
|
||||
go build -tags embed -ldflags='-s -w' -o sub2api-linux ./cmd/server
|
||||
# 产物 ~75MB, static ELF x86-64
|
||||
```
|
||||
|
||||
注意:本机 macOS arm64,VPS 是 x86_64,**必须交叉编译**。`-tags embed` 把前端 dist 打进二进制(配合先跑 `pnpm run build`)。
|
||||
|
||||
### 2. VPS 建目录 + scp 文件
|
||||
|
||||
```bash
|
||||
ssh vps "mkdir -p /opt/sub2api/{app-data,pg-data,redis-data}"
|
||||
|
||||
# 准备 deploy 文件(本地 staging 目录)
|
||||
cp backend/sub2api-linux /Users/mini/Work/dev/sub2api-deploy/
|
||||
# Dockerfile + docker-compose.yml 见下节内容
|
||||
|
||||
scp /Users/mini/Work/dev/sub2api-deploy/{sub2api-linux,Dockerfile,docker-compose.yml} \
|
||||
vps:/opt/sub2api/
|
||||
|
||||
# distroless:nonroot 需要 UID 65532 可写
|
||||
ssh vps "chown -R 65532:65532 /opt/sub2api/app-data"
|
||||
```
|
||||
|
||||
### 3. Dockerfile(`.ci/Dockerfile` 也是同一份)
|
||||
|
||||
```dockerfile
|
||||
FROM gcr.io/distroless/static-debian12:nonroot
|
||||
WORKDIR /app
|
||||
COPY sub2api-linux /app/sub2api
|
||||
EXPOSE 8080
|
||||
ENTRYPOINT ["/app/sub2api"]
|
||||
```
|
||||
|
||||
### 4. docker-compose.yml
|
||||
|
||||
关键点:
|
||||
- `sub2api` 监听 `127.0.0.1:8081:8080`(避开宿主 8080 被 drone 占)
|
||||
- `/app/data` 整个挂到 `./app-data`(含 config.yaml / install.lock / pricing / logs)
|
||||
- PG 密码通过 `POSTGRES_PASSWORD` env(不进仓库)
|
||||
- 独立 `sub2api-net` bridge,不混 devops-net
|
||||
|
||||
生产文件位置:`/opt/sub2api/docker-compose.yml`(**不签入 git**,含 PG 密码)。模板结构:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:15
|
||||
container_name: sub2api-pg
|
||||
environment:
|
||||
POSTGRES_DB: sub2api
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: "<32-hex 强口令>"
|
||||
volumes: [./pg-data:/var/lib/postgresql/data]
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U postgres -d sub2api"]
|
||||
restart: unless-stopped
|
||||
networks: [sub2api-net]
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
container_name: sub2api-redis
|
||||
command: ["redis-server", "--appendonly", "yes"]
|
||||
volumes: [./redis-data:/data]
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
restart: unless-stopped
|
||||
networks: [sub2api-net]
|
||||
|
||||
sub2api:
|
||||
build: .
|
||||
image: sub2api:local
|
||||
container_name: sub2api
|
||||
depends_on:
|
||||
postgres: { condition: service_healthy }
|
||||
redis: { condition: service_healthy }
|
||||
volumes:
|
||||
- ./app-data:/app/data
|
||||
ports: ["127.0.0.1:8081:8080"]
|
||||
restart: unless-stopped
|
||||
networks: [sub2api-net]
|
||||
|
||||
networks:
|
||||
sub2api-net:
|
||||
driver: bridge
|
||||
```
|
||||
|
||||
### 5. 起 stack
|
||||
|
||||
```bash
|
||||
ssh vps "cd /opt/sub2api && docker compose up -d --build"
|
||||
# 等 PG/Redis healthy 后 sub2api 才会起(depends_on + healthcheck 保证)
|
||||
```
|
||||
|
||||
### 6. Caddy 反代
|
||||
|
||||
`/etc/caddy/conf.d/sub2api.conf`:
|
||||
|
||||
```caddy
|
||||
ai.puro.im {
|
||||
reverse_proxy 127.0.0.1:8081 {
|
||||
header_up X-Real-IP {remote_host}
|
||||
header_up X-Forwarded-For {remote_host}
|
||||
header_up X-Forwarded-Proto {scheme}
|
||||
}
|
||||
encode gzip zstd
|
||||
log {
|
||||
output file /var/log/caddy/ai.puro.im.log
|
||||
format json
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```bash
|
||||
ssh vps "systemctl reload caddy"
|
||||
```
|
||||
|
||||
### 7. 首次访问 → Setup Wizard
|
||||
|
||||
**不预先写 `/opt/sub2api/app-data/config.yaml`**——让 sub2api 以 wizard 模式启动,自己生成 config。走:
|
||||
|
||||
1. 浏览器开 `https://ai.puro.im` → 自动进 Setup Wizard
|
||||
2. Step 1 数据库:host=`postgres` / port=`5432` / user=`postgres` / password=<compose 里那个> / dbname=`sub2api` / sslmode=`disable`
|
||||
3. Step 2 Redis:host=`redis` / port=`6379` / password 空 / db=`0`
|
||||
4. Step 3 Admin:邮箱 + ≥8 位密码(自己选好记下来)
|
||||
5. Step 4 提交 → wizard 写 `app-data/config.yaml` + `app-data/.installed` + DB 创建 admin → 进程自杀、compose 重启后以正常模式跑
|
||||
|
||||
### 8. 加 OpenAI OAuth 账号(账号池)
|
||||
|
||||
从本机 `~/.codex/auth.json` 抽 `tokens.refresh_token`,粘到:
|
||||
- 后台 → 账号管理 → 添加账号 → Platform: OpenAI / Type: OAuth → "粘贴 Refresh Token" → 验证
|
||||
- 弹出 ChatGPT 邮箱+Plan 即成功
|
||||
|
||||
### 9. 三件必调(否则 503 "no available OpenAI accounts")
|
||||
|
||||
见 [§四 · 坑 3](#坑-3-新建-oauth-账号一上来无法调度),后台 UI 里:
|
||||
- **开启调度开关**
|
||||
- **去掉 auto_pause_on_expired**
|
||||
- **清掉 expires_at**(或设远期)
|
||||
|
||||
---
|
||||
|
||||
## 三、CI/CD 流水线(Drone)
|
||||
|
||||
### 仓库
|
||||
|
||||
| 位置 | 用途 |
|
||||
|---|---|
|
||||
| GitHub: `Wei-Shaw/sub2api` | 上游,origin remote |
|
||||
| Gitea: `git.puro.im/purovps/sub2api` | 我们的 fork,**Drone trigger source**,remote 名 `gitea` |
|
||||
|
||||
本地开发流程:
|
||||
|
||||
```bash
|
||||
# 日常
|
||||
git checkout main
|
||||
# 改代码...
|
||||
git commit
|
||||
git push gitea main # 触发 CI
|
||||
# 可选同步上游
|
||||
git fetch origin
|
||||
git merge origin/main # 手动决定是否合上游
|
||||
```
|
||||
|
||||
### Drone 激活
|
||||
|
||||
首次激活:https://devops.puro.im → Sync → 点亮 `purovps/sub2api` → Settings → 开 **Trusted**(pipeline 要挂 host socket 和 `/opt/sub2api`)。
|
||||
|
||||
激活后 Gitea 会自动配 webhook(`POST https://devops.puro.im/hook`,事件 `push, pull_request, ...`)。
|
||||
|
||||
### `.drone.yml` 三段 pipeline
|
||||
|
||||
```yaml
|
||||
kind: pipeline
|
||||
type: docker
|
||||
name: default
|
||||
trigger: { branch: [main], event: [push] }
|
||||
|
||||
steps:
|
||||
- name: build-frontend # node:18-alpine + pnpm 10.33
|
||||
# pnpm install --frozen-lockfile && pnpm run build
|
||||
# 产物 → backend/internal/web/dist/
|
||||
|
||||
- name: build-backend # golang:1.23-alpine + GOTOOLCHAIN=auto
|
||||
# 自动拉 Go 1.26.2 toolchain
|
||||
# CGO_ENABLED=0 GOOS=linux GOARCH=amd64 → sub2api-linux
|
||||
|
||||
- name: deploy # docker:cli
|
||||
# cp backend/sub2api-linux /opt/sub2api/sub2api-linux
|
||||
# cp .ci/Dockerfile /opt/sub2api/Dockerfile
|
||||
# cd /opt/sub2api && docker compose up -d --build sub2api
|
||||
# 验证 sub2api 容器 running
|
||||
```
|
||||
|
||||
host 挂载 3 类卷:
|
||||
- `/var/run/docker.sock` — 用宿主 docker daemon 做构建+重启
|
||||
- `/opt/sub2api` — 更新 binary/Dockerfile
|
||||
- `/opt/drone/cache/{pnpm-store,go-build,go-mod}` — 加速后续构建
|
||||
|
||||
### 首次构建时长
|
||||
|
||||
| Step | 首次 | 缓存命中后 |
|
||||
|---|---|---|
|
||||
| build-frontend | ~90s | ~30s |
|
||||
| build-backend | ~60s(+首次下 Go 1.26.2 toolchain) | ~40s |
|
||||
| deploy | ~20s | ~20s |
|
||||
| **合计** | **~3 min** | **~90s** |
|
||||
|
||||
### CI 不碰的内容(secrets / 数据)
|
||||
|
||||
| 文件/目录 | 位置 | 为什么 |
|
||||
|---|---|---|
|
||||
| `docker-compose.yml` | `/opt/sub2api/` | 含 PG 密码 |
|
||||
| `app-data/config.yaml` | `/opt/sub2api/app-data/` | JWT secret / 运行态 |
|
||||
| `pg-data/` | `/opt/sub2api/` | PG 数据 |
|
||||
| `redis-data/` | `/opt/sub2api/` | Redis 数据 |
|
||||
|
||||
CI 只:
|
||||
- 覆写 `/opt/sub2api/sub2api-linux`(二进制)
|
||||
- 覆写 `/opt/sub2api/Dockerfile`
|
||||
- `docker compose up -d --build sub2api` — 只重建 `sub2api` service(PG/Redis 不动)
|
||||
|
||||
### Skip CI
|
||||
|
||||
文档/无代码变动想跳过构建:commit 消息加 `[CI SKIP]`(Drone 官方约定)。
|
||||
|
||||
---
|
||||
|
||||
## 四、踩坑与解法
|
||||
|
||||
### 坑 1:Setup Wizard 默认写 `server.port: 443`
|
||||
|
||||
**现象**:Wizard 跑完 → 容器重启 → `127.0.0.1:8081` 无响应(app 启动了但监听在 container 内的 :443)。
|
||||
|
||||
**根因**:Wizard 默认把 `server.port` 写成 443(面向"容器直接暴露 HTTPS"的用法,不适合我们"容器内 8080 + Caddy 外 443"的模式)。
|
||||
|
||||
**解法**:
|
||||
```bash
|
||||
ssh vps "sed -i 's/port: 443/port: 8080/' /opt/sub2api/app-data/config.yaml && docker restart sub2api"
|
||||
```
|
||||
|
||||
后续 CI 不重写 config.yaml,这个修复是一次性的。
|
||||
|
||||
---
|
||||
|
||||
### 坑 2:distroless:nonroot 日志目录权限
|
||||
|
||||
**现象**:容器启动日志刷 `write error: can't open new logfile: open /app/data/logs/sub2api.log: permission denied`。
|
||||
|
||||
**根因**:`gcr.io/distroless/static-debian12:nonroot` 进程 UID=65532,但 `/opt/sub2api/app-data/logs` 宿主目录 owner=root。
|
||||
|
||||
**解法**:
|
||||
```bash
|
||||
ssh vps "chown -R 65532:65532 /opt/sub2api/app-data"
|
||||
```
|
||||
|
||||
一次性。CI 不碰 app-data,所以不复发。
|
||||
|
||||
---
|
||||
|
||||
### 坑 3:新建 OAuth 账号一上来"无法调度"
|
||||
|
||||
**现象**:refresh_token 粘好、验证通过、UI 显示"正常",curl `/responses` 报 `503 "Service temporarily unavailable"`,日志 `openai.account_select_failed: no available OpenAI accounts`。
|
||||
|
||||
**根因(三个叠加)**:
|
||||
1. UI 默认 `schedulable=false`
|
||||
2. Wizard/后端给 `expires_at` 写了 **access_token 的短期过期**(~7 分钟后),而 `auto_pause_on_expired=true` 默认开
|
||||
3. 每次 sub2api 重启 → `[AccountExpiry] Auto paused 1 expired accounts` → schedulable 被打回 false
|
||||
|
||||
**解法**(SQL 或 UI 都可):
|
||||
```sql
|
||||
UPDATE accounts
|
||||
SET schedulable=true, expires_at=NULL, auto_pause_on_expired=false
|
||||
WHERE id=<n>;
|
||||
```
|
||||
|
||||
**已存 memory**:`feedback_sub2api_account_pitfalls.md`。长期方案:修 fork 里的账号创建逻辑,OAuth 账号的 `expires_at` 应指 refresh_token 过期或订阅到期,不是 access_token。
|
||||
|
||||
---
|
||||
|
||||
### 坑 4:`run_mode: simple` 隐藏 SaaS 菜单
|
||||
|
||||
**现象**:为跳过 `INSUFFICIENT_BALANCE` 临时切到 `run_mode: simple`,重登后台发现**用户管理 / 分组管理 / 渠道管理 / 订阅管理 / 兑换码 / 优惠码**全消失。
|
||||
|
||||
**根因**:**设计行为**,不是 bug。`simple` 模式是给 "单用户/团队内部工具" 用,刻意隐藏 SaaS 管理面板;前端 `stores/auth.ts` 里 `isSimpleMode = computed(() => runMode === 'simple')` 控制路由可见性。
|
||||
|
||||
**解法**:
|
||||
- 如果需要管理 group/订阅/计费/兑换码 → 保持 `run_mode: standard`
|
||||
- `INSUFFICIENT_BALANCE` 的替代方案:给 admin 塞大额 balance 即可(见坑 5)
|
||||
|
||||
---
|
||||
|
||||
### 坑 5:`standard` 模式 + admin balance=0 → 403
|
||||
|
||||
**现象**:切回 `standard` 后 curl `/responses` 报 `INSUFFICIENT_BALANCE`。
|
||||
|
||||
**根因**:中间件 `api_key_auth.go:198` 检查 `apiKey.User.Balance <= 0` → 403。admin 默认余额为 0。
|
||||
|
||||
**解法**:
|
||||
```sql
|
||||
UPDATE users SET balance = 1000000000 WHERE id=1;
|
||||
```
|
||||
|
||||
10 亿够跑很久。接 iShare 后改用 iShare 订阅模式(sub2api 标记为 "subscription mode",走订阅限额而不查 balance)。
|
||||
|
||||
---
|
||||
|
||||
### 坑 6:Redis L2 cache stale
|
||||
|
||||
**现象**:跑 [坑 5] 的 SQL 之后、docker restart sub2api 之后,curl 仍报 `INSUFFICIENT_BALANCE`。
|
||||
|
||||
**根因**:Sub2API 的 API key 鉴权有两级缓存:
|
||||
- L1 进程内 LRU(TTL 15s)
|
||||
- L2 Redis(key `sub2api:apikey:*`,**TTL 300s**)
|
||||
|
||||
docker restart 只清 L1,L2 是独立 Redis 容器,保留着旧的 `balance=0` 缓存条目。
|
||||
|
||||
**解法**:
|
||||
```bash
|
||||
ssh vps "docker exec sub2api-redis redis-cli FLUSHDB && docker restart sub2api"
|
||||
```
|
||||
|
||||
**一般经验**:改 DB 里的 user / api_key / account 后需重启 sub2api **+** 清 Redis。只重启 sub2api 不够。
|
||||
|
||||
---
|
||||
|
||||
### 坑 7:`/setup/*` 路由按需注册
|
||||
|
||||
**现象**:首次手工写了 config.yaml 让容器跑起来,后来想走 Wizard 重新配置;前端进了 `/setup` 页,填完点"测试连接" → `Request failed with status code 404`。
|
||||
|
||||
**根因**:`cmd/server/main.go` 里只有 `NeedsSetup() == true` 分支(走 `runSetupServer()`)才 `setup.RegisterRoutes(r)`。config.yaml 已存在时走 `runMainServer()`,setup 路由不注册 → 前端 `/setup/test-db` → 404。
|
||||
|
||||
**解法**:让 `NeedsSetup()` 返回 true,即 `/app/data/config.yaml` **和** `/app/data/.installed` 都不存在。具体做法:
|
||||
```bash
|
||||
ssh vps "cd /opt/sub2api && mv app-data/config.yaml app-data/config.yaml.bak; \
|
||||
mv app-data/.installed app-data/.installed.bak 2>/dev/null; \
|
||||
docker restart sub2api"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、当前状态 snapshot(2026-04-19)
|
||||
|
||||
```
|
||||
Domain https://ai.puro.im (HTTP/2, Let's Encrypt via Caddy)
|
||||
VPS 217.216.32.230 (Singapore, Ubuntu 22.04)
|
||||
Image sub2api:local (Drone CI-built, distroless + static Go binary)
|
||||
|
||||
Containers on VPS:
|
||||
sub2api ← Drone 每次 push 重建
|
||||
sub2api-pg postgres:15 (devops-net 无关,独立 sub2api-net)
|
||||
sub2api-redis redis:7-alpine
|
||||
|
||||
Accounts:
|
||||
admin@puro.im (id=1, role=admin, balance=1e9)
|
||||
test_myopenai (OpenAI OAuth, group=test_codex, schedulable=true)
|
||||
|
||||
API keys:
|
||||
sk-d2132de2f0b4c1ab64ef7241a16d254cab483f1f8afd47ad4a89e39cf6e2345a
|
||||
(user=admin, group=test_codex)
|
||||
|
||||
Config:
|
||||
run_mode: standard
|
||||
server.port: 8080 (容器内)
|
||||
database.host: postgres (compose DNS)
|
||||
redis.host: redis (compose DNS)
|
||||
|
||||
CI:
|
||||
Drone job trigger = git push gitea main
|
||||
Gitea webhook → Drone webhook (active)
|
||||
最近一次 build: commit 9c34e961 ✓ (前端 ~90s + 后端 ~60s + deploy ~20s)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 六、常用运维命令
|
||||
|
||||
### 日常
|
||||
```bash
|
||||
# 状态
|
||||
ssh vps "cd /opt/sub2api && docker compose ps"
|
||||
|
||||
# 日志
|
||||
ssh vps "docker logs sub2api --tail 100 -f"
|
||||
ssh vps "tail -f /var/log/caddy/ai.puro.im.log"
|
||||
|
||||
# 强制重启 sub2api(不动 PG/Redis)
|
||||
ssh vps "docker restart sub2api"
|
||||
|
||||
# 重载 Caddy
|
||||
ssh vps "systemctl reload caddy"
|
||||
|
||||
# 清 Redis 缓存(改 DB 后要做)
|
||||
ssh vps "docker exec sub2api-redis redis-cli FLUSHDB"
|
||||
```
|
||||
|
||||
### 触发 CI 重建
|
||||
```bash
|
||||
git commit --allow-empty -m "ci: rebuild" && git push gitea main
|
||||
# 跟 build 进度(可选)
|
||||
ssh vps "docker ps --filter 'name=^drone-'"
|
||||
```
|
||||
|
||||
### 数据备份
|
||||
```bash
|
||||
# PG dump
|
||||
ssh vps "docker exec sub2api-pg pg_dump -U postgres sub2api" \
|
||||
> ~/backups/sub2api-$(date +%Y%m%d).sql
|
||||
|
||||
# 整个 /opt/sub2api/
|
||||
ssh vps "tar -czf /tmp/sub2api-full-$(date +%Y%m%d).tgz -C /opt sub2api"
|
||||
scp vps:/tmp/sub2api-full-*.tgz ~/backups/
|
||||
```
|
||||
|
||||
### 查关键 DB 信息
|
||||
```sql
|
||||
-- 账号池
|
||||
SELECT id, name, platform, type, status, schedulable, expires_at FROM accounts;
|
||||
|
||||
-- API keys
|
||||
SELECT id, user_id, name, status, group_id, quota, quota_used FROM api_keys;
|
||||
|
||||
-- 用户 + 余额
|
||||
SELECT id, email, role, balance FROM users;
|
||||
|
||||
-- 今日用量
|
||||
SELECT model_name, SUM(total_tokens), SUM(total_cost)
|
||||
FROM usage_logs
|
||||
WHERE created_at > NOW() - INTERVAL '24 hours'
|
||||
GROUP BY model_name
|
||||
ORDER BY 2 DESC;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 七、清理/从头重来
|
||||
|
||||
```bash
|
||||
# 停 + 删所有容器
|
||||
ssh vps "cd /opt/sub2api && docker compose down"
|
||||
|
||||
# 清数据(会彻底删 wizard 数据、用户、账号池!)
|
||||
ssh vps "rm -rf /opt/sub2api/{app-data,pg-data,redis-data}"
|
||||
ssh vps "mkdir -p /opt/sub2api/{app-data,pg-data,redis-data}"
|
||||
ssh vps "chown -R 65532:65532 /opt/sub2api/app-data"
|
||||
|
||||
# 重新起(走 Wizard)
|
||||
ssh vps "cd /opt/sub2api && docker compose up -d"
|
||||
|
||||
# Caddy 保留,不用动
|
||||
```
|
||||
|
||||
**不清数据的"纯重建"**(CI 跑一遍等价):
|
||||
```bash
|
||||
git commit --allow-empty -m "ci: rebuild" && git push gitea main
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 参考
|
||||
|
||||
- 本地开发:`LOCAL_SETUP_NOTES.md`
|
||||
- CI 配置:`.drone.yml`、`.ci/Dockerfile`、`.ci/README.md`
|
||||
- 上游:https://github.com/Wei-Shaw/sub2api
|
||||
- Fork:https://git.puro.im/purovps/sub2api
|
||||
1183
docs/design-drafts/Landing.html
Normal file
1183
docs/design-drafts/Landing.html
Normal file
File diff suppressed because it is too large
Load Diff
689
docs/design-drafts/Login.html
Normal file
689
docs/design-drafts/Login.html
Normal 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>
|
||||
734
docs/design-drafts/Register.html
Normal file
734
docs/design-drafts/Register.html
Normal 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>
|
||||
637
docs/design-drafts/v2/API Keys.html
Normal file
637
docs/design-drafts/v2/API Keys.html
Normal 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>
|
||||
561
docs/design-drafts/v2/Binding.html
Normal file
561
docs/design-drafts/v2/Binding.html
Normal 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>
|
||||
770
docs/design-drafts/v2/Dashboard.html
Normal file
770
docs/design-drafts/v2/Dashboard.html
Normal 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>
|
||||
670
docs/design-drafts/v2/Design System.html
Normal file
670
docs/design-drafts/v2/Design System.html
Normal 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>
|
||||
623
docs/design-drafts/v2/Docs.html
Normal file
623
docs/design-drafts/v2/Docs.html
Normal 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>
|
||||
222
docs/design-drafts/v2/HANDOFF.md
Normal file
222
docs/design-drafts/v2/HANDOFF.md
Normal 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 / 产品负责人确认实现细节。
|
||||
1423
docs/design-drafts/v2/Landing.html
Normal file
1423
docs/design-drafts/v2/Landing.html
Normal file
File diff suppressed because it is too large
Load Diff
351
docs/design-drafts/v2/Login.html
Normal file
351
docs/design-drafts/v2/Login.html
Normal 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>
|
||||
472
docs/design-drafts/v2/Pricing.html
Normal file
472
docs/design-drafts/v2/Pricing.html
Normal 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>
|
||||
386
docs/design-drafts/v2/Register.html
Normal file
386
docs/design-drafts/v2/Register.html
Normal 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>
|
||||
726
docs/design-drafts/v2/puro.css
Normal file
726
docs/design-drafts/v2/puro.css
Normal 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;
|
||||
}
|
||||
BIN
docs/design-drafts/v2/uploads/pasted-1776589344748-0.png
Normal file
BIN
docs/design-drafts/v2/uploads/pasted-1776589344748-0.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 337 KiB |
BIN
docs/design-drafts/v2/uploads/pasted-1776589408607-0.png
Normal file
BIN
docs/design-drafts/v2/uploads/pasted-1776589408607-0.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 183 KiB |
1746
docs/superpowers/plans/2026-04-19-puro-ai-landing-auth.md
Normal file
1746
docs/superpowers/plans/2026-04-19-puro-ai-landing-auth.md
Normal file
File diff suppressed because it is too large
Load Diff
425
docs/superpowers/specs/2026-04-19-puro-ai-landing-auth-design.md
Normal file
425
docs/superpowers/specs/2026-04-19-puro-ai-landing-auth-design.md
Normal 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** |
|
||||
| **内部代码名** | sub2api(Wei-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-serif(Inter / 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 的 brief(Stage 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 / #0f172a(slate-950 区间)
|
||||
- 主品牌色 #22d3ee(cyan-400)
|
||||
- 辅品牌色 #a855f7(purple-500)
|
||||
- 强调色 #fbbf24(amber-400,仅用于数字对比)
|
||||
- 主文 #f8fafc,副文 #94a3b8 ~ #cbd5e1
|
||||
- 卡片/表单底 #0f172a,边框 #334155
|
||||
|
||||
视觉元素:
|
||||
- 暗底上 radial gradient 光晕(青/紫双色,60% 透明度,blur)
|
||||
- 大量留白,max-width 1100px
|
||||
- 圆角:CTA 8px,卡片 12px
|
||||
- 字体:Inter 或 SF Pro(sans-serif),代码用 ui-monospace
|
||||
- 不要拟物、不要软阴影、不要 gradient 按钮
|
||||
|
||||
## 页面 1:Landing(路由 /,未登录)
|
||||
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):
|
||||
- 上方一段 toml(codex config)
|
||||
- 下方一段 bash(curl 示例)
|
||||
底注小字:支持 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-48px,weight 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. 验收标准
|
||||
|
||||
本地 preview(http://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/.card(puro.css 已 scoped 到 .puro-page,不污染)
|
||||
- [x] `pnpm run typecheck` 0 error,`pnpm run build` 成功
|
||||
- [ ] CI 构建通过,部署后 ai.puro.im 加载正常(Task 13 完成)
|
||||
@@ -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>
|
||||
|
||||
729
frontend/src/assets/puro.css
Normal file
729
frontend/src/assets/puro.css
Normal 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;
|
||||
}
|
||||
@@ -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">
|
||||
© {{ 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">
|
||||
© {{ 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,118 @@ 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;
|
||||
background: var(--bg-0);
|
||||
color: var(--text-0);
|
||||
font-family: var(--font-sans);
|
||||
min-height: 100vh;
|
||||
}
|
||||
@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;
|
||||
}
|
||||
|
||||
/* bg-glow primitives for split mode (not in .puro-page ancestor, so puro.css rules don't apply) */
|
||||
.auth-shell-split .bg-glow {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
.auth-shell-split .bg-glow::before,
|
||||
.auth-shell-split .bg-glow::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: 900px;
|
||||
height: 900px;
|
||||
border-radius: 50%;
|
||||
filter: blur(120px);
|
||||
opacity: 0.15;
|
||||
}
|
||||
.auth-shell-split .bg-glow::before {
|
||||
background: radial-gradient(circle, #22d3ee 0%, transparent 60%);
|
||||
top: -300px;
|
||||
left: -200px;
|
||||
}
|
||||
.auth-shell-split .bg-glow::after {
|
||||
background: radial-gradient(circle, #a855f7 0%, transparent 60%);
|
||||
top: 200px;
|
||||
right: -300px;
|
||||
}
|
||||
|
||||
/* Right-side form area (split mode) inherits --text-0 for headings */
|
||||
.auth-shell-split .auth-main {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
/* In split mode, force Tailwind's light-mode text grays to light colors for dark shell readability.
|
||||
* Uses :deep() to reach slotted LoginView/RegisterView content. */
|
||||
.auth-shell-split :deep(.input-label),
|
||||
.auth-shell-split :deep(label) {
|
||||
color: var(--text-1) !important;
|
||||
}
|
||||
.auth-shell-split :deep(.input) {
|
||||
background: var(--bg-1) !important;
|
||||
border-color: var(--border) !important;
|
||||
color: var(--text-0) !important;
|
||||
}
|
||||
.auth-shell-split :deep(.input::placeholder) {
|
||||
color: var(--text-3) !important;
|
||||
}
|
||||
.auth-shell-split :deep(.btn-primary) {
|
||||
/* keep existing — should still look good */
|
||||
}
|
||||
.auth-shell-split :deep(.text-gray-500),
|
||||
.auth-shell-split :deep(.text-gray-400) {
|
||||
color: var(--text-2) !important;
|
||||
}
|
||||
|
||||
/* 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>
|
||||
|
||||
@@ -498,7 +498,13 @@ export default {
|
||||
invalidResetLink: '无效的重置链接',
|
||||
invalidResetLinkHint: '此密码重置链接无效或已过期。请重新请求一个新链接。',
|
||||
requestNewResetLink: '请求新的重置链接',
|
||||
invalidOrExpiredToken: '密码重置链接无效或已过期。请重新请求一个新链接。'
|
||||
invalidOrExpiredToken: '密码重置链接无效或已过期。请重新请求一个新链接。',
|
||||
// PURO AI redesign
|
||||
puroLoginTitle: '登录',
|
||||
puroLoginSub: '用你的 PURO AI 账户继续',
|
||||
puroRegisterTitle: '创建账户',
|
||||
puroRegisterSub: '5 分钟开始用 PURO AI',
|
||||
confirmPasswordLabel: '确认密码',
|
||||
},
|
||||
|
||||
// Dashboard
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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
|
||||
|
||||
6
frontend/src/router/meta.d.ts
vendored
6
frontend/src/router/meta.d.ts
vendored
@@ -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
|
||||
|
||||
@@ -1,14 +1,51 @@
|
||||
<template>
|
||||
<AuthLayout>
|
||||
<template #narrative>
|
||||
<div class="auth-narrative-inner">
|
||||
<router-link to="/" class="brand">
|
||||
<svg viewBox="0 0 24 24" width="22" height="22" 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>
|
||||
<span>PURO AI</span>
|
||||
</router-link>
|
||||
|
||||
<div>
|
||||
<div class="n-kicker">// 你的订阅,已经付过钱了</div>
|
||||
<div class="auth-narrative-headline" style="margin-top: 12px;">
|
||||
<span class="num-n">N</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="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 class="auth-narrative-foot 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>
|
||||
</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-slate-50">{{ t('auth.puroLoginTitle') }}</h2>
|
||||
<p class="mt-2 text-sm text-slate-400">{{ t('auth.puroLoginSub') }}</p>
|
||||
</div>
|
||||
|
||||
<div v-if="!backendModeEnabled && (linuxdoOAuthEnabled || oidcOAuthEnabled)" class="space-y-4">
|
||||
@@ -439,4 +476,124 @@ 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: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-weight: 700;
|
||||
font-size: 15px;
|
||||
letter-spacing: -0.01em;
|
||||
color: var(--text-0);
|
||||
text-decoration: none;
|
||||
}
|
||||
.brand svg {
|
||||
color: var(--cyan);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.n-kicker {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.1em;
|
||||
color: var(--cyan);
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.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-n { 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);
|
||||
}
|
||||
|
||||
/* Route demo panel */
|
||||
.route-demo {
|
||||
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, 8px);
|
||||
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 .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);
|
||||
}
|
||||
|
||||
/* Bottom status bar */
|
||||
.auth-narrative-foot {
|
||||
font-size: 12px;
|
||||
color: var(--text-3);
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
.n-bottom {
|
||||
display: flex;
|
||||
gap: 14px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.n-bottom .sep { color: var(--border-2, rgba(255,255,255,0.12)); }
|
||||
.n-bottom .live {
|
||||
color: var(--green, #34d399);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
.n-bottom .live .dot {
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
border-radius: 50%;
|
||||
background: var(--green, #34d399);
|
||||
box-shadow: 0 0 6px var(--green, #34d399);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,14 +1,60 @@
|
||||
<template>
|
||||
<AuthLayout>
|
||||
<template #narrative>
|
||||
<div class="auth-narrative-inner">
|
||||
<router-link to="/" class="brand">
|
||||
<svg viewBox="0 0 24 24" width="22" height="22" 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>
|
||||
<span>PURO AI</span>
|
||||
</router-link>
|
||||
|
||||
<div>
|
||||
<div class="n-kicker">// 5 分钟开始用</div>
|
||||
<div class="auth-narrative-headline" style="margin-top: 12px;">
|
||||
<span class="num-n">N</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="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 class="auth-narrative-foot 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>
|
||||
</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-slate-50">{{ t('auth.puroRegisterTitle') }}</h2>
|
||||
<p class="mt-2 text-sm text-slate-400">{{ t('auth.puroRegisterSub') }}</p>
|
||||
</div>
|
||||
|
||||
<div v-if="linuxdoOAuthEnabled || oidcOAuthEnabled" class="space-y-4">
|
||||
@@ -111,6 +157,38 @@
|
||||
<p v-else class="input-hint">
|
||||
{{ t('auth.passwordHint') }}
|
||||
</p>
|
||||
<!-- Password strength meter -->
|
||||
<div class="pw-strength" :data-score="pwScore">
|
||||
<span class="bar"></span>
|
||||
<span class="bar"></span>
|
||||
<span class="bar"></span>
|
||||
<span class="bar"></span>
|
||||
</div>
|
||||
<div class="pw-hint" :data-score="pwScore">
|
||||
<span class="k">// strength · </span>
|
||||
<span class="val">{{ pwScoreLabel }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Confirm Password Input -->
|
||||
<div class="form-field">
|
||||
<label class="input-label">{{ t('auth.confirmPasswordLabel') }}</label>
|
||||
<div class="relative">
|
||||
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3.5">
|
||||
<Icon name="lock" size="md" class="text-gray-400 dark:text-dark-500" />
|
||||
</div>
|
||||
<input
|
||||
v-model="formData.confirmPassword"
|
||||
type="password"
|
||||
class="input pl-11"
|
||||
:placeholder="t('auth.confirmPasswordPlaceholder')"
|
||||
required
|
||||
autocomplete="new-password"
|
||||
/>
|
||||
</div>
|
||||
<div class="match-hint" v-if="formData.confirmPassword" :class="pwMatchClass">
|
||||
<span class="k">// </span><span class="val">{{ pwMatchLabel }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Invitation Code Input (Required when enabled) -->
|
||||
@@ -217,6 +295,14 @@
|
||||
</transition>
|
||||
</div>
|
||||
|
||||
<!-- Terms Checkbox -->
|
||||
<div class="terms-check">
|
||||
<label>
|
||||
<input type="checkbox" v-model="termsAccepted" class="check" />
|
||||
<span>我已阅读并同意 <a href="#" class="underline">服务条款</a> 与 <a href="#" class="underline">隐私政策</a></span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Turnstile Widget -->
|
||||
<div v-if="turnstileEnabled && turnstileSiteKey">
|
||||
<TurnstileWidget
|
||||
@@ -251,7 +337,7 @@
|
||||
<!-- Submit Button -->
|
||||
<button
|
||||
type="submit"
|
||||
:disabled="isLoading || (turnstileEnabled && !turnstileToken)"
|
||||
:disabled="isLoading || (turnstileEnabled && !turnstileToken) || !termsAccepted"
|
||||
class="btn btn-primary w-full"
|
||||
>
|
||||
<svg
|
||||
@@ -302,7 +388,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted, onUnmounted } from 'vue'
|
||||
import { ref, reactive, computed, onMounted, onUnmounted } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { AuthLayout } from '@/components/layout'
|
||||
@@ -333,6 +419,7 @@ const isLoading = ref<boolean>(false)
|
||||
const settingsLoaded = ref<boolean>(false)
|
||||
const errorMessage = ref<string>('')
|
||||
const showPassword = ref<boolean>(false)
|
||||
const termsAccepted = ref<boolean>(false)
|
||||
|
||||
// Public settings
|
||||
const registrationEnabled = ref<boolean>(true)
|
||||
@@ -373,6 +460,7 @@ let invitationValidateTimeout: ReturnType<typeof setTimeout> | null = null
|
||||
const formData = reactive({
|
||||
email: '',
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
promo_code: '',
|
||||
invitation_code: ''
|
||||
})
|
||||
@@ -384,6 +472,28 @@ const errors = reactive({
|
||||
invitation_code: ''
|
||||
})
|
||||
|
||||
// ==================== Password Strength ====================
|
||||
|
||||
const pwScore = computed(() => {
|
||||
const p = formData.password
|
||||
if (!p) return 0
|
||||
let s = 0
|
||||
if (p.length >= 8) s++
|
||||
if (/[A-Z]/.test(p) && /[a-z]/.test(p)) s++
|
||||
if (/[0-9]/.test(p)) s++
|
||||
if (/[^A-Za-z0-9]/.test(p) || p.length >= 12) s++
|
||||
return Math.min(s, 4)
|
||||
})
|
||||
const pwScoreLabel = computed(() => ['—', '弱', '中', '强', '极强'][pwScore.value])
|
||||
|
||||
// ==================== Confirm Password Match ====================
|
||||
|
||||
const pwMatch = computed(() =>
|
||||
!formData.confirmPassword ? null : formData.password === formData.confirmPassword
|
||||
)
|
||||
const pwMatchClass = computed(() => pwMatch.value === true ? 'ok' : pwMatch.value === false ? 'mismatch' : '')
|
||||
const pwMatchLabel = computed(() => pwMatch.value === true ? 'matched' : pwMatch.value === false ? 'passwords do not match' : '')
|
||||
|
||||
// ==================== Lifecycle ====================
|
||||
|
||||
onMounted(async () => {
|
||||
@@ -634,6 +744,12 @@ function validateForm(): boolean {
|
||||
isValid = false
|
||||
}
|
||||
|
||||
// Confirm password validation
|
||||
if (formData.password && formData.confirmPassword && formData.password !== formData.confirmPassword) {
|
||||
errorMessage.value = t('auth.passwordsDoNotMatch')
|
||||
isValid = false
|
||||
}
|
||||
|
||||
// Invitation code validation (required when enabled)
|
||||
if (invitationCodeEnabled.value) {
|
||||
if (!formData.invitation_code.trim()) {
|
||||
@@ -662,6 +778,12 @@ async function handleRegister(): Promise<void> {
|
||||
return
|
||||
}
|
||||
|
||||
// Check passwords match
|
||||
if (formData.confirmPassword && formData.password !== formData.confirmPassword) {
|
||||
errorMessage.value = t('auth.passwordsDoNotMatch')
|
||||
return
|
||||
}
|
||||
|
||||
// Check promo code validation status
|
||||
if (formData.promo_code.trim()) {
|
||||
// If promo code is being validated, wait
|
||||
@@ -767,4 +889,216 @@ 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: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-weight: 700;
|
||||
font-size: 15px;
|
||||
letter-spacing: -0.01em;
|
||||
color: var(--text-0);
|
||||
text-decoration: none;
|
||||
}
|
||||
.brand svg {
|
||||
color: var(--cyan);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.n-kicker {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.1em;
|
||||
color: var(--cyan);
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.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-n { 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);
|
||||
}
|
||||
|
||||
/* 3-step onboarding panel */
|
||||
.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, 8px);
|
||||
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);
|
||||
}
|
||||
|
||||
/* Bottom status bar */
|
||||
.auth-narrative-foot {
|
||||
font-size: 12px;
|
||||
color: var(--text-3);
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
.n-bottom {
|
||||
display: flex;
|
||||
gap: 14px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.n-bottom .sep { color: var(--border-2, rgba(255, 255, 255, 0.12)); }
|
||||
.n-bottom .live {
|
||||
color: var(--green, #34d399);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
.n-bottom .live .dot {
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
border-radius: 50%;
|
||||
background: var(--green, #34d399);
|
||||
box-shadow: 0 0 6px var(--green, #34d399);
|
||||
}
|
||||
|
||||
/* Password strength meter */
|
||||
.pw-strength {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
.pw-strength .bar {
|
||||
flex: 1;
|
||||
height: 3px;
|
||||
background: var(--border);
|
||||
border-radius: 2px;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
.pw-strength[data-score="1"] .bar:nth-child(-n+1),
|
||||
.pw-strength[data-score="2"] .bar:nth-child(-n+2),
|
||||
.pw-strength[data-score="3"] .bar:nth-child(-n+3),
|
||||
.pw-strength[data-score="4"] .bar:nth-child(-n+4) {
|
||||
background: var(--strength-color, #f87171);
|
||||
}
|
||||
.pw-strength[data-score="1"] { --strength-color: #f87171; }
|
||||
.pw-strength[data-score="2"] { --strength-color: #fbbf24; }
|
||||
.pw-strength[data-score="3"] { --strength-color: #22d3ee; }
|
||||
.pw-strength[data-score="4"] { --strength-color: #34d399; }
|
||||
|
||||
.pw-hint {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11px;
|
||||
margin-top: 6px;
|
||||
}
|
||||
.pw-hint .k { color: var(--text-3); }
|
||||
.pw-hint .val { color: var(--strength-color, var(--text-2)); }
|
||||
.pw-hint[data-score="1"] { --strength-color: #f87171; }
|
||||
.pw-hint[data-score="2"] { --strength-color: #fbbf24; }
|
||||
.pw-hint[data-score="3"] { --strength-color: #22d3ee; }
|
||||
.pw-hint[data-score="4"] { --strength-color: #34d399; }
|
||||
|
||||
/* Confirm password match hint */
|
||||
.match-hint {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11px;
|
||||
margin-top: 6px;
|
||||
}
|
||||
.match-hint.ok { color: #34d399; }
|
||||
.match-hint.mismatch { color: #f87171; }
|
||||
.match-hint .k { color: var(--text-3); }
|
||||
|
||||
/* Terms checkbox */
|
||||
.terms-check {
|
||||
margin-top: 16px;
|
||||
font-size: 13px;
|
||||
color: var(--text-2);
|
||||
}
|
||||
.terms-check label {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.terms-check a {
|
||||
color: var(--cyan, #22d3ee);
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 2px;
|
||||
}
|
||||
.terms-check input[type="checkbox"] {
|
||||
accent-color: var(--cyan, #22d3ee);
|
||||
margin-top: 3px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
213
frontend/src/views/docs/DocsView.vue
Normal file
213
frontend/src/views/docs/DocsView.vue
Normal 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> · OpenAI(via ChatGPT Plus / Codex OAuth)</li>
|
||||
<li><code class="mono">gpt-5.4-codex</code> · OpenAI Codex 专用</li>
|
||||
<li><code class="mono">claude-opus-4-7</code> · Anthropic(via Claude Pro / Max OAuth)</li>
|
||||
<li><code class="mono">claude-sonnet-4-6</code> · Anthropic</li>
|
||||
<li><code class="mono">gemini-2.5-pro</code> · Google(via Code Assist OAuth)</li>
|
||||
<li><code class="mono">gemini-2.5-flash</code> · Google</li>
|
||||
</ul>
|
||||
<p class="note">后端 pricing 表实时跟进 <code class="mono">model-price-repo</code>,完整清单登录后在 <router-link to="/dashboard">控制台</router-link> 查看。</p>
|
||||
</section>
|
||||
|
||||
<section id="feedback" class="docs-section">
|
||||
<h2>6. 问题反馈</h2>
|
||||
<p>遇到问题或希望补接某个平台:</p>
|
||||
<div class="callout">
|
||||
<a href="mailto:admin@puro.im">admin@puro.im</a>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// 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>
|
||||
846
frontend/src/views/landing/LandingView.vue
Normal file
846
frontend/src/views/landing/LandingView.vue
Normal file
@@ -0,0 +1,846 @@
|
||||
<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">
|
||||
<svg viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="1.8">
|
||||
<path d="M12 2L21 7V17L12 22L3 17V7L12 2Z" fill="rgba(34, 211, 238, 0.08)"/>
|
||||
</svg>
|
||||
<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="badge">NEW</span>
|
||||
<span>统一接入多个 AI 平台 · 零改动切换</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,零改动接入 <span class="pill-inline">OpenAI</span> / <span class="pill-inline">Anthropic</span> 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">// providers</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-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>
|
||||
<div class="model-name">Claude Pro / Max</div>
|
||||
<div class="model-meta">Anthropic OAuth</div>
|
||||
</div>
|
||||
<div class="status-chip"><span class="dot"></span>online</div>
|
||||
</div>
|
||||
<div class="model-card">
|
||||
<div class="model-logo">
|
||||
<svg width="22" height="22" 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>
|
||||
</div>
|
||||
<div>
|
||||
<div class="model-name">ChatGPT Plus / Pro</div>
|
||||
<div class="model-meta">OpenAI OAuth</div>
|
||||
</div>
|
||||
<div class="status-chip"><span class="dot"></span>online</div>
|
||||
</div>
|
||||
<div class="model-card">
|
||||
<div class="model-logo">
|
||||
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="#f8fafc" stroke-width="1.6"><rect x="4" y="4" width="16" height="16" rx="3" fill="none" stroke="currentColor" stroke-width="1.6"/><path d="M8 10l-2 2 2 2M16 10l2 2-2 2M14 8l-4 8" stroke="currentColor" stroke-width="1.6" fill="none" stroke-linecap="round"/></svg>
|
||||
</div>
|
||||
<div>
|
||||
<div class="model-name">Codex CLI</div>
|
||||
<div class="model-meta">OpenAI OAuth</div>
|
||||
</div>
|
||||
<div class="status-chip"><span class="dot"></span>online</div>
|
||||
</div>
|
||||
<div class="model-card">
|
||||
<div class="model-logo">
|
||||
<svg width="22" height="22" viewBox="0 0 24 24" fill="#4285f4"><path d="M12 2L14 10L22 12L14 14L12 22L10 14L2 12L10 10Z"/></svg>
|
||||
</div>
|
||||
<div>
|
||||
<div class="model-name">Gemini Code Assist</div>
|
||||
<div class="model-meta">Google OAuth</div>
|
||||
</div>
|
||||
<div class="status-chip"><span class="dot"></span>online</div>
|
||||
</div>
|
||||
<div class="model-card is-muted">
|
||||
<div class="model-logo">
|
||||
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="#94a3b8" stroke-width="1.6" stroke-linecap="round"><circle cx="5" cy="12" r="1.5" fill="#94a3b8"/><circle cx="12" cy="12" r="1.5" fill="#94a3b8"/><circle cx="19" cy="12" r="1.5" fill="#94a3b8"/></svg>
|
||||
</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">// capabilities</div>
|
||||
<h2 class="section-title">付一次订阅,<br>用起一整个模型池</h2>
|
||||
<p class="section-sub">把散落在各个平台的订阅,整合成开发者真正能用的基础设施</p>
|
||||
</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>
|
||||
<ul class="feature-bullets">
|
||||
<li>OpenAI Responses API 兼容</li>
|
||||
<li>Anthropic Messages API 兼容</li>
|
||||
<li>智能 model → provider 路由</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="feature">
|
||||
<div class="feature-icon">🔄</div>
|
||||
<h3>账号池高可用</h3>
|
||||
<p>支持多账号自动调度与 failover。某个上游触发限流 / 冷却时,流量切到下一个健康账号,token 刷新全自动。</p>
|
||||
<ul class="feature-bullets">
|
||||
<li>限流/5xx 自动 failover</li>
|
||||
<li>OAuth token 自动刷新</li>
|
||||
<li>加权轮询 · 最少连接</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="feature">
|
||||
<div class="feature-icon">📊</div>
|
||||
<h3>用量看板</h3>
|
||||
<p>每条请求的 tokens、费用、上游账号、延迟全可视化。模型分布饼图 + 趋势曲线 + Top 排行。</p>
|
||||
<ul class="feature-bullets">
|
||||
<li>逐请求审计日志</li>
|
||||
<li>多维度 tokens / cost 统计</li>
|
||||
<li>导出 CSV / 接 Webhook</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ④ Code Demo -->
|
||||
<section class="block container" id="code">
|
||||
<div class="section-header">
|
||||
<div class="section-kicker">// integration</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-head">
|
||||
<div class="traffic">
|
||||
<span></span><span></span><span></span>
|
||||
</div>
|
||||
<div class="code-tabs">
|
||||
<span class="tab active">~/.codex/config.toml</span>
|
||||
</div>
|
||||
<span class="meta">edited 2s ago</span>
|
||||
</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-head">
|
||||
<div class="traffic">
|
||||
<span></span><span></span><span></span>
|
||||
</div>
|
||||
<div class="code-tabs">
|
||||
<span class="tab active">curl.sh</span>
|
||||
</div>
|
||||
<span class="meta">zsh · puro ≈ 210ms</span>
|
||||
</div>
|
||||
<pre class="mono"><code><span class="cm">$</span> curl https://ai.puro.im/responses \
|
||||
-H <span class="str">"Authorization: Bearer sk-xxx"</span> \
|
||||
-d <span class="str">'{"model":"gpt-5.4","input":"hello"}'</span></code></pre>
|
||||
</div>
|
||||
</div>
|
||||
<div class="code-foot">支持 OpenAI Responses API · Anthropic Messages API · Gemini generateContent · 流式 SSE & WebSocket</div>
|
||||
</section>
|
||||
|
||||
<!-- ⑤ Dashboard mockup -->
|
||||
<section class="block container" id="dashboard">
|
||||
<div class="section-header">
|
||||
<div class="section-kicker">// observability</div>
|
||||
<h2 class="section-title">每条请求都看得见</h2>
|
||||
<p class="section-sub">不像第三方 API 池子那种"扣了多少不告诉你"——扣哪个账号、跑哪个模型、用了多少 tokens、上游响应几秒,一目了然。</p>
|
||||
</div>
|
||||
<div class="dash-mock">
|
||||
<!-- browser chrome header -->
|
||||
<div class="dash-chrome">
|
||||
<div class="traffic">
|
||||
<span></span><span></span><span></span>
|
||||
</div>
|
||||
<div class="url-bar">ai.puro.im/dashboard</div>
|
||||
<span class="dash-user mono">me@puro</span>
|
||||
</div>
|
||||
<div class="dash-layout">
|
||||
<!-- sidebar -->
|
||||
<aside class="dash-side">
|
||||
<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</div>
|
||||
<div class="side-item"><span class="ico">📜</span> Logs</div>
|
||||
<div class="side-item"><span class="ico">🔌</span> Accounts<span class="side-count">12</span></div>
|
||||
</div>
|
||||
<div class="side-group">
|
||||
<div class="side-label">SETTINGS</div>
|
||||
<div class="side-item"><span class="ico">👥</span> Team</div>
|
||||
<div class="side-item"><span class="ico">💳</span> Billing</div>
|
||||
<div class="side-item"><span class="ico">👤</span> Profile</div>
|
||||
</div>
|
||||
</aside>
|
||||
<!-- main content -->
|
||||
<div class="dash-main">
|
||||
<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-grid">
|
||||
<div class="chart-card">
|
||||
<div class="chart-title">
|
||||
近 30 天用量趋势
|
||||
<div class="chart-legend">
|
||||
<span><span class="sw" style="background: var(--cyan)"></span> Claude</span>
|
||||
<span><span class="sw" style="background: #a855f7"></span> GPT</span>
|
||||
<span><span class="sw" style="background: var(--amber)"></span> Gemini</span>
|
||||
</div>
|
||||
</div>
|
||||
<svg viewBox="0 0 500 140" class="chart-svg">
|
||||
<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="70" x2="500" y2="70"/>
|
||||
<line x1="0" y1="110" x2="500" y2="110"/>
|
||||
</g>
|
||||
<path d="M0,100 L40,85 L80,90 L120,65 L160,75 L200,50 L240,58 L280,38 L320,45 L360,25 L400,38 L440,28 L500,18 L500,140 L0,140 Z" fill="url(#gc)"/>
|
||||
<path d="M0,100 L40,85 L80,90 L120,65 L160,75 L200,50 L240,58 L280,38 L320,45 L360,25 L400,38 L440,28 L500,18" stroke="#22d3ee" stroke-width="2" fill="none"/>
|
||||
<path d="M0,115 L40,108 L80,100 L120,108 L160,92 L200,96 L240,75 L280,83 L320,65 L360,72 L400,56 L440,62 L500,46" stroke="#a855f7" stroke-width="2" fill="none"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="chart-card donut-card">
|
||||
<div class="chart-title">Model distribution <span class="chart-sub">· 24h</span></div>
|
||||
<div class="donut-wrap">
|
||||
<svg viewBox="0 0 42 42" class="donut-svg">
|
||||
<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 class="donut-legend">
|
||||
<div class="donut-row"><span><span class="sw" style="background:#22d3ee"></span>Claude</span><span class="pct">48%</span></div>
|
||||
<div class="donut-row"><span><span class="sw" style="background:#a855f7"></span>GPT</span><span class="pct">32%</span></div>
|
||||
<div class="donut-row"><span><span class="sw" style="background:#fbbf24"></span>Gemini</span><span class="pct">14%</span></div>
|
||||
<div class="donut-row"><span><span class="sw" style="background:#64748b"></span>Codex</span><span class="pct">6%</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ⑥ Footer -->
|
||||
<footer class="puro-footer">
|
||||
<div class="container footer-grid">
|
||||
<div class="footer-brand">
|
||||
<div class="brand">
|
||||
<svg viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="1.8">
|
||||
<path d="M12 2L21 7V17L12 22L3 17V7L12 2Z" fill="rgba(34, 211, 238, 0.08)"/>
|
||||
</svg>
|
||||
<span>PURO AI</span>
|
||||
</div>
|
||||
<p class="footer-tagline">把多个 AI 订阅聚合成统一 API。<br>让「已经付过钱」的订阅真正为你工作。</p>
|
||||
<p class="footer-meta">© 2026 puro.im · MIT License<br>fork of Wei-Shaw/sub2api</p>
|
||||
<div class="footer-status"><span class="dot-green"></span>all systems operational</div>
|
||||
</div>
|
||||
<div class="footer-col">
|
||||
<div class="footer-col-title">产品</div>
|
||||
<a href="/docs">文档</a>
|
||||
<a href="#features">功能</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>
|
||||
<router-link to="/login">登录</router-link>
|
||||
<router-link to="/register">注册</router-link>
|
||||
<a href="/dashboard">Dashboard</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>
|
||||
<a href="https://git.puro.im/purovps/sub2api" target="_blank" rel="noopener noreferrer">GitHub ↗</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 {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 14px 6px 6px;
|
||||
border: 1px solid var(--border-2);
|
||||
border-radius: 100px;
|
||||
font-size: 12px;
|
||||
color: var(--text-1);
|
||||
margin-bottom: 32px;
|
||||
background: rgba(15, 23, 42, 0.6);
|
||||
}
|
||||
.hero-eyebrow .badge {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
font-family: var(--font-mono);
|
||||
color: var(--bg-0);
|
||||
background: var(--cyan);
|
||||
border-radius: 4px;
|
||||
letter-spacing: 0.1em;
|
||||
margin-right: 4px;
|
||||
}
|
||||
.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-sub .pill-inline {
|
||||
display: inline-block;
|
||||
padding: 1px 8px;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 13px;
|
||||
border: 1px solid var(--border-2);
|
||||
border-radius: 4px;
|
||||
background: rgba(15, 23, 42, 0.6);
|
||||
color: var(--text-1);
|
||||
margin: 0 2px;
|
||||
}
|
||||
.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);
|
||||
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; }
|
||||
|
||||
/* brand SVG */
|
||||
.brand svg { color: var(--cyan); flex-shrink: 0; }
|
||||
|
||||
/* 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-logo {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: var(--r-md);
|
||||
border: 1px solid var(--border-2);
|
||||
background: var(--bg-2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
color: var(--cyan);
|
||||
}
|
||||
.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); }
|
||||
.model-card .status-chip {
|
||||
margin-left: auto;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 10px;
|
||||
color: var(--green, #34d399);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
.model-card .status-chip .dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: var(--green, #34d399);
|
||||
box-shadow: 0 0 6px rgba(52,211,153,0.6);
|
||||
}
|
||||
.model-card.is-muted .status-chip { display: none; }
|
||||
|
||||
/* 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; }
|
||||
.feature-bullets {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin-top: 16px;
|
||||
padding-top: 14px;
|
||||
border-top: 1px dashed var(--border);
|
||||
}
|
||||
.feature-bullets li {
|
||||
padding: 6px 0;
|
||||
color: var(--text-2);
|
||||
font-size: 12px;
|
||||
font-family: var(--font-mono);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.feature-bullets li::before {
|
||||
content: '→';
|
||||
color: var(--cyan);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* 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-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 10px 14px;
|
||||
gap: 12px;
|
||||
background: var(--bg-1);
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.traffic { display: flex; gap: 6px; }
|
||||
.traffic span {
|
||||
width: 10px; height: 10px; border-radius: 50%;
|
||||
}
|
||||
.traffic span:nth-child(1) { background: #f87171; }
|
||||
.traffic span:nth-child(2) { background: #fbbf24; }
|
||||
.traffic span:nth-child(3) { background: #34d399; }
|
||||
.code-tabs .tab {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11px;
|
||||
color: var(--text-2);
|
||||
padding: 2px 10px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
}
|
||||
.code-tabs .tab.active {
|
||||
color: var(--cyan);
|
||||
background: rgba(34,211,238,0.1);
|
||||
border-color: rgba(34,211,238,0.3);
|
||||
}
|
||||
.code-head .meta {
|
||||
margin-left: auto;
|
||||
font-size: 11px;
|
||||
font-family: var(--font-mono);
|
||||
color: var(--text-3);
|
||||
}
|
||||
.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);
|
||||
}
|
||||
|
||||
/* browser chrome */
|
||||
.dash-chrome {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
background: rgba(15, 23, 42, 0.7);
|
||||
}
|
||||
.url-bar {
|
||||
flex: 1;
|
||||
padding: 5px 12px;
|
||||
background: var(--bg-0);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11px;
|
||||
color: var(--text-2);
|
||||
}
|
||||
.dash-user { font-size: 11px; color: var(--text-3); }
|
||||
|
||||
/* dashboard layout */
|
||||
.dash-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 180px 1fr;
|
||||
min-height: 460px;
|
||||
}
|
||||
@media (max-width: 720px) {
|
||||
.dash-layout { grid-template-columns: 1fr; }
|
||||
.dash-side { display: none; }
|
||||
}
|
||||
.dash-side {
|
||||
border-right: 1px solid var(--border);
|
||||
padding: 16px 10px;
|
||||
background: rgba(2, 6, 23, 0.4);
|
||||
}
|
||||
.side-group { margin-bottom: 20px; }
|
||||
.side-label {
|
||||
font-size: 10px;
|
||||
color: var(--text-3);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.12em;
|
||||
padding: 0 8px 6px;
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
.side-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 8px;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
color: var(--text-2);
|
||||
cursor: pointer;
|
||||
}
|
||||
.side-item.active { background: rgba(34, 211, 238, 0.08); color: var(--cyan); }
|
||||
.side-item .ico { font-size: 12px; }
|
||||
.side-count {
|
||||
margin-left: auto;
|
||||
font-size: 10px;
|
||||
color: var(--text-3);
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
.dash-main { 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 grid: 2fr line + 1fr donut */
|
||||
.chart-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 2fr 1fr;
|
||||
gap: 14px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
@media (max-width: 720px) { .chart-grid { grid-template-columns: 1fr; } }
|
||||
.chart-card {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--r-md);
|
||||
background: rgba(15,23,42,0.6);
|
||||
padding: 16px;
|
||||
}
|
||||
.chart-title {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--text-2);
|
||||
margin-bottom: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.chart-sub { font-weight: 400; color: var(--text-3); }
|
||||
.chart-legend {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
font-size: 11px;
|
||||
color: var(--text-3);
|
||||
font-weight: 400;
|
||||
}
|
||||
.chart-legend span { display: inline-flex; align-items: center; gap: 4px; }
|
||||
.sw { display: inline-block; width: 8px; height: 8px; border-radius: 2px; }
|
||||
.chart-svg { width: 100%; height: 140px; display: block; }
|
||||
|
||||
/* donut chart */
|
||||
.donut-card .chart-title { margin-bottom: 8px; }
|
||||
.donut-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
.donut-svg {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
transform: rotate(-90deg);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.donut-legend {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 7px;
|
||||
font-size: 11px;
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
.donut-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
color: var(--text-1);
|
||||
}
|
||||
.donut-row span:first-child { display: flex; align-items: center; }
|
||||
.donut-row .pct { color: var(--text-3); }
|
||||
|
||||
/* 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;
|
||||
}
|
||||
.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; line-height: 1.6; margin-bottom: 8px; max-width: 280px; }
|
||||
.footer-meta { color: var(--text-3); font-size: 12px; line-height: 1.7; margin-bottom: 12px; }
|
||||
.footer-status {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11px;
|
||||
color: var(--text-3);
|
||||
}
|
||||
.dot-green {
|
||||
display: inline-block;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: var(--green, #34d399);
|
||||
box-shadow: 0 0 6px rgba(52,211,153,0.6);
|
||||
}
|
||||
.footer-col-title {
|
||||
color: var(--text-0);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
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); }
|
||||
</style>
|
||||
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user