From 08ebffb251d56f1ad81f4181526b9feb0f326e16 Mon Sep 17 00:00:00 2001 From: Wenj1NG <69745614+macintoshwan@users.noreply.github.com> Date: Mon, 20 Apr 2026 17:58:56 +0800 Subject: [PATCH 01/13] add schedule --- src/components/App.tsx | 227 +++++++++++++++++- src/components/ScheduleTimeline.tsx | 118 +++++++++ src/styles/base.css | 56 +++++ src/types/index.ts | 3 + .../005_add_task_schedule_columns.sql | 10 + 5 files changed, 412 insertions(+), 2 deletions(-) create mode 100644 src/components/ScheduleTimeline.tsx create mode 100644 supabase/migrations/005_add_task_schedule_columns.sql diff --git a/src/components/App.tsx b/src/components/App.tsx index a7766d9..6057aff 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -21,7 +21,8 @@ import { ProjectDetail } from './ProjectDetail'; import { ProjectEditModal } from './ProjectEditModal'; import { TaskEditModal } from './TaskEditModal'; import { TodoList } from './TodoList'; -import type { CheckinTemplate, TodoItem, Project, TaskStatus } from '../types'; +import { ScheduleTimeline } from './ScheduleTimeline'; +import type { CheckinTemplate, TodoItem, Project, TaskStatus, Task } from '../types'; import { useProjectsStore } from '../store/useProjectsStore'; import { deriveTaskStatus } from '../types'; import { useTaskActions } from '../hooks/useActions'; @@ -41,7 +42,7 @@ export default function App() { // 从 store 获取最基础的数据 const { optimisticInsertProject } = useProjectsStore(); - const { setTaskStatus } = useTaskActions(); + const { createTask, setTaskStatus } = useTaskActions(); const projectIds = useProjectsStore((s) => s.projectIds); const projects = useProjectsStore((s) => s.projects); const tasks = useProjectsStore((s) => s.tasks); @@ -67,6 +68,7 @@ export default function App() { for (const taskId of taskIdsArr) { const task = tasks[taskId]; if (!task) continue; + if (task.is_fixed_event) continue; const status = deriveTaskStatus(task); if (status !== 'done') { items.push({ @@ -110,6 +112,7 @@ export default function App() { // 新建项目浮层 const [isNewProjectOpen, setIsNewProjectOpen] = useState(false); const [isCheckinModalOpen, setIsCheckinModalOpen] = useState(false); + const [isScheduleModalOpen, setIsScheduleModalOpen] = useState(false); const [isTodoProjectPickerOpen, setIsTodoProjectPickerOpen] = useState(false); const [todoCreateTarget, setTodoCreateTarget] = useState(null); const [checkins, setCheckins] = useState([]); @@ -249,6 +252,109 @@ export default function App() { setIsCheckinModalOpen(false); }, []); + const handleOpenSchedule = useCallback(() => { + setIsScheduleModalOpen(true); + }, []); + + const handleCloseSchedule = useCallback(() => { + setIsScheduleModalOpen(false); + }, []); + + const scheduleEvents = useMemo(() => { + const dayStart = new Date(`${todayKey}T00:00:00`); + const dayEnd = new Date(`${todayKey}T23:59:59.999`); + + return Object.values(tasks) + .filter((task): task is Task => Boolean(task && task.is_fixed_event && task.schedule_start_at && task.schedule_end_at)) + .map((task) => { + const rawStart = new Date(task.schedule_start_at!); + const rawEnd = new Date(task.schedule_end_at!); + if (isNaN(rawStart.getTime()) || isNaN(rawEnd.getTime())) return null; + if (rawEnd <= dayStart || rawStart >= dayEnd) return null; + + const clippedStart = rawStart < dayStart ? dayStart : rawStart; + const clippedEnd = rawEnd > dayEnd ? dayEnd : rawEnd; + const startMinute = Math.max(0, Math.floor((clippedStart.getTime() - dayStart.getTime()) / 60_000)); + const endMinute = Math.min(1440, Math.ceil((clippedEnd.getTime() - dayStart.getTime()) / 60_000)); + if (endMinute <= startMinute) return null; + + return { + id: task.id, + name: task.name, + startMinute, + endMinute, + }; + }) + .filter((event): event is { id: number; name: string; startMinute: number; endMinute: number } => event !== null) + .sort((a, b) => a.startMinute - b.startMinute); + }, [tasks, todayKey]); + + const handleCreateScheduleTask = useCallback(async (input: { name: string; startTime: string; endTime: string }) => { + if (!user?.id) { + alert('请先登录'); + return; + } + + const name = input.name.trim(); + if (!name) { + alert('请输入事项名称'); + return; + } + + const startMinute = parseTimeToMinute(input.startTime); + const endMinute = parseTimeToMinute(input.endTime); + if (startMinute === null || endMinute === null) { + alert('请输入正确的时间'); + return; + } + if (endMinute <= startMinute) { + alert('结束时间必须晚于开始时间'); + return; + } + + const overlapped = scheduleEvents.some((event) => !(endMinute <= event.startMinute || startMinute >= event.endMinute)); + if (overlapped) { + alert('该时段与已有日程重叠,请调整后重试'); + return; + } + + const startIso = combineDateAndTimeToIso(todayKey, input.startTime); + const endIso = combineDateAndTimeToIso(todayKey, input.endTime); + if (!startIso || !endIso) { + alert('时间格式无效'); + return; + } + + try { + const scheduleProjectId = await resolveTempTodoProjectId(); + await createTask({ + id: Date.now(), + project_id: scheduleProjectId, + name, + plan_start_date: null, + plan_end_date: null, + plan_duration: null, + actual_start_date: null, + actual_end_date: null, + actual_duration: null, + category: '日程', + bounty: null, + completed: false, + prerequisites: null, + priority: null, + user_id: user.id, + description: null, + is_fixed_event: true, + schedule_start_at: startIso, + schedule_end_at: endIso, + }); + setIsScheduleModalOpen(false); + } catch (error) { + console.error('创建日程占用失败:', error); + alert('创建日程占用失败,请重试'); + } + }, [user?.id, todayKey, scheduleEvents, resolveTempTodoProjectId, createTask]); + const todoProjectCandidates = projectIds .map((id) => projects[id]) .filter((project): project is Project => project !== undefined); @@ -346,6 +452,11 @@ export default function App() { onCreateCheckin={handleOpenCheckin} /> + + {checkinLoading &&
正在加载打卡数据...
} )} + {isScheduleModalOpen && ( + + )} + {isTodoProjectPickerOpen && ( ; + onCreateSchedule: () => void; +} + +function ScheduleSection({ events, onCreateSchedule }: ScheduleSectionProps) { + return ( +
+

今日日程

+ +
+ +
+
+ ); +} + +interface ScheduleCreateModalProps { + onClose: () => void; + onConfirm: (input: { name: string; startTime: string; endTime: string }) => void; +} + +function ScheduleCreateModal({ onClose, onConfirm }: ScheduleCreateModalProps) { + const [name, setName] = useState(''); + const [startTime, setStartTime] = useState('08:00'); + const [endTime, setEndTime] = useState('09:00'); + + const handleConfirm = () => { + onConfirm({ + name, + startTime, + endTime, + }); + }; + + return ( +
+
e.stopPropagation()}> +
+ +
+
+

添加占用时间

+
+ + setName(e.target.value)} + placeholder="例如:高数课" + /> +
+
+
+ + setStartTime(e.target.value)} + /> +
+
+ + setEndTime(e.target.value)} + /> +
+
+
+
+ +
+
+
+ ); +} + function getLocalDateKey() { const now = new Date(); const y = now.getFullYear(); @@ -461,6 +662,28 @@ function getLocalDateKey() { return `${y}-${m}-${d}`; } +function parseTimeToMinute(input: string): number | null { + const match = /^(\d{2}):(\d{2})$/.exec(input); + if (!match) return null; + const h = Number(match[1]); + const m = Number(match[2]); + if (!Number.isInteger(h) || !Number.isInteger(m)) return null; + if (h < 0 || h > 23 || m < 0 || m > 59) return null; + return h * 60 + m; +} + +function combineDateAndTimeToIso(dateKey: string, time: string): string | null { + const minute = parseTimeToMinute(time); + if (minute === null) return null; + const hour = Math.floor(minute / 60); + const min = minute % 60; + const hh = String(hour).padStart(2, '0'); + const mm = String(min).padStart(2, '0'); + const local = new Date(`${dateKey}T${hh}:${mm}:00`); + if (isNaN(local.getTime())) return null; + return local.toISOString(); +} + interface TodoProjectPickerModalProps { projects: Project[]; onClose: () => void; diff --git a/src/components/ScheduleTimeline.tsx b/src/components/ScheduleTimeline.tsx new file mode 100644 index 0000000..3bc0e8f --- /dev/null +++ b/src/components/ScheduleTimeline.tsx @@ -0,0 +1,118 @@ +import { memo, useMemo } from 'react'; + +interface ScheduleEvent { + id: number; + name: string; + startMinute: number; + endMinute: number; +} + +interface ScheduleTimelineProps { + events: ScheduleEvent[]; +} + +type Segment = + | { kind: 'free'; startMinute: number; endMinute: number } + | { kind: 'occupied'; startMinute: number; endMinute: number; id: number; name: string }; + +const MINUTE_HEIGHT = 0.4; + +function minuteToLabel(minute: number): string { + const clamped = Math.max(0, Math.min(1440, minute)); + const h = Math.floor(clamped / 60); + const m = clamped % 60; + const hh = String(h).padStart(2, '0'); + const mm = String(m).padStart(2, '0'); + return `${hh}:${mm}`; +} + +function buildSegments(events: ScheduleEvent[]): Segment[] { + const normalized = [...events] + .map((event) => ({ + ...event, + startMinute: Math.max(0, Math.min(1440, event.startMinute)), + endMinute: Math.max(0, Math.min(1440, event.endMinute)), + })) + .filter((event) => event.endMinute > event.startMinute) + .sort((a, b) => a.startMinute - b.startMinute); + + const segments: Segment[] = []; + let cursor = 0; + + for (const event of normalized) { + if (event.startMinute > cursor) { + segments.push({ + kind: 'free', + startMinute: cursor, + endMinute: event.startMinute, + }); + } + + segments.push({ + kind: 'occupied', + startMinute: event.startMinute, + endMinute: event.endMinute, + id: event.id, + name: event.name, + }); + + cursor = Math.max(cursor, event.endMinute); + } + + if (cursor < 1440) { + segments.push({ + kind: 'free', + startMinute: cursor, + endMinute: 1440, + }); + } + + if (segments.length === 0) { + segments.push({ + kind: 'free', + startMinute: 0, + endMinute: 1440, + }); + } + + return segments; +} + +export const ScheduleTimeline = memo(function ScheduleTimeline({ events }: ScheduleTimelineProps) { + const segments = useMemo(() => buildSegments(events), [events]); + + return ( +
+ {segments.map((segment, index) => { + const duration = segment.endMinute - segment.startMinute; + const height = Math.max(32, duration * MINUTE_HEIGHT); + const start = minuteToLabel(segment.startMinute); + const end = minuteToLabel(segment.endMinute); + + if (segment.kind === 'occupied') { + return ( +
+
{start} - {end}
+
{segment.name}
+
+ ); + } + + return ( +
+
{start} - {end}
+
可用
+
+ ); + })} +
+ ); +}); diff --git a/src/styles/base.css b/src/styles/base.css index f62fe32..a5a123e 100644 --- a/src/styles/base.css +++ b/src/styles/base.css @@ -1053,6 +1053,62 @@ button:hover { padding: 0 var(--spacing-xl) var(--spacing-xl); } +/* ========== 日程模块 ========== */ +.schedule-section { + margin-bottom: var(--spacing-xl); + padding: var(--spacing-lg); + border: var(--border-width-module) solid var(--border-module); + border-radius: var(--radius-large); + background: var(--bg-card); +} + +.schedule-timeline { + display: flex; + flex-direction: column; + margin-top: var(--spacing-md); + border: var(--border-width-module) solid var(--border-module); +} + +.schedule-block { + padding: 6px 10px; + border-bottom: var(--border-width-task) solid var(--border-module); + display: flex; + flex-direction: column; + justify-content: center; + gap: 2px; + overflow: hidden; +} + +.schedule-block:last-child { + border-bottom: none; +} + +.schedule-block-free { + background: rgba(102, 255, 204, 0.2); +} + +.schedule-block-occupied { + background: rgba(255, 68, 68, 0.2); +} + +.schedule-block-time { + font-size: 12px; + opacity: 0.9; +} + +.schedule-block-name { + font-size: var(--font-size-base); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.schedule-time-row { + display: grid; + grid-template-columns: 1fr 1fr; + gap: var(--spacing-md); +} + /* ========== 次要按钮 - 透明底+黄色描边 ========== */ .btn-secondary { background: transparent; diff --git a/src/types/index.ts b/src/types/index.ts index ad0fad2..18476bf 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -41,6 +41,9 @@ export interface Task { priority: TaskPriority | null; // jsonb → structured user_id: string; description: string | null; + is_fixed_event?: boolean; + schedule_start_at?: string | null; + schedule_end_at?: string | null; } /** priority 字段 JSONB 结构 */ diff --git a/supabase/migrations/005_add_task_schedule_columns.sql b/supabase/migrations/005_add_task_schedule_columns.sql new file mode 100644 index 0000000..dd5473e --- /dev/null +++ b/supabase/migrations/005_add_task_schedule_columns.sql @@ -0,0 +1,10 @@ +-- Add fixed schedule event fields to tasks +ALTER TABLE public.tasks + ADD COLUMN IF NOT EXISTS is_fixed_event boolean NOT NULL DEFAULT false, + ADD COLUMN IF NOT EXISTS schedule_start_at timestamp with time zone, + ADD COLUMN IF NOT EXISTS schedule_end_at timestamp with time zone; + +-- Fast filter for daily schedule lookup +CREATE INDEX IF NOT EXISTS idx_tasks_fixed_schedule + ON public.tasks (user_id, schedule_start_at) + WHERE is_fixed_event = true; From 920d74c5c9d00fcb31bea970f5a87d624357877d Mon Sep 17 00:00:00 2001 From: Wenj1NG <69745614+macintoshwan@users.noreply.github.com> Date: Mon, 20 Apr 2026 21:43:26 +0800 Subject: [PATCH 02/13] fix ui --- src/components/App.tsx | 2 +- src/styles/base.css | 28 +++++++++++++++++++++++++++- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/src/components/App.tsx b/src/components/App.tsx index 6057aff..c5259b1 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -610,7 +610,7 @@ function ScheduleCreateModal({ onClose, onConfirm }: ScheduleCreateModalProps) { return (
-
e.stopPropagation()}> +
e.stopPropagation()}>
diff --git a/src/styles/base.css b/src/styles/base.css index a5a123e..2e407c9 100644 --- a/src/styles/base.css +++ b/src/styles/base.css @@ -651,6 +651,21 @@ input[type="text"] { color: var(--text-primary); } +input[type="time"] { + width: 100%; + padding: 12px var(--spacing-md); + border: var(--border-width-module) solid var(--border-module); + border-radius: var(--radius-base); + font-size: var(--font-size-base); + outline: none; + background: var(--bg-page); + color: var(--text-primary); +} + +input[type="time"]:focus { + border-color: var(--color-cyan); +} + /* 隐藏 number 输入框的上下调整按钮 */ input[type="number"]::-webkit-inner-spin-button, input[type="number"]::-webkit-outer-spin-button { @@ -1035,10 +1050,19 @@ button:hover { cursor: not-allowed; } -.checkin-modal { +.modal-container.checkin-modal { max-width: 520px; } +.modal-container.schedule-modal { + max-width: 980px; +} + +.schedule-modal .modal-title { + font-size: var(--font-size-xl); + margin-bottom: 16px; +} + .checkin-modal-header { padding-bottom: 0; } @@ -1067,6 +1091,8 @@ button:hover { flex-direction: column; margin-top: var(--spacing-md); border: var(--border-width-module) solid var(--border-module); + max-height: 520px; + overflow-y: auto; } .schedule-block { From 4b6897c22964631a46d2bd4cd13f9f06003d1f02 Mon Sep 17 00:00:00 2001 From: Wenj1NG <69745614+macintoshwan@users.noreply.github.com> Date: Mon, 20 Apr 2026 22:23:55 +0800 Subject: [PATCH 03/13] fix ui --- src/components/App.tsx | 47 +++++++++++++++++++++++++--- src/components/TodoList.tsx | 30 +++++++++++++----- src/components/TodoListContainer.tsx | 3 ++ src/styles/base.css | 9 ++++++ 4 files changed, 77 insertions(+), 12 deletions(-) diff --git a/src/components/App.tsx b/src/components/App.tsx index c5259b1..bf9d167 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -115,6 +115,7 @@ export default function App() { const [isScheduleModalOpen, setIsScheduleModalOpen] = useState(false); const [isTodoProjectPickerOpen, setIsTodoProjectPickerOpen] = useState(false); const [todoCreateTarget, setTodoCreateTarget] = useState(null); + const [activeTodoTaskId, setActiveTodoTaskId] = useState(null); const [checkins, setCheckins] = useState([]); const [checkinRecords, setCheckinRecords] = useState>({}); const [checkinLoading, setCheckinLoading] = useState(false); @@ -213,6 +214,14 @@ export default function App() { setTodoCreateTarget(null); }, []); + const handleSelectTodoTask = useCallback((taskId: number) => { + setActiveTodoTaskId(taskId); + }, []); + + const handleCloseTodoTaskDetail = useCallback(() => { + setActiveTodoTaskId(null); + }, []); + const resolveTempTodoProjectId = useCallback(async (): Promise => { if (!user?.id) { throw new Error('请先登录'); @@ -452,17 +461,18 @@ export default function App() { onCreateCheckin={handleOpenCheckin} /> - - {checkinLoading &&
正在加载打卡数据...
} + +
@@ -514,6 +524,13 @@ export default function App() { resolveProjectId={todoCreateTarget === 'temp' ? resolveTempTodoProjectId : undefined} /> )} + + {activeTodoTaskId !== null && ( + + )} )} @@ -654,6 +671,26 @@ function ScheduleCreateModal({ onClose, onConfirm }: ScheduleCreateModalProps) { ); } +interface TaskDetailModalProps { + taskId: number; + onClose: () => void; +} + +function TaskDetailModal({ taskId, onClose }: TaskDetailModalProps) { + const task = useProjectsStore((state) => state.tasks[taskId]); + const projectId = task?.project_id ?? -1; + + if (!task) return null; + + return ( + + ); +} + function getLocalDateKey() { const now = new Date(); const y = now.getFullYear(); diff --git a/src/components/TodoList.tsx b/src/components/TodoList.tsx index 6fb2677..ac01d41 100644 --- a/src/components/TodoList.tsx +++ b/src/components/TodoList.tsx @@ -12,12 +12,14 @@ interface TodoListProps { items: TodoItem[]; onCreateTodoTask: () => void; onChangeTodoStatus: (taskId: number, status: TaskStatus) => Promise; + onSelectTodoTask: (taskId: number) => void; } export function TodoList({ items, onCreateTodoTask, onChangeTodoStatus, + onSelectTodoTask, }: TodoListProps) { const handleChangeStatus = useCallback( async (taskId: number, status: TaskStatus) => { @@ -39,7 +41,19 @@ export function TodoList({ ) : (
    {items.map((item) => ( -
  • +
  • onSelectTodoTask(item.id)} + role="button" + tabIndex={0} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + onSelectTodoTask(item.id); + } + }} + >
    {STATUS_OPTIONS.map(({ key, label }) => (
    - - {item.name} - - {item.projectName && ( - {item.projectName} - )} +
    + + {item.name} + + {item.projectName && ( + {item.projectName} + )} +
  • ))}
diff --git a/src/components/TodoListContainer.tsx b/src/components/TodoListContainer.tsx index 0fee93e..362b89b 100644 --- a/src/components/TodoListContainer.tsx +++ b/src/components/TodoListContainer.tsx @@ -8,11 +8,13 @@ import type { TaskStatus } from '../types'; interface TodoListContainerProps { onCreateTodoTask: () => void; onChangeTodoStatus: (taskId: number, status: TaskStatus) => Promise; + onSelectTodoTask: (taskId: number) => void; } export function TodoListContainer({ onCreateTodoTask, onChangeTodoStatus, + onSelectTodoTask, }: TodoListContainerProps) { const projectIds = useProjectsStore(selectProjectIds); const projects = useProjectsStore((state) => state.projects); @@ -52,6 +54,7 @@ export function TodoListContainer({ items={todoItems} onCreateTodoTask={onCreateTodoTask} onChangeTodoStatus={onChangeTodoStatus} + onSelectTodoTask={onSelectTodoTask} /> ); } diff --git a/src/styles/base.css b/src/styles/base.css index 2e407c9..a4b371a 100644 --- a/src/styles/base.css +++ b/src/styles/base.css @@ -2141,6 +2141,7 @@ input[readonly] { background: var(--bg-task); border: var(--border-width-task) solid var(--border-module); border-radius: var(--radius-base); + cursor: pointer; } .todo-checkbox { @@ -2156,6 +2157,14 @@ input[readonly] { font-size: var(--font-size-base); } +.todo-content { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + gap: 2px; +} + .todo-text.completed { text-decoration: line-through; opacity: 0.6; From b6ca97e85997e060559335c9935cbef270a77c2e Mon Sep 17 00:00:00 2001 From: Wenj1NG <69745614+macintoshwan@users.noreply.github.com> Date: Mon, 20 Apr 2026 22:42:41 +0800 Subject: [PATCH 04/13] fix ui --- src/components/App.tsx | 4 ++-- src/styles/base.css | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/App.tsx b/src/components/App.tsx index bf9d167..8fa1b38 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -629,7 +629,7 @@ function ScheduleCreateModal({ onClose, onConfirm }: ScheduleCreateModalProps) {
e.stopPropagation()}>
- +

添加占用时间

@@ -733,7 +733,7 @@ function TodoProjectPickerModal({ projects, onClose, onPick, onPickTemp }: TodoP
e.stopPropagation()}>
- +

选择待办所属项目

diff --git a/src/styles/base.css b/src/styles/base.css index a4b371a..3073404 100644 --- a/src/styles/base.css +++ b/src/styles/base.css @@ -1193,7 +1193,7 @@ button:hover { text-align: center; color: var(--text-primary); margin-bottom: 25px; - font-size: var(--font-size-xxl); + font-size: var(--font-size-xl); display: inline-block; width: 100%; } From 7f52f10f951f13223a83442b2d63d50b9377e5b8 Mon Sep 17 00:00:00 2001 From: Wenj1NG <69745614+macintoshwan@users.noreply.github.com> Date: Mon, 20 Apr 2026 22:49:09 +0800 Subject: [PATCH 05/13] fix ui --- src/styles/base.css | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/styles/base.css b/src/styles/base.css index 3073404..e0b35f4 100644 --- a/src/styles/base.css +++ b/src/styles/base.css @@ -2161,8 +2161,9 @@ input[readonly] { flex: 1; min-width: 0; display: flex; - flex-direction: column; - gap: 2px; + flex-direction: row; + gap: 8px; + align-items: center; } .todo-text.completed { From b1a5e8e1e7fcf6fcad10eecd6946ae403998bbbc Mon Sep 17 00:00:00 2001 From: Wenj1NG <69745614+macintoshwan@users.noreply.github.com> Date: Mon, 20 Apr 2026 23:20:33 +0800 Subject: [PATCH 06/13] fix ui --- src/components/ScheduleTimeline.tsx | 3 ++- src/styles/base.css | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/ScheduleTimeline.tsx b/src/components/ScheduleTimeline.tsx index 3bc0e8f..827aa99 100644 --- a/src/components/ScheduleTimeline.tsx +++ b/src/components/ScheduleTimeline.tsx @@ -90,13 +90,14 @@ export const ScheduleTimeline = memo(function ScheduleTimeline({ events }: Sched const end = minuteToLabel(segment.endMinute); if (segment.kind === 'occupied') { + const showTime = height > 50; // 高度超过 50px 才显示时间 return (
-
{start} - {end}
+ {showTime &&
{start} - {end}
}
{segment.name}
); diff --git a/src/styles/base.css b/src/styles/base.css index e0b35f4..a30958a 100644 --- a/src/styles/base.css +++ b/src/styles/base.css @@ -1124,9 +1124,11 @@ button:hover { .schedule-block-name { font-size: var(--font-size-base); + color: var(--text-primary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; + font-weight: 600; } .schedule-time-row { From 54549aeb12698e098f62b5f38bf8353528eeec84 Mon Sep 17 00:00:00 2001 From: Wenj1NG <69745614+macintoshwan@users.noreply.github.com> Date: Mon, 20 Apr 2026 23:25:40 +0800 Subject: [PATCH 07/13] fix ui --- src/components/ScheduleTimeline.tsx | 6 +++--- src/styles/base.css | 8 +++++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/components/ScheduleTimeline.tsx b/src/components/ScheduleTimeline.tsx index 827aa99..8c34816 100644 --- a/src/components/ScheduleTimeline.tsx +++ b/src/components/ScheduleTimeline.tsx @@ -90,14 +90,14 @@ export const ScheduleTimeline = memo(function ScheduleTimeline({ events }: Sched const end = minuteToLabel(segment.endMinute); if (segment.kind === 'occupied') { - const showTime = height > 50; // 高度超过 50px 才显示时间 + const minHeight = Math.max(64, height); return (
- {showTime &&
{start} - {end}
} +
{start} - {end}
{segment.name}
); diff --git a/src/styles/base.css b/src/styles/base.css index a30958a..7a1df63 100644 --- a/src/styles/base.css +++ b/src/styles/base.css @@ -1125,9 +1125,11 @@ button:hover { .schedule-block-name { font-size: var(--font-size-base); color: var(--text-primary); - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; + white-space: normal; + overflow: visible; + text-overflow: clip; + line-height: 1.2; + word-break: break-word; font-weight: 600; } From b188d32f5cb08073698e6ad1d9510f5ab41573bf Mon Sep 17 00:00:00 2001 From: Wenj1NG <69745614+macintoshwan@users.noreply.github.com> Date: Mon, 20 Apr 2026 23:55:03 +0800 Subject: [PATCH 08/13] fix ui --- src/components/ScheduleTimeline.tsx | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/components/ScheduleTimeline.tsx b/src/components/ScheduleTimeline.tsx index 8c34816..299910f 100644 --- a/src/components/ScheduleTimeline.tsx +++ b/src/components/ScheduleTimeline.tsx @@ -16,6 +16,15 @@ type Segment = | { kind: 'occupied'; startMinute: number; endMinute: number; id: number; name: string }; const MINUTE_HEIGHT = 0.4; +const MIN_DISPLAY_MINUTES = 60; +const MIN_BLOCK_HEIGHT = 64; + +function getDisplayHeight(durationMinutes: number): number { + if (durationMinutes < MIN_DISPLAY_MINUTES) { + return MIN_BLOCK_HEIGHT; + } + return Math.max(32, durationMinutes * MINUTE_HEIGHT); +} function minuteToLabel(minute: number): string { const clamped = Math.max(0, Math.min(1440, minute)); @@ -85,17 +94,16 @@ export const ScheduleTimeline = memo(function ScheduleTimeline({ events }: Sched
{segments.map((segment, index) => { const duration = segment.endMinute - segment.startMinute; - const height = Math.max(32, duration * MINUTE_HEIGHT); + const height = getDisplayHeight(duration); const start = minuteToLabel(segment.startMinute); const end = minuteToLabel(segment.endMinute); if (segment.kind === 'occupied') { - const minHeight = Math.max(64, height); return (
{start} - {end}
{segment.name}
@@ -107,7 +115,7 @@ export const ScheduleTimeline = memo(function ScheduleTimeline({ events }: Sched
{start} - {end}
可用
From b775c72d767cfb67f7654dd11588a4e58fabbc3d Mon Sep 17 00:00:00 2001 From: Wenj1NG <69745614+macintoshwan@users.noreply.github.com> Date: Tue, 21 Apr 2026 00:01:15 +0800 Subject: [PATCH 09/13] fix ui --- src/components/ScheduleTimeline.tsx | 2 +- src/styles/base.css | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/components/ScheduleTimeline.tsx b/src/components/ScheduleTimeline.tsx index 299910f..386197c 100644 --- a/src/components/ScheduleTimeline.tsx +++ b/src/components/ScheduleTimeline.tsx @@ -17,7 +17,7 @@ type Segment = const MINUTE_HEIGHT = 0.4; const MIN_DISPLAY_MINUTES = 60; -const MIN_BLOCK_HEIGHT = 64; +const MIN_BLOCK_HEIGHT = 80; function getDisplayHeight(durationMinutes: number): number { if (durationMinutes < MIN_DISPLAY_MINUTES) { diff --git a/src/styles/base.css b/src/styles/base.css index 7a1df63..98e2ef4 100644 --- a/src/styles/base.css +++ b/src/styles/base.css @@ -1091,17 +1091,16 @@ button:hover { flex-direction: column; margin-top: var(--spacing-md); border: var(--border-width-module) solid var(--border-module); - max-height: 520px; - overflow-y: auto; + overflow: visible; } .schedule-block { - padding: 6px 10px; + padding: 8px 10px; border-bottom: var(--border-width-task) solid var(--border-module); display: flex; flex-direction: column; justify-content: center; - gap: 2px; + gap: 4px; overflow: hidden; } @@ -1118,17 +1117,18 @@ button:hover { } .schedule-block-time { - font-size: 12px; + font-size: 11px; + line-height: 1; opacity: 0.9; } .schedule-block-name { - font-size: var(--font-size-base); + font-size: 18px; color: var(--text-primary); white-space: normal; overflow: visible; text-overflow: clip; - line-height: 1.2; + line-height: 1.05; word-break: break-word; font-weight: 600; } From 8773273a6003ae77c91fff1791789bb8eafdfdff Mon Sep 17 00:00:00 2001 From: Wenj1NG <69745614+macintoshwan@users.noreply.github.com> Date: Tue, 21 Apr 2026 02:30:01 +0800 Subject: [PATCH 10/13] add multi day --- src/components/App.tsx | 100 +++++++++---- src/components/ScheduleTimeline.tsx | 209 +++++++++++++++++++++++----- src/styles/base.css | 59 +++++++- 3 files changed, 309 insertions(+), 59 deletions(-) diff --git a/src/components/App.tsx b/src/components/App.tsx index 8fa1b38..ab19a20 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -116,6 +116,8 @@ export default function App() { const [isTodoProjectPickerOpen, setIsTodoProjectPickerOpen] = useState(false); const [todoCreateTarget, setTodoCreateTarget] = useState(null); const [activeTodoTaskId, setActiveTodoTaskId] = useState(null); + const [scheduleViewDays, setScheduleViewDays] = useState(1); + const [scheduleModalDate, setScheduleModalDate] = useState(null); const [checkins, setCheckins] = useState([]); const [checkinRecords, setCheckinRecords] = useState>({}); const [checkinLoading, setCheckinLoading] = useState(false); @@ -261,17 +263,29 @@ export default function App() { setIsCheckinModalOpen(false); }, []); - const handleOpenSchedule = useCallback(() => { + const handleOpenSchedule = useCallback((dateKey?: string) => { + setScheduleModalDate(dateKey ?? todayKey); setIsScheduleModalOpen(true); - }, []); + }, [todayKey]); const handleCloseSchedule = useCallback(() => { setIsScheduleModalOpen(false); }, []); const scheduleEvents = useMemo(() => { - const dayStart = new Date(`${todayKey}T00:00:00`); - const dayEnd = new Date(`${todayKey}T23:59:59.999`); + // 根据 scheduleViewDays 计算日期范围 + const dates: string[] = []; + for (let i = 0; i < scheduleViewDays; i++) { + const d = new Date(todayKey); + d.setDate(d.getDate() + i); + const y = d.getFullYear(); + const m = String(d.getMonth() + 1).padStart(2, '0'); + const dd = String(d.getDate()).padStart(2, '0'); + dates.push(`${y}-${m}-${dd}`); + } + + const rangeStart = new Date(`${dates[0]}T00:00:00`); + const rangeEnd = new Date(`${dates[dates.length - 1]}T23:59:59.999`); return Object.values(tasks) .filter((task): task is Task => Boolean(task && task.is_fixed_event && task.schedule_start_at && task.schedule_end_at)) @@ -279,12 +293,17 @@ export default function App() { const rawStart = new Date(task.schedule_start_at!); const rawEnd = new Date(task.schedule_end_at!); if (isNaN(rawStart.getTime()) || isNaN(rawEnd.getTime())) return null; - if (rawEnd <= dayStart || rawStart >= dayEnd) return null; + if (rawEnd <= rangeStart || rawStart >= rangeEnd) return null; - const clippedStart = rawStart < dayStart ? dayStart : rawStart; - const clippedEnd = rawEnd > dayEnd ? dayEnd : rawEnd; - const startMinute = Math.max(0, Math.floor((clippedStart.getTime() - dayStart.getTime()) / 60_000)); - const endMinute = Math.min(1440, Math.ceil((clippedEnd.getTime() - dayStart.getTime()) / 60_000)); + // 计算该任务属于哪一天及在该天的位置 + const taskDayKey = rawStart.toISOString().split('T')[0]; + const dayStartForTask = new Date(`${taskDayKey}T00:00:00`); + const dayEndForTask = new Date(`${taskDayKey}T23:59:59.999`); + + const clippedStart = rawStart < dayStartForTask ? dayStartForTask : rawStart; + const clippedEnd = rawEnd > dayEndForTask ? dayEndForTask : rawEnd; + const startMinute = Math.max(0, Math.floor((clippedStart.getTime() - dayStartForTask.getTime()) / 60_000)); + const endMinute = Math.min(1440, Math.ceil((clippedEnd.getTime() - dayStartForTask.getTime()) / 60_000)); if (endMinute <= startMinute) return null; return { @@ -292,13 +311,15 @@ export default function App() { name: task.name, startMinute, endMinute, + dateKey: taskDayKey, + dayIndex: dates.indexOf(taskDayKey), }; }) - .filter((event): event is { id: number; name: string; startMinute: number; endMinute: number } => event !== null) - .sort((a, b) => a.startMinute - b.startMinute); - }, [tasks, todayKey]); + .filter((event): event is { id: number; name: string; startMinute: number; endMinute: number; dateKey: string; dayIndex: number } => event !== null && event.dayIndex >= 0) + .sort((a, b) => a.dayIndex !== b.dayIndex ? a.dayIndex - b.dayIndex : a.startMinute - b.startMinute); + }, [tasks, todayKey, scheduleViewDays]); - const handleCreateScheduleTask = useCallback(async (input: { name: string; startTime: string; endTime: string }) => { + const handleCreateScheduleTask = useCallback(async (input: { name: string; startTime: string; endTime: string; scheduleDate: string }) => { if (!user?.id) { alert('请先登录'); return; @@ -321,14 +342,14 @@ export default function App() { return; } - const overlapped = scheduleEvents.some((event) => !(endMinute <= event.startMinute || startMinute >= event.endMinute)); + const overlapped = scheduleEvents.some((event) => event.dateKey === input.scheduleDate && !(endMinute <= event.startMinute || startMinute >= event.endMinute)); if (overlapped) { alert('该时段与已有日程重叠,请调整后重试'); return; } - const startIso = combineDateAndTimeToIso(todayKey, input.startTime); - const endIso = combineDateAndTimeToIso(todayKey, input.endTime); + const startIso = combineDateAndTimeToIso(input.scheduleDate, input.startTime); + const endIso = combineDateAndTimeToIso(input.scheduleDate, input.endTime); if (!startIso || !endIso) { alert('时间格式无效'); return; @@ -473,6 +494,9 @@ export default function App() {
@@ -503,6 +527,7 @@ export default function App() { )} @@ -589,17 +614,33 @@ function CheckinSection({ templates, records, todayKey, onCheckin, onCreateCheck } interface ScheduleSectionProps { - events: Array<{ id: number; name: string; startMinute: number; endMinute: number }>; - onCreateSchedule: () => void; + events: Array<{ id: number; name: string; startMinute: number; endMinute: number; dateKey: string; dayIndex: number }>; + onCreateSchedule: (dateKey?: string) => void; + viewDays: number; + onViewDaysChange: (days: number) => void; + todayKey: string; } -function ScheduleSection({ events, onCreateSchedule }: ScheduleSectionProps) { +function ScheduleSection({ events, onCreateSchedule, viewDays, onViewDaysChange, todayKey }: ScheduleSectionProps) { return (
-

今日日程

- +
+

今日日程

+
+ {[1, 3, 5, 7].map((days) => ( + + ))} +
+
+
-
@@ -609,11 +650,13 @@ function ScheduleSection({ events, onCreateSchedule }: ScheduleSectionProps) { interface ScheduleCreateModalProps { onClose: () => void; - onConfirm: (input: { name: string; startTime: string; endTime: string }) => void; + onConfirm: (input: { name: string; startTime: string; endTime: string; scheduleDate: string }) => void; + defaultDate?: string; } -function ScheduleCreateModal({ onClose, onConfirm }: ScheduleCreateModalProps) { +function ScheduleCreateModal({ onClose, onConfirm, defaultDate }: ScheduleCreateModalProps) { const [name, setName] = useState(''); + const [scheduleDate, setScheduleDate] = useState(defaultDate ?? new Date().toISOString().split('T')[0]); const [startTime, setStartTime] = useState('08:00'); const [endTime, setEndTime] = useState('09:00'); @@ -622,6 +665,7 @@ function ScheduleCreateModal({ onClose, onConfirm }: ScheduleCreateModalProps) { name, startTime, endTime, + scheduleDate, }); }; @@ -642,6 +686,14 @@ function ScheduleCreateModal({ onClose, onConfirm }: ScheduleCreateModalProps) { placeholder="例如:高数课" />
+
+ + setScheduleDate(e.target.value)} + /> +
diff --git a/src/components/ScheduleTimeline.tsx b/src/components/ScheduleTimeline.tsx index 386197c..325c8b5 100644 --- a/src/components/ScheduleTimeline.tsx +++ b/src/components/ScheduleTimeline.tsx @@ -8,7 +8,9 @@ interface ScheduleEvent { } interface ScheduleTimelineProps { - events: ScheduleEvent[]; + events: Array<{ id: number; name: string; startMinute: number; endMinute: number; dateKey: string; dayIndex: number }>; + todayKey: string; + viewDays: number; } type Segment = @@ -87,41 +89,182 @@ function buildSegments(events: ScheduleEvent[]): Segment[] { return segments; } -export const ScheduleTimeline = memo(function ScheduleTimeline({ events }: ScheduleTimelineProps) { - const segments = useMemo(() => buildSegments(events), [events]); +function buildSegmentsForDay(events: Array<{ startMinute: number; endMinute: number; id: number; name: string }>): Segment[] { + const normalized = [...events] + .map((event) => ({ + ...event, + startMinute: Math.max(0, Math.min(1440, event.startMinute)), + endMinute: Math.max(0, Math.min(1440, event.endMinute)), + })) + .filter((event) => event.endMinute > event.startMinute) + .sort((a, b) => a.startMinute - b.startMinute); + + const segments: Segment[] = []; + let cursor = 0; + + for (const event of normalized) { + if (event.startMinute > cursor) { + segments.push({ + kind: 'free', + startMinute: cursor, + endMinute: event.startMinute, + }); + } + + segments.push({ + kind: 'occupied', + startMinute: event.startMinute, + endMinute: event.endMinute, + id: event.id, + name: event.name, + }); + + cursor = Math.max(cursor, event.endMinute); + } + + if (cursor < 1440) { + segments.push({ + kind: 'free', + startMinute: cursor, + endMinute: 1440, + }); + } + + if (segments.length === 0) { + segments.push({ + kind: 'free', + startMinute: 0, + endMinute: 1440, + }); + } + + return segments; +} + +function buildSegmentsForDay(events: Array<{ startMinute: number; endMinute: number; id: number; name: string }>): Segment[] { + const normalized = [...events] + .map((event) => ({ + ...event, + startMinute: Math.max(0, Math.min(1440, event.startMinute)), + endMinute: Math.max(0, Math.min(1440, event.endMinute)), + })) + .filter((event) => event.endMinute > event.startMinute) + .sort((a, b) => a.startMinute - b.startMinute); + + const segments: Segment[] = []; + let cursor = 0; + + for (const event of normalized) { + if (event.startMinute > cursor) { + segments.push({ + kind: 'free', + startMinute: cursor, + endMinute: event.startMinute, + }); + } + + segments.push({ + kind: 'occupied', + startMinute: event.startMinute, + endMinute: event.endMinute, + id: event.id, + name: event.name, + }); + + cursor = Math.max(cursor, event.endMinute); + } + + if (cursor < 1440) { + segments.push({ + kind: 'free', + startMinute: cursor, + endMinute: 1440, + }); + } + + if (segments.length === 0) { + segments.push({ + kind: 'free', + startMinute: 0, + endMinute: 1440, + }); + } + + return segments; +} + +export const ScheduleTimeline = memo(function ScheduleTimeline({ events, todayKey, viewDays }: ScheduleTimelineProps) { + // 生成日期列表 + const dates = useMemo(() => { + const result: string[] = []; + for (let i = 0; i < viewDays; i++) { + const d = new Date(todayKey); + d.setDate(d.getDate() + i); + const y = d.getFullYear(); + const m = String(d.getMonth() + 1).padStart(2, '0'); + const dd = String(d.getDate()).padStart(2, '0'); + result.push(`${y}-${m}-${dd}`); + } + return result; + }, [todayKey, viewDays]); + + // 按日期分组事件 + const eventsByDate = useMemo(() => { + const grouped: Record = {}; + for (const date of dates) { + grouped[date] = events.filter((e) => e.dateKey === date); + } + return grouped; + }, [events, dates]); + + // 获取日期显示文本 + function formatDateLabel(dateKey: string): string { + const d = new Date(`${dateKey}T00:00:00`); + const weekDay = ['日', '一', '二', '三', '四', '五', '六'][d.getDay()]; + const m = String(d.getMonth() + 1).padStart(2, '0'); + const dd = String(d.getDate()).padStart(2, '0'); + return `${m}/${dd} 周${weekDay}`; + } return ( -
- {segments.map((segment, index) => { - const duration = segment.endMinute - segment.startMinute; - const height = getDisplayHeight(duration); - const start = minuteToLabel(segment.startMinute); - const end = minuteToLabel(segment.endMinute); - - if (segment.kind === 'occupied') { - return ( -
-
{start} - {end}
-
{segment.name}
-
- ); - } - - return ( -
-
{start} - {end}
-
可用
+
+ {dates.map((dateKey) => ( +
+
{formatDateLabel(dateKey)}
+
+ {buildSegmentsForDay(eventsByDate[dateKey] || []).map((segment, index) => { + const duration = segment.endMinute - segment.startMinute; + const height = getDisplayHeight(duration); + const start = minuteToLabel(segment.startMinute); + const end = minuteToLabel(segment.endMinute); + + if (segment.kind === 'occupied') { + return ( +
+
{start} - {end}
+
{segment.name}
+
+ ); + } + + return ( +
+
{start} - {end}
+
可用
+
+ ); + })}
- ); - })} +
+ ))}
); }); diff --git a/src/styles/base.css b/src/styles/base.css index 98e2ef4..9dea35b 100644 --- a/src/styles/base.css +++ b/src/styles/base.css @@ -666,6 +666,21 @@ input[type="time"]:focus { border-color: var(--color-cyan); } +input[type="date"] { + width: 100%; + padding: 12px var(--spacing-md); + border: var(--border-width-module) solid var(--border-module); + border-radius: var(--radius-base); + font-size: var(--font-size-base); + outline: none; + background: var(--bg-page); + color: var(--text-primary); +} + +input[type="date"]:focus { + border-color: var(--color-cyan); +} + /* 隐藏 number 输入框的上下调整按钮 */ input[type="number"]::-webkit-inner-spin-button, input[type="number"]::-webkit-outer-spin-button { @@ -1086,11 +1101,51 @@ button:hover { background: var(--bg-card); } +.schedule-view-btn { + padding: 6px 12px; + font-size: 12px; + background: transparent; + color: var(--text-color); + border: var(--border-width-module) solid var(--border-module); + border-radius: var(--radius-base); + cursor: pointer; + transition: all var(--transition-fast); +} + +.schedule-view-btn.active { + background: var(--color-cyan); + color: var(--color-on-primary); + border-color: var(--color-cyan); +} + +.schedule-view-btn:hover { + opacity: 0.85; +} + +.schedule-timeline-container { + display: grid; + gap: var(--spacing-md); + margin-top: var(--spacing-md); +} + +.schedule-day-column { + border: var(--border-width-module) solid var(--border-module); + border-radius: var(--radius-base); + overflow: hidden; +} + +.schedule-day-header { + padding: 8px 12px; + background: rgba(102, 255, 204, 0.1); + color: var(--text-primary); + font-size: 12px; + font-weight: 600; + border-bottom: var(--border-width-module) solid var(--border-module); +} + .schedule-timeline { display: flex; flex-direction: column; - margin-top: var(--spacing-md); - border: var(--border-width-module) solid var(--border-module); overflow: visible; } From 55714ae01cc12cd56cc175d739ee84d58746643a Mon Sep 17 00:00:00 2001 From: Wenj1NG <69745614+macintoshwan@users.noreply.github.com> Date: Tue, 21 Apr 2026 02:35:50 +0800 Subject: [PATCH 11/13] fix multiday --- src/components/App.tsx | 4 +- src/components/ScheduleTimeline.tsx | 111 ---------------------------- 2 files changed, 2 insertions(+), 113 deletions(-) diff --git a/src/components/App.tsx b/src/components/App.tsx index ab19a20..b4315ce 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -296,7 +296,7 @@ export default function App() { if (rawEnd <= rangeStart || rawStart >= rangeEnd) return null; // 计算该任务属于哪一天及在该天的位置 - const taskDayKey = rawStart.toISOString().split('T')[0]; + const taskDayKey = rawStart.toISOString().slice(0, 10); const dayStartForTask = new Date(`${taskDayKey}T00:00:00`); const dayEndForTask = new Date(`${taskDayKey}T23:59:59.999`); @@ -656,7 +656,7 @@ interface ScheduleCreateModalProps { function ScheduleCreateModal({ onClose, onConfirm, defaultDate }: ScheduleCreateModalProps) { const [name, setName] = useState(''); - const [scheduleDate, setScheduleDate] = useState(defaultDate ?? new Date().toISOString().split('T')[0]); + const [scheduleDate, setScheduleDate] = useState(defaultDate ?? new Date().toISOString().slice(0, 10)); const [startTime, setStartTime] = useState('08:00'); const [endTime, setEndTime] = useState('09:00'); diff --git a/src/components/ScheduleTimeline.tsx b/src/components/ScheduleTimeline.tsx index 325c8b5..3477795 100644 --- a/src/components/ScheduleTimeline.tsx +++ b/src/components/ScheduleTimeline.tsx @@ -1,12 +1,5 @@ import { memo, useMemo } from 'react'; -interface ScheduleEvent { - id: number; - name: string; - startMinute: number; - endMinute: number; -} - interface ScheduleTimelineProps { events: Array<{ id: number; name: string; startMinute: number; endMinute: number; dateKey: string; dayIndex: number }>; todayKey: string; @@ -37,110 +30,6 @@ function minuteToLabel(minute: number): string { return `${hh}:${mm}`; } -function buildSegments(events: ScheduleEvent[]): Segment[] { - const normalized = [...events] - .map((event) => ({ - ...event, - startMinute: Math.max(0, Math.min(1440, event.startMinute)), - endMinute: Math.max(0, Math.min(1440, event.endMinute)), - })) - .filter((event) => event.endMinute > event.startMinute) - .sort((a, b) => a.startMinute - b.startMinute); - - const segments: Segment[] = []; - let cursor = 0; - - for (const event of normalized) { - if (event.startMinute > cursor) { - segments.push({ - kind: 'free', - startMinute: cursor, - endMinute: event.startMinute, - }); - } - - segments.push({ - kind: 'occupied', - startMinute: event.startMinute, - endMinute: event.endMinute, - id: event.id, - name: event.name, - }); - - cursor = Math.max(cursor, event.endMinute); - } - - if (cursor < 1440) { - segments.push({ - kind: 'free', - startMinute: cursor, - endMinute: 1440, - }); - } - - if (segments.length === 0) { - segments.push({ - kind: 'free', - startMinute: 0, - endMinute: 1440, - }); - } - - return segments; -} - -function buildSegmentsForDay(events: Array<{ startMinute: number; endMinute: number; id: number; name: string }>): Segment[] { - const normalized = [...events] - .map((event) => ({ - ...event, - startMinute: Math.max(0, Math.min(1440, event.startMinute)), - endMinute: Math.max(0, Math.min(1440, event.endMinute)), - })) - .filter((event) => event.endMinute > event.startMinute) - .sort((a, b) => a.startMinute - b.startMinute); - - const segments: Segment[] = []; - let cursor = 0; - - for (const event of normalized) { - if (event.startMinute > cursor) { - segments.push({ - kind: 'free', - startMinute: cursor, - endMinute: event.startMinute, - }); - } - - segments.push({ - kind: 'occupied', - startMinute: event.startMinute, - endMinute: event.endMinute, - id: event.id, - name: event.name, - }); - - cursor = Math.max(cursor, event.endMinute); - } - - if (cursor < 1440) { - segments.push({ - kind: 'free', - startMinute: cursor, - endMinute: 1440, - }); - } - - if (segments.length === 0) { - segments.push({ - kind: 'free', - startMinute: 0, - endMinute: 1440, - }); - } - - return segments; -} - function buildSegmentsForDay(events: Array<{ startMinute: number; endMinute: number; id: number; name: string }>): Segment[] { const normalized = [...events] .map((event) => ({ From 0443a15c25753ce1a017ebf0e0b2178f6be9bb24 Mon Sep 17 00:00:00 2001 From: Wenj1NG <69745614+macintoshwan@users.noreply.github.com> Date: Tue, 21 Apr 2026 03:09:12 +0800 Subject: [PATCH 12/13] fix multiday --- src/components/ScheduleTimeline.tsx | 17 +++-------------- src/styles/base.css | 10 ++++++++-- 2 files changed, 11 insertions(+), 16 deletions(-) diff --git a/src/components/ScheduleTimeline.tsx b/src/components/ScheduleTimeline.tsx index 3477795..089001f 100644 --- a/src/components/ScheduleTimeline.tsx +++ b/src/components/ScheduleTimeline.tsx @@ -10,17 +10,6 @@ type Segment = | { kind: 'free'; startMinute: number; endMinute: number } | { kind: 'occupied'; startMinute: number; endMinute: number; id: number; name: string }; -const MINUTE_HEIGHT = 0.4; -const MIN_DISPLAY_MINUTES = 60; -const MIN_BLOCK_HEIGHT = 80; - -function getDisplayHeight(durationMinutes: number): number { - if (durationMinutes < MIN_DISPLAY_MINUTES) { - return MIN_BLOCK_HEIGHT; - } - return Math.max(32, durationMinutes * MINUTE_HEIGHT); -} - function minuteToLabel(minute: number): string { const clamped = Math.max(0, Math.min(1440, minute)); const h = Math.floor(clamped / 60); @@ -123,7 +112,7 @@ export const ScheduleTimeline = memo(function ScheduleTimeline({ events, todayKe
{buildSegmentsForDay(eventsByDate[dateKey] || []).map((segment, index) => { const duration = segment.endMinute - segment.startMinute; - const height = getDisplayHeight(duration); + const flexGrow = Math.max(1, duration); const start = minuteToLabel(segment.startMinute); const end = minuteToLabel(segment.endMinute); @@ -132,7 +121,7 @@ export const ScheduleTimeline = memo(function ScheduleTimeline({ events, todayKe
{start} - {end}
{segment.name}
@@ -144,7 +133,7 @@ export const ScheduleTimeline = memo(function ScheduleTimeline({ events, todayKe
{start} - {end}
可用
diff --git a/src/styles/base.css b/src/styles/base.css index 9dea35b..8c77008 100644 --- a/src/styles/base.css +++ b/src/styles/base.css @@ -1123,15 +1123,20 @@ button:hover { } .schedule-timeline-container { - display: grid; + display: flex; gap: var(--spacing-md); margin-top: var(--spacing-md); + overflow-x: auto; + align-items: stretch; + padding-bottom: 4px; } .schedule-day-column { border: var(--border-width-module) solid var(--border-module); border-radius: var(--radius-base); overflow: hidden; + min-width: 220px; + flex: 1 0 220px; } .schedule-day-header { @@ -1146,7 +1151,8 @@ button:hover { .schedule-timeline { display: flex; flex-direction: column; - overflow: visible; + overflow: hidden; + height: 720px; } .schedule-block { From d70a0aa550f5d342f43805e3b29b713b93b60019 Mon Sep 17 00:00:00 2001 From: Wenj1NG <69745614+macintoshwan@users.noreply.github.com> Date: Thu, 23 Apr 2026 02:35:56 +0800 Subject: [PATCH 13/13] docs: update README with V1 stable status and V2 preview --- README.md | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 75af2bf..7483ee4 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,11 @@ Artemis is a project and task management app built with React, TypeScript, and S ## Project Status -This project is currently in a maintenance pause phase. +Current status: V1 stable is ready for daily use. + +This repository is entering a two-track phase: +- `main`: stable maintenance branch for day-to-day usage +- next branch (planned): V2 architecture exploration for AI-first workflow ## Features @@ -28,4 +32,15 @@ This project is currently in a maintenance pause phase. - Live Demo: https://artemis-b7j.pages.dev/ - GitHub: https://github.com/macintoshwan/Artemis +## Next Step Preview (V2) + +Planned direction for the next iteration: + +- Unify task scheduling, journal-style records, and project context under a cleaner data model +- Keep V1 workflow available while V2 is developed in an isolated branch and Supabase project +- Add AI assistant capabilities for proactive progress check-ins and schedule planning +- Introduce LLM + RAG memory pipeline for context-aware task conversations + +V2 will be developed incrementally and will not break current V1 daily usage. + License: see repository LICENSE.