diff --git a/backend/internal/handler/merchant.go b/backend/internal/handler/merchant.go index 74685d0..6e9d301 100644 --- a/backend/internal/handler/merchant.go +++ b/backend/internal/handler/merchant.go @@ -77,6 +77,32 @@ type merchantProductStatusReq struct { // UpdateProductStatus PATCH /api/merchant/products/:id/status // 商户仅能上架/下架商品,价格、成本等配置由平台管理员维护。 +// UpdateProduct PATCH /api/merchant/products/:id — 编辑商品(显示名称、售价、成本、库存、状态)。 +func (h *MerchantHandler) UpdateProduct(c *gin.Context) { + id, _ := strconv.ParseUint(c.Param("id"), 10, 64) + if id == 0 { + response.BadRequest(c, "参数错误") + return + } + var req merchantProductUpdateReq + if err := c.ShouldBindJSON(&req); err != nil { + response.BadRequest(c, "参数错误") + return + } + if err := h.merchantSvc.UpdateMerchantProduct(middleware.GetMerchantID(c), uint(id), service.UpdateMerchantProductInput{ + DisplayName: req.DisplayName, + PriceAmount: req.PriceAmount, + CostAmount: req.CostAmount, + Stock: req.Stock, + Status: req.Status, + FulfillmentConfig: req.FulfillmentConfig, + }, middleware.GetUserID(c)); err != nil { + response.BadRequest(c, err.Error()) + return + } + response.OK(c, nil) +} + func (h *MerchantHandler) UpdateProductStatus(c *gin.Context) { var req merchantProductStatusReq if err := c.ShouldBindJSON(&req); err != nil { diff --git a/backend/internal/router/router.go b/backend/internal/router/router.go index 8f88a1d..1ca9ab5 100644 --- a/backend/internal/router/router.go +++ b/backend/internal/router/router.go @@ -236,6 +236,7 @@ func Setup(h *Handlers) *gin.Engine { { merchant.GET("", h.Merchant.Current) merchant.GET("/products", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureProducts), middleware.RequireMerchantPermissions(h.Tenant, model.PermissionProductsManage), h.Merchant.ListProducts) + merchant.PATCH("/products/:id", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureProducts), middleware.RequireMerchantPermissions(h.Tenant, model.PermissionProductsManage), h.Merchant.UpdateProduct) merchant.PATCH("/products/:id/status", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureProducts), middleware.RequireMerchantPermissions(h.Tenant, model.PermissionProductsManage), h.Merchant.UpdateProductStatus) merchant.DELETE("/products/:id", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureProducts), middleware.RequireMerchantPermissions(h.Tenant, model.PermissionProductsManage), h.Merchant.DeleteProduct) merchant.GET("/orders", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureOrders), middleware.RequireMerchantPermissions(h.Tenant, model.PermissionOrdersManage), h.Merchant.ListOrders) diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index ad27635..a52e81b 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -69,6 +69,16 @@ export const merchantApi = { request.get('/merchant').then((r) => r.data.data as { merchant: Merchant; role: string; permissions: string[] }), products: (params?: Record) => request.get('/merchant/products', { params }).then((r) => r.data.data as PageResult), + updateProduct: ( + id: number, + data: { + display_name?: string + price_amount?: number + cost_amount?: number + stock?: number + status?: 'active' | 'inactive' + } + ) => request.patch(`/merchant/products/${id}`, data).then((r) => r.data.data), updateProductStatus: (id: number, status: MerchantProduct['status']) => request.patch(`/merchant/products/${id}/status`, { status }).then((r) => r.data.data), deleteProduct: (id: number) => diff --git a/frontend/src/pages/MerchantCenter.tsx b/frontend/src/pages/MerchantCenter.tsx index 889dd38..dbf22b8 100644 --- a/frontend/src/pages/MerchantCenter.tsx +++ b/frontend/src/pages/MerchantCenter.tsx @@ -538,6 +538,26 @@ export default function MerchantCenter({ fixedTab, title = '商户中心' }: Mer } } + const updateProduct = async ( + id: number, + data: { + display_name?: string + price_amount?: number + cost_amount?: number + stock?: number + status?: 'active' | 'inactive' + } + ) => { + try { + await merchantApi.updateProduct(id, data) + message.success('商品信息更新成功') + loadProducts(products.page, products.size) + } catch (e) { + message.error(e instanceof Error ? e.message : '更新商品失败') + throw e + } + } + const tabItems = [ { key: 'products', label: '商品列表', disabled: !hasFeature('products'), children: ( ) }, { key: 'orders', label: '发货订单', disabled: !hasFeature('orders'), children: ( diff --git a/frontend/src/pages/merchantCenterTabs.tsx b/frontend/src/pages/merchantCenterTabs.tsx index becba7f..006e1c5 100644 --- a/frontend/src/pages/merchantCenterTabs.tsx +++ b/frontend/src/pages/merchantCenterTabs.tsx @@ -1,5 +1,5 @@ import { useEffect, useState } from 'react' -import { Alert, Button, Card, Col, Form, Input, Modal, Popconfirm, Row, Select, Space, Table, Tabs, Tag, Typography, message } from 'antd' +import { Alert, Button, Card, Col, Form, Input, InputNumber, Modal, Popconfirm, Radio, Row, Select, Space, Table, Tabs, Tag, Typography, message } from 'antd' import type { FormInstance } from 'antd' import { ClockCircleOutlined, @@ -68,6 +68,7 @@ export function ProductsTab({ onDeleteProduct, onFilterChange, onLoadProducts, + onUpdateProduct, }: { loading: boolean products: PageResult @@ -77,8 +78,23 @@ export function ProductsTab({ onDeleteProduct: (record: MerchantProduct) => void onFilterChange: (filters: { keyword: string; status: string }) => void onLoadProducts: (page?: number, size?: number) => void + onUpdateProduct?: ( + id: number, + data: { + display_name?: string + price_amount?: number + cost_amount?: number + stock?: number + status?: 'active' | 'inactive' + } + ) => Promise }) { const [keyword, setKeyword] = useState(filters.keyword) + const [editModalOpen, setEditModalOpen] = useState(false) + const [editingProduct, setEditingProduct] = useState(null) + const [editForm] = Form.useForm() + const [editSubmitting, setEditSubmitting] = useState(false) + const [stockMode, setStockMode] = useState<'unlimited' | 'custom'>('unlimited') useEffect(() => { setKeyword(filters.keyword) @@ -86,6 +102,40 @@ export function ProductsTab({ const submitKeyword = () => onFilterChange({ ...filters, keyword: keyword.trim() }) + const openEditModal = (record: MerchantProduct) => { + setEditingProduct(record) + const isUnlimited = record.stock < 0 + setStockMode(isUnlimited ? 'unlimited' : 'custom') + editForm.setFieldsValue({ + display_name: record.display_name || record.product?.name || '', + price_amount: record.price_amount, + cost_amount: record.cost_amount, + stock_mode: isUnlimited ? 'unlimited' : 'custom', + stock_count: isUnlimited ? 100 : record.stock, + status: record.status, + }) + setEditModalOpen(true) + } + + const handleEditSubmit = async () => { + if (!editingProduct || !onUpdateProduct) return + const values = await editForm.validateFields() + setEditSubmitting(true) + try { + const stock = values.stock_mode === 'unlimited' ? -1 : Number(values.stock_count ?? 0) + await onUpdateProduct(editingProduct.id, { + display_name: values.display_name?.trim(), + price_amount: Number(values.price_amount), + cost_amount: Number(values.cost_amount), + stock, + status: values.status, + }) + setEditModalOpen(false) + } finally { + setEditSubmitting(false) + } + } + const productColumns: ColumnsType = [ { title: '商户 SKU', @@ -142,10 +192,17 @@ export function ProductsTab({ { title: '操作', key: 'action', - width: 140, + width: 160, fixed: 'right', render: (_, record) => canManage ? ( - + + 商品上架与售卖管理 - 可按状态与关键词筛选;已下架商品可删除。 + 可编辑商品名称、对外积分售价、成本价及库存模式;已下架商品可删除。 @@ -219,6 +276,87 @@ export function ProductsTab({ scroll={{ x: 1100 }} pagination={pageConfig(products, onLoadProducts)} /> + + {/* 编辑商品售价与库存 Modal */} + setEditModalOpen(false)} + confirmLoading={editSubmitting} + destroyOnClose + width={560} + > +
+ + {editingProduct?.sku} + + + + + + + + + + + + + + + + + + + + setStockMode(e.target.value)}> + 无限库存 (推荐) + 指定有限库存 + + + {stockMode === 'custom' && ( + + + + )} + + + + + 正常上架 (可售) + 立即下架 (不可售) + + +
+
) }