From 6bd923649e48cfe1d97e40676ff3abb639ca160b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E6=9D=B0?= <17793426456@163.com> Date: Mon, 7 Sep 2026 16:30:56 +0800 Subject: [PATCH 01/17] =?UTF-8?q?fix(coaching):=20=E6=81=A2=E5=A4=8D?= =?UTF-8?q?=E8=87=AA=E7=94=B1=E6=97=B6=E9=97=B4=E6=8E=92=E8=AF=BE=E6=B5=81?= =?UTF-8?q?=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 排课页面恢复按实际开始和结束时间展示与创建,移除固定节次课表映射,保留现有封装组件和排课操作流程。 Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- web/src/components/CoachingSchedulePanel.tsx | 749 +++++++------------ 1 file changed, 288 insertions(+), 461 deletions(-) diff --git a/web/src/components/CoachingSchedulePanel.tsx b/web/src/components/CoachingSchedulePanel.tsx index 29e0b0e..29b8073 100644 --- a/web/src/components/CoachingSchedulePanel.tsx +++ b/web/src/components/CoachingSchedulePanel.tsx @@ -43,15 +43,6 @@ import { useIsMobile } from "./ui/use-mobile"; import { MobileDateWheel } from "./cloudsteps/MobileWheelPicker"; import { CloudSpin } from "./cloudsteps/arco"; import { CloudButton } from "./cloudsteps"; -import { useTimetableStore, blankCourseInput } from "../stores/timetableStore"; -import { CourseEditorDialog } from "./timetable/CourseEditorDialog"; -import { timeToSections } from "../utils/coachingSectionMap"; -import { isCourseShow, weekRangeLabel } from "../utils/timetableFilter"; -import { - DEFAULT_SECTIONS, - type Course, - type TimetableConfig, -} from "../api/timetable"; const pad2 = (n: number) => String(n).padStart(2, "0"); const fmtYMD = (d: Date) => @@ -128,23 +119,6 @@ function isSchedulePast(schedule: CoachingWeekSchedule, nowTs: number): boolean return end.getTime() <= nowTs; } -/** - * 排课显示的结束时间标签: - * - scheduled:计划 endTime - * - in_progress:空字符串(只显示「21:00-」) - * - completed:session.endedAt 的实际 HH:MM,回退到计划 endTime - */ -function displayEndLabel(schedule: CoachingWeekSchedule): string { - if (schedule.status === "in_progress") return ""; - if (schedule.status === "completed" && schedule.session?.endedAt) { - const d = new Date(schedule.session.endedAt); - if (!Number.isNaN(d.getTime())) { - return `${pad2(d.getHours())}:${pad2(d.getMinutes())}`; - } - } - return schedule.endTime?.slice(0, 5) || ""; -} - function parseHmToMinutes(t: string): number { const raw = (t || "").trim().slice(0, 5); const m = /^(\d{1,2}):(\d{2})$/.exec(raw); @@ -278,7 +252,7 @@ function TimetableBlock({ : STATUS_SOFT[schedule.status] || STATUS_SOFT.scheduled; const { title } = lessonDisplay(t, schedule); const start = schedule.startTime?.slice(0, 5) || ""; - const end = displayEndLabel(schedule); + const end = schedule.endTime?.slice(0, 5) || ""; const widthPct = 100 / colCount; const leftPct = col * widthPct; @@ -369,15 +343,6 @@ export function CoachingSchedulePanel({ nowTs, mode = "coach" }: Props) { const [tipHole, setTipHole] = useState(null); const [tipReady, setTipReady] = useState(false); - // localStorage 自定义课程 - const ttLoad = useTimetableStore((s) => s.load); - const ttLoaded = useTimetableStore((s) => s.loaded); - const ttCourses = useTimetableStore((s) => s.courses); - const ttConfig = useTimetableStore((s) => s.config); - const [editorOpen, setEditorOpen] = useState(false); - const [editingCourse, setEditingCourse] = useState(null); - const [coursePreset, setCoursePreset] = useState<{ weekDay: number; startSection: number } | null>(null); - const timetableHostRef = useRef(null); const [axisHeightPx, setAxisHeightPx] = useState(280); @@ -388,8 +353,6 @@ export function CoachingSchedulePanel({ nowTs, mode = "coach" }: Props) { ); const todayYMD = fmtYMD(new Date()); const weekShortLabel = `${fmtMD(weekMon)}–${fmtMD(addDays(weekMon, 6))}`; - const currentWeekMon = useMemo(() => weekMonday(new Date()), []); - const isCurrentCalendarWeek = fmtYMD(weekMon) === fmtYMD(currentWeekMon); const byDay = useMemo(() => { const map: Record = {}; @@ -483,8 +446,7 @@ export function CoachingSchedulePanel({ nowTs, mode = "coach" }: Props) { useEffect(() => { void loadWeek(); void loadQuotas(); - if (!ttLoaded) void ttLoad(); - }, [loadWeek, loadQuotas, ttLoad, ttLoaded]); + }, [loadWeek, loadQuotas]); useLayoutEffect(() => { const el = timetableHostRef.current; @@ -696,111 +658,6 @@ export function CoachingSchedulePanel({ nowTs, mode = "coach" }: Props) { !!parseCoachingSlotEnd(selected.scheduledDate, selected.endTime) && parseCoachingSlotEnd(selected.scheduledDate, selected.endTime)!.getTime() < nowTs; - /** - * 节次网格合并项:后端预约(按时间映射到节次)+ localStorage 自定义课程。 - * 每项带来源标记 kind,点击时按 kind 分流交互。 - */ - const gridItems = useMemo(() => { - const sections = ttConfig.sections.length > 0 ? ttConfig.sections : DEFAULT_SECTIONS; - const items: GridItem[] = []; - - // 后端预约:按 scheduledDate 算出属于本周第几天,时间映射到节次 - for (const s of schedules) { - const ymd = s.scheduledDate?.slice?.(0, 10) || s.scheduledDate; - if (!ymd) continue; - const dayIdx = weekDays.findIndex((d) => fmtYMD(d) === ymd); - if (dayIdx < 0) continue; - const { startSection, endSection } = timeToSections(s.startTime, s.endTime, sections); - const past = isSchedulePast(s, nowTs); - const statusColor = - s.status === "completed" - ? "#F59E0B" - : past - ? "#A78BFA" - : s.status === "in_progress" - ? "#55A3FF" - : s.status === "cancelled" - ? "#E85555" - : "#4ECDC4"; - const { title, subtitle } = lessonDisplay(t, s); - items.push({ - key: `co-${s.id}`, - kind: "coaching", - weekDay: dayIdx + 1, - startSection, - endSection, - color: statusColor, - title, - subtitle, - meta: `${s.startTime?.slice(0, 5)}-${displayEndLabel(s)}`, - typeLabel: t("coaching.schedule_type"), - statusLabel: t(`coaching.status.${s.status}`, { defaultValue: s.status }), - schedule: s, - }); - } - - // localStorage 自定义课程:按 weekDay 归属到本周(不区分具体日期) - for (const c of ttCourses) { - if (c.weekDay < 1 || c.weekDay > 7) continue; - items.push({ - key: `cu-${c.id}`, - kind: "course", - weekDay: c.weekDay, - startSection: c.startSection, - endSection: c.endSection, - color: c.color, - title: c.name, - subtitle: c.room ? `@${c.room}` : undefined, - meta: weekRangeLabel(c), - typeLabel: t("timetable.custom_course"), - course: c, - }); - } - - const byDay = new Map(); - for (const item of items) { - const dayItems = byDay.get(item.weekDay) || []; - dayItems.push(item); - byDay.set(item.weekDay, dayItems); - } - - return Array.from(byDay.values()).flatMap((dayItems) => { - const sorted = [...dayItems].sort( - (a, b) => a.startSection - b.startSection || b.endSection - a.endSection, - ); - const laneEnds: number[] = []; - const assigned = sorted.map((item) => { - let lane = laneEnds.findIndex((end) => end < item.startSection); - if (lane < 0) { - lane = laneEnds.length; - laneEnds.push(item.endSection); - } else { - laneEnds[lane] = item.endSection; - } - return { item, lane }; - }); - const laneCount = Math.max(1, laneEnds.length); - - return assigned.map(({ item, lane }) => ({ - ...item, - lane, - laneCount, - conflict: laneCount > 1 && dayItems.some( - (other) => - other.key !== item.key && - other.startSection <= item.endSection && - other.endSection >= item.startSection, - ), - })); - }); - }, [schedules, ttCourses, ttConfig.sections, weekDays, t, nowTs]); - - function openCourseEditor(course: Course | null, preset: { weekDay: number; startSection: number } | null) { - setEditingCourse(course); - setCoursePreset(preset); - setEditorOpen(true); - } - const weekTrigger = ( + ); + })} + {weekDays.map((d, i) => { + const ymd = fmtYMD(d); + const isToday = ymd === todayYMD; + return ( + + ); + })} + + ) : ( +
+
+ {weekDays.map((d, i) => { + const ymd = fmtYMD(d); + const isToday = ymd === todayYMD; + return ( + + ); + })} + +
+ {axisMarks.map((m) => ( +
+ + {fmtMinutes(m).slice(0, 5)} + +
+ ))} +
+ + {weekDays.map((d, dIdx) => { + const ymd = fmtYMD(d); + const isToday = ymd === todayYMD; + const isPastDay = ymd < todayYMD; + const dayItems = byDay[ymd] || []; + const laidOut = layoutDayEvents( + dayItems, + axisRange.startMin, + axisRange.endMin, + axisHeightPx, + ); + const nowLocalM = new Date(nowTs).getHours() * 60 + new Date(nowTs).getMinutes(); + const todayPastHeightPx = + isToday && nowLocalM > axisRange.startMin + ? Math.min( + axisHeightPx, + ((Math.min(nowLocalM, axisRange.endMin) - axisRange.startMin) / axisSpan) * + axisHeightPx, + ) + : 0; + return ( +
+ {isPastDay ? ( +
+ ) : null} + {todayPastHeightPx > 0 ? ( +
+ ) : null} + {axisMarks.map((m) => ( +
+ ))} + + {isCoach ? ( +
+ ); + })} +
+ )}
)}
@@ -951,7 +986,7 @@ export function CoachingSchedulePanel({ nowTs, mode = "coach" }: Props) {

{selected.scheduledDate?.slice?.(0, 10) || selected.scheduledDate} ·{" "} - {selected.startTime}–{displayEndLabel(selected)} + {selected.startTime}–{selected.endTime}

)} - - - - ); -} - -// ===== 节次网格子组件 ===== - -type GridItem = { - key: string; - kind: "coaching" | "course"; - weekDay: number; - startSection: number; - endSection: number; - color: string; - title: string; - subtitle?: string; - meta?: string; - typeLabel: string; - statusLabel?: string; - conflict?: boolean; - lane?: number; - laneCount?: number; - schedule?: CoachingWeekSchedule; - course?: Course; -}; - -function hexToRgba(hex: string, alpha: number): string { - const h = hex.replace("#", ""); - const n = parseInt(h.length === 3 ? h.split("").map((c) => c + c).join("") : h, 16); - return `rgba(${(n >> 16) & 255}, ${(n >> 8) & 255}, ${n & 255}, ${alpha})`; -} - -function SectionGrid({ - weekDays, - todayYMD, - sections, - items, - isCoach, - t, - onDayHeaderClick, - onCellClick, - onItemClick, -}: { - weekDays: Date[]; - todayYMD: string; - sections: TimetableConfig["sections"]; - items: GridItem[]; - isCoach: boolean; - t: TFunction; - onDayHeaderClick: (d: Date) => void; - onCellClick: (weekDay: number, section: number) => void; - onItemClick: (item: GridItem) => void; -}) { - const isMobile = useIsMobile(); - const totalRows = sections.length; - const gridStyle = { - gridTemplateColumns: isMobile - ? `32px repeat(7, minmax(40px, 1fr))` - : `80px repeat(7, minmax(0, 1fr))`, - gridTemplateRows: isMobile - ? `42px repeat(${totalRows}, 52px)` - : `56px repeat(${totalRows}, 64px)`, - } as const; - - return ( -
- {/* 左上角 */} -
- {t("timetable.section")} -
- {/* 表头:7 天 */} - {weekDays.map((d, i) => { - const ymd = fmtYMD(d); - const isToday = ymd === todayYMD; - return ( - - ); - })} - - {/* 节次标签 + 空白格 */} - {sections.map((sec) => ( -
-
- {sec.no} - {sec.start} - {sec.end} -
- {Array.from({ length: 7 }, (_, dayIdx) => ( - - ))} -
- ))} - - {items.length === 0 && ( -
-
-
- -
-

{t("coaching.empty_schedule_title")}

-

{t("coaching.empty_schedule_desc")}

-
-
- )} - - {/* 课程/预约色块 */} - {items.map((item) => { - const col = item.weekDay + 1; - const rowStart = item.startSection + 1; - const rowEnd = item.endSection + 2; - const span = item.endSection - item.startSection + 1; - const showDetail = span >= 2; - return ( - - ); - })}
); } From 0681ae1b820a1cedf7af401109971580ce79004a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E6=9D=B0?= <17793426456@163.com> Date: Mon, 7 Sep 2026 16:47:41 +0800 Subject: [PATCH 02/17] =?UTF-8?q?fix(coaching):=20=E8=AE=A9=E6=89=8B?= =?UTF-8?q?=E6=9C=BA=E7=AB=AF=E5=91=A8=E8=AF=BE=E8=A1=A8=E6=98=BE=E7=A4=BA?= =?UTF-8?q?=E5=AE=8C=E6=95=B4=E4=B8=83=E5=A4=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 移动端取消周课表的固定最小宽度和横向滚动,让七个日期列按屏幕宽度自适应排列。 Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- web/src/components/CoachingSchedulePanel.tsx | 24 ++++++++------------ 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/web/src/components/CoachingSchedulePanel.tsx b/web/src/components/CoachingSchedulePanel.tsx index 29b8073..c9f8bbc 100644 --- a/web/src/components/CoachingSchedulePanel.tsx +++ b/web/src/components/CoachingSchedulePanel.tsx @@ -398,11 +398,11 @@ export function CoachingSchedulePanel({ nowTs, mode = "coach" }: Props) { ); const axisSpan = axisRange ? Math.max(1, axisRange.endMin - axisRange.startMin) : 1; - /** H5:列宽适中,周六日可右滑 */ - const dayColPx = isMobile ? 88 : 64; - const timeGutterPx = 40; - const weekGridMinW = timeGutterPx + dayColPx * 7; - const emptyGridMinW = dayColPx * 7; + /** H5:七天始终收进同一屏,桌面端保持舒适列宽 */ + const dayColPx = 64; + const timeGutterPx = isMobile ? 32 : 40; + const weekGridMinW = isMobile ? 0 : timeGutterPx + dayColPx * 7; + const emptyGridMinW = isMobile ? 0 : dayColPx * 7; const activeCount = useMemo( () => @@ -736,16 +736,14 @@ export function CoachingSchedulePanel({ nowTs, mode = "coach" }: Props) { ) : ( -
+
{!axisRange ? (
@@ -823,11 +821,9 @@ export function CoachingSchedulePanel({ nowTs, mode = "coach" }: Props) {
From beccf36e71b880f2231896c0696fee494d85c60b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E6=9D=B0?= <17793426456@163.com> Date: Mon, 7 Sep 2026 17:20:44 +0800 Subject: [PATCH 03/17] =?UTF-8?q?fix(coaching):=20=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E9=87=8D=E5=8F=A0=E8=AF=BE=E7=A8=8B=E5=8D=A1=E7=89=87=E5=B1=95?= =?UTF-8?q?=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 同一时段的课程改为压缩重叠显示,点击后展开课程组并将对应课程置于顶层。 Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- web/src/components/CoachingSchedulePanel.tsx | 96 ++++++++++++++------ 1 file changed, 69 insertions(+), 27 deletions(-) diff --git a/web/src/components/CoachingSchedulePanel.tsx b/web/src/components/CoachingSchedulePanel.tsx index c9f8bbc..f84b7ce 100644 --- a/web/src/components/CoachingSchedulePanel.tsx +++ b/web/src/components/CoachingSchedulePanel.tsx @@ -167,6 +167,8 @@ function layoutDayEvents( axisStart: number, axisEnd: number, axisHeightPx: number, + expandedGroupKey: string | null, + raisedId: number | null, ): Array<{ schedule: CoachingWeekSchedule; topPx: number; @@ -174,6 +176,10 @@ function layoutDayEvents( showDetail: boolean; col: number; colCount: number; + overlapGroupKey: string; + overlapIndex: number; + overlapCount: number; + zIndex: number; }> { const span = Math.max(1, axisEnd - axisStart); const raw = items.map((schedule) => { @@ -184,42 +190,62 @@ function layoutDayEvents( e = Math.max(s + 15, Math.min(e, axisEnd)); return { schedule, start: s, end: e }; }); - const sorted = [...raw].sort((a, b) => a.start - b.start || b.end - a.end); - const cols = new Array(sorted.length).fill(0); - for (let i = 0; i < sorted.length; i++) { - const used = new Set(); - for (let j = 0; j < i; j++) { - if (sorted[j].end > sorted[i].start && sorted[j].start < sorted[i].end) { - used.add(cols[j]); + const groups: Array = []; + + for (const event of sorted) { + const matching = groups.filter((group) => + group.some((other) => other.end > event.start && other.start < event.end), + ); + if (matching.length === 0) { + groups.push([event]); + } else { + const target = matching[0]; + target.push(event); + for (const group of matching.slice(1)) { + target.push(...group); + groups.splice(groups.indexOf(group), 1); } } - let c = 0; - while (used.has(c)) c++; - cols[i] = c; } - const colCounts = new Array(sorted.length).fill(1); - for (let i = 0; i < sorted.length; i++) { - let max = cols[i]; - for (let j = 0; j < sorted.length; j++) { - if (sorted[j].end > sorted[i].start && sorted[j].start < sorted[i].end) { - max = Math.max(max, cols[j]); - } - } - colCounts[i] = max + 1; + const groupById = new Map(); + for (const group of groups) { + const ordered = [...group].sort((a, b) => a.start - b.start || b.end - a.end); + const key = ordered.map((event) => event.schedule.id).sort((a, b) => a - b).join("-"); + ordered.forEach((event, index) => { + groupById.set(event.schedule.id, { key, index, count: ordered.length }); + }); } - return sorted.map((ev, i) => { + return sorted.map((ev) => { const topPx = ((ev.start - axisStart) / span) * axisHeightPx; - const heightPx = Math.max(EVENT_MIN_H, ((ev.end - ev.start) / span) * axisHeightPx); + const normalHeightPx = Math.max(EVENT_MIN_H, ((ev.end - ev.start) / span) * axisHeightPx); + const group = groupById.get(ev.schedule.id) || { key: String(ev.schedule.id), index: 0, count: 1 }; + const expanded = group.count > 1 && group.key === expandedGroupKey; + const collapsedHeightPx = Math.max(EVENT_MIN_H, Math.min(normalHeightPx, 64)); + const heightPx = group.count === 1 + ? normalHeightPx + : expanded + ? Math.max(52, Math.min(normalHeightPx, 88)) + : collapsedHeightPx; + const offsetPx = group.count === 1 + ? 0 + : expanded + ? group.index * Math.min(48, Math.max(32, heightPx * 0.55)) + : group.index * 8; + return { schedule: ev.schedule, - topPx, + topPx: topPx + offsetPx, heightPx, - showDetail: heightPx >= 40, - col: cols[i], - colCount: colCounts[i], + showDetail: group.count === 1 ? normalHeightPx >= 40 : expanded, + col: 0, + colCount: 1, + overlapGroupKey: group.key, + overlapIndex: group.index, + overlapCount: group.count, + zIndex: raisedId === ev.schedule.id ? 30 : 10 + group.index, }; }); } @@ -232,6 +258,7 @@ function TimetableBlock({ showDetail, col, colCount, + zIndex, nowTs, onClick, t, @@ -242,6 +269,7 @@ function TimetableBlock({ showDetail: boolean; col: number; colCount: number; + zIndex: number; nowTs: number; onClick: () => void; t: TFunction; @@ -263,12 +291,13 @@ function TimetableBlock({ e.stopPropagation(); onClick(); }} - className={`absolute z-[1] overflow-hidden rounded-lg ${soft.bg} text-left px-1 py-1 shadow-sm active:scale-[0.98] touch-manipulation ${ + className={`absolute overflow-hidden rounded-lg ${soft.bg} text-left px-1 py-1 shadow-sm active:scale-[0.98] touch-manipulation ${ past ? "opacity-90" : "" }`} style={{ top: topPx, height: heightPx, + zIndex, left: `calc(${leftPct}% + 2px)`, width: `calc(${widthPct}% - 4px)`, }} @@ -339,6 +368,8 @@ export function CoachingSchedulePanel({ nowTs, mode = "coach" }: Props) { Record >({}); const [selected, setSelected] = useState(null); + const [expandedOverlapGroup, setExpandedOverlapGroup] = useState(null); + const [raisedOverlapId, setRaisedOverlapId] = useState(null); const [showCellTip, setShowCellTip] = useState(false); const [tipHole, setTipHole] = useState(null); const [tipReady, setTipReady] = useState(false); @@ -893,6 +924,8 @@ export function CoachingSchedulePanel({ nowTs, mode = "coach" }: Props) { axisRange.startMin, axisRange.endMin, axisHeightPx, + expandedOverlapGroup, + raisedOverlapId, ); const nowLocalM = new Date(nowTs).getHours() * 60 + new Date(nowTs).getMinutes(); const todayPastHeightPx = @@ -953,8 +986,17 @@ export function CoachingSchedulePanel({ nowTs, mode = "coach" }: Props) { showDetail={ev.showDetail} col={ev.col} colCount={ev.colCount} + zIndex={ev.zIndex} nowTs={nowTs} - onClick={() => setSelected(ev.schedule)} + onClick={() => { + if (ev.overlapCount > 1 && expandedOverlapGroup !== ev.overlapGroupKey) { + setExpandedOverlapGroup(ev.overlapGroupKey); + setRaisedOverlapId(ev.schedule.id); + return; + } + if (ev.overlapCount > 1) setRaisedOverlapId(ev.schedule.id); + setSelected(ev.schedule); + }} t={t} /> ))} From eebb57c7d6ed780cf2566db4e66e7e3f7bce89cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E6=9D=B0?= <17793426456@163.com> Date: Mon, 7 Sep 2026 17:37:36 +0800 Subject: [PATCH 04/17] =?UTF-8?q?fix(coaching):=20=E9=99=90=E5=88=B6?= =?UTF-8?q?=E5=86=B2=E7=AA=81=E8=AF=BE=E7=A8=8B=E5=8D=A1=E7=89=87=E5=A0=86?= =?UTF-8?q?=E5=8F=A0=E8=8C=83=E5=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 仅对存在时间交集的课程启用堆叠,并为大量冲突课程增加最大宽度和折叠计数,避免邻近课程和脏数据撑坏课表布局。 Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- web/src/components/CoachingSchedulePanel.tsx | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/web/src/components/CoachingSchedulePanel.tsx b/web/src/components/CoachingSchedulePanel.tsx index f84b7ce..86d3a7c 100644 --- a/web/src/components/CoachingSchedulePanel.tsx +++ b/web/src/components/CoachingSchedulePanel.tsx @@ -270,6 +270,9 @@ function TimetableBlock({ col: number; colCount: number; zIndex: number; + overlapCount: number; + overlapIndex: number; + overlapExpanded: boolean; nowTs: number; onClick: () => void; t: TFunction; @@ -300,6 +303,7 @@ function TimetableBlock({ zIndex, left: `calc(${leftPct}% + 2px)`, width: `calc(${widthPct}% - 4px)`, + maxWidth: "min(100%, 280px)", }} > @@ -317,6 +321,11 @@ function TimetableBlock({
) : null}
+ {overlapCount > 1 && !overlapExpanded && overlapIndex === overlapCount - 1 ? ( + + +{overlapCount - 1} + + ) : null} ); } @@ -987,6 +996,9 @@ export function CoachingSchedulePanel({ nowTs, mode = "coach" }: Props) { col={ev.col} colCount={ev.colCount} zIndex={ev.zIndex} + overlapCount={ev.overlapCount} + overlapIndex={ev.overlapIndex} + overlapExpanded={expandedOverlapGroup === ev.overlapGroupKey} nowTs={nowTs} onClick={() => { if (ev.overlapCount > 1 && expandedOverlapGroup !== ev.overlapGroupKey) { From ee0fd4554223a05890d7c8ae521b3d3ce258052f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E6=9D=B0?= <17793426456@163.com> Date: Mon, 7 Sep 2026 17:45:57 +0800 Subject: [PATCH 05/17] =?UTF-8?q?fix(coaching):=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E9=87=8D=E5=8F=A0=E5=8D=A1=E7=89=87=20overlapCount=20=E6=9C=AA?= =?UTF-8?q?=E5=AE=9A=E4=B9=89=E5=BC=95=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/src/components/CoachingSchedulePanel.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/web/src/components/CoachingSchedulePanel.tsx b/web/src/components/CoachingSchedulePanel.tsx index 86d3a7c..d4d7460 100644 --- a/web/src/components/CoachingSchedulePanel.tsx +++ b/web/src/components/CoachingSchedulePanel.tsx @@ -259,6 +259,9 @@ function TimetableBlock({ col, colCount, zIndex, + overlapCount, + overlapIndex, + overlapExpanded, nowTs, onClick, t, From 54deaee3c80901a33c554c3b53df3038140ea346 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E6=9D=B0?= <17793426456@163.com> Date: Mon, 7 Sep 2026 17:59:38 +0800 Subject: [PATCH 06/17] =?UTF-8?q?fix(coaching):=20=E7=A7=BB=E9=99=A4?= =?UTF-8?q?=E8=AF=BE=E7=A8=8B=E5=8D=A1=E7=89=87=E9=BB=98=E8=AE=A4=E6=8C=89?= =?UTF-8?q?=E9=92=AE=E8=BE=B9=E6=A1=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/src/components/CoachingSchedulePanel.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/components/CoachingSchedulePanel.tsx b/web/src/components/CoachingSchedulePanel.tsx index d4d7460..2263f26 100644 --- a/web/src/components/CoachingSchedulePanel.tsx +++ b/web/src/components/CoachingSchedulePanel.tsx @@ -297,7 +297,7 @@ function TimetableBlock({ e.stopPropagation(); onClick(); }} - className={`absolute overflow-hidden rounded-lg ${soft.bg} text-left px-1 py-1 shadow-sm active:scale-[0.98] touch-manipulation ${ + className={`absolute overflow-hidden rounded-lg border-0 ${soft.bg} text-left px-1 py-1 shadow-sm active:scale-[0.98] touch-manipulation ${ past ? "opacity-90" : "" }`} style={{ From 3a778ed205dde3ea4bff189f974eba9669874f2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E6=9D=B0?= <17793426456@163.com> Date: Mon, 7 Sep 2026 18:16:21 +0800 Subject: [PATCH 07/17] =?UTF-8?q?fix(coaching):=20=E8=B0=83=E6=95=B4?= =?UTF-8?q?=E8=AF=BE=E7=A8=8B=E5=8D=A1=E7=89=87=E4=B8=BB=E9=A2=98=E8=83=B6?= =?UTF-8?q?=E5=9B=8A=E8=BE=B9=E6=A1=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/src/components/CoachingSchedulePanel.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/web/src/components/CoachingSchedulePanel.tsx b/web/src/components/CoachingSchedulePanel.tsx index 2263f26..93e8764 100644 --- a/web/src/components/CoachingSchedulePanel.tsx +++ b/web/src/components/CoachingSchedulePanel.tsx @@ -82,25 +82,29 @@ const EVENT_MIN_H = 32; /** 时间轴相对可视区再拉高约 1/3,课块更易读 */ const AXIS_HEIGHT_SCALE = 4 / 3; -const STATUS_SOFT: Record = { +const STATUS_SOFT: Record = { scheduled: { bg: "bg-primary/15", text: "text-primary", + border: "border-primary/60", bar: "bg-primary", }, in_progress: { bg: "bg-sky-100", text: "text-sky-700", + border: "border-sky-500/60", bar: "bg-sky-500", }, completed: { bg: "bg-muted", text: "text-muted-foreground", + border: "border-muted-foreground/50", bar: "bg-muted-foreground/40", }, cancelled: { bg: "bg-red-50", text: "text-red-600", + border: "border-red-500/60", bar: "bg-red-500", }, }; @@ -108,6 +112,7 @@ const STATUS_SOFT: Record = { const PAST_SOFT = { bg: "bg-muted", text: "text-muted-foreground", + border: "border-muted-foreground/50", bar: "bg-muted-foreground/40", }; @@ -297,7 +302,7 @@ function TimetableBlock({ e.stopPropagation(); onClick(); }} - className={`absolute overflow-hidden rounded-lg border-0 ${soft.bg} text-left px-1 py-1 shadow-sm active:scale-[0.98] touch-manipulation ${ + className={`absolute overflow-hidden rounded-2xl border ${soft.border} ${soft.bg} text-left px-1 py-1 shadow-sm active:scale-[0.98] touch-manipulation ${ past ? "opacity-90" : "" }`} style={{ From 0bebd599af59e1c2aeda8333dfc4d4279d2107ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E6=9D=B0?= <17793426456@163.com> Date: Mon, 7 Sep 2026 18:24:08 +0800 Subject: [PATCH 08/17] =?UTF-8?q?fix(coaching):=20=E7=A7=BB=E9=99=A4?= =?UTF-8?q?=E5=B7=A6=E4=BE=A7=E8=89=B2=E6=9D=A1=E5=B9=B6=E4=BD=BF=E7=94=A8?= =?UTF-8?q?=E4=B8=BB=E9=A2=98=E8=89=B2=E8=BE=B9=E6=A1=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/src/components/CoachingSchedulePanel.tsx | 25 ++++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/web/src/components/CoachingSchedulePanel.tsx b/web/src/components/CoachingSchedulePanel.tsx index 93e8764..7cbd48d 100644 --- a/web/src/components/CoachingSchedulePanel.tsx +++ b/web/src/components/CoachingSchedulePanel.tsx @@ -86,34 +86,34 @@ const STATUS_SOFT: Record - -
+
{start}{showDetail ? `–${end}` : ""}
From 3375bd27b0601312315080c2c4b22d81a53ade75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E6=9D=B0?= <17793426456@163.com> Date: Mon, 7 Sep 2026 18:33:11 +0800 Subject: [PATCH 09/17] =?UTF-8?q?fix(coaching):=20=E8=AF=BE=E7=A8=8B?= =?UTF-8?q?=E5=8D=A1=E7=89=87=E8=83=8C=E6=99=AF=E6=94=B9=E5=AE=9E=E5=BF=83?= =?UTF-8?q?=E4=B8=BB=E9=A2=98=E8=89=B2=E5=B9=B6=E7=BB=9F=E4=B8=80=E9=BB=91?= =?UTF-8?q?=E8=89=B2=E6=96=87=E5=AD=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/src/components/CoachingSchedulePanel.tsx | 34 +++++++++----------- 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/web/src/components/CoachingSchedulePanel.tsx b/web/src/components/CoachingSchedulePanel.tsx index 7cbd48d..b13bd0e 100644 --- a/web/src/components/CoachingSchedulePanel.tsx +++ b/web/src/components/CoachingSchedulePanel.tsx @@ -84,36 +84,36 @@ const AXIS_HEIGHT_SCALE = 4 / 3; const STATUS_SOFT: Record = { scheduled: { - bg: "bg-primary/15", - text: "text-primary", + bg: "bg-primary", + text: "text-black", border: "border-primary", bar: "bg-primary", }, in_progress: { - bg: "bg-sky-100", - text: "text-sky-700", + bg: "bg-sky-500", + text: "text-black", border: "border-sky-500", bar: "bg-sky-500", }, completed: { - bg: "bg-primary/10", - text: "text-primary/70", - border: "border-primary/40", - bar: "bg-primary/40", + bg: "bg-primary", + text: "text-black", + border: "border-primary", + bar: "bg-primary", }, cancelled: { - bg: "bg-red-50", - text: "text-red-600", + bg: "bg-red-500", + text: "text-black", border: "border-red-500", bar: "bg-red-500", }, }; const PAST_SOFT = { - bg: "bg-primary/10", - text: "text-primary/70", - border: "border-primary/40", - bar: "bg-primary/40", + bg: "bg-primary", + text: "text-black", + border: "border-primary", + bar: "bg-primary", }; /** 计划时段已结束(不含进行中) */ @@ -320,16 +320,14 @@ function TimetableBlock({
{showDetail ? (
{title}
) : null}
{overlapCount > 1 && !overlapExpanded && overlapIndex === overlapCount - 1 ? ( - + +{overlapCount - 1} ) : null} From 4dfcf80808298240c3e9cbd35cf050f73be78f0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E6=9D=B0?= <17793426456@163.com> Date: Mon, 7 Sep 2026 18:44:27 +0800 Subject: [PATCH 10/17] =?UTF-8?q?fix(coaching):=20=E5=8D=A1=E7=89=87?= =?UTF-8?q?=E6=94=B9=E4=B8=BA=E9=BB=91=E8=89=B2=E8=BE=B9=E6=A1=86=E7=81=B0?= =?UTF-8?q?=E8=89=B2=E8=83=8C=E6=99=AF=E5=B9=B6=E6=98=BE=E7=A4=BA=E5=AD=A6?= =?UTF-8?q?=E7=94=9F=E5=A7=93=E5=90=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/src/components/CoachingSchedulePanel.tsx | 48 +++++++++++--------- 1 file changed, 27 insertions(+), 21 deletions(-) diff --git a/web/src/components/CoachingSchedulePanel.tsx b/web/src/components/CoachingSchedulePanel.tsx index b13bd0e..9f651ec 100644 --- a/web/src/components/CoachingSchedulePanel.tsx +++ b/web/src/components/CoachingSchedulePanel.tsx @@ -84,36 +84,36 @@ const AXIS_HEIGHT_SCALE = 4 / 3; const STATUS_SOFT: Record = { scheduled: { - bg: "bg-primary", + bg: "bg-gray-200", text: "text-black", - border: "border-primary", - bar: "bg-primary", + border: "border-black", + bar: "bg-black", }, in_progress: { - bg: "bg-sky-500", + bg: "bg-gray-200", text: "text-black", - border: "border-sky-500", - bar: "bg-sky-500", + border: "border-black", + bar: "bg-black", }, completed: { - bg: "bg-primary", + bg: "bg-gray-200", text: "text-black", - border: "border-primary", - bar: "bg-primary", + border: "border-black", + bar: "bg-black", }, cancelled: { - bg: "bg-red-500", + bg: "bg-gray-200", text: "text-black", - border: "border-red-500", - bar: "bg-red-500", + border: "border-black", + bar: "bg-black", }, }; const PAST_SOFT = { - bg: "bg-primary", + bg: "bg-gray-200", text: "text-black", - border: "border-primary", - bar: "bg-primary", + border: "border-black", + bar: "bg-black", }; /** 计划时段已结束(不含进行中) */ @@ -289,7 +289,8 @@ function TimetableBlock({ const soft = past ? PAST_SOFT : STATUS_SOFT[schedule.status] || STATUS_SOFT.scheduled; - const { title } = lessonDisplay(t, schedule); + const { title, subtitle } = lessonDisplay(t, schedule); + const studentName = subtitle || (schedule.students?.[0]?.trim() ?? ""); const start = schedule.startTime?.slice(0, 5) || ""; const end = schedule.endTime?.slice(0, 5) || ""; const widthPct = 100 / colCount; @@ -319,11 +320,16 @@ function TimetableBlock({ {start}{showDetail ? `–${end}` : ""}
{showDetail ? ( -
- {title} -
+ <> + {studentName ? ( +
+ {studentName} +
+ ) : null} +
+ {title} +
+ ) : null}
{overlapCount > 1 && !overlapExpanded && overlapIndex === overlapCount - 1 ? ( From 041c612fcdbb5ff33a80688c65527d83ea17272e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E6=9D=B0?= <17793426456@163.com> Date: Mon, 7 Sep 2026 18:50:36 +0800 Subject: [PATCH 11/17] =?UTF-8?q?fix(coaching):=20=E7=A7=BB=E9=99=A4?= =?UTF-8?q?=E8=AF=BE=E7=A8=8B=E5=8D=A1=E7=89=87=E9=80=8F=E6=98=8E=E5=BA=A6?= =?UTF-8?q?=E7=A1=AE=E4=BF=9D=E5=AE=9E=E5=BF=83=E8=A6=86=E7=9B=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/src/components/CoachingSchedulePanel.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/web/src/components/CoachingSchedulePanel.tsx b/web/src/components/CoachingSchedulePanel.tsx index 9f651ec..ea574cb 100644 --- a/web/src/components/CoachingSchedulePanel.tsx +++ b/web/src/components/CoachingSchedulePanel.tsx @@ -303,9 +303,7 @@ function TimetableBlock({ e.stopPropagation(); onClick(); }} - className={`absolute overflow-hidden rounded-2xl border ${soft.border} ${soft.bg} text-left px-1 py-1 shadow-sm active:scale-[0.98] touch-manipulation ${ - past ? "opacity-90" : "" - }`} + className={`absolute overflow-hidden rounded-2xl border ${soft.border} ${soft.bg} text-left px-1 py-1 shadow-sm active:scale-[0.98] touch-manipulation`} style={{ top: topPx, height: heightPx, From a590c500aecae73b4baff0ffe67e99848432bea6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E6=9D=B0?= <17793426456@163.com> Date: Tue, 8 Sep 2026 10:06:00 +0800 Subject: [PATCH 12/17] =?UTF-8?q?fix(coaching):=20=E8=AF=BE=E7=A8=8B?= =?UTF-8?q?=E5=8D=A1=E7=89=87=E6=94=B9=E5=AE=9E=E5=BF=83=E4=B8=BB=E9=A2=98?= =?UTF-8?q?=E8=89=B2=E5=B9=B6=E6=8C=89=E7=8A=B6=E6=80=81=E9=80=82=E9=85=8D?= =?UTF-8?q?=E6=96=87=E5=AD=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 排课课表卡片去掉透明度,背景实心遮盖;文字统一使用 soft.text 按状态自适应,保证在主题色背景上可读。 Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- web/src/components/CoachingSchedulePanel.tsx | 46 ++++++++++---------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/web/src/components/CoachingSchedulePanel.tsx b/web/src/components/CoachingSchedulePanel.tsx index ea574cb..855d76d 100644 --- a/web/src/components/CoachingSchedulePanel.tsx +++ b/web/src/components/CoachingSchedulePanel.tsx @@ -84,36 +84,36 @@ const AXIS_HEIGHT_SCALE = 4 / 3; const STATUS_SOFT: Record = { scheduled: { - bg: "bg-gray-200", - text: "text-black", - border: "border-black", - bar: "bg-black", + bg: "bg-primary", + text: "text-primary-foreground", + border: "border-primary", + bar: "bg-primary", }, in_progress: { - bg: "bg-gray-200", - text: "text-black", - border: "border-black", - bar: "bg-black", + bg: "bg-sky-100", + text: "text-sky-700", + border: "border-sky-500", + bar: "bg-sky-500", }, completed: { - bg: "bg-gray-200", - text: "text-black", - border: "border-black", - bar: "bg-black", + bg: "bg-primary", + text: "text-primary-foreground", + border: "border-primary", + bar: "bg-primary", }, cancelled: { - bg: "bg-gray-200", - text: "text-black", - border: "border-black", - bar: "bg-black", + bg: "bg-red-50", + text: "text-red-600", + border: "border-red-500", + bar: "bg-red-500", }, }; const PAST_SOFT = { - bg: "bg-gray-200", - text: "text-black", - border: "border-black", - bar: "bg-black", + bg: "bg-primary", + text: "text-primary-foreground", + border: "border-primary", + bar: "bg-primary", }; /** 计划时段已结束(不含进行中) */ @@ -320,18 +320,18 @@ function TimetableBlock({ {showDetail ? ( <> {studentName ? ( -
+
{studentName}
) : null} -
+
{title}
) : null}
{overlapCount > 1 && !overlapExpanded && overlapIndex === overlapCount - 1 ? ( - + +{overlapCount - 1} ) : null} From d263b0108335105e475938ba8c6edc7275e70d98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E6=9D=B0?= <17793426456@163.com> Date: Tue, 8 Sep 2026 10:18:49 +0800 Subject: [PATCH 13/17] =?UTF-8?q?fix(coaching):=20=E8=AF=BE=E7=A8=8B?= =?UTF-8?q?=E5=8D=A1=E7=89=87=E6=94=B9=E4=B8=BA=E4=B8=BB=E9=A2=98=E8=89=B2?= =?UTF-8?q?=E8=BE=B9=E6=A1=86=E9=85=8D=E6=9F=94=E5=92=8C=E7=BA=BF=E6=80=A7?= =?UTF-8?q?=E6=B8=90=E5=8F=98=E5=86=85=E5=BA=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 边框使用实心主题色,内部改为 from-primary/15 to-primary/5 的柔和 线性渐变;文字恢复 text-foreground 在浅色渐变上可读。 Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- web/src/components/CoachingSchedulePanel.tsx | 30 ++++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/web/src/components/CoachingSchedulePanel.tsx b/web/src/components/CoachingSchedulePanel.tsx index 855d76d..00a73c9 100644 --- a/web/src/components/CoachingSchedulePanel.tsx +++ b/web/src/components/CoachingSchedulePanel.tsx @@ -84,25 +84,25 @@ const AXIS_HEIGHT_SCALE = 4 / 3; const STATUS_SOFT: Record = { scheduled: { - bg: "bg-primary", - text: "text-primary-foreground", + bg: "bg-gradient-to-br from-primary/15 to-primary/5", + text: "text-primary", border: "border-primary", bar: "bg-primary", }, in_progress: { - bg: "bg-sky-100", + bg: "bg-gradient-to-br from-sky-100 to-sky-50", text: "text-sky-700", border: "border-sky-500", bar: "bg-sky-500", }, completed: { - bg: "bg-primary", - text: "text-primary-foreground", - border: "border-primary", - bar: "bg-primary", + bg: "bg-gradient-to-br from-primary/10 to-primary/5", + text: "text-primary/70", + border: "border-primary/50", + bar: "bg-primary/40", }, cancelled: { - bg: "bg-red-50", + bg: "bg-gradient-to-br from-red-50 to-red-100", text: "text-red-600", border: "border-red-500", bar: "bg-red-500", @@ -110,10 +110,10 @@ const STATUS_SOFT: Record {studentName ? ( -
+
{studentName}
) : null} -
+
{title}
) : null}
{overlapCount > 1 && !overlapExpanded && overlapIndex === overlapCount - 1 ? ( - + +{overlapCount - 1} ) : null} From 25204213ee2bd34d89057a995b4b4d113bedbc37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E6=9D=B0?= <17793426456@163.com> Date: Tue, 8 Sep 2026 10:27:54 +0800 Subject: [PATCH 14/17] =?UTF-8?q?fix(coaching):=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E9=87=8D=E5=8F=A0=E8=AF=BE=E7=A8=8B=E5=8D=A1=E7=89=87=E5=86=85?= =?UTF-8?q?=E5=AE=B9=E8=A2=AB=E9=81=AE=E6=8C=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 增加重叠卡片的堆叠间距并始终显示课程详情,避免上方卡片覆盖下方课程内容。 Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- web/src/components/CoachingSchedulePanel.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/web/src/components/CoachingSchedulePanel.tsx b/web/src/components/CoachingSchedulePanel.tsx index 00a73c9..2921857 100644 --- a/web/src/components/CoachingSchedulePanel.tsx +++ b/web/src/components/CoachingSchedulePanel.tsx @@ -228,7 +228,7 @@ function layoutDayEvents( const normalHeightPx = Math.max(EVENT_MIN_H, ((ev.end - ev.start) / span) * axisHeightPx); const group = groupById.get(ev.schedule.id) || { key: String(ev.schedule.id), index: 0, count: 1 }; const expanded = group.count > 1 && group.key === expandedGroupKey; - const collapsedHeightPx = Math.max(EVENT_MIN_H, Math.min(normalHeightPx, 64)); + const collapsedHeightPx = Math.max(EVENT_MIN_H, Math.min(normalHeightPx, 72)); const heightPx = group.count === 1 ? normalHeightPx : expanded @@ -238,13 +238,13 @@ function layoutDayEvents( ? 0 : expanded ? group.index * Math.min(48, Math.max(32, heightPx * 0.55)) - : group.index * 8; + : group.index * Math.max(48, collapsedHeightPx - 8); return { schedule: ev.schedule, topPx: topPx + offsetPx, heightPx, - showDetail: group.count === 1 ? normalHeightPx >= 40 : expanded, + showDetail: group.count === 1 ? normalHeightPx >= 40 : true, col: 0, colCount: 1, overlapGroupKey: group.key, From 78c106c7cf146ce4ade8c537cc96757c30254d9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E6=9D=B0?= <17793426456@163.com> Date: Tue, 8 Sep 2026 10:36:56 +0800 Subject: [PATCH 15/17] =?UTF-8?q?fix(coaching):=20=E8=AE=A9=E4=B8=8A?= =?UTF-8?q?=E5=B1=82=E8=AF=BE=E7=A8=8B=E5=8D=A1=E7=89=87=E8=A6=86=E7=9B=96?= =?UTF-8?q?=E4=B8=8B=E5=B1=82=E5=86=85=E5=AE=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 恢复重叠卡片的紧凑堆叠,使用不透明柔和渐变遮盖下层,并提高上层卡片的显示层级,避免被覆盖的课程内容透出。 Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- web/src/components/CoachingSchedulePanel.tsx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/web/src/components/CoachingSchedulePanel.tsx b/web/src/components/CoachingSchedulePanel.tsx index 2921857..cc0e106 100644 --- a/web/src/components/CoachingSchedulePanel.tsx +++ b/web/src/components/CoachingSchedulePanel.tsx @@ -84,19 +84,19 @@ const AXIS_HEIGHT_SCALE = 4 / 3; const STATUS_SOFT: Record = { scheduled: { - bg: "bg-gradient-to-br from-primary/15 to-primary/5", + bg: "bg-gradient-to-br from-accent to-background", text: "text-primary", border: "border-primary", bar: "bg-primary", }, in_progress: { - bg: "bg-gradient-to-br from-sky-100 to-sky-50", + bg: "bg-gradient-to-br from-secondary to-background", text: "text-sky-700", border: "border-sky-500", bar: "bg-sky-500", }, completed: { - bg: "bg-gradient-to-br from-primary/10 to-primary/5", + bg: "bg-gradient-to-br from-accent to-background", text: "text-primary/70", border: "border-primary/50", bar: "bg-primary/40", @@ -110,7 +110,7 @@ const STATUS_SOFT: Record 1 && group.key === expandedGroupKey; - const collapsedHeightPx = Math.max(EVENT_MIN_H, Math.min(normalHeightPx, 72)); + const collapsedHeightPx = Math.max(EVENT_MIN_H, Math.min(normalHeightPx, 64)); const heightPx = group.count === 1 ? normalHeightPx : expanded @@ -238,7 +238,7 @@ function layoutDayEvents( ? 0 : expanded ? group.index * Math.min(48, Math.max(32, heightPx * 0.55)) - : group.index * Math.max(48, collapsedHeightPx - 8); + : group.index * 8; return { schedule: ev.schedule, @@ -250,7 +250,7 @@ function layoutDayEvents( overlapGroupKey: group.key, overlapIndex: group.index, overlapCount: group.count, - zIndex: raisedId === ev.schedule.id ? 30 : 10 + group.index, + zIndex: raisedId === ev.schedule.id ? 30 : 10 + group.count - group.index, }; }); } From f8e6b1570bee379330e00afabe5be54c0c853b2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E6=9D=B0?= <17793426456@163.com> Date: Tue, 8 Sep 2026 10:49:45 +0800 Subject: [PATCH 16/17] =?UTF-8?q?fix(coaching):=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E9=87=8D=E5=8F=A0=E8=AF=BE=E7=A8=8B=E5=8D=A1=E7=89=87=E5=86=85?= =?UTF-8?q?=E5=AE=B9=E9=80=8F=E5=87=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 使用不透明底色承载柔和渐变,并调整重叠卡片层级与堆叠间距,确保上层卡片覆盖下层内容。 Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- web/src/components/CoachingSchedulePanel.tsx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/web/src/components/CoachingSchedulePanel.tsx b/web/src/components/CoachingSchedulePanel.tsx index cc0e106..4966c88 100644 --- a/web/src/components/CoachingSchedulePanel.tsx +++ b/web/src/components/CoachingSchedulePanel.tsx @@ -84,19 +84,19 @@ const AXIS_HEIGHT_SCALE = 4 / 3; const STATUS_SOFT: Record = { scheduled: { - bg: "bg-gradient-to-br from-accent to-background", + bg: "bg-gradient-to-br from-primary/15 to-primary/5", text: "text-primary", border: "border-primary", bar: "bg-primary", }, in_progress: { - bg: "bg-gradient-to-br from-secondary to-background", + bg: "bg-gradient-to-br from-sky-100 to-sky-50", text: "text-sky-700", border: "border-sky-500", bar: "bg-sky-500", }, completed: { - bg: "bg-gradient-to-br from-accent to-background", + bg: "bg-gradient-to-br from-primary/10 to-primary/5", text: "text-primary/70", border: "border-primary/50", bar: "bg-primary/40", @@ -110,7 +110,7 @@ const STATUS_SOFT: Record 1 && group.key === expandedGroupKey; - const collapsedHeightPx = Math.max(EVENT_MIN_H, Math.min(normalHeightPx, 64)); + const collapsedHeightPx = Math.max(EVENT_MIN_H, Math.min(normalHeightPx, 72)); const heightPx = group.count === 1 ? normalHeightPx : expanded @@ -238,7 +238,7 @@ function layoutDayEvents( ? 0 : expanded ? group.index * Math.min(48, Math.max(32, heightPx * 0.55)) - : group.index * 8; + : group.index * Math.max(48, collapsedHeightPx - 8); return { schedule: ev.schedule, @@ -250,7 +250,7 @@ function layoutDayEvents( overlapGroupKey: group.key, overlapIndex: group.index, overlapCount: group.count, - zIndex: raisedId === ev.schedule.id ? 30 : 10 + group.count - group.index, + zIndex: raisedId === ev.schedule.id ? 30 : 10 + (group.count - group.index), }; }); } @@ -303,7 +303,7 @@ function TimetableBlock({ e.stopPropagation(); onClick(); }} - className={`absolute overflow-hidden rounded-2xl border ${soft.border} ${soft.bg} text-left px-1 py-1 shadow-sm active:scale-[0.98] touch-manipulation`} + className={`absolute overflow-hidden rounded-2xl border ${soft.border} bg-background ${soft.bg} text-left px-1 py-1 shadow-sm active:scale-[0.98] touch-manipulation`} style={{ top: topPx, height: heightPx, From 24343db34510660cf0bf127011430a515fa9c04e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E6=9D=B0?= <17793426456@163.com> Date: Tue, 8 Sep 2026 10:57:56 +0800 Subject: [PATCH 17/17] =?UTF-8?q?fix(coaching):=20=E9=BB=98=E8=AE=A4?= =?UTF-8?q?=E5=A0=86=E5=8F=A0=E8=AF=BE=E7=A8=8B=E5=8D=A1=E7=89=87=E5=B9=B6?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E7=82=B9=E5=87=BB=E5=B1=95=E5=BC=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 重叠课程默认紧凑堆叠,只显示顶层课程详情;点击后展开显示全部课程内容,并将数量角标放到顶层卡片。 Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- web/src/components/CoachingSchedulePanel.tsx | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/web/src/components/CoachingSchedulePanel.tsx b/web/src/components/CoachingSchedulePanel.tsx index 4966c88..2f06df4 100644 --- a/web/src/components/CoachingSchedulePanel.tsx +++ b/web/src/components/CoachingSchedulePanel.tsx @@ -215,9 +215,11 @@ function layoutDayEvents( } const groupById = new Map(); + const groupStartByKey = new Map(); for (const group of groups) { const ordered = [...group].sort((a, b) => a.start - b.start || b.end - a.end); const key = ordered.map((event) => event.schedule.id).sort((a, b) => a - b).join("-"); + groupStartByKey.set(key, ordered[0].start); ordered.forEach((event, index) => { groupById.set(event.schedule.id, { key, index, count: ordered.length }); }); @@ -238,13 +240,14 @@ function layoutDayEvents( ? 0 : expanded ? group.index * Math.min(48, Math.max(32, heightPx * 0.55)) - : group.index * Math.max(48, collapsedHeightPx - 8); + : group.index * 8; + const collapsedTopPx = ((groupStartByKey.get(group.key) ?? ev.start) - axisStart) / span * axisHeightPx; return { schedule: ev.schedule, - topPx: topPx + offsetPx, + topPx: (!expanded && group.count > 1 ? collapsedTopPx : topPx) + offsetPx, heightPx, - showDetail: group.count === 1 ? normalHeightPx >= 40 : true, + showDetail: group.count === 1 ? normalHeightPx >= 40 : expanded || group.index === 0, col: 0, colCount: 1, overlapGroupKey: group.key, @@ -330,7 +333,7 @@ function TimetableBlock({ ) : null}
- {overlapCount > 1 && !overlapExpanded && overlapIndex === overlapCount - 1 ? ( + {overlapCount > 1 && !overlapExpanded && overlapIndex === 0 ? ( +{overlapCount - 1}