This commit is contained in:
aaron 2026-04-27 11:04:00 +08:00
parent 541a1c5311
commit f6e969f15c
3 changed files with 77 additions and 49 deletions

View File

@ -11,16 +11,22 @@ from app.services.notification_service import create_notifications_for_class
async def create_schedule( async def create_schedule(
db: AsyncSession, class_id: int, data: ScheduleCreate db: AsyncSession, class_id: int, data: ScheduleCreate
) -> Schedule: ) -> Schedule:
payload = data.model_dump()
if payload.get("type") == "deadline" and payload.get("end_time") is not None:
# Deadlines are anchored on the due time only; reuse start_time as the sort/query key.
payload["start_time"] = payload["end_time"]
item = Schedule( item = Schedule(
class_id=class_id, class_id=class_id,
**data.model_dump(), **payload,
) )
db.add(item) db.add(item)
await db.commit() await db.commit()
await db.refresh(item) await db.refresh(item)
# Send notifications + email to class members # Send notifications + email to class members
time_str = data.start_time.strftime("%Y-%m-%d %H:%M") effective_time = payload["end_time"] if payload.get("type") == "deadline" and payload.get("end_time") else payload["start_time"]
time_str = effective_time.strftime("%Y-%m-%d %H:%M")
location_info = f" · {data.location}" if data.location else "" location_info = f" · {data.location}" if data.location else ""
await create_notifications_for_class( await create_notifications_for_class(
db, class_id, "schedule", f"新排期: {data.title}", db, class_id, "schedule", f"新排期: {data.title}",
@ -37,7 +43,13 @@ async def create_schedule(
async def update_schedule( async def update_schedule(
db: AsyncSession, item: Schedule, data: ScheduleUpdate db: AsyncSession, item: Schedule, data: ScheduleUpdate
) -> Schedule: ) -> Schedule:
for field, value in data.model_dump(exclude_unset=True).items(): payload = data.model_dump(exclude_unset=True)
next_type = payload.get("type", item.type)
next_end_time = payload.get("end_time", item.end_time)
if next_type == "deadline" and next_end_time is not None:
payload["start_time"] = next_end_time
for field, value in payload.items():
setattr(item, field, value) setattr(item, field, value)
await db.commit() await db.commit()
await db.refresh(item) await db.refresh(item)

View File

@ -51,6 +51,10 @@ export default function SchedulePage() {
const [formEndTime, setFormEndTime] = useState(""); const [formEndTime, setFormEndTime] = useState("");
const [formLocation, setFormLocation] = useState(""); const [formLocation, setFormLocation] = useState("");
const [formDesc, setFormDesc] = useState(""); const [formDesc, setFormDesc] = useState("");
const isDeadlineType = formType === "deadline";
const getDisplayTime = (item: ScheduleItem) =>
item.type === "deadline" && item.end_time ? item.end_time : item.start_time;
const loadData = useCallback(async () => { const loadData = useCallback(async () => {
if (!activeClassId) { if (!activeClassId) {
@ -82,10 +86,10 @@ export default function SchedulePage() {
void loadData(); void loadData();
}, [loadData]); }, [loadData]);
const getCountdown = (startTime: string) => { const getCountdown = (dateTime: string) => {
const diff = new Date(startTime).getTime() - Date.now(); const diff = new Date(dateTime).getTime() - Date.now();
const days = Math.ceil(diff / (1000 * 60 * 60 * 24)); const days = Math.ceil(diff / (1000 * 60 * 60 * 24));
if (days <= 0) return { text: "已开始", urgent: false }; if (days <= 0) return { text: "已截止", urgent: false };
if (days === 1) return { text: "明天", urgent: true }; if (days === 1) return { text: "明天", urgent: true };
if (days <= 3) return { text: `${days}天后`, urgent: true }; if (days <= 3) return { text: `${days}天后`, urgent: true };
if (days <= 7) return { text: `${days}天后`, urgent: false }; if (days <= 7) return { text: `${days}天后`, urgent: false };
@ -93,28 +97,23 @@ export default function SchedulePage() {
}; };
const handleSubmit = async () => { const handleSubmit = async () => {
if (!formTitle.trim() || !formStartTime) return; const primaryTime = isDeadlineType ? formEndTime : formStartTime;
if (!formTitle.trim() || !primaryTime) return;
setSubmitting(true); setSubmitting(true);
try { try {
const payload = {
type: formType,
title: formTitle,
start_time: isDeadlineType ? primaryTime : formStartTime,
end_time: isDeadlineType ? primaryTime : formEndTime || null,
location: isDeadlineType ? null : formLocation || null,
description: formDesc || null,
};
if (editingId) { if (editingId) {
await putAPI(`/api/schedule/${editingId}`, { await putAPI(`/api/schedule/${editingId}`, payload);
type: formType,
title: formTitle,
start_time: formStartTime,
end_time: formEndTime || null,
location: formLocation || null,
description: formDesc || null,
});
toast.success("排期已更新"); toast.success("排期已更新");
} else { } else {
await postAPI(`/api/schedule/?class_id=${activeClassId}`, { await postAPI(`/api/schedule/?class_id=${activeClassId}`, payload);
type: formType,
title: formTitle,
start_time: formStartTime,
end_time: formEndTime || null,
location: formLocation || null,
description: formDesc || null,
});
toast.success("排期已创建"); toast.success("排期已创建");
} }
setDialogOpen(false); setDialogOpen(false);
@ -137,9 +136,10 @@ export default function SchedulePage() {
const dt = new Date(d); const dt = new Date(d);
return `${dt.getFullYear()}-${pad(dt.getMonth() + 1)}-${pad(dt.getDate())}T${pad(dt.getHours())}:${pad(dt.getMinutes())}`; return `${dt.getFullYear()}-${pad(dt.getMonth() + 1)}-${pad(dt.getDate())}T${pad(dt.getHours())}:${pad(dt.getMinutes())}`;
}; };
setFormStartTime(fmt(item.start_time)); const effectiveTime = item.type === "deadline" && item.end_time ? item.end_time : item.start_time;
setFormEndTime(item.end_time ? fmt(item.end_time) : ""); setFormStartTime(item.type === "deadline" ? "" : fmt(item.start_time));
setFormLocation(item.location || ""); setFormEndTime(item.type === "deadline" ? fmt(effectiveTime) : item.end_time ? fmt(item.end_time) : "");
setFormLocation(item.type === "deadline" ? "" : item.location || "");
setFormDesc(item.description || ""); setFormDesc(item.description || "");
setDialogOpen(true); setDialogOpen(true);
}; };
@ -220,29 +220,42 @@ export default function SchedulePage() {
value={formTitle} value={formTitle}
onChange={(e) => setFormTitle(e.target.value)} onChange={(e) => setFormTitle(e.target.value)}
/> />
<div className="grid grid-cols-2 gap-3"> {isDeadlineType ? (
<div> <div>
<label className="text-sm text-gray-500"></label> <label className="text-sm text-gray-500"></label>
<Input
type="datetime-local"
value={formStartTime}
onChange={(e) => setFormStartTime(e.target.value)}
/>
</div>
<div>
<label className="text-sm text-gray-500"></label>
<Input <Input
type="datetime-local" type="datetime-local"
value={formEndTime} value={formEndTime}
onChange={(e) => setFormEndTime(e.target.value)} onChange={(e) => setFormEndTime(e.target.value)}
/> />
</div> </div>
</div> ) : (
<Input <>
placeholder="地点" <div className="grid grid-cols-2 gap-3">
value={formLocation} <div>
onChange={(e) => setFormLocation(e.target.value)} <label className="text-sm text-gray-500"></label>
/> <Input
type="datetime-local"
value={formStartTime}
onChange={(e) => setFormStartTime(e.target.value)}
/>
</div>
<div>
<label className="text-sm text-gray-500"></label>
<Input
type="datetime-local"
value={formEndTime}
onChange={(e) => setFormEndTime(e.target.value)}
/>
</div>
</div>
<Input
placeholder="地点"
value={formLocation}
onChange={(e) => setFormLocation(e.target.value)}
/>
</>
)}
<Textarea <Textarea
placeholder="描述" placeholder="描述"
value={formDesc} value={formDesc}
@ -270,7 +283,8 @@ export default function SchedulePage() {
<h2 className="text-lg font-semibold mb-3"></h2> <h2 className="text-lg font-semibold mb-3"></h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{upcoming.map((item) => { {upcoming.map((item) => {
const countdown = getCountdown(item.start_time); const displayTime = getDisplayTime(item);
const countdown = getCountdown(displayTime);
const typeInfo = SCHEDULE_TYPES[item.type] || { label: item.type, color: "bg-gray-400" }; const typeInfo = SCHEDULE_TYPES[item.type] || { label: item.type, color: "bg-gray-400" };
return ( return (
<Card key={item.id} className={countdown.urgent ? "border-red-200 bg-red-50/30" : ""}> <Card key={item.id} className={countdown.urgent ? "border-red-200 bg-red-50/30" : ""}>
@ -291,7 +305,7 @@ export default function SchedulePage() {
</div> </div>
<h3 className="font-medium mt-2">{item.title}</h3> <h3 className="font-medium mt-2">{item.title}</h3>
<p className="text-sm text-gray-500 mt-1"> <p className="text-sm text-gray-500 mt-1">
{new Date(item.start_time).toLocaleString("zh-CN", { {new Date(displayTime).toLocaleString("zh-CN", {
month: "short", month: "short",
day: "numeric", day: "numeric",
hour: "2-digit", hour: "2-digit",
@ -358,8 +372,8 @@ export default function SchedulePage() {
<div> <div>
<p className="font-medium">{item.title}</p> <p className="font-medium">{item.title}</p>
<p className="text-sm text-gray-500"> <p className="text-sm text-gray-500">
{new Date(item.start_time).toLocaleString("zh-CN")} {new Date(getDisplayTime(item)).toLocaleString("zh-CN")}
{item.location ? ` · ${item.location}` : ""} {item.type !== "deadline" && item.location ? ` · ${item.location}` : ""}
</p> </p>
</div> </div>
</div> </div>

View File

@ -16,12 +16,14 @@ export function CalendarView({ events, onEventClick }: CalendarViewProps) {
const year = currentDate.getFullYear(); const year = currentDate.getFullYear();
const month = currentDate.getMonth(); const month = currentDate.getMonth();
const getDisplayTime = (event: ScheduleItem) =>
event.type === "deadline" && event.end_time ? event.end_time : event.start_time;
// Get events grouped by date // Get events grouped by date
const eventsByDate = useMemo(() => { const eventsByDate = useMemo(() => {
const map = new Map<string, ScheduleItem[]>(); const map = new Map<string, ScheduleItem[]>();
for (const event of events) { for (const event of events) {
const d = new Date(event.start_time); const d = new Date(getDisplayTime(event));
const key = `${d.getFullYear()}-${d.getMonth()}-${d.getDate()}`; const key = `${d.getFullYear()}-${d.getMonth()}-${d.getDate()}`;
if (!map.has(key)) map.set(key, []); if (!map.has(key)) map.set(key, []);
map.get(key)!.push(event); map.get(key)!.push(event);
@ -166,11 +168,11 @@ export function CalendarView({ events, onEventClick }: CalendarViewProps) {
<div> <div>
<p className="text-sm font-medium text-[#4e1d1a]">{event.title}</p> <p className="text-sm font-medium text-[#4e1d1a]">{event.title}</p>
<p className="text-xs text-[#7a5e4f]"> <p className="text-xs text-[#7a5e4f]">
{new Date(event.start_time).toLocaleTimeString("zh-CN", { {new Date(getDisplayTime(event)).toLocaleTimeString("zh-CN", {
hour: "2-digit", hour: "2-digit",
minute: "2-digit", minute: "2-digit",
})} })}
{event.location ? ` · ${event.location}` : ""} {event.type !== "deadline" && event.location ? ` · ${event.location}` : ""}
</p> </p>
</div> </div>
</div> </div>