668 lines
21 KiB
TypeScript
668 lines
21 KiB
TypeScript
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||
import {
|
||
Alert,
|
||
Button, Card,
|
||
Drawer,
|
||
Image,
|
||
Input,
|
||
InputNumber,
|
||
message,
|
||
Modal,
|
||
Space,
|
||
Table,
|
||
Tag, Tree,
|
||
TreeSelect,
|
||
Typography,
|
||
Upload,
|
||
} from 'antd';
|
||
import { DownloadOutlined, InboxOutlined, TableOutlined, UploadOutlined } from '@ant-design/icons';
|
||
import type { TableProps } from 'antd';
|
||
import XinTable from '@/components/XinTable';
|
||
import AuthButton from '@/components/AuthButton';
|
||
import type { XinTableColumn, XinTableInstance, 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 {IProductCategoryTree} from '@/domain/iProductCategory.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';
|
||
import type { IImportError } from '@/api/product/goods.ts';
|
||
import { downloadProductTemplate, exportProducts, importProducts } from '@/api/product/goods.ts';
|
||
|
||
const { Title, Text } = Typography;
|
||
|
||
/** 四舍五入保留两位 */
|
||
const round2 = (v: number) => Math.round(v * 100) / 100;
|
||
|
||
/** 商品表单用分类树:有子分类的节点禁选(商品只能挂在末级分类上) */
|
||
const markParentDisabled = (nodes: IProductCategoryTree[]): IProductCategoryTree[] =>
|
||
nodes.map((node) => ({
|
||
...node,
|
||
disabled: !!node.children?.length,
|
||
children: node.children?.length ? markParentDisabled(node.children) : node.children,
|
||
}));
|
||
|
||
/** 侧栏用分类树:有子分类的节点不可选中(仅供展开,筛选按末级分类) */
|
||
const markParentUnselectable = (nodes: IProductCategoryTree[]): IProductCategoryTree[] =>
|
||
nodes.map((node) => ({
|
||
...node,
|
||
selectable: !node.children?.length,
|
||
children: node.children?.length ? markParentUnselectable(node.children) : node.children,
|
||
}));
|
||
|
||
/**
|
||
* 商品档案管理(A1 商品列表 / A2 价格矩阵与批量调价)
|
||
*
|
||
* 价格体系:售价 = 成本价 × (100 + 客户等级上浮比例) / 100;
|
||
* 等级上浮比例在「客户等级」中维护,本页价格矩阵仅批量调整成本价。
|
||
*/
|
||
const ProductGoodsPage: React.FC = () => {
|
||
const [suppliers, setSuppliers] = useState<ISupplier[]>([]);
|
||
const [categoryTree, setCategoryTree] = useState<IProductCategoryTree[]>([]);
|
||
// 商品表单专用分类树:父分类禁选,仅末级可选(侧栏筛选/价格矩阵仍用原始树)
|
||
const formCategoryTree = useMemo(() => markParentDisabled(categoryTree), [categoryTree]);
|
||
// 侧栏分类树:父分类不可选中,仅作展开归组
|
||
const sidebarCategoryTree = useMemo(() => markParentUnselectable(categoryTree), [categoryTree]);
|
||
|
||
// ===== 分类侧栏 =====
|
||
const [activeCategory, setActiveCategory] = useState<number | undefined>(undefined);
|
||
const tableRef = useRef<XinTableInstance<IProduct> | 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<IPriceMatrixRow[]>([]);
|
||
const [matrixPage, setMatrixPage] = useState(1);
|
||
const [matrixPageSize, setMatrixPageSize] = useState(20);
|
||
const [matrixTotal, setMatrixTotal] = useState(0);
|
||
const [matrixLevels, setMatrixLevels] = useState<ICustomerLevel[]>([]);
|
||
const [matrixKeyword, setMatrixKeyword] = useState('');
|
||
const [matrixCategory, setMatrixCategory] = useState<number | undefined>(undefined);
|
||
/** 跨页未保存的成本价:productId → cost(null 视为未修改,不提交) */
|
||
const costDirtyRef = useRef<Record<number, number | null>>({});
|
||
|
||
// ===== Excel 导入/导出 =====
|
||
const [exportLoading, setExportLoading] = useState(false);
|
||
const [importOpen, setImportOpen] = useState(false);
|
||
const [importFile, setImportFile] = useState<File | null>(null);
|
||
const [importing, setImporting] = useState(false);
|
||
const [importErrors, setImportErrors] = useState<IImportError[]>([]);
|
||
|
||
/** 导出商品列表(跟随侧栏分类筛选;列格式与导入模板一致) */
|
||
const handleExport = async () => {
|
||
setExportLoading(true);
|
||
try {
|
||
await exportProducts(activeCategory ? { category_id: activeCategory } : {});
|
||
} finally {
|
||
setExportLoading(false);
|
||
}
|
||
};
|
||
|
||
/** 提交导入:成功刷新列表;校验失败展示错误行明细(错误提示由拦截器统一弹出) */
|
||
const handleImport = async () => {
|
||
if (!importFile) {
|
||
return;
|
||
}
|
||
setImporting(true);
|
||
setImportErrors([]);
|
||
try {
|
||
await importProducts(importFile);
|
||
setImportOpen(false);
|
||
setImportFile(null);
|
||
tableRef.current?.reset();
|
||
} catch (err: any) {
|
||
const errors = err?.data?.data?.errors;
|
||
if (Array.isArray(errors)) {
|
||
setImportErrors(errors);
|
||
}
|
||
} finally {
|
||
setImporting(false);
|
||
}
|
||
};
|
||
|
||
useEffect(() => {
|
||
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 levels = res.data.data?.levels ?? [];
|
||
// 叠加跨页未保存的成本价修改,并重算各等级展示价,保证翻页后输入值不回退
|
||
const merged = rows.map((row) => {
|
||
const dirty = costDirtyRef.current[row.id];
|
||
if (dirty === undefined) return row;
|
||
const next: IPriceMatrixRow = { ...row, cost_price: dirty };
|
||
const cost = Number(dirty ?? 0);
|
||
levels.forEach((level) => {
|
||
if (level.id == null) return;
|
||
next[`price_${level.id}`] = cost > 0
|
||
? round2(cost * (1 + Number(level.percent ?? 0) / 100))
|
||
: null;
|
||
});
|
||
return next;
|
||
});
|
||
setMatrixRows(merged);
|
||
setMatrixTotal(res.data.data?.total ?? 0);
|
||
setMatrixLevels(levels);
|
||
} finally {
|
||
setMatrixLoading(false);
|
||
}
|
||
};
|
||
|
||
const openMatrix = () => {
|
||
setMatrixOpen(true);
|
||
setMatrixPage(1);
|
||
setMatrixPageSize(20);
|
||
loadMatrix('', undefined, 1, 20);
|
||
};
|
||
|
||
/** 修改成本价:联动重算各等级展示价(上浮比例取 levels 配置) */
|
||
const onMatrixCostChange = (productId: number, value: number | null) => {
|
||
costDirtyRef.current[productId] = value;
|
||
setMatrixRows((prev) =>
|
||
prev.map((row) => {
|
||
if (row.id !== productId) return row;
|
||
const next: IPriceMatrixRow = { ...row, cost_price: value };
|
||
const cost = Number(value ?? 0);
|
||
matrixLevels.forEach((level) => {
|
||
if (level.id == null) return;
|
||
next[`price_${level.id}`] = cost > 0
|
||
? round2(cost * (1 + Number(level.percent ?? 0) / 100))
|
||
: null;
|
||
});
|
||
return next;
|
||
})
|
||
);
|
||
};
|
||
|
||
/** 提交所有跨页未保存的成本价调整(null 视为清除,不提交) */
|
||
const saveMatrix = async () => {
|
||
const updates: IBatchPriceUpdate[] = [];
|
||
Object.entries(costDirtyRef.current).forEach(([pid, cost]) => {
|
||
if (cost === null || cost === undefined) return;
|
||
updates.push({ product_id: Number(pid), cost_price: cost });
|
||
});
|
||
if (updates.length === 0) {
|
||
message.info('没有需要保存的价格调整');
|
||
return;
|
||
}
|
||
setSaveLoading(true);
|
||
try {
|
||
await batchPrice(updates);
|
||
message.success(`已更新 ${updates.length} 件商品成本价,各等级售价已按上浮比例联动,受影响门店将收到通知`);
|
||
costDirtyRef.current = {};
|
||
await loadMatrix();
|
||
} finally {
|
||
setSaveLoading(false);
|
||
}
|
||
};
|
||
|
||
const matrixColumns: TableProps<IPriceMatrixRow>['columns'] = [
|
||
{
|
||
title: '商品',
|
||
dataIndex: 'name',
|
||
fixed: 'left',
|
||
width: 160,
|
||
render: (name: string, row) => (
|
||
<div>
|
||
<div className="font-medium">{name}</div>
|
||
<Text type="secondary" className="text-xs">
|
||
{row.spec}
|
||
{row.unit ? ` / ${row.unit}` : ''}
|
||
</Text>
|
||
</div>
|
||
),
|
||
},
|
||
{
|
||
title: '成本价',
|
||
key: 'cost_price',
|
||
fixed: 'left',
|
||
width: 130,
|
||
render: (_: unknown, row: IPriceMatrixRow) => (
|
||
<InputNumber
|
||
size="small"
|
||
min={0}
|
||
precision={2}
|
||
prefix="¥"
|
||
value={row.cost_price as number | null}
|
||
onChange={(v) => onMatrixCostChange(row.id, v)}
|
||
className="w-32"
|
||
/>
|
||
),
|
||
},
|
||
...matrixLevels.map((level) => ({
|
||
title: level.name,
|
||
key: `price_${level.id}`,
|
||
width: 150,
|
||
render: (_: unknown, row: IPriceMatrixRow) => {
|
||
const price = row[`price_${level.id}`];
|
||
return (
|
||
<div>
|
||
<div>{price != null ? `¥${price}` : <Text type="secondary">未设成本价</Text>}</div>
|
||
<div className="text-xs text-gray-400">成本上浮 {Number(level.percent ?? 0)}%</div>
|
||
</div>
|
||
);
|
||
},
|
||
})),
|
||
];
|
||
|
||
const columns: XinTableColumn<IProduct>[] = [
|
||
{
|
||
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 (
|
||
<Space wrap={false}>
|
||
{record.images_arr.map(i => (
|
||
<Image
|
||
key={i.id}
|
||
src={i.preview_url}
|
||
width={60}
|
||
height={60}
|
||
style={{ objectFit: 'cover', borderRadius: 4 }}
|
||
/>
|
||
))}
|
||
</Space>
|
||
);
|
||
},
|
||
align: 'center',
|
||
hideInSearch: true,
|
||
colProps: { span: 24 },
|
||
},
|
||
{
|
||
title: '商品名称',
|
||
dataIndex: 'name',
|
||
valueType: 'text',
|
||
colProps: { span: 24 },
|
||
required: true,
|
||
render: (_, record) => {
|
||
return (
|
||
<div>
|
||
<div className={"mb-1.5"}>{ record.name }</div>
|
||
<div className={"text-[#999] text-[12px]"}>{ record.remark }</div>
|
||
</div>
|
||
)
|
||
},
|
||
rules: [{ required: true, message: '请输入商品名称' }],
|
||
},
|
||
{
|
||
title: '商品描述',
|
||
dataIndex: 'remark',
|
||
valueType: 'text',
|
||
hideInSearch: true,
|
||
hideInTable: true,
|
||
colProps: { span: 24 },
|
||
},
|
||
{
|
||
title: '成本价',
|
||
dataIndex: 'cost_price',
|
||
valueType: 'digit',
|
||
hideInSearch: true,
|
||
align: 'center',
|
||
tooltip: '各等级售价 = 成本价 × (100 + 等级上浮比例) / 100',
|
||
fieldProps: { min: 0, precision: 2, prefix: '¥' },
|
||
render: (_, record) => {
|
||
const cost = Number(record.cost_price ?? 0);
|
||
return cost > 0 ? `¥${record.cost_price}` : <Text type="secondary">未设置</Text>;
|
||
},
|
||
},
|
||
{
|
||
title: '规格/包规',
|
||
dataIndex: 'spec',
|
||
valueType: 'text',
|
||
hideInSearch: true,
|
||
align: "center",
|
||
},
|
||
{
|
||
title: '单位',
|
||
dataIndex: 'unit',
|
||
valueType: 'text',
|
||
hideInSearch: true,
|
||
initialValue: '斤',
|
||
align: "center",
|
||
},
|
||
{
|
||
title: '排序',
|
||
dataIndex: 'sort',
|
||
valueType: 'digit',
|
||
hideInSearch: true,
|
||
fieldProps: { min: 0 },
|
||
align: 'center',
|
||
},
|
||
{
|
||
title: '分类',
|
||
dataIndex: 'category_id',
|
||
valueType: 'treeSelect',
|
||
required: true,
|
||
align: "center",
|
||
rules: [{ required: true, message: '请选择分类' }],
|
||
fieldProps: {
|
||
treeData: formCategoryTree,
|
||
fieldNames: { label: 'name', value: 'id', children: 'children' },
|
||
treeDefaultExpandAll: true,
|
||
showSearch: true,
|
||
treeNodeFilterProp: 'name',
|
||
placeholder: '选择末级分类',
|
||
},
|
||
hideInSearch: true,
|
||
render: (_, record) =>
|
||
record.category ? <Tag color="cyan">{record.category.name}</Tag> : '-',
|
||
},
|
||
{
|
||
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: 'status',
|
||
valueType: 'radioButton',
|
||
initialValue: 1,
|
||
fieldProps: {
|
||
options: [
|
||
{ value: 1, label: '上架' },
|
||
{ value: 0, label: '下架' },
|
||
],
|
||
},
|
||
render: (_, record) => {
|
||
const item = PRODUCT_STATUS_MAP[record.status ?? 1];
|
||
return <Tag color={item?.color}>{item?.text}</Tag>;
|
||
},
|
||
align: 'center',
|
||
},
|
||
{
|
||
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: 420,
|
||
align: 'center',
|
||
render: (_, record) => (
|
||
<Space wrap>
|
||
{record.prices?.length
|
||
? record.prices.map((p) => (
|
||
<Tag
|
||
key={p.level_id}
|
||
color="geekblue"
|
||
title={`按成本价上浮 ${p.percent ?? 0}%`}
|
||
>
|
||
{p.level?.name ?? `等级${p.level_id}`}
|
||
<span style={{color: 'red', marginLeft: 5 }}>
|
||
¥{p.price ?? '未设定'}
|
||
</span>
|
||
</Tag>
|
||
))
|
||
: '-'}
|
||
</Space>
|
||
),
|
||
},
|
||
{
|
||
title: '创建时间',
|
||
dataIndex: 'created_at',
|
||
hideInForm: true,
|
||
hideInSearch: true,
|
||
align: 'center',
|
||
},
|
||
];
|
||
|
||
const tableProps: XinTableProps<IProduct> = {
|
||
api: '/product/goods',
|
||
columns,
|
||
rowKey: 'id',
|
||
accessName: 'product.goods',
|
||
tableRef,
|
||
scroll: { x: 1200 },
|
||
actionBarRender: (dom) => [
|
||
dom.add,
|
||
dom.search,
|
||
<Button key="matrix" icon={<TableOutlined />} onClick={openMatrix}>
|
||
价格矩阵
|
||
</Button>,
|
||
<AuthButton key="export" auth="product.goods.export">
|
||
<Button icon={<DownloadOutlined />} loading={exportLoading} onClick={handleExport}>
|
||
导出
|
||
</Button>
|
||
</AuthButton>,
|
||
<AuthButton key="import" auth="product.goods.import">
|
||
<Button icon={<UploadOutlined />} onClick={() => setImportOpen(true)}>
|
||
导入
|
||
</Button>
|
||
</AuthButton>,
|
||
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 (
|
||
<>
|
||
<div className="mb-5">
|
||
<Title level={3}>商品列表</Title>
|
||
<Text type="secondary">
|
||
商品售价按客户等级上浮比例自动换算(成本价 × (100 + 上浮比例) / 100);「价格矩阵」支持批量调整成本价,调价后自动通知受影响门店。
|
||
</Text>
|
||
</div>
|
||
<div className="flex items-start gap-4">
|
||
<Card style={{ width: 200 }} title={'商品分类'}>
|
||
<div
|
||
className={`cursor-pointer rounded-2xl mb-2 px-2 h-6 leading-6 ${activeCategory === undefined ? 'bg-blue-50' : 'hover:bg-gray-50'}`}
|
||
onClick={() => { setActiveCategory(undefined) }}
|
||
>
|
||
全部商品
|
||
</div>
|
||
<Tree<any>
|
||
showLine
|
||
blockNode
|
||
onSelect={(selectedKeys) => setActiveCategory(Number(selectedKeys[0]))}
|
||
treeData={sidebarCategoryTree}
|
||
selectedKeys={activeCategory ? [activeCategory] : undefined}
|
||
fieldNames={{title: 'name', key: 'id', children: 'children'}}
|
||
/>
|
||
</Card>
|
||
<div className="min-w-0 flex-1">
|
||
<XinTable<IProduct> {...tableProps} />
|
||
</div>
|
||
</div>
|
||
|
||
<Modal
|
||
title="导入商品"
|
||
open={importOpen}
|
||
onCancel={() => setImportOpen(false)}
|
||
onOk={handleImport}
|
||
okText="开始导入"
|
||
okButtonProps={{ disabled: !importFile }}
|
||
confirmLoading={importing}
|
||
destroyOnHidden
|
||
width={560}
|
||
>
|
||
<div className="mb-3">
|
||
<Text type="secondary">
|
||
列格式:品名*、分类*(末级分类,支持「父分类/子分类」路径)、供应商(不存在自动创建)、
|
||
规格/包规、单位(默认斤)、成本价、排序、库存、保质期、状态(上架/下架)、备注。
|
||
导入一律新增商品;整表校验,任何一行有错则全部不导入。
|
||
</Text>
|
||
<Button type="link" className="px-0" onClick={downloadProductTemplate}>
|
||
下载导入模板
|
||
</Button>
|
||
</div>
|
||
<Upload.Dragger
|
||
accept=".xlsx,.xls"
|
||
maxCount={1}
|
||
beforeUpload={(file) => {
|
||
setImportFile(file);
|
||
setImportErrors([]);
|
||
return false;
|
||
}}
|
||
onRemove={() => {
|
||
setImportFile(null);
|
||
setImportErrors([]);
|
||
}}
|
||
>
|
||
<p className="ant-upload-drag-icon">
|
||
<InboxOutlined />
|
||
</p>
|
||
<p className="ant-upload-text">点击或拖拽 Excel 文件到此区域</p>
|
||
<p className="ant-upload-hint">仅支持 .xlsx / .xls,单次最多 1000 行</p>
|
||
</Upload.Dragger>
|
||
{importErrors.length > 0 && (
|
||
<Alert
|
||
className="mt-3"
|
||
type="error"
|
||
showIcon
|
||
message={`共 ${importErrors.length} 处错误,修正后请重新导入`}
|
||
description={
|
||
<ul className="max-h-48 overflow-y-auto pl-4 mb-0 list-disc">
|
||
{importErrors.map((item, index) => (
|
||
<li key={index}>第 {item.row} 行:{item.message}</li>
|
||
))}
|
||
</ul>
|
||
}
|
||
/>
|
||
)}
|
||
</Modal>
|
||
|
||
<Drawer
|
||
title="价格矩阵 · 批量调价"
|
||
open={matrixOpen}
|
||
onClose={() => setMatrixOpen(false)}
|
||
size={1200}
|
||
extra={
|
||
<Space>
|
||
<Button onClick={() => loadMatrix()}>刷新</Button>
|
||
<Button type="primary" loading={saveLoading} onClick={saveMatrix}>
|
||
保存调价
|
||
</Button>
|
||
</Space>
|
||
}
|
||
>
|
||
<Space className="mb-4" wrap>
|
||
<TreeSelect
|
||
style={{ width: 180 }}
|
||
placeholder="按分类筛选"
|
||
allowClear
|
||
treeDefaultExpandAll
|
||
fieldNames={{ label: 'name', value: 'id', children: 'children' }}
|
||
treeData={categoryTree}
|
||
value={matrixCategory}
|
||
onChange={(v) => {
|
||
setMatrixCategory(v);
|
||
setMatrixPage(1);
|
||
loadMatrix(matrixKeyword, v, 1, matrixPageSize);
|
||
}}
|
||
/>
|
||
<Input.Search
|
||
style={{ width: 240 }}
|
||
placeholder="搜索品名/规格"
|
||
allowClear
|
||
value={matrixKeyword}
|
||
onChange={(e) => setMatrixKeyword(e.target.value)}
|
||
onSearch={(v) => {
|
||
setMatrixPage(1);
|
||
loadMatrix(v, matrixCategory, 1, matrixPageSize);
|
||
}}
|
||
/>
|
||
<Text type="secondary">等级列按上浮比例自动换算,仅成本价可编辑</Text>
|
||
</Space>
|
||
<Table<IPriceMatrixRow>
|
||
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 + 290 }}
|
||
/>
|
||
</Drawer>
|
||
</>
|
||
);
|
||
};
|
||
|
||
export default ProductGoodsPage;
|