好的, 我们可以对 Todo 项目添加分类功能, 并使用 Turso 数据库和 Drizzle ORM 来实现数据持久化。以下是实现步骤:
- 首先, 安装必要的依赖:
bun add @libsql/client drizzle-orm @types/node
bun add -d drizzle-kit- 创建数据库 schema 文件
db/schema.ts:
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core';
export const categories = sqliteTable('categories', {
id: integer('id').primaryKey(),
name: text('name').notNull(),
});
export const todos = sqliteTable('todos', {
id: integer('id').primaryKey(),
text: text('text').notNull(),
completed: integer('completed', { mode: 'boolean' }).notNull().default(false),
categoryId: integer('category_id').references(() => categories.id),
});- 创建数据库配置文件
db/index.ts:
import { createClient } from '@libsql/client';
import { drizzle } from 'drizzle-orm/libsql';
import * as schema from './schema';
const client = createClient({
url: process.env.DATABASE_URL as string,
authToken: process.env.DATABASE_AUTH_TOKEN,
});
export const db = drizzle(client, { schema });- 更新
server.ts文件以使用数据库:
import { Elysia, t } from 'elysia';
import { html } from '@elysiajs/html';
import { marked } from 'marked';
import { db } from './db';
import { todos, categories } from './db/schema';
import { eq } from 'drizzle-orm';
const app = new Elysia()
.use(html())
.get('/', () => 'Hello Elysia')
// Categories API
.get('/api/categories', async () => {
return await db.select().from(categories);
})
.post('/api/categories', async ({ body }) => {
const [newCategory] = await db.insert(categories).values(body).returning();
return newCategory;
}, {
body: t.Object({
name: t.String()
})
})
// Todos API
.get('/api/todos', async () => {
return await db.select().from(todos).leftJoin(categories, eq(todos.categoryId, categories.id));
})
.post('/api/todos', async ({ body }) => {
const [newTodo] = await db.insert(todos).values(body).returning();
return newTodo;
}, {
body: t.Object({
text: t.String(),
categoryId: t.Optional(t.Number())
})
})
.put('/api/todos/:id', async ({ params, body }) => {
const [updatedTodo] = await db.update(todos)
.set(body)
.where(eq(todos.id, parseInt(params.id)))
.returning();
return updatedTodo;
}, {
params: t.Object({
id: t.String()
}),
body: t.Object({
text: t.Optional(t.String()),
completed: t.Optional(t.Boolean()),
categoryId: t.Optional(t.Number())
})
})
.delete('/api/todos/:id', async ({ params }) => {
await db.delete(todos).where(eq(todos.id, parseInt(params.id)));
return { success: true };
}, {
params: t.Object({
id: t.String()
})
})
.get('/api/todos/render/:id', async ({ params }) => {
const [todo] = await db.select().from(todos).where(eq(todos.id, parseInt(params.id)));
if (todo) {
return html (`<div>${marked (todo. Text)}</div>`);
}
Return 'Todo not found';
}, {
params: t.Object({
id: t.String()
})
})
.listen(3000);
console.log(`🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}`);- 更新
pages/index.tsx以支持分类:
import { useState, useEffect } from 'react';
import { marked } from 'marked';
interface Category {
id: number;
name: string;
}
interface Todo {
id: number;
text: string;
completed: boolean;
categoryId: number | null;
category?: Category;
}
export default function Home() {
const [todos, setTodos] = useState<Todo[]>([]);
const [categories, setCategories] = useState<Category[]>([]);
const [newTodo, setNewTodo] = useState('');
const [newCategory, setNewCategory] = useState('');
const [selectedCategory, setSelectedCategory] = useState<number | null>(null);
const [editingId, setEditingId] = useState<number | null>(null);
const [editingText, setEditingText] = useState('');
useEffect(() => {
fetchTodos();
fetchCategories();
}, []);
const fetchTodos = async () => {
const response = await fetch('/api/todos');
const data = await response.json();
setTodos(data);
};
const fetchCategories = async () => {
const response = await fetch('/api/categories');
const data = await response.json();
setCategories(data);
};
const addTodo = async (e: React.FormEvent) => {
e.preventDefault();
if (newTodo.trim()) {
const response = await fetch('/api/todos', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text: newTodo, categoryId: selectedCategory })
});
const data = await response.json();
setTodos([...todos, data]);
setNewTodo('');
}
};
const addCategory = async (e: React.FormEvent) => {
e.preventDefault();
if (newCategory.trim()) {
const response = await fetch('/api/categories', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: newCategory })
});
const data = await response.json();
setCategories([...categories, data]);
setNewCategory('');
}
};
const updateTodo = async (id: number, text: string, completed: boolean, categoryId: number | null) => {
const response = await fetch(`/api/todos/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text, completed, categoryId })
});
const updatedTodo = await response.json();
setTodos(todos.map(todo => todo.id === id ? updatedTodo : todo));
};
const deleteTodo = async (id: number) => {
Await fetch (`/api/todos/${id}`, { method: 'DELETE' });
SetTodos (todos.Filter (todo => todo. Id !== id));
};
const startEditing = (todo: Todo) => {
setEditingId(todo.id);
setEditingText(todo.text);
setSelectedCategory(todo.categoryId);
};
const saveEdit = async () => {
if (editingId !== null) {
await updateTodo(editingId, editingText, todos.find(t => t.id === editingId)?.completed || false, selectedCategory);
setEditingId(null);
setEditingText('');
setSelectedCategory(null);
}
};
const renderTodoText = async (id: number) => {
const response = await fetch(`/api/todos/render/${id}`);
const html = await response.text();
return { __html: html };
};
return (
<div className="p-4 max-w-2xl mx-auto">
<h1 className="text-3xl font-bold mb-4">Todo App with Categories</h1>
{/* Add Category Form */}
<form onSubmit={addCategory} className="mb-4">
<input
value={newCategory}
onChange={(e) => setNewCategory(e.target.value)}
placeholder="Add a new category"
className="border p-2 mr-2"
/>
<button type="submit" className="bg-green-500 text-white p-2 rounded hover:bg-green-600">Add Category</button>
</form>
{/* Add Todo Form */}
<form onSubmit={addTodo} className="mb-4">
<input
value={newTodo}
onChange={(e) => setNewTodo(e.target.value)}
placeholder="Add a new todo"
className="border p-2 mr-2 w-full mb-2"
/>
<select
value={selectedCategory || ''}
onChange={(e) => setSelectedCategory(Number(e.target.value) || null)}
className="border p-2 mr-2"
>
<option value="">No Category</option>
{categories.map(category => (
<option key={category.id} value={category.id}>{category.name}</option>
))}
</select>
<button type="submit" className="bg-blue-500 text-white p-2 rounded hover:bg-blue-600">Add Todo</button>
</form>
{/* Todo List */}
<ul>
{todos.map(todo => (
<li key={todo.id} className="mb-4 bg-gray-100 p-4 rounded">
{editingId === todo.id ? (
<div>
<textarea
value={editingText}
onChange={(e) => setEditingText(e.target.value)}
className="w-full p-2 border rounded mb-2"
/>
<select
value={selectedCategory || ''}
onChange={(e) => setSelectedCategory(Number(e.target.value) || null)}
className="border p-2 mr-2"
>
<option value="">No Category</option>
{categories.map(category => (
<option key={category.id} value={category.id}>{category.name}</option>
))}
</select>
<button onClick={saveEdit} className="bg-green-500 text-white p-2 rounded mr-2">Save</button>
<button onClick={() => setEditingId(null)} className="bg-gray-500 text-white p-2 rounded">Cancel</button>
</div>
) : (
<div>
<div className="flex items-center mb-2">
<input
type="checkbox"
checked={todo.completed}
onChange={(e) => updateTodo (todo. Id, todo. Text, e.target. Checked, todo. CategoryId)}
ClassName="mr-2"
/>
<span className={todo.completed ? 'line-through' : ''}>{todo.text}</span>
<span className="ml-2 text-sm text-gray-500">{todo.category?.name}</span>
<button onClick={() => startEditing(todo)} className="ml-auto text-blue-500 mr-2">Edit</button>
<button onClick={() => deleteTodo(todo.id)} className="text-red-500">Remove</button>
</div>
<div dangerouslySetInnerHTML={renderTodoText(todo.id)} className="prose prose-sm text-sm text-gray-600" />
</div>
)}
</li>
))}
</ul>
</div>
);
}- 创建一个
.env文件来存储数据库连接信息:
DATABASE_URL=libsql://your-database-url
DATABASE_AUTH_TOKEN=your-auth-token
确保将这些实际值替换为你的 Turso 数据库 URL 和认证令牌。
- 更新
package.json中的 scripts 以包含数据库迁移:
"scripts": {
"dev": "bun run server.ts & next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"db:generate": "drizzle-kit generate:sqlite",
"db:push": "drizzle-kit push:sqlite"
}- 运行数据库迁移:
bun run db:generate
bun run db:push这个更新后的 Todo 应用现在支持分类功能, 并使用 Turso 数据库进行数据存储。用户可以创建新的分类, 将 Todo 项分配到特定分类, 以及按分类筛选 Todo 项。
要运行这个项目, 使用以下命令:
bun run dev这将启动 Elysia 服务器和 Next. Js 开发服务器。你可以在浏览器中访问 http://localhost:3000 来使用这个增强版的 Todo 应用。
请注意, 这个实现假设你已经设置了 Turso 数据库。如果还没有, 你需要创建一个 Turso 账户, 创建一个新的数据库, 并获取连接 URL 和认证令牌。