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(
db: AsyncSession, class_id: int, data: ScheduleCreate
) -> 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(
class_id=class_id,
**data.model_dump(),
**payload,
)
db.add(item)
await db.commit()
await db.refresh(item)
# 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 ""
await create_notifications_for_class(
db, class_id, "schedule", f"新排期: {data.title}",
@ -37,7 +43,13 @@ async def create_schedule(
async def update_schedule(
db: AsyncSession, item: Schedule, data: ScheduleUpdate
) -> 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)
await db.commit()
await db.refresh(item)

View File

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

View File

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