From 8282053a34d31dbefef123c8acba5e9072d1b7bc Mon Sep 17 00:00:00 2001 From: ggyy <34892002@qq.com> Date: Fri, 24 Apr 2026 15:35:40 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=8D=A1=E5=AF=86=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/DataTable.vue | 66 ++++++++ docs/components.md | 105 ++++++++++++ modules/inventory/repository.ts | 38 +++++ modules/inventory/service.ts | 39 ++++- pages/admin/cards/+Page.vue | 205 ++++++++++++++--------- pages/admin/cards/+data.ts | 2 +- pages/admin/cards/deleteCard.telefunc.ts | 7 + pages/admin/cards/queryCards.telefunc.ts | 15 ++ 8 files changed, 398 insertions(+), 79 deletions(-) create mode 100644 components/DataTable.vue create mode 100644 docs/components.md create mode 100644 pages/admin/cards/deleteCard.telefunc.ts create mode 100644 pages/admin/cards/queryCards.telefunc.ts diff --git a/components/DataTable.vue b/components/DataTable.vue new file mode 100644 index 0000000..f10e061 --- /dev/null +++ b/components/DataTable.vue @@ -0,0 +1,66 @@ + + + \ No newline at end of file diff --git a/docs/components.md b/docs/components.md new file mode 100644 index 0000000..5c544a1 --- /dev/null +++ b/docs/components.md @@ -0,0 +1,105 @@ +# 公共组件文档 + +## DataTable + +通用带翻页的表格组件,基于 daisyUI `table` 样式。 + +### Props + +| 属性 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `columns` | `{ key: string; label: string }[]` | — | 列定义 | +| `rows` | `T[]` | — | 当前页数据 | +| `total` | `number` | — | 总条数 | +| `page` | `number` | — | 当前页码(从 1 开始) | +| `pageSize` | `number` | `20` | 每页条数 | +| `emptyText` | `string` | `"暂无数据"` | 空状态文案 | + +### Events + +| 事件 | 参数 | 说明 | +|------|------|------| +| `update:page` | `page: number` | 用户切换页码时触发 | + +### Slots + +每一列都有一个以 `col.key` 命名的具名插槽,用于自定义单元格渲染。 + +插槽 props: + +| 名称 | 类型 | 说明 | +|------|------|------| +| `row` | `T` | 当前行完整数据 | +| `value` | `any` | 当前列的值,等同于 `row[col.key]` | + +不提供插槽时,默认渲染 `value`,值为 `null`/`undefined` 时显示 `-`。 + +### 基本用法 + +```components/DataTable.vue#L1-5 + +``` + +### 自定义列渲染 + +```components/DataTable.vue#L1-10 + + + + + + +``` + +### 完整示例 + +```pages/admin/cards/+Page.vue#L1-30 + + + +``` + +### 分页说明 + +- 总条数 `total <= pageSize` 时,分页控件自动隐藏 +- 页码按钮最多显示 5 个,以当前页为中心滑动 \ No newline at end of file diff --git a/modules/inventory/repository.ts b/modules/inventory/repository.ts index e1491af..2520bdd 100644 --- a/modules/inventory/repository.ts +++ b/modules/inventory/repository.ts @@ -63,3 +63,41 @@ export function countCardStats(prisma: PrismaClient) { }, }); } + +export function listCardRecordsPaged( + prisma: PrismaClient, + params: { + productId?: number; + batchNo?: string; + status?: string; + startDate?: string; + endDate?: string; + page: number; + pageSize: number; + }, +) { + const where: import("../../generated/prisma/client").Prisma.CardWhereInput = {}; + if (params.productId) where.productId = params.productId; + if (params.batchNo) where.batchNo = { contains: params.batchNo }; + if (params.status) where.status = params.status as import("../../generated/prisma/client").CardStatus; + if (params.startDate || params.endDate) { + where.createdAt = {}; + if (params.startDate) where.createdAt.gte = new Date(params.startDate); + if (params.endDate) { + const end = new Date(params.endDate); + end.setHours(23, 59, 59, 999); + where.createdAt.lte = end; + } + } + const skip = (params.page - 1) * params.pageSize; + return Promise.all([ + prisma.card.findMany({ where, include: { product: true }, orderBy: [{ id: "desc" }], skip, take: params.pageSize }), + prisma.card.count({ where }), + ]); +} + +export function deleteCardById(prisma: PrismaClient, id: number) { + return prisma.card.deleteMany({ + where: { id, status: "UNUSED" }, + }); +} diff --git a/modules/inventory/service.ts b/modules/inventory/service.ts index 87c7921..45f6287 100644 --- a/modules/inventory/service.ts +++ b/modules/inventory/service.ts @@ -3,7 +3,7 @@ import type { PrismaClient } from "../../generated/prisma/client"; import { badRequestError } from "../../lib/app-error"; import { getAdminContext, logAdminOperation } from "../auth/service"; import { parseCardLines } from "./importer"; -import { countCardStats, createCardRecord, createManyCards, deleteUnusedCardsByProduct, listCardRecords } from "./repository"; +import { countCardStats, createCardRecord, createManyCards, deleteCardById, deleteUnusedCardsByProduct, listCardRecords, listCardRecordsPaged } from "./repository"; function getInventoryContext() { return getContext<{ prisma: PrismaClient }>(); @@ -155,3 +155,40 @@ export async function deleteUnusedCards(input: { productId: number }) { count: result.count, }; } + +export async function deleteCard(input: { id: number }) { + const adminContext = getAdminContext(); + const { prisma } = adminContext; + const adminId = Number(adminContext.session?.user?.id); + const result = await deleteCardById(prisma, input.id); + if (result.count === 0) throw badRequestError("卡密不存在或已售出,无法删除", "CARD_DELETE_FAILED"); + await logAdminOperation({ action: "DELETE_CARD", targetType: "Card", targetId: String(input.id), detail: "" }, { prisma, adminId }); + return { id: input.id }; +} + +export async function getAdminCardsPaged(params: { + productId?: number; + batchNo?: string; + status?: string; + startDate?: string; + endDate?: string; + page: number; + pageSize: number; +}) { + const { prisma } = getAdminContext(); + const [cards, total] = await listCardRecordsPaged(prisma, params); + return { + total, + items: cards.map((item) => ({ + id: item.id, + productId: item.productId, + productName: item.product.name, + status: item.status, + batchNo: item.batchNo, + orderId: item.orderId, + soldAt: item.soldAt ? item.soldAt.toISOString() : null, + createdAt: item.createdAt.toISOString(), + contentPreview: previewCard(item.content), + })), + }; +} diff --git a/pages/admin/cards/+Page.vue b/pages/admin/cards/+Page.vue index 242bfd7..bbd5664 100644 --- a/pages/admin/cards/+Page.vue +++ b/pages/admin/cards/+Page.vue @@ -13,9 +13,7 @@

