feat: UI optimizations - button feedback, layout fixes, cache clearing, work/exam records

- Add active: state feedback to all buttons across the app
- Fix cache clearing to update Zustand store (not just localStorage)
- Remove checkboxes from settings cache section, compact layout
- Settings page: single outer scroll instead of dual-column scroll
- CourseWorkspace: elastic log panel height, work/exam record counts
- Integrate WorkList/ExamList types and display in UI
- Delete unused CourseList.tsx component
- Fix wk.ts: strict equality, remove unused import

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-26 17:36:02 +08:00
parent 13f0be162b
commit a1911573d1
13 changed files with 603 additions and 302 deletions

View File

@@ -6,11 +6,13 @@ import {
createSignal,
type JSX,
} from "solid-js";
import type { CourseKind, RecordType } from "~/service/wk";
import type { CourseKind, RecordType, WorkListItem, ExamListItem } from "~/service/wk";
import type { AccountItem } from "~/store/account";
import type { CourseType } from "~/types/Course";
import type { RecordItem } from "~/service/wk";
const stripHtml = (value: string) => value.replace(/<[^>]+>/g, "").trim();
type RecordTypeOption = {
label: string;
value: RecordType;
@@ -33,6 +35,8 @@ interface CourseWorkspaceProps {
recordTypeOptions: RecordTypeOption[];
courseRecordTypeOptions: CourseRecordTypeOption[];
records: RecordItem[];
workList: WorkListItem[];
examList: ExamListItem[];
studyLogs: string[];
recordsLoading: boolean;
recordError: string;
@@ -179,7 +183,7 @@ const CourseWorkspace = (props: CourseWorkspaceProps) => {
</select>
<button
type="button"
class="rounded-lg border border-zinc-200 bg-white px-2 py-1 text-xs text-zinc-700 transition hover:bg-zinc-100 disabled:cursor-not-allowed disabled:opacity-60"
class="rounded-lg border border-zinc-200 bg-white px-2 py-1 text-xs text-zinc-700 transition hover:bg-zinc-100 active:bg-zinc-200 disabled:cursor-not-allowed disabled:opacity-60"
disabled={props.isRefreshingCourseRecords}
onClick={props.onRefreshCourseRecords}
>
@@ -232,8 +236,8 @@ const CourseWorkspace = (props: CourseWorkspaceProps) => {
<div
class={
compact()
? "grid min-h-0 gap-1.5 grid-rows-[minmax(0,1fr)_176px] xl:grid-rows-[minmax(0,1fr)_188px]"
: "grid min-h-0 gap-2 grid-rows-[minmax(0,1fr)_188px] xl:grid-rows-[minmax(0,1fr)_208px]"
? "grid min-h-0 gap-1.5 grid-rows-[minmax(0,1fr)_minmax(140px,30vh)]"
: "grid min-h-0 gap-2 grid-rows-[minmax(0,1fr)_minmax(140px,30vh)]"
}
>
<div class="flex min-h-0 flex-col overflow-hidden rounded-[18px] border border-zinc-200 bg-[linear-gradient(180deg,rgba(248,250,252,0.9),rgba(255,255,255,0.95))]">
@@ -259,7 +263,7 @@ const CourseWorkspace = (props: CourseWorkspaceProps) => {
<div class="flex flex-wrap items-center gap-1.5">
<button
type="button"
class="rounded-lg border border-zinc-200 bg-white px-2 py-1 text-xs text-zinc-700 transition hover:bg-zinc-100 disabled:cursor-not-allowed disabled:opacity-60"
class="rounded-lg border border-zinc-200 bg-white px-2 py-1 text-xs text-zinc-700 transition hover:bg-zinc-100 active:bg-zinc-200 disabled:cursor-not-allowed disabled:opacity-60"
disabled={
!props.selectedCourse || props.isRefreshingRecords
}
@@ -287,7 +291,7 @@ const CourseWorkspace = (props: CourseWorkspaceProps) => {
<Show when={props.recordType === ""}>
<button
type="button"
class="rounded-lg border border-cyan-200 bg-cyan-50 px-2 py-1 text-xs text-cyan-700 transition hover:bg-cyan-100"
class="rounded-lg border border-cyan-200 bg-cyan-50 px-2 py-1 text-xs text-cyan-700 transition hover:bg-cyan-100 active:bg-cyan-200"
onClick={
props.isRunningStudy
? props.onStopStudy
@@ -299,25 +303,26 @@ const CourseWorkspace = (props: CourseWorkspaceProps) => {
</Show>
</div>
</div>
<Show when={props.recordType === ""}>
<div class="flex flex-wrap items-center justify-between gap-1.5 border-b border-zinc-200/70 px-2.5 py-1.5">
<div class="flex flex-wrap items-center gap-1.5 text-xs">
<button
type="button"
class={recordFilter() === "all" ? "rounded-full border border-zinc-200 bg-zinc-100 px-2.5 py-0.5 font-medium text-zinc-700" : "rounded-full border border-zinc-200 bg-white px-2.5 py-0.5 text-zinc-600 transition hover:bg-zinc-100"}
class={recordFilter() === "all" ? "rounded-full border border-zinc-200 bg-zinc-100 px-2.5 py-0.5 font-medium text-zinc-700 active:bg-zinc-200" : "rounded-full border border-zinc-200 bg-white px-2.5 py-0.5 text-zinc-600 transition hover:bg-zinc-100 active:bg-zinc-200"}
onClick={() => setRecordFilter("all")}
>
{recordStats().total}
</button>
<button
type="button"
class={recordFilter() === "unlearned" ? "rounded-full border border-amber-200 bg-amber-100 px-2.5 py-0.5 font-medium text-amber-700" : "rounded-full border border-amber-200 bg-white px-2.5 py-0.5 text-amber-700 transition hover:bg-amber-50"}
class={recordFilter() === "unlearned" ? "rounded-full border border-amber-200 bg-amber-100 px-2.5 py-0.5 font-medium text-amber-700 active:bg-amber-200" : "rounded-full border border-amber-200 bg-white px-2.5 py-0.5 text-amber-700 transition hover:bg-amber-50 active:bg-amber-100"}
onClick={() => setRecordFilter("unlearned")}
>
{recordStats().unlearned}
</button>
<button
type="button"
class={recordFilter() === "learned" ? "rounded-full border border-emerald-200 bg-emerald-100 px-2.5 py-0.5 font-medium text-emerald-700" : "rounded-full border border-emerald-200 bg-white px-2.5 py-0.5 text-emerald-700 transition hover:bg-emerald-50"}
class={recordFilter() === "learned" ? "rounded-full border border-emerald-200 bg-emerald-100 px-2.5 py-0.5 font-medium text-emerald-700 active:bg-emerald-200" : "rounded-full border border-emerald-200 bg-white px-2.5 py-0.5 text-emerald-700 transition hover:bg-emerald-50 active:bg-emerald-100"}
onClick={() => setRecordFilter("learned")}
>
{recordStats().learned}
@@ -327,6 +332,17 @@ const CourseWorkspace = (props: CourseWorkspaceProps) => {
{recordFilter() === "all" ? "全部" : recordFilter() === "unlearned" ? "未学" : "已学"}
</p>
</div>
</Show>
<Show when={props.recordType === "/work" && props.workList.length > 0}>
<div class="flex items-center gap-1.5 border-b border-zinc-200/70 px-2.5 py-1.5 text-xs text-zinc-500">
{props.workList.length}
</div>
</Show>
<Show when={props.recordType === "/exam" && props.examList.length > 0}>
<div class="flex items-center gap-1.5 border-b border-zinc-200/70 px-2.5 py-1.5 text-xs text-zinc-500">
{props.examList.length}
</div>
</Show>
<div
class={
@@ -360,6 +376,7 @@ const CourseWorkspace = (props: CourseWorkspaceProps) => {
!props.recordsLoading &&
!props.recordError &&
props.selectedCourse &&
props.recordType === "" &&
filteredRecords().length === 0
}
>
@@ -370,59 +387,195 @@ const CourseWorkspace = (props: CourseWorkspaceProps) => {
</EmptyState>
</Show>
<div class={compact() ? "flex flex-col gap-1.5" : "flex flex-col gap-2"}>
<For each={filteredRecords()}>
{(record) => {
const stateText =
props.renderRecordState(record.state) || "未知状态";
const learned =
stateText.includes("已学") || record.progress === "1.00";
<Show
when={
!props.recordsLoading &&
!props.recordError &&
props.selectedCourse &&
props.recordType === "/work" &&
props.workList.length === 0
}
>
<EmptyState></EmptyState>
</Show>
return (
<div
class={
learned
? compact()
? "rounded-[16px] border border-emerald-200 bg-emerald-50/35 px-2 py-1.5 shadow-sm"
: "rounded-[18px] border border-emerald-200 bg-emerald-50/35 px-2.5 py-2.5 shadow-sm"
: compact()
? "rounded-[16px] border border-amber-200 bg-amber-50/35 px-2 py-1.5 shadow-sm"
: "rounded-[18px] border border-amber-200 bg-amber-50/35 px-2.5 py-2.5 shadow-sm"
}
>
<div class="flex flex-wrap items-start justify-between gap-2">
<div class="min-w-0 flex-1">
<p class="truncate text-sm font-semibold text-zinc-900">
{record.name}
</p>
<p class="mt-1 text-xs text-zinc-500">
ID{record.id} | {record.chapterId}
</p>
<Show
when={
!props.recordsLoading &&
!props.recordError &&
props.selectedCourse &&
props.recordType === "/exam" &&
props.examList.length === 0
}
>
<EmptyState></EmptyState>
</Show>
<Show when={props.recordType === ""}>
<div class={compact() ? "flex flex-col gap-1.5" : "flex flex-col gap-2"}>
<For each={filteredRecords()}>
{(record) => {
const stateText =
props.renderRecordState(record.state) || "未知状态";
const learned =
stateText.includes("已学") || record.progress === "1.00";
return (
<div
class={
learned
? compact()
? "rounded-[16px] border border-emerald-200 bg-emerald-50/35 px-2 py-1.5 shadow-sm"
: "rounded-[18px] border border-emerald-200 bg-emerald-50/35 px-2.5 py-2.5 shadow-sm"
: compact()
? "rounded-[16px] border border-amber-200 bg-amber-50/35 px-2 py-1.5 shadow-sm"
: "rounded-[18px] border border-amber-200 bg-amber-50/35 px-2.5 py-2.5 shadow-sm"
}
>
<div class="flex flex-wrap items-start justify-between gap-2">
<div class="min-w-0 flex-1">
<p class="truncate text-sm font-semibold text-zinc-900">
{record.name}
</p>
<p class="mt-1 text-xs text-zinc-500">
ID{record.id} | {record.chapterId}
</p>
</div>
<span
class={
learned
? "rounded-full bg-emerald-100 px-3 py-1 text-xs font-medium text-emerald-700"
: "rounded-full bg-amber-100 px-3 py-1 text-xs font-medium text-amber-700"
}
>
{stateText}
</span>
</div>
<span
class={
learned
? "rounded-full bg-emerald-100 px-3 py-1 text-xs font-medium text-emerald-700"
: "rounded-full bg-amber-100 px-3 py-1 text-xs font-medium text-amber-700"
}
>
{stateText}
</span>
</div>
<div class="mt-2 grid gap-1 text-xs 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 class="mt-2 grid gap-1 text-xs 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>
</div>
);
}}
</For>
</div>
);
}}
</For>
</div>
</Show>
<Show when={props.recordType === "/work"}>
<div class={compact() ? "flex flex-col gap-1.5" : "flex flex-col gap-2"}>
<For each={props.workList}>
{(work) => {
const stateRaw = stripHtml(work.state);
const stateText = stateRaw || "未做";
const done = stateRaw.includes("已阅") || stateRaw.includes("已提交") || stateRaw.includes("已完成");
return (
<div
class={
done
? compact()
? "rounded-[16px] border border-emerald-200 bg-emerald-50/35 px-2 py-1.5 shadow-sm"
: "rounded-[18px] border border-emerald-200 bg-emerald-50/35 px-2.5 py-2.5 shadow-sm"
: compact()
? "rounded-[16px] border border-amber-200 bg-amber-50/35 px-2 py-1.5 shadow-sm"
: "rounded-[18px] border border-amber-200 bg-amber-50/35 px-2.5 py-2.5 shadow-sm"
}
>
<div class="flex flex-wrap items-start justify-between gap-2">
<div class="min-w-0 flex-1">
<p class="truncate text-sm font-semibold text-zinc-900">
{work.title || work.name}
</p>
<p class="mt-1 text-xs text-zinc-500">
ID{work.id} | {work.chapterId}
</p>
</div>
<span
class={
done
? "rounded-full bg-emerald-100 px-3 py-1 text-xs font-medium text-emerald-700"
: "rounded-full bg-amber-100 px-3 py-1 text-xs font-medium text-amber-700"
}
>
{stateText}
</span>
</div>
<div class="mt-2 grid gap-1 text-xs text-zinc-600 md:grid-cols-2 xl:grid-cols-3">
<p>{work.typeName || work.type || "-"}</p>
<p>{work.score || "-"}</p>
<p>{stripHtml(work.finalScore) || "-"}</p>
<p>{work.topicNumber || "-"}</p>
<p>{work.addTime || "-"}</p>
<p>{work.finishTime !== "-" ? work.finishTime : "-"}</p>
</div>
</div>
);
}}
</For>
</div>
</Show>
<Show when={props.recordType === "/exam"}>
<div class={compact() ? "flex flex-col gap-1.5" : "flex flex-col gap-2"}>
<For each={props.examList}>
{(exam) => {
const stateRaw = stripHtml(exam.state);
const stateText = stateRaw || "未做";
const done = stateRaw.includes("已阅") || stateRaw.includes("已提交") || stateRaw.includes("已完成");
return (
<div
class={
done
? compact()
? "rounded-[16px] border border-emerald-200 bg-emerald-50/35 px-2 py-1.5 shadow-sm"
: "rounded-[18px] border border-emerald-200 bg-emerald-50/35 px-2.5 py-2.5 shadow-sm"
: compact()
? "rounded-[16px] border border-amber-200 bg-amber-50/35 px-2 py-1.5 shadow-sm"
: "rounded-[18px] border border-amber-200 bg-amber-50/35 px-2.5 py-2.5 shadow-sm"
}
>
<div class="flex flex-wrap items-start justify-between gap-2">
<div class="min-w-0 flex-1">
<p class="truncate text-sm font-semibold text-zinc-900">
{exam.title || exam.name}
</p>
<p class="mt-1 text-xs text-zinc-500">
ID{exam.id} | {exam.chapterId}
</p>
</div>
<span
class={
done
? "rounded-full bg-emerald-100 px-3 py-1 text-xs font-medium text-emerald-700"
: "rounded-full bg-amber-100 px-3 py-1 text-xs font-medium text-amber-700"
}
>
{stateText}
</span>
</div>
<div class="mt-2 grid gap-1 text-xs text-zinc-600 md:grid-cols-2 xl:grid-cols-3">
<p>{exam.limitedTime ? `${exam.limitedTime}分钟` : "-"}</p>
<p>{exam.score || "-"}</p>
<p>{stripHtml(exam.finalScore) || "-"}</p>
<p>{exam.topicNumber || "-"}</p>
<p>{exam.addTime || "-"}</p>
<p>{exam.finishTime !== "-" ? exam.finishTime : "-"}</p>
</div>
</div>
);
}}
</For>
</div>
</Show>
</div>
</div>
@@ -440,7 +593,7 @@ const CourseWorkspace = (props: CourseWorkspaceProps) => {
<div class="flex items-center gap-2">
<button
type="button"
class="rounded-lg border border-zinc-200 px-2 py-0.5 text-xs text-zinc-700 transition hover:bg-zinc-100 disabled:cursor-not-allowed disabled:opacity-60"
class="rounded-lg border border-zinc-200 px-2 py-0.5 text-xs text-zinc-700 transition hover:bg-zinc-100 active:bg-zinc-200 disabled:cursor-not-allowed disabled:opacity-60"
disabled={props.isRefreshingLogs}
onClick={props.onRefreshLogs}
>
@@ -448,7 +601,7 @@ const CourseWorkspace = (props: CourseWorkspaceProps) => {
</button>
<button
type="button"
class="rounded-lg border border-zinc-200 px-2 py-0.5 text-xs text-zinc-700 transition hover:bg-zinc-100"
class="rounded-lg border border-zinc-200 px-2 py-0.5 text-xs text-zinc-700 transition hover:bg-zinc-100 active:bg-zinc-200"
onClick={props.onClearLogs}
>