From a642bf16a2bb96a01ac18fcbe5f83c9b80e791f6 Mon Sep 17 00:00:00 2001 From: wangjia <809946525@qq.com> Date: Thu, 11 Jun 2026 23:57:57 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=90=8C=E6=AD=A5=20design/=20?= =?UTF-8?q?=E8=AE=BE=E8=AE=A1=E7=B3=BB=E7=BB=9F(=E5=90=AB=20iPad=20tablet?= =?UTF-8?q?=20kit)=20+=20=E6=9E=B6=E6=9E=84=E4=BB=BB=E5=8A=A1=E6=8B=86?= =?UTF-8?q?=E5=88=86(todo/)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit design/ 同步自最新设计导出,新增 ui_kits/tablet/ 平板分栏布局;todo/ 录入 18 个并行实施任务。 Co-Authored-By: Claude Fable 5 --- design/CLAUDE.md | 157 + design/README.md | 160 + design/SKILL.md | 49 + design/_adherence.oxlintrc.json | 271 + design/_ds_bundle.js | 10643 ++++++++++++++++ design/_ds_manifest.json | 1 + design/assets/app-icon.svg | 17 + design/assets/logo-mark-mono.svg | 10 + design/assets/logo-mark-white.svg | 11 + design/assets/logo-mark.svg | 11 + design/assets/logo-wordmark.svg | 13 + design/colors_and_type.css | 218 + design/flutter/README.md | 135 + design/flutter/assets/app-icon.svg | 17 + design/flutter/assets/logo-mark-white.svg | 11 + design/flutter/assets/logo-mark.svg | 11 + design/flutter/assets/logo-wordmark.svg | 13 + design/flutter/main.dart | 58 + design/flutter/pangolin_theme.dart | 307 + design/flutter/pubspec.yaml | 84 + design/flutter/widgets/account_screens.dart | 296 + design/flutter/widgets/auth_screen.dart | 159 + design/flutter/widgets/connect_button.dart | 147 + design/flutter/widgets/country_code.dart | 61 + design/flutter/widgets/home_shell.dart | 328 + design/flutter/widgets/onboarding_screen.dart | 103 + design/flutter/widgets/pangolin_button.dart | 67 + design/flutter/widgets/pangolin_icons.dart | 85 + design/flutter/widgets/pangolin_logo.dart | 74 + design/flutter/widgets/pangolin_widgets.dart | 14 + design/flutter/widgets/plan_card.dart | 112 + design/flutter/widgets/server_tile.dart | 51 + design/flutter/widgets/status_pill.dart | 45 + design/preview/brand-app-icon.html | 45 + design/preview/brand-logo.html | 19 + design/preview/color-clay.html | 24 + design/preview/color-dark-surfaces.html | 19 + design/preview/color-sand.html | 24 + design/preview/color-semantic.html | 25 + design/preview/comp-badges.html | 22 + design/preview/comp-buttons.html | 27 + design/preview/comp-connect.html | 52 + design/preview/comp-inputs.html | 26 + design/preview/comp-plan-card.html | 24 + design/preview/comp-server-row.html | 25 + design/preview/radii.html | 18 + design/preview/shadows.html | 16 + design/preview/spacing.html | 17 + design/preview/type-body.html | 11 + design/preview/type-cjk.html | 12 + design/preview/type-display.html | 9 + design/preview/type-mono.html | 18 + design/preview/type-scale.html | 16 + .../sketch-2026-06-10T15-35-49-rnni6r.napkin | 6 + design/server/ARCHITECTURE.md | 141 + design/ui_kits/desktop/README.md | 30 + design/ui_kits/desktop/dapp.jsx | 278 + design/ui_kits/desktop/dflows.jsx | 313 + design/ui_kits/desktop/dparts.jsx | 161 + design/ui_kits/desktop/index.html | 31 + design/ui_kits/desktop/macos-window.jsx | 198 + design/ui_kits/mobile/README.md | 29 + design/ui_kits/mobile/flows.jsx | 269 + design/ui_kits/mobile/index.html | 128 + design/ui_kits/mobile/ios-frame.jsx | 348 + design/ui_kits/mobile/parts.jsx | 293 + design/ui_kits/mobile/screens.jsx | 349 + design/ui_kits/tablet/README.md | 19 + design/ui_kits/tablet/index.html | 28 + design/ui_kits/tablet/tabapp.jsx | 368 + design/ui_kits/usercenter/README.md | 22 + design/ui_kits/usercenter/index.html | 29 + design/ui_kits/usercenter/mobile-preview.html | 13 + design/ui_kits/usercenter/ucapp.jsx | 367 + design/ui_kits/usercenter/ucparts.jsx | 96 + design/ui_kits/website/README.md | 29 + design/ui_kits/website/index.html | 378 + design/ui_kits/website/mobile-preview.html | 13 + design/ui_kits/website/site.js | 240 + design/ui_kits/website/website.css | 292 + design/uploads/pasted-1781105742272.txt | 4 + design/uploads/pasted-1781105748445.txt | 4 + design/uploads/pasted-1781108801394-0.png | Bin 0 -> 259432 bytes todo/todo.html | 989 ++ todo/todo.json | 293 + 85 files changed, 19946 insertions(+) create mode 100644 design/CLAUDE.md create mode 100644 design/README.md create mode 100644 design/SKILL.md create mode 100644 design/_adherence.oxlintrc.json create mode 100644 design/_ds_bundle.js create mode 100644 design/_ds_manifest.json create mode 100644 design/assets/app-icon.svg create mode 100644 design/assets/logo-mark-mono.svg create mode 100644 design/assets/logo-mark-white.svg create mode 100644 design/assets/logo-mark.svg create mode 100644 design/assets/logo-wordmark.svg create mode 100644 design/colors_and_type.css create mode 100644 design/flutter/README.md create mode 100644 design/flutter/assets/app-icon.svg create mode 100644 design/flutter/assets/logo-mark-white.svg create mode 100644 design/flutter/assets/logo-mark.svg create mode 100644 design/flutter/assets/logo-wordmark.svg create mode 100644 design/flutter/main.dart create mode 100644 design/flutter/pangolin_theme.dart create mode 100644 design/flutter/pubspec.yaml create mode 100644 design/flutter/widgets/account_screens.dart create mode 100644 design/flutter/widgets/auth_screen.dart create mode 100644 design/flutter/widgets/connect_button.dart create mode 100644 design/flutter/widgets/country_code.dart create mode 100644 design/flutter/widgets/home_shell.dart create mode 100644 design/flutter/widgets/onboarding_screen.dart create mode 100644 design/flutter/widgets/pangolin_button.dart create mode 100644 design/flutter/widgets/pangolin_icons.dart create mode 100644 design/flutter/widgets/pangolin_logo.dart create mode 100644 design/flutter/widgets/pangolin_widgets.dart create mode 100644 design/flutter/widgets/plan_card.dart create mode 100644 design/flutter/widgets/server_tile.dart create mode 100644 design/flutter/widgets/status_pill.dart create mode 100644 design/preview/brand-app-icon.html create mode 100644 design/preview/brand-logo.html create mode 100644 design/preview/color-clay.html create mode 100644 design/preview/color-dark-surfaces.html create mode 100644 design/preview/color-sand.html create mode 100644 design/preview/color-semantic.html create mode 100644 design/preview/comp-badges.html create mode 100644 design/preview/comp-buttons.html create mode 100644 design/preview/comp-connect.html create mode 100644 design/preview/comp-inputs.html create mode 100644 design/preview/comp-plan-card.html create mode 100644 design/preview/comp-server-row.html create mode 100644 design/preview/radii.html create mode 100644 design/preview/shadows.html create mode 100644 design/preview/spacing.html create mode 100644 design/preview/type-body.html create mode 100644 design/preview/type-cjk.html create mode 100644 design/preview/type-display.html create mode 100644 design/preview/type-mono.html create mode 100644 design/preview/type-scale.html create mode 100644 design/scraps/sketch-2026-06-10T15-35-49-rnni6r.napkin create mode 100644 design/server/ARCHITECTURE.md create mode 100644 design/ui_kits/desktop/README.md create mode 100644 design/ui_kits/desktop/dapp.jsx create mode 100644 design/ui_kits/desktop/dflows.jsx create mode 100644 design/ui_kits/desktop/dparts.jsx create mode 100644 design/ui_kits/desktop/index.html create mode 100644 design/ui_kits/desktop/macos-window.jsx create mode 100644 design/ui_kits/mobile/README.md create mode 100644 design/ui_kits/mobile/flows.jsx create mode 100644 design/ui_kits/mobile/index.html create mode 100644 design/ui_kits/mobile/ios-frame.jsx create mode 100644 design/ui_kits/mobile/parts.jsx create mode 100644 design/ui_kits/mobile/screens.jsx create mode 100644 design/ui_kits/tablet/README.md create mode 100644 design/ui_kits/tablet/index.html create mode 100644 design/ui_kits/tablet/tabapp.jsx create mode 100644 design/ui_kits/usercenter/README.md create mode 100644 design/ui_kits/usercenter/index.html create mode 100644 design/ui_kits/usercenter/mobile-preview.html create mode 100644 design/ui_kits/usercenter/ucapp.jsx create mode 100644 design/ui_kits/usercenter/ucparts.jsx create mode 100644 design/ui_kits/website/README.md create mode 100644 design/ui_kits/website/index.html create mode 100644 design/ui_kits/website/mobile-preview.html create mode 100644 design/ui_kits/website/site.js create mode 100644 design/ui_kits/website/website.css create mode 100644 design/uploads/pasted-1781105742272.txt create mode 100644 design/uploads/pasted-1781105748445.txt create mode 100644 design/uploads/pasted-1781108801394-0.png create mode 100644 todo/todo.html create mode 100644 todo/todo.json diff --git a/design/CLAUDE.md b/design/CLAUDE.md new file mode 100644 index 0000000..7181ac2 --- /dev/null +++ b/design/CLAUDE.md @@ -0,0 +1,157 @@ +# CLAUDE.md — 穿山甲 VPN · Pangolin VPN 设计系统 + +> 本文件是项目持久指令,任何会话自动注入。目标:让任何 agent(含 Claude Code)都能**完全还原**这套设计,不走样。生产前端是 **Flutter**;React/HTML 是视觉规范参考。 + +--- + +## 0. 一句话定位 +**穿山甲(Pangolin)** — 极简、轻量、亲和的跨平台消费级网络加速应用。暖大地色(穿山甲鳞甲)+ 大量留白 + 双语单显 + 深浅双主题。核心卖点:一键连接、智能选线、即开即用。 + +--- + +## 1. 不可动摇的设计铁律(违反即跑偏) +1. **颜色只用语义 token**(见 §3 / `colors_and_type.css` / `flutter/pangolin_theme.dart`)。绝不硬编码十六进制到组件里。 +2. **暖色基调,拒绝纯黑纯白大面积填充**。浅色画布=沙米白 `--sand-50`;深色画布=暖 espresso `--sand-950`(非纯黑)。卡面浅色用纯白可,深色用 `#221E19`。 +3. **主色是 clay 黏土铜 `#B96A3D`**,不是蓝/紫。绝不出现蓝紫渐变。渐变只在 clay 同色系内(`clay-600→clay-800`),且仅限 App 图标、PRO 套餐卡、连接成功光晕等少数强调处。 +4. **圆角偏大**(按钮全胶囊 `full`,卡片 `lg–xl`,输入 `md`),传递亲和。 +5. **阴影柔和暖调**(`rgba(45,30,20,…)`),绝不用冷灰阴影。 +6. **单语言显示**:产品任一时刻只显示中文**或**英文,由设置/账户页「中文 / EN」段控切换。**绝不中英并排**(并排只出现在设计系统 specimen 卡)。 +7. **状态用「色点 + 文字胶囊」**,绝不用 emoji。 +8. **国家用 2 字母码块**(HK/JP/SG…),绝不用 emoji 国旗(跨平台渲染不一致)。 +9. **图标用 Lucide 细线条**(2px stroke、圆角端点)。不用实心、不用 emoji、不用 Unicode 字符当图标。 +10. **App 内不放支付表单**。资金流全走外部:兑换激活码 + 渠道(自助发卡店 / Telegram / LINE / 邮箱)。VPN 风控要求。 +11. **品牌母题=行走穿山甲**(实心鳞甲剪影,朝右,圆头、匀称背、长尾)。**绝不手绘新图形或用 emoji 替代**——直接用 `assets/*.svg`。 +12. **不堆砌**:无填充内容、无装饰性数字/图标、无 AI slop。留白即设计。 +13. **脱敏(国内市场)**:产品主名统一「穿山甲 / Pangolin」,**不带 VPN 后缀**;定位话术=「网络加速 / 体验优化 + 隐私保护」。**红线词**(任何界面/宣传文案禁用):VPN、翻墙、科学上网、突破封锁、自由穿越、Go anywhere。安全词:加速、极速畅连、稳定、加速线路、隐私保护、无日志。「VPN」仅可出现在必要的技术文档/代码注释,绝不进 UI 文案、锁版、标题、aria-label。Logo 字标小字用「PANGOLIN」。 + +--- + +## 2. 字体系统 +| 角色 | 字体 | 用途 | +|---|---|---| +| Display | **Sora**(600/700) | 标题、营销大字、Logo 字标 | +| Body/UI | **Manrope**(400–700) | 界面与正文主力 | +| CJK | **Noto Sans SC**(400/500/700) | 中文专用伴随字体 | +| Mono | **JetBrains Mono** | IP/速率/时长/密钥等数据(开 tabular nums) | + +字阶:48 / 36 / 30 / 24 / 20 / 18 / 16 / 14 / 12。标题字距收紧 `-0.02em`,overline 放宽 `0.08em`。 +> 四款均开源(Google Fonts),是本品牌**正式选定**字体,非临时替代。 + +--- + +## 3. 颜色语义 token(速查;完整见 `colors_and_type.css`) +**主色阶 clay**:50 `#FAF3ED` → 500 `#B96A3D`(品牌主色)→ 900 `#3D2213` +**中性 sand**:50 `#FAF8F4` → 900 `#1F1C18` → 950 `#14110E`(暗画布) +**语义**:success `#5B8C5A`(连接/安全)· warning `#D69A3C`(连接中)· danger `#C0533B`(断开/错误),各配 `*-subtle` 浅底。 + +语义变量(浅/深自动切换):`--bg` `--bg-subtle` `--surface` `--surface-2` · `--fg1/2/3` `--fg-on-accent` · `--accent` `--accent-hover/press/subtle/border` · `--border` `--border-strong` `--ring` · `--success/warning/danger(+ -subtle)`。 +深色主题:``;accent 在深色用 `clay-400`(更亮)。 + +间距 4px 基准:4/8/12/16/24/32/48/64。圆角:sm6/md10/lg14/xl20/2xl28/full。 +动效:`ease-out cubic-bezier(.22,1,.36,1)`(进出场)· `ease-in-out`(状态)· 时长 fast140/base220/slow360。淡入+轻移,无浮夸弹跳。 + +交互态:hover 主色加深一档 / press 缩放 .97 + 再深 / focus clay 光环 `0 0 0 4px ring` / selected `accent-subtle` 底 + clay 字 / disabled sand-200 底 + sand-400 字。 + +--- + +## 4. 目录结构与职责 +``` +colors_and_type.css ← 唯一真相来源(CSS 令牌) +flutter/ ← 生产令牌 + 组件(Dart) + pangolin_theme.dart · 令牌 + 明/暗 ThemeData + pubspec.yaml · 工程清单模板(依赖+资产+字体) + main.dart · 入口示例(登录→引导→主框架) + assets/*.svg · 4 个矢量 logo + widgets/ · 组件 + 页面(见 §5) + README.md · Flutter 接入说明 +assets/*.svg ← 品牌 logo(mark/white/mono/wordmark/app-icon) +preview/*.html ← 设计系统 specimen 卡(色彩/字体/间距/组件/品牌) +ui_kits/mobile/ ← 移动 App 视觉参考(React,可点击原型) +ui_kits/tablet/ ← iPad 客户端视觉参考(React,侧栏分栏布局) +ui_kits/desktop/ ← 桌面客户端视觉参考(React) +ui_kits/website/ ← 官网宣传页(产品/定价/下载/文档/Blog,响应式) +ui_kits/usercenter/ ← Web 用户中心(概览/订阅导入/兑换/邀请返利/设置含 2FA) +server/ARCHITECTURE.md ← 后端架构蓝本(Go 控制面 + WireGuard 数据面,供 Claude Code 实现) +README.md ← 品牌语境/内容/视觉/图标/索引 +SKILL.md ← Agent Skill 入口 +``` +> 账户/设备管理已集成进移动+桌面客户端;**Web 用户中心**(`ui_kits/usercenter/`)职责为订阅分发 + 兑换 + 邀请裂变,服务 phase-1 网页闭环,不与客户端重复。 + +--- + +## 5. 还原产品时的页面清单(两端一致) +**移动 App**(`ui_kits/mobile/`,Flutter 见 `flutter/widgets/`): +- 登录/注册(`AuthFlow` / `auth_screen.dart`):登录=邮箱+密码;注册=邮箱→发验证码→填码→设密码(多端登录) +- 首次引导(`Onboarding` / `onboarding_screen.dart`):3 屏(连接全球→授权配置→安全无日志),可跳过 +- 主框架 4 Tab(`home_shell.dart`):**连接 / 节点 / 统计 / 账户**;内容区支持**左右滑动切换 Tab**(>60px 且横向位移明显大于纵向,200ms 方向感知滑入;子页不响应) + - 连接:核心连接键三态(off 暖灰底+虚线轨道环 / connecting 旋转弧 / on 绿底+满环+计时)+ 当前节点卡 + 实时速率;**免费版额外显示额度卡**(今日剩余分钟 + 进度条(≤3 分钟变 warning 色)+ 「看广告开始使用」→ 点击后变绿色「已解锁 · 今日可用」) + - 节点:顶部置顶**「智能选择」推荐卡**(常驻 accent-subtle 底 + clay 渐变 zap 图标 + 「推荐」胶囊 + 文案「根据当前网络环境,自动选择最优节点」,默认选中);其下才是搜索 + 节点列表(国家码块·延迟·信号条·选中态) + - 统计:流量/延迟/时长指标卡 + 本周柱状图 + - 账户:套餐横幅(免费版/PRO 自适应)+ 账户信息(邮箱/密码/退出)+ 设备管理 + 兑换/联系入口 +- 账户子页(`account_screens.dart`):套餐选择 / 设备管理(可移除)/ 兑换&购买 / 联系我们 + +**桌面客户端**(`ui_kits/desktop/`):同上(含智能选择推荐卡与免费版额度卡),布局为 920×600 窗口 + 左侧栏导航(连接/节点/统计/账户/设置);登录为分屏卡(品牌左/表单右)。 + +**iPad 客户端**(`ui_kits/tablet/`):1180×820 横屏,左侧栏导航(触控尺寸,行高 ≥48px)+ 内容区;连接页为双栏(左大按钮 / 右信息列:额度卡→当前节点→速率);节点为双列网格 + 置顶智能推荐卡;复用 `ui_kits/mobile/parts.jsx` 的原子与字串。Flutter 端对应自适应断点:宽度 ≥900 时从底部 Tab 切换为侧栏布局。 + +**Web 用户中心**(`ui_kits/usercenter/`):概览(套餐状态/用量)· 订阅导入(订阅链接+二维码+一键导入三方客户端)· 兑换 · 邀请返利 · 设置(含 **2FA/TOTP 开关**,用户可自选开启)。响应式,移动端底部 Tab + 左右滑动切换。 + +**核心连接键**(最重要的组件,务必还原):圆形,三态—— +- off:暖灰底 `sand-100` + clay power 图标 + 虚线轨道环 + 「点击连接」 +- connecting:clay 实底 + 旋转进度弧 + 旋转 loader +- on:success 实底 + 白盾勾 + 满白环 + 圆内计时 + 「已加密」+ 绿光晕 +> 注意:背景在渐变↔纯色间切换时,过渡只用 `box-shadow`/`background-color`,**不要 `transition: all`**(会导致背景计算失效)。 + +--- + +## 6. 修改设计系统时(本项目是 DS 工程) +- 改令牌 → 编辑 `colors_and_type.css` **同时**同步 `flutter/pangolin_theme.dart`(两者必须一致)。 +- 改 specimen 卡 → `preview/*.html`,首行带 ``。 +- 改组件视觉 → 同步改 React(`ui_kits/`)与 Flutter(`flutter/widgets/`)两处。 +- 改完跑 `check_design_system` 确认无报错(动效 token 标 `@kind other`)。 +- logo 改动 → 同步 5 个 SVG(`assets/`)+ `flutter/assets/` 副本 + React 内联 `Mark`/`DMark` + Flutter `pangolin_logo.dart`。 + +--- + +## 7. 套餐口径(单一来源,所有界面/文案引用此处) +- **新用户体验期(7 天)**:注册即享 7 天免费使用(不限时长与节点)。 +- **体验期后(免费版)**:仅 1 个基础节点;每日 10 分钟时长;**每日使用前需观看激励视频广告解锁**(流量不设上限,时长为唯一限制)。 +- **PRO(付费)**:不限时长流量,80+ 加速线路,5 台设备。¥25/月(年付 ¥20/月)。 +- **团队版**:¥99/月,10 席位。 +- 收款渠道:自助发卡店 / **USDT (TRC20)** / Telegram / LINE / 邮箱。App 与官网内无支付表单。 + +## 8. 演示占位(落地需替换为真实信息) +渠道:自助发卡店 `shop.pangolin.vpn` · Telegram `@PangolinVPN_bot` · LINE `@pangolinvpn` · 邮箱 `support@pangolin.vpn` / `buy@pangolin.vpn`。账户演示 `me@pangolin.vpn`。服务时间「每日 9:00–24:00 (GMT+8)」。 + +--- + +## 9. 给 Claude Code 的实施工作步骤(从设计到产品) + +> 总原则:**先读规范再动手;UI 以 `ui_kits/` React 原型为像素基准,逻辑以 `server/ARCHITECTURE.md` 为蓝本**。任何界面产出前,先打开对应 React 原型比对一遍。 + +### 第 0 步 · 进场必读(顺序固定) +1. 本文件 §1 铁律 → §7 套餐口径 → §5 页面清单。 +2. `colors_and_type.css`(令牌真相源)与 `flutter/pangolin_theme.dart`(Dart 镜像)。 +3. 目标端的 UI Kit 源码(如做移动端就读 `ui_kits/mobile/*.jsx`)。 + +### 第 1 步 · Flutter 客户端(主路径) +1. 新建工程,用 `flutter/pubspec.yaml` 覆盖;拷 `pangolin_theme.dart`、`widgets/`、`main.dart` 到 `lib/`,`flutter/assets/*.svg` 到工程 assets。 +2. 字体:下载 Sora / Manrope / Noto Sans SC / JetBrains Mono 的 ttf 放 `fonts/`(族名对齐 pubspec),或改用 `google_fonts`。 +3. `flutter pub get && flutter run` —— `main.dart` 已串好登录→引导→主框架,应能直接跑出演示态。 +4. 对照 `ui_kits/mobile/` 逐屏补齐 Flutter 端差异(智能选择推荐卡、免费额度卡+看广告解锁、Tab 滑动切换)——React 原型是验收标准。 +5. 接口对接前全部用演示数据;接口就绪后按 `server/ARCHITECTURE.md` §3 的契约替换。 + +### 第 2 步 · 后端(Go) +按 `server/ARCHITECTURE.md` §7 的模块顺序实现(openapi → auth → codes → devices → nodes → usage → 管理端)。每模块完成定义:单测 + OpenAPI 同步 + 错误文案双语且符合铁律 13 脱敏口径。 + +### 第 3 步 · 官网与用户中心(Web) +- 官网:以 `ui_kits/website/` 为样板迁移到静态框架(Astro/Next 皆可),保留 i18n 双语单显、响应式断点与全部脱敏文案。 +- 用户中心:以 `ui_kits/usercenter/` 为样板,接 `server/` 的 me/redeem/devices 接口;2FA 用 TOTP。 + +### 第 4 步 · 验收检查清单(每个界面提交前自查) +- [ ] 颜色全部来自语义 token,无硬编码十六进制 +- [ ] 明/暗两主题均验证过;中/英两语言均验证过(单显不并排) +- [ ] 文案无铁律 13 红线词;套餐数字与 §7 一致 +- [ ] 图标只用 Lucide;国家码块无 emoji 国旗;状态胶囊无 emoji +- [ ] 连接键三态、智能选择默认选中、免费版额度/广告解锁流程与 React 原型一致 +- [ ] App/官网内无任何支付表单,购买只引导到外部渠道 diff --git a/design/README.md b/design/README.md new file mode 100644 index 0000000..bfb766a --- /dev/null +++ b/design/README.md @@ -0,0 +1,160 @@ +# 穿山甲 VPN · Pangolin VPN — Design System + +> 极简、轻量、亲和的跨平台网络加速品牌设计系统。暖大地色(穿山甲鳞甲)+ 大量留白 + 双语界面。 + +这是一套**从零创建**的品牌设计系统(无既有代码库 / Figma / Logo 输入)。所有视觉决策记录于本文件,所有 token 见 `colors_and_type.css`,可预览的卡片在 `preview/`,可拼装的高保真界面在 `ui_kits/`。 + +--- + +## 1. 产品语境 Product Context + +**穿山甲(Pangolin)** 是一款面向大众的消费级网络加速应用。核心价值主张:**一键连接、智能选线、即开即用**。品牌人格定位为「极简轻量、亲和易用」——不堆砌技术参数,不制造焦虑,用干净的留白和温暖的大地色传递「顺畅又轻松」的感觉。 + +> 脱敏说明:面向国内市场,品牌与宣传统一定位为「网络加速 / 体验优化 + 隐私保护」,**不使用「VPN / 翻墙 / 窜 / 自由穿越」等敏感词**。产品主名统一为「穿山甲」(不带 VPN 后缀);VPN 仅在必要技术文档出现。 + +名字 **穿山甲(Pangolin)** 取鳞甲「防护 / 安全」之意。视觉系统提炼出**行走穿山甲**剖面作为核心图形母题。 + +### 覆盖的产品界面(UI Kits) +1. **移动 App**(iOS / Android)—— 登录/注册、首次引导、连接主界面、服务器列表、统计、账户中心(套餐/设备/兑换/联系)。`ui_kits/mobile/` +2. **桌面客户端**(Windows / macOS)—— 登录/注册、首次引导、连接面板、节点、统计、账户中心、联系我们、设置。`ui_kits/desktop/` +3. **iPad 客户端** —— 侧栏分栏布局(连接/节点/统计/账户),触控尺寸,复用移动端原子。`ui_kits/tablet/` + +> 用户后台(账户 / 套餐 / 设备 / 兑换)已**直接集成进移动与桌面客户端**(因支付走外部渠道,独立 Web 后台冗余,故不再单列)。 + +### 生产前端:Flutter +生产代码使用 **Flutter**。设计令牌的 Dart 版本见 `flutter/pangolin_theme.dart`(与 `colors_and_type.css` 一一对应:颜色、间距、圆角、动效、文字、明/暗 `ThemeData`)。`ui_kits/` 的 React/HTML 为**视觉规范参考**,Flutter 实现以其为蓝本。详见 `flutter/README.md`。 + +### 输入来源 Sources +- **无外部素材**。本系统完全从品牌简述「穿山甲 vpn软件」与一轮用户访谈问题推导而来。 +- 若后续有真实 Logo / Figma / 代码库,请通过 Import 菜单附上,我会据此校准。 + +--- + +## 2. 内容基础 Content Fundamentals + +界面为**中英双语**,中文为主、英文为辅。**重要:真实产品在任一时刻只显示一种语言**——用户在设置/账户页通过「中文 / EN」段控切换,中英不会同时出现。设计系统 `preview/` 卡片里出现的中英并排,仅用于同时展示两套字体规范(specimen),不代表产品 UI。三个 UI Kit 均已实现单语言切换。 + +- **人称**:对用户用「你」,自称用「我们」。亲切、平等,不端着。例:「我们不记录你的浏览数据」。 +- **语气**:简短、肯定、轻松。动词开头的祈使句优先:「一键连接」「选择节点」「立即升级」。 +- **大小写**:英文标题用 Title Case 或全大写小字 overline(`PANGOLIN`、`ACCOUNT`);正文句首大写。中文不加句号于短标题/按钮。 +- **数字与单位**:数据读数用等宽字体,数值大、单位小且着 clay 主色(`86.4 Mb/s`、`24 ms`)。 +- **Emoji**:**不使用** emoji 作装饰。状态一律用「色点 + 文字胶囊」表达。 +- **专业术语**:保留行业通用英文词(WireGuard、Kill Switch、P2P),不强行翻译,降低理解成本。 +- **Vibe**:像一位靠谱又不啰嗦的朋友。文案示例: + - 连接态:`已连接 Connected` / `连接中 Connecting` / `未连接 Off` + - 营销:`极速畅连 Smooth`、`极速 · 稳定 · 省心` + - 隐私承诺:`我们不记录你的浏览数据 · 端到端加密` + +--- + +## 3. 视觉基础 Visual Foundations + +### 色彩 Color +- **主色 Clay / Copper**(`--clay-500 #B96A3D`):温暖的黏土铜色,呼应穿山甲鳞甲与大地。用于主按钮、强调、链接、选中态。完整 50–900 色阶见 token。 +- **中性 Sand / Taupe**:暖灰中性 50–950,承担文本与表面。`--sand-950 #14110E` 是深色画布(暖 espresso,**非纯黑**——保持温度)。 +- **语义色**(大地调校):`success #5B8C5A`(连接/安全)、`warning #D69A3C`(连接中)、`danger #C0533B`(断开/错误)。各配 `*-subtle` 浅底。 +- **双主题**:浅色以沙米白(`--sand-50`)为画布、纯白为卡面;深色以暖 espresso 为画布、`#221E19` 为卡面。切换方式:``。**所有颜色都走语义 token,组件天然双主题。** +- **影像基调**(若引入照片):偏暖、自然光、低饱和大地色,可加细微胶片颗粒;避免冷色与高对比霓虹。 + +### 字体 Type +- **Display — Sora**(600/700):标题、营销大字、Logo 字标。几何但不冰冷。 +- **Body / UI — Manrope**(400–700):界面与正文主力。 +- **CJK — Noto Sans SC**(400/500/700):中文专用伴随字体,与上面两者度量协调。 +- **Mono — JetBrains Mono**:IP、速率、时长、密钥等数据读数,启用 tabular nums。 +- 字阶:48 / 36 / 30 / 24 / 20 / 18 / 16 / 14 / 12。标题字距收紧(-0.02em),overline 字距放宽(0.08em)。 +- ⚠️ **字体说明**:四款均为开源字体(Google Fonts),作为本品牌**正式选定**字体,非临时替代。若你已有授权品牌字体,告诉我替换。 + +### 间距与网格 Spacing +- **4px 基准网格**。常用:4 / 8 / 12 / 16 / 24 / 32 / 48 / 64。 +- 留白慷慨——这是「极简轻量」气质的关键,卡片内边距偏大(16–24px),区块间距 32–48px。 + +### 圆角 Radius +- 偏大圆角传递亲和:`sm 6 / md 10 / lg 14 / xl 20 / 2xl 28 / full`。 +- 按钮多用 `full`(胶囊);卡片用 `lg`–`xl`;输入框用 `md`。 + +### 阴影与高度 Elevation +- 暖色调投影(`rgba(45,30,20,…)`),**柔和克制**,不用冷灰阴影。 +- `sm` 卡片静置、`md` 浮起卡片/按钮悬浮、`lg` 弹窗/抽屉、`xl` 模态。 +- 深色主题下投影改为深黑透明并加强。 +- **焦点环**:`0 0 0 4px var(--ring)`(clay 半透明),用于输入与可聚焦元素。 + +### 边框 Borders +- 细边 1–1.5px,色用 `--border`(浅:sand-200 / 深:白 10% 透明)。强调边 `--border-strong`。 +- 卡片优先「`surface` + `shadow-sm` + 极淡边框」组合,而非重边框。 + +### 动效 Motion +- 缓动:进出场用 `--ease-out cubic-bezier(.22,1,.36,1)`,状态切换用 `--ease-in-out`。 +- 时长:`fast 140ms`(hover/press)、`base 220ms`(开关/展开)、`slow 360ms`(页面/抽屉)。 +- 风格:**淡入 + 轻微位移**为主,无浮夸弹跳。连接中状态用环形 loader 旋转;连接成功用色环扩散(`box-shadow` 外环)。 + +### 交互态 States +- **Hover**:主色按钮加深一档(`--accent-hover`);幽灵按钮显示 `--accent-subtle` 浅底;列表行显示 sand 浅底。 +- **Press**:轻微缩放(`scale .97`)+ 再深一档(`--accent-press`)。 +- **Focus**:clay 焦点环(见上)。 +- **Selected**:`--accent-subtle` 底 + clay 文字/勾选。 +- **Disabled**:sand-200 底 + sand-400 字,`not-allowed`。 + +### 透明与模糊 Transparency / Blur +- 克制使用。仅用于:导航/工具栏滚动时的毛玻璃(`backdrop-filter: blur(12px)` + 表面色 80% 透明)、模态遮罩(`--overlay`)。 +- 不做大面积玻璃拟态。 + +### 布局规则 Layout +- 移动端:顶栏 + 中部巨型连接键 + 底部节点条/Tab,关键操作单手可达。 +- 桌面端:紧凑窄窗(~360–420px 宽),托盘风格;或带左侧栏的标准窗口。 +- Web 后台:左侧导航 + 内容区,最大内容宽度约束(~1080px),大量留白。 +- 固定元素:移动端底部 Tab、桌面端标题栏、Web 顶栏(滚动毛玻璃)。 + +### 背景 Backgrounds +- 以**纯色画布**为主(沙米白 / 暖 espresso),**不滥用渐变**。 +- 渐变仅出现在:App 图标、PRO 套餐卡、连接成功的氛围光晕等少数强调处,且只在 clay 同色系内(`clay-600 → clay-800`)。 +- 不使用紫蓝渐变、不使用重复纹理贴图。 + +--- + +## 4. 图标系统 Iconography + +- **图标库:[Lucide](https://lucide.dev)**(细线条、2px stroke、圆角端点)。与「极简轻量、亲和」气质一致。**经 CDN 引入**,非本地字体: + ```html + + + + ``` + 在 React/JSX 中可用 `lucide-react` 或直接渲染 `` 后调用 `createIcons()`。 +- **常用图标**:`power`(连接)、`shield-check`(已保护)、`globe`(节点)、`zap`(极速)、`settings`、`loader`(连接中,旋转)、`check`(选中)、`chevron-right`、`map-pin`、`user`、`credit-card`。 +- ⚠️ **替代说明**:本品牌无自有图标字体,Lucide 为**选定的最接近匹配**(同等线宽/圆角风格)。如需自有图标集请提供。 +- **国旗/国家**:服务器列表**不用 emoji 国旗**(跨平台渲染不一致),改用 sand 底圆角方块内的 2 字母**国家码**(HK / JP / SG / US),选中态翻转为 clay 底白字。 +- **Unicode 装饰字符**:不使用。 +- **品牌图形**:核心母题为「**行走穿山甲**」(实心鳞甲剪影,朝右),见 `assets/logo-*.svg` 与 `assets/app-icon.svg`,**不要用 emoji 或随手 SVG 替代,直接复制**。 + +--- + +## 5. 资产清单 Assets + +`assets/` 内: +- `logo-mark.svg` — 主标记(clay 行走穿山甲,鳞甲剪影,朝右) +- `logo-mark-white.svg` — 反白标记(深色背景用,奶油色身体 + clay 鳞脊) +- `logo-mark-mono.svg` — 单色标记(`currentColor`,内联使用时随文字色) +- `logo-wordmark.svg` — 标记 + 字标横版锁版(穿山甲 / PANGOLIN) +- `app-icon.svg` — App 图标(clay 渐变圆角方 + 白色穿山甲) + +> ⚠️ Logo 为本系统**新创建**的图形(用户选择「抽象几何标记 + 字标」,迭代后定为行走穿山甲剪影,朝右)。若你有正式 Logo,请替换 `assets/` 内文件并告知我。 + +--- + +## 6. 索引 Index — 本仓库文件清单 + +| 路径 | 内容 | +|---|---| +| `README.md` | 本文件 — 品牌语境、内容基础、视觉基础、图标系统、索引 | +| `colors_and_type.css` | **唯一真相来源**:色阶、语义 token(浅/深)、字体、字阶、间距、圆角、阴影、动效 | +| `flutter/` | **生产令牌 + 组件雏形**:`pangolin_theme.dart`(明/暗 ThemeData + 全量令牌)、`widgets/`(连接键/服务器行/套餐卡/按钮/状态胶囊 Dart 雏形)+ 接入说明 | +| `assets/` | Logo 各变体 + App 图标(SVG) | +| `preview/` | 设计系统卡片(色彩 / 字体 / 间距 / 组件 / 品牌)— 渲染于 Design System 标签页 | +| `ui_kits/mobile/` | 移动 App UI Kit(登录/引导/连接/节点/统计/账户中心)| +| `ui_kits/desktop/` | 桌面客户端 UI Kit(登录/引导/连接/节点/统计/账户/联系/设置)| +| `ui_kits/tablet/` | iPad 客户端 UI Kit(侧栏分栏布局,复用 mobile 原子)| +| `ui_kits/website/` | 官网宣传页 UI Kit(产品/定价/下载/文档/Blog,响应式 + 注册引导)| +| `ui_kits/usercenter/` | Web 用户中心 UI Kit(概览/订阅导入/兑换/邀请返利)| +| `SKILL.md` | Agent Skill 入口(可下载用于 Claude Code) | + +每个 `ui_kits//` 内含 `README.md`、`index.html`(可点击 demo)与若干 `*.jsx` 组件。 diff --git a/design/SKILL.md b/design/SKILL.md new file mode 100644 index 0000000..897db15 --- /dev/null +++ b/design/SKILL.md @@ -0,0 +1,49 @@ +--- +name: pangolin-design +description: Use this skill to generate well-branded interfaces and assets for 穿山甲 (Pangolin) — for production (Flutter client, Go backend, web) or throwaway prototypes/mocks. Contains the full design system (tokens, type, brand assets), four React UI kits (mobile / desktop / website / usercenter), a ready-to-run Flutter token+widget package, and a backend architecture blueprint. Invoke whenever building or reproducing any Pangolin surface. +user-invocable: true +--- + +# 穿山甲 · Pangolin — 设计与实现技能 + +这套技能让你(含 Claude Code)**完全还原**穿山甲的设计并落地为产品。UI 以 `ui_kits/` 的 React 原型为像素基准;客户端生产代码用 Flutter;后端按 `server/ARCHITECTURE.md` 蓝本实现。 + +## 第一步:必读(顺序固定) +1. **`CLAUDE.md`** — 设计铁律 13 条(§1)、套餐口径单一来源(§7)、页面清单(§5)、**实施工作步骤(§9)**。先读它,§9 就是工作流。 +2. **`README.md`** — 品牌语境、内容基础、视觉基础、图标系统、文件索引。 +3. 按需:`colors_and_type.css`(令牌真相源)、`flutter/README.md`(Flutter 接入)、`server/ARCHITECTURE.md`(后端)。 + +## 这是什么品牌 +**穿山甲 / Pangolin** — 极简、轻量、亲和的跨平台消费级**网络加速应用**(对外一律不用"VPN"等红线词,见 CLAUDE.md 铁律 13)。暖大地色(穿山甲鳞甲)+ 大量留白 + 双语单显 + 深浅双主题。核心卖点:一键连接、智能选线、即开即用。 + +## 仓库地图 +| 路径 | 内容 | 用途 | +|---|---|---| +| `CLAUDE.md` | 铁律 / 套餐口径 / 页面清单 / **工作步骤 §9** | 一切工作的总纲 | +| `colors_and_type.css` | CSS 令牌(唯一真相源) | HTML/Web 直接链入 | +| `flutter/` | Dart 令牌镜像 + widgets + pubspec + main.dart | Flutter 客户端起步包,拿来即跑 | +| `ui_kits/mobile/` | 移动 App 完整原型(登录/引导/4 Tab/账户子页/滑动切换) | 客户端像素验收标准 | +| `ui_kits/tablet/` | iPad 客户端(侧栏分栏布局,触控尺寸,复用 mobile 原子) | 同上(平板) | +| `ui_kits/desktop/` | 桌面客户端(920×600 侧栏布局 + 登录/引导) | 同上(桌面) | +| `ui_kits/website/` | 官网(产品/定价/下载/文档/Blog,响应式+i18n) | 官网迁移样板 | +| `ui_kits/usercenter/` | Web 用户中心(概览/订阅导入/兑换/邀请/设置含 2FA,移动适配) | 用户中心样板 | +| `server/ARCHITECTURE.md` | Go 控制面 + WireGuard 数据面蓝本(数据模型/API/流程/实现顺序) | 后端实现总纲 | +| `assets/` `preview/` | 品牌 SVG / 设计系统 specimen 卡 | 资产与规范预览 | + +## 工作步骤(交给 Claude Code 的执行计划) +完整版见 **CLAUDE.md §9**,摘要: + +1. **进场必读**:CLAUDE.md §1 → §7 → §5;再读 `colors_and_type.css` + 目标端的 UI Kit 源码。 +2. **Flutter 客户端**:用 `flutter/` 起步包建工程(pubspec 覆盖 → 拷 theme/widgets/main → 配字体 → `flutter run` 即出演示态);再对照 `ui_kits/mobile/` 逐屏补齐(智能选择推荐卡、免费额度卡+看广告解锁、Tab 滑动切换);先演示数据,后按 `server/ARCHITECTURE.md` §3 契约接 API。 +3. **后端(Go)**:按 `server/ARCHITECTURE.md` §7 模块顺序:openapi → auth → codes → devices → nodes → usage → 管理端。每模块:单测 + OpenAPI 同步 + 双语错误文案 + 脱敏。 +4. **官网 / 用户中心**:以 `ui_kits/website/`、`ui_kits/usercenter/` 为样板迁移到正式框架,保留 i18n 单显、响应式与脱敏文案;用户中心接 me/redeem/devices,2FA 用 TOTP。 +5. **每个界面提交前自查**(完整清单见 CLAUDE.md §9 第 4 步):语义 token 无硬编码色 / 明暗+中英四态验证 / 无红线词 / 套餐数字与 §7 一致 / Lucide 图标无 emoji / 连接键三态与原型一致 / 无 App 内支付。 + +## 铁律摘要(完整 13 条见 CLAUDE.md §1) +暖大地色调,**绝不纯黑纯白大面积填充**;主色 clay `#B96A3D`,**绝不蓝紫渐变**;圆角偏大;阴影柔和暖调;**单语言显示**(中/英切换,不并排);状态用色点+文字胶囊(无 emoji);国家用 2 字母码块(无 emoji 国旗);Lucide 细线图标;**App 内无支付**(兑换码+外部渠道:发卡店/USDT/TG/LINE/邮箱);品牌母题=行走穿山甲(拷 `assets/*.svg`,绝不重绘);不堆砌、留白即设计;**全站脱敏**(红线词清单见铁律 13)。 + +## 套餐口径(单一来源 = CLAUDE.md §7,改数字先改那里) +注册享 **7 天免费试用**(不限时长节点)→ 之后免费版 **1 个基础节点 + 每日 10 分钟 + 每日使用前看激励视频解锁**;PRO ¥25/月(年付 ¥20/月),80+ 线路,5 设备;团队版 ¥99/月 10 席位。 + +## 无指令时 +若用户只调用技能未说要做什么:问他要建/设计什么,问几个聚焦问题,作为本品牌专家输出 —— 按需产出 HTML 原型、Flutter 生产代码或后端模块。 diff --git a/design/_adherence.oxlintrc.json b/design/_adherence.oxlintrc.json new file mode 100644 index 0000000..2e92c8e --- /dev/null +++ b/design/_adherence.oxlintrc.json @@ -0,0 +1,271 @@ +{ + "plugins": [ + "react", + "import" + ], + "rules": { + "react/forbid-elements": [ + "warn", + { + "forbid": [] + } + ], + "no-restricted-imports": [ + "warn", + { + "patterns": [ + { + "group": [ + "ui_kits/desktop/**", + "ui_kits/mobile/**", + "ui_kits/tablet/**", + "ui_kits/usercenter/**", + "ui_kits/website/**" + ], + "message": "Import design-system components from 'index.js', not component internals." + } + ] + } + ], + "no-restricted-syntax": [ + "warn", + { + "selector": "Literal[value=/#[0-9a-fA-F]{3,8}\\b/]", + "message": "Raw hex color — use a design-system color token via var()." + }, + { + "selector": "Literal[value=/\\b\\d+px\\b/]", + "message": "Raw px value — use a design-system spacing token via var()." + }, + { + "selector": "Literal[value=/font-family\\s*:\\s*(?!['\\\"]?(?:Sora|Manrope|Noto Sans SC|JetBrains Mono))/i]", + "message": "Font not provided by the design system. Available: Sora, Manrope, Noto Sans SC, JetBrains Mono." + } + ] + }, + "overrides": [ + { + "files": [ + "**/index.js" + ], + "rules": { + "no-restricted-imports": "off" + } + } + ], + "x-omelette": { + "components": {}, + "tokens": [ + "--accent", + "--accent-border", + "--accent-hover", + "--accent-press", + "--accent-subtle", + "--amber-400", + "--amber-500", + "--amber-600", + "--bg", + "--bg-subtle", + "--border", + "--border-strong", + "--clay-100", + "--clay-200", + "--clay-300", + "--clay-400", + "--clay-50", + "--clay-500", + "--clay-600", + "--clay-700", + "--clay-800", + "--clay-900", + "--danger", + "--danger-subtle", + "--dur-base", + "--dur-fast", + "--dur-slow", + "--ease-in-out", + "--ease-out", + "--fg-on-accent", + "--fg1", + "--fg2", + "--fg3", + "--font-cjk", + "--font-display", + "--font-mono", + "--font-sans", + "--green-400", + "--green-500", + "--green-600", + "--leading-normal", + "--leading-relaxed", + "--leading-snug", + "--leading-tight", + "--overlay", + "--radius-2xl", + "--radius-full", + "--radius-lg", + "--radius-md", + "--radius-sm", + "--radius-xl", + "--red-400", + "--red-500", + "--red-600", + "--ring", + "--sand-100", + "--sand-200", + "--sand-300", + "--sand-400", + "--sand-50", + "--sand-500", + "--sand-600", + "--sand-700", + "--sand-800", + "--sand-900", + "--sand-950", + "--shadow-focus", + "--shadow-lg", + "--shadow-md", + "--shadow-sm", + "--shadow-xl", + "--space-0", + "--space-1", + "--space-10", + "--space-12", + "--space-16", + "--space-2", + "--space-3", + "--space-4", + "--space-5", + "--space-6", + "--space-8", + "--success", + "--success-subtle", + "--surface", + "--surface-2", + "--text-body", + "--text-body-lg", + "--text-caption", + "--text-display", + "--text-display-xl", + "--text-h1", + "--text-h2", + "--text-h3", + "--text-sm", + "--tracking-caps", + "--tracking-snug", + "--tracking-tight", + "--tracking-wide", + "--warning", + "--warning-subtle" + ], + "tokenKinds": { + "--clay-50": "color", + "--clay-100": "color", + "--clay-200": "color", + "--clay-300": "color", + "--clay-400": "color", + "--clay-500": "color", + "--clay-600": "color", + "--clay-700": "color", + "--clay-800": "color", + "--clay-900": "color", + "--sand-50": "color", + "--sand-100": "color", + "--sand-200": "color", + "--sand-300": "color", + "--sand-400": "color", + "--sand-500": "color", + "--sand-600": "color", + "--sand-700": "color", + "--sand-800": "color", + "--sand-900": "color", + "--sand-950": "color", + "--green-400": "color", + "--green-500": "color", + "--green-600": "color", + "--amber-400": "color", + "--amber-500": "color", + "--amber-600": "color", + "--red-400": "color", + "--red-500": "color", + "--red-600": "color", + "--bg": "color", + "--bg-subtle": "color", + "--surface": "color", + "--surface-2": "color", + "--overlay": "color", + "--fg1": "color", + "--fg2": "color", + "--fg3": "color", + "--fg-on-accent": "color", + "--accent": "color", + "--accent-hover": "color", + "--accent-press": "color", + "--accent-subtle": "color", + "--accent-border": "color", + "--border": "color", + "--border-strong": "color", + "--ring": "color", + "--success": "color", + "--success-subtle": "color", + "--warning": "color", + "--warning-subtle": "color", + "--danger": "color", + "--danger-subtle": "color", + "--font-display": "font", + "--font-sans": "font", + "--font-cjk": "font", + "--font-mono": "font", + "--text-display-xl": "font", + "--text-display": "font", + "--text-h1": "font", + "--text-h2": "font", + "--text-h3": "font", + "--text-body-lg": "font", + "--text-body": "font", + "--text-sm": "font", + "--text-caption": "font", + "--leading-tight": "font", + "--leading-snug": "font", + "--leading-normal": "font", + "--leading-relaxed": "font", + "--tracking-tight": "font", + "--tracking-snug": "font", + "--tracking-wide": "font", + "--tracking-caps": "font", + "--space-0": "spacing", + "--space-1": "spacing", + "--space-2": "spacing", + "--space-3": "spacing", + "--space-4": "spacing", + "--space-5": "spacing", + "--space-6": "spacing", + "--space-8": "spacing", + "--space-10": "spacing", + "--space-12": "spacing", + "--space-16": "spacing", + "--radius-sm": "radius", + "--radius-md": "radius", + "--radius-lg": "radius", + "--radius-xl": "radius", + "--radius-2xl": "radius", + "--radius-full": "radius", + "--shadow-sm": "shadow", + "--shadow-md": "shadow", + "--shadow-lg": "shadow", + "--shadow-xl": "shadow", + "--shadow-focus": "shadow", + "--ease-out": "other", + "--ease-in-out": "other", + "--dur-fast": "other", + "--dur-base": "other", + "--dur-slow": "other" + }, + "fontFamilies": [ + "JetBrains Mono", + "Manrope", + "Noto Sans SC", + "Sora" + ] + } +} \ No newline at end of file diff --git a/design/_ds_bundle.js b/design/_ds_bundle.js new file mode 100644 index 0000000..f5eb5b2 --- /dev/null +++ b/design/_ds_bundle.js @@ -0,0 +1,10643 @@ +/* @ds-bundle: {"format":3,"namespace":"VPNDesignSystem_4c9564","components":[],"sourceHashes":{"ui_kits/desktop/dapp.jsx":"0261ad8c8093","ui_kits/desktop/dflows.jsx":"b52d86fc92ea","ui_kits/desktop/dparts.jsx":"e7fec2d75fc0","ui_kits/desktop/macos-window.jsx":"83e5cb33277e","ui_kits/mobile/flows.jsx":"838173b11d20","ui_kits/mobile/ios-frame.jsx":"39f3a091d97d","ui_kits/mobile/parts.jsx":"44efd184bf6a","ui_kits/mobile/screens.jsx":"cde11b0b1ad6","ui_kits/tablet/tabapp.jsx":"86e2e0f22462","ui_kits/usercenter/ucapp.jsx":"abe00336c077","ui_kits/usercenter/ucparts.jsx":"ba322907d9b7","ui_kits/website/site.js":"47ee2f38bcef"},"inlinedExternals":[],"unexposedExports":[]} */ + +(() => { + +const __ds_ns = (window.VPNDesignSystem_4c9564 = window.VPNDesignSystem_4c9564 || {}); + +const __ds_scope = {}; + +(__ds_ns.__errors = __ds_ns.__errors || []); + +// ui_kits/desktop/dapp.jsx +try { (() => { +/* dapp.jsx — 穿山甲 VPN desktop · window + views (i18n) */ +const { + useState, + useEffect, + useRef +} = React; +function DToggle({ + on, + onChange +}) { + return /*#__PURE__*/React.createElement("div", { + onClick: onChange, + style: { + width: 42, + height: 25, + borderRadius: 999, + position: 'relative', + cursor: 'pointer', + flexShrink: 0, + background: on ? 'var(--accent)' : 'var(--border-strong)', + transition: 'background 220ms var(--ease-out)' + } + }, /*#__PURE__*/React.createElement("div", { + style: { + position: 'absolute', + top: 3, + left: on ? 20 : 3, + width: 19, + height: 19, + borderRadius: '50%', + background: '#fff', + boxShadow: 'var(--shadow-sm)', + transition: 'left 220ms var(--ease-out)' + } + })); +} +function DLangSwitch({ + lang, + setLang +}) { + return /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + background: 'var(--bg-subtle)', + borderRadius: 999, + padding: 3, + gap: 2 + } + }, [['zh', '中文'], ['en', 'EN']].map(([v, l]) => /*#__PURE__*/React.createElement("button", { + key: v, + onClick: () => setLang(v), + style: { + border: 'none', + cursor: 'pointer', + borderRadius: 999, + padding: '4px 12px', + fontFamily: 'var(--font-sans)', + fontSize: 12, + fontWeight: 700, + background: lang === v ? 'var(--accent)' : 'transparent', + color: lang === v ? 'var(--fg-on-accent)' : 'var(--fg3)', + transition: 'all 140ms var(--ease-out)' + } + }, l))); +} +function NavItem({ + icon, + label, + active, + onClick +}) { + return /*#__PURE__*/React.createElement("button", { + onClick: onClick, + style: { + display: 'flex', + alignItems: 'center', + gap: 11, + width: '100%', + textAlign: 'left', + border: 'none', + cursor: 'pointer', + background: active ? 'var(--accent-subtle)' : 'transparent', + color: active ? 'var(--accent)' : 'var(--fg2)', + padding: '9px 12px', + borderRadius: 'var(--radius-md)', + fontFamily: 'var(--font-sans)', + fontSize: 13.5, + fontWeight: active ? 600 : 500, + transition: 'background-color 140ms var(--ease-out), color 140ms var(--ease-out)' + } + }, /*#__PURE__*/React.createElement(DIcon, { + name: icon, + size: 18, + stroke: active ? 2.3 : 2, + color: active ? 'var(--accent)' : 'var(--fg3)' + }), label); +} +function DConnect({ + t, + lang, + status, + onToggle, + server, + smart, + free, + mins, + adDone, + onAd, + elapsed +}) { + const map = { + off: { + fill: 'var(--sand-100)', + fg: 'var(--clay-500)', + icon: 'power', + cap: t('capOff'), + sub: t('subOff'), + ring: 'track' + }, + connecting: { + fill: 'var(--accent)', + fg: '#fff', + icon: 'loader-circle', + cap: t('capConnecting'), + sub: t('subConnecting'), + ring: 'spin' + }, + on: { + fill: 'var(--success)', + fg: '#fff', + icon: 'shield-check', + cap: t('capOn'), + sub: t('subOn'), + ring: 'full' + } + }[status]; + const fmt = s => `${String(Math.floor(s / 3600)).padStart(2, '0')}:${String(Math.floor(s % 3600 / 60)).padStart(2, '0')}:${String(s % 60).padStart(2, '0')}`; + const shadow = status === 'off' ? 'inset 0 0 0 1px var(--sand-200), var(--shadow-md)' : `0 0 0 8px ${status === 'on' ? 'var(--success-subtle)' : 'var(--accent-subtle)'}, var(--shadow-lg)`; + return /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'center', + height: '100%', + gap: 24, + padding: '10px 40px' + } + }, /*#__PURE__*/React.createElement("button", { + onClick: onToggle, + style: { + position: 'relative', + width: 176, + height: 176, + borderRadius: '50%', + border: 'none', + cursor: 'pointer', + background: map.fill, + color: map.fg, + boxShadow: shadow, + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'center', + gap: 7, + transition: 'box-shadow 300ms var(--ease-out), background-color 300ms var(--ease-out)' + } + }, /*#__PURE__*/React.createElement("svg", { + viewBox: "0 0 176 176", + style: { + position: 'absolute', + inset: 0, + width: '100%', + height: '100%', + transformOrigin: 'center', + animation: map.ring === 'spin' ? 'pg-spin 1.4s linear infinite' : 'none' + } + }, map.ring === 'track' && /*#__PURE__*/React.createElement("circle", { + cx: "88", + cy: "88", + r: "81", + fill: "none", + stroke: "var(--sand-200)", + strokeWidth: "3", + strokeDasharray: "2 8", + strokeLinecap: "round" + }), map.ring === 'spin' && /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("circle", { + cx: "88", + cy: "88", + r: "81", + fill: "none", + stroke: "rgba(255,255,255,.3)", + strokeWidth: "4" + }), /*#__PURE__*/React.createElement("circle", { + cx: "88", + cy: "88", + r: "81", + fill: "none", + stroke: "#fff", + strokeWidth: "4", + strokeLinecap: "round", + strokeDasharray: "130 380" + })), map.ring === 'full' && /*#__PURE__*/React.createElement("circle", { + cx: "88", + cy: "88", + r: "81", + fill: "none", + stroke: "rgba(255,255,255,.85)", + strokeWidth: "4", + strokeLinecap: "round" + })), /*#__PURE__*/React.createElement(DIcon, { + name: map.icon, + size: 46, + stroke: 1.7, + style: status === 'connecting' ? { + animation: 'pg-spin 1s linear infinite' + } : {} + }), status === 'off' && /*#__PURE__*/React.createElement("span", { + style: { + fontFamily: 'var(--font-sans)', + fontWeight: 700, + fontSize: 13 + } + }, t('connectNow')), status === 'connecting' && /*#__PURE__*/React.createElement("span", { + style: { + fontFamily: 'var(--font-mono)', + fontWeight: 700, + fontSize: 13, + letterSpacing: '0.08em' + } + }, "\xB7\xB7\xB7"), status === 'on' && /*#__PURE__*/React.createElement("span", { + style: { + fontFamily: 'var(--font-mono)', + fontWeight: 600, + fontSize: 15 + } + }, fmt(elapsed))), /*#__PURE__*/React.createElement("div", { + style: { + textAlign: 'center' + } + }, /*#__PURE__*/React.createElement("div", { + style: { + fontFamily: 'var(--font-display)', + fontWeight: 700, + fontSize: 22, + color: 'var(--fg1)' + } + }, map.cap), /*#__PURE__*/React.createElement("div", { + style: { + fontSize: 13, + color: 'var(--fg3)', + marginTop: 4 + } + }, map.sub), /*#__PURE__*/React.createElement("div", { + style: { + display: 'inline-flex', + alignItems: 'center', + gap: 7, + marginTop: 10, + background: 'var(--surface)', + border: '1px solid var(--border)', + borderRadius: 'var(--radius-full)', + padding: '6px 14px', + fontSize: 12.5, + fontWeight: 600, + color: 'var(--fg2)' + } + }, smart && /*#__PURE__*/React.createElement(DIcon, { + name: "zap", + size: 13, + stroke: 2.4, + color: "var(--accent)" + }), /*#__PURE__*/React.createElement("span", { + style: { + whiteSpace: 'nowrap' + } + }, smart ? `${t('smartSelect')} · ` : '', dName(server, lang), " \xB7 ", server.ping, "ms"))), free && /*#__PURE__*/React.createElement("div", { + style: { + width: 340, + background: 'var(--surface)', + borderRadius: 'var(--radius-lg)', + padding: '12px 14px', + boxShadow: 'var(--shadow-sm)', + border: '1px solid var(--border)' + } + }, /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + alignItems: 'center', + gap: 7, + marginBottom: 8 + } + }, /*#__PURE__*/React.createElement(DIcon, { + name: "clock", + size: 14, + stroke: 2.2, + color: "var(--accent)" + }), /*#__PURE__*/React.createElement("span", { + style: { + fontSize: 12, + fontWeight: 600, + color: 'var(--fg2)' + } + }, t('quotaToday')), /*#__PURE__*/React.createElement("span", { + style: { + fontFamily: 'var(--font-mono)', + fontSize: 13.5, + fontWeight: 600, + color: 'var(--fg1)', + whiteSpace: 'nowrap' + } + }, mins, " ", t('minutes')), /*#__PURE__*/React.createElement("span", { + style: { + marginLeft: 'auto', + fontSize: 10.5, + fontWeight: 600, + color: 'var(--fg3)', + background: 'var(--bg-subtle)', + padding: '3px 9px', + borderRadius: 'var(--radius-full)', + whiteSpace: 'nowrap' + } + }, t('quotaFree'))), /*#__PURE__*/React.createElement("div", { + style: { + height: 6, + background: 'var(--bg-subtle)', + borderRadius: 3, + overflow: 'hidden', + marginBottom: 10 + } + }, /*#__PURE__*/React.createElement("div", { + style: { + width: `${Math.min(100, mins / 10 * 100)}%`, + height: '100%', + background: mins <= 3 ? 'var(--warning)' : 'var(--accent)', + borderRadius: 3 + } + })), adDone ? /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + gap: 7, + color: 'var(--success)', + fontWeight: 700, + fontSize: 13, + padding: '8px' + } + }, /*#__PURE__*/React.createElement(DIcon, { + name: "check-circle", + size: 15, + stroke: 2.2, + color: "var(--success)" + }), t('adUnlocked')) : /*#__PURE__*/React.createElement("button", { + onClick: onAd, + style: { + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + gap: 7, + width: '100%', + border: '1.5px solid var(--accent-border)', + background: 'var(--accent-subtle)', + color: 'var(--accent)', + fontFamily: 'var(--font-sans)', + fontWeight: 700, + fontSize: 13, + padding: '8px', + borderRadius: 'var(--radius-full)', + cursor: 'pointer' + } + }, /*#__PURE__*/React.createElement(DIcon, { + name: "play-circle", + size: 15, + stroke: 2.2 + }), t('watchAd'))), status === 'on' && /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + gap: 10 + } + }, [['arrow-down', '86.4', 'Mb/s'], ['arrow-up', '12.1', 'Mb/s'], ['zap', String(server.ping), 'ms']].map(([ic, v, u]) => /*#__PURE__*/React.createElement("div", { + key: ic, + style: { + display: 'flex', + alignItems: 'center', + gap: 7, + background: 'var(--surface)', + border: '1px solid var(--border)', + borderRadius: 'var(--radius-md)', + padding: '8px 14px', + boxShadow: 'var(--shadow-sm)' + } + }, /*#__PURE__*/React.createElement(DIcon, { + name: ic, + size: 15, + stroke: 2.2, + color: "var(--accent)" + }), /*#__PURE__*/React.createElement("span", { + style: { + fontFamily: 'var(--font-mono)', + fontSize: 14, + color: 'var(--fg1)' + } + }, v, /*#__PURE__*/React.createElement("span", { + style: { + fontSize: 10, + color: 'var(--fg3)' + } + }, " ", u)))))); +} +function DServers({ + t, + lang, + current, + onPick +}) { + const [q, setQ] = useState(''); + const list = DSERVERS.filter(s => (s.name + s.en).toLowerCase().includes(q.toLowerCase())); + const smartActive = current === 'AUTO'; + return /*#__PURE__*/React.createElement("div", { + style: { + padding: '4px 28px 24px' + } + }, /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + alignItems: 'center', + gap: 9, + background: 'var(--bg-subtle)', + borderRadius: 'var(--radius-md)', + padding: '9px 13px', + marginBottom: 16, + maxWidth: 340 + } + }, /*#__PURE__*/React.createElement(DIcon, { + name: "search", + size: 17, + color: "var(--fg3)" + }), /*#__PURE__*/React.createElement("input", { + value: q, + onChange: e => setQ(e.target.value), + placeholder: t('search'), + style: { + border: 'none', + background: 'transparent', + outline: 'none', + flex: 1, + fontFamily: 'var(--font-sans)', + fontSize: 13.5, + color: 'var(--fg1)' + } + })), /*#__PURE__*/React.createElement("button", { + onClick: () => onPick('AUTO'), + style: { + display: 'flex', + alignItems: 'center', + gap: 12, + width: '100%', + textAlign: 'left', + cursor: 'pointer', + background: 'var(--accent-subtle)', + border: `1.5px solid ${smartActive ? 'var(--accent)' : 'var(--accent-border)'}`, + borderRadius: 'var(--radius-xl)', + padding: '14px', + boxShadow: smartActive ? 'var(--shadow-md)' : 'var(--shadow-sm)', + marginBottom: 10 + } + }, /*#__PURE__*/React.createElement("div", { + style: { + width: 38, + height: 38, + borderRadius: 'var(--radius-md)', + background: 'linear-gradient(150deg,var(--clay-500),var(--clay-700))', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + flexShrink: 0 + } + }, /*#__PURE__*/React.createElement(DIcon, { + name: "zap", + size: 18, + stroke: 2.2, + color: "#fff" + })), /*#__PURE__*/React.createElement("div", { + style: { + flex: 1 + } + }, /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + alignItems: 'center', + gap: 8 + } + }, /*#__PURE__*/React.createElement("span", { + style: { + fontFamily: 'var(--font-sans)', + fontSize: 14.5, + fontWeight: 700, + color: 'var(--fg1)' + } + }, t('smartSelect')), /*#__PURE__*/React.createElement("span", { + style: { + fontSize: 10, + fontWeight: 700, + color: '#fff', + background: 'var(--accent)', + padding: '2px 8px', + borderRadius: 999, + letterSpacing: '0.04em' + } + }, t('recommended'))), /*#__PURE__*/React.createElement("div", { + style: { + fontSize: 11.5, + color: 'var(--fg2)', + marginTop: 3 + } + }, t('smartSub'))), smartActive && /*#__PURE__*/React.createElement(DIcon, { + name: "check", + size: 18, + color: "var(--accent)", + stroke: 2.6 + })), /*#__PURE__*/React.createElement("div", { + style: { + display: 'grid', + gridTemplateColumns: '1fr 1fr', + gap: 10 + } + }, list.map(s => { + const active = s.code === current; + return /*#__PURE__*/React.createElement("button", { + key: s.code, + onClick: () => onPick(s.code), + style: { + display: 'flex', + alignItems: 'center', + gap: 12, + textAlign: 'left', + cursor: 'pointer', + background: active ? 'var(--accent-subtle)' : 'var(--surface)', + border: `1.5px solid ${active ? 'var(--accent-border)' : 'var(--border)'}`, + borderRadius: 'var(--radius-lg)', + padding: '12px 14px', + boxShadow: 'var(--shadow-sm)' + } + }, /*#__PURE__*/React.createElement(DCC, { + code: s.code, + active: active + }), /*#__PURE__*/React.createElement("div", { + style: { + flex: 1, + minWidth: 0 + } + }, /*#__PURE__*/React.createElement("div", { + style: { + fontFamily: 'var(--font-sans)', + fontSize: 14, + fontWeight: 600, + color: 'var(--fg1)' + } + }, dName(s, lang)), /*#__PURE__*/React.createElement("div", { + style: { + fontSize: 11.5, + color: 'var(--fg3)', + whiteSpace: 'nowrap', + overflow: 'hidden', + textOverflow: 'ellipsis' + } + }, dSub(s, lang))), /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + flexDirection: 'column', + alignItems: 'flex-end', + gap: 5 + } + }, /*#__PURE__*/React.createElement("span", { + style: { + fontFamily: 'var(--font-mono)', + fontSize: 11.5, + color: 'var(--fg2)' + } + }, s.ping, "ms"), /*#__PURE__*/React.createElement(DSignal, { + ping: s.ping + })), active && /*#__PURE__*/React.createElement(DIcon, { + name: "check", + size: 17, + color: "var(--accent)" + })); + }))); +} +function DStats({ + t, + lang +}) { + const vals = [2.1, 3.4, 1.8, 4.6, 5.2, 6.1, 3.0]; + const labels = t('days'); + const max = 6.1; + return /*#__PURE__*/React.createElement("div", { + style: { + padding: '8px 32px 24px' + } + }, /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + gap: 14, + marginBottom: 22 + } + }, [[t('trafficMonth'), '42.6 GB'], [t('avgPing'), '29 ms'], [t('durMonth'), '86.4 h']].map(([l, v]) => /*#__PURE__*/React.createElement("div", { + key: l, + style: { + flex: 1, + background: 'var(--surface)', + border: '1px solid var(--border)', + borderRadius: 'var(--radius-lg)', + padding: '16px 18px', + boxShadow: 'var(--shadow-sm)' + } + }, /*#__PURE__*/React.createElement("div", { + style: { + fontSize: 12, + color: 'var(--fg3)', + fontWeight: 600 + } + }, l), /*#__PURE__*/React.createElement("div", { + style: { + fontFamily: 'var(--font-mono)', + fontSize: 24, + fontWeight: 500, + color: 'var(--fg1)', + marginTop: 6 + } + }, v)))), /*#__PURE__*/React.createElement("div", { + style: { + background: 'var(--surface)', + border: '1px solid var(--border)', + borderRadius: 'var(--radius-lg)', + padding: '18px 20px', + boxShadow: 'var(--shadow-sm)' + } + }, /*#__PURE__*/React.createElement("div", { + style: { + fontSize: 13, + fontWeight: 600, + color: 'var(--fg1)', + marginBottom: 18 + } + }, t('weekTraffic')), /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + alignItems: 'flex-end', + gap: 18, + height: 120 + } + }, vals.map((v, i) => /*#__PURE__*/React.createElement("div", { + key: i, + style: { + flex: 1, + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + gap: 8 + } + }, /*#__PURE__*/React.createElement("div", { + style: { + fontFamily: 'var(--font-mono)', + fontSize: 10, + color: 'var(--fg3)' + } + }, v), /*#__PURE__*/React.createElement("div", { + style: { + width: '100%', + maxWidth: 34, + height: `${v / max * 86}px`, + background: 'var(--accent)', + borderRadius: '6px 6px 0 0', + opacity: 0.85 + } + }), /*#__PURE__*/React.createElement("div", { + style: { + fontSize: 11, + color: 'var(--fg3)' + } + }, labels[i])))))); +} +function DSettings({ + t, + lang, + setLang, + dark, + setDark, + free, + setFree, + onReplayAuth, + onReplayOnboarding, + onRedeem +}) { + const [auto, setAuto] = useState(true); + const [kill, setKill] = useState(true); + const [boot, setBoot] = useState(true); + const card = { + background: 'var(--surface)', + border: '1px solid var(--border)', + borderRadius: 'var(--radius-lg)', + boxShadow: 'var(--shadow-sm)', + overflow: 'hidden', + marginBottom: 16 + }; + const Row = ({ + title, + sub, + right, + last, + onClick + }) => /*#__PURE__*/React.createElement("div", { + onClick: onClick, + style: { + display: 'flex', + alignItems: 'center', + gap: 12, + padding: '14px 18px', + borderBottom: last ? 'none' : '1px solid var(--border)', + cursor: onClick ? 'pointer' : 'default' + } + }, /*#__PURE__*/React.createElement("div", { + style: { + flex: 1 + } + }, /*#__PURE__*/React.createElement("div", { + style: { + fontSize: 14, + fontWeight: 500, + color: 'var(--fg1)' + } + }, title), sub && /*#__PURE__*/React.createElement("div", { + style: { + fontSize: 12, + color: 'var(--fg3)', + marginTop: 2 + } + }, sub)), right); + return /*#__PURE__*/React.createElement("div", { + style: { + padding: '8px 32px 24px', + maxWidth: 560 + } + }, /*#__PURE__*/React.createElement("div", { + style: card + }, /*#__PURE__*/React.createElement(Row, { + title: t('redeemEntry'), + sub: t('proMember'), + right: /*#__PURE__*/React.createElement(DIcon, { + name: "chevron-right", + size: 18, + color: "var(--fg3)" + }), + last: true, + onClick: onRedeem + })), /*#__PURE__*/React.createElement("div", { + style: card + }, /*#__PURE__*/React.createElement(Row, { + title: t('autostart'), + sub: t('autostartSub'), + right: /*#__PURE__*/React.createElement(DToggle, { + on: boot, + onChange: () => setBoot(!boot) + }) + }), /*#__PURE__*/React.createElement(Row, { + title: t('smartRoute'), + sub: t('smartRouteSub'), + right: /*#__PURE__*/React.createElement(DToggle, { + on: auto, + onChange: () => setAuto(!auto) + }) + }), /*#__PURE__*/React.createElement(Row, { + title: t('killSwitch'), + sub: t('killSwitchSub'), + right: /*#__PURE__*/React.createElement(DToggle, { + on: kill, + onChange: () => setKill(!kill) + }), + last: true + })), /*#__PURE__*/React.createElement("div", { + style: card + }, /*#__PURE__*/React.createElement(Row, { + title: t('language'), + right: /*#__PURE__*/React.createElement(DLangSwitch, { + lang: lang, + setLang: setLang + }) + }), /*#__PURE__*/React.createElement(Row, { + title: t('darkAppearance'), + sub: dark ? t('stateOn') : t('followLight'), + right: /*#__PURE__*/React.createElement(DToggle, { + on: dark, + onChange: () => setDark(!dark) + }) + }), /*#__PURE__*/React.createElement(Row, { + title: t('protocol'), + right: /*#__PURE__*/React.createElement("span", { + style: { + fontFamily: 'var(--font-mono)', + fontSize: 13, + color: 'var(--fg3)' + } + }, "WireGuard") + }), /*#__PURE__*/React.createElement(Row, { + title: t('checkUpdate'), + right: /*#__PURE__*/React.createElement(DIcon, { + name: "chevron-right", + size: 18, + color: "var(--fg3)" + }) + }), /*#__PURE__*/React.createElement(Row, { + title: t('version'), + right: /*#__PURE__*/React.createElement("span", { + style: { + fontFamily: 'var(--font-mono)', + fontSize: 13, + color: 'var(--fg3)' + } + }, "v2.4.0 (macOS)"), + last: true + })), /*#__PURE__*/React.createElement("div", { + style: card + }, /*#__PURE__*/React.createElement(Row, { + title: t('replayAuth'), + sub: t('replayAuthSub'), + right: /*#__PURE__*/React.createElement(DIcon, { + name: "chevron-right", + size: 18, + color: "var(--fg3)" + }), + onClick: onReplayAuth + }), /*#__PURE__*/React.createElement(Row, { + title: t('replayOnboarding'), + sub: t('replayOnboardingSub'), + right: /*#__PURE__*/React.createElement(DIcon, { + name: "chevron-right", + size: 18, + color: "var(--fg3)" + }), + onClick: onReplayOnboarding + }), /*#__PURE__*/React.createElement(Row, { + title: t('freeDemo'), + sub: t('freeDemoSub'), + right: /*#__PURE__*/React.createElement(DToggle, { + on: free, + onChange: () => setFree(!free) + }), + last: true + }))); +} +function DesktopApp() { + const [stage, setStage] = useState('app'); // app | auth | onboarding — UI kit opens on the main app + const [view, setView] = useState('connect'); + const [status, setStatus] = useState('off'); + const [code, setCode] = useState('AUTO'); // 'AUTO' = 智能选择(默认) + const [dark, setDark] = useState(false); + const [lang, setLang] = useState('zh'); + const [free, setFree] = useState(true); // 演示:免费版视角(与移动端一致) + const [mins, setMins] = useState(6); + const [adDone, setAdDone] = useState(false); + const [elapsed, setElapsed] = useState(0); + const timer = useRef(null); + const t = dT(lang); + const smart = code === 'AUTO'; + const server = smart ? DSERVERS.reduce((a, b) => a.ping < b.ping ? a : b) : DSERVERS.find(s => s.code === code); + const titles = { + connect: t('connect'), + servers: t('servers'), + stats: t('stats'), + account: t('navAccount'), + plans: t('choosePlan'), + redeem: t('redeemTitle'), + contact: t('navContact'), + settings: t('settings') + }; + function toggle() { + if (status === 'off') { + setStatus('connecting'); + setTimeout(() => { + setStatus('on'); + setElapsed(0); + }, 1500); + } else setStatus('off'); + } + function pick(c) { + setCode(c); + setView('connect'); + if (status === 'on') { + setStatus('connecting'); + setTimeout(() => { + setStatus('on'); + setElapsed(0); + }, 1200); + } + } + useEffect(() => { + if (status === 'on') timer.current = setInterval(() => setElapsed(e => e + 1), 1000); + return () => clearInterval(timer.current); + }, [status]); + if (stage === 'auth') return /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + minHeight: '100vh', + padding: 28, + boxSizing: 'border-box' + } + }, /*#__PURE__*/React.createElement(DAuthFlow, { + t: t, + lang: lang, + dark: dark, + onDone: () => setStage('onboarding') + })); + if (stage === 'onboarding') return /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + minHeight: '100vh', + padding: 28, + boxSizing: 'border-box' + } + }, /*#__PURE__*/React.createElement(DOnboarding, { + t: t, + lang: lang, + dark: dark, + onDone: () => setStage('app') + })); + let main; + if (view === 'connect') main = /*#__PURE__*/React.createElement(DConnect, { + t: t, + lang: lang, + status: status, + onToggle: toggle, + server: server, + smart: smart, + free: free, + mins: mins, + adDone: adDone, + onAd: () => setAdDone(true), + elapsed: elapsed + });else if (view === 'servers') main = /*#__PURE__*/React.createElement(DServers, { + t: t, + lang: lang, + current: code, + onPick: pick + });else if (view === 'stats') main = /*#__PURE__*/React.createElement(DStats, { + t: t, + lang: lang + });else if (view === 'account') main = /*#__PURE__*/React.createElement(DAccountView, { + t: t, + lang: lang, + free: free, + onUpgrade: () => setView('plans'), + onSignOut: () => setStage('auth') + });else if (view === 'plans') main = /*#__PURE__*/React.createElement(DPlansView, { + t: t, + lang: lang, + onChoose: () => setView('redeem') + });else if (view === 'redeem') main = /*#__PURE__*/React.createElement(DRedeemView, { + t: t, + lang: lang + });else if (view === 'contact') main = /*#__PURE__*/React.createElement(DContactView, { + t: t, + lang: lang + });else main = /*#__PURE__*/React.createElement(DSettings, { + t: t, + lang: lang, + setLang: setLang, + dark: dark, + setDark: setDark, + free: free, + setFree: setFree, + onReplayAuth: () => setStage('auth'), + onReplayOnboarding: () => setStage('onboarding'), + onRedeem: () => setView('redeem') + }); + return /*#__PURE__*/React.createElement("div", { + "data-theme": dark ? 'dark' : 'light', + style: { + width: 920, + height: 600, + borderRadius: 16, + overflow: 'hidden', + display: 'flex', + background: 'var(--bg)', + boxShadow: '0 0 0 1px rgba(0,0,0,0.12), 0 28px 70px rgba(0,0,0,0.4)', + fontFamily: 'var(--font-sans)' + } + }, /*#__PURE__*/React.createElement("div", { + style: { + width: 204, + flexShrink: 0, + background: 'var(--bg-subtle)', + borderRight: '1px solid var(--border)', + display: 'flex', + flexDirection: 'column', + padding: '14px 12px' + } + }, /*#__PURE__*/React.createElement("div", { + style: { + padding: '2px 4px 16px' + } + }, /*#__PURE__*/React.createElement(MacTrafficLights, null)), /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + alignItems: 'center', + gap: 9, + padding: '0 6px 16px' + } + }, /*#__PURE__*/React.createElement(DMark, { + size: 28 + }), /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("div", { + style: { + fontFamily: 'var(--font-display)', + fontWeight: 700, + fontSize: 16, + color: 'var(--fg1)', + lineHeight: 1 + } + }, lang === 'zh' ? '穿山甲' : 'Pangolin'), /*#__PURE__*/React.createElement("div", { + style: { + fontSize: 9, + fontWeight: 600, + letterSpacing: '0.18em', + color: 'var(--accent)', + marginTop: 3 + } + }, "PANGOLIN"))), /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + flexDirection: 'column', + gap: 3 + } + }, /*#__PURE__*/React.createElement(NavItem, { + icon: "power", + label: t('navConnect'), + active: view === 'connect', + onClick: () => setView('connect') + }), /*#__PURE__*/React.createElement(NavItem, { + icon: "globe", + label: t('navServers'), + active: view === 'servers', + onClick: () => setView('servers') + }), /*#__PURE__*/React.createElement(NavItem, { + icon: "chart-no-axes-column", + label: t('navStats'), + active: view === 'stats', + onClick: () => setView('stats') + }), /*#__PURE__*/React.createElement(NavItem, { + icon: "user", + label: t('navAccount'), + active: view === 'account' || view === 'plans' || view === 'redeem', + onClick: () => setView('account') + }), /*#__PURE__*/React.createElement(NavItem, { + icon: "message-circle", + label: t('navContact'), + active: view === 'contact', + onClick: () => setView('contact') + }), /*#__PURE__*/React.createElement(NavItem, { + icon: "settings", + label: t('navSettings'), + active: view === 'settings', + onClick: () => setView('settings') + })), /*#__PURE__*/React.createElement("div", { + style: { + flex: 1 + } + }), /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + alignItems: 'center', + gap: 10, + background: 'var(--surface)', + border: '1px solid var(--border)', + borderRadius: 'var(--radius-md)', + padding: '9px 11px' + } + }, /*#__PURE__*/React.createElement("div", { + style: { + width: 30, + height: 30, + borderRadius: '50%', + background: 'linear-gradient(150deg,var(--clay-500),var(--clay-700))', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + flexShrink: 0 + } + }, /*#__PURE__*/React.createElement(DIcon, { + name: "crown", + size: 15, + color: "#fff" + })), /*#__PURE__*/React.createElement("div", { + style: { + minWidth: 0 + } + }, /*#__PURE__*/React.createElement("div", { + style: { + fontSize: 12, + fontWeight: 600, + color: 'var(--fg1)', + whiteSpace: 'nowrap', + overflow: 'hidden', + textOverflow: 'ellipsis' + } + }, free ? t('freePlanName') : t('proMember')), /*#__PURE__*/React.createElement("div", { + style: { + fontSize: 10.5, + color: 'var(--fg3)', + whiteSpace: 'nowrap', + overflow: 'hidden', + textOverflow: 'ellipsis' + } + }, free ? t('quotaFree') : t('until'))))), /*#__PURE__*/React.createElement("div", { + style: { + flex: 1, + display: 'flex', + flexDirection: 'column', + minWidth: 0 + } + }, /*#__PURE__*/React.createElement("div", { + style: { + height: 52, + flexShrink: 0, + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + padding: '0 24px', + borderBottom: '1px solid var(--border)' + } + }, /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + alignItems: 'center', + gap: 10 + } + }, (view === 'redeem' || view === 'plans') && /*#__PURE__*/React.createElement("button", { + onClick: () => setView('account'), + style: { + border: 'none', + background: 'transparent', + cursor: 'pointer', + padding: 4, + display: 'flex' + } + }, /*#__PURE__*/React.createElement(DIcon, { + name: "arrow-left", + size: 20, + color: "var(--fg1)" + })), /*#__PURE__*/React.createElement("div", { + style: { + fontFamily: 'var(--font-display)', + fontWeight: 700, + fontSize: 17, + color: 'var(--fg1)' + } + }, titles[view])), /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + alignItems: 'center', + gap: 12 + } + }, /*#__PURE__*/React.createElement("span", { + style: { + fontFamily: 'var(--font-mono)', + fontSize: 12, + fontWeight: 600, + whiteSpace: 'nowrap', + color: status === 'on' ? 'var(--success)' : 'var(--fg3)' + } + }, status === 'on' ? '● ' + t('connected') + ' ' + server.code : '○ ' + t('notConnected')), /*#__PURE__*/React.createElement("button", { + onClick: () => setDark(!dark), + style: { + border: 'none', + background: 'transparent', + cursor: 'pointer', + padding: 6, + borderRadius: 'var(--radius-sm)', + display: 'flex' + } + }, /*#__PURE__*/React.createElement(DIcon, { + name: dark ? 'sun' : 'moon', + size: 18, + color: "var(--fg2)" + })))), /*#__PURE__*/React.createElement("div", { + style: { + flex: 1, + overflow: 'auto', + minHeight: 0 + } + }, main))); +} +Object.assign(window, { + DesktopApp +}); +})(); } catch (e) { __ds_ns.__errors.push({ path: "ui_kits/desktop/dapp.jsx", error: String((e && e.message) || e) }); } + +// ui_kits/desktop/dflows.jsx +try { (() => { +/* dflows.jsx — 穿山甲 VPN desktop · auth + account management (integrated dashboard) + redeem */ +const { + useState: useStateD +} = React; +const dCard = { + background: 'var(--surface)', + border: '1px solid var(--border)', + borderRadius: 'var(--radius-xl)', + boxShadow: 'var(--shadow-sm)' +}; +const dInput = { + border: '1.5px solid var(--border-strong)', + borderRadius: 'var(--radius-md)', + padding: '11px 13px', + fontFamily: 'var(--font-sans)', + fontSize: 14, + color: 'var(--fg1)', + background: 'var(--bg)', + outline: 'none', + width: '100%', + boxSizing: 'border-box' +}; +function DField({ + label, + children +}) { + return /*#__PURE__*/React.createElement("label", { + style: { + display: 'block' + } + }, /*#__PURE__*/React.createElement("span", { + style: { + display: 'block', + fontSize: 12.5, + fontWeight: 600, + color: 'var(--fg2)', + marginBottom: 6 + } + }, label), children); +} + +/* ───────── AUTH — split panel, brand left / form right ───────── */ +function DAuthFlow({ + t, + lang, + dark, + onDone +}) { + const [mode, setMode] = useStateD('login'); + const [step, setStep] = useStateD(0); + const [email, setEmail] = useStateD(''); + const [code, setCode] = useStateD(''); + const [pw, setPw] = useStateD(''); + const [sent, setSent] = useStateD(false); + const [totp, setTotp] = useStateD(false); // 2FA step after login (PRO) + const [otp, setOtp] = useStateD(''); + const valid = /\S+@\S+\.\S+/.test(email); + const tab = (id, label) => /*#__PURE__*/React.createElement("button", { + onClick: () => { + setMode(id); + setStep(0); + setSent(false); + }, + style: { + flex: 1, + border: 'none', + cursor: 'pointer', + background: 'transparent', + padding: '10px 0', + fontFamily: 'var(--font-sans)', + fontSize: 15, + fontWeight: mode === id ? 700 : 500, + color: mode === id ? 'var(--fg1)' : 'var(--fg3)', + borderBottom: mode === id ? '2px solid var(--accent)' : '2px solid transparent' + } + }, label); + const primary = (label, onClick, enabled) => /*#__PURE__*/React.createElement("button", { + onClick: enabled ? onClick : undefined, + disabled: !enabled, + style: { + width: '100%', + border: 'none', + borderRadius: 'var(--radius-full)', + padding: '13px', + fontFamily: 'var(--font-sans)', + fontWeight: 700, + fontSize: 15, + cursor: enabled ? 'pointer' : 'not-allowed', + background: 'var(--accent)', + color: 'var(--fg-on-accent)', + opacity: enabled ? 1 : 0.45, + marginTop: 4 + } + }, label); + return /*#__PURE__*/React.createElement("div", { + "data-theme": dark ? 'dark' : 'light', + style: { + width: 880, + height: 560, + borderRadius: 16, + overflow: 'hidden', + display: 'flex', + boxShadow: '0 0 0 1px rgba(0,0,0,0.12), 0 28px 70px rgba(0,0,0,0.4)', + fontFamily: 'var(--font-sans)' + } + }, /*#__PURE__*/React.createElement("div", { + style: { + width: 340, + flexShrink: 0, + background: 'linear-gradient(165deg,var(--clay-500),var(--clay-800))', + color: '#fff', + padding: '48px 40px', + display: 'flex', + flexDirection: 'column' + } + }, /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + alignItems: 'center', + gap: 11 + } + }, /*#__PURE__*/React.createElement(DMark, { + size: 34, + color: "#fff" + }), /*#__PURE__*/React.createElement("div", { + style: { + fontFamily: 'var(--font-display)', + fontWeight: 700, + fontSize: 20 + } + }, lang === 'zh' ? '穿山甲' : 'Pangolin')), /*#__PURE__*/React.createElement("div", { + style: { + flex: 1, + display: 'flex', + flexDirection: 'column', + justifyContent: 'center' + } + }, /*#__PURE__*/React.createElement("div", { + style: { + fontFamily: 'var(--font-display)', + fontWeight: 700, + fontSize: 28, + lineHeight: 1.3 + } + }, t('authSidePitch'))), /*#__PURE__*/React.createElement("div", { + style: { + fontSize: 12, + opacity: .8, + letterSpacing: '0.04em' + } + }, t('authTagline'))), /*#__PURE__*/React.createElement("div", { + style: { + flex: 1, + background: 'var(--bg)', + padding: '44px 46px', + display: 'flex', + flexDirection: 'column' + } + }, /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + borderBottom: '1px solid var(--border)', + marginBottom: 26 + } + }, tab('login', t('tabLogin')), tab('register', t('tabRegister'))), /*#__PURE__*/React.createElement("div", { + style: { + flex: 1, + display: 'flex', + flexDirection: 'column', + gap: 16 + } + }, mode === 'login' && !totp && /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(DField, { + label: t('emailLabel') + }, /*#__PURE__*/React.createElement("input", { + style: dInput, + value: email, + onChange: e => setEmail(e.target.value), + placeholder: t('emailPh') + })), /*#__PURE__*/React.createElement(DField, { + label: t('pwLabel') + }, /*#__PURE__*/React.createElement("input", { + style: dInput, + type: "password", + value: pw, + onChange: e => setPw(e.target.value), + placeholder: t('pwPh') + })), /*#__PURE__*/React.createElement("div", { + style: { + textAlign: 'right', + marginTop: -6 + } + }, /*#__PURE__*/React.createElement("span", { + style: { + fontSize: 13, + color: 'var(--accent)', + fontWeight: 600, + cursor: 'pointer' + } + }, t('forgotPw'))), primary(t('doLogin'), () => setTotp(true), valid && pw.length > 0)), mode === 'login' && totp && /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + alignItems: 'center', + gap: 8 + } + }, /*#__PURE__*/React.createElement(DIcon, { + name: "shield-check", + size: 18, + color: "var(--accent)" + }), /*#__PURE__*/React.createElement("span", { + style: { + fontFamily: 'var(--font-display)', + fontWeight: 700, + fontSize: 17, + color: 'var(--fg1)' + } + }, t('twoFATitle')), /*#__PURE__*/React.createElement("span", { + style: { + fontSize: 10, + fontWeight: 700, + color: 'var(--accent)', + background: 'var(--accent-subtle)', + border: '1px solid var(--accent-border)', + padding: '2px 8px', + borderRadius: 999 + } + }, "PRO")), /*#__PURE__*/React.createElement("div", { + style: { + fontSize: 12.5, + color: 'var(--fg3)', + lineHeight: 1.6 + } + }, t('twoFAHint')), /*#__PURE__*/React.createElement("input", { + value: otp, + onChange: e => setOtp(e.target.value.replace(/\D/g, '').slice(0, 6)), + placeholder: "\xB7\xB7\xB7\xB7\xB7\xB7", + style: { + ...dInput, + fontFamily: 'var(--font-mono)', + fontSize: 20, + letterSpacing: '0.5em', + textAlign: 'center' + } + }), primary(t('twoFAConfirm'), onDone, otp.length === 6), /*#__PURE__*/React.createElement("div", { + onClick: () => setTotp(false), + style: { + textAlign: 'center', + fontSize: 12.5, + color: 'var(--fg3)', + cursor: 'pointer' + } + }, t('twoFABack'))), mode === 'register' && step === 0 && /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(DField, { + label: t('emailLabel') + }, /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + gap: 8 + } + }, /*#__PURE__*/React.createElement("input", { + style: dInput, + value: email, + onChange: e => setEmail(e.target.value), + placeholder: t('emailPh') + }), /*#__PURE__*/React.createElement("button", { + onClick: () => valid && setSent(true), + style: { + border: '1.5px solid var(--border-strong)', + background: 'transparent', + borderRadius: 'var(--radius-md)', + padding: '0 16px', + cursor: valid ? 'pointer' : 'default', + color: valid ? 'var(--accent)' : 'var(--fg3)', + fontWeight: 700, + fontSize: 13, + whiteSpace: 'nowrap', + flexShrink: 0 + } + }, sent ? t('resend') : t('sendCode')))), sent && /*#__PURE__*/React.createElement("div", { + style: { + fontSize: 12, + color: 'var(--success)', + marginTop: -8, + display: 'flex', + alignItems: 'center', + gap: 6 + } + }, /*#__PURE__*/React.createElement(DIcon, { + name: "check-circle", + size: 14, + color: "var(--success)" + }), t('codeSentTo'), " ", email), /*#__PURE__*/React.createElement(DField, { + label: t('codeLabel') + }, /*#__PURE__*/React.createElement("input", { + style: { + ...dInput, + fontFamily: 'var(--font-mono)', + letterSpacing: '0.3em' + }, + value: code, + onChange: e => setCode(e.target.value.replace(/\D/g, '').slice(0, 6)), + placeholder: "\xB7\xB7\xB7\xB7\xB7\xB7" + })), primary(t('doNext'), () => setStep(1), sent && code.length === 6)), mode === 'register' && step === 1 && /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("div", { + style: { + fontSize: 13, + color: 'var(--fg2)', + display: 'flex', + alignItems: 'center', + gap: 7 + } + }, /*#__PURE__*/React.createElement(DIcon, { + name: "check-circle", + size: 15, + color: "var(--success)" + }), email), /*#__PURE__*/React.createElement(DField, { + label: t('pwLabel') + }, /*#__PURE__*/React.createElement("input", { + style: dInput, + type: "password", + value: pw, + onChange: e => setPw(e.target.value), + placeholder: t('setPwPh') + })), primary(t('doCreate'), onDone, pw.length >= 6))), /*#__PURE__*/React.createElement("div", { + style: { + fontSize: 11, + color: 'var(--fg3)', + textAlign: 'center', + lineHeight: 1.5, + paddingTop: 18 + } + }, t('tos')))); +} + +/* ───────── ACCOUNT (integrated: plan + account info + devices) ───────── */ +function DAccountView({ + t, + lang, + free, + onUpgrade, + onSignOut +}) { + const [twoFA, setTwoFA] = useStateD(true); + const [devices, setDevices] = useStateD([{ + id: 1, + icon: 'laptop', + name: 'MacBook Pro', + os: 'macOS 26', + active: t('thisDevice'), + me: true + }, { + id: 2, + icon: 'smartphone', + name: 'iPhone 17', + os: 'iOS 26', + active: '2 min', + me: false + }, { + id: 3, + icon: 'monitor-smartphone', + name: 'iPad Air', + os: 'iPadOS 26', + active: lang === 'zh' ? '3 小时前' : '3h ago', + me: false + }]); + const infoRow = (label, value, action, last) => /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + alignItems: 'center', + gap: 12, + padding: '14px 18px', + borderBottom: last ? 'none' : '1px solid var(--border)' + } + }, /*#__PURE__*/React.createElement("div", { + style: { + width: 96, + fontSize: 13, + fontWeight: 600, + color: 'var(--fg2)' + } + }, label), /*#__PURE__*/React.createElement("div", { + style: { + flex: 1, + fontSize: 14, + color: 'var(--fg1)' + } + }, value), action); + const smallBtn = { + border: '1.5px solid var(--border-strong)', + background: 'transparent', + color: 'var(--fg2)', + fontSize: 13, + fontWeight: 600, + padding: '7px 14px', + borderRadius: 'var(--radius-full)', + cursor: 'pointer' + }; + return /*#__PURE__*/React.createElement("div", { + style: { + padding: '8px 32px 28px', + display: 'flex', + flexDirection: 'column', + gap: 22, + maxWidth: 760 + } + }, /*#__PURE__*/React.createElement("div", { + style: { + ...dCard, + background: 'linear-gradient(150deg,var(--clay-600),var(--clay-800))', + border: 'none', + color: '#fff', + padding: '20px 24px', + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between' + } + }, /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + alignItems: 'center', + gap: 15 + } + }, /*#__PURE__*/React.createElement("div", { + style: { + width: 48, + height: 48, + borderRadius: '50%', + background: 'rgba(255,255,255,.18)', + display: 'flex', + alignItems: 'center', + justifyContent: 'center' + } + }, /*#__PURE__*/React.createElement(DIcon, { + name: "crown", + size: 24, + color: "#fff" + })), /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("div", { + style: { + fontSize: 12.5, + opacity: .8 + } + }, t('curPlan')), /*#__PURE__*/React.createElement("div", { + style: { + fontFamily: 'var(--font-display)', + fontSize: 20, + fontWeight: 700, + marginTop: 2 + } + }, free ? t('freePlanName') : t('proMember')), /*#__PURE__*/React.createElement("div", { + style: { + fontSize: 11.5, + opacity: .8, + marginTop: 3, + fontFamily: 'var(--font-mono)', + whiteSpace: 'nowrap' + } + }, free ? t('quotaFree') : `${t('expires')} 2026-12-31 · ${t('accUsage')} 42.6 GB`))), /*#__PURE__*/React.createElement("button", { + onClick: onUpgrade, + style: { + border: 'none', + background: '#fff', + color: 'var(--clay-700)', + fontWeight: 700, + fontSize: 14, + padding: '11px 20px', + borderRadius: 'var(--radius-full)', + cursor: 'pointer', + display: 'inline-flex', + alignItems: 'center', + gap: 8 + } + }, /*#__PURE__*/React.createElement(DIcon, { + name: "ticket", + size: 16, + color: "var(--clay-700)" + }), t('renew'))), /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("div", { + style: { + fontFamily: 'var(--font-display)', + fontSize: 17, + fontWeight: 700, + color: 'var(--fg1)', + marginBottom: 14 + } + }, t('accInfoTitle')), /*#__PURE__*/React.createElement("div", { + style: { + ...dCard, + overflow: 'hidden' + } + }, infoRow(t('accEmail'), 'me@pangolin.vpn', /*#__PURE__*/React.createElement("button", { + style: smallBtn + }, t('accChange'))), infoRow(t('accPassword'), '••••••••••', /*#__PURE__*/React.createElement("button", { + style: smallBtn + }, t('accChange'))), infoRow(t('twoFA'), t('twoFASub'), /*#__PURE__*/React.createElement(DToggle, { + on: twoFA, + onChange: () => setTwoFA(!twoFA) + })), infoRow(t('ucEntry'), t('ucEntrySub'), /*#__PURE__*/React.createElement("button", { + onClick: () => window.open('../usercenter/index.html', '_blank'), + style: { + ...smallBtn, + display: 'inline-flex', + alignItems: 'center', + gap: 6 + } + }, /*#__PURE__*/React.createElement(DIcon, { + name: "external-link", + size: 14, + color: "var(--fg2)" + }), t('ucOpen'))), /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + alignItems: 'center', + gap: 12, + padding: '14px 18px' + } + }, /*#__PURE__*/React.createElement("div", { + style: { + width: 96, + fontSize: 13, + fontWeight: 600, + color: 'var(--danger)' + } + }, t('accSignOut')), /*#__PURE__*/React.createElement("div", { + style: { + flex: 1 + } + }), /*#__PURE__*/React.createElement("button", { + onClick: onSignOut, + style: { + ...smallBtn, + color: 'var(--danger)', + borderColor: 'var(--danger-subtle)', + background: 'var(--danger-subtle)', + display: 'inline-flex', + alignItems: 'center', + gap: 7 + } + }, /*#__PURE__*/React.createElement(DIcon, { + name: "log-out", + size: 15, + color: "var(--danger)" + }), t('accSignOut'))))), /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("div", { + style: { + fontFamily: 'var(--font-display)', + fontSize: 17, + fontWeight: 700, + color: 'var(--fg1)' + } + }, t('myDevices')), /*#__PURE__*/React.createElement("div", { + style: { + fontSize: 12.5, + color: 'var(--fg3)', + margin: '3px 0 14px' + } + }, t('devicesSub')), /*#__PURE__*/React.createElement("div", { + style: { + ...dCard, + overflow: 'hidden' + } + }, devices.map((d, i) => /*#__PURE__*/React.createElement("div", { + key: d.id, + style: { + display: 'flex', + alignItems: 'center', + gap: 14, + padding: '14px 18px', + borderBottom: i < devices.length - 1 ? '1px solid var(--border)' : 'none' + } + }, /*#__PURE__*/React.createElement("div", { + style: { + width: 40, + height: 40, + borderRadius: 'var(--radius-md)', + background: 'var(--accent-subtle)', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + flexShrink: 0 + } + }, /*#__PURE__*/React.createElement(DIcon, { + name: d.icon, + size: 20, + color: "var(--accent)" + })), /*#__PURE__*/React.createElement("div", { + style: { + flex: 1 + } + }, /*#__PURE__*/React.createElement("div", { + style: { + fontSize: 14.5, + fontWeight: 600, + color: 'var(--fg1)', + display: 'flex', + alignItems: 'center', + gap: 8 + } + }, d.name, d.me && /*#__PURE__*/React.createElement("span", { + style: { + fontSize: 11, + fontWeight: 600, + color: 'var(--success)', + background: 'var(--success-subtle)', + padding: '2px 8px', + borderRadius: 999 + } + }, d.active)), /*#__PURE__*/React.createElement("div", { + style: { + fontSize: 12, + color: 'var(--fg3)', + marginTop: 2 + } + }, d.os, !d.me && ` · ${t('lastActive')} ${d.active}`)), !d.me && /*#__PURE__*/React.createElement("button", { + onClick: () => setDevices(devices.filter(x => x.id !== d.id)), + style: { + border: '1.5px solid var(--border-strong)', + background: 'transparent', + color: 'var(--fg2)', + fontSize: 13, + fontWeight: 600, + padding: '7px 14px', + borderRadius: 'var(--radius-full)', + cursor: 'pointer' + } + }, t('remove'))))))); +} + +/* ───────── PLANS (shown on renew / upgrade) ───────── */ +function DPlansView({ + t, + lang, + onChoose +}) { + const plans = [{ + id: 'free', + name: t('free'), + price: '¥0', + feats: t('feFree'), + cta: t('current'), + variant: 'cur' + }, { + id: 'pro', + name: t('proPlan'), + price: '¥25', + feats: t('fePro'), + cta: t('upgrade'), + variant: 'pro' + }, { + id: 'team', + name: t('team'), + price: '¥99', + feats: t('feTeam'), + cta: t('choose'), + variant: 'plain' + }]; + return /*#__PURE__*/React.createElement("div", { + style: { + padding: '8px 32px 28px' + } + }, /*#__PURE__*/React.createElement("div", { + style: { + display: 'grid', + gridTemplateColumns: '1fr 1fr 1fr', + gap: 16, + alignItems: 'start', + maxWidth: 760 + } + }, plans.map(p => { + const isPro = p.variant === 'pro'; + return /*#__PURE__*/React.createElement("div", { + key: p.id, + style: { + ...dCard, + position: 'relative', + padding: '22px 20px', + ...(isPro ? { + background: 'linear-gradient(160deg,var(--clay-600),var(--clay-800))', + border: 'none', + color: '#fff', + boxShadow: 'var(--shadow-md)' + } : {}) + } + }, isPro && /*#__PURE__*/React.createElement("div", { + style: { + position: 'absolute', + top: -10, + left: '50%', + transform: 'translateX(-50%)', + background: '#fff', + color: 'var(--clay-700)', + fontSize: 11, + fontWeight: 700, + padding: '3px 11px', + borderRadius: 999, + boxShadow: 'var(--shadow-sm)' + } + }, t('mostPopular')), /*#__PURE__*/React.createElement("div", { + style: { + fontSize: 13.5, + fontWeight: 600, + color: isPro ? '#fff' : 'var(--fg2)', + opacity: isPro ? .85 : 1 + } + }, p.name), /*#__PURE__*/React.createElement("div", { + style: { + fontFamily: 'var(--font-display)', + fontSize: 30, + fontWeight: 700, + marginTop: 6, + color: isPro ? '#fff' : 'var(--fg1)' + } + }, p.price, /*#__PURE__*/React.createElement("span", { + style: { + fontSize: 13, + fontWeight: 500, + opacity: .7 + } + }, t('perMonth'))), /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + flexDirection: 'column', + gap: 9, + margin: '16px 0 18px' + } + }, p.feats.map((f, i) => /*#__PURE__*/React.createElement("div", { + key: i, + style: { + display: 'flex', + alignItems: 'center', + gap: 9, + fontSize: 13, + color: isPro ? 'rgba(255,255,255,.92)' : 'var(--fg2)' + } + }, /*#__PURE__*/React.createElement(DIcon, { + name: "check", + size: 15, + color: isPro ? '#fff' : 'var(--success)', + stroke: 2.4 + }), f))), /*#__PURE__*/React.createElement("button", { + onClick: p.variant !== 'cur' ? onChoose : undefined, + style: { + width: '100%', + border: p.variant === 'cur' ? '1.5px solid var(--border-strong)' : 'none', + cursor: p.variant === 'cur' ? 'default' : 'pointer', + borderRadius: 'var(--radius-full)', + padding: '11px', + fontWeight: 600, + fontSize: 14, + background: isPro ? '#fff' : p.variant === 'cur' ? 'transparent' : 'var(--accent)', + color: isPro ? 'var(--clay-700)' : p.variant === 'cur' ? 'var(--fg2)' : '#fff' + } + }, p.cta)); + }))); +} + +/* ───────── REDEEM & BUY ───────── */ +function DRedeemView({ + t, + lang +}) { + const [code, setCode] = useStateD(''); + const [ok, setOk] = useStateD(false); + const channels = [{ + icon: 'shopping-bag', + name: t('chStore'), + sub: t('chStoreSub'), + accent: true + }, { + icon: 'credit-card', + name: 'USDT (TRC20)', + sub: t('chUsdtSub'), + mono: false + }, { + icon: 'send', + name: 'Telegram', + sub: t('chTgSub'), + mono: true + }, { + icon: 'message-circle', + name: 'LINE', + sub: t('chLineSub'), + mono: true + }, { + icon: 'mail', + name: t('chEmail'), + sub: t('chEmailSub'), + mono: true + }]; + return /*#__PURE__*/React.createElement("div", { + style: { + padding: '8px 32px 28px', + maxWidth: 680 + } + }, /*#__PURE__*/React.createElement("div", { + style: { + ...dCard, + padding: '20px 22px', + marginBottom: 24 + } + }, /*#__PURE__*/React.createElement("div", { + style: { + fontSize: 14.5, + fontWeight: 700, + color: 'var(--fg1)', + marginBottom: 12 + } + }, t('redeemCodeTitle')), ok ? /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + alignItems: 'center', + gap: 9, + color: 'var(--success)', + fontWeight: 600, + fontSize: 15, + padding: '6px 0' + } + }, /*#__PURE__*/React.createElement(DIcon, { + name: "check-circle", + size: 20, + color: "var(--success)" + }), t('redeemOk')) : /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + gap: 10 + } + }, /*#__PURE__*/React.createElement("input", { + value: code, + onChange: e => setCode(e.target.value.toUpperCase()), + placeholder: t('redeemPh'), + style: { + ...dInput, + fontFamily: 'var(--font-mono)', + letterSpacing: '0.08em' + } + }), /*#__PURE__*/React.createElement("button", { + onClick: () => code.length >= 4 && setOk(true), + style: { + border: 'none', + borderRadius: 'var(--radius-md)', + padding: '0 22px', + background: 'var(--accent)', + color: '#fff', + fontWeight: 700, + fontSize: 14, + cursor: 'pointer', + flexShrink: 0 + } + }, t('redeemBtn')))), /*#__PURE__*/React.createElement("div", { + style: { + fontSize: 14.5, + fontWeight: 700, + color: 'var(--fg1)', + marginBottom: 6 + } + }, t('buyTitle')), /*#__PURE__*/React.createElement("div", { + style: { + fontSize: 12.5, + color: 'var(--fg3)', + lineHeight: 1.5, + marginBottom: 14 + } + }, t('buySub')), /*#__PURE__*/React.createElement("div", { + style: { + display: 'grid', + gridTemplateColumns: '1fr 1fr', + gap: 12 + } + }, channels.map(c => /*#__PURE__*/React.createElement("button", { + key: c.name, + style: { + display: 'flex', + alignItems: 'center', + gap: 13, + textAlign: 'left', + cursor: 'pointer', + background: c.accent ? 'var(--accent-subtle)' : 'var(--surface)', + border: `1px solid ${c.accent ? 'var(--accent-border)' : 'var(--border)'}`, + borderRadius: 'var(--radius-lg)', + padding: '14px 16px', + boxShadow: 'var(--shadow-sm)' + } + }, /*#__PURE__*/React.createElement("div", { + style: { + width: 40, + height: 40, + borderRadius: 'var(--radius-md)', + background: c.accent ? 'var(--accent)' : 'var(--bg-subtle)', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + flexShrink: 0 + } + }, /*#__PURE__*/React.createElement(DIcon, { + name: c.icon, + size: 20, + color: c.accent ? '#fff' : 'var(--accent)' + })), /*#__PURE__*/React.createElement("div", { + style: { + flex: 1, + minWidth: 0 + } + }, /*#__PURE__*/React.createElement("div", { + style: { + fontSize: 14.5, + fontWeight: 600, + color: 'var(--fg1)' + } + }, c.name), /*#__PURE__*/React.createElement("div", { + style: { + fontSize: 12, + color: 'var(--fg3)', + fontFamily: c.mono ? 'var(--font-mono)' : 'var(--font-sans)', + whiteSpace: 'nowrap', + overflow: 'hidden', + textOverflow: 'ellipsis' + } + }, c.sub)), /*#__PURE__*/React.createElement(DIcon, { + name: "external-link", + size: 16, + color: "var(--fg3)" + }))))); +} + +/* ───────── ONBOARDING — window card, 3 screens ───────── */ +function DOnboarding({ + t, + lang, + dark, + onDone +}) { + const [i, setI] = useStateD(0); + const steps = [{ + icon: 'globe', + title: t('ob1Title'), + sub: t('ob1Sub'), + cta: t('obNext') + }, { + icon: 'lock', + title: t('ob2Title'), + sub: t('ob2Sub'), + cta: t('obAllow') + }, { + icon: 'shield-check', + title: t('ob3Title'), + sub: t('ob3Sub'), + cta: t('obStart') + }]; + const s = steps[i]; + const last = i === steps.length - 1; + return /*#__PURE__*/React.createElement("div", { + "data-theme": dark ? 'dark' : 'light', + style: { + width: 560, + height: 520, + borderRadius: 16, + overflow: 'hidden', + display: 'flex', + flexDirection: 'column', + background: 'var(--bg)', + boxShadow: '0 0 0 1px rgba(0,0,0,0.12), 0 28px 70px rgba(0,0,0,0.4)', + fontFamily: 'var(--font-sans)' + } + }, /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + padding: '18px 22px' + } + }, /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + alignItems: 'center', + gap: 9 + } + }, /*#__PURE__*/React.createElement(DMark, { + size: 24 + }), /*#__PURE__*/React.createElement("span", { + style: { + fontFamily: 'var(--font-display)', + fontWeight: 700, + fontSize: 15, + color: 'var(--fg1)' + } + }, lang === 'zh' ? '穿山甲' : 'Pangolin')), !last && /*#__PURE__*/React.createElement("button", { + onClick: onDone, + style: { + border: 'none', + background: 'transparent', + cursor: 'pointer', + fontSize: 13, + fontWeight: 600, + color: 'var(--fg3)' + } + }, t('obSkip'))), /*#__PURE__*/React.createElement("div", { + style: { + flex: 1, + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'center', + textAlign: 'center', + padding: '0 56px', + gap: 22 + } + }, /*#__PURE__*/React.createElement("div", { + style: { + width: 108, + height: 108, + borderRadius: '50%', + background: 'var(--accent-subtle)', + display: 'flex', + alignItems: 'center', + justifyContent: 'center' + } + }, /*#__PURE__*/React.createElement(DIcon, { + name: s.icon, + size: 52, + stroke: 1.6, + color: "var(--accent)" + })), /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("div", { + style: { + fontFamily: 'var(--font-display)', + fontSize: 26, + fontWeight: 700, + color: 'var(--fg1)' + } + }, s.title), /*#__PURE__*/React.createElement("div", { + style: { + fontSize: 15, + color: 'var(--fg2)', + lineHeight: 1.6, + marginTop: 10, + maxWidth: 380 + } + }, s.sub))), /*#__PURE__*/React.createElement("div", { + style: { + padding: '0 40px 36px', + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + gap: 20 + } + }, /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + gap: 8 + } + }, steps.map((_, k) => /*#__PURE__*/React.createElement("div", { + key: k, + style: { + width: k === i ? 22 : 8, + height: 8, + borderRadius: 999, + background: k === i ? 'var(--accent)' : 'var(--border-strong)', + transition: 'all 220ms var(--ease-out)' + } + }))), /*#__PURE__*/React.createElement("button", { + onClick: () => last ? onDone() : setI(i + 1), + style: { + width: '100%', + maxWidth: 360, + border: 'none', + borderRadius: 'var(--radius-full)', + padding: '13px', + fontFamily: 'var(--font-sans)', + fontWeight: 700, + fontSize: 15, + cursor: 'pointer', + background: 'var(--accent)', + color: 'var(--fg-on-accent)' + } + }, s.cta))); +} + +/* ───────── CONTACT US ───────── */ +function DContactView({ + t, + lang +}) { + const channels = [{ + icon: 'send', + name: t('contactTg'), + sub: '@PangolinVPN_bot', + accent: true + }, { + icon: 'message-circle', + name: t('contactLine'), + sub: '@pangolinvpn' + }, { + icon: 'mail', + name: t('contactEmail'), + sub: 'support@pangolin.vpn' + }, { + icon: 'shopping-bag', + name: t('contactStore'), + sub: 'shop.pangolin.vpn' + }]; + return /*#__PURE__*/React.createElement("div", { + style: { + padding: '8px 32px 28px', + maxWidth: 680 + } + }, /*#__PURE__*/React.createElement("div", { + style: { + fontSize: 13.5, + color: 'var(--fg2)', + lineHeight: 1.6, + marginBottom: 18 + } + }, t('contactIntro')), /*#__PURE__*/React.createElement("div", { + style: { + display: 'grid', + gridTemplateColumns: '1fr 1fr', + gap: 12, + marginBottom: 22 + } + }, channels.map(c => /*#__PURE__*/React.createElement("button", { + key: c.name, + style: { + display: 'flex', + alignItems: 'center', + gap: 13, + textAlign: 'left', + cursor: 'pointer', + background: c.accent ? 'var(--accent-subtle)' : 'var(--surface)', + border: `1px solid ${c.accent ? 'var(--accent-border)' : 'var(--border)'}`, + borderRadius: 'var(--radius-lg)', + padding: '14px 16px', + boxShadow: 'var(--shadow-sm)' + } + }, /*#__PURE__*/React.createElement("div", { + style: { + width: 40, + height: 40, + borderRadius: 'var(--radius-md)', + background: c.accent ? 'var(--accent)' : 'var(--bg-subtle)', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + flexShrink: 0 + } + }, /*#__PURE__*/React.createElement(DIcon, { + name: c.icon, + size: 20, + color: c.accent ? '#fff' : 'var(--accent)' + })), /*#__PURE__*/React.createElement("div", { + style: { + flex: 1, + minWidth: 0 + } + }, /*#__PURE__*/React.createElement("div", { + style: { + fontSize: 14.5, + fontWeight: 600, + color: 'var(--fg1)' + } + }, c.name), /*#__PURE__*/React.createElement("div", { + style: { + fontSize: 12, + color: 'var(--fg3)', + fontFamily: 'var(--font-mono)', + whiteSpace: 'nowrap', + overflow: 'hidden', + textOverflow: 'ellipsis' + } + }, c.sub)), /*#__PURE__*/React.createElement(DIcon, { + name: "external-link", + size: 16, + color: "var(--fg3)" + })))), /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + gap: 14 + } + }, /*#__PURE__*/React.createElement("div", { + style: { + ...dCard, + flex: 1, + padding: '16px 18px' + } + }, /*#__PURE__*/React.createElement("div", { + style: { + fontSize: 12.5, + fontWeight: 600, + color: 'var(--fg3)' + } + }, t('contactHoursTitle')), /*#__PURE__*/React.createElement("div", { + style: { + fontSize: 15, + fontWeight: 600, + color: 'var(--fg1)', + marginTop: 6, + fontFamily: 'var(--font-mono)' + } + }, t('contactHours'))), /*#__PURE__*/React.createElement("div", { + style: { + ...dCard, + flex: 2, + padding: '16px 18px', + display: 'flex', + alignItems: 'center', + gap: 14 + } + }, /*#__PURE__*/React.createElement("div", { + style: { + flex: 1 + } + }, /*#__PURE__*/React.createElement("div", { + style: { + fontSize: 12.5, + fontWeight: 600, + color: 'var(--fg3)' + } + }, t('contactFaqTitle')), /*#__PURE__*/React.createElement("div", { + style: { + fontSize: 13, + color: 'var(--fg2)', + marginTop: 5, + lineHeight: 1.5 + } + }, t('contactFaq'))), /*#__PURE__*/React.createElement("button", { + style: { + border: '1.5px solid var(--border-strong)', + background: 'transparent', + color: 'var(--fg1)', + fontSize: 13, + fontWeight: 600, + padding: '9px 15px', + borderRadius: 'var(--radius-full)', + cursor: 'pointer', + whiteSpace: 'nowrap', + flexShrink: 0 + } + }, t('contactFaqBtn'))))); +} +Object.assign(window, { + DAuthFlow, + DOnboarding, + DAccountView, + DPlansView, + DRedeemView, + DContactView +}); +})(); } catch (e) { __ds_ns.__errors.push({ path: "ui_kits/desktop/dflows.jsx", error: String((e && e.message) || e) }); } + +// ui_kits/desktop/dparts.jsx +try { (() => { +/* dparts.jsx — 穿山甲 VPN desktop · atoms (inline Lucide paths) */ +const DLUCIDE = { + power: '', + 'shield-check': '', + globe: '', + 'loader-circle': '', + 'chart-no-axes-column': '', + settings: '', + check: '', + zap: '', + 'arrow-down': '', + 'arrow-up': '', + search: '', + moon: '', + sun: '', + crown: '', + mail: '', + lock: '', + 'check-circle': '', + 'arrow-left': '', + 'chevron-right': '', + send: '', + 'message-circle': '', + 'external-link': '', + 'shopping-bag': '', + laptop: '', + smartphone: '', + 'monitor-smartphone': '', + 'log-out': '', + ticket: '', + clock: '', + 'play-circle': '', + compass: '', + 'refresh-cw': '', + 'credit-card': '', + user: '' +}; +function DIcon({ + name, + size = 20, + stroke = 2, + color = 'currentColor', + style = {} +}) { + return /*#__PURE__*/React.createElement("svg", { + width: size, + height: size, + viewBox: "0 0 24 24", + fill: "none", + stroke: color, + strokeWidth: stroke, + strokeLinecap: "round", + strokeLinejoin: "round", + style: { + display: 'block', + flexShrink: 0, + ...style + }, + dangerouslySetInnerHTML: { + __html: DLUCIDE[name] || '' + } + }); +} +function DMark({ + size = 28, + color = 'var(--accent)' +}) { + return /*#__PURE__*/React.createElement("svg", { + width: size, + height: size, + viewBox: "0 0 96 96", + fill: "none", + style: { + display: 'block' + } + }, /*#__PURE__*/React.createElement("g", { + transform: "translate(96,0) scale(-1,1)" + }, /*#__PURE__*/React.createElement("path", { + d: "M16 59 Q17 51 26 50 Q34 31 45 30 Q55 30 62 37 Q69 41 75 45 Q85 49 89 44 Q92 50 84 52 Q75 53 67 53 Q66 62 60 62 L56 62 Q54 55 49 55 Q47 62 37 62 L33 62 Q31 56 26 56 Q20 59 16 59 Z", + fill: color + }), /*#__PURE__*/React.createElement("g", { + stroke: "#fff", + strokeWidth: "2.4", + strokeLinecap: "round", + strokeLinejoin: "round", + fill: "none", + opacity: "0.5" + }, /*#__PURE__*/React.createElement("path", { + d: "M26 50 Q34 43 42 50" + }), /*#__PURE__*/React.createElement("path", { + d: "M36 48 Q44 41 52 48" + }), /*#__PURE__*/React.createElement("path", { + d: "M46 48 Q54 41 62 49" + })), /*#__PURE__*/React.createElement("circle", { + cx: "23", + cy: "55", + r: "2.2", + fill: "#fff", + opacity: "0.85" + }))); +} +function DCC({ + code, + active, + size = 34 +}) { + return /*#__PURE__*/React.createElement("div", { + style: { + width: size, + height: size, + borderRadius: 'var(--radius-md)', + flexShrink: 0, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + fontFamily: 'var(--font-mono)', + fontSize: size * 0.31, + fontWeight: 600, + background: active ? 'var(--accent)' : 'var(--bg-subtle)', + color: active ? '#fff' : 'var(--fg2)' + } + }, code); +} +function DSignal({ + ping +}) { + const lvl = ping < 40 ? 3 : ping < 90 ? 2 : 1; + return /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + gap: 2, + alignItems: 'flex-end', + height: 13 + } + }, [4, 8, 13].map((h, i) => /*#__PURE__*/React.createElement("div", { + key: i, + style: { + width: 3, + height: h, + borderRadius: 1, + background: i < lvl ? 'var(--success)' : 'var(--border-strong)' + } + }))); +} +const DSTRINGS = { + connect: { + zh: '连接', + en: 'Connect' + }, + servers: { + zh: '选择节点', + en: 'Choose server' + }, + stats: { + zh: '统计', + en: 'Stats' + }, + settings: { + zh: '设置', + en: 'Settings' + }, + navConnect: { + zh: '连接', + en: 'Connect' + }, + navServers: { + zh: '节点', + en: 'Servers' + }, + navStats: { + zh: '统计', + en: 'Stats' + }, + navSettings: { + zh: '设置', + en: 'Settings' + }, + proMember: { + zh: 'PRO 会员', + en: 'PRO member' + }, + until: { + zh: '至 2026-12-31', + en: 'until 2026-12-31' + }, + connected: { + zh: '已连接', + en: 'Connected' + }, + notConnected: { + zh: '未连接', + en: 'Not connected' + }, + capOff: { + zh: '未连接', + en: 'Not connected' + }, + subOff: { + zh: '点击下方按钮开始保护', + en: 'Tap the button below to start' + }, + capConnecting: { + zh: '连接中…', + en: 'Connecting…' + }, + subConnecting: { + zh: '正在建立加密隧道', + en: 'Establishing encrypted tunnel' + }, + capOn: { + zh: '已连接', + en: 'Connected' + }, + subOn: { + zh: '你的网络已加密保护', + en: 'Your network is encrypted' + }, + connectNow: { + zh: '点击连接', + en: 'CONNECT' + }, + search: { + zh: '搜索国家 / 城市', + en: 'Search country / city' + }, + trafficMonth: { + zh: '本月流量', + en: 'Traffic' + }, + avgPing: { + zh: '平均延迟', + en: 'Avg ping' + }, + durMonth: { + zh: '本月时长', + en: 'Time' + }, + weekTraffic: { + zh: '本周流量 (GB)', + en: 'This week (GB)' + }, + autostart: { + zh: '开机自启动', + en: 'Launch at login' + }, + autostartSub: { + zh: '登录系统时自动启动穿山甲', + en: 'Start Pangolin when you log in' + }, + smartRoute: { + zh: '智能分流', + en: 'Smart routing' + }, + smartRouteSub: { + zh: '海外应用加速,本地直连', + en: 'Accelerate overseas apps, keep local direct' + }, + killSwitch: { + zh: 'Kill Switch', + en: 'Kill Switch' + }, + killSwitchSub: { + zh: '断线时阻断网络,防止 IP 泄露', + en: 'Block traffic if the VPN drops' + }, + language: { + zh: '语言', + en: 'Language' + }, + darkAppearance: { + zh: '深色外观', + en: 'Dark appearance' + }, + stateOn: { + zh: '已开启', + en: 'On' + }, + followLight: { + zh: '跟随浅色', + en: 'Off' + }, + protocol: { + zh: '协议', + en: 'Protocol' + }, + version: { + zh: '版本', + en: 'Version' + }, + days: { + zh: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'], + en: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] + }, + /* auth */ + authTagline: { + zh: '极速 · 稳定 · 省心', + en: 'Fast · Stable · Effortless' + }, + tabLogin: { + zh: '登录', + en: 'Log in' + }, + tabRegister: { + zh: '注册', + en: 'Sign up' + }, + emailLabel: { + zh: '邮箱', + en: 'Email' + }, + emailPh: { + zh: '你的邮箱地址', + en: 'your@email.com' + }, + pwLabel: { + zh: '密码', + en: 'Password' + }, + pwPh: { + zh: '输入密码', + en: 'Enter password' + }, + setPwPh: { + zh: '设置登录密码(用于多端登录)', + en: 'Set a password (for multi-device login)' + }, + codeLabel: { + zh: '邮箱验证码', + en: 'Verification code' + }, + codeSentTo: { + zh: '验证码已发送至', + en: 'Code sent to' + }, + sendCode: { + zh: '发送验证码', + en: 'Send code' + }, + resend: { + zh: '重新发送', + en: 'Resend' + }, + doLogin: { + zh: '登录', + en: 'Log in' + }, + doNext: { + zh: '下一步', + en: 'Next' + }, + doCreate: { + zh: '创建账户', + en: 'Create account' + }, + forgotPw: { + zh: '忘记密码?', + en: 'Forgot password?' + }, + tos: { + zh: '继续即代表同意《服务条款》与《隐私政策》', + en: 'By continuing you agree to our Terms & Privacy Policy' + }, + authSidePitch: { + zh: '一个账户,全平台同步。守护你的每一次连接。', + en: 'One account, every device. Protecting every connection.' + }, + /* redeem & buy */ + redeemEntry: { + zh: '兑换 & 购买', + en: 'Redeem & buy' + }, + redeemTitle: { + zh: '兑换 & 购买', + en: 'Redeem & buy' + }, + redeemCodeTitle: { + zh: '兑换激活码', + en: 'Redeem a code' + }, + redeemPh: { + zh: '输入激活码', + en: 'Enter activation code' + }, + redeemBtn: { + zh: '激活', + en: 'Redeem' + }, + redeemOk: { + zh: '激活成功 · PRO 已开通', + en: 'Activated · PRO unlocked' + }, + buyTitle: { + zh: '购买渠道', + en: 'Where to buy' + }, + buySub: { + zh: '客户端内不支持直接支付。请通过以下渠道获取激活码:', + en: 'In-app payment is unavailable. Get an activation code via:' + }, + chStore: { + zh: '自助发卡商店', + en: 'Self-serve store' + }, + chStoreSub: { + zh: '支付宝 / 微信 · 自动发码', + en: 'Alipay / WeChat · instant code' + }, + chUsdtSub: { + zh: '链上转账 · 最隐私 · 自动发码', + en: 'On-chain · most private · instant code' + }, + smartSelect: { + zh: '智能选择', + en: 'Smart select' + }, + smartSub: { + zh: '根据当前网络环境,自动选择最优节点', + en: 'Picks the best node for your network' + }, + recommended: { + zh: '推荐', + en: 'Recommended' + }, + twoFA: { + zh: '双重认证', + en: 'Two-factor auth' + }, + twoFASub: { + zh: 'PRO 专享 · 登录需身份验证器动态码', + en: 'PRO only · TOTP code at login' + }, + ucEntry: { + zh: 'Web 用户中心', + en: 'Web account center' + }, + ucEntrySub: { + zh: '订阅导入 · 邀请返利', + en: 'Subscription import · referral' + }, + ucOpen: { + zh: '打开', + en: 'Open' + }, + /* quota + free (口径见 CLAUDE.md §7,与移动端一致) */ + quotaToday: { + zh: '今日剩余', + en: 'Left today' + }, + minutes: { + zh: '分钟', + en: 'min' + }, + quotaFree: { + zh: '免费版 · 每日 10 分钟', + en: 'Free · 10 min/day' + }, + watchAd: { + zh: '看广告开始使用', + en: 'Watch ad to start' + }, + adUnlocked: { + zh: '已解锁 · 今日可用', + en: 'Unlocked for today' + }, + freePlanName: { + zh: '免费版', + en: 'Free' + }, + freeDemo: { + zh: '演示:免费版视角', + en: 'Demo: free-tier view' + }, + freeDemoSub: { + zh: '切换连接页的额度展示与套餐横幅', + en: 'Toggles quota UI & plan banner' + }, + checkUpdate: { + zh: '检查更新', + en: 'Check for updates' + }, + replayOnboarding: { + zh: '查看首次引导', + en: 'View onboarding' + }, + replayOnboardingSub: { + zh: '演示用 · 重看 3 屏引导', + en: 'Demo · replay the 3 intro screens' + }, + /* 2FA login step (与用户中心一致) */ + twoFATitle: { + zh: '双重认证', + en: 'Two-factor auth' + }, + twoFAHint: { + zh: '你的账户已开启双重认证。请输入身份验证器 App 中的 6 位动态码。', + en: 'Two-factor auth is on for this account. Enter the 6-digit code from your authenticator app.' + }, + twoFAConfirm: { + zh: '验证并登录', + en: 'Verify & log in' + }, + twoFABack: { + zh: '返回上一步', + en: 'Back' + }, + chTgSub: { + zh: '@PangolinVPN_bot', + en: '@PangolinVPN_bot' + }, + chLineSub: { + zh: '@pangolinvpn', + en: '@pangolinvpn' + }, + chEmail: { + zh: '邮箱客服', + en: 'Email support' + }, + chEmailSub: { + zh: 'buy@pangolin.vpn', + en: 'buy@pangolin.vpn' + }, + /* account management (integrated dashboard) */ + navAccount: { + zh: '账户', + en: 'Account' + }, + curPlan: { + zh: '当前套餐', + en: 'Current plan' + }, + renew: { + zh: '续费 / 升级', + en: 'Renew / Upgrade' + }, + myDevices: { + zh: '我的设备', + en: 'My devices' + }, + devicesSub: { + zh: 'PRO 套餐最多 5 台设备同时在线', + en: 'Up to 5 devices on PRO' + }, + thisDevice: { + zh: '当前设备', + en: 'This device' + }, + lastActive: { + zh: '最近活跃', + en: 'Last active' + }, + remove: { + zh: '移除', + en: 'Remove' + }, + choosePlan: { + zh: '选择套餐', + en: 'Choose your plan' + }, + free: { + zh: '免费版', + en: 'Free' + }, + proPlan: { + zh: '专业版', + en: 'Pro' + }, + team: { + zh: '团队版', + en: 'Team' + }, + perMonth: { + zh: '/月', + en: '/mo' + }, + current: { + zh: '当前', + en: 'Current' + }, + upgrade: { + zh: '立即升级', + en: 'Upgrade' + }, + choose: { + zh: '选择', + en: 'Choose' + }, + mostPopular: { + zh: '最受欢迎', + en: 'Popular' + }, + feFree: { + zh: ['仅 1 个基础节点', '每日 10 分钟时长', '使用前观看广告', '注册享 7 天免费试用'], + en: ['1 basic node only', '10 min per day', 'Watch an ad to start', '7-day free trial on sign-up'] + }, + fePro: { + zh: ['80+ 全球节点', '无限流量 · 极速', '5 台设备同时在线', '流媒体优化'], + en: ['80+ locations', 'Unlimited · fast', '5 devices', 'Streaming optimized'] + }, + feTeam: { + zh: ['Pro 全部功能', '10 个成员席位', '集中计费与管理', '优先客服'], + en: ['Everything in Pro', '10 seats', 'Central billing', 'Priority support'] + }, + /* account info */ + accInfoTitle: { + zh: '账户信息', + en: 'Account info' + }, + accEmail: { + zh: '邮箱', + en: 'Email' + }, + accPassword: { + zh: '密码', + en: 'Password' + }, + accChange: { + zh: '修改', + en: 'Change' + }, + accUsage: { + zh: '本月用量', + en: 'Used this month' + }, + accSignOut: { + zh: '退出登录', + en: 'Sign out' + }, + backToAccount: { + zh: '返回账户', + en: 'Back to account' + }, + /* demo replay */ + replayAuth: { + zh: '查看登录 / 注册流程', + en: 'View login / sign-up' + }, + replayAuthSub: { + zh: '演示用 · 退出到登录页', + en: 'Demo · back to login' + }, + replayOnboarding: { + zh: '查看首次引导', + en: 'View onboarding' + }, + replayOnboardingSub: { + zh: '演示用 · 重看引导页', + en: 'Demo · replay intro' + }, + /* onboarding */ + obSkip: { + zh: '跳过', + en: 'Skip' + }, + obNext: { + zh: '下一步', + en: 'Next' + }, + obAllow: { + zh: '允许并继续', + en: 'Allow & continue' + }, + obStart: { + zh: '开始使用', + en: 'Get started' + }, + ob1Title: { + zh: '一键连接全球', + en: 'Connect worldwide' + }, + ob1Sub: { + zh: '80+ 节点,智能选择最快线路,稳定不掉线。', + en: '80+ locations, auto-fastest routing, rock-solid.' + }, + ob2Title: { + zh: '授权网络配置', + en: 'Allow network setup' + }, + ob2Sub: { + zh: '系统会请求添加一个网络配置,用于建立加密隧道。', + en: 'Your system will ask to add a network profile to build the encrypted tunnel.' + }, + ob3Title: { + zh: '安全 · 无日志', + en: 'Private by design' + }, + ob3Sub: { + zh: '端到端加密,我们不记录你的任何浏览数据。', + en: 'End-to-end encrypted. We keep zero browsing logs.' + }, + /* contact us */ + navContact: { + zh: '联系我们', + en: 'Contact us' + }, + contactIntro: { + zh: '遇到问题?通过以下任一渠道联系我们,通常数分钟内回复。', + en: 'Need help? Reach us through any channel below — we usually reply within minutes.' + }, + contactTg: { + zh: 'Telegram 客服', + en: 'Telegram support' + }, + contactLine: { + zh: 'LINE 客服', + en: 'LINE support' + }, + contactEmail: { + zh: '邮箱客服', + en: 'Email support' + }, + contactStore: { + zh: '自助发卡商店', + en: 'Self-serve store' + }, + contactHoursTitle: { + zh: '服务时间', + en: 'Support hours' + }, + contactHours: { + zh: '每日 9:00 – 24:00 (GMT+8)', + en: 'Daily 9:00 – 24:00 (GMT+8)' + }, + contactFaqTitle: { + zh: '常见问题', + en: 'FAQ' + }, + contactFaq: { + zh: '连不上 / 兑换码无效 / 多设备登录,先查阅帮助中心。', + en: 'Connection issues, invalid codes, multi-device — check the Help Center first.' + }, + contactFaqBtn: { + zh: '打开帮助中心', + en: 'Open Help Center' + } +}; +function dT(lang) { + return k => { + const v = DSTRINGS[k]; + return v ? v[lang] || k : k; + }; +} +function dName(s, lang) { + return lang === 'zh' ? s.name : s.en; +} +function dSub(s, lang) { + if (lang === 'zh') return s.tag || s.en; + return s.tag === '流媒体优化' ? 'Streaming' : s.tag === 'P2P' ? 'P2P' : s.name; +} +Object.assign(window, { + DIcon, + DMark, + DCC, + DSignal, + DSTRINGS, + dT, + dName, + dSub, + DSERVERS: [{ + code: 'HK', + name: '香港 · 流媒体', + en: 'Hong Kong', + ping: 18, + tag: '流媒体优化' + }, { + code: 'JP', + name: '日本 东京', + en: 'Tokyo', + ping: 32, + tag: 'P2P' + }, { + code: 'TW', + name: '台湾 台北', + en: 'Taipei', + ping: 28, + tag: '' + }, { + code: 'SG', + name: '新加坡', + en: 'Singapore', + ping: 54, + tag: 'P2P' + }, { + code: 'KR', + name: '韩国 首尔', + en: 'Seoul', + ping: 41, + tag: '' + }, { + code: 'US', + name: '美国 洛杉矶', + en: 'Los Angeles', + ping: 146, + tag: '流媒体优化' + }, { + code: 'DE', + name: '德国 法兰克福', + en: 'Frankfurt', + ping: 198, + tag: '' + }, { + code: 'UK', + name: '英国 伦敦', + en: 'London', + ping: 210, + tag: '' + }] +}); +})(); } catch (e) { __ds_ns.__errors.push({ path: "ui_kits/desktop/dparts.jsx", error: String((e && e.message) || e) }); } + +// ui_kits/desktop/macos-window.jsx +try { (() => { +/* BEGIN USAGE */ +// MacOS.jsx — Simplified macOS Tahoe (Liquid Glass) window +// Based on the macOS Tahoe UI Kit. No image assets, no dependencies. +// Exports (to window): MacWindow, MacSidebar, MacSidebarItem, MacSidebarHeader, MacToolbar, MacGlass, MacTrafficLights +// +// Usage — wrap your app content in to get the window chrome +// (traffic lights + titlebar). Props: width, height, title, sidebar (pass a +// element); compose MacToolbar/MacGlass inside as needed: +// +// }> +// ...your app content... +// +/* END USAGE */ + +const MAC_FONT = '-apple-system, BlinkMacSystemFont, "SF Pro", "Helvetica Neue", sans-serif'; + +// ───────────────────────────────────────────────────────────── +// Liquid glass primitive — blur + white tint + inset highlight +// ───────────────────────────────────────────────────────────── +function MacGlass({ + children, + radius = 296, + dark = false, + style = {} +}) { + return /*#__PURE__*/React.createElement("div", { + style: { + position: 'relative', + borderRadius: radius, + ...style + } + }, /*#__PURE__*/React.createElement("div", { + style: { + position: 'absolute', + inset: 0, + borderRadius: radius, + background: dark ? 'rgba(255,255,255,0.08)' : 'rgba(255,255,255,0.35)', + backdropFilter: 'blur(40px) saturate(180%)', + WebkitBackdropFilter: 'blur(40px) saturate(180%)', + border: dark ? '0.5px solid rgba(255,255,255,0.12)' : '0.5px solid rgba(255,255,255,0.6)', + boxShadow: dark ? '0 8px 40px rgba(0,0,0,0.2)' : '0 8px 40px rgba(0,0,0,0.08), inset 0 1px 0 rgba(255,255,255,0.4)' + } + }), /*#__PURE__*/React.createElement("div", { + style: { + position: 'relative', + zIndex: 1 + } + }, children)); +} + +// ───────────────────────────────────────────────────────────── +// Traffic lights (14px, Tahoe colors) +// ───────────────────────────────────────────────────────────── +function MacTrafficLights({ + style = {} +}) { + const dot = bg => /*#__PURE__*/React.createElement("div", { + style: { + width: 14, + height: 14, + borderRadius: '50%', + background: bg, + border: '0.5px solid rgba(0,0,0,0.1)' + } + }); + return /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + gap: 9, + alignItems: 'center', + padding: 1, + ...style + } + }, dot('#ff736a'), dot('#febc2e'), dot('#19c332')); +} + +// ───────────────────────────────────────────────────────────── +// Toolbar — title + single glass pill icon +// ───────────────────────────────────────────────────────────── +function MacToolbar({ + title = 'Folder' +}) { + return /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + gap: 8, + alignItems: 'center', + padding: 8, + flexShrink: 0 + } + }, /*#__PURE__*/React.createElement("div", { + style: { + fontFamily: MAC_FONT, + fontSize: 15, + fontWeight: 700, + color: 'rgba(0,0,0,0.85)', + whiteSpace: 'nowrap', + paddingLeft: 8 + } + }, title), /*#__PURE__*/React.createElement("div", { + style: { + flex: 1 + } + }), /*#__PURE__*/React.createElement(MacGlass, null, /*#__PURE__*/React.createElement("div", { + style: { + width: 36, + height: 36, + display: 'flex', + alignItems: 'center', + justifyContent: 'center' + } + }, /*#__PURE__*/React.createElement("div", { + style: { + width: 14, + height: 14, + borderRadius: '50%', + background: '#4c4c4c', + opacity: 0.4 + } + }))), /*#__PURE__*/React.createElement(MacGlass, null, /*#__PURE__*/React.createElement("div", { + style: { + width: 140, + height: 36, + display: 'flex', + alignItems: 'center', + gap: 6, + padding: '0 12px' + } + }, /*#__PURE__*/React.createElement("svg", { + width: "13", + height: "13", + viewBox: "0 0 13 13", + fill: "none" + }, /*#__PURE__*/React.createElement("circle", { + cx: "5.5", + cy: "5.5", + r: "4", + stroke: "#727272", + strokeWidth: "1.5" + }), /*#__PURE__*/React.createElement("path", { + d: "M8.5 8.5l3 3", + stroke: "#727272", + strokeWidth: "1.5", + strokeLinecap: "round" + })), /*#__PURE__*/React.createElement("span", { + style: { + fontFamily: MAC_FONT, + fontSize: 13, + fontWeight: 500, + color: '#727272' + } + }, "Search")))); +} + +// ───────────────────────────────────────────────────────────── +// Sidebar — frosted glass panel floating inside the window +// ───────────────────────────────────────────────────────────── +function MacSidebarItem({ + label, + selected = false +}) { + return /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + alignItems: 'center', + gap: 6, + height: 24, + padding: '4px 10px 4px 6px', + margin: '0 10px', + borderRadius: 8, + position: 'relative', + fontFamily: MAC_FONT, + fontSize: 11, + fontWeight: 500 + } + }, selected && /*#__PURE__*/React.createElement("div", { + style: { + position: 'absolute', + inset: 0, + borderRadius: 8, + background: 'rgba(0,0,0,0.11)', + mixBlendMode: 'multiply' + } + }), /*#__PURE__*/React.createElement("div", { + style: { + width: 14, + height: 14, + borderRadius: '50%', + background: selected ? '#007aff' : 'rgba(0,0,0,0.4)', + opacity: selected ? 1 : 0.5, + flexShrink: 0, + position: 'relative' + } + }), /*#__PURE__*/React.createElement("span", { + style: { + color: 'rgba(0,0,0,0.85)', + position: 'relative' + } + }, label)); +} +function MacSidebar({ + children +}) { + return /*#__PURE__*/React.createElement("div", { + style: { + width: 220, + height: '100%', + padding: 8, + flexShrink: 0, + position: 'relative', + display: 'flex', + flexDirection: 'column' + } + }, /*#__PURE__*/React.createElement("div", { + style: { + position: 'absolute', + inset: 8, + borderRadius: 18, + background: 'rgba(210,225,245,0.45)', + backdropFilter: 'blur(50px) saturate(200%)', + WebkitBackdropFilter: 'blur(50px) saturate(200%)', + border: '0.5px solid rgba(255,255,255,0.5)', + boxShadow: '0 8px 40px rgba(0,0,0,0.10), inset 0 1px 0 rgba(255,255,255,0.35)' + } + }), /*#__PURE__*/React.createElement("div", { + style: { + position: 'relative', + zIndex: 1, + padding: '10px 0', + display: 'flex', + flexDirection: 'column', + gap: 2 + } + }, /*#__PURE__*/React.createElement("div", { + style: { + height: 32, + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + padding: '0 10px', + marginBottom: 4 + } + }, /*#__PURE__*/React.createElement(MacTrafficLights, null)), children)); +} +function MacSidebarHeader({ + title +}) { + return /*#__PURE__*/React.createElement("div", { + style: { + padding: '14px 18px 5px', + fontFamily: MAC_FONT, + fontSize: 11, + fontWeight: 700, + color: 'rgba(0,0,0,0.5)' + } + }, title); +} + +// ───────────────────────────────────────────────────────────── +// Window — r:26, big shadow, sidebar + toolbar + content +// ───────────────────────────────────────────────────────────── +function MacWindow({ + width = 900, + height = 600, + title = 'Folder', + sidebar, + children +}) { + return /*#__PURE__*/React.createElement("div", { + style: { + width, + height, + borderRadius: 26, + overflow: 'hidden', + background: '#fff', + boxShadow: '0 0 0 1px rgba(0,0,0,0.23), 0 16px 48px rgba(0,0,0,0.35)', + display: 'flex', + position: 'relative', + fontFamily: MAC_FONT + } + }, /*#__PURE__*/React.createElement(MacSidebar, null, sidebar), /*#__PURE__*/React.createElement("div", { + style: { + flex: 1, + display: 'flex', + flexDirection: 'column' + } + }, /*#__PURE__*/React.createElement(MacToolbar, { + title: title + }), /*#__PURE__*/React.createElement("div", { + style: { + flex: 1, + overflow: 'auto', + padding: '4px 8px' + } + }, children))); +} +Object.assign(window, { + MacWindow, + MacSidebar, + MacSidebarItem, + MacSidebarHeader, + MacToolbar, + MacGlass, + MacTrafficLights +}); +})(); } catch (e) { __ds_ns.__errors.push({ path: "ui_kits/desktop/macos-window.jsx", error: String((e && e.message) || e) }); } + +// ui_kits/mobile/flows.jsx +try { (() => { +/* flows.jsx — 穿山甲 VPN mobile · auth, onboarding, redeem/buy */ +const { + useState: useStateF +} = React; + +/* shared field */ +function Field({ + icon, + label, + children +}) { + return /*#__PURE__*/React.createElement("label", { + style: { + display: 'block' + } + }, /*#__PURE__*/React.createElement("span", { + style: { + display: 'block', + fontSize: 12, + fontWeight: 600, + color: 'var(--fg2)', + marginBottom: 7 + } + }, label), /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + alignItems: 'center', + gap: 10, + background: 'var(--surface)', + border: '1.5px solid var(--border-strong)', + borderRadius: 'var(--radius-md)', + padding: '12px 14px' + } + }, icon && /*#__PURE__*/React.createElement(Icon, { + name: icon, + size: 18, + color: "var(--fg3)" + }), children)); +} +const inputStyle = { + border: 'none', + background: 'transparent', + outline: 'none', + flex: 1, + fontFamily: 'var(--font-sans)', + fontSize: 15, + color: 'var(--fg1)', + minWidth: 0 +}; + +/* ───────── AUTH (login + email-code register) ───────── */ +function AuthFlow({ + t, + lang, + onDone +}) { + const [mode, setMode] = useStateF('login'); // login | register + const [step, setStep] = useStateF(0); // register: 0 email+code, 1 set password + const [email, setEmail] = useStateF(''); + const [code, setCode] = useStateF(''); + const [pw, setPw] = useStateF(''); + const [sent, setSent] = useStateF(false); + const [totp, setTotp] = useStateF(false); // 2FA step after login (PRO) + const [otp, setOtp] = useStateF(''); + const valid = /\S+@\S+\.\S+/.test(email); + const tab = (id, label) => /*#__PURE__*/React.createElement("button", { + onClick: () => { + setMode(id); + setStep(0); + setSent(false); + }, + style: { + flex: 1, + border: 'none', + cursor: 'pointer', + background: 'transparent', + padding: '10px 0', + fontFamily: 'var(--font-sans)', + fontSize: 15, + fontWeight: mode === id ? 700 : 500, + color: mode === id ? 'var(--fg1)' : 'var(--fg3)', + borderBottom: mode === id ? '2px solid var(--accent)' : '2px solid transparent' + } + }, label); + const primaryBtn = (label, onClick, enabled) => /*#__PURE__*/React.createElement("button", { + onClick: enabled ? onClick : undefined, + disabled: !enabled, + style: { + width: '100%', + border: 'none', + borderRadius: 'var(--radius-full)', + padding: '14px', + fontFamily: 'var(--font-sans)', + fontWeight: 700, + fontSize: 15, + cursor: enabled ? 'pointer' : 'not-allowed', + background: 'var(--accent)', + color: 'var(--fg-on-accent)', + opacity: enabled ? 1 : 0.45, + marginTop: 4 + } + }, label); + return /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + flexDirection: 'column', + height: '100%', + padding: '0 28px', + background: 'var(--bg)' + } + }, /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + gap: 12, + paddingTop: 18, + paddingBottom: 28 + } + }, /*#__PURE__*/React.createElement(Mark, { + size: 56 + }), /*#__PURE__*/React.createElement("div", { + style: { + textAlign: 'center' + } + }, /*#__PURE__*/React.createElement("div", { + style: { + fontFamily: 'var(--font-display)', + fontWeight: 700, + fontSize: 24, + color: 'var(--fg1)' + } + }, t('brand')), /*#__PURE__*/React.createElement("div", { + style: { + fontSize: 13, + color: 'var(--accent)', + fontWeight: 600, + letterSpacing: '0.04em', + marginTop: 4 + } + }, t('authTagline')))), /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + borderBottom: '1px solid var(--border)', + marginBottom: 22 + } + }, tab('login', t('tabLogin')), tab('register', t('tabRegister'))), /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + flexDirection: 'column', + gap: 16, + flex: 1 + } + }, mode === 'login' && !totp && /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(Field, { + icon: "mail", + label: t('emailLabel') + }, /*#__PURE__*/React.createElement("input", { + style: inputStyle, + value: email, + onChange: e => setEmail(e.target.value), + placeholder: t('emailPh') + })), /*#__PURE__*/React.createElement(Field, { + icon: "lock", + label: t('pwLabel') + }, /*#__PURE__*/React.createElement("input", { + style: inputStyle, + type: "password", + value: pw, + onChange: e => setPw(e.target.value), + placeholder: t('pwPh') + })), /*#__PURE__*/React.createElement("div", { + style: { + textAlign: 'right', + marginTop: -6 + } + }, /*#__PURE__*/React.createElement("span", { + style: { + fontSize: 13, + color: 'var(--accent)', + fontWeight: 600 + } + }, t('forgotPw'))), primaryBtn(t('doLogin'), () => setTotp(true), valid && pw.length > 0)), mode === 'login' && totp && /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + alignItems: 'center', + gap: 8 + } + }, /*#__PURE__*/React.createElement(Icon, { + name: "shield-check", + size: 19, + color: "var(--accent)" + }), /*#__PURE__*/React.createElement("span", { + style: { + fontFamily: 'var(--font-display)', + fontWeight: 700, + fontSize: 18, + color: 'var(--fg1)' + } + }, t('twoFATitle')), /*#__PURE__*/React.createElement("span", { + style: { + fontSize: 10, + fontWeight: 700, + color: 'var(--accent)', + background: 'var(--accent-subtle)', + border: '1px solid var(--accent-border)', + padding: '2px 8px', + borderRadius: 999 + } + }, "PRO")), /*#__PURE__*/React.createElement("div", { + style: { + fontSize: 12.5, + color: 'var(--fg3)', + lineHeight: 1.6, + marginTop: -6 + } + }, t('twoFAHint')), /*#__PURE__*/React.createElement(Field, { + icon: "shield-check", + label: t('twoFATitle') + }, /*#__PURE__*/React.createElement("input", { + style: { + ...inputStyle, + fontFamily: 'var(--font-mono)', + fontSize: 20, + letterSpacing: '0.5em', + textAlign: 'center' + }, + value: otp, + onChange: e => setOtp(e.target.value.replace(/\D/g, '').slice(0, 6)), + placeholder: "\xB7\xB7\xB7\xB7\xB7\xB7" + })), primaryBtn(t('twoFAConfirm'), onDone, otp.length === 6), /*#__PURE__*/React.createElement("div", { + onClick: () => setTotp(false), + style: { + textAlign: 'center', + fontSize: 12.5, + color: 'var(--fg3)', + cursor: 'pointer' + } + }, t('twoFABack'))), mode === 'register' && step === 0 && /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(Field, { + icon: "mail", + label: t('emailLabel') + }, /*#__PURE__*/React.createElement("input", { + style: inputStyle, + value: email, + onChange: e => setEmail(e.target.value), + placeholder: t('emailPh') + }), /*#__PURE__*/React.createElement("button", { + onClick: () => valid && setSent(true), + style: { + border: 'none', + background: 'transparent', + cursor: valid ? 'pointer' : 'default', + color: valid ? 'var(--accent)' : 'var(--fg3)', + fontWeight: 700, + fontSize: 13, + whiteSpace: 'nowrap', + flexShrink: 0 + } + }, sent ? t('resend') : t('sendCode'))), sent && /*#__PURE__*/React.createElement("div", { + style: { + fontSize: 12, + color: 'var(--success)', + marginTop: -8, + display: 'flex', + alignItems: 'center', + gap: 6 + } + }, /*#__PURE__*/React.createElement(Icon, { + name: "check-circle", + size: 14, + color: "var(--success)" + }), t('codeSentTo'), " ", email), /*#__PURE__*/React.createElement(Field, { + icon: "shield-check", + label: t('codeLabel') + }, /*#__PURE__*/React.createElement("input", { + style: { + ...inputStyle, + fontFamily: 'var(--font-mono)', + letterSpacing: '0.3em' + }, + value: code, + onChange: e => setCode(e.target.value.replace(/\D/g, '').slice(0, 6)), + placeholder: "\xB7\xB7\xB7\xB7\xB7\xB7" + })), primaryBtn(t('doNext'), () => setStep(1), sent && code.length === 6)), mode === 'register' && step === 1 && /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("div", { + style: { + fontSize: 13, + color: 'var(--fg2)', + display: 'flex', + alignItems: 'center', + gap: 7, + marginBottom: -4 + } + }, /*#__PURE__*/React.createElement(Icon, { + name: "check-circle", + size: 15, + color: "var(--success)" + }), email), /*#__PURE__*/React.createElement(Field, { + icon: "lock", + label: t('pwLabel') + }, /*#__PURE__*/React.createElement("input", { + style: inputStyle, + type: "password", + value: pw, + onChange: e => setPw(e.target.value), + placeholder: t('setPwPh') + })), primaryBtn(t('doCreate'), onDone, pw.length >= 6))), /*#__PURE__*/React.createElement("div", { + style: { + fontSize: 11, + color: 'var(--fg3)', + textAlign: 'center', + lineHeight: 1.5, + padding: '18px 8px 24px' + } + }, t('tos'))); +} + +/* ───────── ONBOARDING (3 screens) ───────── */ +function Onboarding({ + t, + lang, + onDone +}) { + const [i, setI] = useStateF(0); + const slides = [{ + icon: 'globe', + tint: 'var(--accent)', + bg: 'var(--accent-subtle)', + title: t('ob1Title'), + sub: t('ob1Sub'), + cta: t('obNext') + }, { + icon: 'shield', + tint: 'var(--accent)', + bg: 'var(--accent-subtle)', + title: t('ob2Title'), + sub: t('ob2Sub'), + cta: t('obAllow') + }, { + icon: 'shield-check', + tint: 'var(--success)', + bg: 'var(--success-subtle)', + title: t('ob3Title'), + sub: t('ob3Sub'), + cta: t('obStart') + }]; + const s = slides[i]; + const next = () => i < 2 ? setI(i + 1) : onDone(); + return /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + flexDirection: 'column', + height: '100%', + padding: '0 28px', + background: 'var(--bg)' + } + }, /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + justifyContent: 'flex-end', + paddingTop: 10 + } + }, /*#__PURE__*/React.createElement("button", { + onClick: onDone, + style: { + border: 'none', + background: 'transparent', + cursor: 'pointer', + color: 'var(--fg3)', + fontSize: 14, + fontWeight: 600, + padding: 8 + } + }, t('obSkip'))), /*#__PURE__*/React.createElement("div", { + style: { + flex: 1, + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'center', + gap: 28, + textAlign: 'center' + } + }, /*#__PURE__*/React.createElement("div", { + style: { + width: 120, + height: 120, + borderRadius: '50%', + background: s.bg, + display: 'flex', + alignItems: 'center', + justifyContent: 'center' + } + }, /*#__PURE__*/React.createElement(Icon, { + name: s.icon, + size: 52, + stroke: 1.6, + color: s.tint + })), /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("div", { + style: { + fontFamily: 'var(--font-display)', + fontWeight: 700, + fontSize: 26, + color: 'var(--fg1)' + } + }, s.title), /*#__PURE__*/React.createElement("div", { + style: { + fontSize: 15, + color: 'var(--fg2)', + lineHeight: 1.6, + marginTop: 12, + maxWidth: 280 + } + }, s.sub))), /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + justifyContent: 'center', + gap: 8, + marginBottom: 22 + } + }, slides.map((_, k) => /*#__PURE__*/React.createElement("div", { + key: k, + style: { + width: k === i ? 22 : 7, + height: 7, + borderRadius: 999, + background: k === i ? 'var(--accent)' : 'var(--border-strong)', + transition: 'all 220ms var(--ease-out)' + } + }))), /*#__PURE__*/React.createElement("button", { + onClick: next, + style: { + width: '100%', + border: 'none', + borderRadius: 'var(--radius-full)', + padding: '15px', + fontFamily: 'var(--font-sans)', + fontWeight: 700, + fontSize: 15, + cursor: 'pointer', + background: 'var(--accent)', + color: 'var(--fg-on-accent)', + marginBottom: 26 + } + }, s.cta)); +} + +/* ───────── REDEEM & BUY ───────── */ +function RedeemScreen({ + t, + lang, + onBack +}) { + const [code, setCode] = useStateF(''); + const [ok, setOk] = useStateF(false); + const channels = [{ + icon: 'shopping-bag', + name: t('chStore'), + sub: t('chStoreSub'), + accent: true + }, { + icon: 'credit-card', + name: 'USDT (TRC20)', + sub: t('chUsdtSub') + }, { + icon: 'send', + name: 'Telegram', + sub: t('chTgSub') + }, { + icon: 'message-circle', + name: 'LINE', + sub: t('chLineSub') + }, { + icon: 'mail', + name: t('chEmail'), + sub: t('chEmailSub') + }]; + return /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + flexDirection: 'column', + height: '100%' + } + }, /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + alignItems: 'center', + gap: 8, + padding: '6px 16px 14px' + } + }, /*#__PURE__*/React.createElement("button", { + onClick: onBack, + style: { + border: 'none', + background: 'transparent', + cursor: 'pointer', + padding: 6, + display: 'flex' + } + }, /*#__PURE__*/React.createElement(Icon, { + name: "arrow-left", + size: 22, + color: "var(--fg1)" + })), /*#__PURE__*/React.createElement("span", { + style: { + fontFamily: 'var(--font-display)', + fontWeight: 700, + fontSize: 18, + color: 'var(--fg1)', + whiteSpace: 'nowrap' + } + }, t('redeemTitle'))), /*#__PURE__*/React.createElement("div", { + style: { + flex: 1, + overflow: 'auto', + padding: '0 20px 20px' + } + }, /*#__PURE__*/React.createElement("div", { + style: { + background: 'var(--surface)', + border: '1px solid var(--border)', + borderRadius: 'var(--radius-lg)', + boxShadow: 'var(--shadow-sm)', + padding: 18, + marginBottom: 22 + } + }, /*#__PURE__*/React.createElement("div", { + style: { + fontSize: 14, + fontWeight: 700, + color: 'var(--fg1)', + marginBottom: 12 + } + }, t('redeemCodeTitle')), ok ? /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + alignItems: 'center', + gap: 9, + color: 'var(--success)', + fontWeight: 600, + fontSize: 15, + padding: '6px 0' + } + }, /*#__PURE__*/React.createElement(Icon, { + name: "check-circle", + size: 20, + color: "var(--success)" + }), t('redeemOk')) : /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + gap: 10 + } + }, /*#__PURE__*/React.createElement("input", { + value: code, + onChange: e => setCode(e.target.value.toUpperCase()), + placeholder: t('redeemPh'), + style: { + flex: 1, + minWidth: 0, + border: '1.5px solid var(--border-strong)', + borderRadius: 'var(--radius-md)', + padding: '12px 14px', + fontFamily: 'var(--font-mono)', + fontSize: 14, + letterSpacing: '0.08em', + color: 'var(--fg1)', + background: 'var(--bg)', + outline: 'none' + } + }), /*#__PURE__*/React.createElement("button", { + onClick: () => code.length >= 4 && setOk(true), + style: { + border: 'none', + borderRadius: 'var(--radius-md)', + padding: '0 20px', + background: 'var(--accent)', + color: '#fff', + fontWeight: 700, + fontSize: 14, + cursor: 'pointer', + flexShrink: 0 + } + }, t('redeemBtn'))))), /*#__PURE__*/React.createElement("div", { + style: { + fontSize: 14, + fontWeight: 700, + color: 'var(--fg1)', + marginBottom: 6 + } + }, t('buyTitle')), /*#__PURE__*/React.createElement("div", { + style: { + fontSize: 12.5, + color: 'var(--fg3)', + lineHeight: 1.5, + marginBottom: 14 + } + }, t('buySub')), /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + flexDirection: 'column', + gap: 10 + } + }, channels.map(c => /*#__PURE__*/React.createElement("button", { + key: c.name, + style: { + display: 'flex', + alignItems: 'center', + gap: 13, + width: '100%', + textAlign: 'left', + cursor: 'pointer', + background: c.accent ? 'var(--accent-subtle)' : 'var(--surface)', + border: `1px solid ${c.accent ? 'var(--accent-border)' : 'var(--border)'}`, + borderRadius: 'var(--radius-lg)', + padding: '14px 16px', + boxShadow: 'var(--shadow-sm)' + } + }, /*#__PURE__*/React.createElement("div", { + style: { + width: 40, + height: 40, + borderRadius: 'var(--radius-md)', + background: c.accent ? 'var(--accent)' : 'var(--bg-subtle)', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + flexShrink: 0 + } + }, /*#__PURE__*/React.createElement(Icon, { + name: c.icon, + size: 20, + color: c.accent ? '#fff' : 'var(--accent)' + })), /*#__PURE__*/React.createElement("div", { + style: { + flex: 1 + } + }, /*#__PURE__*/React.createElement("div", { + style: { + fontSize: 15, + fontWeight: 600, + color: 'var(--fg1)' + } + }, c.name), /*#__PURE__*/React.createElement("div", { + style: { + fontSize: 12.5, + color: 'var(--fg3)', + fontFamily: c.icon === 'send' || c.icon === 'mail' || c.icon === 'message-circle' ? 'var(--font-mono)' : 'var(--font-sans)' + } + }, c.sub)), /*#__PURE__*/React.createElement(Icon, { + name: "external-link", + size: 17, + color: "var(--fg3)" + })))))); +} + +/* ───────── PLANS (shown on upgrade) ───────── */ +function PlansScreen({ + t, + lang, + onBack, + onChoose +}) { + const plans = [{ + id: 'free', + name: t('free'), + price: '¥0', + feats: t('feFree'), + cta: t('current'), + variant: 'cur' + }, { + id: 'pro', + name: t('proPlan'), + price: '¥25', + feats: t('fePro'), + cta: t('upgrade'), + variant: 'pro' + }, { + id: 'team', + name: t('team'), + price: '¥99', + feats: t('feTeam'), + cta: t('choose'), + variant: 'plain' + }]; + return /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + flexDirection: 'column', + height: '100%' + } + }, /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + alignItems: 'center', + gap: 8, + padding: '6px 16px 14px' + } + }, /*#__PURE__*/React.createElement("button", { + onClick: onBack, + style: { + border: 'none', + background: 'transparent', + cursor: 'pointer', + padding: 6, + display: 'flex' + } + }, /*#__PURE__*/React.createElement(Icon, { + name: "arrow-left", + size: 22, + color: "var(--fg1)" + })), /*#__PURE__*/React.createElement("span", { + style: { + fontFamily: 'var(--font-display)', + fontWeight: 700, + fontSize: 18, + color: 'var(--fg1)', + whiteSpace: 'nowrap' + } + }, t('choosePlan'))), /*#__PURE__*/React.createElement("div", { + style: { + flex: 1, + overflow: 'auto', + padding: '4px 20px 20px', + display: 'flex', + flexDirection: 'column', + gap: 14 + } + }, plans.map(p => { + const isPro = p.variant === 'pro'; + return /*#__PURE__*/React.createElement("div", { + key: p.id, + style: { + position: 'relative', + borderRadius: 'var(--radius-xl)', + padding: '18px 20px', + ...(isPro ? { + background: 'linear-gradient(155deg,var(--clay-600),var(--clay-800))', + color: '#fff', + boxShadow: 'var(--shadow-md)' + } : { + background: 'var(--surface)', + border: '1px solid var(--border)', + boxShadow: 'var(--shadow-sm)' + }) + } + }, /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + alignItems: 'baseline', + justifyContent: 'space-between' + } + }, /*#__PURE__*/React.createElement("div", { + style: { + fontSize: 14, + fontWeight: 600, + color: isPro ? '#fff' : 'var(--fg2)', + opacity: isPro ? 0.9 : 1 + } + }, p.name, isPro && /*#__PURE__*/React.createElement("span", { + style: { + marginLeft: 8, + fontSize: 10.5, + fontWeight: 700, + background: '#fff', + color: 'var(--clay-700)', + padding: '2px 8px', + borderRadius: 999 + } + }, t('mostPopular'))), /*#__PURE__*/React.createElement("div", { + style: { + fontFamily: 'var(--font-display)', + fontSize: 26, + fontWeight: 700, + color: isPro ? '#fff' : 'var(--fg1)' + } + }, p.price, /*#__PURE__*/React.createElement("span", { + style: { + fontSize: 12, + fontWeight: 500, + opacity: 0.7 + } + }, t('perMonth')))), /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + flexWrap: 'wrap', + gap: '7px 16px', + margin: '14px 0 16px' + } + }, p.feats.map((f, i) => /*#__PURE__*/React.createElement("div", { + key: i, + style: { + display: 'flex', + alignItems: 'center', + gap: 7, + fontSize: 12.5, + color: isPro ? 'rgba(255,255,255,.92)' : 'var(--fg2)' + } + }, /*#__PURE__*/React.createElement(Icon, { + name: "check", + size: 14, + color: isPro ? '#fff' : 'var(--success)', + stroke: 2.4 + }), f))), /*#__PURE__*/React.createElement("button", { + onClick: p.variant !== 'cur' ? onChoose : undefined, + style: { + width: '100%', + border: p.variant === 'cur' ? '1.5px solid var(--border-strong)' : 'none', + cursor: p.variant === 'cur' ? 'default' : 'pointer', + borderRadius: 'var(--radius-full)', + padding: '12px', + fontWeight: 700, + fontSize: 14, + background: isPro ? '#fff' : p.variant === 'cur' ? 'transparent' : 'var(--accent)', + color: isPro ? 'var(--clay-700)' : p.variant === 'cur' ? 'var(--fg2)' : '#fff' + } + }, p.cta)); + }))); +} + +/* ───────── CONTACT US ───────── */ +function ContactScreen({ + t, + lang, + onBack +}) { + const channels = [{ + icon: 'send', + name: 'Telegram', + sub: '@PangolinVPN_bot', + accent: true + }, { + icon: 'message-circle', + name: 'LINE', + sub: '@pangolinvpn' + }, { + icon: 'mail', + name: t('contactEmail'), + sub: 'support@pangolin.vpn' + }, { + icon: 'shopping-bag', + name: t('contactStore'), + sub: 'shop.pangolin.vpn' + }]; + return /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + flexDirection: 'column', + height: '100%' + } + }, /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + alignItems: 'center', + gap: 8, + padding: '6px 16px 14px' + } + }, /*#__PURE__*/React.createElement("button", { + onClick: onBack, + style: { + border: 'none', + background: 'transparent', + cursor: 'pointer', + padding: 6, + display: 'flex' + } + }, /*#__PURE__*/React.createElement(Icon, { + name: "arrow-left", + size: 22, + color: "var(--fg1)" + })), /*#__PURE__*/React.createElement("span", { + style: { + fontFamily: 'var(--font-display)', + fontWeight: 700, + fontSize: 18, + color: 'var(--fg1)', + whiteSpace: 'nowrap' + } + }, t('contactTitle'))), /*#__PURE__*/React.createElement("div", { + style: { + flex: 1, + overflow: 'auto', + padding: '0 20px 20px' + } + }, /*#__PURE__*/React.createElement("div", { + style: { + fontSize: 13, + color: 'var(--fg2)', + lineHeight: 1.6, + marginBottom: 16 + } + }, t('contactIntro')), /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + flexDirection: 'column', + gap: 10, + marginBottom: 18 + } + }, channels.map(c => /*#__PURE__*/React.createElement("button", { + key: c.name, + style: { + display: 'flex', + alignItems: 'center', + gap: 13, + width: '100%', + textAlign: 'left', + cursor: 'pointer', + background: c.accent ? 'var(--accent-subtle)' : 'var(--surface)', + border: `1px solid ${c.accent ? 'var(--accent-border)' : 'var(--border)'}`, + borderRadius: 'var(--radius-lg)', + padding: '14px 16px', + boxShadow: 'var(--shadow-sm)' + } + }, /*#__PURE__*/React.createElement("div", { + style: { + width: 40, + height: 40, + borderRadius: 'var(--radius-md)', + background: c.accent ? 'var(--accent)' : 'var(--bg-subtle)', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + flexShrink: 0 + } + }, /*#__PURE__*/React.createElement(Icon, { + name: c.icon, + size: 20, + color: c.accent ? '#fff' : 'var(--accent)' + })), /*#__PURE__*/React.createElement("div", { + style: { + flex: 1, + minWidth: 0 + } + }, /*#__PURE__*/React.createElement("div", { + style: { + fontSize: 15, + fontWeight: 600, + color: 'var(--fg1)' + } + }, c.name), /*#__PURE__*/React.createElement("div", { + style: { + fontSize: 12.5, + color: 'var(--fg3)', + fontFamily: 'var(--font-mono)' + } + }, c.sub)), /*#__PURE__*/React.createElement(Icon, { + name: "external-link", + size: 17, + color: "var(--fg3)" + })))), /*#__PURE__*/React.createElement("div", { + style: { + background: 'var(--surface)', + border: '1px solid var(--border)', + borderRadius: 'var(--radius-lg)', + boxShadow: 'var(--shadow-sm)', + padding: '14px 16px' + } + }, /*#__PURE__*/React.createElement("div", { + style: { + fontSize: 12, + fontWeight: 600, + color: 'var(--fg3)' + } + }, t('contactHoursTitle')), /*#__PURE__*/React.createElement("div", { + style: { + fontSize: 14, + fontWeight: 600, + color: 'var(--fg1)', + marginTop: 5, + fontFamily: 'var(--font-mono)' + } + }, t('contactHours'))))); +} +Object.assign(window, { + AuthFlow, + Onboarding, + RedeemScreen, + PlansScreen, + ContactScreen +}); +})(); } catch (e) { __ds_ns.__errors.push({ path: "ui_kits/mobile/flows.jsx", error: String((e && e.message) || e) }); } + +// ui_kits/mobile/ios-frame.jsx +try { (() => { +/* BEGIN USAGE */ +// iOS.jsx — Simplified iOS 26 (Liquid Glass) device frame +// Based on the iOS 26 UI Kit + Figma status bar spec. No assets, no deps. +// Exports (to window): IOSDevice, IOSStatusBar, IOSNavBar, IOSGlassPill, IOSList, IOSListRow, IOSKeyboard +// +// Usage — wrap your screen content in to get the bezel, status bar +// and home indicator (props: title, dark, keyboard): +// +// +// ...your screen content... +// +// +/* END USAGE */ + +// ───────────────────────────────────────────────────────────── +// Status bar +// ───────────────────────────────────────────────────────────── +function IOSStatusBar({ + dark = false, + time = '9:41' +}) { + const c = dark ? '#fff' : '#000'; + return /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + gap: 154, + alignItems: 'center', + justifyContent: 'center', + padding: '21px 24px 19px', + boxSizing: 'border-box', + position: 'relative', + zIndex: 20, + width: '100%' + } + }, /*#__PURE__*/React.createElement("div", { + style: { + flex: 1, + height: 22, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + paddingTop: 1.5 + } + }, /*#__PURE__*/React.createElement("span", { + style: { + fontFamily: '-apple-system, "SF Pro", system-ui', + fontWeight: 590, + fontSize: 17, + lineHeight: '22px', + color: c + } + }, time)), /*#__PURE__*/React.createElement("div", { + style: { + flex: 1, + height: 22, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + gap: 7, + paddingTop: 1, + paddingRight: 1 + } + }, /*#__PURE__*/React.createElement("svg", { + width: "19", + height: "12", + viewBox: "0 0 19 12" + }, /*#__PURE__*/React.createElement("rect", { + x: "0", + y: "7.5", + width: "3.2", + height: "4.5", + rx: "0.7", + fill: c + }), /*#__PURE__*/React.createElement("rect", { + x: "4.8", + y: "5", + width: "3.2", + height: "7", + rx: "0.7", + fill: c + }), /*#__PURE__*/React.createElement("rect", { + x: "9.6", + y: "2.5", + width: "3.2", + height: "9.5", + rx: "0.7", + fill: c + }), /*#__PURE__*/React.createElement("rect", { + x: "14.4", + y: "0", + width: "3.2", + height: "12", + rx: "0.7", + fill: c + })), /*#__PURE__*/React.createElement("svg", { + width: "17", + height: "12", + viewBox: "0 0 17 12" + }, /*#__PURE__*/React.createElement("path", { + d: "M8.5 3.2C10.8 3.2 12.9 4.1 14.4 5.6L15.5 4.5C13.7 2.7 11.2 1.5 8.5 1.5C5.8 1.5 3.3 2.7 1.5 4.5L2.6 5.6C4.1 4.1 6.2 3.2 8.5 3.2Z", + fill: c + }), /*#__PURE__*/React.createElement("path", { + d: "M8.5 6.8C9.9 6.8 11.1 7.3 12 8.2L13.1 7.1C11.8 5.9 10.2 5.1 8.5 5.1C6.8 5.1 5.2 5.9 3.9 7.1L5 8.2C5.9 7.3 7.1 6.8 8.5 6.8Z", + fill: c + }), /*#__PURE__*/React.createElement("circle", { + cx: "8.5", + cy: "10.5", + r: "1.5", + fill: c + })), /*#__PURE__*/React.createElement("svg", { + width: "27", + height: "13", + viewBox: "0 0 27 13" + }, /*#__PURE__*/React.createElement("rect", { + x: "0.5", + y: "0.5", + width: "23", + height: "12", + rx: "3.5", + stroke: c, + strokeOpacity: "0.35", + fill: "none" + }), /*#__PURE__*/React.createElement("rect", { + x: "2", + y: "2", + width: "20", + height: "9", + rx: "2", + fill: c + }), /*#__PURE__*/React.createElement("path", { + d: "M25 4.5V8.5C25.8 8.2 26.5 7.2 26.5 6.5C26.5 5.8 25.8 4.8 25 4.5Z", + fill: c, + fillOpacity: "0.4" + })))); +} + +// ───────────────────────────────────────────────────────────── +// Liquid glass pill — blur + tint + shine +// ───────────────────────────────────────────────────────────── +function IOSGlassPill({ + children, + dark = false, + style = {} +}) { + return /*#__PURE__*/React.createElement("div", { + style: { + height: 44, + minWidth: 44, + borderRadius: 9999, + position: 'relative', + overflow: 'hidden', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + boxShadow: dark ? '0 2px 6px rgba(0,0,0,0.35), 0 6px 16px rgba(0,0,0,0.2)' : '0 1px 3px rgba(0,0,0,0.07), 0 3px 10px rgba(0,0,0,0.06)', + ...style + } + }, /*#__PURE__*/React.createElement("div", { + style: { + position: 'absolute', + inset: 0, + borderRadius: 9999, + backdropFilter: 'blur(12px) saturate(180%)', + WebkitBackdropFilter: 'blur(12px) saturate(180%)', + background: dark ? 'rgba(120,120,128,0.28)' : 'rgba(255,255,255,0.5)' + } + }), /*#__PURE__*/React.createElement("div", { + style: { + position: 'absolute', + inset: 0, + borderRadius: 9999, + boxShadow: dark ? 'inset 1.5px 1.5px 1px rgba(255,255,255,0.15), inset -1px -1px 1px rgba(255,255,255,0.08)' : 'inset 1.5px 1.5px 1px rgba(255,255,255,0.7), inset -1px -1px 1px rgba(255,255,255,0.4)', + border: dark ? '0.5px solid rgba(255,255,255,0.15)' : '0.5px solid rgba(0,0,0,0.06)' + } + }), /*#__PURE__*/React.createElement("div", { + style: { + position: 'relative', + zIndex: 1, + display: 'flex', + alignItems: 'center', + padding: '0 4px' + } + }, children)); +} + +// ───────────────────────────────────────────────────────────── +// Navigation bar — glass pills + large title +// ───────────────────────────────────────────────────────────── +function IOSNavBar({ + title = 'Title', + dark = false, + trailingIcon = true +}) { + const muted = dark ? 'rgba(255,255,255,0.6)' : '#404040'; + const text = dark ? '#fff' : '#000'; + const pillIcon = content => /*#__PURE__*/React.createElement(IOSGlassPill, { + dark: dark + }, /*#__PURE__*/React.createElement("div", { + style: { + width: 36, + height: 36, + display: 'flex', + alignItems: 'center', + justifyContent: 'center' + } + }, content)); + return /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + flexDirection: 'column', + gap: 10, + paddingTop: 62, + paddingBottom: 10, + position: 'relative', + zIndex: 5 + } + }, /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + padding: '0 16px' + } + }, pillIcon(/*#__PURE__*/React.createElement("svg", { + width: "12", + height: "20", + viewBox: "0 0 12 20", + fill: "none", + style: { + marginLeft: -1 + } + }, /*#__PURE__*/React.createElement("path", { + d: "M10 2L2 10l8 8", + stroke: muted, + strokeWidth: "2.5", + strokeLinecap: "round", + strokeLinejoin: "round" + }))), trailingIcon && pillIcon(/*#__PURE__*/React.createElement("svg", { + width: "22", + height: "6", + viewBox: "0 0 22 6" + }, /*#__PURE__*/React.createElement("circle", { + cx: "3", + cy: "3", + r: "2.5", + fill: muted + }), /*#__PURE__*/React.createElement("circle", { + cx: "11", + cy: "3", + r: "2.5", + fill: muted + }), /*#__PURE__*/React.createElement("circle", { + cx: "19", + cy: "3", + r: "2.5", + fill: muted + })))), /*#__PURE__*/React.createElement("div", { + style: { + padding: '0 16px', + fontFamily: '-apple-system, system-ui', + fontSize: 34, + fontWeight: 700, + lineHeight: '41px', + color: text, + letterSpacing: 0.4 + } + }, title)); +} + +// ───────────────────────────────────────────────────────────── +// Grouped list (inset card, r:26) + row (52px) +// ───────────────────────────────────────────────────────────── +function IOSListRow({ + title, + detail, + icon, + chevron = true, + isLast = false, + dark = false +}) { + const text = dark ? '#fff' : '#000'; + const sec = dark ? 'rgba(235,235,245,0.6)' : 'rgba(60,60,67,0.6)'; + const ter = dark ? 'rgba(235,235,245,0.3)' : 'rgba(60,60,67,0.3)'; + const sep = dark ? 'rgba(84,84,88,0.65)' : 'rgba(60,60,67,0.12)'; + return /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + alignItems: 'center', + minHeight: 52, + padding: '0 16px', + position: 'relative', + fontFamily: '-apple-system, system-ui', + fontSize: 17, + letterSpacing: -0.43 + } + }, icon && /*#__PURE__*/React.createElement("div", { + style: { + width: 30, + height: 30, + borderRadius: 7, + background: icon, + marginRight: 12, + flexShrink: 0 + } + }), /*#__PURE__*/React.createElement("div", { + style: { + flex: 1, + color: text + } + }, title), detail && /*#__PURE__*/React.createElement("span", { + style: { + color: sec, + marginRight: 6 + } + }, detail), chevron && /*#__PURE__*/React.createElement("svg", { + width: "8", + height: "14", + viewBox: "0 0 8 14", + style: { + flexShrink: 0 + } + }, /*#__PURE__*/React.createElement("path", { + d: "M1 1l6 6-6 6", + stroke: ter, + strokeWidth: "2", + fill: "none", + strokeLinecap: "round", + strokeLinejoin: "round" + })), !isLast && /*#__PURE__*/React.createElement("div", { + style: { + position: 'absolute', + bottom: 0, + right: 0, + left: icon ? 58 : 16, + height: 0.5, + background: sep + } + })); +} +function IOSList({ + header, + children, + dark = false +}) { + const hc = dark ? 'rgba(235,235,245,0.6)' : 'rgba(60,60,67,0.6)'; + const bg = dark ? '#1C1C1E' : '#fff'; + return /*#__PURE__*/React.createElement("div", null, header && /*#__PURE__*/React.createElement("div", { + style: { + fontFamily: '-apple-system, system-ui', + fontSize: 13, + color: hc, + textTransform: 'uppercase', + padding: '8px 36px 6px', + letterSpacing: -0.08 + } + }, header), /*#__PURE__*/React.createElement("div", { + style: { + background: bg, + borderRadius: 26, + margin: '0 16px', + overflow: 'hidden' + } + }, children)); +} + +// ───────────────────────────────────────────────────────────── +// Device frame +// ───────────────────────────────────────────────────────────── +function IOSDevice({ + children, + width = 402, + height = 874, + dark = false, + title, + keyboard = false +}) { + return /*#__PURE__*/React.createElement("div", { + style: { + width, + height, + borderRadius: 48, + overflow: 'hidden', + position: 'relative', + background: dark ? '#000' : '#F2F2F7', + boxShadow: '0 40px 80px rgba(0,0,0,0.18), 0 0 0 1px rgba(0,0,0,0.12)', + fontFamily: '-apple-system, system-ui, sans-serif', + WebkitFontSmoothing: 'antialiased' + } + }, /*#__PURE__*/React.createElement("div", { + style: { + position: 'absolute', + top: 11, + left: '50%', + transform: 'translateX(-50%)', + width: 126, + height: 37, + borderRadius: 24, + background: '#000', + zIndex: 50 + } + }), /*#__PURE__*/React.createElement("div", { + style: { + position: 'absolute', + top: 0, + left: 0, + right: 0, + zIndex: 10 + } + }, /*#__PURE__*/React.createElement(IOSStatusBar, { + dark: dark + })), /*#__PURE__*/React.createElement("div", { + style: { + height: '100%', + display: 'flex', + flexDirection: 'column' + } + }, title !== undefined && /*#__PURE__*/React.createElement(IOSNavBar, { + title: title, + dark: dark + }), /*#__PURE__*/React.createElement("div", { + style: { + flex: 1, + overflow: 'auto' + } + }, children), keyboard && /*#__PURE__*/React.createElement(IOSKeyboard, { + dark: dark + })), /*#__PURE__*/React.createElement("div", { + style: { + position: 'absolute', + bottom: 0, + left: 0, + right: 0, + zIndex: 60, + height: 34, + display: 'flex', + justifyContent: 'center', + alignItems: 'flex-end', + paddingBottom: 8, + pointerEvents: 'none' + } + }, /*#__PURE__*/React.createElement("div", { + style: { + width: 139, + height: 5, + borderRadius: 100, + background: dark ? 'rgba(255,255,255,0.7)' : 'rgba(0,0,0,0.25)' + } + }))); +} + +// ───────────────────────────────────────────────────────────── +// Keyboard — iOS 26 liquid glass +// ───────────────────────────────────────────────────────────── +function IOSKeyboard({ + dark = false +}) { + const glyph = dark ? 'rgba(255,255,255,0.7)' : '#595959'; + const sugg = dark ? 'rgba(255,255,255,0.6)' : '#333'; + const keyBg = dark ? 'rgba(255,255,255,0.22)' : 'rgba(255,255,255,0.85)'; + + // special-key icons + const icons = { + shift: /*#__PURE__*/React.createElement("svg", { + width: "19", + height: "17", + viewBox: "0 0 19 17" + }, /*#__PURE__*/React.createElement("path", { + d: "M9.5 1L1 9.5h4.5V16h8V9.5H18L9.5 1z", + fill: glyph + })), + del: /*#__PURE__*/React.createElement("svg", { + width: "23", + height: "17", + viewBox: "0 0 23 17" + }, /*#__PURE__*/React.createElement("path", { + d: "M7 1h13a2 2 0 012 2v11a2 2 0 01-2 2H7l-6-7.5L7 1z", + fill: "none", + stroke: glyph, + strokeWidth: "1.6", + strokeLinejoin: "round" + }), /*#__PURE__*/React.createElement("path", { + d: "M10 5l7 7M17 5l-7 7", + stroke: glyph, + strokeWidth: "1.6", + strokeLinecap: "round" + })), + ret: /*#__PURE__*/React.createElement("svg", { + width: "20", + height: "14", + viewBox: "0 0 20 14" + }, /*#__PURE__*/React.createElement("path", { + d: "M18 1v6H4m0 0l4-4M4 7l4 4", + fill: "none", + stroke: "#fff", + strokeWidth: "1.8", + strokeLinecap: "round", + strokeLinejoin: "round" + })) + }; + const key = (content, { + w, + flex, + ret, + fs = 25, + k + } = {}) => /*#__PURE__*/React.createElement("div", { + key: k, + style: { + height: 42, + borderRadius: 8.5, + flex: flex ? 1 : undefined, + width: w, + minWidth: 0, + background: ret ? '#08f' : keyBg, + boxShadow: '0 1px 0 rgba(0,0,0,0.075)', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + fontFamily: '-apple-system, "SF Compact", system-ui', + fontSize: fs, + fontWeight: 458, + color: ret ? '#fff' : glyph + } + }, content); + const row = (keys, pad = 0) => /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + gap: 6.5, + justifyContent: 'center', + padding: `0 ${pad}px` + } + }, keys.map(l => key(l, { + flex: true, + k: l + }))); + return /*#__PURE__*/React.createElement("div", { + style: { + position: 'relative', + zIndex: 15, + borderRadius: 27, + overflow: 'hidden', + padding: '11px 0 2px', + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + boxShadow: dark ? '0 -2px 20px rgba(0,0,0,0.09)' : '0 -1px 6px rgba(0,0,0,0.018), 0 -3px 20px rgba(0,0,0,0.012)' + } + }, /*#__PURE__*/React.createElement("div", { + style: { + position: 'absolute', + inset: 0, + borderRadius: 27, + backdropFilter: 'blur(12px) saturate(180%)', + WebkitBackdropFilter: 'blur(12px) saturate(180%)', + background: dark ? 'rgba(120,120,128,0.14)' : 'rgba(255,255,255,0.25)' + } + }), /*#__PURE__*/React.createElement("div", { + style: { + position: 'absolute', + inset: 0, + borderRadius: 27, + boxShadow: dark ? 'inset 1.5px 1.5px 1px rgba(255,255,255,0.15)' : 'inset 1.5px 1.5px 1px rgba(255,255,255,0.7), inset -1px -1px 1px rgba(255,255,255,0.4)', + border: dark ? '0.5px solid rgba(255,255,255,0.15)' : '0.5px solid rgba(0,0,0,0.06)', + pointerEvents: 'none' + } + }), /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + gap: 20, + alignItems: 'center', + padding: '8px 22px 13px', + width: '100%', + boxSizing: 'border-box', + position: 'relative' + } + }, ['"The"', 'the', 'to'].map((w, i) => /*#__PURE__*/React.createElement(React.Fragment, { + key: i + }, i > 0 && /*#__PURE__*/React.createElement("div", { + style: { + width: 1, + height: 25, + background: '#ccc', + opacity: 0.3 + } + }), /*#__PURE__*/React.createElement("div", { + style: { + flex: 1, + textAlign: 'center', + fontFamily: '-apple-system, system-ui', + fontSize: 17, + color: sugg, + letterSpacing: -0.43, + lineHeight: '22px' + } + }, w)))), /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + flexDirection: 'column', + gap: 13, + padding: '0 6.5px', + width: '100%', + boxSizing: 'border-box', + position: 'relative' + } + }, row(['q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p']), row(['a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l'], 20), /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + gap: 14.25, + alignItems: 'center' + } + }, key(icons.shift, { + w: 45, + k: 'shift' + }), /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + gap: 6.5, + flex: 1 + } + }, ['z', 'x', 'c', 'v', 'b', 'n', 'm'].map(l => key(l, { + flex: true, + k: l + }))), key(icons.del, { + w: 45, + k: 'del' + })), /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + gap: 6, + alignItems: 'center' + } + }, key('ABC', { + w: 92.25, + fs: 18, + k: 'abc' + }), key('', { + flex: true, + k: 'space' + }), key(icons.ret, { + w: 92.25, + ret: true, + k: 'ret' + }))), /*#__PURE__*/React.createElement("div", { + style: { + height: 56, + width: '100%', + position: 'relative' + } + })); +} +Object.assign(window, { + IOSDevice, + IOSStatusBar, + IOSNavBar, + IOSGlassPill, + IOSList, + IOSListRow, + IOSKeyboard +}); +})(); } catch (e) { __ds_ns.__errors.push({ path: "ui_kits/mobile/ios-frame.jsx", error: String((e && e.message) || e) }); } + +// ui_kits/mobile/parts.jsx +try { (() => { +/* parts.jsx — 穿山甲 VPN mobile · shared atoms + Icon uses inline Lucide path data (lucide.dev) for reliability in React. */ + +const LUCIDE = { + power: '', + 'shield-check': '', + globe: '', + settings: '', + 'chevron-right': '', + 'chevron-down': '', + check: '', + zap: '', + 'loader-circle': '', + user: '', + x: '', + crown: '', + 'map-pin': '', + 'arrow-down': '', + 'arrow-up': '', + search: '', + moon: '', + sun: '', + shield: '', + 'refresh-cw': '', + chart: '', + wifi: '', + mail: '', + lock: '', + 'arrow-left': '', + 'shopping-bag': '', + send: '', + 'message-circle': '', + 'external-link': '', + 'check-circle': '', + 'log-out': '', + compass: '', + laptop: '', + smartphone: '', + 'monitor-smartphone': '', + ticket: '', + clock: '', + 'credit-card': '', + 'play-circle': '' +}; +function Icon({ + name, + size = 24, + stroke = 2, + color = 'currentColor', + style = {} +}) { + return /*#__PURE__*/React.createElement("svg", { + width: size, + height: size, + viewBox: "0 0 24 24", + fill: "none", + stroke: color, + strokeWidth: stroke, + strokeLinecap: "round", + strokeLinejoin: "round", + style: { + display: 'block', + flexShrink: 0, + ...style + }, + dangerouslySetInnerHTML: { + __html: LUCIDE[name] || '' + } + }); +} + +/* Brand mark — walking pangolin (matches app icon), facing right */ +function Mark({ + size = 40, + color = 'var(--accent)' +}) { + return /*#__PURE__*/React.createElement("svg", { + width: size, + height: size, + viewBox: "0 0 96 96", + fill: "none", + style: { + display: 'block' + } + }, /*#__PURE__*/React.createElement("g", { + transform: "translate(96,0) scale(-1,1)" + }, /*#__PURE__*/React.createElement("path", { + d: "M16 59 Q17 51 26 50 Q34 31 45 30 Q55 30 62 37 Q69 41 75 45 Q85 49 89 44 Q92 50 84 52 Q75 53 67 53 Q66 62 60 62 L56 62 Q54 55 49 55 Q47 62 37 62 L33 62 Q31 56 26 56 Q20 59 16 59 Z", + fill: color + }), /*#__PURE__*/React.createElement("g", { + stroke: "#fff", + strokeWidth: "2.4", + strokeLinecap: "round", + strokeLinejoin: "round", + fill: "none", + opacity: "0.5" + }, /*#__PURE__*/React.createElement("path", { + d: "M26 50 Q34 43 42 50" + }), /*#__PURE__*/React.createElement("path", { + d: "M36 48 Q44 41 52 48" + }), /*#__PURE__*/React.createElement("path", { + d: "M46 48 Q54 41 62 49" + })), /*#__PURE__*/React.createElement("circle", { + cx: "23", + cy: "55", + r: "2.2", + fill: "#fff", + opacity: "0.85" + }))); +} + +/* Country code tile */ +function CC({ + code, + active +}) { + return /*#__PURE__*/React.createElement("div", { + style: { + width: 38, + height: 38, + borderRadius: 'var(--radius-md)', + flexShrink: 0, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + fontFamily: 'var(--font-mono)', + fontSize: 12, + fontWeight: 600, + background: active ? 'var(--accent)' : 'var(--bg-subtle)', + color: active ? '#fff' : 'var(--fg2)' + } + }, code); +} + +/* Signal bars from ping */ +function Signal({ + ping +}) { + const lvl = ping < 40 ? 3 : ping < 90 ? 2 : 1; + return /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + gap: 2, + alignItems: 'flex-end', + height: 14 + } + }, [5, 9, 14].map((h, i) => /*#__PURE__*/React.createElement("div", { + key: i, + style: { + width: 3, + height: h, + borderRadius: 1, + background: i < lvl ? 'var(--success)' : 'var(--border-strong)' + } + }))); +} + +/* Pill button */ +function Pill({ + children, + variant = 'primary', + onClick, + style = {}, + disabled +}) { + const v = { + primary: { + background: 'var(--accent)', + color: 'var(--fg-on-accent)' + }, + secondary: { + background: 'var(--surface)', + color: 'var(--fg1)', + border: '1.5px solid var(--border-strong)' + }, + ghost: { + background: 'transparent', + color: 'var(--accent)' + }, + danger: { + background: 'var(--danger-subtle)', + color: 'var(--danger)' + } + }[variant]; + return /*#__PURE__*/React.createElement("button", { + onClick: onClick, + disabled: disabled, + style: { + display: 'inline-flex', + alignItems: 'center', + justifyContent: 'center', + gap: 8, + fontFamily: 'var(--font-sans)', + fontWeight: 600, + fontSize: 15, + border: 'none', + borderRadius: 'var(--radius-full)', + padding: '13px 22px', + cursor: disabled ? 'not-allowed' : 'pointer', + transition: 'all 140ms var(--ease-out)', + opacity: disabled ? 0.5 : 1, + ...v, + ...style + } + }, children); +} + +/* ───────── i18n ───────── + Real product shows ONE language at a time (toggle in settings). + Bilingual specimens only live in the design-system preview cards. */ +const STRINGS = { + brand: { + zh: '穿山甲', + en: 'Pangolin' + }, + online: { + zh: '● 在线', + en: '● Online' + }, + offline: { + zh: '○ 离线', + en: '○ Offline' + }, + capOff: { + zh: '未连接 · 轻点连接', + en: 'Tap to connect' + }, + capConnecting: { + zh: '连接中…', + en: 'Connecting…' + }, + capOn: { + zh: '已连接 · 网络已加密', + en: 'Connected · Encrypted' + }, + centerOff: { + zh: 'TAP', + en: 'TAP' + }, + download: { + zh: '下载', + en: 'Down' + }, + upload: { + zh: '上传', + en: 'Up' + }, + latency: { + zh: '延迟', + en: 'Ping' + }, + chooseNode: { + zh: '选择节点', + en: 'Choose server' + }, + search: { + zh: '搜索国家 / 城市', + en: 'Search country / city' + }, + tabConnect: { + zh: '连接', + en: 'Connect' + }, + tabServers: { + zh: '节点', + en: 'Servers' + }, + tabStats: { + zh: '统计', + en: 'Stats' + }, + tabMe: { + zh: '我的', + en: 'Account' + }, + connectNow: { + zh: '点击连接', + en: 'CONNECT' + }, + secure: { + zh: '已加密', + en: 'SECURE' + }, + statsTitle: { + zh: '使用统计', + en: 'Statistics' + }, + trafficMonth: { + zh: '本月流量', + en: 'Traffic' + }, + avgPing: { + zh: '平均延迟', + en: 'Avg ping' + }, + durMonth: { + zh: '本月时长', + en: 'Time' + }, + weekTraffic: { + zh: '本周流量 (GB)', + en: 'This week (GB)' + }, + days7: { + zh: ['一', '二', '三', '四', '五', '六', '日'], + en: ['M', 'T', 'W', 'T', 'F', 'S', 'S'] + }, + proMember: { + zh: 'PRO 会员', + en: 'PRO member' + }, + expires: { + zh: '有效期至', + en: 'Expires' + }, + usage: { + zh: '本月用量', + en: 'Used this month' + }, + smartRoute: { + zh: '智能分流', + en: 'Smart routing' + }, + smartRouteSub: { + zh: '海外应用加速,本地直连', + en: 'Accelerate overseas apps, keep local direct' + }, + killSwitch: { + zh: 'Kill Switch', + en: 'Kill Switch' + }, + killSwitchSub: { + zh: '断线时阻断网络,防止泄露', + en: 'Block traffic if the VPN drops' + }, + darkAppearance: { + zh: '深色外观', + en: 'Dark appearance' + }, + stateOn: { + zh: '已开启', + en: 'On' + }, + followLight: { + zh: '跟随浅色', + en: 'Off' + }, + protocol: { + zh: '协议', + en: 'Protocol' + }, + checkUpdate: { + zh: '检查更新', + en: 'Check for updates' + }, + language: { + zh: '语言', + en: 'Language' + }, + /* auth */ + authTagline: { + zh: '极速 · 稳定 · 省心', + en: 'Fast · Stable · Effortless' + }, + tabLogin: { + zh: '登录', + en: 'Log in' + }, + tabRegister: { + zh: '注册', + en: 'Sign up' + }, + emailLabel: { + zh: '邮箱', + en: 'Email' + }, + emailPh: { + zh: '你的邮箱地址', + en: 'your@email.com' + }, + pwLabel: { + zh: '密码', + en: 'Password' + }, + pwPh: { + zh: '输入密码', + en: 'Enter password' + }, + setPwPh: { + zh: '设置登录密码(用于多端登录)', + en: 'Set a password (for multi-device login)' + }, + codeLabel: { + zh: '邮箱验证码', + en: 'Verification code' + }, + codeSentTo: { + zh: '验证码已发送至', + en: 'Code sent to' + }, + sendCode: { + zh: '发送验证码', + en: 'Send code' + }, + resend: { + zh: '重新发送', + en: 'Resend' + }, + doLogin: { + zh: '登录', + en: 'Log in' + }, + doNext: { + zh: '下一步', + en: 'Next' + }, + doCreate: { + zh: '创建账户', + en: 'Create account' + }, + forgotPw: { + zh: '忘记密码?', + en: 'Forgot password?' + }, + tos: { + zh: '继续即代表同意《服务条款》与《隐私政策》', + en: 'By continuing you agree to our Terms & Privacy Policy' + }, + stepEmail: { + zh: '验证邮箱', + en: 'Verify email' + }, + stepPw: { + zh: '设置密码', + en: 'Set password' + }, + /* onboarding */ + obSkip: { + zh: '跳过', + en: 'Skip' + }, + obNext: { + zh: '下一步', + en: 'Next' + }, + obAllow: { + zh: '允许并继续', + en: 'Allow & continue' + }, + obStart: { + zh: '开始使用', + en: 'Get started' + }, + ob1Title: { + zh: '一键连接全球', + en: 'Connect worldwide' + }, + ob1Sub: { + zh: '80+ 节点,智能选择最快线路,稳定不掉线。', + en: '80+ locations, auto-fastest routing, rock-solid.' + }, + ob2Title: { + zh: '授权网络配置', + en: 'Allow network setup' + }, + ob2Sub: { + zh: '系统会请求添加一个网络配置,用于建立加密隧道。', + en: 'iOS will ask to add a network profile to build the encrypted tunnel.' + }, + ob3Title: { + zh: '安全 · 无日志', + en: 'Private by design' + }, + ob3Sub: { + zh: '端到端加密,我们不记录你的任何浏览数据。', + en: 'End-to-end encrypted. We keep zero browsing logs.' + }, + /* redeem & buy */ + redeemEntry: { + zh: '兑换 & 购买', + en: 'Redeem & buy' + }, + redeemTitle: { + zh: '兑换 & 购买', + en: 'Redeem & buy' + }, + redeemCodeTitle: { + zh: '兑换激活码', + en: 'Redeem a code' + }, + redeemPh: { + zh: '输入激活码', + en: 'Enter activation code' + }, + redeemBtn: { + zh: '激活', + en: 'Activate' + }, + redeemOk: { + zh: '激活成功 · PRO 已开通', + en: 'Activated · PRO unlocked' + }, + buyTitle: { + zh: '购买渠道', + en: 'Where to buy' + }, + buySub: { + zh: 'App 内不支持直接支付。请通过以下渠道获取激活码:', + en: 'In-app payment is unavailable. Get an activation code via:' + }, + chStore: { + zh: '自助发卡商店', + en: 'Self-serve store' + }, + chStoreSub: { + zh: '支付宝 / 微信 · 自动发码', + en: 'Alipay / WeChat · instant code' + }, + chUsdtSub: { + zh: '链上转账 · 最隐私 · 自动发码', + en: 'On-chain · most private · instant code' + }, + chTgSub: { + zh: '@PangolinVPN_bot', + en: '@PangolinVPN_bot' + }, + chLineSub: { + zh: '@pangolinvpn', + en: '@pangolinvpn' + }, + chEmail: { + zh: '邮箱', + en: 'Email' + }, + chEmailSub: { + zh: 'buy@pangolin.vpn', + en: 'buy@pangolin.vpn' + }, + back: { + zh: '返回', + en: 'Back' + }, + replayAuth: { + zh: '查看登录 / 注册流程', + en: 'View login / sign-up' + }, + twoFA: { + zh: '双重认证', + en: 'Two-factor auth' + }, + twoFASub: { + zh: 'PRO 专享 · 登录需身份验证器动态码', + en: 'PRO only · TOTP code at login' + }, + twoFATitle: { + zh: '双重认证', + en: 'Two-factor auth' + }, + twoFAHint: { + zh: '你的账户已开启双重认证。请输入身份验证器 App 中的 6 位动态码。', + en: 'Two-factor auth is on for this account. Enter the 6-digit code from your authenticator app.' + }, + twoFAConfirm: { + zh: '验证并登录', + en: 'Verify & log in' + }, + twoFABack: { + zh: '返回上一步', + en: 'Back' + }, + ucEntry: { + zh: 'Web 用户中心', + en: 'Web account center' + }, + ucEntrySub: { + zh: '订阅导入 · 邀请返利', + en: 'Subscription import · referral' + }, + replayAuthSub: { + zh: '演示用 · 退出到登录页', + en: 'Demo · back to login' + }, + replayOnboarding: { + zh: '查看首次引导', + en: 'View onboarding' + }, + replayOnboardingSub: { + zh: '演示用 · 重看 3 屏引导', + en: 'Demo · replay the 3 intro screens' + }, + /* account management (integrated) */ + meTitle: { + zh: '我的', + en: 'Account' + }, + curPlan: { + zh: '当前套餐', + en: 'Current plan' + }, + expires: { + zh: '有效期至', + en: 'Expires' + }, + upgradeBtn: { + zh: '续费 / 升级', + en: 'Renew / Upgrade' + }, + accInfoTitle: { + zh: '账户信息', + en: 'Account info' + }, + accEmail: { + zh: '邮箱', + en: 'Email' + }, + accPassword: { + zh: '密码', + en: 'Password' + }, + accChange: { + zh: '修改', + en: 'Change' + }, + accSignOut: { + zh: '退出登录', + en: 'Sign out' + }, + myDevices: { + zh: '我的设备', + en: 'My devices' + }, + devicesSub: { + zh: 'PRO 套餐最多 5 台设备同时在线', + en: 'Up to 5 devices on PRO' + }, + thisDevice: { + zh: '当前设备', + en: 'This device' + }, + lastActive: { + zh: '最近活跃', + en: 'Last active' + }, + remove: { + zh: '移除', + en: 'Remove' + }, + choosePlan: { + zh: '选择套餐', + en: 'Choose plan' + }, + free: { + zh: '免费版', + en: 'Free' + }, + proPlan: { + zh: '专业版', + en: 'Pro' + }, + team: { + zh: '团队版', + en: 'Team' + }, + perMonth: { + zh: '/月', + en: '/mo' + }, + current: { + zh: '当前', + en: 'Current' + }, + upgrade: { + zh: '立即升级', + en: 'Upgrade' + }, + choose: { + zh: '选择', + en: 'Choose' + }, + mostPopular: { + zh: '最受欢迎', + en: 'Popular' + }, + feFree: { + zh: ['仅 1 个基础节点', '每日 10 分钟时长', '使用前观看广告', '注册享 7 天免费试用'], + en: ['1 basic node only', '10 min per day', 'Watch an ad to start', '7-day free trial on sign-up'] + }, + fePro: { + zh: ['80+ 全球节点', '无限流量 · 极速', '5 台设备同时在线', '流媒体优化'], + en: ['80+ locations', 'Unlimited · fast', '5 devices', 'Streaming optimized'] + }, + feTeam: { + zh: ['Pro 全部功能', '10 个成员席位', '集中计费与管理', '优先客服'], + en: ['Everything in Pro', '10 seats', 'Central billing', 'Priority support'] + }, + /* contact */ + contactEntry: { + zh: '联系我们', + en: 'Contact us' + }, + contactTitle: { + zh: '联系我们', + en: 'Contact us' + }, + contactIntro: { + zh: '遇到问题?通过以下任一渠道联系我们,通常数分钟内回复。', + en: 'Need help? Reach us via any channel below — usually replies in minutes.' + }, + contactEmail: { + zh: '邮箱客服', + en: 'Email support' + }, + contactStore: { + zh: '自助发卡商店', + en: 'Self-serve store' + }, + contactHoursTitle: { + zh: '服务时间', + en: 'Support hours' + }, + contactHours: { + zh: '每日 9:00 – 24:00 (GMT+8)', + en: 'Daily 9:00 – 24:00 (GMT+8)' + }, + devicesCount: { + zh: '台设备', + en: 'devices' + }, + /* smart select + quota (口径见 CLAUDE.md §7) */ + smartSelect: { + zh: '智能选择', + en: 'Smart select' + }, + smartSub: { + zh: '根据当前网络环境,自动选择最优节点', + en: 'Picks the best node for your network' + }, + recommended: { + zh: '推荐', + en: 'Recommended' + }, + quotaToday: { + zh: '今日剩余', + en: 'Left today' + }, + minutes: { + zh: '分钟', + en: 'min' + }, + quotaFree: { + zh: '免费版 · 每日 10 分钟', + en: 'Free · 10 min/day' + }, + quotaTrial: { + zh: '体验期 · 7 天免费使用', + en: 'Trial · 7 days free' + }, + watchAd: { + zh: '看广告开始使用', + en: 'Watch ad to start' + }, + adUnlocked: { + zh: '已解锁 · 今日可用', + en: 'Unlocked for today' + }, + freePlanName: { + zh: '免费版', + en: 'Free' + }, + upgradeNow: { + zh: '升级 PRO', + en: 'Go PRO' + }, + freeDemo: { + zh: '演示:免费版视角', + en: 'Demo: free-tier view' + }, + freeDemoSub: { + zh: '切换连接页的额度展示与套餐横幅', + en: 'Toggles quota UI & plan banner' + } +}; +function makeT(lang) { + return k => STRINGS[k] && STRINGS[k][lang] || k; +} +/* localized server label */ +function srvName(s, lang) { + return lang === 'zh' ? s.name : s.en; +} +function srvSub(s, lang) { + const tag = s.tags && s.tags[0]; + if (lang === 'zh') return tag ? tag : s.en; + return tag === '流媒体优化' ? 'Streaming' : tag === 'P2P' ? 'P2P' : s.name; +} +Object.assign(window, { + Icon, + Mark, + CC, + Signal, + Pill, + STRINGS, + makeT, + srvName, + srvSub, + SERVERS: [{ + code: 'HK', + name: '香港 · 流媒体', + en: 'Hong Kong', + ping: 18, + tags: ['流媒体优化'] + }, { + code: 'JP', + name: '日本 东京', + en: 'Tokyo', + ping: 32, + tags: ['P2P'] + }, { + code: 'SG', + name: '新加坡', + en: 'Singapore', + ping: 54, + tags: ['P2P'] + }, { + code: 'TW', + name: '台湾 台北', + en: 'Taipei', + ping: 28, + tags: [] + }, { + code: 'US', + name: '美国 洛杉矶', + en: 'Los Angeles', + ping: 146, + tags: ['流媒体优化'] + }, { + code: 'DE', + name: '德国 法兰克福', + en: 'Frankfurt', + ping: 198, + tags: [] + }, { + code: 'UK', + name: '英国 伦敦', + en: 'London', + ping: 210, + tags: [] + }, { + code: 'KR', + name: '韩国 首尔', + en: 'Seoul', + ping: 41, + tags: [] + }] +}); +})(); } catch (e) { __ds_ns.__errors.push({ path: "ui_kits/mobile/parts.jsx", error: String((e && e.message) || e) }); } + +// ui_kits/mobile/screens.jsx +try { (() => { +/* screens.jsx — 穿山甲 VPN mobile · screens + bottom tab (i18n) */ + +const { + useState, + useEffect, + useRef +} = React; + +/* ───────── Top brand bar ───────── */ +function TopBar({ + t, + lang, + right +}) { + return /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + padding: '6px 20px 14px' + } + }, /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + alignItems: 'center', + gap: 9 + } + }, /*#__PURE__*/React.createElement(Mark, { + size: 28 + }), /*#__PURE__*/React.createElement("span", { + style: { + fontFamily: 'var(--font-display)', + fontWeight: 700, + fontSize: 18, + color: 'var(--fg1)', + letterSpacing: '-0.01em' + } + }, t('brand'))), right); +} + +/* ───────── CONNECT screen ───────── */ +function ConnectScreen({ + t, + lang, + status, + onToggle, + server, + smart, + free, + mins, + adDone, + onAd, + goServers, + elapsed +}) { + const map = { + off: { + fill: 'var(--sand-100)', + fg: 'var(--clay-500)', + icon: 'power', + cap: t('capOff'), + ring: 'track' + }, + connecting: { + fill: 'var(--accent)', + fg: '#fff', + icon: 'loader-circle', + cap: t('capConnecting'), + ring: 'spin' + }, + on: { + fill: 'var(--success)', + fg: '#fff', + icon: 'shield-check', + cap: t('capOn'), + ring: 'full' + } + }[status]; + const fmt = s => `${String(Math.floor(s / 3600)).padStart(2, '0')}:${String(Math.floor(s % 3600 / 60)).padStart(2, '0')}:${String(s % 60).padStart(2, '0')}`; + const shadow = status === 'off' ? 'inset 0 0 0 1px var(--sand-200), var(--shadow-md)' : `0 0 0 9px ${status === 'on' ? 'var(--success-subtle)' : 'var(--accent-subtle)'}, var(--shadow-lg)`; + // in-circle center block + const center = status === 'off' ? /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("span", { + style: { + fontFamily: 'var(--font-sans)', + fontWeight: 700, + fontSize: 14 + } + }, t('connectNow'))) : status === 'connecting' ? /*#__PURE__*/React.createElement("span", { + style: { + fontFamily: 'var(--font-mono)', + fontWeight: 700, + fontSize: 14, + letterSpacing: '0.08em' + } + }, "\xB7\xB7\xB7") : /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("span", { + style: { + fontFamily: 'var(--font-mono)', + fontWeight: 600, + fontSize: 18 + } + }, fmt(elapsed)), /*#__PURE__*/React.createElement("span", { + style: { + fontFamily: 'var(--font-sans)', + fontWeight: 700, + fontSize: 10, + letterSpacing: '0.1em', + opacity: .9 + } + }, t('secure'))); + return /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + flexDirection: 'column', + height: '100%' + } + }, /*#__PURE__*/React.createElement(TopBar, { + t: t, + lang: lang, + right: /*#__PURE__*/React.createElement("span", { + style: { + fontFamily: 'var(--font-mono)', + fontSize: 12, + whiteSpace: 'nowrap', + color: status === 'on' ? 'var(--success)' : 'var(--fg3)', + fontWeight: 600 + } + }, status === 'on' ? t('online') : t('offline')) + }), /*#__PURE__*/React.createElement("div", { + style: { + flex: 1, + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'center', + gap: 26, + padding: '0 24px' + } + }, /*#__PURE__*/React.createElement("button", { + onClick: onToggle, + style: { + position: 'relative', + width: 208, + height: 208, + borderRadius: '50%', + border: 'none', + cursor: 'pointer', + background: map.fill, + color: map.fg, + boxShadow: shadow, + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'center', + gap: 6, + transition: 'box-shadow 300ms var(--ease-out), background-color 300ms var(--ease-out)' + } + }, /*#__PURE__*/React.createElement("svg", { + viewBox: "0 0 208 208", + style: { + position: 'absolute', + inset: 0, + width: '100%', + height: '100%', + transformOrigin: 'center', + animation: map.ring === 'spin' ? 'pg-spin 1.4s linear infinite' : 'none' + } + }, map.ring === 'track' && /*#__PURE__*/React.createElement("circle", { + cx: "104", + cy: "104", + r: "96", + fill: "none", + stroke: "var(--sand-200)", + strokeWidth: "3", + strokeDasharray: "2 8", + strokeLinecap: "round" + }), map.ring === 'spin' && /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("circle", { + cx: "104", + cy: "104", + r: "96", + fill: "none", + stroke: "rgba(255,255,255,.3)", + strokeWidth: "4" + }), /*#__PURE__*/React.createElement("circle", { + cx: "104", + cy: "104", + r: "96", + fill: "none", + stroke: "#fff", + strokeWidth: "4", + strokeLinecap: "round", + strokeDasharray: "150 453" + })), map.ring === 'full' && /*#__PURE__*/React.createElement("circle", { + cx: "104", + cy: "104", + r: "96", + fill: "none", + stroke: "rgba(255,255,255,.85)", + strokeWidth: "4", + strokeLinecap: "round" + })), /*#__PURE__*/React.createElement(Icon, { + name: map.icon, + size: 50, + stroke: 1.7, + style: status === 'connecting' ? { + animation: 'pg-spin 1s linear infinite' + } : {} + }), center), /*#__PURE__*/React.createElement("div", { + style: { + textAlign: 'center' + } + }, /*#__PURE__*/React.createElement("div", { + style: { + fontFamily: 'var(--font-sans)', + fontSize: 15, + fontWeight: 600, + color: 'var(--fg2)', + whiteSpace: 'nowrap' + } + }, map.cap))), /*#__PURE__*/React.createElement("div", { + style: { + padding: '0 20px 18px', + display: 'flex', + flexDirection: 'column', + gap: 12 + } + }, free && /*#__PURE__*/React.createElement("div", { + style: { + background: 'var(--surface)', + borderRadius: 'var(--radius-lg)', + padding: '12px 14px', + boxShadow: 'var(--shadow-sm)', + border: '1px solid var(--border)' + } + }, /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + alignItems: 'center', + gap: 7, + marginBottom: 8 + } + }, /*#__PURE__*/React.createElement(Icon, { + name: "clock", + size: 15, + stroke: 2.2, + color: "var(--accent)" + }), /*#__PURE__*/React.createElement("span", { + style: { + fontSize: 12, + fontWeight: 600, + color: 'var(--fg2)' + } + }, t('quotaToday')), /*#__PURE__*/React.createElement("span", { + style: { + fontFamily: 'var(--font-mono)', + fontSize: 14, + fontWeight: 600, + color: 'var(--fg1)', + whiteSpace: 'nowrap' + } + }, mins, " ", t('minutes')), /*#__PURE__*/React.createElement("span", { + style: { + marginLeft: 'auto', + fontSize: 10.5, + fontWeight: 600, + color: 'var(--fg3)', + background: 'var(--bg-subtle)', + padding: '3px 9px', + borderRadius: 'var(--radius-full)', + whiteSpace: 'nowrap' + } + }, t('quotaFree'))), /*#__PURE__*/React.createElement("div", { + style: { + height: 6, + background: 'var(--bg-subtle)', + borderRadius: 3, + overflow: 'hidden', + marginBottom: 10 + } + }, /*#__PURE__*/React.createElement("div", { + style: { + width: `${Math.min(100, mins / 10 * 100)}%`, + height: '100%', + background: mins <= 3 ? 'var(--warning)' : 'var(--accent)', + borderRadius: 3, + transition: 'width 300ms var(--ease-out)' + } + })), adDone ? /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + gap: 7, + color: 'var(--success)', + fontWeight: 700, + fontSize: 13, + padding: '9px' + } + }, /*#__PURE__*/React.createElement(Icon, { + name: "check-circle", + size: 16, + stroke: 2.2, + color: "var(--success)" + }), t('adUnlocked')) : /*#__PURE__*/React.createElement("button", { + onClick: onAd, + style: { + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + gap: 7, + width: '100%', + border: '1.5px solid var(--accent-border)', + background: 'var(--accent-subtle)', + color: 'var(--accent)', + fontFamily: 'var(--font-sans)', + fontWeight: 700, + fontSize: 13, + padding: '9px', + borderRadius: 'var(--radius-full)', + cursor: 'pointer' + } + }, /*#__PURE__*/React.createElement(Icon, { + name: "play-circle", + size: 16, + stroke: 2.2 + }), t('watchAd'))), status === 'on' && /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + gap: 12 + } + }, [['arrow-down', t('download'), '86.4', 'Mb/s'], ['arrow-up', t('upload'), '12.1', 'Mb/s'], ['zap', t('latency'), String(server.ping), 'ms']].map(([ic, l, v, u]) => /*#__PURE__*/React.createElement("div", { + key: l, + style: { + flex: 1, + background: 'var(--surface)', + borderRadius: 'var(--radius-lg)', + padding: '12px 14px', + boxShadow: 'var(--shadow-sm)', + border: '1px solid var(--border)' + } + }, /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + alignItems: 'center', + gap: 5, + color: 'var(--fg3)', + fontSize: 11, + fontWeight: 600, + marginBottom: 4 + } + }, /*#__PURE__*/React.createElement(Icon, { + name: ic, + size: 13, + stroke: 2.2, + color: "var(--accent)" + }), l), /*#__PURE__*/React.createElement("div", { + style: { + fontFamily: 'var(--font-mono)', + fontSize: 17, + fontWeight: 500, + color: 'var(--fg1)' + } + }, v, /*#__PURE__*/React.createElement("span", { + style: { + fontSize: 11, + color: 'var(--fg3)' + } + }, " ", u))))), /*#__PURE__*/React.createElement("button", { + onClick: goServers, + style: { + display: 'flex', + alignItems: 'center', + gap: 12, + width: '100%', + textAlign: 'left', + background: 'var(--surface)', + border: '1px solid var(--border)', + borderRadius: 'var(--radius-lg)', + padding: '12px 14px', + cursor: 'pointer', + boxShadow: 'var(--shadow-sm)' + } + }, /*#__PURE__*/React.createElement(CC, { + code: server.code, + active: true + }), /*#__PURE__*/React.createElement("div", { + style: { + flex: 1 + } + }, /*#__PURE__*/React.createElement("div", { + style: { + fontFamily: 'var(--font-sans)', + fontSize: 15, + fontWeight: 600, + color: 'var(--fg1)', + display: 'flex', + alignItems: 'center', + gap: 7 + } + }, smart ? t('smartSelect') : srvName(server, lang), smart && /*#__PURE__*/React.createElement(Icon, { + name: "zap", + size: 13, + stroke: 2.4, + color: "var(--accent)" + })), /*#__PURE__*/React.createElement("div", { + style: { + fontSize: 12, + color: 'var(--fg3)' + } + }, smart ? `${srvName(server, lang)} · ${server.ping}ms` : `${srvSub(server, lang)} · ${server.ping}ms`)), /*#__PURE__*/React.createElement(Icon, { + name: "chevron-right", + size: 20, + color: "var(--fg3)" + })))); +} + +/* ───────── SERVERS screen ───────── */ +function ServersScreen({ + t, + lang, + current, + onPick, + q, + setQ +}) { + const list = SERVERS.filter(s => (s.name + s.en).toLowerCase().includes(q.toLowerCase())); + return /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + flexDirection: 'column', + height: '100%' + } + }, /*#__PURE__*/React.createElement(TopBar, { + t: t, + lang: lang + }), /*#__PURE__*/React.createElement("div", { + style: { + padding: '0 20px 12px' + } + }, /*#__PURE__*/React.createElement("div", { + style: { + fontFamily: 'var(--font-display)', + fontSize: 24, + fontWeight: 700, + color: 'var(--fg1)', + marginBottom: 12 + } + }, t('chooseNode')), /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + alignItems: 'center', + gap: 10, + background: 'var(--bg-subtle)', + borderRadius: 'var(--radius-md)', + padding: '10px 14px' + } + }, /*#__PURE__*/React.createElement(Icon, { + name: "search", + size: 18, + color: "var(--fg3)" + }), /*#__PURE__*/React.createElement("input", { + value: q, + onChange: e => setQ(e.target.value), + placeholder: t('search'), + style: { + border: 'none', + background: 'transparent', + outline: 'none', + flex: 1, + fontFamily: 'var(--font-sans)', + fontSize: 15, + color: 'var(--fg1)' + } + }))), /*#__PURE__*/React.createElement("div", { + style: { + flex: 1, + overflow: 'auto', + padding: '0 20px 20px' + } + }, /*#__PURE__*/React.createElement("button", { + onClick: () => onPick('AUTO'), + style: { + display: 'flex', + alignItems: 'center', + gap: 13, + width: '100%', + textAlign: 'left', + padding: '15px 14px', + border: `1.5px solid ${current === 'AUTO' ? 'var(--accent)' : 'var(--accent-border)'}`, + cursor: 'pointer', + background: 'var(--accent-subtle)', + borderRadius: 'var(--radius-xl)', + boxShadow: current === 'AUTO' ? 'var(--shadow-md)' : 'var(--shadow-sm)', + marginBottom: 12 + } + }, /*#__PURE__*/React.createElement("div", { + style: { + width: 42, + height: 42, + borderRadius: 'var(--radius-md)', + background: 'linear-gradient(150deg,var(--clay-500),var(--clay-700))', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + flexShrink: 0 + } + }, /*#__PURE__*/React.createElement(Icon, { + name: "zap", + size: 21, + stroke: 2.2, + color: "#fff" + })), /*#__PURE__*/React.createElement("div", { + style: { + flex: 1 + } + }, /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + alignItems: 'center', + gap: 8 + } + }, /*#__PURE__*/React.createElement("span", { + style: { + fontFamily: 'var(--font-sans)', + fontSize: 15.5, + fontWeight: 700, + color: 'var(--fg1)' + } + }, t('smartSelect')), /*#__PURE__*/React.createElement("span", { + style: { + fontSize: 10, + fontWeight: 700, + color: '#fff', + background: 'var(--accent)', + padding: '2px 8px', + borderRadius: 999, + letterSpacing: '0.04em' + } + }, t('recommended'))), /*#__PURE__*/React.createElement("div", { + style: { + fontSize: 12, + color: 'var(--fg2)', + marginTop: 3, + lineHeight: 1.5 + } + }, t('smartSub'))), current === 'AUTO' && /*#__PURE__*/React.createElement(Icon, { + name: "check", + size: 19, + color: "var(--accent)", + stroke: 2.6 + })), /*#__PURE__*/React.createElement("div", { + style: { + background: 'var(--surface)', + borderRadius: 'var(--radius-lg)', + boxShadow: 'var(--shadow-sm)', + overflow: 'hidden', + border: '1px solid var(--border)' + } + }, list.map((s, i) => { + const active = s.code === current; + return /*#__PURE__*/React.createElement("button", { + key: s.code, + onClick: () => onPick(s.code), + style: { + display: 'flex', + alignItems: 'center', + gap: 13, + width: '100%', + textAlign: 'left', + padding: '12px 14px', + border: 'none', + cursor: 'pointer', + borderBottom: i < list.length - 1 ? '1px solid var(--border)' : 'none', + background: active ? 'var(--accent-subtle)' : 'transparent' + } + }, /*#__PURE__*/React.createElement(CC, { + code: s.code, + active: active + }), /*#__PURE__*/React.createElement("div", { + style: { + flex: 1 + } + }, /*#__PURE__*/React.createElement("div", { + style: { + fontFamily: 'var(--font-sans)', + fontSize: 15, + fontWeight: 600, + color: 'var(--fg1)' + } + }, srvName(s, lang)), /*#__PURE__*/React.createElement("div", { + style: { + fontSize: 12, + color: 'var(--fg3)' + } + }, srvSub(s, lang))), /*#__PURE__*/React.createElement("span", { + style: { + fontFamily: 'var(--font-mono)', + fontSize: 12, + color: 'var(--fg2)' + } + }, s.ping, "ms"), /*#__PURE__*/React.createElement(Signal, { + ping: s.ping + }), active && /*#__PURE__*/React.createElement(Icon, { + name: "check", + size: 18, + color: "var(--accent)" + })); + })))); +} + +/* ───────── SETTINGS screen ───────── */ +function Toggle({ + on, + onChange +}) { + return /*#__PURE__*/React.createElement("div", { + onClick: onChange, + style: { + width: 46, + height: 28, + borderRadius: 999, + position: 'relative', + cursor: 'pointer', + flexShrink: 0, + background: on ? 'var(--accent)' : 'var(--border-strong)', + transition: 'background 220ms var(--ease-out)' + } + }, /*#__PURE__*/React.createElement("div", { + style: { + position: 'absolute', + top: 3, + left: on ? 21 : 3, + width: 22, + height: 22, + borderRadius: '50%', + background: '#fff', + boxShadow: 'var(--shadow-sm)', + transition: 'left 220ms var(--ease-out)' + } + })); +} +/* segmented language switch 中 / EN */ +function LangSwitch({ + lang, + setLang +}) { + return /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + background: 'var(--bg-subtle)', + borderRadius: 'var(--radius-full)', + padding: 3, + gap: 2 + } + }, [['zh', '中文'], ['en', 'EN']].map(([v, l]) => /*#__PURE__*/React.createElement("button", { + key: v, + onClick: () => setLang(v), + style: { + border: 'none', + cursor: 'pointer', + borderRadius: 'var(--radius-full)', + padding: '5px 13px', + fontFamily: 'var(--font-sans)', + fontSize: 12.5, + fontWeight: 700, + background: lang === v ? 'var(--accent)' : 'transparent', + color: lang === v ? 'var(--fg-on-accent)' : 'var(--fg3)', + transition: 'all 140ms var(--ease-out)' + } + }, l))); +} +function Row({ + icon, + title, + sub, + right, + last, + onClick +}) { + return /*#__PURE__*/React.createElement("div", { + onClick: onClick, + style: { + display: 'flex', + alignItems: 'center', + gap: 13, + padding: '13px 16px', + borderBottom: last ? 'none' : '1px solid var(--border)', + cursor: onClick ? 'pointer' : 'default' + } + }, icon && /*#__PURE__*/React.createElement("div", { + style: { + width: 32, + height: 32, + borderRadius: 'var(--radius-sm)', + background: 'var(--accent-subtle)', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + flexShrink: 0 + } + }, /*#__PURE__*/React.createElement(Icon, { + name: icon, + size: 17, + color: "var(--accent)", + stroke: 2.2 + })), /*#__PURE__*/React.createElement("div", { + style: { + flex: 1 + } + }, /*#__PURE__*/React.createElement("div", { + style: { + fontFamily: 'var(--font-sans)', + fontSize: 15, + fontWeight: 500, + color: 'var(--fg1)' + } + }, title), sub && /*#__PURE__*/React.createElement("div", { + style: { + fontSize: 12, + color: 'var(--fg3)' + } + }, sub)), right); +} +function SettingsScreen({ + t, + lang, + setLang, + dark, + setDark, + free, + setFree, + onRedeem, + onUpgrade, + onContact, + onSignOut, + onReplayAuth, + onReplayOnboarding +}) { + const [autostart, setAuto] = useState(true); + const [kill, setKill] = useState(true); + const [twoFA, setTwoFA] = useState(true); + const [devices, setDevices] = useState([{ + id: 1, + icon: 'laptop', + name: 'MacBook Pro', + os: 'macOS 26', + active: t('thisDevice'), + me: true + }, { + id: 2, + icon: 'smartphone', + name: 'iPhone 17', + os: 'iOS 26', + active: '2 min', + me: false + }, { + id: 3, + icon: 'monitor-smartphone', + name: 'iPad Air', + os: 'iPadOS 26', + active: lang === 'zh' ? '3 小时前' : '3h ago', + me: false + }]); + const card = { + background: 'var(--surface)', + borderRadius: 'var(--radius-lg)', + boxShadow: 'var(--shadow-sm)', + border: '1px solid var(--border)', + overflow: 'hidden', + marginBottom: 18 + }; + return /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + flexDirection: 'column', + height: '100%' + } + }, /*#__PURE__*/React.createElement(TopBar, { + t: t, + lang: lang + }), /*#__PURE__*/React.createElement("div", { + style: { + flex: 1, + overflow: 'auto', + padding: '0 20px 20px' + } + }, /*#__PURE__*/React.createElement("div", { + style: { + fontFamily: 'var(--font-display)', + fontSize: 24, + fontWeight: 700, + color: 'var(--fg1)', + marginBottom: 14 + } + }, t('meTitle')), /*#__PURE__*/React.createElement("div", { + style: { + background: 'linear-gradient(155deg,var(--clay-600),var(--clay-800))', + borderRadius: 'var(--radius-xl)', + padding: 18, + color: '#fff', + marginBottom: 18, + boxShadow: 'var(--shadow-md)' + } + }, /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + alignItems: 'center', + gap: 11 + } + }, /*#__PURE__*/React.createElement("div", { + style: { + width: 44, + height: 44, + borderRadius: '50%', + background: 'rgba(255,255,255,0.18)', + display: 'flex', + alignItems: 'center', + justifyContent: 'center' + } + }, /*#__PURE__*/React.createElement(Icon, { + name: "crown", + size: 22, + color: "#fff" + })), /*#__PURE__*/React.createElement("div", { + style: { + flex: 1 + } + }, /*#__PURE__*/React.createElement("div", { + style: { + fontWeight: 700, + fontSize: 16 + } + }, "me@pangolin.vpn"), /*#__PURE__*/React.createElement("div", { + style: { + display: 'inline-flex', + alignItems: 'center', + gap: 4, + fontSize: 11, + fontWeight: 600, + background: 'rgba(255,255,255,0.2)', + padding: '2px 8px', + borderRadius: 999, + marginTop: 4 + } + }, free ? t('freePlanName') : t('proMember')))), /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + justifyContent: 'space-between', + alignItems: 'flex-end', + marginTop: 16, + paddingTop: 14, + borderTop: '1px solid rgba(255,255,255,0.18)', + fontSize: 12 + } + }, /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("div", { + style: { + opacity: 0.7 + } + }, free ? t('quotaToday') : t('expires')), /*#__PURE__*/React.createElement("div", { + style: { + fontWeight: 600, + marginTop: 2, + fontFamily: 'var(--font-mono)', + whiteSpace: 'nowrap' + } + }, free ? t('quotaFree') : '2026-12-31 · 42.6 GB')), /*#__PURE__*/React.createElement("button", { + onClick: onUpgrade, + style: { + border: 'none', + background: '#fff', + color: 'var(--clay-700)', + fontWeight: 700, + fontSize: 13, + padding: '9px 16px', + borderRadius: 'var(--radius-full)', + cursor: 'pointer', + display: 'inline-flex', + alignItems: 'center', + gap: 6 + } + }, /*#__PURE__*/React.createElement(Icon, { + name: "ticket", + size: 15, + color: "var(--clay-700)" + }), t('upgradeBtn')))), /*#__PURE__*/React.createElement("div", { + style: { + fontSize: 13, + fontWeight: 700, + color: 'var(--fg2)', + margin: '4px 4px 10px' + } + }, t('accInfoTitle')), /*#__PURE__*/React.createElement("div", { + style: card + }, /*#__PURE__*/React.createElement(Row, { + icon: "mail", + title: t('accEmail'), + sub: "me@pangolin.vpn", + right: /*#__PURE__*/React.createElement("span", { + style: { + fontSize: 13, + color: 'var(--accent)', + fontWeight: 600 + } + }, t('accChange')) + }), /*#__PURE__*/React.createElement(Row, { + icon: "lock", + title: t('accPassword'), + sub: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022", + right: /*#__PURE__*/React.createElement("span", { + style: { + fontSize: 13, + color: 'var(--accent)', + fontWeight: 600 + } + }, t('accChange')) + }), /*#__PURE__*/React.createElement(Row, { + icon: "shield", + title: t('twoFA'), + sub: t('twoFASub'), + right: /*#__PURE__*/React.createElement(Toggle, { + on: twoFA, + onChange: () => setTwoFA(!twoFA) + }), + last: true + })), /*#__PURE__*/React.createElement("div", { + style: { + fontSize: 13, + fontWeight: 700, + color: 'var(--fg2)', + margin: '4px 4px 4px' + } + }, t('myDevices')), /*#__PURE__*/React.createElement("div", { + style: { + fontSize: 11.5, + color: 'var(--fg3)', + margin: '0 4px 10px' + } + }, t('devicesSub')), /*#__PURE__*/React.createElement("div", { + style: card + }, devices.map((d, i) => /*#__PURE__*/React.createElement("div", { + key: d.id, + style: { + display: 'flex', + alignItems: 'center', + gap: 12, + padding: '12px 16px', + borderBottom: i < devices.length - 1 ? '1px solid var(--border)' : 'none' + } + }, /*#__PURE__*/React.createElement("div", { + style: { + width: 34, + height: 34, + borderRadius: 'var(--radius-md)', + background: 'var(--accent-subtle)', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + flexShrink: 0 + } + }, /*#__PURE__*/React.createElement(Icon, { + name: d.icon, + size: 18, + color: "var(--accent)" + })), /*#__PURE__*/React.createElement("div", { + style: { + flex: 1, + minWidth: 0 + } + }, /*#__PURE__*/React.createElement("div", { + style: { + fontSize: 14, + fontWeight: 600, + color: 'var(--fg1)', + display: 'flex', + alignItems: 'center', + gap: 7 + } + }, d.name, d.me && /*#__PURE__*/React.createElement("span", { + style: { + fontSize: 10.5, + fontWeight: 600, + color: 'var(--success)', + background: 'var(--success-subtle)', + padding: '1px 7px', + borderRadius: 999 + } + }, d.active)), /*#__PURE__*/React.createElement("div", { + style: { + fontSize: 11.5, + color: 'var(--fg3)', + marginTop: 1 + } + }, d.os, !d.me && ` · ${d.active}`)), !d.me && /*#__PURE__*/React.createElement("button", { + onClick: () => setDevices(devices.filter(x => x.id !== d.id)), + style: { + border: '1.5px solid var(--border-strong)', + background: 'transparent', + color: 'var(--fg2)', + fontSize: 12.5, + fontWeight: 600, + padding: '6px 12px', + borderRadius: 'var(--radius-full)', + cursor: 'pointer' + } + }, t('remove'))))), /*#__PURE__*/React.createElement("div", { + style: card + }, /*#__PURE__*/React.createElement(Row, { + icon: "shopping-bag", + title: t('redeemEntry'), + right: /*#__PURE__*/React.createElement(Icon, { + name: "chevron-right", + size: 18, + color: "var(--fg3)" + }), + onClick: onRedeem + }), /*#__PURE__*/React.createElement(Row, { + icon: "external-link", + title: t('ucEntry'), + sub: t('ucEntrySub'), + right: /*#__PURE__*/React.createElement(Icon, { + name: "chevron-right", + size: 18, + color: "var(--fg3)" + }), + onClick: () => window.open('../usercenter/index.html', '_blank') + }), /*#__PURE__*/React.createElement(Row, { + icon: "message-circle", + title: t('contactEntry'), + right: /*#__PURE__*/React.createElement(Icon, { + name: "chevron-right", + size: 18, + color: "var(--fg3)" + }), + last: true, + onClick: onContact + })), /*#__PURE__*/React.createElement("div", { + style: card + }, /*#__PURE__*/React.createElement(Row, { + icon: "zap", + title: t('smartRoute'), + sub: t('smartRouteSub'), + right: /*#__PURE__*/React.createElement(Toggle, { + on: autostart, + onChange: () => setAuto(!autostart) + }) + }), /*#__PURE__*/React.createElement(Row, { + icon: "shield", + title: t('killSwitch'), + sub: t('killSwitchSub'), + right: /*#__PURE__*/React.createElement(Toggle, { + on: kill, + onChange: () => setKill(!kill) + }), + last: true + })), /*#__PURE__*/React.createElement("div", { + style: card + }, /*#__PURE__*/React.createElement(Row, { + icon: "globe", + title: t('language'), + right: /*#__PURE__*/React.createElement(LangSwitch, { + lang: lang, + setLang: setLang + }) + }), /*#__PURE__*/React.createElement(Row, { + icon: dark ? 'moon' : 'sun', + title: t('darkAppearance'), + sub: dark ? t('stateOn') : t('followLight'), + right: /*#__PURE__*/React.createElement(Toggle, { + on: dark, + onChange: () => setDark(!dark) + }) + }), /*#__PURE__*/React.createElement(Row, { + icon: "shield", + title: t('protocol'), + right: /*#__PURE__*/React.createElement("span", { + style: { + fontFamily: 'var(--font-mono)', + fontSize: 13, + color: 'var(--fg3)' + } + }, "WireGuard") + }), /*#__PURE__*/React.createElement(Row, { + icon: "refresh-cw", + title: t('checkUpdate'), + right: /*#__PURE__*/React.createElement(Icon, { + name: "chevron-right", + size: 18, + color: "var(--fg3)" + }), + last: true + })), /*#__PURE__*/React.createElement("div", { + style: card + }, /*#__PURE__*/React.createElement(Row, { + icon: "log-out", + title: t('replayAuth'), + sub: t('replayAuthSub'), + right: /*#__PURE__*/React.createElement(Icon, { + name: "chevron-right", + size: 18, + color: "var(--fg3)" + }), + onClick: onReplayAuth + }), /*#__PURE__*/React.createElement(Row, { + icon: "compass", + title: t('replayOnboarding'), + sub: t('replayOnboardingSub'), + right: /*#__PURE__*/React.createElement(Icon, { + name: "chevron-right", + size: 18, + color: "var(--fg3)" + }), + onClick: onReplayOnboarding + }), /*#__PURE__*/React.createElement(Row, { + icon: "clock", + title: t('freeDemo'), + sub: t('freeDemoSub'), + right: /*#__PURE__*/React.createElement(Toggle, { + on: free, + onChange: () => setFree(!free) + }), + last: true + })), /*#__PURE__*/React.createElement("div", { + style: { + textAlign: 'center', + fontSize: 12, + color: 'var(--fg3)', + fontFamily: 'var(--font-mono)' + } + }, "\u7A7F\u5C71\u7532 \xB7 v2.4.0"))); +} + +/* ───────── STATS screen ───────── */ +function StatsScreen({ + t, + lang +}) { + const vals = [2.1, 3.4, 1.8, 4.6, 5.2, 6.1, 3.0]; + const max = 6.1; + const labels = t('days7'); + const card = { + background: 'var(--surface)', + border: '1px solid var(--border)', + borderRadius: 'var(--radius-lg)', + boxShadow: 'var(--shadow-sm)' + }; + return /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + flexDirection: 'column', + height: '100%' + } + }, /*#__PURE__*/React.createElement(TopBar, { + t: t, + lang: lang + }), /*#__PURE__*/React.createElement("div", { + style: { + flex: 1, + overflow: 'auto', + padding: '0 20px 20px' + } + }, /*#__PURE__*/React.createElement("div", { + style: { + fontFamily: 'var(--font-display)', + fontSize: 24, + fontWeight: 700, + color: 'var(--fg1)', + marginBottom: 16 + } + }, t('statsTitle')), /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + gap: 12, + marginBottom: 16 + } + }, [[t('trafficMonth'), '42.6', 'GB'], [t('avgPing'), '29', 'ms'], [t('durMonth'), '86.4', 'h']].map(([l, v, u]) => /*#__PURE__*/React.createElement("div", { + key: l, + style: { + ...card, + flex: 1, + padding: '14px 14px' + } + }, /*#__PURE__*/React.createElement("div", { + style: { + fontSize: 11, + color: 'var(--fg3)', + fontWeight: 600 + } + }, l), /*#__PURE__*/React.createElement("div", { + style: { + fontFamily: 'var(--font-mono)', + fontSize: 20, + fontWeight: 500, + color: 'var(--fg1)', + marginTop: 6 + } + }, v, /*#__PURE__*/React.createElement("span", { + style: { + fontSize: 11, + color: 'var(--fg3)' + } + }, " ", u))))), /*#__PURE__*/React.createElement("div", { + style: { + ...card, + padding: '18px 18px' + } + }, /*#__PURE__*/React.createElement("div", { + style: { + fontSize: 13, + fontWeight: 600, + color: 'var(--fg1)', + marginBottom: 18 + } + }, t('weekTraffic')), /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + alignItems: 'flex-end', + gap: 12, + height: 120 + } + }, vals.map((v, i) => /*#__PURE__*/React.createElement("div", { + key: i, + style: { + flex: 1, + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + gap: 7 + } + }, /*#__PURE__*/React.createElement("div", { + style: { + fontFamily: 'var(--font-mono)', + fontSize: 10, + color: 'var(--fg3)' + } + }, v), /*#__PURE__*/React.createElement("div", { + style: { + width: '100%', + maxWidth: 30, + height: `${v / max * 84}px`, + background: 'var(--accent)', + borderRadius: '6px 6px 0 0', + opacity: 0.85 + } + }), /*#__PURE__*/React.createElement("div", { + style: { + fontSize: 11, + color: 'var(--fg3)' + } + }, labels[i]))))))); +} + +/* ───────── Bottom tab ───────── */ +function BottomTab({ + t, + tab, + setTab +}) { + const items = [['connect', 'power', t('tabConnect')], ['servers', 'globe', t('tabServers')], ['stats', 'chart', t('tabStats')], ['me', 'user', t('tabMe')]]; + return /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + borderTop: '1px solid var(--border)', + background: 'var(--surface)', + paddingBottom: 22 + } + }, items.map(([id, ic, label]) => { + const a = tab === id; + return /*#__PURE__*/React.createElement("button", { + key: id, + onClick: () => setTab(id), + style: { + flex: 1, + border: 'none', + background: 'transparent', + cursor: 'pointer', + padding: '10px 0 4px', + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + gap: 4 + } + }, /*#__PURE__*/React.createElement(Icon, { + name: ic, + size: 22, + stroke: a ? 2.4 : 2, + color: a ? 'var(--accent)' : 'var(--fg3)' + }), /*#__PURE__*/React.createElement("span", { + style: { + fontFamily: 'var(--font-sans)', + fontSize: 11, + fontWeight: a ? 700 : 500, + color: a ? 'var(--accent)' : 'var(--fg3)' + } + }, label)); + })); +} +Object.assign(window, { + ConnectScreen, + ServersScreen, + StatsScreen, + SettingsScreen, + BottomTab, + LangSwitch +}); +})(); } catch (e) { __ds_ns.__errors.push({ path: "ui_kits/mobile/screens.jsx", error: String((e && e.message) || e) }); } + +// ui_kits/tablet/tabapp.jsx +try { (() => { +/* tabapp.jsx — 穿山甲 iPad 客户端 · 分栏布局 + 复用 ../mobile/parts.jsx 的 Icon/Mark/CC/Signal/SERVERS/makeT/srvName/srvSub。 + 仅 iPad 专属字串定义在本文件 TSTR。 */ + +const { + useState, + useEffect, + useRef +} = React; + +/* iPad 专属字串(双语单显) */ +const TSTR = { + navConnect: { + zh: '连接', + en: 'Connect' + }, + navNodes: { + zh: '节点', + en: 'Nodes' + }, + navStats: { + zh: '统计', + en: 'Stats' + }, + navAccount: { + zh: '账户', + en: 'Account' + }, + capOff: { + zh: '未连接 · 轻点连接', + en: 'Not connected · tap to start' + }, + capConnecting: { + zh: '连接中 Connecting…', + en: 'Connecting…' + }, + capOn: { + zh: '已连接 · 网络已加密', + en: 'Connected · traffic encrypted' + }, + download: { + zh: '下载', + en: 'Down' + }, + upload: { + zh: '上传', + en: 'Up' + }, + latency: { + zh: '延迟', + en: 'Ping' + }, + curNode: { + zh: '当前节点', + en: 'Current node' + }, + searchPh: { + zh: '搜索国家 / 城市', + en: 'Search country / city' + }, + statTraffic: { + zh: '本月流量', + en: 'Data this month' + }, + statPing: { + zh: '平均延迟', + en: 'Avg latency' + }, + statTime: { + zh: '本月时长', + en: 'Time this month' + }, + weekTraffic: { + zh: '本周流量 (GB)', + en: 'This week (GB)' + }, + days: { + zh: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'], + en: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] + }, + acctEmail: { + zh: '邮箱', + en: 'Email' + }, + language: { + zh: '语言', + en: 'Language' + }, + darkMode: { + zh: '深色外观', + en: 'Dark appearance' + }, + smartRoute: { + zh: '智能分流', + en: 'Smart routing' + }, + smartRouteSub: { + zh: '海外应用走加速,国内直连', + en: 'Accelerate abroad, direct at home' + }, + killSwitch: { + zh: 'Kill Switch', + en: 'Kill Switch' + }, + killSub: { + zh: '断线时阻断网络,防止泄露', + en: 'Block traffic if the link drops' + }, + freeBanner: { + zh: '免费版', + en: 'Free plan' + }, + trialNote: { + zh: '升级 PRO:不限时长 · 80+ 加速线路 · 5 台设备', + en: 'Go PRO: unlimited time · 80+ routes · 5 devices' + }, + version: { + zh: '穿山甲 · iPadOS · v2.4.0', + en: 'Pangolin · iPadOS · v2.4.0' + } +}; +function makeTT(lang) { + return k => (TSTR[k] && TSTR[k][lang]) !== undefined ? TSTR[k][lang] : k; +} +function fmtHMS(s) { + return `${String(Math.floor(s / 3600)).padStart(2, '0')}:${String(Math.floor(s % 3600 / 60)).padStart(2, '0')}:${String(s % 60).padStart(2, '0')}`; +} + +/* ── 左侧栏 ── */ +function TabRail({ + tt, + view, + setView, + free, + t +}) { + const items = [['connect', 'power', 'navConnect'], ['nodes', 'globe', 'navNodes'], ['stats', 'chart', 'navStats'], ['account', 'user', 'navAccount']]; + return /*#__PURE__*/React.createElement("div", { + style: { + width: 232, + flexShrink: 0, + background: 'var(--bg-subtle)', + borderRight: '1px solid var(--border)', + display: 'flex', + flexDirection: 'column', + padding: '18px 14px 16px' + } + }, /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + alignItems: 'center', + gap: 10, + padding: '0 8px 20px' + } + }, /*#__PURE__*/React.createElement(Mark, { + size: 30 + }), /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("div", { + style: { + fontFamily: 'var(--font-display)', + fontWeight: 700, + fontSize: 17, + color: 'var(--fg1)', + lineHeight: 1 + } + }, "\u7A7F\u5C71\u7532"), /*#__PURE__*/React.createElement("div", { + style: { + fontSize: 9, + fontWeight: 600, + letterSpacing: '0.2em', + color: 'var(--accent)', + marginTop: 3 + } + }, "PANGOLIN"))), /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + flexDirection: 'column', + gap: 4 + } + }, items.map(([id, ic, lk]) => { + const a = view === id; + return /*#__PURE__*/React.createElement("button", { + key: id, + onClick: () => setView(id), + style: { + display: 'flex', + alignItems: 'center', + gap: 12, + width: '100%', + textAlign: 'left', + border: 'none', + cursor: 'pointer', + background: a ? 'var(--accent-subtle)' : 'transparent', + color: a ? 'var(--accent)' : 'var(--fg2)', + padding: '13px 14px', + borderRadius: 'var(--radius-md)', + fontFamily: 'var(--font-sans)', + fontSize: 15, + fontWeight: a ? 700 : 500, + transition: 'background-color 140ms var(--ease-out), color 140ms var(--ease-out)', + minHeight: 48 + } + }, /*#__PURE__*/React.createElement(Icon, { + name: ic, + size: 20, + stroke: a ? 2.4 : 2, + color: a ? 'var(--accent)' : 'var(--fg3)' + }), tt(lk)); + })), /*#__PURE__*/React.createElement("div", { + style: { + flex: 1 + } + }), /*#__PURE__*/React.createElement("div", { + style: { + background: free ? 'var(--surface)' : 'linear-gradient(150deg,var(--clay-600),var(--clay-800))', + border: free ? '1px solid var(--border)' : 'none', + borderRadius: 'var(--radius-lg)', + padding: '12px 13px', + color: free ? 'var(--fg1)' : '#fff' + } + }, /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + alignItems: 'center', + gap: 9 + } + }, /*#__PURE__*/React.createElement("div", { + style: { + width: 32, + height: 32, + borderRadius: '50%', + background: free ? 'var(--bg-subtle)' : 'rgba(255,255,255,0.18)', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + flexShrink: 0 + } + }, /*#__PURE__*/React.createElement(Icon, { + name: free ? 'user' : 'crown', + size: 16, + color: free ? 'var(--fg2)' : '#fff' + })), /*#__PURE__*/React.createElement("div", { + style: { + minWidth: 0 + } + }, /*#__PURE__*/React.createElement("div", { + style: { + fontSize: 12.5, + fontWeight: 700, + whiteSpace: 'nowrap' + } + }, free ? tt('freeBanner') : t('proMember')), /*#__PURE__*/React.createElement("div", { + style: { + fontSize: 10.5, + opacity: 0.65, + whiteSpace: 'nowrap' + } + }, "me@pangolin.vpn"))))); +} + +/* ── 连接视图:左大按钮 + 右信息列 ── */ +function TabConnect({ + t, + tt, + lang, + status, + onToggle, + server, + smart, + free, + mins, + adDone, + onAd, + elapsed, + goNodes +}) { + const map = { + off: { + ring: 'var(--border)', + fill: 'var(--surface)', + fg: 'var(--fg2)', + icon: 'power', + label: 'TAP', + cap: tt('capOff') + }, + connecting: { + ring: 'var(--accent-subtle)', + fill: 'var(--accent)', + fg: '#fff', + icon: 'loader-circle', + label: '···', + cap: tt('capConnecting') + }, + on: { + ring: 'var(--success-subtle)', + fill: 'var(--success)', + fg: '#fff', + icon: 'shield-check', + label: 'SAFE', + cap: tt('capOn') + } + }[status]; + const card = { + background: 'var(--surface)', + border: '1px solid var(--border)', + borderRadius: 'var(--radius-lg)', + boxShadow: 'var(--shadow-sm)' + }; + return /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + height: '100%', + alignItems: 'stretch' + } + }, /*#__PURE__*/React.createElement("div", { + style: { + flex: 1.25, + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'center', + gap: 26 + } + }, /*#__PURE__*/React.createElement("button", { + onClick: onToggle, + style: { + width: 216, + height: 216, + borderRadius: '50%', + border: 'none', + cursor: 'pointer', + background: map.fill, + color: map.fg, + boxShadow: `0 0 0 11px ${map.ring}, var(--shadow-lg)`, + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'center', + gap: 12, + transition: 'box-shadow 300ms var(--ease-out), background-color 300ms var(--ease-out)' + } + }, /*#__PURE__*/React.createElement(Icon, { + name: map.icon, + size: 60, + stroke: 1.6, + style: status === 'connecting' ? { + animation: 'pg-spin 1s linear infinite' + } : {} + }), /*#__PURE__*/React.createElement("span", { + style: { + fontFamily: 'var(--font-mono)', + fontWeight: 700, + fontSize: 14, + letterSpacing: '0.08em' + } + }, map.label)), /*#__PURE__*/React.createElement("div", { + style: { + textAlign: 'center' + } + }, /*#__PURE__*/React.createElement("div", { + style: { + fontFamily: 'var(--font-sans)', + fontSize: 16, + fontWeight: 600, + color: 'var(--fg2)', + whiteSpace: 'nowrap' + } + }, map.cap), status === 'on' && /*#__PURE__*/React.createElement("div", { + style: { + fontFamily: 'var(--font-mono)', + fontSize: 28, + fontWeight: 500, + color: 'var(--fg1)', + marginTop: 8, + letterSpacing: '-0.01em' + } + }, fmtHMS(elapsed)))), /*#__PURE__*/React.createElement("div", { + style: { + width: 332, + flexShrink: 0, + display: 'flex', + flexDirection: 'column', + gap: 14, + justifyContent: 'center', + paddingRight: 28 + } + }, free && /*#__PURE__*/React.createElement("div", { + style: { + ...card, + padding: '14px 16px' + } + }, /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + alignItems: 'center', + gap: 8, + marginBottom: 9 + } + }, /*#__PURE__*/React.createElement(Icon, { + name: "clock", + size: 15, + stroke: 2.2, + color: "var(--accent)" + }), /*#__PURE__*/React.createElement("span", { + style: { + fontSize: 12.5, + fontWeight: 600, + color: 'var(--fg2)' + } + }, t('quotaToday')), /*#__PURE__*/React.createElement("span", { + style: { + fontFamily: 'var(--font-mono)', + fontSize: 14, + fontWeight: 600, + color: 'var(--fg1)', + whiteSpace: 'nowrap' + } + }, mins, " ", t('minutes')), /*#__PURE__*/React.createElement("span", { + style: { + marginLeft: 'auto', + fontSize: 10.5, + fontWeight: 600, + color: 'var(--fg3)', + background: 'var(--bg-subtle)', + padding: '3px 9px', + borderRadius: 'var(--radius-full)', + whiteSpace: 'nowrap' + } + }, t('quotaFree'))), /*#__PURE__*/React.createElement("div", { + style: { + height: 6, + background: 'var(--bg-subtle)', + borderRadius: 3, + overflow: 'hidden', + marginBottom: 11 + } + }, /*#__PURE__*/React.createElement("div", { + style: { + width: `${Math.min(100, mins / 10 * 100)}%`, + height: '100%', + background: mins <= 3 ? 'var(--warning)' : 'var(--accent)', + borderRadius: 3 + } + })), adDone ? /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + gap: 7, + color: 'var(--success)', + fontWeight: 700, + fontSize: 13, + padding: '8px' + } + }, /*#__PURE__*/React.createElement(Icon, { + name: "check-circle", + size: 16, + stroke: 2.2, + color: "var(--success)" + }), t('adUnlocked')) : /*#__PURE__*/React.createElement("button", { + onClick: onAd, + style: { + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + gap: 7, + width: '100%', + border: '1.5px solid var(--accent-border)', + background: 'var(--accent-subtle)', + color: 'var(--accent)', + fontFamily: 'var(--font-sans)', + fontWeight: 700, + fontSize: 13.5, + padding: '10px', + borderRadius: 'var(--radius-full)', + cursor: 'pointer', + minHeight: 44 + } + }, /*#__PURE__*/React.createElement(Icon, { + name: "play-circle", + size: 16, + stroke: 2.2 + }), t('watchAd'))), /*#__PURE__*/React.createElement("button", { + onClick: goNodes, + style: { + ...card, + display: 'flex', + alignItems: 'center', + gap: 13, + textAlign: 'left', + padding: '13px 15px', + cursor: 'pointer', + minHeight: 48 + } + }, /*#__PURE__*/React.createElement(CC, { + code: server.code, + active: true + }), /*#__PURE__*/React.createElement("div", { + style: { + flex: 1 + } + }, /*#__PURE__*/React.createElement("div", { + style: { + fontSize: 11, + fontWeight: 600, + color: 'var(--fg3)', + marginBottom: 2 + } + }, tt('curNode')), /*#__PURE__*/React.createElement("div", { + style: { + fontFamily: 'var(--font-sans)', + fontSize: 15, + fontWeight: 600, + color: 'var(--fg1)', + display: 'flex', + alignItems: 'center', + gap: 7 + } + }, smart ? t('smartSelect') : srvName(server, lang), smart && /*#__PURE__*/React.createElement(Icon, { + name: "zap", + size: 13, + stroke: 2.4, + color: "var(--accent)" + })), /*#__PURE__*/React.createElement("div", { + style: { + fontSize: 12, + color: 'var(--fg3)' + } + }, smart ? `${srvName(server, lang)} · ${server.ping}ms` : `${srvSub(server, lang)} · ${server.ping}ms`)), /*#__PURE__*/React.createElement(Icon, { + name: "chevron-right", + size: 20, + color: "var(--fg3)" + })), status === 'on' && /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + gap: 10 + } + }, [['arrow-down', tt('download'), '86.4', 'Mb/s'], ['arrow-up', tt('upload'), '12.1', 'Mb/s'], ['zap', tt('latency'), String(server.ping), 'ms']].map(([ic, l, v, u]) => /*#__PURE__*/React.createElement("div", { + key: ic, + style: { + ...card, + flex: 1, + padding: '11px 13px' + } + }, /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + alignItems: 'center', + gap: 5, + color: 'var(--fg3)', + fontSize: 11, + fontWeight: 600, + marginBottom: 4 + } + }, /*#__PURE__*/React.createElement(Icon, { + name: ic, + size: 13, + stroke: 2.2, + color: "var(--accent)" + }), l), /*#__PURE__*/React.createElement("div", { + style: { + fontFamily: 'var(--font-mono)', + fontSize: 16, + fontWeight: 500, + color: 'var(--fg1)' + } + }, v, /*#__PURE__*/React.createElement("span", { + style: { + fontSize: 10.5, + color: 'var(--fg3)' + } + }, " ", u))))))); +} + +/* ── 节点视图 ── */ +function TabNodes({ + t, + tt, + lang, + current, + onPick +}) { + const [q, setQ] = useState(''); + const list = SERVERS.filter(s => (s.name + s.en).toLowerCase().includes(q.toLowerCase())); + const smartActive = current === 'AUTO'; + return /*#__PURE__*/React.createElement("div", { + style: { + padding: '6px 28px 24px', + overflow: 'auto', + height: '100%', + boxSizing: 'border-box' + } + }, /*#__PURE__*/React.createElement("button", { + onClick: () => onPick('AUTO'), + style: { + display: 'flex', + alignItems: 'center', + gap: 13, + width: '100%', + textAlign: 'left', + cursor: 'pointer', + background: 'var(--accent-subtle)', + border: `1.5px solid ${smartActive ? 'var(--accent)' : 'var(--accent-border)'}`, + borderRadius: 'var(--radius-xl)', + padding: '15px 16px', + boxShadow: smartActive ? 'var(--shadow-md)' : 'var(--shadow-sm)', + marginBottom: 14, + minHeight: 56 + } + }, /*#__PURE__*/React.createElement("div", { + style: { + width: 42, + height: 42, + borderRadius: 'var(--radius-md)', + background: 'linear-gradient(150deg,var(--clay-500),var(--clay-700))', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + flexShrink: 0 + } + }, /*#__PURE__*/React.createElement(Icon, { + name: "zap", + size: 21, + stroke: 2.2, + color: "#fff" + })), /*#__PURE__*/React.createElement("div", { + style: { + flex: 1 + } + }, /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + alignItems: 'center', + gap: 8 + } + }, /*#__PURE__*/React.createElement("span", { + style: { + fontFamily: 'var(--font-sans)', + fontSize: 15.5, + fontWeight: 700, + color: 'var(--fg1)' + } + }, t('smartSelect')), /*#__PURE__*/React.createElement("span", { + style: { + fontSize: 10, + fontWeight: 700, + color: '#fff', + background: 'var(--accent)', + padding: '2px 8px', + borderRadius: 999, + letterSpacing: '0.04em' + } + }, t('recommended'))), /*#__PURE__*/React.createElement("div", { + style: { + fontSize: 12, + color: 'var(--fg2)', + marginTop: 3 + } + }, t('smartSub'))), smartActive && /*#__PURE__*/React.createElement(Icon, { + name: "check", + size: 19, + color: "var(--accent)", + stroke: 2.6 + })), /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + alignItems: 'center', + gap: 10, + background: 'var(--bg-subtle)', + borderRadius: 'var(--radius-md)', + padding: '11px 14px', + marginBottom: 14, + maxWidth: 380 + } + }, /*#__PURE__*/React.createElement(Icon, { + name: "search", + size: 18, + color: "var(--fg3)" + }), /*#__PURE__*/React.createElement("input", { + value: q, + onChange: e => setQ(e.target.value), + placeholder: tt('searchPh'), + style: { + border: 'none', + background: 'transparent', + outline: 'none', + flex: 1, + fontFamily: 'var(--font-sans)', + fontSize: 14.5, + color: 'var(--fg1)' + } + })), /*#__PURE__*/React.createElement("div", { + style: { + display: 'grid', + gridTemplateColumns: '1fr 1fr', + gap: 11 + } + }, list.map(s => { + const active = s.code === current; + return /*#__PURE__*/React.createElement("button", { + key: s.code, + onClick: () => onPick(s.code), + style: { + display: 'flex', + alignItems: 'center', + gap: 12, + textAlign: 'left', + cursor: 'pointer', + background: active ? 'var(--accent-subtle)' : 'var(--surface)', + border: `1.5px solid ${active ? 'var(--accent-border)' : 'var(--border)'}`, + borderRadius: 'var(--radius-lg)', + padding: '13px 15px', + boxShadow: 'var(--shadow-sm)', + minHeight: 56 + } + }, /*#__PURE__*/React.createElement(CC, { + code: s.code, + active: active + }), /*#__PURE__*/React.createElement("div", { + style: { + flex: 1, + minWidth: 0 + } + }, /*#__PURE__*/React.createElement("div", { + style: { + fontFamily: 'var(--font-sans)', + fontSize: 14.5, + fontWeight: 600, + color: 'var(--fg1)' + } + }, srvName(s, lang)), /*#__PURE__*/React.createElement("div", { + style: { + fontSize: 12, + color: 'var(--fg3)', + whiteSpace: 'nowrap', + overflow: 'hidden', + textOverflow: 'ellipsis' + } + }, srvSub(s, lang))), /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + flexDirection: 'column', + alignItems: 'flex-end', + gap: 5 + } + }, /*#__PURE__*/React.createElement("span", { + style: { + fontFamily: 'var(--font-mono)', + fontSize: 12, + color: 'var(--fg2)' + } + }, s.ping, "ms"), /*#__PURE__*/React.createElement(Signal, { + ping: s.ping + })), active && /*#__PURE__*/React.createElement(Icon, { + name: "check", + size: 18, + color: "var(--accent)" + })); + }))); +} + +/* ── 统计视图 ── */ +function TabStats({ + tt, + lang +}) { + const vals = [2.1, 3.4, 1.8, 4.6, 5.2, 6.1, 3.0]; + const days = TSTR.days[lang]; + const max = Math.max(...vals); + const card = { + background: 'var(--surface)', + border: '1px solid var(--border)', + borderRadius: 'var(--radius-lg)', + boxShadow: 'var(--shadow-sm)' + }; + return /*#__PURE__*/React.createElement("div", { + style: { + padding: '6px 28px 24px', + overflow: 'auto', + height: '100%', + boxSizing: 'border-box' + } + }, /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + gap: 14, + marginBottom: 20 + } + }, [[tt('statTraffic'), '42.6 GB'], [tt('statPing'), '29 ms'], [tt('statTime'), '86.4 h']].map(([l, v]) => /*#__PURE__*/React.createElement("div", { + key: l, + style: { + ...card, + flex: 1, + padding: '18px 20px' + } + }, /*#__PURE__*/React.createElement("div", { + style: { + fontSize: 12.5, + color: 'var(--fg3)', + fontWeight: 600 + } + }, l), /*#__PURE__*/React.createElement("div", { + style: { + fontFamily: 'var(--font-mono)', + fontSize: 26, + fontWeight: 500, + color: 'var(--fg1)', + marginTop: 7 + } + }, v)))), /*#__PURE__*/React.createElement("div", { + style: { + ...card, + padding: '20px 22px' + } + }, /*#__PURE__*/React.createElement("div", { + style: { + fontSize: 13.5, + fontWeight: 600, + color: 'var(--fg1)', + marginBottom: 20 + } + }, tt('weekTraffic')), /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + alignItems: 'flex-end', + gap: 22, + height: 150 + } + }, vals.map((v, i) => /*#__PURE__*/React.createElement("div", { + key: i, + style: { + flex: 1, + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + gap: 9 + } + }, /*#__PURE__*/React.createElement("div", { + style: { + fontFamily: 'var(--font-mono)', + fontSize: 10.5, + color: 'var(--fg3)' + } + }, v), /*#__PURE__*/React.createElement("div", { + style: { + width: '100%', + maxWidth: 40, + height: `${v / max * 104}px`, + background: 'var(--accent)', + borderRadius: '6px 6px 0 0', + opacity: 0.85 + } + }), /*#__PURE__*/React.createElement("div", { + style: { + fontSize: 11.5, + color: 'var(--fg3)' + } + }, days[i])))))); +} + +/* ── 账户视图 ── */ +function TabToggle({ + on, + onChange +}) { + return /*#__PURE__*/React.createElement("div", { + onClick: onChange, + style: { + width: 48, + height: 29, + borderRadius: 999, + position: 'relative', + cursor: 'pointer', + flexShrink: 0, + background: on ? 'var(--accent)' : 'var(--border-strong)', + transition: 'background 220ms var(--ease-out)' + } + }, /*#__PURE__*/React.createElement("div", { + style: { + position: 'absolute', + top: 3, + left: on ? 22 : 3, + width: 23, + height: 23, + borderRadius: '50%', + background: '#fff', + boxShadow: 'var(--shadow-sm)', + transition: 'left 220ms var(--ease-out)' + } + })); +} +function TabAccount({ + t, + tt, + lang, + setLang, + dark, + setDark, + free, + setFree +}) { + const [smartRoute, setSmartRoute] = useState(true); + const [kill, setKill] = useState(true); + const card = { + background: 'var(--surface)', + border: '1px solid var(--border)', + borderRadius: 'var(--radius-lg)', + boxShadow: 'var(--shadow-sm)', + overflow: 'hidden', + marginBottom: 16 + }; + const Row = ({ + icon, + title, + sub, + right, + last + }) => /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + alignItems: 'center', + gap: 13, + padding: '15px 18px', + borderBottom: last ? 'none' : '1px solid var(--border)', + minHeight: 48, + boxSizing: 'border-box' + } + }, icon && /*#__PURE__*/React.createElement("div", { + style: { + width: 34, + height: 34, + borderRadius: 'var(--radius-sm)', + background: 'var(--accent-subtle)', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + flexShrink: 0 + } + }, /*#__PURE__*/React.createElement(Icon, { + name: icon, + size: 17, + color: "var(--accent)", + stroke: 2.2 + })), /*#__PURE__*/React.createElement("div", { + style: { + flex: 1 + } + }, /*#__PURE__*/React.createElement("div", { + style: { + fontSize: 15, + fontWeight: 500, + color: 'var(--fg1)' + } + }, title), sub && /*#__PURE__*/React.createElement("div", { + style: { + fontSize: 12, + color: 'var(--fg3)', + marginTop: 2 + } + }, sub)), right); + const seg = val => ({ + border: 'none', + cursor: 'pointer', + padding: '7px 16px', + borderRadius: 'var(--radius-full)', + fontFamily: 'var(--font-sans)', + fontSize: 13, + fontWeight: 600, + minHeight: 32, + background: lang === val ? 'var(--surface)' : 'transparent', + color: lang === val ? 'var(--fg1)' : 'var(--fg3)', + boxShadow: lang === val ? 'var(--shadow-sm)' : 'none' + }); + return /*#__PURE__*/React.createElement("div", { + style: { + padding: '6px 28px 24px', + overflow: 'auto', + height: '100%', + boxSizing: 'border-box', + maxWidth: 640 + } + }, /*#__PURE__*/React.createElement("div", { + style: { + background: free ? 'var(--surface)' : 'linear-gradient(155deg,var(--clay-600),var(--clay-800))', + border: free ? '1px solid var(--border)' : 'none', + borderRadius: 'var(--radius-xl)', + padding: 20, + color: free ? 'var(--fg1)' : '#fff', + marginBottom: 18, + boxShadow: 'var(--shadow-md)' + } + }, /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + alignItems: 'center', + gap: 12 + } + }, /*#__PURE__*/React.createElement("div", { + style: { + width: 46, + height: 46, + borderRadius: '50%', + background: free ? 'var(--bg-subtle)' : 'rgba(255,255,255,0.18)', + display: 'flex', + alignItems: 'center', + justifyContent: 'center' + } + }, /*#__PURE__*/React.createElement(Icon, { + name: free ? 'user' : 'crown', + size: 22, + color: free ? 'var(--fg2)' : '#fff' + })), /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("div", { + style: { + fontWeight: 700, + fontSize: 16.5 + } + }, "me@pangolin.vpn"), /*#__PURE__*/React.createElement("div", { + style: { + display: 'inline-block', + fontSize: 11, + fontWeight: 600, + background: free ? 'var(--bg-subtle)' : 'rgba(255,255,255,0.2)', + color: free ? 'var(--fg2)' : '#fff', + padding: '2px 9px', + borderRadius: 999, + marginTop: 5 + } + }, free ? tt('freeBanner') : t('proMember')))), /*#__PURE__*/React.createElement("div", { + style: { + marginTop: 14, + paddingTop: 13, + borderTop: `1px solid ${free ? 'var(--border)' : 'rgba(255,255,255,0.18)'}`, + fontSize: 12.5, + opacity: free ? 1 : 0.92, + color: free ? 'var(--fg2)' : '#fff' + } + }, free ? tt('trialNote') : '2026-12-31 · 42.6 GB')), /*#__PURE__*/React.createElement("div", { + style: card + }, /*#__PURE__*/React.createElement(Row, { + icon: "zap", + title: tt('smartRoute'), + sub: tt('smartRouteSub'), + right: /*#__PURE__*/React.createElement(TabToggle, { + on: smartRoute, + onChange: () => setSmartRoute(!smartRoute) + }) + }), /*#__PURE__*/React.createElement(Row, { + icon: "shield", + title: tt('killSwitch'), + sub: tt('killSub'), + right: /*#__PURE__*/React.createElement(TabToggle, { + on: kill, + onChange: () => setKill(!kill) + }), + last: true + })), /*#__PURE__*/React.createElement("div", { + style: card + }, /*#__PURE__*/React.createElement(Row, { + icon: "globe", + title: tt('language'), + right: /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + gap: 4, + background: 'var(--bg-subtle)', + borderRadius: 'var(--radius-full)', + padding: 3 + } + }, /*#__PURE__*/React.createElement("button", { + style: seg('zh'), + onClick: () => setLang('zh') + }, "\u4E2D\u6587"), /*#__PURE__*/React.createElement("button", { + style: seg('en'), + onClick: () => setLang('en') + }, "EN")) + }), /*#__PURE__*/React.createElement(Row, { + icon: dark ? 'moon' : 'sun', + title: tt('darkMode'), + right: /*#__PURE__*/React.createElement(TabToggle, { + on: dark, + onChange: () => setDark(!dark) + }) + }), /*#__PURE__*/React.createElement(Row, { + icon: "clock", + title: t('freeDemo'), + sub: t('freeDemoSub'), + right: /*#__PURE__*/React.createElement(TabToggle, { + on: free, + onChange: () => setFree(!free) + }), + last: true + })), /*#__PURE__*/React.createElement("div", { + style: { + textAlign: 'center', + fontSize: 12, + color: 'var(--fg3)', + fontFamily: 'var(--font-mono)' + } + }, tt('version'))); +} + +/* ── iPad 外壳 + 应用 ── */ +function TabletApp() { + const [view, setView] = useState('connect'); + const [status, setStatus] = useState('off'); + const [code, setCode] = useState('AUTO'); + const [lang, setLang] = useState('zh'); + const [dark, setDark] = useState(false); + const [free, setFree] = useState(true); + const [mins, setMins] = useState(6); + const [adDone, setAdDone] = useState(false); + const [elapsed, setElapsed] = useState(0); + const timer = useRef(null); + const t = makeT(lang), + tt = makeTT(lang); + const smart = code === 'AUTO'; + const server = smart ? SERVERS.reduce((a, b) => a.ping < b.ping ? a : b) : SERVERS.find(s => s.code === code); + const titles = { + connect: tt('navConnect'), + nodes: tt('navNodes'), + stats: tt('navStats'), + account: tt('navAccount') + }; + function toggle() { + if (status === 'off') { + setStatus('connecting'); + setTimeout(() => { + setStatus('on'); + setElapsed(0); + }, 1500); + } else setStatus('off'); + } + function pick(c) { + setCode(c); + setView('connect'); + if (status === 'on') { + setStatus('connecting'); + setTimeout(() => { + setStatus('on'); + setElapsed(0); + }, 1200); + } + } + useEffect(() => { + if (status === 'on') timer.current = setInterval(() => setElapsed(e => e + 1), 1000); + return () => clearInterval(timer.current); + }, [status]); + let main; + if (view === 'connect') main = /*#__PURE__*/React.createElement(TabConnect, { + t: t, + tt: tt, + lang: lang, + status: status, + onToggle: toggle, + server: server, + smart: smart, + free: free, + mins: mins, + adDone: adDone, + onAd: () => setAdDone(true), + elapsed: elapsed, + goNodes: () => setView('nodes') + });else if (view === 'nodes') main = /*#__PURE__*/React.createElement(TabNodes, { + t: t, + tt: tt, + lang: lang, + current: code, + onPick: pick + });else if (view === 'stats') main = /*#__PURE__*/React.createElement(TabStats, { + tt: tt, + lang: lang + });else main = /*#__PURE__*/React.createElement(TabAccount, { + t: t, + tt: tt, + lang: lang, + setLang: setLang, + dark: dark, + setDark: setDark, + free: free, + setFree: setFree + }); + return /*#__PURE__*/React.createElement("div", { + style: { + background: '#1a1714', + borderRadius: 34, + padding: 16, + boxShadow: '0 0 0 1px rgba(0,0,0,0.3), 0 30px 80px rgba(0,0,0,0.45)' + } + }, /*#__PURE__*/React.createElement("div", { + "data-theme": dark ? 'dark' : 'light', + style: { + width: 1180, + height: 820, + borderRadius: 20, + overflow: 'hidden', + background: 'var(--bg)', + display: 'flex', + flexDirection: 'column', + fontFamily: 'var(--font-sans)', + position: 'relative' + } + }, /*#__PURE__*/React.createElement("div", { + style: { + height: 30, + flexShrink: 0, + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + padding: '0 22px', + fontFamily: 'var(--font-sans)', + fontSize: 12.5, + fontWeight: 600, + color: 'var(--fg1)' + } + }, /*#__PURE__*/React.createElement("span", null, "9:41"), /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + alignItems: 'center', + gap: 8 + } + }, /*#__PURE__*/React.createElement(Icon, { + name: "wifi", + size: 15, + stroke: 2.2, + color: "var(--fg1)" + }), /*#__PURE__*/React.createElement("span", { + style: { + fontFamily: 'var(--font-mono)', + fontSize: 11.5 + } + }, "87%"), /*#__PURE__*/React.createElement("div", { + style: { + width: 24, + height: 12, + border: '1.5px solid var(--fg2)', + borderRadius: 3.5, + padding: 1.5, + boxSizing: 'border-box' + } + }, /*#__PURE__*/React.createElement("div", { + style: { + width: '80%', + height: '100%', + background: 'var(--fg1)', + borderRadius: 1.5 + } + })))), /*#__PURE__*/React.createElement("div", { + style: { + flex: 1, + minHeight: 0, + display: 'flex' + } + }, /*#__PURE__*/React.createElement(TabRail, { + tt: tt, + view: view, + setView: setView, + free: free, + t: t + }), /*#__PURE__*/React.createElement("div", { + style: { + flex: 1, + minWidth: 0, + display: 'flex', + flexDirection: 'column' + } + }, /*#__PURE__*/React.createElement("div", { + style: { + height: 56, + flexShrink: 0, + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + padding: '0 28px' + } + }, /*#__PURE__*/React.createElement("div", { + style: { + fontFamily: 'var(--font-display)', + fontWeight: 700, + fontSize: 19, + color: 'var(--fg1)' + } + }, titles[view]), /*#__PURE__*/React.createElement("span", { + style: { + fontFamily: 'var(--font-mono)', + fontSize: 12.5, + fontWeight: 600, + color: status === 'on' ? 'var(--success)' : 'var(--fg3)', + whiteSpace: 'nowrap' + } + }, status === 'on' ? '● ' + server.code : '○')), /*#__PURE__*/React.createElement("div", { + style: { + flex: 1, + minHeight: 0 + } + }, main))))); +} +Object.assign(window, { + TabletApp +}); +})(); } catch (e) { __ds_ns.__errors.push({ path: "ui_kits/tablet/tabapp.jsx", error: String((e && e.message) || e) }); } + +// ui_kits/usercenter/ucapp.jsx +try { (() => { +/* ucapp.jsx — 穿山甲 Web 用户中心 · views */ +const { + useState: useStateU +} = React; +const ucCard = { + background: 'var(--surface)', + border: '1px solid var(--border)', + borderRadius: 'var(--radius-xl)', + boxShadow: 'var(--shadow-sm)' +}; +const ucInput = { + border: '1.5px solid var(--border-strong)', + borderRadius: 'var(--radius-md)', + padding: '12px 14px', + fontFamily: 'var(--font-sans)', + fontSize: 14.5, + color: 'var(--fg1)', + background: 'var(--surface)', + outline: 'none', + width: '100%', + boxSizing: 'border-box' +}; + +/* viewport hook — mobile ≤ 700px */ +function useIsMobileUC() { + const [m, setM] = useStateU(typeof window !== 'undefined' && window.innerWidth <= 700); + React.useEffect(() => { + const f = () => setM(window.innerWidth <= 700); + window.addEventListener('resize', f); + return () => window.removeEventListener('resize', f); + }, []); + return m; +} +function UCLang({ + lang, + setLang +}) { + return /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + background: 'var(--bg-subtle)', + borderRadius: 999, + padding: 3, + gap: 2 + } + }, [['zh', '中文'], ['en', 'EN']].map(([v, l]) => /*#__PURE__*/React.createElement("button", { + key: v, + onClick: () => setLang(v), + style: { + border: 'none', + cursor: 'pointer', + borderRadius: 999, + padding: '5px 12px', + fontFamily: 'var(--font-sans)', + fontSize: 12.5, + fontWeight: 700, + background: lang === v ? 'var(--accent)' : 'transparent', + color: lang === v ? 'var(--fg-on-accent)' : 'var(--fg3)' + } + }, l))); +} + +/* ───────── Login (+ PRO 2FA step) ───────── */ +function UCLogin({ + t, + lang, + setLang, + onDone +}) { + const [email, setEmail] = useStateU(''); + const [pw, setPw] = useStateU(''); + const [step, setStep] = useStateU('cred'); // cred | totp + const [otp, setOtp] = useStateU(''); + const ok = /\S+@\S+\.\S+/.test(email) && pw.length > 0; + return /*#__PURE__*/React.createElement("div", { + style: { + minHeight: '100vh', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + background: 'var(--bg)', + padding: 24 + } + }, /*#__PURE__*/React.createElement("div", { + style: { + ...ucCard, + width: 400, + maxWidth: '100%', + padding: '36px 34px', + boxSizing: 'border-box' + } + }, /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + marginBottom: 26 + } + }, /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + alignItems: 'center', + gap: 10 + } + }, /*#__PURE__*/React.createElement(UCMark, { + size: 32 + }), /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("div", { + style: { + fontFamily: 'var(--font-display)', + fontWeight: 700, + fontSize: 17, + color: 'var(--fg1)', + lineHeight: 1 + } + }, "\u7A7F\u5C71\u7532"), /*#__PURE__*/React.createElement("div", { + style: { + fontSize: 8.5, + fontWeight: 600, + letterSpacing: '0.2em', + color: 'var(--accent)', + marginTop: 3 + } + }, "PANGOLIN"))), /*#__PURE__*/React.createElement(UCLang, { + lang: lang, + setLang: setLang + })), step === 'cred' ? /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("div", { + style: { + fontFamily: 'var(--font-display)', + fontWeight: 700, + fontSize: 22, + color: 'var(--fg1)' + } + }, t('loginTitle')), /*#__PURE__*/React.createElement("div", { + style: { + fontSize: 13.5, + color: 'var(--fg3)', + margin: '6px 0 24px' + } + }, t('loginSub')), /*#__PURE__*/React.createElement("label", { + style: { + display: 'block', + marginBottom: 14 + } + }, /*#__PURE__*/React.createElement("span", { + style: { + display: 'block', + fontSize: 12.5, + fontWeight: 600, + color: 'var(--fg2)', + marginBottom: 6 + } + }, t('emailLabel')), /*#__PURE__*/React.createElement("input", { + style: ucInput, + value: email, + onChange: e => setEmail(e.target.value), + placeholder: t('emailPh') + })), /*#__PURE__*/React.createElement("label", { + style: { + display: 'block' + } + }, /*#__PURE__*/React.createElement("span", { + style: { + display: 'block', + fontSize: 12.5, + fontWeight: 600, + color: 'var(--fg2)', + marginBottom: 6 + } + }, t('pwLabel')), /*#__PURE__*/React.createElement("input", { + style: ucInput, + type: "password", + value: pw, + onChange: e => setPw(e.target.value), + placeholder: t('pwPh') + })), /*#__PURE__*/React.createElement("div", { + style: { + textAlign: 'right', + margin: '10px 0 18px' + } + }, /*#__PURE__*/React.createElement("span", { + style: { + fontSize: 13, + color: 'var(--accent)', + fontWeight: 600, + cursor: 'pointer' + } + }, t('forgotPw'))), /*#__PURE__*/React.createElement("button", { + onClick: ok ? () => setStep('totp') : undefined, + disabled: !ok, + style: { + width: '100%', + border: 'none', + borderRadius: 'var(--radius-full)', + padding: '13px', + fontFamily: 'var(--font-sans)', + fontWeight: 700, + fontSize: 15, + cursor: ok ? 'pointer' : 'not-allowed', + background: 'var(--accent)', + color: 'var(--fg-on-accent)', + opacity: ok ? 1 : 0.45 + } + }, t('doLogin')), /*#__PURE__*/React.createElement("div", { + style: { + textAlign: 'center', + fontSize: 12.5, + color: 'var(--fg3)', + marginTop: 18 + } + }, t('noAccount'))) : /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + alignItems: 'center', + gap: 9, + marginBottom: 6 + } + }, /*#__PURE__*/React.createElement(UCIcon, { + name: "shield-check", + size: 20, + color: "var(--accent)" + }), /*#__PURE__*/React.createElement("span", { + style: { + fontFamily: 'var(--font-display)', + fontWeight: 700, + fontSize: 20, + color: 'var(--fg1)' + } + }, t('twoFATitle')), /*#__PURE__*/React.createElement("span", { + style: { + fontSize: 10.5, + fontWeight: 700, + color: 'var(--accent)', + background: 'var(--accent-subtle)', + border: '1px solid var(--accent-border)', + padding: '2px 8px', + borderRadius: 999 + } + }, "PRO")), /*#__PURE__*/React.createElement("div", { + style: { + fontSize: 13, + color: 'var(--fg3)', + margin: '4px 0 20px', + lineHeight: 1.6 + } + }, t('twoFAHint')), /*#__PURE__*/React.createElement("input", { + value: otp, + onChange: e => setOtp(e.target.value.replace(/\D/g, '').slice(0, 6)), + placeholder: "\xB7\xB7\xB7\xB7\xB7\xB7", + style: { + ...ucInput, + fontFamily: 'var(--font-mono)', + fontSize: 22, + letterSpacing: '0.5em', + textAlign: 'center', + padding: '14px' + } + }), /*#__PURE__*/React.createElement("button", { + onClick: otp.length === 6 ? onDone : undefined, + disabled: otp.length !== 6, + style: { + width: '100%', + border: 'none', + borderRadius: 'var(--radius-full)', + padding: '13px', + marginTop: 16, + fontFamily: 'var(--font-sans)', + fontWeight: 700, + fontSize: 15, + cursor: otp.length === 6 ? 'pointer' : 'not-allowed', + background: 'var(--accent)', + color: 'var(--fg-on-accent)', + opacity: otp.length === 6 ? 1 : 0.45 + } + }, t('twoFAConfirm')), /*#__PURE__*/React.createElement("div", { + onClick: () => setStep('cred'), + style: { + textAlign: 'center', + fontSize: 12.5, + color: 'var(--fg3)', + marginTop: 18, + cursor: 'pointer' + } + }, t('twoFABack'))))); +} + +/* ───────── Overview ───────── */ +function UCOverview({ + t, + lang, + free, + mobile, + goSub, + goRedeem +}) { + const vals = [1.8, 2.4, 1.2, 3.1, 4.0, 5.2, 2.6]; + const max = 5.2; + const labels = lang === 'zh' ? ['一', '二', '三', '四', '五', '六', '日'] : ['M', 'T', 'W', 'T', 'F', 'S', 'S']; + return /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + flexDirection: 'column', + gap: 20 + } + }, /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("div", { + style: { + fontFamily: 'var(--font-display)', + fontSize: 25, + fontWeight: 700, + color: 'var(--fg1)' + } + }, t('greeting')), /*#__PURE__*/React.createElement("div", { + style: { + fontSize: 13.5, + color: 'var(--fg3)', + marginTop: 3 + } + }, "me@pangolin.vpn")), /*#__PURE__*/React.createElement("div", { + style: { + ...ucCard, + background: 'linear-gradient(150deg,var(--clay-600),var(--clay-800))', + border: 'none', + color: '#fff', + padding: '20px 24px', + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + gap: 16, + flexWrap: 'wrap' + } + }, /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + alignItems: 'center', + gap: 14 + } + }, /*#__PURE__*/React.createElement("div", { + style: { + width: 46, + height: 46, + borderRadius: '50%', + background: 'rgba(255,255,255,.18)', + display: 'flex', + alignItems: 'center', + justifyContent: 'center' + } + }, /*#__PURE__*/React.createElement(UCIcon, { + name: "crown", + size: 23, + color: "#fff" + })), /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("div", { + style: { + fontSize: 12, + opacity: .8 + } + }, t('curPlan')), /*#__PURE__*/React.createElement("div", { + style: { + fontFamily: 'var(--font-display)', + fontSize: 19, + fontWeight: 700, + marginTop: 2 + } + }, free ? t('freePlan') : t('proMember')), /*#__PURE__*/React.createElement("div", { + style: { + fontSize: 11.5, + opacity: .8, + marginTop: 3, + fontFamily: 'var(--font-mono)', + whiteSpace: 'nowrap' + } + }, free ? t('quotaFree') : `${t('expires')} 2026-12-31`))), /*#__PURE__*/React.createElement("button", { + onClick: goRedeem, + style: { + border: 'none', + background: '#fff', + color: 'var(--clay-700)', + fontWeight: 700, + fontSize: 13.5, + padding: '11px 20px', + borderRadius: 'var(--radius-full)', + cursor: 'pointer', + display: 'inline-flex', + alignItems: 'center', + gap: 7 + } + }, /*#__PURE__*/React.createElement(UCIcon, { + name: "ticket", + size: 15, + color: "var(--clay-700)" + }), t('renew'))), /*#__PURE__*/React.createElement("div", { + style: { + display: 'grid', + gridTemplateColumns: 'repeat(auto-fit,minmax(160px,1fr))', + gap: 14 + } + }, [['clock', t('quotaToday'), free ? '6' : '∞', free ? 'min' : ''], ['arrow-down', t('dataToday'), '0.8', 'GB'], ['smartphone', t('devices'), '2 / 5', '']].map(([ic, l, v, u]) => /*#__PURE__*/React.createElement("div", { + key: l, + style: { + ...ucCard, + padding: '16px 18px' + } + }, /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + alignItems: 'center', + gap: 7, + color: 'var(--fg3)', + fontSize: 12, + fontWeight: 600 + } + }, /*#__PURE__*/React.createElement(UCIcon, { + name: ic, + size: 15, + color: "var(--accent)" + }), l), /*#__PURE__*/React.createElement("div", { + style: { + fontFamily: 'var(--font-mono)', + fontSize: 23, + fontWeight: 500, + color: 'var(--fg1)', + marginTop: 7 + } + }, v, u && /*#__PURE__*/React.createElement("span", { + style: { + fontSize: 12, + color: 'var(--fg3)' + } + }, " ", u))))), /*#__PURE__*/React.createElement("div", { + style: { + display: 'grid', + gridTemplateColumns: mobile ? '1fr' : '1.6fr 1fr', + gap: 14, + alignItems: 'start' + } + }, /*#__PURE__*/React.createElement("div", { + style: { + ...ucCard, + padding: '18px 22px' + } + }, /*#__PURE__*/React.createElement("div", { + style: { + fontSize: 13.5, + fontWeight: 600, + color: 'var(--fg1)', + marginBottom: 16 + } + }, t('usageTitle')), /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + alignItems: 'flex-end', + gap: 14, + height: 110 + } + }, vals.map((v, i) => /*#__PURE__*/React.createElement("div", { + key: i, + style: { + flex: 1, + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + gap: 6 + } + }, /*#__PURE__*/React.createElement("div", { + style: { + fontFamily: 'var(--font-mono)', + fontSize: 9.5, + color: 'var(--fg3)' + } + }, v), /*#__PURE__*/React.createElement("div", { + style: { + width: '100%', + maxWidth: 30, + height: `${v / max * 76}px`, + background: 'var(--accent)', + borderRadius: '5px 5px 0 0', + opacity: .85 + } + }), /*#__PURE__*/React.createElement("div", { + style: { + fontSize: 10.5, + color: 'var(--fg3)' + } + }, labels[i]))))), /*#__PURE__*/React.createElement("div", { + style: { + ...ucCard, + padding: '16px 18px' + } + }, /*#__PURE__*/React.createElement("div", { + style: { + fontSize: 13.5, + fontWeight: 600, + color: 'var(--fg1)', + marginBottom: 12 + } + }, t('quickSub')), /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + flexDirection: 'column', + gap: 8 + } + }, [['link', t('qaSub'), goSub], ['ticket', t('qaRedeem'), goRedeem], ['download', t('qaApp'), null]].map(([ic, l, fn]) => /*#__PURE__*/React.createElement("button", { + key: l, + onClick: fn || undefined, + style: { + display: 'flex', + alignItems: 'center', + gap: 11, + width: '100%', + textAlign: 'left', + cursor: 'pointer', + background: 'var(--bg-subtle)', + border: '1px solid var(--border)', + borderRadius: 'var(--radius-md)', + padding: '11px 13px' + } + }, /*#__PURE__*/React.createElement(UCIcon, { + name: ic, + size: 16, + color: "var(--accent)" + }), /*#__PURE__*/React.createElement("span", { + style: { + flex: 1, + fontSize: 13.5, + fontWeight: 600, + color: 'var(--fg1)' + } + }, l), /*#__PURE__*/React.createElement(UCIcon, { + name: "chevron-right", + size: 15, + color: "var(--fg3)" + }))))))); +} + +/* ───────── Subscription ───────── */ +function UCSub({ + t, + lang, + mobile +}) { + const [copied, setCopied] = useStateU(false); + const [reset, setReset] = useStateU(false); + const url = 'https://sub.pangolin.vpn/s/8f3kx92m' + (reset ? '-new' : ''); + const clients = [{ + name: '穿山甲 App', + sub: 'iOS / Android / 桌面', + icon: 'shield-check', + accent: true + }, { + name: 'Shadowrocket', + sub: 'iOS', + icon: 'external-link' + }, { + name: 'Clash Verge', + sub: 'Windows / macOS', + icon: 'external-link' + }, { + name: 'v2rayN', + sub: 'Windows', + icon: 'external-link' + }, { + name: 'sing-box', + sub: '全平台', + icon: 'external-link' + }]; + return /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + flexDirection: 'column', + gap: 18, + maxWidth: 720 + } + }, /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("div", { + style: { + fontFamily: 'var(--font-display)', + fontSize: 25, + fontWeight: 700, + color: 'var(--fg1)' + } + }, t('subTitle')), /*#__PURE__*/React.createElement("div", { + style: { + fontSize: 13, + color: 'var(--fg3)', + marginTop: 5, + lineHeight: 1.6 + } + }, t('subDesc'))), /*#__PURE__*/React.createElement("div", { + style: { + display: 'grid', + gridTemplateColumns: mobile ? '1fr' : '1fr auto', + gap: 14, + alignItems: 'start' + } + }, /*#__PURE__*/React.createElement("div", { + style: { + ...ucCard, + padding: '18px 20px' + } + }, /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + alignItems: 'center', + gap: 9, + background: 'var(--bg-subtle)', + border: '1px solid var(--border)', + borderRadius: 'var(--radius-md)', + padding: '11px 13px', + marginBottom: 12 + } + }, /*#__PURE__*/React.createElement(UCIcon, { + name: "link", + size: 15, + color: "var(--fg3)" + }), /*#__PURE__*/React.createElement("span", { + style: { + flex: 1, + fontFamily: 'var(--font-mono)', + fontSize: 12.5, + color: 'var(--fg1)', + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap' + } + }, url)), /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + gap: 10, + flexWrap: 'wrap' + } + }, /*#__PURE__*/React.createElement("button", { + onClick: () => { + setCopied(true); + setTimeout(() => setCopied(false), 1800); + }, + style: { + display: 'inline-flex', + alignItems: 'center', + gap: 7, + border: 'none', + borderRadius: 'var(--radius-full)', + padding: '10px 18px', + background: 'var(--accent)', + color: '#fff', + fontWeight: 700, + fontSize: 13.5, + cursor: 'pointer' + } + }, /*#__PURE__*/React.createElement(UCIcon, { + name: copied ? 'check' : 'copy', + size: 15, + color: "#fff" + }), copied ? t('subCopied') : t('subCopy')), /*#__PURE__*/React.createElement("button", { + onClick: () => setReset(true), + style: { + display: 'inline-flex', + alignItems: 'center', + gap: 7, + border: '1.5px solid var(--border-strong)', + borderRadius: 'var(--radius-full)', + padding: '10px 18px', + background: 'transparent', + color: 'var(--fg2)', + fontWeight: 600, + fontSize: 13.5, + cursor: 'pointer' + } + }, /*#__PURE__*/React.createElement(UCIcon, { + name: "refresh-cw", + size: 14, + color: "var(--fg2)" + }), t('subReset'))), /*#__PURE__*/React.createElement("div", { + style: { + fontSize: 11.5, + color: reset ? 'var(--success)' : 'var(--fg3)', + marginTop: 10, + display: 'flex', + alignItems: 'center', + gap: 6 + } + }, reset && /*#__PURE__*/React.createElement(UCIcon, { + name: "check-circle", + size: 13, + color: "var(--success)" + }), reset ? t('subResetOk') : t('subResetSub'))), /*#__PURE__*/React.createElement("div", { + style: { + ...ucCard, + padding: '16px', + width: mobile ? '100%' : 170, + boxSizing: 'border-box', + textAlign: 'center' + } + }, /*#__PURE__*/React.createElement("div", { + style: { + width: 138, + height: 138, + margin: '0 auto', + borderRadius: 'var(--radius-md)', + background: 'var(--bg-subtle)', + border: '1px dashed var(--border-strong)', + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'center', + gap: 8 + } + }, /*#__PURE__*/React.createElement(UCIcon, { + name: "qr-code", + size: 42, + color: "var(--fg3)", + stroke: 1.5 + }), /*#__PURE__*/React.createElement("span", { + style: { + fontSize: 10.5, + color: 'var(--fg3)', + fontWeight: 600 + } + }, t('scanTitle'))), /*#__PURE__*/React.createElement("div", { + style: { + fontSize: 10.5, + color: 'var(--fg3)', + marginTop: 10, + lineHeight: 1.5 + } + }, t('scanSub')))), /*#__PURE__*/React.createElement("div", { + style: { + ...ucCard, + padding: '18px 20px' + } + }, /*#__PURE__*/React.createElement("div", { + style: { + fontSize: 14.5, + fontWeight: 700, + color: 'var(--fg1)' + } + }, t('importTitle')), /*#__PURE__*/React.createElement("div", { + style: { + fontSize: 12.5, + color: 'var(--fg3)', + margin: '4px 0 14px' + } + }, t('importSub')), /*#__PURE__*/React.createElement("div", { + style: { + display: 'grid', + gridTemplateColumns: 'repeat(auto-fill,minmax(190px,1fr))', + gap: 10 + } + }, clients.map(c => /*#__PURE__*/React.createElement("button", { + key: c.name, + style: { + display: 'flex', + alignItems: 'center', + gap: 11, + textAlign: 'left', + cursor: 'pointer', + background: c.accent ? 'var(--accent-subtle)' : 'var(--surface)', + border: `1px solid ${c.accent ? 'var(--accent-border)' : 'var(--border)'}`, + borderRadius: 'var(--radius-lg)', + padding: '12px 14px' + } + }, /*#__PURE__*/React.createElement("div", { + style: { + width: 34, + height: 34, + borderRadius: 'var(--radius-md)', + background: c.accent ? 'var(--accent)' : 'var(--bg-subtle)', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + flexShrink: 0 + } + }, /*#__PURE__*/React.createElement(UCIcon, { + name: c.icon, + size: 17, + color: c.accent ? '#fff' : 'var(--accent)' + })), /*#__PURE__*/React.createElement("div", { + style: { + minWidth: 0 + } + }, /*#__PURE__*/React.createElement("div", { + style: { + fontSize: 13.5, + fontWeight: 600, + color: 'var(--fg1)', + whiteSpace: 'nowrap' + } + }, c.name), /*#__PURE__*/React.createElement("div", { + style: { + fontSize: 11, + color: 'var(--fg3)' + } + }, c.sub))))), /*#__PURE__*/React.createElement("div", { + style: { + fontSize: 11.5, + color: 'var(--fg3)', + marginTop: 12 + } + }, t('fmtNote')))); +} + +/* ───────── Redeem ───────── */ +function UCRedeem({ + t, + lang +}) { + const [code, setCode] = useStateU(''); + const [ok, setOk] = useStateU(false); + const channels = [{ + icon: 'shopping-bag', + name: t('chStore'), + sub: t('chStoreSub'), + accent: true + }, { + icon: 'credit-card', + name: 'USDT (TRC20)', + sub: t('chUsdtSub') + }, { + icon: 'send', + name: 'Telegram', + sub: '@PangolinVPN_bot', + mono: true + }, { + icon: 'message-circle', + name: 'LINE', + sub: '@pangolinvpn', + mono: true + }, { + icon: 'mail', + name: t('chEmail'), + sub: 'buy@pangolin.vpn', + mono: true + }]; + return /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + flexDirection: 'column', + gap: 18, + maxWidth: 680 + } + }, /*#__PURE__*/React.createElement("div", { + style: { + fontFamily: 'var(--font-display)', + fontSize: 25, + fontWeight: 700, + color: 'var(--fg1)' + } + }, t('navRedeem')), /*#__PURE__*/React.createElement("div", { + style: { + ...ucCard, + padding: '20px 22px' + } + }, /*#__PURE__*/React.createElement("div", { + style: { + fontSize: 14.5, + fontWeight: 700, + color: 'var(--fg1)', + marginBottom: 12 + } + }, t('redeemTitle')), ok ? /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + alignItems: 'center', + gap: 9, + color: 'var(--success)', + fontWeight: 600, + fontSize: 15, + padding: '4px 0' + } + }, /*#__PURE__*/React.createElement(UCIcon, { + name: "check-circle", + size: 19, + color: "var(--success)" + }), t('redeemOk')) : /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + gap: 10 + } + }, /*#__PURE__*/React.createElement("input", { + value: code, + onChange: e => setCode(e.target.value.toUpperCase()), + placeholder: t('redeemPh'), + style: { + ...ucInput, + fontFamily: 'var(--font-mono)', + letterSpacing: '0.08em' + } + }), /*#__PURE__*/React.createElement("button", { + onClick: () => code.length >= 4 && setOk(true), + style: { + border: 'none', + borderRadius: 'var(--radius-md)', + padding: '0 22px', + background: 'var(--accent)', + color: '#fff', + fontWeight: 700, + fontSize: 14, + cursor: 'pointer', + flexShrink: 0 + } + }, t('redeemBtn')))), /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("div", { + style: { + fontSize: 14.5, + fontWeight: 700, + color: 'var(--fg1)', + marginBottom: 5 + } + }, t('buyTitle')), /*#__PURE__*/React.createElement("div", { + style: { + fontSize: 12.5, + color: 'var(--fg3)', + lineHeight: 1.5, + marginBottom: 13 + } + }, t('buySub')), /*#__PURE__*/React.createElement("div", { + style: { + display: 'grid', + gridTemplateColumns: 'repeat(auto-fit,minmax(240px,1fr))', + gap: 11 + } + }, channels.map(c => /*#__PURE__*/React.createElement("button", { + key: c.name, + style: { + display: 'flex', + alignItems: 'center', + gap: 12, + textAlign: 'left', + cursor: 'pointer', + background: c.accent ? 'var(--accent-subtle)' : 'var(--surface)', + border: `1px solid ${c.accent ? 'var(--accent-border)' : 'var(--border)'}`, + borderRadius: 'var(--radius-lg)', + padding: '13px 15px', + boxShadow: 'var(--shadow-sm)' + } + }, /*#__PURE__*/React.createElement("div", { + style: { + width: 38, + height: 38, + borderRadius: 'var(--radius-md)', + background: c.accent ? 'var(--accent)' : 'var(--bg-subtle)', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + flexShrink: 0 + } + }, /*#__PURE__*/React.createElement(UCIcon, { + name: c.icon, + size: 18, + color: c.accent ? '#fff' : 'var(--accent)' + })), /*#__PURE__*/React.createElement("div", { + style: { + flex: 1, + minWidth: 0 + } + }, /*#__PURE__*/React.createElement("div", { + style: { + fontSize: 14, + fontWeight: 600, + color: 'var(--fg1)' + } + }, c.name), /*#__PURE__*/React.createElement("div", { + style: { + fontSize: 11.5, + color: 'var(--fg3)', + fontFamily: c.mono ? 'var(--font-mono)' : 'var(--font-sans)', + whiteSpace: 'nowrap', + overflow: 'hidden', + textOverflow: 'ellipsis' + } + }, c.sub)), /*#__PURE__*/React.createElement(UCIcon, { + name: "external-link", + size: 15, + color: "var(--fg3)" + })))))); +} + +/* ───────── Invite ───────── */ +function UCInvite({ + t, + lang +}) { + const [copied, setCopied] = useStateU(false); + const records = [{ + mail: 'k***@gmail.com', + date: '2026-06-08', + paid: true + }, { + mail: 'w***@outlook.com', + date: '2026-06-05', + paid: false + }, { + mail: 'z***@qq.com', + date: '2026-05-30', + paid: true + }]; + return /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + flexDirection: 'column', + gap: 18, + maxWidth: 680 + } + }, /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("div", { + style: { + fontFamily: 'var(--font-display)', + fontSize: 25, + fontWeight: 700, + color: 'var(--fg1)' + } + }, t('inviteTitle')), /*#__PURE__*/React.createElement("div", { + style: { + fontSize: 13, + color: 'var(--fg3)', + marginTop: 5, + lineHeight: 1.6 + } + }, t('inviteSub'))), /*#__PURE__*/React.createElement("div", { + style: { + ...ucCard, + padding: '18px 20px' + } + }, /*#__PURE__*/React.createElement("div", { + style: { + fontSize: 12.5, + fontWeight: 600, + color: 'var(--fg2)', + marginBottom: 8 + } + }, t('inviteLink')), /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + gap: 10 + } + }, /*#__PURE__*/React.createElement("div", { + style: { + flex: 1, + display: 'flex', + alignItems: 'center', + gap: 9, + background: 'var(--bg-subtle)', + border: '1px solid var(--border)', + borderRadius: 'var(--radius-md)', + padding: '11px 13px', + minWidth: 0 + } + }, /*#__PURE__*/React.createElement(UCIcon, { + name: "gift", + size: 15, + color: "var(--fg3)" + }), /*#__PURE__*/React.createElement("span", { + style: { + fontFamily: 'var(--font-mono)', + fontSize: 12.5, + color: 'var(--fg1)', + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap' + } + }, "https://pangolin.vpn/r/PG8F3K")), /*#__PURE__*/React.createElement("button", { + onClick: () => { + setCopied(true); + setTimeout(() => setCopied(false), 1800); + }, + style: { + display: 'inline-flex', + alignItems: 'center', + gap: 7, + border: 'none', + borderRadius: 'var(--radius-md)', + padding: '0 18px', + background: 'var(--accent)', + color: '#fff', + fontWeight: 700, + fontSize: 13.5, + cursor: 'pointer', + flexShrink: 0 + } + }, /*#__PURE__*/React.createElement(UCIcon, { + name: copied ? 'check' : 'copy', + size: 15, + color: "#fff" + }), copied ? t('subCopied') : t('subCopy')))), /*#__PURE__*/React.createElement("div", { + style: { + display: 'grid', + gridTemplateColumns: 'repeat(auto-fit,minmax(150px,1fr))', + gap: 14 + } + }, [[t('invited'), '8', ''], [t('paidUsers'), '3', ''], [t('earned'), '¥45', '']].map(([l, v]) => /*#__PURE__*/React.createElement("div", { + key: l, + style: { + ...ucCard, + padding: '16px 18px' + } + }, /*#__PURE__*/React.createElement("div", { + style: { + fontSize: 12, + color: 'var(--fg3)', + fontWeight: 600 + } + }, l), /*#__PURE__*/React.createElement("div", { + style: { + fontFamily: 'var(--font-mono)', + fontSize: 23, + fontWeight: 500, + color: 'var(--fg1)', + marginTop: 7 + } + }, v)))), /*#__PURE__*/React.createElement("div", { + style: { + ...ucCard, + overflow: 'hidden' + } + }, /*#__PURE__*/React.createElement("div", { + style: { + padding: '13px 18px', + fontSize: 13.5, + fontWeight: 700, + color: 'var(--fg1)', + borderBottom: '1px solid var(--border)' + } + }, t('inviteRecord')), records.map((r, i) => /*#__PURE__*/React.createElement("div", { + key: i, + style: { + display: 'flex', + alignItems: 'center', + gap: 12, + padding: '12px 18px', + borderBottom: i < records.length - 1 ? '1px solid var(--border)' : 'none' + } + }, /*#__PURE__*/React.createElement("span", { + style: { + flex: 1, + fontFamily: 'var(--font-mono)', + fontSize: 13, + color: 'var(--fg1)' + } + }, r.mail), /*#__PURE__*/React.createElement("span", { + style: { + fontFamily: 'var(--font-mono)', + fontSize: 12, + color: 'var(--fg3)' + } + }, r.date), /*#__PURE__*/React.createElement("span", { + style: { + fontSize: 11, + fontWeight: 600, + padding: '3px 10px', + borderRadius: 999, + background: r.paid ? 'var(--success-subtle)' : 'var(--bg-subtle)', + color: r.paid ? 'var(--success)' : 'var(--fg3)' + } + }, r.paid ? t('recPaid') : t('recRegistered')))))); +} + +/* ───────── App shell ───────── */ +function UserCenterApp() { + const [stage, setStage] = useStateU('app'); // app | login — opens on the dashboard + const [view, setView] = useStateU('overview'); + const [lang, setLang] = useStateU('zh'); + const [free] = useStateU(false); + const [dir, setDir] = useStateU(0); // -1 左进 / 1 右进 / 0 无动画 + const mobile = useIsMobileUC(); + const t = ucT(lang); + const touchRef = React.useRef({ + x: 0, + y: 0 + }); + const ORDER = ['overview', 'sub', 'redeem', 'invite']; + function go(id) { + const a = ORDER.indexOf(view), + b = ORDER.indexOf(id); + setDir(b > a ? 1 : b < a ? -1 : 0); + setView(id); + setTimeout(() => setDir(0), 300); + } + function onTouchStart(e) { + const p = e.touches ? e.touches[0] : e; + touchRef.current = { + x: p.clientX, + y: p.clientY + }; + } + function onTouchEnd(e) { + const p = e.changedTouches ? e.changedTouches[0] : e; + const dx = p.clientX - touchRef.current.x, + dy = p.clientY - touchRef.current.y; + if (Math.abs(dx) > 60 && Math.abs(dx) > Math.abs(dy) * 1.5) { + const i = ORDER.indexOf(view); + const ni = dx < 0 ? Math.min(ORDER.length - 1, i + 1) : Math.max(0, i - 1); + if (ni !== i) go(ORDER[ni]); + } + } + if (stage === 'login') return /*#__PURE__*/React.createElement(UCLogin, { + t: t, + lang: lang, + setLang: setLang, + onDone: () => setStage('app') + }); + const nav = [['overview', 'layout-dashboard', t('navOverview')], ['sub', 'link', t('navSub')], ['redeem', 'ticket', t('navRedeem')], ['invite', 'users', t('navInvite')]]; + let main; + if (view === 'overview') main = /*#__PURE__*/React.createElement(UCOverview, { + t: t, + lang: lang, + free: free, + mobile: mobile, + goSub: () => setView('sub'), + goRedeem: () => setView('redeem') + });else if (view === 'sub') main = /*#__PURE__*/React.createElement(UCSub, { + t: t, + lang: lang, + mobile: mobile + });else if (view === 'redeem') main = /*#__PURE__*/React.createElement(UCRedeem, { + t: t, + lang: lang + });else main = /*#__PURE__*/React.createElement(UCInvite, { + t: t, + lang: lang + }); + const navBtns = nav.map(([id, ic, l]) => /*#__PURE__*/React.createElement("button", { + key: id, + onClick: () => go(id), + style: { + display: 'inline-flex', + alignItems: 'center', + gap: 7, + border: 'none', + cursor: 'pointer', + borderRadius: 'var(--radius-md)', + padding: '8px 13px', + flexShrink: 0, + background: view === id ? 'var(--accent-subtle)' : 'transparent', + color: view === id ? 'var(--accent)' : 'var(--fg2)', + fontFamily: 'var(--font-sans)', + fontSize: 13.5, + fontWeight: view === id ? 700 : 500, + whiteSpace: 'nowrap' + } + }, /*#__PURE__*/React.createElement(UCIcon, { + name: ic, + size: 15, + color: view === id ? 'var(--accent)' : 'var(--fg3)' + }), l)); + return /*#__PURE__*/React.createElement("div", { + style: { + minHeight: '100vh', + background: 'var(--bg)', + fontFamily: 'var(--font-sans)' + } + }, /*#__PURE__*/React.createElement("div", { + style: { + position: 'sticky', + top: 0, + zIndex: 10, + background: 'color-mix(in srgb, var(--bg) 85%, transparent)', + backdropFilter: 'blur(12px)', + borderBottom: '1px solid var(--border)' + } + }, /*#__PURE__*/React.createElement("div", { + style: { + maxWidth: 1000, + margin: '0 auto', + padding: mobile ? '0 16px' : '0 24px', + height: mobile ? 54 : 60, + display: 'flex', + alignItems: 'center', + gap: mobile ? 12 : 22 + } + }, /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + alignItems: 'center', + gap: 9 + } + }, /*#__PURE__*/React.createElement(UCMark, { + size: 26 + }), /*#__PURE__*/React.createElement("span", { + style: { + fontFamily: 'var(--font-display)', + fontWeight: 700, + fontSize: 16.5, + color: 'var(--fg1)' + } + }, "\u7A7F\u5C71\u7532")), !mobile && /*#__PURE__*/React.createElement("nav", { + style: { + display: 'flex', + gap: 4, + flex: 1 + } + }, navBtns), mobile && /*#__PURE__*/React.createElement("div", { + style: { + flex: 1 + } + }), /*#__PURE__*/React.createElement(UCLang, { + lang: lang, + setLang: setLang + }), /*#__PURE__*/React.createElement("button", { + onClick: () => setStage('login'), + style: { + display: 'inline-flex', + alignItems: 'center', + gap: 7, + border: 'none', + background: 'transparent', + cursor: 'pointer', + color: 'var(--fg2)', + fontSize: 13, + fontWeight: 600, + padding: 6 + } + }, /*#__PURE__*/React.createElement(UCIcon, { + name: "log-out", + size: 15, + color: "var(--fg3)" + }), !mobile && t('signOut'))), mobile && /*#__PURE__*/React.createElement("nav", { + style: { + display: 'flex', + gap: 2, + padding: '0 12px 8px', + overflowX: 'auto' + } + }, navBtns)), /*#__PURE__*/React.createElement("div", { + onTouchStart: mobile ? onTouchStart : undefined, + onTouchEnd: mobile ? onTouchEnd : undefined, + style: { + maxWidth: 1000, + margin: '0 auto', + padding: mobile ? '20px 16px 40px' : '30px 24px 48px' + } + }, /*#__PURE__*/React.createElement("div", { + key: view, + onAnimationEnd: () => setDir(0), + style: { + animation: dir !== 0 ? `uc-in-${dir === 1 ? 'l' : 'r'} 200ms var(--ease-out)` : 'none' + } + }, main))); +} +Object.assign(window, { + UserCenterApp +}); +})(); } catch (e) { __ds_ns.__errors.push({ path: "ui_kits/usercenter/ucapp.jsx", error: String((e && e.message) || e) }); } + +// ui_kits/usercenter/ucparts.jsx +try { (() => { +/* ucparts.jsx — 穿山甲 Web 用户中心 · atoms + i18n */ +const UCLUCIDE = { + 'layout-dashboard': '', + link: '', + ticket: '', + users: '', + copy: '', + 'refresh-cw': '', + 'qr-code': '', + download: '', + check: '', + 'check-circle': '', + zap: '', + clock: '', + 'shield-check': '', + crown: '', + 'credit-card': '', + send: '', + 'message-circle': '', + mail: '', + 'shopping-bag': '', + 'log-out': '', + 'external-link': '', + gift: '', + smartphone: '', + lock: '', + 'chevron-right': '', + 'arrow-down': '' +}; +function UCIcon({ + name, + size = 18, + stroke = 2, + color = 'currentColor', + style = {} +}) { + return /*#__PURE__*/React.createElement("svg", { + width: size, + height: size, + viewBox: "0 0 24 24", + fill: "none", + stroke: color, + strokeWidth: stroke, + strokeLinecap: "round", + strokeLinejoin: "round", + style: { + display: 'block', + flexShrink: 0, + ...style + }, + dangerouslySetInnerHTML: { + __html: UCLUCIDE[name] || '' + } + }); +} +function UCMark({ + size = 30, + color = 'var(--accent)' +}) { + return /*#__PURE__*/React.createElement("svg", { + width: size, + height: size, + viewBox: "0 0 96 96", + fill: "none", + style: { + display: 'block' + } + }, /*#__PURE__*/React.createElement("g", { + transform: "translate(96,0) scale(-1,1)" + }, /*#__PURE__*/React.createElement("path", { + d: "M16 59 Q17 51 26 50 Q34 31 45 30 Q55 30 62 37 Q69 41 75 45 Q85 49 89 44 Q92 50 84 52 Q75 53 67 53 Q66 62 60 62 L56 62 Q54 55 49 55 Q47 62 37 62 L33 62 Q31 56 26 56 Q20 59 16 59 Z", + fill: color + }), /*#__PURE__*/React.createElement("g", { + stroke: "#fff", + strokeWidth: "2.4", + strokeLinecap: "round", + strokeLinejoin: "round", + fill: "none", + opacity: "0.5" + }, /*#__PURE__*/React.createElement("path", { + d: "M27 50 Q35 43 43 50" + }), /*#__PURE__*/React.createElement("path", { + d: "M37 48 Q45 41 53 48" + }), /*#__PURE__*/React.createElement("path", { + d: "M47 48 Q55 41 63 49" + })), /*#__PURE__*/React.createElement("circle", { + cx: "19", + cy: "56", + r: "2.2", + fill: "#fff", + opacity: "0.85" + }))); +} +const UCSTRINGS = { + navOverview: { + zh: '概览', + en: 'Overview' + }, + navSub: { + zh: '订阅', + en: 'Subscription' + }, + navRedeem: { + zh: '兑换 & 购买', + en: 'Redeem & buy' + }, + navInvite: { + zh: '邀请返利', + en: 'Referral' + }, + signOut: { + zh: '退出', + en: 'Sign out' + }, + /* login */ + loginTitle: { + zh: '登录用户中心', + en: 'Log in to your account' + }, + loginSub: { + zh: '管理订阅、兑换激活码、查看用量', + en: 'Manage subscription, redeem codes, track usage' + }, + emailLabel: { + zh: '邮箱', + en: 'Email' + }, + emailPh: { + zh: '你的邮箱地址', + en: 'your@email.com' + }, + pwLabel: { + zh: '密码', + en: 'Password' + }, + pwPh: { + zh: '输入密码', + en: 'Enter password' + }, + doLogin: { + zh: '登录', + en: 'Log in' + }, + forgotPw: { + zh: '忘记密码?', + en: 'Forgot password?' + }, + noAccount: { + zh: '没有账户?在 App 内注册', + en: 'No account? Sign up in the app' + }, + /* overview */ + greeting: { + zh: '欢迎回来', + en: 'Welcome back' + }, + curPlan: { + zh: '当前套餐', + en: 'Current plan' + }, + freePlan: { + zh: '免费版', + en: 'Free' + }, + proMember: { + zh: 'PRO 会员', + en: 'PRO member' + }, + expires: { + zh: '有效期至', + en: 'Expires' + }, + renew: { + zh: '续费 / 升级', + en: 'Renew / Upgrade' + }, + quotaToday: { + zh: '今日剩余时长', + en: 'Time left today' + }, + dataToday: { + zh: '今日已用流量', + en: 'Data used today' + }, + devices: { + zh: '在线设备', + en: 'Devices online' + }, + quotaFree: { + zh: '免费版 · 每日 10 分钟', + en: 'Free · 10 min/day' + }, + usageTitle: { + zh: '近 7 日流量 (GB)', + en: 'Last 7 days (GB)' + }, + quickSub: { + zh: '快速操作', + en: 'Quick actions' + }, + qaSub: { + zh: '获取订阅链接', + en: 'Get subscription' + }, + qaRedeem: { + zh: '兑换激活码', + en: 'Redeem a code' + }, + qaApp: { + zh: '下载 App', + en: 'Download apps' + }, + /* subscription */ + subTitle: { + zh: '我的订阅', + en: 'My subscription' + }, + subDesc: { + zh: '订阅链接是你的专属凭证,泄露后他人可使用你的额度。请勿分享。', + en: 'Your subscription link is a private credential. Never share it.' + }, + subCopy: { + zh: '复制链接', + en: 'Copy link' + }, + subCopied: { + zh: '已复制', + en: 'Copied' + }, + subReset: { + zh: '重置链接', + en: 'Reset link' + }, + subResetSub: { + zh: '旧链接立即失效,所有设备需重新导入', + en: 'Old link stops working; re-import on all devices' + }, + subResetOk: { + zh: '已重置,请重新导入', + en: 'Reset — re-import on your devices' + }, + scanTitle: { + zh: '扫码导入', + en: 'Scan to import' + }, + scanSub: { + zh: '用穿山甲 App 或三方客户端扫码', + en: 'Scan with the Pangolin app or a 3rd-party client' + }, + importTitle: { + zh: '一键导入三方客户端', + en: 'One-tap import' + }, + importSub: { + zh: '已安装对应客户端时,点击即自动导入订阅。', + en: 'If the client is installed, tapping imports the subscription automatically.' + }, + fmtNote: { + zh: '同一链接同时兼容 sing-box / Clash.Meta / v2ray 格式。', + en: 'One link serves sing-box / Clash.Meta / v2ray formats.' + }, + /* redeem */ + redeemTitle: { + zh: '兑换激活码', + en: 'Redeem a code' + }, + redeemPh: { + zh: '输入激活码', + en: 'Enter activation code' + }, + redeemBtn: { + zh: '激活', + en: 'Redeem' + }, + redeemOk: { + zh: '激活成功 · 套餐已到账', + en: 'Activated — plan applied' + }, + buyTitle: { + zh: '购买渠道', + en: 'Where to buy' + }, + buySub: { + zh: '本站不直接收款。通过以下渠道购买激活码,回到本页兑换即可:', + en: 'We never take payment on this site. Buy a code via a channel below, then redeem here:' + }, + chStore: { + zh: '自助发卡商店', + en: 'Self-serve store' + }, + chStoreSub: { + zh: '支付宝 / 微信 · 自动发码', + en: 'Alipay / WeChat · instant code' + }, + chUsdtSub: { + zh: '链上转账 · 最隐私 · 自动发码', + en: 'On-chain · most private · instant code' + }, + chEmail: { + zh: '邮箱客服', + en: 'Email support' + }, + /* invite */ + inviteTitle: { + zh: '邀请返利', + en: 'Referral' + }, + inviteSub: { + zh: '好友通过你的链接注册并付费,你获得其首单 20% 余额返利,可抵扣续费。', + en: 'When a friend signs up and pays via your link, you earn 20% of their first order as credit.' + }, + inviteLink: { + zh: '我的邀请链接', + en: 'My invite link' + }, + invited: { + zh: '已邀请', + en: 'Invited' + }, + paidUsers: { + zh: '已付费', + en: 'Converted' + }, + earned: { + zh: '累计返利', + en: 'Credit earned' + }, + inviteRecord: { + zh: '邀请记录', + en: 'History' + }, + recEmpty: { + zh: '暂无邀请记录', + en: 'No referrals yet' + }, + recRegistered: { + zh: '已注册', + en: 'Registered' + }, + recPaid: { + zh: '已付费', + en: 'Paid' + }, + /* 2FA */ + twoFATitle: { + zh: '双重认证', + en: 'Two-factor auth' + }, + twoFAHint: { + zh: '你的账户已开启双重认证。请输入身份验证器 App 中的 6 位动态码。', + en: 'Two-factor auth is on for this account. Enter the 6-digit code from your authenticator app.' + }, + twoFAConfirm: { + zh: '验证并登录', + en: 'Verify & log in' + }, + twoFABack: { + zh: '返回上一步', + en: 'Back' + } +}; +function ucT(lang) { + return k => { + const v = UCSTRINGS[k]; + return v ? v[lang] || k : k; + }; +} +Object.assign(window, { + UCIcon, + UCMark, + UCSTRINGS, + ucT +}); +})(); } catch (e) { __ds_ns.__errors.push({ path: "ui_kits/usercenter/ucparts.jsx", error: String((e && e.message) || e) }); } + +// ui_kits/website/site.js +try { (() => { +/* site.js — 穿山甲 VPN 官网 · i18n + 交互 */ +(function () { + // ---------- i18n ---------- + const I18N = { + 'ann.txt': ['新用户注册即享 7 天免费试用', 'New users get a 7-day free trial'], + 'ann.cta': ['立即注册 →', 'Sign up →'], + 'su.ph': ['输入邮箱,免费注册', 'Enter email to sign up free'], + 'su.btn': ['免费注册', 'Sign up free'], + 'su.ok': ['验证码已发送!下载 App 并用同一邮箱继续完成注册。', 'Code sent! Download the app and continue with the same email.'], + 'su.hint': ['免费注册 · 无需付款 · 7 天免费试用', 'Free to join · no payment · 7-day free trial'], + 'su.dl': ['或直接下载 App', 'or just download the app'], + 'why.eyebrow': ['免费注册', 'Sign up'], + 'why.h': ['注册一次,处处可用', 'One account, everywhere'], + 'why.sub': ['一个邮箱就能开始。注册免费,不需要任何付款信息。', 'All you need is an email. Free to join — no payment details, ever.'], + 'why.1t': ['7 天免费试用', '7-day free trial'], + 'why.1d': ['新用户注册即享 7 天免费使用,无需任何付款信息。', 'New accounts get 7 days of free, unrestricted use — no payment details.'], + 'why.2t': ['多端同步', 'Multi-device sync'], + 'why.2d': ['手机、平板、电脑共用一个账户,套餐与设置随身走。', 'Phone, tablet and desktop share one account — your plan follows you.'], + 'why.3t': ['无需付款信息', 'No payment details'], + 'why.3d': ['注册只要邮箱。想升级时,再通过外部渠道兑换激活码。', 'Just an email to join. Upgrade later with an activation code.'], + 'why.s1': ['填写邮箱', 'Enter email'], + 'why.s2': ['输入验证码', 'Verify code'], + 'why.s3': ['开始畅连', 'Start connecting'], + 'why.cta': ['立即免费注册', 'Sign up free'], + 'cta.btn2': ['免费注册', 'Sign up free'], + 'nav.product': ['产品', 'Product'], + 'nav.pricing': ['定价', 'Pricing'], + 'nav.download': ['下载', 'Download'], + 'nav.docs': ['文档', 'Docs'], + 'nav.blog': ['Blog', 'Blog'], + 'nav.login': ['登录', 'Log in'], + 'nav.get': ['立即下载', 'Get the app'], + 'hero.eyebrow': ['极速 · 稳定 · 省心', 'Fast · Stable · Effortless'], + 'hero.h1': ['极速畅连,\n网络如丝顺滑', 'Faster, smoother,\neverywhere'], + 'hero.lede': ['轻盈、亲和、即开即用的跨平台网络加速应用。一键连接,智能选线,稳定不掉线。', 'A lightweight, friendly cross-platform network accelerator. One tap, smart routing, rock-solid.'], + 'hero.cta1': ['免费下载', 'Download free'], + 'hero.cta2': ['查看定价', 'See pricing'], + 'hero.t1': ['80+ 全球加速线路', '80+ global routes'], + 'hero.t2': ['严格无日志', 'Strict no-logs'], + 'hero.t3': ['端到端加密', 'End-to-end encrypted'], + 'orb.cap': ['已连接', 'CONNECTED'], + 'orb.down': ['下载', 'Down'], + 'orb.up': ['上传', 'Up'], + 'orb.node': ['香港 · 流媒体', 'Hong Kong'], + 'orb.nodesub': ['延迟 18ms · 已连接', '18ms · Connected'], + 'strip.txt': ['已支持', 'Available on'], + 'feat.eyebrow': ['产品', 'Product'], + 'feat.h': ['为日常而生的连接体验', 'Built for everyday connection'], + 'feat.sub': ['没有复杂配置,没有技术门槛。打开就用,把稳定与隐私留给我们。', 'No complex setup, no jargon. Just open and go — we handle speed and privacy.'], + 'feat.1t': ['一键连接', 'One-tap connect'], + 'feat.1d': ['一颗按钮,智能挑选最快线路。无需手动配置,也能秒连。', 'One button picks the fastest route. No manual setup, instant connect.'], + 'feat.2t': ['智能分流', 'Smart routing'], + 'feat.2d': ['按规则智能分流,该加速的加速,本地服务直连,互不打扰。', 'Smart rule-based routing — accelerate what needs it, keep local traffic direct.'], + 'feat.3t': ['80+ 全球加速线路', '80+ global routes'], + 'feat.3d': ['覆盖港日新美欧韩等地,视频与游戏优化线路随心选。', 'Routes across HK / JP / SG / US / EU and more, tuned for video & gaming.'], + 'feat.4t': ['严格无日志', 'Strict no-logs'], + 'feat.4d': ['我们不记录你的浏览数据。隐私是底线,不是卖点。', 'We never log your browsing. Privacy is the baseline, not a feature.'], + 'feat.5t': ['Kill Switch', 'Kill Switch'], + 'feat.5d': ['一旦连接中断,立即阻断网络,杜绝真实 IP 泄露。', 'If the tunnel drops, traffic is blocked instantly — no IP leaks.'], + 'feat.6t': ['多端同步', 'Multi-device'], + 'feat.6d': ['一个账户,手机、平板、电脑同时在线,最多 5 台设备。', 'One account across phone, tablet and desktop — up to 5 devices.'], + 'price.eyebrow': ['定价', 'Pricing'], + 'price.h': ['简单透明的价格', 'Simple, honest pricing'], + 'price.sub': ['随时升级或取消。所有套餐均含核心加密与无日志承诺。', 'Upgrade or cancel anytime. Every plan includes core encryption and our no-logs promise.'], + 'price.monthly': ['按月', 'Monthly'], + 'price.yearly': ['按年', 'Yearly'], + 'price.save': ['省 20%', 'Save 20%'], + 'price.permo': ['/月', '/mo'], + 'price.free': ['免费版', 'Free'], + 'price.freedesc': ['适合轻度使用与尝鲜', 'For light, casual use'], + 'price.pro': ['专业版', 'Pro'], + 'price.prodesc': ['个人用户的最佳选择', 'Best for individuals'], + 'price.team': ['团队版', 'Team'], + 'price.teamdesc': ['小团队集中管理', 'For small teams'], + 'price.popular': ['最受欢迎', 'Most popular'], + 'price.cta_free': ['免费下载', 'Download free'], + 'price.cta_pro': ['获取激活码', 'Get a code'], + 'price.cta_team': ['获取激活码', 'Get a code'], + 'pf.free1': ['每日 10 分钟时长', '10 min per day'], + 'pf.free2': ['仅 1 个基础节点', '1 basic node only'], + 'pf.free3': ['使用前观看广告', 'Watch an ad to start'], + 'pf.free4': ['核心加密 · 无日志', 'Core encryption · no-logs'], + 'pf.pro1': ['80+ 全球加速线路', '80+ global routes'], + 'pf.pro2': ['无限流量 · 极速', 'Unlimited · top speed'], + 'pf.pro3': ['5 台设备同时在线', '5 devices at once'], + 'pf.pro4': ['流媒体 & P2P 优化', 'Streaming & P2P routes'], + 'pf.pro5': ['Kill Switch · 智能分流', 'Kill Switch · smart routing'], + 'pf.team1': ['专业版全部功能', 'Everything in Pro'], + 'pf.team2': ['10 个成员席位', '10 seats'], + 'pf.team3': ['集中计费与管理', 'Central billing & admin'], + 'pf.team4': ['优先客服', 'Priority support'], + 'pay.title': ['为什么网页上没有支付按钮?', 'Why no checkout on this page?'], + 'pay.body': ['出于风控与隐私考虑,穿山甲不在网页或 App 内直接收款。你可以通过以下任一渠道获取激活码,在客户端内兑换即可开通。', 'For risk and privacy reasons, Pangolin never takes payment in the web or app. Get an activation code through any channel below, then redeem it in the client.'], + 'pay.store': ['自助发卡商店', 'Self-serve store'], + 'pay.usdt': ['USDT (TRC20) · 最隐私', 'USDT (TRC20) · most private'], + 'cmp.h': ['功能对比', 'Compare plans'], + 'cmp.feature': ['功能', 'Feature'], + 'cmp.locations': ['可用线路', 'Routes'], + 'cmp.data': ['流量', 'Data'], + 'cmp.time': ['每日时长', 'Daily time'], + 'cmp.time_free': ['10 分钟/天(试用期不限)', '10 min/day (trial: unlimited)'], + 'cmp.devices': ['设备数', 'Devices'], + 'cmp.speed': ['极速线路', 'Top-speed routes'], + 'cmp.stream': ['视频优化', 'Video routes'], + 'cmp.kill': ['Kill Switch', 'Kill Switch'], + 'cmp.support': ['客服', 'Support'], + 'cmp.unlimited': ['无限', 'Unlimited'], + 'cmp.daily': ['不限', 'Unlimited'], + 'cmp.basic': ['标准', 'Standard'], + 'cmp.priority': ['优先', 'Priority'], + 'dl.eyebrow': ['下载', 'Download'], + 'dl.h': ['全平台,随处可用', 'Every platform, everywhere'], + 'dl.sub': ['一个账户,所有设备同步。下载即用,无需配置。', 'One account syncs every device. Download and go.'], + 'dl.get': ['下载', 'Download'], + 'docs.eyebrow': ['文档', 'Docs'], + 'docs.h': ['需要帮助?都在这里', 'Need help? It’s all here'], + 'docs.sub': ['从快速上手到协议细节,清晰直接。', 'From quickstart to protocol details — clear and direct.'], + 'docs.1t': ['快速开始', 'Quickstart'], + 'docs.1d': ['三分钟完成注册、下载与首次连接。', 'Sign up, download and connect in three minutes.'], + 'docs.2t': ['常见问题', 'FAQ'], + 'docs.2d': ['连接、计费、设备与兑换码的常见疑问。', 'Connection, billing, devices and redeem codes.'], + 'docs.3t': ['协议与安全', 'Protocol & security'], + 'docs.3d': ['WireGuard、加密方式与无日志架构说明。', 'WireGuard, encryption and our no-logs architecture.'], + 'docs.4t': ['隐私政策', 'Privacy policy'], + 'docs.4d': ['我们收集什么、不收集什么,一目了然。', 'Exactly what we collect — and what we never do.'], + 'docs.read': ['阅读', 'Read'], + 'blog.eyebrow': ['Blog', 'Blog'], + 'blog.h': ['来自团队的最新动态', 'Latest from the team'], + 'blog.sub': ['产品更新、安全科普与节点公告。', 'Product updates, security explainers and node news.'], + 'blog.1tag': ['产品更新', 'Product'], + 'blog.1t': ['v2.4:更快的智能分流与全新统计页', 'v2.4: faster smart routing & a new stats tab'], + 'blog.1d': ['本次更新重写了分流引擎,连接建立速度提升约 40%。', 'We rewrote the routing engine — connections are ~40% faster.'], + 'blog.2tag': ['安全', 'Security'], + 'blog.2t': ['无日志到底意味着什么?', 'What “no-logs” really means'], + 'blog.2d': ['我们逐条拆解收集与不收集的数据,以及背后的架构。', 'A line-by-line look at what we keep — and what we never touch.'], + 'blog.3tag': ['公告', 'News'], + 'blog.3t': ['新增首尔与法兰克福高速节点', 'New high-speed nodes: Seoul & Frankfurt'], + 'blog.3d': ['两条新线路已上线,为东北亚与欧洲用户带来更低延迟。', 'Two new routes are live, cutting latency for NE Asia and Europe.'], + 'blog.readmore': ['阅读全文', 'Read more'], + 'cta.h': ['准备好体验极速畅连了吗?', 'Ready for a faster connection?'], + 'cta.p': ['免费下载,数分钟内开始你的第一次极速连接。', 'Download free and get connected in minutes.'], + 'cta.btn': ['免费下载', 'Download free'], + 'ft.tag': ['极简、轻量、亲和的跨平台网络加速应用。守护你的每一次连接。', 'A minimal, friendly cross-platform network accelerator. Protecting every connection.'], + 'ft.product': ['产品', 'Product'], + 'ft.resources': ['资源', 'Resources'], + 'ft.contact': ['联系与渠道', 'Contact'], + 'ft.features': ['功能', 'Features'], + 'ft.pricing': ['定价', 'Pricing'], + 'ft.download': ['下载', 'Download'], + 'ft.docs': ['文档', 'Docs'], + 'ft.blog': ['Blog', 'Blog'], + 'ft.faq': ['常见问题', 'FAQ'], + 'ft.privacy': ['隐私政策', 'Privacy'], + 'ft.status': ['服务状态', 'Status'], + 'ft.hours': ['服务时间 每日 9:00–24:00 (GMT+8)', 'Hours: Daily 9:00–24:00 (GMT+8)'], + 'ft.copy': ['© 2026 穿山甲 · Pangolin', '© 2026 Pangolin'], + 'ft.madenote': ['App 内不支持直接支付 · 资金流全走外部渠道', 'No in-app payment · all purchases via external channels'] + }; + let lang = localStorage.getItem('pg_site_lang') || 'zh'; + function applyLang() { + const i = lang === 'zh' ? 0 : 1; + document.documentElement.lang = lang === 'zh' ? 'zh' : 'en'; + document.querySelectorAll('[data-i18n]').forEach(el => { + const k = el.getAttribute('data-i18n'); + if (I18N[k]) el.textContent = I18N[k][i]; + }); + document.querySelectorAll('[data-i18n-ph]').forEach(el => { + const k = el.getAttribute('data-i18n-ph'); + if (I18N[k]) el.placeholder = I18N[k][i]; + }); + document.querySelectorAll('.langseg button').forEach(b => b.classList.toggle('on', b.dataset.lang === lang)); + } + window.__setLang = function (l) { + lang = l; + localStorage.setItem('pg_site_lang', l); + applyLang(); + }; + + // ---------- pricing monthly/yearly ---------- + const PRICES = { + free: ['¥0', '¥0'], + pro: ['¥25', '¥20'], + team: ['¥99', '¥79'] + }; + let cycle = 'monthly'; + window.__setCycle = function (c) { + cycle = c; + const i = c === 'monthly' ? 0 : 1; + document.querySelectorAll('[data-price]').forEach(el => { + const p = el.getAttribute('data-price'); + el.firstChild.textContent = PRICES[p][i]; + }); + document.querySelectorAll('.price-toggle button').forEach(b => b.classList.toggle('on', b.dataset.cycle === c)); + }; + + // ---------- signup (demo: real impl sends a verification code) ---------- + window.__signup = function (e) { + e.preventDefault(); + const em = document.getElementById('su-email'); + if (!/\S+@\S+\.\S+/.test(em.value)) { + em.focus(); + return false; + } + document.getElementById('su-ok').hidden = false; + return false; + }; + + // ---------- announcement bar ---------- + window.__closeAnn = function () { + const a = document.getElementById('ann'); + if (a) a.remove(); + }; + + // ---------- mobile menu ---------- + window.__toggleMenu = function () { + document.getElementById('mnav').classList.toggle('open'); + }; + + // ---------- scroll glass header ---------- + function onScroll() { + document.querySelector('.hdr').classList.toggle('scrolled', window.scrollY > 8); + } + window.addEventListener('scroll', onScroll, { + passive: true + }); + + // ---------- init ---------- + document.addEventListener('DOMContentLoaded', function () { + applyLang(); + onScroll(); + // close mobile menu when a link is tapped + document.querySelectorAll('#mnav a').forEach(a => a.addEventListener('click', () => document.getElementById('mnav').classList.remove('open'))); + if (window.lucide) lucide.createIcons(); + }); +})(); +})(); } catch (e) { __ds_ns.__errors.push({ path: "ui_kits/website/site.js", error: String((e && e.message) || e) }); } + +})(); diff --git a/design/_ds_manifest.json b/design/_ds_manifest.json new file mode 100644 index 0000000..bd32acc --- /dev/null +++ b/design/_ds_manifest.json @@ -0,0 +1 @@ +{"namespace":"VPNDesignSystem_4c9564","components":[],"startingPoints":[],"cards":[{"path":"preview/brand-app-icon.html","group":"Brand","viewport":"700x170","subtitle":"clay 渐变圆角方,多尺寸","name":"App 图标家族"},{"path":"preview/brand-logo.html","group":"Brand","viewport":"700x185","subtitle":"浅色主锁版 / 深色反白锁版","name":"Logo 锁版"},{"path":"preview/color-clay.html","group":"Colors","viewport":"700x150","subtitle":"穿山甲铜棕主色 50–900,500 为品牌主色","name":"Clay 主色阶"},{"path":"preview/color-dark-surfaces.html","group":"Colors","viewport":"700x185","subtitle":"暖 espresso 背景与表面层级","name":"暗色主题表面"},{"path":"preview/color-sand.html","group":"Colors","viewport":"700x150","subtitle":"暖中性 taupe 50–950,950 为暗色画布","name":"Sand 中性色阶"},{"path":"preview/color-semantic.html","group":"Colors","viewport":"700x175","subtitle":"success / warning / danger 及 subtle 变体","name":"语义状态色"},{"path":"preview/comp-badges.html","group":"Components","viewport":"700x150","subtitle":"连接态 / 套餐 / 节点属性胶囊","name":"状态徽章"},{"path":"preview/comp-buttons.html","group":"Components","viewport":"700x150","subtitle":"主/次/幽灵/危险/禁用/图标","name":"按钮"},{"path":"preview/comp-connect.html","group":"Components","viewport":"700x210","subtitle":"断开 / 连接中 / 已连接 三态","name":"核心连接按钮"},{"path":"preview/comp-inputs.html","group":"Components","viewport":"700x175","subtitle":"输入框(focus 光环)与开关","name":"表单控件"},{"path":"preview/comp-plan-card.html","group":"Components","viewport":"700x300","subtitle":"免费版与 PRO 渐变卡","name":"套餐卡"},{"path":"preview/comp-server-row.html","group":"Components","viewport":"700x300","subtitle":"国家码 · 延迟 · 信号 · 选中态","name":"服务器列表行"},{"path":"preview/radii.html","group":"Spacing","viewport":"700x150","subtitle":"偏大圆角 = 亲和友好","name":"圆角"},{"path":"preview/shadows.html","group":"Spacing","viewport":"700x180","subtitle":"暖色调柔和投影 sm–xl","name":"阴影系统"},{"path":"preview/spacing.html","group":"Spacing","viewport":"700x290","subtitle":"4px 基准间距标尺","name":"间距网格"},{"path":"preview/type-body.html","group":"Type","viewport":"700x190","subtitle":"界面与正文,人文几何","name":"正文字体 (Manrope)"},{"path":"preview/type-cjk.html","group":"Type","viewport":"700x185","subtitle":"CJK 专用伴随字体","name":"中文字体 (Noto Sans SC)"},{"path":"preview/type-display.html","group":"Type","viewport":"700x175","subtitle":"标题大字,几何友好","name":"Display 字体 (Sora)"},{"path":"preview/type-mono.html","group":"Type","viewport":"700x190","subtitle":"IP / 速率 / 密钥读数,tabular nums","name":"等宽数据字体 (JetBrains Mono)"},{"path":"preview/type-scale.html","group":"Type","viewport":"700x330","subtitle":"48 → 12 完整字阶","name":"字号梯度"},{"path":"ui_kits/desktop/index.html","group":"UI Kit — Desktop","viewport":"980x640","subtitle":"macOS 窗口 · 连接/节点/统计/设置 · 中英切换","name":"UI Kit · 桌面客户端"},{"path":"ui_kits/mobile/index.html","group":"UI Kit — Mobile","viewport":"390x760","subtitle":"iOS 连接界面 · 节点 · 账户 · 中英切换 · 双主题","name":"UI Kit · 移动 App"},{"path":"ui_kits/tablet/index.html","group":"UI Kit — Tablet","viewport":"1260x900","subtitle":"iPad 分栏布局 · 侧栏导航 · 连接/节点/统计/账户 · 双语双主题","name":"UI Kit · iPad"},{"path":"ui_kits/usercenter/index.html","group":"UI Kit — Web 用户中心","viewport":"1000x720","subtitle":"概览 / 订阅导入 / 兑换 / 邀请返利 · 中英切换","name":"UI Kit · Web 用户中心"},{"path":"ui_kits/usercenter/mobile-preview.html","group":"UI Kit — Web 用户中心","viewport":"390x780","subtitle":"390px 手机布局 · 横滑导航 · 单列卡片","name":"UI Kit · 用户中心 · 移动端"},{"path":"ui_kits/website/index.html","group":"UI Kit — Website","viewport":"1140x720","subtitle":"产品/定价/下载/文档/Blog · 暖大地色 · 中英切换","name":"UI Kit · 官网宣传页"},{"path":"ui_kits/website/mobile-preview.html","group":"UI Kit — Website","viewport":"390x800","subtitle":"390px 手机布局 · 汉堡菜单 · 注册引导","name":"UI Kit · 官网 · 移动端"}],"templates":[],"globalCssPaths":["colors_and_type.css","ui_kits/website/website.css"],"tokens":[{"name":"--clay-50","value":"#FAF3ED","kind":"color","definedIn":"colors_and_type.css"},{"name":"--clay-100","value":"#F2E2D4","kind":"color","definedIn":"colors_and_type.css"},{"name":"--clay-200","value":"#E6C7AC","kind":"color","definedIn":"colors_and_type.css"},{"name":"--clay-300","value":"#D9A982","kind":"color","definedIn":"colors_and_type.css"},{"name":"--clay-400","value":"#CC8B5C","kind":"color","definedIn":"colors_and_type.css"},{"name":"--clay-500","value":"#B96A3D","kind":"color","definedIn":"colors_and_type.css"},{"name":"--clay-600","value":"#9E5630","kind":"color","definedIn":"colors_and_type.css"},{"name":"--clay-700","value":"#7E4426","kind":"color","definedIn":"colors_and_type.css"},{"name":"--clay-800","value":"#5E331D","kind":"color","definedIn":"colors_and_type.css"},{"name":"--clay-900","value":"#3D2213","kind":"color","definedIn":"colors_and_type.css"},{"name":"--sand-50","value":"#FAF8F4","kind":"color","definedIn":"colors_and_type.css"},{"name":"--sand-100","value":"#F2EEE7","kind":"color","definedIn":"colors_and_type.css"},{"name":"--sand-200","value":"#E6DFD3","kind":"color","definedIn":"colors_and_type.css"},{"name":"--sand-300","value":"#D2C8B8","kind":"color","definedIn":"colors_and_type.css"},{"name":"--sand-400","value":"#B0A491","kind":"color","definedIn":"colors_and_type.css"},{"name":"--sand-500","value":"#8C8270","kind":"color","definedIn":"colors_and_type.css"},{"name":"--sand-600","value":"#6B6253","kind":"color","definedIn":"colors_and_type.css"},{"name":"--sand-700","value":"#4E4940","kind":"color","definedIn":"colors_and_type.css"},{"name":"--sand-800","value":"#2E2A24","kind":"color","definedIn":"colors_and_type.css"},{"name":"--sand-900","value":"#1F1C18","kind":"color","definedIn":"colors_and_type.css"},{"name":"--sand-950","value":"#14110E","kind":"color","definedIn":"colors_and_type.css"},{"name":"--green-400","value":"#7FB07A","kind":"color","definedIn":"colors_and_type.css"},{"name":"--green-500","value":"#5B8C5A","kind":"color","definedIn":"colors_and_type.css"},{"name":"--green-600","value":"#467046","kind":"color","definedIn":"colors_and_type.css"},{"name":"--amber-400","value":"#E2B05A","kind":"color","definedIn":"colors_and_type.css"},{"name":"--amber-500","value":"#D69A3C","kind":"color","definedIn":"colors_and_type.css"},{"name":"--amber-600","value":"#B47E29","kind":"color","definedIn":"colors_and_type.css"},{"name":"--red-400","value":"#D4715A","kind":"color","definedIn":"colors_and_type.css"},{"name":"--red-500","value":"#C0533B","kind":"color","definedIn":"colors_and_type.css"},{"name":"--red-600","value":"#9E4230","kind":"color","definedIn":"colors_and_type.css"},{"name":"--bg","value":"var(--sand-50)","kind":"color","definedIn":"colors_and_type.css"},{"name":"--bg-subtle","value":"var(--sand-100)","kind":"color","definedIn":"colors_and_type.css"},{"name":"--surface","value":"#FFFFFF","kind":"color","definedIn":"colors_and_type.css"},{"name":"--surface-2","value":"var(--sand-50)","kind":"color","definedIn":"colors_and_type.css"},{"name":"--overlay","value":"rgba(31, 28, 24, 0.45)","kind":"color","definedIn":"colors_and_type.css"},{"name":"--fg1","value":"var(--sand-900)","kind":"color","definedIn":"colors_and_type.css"},{"name":"--fg2","value":"var(--sand-600)","kind":"color","definedIn":"colors_and_type.css"},{"name":"--fg3","value":"var(--sand-500)","kind":"color","definedIn":"colors_and_type.css"},{"name":"--fg-on-accent","value":"#FFFFFF","kind":"color","definedIn":"colors_and_type.css"},{"name":"--accent","value":"var(--clay-500)","kind":"color","definedIn":"colors_and_type.css"},{"name":"--accent-hover","value":"var(--clay-600)","kind":"color","definedIn":"colors_and_type.css"},{"name":"--accent-press","value":"var(--clay-700)","kind":"color","definedIn":"colors_and_type.css"},{"name":"--accent-subtle","value":"var(--clay-50)","kind":"color","definedIn":"colors_and_type.css"},{"name":"--accent-border","value":"var(--clay-200)","kind":"color","definedIn":"colors_and_type.css"},{"name":"--border","value":"var(--sand-200)","kind":"color","definedIn":"colors_and_type.css"},{"name":"--border-strong","value":"var(--sand-300)","kind":"color","definedIn":"colors_and_type.css"},{"name":"--ring","value":"rgba(185, 106, 61, 0.35)","kind":"color","definedIn":"colors_and_type.css"},{"name":"--success","value":"var(--green-500)","kind":"color","definedIn":"colors_and_type.css"},{"name":"--success-subtle","value":"#E9F0E6","kind":"color","definedIn":"colors_and_type.css"},{"name":"--warning","value":"var(--amber-500)","kind":"color","definedIn":"colors_and_type.css"},{"name":"--warning-subtle","value":"#F8EED6","kind":"color","definedIn":"colors_and_type.css"},{"name":"--danger","value":"var(--red-500)","kind":"color","definedIn":"colors_and_type.css"},{"name":"--danger-subtle","value":"#F6E1DA","kind":"color","definedIn":"colors_and_type.css"},{"name":"--font-display","value":"'Sora', 'Noto Sans SC', system-ui, sans-serif","kind":"font","definedIn":"colors_and_type.css"},{"name":"--font-sans","value":"'Manrope', 'Noto Sans SC', system-ui, sans-serif","kind":"font","definedIn":"colors_and_type.css"},{"name":"--font-cjk","value":"'Noto Sans SC', 'Manrope', system-ui, sans-serif","kind":"font","definedIn":"colors_and_type.css"},{"name":"--font-mono","value":"'JetBrains Mono', ui-monospace, 'SFMono-Regular', monospace","kind":"font","definedIn":"colors_and_type.css"},{"name":"--text-display-xl","value":"3rem","kind":"font","definedIn":"colors_and_type.css"},{"name":"--text-display","value":"2.25rem","kind":"font","definedIn":"colors_and_type.css"},{"name":"--text-h1","value":"1.875rem","kind":"font","definedIn":"colors_and_type.css"},{"name":"--text-h2","value":"1.5rem","kind":"font","definedIn":"colors_and_type.css"},{"name":"--text-h3","value":"1.25rem","kind":"font","definedIn":"colors_and_type.css"},{"name":"--text-body-lg","value":"1.125rem","kind":"font","definedIn":"colors_and_type.css"},{"name":"--text-body","value":"1rem","kind":"font","definedIn":"colors_and_type.css"},{"name":"--text-sm","value":"0.875rem","kind":"font","definedIn":"colors_and_type.css"},{"name":"--text-caption","value":"0.75rem","kind":"font","definedIn":"colors_and_type.css"},{"name":"--leading-tight","value":"1.15","kind":"font","definedIn":"colors_and_type.css"},{"name":"--leading-snug","value":"1.3","kind":"font","definedIn":"colors_and_type.css"},{"name":"--leading-normal","value":"1.5","kind":"font","definedIn":"colors_and_type.css"},{"name":"--leading-relaxed","value":"1.65","kind":"font","definedIn":"colors_and_type.css"},{"name":"--tracking-tight","value":"-0.02em","kind":"font","definedIn":"colors_and_type.css"},{"name":"--tracking-snug","value":"-0.01em","kind":"font","definedIn":"colors_and_type.css"},{"name":"--tracking-wide","value":"0.04em","kind":"font","definedIn":"colors_and_type.css"},{"name":"--tracking-caps","value":"0.08em","kind":"font","definedIn":"colors_and_type.css"},{"name":"--space-0","value":"0","kind":"spacing","definedIn":"colors_and_type.css"},{"name":"--space-1","value":"0.25rem","kind":"spacing","definedIn":"colors_and_type.css"},{"name":"--space-2","value":"0.5rem","kind":"spacing","definedIn":"colors_and_type.css"},{"name":"--space-3","value":"0.75rem","kind":"spacing","definedIn":"colors_and_type.css"},{"name":"--space-4","value":"1rem","kind":"spacing","definedIn":"colors_and_type.css"},{"name":"--space-5","value":"1.25rem","kind":"spacing","definedIn":"colors_and_type.css"},{"name":"--space-6","value":"1.5rem","kind":"spacing","definedIn":"colors_and_type.css"},{"name":"--space-8","value":"2rem","kind":"spacing","definedIn":"colors_and_type.css"},{"name":"--space-10","value":"2.5rem","kind":"spacing","definedIn":"colors_and_type.css"},{"name":"--space-12","value":"3rem","kind":"spacing","definedIn":"colors_and_type.css"},{"name":"--space-16","value":"4rem","kind":"spacing","definedIn":"colors_and_type.css"},{"name":"--radius-sm","value":"6px","kind":"radius","definedIn":"colors_and_type.css"},{"name":"--radius-md","value":"10px","kind":"radius","definedIn":"colors_and_type.css"},{"name":"--radius-lg","value":"14px","kind":"radius","definedIn":"colors_and_type.css"},{"name":"--radius-xl","value":"20px","kind":"radius","definedIn":"colors_and_type.css"},{"name":"--radius-2xl","value":"28px","kind":"radius","definedIn":"colors_and_type.css"},{"name":"--radius-full","value":"999px","kind":"radius","definedIn":"colors_and_type.css"},{"name":"--shadow-sm","value":"0 1px 2px rgba(45, 30, 20, 0.06)","kind":"shadow","definedIn":"colors_and_type.css"},{"name":"--shadow-md","value":"0 4px 14px rgba(45, 30, 20, 0.08)","kind":"shadow","definedIn":"colors_and_type.css"},{"name":"--shadow-lg","value":"0 12px 32px rgba(45, 30, 20, 0.12)","kind":"shadow","definedIn":"colors_and_type.css"},{"name":"--shadow-xl","value":"0 24px 60px rgba(45, 30, 20, 0.16)","kind":"shadow","definedIn":"colors_and_type.css"},{"name":"--shadow-focus","value":"0 0 0 4px var(--ring)","kind":"shadow","definedIn":"colors_and_type.css"},{"name":"--ease-out","value":"cubic-bezier(0.22, 1, 0.36, 1)","kind":"other","definedIn":"colors_and_type.css","annotation":"other"},{"name":"--ease-in-out","value":"cubic-bezier(0.65, 0, 0.35, 1)","kind":"other","definedIn":"colors_and_type.css","annotation":"other"},{"name":"--dur-fast","value":"140ms","kind":"other","definedIn":"colors_and_type.css","annotation":"other"},{"name":"--dur-base","value":"220ms","kind":"other","definedIn":"colors_and_type.css","annotation":"other"},{"name":"--dur-slow","value":"360ms","kind":"other","definedIn":"colors_and_type.css","annotation":"other"},{"name":"--bg","value":"var(--sand-950)","kind":"color","definedIn":"colors_and_type.css","scope":"[data-theme=\"dark\"]"},{"name":"--bg-subtle","value":"var(--sand-900)","kind":"color","definedIn":"colors_and_type.css","scope":"[data-theme=\"dark\"]"},{"name":"--surface","value":"#221E19","kind":"color","definedIn":"colors_and_type.css","scope":"[data-theme=\"dark\"]"},{"name":"--surface-2","value":"#2A251F","kind":"color","definedIn":"colors_and_type.css","scope":"[data-theme=\"dark\"]"},{"name":"--overlay","value":"rgba(0, 0, 0, 0.6)","kind":"color","definedIn":"colors_and_type.css","scope":"[data-theme=\"dark\"]"},{"name":"--fg1","value":"#F4EFE8","kind":"color","definedIn":"colors_and_type.css","scope":"[data-theme=\"dark\"]"},{"name":"--fg2","value":"#B6AC9C","kind":"color","definedIn":"colors_and_type.css","scope":"[data-theme=\"dark\"]"},{"name":"--fg3","value":"#897F6F","kind":"color","definedIn":"colors_and_type.css","scope":"[data-theme=\"dark\"]"},{"name":"--fg-on-accent","value":"#1F1C18","kind":"color","definedIn":"colors_and_type.css","scope":"[data-theme=\"dark\"]"},{"name":"--accent","value":"var(--clay-400)","kind":"color","definedIn":"colors_and_type.css","scope":"[data-theme=\"dark\"]"},{"name":"--accent-hover","value":"var(--clay-300)","kind":"color","definedIn":"colors_and_type.css","scope":"[data-theme=\"dark\"]"},{"name":"--accent-press","value":"var(--clay-500)","kind":"color","definedIn":"colors_and_type.css","scope":"[data-theme=\"dark\"]"},{"name":"--accent-subtle","value":"rgba(204, 139, 92, 0.14)","kind":"color","definedIn":"colors_and_type.css","scope":"[data-theme=\"dark\"]"},{"name":"--accent-border","value":"rgba(204, 139, 92, 0.30)","kind":"color","definedIn":"colors_and_type.css","scope":"[data-theme=\"dark\"]"},{"name":"--border","value":"rgba(242, 238, 231, 0.10)","kind":"color","definedIn":"colors_and_type.css","scope":"[data-theme=\"dark\"]"},{"name":"--border-strong","value":"rgba(242, 238, 231, 0.18)","kind":"color","definedIn":"colors_and_type.css","scope":"[data-theme=\"dark\"]"},{"name":"--ring","value":"rgba(204, 139, 92, 0.45)","kind":"color","definedIn":"colors_and_type.css","scope":"[data-theme=\"dark\"]"},{"name":"--success","value":"var(--green-400)","kind":"color","definedIn":"colors_and_type.css","scope":"[data-theme=\"dark\"]"},{"name":"--success-subtle","value":"rgba(127, 176, 122, 0.16)","kind":"color","definedIn":"colors_and_type.css","scope":"[data-theme=\"dark\"]"},{"name":"--warning","value":"var(--amber-400)","kind":"color","definedIn":"colors_and_type.css","scope":"[data-theme=\"dark\"]"},{"name":"--warning-subtle","value":"rgba(226, 176, 90, 0.16)","kind":"color","definedIn":"colors_and_type.css","scope":"[data-theme=\"dark\"]"},{"name":"--danger","value":"var(--red-400)","kind":"color","definedIn":"colors_and_type.css","scope":"[data-theme=\"dark\"]"},{"name":"--danger-subtle","value":"rgba(212, 113, 90, 0.16)","kind":"color","definedIn":"colors_and_type.css","scope":"[data-theme=\"dark\"]"},{"name":"--shadow-sm","value":"0 1px 2px rgba(0, 0, 0, 0.4)","kind":"shadow","definedIn":"colors_and_type.css","scope":"[data-theme=\"dark\"]"},{"name":"--shadow-md","value":"0 4px 14px rgba(0, 0, 0, 0.45)","kind":"shadow","definedIn":"colors_and_type.css","scope":"[data-theme=\"dark\"]"},{"name":"--shadow-lg","value":"0 12px 32px rgba(0, 0, 0, 0.5)","kind":"shadow","definedIn":"colors_and_type.css","scope":"[data-theme=\"dark\"]"},{"name":"--shadow-xl","value":"0 24px 60px rgba(0, 0, 0, 0.55)","kind":"shadow","definedIn":"colors_and_type.css","scope":"[data-theme=\"dark\"]"}],"themes":[{"selector":"[data-theme=\"dark\"]","label":"Dark"}],"fonts":[],"brandFonts":[{"family":"Sora","status":"ok","tokens":["--font-display"],"path":"colors_and_type.css"},{"family":"Manrope","status":"ok","tokens":["--font-sans"],"path":"colors_and_type.css"},{"family":"Noto Sans SC","status":"ok","tokens":["--font-cjk"],"path":"colors_and_type.css"},{"family":"JetBrains Mono","status":"ok","tokens":["--font-mono"],"path":"colors_and_type.css"}],"source":"spa"} \ No newline at end of file diff --git a/design/assets/app-icon.svg b/design/assets/app-icon.svg new file mode 100644 index 0000000..c7eae9a --- /dev/null +++ b/design/assets/app-icon.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/design/assets/logo-mark-mono.svg b/design/assets/logo-mark-mono.svg new file mode 100644 index 0000000..9146fd1 --- /dev/null +++ b/design/assets/logo-mark-mono.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/design/assets/logo-mark-white.svg b/design/assets/logo-mark-white.svg new file mode 100644 index 0000000..0584f5d --- /dev/null +++ b/design/assets/logo-mark-white.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/design/assets/logo-mark.svg b/design/assets/logo-mark.svg new file mode 100644 index 0000000..b3e364b --- /dev/null +++ b/design/assets/logo-mark.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/design/assets/logo-wordmark.svg b/design/assets/logo-wordmark.svg new file mode 100644 index 0000000..8603db5 --- /dev/null +++ b/design/assets/logo-wordmark.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + 穿山甲 + PANGOLIN + \ No newline at end of file diff --git a/design/colors_and_type.css b/design/colors_and_type.css new file mode 100644 index 0000000..3b1b48b --- /dev/null +++ b/design/colors_and_type.css @@ -0,0 +1,218 @@ +/* ============================================================= + 穿山甲 VPN · Pangolin VPN — Design Tokens + colors_and_type.css + Single source of truth: color ramps, semantic colors (light + dark), + typography, spacing, radii, shadows, motion. + ============================================================= */ + +/* ---- Webfonts (open-source; documented as the brand's chosen faces) ---- + Sora — display / headings (geometric, friendly) + Manrope — body / UI (humanist geometric) + Noto Sans SC — Chinese (CJK companion) + JetBrains Mono — data readouts (IP, speed, keys) */ +@import url('https://fonts.googleapis.com/css2?family=Sora:wght@500;600;700&family=Manrope:wght@400;500;600;700&family=Noto+Sans+SC:wght@400;500;700&family=JetBrains+Mono:wght@400;500&display=swap'); + +:root { + /* =========================================================== + 1. PRIMITIVE COLOR RAMPS + =========================================================== */ + + /* Clay / Copper — the pangolin-armor primary (warm earth) */ + --clay-50: #FAF3ED; + --clay-100: #F2E2D4; + --clay-200: #E6C7AC; + --clay-300: #D9A982; + --clay-400: #CC8B5C; + --clay-500: #B96A3D; /* ← brand primary */ + --clay-600: #9E5630; + --clay-700: #7E4426; + --clay-800: #5E331D; + --clay-900: #3D2213; + + /* Sand / Taupe — warm neutral ramp */ + --sand-50: #FAF8F4; + --sand-100: #F2EEE7; + --sand-200: #E6DFD3; + --sand-300: #D2C8B8; + --sand-400: #B0A491; + --sand-500: #8C8270; + --sand-600: #6B6253; + --sand-700: #4E4940; + --sand-800: #2E2A24; + --sand-900: #1F1C18; + --sand-950: #14110E; /* warm espresso near-black */ + + /* Semantic hues (earth-tuned) */ + --green-400: #7FB07A; + --green-500: #5B8C5A; /* connected / secure */ + --green-600: #467046; + --amber-400: #E2B05A; + --amber-500: #D69A3C; /* connecting / warning */ + --amber-600: #B47E29; + --red-400: #D4715A; + --red-500: #C0533B; /* error / disconnect */ + --red-600: #9E4230; + + /* =========================================================== + 2. SEMANTIC TOKENS — LIGHT THEME (default) + =========================================================== */ + + /* Backgrounds & surfaces */ + --bg: var(--sand-50); /* app canvas */ + --bg-subtle: var(--sand-100); /* striped / inset regions */ + --surface: #FFFFFF; /* cards, sheets */ + --surface-2: var(--sand-50); /* nested surface */ + --overlay: rgba(31, 28, 24, 0.45); + + /* Foreground / text */ + --fg1: var(--sand-900); /* primary text */ + --fg2: var(--sand-600); /* secondary text */ + --fg3: var(--sand-500); /* tertiary / captions */ + --fg-on-accent: #FFFFFF; /* text on clay fills */ + + /* Brand / accent */ + --accent: var(--clay-500); + --accent-hover: var(--clay-600); + --accent-press: var(--clay-700); + --accent-subtle: var(--clay-50); + --accent-border: var(--clay-200); + + /* Borders & lines */ + --border: var(--sand-200); + --border-strong: var(--sand-300); + --ring: rgba(185, 106, 61, 0.35); /* focus ring (clay) */ + + /* Status */ + --success: var(--green-500); + --success-subtle: #E9F0E6; + --warning: var(--amber-500); + --warning-subtle: #F8EED6; + --danger: var(--red-500); + --danger-subtle: #F6E1DA; + + /* =========================================================== + 3. TYPOGRAPHY + =========================================================== */ + --font-display: 'Sora', 'Noto Sans SC', system-ui, sans-serif; + --font-sans: 'Manrope', 'Noto Sans SC', system-ui, sans-serif; + --font-cjk: 'Noto Sans SC', 'Manrope', system-ui, sans-serif; + --font-mono: 'JetBrains Mono', ui-monospace, 'SFMono-Regular', monospace; + + /* Type scale (root 16px) */ + --text-display-xl: 3rem; /* 48 */ + --text-display: 2.25rem; /* 36 */ + --text-h1: 1.875rem; /* 30 */ + --text-h2: 1.5rem; /* 24 */ + --text-h3: 1.25rem; /* 20 */ + --text-body-lg: 1.125rem; /* 18 */ + --text-body: 1rem; /* 16 */ + --text-sm: 0.875rem; /* 14 */ + --text-caption: 0.75rem; /* 12 */ + + --leading-tight: 1.15; + --leading-snug: 1.3; + --leading-normal:1.5; + --leading-relaxed:1.65; + + --tracking-tight: -0.02em; + --tracking-snug: -0.01em; + --tracking-wide: 0.04em; + --tracking-caps: 0.08em; + + /* =========================================================== + 4. SPACING (4px base) + =========================================================== */ + --space-0: 0; + --space-1: 0.25rem; /* 4 */ + --space-2: 0.5rem; /* 8 */ + --space-3: 0.75rem; /* 12 */ + --space-4: 1rem; /* 16 */ + --space-5: 1.25rem; /* 20 */ + --space-6: 1.5rem; /* 24 */ + --space-8: 2rem; /* 32 */ + --space-10: 2.5rem; /* 40 */ + --space-12: 3rem; /* 48 */ + --space-16: 4rem; /* 64 */ + + /* =========================================================== + 5. RADII (generous = friendly) + =========================================================== */ + --radius-sm: 6px; + --radius-md: 10px; + --radius-lg: 14px; + --radius-xl: 20px; + --radius-2xl: 28px; + --radius-full: 999px; + + /* =========================================================== + 6. SHADOWS (warm-tinted, soft) + =========================================================== */ + --shadow-sm: 0 1px 2px rgba(45, 30, 20, 0.06); + --shadow-md: 0 4px 14px rgba(45, 30, 20, 0.08); + --shadow-lg: 0 12px 32px rgba(45, 30, 20, 0.12); + --shadow-xl: 0 24px 60px rgba(45, 30, 20, 0.16); + --shadow-focus: 0 0 0 4px var(--ring); + + /* =========================================================== + 7. MOTION + =========================================================== */ + --ease-out: cubic-bezier(0.22, 1, 0.36, 1); /* @kind other */ + --ease-in-out: cubic-bezier(0.65, 0, 0.35, 1); /* @kind other */ + --dur-fast: 140ms; /* @kind other */ + --dur-base: 220ms; /* @kind other */ + --dur-slow: 360ms; /* @kind other */ +} + +/* =========================================================== + DARK THEME — warm espresso + Apply via [data-theme="dark"] on or any container. + =========================================================== */ +[data-theme="dark"] { + --bg: var(--sand-950); + --bg-subtle: var(--sand-900); + --surface: #221E19; + --surface-2: #2A251F; + --overlay: rgba(0, 0, 0, 0.6); + + --fg1: #F4EFE8; + --fg2: #B6AC9C; + --fg3: #897F6F; + --fg-on-accent: #1F1C18; + + --accent: var(--clay-400); + --accent-hover: var(--clay-300); + --accent-press: var(--clay-500); + --accent-subtle: rgba(204, 139, 92, 0.14); + --accent-border: rgba(204, 139, 92, 0.30); + + --border: rgba(242, 238, 231, 0.10); + --border-strong: rgba(242, 238, 231, 0.18); + --ring: rgba(204, 139, 92, 0.45); + + --success: var(--green-400); + --success-subtle: rgba(127, 176, 122, 0.16); + --warning: var(--amber-400); + --warning-subtle: rgba(226, 176, 90, 0.16); + --danger: var(--red-400); + --danger-subtle: rgba(212, 113, 90, 0.16); + + --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.4); + --shadow-md: 0 4px 14px rgba(0, 0, 0, 0.45); + --shadow-lg: 0 12px 32px rgba(0, 0, 0, 0.5); + --shadow-xl: 0 24px 60px rgba(0, 0, 0, 0.55); +} + +/* =========================================================== + SEMANTIC TYPE CLASSES (use directly in markup) + =========================================================== */ +.t-display-xl { font-family: var(--font-display); font-size: var(--text-display-xl); font-weight: 700; line-height: var(--leading-tight); letter-spacing: var(--tracking-tight); } +.t-display { font-family: var(--font-display); font-size: var(--text-display); font-weight: 700; line-height: var(--leading-tight); letter-spacing: var(--tracking-tight); } +.t-h1 { font-family: var(--font-display); font-size: var(--text-h1); font-weight: 600; line-height: var(--leading-snug); letter-spacing: var(--tracking-snug); } +.t-h2 { font-family: var(--font-display); font-size: var(--text-h2); font-weight: 600; line-height: var(--leading-snug); letter-spacing: var(--tracking-snug); } +.t-h3 { font-family: var(--font-sans); font-size: var(--text-h3); font-weight: 600; line-height: var(--leading-snug); } +.t-body-lg { font-family: var(--font-sans); font-size: var(--text-body-lg); font-weight: 400; line-height: var(--leading-relaxed); } +.t-body { font-family: var(--font-sans); font-size: var(--text-body); font-weight: 400; line-height: var(--leading-normal); } +.t-sm { font-family: var(--font-sans); font-size: var(--text-sm); font-weight: 400; line-height: var(--leading-normal); } +.t-caption { font-family: var(--font-sans); font-size: var(--text-caption); font-weight: 500; line-height: var(--leading-normal); } +.t-overline { font-family: var(--font-sans); font-size: var(--text-caption); font-weight: 600; text-transform: uppercase; letter-spacing: var(--tracking-caps); } +.t-mono { font-family: var(--font-mono); font-size: var(--text-sm); font-weight: 400; font-feature-settings: 'tnum' 1; } diff --git a/design/flutter/README.md b/design/flutter/README.md new file mode 100644 index 0000000..7b3feb2 --- /dev/null +++ b/design/flutter/README.md @@ -0,0 +1,135 @@ +# 穿山甲 VPN — Flutter 设计令牌 + +生产前端使用 **Flutter**。本目录提供与 `../colors_and_type.css` **一一对应**的 Dart 令牌,Flutter 可直接消费。`ui_kits/` 下的 React/HTML 是**视觉规范参考**(像素级还原 + 交互),Flutter 实现以它们为蓝本、以本文件为令牌来源。 + +## 快速开始 +1. 新建 Flutter 工程,把本目录的 `pubspec.yaml` 覆盖到根目录(按需改 `name`)。 +2. 拷贝:`pangolin_theme.dart` → `lib/`;`widgets/` 整个目录 → `lib/widgets/`;`main.dart` → `lib/`;`assets/*.svg` → 工程 `assets/`。 +3. 自备 4 款字体的 ttf 放 `fonts/`(或改用 `google_fonts` 包),族名须与 `pubspec.yaml` 一致。 +4. `flutter pub get` → `flutter run`。`main.dart` 已串好「登录 → 引导 → 主框架」可直接跑。 + +## 文件 +- `pubspec.yaml` — **工程清单模板**:已配好 `flutter_svg` / `lucide_icons` 依赖、4 个 SVG 资产、4 款字体声明(族名对齐 `PangolinFonts`)。拷到工程根目录,补字体 ttf,`flutter pub get` 即可。 +- `main.dart` — **入口示例**:`runApp` + 明/暗主题 + 「登录 → 引导 → 主框架」`RootFlow` 串接,可直接跑通。 +- `pangolin_theme.dart` — 全部令牌 + 明/暗 `ThemeData`: + - `PangolinColors` — clay / sand / 语义色阶(const `Color`) + - `PangolinScheme`(`ThemeExtension`)— 语义令牌(bg/surface/fg/accent/border/success…),含 `.light` / `.dark` + - `PangolinSpacing`(4px 基准)、`PangolinRadius`、`PangolinMotion`(时长 + 缓动曲线) + - `PangolinFonts`、`PangolinText`(字号阶)、`PangolinShadow`(暖色软阴影) + - `PangolinTheme.light` / `PangolinTheme.dark` — 开箱即用 +- `widgets/` — **核心组件 Dart 雏形**(纯样式,业务逻辑自接),全部读 `context.pangolin` 令牌: + - `pangolin_icons.dart` — **Lucide 图标映射表**:`PangolinIcons.power` 等直用常量 + `PangolinIcons.byName('shield-check')` 查表(从 React `data-lucide` 名移植)。需 `lucide_icons` 包。 + - `pangolin_button.dart` — `PangolinButton`(primary / secondary / ghost / danger,胶囊形) + - `country_code.dart` — `CountryCode`(2 字母国家码块)、`SignalBars`(延迟信号条) + - `status_pill.dart` — `StatusPill`(连接态 / PRO 等色点胶囊) + - `connect_button.dart` — `ConnectButton`(核心连接键三态:off 轨道环 / connecting 旋转弧 / on 满环 + 计时) + - `server_tile.dart` — `ServerTile` + `ServerInfo`(服务器列表行) + - `plan_card.dart` — `PlanCard`(套餐卡,专业版渐变高亮 + 「最受欢迎」浮标) + - `auth_screen.dart` — `AuthScreen`(登录 / 邮箱验证码注册,分屏式,带 `zh` 简单 i18n) + - `onboarding_screen.dart` — `OnboardingScreen`(首次引导 `PageView`,3 屏可滑动 + 进度点 + 跳过) + - `home_shell.dart` — `HomeShell`(主框架:底部 4 Tab 连接/节点/统计/账户 + 连接状态机 + 计时;各页骨架) + - `account_screens.dart` — 账户子页:`PlansScreen`(套餐选择)、`DevicesScreen`(设备管理 + 移除)、`RedeemScreen`(兑换码 + 购买渠道)、`ContactScreen`(联系渠道 + 服务时间);均为带返回栏的独立页,`HomeShell` 账户页已 `push` 接入 + - `pangolin_logo.dart` — `PangolinMark` / `PangolinBrandLockup` / `PangolinWordmark` / `PangolinAppIcon`(`flutter_svg` 加载 `assets/` 矢量 logo,深色自动反白) + - `pangolin_widgets.dart` — 统一 `export` + > 这些是**视觉雏形**:对应 React UI Kit 的同名组件,Flutter 工程师可直接拷贝/微调,接上真实状态与导航。图标已走 `PangolinIcons`(Lucide),与设计一致。 + +## 接入 +```dart +MaterialApp( + theme: PangolinTheme.light, + darkTheme: PangolinTheme.dark, + themeMode: ThemeMode.system, // 跟随系统;也可由设置里的开关控制 +); +``` + +读语义色(随明暗自动切换): +```dart +final c = context.pangolin; // PangolinScheme +Container(color: c.surface); +Text('已连接', style: PangolinText.h3.copyWith(color: c.success)); +``` + +令牌直取: +```dart +PangolinColors.clay500 // 品牌主色 +PangolinSpacing.x4 // 16 +BorderRadius.circular(PangolinRadius.lg) +AnimatedContainer(duration: PangolinMotion.base, curve: PangolinMotion.easeOut, ...) +``` + +组件雏形(组合示例): +```dart +import 'widgets/pangolin_widgets.dart'; + +// 连接键 +ConnectButton(status: VpnStatus.on, elapsed: const Duration(seconds: 8048), onTap: toggle); + +// 服务器行 +ServerTile( + server: const ServerInfo(code: 'HK', name: '香港 · 流媒体', sub: 'Hong Kong', ping: 18), + active: true, onTap: () {}, +); + +// 套餐卡(专业版高亮) +PlanCard( + name: '专业版', price: '¥25', period: '/月', popularLabel: '最受欢迎', featured: true, + features: const ['80+ 全球节点', '无限流量 · 极速', '5 台设备同时在线'], + ctaLabel: '立即升级', onPressed: () {}, +); + +PangolinButton(label: '连接', icon: PangolinIcons.power, onPressed: () {}); +StatusPill(label: '已连接', status: PangolinStatus.connected); +``` + +页面串接(登录 → 引导 → 主应用): +```dart +enum AppStage { auth, onboarding, app } + +class _RootState extends State { + AppStage stage = AppStage.auth; + @override + Widget build(BuildContext context) => switch (stage) { + AppStage.auth => AuthScreen(onDone: () => setState(() => stage = AppStage.onboarding)), + AppStage.onboarding => OnboardingScreen(onDone: () => setState(() => stage = AppStage.app)), + AppStage.app => const HomeShell(), // 主框架:底部 4 Tab(连接/节点/统计/账户) + }; +} +``` + +## 字体 +在 `pubspec.yaml` 注册 **Sora**(display)、**Manrope**(正文/UI)、**Noto Sans SC**(中文)、**JetBrains Mono**(数据)。可用 `google_fonts` 包或打包本地字体;族名见 `PangolinFonts`。 + +## 图标 +UI Kit 用 Lucide(细线条)。Flutter 侧用 [`lucide_icons`](https://pub.dev/packages/lucide_icons) 包,经 `widgets/pangolin_icons.dart` 的映射表引用:`Icon(PangolinIcons.power)`。`pubspec.yaml` 加 `lucide_icons: ^0.x`。某常量名若在你的包版本缺失,按 IDE 提示替换为最接近名即可(文件内已注明 Lucide 原名)。 + +## Logo / 品牌标(SVG) +矢量 logo 在 `assets/`(`logo-mark.svg` clay 标、`logo-mark-white.svg` 反白标、`logo-wordmark.svg` 字标、`app-icon.svg` App 图标)。用 [`flutter_svg`](https://pub.dev/packages/flutter_svg) 加载,封装在 `widgets/pangolin_logo.dart`: +- `PangolinMark(size: 28)` — 仅标记,**深色主题自动用反白版**(可 `forceLight`/`forceDark`) +- `PangolinBrandLockup(zh: true)` — 标记 + 「穿山甲 / Pangolin」(随主题变色 + 双语,顶栏用) +- `PangolinWordmark(height: 28)` — 横版字标锁版(浅背景) +- `PangolinAppIcon(size: 64)` — App 图标(自动圆角) + +接入步骤: +1. `pubspec.yaml` 加依赖与资产: + ```yaml + dependencies: + flutter_svg: ^2.0.0 + flutter: + assets: + - assets/logo-mark.svg + - assets/logo-mark-white.svg + - assets/logo-wordmark.svg + - assets/app-icon.svg + ``` +2. 把本目录的 `assets/*.svg` 拷进你工程的 `assets/`(若用别的前缀,改 `pangolin_logo.dart` 顶部的 `_base`)。 +3. `AuthScreen` / `HomeShell` 顶栏已用 `PangolinMark` / `PangolinBrandLockup`,无需再改。 +4. App 启动图标 / 桌面图标用 `app-icon.svg` 导出 PNG(`flutter_launcher_icons` 包)。 + +## 规则(务必遵守) +- 颜色只用语义令牌,组件天然适配明/暗,不要硬编码十六进制。 +- 暖色基调:表面是暖白/暖 espresso,**不要纯白纯黑**大面积填充。 +- 圆角偏大、阴影柔和暖调;状态用「色点 + 文字」,不用 emoji;国家用 2 字母码块,不用 emoji 国旗。 +- 界面单语言显示(中文**或** EN),由设置切换;不要中英并排。 +- App 内不放支付表单;走兑换码 + 外部渠道。 + +> ⚠️ 字体均为开源字体(Google Fonts),作为本品牌正式选定字体。若你已有授权品牌字体,替换族名与文件即可。 diff --git a/design/flutter/assets/app-icon.svg b/design/flutter/assets/app-icon.svg new file mode 100644 index 0000000..c7eae9a --- /dev/null +++ b/design/flutter/assets/app-icon.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/design/flutter/assets/logo-mark-white.svg b/design/flutter/assets/logo-mark-white.svg new file mode 100644 index 0000000..0584f5d --- /dev/null +++ b/design/flutter/assets/logo-mark-white.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/design/flutter/assets/logo-mark.svg b/design/flutter/assets/logo-mark.svg new file mode 100644 index 0000000..b3e364b --- /dev/null +++ b/design/flutter/assets/logo-mark.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/design/flutter/assets/logo-wordmark.svg b/design/flutter/assets/logo-wordmark.svg new file mode 100644 index 0000000..8603db5 --- /dev/null +++ b/design/flutter/assets/logo-wordmark.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + 穿山甲 + PANGOLIN + \ No newline at end of file diff --git a/design/flutter/main.dart b/design/flutter/main.dart new file mode 100644 index 0000000..f03db95 --- /dev/null +++ b/design/flutter/main.dart @@ -0,0 +1,58 @@ +// main.dart — 入口示例 +// 演示:登录 → 引导 → 主框架(4 Tab)的完整串接,套用穿山甲明/暗主题。 +// 把 lib/ 下的 pangolin_theme.dart 与 widgets/ 一并拷进工程,按实际目录改 import 路径。 +import 'package:flutter/material.dart'; +import 'pangolin_theme.dart'; +import 'widgets/pangolin_widgets.dart'; + +void main() => runApp(const PangolinApp()); + +class PangolinApp extends StatefulWidget { + const PangolinApp({super.key}); + @override + State createState() => _PangolinAppState(); +} + +class _PangolinAppState extends State { + // 全局开关:语言 + 主题(真实工程可换成 Provider / Riverpod / Bloc 等) + bool zh = true; + ThemeMode mode = ThemeMode.system; + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: '穿山甲', + debugShowCheckedModeBanner: false, + theme: PangolinTheme.light, + darkTheme: PangolinTheme.dark, + themeMode: mode, + home: RootFlow(zh: zh), + ); + } +} + +/// 应用阶段:登录 → 引导 → 主应用 +enum AppStage { auth, onboarding, app } + +class RootFlow extends StatefulWidget { + const RootFlow({super.key, this.zh = true}); + final bool zh; + @override + State createState() => _RootFlowState(); +} + +class _RootFlowState extends State { + AppStage stage = AppStage.auth; + + @override + Widget build(BuildContext context) { + switch (stage) { + case AppStage.auth: + return AuthScreen(zh: widget.zh, onDone: () => setState(() => stage = AppStage.onboarding)); + case AppStage.onboarding: + return OnboardingScreen(onDone: () => setState(() => stage = AppStage.app)); + case AppStage.app: + return HomeShell(zh: widget.zh); + } + } +} diff --git a/design/flutter/pangolin_theme.dart b/design/flutter/pangolin_theme.dart new file mode 100644 index 0000000..9548313 --- /dev/null +++ b/design/flutter/pangolin_theme.dart @@ -0,0 +1,307 @@ +// pangolin_theme.dart +// 穿山甲 VPN · Pangolin VPN — Flutter design tokens +// +// Single Dart source mirroring colors_and_type.css. Generated by hand to be +// Flutter-friendly: const color ramps, spacing/radii/durations, text styles, +// and ready-to-use light + dark ThemeData. +// +// Fonts: add Sora, Manrope, Noto Sans SC, JetBrains Mono to pubspec.yaml +// (google_fonts package or bundled assets). Family names below assume those. +// +// Usage: +// MaterialApp( +// theme: PangolinTheme.light, +// darkTheme: PangolinTheme.dark, +// themeMode: ThemeMode.system, +// ); +// // tokens: PangolinColors.clay500, PangolinSpacing.x4, PangolinRadius.lg ... + +import 'package:flutter/material.dart'; + +/// ── Primitive color ramps ─────────────────────────────────────────── +class PangolinColors { + PangolinColors._(); + + // Clay / Copper — pangolin-armor primary (warm earth) + static const clay50 = Color(0xFFFAF3ED); + static const clay100 = Color(0xFFF2E2D4); + static const clay200 = Color(0xFFE6C7AC); + static const clay300 = Color(0xFFD9A982); + static const clay400 = Color(0xFFCC8B5C); + static const clay500 = Color(0xFFB96A3D); // brand primary + static const clay600 = Color(0xFF9E5630); + static const clay700 = Color(0xFF7E4426); + static const clay800 = Color(0xFF5E331D); + static const clay900 = Color(0xFF3D2213); + + // Sand / Taupe — warm neutral ramp + static const sand50 = Color(0xFFFAF8F4); + static const sand100 = Color(0xFFF2EEE7); + static const sand200 = Color(0xFFE6DFD3); + static const sand300 = Color(0xFFD2C8B8); + static const sand400 = Color(0xFFB0A491); + static const sand500 = Color(0xFF8C8270); + static const sand600 = Color(0xFF6B6253); + static const sand700 = Color(0xFF4E4940); + static const sand800 = Color(0xFF2E2A24); + static const sand900 = Color(0xFF1F1C18); + static const sand950 = Color(0xFF14110E); // warm espresso near-black + + // Semantic hues (earth-tuned) + static const green400 = Color(0xFF7FB07A); + static const green500 = Color(0xFF5B8C5A); // connected / secure + static const green600 = Color(0xFF467046); + static const amber400 = Color(0xFFE2B05A); + static const amber500 = Color(0xFFD69A3C); // connecting / warning + static const amber600 = Color(0xFFB47E29); + static const red400 = Color(0xFFD4715A); + static const red500 = Color(0xFFC0533B); // error / disconnect + static const red600 = Color(0xFF9E4230); + + static const white = Color(0xFFFFFFFF); +} + +/// ── Semantic tokens, resolved per brightness ─────────────────────── +/// Read these off the active theme: `context.pangolin` (see extension below), +/// or build directly with PangolinScheme.light / .dark. +@immutable +class PangolinScheme extends ThemeExtension { + const PangolinScheme({ + required this.bg, + required this.bgSubtle, + required this.surface, + required this.surface2, + required this.overlay, + required this.fg1, + required this.fg2, + required this.fg3, + required this.fgOnAccent, + required this.accent, + required this.accentHover, + required this.accentPress, + required this.accentSubtle, + required this.accentBorder, + required this.border, + required this.borderStrong, + required this.ring, + required this.success, + required this.successSubtle, + required this.warning, + required this.warningSubtle, + required this.danger, + required this.dangerSubtle, + }); + + final Color bg, bgSubtle, surface, surface2, overlay; + final Color fg1, fg2, fg3, fgOnAccent; + final Color accent, accentHover, accentPress, accentSubtle, accentBorder; + final Color border, borderStrong, ring; + final Color success, successSubtle, warning, warningSubtle, danger, dangerSubtle; + + static const light = PangolinScheme( + bg: PangolinColors.sand50, + bgSubtle: PangolinColors.sand100, + surface: PangolinColors.white, + surface2: PangolinColors.sand50, + overlay: Color(0x731F1C18), // rgba(31,28,24,0.45) + fg1: PangolinColors.sand900, + fg2: PangolinColors.sand600, + fg3: PangolinColors.sand500, + fgOnAccent: PangolinColors.white, + accent: PangolinColors.clay500, + accentHover: PangolinColors.clay600, + accentPress: PangolinColors.clay700, + accentSubtle: PangolinColors.clay50, + accentBorder: PangolinColors.clay200, + border: PangolinColors.sand200, + borderStrong: PangolinColors.sand300, + ring: Color(0x59B96A3D), // rgba(185,106,61,0.35) + success: PangolinColors.green500, + successSubtle: Color(0xFFE9F0E6), + warning: PangolinColors.amber500, + warningSubtle: Color(0xFFF8EED6), + danger: PangolinColors.red500, + dangerSubtle: Color(0xFFF6E1DA), + ); + + static const dark = PangolinScheme( + bg: PangolinColors.sand950, + bgSubtle: PangolinColors.sand900, + surface: Color(0xFF221E19), + surface2: Color(0xFF2A251F), + overlay: Color(0x99000000), // rgba(0,0,0,0.6) + fg1: Color(0xFFF4EFE8), + fg2: Color(0xFFB6AC9C), + fg3: Color(0xFF897F6F), + fgOnAccent: PangolinColors.sand900, + accent: PangolinColors.clay400, + accentHover: PangolinColors.clay300, + accentPress: PangolinColors.clay500, + accentSubtle: Color(0x24CC8B5C), // rgba(204,139,92,0.14) + accentBorder: Color(0x4DCC8B5C), // rgba(204,139,92,0.30) + border: Color(0x1AF2EEE7), // rgba(242,238,231,0.10) + borderStrong: Color(0x2EF2EEE7), // rgba(242,238,231,0.18) + ring: Color(0x73CC8B5C), // rgba(204,139,92,0.45) + success: PangolinColors.green400, + successSubtle: Color(0x297FB07A), + warning: PangolinColors.amber400, + warningSubtle: Color(0x29E2B05A), + danger: PangolinColors.red400, + dangerSubtle: Color(0x29D4715A), + ); + + @override + PangolinScheme copyWith({Color? bg, Color? fg1, Color? accent}) => PangolinScheme( + bg: bg ?? this.bg, bgSubtle: bgSubtle, surface: surface, surface2: surface2, + overlay: overlay, fg1: fg1 ?? this.fg1, fg2: fg2, fg3: fg3, fgOnAccent: fgOnAccent, + accent: accent ?? this.accent, accentHover: accentHover, accentPress: accentPress, + accentSubtle: accentSubtle, accentBorder: accentBorder, border: border, + borderStrong: borderStrong, ring: ring, success: success, successSubtle: successSubtle, + warning: warning, warningSubtle: warningSubtle, danger: danger, dangerSubtle: dangerSubtle, + ); + + @override + PangolinScheme lerp(ThemeExtension? other, double t) { + if (other is! PangolinScheme) return this; + Color c(Color a, Color b) => Color.lerp(a, b, t)!; + return PangolinScheme( + bg: c(bg, other.bg), bgSubtle: c(bgSubtle, other.bgSubtle), surface: c(surface, other.surface), + surface2: c(surface2, other.surface2), overlay: c(overlay, other.overlay), + fg1: c(fg1, other.fg1), fg2: c(fg2, other.fg2), fg3: c(fg3, other.fg3), fgOnAccent: c(fgOnAccent, other.fgOnAccent), + accent: c(accent, other.accent), accentHover: c(accentHover, other.accentHover), accentPress: c(accentPress, other.accentPress), + accentSubtle: c(accentSubtle, other.accentSubtle), accentBorder: c(accentBorder, other.accentBorder), + border: c(border, other.border), borderStrong: c(borderStrong, other.borderStrong), ring: c(ring, other.ring), + success: c(success, other.success), successSubtle: c(successSubtle, other.successSubtle), + warning: c(warning, other.warning), warningSubtle: c(warningSubtle, other.warningSubtle), + danger: c(danger, other.danger), dangerSubtle: c(dangerSubtle, other.dangerSubtle), + ); + } +} + +extension PangolinContext on BuildContext { + PangolinScheme get pangolin => Theme.of(this).extension()!; +} + +/// ── Spacing (4px base) ────────────────────────────────────────────── +class PangolinSpacing { + PangolinSpacing._(); + static const double x1 = 4, x2 = 8, x3 = 12, x4 = 16, x5 = 20, + x6 = 24, x8 = 32, x10 = 40, x12 = 48, x16 = 64; +} + +/// ── Radii ─────────────────────────────────────────────────────────── +class PangolinRadius { + PangolinRadius._(); + static const double sm = 6, md = 10, lg = 14, xl = 20, xxl = 28, full = 999; + static const rSm = Radius.circular(sm); + static const rMd = Radius.circular(md); + static const rLg = Radius.circular(lg); + static const rXl = Radius.circular(xl); + static const rXxl = Radius.circular(xxl); +} + +/// ── Motion ────────────────────────────────────────────────────────── +class PangolinMotion { + PangolinMotion._(); + static const fast = Duration(milliseconds: 140); + static const base = Duration(milliseconds: 220); + static const slow = Duration(milliseconds: 360); + // easeOut cubic-bezier(0.22, 1, 0.36, 1) + static const easeOut = Cubic(0.22, 1, 0.36, 1); + static const easeInOut = Cubic(0.65, 0, 0.35, 1); +} + +/// ── Font families ─────────────────────────────────────────────────── +class PangolinFonts { + PangolinFonts._(); + static const display = 'Sora'; + static const sans = 'Manrope'; + static const cjk = 'Noto Sans SC'; + static const mono = 'JetBrains Mono'; +} + +/// ── Type scale (sizes mirror the CSS rem scale at 16px root) ──────── +class PangolinText { + PangolinText._(); + // Build with a color at call site: PangolinText.h1.copyWith(color: ctx.pangolin.fg1) + static const displayXl = TextStyle(fontFamily: PangolinFonts.display, fontSize: 48, fontWeight: FontWeight.w700, height: 1.15, letterSpacing: -0.96); + static const display = TextStyle(fontFamily: PangolinFonts.display, fontSize: 36, fontWeight: FontWeight.w700, height: 1.15, letterSpacing: -0.72); + static const h1 = TextStyle(fontFamily: PangolinFonts.display, fontSize: 30, fontWeight: FontWeight.w600, height: 1.3, letterSpacing: -0.3); + static const h2 = TextStyle(fontFamily: PangolinFonts.display, fontSize: 24, fontWeight: FontWeight.w600, height: 1.3, letterSpacing: -0.24); + static const h3 = TextStyle(fontFamily: PangolinFonts.sans, fontSize: 20, fontWeight: FontWeight.w600, height: 1.3); + static const bodyLg = TextStyle(fontFamily: PangolinFonts.sans, fontSize: 18, fontWeight: FontWeight.w400, height: 1.65); + static const body = TextStyle(fontFamily: PangolinFonts.sans, fontSize: 16, fontWeight: FontWeight.w400, height: 1.5); + static const sm = TextStyle(fontFamily: PangolinFonts.sans, fontSize: 14, fontWeight: FontWeight.w400, height: 1.5); + static const caption = TextStyle(fontFamily: PangolinFonts.sans, fontSize: 12, fontWeight: FontWeight.w500, height: 1.5); + static const overline = TextStyle(fontFamily: PangolinFonts.sans, fontSize: 12, fontWeight: FontWeight.w600, letterSpacing: 0.96); + static const mono = TextStyle(fontFamily: PangolinFonts.mono, fontSize: 14, fontWeight: FontWeight.w400, fontFeatures: [FontFeature.tabularFigures()]); +} + +/// ── Soft warm-tinted shadows ──────────────────────────────────────── +class PangolinShadow { + PangolinShadow._(); + static const _tint = Color(0xFF2D1E14); // warm 45,30,20 + static List sm = [BoxShadow(color: _tint.withOpacity(0.06), blurRadius: 2, offset: const Offset(0, 1))]; + static List md = [BoxShadow(color: _tint.withOpacity(0.08), blurRadius: 14, offset: const Offset(0, 4))]; + static List lg = [BoxShadow(color: _tint.withOpacity(0.12), blurRadius: 32, offset: const Offset(0, 12))]; + static List xl = [BoxShadow(color: _tint.withOpacity(0.16), blurRadius: 60, offset: const Offset(0, 24))]; +} + +/// ── Ready-to-use ThemeData ────────────────────────────────────────── +class PangolinTheme { + PangolinTheme._(); + + static ThemeData get light => _build(PangolinScheme.light, Brightness.light); + static ThemeData get dark => _build(PangolinScheme.dark, Brightness.dark); + + static ThemeData _build(PangolinScheme s, Brightness b) { + final scheme = ColorScheme( + brightness: b, + primary: s.accent, + onPrimary: s.fgOnAccent, + secondary: s.accent, + onSecondary: s.fgOnAccent, + error: s.danger, + onError: PangolinColors.white, + surface: s.surface, + onSurface: s.fg1, + ); + return ThemeData( + useMaterial3: true, + brightness: b, + colorScheme: scheme, + scaffoldBackgroundColor: s.bg, + fontFamily: PangolinFonts.sans, + extensions: [s], + filledButtonTheme: FilledButtonThemeData( + style: FilledButton.styleFrom( + backgroundColor: s.accent, + foregroundColor: s.fgOnAccent, + textStyle: const TextStyle(fontFamily: PangolinFonts.sans, fontWeight: FontWeight.w600, fontSize: 15), + shape: const StadiumBorder(), + padding: const EdgeInsets.symmetric(horizontal: 22, vertical: 14), + ), + ), + cardTheme: CardThemeData( + color: s.surface, + elevation: 0, + shape: RoundedRectangleBorder( + side: BorderSide(color: s.border), + borderRadius: BorderRadius.circular(PangolinRadius.lg), + ), + ), + inputDecorationTheme: InputDecorationTheme( + filled: true, + fillColor: s.bg, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(PangolinRadius.md), + borderSide: BorderSide(color: s.borderStrong, width: 1.5), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(PangolinRadius.md), + borderSide: BorderSide(color: s.accent, width: 1.5), + ), + ), + ); + } +} diff --git a/design/flutter/pubspec.yaml b/design/flutter/pubspec.yaml new file mode 100644 index 0000000..8e39002 --- /dev/null +++ b/design/flutter/pubspec.yaml @@ -0,0 +1,84 @@ +# pubspec.yaml — 穿山甲 VPN · Pangolin VPN(模板) +# 把本文件放到 Flutter 工程根目录,按需调整 name/version,然后 `flutter pub get`。 +# 字体文件需自备(Google Fonts 下载或用 google_fonts 包);SVG 资产已在 flutter/assets/。 + +name: pangolin_vpn +description: 穿山甲 · Pangolin — 极简、稳定、跨平台网络加速客户端。 +publish_to: "none" +version: 1.0.0+1 + +environment: + sdk: ^3.5.0 + flutter: ">=3.24.0" + +dependencies: + flutter: + sdk: flutter + flutter_svg: ^2.0.10 # 加载 assets/ 下的矢量 logo + lucide_icons: ^0.257.0 # Lucide 细线条图标(版本以 pub.dev 最新为准) + # 可选:用 google_fonts 在线加载字体,免去本地 ttf + # google_fonts: ^6.2.1 + # 可选:打开外部购买/联系渠道链接 + # url_launcher: ^6.3.0 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^4.0.0 + # 可选:用 app-icon.svg 生成各平台启动图标 + # flutter_launcher_icons: ^0.13.1 + +flutter: + uses-material-design: true + + # ── 资产(SVG logo)── + # 把 flutter/assets/*.svg 拷到工程 assets/ 下;如改路径,同步改 pangolin_logo.dart 的 _base。 + assets: + - assets/logo-mark.svg + - assets/logo-mark-white.svg + - assets/logo-wordmark.svg + - assets/app-icon.svg + + # ── 字体 ── + # 自备 ttf 放 fonts/;或改用 google_fonts 包(则删掉此段,在代码里用 GoogleFonts.*)。 + # 族名必须与 pangolin_theme.dart 的 PangolinFonts 完全一致。 + fonts: + - family: Sora + fonts: + - asset: fonts/Sora-Medium.ttf + weight: 500 + - asset: fonts/Sora-SemiBold.ttf + weight: 600 + - asset: fonts/Sora-Bold.ttf + weight: 700 + - family: Manrope + fonts: + - asset: fonts/Manrope-Regular.ttf + weight: 400 + - asset: fonts/Manrope-Medium.ttf + weight: 500 + - asset: fonts/Manrope-SemiBold.ttf + weight: 600 + - asset: fonts/Manrope-Bold.ttf + weight: 700 + - family: Noto Sans SC + fonts: + - asset: fonts/NotoSansSC-Regular.ttf + weight: 400 + - asset: fonts/NotoSansSC-Medium.ttf + weight: 500 + - asset: fonts/NotoSansSC-Bold.ttf + weight: 700 + - family: JetBrains Mono + fonts: + - asset: fonts/JetBrainsMono-Regular.ttf + weight: 400 + - asset: fonts/JetBrainsMono-Medium.ttf + weight: 500 + +# ── flutter_launcher_icons 配置(可选,取消注释并运行 `dart run flutter_launcher_icons`)── +# flutter_launcher_icons: +# image_path: "assets/app-icon-1024.png" # 先把 app-icon.svg 导出 1024px PNG +# android: true +# ios: true +# remove_alpha_ios: true diff --git a/design/flutter/widgets/account_screens.dart b/design/flutter/widgets/account_screens.dart new file mode 100644 index 0000000..3a8f4d6 --- /dev/null +++ b/design/flutter/widgets/account_screens.dart @@ -0,0 +1,296 @@ +// account_screens.dart — 账户子页(套餐选择 / 设备管理 / 兑换 & 购买 / 联系我们) +// 对应 React UI Kit 的 PlansScreen / DAccountView 设备段 / RedeemScreen / ContactScreen。 +// 均为带返回栏的独立页;HomeShell 账户页的行点击后 push 这些页即可。 +import 'package:flutter/material.dart'; +import '../pangolin_theme.dart'; +import 'pangolin_icons.dart'; +import 'plan_card.dart'; +import 'pangolin_button.dart'; + +/// 通用:带返回箭头的子页骨架 +class _SubScaffold extends StatelessWidget { + const _SubScaffold({required this.title, required this.child, this.onBack}); + final String title; final Widget child; final VoidCallback? onBack; + @override + Widget build(BuildContext context) { + final c = context.pangolin; + return Scaffold( + backgroundColor: c.bg, + body: SafeArea(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Padding( + padding: const EdgeInsets.fromLTRB(8, 6, 16, 10), + child: Row(children: [ + IconButton(onPressed: onBack ?? () => Navigator.of(context).maybePop(), icon: Icon(PangolinIcons.arrowLeft, size: 22, color: c.fg1)), + Text(title, style: PangolinText.h3.copyWith(color: c.fg1, fontFamily: PangolinFonts.display, fontWeight: FontWeight.w700)), + ]), + ), + Expanded(child: child), + ])), + ); + } +} + +/// ── 套餐选择 ──(点「续费 / 升级」进入) +class PlansScreen extends StatelessWidget { + const PlansScreen({super.key, this.zh = true, this.onChoose, this.onBack}); + final bool zh; final ValueChanged? onChoose; final VoidCallback? onBack; + + @override + Widget build(BuildContext context) { + final plans = [ + ( + id: 'free', name: zh ? '免费版' : 'Free', price: '¥0', cta: zh ? '当前' : 'Current', featured: false, current: true, pop: null, + feats: zh ? ['3 个基础节点', '每日 1GB 流量', '1 台设备'] : ['3 basic locations', '1GB / day', '1 device'], + ), + ( + id: 'pro', name: zh ? '专业版' : 'Pro', price: '¥25', cta: zh ? '立即升级' : 'Upgrade', featured: true, current: false, pop: zh ? '最受欢迎' : 'Popular', + feats: zh ? ['80+ 全球节点', '无限流量 · 极速', '5 台设备同时在线', '流媒体优化'] : ['80+ locations', 'Unlimited · fast', '5 devices', 'Streaming optimized'], + ), + ( + id: 'team', name: zh ? '团队版' : 'Team', price: '¥99', cta: zh ? '选择' : 'Choose', featured: false, current: false, pop: null, + feats: zh ? ['Pro 全部功能', '10 个成员席位', '集中计费与管理', '优先客服'] : ['Everything in Pro', '10 seats', 'Central billing', 'Priority support'], + ), + ]; + return _SubScaffold( + title: zh ? '选择套餐' : 'Choose plan', + onBack: onBack, + child: ListView( + padding: const EdgeInsets.fromLTRB(20, 14, 20, 24), + children: [ + for (final p in plans) Padding( + padding: EdgeInsets.only(bottom: 14, top: p.pop != null ? 12 : 0), + child: PlanCard( + name: p.name, price: p.price, period: zh ? '/月' : '/mo', + features: p.feats, ctaLabel: p.cta, featured: p.featured, isCurrent: p.current, + popularLabel: p.pop, onPressed: () => onChoose?.call(p.id), + ), + ), + ], + ), + ); + } +} + +/// ── 设备管理 ── +class DeviceItem { + const DeviceItem({required this.id, required this.icon, required this.name, required this.os, required this.active, this.me = false}); + final String id, name, os, active; final IconData icon; final bool me; +} + +class DevicesScreen extends StatefulWidget { + const DevicesScreen({super.key, this.zh = true, this.onBack}); + final bool zh; final VoidCallback? onBack; + @override + State createState() => _DevicesScreenState(); +} + +class _DevicesScreenState extends State { + late List _devices = [ + DeviceItem(id: '1', icon: PangolinIcons.laptop, name: 'MacBook Pro', os: 'macOS 26', active: widget.zh ? '当前设备' : 'This device', me: true), + DeviceItem(id: '2', icon: PangolinIcons.smartphone, name: 'iPhone 17', os: 'iOS 26', active: '2 min'), + DeviceItem(id: '3', icon: PangolinIcons.monitorSmartphone, name: 'iPad Air', os: 'iPadOS 26', active: widget.zh ? '3 小时前' : '3h ago'), + ]; + + @override + Widget build(BuildContext context) { + final c = context.pangolin; + return _SubScaffold( + title: widget.zh ? '我的设备' : 'My devices', + onBack: widget.onBack, + child: ListView(padding: const EdgeInsets.fromLTRB(20, 4, 20, 24), children: [ + Text(widget.zh ? 'PRO 套餐最多 5 台设备同时在线' : 'Up to 5 devices on PRO', style: PangolinText.caption.copyWith(color: c.fg3, fontWeight: FontWeight.w400)), + const SizedBox(height: 12), + Container( + decoration: BoxDecoration(color: c.surface, borderRadius: BorderRadius.circular(PangolinRadius.lg), border: Border.all(color: c.border), boxShadow: PangolinShadow.sm), + clipBehavior: Clip.antiAlias, + child: Column(children: [ + for (var i = 0; i < _devices.length; i++) _row(c, _devices[i], i < _devices.length - 1), + ]), + ), + ]), + ); + } + + Widget _row(PangolinScheme c, DeviceItem d, bool divider) { + return Container( + decoration: BoxDecoration(border: divider ? Border(bottom: BorderSide(color: c.border)) : null), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), + child: Row(children: [ + Container(width: 38, height: 38, decoration: BoxDecoration(color: c.accentSubtle, borderRadius: BorderRadius.circular(PangolinRadius.md)), + child: Icon(d.icon, size: 19, color: c.accent)), + const SizedBox(width: 12), + Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Row(children: [ + Flexible(child: Text(d.name, style: PangolinText.sm.copyWith(color: c.fg1, fontWeight: FontWeight.w600, fontSize: 14.5))), + if (d.me) Container(margin: const EdgeInsets.only(left: 7), padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 1), + decoration: BoxDecoration(color: c.successSubtle, borderRadius: BorderRadius.circular(PangolinRadius.full)), + child: Text(d.active, style: PangolinText.caption.copyWith(color: c.success, fontWeight: FontWeight.w600, fontSize: 10.5))), + ]), + const SizedBox(height: 1), + Text(d.me ? d.os : '${d.os} · ${d.active}', style: PangolinText.caption.copyWith(color: c.fg3, fontWeight: FontWeight.w400)), + ])), + if (!d.me) OutlinedButton( + onPressed: () => setState(() => _devices.removeWhere((x) => x.id == d.id)), + style: OutlinedButton.styleFrom(foregroundColor: c.fg2, side: BorderSide(color: c.borderStrong, width: 1.5), shape: const StadiumBorder(), padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 7), minimumSize: Size.zero), + child: Text(widget.zh ? '移除' : 'Remove', style: PangolinText.caption.copyWith(fontWeight: FontWeight.w600)), + ), + ]), + ); + } +} + +/// ── 兑换 & 购买 ──(App 内无支付,走兑换码 + 外部渠道) +class RedeemScreen extends StatefulWidget { + const RedeemScreen({super.key, this.zh = true, this.onBack}); + final bool zh; final VoidCallback? onBack; + @override + State createState() => _RedeemScreenState(); +} + +class _RedeemScreenState extends State { + final _code = TextEditingController(); + bool _ok = false; + + @override + void dispose() { _code.dispose(); super.dispose(); } + + @override + Widget build(BuildContext context) { + final c = context.pangolin; + final zh = widget.zh; + final channels = [ + (icon: PangolinIcons.shoppingBag, name: zh ? '自助发卡商店' : 'Self-serve store', sub: 'shop.pangolin.vpn', accent: true), + (icon: PangolinIcons.send, name: 'Telegram', sub: '@PangolinVPN_bot', accent: false), + (icon: PangolinIcons.messageCircle, name: 'LINE', sub: '@pangolinvpn', accent: false), + (icon: PangolinIcons.mail, name: zh ? '邮箱客服' : 'Email support', sub: 'buy@pangolin.vpn', accent: false), + ]; + return _SubScaffold( + title: zh ? '兑换 & 购买' : 'Redeem & buy', + onBack: widget.onBack, + child: ListView(padding: const EdgeInsets.fromLTRB(20, 4, 20, 24), children: [ + // redeem code + Container( + padding: const EdgeInsets.all(18), + decoration: BoxDecoration(color: c.surface, borderRadius: BorderRadius.circular(PangolinRadius.xl), border: Border.all(color: c.border), boxShadow: PangolinShadow.sm), + child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Text(zh ? '兑换激活码' : 'Redeem a code', style: PangolinText.sm.copyWith(color: c.fg1, fontWeight: FontWeight.w700, fontSize: 14.5)), + const SizedBox(height: 12), + if (_ok) Row(children: [ + Icon(PangolinIcons.checkCircle, size: 20, color: c.success), const SizedBox(width: 9), + Text(zh ? '激活成功 · PRO 已开通' : 'Activated · PRO unlocked', style: PangolinText.body.copyWith(color: c.success, fontWeight: FontWeight.w600)), + ]) else Row(children: [ + Expanded(child: Container( + padding: const EdgeInsets.symmetric(horizontal: 14), + decoration: BoxDecoration(color: c.bg, borderRadius: BorderRadius.circular(PangolinRadius.md), border: Border.all(color: c.borderStrong, width: 1.5)), + child: TextField(controller: _code, + textCapitalization: TextCapitalization.characters, + style: PangolinText.mono.copyWith(color: c.fg1, letterSpacing: 1.5), + decoration: InputDecoration(border: InputBorder.none, isCollapsed: true, contentPadding: const EdgeInsets.symmetric(vertical: 13), + hintText: zh ? '输入激活码' : 'Enter code', hintStyle: PangolinText.sm.copyWith(color: c.fg3)), + onChanged: (_) => setState(() {})), + )), + const SizedBox(width: 10), + PangolinButton(label: zh ? '激活' : 'Redeem', onPressed: _code.text.trim().length >= 4 ? () => setState(() => _ok = true) : null), + ]), + ]), + ), + const SizedBox(height: 22), + Text(zh ? '购买渠道' : 'Where to buy', style: PangolinText.sm.copyWith(color: c.fg1, fontWeight: FontWeight.w700, fontSize: 14.5)), + const SizedBox(height: 6), + Text(zh ? 'App 内不支持直接支付。请通过以下渠道获取激活码:' : 'In-app payment is unavailable. Get a code via:', + style: PangolinText.caption.copyWith(color: c.fg3, fontWeight: FontWeight.w400, height: 1.5)), + const SizedBox(height: 14), + for (final ch in channels) Padding(padding: const EdgeInsets.only(bottom: 10), child: _channel(c, ch.icon, ch.name, ch.sub, ch.accent)), + ]), + ); + } + + Widget _channel(PangolinScheme c, IconData icon, String name, String sub, bool accent) { + return InkWell( + borderRadius: BorderRadius.circular(PangolinRadius.lg), + onTap: () {/* launchUrl(...) */}, + child: Container( + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: accent ? c.accentSubtle : c.surface, + borderRadius: BorderRadius.circular(PangolinRadius.lg), + border: Border.all(color: accent ? c.accentBorder : c.border), + boxShadow: PangolinShadow.sm, + ), + child: Row(children: [ + Container(width: 40, height: 40, decoration: BoxDecoration(color: accent ? c.accent : c.bgSubtle, borderRadius: BorderRadius.circular(PangolinRadius.md)), + child: Icon(icon, size: 20, color: accent ? PangolinColors.white : c.accent)), + const SizedBox(width: 13), + Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Text(name, style: PangolinText.body.copyWith(color: c.fg1, fontWeight: FontWeight.w600)), + Text(sub, style: PangolinText.caption.copyWith(color: c.fg3, fontFamily: PangolinFonts.mono, fontWeight: FontWeight.w400)), + ])), + Icon(PangolinIcons.externalLink, size: 16, color: c.fg3), + ]), + ), + ); + } +} + +/// ── 联系我们 ── +class ContactScreen extends StatelessWidget { + const ContactScreen({super.key, this.zh = true, this.onBack}); + final bool zh; final VoidCallback? onBack; + + @override + Widget build(BuildContext context) { + final c = context.pangolin; + final channels = [ + (icon: PangolinIcons.send, name: 'Telegram', sub: '@PangolinVPN_bot', accent: true), + (icon: PangolinIcons.messageCircle, name: 'LINE', sub: '@pangolinvpn', accent: false), + (icon: PangolinIcons.mail, name: zh ? '邮箱客服' : 'Email support', sub: 'support@pangolin.vpn', accent: false), + (icon: PangolinIcons.shoppingBag, name: zh ? '自助发卡商店' : 'Self-serve store', sub: 'shop.pangolin.vpn', accent: false), + ]; + return _SubScaffold( + title: zh ? '联系我们' : 'Contact us', + onBack: onBack, + child: ListView(padding: const EdgeInsets.fromLTRB(20, 4, 20, 24), children: [ + Text(zh ? '遇到问题?通过以下任一渠道联系我们,通常数分钟内回复。' : 'Need help? Reach us via any channel below — usually replies in minutes.', + style: PangolinText.sm.copyWith(color: c.fg2, height: 1.6)), + const SizedBox(height: 16), + for (final ch in channels) Padding(padding: const EdgeInsets.only(bottom: 10), child: _contact(c, ch.icon, ch.name, ch.sub, ch.accent)), + const SizedBox(height: 8), + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration(color: c.surface, borderRadius: BorderRadius.circular(PangolinRadius.lg), border: Border.all(color: c.border), boxShadow: PangolinShadow.sm), + child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Text(zh ? '服务时间' : 'Support hours', style: PangolinText.caption.copyWith(color: c.fg3, fontWeight: FontWeight.w600)), + const SizedBox(height: 5), + Text(zh ? '每日 9:00 – 24:00 (GMT+8)' : 'Daily 9:00 – 24:00 (GMT+8)', style: PangolinText.sm.copyWith(color: c.fg1, fontWeight: FontWeight.w600, fontFamily: PangolinFonts.mono)), + ]), + ), + ]), + ); + } + + Widget _contact(PangolinScheme c, IconData icon, String name, String sub, bool accent) { + return InkWell( + borderRadius: BorderRadius.circular(PangolinRadius.lg), + onTap: () {/* launchUrl(...) */}, + child: Container( + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: accent ? c.accentSubtle : c.surface, + borderRadius: BorderRadius.circular(PangolinRadius.lg), + border: Border.all(color: accent ? c.accentBorder : c.border), + boxShadow: PangolinShadow.sm, + ), + child: Row(children: [ + Container(width: 40, height: 40, decoration: BoxDecoration(color: accent ? c.accent : c.bgSubtle, borderRadius: BorderRadius.circular(PangolinRadius.md)), + child: Icon(icon, size: 20, color: accent ? PangolinColors.white : c.accent)), + const SizedBox(width: 13), + Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Text(name, style: PangolinText.body.copyWith(color: c.fg1, fontWeight: FontWeight.w600)), + Text(sub, style: PangolinText.caption.copyWith(color: c.fg3, fontFamily: PangolinFonts.mono, fontWeight: FontWeight.w400)), + ])), + Icon(PangolinIcons.externalLink, size: 16, color: c.fg3), + ]), + ), + ); + } +} diff --git a/design/flutter/widgets/auth_screen.dart b/design/flutter/widgets/auth_screen.dart new file mode 100644 index 0000000..3818e33 --- /dev/null +++ b/design/flutter/widgets/auth_screen.dart @@ -0,0 +1,159 @@ +// auth_screen.dart — 登录 / 注册页雏形 +// 对应 React UI Kit 的 AuthFlow / DAuthFlow。 +// 登录:邮箱 + 密码;注册:邮箱 → 发送验证码 → 填验证码 → 设置密码(多端登录)。 +import 'package:flutter/material.dart'; +import '../pangolin_theme.dart'; +import 'pangolin_icons.dart'; +import 'pangolin_button.dart'; +import 'pangolin_logo.dart'; + +enum _AuthMode { login, register } + +class AuthScreen extends StatefulWidget { + const AuthScreen({super.key, required this.onDone, this.zh = true}); + final VoidCallback onDone; // 登录/注册成功 → 进入引导或主应用 + final bool zh; // 简单 i18n:true=中文 false=EN + + @override + State createState() => _AuthScreenState(); +} + +class _AuthScreenState extends State { + _AuthMode _mode = _AuthMode.login; + int _step = 0; // 注册步骤:0 邮箱+验证码,1 设置密码 + bool _sent = false; + final _email = TextEditingController(); + final _code = TextEditingController(); + final _pw = TextEditingController(); + + bool get _emailValid => RegExp(r'\S+@\S+\.\S+').hasMatch(_email.text); + + String _t(String zh, String en) => widget.zh ? zh : en; + + @override + void dispose() { _email.dispose(); _code.dispose(); _pw.dispose(); super.dispose(); } + + @override + Widget build(BuildContext context) { + final c = context.pangolin; + return Scaffold( + backgroundColor: c.bg, + body: SafeArea( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 28), + child: Column( + children: [ + const SizedBox(height: 18), + // brand + Column(children: [ + const PangolinMark(size: 52), + const SizedBox(height: 12), + Text(widget.zh ? '穿山甲' : 'Pangolin', style: PangolinText.h1.copyWith(color: c.fg1)), + const SizedBox(height: 4), + Text(_t('极速 · 稳定 · 省心', 'Fast · Stable · Effortless'), + style: PangolinText.sm.copyWith(color: c.accent, fontWeight: FontWeight.w600)), + ]), + const SizedBox(height: 28), + // tabs + Row(children: [ + _tab(_AuthMode.login, _t('登录', 'Log in'), c), + _tab(_AuthMode.register, _t('注册', 'Sign up'), c), + ]), + Divider(height: 1, color: c.border), + const SizedBox(height: 22), + Expanded(child: _mode == _AuthMode.login ? _login(c) : _register(c)), + Padding( + padding: const EdgeInsets.symmetric(vertical: 20), + child: Text( + _t('继续即代表同意《服务条款》与《隐私政策》', 'By continuing you agree to our Terms & Privacy Policy'), + textAlign: TextAlign.center, + style: PangolinText.caption.copyWith(color: c.fg3, fontWeight: FontWeight.w400), + ), + ), + ], + ), + ), + ), + ); + } + + Widget _tab(_AuthMode m, String label, PangolinScheme c) { + final on = _mode == m; + return Expanded( + child: GestureDetector( + onTap: () => setState(() { _mode = m; _step = 0; _sent = false; }), + child: Container( + padding: const EdgeInsets.symmetric(vertical: 10), + decoration: BoxDecoration(border: Border(bottom: BorderSide(color: on ? c.accent : Colors.transparent, width: 2))), + child: Center(child: Text(label, style: PangolinText.body.copyWith(color: on ? c.fg1 : c.fg3, fontWeight: on ? FontWeight.w700 : FontWeight.w500))), + ), + ), + ); + } + + Widget _field(PangolinScheme c, {required IconData icon, required String label, required Widget child}) { + return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Text(label, style: PangolinText.caption.copyWith(color: c.fg2, fontWeight: FontWeight.w600)), + const SizedBox(height: 7), + Container( + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 2), + decoration: BoxDecoration(color: c.surface, borderRadius: BorderRadius.circular(PangolinRadius.md), border: Border.all(color: c.borderStrong, width: 1.5)), + child: Row(children: [Icon(icon, size: 18, color: c.fg3), const SizedBox(width: 10), Expanded(child: child)]), + ), + ]); + } + + InputDecoration get _bare => const InputDecoration(border: InputBorder.none, isCollapsed: true, contentPadding: EdgeInsets.symmetric(vertical: 12)); + + Widget _login(PangolinScheme c) { + return Column(children: [ + _field(c, icon: PangolinIcons.mail, label: _t('邮箱', 'Email'), + child: TextField(controller: _email, decoration: _bare.copyWith(hintText: 'your@email.com'), onChanged: (_) => setState(() {}))), + const SizedBox(height: 16), + _field(c, icon: PangolinIcons.lock, label: _t('密码', 'Password'), + child: TextField(controller: _pw, obscureText: true, decoration: _bare.copyWith(hintText: _t('输入密码', 'Enter password')), onChanged: (_) => setState(() {}))), + Align(alignment: Alignment.centerRight, child: Padding( + padding: const EdgeInsets.only(top: 8), + child: Text(_t('忘记密码?', 'Forgot password?'), style: PangolinText.sm.copyWith(color: c.accent, fontWeight: FontWeight.w600)), + )), + const SizedBox(height: 18), + PangolinButton(label: _t('登录', 'Log in'), expand: true, onPressed: (_emailValid && _pw.text.isNotEmpty) ? widget.onDone : null), + ]); + } + + Widget _register(PangolinScheme c) { + if (_step == 0) { + return Column(children: [ + _field(c, icon: PangolinIcons.mail, label: _t('邮箱', 'Email'), + child: Row(children: [ + Expanded(child: TextField(controller: _email, decoration: _bare.copyWith(hintText: 'your@email.com'), onChanged: (_) => setState(() {}))), + GestureDetector( + onTap: _emailValid ? () => setState(() => _sent = true) : null, + child: Text(_sent ? _t('重新发送', 'Resend') : _t('发送验证码', 'Send code'), + style: PangolinText.caption.copyWith(color: _emailValid ? c.accent : c.fg3, fontWeight: FontWeight.w700)), + ), + ])), + if (_sent) Padding(padding: const EdgeInsets.only(top: 8), child: Row(children: [ + Icon(PangolinIcons.checkCircle, size: 14, color: c.success), const SizedBox(width: 6), + Text('${_t('验证码已发送至', 'Code sent to')} ${_email.text}', style: PangolinText.caption.copyWith(color: c.success, fontWeight: FontWeight.w400)), + ])), + const SizedBox(height: 16), + _field(c, icon: PangolinIcons.shieldCheck, label: _t('邮箱验证码', 'Verification code'), + child: TextField(controller: _code, keyboardType: TextInputType.number, maxLength: 6, + style: PangolinText.mono.copyWith(letterSpacing: 6, color: c.fg1), + decoration: _bare.copyWith(counterText: '', hintText: '······'), onChanged: (_) => setState(() {}))), + const SizedBox(height: 8), + PangolinButton(label: _t('下一步', 'Next'), expand: true, onPressed: (_sent && _code.text.length == 6) ? () => setState(() => _step = 1) : null), + ]); + } + return Column(children: [ + Row(children: [Icon(PangolinIcons.checkCircle, size: 15, color: c.success), const SizedBox(width: 7), Text(_email.text, style: PangolinText.sm.copyWith(color: c.fg2))]), + const SizedBox(height: 16), + _field(c, icon: PangolinIcons.lock, label: _t('密码', 'Password'), + child: TextField(controller: _pw, obscureText: true, + decoration: _bare.copyWith(hintText: _t('设置登录密码(用于多端登录)', 'Set a password (multi-device)')), onChanged: (_) => setState(() {}))), + const SizedBox(height: 18), + PangolinButton(label: _t('创建账户', 'Create account'), expand: true, onPressed: _pw.text.length >= 6 ? widget.onDone : null), + ]); + } +} diff --git a/design/flutter/widgets/connect_button.dart b/design/flutter/widgets/connect_button.dart new file mode 100644 index 0000000..61b8510 --- /dev/null +++ b/design/flutter/widgets/connect_button.dart @@ -0,0 +1,147 @@ +// connect_button.dart — 核心连接键(三态:off / connecting / on) +// 对应 React UI Kit 的 ConnectScreen 大圆按钮:暖底轨道环 → 旋转弧 → 满环。 +import 'package:flutter/material.dart'; +import '../pangolin_theme.dart'; +import 'pangolin_icons.dart'; + +enum VpnStatus { off, connecting, on } + +class ConnectButton extends StatefulWidget { + const ConnectButton({ + super.key, + required this.status, + required this.onTap, + this.elapsed = Duration.zero, + this.size = 208, + }); + + final VpnStatus status; + final VoidCallback onTap; + final Duration elapsed; + final double size; + + @override + State createState() => _ConnectButtonState(); +} + +class _ConnectButtonState extends State with SingleTickerProviderStateMixin { + late final AnimationController _spin = + AnimationController(vsync: this, duration: const Duration(milliseconds: 1400))..repeat(); + + @override + void dispose() { + _spin.dispose(); + super.dispose(); + } + + String _fmt(Duration d) { + String two(int n) => n.toString().padLeft(2, '0'); + return '${two(d.inHours)}:${two(d.inMinutes % 60)}:${two(d.inSeconds % 60)}'; + } + + @override + Widget build(BuildContext context) { + final c = context.pangolin; + final s = widget.status; + + final Color fill = switch (s) { + VpnStatus.off => c.bgSubtle, + VpnStatus.connecting => c.accent, + VpnStatus.on => c.success, + }; + final Color fg = s == VpnStatus.off ? c.accent : PangolinColors.white; + final IconData icon = switch (s) { + VpnStatus.off => PangolinIcons.power, + VpnStatus.connecting => PangolinIcons.loader, + VpnStatus.on => PangolinIcons.shieldCheck, + }; + + final List glow = s == VpnStatus.off + ? PangolinShadow.md + : [ + BoxShadow( + color: (s == VpnStatus.on ? c.success : c.accent).withOpacity(0.18), + blurRadius: 0, + spreadRadius: 9, + ), + ...PangolinShadow.lg, + ]; + + return GestureDetector( + onTap: widget.onTap, + child: AnimatedContainer( + duration: PangolinMotion.slow, + curve: PangolinMotion.easeOut, + width: widget.size, + height: widget.size, + decoration: BoxDecoration(color: fill, shape: BoxShape.circle, boxShadow: glow), + child: Stack( + alignment: Alignment.center, + children: [ + // track ring + Positioned.fill( + child: AnimatedBuilder( + animation: _spin, + builder: (_, __) => CustomPaint( + painter: _RingPainter( + status: s, + track: c.sand200, + progress: PangolinColors.white, + turns: s == VpnStatus.connecting ? _spin.value : 0, + ), + ), + ), + ), + Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(icon, size: 50, color: fg), + const SizedBox(height: 6), + if (s == VpnStatus.on) + Text(_fmt(widget.elapsed), style: PangolinText.mono.copyWith(color: fg, fontSize: 18, fontWeight: FontWeight.w600)) + else + Text(s == VpnStatus.connecting ? '···' : 'TAP', + style: PangolinText.mono.copyWith(color: fg, fontSize: 14, fontWeight: FontWeight.w700, letterSpacing: 1.1)), + ], + ), + ], + ), + ), + ); + } +} + +class _RingPainter extends CustomPainter { + _RingPainter({required this.status, required this.track, required this.progress, required this.turns}); + final VpnStatus status; + final Color track; + final Color progress; + final double turns; + + @override + void paint(Canvas canvas, Size size) { + final center = size.center(Offset.zero); + final radius = size.width / 2 - 8; + final rect = Rect.fromCircle(center: center, radius: radius); + + if (status == VpnStatus.off) { + // dotted track + final p = Paint()..style = PaintingStyle.stroke..strokeWidth = 3..strokeCap = StrokeCap.round..color = track; + const dash = 0.045, gap = 0.11; + for (double a = 0; a < 6.283; a += dash + gap) { + canvas.drawArc(rect, a, dash, false, p); + } + } else if (status == VpnStatus.connecting) { + final base = Paint()..style = PaintingStyle.stroke..strokeWidth = 4..color = progress.withOpacity(0.3); + canvas.drawArc(rect, 0, 6.283, false, base); + final arc = Paint()..style = PaintingStyle.stroke..strokeWidth = 4..strokeCap = StrokeCap.round..color = progress; + canvas.drawArc(rect, turns * 6.283, 1.6, false, arc); + } else { + final p = Paint()..style = PaintingStyle.stroke..strokeWidth = 4..strokeCap = StrokeCap.round..color = progress.withOpacity(0.85); + canvas.drawArc(rect, 0, 6.283, false, p); + } + } + + @override + bool shouldRepaint(_RingPainter old) => old.turns != turns || old.status != status; +} diff --git a/design/flutter/widgets/country_code.dart b/design/flutter/widgets/country_code.dart new file mode 100644 index 0000000..dcba385 --- /dev/null +++ b/design/flutter/widgets/country_code.dart @@ -0,0 +1,61 @@ +// country_code.dart — 国家码方块 + 信号条 +// 对应 React UI Kit 的 CC / Signal 组件。不用 emoji 国旗。 +import 'package:flutter/material.dart'; +import '../pangolin_theme.dart'; + +class CountryCode extends StatelessWidget { + const CountryCode({super.key, required this.code, this.active = false, this.size = 38}); + final String code; + final bool active; + final double size; + + @override + Widget build(BuildContext context) { + final c = context.pangolin; + return Container( + width: size, + height: size, + alignment: Alignment.center, + decoration: BoxDecoration( + color: active ? c.accent : c.bgSubtle, + borderRadius: BorderRadius.circular(PangolinRadius.md), + ), + child: Text( + code, + style: PangolinText.mono.copyWith( + fontSize: size * 0.31, + fontWeight: FontWeight.w600, + color: active ? PangolinColors.white : c.fg2, + ), + ), + ); + } +} + +/// 三格信号条,依据延迟 ping 显示强度。 +class SignalBars extends StatelessWidget { + const SignalBars({super.key, required this.ping}); + final int ping; + + @override + Widget build(BuildContext context) { + final c = context.pangolin; + final level = ping < 40 ? 3 : ping < 90 ? 2 : 1; + const heights = [4.0, 8.0, 13.0]; + return Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.end, + children: List.generate(3, (i) { + return Container( + width: 3, + height: heights[i], + margin: EdgeInsets.only(left: i == 0 ? 0 : 2), + decoration: BoxDecoration( + color: i < level ? c.success : c.borderStrong, + borderRadius: BorderRadius.circular(1), + ), + ); + }), + ); + } +} diff --git a/design/flutter/widgets/home_shell.dart b/design/flutter/widgets/home_shell.dart new file mode 100644 index 0000000..dff720b --- /dev/null +++ b/design/flutter/widgets/home_shell.dart @@ -0,0 +1,328 @@ +// home_shell.dart — 主框架雏形(底部 4 Tab:连接 / 节点 / 统计 / 账户) +// 对应 React UI Kit 的 BottomTab + App 状态机。 +// 这是「外壳」:负责导航与连接状态机,各页内容为占位骨架,工程师按需接真实逻辑。 +import 'dart:async'; +import 'package:flutter/material.dart'; +import '../pangolin_theme.dart'; +import 'pangolin_icons.dart'; +import 'connect_button.dart'; +import 'server_tile.dart'; +import 'country_code.dart'; +import 'account_screens.dart'; +import 'pangolin_logo.dart'; + +class HomeShell extends StatefulWidget { + const HomeShell({super.key, this.zh = true}); + final bool zh; + @override + State createState() => _HomeShellState(); +} + +class _HomeShellState extends State { + int _tab = 0; // 0 连接 1 节点 2 统计 3 账户 + VpnStatus _status = VpnStatus.off; + ServerInfo _server = const ServerInfo(code: 'HK', name: '香港 · 流媒体', sub: 'Hong Kong', ping: 18); + Timer? _timer; + int _elapsed = 0; + + String _t(String zh, String en) => widget.zh ? zh : en; + + void _toggle() { + if (_status == VpnStatus.off) { + setState(() => _status = VpnStatus.connecting); + Future.delayed(const Duration(milliseconds: 1500), () { + if (!mounted) return; + setState(() { _status = VpnStatus.on; _elapsed = 0; }); + _timer = Timer.periodic(const Duration(seconds: 1), (_) => setState(() => _elapsed++)); + }); + } else { + _timer?.cancel(); + setState(() => _status = VpnStatus.off); + } + } + + void _pick(ServerInfo s) { + setState(() { _server = s; _tab = 0; }); + if (_status == VpnStatus.on) { + _timer?.cancel(); + setState(() => _status = VpnStatus.connecting); + Future.delayed(const Duration(milliseconds: 1200), () { + if (!mounted) return; + setState(() { _status = VpnStatus.on; _elapsed = 0; }); + _timer = Timer.periodic(const Duration(seconds: 1), (_) => setState(() => _elapsed++)); + }); + } + } + + @override + void dispose() { _timer?.cancel(); super.dispose(); } + + @override + Widget build(BuildContext context) { + final c = context.pangolin; + final pages = [ + _ConnectPage(zh: widget.zh, status: _status, server: _server, elapsedSec: _elapsed, onToggle: _toggle, onOpenServers: () => setState(() => _tab = 1)), + _ServersPage(zh: widget.zh, current: _server.code, onPick: _pick), + _StatsPage(zh: widget.zh), + _AccountPage(zh: widget.zh), + ]; + return Scaffold( + backgroundColor: c.bg, + body: SafeArea(bottom: false, child: pages[_tab]), + bottomNavigationBar: _BottomTab(zh: widget.zh, index: _tab, onTap: (i) => setState(() => _tab = i)), + ); + } +} + +/// ── Bottom tab bar ── +class _BottomTab extends StatelessWidget { + const _BottomTab({required this.zh, required this.index, required this.onTap}); + final bool zh; final int index; final ValueChanged onTap; + + @override + Widget build(BuildContext context) { + final c = context.pangolin; + final items = [ + (PangolinIcons.power, zh ? '连接' : 'Connect'), + (PangolinIcons.globe, zh ? '节点' : 'Servers'), + (PangolinIcons.barChart, zh ? '统计' : 'Stats'), + (PangolinIcons.user, zh ? '账户' : 'Account'), + ]; + return Container( + decoration: BoxDecoration(color: c.surface, border: Border(top: BorderSide(color: c.border))), + padding: const EdgeInsets.only(top: 8, bottom: 22), + child: Row(children: [ + for (var i = 0; i < items.length; i++) + Expanded(child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () => onTap(i), + child: Column(mainAxisSize: MainAxisSize.min, children: [ + Icon(items[i].$1, size: 22, color: i == index ? c.accent : c.fg3), + const SizedBox(height: 4), + Text(items[i].$2, style: PangolinText.caption.copyWith(color: i == index ? c.accent : c.fg3, fontWeight: i == index ? FontWeight.w700 : FontWeight.w500)), + ]), + )), + ]), + ); + } +} + +/// ── Connect page ── +class _ConnectPage extends StatelessWidget { + const _ConnectPage({required this.zh, required this.status, required this.server, required this.elapsedSec, required this.onToggle, required this.onOpenServers}); + final bool zh; final VpnStatus status; final ServerInfo server; final int elapsedSec; final VoidCallback onToggle, onOpenServers; + + @override + Widget build(BuildContext context) { + final c = context.pangolin; + final cap = switch (status) { + VpnStatus.off => zh ? '未连接 · 轻点连接' : 'Tap to connect', + VpnStatus.connecting => zh ? '连接中…' : 'Connecting…', + VpnStatus.on => zh ? '已连接 · 网络已加密' : 'Connected · Encrypted', + }; + return Column(children: [ + _TopBar(zh: zh, trailing: Text(status == VpnStatus.on ? (zh ? '● 在线' : '● Online') : (zh ? '○ 离线' : '○ Offline'), + style: PangolinText.caption.copyWith(fontFamily: PangolinFonts.mono, color: status == VpnStatus.on ? c.success : c.fg3, fontWeight: FontWeight.w600))), + Expanded(child: Center(child: Column(mainAxisSize: MainAxisSize.min, children: [ + ConnectButton(status: status, elapsed: Duration(seconds: elapsedSec), onTap: onToggle), + const SizedBox(height: 24), + Text(cap, style: PangolinText.body.copyWith(color: c.fg2, fontWeight: FontWeight.w600)), + ]))), + // current server card + Padding( + padding: const EdgeInsets.fromLTRB(20, 0, 20, 18), + child: GestureDetector( + onTap: onOpenServers, + child: Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration(color: c.surface, borderRadius: BorderRadius.circular(PangolinRadius.lg), border: Border.all(color: c.border), boxShadow: PangolinShadow.sm), + child: Row(children: [ + CountryCode(code: server.code, active: true), + const SizedBox(width: 12), + Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Text(server.name, style: PangolinText.body.copyWith(color: c.fg1, fontWeight: FontWeight.w600)), + Text('${server.sub} · ${server.ping}ms', style: PangolinText.caption.copyWith(color: c.fg3, fontWeight: FontWeight.w400)), + ])), + Icon(PangolinIcons.chevronRight, size: 20, color: c.fg3), + ]), + ), + ), + ), + ]); + } +} + +/// ── Servers page ── +class _ServersPage extends StatelessWidget { + const _ServersPage({required this.zh, required this.current, required this.onPick}); + final bool zh; final String current; final ValueChanged onPick; + + static const _servers = [ + ServerInfo(code: 'HK', name: '香港 · 流媒体', sub: 'Hong Kong', ping: 18), + ServerInfo(code: 'JP', name: '日本 东京', sub: 'Tokyo', ping: 32), + ServerInfo(code: 'TW', name: '台湾 台北', sub: 'Taipei', ping: 28), + ServerInfo(code: 'SG', name: '新加坡', sub: 'Singapore · P2P', ping: 54), + ServerInfo(code: 'KR', name: '韩国 首尔', sub: 'Seoul', ping: 41), + ServerInfo(code: 'US', name: '美国 洛杉矶', sub: 'Los Angeles', ping: 146), + ]; + + @override + Widget build(BuildContext context) { + final c = context.pangolin; + return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + _TopBar(zh: zh), + Padding(padding: const EdgeInsets.fromLTRB(20, 0, 20, 12), + child: Text(zh ? '选择节点' : 'Choose server', style: PangolinText.h2.copyWith(color: c.fg1))), + Expanded(child: ListView.builder( + padding: const EdgeInsets.fromLTRB(20, 0, 20, 20), + itemCount: _servers.length, + itemBuilder: (_, i) => Padding( + padding: const EdgeInsets.only(bottom: 10), + child: ServerTile(server: _servers[i], active: _servers[i].code == current, onTap: () => onPick(_servers[i])), + ), + )), + ]); + } +} + +/// ── Stats page (skeleton) ── +class _StatsPage extends StatelessWidget { + const _StatsPage({required this.zh}); + final bool zh; + @override + Widget build(BuildContext context) { + final c = context.pangolin; + final vals = [2.1, 3.4, 1.8, 4.6, 5.2, 6.1, 3.0]; + final labels = zh ? ['一','二','三','四','五','六','日'] : ['M','T','W','T','F','S','S']; + const maxV = 6.1; + return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + _TopBar(zh: zh), + Padding(padding: const EdgeInsets.fromLTRB(20, 0, 20, 16), + child: Text(zh ? '使用统计' : 'Statistics', style: PangolinText.h2.copyWith(color: c.fg1))), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Row(children: [ + for (final m in [(zh ? '本月流量' : 'Traffic', '42.6', 'GB'), (zh ? '平均延迟' : 'Ping', '29', 'ms'), (zh ? '本月时长' : 'Time', '86.4', 'h')]) + Expanded(child: Container( + margin: const EdgeInsets.only(right: 12), + padding: const EdgeInsets.all(14), + decoration: BoxDecoration(color: c.surface, borderRadius: BorderRadius.circular(PangolinRadius.lg), border: Border.all(color: c.border), boxShadow: PangolinShadow.sm), + child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Text(m.$1, style: PangolinText.caption.copyWith(color: c.fg3, fontWeight: FontWeight.w600)), + const SizedBox(height: 6), + Text.rich(TextSpan(text: m.$2, style: PangolinText.mono.copyWith(fontSize: 20, color: c.fg1, fontWeight: FontWeight.w500), + children: [TextSpan(text: ' ${m.$3}', style: PangolinText.caption.copyWith(color: c.fg3))])), + ]), + )), + ]), + ), + const SizedBox(height: 16), + Expanded(child: Padding( + padding: const EdgeInsets.fromLTRB(20, 0, 20, 20), + child: Container( + width: double.infinity, + padding: const EdgeInsets.all(18), + decoration: BoxDecoration(color: c.surface, borderRadius: BorderRadius.circular(PangolinRadius.lg), border: Border.all(color: c.border), boxShadow: PangolinShadow.sm), + child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Text(zh ? '本周流量 (GB)' : 'This week (GB)', style: PangolinText.sm.copyWith(color: c.fg1, fontWeight: FontWeight.w600)), + const SizedBox(height: 18), + Expanded(child: Row(crossAxisAlignment: CrossAxisAlignment.end, children: [ + for (var i = 0; i < vals.length; i++) + Expanded(child: Column(mainAxisAlignment: MainAxisAlignment.end, children: [ + Text('${vals[i]}', style: PangolinText.caption.copyWith(fontFamily: PangolinFonts.mono, color: c.fg3)), + const SizedBox(height: 6), + Container(height: 90 * (vals[i] / maxV), margin: const EdgeInsets.symmetric(horizontal: 6), + decoration: BoxDecoration(color: c.accent.withOpacity(.85), borderRadius: const BorderRadius.vertical(top: Radius.circular(6)))), + const SizedBox(height: 6), + Text(labels[i], style: PangolinText.caption.copyWith(color: c.fg3)), + ])), + ])), + ]), + ), + )), + ]); + } +} + +/// ── Account page (skeleton — 套餐横幅 + 账户信息 + 设备入口) ── +class _AccountPage extends StatelessWidget { + const _AccountPage({required this.zh}); + final bool zh; + @override + Widget build(BuildContext context) { + final c = context.pangolin; + return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + _TopBar(zh: zh), + Padding(padding: const EdgeInsets.fromLTRB(20, 0, 20, 14), + child: Text(zh ? '我的' : 'Account', style: PangolinText.h2.copyWith(color: c.fg1))), + Expanded(child: ListView(padding: const EdgeInsets.fromLTRB(20, 0, 20, 20), children: [ + // plan banner + Container( + padding: const EdgeInsets.all(18), + decoration: BoxDecoration( + gradient: const LinearGradient(begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [PangolinColors.clay600, PangolinColors.clay800]), + borderRadius: BorderRadius.circular(PangolinRadius.xl), boxShadow: PangolinShadow.md, + ), + child: Row(children: [ + Container(width: 44, height: 44, decoration: BoxDecoration(color: PangolinColors.white.withOpacity(.18), shape: BoxShape.circle), + child: const Icon(PangolinIcons.crown, size: 22, color: PangolinColors.white)), + const SizedBox(width: 11), + Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + const Text('me@pangolin.vpn', style: TextStyle(color: PangolinColors.white, fontWeight: FontWeight.w700, fontSize: 16)), + const SizedBox(height: 4), + Text(zh ? 'PRO 会员 · 至 2026-12-31' : 'PRO · until 2026-12-31', style: TextStyle(color: PangolinColors.white.withOpacity(.85), fontSize: 12)), + ])), + FilledButton(onPressed: () => Navigator.of(context).push(MaterialPageRoute(builder: (_) => PlansScreen(zh: zh, onChoose: (_) => Navigator.of(context).push(MaterialPageRoute(builder: (_) => RedeemScreen(zh: zh)))))), style: FilledButton.styleFrom(backgroundColor: PangolinColors.white, foregroundColor: PangolinColors.clay700, shape: const StadiumBorder(), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10)), + child: Text(zh ? '续费 / 升级' : 'Renew', style: const TextStyle(fontWeight: FontWeight.w700, fontSize: 13))), + ]), + ), + const SizedBox(height: 18), + _accountRow(c, PangolinIcons.mail, zh ? '邮箱' : 'Email', 'me@pangolin.vpn'), + _accountRow(c, PangolinIcons.lock, zh ? '密码' : 'Password', '••••••••••'), + _accountRow(c, PangolinIcons.monitorSmartphone, zh ? '我的设备' : 'My devices', '', + onTap: () => Navigator.of(context).push(MaterialPageRoute(builder: (_) => DevicesScreen(zh: zh)))), + _accountRow(c, PangolinIcons.shoppingBag, zh ? '兑换 & 购买' : 'Redeem & buy', '', + onTap: () => Navigator.of(context).push(MaterialPageRoute(builder: (_) => RedeemScreen(zh: zh)))), + _accountRow(c, PangolinIcons.messageCircle, zh ? '联系我们' : 'Contact us', '', + onTap: () => Navigator.of(context).push(MaterialPageRoute(builder: (_) => ContactScreen(zh: zh)))), + _accountRow(c, PangolinIcons.logOut, zh ? '退出登录' : 'Sign out', '', danger: true), + ])), + ]); + } + + Widget _accountRow(PangolinScheme c, IconData icon, String title, String value, {bool danger = false, VoidCallback? onTap}) { + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: onTap, + child: Container( + margin: const EdgeInsets.only(bottom: 10), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), + decoration: BoxDecoration(color: c.surface, borderRadius: BorderRadius.circular(PangolinRadius.lg), border: Border.all(color: c.border), boxShadow: PangolinShadow.sm), + child: Row(children: [ + Icon(icon, size: 20, color: danger ? c.danger : c.accent), + const SizedBox(width: 13), + Expanded(child: Text(title, style: PangolinText.body.copyWith(color: danger ? c.danger : c.fg1, fontWeight: FontWeight.w500))), + if (value.isNotEmpty) Text(value, style: PangolinText.sm.copyWith(color: c.fg3)) + else Icon(PangolinIcons.chevronRight, size: 18, color: c.fg3), + ]), + ), + ); + } +} + +/// ── Shared top bar ── +class _TopBar extends StatelessWidget { + const _TopBar({required this.zh, this.trailing}); + final bool zh; final Widget? trailing; + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.fromLTRB(20, 6, 20, 14), + child: Row(children: [ + PangolinBrandLockup(zh: zh, markSize: 24), + const Spacer(), + if (trailing != null) trailing!, + ]), + ); + } +} diff --git a/design/flutter/widgets/onboarding_screen.dart b/design/flutter/widgets/onboarding_screen.dart new file mode 100644 index 0000000..207a44f --- /dev/null +++ b/design/flutter/widgets/onboarding_screen.dart @@ -0,0 +1,103 @@ +// onboarding_screen.dart — 首次引导页雏形(3 屏) +// 对应 React UI Kit 的 Onboarding / DOnboarding。 +import 'package:flutter/material.dart'; +import '../pangolin_theme.dart'; +import 'pangolin_icons.dart'; +import 'pangolin_button.dart'; + +class OnboardingStep { + const OnboardingStep({required this.icon, required this.title, required this.body, required this.cta, this.tint, this.tintBg}); + final IconData icon; + final String title, body, cta; + final Color? tint, tintBg; +} + +class OnboardingScreen extends StatefulWidget { + const OnboardingScreen({super.key, required this.onDone, this.steps}); + final VoidCallback onDone; + final List? steps; // 不传则用默认中文 3 屏 + + @override + State createState() => _OnboardingScreenState(); +} + +class _OnboardingScreenState extends State { + int _i = 0; + final _pager = PageController(); + + List _defaults(PangolinScheme c) => [ + OnboardingStep(icon: PangolinIcons.globe, title: '一键连接全球', body: '80+ 节点,智能选择最快线路,稳定不掉线。', cta: '下一步'), + OnboardingStep(icon: PangolinIcons.shield, title: '授权网络配置', body: '系统会请求添加一个网络配置,用于建立加密隧道。', cta: '允许并继续'), + OnboardingStep(icon: PangolinIcons.shieldCheck, title: '安全 · 无日志', body: '端到端加密,我们不记录你的任何浏览数据。', cta: '开始使用', + tint: c.success, tintBg: c.successSubtle), + ]; + + @override + void dispose() { _pager.dispose(); super.dispose(); } + + @override + Widget build(BuildContext context) { + final c = context.pangolin; + final steps = widget.steps ?? _defaults(c); + final last = _i == steps.length - 1; + + return Scaffold( + backgroundColor: c.bg, + body: SafeArea( + child: Column(children: [ + // skip + Align( + alignment: Alignment.centerRight, + child: Padding( + padding: const EdgeInsets.all(8), + child: TextButton(onPressed: widget.onDone, child: Text('跳过', style: PangolinText.sm.copyWith(color: c.fg3, fontWeight: FontWeight.w600))), + ), + ), + Expanded( + child: PageView.builder( + controller: _pager, + itemCount: steps.length, + onPageChanged: (v) => setState(() => _i = v), + itemBuilder: (_, idx) { + final s = steps[idx]; + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 32), + child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [ + Container( + width: 120, height: 120, + decoration: BoxDecoration(color: s.tintBg ?? c.accentSubtle, shape: BoxShape.circle), + child: Icon(s.icon, size: 52, color: s.tint ?? c.accent), + ), + const SizedBox(height: 28), + Text(s.title, textAlign: TextAlign.center, style: PangolinText.h1.copyWith(color: c.fg1, fontSize: 26)), + const SizedBox(height: 12), + Text(s.body, textAlign: TextAlign.center, style: PangolinText.body.copyWith(color: c.fg2, height: 1.6)), + ]), + ); + }, + ), + ), + // dots + Row(mainAxisAlignment: MainAxisAlignment.center, children: List.generate(steps.length, (k) { + final on = k == _i; + return AnimatedContainer( + duration: PangolinMotion.base, curve: PangolinMotion.easeOut, + width: on ? 22 : 7, height: 7, margin: const EdgeInsets.symmetric(horizontal: 4), + decoration: BoxDecoration(color: on ? c.accent : c.borderStrong, borderRadius: BorderRadius.circular(PangolinRadius.full)), + ); + })), + Padding( + padding: const EdgeInsets.fromLTRB(28, 22, 28, 26), + child: PangolinButton( + label: steps[_i].cta, expand: true, + onPressed: () { + if (last) { widget.onDone(); } + else { _pager.nextPage(duration: PangolinMotion.base, curve: PangolinMotion.easeOut); } + }, + ), + ), + ]), + ), + ); + } +} diff --git a/design/flutter/widgets/pangolin_button.dart b/design/flutter/widgets/pangolin_button.dart new file mode 100644 index 0000000..122ce2a --- /dev/null +++ b/design/flutter/widgets/pangolin_button.dart @@ -0,0 +1,67 @@ +// pangolin_button.dart — 胶囊按钮(primary / secondary / ghost / danger) +// 对应 React UI Kit 的 .btn / Pill 组件。 +import 'package:flutter/material.dart'; +import '../pangolin_theme.dart'; + +enum PangolinButtonVariant { primary, secondary, ghost, danger } + +class PangolinButton extends StatelessWidget { + const PangolinButton({ + super.key, + required this.label, + this.onPressed, + this.icon, + this.variant = PangolinButtonVariant.primary, + this.expand = false, + }); + + final String label; + final VoidCallback? onPressed; + final IconData? icon; + final PangolinButtonVariant variant; + final bool expand; + + @override + Widget build(BuildContext context) { + final c = context.pangolin; + final disabled = onPressed == null; + + late final Color bg, fg; + Border? border; + switch (variant) { + case PangolinButtonVariant.primary: + bg = c.accent; fg = c.fgOnAccent; break; + case PangolinButtonVariant.secondary: + bg = c.surface; fg = c.fg1; border = Border.all(color: c.borderStrong, width: 1.5); break; + case PangolinButtonVariant.ghost: + bg = Colors.transparent; fg = c.accent; break; + case PangolinButtonVariant.danger: + bg = c.dangerSubtle; fg = c.danger; break; + } + + final child = Row( + mainAxisSize: expand ? MainAxisSize.max : MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (icon != null) ...[Icon(icon, size: 18, color: fg), const SizedBox(width: 8)], + Text(label, style: PangolinText.sm.copyWith(color: fg, fontWeight: FontWeight.w600, fontSize: 15)), + ], + ); + + return Opacity( + opacity: disabled ? 0.45 : 1, + child: Material( + color: bg, + shape: StadiumBorder(side: border?.top ?? BorderSide.none), + clipBehavior: Clip.antiAlias, + child: InkWell( + onTap: onPressed, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 22, vertical: 13), + child: child, + ), + ), + ), + ); + } +} diff --git a/design/flutter/widgets/pangolin_icons.dart b/design/flutter/widgets/pangolin_icons.dart new file mode 100644 index 0000000..270c32a --- /dev/null +++ b/design/flutter/widgets/pangolin_icons.dart @@ -0,0 +1,85 @@ +// pangolin_icons.dart — Lucide 图标映射表 +// +// React UI Kit 用 Lucide 图标(按名引用,如 'power' / 'shield-check')。 +// 本文件把这些名映射到 Flutter 的 lucide_icons 包常量,保证与设计像素一致。 +// +// 依赖:在 pubspec.yaml 添加 +// dependencies: +// lucide_icons: ^0.x # https://pub.dev/packages/lucide_icons +// +// 用法 A(直用常量,推荐): +// Icon(PangolinIcons.power, size: 20, color: c.accent) +// 用法 B(按 Lucide 名查表,便于从 React data-lucide 移植): +// Icon(PangolinIcons.byName('shield-check')) +// +// ⚠️ lucide_icons 各版本常量名可能略有差异;若某常量在你的版本里不存在, +// 按 IDE 提示替换为最接近的名称即可(已在右侧注明 Lucide 原名)。 + +import 'package:flutter/widgets.dart'; +import 'package:lucide_icons/lucide_icons.dart'; + +class PangolinIcons { + PangolinIcons._(); + + // ── 连接 / 状态 ── + static const power = LucideIcons.power; // power — 连接键 + static const shieldCheck = LucideIcons.shieldCheck; // shield-check — 已加密 + static const shield = LucideIcons.shield; // shield — Kill Switch + static const loader = LucideIcons.loader; // loader-circle — 连接中(旋转) + static const zap = LucideIcons.zap; // zap — 延迟 / 极速 + static const globe = LucideIcons.globe; // globe — 节点 + + // ── 导航 / 操作 ── + static const settings = LucideIcons.settings; // settings + static const user = LucideIcons.user; // user — 账户 + static const search = LucideIcons.search; // search + static const check = LucideIcons.check; // check — 选中 + static const checkCircle = LucideIcons.checkCircle; // check-circle — 成功 + static const x = LucideIcons.x; // x — 关闭 + static const chevronRight = LucideIcons.chevronRight; // chevron-right + static const chevronDown = LucideIcons.chevronDown; // chevron-down + static const arrowLeft = LucideIcons.arrowLeft; // arrow-left — 返回 + static const arrowUp = LucideIcons.arrowUp; // arrow-up — 上传 + static const arrowDown = LucideIcons.arrowDown; // arrow-down — 下载 + static const refreshCw = LucideIcons.refreshCw; // refresh-cw — 检查更新 + static const compass = LucideIcons.compass; // compass — 引导 + static const barChart = LucideIcons.barChart; // chart-no-axes-column / chart — 统计 + static const mapPin = LucideIcons.mapPin; // map-pin + + // ── 主题 ── + static const moon = LucideIcons.moon; // moon — 深色 + static const sun = LucideIcons.sun; // sun — 浅色 + + // ── 账户 / 套餐 / 兑换 ── + static const crown = LucideIcons.crown; // crown — PRO + static const ticket = LucideIcons.ticket; // ticket — 兑换 / 续费 + static const creditCard = LucideIcons.creditCard; // credit-card + static const shoppingBag = LucideIcons.shoppingBag; // shopping-bag — 发卡商店 + static const mail = LucideIcons.mail; // mail — 邮箱 + static const lock = LucideIcons.lock; // lock — 密码 + static const logOut = LucideIcons.logOut; // log-out — 退出 + static const send = LucideIcons.send; // send — Telegram + static const messageCircle= LucideIcons.messageCircle; // message-circle — LINE / 联系 + static const externalLink = LucideIcons.externalLink; // external-link — 外链 + + // ── 设备 ── + static const laptop = LucideIcons.laptop; // laptop + static const smartphone = LucideIcons.smartphone; // smartphone + // monitor-smartphone 在部分版本缺失,回退到 smartphone(平板亦可用 LucideIcons.tablet) + static const monitorSmartphone = LucideIcons.smartphone; // monitor-smartphone(回退) + + /// 按 Lucide 原名查表(React data-lucide 名 → IconData)。未命中返回 null。 + static IconData? byName(String name) => _byName[name]; + + static const Map _byName = { + 'power': power, 'shield-check': shieldCheck, 'shield': shield, 'loader-circle': loader, + 'zap': zap, 'globe': globe, 'settings': settings, 'user': user, 'search': search, + 'check': check, 'check-circle': checkCircle, 'x': x, 'chevron-right': chevronRight, + 'chevron-down': chevronDown, 'arrow-left': arrowLeft, 'arrow-up': arrowUp, 'arrow-down': arrowDown, + 'refresh-cw': refreshCw, 'compass': compass, 'chart-no-axes-column': barChart, 'chart': barChart, + 'map-pin': mapPin, 'moon': moon, 'sun': sun, 'crown': crown, 'ticket': ticket, + 'credit-card': creditCard, 'shopping-bag': shoppingBag, 'mail': mail, 'lock': lock, + 'log-out': logOut, 'send': send, 'message-circle': messageCircle, 'external-link': externalLink, + 'laptop': laptop, 'smartphone': smartphone, 'monitor-smartphone': monitorSmartphone, + }; +} diff --git a/design/flutter/widgets/pangolin_logo.dart b/design/flutter/widgets/pangolin_logo.dart new file mode 100644 index 0000000..8bdb5b4 --- /dev/null +++ b/design/flutter/widgets/pangolin_logo.dart @@ -0,0 +1,74 @@ +// pangolin_logo.dart — 品牌标 / 字标 / App 图标(SVG) +// 用 flutter_svg 加载 assets/ 下的矢量 logo,自动适配明暗主题。 +// +// 依赖:pubspec.yaml +// dependencies: +// flutter_svg: ^2.0.0 +// flutter: +// assets: +// - assets/logo-mark.svg +// - assets/logo-mark-white.svg +// - assets/logo-wordmark.svg +// - assets/app-icon.svg +// +// 资产路径:把本目录上一级的 assets/*.svg 拷进你的 Flutter 工程 assets/。 +// 若放在其他包/前缀下,改 _base 即可。 +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import '../pangolin_theme.dart'; + +const String _base = 'assets'; + +/// 仅标记(行走穿山甲)。深色主题自动用反白版。 +class PangolinMark extends StatelessWidget { + const PangolinMark({super.key, this.size = 28, this.forceLight, this.forceDark}); + final double size; + final bool? forceLight; // 强制浅色锁版(clay 标) + final bool? forceDark; // 强制深色锁版(反白标) + + @override + Widget build(BuildContext context) { + final isDark = forceDark ?? (forceLight == true ? false : Theme.of(context).brightness == Brightness.dark); + final asset = isDark ? '$_base/logo-mark-white.svg' : '$_base/logo-mark.svg'; + return SvgPicture.asset(asset, width: size, height: size); + } +} + +/// 标记 + 字标(横版锁版)。深色下文字需反白时建议改用 PangolinMark + Text 自绘, +/// 因 wordmark.svg 内文字为深色;此处提供浅背景用法。 +class PangolinWordmark extends StatelessWidget { + const PangolinWordmark({super.key, this.height = 28}); + final double height; + @override + Widget build(BuildContext context) => SvgPicture.asset('$_base/logo-wordmark.svg', height: height); +} + +/// App 图标(clay 渐变圆角方 + 白色穿山甲)。圆角随尺寸。 +class PangolinAppIcon extends StatelessWidget { + const PangolinAppIcon({super.key, this.size = 64}); + final double size; + @override + Widget build(BuildContext context) { + return ClipRRect( + borderRadius: BorderRadius.circular(size * 0.22), + child: SvgPicture.asset('$_base/app-icon.svg', width: size, height: size), + ); + } +} + +/// 顶栏品牌锁版:标记 + 「穿山甲 / Pangolin」字样(用本地字体,随主题变色)。 +/// 比直接用 wordmark.svg 更灵活(可双语 + 深浅自适应)。 +class PangolinBrandLockup extends StatelessWidget { + const PangolinBrandLockup({super.key, this.zh = true, this.markSize = 26}); + final bool zh; final double markSize; + @override + Widget build(BuildContext context) { + final c = context.pangolin; + return Row(mainAxisSize: MainAxisSize.min, children: [ + PangolinMark(size: markSize), + const SizedBox(width: 9), + Text(zh ? '穿山甲' : 'Pangolin', + style: PangolinText.h3.copyWith(color: c.fg1, fontFamily: PangolinFonts.display, fontWeight: FontWeight.w700)), + ]); + } +} diff --git a/design/flutter/widgets/pangolin_widgets.dart b/design/flutter/widgets/pangolin_widgets.dart new file mode 100644 index 0000000..0d1b62b --- /dev/null +++ b/design/flutter/widgets/pangolin_widgets.dart @@ -0,0 +1,14 @@ +// pangolin_widgets.dart — 统一导出穿山甲 VPN 核心组件雏形。 +// 用法:import 'widgets/pangolin_widgets.dart'; +export 'pangolin_icons.dart'; +export 'pangolin_button.dart'; +export 'country_code.dart'; +export 'status_pill.dart'; +export 'connect_button.dart'; +export 'server_tile.dart'; +export 'plan_card.dart'; +export 'auth_screen.dart'; +export 'onboarding_screen.dart'; +export 'home_shell.dart'; +export 'account_screens.dart'; +export 'pangolin_logo.dart'; diff --git a/design/flutter/widgets/plan_card.dart b/design/flutter/widgets/plan_card.dart new file mode 100644 index 0000000..b61283a --- /dev/null +++ b/design/flutter/widgets/plan_card.dart @@ -0,0 +1,112 @@ +// plan_card.dart — 套餐卡(免费 / 专业渐变高亮 / 团队) +// 对应 React UI Kit 的 PlanCard / PlansScreen。 +import 'package:flutter/material.dart'; +import '../pangolin_theme.dart'; +import 'pangolin_icons.dart'; + +class PlanCard extends StatelessWidget { + const PlanCard({ + super.key, + required this.name, + required this.price, + required this.period, + required this.features, + required this.ctaLabel, + this.featured = false, + this.isCurrent = false, + this.popularLabel, + this.onPressed, + }); + + final String name, price, period, ctaLabel; + final List features; + final bool featured, isCurrent; + final String? popularLabel; + final VoidCallback? onPressed; + + @override + Widget build(BuildContext context) { + final c = context.pangolin; + final onCard = featured ? PangolinColors.white : c.fg1; + final subColor = featured ? PangolinColors.white.withOpacity(0.92) : c.fg2; + + final card = Container( + padding: const EdgeInsets.fromLTRB(20, 22, 20, 20), + decoration: BoxDecoration( + gradient: featured + ? const LinearGradient(begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [PangolinColors.clay600, PangolinColors.clay800]) + : null, + color: featured ? null : c.surface, + border: featured ? null : Border.all(color: c.border), + borderRadius: BorderRadius.circular(PangolinRadius.xl), + boxShadow: featured ? PangolinShadow.md : PangolinShadow.sm, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text(name, style: PangolinText.sm.copyWith(color: featured ? PangolinColors.white.withOpacity(0.85) : c.fg2, fontWeight: FontWeight.w600)), + const SizedBox(height: 6), + Row( + crossAxisAlignment: CrossAxisAlignment.baseline, + textBaseline: TextBaseline.alphabetic, + children: [ + Text(price, style: PangolinText.display.copyWith(color: onCard, fontSize: 30)), + Text(period, style: PangolinText.caption.copyWith(color: subColor, fontSize: 13)), + ], + ), + const SizedBox(height: 16), + ...features.map((f) => Padding( + padding: const EdgeInsets.only(bottom: 9), + child: Row(children: [ + Icon(PangolinIcons.check, size: 15, color: featured ? PangolinColors.white : c.success), + const SizedBox(width: 9), + Expanded(child: Text(f, style: PangolinText.sm.copyWith(color: subColor, fontSize: 13))), + ]), + )), + const SizedBox(height: 8), + SizedBox( + width: double.infinity, + child: Material( + color: featured ? PangolinColors.white : (isCurrent ? Colors.transparent : c.accent), + shape: StadiumBorder(side: isCurrent ? BorderSide(color: c.borderStrong, width: 1.5) : BorderSide.none), + clipBehavior: Clip.antiAlias, + child: InkWell( + onTap: isCurrent ? null : onPressed, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 12), + child: Center( + child: Text(ctaLabel, + style: PangolinText.sm.copyWith( + fontWeight: FontWeight.w700, + fontSize: 14, + color: featured ? PangolinColors.clay700 : (isCurrent ? c.fg2 : PangolinColors.white), + )), + ), + ), + ), + ), + ), + ], + ), + ); + + if (popularLabel == null) return card; + // 带「最受欢迎」浮标 + return Stack(clipBehavior: Clip.none, children: [ + card, + Positioned( + top: -10, + left: 0, + right: 0, + child: Center( + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 11, vertical: 3), + decoration: BoxDecoration(color: PangolinColors.white, borderRadius: BorderRadius.circular(PangolinRadius.full), boxShadow: PangolinShadow.sm), + child: Text(popularLabel!, style: PangolinText.caption.copyWith(color: PangolinColors.clay700, fontWeight: FontWeight.w700, fontSize: 11)), + ), + ), + ), + ]); + } +} diff --git a/design/flutter/widgets/server_tile.dart b/design/flutter/widgets/server_tile.dart new file mode 100644 index 0000000..e600e16 --- /dev/null +++ b/design/flutter/widgets/server_tile.dart @@ -0,0 +1,51 @@ +// server_tile.dart — 服务器列表行(国家码 · 名称 · 延迟 · 信号 · 选中态) +// 对应 React UI Kit 的 ServerRow。 +import 'package:flutter/material.dart'; +import '../pangolin_theme.dart'; +import 'country_code.dart'; +import 'pangolin_icons.dart'; + +class ServerInfo { + const ServerInfo({required this.code, required this.name, required this.sub, required this.ping}); + final String code, name, sub; + final int ping; +} + +class ServerTile extends StatelessWidget { + const ServerTile({super.key, required this.server, this.active = false, this.onTap}); + final ServerInfo server; + final bool active; + final VoidCallback? onTap; + + @override + Widget build(BuildContext context) { + final c = context.pangolin; + return InkWell( + onTap: onTap, + child: Container( + color: active ? c.accentSubtle : Colors.transparent, + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), + child: Row( + children: [ + CountryCode(code: server.code, active: active), + const SizedBox(width: 13), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(server.name, style: PangolinText.sm.copyWith(color: c.fg1, fontWeight: FontWeight.w600, fontSize: 15)), + const SizedBox(height: 2), + Text(server.sub, style: PangolinText.caption.copyWith(color: c.fg3, fontWeight: FontWeight.w400)), + ], + ), + ), + Text('${server.ping}ms', style: PangolinText.mono.copyWith(color: c.fg2, fontSize: 12)), + const SizedBox(width: 8), + SignalBars(ping: server.ping), + if (active) ...[const SizedBox(width: 10), Icon(PangolinIcons.check, size: 18, color: c.accent)], + ], + ), + ), + ); + } +} diff --git a/design/flutter/widgets/status_pill.dart b/design/flutter/widgets/status_pill.dart new file mode 100644 index 0000000..2365944 --- /dev/null +++ b/design/flutter/widgets/status_pill.dart @@ -0,0 +1,45 @@ +// status_pill.dart — 状态胶囊(色点 + 文字)。不用 emoji。 +// 对应 React UI Kit 的 .pill / 徽章。 +import 'package:flutter/material.dart'; +import '../pangolin_theme.dart'; + +enum PangolinStatus { connected, connecting, error, neutral, pro } + +class StatusPill extends StatelessWidget { + const StatusPill({super.key, required this.label, this.status = PangolinStatus.neutral}); + final String label; + final PangolinStatus status; + + @override + Widget build(BuildContext context) { + final c = context.pangolin; + late final Color bg, fg, dot; + bool showDot = true; + switch (status) { + case PangolinStatus.connected: + bg = c.successSubtle; fg = c.success; dot = c.success; break; + case PangolinStatus.connecting: + bg = c.warningSubtle; fg = c.warning; dot = c.warning; break; + case PangolinStatus.error: + bg = c.dangerSubtle; fg = c.danger; dot = c.danger; break; + case PangolinStatus.pro: + bg = c.accent; fg = c.fgOnAccent; dot = c.fgOnAccent; showDot = false; break; + case PangolinStatus.neutral: + bg = c.bgSubtle; fg = c.fg2; dot = c.fg3; break; + } + return Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration(color: bg, borderRadius: BorderRadius.circular(PangolinRadius.full)), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (showDot) ...[ + Container(width: 7, height: 7, decoration: BoxDecoration(color: dot, shape: BoxShape.circle)), + const SizedBox(width: 7), + ], + Text(label, style: PangolinText.caption.copyWith(color: fg, fontWeight: FontWeight.w600)), + ], + ), + ); + } +} diff --git a/design/preview/brand-app-icon.html b/design/preview/brand-app-icon.html new file mode 100644 index 0000000..e712253 --- /dev/null +++ b/design/preview/brand-app-icon.html @@ -0,0 +1,45 @@ + + + +
APP ICON · 行走穿山甲(纹路 + 眼睛,与 Logo 一致;40/28 仅纹路) — 最终锁版
+ + + + + +
+
+
主图标 104px
+
+ +
+
64 · 纹路+眼睛
+
40 · 仅纹路
+
28 · 仅纹路
+
+ + + diff --git a/design/preview/brand-logo.html b/design/preview/brand-logo.html new file mode 100644 index 0000000..ef61c6c --- /dev/null +++ b/design/preview/brand-logo.html @@ -0,0 +1,19 @@ + + + +
+
穿山甲PANGOLIN
+ 主锁版 · 浅色背景 +
+
+
穿山甲PANGOLIN
+ 反白锁版 · 深色背景 +
+ diff --git a/design/preview/color-clay.html b/design/preview/color-clay.html new file mode 100644 index 0000000..1646a73 --- /dev/null +++ b/design/preview/color-clay.html @@ -0,0 +1,24 @@ + + + +
+
50
+
100
+
200
+
300
+
400
+
500 ●
+
600
+
700
+
800
+
900
+
+
Clay / Copper · 主色 · 穿山甲鳞甲● 500 = #B96A3D primary
+ diff --git a/design/preview/color-dark-surfaces.html b/design/preview/color-dark-surfaces.html new file mode 100644 index 0000000..b7194b9 --- /dev/null +++ b/design/preview/color-dark-surfaces.html @@ -0,0 +1,19 @@ + + + +
DARK THEME · 暗色主题 — 暖 espresso 表面层级
+
+
bg
应用画布
#14110E
+
surface
卡片 / 面板
#221E19
+
surface-2
嵌套层
#2A251F
Accent
+
+ diff --git a/design/preview/color-sand.html b/design/preview/color-sand.html new file mode 100644 index 0000000..6eb1988 --- /dev/null +++ b/design/preview/color-sand.html @@ -0,0 +1,24 @@ + + + +
+
50
+
100
+
200
+
300
+
400
+
500
+
600
+
700
+
800
+
900
+
950
+
+
Sand / Taupe · 暖中性 · 文本与表面950 = 暗色画布 espresso
+ diff --git a/design/preview/color-semantic.html b/design/preview/color-semantic.html new file mode 100644 index 0000000..d5fc4de --- /dev/null +++ b/design/preview/color-semantic.html @@ -0,0 +1,25 @@ + + + +
+
已连接 Connected
+
subtle
+
success · #5B8C5A
+
+
+
连接中 Connecting
+
subtle
+
warning · #D69A3C
+
+
+
已断开 Error
+
subtle
+
danger · #C0533B
+
+ diff --git a/design/preview/comp-badges.html b/design/preview/comp-badges.html new file mode 100644 index 0000000..4864b28 --- /dev/null +++ b/design/preview/comp-badges.html @@ -0,0 +1,22 @@ + + + +
BADGES / STATUS · 状态胶囊 — 连接态 · 套餐 · 节点属性
+已连接 Connected +连接中 Connecting +已断开 Disconnected +未登录 +PRO 会员 +流媒体优化 +P2P +延迟 24ms + diff --git a/design/preview/comp-buttons.html b/design/preview/comp-buttons.html new file mode 100644 index 0000000..c7c9620 --- /dev/null +++ b/design/preview/comp-buttons.html @@ -0,0 +1,27 @@ + + + + +
BUTTONS · 全圆角 (full) · hover 加深 · press 收缩
+
+ + + + + + +
+ + diff --git a/design/preview/comp-connect.html b/design/preview/comp-connect.html new file mode 100644 index 0000000..485253a --- /dev/null +++ b/design/preview/comp-connect.html @@ -0,0 +1,52 @@ + + + + +
CONNECT BUTTON · 核心连接键 — 轨道环 + 圆内状态,三态:断开 / 连接中 / 已连接
+ +
+ + 未连接 Off +
+ +
+ + 建立隧道 Connecting +
+ +
+ + 已连接 Connected +
+ + + diff --git a/design/preview/comp-inputs.html b/design/preview/comp-inputs.html new file mode 100644 index 0000000..bec3c8e --- /dev/null +++ b/design/preview/comp-inputs.html @@ -0,0 +1,26 @@ + + + +
FORM · 输入框 (focus 显示 clay 光环) · 开关
+
+
+
+
+
开机自启
+
Kill Switch
+
+
+ diff --git a/design/preview/comp-plan-card.html b/design/preview/comp-plan-card.html new file mode 100644 index 0000000..43faa25 --- /dev/null +++ b/design/preview/comp-plan-card.html @@ -0,0 +1,24 @@ + + + + +
PLAN CARD · 套餐卡 — 普通 / PRO(clay 渐变)
+
免费版 Free
¥0/月
+
3 个基础节点
每日 1GB 流量
+
+
PRO 会员
¥25/月
+
80+ 全球节点
无限流量 · 极速
5 台设备同时在线
+
+ + diff --git a/design/preview/comp-server-row.html b/design/preview/comp-server-row.html new file mode 100644 index 0000000..9a6c2aa --- /dev/null +++ b/design/preview/comp-server-row.html @@ -0,0 +1,25 @@ + + + + +
SERVER ROW · 服务器列表 — 国家码 · 延迟 · 信号 · 选中态
+
+
HK
香港 · 流媒体
Hong Kong · 1.2× 倍率
18ms
+
JP
日本 东京
Tokyo
32ms
+
SG
新加坡
Singapore · P2P
54ms
+
US
美国 洛杉矶
Los Angeles
146ms
+
+ + diff --git a/design/preview/radii.html b/design/preview/radii.html new file mode 100644 index 0000000..92c1b2f --- /dev/null +++ b/design/preview/radii.html @@ -0,0 +1,18 @@ + + + +
RADII · 圆角 — 偏大 = 亲和友好
+
+
sm 6
+
md 10
+
lg 14
+
xl 20
+
2xl 28
+
full
+
+ diff --git a/design/preview/shadows.html b/design/preview/shadows.html new file mode 100644 index 0000000..d8853bc --- /dev/null +++ b/design/preview/shadows.html @@ -0,0 +1,16 @@ + + + +
SHADOWS · 暖色调投影 (rgba 45,30,20) — 柔和、克制
+
+
sm
+
md
+
lg
+
xl
+
+ diff --git a/design/preview/spacing.html b/design/preview/spacing.html new file mode 100644 index 0000000..9eaef63 --- /dev/null +++ b/design/preview/spacing.html @@ -0,0 +1,17 @@ + + + +
SPACING · 4px 基准网格
+
1 · 4px
+
2 · 8px
+
3 · 12px
+
4 · 16px
+
6 · 24px
+
8 · 32px
+
12 · 48px
+
16 · 64px
+ diff --git a/design/preview/type-body.html b/design/preview/type-body.html new file mode 100644 index 0000000..0dd3555 --- /dev/null +++ b/design/preview/type-body.html @@ -0,0 +1,11 @@ + + + +
BODY · Manrope — 界面与正文 · 400 / 500 / 600 / 700
+

Connect with one tap 一键连接

+

A lightweight, friendly VPN that just works. 轻盈、亲和、即开即用。

+

Body 16px — 全球 80+ 节点,智能选择最快线路,稳定不掉线。

+

Small 14px — 适用于次要说明、列表副标题与表单提示文字。

+ diff --git a/design/preview/type-cjk.html b/design/preview/type-cjk.html new file mode 100644 index 0000000..3ddc120 --- /dev/null +++ b/design/preview/type-cjk.html @@ -0,0 +1,12 @@ + + + +
CJK · Noto Sans SC — 中文专用 · 400 / 500 / 700
+

穿山甲 · 守护你的每一次连接

+

无需配置,打开即用。智能分流,海外应用秒开。

+

条款与隐私 · 我们不记录你的浏览数据 · 端到端加密

+ diff --git a/design/preview/type-display.html b/design/preview/type-display.html new file mode 100644 index 0000000..67eed27 --- /dev/null +++ b/design/preview/type-display.html @@ -0,0 +1,9 @@ + + + +
DISPLAY · Sora — 标题与营销大字 · weights 600 / 700
+

极速畅连 Smooth

+

极速 · 稳定 · 省心

+ diff --git a/design/preview/type-mono.html b/design/preview/type-mono.html new file mode 100644 index 0000000..27eed70 --- /dev/null +++ b/design/preview/type-mono.html @@ -0,0 +1,18 @@ + + + +
MONO · JetBrains Mono — 数据读数 · tabular nums · IP / 速率 / 密钥
+
+
DOWNLOAD
86.4 Mb/s
+
PING
24 ms
+
UPTIME
02:14:08
+
EXIT IP
103.27.18.204
+
PROTOCOL
WireGuard®
+
DATA
1.82 GB
+
+ diff --git a/design/preview/type-scale.html b/design/preview/type-scale.html new file mode 100644 index 0000000..c6d3de6 --- /dev/null +++ b/design/preview/type-scale.html @@ -0,0 +1,16 @@ + + + +
TYPE SCALE · 48 / 36 / 30 / 24 / 20 / 18 / 16 / 14 / 12
+
display 48

穿山甲 Pangolin

+
h1 30

一键连接全球

+
h2 24

选择服务器节点

+
h3 20

智能分流规则

+
body 16

默认正文,清晰易读。

+
sm 14

次要说明文字

+
overline 12

ACCOUNT · 账户

+ diff --git a/design/scraps/sketch-2026-06-10T15-35-49-rnni6r.napkin b/design/scraps/sketch-2026-06-10T15-35-49-rnni6r.napkin new file mode 100644 index 0000000..0ad71aa --- /dev/null +++ b/design/scraps/sketch-2026-06-10T15-35-49-rnni6r.napkin @@ -0,0 +1,6 @@ +{ + "version": 1, + "created": "2026-06-10T15:35:49.776Z", + "modified": "2026-06-10T15:35:49.776Z", + "objects": [] +} \ No newline at end of file diff --git a/design/server/ARCHITECTURE.md b/design/server/ARCHITECTURE.md new file mode 100644 index 0000000..6da9312 --- /dev/null +++ b/design/server/ARCHITECTURE.md @@ -0,0 +1,141 @@ +# 穿山甲 · 后端架构设计(初稿 v0.1) + +> 本文档是给 Claude Code 的实现蓝本:先审阅 → 修订 → 再按模块实现。 +> 产品语境见 `../CLAUDE.md`(设计系统)。脱敏规则同样适用于日志、报错文案与对外文档。 + +--- + +## 0. 设计原则 +1. **控制面 / 数据面彻底分离**:API 服务器只管账户、套餐、节点目录、兑换;用户流量只走加速节点(WireGuard),绝不经过 API。 +2. **App 内无支付**:资金流全部外部化(发卡店/TG/LINE/邮箱)→ 后端只做「激活码」的生成、分发对账与兑换。 +3. **节点可秒级灰度**:节点列表带版本,被封节点能即时下架;客户端缓存最后一份可用列表兜底。 +4. **最小数据原则(无日志承诺)**:不记录浏览内容/目的地;只保留计费与风控所需的最小元数据(用量字节数、设备数、最后活跃时间),并在文档中公开口径。 + +--- + +## 1. 系统拓扑 + +``` +┌──────────┐ HTTPS ┌─────────────────┐ +│ Flutter │ ───────▶ │ API (控制面) │── Postgres / Redis +│ 客户端 │ │ Go + chi/gin │ +└────┬─────┘ └───────┬─────────┘ + │ WireGuard UDP │ 节点注册/心跳 (mTLS gRPC) +┌────▼─────────────┐ ┌───────▼─────────┐ +│ 加速节点 (数据面) │ │ 发卡店 Webhook │ +│ wireguard + agent │ │ (激活码入库) │ +└──────────────────┘ └─────────────────┘ +``` + +- **API**:Go(推荐,wgctrl/生态成熟)。单体起步,按模块分包,未来可拆。 +- **节点 agent**:每台加速节点跑一个轻量 agent:注册、心跳、上报负载、下发/回收 WireGuard peer。与 API 间用 mTLS gRPC。 +- **存储**:Postgres(主数据)+ Redis(验证码、限流、节点实时负载)。 + +--- + +## 2. 数据模型(核心表) + +```sql +users (id, email UNIQUE, pw_hash, created_at, status) +devices (id, user_id FK, name, platform, pubkey, last_seen, created_at) +plans (id, code['free'|'pro'|'team'], max_devices, max_routes, + daily_minutes NULL=无限, ad_gate bool) -- free: 1 节点/10 分钟/须看广告 +subscriptions (id, user_id FK, plan_id FK, expires_at, source['trial'|'code'], created_at) +codes (id, code UNIQUE, plan_id, duration_days, batch_id, channel, + status['unused'|'redeemed'|'void'], redeemed_by NULL, redeemed_at NULL) +code_batches (id, channel['store'|'tg'|'line'|'manual'], created_by, note, created_at) +nodes (id, region, name_zh, name_en, endpoint, pubkey, tags[], tier['free'|'pro'], + status['up'|'down'|'draining'], weight) +usage_daily (user_id, date, bytes_up, bytes_down, minutes_used, ad_unlocked_at NULL) + -- 仅字节数/分钟数,无目的地;ad_unlocked_at = 当日激励视频解锁时刻(免费版) +audit_log (id, actor, action, target, meta jsonb, at) -- 兑换/封禁/节点操作必记 +``` + +要点: +- 试用 = 注册时自动插入 `subscriptions(plan=pro, expires_at=now()+7d, source='trial')`,一邮箱仅一次(套餐口径见 `../CLAUDE.md` §7)。 +- 体验期后回落 free:仅 1 个基础节点(nodes.tier='free')、每日 10 分钟;每日首次连接前需激励视频解锁(客户端上报 → `POST /v1/ads/unlock` 记 `ad_unlocked_at`,connect 接口校验)。 +- `codes.code` 用 Crockford Base32,16 位,带校验位;明文只出现一次(生成响应/发卡店),库里可存 hash。 + +--- + +## 3. API 契约(v1 摘要,正式版用 OpenAPI 定义) + +``` +POST /v1/auth/code {email} → 发 6 位验证码(Redis, 10min, 限 1/min) +POST /v1/auth/register {email, code, password} → 创建账户 + 7 天试用 + JWT +POST /v1/auth/login {email, password} → JWT (access 15min + refresh 30d) +POST /v1/auth/refresh {refresh_token} + +GET /v1/me → 账户 + 订阅 + 用量摘要 +GET /v1/me/devices → 设备列表 +DELETE /v1/me/devices/:id → 移除设备(回收节点 peer) + +POST /v1/redeem {code} → 兑换激活码(幂等,审计) +POST /v1/ads/unlock {device_id, ad_token} → 免费版激励视频解锁当日时长(验广告 SDK 回执) +GET /v1/plans → 套餐目录(给客户端展示) + +GET /v1/nodes ?if_version=N → 节点目录(按套餐过滤, 304 支持) +POST /v1/nodes/:id/connect {device_pubkey} → 下发 WireGuard 配置(peer 注册) +POST /v1/nodes/:id/disconnect + +GET /v1/usage ?days=7 → 用量曲线(统计页) +``` + +约定:错误体 `{code, message_zh, message_en}`;全部接口 JWT(除 auth);限流用 Redis 滑窗。 + +--- + +## 4. 关键流程 + +### 4.1 注册(对齐客户端 UI:邮箱 → 验证码 → 设密码) +1. `POST /auth/code`:风控(IP+邮箱限频、一次性邮箱域黑名单)→ 发码。 +2. `POST /auth/register`:验码 → 建号 → **自动开 7 天 PRO 试用** → 返回 JWT。 + +### 4.2 激活码生命周期 +``` +发卡店售出 → webhook(带签名) → codes 入库(status=unused, channel=store) + 人工渠道(TG/LINE/邮箱) → 管理端批量生成 batch → 导出给客服 +客户端 POST /redeem → 校验+事务:code→redeemed, 订阅顺延(叠加而非覆盖) → audit_log +``` +- 兑换幂等:同一用户重复提交同一码返回首次结果。 +- 风控:单用户兑换失败 5 次/小时锁 1 小时。 + +### 4.3 连接(数据面) +1. 客户端拉 `/nodes`(带套餐过滤 + 客户端测延迟自选)。 +2. `POST /nodes/:id/connect`:API 校验订阅与设备数(免费版另校验当日 `ad_unlocked_at` 与剩余分钟)→ 通过 gRPC 让节点 agent 添加 peer → 返回 WireGuard 配置(endpoint、server_pubkey、分配的内网 IP、DNS)。免费版 peer TTL = 剩余分钟数,到时 agent 自动回收。 +3. 心跳/续期:peer 默认 TTL 24h,客户端在线自动续;订阅过期或设备被移除 → agent 回收 peer。 +4. 节点 agent 每 30s 上报负载(在线 peer 数、带宽)→ Redis,用于目录排序与"信号条"。 + +### 4.4 节点目录灰度 +- `nodes` 任何变更 bump 全局 version;客户端带 `if_version` 轮询(或推送)。 +- 节点被封:status=down 立即从目录消失;`draining` 用于计划下线(不接新连接)。 + +--- + +## 5. 安全与合规 +- 密码 argon2id;JWT RS256,密钥轮换;全站 TLS1.3。 +- 验证码与兑换接口必须有人机/限流防刷;注册防一次性邮箱。 +- **无日志口径**(写进隐私政策并据实执行):不记录目的地址/DNS 查询/流量内容;仅 `usage_daily` 字节数。 +- 管理端(内部)单独服务 + IP 白名单 + 双因素;一切敏感操作进 audit_log。 +- 节点服务器上不落用户身份明文,peer 只对应 device_id。 + +--- + +## 6. 部署与运维(起步规模) +- API:单区域 2 实例 + LB;Postgres 托管(每日备份);Redis 托管。 +- 节点:按地区购 VPS(HK/JP/SG/US…),Ansible/脚本一键装 agent + WireGuard;出口 IP 池可轮换。 +- 监控:节点心跳缺失告警、兑换失败率、注册转化漏斗。 +- CI:GitHub Actions——lint + 单测 + OpenAPI 校验 + docker 镜像。 + +--- + +## 7. 给 Claude Code 的实现顺序 +1. `openapi.yaml`(把 §3 展开成正式契约)+ 数据库 migration +2. **auth 模块**(验证码/注册/登录/JWT)+ 测试 +3. **codes 模块**(批量生成/webhook/兑换)+ 审计 +4. devices + 订阅校验中间件 +5. nodes 目录 + agent gRPC 协议(proto 文件)+ connect/disconnect +6. usage 统计 + 管理端最小后台 +7. Flutter 端接 API(替换 UI Kit 演示数据;UI 规范见 design/) + +> 每个模块完成的定义:有单测、有 OpenAPI 同步、错误文案双语且符合脱敏口径。 diff --git a/design/ui_kits/desktop/README.md b/design/ui_kits/desktop/README.md new file mode 100644 index 0000000..6519d27 --- /dev/null +++ b/design/ui_kits/desktop/README.md @@ -0,0 +1,30 @@ +# 穿山甲 VPN — 桌面客户端 UI Kit + +macOS / Windows 桌面客户端的高保真原型。920×600 窗口,侧栏导航 + 内容区,支持深浅主题与中英切换。 + +## 运行 +打开 `index.html`。窗口 chrome(交通灯)来自 `macos-window.jsx` 启动组件。完整流程:**登录/注册 → 首次引导 → 主应用**。可交互: +- **登录 / 注册**(`DAuthFlow`):分屏式,左侧 clay 品牌面板 + 右侧表单。登录用邮箱 + 密码;注册走「邮箱 → 发送验证码 → 填验证码 → 设置密码」。 +- **首次引导**(`DOnboarding`):窗口卡片,3 屏(连接全球 → 授权 VPN 配置 → 安全无日志),可跳过。 +- **侧栏导航**:连接 / 节点 / 统计 / **账户** / **联系我们** / 设置;底部 PRO 账户卡。 +- **连接键**:三态(断开 / 连接中 / 已连接 + 计时 + 速率)。 +- **节点**:双列网格,搜索 + 点选切换。 +- **统计**:流量 / 延迟 / 时长指标卡 + 本周流量柱状图。 +- **账户**(集成后台):套餐横幅(续费/升级)、设备管理(可移除)、套餐选择(免费/专业/团队)→ 进入**兑换 & 购买**(激活码 + 渠道)。 +- **联系我们**(`DContactView`):Telegram / LINE / 邮箱 / 发卡商店 + 服务时间 + FAQ。 +- **设置**:兑换入口、开机自启 / 智能分流 / Kill Switch / 语言切换 / 深色外观 / 协议 / 版本 / 查看登录流程。 +- 顶栏右侧:连接状态 + 明暗快捷切换。 + +## 文件 +| 文件 | 内容 | +|---|---| +| `index.html` | 挂载 `DesktopApp` | +| `dparts.jsx` | 原子:`DIcon` / `DMark`(穿山甲标)/ `DCC` / `DSignal`;`DSTRINGS` i18n 字典、`dT/dName/dSub`、`DSERVERS` | +| `dflows.jsx` | `DAuthFlow`(登录/注册)/ `DOnboarding`(3 屏引导)/ `DAccountView`(集成后台)/ `DRedeemView`(兑换+购买)/ `DContactView`(联系我们) | +| `dapp.jsx` | `DesktopApp` 状态机(stage: app/auth/onboarding)+ `DConnect/DServers/DStats/DSettings/NavItem/DToggle/DLangSwitch` | +| `macos-window.jsx` | macOS 窗口 chrome 启动组件(此 Kit 仅用其 `MacTrafficLights`) | + +## 关键模式 +- 与移动端共用同一套语义 token 与穿山甲品牌标,保证跨端一致。 +- 语言为单选(中文**或** EN),通过设置里的段控切换 —— 反映真实产品而非中英并排。 +- Windows 版本仅交通灯换成 Windows 控制按钮即可复用全部视图。 diff --git a/design/ui_kits/desktop/dapp.jsx b/design/ui_kits/desktop/dapp.jsx new file mode 100644 index 0000000..5fa9e4e --- /dev/null +++ b/design/ui_kits/desktop/dapp.jsx @@ -0,0 +1,278 @@ +/* dapp.jsx — 穿山甲 VPN desktop · window + views (i18n) */ +const { useState, useEffect, useRef } = React; + +function DToggle({ on, onChange }) { + return
+
; +} +function DLangSwitch({ lang, setLang }) { + return
+ {[['zh','中文'],['en','EN']].map(([v,l])=>( + + ))}
; +} +function NavItem({ icon, label, active, onClick }) { + return ( + + ); +} + +function DConnect({ t, lang, status, onToggle, server, smart, free, mins, adDone, onAd, elapsed }) { + const map = { + off:{ fill:'var(--sand-100)', fg:'var(--clay-500)', icon:'power', cap:t('capOff'), sub:t('subOff'), ring:'track' }, + connecting:{ fill:'var(--accent)', fg:'#fff', icon:'loader-circle', cap:t('capConnecting'), sub:t('subConnecting'), ring:'spin' }, + on:{ fill:'var(--success)', fg:'#fff', icon:'shield-check', cap:t('capOn'), sub:t('subOn'), ring:'full' }, + }[status]; + const fmt = s => `${String(Math.floor(s/3600)).padStart(2,'0')}:${String(Math.floor(s%3600/60)).padStart(2,'0')}:${String(s%60).padStart(2,'0')}`; + const shadow = status==='off' ? 'inset 0 0 0 1px var(--sand-200), var(--shadow-md)' : `0 0 0 8px ${status==='on'?'var(--success-subtle)':'var(--accent-subtle)'}, var(--shadow-lg)`; + return ( +
+ +
+
{map.cap}
+
{map.sub}
+
+ {smart && } + {smart ? `${t('smartSelect')} · ` : ''}{dName(server,lang)} · {server.ping}ms +
+
+ {free && ( +
+
+ + {t('quotaToday')} + {mins} {t('minutes')} + {t('quotaFree')} +
+
+
+
+ {adDone ? ( +
+ {t('adUnlocked')} +
+ ) : ( + + )} +
+ )} + {status==='on' && +
+ {[['arrow-down','86.4','Mb/s'],['arrow-up','12.1','Mb/s'],['zap',String(server.ping),'ms']].map(([ic,v,u])=>( +
+ {v} {u} +
+ ))} +
} +
+ ); +} + +function DServers({ t, lang, current, onPick }) { + const [q,setQ]=useState(''); + const list = DSERVERS.filter(s=>(s.name+s.en).toLowerCase().includes(q.toLowerCase())); + const smartActive = current==='AUTO'; + return ( +
+
+ setQ(e.target.value)} placeholder={t('search')} style={{ border:'none', background:'transparent', outline:'none', flex:1, fontFamily:'var(--font-sans)', fontSize:13.5, color:'var(--fg1)' }} /> +
+ {/* smart select — recommended card */} + +
+ {list.map(s=>{ + const active = s.code===current; + return ( + + ); + })} +
+
+ ); +} + +function DStats({ t, lang }) { + const vals=[2.1,3.4,1.8,4.6,5.2,6.1,3.0]; const labels=t('days'); const max=6.1; + return ( +
+
+ {[[t('trafficMonth'),'42.6 GB'],[t('avgPing'),'29 ms'],[t('durMonth'),'86.4 h']].map(([l,v])=>( +
+
{l}
+
{v}
+
+ ))} +
+
+
{t('weekTraffic')}
+
+ {vals.map((v,i)=>( +
+
{v}
+
+
{labels[i]}
+
+ ))} +
+
+
+ ); +} + +function DSettings({ t, lang, setLang, dark, setDark, free, setFree, onReplayAuth, onReplayOnboarding, onRedeem }) { + const [auto,setAuto]=useState(true); const [kill,setKill]=useState(true); const [boot,setBoot]=useState(true); + const card={ background:'var(--surface)', border:'1px solid var(--border)', borderRadius:'var(--radius-lg)', boxShadow:'var(--shadow-sm)', overflow:'hidden', marginBottom:16 }; + const Row=({title,sub,right,last,onClick})=>( +
+
{title}
{sub&&
{sub}
}
{right} +
+ ); + return ( +
+
+ } last onClick={onRedeem} /> +
+
+ setBoot(!boot)} />} /> + setAuto(!auto)} />} /> + setKill(!kill)} />} last /> +
+
+ } /> + setDark(!dark)} />} /> + WireGuard} /> + } /> + v2.4.0 (macOS)} last /> +
+
+ } onClick={onReplayAuth} /> + } onClick={onReplayOnboarding} /> + setFree(!free)} />} last /> +
+
+ ); +} + +function DesktopApp() { + const [stage,setStage]=useState('app'); // app | auth | onboarding — UI kit opens on the main app + const [view,setView]=useState('connect'); + const [status,setStatus]=useState('off'); + const [code,setCode]=useState('AUTO'); // 'AUTO' = 智能选择(默认) + const [dark,setDark]=useState(false); + const [lang,setLang]=useState('zh'); + const [free,setFree]=useState(true); // 演示:免费版视角(与移动端一致) + const [mins,setMins]=useState(6); + const [adDone,setAdDone]=useState(false); + const [elapsed,setElapsed]=useState(0); + const timer=useRef(null); + const t=dT(lang); + const smart=code==='AUTO'; + const server=smart ? DSERVERS.reduce((a,b)=>a.pings.code===code); + const titles={connect:t('connect'),servers:t('servers'),stats:t('stats'),account:t('navAccount'),plans:t('choosePlan'),redeem:t('redeemTitle'),contact:t('navContact'),settings:t('settings')}; + + function toggle(){ if(status==='off'){ setStatus('connecting'); setTimeout(()=>{setStatus('on');setElapsed(0);},1500); } else setStatus('off'); } + function pick(c){ setCode(c); setView('connect'); if(status==='on'){ setStatus('connecting'); setTimeout(()=>{setStatus('on');setElapsed(0);},1200); } } + useEffect(()=>{ if(status==='on') timer.current=setInterval(()=>setElapsed(e=>e+1),1000); return ()=>clearInterval(timer.current); },[status]); + + if(stage==='auth') return ( +
+ setStage('onboarding')} /> +
+ ); + if(stage==='onboarding') return ( +
+ setStage('app')} /> +
+ ); + + let main; + if(view==='connect') main=setAdDone(true)} elapsed={elapsed} />; + else if(view==='servers') main=; + else if(view==='stats') main=; + else if(view==='account') main=setView('plans')} onSignOut={()=>setStage('auth')} />; + else if(view==='plans') main=setView('redeem')} />; + else if(view==='redeem') main=; + else if(view==='contact') main=; + else main=setStage('auth')} onReplayOnboarding={()=>setStage('onboarding')} onRedeem={()=>setView('redeem')} />; + + return ( +
+
+
+
+
{lang==='zh'?'穿山甲':'Pangolin'}
PANGOLIN
+
+
+ setView('connect')} /> + setView('servers')} /> + setView('stats')} /> + setView('account')} /> + setView('contact')} /> + setView('settings')} /> +
+
+
+
+
{free?t('freePlanName'):t('proMember')}
{free?t('quotaFree'):t('until')}
+
+
+
+
+
+ {(view==='redeem'||view==='plans') && } +
{titles[view]}
+
+
+ {status==='on'?'● '+t('connected')+' '+server.code:'○ '+t('notConnected')} + +
+
+
{main}
+
+
+ ); +} +Object.assign(window, { DesktopApp }); diff --git a/design/ui_kits/desktop/dflows.jsx b/design/ui_kits/desktop/dflows.jsx new file mode 100644 index 0000000..521ce10 --- /dev/null +++ b/design/ui_kits/desktop/dflows.jsx @@ -0,0 +1,313 @@ +/* dflows.jsx — 穿山甲 VPN desktop · auth + account management (integrated dashboard) + redeem */ +const { useState: useStateD } = React; + +const dCard = { background:'var(--surface)', border:'1px solid var(--border)', borderRadius:'var(--radius-xl)', boxShadow:'var(--shadow-sm)' }; +const dInput = { border:'1.5px solid var(--border-strong)', borderRadius:'var(--radius-md)', padding:'11px 13px', fontFamily:'var(--font-sans)', fontSize:14, color:'var(--fg1)', background:'var(--bg)', outline:'none', width:'100%', boxSizing:'border-box' }; + +function DField({ label, children }) { + return ; +} + +/* ───────── AUTH — split panel, brand left / form right ───────── */ +function DAuthFlow({ t, lang, dark, onDone }) { + const [mode,setMode]=useStateD('login'); + const [step,setStep]=useStateD(0); + const [email,setEmail]=useStateD(''); + const [code,setCode]=useStateD(''); + const [pw,setPw]=useStateD(''); + const [sent,setSent]=useStateD(false); + const [totp,setTotp]=useStateD(false); // 2FA step after login (PRO) + const [otp,setOtp]=useStateD(''); + const valid=/\S+@\S+\.\S+/.test(email); + + const tab=(id,label)=>( + + ); + const primary=(label,onClick,enabled)=>( + + ); + + return ( +
+ {/* brand panel */} +
+
+
{lang==='zh'?'穿山甲':'Pangolin'}
+
+
+
{t('authSidePitch')}
+
+
{t('authTagline')}
+
+ {/* form panel */} +
+
{tab('login',t('tabLogin'))}{tab('register',t('tabRegister'))}
+
+ {mode==='login' && !totp && (<> + setEmail(e.target.value)} placeholder={t('emailPh')} /> + setPw(e.target.value)} placeholder={t('pwPh')} /> +
{t('forgotPw')}
+ {primary(t('doLogin'), ()=>setTotp(true), valid && pw.length>0)} + )} + {mode==='login' && totp && (<> +
+ + {t('twoFATitle')} + PRO +
+
{t('twoFAHint')}
+ setOtp(e.target.value.replace(/\D/g,'').slice(0,6))} placeholder="······" + style={{ ...dInput, fontFamily:'var(--font-mono)', fontSize:20, letterSpacing:'0.5em', textAlign:'center' }} /> + {primary(t('twoFAConfirm'), onDone, otp.length===6)} +
setTotp(false)} style={{ textAlign:'center', fontSize:12.5, color:'var(--fg3)', cursor:'pointer' }}>{t('twoFABack')}
+ )} + {mode==='register' && step===0 && (<> + +
+ setEmail(e.target.value)} placeholder={t('emailPh')} /> + +
+
+ {sent &&
{t('codeSentTo')} {email}
} + setCode(e.target.value.replace(/\D/g,'').slice(0,6))} placeholder="······" /> + {primary(t('doNext'), ()=>setStep(1), sent && code.length===6)} + )} + {mode==='register' && step===1 && (<> +
{email}
+ setPw(e.target.value)} placeholder={t('setPwPh')} /> + {primary(t('doCreate'), onDone, pw.length>=6)} + )} +
+
{t('tos')}
+
+
+ ); +} + +/* ───────── ACCOUNT (integrated: plan + account info + devices) ───────── */ +function DAccountView({ t, lang, free, onUpgrade, onSignOut }) { + const [twoFA,setTwoFA]=useStateD(true); + const [devices,setDevices]=useStateD([ + { id:1, icon:'laptop', name:'MacBook Pro', os:'macOS 26', active:t('thisDevice'), me:true }, + { id:2, icon:'smartphone', name:'iPhone 17', os:'iOS 26', active:'2 min', me:false }, + { id:3, icon:'monitor-smartphone', name:'iPad Air', os:'iPadOS 26', active:lang==='zh'?'3 小时前':'3h ago', me:false }, + ]); + const infoRow=(label,value,action,last)=>( +
+
{label}
+
{value}
+ {action} +
+ ); + const smallBtn={ border:'1.5px solid var(--border-strong)', background:'transparent', color:'var(--fg2)', fontSize:13, fontWeight:600, padding:'7px 14px', borderRadius:'var(--radius-full)', cursor:'pointer' }; + return ( +
+ {/* plan banner */} +
+
+
+
+
{t('curPlan')}
+
{free?t('freePlanName'):t('proMember')}
+
{free?t('quotaFree'):`${t('expires')} 2026-12-31 · ${t('accUsage')} 42.6 GB`}
+
+
+ +
+ + {/* account info */} +
+
{t('accInfoTitle')}
+
+ {infoRow(t('accEmail'), 'me@pangolin.vpn', )} + {infoRow(t('accPassword'), '••••••••••', )} + {infoRow(t('twoFA'), t('twoFASub'), setTwoFA(!twoFA)} />)} + {infoRow(t('ucEntry'), t('ucEntrySub'), )} +
+
{t('accSignOut')}
+
+ +
+
+
+ + {/* devices */} +
+
{t('myDevices')}
+
{t('devicesSub')}
+
+ {devices.map((d,i)=>( +
+
+
+
{d.name}{d.me && {d.active}}
+
{d.os}{!d.me && ` · ${t('lastActive')} ${d.active}`}
+
+ {!d.me && } +
+ ))} +
+
+
+ ); +} + +/* ───────── PLANS (shown on renew / upgrade) ───────── */ +function DPlansView({ t, lang, onChoose }) { + const plans=[ + { id:'free', name:t('free'), price:'¥0', feats:t('feFree'), cta:t('current'), variant:'cur' }, + { id:'pro', name:t('proPlan'), price:'¥25', feats:t('fePro'), cta:t('upgrade'), variant:'pro' }, + { id:'team', name:t('team'), price:'¥99', feats:t('feTeam'), cta:t('choose'), variant:'plain' }, + ]; + return ( +
+
+ {plans.map(p=>{ + const isPro=p.variant==='pro'; + return ( +
+ {isPro &&
{t('mostPopular')}
} +
{p.name}
+
{p.price}{t('perMonth')}
+
+ {p.feats.map((f,i)=>(
{f}
))} +
+ +
+ ); + })} +
+
+ ); +} + +/* ───────── REDEEM & BUY ───────── */ +function DRedeemView({ t, lang }) { + const [code,setCode]=useStateD(''); + const [ok,setOk]=useStateD(false); + const channels=[ + { icon:'shopping-bag', name:t('chStore'), sub:t('chStoreSub'), accent:true }, + { icon:'credit-card', name:'USDT (TRC20)', sub:t('chUsdtSub'), mono:false }, + { icon:'send', name:'Telegram', sub:t('chTgSub'), mono:true }, + { icon:'message-circle', name:'LINE', sub:t('chLineSub'), mono:true }, + { icon:'mail', name:t('chEmail'), sub:t('chEmailSub'), mono:true }, + ]; + return ( +
+
+
{t('redeemCodeTitle')}
+ {ok ? ( +
{t('redeemOk')}
+ ) : ( +
+ setCode(e.target.value.toUpperCase())} placeholder={t('redeemPh')} style={{ ...dInput, fontFamily:'var(--font-mono)', letterSpacing:'0.08em' }} /> + +
+ )} +
+
{t('buyTitle')}
+
{t('buySub')}
+
+ {channels.map(c=>( + + ))} +
+
+ ); +} + +/* ───────── ONBOARDING — window card, 3 screens ───────── */ +function DOnboarding({ t, lang, dark, onDone }) { + const [i,setI]=useStateD(0); + const steps=[ + { icon:'globe', title:t('ob1Title'), sub:t('ob1Sub'), cta:t('obNext') }, + { icon:'lock', title:t('ob2Title'), sub:t('ob2Sub'), cta:t('obAllow') }, + { icon:'shield-check', title:t('ob3Title'), sub:t('ob3Sub'), cta:t('obStart') }, + ]; + const s=steps[i]; + const last=i===steps.length-1; + return ( +
+
+
{lang==='zh'?'穿山甲':'Pangolin'}
+ {!last && } +
+
+
+ +
+
+
{s.title}
+
{s.sub}
+
+
+
+
+ {steps.map((_,k)=>(
))} +
+ +
+
+ ); +} + +/* ───────── CONTACT US ───────── */ +function DContactView({ t, lang }) { + const channels=[ + { icon:'send', name:t('contactTg'), sub:'@PangolinVPN_bot', accent:true }, + { icon:'message-circle', name:t('contactLine'), sub:'@pangolinvpn' }, + { icon:'mail', name:t('contactEmail'), sub:'support@pangolin.vpn' }, + { icon:'shopping-bag', name:t('contactStore'), sub:'shop.pangolin.vpn' }, + ]; + return ( +
+
{t('contactIntro')}
+
+ {channels.map(c=>( + + ))} +
+
+
+
{t('contactHoursTitle')}
+
{t('contactHours')}
+
+
+
+
{t('contactFaqTitle')}
+
{t('contactFaq')}
+
+ +
+
+
+ ); +} + +Object.assign(window, { DAuthFlow, DOnboarding, DAccountView, DPlansView, DRedeemView, DContactView }); diff --git a/design/ui_kits/desktop/dparts.jsx b/design/ui_kits/desktop/dparts.jsx new file mode 100644 index 0000000..f0e1455 --- /dev/null +++ b/design/ui_kits/desktop/dparts.jsx @@ -0,0 +1,161 @@ +/* dparts.jsx — 穿山甲 VPN desktop · atoms (inline Lucide paths) */ +const DLUCIDE = { + power: '', + 'shield-check': '', + globe: '', + 'loader-circle': '', + 'chart-no-axes-column': '', + settings: '', + check: '', + zap: '', + 'arrow-down': '', + 'arrow-up': '', + search: '', + moon: '', + sun: '', + crown: '', + mail: '', + lock: '', + 'check-circle': '', + 'arrow-left': '', + 'chevron-right': '', + send: '', + 'message-circle': '', + 'external-link': '', + 'shopping-bag': '', + laptop: '', + smartphone: '', + 'monitor-smartphone': '', + 'log-out': '', + ticket: '', + clock: '', + 'play-circle': '', + compass: '', + 'refresh-cw': '', + 'credit-card': '', + user: '', +}; +function DIcon({ name, size = 20, stroke = 2, color = 'currentColor', style = {} }) { + return ; +} +function DMark({ size = 28, color = 'var(--accent)' }) { + return ( + + + + + + + + + + ); +} +function DCC({ code, active, size = 34 }) { + return
{code}
; +} +function DSignal({ ping }) { + const lvl = ping < 40 ? 3 : ping < 90 ? 2 : 1; + return
{[4,8,13].map((h,i)=>
)}
; +} +const DSTRINGS = { + connect:{zh:'连接',en:'Connect'}, servers:{zh:'选择节点',en:'Choose server'}, stats:{zh:'统计',en:'Stats'}, settings:{zh:'设置',en:'Settings'}, + navConnect:{zh:'连接',en:'Connect'}, navServers:{zh:'节点',en:'Servers'}, navStats:{zh:'统计',en:'Stats'}, navSettings:{zh:'设置',en:'Settings'}, + proMember:{zh:'PRO 会员',en:'PRO member'}, until:{zh:'至 2026-12-31',en:'until 2026-12-31'}, + connected:{zh:'已连接',en:'Connected'}, notConnected:{zh:'未连接',en:'Not connected'}, + capOff:{zh:'未连接',en:'Not connected'}, subOff:{zh:'点击下方按钮开始保护',en:'Tap the button below to start'}, + capConnecting:{zh:'连接中…',en:'Connecting…'}, subConnecting:{zh:'正在建立加密隧道',en:'Establishing encrypted tunnel'}, + capOn:{zh:'已连接',en:'Connected'}, subOn:{zh:'你的网络已加密保护',en:'Your network is encrypted'}, + connectNow:{zh:'点击连接',en:'CONNECT'}, + search:{zh:'搜索国家 / 城市',en:'Search country / city'}, + trafficMonth:{zh:'本月流量',en:'Traffic'}, avgPing:{zh:'平均延迟',en:'Avg ping'}, durMonth:{zh:'本月时长',en:'Time'}, + weekTraffic:{zh:'本周流量 (GB)',en:'This week (GB)'}, + autostart:{zh:'开机自启动',en:'Launch at login'}, autostartSub:{zh:'登录系统时自动启动穿山甲',en:'Start Pangolin when you log in'}, + smartRoute:{zh:'智能分流',en:'Smart routing'}, smartRouteSub:{zh:'海外应用加速,本地直连',en:'Accelerate overseas apps, keep local direct'}, + killSwitch:{zh:'Kill Switch',en:'Kill Switch'}, killSwitchSub:{zh:'断线时阻断网络,防止 IP 泄露',en:'Block traffic if the VPN drops'}, + language:{zh:'语言',en:'Language'}, darkAppearance:{zh:'深色外观',en:'Dark appearance'}, stateOn:{zh:'已开启',en:'On'}, followLight:{zh:'跟随浅色',en:'Off'}, + protocol:{zh:'协议',en:'Protocol'}, version:{zh:'版本',en:'Version'}, + days:{zh:['周一','周二','周三','周四','周五','周六','周日'],en:['Mon','Tue','Wed','Thu','Fri','Sat','Sun']}, + /* auth */ + authTagline:{zh:'极速 · 稳定 · 省心',en:'Fast · Stable · Effortless'}, + tabLogin:{zh:'登录',en:'Log in'}, tabRegister:{zh:'注册',en:'Sign up'}, + emailLabel:{zh:'邮箱',en:'Email'}, emailPh:{zh:'你的邮箱地址',en:'your@email.com'}, + pwLabel:{zh:'密码',en:'Password'}, pwPh:{zh:'输入密码',en:'Enter password'}, setPwPh:{zh:'设置登录密码(用于多端登录)',en:'Set a password (for multi-device login)'}, + codeLabel:{zh:'邮箱验证码',en:'Verification code'}, codeSentTo:{zh:'验证码已发送至',en:'Code sent to'}, + sendCode:{zh:'发送验证码',en:'Send code'}, resend:{zh:'重新发送',en:'Resend'}, + doLogin:{zh:'登录',en:'Log in'}, doNext:{zh:'下一步',en:'Next'}, doCreate:{zh:'创建账户',en:'Create account'}, + forgotPw:{zh:'忘记密码?',en:'Forgot password?'}, + tos:{zh:'继续即代表同意《服务条款》与《隐私政策》',en:'By continuing you agree to our Terms & Privacy Policy'}, + authSidePitch:{zh:'一个账户,全平台同步。守护你的每一次连接。',en:'One account, every device. Protecting every connection.'}, + /* redeem & buy */ + redeemEntry:{zh:'兑换 & 购买',en:'Redeem & buy'}, redeemTitle:{zh:'兑换 & 购买',en:'Redeem & buy'}, + redeemCodeTitle:{zh:'兑换激活码',en:'Redeem a code'}, redeemPh:{zh:'输入激活码',en:'Enter activation code'}, redeemBtn:{zh:'激活',en:'Redeem'}, + redeemOk:{zh:'激活成功 · PRO 已开通',en:'Activated · PRO unlocked'}, + buyTitle:{zh:'购买渠道',en:'Where to buy'}, + buySub:{zh:'客户端内不支持直接支付。请通过以下渠道获取激活码:',en:'In-app payment is unavailable. Get an activation code via:'}, + chStore:{zh:'自助发卡商店',en:'Self-serve store'}, chStoreSub:{zh:'支付宝 / 微信 · 自动发码',en:'Alipay / WeChat · instant code'}, + chUsdtSub:{zh:'链上转账 · 最隐私 · 自动发码',en:'On-chain · most private · instant code'}, + smartSelect:{zh:'智能选择',en:'Smart select'}, smartSub:{zh:'根据当前网络环境,自动选择最优节点',en:'Picks the best node for your network'}, + recommended:{zh:'推荐',en:'Recommended'}, + twoFA:{zh:'双重认证',en:'Two-factor auth'}, twoFASub:{zh:'PRO 专享 · 登录需身份验证器动态码',en:'PRO only · TOTP code at login'}, + ucEntry:{zh:'Web 用户中心',en:'Web account center'}, ucEntrySub:{zh:'订阅导入 · 邀请返利',en:'Subscription import · referral'}, ucOpen:{zh:'打开',en:'Open'}, + /* quota + free (口径见 CLAUDE.md §7,与移动端一致) */ + quotaToday:{zh:'今日剩余',en:'Left today'}, minutes:{zh:'分钟',en:'min'}, + quotaFree:{zh:'免费版 · 每日 10 分钟',en:'Free · 10 min/day'}, + watchAd:{zh:'看广告开始使用',en:'Watch ad to start'}, + adUnlocked:{zh:'已解锁 · 今日可用',en:'Unlocked for today'}, + freePlanName:{zh:'免费版',en:'Free'}, + freeDemo:{zh:'演示:免费版视角',en:'Demo: free-tier view'}, freeDemoSub:{zh:'切换连接页的额度展示与套餐横幅',en:'Toggles quota UI & plan banner'}, + checkUpdate:{zh:'检查更新',en:'Check for updates'}, + replayOnboarding:{zh:'查看首次引导',en:'View onboarding'}, replayOnboardingSub:{zh:'演示用 · 重看 3 屏引导',en:'Demo · replay the 3 intro screens'}, + /* 2FA login step (与用户中心一致) */ + twoFATitle:{zh:'双重认证',en:'Two-factor auth'}, + twoFAHint:{zh:'你的账户已开启双重认证。请输入身份验证器 App 中的 6 位动态码。',en:'Two-factor auth is on for this account. Enter the 6-digit code from your authenticator app.'}, + twoFAConfirm:{zh:'验证并登录',en:'Verify & log in'}, + twoFABack:{zh:'返回上一步',en:'Back'}, + chTgSub:{zh:'@PangolinVPN_bot',en:'@PangolinVPN_bot'}, chLineSub:{zh:'@pangolinvpn',en:'@pangolinvpn'}, + chEmail:{zh:'邮箱客服',en:'Email support'}, chEmailSub:{zh:'buy@pangolin.vpn',en:'buy@pangolin.vpn'}, + /* account management (integrated dashboard) */ + navAccount:{zh:'账户',en:'Account'}, + curPlan:{zh:'当前套餐',en:'Current plan'}, renew:{zh:'续费 / 升级',en:'Renew / Upgrade'}, + myDevices:{zh:'我的设备',en:'My devices'}, devicesSub:{zh:'PRO 套餐最多 5 台设备同时在线',en:'Up to 5 devices on PRO'}, + thisDevice:{zh:'当前设备',en:'This device'}, lastActive:{zh:'最近活跃',en:'Last active'}, remove:{zh:'移除',en:'Remove'}, + choosePlan:{zh:'选择套餐',en:'Choose your plan'}, free:{zh:'免费版',en:'Free'}, proPlan:{zh:'专业版',en:'Pro'}, team:{zh:'团队版',en:'Team'}, + perMonth:{zh:'/月',en:'/mo'}, current:{zh:'当前',en:'Current'}, upgrade:{zh:'立即升级',en:'Upgrade'}, choose:{zh:'选择',en:'Choose'}, mostPopular:{zh:'最受欢迎',en:'Popular'}, + feFree:{zh:['仅 1 个基础节点','每日 10 分钟时长','使用前观看广告','注册享 7 天免费试用'],en:['1 basic node only','10 min per day','Watch an ad to start','7-day free trial on sign-up']}, + fePro:{zh:['80+ 全球节点','无限流量 · 极速','5 台设备同时在线','流媒体优化'],en:['80+ locations','Unlimited · fast','5 devices','Streaming optimized']}, + feTeam:{zh:['Pro 全部功能','10 个成员席位','集中计费与管理','优先客服'],en:['Everything in Pro','10 seats','Central billing','Priority support']}, + /* account info */ + accInfoTitle:{zh:'账户信息',en:'Account info'}, accEmail:{zh:'邮箱',en:'Email'}, accPassword:{zh:'密码',en:'Password'}, accChange:{zh:'修改',en:'Change'}, + accUsage:{zh:'本月用量',en:'Used this month'}, accSignOut:{zh:'退出登录',en:'Sign out'}, + backToAccount:{zh:'返回账户',en:'Back to account'}, + /* demo replay */ + replayAuth:{zh:'查看登录 / 注册流程',en:'View login / sign-up'}, replayAuthSub:{zh:'演示用 · 退出到登录页',en:'Demo · back to login'}, + replayOnboarding:{zh:'查看首次引导',en:'View onboarding'}, replayOnboardingSub:{zh:'演示用 · 重看引导页',en:'Demo · replay intro'}, + /* onboarding */ + obSkip:{zh:'跳过',en:'Skip'}, obNext:{zh:'下一步',en:'Next'}, obAllow:{zh:'允许并继续',en:'Allow & continue'}, obStart:{zh:'开始使用',en:'Get started'}, + ob1Title:{zh:'一键连接全球',en:'Connect worldwide'}, ob1Sub:{zh:'80+ 节点,智能选择最快线路,稳定不掉线。',en:'80+ locations, auto-fastest routing, rock-solid.'}, + ob2Title:{zh:'授权网络配置',en:'Allow network setup'}, ob2Sub:{zh:'系统会请求添加一个网络配置,用于建立加密隧道。',en:'Your system will ask to add a network profile to build the encrypted tunnel.'}, + ob3Title:{zh:'安全 · 无日志',en:'Private by design'}, ob3Sub:{zh:'端到端加密,我们不记录你的任何浏览数据。',en:'End-to-end encrypted. We keep zero browsing logs.'}, + /* contact us */ + navContact:{zh:'联系我们',en:'Contact us'}, + contactIntro:{zh:'遇到问题?通过以下任一渠道联系我们,通常数分钟内回复。',en:'Need help? Reach us through any channel below — we usually reply within minutes.'}, + contactTg:{zh:'Telegram 客服',en:'Telegram support'}, contactLine:{zh:'LINE 客服',en:'LINE support'}, + contactEmail:{zh:'邮箱客服',en:'Email support'}, contactStore:{zh:'自助发卡商店',en:'Self-serve store'}, + contactHoursTitle:{zh:'服务时间',en:'Support hours'}, contactHours:{zh:'每日 9:00 – 24:00 (GMT+8)',en:'Daily 9:00 – 24:00 (GMT+8)'}, + contactFaqTitle:{zh:'常见问题',en:'FAQ'}, contactFaq:{zh:'连不上 / 兑换码无效 / 多设备登录,先查阅帮助中心。',en:'Connection issues, invalid codes, multi-device — check the Help Center first.'}, + contactFaqBtn:{zh:'打开帮助中心',en:'Open Help Center'}, +}; +function dT(lang){ return k => { const v=DSTRINGS[k]; return v ? (v[lang]||k) : k; }; } +function dName(s,lang){ return lang==='zh'?s.name:s.en; } +function dSub(s,lang){ if(lang==='zh') return s.tag||s.en; return s.tag==='流媒体优化'?'Streaming':s.tag==='P2P'?'P2P':s.name; } +Object.assign(window, { DIcon, DMark, DCC, DSignal, DSTRINGS, dT, dName, dSub, DSERVERS: [ + { code:'HK', name:'香港 · 流媒体', en:'Hong Kong', ping:18, tag:'流媒体优化' }, + { code:'JP', name:'日本 东京', en:'Tokyo', ping:32, tag:'P2P' }, + { code:'TW', name:'台湾 台北', en:'Taipei', ping:28, tag:'' }, + { code:'SG', name:'新加坡', en:'Singapore', ping:54, tag:'P2P' }, + { code:'KR', name:'韩国 首尔', en:'Seoul', ping:41, tag:'' }, + { code:'US', name:'美国 洛杉矶', en:'Los Angeles', ping:146, tag:'流媒体优化' }, + { code:'DE', name:'德国 法兰克福', en:'Frankfurt', ping:198, tag:'' }, + { code:'UK', name:'英国 伦敦', en:'London', ping:210, tag:'' }, +] }); diff --git a/design/ui_kits/desktop/index.html b/design/ui_kits/desktop/index.html new file mode 100644 index 0000000..28958f0 --- /dev/null +++ b/design/ui_kits/desktop/index.html @@ -0,0 +1,31 @@ + + + + + + +穿山甲 · 桌面客户端 + + + + + + + +
+ + + + + + + diff --git a/design/ui_kits/desktop/macos-window.jsx b/design/ui_kits/desktop/macos-window.jsx new file mode 100644 index 0000000..ef54ed2 --- /dev/null +++ b/design/ui_kits/desktop/macos-window.jsx @@ -0,0 +1,198 @@ + +/* BEGIN USAGE */ +// MacOS.jsx — Simplified macOS Tahoe (Liquid Glass) window +// Based on the macOS Tahoe UI Kit. No image assets, no dependencies. +// Exports (to window): MacWindow, MacSidebar, MacSidebarItem, MacSidebarHeader, MacToolbar, MacGlass, MacTrafficLights +// +// Usage — wrap your app content in to get the window chrome +// (traffic lights + titlebar). Props: width, height, title, sidebar (pass a +// element); compose MacToolbar/MacGlass inside as needed: +// +// }> +// ...your app content... +// +/* END USAGE */ + +const MAC_FONT = '-apple-system, BlinkMacSystemFont, "SF Pro", "Helvetica Neue", sans-serif'; + +// ───────────────────────────────────────────────────────────── +// Liquid glass primitive — blur + white tint + inset highlight +// ───────────────────────────────────────────────────────────── +function MacGlass({ children, radius = 296, dark = false, style = {} }) { + return ( +
+
+
{children}
+
+ ); +} + +// ───────────────────────────────────────────────────────────── +// Traffic lights (14px, Tahoe colors) +// ───────────────────────────────────────────────────────────── +function MacTrafficLights({ style = {} }) { + const dot = (bg) => ( +
+ ); + return ( +
+ {dot('#ff736a')}{dot('#febc2e')}{dot('#19c332')} +
+ ); +} + +// ───────────────────────────────────────────────────────────── +// Toolbar — title + single glass pill icon +// ───────────────────────────────────────────────────────────── +function MacToolbar({ title = 'Folder' }) { + return ( +
+ {/* title */} +
{title}
+
+ {/* single action */} + +
+
+
+ + {/* search */} + +
+ + + + + Search +
+
+
+ ); +} + +// ───────────────────────────────────────────────────────────── +// Sidebar — frosted glass panel floating inside the window +// ───────────────────────────────────────────────────────────── +function MacSidebarItem({ label, selected = false }) { + return ( +
+ {selected && ( +
+ )} +
+ {label} +
+ ); +} + +function MacSidebar({ children }) { + return ( +
+ {/* glass panel */} +
+ {/* content */} +
+ {/* window controls + sidebar toggle */} +
+ +
+ {children} +
+
+ ); +} + +function MacSidebarHeader({ title }) { + return ( +
{title}
+ ); +} + +// ───────────────────────────────────────────────────────────── +// Window — r:26, big shadow, sidebar + toolbar + content +// ───────────────────────────────────────────────────────────── +function MacWindow({ + width = 900, height = 600, title = 'Folder', + sidebar, children, +}) { + return ( +
+ {sidebar} +
+ +
+ {children} +
+
+
+ ); +} + +Object.assign(window, { + MacWindow, MacSidebar, MacSidebarItem, MacSidebarHeader, + MacToolbar, MacGlass, MacTrafficLights, +}); diff --git a/design/ui_kits/mobile/README.md b/design/ui_kits/mobile/README.md new file mode 100644 index 0000000..678dad1 --- /dev/null +++ b/design/ui_kits/mobile/README.md @@ -0,0 +1,29 @@ +# 穿山甲 VPN — 移动 App UI Kit + +iOS / Android 连接界面的高保真可点击原型。极简、暖大地色、双语、支持深浅主题。 + +## 运行 +打开 `index.html`。设备框由 `ios-frame.jsx`(启动组件)提供。完整流程:**登录/注册 → 首次引导 → 主应用**。可交互: +- **登录 / 注册**(`AuthFlow`):登录用邮箱 + 密码;注册走「邮箱 → 发送验证码 → 填验证码 → 设置密码」。密码用于多端登录。 +- **首次引导**(`Onboarding`):3 屏(连接全球 → 授权 VPN 配置 → 安全无日志),可跳过。 +- **连接键**:轻点 → 连接中(旋转 loader + 进度弧)→ 已连接(SAFE + 计时 + 实时速率)。再点断开。 +- **底部 Tab**:连接 / 节点 / 统计 / 我的。 +- **节点**:搜索 + 列表,点选切换(已连接时短暂重连),选中态高亮。 +- **统计**:流量 / 延迟 / 时长指标卡 + 本周柱状图。 +- **我的**:**兑换 & 购买**入口、PRO 账户卡、智能分流 / Kill Switch 开关、**深色外观开关**、语言切换。 +- **兑换 & 购买**(`RedeemScreen`):兑换激活码 + 购买渠道(自助发卡 / Telegram / LINE / 邮箱)。**App 内无支付表单**——资金流全走外部,符合 VPN 风控。 + +## 文件 +| 文件 | 内容 | +|---|---| +| `index.html` | 应用状态机:stage(auth/onboarding/app)/ tab / 连接态 / 节点 / 主题 / 计时器 | +| `parts.jsx` | 原子:`Icon`(内联 Lucide 路径)、`Mark`(品牌标)、`CC`(国家码块)、`Signal`、`Pill`;`SERVERS` 数据;`STRINGS` i18n 字典 | +| `screens.jsx` | `ConnectScreen` / `ServersScreen` / `StatsScreen` / `SettingsScreen` / `BottomTab` / `Toggle` / `Row` | +| `flows.jsx` | `AuthFlow`(登录/邮箱验证码注册)/ `Onboarding`(3 屏)/ `RedeemScreen`(兑换+购买渠道) | +| `ios-frame.jsx` | 设备框启动组件(状态栏 / 灵动岛 / Home 指示器) | + +## 关键模式 +- 颜色 / 字体 / 圆角 / 阴影全部走 `../../colors_and_type.css` 的语义 token,组件天然双主题。 +- 图标用内联 Lucide 路径数据(`Icon` 组件),避免 React 中 `createIcons()` 时序问题。 +- 连接键三态用色环(`box-shadow` 外环)区分:中性 / clay / success。 +- 国家用 2 字母码块,不用 emoji 国旗。 diff --git a/design/ui_kits/mobile/flows.jsx b/design/ui_kits/mobile/flows.jsx new file mode 100644 index 0000000..fa85e6a --- /dev/null +++ b/design/ui_kits/mobile/flows.jsx @@ -0,0 +1,269 @@ +/* flows.jsx — 穿山甲 VPN mobile · auth, onboarding, redeem/buy */ +const { useState: useStateF } = React; + +/* shared field */ +function Field({ icon, label, children }) { + return ( + + ); +} +const inputStyle = { border: 'none', background: 'transparent', outline: 'none', flex: 1, fontFamily: 'var(--font-sans)', fontSize: 15, color: 'var(--fg1)', minWidth: 0 }; + +/* ───────── AUTH (login + email-code register) ───────── */ +function AuthFlow({ t, lang, onDone }) { + const [mode, setMode] = useStateF('login'); // login | register + const [step, setStep] = useStateF(0); // register: 0 email+code, 1 set password + const [email, setEmail] = useStateF(''); + const [code, setCode] = useStateF(''); + const [pw, setPw] = useStateF(''); + const [sent, setSent] = useStateF(false); + const [totp, setTotp] = useStateF(false); // 2FA step after login (PRO) + const [otp, setOtp] = useStateF(''); + + const valid = /\S+@\S+\.\S+/.test(email); + const tab = (id, label) => ( + + ); + const primaryBtn = (label, onClick, enabled) => ( + + ); + + return ( +
+ {/* brand header */} +
+ +
+
{t('brand')}
+
{t('authTagline')}
+
+
+ + {/* tabs */} +
+ {tab('login', t('tabLogin'))} + {tab('register', t('tabRegister'))} +
+ +
+ {mode === 'login' && !totp && (<> + setEmail(e.target.value)} placeholder={t('emailPh')} /> + setPw(e.target.value)} placeholder={t('pwPh')} /> +
{t('forgotPw')}
+ {primaryBtn(t('doLogin'), () => setTotp(true), valid && pw.length > 0)} + )} + + {mode === 'login' && totp && (<> +
+ + {t('twoFATitle')} + PRO +
+
{t('twoFAHint')}
+ + setOtp(e.target.value.replace(/\D/g, '').slice(0, 6))} placeholder="······" /> + + {primaryBtn(t('twoFAConfirm'), onDone, otp.length === 6)} +
setTotp(false)} style={{ textAlign: 'center', fontSize: 12.5, color: 'var(--fg3)', cursor: 'pointer' }}>{t('twoFABack')}
+ )} + + {mode === 'register' && step === 0 && (<> + + setEmail(e.target.value)} placeholder={t('emailPh')} /> + + + {sent &&
{t('codeSentTo')} {email}
} + + setCode(e.target.value.replace(/\D/g, '').slice(0, 6))} placeholder="······" /> + + {primaryBtn(t('doNext'), () => setStep(1), sent && code.length === 6)} + )} + + {mode === 'register' && step === 1 && (<> +
{email}
+ setPw(e.target.value)} placeholder={t('setPwPh')} /> + {primaryBtn(t('doCreate'), onDone, pw.length >= 6)} + )} +
+ +
{t('tos')}
+
+ ); +} + +/* ───────── ONBOARDING (3 screens) ───────── */ +function Onboarding({ t, lang, onDone }) { + const [i, setI] = useStateF(0); + const slides = [ + { icon: 'globe', tint: 'var(--accent)', bg: 'var(--accent-subtle)', title: t('ob1Title'), sub: t('ob1Sub'), cta: t('obNext') }, + { icon: 'shield', tint: 'var(--accent)', bg: 'var(--accent-subtle)', title: t('ob2Title'), sub: t('ob2Sub'), cta: t('obAllow') }, + { icon: 'shield-check', tint: 'var(--success)', bg: 'var(--success-subtle)', title: t('ob3Title'), sub: t('ob3Sub'), cta: t('obStart') }, + ]; + const s = slides[i]; + const next = () => (i < 2 ? setI(i + 1) : onDone()); + return ( +
+
+ +
+
+
+ +
+
+
{s.title}
+
{s.sub}
+
+
+
+ {slides.map((_, k) =>
)} +
+ +
+ ); +} + +/* ───────── REDEEM & BUY ───────── */ +function RedeemScreen({ t, lang, onBack }) { + const [code, setCode] = useStateF(''); + const [ok, setOk] = useStateF(false); + const channels = [ + { icon: 'shopping-bag', name: t('chStore'), sub: t('chStoreSub'), accent: true }, + { icon: 'credit-card', name: 'USDT (TRC20)', sub: t('chUsdtSub') }, + { icon: 'send', name: 'Telegram', sub: t('chTgSub') }, + { icon: 'message-circle', name: 'LINE', sub: t('chLineSub') }, + { icon: 'mail', name: t('chEmail'), sub: t('chEmailSub') }, + ]; + return ( +
+
+ + {t('redeemTitle')} +
+
+ {/* redeem code */} +
+
{t('redeemCodeTitle')}
+ {ok ? ( +
{t('redeemOk')}
+ ) : (<> +
+ setCode(e.target.value.toUpperCase())} placeholder={t('redeemPh')} style={{ flex: 1, minWidth: 0, border: '1.5px solid var(--border-strong)', borderRadius: 'var(--radius-md)', padding: '12px 14px', fontFamily: 'var(--font-mono)', fontSize: 14, letterSpacing: '0.08em', color: 'var(--fg1)', background: 'var(--bg)', outline: 'none' }} /> + +
+ )} +
+ {/* buy channels */} +
{t('buyTitle')}
+
{t('buySub')}
+
+ {channels.map(c => ( + + ))} +
+
+
+ ); +} + +/* ───────── PLANS (shown on upgrade) ───────── */ +function PlansScreen({ t, lang, onBack, onChoose }) { + const plans = [ + { id: 'free', name: t('free'), price: '¥0', feats: t('feFree'), cta: t('current'), variant: 'cur' }, + { id: 'pro', name: t('proPlan'), price: '¥25', feats: t('fePro'), cta: t('upgrade'), variant: 'pro' }, + { id: 'team', name: t('team'), price: '¥99', feats: t('feTeam'), cta: t('choose'), variant: 'plain' }, + ]; + return ( +
+
+ + {t('choosePlan')} +
+
+ {plans.map(p => { + const isPro = p.variant === 'pro'; + return ( +
+
+
{p.name}{isPro && {t('mostPopular')}}
+
{p.price}{t('perMonth')}
+
+
+ {p.feats.map((f, i) => (
{f}
))} +
+ +
+ ); + })} +
+
+ ); +} + +/* ───────── CONTACT US ───────── */ +function ContactScreen({ t, lang, onBack }) { + const channels = [ + { icon: 'send', name: 'Telegram', sub: '@PangolinVPN_bot', accent: true }, + { icon: 'message-circle', name: 'LINE', sub: '@pangolinvpn' }, + { icon: 'mail', name: t('contactEmail'), sub: 'support@pangolin.vpn' }, + { icon: 'shopping-bag', name: t('contactStore'), sub: 'shop.pangolin.vpn' }, + ]; + return ( +
+
+ + {t('contactTitle')} +
+
+
{t('contactIntro')}
+
+ {channels.map(c => ( + + ))} +
+
+
{t('contactHoursTitle')}
+
{t('contactHours')}
+
+
+
+ ); +} + +Object.assign(window, { AuthFlow, Onboarding, RedeemScreen, PlansScreen, ContactScreen }); diff --git a/design/ui_kits/mobile/index.html b/design/ui_kits/mobile/index.html new file mode 100644 index 0000000..ecf43d4 --- /dev/null +++ b/design/ui_kits/mobile/index.html @@ -0,0 +1,128 @@ + + + + + + +穿山甲 · 移动 App + + + + + + + +
+ + + + + + + diff --git a/design/ui_kits/mobile/ios-frame.jsx b/design/ui_kits/mobile/ios-frame.jsx new file mode 100644 index 0000000..8703a44 --- /dev/null +++ b/design/ui_kits/mobile/ios-frame.jsx @@ -0,0 +1,348 @@ + +/* BEGIN USAGE */ +// iOS.jsx — Simplified iOS 26 (Liquid Glass) device frame +// Based on the iOS 26 UI Kit + Figma status bar spec. No assets, no deps. +// Exports (to window): IOSDevice, IOSStatusBar, IOSNavBar, IOSGlassPill, IOSList, IOSListRow, IOSKeyboard +// +// Usage — wrap your screen content in to get the bezel, status bar +// and home indicator (props: title, dark, keyboard): +// +// +// ...your screen content... +// +// +/* END USAGE */ + +// ───────────────────────────────────────────────────────────── +// Status bar +// ───────────────────────────────────────────────────────────── +function IOSStatusBar({ dark = false, time = '9:41' }) { + const c = dark ? '#fff' : '#000'; + return ( +
+
+ {time} +
+
+ + + + + + + + + + + + + + + + +
+
+ ); +} + +// ───────────────────────────────────────────────────────────── +// Liquid glass pill — blur + tint + shine +// ───────────────────────────────────────────────────────────── +function IOSGlassPill({ children, dark = false, style = {} }) { + return ( +
+ {/* blur + tint */} +
+ {/* shine */} +
+
+ {children} +
+
+ ); +} + +// ───────────────────────────────────────────────────────────── +// Navigation bar — glass pills + large title +// ───────────────────────────────────────────────────────────── +function IOSNavBar({ title = 'Title', dark = false, trailingIcon = true }) { + const muted = dark ? 'rgba(255,255,255,0.6)' : '#404040'; + const text = dark ? '#fff' : '#000'; + const pillIcon = (content) => ( + +
+ {content} +
+
+ ); + return ( +
+
+ {/* back chevron */} + {pillIcon( + + + + )} + {/* trailing ellipsis */} + {trailingIcon && pillIcon( + + + + + + )} +
+ {/* large title */} +
{title}
+
+ ); +} + +// ───────────────────────────────────────────────────────────── +// Grouped list (inset card, r:26) + row (52px) +// ───────────────────────────────────────────────────────────── +function IOSListRow({ title, detail, icon, chevron = true, isLast = false, dark = false }) { + const text = dark ? '#fff' : '#000'; + const sec = dark ? 'rgba(235,235,245,0.6)' : 'rgba(60,60,67,0.6)'; + const ter = dark ? 'rgba(235,235,245,0.3)' : 'rgba(60,60,67,0.3)'; + const sep = dark ? 'rgba(84,84,88,0.65)' : 'rgba(60,60,67,0.12)'; + return ( +
+ {icon && ( +
+ )} +
{title}
+ {detail && {detail}} + {chevron && ( + + + + )} + {!isLast && ( +
+ )} +
+ ); +} + +function IOSList({ header, children, dark = false }) { + const hc = dark ? 'rgba(235,235,245,0.6)' : 'rgba(60,60,67,0.6)'; + const bg = dark ? '#1C1C1E' : '#fff'; + return ( +
+ {header && ( +
{header}
+ )} +
{children}
+
+ ); +} + +// ───────────────────────────────────────────────────────────── +// Device frame +// ───────────────────────────────────────────────────────────── +function IOSDevice({ + children, width = 402, height = 874, dark = false, + title, keyboard = false, +}) { + return ( +
+ {/* dynamic island */} +
+ {/* status bar (absolute) */} +
+ +
+ {/* nav + content */} +
+ {title !== undefined && } +
{children}
+ {keyboard && } +
+ {/* home indicator — always on top */} +
+
+
+
+ ); +} + +// ───────────────────────────────────────────────────────────── +// Keyboard — iOS 26 liquid glass +// ───────────────────────────────────────────────────────────── +function IOSKeyboard({ dark = false }) { + const glyph = dark ? 'rgba(255,255,255,0.7)' : '#595959'; + const sugg = dark ? 'rgba(255,255,255,0.6)' : '#333'; + const keyBg = dark ? 'rgba(255,255,255,0.22)' : 'rgba(255,255,255,0.85)'; + + // special-key icons + const icons = { + shift: , + del: , + ret: , + }; + + const key = (content, { w, flex, ret, fs = 25, k } = {}) => ( +
{content}
+ ); + + const row = (keys, pad = 0) => ( +
+ {keys.map(l => key(l, { flex: true, k: l }))} +
+ ); + + return ( +
+ {/* liquid glass bg — same recipe as nav pills */} +
+
+ + {/* autocorrect bar */} +
+ {['"The"', 'the', 'to'].map((w, i) => ( + + {i > 0 &&
} +
{w}
+ + ))} +
+ + {/* key layout */} +
+ {row(['q','w','e','r','t','y','u','i','o','p'])} + {row(['a','s','d','f','g','h','j','k','l'], 20)} +
+ {key(icons.shift, { w: 45, k: 'shift' })} +
+ {['z','x','c','v','b','n','m'].map(l => key(l, { flex: true, k: l }))} +
+ {key(icons.del, { w: 45, k: 'del' })} +
+
+ {key('ABC', { w: 92.25, fs: 18, k: 'abc' })} + {key('', { flex: true, k: 'space' })} + {key(icons.ret, { w: 92.25, ret: true, k: 'ret' })} +
+
+ + {/* bottom spacer (emoji+mic area, icons omitted) */} +
+
+ ); +} + +Object.assign(window, { + IOSDevice, IOSStatusBar, IOSNavBar, IOSGlassPill, IOSList, IOSListRow, IOSKeyboard, +}); diff --git a/design/ui_kits/mobile/parts.jsx b/design/ui_kits/mobile/parts.jsx new file mode 100644 index 0000000..3114eff --- /dev/null +++ b/design/ui_kits/mobile/parts.jsx @@ -0,0 +1,293 @@ +/* parts.jsx — 穿山甲 VPN mobile · shared atoms + Icon uses inline Lucide path data (lucide.dev) for reliability in React. */ + +const LUCIDE = { + power: '', + 'shield-check': '', + globe: '', + settings: '', + 'chevron-right': '', + 'chevron-down': '', + check: '', + zap: '', + 'loader-circle': '', + user: '', + x: '', + crown: '', + 'map-pin': '', + 'arrow-down': '', + 'arrow-up': '', + search: '', + moon: '', + sun: '', + shield: '', + 'refresh-cw': '', + chart: '', + wifi: '', + mail: '', + lock: '', + 'arrow-left': '', + 'shopping-bag': '', + send: '', + 'message-circle': '', + 'external-link': '', + 'check-circle': '', + 'log-out': '', + compass: '', + laptop: '', + smartphone: '', + 'monitor-smartphone': '', + ticket: '', + clock: '', + 'credit-card': '', + 'play-circle': '', +}; + +function Icon({ name, size = 24, stroke = 2, color = 'currentColor', style = {} }) { + return ( + + ); +} + +/* Brand mark — walking pangolin (matches app icon), facing right */ +function Mark({ size = 40, color = 'var(--accent)' }) { + return ( + + + + + + + + + + ); +} + +/* Country code tile */ +function CC({ code, active }) { + return ( +
{code}
+ ); +} + +/* Signal bars from ping */ +function Signal({ ping }) { + const lvl = ping < 40 ? 3 : ping < 90 ? 2 : 1; + return ( +
+ {[5, 9, 14].map((h, i) => ( +
+ ))} +
+ ); +} + +/* Pill button */ +function Pill({ children, variant = 'primary', onClick, style = {}, disabled }) { + const v = { + primary: { background: 'var(--accent)', color: 'var(--fg-on-accent)' }, + secondary: { background: 'var(--surface)', color: 'var(--fg1)', border: '1.5px solid var(--border-strong)' }, + ghost: { background: 'transparent', color: 'var(--accent)' }, + danger: { background: 'var(--danger-subtle)', color: 'var(--danger)' }, + }[variant]; + return ( + + ); +} + +/* ───────── i18n ───────── + Real product shows ONE language at a time (toggle in settings). + Bilingual specimens only live in the design-system preview cards. */ +const STRINGS = { + brand: { zh: '穿山甲', en: 'Pangolin' }, + online: { zh: '● 在线', en: '● Online' }, + offline: { zh: '○ 离线', en: '○ Offline' }, + capOff: { zh: '未连接 · 轻点连接', en: 'Tap to connect' }, + capConnecting:{ zh: '连接中…', en: 'Connecting…' }, + capOn: { zh: '已连接 · 网络已加密', en: 'Connected · Encrypted' }, + centerOff: { zh: 'TAP', en: 'TAP' }, + download: { zh: '下载', en: 'Down' }, + upload: { zh: '上传', en: 'Up' }, + latency: { zh: '延迟', en: 'Ping' }, + chooseNode: { zh: '选择节点', en: 'Choose server' }, + search: { zh: '搜索国家 / 城市', en: 'Search country / city' }, + tabConnect: { zh: '连接', en: 'Connect' }, + tabServers: { zh: '节点', en: 'Servers' }, + tabStats: { zh: '统计', en: 'Stats' }, + tabMe: { zh: '我的', en: 'Account' }, + connectNow: { zh: '点击连接', en: 'CONNECT' }, + secure: { zh: '已加密', en: 'SECURE' }, + statsTitle: { zh: '使用统计', en: 'Statistics' }, + trafficMonth: { zh: '本月流量', en: 'Traffic' }, + avgPing: { zh: '平均延迟', en: 'Avg ping' }, + durMonth: { zh: '本月时长', en: 'Time' }, + weekTraffic: { zh: '本周流量 (GB)', en: 'This week (GB)' }, + days7: { zh: ['一','二','三','四','五','六','日'], en: ['M','T','W','T','F','S','S'] }, + proMember: { zh: 'PRO 会员', en: 'PRO member' }, + expires: { zh: '有效期至', en: 'Expires' }, + usage: { zh: '本月用量', en: 'Used this month' }, + smartRoute: { zh: '智能分流', en: 'Smart routing' }, + smartRouteSub:{ zh: '海外应用加速,本地直连', en: 'Accelerate overseas apps, keep local direct' }, + killSwitch: { zh: 'Kill Switch', en: 'Kill Switch' }, + killSwitchSub:{ zh: '断线时阻断网络,防止泄露', en: 'Block traffic if the VPN drops' }, + darkAppearance:{ zh: '深色外观', en: 'Dark appearance' }, + stateOn: { zh: '已开启', en: 'On' }, + followLight: { zh: '跟随浅色', en: 'Off' }, + protocol: { zh: '协议', en: 'Protocol' }, + checkUpdate: { zh: '检查更新', en: 'Check for updates' }, + language: { zh: '语言', en: 'Language' }, + + /* auth */ + authTagline: { zh: '极速 · 稳定 · 省心', en: 'Fast · Stable · Effortless' }, + tabLogin: { zh: '登录', en: 'Log in' }, + tabRegister: { zh: '注册', en: 'Sign up' }, + emailLabel: { zh: '邮箱', en: 'Email' }, + emailPh: { zh: '你的邮箱地址', en: 'your@email.com' }, + pwLabel: { zh: '密码', en: 'Password' }, + pwPh: { zh: '输入密码', en: 'Enter password' }, + setPwPh: { zh: '设置登录密码(用于多端登录)', en: 'Set a password (for multi-device login)' }, + codeLabel: { zh: '邮箱验证码', en: 'Verification code' }, + codeSentTo: { zh: '验证码已发送至', en: 'Code sent to' }, + sendCode: { zh: '发送验证码', en: 'Send code' }, + resend: { zh: '重新发送', en: 'Resend' }, + doLogin: { zh: '登录', en: 'Log in' }, + doNext: { zh: '下一步', en: 'Next' }, + doCreate: { zh: '创建账户', en: 'Create account' }, + forgotPw: { zh: '忘记密码?', en: 'Forgot password?' }, + tos: { zh: '继续即代表同意《服务条款》与《隐私政策》', en: 'By continuing you agree to our Terms & Privacy Policy' }, + stepEmail: { zh: '验证邮箱', en: 'Verify email' }, + stepPw: { zh: '设置密码', en: 'Set password' }, + + /* onboarding */ + obSkip: { zh: '跳过', en: 'Skip' }, + obNext: { zh: '下一步', en: 'Next' }, + obAllow: { zh: '允许并继续', en: 'Allow & continue' }, + obStart: { zh: '开始使用', en: 'Get started' }, + ob1Title: { zh: '一键连接全球', en: 'Connect worldwide' }, + ob1Sub: { zh: '80+ 节点,智能选择最快线路,稳定不掉线。', en: '80+ locations, auto-fastest routing, rock-solid.' }, + ob2Title: { zh: '授权网络配置', en: 'Allow network setup' }, + ob2Sub: { zh: '系统会请求添加一个网络配置,用于建立加密隧道。', en: 'iOS will ask to add a network profile to build the encrypted tunnel.' }, + ob3Title: { zh: '安全 · 无日志', en: 'Private by design' }, + ob3Sub: { zh: '端到端加密,我们不记录你的任何浏览数据。', en: 'End-to-end encrypted. We keep zero browsing logs.' }, + + /* redeem & buy */ + redeemEntry: { zh: '兑换 & 购买', en: 'Redeem & buy' }, + redeemTitle: { zh: '兑换 & 购买', en: 'Redeem & buy' }, + redeemCodeTitle:{ zh: '兑换激活码', en: 'Redeem a code' }, + redeemPh: { zh: '输入激活码', en: 'Enter activation code' }, + redeemBtn: { zh: '激活', en: 'Activate' }, + redeemOk: { zh: '激活成功 · PRO 已开通', en: 'Activated · PRO unlocked' }, + buyTitle: { zh: '购买渠道', en: 'Where to buy' }, + buySub: { zh: 'App 内不支持直接支付。请通过以下渠道获取激活码:', en: 'In-app payment is unavailable. Get an activation code via:' }, + chStore: { zh: '自助发卡商店', en: 'Self-serve store' }, + chStoreSub: { zh: '支付宝 / 微信 · 自动发码', en: 'Alipay / WeChat · instant code' }, + chUsdtSub: { zh: '链上转账 · 最隐私 · 自动发码', en: 'On-chain · most private · instant code' }, + chTgSub: { zh: '@PangolinVPN_bot', en: '@PangolinVPN_bot' }, + chLineSub: { zh: '@pangolinvpn', en: '@pangolinvpn' }, + chEmail: { zh: '邮箱', en: 'Email' }, + chEmailSub: { zh: 'buy@pangolin.vpn', en: 'buy@pangolin.vpn' }, + back: { zh: '返回', en: 'Back' }, + replayAuth: { zh: '查看登录 / 注册流程', en: 'View login / sign-up' }, + twoFA: { zh: '双重认证', en: 'Two-factor auth' }, + twoFASub: { zh: 'PRO 专享 · 登录需身份验证器动态码', en: 'PRO only · TOTP code at login' }, + twoFATitle: { zh: '双重认证', en: 'Two-factor auth' }, + twoFAHint: { zh: '你的账户已开启双重认证。请输入身份验证器 App 中的 6 位动态码。', en: 'Two-factor auth is on for this account. Enter the 6-digit code from your authenticator app.' }, + twoFAConfirm: { zh: '验证并登录', en: 'Verify & log in' }, + twoFABack: { zh: '返回上一步', en: 'Back' }, + ucEntry: { zh: 'Web 用户中心', en: 'Web account center' }, + ucEntrySub: { zh: '订阅导入 · 邀请返利', en: 'Subscription import · referral' }, + replayAuthSub:{ zh: '演示用 · 退出到登录页', en: 'Demo · back to login' }, + replayOnboarding: { zh: '查看首次引导', en: 'View onboarding' }, + replayOnboardingSub: { zh: '演示用 · 重看 3 屏引导', en: 'Demo · replay the 3 intro screens' }, + + /* account management (integrated) */ + meTitle: { zh: '我的', en: 'Account' }, + curPlan: { zh: '当前套餐', en: 'Current plan' }, + expires: { zh: '有效期至', en: 'Expires' }, + upgradeBtn: { zh: '续费 / 升级', en: 'Renew / Upgrade' }, + accInfoTitle: { zh: '账户信息', en: 'Account info' }, + accEmail: { zh: '邮箱', en: 'Email' }, + accPassword: { zh: '密码', en: 'Password' }, + accChange: { zh: '修改', en: 'Change' }, + accSignOut: { zh: '退出登录', en: 'Sign out' }, + myDevices: { zh: '我的设备', en: 'My devices' }, + devicesSub: { zh: 'PRO 套餐最多 5 台设备同时在线', en: 'Up to 5 devices on PRO' }, + thisDevice: { zh: '当前设备', en: 'This device' }, + lastActive: { zh: '最近活跃', en: 'Last active' }, + remove: { zh: '移除', en: 'Remove' }, + choosePlan: { zh: '选择套餐', en: 'Choose plan' }, + free: { zh: '免费版', en: 'Free' }, + proPlan: { zh: '专业版', en: 'Pro' }, + team: { zh: '团队版', en: 'Team' }, + perMonth: { zh: '/月', en: '/mo' }, + current: { zh: '当前', en: 'Current' }, + upgrade: { zh: '立即升级', en: 'Upgrade' }, + choose: { zh: '选择', en: 'Choose' }, + mostPopular: { zh: '最受欢迎', en: 'Popular' }, + feFree: { zh: ['仅 1 个基础节点','每日 10 分钟时长','使用前观看广告','注册享 7 天免费试用'], en: ['1 basic node only','10 min per day','Watch an ad to start','7-day free trial on sign-up'] }, + fePro: { zh: ['80+ 全球节点','无限流量 · 极速','5 台设备同时在线','流媒体优化'], en: ['80+ locations','Unlimited · fast','5 devices','Streaming optimized'] }, + feTeam: { zh: ['Pro 全部功能','10 个成员席位','集中计费与管理','优先客服'], en: ['Everything in Pro','10 seats','Central billing','Priority support'] }, + /* contact */ + contactEntry: { zh: '联系我们', en: 'Contact us' }, + contactTitle: { zh: '联系我们', en: 'Contact us' }, + contactIntro: { zh: '遇到问题?通过以下任一渠道联系我们,通常数分钟内回复。', en: 'Need help? Reach us via any channel below — usually replies in minutes.' }, + contactEmail: { zh: '邮箱客服', en: 'Email support' }, + contactStore: { zh: '自助发卡商店', en: 'Self-serve store' }, + contactHoursTitle: { zh: '服务时间', en: 'Support hours' }, + contactHours: { zh: '每日 9:00 – 24:00 (GMT+8)', en: 'Daily 9:00 – 24:00 (GMT+8)' }, + devicesCount: { zh: '台设备', en: 'devices' }, + /* smart select + quota (口径见 CLAUDE.md §7) */ + smartSelect: { zh: '智能选择', en: 'Smart select' }, + smartSub: { zh: '根据当前网络环境,自动选择最优节点', en: 'Picks the best node for your network' }, + recommended: { zh: '推荐', en: 'Recommended' }, + quotaToday: { zh: '今日剩余', en: 'Left today' }, + minutes: { zh: '分钟', en: 'min' }, + quotaFree: { zh: '免费版 · 每日 10 分钟', en: 'Free · 10 min/day' }, + quotaTrial: { zh: '体验期 · 7 天免费使用', en: 'Trial · 7 days free' }, + watchAd: { zh: '看广告开始使用', en: 'Watch ad to start' }, + adUnlocked: { zh: '已解锁 · 今日可用', en: 'Unlocked for today' }, + freePlanName: { zh: '免费版', en: 'Free' }, + upgradeNow: { zh: '升级 PRO', en: 'Go PRO' }, + freeDemo: { zh: '演示:免费版视角', en: 'Demo: free-tier view' }, + freeDemoSub: { zh: '切换连接页的额度展示与套餐横幅', en: 'Toggles quota UI & plan banner' }, +}; +function makeT(lang) { return (k) => (STRINGS[k] && STRINGS[k][lang]) || k; } +/* localized server label */ +function srvName(s, lang) { return lang === 'zh' ? s.name : s.en; } +function srvSub(s, lang) { + const tag = s.tags && s.tags[0]; + if (lang === 'zh') return (tag ? tag : s.en); + return (tag === '流媒体优化' ? 'Streaming' : tag === 'P2P' ? 'P2P' : s.name); +} + +Object.assign(window, { Icon, Mark, CC, Signal, Pill, STRINGS, makeT, srvName, srvSub, SERVERS: [ + { code: 'HK', name: '香港 · 流媒体', en: 'Hong Kong', ping: 18, tags: ['流媒体优化'] }, + { code: 'JP', name: '日本 东京', en: 'Tokyo', ping: 32, tags: ['P2P'] }, + { code: 'SG', name: '新加坡', en: 'Singapore', ping: 54, tags: ['P2P'] }, + { code: 'TW', name: '台湾 台北', en: 'Taipei', ping: 28, tags: [] }, + { code: 'US', name: '美国 洛杉矶', en: 'Los Angeles', ping: 146, tags: ['流媒体优化'] }, + { code: 'DE', name: '德国 法兰克福', en: 'Frankfurt', ping: 198, tags: [] }, + { code: 'UK', name: '英国 伦敦', en: 'London', ping: 210, tags: [] }, + { code: 'KR', name: '韩国 首尔', en: 'Seoul', ping: 41, tags: [] }, +] }); diff --git a/design/ui_kits/mobile/screens.jsx b/design/ui_kits/mobile/screens.jsx new file mode 100644 index 0000000..d9bc97e --- /dev/null +++ b/design/ui_kits/mobile/screens.jsx @@ -0,0 +1,349 @@ +/* screens.jsx — 穿山甲 VPN mobile · screens + bottom tab (i18n) */ + +const { useState, useEffect, useRef } = React; + +/* ───────── Top brand bar ───────── */ +function TopBar({ t, lang, right }) { + return ( +
+
+ + {t('brand')} +
+ {right} +
+ ); +} + +/* ───────── CONNECT screen ───────── */ +function ConnectScreen({ t, lang, status, onToggle, server, smart, free, mins, adDone, onAd, goServers, elapsed }) { + const map = { + off: { fill: 'var(--sand-100)', fg: 'var(--clay-500)', icon: 'power', cap: t('capOff'), ring: 'track' }, + connecting: { fill: 'var(--accent)', fg: '#fff', icon: 'loader-circle', cap: t('capConnecting'), ring: 'spin' }, + on: { fill: 'var(--success)', fg: '#fff', icon: 'shield-check', cap: t('capOn'), ring: 'full' }, + }[status]; + const fmt = (s) => `${String(Math.floor(s/3600)).padStart(2,'0')}:${String(Math.floor(s%3600/60)).padStart(2,'0')}:${String(s%60).padStart(2,'0')}`; + const shadow = status==='off' + ? 'inset 0 0 0 1px var(--sand-200), var(--shadow-md)' + : `0 0 0 9px ${status==='on' ? 'var(--success-subtle)' : 'var(--accent-subtle)'}, var(--shadow-lg)`; + // in-circle center block + const center = status==='off' + ? (<>{t('connectNow')}) + : status==='connecting' + ? (···) + : (<>{fmt(elapsed)}{t('secure')}); + + return ( +
+ {status==='on'?t('online'):t('offline')}} /> + +
+ + +
+
{map.cap}
+
+
+ +
+ {free && ( +
+
+ + {t('quotaToday')} + {mins} {t('minutes')} + {t('quotaFree')} +
+
+
+
+ {adDone ? ( +
+ {t('adUnlocked')} +
+ ) : ( + + )} +
+ )} + {status === 'on' && ( +
+ {[['arrow-down',t('download'),'86.4','Mb/s'],['arrow-up',t('upload'),'12.1','Mb/s'],['zap',t('latency'), String(server.ping),'ms']].map(([ic,l,v,u]) => ( +
+
{l}
+
{v} {u}
+
+ ))} +
+ )} + +
+
+ ); +} + +/* ───────── SERVERS screen ───────── */ +function ServersScreen({ t, lang, current, onPick, q, setQ }) { + const list = SERVERS.filter(s => (s.name + s.en).toLowerCase().includes(q.toLowerCase())); + return ( +
+ +
+
{t('chooseNode')}
+
+ + setQ(e.target.value)} placeholder={t('search')} + style={{ border: 'none', background: 'transparent', outline: 'none', flex: 1, fontFamily: 'var(--font-sans)', fontSize: 15, color: 'var(--fg1)' }} /> +
+
+
+ {/* smart select — recommended card */} + +
+ {list.map((s, i) => { + const active = s.code === current; + return ( + + ); + })} +
+
+
+ ); +} + +/* ───────── SETTINGS screen ───────── */ +function Toggle({ on, onChange }) { + return ( +
+
+
+ ); +} +/* segmented language switch 中 / EN */ +function LangSwitch({ lang, setLang }) { + return ( +
+ {[['zh','中文'],['en','EN']].map(([v,l]) => ( + + ))} +
+ ); +} +function Row({ icon, title, sub, right, last, onClick }) { + return ( +
+ {icon &&
} +
+
{title}
+ {sub &&
{sub}
} +
+ {right} +
+ ); +} +function SettingsScreen({ t, lang, setLang, dark, setDark, free, setFree, onRedeem, onUpgrade, onContact, onSignOut, onReplayAuth, onReplayOnboarding }) { + const [autostart, setAuto] = useState(true); + const [kill, setKill] = useState(true); + const [twoFA, setTwoFA] = useState(true); + const [devices, setDevices] = useState([ + { id: 1, icon: 'laptop', name: 'MacBook Pro', os: 'macOS 26', active: t('thisDevice'), me: true }, + { id: 2, icon: 'smartphone', name: 'iPhone 17', os: 'iOS 26', active: '2 min', me: false }, + { id: 3, icon: 'monitor-smartphone', name: 'iPad Air', os: 'iPadOS 26', active: lang === 'zh' ? '3 小时前' : '3h ago', me: false }, + ]); + const card = { background: 'var(--surface)', borderRadius: 'var(--radius-lg)', boxShadow: 'var(--shadow-sm)', border: '1px solid var(--border)', overflow: 'hidden', marginBottom: 18 }; + return ( +
+ +
+
{t('meTitle')}
+ + {/* plan banner */} +
+
+
+
+
me@pangolin.vpn
+
{free ? t('freePlanName') : t('proMember')}
+
+
+
+
+
{free ? t('quotaToday') : t('expires')}
+
{free ? t('quotaFree') : '2026-12-31 · 42.6 GB'}
+
+ +
+
+ + {/* account info */} +
{t('accInfoTitle')}
+
+ {t('accChange')}} /> + {t('accChange')}} /> + setTwoFA(!twoFA)} />} last /> +
+ + {/* devices */} +
{t('myDevices')}
+
{t('devicesSub')}
+
+ {devices.map((d, i) => ( +
+
+
+
{d.name}{d.me && {d.active}}
+
{d.os}{!d.me && ` · ${d.active}`}
+
+ {!d.me && } +
+ ))} +
+ + {/* redeem + contact */} +
+ } onClick={onRedeem} /> + } onClick={() => window.open('../usercenter/index.html', '_blank')} /> + } last onClick={onContact} /> +
+ + {/* app settings */} +
+ setAuto(!autostart)} />} /> + setKill(!kill)} />} last /> +
+
+ } /> + setDark(!dark)} />} /> + WireGuard} /> + } last /> +
+
+ } onClick={onReplayAuth} /> + } onClick={onReplayOnboarding} /> + setFree(!free)} />} last /> +
+
穿山甲 · v2.4.0
+
+
+ ); +} + +/* ───────── STATS screen ───────── */ +function StatsScreen({ t, lang }) { + const vals = [2.1, 3.4, 1.8, 4.6, 5.2, 6.1, 3.0]; const max = 6.1; const labels = t('days7'); + const card = { background: 'var(--surface)', border: '1px solid var(--border)', borderRadius: 'var(--radius-lg)', boxShadow: 'var(--shadow-sm)' }; + return ( +
+ +
+
{t('statsTitle')}
+
+ {[[t('trafficMonth'), '42.6', 'GB'], [t('avgPing'), '29', 'ms'], [t('durMonth'), '86.4', 'h']].map(([l, v, u]) => ( +
+
{l}
+
{v} {u}
+
+ ))} +
+
+
{t('weekTraffic')}
+
+ {vals.map((v, i) => ( +
+
{v}
+
+
{labels[i]}
+
+ ))} +
+
+
+
+ ); +} + +/* ───────── Bottom tab ───────── */ +function BottomTab({ t, tab, setTab }) { + const items = [['connect','power',t('tabConnect')],['servers','globe',t('tabServers')],['stats','chart',t('tabStats')],['me','user',t('tabMe')]]; + return ( +
+ {items.map(([id, ic, label]) => { + const a = tab === id; + return ( + + ); + })} +
+ ); +} + +Object.assign(window, { ConnectScreen, ServersScreen, StatsScreen, SettingsScreen, BottomTab, LangSwitch }); diff --git a/design/ui_kits/tablet/README.md b/design/ui_kits/tablet/README.md new file mode 100644 index 0000000..912c490 --- /dev/null +++ b/design/ui_kits/tablet/README.md @@ -0,0 +1,19 @@ +# 穿山甲 — iPad 客户端 UI Kit + +iPad 横屏(1180×820)分栏布局的高保真可点击原型。介于手机与桌面之间:**左侧栏导航(触控尺寸,行高 ≥48px)+ 内容区**。 + +## 运行 +打开 `index.html`。可交互:连接键三态、智能选择推荐卡(默认选中)、免费版额度卡(看广告解锁)、节点双列网格、统计、账户(语言/深色/免费版视角切换)。 + +## 文件 +| 文件 | 内容 | +|---|---| +| `index.html` | 入口(iPad 外壳 + 状态栏) | +| `tabapp.jsx` | `TabletApp` 外壳 + `TabRail` 侧栏 + 连接/节点/统计/账户四视图 | +| `../mobile/parts.jsx` | 复用:`Icon`/`Mark`/`CC`/`Signal`/`SERVERS`/`makeT` 与共享字串 | + +## 关键模式 +- **连接页双栏**:左=大连接按钮;右=信息列(额度卡 → 当前节点 → 实时速率)。 +- 节点页:置顶智能推荐卡 + 双列网格(同桌面)。 +- 全部走语义 token,双主题双语;Flutter 端建议宽度 ≥900 时从底部 Tab 切换为此侧栏布局。 +- 注意:交互态过渡只写 `background-color`/`color`,**不要 `transition: all`**(背景变量切换会失效,CLAUDE.md §5 有记录)。 diff --git a/design/ui_kits/tablet/index.html b/design/ui_kits/tablet/index.html new file mode 100644 index 0000000..179da8d --- /dev/null +++ b/design/ui_kits/tablet/index.html @@ -0,0 +1,28 @@ + + + + + + +穿山甲 · iPad 客户端 + + + + + + + +
+ + + + + diff --git a/design/ui_kits/tablet/tabapp.jsx b/design/ui_kits/tablet/tabapp.jsx new file mode 100644 index 0000000..880348f --- /dev/null +++ b/design/ui_kits/tablet/tabapp.jsx @@ -0,0 +1,368 @@ +/* tabapp.jsx — 穿山甲 iPad 客户端 · 分栏布局 + 复用 ../mobile/parts.jsx 的 Icon/Mark/CC/Signal/SERVERS/makeT/srvName/srvSub。 + 仅 iPad 专属字串定义在本文件 TSTR。 */ + +const { useState, useEffect, useRef } = React; + +/* iPad 专属字串(双语单显) */ +const TSTR = { + navConnect: { zh: '连接', en: 'Connect' }, + navNodes: { zh: '节点', en: 'Nodes' }, + navStats: { zh: '统计', en: 'Stats' }, + navAccount: { zh: '账户', en: 'Account' }, + capOff: { zh: '未连接 · 轻点连接', en: 'Not connected · tap to start' }, + capConnecting:{ zh: '连接中 Connecting…', en: 'Connecting…' }, + capOn: { zh: '已连接 · 网络已加密', en: 'Connected · traffic encrypted' }, + download: { zh: '下载', en: 'Down' }, + upload: { zh: '上传', en: 'Up' }, + latency: { zh: '延迟', en: 'Ping' }, + curNode: { zh: '当前节点', en: 'Current node' }, + searchPh: { zh: '搜索国家 / 城市', en: 'Search country / city' }, + statTraffic:{ zh: '本月流量', en: 'Data this month' }, + statPing: { zh: '平均延迟', en: 'Avg latency' }, + statTime: { zh: '本月时长', en: 'Time this month' }, + weekTraffic:{ zh: '本周流量 (GB)', en: 'This week (GB)' }, + days: { zh: ['周一','周二','周三','周四','周五','周六','周日'], en: ['Mon','Tue','Wed','Thu','Fri','Sat','Sun'] }, + acctEmail: { zh: '邮箱', en: 'Email' }, + language: { zh: '语言', en: 'Language' }, + darkMode: { zh: '深色外观', en: 'Dark appearance' }, + smartRoute: { zh: '智能分流', en: 'Smart routing' }, + smartRouteSub:{ zh: '海外应用走加速,国内直连', en: 'Accelerate abroad, direct at home' }, + killSwitch: { zh: 'Kill Switch', en: 'Kill Switch' }, + killSub: { zh: '断线时阻断网络,防止泄露', en: 'Block traffic if the link drops' }, + freeBanner: { zh: '免费版', en: 'Free plan' }, + trialNote: { zh: '升级 PRO:不限时长 · 80+ 加速线路 · 5 台设备', en: 'Go PRO: unlimited time · 80+ routes · 5 devices' }, + version: { zh: '穿山甲 · iPadOS · v2.4.0', en: 'Pangolin · iPadOS · v2.4.0' }, +}; +function makeTT(lang) { return (k) => (TSTR[k] && TSTR[k][lang]) !== undefined ? TSTR[k][lang] : k; } + +function fmtHMS(s) { return `${String(Math.floor(s/3600)).padStart(2,'0')}:${String(Math.floor(s%3600/60)).padStart(2,'0')}:${String(s%60).padStart(2,'0')}`; } + +/* ── 左侧栏 ── */ +function TabRail({ tt, view, setView, free, t }) { + const items = [['connect','power','navConnect'],['nodes','globe','navNodes'],['stats','chart','navStats'],['account','user','navAccount']]; + return ( +
+
+ +
+
穿山甲
+
PANGOLIN
+
+
+
+ {items.map(([id, ic, lk]) => { + const a = view === id; + return ( + + ); + })} +
+
+
+
+
+ +
+
+
{free ? tt('freeBanner') : t('proMember')}
+
me@pangolin.vpn
+
+
+
+
+ ); +} + +/* ── 连接视图:左大按钮 + 右信息列 ── */ +function TabConnect({ t, tt, lang, status, onToggle, server, smart, free, mins, adDone, onAd, elapsed, goNodes }) { + const map = { + off: { ring: 'var(--border)', fill: 'var(--surface)', fg: 'var(--fg2)', icon: 'power', label: 'TAP', cap: tt('capOff') }, + connecting: { ring: 'var(--accent-subtle)', fill: 'var(--accent)', fg: '#fff', icon: 'loader-circle', label: '···', cap: tt('capConnecting') }, + on: { ring: 'var(--success-subtle)', fill: 'var(--success)', fg: '#fff', icon: 'shield-check', label: 'SAFE', cap: tt('capOn') }, + }[status]; + const card = { background: 'var(--surface)', border: '1px solid var(--border)', borderRadius: 'var(--radius-lg)', boxShadow: 'var(--shadow-sm)' }; + return ( +
+
+ +
+
{map.cap}
+ {status === 'on' &&
{fmtHMS(elapsed)}
} +
+
+ +
+ {free && ( +
+
+ + {t('quotaToday')} + {mins} {t('minutes')} + {t('quotaFree')} +
+
+
+
+ {adDone ? ( +
+ {t('adUnlocked')} +
+ ) : ( + + )} +
+ )} + + + + {status === 'on' && ( +
+ {[['arrow-down', tt('download'), '86.4', 'Mb/s'], ['arrow-up', tt('upload'), '12.1', 'Mb/s'], ['zap', tt('latency'), String(server.ping), 'ms']].map(([ic, l, v, u]) => ( +
+
{l}
+
{v} {u}
+
+ ))} +
+ )} +
+
+ ); +} + +/* ── 节点视图 ── */ +function TabNodes({ t, tt, lang, current, onPick }) { + const [q, setQ] = useState(''); + const list = SERVERS.filter(s => (s.name + s.en).toLowerCase().includes(q.toLowerCase())); + const smartActive = current === 'AUTO'; + return ( +
+ + +
+ + setQ(e.target.value)} placeholder={tt('searchPh')} style={{ border: 'none', background: 'transparent', outline: 'none', flex: 1, fontFamily: 'var(--font-sans)', fontSize: 14.5, color: 'var(--fg1)' }} /> +
+ +
+ {list.map(s => { + const active = s.code === current; + return ( + + ); + })} +
+
+ ); +} + +/* ── 统计视图 ── */ +function TabStats({ tt, lang }) { + const vals = [2.1, 3.4, 1.8, 4.6, 5.2, 6.1, 3.0]; + const days = TSTR.days[lang]; + const max = Math.max(...vals); + const card = { background: 'var(--surface)', border: '1px solid var(--border)', borderRadius: 'var(--radius-lg)', boxShadow: 'var(--shadow-sm)' }; + return ( +
+
+ {[[tt('statTraffic'), '42.6 GB'], [tt('statPing'), '29 ms'], [tt('statTime'), '86.4 h']].map(([l, v]) => ( +
+
{l}
+
{v}
+
+ ))} +
+
+
{tt('weekTraffic')}
+
+ {vals.map((v, i) => ( +
+
{v}
+
+
{days[i]}
+
+ ))} +
+
+
+ ); +} + +/* ── 账户视图 ── */ +function TabToggle({ on, onChange }) { + return ( +
+
+
+ ); +} +function TabAccount({ t, tt, lang, setLang, dark, setDark, free, setFree }) { + const [smartRoute, setSmartRoute] = useState(true); + const [kill, setKill] = useState(true); + const card = { background: 'var(--surface)', border: '1px solid var(--border)', borderRadius: 'var(--radius-lg)', boxShadow: 'var(--shadow-sm)', overflow: 'hidden', marginBottom: 16 }; + const Row = ({ icon, title, sub, right, last }) => ( +
+ {icon &&
} +
+
{title}
+ {sub &&
{sub}
} +
+ {right} +
+ ); + const seg = (val) => ({ + border: 'none', cursor: 'pointer', padding: '7px 16px', borderRadius: 'var(--radius-full)', fontFamily: 'var(--font-sans)', fontSize: 13, fontWeight: 600, minHeight: 32, + background: lang === val ? 'var(--surface)' : 'transparent', color: lang === val ? 'var(--fg1)' : 'var(--fg3)', + boxShadow: lang === val ? 'var(--shadow-sm)' : 'none', + }); + return ( +
+
+
+
+
+
me@pangolin.vpn
+
{free ? tt('freeBanner') : t('proMember')}
+
+
+
+ {free ? tt('trialNote') : '2026-12-31 · 42.6 GB'} +
+
+ +
+ setSmartRoute(!smartRoute)} />} /> + setKill(!kill)} />} last /> +
+
+ + + +
+ } /> + setDark(!dark)} />} /> + setFree(!free)} />} last /> +
+
{tt('version')}
+
+ ); +} + +/* ── iPad 外壳 + 应用 ── */ +function TabletApp() { + const [view, setView] = useState('connect'); + const [status, setStatus] = useState('off'); + const [code, setCode] = useState('AUTO'); + const [lang, setLang] = useState('zh'); + const [dark, setDark] = useState(false); + const [free, setFree] = useState(true); + const [mins, setMins] = useState(6); + const [adDone, setAdDone] = useState(false); + const [elapsed, setElapsed] = useState(0); + const timer = useRef(null); + const t = makeT(lang), tt = makeTT(lang); + + const smart = code === 'AUTO'; + const server = smart ? SERVERS.reduce((a, b) => (a.ping < b.ping ? a : b)) : SERVERS.find(s => s.code === code); + const titles = { connect: tt('navConnect'), nodes: tt('navNodes'), stats: tt('navStats'), account: tt('navAccount') }; + + function toggle() { + if (status === 'off') { setStatus('connecting'); setTimeout(() => { setStatus('on'); setElapsed(0); }, 1500); } + else setStatus('off'); + } + function pick(c) { setCode(c); setView('connect'); if (status === 'on') { setStatus('connecting'); setTimeout(() => { setStatus('on'); setElapsed(0); }, 1200); } } + useEffect(() => { if (status === 'on') timer.current = setInterval(() => setElapsed(e => e + 1), 1000); return () => clearInterval(timer.current); }, [status]); + + let main; + if (view === 'connect') main = setAdDone(true)} elapsed={elapsed} goNodes={() => setView('nodes')} />; + else if (view === 'nodes') main = ; + else if (view === 'stats') main = ; + else main = ; + + return ( +
+
+ {/* status bar */} +
+ 9:41 +
+ + 87% +
+
+
+
+ +
+
+
{titles[view]}
+ {status === 'on' ? '● ' + server.code : '○'} +
+
{main}
+
+
+
+
+ ); +} + +Object.assign(window, { TabletApp }); diff --git a/design/ui_kits/usercenter/README.md b/design/ui_kits/usercenter/README.md new file mode 100644 index 0000000..c364045 --- /dev/null +++ b/design/ui_kits/usercenter/README.md @@ -0,0 +1,22 @@ +# 穿山甲 — Web 用户中心 UI Kit + +phase-1 网页闭环(登录 → 取订阅 → 三方导入 → 兑换)的高保真原型;phase-3 自研后端时作为前端蓝本。中英单语言切换。 + +## 页面 +- **登录**(`UCLogin`):邮箱 + 密码卡片;注册引导去 App(对齐"注册在 App 内完成"的流程)。从顶栏「退出」可回看。 +- **概览**(`UCOverview`):套餐横幅(续费/升级)、今日时长/流量/设备指标卡、近 7 日用量图、快速操作。 +- **订阅**(`UCSub`)——核心页:订阅链接(复制/重置,重置提示旧链接失效)、扫码导入占位、**一键导入三方客户端**(穿山甲 App / Shadowrocket / Clash Verge / v2rayN / sing-box),注明一条链接多格式兼容。 +- **兑换 & 购买**(`UCRedeem`):激活码兑换 + 渠道卡(自助发卡 / **USDT TRC20** / Telegram / LINE / 邮箱)。 +- **邀请返利**(`UCInvite`):邀请链接复制、已邀请/已付费/累计返利指标、邀请记录表。 + +## 文件 +| 文件 | 内容 | +|---|---| +| `index.html` | 挂载 `UserCenterApp`(默认进概览;顶栏可登出回登录页) | +| `ucparts.jsx` | `UCIcon`(内联 Lucide)/ `UCMark`(穿山甲标)/ `UCSTRINGS` i18n | +| `ucapp.jsx` | 全部视图 + 顶栏导航壳 | + +## 备注 +- 套餐口径引用根 `CLAUDE.md` §7;渠道为演示占位。 +- 二维码为占位框(落地由后端按订阅链接生成)。 +- 设备管理/账户信息已集成在移动与桌面客户端,本站不重复(职责:订阅分发 + 兑换 + 裂变)。 diff --git a/design/ui_kits/usercenter/index.html b/design/ui_kits/usercenter/index.html new file mode 100644 index 0000000..ba8e4c9 --- /dev/null +++ b/design/ui_kits/usercenter/index.html @@ -0,0 +1,29 @@ + + + + + + +穿山甲 · 用户中心 + + + + + + + +
+ + + + + diff --git a/design/ui_kits/usercenter/mobile-preview.html b/design/ui_kits/usercenter/mobile-preview.html new file mode 100644 index 0000000..6d94ba4 --- /dev/null +++ b/design/ui_kits/usercenter/mobile-preview.html @@ -0,0 +1,13 @@ + + + + + + +穿山甲 · 用户中心移动端预览 + + + + + + diff --git a/design/ui_kits/usercenter/ucapp.jsx b/design/ui_kits/usercenter/ucapp.jsx new file mode 100644 index 0000000..e9e69e3 --- /dev/null +++ b/design/ui_kits/usercenter/ucapp.jsx @@ -0,0 +1,367 @@ +/* ucapp.jsx — 穿山甲 Web 用户中心 · views */ +const { useState: useStateU } = React; + +const ucCard = { background:'var(--surface)', border:'1px solid var(--border)', borderRadius:'var(--radius-xl)', boxShadow:'var(--shadow-sm)' }; +const ucInput = { border:'1.5px solid var(--border-strong)', borderRadius:'var(--radius-md)', padding:'12px 14px', fontFamily:'var(--font-sans)', fontSize:14.5, color:'var(--fg1)', background:'var(--surface)', outline:'none', width:'100%', boxSizing:'border-box' }; + +/* viewport hook — mobile ≤ 700px */ +function useIsMobileUC() { + const [m, setM] = useStateU(typeof window !== 'undefined' && window.innerWidth <= 700); + React.useEffect(() => { + const f = () => setM(window.innerWidth <= 700); + window.addEventListener('resize', f); + return () => window.removeEventListener('resize', f); + }, []); + return m; +} + +function UCLang({ lang, setLang }) { + return
+ {[['zh','中文'],['en','EN']].map(([v,l])=>( + + ))}
; +} + +/* ───────── Login (+ PRO 2FA step) ───────── */ +function UCLogin({ t, lang, setLang, onDone }) { + const [email,setEmail]=useStateU(''); + const [pw,setPw]=useStateU(''); + const [step,setStep]=useStateU('cred'); // cred | totp + const [otp,setOtp]=useStateU(''); + const ok=/\S+@\S+\.\S+/.test(email)&&pw.length>0; + return ( +
+
+
+
+ +
+
穿山甲
+
PANGOLIN
+
+
+ +
+ {step==='cred' ? (<> +
{t('loginTitle')}
+
{t('loginSub')}
+ + +
{t('forgotPw')}
+ +
{t('noAccount')}
+ ) : (<> +
+ + {t('twoFATitle')} + PRO +
+
{t('twoFAHint')}
+ setOtp(e.target.value.replace(/\D/g,'').slice(0,6))} placeholder="······" + style={{ ...ucInput, fontFamily:'var(--font-mono)', fontSize:22, letterSpacing:'0.5em', textAlign:'center', padding:'14px' }} /> + +
setStep('cred')} style={{ textAlign:'center', fontSize:12.5, color:'var(--fg3)', marginTop:18, cursor:'pointer' }}>{t('twoFABack')}
+ )} +
+
+ ); +} + +/* ───────── Overview ───────── */ +function UCOverview({ t, lang, free, mobile, goSub, goRedeem }) { + const vals=[1.8,2.4,1.2,3.1,4.0,5.2,2.6]; const max=5.2; + const labels = lang==='zh'?['一','二','三','四','五','六','日']:['M','T','W','T','F','S','S']; + return ( +
+
+
{t('greeting')}
+
me@pangolin.vpn
+
+ {/* plan banner */} +
+
+
+
+
{t('curPlan')}
+
{free?t('freePlan'):t('proMember')}
+
{free?t('quotaFree'):`${t('expires')} 2026-12-31`}
+
+
+ +
+ {/* stats */} +
+ {[['clock',t('quotaToday'), free?'6':'∞', free?'min':''],['arrow-down',t('dataToday'),'0.8','GB'],['smartphone',t('devices'),'2 / 5','']].map(([ic,l,v,u])=>( +
+
{l}
+
{v}{u&& {u}}
+
+ ))} +
+ {/* usage chart + quick actions */} +
+
+
{t('usageTitle')}
+
+ {vals.map((v,i)=>( +
+
{v}
+
+
{labels[i]}
+
+ ))} +
+
+
+
{t('quickSub')}
+
+ {[['link',t('qaSub'),goSub],['ticket',t('qaRedeem'),goRedeem],['download',t('qaApp'),null]].map(([ic,l,fn])=>( + + ))} +
+
+
+
+ ); +} + +/* ───────── Subscription ───────── */ +function UCSub({ t, lang, mobile }) { + const [copied,setCopied]=useStateU(false); + const [reset,setReset]=useStateU(false); + const url='https://sub.pangolin.vpn/s/8f3kx92m' + (reset?'-new':''); + const clients=[ + { name:'穿山甲 App', sub:'iOS / Android / 桌面', icon:'shield-check', accent:true }, + { name:'Shadowrocket', sub:'iOS', icon:'external-link' }, + { name:'Clash Verge', sub:'Windows / macOS', icon:'external-link' }, + { name:'v2rayN', sub:'Windows', icon:'external-link' }, + { name:'sing-box', sub:'全平台', icon:'external-link' }, + ]; + return ( +
+
+
{t('subTitle')}
+
{t('subDesc')}
+
+
+ {/* link card */} +
+
+ + {url} +
+
+ + +
+
+ {reset&&}{reset?t('subResetOk'):t('subResetSub')} +
+
+ {/* QR */} +
+
+ + {t('scanTitle')} +
+
{t('scanSub')}
+
+
+ {/* one-tap import */} +
+
{t('importTitle')}
+
{t('importSub')}
+
+ {clients.map(c=>( + + ))} +
+
{t('fmtNote')}
+
+
+ ); +} + +/* ───────── Redeem ───────── */ +function UCRedeem({ t, lang }) { + const [code,setCode]=useStateU(''); + const [ok,setOk]=useStateU(false); + const channels=[ + { icon:'shopping-bag', name:t('chStore'), sub:t('chStoreSub'), accent:true }, + { icon:'credit-card', name:'USDT (TRC20)', sub:t('chUsdtSub') }, + { icon:'send', name:'Telegram', sub:'@PangolinVPN_bot', mono:true }, + { icon:'message-circle', name:'LINE', sub:'@pangolinvpn', mono:true }, + { icon:'mail', name:t('chEmail'), sub:'buy@pangolin.vpn', mono:true }, + ]; + return ( +
+
{t('navRedeem')}
+
+
{t('redeemTitle')}
+ {ok ? ( +
{t('redeemOk')}
+ ) : ( +
+ setCode(e.target.value.toUpperCase())} placeholder={t('redeemPh')} style={{ ...ucInput, fontFamily:'var(--font-mono)', letterSpacing:'0.08em' }} /> + +
+ )} +
+
+
{t('buyTitle')}
+
{t('buySub')}
+
+ {channels.map(c=>( + + ))} +
+
+
+ ); +} + +/* ───────── Invite ───────── */ +function UCInvite({ t, lang }) { + const [copied,setCopied]=useStateU(false); + const records=[ + { mail:'k***@gmail.com', date:'2026-06-08', paid:true }, + { mail:'w***@outlook.com', date:'2026-06-05', paid:false }, + { mail:'z***@qq.com', date:'2026-05-30', paid:true }, + ]; + return ( +
+
+
{t('inviteTitle')}
+
{t('inviteSub')}
+
+
+
{t('inviteLink')}
+
+
+ + https://pangolin.vpn/r/PG8F3K +
+ +
+
+
+ {[[t('invited'),'8',''],[t('paidUsers'),'3',''],[t('earned'),'¥45','']].map(([l,v])=>( +
+
{l}
+
{v}
+
+ ))} +
+
+
{t('inviteRecord')}
+ {records.map((r,i)=>( +
+ {r.mail} + {r.date} + {r.paid?t('recPaid'):t('recRegistered')} +
+ ))} +
+
+ ); +} + +/* ───────── App shell ───────── */ +function UserCenterApp() { + const [stage,setStage]=useStateU('app'); // app | login — opens on the dashboard + const [view,setView]=useStateU('overview'); + const [lang,setLang]=useStateU('zh'); + const [free]=useStateU(false); + const [dir,setDir]=useStateU(0); // -1 左进 / 1 右进 / 0 无动画 + const mobile=useIsMobileUC(); + const t=ucT(lang); + const touchRef=React.useRef({x:0,y:0}); + const ORDER=['overview','sub','redeem','invite']; + + function go(id){ const a=ORDER.indexOf(view), b=ORDER.indexOf(id); setDir(b>a?1:bsetDir(0),300); } + function onTouchStart(e){ const p=e.touches?e.touches[0]:e; touchRef.current={x:p.clientX,y:p.clientY}; } + function onTouchEnd(e){ + const p=e.changedTouches?e.changedTouches[0]:e; + const dx=p.clientX-touchRef.current.x, dy=p.clientY-touchRef.current.y; + if(Math.abs(dx)>60 && Math.abs(dx)>Math.abs(dy)*1.5){ + const i=ORDER.indexOf(view); + const ni=dx<0?Math.min(ORDER.length-1,i+1):Math.max(0,i-1); + if(ni!==i) go(ORDER[ni]); + } + } + + if(stage==='login') return setStage('app')} />; + + const nav=[['overview','layout-dashboard',t('navOverview')],['sub','link',t('navSub')],['redeem','ticket',t('navRedeem')],['invite','users',t('navInvite')]]; + let main; + if(view==='overview') main=setView('sub')} goRedeem={()=>setView('redeem')} />; + else if(view==='sub') main=; + else if(view==='redeem') main=; + else main=; + + const navBtns = nav.map(([id,ic,l])=>( + + )); + + return ( +
+ {/* top bar */} +
+
+
+ + 穿山甲 +
+ {!mobile && } + {mobile &&
} + + +
+ {mobile && } +
+
+
setDir(0)} style={{ animation: dir!==0 ? `uc-in-${dir===1?'l':'r'} 200ms var(--ease-out)` : 'none' }}>{main}
+
+
+ ); +} +Object.assign(window, { UserCenterApp }); diff --git a/design/ui_kits/usercenter/ucparts.jsx b/design/ui_kits/usercenter/ucparts.jsx new file mode 100644 index 0000000..94884d7 --- /dev/null +++ b/design/ui_kits/usercenter/ucparts.jsx @@ -0,0 +1,96 @@ +/* ucparts.jsx — 穿山甲 Web 用户中心 · atoms + i18n */ +const UCLUCIDE = { + 'layout-dashboard':'', + link:'', + ticket:'', + users:'', + copy:'', + 'refresh-cw':'', + 'qr-code':'', + download:'', + check:'', + 'check-circle':'', + zap:'', + clock:'', + 'shield-check':'', + crown:'', + 'credit-card':'', + send:'', + 'message-circle':'', + mail:'', + 'shopping-bag':'', + 'log-out':'', + 'external-link':'', + gift:'', + smartphone:'', + lock:'', + 'chevron-right':'', + 'arrow-down':'', +}; +function UCIcon({ name, size=18, stroke=2, color='currentColor', style={} }) { + return ; +} +function UCMark({ size=30, color='var(--accent)' }) { + return ( + + + + + + + + ); +} + +const UCSTRINGS = { + navOverview:{zh:'概览',en:'Overview'}, navSub:{zh:'订阅',en:'Subscription'}, navRedeem:{zh:'兑换 & 购买',en:'Redeem & buy'}, navInvite:{zh:'邀请返利',en:'Referral'}, + signOut:{zh:'退出',en:'Sign out'}, + /* login */ + loginTitle:{zh:'登录用户中心',en:'Log in to your account'}, loginSub:{zh:'管理订阅、兑换激活码、查看用量',en:'Manage subscription, redeem codes, track usage'}, + emailLabel:{zh:'邮箱',en:'Email'}, emailPh:{zh:'你的邮箱地址',en:'your@email.com'}, + pwLabel:{zh:'密码',en:'Password'}, pwPh:{zh:'输入密码',en:'Enter password'}, + doLogin:{zh:'登录',en:'Log in'}, forgotPw:{zh:'忘记密码?',en:'Forgot password?'}, + noAccount:{zh:'没有账户?在 App 内注册',en:'No account? Sign up in the app'}, + /* overview */ + greeting:{zh:'欢迎回来',en:'Welcome back'}, + curPlan:{zh:'当前套餐',en:'Current plan'}, freePlan:{zh:'免费版',en:'Free'}, proMember:{zh:'PRO 会员',en:'PRO member'}, + expires:{zh:'有效期至',en:'Expires'}, renew:{zh:'续费 / 升级',en:'Renew / Upgrade'}, + quotaToday:{zh:'今日剩余时长',en:'Time left today'}, dataToday:{zh:'今日已用流量',en:'Data used today'}, devices:{zh:'在线设备',en:'Devices online'}, + quotaFree:{zh:'免费版 · 每日 10 分钟',en:'Free · 10 min/day'}, + usageTitle:{zh:'近 7 日流量 (GB)',en:'Last 7 days (GB)'}, + quickSub:{zh:'快速操作',en:'Quick actions'}, + qaSub:{zh:'获取订阅链接',en:'Get subscription'}, qaRedeem:{zh:'兑换激活码',en:'Redeem a code'}, qaApp:{zh:'下载 App',en:'Download apps'}, + /* subscription */ + subTitle:{zh:'我的订阅',en:'My subscription'}, + subDesc:{zh:'订阅链接是你的专属凭证,泄露后他人可使用你的额度。请勿分享。',en:'Your subscription link is a private credential. Never share it.'}, + subCopy:{zh:'复制链接',en:'Copy link'}, subCopied:{zh:'已复制',en:'Copied'}, + subReset:{zh:'重置链接',en:'Reset link'}, subResetSub:{zh:'旧链接立即失效,所有设备需重新导入',en:'Old link stops working; re-import on all devices'}, + subResetOk:{zh:'已重置,请重新导入',en:'Reset — re-import on your devices'}, + scanTitle:{zh:'扫码导入',en:'Scan to import'}, scanSub:{zh:'用穿山甲 App 或三方客户端扫码',en:'Scan with the Pangolin app or a 3rd-party client'}, + importTitle:{zh:'一键导入三方客户端',en:'One-tap import'}, + importSub:{zh:'已安装对应客户端时,点击即自动导入订阅。',en:'If the client is installed, tapping imports the subscription automatically.'}, + fmtNote:{zh:'同一链接同时兼容 sing-box / Clash.Meta / v2ray 格式。',en:'One link serves sing-box / Clash.Meta / v2ray formats.'}, + /* redeem */ + redeemTitle:{zh:'兑换激活码',en:'Redeem a code'}, redeemPh:{zh:'输入激活码',en:'Enter activation code'}, redeemBtn:{zh:'激活',en:'Redeem'}, + redeemOk:{zh:'激活成功 · 套餐已到账',en:'Activated — plan applied'}, + buyTitle:{zh:'购买渠道',en:'Where to buy'}, + buySub:{zh:'本站不直接收款。通过以下渠道购买激活码,回到本页兑换即可:',en:'We never take payment on this site. Buy a code via a channel below, then redeem here:'}, + chStore:{zh:'自助发卡商店',en:'Self-serve store'}, chStoreSub:{zh:'支付宝 / 微信 · 自动发码',en:'Alipay / WeChat · instant code'}, + chUsdtSub:{zh:'链上转账 · 最隐私 · 自动发码',en:'On-chain · most private · instant code'}, + chEmail:{zh:'邮箱客服',en:'Email support'}, + /* invite */ + inviteTitle:{zh:'邀请返利',en:'Referral'}, + inviteSub:{zh:'好友通过你的链接注册并付费,你获得其首单 20% 余额返利,可抵扣续费。',en:'When a friend signs up and pays via your link, you earn 20% of their first order as credit.'}, + inviteLink:{zh:'我的邀请链接',en:'My invite link'}, + invited:{zh:'已邀请',en:'Invited'}, paidUsers:{zh:'已付费',en:'Converted'}, earned:{zh:'累计返利',en:'Credit earned'}, + inviteRecord:{zh:'邀请记录',en:'History'}, recEmpty:{zh:'暂无邀请记录',en:'No referrals yet'}, + recRegistered:{zh:'已注册',en:'Registered'}, recPaid:{zh:'已付费',en:'Paid'}, + /* 2FA */ + twoFATitle:{zh:'双重认证',en:'Two-factor auth'}, + twoFAHint:{zh:'你的账户已开启双重认证。请输入身份验证器 App 中的 6 位动态码。',en:'Two-factor auth is on for this account. Enter the 6-digit code from your authenticator app.'}, + twoFAConfirm:{zh:'验证并登录',en:'Verify & log in'}, + twoFABack:{zh:'返回上一步',en:'Back'}, +}; +function ucT(lang){ return k => { const v=UCSTRINGS[k]; return v?(v[lang]||k):k; }; } + +Object.assign(window, { UCIcon, UCMark, UCSTRINGS, ucT }); diff --git a/design/ui_kits/website/README.md b/design/ui_kits/website/README.md new file mode 100644 index 0000000..ca35f8a --- /dev/null +++ b/design/ui_kits/website/README.md @@ -0,0 +1,29 @@ +# 穿山甲 VPN — 官网宣传页 UI Kit + +品牌官网落地页,沿用暖大地色设计系统。含 **产品 / 定价 / 下载 / 文档 / Blog** 全部板块,定价结构参考 Bitwarden(分档 + 功能对比表)但配色保持 clay 暖调。 + +## 运行 +打开 `index.html`(响应式:≤980px 单列堆叠,>980px 桌面两列/多列)。 +- **顶栏**:品牌标 + 锚点导航(产品/定价/下载/文档/Blog)+ 中/EN 切换 + 登录 + 立即下载;滚动时毛玻璃 + 描边。 +- **Hero**:大标题 + 连接卡(success 态 orb + 速率 + 当前节点)+ 平台条。 +- **产品**:6 个功能卡(一键连接/智能分流/80+节点/无日志/Kill Switch/多端同步)。 +- **定价**:月/年切换(年付 -20%)+ 3 档套餐(专业版 clay 渐变高亮「最受欢迎」)+ **为何无支付按钮**说明 + 渠道 chips + 功能对比表。 +- **下载**:5 平台卡(iOS/Android/macOS/Windows/Linux)。 +- **文档**:4 个文档入口(快速开始/常见问题/协议安全/隐私政策)。 +- **Blog**:3 篇文章卡。 +- **CTA band** + **页脚**(渠道/资源/服务时间)。 + +## 文件 +| 文件 | 内容 | +|---|---| +| `index.html` | 页面结构(语义化 + `data-i18n` 键) | +| `website.css` | 全部样式,基于 `../../colors_and_type.css` 令牌 | +| `site.js` | i18n 字典(中/EN 全量切换,持久化)+ 月/年定价切换 + 滚动毛玻璃 | + +## 关键模式 +- 颜色/字体/圆角/阴影全部走设计系统语义 token;暖大地色,无蓝紫。 +- **单语言显示**:中/EN 通过顶栏段控整页切换(`data-i18n` + JS 字典),不并排。 +- **App 内无支付** 原则延伸到官网:定价区不放支付表单,CTA 为「获取激活码」+ 外部渠道(自助发卡/Telegram/LINE/邮箱),并有显式说明。 +- 连接卡 orb 复用 App 核心连接键的 success 态视觉(满白环 + 盾勾 + 计时 + 绿光晕)。 +- 图标 Lucide(CDN);品牌标内联 SVG(行走穿山甲,与全系统一致)。 +- 演示渠道/价格为占位,落地替换。 diff --git a/design/ui_kits/website/index.html b/design/ui_kits/website/index.html new file mode 100644 index 0000000..47476d6 --- /dev/null +++ b/design/ui_kits/website/index.html @@ -0,0 +1,378 @@ + + + + + + +穿山甲 · Pangolin — 极速 · 稳定 · 省心 + + + + + + + +
+
+ + 新用户注册即享 7 天免费试用 + 立即注册 → + +
+
+ + +
+
+ + + 穿山甲 + + +
+
+ + +
+ 登录 + 立即下载 + +
+
+ + +
+ + + + +
+
+
+
极速 · 稳定 · 省心
+

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

+

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

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

为日常而生的连接体验

+

没有复杂配置,没有技术门槛。打开就用,把安全留给我们。

+
+
+
+

一键连接

+

智能分流

+

80+ 全球加速线路

+

严格无日志

+

Kill Switch

+

多端同步

+
+
+
+ + +
+
+
免费注册
+

注册一次,处处可用

+

一个邮箱就能开始。注册免费,不需要任何付款信息。

+
+
+
+

7 天免费试用

新用户注册即享 7 天免费使用,无需任何付款信息。

+

多端同步

手机、平板、电脑共用一个账户,套餐与设置随身走。

+

无需付款信息

注册只要邮箱。想升级时,再通过外部渠道兑换激活码。

+
+
+
1填写邮箱
+ +
2输入验证码
+ +
3开始畅连
+
+ +
+
+ + +
+
+
定价
+

简单透明的价格

+

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

+
+ + +
+
+
+
+ +
+
免费版
+
¥0/月
+
适合轻度使用与尝鲜
+
    +
  • 每日 10 分钟时长
  • +
  • 仅 1 个基础节点
  • +
  • 使用前观看广告
  • +
  • 核心加密 · 无日志
  • +
+ +
+ +
+
最受欢迎
+
专业版
+
¥25/月
+
个人用户的最佳选择
+
    +
  • 80+ 全球加速线路
  • +
  • 无限流量 · 极速
  • +
  • 5 台设备同时在线
  • +
  • 流媒体 & P2P 优化
  • +
  • Kill Switch · 智能分流
  • +
+ +
+ +
+
团队版
+
¥99/月
+
小团队集中管理
+
    +
  • 专业版全部功能
  • +
  • 10 个成员席位
  • +
  • 集中计费与管理
  • +
  • 优先客服
  • +
+ +
+
+ + +
+ +
+ 为什么网页上没有支付按钮?
+ 出于风控与隐私考虑,穿山甲不在网页或 App 内直接收款。你可以通过以下任一渠道获取激活码,在客户端内兑换即可开通。 +
+ 自助发卡商店 + USDT (TRC20) · 最隐私 + Telegram @PangolinVPN_bot + LINE @pangolinvpn + buy@pangolin.vpn +
+
+
+ + +

功能对比

+
+ + + + + + + + + + + + + + + + + +
功能免费版专业版团队版
可用线路180+80+
流量不限无限无限
每日时长10 分钟/天(试用期不限)无限无限
设备数1510
极速线路
流媒体优化
Kill Switch
客服标准标准优先
+
+
+
+ + +
+
+
下载
+

全平台,随处可用

+

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

+
+
+
+
iOS
iOS 16+
下载
+
Android
Android 9+
下载
+
macOS
macOS 12+
下载
+
Windows
Win 10/11
下载
+
Linux
deb / rpm
下载
+
+
+
+ + +
+
+
文档
+

需要帮助?都在这里

+

从快速上手到协议细节,清晰直接。

+
+ +
+ + +
+
+
Blog
+

来自团队的最新动态

+

产品更新、安全科普与节点公告。

+
+ +
+ + +
+
+
+

准备好体验极速畅连了吗?

+

免费下载,数分钟内开始你的第一次极速连接。

+ +
+
+
+ + + + + + + diff --git a/design/ui_kits/website/mobile-preview.html b/design/ui_kits/website/mobile-preview.html new file mode 100644 index 0000000..4de2ae9 --- /dev/null +++ b/design/ui_kits/website/mobile-preview.html @@ -0,0 +1,13 @@ + + + + + + +穿山甲 · 官网移动端预览 + + + + + + diff --git a/design/ui_kits/website/site.js b/design/ui_kits/website/site.js new file mode 100644 index 0000000..cbe4492 --- /dev/null +++ b/design/ui_kits/website/site.js @@ -0,0 +1,240 @@ +/* site.js — 穿山甲 VPN 官网 · i18n + 交互 */ +(function () { + // ---------- i18n ---------- + const I18N = { + 'ann.txt': ['新用户注册即享 7 天免费试用', 'New users get a 7-day free trial'], + 'ann.cta': ['立即注册 →', 'Sign up →'], + + 'su.ph': ['输入邮箱,免费注册', 'Enter email to sign up free'], + 'su.btn': ['免费注册', 'Sign up free'], + 'su.ok': ['验证码已发送!下载 App 并用同一邮箱继续完成注册。', 'Code sent! Download the app and continue with the same email.'], + 'su.hint': ['免费注册 · 无需付款 · 7 天免费试用', 'Free to join · no payment · 7-day free trial'], + 'su.dl': ['或直接下载 App', 'or just download the app'], + + 'why.eyebrow': ['免费注册', 'Sign up'], + 'why.h': ['注册一次,处处可用', 'One account, everywhere'], + 'why.sub': ['一个邮箱就能开始。注册免费,不需要任何付款信息。', 'All you need is an email. Free to join — no payment details, ever.'], + 'why.1t': ['7 天免费试用', '7-day free trial'], + 'why.1d': ['新用户注册即享 7 天免费使用,无需任何付款信息。', 'New accounts get 7 days of free, unrestricted use — no payment details.'], + 'why.2t': ['多端同步', 'Multi-device sync'], + 'why.2d': ['手机、平板、电脑共用一个账户,套餐与设置随身走。', 'Phone, tablet and desktop share one account — your plan follows you.'], + 'why.3t': ['无需付款信息', 'No payment details'], + 'why.3d': ['注册只要邮箱。想升级时,再通过外部渠道兑换激活码。', 'Just an email to join. Upgrade later with an activation code.'], + 'why.s1': ['填写邮箱', 'Enter email'], + 'why.s2': ['输入验证码', 'Verify code'], + 'why.s3': ['开始畅连', 'Start connecting'], + 'why.cta': ['立即免费注册', 'Sign up free'], + + 'cta.btn2': ['免费注册', 'Sign up free'], + + 'nav.product': ['产品', 'Product'], + 'nav.pricing': ['定价', 'Pricing'], + 'nav.download': ['下载', 'Download'], + 'nav.docs': ['文档', 'Docs'], + 'nav.blog': ['Blog', 'Blog'], + 'nav.login': ['登录', 'Log in'], + 'nav.get': ['立即下载', 'Get the app'], + + 'hero.eyebrow': ['极速 · 稳定 · 省心', 'Fast · Stable · Effortless'], + 'hero.h1': ['极速畅连,\n网络如丝顺滑', 'Faster, smoother,\neverywhere'], + 'hero.lede': ['轻盈、亲和、即开即用的跨平台网络加速应用。一键连接,智能选线,稳定不掉线。', 'A lightweight, friendly cross-platform network accelerator. One tap, smart routing, rock-solid.'], + 'hero.cta1': ['免费下载', 'Download free'], + 'hero.cta2': ['查看定价', 'See pricing'], + 'hero.t1': ['80+ 全球加速线路', '80+ global routes'], + 'hero.t2': ['严格无日志', 'Strict no-logs'], + 'hero.t3': ['端到端加密', 'End-to-end encrypted'], + 'orb.cap': ['已连接', 'CONNECTED'], + 'orb.down': ['下载', 'Down'], + 'orb.up': ['上传', 'Up'], + 'orb.node': ['香港 · 流媒体', 'Hong Kong'], + 'orb.nodesub': ['延迟 18ms · 已连接', '18ms · Connected'], + + 'strip.txt': ['已支持', 'Available on'], + + 'feat.eyebrow': ['产品', 'Product'], + 'feat.h': ['为日常而生的连接体验', 'Built for everyday connection'], + 'feat.sub': ['没有复杂配置,没有技术门槛。打开就用,把稳定与隐私留给我们。', 'No complex setup, no jargon. Just open and go — we handle speed and privacy.'], + 'feat.1t': ['一键连接', 'One-tap connect'], + 'feat.1d': ['一颗按钮,智能挑选最快线路。无需手动配置,也能秒连。', 'One button picks the fastest route. No manual setup, instant connect.'], + 'feat.2t': ['智能分流', 'Smart routing'], + 'feat.2d': ['按规则智能分流,该加速的加速,本地服务直连,互不打扰。', 'Smart rule-based routing — accelerate what needs it, keep local traffic direct.'], + 'feat.3t': ['80+ 全球加速线路', '80+ global routes'], + 'feat.3d': ['覆盖港日新美欧韩等地,视频与游戏优化线路随心选。', 'Routes across HK / JP / SG / US / EU and more, tuned for video & gaming.'], + 'feat.4t': ['严格无日志', 'Strict no-logs'], + 'feat.4d': ['我们不记录你的浏览数据。隐私是底线,不是卖点。', 'We never log your browsing. Privacy is the baseline, not a feature.'], + 'feat.5t': ['Kill Switch', 'Kill Switch'], + 'feat.5d': ['一旦连接中断,立即阻断网络,杜绝真实 IP 泄露。', 'If the tunnel drops, traffic is blocked instantly — no IP leaks.'], + 'feat.6t': ['多端同步', 'Multi-device'], + 'feat.6d': ['一个账户,手机、平板、电脑同时在线,最多 5 台设备。', 'One account across phone, tablet and desktop — up to 5 devices.'], + + 'price.eyebrow': ['定价', 'Pricing'], + 'price.h': ['简单透明的价格', 'Simple, honest pricing'], + 'price.sub': ['随时升级或取消。所有套餐均含核心加密与无日志承诺。', 'Upgrade or cancel anytime. Every plan includes core encryption and our no-logs promise.'], + 'price.monthly': ['按月', 'Monthly'], + 'price.yearly': ['按年', 'Yearly'], + 'price.save': ['省 20%', 'Save 20%'], + 'price.permo': ['/月', '/mo'], + 'price.free': ['免费版', 'Free'], + 'price.freedesc': ['适合轻度使用与尝鲜', 'For light, casual use'], + 'price.pro': ['专业版', 'Pro'], + 'price.prodesc': ['个人用户的最佳选择', 'Best for individuals'], + 'price.team': ['团队版', 'Team'], + 'price.teamdesc': ['小团队集中管理', 'For small teams'], + 'price.popular': ['最受欢迎', 'Most popular'], + 'price.cta_free': ['免费下载', 'Download free'], + 'price.cta_pro': ['获取激活码', 'Get a code'], + 'price.cta_team': ['获取激活码', 'Get a code'], + 'pf.free1': ['每日 10 分钟时长', '10 min per day'], + 'pf.free2': ['仅 1 个基础节点', '1 basic node only'], + 'pf.free3': ['使用前观看广告', 'Watch an ad to start'], + 'pf.free4': ['核心加密 · 无日志', 'Core encryption · no-logs'], + 'pf.pro1': ['80+ 全球加速线路', '80+ global routes'], + 'pf.pro2': ['无限流量 · 极速', 'Unlimited · top speed'], + 'pf.pro3': ['5 台设备同时在线', '5 devices at once'], + 'pf.pro4': ['流媒体 & P2P 优化', 'Streaming & P2P routes'], + 'pf.pro5': ['Kill Switch · 智能分流', 'Kill Switch · smart routing'], + 'pf.team1': ['专业版全部功能', 'Everything in Pro'], + 'pf.team2': ['10 个成员席位', '10 seats'], + 'pf.team3': ['集中计费与管理', 'Central billing & admin'], + 'pf.team4': ['优先客服', 'Priority support'], + + 'pay.title': ['为什么网页上没有支付按钮?', 'Why no checkout on this page?'], + 'pay.body': ['出于风控与隐私考虑,穿山甲不在网页或 App 内直接收款。你可以通过以下任一渠道获取激活码,在客户端内兑换即可开通。', 'For risk and privacy reasons, Pangolin never takes payment in the web or app. Get an activation code through any channel below, then redeem it in the client.'], + 'pay.store': ['自助发卡商店', 'Self-serve store'], + + 'pay.usdt': ['USDT (TRC20) · 最隐私', 'USDT (TRC20) · most private'], + 'cmp.h': ['功能对比', 'Compare plans'], + 'cmp.feature': ['功能', 'Feature'], + 'cmp.locations': ['可用线路', 'Routes'], + 'cmp.data': ['流量', 'Data'], + 'cmp.time': ['每日时长', 'Daily time'], + 'cmp.time_free': ['10 分钟/天(试用期不限)', '10 min/day (trial: unlimited)'], + 'cmp.devices': ['设备数', 'Devices'], + 'cmp.speed': ['极速线路', 'Top-speed routes'], + 'cmp.stream': ['视频优化', 'Video routes'], + 'cmp.kill': ['Kill Switch', 'Kill Switch'], + 'cmp.support': ['客服', 'Support'], + 'cmp.unlimited': ['无限', 'Unlimited'], + 'cmp.daily': ['不限', 'Unlimited'], + 'cmp.basic': ['标准', 'Standard'], + 'cmp.priority': ['优先', 'Priority'], + + 'dl.eyebrow': ['下载', 'Download'], + 'dl.h': ['全平台,随处可用', 'Every platform, everywhere'], + 'dl.sub': ['一个账户,所有设备同步。下载即用,无需配置。', 'One account syncs every device. Download and go.'], + 'dl.get': ['下载', 'Download'], + + 'docs.eyebrow': ['文档', 'Docs'], + 'docs.h': ['需要帮助?都在这里', 'Need help? It’s all here'], + 'docs.sub': ['从快速上手到协议细节,清晰直接。', 'From quickstart to protocol details — clear and direct.'], + 'docs.1t': ['快速开始', 'Quickstart'], + 'docs.1d': ['三分钟完成注册、下载与首次连接。', 'Sign up, download and connect in three minutes.'], + 'docs.2t': ['常见问题', 'FAQ'], + 'docs.2d': ['连接、计费、设备与兑换码的常见疑问。', 'Connection, billing, devices and redeem codes.'], + 'docs.3t': ['协议与安全', 'Protocol & security'], + 'docs.3d': ['WireGuard、加密方式与无日志架构说明。', 'WireGuard, encryption and our no-logs architecture.'], + 'docs.4t': ['隐私政策', 'Privacy policy'], + 'docs.4d': ['我们收集什么、不收集什么,一目了然。', 'Exactly what we collect — and what we never do.'], + 'docs.read': ['阅读', 'Read'], + + 'blog.eyebrow': ['Blog', 'Blog'], + 'blog.h': ['来自团队的最新动态', 'Latest from the team'], + 'blog.sub': ['产品更新、安全科普与节点公告。', 'Product updates, security explainers and node news.'], + 'blog.1tag': ['产品更新', 'Product'], + 'blog.1t': ['v2.4:更快的智能分流与全新统计页', 'v2.4: faster smart routing & a new stats tab'], + 'blog.1d': ['本次更新重写了分流引擎,连接建立速度提升约 40%。', 'We rewrote the routing engine — connections are ~40% faster.'], + 'blog.2tag': ['安全', 'Security'], + 'blog.2t': ['无日志到底意味着什么?', 'What “no-logs” really means'], + 'blog.2d': ['我们逐条拆解收集与不收集的数据,以及背后的架构。', 'A line-by-line look at what we keep — and what we never touch.'], + 'blog.3tag': ['公告', 'News'], + 'blog.3t': ['新增首尔与法兰克福高速节点', 'New high-speed nodes: Seoul & Frankfurt'], + 'blog.3d': ['两条新线路已上线,为东北亚与欧洲用户带来更低延迟。', 'Two new routes are live, cutting latency for NE Asia and Europe.'], + 'blog.readmore': ['阅读全文', 'Read more'], + + 'cta.h': ['准备好体验极速畅连了吗?', 'Ready for a faster connection?'], + 'cta.p': ['免费下载,数分钟内开始你的第一次极速连接。', 'Download free and get connected in minutes.'], + 'cta.btn': ['免费下载', 'Download free'], + + 'ft.tag': ['极简、轻量、亲和的跨平台网络加速应用。守护你的每一次连接。', 'A minimal, friendly cross-platform network accelerator. Protecting every connection.'], + 'ft.product': ['产品', 'Product'], + 'ft.resources': ['资源', 'Resources'], + 'ft.contact': ['联系与渠道', 'Contact'], + 'ft.features': ['功能', 'Features'], + 'ft.pricing': ['定价', 'Pricing'], + 'ft.download': ['下载', 'Download'], + 'ft.docs': ['文档', 'Docs'], + 'ft.blog': ['Blog', 'Blog'], + 'ft.faq': ['常见问题', 'FAQ'], + 'ft.privacy': ['隐私政策', 'Privacy'], + 'ft.status': ['服务状态', 'Status'], + 'ft.hours': ['服务时间 每日 9:00–24:00 (GMT+8)', 'Hours: Daily 9:00–24:00 (GMT+8)'], + 'ft.copy': ['© 2026 穿山甲 · Pangolin', '© 2026 Pangolin'], + 'ft.madenote': ['App 内不支持直接支付 · 资金流全走外部渠道', 'No in-app payment · all purchases via external channels'], + }; + + let lang = localStorage.getItem('pg_site_lang') || 'zh'; + + function applyLang() { + const i = lang === 'zh' ? 0 : 1; + document.documentElement.lang = lang === 'zh' ? 'zh' : 'en'; + document.querySelectorAll('[data-i18n]').forEach((el) => { + const k = el.getAttribute('data-i18n'); + if (I18N[k]) el.textContent = I18N[k][i]; + }); + document.querySelectorAll('[data-i18n-ph]').forEach((el) => { + const k = el.getAttribute('data-i18n-ph'); + if (I18N[k]) el.placeholder = I18N[k][i]; + }); + document.querySelectorAll('.langseg button').forEach((b) => b.classList.toggle('on', b.dataset.lang === lang)); + } + window.__setLang = function (l) { lang = l; localStorage.setItem('pg_site_lang', l); applyLang(); }; + + // ---------- pricing monthly/yearly ---------- + const PRICES = { free: ['¥0', '¥0'], pro: ['¥25', '¥20'], team: ['¥99', '¥79'] }; + let cycle = 'monthly'; + window.__setCycle = function (c) { + cycle = c; + const i = c === 'monthly' ? 0 : 1; + document.querySelectorAll('[data-price]').forEach((el) => { + const p = el.getAttribute('data-price'); + el.firstChild.textContent = PRICES[p][i]; + }); + document.querySelectorAll('.price-toggle button').forEach((b) => b.classList.toggle('on', b.dataset.cycle === c)); + }; + + // ---------- signup (demo: real impl sends a verification code) ---------- + window.__signup = function (e) { + e.preventDefault(); + const em = document.getElementById('su-email'); + if (!/\S+@\S+\.\S+/.test(em.value)) { em.focus(); return false; } + document.getElementById('su-ok').hidden = false; + return false; + }; + + // ---------- announcement bar ---------- + window.__closeAnn = function () { + const a = document.getElementById('ann'); + if (a) a.remove(); + }; + + // ---------- mobile menu ---------- + window.__toggleMenu = function () { + document.getElementById('mnav').classList.toggle('open'); + }; + + // ---------- scroll glass header ---------- + function onScroll() { + document.querySelector('.hdr').classList.toggle('scrolled', window.scrollY > 8); + } + window.addEventListener('scroll', onScroll, { passive: true }); + + // ---------- init ---------- + document.addEventListener('DOMContentLoaded', function () { + applyLang(); + onScroll(); + // close mobile menu when a link is tapped + document.querySelectorAll('#mnav a').forEach((a) => + a.addEventListener('click', () => document.getElementById('mnav').classList.remove('open'))); + if (window.lucide) lucide.createIcons(); + }); +})(); diff --git a/design/ui_kits/website/website.css b/design/ui_kits/website/website.css new file mode 100644 index 0000000..891a697 --- /dev/null +++ b/design/ui_kits/website/website.css @@ -0,0 +1,292 @@ +/* website.css — 穿山甲 VPN 官网宣传页 · 暖大地色系统 */ +*{box-sizing:border-box} +html{scroll-behavior:smooth} +body{margin:0;background:var(--bg);color:var(--fg1);font-family:var(--font-sans);-webkit-font-smoothing:antialiased} +a{color:inherit;text-decoration:none} +img,svg{display:block} +.wrap{max-width:1140px;margin:0 auto;padding:0 28px} +.sr{font-family:var(--font-sans)} + +/* ---------- announcement bar (A) ---------- */ +.ann{background:linear-gradient(90deg,var(--clay-600),var(--clay-700));color:#fff} +.ann-in{display:flex;align-items:center;justify-content:center;gap:10px;padding:9px 48px;font-size:13.5px;font-weight:600;position:relative} +.ann-in span{white-space:nowrap} +.ann svg{width:15px;height:15px;flex-shrink:0} +.ann a{text-decoration:underline;text-underline-offset:3px;font-weight:700;white-space:nowrap} +.ann a:hover{opacity:.85} +.ann-x{position:absolute;right:16px;top:50%;transform:translateY(-50%);background:transparent;border:none;color:rgba(255,255,255,.8);cursor:pointer;display:flex;padding:4px} +.ann-x svg{width:14px;height:14px} + +/* ---------- header ---------- */ +.hdr{position:sticky;top:0;z-index:50;background:color-mix(in srgb, var(--bg) 82%, transparent);backdrop-filter:blur(14px);border-bottom:1px solid transparent;transition:border-color var(--dur-base) var(--ease-out), background var(--dur-base)} +.hdr.scrolled{border-bottom-color:var(--border)} +.hdr .row{display:flex;align-items:center;gap:28px;height:68px} +.brand{display:flex;align-items:center;gap:10px} +.brand .nm{font-family:var(--font-display);font-weight:700;font-size:19px;letter-spacing:-0.01em} +.nav{display:flex;gap:6px;margin-left:8px} +.nav a{padding:8px 13px;border-radius:var(--radius-md);font-size:14.5px;font-weight:500;color:var(--fg2);transition:all var(--dur-fast) var(--ease-out)} +.nav a:hover{background:var(--accent-subtle);color:var(--accent)} +.hdr .right{margin-left:auto;display:flex;align-items:center;gap:14px} +.langseg{display:flex;background:var(--bg-subtle);border-radius:var(--radius-full);padding:3px;gap:2px} +.langseg button{border:none;cursor:pointer;border-radius:var(--radius-full);padding:5px 11px;font-family:var(--font-sans);font-size:12.5px;font-weight:700;background:transparent;color:var(--fg3)} +.langseg button.on{background:var(--accent);color:var(--fg-on-accent)} +.btn{display:inline-flex;align-items:center;gap:8px;font-family:var(--font-sans);font-weight:600;font-size:14.5px;border:none;border-radius:var(--radius-full);padding:11px 20px;cursor:pointer;transition:all var(--dur-fast) var(--ease-out);white-space:nowrap} +.btn svg{width:16px;height:16px} +.btn-primary{background:var(--accent);color:var(--fg-on-accent)} +.btn-primary:hover{background:var(--accent-hover)} +.btn-ghost{background:transparent;color:var(--fg1)} +.btn-ghost:hover{color:var(--accent)} +.btn-secondary{background:var(--surface);color:var(--fg1);border:1.5px solid var(--border-strong)} +.btn-secondary:hover{border-color:var(--accent);color:var(--accent)} +.btn-lg{padding:14px 26px;font-size:16px} +.linklogin{font-size:14.5px;font-weight:600;color:var(--fg1);cursor:pointer} +.linklogin:hover{color:var(--accent)} + +/* mobile nav */ +.menu-btn{display:none;border:none;background:transparent;cursor:pointer;padding:7px;border-radius:var(--radius-sm);color:var(--fg1)} +.menu-btn svg{width:22px;height:22px} +.mnav{display:none;background:var(--surface);border-bottom:1px solid var(--border);box-shadow:var(--shadow-md)} +.mnav.open{display:flex;flex-direction:column;padding:8px 28px 18px} +.mnav a{padding:13px 4px;font-size:15.5px;font-weight:600;color:var(--fg1);border-bottom:1px solid var(--border)} +.mnav a:active{color:var(--accent)} +.mnav .btn{margin-top:14px;justify-content:center;border-bottom:none} + +/* ---------- generic section ---------- */ +section{padding:88px 0} +.sec-tan{background:var(--bg-subtle)} +.eyebrow{font-family:var(--font-sans);font-size:13px;font-weight:700;letter-spacing:0.08em;text-transform:uppercase;color:var(--accent)} +.h-sec{font-family:var(--font-display);font-weight:700;font-size:38px;letter-spacing:-0.02em;line-height:1.15;margin:12px 0 0} +.sub-sec{font-size:17px;color:var(--fg2);line-height:1.6;margin:14px 0 0;max-width:560px} +.center{text-align:center} +.center .sub-sec{margin-left:auto;margin-right:auto} + +/* ---------- hero ---------- */ +.hero{padding:64px 0 84px;position:relative;overflow:hidden} +.hero .grid{display:grid;grid-template-columns:1.05fr 0.95fr;gap:48px;align-items:center} +.hero h1{font-family:var(--font-display);font-weight:700;font-size:54px;line-height:1.08;letter-spacing:-0.03em;margin:16px 0 0} +.hero .lede{font-size:19px;color:var(--fg2);line-height:1.6;margin:20px 0 0;max-width:480px} +.hero .cta{display:flex;gap:14px;margin-top:30px;flex-wrap:wrap} +.trust{display:flex;gap:20px;margin-top:28px;flex-wrap:wrap;color:var(--fg3);font-size:13.5px} +.trust span{display:inline-flex;align-items:center;gap:7px;white-space:nowrap} +.trust svg{width:15px;height:15px;color:var(--success)} + +/* hero inline signup (B) */ +.signup{margin-top:28px;max-width:480px} +.su-row{display:flex;gap:10px} +.su-field{flex:1;display:flex;align-items:center;gap:9px;background:var(--surface);border:1.5px solid var(--border-strong);border-radius:var(--radius-full);padding:0 18px;transition:border-color var(--dur-fast) var(--ease-out), box-shadow var(--dur-fast)} +.su-field:focus-within{border-color:var(--accent);box-shadow:var(--shadow-focus)} +.su-field svg{width:17px;height:17px;color:var(--fg3);flex-shrink:0} +.su-field input{border:none;background:transparent;outline:none;flex:1;font-family:var(--font-sans);font-size:15px;color:var(--fg1);padding:14px 0;min-width:0} +.su-btn{flex-shrink:0} +.su-ok{display:flex;align-items:center;gap:8px;margin-top:12px;color:var(--success);font-size:13.5px;font-weight:600} +.su-ok[hidden]{display:none} +.su-ok svg{width:16px;height:16px;flex-shrink:0} +.su-hint{display:flex;align-items:center;gap:16px;flex-wrap:wrap;margin-top:14px;font-size:13px;color:var(--fg3)} +.su-hint span{white-space:nowrap} +.su-dl{display:inline-flex;align-items:center;gap:6px;font-weight:600;color:var(--accent);white-space:nowrap} +.su-dl svg{width:14px;height:14px} + +/* hero visual: connect orb */ +.orb-wrap{display:flex;align-items:center;justify-content:center;position:relative} +.orb-card{width:100%;max-width:380px;background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-2xl);box-shadow:var(--shadow-xl);padding:34px 30px;display:flex;flex-direction:column;align-items:center;gap:22px} +.orb{position:relative;width:184px;height:184px;border-radius:50%;background:var(--success);color:#fff;box-shadow:0 0 0 9px var(--success-subtle), var(--shadow-lg);display:flex;flex-direction:column;align-items:center;justify-content:center;gap:6px} +.orb svg.ic{width:46px;height:46px} +.orb .tm{font-family:var(--font-mono);font-weight:600;font-size:17px} +.orb .sf{font-size:10px;font-weight:700;letter-spacing:0.1em;opacity:.9} +.orb .ring{position:absolute;inset:0} +.orb-meta{width:100%;display:flex;gap:10px} +.orb-meta .m{flex:1;background:var(--bg-subtle);border-radius:var(--radius-md);padding:10px 12px} +.orb-meta .k{display:flex;align-items:center;gap:5px;font-size:10.5px;color:var(--fg3);font-weight:600} +.orb-meta .k svg{width:12px;height:12px;color:var(--accent)} +.orb-meta .v{font-family:var(--font-mono);font-size:15px;margin-top:3px} +.orb-node{width:100%;display:flex;align-items:center;gap:11px;background:var(--bg-subtle);border-radius:var(--radius-lg);padding:11px 13px} +.cc{width:34px;height:34px;border-radius:var(--radius-md);background:var(--accent);color:#fff;display:flex;align-items:center;justify-content:center;font-family:var(--font-mono);font-size:12px;font-weight:600;flex-shrink:0} +.orb-node .nn{font-size:14px;font-weight:600} +.orb-node .ns{font-size:11px;color:var(--fg3)} + +/* logos strip */ +.strip{display:flex;align-items:center;justify-content:center;gap:34px;flex-wrap:wrap;color:var(--fg3);font-size:13px;font-weight:600;letter-spacing:0.03em;padding-top:8px} +.strip span{display:inline-flex;align-items:center;gap:8px} +.strip svg{width:18px;height:18px} + +/* ---------- features ---------- */ +.feat-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:20px;margin-top:48px} +.feat{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-xl);padding:26px;box-shadow:var(--shadow-sm);transition:transform var(--dur-base) var(--ease-out), box-shadow var(--dur-base)} +.feat:hover{transform:translateY(-3px);box-shadow:var(--shadow-md)} +.feat .ico{width:46px;height:46px;border-radius:var(--radius-lg);background:var(--accent-subtle);display:flex;align-items:center;justify-content:center;margin-bottom:16px} +.feat .ico svg{width:23px;height:23px;color:var(--accent)} +.feat h3{font-family:var(--font-display);font-weight:600;font-size:18px;margin:0 0 8px} +.feat p{font-size:14px;color:var(--fg2);line-height:1.6;margin:0} + +/* ---------- why sign up (C) ---------- */ +.why-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:20px;margin-top:46px} +.why{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-xl);padding:28px 26px;box-shadow:var(--shadow-sm);text-align:center} +.why .ico{width:54px;height:54px;border-radius:50%;background:var(--accent-subtle);display:flex;align-items:center;justify-content:center;margin:0 auto 16px} +.why .ico svg{width:26px;height:26px;color:var(--accent)} +.why h3{font-family:var(--font-display);font-weight:600;font-size:18px;margin:0 0 8px} +.why p{font-size:14px;color:var(--fg2);line-height:1.6;margin:0} +.steps{display:flex;align-items:center;justify-content:center;gap:14px;margin-top:44px;flex-wrap:wrap} +.step{display:flex;align-items:center;gap:10px;background:var(--bg-subtle);border:1px solid var(--border);border-radius:var(--radius-full);padding:10px 20px 10px 12px} +.step .n{width:26px;height:26px;border-radius:50%;background:var(--accent);color:#fff;font-weight:700;font-size:13px;display:flex;align-items:center;justify-content:center;flex-shrink:0} +.step svg{width:17px;height:17px;color:var(--accent)} +.step .t{font-size:14px;font-weight:600;white-space:nowrap} +.step-arr{display:flex} +.step-arr svg{width:18px;height:18px;color:var(--fg3)} +.why-cta{text-align:center;margin-top:38px} + +/* ---------- pricing ---------- */ +.price-toggle{display:inline-flex;background:var(--bg-subtle);border-radius:var(--radius-full);padding:4px;gap:2px;margin:22px auto 0} +.price-toggle button{border:none;cursor:pointer;border-radius:var(--radius-full);padding:8px 18px;font-family:var(--font-sans);font-size:13.5px;font-weight:600;background:transparent;color:var(--fg3)} +.price-toggle button.on{background:var(--surface);color:var(--fg1);box-shadow:var(--shadow-sm)} +.price-toggle .save{font-size:11px;color:var(--success);font-weight:700;margin-left:5px} +.price-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:20px;margin-top:42px;align-items:start} +.plan{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-2xl);padding:30px 28px;box-shadow:var(--shadow-sm);position:relative} +.plan.feat-plan{background:linear-gradient(160deg,var(--clay-600),var(--clay-800));color:#fff;border:none;box-shadow:var(--shadow-lg)} +.plan .pop{position:absolute;top:-13px;left:50%;transform:translateX(-50%);background:#fff;color:var(--clay-700);font-size:11.5px;font-weight:700;padding:5px 14px;border-radius:var(--radius-full);box-shadow:var(--shadow-sm);white-space:nowrap} +.plan .pname{font-size:15px;font-weight:600;color:var(--fg2)} +.plan.feat-plan .pname{color:rgba(255,255,255,.85)} +.plan .price{font-family:var(--font-display);font-weight:700;font-size:42px;letter-spacing:-0.02em;margin:10px 0 2px} +.plan .price small{font-size:15px;font-weight:500;color:var(--fg3)} +.plan.feat-plan .price small{color:rgba(255,255,255,.7)} +.plan .pdesc{font-size:13.5px;color:var(--fg3);min-height:38px} +.plan.feat-plan .pdesc{color:rgba(255,255,255,.8)} +.plan .feats{list-style:none;padding:0;margin:20px 0 24px;display:flex;flex-direction:column;gap:11px} +.plan .feats li{display:flex;align-items:flex-start;gap:9px;font-size:14px;color:var(--fg2);line-height:1.45} +.plan.feat-plan .feats li{color:rgba(255,255,255,.92)} +.plan .feats svg{width:16px;height:16px;color:var(--success);flex-shrink:0;margin-top:1px} +.plan.feat-plan .feats svg{color:#fff} +.plan .pcta{width:100%;border:none;border-radius:var(--radius-full);padding:13px;font-family:var(--font-sans);font-weight:700;font-size:14.5px;cursor:pointer} +.pcta-fill{background:var(--accent);color:#fff} +.pcta-white{background:#fff;color:var(--clay-700)} +.pcta-out{background:transparent;color:var(--fg2);border:1.5px solid var(--border-strong)!important} +.pay-note{display:flex;align-items:flex-start;gap:12px;margin-top:30px;background:var(--accent-subtle);border:1px solid var(--accent-border);border-radius:var(--radius-lg);padding:16px 18px;font-size:13.5px;color:var(--fg2);line-height:1.6} +.pay-note svg{width:18px;height:18px;color:var(--accent);flex-shrink:0;margin-top:2px} +.pay-note b{color:var(--fg1)} +.chips{display:flex;gap:8px;flex-wrap:wrap;margin-top:8px} +.chip{display:inline-flex;align-items:center;gap:6px;font-size:12.5px;font-weight:600;color:var(--accent);background:var(--surface);border:1px solid var(--accent-border);border-radius:var(--radius-full);padding:6px 12px} +.chip svg{width:13px;height:13px} + +/* comparison table */ +.cmp{margin-top:46px;border:1px solid var(--border);border-radius:var(--radius-xl);overflow:hidden;background:var(--surface);box-shadow:var(--shadow-sm)} +.cmp table{width:100%;border-collapse:collapse} +.cmp th,.cmp td{text-align:left;padding:14px 20px;font-size:14px;border-bottom:1px solid var(--border)} +.cmp thead th{background:var(--bg-subtle);font-family:var(--font-display);font-weight:700;font-size:14.5px} +.cmp thead th:not(:first-child),.cmp td:not(:first-child){text-align:center;width:130px} +.cmp tbody tr:last-child td{border-bottom:none} +.cmp td:first-child{color:var(--fg2)} +.cmp .yes{color:var(--success)} +.cmp .yes svg{width:18px;height:18px;display:inline} +.cmp .no{color:var(--fg3)} +.cmp .tx{font-family:var(--font-mono);font-size:13px;color:var(--fg1)} + +/* ---------- download ---------- */ +.dl-grid{display:grid;grid-template-columns:repeat(5,1fr);gap:16px;margin-top:46px} +.dl{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-xl);padding:24px 18px;text-align:center;box-shadow:var(--shadow-sm);transition:all var(--dur-base) var(--ease-out)} +.dl:hover{transform:translateY(-3px);box-shadow:var(--shadow-md);border-color:var(--accent-border)} +.dl .ico{width:48px;height:48px;margin:0 auto 14px;border-radius:var(--radius-lg);background:var(--bg-subtle);display:flex;align-items:center;justify-content:center} +.dl .ico svg{width:24px;height:24px;color:var(--fg1)} +.dl .pn{font-family:var(--font-display);font-weight:600;font-size:16px} +.dl .pv{font-size:12px;color:var(--fg3);margin:3px 0 14px;font-family:var(--font-mono)} +.dl .gb{display:inline-flex;align-items:center;gap:6px;font-size:13px;font-weight:600;color:var(--accent)} +.dl .gb svg{width:14px;height:14px} + +/* ---------- docs ---------- */ +.docs-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:18px;margin-top:44px} +.doc{display:block;background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-xl);padding:24px;box-shadow:var(--shadow-sm);transition:all var(--dur-base) var(--ease-out)} +.doc:hover{transform:translateY(-3px);box-shadow:var(--shadow-md)} +.doc .ico{width:42px;height:42px;border-radius:var(--radius-lg);background:var(--accent-subtle);display:flex;align-items:center;justify-content:center;margin-bottom:14px} +.doc .ico svg{width:21px;height:21px;color:var(--accent)} +.doc h3{font-family:var(--font-display);font-weight:600;font-size:16px;margin:0 0 6px} +.doc p{font-size:13px;color:var(--fg2);line-height:1.55;margin:0 0 12px} +.doc .ln{font-size:13px;font-weight:600;color:var(--accent);display:inline-flex;align-items:center;gap:5px} +.doc .ln svg{width:14px;height:14px} + +/* ---------- blog ---------- */ +.blog-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:22px;margin-top:44px} +.post{display:flex;flex-direction:column;background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-xl);overflow:hidden;box-shadow:var(--shadow-sm);transition:all var(--dur-base) var(--ease-out)} +.post:hover{transform:translateY(-3px);box-shadow:var(--shadow-md)} +.post .cover{height:140px;background:linear-gradient(150deg,var(--clay-300),var(--clay-600));position:relative} +.post .cover.c2{background:linear-gradient(150deg,var(--sand-300),var(--sand-600))} +.post .cover.c3{background:linear-gradient(150deg,var(--green-400),var(--green-600))} +.post .cover .tag{position:absolute;top:12px;left:12px;background:rgba(255,255,255,.9);color:var(--clay-700);font-size:11px;font-weight:700;padding:4px 10px;border-radius:var(--radius-full)} +.post .body{padding:18px 20px 22px} +.post .date{font-size:12px;color:var(--fg3);font-family:var(--font-mono)} +.post h3{font-family:var(--font-display);font-weight:600;font-size:17px;line-height:1.3;margin:8px 0 8px} +.post p{font-size:13.5px;color:var(--fg2);line-height:1.55;margin:0} + +/* ---------- CTA band ---------- */ +.cta-band{background:linear-gradient(160deg,var(--clay-500),var(--clay-800));color:#fff;border-radius:var(--radius-2xl);padding:56px;text-align:center;box-shadow:var(--shadow-lg)} +.cta-band h2{font-family:var(--font-display);font-weight:700;font-size:36px;letter-spacing:-0.02em;margin:0} +.cta-band p{font-size:17px;opacity:.9;margin:14px 0 28px} +.cta-band .btn-white{background:#fff;color:var(--clay-700)} +.cta-band .btn-white:hover{background:var(--sand-100)} +.cta-actions{display:flex;gap:12px;justify-content:center;flex-wrap:wrap} +.cta-ghost{background:transparent;color:#fff;border:1.5px solid rgba(255,255,255,.45)} +.cta-ghost:hover{border-color:#fff} + +/* ---------- footer ---------- */ +.ftr{background:var(--sand-950);color:#cfc6b8;padding:64px 0 32px} +[data-theme="dark"] .ftr{background:#000} +.ftr .top{display:grid;grid-template-columns:1.4fr 1fr 1fr 1fr;gap:36px} +.ftr .nm{font-family:var(--font-display);font-weight:700;font-size:18px;color:#fff;display:flex;align-items:center;gap:10px} +.ftr .tag{font-size:13.5px;line-height:1.6;margin:14px 0 0;max-width:280px;color:#a89e8e} +.ftr h4{font-family:var(--font-sans);font-size:12px;font-weight:700;letter-spacing:0.08em;text-transform:uppercase;color:#8a8070;margin:0 0 14px} +.ftr ul{list-style:none;padding:0;margin:0;display:flex;flex-direction:column;gap:10px} +.ftr ul a{font-size:14px;color:#cfc6b8} +.ftr ul a:hover{color:#fff} +.ftr .mono{font-family:var(--font-mono);font-size:12.5px} +.ftr .bot{display:flex;align-items:center;justify-content:space-between;margin-top:46px;padding-top:24px;border-top:1px solid rgba(255,255,255,.1);font-size:12.5px;color:#8a8070;flex-wrap:wrap;gap:12px} + +/* ---------- responsive ---------- */ +@media (max-width:980px){ + .nav{display:none} + .menu-btn{display:flex} + .linklogin{display:none} + .hero .grid{grid-template-columns:1fr;gap:36px} + .feat-grid,.price-grid,.blog-grid,.why-grid{grid-template-columns:1fr} + .docs-grid{grid-template-columns:1fr 1fr} + .dl-grid{grid-template-columns:1fr 1fr} + .ftr .top{grid-template-columns:1fr 1fr} + .hero h1{font-size:40px} + .h-sec{font-size:30px} + .cmp{overflow-x:auto} + .cmp table{min-width:560px} +} +@media (max-width:640px){ + .wrap{padding:0 20px} + .ann-in span{white-space:normal} + section{padding:56px 0} + .hero{padding:32px 0 56px} + .hero h1{font-size:34px} + .hero .lede{font-size:16px} + .h-sec{font-size:26px} + .sub-sec{font-size:15px} + .hdr .row{height:60px;gap:14px} + .hdr .right{gap:10px} + .hdr .right .btn{display:none} + .mnav.open{padding:8px 20px 16px} + .ann-in{font-size:12.5px;padding:8px 44px 8px 16px;justify-content:flex-start;flex-wrap:wrap} + .su-row{flex-direction:column} + .su-btn{justify-content:center;width:100%} + .su-field input{padding:13px 0} + .trust{gap:12px} + .orb-card{max-width:330px;padding:26px 22px} + .strip{gap:16px;font-size:12px} + .price-toggle{margin-top:18px} + .steps{gap:10px} + .step{padding:8px 14px 8px 10px} + .step .t{font-size:13px} + .step-arr{display:none} + .docs-grid{grid-template-columns:1fr} + .cta-band{padding:42px 24px} + .cta-band h2{font-size:27px} + .cta-band p{font-size:15px} + .ftr{padding:48px 0 28px} + .ftr .top{grid-template-columns:1fr 1fr;gap:30px 20px} + .ftr .bot{flex-direction:column;align-items:flex-start;gap:8px} +} +@media (max-width:420px){ + .dl-grid{grid-template-columns:1fr} + .hero h1{font-size:30px} + .langseg button{padding:4px 9px} +} diff --git a/design/uploads/pasted-1781105742272.txt b/design/uploads/pasted-1781105742272.txt new file mode 100644 index 0000000..ef8d23f --- /dev/null +++ b/design/uploads/pasted-1781105742272.txt @@ -0,0 +1,4 @@ +pangolin/ + app/ ← Flutter 客户端(把 zip 里的 flutter/ 作为起点) + server/ ← 后端 API + design/ ← 这套设计系统(解压进去,CLAUDE.md 自动生效) \ No newline at end of file diff --git a/design/uploads/pasted-1781105748445.txt b/design/uploads/pasted-1781105748445.txt new file mode 100644 index 0000000..ef8d23f --- /dev/null +++ b/design/uploads/pasted-1781105748445.txt @@ -0,0 +1,4 @@ +pangolin/ + app/ ← Flutter 客户端(把 zip 里的 flutter/ 作为起点) + server/ ← 后端 API + design/ ← 这套设计系统(解压进去,CLAUDE.md 自动生效) \ No newline at end of file diff --git a/design/uploads/pasted-1781108801394-0.png b/design/uploads/pasted-1781108801394-0.png new file mode 100644 index 0000000000000000000000000000000000000000..764c1e43b83ea4be54861e6a24f21a2c5cef5115 GIT binary patch literal 259432 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

)*+j;ca=Vx7sSy@@Bw-~#<|M|>#E0ijZ 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

XR_?x7Z_2aQd#~b2|M?o?=l4+xv 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 HB{Ts5A>|yf literal 0 HcmV?d00001 diff --git a/todo/todo.html b/todo/todo.html new file mode 100644 index 0000000..efe8630 --- /dev/null +++ b/todo/todo.html @@ -0,0 +1,989 @@ + + + + + +doc — 项目 TODO + + + + +

+
+

doc — 项目 TODO

+
生成于 2026-06-11 · 真相源 todo/todo.json
+
+
18全部
+
18待开始
+
0开发中
+
0待验收
+
0已验收
+
+
+
+ +
+
+ 重要度 + + + + +
+
+
+ 状态 + + + + + +
+
+
+ 改动等级 + + + + +
+
+
+ 平台 / 标签 + +
+
+ +
+ + +
+
+ 📋 待开始 18 + ▴ 收起 +
+
+
    + +
  • +
    + OpenAPI 契约 + MySQL migration 基线 +
    + 待开始 + 高优 · 紧急 + 一级 + + +
    +
    + +
    依据 doc/02、doc/03:openapi.yaml 展开全部 /v1 契约(connect 返回 sing-box 凭证而非 WG peer);MySQL 8 全量 DDL(users/devices/plans/subscriptions/codes/usage_daily/audit_log + providers/node_events/directory_version)。后端各模块的共同前置。
    + + +
  • + +
  • +
    + nodes 模块 + agent gRPC 协议设计 +
    + 待开始 + 高优 · 紧急 + 一级 + + +
    +
    + +
    proto 定义(注册/心跳/凭证下发与回收/用量上报,mTLS 双向)、节点目录 version 灰度(if_version 304)、connect/disconnect 下发 REALITY/Hy2 参数、free 凭证 TTL。依赖 #1。
    + + +
  • + +
  • +
    + Flutter 客户端工程搭建(演示态跑通) +
    + 待开始 + 高优 · 紧急 + 二级 + + +
    +
    + +
    用 design/flutter/ 起步包建工程:pubspec 覆盖、theme/widgets/main 拷入、字体配置,flutter run 出登录→引导→主框架演示态。前端线起点,可与后端完全并行。
    + + +
  • + +
  • +
    + sing-box libbox 桥接 PoC(三端隧道) +
    + 待开始 + 高优 · 紧急 + 一级 + + +
    +
    + +
    gomobile AAR/XCFramework + iOS NetworkExtension / Android VpnService / 桌面 TUN 子进程;URLTest 智能选线、Kill-switch。全项目最大技术风险,应尽早并行启动。
    + + +
  • + +
  • +
    + 探针判封 + 自动更换 scheduler +
    + 待开始 + 高优 · 紧急 + 一级 + + +
    +
    + +
    境内多 ISP 拨测 + 境外对照 + 流量骤降三路互证;blocked_suspect/confirmed 状态机、阈值与熔断、池水位告警、自动补新。依赖 #5、#14。
    + + +
  • + +
  • +
    + auth 模块(验证码/注册/登录/JWT) +
    + 待开始 + 重要 + 二级 + + +
    +
    + +
    邮箱验证码限频、注册自动 7 天 PRO 试用、argon2id、JWT RS256(access 15min + refresh 30d)、Redis 滑窗限流。依赖 #1。
    + + +
  • + +
  • +
    + codes 激活码模块 +
    + 待开始 + 重要 + 二级 + + +
    +
    + +
    批量生成(Crockford Base32 + 校验位,库存 hash)、发卡店 webhook(HMAC)、兑换幂等 + 订阅顺延叠加、失败锁定、audit_log。依赖 #1。
    + + +
  • + +
  • +
    + devices + 订阅校验中间件 +
    + 待开始 + 重要 + 二级 + + +
    +
    + +
    设备登记/移除(同步回收节点侧凭证)、套餐/到期/设备数校验中间件。依赖 #1、#2。
    + + +
  • + +
  • +
    + 节点 agent 实现 +
    + 待开始 + 重要 + 二级 + + +
    +
    + +
    轻量 agent:mTLS 自注册、心跳、sing-box 用户表增删(凭证下发/回收/TTL)、负载上报 Redis;cloud-init 一段式安装。依赖 #5 的 proto。
    + + +
  • + +
  • +
    + usage 用量 + 广告解锁 +
    + 待开始 + 重要 + 二级 + + +
    +
    + +
    usage_daily 聚合(仅字节/分钟,无日志口径)、/v1/ads/unlock 验广告 SDK 回执、free 用户 connect 额度校验。依赖 #1。
    + + +
  • + +
  • +
    + Flutter 逐屏还原 + iPad 断点 +
    + 待开始 + 重要 + 二级 + + +
    +
    + +
    对照 ui_kits/mobile + tablet 逐屏补齐:智能选择推荐卡、免费额度卡+看广告解锁、Tab 滑动切换、宽度 ≥900 切侧栏分栏(连接页双栏/节点双列网格);四态验证(明暗×中英)。依赖 #9。
    + + +
  • + +
  • +
    + 官网 Astro 迁移 +
    + 待开始 + 重要 + 二级 + + +
    +
    + +
    ui_kits/website → Astro SSG + Cloudflare Pages 多镜像;保留 i18n 单显、响应式断点、全部脱敏文案。完全独立可并行。
    + + +
  • + +
  • +
    + Web 用户中心(Next.js) +
    + 待开始 + 重要 + 二级 + + +
    +
    + +
    ui_kits/usercenter React 组件直接复用;先 mock 数据,后接 me/redeem/devices;TOTP 2FA。UI 部分独立并行,接口联调依赖 #2。
    + + +
  • + +
  • +
    + 弹性节点基建:Terraform + 一键更换 +
    + 待开始 + 重要 + 二级 + + +
    +
    + +
    多厂商 API 适配层(providers 池)、cloud-init 模板、replace 一键更换(make-before-break)与全量滚动轮换、换 IP 不换机。依赖 #6 的 agent。
    + + +
  • + +
  • +
    + 域名池 + CDN 前置 + 签名端点分发 +
    + 待开始 + 重要 + 二级 + + +
    +
    + +
    四组域名隔离与冷备池、源站隐藏(回源鉴权)、Ed25519 签名端点更新与公告 JSON 多镜像、客户端 DoH/IP 直连兜底联动。可独立并行。
    + + +
  • + +
  • +
    + 备份与灾备演练 +
    + 待开始 + 重要 + 三级 + + +
    +
    + +
    xtrabackup 每日全量 + binlog 15min 增量、age 加密异地(独立身份账号)、月度恢复演练脚本、RPO≤15min/RTO≤4h 验证。
    + + +
  • + +
  • +
    + 管理端最小后台 +
    + 待开始 + 一般 / 优化 + 二级 + + +
    +
    + +
    码批次生成导出、节点操作(replace/draining)、audit_log 查看;独立监听 + 内网白名单 + 2FA。依赖 #1。
    + + +
  • + +
  • +
    + CI 流水线 +
    + 待开始 + 一般 / 优化 + 三级 + + +
    +
    + +
    lint + 单测 + OpenAPI 同步校验 + 脱敏红线词扫描(UI 文案资源)+ 容器镜像构建。
    + + +
  • +
+
+
+
+
+ 🔨 开发中 0 + ▴ 收起 +
+
+
    +

    暂无条目

    +
+
+
+
+
+ 🔍 待验收 0 + ▴ 收起 +
+
+
    +

    暂无条目

    +
+
+
+
+
+ ✅ 已验收 0 + ▾ 展开 +
+ +
+ +
+ + + + + + + \ No newline at end of file diff --git a/todo/todo.json b/todo/todo.json new file mode 100644 index 0000000..69603d1 --- /dev/null +++ b/todo/todo.json @@ -0,0 +1,293 @@ +{ + "meta": { + "title": "doc — 项目 TODO", + "updated_at": "2026-06-11T15:48:46.142Z" + }, + "seq": 18, + "items": [ + { + "id": 1, + "title": "OpenAPI 契约 + MySQL migration 基线", + "desc": "依据 doc/02、doc/03:openapi.yaml 展开全部 /v1 契约(connect 返回 sing-box 凭证而非 WG peer);MySQL 8 全量 DDL(users/devices/plans/subscriptions/codes/usage_daily/audit_log + providers/node_events/directory_version)。后端各模块的共同前置。", + "level": "high", + "tier": 1, + "tags": [ + "后端", + "数据库" + ], + "status": "open", + "created_at": "2026-06-11T15:48:05.656Z", + "done": false, + "completed_at": null, + "version": null + }, + { + "id": 2, + "title": "auth 模块(验证码/注册/登录/JWT)", + "desc": "邮箱验证码限频、注册自动 7 天 PRO 试用、argon2id、JWT RS256(access 15min + refresh 30d)、Redis 滑窗限流。依赖 #1。", + "level": "mid", + "tier": 2, + "tags": [ + "后端" + ], + "status": "open", + "created_at": "2026-06-11T15:48:05.707Z", + "done": false, + "completed_at": null, + "version": null + }, + { + "id": 3, + "title": "codes 激活码模块", + "desc": "批量生成(Crockford Base32 + 校验位,库存 hash)、发卡店 webhook(HMAC)、兑换幂等 + 订阅顺延叠加、失败锁定、audit_log。依赖 #1。", + "level": "mid", + "tier": 2, + "tags": [ + "后端" + ], + "status": "open", + "created_at": "2026-06-11T15:48:05.764Z", + "done": false, + "completed_at": null, + "version": null + }, + { + "id": 4, + "title": "devices + 订阅校验中间件", + "desc": "设备登记/移除(同步回收节点侧凭证)、套餐/到期/设备数校验中间件。依赖 #1、#2。", + "level": "mid", + "tier": 2, + "tags": [ + "后端" + ], + "status": "open", + "created_at": "2026-06-11T15:48:05.818Z", + "done": false, + "completed_at": null, + "version": null + }, + { + "id": 5, + "title": "nodes 模块 + agent gRPC 协议设计", + "desc": "proto 定义(注册/心跳/凭证下发与回收/用量上报,mTLS 双向)、节点目录 version 灰度(if_version 304)、connect/disconnect 下发 REALITY/Hy2 参数、free 凭证 TTL。依赖 #1。", + "level": "high", + "tier": 1, + "tags": [ + "后端" + ], + "status": "open", + "created_at": "2026-06-11T15:48:05.872Z", + "done": false, + "completed_at": null, + "version": null + }, + { + "id": 6, + "title": "节点 agent 实现", + "desc": "轻量 agent:mTLS 自注册、心跳、sing-box 用户表增删(凭证下发/回收/TTL)、负载上报 Redis;cloud-init 一段式安装。依赖 #5 的 proto。", + "level": "mid", + "tier": 2, + "tags": [ + "后端" + ], + "status": "open", + "created_at": "2026-06-11T15:48:25.479Z", + "done": false, + "completed_at": null, + "version": null + }, + { + "id": 7, + "title": "usage 用量 + 广告解锁", + "desc": "usage_daily 聚合(仅字节/分钟,无日志口径)、/v1/ads/unlock 验广告 SDK 回执、free 用户 connect 额度校验。依赖 #1。", + "level": "mid", + "tier": 2, + "tags": [ + "后端" + ], + "status": "open", + "created_at": "2026-06-11T15:48:25.532Z", + "done": false, + "completed_at": null, + "version": null + }, + { + "id": 8, + "title": "管理端最小后台", + "desc": "码批次生成导出、节点操作(replace/draining)、audit_log 查看;独立监听 + 内网白名单 + 2FA。依赖 #1。", + "level": "low", + "tier": 2, + "tags": [ + "后端", + "Web" + ], + "status": "open", + "created_at": "2026-06-11T15:48:25.585Z", + "done": false, + "completed_at": null, + "version": null + }, + { + "id": 9, + "title": "Flutter 客户端工程搭建(演示态跑通)", + "desc": "用 design/flutter/ 起步包建工程:pubspec 覆盖、theme/widgets/main 拷入、字体配置,flutter run 出登录→引导→主框架演示态。前端线起点,可与后端完全并行。", + "level": "high", + "tier": 2, + "tags": [ + "前端", + "iOS", + "Android" + ], + "status": "open", + "created_at": "2026-06-11T15:48:25.638Z", + "done": false, + "completed_at": null, + "version": null + }, + { + "id": 10, + "title": "Flutter 逐屏还原 + iPad 断点", + "desc": "对照 ui_kits/mobile + tablet 逐屏补齐:智能选择推荐卡、免费额度卡+看广告解锁、Tab 滑动切换、宽度 ≥900 切侧栏分栏(连接页双栏/节点双列网格);四态验证(明暗×中英)。依赖 #9。", + "level": "mid", + "tier": 2, + "tags": [ + "前端", + "iOS", + "Android" + ], + "status": "open", + "created_at": "2026-06-11T15:48:25.690Z", + "done": false, + "completed_at": null, + "version": null + }, + { + "id": 11, + "title": "sing-box libbox 桥接 PoC(三端隧道)", + "desc": "gomobile AAR/XCFramework + iOS NetworkExtension / Android VpnService / 桌面 TUN 子进程;URLTest 智能选线、Kill-switch。全项目最大技术风险,应尽早并行启动。", + "level": "high", + "tier": 1, + "tags": [ + "前端", + "iOS", + "Android", + "mac", + "Windows" + ], + "status": "open", + "created_at": "2026-06-11T15:48:25.745Z", + "done": false, + "completed_at": null, + "version": null + }, + { + "id": 12, + "title": "官网 Astro 迁移", + "desc": "ui_kits/website → Astro SSG + Cloudflare Pages 多镜像;保留 i18n 单显、响应式断点、全部脱敏文案。完全独立可并行。", + "level": "mid", + "tier": 2, + "tags": [ + "Web", + "前端" + ], + "status": "open", + "created_at": "2026-06-11T15:48:45.836Z", + "done": false, + "completed_at": null, + "version": null + }, + { + "id": 13, + "title": "Web 用户中心(Next.js)", + "desc": "ui_kits/usercenter React 组件直接复用;先 mock 数据,后接 me/redeem/devices;TOTP 2FA。UI 部分独立并行,接口联调依赖 #2。", + "level": "mid", + "tier": 2, + "tags": [ + "Web", + "前端" + ], + "status": "open", + "created_at": "2026-06-11T15:48:45.886Z", + "done": false, + "completed_at": null, + "version": null + }, + { + "id": 14, + "title": "弹性节点基建:Terraform + 一键更换", + "desc": "多厂商 API 适配层(providers 池)、cloud-init 模板、replace 一键更换(make-before-break)与全量滚动轮换、换 IP 不换机。依赖 #6 的 agent。", + "level": "mid", + "tier": 2, + "tags": [ + "CI/CD", + "后端" + ], + "status": "open", + "created_at": "2026-06-11T15:48:45.936Z", + "done": false, + "completed_at": null, + "version": null + }, + { + "id": 15, + "title": "探针判封 + 自动更换 scheduler", + "desc": "境内多 ISP 拨测 + 境外对照 + 流量骤降三路互证;blocked_suspect/confirmed 状态机、阈值与熔断、池水位告警、自动补新。依赖 #5、#14。", + "level": "high", + "tier": 1, + "tags": [ + "后端" + ], + "status": "open", + "created_at": "2026-06-11T15:48:45.987Z", + "done": false, + "completed_at": null, + "version": null + }, + { + "id": 16, + "title": "域名池 + CDN 前置 + 签名端点分发", + "desc": "四组域名隔离与冷备池、源站隐藏(回源鉴权)、Ed25519 签名端点更新与公告 JSON 多镜像、客户端 DoH/IP 直连兜底联动。可独立并行。", + "level": "mid", + "tier": 2, + "tags": [ + "Web" + ], + "status": "open", + "created_at": "2026-06-11T15:48:46.039Z", + "done": false, + "completed_at": null, + "version": null + }, + { + "id": 17, + "title": "备份与灾备演练", + "desc": "xtrabackup 每日全量 + binlog 15min 增量、age 加密异地(独立身份账号)、月度恢复演练脚本、RPO≤15min/RTO≤4h 验证。", + "level": "mid", + "tier": 3, + "tags": [ + "数据库", + "CI/CD" + ], + "status": "open", + "created_at": "2026-06-11T15:48:46.090Z", + "done": false, + "completed_at": null, + "version": null + }, + { + "id": 18, + "title": "CI 流水线", + "desc": "lint + 单测 + OpenAPI 同步校验 + 脱敏红线词扫描(UI 文案资源)+ 容器镜像构建。", + "level": "low", + "tier": 3, + "tags": [ + "CI/CD" + ], + "status": "open", + "created_at": "2026-06-11T15:48:46.141Z", + "done": false, + "completed_at": null, + "version": null + } + ] +}