32 KiB
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 API,TodoView 独立页面
- 数据库:新表
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 的写法:
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,添加导出
在文件末尾添加:
from app.models.todo import DailyTodo
__all__ = [..., "DailyTodo"]
- Step 3: 提交
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
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: 提交
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
完整实现以下端点:
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: 提交
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
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: 提交
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 等),添加:
from app.routers.todo import router as todo_router
app.include_router(todo_router)
- Step 2: 提交
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 中添加:
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 中注册:
scheduler.add_job(
daily_todo_generation,
"cron",
hour=8, minute=0,
id="daily_todo_generation",
replace_existing=True,
)
- Step 3: 提交
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 的模式:
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: 提交
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 风格
核心结构:
<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: 提交
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 数组中添加:
{
path: 'todo',
name: 'todo',
component: () => import('@/views/TodoView.vue'),
},
- Step 2: 在
SidebarNav.vue中添加待办菜单项
在 navItems 数组中添加:
{ name: '待办', path: '/todo', icon: CheckSquare },
导入 CheckSquare:
import { CheckSquare } from 'lucide-vue-next'
- Step 3: 提交
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 的定义):
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 目录):
cd backend
sqlite3 myagents.db < create_daily_todos.sql
或通过 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: 提交迁移脚本
git add docs/superpowers/plans/2026-03-20-daily-todo-migration.sql
git commit -m "feat(todo): add database migration"
验证清单
完成所有 Task 后,验证以下内容:
- 后端 API 可访问:
GET /api/todos返回 200 - CRUD 正常:新增、修改、删除待办均正常
- AI 生成正常:
POST /api/todos/ai-generate返回待办列表 - 前端页面正常:
/todo页面可访问 - 侧边栏菜单:待办菜单项显示
- 样式正常:页面风格与 AgentView 一致(sci-fi 全息终端)
- 日期切换正常:昨天/前天数据正确加载
- 历史只读:修改历史日期的待办返回 403
实现顺序建议
- Task 1 → 2 → 3 → 5(后端核心)
- Task 4(AI Service)
- Task 6(定时任务)
- Task 10(数据库)
- Task 7 → 8 → 9(前端)