import { createEffect, createMemo, createSignal, For, onCleanup, onMount, Show, } from "solid-js"; import { accountStore } from "~/store/account"; import { settingsStore } from "~/store/settings"; type LogRow = { id: string; accountId: string; accountName: string; host: string; seq: number; timestamp: string; timestampValue: number; content: string; }; const extractTimestamp = (message: string) => { const match = message.match(/^\[(\d{2}:\d{2}:\d{2})\]\s*/); return match?.[1] ?? null; }; const stripTimestamp = (message: string) => { return message.replace(/^\[(\d{2}:\d{2}:\d{2})\]\s*/, ""); }; const parseTimestampValue = (timestamp: string | null) => { if (!timestamp) { return -1; } const [hours, minutes, seconds] = timestamp.split(":").map(Number); if ([hours, minutes, seconds].some((item) => Number.isNaN(item))) { return -1; } return hours * 3600 + minutes * 60 + seconds; }; const normalizeContent = (message: string) => { const value = stripTimestamp(message).replace(/\s+/g, " ").trim(); return value || "-"; }; const Logs = () => { const [accountState, setAccountState] = createSignal(accountStore.getState()); const [settingsState, setSettingsState] = createSignal( settingsStore.getState(), ); let logContainerRef: HTMLDivElement | undefined; onMount(() => { const unsubscribeAccount = accountStore.subscribe((state) => { setAccountState(state); }); const unsubscribeSettings = settingsStore.subscribe((state) => { setSettingsState(state); }); onCleanup(() => { unsubscribeAccount(); unsubscribeSettings(); }); }); const accountInfoMap = createMemo(() => { return Object.fromEntries( accountState().accounts.map((account) => [ account.id, { name: account.user.name, host: account.host, }, ]), ) as Record; }); const logRows = createMemo(() => { const rows = Object.entries(accountState().studyLogsMap).flatMap( ([accountId, messages]) => { const accountInfo = accountInfoMap()[accountId]; return messages.map((message, index) => { const timestamp = extractTimestamp(message); return { id: `${accountId}-${index}`, accountId, accountName: accountInfo?.name ?? accountId, host: accountInfo?.host ?? "-", seq: index + 1, timestamp: timestamp ?? "--:--:--", timestampValue: parseTimestampValue(timestamp), content: normalizeContent(message), }; }); }, ); return rows.sort((left, right) => { const leftTs = left.timestampValue >= 0 ? left.timestampValue : Number.MAX_SAFE_INTEGER; const rightTs = right.timestampValue >= 0 ? right.timestampValue : Number.MAX_SAFE_INTEGER; if (leftTs !== rightTs) { return leftTs - rightTs; } if (left.accountName !== right.accountName) { return left.accountName.localeCompare(right.accountName, "zh-CN"); } if (left.seq !== right.seq) { return left.seq - right.seq; } return left.accountId.localeCompare(right.accountId); }); }); const totalAccountsWithLogs = createMemo(() => { return Object.values(accountState().studyLogsMap).filter( (messages) => messages.length > 0, ).length; }); const latestLog = createMemo(() => { const rows = logRows(); return rows.length > 0 ? rows[rows.length - 1] : null; }); createEffect(() => { logRows().length; if (!settingsState().autoScrollLogs) { return; } requestAnimationFrame(() => { const element = logContainerRef; if (element) { element.scrollTo({ top: element.scrollHeight, behavior: "smooth" }); } }); }); createEffect(() => { if (logContainerRef) { logContainerRef.style.fontSize = `${Math.max(settingsState().logFontSize, 12)}px`; } }); const clearAllLogs = () => { accountStore.getState().clearAllStudyLogs(); }; return (

Study Stream

学习日志

单行并列展示时间、账号、姓名与学习日志内容。

TOTAL LOGS {logRows().length}
ACTIVE ACCOUNTS {totalAccountsWithLogs()}
最新: {latestLog()?.timestamp} / {latestLog()?.accountName}
{settingsState().showLogTimestamps ? "显示时间" : "隐藏时间"} {settingsState().autoScrollLogs ? "自动滚动中" : "自动滚动关闭"}
0} fallback={
暂无日志输出
} >
时间 姓名 账号 主机 序号 内容
{(row) => (
{settingsState().showLogTimestamps ? row.timestamp : "--:--:--"} {row.accountName} {row.accountId} {row.host} #{row.seq} {row.content}
)}
); }; export default Logs;