飞轮智能成长阶段一:
- 梦核 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 可测试性规范(依赖注入强制要求)
105 lines
3.7 KiB
TypeScript
105 lines
3.7 KiB
TypeScript
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>
|
||
);
|
||
}
|