mirror of
https://github.com/joeseesun/wechat-radar.git
synced 2026-09-08 03:18:31 +09:00
136 lines
4.6 KiB
TypeScript
136 lines
4.6 KiB
TypeScript
'use client';
|
|
|
|
import { useEffect, useRef, useState } from 'react';
|
|
import Sidebar from '@/components/Sidebar';
|
|
import { Activity, Pause, Play } from 'lucide-react';
|
|
|
|
type StreamMessage = {
|
|
local_id: number;
|
|
username: string;
|
|
chat_name: string;
|
|
sender: string;
|
|
content: string;
|
|
time: string;
|
|
timestamp: number;
|
|
type: string;
|
|
};
|
|
|
|
type StreamEvent =
|
|
| { type: 'open'; interval: number }
|
|
| { type: 'tick'; count: number; items: StreamMessage[]; ts: number }
|
|
| { type: 'error'; error: string };
|
|
|
|
export default function SignalsPage() {
|
|
const [items, setItems] = useState<StreamMessage[]>([]);
|
|
const [running, setRunning] = useState(true);
|
|
const [lastTick, setLastTick] = useState<number | null>(null);
|
|
const [err, setErr] = useState<string | null>(null);
|
|
const ctlRef = useRef<AbortController | null>(null);
|
|
|
|
useEffect(() => {
|
|
if (!running) {
|
|
ctlRef.current?.abort();
|
|
ctlRef.current = null;
|
|
return;
|
|
}
|
|
const ctl = new AbortController();
|
|
ctlRef.current = ctl;
|
|
(async () => {
|
|
try {
|
|
const r = await fetch('/api/new-messages', { signal: ctl.signal });
|
|
if (!r.body) return;
|
|
const reader = r.body.getReader();
|
|
const dec = new TextDecoder();
|
|
let buf = '';
|
|
while (true) {
|
|
const { value, done } = await reader.read();
|
|
if (done) break;
|
|
buf += dec.decode(value, { stream: true });
|
|
let nl;
|
|
while ((nl = buf.indexOf('\n\n')) !== -1) {
|
|
const chunk = buf.slice(0, nl).trim();
|
|
buf = buf.slice(nl + 2);
|
|
if (!chunk.startsWith('data:')) continue;
|
|
try {
|
|
const evt = JSON.parse(chunk.slice(5).trim()) as StreamEvent;
|
|
if (evt.type === 'tick') {
|
|
setLastTick(evt.ts);
|
|
setErr(null);
|
|
if (evt.items.length) {
|
|
setItems((prev) => [...evt.items, ...prev].slice(0, 200));
|
|
}
|
|
} else if (evt.type === 'error') {
|
|
setErr(evt.error);
|
|
}
|
|
} catch {}
|
|
}
|
|
}
|
|
} catch (e) {
|
|
if ((e as Error).name !== 'AbortError') setErr((e as Error).message);
|
|
}
|
|
})();
|
|
return () => ctl.abort();
|
|
}, [running]);
|
|
|
|
return (
|
|
<div className="flex h-screen">
|
|
<Sidebar />
|
|
<main className="flex flex-1 flex-col overflow-hidden">
|
|
<div className="flex items-center justify-between border-b border-[var(--border-soft)] bg-[var(--chrome-bg)] px-6 py-3 backdrop-blur">
|
|
<div>
|
|
<div className="report-kicker">Live Signals</div>
|
|
<div className="flex items-center gap-2 text-[15px] font-semibold">
|
|
<Activity size={16} className="text-[var(--accent)]" />
|
|
信号流 · 实时
|
|
</div>
|
|
<div className="mt-0.5 text-[11px] text-[var(--text-3)]">
|
|
{err
|
|
? `错误:${err}`
|
|
: lastTick
|
|
? `上次刷新:${new Date(lastTick).toLocaleTimeString()} · ${items.length} 条已收`
|
|
: '等待第一条消息…'}
|
|
</div>
|
|
</div>
|
|
<button
|
|
className={`btn ${running ? 'btn-warn' : 'btn-primary'}`}
|
|
onClick={() => setRunning((v) => !v)}
|
|
>
|
|
{running ? <Pause size={13} /> : <Play size={13} />}
|
|
<span>{running ? '暂停' : '继续'}</span>
|
|
</button>
|
|
</div>
|
|
|
|
<div className="flex-1 overflow-y-auto px-6 py-4">
|
|
{items.length === 0 ? (
|
|
<div className="py-20 text-center text-[12px] text-[var(--text-3)]">
|
|
等待新消息(每 5 秒拉取一次)…
|
|
</div>
|
|
) : (
|
|
<div className="space-y-2">
|
|
{items.map((m, i) => (
|
|
<Row key={`${m.username}-${m.local_id}-${i}`} m={m} />
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</main>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function Row({ m }: { m: StreamMessage }) {
|
|
return (
|
|
<div className="card grid grid-cols-[140px_1fr_120px] gap-3 px-4 py-3 text-[13px]">
|
|
<div className="truncate text-[var(--text-2)]">
|
|
<div className="truncate font-medium text-[var(--text)]">{m.chat_name}</div>
|
|
<div className="truncate text-[11px] text-[var(--text-3)]">{m.sender}</div>
|
|
</div>
|
|
<div className="min-w-0">
|
|
<div className="truncate text-[var(--text)]">{m.content}</div>
|
|
<div className="mt-0.5 text-[11px] text-[var(--text-3)]">类型:{m.type}</div>
|
|
</div>
|
|
<div className="text-right text-[11px] text-[var(--text-3)] tabular-nums">{m.time}</div>
|
|
</div>
|
|
);
|
|
}
|