单条新增

@@ -26,9 +24,7 @@

批量导入

@@ -45,109 +41,154 @@
-
-

库存列表

-
- - - - - - - - - - - - - - - - - - - - - - - - - - -
ID商品卡密预览批次状态订单创建时间
当前还没有库存卡密。
{{ card.id }}{{ card.productName }}{{ card.contentPreview }}{{ card.batchNo || '-' }} - - {{ getStatusLabel(card.status) }} - - {{ card.orderId || '-' }}{{ formatDate(card.createdAt) }}
+
+

库存列表

+ + +
+ + + + +
+
+ + +
+ + + + + + +
+ \ No newline at end of file diff --git a/pages/admin/cards/+data.ts b/pages/admin/cards/+data.ts index 427165d..4987a43 100644 --- a/pages/admin/cards/+data.ts +++ b/pages/admin/cards/+data.ts @@ -1,7 +1,7 @@ import { getAdminProducts } from "../../../modules/catalog/service"; import { getAdminCards, getInventoryOverview } from "../../../modules/inventory/service"; -export type Data = ReturnType; +export type Data = Awaited>; export async function data(pageContext: { prisma: import("../../../generated/prisma/client").PrismaClient; diff --git a/pages/admin/cards/deleteCard.telefunc.ts b/pages/admin/cards/deleteCard.telefunc.ts new file mode 100644 index 0000000..566d5b0 --- /dev/null +++ b/pages/admin/cards/deleteCard.telefunc.ts @@ -0,0 +1,7 @@ +import { assertAdminAccess } from "../../../modules/auth/service"; +import { deleteCard } from "../../../modules/inventory/service"; + +export async function onDeleteCard(input: { id: number }) { + assertAdminAccess(); + return deleteCard(input); +} \ No newline at end of file diff --git a/pages/admin/cards/queryCards.telefunc.ts b/pages/admin/cards/queryCards.telefunc.ts new file mode 100644 index 0000000..bf6c5c2 --- /dev/null +++ b/pages/admin/cards/queryCards.telefunc.ts @@ -0,0 +1,15 @@ +import { assertAdminAccess } from "../../../modules/auth/service"; +import { getAdminCardsPaged } from "../../../modules/inventory/service"; + +export async function onQueryCards(params: { + productId?: number; + batchNo?: string; + status?: string; + startDate?: string; + endDate?: string; + page: number; + pageSize: number; +}) { + assertAdminAccess(); + return getAdminCardsPaged(params); +} \ No newline at end of file