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:
@@ -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}
|
||||
>
|
||||
清空日志
|
||||
|
||||
Reference in New Issue
Block a user