From f6ce5267d59c8a7ecf3bee417624589d2acc213a Mon Sep 17 00:00:00 2001 From: wangjia <809946525@qq.com> Date: Sat, 11 Jul 2026 23:45:55 +0800 Subject: [PATCH] =?UTF-8?q?feat(design):=20=E6=94=AF=E4=BB=98=E6=B8=A0?= =?UTF-8?q?=E9=81=93=20switch=20=E5=8E=9F=E5=9E=8B=20+=20segswitch=20?= =?UTF-8?q?=E5=8E=9F=E5=AD=90=20+=20=E5=9B=BE=E6=A0=87/=E8=AF=AD=E8=A8=80?= =?UTF-8?q?=E5=90=8C=E6=BA=90=E9=97=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - .segswitch 段控原子入 atoms.css + 登记 index.html - purchase/ui-mobile/ui-desktop 三屏:购买页支付渠道 switch(USDT→crypto 默认 / 人民币→alipay),套餐价随渠道分币种显示,删两步弹层 + nezha - 订单列表/详情集成进移动+桌面主框架 - icons.js 58 图标 sprite 单源 + gen_flutter_icons codegen + check-l1-sync 同源闸(图标同集 + l10n 完整) - pay-v2 开发决策日志 docs/pay-v2-dev-log.md Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_013nMthbVEmQquxBRKb9Fj8u --- design/CLAUDE.md | 9 +- design/codegen/gen_flutter_icons.mjs | 45 + design/prototype/assets | 1 + design/prototype/atoms.css | 454 +++++- design/prototype/icons.js | 1 - design/prototype/index.html | 530 ++++++- design/prototype/screens/orders.html | 342 +++++ design/prototype/screens/purchase.html | 403 ++++++ design/prototype/screens/site.html | 802 +++++++++++ design/prototype/screens/ui-desktop.html | 1488 ++++++++++++++++++++ design/prototype/screens/ui-mobile.html | 1607 ++++++++++++++++++++++ design/prototype/tools/check-l1-sync.mjs | 62 + docs/pay-v2-dev-log.md | 28 + tools/check-l1-sync.mjs | 189 +-- 14 files changed, 5800 insertions(+), 161 deletions(-) create mode 100644 design/codegen/gen_flutter_icons.mjs create mode 120000 design/prototype/assets create mode 100644 design/prototype/screens/orders.html create mode 100644 design/prototype/screens/purchase.html create mode 100644 design/prototype/screens/site.html create mode 100644 design/prototype/screens/ui-desktop.html create mode 100644 design/prototype/screens/ui-mobile.html create mode 100644 design/prototype/tools/check-l1-sync.mjs create mode 100644 docs/pay-v2-dev-log.md diff --git a/design/CLAUDE.md b/design/CLAUDE.md index 6ad46dc..bff10e7 100644 --- a/design/CLAUDE.md +++ b/design/CLAUDE.md @@ -96,7 +96,7 @@ SKILL.md ← Agent Skill 入口 - 登录/注册(`AuthFlow` / `auth_screen.dart`):登录=邮箱+密码;注册=邮箱→发验证码→填码→设密码(多端登录) - 首次引导(`Onboarding` / `onboarding_screen.dart`):3 屏(连接全球→授权配置→安全无日志),可跳过 - 主框架 4 Tab(`home_shell.dart`):**连接 / 节点 / 统计 / 账户**;内容区支持**左右滑动切换 Tab**(>60px 且横向位移明显大于纵向,200ms 方向感知滑入;子页不响应) - - 连接:核心连接键三态(off 暖灰底+虚线轨道环 / connecting 旋转弧 / on 绿底+满环+计时)+ 当前节点卡 + 实时速率;**免费版额外显示额度卡**(今日剩余分钟 + 进度条(≤3 分钟变 warning 色)+ 「看广告开始使用」→ 点击后变绿色「已解锁 · 今日可用」) + - 连接:核心连接键**四态**(off 暖灰底+虚线轨道环 / connecting 旋转弧 / on 绿底+满环+计时 / **disabled 免费到点切断:暖灰底+lock 图标+not-allowed,点击弹「看广告加时」**)+ 当前节点卡 + 实时速率;**免费版额外显示额度卡**(今日剩余分钟 + 进度条(≤3 分钟变 warning、耗尽变 danger)+ 「看广告加时」;**连接期用倒计时收缩条表达剩余,无「已解锁」绿态** —— 旧「点击后变绿色已解锁」描述已废,以线上 `quota_card.dart` 为准) - 节点:顶部置顶**「智能选择」推荐卡**(常驻 accent-subtle 底 + clay 渐变 zap 图标 + 「推荐」胶囊 + 文案「根据当前网络环境,自动选择最优节点」,默认选中);其下才是搜索 + 节点列表(国家码块·延迟·信号条·选中态) - 统计:流量/延迟/时长指标卡 + 本周柱状图 - 账户:套餐横幅(免费版/PRO 自适应)+ 账户信息(邮箱/密码/退出)+ 设备管理 + 兑换/联系入口 @@ -108,10 +108,11 @@ SKILL.md ← Agent Skill 入口 **Web 用户中心**(`ui_kits/usercenter/`):概览(套餐状态/用量)· 订阅导入(订阅链接+二维码+一键导入三方客户端)· 兑换 · 邀请返利 · 设置(含 **2FA/TOTP 开关**,用户可自选开启)。响应式,移动端底部 Tab + 左右滑动切换。 -**核心连接键**(最重要的组件,务必还原):圆形,三态—— -- off:暖灰底 `sand-100` + clay power 图标 + 虚线轨道环 + 「点击连接」 +**核心连接键**(最重要的组件,务必还原):圆形,**四态**(对齐线上 `connect_button.dart`)—— +- off:暖灰底 `bg-subtle` + clay power 图标 + 虚线轨道环 + 「点击连接」 - connecting:clay 实底 + 旋转进度弧 + 旋转 loader - on:success 实底 + 白盾勾 + 满白环 + 圆内计时 + 「已加密」+ 绿光晕 +- **disabled**(免费到点切断/额度耗尽):`bg-subtle` 底 + `fg3` 前景 + **lock 图标**(非 power)+ not-allowed;点击走「看广告加时」。原型原子 `.connect-btn.is-disabled` > 注意:背景在渐变↔纯色间切换时,过渡只用 `box-shadow`/`background-color`,**不要 `transition: all`**(会导致背景计算失效)。 --- @@ -169,5 +170,5 @@ SKILL.md ← Agent Skill 入口 - [ ] 明/暗两主题均验证过;中/英两语言均验证过(单显不并排) - [ ] 文案无铁律 13 红线词;套餐数字与 §7 一致 - [ ] 图标只用 Lucide;国家码块无 emoji 国旗;状态胶囊无 emoji -- [ ] 连接键三态、智能选择默认选中、免费版额度/广告解锁流程与 React 原型一致 +- [ ] 连接键四态(含免费到点 disabled 锁态)、智能选择默认选中、免费版额度(倒计时收缩条)/看广告加时流程与原型单源一致 - [ ] App/官网内无任何支付表单,购买只引导到外部渠道 diff --git a/design/codegen/gen_flutter_icons.mjs b/design/codegen/gen_flutter_icons.mjs new file mode 100644 index 0000000..b6fd06b --- /dev/null +++ b/design/codegen/gen_flutter_icons.mjs @@ -0,0 +1,45 @@ +#!/usr/bin/env node +// gen_flutter_icons.mjs —— 图标单源 codegen(ds-flow step 2a)。 +// 图标真源 = design/prototype/icons.js(Lucide sprite)。读其 key 集,kebab→camel, +// 生成 client/lib/widgets/pangolin_icons.dart(每个 sprite key 一行 static const)。 +// 零依赖(Node fs)。运行:node design/codegen/gen_flutter_icons.mjs +import { readFileSync, writeFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { dirname, resolve } from 'node:path'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const root = resolve(__dirname, '..', '..'); +const SPRITE = resolve(root, 'design/prototype/icons.js'); +const OUT = resolve(root, 'client/lib/widgets/pangolin_icons.dart'); + +const src = readFileSync(SPRITE, 'utf8'); +// 抽 key:形如 'kebab-name': '<... +const keys = []; +const re = /'([a-z0-9-]+)'\s*:\s*' (seen.has(k) ? false : (seen.add(k), true))); + +const camel = (k) => k.replace(/-([a-z0-9])/g, (_, c) => c.toUpperCase()); + +const lines = ordered.map((k) => { + const c = camel(k); + return ` static const ${c} = LucideIcons.${c};`; +}); + +const out = `// GENERATED by design/codegen/gen_flutter_icons.mjs —— 勿手改;图标真源=design/prototype/icons.js +// 重新生成:node design/codegen/gen_flutter_icons.mjs +import 'package:lucide_icons_flutter/lucide_icons.dart'; + +class PangolinIcons { + PangolinIcons._(); + +${lines.join('\n')} +} +`; + +writeFileSync(OUT, out); +console.log(`generated ${OUT} (${ordered.length} icons)`); diff --git a/design/prototype/assets b/design/prototype/assets new file mode 120000 index 0000000..ec2e4be --- /dev/null +++ b/design/prototype/assets @@ -0,0 +1 @@ +../assets \ No newline at end of file diff --git a/design/prototype/atoms.css b/design/prototype/atoms.css index cbc0307..f45d424 100644 --- a/design/prototype/atoms.css +++ b/design/prototype/atoms.css @@ -82,8 +82,8 @@ } .btn-subtle:hover { border-color: var(--accent); color: var(--accent); } -/* Danger — 危险动作(断开 / 移除设备) */ -.btn-danger { background: var(--danger-subtle); color: var(--red-600); } +/* Danger — 危险动作(断开 / 移除设备)。文字用语义 --danger(对齐线上 c.danger,随主题翻色) */ +.btn-danger { background: var(--danger-subtle); color: var(--danger); } .btn-danger:hover { background: var(--danger); color: var(--fg-on-accent); } /* 尺寸 / 形态修饰 */ @@ -94,12 +94,13 @@ /* ============================================================= CARD · .card - surface 底 + border + radius-xl + shadow-sm + surface 底 + border + radius-lg + shadow-sm + 圆角对齐线上 cardTheme(PangolinRadius.lg=14);阴影保留 shadow-sm(暖阴影铁律)。 ============================================================= */ .card { background: var(--surface); border: 1px solid var(--border); - border-radius: var(--radius-xl); + border-radius: var(--radius-lg); box-shadow: var(--shadow-sm); padding: var(--space-5); } @@ -164,27 +165,29 @@ line-height: 1; padding: var(--space-2) var(--space-3); border-radius: var(--radius-full); - background: var(--sand-100); + background: var(--bg-subtle); color: var(--fg2); } .pill .dot { width: 7px; height: 7px; border-radius: var(--radius-full); - background: var(--sand-400); + background: var(--fg3); flex-shrink: 0; } /* alias:.badge === .pill(语义等价,命名习惯不同) */ .badge { } -.pill.is-success { background: var(--success-subtle); color: var(--green-600); } +/* 状态色对齐线上 status_pill.dart:文字用语义档 --success/--warning/--danger(非 -600 深档), + neutral 用 --bg-subtle/--fg3(原 sand-100/sand-400 硬编码在深色不翻色,已修)。 */ +.pill.is-success { background: var(--success-subtle); color: var(--success); } .pill.is-success .dot { background: var(--success); } -.pill.is-warning { background: var(--warning-subtle); color: var(--amber-600); } +.pill.is-warning { background: var(--warning-subtle); color: var(--warning); } .pill.is-warning .dot { background: var(--warning); } -.pill.is-danger { background: var(--danger-subtle); color: var(--red-600); } +.pill.is-danger { background: var(--danger-subtle); color: var(--danger); } .pill.is-danger .dot { background: var(--danger); } -.pill.is-neutral { background: var(--sand-100); color: var(--fg2); } -.pill.is-neutral .dot { background: var(--sand-400); } +.pill.is-neutral { background: var(--bg-subtle); color: var(--fg2); } +.pill.is-neutral .dot { background: var(--fg3); } /* 实心 accent(PRO 会员等强调标签) */ .pill-accent { background: var(--accent); color: var(--fg-on-accent); } @@ -246,6 +249,8 @@ gap: 1px; z-index: 60; } +/* hidden 属性生效:菜单默认收起,靠 JS 去/加 hidden 或 .is-open 控制显隐 */ +.menu[hidden] { display: none; } .menu-item { display: block; width: 100%; @@ -270,3 +275,430 @@ color: var(--accent); font-weight: 700; } + + +/* ============================================================= + PLAN CARD · .plan-card (购买套餐卡) + surface 底常规档;.is-featured 走 clay 渐变高亮(年付/最优惠)。 + 镜像 client/lib/widgets/plan_card.dart 的视觉配方(原型先行单源)。 + 子件:.plan-name / .plan-price / .plan-per / .plan-feats / .plan-feat + / .plan-pop(顶部「最受欢迎」浮标)。 + ============================================================= */ +.plan-card { + position: relative; + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius-xl); + box-shadow: var(--shadow-sm); + padding: var(--space-6) var(--space-5) var(--space-5); +} +.plan-name { + font-family: var(--font-sans); + font-size: var(--text-sm); + font-weight: 600; + color: var(--fg2); +} +.plan-price-row { display: flex; align-items: baseline; gap: var(--space-2); margin-top: var(--space-2); } +.plan-price { + font-family: var(--font-display); + font-size: var(--text-display); + font-weight: 700; + line-height: 1; + letter-spacing: var(--tracking-tight); + color: var(--fg1); +} +.plan-per { font-family: var(--font-sans); font-size: var(--text-sm); color: var(--fg3); } +.plan-feats { margin: var(--space-4) 0 var(--space-5); display: flex; flex-direction: column; gap: var(--space-2); } +.plan-feat { display: flex; align-items: center; gap: var(--space-3); font-family: var(--font-sans); font-size: var(--text-sm); color: var(--fg2); } +.plan-feat > svg { width: 15px; height: 15px; flex-shrink: 0; color: var(--success); } + +/* 「最受欢迎」浮标:白底胶囊悬于卡顶 */ +.plan-pop { + position: absolute; + top: calc(-1 * var(--space-3)); + left: 50%; + transform: translateX(-50%); + background: #fff; + color: var(--clay-700); + font-family: var(--font-sans); + font-size: var(--text-caption); + font-weight: 700; + padding: var(--space-1) var(--space-3); + border-radius: var(--radius-full); + box-shadow: var(--shadow-sm); + white-space: nowrap; +} + +/* Featured — clay 渐变高亮(年付/最优惠)。渐变内文字恒为白,靠 opacity 分级。 */ +.plan-card.is-featured { + background: linear-gradient(135deg, var(--clay-600), var(--clay-800)); + border-color: transparent; + box-shadow: var(--shadow-md); +} +.plan-card.is-featured .plan-name { color: #fff; opacity: 0.85; } +.plan-card.is-featured .plan-price { color: #fff; } +.plan-card.is-featured .plan-per { color: #fff; opacity: 0.85; } +.plan-card.is-featured .plan-feat { color: #fff; opacity: 0.92; } +.plan-card.is-featured .plan-feat > svg { color: #fff; } +/* Featured 卡内 CTA 反色:白底 clay 字 */ +.plan-card.is-featured .btn-primary { background: #fff; color: var(--clay-700); } +.plan-card.is-featured .btn-primary:hover { background: #fff; opacity: 0.92; } + + +/* ============================================================= + PAY METHOD · .pay-method (选择支付方式列表项) + 底部弹层里的一行:leading 图标 + 标题 + trailing chevron。 + 资金外流:点选后拉起外部渠道 / 展示地址,App 内无卡号录入表单(铁律 #10)。 + ============================================================= */ +.pay-method { + display: flex; + align-items: center; + gap: var(--space-4); + width: 100%; + padding: var(--space-4) var(--space-4); + border: none; + background: transparent; + border-radius: var(--radius-md); + cursor: pointer; + text-align: left; + transition: background-color var(--dur-fast) var(--ease-out); +} +.pay-method:hover { background: var(--bg-subtle); } +.pay-method > .pm-ic { width: 22px; height: 22px; flex-shrink: 0; color: var(--fg2); } +.pay-method > .pm-title { flex: 1; font-family: var(--font-sans); font-size: var(--text-body); color: var(--fg1); } +.pay-method > .pm-chev { width: 18px; height: 18px; flex-shrink: 0; color: var(--fg3); } + + +/* ============================================================= + KV ROW · .kv-row (可复制键值行 — 支付明细:网络/地址/金额) + label(次要)+ 等宽值 + 复制按钮。用于 crypto_address / qr 支付态。 + ============================================================= */ +.kv-row { display: flex; align-items: flex-start; gap: var(--space-3); padding: var(--space-2) 0; } +.kv-label { width: 76px; flex-shrink: 0; font-family: var(--font-sans); font-size: var(--text-sm); color: var(--fg3); padding-top: 2px; } +.kv-val { flex: 1; min-width: 0; font-family: var(--font-mono); font-size: var(--text-sm); color: var(--fg1); word-break: break-all; font-feature-settings: 'tnum' 1; } +.kv-copy { + flex-shrink: 0; + border: none; + background: transparent; + color: var(--fg3); + cursor: pointer; + padding: var(--space-1); + border-radius: var(--radius-sm); + transition: color var(--dur-fast) var(--ease-out), background-color var(--dur-fast) var(--ease-out); +} +.kv-copy:hover { color: var(--accent); background: var(--accent-subtle); } +.kv-copy > svg { width: 16px; height: 16px; display: block; } + + +/* ============================================================= + RESULT STATE · .result (支付成功 / 失败终态) + 居中大图标 + 标题 + 说明。成功用 --success,失败用 --danger。 + ============================================================= */ +.result { display: flex; flex-direction: column; align-items: center; text-align: center; padding: var(--space-8) var(--space-6); } +.result-icon { + width: 72px; height: 72px; + border-radius: var(--radius-full); + display: flex; align-items: center; justify-content: center; + background: var(--success-subtle); + color: var(--success); + margin-bottom: var(--space-4); +} +.result-icon > svg { width: 38px; height: 38px; } +.result.is-fail .result-icon { background: var(--danger-subtle); color: var(--danger); } +.result-title { font-family: var(--font-display); font-size: var(--text-h2); font-weight: 700; color: var(--fg1); letter-spacing: var(--tracking-snug); } +.result.is-fail .result-title { color: var(--danger); } +.result-sub { margin-top: var(--space-2); font-family: var(--font-sans); font-size: var(--text-sm); color: var(--fg2); } + + +/* ============================================================= + CONNECT BUTTON · .connect-btn (核心连接键 — 产品招牌组件) + 圆形一键连接键,严格三态:.is-off / .is-connecting / .is-on。 + 镜像 client/lib/widgets/connect_button.dart 的视觉配方(原型先行单源)。 + ⚠️ 铁律(design/CLAUDE.md §5):背景在渐变↔纯色间切换,过渡只走 + box-shadow / background-color,禁 transition:all(否则背景计算失效)。 + 注:圆内文字/图标/环在 clay·green 实底上恒为白(对齐 Flutter PangolinColors.white), + 故用白名单 #fff;轨道环半透明白靠 stroke-opacity 属性表达(非 rgba 硬编码色)。 + ============================================================= */ +.connect-btn { + position: relative; + width: 140px; + height: 140px; + border: none; + border-radius: var(--radius-full); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: var(--space-1); + cursor: pointer; + transition: background-color var(--dur-slow) var(--ease-out), + box-shadow var(--dur-slow) var(--ease-out); +} +.connect-btn:active { transform: scale(0.97); } +.connect-btn:focus-visible { outline: none; box-shadow: var(--shadow-focus); } + +/* 轨道环:铺满按钮的 svg,垫在圆内文字/图标之下 */ +.cb-ring { position: absolute; inset: 0; z-index: 0; } +.cb-ring circle { fill: none; } +.cb-icon, .cb-label, .cb-timer, .cb-sub { position: relative; z-index: 1; } +.cb-icon { width: 38px; height: 38px; color: currentColor; } +.cb-label { + font-family: var(--font-sans); + font-size: var(--text-caption); + font-weight: 700; + letter-spacing: var(--tracking-wide); +} +.cb-timer { + font-family: var(--font-mono); + font-size: var(--text-body-lg); + font-weight: 600; + line-height: 1; + font-feature-settings: 'tnum' 1; +} +.cb-sub { + font-family: var(--font-sans); + font-size: var(--text-caption); + font-weight: 700; + letter-spacing: var(--tracking-caps); + opacity: 0.85; +} +@keyframes cb-spin { to { transform: rotate(360deg); } } +.cb-spin { animation: cb-spin 1.1s linear infinite; transform-origin: 50% 50%; } + +/* OFF:暖灰底(微渐变)+ clay 前景 + 虚线轨道环 */ +.connect-btn.is-off { + background: radial-gradient(circle at 50% 38%, var(--sand-50), var(--sand-100)); + color: var(--accent); + box-shadow: inset 0 0 0 1px var(--sand-200), var(--shadow-md); +} +.connect-btn.is-off .cb-sub { color: var(--fg3); opacity: 1; } +.cb-track { stroke: var(--sand-200); stroke-width: 3; stroke-linecap: round; stroke-dasharray: 2 7; } + +/* CONNECTING:clay 实底 + accent 光晕 + 旋转进度弧 */ +.connect-btn.is-connecting { + background: var(--accent); + color: #fff; + box-shadow: 0 0 0 8px var(--accent-subtle), var(--shadow-lg); +} +.cb-arc-track { stroke: #fff; stroke-opacity: 0.3; stroke-width: 4; } +.cb-arc { stroke: #fff; stroke-width: 4; stroke-linecap: round; stroke-dasharray: 100 302; } + +/* ON:success 实底 + 绿光晕 + 满白环 + 圆内计时 */ +.connect-btn.is-on { + background: var(--success); + color: #fff; + box-shadow: 0 0 0 8px var(--success-subtle), var(--shadow-lg); +} +.cb-ring-full { stroke: #fff; stroke-opacity: 0.85; stroke-width: 4; stroke-linecap: round; } + +/* DISABLED(免费到点切断 / 额度耗尽)——对齐线上 connect_button.dart 第 4 态: + bg-subtle 底 + fg3 前景 + lock 图标 + not-allowed;点击走「看广告加时」。仅 off 场景触发。 */ +.connect-btn.is-disabled { + background: var(--bg-subtle); + color: var(--fg3); + box-shadow: var(--shadow-md); + cursor: not-allowed; +} +.connect-btn.is-disabled .cb-track { stroke: var(--border-strong); } + + +/* ============================================================= + SERVER ROW · .server-row (节点列表行) + 镜像 client/lib/widgets/server_tile.dart + design/preview/comp-server-row.html。 + 放进 .card.card-flush 列表容器;一行 = 国家码块 + 名称/子名 + 属性胶囊 + (.pill-outline) + 延迟(mono·按值变色) + 信号条 + 选中/推荐/不可用态。 + ============================================================= */ +.server-row { + display: flex; + align-items: center; + gap: var(--space-3); + padding: var(--space-3) var(--space-4); + border-bottom: 1px solid var(--border); + cursor: pointer; + transition: background-color var(--dur-fast) var(--ease-out); +} +.server-row:last-child { border-bottom: none; } +.server-row:hover { background: var(--bg-subtle); } + +/* 国家码块(2 字母码,绝不用 emoji 国旗 · 铁律 #8) */ +.sr-cc { + width: 34px; height: 34px; flex-shrink: 0; + display: flex; align-items: center; justify-content: center; + border-radius: var(--radius-md); + background: var(--sand-100); + font-family: var(--font-mono); font-size: var(--text-caption); font-weight: 600; + color: var(--fg2); +} +.sr-body { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; } +.sr-title { font-family: var(--font-sans); font-size: var(--text-sm); font-weight: 600; color: var(--fg1); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +.sr-sub { font-family: var(--font-sans); font-size: var(--text-caption); font-weight: 400; color: var(--fg3); } +.sr-tags { display: flex; flex-wrap: wrap; gap: var(--space-2); margin-top: 2px; } +.sr-tags .pill { padding: 3px var(--space-2); font-size: 11px; } + +/* 延迟(等宽 · tabular nums)——对齐线上 server_tile:**恒 fg2,不按阈值变色**; + 快慢改由信号条格数编码。旧 .is-good/.is-warn/.is-bad 一律回落 fg2(兼容不变色)。 */ +.sr-latency { flex-shrink: 0; font-family: var(--font-mono); font-size: var(--text-caption); color: var(--fg2); font-feature-settings: 'tnum' 1; } +.sr-latency.is-good, .sr-latency.is-warn, .sr-latency.is-bad { color: var(--fg2); } + +/* 信号条(纯 div + token 色)——默认暗格 border-strong,按等级点亮 success: + lv3=3 格(ping<40) / lv2=前 2 格(<90) / lv1=第 1 格(≥90)。对齐线上 SignalBars。 */ +.sr-signal { display: flex; align-items: flex-end; gap: 2px; height: 14px; flex-shrink: 0; } +.sr-signal i { width: 3px; border-radius: 1px; background: var(--border-strong); } +.sr-signal i:nth-child(1) { height: 5px; } +.sr-signal i:nth-child(2) { height: 9px; } +.sr-signal i:nth-child(3) { height: 14px; } +.sr-signal.lv3 i, +.sr-signal.lv2 i:nth-child(-n+2), +.sr-signal.lv1 i:nth-child(1) { background: var(--success); } +.sr-signal i.is-off { background: var(--border-strong); } + +.sr-check { flex-shrink: 0; color: var(--accent); display: inline-flex; } +.sr-check svg { width: 18px; height: 18px; } + +/* 选中态:accent-subtle 底 + 码块反色实心 clay */ +.server-row.is-selected { background: var(--accent-subtle); } +.server-row.is-selected:hover { background: var(--accent-subtle); } +.server-row.is-selected .sr-cc { background: var(--accent); color: var(--fg-on-accent); } + +/* 不可用态(agent 离线) */ +.server-row.is-down { opacity: 0.55; cursor: not-allowed; } +.server-row.is-down:hover { background: transparent; } +.sr-unavail { flex-shrink: 0; font-family: var(--font-sans); font-size: var(--text-caption); font-weight: 600; color: var(--danger); } + +/* 智能选择推荐行(节点页置顶 · design/CLAUDE.md §5) */ +.server-row.is-smart { background: var(--accent-subtle); } +.sr-smart-ic { + width: 34px; height: 34px; flex-shrink: 0; + display: flex; align-items: center; justify-content: center; + border-radius: var(--radius-md); + background: linear-gradient(135deg, var(--clay-500), var(--clay-700)); /* 对齐线上 smart_select_card */ + color: #fff; /* ds-allow: clay 渐变块上恒为白,对齐 .ds-brand .mk / plan-card featured */ +} +.sr-smart-ic svg { width: 18px; height: 18px; } + + +/* ============================================================= + 线上对齐补建原子(2026-07 回填:atoms 对齐 client/lib/widgets) + ============================================================= */ + +/* ICON BOX · accent-subtle 圆角图标框(设备/设置行前导) */ +.icon-box { width: 36px; height: 36px; flex-shrink: 0; display: flex; align-items: center; justify-content: center; border-radius: var(--radius-md); background: var(--accent-subtle); color: var(--accent); } +.icon-box > svg { width: 18px; height: 18px; } +.icon-box.is-danger { background: var(--danger-subtle); color: var(--danger); } + +/* STATUS DOT · 独立状态点 */ +.status-dot { width: 8px; height: 8px; border-radius: var(--radius-full); background: var(--fg3); flex-shrink: 0; display: inline-block; } +.status-dot.is-online { background: var(--success); } +.status-dot.is-offline { background: var(--fg3); } +.status-dot.is-busy { background: var(--warning); } + +/* TOGGLE · 开关(对齐线上 dtoggle) */ +.toggle { width: 42px; height: 24px; flex-shrink: 0; border-radius: var(--radius-full); background: var(--border-strong); border: none; cursor: pointer; position: relative; transition: background-color var(--dur-fast) var(--ease-out); } +.toggle > i { position: absolute; top: 3px; left: 3px; width: 18px; height: 18px; border-radius: var(--radius-full); background: #fff; box-shadow: var(--shadow-sm); transition: transform var(--dur-fast) var(--ease-out); } /* ds-allow: 开关滑块恒为白 */ +.toggle.is-on { background: var(--accent); } +.toggle.is-on > i { transform: translateX(18px); } + +/* SEGSWITCH · 二选一段控开关(支付渠道/币种等的横向 switch;iOS 式滑块高亮) */ +.segswitch { display: flex; background: var(--bg-subtle); border: 1px solid var(--border); border-radius: var(--radius-full); padding: var(--space-1); gap: var(--space-1); } +.segswitch > .segswitch-opt { flex: 1; border: none; background: transparent; cursor: pointer; font-family: var(--font-sans); font-size: var(--text-sm); font-weight: 600; color: var(--fg2); padding: var(--space-2) var(--space-4); border-radius: var(--radius-full); display: inline-flex; align-items: center; justify-content: center; gap: var(--space-2); transition: background-color var(--dur-fast) var(--ease-out), color var(--dur-fast) var(--ease-out); } +.segswitch > .segswitch-opt > svg { width: 16px; height: 16px; flex-shrink: 0; } +.segswitch > .segswitch-opt.is-active { background: var(--surface); color: var(--fg1); box-shadow: var(--shadow-sm); } + +/* FIELD BOX · 复合输入(前置图标 + 裸 input + 后置槽;对齐 pangolin_field.dart) */ +.field-box { display: flex; align-items: center; gap: var(--space-3); background: var(--surface); border: 1.5px solid var(--border-strong); border-radius: var(--radius-md); padding: 0 var(--space-4); transition: border-color var(--dur-fast) var(--ease-out), box-shadow var(--dur-fast) var(--ease-out); } +.field-box.is-focused { border-color: var(--accent); box-shadow: var(--shadow-focus); } +.field-box .fb-lead { width: 18px; height: 18px; flex-shrink: 0; color: var(--fg3); } +.field-box .fb-input { flex: 1; min-width: 0; border: none; background: transparent; font-family: var(--font-sans); font-size: var(--text-sm); color: var(--fg1); padding: var(--space-3) 0; } +.field-box .fb-input:focus { outline: none; } +.field-box .fb-input::placeholder { color: var(--fg3); } +.field-box .fb-trailing { flex-shrink: 0; display: inline-flex; color: var(--fg3); } +.field-box.is-plain { border-color: transparent; background: var(--bg-subtle); box-shadow: none; } + +/* TOAST · 轻提示(对齐 pangolin_toast / snackBarTheme) */ +.toast { display: inline-flex; align-items: center; justify-content: center; gap: var(--space-2); text-align: center; background: var(--surface); color: var(--fg1); border: 1px solid var(--border); border-radius: var(--radius-md); box-shadow: var(--shadow-lg); padding: var(--space-3) var(--space-4); font-family: var(--font-sans); font-size: var(--text-sm); font-weight: 600; } +.toast > svg { width: 14px; height: 14px; flex-shrink: 0; } + +/* QUOTA CARD · 免费版今日额度(对齐 quota_card.dart:倒计时收缩条 + 免费版标签 + 三档阈值;无绿色「已解锁」态) */ +.quota-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); box-shadow: var(--shadow-sm); padding: var(--space-3) var(--space-4); display: flex; flex-direction: column; gap: var(--space-2); } +.quota-head { display: flex; align-items: center; gap: var(--space-2); } +.quota-head > svg { width: 15px; height: 15px; color: var(--accent); flex-shrink: 0; } +.quota-card.is-exhausted .quota-head > svg { color: var(--danger); } +.quota-label { font-family: var(--font-sans); font-size: var(--text-caption); font-weight: 600; color: var(--fg2); } +.quota-val { font-family: var(--font-mono); font-size: 14px; font-weight: 600; color: var(--fg1); font-feature-settings: 'tnum' 1; } +.quota-tag { margin-left: auto; background: var(--bg-subtle); color: var(--fg3); font-family: var(--font-sans); font-size: 11px; font-weight: 600; padding: 3px var(--space-2); border-radius: var(--radius-full); } +.quota-bar { height: 6px; border-radius: var(--radius-full); background: var(--bg-subtle); overflow: hidden; } +.quota-bar > i { display: block; height: 100%; border-radius: var(--radius-full); background: var(--accent); transition: width var(--dur-base) var(--ease-out); } +.quota-bar.is-low > i { background: var(--warning); } +.quota-bar.is-exhausted > i { background: var(--danger); } + +/* METRIC · 统计指标瓦片 */ +.metric { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); box-shadow: var(--shadow-sm); padding: var(--space-3); } +.metric .m-ic { width: 26px; height: 26px; border-radius: var(--radius-sm); background: var(--accent-subtle); color: var(--accent); display: flex; align-items: center; justify-content: center; margin-bottom: var(--space-2); } +.metric .m-ic > svg { width: 15px; height: 15px; } +.metric .m-val { font-family: var(--font-mono); font-size: var(--text-body); font-weight: 600; color: var(--fg1); font-feature-settings: 'tnum' 1; } +.metric .m-label { font-family: var(--font-sans); font-size: 11px; color: var(--fg3); margin-top: 2px; } + +/* DEVICE ROW · 设备管理行(对齐 DevicesScreen:icon-box + 名 + 最后在线 + 本机/移除/⋯) */ +.device-row { display: flex; align-items: center; gap: var(--space-3); padding: var(--space-3) var(--space-4); border-bottom: 1px solid var(--border); } +.device-row:last-child { border-bottom: none; } +.device-row .dv-body { flex: 1; min-width: 0; } +.device-row .dv-name { font-family: var(--font-sans); font-size: var(--text-sm); font-weight: 600; color: var(--fg1); } +.device-row .dv-sub { font-family: var(--font-sans); font-size: var(--text-caption); color: var(--fg3); margin-top: 2px; display: flex; align-items: center; gap: var(--space-2); } +.device-row .dv-trail { flex-shrink: 0; display: inline-flex; align-items: center; gap: var(--space-2); } + +/* SETTING ROW · 设置/导航行(对齐 settings _Row/_NavRow:icon-box + 标题 + 副标 + trailing) */ +.setting-row { display: flex; align-items: center; gap: var(--space-3); padding: var(--space-3) var(--space-4); border-bottom: 1px solid var(--border); } +.setting-row:last-child { border-bottom: none; } +.setting-row.is-tap { cursor: pointer; transition: background-color var(--dur-fast) var(--ease-out); } +.setting-row.is-tap:hover { background: var(--bg-subtle); } +.setting-row .st-body { flex: 1; min-width: 0; } +.setting-row .st-title { font-family: var(--font-sans); font-size: var(--text-sm); font-weight: 600; color: var(--fg1); } +.setting-row .st-sub { font-family: var(--font-sans); font-size: var(--text-caption); color: var(--fg3); margin-top: 2px; } +.setting-row .st-trail { flex-shrink: 0; display: inline-flex; align-items: center; gap: var(--space-2); color: var(--fg3); } +.setting-row .st-trail > svg { width: 18px; height: 18px; } +.setting-row .st-val { font-family: var(--font-sans); font-size: var(--text-sm); color: var(--fg2); } + +/* PLAN BANNER · 账户页套餐横幅(免费=surface / .is-pro=clay 渐变;对齐 _PlanBanner) */ +.plan-banner { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); box-shadow: var(--shadow-sm); padding: var(--space-5); } +.plan-banner .pbn-row { display: flex; align-items: center; gap: var(--space-3); } +.plan-banner .pbn-info { flex: 1 1 auto; min-width: 0; } +.plan-banner .pbn-row > .btn { flex-shrink: 0; } +.plan-banner .pbn-badge { width: 44px; height: 44px; flex-shrink: 0; border-radius: var(--radius-lg); display: flex; align-items: center; justify-content: center; background: var(--accent-subtle); color: var(--accent); } +.plan-banner .pbn-badge > svg { width: 24px; height: 24px; } +.plan-banner .pbn-name { font-family: var(--font-display); font-size: var(--text-h3); font-weight: 700; color: var(--fg1); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +.plan-banner .pbn-meta { font-family: var(--font-sans); font-size: var(--text-caption); color: var(--fg3); margin-top: 2px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +.plan-banner.is-pro { background: linear-gradient(135deg, var(--clay-600), var(--clay-800)); border-color: transparent; box-shadow: var(--shadow-md); } +.plan-banner.is-pro .pbn-badge { background: transparent; border: 1.5px solid #fff; color: #fff; } +.plan-banner.is-pro .pbn-name { color: #fff; } +.plan-banner.is-pro .pbn-meta { color: #fff; opacity: 0.85; } +.plan-banner.is-pro .btn-primary { background: #fff; color: var(--clay-700); } + +/* PLAN BADGE · 桌面侧栏底部会员卡(对齐 plan_badge_card.dart) */ +.plan-badge { display: flex; align-items: center; gap: var(--space-2); background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-md); padding: var(--space-2) var(--space-3); cursor: pointer; transition: background-color var(--dur-fast) var(--ease-out), border-color var(--dur-fast) var(--ease-out); } +.plan-badge.is-active { background: var(--accent-subtle); border-color: var(--accent); } +.plan-badge .plb-crown { width: 30px; height: 30px; flex-shrink: 0; display: flex; align-items: center; justify-content: center; border-radius: var(--radius-full); background: linear-gradient(135deg, var(--clay-500), var(--clay-700)); color: #fff; } /* ds-allow: clay 渐变块上恒为白 */ +.plan-badge .plb-crown > svg { width: 15px; height: 15px; } +.plan-badge .plb-body { flex: 1; min-width: 0; display: flex; flex-direction: column; } +.plan-badge .plb-name { font-family: var(--font-sans); font-size: var(--text-caption); font-weight: 600; color: var(--fg1); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +.plan-badge .plb-sub { font-family: var(--font-sans); font-size: var(--text-caption); color: var(--fg3); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +.plan-badge .plb-chev { width: 14px; height: 14px; flex-shrink: 0; color: var(--fg3); } + +/* UPDATE BANNER · jiu 式更新顶部横条(对齐 UpdateBanner;非强制态) */ +.update-banner { display: flex; align-items: center; gap: var(--space-3); background: var(--accent-subtle); padding: var(--space-2) var(--space-3) var(--space-2) var(--space-4); } +.update-banner .ub-ic { width: 17px; height: 17px; flex-shrink: 0; color: var(--accent); } +.update-banner .ub-title { flex: 1; min-width: 0; font-family: var(--font-sans); font-size: var(--text-sm); font-weight: 700; color: var(--fg1); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +.update-banner .ub-cta { flex-shrink: 0; background: var(--accent); color: var(--fg-on-accent); border: none; border-radius: var(--radius-full); padding: var(--space-1) var(--space-4); font-family: var(--font-sans); font-size: var(--text-caption); font-weight: 700; cursor: pointer; } +.update-banner .ub-close { flex-shrink: 0; width: 30px; height: 30px; display: flex; align-items: center; justify-content: center; border: none; background: transparent; color: var(--fg3); cursor: pointer; border-radius: var(--radius-sm); } +.update-banner .ub-close > svg { width: 16px; height: 16px; } + +/* TAB BAR · 移动端底部 4-Tab(对齐 bottom_tab_bar.dart) */ +.tabbar { display: flex; height: 60px; border-top: 1px solid var(--border); background: var(--surface); } +.tab { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 3px; border: none; background: none; cursor: pointer; color: var(--fg3); font-family: var(--font-sans); font-size: 11px; font-weight: 600; } +.tab > svg { width: 21px; height: 21px; } +.tab.is-active { color: var(--accent); } + +/* NAV ITEM · 桌面/iPad 左侧栏导航项(对齐 nav_sidebar.dart) */ +.nav-item { display: flex; align-items: center; gap: var(--space-3); width: 100%; padding: var(--space-3); border: none; background: transparent; border-radius: var(--radius-md); cursor: pointer; text-align: left; color: var(--fg2); font-family: var(--font-sans); font-size: var(--text-sm); font-weight: 600; transition: background-color var(--dur-fast) var(--ease-out), color var(--dur-fast) var(--ease-out); } +.nav-item > svg { width: 20px; height: 20px; flex-shrink: 0; } +.nav-item:hover { background: var(--bg-subtle); color: var(--fg1); } +.nav-item.is-active { background: var(--accent-subtle); color: var(--accent); } diff --git a/design/prototype/icons.js b/design/prototype/icons.js index 470444a..b2faa60 100644 --- a/design/prototype/icons.js +++ b/design/prototype/icons.js @@ -56,7 +56,6 @@ 'arrow-right': '', 'compass': '', 'chart-no-axes-column': '', - 'chart': '', 'home': '', 'map-pin': '', 'laptop': '', diff --git a/design/prototype/index.html b/design/prototype/index.html index 777fbbb..bd9d202 100644 --- a/design/prototype/index.html +++ b/design/prototype/index.html @@ -37,10 +37,33 @@ /* 三栏文档布局 */ .doc{ display:flex; align-items:flex-start; max-width:1360px; margin:0 auto; } .doc-nav{ width:238px; flex:none; position:sticky; top:60px; height:calc(100vh - 60px); - overflow:auto; border-right:1px solid var(--border); padding:var(--space-5) var(--space-4); } - .dgroup{ font-size:var(--text-caption); font-weight:700; color:var(--fg3); letter-spacing:var(--tracking-caps); - text-transform:uppercase; padding:var(--space-4) var(--space-2) var(--space-2); } - .dnav{ display:block; padding:var(--space-2) var(--space-3); border-radius:var(--radius-md); + overflow-y:auto; overscroll-behavior:contain; border-right:1px solid var(--border); + padding:var(--space-5) var(--space-4) calc(var(--space-16) * 2); + transition:width var(--dur-base) var(--ease-out); } + /* 收起态(宽度由 JS 置 0,此处收内边距/边框) */ + .doc.nav-collapsed .doc-nav{ padding-left:0; padding-right:0; border-right:none; overflow:hidden; } + .doc.nav-collapsed .nav-resize{ display:none; } + /* 拖拽把手(改侧栏宽度) */ + .nav-resize{ flex:none; align-self:stretch; position:sticky; top:60px; height:calc(100vh - 60px); + width:9px; margin-left:-5px; cursor:col-resize; z-index:40; background:transparent; } + .nav-resize::after{ content:""; position:absolute; left:4px; top:0; bottom:0; width:2px; border-radius:var(--radius-full); + background:transparent; transition:background-color var(--dur-fast) var(--ease-out); } + .nav-resize:hover::after, .nav-resize.dragging::after{ background:var(--accent); } + body.nav-dragging{ cursor:col-resize; user-select:none; } + /* 顶栏收起按钮 */ + .nav-toggle{ display:inline-flex; align-items:center; justify-content:center; width:34px; height:34px; flex:none; + border:1px solid var(--border); border-radius:var(--radius-md); background:var(--bg-subtle); + color:var(--fg2); cursor:pointer; transition:color var(--dur-fast) var(--ease-out), border-color var(--dur-fast) var(--ease-out); } + .nav-toggle:hover{ color:var(--fg1); border-color:var(--border-strong); } + .nav-toggle svg{ width:18px; height:18px; } + + /* L1 分组头 —— 分隔线 + 提亮加粗,与二级明显区分 */ + .dgroup{ font-size:var(--text-caption); font-weight:700; color:var(--fg2); letter-spacing:var(--tracking-caps); + text-transform:uppercase; padding:var(--space-2) var(--space-2); + margin-top:var(--space-5); border-top:1px solid var(--border); } + .doc-nav .dgroup:first-child{ margin-top:var(--space-1); border-top:none; } + /* L2 条目 —— 缩进内嵌于分组下 */ + .dnav{ display:block; padding:var(--space-2) var(--space-3) var(--space-2) var(--space-5); border-radius:var(--radius-md); font-size:var(--text-sm); color:var(--fg2); text-decoration:none; cursor:pointer; transition:background-color var(--dur-fast) var(--ease-out), color var(--dur-fast) var(--ease-out); } .dnav:hover{ background:var(--bg-subtle); color:var(--fg1); } @@ -102,13 +125,36 @@ @media (max-width:840px){ .doc-nav{ display:none; } + .nav-resize{ display:none; } + .nav-toggle{ display:none; } .doc-main{ padding:var(--space-6) var(--space-5) var(--space-12); } } + + /* ── 品牌 / 字体 specimen 展示用(登记页 chrome,非 atoms;全走 token) ── */ + .brand-appicon{ display:flex; align-items:flex-end; gap:var(--space-8); flex-wrap:wrap; } + .brand-appicon .unit{ display:flex; flex-direction:column; align-items:center; gap:var(--space-2); } + .brand-appicon .unit .n{ font-family:var(--font-mono); font-size:11px; color:var(--fg3); } + .brand-appicon img{ display:block; box-shadow:var(--shadow-md); border-radius:22%; } + .lockup{ display:flex; align-items:center; gap:var(--space-3); } + .lockup img{ display:block; } + .lockup .tw{ display:flex; flex-direction:column; line-height:1; } + .lockup .cn{ font-family:var(--font-display); font-weight:700; font-size:22px; letter-spacing:var(--tracking-snug); color:var(--fg1); } + .lockup .en{ font-family:var(--font-display); font-weight:600; font-size:10px; letter-spacing:0.22em; color:var(--accent); margin-top:4px; } + .lockup-panel{ flex:1; min-width:220px; display:flex; flex-direction:column; align-items:center; gap:var(--space-3); + padding:var(--space-8); border-radius:var(--radius-lg); border:1px solid var(--border); } + .lockup-panel .plab{ font-family:var(--font-mono); font-size:10px; color:var(--fg3); } + .spec-mono-grid{ display:grid; grid-template-columns:repeat(3,1fr); gap:var(--space-5); width:100%; } + .spec-mono-grid .k{ font-family:var(--font-mono); font-size:11px; color:var(--fg3); margin-bottom:4px; } + .spec-mono-grid .v{ font-family:var(--font-mono); font-size:22px; font-weight:500; letter-spacing:var(--tracking-snug); + color:var(--fg1); font-feature-settings:'tnum' 1; } + .spec-mono-grid .v .u{ font-size:12px; color:var(--accent); font-weight:400; } + @media (max-width:560px){ .spec-mono-grid{ grid-template-columns:repeat(2,1fr); } }
-
穿山甲 Pangolin
+ +
穿山甲穿山甲 Pangolin
设计系统登记簿 · Prototype - ↖ 默认展开演示,可点击触发按钮收起 / 展开
+ +
+

