Files
multi-simadmin/apps/web/src/instances/messages-module.tsx
T

178 lines
6.0 KiB
TypeScript

import { useEffect, useRef, useState } from 'react';
import type { InstanceContext } from '../app-shell.js';
export interface SmsMessage {
readonly id: string;
readonly direction: string;
readonly phoneNumber: string;
readonly content: string;
readonly timestamp: string;
readonly status: string;
}
export interface MessagesSnapshot {
readonly messages: readonly SmsMessage[];
}
export interface SendMessageInput {
readonly phoneNumber: string;
readonly content: string;
}
export interface MessagesDataSource {
load(instanceId: string, signal: AbortSignal): Promise<MessagesSnapshot>;
send(instanceId: string, input: SendMessageInput): Promise<void>;
}
export interface MessagesModuleProps {
readonly instance: InstanceContext;
readonly dataSource?: MessagesDataSource;
readonly refreshSignal?: unknown;
}
const directionLabel = (value: string): string =>
value === 'incoming' || value === 'received'
? '收到'
: value === 'outgoing' || value === 'sent'
? '发送'
: '未知';
export function MessagesModule({ instance, dataSource, refreshSignal }: MessagesModuleProps) {
const owner = useRef(0);
const [messages, setMessages] = useState<readonly SmsMessage[]>();
const [loadError, setLoadError] = useState(false);
const [retry, setRetry] = useState(0);
const [phoneNumber, setPhoneNumber] = useState('');
const [content, setContent] = useState('');
const [pendingSend, setPendingSend] = useState<SendMessageInput>();
const [sending, setSending] = useState(false);
const [sendState, setSendState] = useState<'success' | 'error'>();
useEffect(() => {
const request = ++owner.current;
const controller = new AbortController();
setMessages(undefined);
setLoadError(false);
if (!dataSource) return () => controller.abort();
void dataSource.load(instance.id, controller.signal).then(
(snapshot) => {
if (request === owner.current && !controller.signal.aborted) setMessages(snapshot.messages);
},
() => {
if (request === owner.current && !controller.signal.aborted) setLoadError(true);
},
);
return () => controller.abort();
}, [dataSource, instance.id, refreshSignal, retry]);
async function send(input: SendMessageInput): Promise<void> {
if (!dataSource || sending) return;
setSending(true);
setSendState(undefined);
try {
await dataSource.send(instance.id, input);
setContent('');
setPendingSend(undefined);
setSendState('success');
setRetry((value) => value + 1);
} catch {
setSendState('error');
} finally {
setSending(false);
}
}
if (!dataSource)
return (
<div className="state-panel" role="status">
短信功能暂不可用。
</div>
);
return (
<div className="messages-module">
<section className="messages-card" aria-labelledby="send-sms-title">
<h2 id="send-sms-title">发送短信</h2>
<p>发送会立即提交到当前设备,不会自动重试。请确认号码和内容。</p>
<form
onSubmit={(event) => {
event.preventDefault();
setPendingSend({ phoneNumber: phoneNumber.trim(), content: content.trim() });
}}
>
<label>
手机号
<input
name="phoneNumber"
value={phoneNumber}
onChange={(event) => setPhoneNumber(event.target.value)}
minLength={3}
maxLength={32}
required
/>
</label>
<label>
短信内容
<textarea
name="content"
value={content}
onChange={(event) => setContent(event.target.value)}
maxLength={1600}
required
/>
</label>
<button type="submit" disabled={sending || !phoneNumber.trim() || !content.trim()}>
{sending ? '正在发送…' : '发送短信'}
</button>
</form>
{sendState === 'success' ? <p role="status">短信已提交发送。</p> : null}
{sendState === 'error' ? <p role="alert">短信发送失败,请核对设备状态后重试。</p> : null}
{pendingSend ? (
<section className="send-confirmation" role="region" aria-label="确认发送短信">
<h3>发送前确认</h3>
<p>号码:{pendingSend.phoneNumber}</p>
<p>内容:{pendingSend.content}</p>
<p>确认后会立即提交到当前设备。</p>
<div className="fleet-card-actions">
<button type="button" disabled={sending} onClick={() => void send(pendingSend)}>
{sending ? '正在发送…' : '确认并发送'}
</button>
<button type="button" disabled={sending} onClick={() => setPendingSend(undefined)}>
返回修改
</button>
</div>
</section>
) : null}
</section>
<section className="messages-card" aria-labelledby="sms-list-title">
<h2 id="sms-list-title">短信列表</h2>
{loadError ? (
<div role="alert">
<p>无法加载短信列表。</p>
<button type="button" onClick={() => setRetry((value) => value + 1)}>
重试加载
</button>
</div>
) : messages === undefined ? (
<p role="status">正在加载短信…</p>
) : messages.length === 0 ? (
<p>暂无短信。</p>
) : (
<ol className="sms-list">
{messages.map((message) => (
<li key={message.id} className="sms-message-card">
<header>
<strong>
{directionLabel(message.direction)} · {message.phoneNumber}
</strong>
<time>{message.timestamp}</time>
</header>
<p>{message.content}</p>
<small>状态:{message.status}</small>
</li>
))}
</ol>
)}
</section>
</div>
);
}