好的, 我们可以对 Todo 项目添加分类功能, 并使用 Turso 数据库和 Drizzle ORM 来实现数据持久化。以下是实现步骤:

  1. 首先, 安装必要的依赖:
bun add @libsql/client drizzle-orm @types/node
bun add -d drizzle-kit
  1. 创建数据库 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),
});
  1. 创建数据库配置文件 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 });
  1. 更新 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}`);
  1. 更新 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>
  );
}
  1. 创建一个 .env 文件来存储数据库连接信息:
DATABASE_URL=libsql://your-database-url
DATABASE_AUTH_TOKEN=your-auth-token

确保将这些实际值替换为你的 Turso 数据库 URL 和认证令牌。

  1. 更新 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"
}
  1. 运行数据库迁移:
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 和认证令牌。