Files
JARVIS/docs/superpowers/plans/2026-03-20-daily-todo-plan.md

1185 lines
32 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Daily Todo Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 实现每日待办功能——AI 每天早上自动预生成今日待办(看板未完成任务+前一天对话分析),用户可手动增删改。
**Architecture:**
- 后端FastAPI Router + SQLAlchemy Model + LLM Service 分析对话
- 前端Vue 3 Composition APITodoView 独立页面
- 数据库:新表 `daily_todos`,按日期过滤查询
**Tech Stack:** FastAPI + SQLAlchemy + APScheduler + LLM Service / Vue 3 + Motion + axios
---
## 文件总览
```
backend/
app/
models/
__init__.py # 修改:导出 DailyTodo
todo.py # 新建DailyTodo 模型
schemas/
todo.py # 新建Pydantic Schema
routers/
todo.py # 新建API Router
services/
todo_service.py # 新建AI 分析逻辑
main.py # 修改:注册 todo router
frontend/
src/
api/
todo.ts # 新建API 客户端
views/
TodoView.vue # 新建:待办页面
router/
index.ts # 修改:添加 /todo 路由
components/
SidebarNav.vue # 修改:添加待办菜单
```
---
## Task 1: 后端 - 数据模型
**Files:**
- Create: `backend/app/models/todo.py`
- Modify: `backend/app/models/__init__.py`
- [ ] **Step 1: 创建 `app/models/todo.py`**
参考现有 `app/models/task.py` 的写法:
```python
from sqlalchemy import Column, String, Boolean, DateTime, Enum as SQLEnum
from datetime import datetime
from enum import Enum as PyEnum
from app.models.base import BaseModel
class TodoSource(str, PyEnum):
AI_KANBAN = "ai_kanban"
AI_CHAT = "ai_chat"
MANUAL = "manual"
class DailyTodo(BaseModel):
__tablename__ = "daily_todos"
user_id = Column(String(36), nullable=False, index=True)
title = Column(String(500), nullable=False)
is_completed = Column(Boolean, default=False, nullable=False)
source = Column(SQLEnum(TodoSource), default=TodoSource.MANUAL, nullable=False)
source_detail = Column(String(500), nullable=True)
source_ref_id = Column(String(36), nullable=True)
todo_date = Column(String(10), nullable=False) # YYYY-MM-DD
completed_at = Column(DateTime, nullable=True)
```
- [ ] **Step 2: 修改 `app/models/__init__.py`,添加导出**
在文件末尾添加:
```python
from app.models.todo import DailyTodo
__all__ = [..., "DailyTodo"]
```
- [ ] **Step 3: 提交**
```bash
git add backend/app/models/todo.py backend/app/models/__init__.py
git commit -m "feat(todo): add DailyTodo model"
```
---
## Task 2: 后端 - Schema 定义
**Files:**
- Create: `backend/app/schemas/todo.py`
- [ ] **Step 1: 创建 `app/schemas/todo.py`**
```python
from pydantic import BaseModel
from datetime import datetime
from app.models.todo import TodoSource
class TodoCreate(BaseModel):
title: str
class TodoUpdate(BaseModel):
title: str | None = None
is_completed: bool | None = None
class TodoOut(BaseModel):
id: str
title: str
is_completed: bool
source: TodoSource
source_detail: str | None
todo_date: str
completed_at: datetime | None
created_at: datetime
updated_at: datetime
model_config = {"from_attributes": True}
class TodoListOut(BaseModel):
items: list[TodoOut]
total: int
page: int
page_size: int
class TodoSummaryOut(BaseModel):
date: str
total: int
completed: int
pending: int
```
- [ ] **Step 2: 提交**
```bash
git add backend/app/schemas/todo.py
git commit -m "feat(todo): add Pydantic schemas"
```
---
## Task 3: 后端 - API Router
**Files:**
- Create: `backend/app/routers/todo.py`
- [ ] **Step 1: 创建 `app/routers/todo.py`**
完整实现以下端点:
```python
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, func
from datetime import date
from app.database import get_db
from app.models.todo import DailyTodo, TodoSource
from app.models.user import User
from app.routers.auth import get_current_user
from app.schemas.todo import (
TodoCreate, TodoUpdate, TodoOut, TodoListOut, TodoSummaryOut
)
from app.services.todo_service import generate_daily_todos
router = APIRouter(prefix="/api/todos", tags=["待办"])
@router.get("", response_model=TodoListOut)
async def list_todos(
date_str: str = Query(default=None), # YYYY-MM-DD默认当天
page: int = Query(default=1, ge=1),
page_size: int = Query(default=50, ge=1, le=100),
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
target_date = date_str or date.today().isoformat()
offset = (page - 1) * page_size
# 查询总数
count_q = select(func.count()).select_from(DailyTodo).where(
DailyTodo.user_id == current_user.id,
DailyTodo.todo_date == target_date,
)
total = (await db.execute(count_q)).scalar()
# 查询列表
q = select(DailyTodo).where(
DailyTodo.user_id == current_user.id,
DailyTodo.todo_date == target_date,
).order_by(DailyTodo.created_at.desc()).offset(offset).limit(page_size)
items = (await db.execute(q)).scalars().all()
return TodoListOut(items=items, total=total, page=page, page_size=page_size)
@router.post("", response_model=TodoOut, status_code=201)
async def create_todo(
data: TodoCreate,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
todo = DailyTodo(
user_id=current_user.id,
title=data.title,
source=TodoSource.MANUAL,
todo_date=date.today().isoformat(),
)
db.add(todo)
await db.commit()
await db.refresh(todo)
return todo
@router.patch("/{todo_id}", response_model=TodoOut)
async def update_todo(
todo_id: str,
data: TodoUpdate,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
result = await db.execute(
select(DailyTodo).where(DailyTodo.id == todo_id, DailyTodo.user_id == current_user.id)
)
todo = result.scalar_one_or_none()
if not todo:
raise HTTPException(status_code=404, detail="待办不存在")
# 历史日期不允许修改
if todo.todo_date != date.today().isoformat():
raise HTTPException(status_code=403, detail="历史待办不可修改")
if data.title is not None:
todo.title = data.title
if data.is_completed is not None:
from datetime import datetime
todo.is_completed = data.is_completed
todo.completed_at = datetime.utcnow() if data.is_completed else None
await db.commit()
await db.refresh(todo)
return todo
@router.delete("/{todo_id}", status_code=204)
async def delete_todo(
todo_id: str,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
result = await db.execute(
select(DailyTodo).where(DailyTodo.id == todo_id, DailyTodo.user_id == current_user.id)
)
todo = result.scalar_one_or_none()
if not todo:
raise HTTPException(status_code=404, detail="待办不存在")
if todo.todo_date != date.today().isoformat():
raise HTTPException(status_code=403, detail="历史待办不可删除")
await db.delete(todo)
await db.commit()
@router.post("/ai-generate", response_model=TodoListOut)
async def ai_generate_todos(
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
target_date = date.today().isoformat()
# 幂等检查是否已有AI生成记录
check_q = select(func.count()).select_from(DailyTodo).where(
DailyTodo.user_id == current_user.id,
DailyTodo.todo_date == target_date,
DailyTodo.source.in_([TodoSource.AI_KANBAN, TodoSource.AI_CHAT]),
)
count = (await db.execute(check_q)).scalar()
if count > 0:
# 已生成,返回现有记录
q = select(DailyTodo).where(
DailyTodo.user_id == current_user.id,
DailyTodo.todo_date == target_date,
).order_by(DailyTodo.created_at.desc())
items = (await db.execute(q)).scalars().all()
return TodoListOut(items=items, total=len(items), page=1, page_size=50)
# 执行AI生成
todos = await generate_daily_todos(current_user.id, db)
return TodoListOut(items=todos, total=len(todos), page=1, page_size=50)
@router.get("/summary", response_model=TodoSummaryOut)
async def get_summary(
date_str: str = Query(default=None),
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
target_date = date_str or date.today().isoformat()
q = select(DailyTodo).where(
DailyTodo.user_id == current_user.id,
DailyTodo.todo_date == target_date,
)
todos = (await db.execute(q)).scalars().all()
completed = sum(1 for t in todos if t.is_completed)
return TodoSummaryOut(date=target_date, total=len(todos), completed=completed, pending=len(todos) - completed)
```
- [ ] **Step 2: 提交**
```bash
git add backend/app/routers/todo.py
git commit -m "feat(todo): add API router with CRUD + ai-generate"
```
---
## Task 4: 后端 - AI 生成逻辑 Service
**Files:**
- Create: `backend/app/services/todo_service.py`
- [ ] **Step 1: 创建 `app/services/todo_service.py`**
```python
import json
import logging
from datetime import date, datetime, timedelta
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from app.models.todo import DailyTodo, TodoSource
from app.models.task import Task, TaskStatus
from app.models.conversation import Conversation, Message
from app.services.llm_service import llm_service
logger = logging.getLogger(__name__)
async def generate_daily_todos(user_id: str, db: AsyncSession) -> list[DailyTodo]:
"""
为用户生成今日待办:
1. 来自前一天未完成的看板任务最多20条
2. 来自前一天对话记录分析最多3条
"""
today = date.today()
yesterday = (today - timedelta(days=1)).isoformat()
todos: list[DailyTodo] = []
# 1. 从看板任务导入
kanban_todos = await _import_kanban_tasks(user_id, yesterday, db)
todos.extend(kanban_todos)
# 2. 从对话记录分析
chat_todos = await _analyze_chat_history(user_id, yesterday, db)
todos.extend(chat_todos)
return todos
async def _import_kanban_tasks(user_id: str, date_str: str, db: AsyncSession) -> list[DailyTodo]:
"""导入前一天创建的、未完成的看板任务"""
q = select(Task).where(
Task.user_id == user_id,
Task.status != TaskStatus.DONE,
).order_by(Task.created_at.desc()).limit(20)
tasks = (await db.execute(q)).scalars().all()
todos = []
for task in tasks:
todo = DailyTodo(
user_id=user_id,
title=task.title,
source=TodoSource.AI_KANBAN,
source_detail=f"看板:{task.title}",
source_ref_id=task.id,
todo_date=date.today().isoformat(),
)
db.add(todo)
todos.append(todo)
if todos:
await db.commit()
for todo in todos:
await db.refresh(todo)
return todos
async def _analyze_chat_history(user_id: str, date_str: str, db: AsyncSession) -> list[DailyTodo]:
"""分析前一天对话,提取待办事项"""
try:
# 查询前一天创建的对话
conv_q = select(Conversation).where(
Conversation.user_id == user_id,
).order_by(Conversation.created_at.desc()).limit(10)
convs = (await db.execute(conv_q)).scalars().all()
# 过滤出昨天的对话
yesterday_convs = []
for conv in convs:
created = conv.created_at
if hasattr(created, 'date'):
created_date = created.date() if hasattr(created, 'date') else created
else:
created_date = datetime.fromisoformat(str(created)).date()
if str(created_date) == date_str or (created + timedelta(hours=8)).strftime('%Y-%m-%d') == date_str:
yesterday_convs.append(conv)
if not yesterday_convs:
return []
# 收集消息内容限制2000字
messages_content = []
for conv in yesterday_convs:
msg_q = select(Message).where(
Message.conversation_id == conv.id
).order_by(Message.created_at.asc()).limit(50)
msgs = (await db.execute(msg_q)).scalars().all()
for msg in msgs:
if msg.content:
messages_content.append(f"[{msg.role}]: {msg.content[:500]}")
if not messages_content:
return []
full_text = "\n".join(messages_content)[:2000]
# 调用 LLM 分析
prompt = f"""你是一个任务规划助手。请分析以下对话记录,提取其中用户想要完成但尚未明确完成的事项。
要求:
- 最多提取 3 条
- 每条格式:{{"title": "事项描述50字以内", "reason": "来源说明60字以内"}}
- 只提取用户明确表达过需求但还未完成的事项
- 如果没有可提取的内容,返回空数组 []
对话记录:
{full_text}
返回 JSON 数组:"""
response = await llm_service.chat(prompt=prompt, system=None)
content = response if isinstance(response, str) else (response.get("content") or "")
# 尝试解析 JSON
try:
# 提取 JSON 数组
start = content.find('[')
end = content.rfind(']') + 1
if start != -1 and end > start:
items = json.loads(content[start:end])
else:
items = []
except (json.JSONDecodeError, ValueError):
logger.warning(f"LLM 返回格式异常,跳过对话分析: {content[:200]}")
items = []
if not items:
return []
todos = []
for item in items[:3]:
todo = DailyTodo(
user_id=user_id,
title=item.get("title", "")[:500],
source=TodoSource.AI_CHAT,
source_detail=f"对话:{item.get('reason', '')[:60]}",
todo_date=date.today().isoformat(),
)
db.add(todo)
todos.append(todo)
if todos:
await db.commit()
for todo in todos:
await db.refresh(todo)
return todos
except Exception as e:
logger.error(f"对话分析失败: {e}")
return []
```
- [ ] **Step 2: 提交**
```bash
git add backend/app/services/todo_service.py
git commit -m "feat(todo): add AI generation service"
```
---
## Task 5: 后端 - 注册路由
**Files:**
- Modify: `backend/app/main.py`
- [ ] **Step 1: 在 `main.py` 中添加 todo router**
找到其他 router 注册的位置(如 `app/routers/conversation` 等),添加:
```python
from app.routers.todo import router as todo_router
app.include_router(todo_router)
```
- [ ] **Step 2: 提交**
```bash
git add backend/app/main.py
git commit -m "feat(todo): register todo router in main"
```
---
## Task 6: 后端 - 定时任务APScheduler
**Files:**
- Modify: `backend/app/routers/scheduler.py`
- Create: `backend/app/services/scheduler_service.py` (或修改现有)
- [ ] **Step 1: 查看现有的 scheduler.py 结构**
读取 `backend/app/routers/scheduler.py``backend/app/services/scheduler_service.py`,了解现有定时任务模式。
- [ ] **Step 2: 添加每日 AI 生成定时任务**
在 scheduler_service 中添加:
```python
async def daily_todo_generation():
"""每天早上8点为所有活跃用户生成待办"""
from app.database import async_session
from app.models.user import User
from sqlalchemy import select
from app.services.todo_service import generate_daily_todos
async with async_session() as db:
result = await db.execute(select(User).where(User.is_active == True))
users = result.scalars().all()
for user in users:
try:
await generate_daily_todos(user.id, db)
except Exception as e:
logger.error(f"用户 {user.id} 定时生成待办失败: {e}")
```
在 APScheduler 中注册:
```python
scheduler.add_job(
daily_todo_generation,
"cron",
hour=8, minute=0,
id="daily_todo_generation",
replace_existing=True,
)
```
- [ ] **Step 3: 提交**
```bash
git add backend/app/services/scheduler_service.py
git commit -m "feat(todo): add daily AI todo generation scheduler"
```
---
## Task 7: 前端 - API 客户端
**Files:**
- Create: `frontend/src/api/todo.ts`
- [ ] **Step 1: 创建 `src/api/todo.ts`**
参考 `src/api/task.ts` 的模式:
```typescript
import api from './index'
export type TodoSource = 'ai_kanban' | 'ai_chat' | 'manual'
export interface Todo {
id: string
title: string
is_completed: boolean
source: TodoSource
source_detail: string | null
todo_date: string
completed_at: string | null
created_at: string
updated_at: string
}
export interface TodoListResponse {
items: Todo[]
total: number
page: number
page_size: number
}
export interface TodoSummary {
date: string
total: number
completed: number
pending: number
}
export const todoApi = {
list(date?: string, page = 1, pageSize = 50) {
return api.get<TodoListResponse>('/api/todos', {
params: date ? { date_str: date, page, page_size: pageSize } : { page, page_size: pageSize },
})
},
create(title: string) {
return api.post<Todo>('/api/todos', { title })
},
update(id: string, data: { title?: string; is_completed?: boolean }) {
return api.patch<Todo>(`/api/todos/${id}`, data)
},
delete(id: string) {
return api.delete(`/api/todos/${id}`)
},
aiGenerate() {
return api.post<TodoListResponse>('/api/todos/ai-generate')
},
summary(date?: string) {
return api.get<TodoSummary>('/api/todos/summary', {
params: date ? { date_str: date } : {},
})
},
}
```
- [ ] **Step 2: 提交**
```bash
git add frontend/src/api/todo.ts
git commit -m "feat(todo): add todo API client"
```
---
## Task 8: 前端 - 待办页面 TodoView.vue
**Files:**
- Create: `frontend/src/views/TodoView.vue`
- [ ] **Step 1: 创建 TodoView.vue**
页面布局:顶部日期导航 + 待办列表,参考 AgentView 的 sci-fi 风格
核心结构:
```vue
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { todoApi, type Todo } from '@/api/todo'
import { CheckSquare, Plus, Sparkles, ChevronLeft, ChevronRight, Calendar } from 'lucide-vue-next'
import { animate } from 'motion'
// 状态
const selectedDate = ref(new Date().toISOString().slice(0, 10)) // YYYY-MM-DD
const todos = ref<Todo[]>([])
const loading = ref(false)
const generating = ref(false)
const newTitle = ref('')
const editingId = ref<string | null>(null)
const editTitle = ref('')
const isToday = computed(() => selectedDate.value === new Date().toISOString().slice(0, 10))
// 日期快捷切换
function goToday() { selectedDate.value = new Date().toISOString().slice(0, 10) }
function goYesterday() {
const d = new Date(); d.setDate(d.getDate() - 1)
selectedDate.value = d.toISOString().slice(0, 10)
}
function goBeforeYesterday() {
const d = new Date(); d.setDate(d.getDate() - 2)
selectedDate.value = d.toISOString().slice(0, 10)
}
// 加载数据
async function loadTodos() {
loading.value = true
try {
const res = await todoApi.list(selectedDate.value)
todos.value = res.data.items
} finally {
loading.value = false
}
}
// 新增
async function addTodo() {
if (!newTitle.value.trim()) return
const res = await todoApi.create(newTitle.value.trim())
todos.value.unshift(res.data)
newTitle.value = ''
}
// 切换完成状态
async function toggleComplete(todo: Todo) {
const res = await todoApi.update(todo.id, { is_completed: !todo.is_completed })
const idx = todos.value.findIndex(t => t.id === todo.id)
if (idx !== -1) todos.value[idx] = res.data
}
// 删除
async function deleteTodo(id: string) {
await todoApi.delete(id)
todos.value = todos.value.filter(t => t.id !== id)
}
// AI 生成
async function aiGenerate() {
generating.value = true
try {
const res = await todoApi.aiGenerate()
todos.value = res.data.items
} finally {
generating.value = false
}
}
onMounted(loadTodos)
</script>
<template>
<div class="todo-view scanlines">
<!-- 背景复用 AgentView 的背景层 -->
<div class="bg-grid"></div>
<div class="bg-glow"></div>
<!-- Header -->
<div class="view-header">
<div class="header-title">
<CheckSquare :size="16" />
<span class="title-bracket">[</span>
<span class="title-text">DAILY TODO</span>
<span class="title-bracket">]</span>
</div>
<div class="header-actions">
<div v-if="isToday" class="ai-btn" @click="aiGenerate" :class="{ loading: generating }">
<Sparkles :size="14" :class="{ 'ai-spin': generating }" />
<span>{{ generating ? '生成中...' : 'AI 规划今日' }}</span>
</div>
</div>
</div>
<!-- 日期导航 -->
<div class="date-nav">
<button class="date-btn" :class="{ active: !isToday }" @click="goBeforeYesterday">前天</button>
<button class="date-btn" @click="goYesterday">昨天</button>
<button class="date-btn primary" :class="{ active: isToday }" @click="goToday">
今天
<Calendar :size="12" />
</button>
</div>
<!-- 主内容 -->
<div class="todo-content">
<!-- 今日新增输入框 -->
<div v-if="isToday" class="add-form">
<input
v-model="newTitle"
class="add-input"
placeholder="输入待办事项,按回车添加..."
@keyup.enter="addTodo"
/>
<button class="add-btn" @click="addTodo">
<Plus :size="16" />
</button>
</div>
<!-- 待办列表 -->
<div class="todo-list">
<div
v-for="todo in todos"
:key="todo.id"
class="todo-item"
:class="{ completed: todo.is_completed, 'ai-source': todo.source !== 'manual' }"
>
<button class="check-btn" @click="toggleComplete(todo)">
<span class="check-box" :class="{ checked: todo.is_completed }">
<span v-if="todo.is_completed" class="check-mark"></span>
</span>
</button>
<div class="todo-content">
<span class="todo-title">{{ todo.title }}</span>
<span v-if="todo.source_detail" class="todo-source">{{ todo.source_detail }}</span>
</div>
<button v-if="isToday" class="del-btn" @click="deleteTodo(todo.id)">
<span>×</span>
</button>
</div>
<!-- 空状态 -->
<div v-if="!loading && todos.length === 0" class="empty-state">
<span class="empty-icon">[ ]</span>
<span class="empty-text">{{ isToday ? '今日待办为空,点击上方新增' : '该日无待办记录' }}</span>
</div>
<!-- 加载中 -->
<div v-if="loading" class="loading-state">
<span class="loading-text">LOADING...</span>
</div>
</div>
</div>
</div>
</template>
<style scoped>
/* 布局 */
.todo-view {
display: flex;
flex-direction: column;
height: 100%;
background: var(--bg-void);
position: relative;
overflow: hidden;
}
.view-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 24px;
border-bottom: 1px solid var(--border-dim);
background: rgba(5,8,16,0.6);
backdrop-filter: blur(8px);
position: relative;
z-index: 10;
}
.header-title {
display: flex;
align-items: center;
gap: 8px;
font-family: var(--font-display);
font-size: 13px;
letter-spacing: 0.2em;
color: var(--text-primary);
}
.title-bracket { color: var(--accent-cyan); opacity: 0.6; }
.ai-btn {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 14px;
background: rgba(249,168,37,0.08);
border: 1px solid rgba(249,168,37,0.3);
border-radius: var(--radius-sm);
color: var(--accent-amber);
font-family: var(--font-mono);
font-size: 11px;
letter-spacing: 0.1em;
cursor: pointer;
transition: all var(--transition-fast);
}
.ai-btn:hover { background: rgba(249,168,37,0.15); border-color: var(--accent-amber); box-shadow: 0 0 12px rgba(249,168,37,0.2); }
.ai-spin { animation: spin 1s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }
/* 日期导航 */
.date-nav {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 12px;
border-bottom: 1px solid var(--border-dim);
}
.date-btn {
padding: 5px 14px;
background: transparent;
border: 1px solid var(--border-mid);
border-radius: var(--radius-sm);
color: var(--text-dim);
font-family: var(--font-mono);
font-size: 11px;
letter-spacing: 0.1em;
cursor: pointer;
transition: all var(--transition-fast);
display: flex;
align-items: center;
gap: 6px;
}
.date-btn:hover { border-color: var(--accent-cyan); color: var(--accent-cyan); }
.date-btn.active { border-color: var(--accent-cyan); color: var(--accent-cyan); background: rgba(0,245,212,0.08); }
.date-btn.primary { font-weight: 600; }
/* 内容区 */
.todo-content {
flex: 1;
overflow-y: auto;
padding: 20px 24px;
}
/* 新增表单 */
.add-form {
display: flex;
gap: 8px;
margin-bottom: 20px;
}
.add-input {
flex: 1;
padding: 10px 16px;
background: var(--bg-card);
border: 1px solid var(--border-mid);
border-radius: var(--radius-md);
color: var(--text-primary);
font-family: var(--font-mono);
font-size: 13px;
transition: all var(--transition-fast);
}
.add-input:focus {
outline: none;
border-color: var(--accent-cyan);
box-shadow: 0 0 0 2px rgba(0,245,212,0.1);
}
.add-input::placeholder { color: var(--text-dim); }
.add-btn {
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0,245,212,0.08);
border: 1px solid rgba(0,245,212,0.3);
border-radius: var(--radius-md);
color: var(--accent-cyan);
cursor: pointer;
transition: all var(--transition-fast);
}
.add-btn:hover { background: rgba(0,245,212,0.15); box-shadow: var(--glow-cyan); }
/* 待办列表 */
.todo-list {
display: flex;
flex-direction: column;
gap: 6px;
}
.todo-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
background: var(--bg-card);
border: 1px solid var(--border-dim);
border-radius: var(--radius-md);
transition: all var(--transition-fast);
}
.todo-item:hover { border-color: var(--border-mid); }
.todo-item.ai-source { border-left: 2px solid var(--accent-amber); }
.todo-item.completed { opacity: 0.5; }
.todo-item.completed .todo-title { text-decoration: line-through; color: var(--text-dim); }
.check-btn {
background: none;
border: none;
cursor: pointer;
padding: 0;
display: flex;
align-items: center;
}
.check-box {
width: 18px;
height: 18px;
border: 1px solid var(--border-mid);
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
transition: all var(--transition-fast);
}
.check-box.checked { background: var(--accent-cyan); border-color: var(--accent-cyan); }
.check-mark { color: var(--bg-void); font-size: 12px; font-weight: bold; }
.todo-content { flex: 1; min-width: 0; }
.todo-title { display: block; font-size: 13px; color: var(--text-primary); font-family: var(--font-mono); }
.todo-source { display: block; font-size: 10px; color: var(--text-dim); margin-top: 3px; font-family: var(--font-mono); }
.del-btn {
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
background: none;
border: none;
color: var(--text-dim);
cursor: pointer;
font-size: 18px;
border-radius: 4px;
transition: all var(--transition-fast);
}
.del-btn:hover { color: var(--accent-red); background: rgba(255,71,87,0.1); }
/* 空/加载状态 */
.empty-state, .loading-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 20px;
gap: 12px;
}
.empty-icon { font-family: var(--font-mono); font-size: 32px; color: var(--text-dim); opacity: 0.3; }
.empty-text { font-family: var(--font-mono); font-size: 12px; color: var(--text-dim); letter-spacing: 0.1em; }
.loading-text { font-family: var(--font-mono); font-size: 11px; color: var(--accent-cyan); letter-spacing: 0.2em; animation: pulse 1s ease-in-out infinite; }
@keyframes pulse { 0%, 100% { opacity: 0.4; } 50% { opacity: 1; } }
</style>
```
- [ ] **Step 2: 提交**
```bash
git add frontend/src/views/TodoView.vue
git commit -m "feat(todo): add TodoView page"
```
---
## Task 9: 前端 - 路由和侧边栏
**Files:**
- Modify: `frontend/src/router/index.ts`
- Modify: `frontend/src/components/SidebarNav.vue`
- [ ] **Step 1: 在 `router/index.ts` 中添加 /todo 路由**
在 children 数组中添加:
```typescript
{
path: 'todo',
name: 'todo',
component: () => import('@/views/TodoView.vue'),
},
```
- [ ] **Step 2: 在 `SidebarNav.vue` 中添加待办菜单项**
在 navItems 数组中添加:
```typescript
{ name: '待办', path: '/todo', icon: CheckSquare },
```
导入 CheckSquare
```typescript
import { CheckSquare } from 'lucide-vue-next'
```
- [ ] **Step 3: 提交**
```bash
git add frontend/src/router/index.ts frontend/src/components/SidebarNav.vue
git commit -m "feat(todo): add route and sidebar menu"
```
---
## Task 10: 数据库迁移
**Files:**
- 创建迁移脚本(手动 SQL 或 Alembic
- [ ] **Step 1: 创建数据库表**
直接执行 SQL参考 `app/models/todo.py` 的定义):
```sql
CREATE TABLE IF NOT EXISTS daily_todos (
id VARCHAR(36) PRIMARY KEY,
user_id VARCHAR(36) NOT NULL,
title VARCHAR(500) NOT NULL,
is_completed BOOLEAN NOT NULL DEFAULT FALSE,
source VARCHAR(20) NOT NULL DEFAULT 'manual',
source_detail VARCHAR(500),
source_ref_id VARCHAR(36),
todo_date VARCHAR(10) NOT NULL,
completed_at TIMESTAMP NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX idx_user_date (user_id, todo_date),
INDEX idx_user_id (user_id)
);
```
使用 sqlite3 执行(假设数据库文件在 backend 目录):
```bash
cd backend
sqlite3 myagents.db < create_daily_todos.sql
```
或通过 Python 直接创建表:
```python
# 在 backend 目录执行
python -c "
import asyncio
from app.database import engine, BaseModel
from app.models.todo import DailyTodo
from sqlalchemy import text
async def create_table():
async with engine.begin() as conn:
await conn.execute(text('''
CREATE TABLE IF NOT EXISTS daily_todos (
id VARCHAR(36) PRIMARY KEY,
user_id VARCHAR(36) NOT NULL,
title VARCHAR(500) NOT NULL,
is_completed BOOLEAN NOT NULL DEFAULT 0,
source VARCHAR(20) NOT NULL DEFAULT 'manual',
source_detail VARCHAR(500),
source_ref_id VARCHAR(36),
todo_date VARCHAR(10) NOT NULL,
completed_at TIMESTAMP NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
)
'''))
await conn.execute(text('CREATE INDEX IF NOT EXISTS idx_daily_todos_user_date ON daily_todos(user_id, todo_date)'))
await conn.execute(text('CREATE INDEX IF NOT EXISTS idx_daily_todos_user_id ON daily_todos(user_id)'))
print('Table created successfully')
asyncio.run(create_table())
"
```
- [ ] **Step 2: 提交迁移脚本**
```bash
git add docs/superpowers/plans/2026-03-20-daily-todo-migration.sql
git commit -m "feat(todo): add database migration"
```
---
## 验证清单
完成所有 Task 后,验证以下内容:
1. **后端 API 可访问**`GET /api/todos` 返回 200
2. **CRUD 正常**:新增、修改、删除待办均正常
3. **AI 生成正常**`POST /api/todos/ai-generate` 返回待办列表
4. **前端页面正常**`/todo` 页面可访问
5. **侧边栏菜单**:待办菜单项显示
6. **样式正常**:页面风格与 AgentView 一致sci-fi 全息终端)
7. **日期切换正常**:昨天/前天数据正确加载
8. **历史只读**:修改历史日期的待办返回 403
---
## 实现顺序建议
1. Task 1 → 2 → 3 → 5后端核心
2. Task 4AI Service
3. Task 6定时任务
4. Task 10数据库
5. Task 7 → 8 → 9前端