feat(design): 支付渠道 switch 原型 + segswitch 原子 + 图标/语言同源闸
- .segswitch 段控原子入 atoms.css + 登记 index.html - purchase/ui-mobile/ui-desktop 三屏:购买页支付渠道 switch(USDT→crypto 默认 / 人民币→alipay),套餐价随渠道分币种显示,删两步弹层 + nezha - 订单列表/详情集成进移动+桌面主框架 - icons.js 58 图标 sprite 单源 + gen_flutter_icons codegen + check-l1-sync 同源闸(图标同集 + l10n 完整) - pay-v2 开发决策日志 docs/pay-v2-dev-log.md Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013nMthbVEmQquxBRKb9Fj8u
This commit is contained in:
+5
-4
@@ -96,7 +96,7 @@ SKILL.md ← Agent Skill 入口
|
||||
- 登录/注册(`AuthFlow` / `auth_screen.dart`):登录=邮箱+密码;注册=邮箱→发验证码→填码→设密码(多端登录)
|
||||
- 首次引导(`Onboarding` / `onboarding_screen.dart`):3 屏(连接全球→授权配置→安全无日志),可跳过
|
||||
- 主框架 4 Tab(`home_shell.dart`):**连接 / 节点 / 统计 / 账户**;内容区支持**左右滑动切换 Tab**(>60px 且横向位移明显大于纵向,200ms 方向感知滑入;子页不响应)
|
||||
- 连接:核心连接键三态(off 暖灰底+虚线轨道环 / connecting 旋转弧 / on 绿底+满环+计时)+ 当前节点卡 + 实时速率;**免费版额外显示额度卡**(今日剩余分钟 + 进度条(≤3 分钟变 warning 色)+ 「看广告开始使用」→ 点击后变绿色「已解锁 · 今日可用」)
|
||||
- 连接:核心连接键**四态**(off 暖灰底+虚线轨道环 / connecting 旋转弧 / on 绿底+满环+计时 / **disabled 免费到点切断:暖灰底+lock 图标+not-allowed,点击弹「看广告加时」**)+ 当前节点卡 + 实时速率;**免费版额外显示额度卡**(今日剩余分钟 + 进度条(≤3 分钟变 warning、耗尽变 danger)+ 「看广告加时」;**连接期用倒计时收缩条表达剩余,无「已解锁」绿态** —— 旧「点击后变绿色已解锁」描述已废,以线上 `quota_card.dart` 为准)
|
||||
- 节点:顶部置顶**「智能选择」推荐卡**(常驻 accent-subtle 底 + clay 渐变 zap 图标 + 「推荐」胶囊 + 文案「根据当前网络环境,自动选择最优节点」,默认选中);其下才是搜索 + 节点列表(国家码块·延迟·信号条·选中态)
|
||||
- 统计:流量/延迟/时长指标卡 + 本周柱状图
|
||||
- 账户:套餐横幅(免费版/PRO 自适应)+ 账户信息(邮箱/密码/退出)+ 设备管理 + 兑换/联系入口
|
||||
@@ -108,10 +108,11 @@ SKILL.md ← Agent Skill 入口
|
||||
|
||||
**Web 用户中心**(`ui_kits/usercenter/`):概览(套餐状态/用量)· 订阅导入(订阅链接+二维码+一键导入三方客户端)· 兑换 · 邀请返利 · 设置(含 **2FA/TOTP 开关**,用户可自选开启)。响应式,移动端底部 Tab + 左右滑动切换。
|
||||
|
||||
**核心连接键**(最重要的组件,务必还原):圆形,三态——
|
||||
- off:暖灰底 `sand-100` + clay power 图标 + 虚线轨道环 + 「点击连接」
|
||||
**核心连接键**(最重要的组件,务必还原):圆形,**四态**(对齐线上 `connect_button.dart`)——
|
||||
- off:暖灰底 `bg-subtle` + clay power 图标 + 虚线轨道环 + 「点击连接」
|
||||
- connecting:clay 实底 + 旋转进度弧 + 旋转 loader
|
||||
- on:success 实底 + 白盾勾 + 满白环 + 圆内计时 + 「已加密」+ 绿光晕
|
||||
- **disabled**(免费到点切断/额度耗尽):`bg-subtle` 底 + `fg3` 前景 + **lock 图标**(非 power)+ not-allowed;点击走「看广告加时」。原型原子 `.connect-btn.is-disabled`
|
||||
> 注意:背景在渐变↔纯色间切换时,过渡只用 `box-shadow`/`background-color`,**不要 `transition: all`**(会导致背景计算失效)。
|
||||
|
||||
---
|
||||
@@ -169,5 +170,5 @@ SKILL.md ← Agent Skill 入口
|
||||
- [ ] 明/暗两主题均验证过;中/英两语言均验证过(单显不并排)
|
||||
- [ ] 文案无铁律 13 红线词;套餐数字与 §7 一致
|
||||
- [ ] 图标只用 Lucide;国家码块无 emoji 国旗;状态胶囊无 emoji
|
||||
- [ ] 连接键三态、智能选择默认选中、免费版额度/广告解锁流程与 React 原型一致
|
||||
- [ ] 连接键四态(含免费到点 disabled 锁态)、智能选择默认选中、免费版额度(倒计时收缩条)/看广告加时流程与原型单源一致
|
||||
- [ ] App/官网内无任何支付表单,购买只引导到外部渠道
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
#!/usr/bin/env node
|
||||
// gen_flutter_icons.mjs —— 图标单源 codegen(ds-flow step 2a)。
|
||||
// 图标真源 = design/prototype/icons.js(Lucide sprite)。读其 key 集,kebab→camel,
|
||||
// 生成 client/lib/widgets/pangolin_icons.dart(每个 sprite key 一行 static const)。
|
||||
// 零依赖(Node fs)。运行:node design/codegen/gen_flutter_icons.mjs
|
||||
import { readFileSync, writeFileSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { dirname, resolve } from 'node:path';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const root = resolve(__dirname, '..', '..');
|
||||
const SPRITE = resolve(root, 'design/prototype/icons.js');
|
||||
const OUT = resolve(root, 'client/lib/widgets/pangolin_icons.dart');
|
||||
|
||||
const src = readFileSync(SPRITE, 'utf8');
|
||||
// 抽 key:形如 'kebab-name': '<...
|
||||
const keys = [];
|
||||
const re = /'([a-z0-9-]+)'\s*:\s*'</g;
|
||||
let m;
|
||||
while ((m = re.exec(src)) !== null) keys.push(m[1]);
|
||||
if (keys.length === 0) { console.error('no icon keys found in', SPRITE); process.exit(1); }
|
||||
// 去重、稳定顺序(保留 sprite 出现次序)
|
||||
const seen = new Set();
|
||||
const ordered = keys.filter((k) => (seen.has(k) ? false : (seen.add(k), true)));
|
||||
|
||||
const camel = (k) => k.replace(/-([a-z0-9])/g, (_, c) => c.toUpperCase());
|
||||
|
||||
const lines = ordered.map((k) => {
|
||||
const c = camel(k);
|
||||
return ` static const ${c} = LucideIcons.${c};`;
|
||||
});
|
||||
|
||||
const out = `// GENERATED by design/codegen/gen_flutter_icons.mjs —— 勿手改;图标真源=design/prototype/icons.js
|
||||
// 重新生成:node design/codegen/gen_flutter_icons.mjs
|
||||
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||||
|
||||
class PangolinIcons {
|
||||
PangolinIcons._();
|
||||
|
||||
${lines.join('\n')}
|
||||
}
|
||||
`;
|
||||
|
||||
writeFileSync(OUT, out);
|
||||
console.log(`generated ${OUT} (${ordered.length} icons)`);
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../assets
|
||||
+443
-11
@@ -82,8 +82,8 @@
|
||||
}
|
||||
.btn-subtle:hover { border-color: var(--accent); color: var(--accent); }
|
||||
|
||||
/* Danger — 危险动作(断开 / 移除设备) */
|
||||
.btn-danger { background: var(--danger-subtle); color: var(--red-600); }
|
||||
/* Danger — 危险动作(断开 / 移除设备)。文字用语义 --danger(对齐线上 c.danger,随主题翻色) */
|
||||
.btn-danger { background: var(--danger-subtle); color: var(--danger); }
|
||||
.btn-danger:hover { background: var(--danger); color: var(--fg-on-accent); }
|
||||
|
||||
/* 尺寸 / 形态修饰 */
|
||||
@@ -94,12 +94,13 @@
|
||||
|
||||
/* =============================================================
|
||||
CARD · .card
|
||||
surface 底 + border + radius-xl + shadow-sm
|
||||
surface 底 + border + radius-lg + shadow-sm
|
||||
圆角对齐线上 cardTheme(PangolinRadius.lg=14);阴影保留 shadow-sm(暖阴影铁律)。
|
||||
============================================================= */
|
||||
.card {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-xl);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-sm);
|
||||
padding: var(--space-5);
|
||||
}
|
||||
@@ -164,27 +165,29 @@
|
||||
line-height: 1;
|
||||
padding: var(--space-2) var(--space-3);
|
||||
border-radius: var(--radius-full);
|
||||
background: var(--sand-100);
|
||||
background: var(--bg-subtle);
|
||||
color: var(--fg2);
|
||||
}
|
||||
.pill .dot {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: var(--radius-full);
|
||||
background: var(--sand-400);
|
||||
background: var(--fg3);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
/* alias:.badge === .pill(语义等价,命名习惯不同) */
|
||||
.badge { }
|
||||
|
||||
.pill.is-success { background: var(--success-subtle); color: var(--green-600); }
|
||||
/* 状态色对齐线上 status_pill.dart:文字用语义档 --success/--warning/--danger(非 -600 深档),
|
||||
neutral 用 --bg-subtle/--fg3(原 sand-100/sand-400 硬编码在深色不翻色,已修)。 */
|
||||
.pill.is-success { background: var(--success-subtle); color: var(--success); }
|
||||
.pill.is-success .dot { background: var(--success); }
|
||||
.pill.is-warning { background: var(--warning-subtle); color: var(--amber-600); }
|
||||
.pill.is-warning { background: var(--warning-subtle); color: var(--warning); }
|
||||
.pill.is-warning .dot { background: var(--warning); }
|
||||
.pill.is-danger { background: var(--danger-subtle); color: var(--red-600); }
|
||||
.pill.is-danger { background: var(--danger-subtle); color: var(--danger); }
|
||||
.pill.is-danger .dot { background: var(--danger); }
|
||||
.pill.is-neutral { background: var(--sand-100); color: var(--fg2); }
|
||||
.pill.is-neutral .dot { background: var(--sand-400); }
|
||||
.pill.is-neutral { background: var(--bg-subtle); color: var(--fg2); }
|
||||
.pill.is-neutral .dot { background: var(--fg3); }
|
||||
|
||||
/* 实心 accent(PRO 会员等强调标签) */
|
||||
.pill-accent { background: var(--accent); color: var(--fg-on-accent); }
|
||||
@@ -246,6 +249,8 @@
|
||||
gap: 1px;
|
||||
z-index: 60;
|
||||
}
|
||||
/* hidden 属性生效:菜单默认收起,靠 JS 去/加 hidden 或 .is-open 控制显隐 */
|
||||
.menu[hidden] { display: none; }
|
||||
.menu-item {
|
||||
display: block;
|
||||
width: 100%;
|
||||
@@ -270,3 +275,430 @@
|
||||
color: var(--accent);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
|
||||
/* =============================================================
|
||||
PLAN CARD · .plan-card (购买套餐卡)
|
||||
surface 底常规档;.is-featured 走 clay 渐变高亮(年付/最优惠)。
|
||||
镜像 client/lib/widgets/plan_card.dart 的视觉配方(原型先行单源)。
|
||||
子件:.plan-name / .plan-price / .plan-per / .plan-feats / .plan-feat
|
||||
/ .plan-pop(顶部「最受欢迎」浮标)。
|
||||
============================================================= */
|
||||
.plan-card {
|
||||
position: relative;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-xl);
|
||||
box-shadow: var(--shadow-sm);
|
||||
padding: var(--space-6) var(--space-5) var(--space-5);
|
||||
}
|
||||
.plan-name {
|
||||
font-family: var(--font-sans);
|
||||
font-size: var(--text-sm);
|
||||
font-weight: 600;
|
||||
color: var(--fg2);
|
||||
}
|
||||
.plan-price-row { display: flex; align-items: baseline; gap: var(--space-2); margin-top: var(--space-2); }
|
||||
.plan-price {
|
||||
font-family: var(--font-display);
|
||||
font-size: var(--text-display);
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
letter-spacing: var(--tracking-tight);
|
||||
color: var(--fg1);
|
||||
}
|
||||
.plan-per { font-family: var(--font-sans); font-size: var(--text-sm); color: var(--fg3); }
|
||||
.plan-feats { margin: var(--space-4) 0 var(--space-5); display: flex; flex-direction: column; gap: var(--space-2); }
|
||||
.plan-feat { display: flex; align-items: center; gap: var(--space-3); font-family: var(--font-sans); font-size: var(--text-sm); color: var(--fg2); }
|
||||
.plan-feat > svg { width: 15px; height: 15px; flex-shrink: 0; color: var(--success); }
|
||||
|
||||
/* 「最受欢迎」浮标:白底胶囊悬于卡顶 */
|
||||
.plan-pop {
|
||||
position: absolute;
|
||||
top: calc(-1 * var(--space-3));
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: #fff;
|
||||
color: var(--clay-700);
|
||||
font-family: var(--font-sans);
|
||||
font-size: var(--text-caption);
|
||||
font-weight: 700;
|
||||
padding: var(--space-1) var(--space-3);
|
||||
border-radius: var(--radius-full);
|
||||
box-shadow: var(--shadow-sm);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Featured — clay 渐变高亮(年付/最优惠)。渐变内文字恒为白,靠 opacity 分级。 */
|
||||
.plan-card.is-featured {
|
||||
background: linear-gradient(135deg, var(--clay-600), var(--clay-800));
|
||||
border-color: transparent;
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
.plan-card.is-featured .plan-name { color: #fff; opacity: 0.85; }
|
||||
.plan-card.is-featured .plan-price { color: #fff; }
|
||||
.plan-card.is-featured .plan-per { color: #fff; opacity: 0.85; }
|
||||
.plan-card.is-featured .plan-feat { color: #fff; opacity: 0.92; }
|
||||
.plan-card.is-featured .plan-feat > svg { color: #fff; }
|
||||
/* Featured 卡内 CTA 反色:白底 clay 字 */
|
||||
.plan-card.is-featured .btn-primary { background: #fff; color: var(--clay-700); }
|
||||
.plan-card.is-featured .btn-primary:hover { background: #fff; opacity: 0.92; }
|
||||
|
||||
|
||||
/* =============================================================
|
||||
PAY METHOD · .pay-method (选择支付方式列表项)
|
||||
底部弹层里的一行:leading 图标 + 标题 + trailing chevron。
|
||||
资金外流:点选后拉起外部渠道 / 展示地址,App 内无卡号录入表单(铁律 #10)。
|
||||
============================================================= */
|
||||
.pay-method {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-4);
|
||||
width: 100%;
|
||||
padding: var(--space-4) var(--space-4);
|
||||
border: none;
|
||||
background: transparent;
|
||||
border-radius: var(--radius-md);
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
transition: background-color var(--dur-fast) var(--ease-out);
|
||||
}
|
||||
.pay-method:hover { background: var(--bg-subtle); }
|
||||
.pay-method > .pm-ic { width: 22px; height: 22px; flex-shrink: 0; color: var(--fg2); }
|
||||
.pay-method > .pm-title { flex: 1; font-family: var(--font-sans); font-size: var(--text-body); color: var(--fg1); }
|
||||
.pay-method > .pm-chev { width: 18px; height: 18px; flex-shrink: 0; color: var(--fg3); }
|
||||
|
||||
|
||||
/* =============================================================
|
||||
KV ROW · .kv-row (可复制键值行 — 支付明细:网络/地址/金额)
|
||||
label(次要)+ 等宽值 + 复制按钮。用于 crypto_address / qr 支付态。
|
||||
============================================================= */
|
||||
.kv-row { display: flex; align-items: flex-start; gap: var(--space-3); padding: var(--space-2) 0; }
|
||||
.kv-label { width: 76px; flex-shrink: 0; font-family: var(--font-sans); font-size: var(--text-sm); color: var(--fg3); padding-top: 2px; }
|
||||
.kv-val { flex: 1; min-width: 0; font-family: var(--font-mono); font-size: var(--text-sm); color: var(--fg1); word-break: break-all; font-feature-settings: 'tnum' 1; }
|
||||
.kv-copy {
|
||||
flex-shrink: 0;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--fg3);
|
||||
cursor: pointer;
|
||||
padding: var(--space-1);
|
||||
border-radius: var(--radius-sm);
|
||||
transition: color var(--dur-fast) var(--ease-out), background-color var(--dur-fast) var(--ease-out);
|
||||
}
|
||||
.kv-copy:hover { color: var(--accent); background: var(--accent-subtle); }
|
||||
.kv-copy > svg { width: 16px; height: 16px; display: block; }
|
||||
|
||||
|
||||
/* =============================================================
|
||||
RESULT STATE · .result (支付成功 / 失败终态)
|
||||
居中大图标 + 标题 + 说明。成功用 --success,失败用 --danger。
|
||||
============================================================= */
|
||||
.result { display: flex; flex-direction: column; align-items: center; text-align: center; padding: var(--space-8) var(--space-6); }
|
||||
.result-icon {
|
||||
width: 72px; height: 72px;
|
||||
border-radius: var(--radius-full);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
background: var(--success-subtle);
|
||||
color: var(--success);
|
||||
margin-bottom: var(--space-4);
|
||||
}
|
||||
.result-icon > svg { width: 38px; height: 38px; }
|
||||
.result.is-fail .result-icon { background: var(--danger-subtle); color: var(--danger); }
|
||||
.result-title { font-family: var(--font-display); font-size: var(--text-h2); font-weight: 700; color: var(--fg1); letter-spacing: var(--tracking-snug); }
|
||||
.result.is-fail .result-title { color: var(--danger); }
|
||||
.result-sub { margin-top: var(--space-2); font-family: var(--font-sans); font-size: var(--text-sm); color: var(--fg2); }
|
||||
|
||||
|
||||
/* =============================================================
|
||||
CONNECT BUTTON · .connect-btn (核心连接键 — 产品招牌组件)
|
||||
圆形一键连接键,严格三态:.is-off / .is-connecting / .is-on。
|
||||
镜像 client/lib/widgets/connect_button.dart 的视觉配方(原型先行单源)。
|
||||
⚠️ 铁律(design/CLAUDE.md §5):背景在渐变↔纯色间切换,过渡只走
|
||||
box-shadow / background-color,禁 transition:all(否则背景计算失效)。
|
||||
注:圆内文字/图标/环在 clay·green 实底上恒为白(对齐 Flutter PangolinColors.white),
|
||||
故用白名单 #fff;轨道环半透明白靠 stroke-opacity 属性表达(非 rgba 硬编码色)。
|
||||
============================================================= */
|
||||
.connect-btn {
|
||||
position: relative;
|
||||
width: 140px;
|
||||
height: 140px;
|
||||
border: none;
|
||||
border-radius: var(--radius-full);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--space-1);
|
||||
cursor: pointer;
|
||||
transition: background-color var(--dur-slow) var(--ease-out),
|
||||
box-shadow var(--dur-slow) var(--ease-out);
|
||||
}
|
||||
.connect-btn:active { transform: scale(0.97); }
|
||||
.connect-btn:focus-visible { outline: none; box-shadow: var(--shadow-focus); }
|
||||
|
||||
/* 轨道环:铺满按钮的 svg,垫在圆内文字/图标之下 */
|
||||
.cb-ring { position: absolute; inset: 0; z-index: 0; }
|
||||
.cb-ring circle { fill: none; }
|
||||
.cb-icon, .cb-label, .cb-timer, .cb-sub { position: relative; z-index: 1; }
|
||||
.cb-icon { width: 38px; height: 38px; color: currentColor; }
|
||||
.cb-label {
|
||||
font-family: var(--font-sans);
|
||||
font-size: var(--text-caption);
|
||||
font-weight: 700;
|
||||
letter-spacing: var(--tracking-wide);
|
||||
}
|
||||
.cb-timer {
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--text-body-lg);
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
font-feature-settings: 'tnum' 1;
|
||||
}
|
||||
.cb-sub {
|
||||
font-family: var(--font-sans);
|
||||
font-size: var(--text-caption);
|
||||
font-weight: 700;
|
||||
letter-spacing: var(--tracking-caps);
|
||||
opacity: 0.85;
|
||||
}
|
||||
@keyframes cb-spin { to { transform: rotate(360deg); } }
|
||||
.cb-spin { animation: cb-spin 1.1s linear infinite; transform-origin: 50% 50%; }
|
||||
|
||||
/* OFF:暖灰底(微渐变)+ clay 前景 + 虚线轨道环 */
|
||||
.connect-btn.is-off {
|
||||
background: radial-gradient(circle at 50% 38%, var(--sand-50), var(--sand-100));
|
||||
color: var(--accent);
|
||||
box-shadow: inset 0 0 0 1px var(--sand-200), var(--shadow-md);
|
||||
}
|
||||
.connect-btn.is-off .cb-sub { color: var(--fg3); opacity: 1; }
|
||||
.cb-track { stroke: var(--sand-200); stroke-width: 3; stroke-linecap: round; stroke-dasharray: 2 7; }
|
||||
|
||||
/* CONNECTING:clay 实底 + accent 光晕 + 旋转进度弧 */
|
||||
.connect-btn.is-connecting {
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
box-shadow: 0 0 0 8px var(--accent-subtle), var(--shadow-lg);
|
||||
}
|
||||
.cb-arc-track { stroke: #fff; stroke-opacity: 0.3; stroke-width: 4; }
|
||||
.cb-arc { stroke: #fff; stroke-width: 4; stroke-linecap: round; stroke-dasharray: 100 302; }
|
||||
|
||||
/* ON:success 实底 + 绿光晕 + 满白环 + 圆内计时 */
|
||||
.connect-btn.is-on {
|
||||
background: var(--success);
|
||||
color: #fff;
|
||||
box-shadow: 0 0 0 8px var(--success-subtle), var(--shadow-lg);
|
||||
}
|
||||
.cb-ring-full { stroke: #fff; stroke-opacity: 0.85; stroke-width: 4; stroke-linecap: round; }
|
||||
|
||||
/* DISABLED(免费到点切断 / 额度耗尽)——对齐线上 connect_button.dart 第 4 态:
|
||||
bg-subtle 底 + fg3 前景 + lock 图标 + not-allowed;点击走「看广告加时」。仅 off 场景触发。 */
|
||||
.connect-btn.is-disabled {
|
||||
background: var(--bg-subtle);
|
||||
color: var(--fg3);
|
||||
box-shadow: var(--shadow-md);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.connect-btn.is-disabled .cb-track { stroke: var(--border-strong); }
|
||||
|
||||
|
||||
/* =============================================================
|
||||
SERVER ROW · .server-row (节点列表行)
|
||||
镜像 client/lib/widgets/server_tile.dart + design/preview/comp-server-row.html。
|
||||
放进 .card.card-flush 列表容器;一行 = 国家码块 + 名称/子名 + 属性胶囊
|
||||
(.pill-outline) + 延迟(mono·按值变色) + 信号条 + 选中/推荐/不可用态。
|
||||
============================================================= */
|
||||
.server-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-3);
|
||||
padding: var(--space-3) var(--space-4);
|
||||
border-bottom: 1px solid var(--border);
|
||||
cursor: pointer;
|
||||
transition: background-color var(--dur-fast) var(--ease-out);
|
||||
}
|
||||
.server-row:last-child { border-bottom: none; }
|
||||
.server-row:hover { background: var(--bg-subtle); }
|
||||
|
||||
/* 国家码块(2 字母码,绝不用 emoji 国旗 · 铁律 #8) */
|
||||
.sr-cc {
|
||||
width: 34px; height: 34px; flex-shrink: 0;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--sand-100);
|
||||
font-family: var(--font-mono); font-size: var(--text-caption); font-weight: 600;
|
||||
color: var(--fg2);
|
||||
}
|
||||
.sr-body { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
|
||||
.sr-title { font-family: var(--font-sans); font-size: var(--text-sm); font-weight: 600; color: var(--fg1); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.sr-sub { font-family: var(--font-sans); font-size: var(--text-caption); font-weight: 400; color: var(--fg3); }
|
||||
.sr-tags { display: flex; flex-wrap: wrap; gap: var(--space-2); margin-top: 2px; }
|
||||
.sr-tags .pill { padding: 3px var(--space-2); font-size: 11px; }
|
||||
|
||||
/* 延迟(等宽 · tabular nums)——对齐线上 server_tile:**恒 fg2,不按阈值变色**;
|
||||
快慢改由信号条格数编码。旧 .is-good/.is-warn/.is-bad 一律回落 fg2(兼容不变色)。 */
|
||||
.sr-latency { flex-shrink: 0; font-family: var(--font-mono); font-size: var(--text-caption); color: var(--fg2); font-feature-settings: 'tnum' 1; }
|
||||
.sr-latency.is-good, .sr-latency.is-warn, .sr-latency.is-bad { color: var(--fg2); }
|
||||
|
||||
/* 信号条(纯 div + token 色)——默认暗格 border-strong,按等级点亮 success:
|
||||
lv3=3 格(ping<40) / lv2=前 2 格(<90) / lv1=第 1 格(≥90)。对齐线上 SignalBars。 */
|
||||
.sr-signal { display: flex; align-items: flex-end; gap: 2px; height: 14px; flex-shrink: 0; }
|
||||
.sr-signal i { width: 3px; border-radius: 1px; background: var(--border-strong); }
|
||||
.sr-signal i:nth-child(1) { height: 5px; }
|
||||
.sr-signal i:nth-child(2) { height: 9px; }
|
||||
.sr-signal i:nth-child(3) { height: 14px; }
|
||||
.sr-signal.lv3 i,
|
||||
.sr-signal.lv2 i:nth-child(-n+2),
|
||||
.sr-signal.lv1 i:nth-child(1) { background: var(--success); }
|
||||
.sr-signal i.is-off { background: var(--border-strong); }
|
||||
|
||||
.sr-check { flex-shrink: 0; color: var(--accent); display: inline-flex; }
|
||||
.sr-check svg { width: 18px; height: 18px; }
|
||||
|
||||
/* 选中态:accent-subtle 底 + 码块反色实心 clay */
|
||||
.server-row.is-selected { background: var(--accent-subtle); }
|
||||
.server-row.is-selected:hover { background: var(--accent-subtle); }
|
||||
.server-row.is-selected .sr-cc { background: var(--accent); color: var(--fg-on-accent); }
|
||||
|
||||
/* 不可用态(agent 离线) */
|
||||
.server-row.is-down { opacity: 0.55; cursor: not-allowed; }
|
||||
.server-row.is-down:hover { background: transparent; }
|
||||
.sr-unavail { flex-shrink: 0; font-family: var(--font-sans); font-size: var(--text-caption); font-weight: 600; color: var(--danger); }
|
||||
|
||||
/* 智能选择推荐行(节点页置顶 · design/CLAUDE.md §5) */
|
||||
.server-row.is-smart { background: var(--accent-subtle); }
|
||||
.sr-smart-ic {
|
||||
width: 34px; height: 34px; flex-shrink: 0;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
border-radius: var(--radius-md);
|
||||
background: linear-gradient(135deg, var(--clay-500), var(--clay-700)); /* 对齐线上 smart_select_card */
|
||||
color: #fff; /* ds-allow: clay 渐变块上恒为白,对齐 .ds-brand .mk / plan-card featured */
|
||||
}
|
||||
.sr-smart-ic svg { width: 18px; height: 18px; }
|
||||
|
||||
|
||||
/* =============================================================
|
||||
线上对齐补建原子(2026-07 回填:atoms 对齐 client/lib/widgets)
|
||||
============================================================= */
|
||||
|
||||
/* ICON BOX · accent-subtle 圆角图标框(设备/设置行前导) */
|
||||
.icon-box { width: 36px; height: 36px; flex-shrink: 0; display: flex; align-items: center; justify-content: center; border-radius: var(--radius-md); background: var(--accent-subtle); color: var(--accent); }
|
||||
.icon-box > svg { width: 18px; height: 18px; }
|
||||
.icon-box.is-danger { background: var(--danger-subtle); color: var(--danger); }
|
||||
|
||||
/* STATUS DOT · 独立状态点 */
|
||||
.status-dot { width: 8px; height: 8px; border-radius: var(--radius-full); background: var(--fg3); flex-shrink: 0; display: inline-block; }
|
||||
.status-dot.is-online { background: var(--success); }
|
||||
.status-dot.is-offline { background: var(--fg3); }
|
||||
.status-dot.is-busy { background: var(--warning); }
|
||||
|
||||
/* TOGGLE · 开关(对齐线上 dtoggle) */
|
||||
.toggle { width: 42px; height: 24px; flex-shrink: 0; border-radius: var(--radius-full); background: var(--border-strong); border: none; cursor: pointer; position: relative; transition: background-color var(--dur-fast) var(--ease-out); }
|
||||
.toggle > i { position: absolute; top: 3px; left: 3px; width: 18px; height: 18px; border-radius: var(--radius-full); background: #fff; box-shadow: var(--shadow-sm); transition: transform var(--dur-fast) var(--ease-out); } /* ds-allow: 开关滑块恒为白 */
|
||||
.toggle.is-on { background: var(--accent); }
|
||||
.toggle.is-on > i { transform: translateX(18px); }
|
||||
|
||||
/* SEGSWITCH · 二选一段控开关(支付渠道/币种等的横向 switch;iOS 式滑块高亮) */
|
||||
.segswitch { display: flex; background: var(--bg-subtle); border: 1px solid var(--border); border-radius: var(--radius-full); padding: var(--space-1); gap: var(--space-1); }
|
||||
.segswitch > .segswitch-opt { flex: 1; border: none; background: transparent; cursor: pointer; font-family: var(--font-sans); font-size: var(--text-sm); font-weight: 600; color: var(--fg2); padding: var(--space-2) var(--space-4); border-radius: var(--radius-full); display: inline-flex; align-items: center; justify-content: center; gap: var(--space-2); transition: background-color var(--dur-fast) var(--ease-out), color var(--dur-fast) var(--ease-out); }
|
||||
.segswitch > .segswitch-opt > svg { width: 16px; height: 16px; flex-shrink: 0; }
|
||||
.segswitch > .segswitch-opt.is-active { background: var(--surface); color: var(--fg1); box-shadow: var(--shadow-sm); }
|
||||
|
||||
/* FIELD BOX · 复合输入(前置图标 + 裸 input + 后置槽;对齐 pangolin_field.dart) */
|
||||
.field-box { display: flex; align-items: center; gap: var(--space-3); background: var(--surface); border: 1.5px solid var(--border-strong); border-radius: var(--radius-md); padding: 0 var(--space-4); transition: border-color var(--dur-fast) var(--ease-out), box-shadow var(--dur-fast) var(--ease-out); }
|
||||
.field-box.is-focused { border-color: var(--accent); box-shadow: var(--shadow-focus); }
|
||||
.field-box .fb-lead { width: 18px; height: 18px; flex-shrink: 0; color: var(--fg3); }
|
||||
.field-box .fb-input { flex: 1; min-width: 0; border: none; background: transparent; font-family: var(--font-sans); font-size: var(--text-sm); color: var(--fg1); padding: var(--space-3) 0; }
|
||||
.field-box .fb-input:focus { outline: none; }
|
||||
.field-box .fb-input::placeholder { color: var(--fg3); }
|
||||
.field-box .fb-trailing { flex-shrink: 0; display: inline-flex; color: var(--fg3); }
|
||||
.field-box.is-plain { border-color: transparent; background: var(--bg-subtle); box-shadow: none; }
|
||||
|
||||
/* TOAST · 轻提示(对齐 pangolin_toast / snackBarTheme) */
|
||||
.toast { display: inline-flex; align-items: center; justify-content: center; gap: var(--space-2); text-align: center; background: var(--surface); color: var(--fg1); border: 1px solid var(--border); border-radius: var(--radius-md); box-shadow: var(--shadow-lg); padding: var(--space-3) var(--space-4); font-family: var(--font-sans); font-size: var(--text-sm); font-weight: 600; }
|
||||
.toast > svg { width: 14px; height: 14px; flex-shrink: 0; }
|
||||
|
||||
/* QUOTA CARD · 免费版今日额度(对齐 quota_card.dart:倒计时收缩条 + 免费版标签 + 三档阈值;无绿色「已解锁」态) */
|
||||
.quota-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); box-shadow: var(--shadow-sm); padding: var(--space-3) var(--space-4); display: flex; flex-direction: column; gap: var(--space-2); }
|
||||
.quota-head { display: flex; align-items: center; gap: var(--space-2); }
|
||||
.quota-head > svg { width: 15px; height: 15px; color: var(--accent); flex-shrink: 0; }
|
||||
.quota-card.is-exhausted .quota-head > svg { color: var(--danger); }
|
||||
.quota-label { font-family: var(--font-sans); font-size: var(--text-caption); font-weight: 600; color: var(--fg2); }
|
||||
.quota-val { font-family: var(--font-mono); font-size: 14px; font-weight: 600; color: var(--fg1); font-feature-settings: 'tnum' 1; }
|
||||
.quota-tag { margin-left: auto; background: var(--bg-subtle); color: var(--fg3); font-family: var(--font-sans); font-size: 11px; font-weight: 600; padding: 3px var(--space-2); border-radius: var(--radius-full); }
|
||||
.quota-bar { height: 6px; border-radius: var(--radius-full); background: var(--bg-subtle); overflow: hidden; }
|
||||
.quota-bar > i { display: block; height: 100%; border-radius: var(--radius-full); background: var(--accent); transition: width var(--dur-base) var(--ease-out); }
|
||||
.quota-bar.is-low > i { background: var(--warning); }
|
||||
.quota-bar.is-exhausted > i { background: var(--danger); }
|
||||
|
||||
/* METRIC · 统计指标瓦片 */
|
||||
.metric { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); box-shadow: var(--shadow-sm); padding: var(--space-3); }
|
||||
.metric .m-ic { width: 26px; height: 26px; border-radius: var(--radius-sm); background: var(--accent-subtle); color: var(--accent); display: flex; align-items: center; justify-content: center; margin-bottom: var(--space-2); }
|
||||
.metric .m-ic > svg { width: 15px; height: 15px; }
|
||||
.metric .m-val { font-family: var(--font-mono); font-size: var(--text-body); font-weight: 600; color: var(--fg1); font-feature-settings: 'tnum' 1; }
|
||||
.metric .m-label { font-family: var(--font-sans); font-size: 11px; color: var(--fg3); margin-top: 2px; }
|
||||
|
||||
/* DEVICE ROW · 设备管理行(对齐 DevicesScreen:icon-box + 名 + 最后在线 + 本机/移除/⋯) */
|
||||
.device-row { display: flex; align-items: center; gap: var(--space-3); padding: var(--space-3) var(--space-4); border-bottom: 1px solid var(--border); }
|
||||
.device-row:last-child { border-bottom: none; }
|
||||
.device-row .dv-body { flex: 1; min-width: 0; }
|
||||
.device-row .dv-name { font-family: var(--font-sans); font-size: var(--text-sm); font-weight: 600; color: var(--fg1); }
|
||||
.device-row .dv-sub { font-family: var(--font-sans); font-size: var(--text-caption); color: var(--fg3); margin-top: 2px; display: flex; align-items: center; gap: var(--space-2); }
|
||||
.device-row .dv-trail { flex-shrink: 0; display: inline-flex; align-items: center; gap: var(--space-2); }
|
||||
|
||||
/* SETTING ROW · 设置/导航行(对齐 settings _Row/_NavRow:icon-box + 标题 + 副标 + trailing) */
|
||||
.setting-row { display: flex; align-items: center; gap: var(--space-3); padding: var(--space-3) var(--space-4); border-bottom: 1px solid var(--border); }
|
||||
.setting-row:last-child { border-bottom: none; }
|
||||
.setting-row.is-tap { cursor: pointer; transition: background-color var(--dur-fast) var(--ease-out); }
|
||||
.setting-row.is-tap:hover { background: var(--bg-subtle); }
|
||||
.setting-row .st-body { flex: 1; min-width: 0; }
|
||||
.setting-row .st-title { font-family: var(--font-sans); font-size: var(--text-sm); font-weight: 600; color: var(--fg1); }
|
||||
.setting-row .st-sub { font-family: var(--font-sans); font-size: var(--text-caption); color: var(--fg3); margin-top: 2px; }
|
||||
.setting-row .st-trail { flex-shrink: 0; display: inline-flex; align-items: center; gap: var(--space-2); color: var(--fg3); }
|
||||
.setting-row .st-trail > svg { width: 18px; height: 18px; }
|
||||
.setting-row .st-val { font-family: var(--font-sans); font-size: var(--text-sm); color: var(--fg2); }
|
||||
|
||||
/* PLAN BANNER · 账户页套餐横幅(免费=surface / .is-pro=clay 渐变;对齐 _PlanBanner) */
|
||||
.plan-banner { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); box-shadow: var(--shadow-sm); padding: var(--space-5); }
|
||||
.plan-banner .pbn-row { display: flex; align-items: center; gap: var(--space-3); }
|
||||
.plan-banner .pbn-info { flex: 1 1 auto; min-width: 0; }
|
||||
.plan-banner .pbn-row > .btn { flex-shrink: 0; }
|
||||
.plan-banner .pbn-badge { width: 44px; height: 44px; flex-shrink: 0; border-radius: var(--radius-lg); display: flex; align-items: center; justify-content: center; background: var(--accent-subtle); color: var(--accent); }
|
||||
.plan-banner .pbn-badge > svg { width: 24px; height: 24px; }
|
||||
.plan-banner .pbn-name { font-family: var(--font-display); font-size: var(--text-h3); font-weight: 700; color: var(--fg1); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.plan-banner .pbn-meta { font-family: var(--font-sans); font-size: var(--text-caption); color: var(--fg3); margin-top: 2px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.plan-banner.is-pro { background: linear-gradient(135deg, var(--clay-600), var(--clay-800)); border-color: transparent; box-shadow: var(--shadow-md); }
|
||||
.plan-banner.is-pro .pbn-badge { background: transparent; border: 1.5px solid #fff; color: #fff; }
|
||||
.plan-banner.is-pro .pbn-name { color: #fff; }
|
||||
.plan-banner.is-pro .pbn-meta { color: #fff; opacity: 0.85; }
|
||||
.plan-banner.is-pro .btn-primary { background: #fff; color: var(--clay-700); }
|
||||
|
||||
/* PLAN BADGE · 桌面侧栏底部会员卡(对齐 plan_badge_card.dart) */
|
||||
.plan-badge { display: flex; align-items: center; gap: var(--space-2); background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-md); padding: var(--space-2) var(--space-3); cursor: pointer; transition: background-color var(--dur-fast) var(--ease-out), border-color var(--dur-fast) var(--ease-out); }
|
||||
.plan-badge.is-active { background: var(--accent-subtle); border-color: var(--accent); }
|
||||
.plan-badge .plb-crown { width: 30px; height: 30px; flex-shrink: 0; display: flex; align-items: center; justify-content: center; border-radius: var(--radius-full); background: linear-gradient(135deg, var(--clay-500), var(--clay-700)); color: #fff; } /* ds-allow: clay 渐变块上恒为白 */
|
||||
.plan-badge .plb-crown > svg { width: 15px; height: 15px; }
|
||||
.plan-badge .plb-body { flex: 1; min-width: 0; display: flex; flex-direction: column; }
|
||||
.plan-badge .plb-name { font-family: var(--font-sans); font-size: var(--text-caption); font-weight: 600; color: var(--fg1); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.plan-badge .plb-sub { font-family: var(--font-sans); font-size: var(--text-caption); color: var(--fg3); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.plan-badge .plb-chev { width: 14px; height: 14px; flex-shrink: 0; color: var(--fg3); }
|
||||
|
||||
/* UPDATE BANNER · jiu 式更新顶部横条(对齐 UpdateBanner;非强制态) */
|
||||
.update-banner { display: flex; align-items: center; gap: var(--space-3); background: var(--accent-subtle); padding: var(--space-2) var(--space-3) var(--space-2) var(--space-4); }
|
||||
.update-banner .ub-ic { width: 17px; height: 17px; flex-shrink: 0; color: var(--accent); }
|
||||
.update-banner .ub-title { flex: 1; min-width: 0; font-family: var(--font-sans); font-size: var(--text-sm); font-weight: 700; color: var(--fg1); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.update-banner .ub-cta { flex-shrink: 0; background: var(--accent); color: var(--fg-on-accent); border: none; border-radius: var(--radius-full); padding: var(--space-1) var(--space-4); font-family: var(--font-sans); font-size: var(--text-caption); font-weight: 700; cursor: pointer; }
|
||||
.update-banner .ub-close { flex-shrink: 0; width: 30px; height: 30px; display: flex; align-items: center; justify-content: center; border: none; background: transparent; color: var(--fg3); cursor: pointer; border-radius: var(--radius-sm); }
|
||||
.update-banner .ub-close > svg { width: 16px; height: 16px; }
|
||||
|
||||
/* TAB BAR · 移动端底部 4-Tab(对齐 bottom_tab_bar.dart) */
|
||||
.tabbar { display: flex; height: 60px; border-top: 1px solid var(--border); background: var(--surface); }
|
||||
.tab { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 3px; border: none; background: none; cursor: pointer; color: var(--fg3); font-family: var(--font-sans); font-size: 11px; font-weight: 600; }
|
||||
.tab > svg { width: 21px; height: 21px; }
|
||||
.tab.is-active { color: var(--accent); }
|
||||
|
||||
/* NAV ITEM · 桌面/iPad 左侧栏导航项(对齐 nav_sidebar.dart) */
|
||||
.nav-item { display: flex; align-items: center; gap: var(--space-3); width: 100%; padding: var(--space-3); border: none; background: transparent; border-radius: var(--radius-md); cursor: pointer; text-align: left; color: var(--fg2); font-family: var(--font-sans); font-size: var(--text-sm); font-weight: 600; transition: background-color var(--dur-fast) var(--ease-out), color var(--dur-fast) var(--ease-out); }
|
||||
.nav-item > svg { width: 20px; height: 20px; flex-shrink: 0; }
|
||||
.nav-item:hover { background: var(--bg-subtle); color: var(--fg1); }
|
||||
.nav-item.is-active { background: var(--accent-subtle); color: var(--accent); }
|
||||
|
||||
Vendored
-1
@@ -56,7 +56,6 @@
|
||||
'arrow-right': '<path d="M5 12h14"/><path d="m12 5 7 7-7 7"/>',
|
||||
'compass': '<path d="m16.24 7.76-1.804 5.411a2 2 0 0 1-1.265 1.265L7.76 16.24l1.804-5.411a2 2 0 0 1 1.265-1.265z"/><circle cx="12" cy="12" r="10"/>',
|
||||
'chart-no-axes-column': '<line x1="18" x2="18" y1="20" y2="10"/><line x1="12" x2="12" y1="20" y2="4"/><line x1="6" x2="6" y1="20" y2="14"/>',
|
||||
'chart': '<line x1="18" x2="18" y1="20" y2="10"/><line x1="12" x2="12" y1="20" y2="4"/><line x1="6" x2="6" y1="20" y2="14"/>',
|
||||
'home': '<path d="M3 9.5 12 3l9 6.5"/><path d="M5 10v10a1 1 0 0 0 1 1h3v-6h6v6h3a1 1 0 0 0 1-1V10"/>',
|
||||
'map-pin': '<path d="M20 10c0 4.993-5.539 10.193-7.399 11.799a1 1 0 0 1-1.202 0C9.539 20.193 4 14.993 4 10a8 8 0 0 1 16 0"/><circle cx="12" cy="10" r="3"/>',
|
||||
'laptop': '<path d="M20 16V7a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v9m16 0H4m16 0 1.28 2.55a1 1 0 0 1-.9 1.45H3.62a1 1 0 0 1-.9-1.45L4 16"/>',
|
||||
|
||||
+521
-9
@@ -37,10 +37,33 @@
|
||||
/* 三栏文档布局 */
|
||||
.doc{ display:flex; align-items:flex-start; max-width:1360px; margin:0 auto; }
|
||||
.doc-nav{ width:238px; flex:none; position:sticky; top:60px; height:calc(100vh - 60px);
|
||||
overflow:auto; border-right:1px solid var(--border); padding:var(--space-5) var(--space-4); }
|
||||
.dgroup{ font-size:var(--text-caption); font-weight:700; color:var(--fg3); letter-spacing:var(--tracking-caps);
|
||||
text-transform:uppercase; padding:var(--space-4) var(--space-2) var(--space-2); }
|
||||
.dnav{ display:block; padding:var(--space-2) var(--space-3); border-radius:var(--radius-md);
|
||||
overflow-y:auto; overscroll-behavior:contain; border-right:1px solid var(--border);
|
||||
padding:var(--space-5) var(--space-4) calc(var(--space-16) * 2);
|
||||
transition:width var(--dur-base) var(--ease-out); }
|
||||
/* 收起态(宽度由 JS 置 0,此处收内边距/边框) */
|
||||
.doc.nav-collapsed .doc-nav{ padding-left:0; padding-right:0; border-right:none; overflow:hidden; }
|
||||
.doc.nav-collapsed .nav-resize{ display:none; }
|
||||
/* 拖拽把手(改侧栏宽度) */
|
||||
.nav-resize{ flex:none; align-self:stretch; position:sticky; top:60px; height:calc(100vh - 60px);
|
||||
width:9px; margin-left:-5px; cursor:col-resize; z-index:40; background:transparent; }
|
||||
.nav-resize::after{ content:""; position:absolute; left:4px; top:0; bottom:0; width:2px; border-radius:var(--radius-full);
|
||||
background:transparent; transition:background-color var(--dur-fast) var(--ease-out); }
|
||||
.nav-resize:hover::after, .nav-resize.dragging::after{ background:var(--accent); }
|
||||
body.nav-dragging{ cursor:col-resize; user-select:none; }
|
||||
/* 顶栏收起按钮 */
|
||||
.nav-toggle{ display:inline-flex; align-items:center; justify-content:center; width:34px; height:34px; flex:none;
|
||||
border:1px solid var(--border); border-radius:var(--radius-md); background:var(--bg-subtle);
|
||||
color:var(--fg2); cursor:pointer; transition:color var(--dur-fast) var(--ease-out), border-color var(--dur-fast) var(--ease-out); }
|
||||
.nav-toggle:hover{ color:var(--fg1); border-color:var(--border-strong); }
|
||||
.nav-toggle svg{ width:18px; height:18px; }
|
||||
|
||||
/* L1 分组头 —— 分隔线 + 提亮加粗,与二级明显区分 */
|
||||
.dgroup{ font-size:var(--text-caption); font-weight:700; color:var(--fg2); letter-spacing:var(--tracking-caps);
|
||||
text-transform:uppercase; padding:var(--space-2) var(--space-2);
|
||||
margin-top:var(--space-5); border-top:1px solid var(--border); }
|
||||
.doc-nav .dgroup:first-child{ margin-top:var(--space-1); border-top:none; }
|
||||
/* L2 条目 —— 缩进内嵌于分组下 */
|
||||
.dnav{ display:block; padding:var(--space-2) var(--space-3) var(--space-2) var(--space-5); border-radius:var(--radius-md);
|
||||
font-size:var(--text-sm); color:var(--fg2); text-decoration:none; cursor:pointer;
|
||||
transition:background-color var(--dur-fast) var(--ease-out), color var(--dur-fast) var(--ease-out); }
|
||||
.dnav:hover{ background:var(--bg-subtle); color:var(--fg1); }
|
||||
@@ -102,13 +125,36 @@
|
||||
|
||||
@media (max-width:840px){
|
||||
.doc-nav{ display:none; }
|
||||
.nav-resize{ display:none; }
|
||||
.nav-toggle{ display:none; }
|
||||
.doc-main{ padding:var(--space-6) var(--space-5) var(--space-12); }
|
||||
}
|
||||
|
||||
/* ── 品牌 / 字体 specimen 展示用(登记页 chrome,非 atoms;全走 token) ── */
|
||||
.brand-appicon{ display:flex; align-items:flex-end; gap:var(--space-8); flex-wrap:wrap; }
|
||||
.brand-appicon .unit{ display:flex; flex-direction:column; align-items:center; gap:var(--space-2); }
|
||||
.brand-appicon .unit .n{ font-family:var(--font-mono); font-size:11px; color:var(--fg3); }
|
||||
.brand-appicon img{ display:block; box-shadow:var(--shadow-md); border-radius:22%; }
|
||||
.lockup{ display:flex; align-items:center; gap:var(--space-3); }
|
||||
.lockup img{ display:block; }
|
||||
.lockup .tw{ display:flex; flex-direction:column; line-height:1; }
|
||||
.lockup .cn{ font-family:var(--font-display); font-weight:700; font-size:22px; letter-spacing:var(--tracking-snug); color:var(--fg1); }
|
||||
.lockup .en{ font-family:var(--font-display); font-weight:600; font-size:10px; letter-spacing:0.22em; color:var(--accent); margin-top:4px; }
|
||||
.lockup-panel{ flex:1; min-width:220px; display:flex; flex-direction:column; align-items:center; gap:var(--space-3);
|
||||
padding:var(--space-8); border-radius:var(--radius-lg); border:1px solid var(--border); }
|
||||
.lockup-panel .plab{ font-family:var(--font-mono); font-size:10px; color:var(--fg3); }
|
||||
.spec-mono-grid{ display:grid; grid-template-columns:repeat(3,1fr); gap:var(--space-5); width:100%; }
|
||||
.spec-mono-grid .k{ font-family:var(--font-mono); font-size:11px; color:var(--fg3); margin-bottom:4px; }
|
||||
.spec-mono-grid .v{ font-family:var(--font-mono); font-size:22px; font-weight:500; letter-spacing:var(--tracking-snug);
|
||||
color:var(--fg1); font-feature-settings:'tnum' 1; }
|
||||
.spec-mono-grid .v .u{ font-size:12px; color:var(--accent); font-weight:400; }
|
||||
@media (max-width:560px){ .spec-mono-grid{ grid-template-columns:repeat(2,1fr); } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="ds-top">
|
||||
<div class="ds-brand"><span class="mk"><svg class="ic"><use href="#i-shield-check"/></svg></span>穿山甲 Pangolin</div>
|
||||
<button class="nav-toggle" id="navToggle" aria-label="收起 / 展开侧栏" title="收起 / 展开侧栏"><svg class="ic"><use href="#i-menu"/></svg></button>
|
||||
<div class="ds-brand"><span class="mk"><img src="../assets/app-icon.svg" alt="穿山甲" style="width:100%;height:100%;object-fit:contain;border-radius:inherit"></span>穿山甲 Pangolin</div>
|
||||
<span class="ds-tag">设计系统登记簿 · Prototype</span>
|
||||
<button class="theme-toggle" id="themeBtn" aria-label="切换主题">
|
||||
<svg class="ic"><use href="#i-sun" id="themeIcon"/></svg><span id="themeLabel">浅色</span>
|
||||
@@ -117,12 +163,24 @@
|
||||
|
||||
<div class="doc">
|
||||
<nav class="doc-nav" id="docNav">
|
||||
<div class="dgroup">UI Kit · 交互原型</div>
|
||||
<a class="dnav" href="screens/ui-mobile.html" style="color:var(--accent);font-weight:600">↗ 移动版 App</a>
|
||||
<a class="dnav" href="screens/ui-desktop.html" style="color:var(--accent);font-weight:600">↗ 桌面版</a>
|
||||
<a class="dnav" href="screens/site.html" style="color:var(--accent);font-weight:600">↗ 官网 Site</a>
|
||||
<div class="dgroup">品牌 Brand</div>
|
||||
<a class="dnav" data-s="b-appicon">App 图标家族</a>
|
||||
<a class="dnav" data-s="b-logo">Logo 锁版</a>
|
||||
<div class="dgroup">色彩 Colors</div>
|
||||
<a class="dnav" data-s="c-clay">clay 黏土铜色阶</a>
|
||||
<a class="dnav" data-s="c-sand">sand 沙米中性阶</a>
|
||||
<a class="dnav" data-s="c-hue">语义色相</a>
|
||||
<a class="dnav" data-s="c-sem">语义 token</a>
|
||||
<a class="dnav" data-s="c-dark">暗色主题表面</a>
|
||||
<div class="dgroup">字体 Type</div>
|
||||
<a class="dnav" data-s="t-body">正文字体 Manrope</a>
|
||||
<a class="dnav" data-s="t-cjk">中文字体 Noto Sans SC</a>
|
||||
<a class="dnav" data-s="t-display">Display 字体 Sora</a>
|
||||
<a class="dnav" data-s="t-mono">等宽数据字体 JetBrains Mono</a>
|
||||
<a class="dnav" data-s="type">字号梯度</a>
|
||||
<div class="dgroup">尺度 Scale</div>
|
||||
<a class="dnav" data-s="radius">圆角</a>
|
||||
@@ -134,11 +192,57 @@
|
||||
<a class="dnav" data-s="a-input">输入 .input</a>
|
||||
<a class="dnav" data-s="a-pill">胶囊 .pill / .badge</a>
|
||||
<a class="dnav" data-s="a-lang">语言下拉 .langsel</a>
|
||||
<a class="dnav" data-s="a-connect">核心连接按钮 .connect-btn</a>
|
||||
<a class="dnav" data-s="a-server">服务器列表行 .server-row</a>
|
||||
<a class="dnav" data-s="a-badge">状态徽章 .pill 场景</a>
|
||||
<div class="dgroup">组件 · 线上对齐</div>
|
||||
<a class="dnav" data-s="a-formctl">复合输入 · 开关 · 图标框</a>
|
||||
<a class="dnav" data-s="a-quota">额度卡 · 指标卡</a>
|
||||
<a class="dnav" data-s="a-rows">设备行 · 设置行</a>
|
||||
<a class="dnav" data-s="a-planbn">套餐横幅 · 会员卡</a>
|
||||
<a class="dnav" data-s="a-appchrome">更新条 · Tab · 侧栏项</a>
|
||||
<div class="dgroup">购买 Purchase</div>
|
||||
<a class="dnav" data-s="a-plan">套餐卡 .plan-card</a>
|
||||
<a class="dnav" data-s="a-paym">支付方式 .pay-method</a>
|
||||
<a class="dnav" data-s="a-kv">明细行 .kv-row</a>
|
||||
<a class="dnav" data-s="a-result">结果态 .result</a>
|
||||
<div class="dgroup">图标 Icons</div>
|
||||
<a class="dnav" data-s="icons">Lucide 图标库</a>
|
||||
</nav>
|
||||
|
||||
<div class="nav-resize" id="navResize" title="拖动调整侧栏宽度"></div>
|
||||
|
||||
<main class="doc-main">
|
||||
<!-- ══════════ 品牌 Brand ══════════ -->
|
||||
<section class="section" id="b-appicon">
|
||||
<h2>App 图标家族</h2>
|
||||
<p class="desc">母题=行走穿山甲(实心鳞甲剪影,朝右)置于 clay 渐变圆角方。矢量单源 <code>design/assets/app-icon.svg</code>,多尺寸直接引用同一文件(零漂移,非内联副本)。</p>
|
||||
<div class="pcard"><div class="pcard-h"><h3>多尺寸</h3><span class="tk">104 · 64 · 40 · 28</span></div><div class="pcard-b"><div class="brand-appicon">
|
||||
<div class="unit"><img src="../assets/app-icon.svg" width="104" height="104" alt="穿山甲 app 图标 104"><span class="n">104 · 主图标</span></div>
|
||||
<div class="unit"><img src="../assets/app-icon.svg" width="64" height="64" alt="app 图标 64"><span class="n">64</span></div>
|
||||
<div class="unit"><img src="../assets/app-icon.svg" width="40" height="40" alt="app 图标 40"><span class="n">40</span></div>
|
||||
<div class="unit"><img src="../assets/app-icon.svg" width="28" height="28" alt="app 图标 28"><span class="n">28</span></div>
|
||||
</div></div></div>
|
||||
</section>
|
||||
|
||||
<section class="section" id="b-logo">
|
||||
<h2>Logo 锁版</h2>
|
||||
<p class="desc">图标 mark + 字标水平锁版。浅色背景用主锁版 <code>logo-mark.svg</code>,深色背景用反白锁版 <code>logo-mark-white.svg</code>;整体横版字标 <code>logo-wordmark.svg</code>。矢量单源 <code>design/assets/</code>。</p>
|
||||
<div class="pcard"><div class="pcard-h"><h3>主锁版 / 反白锁版</h3><span class="tk">logo-mark.svg · logo-mark-white.svg</span></div><div class="pcard-b" style="align-items:stretch">
|
||||
<div class="lockup-panel" style="background:var(--sand-50)">
|
||||
<div class="lockup"><img src="../assets/logo-mark.svg" width="46" height="46" alt="穿山甲 mark"><div class="tw"><span class="cn">穿山甲</span><span class="en">PANGOLIN</span></div></div>
|
||||
<span class="plab">主锁版 · 浅色背景</span>
|
||||
</div>
|
||||
<div class="lockup-panel" data-theme="dark" style="background:var(--sand-950)">
|
||||
<div class="lockup"><img src="../assets/logo-mark-white.svg" width="46" height="46" alt="穿山甲 mark 反白"><div class="tw"><span class="cn">穿山甲</span><span class="en">PANGOLIN</span></div></div>
|
||||
<span class="plab">反白锁版 · 深色背景</span>
|
||||
</div>
|
||||
</div></div>
|
||||
<div class="pcard"><div class="pcard-h"><h3>横版字标</h3><span class="tk">logo-wordmark.svg · 240×56</span></div><div class="pcard-b" style="background:var(--sand-50)">
|
||||
<img src="../assets/logo-wordmark.svg" width="240" height="56" alt="穿山甲 Pangolin 字标">
|
||||
</div></div>
|
||||
</section>
|
||||
|
||||
<!-- ══════════ 色彩 ══════════ -->
|
||||
<section class="section" id="c-clay">
|
||||
<h2>clay · 黏土铜色阶</h2>
|
||||
@@ -168,6 +272,62 @@
|
||||
<div class="pcard"><div class="pcard-h"><h3>状态语义</h3><span class="tk">success / warning / danger (+ -subtle)</span></div><div class="pcard-b"><div class="sw-grid" data-swatches="--success,--success-subtle,--warning,--warning-subtle,--danger,--danger-subtle"></div></div></div>
|
||||
</section>
|
||||
|
||||
<section class="section" id="c-dark">
|
||||
<h2>暗色主题表面</h2>
|
||||
<p class="desc">暗色画布=暖 espresso <code>--sand-950</code>(非纯黑)。表面层级 <code>--bg</code>(应用画布)→ <code>--surface</code>(卡片/面板)→ <code>--surface-2</code>(嵌套层),逐级提亮保持暖调。此卡固定演示暗色,不随右上主题切换。</p>
|
||||
<div class="pcard"><div class="pcard-h"><h3>表面层级(固定暗色演示)</h3><span class="tk">--bg · --surface · --surface-2</span></div>
|
||||
<div class="pcard-b" data-theme="dark" style="background:var(--sand-950); gap:var(--space-3)">
|
||||
<div class="card" style="flex:1; min-width:150px; background:var(--bg)"><div class="t-h3" style="color:var(--fg1); font-size:15px">bg</div><div class="t-caption" style="color:var(--fg2); margin-top:4px">应用画布</div><div class="t-mono" style="color:var(--fg3); font-size:11px; margin-top:2px">#14110E</div></div>
|
||||
<div class="card" style="flex:1; min-width:150px; background:var(--surface)"><div class="t-h3" style="color:var(--fg1); font-size:15px">surface</div><div class="t-caption" style="color:var(--fg2); margin-top:4px">卡片 / 面板</div><div class="t-mono" style="color:var(--fg3); font-size:11px; margin-top:2px">#221E19</div></div>
|
||||
<div class="card" style="flex:1; min-width:150px; background:var(--surface-2)"><div class="t-h3" style="color:var(--fg1); font-size:15px">surface-2</div><div class="t-caption" style="color:var(--fg2); margin-top:4px">嵌套层</div><div class="t-mono" style="color:var(--fg3); font-size:11px; margin-top:2px">#2A251F</div><span class="pill pill-accent" style="margin-top:var(--space-3)">Accent</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ══════════ 字体 specimen ══════════ -->
|
||||
<section class="section" id="t-body">
|
||||
<h2>正文字体 · Manrope</h2>
|
||||
<p class="desc">界面与正文主力(<code>--font-sans</code>,权重 400–700)。人文几何,中英混排稳定。示例中英分行、单句不并排。</p>
|
||||
<div class="pcard"><div class="pcard-h"><h3>Body · Manrope</h3><span class="tk">--font-sans · 400 / 500 / 600 / 700</span></div><div class="pcard-b col">
|
||||
<p class="t-h3" style="margin:0 0 var(--space-2)">一键连接,极速畅连</p>
|
||||
<p class="t-body-lg" style="margin:0 0 var(--space-2); color:var(--fg2)">轻盈、亲和、即开即用的网络加速。</p>
|
||||
<p class="t-body" style="margin:0 0 var(--space-2)">Body 16px — 全球 80+ 加速线路,智能选择最快节点,稳定不掉线。</p>
|
||||
<p class="t-sm" style="margin:0; color:var(--fg2)">Small 14px — 适用于次要说明、列表副标题与表单提示文字。</p>
|
||||
</div></div>
|
||||
</section>
|
||||
|
||||
<section class="section" id="t-cjk">
|
||||
<h2>中文字体 · Noto Sans SC</h2>
|
||||
<p class="desc">中文专用伴随字体(<code>--font-cjk</code>,权重 400 / 500 / 700)。与 Manrope / Sora 同框时保证中文字形与字重一致。</p>
|
||||
<div class="pcard"><div class="pcard-h"><h3>CJK · Noto Sans SC</h3><span class="tk">--font-cjk · 400 / 500 / 700</span></div><div class="pcard-b col" style="font-family:var(--font-cjk)">
|
||||
<p style="margin:0; font-size:34px; font-weight:700; letter-spacing:var(--tracking-snug)">穿山甲 · 守护每一次连接</p>
|
||||
<p style="margin:var(--space-3) 0 0; font-size:18px; font-weight:500; color:var(--fg2)">无需配置,打开即用。智能分流,海外应用秒开。</p>
|
||||
<p style="margin:var(--space-2) 0 0; font-size:14px; color:var(--fg3)">隐私保护 · 我们不记录你的浏览数据 · 端到端加密</p>
|
||||
</div></div>
|
||||
</section>
|
||||
|
||||
<section class="section" id="t-display">
|
||||
<h2>Display 字体 · Sora</h2>
|
||||
<p class="desc">标题、营销大字、Logo 字标(<code>--font-display</code>,权重 600 / 700)。几何友好,字距收紧 <code>-0.02em</code>。</p>
|
||||
<div class="pcard"><div class="pcard-h"><h3>Display · Sora</h3><span class="tk">--font-display · 600 / 700</span></div><div class="pcard-b col">
|
||||
<p class="t-display-xl" style="margin:0 0 var(--space-3); line-height:1.05">极速畅连 Smooth</p>
|
||||
<p class="t-display" style="margin:0; line-height:1.05; color:var(--accent)">极速 · 稳定 · 省心</p>
|
||||
</div></div>
|
||||
</section>
|
||||
|
||||
<section class="section" id="t-mono">
|
||||
<h2>等宽数据字体 · JetBrains Mono</h2>
|
||||
<p class="desc">IP / 速率 / 时长 / 密钥等数据读数(<code>--font-mono</code>,开 tabular nums 等宽对齐,数字不跳动)。</p>
|
||||
<div class="pcard"><div class="pcard-h"><h3>Mono · JetBrains Mono</h3><span class="tk">--font-mono · tnum</span></div><div class="pcard-b"><div class="spec-mono-grid">
|
||||
<div><div class="k">DOWNLOAD</div><div class="v">86.4<span class="u"> Mb/s</span></div></div>
|
||||
<div><div class="k">PING</div><div class="v">24<span class="u"> ms</span></div></div>
|
||||
<div><div class="k">UPTIME</div><div class="v">02:14:08</div></div>
|
||||
<div><div class="k">EXIT IP</div><div class="v" style="font-size:16px">103.27.18.204</div></div>
|
||||
<div><div class="k">PROTOCOL</div><div class="v" style="font-size:16px">REALITY</div></div>
|
||||
<div><div class="k">DATA</div><div class="v">1.82<span class="u"> GB</span></div></div>
|
||||
</div></div></div>
|
||||
</section>
|
||||
|
||||
<!-- ══════════ 字号 ══════════ -->
|
||||
<section class="section" id="type">
|
||||
<h2>字号梯度</h2>
|
||||
@@ -305,21 +465,331 @@
|
||||
<section class="section" id="a-lang">
|
||||
<h2>语言下拉 · .langsel</h2>
|
||||
<p class="desc">不依赖原生 <code><select></code>:触发药丸 <code>.langsel-trigger</code> + 自控绝对定位菜单 <code>.menu</code> / <code>.menu-item</code>(选中态 <code>.is-selected</code>)。点触发按钮可实际展开/收起。</p>
|
||||
<div class="pcard"><div class="pcard-b" style="min-height:180px; align-items:flex-start">
|
||||
<div class="pcard" style="overflow:visible"><div class="pcard-b" style="min-height:180px; align-items:flex-start">
|
||||
<div class="langsel" id="langselDemo">
|
||||
<button class="langsel-trigger" id="langTrigger" aria-expanded="true" aria-haspopup="true">
|
||||
简体中文 <svg class="ic"><use href="#i-chevron-down"/></svg>
|
||||
</button>
|
||||
<div class="menu" id="langMenu">
|
||||
<div class="menu" id="langMenu" style="left:0; right:auto">
|
||||
<button class="menu-item is-selected" aria-selected="true">简体中文</button>
|
||||
<button class="menu-item">English</button>
|
||||
<button class="menu-item">繁體中文</button>
|
||||
<button class="menu-item">日本語</button>
|
||||
<button class="menu-item">한국어</button>
|
||||
<button class="menu-item">Русский</button>
|
||||
<button class="menu-item">Español</button>
|
||||
</div>
|
||||
</div>
|
||||
<span class="t-caption" style="color:var(--fg3)">↖ 默认展开演示,可点击触发按钮收起 / 展开</span>
|
||||
</div></div>
|
||||
</section>
|
||||
|
||||
<!-- ══════════ 核心连接按钮 ══════════ -->
|
||||
<section class="section" id="a-connect">
|
||||
<h2>核心连接按钮 · .connect-btn</h2>
|
||||
<p class="desc">产品<strong>招牌组件</strong>:圆形一键连接键,三态 <code>.is-off</code> / <code>.is-connecting</code> / <code>.is-on</code>。背景在渐变↔纯色间切换仅过渡 <code>box-shadow</code>/<code>background-color</code>(<strong>禁</strong> <code>transition:all</code>,否则背景计算失效)。轨道环用内联 <code><svg></code>,圆内为图标 + 标签 / 计时。镜像 <code>client/lib/widgets/connect_button.dart</code>。</p>
|
||||
<div class="pcard"><div class="pcard-b" style="justify-content:center; gap:var(--space-10); padding:var(--space-8) var(--space-5)">
|
||||
|
||||
<div style="display:flex; flex-direction:column; align-items:center; gap:var(--space-3)">
|
||||
<button class="connect-btn is-off">
|
||||
<svg class="cb-ring" viewBox="0 0 140 140"><circle class="cb-track" cx="70" cy="70" r="64"/></svg>
|
||||
<svg class="cb-icon"><use href="#i-power"/></svg>
|
||||
<span class="cb-label">点击连接</span>
|
||||
<span class="cb-sub">TAP TO CONNECT</span>
|
||||
</button>
|
||||
<span class="t-caption" style="color:var(--fg3)">未连接 Off</span>
|
||||
</div>
|
||||
|
||||
<div style="display:flex; flex-direction:column; align-items:center; gap:var(--space-3)">
|
||||
<button class="connect-btn is-connecting">
|
||||
<svg class="cb-ring cb-spin" viewBox="0 0 140 140">
|
||||
<circle class="cb-arc-track" cx="70" cy="70" r="64"/>
|
||||
<circle class="cb-arc" cx="70" cy="70" r="64"/>
|
||||
</svg>
|
||||
<svg class="cb-icon cb-spin"><use href="#i-loader-circle"/></svg>
|
||||
<span class="cb-label">连接中</span>
|
||||
<span class="cb-sub">CONNECTING…</span>
|
||||
</button>
|
||||
<span class="t-caption" style="color:var(--fg3)">建立隧道 Connecting</span>
|
||||
</div>
|
||||
|
||||
<div style="display:flex; flex-direction:column; align-items:center; gap:var(--space-3)">
|
||||
<button class="connect-btn is-on">
|
||||
<svg class="cb-ring" viewBox="0 0 140 140"><circle class="cb-ring-full" cx="70" cy="70" r="64"/></svg>
|
||||
<svg class="cb-icon"><use href="#i-shield-check"/></svg>
|
||||
<span class="cb-timer">02:14:08</span>
|
||||
<span class="cb-sub">已加密 · SECURE</span>
|
||||
</button>
|
||||
<span class="t-caption" style="color:var(--fg3)">已连接 Connected</span>
|
||||
</div>
|
||||
|
||||
</div></div>
|
||||
</section>
|
||||
|
||||
<!-- ══════════ 服务器列表行 ══════════ -->
|
||||
<section class="section" id="a-server">
|
||||
<h2>服务器列表行 · .server-row</h2>
|
||||
<p class="desc">节点列表一行:<strong>国家码块</strong>(2 字母,绝不用 emoji 国旗)+ 名称/子名 + 属性胶囊(<code>.pill-outline</code>)+ 延迟(<code>--font-mono</code>·按值变色)+ 信号条 + 选中/推荐/不可用态。镜像 <code>client/lib/widgets/server_tile.dart</code>。放进 <code>.card.card-flush</code> 列表容器。</p>
|
||||
|
||||
<div class="pcard"><div class="pcard-h"><h3>列表行</h3><span class="tk">.server-row · .is-selected · .is-down</span></div><div class="pcard-b col">
|
||||
<div class="card card-flush" style="max-width:460px; width:100%">
|
||||
<div class="server-row is-selected">
|
||||
<div class="sr-cc">HK</div>
|
||||
<div class="sr-body">
|
||||
<div class="sr-title">香港 · 加速线路</div>
|
||||
<div class="sr-tags"><span class="pill pill-outline">流媒体</span></div>
|
||||
</div>
|
||||
<span class="sr-latency is-good">18ms</span>
|
||||
<span class="sr-check"><svg class="ic"><use href="#i-check"/></svg></span>
|
||||
</div>
|
||||
<div class="server-row">
|
||||
<div class="sr-cc">JP</div>
|
||||
<div class="sr-body">
|
||||
<div class="sr-title">日本 东京</div>
|
||||
<div class="sr-sub">Tokyo</div>
|
||||
</div>
|
||||
<span class="sr-latency is-good">32ms</span>
|
||||
<span class="sr-signal"><i></i><i></i><i></i></span>
|
||||
</div>
|
||||
<div class="server-row">
|
||||
<div class="sr-cc">SG</div>
|
||||
<div class="sr-body">
|
||||
<div class="sr-title">新加坡</div>
|
||||
<div class="sr-tags"><span class="pill pill-outline">P2P</span></div>
|
||||
</div>
|
||||
<span class="sr-latency is-warn">54ms</span>
|
||||
<span class="sr-signal"><i></i><i></i><i class="is-off"></i></span>
|
||||
</div>
|
||||
<div class="server-row is-down">
|
||||
<div class="sr-cc">US</div>
|
||||
<div class="sr-body">
|
||||
<div class="sr-title">美国 洛杉矶</div>
|
||||
<div class="sr-sub">Los Angeles</div>
|
||||
</div>
|
||||
<span class="sr-unavail">不可用</span>
|
||||
</div>
|
||||
</div>
|
||||
</div></div>
|
||||
|
||||
<div class="pcard"><div class="pcard-h"><h3>智能选择推荐行</h3><span class="tk">.server-row.is-smart · zap + 推荐胶囊</span></div><div class="pcard-b col">
|
||||
<div class="card card-flush" style="max-width:460px; width:100%">
|
||||
<div class="server-row is-smart">
|
||||
<div class="sr-smart-ic"><svg class="ic"><use href="#i-zap"/></svg></div>
|
||||
<div class="sr-body">
|
||||
<div class="sr-title">智能选择</div>
|
||||
<div class="sr-sub">根据当前网络环境,自动选择最优节点</div>
|
||||
</div>
|
||||
<span class="pill pill-outline">推荐</span>
|
||||
<span class="sr-check"><svg class="ic"><use href="#i-check"/></svg></span>
|
||||
</div>
|
||||
</div>
|
||||
</div></div>
|
||||
</section>
|
||||
|
||||
<!-- ══════════ 状态徽章 ══════════ -->
|
||||
<section class="section" id="a-badge">
|
||||
<h2>状态徽章 · .pill 场景</h2>
|
||||
<p class="desc">徽章复用胶囊原子 <code>.pill</code>(见「胶囊 .pill / .badge」),此处按业务场景分组:<strong>连接态</strong>(色点 + 文字,非 emoji · 铁律 #7)· <strong>套餐</strong> · <strong>节点属性</strong>。镜像 <code>client/lib/widgets/status_pill.dart</code>。</p>
|
||||
|
||||
<div class="pcard"><div class="pcard-h"><h3>连接态</h3><span class="tk">.pill.is-success/-warning/-danger/-neutral</span></div><div class="pcard-b">
|
||||
<span class="pill is-success"><span class="dot"></span>已连接</span>
|
||||
<span class="pill is-warning"><span class="dot"></span>连接中</span>
|
||||
<span class="pill is-danger"><span class="dot"></span>已断开</span>
|
||||
<span class="pill is-neutral"><span class="dot"></span>未登录</span>
|
||||
</div></div>
|
||||
|
||||
<div class="pcard"><div class="pcard-h"><h3>套餐</h3><span class="tk">.pill-accent · .pill.is-neutral</span></div><div class="pcard-b">
|
||||
<span class="pill pill-accent">PRO 会员</span>
|
||||
<span class="pill is-neutral">免费版</span>
|
||||
</div></div>
|
||||
|
||||
<div class="pcard"><div class="pcard-h"><h3>节点属性</h3><span class="tk">.pill-outline · .pill.is-neutral(数据)</span></div><div class="pcard-b">
|
||||
<span class="pill pill-outline">流媒体优化</span>
|
||||
<span class="pill pill-outline">P2P</span>
|
||||
<span class="pill is-neutral">延迟 24ms</span>
|
||||
</div></div>
|
||||
</section>
|
||||
|
||||
<!-- ══════════ 组件 · 线上对齐 ══════════ -->
|
||||
<section class="section" id="a-formctl">
|
||||
<h2>复合输入 · 开关 · 图标框 · Toast</h2>
|
||||
<p class="desc">对齐线上 <code>pangolin_field.dart</code> / <code>dtoggle</code> / <code>pangolin_toast.dart</code>。<code>.field-box</code> 复合输入(前置图标 + 裸 input + 后置槽),<code>.toggle</code> 开关,<code>.segswitch</code> 二选一段控开关(支付渠道/币种),<code>.icon-box</code> 图标框,<code>.status-dot</code> 状态点,<code>.toast</code> 轻提示。</p>
|
||||
<div class="pcard"><div class="pcard-b" style="align-items:flex-start; gap:var(--space-4)">
|
||||
<label class="field-box" style="width:240px"><svg class="fb-lead"><use href="#i-search"/></svg><input class="fb-input" placeholder="搜索国家或城市"></label>
|
||||
<label class="field-box" style="width:240px"><svg class="fb-lead"><use href="#i-lock"/></svg><input class="fb-input" type="password" value="123456"><button class="fb-trailing" style="border:none;background:none;cursor:pointer"><svg style="width:16px;height:16px"><use href="#i-eye"/></svg></button></label>
|
||||
<label class="field-box is-plain" style="width:240px"><svg class="fb-lead"><use href="#i-search"/></svg><input class="fb-input" placeholder="无边框搜索变体"></label>
|
||||
</div></div>
|
||||
<div class="pcard"><div class="pcard-b">
|
||||
<span class="toggle is-on"><i></i></span>
|
||||
<span class="toggle"><i></i></span>
|
||||
<span class="icon-box"><svg><use href="#i-smartphone"/></svg></span>
|
||||
<span class="icon-box is-danger"><svg><use href="#i-trash-2"/></svg></span>
|
||||
<span style="display:inline-flex;align-items:center;gap:8px"><span class="status-dot is-online"></span>在线</span>
|
||||
<span style="display:inline-flex;align-items:center;gap:8px"><span class="status-dot is-offline"></span>离线</span>
|
||||
<span class="toast"><svg><use href="#i-check-circle"/></svg>已复制</span>
|
||||
</div></div>
|
||||
<div class="pcard"><div class="pcard-b" style="flex-direction:column; align-items:stretch; gap:var(--space-3); max-width:320px">
|
||||
<div class="segswitch">
|
||||
<button class="segswitch-opt is-active"><svg><use href="#i-globe"/></svg>USDT</button>
|
||||
<button class="segswitch-opt"><svg><use href="#i-credit-card"/></svg>人民币</button>
|
||||
</div>
|
||||
<div class="segswitch">
|
||||
<button class="segswitch-opt">USDT · 加密货币</button>
|
||||
<button class="segswitch-opt is-active">人民币 · 支付宝</button>
|
||||
</div>
|
||||
</div></div>
|
||||
</section>
|
||||
|
||||
<section class="section" id="a-quota">
|
||||
<h2>额度卡 · 指标卡</h2>
|
||||
<p class="desc">对齐线上 <code>quota_card.dart</code>(免费版今日额度:倒计时收缩条 + 「免费版」短标签 + 三档阈值;<strong>无绿色「已解锁」态</strong>,线上以倒计时收缩条表达)与统计 <code>.metric</code> 指标瓦片。</p>
|
||||
<div class="pcard"><div class="pcard-b" style="align-items:flex-start; gap:var(--space-4)">
|
||||
<div class="quota-card" style="width:280px">
|
||||
<div class="quota-head"><svg><use href="#i-clock"/></svg><span class="quota-label">今日剩余</span><span class="quota-val">10 分钟</span><span class="quota-tag">免费版</span></div>
|
||||
<div class="quota-bar"><i style="width:100%"></i></div>
|
||||
</div>
|
||||
<div class="quota-card" style="width:280px">
|
||||
<div class="quota-head"><svg><use href="#i-clock"/></svg><span class="quota-label">剩余</span><span class="quota-val">02:18</span><span class="quota-tag">免费版</span></div>
|
||||
<div class="quota-bar is-low"><i style="width:23%"></i></div>
|
||||
</div>
|
||||
<div class="quota-card is-exhausted" style="width:280px">
|
||||
<div class="quota-head"><svg><use href="#i-clock"/></svg><span class="quota-label">今日已用完</span><span class="quota-tag">免费版</span></div>
|
||||
<div class="quota-bar is-exhausted"><i style="width:100%"></i></div>
|
||||
</div>
|
||||
</div></div>
|
||||
<div class="pcard"><div class="pcard-b" style="gap:var(--space-3)">
|
||||
<div class="metric" style="width:150px"><span class="m-ic"><svg><use href="#i-arrow-down"/></svg></span><div class="m-val">12.4G</div><div class="m-label">本周流量</div></div>
|
||||
<div class="metric" style="width:150px"><span class="m-ic"><svg><use href="#i-clock"/></svg></span><div class="m-val">6.2h</div><div class="m-label">在线时长</div></div>
|
||||
</div></div>
|
||||
</section>
|
||||
|
||||
<section class="section" id="a-rows">
|
||||
<h2>设备行 · 设置行</h2>
|
||||
<p class="desc">对齐线上 <code>DevicesScreen</code> / <code>settings_page.dart</code>。<code>.device-row</code>(图标框 + 名 + 最后在线 + 本机/移除/⋯)与 <code>.setting-row</code>(图标框 + 标题 + 副标 + trailing:chevron / 开关 / 值)。</p>
|
||||
<div class="pcard"><div class="pcard-h"><h3>设备管理</h3><span class="tk">.device-row</span></div><div class="pcard-b col">
|
||||
<div class="card card-flush" style="max-width:460px">
|
||||
<div class="device-row"><span class="icon-box"><svg><use href="#i-smartphone"/></svg></span><div class="dv-body"><div class="dv-name">iPhone 15</div><div class="dv-sub"><span class="status-dot is-online"></span>当前设备 · 刚刚在线</div></div><span class="dv-trail"><span class="pill is-success"><span class="dot"></span>本机</span></span></div>
|
||||
<div class="device-row"><span class="icon-box"><svg><use href="#i-laptop"/></svg></span><div class="dv-body"><div class="dv-name">MacBook Pro</div><div class="dv-sub"><span class="status-dot is-offline"></span>最后在线 2 小时前</div></div><span class="dv-trail"><button class="btn btn-icon btn-subtle" aria-label="更多"><svg class="ic" style="width:16px;height:16px"><use href="#i-more-vertical"/></svg></button></span></div>
|
||||
</div>
|
||||
</div></div>
|
||||
<div class="pcard"><div class="pcard-h"><h3>设置</h3><span class="tk">.setting-row</span></div><div class="pcard-b col">
|
||||
<div class="card card-flush" style="max-width:460px">
|
||||
<div class="setting-row"><span class="icon-box"><svg><use href="#i-zap"/></svg></span><div class="st-body"><div class="st-title">智能分流</div><div class="st-sub">海外应用走加速,国内直连</div></div><span class="st-trail"><span class="toggle is-on"><i></i></span></span></div>
|
||||
<div class="setting-row"><span class="icon-box"><svg><use href="#i-globe"/></svg></span><div class="st-body"><div class="st-title">语言</div></div><span class="st-trail"><span class="st-val">简体中文</span></span></div>
|
||||
<div class="setting-row is-tap"><span class="icon-box"><svg><use href="#i-shield"/></svg></span><div class="st-body"><div class="st-title">协议</div></div><span class="st-trail"><span class="st-val">REALITY</span><svg><use href="#i-chevron-right"/></svg></span></div>
|
||||
</div>
|
||||
</div></div>
|
||||
</section>
|
||||
|
||||
<section class="section" id="a-planbn">
|
||||
<h2>套餐横幅 · 侧栏会员卡</h2>
|
||||
<p class="desc">对齐线上 <code>_PlanBanner</code>(账户页)与 <code>plan_badge_card.dart</code>(桌面侧栏底部)。免费=surface 卡;<code>.is-pro</code>=clay 渐变。</p>
|
||||
<div class="pcard"><div class="pcard-b" style="align-items:stretch; gap:var(--space-4)">
|
||||
<div class="plan-banner" style="width:300px">
|
||||
<div class="pbn-row"><span class="pbn-badge"><svg><use href="#i-gift"/></svg></span><div style="flex:1"><div class="pbn-name">免费版</div><div class="pbn-meta">注册享 7 天免费试用</div></div><button class="btn btn-primary"><svg><use href="#i-crown"/></svg>升级</button></div>
|
||||
</div>
|
||||
<div class="plan-banner is-pro" style="width:300px">
|
||||
<div class="pbn-row"><span class="pbn-badge"><svg><use href="#i-crown"/></svg></span><div style="flex:1"><div class="pbn-name">PRO 会员</div><div class="pbn-meta">有效期至 2027-07-11</div></div><button class="btn btn-primary">续费</button></div>
|
||||
</div>
|
||||
</div></div>
|
||||
<div class="pcard"><div class="pcard-h"><h3>侧栏会员卡</h3><span class="tk">.plan-badge</span></div><div class="pcard-b">
|
||||
<div class="plan-badge is-active" style="width:200px"><span class="plb-crown"><svg><use href="#i-crown"/></svg></span><div class="plb-body"><span class="plb-name">PRO 会员</span><span class="plb-sub">至 2027-07-11</span></div><svg class="plb-chev"><use href="#i-chevron-right"/></svg></div>
|
||||
</div></div>
|
||||
</section>
|
||||
|
||||
<section class="section" id="a-appchrome">
|
||||
<h2>更新条 · 底部 Tab · 侧栏项</h2>
|
||||
<p class="desc">app-chrome 组件。<code>.update-banner</code>(jiu 式更新顶部横条,对齐 <code>UpdateBanner</code>)、<code>.tabbar</code>/<code>.tab</code>(移动底部 4-Tab,对齐 <code>bottom_tab_bar.dart</code>)、<code>.nav-item</code>(桌面/iPad 侧栏项,对齐 <code>nav_sidebar.dart</code>)。</p>
|
||||
<div class="pcard"><div class="pcard-b col" style="padding:0">
|
||||
<div class="update-banner"><svg class="ub-ic"><use href="#i-zap"/></svg><span class="ub-title">发现新版本 v1.1.0,建议更新</span><button class="ub-cta">立即更新</button><button class="ub-close"><svg><use href="#i-x"/></svg></button></div>
|
||||
</div></div>
|
||||
<div class="pcard"><div class="pcard-b col" style="padding:0">
|
||||
<div class="tabbar">
|
||||
<button class="tab is-active"><svg><use href="#i-power"/></svg>连接</button>
|
||||
<button class="tab"><svg><use href="#i-globe"/></svg>节点</button>
|
||||
<button class="tab"><svg><use href="#i-chart-no-axes-column"/></svg>统计</button>
|
||||
<button class="tab"><svg><use href="#i-user"/></svg>我的</button>
|
||||
</div>
|
||||
</div></div>
|
||||
<div class="pcard"><div class="pcard-b col">
|
||||
<div style="max-width:220px; display:flex; flex-direction:column; gap:var(--space-1)">
|
||||
<div class="nav-item is-active"><svg><use href="#i-power"/></svg>连接</div>
|
||||
<div class="nav-item"><svg><use href="#i-globe"/></svg>节点</div>
|
||||
<div class="nav-item"><svg><use href="#i-chart-no-axes-column"/></svg>统计</div>
|
||||
<div class="nav-item"><svg><use href="#i-settings"/></svg>设置</div>
|
||||
<div class="nav-item"><svg><use href="#i-message-circle"/></svg>联系</div>
|
||||
</div>
|
||||
</div></div>
|
||||
</section>
|
||||
|
||||
<!-- ══════════ 购买 Purchase ══════════ -->
|
||||
<section class="section" id="a-plan">
|
||||
<h2>套餐卡 · .plan-card</h2>
|
||||
<p class="desc">购买套餐档位卡。常规档 surface 底;<code>.is-featured</code> 走 clay 渐变高亮(年付/最优惠),内含反色 CTA,顶部可挂 <code>.plan-pop</code>「最受欢迎」浮标。镜像 <code>client/lib/widgets/plan_card.dart</code>。金额仅展示,扣款以 pay 服务为准。</p>
|
||||
<div class="pcard"><div class="pcard-b" style="align-items:stretch; gap:var(--space-4)">
|
||||
<div class="plan-card" style="width:230px">
|
||||
<div class="plan-name">按月</div>
|
||||
<div class="plan-price-row"><span class="plan-price">¥29.99</span><span class="plan-per">/ 月</span></div>
|
||||
<div class="plan-feats">
|
||||
<div class="plan-feat"><svg><use href="#i-check"/></svg>不限时长 · 不限流量</div>
|
||||
<div class="plan-feat"><svg><use href="#i-check"/></svg>80+ 全球加速线路</div>
|
||||
<div class="plan-feat"><svg><use href="#i-check"/></svg>5 台设备同时在线</div>
|
||||
</div>
|
||||
<button class="btn btn-primary btn-block">立即购买</button>
|
||||
</div>
|
||||
<div class="plan-card is-featured" style="width:230px">
|
||||
<span class="plan-pop">最受欢迎</span>
|
||||
<div class="plan-name">按年</div>
|
||||
<div class="plan-price-row"><span class="plan-price">¥199.99</span><span class="plan-per">/ 年 · ¥16.7/月</span></div>
|
||||
<div class="plan-feats">
|
||||
<div class="plan-feat"><svg><use href="#i-check"/></svg>不限时长 · 不限流量</div>
|
||||
<div class="plan-feat"><svg><use href="#i-check"/></svg>80+ 全球加速线路</div>
|
||||
<div class="plan-feat"><svg><use href="#i-check"/></svg>5 台设备同时在线</div>
|
||||
</div>
|
||||
<button class="btn btn-primary btn-block">立即购买</button>
|
||||
</div>
|
||||
</div></div>
|
||||
</section>
|
||||
|
||||
<section class="section" id="a-paym">
|
||||
<h2>支付方式 · .pay-method</h2>
|
||||
<p class="desc">底部弹层选择支付渠道,一行 = leading 图标 + 标题 + trailing chevron。资金外流(拉起支付宝 / 展示加密地址),<strong>App 内无卡号录入表单</strong>(设计铁律 #10)。生产渠道:哪吒聚合(支付宝)+ 加密货币 USDT。</p>
|
||||
<div class="pcard"><div class="pcard-b col">
|
||||
<div class="card card-flush" style="max-width:360px; padding:var(--space-2)">
|
||||
<button class="pay-method"><svg class="pm-ic"><use href="#i-credit-card"/></svg><span class="pm-title">支付宝</span><svg class="pm-chev"><use href="#i-chevron-right"/></svg></button>
|
||||
<button class="pay-method"><svg class="pm-ic"><use href="#i-globe"/></svg><span class="pm-title">加密货币 · USDT (TRC20)</span><svg class="pm-chev"><use href="#i-chevron-right"/></svg></button>
|
||||
</div>
|
||||
</div></div>
|
||||
</section>
|
||||
|
||||
<section class="section" id="a-kv">
|
||||
<h2>明细行 · .kv-row</h2>
|
||||
<p class="desc">可复制键值行,用于加密支付态展示「网络 / 收款地址 / 精确金额」。值用等宽字(<code>--font-mono</code> + tabular nums),右侧复制按钮。</p>
|
||||
<div class="pcard"><div class="pcard-b col">
|
||||
<div class="card" style="max-width:420px">
|
||||
<div class="kv-row"><span class="kv-label">网络</span><span class="kv-val">USDT · TRC20</span><button class="kv-copy"><svg><use href="#i-copy"/></svg></button></div>
|
||||
<div class="kv-row"><span class="kv-label">收款地址</span><span class="kv-val">TB1e7q1333iuLMLc2iQaoaqeuDF2U7cxan</span><button class="kv-copy"><svg><use href="#i-copy"/></svg></button></div>
|
||||
<div class="kv-row"><span class="kv-label">精确金额</span><span class="kv-val">4.990001</span><button class="kv-copy"><svg><use href="#i-copy"/></svg></button></div>
|
||||
</div>
|
||||
</div></div>
|
||||
</section>
|
||||
|
||||
<section class="section" id="a-result">
|
||||
<h2>结果态 · .result</h2>
|
||||
<p class="desc">支付终态:居中大图标 + 标题 + 说明。成功用 <code>--success</code>,失败加 <code>.is-fail</code> 走 <code>--danger</code>。</p>
|
||||
<div class="pcard"><div class="pcard-b" style="gap:var(--space-6)">
|
||||
<div class="result" style="width:280px">
|
||||
<div class="result-icon"><svg><use href="#i-check-circle"/></svg></div>
|
||||
<div class="result-title">支付成功</div>
|
||||
<div class="result-sub">有效期至 2027-07-11</div>
|
||||
</div>
|
||||
<div class="result is-fail" style="width:280px">
|
||||
<div class="result-icon"><svg><use href="#i-alert-triangle"/></svg></div>
|
||||
<div class="result-title">支付失败</div>
|
||||
<div class="result-sub">订单已超时,请重试</div>
|
||||
</div>
|
||||
</div></div>
|
||||
</section>
|
||||
|
||||
<!-- ══════════ 图标 ══════════ -->
|
||||
<section class="section" id="icons">
|
||||
<h2>Lucide 图标库 <span class="pill pill-outline count-pill" id="iconCount">—</span></h2>
|
||||
@@ -390,10 +860,52 @@ function renderIcons(){
|
||||
|
||||
/* ── 导航 scrollspy ── */
|
||||
const navLinks=[...document.querySelectorAll('.dnav')];
|
||||
navLinks.forEach(a=>a.onclick=(e)=>{ e.preventDefault(); const el=document.getElementById(a.dataset.s); if(el) el.scrollIntoView({behavior:'smooth',block:'start'}); });
|
||||
navLinks.forEach(a=>a.onclick=(e)=>{ if(!a.dataset.s) return; /* 外链(如屏预览)正常跳转 */ e.preventDefault(); const el=document.getElementById(a.dataset.s); if(el) el.scrollIntoView({behavior:'smooth',block:'start'}); });
|
||||
const obs=new IntersectionObserver(es=>{ es.forEach(en=>{ if(en.isIntersecting){ const id=en.target.id; navLinks.forEach(a=>a.classList.toggle('on',a.dataset.s===id)); } }); },{rootMargin:'-70px 0px -75% 0px'});
|
||||
document.querySelectorAll('.section').forEach(s=>obs.observe(s));
|
||||
|
||||
/* ── 侧栏:收起 + 拖拽改宽(持久化 localStorage) ── */
|
||||
(function(){
|
||||
const doc = document.querySelector('.doc');
|
||||
const nav = document.querySelector('.doc-nav');
|
||||
const handle = document.getElementById('navResize');
|
||||
const toggle = document.getElementById('navToggle');
|
||||
if(!doc || !nav || !handle || !toggle) return;
|
||||
const MIN = 180, MAX = 460, DEF = 238;
|
||||
let curW = parseInt(localStorage.getItem('pangolin-ds-navw') || '', 10);
|
||||
if (!(curW >= MIN && curW <= MAX)) curW = DEF;
|
||||
let collapsed = localStorage.getItem('pangolin-ds-navcollapsed') === '1';
|
||||
function apply(){
|
||||
if (collapsed) { doc.classList.add('nav-collapsed'); nav.style.width = '0px'; }
|
||||
else { doc.classList.remove('nav-collapsed'); nav.style.width = curW + 'px'; }
|
||||
}
|
||||
apply();
|
||||
// 收起 / 展开
|
||||
toggle.onclick = () => {
|
||||
collapsed = !collapsed;
|
||||
localStorage.setItem('pangolin-ds-navcollapsed', collapsed ? '1' : '0');
|
||||
apply();
|
||||
};
|
||||
// 拖拽改宽(纯 JS 内联宽度,避免自定义 CSS 变量触发闸误报)
|
||||
let dragging = false;
|
||||
handle.addEventListener('mousedown', (e) => {
|
||||
if (collapsed) return;
|
||||
dragging = true; handle.classList.add('dragging'); document.body.classList.add('nav-dragging');
|
||||
e.preventDefault();
|
||||
});
|
||||
window.addEventListener('mousemove', (e) => {
|
||||
if (!dragging) return;
|
||||
const rect = nav.getBoundingClientRect();
|
||||
curW = Math.max(MIN, Math.min(MAX, e.clientX - rect.left));
|
||||
nav.style.width = curW + 'px';
|
||||
});
|
||||
window.addEventListener('mouseup', () => {
|
||||
if (!dragging) return;
|
||||
dragging = false; handle.classList.remove('dragging'); document.body.classList.remove('nav-dragging');
|
||||
localStorage.setItem('pangolin-ds-navw', curW);
|
||||
});
|
||||
})();
|
||||
|
||||
/* ── init(sprite 注入可能在 DOMContentLoaded,稍等确保 PANGOLIN_ICONS 就绪) ── */
|
||||
function boot(){
|
||||
applyTheme(localStorage.getItem('pangolin-ds-theme') || 'light');
|
||||
|
||||
@@ -0,0 +1,342 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Pangolin · Orders (Interactive Prototype)</title>
|
||||
<link rel="stylesheet" href="../tokens.css">
|
||||
<link rel="stylesheet" href="../atoms.css">
|
||||
<script src="../icons.js"></script>
|
||||
<style>
|
||||
body{ margin:0; background:var(--bg); color:var(--fg1); font-family:var(--font-sans);
|
||||
-webkit-font-smoothing:antialiased; transition:background-color var(--dur-base) var(--ease-out); }
|
||||
|
||||
/* 顶栏 chrome(照抄 purchase.html) */
|
||||
.top{ position:sticky; top:0; z-index:50; height:58px; background:var(--surface);
|
||||
border-bottom:1px solid var(--border); display:flex; align-items:center; gap:var(--space-4);
|
||||
padding:0 var(--space-6); box-shadow:var(--shadow-sm); }
|
||||
.brand{ display:flex; align-items:center; gap:var(--space-3); font-family:var(--font-display);
|
||||
font-weight:700; font-size:var(--text-body); }
|
||||
.brand .mk{ width:28px; height:28px; border-radius:var(--radius-md);
|
||||
background:linear-gradient(135deg,var(--clay-500),var(--clay-800));
|
||||
color:#fff; display:flex; align-items:center; justify-content:center; }
|
||||
.tag{ font-size:var(--text-caption); color:var(--fg3); padding-left:var(--space-3);
|
||||
border-left:1px solid var(--border); font-weight:500; }
|
||||
.chrome-actions{ margin-left:auto; display:inline-flex; align-items:center; gap:var(--space-3); }
|
||||
.pill-btn{ display:inline-flex; align-items:center; gap:var(--space-2);
|
||||
background:var(--bg-subtle); border:1px solid var(--border); color:var(--fg2);
|
||||
border-radius:var(--radius-full); padding:var(--space-2) var(--space-4);
|
||||
font-weight:600; font-size:var(--text-caption); cursor:pointer; }
|
||||
.pill-btn svg{ width:15px; height:15px; }
|
||||
.back{ font-size:var(--text-sm); color:var(--accent); text-decoration:none; font-weight:600; }
|
||||
|
||||
.wrap{ max-width:760px; margin:0 auto; padding:var(--space-8) var(--space-6) var(--space-16);
|
||||
display:flex; flex-direction:column; align-items:center; }
|
||||
.lead{ font-size:var(--text-sm); color:var(--fg2); line-height:var(--leading-relaxed);
|
||||
margin:0 0 var(--space-8); text-align:center; }
|
||||
.lead strong{ color:var(--fg1); }
|
||||
|
||||
/* 手机框 */
|
||||
.phone{ width:320px; height:660px; border-radius:var(--radius-2xl); overflow:hidden;
|
||||
background:var(--bg); border:1px solid var(--border); box-shadow:var(--shadow-lg);
|
||||
display:flex; flex-direction:column; position:relative; }
|
||||
.phone-top{ height:48px; flex:none; display:flex; align-items:center; gap:var(--space-2);
|
||||
padding:0 var(--space-2); border-bottom:1px solid var(--border); background:var(--surface); z-index:5; }
|
||||
.phone-top .ttl{ font-family:var(--font-sans); font-size:var(--text-h3); font-weight:700; color:var(--fg1); }
|
||||
.icon-btn{ width:38px; height:38px; display:flex; align-items:center; justify-content:center;
|
||||
border:none; background:transparent; color:var(--fg1); cursor:pointer; }
|
||||
.icon-btn svg{ width:20px; height:20px; }
|
||||
.icon-btn[hidden]{ display:none; }
|
||||
|
||||
.phone-body{ flex:1; position:relative; overflow:hidden; }
|
||||
.screen{ position:absolute; inset:0; display:none; flex-direction:column; }
|
||||
.screen.is-active{ display:flex; }
|
||||
.scr{ flex:1; overflow:auto; padding:var(--space-4); }
|
||||
.stack > *{ margin-bottom:var(--space-4); }
|
||||
.stack > *:last-child{ margin-bottom:0; }
|
||||
|
||||
/* 订单列表行内胶囊压缩 */
|
||||
.setting-row .st-trail .pill{ padding:3px var(--space-2); font-size:11px; }
|
||||
.setting-row .st-trail > svg{ width:18px; height:18px; }
|
||||
.pill-accent .dot{ background:var(--fg-on-accent); }
|
||||
|
||||
/* 订单详情 · 顶部概要 */
|
||||
.od-summary{ display:flex; flex-direction:column; align-items:flex-start; gap:var(--space-3);
|
||||
padding:var(--space-5); }
|
||||
.od-plan{ font-family:var(--font-sans); font-size:var(--text-sm); font-weight:600; color:var(--fg2); }
|
||||
.od-amount{ font-family:var(--font-display); font-size:var(--text-h1); font-weight:700; color:var(--fg1);
|
||||
letter-spacing:var(--tracking-tight); font-feature-settings:'tnum' 1; line-height:1; }
|
||||
|
||||
.sec-label{ font-size:var(--text-caption); color:var(--fg3); font-weight:700;
|
||||
letter-spacing:var(--tracking-caps); text-transform:uppercase;
|
||||
padding:0 var(--space-2) var(--space-2); }
|
||||
.kv-status{ padding-top:2px; }
|
||||
.od-actions{ display:flex; flex-direction:column; gap:var(--space-3); }
|
||||
|
||||
/* toast(点击复制) */
|
||||
.toast{ position:absolute; left:50%; bottom:var(--space-6); transform:translate(-50%, 12px);
|
||||
background:var(--fg1); color:var(--bg); font-size:var(--text-caption); font-weight:600;
|
||||
padding:var(--space-2) var(--space-4); border-radius:var(--radius-full); box-shadow:var(--shadow-lg);
|
||||
display:flex; align-items:center; gap:var(--space-2); opacity:0; pointer-events:none;
|
||||
transition:opacity var(--dur-base) var(--ease-out), transform var(--dur-base) var(--ease-out); z-index:60; }
|
||||
.toast svg{ width:14px; height:14px; }
|
||||
.toast.show{ opacity:1; transform:translate(-50%, 0); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="top">
|
||||
<div class="brand"><span class="mk"><img src="../assets/app-icon.svg" alt="Pangolin" style="width:100%;height:100%;object-fit:contain;border-radius:inherit"></span>Pangolin 穿山甲</div>
|
||||
<span class="tag" data-i18n="tag">Orders · Design preview</span>
|
||||
<div class="chrome-actions">
|
||||
<a class="back" href="../index.html" data-i18n="back">← Registry</a>
|
||||
<button class="pill-btn" id="langBtn" aria-label="language"><svg><use href="#i-globe"/></svg><span id="langLabel">中文</span></button>
|
||||
<button class="pill-btn" id="themeBtn" aria-label="theme">
|
||||
<svg><use href="#i-sun" id="themeIcon"/></svg><span id="themeLabel">Light</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="wrap">
|
||||
<p class="lead" data-i18n="lead">
|
||||
Single phone frame walking the order flow: <strong>Orders list</strong> → tap a row → <strong>Order detail</strong> → back. Four lifecycle states (awaiting payment / paid · activating / completed / expired). Copyable order & trade numbers. Toggle language (EN / 中) and theme top-right.
|
||||
</p>
|
||||
|
||||
<div class="phone">
|
||||
<div class="phone-top">
|
||||
<button class="icon-btn" id="backBtn" aria-label="back" hidden><svg><use href="#i-arrow-left"/></svg></button>
|
||||
<span class="ttl" id="phoneTitle" data-i18n="listTitle">Orders</span>
|
||||
</div>
|
||||
|
||||
<div class="phone-body">
|
||||
|
||||
<!-- 页面 1:订单列表 -->
|
||||
<section class="screen is-active" data-screen="list">
|
||||
<div class="scr">
|
||||
<div class="card card-flush" id="orderList"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 页面 2:订单详情 -->
|
||||
<section class="screen" data-screen="detail">
|
||||
<div class="scr stack">
|
||||
<div class="card">
|
||||
<div class="od-summary">
|
||||
<div class="od-plan" id="odPlan"></div>
|
||||
<div class="od-amount" id="odAmount"></div>
|
||||
<div id="odStatus"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="sec-label" data-i18n="secDetail">Details</div>
|
||||
<div class="card" id="odKv"></div>
|
||||
</div>
|
||||
|
||||
<div class="od-actions" id="odActions"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- toast -->
|
||||
<div class="toast" id="toast"><svg><use href="#i-check"/></svg><span id="toastText"></span></div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
/* ---------- 主题 ---------- */
|
||||
function applyTheme(t){
|
||||
if(t==='dark'){ document.documentElement.dataset.theme='dark'; }
|
||||
else { delete document.documentElement.dataset.theme; }
|
||||
document.getElementById('themeIcon').setAttribute('href', t==='dark' ? '#i-moon' : '#i-sun');
|
||||
document.getElementById('themeLabel').textContent = (lang==='zh') ? (t==='dark'?'深色':'浅色') : (t==='dark'?'Dark':'Light');
|
||||
localStorage.setItem('pangolin-ds-theme', t);
|
||||
}
|
||||
document.getElementById('themeBtn').onclick = () => {
|
||||
const cur = document.documentElement.dataset.theme==='dark' ? 'dark' : 'light';
|
||||
applyTheme(cur==='dark' ? 'light' : 'dark');
|
||||
};
|
||||
|
||||
/* ---------- i18n ---------- */
|
||||
const I18N = {
|
||||
en:{ tag:'Orders · Design preview', back:'← Registry', listTitle:'Orders', detailTitle:'Order Detail',
|
||||
secDetail:'Details',
|
||||
kOrderNo:'Order No.', kPlan:'Plan', kAmount:'Amount', kMethod:'Payment', kStatus:'Status',
|
||||
kCreated:'Created', kValid:'Valid until', kDeadline:'Pay before', kTrade:'Trade No.',
|
||||
copied:'Copied', langBtn:'中文',
|
||||
actContinue:'Continue payment', actCancel:'Cancel order', actRefresh:'Refresh status',
|
||||
actMembership:'View membership', actBuyAgain:'Buy again',
|
||||
lead:'Single phone frame walking the order flow: <strong>Orders list</strong> → tap a row → <strong>Order detail</strong> → back. Four lifecycle states (awaiting payment / paid · activating / completed / expired). Copyable order & trade numbers. Toggle language (EN / 中) and theme top-right.' },
|
||||
zh:{ tag:'订单 · 设计预览', back:'← 返回登记簿', listTitle:'我的订单', detailTitle:'订单详情',
|
||||
secDetail:'明细',
|
||||
kOrderNo:'订单号', kPlan:'套餐', kAmount:'金额', kMethod:'支付方式', kStatus:'状态',
|
||||
kCreated:'创建时间', kValid:'有效期至', kDeadline:'支付截止', kTrade:'交易流水号',
|
||||
copied:'已复制', langBtn:'EN',
|
||||
actContinue:'继续支付', actCancel:'取消订单', actRefresh:'刷新状态',
|
||||
actMembership:'查看会员', actBuyAgain:'重新购买',
|
||||
lead:'单手机框走完整订单流程:<strong>订单列表</strong> → 点某条 → <strong>订单详情</strong> → 返回。四种生命周期状态(等待付款 / 付款完成·待开通 / 交易完成 / 已过期),订单号 / 流水号可复制。右上角可切语言(EN / 中)与主题。' }
|
||||
};
|
||||
let lang = 'en';
|
||||
|
||||
/* ---------- 数据模型(pay v2) ---------- */
|
||||
const PLAN = {
|
||||
pro_month:{ en:'PRO Monthly', zh:'按月 PRO' },
|
||||
pro_quarter:{ en:'PRO Quarterly', zh:'按季 PRO' },
|
||||
pro_year:{ en:'PRO Yearly', zh:'按年 PRO' }
|
||||
};
|
||||
const METHOD = {
|
||||
alipay:{ en:'Alipay', zh:'支付宝' },
|
||||
crypto:{ en:'Crypto · USDT (TRC20)', zh:'加密货币 · USDT (TRC20)' },
|
||||
nazha:{ en:'Nazhapay', zh:'哪吒支付' }
|
||||
};
|
||||
const STATUS = {
|
||||
pending:{ pill:'is-warning', en:'Awaiting payment', zh:'等待付款' },
|
||||
paid:{ pill:'pill-accent', en:'Paid · Activating', zh:'付款完成·待开通' },
|
||||
settled:{ pill:'is-success', en:'Completed', zh:'交易完成' },
|
||||
expired:{ pill:'is-neutral', en:'Expired', zh:'已过期' }
|
||||
};
|
||||
|
||||
const ORDERS = [
|
||||
{ id:'PGL-20260711-8F3A', plan:'pro_year', amount:'¥199.99', method:'alipay', status:'settled',
|
||||
created:'2026-07-11 14:32', endKind:'valid', ended:'2027-07-11', trade:'2026071122001487652910' },
|
||||
{ id:'PGL-20260710-2K9D', plan:'pro_month', amount:'¥29.99', method:'crypto', status:'paid',
|
||||
created:'2026-07-10 09:18', endKind:'valid', ended:'2026-08-10', trade:'a7f3…4c1d2 (TRC20)' },
|
||||
{ id:'PGL-20260709-7QX2', plan:'pro_quarter', amount:'¥68.88', method:'alipay', status:'pending',
|
||||
created:'2026-07-09 21:05', endKind:'deadline', ended:'2026-07-09 21:20', trade:'—' },
|
||||
{ id:'PGL-20260705-3M1P', plan:'pro_month', amount:'4.99 USDT', method:'crypto', status:'expired',
|
||||
created:'2026-07-05 11:40', endKind:'deadline', ended:'2026-07-05 11:55', trade:'—' },
|
||||
{ id:'PGL-20260628-9ABF', plan:'pro_year', amount:'¥199.99', method:'nazha', status:'settled',
|
||||
created:'2026-06-28 16:22', endKind:'valid', ended:'2027-06-28', trade:'NZ2026062899310277' }
|
||||
];
|
||||
|
||||
const ACTIONS = {
|
||||
pending: [ {key:'actContinue', cls:'btn-primary'}, {key:'actCancel', cls:'btn-ghost'} ],
|
||||
paid: [ {key:'actRefresh', cls:'btn-subtle'} ],
|
||||
settled: [ {key:'actMembership', cls:'btn-primary'} ],
|
||||
expired: [ {key:'actBuyAgain', cls:'btn-primary'} ]
|
||||
};
|
||||
|
||||
/* ---------- 渲染工具 ---------- */
|
||||
function t(k){ return I18N[lang][k]; }
|
||||
function esc(s){ return String(s).replace(/[&<>"]/g, c => ({'&':'&','<':'<','>':'>','"':'"'}[c])); }
|
||||
function planLabel(p){ return PLAN[p][lang]; }
|
||||
function methodLabel(m){ return METHOD[m][lang]; }
|
||||
function statusPill(s){
|
||||
const st = STATUS[s];
|
||||
return '<span class="pill ' + st.pill + '"><span class="dot"></span>' + esc(st[lang]) + '</span>';
|
||||
}
|
||||
|
||||
/* ---------- 列表 ---------- */
|
||||
function renderList(){
|
||||
const html = ORDERS.map(o => {
|
||||
const date = o.created.split(' ')[0];
|
||||
return '<div class="setting-row is-tap" data-id="' + o.id + '">'
|
||||
+ '<div class="st-body">'
|
||||
+ '<div class="st-title">' + esc(planLabel(o.plan)) + '</div>'
|
||||
+ '<div class="st-sub">' + esc(o.amount) + ' · ' + esc(date) + '</div>'
|
||||
+ '</div>'
|
||||
+ '<div class="st-trail">' + statusPill(o.status)
|
||||
+ '<svg><use href="#i-chevron-right"/></svg>'
|
||||
+ '</div>'
|
||||
+ '</div>';
|
||||
}).join('');
|
||||
const list = document.getElementById('orderList');
|
||||
list.innerHTML = html;
|
||||
list.querySelectorAll('.setting-row').forEach(row =>
|
||||
row.onclick = () => openDetail(row.dataset.id));
|
||||
}
|
||||
|
||||
/* ---------- 详情 ---------- */
|
||||
function kvRow(labelKey, value, copyable){
|
||||
const copy = copyable
|
||||
? '<button class="kv-copy js-copy" data-copy="' + esc(value) + '"><svg><use href="#i-copy"/></svg></button>'
|
||||
: '';
|
||||
return '<div class="kv-row"><span class="kv-label">' + esc(t(labelKey)) + '</span>'
|
||||
+ '<span class="kv-val">' + value + '</span>' + copy + '</div>';
|
||||
}
|
||||
|
||||
function openDetail(id){
|
||||
const o = ORDERS.find(x => x.id === id);
|
||||
if(!o) return;
|
||||
document.getElementById('odPlan').textContent = planLabel(o.plan);
|
||||
document.getElementById('odAmount').textContent = o.amount;
|
||||
document.getElementById('odStatus').innerHTML = statusPill(o.status);
|
||||
|
||||
const endKey = o.endKind === 'valid' ? 'kValid' : 'kDeadline';
|
||||
const kv = ''
|
||||
+ kvRow('kOrderNo', esc(o.id), true)
|
||||
+ kvRow('kPlan', esc(planLabel(o.plan)), false)
|
||||
+ kvRow('kAmount', esc(o.amount), false)
|
||||
+ kvRow('kMethod', esc(methodLabel(o.method)), false)
|
||||
+ '<div class="kv-row"><span class="kv-label">' + esc(t('kStatus')) + '</span>'
|
||||
+ '<span class="kv-val kv-status">' + statusPill(o.status) + '</span></div>'
|
||||
+ kvRow('kCreated', esc(o.created), false)
|
||||
+ kvRow(endKey, esc(o.ended), false)
|
||||
+ kvRow('kTrade', esc(o.trade), o.trade !== '—');
|
||||
document.getElementById('odKv').innerHTML = kv;
|
||||
|
||||
const acts = ACTIONS[o.status] || [];
|
||||
document.getElementById('odActions').innerHTML = acts.map(a =>
|
||||
'<button class="btn ' + a.cls + ' btn-block js-act" data-key="' + a.key + '">' + esc(t(a.key)) + '</button>'
|
||||
).join('');
|
||||
document.querySelectorAll('.js-act').forEach(b => b.onclick = () => {
|
||||
if(b.dataset.key === 'actCancel') { show('list'); return; }
|
||||
toast(t(b.dataset.key));
|
||||
});
|
||||
document.querySelectorAll('.js-copy').forEach(b => b.onclick = () => toast(t('copied') + ':' + b.dataset.copy));
|
||||
|
||||
show('detail');
|
||||
}
|
||||
|
||||
/* ---------- 屏切换 ---------- */
|
||||
const screens = document.querySelectorAll('.screen');
|
||||
const titleEl = document.getElementById('phoneTitle');
|
||||
const backBtn = document.getElementById('backBtn');
|
||||
function show(name){
|
||||
screens.forEach(s => s.classList.toggle('is-active', s.dataset.screen === name));
|
||||
titleEl.textContent = name === 'detail' ? t('detailTitle') : t('listTitle');
|
||||
backBtn.hidden = (name === 'list');
|
||||
document.querySelectorAll('.scr').forEach(el => el.scrollTop = 0);
|
||||
window.__screen = name;
|
||||
}
|
||||
backBtn.onclick = () => show('list');
|
||||
|
||||
/* ---------- toast ---------- */
|
||||
let toastTimer;
|
||||
function toast(msg){
|
||||
const el = document.getElementById('toast');
|
||||
document.getElementById('toastText').textContent = msg;
|
||||
el.classList.add('show');
|
||||
clearTimeout(toastTimer);
|
||||
toastTimer = setTimeout(() => el.classList.remove('show'), 1800);
|
||||
}
|
||||
|
||||
/* ---------- 语言 ---------- */
|
||||
function applyLang(l){
|
||||
lang = l;
|
||||
document.documentElement.lang = (l === 'zh') ? 'zh-CN' : 'en';
|
||||
document.querySelectorAll('[data-i18n]').forEach(el => {
|
||||
const k = el.dataset.i18n;
|
||||
if(k === 'lead') el.innerHTML = t('lead'); else el.textContent = t(k);
|
||||
});
|
||||
document.getElementById('langLabel').textContent = t('langBtn');
|
||||
const curTheme = document.documentElement.dataset.theme === 'dark' ? 'dark' : 'light';
|
||||
document.getElementById('themeLabel').textContent = (l==='zh') ? (curTheme==='dark'?'深色':'浅色') : (curTheme==='dark'?'Dark':'Light');
|
||||
renderList();
|
||||
if(window.__screen === 'detail' && window.__curId) openDetail(window.__curId);
|
||||
else show(window.__screen || 'list');
|
||||
localStorage.setItem('pangolin-ds-lang', l);
|
||||
}
|
||||
document.getElementById('langBtn').onclick = () => applyLang(lang === 'en' ? 'zh' : 'en');
|
||||
|
||||
/* 记住详情当前单以便切语言重渲染 */
|
||||
const _openDetail = openDetail;
|
||||
openDetail = function(id){ window.__curId = id; _openDetail(id); };
|
||||
|
||||
/* ---------- 初始化 ---------- */
|
||||
applyTheme(localStorage.getItem('pangolin-ds-theme') || 'light');
|
||||
applyLang(localStorage.getItem('pangolin-ds-lang') || 'en');
|
||||
show('list');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,403 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>穿山甲 · 购买流程(交互原型)</title>
|
||||
<link rel="stylesheet" href="../tokens.css">
|
||||
<link rel="stylesheet" href="../atoms.css">
|
||||
<script src="../icons.js"></script>
|
||||
<style>
|
||||
body{ margin:0; background:var(--bg); color:var(--fg1); font-family:var(--font-sans);
|
||||
-webkit-font-smoothing:antialiased; transition:background-color var(--dur-base) var(--ease-out); }
|
||||
|
||||
/* 顶栏(照抄 purchase.html chrome) */
|
||||
.top{ position:sticky; top:0; z-index:50; height:58px; background:var(--surface);
|
||||
border-bottom:1px solid var(--border); display:flex; align-items:center; gap:var(--space-4);
|
||||
padding:0 var(--space-6); box-shadow:var(--shadow-sm); }
|
||||
.brand{ display:flex; align-items:center; gap:var(--space-3); font-family:var(--font-display);
|
||||
font-weight:700; font-size:var(--text-body); }
|
||||
.brand .mk{ width:28px; height:28px; border-radius:var(--radius-md);
|
||||
background:linear-gradient(135deg,var(--clay-500),var(--clay-800));
|
||||
color:#fff; display:flex; align-items:center; justify-content:center; }
|
||||
.brand .mk svg{ width:16px; height:16px; }
|
||||
.tag{ font-size:var(--text-caption); color:var(--fg3); padding-left:var(--space-3);
|
||||
border-left:1px solid var(--border); font-weight:500; }
|
||||
.theme-toggle{ margin-left:auto; display:inline-flex; align-items:center; gap:var(--space-2);
|
||||
background:var(--bg-subtle); border:1px solid var(--border); color:var(--fg2);
|
||||
border-radius:var(--radius-full); padding:var(--space-2) var(--space-4);
|
||||
font-weight:600; font-size:var(--text-caption); cursor:pointer; }
|
||||
.theme-toggle svg{ width:15px; height:15px; }
|
||||
.back{ font-size:var(--text-sm); color:var(--accent); text-decoration:none; font-weight:600; }
|
||||
|
||||
.wrap{ max-width:760px; margin:0 auto; padding:var(--space-8) var(--space-6) var(--space-16);
|
||||
display:flex; flex-direction:column; align-items:center; }
|
||||
.lead{ font-size:var(--text-sm); color:var(--fg2); line-height:var(--leading-relaxed);
|
||||
margin:0 0 var(--space-8); text-align:center; }
|
||||
.lead code{ font-family:var(--font-mono); font-size:0.85em; color:var(--accent);
|
||||
background:var(--accent-subtle); padding:1px 6px; border-radius:var(--radius-sm); }
|
||||
|
||||
/* 手机框 */
|
||||
.phone{ width:320px; height:660px; border-radius:var(--radius-2xl); overflow:hidden;
|
||||
background:var(--bg); border:1px solid var(--border); box-shadow:var(--shadow-lg);
|
||||
display:flex; flex-direction:column; position:relative; }
|
||||
.phone-top{ height:48px; flex:none; display:flex; align-items:center; gap:var(--space-2);
|
||||
padding:0 var(--space-2); border-bottom:1px solid var(--border); background:var(--surface); z-index:5; }
|
||||
.phone-top .ttl{ font-family:var(--font-sans); font-size:var(--text-h3); font-weight:700; color:var(--fg1); }
|
||||
.icon-btn{ width:38px; height:38px; display:flex; align-items:center; justify-content:center;
|
||||
border:none; background:transparent; color:var(--fg1); cursor:pointer; }
|
||||
.icon-btn svg{ width:20px; height:20px; }
|
||||
.icon-btn[hidden]{ display:none; }
|
||||
|
||||
.phone-body{ flex:1; position:relative; overflow:hidden; }
|
||||
.screen{ position:absolute; inset:0; display:none; flex-direction:column; }
|
||||
.screen.is-active{ display:flex; }
|
||||
.scr{ flex:1; overflow:auto; padding:var(--space-5); }
|
||||
.scr.center{ display:flex; flex-direction:column; justify-content:center; }
|
||||
.stack > *{ margin-bottom:var(--space-4); }
|
||||
.stack > *:last-child{ margin-bottom:0; }
|
||||
|
||||
/* ---------- 购买页:统一特性块(只出现一次) ---------- */
|
||||
.feats-note{ font-size:var(--text-caption); font-weight:700; color:var(--fg3);
|
||||
letter-spacing:var(--tracking-caps); text-transform:uppercase;
|
||||
margin:0 0 var(--space-3); padding-left:var(--space-1); }
|
||||
.feat-block{ padding:var(--space-4) var(--space-5); }
|
||||
.feat-list{ display:flex; flex-direction:column; gap:var(--space-3); }
|
||||
.feat-item{ display:flex; align-items:center; gap:var(--space-3);
|
||||
font-size:var(--text-sm); font-weight:500; color:var(--fg1); }
|
||||
.feat-item svg{ width:16px; height:16px; flex-shrink:0; color:var(--success); }
|
||||
|
||||
/* ---------- 购买页:精简套餐卡(去逐卡特性,只留周期+价格+按钮) ---------- */
|
||||
.plan-card{ padding:var(--space-4) var(--space-5); } /* override atoms:更矮更紧凑 */
|
||||
.plan-head{ display:flex; align-items:center; justify-content:space-between; gap:var(--space-4); }
|
||||
.plan-meta{ display:flex; flex-direction:column; gap:var(--space-1); }
|
||||
.plan-meta .plan-per{ margin:0; }
|
||||
.plan-card .btn-primary{ margin-top:var(--space-4); }
|
||||
|
||||
/* ---------- 支付页:订单信息卡 ---------- */
|
||||
.order-card{ padding:var(--space-4) var(--space-5); }
|
||||
.order-hd{ font-size:var(--text-caption); font-weight:700; color:var(--fg3);
|
||||
letter-spacing:var(--tracking-caps); text-transform:uppercase; margin-bottom:var(--space-3); }
|
||||
.order-row{ display:flex; align-items:baseline; justify-content:space-between; gap:var(--space-4);
|
||||
padding:var(--space-2) 0; border-top:1px solid var(--border); }
|
||||
.order-row:first-of-type{ border-top:none; padding-top:0; }
|
||||
.order-k{ font-size:var(--text-sm); color:var(--fg3); }
|
||||
.order-v{ font-family:var(--font-sans); font-size:var(--text-sm); font-weight:600; color:var(--fg1); }
|
||||
.order-v.order-amt{ font-family:var(--font-mono); font-weight:700; color:var(--accent);
|
||||
font-feature-settings:'tnum' 1; }
|
||||
|
||||
/* 按支付渠道切换 pay 屏内容(渠道即币种即方式:usdt→crypto / cny→alipay) */
|
||||
.screen[data-method="crypto"] .only-alipay{ display:none; }
|
||||
.screen[data-method="alipay"] .only-crypto{ display:none; }
|
||||
|
||||
/* 轮询中 */
|
||||
.polling{ display:flex; align-items:center; gap:var(--space-3); margin:var(--space-4) 0 var(--space-2);
|
||||
font-size:var(--text-sm); color:var(--fg2); }
|
||||
.polling .spin{ width:15px; height:15px; color:var(--accent); animation:spin 1s linear infinite; }
|
||||
@keyframes spin{ to{ transform:rotate(360deg); } }
|
||||
.hint-warn{ font-size:var(--text-caption); color:var(--warning); margin-top:var(--space-2); line-height:var(--leading-normal); }
|
||||
.hint-mut{ font-size:var(--text-caption); color:var(--fg3); margin-top:var(--space-2); line-height:var(--leading-normal); text-align:center; }
|
||||
.full{ width:100%; }
|
||||
|
||||
/* 评审演示区(真实由 pay 服务回调驱动,此处仅供点击走通流程) */
|
||||
.demo-zone{ margin-top:var(--space-5); padding-top:var(--space-4);
|
||||
border-top:1px dashed var(--border-strong); }
|
||||
.demo-cap{ font-size:var(--text-caption); color:var(--fg3); text-align:center;
|
||||
margin-bottom:var(--space-3); letter-spacing:var(--tracking-caps); text-transform:uppercase; font-weight:700; }
|
||||
.demo-fail{ background:none; border:none; color:var(--fg3); font-size:var(--text-caption);
|
||||
cursor:pointer; width:100%; padding:var(--space-2); font-weight:600; }
|
||||
|
||||
/* toast(点击复制 / 拉起反馈) */
|
||||
.toast{ position:absolute; left:50%; bottom:var(--space-6); transform:translate(-50%, 12px);
|
||||
background:var(--fg1); color:var(--bg); font-size:var(--text-caption); font-weight:600;
|
||||
padding:var(--space-2) var(--space-4); border-radius:var(--radius-full); box-shadow:var(--shadow-lg);
|
||||
display:flex; align-items:center; gap:var(--space-2); opacity:0; pointer-events:none;
|
||||
transition:opacity var(--dur-base) var(--ease-out), transform var(--dur-base) var(--ease-out); z-index:60; }
|
||||
.toast svg{ width:14px; height:14px; }
|
||||
.toast.show{ opacity:1; transform:translate(-50%, 0); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="top">
|
||||
<div class="brand"><span class="mk"><img src="../assets/app-icon.svg" alt="穿山甲" style="width:100%;height:100%;object-fit:contain;border-radius:inherit"></span>穿山甲 Pangolin</div>
|
||||
<span class="tag">购买流程 · 交互原型</span>
|
||||
<a class="back" href="../index.html">← 返回登记簿</a>
|
||||
<button class="theme-toggle" id="themeBtn" aria-label="切换主题">
|
||||
<svg><use href="#i-sun" id="themeIcon"/></svg><span id="themeLabel">浅色</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="wrap">
|
||||
<p class="lead">
|
||||
单手机框走完整购买流程:<strong>购买</strong>(特性统一置顶 + <strong>支付渠道 switch</strong> + 精简套餐卡)→ <strong>创建订单</strong>(顶部订单信息按所选渠道币种显价)→ <strong>成功 / 失败</strong>。
|
||||
支付渠道即币种即方式:<strong>USDT → 加密货币</strong>(默认)/ <strong>人民币 → 支付宝</strong>;套餐价随渠道切换(各币种一口价,不做汇率换算)。结算币种一经下单锁定,跨币种需重新下单(pay 侧 <code>currency_mismatch</code>)。资金外流全在 App 外,<strong>App 内无卡号录入</strong>(铁律 #10)。「▸ 模拟到账 / 失败」为评审用。切右上角看深色。
|
||||
</p>
|
||||
|
||||
<div class="phone">
|
||||
<div class="phone-top">
|
||||
<button class="icon-btn" id="backBtn" aria-label="返回"><svg><use href="#i-arrow-left"/></svg></button>
|
||||
<span class="ttl" id="phoneTitle">升级 PRO</span>
|
||||
</div>
|
||||
|
||||
<div class="phone-body">
|
||||
|
||||
<!-- 购买页 -->
|
||||
<section class="screen is-active" data-screen="purchase">
|
||||
<div class="scr stack">
|
||||
|
||||
<!-- 统一特性块:只出现一次,所有套餐均包含 -->
|
||||
<div>
|
||||
<p class="feats-note">所有套餐均包含</p>
|
||||
<div class="card feat-block">
|
||||
<div class="feat-list">
|
||||
<div class="feat-item"><svg><use href="#i-check"/></svg>不限时长 · 不限流量</div>
|
||||
<div class="feat-item"><svg><use href="#i-check"/></svg>80+ 全球加速线路</div>
|
||||
<div class="feat-item"><svg><use href="#i-check"/></svg>5 台设备同时在线</div>
|
||||
<div class="feat-item"><svg><use href="#i-check"/></svg>优先客服</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 支付渠道 switch(默认 USDT·加密货币;人民币走支付宝)→ 套餐价按此币种显示 -->
|
||||
<div>
|
||||
<p class="feats-note">支付渠道</p>
|
||||
<div class="segswitch" id="chanSwitch">
|
||||
<button class="segswitch-opt is-active js-chan" data-chan="usdt"><svg><use href="#i-globe"/></svg>USDT · 加密货币</button>
|
||||
<button class="segswitch-opt js-chan" data-chan="cny"><svg><use href="#i-credit-card"/></svg>人民币 · 支付宝</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 精简套餐卡:仅 周期 + 大价格 + 每月折算 + 立即购买;价格随渠道币种切换 -->
|
||||
<div class="plan-card" data-plan="按月">
|
||||
<div class="plan-head">
|
||||
<div class="plan-meta">
|
||||
<div class="plan-name">按月</div>
|
||||
<div class="plan-per"></div>
|
||||
</div>
|
||||
<div class="plan-price"></div>
|
||||
</div>
|
||||
<button class="btn btn-primary btn-block js-buy" data-name="按月" data-until="2026-08-11">立即购买</button>
|
||||
</div>
|
||||
|
||||
<div class="plan-card" data-plan="按季">
|
||||
<div class="plan-head">
|
||||
<div class="plan-meta">
|
||||
<div class="plan-name">按季</div>
|
||||
<div class="plan-per"></div>
|
||||
</div>
|
||||
<div class="plan-price"></div>
|
||||
</div>
|
||||
<button class="btn btn-primary btn-block js-buy" data-name="按季" data-until="2026-10-11">立即购买</button>
|
||||
</div>
|
||||
|
||||
<div class="plan-card is-featured" data-plan="按年" style="margin-top:var(--space-5)">
|
||||
<span class="plan-pop">最受欢迎</span>
|
||||
<div class="plan-head">
|
||||
<div class="plan-meta">
|
||||
<div class="plan-name">按年</div>
|
||||
<div class="plan-per"></div>
|
||||
</div>
|
||||
<div class="plan-price"></div>
|
||||
</div>
|
||||
<button class="btn btn-primary btn-block js-buy" data-name="按年" data-until="2027-07-11">立即购买</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 支付页(USDT / 支付宝 双态,data-method 切换) -->
|
||||
<section class="screen" data-screen="pay" data-method="crypto">
|
||||
<div class="scr stack">
|
||||
|
||||
<!-- 订单信息(两态共用,值随所选套餐+渠道带过来) -->
|
||||
<div class="card order-card">
|
||||
<div class="order-hd">订单信息</div>
|
||||
<div class="order-row"><span class="order-k">套餐</span><span class="order-v" id="orderName">按年 · PRO</span></div>
|
||||
<div class="order-row"><span class="order-k">支付渠道</span><span class="order-v" id="orderChan">加密货币 · USDT</span></div>
|
||||
<div class="order-row"><span class="order-k">应付金额</span><span class="order-v order-amt" id="orderPrice">$34.99</span></div>
|
||||
<div class="order-row"><span class="order-k">到期日</span><span class="order-v" id="orderUntil">2027-07-11</span></div>
|
||||
</div>
|
||||
|
||||
<!-- USDT · crypto_address -->
|
||||
<div class="card only-crypto">
|
||||
<div class="kv-row"><span class="kv-label">网络</span><span class="kv-val">USDT · TRC20</span><button class="kv-copy js-copy" data-copy="USDT TRC20"><svg><use href="#i-copy"/></svg></button></div>
|
||||
<div class="kv-row"><span class="kv-label">收款地址</span><span class="kv-val">TB1e7q1333iuLMLc2iQaoaqeuDF2U7cxan</span><button class="kv-copy js-copy" data-copy="TB1e7q1333iuLMLc2iQaoaqeuDF2U7cxan"><svg><use href="#i-copy"/></svg></button></div>
|
||||
<div class="kv-row"><span class="kv-label">精确金额</span><span class="kv-val" id="cryptoExact">34.990001</span><button class="kv-copy js-copy" id="cryptoExactCopy" data-copy="34.990001"><svg><use href="#i-copy"/></svg></button></div>
|
||||
<div class="hint-warn">请转账 <strong id="cryptoExactHint">34.990001 USDT</strong>(含小数末位),金额用于自动对账识别,多退少不到账。</div>
|
||||
</div>
|
||||
<!-- 支付宝 · redirect -->
|
||||
<div class="card only-alipay">
|
||||
<button class="btn btn-primary btn-block btn-lg js-alipay"><svg><use href="#i-external-link"/></svg>打开支付宝</button>
|
||||
<div class="hint-mut">将跳转至支付宝完成付款,付款后自动返回本页并开通。</div>
|
||||
</div>
|
||||
|
||||
<div class="polling"><svg class="spin"><use href="#i-loader-circle"/></svg><span id="pollText">等待到账确认…</span></div>
|
||||
|
||||
<button class="btn btn-subtle btn-block js-switch">更换支付渠道</button>
|
||||
<button class="btn btn-ghost btn-block js-cancel">取消订单</button>
|
||||
|
||||
<div class="demo-zone">
|
||||
<div class="demo-cap">评审演示</div>
|
||||
<button class="btn btn-subtle btn-block js-paid">▸ 模拟到账</button>
|
||||
<button class="demo-fail js-failed">▸ 模拟失败</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 成功态 -->
|
||||
<section class="screen" data-screen="success">
|
||||
<div class="scr center">
|
||||
<div class="result">
|
||||
<div class="result-icon"><svg><use href="#i-check-circle"/></svg></div>
|
||||
<div class="result-title">支付成功</div>
|
||||
<div class="result-sub" id="successSub">PRO 有效期至 2027-07-11</div>
|
||||
</div>
|
||||
<button class="btn btn-primary btn-block btn-lg full js-done">完成</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 失败态 -->
|
||||
<section class="screen" data-screen="fail">
|
||||
<div class="scr center">
|
||||
<div class="result is-fail">
|
||||
<div class="result-icon"><svg><use href="#i-alert-triangle"/></svg></div>
|
||||
<div class="result-title">支付失败</div>
|
||||
<div class="result-sub">订单已超时,请重试</div>
|
||||
</div>
|
||||
<button class="btn btn-primary btn-block btn-lg full js-retry">重新支付</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- toast -->
|
||||
<div class="toast" id="toast"><svg><use href="#i-check"/></svg><span id="toastText"></span></div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
/* ---------- 主题 ---------- */
|
||||
function applyTheme(t){
|
||||
if(t==='dark'){ document.documentElement.dataset.theme='dark'; }
|
||||
else { delete document.documentElement.dataset.theme; }
|
||||
document.getElementById('themeIcon').setAttribute('href', t==='dark' ? '#i-moon' : '#i-sun');
|
||||
document.getElementById('themeLabel').textContent = t==='dark' ? '深色' : '浅色';
|
||||
localStorage.setItem('pangolin-ds-theme', t);
|
||||
}
|
||||
document.getElementById('themeBtn').onclick = () => {
|
||||
const cur = document.documentElement.dataset.theme==='dark' ? 'dark' : 'light';
|
||||
applyTheme(cur==='dark' ? 'light' : 'dark');
|
||||
};
|
||||
applyTheme(localStorage.getItem('pangolin-ds-theme') || 'light');
|
||||
|
||||
/* ---------- 定价:各币种一口价(pay 侧每 SKU 按结算币种各定价,不做实时汇率换算) ---------- */
|
||||
const PRICES = {
|
||||
'按月': { cny:'¥29.99', cnyPer:'¥29.99 / 月', usdt:'$4.99', usdtPer:'$4.99 / 月', usdtExact:'4.990001' },
|
||||
'按季': { cny:'¥68.88', cnyPer:'¥22.9 / 月', usdt:'$12.99', usdtPer:'$4.33 / 月', usdtExact:'12.990001' },
|
||||
'按年': { cny:'¥199.99', cnyPer:'¥16.7 / 月', usdt:'$34.99', usdtPer:'$2.92 / 月', usdtExact:'34.990001' },
|
||||
};
|
||||
/* 渠道 → 币种 / 支付方式 / 订单渠道文案(USDT→crypto 默认;人民币→alipay) */
|
||||
const CHAN = {
|
||||
usdt: { currency:'usdt', method:'crypto', orderLabel:'加密货币 · USDT' },
|
||||
cny: { currency:'cny', method:'alipay', orderLabel:'支付宝 · 人民币' },
|
||||
};
|
||||
const order = { name:'按年', until:'2027-07-11', chan:'usdt', currency:'usdt', method:'crypto', price:'$34.99', usdtExact:'34.990001' };
|
||||
|
||||
const TITLES = { purchase:'升级 PRO', pay:'完成支付', success:'完成支付', fail:'完成支付' };
|
||||
|
||||
/* 按当前渠道币种渲染三张套餐卡的价格 */
|
||||
function renderPrices(){
|
||||
const cur = CHAN[order.chan].currency;
|
||||
document.querySelectorAll('.plan-card').forEach(card => {
|
||||
const p = PRICES[card.dataset.plan];
|
||||
card.querySelector('.plan-price').textContent = cur==='usdt' ? p.usdt : p.cny;
|
||||
card.querySelector('.plan-per').textContent = cur==='usdt' ? p.usdtPer : p.cnyPer;
|
||||
});
|
||||
}
|
||||
|
||||
const screens = document.querySelectorAll('.screen');
|
||||
const payScreen = document.querySelector('.screen[data-screen="pay"]');
|
||||
const titleEl = document.getElementById('phoneTitle');
|
||||
const backBtn = document.getElementById('backBtn');
|
||||
|
||||
function show(name){
|
||||
screens.forEach(s => s.classList.toggle('is-active', s.dataset.screen === name));
|
||||
titleEl.textContent = TITLES[name] || '升级 PRO';
|
||||
backBtn.hidden = (name === 'purchase');
|
||||
document.querySelectorAll('.scr').forEach(el => el.scrollTop = 0);
|
||||
window.__screen = name;
|
||||
}
|
||||
|
||||
function enterPay(){
|
||||
const c = CHAN[order.chan];
|
||||
const p = PRICES[order.name];
|
||||
order.currency = c.currency; order.method = c.method;
|
||||
order.price = c.currency==='usdt' ? p.usdt : p.cny;
|
||||
order.usdtExact = p.usdtExact;
|
||||
payScreen.dataset.method = order.method;
|
||||
/* 订单信息卡:金额按所选结算币种显价(CNY ¥ / USDT $) */
|
||||
document.getElementById('orderName').textContent = order.name + ' · PRO';
|
||||
document.getElementById('orderChan').textContent = c.orderLabel;
|
||||
document.getElementById('orderPrice').textContent = order.price;
|
||||
document.getElementById('orderUntil').textContent = order.until;
|
||||
/* USDT 态:精确金额(含小数末位对账)随套餐带过来 */
|
||||
if(order.method === 'crypto'){
|
||||
document.getElementById('cryptoExact').textContent = p.usdtExact;
|
||||
document.getElementById('cryptoExactCopy').dataset.copy = p.usdtExact;
|
||||
document.getElementById('cryptoExactHint').textContent = p.usdtExact + ' USDT';
|
||||
}
|
||||
document.getElementById('pollText').textContent =
|
||||
order.method === 'crypto' ? '等待到账确认…' : '等待付款确认…';
|
||||
show('pay');
|
||||
}
|
||||
|
||||
/* toast */
|
||||
let toastTimer;
|
||||
function toast(msg){
|
||||
const t = document.getElementById('toast');
|
||||
document.getElementById('toastText').textContent = msg;
|
||||
t.classList.add('show');
|
||||
clearTimeout(toastTimer);
|
||||
toastTimer = setTimeout(() => t.classList.remove('show'), 1800);
|
||||
}
|
||||
|
||||
/* 支付渠道 switch:切币种 → 套餐价随之刷新 */
|
||||
document.querySelectorAll('.js-chan').forEach(b => b.onclick = () => {
|
||||
order.chan = b.dataset.chan;
|
||||
document.querySelectorAll('.js-chan').forEach(x => x.classList.toggle('is-active', x === b));
|
||||
renderPrices();
|
||||
});
|
||||
|
||||
/* 购买 → 创建订单(渠道已选)→ 支付页 */
|
||||
document.querySelectorAll('.js-buy').forEach(b => b.onclick = () => {
|
||||
order.name = b.dataset.name; order.until = b.dataset.until;
|
||||
enterPay();
|
||||
});
|
||||
|
||||
/* 支付页交互 */
|
||||
document.querySelectorAll('.js-copy').forEach(b => b.onclick = () => toast('已复制:' + b.dataset.copy));
|
||||
document.querySelector('.js-alipay').onclick = () => toast('已拉起支付宝(演示)');
|
||||
document.querySelector('.js-switch').onclick = () => show('purchase'); /* 更换渠道 → 回购买页改 switch */
|
||||
document.querySelector('.js-cancel').onclick = () => show('purchase');
|
||||
document.querySelector('.js-paid').onclick = () => {
|
||||
document.getElementById('successSub').textContent = 'PRO(' + order.name + ')有效期至 ' + order.until;
|
||||
show('success');
|
||||
};
|
||||
document.querySelector('.js-failed').onclick = () => show('fail');
|
||||
|
||||
/* 成功 / 失败 */
|
||||
document.querySelector('.js-done').onclick = () => show('purchase');
|
||||
document.querySelector('.js-retry').onclick = () => enterPay();
|
||||
|
||||
/* 顶栏返回:回上一态 */
|
||||
backBtn.onclick = () => {
|
||||
const s = window.__screen;
|
||||
if(s === 'pay'){ show('purchase'); }
|
||||
else if(s === 'success' || s === 'fail'){ enterPay(); }
|
||||
};
|
||||
|
||||
renderPrices();
|
||||
show('purchase');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,802 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>穿山甲 · 官网 Site 完整原型(可交互)</title>
|
||||
<link rel="stylesheet" href="../tokens.css">
|
||||
<link rel="stylesheet" href="../atoms.css">
|
||||
<script src="../icons.js"></script>
|
||||
<style>
|
||||
html{ scroll-behavior:smooth; }
|
||||
body{ margin:0; background:var(--bg); color:var(--fg1); font-family:var(--font-sans);
|
||||
-webkit-font-smoothing:antialiased; transition:background-color var(--dur-base) var(--ease-out); }
|
||||
|
||||
/* ===== 登记簿 chrome(顶栏 + lead),照抄 purchase.html ===== */
|
||||
.top{ position:sticky; top:0; z-index:80; height:58px; background:var(--surface);
|
||||
border-bottom:1px solid var(--border); display:flex; align-items:center; gap:var(--space-4);
|
||||
padding:0 var(--space-6); box-shadow:var(--shadow-sm); }
|
||||
.brand{ display:flex; align-items:center; gap:var(--space-3); font-family:var(--font-display);
|
||||
font-weight:700; font-size:var(--text-body); }
|
||||
.brand .mk{ width:28px; height:28px; border-radius:var(--radius-md);
|
||||
background:linear-gradient(135deg,var(--clay-500),var(--clay-800));
|
||||
color:#fff; display:flex; align-items:center; justify-content:center; }
|
||||
.brand .mk svg{ width:16px; height:16px; }
|
||||
.tag{ font-size:var(--text-caption); color:var(--fg3); padding-left:var(--space-3);
|
||||
border-left:1px solid var(--border); font-weight:500; }
|
||||
.theme-toggle{ margin-left:auto; display:inline-flex; align-items:center; gap:var(--space-2);
|
||||
background:var(--bg-subtle); border:1px solid var(--border); color:var(--fg2);
|
||||
border-radius:var(--radius-full); padding:var(--space-2) var(--space-4);
|
||||
font-weight:600; font-size:var(--text-caption); cursor:pointer; }
|
||||
.theme-toggle svg{ width:15px; height:15px; }
|
||||
.back{ font-size:var(--text-sm); color:var(--accent); text-decoration:none; font-weight:600; }
|
||||
|
||||
.wrap{ max-width:1160px; margin:0 auto; padding:var(--space-8) var(--space-6) var(--space-16); }
|
||||
.lead{ font-size:var(--text-sm); color:var(--fg2); line-height:var(--leading-relaxed);
|
||||
max-width:820px; margin:0 0 var(--space-6); }
|
||||
.lead code{ font-family:var(--font-mono); font-size:0.85em; color:var(--accent);
|
||||
background:var(--accent-subtle); padding:1px 6px; border-radius:var(--radius-sm); }
|
||||
.vp-note{ display:inline-flex; align-items:center; gap:var(--space-2); font-size:var(--text-caption);
|
||||
font-weight:600; color:var(--fg3); background:var(--bg-subtle); border:1px solid var(--border);
|
||||
border-radius:var(--radius-full); padding:var(--space-2) var(--space-3); margin-bottom:var(--space-6); }
|
||||
.vp-note svg{ width:14px; height:14px; }
|
||||
|
||||
/* ===== 浏览器外框(把整站装进一个 mock 浏览器窗口)===== */
|
||||
.browser{ border:1px solid var(--border); border-radius:var(--radius-xl); overflow:hidden;
|
||||
box-shadow:var(--shadow-lg); background:var(--bg); }
|
||||
.bchrome{ display:flex; align-items:center; gap:var(--space-3); height:42px; flex:none;
|
||||
padding:0 var(--space-4); background:var(--surface); border-bottom:1px solid var(--border); }
|
||||
.bdots{ display:flex; gap:var(--space-2); }
|
||||
.bdots i{ width:11px; height:11px; border-radius:var(--radius-full); background:var(--border-strong); }
|
||||
.baddr{ flex:1; display:inline-flex; align-items:center; gap:var(--space-2); max-width:360px;
|
||||
font-family:var(--font-mono); font-size:var(--text-caption); color:var(--fg3);
|
||||
background:var(--bg-subtle); border:1px solid var(--border); border-radius:var(--radius-full);
|
||||
padding:var(--space-1) var(--space-3); }
|
||||
.baddr svg{ width:12px; height:12px; color:var(--success); }
|
||||
|
||||
/* ===== 站点容器 ===== */
|
||||
.site{ background:var(--bg); }
|
||||
.swrap{ max-width:1080px; margin:0 auto; padding:0 var(--space-6); }
|
||||
.eyebrow{ font-family:var(--font-sans); font-size:var(--text-caption); font-weight:600;
|
||||
text-transform:uppercase; letter-spacing:var(--tracking-caps); color:var(--accent); }
|
||||
|
||||
/* 锚点滚动补偿(登记簿 top 58 + 站点 shead 60)*/
|
||||
.site [id]{ scroll-margin-top:126px; }
|
||||
|
||||
/* 站点头 */
|
||||
.shead{ position:sticky; top:58px; z-index:20; display:flex; align-items:center; gap:var(--space-5);
|
||||
height:60px; padding:0 var(--space-6); background:var(--surface);
|
||||
border-bottom:1px solid var(--border); }
|
||||
.slogo{ display:flex; align-items:center; gap:var(--space-3); text-decoration:none; }
|
||||
.slogo img{ width:34px; height:34px; display:block; }
|
||||
.slogo .wm{ display:flex; flex-direction:column; line-height:1; }
|
||||
.slogo .wm b{ font-family:var(--font-display); font-weight:700; font-size:var(--text-body); color:var(--fg1); }
|
||||
.slogo .wm small{ font-family:var(--font-display); font-size:9px; font-weight:600;
|
||||
letter-spacing:var(--tracking-caps); color:var(--accent); margin-top:2px; }
|
||||
.snav{ display:flex; align-items:center; gap:var(--space-5); margin-left:var(--space-4); }
|
||||
.snav a{ position:relative; font-size:var(--text-sm); font-weight:600; color:var(--fg2); text-decoration:none;
|
||||
padding:var(--space-1) 0; cursor:pointer; transition:color var(--dur-fast) var(--ease-out); }
|
||||
.snav a:hover{ color:var(--accent); }
|
||||
.snav a.is-active{ color:var(--accent); }
|
||||
.snav a.is-active::after{ content:""; position:absolute; left:0; right:0; bottom:-2px; height:2px;
|
||||
border-radius:var(--radius-full); background:var(--accent); }
|
||||
.shead-right{ margin-left:auto; display:flex; align-items:center; gap:var(--space-3); }
|
||||
.snavtoggle{ display:none; }
|
||||
|
||||
/* Hero */
|
||||
.hero{ padding:var(--space-16) 0 var(--space-12); }
|
||||
.hero-grid{ display:grid; grid-template-columns:1.05fr 0.95fr; gap:var(--space-12); align-items:center; }
|
||||
.hero h1{ margin:var(--space-3) 0 var(--space-4); white-space:pre-line; }
|
||||
.hero .lede{ font-size:var(--text-body-lg); color:var(--fg2); line-height:var(--leading-relaxed);
|
||||
max-width:440px; margin:0 0 var(--space-6); }
|
||||
.hero-cta{ display:flex; gap:var(--space-3); flex-wrap:wrap; }
|
||||
.trust{ display:flex; gap:var(--space-5); flex-wrap:wrap; margin-top:var(--space-8); }
|
||||
.trust span.t{ display:inline-flex; align-items:center; gap:var(--space-2); font-size:var(--text-sm);
|
||||
font-weight:600; color:var(--fg2); }
|
||||
.trust span.t svg{ width:16px; height:16px; color:var(--success); }
|
||||
|
||||
/* Hero 右侧:连接卡 */
|
||||
.orb-card{ background:var(--surface); border:1px solid var(--border); border-radius:var(--radius-2xl);
|
||||
box-shadow:var(--shadow-lg); padding:var(--space-8) var(--space-6) var(--space-6);
|
||||
display:flex; flex-direction:column; align-items:center; gap:var(--space-6); }
|
||||
.orb-meta{ display:flex; gap:var(--space-8); width:100%; justify-content:center; }
|
||||
.orb-m{ text-align:center; }
|
||||
.orb-m .k{ display:inline-flex; align-items:center; gap:var(--space-1); font-size:var(--text-caption);
|
||||
font-weight:600; color:var(--fg3); }
|
||||
.orb-m .k svg{ width:12px; height:12px; }
|
||||
.orb-m .v{ font-family:var(--font-mono); font-size:var(--text-h3); font-weight:500; color:var(--fg1);
|
||||
margin-top:var(--space-1); font-feature-settings:'tnum' 1; }
|
||||
.orb-m .v small{ font-size:var(--text-caption); color:var(--fg3); }
|
||||
.orb-node{ display:flex; align-items:center; gap:var(--space-3); width:100%;
|
||||
background:var(--bg-subtle); border:1px solid var(--border); border-radius:var(--radius-lg);
|
||||
padding:var(--space-3) var(--space-4); }
|
||||
.orb-node .cc{ width:34px; height:34px; flex:none; display:flex; align-items:center; justify-content:center;
|
||||
border-radius:var(--radius-md); background:var(--accent); color:var(--fg-on-accent);
|
||||
font-family:var(--font-mono); font-size:var(--text-caption); font-weight:600; }
|
||||
.orb-node .g{ flex:1; min-width:0; }
|
||||
.orb-node .nn{ font-size:var(--text-sm); font-weight:600; color:var(--fg1); }
|
||||
.orb-node .ns{ font-family:var(--font-mono); font-size:var(--text-caption); color:var(--fg3); margin-top:2px; }
|
||||
.orb-node .chev{ width:18px; height:18px; color:var(--fg3); }
|
||||
|
||||
/* 平台条 */
|
||||
.strip{ display:flex; align-items:center; justify-content:center; flex-wrap:wrap; gap:var(--space-5);
|
||||
padding:var(--space-6) 0 0; border-top:1px solid var(--border); margin-top:var(--space-10); }
|
||||
.strip .lbl{ font-size:var(--text-caption); font-weight:600; text-transform:uppercase;
|
||||
letter-spacing:var(--tracking-caps); color:var(--fg3); }
|
||||
.strip span.p{ display:inline-flex; align-items:center; gap:var(--space-2); font-size:var(--text-sm);
|
||||
font-weight:600; color:var(--fg2); }
|
||||
.strip span.p svg{ width:16px; height:16px; color:var(--fg3); }
|
||||
|
||||
/* 通用 section 头 */
|
||||
.band{ padding:var(--space-16) 0; }
|
||||
.band.subtle{ background:var(--bg-subtle); }
|
||||
.band.tan{ background:var(--accent-subtle); }
|
||||
.sec-head{ text-align:center; max-width:600px; margin:0 auto var(--space-10); }
|
||||
.sec-head h2{ margin:var(--space-3) 0 var(--space-3); }
|
||||
.sec-head p{ font-size:var(--text-body); color:var(--fg2); line-height:var(--leading-relaxed); margin:0; }
|
||||
|
||||
/* 定价「月付 / 年付」段控(chrome,仅用 token)*/
|
||||
.priceseg{ display:inline-flex; align-items:center; gap:var(--space-3); margin-top:var(--space-6); }
|
||||
.seg{ display:inline-flex; background:var(--bg-subtle); border:1px solid var(--border);
|
||||
border-radius:var(--radius-full); padding:3px; }
|
||||
.seg button{ border:none; background:transparent; cursor:pointer; font-family:var(--font-sans);
|
||||
font-weight:600; font-size:var(--text-sm); color:var(--fg2);
|
||||
padding:var(--space-2) var(--space-5); border-radius:var(--radius-full);
|
||||
transition:background-color var(--dur-fast) var(--ease-out), color var(--dur-fast) var(--ease-out); }
|
||||
.seg button.is-active{ background:var(--surface); color:var(--accent); box-shadow:var(--shadow-sm); }
|
||||
|
||||
/* Features 网格 */
|
||||
.feat-grid{ display:grid; grid-template-columns:repeat(3,1fr); gap:var(--space-5); }
|
||||
.feat-card{ background:var(--surface); border:1px solid var(--border); border-radius:var(--radius-xl);
|
||||
box-shadow:var(--shadow-sm); padding:var(--space-6); }
|
||||
.feat-ic{ width:44px; height:44px; border-radius:var(--radius-lg); display:flex; align-items:center;
|
||||
justify-content:center; background:var(--accent-subtle); color:var(--accent);
|
||||
margin-bottom:var(--space-4); }
|
||||
.feat-ic svg{ width:22px; height:22px; }
|
||||
.feat-card h3{ font-family:var(--font-sans); font-size:var(--text-body-lg); font-weight:700;
|
||||
color:var(--fg1); margin:0 0 var(--space-2); }
|
||||
.feat-card p{ font-size:var(--text-sm); color:var(--fg2); line-height:var(--leading-normal); margin:0; }
|
||||
|
||||
/* Pricing */
|
||||
.price-grid{ display:grid; grid-template-columns:repeat(3,1fr); gap:var(--space-6);
|
||||
align-items:start; max-width:960px; margin:0 auto; }
|
||||
.plan-card .plan-desc{ font-size:var(--text-caption); color:var(--fg3); margin-top:var(--space-1); }
|
||||
.plan-card.is-featured .plan-desc{ color:#fff; opacity:0.8; }
|
||||
.plan-card.is-featured .plan-pop{ background:#fff; }
|
||||
.plan-save{ margin-left:var(--space-2); }
|
||||
|
||||
.pay-note{ display:flex; gap:var(--space-4); align-items:flex-start; max-width:960px;
|
||||
margin:var(--space-10) auto 0; background:var(--surface); border:1px solid var(--border);
|
||||
border-radius:var(--radius-xl); box-shadow:var(--shadow-sm); padding:var(--space-6); }
|
||||
.pay-note > svg{ width:26px; height:26px; flex:none; color:var(--accent); margin-top:2px; }
|
||||
.pay-note b{ font-family:var(--font-sans); font-size:var(--text-body); color:var(--fg1); }
|
||||
.pay-note p{ font-size:var(--text-sm); color:var(--fg2); line-height:var(--leading-relaxed);
|
||||
margin:var(--space-2) 0 var(--space-4); }
|
||||
.chips{ display:flex; flex-wrap:wrap; gap:var(--space-2); }
|
||||
.chips .pill svg{ width:13px; height:13px; }
|
||||
|
||||
/* Download */
|
||||
.dl-grid{ display:grid; grid-template-columns:repeat(5,1fr); gap:var(--space-4); max-width:960px; margin:0 auto; }
|
||||
.dl-card{ background:var(--surface); border:1px solid var(--border); border-radius:var(--radius-xl);
|
||||
box-shadow:var(--shadow-sm); padding:var(--space-6) var(--space-4); text-align:center;
|
||||
display:flex; flex-direction:column; align-items:center; gap:var(--space-2);
|
||||
transition:transform var(--dur-fast) var(--ease-out), box-shadow var(--dur-fast) var(--ease-out); }
|
||||
.dl-card:hover{ transform:translateY(-3px); box-shadow:var(--shadow-lg); }
|
||||
.dl-ic{ width:48px; height:48px; border-radius:var(--radius-lg); display:flex; align-items:center;
|
||||
justify-content:center; background:var(--bg-subtle); color:var(--accent); margin-bottom:var(--space-1); }
|
||||
.dl-ic svg{ width:24px; height:24px; }
|
||||
.dl-card .pn{ font-size:var(--text-body); font-weight:700; color:var(--fg1); }
|
||||
.dl-card .pv{ font-family:var(--font-mono); font-size:var(--text-caption); color:var(--fg3); margin-bottom:var(--space-3); }
|
||||
|
||||
/* Footer */
|
||||
.foot{ background:var(--bg-subtle); border-top:1px solid var(--border); padding:var(--space-12) 0 var(--space-8); }
|
||||
.foot-grid{ display:grid; grid-template-columns:1.4fr 1fr 1fr 1fr; gap:var(--space-8); }
|
||||
.foot-brand .slogo{ margin-bottom:var(--space-3); }
|
||||
.foot-brand p{ font-size:var(--text-sm); color:var(--fg2); line-height:var(--leading-relaxed);
|
||||
max-width:280px; margin:0; }
|
||||
.foot-col h4{ font-size:var(--text-caption); font-weight:700; text-transform:uppercase;
|
||||
letter-spacing:var(--tracking-caps); color:var(--fg3); margin:0 0 var(--space-4); }
|
||||
.foot-col a{ display:block; font-size:var(--text-sm); color:var(--fg2); text-decoration:none;
|
||||
margin-bottom:var(--space-3); cursor:pointer; transition:color var(--dur-fast) var(--ease-out); }
|
||||
.foot-col a:hover{ color:var(--accent); }
|
||||
.foot-bar{ display:flex; align-items:center; flex-wrap:wrap; gap:var(--space-4);
|
||||
border-top:1px solid var(--border); margin-top:var(--space-10);
|
||||
padding-top:var(--space-6); font-size:var(--text-caption); color:var(--fg3); }
|
||||
.foot-bar .icp{ font-family:var(--font-mono); }
|
||||
.foot-bar .sep{ margin-left:auto; }
|
||||
|
||||
/* ===== 响应式 ===== */
|
||||
@media (max-width:900px){
|
||||
.feat-grid{ grid-template-columns:repeat(2,1fr); }
|
||||
.dl-grid{ grid-template-columns:repeat(3,1fr); }
|
||||
.price-grid{ grid-template-columns:1fr; max-width:420px; }
|
||||
.foot-grid{ grid-template-columns:1fr 1fr; }
|
||||
}
|
||||
@media (max-width:720px){
|
||||
.hero-grid{ grid-template-columns:1fr; gap:var(--space-8); }
|
||||
.hero{ padding:var(--space-10) 0; }
|
||||
.snav{ display:none; }
|
||||
.snavtoggle{ display:inline-flex; }
|
||||
.feat-grid{ grid-template-columns:1fr; }
|
||||
.dl-grid{ grid-template-columns:repeat(2,1fr); }
|
||||
.foot-grid{ grid-template-columns:1fr; gap:var(--space-6); }
|
||||
.foot-bar .sep{ margin-left:0; }
|
||||
.shead-right .btn-lbl{ display:none; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- 登记簿 chrome -->
|
||||
<div class="top">
|
||||
<div class="brand"><span class="mk"><img src="../assets/app-icon.svg" alt="穿山甲" style="width:100%;height:100%;object-fit:contain;border-radius:inherit"></span>穿山甲 Pangolin</div>
|
||||
<span class="tag">官网 Site · 可交互原型</span>
|
||||
<a class="back" href="../index.html">← 返回登记簿</a>
|
||||
<button class="theme-toggle" id="themeBtn" aria-label="切换主题">
|
||||
<svg><use href="#i-sun" id="themeIcon"/></svg><span id="themeLabel">浅色</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="wrap">
|
||||
<p class="lead">
|
||||
官网宣传页完整版式(顶栏 / Hero / 特性 / 定价 / 下载 / 页脚),装进 mock 浏览器窗口,<strong>已加交互</strong>:
|
||||
导航平滑滚动 + scrollspy 高亮、语言下拉(六语单显切换:简体中文 / EN / 日 / 韩 / 俄 / 西)、明暗主题、定价「月付 / 年付」段控切价。
|
||||
仅引 <code>../tokens.css</code> / <code>../atoms.css</code> / <code>../icons.js</code>,业务色全走 <code>var(--token)</code>;
|
||||
套餐数字对齐 §7;文案脱敏(铁律 #13)——只讲「网络加速 / 极速畅连 / 隐私保护 / 无日志」。切右上角看深色。
|
||||
</p>
|
||||
<div class="vp-note"><svg><use href="#i-monitor-smartphone"/></svg>此页自适应:窗口 < 720px 时导航折叠、各区块单列堆叠</div>
|
||||
|
||||
<div class="browser">
|
||||
<div class="bchrome">
|
||||
<div class="bdots"><i></i><i></i><i></i></div>
|
||||
<span class="baddr"><svg><use href="#i-lock"/></svg>pangolin.example</span>
|
||||
</div>
|
||||
|
||||
<div class="site" id="siteRoot">
|
||||
<!-- ============ 站点头 ============ -->
|
||||
<header class="shead">
|
||||
<a class="slogo" href="#top">
|
||||
<img src="../assets/logo-mark.svg" alt="穿山甲">
|
||||
<span class="wm"><b data-i18n="nav.brand">穿山甲</b><small>PANGOLIN</small></span>
|
||||
</a>
|
||||
<nav class="snav">
|
||||
<a href="#feat" data-i18n="nav.product">产品</a>
|
||||
<a href="#pricing" data-i18n="nav.pricing">定价</a>
|
||||
<a href="#download" data-i18n="nav.download">下载</a>
|
||||
<a href="#docs" data-i18n="nav.docs">文档</a>
|
||||
</nav>
|
||||
<div class="shead-right">
|
||||
<!-- 语言下拉(单语言单显,真能切换页面主要文案;6 语对齐线上 web/website)-->
|
||||
<div class="langsel" data-langsel>
|
||||
<button class="langsel-trigger" aria-haspopup="true" aria-expanded="false">
|
||||
<svg><use href="#i-globe"/></svg><span data-langlabel>简体中文</span><svg><use href="#i-chevron-down"/></svg>
|
||||
</button>
|
||||
<div class="menu" role="menu" hidden>
|
||||
<button class="menu-item is-selected" role="menuitem" data-lang="zh">简体中文</button>
|
||||
<button class="menu-item" role="menuitem" data-lang="en">English</button>
|
||||
<button class="menu-item" role="menuitem" data-lang="ja">日本語</button>
|
||||
<button class="menu-item" role="menuitem" data-lang="ko">한국어</button>
|
||||
<button class="menu-item" role="menuitem" data-lang="ru">Русский</button>
|
||||
<button class="menu-item" role="menuitem" data-lang="es">Español</button>
|
||||
</div>
|
||||
</div>
|
||||
<a class="btn btn-primary" href="#download"><svg><use href="#i-download"/></svg><span class="btn-lbl" data-i18n="nav.get">立即下载</span></a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- ============ Hero ============ -->
|
||||
<section class="swrap hero" id="top">
|
||||
<div class="hero-grid">
|
||||
<div>
|
||||
<div class="eyebrow" data-i18n="hero.eyebrow">极速 · 稳定 · 省心</div>
|
||||
<h1 class="t-display-xl" data-i18n="hero.h1">极速畅连,
|
||||
网络如丝顺滑</h1>
|
||||
<p class="lede" data-i18n="hero.lede">轻盈、亲和、即开即用的跨平台网络加速应用。一键连接,智能选线,稳定不掉线。</p>
|
||||
<div class="hero-cta">
|
||||
<a class="btn btn-primary btn-lg" href="#download"><svg><use href="#i-download"/></svg><span data-i18n="hero.cta1">免费下载</span></a>
|
||||
<a class="btn btn-subtle btn-lg" href="#pricing" data-i18n="hero.cta2">查看定价</a>
|
||||
</div>
|
||||
<div class="trust">
|
||||
<span class="t"><svg><use href="#i-check"/></svg><span data-i18n="hero.t1">全球加速线路</span></span>
|
||||
<span class="t"><svg><use href="#i-check"/></svg><span data-i18n="hero.t2">严格无日志</span></span>
|
||||
<span class="t"><svg><use href="#i-check"/></svg><span data-i18n="hero.t3">端到端加密</span></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="orb-card">
|
||||
<button class="connect-btn is-on" aria-label="已连接">
|
||||
<svg class="cb-ring" viewBox="0 0 140 140"><circle class="cb-ring-full" cx="70" cy="70" r="64"/></svg>
|
||||
<svg class="cb-icon"><use href="#i-shield-check"/></svg>
|
||||
<span class="cb-timer">02:14:08</span>
|
||||
<span class="cb-sub" data-i18n="orb.enc">已加密</span>
|
||||
</button>
|
||||
<div class="orb-meta">
|
||||
<div class="orb-m"><div class="k"><svg><use href="#i-arrow-down"/></svg><span data-i18n="orb.down">下载</span></div><div class="v">86.4<small> Mb/s</small></div></div>
|
||||
<div class="orb-m"><div class="k"><svg><use href="#i-arrow-up"/></svg><span data-i18n="orb.up">上传</span></div><div class="v">12.1<small> Mb/s</small></div></div>
|
||||
</div>
|
||||
<div class="orb-node">
|
||||
<span class="cc">HK</span>
|
||||
<div class="g"><div class="nn" data-i18n="orb.node">香港 · 流媒体</div><div class="ns" data-i18n="orb.nodesub">延迟 18ms · 已连接</div></div>
|
||||
<svg class="chev"><use href="#i-chevron-right"/></svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="strip">
|
||||
<span class="lbl" data-i18n="strip.txt">已支持</span>
|
||||
<span class="p"><svg><use href="#i-smartphone"/></svg>iOS</span>
|
||||
<span class="p"><svg><use href="#i-smartphone"/></svg>Android</span>
|
||||
<span class="p"><svg><use href="#i-laptop"/></svg>macOS</span>
|
||||
<span class="p"><svg><use href="#i-monitor"/></svg>Windows</span>
|
||||
<span class="p"><svg><use href="#i-terminal"/></svg>Linux</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ============ Features ============ -->
|
||||
<section class="band subtle" id="feat">
|
||||
<div class="swrap">
|
||||
<div class="sec-head">
|
||||
<div class="eyebrow" data-i18n="feat.eyebrow">产品</div>
|
||||
<h2 class="t-h1" data-i18n="feat.h">为日常而生的连接体验</h2>
|
||||
<p data-i18n="feat.sub">没有复杂配置,没有技术门槛。打开就用,把稳定与隐私留给我们。</p>
|
||||
</div>
|
||||
<div class="feat-grid">
|
||||
<div class="feat-card">
|
||||
<div class="feat-ic"><svg><use href="#i-power"/></svg></div>
|
||||
<h3 data-i18n="feat.1t">一键连接</h3>
|
||||
<p data-i18n="feat.1d">一颗按钮,智能挑选最快线路。无需手动配置,也能秒连。</p>
|
||||
</div>
|
||||
<div class="feat-card">
|
||||
<div class="feat-ic"><svg><use href="#i-zap"/></svg></div>
|
||||
<h3 data-i18n="feat.2t">智能分流</h3>
|
||||
<p data-i18n="feat.2d">按规则智能分流,该加速的加速,本地服务直连,互不打扰。</p>
|
||||
</div>
|
||||
<div class="feat-card">
|
||||
<div class="feat-ic"><svg><use href="#i-globe"/></svg></div>
|
||||
<h3 data-i18n="feat.3t">全球加速线路</h3>
|
||||
<p data-i18n="feat.3d">覆盖多地优质线路,视频与游戏优化随心选。</p>
|
||||
</div>
|
||||
<div class="feat-card">
|
||||
<div class="feat-ic"><svg><use href="#i-eye-off"/></svg></div>
|
||||
<h3 data-i18n="feat.4t">严格无日志</h3>
|
||||
<p data-i18n="feat.4d">我们不记录你的浏览数据。隐私是底线,不是卖点。</p>
|
||||
</div>
|
||||
<div class="feat-card">
|
||||
<div class="feat-ic"><svg><use href="#i-shield"/></svg></div>
|
||||
<h3 data-i18n="feat.5t">Kill Switch</h3>
|
||||
<p data-i18n="feat.5d">一旦连接中断,立即阻断网络,杜绝真实 IP 泄露。</p>
|
||||
</div>
|
||||
<div class="feat-card">
|
||||
<div class="feat-ic"><svg><use href="#i-monitor-smartphone"/></svg></div>
|
||||
<h3 data-i18n="feat.6t">多端同步</h3>
|
||||
<p data-i18n="feat.6d">一个账户,手机、平板、电脑同时在线,最多 5 台设备。</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ============ Pricing ============ -->
|
||||
<section class="band" id="pricing">
|
||||
<div class="swrap">
|
||||
<div class="sec-head">
|
||||
<div class="eyebrow" data-i18n="price.eyebrow">定价</div>
|
||||
<h2 class="t-h1" data-i18n="price.h">简单透明的价格</h2>
|
||||
<p data-i18n="price.sub">随时升级或取消。所有套餐均含核心加密与无日志承诺。</p>
|
||||
<div class="priceseg">
|
||||
<div class="seg" role="tablist" aria-label="计费周期">
|
||||
<button type="button" class="is-active" data-cycle="0" role="tab" aria-selected="true" data-i18n="price.monthly">按月</button>
|
||||
<button type="button" data-cycle="1" role="tab" aria-selected="false" data-i18n="price.yearly">按年</button>
|
||||
</div>
|
||||
<span class="pill is-success" data-save hidden><span class="dot"></span><span data-i18n="price.save">省 20%</span></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="price-grid">
|
||||
<!-- 免费版 -->
|
||||
<div class="plan-card">
|
||||
<div class="plan-name" data-i18n="price.free">免费版</div>
|
||||
<div class="plan-desc" data-i18n="price.freedesc">适合轻度使用与尝鲜</div>
|
||||
<div class="plan-price-row"><span class="plan-price" data-price-free>¥0</span></div>
|
||||
<div class="plan-feats">
|
||||
<div class="plan-feat"><svg><use href="#i-check"/></svg><span data-i18n="pf.free1">每日 10 分钟时长</span></div>
|
||||
<div class="plan-feat"><svg><use href="#i-check"/></svg><span data-i18n="pf.free2">仅 1 个基础节点</span></div>
|
||||
<div class="plan-feat"><svg><use href="#i-check"/></svg><span data-i18n="pf.free3">使用前观看广告</span></div>
|
||||
<div class="plan-feat"><svg><use href="#i-check"/></svg><span data-i18n="pf.free4">核心加密 · 无日志</span></div>
|
||||
</div>
|
||||
<button class="btn btn-subtle btn-block" data-i18n="price.cta_free">免费下载</button>
|
||||
</div>
|
||||
|
||||
<!-- PRO -->
|
||||
<div class="plan-card is-featured">
|
||||
<span class="plan-pop" data-i18n="price.popular">最受欢迎</span>
|
||||
<div class="plan-name" data-i18n="price.pro">专业版</div>
|
||||
<div class="plan-desc" data-i18n="price.prodesc">个人用户的最佳选择</div>
|
||||
<div class="plan-price-row"><span class="plan-price" data-price-pro>¥25</span><span class="plan-per" data-i18n="price.permo">/月</span></div>
|
||||
<div class="plan-feats">
|
||||
<div class="plan-feat"><svg><use href="#i-check"/></svg><span data-i18n="pf.pro1">全球加速线路</span></div>
|
||||
<div class="plan-feat"><svg><use href="#i-check"/></svg><span data-i18n="pf.pro2">无限流量 · 极速</span></div>
|
||||
<div class="plan-feat"><svg><use href="#i-check"/></svg><span data-i18n="pf.pro3">5 台设备同时在线</span></div>
|
||||
<div class="plan-feat"><svg><use href="#i-check"/></svg><span data-i18n="pf.pro4">流媒体 & P2P 优化</span></div>
|
||||
<div class="plan-feat"><svg><use href="#i-check"/></svg><span data-i18n="pf.pro5">Kill Switch · 智能分流</span></div>
|
||||
</div>
|
||||
<button class="btn btn-primary btn-block" data-i18n="price.cta_pro">获取激活码</button>
|
||||
</div>
|
||||
|
||||
<!-- 团队版 -->
|
||||
<div class="plan-card">
|
||||
<div class="plan-name" data-i18n="price.team">团队版</div>
|
||||
<div class="plan-desc" data-i18n="price.teamdesc">小团队集中管理</div>
|
||||
<div class="plan-price-row"><span class="plan-price" data-price-team>¥99</span><span class="plan-per" data-i18n="team.per">/月 · 10 席位</span></div>
|
||||
<div class="plan-feats">
|
||||
<div class="plan-feat"><svg><use href="#i-check"/></svg><span data-i18n="pf.team1">专业版全部功能</span></div>
|
||||
<div class="plan-feat"><svg><use href="#i-check"/></svg><span data-i18n="pf.team2">10 个成员席位</span></div>
|
||||
<div class="plan-feat"><svg><use href="#i-check"/></svg><span data-i18n="pf.team3">集中计费与管理</span></div>
|
||||
<div class="plan-feat"><svg><use href="#i-check"/></svg><span data-i18n="pf.team4">优先客服</span></div>
|
||||
</div>
|
||||
<button class="btn btn-primary btn-block" data-i18n="price.cta_team">获取激活码</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 为什么网页上没有支付按钮 -->
|
||||
<div class="pay-note">
|
||||
<svg><use href="#i-shield-check"/></svg>
|
||||
<div>
|
||||
<b data-i18n="pay.title">为什么网页上没有支付按钮?</b>
|
||||
<p data-i18n="pay.body">出于风控与隐私考虑,穿山甲不在网页或 App 内直接收款。你可以通过以下任一渠道获取激活码,在客户端内兑换即可开通。</p>
|
||||
<div class="chips">
|
||||
<span class="pill pill-outline"><svg><use href="#i-shopping-bag"/></svg><span data-i18n="pay.store">自助发卡商店</span></span>
|
||||
<span class="pill pill-outline"><svg><use href="#i-credit-card"/></svg><span data-i18n="pay.usdt">USDT (TRC20) · 最隐私</span></span>
|
||||
<span class="pill pill-outline"><svg><use href="#i-send"/></svg><span data-i18n="chip.tg">Telegram</span></span>
|
||||
<span class="pill pill-outline"><svg><use href="#i-message-circle"/></svg><span data-i18n="chip.line">LINE</span></span>
|
||||
<span class="pill pill-outline"><svg><use href="#i-mail"/></svg><span data-i18n="chip.mail">邮箱</span></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ============ Download ============ -->
|
||||
<section class="band tan" id="download">
|
||||
<div class="swrap">
|
||||
<div class="sec-head">
|
||||
<div class="eyebrow" data-i18n="dl.eyebrow">下载</div>
|
||||
<h2 class="t-h1" data-i18n="dl.h">全平台,随处可用</h2>
|
||||
<p data-i18n="dl.sub">一个账户,所有设备同步。下载即用,无需配置。</p>
|
||||
</div>
|
||||
<div class="dl-grid">
|
||||
<div class="dl-card">
|
||||
<div class="dl-ic"><svg><use href="#i-smartphone"/></svg></div>
|
||||
<div class="pn">iOS</div><div class="pv">iOS 16+</div>
|
||||
<span class="btn btn-subtle is-disabled btn-block"><svg><use href="#i-download"/></svg><span data-i18n="dl.soon">敬请期待</span></span>
|
||||
</div>
|
||||
<div class="dl-card">
|
||||
<div class="dl-ic"><svg><use href="#i-smartphone"/></svg></div>
|
||||
<div class="pn">Android</div><div class="pv">Android 9+</div>
|
||||
<button class="btn btn-primary btn-block"><svg><use href="#i-download"/></svg><span data-i18n="dl.get">下载</span></button>
|
||||
</div>
|
||||
<div class="dl-card">
|
||||
<div class="dl-ic"><svg><use href="#i-laptop"/></svg></div>
|
||||
<div class="pn">macOS</div><div class="pv">macOS 12+</div>
|
||||
<button class="btn btn-primary btn-block"><svg><use href="#i-download"/></svg><span data-i18n="dl.get">下载</span></button>
|
||||
</div>
|
||||
<div class="dl-card">
|
||||
<div class="dl-ic"><svg><use href="#i-monitor"/></svg></div>
|
||||
<div class="pn">Windows</div><div class="pv">Win 10/11</div>
|
||||
<button class="btn btn-primary btn-block"><svg><use href="#i-download"/></svg><span data-i18n="dl.get">下载</span></button>
|
||||
</div>
|
||||
<div class="dl-card">
|
||||
<div class="dl-ic"><svg><use href="#i-terminal"/></svg></div>
|
||||
<div class="pn">Linux</div><div class="pv">deb / rpm</div>
|
||||
<span class="btn btn-subtle is-disabled btn-block"><svg><use href="#i-download"/></svg><span data-i18n="dl.soon">敬请期待</span></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ============ Footer ============ -->
|
||||
<footer class="foot" id="docs">
|
||||
<div class="swrap">
|
||||
<div class="foot-grid">
|
||||
<div class="foot-brand">
|
||||
<a class="slogo" href="#top">
|
||||
<img src="../assets/logo-mark.svg" alt="穿山甲">
|
||||
<span class="wm"><b data-i18n="nav.brand">穿山甲</b><small>PANGOLIN</small></span>
|
||||
</a>
|
||||
<p data-i18n="ft.tag">极简、轻量、亲和的跨平台网络加速应用。守护你的每一次连接。</p>
|
||||
</div>
|
||||
<div class="foot-col">
|
||||
<h4 data-i18n="ft.product">产品</h4>
|
||||
<a href="#feat" data-i18n="ft.features">功能</a>
|
||||
<a href="#pricing" data-i18n="ft.pricing">定价</a>
|
||||
<a href="#download" data-i18n="ft.download">下载</a>
|
||||
</div>
|
||||
<div class="foot-col">
|
||||
<h4 data-i18n="ft.resources">资源</h4>
|
||||
<a href="#docs" data-i18n="ft.docs">文档</a>
|
||||
<a href="#docs" data-i18n="ft.faq">常见问题</a>
|
||||
<a href="#docs" data-i18n="ft.privacy">隐私政策</a>
|
||||
<a href="#docs" data-i18n="ft.status">服务状态</a>
|
||||
</div>
|
||||
<div class="foot-col">
|
||||
<h4 data-i18n="ft.contact">联系与渠道</h4>
|
||||
<a href="#" data-i18n="chip.tg">Telegram</a>
|
||||
<a href="#" data-i18n="chip.line">LINE</a>
|
||||
<a href="#" data-i18n="chip.mail">邮箱</a>
|
||||
<span class="pill is-neutral" style="margin-top:var(--space-1)"><span class="dot"></span><span data-i18n="ft.hours">服务时间 每日 9:00–24:00 (GMT+8)</span></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="foot-bar">
|
||||
<span data-i18n="ft.copy">© 2026 穿山甲 · Pangolin</span>
|
||||
<span class="icp">京ICP备 0000000 号-0</span>
|
||||
<span data-i18n="ft.madenote">App 内不支持直接支付 · 资金流全走外部渠道</span>
|
||||
<span class="sep">
|
||||
<span class="langsel" data-langsel>
|
||||
<button class="langsel-trigger" aria-haspopup="true" aria-expanded="false"><svg><use href="#i-globe"/></svg><span data-langlabel>简体中文</span><svg><use href="#i-chevron-down"/></svg></button>
|
||||
<div class="menu" role="menu" hidden style="right:auto; left:0; top:auto; bottom:calc(100% + var(--space-2))">
|
||||
<button class="menu-item is-selected" role="menuitem" data-lang="zh">简体中文</button>
|
||||
<button class="menu-item" role="menuitem" data-lang="en">English</button>
|
||||
<button class="menu-item" role="menuitem" data-lang="ja">日本語</button>
|
||||
<button class="menu-item" role="menuitem" data-lang="ko">한국어</button>
|
||||
<button class="menu-item" role="menuitem" data-lang="ru">Русский</button>
|
||||
<button class="menu-item" role="menuitem" data-lang="es">Español</button>
|
||||
</div>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div><!-- /.site -->
|
||||
</div><!-- /.browser -->
|
||||
</div>
|
||||
|
||||
<script>
|
||||
/* ============ 主题切换(明 / 暗)============ */
|
||||
function applyTheme(t){
|
||||
if(t==='dark'){ document.documentElement.dataset.theme='dark'; }
|
||||
else { delete document.documentElement.dataset.theme; }
|
||||
document.getElementById('themeIcon').setAttribute('href', t==='dark' ? '#i-moon' : '#i-sun');
|
||||
document.getElementById('themeLabel').textContent = t==='dark' ? '深色' : '浅色';
|
||||
localStorage.setItem('pangolin-ds-theme', t);
|
||||
}
|
||||
document.getElementById('themeBtn').onclick = () => {
|
||||
const cur = document.documentElement.dataset.theme==='dark' ? 'dark' : 'light';
|
||||
applyTheme(cur==='dark' ? 'light' : 'dark');
|
||||
};
|
||||
applyTheme(localStorage.getItem('pangolin-ds-theme') || 'light');
|
||||
|
||||
/* ============ i18n 字典(6 语单显,逐字取自 web/website/src/i18n/strings.ts)============ */
|
||||
const DICT = {
|
||||
'nav.brand':{zh:'穿山甲',en:'Pangolin',ja:'Pangolin',ko:'Pangolin',ru:'Pangolin',es:'Pangolin'},
|
||||
'nav.product':{zh:'产品',en:'Product',ja:'製品',ko:'제품',ru:'Продукт',es:'Producto'},
|
||||
'nav.pricing':{zh:'定价',en:'Pricing',ja:'料金',ko:'요금제',ru:'Цены',es:'Precios'},
|
||||
'nav.download':{zh:'下载',en:'Download',ja:'ダウンロード',ko:'다운로드',ru:'Скачать',es:'Descargar'},
|
||||
'nav.docs':{zh:'文档',en:'Docs',ja:'ドキュメント',ko:'문서',ru:'Документация',es:'Documentación'},
|
||||
'nav.get':{zh:'立即下载',en:'Get the app',ja:'アプリを入手',ko:'앱 받기',ru:'Получить приложение',es:'Obtener la app'},
|
||||
|
||||
'hero.eyebrow':{zh:'极速 · 稳定 · 省心',en:'Fast · Stable · Effortless',ja:'高速 · 安定 · 快適',ko:'빠름 · 안정 · 간편',ru:'Быстро · Стабильно · Без забот',es:'Rápido · Estable · Sin complicaciones'},
|
||||
'hero.h1':{zh:'极速畅连,\n网络如丝顺滑',en:'Faster, smoother,\neverywhere',ja:'もっと速く、もっと滑らかに、\nどこでも',ko:'더 빠르고 더 매끄럽게,\n어디서나',ru:'Быстрее, плавнее,\nвезде',es:'Más rápido, más fluido,\nen todas partes'},
|
||||
'hero.lede':{zh:'轻盈、亲和、即开即用的跨平台网络加速应用。一键连接,智能选线,稳定不掉线。',en:'A lightweight, friendly cross-platform network accelerator. One tap, smart routing, rock-solid.',ja:'軽快で使いやすい、クロスプラットフォームのネットワークアクセラレーター。ワンタップ接続、スマートルーティング、安定した通信。',ko:'가볍고 친근한 크로스플랫폼 네트워크 가속 앱. 원 탭 연결, 스마트 라우팅, 흔들림 없는 안정성.',ru:'Лёгкий и удобный кроссплатформенный ускоритель сети. Одно касание, умная маршрутизация, надёжное соединение.',es:'Un acelerador de red multiplataforma, ligero y fácil de usar. Un toque, enrutamiento inteligente, total estabilidad.'},
|
||||
'hero.cta1':{zh:'免费下载',en:'Download free',ja:'無料ダウンロード',ko:'무료 다운로드',ru:'Скачать бесплатно',es:'Descargar gratis'},
|
||||
'hero.cta2':{zh:'查看定价',en:'See pricing',ja:'料金を見る',ko:'요금 보기',ru:'Посмотреть цены',es:'Ver precios'},
|
||||
'hero.t1':{zh:'全球加速线路',en:'Global routes',ja:'グローバル回線',ko:'글로벌 라우트',ru:'Глобальные маршруты',es:'Rutas globales'},
|
||||
'hero.t2':{zh:'严格无日志',en:'Strict no-logs',ja:'厳格なノーログ',ko:'엄격한 노로그',ru:'Строго без логов',es:'Sin registros, estricto'},
|
||||
'hero.t3':{zh:'端到端加密',en:'End-to-end encrypted',ja:'エンドツーエンド暗号化',ko:'엔드투엔드 암호화',ru:'Сквозное шифрование',es:'Cifrado de extremo a extremo'},
|
||||
|
||||
'orb.enc':{zh:'已加密',en:'Encrypted',ja:'暗号化済み',ko:'암호화됨',ru:'Зашифровано',es:'Cifrado'},
|
||||
'orb.down':{zh:'下载',en:'Down',ja:'下り',ko:'다운',ru:'Приём',es:'Bajada'},
|
||||
'orb.up':{zh:'上传',en:'Up',ja:'上り',ko:'업',ru:'Отдача',es:'Subida'},
|
||||
'orb.node':{zh:'香港 · 流媒体',en:'Hong Kong',ja:'香港',ko:'홍콩',ru:'Гонконг',es:'Hong Kong'},
|
||||
'orb.nodesub':{zh:'延迟 18ms · 已连接',en:'18ms · Connected',ja:'18ms · 接続済み',ko:'18ms · 연결됨',ru:'18 мс · Подключено',es:'18 ms · Conectado'},
|
||||
|
||||
'strip.txt':{zh:'已支持',en:'Available on',ja:'対応プラットフォーム',ko:'지원 플랫폼',ru:'Доступно на',es:'Disponible en'},
|
||||
|
||||
'feat.eyebrow':{zh:'产品',en:'Product',ja:'製品',ko:'제품',ru:'Продукт',es:'Producto'},
|
||||
'feat.h':{zh:'为日常而生的连接体验',en:'Built for everyday connection',ja:'毎日の接続のために',ko:'일상을 위한 연결 경험',ru:'Создан для повседневных подключений',es:'Diseñado para la conexión de cada día'},
|
||||
'feat.sub':{zh:'没有复杂配置,没有技术门槛。打开就用,把稳定与隐私留给我们。',en:'No complex setup, no jargon. Just open and go — we handle speed and privacy.',ja:'複雑な設定も専門用語も不要。開いて使うだけ。速度とプライバシーはお任せください。',ko:'복잡한 설정도, 어려운 용어도 없습니다. 열면 바로 사용, 속도와 개인정보는 저희가 책임집니다.',ru:'Никаких сложных настроек и жаргона. Просто откройте и пользуйтесь — скорость и приватность на нас.',es:'Sin configuraciones complejas ni tecnicismos. Ábrelo y listo: la velocidad y la privacidad corren por nuestra cuenta.'},
|
||||
'feat.1t':{zh:'一键连接',en:'One-tap connect',ja:'ワンタップ接続',ko:'원 탭 연결',ru:'Подключение одним касанием',es:'Conexión con un toque'},
|
||||
'feat.1d':{zh:'一颗按钮,智能挑选最快线路。无需手动配置,也能秒连。',en:'One button picks the fastest route. No manual setup, instant connect.',ja:'ボタン一つで最速回線を自動選択。手動設定なしで瞬時に接続。',ko:'버튼 하나로 가장 빠른 라우트를 자동 선택. 수동 설정 없이 바로 연결.',ru:'Одна кнопка выбирает самый быстрый маршрут. Без ручных настроек — мгновенное подключение.',es:'Un botón elige la ruta más rápida. Sin ajustes manuales, conexión instantánea.'},
|
||||
'feat.2t':{zh:'智能分流',en:'Smart routing',ja:'スマートルーティング',ko:'스마트 라우팅',ru:'Умная маршрутизация',es:'Enrutamiento inteligente'},
|
||||
'feat.2d':{zh:'按规则智能分流,该加速的加速,本地服务直连,互不打扰。',en:'Smart rule-based routing — accelerate what needs it, keep local traffic direct.',ja:'ルールベースのスマートルーティングで、必要な通信は高速化、ローカル通信は直結。',ko:'규칙 기반 스마트 라우팅으로 필요한 트래픽은 가속하고 로컬 트래픽은 직접 연결.',ru:'Умная маршрутизация по правилам: ускоряем нужное, локальный трафик идёт напрямую.',es:'Enrutamiento inteligente por reglas: acelera lo que lo necesita y deja el tráfico local directo.'},
|
||||
'feat.3t':{zh:'全球加速线路',en:'Global routes',ja:'グローバル回線',ko:'글로벌 라우트',ru:'Глобальные маршруты',es:'Rutas globales'},
|
||||
'feat.3d':{zh:'覆盖多地优质线路,视频与游戏优化随心选。',en:'Premium routes across regions, tuned for video & gaming.',ja:'各地の高品質回線を網羅、動画・ゲーム向けに最適化。',ko:'여러 지역의 프리미엄 라우트, 영상과 게임에 맞춰 최적화.',ru:'Премиум-маршруты по регионам, настроенные для видео и игр.',es:'Rutas premium en varias regiones, optimizadas para vídeo y juegos.'},
|
||||
'feat.4t':{zh:'严格无日志',en:'Strict no-logs',ja:'厳格なノーログ',ko:'엄격한 노로그',ru:'Строго без логов',es:'Sin registros, estricto'},
|
||||
'feat.4d':{zh:'我们不记录你的浏览数据。隐私是底线,不是卖点。',en:'We never log your browsing. Privacy is the baseline, not a feature.',ja:'閲覧データは一切記録しません。プライバシーは売り文句ではなく、当然の前提です。',ko:'고객의 브라우징 데이터를 절대 기록하지 않습니다. 개인정보 보호는 기본이지 특별 기능이 아닙니다.',ru:'Мы не ведём журнал вашего трафика. Приватность — это база, а не опция.',es:'Nunca registramos tu navegación. La privacidad es la base, no un extra.'},
|
||||
'feat.5t':{ zh:'Kill Switch', en:'Kill Switch', ja:'キルスイッチ', ko:'킬 스위치', ru:'Kill Switch', es:'Kill Switch' },
|
||||
'feat.5d':{ zh:'一旦连接中断,立即阻断网络,杜绝真实 IP 泄露。', en:'If the tunnel drops, traffic is blocked instantly — no IP leaks.', ja:'接続が切れた瞬間に通信を遮断し、実IPの漏洩を防ぎます。', ko:'터널이 끊기면 즉시 트래픽을 차단해 실제 IP 노출을 막습니다.', ru:'Если соединение обрывается, трафик мгновенно блокируется — без утечек IP.', es:'Si la conexión se cae, el tráfico se bloquea al instante:sin filtraciones de IP.' },
|
||||
'feat.6t':{ zh:'多端同步', en:'Multi-device', ja:'マルチデバイス', ko:'멀티 디바이스', ru:'Много устройств', es:'Multidispositivo' },
|
||||
'feat.6d':{ zh:'一个账户,手机、平板、电脑同时在线,最多 5 台设备。', en:'One account across phone, tablet and desktop — up to 5 devices.', ja:'一つのアカウントでスマホ・タブレット・PCを同時に、最大5台まで。', ko:'하나의 계정으로 휴대폰, 태블릿, PC를 동시에, 최대 5대까지.', ru:'Один аккаунт на телефоне, планшете и компьютере — до 5 устройств.', es:'Una cuenta en teléfono, tableta y ordenador:hasta 5 dispositivos.' },
|
||||
|
||||
'price.eyebrow':{zh:'定价',en:'Pricing',ja:'料金',ko:'요금제',ru:'Цены',es:'Precios'},
|
||||
'price.h':{zh:'简单透明的价格',en:'Simple, honest pricing',ja:'シンプルで正直な料金',ko:'단순하고 정직한 가격',ru:'Простые и честные цены',es:'Precios simples y honestos'},
|
||||
'price.sub':{zh:'随时升级或取消。所有套餐均含核心加密与无日志承诺。',en:'Upgrade or cancel anytime. Every plan includes core encryption and our no-logs promise.',ja:'いつでもアップグレード・解約可能。すべてのプランにコア暗号化とノーログの約束が含まれます。',ko:'언제든 업그레이드하거나 해지하세요. 모든 요금제에 핵심 암호화와 노로그 약속이 포함됩니다.',ru:'Меняйте тариф или отменяйте в любой момент. В каждый план входит базовое шифрование и обещание не вести логи.',es:'Mejora o cancela cuando quieras. Cada plan incluye cifrado esencial y nuestra promesa sin registros.'},
|
||||
'price.monthly':{zh:'按月',en:'Monthly',ja:'月払い',ko:'월간',ru:'Помесячно',es:'Mensual'},
|
||||
'price.yearly':{zh:'按年',en:'Yearly',ja:'年払い',ko:'연간',ru:'Ежегодно',es:'Anual'},
|
||||
'price.save':{zh:'省 20%',en:'Save 20%',ja:'20%お得',ko:'20% 절약',ru:'Экономия 20%',es:'Ahorra 20%'},
|
||||
'price.permo':{zh:'/月',en:'/mo',ja:'/月',ko:'/월',ru:'/мес',es:'/mes'},
|
||||
'price.free':{zh:'免费版',en:'Free',ja:'無料版',ko:'무료',ru:'Бесплатный',es:'Gratis'},
|
||||
'price.freedesc':{zh:'适合轻度使用与尝鲜',en:'For light, casual use',ja:'ライトなお試し利用に',ko:'가볍게 써보는 사용자에게',ru:'Для лёгкого, эпизодического использования',es:'Para un uso ligero y ocasional'},
|
||||
'price.pro':{zh:'专业版',en:'Pro',ja:'プロ版',ko:'프로',ru:'Pro',es:'Pro'},
|
||||
'price.prodesc':{zh:'个人用户的最佳选择',en:'Best for individuals',ja:'個人ユーザーに最適',ko:'개인 사용자에게 최적',ru:'Оптимально для одного пользователя',es:'Ideal para uso individual'},
|
||||
'price.team':{zh:'团队版',en:'Team',ja:'チーム版',ko:'팀',ru:'Команда',es:'Equipo'},
|
||||
'price.teamdesc':{zh:'小团队集中管理',en:'For small teams',ja:'小規模チームの一括管理に',ko:'소규모 팀 통합 관리',ru:'Для небольших команд',es:'Para equipos pequeños'},
|
||||
'price.popular':{zh:'最受欢迎',en:'Most popular',ja:'一番人気',ko:'가장 인기',ru:'Самый популярный',es:'El más popular'},
|
||||
'price.cta_free':{zh:'免费下载',en:'Download free',ja:'無料ダウンロード',ko:'무료 다운로드',ru:'Скачать бесплатно',es:'Descargar gratis'},
|
||||
'price.cta_pro':{zh:'获取激活码',en:'Get a code',ja:'コードを入手',ko:'코드 받기',ru:'Получить код',es:'Obtener un código'},
|
||||
'price.cta_team':{zh:'获取激活码',en:'Get a code',ja:'コードを入手',ko:'코드 받기',ru:'Получить код',es:'Obtener un código'},
|
||||
'team.per':{zh:'/月 · 10 席位',en:'/mo · 10 seats',ja:'/月 · 10席',ko:'/월 · 10석',ru:'/мес · 10 мест',es:'/mes · 10 plazas'},
|
||||
|
||||
'pf.free1':{zh:'每日 10 分钟时长',en:'10 min per day',ja:'1日10分',ko:'하루 10분',ru:'10 минут в день',es:'10 min al día'},
|
||||
'pf.free2':{zh:'仅 1 个基础节点',en:'1 basic node only',ja:'基本ノード1つのみ',ko:'기본 노드 1개만',ru:'Только 1 базовый узел',es:'Solo 1 nodo básico'},
|
||||
'pf.free3':{zh:'使用前观看广告',en:'Watch an ad to start',ja:'利用前に広告を視聴',ko:'사용 전 광고 시청',ru:'Просмотр рекламы перед запуском',es:'Ver un anuncio para empezar'},
|
||||
'pf.free4':{zh:'核心加密 · 无日志',en:'Core encryption · no-logs',ja:'コア暗号化 · ノーログ',ko:'핵심 암호화 · 노로그',ru:'Базовое шифрование · без логов',es:'Cifrado esencial · sin registros'},
|
||||
'pf.pro1':{zh:'全球加速线路',en:'Global routes',ja:'グローバル回線',ko:'글로벌 라우트',ru:'Глобальные маршруты',es:'Rutas globales'},
|
||||
'pf.pro2':{zh:'无限流量 · 极速',en:'Unlimited · top speed',ja:'無制限 · 最高速度',ko:'무제한 · 최고 속도',ru:'Без лимитов · максимальная скорость',es:'Ilimitado · máxima velocidad'},
|
||||
'pf.pro3':{zh:'5 台设备同时在线',en:'5 devices at once',ja:'5台同時接続',ko:'동시 5대',ru:'5 устройств одновременно',es:'5 dispositivos a la vez'},
|
||||
'pf.pro4':{zh:'流媒体 & P2P 优化',en:'Streaming & P2P routes',ja:'ストリーミング & P2P最適化',ko:'스트리밍 & P2P 라우트',ru:'Маршруты для стриминга и P2P',es:'Rutas para streaming y P2P'},
|
||||
'pf.pro5':{zh:'Kill Switch · 智能分流',en:'Kill Switch · smart routing',ja:'キルスイッチ · スマートルーティング',ko:'킬 스위치 · 스마트 라우팅',ru:'Kill Switch · умная маршрутизация',es:'Kill Switch · enrutamiento inteligente'},
|
||||
'pf.team1':{zh:'专业版全部功能',en:'Everything in Pro',ja:'プロ版の全機能',ko:'프로의 모든 기능',ru:'Всё из тарифа Pro',es:'Todo lo de Pro'},
|
||||
'pf.team2':{zh:'10 个成员席位',en:'10 seats',ja:'10メンバー席',ko:'멤버 10석',ru:'10 мест для участников',es:'10 plazas'},
|
||||
'pf.team3':{zh:'集中计费与管理',en:'Central billing & admin',ja:'一括請求と管理',ko:'통합 결제 및 관리',ru:'Единый биллинг и администрирование',es:'Facturación y administración centralizadas'},
|
||||
'pf.team4':{zh:'优先客服',en:'Priority support',ja:'優先サポート',ko:'우선 지원',ru:'Приоритетная поддержка',es:'Soporte prioritario'},
|
||||
|
||||
'pay.title':{zh:'为什么网页上没有支付按钮?',en:'Why no checkout on this page?',ja:'なぜこのページに決済ボタンがないのですか?',ko:'이 페이지에 결제 버튼이 없는 이유는?',ru:'Почему на этой странице нет оплаты?',es:'¿Por qué no hay pago en esta página?'},
|
||||
'pay.body':{zh:'出于风控与隐私考虑,穿山甲不在网页或 App 内直接收款。你可以通过以下任一渠道获取激活码,在客户端内兑换即可开通。',en:'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.',ja:'リスク管理とプライバシーの観点から、Pangolinはウェブやアプリ内で直接決済を受け付けません。以下のいずれかの窓口でアクティベーションコードを入手し、クライアント内で引き換えてください。',ko:'리스크 관리와 개인정보 보호를 위해 Pangolin은 웹이나 앱에서 직접 결제를 받지 않습니다. 아래 채널에서 활성화 코드를 받아 클라이언트에서 등록하세요.',ru:'В целях безопасности и приватности Pangolin не принимает оплату в вебе или приложении. Получите код активации по любому из каналов ниже и активируйте его в клиенте.',es:'Por motivos de seguridad y privacidad, Pangolin nunca cobra en la web ni en la app. Consigue un código de activación por cualquiera de estos canales y canjéalo en el cliente.'},
|
||||
'pay.store':{zh:'自助发卡商店',en:'Self-serve store',ja:'セルフサービスストア',ko:'셀프 발급 스토어',ru:'Магазин самообслуживания',es:'Tienda de autoservicio'},
|
||||
'pay.usdt':{zh:'USDT (TRC20) · 最隐私',en:'USDT (TRC20) · most private',ja:'USDT (TRC20) · 最もプライベート',ko:'USDT (TRC20) · 가장 프라이빗',ru:'USDT (TRC20) · максимум приватности',es:'USDT (TRC20) · lo más privado'},
|
||||
'chip.tg':{zh:'Telegram',en:'Telegram',ja:'Telegram',ko:'Telegram',ru:'Telegram',es:'Telegram'},
|
||||
'chip.line':{zh:'LINE',en:'LINE',ja:'LINE',ko:'LINE',ru:'LINE',es:'LINE'},
|
||||
'chip.mail':{zh:'邮箱',en:'Email',ja:'メール',ko:'이메일',ru:'Эл. почта',es:'Correo'},
|
||||
|
||||
'dl.eyebrow':{zh:'下载',en:'Download',ja:'ダウンロード',ko:'다운로드',ru:'Скачать',es:'Descargar'},
|
||||
'dl.h':{zh:'全平台,随处可用',en:'Every platform, everywhere',ja:'すべてのプラットフォーム、どこでも',ko:'모든 플랫폼, 어디서나',ru:'Все платформы, везде',es:'Todas las plataformas, en todas partes'},
|
||||
'dl.sub':{zh:'一个账户,所有设备同步。下载即用,无需配置。',en:'One account syncs every device. Download and go.',ja:'一つのアカウントで全デバイスを同期。ダウンロードしてすぐ使えます。',ko:'하나의 계정으로 모든 기기를 동기화. 내려받아 바로 사용.',ru:'Один аккаунт синхронизирует все устройства. Скачал — и в путь.',es:'Una cuenta sincroniza todos tus dispositivos. Descarga y listo.'},
|
||||
'dl.get':{zh:'下载',en:'Download',ja:'ダウンロード',ko:'다운로드',ru:'Скачать',es:'Descargar'},
|
||||
'dl.soon':{zh:'敬请期待',en:'Coming soon',ja:'近日公開',ko:'출시 예정',ru:'Скоро',es:'Próximamente'},
|
||||
|
||||
'ft.tag':{zh:'极简、轻量、亲和的跨平台网络加速应用。守护你的每一次连接。',en:'A minimal, friendly cross-platform network accelerator. Protecting every connection.',ja:'ミニマルで軽快、使いやすいクロスプラットフォームのネットワークアクセラレーター。すべての接続を守ります。',ko:'미니멀하고 가벼우며 친근한 크로스플랫폼 네트워크 가속 앱. 모든 연결을 지킵니다.',ru:'Минималистичный, лёгкий и удобный кроссплатформенный ускоритель сети. Защищает каждое соединение.',es:'Un acelerador de red multiplataforma minimalista, ligero y amable. Protegiendo cada conexión.'},
|
||||
'ft.product':{zh:'产品',en:'Product',ja:'製品',ko:'제품',ru:'Продукт',es:'Producto'},
|
||||
'ft.resources':{zh:'资源',en:'Resources',ja:'リソース',ko:'리소스',ru:'Ресурсы',es:'Recursos'},
|
||||
'ft.contact':{zh:'联系与渠道',en:'Contact',ja:'お問い合わせ',ko:'문의 및 채널',ru:'Контакты',es:'Contacto'},
|
||||
'ft.features':{zh:'功能',en:'Features',ja:'機能',ko:'기능',ru:'Возможности',es:'Funciones'},
|
||||
'ft.pricing':{zh:'定价',en:'Pricing',ja:'料金',ko:'요금제',ru:'Цены',es:'Precios'},
|
||||
'ft.download':{zh:'下载',en:'Download',ja:'ダウンロード',ko:'다운로드',ru:'Скачать',es:'Descargar'},
|
||||
'ft.docs':{zh:'文档',en:'Docs',ja:'ドキュメント',ko:'문서',ru:'Документация',es:'Documentación'},
|
||||
'ft.faq':{zh:'常见问题',en:'FAQ',ja:'よくある質問',ko:'자주 묻는 질문',ru:'Вопросы и ответы',es:'Preguntas frecuentes'},
|
||||
'ft.privacy':{zh:'隐私政策',en:'Privacy',ja:'プライバシー',ko:'개인정보',ru:'Конфиденциальность',es:'Privacidad'},
|
||||
'ft.status':{zh:'服务状态',en:'Status',ja:'サービス状況',ko:'서비스 상태',ru:'Статус',es:'Estado'},
|
||||
'ft.hours':{zh:'服务时间 每日 9:00–24:00 (GMT+8)',en:'Hours: Daily 9:00–24:00 (GMT+8)',ja:'営業時間 毎日 9:00–24:00 (GMT+8)',ko:'운영 시간 매일 9:00–24:00 (GMT+8)',ru:'Часы работы: ежедневно 9:00–24:00 (GMT+8)',es:'Horario: todos los días 9:00–24:00 (GMT+8)'},
|
||||
'ft.copy':{zh:'© 2026 穿山甲 · Pangolin',en:'© 2026 Pangolin',ja:'© 2026 Pangolin',ko:'© 2026 Pangolin',ru:'© 2026 Pangolin',es:'© 2026 Pangolin'},
|
||||
'ft.madenote':{zh:'App 内不支持直接支付 · 资金流全走外部渠道',en:'No in-app payment · all purchases via external channels',ja:'アプリ内決済なし · すべての購入は外部窓口で',ko:'앱 내 결제 없음 · 모든 구매는 외부 채널로',ru:'Без оплаты в приложении · все покупки через внешние каналы',es:'Sin pago en la app · todas las compras por canales externos'}
|
||||
};
|
||||
|
||||
/* 语言原生名(下拉触发器显示)*/
|
||||
const LANG_LABEL = { zh:'简体中文', en:'English', ja:'日本語', ko:'한국어', ru:'Русский', es:'Español' };
|
||||
const HTML_LANG = { zh:'zh-CN', en:'en', ja:'ja', ko:'ko', ru:'ru', es:'es' };
|
||||
|
||||
/* 价格表:[月付, 年付·按月],对齐 §7 / strings.ts(zh 人民币、其余美元)*/
|
||||
const PRICES = {
|
||||
zh:{ free:['¥0','¥0'], pro:['¥25','¥20'], team:['¥99','¥79'] },
|
||||
en:{ free:['$0','$0'], pro:['$3.99','$3.19'], team:['$13.99','$11.19'] },
|
||||
ja:{ free:['$0','$0'], pro:['$3.99','$3.19'], team:['$13.99','$11.19'] },
|
||||
ko:{ free:['$0','$0'], pro:['$3.99','$3.19'], team:['$13.99','$11.19'] },
|
||||
ru:{ free:['$0','$0'], pro:['$3.99','$3.19'], team:['$13.99','$11.19'] },
|
||||
es:{ free:['$0','$0'], pro:['$3.99','$3.19'], team:['$13.99','$11.19'] }
|
||||
};
|
||||
|
||||
let curLang = localStorage.getItem('pangolin-site-lang') || 'zh';
|
||||
if(!LANG_LABEL[curLang]) curLang = 'zh';
|
||||
let curCycle = 0; // 0=月付 1=年付
|
||||
|
||||
function renderPrices(){
|
||||
const p = PRICES[curLang] || PRICES.en;
|
||||
document.querySelector('[data-price-free]').textContent = p.free[curCycle];
|
||||
document.querySelector('[data-price-pro]').textContent = p.pro[curCycle];
|
||||
document.querySelector('[data-price-team]').textContent = p.team[curCycle];
|
||||
document.querySelectorAll('[data-save]').forEach(el => el.hidden = curCycle !== 1);
|
||||
}
|
||||
|
||||
function setLang(lang){
|
||||
if(!LANG_LABEL[lang]) lang = 'zh';
|
||||
curLang = lang;
|
||||
document.documentElement.lang = HTML_LANG[lang] || lang;
|
||||
document.querySelectorAll('[data-i18n]').forEach(el => {
|
||||
const v = DICT[el.getAttribute('data-i18n')];
|
||||
if(v) el.textContent = (v[lang] != null ? v[lang] : v.en);
|
||||
});
|
||||
document.querySelectorAll('[data-langlabel]').forEach(el => el.textContent = LANG_LABEL[lang]);
|
||||
document.querySelectorAll('[data-langsel] .menu-item').forEach(it => {
|
||||
const on = it.getAttribute('data-lang') === lang;
|
||||
it.classList.toggle('is-selected', on);
|
||||
});
|
||||
renderPrices();
|
||||
localStorage.setItem('pangolin-site-lang', lang);
|
||||
}
|
||||
|
||||
/* ============ 语言下拉:展开/收起 + 选中切换(头 + 脚共用)============ */
|
||||
function initLangsels(){
|
||||
const sels = [...document.querySelectorAll('[data-langsel]')];
|
||||
function closeAll(){
|
||||
sels.forEach(s => {
|
||||
s.classList.remove('is-open');
|
||||
const t = s.querySelector('.langsel-trigger');
|
||||
const m = s.querySelector('.menu');
|
||||
if(t) t.setAttribute('aria-expanded','false');
|
||||
if(m) m.hidden = true;
|
||||
});
|
||||
}
|
||||
sels.forEach(sel => {
|
||||
const trigger = sel.querySelector('.langsel-trigger');
|
||||
const menu = sel.querySelector('.menu');
|
||||
trigger.addEventListener('click', e => {
|
||||
e.stopPropagation();
|
||||
const willOpen = menu.hidden;
|
||||
closeAll();
|
||||
if(willOpen){
|
||||
menu.hidden = false;
|
||||
trigger.setAttribute('aria-expanded','true');
|
||||
sel.classList.add('is-open');
|
||||
}
|
||||
});
|
||||
menu.querySelectorAll('.menu-item').forEach(it => {
|
||||
it.addEventListener('click', () => { setLang(it.getAttribute('data-lang')); closeAll(); });
|
||||
});
|
||||
});
|
||||
document.addEventListener('click', closeAll);
|
||||
}
|
||||
|
||||
/* ============ 定价「月 / 年」段控 ============ */
|
||||
function initPriceSeg(){
|
||||
const seg = document.querySelector('.seg');
|
||||
if(!seg) return;
|
||||
seg.querySelectorAll('button').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
curCycle = Number(btn.getAttribute('data-cycle'));
|
||||
seg.querySelectorAll('button').forEach(b => {
|
||||
const on = b === btn;
|
||||
b.classList.toggle('is-active', on);
|
||||
b.setAttribute('aria-selected', String(on));
|
||||
});
|
||||
renderPrices();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/* ============ 顶栏导航:平滑滚动 + scrollspy 高亮 ============ */
|
||||
function initNav(){
|
||||
// 站内锚点平滑滚动(scroll-margin-top 已处理 sticky 偏移)
|
||||
document.querySelectorAll('a[href^="#"]').forEach(a => {
|
||||
a.addEventListener('click', e => {
|
||||
const id = a.getAttribute('href');
|
||||
if(id === '#') return;
|
||||
const target = document.querySelector(id);
|
||||
if(!target) return;
|
||||
e.preventDefault();
|
||||
target.scrollIntoView({ behavior:'smooth', block:'start' });
|
||||
});
|
||||
});
|
||||
|
||||
// scrollspy:观察各区块,高亮对应导航项
|
||||
const map = { feat:'#feat', pricing:'#pricing', download:'#download', docs:'#docs' };
|
||||
const navLinks = [...document.querySelectorAll('.snav a')];
|
||||
function setActive(id){
|
||||
navLinks.forEach(l => l.classList.toggle('is-active', l.getAttribute('href') === '#' + id));
|
||||
}
|
||||
const sections = Object.keys(map).map(id => document.getElementById(id)).filter(Boolean);
|
||||
const obs = new IntersectionObserver(entries => {
|
||||
// 取当前处于视口上部、可见比例最高的区块
|
||||
let best = null;
|
||||
entries.forEach(en => {
|
||||
if(en.isIntersecting){
|
||||
if(!best || en.intersectionRatio > best.intersectionRatio) best = en;
|
||||
}
|
||||
});
|
||||
if(best) setActive(best.target.id);
|
||||
}, { rootMargin:'-140px 0px -55% 0px', threshold:[0,0.1,0.25,0.5,1] });
|
||||
sections.forEach(s => obs.observe(s));
|
||||
}
|
||||
|
||||
initLangsels();
|
||||
initPriceSeg();
|
||||
initNav();
|
||||
setLang(curLang);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,62 @@
|
||||
#!/usr/bin/env node
|
||||
// check-l1-sync.mjs —— L1 跨端同源闸(ds-flow)。零依赖。
|
||||
// ① 图标集合:Flutter 图标集 == 原型 sprite 图标集(camel 化后逐名相等)。
|
||||
// 真源 design/prototype/icons.js;镜像 client/lib/widgets/pangolin_icons.dart。
|
||||
// ② l10n 完整性:app_text.dart 声明的每个 getter,6 个语言文件必须全部实现
|
||||
// (谁少 key 就红 —— 防「某语言漏字串」漂移,如支付字串只加 zh/en)。
|
||||
// 任一不通过 → 列出并 exit 1。
|
||||
import { readFileSync, existsSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { dirname, resolve } from 'node:path';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const root = resolve(__dirname, '..', '..', '..');
|
||||
let failed = 0;
|
||||
|
||||
/* ① 图标集合 */
|
||||
const spriteKeys = new Set(
|
||||
[...readFileSync(resolve(root, 'design/prototype/icons.js'), 'utf8')
|
||||
.matchAll(/'([a-z0-9-]+)'\s*:\s*'</g)].map((m) => m[1].replace(/-([a-z0-9])/g, (_, c) => c.toUpperCase()))
|
||||
);
|
||||
const dartKeys = new Set(
|
||||
[...readFileSync(resolve(root, 'client/lib/widgets/pangolin_icons.dart'), 'utf8')
|
||||
.matchAll(/LucideIcons\.([A-Za-z0-9]+)/g)].map((m) => m[1])
|
||||
);
|
||||
const iconMissing = [...spriteKeys].filter((k) => !dartKeys.has(k)).sort();
|
||||
const iconExtra = [...dartKeys].filter((k) => !spriteKeys.has(k)).sort();
|
||||
if (iconMissing.length === 0 && iconExtra.length === 0) {
|
||||
console.log(`✓ 图标同集 OK —— ${spriteKeys.size} 个 sprite ↔ Flutter 逐名相等`);
|
||||
} else {
|
||||
failed++;
|
||||
console.error('✗ 图标同集 MISMATCH:');
|
||||
if (iconMissing.length) console.error(' sprite 有 / Flutter 缺:', iconMissing.join(', '));
|
||||
if (iconExtra.length) console.error(' Flutter 多 / sprite 无:', iconExtra.join(', '));
|
||||
console.error(' 修复:node design/codegen/gen_flutter_icons.mjs');
|
||||
}
|
||||
|
||||
/* ② Flutter l10n 完整性 */
|
||||
const L10N = resolve(root, 'client/lib/l10n');
|
||||
const BASE = resolve(L10N, 'app_text.dart');
|
||||
const LANGS = ['zh', 'en', 'ja', 'ko', 'ru', 'es'];
|
||||
const getterSet = (src) => new Set([...src.matchAll(/\bString\s+get\s+(\w+)\b/g)].map((m) => m[1]));
|
||||
if (!existsSync(BASE)) {
|
||||
console.log('· l10n 完整性:跳过(无 app_text.dart)');
|
||||
} else {
|
||||
// canonical = 仅抽象 getter(`String get X;` 分号结尾);块体默认 `String get X {…}` 是继承默认,不要求 langs 实现
|
||||
const canonical = new Set([...readFileSync(BASE, 'utf8').matchAll(/\bString\s+get\s+(\w+)\s*;/g)].map((m) => m[1]));
|
||||
const gaps = [];
|
||||
for (const lang of LANGS) {
|
||||
const f = resolve(L10N, `strings_${lang}.dart`);
|
||||
if (!existsSync(f)) { gaps.push(` ${lang}: 文件缺失`); continue; }
|
||||
const miss = [...canonical].filter((k) => !getterSet(readFileSync(f, 'utf8')).has(k)).sort();
|
||||
if (miss.length) gaps.push(` ${lang}: 缺 ${miss.length} 个 → ${miss.slice(0, 12).join(', ')}${miss.length > 12 ? ' …' : ''}`);
|
||||
}
|
||||
if (gaps.length === 0) {
|
||||
console.log(`✓ l10n 完整 OK —— ${canonical.size} 个 getter × ${LANGS.length} 语言全实现`);
|
||||
} else {
|
||||
failed++;
|
||||
console.error('✗ l10n 不完整(某语言漏 key):');
|
||||
gaps.forEach((g) => console.error(g));
|
||||
}
|
||||
}
|
||||
process.exit(failed ? 1 : 0);
|
||||
@@ -0,0 +1,28 @@
|
||||
# pay v2 支付+订单 — 自动开发决策日志
|
||||
|
||||
> 2026-07-11 起,自动模式落地。用户明早 double check。每个决断记这里。
|
||||
|
||||
## 背景
|
||||
用户指令:把今天设计的支付/订单功能全部落成真实代码(Flutter + Go),前后端测试,增量提交,自行决断,留记录。
|
||||
|
||||
## 最终 UX 模型(prototype 已定稿)
|
||||
- 购买页一个**支付渠道 segswitch**:默认 **USDT→加密货币**,可切 **人民币→支付宝**。渠道=币种=方式,一步定,无弹层。
|
||||
- 套餐价随渠道切换(一口价不换算):月 ¥29.99/$4.99 · 季 ¥68.88/$12.99 · 年 ¥199.99/$34.99。
|
||||
- 买完直达支付页:crypto=USDT 地址+精确金额;alipay=桌面二维码/移动拉起 App。
|
||||
- 订单:列表→详情,状态 pending/paid/settled/expired。
|
||||
- nezha(哪吒)已从购买流程移除。
|
||||
|
||||
## 决策日志
|
||||
- **[step0]** prototype 支付渠道 switch 收口(3 屏 + `.segswitch` 原子入真相源+登记)。→ 提交后进开发。
|
||||
|
||||
## [step1] prototype 支付渠道 switch 收口(2026-07-11)
|
||||
- `.segswitch` 原子入 atoms.css + 登记 index.html;purchase.html/ui-mobile/ui-desktop 三屏改成购买页渠道 switch(默认 USDT→crypto / 人民币→alipay),删两步弹层 + nezha。
|
||||
- 闸全绿:check-ds 5 项 / check-l1-sync(58 图标 + 190 l10n×6)。
|
||||
|
||||
## [step2] 服务端 pay orders + 分币种 catalog(2026-07-11)
|
||||
- catalog.go:`CatalogItem.PriceUsdtMicro`(USDT 展示价,一口价)+ `SettlementCurrency`/`DisplayAmountMinor` helper(crypto→USDT / 法币→CNY)。
|
||||
- store.go:`PurchaseRow.CreatedAt`;`Insert` 增 amount/currency 参(下单落展示金额,webhook 覆盖实际);新增 `ListByUser`。
|
||||
- handler.go:新增 `List`(GET /v1/pay/orders,纯本地台账倒序)+ `orderView` 富字段;`GetOrder` 详情回带 sku/plan/method/amount/currency/created_at/paid_at;下单按方式落展示金额。
|
||||
- main.go:注册 `GET /v1/pay/orders`。
|
||||
- 测试:新增 List 越权/倒序/富字段、下单展示金额分币种、详情富字段;**修 3 个 pre-existing 迁移测试**(renumber 21→23 遗留)。`go test ./...` 全绿。
|
||||
- 决策:USDT 展示价放服务端 catalog($4.99/$12.99/$34.99),客户端 switch 直接读,不硬编码;实际扣款仍以 pay 侧为准。
|
||||
+53
-136
@@ -1,145 +1,62 @@
|
||||
#!/usr/bin/env node
|
||||
// tools/check-l1-sync.mjs — L1 设计真相源跨端同源闸(纯 Node 零依赖)
|
||||
//
|
||||
// 治理依据:根 CLAUDE.md「前端设计系统治理(ds-flow)」——tokens/icons 以
|
||||
// design/prototype/ 为单一真源,Web 两端(website/usercenter)只能是同步副本。
|
||||
// Flutter 的 token 同源由 ci/check-codegen-drift.sh 守护(codegen 零 diff),
|
||||
// 本闸补 Web token 值同源 + 三端图标 ⊆ 原型 sprite + Web 硬编码色扫描。
|
||||
//
|
||||
// 本地/CI 直接跑:node tools/check-l1-sync.mjs
|
||||
//
|
||||
// 四道检查(任一违规 exit 1):
|
||||
// ① website token 值同源 web/website/src/styles/tokens.gen.css 每个 token
|
||||
// 值 ≡ design/prototype/tokens.css(:root + dark)。
|
||||
// ② usercenter token 同源 web/usercenter/public/colors_and_type.css 同上。
|
||||
// ③ 图标 ⊆ 原型 sprite usercenter LUCIDE(键+路径)+ Flutter pangolin_icons
|
||||
// _byName(键)必须 ⊆ 原型 icons.js ICONS。
|
||||
// ④ Web 硬编码色扫描 website src / usercenter app|components|lib 禁裸 hex;
|
||||
// 白名单 #fff/#000 + 品牌 logo 固定色;行内 `ds-allow` 豁免;
|
||||
// 排除生成的 token 定义文件。
|
||||
|
||||
import { readFileSync, readdirSync, statSync, existsSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
// check-l1-sync.mjs —— L1 跨端同源闸(ds-flow)。零依赖。
|
||||
// ① 图标集合:Flutter 图标集 == 原型 sprite 图标集(camel 化后逐名相等)。
|
||||
// 真源 design/prototype/icons.js;镜像 client/lib/widgets/pangolin_icons.dart。
|
||||
// ② l10n 完整性:app_text.dart 声明的每个 getter,6 个语言文件必须全部实现
|
||||
// (谁少 key 就红 —— 防「某语言漏字串」漂移,如支付字串只加 zh/en)。
|
||||
// 任一不通过 → 列出并 exit 1。
|
||||
import { readFileSync, existsSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { dirname, resolve } from 'node:path';
|
||||
|
||||
const ROOT = fileURLToPath(new URL('..', import.meta.url));
|
||||
const read = (p) => readFileSync(join(ROOT, p), 'utf8');
|
||||
const problems = [];
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const root = resolve(__dirname, '..');
|
||||
let failed = 0;
|
||||
|
||||
// 解析 CSS 块里的 --var: value;(压掉空白便于比较)
|
||||
function parseVars(cssBlock) {
|
||||
const out = {};
|
||||
for (const [, k, v] of cssBlock.matchAll(/(--[\w-]+)\s*:\s*([^;}]+)[;}]/g)) {
|
||||
out[k] = v.replace(/\s+/g, ' ').trim();
|
||||
}
|
||||
return out;
|
||||
}
|
||||
// 取 tokens.css 的 :root + [data-theme="dark"] 全量 token
|
||||
function protoTokens() {
|
||||
const css = read('design/prototype/tokens.css');
|
||||
const root = css.match(/:root\s*\{([^}]*)\}/s)?.[1] ?? '';
|
||||
const dark = css.match(/\[data-theme="dark"\]\s*\{([^}]*)\}/s)?.[1] ?? '';
|
||||
return { ...parseVars(root), ...parseVars(dark) };
|
||||
/* ① 图标集合 */
|
||||
const spriteKeys = new Set(
|
||||
[...readFileSync(resolve(root, 'design/prototype/icons.js'), 'utf8')
|
||||
.matchAll(/'([a-z0-9-]+)'\s*:\s*'</g)].map((m) => m[1].replace(/-([a-z0-9])/g, (_, c) => c.toUpperCase()))
|
||||
);
|
||||
const dartKeys = new Set(
|
||||
[...readFileSync(resolve(root, 'client/lib/widgets/pangolin_icons.dart'), 'utf8')
|
||||
.matchAll(/LucideIcons\.([A-Za-z0-9]+)/g)].map((m) => m[1])
|
||||
);
|
||||
const iconMissing = [...spriteKeys].filter((k) => !dartKeys.has(k)).sort();
|
||||
const iconExtra = [...dartKeys].filter((k) => !spriteKeys.has(k)).sort();
|
||||
if (iconMissing.length === 0 && iconExtra.length === 0) {
|
||||
console.log(`✓ 图标同集 OK —— ${spriteKeys.size} 个 sprite ↔ Flutter 逐名相等`);
|
||||
} else {
|
||||
failed++;
|
||||
console.error('✗ 图标同集 MISMATCH:');
|
||||
if (iconMissing.length) console.error(' sprite 有 / Flutter 缺:', iconMissing.join(', '));
|
||||
if (iconExtra.length) console.error(' Flutter 多 / sprite 无:', iconExtra.join(', '));
|
||||
console.error(' 修复:node design/codegen/gen_flutter_icons.mjs');
|
||||
}
|
||||
|
||||
// ── ①② Web token 值同源 ────────────────────────────────────
|
||||
function checkWebTokenSync(label, webPath) {
|
||||
if (!existsSync(join(ROOT, webPath))) {
|
||||
problems.push(`[${label}] 找不到 ${webPath} —— 先跑 npm run gen:tokens`);
|
||||
return;
|
||||
/* ② Flutter l10n 完整性 */
|
||||
const L10N = resolve(root, 'client/lib/l10n');
|
||||
const BASE = resolve(L10N, 'app_text.dart');
|
||||
const LANGS = ['zh', 'en', 'ja', 'ko', 'ru', 'es'];
|
||||
const getterSet = (src) => new Set([...src.matchAll(/\bString\s+get\s+(\w+)\b/g)].map((m) => m[1]));
|
||||
if (!existsSync(BASE)) {
|
||||
console.log('· l10n 完整性:跳过(无 app_text.dart)');
|
||||
} else {
|
||||
// canonical = 仅抽象 getter(`String get X;` 分号结尾);块体默认 `String get X {…}` 是继承默认,不要求 langs 实现
|
||||
const canonical = new Set([...readFileSync(BASE, 'utf8').matchAll(/\bString\s+get\s+(\w+)\s*;/g)].map((m) => m[1]));
|
||||
const gaps = [];
|
||||
for (const lang of LANGS) {
|
||||
const f = resolve(L10N, `strings_${lang}.dart`);
|
||||
if (!existsSync(f)) { gaps.push(` ${lang}: 文件缺失`); continue; }
|
||||
const miss = [...canonical].filter((k) => !getterSet(readFileSync(f, 'utf8')).has(k)).sort();
|
||||
if (miss.length) gaps.push(` ${lang}: 缺 ${miss.length} 个 → ${miss.slice(0, 12).join(', ')}${miss.length > 12 ? ' …' : ''}`);
|
||||
}
|
||||
const proto = protoTokens();
|
||||
const web = parseVars(read(webPath));
|
||||
for (const [k, v] of Object.entries(proto)) {
|
||||
if (!(k in web)) {
|
||||
problems.push(`[${label}] 缺 token ${k} —— ${webPath} 未同步原型(重跑 gen:tokens)`);
|
||||
} else if (web[k] !== v) {
|
||||
problems.push(`[${label}] ${k} 值漂移:原型=${v} Web=${web[k]} —— ${webPath} 应由 build-tokens 从原型再生,勿手改`);
|
||||
}
|
||||
if (gaps.length === 0) {
|
||||
console.log(`✓ l10n 完整 OK —— ${canonical.size} 个 getter × ${LANGS.length} 语言全实现`);
|
||||
} else {
|
||||
failed++;
|
||||
console.error('✗ l10n 不完整(某语言漏 key):');
|
||||
gaps.forEach((g) => console.error(g));
|
||||
}
|
||||
}
|
||||
|
||||
// ── ③ 图标 ⊆ 原型 sprite ────────────────────────────────────
|
||||
{
|
||||
// 原型 icons.js: var ICONS = { 'name': '<path.../>', ... }
|
||||
const protoJs = read('design/prototype/icons.js');
|
||||
const protoBody = protoJs.match(/var ICONS\s*=\s*\{([\s\S]*?)\n\s*\};/)?.[1] ?? '';
|
||||
const protoIcons = {};
|
||||
for (const [, id, body] of protoBody.matchAll(/'([^']+)'\s*:\s*'([^']*)'/g)) protoIcons[id] = body;
|
||||
if (!Object.keys(protoIcons).length) {
|
||||
problems.push('[icons] 解析原型 icons.js ICONS 为空 —— 检查文件结构');
|
||||
}
|
||||
|
||||
// usercenter: export const LUCIDE: Record<string,string> = { 'name': '<path/>', ... }
|
||||
const ucJs = read('web/usercenter/components/icons.tsx');
|
||||
const ucBody = ucJs.match(/LUCIDE\s*:[^=]*=\s*\{([\s\S]*?)\n\};/)?.[1] ?? ucJs.match(/LUCIDE\s*=\s*\{([\s\S]*?)\n\};/)?.[1] ?? '';
|
||||
// 键可能带引号('refresh-cw')或裸标识符(home)—— 两种都匹配(裸键此前被漏检)。
|
||||
for (const [, id, body] of ucBody.matchAll(/['"]?([\w-]+)['"]?\s*:\s*'([^']*)'/g)) {
|
||||
if (!(id in protoIcons)) {
|
||||
problems.push(`[icons] usercenter 图标「${id}」不在原型 sprite —— 先登记 design/prototype/icons.js 再用`);
|
||||
} else if (protoIcons[id] !== body) {
|
||||
problems.push(`[icons] usercenter 图标「${id}」路径与原型 sprite 不一致 —— 以原型为准`);
|
||||
}
|
||||
}
|
||||
|
||||
// Flutter: static const Map<String,IconData> _byName = { 'name': ..., }
|
||||
const dart = read('client/lib/widgets/pangolin_icons.dart');
|
||||
const dartBody = dart.match(/_byName\s*=\s*\{([\s\S]*?)\};/)?.[1] ?? '';
|
||||
for (const [, id] of dartBody.matchAll(/'([^']+)'\s*:/g)) {
|
||||
if (!(id in protoIcons)) {
|
||||
problems.push(`[icons] Flutter 图标「${id}」不在原型 sprite —— 新图标先登记 design/prototype/icons.js`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── ④ Web 硬编码色扫描 ──────────────────────────────────────
|
||||
{
|
||||
// 白名单:纯白/纯黑 + 品牌 logo 固定色(SVG 内联 fill,非业务散色)
|
||||
const ALLOW = new Set(['fff', 'ffffff', '000', '000000', 'b96a3d', 'faf3ed', 'f4efe8', '9e5630', '3d2213']);
|
||||
// 排除:生成的 token 定义文件 + 构建产物
|
||||
const SKIP = new Set([
|
||||
'web/website/src/styles/tokens.gen.css',
|
||||
'web/usercenter/public/colors_and_type.css',
|
||||
]);
|
||||
const SKIP_DIR = new Set(['node_modules', 'dist', 'out', '.next', 'build', '.astro', 'public']);
|
||||
const ROOTS = ['web/website/src', 'web/usercenter/app', 'web/usercenter/components', 'web/usercenter/lib'];
|
||||
const files = [];
|
||||
const walk = (dir) => {
|
||||
if (!existsSync(join(ROOT, dir))) return;
|
||||
for (const name of readdirSync(join(ROOT, dir))) {
|
||||
if (SKIP_DIR.has(name)) continue;
|
||||
const rel = `${dir}/${name}`;
|
||||
const st = statSync(join(ROOT, rel));
|
||||
if (st.isDirectory()) walk(rel);
|
||||
else if (/\.(astro|jsx|tsx|ts|js|css)$/.test(name) && !SKIP.has(rel)) files.push(rel);
|
||||
}
|
||||
};
|
||||
ROOTS.forEach(walk);
|
||||
for (const f of files) {
|
||||
read(f).split('\n').forEach((line, i) => {
|
||||
if (line.includes('ds-allow')) return;
|
||||
for (const [hex] of line.matchAll(/#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})\b/g)) {
|
||||
const v = hex.slice(1).toLowerCase();
|
||||
if (!ALLOW.has(v)) {
|
||||
problems.push(`[web-hex] ${f}:${i + 1} 硬编码色 ${hex} —— 改用 var(--token),确需保留(如品牌 logo)加行内 ds-allow 注释`);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
checkWebTokenSync('website-token', 'web/website/src/styles/tokens.gen.css');
|
||||
checkWebTokenSync('usercenter-token', 'web/usercenter/public/colors_and_type.css');
|
||||
|
||||
// ── 汇总 ────────────────────────────────────────────────────
|
||||
const line = '='.repeat(60);
|
||||
if (problems.length) {
|
||||
console.error(line);
|
||||
console.error(`✗ L1 跨端同源闸未过(${problems.length} 处):`);
|
||||
for (const p of problems) console.error(' · ' + p);
|
||||
console.error(line);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(line);
|
||||
console.log('✓ 通过:L1 跨端同源(website/usercenter token 值 · 三端图标 ⊆ 原型 sprite · Web 无硬编码色)');
|
||||
console.log(line);
|
||||
process.exit(failed ? 1 : 0);
|
||||
|
||||
Reference in New Issue
Block a user