From 1494492b95f78b501a95d195a02923e154519768 Mon Sep 17 00:00:00 2001 From: imbytecat Date: Mon, 30 Mar 2026 21:28:10 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E4=B9=A6=E7=AD=BE?= =?UTF-8?q?=E6=A8=A1=E5=9D=97=20API=EF=BC=8C=E7=A7=BB=E9=99=A4=20Todo=20?= =?UTF-8?q?=E7=A4=BA=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/server/src/client/orpc.ts | 67 ++++-- apps/server/src/modules/bookmarks/contract.ts | 35 ++++ apps/server/src/modules/bookmarks/router.ts | 124 +++++++++++ apps/server/src/routes/index.tsx | 193 ------------------ apps/server/src/server/api/contracts/index.ts | 4 +- .../src/server/api/contracts/todo.contract.ts | 32 --- apps/server/src/server/api/routers/index.ts | 4 +- .../src/server/api/routers/todo.router.ts | 40 ---- apps/server/src/server/db/schema/todo.ts | 8 - 9 files changed, 216 insertions(+), 291 deletions(-) create mode 100644 apps/server/src/modules/bookmarks/contract.ts create mode 100644 apps/server/src/modules/bookmarks/router.ts delete mode 100644 apps/server/src/routes/index.tsx delete mode 100644 apps/server/src/server/api/contracts/todo.contract.ts delete mode 100644 apps/server/src/server/api/routers/todo.router.ts delete mode 100644 apps/server/src/server/db/schema/todo.ts diff --git a/apps/server/src/client/orpc.ts b/apps/server/src/client/orpc.ts index 6b9abab..e08fd22 100644 --- a/apps/server/src/client/orpc.ts +++ b/apps/server/src/client/orpc.ts @@ -26,25 +26,64 @@ const client: RouterClient = getORPCClient() export const orpc = createTanstackQueryUtils(client, { experimental_defaults: { - todo: { - create: { - mutationOptions: { - onSuccess: (_, __, ___, ctx) => { - ctx.client.invalidateQueries({ queryKey: orpc.todo.list.key() }) + bookmarks: { + category: { + create: { + mutationOptions: { + onSuccess: (_, __, ___, ctx) => { + ctx.client.invalidateQueries({ queryKey: orpc.bookmarks.category.list.key() }) + }, + }, + }, + update: { + mutationOptions: { + onSuccess: (_, __, ___, ctx) => { + ctx.client.invalidateQueries({ queryKey: orpc.bookmarks.category.list.key() }) + }, + }, + }, + remove: { + mutationOptions: { + onSuccess: (_, __, ___, ctx) => { + ctx.client.invalidateQueries({ queryKey: orpc.bookmarks.category.list.key() }) + }, + }, + }, + reorder: { + mutationOptions: { + onSuccess: (_, __, ___, ctx) => { + ctx.client.invalidateQueries({ queryKey: orpc.bookmarks.category.list.key() }) + }, }, }, }, - update: { - mutationOptions: { - onSuccess: (_, __, ___, ctx) => { - ctx.client.invalidateQueries({ queryKey: orpc.todo.list.key() }) + bookmark: { + create: { + mutationOptions: { + onSuccess: (_, __, ___, ctx) => { + ctx.client.invalidateQueries({ queryKey: orpc.bookmarks.category.list.key() }) + }, }, }, - }, - remove: { - mutationOptions: { - onSuccess: (_, __, ___, ctx) => { - ctx.client.invalidateQueries({ queryKey: orpc.todo.list.key() }) + update: { + mutationOptions: { + onSuccess: (_, __, ___, ctx) => { + ctx.client.invalidateQueries({ queryKey: orpc.bookmarks.category.list.key() }) + }, + }, + }, + remove: { + mutationOptions: { + onSuccess: (_, __, ___, ctx) => { + ctx.client.invalidateQueries({ queryKey: orpc.bookmarks.category.list.key() }) + }, + }, + }, + reorder: { + mutationOptions: { + onSuccess: (_, __, ___, ctx) => { + ctx.client.invalidateQueries({ queryKey: orpc.bookmarks.category.list.key() }) + }, }, }, }, diff --git a/apps/server/src/modules/bookmarks/contract.ts b/apps/server/src/modules/bookmarks/contract.ts new file mode 100644 index 0000000..d326498 --- /dev/null +++ b/apps/server/src/modules/bookmarks/contract.ts @@ -0,0 +1,35 @@ +import { oc } from '@orpc/contract' +import { createInsertSchema, createSelectSchema, createUpdateSchema } from 'drizzle-orm/zod' +import { z } from 'zod' +import { bookmarkTable, categoryTable } from '@/modules/bookmarks/schema' +import { generatedFieldKeys } from '@/server/db/fields' + +const categorySelect = createSelectSchema(categoryTable) +const categoryInsert = createInsertSchema(categoryTable).omit(generatedFieldKeys).omit({ userId: true }) +const categoryUpdate = createUpdateSchema(categoryTable).omit(generatedFieldKeys).omit({ userId: true }) + +const bookmarkSelect = createSelectSchema(bookmarkTable) +const bookmarkInsert = createInsertSchema(bookmarkTable).omit(generatedFieldKeys).omit({ userId: true }) +const bookmarkUpdate = createUpdateSchema(bookmarkTable).omit(generatedFieldKeys).omit({ userId: true }) + +export const category = { + list: oc.input(z.void()).output(z.array(categorySelect.extend({ bookmarks: z.array(bookmarkSelect) }))), + + create: oc.input(categoryInsert).output(categorySelect), + + update: oc.input(z.object({ id: z.uuid(), data: categoryUpdate })).output(categorySelect), + + remove: oc.input(z.object({ id: z.uuid() })).output(z.void()), + + reorder: oc.input(z.array(z.object({ id: z.uuid(), orderId: z.number().int() }))).output(z.void()), +} + +export const bookmark = { + create: oc.input(bookmarkInsert).output(bookmarkSelect), + + update: oc.input(z.object({ id: z.uuid(), data: bookmarkUpdate })).output(bookmarkSelect), + + remove: oc.input(z.object({ id: z.uuid() })).output(z.void()), + + reorder: oc.input(z.array(z.object({ id: z.uuid(), orderId: z.number().int() }))).output(z.void()), +} diff --git a/apps/server/src/modules/bookmarks/router.ts b/apps/server/src/modules/bookmarks/router.ts new file mode 100644 index 0000000..079f275 --- /dev/null +++ b/apps/server/src/modules/bookmarks/router.ts @@ -0,0 +1,124 @@ +import { ORPCError } from '@orpc/server' +import { and, eq } from 'drizzle-orm' +import { bookmarkTable, categoryTable } from '@/modules/bookmarks/schema' +import { authMiddleware, db } from '@/server/api/middlewares' +import { os } from '@/server/api/server' + +export const category = { + list: os.bookmarks.category.list + .use(db) + .use(authMiddleware) + .handler(async ({ context }) => { + return await context.db.query.categoryTable.findMany({ + where: { userId: context.user.id }, + orderBy: { orderId: 'asc' }, + with: { + bookmarks: { + orderBy: { orderId: 'asc' }, + }, + }, + }) + }), + + create: os.bookmarks.category.create + .use(db) + .use(authMiddleware) + .handler(async ({ context, input }) => { + const [created] = await context.db + .insert(categoryTable) + .values({ ...input, userId: context.user.id }) + .returning() + if (!created) throw new ORPCError('INTERNAL_SERVER_ERROR', { message: 'Failed to create category' }) + return created + }), + + update: os.bookmarks.category.update + .use(db) + .use(authMiddleware) + .handler(async ({ context, input }) => { + const [updated] = await context.db + .update(categoryTable) + .set(input.data) + .where(and(eq(categoryTable.id, input.id), eq(categoryTable.userId, context.user.id))) + .returning() + if (!updated) throw new ORPCError('NOT_FOUND') + return updated + }), + + remove: os.bookmarks.category.remove + .use(db) + .use(authMiddleware) + .handler(async ({ context, input }) => { + const [deleted] = await context.db + .delete(categoryTable) + .where(and(eq(categoryTable.id, input.id), eq(categoryTable.userId, context.user.id))) + .returning({ id: categoryTable.id }) + if (!deleted) throw new ORPCError('NOT_FOUND') + }), + + reorder: os.bookmarks.category.reorder + .use(db) + .use(authMiddleware) + .handler(async ({ context, input }) => { + await context.db.transaction(async (tx) => { + for (const item of input) { + await tx + .update(categoryTable) + .set({ orderId: item.orderId }) + .where(and(eq(categoryTable.id, item.id), eq(categoryTable.userId, context.user.id))) + } + }) + }), +} + +export const bookmark = { + create: os.bookmarks.bookmark.create + .use(db) + .use(authMiddleware) + .handler(async ({ context, input }) => { + const [created] = await context.db + .insert(bookmarkTable) + .values({ ...input, userId: context.user.id }) + .returning() + if (!created) throw new ORPCError('INTERNAL_SERVER_ERROR', { message: 'Failed to create bookmark' }) + return created + }), + + update: os.bookmarks.bookmark.update + .use(db) + .use(authMiddleware) + .handler(async ({ context, input }) => { + const [updated] = await context.db + .update(bookmarkTable) + .set(input.data) + .where(and(eq(bookmarkTable.id, input.id), eq(bookmarkTable.userId, context.user.id))) + .returning() + if (!updated) throw new ORPCError('NOT_FOUND') + return updated + }), + + remove: os.bookmarks.bookmark.remove + .use(db) + .use(authMiddleware) + .handler(async ({ context, input }) => { + const [deleted] = await context.db + .delete(bookmarkTable) + .where(and(eq(bookmarkTable.id, input.id), eq(bookmarkTable.userId, context.user.id))) + .returning({ id: bookmarkTable.id }) + if (!deleted) throw new ORPCError('NOT_FOUND') + }), + + reorder: os.bookmarks.bookmark.reorder + .use(db) + .use(authMiddleware) + .handler(async ({ context, input }) => { + await context.db.transaction(async (tx) => { + for (const item of input) { + await tx + .update(bookmarkTable) + .set({ orderId: item.orderId }) + .where(and(eq(bookmarkTable.id, item.id), eq(bookmarkTable.userId, context.user.id))) + } + }) + }), +} diff --git a/apps/server/src/routes/index.tsx b/apps/server/src/routes/index.tsx deleted file mode 100644 index fa57a89..0000000 --- a/apps/server/src/routes/index.tsx +++ /dev/null @@ -1,193 +0,0 @@ -import { useMutation, useSuspenseQuery } from '@tanstack/react-query' -import { createFileRoute } from '@tanstack/react-router' -import type { ChangeEventHandler, SubmitEventHandler } from 'react' -import { useState } from 'react' -import { orpc } from '@/client/orpc' - -export const Route = createFileRoute('/')({ - component: Todos, - loader: async ({ context }) => { - await context.queryClient.ensureQueryData(orpc.todo.list.queryOptions()) - }, -}) - -function Todos() { - const [newTodoTitle, setNewTodoTitle] = useState('') - - const listQuery = useSuspenseQuery(orpc.todo.list.queryOptions()) - const createMutation = useMutation(orpc.todo.create.mutationOptions()) - const updateMutation = useMutation(orpc.todo.update.mutationOptions()) - const deleteMutation = useMutation(orpc.todo.remove.mutationOptions()) - - const handleCreateTodo: SubmitEventHandler = (e) => { - e.preventDefault() - if (newTodoTitle.trim()) { - createMutation.mutate({ title: newTodoTitle.trim() }) - setNewTodoTitle('') - } - } - - const handleInputChange: ChangeEventHandler = (e) => { - setNewTodoTitle(e.target.value) - } - - const handleToggleTodo = (id: string, currentCompleted: boolean) => { - updateMutation.mutate({ - id, - data: { completed: !currentCompleted }, - }) - } - - const handleDeleteTodo = (id: string) => { - deleteMutation.mutate({ id }) - } - - const todos = listQuery.data - const completedCount = todos.filter((todo) => todo.completed).length - const totalCount = todos.length - const progress = totalCount > 0 ? (completedCount / totalCount) * 100 : 0 - - return ( -
-
- {/* Header */} -
-
-

我的待办

-

保持专注,逐个击破

-
-
-
- {completedCount} - /{totalCount} -
-
已完成
-
-
- - {/* Add Todo Form */} -
-
- - -
-
- - {/* Progress Bar (Only visible when there are tasks) */} - {totalCount > 0 && ( -
-
-
- )} - - {/* Todo List */} -
- {todos.length === 0 ? ( -
-
- -
-

没有待办事项

-

输入上方内容添加您的第一个任务

-
- ) : ( - todos.map((todo) => ( -
- - -
-

- {todo.title} -

-
- -
- - {new Date(todo.createdAt).toLocaleDateString('zh-CN')} - - -
-
- )) - )} -
-
-
- ) -} diff --git a/apps/server/src/server/api/contracts/index.ts b/apps/server/src/server/api/contracts/index.ts index 669cfd5..07b9020 100644 --- a/apps/server/src/server/api/contracts/index.ts +++ b/apps/server/src/server/api/contracts/index.ts @@ -1,7 +1,7 @@ -import * as todo from './todo.contract' +import * as bookmarks from '@/modules/bookmarks/contract' export const contract = { - todo, + bookmarks, } export type Contract = typeof contract diff --git a/apps/server/src/server/api/contracts/todo.contract.ts b/apps/server/src/server/api/contracts/todo.contract.ts deleted file mode 100644 index 8aed5cc..0000000 --- a/apps/server/src/server/api/contracts/todo.contract.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { oc } from '@orpc/contract' -import { createInsertSchema, createSelectSchema, createUpdateSchema } from 'drizzle-orm/zod' -import { z } from 'zod' -import { generatedFieldKeys } from '@/server/db/fields' -import { todoTable } from '@/server/db/schema' - -const selectSchema = createSelectSchema(todoTable) - -const insertSchema = createInsertSchema(todoTable).omit(generatedFieldKeys) - -const updateSchema = createUpdateSchema(todoTable).omit(generatedFieldKeys) - -export const list = oc.input(z.void()).output(z.array(selectSchema)) - -export const create = oc.input(insertSchema).output(selectSchema) - -export const update = oc - .input( - z.object({ - id: z.uuid(), - data: updateSchema, - }), - ) - .output(selectSchema) - -export const remove = oc - .input( - z.object({ - id: z.uuid(), - }), - ) - .output(z.void()) diff --git a/apps/server/src/server/api/routers/index.ts b/apps/server/src/server/api/routers/index.ts index 02a11fe..ede4e3e 100644 --- a/apps/server/src/server/api/routers/index.ts +++ b/apps/server/src/server/api/routers/index.ts @@ -1,6 +1,6 @@ +import * as bookmarks from '@/modules/bookmarks/router' import { os } from '../server' -import * as todo from './todo.router' export const router = os.router({ - todo, + bookmarks, }) diff --git a/apps/server/src/server/api/routers/todo.router.ts b/apps/server/src/server/api/routers/todo.router.ts deleted file mode 100644 index 40c988f..0000000 --- a/apps/server/src/server/api/routers/todo.router.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { ORPCError } from '@orpc/server' -import { eq } from 'drizzle-orm' -import { todoTable } from '@/server/db/schema' -import { db } from '../middlewares' -import { os } from '../server' - -export const list = os.todo.list.use(db).handler(async ({ context }) => { - const todos = await context.db.query.todoTable.findMany({ - orderBy: { createdAt: 'desc' }, - }) - return todos -}) - -export const create = os.todo.create.use(db).handler(async ({ context, input }) => { - const [newTodo] = await context.db.insert(todoTable).values(input).returning() - - if (!newTodo) { - throw new ORPCError('INTERNAL_SERVER_ERROR', { message: 'Failed to create todo' }) - } - - return newTodo -}) - -export const update = os.todo.update.use(db).handler(async ({ context, input }) => { - const [updatedTodo] = await context.db.update(todoTable).set(input.data).where(eq(todoTable.id, input.id)).returning() - - if (!updatedTodo) { - throw new ORPCError('NOT_FOUND') - } - - return updatedTodo -}) - -export const remove = os.todo.remove.use(db).handler(async ({ context, input }) => { - const [deleted] = await context.db.delete(todoTable).where(eq(todoTable.id, input.id)).returning({ id: todoTable.id }) - - if (!deleted) { - throw new ORPCError('NOT_FOUND') - } -}) diff --git a/apps/server/src/server/db/schema/todo.ts b/apps/server/src/server/db/schema/todo.ts deleted file mode 100644 index 5ca821c..0000000 --- a/apps/server/src/server/db/schema/todo.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { boolean, pgTable, text } from 'drizzle-orm/pg-core' -import { generatedFields } from '../fields' - -export const todoTable = pgTable('todo', { - ...generatedFields, - title: text('title').notNull(), - completed: boolean('completed').notNull().default(false), -})