核心连接按钮 · .connect-btn

+

产品招牌组件:圆形一键连接键,三态 .is-off / .is-connecting / .is-on。背景在渐变↔纯色间切换仅过渡 box-shadow/background-color transition:all,否则背景计算失效)。轨道环用内联 <svg>,圆内为图标 + 标签 / 计时。镜像 client/lib/widgets/connect_button.dart

+
+ +
+ + 未连接 Off +
+ +
+ + 建立隧道 Connecting +
+ +
+ + 已连接 Connected +
+ +
+
+ + +
+

服务器列表行 · .server-row

+

节点列表一行:国家码块(2 字母,绝不用 emoji 国旗)+ 名称/子名 + 属性胶囊(.pill-outline)+ 延迟(--font-mono·按值变色)+ 信号条 + 选中/推荐/不可用态。镜像 client/lib/widgets/server_tile.dart。放进 .card.card-flush 列表容器。

+ +

列表行

.server-row · .is-selected · .is-down
+
+
+
HK
+
+
香港 · 加速线路
+
流媒体
+
+ 18ms + +
+
+
JP
+
+
日本 东京
+
Tokyo
+
+ 32ms + +
+
+
SG
+
+
新加坡
+
P2P
+
+ 54ms + +
+
+
US
+
+
美国 洛杉矶
+
Los Angeles
+
+ 不可用 +
+
+
+ +

