2026-04-07 10:28:31 +08:00
|
|
|
import { computed, onMounted, ref, watch, toRef } from 'vue'
|
2026-04-06 23:48:52 +08:00
|
|
|
import { CornerDownLeft, Database, Sparkles, Sun, ListTodo } from 'lucide-vue-next'
|
2026-04-05 20:45:16 +08:00
|
|
|
import { scheduleCenterApi, type ScheduleCenterDateResponse, type ScheduleCenterDaySummary } from '@/api/scheduleCenter'
|
2026-04-07 10:28:31 +08:00
|
|
|
import type { Conversation } from '@/api/conversation'
|
2026-04-05 20:45:16 +08:00
|
|
|
|
|
|
|
|
export interface SidebarFocusItem {
|
|
|
|
|
id: string; label: string; title: string; meta: string; tone: 'done' | 'doing' | 'pending'
|
|
|
|
|
}
|
|
|
|
|
export interface SidebarNewsItem {
|
|
|
|
|
id: string; title: string; meta: string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const sidebarCollapsedModules = [
|
|
|
|
|
{ id: 'calendar', label: '日历', icon: Sun },
|
|
|
|
|
{ id: 'status', label: '计划', icon: Database },
|
|
|
|
|
{ id: 'focus', label: '重点', icon: Sparkles },
|
2026-04-06 23:48:52 +08:00
|
|
|
{ id: 'kanban', label: '待办', icon: ListTodo },
|
2026-04-05 20:45:16 +08:00
|
|
|
{ id: 'review', label: '复盘', icon: CornerDownLeft },
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
function formatDateKey(date: Date) {
|
2026-04-07 11:18:07 +08:00
|
|
|
const year = date.getUTCFullYear()
|
|
|
|
|
const month = String(date.getUTCMonth() + 1).padStart(2, '0')
|
|
|
|
|
const day = String(date.getUTCDate()).padStart(2, '0')
|
2026-04-05 20:45:16 +08:00
|
|
|
return `${year}-${month}-${day}`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function formatMonthKey(date: Date) {
|
2026-04-07 11:18:07 +08:00
|
|
|
const year = date.getUTCFullYear()
|
|
|
|
|
const month = String(date.getUTCMonth() + 1).padStart(2, '0')
|
2026-04-05 20:45:16 +08:00
|
|
|
return `${year}-${month}`
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-07 10:28:31 +08:00
|
|
|
export function useSidebarPlan(clientTimeRef: { value: Date }, loadDailyDigestFn: () => void, conversationsRef: Conversation[] = []) {
|
2026-04-05 20:45:16 +08:00
|
|
|
const todayPlanDetail = ref<ScheduleCenterDateResponse | null>(null)
|
|
|
|
|
const monthPlanDays = ref<ScheduleCenterDaySummary[]>([])
|
2026-04-07 10:28:31 +08:00
|
|
|
const selectedDate = ref<string | null>(null)
|
|
|
|
|
|
|
|
|
|
// Build a map of date -> has conversation
|
|
|
|
|
const conversationDateMap = computed(() => {
|
|
|
|
|
const map = new Map<string, boolean>()
|
2026-04-07 11:18:07 +08:00
|
|
|
const conversations = Array.isArray(conversationsRef) ? conversationsRef : (conversationsRef.value ?? [])
|
|
|
|
|
conversations.forEach((conv) => {
|
|
|
|
|
const date = new Date(conv.updated_at)
|
|
|
|
|
const dateKey = formatDateKey(date)
|
2026-04-07 10:28:31 +08:00
|
|
|
map.set(dateKey, true)
|
|
|
|
|
})
|
|
|
|
|
return map
|
|
|
|
|
})
|
2026-04-05 20:45:16 +08:00
|
|
|
|
|
|
|
|
const todayDateKey = computed(() => formatDateKey(clientTimeRef.value))
|
|
|
|
|
const monthPlanSummaryMap = computed(() => new Map(monthPlanDays.value.map((item) => [item.date, item])))
|
|
|
|
|
|
|
|
|
|
const calendarCells = computed(() => {
|
2026-04-07 11:18:07 +08:00
|
|
|
const year = clientTimeRef.value.getUTCFullYear()
|
|
|
|
|
const month = clientTimeRef.value.getUTCMonth()
|
|
|
|
|
const daysInMonth = new Date(Date.UTC(year, month + 1, 0)).getUTCDate()
|
|
|
|
|
const firstDayOffset = (new Date(Date.UTC(year, month, 1)).getUTCDay() + 6) % 7
|
|
|
|
|
const today = clientTimeRef.value.getUTCDate()
|
2026-04-07 10:28:31 +08:00
|
|
|
const cells: Array<{ key: string; value: number | null; active: boolean; busy: boolean; selected: boolean; hasConversation: boolean }> = []
|
2026-04-05 20:45:16 +08:00
|
|
|
for (let index = 0; index < firstDayOffset; index += 1) {
|
2026-04-07 10:28:31 +08:00
|
|
|
cells.push({ key: `blank-start-${index}`, value: null, active: false, busy: false, selected: false, hasConversation: false })
|
2026-04-05 20:45:16 +08:00
|
|
|
}
|
|
|
|
|
for (let day = 1; day <= daysInMonth; day += 1) {
|
2026-04-07 11:18:07 +08:00
|
|
|
const monthDate = new Date(Date.UTC(year, month, day))
|
2026-04-05 20:45:16 +08:00
|
|
|
const dateKey = formatDateKey(monthDate)
|
|
|
|
|
const summary = monthPlanSummaryMap.value.get(dateKey)
|
|
|
|
|
const busy = Boolean(summary && (summary.todo_total + summary.task_due_total + summary.goal_total + summary.reminder_total) > 0)
|
2026-04-07 10:28:31 +08:00
|
|
|
const hasConv = conversationDateMap.value.get(dateKey) || false
|
2026-04-07 11:18:07 +08:00
|
|
|
cells.push({ key: dateKey, value: day, active: day === today, busy, selected: dateKey === selectedDate.value, hasConversation: hasConv })
|
2026-04-05 20:45:16 +08:00
|
|
|
}
|
|
|
|
|
while (cells.length % 7 !== 0) {
|
2026-04-07 10:28:31 +08:00
|
|
|
cells.push({ key: `blank-end-${cells.length}`, value: null, active: false, busy: false, selected: false, hasConversation: false })
|
2026-04-05 20:45:16 +08:00
|
|
|
}
|
|
|
|
|
return cells
|
|
|
|
|
})
|
|
|
|
|
|
2026-04-07 11:18:07 +08:00
|
|
|
const calendarYear = computed(() => clientTimeRef.value.getUTCFullYear())
|
|
|
|
|
const calendarMonth = computed(() => clientTimeRef.value.getUTCMonth() + 1)
|
2026-04-06 21:33:45 +08:00
|
|
|
|
2026-04-05 20:45:16 +08:00
|
|
|
const todayPlanCounters = computed(() => {
|
|
|
|
|
const detail = todayPlanDetail.value
|
|
|
|
|
if (!detail) return { done: 0, doing: 0, pending: 0, total: 0, completion: 0 }
|
|
|
|
|
const todoDone = detail.todos.filter((item) => item.is_completed).length
|
|
|
|
|
const todoPending = detail.todos.filter((item) => !item.is_completed).length
|
|
|
|
|
const taskDone = detail.tasks.filter((item) => item.status === 'done').length
|
|
|
|
|
const taskDoing = detail.tasks.filter((item) => item.status === 'in_progress').length
|
|
|
|
|
const taskPending = detail.tasks.filter((item) => item.status === 'todo').length
|
|
|
|
|
const goalDone = detail.goals.filter((item) => item.status === 'done').length
|
|
|
|
|
const goalPending = detail.goals.filter((item) => item.status !== 'done').length
|
|
|
|
|
const reminderDone = detail.reminders.filter((item) => item.status === 'done' || item.is_dismissed).length
|
|
|
|
|
const reminderPending = detail.reminders.filter((item) => item.status !== 'done' && !item.is_dismissed).length
|
|
|
|
|
const done = todoDone + taskDone + goalDone + reminderDone
|
|
|
|
|
const doing = taskDoing
|
|
|
|
|
const pending = todoPending + taskPending + goalPending + reminderPending
|
|
|
|
|
const total = done + doing + pending
|
|
|
|
|
return { done, doing, pending, total, completion: total > 0 ? Math.round((done / total) * 100) : 0 }
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const monthReviewStats = computed(() => monthPlanDays.value.reduce(
|
|
|
|
|
(acc, item) => {
|
|
|
|
|
acc.todoTotal += item.todo_total
|
|
|
|
|
acc.todoCompleted += item.todo_completed
|
|
|
|
|
acc.taskTotal += item.task_due_total
|
|
|
|
|
acc.reminderTotal += item.reminder_total
|
|
|
|
|
acc.goalTotal += item.goal_total
|
|
|
|
|
acc.highPriorityTotal += item.high_priority_total
|
|
|
|
|
if (item.todo_total + item.task_due_total + item.reminder_total + item.goal_total > 0) acc.activeDays += 1
|
|
|
|
|
return acc
|
|
|
|
|
},
|
|
|
|
|
{ todoTotal: 0, todoCompleted: 0, taskTotal: 0, reminderTotal: 0, goalTotal: 0, highPriorityTotal: 0, activeDays: 0 },
|
|
|
|
|
))
|
|
|
|
|
|
2026-04-06 21:33:45 +08:00
|
|
|
const sidebarWeekLabels = [
|
|
|
|
|
{ label: '一', isWeekend: false },
|
|
|
|
|
{ label: '二', isWeekend: false },
|
|
|
|
|
{ label: '三', isWeekend: false },
|
|
|
|
|
{ label: '四', isWeekend: false },
|
|
|
|
|
{ label: '五', isWeekend: false },
|
|
|
|
|
{ label: '六', isWeekend: true },
|
|
|
|
|
{ label: '日', isWeekend: true },
|
|
|
|
|
]
|
2026-04-05 20:45:16 +08:00
|
|
|
|
|
|
|
|
const sidebarStatusHeadline = computed(() => (
|
|
|
|
|
todayPlanCounters.value.total
|
|
|
|
|
? `今日共 ${todayPlanCounters.value.total} 项计划,已完成 ${todayPlanCounters.value.done} 项`
|
2026-04-06 21:33:45 +08:00
|
|
|
: ''
|
2026-04-05 20:45:16 +08:00
|
|
|
))
|
|
|
|
|
|
|
|
|
|
const sidebarStatusBreakdown = computed(() => [
|
2026-04-06 23:48:52 +08:00
|
|
|
{ key: 'done', label: 'Completed', value: todayPlanCounters.value.done, tone: 'done' },
|
|
|
|
|
{ key: 'doing', label: 'In Progress', value: todayPlanCounters.value.doing, tone: 'doing' },
|
|
|
|
|
{ key: 'pending', label: 'Pending', value: todayPlanCounters.value.pending, tone: 'pending' },
|
|
|
|
|
{ key: 'total', label: 'Total', value: todayPlanCounters.value.total, tone: 'total' },
|
2026-04-05 20:45:16 +08:00
|
|
|
])
|
|
|
|
|
|
2026-04-06 21:33:45 +08:00
|
|
|
// 模拟数据 - 用于测试滑动条
|
|
|
|
|
const mockFocusItems: SidebarFocusItem[] = [
|
|
|
|
|
{ id: 'mock-1', label: '任务', title: '完成用户登录模块开发', meta: '处理中', tone: 'doing' },
|
|
|
|
|
{ id: 'mock-2', label: '任务', title: '修复首页加载慢的问题', meta: '待启动', tone: 'pending' },
|
|
|
|
|
{ id: 'mock-3', label: '目标', title: '本周完成核心功能上线', meta: '今日目标推进', tone: 'doing' },
|
|
|
|
|
{ id: 'mock-4', label: '待办', title: '整理本周工作报告', meta: '手动记录', tone: 'done' },
|
|
|
|
|
{ id: 'mock-5', label: '任务', title: '优化数据库查询性能', meta: '待启动', tone: 'pending' },
|
|
|
|
|
{ id: 'mock-6', label: '提醒', title: '下午3点团队会议', meta: '15:00', tone: 'pending' },
|
|
|
|
|
{ id: 'mock-7', label: '任务', title: 'Code Review 代码审查', meta: '处理中', tone: 'doing' },
|
|
|
|
|
{ id: 'mock-8', label: '待办', title: '更新项目文档', meta: '系统同步', tone: 'done' },
|
|
|
|
|
{ id: 'mock-9', label: '任务', title: '部署测试环境', meta: '待启动', tone: 'pending' },
|
|
|
|
|
{ id: 'mock-10', label: '目标', title: '本月用户增长10%', meta: '今日目标推进', tone: 'pending' },
|
|
|
|
|
{ id: 'mock-11', label: '待办', title: '提交本周周报', meta: '手动记录', tone: 'done' },
|
|
|
|
|
{ id: 'mock-12', label: '任务', title: '接口联调测试', meta: '待启动', tone: 'pending' },
|
|
|
|
|
{ id: 'mock-13', label: '提醒', title: '周三产品评审会', meta: '14:00', tone: 'pending' },
|
|
|
|
|
{ id: 'mock-14', label: '任务', title: '性能优化与监控', meta: '处理中', tone: 'doing' },
|
|
|
|
|
{ id: 'mock-15', label: '待办', title: '备份重要数据', meta: '系统同步', tone: 'done' },
|
|
|
|
|
{ id: 'mock-16', label: '任务', title: '编写单元测试用例', meta: '待启动', tone: 'pending' },
|
|
|
|
|
{ id: 'mock-17', label: '目标', title: '提升系统安全性', meta: '今日目标推进', tone: 'pending' },
|
|
|
|
|
{ id: 'mock-18', label: '待办', title: '清理无用代码文件', meta: '手动记录', tone: 'done' },
|
|
|
|
|
{ id: 'mock-19', label: '任务', title: '配置CI/CD自动化部署', meta: '待启动', tone: 'pending' },
|
|
|
|
|
{ id: 'mock-20', label: '提醒', title: '周五项目复盘会', meta: '10:00', tone: 'pending' },
|
|
|
|
|
]
|
|
|
|
|
|
2026-04-05 20:45:16 +08:00
|
|
|
const sidebarFocusItems = computed<SidebarFocusItem[]>(() => {
|
2026-04-06 21:33:45 +08:00
|
|
|
// 暂时强制返回模拟数据用于测试
|
|
|
|
|
return mockFocusItems
|
2026-04-05 20:45:16 +08:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const sidebarReviewAchievements = computed(() => {
|
|
|
|
|
const stats = monthReviewStats.value
|
|
|
|
|
const items = [
|
|
|
|
|
stats.todoCompleted > 0 ? `累计完成 ${stats.todoCompleted} 项待办,执行节啬已形成闭环。` : '',
|
|
|
|
|
stats.activeDays > 0 ? `本月已有 ${stats.activeDays} 天产生有效计划记录,日程连贯性稳定。` : '',
|
|
|
|
|
stats.highPriorityTotal > 0 ? `高优事项共 ${stats.highPriorityTotal} 项进行中,重点任务没有脱离视野。` : '',
|
|
|
|
|
].filter(Boolean)
|
|
|
|
|
if (items.length > 0) return items.slice(0, 3)
|
|
|
|
|
return ['本月计划数据还在积累中,可以从今日重点开始逐步建立复盘样本。']
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const sidebarReviewReflections = computed(() => {
|
|
|
|
|
const stats = monthReviewStats.value
|
|
|
|
|
const pendingTodoCount = Math.max(stats.todoTotal - stats.todoCompleted, 0)
|
|
|
|
|
const items = [
|
|
|
|
|
pendingTodoCount > 0 ? `仍有 ${pendingTodoCount} 项待办未完成,建议拆成更短的收尾窗口。` : '',
|
|
|
|
|
stats.highPriorityTotal >= 8 ? '高优事项密度偏高,最好提前锁定 1 到 2 个绝对优先级别。' : '',
|
|
|
|
|
stats.reminderTotal >= Math.max(6, stats.activeDays) ? '提醒数量较多,说明执行中断点偏多,适合增加固定回固时段。' : '',
|
|
|
|
|
].filter(Boolean)
|
|
|
|
|
if (items.length > 0) return items.slice(0, 3)
|
|
|
|
|
return ['本月节啬相对稳定,下一步可以把重点事项再收到更清晰的主线。']
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const sidebarFeedItems = computed<SidebarNewsItem[]>(() => [
|
|
|
|
|
{ id: 'fallback-1', title: 'AI 研发节啬继续升温,模型与工作流一体化成为主溜话题。', meta: 'Industry' },
|
|
|
|
|
{ id: 'fallback-2', title: '本地知识库与计划系统的联动体验,正在成为效率工具的新竞争点。', meta: 'Product' },
|
|
|
|
|
{ id: 'fallback-3', title: '建议接入真实 RSS 源后替换当前占位卡片,以获得即时资讯流。', meta: 'System' },
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
const topbarFeedItems = computed(() => sidebarFeedItems.value.length > 0 ? [...sidebarFeedItems.value, ...sidebarFeedItems.value] : [])
|
|
|
|
|
|
|
|
|
|
async function loadSidebarPlanSnapshot(date = new Date()) {
|
|
|
|
|
const dateKey = formatDateKey(date)
|
|
|
|
|
const monthKey = formatMonthKey(date)
|
|
|
|
|
try {
|
|
|
|
|
const [todayResponse, monthResponse] = await Promise.all([
|
|
|
|
|
scheduleCenterApi.date(dateKey),
|
|
|
|
|
scheduleCenterApi.month(monthKey),
|
|
|
|
|
])
|
|
|
|
|
todayPlanDetail.value = todayResponse.data
|
|
|
|
|
monthPlanDays.value = monthResponse.data.days
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.warn('Failed to load sidebar plan snapshot:', err)
|
|
|
|
|
todayPlanDetail.value = null
|
|
|
|
|
monthPlanDays.value = []
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
watch(todayDateKey, (next, previous) => {
|
|
|
|
|
if (next === previous) return
|
|
|
|
|
void loadDailyDigestFn()
|
|
|
|
|
void loadSidebarPlanSnapshot(clientTimeRef.value)
|
|
|
|
|
})
|
|
|
|
|
|
2026-04-07 10:28:31 +08:00
|
|
|
function selectCalendarDate(dateKey: string) {
|
|
|
|
|
selectedDate.value = dateKey
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-05 20:45:16 +08:00
|
|
|
onMounted(() => {
|
|
|
|
|
void loadDailyDigestFn()
|
|
|
|
|
void loadSidebarPlanSnapshot(new Date())
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
todayPlanDetail, monthPlanDays, todayDateKey, monthPlanSummaryMap,
|
2026-04-06 21:33:45 +08:00
|
|
|
calendarCells, calendarYear, calendarMonth, todayPlanCounters, monthReviewStats,
|
2026-04-05 20:45:16 +08:00
|
|
|
sidebarWeekLabels, sidebarStatusHeadline, sidebarStatusBreakdown,
|
|
|
|
|
sidebarFocusItems, sidebarReviewAchievements, sidebarReviewReflections,
|
2026-04-07 10:28:31 +08:00
|
|
|
sidebarFeedItems, topbarFeedItems, loadSidebarPlanSnapshot, sidebarCollapsedModules,
|
|
|
|
|
selectedDate, selectCalendarDate, conversationDateMap
|
2026-04-05 20:45:16 +08:00
|
|
|
}
|
|
|
|
|
}
|