feat(web): localize and redesign operations console

This commit is contained in:
chick
2026-07-18 22:22:41 +08:00
parent d575ab4d9d
commit abfd07f8a3
34 changed files with 1536 additions and 1058 deletions
+63 -28
View File
@@ -34,14 +34,53 @@ type ReadState =
| { kind: 'error'; message: string; snapshot?: OverviewSnapshot };
const SECTIONS = [
['device', 'Device'],
['device', '设备'],
['sim', 'SIM'],
['network', 'Network'],
['stats', 'Statistics'],
['network', '网络'],
['stats', '统计'],
['cpu', 'CPU'],
['connectivity', 'Connectivity'],
['connectivity', '连接状态'],
] as const;
const FIELD_LABELS: Readonly<Record<string, string>> = {
Model: '型号',
Uptime: '运行时间',
Slots: '卡槽数',
Active: '活跃数',
Operator: '运营商',
Registration: '注册状态',
'Messages today': '今日消息数',
Calls: '通话数',
Usage: '使用率',
Temperature: '温度',
State: '状态',
Latency: '延迟',
};
const ENUM_LABELS: Readonly<Record<string, string>> = {
connected: '已连接',
disconnected: '未连接',
connecting: '正在连接',
registered: '已注册',
unregistered: '未注册',
searching: '正在搜索',
roaming: '漫游中',
online: '在线',
offline: '离线',
unknown: '未知',
};
function displayFieldLabel(key: string): string {
return FIELD_LABELS[key] ?? key;
}
function displayFieldValue(value: OverviewFieldValue): string {
if (value == null) return '不可用';
if (typeof value === 'boolean') return value ? '是' : '否';
if (typeof value === 'string') return ENUM_LABELS[value.toLocaleLowerCase()] ?? value;
return String(value);
}
function StructuredSection({ label, values }: { label: string; values: OverviewSection }) {
const entries = Object.entries(values);
return (
@@ -51,13 +90,13 @@ function StructuredSection({ label, values }: { label: string; values: OverviewS
<dl>
{entries.map(([key, value]) => (
<div key={key}>
<dt>{key}</dt>
<dd>{value == null ? 'Unavailable' : String(value)}</dd>
<dt>{displayFieldLabel(key)}</dt>
<dd>{displayFieldValue(value)}</dd>
</div>
))}
</dl>
) : (
<p>No {label.toLocaleLowerCase()} data was supplied.</p>
<p>{label}</p>
)}
</section>
);
@@ -103,7 +142,7 @@ export function OverviewSystemPage({
if (request === requestOwner.current && !controller.signal.aborted)
setState((current) => ({
kind: 'error',
message: 'Overview data could not be loaded.',
message: '无法加载概览数据。',
...(current.kind === 'loading' && current.snapshot
? { snapshot: current.snapshot }
: {}),
@@ -116,15 +155,14 @@ export function OverviewSystemPage({
if (instance.authentication === 'auth-required')
return (
<div className="state-panel state-error" role="alert">
Authentication is required before overview data can be read for this instance.
</div>
);
if (!dataSource)
return (
<div className="state-panel" role="status" aria-label="Overview unavailable">
No safe overview read data source is available. This console will not invent or call an
uncontracted production endpoint.
<div className="state-panel" role="status" aria-label="概览不可用">
</div>
);
@@ -133,17 +171,17 @@ export function OverviewSystemPage({
if ((state.kind === 'loading' || state.kind === 'idle') && !retainedSnapshot)
return (
<p role="status" aria-label="Overview loading status">
Loading overview
<p role="status" aria-label="概览加载状态">
</p>
);
if (state.kind === 'error' && !state.snapshot)
return (
<div className="state-panel state-error" role="alert">
<p>Unable to load overview: {state.message}</p>
<p> {state.message}</p>
<button type="button" onClick={() => setRetry((value) => value + 1)}>
Retry loading overview
</button>
</div>
);
@@ -153,32 +191,29 @@ export function OverviewSystemPage({
return (
<div className="overview-system">
{state.kind === 'loading' ? <p role="status">Refreshing overview</p> : null}
{state.kind === 'loading' ? <p role="status"></p> : null}
{state.kind === 'error' ? (
<div className="state-panel state-error" role="alert">
<p>Refresh failed; showing the last known overview.</p>
<p></p>
<button type="button" onClick={() => setRetry((value) => value + 1)}>
Retry loading overview
</button>
</div>
) : null}
{instance.freshness !== 'fresh' ? (
<p className="state-panel" role="status" aria-label="Overview freshness">
Overview data is {instance.freshness}; verify freshness before relying on these values.
<p className="state-panel" role="status" aria-label="概览数据新鲜度">
使
</p>
) : null}
{snapshot.observedAt ? <p>Observed {snapshot.observedAt}</p> : null}
{snapshot.observedAt ? <p>{snapshot.observedAt}</p> : null}
<div className="overview-grid">
{SECTIONS.map(([key, label]) => (
<StructuredSection key={key} label={label} values={snapshot[key]} />
))}
</div>
<section className="state-panel" aria-label="System actions">
<h2>System actions</h2>
<p>
Restart is unavailable because the backend R3 restart operation is not implemented. No
action is offered.
</p>
<section className="state-panel" aria-label="系统操作">
<h2></h2>
<p> R3 </p>
</section>
</div>
);