智能选择推荐行

.server-row.is-smart · zap + 推荐胶囊
+
+
+
+
+
智能选择
+
根据当前网络环境,自动选择最优节点
+
+ 推荐 + +
+
+
+
+ + +
+

状态徽章 · .pill 场景

+

徽章复用胶囊原子 .pill(见「胶囊 .pill / .badge」),此处按业务场景分组:连接态(色点 + 文字,非 emoji · 铁律 #7)· 套餐 · 节点属性。镜像 client/lib/widgets/status_pill.dart

+ +

连接态

.pill.is-success/-warning/-danger/-neutral
+ 已连接 + 连接中 + 已断开 + 未登录 +
+ +

套餐

.pill-accent · .pill.is-neutral
+ PRO 会员 + 免费版 +
+ +

节点属性

.pill-outline · .pill.is-neutral(数据)
+ 流媒体优化 + P2P + 延迟 24ms +
+
+ + +
+

复合输入 · 开关 · 图标框 · Toast

+

对齐线上 pangolin_field.dart / dtoggle / pangolin_toast.dart.field-box 复合输入(前置图标 + 裸 input + 后置槽),.toggle 开关,.segswitch 二选一段控开关(支付渠道/币种),.icon-box 图标框,.status-dot 状态点,.toast 轻提示。

+
+ + + +
+
+ + + + + 在线 + 离线 + 已复制 +
+
+
+ + +
+
+ + +
+
+
+ +
+

额度卡 · 指标卡

+

对齐线上 quota_card.dart(免费版今日额度:倒计时收缩条 + 「免费版」短标签 + 三档阈值;无绿色「已解锁」态,线上以倒计时收缩条表达)与统计 .metric 指标瓦片。

+
+
+
今日剩余10 分钟免费版
+
+
+
+
剩余02:18免费版
+
+
+
+
今日已用完免费版
+
+
+
+
+
12.4G
本周流量
+
6.2h
在线时长
+
+
+ +
+

设备行 · 设置行

+

对齐线上 DevicesScreen / settings_page.dart.device-row(图标框 + 名 + 最后在线 + 本机/移除/⋯)与 .setting-row(图标框 + 标题 + 副标 + trailing:chevron / 开关 / 值)。

+

设备管理

.device-row
+
+
iPhone 15
当前设备 · 刚刚在线
本机
+
MacBook Pro
最后在线 2 小时前
+
+
+

设置

.setting-row
+
+
智能分流
海外应用走加速,国内直连
+
语言
简体中文
+
协议
REALITY
+
+
+
+ +
+

套餐横幅 · 侧栏会员卡

+

对齐线上 _PlanBanner(账户页)与 plan_badge_card.dart(桌面侧栏底部)。免费=surface 卡;.is-pro=clay 渐变。

+
+
+
免费版
注册享 7 天免费试用
+
+
+
PRO 会员
有效期至 2027-07-11
+
+
+

侧栏会员卡

.plan-badge
+
PRO 会员至 2027-07-11
+
+
+ +
+

更新条 · 底部 Tab · 侧栏项

+

app-chrome 组件。.update-banner(jiu 式更新顶部横条,对齐 UpdateBanner)、.tabbar/.tab(移动底部 4-Tab,对齐 bottom_tab_bar.dart)、.nav-item(桌面/iPad 侧栏项,对齐 nav_sidebar.dart)。

+
+
发现新版本 v1.1.0,建议更新
+
+
+
+ + + + +
+
+
+
+ + + + + +
+
+
+ + +
+

套餐卡 · .plan-card

+

购买套餐档位卡。常规档 surface 底;.is-featured 走 clay 渐变高亮(年付/最优惠),内含反色 CTA,顶部可挂 .plan-pop「最受欢迎」浮标。镜像 client/lib/widgets/plan_card.dart。金额仅展示,扣款以 pay 服务为准。

+
+
+
按月
+
¥29.99/ 月
+
+
不限时长 · 不限流量
+
80+ 全球加速线路
+
5 台设备同时在线
+
+ +
+ +
+
+ +
+

支付方式 · .pay-method

+

底部弹层选择支付渠道,一行 = leading 图标 + 标题 + trailing chevron。资金外流(拉起支付宝 / 展示加密地址),App 内无卡号录入表单(设计铁律 #10)。生产渠道:哪吒聚合(支付宝)+ 加密货币 USDT。

+
+
+ + +
+
+
+ +
+

明细行 · .kv-row

+

可复制键值行,用于加密支付态展示「网络 / 收款地址 / 精确金额」。值用等宽字(--font-mono + tabular nums),右侧复制按钮。

+
+
+
网络USDT · TRC20
+
收款地址TB1e7q1333iuLMLc2iQaoaqeuDF2U7cxan
+
精确金额4.990001
+
+
+
+ +
+

结果态 · .result

+

支付终态:居中大图标 + 标题 + 说明。成功用 --success,失败加 .is-fail--danger

+
+
+
+
支付成功
+
有效期至 2027-07-11
+
+
+
+
支付失败
+
订单已超时,请重试
+
+
+
+

Lucide 图标库

@@ -390,10 +860,52 @@ function renderIcons(){ /* ── 导航 scrollspy ── */ const navLinks=[...document.querySelectorAll('.dnav')]; -navLinks.forEach(a=>a.onclick=(e)=>{ e.preventDefault(); const el=document.getElementById(a.dataset.s); if(el) el.scrollIntoView({behavior:'smooth',block:'start'}); }); +navLinks.forEach(a=>a.onclick=(e)=>{ if(!a.dataset.s) return; /* 外链(如屏预览)正常跳转 */ e.preventDefault(); const el=document.getElementById(a.dataset.s); if(el) el.scrollIntoView({behavior:'smooth',block:'start'}); }); const obs=new IntersectionObserver(es=>{ es.forEach(en=>{ if(en.isIntersecting){ const id=en.target.id; navLinks.forEach(a=>a.classList.toggle('on',a.dataset.s===id)); } }); },{rootMargin:'-70px 0px -75% 0px'}); document.querySelectorAll('.section').forEach(s=>obs.observe(s)); +/* ── 侧栏:收起 + 拖拽改宽(持久化 localStorage) ── */ +(function(){ + const doc = document.querySelector('.doc'); + const nav = document.querySelector('.doc-nav'); + const handle = document.getElementById('navResize'); + const toggle = document.getElementById('navToggle'); + if(!doc || !nav || !handle || !toggle) return; + const MIN = 180, MAX = 460, DEF = 238; + let curW = parseInt(localStorage.getItem('pangolin-ds-navw') || '', 10); + if (!(curW >= MIN && curW <= MAX)) curW = DEF; + let collapsed = localStorage.getItem('pangolin-ds-navcollapsed') === '1'; + function apply(){ + if (collapsed) { doc.classList.add('nav-collapsed'); nav.style.width = '0px'; } + else { doc.classList.remove('nav-collapsed'); nav.style.width = curW + 'px'; } + } + apply(); + // 收起 / 展开 + toggle.onclick = () => { + collapsed = !collapsed; + localStorage.setItem('pangolin-ds-navcollapsed', collapsed ? '1' : '0'); + apply(); + }; + // 拖拽改宽(纯 JS 内联宽度,避免自定义 CSS 变量触发闸误报) + let dragging = false; + handle.addEventListener('mousedown', (e) => { + if (collapsed) return; + dragging = true; handle.classList.add('dragging'); document.body.classList.add('nav-dragging'); + e.preventDefault(); + }); + window.addEventListener('mousemove', (e) => { + if (!dragging) return; + const rect = nav.getBoundingClientRect(); + curW = Math.max(MIN, Math.min(MAX, e.clientX - rect.left)); + nav.style.width = curW + 'px'; + }); + window.addEventListener('mouseup', () => { + if (!dragging) return; + dragging = false; handle.classList.remove('dragging'); document.body.classList.remove('nav-dragging'); + localStorage.setItem('pangolin-ds-navw', curW); + }); +})(); + /* ── init(sprite 注入可能在 DOMContentLoaded,稍等确保 PANGOLIN_ICONS 就绪) ── */ function boot(){ applyTheme(localStorage.getItem('pangolin-ds-theme') || 'light'); diff --git a/design/prototype/screens/orders.html b/design/prototype/screens/orders.html new file mode 100644 index 0000000..5293bac --- /dev/null +++ b/design/prototype/screens/orders.html @@ -0,0 +1,342 @@ + + + + + +Pangolin · Orders (Interactive Prototype) + + + + + + +
+
PangolinPangolin 穿山甲
+ Orders · Design preview +
+ ← Registry + + +
+
+ +
+

+ Single phone frame walking the order flow: Orders list → tap a row → Order detail → back. Four lifecycle states (awaiting payment / paid · activating / completed / expired). Copyable order & trade numbers. Toggle language (EN / 中) and theme top-right. +

+ +
+
+ + Orders +
+ +
+ + +
+
+
+
+
+ + +
+
+
+
+
+
+
+
+
+ +
+
Details
+
+
+ +
+
+
+ + +
+ +
+
+
+ + + + diff --git a/design/prototype/screens/purchase.html b/design/prototype/screens/purchase.html new file mode 100644 index 0000000..15ac9de --- /dev/null +++ b/design/prototype/screens/purchase.html @@ -0,0 +1,403 @@ + + + + + +穿山甲 · 购买流程(交互原型) + + + + + + +
+
穿山甲穿山甲 Pangolin
+ 购买流程 · 交互原型 + ← 返回登记簿 + +
+ +
+

+ 单手机框走完整购买流程:购买(特性统一置顶 + 支付渠道 switch + 精简套餐卡)→ 创建订单(顶部订单信息按所选渠道币种显价)→ 成功 / 失败。 + 支付渠道即币种即方式:USDT → 加密货币(默认)/ 人民币 → 支付宝;套餐价随渠道切换(各币种一口价,不做汇率换算)。结算币种一经下单锁定,跨币种需重新下单(pay 侧 currency_mismatch)。资金外流全在 App 外,App 内无卡号录入(铁律 #10)。「▸ 模拟到账 / 失败」为评审用。切右上角看深色。 +

+ +
+
+ + 升级 PRO +
+ +
+ + +
+
+ + +
+

所有套餐均包含

+
+
+
不限时长 · 不限流量
+
80+ 全球加速线路
+
5 台设备同时在线
+
优先客服
+
+
+
+ + +
+

支付渠道

+
+ + +
+
+ + +
+
+
+
按月
+
+
+
+
+ +
+ +
+
+
+
按季
+
+
+
+
+ +
+ + + +
+
+ + +
+
+ + +
+
订单信息
+
套餐按年 · PRO
+
支付渠道加密货币 · USDT
+
应付金额$34.99
+
到期日2027-07-11
+
+ + +
+
网络USDT · TRC20
+
收款地址TB1e7q1333iuLMLc2iQaoaqeuDF2U7cxan
+
精确金额34.990001
+
请转账 34.990001 USDT(含小数末位),金额用于自动对账识别,多退少不到账。
+
+ +
+ +
将跳转至支付宝完成付款,付款后自动返回本页并开通。
+
+ +
等待到账确认…
+ + + + +
+
评审演示
+ + +
+
+
+ + +
+
+
+
+
支付成功
+
PRO 有效期至 2027-07-11
+
+ +
+
+ + +
+
+
+
+
支付失败
+
订单已超时,请重试
+
+ +
+
+ + +
+ +
+
+
+ + + + diff --git a/design/prototype/screens/site.html b/design/prototype/screens/site.html new file mode 100644 index 0000000..a17fe24 --- /dev/null +++ b/design/prototype/screens/site.html @@ -0,0 +1,802 @@ + + + + + +穿山甲 · 官网 Site 完整原型(可交互) + + + + + + + +
+
穿山甲穿山甲 Pangolin
+ 官网 Site · 可交互原型 + ← 返回登记簿 + +
+ +
+

+ 官网宣传页完整版式(顶栏 / Hero / 特性 / 定价 / 下载 / 页脚),装进 mock 浏览器窗口,已加交互: + 导航平滑滚动 + scrollspy 高亮、语言下拉(六语单显切换:简体中文 / EN / 日 / 韩 / 俄 / 西)、明暗主题、定价「月付 / 年付」段控切价。 + 仅引 ../tokens.css / ../atoms.css / ../icons.js,业务色全走 var(--token); + 套餐数字对齐 §7;文案脱敏(铁律 #13)——只讲「网络加速 / 极速畅连 / 隐私保护 / 无日志」。切右上角看深色。 +

+
此页自适应:窗口 < 720px 时导航折叠、各区块单列堆叠
+ +
+
+
+ pangolin.example +
+ +
+ +
+ + +
+ +
+ + +
+ 立即下载 +
+
+ + +
+
+
+
极速 · 稳定 · 省心
+

极速畅连, +网络如丝顺滑

+

轻盈、亲和、即开即用的跨平台网络加速应用。一键连接,智能选线,稳定不掉线。

+ +
+ 全球加速线路 + 严格无日志 + 端到端加密 +
+
+ +
+ +
+
下载
86.4 Mb/s
+
上传
12.1 Mb/s
+
+
+ HK +
香港 · 流媒体
延迟 18ms · 已连接
+ +
+
+
+ +
+ 已支持 + iOS + Android + macOS + Windows + Linux +
+
+ + +
+
+
+
产品
+

为日常而生的连接体验

+

没有复杂配置,没有技术门槛。打开就用,把稳定与隐私留给我们。

+
+
+
+
+

一键连接

+

一颗按钮,智能挑选最快线路。无需手动配置,也能秒连。

+
+
+
+

智能分流

+

按规则智能分流,该加速的加速,本地服务直连,互不打扰。

+
+
+
+

全球加速线路

+

覆盖多地优质线路,视频与游戏优化随心选。

+
+
+
+

严格无日志

+

我们不记录你的浏览数据。隐私是底线,不是卖点。

+
+
+
+

Kill Switch

+

一旦连接中断,立即阻断网络,杜绝真实 IP 泄露。

+
+
+
+

多端同步

+

一个账户,手机、平板、电脑同时在线,最多 5 台设备。

+
+
+
+
+ + +
+
+
+
定价
+

简单透明的价格

+

随时升级或取消。所有套餐均含核心加密与无日志承诺。

+
+
+ + +
+ +
+
+
+ +
+
免费版
+
适合轻度使用与尝鲜
+
¥0
+
+
每日 10 分钟时长
+
仅 1 个基础节点
+
使用前观看广告
+
核心加密 · 无日志
+
+ +
+ + + + + +
+
团队版
+
小团队集中管理
+
¥99/月 · 10 席位
+
+
专业版全部功能
+
10 个成员席位
+
集中计费与管理
+
优先客服
+
+ +
+
+ + +
+ +
+ 为什么网页上没有支付按钮? +

出于风控与隐私考虑,穿山甲不在网页或 App 内直接收款。你可以通过以下任一渠道获取激活码,在客户端内兑换即可开通。

+
+ 自助发卡商店 + USDT (TRC20) · 最隐私 + Telegram + LINE + 邮箱 +
+
+
+
+
+ + +
+
+
+
下载
+

全平台,随处可用

+

一个账户,所有设备同步。下载即用,无需配置。

+
+
+
+
+
iOS
iOS 16+
+ 敬请期待 +
+
+
+
Android
Android 9+
+ +
+
+
+
macOS
macOS 12+
+ +
+
+
+
Windows
Win 10/11
+ +
+
+
+
Linux
deb / rpm
+ 敬请期待 +
+
+
+
+ + +
+
+
+
+ +

极简、轻量、亲和的跨平台网络加速应用。守护你的每一次连接。

+
+
+

产品

+ 功能 + 定价 + 下载 +
+ +
+

联系与渠道

+ Telegram + LINE + 邮箱 + 服务时间 每日 9:00–24:00 (GMT+8) +
+
+
+ © 2026 穿山甲 · Pangolin + 京ICP备 0000000 号-0 + App 内不支持直接支付 · 资金流全走外部渠道 + + + + + + +
+
+
+
+
+
+ + + + diff --git a/design/prototype/screens/ui-desktop.html b/design/prototype/screens/ui-desktop.html new file mode 100644 index 0000000..5fe1340 --- /dev/null +++ b/design/prototype/screens/ui-desktop.html @@ -0,0 +1,1488 @@ + + + + + +穿山甲 · 桌面客户端交互原型 + + + + + + +
+
穿山甲穿山甲 Pangolin
+ 桌面客户端 · 交互原型 + ← 返回登记簿 +
+
主题 +
+
语言 +
+
视角 +
+
更新条 +
+
+
+ +
+

+ 单窗口可交互桌面客户端(980×640)。左侧栏切视图、连接键状态机、设置开关真能开关、语言中/EN 单显切换、视角 免费版/PRO 切换。品牌区与底部会员卡均可进账户。顶栏演示控制段与设置页开关双向联动。登录与设备超限另附独立静态帧。 +

+ +
+
+ + + + +
+
连接
○ 未连接
+
+ + +
+ + 发现新版本 v2.5.0,建议更新 + + +
+ + +
+
+ +
+
未连接
+
点击圆钮开始极速畅连
+
智能选择 · 香港 · 28ms
+
+ +
+
+ + +
+
+ +
+
+ +
+ 智能选择 + 根据当前网络环境,自动选择最优节点 +
+ 推荐 + +
+
+ HK +
+ 香港 · 优选 +
流媒体
+
+ 28 ms + +
+
+ JP +
+ 日本 · 东京 +
P2P
+
+ 42 ms + +
+
+ SG +
新加坡
+ 96 ms + +
+
+ US +
美国 · 洛杉矶
+ 168 ms + +
+
+ DE +
德国 · 法兰克福
+ 暂不可用 +
+
+
+
+ + +
+
+
+ 统计 + + + + +
+
+
+
本月
+
+
下行
38.2 GB
+
上行
4.4 GB
+
时长
86.4 h
+
+
+
+
本周
+
+
下行
9.1 GB
+
上行
1.2 GB
+
时长
21.5 h
+
+
+
+
今日
+
+
下行
1.4 GB
+
上行
0.2 GB
+
时长
2.1 h
+
+
+
+
+
最近两周流量GB
+
+
+
+
+ + +
+ +
+ + +
+
+
+
+
兑换 & 购买
PRO 会员
+ +
+
+ +
我的订单
查看订单记录
+ +
+
+ +
+
+
开机自启
系统启动时自动运行
+ +
+
+
自动连接
启动时自动连接节点
+ +
+
+
智能分流
海外应用走加速,国内直连
+ +
+
+
断连保护 Kill Switch
断线时阻断网络,防止数据泄露
+ +
+
+ +
+
+
语言
+ + + + + + +
+
+
深色外观
跟随浅色
+ +
+
+
协议
+ REALITY / Hysteria2 +
+
+
用户中心(网页)
+ +
+
+
检查更新
+ +
+
+
Version
+ v2.4.0 (macOS) +
+
+
+
+ + +
+
+

遇到问题?通过以下任一渠道联系我们,通常数分钟内回复。

+
+
+ +
Telegram
@pangolin_app
+ +
+
+ +
邮箱客服
support@yanmeiai.com
+ +
+
+ +
官网
pangolin.yanmeiai.com
+ +
+
+
+ +
+
服务时间
+
每日 9:00 – 24:00 (GMT+8)
+
+
+
+
+ + +
+
+ + + +
+
+
PRO 全部特性
+
+
不限时长 · 不限流量
+
80+ 全球加速线路
+
5 台设备同时在线
+
优先客服支持
+
+
+ + +
+
支付渠道
+
+ + +
+
+ +
+
+
按月
+
+
+ +
+
+
按季
+
+
+ +
+ +
+
+ + +
+
+
+
按年 · PRO¥199.99
+
加密货币 · USDT
+
有效期至 2027-07-11
+
+
+
网络USDT · TRC20
+
收款地址TB1e7q1333iuLMLc2iQaoaqeuDF2U7cxan
+
精确金额34.990001
+
请按精确金额(含小数末位)转账,金额用于自动对账识别,多退少不到账。
+
+
+
+
¥199.99
+
用支付宝扫码支付
+
付款后自动开通,无需手动确认。
+
+
等待到账确认…
+ + +
+
评审演示
+ + +
+
+
+ + +
+
+
+
+
支付成功
+
PRO 有效期至 2027-07-11
+
+ +
+
+ + +
+
+
+
+
支付失败
+
订单已超时,请重试
+
+ +
+
+
+
+ + +
+
+ + + +
+
+
+ + +
+
+
+
+
+
+
+
+
+
明细
+
+
+
+
+
+
+ + +
+ +
+
+
+
+ + +
+
登录分屏卡 · 品牌左 / 表单右(独立静态屏,不并入交互窗口)
+ +
+ + +
+
设备超限登录后设备数超上限挡板(独立静态屏,不并入交互窗口)
+
+ +
设备数已达上限
+
你的套餐最多 5 台设备,请移除一台以继续。
+ +
+
+ +
iPhone 15
最后在线 2 小时前
+
+
+
+ +
MacBook Pro
本机
+
+
+
+ +
iPad Air
最后在线 昨天
+
+
+
+ +
+
+ +
+ + + + diff --git a/design/prototype/screens/ui-mobile.html b/design/prototype/screens/ui-mobile.html new file mode 100644 index 0000000..f35584f --- /dev/null +++ b/design/prototype/screens/ui-mobile.html @@ -0,0 +1,1607 @@ + + + + + +穿山甲 · 移动 App 交互原型 + + + + + + +
+
穿山甲穿山甲 Pangolin
+ 移动 App · 交互原型 + ← 返回登记簿 +
+
+ 主题 +
+ + +
+
+
+ 语言 +
+ + +
+
+
+ 视角 +
+ + +
+
+
+ 更新 +
+ + +
+
+
+
+ +
+

+ 单机可交互原型:底部四 Tab、连接键三态状态机(未连接 → 连接中 → 已连接)、 + 节点选择联动、套餐视角切换。全部复用 ../atoms.css 原子,vanilla JS 无依赖。用顶栏右侧演示控制段切主题 / 语言 / 视角 / 更新提示。 +

+ +
+ +
+
交互主框架 · 四 Tab 状态机
+
+ +
+ 9:41 +
+ + + +
+
+ + +
+ 连接 +
+
+ + +
+ + 发现新版本 v1.1.0 + + +
+ +
+ +
+
+ +
未连接
+
+ +
+
+ + +
+
+
+
+ +
+
+
+ +
+
智能选择
+
根据当前网络环境,自动选择最优节点
+
+ 推荐 + +
+
+
+
+ HK +
香港 01
+
流媒体
+ 28 ms + + +
+
+ JP +
日本 · 东京 02
低延迟
+ 42 ms + + +
+
+ SG +
新加坡 01
高速稳定
+ 96 ms + + +
+
+ US +
美国 · 洛杉矶 03
远距离
+ 168 ms + + +
+
+ TW +
台湾 01
维护中
+ 不可用 +
+
+
+ + +
+
+ 设备 +
+ + +
+
+
+
+
近两周流量GB
+
+
+
+ + +
+ +
+
+ +
+
me@pangolin.app
+
免费版
+
+ +
+
+ + + +
账户信息
+
+
+ +
邮箱
me@pangolin.app
+
修改
+
+
+ +
密码
••••••••••
+
修改
+
+
+ + +
+
我的设备 · 1 / 1 台
+
+
+ +
+
iPhone 15本机
+
当前在线
+
+
+
+
+
+ + + +
偏好
+
+
+ +
语言
+
+
+ + +
+
+
+
+ +
深色外观
跟随浅色
+
+
+
+ +
协议
+
REALITY / Hysteria2
+
+
+ +
更多
+
+
+ +
设备管理
+
+
+
+ +
设置
+
+
+
+ +
兑换 & 购买
+
+
+
+ +
我的订单
+
+
+
+ +
联系我们
+
+
+
+ +
网页用户中心
+
+
+
+ +
+
+ +
退出登录
+
+
+
+
+ + + + + +
+ + + + +
+ + +
+
+ + Upgrade to PRO +
+
+ + +
+
+
+
All plans include
+
+
Unlimited time and data
+
80+ global nodes
+
5 devices at once
+
Priority support
+
+
+
+
Payment channel
+
+ + +
+
+
+
Monthly
+
+ +
+
+
Quarterly
+
+ +
+ +
+
+ + +
+
+
+
Plan
+
Payment channel
+
Amount
+
Valid until
+
+
+
NetworkUSDT · TRC20
+
AddressTB1e7q1333iuLMLc2iQaoaqeuDF2U7cxan
+
Exact amount4.990001
+
+
+
+ +
You will be redirected to Alipay.
+
+ +
Waiting for confirmation...
+ + + + +
+
Demo
+ + +
+
+
+ + +
+
+
+
+
Payment successful
+
+
+ +
+
+ + +
+
+
+
+
Payment failed
+
Order timed out, please retry
+
+ +
+
+ + + +
+ +
+
+ + +
+
+ + Orders +
+
+ + +
+
+
+
+
+ + +
+
+
+
+
+
+
+
+
+
+
Details
+
+
+
+
+
+ + +
+ +
+
+
+
+ + +
+
静态登录 · 注册
+
+
+ 9:41 +
+ + + +
+
+
登录
+
+
+ 穿山甲 +
穿山甲
+
Pangolin
+
极速 · 稳定 · 省心
+
+
+
+ + +
+ +
+
+
+ +
+
+
+
+ +
+
+
+
忘记密码?
+ +
+
+ + +
继续即代表同意《服务条款》与《隐私政策》
+
+
+
+
+ + +
+
静态设备超限挡板 · #16
+
+
+ 9:41 +
+ + + +
+
+
+
+ +
设备数已达上限
+
你的账户最多可同时登录 5 台设备,请移除一台后继续。
+
+ +
+
+ +
+
iPhone 15本机
+
在线
+
+
+
+
+ +
+
MacBook Pro
+
最后在线 2 小时前
+
+
+
+
+ +
+
iPad Air
+
最后在线 昨天
+
+
+
+
+ +
+
Windows PC
+
最后在线 3 天前
+
+
+
+
+ +
+
+
+
+
+ + + + diff --git a/design/prototype/tools/check-l1-sync.mjs b/design/prototype/tools/check-l1-sync.mjs new file mode 100644 index 0000000..086a85d --- /dev/null +++ b/design/prototype/tools/check-l1-sync.mjs @@ -0,0 +1,62 @@ +#!/usr/bin/env node +// check-l1-sync.mjs —— L1 跨端同源闸(ds-flow)。零依赖。 +// ① 图标集合:Flutter 图标集 == 原型 sprite 图标集(camel 化后逐名相等)。 +// 真源 design/prototype/icons.js;镜像 client/lib/widgets/pangolin_icons.dart。 +// ② l10n 完整性:app_text.dart 声明的每个 getter,6 个语言文件必须全部实现 +// (谁少 key 就红 —— 防「某语言漏字串」漂移,如支付字串只加 zh/en)。 +// 任一不通过 → 列出并 exit 1。 +import { readFileSync, existsSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { dirname, resolve } from 'node:path'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const root = resolve(__dirname, '..', '..', '..'); +let failed = 0; + +/* ① 图标集合 */ +const spriteKeys = new Set( + [...readFileSync(resolve(root, 'design/prototype/icons.js'), 'utf8') + .matchAll(/'([a-z0-9-]+)'\s*:\s*' m[1].replace(/-([a-z0-9])/g, (_, c) => c.toUpperCase())) +); +const dartKeys = new Set( + [...readFileSync(resolve(root, 'client/lib/widgets/pangolin_icons.dart'), 'utf8') + .matchAll(/LucideIcons\.([A-Za-z0-9]+)/g)].map((m) => m[1]) +); +const iconMissing = [...spriteKeys].filter((k) => !dartKeys.has(k)).sort(); +const iconExtra = [...dartKeys].filter((k) => !spriteKeys.has(k)).sort(); +if (iconMissing.length === 0 && iconExtra.length === 0) { + console.log(`✓ 图标同集 OK —— ${spriteKeys.size} 个 sprite ↔ Flutter 逐名相等`); +} else { + failed++; + console.error('✗ 图标同集 MISMATCH:'); + if (iconMissing.length) console.error(' sprite 有 / Flutter 缺:', iconMissing.join(', ')); + if (iconExtra.length) console.error(' Flutter 多 / sprite 无:', iconExtra.join(', ')); + console.error(' 修复:node design/codegen/gen_flutter_icons.mjs'); +} + +/* ② Flutter l10n 完整性 */ +const L10N = resolve(root, 'client/lib/l10n'); +const BASE = resolve(L10N, 'app_text.dart'); +const LANGS = ['zh', 'en', 'ja', 'ko', 'ru', 'es']; +const getterSet = (src) => new Set([...src.matchAll(/\bString\s+get\s+(\w+)\b/g)].map((m) => m[1])); +if (!existsSync(BASE)) { + console.log('· l10n 完整性:跳过(无 app_text.dart)'); +} else { + // canonical = 仅抽象 getter(`String get X;` 分号结尾);块体默认 `String get X {…}` 是继承默认,不要求 langs 实现 + const canonical = new Set([...readFileSync(BASE, 'utf8').matchAll(/\bString\s+get\s+(\w+)\s*;/g)].map((m) => m[1])); + const gaps = []; + for (const lang of LANGS) { + const f = resolve(L10N, `strings_${lang}.dart`); + if (!existsSync(f)) { gaps.push(` ${lang}: 文件缺失`); continue; } + const miss = [...canonical].filter((k) => !getterSet(readFileSync(f, 'utf8')).has(k)).sort(); + if (miss.length) gaps.push(` ${lang}: 缺 ${miss.length} 个 → ${miss.slice(0, 12).join(', ')}${miss.length > 12 ? ' …' : ''}`); + } + if (gaps.length === 0) { + console.log(`✓ l10n 完整 OK —— ${canonical.size} 个 getter × ${LANGS.length} 语言全实现`); + } else { + failed++; + console.error('✗ l10n 不完整(某语言漏 key):'); + gaps.forEach((g) => console.error(g)); + } +} +process.exit(failed ? 1 : 0); diff --git a/docs/pay-v2-dev-log.md b/docs/pay-v2-dev-log.md new file mode 100644 index 0000000..2d35560 --- /dev/null +++ b/docs/pay-v2-dev-log.md @@ -0,0 +1,28 @@ +# pay v2 支付+订单 — 自动开发决策日志 + +> 2026-07-11 起,自动模式落地。用户明早 double check。每个决断记这里。 + +## 背景 +用户指令:把今天设计的支付/订单功能全部落成真实代码(Flutter + Go),前后端测试,增量提交,自行决断,留记录。 + +## 最终 UX 模型(prototype 已定稿) +- 购买页一个**支付渠道 segswitch**:默认 **USDT→加密货币**,可切 **人民币→支付宝**。渠道=币种=方式,一步定,无弹层。 +- 套餐价随渠道切换(一口价不换算):月 ¥29.99/$4.99 · 季 ¥68.88/$12.99 · 年 ¥199.99/$34.99。 +- 买完直达支付页:crypto=USDT 地址+精确金额;alipay=桌面二维码/移动拉起 App。 +- 订单:列表→详情,状态 pending/paid/settled/expired。 +- nezha(哪吒)已从购买流程移除。 + +## 决策日志 +- **[step0]** prototype 支付渠道 switch 收口(3 屏 + `.segswitch` 原子入真相源+登记)。→ 提交后进开发。 + +## [step1] prototype 支付渠道 switch 收口(2026-07-11) +- `.segswitch` 原子入 atoms.css + 登记 index.html;purchase.html/ui-mobile/ui-desktop 三屏改成购买页渠道 switch(默认 USDT→crypto / 人民币→alipay),删两步弹层 + nezha。 +- 闸全绿:check-ds 5 项 / check-l1-sync(58 图标 + 190 l10n×6)。 + +## [step2] 服务端 pay orders + 分币种 catalog(2026-07-11) +- catalog.go:`CatalogItem.PriceUsdtMicro`(USDT 展示价,一口价)+ `SettlementCurrency`/`DisplayAmountMinor` helper(crypto→USDT / 法币→CNY)。 +- store.go:`PurchaseRow.CreatedAt`;`Insert` 增 amount/currency 参(下单落展示金额,webhook 覆盖实际);新增 `ListByUser`。 +- handler.go:新增 `List`(GET /v1/pay/orders,纯本地台账倒序)+ `orderView` 富字段;`GetOrder` 详情回带 sku/plan/method/amount/currency/created_at/paid_at;下单按方式落展示金额。 +- main.go:注册 `GET /v1/pay/orders`。 +- 测试:新增 List 越权/倒序/富字段、下单展示金额分币种、详情富字段;**修 3 个 pre-existing 迁移测试**(renumber 21→23 遗留)。`go test ./...` 全绿。 +- 决策:USDT 展示价放服务端 catalog($4.99/$12.99/$34.99),客户端 switch 直接读,不硬编码;实际扣款仍以 pay 侧为准。 diff --git a/tools/check-l1-sync.mjs b/tools/check-l1-sync.mjs index 5a56301..b9ad23e 100644 --- a/tools/check-l1-sync.mjs +++ b/tools/check-l1-sync.mjs @@ -1,145 +1,62 @@ #!/usr/bin/env node -// tools/check-l1-sync.mjs — L1 设计真相源跨端同源闸(纯 Node 零依赖) -// -// 治理依据:根 CLAUDE.md「前端设计系统治理(ds-flow)」——tokens/icons 以 -// design/prototype/ 为单一真源,Web 两端(website/usercenter)只能是同步副本。 -// Flutter 的 token 同源由 ci/check-codegen-drift.sh 守护(codegen 零 diff), -// 本闸补 Web token 值同源 + 三端图标 ⊆ 原型 sprite + Web 硬编码色扫描。 -// -// 本地/CI 直接跑:node tools/check-l1-sync.mjs -// -// 四道检查(任一违规 exit 1): -// ① website token 值同源 web/website/src/styles/tokens.gen.css 每个 token -// 值 ≡ design/prototype/tokens.css(:root + dark)。 -// ② usercenter token 同源 web/usercenter/public/colors_and_type.css 同上。 -// ③ 图标 ⊆ 原型 sprite usercenter LUCIDE(键+路径)+ Flutter pangolin_icons -// _byName(键)必须 ⊆ 原型 icons.js ICONS。 -// ④ Web 硬编码色扫描 website src / usercenter app|components|lib 禁裸 hex; -// 白名单 #fff/#000 + 品牌 logo 固定色;行内 `ds-allow` 豁免; -// 排除生成的 token 定义文件。 - -import { readFileSync, readdirSync, statSync, existsSync } from 'node:fs'; -import { join } from 'node:path'; +// check-l1-sync.mjs —— L1 跨端同源闸(ds-flow)。零依赖。 +// ① 图标集合:Flutter 图标集 == 原型 sprite 图标集(camel 化后逐名相等)。 +// 真源 design/prototype/icons.js;镜像 client/lib/widgets/pangolin_icons.dart。 +// ② l10n 完整性:app_text.dart 声明的每个 getter,6 个语言文件必须全部实现 +// (谁少 key 就红 —— 防「某语言漏字串」漂移,如支付字串只加 zh/en)。 +// 任一不通过 → 列出并 exit 1。 +import { readFileSync, existsSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; +import { dirname, resolve } from 'node:path'; -const ROOT = fileURLToPath(new URL('..', import.meta.url)); -const read = (p) => readFileSync(join(ROOT, p), 'utf8'); -const problems = []; +const __dirname = dirname(fileURLToPath(import.meta.url)); +const root = resolve(__dirname, '..'); +let failed = 0; -// 解析 CSS 块里的 --var: value;(压掉空白便于比较) -function parseVars(cssBlock) { - const out = {}; - for (const [, k, v] of cssBlock.matchAll(/(--[\w-]+)\s*:\s*([^;}]+)[;}]/g)) { - out[k] = v.replace(/\s+/g, ' ').trim(); - } - return out; -} -// 取 tokens.css 的 :root + [data-theme="dark"] 全量 token -function protoTokens() { - const css = read('design/prototype/tokens.css'); - const root = css.match(/:root\s*\{([^}]*)\}/s)?.[1] ?? ''; - const dark = css.match(/\[data-theme="dark"\]\s*\{([^}]*)\}/s)?.[1] ?? ''; - return { ...parseVars(root), ...parseVars(dark) }; +/* ① 图标集合 */ +const spriteKeys = new Set( + [...readFileSync(resolve(root, 'design/prototype/icons.js'), 'utf8') + .matchAll(/'([a-z0-9-]+)'\s*:\s*' m[1].replace(/-([a-z0-9])/g, (_, c) => c.toUpperCase())) +); +const dartKeys = new Set( + [...readFileSync(resolve(root, 'client/lib/widgets/pangolin_icons.dart'), 'utf8') + .matchAll(/LucideIcons\.([A-Za-z0-9]+)/g)].map((m) => m[1]) +); +const iconMissing = [...spriteKeys].filter((k) => !dartKeys.has(k)).sort(); +const iconExtra = [...dartKeys].filter((k) => !spriteKeys.has(k)).sort(); +if (iconMissing.length === 0 && iconExtra.length === 0) { + console.log(`✓ 图标同集 OK —— ${spriteKeys.size} 个 sprite ↔ Flutter 逐名相等`); +} else { + failed++; + console.error('✗ 图标同集 MISMATCH:'); + if (iconMissing.length) console.error(' sprite 有 / Flutter 缺:', iconMissing.join(', ')); + if (iconExtra.length) console.error(' Flutter 多 / sprite 无:', iconExtra.join(', ')); + console.error(' 修复:node design/codegen/gen_flutter_icons.mjs'); } -// ── ①② Web token 值同源 ──────────────────────────────────── -function checkWebTokenSync(label, webPath) { - if (!existsSync(join(ROOT, webPath))) { - problems.push(`[${label}] 找不到 ${webPath} —— 先跑 npm run gen:tokens`); - return; +/* ② Flutter l10n 完整性 */ +const L10N = resolve(root, 'client/lib/l10n'); +const BASE = resolve(L10N, 'app_text.dart'); +const LANGS = ['zh', 'en', 'ja', 'ko', 'ru', 'es']; +const getterSet = (src) => new Set([...src.matchAll(/\bString\s+get\s+(\w+)\b/g)].map((m) => m[1])); +if (!existsSync(BASE)) { + console.log('· l10n 完整性:跳过(无 app_text.dart)'); +} else { + // canonical = 仅抽象 getter(`String get X;` 分号结尾);块体默认 `String get X {…}` 是继承默认,不要求 langs 实现 + const canonical = new Set([...readFileSync(BASE, 'utf8').matchAll(/\bString\s+get\s+(\w+)\s*;/g)].map((m) => m[1])); + const gaps = []; + for (const lang of LANGS) { + const f = resolve(L10N, `strings_${lang}.dart`); + if (!existsSync(f)) { gaps.push(` ${lang}: 文件缺失`); continue; } + const miss = [...canonical].filter((k) => !getterSet(readFileSync(f, 'utf8')).has(k)).sort(); + if (miss.length) gaps.push(` ${lang}: 缺 ${miss.length} 个 → ${miss.slice(0, 12).join(', ')}${miss.length > 12 ? ' …' : ''}`); } - const proto = protoTokens(); - const web = parseVars(read(webPath)); - for (const [k, v] of Object.entries(proto)) { - if (!(k in web)) { - problems.push(`[${label}] 缺 token ${k} —— ${webPath} 未同步原型(重跑 gen:tokens)`); - } else if (web[k] !== v) { - problems.push(`[${label}] ${k} 值漂移:原型=${v} Web=${web[k]} —— ${webPath} 应由 build-tokens 从原型再生,勿手改`); - } + if (gaps.length === 0) { + console.log(`✓ l10n 完整 OK —— ${canonical.size} 个 getter × ${LANGS.length} 语言全实现`); + } else { + failed++; + console.error('✗ l10n 不完整(某语言漏 key):'); + gaps.forEach((g) => console.error(g)); } } - -// ── ③ 图标 ⊆ 原型 sprite ──────────────────────────────────── -{ - // 原型 icons.js: var ICONS = { 'name': '', ... } - const protoJs = read('design/prototype/icons.js'); - const protoBody = protoJs.match(/var ICONS\s*=\s*\{([\s\S]*?)\n\s*\};/)?.[1] ?? ''; - const protoIcons = {}; - for (const [, id, body] of protoBody.matchAll(/'([^']+)'\s*:\s*'([^']*)'/g)) protoIcons[id] = body; - if (!Object.keys(protoIcons).length) { - problems.push('[icons] 解析原型 icons.js ICONS 为空 —— 检查文件结构'); - } - - // usercenter: export const LUCIDE: Record = { 'name': '', ... } - const ucJs = read('web/usercenter/components/icons.tsx'); - const ucBody = ucJs.match(/LUCIDE\s*:[^=]*=\s*\{([\s\S]*?)\n\};/)?.[1] ?? ucJs.match(/LUCIDE\s*=\s*\{([\s\S]*?)\n\};/)?.[1] ?? ''; - // 键可能带引号('refresh-cw')或裸标识符(home)—— 两种都匹配(裸键此前被漏检)。 - for (const [, id, body] of ucBody.matchAll(/['"]?([\w-]+)['"]?\s*:\s*'([^']*)'/g)) { - if (!(id in protoIcons)) { - problems.push(`[icons] usercenter 图标「${id}」不在原型 sprite —— 先登记 design/prototype/icons.js 再用`); - } else if (protoIcons[id] !== body) { - problems.push(`[icons] usercenter 图标「${id}」路径与原型 sprite 不一致 —— 以原型为准`); - } - } - - // Flutter: static const Map _byName = { 'name': ..., } - const dart = read('client/lib/widgets/pangolin_icons.dart'); - const dartBody = dart.match(/_byName\s*=\s*\{([\s\S]*?)\};/)?.[1] ?? ''; - for (const [, id] of dartBody.matchAll(/'([^']+)'\s*:/g)) { - if (!(id in protoIcons)) { - problems.push(`[icons] Flutter 图标「${id}」不在原型 sprite —— 新图标先登记 design/prototype/icons.js`); - } - } -} - -// ── ④ Web 硬编码色扫描 ────────────────────────────────────── -{ - // 白名单:纯白/纯黑 + 品牌 logo 固定色(SVG 内联 fill,非业务散色) - const ALLOW = new Set(['fff', 'ffffff', '000', '000000', 'b96a3d', 'faf3ed', 'f4efe8', '9e5630', '3d2213']); - // 排除:生成的 token 定义文件 + 构建产物 - const SKIP = new Set([ - 'web/website/src/styles/tokens.gen.css', - 'web/usercenter/public/colors_and_type.css', - ]); - const SKIP_DIR = new Set(['node_modules', 'dist', 'out', '.next', 'build', '.astro', 'public']); - const ROOTS = ['web/website/src', 'web/usercenter/app', 'web/usercenter/components', 'web/usercenter/lib']; - const files = []; - const walk = (dir) => { - if (!existsSync(join(ROOT, dir))) return; - for (const name of readdirSync(join(ROOT, dir))) { - if (SKIP_DIR.has(name)) continue; - const rel = `${dir}/${name}`; - const st = statSync(join(ROOT, rel)); - if (st.isDirectory()) walk(rel); - else if (/\.(astro|jsx|tsx|ts|js|css)$/.test(name) && !SKIP.has(rel)) files.push(rel); - } - }; - ROOTS.forEach(walk); - for (const f of files) { - read(f).split('\n').forEach((line, i) => { - if (line.includes('ds-allow')) return; - for (const [hex] of line.matchAll(/#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})\b/g)) { - const v = hex.slice(1).toLowerCase(); - if (!ALLOW.has(v)) { - problems.push(`[web-hex] ${f}:${i + 1} 硬编码色 ${hex} —— 改用 var(--token),确需保留(如品牌 logo)加行内 ds-allow 注释`); - } - } - }); - } -} - -checkWebTokenSync('website-token', 'web/website/src/styles/tokens.gen.css'); -checkWebTokenSync('usercenter-token', 'web/usercenter/public/colors_and_type.css'); - -// ── 汇总 ──────────────────────────────────────────────────── -const line = '='.repeat(60); -if (problems.length) { - console.error(line); - console.error(`✗ L1 跨端同源闸未过(${problems.length} 处):`); - for (const p of problems) console.error(' · ' + p); - console.error(line); - process.exit(1); -} -console.log(line); -console.log('✓ 通过:L1 跨端同源(website/usercenter token 值 · 三端图标 ⊆ 原型 sprite · Web 无硬编码色)'); -console.log(line); +process.exit(failed ? 1 : 0);