import type { CandidateBreakdown, TransferRecord } from "../../store"; import { t } from "../../i18n"; import { formatBytes } from "../../utils/format"; import s from "./TransferDetails.module.css"; export interface TransferDetailsProps { record: TransferRecord; } // 二级详情面板。常态下被折叠在 TransferRow 末端「详情」之后,仅工程视角访问。 // 排版克制:mono 字号、文本对齐为主,不做花哨可视化。 export function TransferDetails(props: TransferDetailsProps) { const { record: tr } = props; const st = tr.iceStats; return (
{/* —— 顶部一行 chip:模式 + 状态 + 文件大小 —— */}
{tr.mode && ( {tr.mode === "p2p" ? t("transfer.mode.p2p") : t("transfer.mode.relay")} )}
{/* —— ICE 状态 —— */} {st && ( <>
{t("transfer.debug.iceGathering")} {st.gathering} {t("transfer.debug.iceConnection")} {st.connection}
{/* —— 候选明细表格 —— */}
{/* —— 选中候选对 —— */}
pair {st.selectedPair ? ( <> {st.selectedPair.local} {st.selectedPair.remote} ) : ( {t("transfer.debug.noPair")} )}
{/* —— DataChannel —— */} {st.dataChannel && (
readyState {st.dataChannel.readyState} buffered {formatBytes(st.dataChannel.bufferedAmount)}
)} )} {/* —— phase 当前 —— */} {tr.phase && (
phase {tr.phase} {tr.lastProgressAt && ( <> last tick {isoSeconds(Math.floor(tr.lastProgressAt / 1000))} )}
)}
); } function Section(props: { title: string; children: React.ReactNode }) { return (
{props.title}
{props.children}
); } function KV(props: { k: string; v: string | number }) { return ( {props.k} {props.v} ); } interface CandidateTableProps { local: CandidateBreakdown; remote: CandidateBreakdown; } function CandidateTable(props: CandidateTableProps) { const { local, remote } = props; const cols = [ "host", "mdns", "srflx", "prflx", "relay" ] as const; return (
{cols.map((c) => {c})} 本端 {cols.map((c) => ( {local[c]} ))} 对端 {cols.map((c) => ( {remote[c]} ))}
); } // 把 unix 秒转 "HH:MM:SS" 本地时间,更适合诊断面板对照浏览器日志时间戳。 function isoSeconds(secs: number): string { const d = new Date(secs * 1000); if (Number.isNaN(d.getTime())) { return String(secs); } return d.toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit", second: "2-digit" }); }