import React, { useEffect, useRef, useState } from 'react'; import { Button, Card, Drawer, Form, Image, Input, InputNumber, message, Space, Table, Tag, Tree, TreeSelect, Typography, } from 'antd'; import { TableOutlined } from '@ant-design/icons'; import type { FormInstance, TableProps } from 'antd'; import XinTable from '@/components/XinTable'; import type { XinTableColumn, XinTableInstance, XinTableProps } from '@/components/XinTable/typings.ts'; import type IProduct from '@/domain/iProduct.ts'; import type { IBatchPriceUpdate, IPriceMatrixRow, IProductPrice } from '@/domain/iProduct.ts'; import { PRODUCT_STATUS_MAP } from '@/domain/iProduct.ts'; import type ICustomerLevel from '@/domain/iCustomerLevel.ts'; import type {IProductCategoryTree} from '@/domain/iProductCategory.ts'; import { getLevelOptions } from '@/api/customer/level.ts'; import { getSupplierOptions } from '@/api/customer/supplier.ts'; import type ISupplier from '@/domain/iSupplier.ts'; import { getCategoryTree } from '@/api/product/category.ts'; import { batchPrice, getPriceMatrix } from '@/api/product/goods.ts'; const { Title, Text } = Typography; /** * 等级价格表单 */ const LevelPriceFields: React.FC<{ form: FormInstance; levels: ICustomerLevel[]; }> = ({ form, levels }) => { const prices = Form.useWatch('prices', form) ?? []; if (levels.length === 0) { return ( 暂无可配置的客户等级,请先到「客户等级」中新增 ); } const setLevelPrice = (levelId: number, price: number | null) => { const next = prices.filter((p) => p.level_id !== levelId); if (price !== null) { next.push({ level_id: levelId, price }); } form.setFieldValue('prices', next); }; return ( {levels.map((level) => { if (level.id == null) return null; const row = prices.find((p) => p.level_id === level.id); return ( {level.name}} suffix={'¥'} placeholder="未设定" value={(row?.price as number | null) ?? null} onChange={(v) => setLevelPrice(level.id as number, v)} style={{ width: 240 }} /> ); })} ); }; /** * 商品档案管理(A1 商品列表 / A2 价格矩阵与批量调价) */ const ProductGoodsPage: React.FC = () => { const [levels, setLevels] = useState([]); const [suppliers, setSuppliers] = useState([]); const [categoryTree, setCategoryTree] = useState([]); // ===== 分类侧栏 ===== const [activeCategory, setActiveCategory] = useState(undefined); const tableRef = useRef | null>(null); useEffect(() => { tableRef.current?.reset() }, [activeCategory]); // ===== 价格矩阵抽屉 ===== const [matrixOpen, setMatrixOpen] = useState(false); const [matrixLoading, setMatrixLoading] = useState(false); const [saveLoading, setSaveLoading] = useState(false); const [matrixRows, setMatrixRows] = useState([]); const [matrixPage, setMatrixPage] = useState(1); const [matrixPageSize, setMatrixPageSize] = useState(20); const [matrixTotal, setMatrixTotal] = useState(0); const [matrixLevels, setMatrixLevels] = useState([]); const [matrixKeyword, setMatrixKeyword] = useState(''); const [matrixCategory, setMatrixCategory] = useState(undefined); /** 跨页未保存的调价:`${productId}:${levelId}` → price(用 ref 避免异步闭包读到旧值) */ const matrixDirtyRef = useRef>({}); useEffect(() => { getLevelOptions().then((res) => setLevels(res.data.data ?? [])); getSupplierOptions().then((res) => setSuppliers(res.data.data ?? [])); getCategoryTree().then((res) => setCategoryTree(res.data.data ?? [])); }, []); const loadMatrix = async ( keyword = matrixKeyword, categoryId = matrixCategory, page = matrixPage, pageSize = matrixPageSize ) => { setMatrixLoading(true); try { const res = await getPriceMatrix({ keyword: keyword || undefined, category_id: categoryId, page, pageSize, }); const rows = res.data.data?.rows ?? []; // 叠加跨页未保存的修改,保证翻页后输入值不回退 const dirty = matrixDirtyRef.current; const merged = rows.map((row) => { const next = { ...row }; Object.keys(dirty).forEach((key) => { const [pid, lid] = key.split(':'); if (String(row.id) === pid) { next[`price_${lid}`] = dirty[key]; } }); return next; }); setMatrixRows(merged); setMatrixTotal(res.data.data?.total ?? 0); setMatrixLevels(res.data.data?.levels ?? []); } finally { setMatrixLoading(false); } }; const openMatrix = () => { setMatrixOpen(true); setMatrixPage(1); setMatrixPageSize(20); loadMatrix('', undefined, 1, 20); }; const onMatrixPriceChange = ( productId: number, levelId: number, value: number | null ) => { setMatrixRows((prev) => prev.map((row) => row.id === productId ? { ...row, [`price_${levelId}`]: value } : row ) ); matrixDirtyRef.current[`${productId}:${levelId}`] = value; }; /** 提交所有跨页未保存的调价(null 视为清除,不提交) */ const saveMatrix = async () => { const updates: IBatchPriceUpdate[] = []; Object.entries(matrixDirtyRef.current).forEach(([key, price]) => { if (price === null || price === undefined) return; const [pid, lid] = key.split(':'); updates.push({ product_id: Number(pid), level_id: Number(lid), price }); }); if (updates.length === 0) { message.info('没有需要保存的价格调整'); return; } setSaveLoading(true); try { await batchPrice(updates); message.success(`已更新 ${updates.length} 条价格,受影响门店将收到通知`); matrixDirtyRef.current = {}; await loadMatrix(); } finally { setSaveLoading(false); } }; const matrixColumns: TableProps['columns'] = [ { title: '商品', dataIndex: 'name', fixed: 'left', width: 160, render: (name: string, row) => (
{name}
{row.spec} {row.unit ? ` / ${row.unit}` : ''}
), }, ...matrixLevels.map((level) => ({ title: level.name, key: `price_${level.id}`, width: 150, render: (_: unknown, row: IPriceMatrixRow) => ( onMatrixPriceChange(row.id, level.id!, v)} className="w-32" /> ), })), ]; const columns: XinTableColumn[] = [ { title: 'ID', dataIndex: 'id', hideInForm: true, hideInSearch: true, width: 70, align: 'center', }, { title: '商品图片', dataIndex: 'image_ids', valueType: 'image', fieldProps: { action: '/product/goods/upload', maxWidth: 3000, maxHeight: 3000, maxSize: 10, mode: 'multiple', maxCount: 5, changeType: "id" }, render: (_, record) => { if (!record.images_arr || record.images_arr?.length <= 0) return '-'; return ( {record.images_arr.map(i => ( ))} ); }, align: 'center', hideInSearch: true, colProps: { span: 24 }, }, { title: '商品名称', dataIndex: 'name', valueType: 'text', colProps: { span: 24 }, required: true, render: (_, record) => { return (
{ record.name }
{ record.remark }
) }, rules: [{ required: true, message: '请输入商品名称' }], }, { title: '商品描述', dataIndex: 'remark', valueType: 'text', hideInSearch: true, hideInTable: true, colProps: { span: 24 }, }, { title: '规格/包规', dataIndex: 'spec', valueType: 'text', hideInSearch: true, align: "center", }, { title: '单位', dataIndex: 'unit', valueType: 'text', hideInSearch: true, initialValue: '斤', align: "center", }, { title: '分类', dataIndex: 'category_id', valueType: 'treeSelect', required: true, align: "center", rules: [{ required: true, message: '请选择分类' }], fieldProps: { treeData: categoryTree, fieldNames: { label: 'name', value: 'id', children: 'children' }, treeDefaultExpandAll: true, showSearch: true, treeNodeFilterProp: 'name', placeholder: '选择分类', }, hideInSearch: true, render: (_, record) => record.category ? {record.category.name} : '-', }, { title: '供应商', dataIndex: 'supplier_id', valueType: 'select', hideInSearch: true, align: "center", fieldProps: { options: suppliers.map((s) => ({ label: s.name, value: s.id })), showSearch: true, optionFilterProp: 'label', allowClear: true, placeholder: '默认供应商(可选)', }, render: (_, record) => record.supplier?.name ?? '-', }, { title: '图文详情', dataIndex: 'content', valueType: 'richText', hideInSearch: true, hideInTable: true, colProps: { span: 24 }, fieldProps: { height: 400, groupId: 11, placeholder: '输入商品图文详情,支持插入图片', }, }, { title: '等级价格', dataIndex: 'prices', hideInForm: true, hideInSearch: true, width: 370, align: 'center', render: (_, record) => ( {record.prices?.length ? record.prices.map((p) => ( {p.level?.name ?? `等级${p.level_id}`} ¥{p.price ?? '未设定'} )) : '-'} ), }, { title: '排序', dataIndex: 'sort', valueType: 'digit', hideInSearch: true, fieldProps: { min: 0 }, align: 'center', }, { title: '状态', dataIndex: 'status', valueType: 'radioButton', initialValue: 1, fieldProps: { options: [ { value: 1, label: '上架' }, { value: 0, label: '下架' }, ], }, render: (_, record) => { const item = PRODUCT_STATUS_MAP[record.status ?? 1]; return {item?.text}; }, align: 'center', }, { title: '等级价格设置', dataIndex: 'prices', hideInTable: true, hideInSearch: true, colProps: { span: 24 }, fieldRender: (form) => , }, { title: '创建时间', dataIndex: 'created_at', hideInForm: true, hideInSearch: true, align: 'center', }, ]; const tableProps: XinTableProps = { api: '/product/goods', columns, rowKey: 'id', accessName: 'product.goods', tableRef, scroll: { x: 1200 }, actionBarRender: (dom) => [ dom.add, dom.search, , dom.keywordSearch, ], formProps: { grid: true, colProps: { span: 12 }, rowProps: { gutter: 20 }, layout: 'vertical', }, requestParams: (params) => ({ ...params, category_id: activeCategory }), modalProps: { width: 800, centered: true, classNames: { body: "overflow-y-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden h-[80vh]" }, }, }; return ( <>
商品列表 商品档案与多等级价格体系;「价格矩阵」支持按等级批量调价,调价后自动通知受影响门店。
{ setActiveCategory(undefined) }} > 全部商品
showLine blockNode onSelect={(selectedKeys) => setActiveCategory(Number(selectedKeys[0]))} treeData={categoryTree} selectedKeys={activeCategory ? [activeCategory] : undefined} fieldNames={{title: 'name', key: 'id', children: 'children'}} />
{...tableProps} />
setMatrixOpen(false)} size={1200} extra={ } > { setMatrixCategory(v); setMatrixPage(1); loadMatrix(matrixKeyword, v, 1, matrixPageSize); }} /> setMatrixKeyword(e.target.value)} onSearch={(v) => { setMatrixPage(1); loadMatrix(v, matrixCategory, 1, matrixPageSize); }} /> rowKey="id" size="small" loading={matrixLoading} columns={matrixColumns} dataSource={matrixRows} pagination={{ current: matrixPage, pageSize: matrixPageSize, total: matrixTotal, showSizeChanger: true, pageSizeOptions: [20, 50, 100], showTotal: (total) => `共 ${total} 件商品`, onChange: (page, pageSize) => { setMatrixPage(page); setMatrixPageSize(pageSize); loadMatrix(matrixKeyword, matrixCategory, page, pageSize); }, }} scroll={{ x: matrixLevels.length * 150 + 160 }} /> ); }; export default ProductGoodsPage;