dev-manager-tauri/src/components/dashboard/UserTodoPanel.tsx
lanrtop 6162533c77 feat: 飞轮阶段一 + 项目个人备忘功能
飞轮智能成长阶段一:
- 梦核 prompt 注入跨项目复盘笔记,新增【高频陷阱】分析节
- 修复 collect_flywheel_notes 查错表 bug(projects → project_workspaces)
- MCP append_project_note 描述内嵌【复盘】结构化格式,引导 Claude 写标准笔记
- FlywheelStats 新增 project_notes 字段,支持 win_path/wsl_path 两种路径匹配

项目个人备忘(user_project_todos):
- 新表 + 4 个 Rust command,依赖注入写法,6 个单元测试全绿
- UserTodoPanel 组件,📝 按钮展开,支持新增/勾选/删除

CONVENTIONS v1.4.4:新增 Rust 可测试性规范(依赖注入强制要求)
2026-04-11 17:21:47 +09:00

105 lines
3.7 KiB
TypeScript
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.

import { useState, useRef, useEffect } from "react";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { getUserTodos, addUserTodo, toggleUserTodo, deleteUserTodo } from "../../lib/commands";
export function UserTodoPanel({ projectId }: { projectId: string }) {
const qc = useQueryClient();
const [input, setInput] = useState("");
const inputRef = useRef<HTMLInputElement>(null);
const { data: todos = [] } = useQuery({
queryKey: ["user-todos", projectId],
queryFn: () => getUserTodos(projectId),
});
const addMut = useMutation({
mutationFn: (content: string) => addUserTodo(projectId, content),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ["user-todos", projectId] });
setInput("");
},
});
const toggleMut = useMutation({
mutationFn: (id: number) => toggleUserTodo(id),
onSuccess: () => qc.invalidateQueries({ queryKey: ["user-todos", projectId] }),
});
const deleteMut = useMutation({
mutationFn: (id: number) => deleteUserTodo(id),
onSuccess: () => qc.invalidateQueries({ queryKey: ["user-todos", projectId] }),
});
useEffect(() => {
inputRef.current?.focus();
}, []);
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter" && input.trim()) {
addMut.mutate(input.trim());
}
};
const pending = todos.filter((t) => !t.done);
const done = todos.filter((t) => t.done);
return (
<div className="mt-2 border-t border-gray-50 pt-3">
{/* 输入框 */}
<input
ref={inputRef}
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="记点什么… 回车确认"
className="w-full text-xs px-2 py-1.5 rounded border border-gray-200 bg-gray-50
placeholder-gray-300 focus:outline-none focus:border-gray-400 mb-2"
/>
{/* 待办列表 */}
{pending.length === 0 && done.length === 0 && (
<p className="text-xs text-gray-300 text-center py-1">暂无备忘</p>
)}
<ul className="flex flex-col gap-0.5">
{pending.map((t) => (
<li key={t.id} className="flex items-center gap-2 group">
<button
onClick={() => toggleMut.mutate(t.id)}
className="w-3.5 h-3.5 rounded border border-gray-300 shrink-0
hover:border-blue-400 transition-colors"
/>
<span className="text-xs text-gray-600 flex-1 leading-relaxed">{t.content}</span>
<button
onClick={() => deleteMut.mutate(t.id)}
className="text-gray-200 hover:text-red-400 transition-colors opacity-0 group-hover:opacity-100 text-xs shrink-0"
>
×
</button>
</li>
))}
{/* 已完成的条目折叠在末尾 */}
{done.map((t) => (
<li key={t.id} className="flex items-center gap-2 group">
<button
onClick={() => toggleMut.mutate(t.id)}
className="w-3.5 h-3.5 rounded border border-gray-200 bg-gray-100 shrink-0
flex items-center justify-center hover:border-gray-400 transition-colors"
>
<span className="text-gray-400 text-[9px] leading-none">✓</span>
</button>
<span className="text-xs text-gray-300 line-through flex-1 leading-relaxed">{t.content}</span>
<button
onClick={() => deleteMut.mutate(t.id)}
className="text-gray-200 hover:text-red-400 transition-colors opacity-0 group-hover:opacity-100 text-xs shrink-0"
>
×
</button>
</li>
))}
</ul>
</div>
);
}