feat: build account dashboard and settings workspace

This commit is contained in:
2026-03-26 23:03:45 +08:00
parent be4bc8a3af
commit 8ee9a696b4
18 changed files with 2468 additions and 54 deletions

View File

@@ -0,0 +1,346 @@
import { For, Show, createEffect, type JSX } from "solid-js";
import type { RecordType } from "~/service/wk";
import type { AccountItem } from "~/store/account";
import type { CourseType } from "~/types/Course";
import type { RecordItem } from "~/service/wk";
type RecordTypeOption = {
label: string;
value: RecordType;
};
interface CourseWorkspaceProps {
selectedAccount: AccountItem | null;
selectedCourseList: CourseType[];
selectedCourseId: number | null;
selectedCourse: CourseType | null;
recordType: RecordType;
recordTypeOptions: RecordTypeOption[];
records: RecordItem[];
studyLogs: string[];
recordsLoading: boolean;
recordError: string;
isRefreshingRecords: boolean;
isRunningStudy: boolean;
isRefreshingLogs: boolean;
autoScrollLogs: boolean;
showLogTimestamps: boolean;
densityMode: "comfortable" | "compact";
logFontSize: number;
onSelectCourse: (courseId: number) => void;
onRefreshRecords: () => void;
onChangeRecordType: (value: RecordType) => void;
onStartStudy: () => void;
onStopStudy: () => void;
onRefreshLogs: () => void;
onClearLogs: () => void;
renderRecordState: (value: string) => string;
}
const EmptyState = (props: { children: JSX.Element }) => (
<div class="rounded-[24px] border border-dashed border-zinc-300 bg-white px-4 py-8 text-center text-zinc-500">
{props.children}
</div>
);
const CourseWorkspace = (props: CourseWorkspaceProps) => {
let logContainerRef: HTMLDivElement | undefined;
createEffect(() => {
props.studyLogs.length;
if (!props.autoScrollLogs) {
return;
}
requestAnimationFrame(() => {
const element = logContainerRef;
if (element) {
element.scrollTo({ top: element.scrollHeight, behavior: "smooth" });
}
});
});
createEffect(() => {
if (logContainerRef) {
logContainerRef.style.fontSize = `${props.logFontSize}px`;
}
});
const compact = () => props.densityMode === "compact";
return (
<section class="flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden rounded-[28px] border border-white/80 bg-white/85 shadow-[0_18px_50px_-28px_rgba(15,23,42,0.3)]">
<div class="flex items-center justify-between border-b border-zinc-200/80 px-5 py-4">
<div>
<p class="text-lg font-semibold text-zinc-900"></p>
<p class="mt-1 text-sm text-zinc-500"></p>
</div>
<Show when={props.selectedAccount}>
<div class="rounded-full bg-cyan-50 px-3 py-1 text-sm text-cyan-700">
{props.selectedAccount?.user.name}
</div>
</Show>
</div>
<Show
when={props.selectedAccount}
fallback={
<div class="flex h-full items-center justify-center px-6 text-zinc-500">
</div>
}
>
<div
class={
compact()
? "grid min-h-0 flex-1 gap-3 p-3 xl:grid-cols-[320px_minmax(0,1fr)]"
: "grid min-h-0 flex-1 gap-4 p-4 xl:grid-cols-[340px_minmax(0,1fr)]"
}
>
<div class="flex min-h-0 flex-col overflow-hidden rounded-[24px] border border-zinc-200 bg-[linear-gradient(180deg,_rgba(248,250,252,0.9),_rgba(255,255,255,0.95))]">
<div class="border-b border-zinc-200 px-4 py-3">
<p class="text-sm font-semibold text-zinc-800"></p>
<p class="mt-1 text-xs text-zinc-500"></p>
</div>
<div
class={
compact()
? "flex min-h-0 flex-1 flex-col gap-2 overflow-y-auto p-2.5"
: "flex min-h-0 flex-1 flex-col gap-3 overflow-y-auto p-3"
}
>
<For each={props.selectedCourseList}>
{(course) => {
const selected = course.id === props.selectedCourseId;
return (
<button
type="button"
class={
selected
? compact()
? "rounded-[20px] border border-cyan-300 bg-[linear-gradient(145deg,_rgba(236,254,255,0.95),_rgba(240,253,244,0.95))] px-3 py-3 text-left shadow-sm"
: "rounded-[22px] border border-cyan-300 bg-[linear-gradient(145deg,_rgba(236,254,255,0.95),_rgba(240,253,244,0.95))] px-4 py-4 text-left shadow-sm"
: compact()
? "rounded-[20px] border border-zinc-200 bg-white px-3 py-3 text-left shadow-sm transition hover:border-cyan-200 hover:bg-cyan-50/30"
: "rounded-[22px] border border-zinc-200 bg-white px-4 py-4 text-left shadow-sm transition hover:border-cyan-200 hover:bg-cyan-50/30"
}
onClick={() => props.onSelectCourse(course.id)}
>
<p class="truncate text-base font-semibold text-zinc-900">
{course.name}
</p>
<div class="mt-3 grid gap-1 text-sm text-zinc-600">
<p>{course.id}</p>
<p>{course.teacher}</p>
<p>{course.progress}</p>
</div>
</button>
);
}}
</For>
</div>
</div>
<div
class={
compact()
? "grid min-h-0 gap-3 xl:grid-rows-[minmax(0,1fr)_240px]"
: "grid min-h-0 gap-4 xl:grid-rows-[minmax(0,1fr)_260px]"
}
>
<div class="flex min-h-0 flex-col overflow-hidden rounded-[24px] border border-zinc-200 bg-[linear-gradient(180deg,_rgba(248,250,252,0.9),_rgba(255,255,255,0.95))]">
<div class="flex flex-wrap items-center justify-between gap-3 border-b border-zinc-200 px-4 py-3">
<div>
<p class="text-sm font-semibold text-zinc-800"></p>
<p class="mt-1 text-xs text-zinc-500">
{props.selectedCourse
? props.selectedCourse.name
: "请选择课程"}
</p>
</div>
<div class="flex flex-wrap items-center gap-2">
<button
type="button"
class="rounded-xl border border-zinc-200 bg-white px-3 py-2 text-sm text-zinc-700 transition hover:bg-zinc-100 disabled:cursor-not-allowed disabled:opacity-60"
disabled={
!props.selectedCourse || props.isRefreshingRecords
}
onClick={props.onRefreshRecords}
>
{props.isRefreshingRecords ? "刷新中..." : "刷新记录"}
</button>
<select
class="rounded-xl border border-zinc-200 bg-white px-3 py-2 text-sm transition outline-none focus:border-cyan-400"
value={props.recordType}
onChange={(event) =>
props.onChangeRecordType(
event.currentTarget.value as RecordType,
)
}
>
<For each={props.recordTypeOptions}>
{(item) => (
<option value={item.value}>{item.label}</option>
)}
</For>
</select>
<Show when={props.recordType === ""}>
<button
type="button"
class="rounded-xl border border-cyan-200 bg-cyan-50 px-3 py-2 text-sm text-cyan-700 transition hover:bg-cyan-100"
onClick={
props.isRunningStudy
? props.onStopStudy
: props.onStartStudy
}
>
{props.isRunningStudy ? "停止刷课" : "开始刷课"}
</button>
</Show>
</div>
</div>
<div
class={
compact()
? "min-h-0 flex-1 overflow-y-auto p-2.5"
: "min-h-0 flex-1 overflow-y-auto p-3"
}
>
<Show when={props.recordsLoading}>
<EmptyState>...</EmptyState>
</Show>
<Show when={!props.recordsLoading && props.recordError}>
<div class="rounded-[24px] border border-rose-200 bg-rose-50 px-4 py-3 text-sm text-rose-600">
{props.recordError}
</div>
</Show>
<Show
when={
!props.recordsLoading &&
!props.recordError &&
!props.selectedCourse
}
>
<EmptyState></EmptyState>
</Show>
<Show
when={
!props.recordsLoading &&
!props.recordError &&
props.selectedCourse &&
props.records.length === 0
}
>
<EmptyState></EmptyState>
</Show>
<div class="flex flex-col gap-3">
<For each={props.records}>
{(record) => (
<div
class={
compact()
? "rounded-[20px] border border-zinc-200 bg-white px-3 py-3 shadow-sm"
: "rounded-[22px] border border-zinc-200 bg-white px-4 py-4 shadow-sm"
}
>
<div class="flex flex-wrap items-start justify-between gap-3">
<div class="min-w-0 flex-1">
<p class="truncate text-base font-semibold text-zinc-900">
{record.name}
</p>
<p class="mt-1 text-sm text-zinc-500">
ID{record.id} | {record.chapterId}
</p>
</div>
<span class="rounded-full bg-zinc-100 px-3 py-1 text-xs font-medium text-zinc-700">
{props.renderRecordState(record.state) ||
"未知状态"}
</span>
</div>
<div class="mt-4 grid gap-2 text-sm text-zinc-600 md:grid-cols-2 xl:grid-cols-3">
<p>{record.videoDuration}</p>
<p>{record.duration}</p>
<p>{record.progress}</p>
<p>{record.beginTime || "-"}</p>
<p>{record.finalTime || "-"}</p>
<p>{record.viewCount}</p>
</div>
</div>
)}
</For>
</div>
</div>
</div>
<div class="flex min-h-0 flex-col overflow-hidden rounded-[24px] border border-zinc-200 bg-white">
<div class="flex items-center justify-between border-b border-zinc-200 px-4 py-3">
<div>
<p class="text-sm font-semibold text-zinc-800"></p>
<p class="mt-1 text-xs text-zinc-500">
</p>
</div>
<div class="flex items-center gap-2">
<button
type="button"
class="rounded-lg border border-zinc-200 px-3 py-1 text-xs text-zinc-700 transition hover:bg-zinc-100 disabled:cursor-not-allowed disabled:opacity-60"
disabled={props.isRefreshingLogs}
onClick={props.onRefreshLogs}
>
{props.isRefreshingLogs ? "刷新中..." : "刷新日志"}
</button>
<button
type="button"
class="rounded-lg border border-zinc-200 px-3 py-1 text-xs text-zinc-700 transition hover:bg-zinc-100"
onClick={props.onClearLogs}
>
</button>
</div>
</div>
<div
ref={logContainerRef}
class="min-h-0 flex-1 overflow-y-auto bg-zinc-950 px-4 py-3 font-mono text-emerald-300"
>
<Show
when={props.studyLogs.length > 0}
fallback={<p></p>}
>
<div class="space-y-2">
<For each={props.studyLogs}>
{(log, index) => (
<p>
[{index() + 1}]{" "}
{props.showLogTimestamps
? `${new Date().toLocaleTimeString()} `
: ""}
{log}
</p>
)}
</For>
</div>
</Show>
</div>
</div>
</div>
</div>
</Show>
</section>
);
};
export default CourseWorkspace;