feat(release): bump version to 0.1.2
## 详细信息 - 升级项目版本号到 0.1.2 - 增强刷课稳定性(失败重试、心跳检测、状态自动纠正) - 优化账号页与工作台紧凑布局,增加已学/未学区分与记录筛选 - 新增首次进入更新检查、更新日志弹窗与在线下载/回退 Release 跳转
This commit is contained in:
@@ -13,9 +13,11 @@ import AddAccountDialog, {
|
||||
import CourseWorkspace from "~/components/account/CourseWorkspace";
|
||||
import {
|
||||
createWkClient,
|
||||
hostApi,
|
||||
loginApi,
|
||||
runStudyQueue,
|
||||
type CourseKind,
|
||||
type RecordItem,
|
||||
type RecordType,
|
||||
} from "~/service/wk";
|
||||
import { setUnauthorizedHandler } from "~/service/http";
|
||||
@@ -47,13 +49,32 @@ const createDefaultForm = (host: string): LoginForm => ({
|
||||
const stripHtml = (value: string) => value.replace(/<[^>]+>/g, "").trim();
|
||||
|
||||
const parseDurationToSeconds = (value: string) => {
|
||||
const parts = value.split(":").map(Number);
|
||||
if (parts.length !== 3 || parts.some((part) => Number.isNaN(part))) {
|
||||
const input = value.trim();
|
||||
if (!input) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const [hours, minutes, seconds] = parts;
|
||||
return hours * 3600 + minutes * 60 + seconds;
|
||||
const numeric = Number(input);
|
||||
if (!Number.isNaN(numeric)) {
|
||||
return Math.max(0, Math.floor(numeric));
|
||||
}
|
||||
|
||||
const parts = input.split(":").map(Number);
|
||||
if (parts.some((part) => Number.isNaN(part))) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (parts.length === 3) {
|
||||
const [hours, minutes, seconds] = parts;
|
||||
return hours * 3600 + minutes * 60 + seconds;
|
||||
}
|
||||
|
||||
if (parts.length === 2) {
|
||||
const [minutes, seconds] = parts;
|
||||
return minutes * 60 + seconds;
|
||||
}
|
||||
|
||||
return 0;
|
||||
};
|
||||
|
||||
const createRecordCacheKey = (
|
||||
@@ -61,6 +82,8 @@ const createRecordCacheKey = (
|
||||
courseId: number,
|
||||
recordType: RecordType,
|
||||
) => `${accountId}::${courseId}::${recordType || "course"}`;
|
||||
const STUDY_HEARTBEAT_CHECK_INTERVAL_MS = 5000;
|
||||
const STUDY_HEARTBEAT_TIMEOUT_MS = 45000;
|
||||
|
||||
const Account = () => {
|
||||
const [storeState, setStoreState] = createSignal(accountStore.getState());
|
||||
@@ -80,8 +103,67 @@ const Account = () => {
|
||||
const [isRefreshingRecords, setIsRefreshingRecords] = createSignal(false);
|
||||
const [isRefreshingCourses, setIsRefreshingCourses] = createSignal(false);
|
||||
const [isRefreshingLogs, setIsRefreshingLogs] = createSignal(false);
|
||||
const [isLoadingRemoteHosts, setIsLoadingRemoteHosts] = createSignal(false);
|
||||
const [hasRestoredRecords, setHasRestoredRecords] = createSignal(false);
|
||||
const accountClients = new Map<string, ReturnType<typeof createWkClient>>();
|
||||
let recordRequestToken = 0;
|
||||
|
||||
const touchStudyHeartbeat = (accountId: string) => {
|
||||
accountStore.getState().touchStudyHeartbeat(accountId);
|
||||
};
|
||||
const clearStudyHeartbeat = (accountId: string) => {
|
||||
accountStore.getState().clearStudyHeartbeat(accountId);
|
||||
};
|
||||
const runStudyHeartbeatWatchdog = () => {
|
||||
const now = Date.now();
|
||||
const snapshot = accountStore.getState();
|
||||
|
||||
for (const [accountId, running] of Object.entries(snapshot.runningStudyMap)) {
|
||||
if (!running) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const lastHeartbeat = snapshot.studyHeartbeatMap[accountId] ?? 0;
|
||||
if (now - lastHeartbeat <= STUDY_HEARTBEAT_TIMEOUT_MS) {
|
||||
continue;
|
||||
}
|
||||
|
||||
snapshot.setAccountRunningStudy(accountId, false);
|
||||
snapshot.clearStudyHeartbeat(accountId);
|
||||
snapshot.appendStudyLog(
|
||||
accountId,
|
||||
`检测到刷课任务超时(超过 ${Math.floor(
|
||||
STUDY_HEARTBEAT_TIMEOUT_MS / 1000,
|
||||
)} 秒无活动),已自动重置状态`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const loadRemoteHostsIfNeeded = async () => {
|
||||
if (isLoadingRemoteHosts()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (settingsStore.getState().remoteHosts.length > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoadingRemoteHosts(true);
|
||||
|
||||
try {
|
||||
const res = await hostApi();
|
||||
settingsStore.getState().setRemoteHosts(
|
||||
res.data.list.map((item) => ({
|
||||
label: item.name,
|
||||
host: item.host,
|
||||
})),
|
||||
);
|
||||
} catch {
|
||||
// Ignore host bootstrap errors in account page to avoid blocking main flow.
|
||||
} finally {
|
||||
setIsLoadingRemoteHosts(false);
|
||||
}
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
const unsubscribeAccount = accountStore.subscribe((state) => {
|
||||
@@ -90,12 +172,19 @@ const Account = () => {
|
||||
const unsubscribeSettings = settingsStore.subscribe((state) => {
|
||||
setSettingsState(state);
|
||||
});
|
||||
void loadRemoteHostsIfNeeded();
|
||||
setUnauthorizedHandler(reloginBySession);
|
||||
runStudyHeartbeatWatchdog();
|
||||
const heartbeatTimer = window.setInterval(
|
||||
runStudyHeartbeatWatchdog,
|
||||
STUDY_HEARTBEAT_CHECK_INTERVAL_MS,
|
||||
);
|
||||
|
||||
onCleanup(() => {
|
||||
unsubscribeAccount();
|
||||
unsubscribeSettings();
|
||||
setUnauthorizedHandler(null);
|
||||
window.clearInterval(heartbeatTimer);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -189,6 +278,12 @@ const Account = () => {
|
||||
accountStore.getState().appendStudyLog(targetAccountId, message);
|
||||
};
|
||||
|
||||
const cancelPendingRecordRequest = () => {
|
||||
recordRequestToken += 1;
|
||||
setRecordsLoading(false);
|
||||
setIsRefreshingRecords(false);
|
||||
};
|
||||
|
||||
const getAccountClient = (accountId: string) => {
|
||||
const existingClient = accountClients.get(accountId);
|
||||
if (existingClient) {
|
||||
@@ -313,6 +408,7 @@ const Account = () => {
|
||||
};
|
||||
|
||||
const handleSelectAccount = (accountId: string) => {
|
||||
cancelPendingRecordRequest();
|
||||
accountStore.getState().setSelectedAccountId(accountId);
|
||||
accountStore.getState().setSelectedCourseId(null);
|
||||
accountStore.getState().setRecords([]);
|
||||
@@ -360,6 +456,7 @@ const Account = () => {
|
||||
setErrorMessage(message);
|
||||
accountStore.getState().setAccountCourses(accountId, []);
|
||||
if (accountStore.getState().selectedAccountId === accountId) {
|
||||
cancelPendingRecordRequest();
|
||||
accountStore.getState().setSelectedCourseId(null);
|
||||
accountStore.getState().setRecords([]);
|
||||
}
|
||||
@@ -437,36 +534,59 @@ const Account = () => {
|
||||
if (!account) {
|
||||
return;
|
||||
}
|
||||
const requestToken = ++recordRequestToken;
|
||||
const accountId = account.id;
|
||||
|
||||
setRecordsLoading(true);
|
||||
setIsRefreshingRecords(true);
|
||||
setRecordError("");
|
||||
|
||||
try {
|
||||
const res = await getAccountClient(account.id).recordApi({
|
||||
const res = await getAccountClient(accountId).recordApi({
|
||||
course_id: String(courseId),
|
||||
page: 0,
|
||||
record_type: nextRecordType,
|
||||
});
|
||||
accountStore.getState().setRecords(res.data.list);
|
||||
if (requestToken !== recordRequestToken) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rawList = (res as { data?: { list?: unknown } })?.data?.list;
|
||||
const list = (Array.isArray(rawList) ? rawList : []) as RecordItem[];
|
||||
const snapshot = accountStore.getState();
|
||||
if (
|
||||
snapshot.selectedAccountId !== accountId ||
|
||||
snapshot.selectedCourseId !== courseId ||
|
||||
snapshot.recordType !== nextRecordType
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
accountStore.getState().setRecords(list);
|
||||
accountStore
|
||||
.getState()
|
||||
.setRecordCache(
|
||||
createRecordCacheKey(account.id, courseId, nextRecordType),
|
||||
res.data.list,
|
||||
createRecordCacheKey(accountId, courseId, nextRecordType),
|
||||
list,
|
||||
);
|
||||
} catch (error) {
|
||||
if (requestToken !== recordRequestToken) {
|
||||
return;
|
||||
}
|
||||
const message =
|
||||
error instanceof Error ? error.message : "获取记录失败,请稍后重试。";
|
||||
setRecordError(message);
|
||||
accountStore.getState().setRecords([]);
|
||||
} finally {
|
||||
setRecordsLoading(false);
|
||||
setIsRefreshingRecords(false);
|
||||
if (requestToken === recordRequestToken) {
|
||||
setRecordsLoading(false);
|
||||
setIsRefreshingRecords(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelectCourse = (courseId: number) => {
|
||||
cancelPendingRecordRequest();
|
||||
accountStore.getState().setSelectedCourseId(courseId);
|
||||
};
|
||||
|
||||
@@ -507,18 +627,19 @@ const Account = () => {
|
||||
}
|
||||
|
||||
const queueItems = records()
|
||||
.filter((item) => item.progress !== "1.00")
|
||||
.map((item) => ({
|
||||
nodeId: item.id,
|
||||
name: item.name,
|
||||
currentTime: Number(item.duration || 0),
|
||||
currentTime: parseDurationToSeconds(item.duration),
|
||||
totalTime: parseDurationToSeconds(item.videoDuration),
|
||||
progress: item.progress,
|
||||
completed: stripHtml(item.state) === "已学",
|
||||
}));
|
||||
}))
|
||||
.filter((item) => item.progress !== "1.00" && !item.completed);
|
||||
|
||||
try {
|
||||
accountStore.getState().setAccountRunningStudy(account.id, true);
|
||||
touchStudyHeartbeat(account.id);
|
||||
appendStudyLog(`开始刷课:${course.name}`, account.id);
|
||||
await runStudyQueue({
|
||||
accountId: account.id,
|
||||
@@ -528,10 +649,14 @@ const Account = () => {
|
||||
client: getAccountClient(account.id),
|
||||
isRunningStudy: () =>
|
||||
!!accountStore.getState().runningStudyMap[account.id],
|
||||
setIsRunningStudy: () =>
|
||||
accountStore.getState().setAccountRunningStudy(account.id, false),
|
||||
onLog: (message: string, accoundID: string) =>
|
||||
appendStudyLog(message, accoundID),
|
||||
setIsRunningStudy: () => {
|
||||
accountStore.getState().setAccountRunningStudy(account.id, false);
|
||||
clearStudyHeartbeat(account.id);
|
||||
},
|
||||
onLog: (message: string, accoundID: string) => {
|
||||
touchStudyHeartbeat(accoundID);
|
||||
appendStudyLog(message, accoundID);
|
||||
},
|
||||
});
|
||||
if (accountStore.getState().runningStudyMap[account.id]) {
|
||||
appendStudyLog(`刷课完成:${course.name}`, account.id);
|
||||
@@ -543,6 +668,7 @@ const Account = () => {
|
||||
setRecordError(message);
|
||||
} finally {
|
||||
accountStore.getState().setAccountRunningStudy(account.id, false);
|
||||
clearStudyHeartbeat(account.id);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -553,12 +679,14 @@ const Account = () => {
|
||||
}
|
||||
|
||||
accountStore.getState().setAccountRunningStudy(account.id, false);
|
||||
clearStudyHeartbeat(account.id);
|
||||
appendStudyLog(`已发送停止刷课指令:${account.user.name}`, account.id);
|
||||
};
|
||||
|
||||
createEffect(
|
||||
on([selectedAccountId, courseKind], ([accountId, kind]) => {
|
||||
if (!accountId) {
|
||||
cancelPendingRecordRequest();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -570,6 +698,7 @@ const Account = () => {
|
||||
on(
|
||||
[selectedAccountId, selectedCourseId, recordType],
|
||||
([accountId, courseId, type]) => {
|
||||
cancelPendingRecordRequest();
|
||||
if (!accountId || !courseId) {
|
||||
accountStore.getState().setRecords([]);
|
||||
return;
|
||||
@@ -613,12 +742,12 @@ const Account = () => {
|
||||
|
||||
return (
|
||||
<div class="flex h-full min-h-0 flex-col overflow-hidden">
|
||||
<div class="flex shrink-0 items-center justify-between gap-3 rounded-[24px] border border-white/80 bg-[linear-gradient(135deg,rgba(236,254,255,0.95),rgba(255,255,255,0.92))] px-4 py-3 shadow-[0_18px_45px_-28px_rgba(8,145,178,0.35)]">
|
||||
<div class="flex shrink-0 items-center justify-between gap-2.5 rounded-[22px] border border-white/80 bg-[linear-gradient(135deg,rgba(236,254,255,0.95),rgba(255,255,255,0.92))] px-3.5 py-2.5 shadow-[0_16px_40px_-28px_rgba(8,145,178,0.35)]">
|
||||
<div>
|
||||
<p class="text-xs font-medium tracking-[0.26em] text-cyan-700/75 uppercase">
|
||||
Account Center
|
||||
</p>
|
||||
<h1 class="mt-1 text-xl font-semibold text-zinc-900 sm:text-2xl">
|
||||
<h1 class="mt-1 text-lg font-semibold text-zinc-900 sm:text-xl">
|
||||
账号管理
|
||||
</h1>
|
||||
<p class="mt-0.5 text-xs text-zinc-500 sm:text-sm">
|
||||
@@ -627,14 +756,14 @@ const Account = () => {
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="rounded-xl bg-[linear-gradient(135deg,#06b6d4,#14b8a6)] px-3.5 py-2.5 text-sm font-medium text-white shadow-lg shadow-cyan-500/20 transition hover:-translate-y-px hover:shadow-cyan-500/30"
|
||||
class="rounded-xl bg-[linear-gradient(135deg,#06b6d4,#14b8a6)] px-3 py-2 text-sm font-medium text-white shadow-lg shadow-cyan-500/20 transition hover:-translate-y-px hover:shadow-cyan-500/30"
|
||||
onClick={openDialog}
|
||||
>
|
||||
添加账号
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex min-h-0 flex-1 flex-col gap-4 xl:flex-row">
|
||||
<div class="mt-3 flex min-h-0 flex-1 flex-col gap-3 xl:flex-row">
|
||||
<AccountSidebar
|
||||
accounts={accounts()}
|
||||
selectedAccountId={selectedAccountId()}
|
||||
@@ -682,6 +811,7 @@ const Account = () => {
|
||||
accountStore.getState().setRecordType(value)
|
||||
}
|
||||
onChangeCourseRecordType={(value) => {
|
||||
cancelPendingRecordRequest();
|
||||
accountStore.getState().setCourseKind(value);
|
||||
accountStore.getState().setSelectedCourseId(null);
|
||||
accountStore.getState().setRecords([]);
|
||||
|
||||
@@ -10,6 +10,17 @@ import {
|
||||
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;
|
||||
@@ -25,7 +36,6 @@ const parseTimestampValue = (timestamp: string | null) => {
|
||||
}
|
||||
|
||||
const [hours, minutes, seconds] = timestamp.split(":").map(Number);
|
||||
|
||||
if ([hours, minutes, seconds].some((item) => Number.isNaN(item))) {
|
||||
return -1;
|
||||
}
|
||||
@@ -33,6 +43,11 @@ const parseTimestampValue = (timestamp: string | null) => {
|
||||
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(
|
||||
@@ -66,75 +81,60 @@ const Logs = () => {
|
||||
) as Record<string, { name: string; host: string }>;
|
||||
});
|
||||
|
||||
const allLogs = createMemo(() => {
|
||||
return Object.entries(accountState().studyLogsMap)
|
||||
.flatMap(([accountId, messages]) => {
|
||||
const logRows = createMemo<LogRow[]>(() => {
|
||||
const rows = Object.entries(accountState().studyLogsMap).flatMap(
|
||||
([accountId, messages]) => {
|
||||
const accountInfo = accountInfoMap()[accountId];
|
||||
const accountName = accountInfo?.name ?? accountId;
|
||||
|
||||
return messages.map((message, index) => {
|
||||
const timestamp = extractTimestamp(message);
|
||||
|
||||
return {
|
||||
id: `${accountId}-${index}`,
|
||||
accountId,
|
||||
accountName,
|
||||
index: index + 1,
|
||||
timestamp,
|
||||
accountName: accountInfo?.name ?? accountId,
|
||||
host: accountInfo?.host ?? "-",
|
||||
seq: index + 1,
|
||||
timestamp: timestamp ?? "--:--:--",
|
||||
timestampValue: parseTimestampValue(timestamp),
|
||||
content: stripTimestamp(message),
|
||||
raw: message,
|
||||
content: normalizeContent(message),
|
||||
};
|
||||
});
|
||||
})
|
||||
.sort((left, right) => {
|
||||
if (left.timestampValue !== right.timestampValue) {
|
||||
return left.timestampValue - right.timestampValue;
|
||||
}
|
||||
|
||||
if (left.index !== right.index) {
|
||||
return left.index - right.index;
|
||||
}
|
||||
|
||||
return left.accountId.localeCompare(right.accountId);
|
||||
});
|
||||
});
|
||||
|
||||
const accountSummaries = createMemo(() => {
|
||||
const accountIds = Array.from(
|
||||
new Set([
|
||||
...accountState().accounts.map((account) => account.id),
|
||||
...Object.keys(accountState().studyLogsMap),
|
||||
]),
|
||||
},
|
||||
);
|
||||
|
||||
return accountIds.map((accountId) => {
|
||||
const accountInfo = accountInfoMap()[accountId];
|
||||
const logs = accountState().studyLogsMap[accountId] ?? [];
|
||||
const latestMessage = logs[logs.length - 1] ?? "暂无日志";
|
||||
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;
|
||||
|
||||
return {
|
||||
id: accountId,
|
||||
name: accountInfo?.name ?? accountId,
|
||||
host: accountInfo?.host ?? "未知账号来源",
|
||||
total: logs.length,
|
||||
latestMessage: stripTimestamp(latestMessage),
|
||||
latestTime: extractTimestamp(latestMessage),
|
||||
};
|
||||
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 latestLog = createMemo(() => {
|
||||
const logs = allLogs();
|
||||
return logs.length > 0 ? logs[logs.length - 1] : null;
|
||||
const totalAccountsWithLogs = createMemo(() => {
|
||||
return Object.values(accountState().studyLogsMap).filter(
|
||||
(messages) => messages.length > 0,
|
||||
).length;
|
||||
});
|
||||
|
||||
const totalAccountsWithLogs = createMemo(() => {
|
||||
return accountSummaries().filter((item) => item.total > 0).length;
|
||||
const latestLog = createMemo(() => {
|
||||
const rows = logRows();
|
||||
return rows.length > 0 ? rows[rows.length - 1] : null;
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
allLogs().length;
|
||||
logRows().length;
|
||||
|
||||
if (!settingsState().autoScrollLogs) {
|
||||
return;
|
||||
@@ -170,7 +170,7 @@ const Logs = () => {
|
||||
日志中心
|
||||
</h1>
|
||||
<p class="mt-1 text-xs leading-5 text-zinc-600 sm:text-sm">
|
||||
聚合全部账号日志,优先把空间留给正文。
|
||||
单行并列展示时间、账号、姓名与日志内容。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -180,7 +180,7 @@ const Logs = () => {
|
||||
TOTAL LOGS
|
||||
</span>
|
||||
<span class="text-sm font-semibold text-zinc-950">
|
||||
{allLogs().length}
|
||||
{logRows().length}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 rounded-full border border-white/80 bg-white/85 px-3 py-1.5 shadow-sm">
|
||||
@@ -191,14 +191,11 @@ const Logs = () => {
|
||||
{totalAccountsWithLogs()}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 rounded-full border border-white/80 bg-white/85 px-3 py-1.5 shadow-sm">
|
||||
<span class="text-[10px] tracking-[0.18em] text-zinc-400 uppercase">
|
||||
AUTO SCROLL
|
||||
</span>
|
||||
<span class="text-sm font-medium text-zinc-900">
|
||||
{settingsState().autoScrollLogs ? "跟随最新" : "手动查看"}
|
||||
</span>
|
||||
</div>
|
||||
<Show when={latestLog()}>
|
||||
<div class="rounded-full border border-white/80 bg-white/85 px-3 py-1.5 text-xs text-zinc-600 shadow-sm">
|
||||
最新: {latestLog()?.timestamp} / {latestLog()?.accountName}
|
||||
</div>
|
||||
</Show>
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-full border border-rose-200 bg-[linear-gradient(135deg,rgba(255,241,242,0.95),rgba(255,255,255,0.9))] px-3.5 py-1.5 text-sm font-medium text-rose-600 transition hover:-translate-y-px hover:bg-rose-50"
|
||||
@@ -210,120 +207,73 @@ const Logs = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex min-h-0 flex-1 flex-col gap-3 overflow-y-auto px-3 py-3">
|
||||
<section class="flex min-h-0 flex-col overflow-hidden rounded-[26px] border border-white/80 bg-[linear-gradient(180deg,rgba(255,255,255,0.92),rgba(248,250,252,0.9))] shadow-[0_18px_48px_-30px_rgba(15,23,42,0.24)]">
|
||||
<div class="shrink-0 border-b border-zinc-200/80 px-4 py-3">
|
||||
<p class="text-base font-semibold text-zinc-950">账号概览</p>
|
||||
<p class="mt-1 text-xs text-zinc-500">最新输出与日志数量</p>
|
||||
<section class="mt-3 flex min-h-0 flex-1 flex-col overflow-hidden rounded-[26px] border border-zinc-200/80 bg-[linear-gradient(180deg,rgba(11,18,32,0.98),rgba(10,10,16,1))] shadow-[0_24px_60px_-32px_rgba(15,23,42,0.45)]">
|
||||
<div class="shrink-0 border-b border-white/10 px-4 py-3">
|
||||
<div class="flex flex-wrap items-center gap-2 text-xs">
|
||||
<span class="rounded-full border border-emerald-400/20 bg-emerald-400/10 px-3 py-1 text-emerald-200">
|
||||
{settingsState().showLogTimestamps ? "显示时间" : "隐藏时间"}
|
||||
</span>
|
||||
<span class="rounded-full border border-white/10 bg-white/5 px-3 py-1 text-slate-300">
|
||||
{settingsState().autoScrollLogs ? "自动滚动中" : "自动滚动关闭"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex min-h-0 shrink-0 flex-row gap-3 space-y-2.5 overflow-y-auto px-3 py-3">
|
||||
<For each={accountSummaries()}>
|
||||
{(account) => (
|
||||
<div class="rounded-2xl border border-zinc-200 bg-white p-4 transition hover:shadow-md">
|
||||
{/* 顶部 */}
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="min-w-0">
|
||||
<p class="truncate text-sm font-semibold text-zinc-900">
|
||||
{account.name}
|
||||
</p>
|
||||
<p class="text-xs text-zinc-400">{account.host}</p>
|
||||
</div>
|
||||
|
||||
<div class="rounded-full bg-amber-100 px-2.5 py-1 text-xs text-amber-700">
|
||||
{account.total}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 最新日志 */}
|
||||
<div class="mt-3 rounded-xl bg-zinc-50 p-3">
|
||||
<div class="flex justify-between text-[11px] text-zinc-400">
|
||||
<span>Latest</span>
|
||||
<span>{account.latestTime ?? "--:--"}</span>
|
||||
</div>
|
||||
|
||||
<p class="mt-1 line-clamp-2 text-xs text-zinc-600">
|
||||
{account.latestMessage}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="flex min-h-0 flex-1 flex-col overflow-hidden rounded-[26px] border border-zinc-200/80 bg-[linear-gradient(180deg,rgba(11,18,32,0.98),rgba(10,10,16,1))] shadow-[0_24px_60px_-32px_rgba(15,23,42,0.45)]">
|
||||
<div class="shrink-0 border-b border-white/10 px-4 py-3">
|
||||
<div class="flex flex-row items-center justify-between gap-2">
|
||||
<div>
|
||||
<p class="text-base font-semibold text-slate-400">实时日志流</p>
|
||||
<p class="mt-1 text-xs text-slate-400">
|
||||
保留关键信息,把更多高度让给正文
|
||||
</p>
|
||||
<div
|
||||
ref={logContainerRef}
|
||||
class="min-h-0 flex-1 overflow-auto px-3 pt-0 pb-3 font-mono text-emerald-200"
|
||||
>
|
||||
<Show
|
||||
when={logRows().length > 0}
|
||||
fallback={
|
||||
<div class="flex h-full items-center justify-center rounded-[26px] border border-dashed border-white/10 bg-white/3 px-6 text-slate-500">
|
||||
暂无日志输出
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-2 text-xs">
|
||||
<span class="rounded-full border border-emerald-400/20 bg-emerald-400/10 px-3 py-1 text-emerald-200">
|
||||
{settingsState().showLogTimestamps ? "显示时间" : "隐藏时间"}
|
||||
</span>
|
||||
<span class="rounded-full border border-white/10 bg-white/5 px-3 py-1 text-slate-300">
|
||||
{settingsState().autoScrollLogs
|
||||
? "自动滚动中"
|
||||
: "自动滚动关闭"}
|
||||
</span>
|
||||
<Show when={latestLog()}>
|
||||
<span class="rounded-full border border-amber-400/20 bg-amber-400/10 px-3 py-1 text-amber-200">
|
||||
最近来源 {latestLog()?.accountName}
|
||||
</span>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
ref={logContainerRef}
|
||||
class="min-h-0 flex-1 overflow-y-auto px-4 py-3 font-mono text-emerald-200"
|
||||
}
|
||||
>
|
||||
<Show
|
||||
when={allLogs().length > 0}
|
||||
fallback={
|
||||
<div class="flex h-full items-center justify-center rounded-[26px] border border-dashed border-white/10 bg-white/3 px-6 text-slate-500">
|
||||
暂无日志输出
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div class="space-y-1">
|
||||
<For each={allLogs()}>
|
||||
{(log) => (
|
||||
<div class="rounded-lg px-3 py-2 transition hover:bg-white/4">
|
||||
{/* 头部 */}
|
||||
<div class="flex items-center justify-between text-[11px] text-slate-400">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-amber-300">{log.accountName}</span>
|
||||
<span>#{log.index}</span>
|
||||
<div class="min-w-[980px]">
|
||||
<div class="sticky top-0 z-20 grid grid-cols-[100px_140px_220px_180px_80px_minmax(320px,1fr)] gap-3 border-b border-white/10 bg-zinc-950 px-3 py-2 text-[11px] tracking-wide text-slate-300 uppercase shadow-[0_8px_16px_-12px_rgba(0,0,0,0.8)]">
|
||||
<span>时间</span>
|
||||
<span>姓名</span>
|
||||
<span>账号</span>
|
||||
<span>主机</span>
|
||||
<span>序号</span>
|
||||
<span>内容</span>
|
||||
</div>
|
||||
|
||||
<Show when={settingsState().showLogTimestamps}>
|
||||
<span class="text-emerald-300/80">
|
||||
{log.timestamp}
|
||||
</span>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<span class="text-slate-500">{log.accountId}</span>
|
||||
</div>
|
||||
|
||||
{/* 内容 */}
|
||||
<p class="mt-1 text-sm leading-5 whitespace-pre-wrap text-emerald-200">
|
||||
{log.content}
|
||||
</p>
|
||||
<div class="relative z-0">
|
||||
<For each={logRows()}>
|
||||
{(row) => (
|
||||
<div class="grid grid-cols-[100px_140px_220px_180px_80px_minmax(320px,1fr)] items-center gap-3 border-b border-white/5 px-3 py-2 hover:bg-white/4">
|
||||
<span class="truncate text-emerald-300">
|
||||
{settingsState().showLogTimestamps
|
||||
? row.timestamp
|
||||
: "--:--:--"}
|
||||
</span>
|
||||
<span
|
||||
class="truncate text-amber-200"
|
||||
title={row.accountName}
|
||||
>
|
||||
{row.accountName}
|
||||
</span>
|
||||
<span class="truncate text-slate-300" title={row.accountId}>
|
||||
{row.accountId}
|
||||
</span>
|
||||
<span class="truncate text-slate-400" title={row.host}>
|
||||
{row.host}
|
||||
</span>
|
||||
<span class="truncate text-cyan-200">#{row.seq}</span>
|
||||
<span class="truncate text-emerald-100" title={row.content}>
|
||||
{row.content}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user