// 任务列表组织(方案 A,提案 2026-09-30):纯函数,供 TaskSidebar 与测试消费。 // 组织是本端视图偏好(P8),不改数据模型——数据源恒为跨项目 tasks(updated_at 倒序)。 import type { Conversation, Project } from './contracts' export type TaskOrg = 'project' | 'timeline' export interface TaskGroup { key: string label: string /** 项目分组时携带(组头「在此项目新建」入口与计数)。 */ projectId?: string tasks: Conversation[] } /** 按项目分组:项目按服务端序(更新倒序),组内任务按列表序(置顶已在服务端排首)。 */ export function groupByProject(tasks: Conversation[], projects: Project[]): TaskGroup[] { const byProject = new Map() for (const t of tasks) { const list = byProject.get(t.projectId) ?? [] list.push(t) byProject.set(t.projectId, list) } const groups: TaskGroup[] = [] for (const p of projects) { const list = byProject.get(p.id) if (!list || list.length === 0) continue groups.push({ key: `prj:${p.id}`, label: p.name, projectId: p.id, tasks: list }) byProject.delete(p.id) } // 项目已删但会话仍在(竞态残留):收进"未分组",不静默丢弃。 for (const [pid, list] of byProject) { groups.push({ key: `prj:${pid}`, label: '未分组', projectId: pid, tasks: list }) } return groups } /** 按时间线分组:今天 / 昨天 / 本周更早 / 更早(按 updatedAt 本地时区)。 */ export function groupByTimeline(tasks: Conversation[], now = Date.now()): TaskGroup[] { const startOfDay = (ts: number) => { const d = new Date(ts); d.setHours(0, 0, 0, 0); return d.getTime() } const today0 = startOfDay(now) const dayMs = 86_400_000 const buckets: TaskGroup[] = [ { key: 'today', label: '今天', tasks: [] }, { key: 'yesterday', label: '昨天', tasks: [] }, { key: 'week', label: '本周更早', tasks: [] }, { key: 'older', label: '更早', tasks: [] }, ] const weekday = (new Date(now).getDay() + 6) % 7 // 周一=0 const weekStart = today0 - weekday * dayMs for (const t of tasks) { const ts = Date.parse(t.updatedAt) if (Number.isNaN(ts)) { buckets[3].tasks.push(t); continue } if (ts >= today0) buckets[0].tasks.push(t) else if (ts >= today0 - dayMs) buckets[1].tasks.push(t) else if (ts >= weekStart) buckets[2].tasks.push(t) else buckets[3].tasks.push(t) } return buckets.filter(g => g.tasks.length > 0) } /** 相对时间(侧栏行尾):刚刚 / N 分钟 / N 小时 / 昨天 / N 天 / 月-日。 */ export function relativeTime(iso: string, now = Date.now()): string { const ts = Date.parse(iso) if (Number.isNaN(ts)) return '' const diff = now - ts if (diff < 60_000) return '刚刚' if (diff < 3_600_000) return `${Math.floor(diff / 60_000)} 分钟` if (diff < 86_400_000) return `${Math.floor(diff / 3_600_000)} 小时` if (diff < 172_800_000) return '昨天' if (diff < 7 * 86_400_000) return `${Math.floor(diff / 86_400_000)} 天` const d = new Date(ts) const nowD = new Date(now) const md = `${d.getMonth() + 1}/${d.getDate()}` return d.getFullYear() === nowD.getFullYear() ? md : `${d.getFullYear()}/${md}` } /** 标题过滤(不区分大小写;空串返回原列表)。 */ export function filterTasks(tasks: Conversation[], q: string): Conversation[] { const needle = q.trim().toLowerCase() if (!needle) return tasks return tasks.filter(t => t.title.toLowerCase().includes(needle)) }