客户等级设置
This commit is contained in:
@@ -10,8 +10,8 @@ export interface PriceMatrixParams {
|
||||
}
|
||||
|
||||
/**
|
||||
* A2 价格矩阵:行=商品,列=启用等级,值=实际销售价(缺失 null);
|
||||
* 行内含 cost_price 与 price_type_{level_id} / percent_{level_id}
|
||||
* A2 价格矩阵:行=商品,列=启用等级,值=按等级上浮比例换算的售价(成本价未设置 null);
|
||||
* 仅成本价可编辑
|
||||
*/
|
||||
export async function getPriceMatrix(params?: PriceMatrixParams) {
|
||||
return createAxios<IPriceMatrix>({
|
||||
@@ -22,7 +22,7 @@ export async function getPriceMatrix(params?: PriceMatrixParams) {
|
||||
}
|
||||
|
||||
/**
|
||||
* A2 批量调价:支持成本价 / 固定价 / 成本百分比三类更新行(提交后给受影响门店生成价格变更通知)
|
||||
* A2 批量调价:批量调整成本价(等级售价随上浮比例联动;提交后给受影响门店生成价格变更通知)
|
||||
*/
|
||||
export async function batchPrice(updates: IBatchPriceUpdate[]) {
|
||||
return createAxios({
|
||||
|
||||
@@ -5,6 +5,8 @@ export default interface ICustomerLevel {
|
||||
id?: number;
|
||||
/** 等级名称 */
|
||||
name?: string;
|
||||
/** 价格上浮比例(%,售价 = 成本价 × (100 + percent) / 100) */
|
||||
percent?: string | number;
|
||||
sort?: number;
|
||||
status?: number;
|
||||
icon_id?: number;
|
||||
|
||||
+12
-27
@@ -2,19 +2,14 @@ import type IProductCategory from '@/domain/iProductCategory.ts';
|
||||
import type ISupplier from '@/domain/iSupplier.ts';
|
||||
import type {ISysFileInfo} from "@/domain/iSysFile.ts";
|
||||
|
||||
/** 商品等级价格行(null = 该等级未设定价格,编辑时移除该行) */
|
||||
/** 商品等级展示价(后端按等级上浮比例换算:售价 = 成本价 × (100 + percent) / 100) */
|
||||
export interface IProductPrice {
|
||||
id?: number;
|
||||
product_id?: number;
|
||||
/** 客户等级ID */
|
||||
level_id?: number;
|
||||
/** 单价:固定价=实际单价;成本百分比=等价固定价 */
|
||||
/** 该等级的价格上浮比例(%) */
|
||||
percent?: string | number;
|
||||
/** 换算后的售价(两位小数字符串) */
|
||||
price?: string | number | null;
|
||||
/** 计价类型:0 固定价(默认) 1 成本百分比 */
|
||||
price_type?: number;
|
||||
/** 成本上浮百分点(price_type=1 时生效,如 30 = 上浮 30%) */
|
||||
percent?: string | number | null;
|
||||
/** 实际销售价(模型换算:百分比 = 成本价 × (100 + percent) / 100) */
|
||||
actual_price?: string | number;
|
||||
level?: { id: number; name: string };
|
||||
}
|
||||
|
||||
@@ -45,7 +40,7 @@ export default interface IProduct {
|
||||
stock?: number;
|
||||
/** 状态 */
|
||||
status?: number;
|
||||
/** 成本价(元,成本百分比计价的基数) */
|
||||
/** 成本价(元,等级上浮计价的基数) */
|
||||
cost_price?: string | number;
|
||||
/** 描述 */
|
||||
remark?: string;
|
||||
@@ -53,7 +48,7 @@ export default interface IProduct {
|
||||
category?: IProductCategory;
|
||||
/** 供应商关联数据 */
|
||||
supplier?: ISupplier;
|
||||
/** 多等级价格 */
|
||||
/** 各启用等级的换算展示价 */
|
||||
prices?: IProductPrice[];
|
||||
/** 创建时间 */
|
||||
created_at?: string;
|
||||
@@ -65,7 +60,7 @@ export const PRODUCT_STATUS_MAP: Record<number, { text: string; color: string }>
|
||||
};
|
||||
|
||||
/**
|
||||
* 价格矩阵行:固定列 + price_{level_id}(实际价)/ price_type_{level_id} / percent_{level_id} 动态列
|
||||
* 价格矩阵行:固定列 + price_{level_id}(按等级上浮比例换算的售价)动态列
|
||||
*/
|
||||
export type IPriceMatrixRow = {
|
||||
id: number;
|
||||
@@ -77,27 +72,17 @@ export type IPriceMatrixRow = {
|
||||
} & Record<string, string | number | null | undefined>;
|
||||
|
||||
export interface IPriceMatrix {
|
||||
levels: { id: number; name: string }[];
|
||||
levels: { id: number; name: string; percent: string | number }[];
|
||||
rows: IPriceMatrixRow[];
|
||||
total: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量调价更新行(三类,可混合同一行):
|
||||
* - 成本行 { product_id, cost_price }
|
||||
* - 固定价行 { product_id, level_id, price_type: 0, price }
|
||||
* - 百分比行 { product_id, level_id, price_type: 1, percent }(price 可选,等价固定价)
|
||||
* 批量调价更新行:{ product_id, cost_price }
|
||||
* (等级售价 = 成本价 × (100 + 等级上浮比例) / 100,调价即调成本价)
|
||||
*/
|
||||
export interface IBatchPriceUpdate {
|
||||
product_id: number;
|
||||
/** 成本价(成本行) */
|
||||
/** 成本价 */
|
||||
cost_price?: number;
|
||||
/** 客户等级ID(等级价格行) */
|
||||
level_id?: number;
|
||||
/** 计价类型:0 固定价(默认) 1 成本百分比 */
|
||||
price_type?: number;
|
||||
/** 固定价 / 百分比行的等价固定价 */
|
||||
price?: number;
|
||||
/** 成本上浮百分点(price_type=1 时必填) */
|
||||
percent?: number;
|
||||
}
|
||||
|
||||
@@ -28,6 +28,17 @@ const CustomerLevelPage: React.FC = () => {
|
||||
align: 'center',
|
||||
rules: [{ required: true, message: '请输入等级名称' }],
|
||||
},
|
||||
{
|
||||
title: '价格上浮比例',
|
||||
dataIndex: 'percent',
|
||||
valueType: 'digit',
|
||||
hideInSearch: true,
|
||||
initialValue: 0,
|
||||
tooltip: '该等级售价 = 成本价 × (100 + 上浮比例) / 100',
|
||||
fieldProps: { min: 0, max: 999.99, precision: 2, suffix: '%', placeholder: '如 30 表示成本价上浮 30%' },
|
||||
align: 'center',
|
||||
render: (_, record) => <Tag color="blue">{Number(record.percent ?? 0)}%</Tag>,
|
||||
},
|
||||
{
|
||||
title: '排序',
|
||||
dataIndex: 'sort',
|
||||
@@ -111,7 +122,7 @@ const CustomerLevelPage: React.FC = () => {
|
||||
<>
|
||||
<div className="mb-5">
|
||||
<Title level={3}>客户等级</Title>
|
||||
<Text type="secondary">同一商品按客户等级显示不同单价,门店绑定等级后小程序端按对应价格展示。</Text>
|
||||
<Text type="secondary">按等级配置价格上浮比例:售价 = 成本价 × (100 + 上浮比例) / 100,门店绑定等级后小程序端按对应价格展示。</Text>
|
||||
</div>
|
||||
<XinTable<ICustomerLevel> {...tableProps} />
|
||||
</>
|
||||
|
||||
@@ -454,7 +454,7 @@ const Index: React.FC = () => {
|
||||
title: t("dashboard.analysis.quantity"),
|
||||
key: "quantity",
|
||||
width: 100,
|
||||
render: (_, record) => `${record.quantity} ${record.unit}`,
|
||||
render: (_, record) => `${record.quantity}`,
|
||||
},
|
||||
{
|
||||
title: t("dashboard.analysis.amount"),
|
||||
|
||||
+88
-280
@@ -2,11 +2,10 @@ import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import {
|
||||
Button, Card,
|
||||
Drawer,
|
||||
Form, Image,
|
||||
Image,
|
||||
Input,
|
||||
InputNumber,
|
||||
message,
|
||||
Select,
|
||||
Space,
|
||||
Table,
|
||||
Tag, Tree,
|
||||
@@ -14,15 +13,14 @@ import {
|
||||
Typography,
|
||||
} from 'antd';
|
||||
import { TableOutlined } from '@ant-design/icons';
|
||||
import type { FormInstance, TableProps } from 'antd';
|
||||
import type { 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 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 { 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';
|
||||
@@ -30,14 +28,6 @@ import { batchPrice, getPriceMatrix } from '@/api/product/goods.ts';
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
|
||||
/** 计价类型:0 固定价 / 1 成本百分比(与后端 ProductPriceModel::PRICE_TYPE_* 一致) */
|
||||
const PRICE_TYPE_FIXED = 0;
|
||||
const PRICE_TYPE_PERCENT = 1;
|
||||
const PRICE_TYPE_OPTIONS = [
|
||||
{ value: PRICE_TYPE_FIXED, label: '固定价' },
|
||||
{ value: PRICE_TYPE_PERCENT, label: '成本百分比' },
|
||||
];
|
||||
|
||||
/** 四舍五入保留两位 */
|
||||
const round2 = (v: number) => Math.round(v * 100) / 100;
|
||||
|
||||
@@ -57,122 +47,13 @@ const markParentUnselectable = (nodes: IProductCategoryTree[]): IProductCategory
|
||||
children: node.children?.length ? markParentUnselectable(node.children) : node.children,
|
||||
}));
|
||||
|
||||
/**
|
||||
* 等级价格表单:每个等级 = 计价类型(固定价/成本百分比)+ 对应输入框;
|
||||
* 切换计价类型时按成本价自动换算(固定→百分比:percent=(price/cost-1)*100;百分比→固定:price=cost*(1+percent/100))
|
||||
*/
|
||||
const LevelPriceFields: React.FC<{
|
||||
form: FormInstance;
|
||||
levels: ICustomerLevel[];
|
||||
}> = ({ form, levels }) => {
|
||||
const prices = Form.useWatch<IProductPrice[]>('prices', form) ?? [];
|
||||
const costPrice = Number(Form.useWatch<string | number | undefined>('cost_price', form) ?? 0);
|
||||
|
||||
if (levels.length === 0) {
|
||||
return (
|
||||
<Text type="secondary">
|
||||
暂无可配置的客户等级,请先到「客户等级」中新增
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
const updateRow = (levelId: number, patch: Partial<IProductPrice>) => {
|
||||
const next = prices.filter((p) => p.level_id !== levelId);
|
||||
next.push({ ...patch, level_id: levelId });
|
||||
form.setFieldValue('prices', next);
|
||||
};
|
||||
|
||||
const removeRow = (levelId: number) => {
|
||||
form.setFieldValue('prices', prices.filter((p) => p.level_id !== levelId));
|
||||
};
|
||||
|
||||
/** 切换计价类型:按成本价自动换算(成本未设置时百分比计价不可用) */
|
||||
const onTypeChange = (levelId: number, row: IProductPrice | undefined, type: number) => {
|
||||
if (type === PRICE_TYPE_PERCENT) {
|
||||
if (!(costPrice > 0)) {
|
||||
message.warning('请先在商品信息中设置成本价,才能按成本百分比计价');
|
||||
return; // Select 为受控组件,未更新表单值即回弹
|
||||
}
|
||||
const price = Number(row?.price ?? 0);
|
||||
updateRow(levelId, {
|
||||
price_type: type,
|
||||
percent: price > 0 ? round2((price / costPrice - 1) * 100) : null,
|
||||
});
|
||||
} else {
|
||||
const percent = Number(row?.percent ?? 0);
|
||||
updateRow(levelId, {
|
||||
price_type: type,
|
||||
price: costPrice > 0 && percent > 0 ? round2(costPrice * (1 + percent / 100)) : (row?.price ?? null),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Space wrap>
|
||||
{levels.map((level) => {
|
||||
if (level.id == null) return null;
|
||||
const row = prices.find((p) => p.level_id === level.id);
|
||||
const type = row?.price_type ?? PRICE_TYPE_FIXED;
|
||||
return (
|
||||
<Space key={level.id} wrap={false}>
|
||||
<Text style={{ width: 60, display: 'inline-block', textAlign: 'right' }}>{level.name}</Text>
|
||||
<Select
|
||||
size="middle"
|
||||
style={{ width: 110 }}
|
||||
value={type}
|
||||
options={PRICE_TYPE_OPTIONS}
|
||||
onChange={(v) => onTypeChange(level.id as number, row, v as number)}
|
||||
/>
|
||||
{type === PRICE_TYPE_PERCENT ? (
|
||||
<InputNumber
|
||||
min={0}
|
||||
precision={0}
|
||||
suffix={'%'}
|
||||
placeholder="上浮百分点"
|
||||
value={(row?.percent as number | null) ?? null}
|
||||
onChange={(v) => {
|
||||
if (v === null) {
|
||||
removeRow(level.id as number);
|
||||
return;
|
||||
}
|
||||
// 同步维护等价固定价 price,保证提交给后端的 price 与 percent 一致
|
||||
updateRow(level.id as number, {
|
||||
price_type: PRICE_TYPE_PERCENT,
|
||||
percent: v,
|
||||
price: costPrice > 0 ? round2(costPrice * (1 + v / 100)) : row?.price,
|
||||
});
|
||||
}}
|
||||
style={{ width: 160 }}
|
||||
/>
|
||||
) : (
|
||||
<InputNumber
|
||||
min={0}
|
||||
precision={2}
|
||||
suffix={'¥'}
|
||||
placeholder="未设定"
|
||||
value={(row?.price as number | null) ?? null}
|
||||
onChange={(v) => {
|
||||
if (v === null) {
|
||||
removeRow(level.id as number);
|
||||
return;
|
||||
}
|
||||
updateRow(level.id as number, { price_type: PRICE_TYPE_FIXED, price: v });
|
||||
}}
|
||||
style={{ width: 160 }}
|
||||
/>
|
||||
)}
|
||||
</Space>
|
||||
);
|
||||
})}
|
||||
</Space>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* 商品档案管理(A1 商品列表 / A2 价格矩阵与批量调价)
|
||||
*
|
||||
* 价格体系:售价 = 成本价 × (100 + 客户等级上浮比例) / 100;
|
||||
* 等级上浮比例在「客户等级」中维护,本页价格矩阵仅批量调整成本价。
|
||||
*/
|
||||
const ProductGoodsPage: React.FC = () => {
|
||||
const [levels, setLevels] = useState<ICustomerLevel[]>([]);
|
||||
const [suppliers, setSuppliers] = useState<ISupplier[]>([]);
|
||||
const [categoryTree, setCategoryTree] = useState<IProductCategoryTree[]>([]);
|
||||
// 商品表单专用分类树:父分类禁选,仅末级可选(侧栏筛选/价格矩阵仍用原始树)
|
||||
@@ -196,15 +77,10 @@ const ProductGoodsPage: React.FC = () => {
|
||||
const [matrixLevels, setMatrixLevels] = useState<ICustomerLevel[]>([]);
|
||||
const [matrixKeyword, setMatrixKeyword] = useState('');
|
||||
const [matrixCategory, setMatrixCategory] = useState<number | undefined>(undefined);
|
||||
/** 跨页未保存的等级调价:`${productId}:${levelId}` → { price, price_type }(用 ref 避免异步闭包读到旧值) */
|
||||
const matrixDirtyRef = useRef<Record<string, { price: number | null; price_type: number }>>({});
|
||||
/** 跨页未保存的成本价:productId → cost(null 视为未修改,不提交) */
|
||||
const costDirtyRef = useRef<Record<number, number | null>>({});
|
||||
/** 服务端原始成本价:productId → cost(loadMatrix 填充;跨页百分比行保存时反算 percent 用) */
|
||||
const serverCostRef = useRef<Record<number, number>>({});
|
||||
|
||||
useEffect(() => {
|
||||
getLevelOptions().then((res) => setLevels(res.data.data ?? []));
|
||||
getSupplierOptions().then((res) => setSuppliers(res.data.data ?? []));
|
||||
getCategoryTree().then((res) => setCategoryTree(res.data.data ?? []));
|
||||
}, []);
|
||||
@@ -224,29 +100,24 @@ const ProductGoodsPage: React.FC = () => {
|
||||
pageSize,
|
||||
});
|
||||
const rows = res.data.data?.rows ?? [];
|
||||
// 服务端原始成本价快照(供跨页百分比行保存时反算 percent)
|
||||
rows.forEach((row) => {
|
||||
serverCostRef.current[row.id] = Number(row.cost_price ?? 0);
|
||||
});
|
||||
// 叠加跨页未保存的修改,保证翻页后输入值不回退
|
||||
const levels = res.data.data?.levels ?? [];
|
||||
// 叠加跨页未保存的成本价修改,并重算各等级展示价,保证翻页后输入值不回退
|
||||
const merged = rows.map((row) => {
|
||||
const next = { ...row };
|
||||
// 成本价修改
|
||||
if (costDirtyRef.current[row.id] !== undefined) {
|
||||
next.cost_price = costDirtyRef.current[row.id];
|
||||
}
|
||||
// 等级格修改(快照含 price_type)
|
||||
Object.entries(matrixDirtyRef.current).forEach(([key, snap]) => {
|
||||
const [pid, lid] = key.split(':');
|
||||
if (String(row.id) === pid) {
|
||||
next[`price_${lid}`] = snap.price;
|
||||
}
|
||||
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(res.data.data?.levels ?? []);
|
||||
setMatrixLevels(levels);
|
||||
} finally {
|
||||
setMatrixLoading(false);
|
||||
}
|
||||
@@ -259,23 +130,7 @@ const ProductGoodsPage: React.FC = () => {
|
||||
loadMatrix('', undefined, 1, 20);
|
||||
};
|
||||
|
||||
const onMatrixPriceChange = (
|
||||
productId: number,
|
||||
levelId: number,
|
||||
value: number | null
|
||||
) => {
|
||||
setMatrixRows((prev) =>
|
||||
prev.map((row) => {
|
||||
if (row.id !== productId) return row;
|
||||
// 快照该格的计价类型(读当前行数据,避免闭包旧 state)
|
||||
const type = Number(row[`price_type_${levelId}`] ?? PRICE_TYPE_FIXED);
|
||||
matrixDirtyRef.current[`${productId}:${levelId}`] = { price: value, price_type: type };
|
||||
return { ...row, [`price_${levelId}`]: value };
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
/** 修改成本价:联动重算「未被手工修改过」的百分比格显示值(percent 取服务端快照) */
|
||||
/** 修改成本价:联动重算各等级展示价(上浮比例取 levels 配置) */
|
||||
const onMatrixCostChange = (productId: number, value: number | null) => {
|
||||
costDirtyRef.current[productId] = value;
|
||||
setMatrixRows((prev) =>
|
||||
@@ -283,54 +138,24 @@ const ProductGoodsPage: React.FC = () => {
|
||||
if (row.id !== productId) return row;
|
||||
const next: IPriceMatrixRow = { ...row, cost_price: value };
|
||||
const cost = Number(value ?? 0);
|
||||
Object.keys(next).forEach((key) => {
|
||||
if (!key.startsWith('percent_')) return;
|
||||
const lid = key.replace('percent_', '');
|
||||
if (matrixDirtyRef.current[`${productId}:${lid}`]) return; // 已手工改过,不覆盖
|
||||
if (Number(next[`price_type_${lid}`]) !== PRICE_TYPE_PERCENT) return;
|
||||
const percent = Number(next[key] ?? 0);
|
||||
next[`price_${lid}`] = cost > 0 ? round2(cost * (1 + percent / 100)) : null;
|
||||
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 视为清除,不提交) */
|
||||
/** 提交所有跨页未保存的成本价调整(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 });
|
||||
});
|
||||
// 等级价格行(按快照计价类型组装:百分比格按「当前成本价」反算上浮百分点)
|
||||
for (const [key, snap] of Object.entries(matrixDirtyRef.current)) {
|
||||
if (snap.price === null || snap.price === undefined) continue;
|
||||
const [pid, lid] = key.split(':');
|
||||
const productId = Number(pid);
|
||||
if (snap.price_type === PRICE_TYPE_PERCENT) {
|
||||
const cost = costDirtyRef.current[productId] ?? serverCostRef.current[productId] ?? 0;
|
||||
if (!(cost > 0)) {
|
||||
message.error(`商品 #${productId} 未设置成本价,无法保存百分比价格`);
|
||||
return;
|
||||
}
|
||||
updates.push({
|
||||
product_id: productId,
|
||||
level_id: Number(lid),
|
||||
price_type: PRICE_TYPE_PERCENT,
|
||||
percent: round2((snap.price / cost - 1) * 100),
|
||||
price: snap.price, // 等价固定价
|
||||
});
|
||||
} else {
|
||||
updates.push({
|
||||
product_id: productId,
|
||||
level_id: Number(lid),
|
||||
price_type: PRICE_TYPE_FIXED,
|
||||
price: snap.price,
|
||||
});
|
||||
}
|
||||
}
|
||||
if (updates.length === 0) {
|
||||
message.info('没有需要保存的价格调整');
|
||||
return;
|
||||
@@ -338,8 +163,7 @@ const ProductGoodsPage: React.FC = () => {
|
||||
setSaveLoading(true);
|
||||
try {
|
||||
await batchPrice(updates);
|
||||
message.success(`已更新 ${updates.length} 条价格,受影响门店将收到通知`);
|
||||
matrixDirtyRef.current = {};
|
||||
message.success(`已更新 ${updates.length} 件商品成本价,各等级售价已按上浮比例联动,受影响门店将收到通知`);
|
||||
costDirtyRef.current = {};
|
||||
await loadMatrix();
|
||||
} finally {
|
||||
@@ -385,22 +209,11 @@ const ProductGoodsPage: React.FC = () => {
|
||||
key: `price_${level.id}`,
|
||||
width: 150,
|
||||
render: (_: unknown, row: IPriceMatrixRow) => {
|
||||
const isPercent = Number(row[`price_type_${level.id}`] ?? PRICE_TYPE_FIXED) === PRICE_TYPE_PERCENT;
|
||||
const percent = Number(row[`percent_${level.id}`] ?? 0);
|
||||
const price = row[`price_${level.id}`];
|
||||
return (
|
||||
<div>
|
||||
<InputNumber
|
||||
size="small"
|
||||
min={0}
|
||||
precision={2}
|
||||
prefix="¥"
|
||||
value={row[`price_${level.id}`] as number | null}
|
||||
onChange={(v) => onMatrixPriceChange(row.id, level.id!, v)}
|
||||
className="w-32"
|
||||
/>
|
||||
{isPercent && (
|
||||
<div className="text-xs text-gray-400">成本上浮 {percent}%</div>
|
||||
)}
|
||||
<div>{price != null ? `¥${price}` : <Text type="secondary">未设成本价</Text>}</div>
|
||||
<div className="text-xs text-gray-400">成本上浮 {Number(level.percent ?? 0)}%</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
@@ -473,6 +286,19 @@ const ProductGoodsPage: React.FC = () => {
|
||||
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',
|
||||
@@ -488,6 +314,14 @@ const ProductGoodsPage: React.FC = () => {
|
||||
initialValue: '斤',
|
||||
align: "center",
|
||||
},
|
||||
{
|
||||
title: '排序',
|
||||
dataIndex: 'sort',
|
||||
valueType: 'digit',
|
||||
hideInSearch: true,
|
||||
fieldProps: { min: 0 },
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '分类',
|
||||
dataIndex: 'category_id',
|
||||
@@ -522,65 +356,6 @@ const ProductGoodsPage: React.FC = () => {
|
||||
},
|
||||
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: 'cost_price',
|
||||
valueType: 'digit',
|
||||
hideInSearch: true,
|
||||
align: 'center',
|
||||
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: 'prices',
|
||||
hideInForm: true,
|
||||
hideInSearch: true,
|
||||
width: 420,
|
||||
align: 'center',
|
||||
render: (_, record) => (
|
||||
<Space wrap>
|
||||
{record.prices?.length
|
||||
? record.prices.map((p) => (
|
||||
<Tag
|
||||
key={p.id}
|
||||
color="geekblue"
|
||||
title={Number(p.price_type) === PRICE_TYPE_PERCENT ? `按成本价上浮 ${p.percent}%` : undefined}
|
||||
>
|
||||
{p.level?.name ?? `等级${p.level_id}`}
|
||||
<span style={{color: 'red', marginLeft: 5 }}>
|
||||
¥{p.actual_price ?? p.price ?? '未设定'}
|
||||
</span>
|
||||
</Tag>
|
||||
))
|
||||
: '-'}
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '排序',
|
||||
dataIndex: 'sort',
|
||||
valueType: 'digit',
|
||||
hideInSearch: true,
|
||||
fieldProps: { min: 0 },
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
@@ -599,12 +374,44 @@ const ProductGoodsPage: React.FC = () => {
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '等级价格设置',
|
||||
dataIndex: 'prices',
|
||||
hideInTable: true,
|
||||
title: '图文详情',
|
||||
dataIndex: 'content',
|
||||
valueType: 'richText',
|
||||
hideInSearch: true,
|
||||
hideInTable: true,
|
||||
colProps: { span: 24 },
|
||||
fieldRender: (form) => <LevelPriceFields form={form} levels={levels} />,
|
||||
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: '创建时间',
|
||||
@@ -652,7 +459,7 @@ const ProductGoodsPage: React.FC = () => {
|
||||
<div className="mb-5">
|
||||
<Title level={3}>商品列表</Title>
|
||||
<Text type="secondary">
|
||||
商品档案与多等级价格体系;「价格矩阵」支持按等级批量调价,调价后自动通知受影响门店。
|
||||
商品售价按客户等级上浮比例自动换算(成本价 × (100 + 上浮比例) / 100);「价格矩阵」支持批量调整成本价,调价后自动通知受影响门店。
|
||||
</Text>
|
||||
</div>
|
||||
<div className="flex items-start gap-4">
|
||||
@@ -717,6 +524,7 @@ const ProductGoodsPage: React.FC = () => {
|
||||
loadMatrix(v, matrixCategory, 1, matrixPageSize);
|
||||
}}
|
||||
/>
|
||||
<Text type="secondary">等级列按上浮比例自动换算,仅成本价可编辑</Text>
|
||||
</Space>
|
||||
<Table<IPriceMatrixRow>
|
||||
rowKey="id"
|
||||
|
||||
Reference in New Issue
Block a user