diff --git a/design/CONTRACT.md b/design/CONTRACT.md
index b17df3d..2332c55 100644
--- a/design/CONTRACT.md
+++ b/design/CONTRACT.md
@@ -58,10 +58,11 @@
- 搜索框:bg-subtle、radius md、padding `11 14`、maxWidth 380、marginBottom 14。
- 网格:**2 列 `1fr 1fr`,gap 11**;每格 padding `13 15`、minHeight 56、active=accent-subtle+accent-border:CC + 名(14.5/600) + 副 + ping(mono 12) + Signal + active check。
-### 统计视图 TabStats
+### 统计视图 TabStats(上聚合 · 下分设备)
- padding `6 28 24`。
-- 3 指标卡一行,gap 14,marginBottom 20;各 padding `18 20`:label(12.5/fg3/600) + 值(mono 26/fg1)。
-- 周柱卡 padding `20 22`:标题(13.5/600) + 7 柱(gap 22, 容器高150, 柱 maxWidth 40, accent opacity .85, radius `6 6 0 0`) + 值(mono 10.5) + 星期(11.5)。
+- **上聚合**:3 指标卡一行,gap 14,marginBottom 20;各 padding `18 20`:label(12.5/fg3/600) + 值(mono 26/fg1)。周柱卡 padding `20 22`:标题(13.5/600) + 7 柱(gap 22, 容器高150, 柱 maxWidth 40, accent opacity .85, radius `6 6 0 0`) + 值(mono 10.5) + 星期(11.5)。
+- **下分设备**「设备明细 / By device」(统一组件 `DeviceStatRow`,四端复用):周柱卡下 marginTop 20,标题(13/600/fg1);设备卡 surface+border+radius-lg+shadow-sm,行内分隔线 border。每行 padding `14 16/15 18`:左平台图标盒 38×38(accent-subtle, radius md, Lucide smartphone/laptop/monitor-smartphone, clay 2.2px) + 中(名 14.5–15/600 省略号 + 占比迷你条 height 4 radius 999, track=border, fill=accent .85, 宽=本设备流量/最忙设备) + 右(流量 mono 14.5–15 + " GB" caption;时长 caption "X h")。流量降序;空数据显示占位「暂无设备用量 / No device usage yet」。
+ - 数据源:`GET /v1/usage/devices?days=N`(账户内每设备窗口聚合,busiest-first)→ `deviceUsageProvider`。
### 账户视图 TabAccount
- padding `6 28 24`,maxWidth 640(生产页功能更丰富,沿用同设计语言:套餐横幅 + 分区卡 + 行 + 段控/开关)。
diff --git a/design/ui_kits/desktop/dapp.jsx b/design/ui_kits/desktop/dapp.jsx
index 5fa9e4e..c45dde7 100644
--- a/design/ui_kits/desktop/dapp.jsx
+++ b/design/ui_kits/desktop/dapp.jsx
@@ -135,6 +135,11 @@ function DServers({ t, lang, current, onPick }) {
function DStats({ t, lang }) {
const vals=[2.1,3.4,1.8,4.6,5.2,6.1,3.0]; const labels=t('days'); const max=6.1;
+ const devs=[
+ { name:'iPhone 15', icon:'smartphone', gb:'11.3', h:'5.2', frac:1.0 },
+ { name:'MacBook Pro', icon:'laptop', gb:'6.0', h:'3.1', frac:0.53 },
+ { name:'Pixel 8', icon:'smartphone', gb:'1.6', h:'1.1', frac:0.14 },
+ ];
return (
@@ -157,6 +162,26 @@ function DStats({ t, lang }) {
))}
+ {t('byDevice')}
+
+ {devs.map((d,i)=>(
+
+ ))}
+
);
}
diff --git a/design/ui_kits/desktop/dparts.jsx b/design/ui_kits/desktop/dparts.jsx
index 9dd45b8..be21b38 100644
--- a/design/ui_kits/desktop/dparts.jsx
+++ b/design/ui_kits/desktop/dparts.jsx
@@ -70,6 +70,7 @@ const DSTRINGS = {
search:{zh:'搜索国家 / 城市',en:'Search country / city'},
trafficMonth:{zh:'本月流量',en:'Traffic'}, avgPing:{zh:'平均延迟',en:'Avg ping'}, durMonth:{zh:'本月时长',en:'Time'},
weekTraffic:{zh:'本周流量 (GB)',en:'This week (GB)'},
+ byDevice:{zh:'设备明细',en:'By device'}, noDeviceUsage:{zh:'暂无设备用量',en:'No device usage yet'},
autostart:{zh:'开机自启动',en:'Launch at login'}, autostartSub:{zh:'登录系统时自动启动穿山甲',en:'Start Pangolin when you log in'},
smartRoute:{zh:'智能分流',en:'Smart routing'}, smartRouteSub:{zh:'海外应用加速,本地直连',en:'Accelerate overseas apps, keep local direct'},
killSwitch:{zh:'Kill Switch',en:'Kill Switch'}, killSwitchSub:{zh:'断线时阻断网络,防止 IP 泄露',en:'Block all traffic if connection drops'},
diff --git a/design/ui_kits/mobile/parts.jsx b/design/ui_kits/mobile/parts.jsx
index a8fb961..e876b59 100644
--- a/design/ui_kits/mobile/parts.jsx
+++ b/design/ui_kits/mobile/parts.jsx
@@ -144,6 +144,8 @@ const STRINGS = {
durMonth: { zh: '本月时长', en: 'Time' },
weekTraffic: { zh: '本周流量 (GB)', en: 'This week (GB)' },
days7: { zh: ['一','二','三','四','五','六','日'], en: ['M','T','W','T','F','S','S'] },
+ byDevice: { zh: '设备明细', en: 'By device' },
+ noDeviceUsage:{ zh: '暂无设备用量', en: 'No device usage yet' },
proMember: { zh: 'PRO 会员', en: 'PRO member' },
expires: { zh: '有效期至', en: 'Expires' },
usage: { zh: '本月用量', en: 'Used this month' },
diff --git a/design/ui_kits/mobile/screens.jsx b/design/ui_kits/mobile/screens.jsx
index d9bc97e..ac47343 100644
--- a/design/ui_kits/mobile/screens.jsx
+++ b/design/ui_kits/mobile/screens.jsx
@@ -298,6 +298,12 @@ function SettingsScreen({ t, lang, setLang, dark, setDark, free, setFree, onRede
function StatsScreen({ t, lang }) {
const vals = [2.1, 3.4, 1.8, 4.6, 5.2, 6.1, 3.0]; const max = 6.1; const labels = t('days7');
const card = { background: 'var(--surface)', border: '1px solid var(--border)', borderRadius: 'var(--radius-lg)', boxShadow: 'var(--shadow-sm)' };
+ // 分设备用量(下分设备):名称 + 占比迷你条 + 流量/时长,流量降序。
+ const devs = [
+ { name: 'iPhone 15', icon: 'smartphone', gb: '11.3', h: '5.2', frac: 1.0 },
+ { name: 'MacBook Pro', icon: 'laptop', gb: '6.0', h: '3.1', frac: 0.53 },
+ { name: 'Pixel 8', icon: 'smartphone', gb: '1.6', h: '1.1', frac: 0.14 },
+ ];
return (
@@ -323,6 +329,26 @@ function StatsScreen({ t, lang }) {
))}
+ {t('byDevice')}
+
+ {devs.map((d, i) => (
+
+ ))}
+
);
diff --git a/design/ui_kits/tablet/tabapp.jsx b/design/ui_kits/tablet/tabapp.jsx
index 880348f..31a4300 100644
--- a/design/ui_kits/tablet/tabapp.jsx
+++ b/design/ui_kits/tablet/tabapp.jsx
@@ -23,6 +23,8 @@ const TSTR = {
statTime: { zh: '本月时长', en: 'Time this month' },
weekTraffic:{ zh: '本周流量 (GB)', en: 'This week (GB)' },
days: { zh: ['周一','周二','周三','周四','周五','周六','周日'], en: ['Mon','Tue','Wed','Thu','Fri','Sat','Sun'] },
+ byDevice: { zh: '设备明细', en: 'By device' },
+ noDeviceUsage:{ zh: '暂无设备用量', en: 'No device usage yet' },
acctEmail: { zh: '邮箱', en: 'Email' },
language: { zh: '语言', en: 'Language' },
darkMode: { zh: '深色外观', en: 'Dark appearance' },
@@ -220,6 +222,11 @@ function TabStats({ tt, lang }) {
const days = TSTR.days[lang];
const max = Math.max(...vals);
const card = { background: 'var(--surface)', border: '1px solid var(--border)', borderRadius: 'var(--radius-lg)', boxShadow: 'var(--shadow-sm)' };
+ const devs = [
+ { name: 'iPhone 15', icon: 'smartphone', gb: '11.3', h: '5.2', frac: 1.0 },
+ { name: 'MacBook Pro', icon: 'laptop', gb: '6.0', h: '3.1', frac: 0.53 },
+ { name: 'Pixel 8', icon: 'smartphone', gb: '1.6', h: '1.1', frac: 0.14 },
+ ];
return (
@@ -242,6 +249,26 @@ function TabStats({ tt, lang }) {
))}
+ {tt('byDevice')}
+
+ {devs.map((d, i) => (
+
+ ))}
+
);
}