diff --git a/.claude/worktrees/arch-doc b/.claude/worktrees/arch-doc deleted file mode 160000 index 3ae96ef..0000000 --- a/.claude/worktrees/arch-doc +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 3ae96ef22946eb56799edd94a8492613853335f8 diff --git a/.gitignore b/.gitignore index db39f09..4868765 100644 --- a/.gitignore +++ b/.gitignore @@ -49,6 +49,15 @@ app/kernel/.build/ server/mockserver server/pangolin-server +# Flutter / Dart 构建产物与本地配置 +client/android/local.properties +client/android/app/src/main/java/ +client/ios/Flutter/ephemeral/ +client/pubspec.lock + +# Claude worktrees(临时工作目录,本地专用) +.claude/worktrees/ + # 杂项 .DS_Store *.log diff --git a/design/flutter/README.md b/design/flutter/README.md deleted file mode 100644 index 7b3feb2..0000000 --- a/design/flutter/README.md +++ /dev/null @@ -1,135 +0,0 @@ -# 穿山甲 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 deleted file mode 100644 index c7eae9a..0000000 --- a/design/flutter/assets/app-icon.svg +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/design/flutter/assets/logo-mark-white.svg b/design/flutter/assets/logo-mark-white.svg deleted file mode 100644 index 0584f5d..0000000 --- a/design/flutter/assets/logo-mark-white.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/design/flutter/assets/logo-mark.svg b/design/flutter/assets/logo-mark.svg deleted file mode 100644 index b3e364b..0000000 --- a/design/flutter/assets/logo-mark.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/design/flutter/assets/logo-wordmark.svg b/design/flutter/assets/logo-wordmark.svg deleted file mode 100644 index 8603db5..0000000 --- a/design/flutter/assets/logo-wordmark.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - 穿山甲 - PANGOLIN - \ No newline at end of file diff --git a/design/flutter/main.dart b/design/flutter/main.dart deleted file mode 100644 index f03db95..0000000 --- a/design/flutter/main.dart +++ /dev/null @@ -1,58 +0,0 @@ -// 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 deleted file mode 100644 index 9548313..0000000 --- a/design/flutter/pangolin_theme.dart +++ /dev/null @@ -1,307 +0,0 @@ -// 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 deleted file mode 100644 index 8e39002..0000000 --- a/design/flutter/pubspec.yaml +++ /dev/null @@ -1,84 +0,0 @@ -# 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 deleted file mode 100644 index 3a8f4d6..0000000 --- a/design/flutter/widgets/account_screens.dart +++ /dev/null @@ -1,296 +0,0 @@ -// 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 deleted file mode 100644 index 3818e33..0000000 --- a/design/flutter/widgets/auth_screen.dart +++ /dev/null @@ -1,159 +0,0 @@ -// 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 deleted file mode 100644 index 61b8510..0000000 --- a/design/flutter/widgets/connect_button.dart +++ /dev/null @@ -1,147 +0,0 @@ -// 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 deleted file mode 100644 index dcba385..0000000 --- a/design/flutter/widgets/country_code.dart +++ /dev/null @@ -1,61 +0,0 @@ -// 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 deleted file mode 100644 index dff720b..0000000 --- a/design/flutter/widgets/home_shell.dart +++ /dev/null @@ -1,328 +0,0 @@ -// 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 deleted file mode 100644 index 207a44f..0000000 --- a/design/flutter/widgets/onboarding_screen.dart +++ /dev/null @@ -1,103 +0,0 @@ -// 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 deleted file mode 100644 index 122ce2a..0000000 --- a/design/flutter/widgets/pangolin_button.dart +++ /dev/null @@ -1,67 +0,0 @@ -// 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 deleted file mode 100644 index 270c32a..0000000 --- a/design/flutter/widgets/pangolin_icons.dart +++ /dev/null @@ -1,85 +0,0 @@ -// 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 deleted file mode 100644 index 8bdb5b4..0000000 --- a/design/flutter/widgets/pangolin_logo.dart +++ /dev/null @@ -1,74 +0,0 @@ -// 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 deleted file mode 100644 index 0d1b62b..0000000 --- a/design/flutter/widgets/pangolin_widgets.dart +++ /dev/null @@ -1,14 +0,0 @@ -// 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 deleted file mode 100644 index b61283a..0000000 --- a/design/flutter/widgets/plan_card.dart +++ /dev/null @@ -1,112 +0,0 @@ -// 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 deleted file mode 100644 index e600e16..0000000 --- a/design/flutter/widgets/server_tile.dart +++ /dev/null @@ -1,51 +0,0 @@ -// 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 deleted file mode 100644 index 2365944..0000000 --- a/design/flutter/widgets/status_pill.dart +++ /dev/null @@ -1,45 +0,0 @@ -// 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)), - ], - ), - ); - } -}