feat: 添加日历批量操作与客户端时区感知功能,优化前端 UI 交互体验

This commit is contained in:
zl-q
2026-03-17 00:13:41 +08:00
parent d3783522e6
commit c26cdbbc27
27 changed files with 1532 additions and 412 deletions
+42 -9
View File
@@ -1,5 +1,6 @@
from __future__ import annotations
from datetime import datetime, timezone
from typing import TYPE_CHECKING, Protocol, Literal
from uuid import UUID
@@ -83,15 +84,18 @@ class ScheduleItemService(BaseService):
) -> ScheduleItemResponse:
user_id = self.require_user_id()
if request.end_at and request.end_at <= request.start_at:
normalized_start_at = self._to_utc_required(request.start_at)
normalized_end_at = self._to_utc(request.end_at)
if normalized_end_at and normalized_end_at <= normalized_start_at:
raise HTTPException(status_code=400, detail="end_at must be after start_at")
data = {
"owner_id": user_id,
"title": request.title,
"description": request.description,
"start_at": request.start_at,
"end_at": request.end_at,
"start_at": normalized_start_at,
"end_at": normalized_end_at,
"timezone": request.timezone,
"extra_metadata": request.metadata.model_dump() if request.metadata else {},
"source_type": source_type,
@@ -168,10 +172,21 @@ class ScheduleItemService(BaseService):
# Validate time range
next_start = update_data.get("start_at", existing.start_at)
next_end = update_data.get("end_at", existing.end_at)
if next_end is not None and next_end <= next_start:
raise HTTPException(
status_code=400, detail="end_at must be after start_at"
)
if isinstance(next_start, datetime):
next_start = self._to_utc_required(next_start)
update_data["start_at"] = next_start
if isinstance(next_end, datetime):
next_end = self._to_utc(next_end)
update_data["end_at"] = next_end
if next_end is not None:
if not isinstance(next_start, datetime):
raise HTTPException(
status_code=400, detail="start_at must include timezone"
)
if next_end <= next_start:
raise HTTPException(
status_code=400, detail="end_at must be after start_at"
)
if not update_data:
return self._to_response(existing)
@@ -218,13 +233,16 @@ class ScheduleItemService(BaseService):
) -> list[ScheduleItemResponse]:
user_id = self.require_user_id()
if request.end_at <= request.start_at:
normalized_start_at = self._to_utc_required(request.start_at)
normalized_end_at = self._to_utc_required(request.end_at)
if normalized_end_at <= normalized_start_at:
raise HTTPException(status_code=400, detail="end_at must be after start_at")
try:
subscribed_items = (
await self._repository.list_subscribed_items_by_date_range(
user_id, request.start_at, request.end_at
user_id, normalized_start_at, normalized_end_at
)
)
@@ -518,3 +536,18 @@ class ScheduleItemService(BaseService):
if subscriptions:
await self._session.commit()
def _to_utc(self, dt: datetime | None) -> datetime | None:
if dt is None:
return None
if dt.tzinfo is None:
raise HTTPException(
status_code=400, detail="datetime must include timezone"
)
return dt.astimezone(timezone.utc)
def _to_utc_required(self, dt: datetime) -> datetime:
normalized = self._to_utc(dt)
if normalized is None:
raise HTTPException(status_code=400, detail="datetime is required")
return normalized