feat(web): protect phone data and polish release UI
This commit is contained in:
+324
-293
@@ -16,6 +16,7 @@ import type {
|
||||
FleetMessagesDataSource,
|
||||
} from './fleet-messages-api-data-source.js';
|
||||
import { loadFleetMessageSummaries } from './fleet-messages-api-data-source.js';
|
||||
import { formatPhoneNumbers } from './phone-privacy.js';
|
||||
import { createOperationClient, type OperationClient } from '../operations/operation-client.js';
|
||||
import { safeUiError } from '../ui/locale.js';
|
||||
import { Icon, type IconName } from '../ui/icon.js';
|
||||
@@ -171,6 +172,7 @@ export function FleetPage({
|
||||
direction: 'asc',
|
||||
});
|
||||
const [selectedIds, setSelectedIds] = useState<ReadonlySet<string>>(new Set());
|
||||
const [revealedPhoneIds, setRevealedPhoneIds] = useState<ReadonlySet<string>>(new Set());
|
||||
const [selectionMode, setSelectionMode] = useState(false);
|
||||
const [filtersCollapsed, setFiltersCollapsed] = useState(false);
|
||||
const [batchBusy, setBatchBusy] = useState(false);
|
||||
@@ -458,6 +460,15 @@ export function FleetPage({
|
||||
});
|
||||
}
|
||||
|
||||
function togglePhonePrivacy(id: string): void {
|
||||
setRevealedPhoneIds((current) => {
|
||||
const next = new Set(current);
|
||||
if (next.has(id)) next.delete(id);
|
||||
else next.add(id);
|
||||
return next;
|
||||
});
|
||||
}
|
||||
|
||||
function selectedTargets(): Array<{ instanceId: string; revision: number }> {
|
||||
const instances = snapshot?.instances ?? [];
|
||||
return model.selectedIds.flatMap((id) => {
|
||||
@@ -637,7 +648,6 @@ export function FleetPage({
|
||||
<div className="fleet-heading">
|
||||
<div>
|
||||
<h1 id="fleet-title">节点</h1>
|
||||
<p>统一观察蜂窝设备健康度、连接状态与关键资源,快速进入节点执行运维。</p>
|
||||
</div>
|
||||
<div className="fleet-actions">
|
||||
<a className="primary-action" href="/instances/new">
|
||||
@@ -668,7 +678,6 @@ export function FleetPage({
|
||||
>
|
||||
<div className="fleet-sidebar-header">
|
||||
<div>
|
||||
<p className="fleet-sidebar-eyebrow">NODE DIRECTORY</p>
|
||||
<strong>
|
||||
<Icon name="server" />
|
||||
节点总览
|
||||
@@ -701,7 +710,6 @@ export function FleetPage({
|
||||
实例总数
|
||||
</span>
|
||||
<strong>{fleetSummary.total}</strong>
|
||||
<small>已纳入统一管理</small>
|
||||
</div>
|
||||
<div data-tone="online">
|
||||
<span>
|
||||
@@ -709,7 +717,6 @@ export function FleetPage({
|
||||
在线
|
||||
</span>
|
||||
<strong>{fleetSummary.online}</strong>
|
||||
<small>连接与认证正常</small>
|
||||
</div>
|
||||
<div data-tone="attention">
|
||||
<span>
|
||||
@@ -717,7 +724,6 @@ export function FleetPage({
|
||||
需处理
|
||||
</span>
|
||||
<strong>{fleetSummary.attention}</strong>
|
||||
<small>离线、认证或未知</small>
|
||||
</div>
|
||||
</section>
|
||||
) : null}
|
||||
@@ -796,9 +802,6 @@ export function FleetPage({
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<p className="fleet-sidebar-result-count" aria-live="polite">
|
||||
显示 <strong>{model.filteredCount}</strong> / {model.totalCount}
|
||||
</p>
|
||||
{activeFilterChips.length > 0 ? (
|
||||
<div className="fleet-active-filters" aria-label="当前筛选条件">
|
||||
{activeFilterChips.map((chip) => (
|
||||
@@ -885,9 +888,6 @@ export function FleetPage({
|
||||
</div>
|
||||
<div className="fleet-results-header">
|
||||
<div>
|
||||
<p className="fleet-results-eyebrow">
|
||||
<Icon name="grid" /> LIVE NODE MATRIX
|
||||
</p>
|
||||
<p className="fleet-result-count" aria-live="polite">
|
||||
显示 <strong>{model.filteredCount}</strong> / {model.totalCount}
|
||||
{model.pageCount > 1 ? ` · 第 ${model.page}/${model.pageCount} 页` : null}
|
||||
@@ -969,297 +969,328 @@ export function FleetPage({
|
||||
<>
|
||||
<section className="fleet-card-section" aria-label="实例卡片">
|
||||
<div className="fleet-card-grid">
|
||||
{model.rows.map((row) => (
|
||||
<Card
|
||||
role="article"
|
||||
className={`fleet-card${row.selected ? ' is-selected' : ''}`}
|
||||
data-status={row.statusKind}
|
||||
aria-label={`${row.displayName} 实例概览`}
|
||||
key={row.id}
|
||||
>
|
||||
<div className="fleet-card-glow" aria-hidden="true" />
|
||||
<header>
|
||||
<div className="fleet-card-title-group">
|
||||
<span className="fleet-node-avatar" aria-hidden="true">
|
||||
{initials(row.displayName)}
|
||||
</span>
|
||||
<div>
|
||||
<a
|
||||
className="fleet-card-entry"
|
||||
href={`/instances/${encodeURIComponent(row.id)}/overview`}
|
||||
aria-label={`打开 ${row.displayName} 实例仪表盘`}
|
||||
>
|
||||
<h2>{row.displayName}</h2>
|
||||
<span className="fleet-card-version">
|
||||
{row.version ? `SimAdmin ${row.version}` : '版本未知'}
|
||||
</span>
|
||||
<span className="fleet-card-entry-label">
|
||||
进入仪表盘 <Icon name="chevron-right" />
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div className="fleet-card-header-meta">
|
||||
<Tag
|
||||
className={`status status-${row.statusKind}`}
|
||||
color={STATUS_COLORS[row.statusKind]}
|
||||
variant="soft"
|
||||
size="small"
|
||||
>
|
||||
<span className="status-dot" aria-hidden="true" />
|
||||
{STATUS_LABELS[row.statusKind]}
|
||||
</Tag>
|
||||
{selectionMode ? (
|
||||
<label className="fleet-card-select touch-target">
|
||||
<input
|
||||
type="checkbox"
|
||||
aria-label={`选择 ${row.displayName}`}
|
||||
checked={row.selected}
|
||||
onChange={(event) => toggleOne(row.id, event.currentTarget.checked)}
|
||||
/>
|
||||
</label>
|
||||
) : null}
|
||||
<div className="fleet-card-menu">
|
||||
<button
|
||||
ref={(element) => {
|
||||
if (element) cardMenuTriggerRefs.current.set(row.id, element);
|
||||
else cardMenuTriggerRefs.current.delete(row.id);
|
||||
}}
|
||||
type="button"
|
||||
className="fleet-card-menu-trigger"
|
||||
aria-label={`实例操作 ${row.displayName}`}
|
||||
aria-haspopup="menu"
|
||||
aria-expanded={cardMenuId === row.id}
|
||||
aria-controls={`fleet-card-menu-${row.id}`}
|
||||
title="实例操作"
|
||||
onClick={() => {
|
||||
cardMenuInitialFocus.current = 'first';
|
||||
setCardMenuId((current) =>
|
||||
current === row.id ? undefined : row.id,
|
||||
);
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key !== 'ArrowDown' && event.key !== 'ArrowUp') return;
|
||||
event.preventDefault();
|
||||
cardMenuInitialFocus.current =
|
||||
event.key === 'ArrowUp' ? 'last' : 'first';
|
||||
setCardMenuId(row.id);
|
||||
}}
|
||||
>
|
||||
<Icon name="more" />
|
||||
</button>
|
||||
{cardMenuId === row.id ? (
|
||||
<div
|
||||
ref={cardMenuPanelRef}
|
||||
id={`fleet-card-menu-${row.id}`}
|
||||
className="fleet-card-menu-panel"
|
||||
role="menu"
|
||||
aria-label={`${row.displayName} 操作`}
|
||||
onKeyDown={(event) => moveCardMenuFocus(event, row.id)}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
role="menuitem"
|
||||
tabIndex={-1}
|
||||
aria-label={`重启服务 ${row.displayName}`}
|
||||
disabled={cardAction?.id === row.id && cardAction.busy}
|
||||
onClick={() => {
|
||||
closeCardMenu(row.id, true);
|
||||
void runCardAction(row.id, 'service-restart');
|
||||
}}
|
||||
>
|
||||
<Icon name="restart" />
|
||||
<span>重启服务</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
role="menuitem"
|
||||
tabIndex={-1}
|
||||
className="fleet-card-menu-danger"
|
||||
aria-label={`系统重启 ${row.displayName}`}
|
||||
disabled={cardAction?.id === row.id && cardAction.busy}
|
||||
onClick={() => {
|
||||
closeCardMenu(row.id, true);
|
||||
void runCardAction(row.id, 'system-reboot');
|
||||
}}
|
||||
>
|
||||
<Icon name="alert" />
|
||||
<span>系统重启</span>
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<div className="fleet-card-metadata">
|
||||
<div className="fleet-card-origin-row">
|
||||
<Icon name="globe" />
|
||||
{(() => {
|
||||
const origin = canonicalHttpOrigin(row.instance.url);
|
||||
return origin ? (
|
||||
{model.rows.map((row) => {
|
||||
const phoneNumbers = row.status?.summary?.resources?.phoneNumbers ?? [];
|
||||
const latestMessage = messageStates.get(row.id)?.latest;
|
||||
const phoneNumbersRevealed = revealedPhoneIds.has(row.id);
|
||||
const hasPrivatePhone = phoneNumbers.length > 0 || Boolean(latestMessage);
|
||||
return (
|
||||
<Card
|
||||
role="article"
|
||||
className={`fleet-card${row.selected ? ' is-selected' : ''}`}
|
||||
data-status={row.statusKind}
|
||||
aria-label={`${row.displayName} 实例概览`}
|
||||
key={row.id}
|
||||
>
|
||||
<div className="fleet-card-glow" aria-hidden="true" />
|
||||
<header>
|
||||
<div className="fleet-card-title-group">
|
||||
<span className="fleet-node-avatar" aria-hidden="true">
|
||||
{initials(row.displayName)}
|
||||
</span>
|
||||
<div>
|
||||
<a
|
||||
className="fleet-card-origin fleet-card-origin-link"
|
||||
href={origin}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label={`打开 ${row.displayName} 节点入口`}
|
||||
title={`在新窗口打开 ${origin}`}
|
||||
className="fleet-card-entry"
|
||||
href={`/instances/${encodeURIComponent(row.id)}/overview`}
|
||||
aria-label={`打开 ${row.displayName} 实例仪表盘`}
|
||||
>
|
||||
<span>{origin}</span>
|
||||
<Icon className="fleet-origin-arrow" name="arrow-up-right" />
|
||||
<h2>{row.displayName}</h2>
|
||||
<span className="fleet-card-version">
|
||||
{row.version ? `SimAdmin ${row.version}` : '版本未知'}
|
||||
</span>
|
||||
<span className="fleet-card-entry-label">
|
||||
进入仪表盘 <Icon name="chevron-right" />
|
||||
</span>
|
||||
</a>
|
||||
) : (
|
||||
<span
|
||||
className="fleet-card-origin fleet-card-origin-invalid"
|
||||
aria-label={`${row.displayName} 源地址无效`}
|
||||
</div>
|
||||
</div>
|
||||
<div className="fleet-card-header-meta">
|
||||
<Tag
|
||||
className={`status status-${row.statusKind}`}
|
||||
color={STATUS_COLORS[row.statusKind]}
|
||||
variant="soft"
|
||||
size="small"
|
||||
>
|
||||
<span className="status-dot" aria-hidden="true" />
|
||||
{STATUS_LABELS[row.statusKind]}
|
||||
</Tag>
|
||||
{selectionMode ? (
|
||||
<label className="fleet-card-select touch-target">
|
||||
<input
|
||||
type="checkbox"
|
||||
aria-label={`选择 ${row.displayName}`}
|
||||
checked={row.selected}
|
||||
onChange={(event) =>
|
||||
toggleOne(row.id, event.currentTarget.checked)
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
) : null}
|
||||
<div className="fleet-card-menu">
|
||||
<button
|
||||
ref={(element) => {
|
||||
if (element) cardMenuTriggerRefs.current.set(row.id, element);
|
||||
else cardMenuTriggerRefs.current.delete(row.id);
|
||||
}}
|
||||
type="button"
|
||||
className="fleet-card-menu-trigger"
|
||||
aria-label={`实例操作 ${row.displayName}`}
|
||||
aria-haspopup="menu"
|
||||
aria-expanded={cardMenuId === row.id}
|
||||
aria-controls={`fleet-card-menu-${row.id}`}
|
||||
title="实例操作"
|
||||
onClick={() => {
|
||||
cardMenuInitialFocus.current = 'first';
|
||||
setCardMenuId((current) =>
|
||||
current === row.id ? undefined : row.id,
|
||||
);
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key !== 'ArrowDown' && event.key !== 'ArrowUp') return;
|
||||
event.preventDefault();
|
||||
cardMenuInitialFocus.current =
|
||||
event.key === 'ArrowUp' ? 'last' : 'first';
|
||||
setCardMenuId(row.id);
|
||||
}}
|
||||
>
|
||||
源地址无效
|
||||
</span>
|
||||
);
|
||||
})()}
|
||||
<Icon name="more" />
|
||||
</button>
|
||||
{cardMenuId === row.id ? (
|
||||
<div
|
||||
ref={cardMenuPanelRef}
|
||||
id={`fleet-card-menu-${row.id}`}
|
||||
className="fleet-card-menu-panel"
|
||||
role="menu"
|
||||
aria-label={`${row.displayName} 操作`}
|
||||
onKeyDown={(event) => moveCardMenuFocus(event, row.id)}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
role="menuitem"
|
||||
tabIndex={-1}
|
||||
aria-label={`重启服务 ${row.displayName}`}
|
||||
disabled={cardAction?.id === row.id && cardAction.busy}
|
||||
onClick={() => {
|
||||
closeCardMenu(row.id, true);
|
||||
void runCardAction(row.id, 'service-restart');
|
||||
}}
|
||||
>
|
||||
<Icon name="restart" />
|
||||
<span>重启服务</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
role="menuitem"
|
||||
tabIndex={-1}
|
||||
className="fleet-card-menu-danger"
|
||||
aria-label={`系统重启 ${row.displayName}`}
|
||||
disabled={cardAction?.id === row.id && cardAction.busy}
|
||||
onClick={() => {
|
||||
closeCardMenu(row.id, true);
|
||||
void runCardAction(row.id, 'system-reboot');
|
||||
}}
|
||||
>
|
||||
<Icon name="alert" />
|
||||
<span>系统重启</span>
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<div className="fleet-card-metadata">
|
||||
<div className="fleet-card-origin-row">
|
||||
<Icon name="globe" />
|
||||
{(() => {
|
||||
const origin = canonicalHttpOrigin(row.instance.url);
|
||||
return origin ? (
|
||||
<a
|
||||
className="fleet-card-origin fleet-card-origin-link"
|
||||
href={origin}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label={`打开 ${row.displayName} 节点入口`}
|
||||
title={`在新窗口打开 ${origin}`}
|
||||
>
|
||||
<span>{origin}</span>
|
||||
<Icon className="fleet-origin-arrow" name="arrow-up-right" />
|
||||
</a>
|
||||
) : (
|
||||
<span
|
||||
className="fleet-card-origin fleet-card-origin-invalid"
|
||||
aria-label={`${row.displayName} 源地址无效`}
|
||||
>
|
||||
源地址无效
|
||||
</span>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
{row.instance.description ? (
|
||||
<p className="fleet-card-description">{row.instance.description}</p>
|
||||
) : null}
|
||||
{row.anomalies.length > 0 ? (
|
||||
<p
|
||||
className="fleet-card-anomalies"
|
||||
aria-label={`${row.displayName} 异常`}
|
||||
>
|
||||
{row.anomalies.join(', ')}
|
||||
</p>
|
||||
) : null}
|
||||
{row.tags.length > 0 ? (
|
||||
<div className="fleet-instance-tags" aria-label="实例标签">
|
||||
{row.tags.map((item) => (
|
||||
<Tag key={item} size="small" color="app-yellow" variant="soft">
|
||||
<Icon name="tag" />
|
||||
{item}
|
||||
</Tag>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
{row.capabilities.length > 0 ? (
|
||||
<div className="capability-tags" aria-label="能力">
|
||||
{row.capabilities.map((item) => (
|
||||
<Tag key={item} size="small" color="app-teal" variant="soft">
|
||||
<Icon name={CAPABILITY_ICONS[item] ?? 'grid'} />
|
||||
{capabilityLabel(item)}
|
||||
</Tag>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
{row.instance.description ? (
|
||||
<p className="fleet-card-description">{row.instance.description}</p>
|
||||
) : null}
|
||||
{row.anomalies.length > 0 ? (
|
||||
<dl className="fleet-card-hardware" role="group" aria-label="节点硬件信息">
|
||||
<div className="fleet-card-phone">
|
||||
<dt>
|
||||
<Icon name="phone" />
|
||||
手机号
|
||||
</dt>
|
||||
<dd>
|
||||
<span className="fleet-card-phone-value">
|
||||
{phoneNumbers.length > 0
|
||||
? formatPhoneNumbers(phoneNumbers, phoneNumbersRevealed)
|
||||
: '暂未获取'}
|
||||
</span>
|
||||
{hasPrivatePhone ? (
|
||||
<button
|
||||
type="button"
|
||||
className="fleet-phone-privacy-toggle"
|
||||
aria-label={`${phoneNumbersRevealed ? '隐藏' : '显示'} ${row.displayName} 手机号`}
|
||||
aria-pressed={phoneNumbersRevealed}
|
||||
title={phoneNumbersRevealed ? '隐藏手机号' : '显示手机号'}
|
||||
onClick={() => togglePhonePrivacy(row.id)}
|
||||
>
|
||||
<Icon name={phoneNumbersRevealed ? 'eye-off' : 'eye'} />
|
||||
</button>
|
||||
) : null}
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>
|
||||
<Icon name="temperature" />
|
||||
最高温度
|
||||
</dt>
|
||||
<dd>
|
||||
{temperature(row.status?.summary?.resources?.maxTemperatureCelsius)}
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<section className="fleet-card-telemetry" aria-label="资源遥测">
|
||||
<div className="fleet-resource-row">
|
||||
<span className="fleet-resource-label">
|
||||
<Icon name="cpu" />
|
||||
CPU
|
||||
</span>
|
||||
{row.status?.summary?.resources?.cpuPercent === undefined ? (
|
||||
<span className="fleet-resource-unavailable">--</span>
|
||||
) : (
|
||||
<div className="fleet-resource-meter">
|
||||
<Progress
|
||||
percent={row.status.summary.resources.cpuPercent}
|
||||
size="small"
|
||||
showInfo={false}
|
||||
duration={0}
|
||||
aria-label="CPU 使用率"
|
||||
/>
|
||||
<strong>{percent(row.status.summary.resources.cpuPercent)}</strong>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="fleet-resource-row">
|
||||
<span className="fleet-resource-label">
|
||||
<Icon name="memory" />
|
||||
内存
|
||||
</span>
|
||||
{row.status?.summary?.resources?.memoryPercent === undefined ? (
|
||||
<span className="fleet-resource-unavailable">--</span>
|
||||
) : (
|
||||
<div className="fleet-resource-meter">
|
||||
<Progress
|
||||
percent={row.status.summary.resources.memoryPercent}
|
||||
size="small"
|
||||
showInfo={false}
|
||||
duration={0}
|
||||
aria-label="内存使用率"
|
||||
/>
|
||||
<strong>
|
||||
{percent(row.status.summary.resources.memoryPercent)}
|
||||
</strong>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
<section className="fleet-card-footer" aria-label="短信状态">
|
||||
{messageStates.get(row.id)?.unavailable ? (
|
||||
<>
|
||||
<span className="fleet-card-footer-primary">
|
||||
<Icon name="message" />
|
||||
短信暂不可用
|
||||
</span>
|
||||
<span className="fleet-card-footer-time">--</span>
|
||||
</>
|
||||
) : messageStates.get(row.id)?.latest ? (
|
||||
<>
|
||||
<span className="fleet-card-footer-primary">
|
||||
<Icon name="message" />
|
||||
<span>
|
||||
{messageDirection(messageStates.get(row.id)!.latest!.direction)}
|
||||
</span>
|
||||
<span aria-hidden="true">·</span>
|
||||
<span>
|
||||
{formatPhoneNumbers(
|
||||
[messageStates.get(row.id)!.latest!.phoneNumber],
|
||||
phoneNumbersRevealed,
|
||||
)}
|
||||
</span>
|
||||
</span>
|
||||
<time
|
||||
className="fleet-card-footer-time"
|
||||
dateTime={messageStates.get(row.id)!.latest!.timestamp}
|
||||
>
|
||||
{messageTime(messageStates.get(row.id)!.latest!.timestamp)}
|
||||
</time>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span className="fleet-card-footer-primary">
|
||||
<Icon name="message" />
|
||||
暂无短信
|
||||
</span>
|
||||
<span className="fleet-card-footer-time">未收到</span>
|
||||
</>
|
||||
)}
|
||||
</section>
|
||||
{cardAction?.id === row.id ? (
|
||||
<p
|
||||
className="fleet-card-anomalies"
|
||||
aria-label={`${row.displayName} 异常`}
|
||||
role={cardAction.error ? 'alert' : 'status'}
|
||||
className={
|
||||
cardAction.error ? 'card-operation-error' : 'card-operation-success'
|
||||
}
|
||||
>
|
||||
{row.anomalies.join(', ')}
|
||||
{cardAction.error ?? cardAction.message}
|
||||
</p>
|
||||
) : null}
|
||||
{row.tags.length > 0 ? (
|
||||
<div className="fleet-instance-tags" aria-label="实例标签">
|
||||
{row.tags.map((item) => (
|
||||
<Tag key={item} size="small" color="app-yellow" variant="soft">
|
||||
<Icon name="tag" />
|
||||
{item}
|
||||
</Tag>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
{row.capabilities.length > 0 ? (
|
||||
<div className="capability-tags" aria-label="能力">
|
||||
{row.capabilities.map((item) => (
|
||||
<Tag key={item} size="small" color="app-teal" variant="soft">
|
||||
<Icon name={CAPABILITY_ICONS[item] ?? 'grid'} />
|
||||
{capabilityLabel(item)}
|
||||
</Tag>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<dl className="fleet-card-hardware" role="group" aria-label="节点硬件信息">
|
||||
<div>
|
||||
<dt>
|
||||
<Icon name="phone" />
|
||||
手机号
|
||||
</dt>
|
||||
<dd>
|
||||
{row.status?.summary?.resources?.phoneNumbers?.join('、') || '暂未获取'}
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>
|
||||
<Icon name="temperature" />
|
||||
最高温度
|
||||
</dt>
|
||||
<dd>
|
||||
{temperature(row.status?.summary?.resources?.maxTemperatureCelsius)}
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<section className="fleet-card-telemetry" aria-label="资源遥测">
|
||||
<div className="fleet-resource-row">
|
||||
<span className="fleet-resource-label">
|
||||
<Icon name="cpu" />
|
||||
CPU
|
||||
</span>
|
||||
{row.status?.summary?.resources?.cpuPercent === undefined ? (
|
||||
<span className="fleet-resource-unavailable">--</span>
|
||||
) : (
|
||||
<div className="fleet-resource-meter">
|
||||
<Progress
|
||||
percent={row.status.summary.resources.cpuPercent}
|
||||
size="small"
|
||||
showInfo={false}
|
||||
duration={0}
|
||||
aria-label="CPU 使用率"
|
||||
/>
|
||||
<strong>{percent(row.status.summary.resources.cpuPercent)}</strong>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="fleet-resource-row">
|
||||
<span className="fleet-resource-label">
|
||||
<Icon name="memory" />
|
||||
内存
|
||||
</span>
|
||||
{row.status?.summary?.resources?.memoryPercent === undefined ? (
|
||||
<span className="fleet-resource-unavailable">--</span>
|
||||
) : (
|
||||
<div className="fleet-resource-meter">
|
||||
<Progress
|
||||
percent={row.status.summary.resources.memoryPercent}
|
||||
size="small"
|
||||
showInfo={false}
|
||||
duration={0}
|
||||
aria-label="内存使用率"
|
||||
/>
|
||||
<strong>{percent(row.status.summary.resources.memoryPercent)}</strong>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
<section className="fleet-card-footer" aria-label="短信状态">
|
||||
{messageStates.get(row.id)?.unavailable ? (
|
||||
<>
|
||||
<span className="fleet-card-footer-primary">
|
||||
<Icon name="message" />
|
||||
短信暂不可用
|
||||
</span>
|
||||
<span className="fleet-card-footer-time">--</span>
|
||||
</>
|
||||
) : messageStates.get(row.id)?.latest ? (
|
||||
<>
|
||||
<span className="fleet-card-footer-primary">
|
||||
<Icon name="message" />
|
||||
<span>
|
||||
{messageDirection(messageStates.get(row.id)!.latest!.direction)}
|
||||
</span>
|
||||
<span aria-hidden="true">·</span>
|
||||
<span>{messageStates.get(row.id)!.latest!.phoneNumber}</span>
|
||||
</span>
|
||||
<time
|
||||
className="fleet-card-footer-time"
|
||||
dateTime={messageStates.get(row.id)!.latest!.timestamp}
|
||||
>
|
||||
{messageTime(messageStates.get(row.id)!.latest!.timestamp)}
|
||||
</time>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span className="fleet-card-footer-primary">
|
||||
<Icon name="message" />
|
||||
暂无短信
|
||||
</span>
|
||||
<span className="fleet-card-footer-time">未收到</span>
|
||||
</>
|
||||
)}
|
||||
</section>
|
||||
{cardAction?.id === row.id ? (
|
||||
<p
|
||||
role={cardAction.error ? 'alert' : 'status'}
|
||||
className={
|
||||
cardAction.error ? 'card-operation-error' : 'card-operation-success'
|
||||
}
|
||||
>
|
||||
{cardAction.error ?? cardAction.message}
|
||||
</p>
|
||||
) : null}
|
||||
</Card>
|
||||
))}
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
<nav className="pagination" aria-label="实例分页">
|
||||
|
||||
Reference in New Issue
Block a user