diff --git a/web/app/dashboard/agent-team-schedules/_components/calendar-date-range.test.mjs b/web/app/dashboard/agent-team-schedules/_components/calendar-date-range.test.mjs
new file mode 100644
index 0000000..8fb2c7c
--- /dev/null
+++ b/web/app/dashboard/agent-team-schedules/_components/calendar-date-range.test.mjs
@@ -0,0 +1,14 @@
+import assert from "node:assert/strict"
+import test from "node:test"
+
+import { addDays, formatWeekTitle, startOfWeek } from "./calendar-date-range.ts"
+
+test("builds Monday-based week range and title", () => {
+ const start = startOfWeek(new Date(2026, 3, 29, 14, 0, 0))
+
+ assert.equal(start.getFullYear(), 2026)
+ assert.equal(start.getMonth(), 3)
+ assert.equal(start.getDate(), 27)
+ assert.equal(formatWeekTitle(start), "2026-04-27 - 2026-05-03")
+ assert.equal(addDays(start, 7).getDate(), 4)
+})
diff --git a/web/app/dashboard/agent-team-schedules/_components/calendar-date-range.ts b/web/app/dashboard/agent-team-schedules/_components/calendar-date-range.ts
new file mode 100644
index 0000000..4d895f4
--- /dev/null
+++ b/web/app/dashboard/agent-team-schedules/_components/calendar-date-range.ts
@@ -0,0 +1,68 @@
+export function startOfDay(date: Date) {
+ const ret = new Date(date)
+ ret.setHours(0, 0, 0, 0)
+ return ret
+}
+
+export function startOfWeek(date: Date) {
+ const ret = startOfDay(date)
+ const day = ret.getDay()
+ const offset = day === 0 ? -6 : 1 - day
+ ret.setDate(ret.getDate() + offset)
+ return ret
+}
+
+export function startOfMonth(date: Date) {
+ const ret = startOfDay(date)
+ ret.setDate(1)
+ return ret
+}
+
+export function startOfMonthCalendar(date: Date) {
+ return startOfWeek(startOfMonth(date))
+}
+
+export function endOfMonthCalendar(date: Date) {
+ const monthEnd = startOfMonth(date)
+ monthEnd.setMonth(monthEnd.getMonth() + 1)
+ const ret = startOfWeek(monthEnd)
+ if (ret.getTime() < monthEnd.getTime()) {
+ ret.setDate(ret.getDate() + 7)
+ }
+ return ret
+}
+
+export function addDays(date: Date, days: number) {
+ const ret = new Date(date)
+ ret.setDate(ret.getDate() + days)
+ return ret
+}
+
+export function addMonths(date: Date, months: number) {
+ const ret = startOfMonth(date)
+ ret.setMonth(ret.getMonth() + months)
+ return ret
+}
+
+export function formatDateTimeValue(date: Date) {
+ const month = String(date.getMonth() + 1).padStart(2, "0")
+ const day = String(date.getDate()).padStart(2, "0")
+ const hour = String(date.getHours()).padStart(2, "0")
+ const minute = String(date.getMinutes()).padStart(2, "0")
+ const second = String(date.getSeconds()).padStart(2, "0")
+ return `${date.getFullYear()}-${month}-${day} ${hour}:${minute}:${second}`
+}
+
+export function formatMonthTitle(monthStart: Date) {
+ return `${monthStart.getFullYear()}年${String(monthStart.getMonth() + 1).padStart(2, "0")}月`
+}
+
+function formatDate(date: Date) {
+ const month = String(date.getMonth() + 1).padStart(2, "0")
+ const day = String(date.getDate()).padStart(2, "0")
+ return `${date.getFullYear()}-${month}-${day}`
+}
+
+export function formatWeekTitle(weekStart: Date) {
+ return `${formatDate(weekStart)} - ${formatDate(addDays(weekStart, 6))}`
+}
diff --git a/web/app/dashboard/agent-team-schedules/_components/calendar.tsx b/web/app/dashboard/agent-team-schedules/_components/calendar.tsx
index a4a3bf7..1368e31 100644
--- a/web/app/dashboard/agent-team-schedules/_components/calendar.tsx
+++ b/web/app/dashboard/agent-team-schedules/_components/calendar.tsx
@@ -18,6 +18,7 @@ const minuteMs = 60 * 1000
const minDurationMs = 15 * minuteMs
type ScheduleCalendarProps = {
+ variant?: "month" | "week"
monthStart: Date
calendarStart: Date
calendarEnd: Date
@@ -218,6 +219,7 @@ function buildCalendarDays(calendarStart: Date, calendarEnd: Date) {
}
export function ScheduleCalendar({
+ variant = "month",
monthStart,
calendarStart,
calendarEnd,
@@ -374,6 +376,187 @@ export function ScheduleCalendar({
)
}
+ function renderDayCell(day: Date, dayIndex: number, options?: { inMonth?: boolean; className?: string; showFullDate?: boolean }) {
+ const date = formatDate(day)
+ const inMonth = options?.inMonth ?? day.getMonth() === monthStart.getMonth()
+ const historical = isHistoricalDay(day)
+ const daySchedules = schedules
+ .filter((item) => intersectsDay(item, day))
+ .sort((a, b) => parseLocalDateTime(a.startAt).getTime() - parseLocalDateTime(b.startAt).getTime())
+ const dayTimeLayout = buildDayTimeLayout(daySchedules, day)
+ return (
+
{
+ if ((event.target as HTMLElement).closest("[data-schedule-block]")) {
+ return
+ }
+ if (historical) {
+ return
+ }
+ handleBlankCellClick(day)
+ }}
+ onKeyDown={(event) => {
+ if (historical) {
+ return
+ }
+ if (event.key === "Enter" || event.key === " ") {
+ event.preventDefault()
+ handleBlankCellClick(day)
+ }
+ }}
+ >
+
+
+
+ {options?.showFullDate ? date : day.getDate()}
+
+ {dayTimeLayout.rangeLabel ? (
+
{dayTimeLayout.rangeLabel}
+ ) : null}
+
+ {historical ? null :
}
+
+
+ {daySchedules.slice(0, 5).map((item) => {
+ const teamName = item.teamName || teams.find((team) => team.id === item.teamId)?.name || `客服组#${item.teamId}`
+ const busy = savingId === item.id
+ const active = interactionPreview?.itemId === item.id
+ const timeLayout = dayTimeLayout.items.get(item.id)
+ const readonly = historical || isHistoricalDay(parseLocalDateTime(item.startAt))
+ return (
+
+
{
+ if (readonly) {
+ event.preventDefault()
+ event.stopPropagation()
+ return
+ }
+ handlePointerDown(event, item, "move")
+ }}
+ onKeyDown={(event) => {
+ if (readonly) {
+ return
+ }
+ if (event.key === "Enter" || event.key === " ") {
+ event.preventDefault()
+ onEdit(item)
+ }
+ }}
+ >
+
{
+ if (readonly) {
+ event.preventDefault()
+ event.stopPropagation()
+ return
+ }
+ handlePointerDown(event, item, "resize", "start")
+ }}
+ >
+
+
+
{
+ if (readonly) {
+ event.preventDefault()
+ event.stopPropagation()
+ return
+ }
+ handlePointerDown(event, item, "resize", "end")
+ }}
+ >
+
+
+
{teamName}
+
+ {timeLayout ? `${timeLayout.startLabel} - ${timeLayout.endLabel}` : `${formatTime(item.startAt)} - ${formatTime(item.endAt)}`}
+
+ {item.remark ?
{item.remark}
: null}
+
+
+ )
+ })}
+ {daySchedules.length > 5 ? (
+
还有 {daySchedules.length - 5} 条
+ ) : null}
+
+
+ )
+ }
+
+ if (variant === "week") {
+ return (
+
+
+ {days.map((day, dayIndex) => {
+ const date = formatDate(day)
+ return (
+
+
+
周{weekDayNames[dayIndex] ?? ""}
+
{date.slice(5)}
+
+ {renderDayCell(day, dayIndex, {
+ inMonth: true,
+ className: "min-h-24 border-l-0 border-t-0",
+ showFullDate: false,
+ })}
+
+ )
+ })}
+
+ {interactionPreview ? (
+
+ {interactionPreview.label}
+
+ ) : null}
+
+ )
+ }
+
return (
@@ -384,143 +567,7 @@ export function ScheduleCalendar({
))}
- {days.map((day, dayIndex) => {
- const date = formatDate(day)
- const inMonth = day.getMonth() === monthStart.getMonth()
- const historical = isHistoricalDay(day)
- const daySchedules = schedules
- .filter((item) => intersectsDay(item, day))
- .sort((a, b) => parseLocalDateTime(a.startAt).getTime() - parseLocalDateTime(b.startAt).getTime())
- const dayTimeLayout = buildDayTimeLayout(daySchedules, day)
- return (
-
{
- if ((event.target as HTMLElement).closest("[data-schedule-block]")) {
- return
- }
- if (historical) {
- return
- }
- handleBlankCellClick(day)
- }}
- onKeyDown={(event) => {
- if (historical) {
- return
- }
- if (event.key === "Enter" || event.key === " ") {
- event.preventDefault()
- handleBlankCellClick(day)
- }
- }}
- >
-
-
-
{day.getDate()}
- {dayTimeLayout.rangeLabel ? (
-
{dayTimeLayout.rangeLabel}
- ) : null}
-
- {historical ? null :
}
-
-
- {daySchedules.slice(0, 5).map((item) => {
- const teamName = item.teamName || teams.find((team) => team.id === item.teamId)?.name || `客服组#${item.teamId}`
- const busy = savingId === item.id
- const active = interactionPreview?.itemId === item.id
- const timeLayout = dayTimeLayout.items.get(item.id)
- const readonly = historical || isHistoricalDay(parseLocalDateTime(item.startAt))
- return (
-
-
{
- if (readonly) {
- event.preventDefault()
- event.stopPropagation()
- return
- }
- handlePointerDown(event, item, "move")
- }}
- onKeyDown={(event) => {
- if (readonly) {
- return
- }
- if (event.key === "Enter" || event.key === " ") {
- event.preventDefault()
- onEdit(item)
- }
- }}
- >
-
{
- if (readonly) {
- event.preventDefault()
- event.stopPropagation()
- return
- }
- handlePointerDown(event, item, "resize", "start")
- }}
- >
-
-
-
{
- if (readonly) {
- event.preventDefault()
- event.stopPropagation()
- return
- }
- handlePointerDown(event, item, "resize", "end")
- }}
- >
-
-
-
{teamName}
-
- {timeLayout ? `${timeLayout.startLabel} - ${timeLayout.endLabel}` : `${formatTime(item.startAt)} - ${formatTime(item.endAt)}`}
-
- {item.remark ?
{item.remark}
: null}
-
-
- )
- })}
- {daySchedules.length > 5 ? (
-
还有 {daySchedules.length - 5} 条
- ) : null}
-
-
- )
- })}
+ {days.map((day, dayIndex) => renderDayCell(day, dayIndex, { className: "min-h-36" }))}
{interactionPreview ? (
("calendar")
+ const [viewMode, setViewMode] = useState
("month")
const [teamFilterInput, setTeamFilterInput] = useState("all")
const [teamFilter, setTeamFilter] = useState("all")
const [monthStart, setMonthStart] = useState(() => startOfMonth(new Date()))
+ const [weekStart, setWeekStart] = useState(() => startOfWeek(new Date()))
const [page, setPage] = useState(1)
const [limit, setLimit] = useState(20)
const [loading, setLoading] = useState(true)
@@ -167,10 +122,12 @@ export default function DashboardAgentTeamSchedulesPage() {
const loadCalendarData = useCallback(async () => {
setCalendarLoading(true)
+ const rangeStart = viewMode === "week" ? weekStart : startOfMonthCalendar(monthStart)
+ const rangeEnd = viewMode === "week" ? addDays(weekStart, 7) : endOfMonthCalendar(monthStart)
try {
const data = await fetchAgentTeamScheduleCalendar({
- startAt: formatDateTimeValue(startOfMonthCalendar(monthStart)),
- endAt: formatDateTimeValue(endOfMonthCalendar(monthStart)),
+ startAt: formatDateTimeValue(rangeStart),
+ endAt: formatDateTimeValue(rangeEnd),
teamId: teamFilter === "all" ? undefined : teamFilter,
})
setCalendarItems(data)
@@ -179,7 +136,7 @@ export default function DashboardAgentTeamSchedulesPage() {
} finally {
setCalendarLoading(false)
}
- }, [monthStart, teamFilter])
+ }, [monthStart, teamFilter, viewMode, weekStart])
const loadTeams = useCallback(async () => {
try {
@@ -320,12 +277,20 @@ export default function DashboardAgentTeamSchedulesPage() {
+
- {viewMode === "calendar" ? (
+ {viewMode === "month" ? (
) : null}
- {viewMode === "calendar" ? (
+ {viewMode === "week" ? (
+
+
+
+
+
+ ) : null}
+ {viewMode === "month" ? (
{formatMonthTitle(monthStart)}
) : null}
+ {viewMode === "week" ? (
+
{formatWeekTitle(weekStart)}
+ ) : null}
@@ -387,12 +368,13 @@ export default function DashboardAgentTeamSchedulesPage() {
- {viewMode === "calendar" ? (
+ {viewMode === "month" || viewMode === "week" ? (