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 @@
+
+
+
+
+
+
+ | {{ col.label }} |
+
+
+
+
+ | {{ emptyText }} |
+
+
+ |
+ {{ row[col.key] ?? '-' }}
+ |
+
+
+
+
+
+
+
共 {{ total }} 条,第 {{ page }}/{{ totalPages }} 页
+
+
+
+
+
+
+
+
+
+
\ 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
+ page = p">
+
+
+ {{ value }}
+
+
+
+ {{ new Date(value).toLocaleString() }}
+
+
+```
+
+### 完整示例
+
+```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