import React, { useEffect, useState } from 'react'; import { Button, Drawer, Form, Input, InputNumber, message, Select, Space, Table, Tag, TreeSelect, Typography, } from 'antd'; import { MinusCircleOutlined, PlusOutlined, TableOutlined } from '@ant-design/icons'; import type { TableProps } from 'antd'; import XinTable from '@/components/XinTable'; import type { XinTableColumn, XinTableProps } from '@/components/XinTable/typings.ts'; import type IProduct from '@/domain/iProduct.ts'; import type { IBatchPriceUpdate, IPriceMatrixRow } from '@/domain/iProduct.ts'; import { PRODUCT_STATUS_MAP } from '@/domain/iProduct.ts'; import type ICustomerLevel from '@/domain/iCustomerLevel.ts'; import type IProductCategory 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; /** * 商品档案管理(A1 商品列表 / A2 价格矩阵与批量调价) */ const ProductGoodsPage: React.FC = () => { const [levels, setLevels] = useState([]); const [suppliers, setSuppliers] = useState([]); const [categoryTree, setCategoryTree] = useState([]); // ===== 价格矩阵抽屉 ===== const [matrixOpen, setMatrixOpen] = useState(false); const [matrixLoading, setMatrixLoading] = useState(false); const [saveLoading, setSaveLoading] = useState(false); const [matrixRows, setMatrixRows] = useState([]); const [matrixSnapshot, setMatrixSnapshot] = useState([]); const [matrixLevels, setMatrixLevels] = useState([]); const [matrixKeyword, setMatrixKeyword] = useState(''); const [matrixCategory, setMatrixCategory] = useState(undefined); 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 ) => { setMatrixLoading(true); try { const res = await getPriceMatrix({ keyword: keyword || undefined, category_id: categoryId, }); const rows = res.data.data?.rows ?? []; setMatrixRows(rows); setMatrixSnapshot(JSON.parse(JSON.stringify(rows))); setMatrixLevels(res.data.data?.levels ?? []); } finally { setMatrixLoading(false); } }; const openMatrix = () => { setMatrixOpen(true); loadMatrix('', undefined); }; const onMatrixPriceChange = ( productId: number, levelId: number, value: number | null ) => { setMatrixRows((prev) => prev.map((row) => row.id === productId ? { ...row, [`price_${levelId}`]: value } : row ) ); }; /** diff 出被修改的价格行,提交批量调价 */ const saveMatrix = async () => { const updates: IBatchPriceUpdate[] = []; for (const row of matrixRows) { const old = matrixSnapshot.find((r) => r.id === row.id); for (const level of matrixLevels) { const key = `price_${level.id}`; const next = row[key]; const prev = old?.[key]; if (next !== null && next !== undefined && String(next) !== String(prev ?? '')) { updates.push({ product_id: row.id, level_id: level.id!, price: next as number }); } } } if (updates.length === 0) { message.info('没有需要保存的价格调整'); return; } setSaveLoading(true); try { await batchPrice(updates); message.success(`已更新 ${updates.length} 条价格,受影响门店将收到通知`); 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: 'name', valueType: 'text', required: true, rules: [{ required: true, message: '请输入商品名称' }], }, { title: '规格/包规', dataIndex: 'spec', valueType: 'text', hideInSearch: true, }, { title: '等级', dataIndex: 'grade', valueType: 'text', hideInSearch: true, }, { title: '单位', dataIndex: 'unit', valueType: 'text', hideInSearch: true, initialValue: '斤', }, { title: '分类', dataIndex: 'category_id', valueType: 'treeSelect', required: true, rules: [{ required: true, message: '请选择分类' }], fieldProps: { treeData: categoryTree, fieldNames: { label: 'name', value: 'id', children: 'children' }, treeDefaultExpandAll: true, showSearch: true, treeNodeFilterProp: 'name', placeholder: '选择分类', }, render: (_, record) => record.category ? {record.category.name} : '-', }, { title: '供应商', dataIndex: 'supplier_id', valueType: 'select', hideInSearch: true, 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: 'prices', hideInForm: true, hideInSearch: true, 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: 'remark', valueType: 'textarea', hideInSearch: true, hideInTable: true, fieldProps: { rows: 2 }, }, { title: '等级价格设置', dataIndex: 'prices', hideInTable: true, hideInSearch: true, fieldRender: () => ( {(fields, { add, remove }) => (
{fields.map(({ key, name, ...restField }) => (