feat(web): prioritize SMS operations workspace

This commit is contained in:
chick
2026-07-19 10:30:52 +08:00
parent 0112e3320a
commit f7d69a0fa7
10 changed files with 532 additions and 68 deletions
+76 -9
View File
@@ -10,6 +10,11 @@ import {
type SortDirection,
} from './fleet-table-view-model.js';
import type { InstanceDataSource } from '../instances/instance-crud.js';
import type {
FleetMessageLoadState,
FleetMessagesDataSource,
} from './fleet-messages-api-data-source.js';
import { loadFleetMessageSummaries } from './fleet-messages-api-data-source.js';
export interface FleetSnapshot {
readonly instances: readonly FleetInstance[];
@@ -20,10 +25,12 @@ export interface FleetDataSource {
}
export interface FleetPageProps {
readonly dataSource?: FleetDataSource;
readonly messagesDataSource?: FleetMessagesDataSource;
readonly instanceDataSource?: InstanceDataSource;
readonly initialData?: FleetSnapshot;
readonly refreshSignal?: number;
}
type FleetMessageState = FleetMessageLoadState;
const EMPTY_SNAPSHOT: FleetSnapshot = { instances: [], statuses: new Map() };
const STATUS_LABELS: Readonly<Record<string, string>> = {
@@ -69,6 +76,18 @@ const percent = (value: number | undefined): string =>
value === undefined ? '暂未获取' : `${value.toFixed(1)}%`;
const temperature = (value: number | undefined): string =>
value === undefined ? '暂未获取' : `${value.toFixed(1)} °C`;
const messageDirection = (value: string): string =>
value === 'incoming' || value === 'received'
? '收到'
: value === 'outgoing' || value === 'sent'
? '发送'
: '未知方向';
const messageExcerpt = (value: string): string =>
value.length > 36 ? `${value.slice(0, 36)}` : value;
const messageTime = (value: string): string => {
const date = new Date(value);
return Number.isNaN(date.getTime()) ? value : date.toLocaleString('zh-CN');
};
export function canonicalHttpOrigin(value: string): string | null {
try {
@@ -84,6 +103,7 @@ export function canonicalHttpOrigin(value: string): string | null {
export function FleetPage({
dataSource,
messagesDataSource,
instanceDataSource,
initialData,
refreshSignal = 0,
@@ -114,6 +134,10 @@ export function FleetPage({
const [deleteStatus, setDeleteStatus] =
useState<Readonly<{ id: string; kind: 'success' | 'error'; message: string }>>();
const selectAllRef = useRef<HTMLInputElement>(null);
const messagesOwner = useRef(0);
const [messageStates, setMessageStates] = useState<ReadonlyMap<string, FleetMessageState>>(
new Map(),
);
useEffect(() => {
if (initialData && refreshSignal === 0) {
@@ -141,6 +165,23 @@ export function FleetPage({
};
}, [attempt, dataSource, initialData, refreshSignal]);
useEffect(() => {
const request = ++messagesOwner.current;
const controller = new AbortController();
const instances = snapshot?.instances;
setMessageStates(new Map<string, FleetMessageState>());
if (!messagesDataSource || !instances) return () => controller.abort();
void loadFleetMessageSummaries(
messagesDataSource,
instances.map((instance) => instance.id),
controller.signal,
).then((entries) => {
if (request === messagesOwner.current && !controller.signal.aborted)
setMessageStates(entries);
});
return () => controller.abort();
}, [messagesDataSource, snapshot]);
const model = useMemo(
() =>
buildFleetTableViewModel(snapshot?.instances ?? [], snapshot?.statuses ?? new Map(), {
@@ -445,21 +486,47 @@ export function FleetPage({
<dd>{temperature(row.status?.summary?.resources?.maxTemperatureCelsius)}</dd>
</div>
</dl>
<div className="capability-tags" aria-label="能力">
{row.capabilities.length > 0 ? (
row.capabilities.map((item) => (
{row.capabilities.length > 0 ? (
<div className="capability-tags" aria-label="能力">
{row.capabilities.map((item) => (
<span key={item}>{capabilityLabel(item)}</span>
))
))}
</div>
) : null}
<section className="fleet-card-sms" aria-label="最近短信">
<h3></h3>
{messageStates.get(row.id)?.unavailable ? (
<p className="fleet-card-sms-empty"></p>
) : messageStates.get(row.id)?.latest ? (
<dl>
<div>
<dt></dt>
<dd>{messageDirection(messageStates.get(row.id)!.latest!.direction)}</dd>
</div>
<div>
<dt></dt>
<dd>{messageStates.get(row.id)!.latest!.phoneNumber}</dd>
</div>
<div>
<dt></dt>
<dd>{messageExcerpt(messageStates.get(row.id)!.latest!.content)}</dd>
</div>
<div>
<dt></dt>
<dd>{messageTime(messageStates.get(row.id)!.latest!.timestamp)}</dd>
</div>
</dl>
) : (
<span></span>
<p className="fleet-card-sms-empty"></p>
)}
</div>
</section>
<div className="fleet-card-actions">
<a
href={`/instances/${encodeURIComponent(row.id)}/overview`}
aria-label={`管理 ${row.displayName}`}
className="primary-action"
href={`/instances/${encodeURIComponent(row.id)}/messages`}
aria-label={`查看 ${row.displayName} 的短信`}
>
</a>
<a
href={`/settings/instances/${encodeURIComponent(row.id)}`}