商品删除功能(需要二次确认)

This commit is contained in:
2026-07-06 16:34:54 +08:00
parent 8fc142b7c8
commit da80b28ba4
3 changed files with 58 additions and 10 deletions
+35 -9
View File
@@ -1,6 +1,6 @@
import { useState } from 'react';
import {
Button, Descriptions, Divider, Drawer, Form, Input, InputNumber, Modal, Select, Space,
Button, Descriptions, Divider, Drawer, Form, Input, InputNumber, Modal, Popconfirm, Select, Space,
Table, Tabs, Tag, Typography, message,
} from 'antd';
import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons';
@@ -244,6 +244,20 @@ export default function ProductsPage() {
const [drawerOpen, setDrawerOpen] = useState(false);
const [createOpen, setCreateOpen] = useState(false);
async function handleDelete(row: Row) {
try {
await request(`/admin/products/${row.id}`, { method: 'DELETE' });
message.success('已删除');
if (detail?.id === row.id) {
setDrawerOpen(false);
setDetail(null);
}
void reload();
} catch (e) {
message.error(e instanceof Error ? e.message : '删除失败');
}
}
const columns: ColumnsType<Row> = [
{ title: 'SKU', dataIndex: 'skuCode', width: 90 },
{ title: '商品名', dataIndex: 'name', width: 180, ellipsis: true },
@@ -255,14 +269,26 @@ export default function ProductsPage() {
{ title: '排序', dataIndex: 'sortOrder', width: 60 },
{ title: '创建', dataIndex: 'createdAt', width: 160, render: fmtTime },
{
title: '操作', width: 80,
title: '操作', width: 120,
render: (_, row) => (
<Button type="link" size="small" onClick={async () => {
const d = await request<Record<string, unknown>>(`/admin/products/${row.id}`);
setDetail(d);
editForm.setFieldsValue(mapDetailToForm(d));
setDrawerOpen(true);
}}></Button>
<Space size={0}>
<Button type="link" size="small" onClick={async () => {
const d = await request<Record<string, unknown>>(`/admin/products/${row.id}`);
setDetail(d);
editForm.setFieldsValue(mapDetailToForm(d));
setDrawerOpen(true);
}}></Button>
<Popconfirm
title="确认删除该商品?"
description={`将永久删除「${row.name}」(${row.skuCode}),此操作不可恢复。`}
okText="确认删除"
cancelText="取消"
okButtonProps={{ danger: true }}
onConfirm={() => handleDelete(row)}
>
<Button type="link" size="small" danger></Button>
</Popconfirm>
</Space>
),
},
];
@@ -283,7 +309,7 @@ export default function ProductsPage() {
</Form.Item>
<Form.Item><Button type="primary" htmlType="submit"></Button></Form.Item>
</Form>
<Table rowKey="id" className="admin-table-nowrap" loading={loading} columns={columns} dataSource={data?.items ?? []} scroll={{ x: 1100 }}
<Table rowKey="id" className="admin-table-nowrap" loading={loading} columns={columns} dataSource={data?.items ?? []} scroll={{ x: 1140 }}
pagination={{ current: page, pageSize, total: data?.total ?? 0, showSizeChanger: true, onChange: (p, ps) => { setPage(p); setPageSize(ps); } }} />
<Drawer title="编辑商品" width={720} open={drawerOpen} onClose={() => setDrawerOpen(false)}
extra={detail && (
@@ -1,4 +1,4 @@
import { Body, Controller, Get, Param, Post, Put, Query, UseGuards } from '@nestjs/common';
import { Body, Controller, Delete, Get, Param, Post, Put, Query, UseGuards } from '@nestjs/common';
import { HqAuthGuard } from '../../common/guards/hq-auth.guard';
import { AdminProductsService } from './admin-products.service';
import { AdminProductsQueryDto } from './dto/admin-query.dto';
@@ -28,4 +28,9 @@ export class AdminProductsController {
update(@Param('id') id: string, @Body() dto: UpdateProductDto) {
return this.service.update(BigInt(id), dto);
}
@Delete(':id')
remove(@Param('id') id: string) {
return this.service.remove(BigInt(id));
}
}
@@ -135,6 +135,23 @@ export class AdminProductsService {
return this.detail(id);
}
async remove(id: bigint) {
const product = await this.prisma.commonProductItem.findUnique({ where: { id } });
if (!product) throw new NotFoundException('商品不存在');
const orderCount = await this.prisma.order.count({ where: { productId: id } });
if (orderCount > 0) {
throw new BadRequestException(`该商品已有 ${orderCount} 笔关联订单,无法删除`);
}
await this.prisma.commonResource.deleteMany({
where: { ownerType: 'PRODUCT', ownerId: id },
});
await this.prisma.commonProductItem.delete({ where: { id } });
return { ok: true };
}
private formatProduct(
product: Prisma.CommonProductItemGetPayload<{ include: { coverResource: true } }>,
extraResources: Prisma.CommonResourceGetPayload<object>[],