cangku/frontend/src/pages/PricingCalculator/DropshipCalculator.jsx

910 lines
34 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useCallback, useEffect, useMemo, useState } from 'react';
import {
Alert,
Button,
Empty,
Image,
Input,
InputNumber,
Modal,
Popover,
Select,
Table,
Tag,
Tooltip,
message,
} from 'antd';
import {
CalculatorOutlined,
ControlOutlined,
DownloadOutlined,
ExportOutlined,
HistoryOutlined,
QuestionCircleOutlined,
ReloadOutlined,
SaveOutlined,
SearchOutlined,
SettingOutlined,
ShopOutlined,
} from '@ant-design/icons';
import { SycmDashboardMetric } from '../../components/Common';
import { getPlanningAlibaba1688ProductDetail } from '../../api/planningUpload';
import {
exportPricingTable,
fetchPricingHistoryDetail,
fetchPricingPrefs,
savePricingHistory,
savePricingPrefs,
} from '../../api/pricingCalculator';
import { computeRow, formatMoney, formatPercent } from './calc';
import { DiscountInput, MoneyInput, PercentInput } from './inputs';
import DefaultsConfigModal from './DefaultsConfigModal';
import FieldConfigDrawer from './FieldConfigDrawer';
import HistoryDrawer from './HistoryDrawer';
import {
applyDropshipDefaultsToRows,
createDropshipRows,
DROPSHIP_FIXED_RATES,
DROPSHIP_SYSTEM_DEFAULTS,
getDropshipDimensions,
getDropshipSpecValue,
mergeDropshipSelectionPricingRows,
parseAlibaba1688ProductId,
} from './dropship';
import {
createDefaultDropshipFieldConfig,
DROPSHIP_FIELD_LABELS,
loadDropshipFieldConfig,
saveDropshipFieldConfig,
} from './dropshipFieldConfig';
import { PERCENT_FIELD_KEYS } from './fieldConfig';
import './style.css';
const DEFAULT_FILTERS = { available: undefined, keyword: '', specs: {} };
const LEGEND_ITEMS = [
{ className: 'is-input', label: '可填写(到手价 / 安装费 / 营业利润率)' },
{ className: 'is-auto', label: '1688 自动获取 / 固定比例' },
{ className: 'is-formula', label: '公式' },
{ className: 'is-pdd', label: '拼多多活动(优惠券 / 折扣可填)' },
{ className: 'is-danger', label: '营业利润额为负(亏本)' },
];
const ProductThumb = ({ url, size = 36 }) => {
const [failedUrl, setFailedUrl] = useState('');
const normalizedUrl = String(url || '').trim().replace(/^\/\//, 'https://');
if (!normalizedUrl || failedUrl === normalizedUrl) {
return (
<span className="pricing-calc-thumb is-empty" style={{ width: size, height: size }}>
无图
</span>
);
}
return (
<Image
className="pricing-calc-thumb"
width={size}
height={size}
style={{ objectFit: 'cover' }}
src={normalizedUrl}
alt="商品图片"
referrerPolicy="no-referrer"
preview={{
src: normalizedUrl,
// 1688 图片有防盗链,预览大图也必须以 no-referrer 请求
imageRender: () => (
<img
referrerPolicy="no-referrer"
src={normalizedUrl}
alt="商品图片预览"
style={{ maxWidth: '100%', maxHeight: '100%', objectFit: 'contain' }}
/>
),
}}
onError={() => setFailedUrl(normalizedUrl)}
/>
);
};
const AutoHeaderTitle = ({ children, tip }) => (
<span className="pricing-calc-th-title">
{children}
{tip ? (
<Tooltip title={tip}>
<QuestionCircleOutlined />
</Tooltip>
) : null}
</span>
);
const numericSorter = (field) => (leftRow, rightRow) => {
const left = Number.isFinite(leftRow[field]) ? leftRow[field] : Number.NEGATIVE_INFINITY;
const right = Number.isFinite(rightRow[field]) ? rightRow[field] : Number.NEGATIVE_INFINITY;
return left - right;
};
const getFriendlyError = (error, fallback) => (
error?.response?.data?.error || error?.message || fallback
);
const normalizeRestoredRows = (rows, defaults) => (Array.isArray(rows) ? rows : []).map((row, index) => ({
...row,
id: row.id || `1688:restored:${row.skuCode || index}:${index}`,
specs: Array.isArray(row.specs) ? row.specs : [],
specsText: row.specsText || (Array.isArray(row.specs)
? row.specs.map((spec) => `${spec.name}:${spec.value}`).join(' / ')
: ''),
price: row.price ?? row.groupPrice ?? null,
coupon: row.coupon ?? defaults.coupon,
activityDiscount: row.activityDiscount ?? defaults.activityDiscount,
installFee: row.installFee ?? defaults.installFee,
targetMargin: row.targetMargin ?? defaults.targetMargin,
...DROPSHIP_FIXED_RATES,
}));
const DropshipCalculator = ({
embedded = false,
productId: embeddedProductId = '',
selectionPlanId = '',
selectionItemId = '',
selectionRows = [],
onSelectionPricingSaved,
} = {}) => {
const [input, setInput] = useState(embeddedProductId);
const [productDetail, setProductDetail] = useState(null);
const [rows, setRows] = useState([]);
const [searching, setSearching] = useState(false);
const [searchError, setSearchError] = useState('');
const [filters, setFilters] = useState(DEFAULT_FILTERS);
const [fieldConfig, setFieldConfig] = useState(loadDropshipFieldConfig);
const [fieldConfigOpen, setFieldConfigOpen] = useState(false);
const [defaults, setDefaults] = useState(null);
const [defaultsLoaded, setDefaultsLoaded] = useState(false);
const [defaultsOpen, setDefaultsOpen] = useState(false);
const [batchDiscount, setBatchDiscount] = useState(null);
const [defaultsSaving, setDefaultsSaving] = useState(false);
const [exporting, setExporting] = useState(false);
const [historyOpen, setHistoryOpen] = useState(false);
const [historyRefreshKey, setHistoryRefreshKey] = useState(0);
const [restoring, setRestoring] = useState(false);
const [saveModalOpen, setSaveModalOpen] = useState(false);
const [saveNote, setSaveNote] = useState('');
const [saving, setSaving] = useState(false);
const autoLoadedProductRef = React.useRef('');
const isSelectionPricingMode = Boolean(selectionPlanId && selectionItemId && embeddedProductId);
useEffect(() => {
let cancelled = false;
fetchPricingPrefs('alibaba1688')
.then((data) => {
if (!cancelled) setDefaults(data?.prefs || null);
})
.catch(() => {})
.finally(() => {
if (!cancelled) setDefaultsLoaded(true);
});
return () => { cancelled = true; };
}, []);
const effectiveDefaults = useMemo(
() => ({ ...DROPSHIP_SYSTEM_DEFAULTS, ...(defaults || {}) }),
[defaults]
);
const computedRows = useMemo(
() => rows.map((row) => ({ ...row, ...computeRow(row) })),
[rows]
);
const dimensions = useMemo(
() => getDropshipDimensions(productDetail, rows),
[productDetail, rows]
);
const dimensionOptions = useMemo(() => Object.fromEntries(dimensions.map((dimension) => [
dimension,
[...new Set(rows.map((row) => getDropshipSpecValue(row, dimension)).filter(Boolean))]
.map((value) => ({ value, label: value })),
])), [dimensions, rows]);
const filteredRows = useMemo(() => {
const keyword = filters.keyword.trim().toLowerCase();
return computedRows.filter((row) => {
if (filters.available !== undefined && row.available !== filters.available) return false;
if (Object.entries(filters.specs).some(([name, value]) => (
value && getDropshipSpecValue(row, name) !== value
))) return false;
if (keyword && ![row.skuCode, row.name, row.specsText]
.some((value) => String(value || '').toLowerCase().includes(keyword))) return false;
return true;
});
}, [computedRows, filters]);
const summaryMetrics = useMemo(() => {
const margins = filteredRows.map((row) => row.marginRate).filter(Number.isFinite);
const profits = filteredRows.map((row) => row.profitAmount).filter(Number.isFinite);
const average = (values) => (values.length
? values.reduce((sum, value) => sum + value, 0) / values.length
: null);
return [
{ key: 'count', label: '测算 SKU 数', value: `${filteredRows.length}` },
{ key: 'margin', label: '平均毛利率', value: margins.length ? formatPercent(average(margins), 1) : '--' },
{ key: 'profit', label: '平均营业利润额', value: profits.length ? `¥${formatMoney(average(profits))}` : '--' },
{
key: 'negativeProfit',
label: '营业利润额为负(亏本)',
value: `${filteredRows.filter((row) => Number.isFinite(row.profitAmount) && row.profitAmount < 0).length} 行`,
},
];
}, [filteredRows]);
const runSearch = useCallback(async (rawInput) => {
const parsed = parseAlibaba1688ProductId(rawInput);
if (!parsed.productId) {
setSearchError(parsed.error);
return;
}
setSearching(true);
setSearchError('');
try {
const response = await getPlanningAlibaba1688ProductDetail(parsed.productId);
if (response?.success === false) throw new Error(response.error || '1688 商品查询失败');
const detail = response?.data || response;
const nextRows = mergeDropshipSelectionPricingRows(
createDropshipRows(detail, effectiveDefaults, {
useSourceSkuCode: isSelectionPricingMode,
}),
selectionRows
);
if (!nextRows.length) {
throw new Error(`商品「${parsed.productId}」没有可用于测算的 SKU 明细`);
}
setInput(parsed.productId);
setProductDetail(detail);
setRows(nextRows);
setFilters(DEFAULT_FILTERS);
} catch (error) {
setProductDetail(null);
setRows([]);
setSearchError(getFriendlyError(error, '1688 商品查询失败,请稍后重试'));
} finally {
setSearching(false);
}
}, [effectiveDefaults, isSelectionPricingMode, selectionRows]);
useEffect(() => {
const productId = String(embeddedProductId || '').trim();
if (!defaultsLoaded || !productId || autoLoadedProductRef.current === productId) return;
autoLoadedProductRef.current = productId;
setInput(productId);
runSearch(productId);
}, [defaultsLoaded, embeddedProductId, runSearch]);
const updateRow = (id, patch) => {
setRows((current) => current.map((row) => (row.id === id ? { ...row, ...patch } : row)));
};
// 一键批量设置折扣:作用于当前筛选后的全部行
const applyBatchDiscount = () => {
if (!batchDiscount || batchDiscount <= 0) return;
const ids = new Set(filteredRows.map((row) => row.id));
setRows((current) => current.map((row) => (ids.has(row.id) ? { ...row, activityDiscount: batchDiscount } : row)));
message.success(`已把 ${ids.size} 行的活动折扣统一设置为 ${batchDiscount}(即 ${batchDiscount * 10} 折)`);
};
const resetToHero = () => {
setProductDetail(null);
setRows([]);
setSearchError('');
setFilters(DEFAULT_FILTERS);
};
const applyFilter = (patch) => {
setFilters((current) => ({ ...current, ...patch }));
};
const applySpecFilter = (dimension, value) => {
setFilters((current) => ({
...current,
specs: { ...current.specs, [dimension]: value },
}));
};
const resetFilters = () => {
setFilters(DEFAULT_FILTERS);
};
const handleSaveDefaults = async (form) => {
setDefaultsSaving(true);
try {
const data = await savePricingPrefs(form, 'alibaba1688');
const nextDefaults = { ...DROPSHIP_SYSTEM_DEFAULTS, ...(data?.prefs || form) };
setRows((current) => applyDropshipDefaultsToRows(current, defaults, nextDefaults));
setDefaults(data?.prefs || form);
setDefaultsOpen(false);
message.success('1688 默认值已保存,未手动修改的行已同步更新');
} catch (error) {
message.error(getFriendlyError(error, '保存默认值配置失败'));
} finally {
setDefaultsSaving(false);
}
};
const product = productDetail?.product || productDetail || {};
const productId = String(productDetail?.productId || product.productId || '');
const productTitle = productDetail?.title || product.title || '';
const productMainImage = productDetail?.mainImage || product.mainImage || rows[0]?.imageUrl || '';
const productSourceUrl = productDetail?.sourceUrl || product.sourceUrl || '';
const formulaCell = { className: 'pricing-calc-td--formula' };
const inputCell = { className: 'pricing-calc-td--input' };
const pddCell = { className: 'pricing-calc-td--pdd' };
const pddInputCell = { className: 'pricing-calc-td--pdd-input' };
const autoHeader = { className: 'pricing-calc-th--auto' };
const inputHeader = { className: 'pricing-calc-th--input' };
const formulaHeader = { className: 'pricing-calc-th--formula' };
const pddHeader = { className: 'pricing-calc-th--pdd' };
const columns = [
{
title: '图片',
dataIndex: 'imageUrl',
width: 64,
fixed: 'left',
align: 'center',
render: (value) => <ProductThumb url={value} />,
},
{
title: '规格组合',
dataIndex: 'specsText',
width: 260,
render: (_, record) => (
<div className="pricing-calc-spec-list">
{record.specs.length ? record.specs.map((spec, index) => (
<span key={`${spec.name}-${spec.value}-${index}`} className="pricing-calc-spec-item">
<b>{spec.name}</b>
<span>{spec.value}</span>
</span>
)) : <span className="pricing-calc-text">-</span>}
</div>
),
},
{
title: <AutoHeaderTitle tip="1688 SKU 采购价,自动获取">采购成本</AutoHeaderTitle>,
dataIndex: 'cost',
width: 100,
align: 'right',
sorter: numericSorter('cost'),
onHeaderCell: () => autoHeader,
render: (value) => <span className="pricing-calc-value">{formatMoney(value)}</span>,
},
{
title: <AutoHeaderTitle tip="默认按(采购成本 + 运费)/ 0.55 计算,可填写;利润按优惠后实际到手价计算">到手价</AutoHeaderTitle>,
dataIndex: 'price',
width: 110,
sorter: numericSorter('price'),
onHeaderCell: () => inputHeader,
onCell: () => inputCell,
render: (value, record) => (
<MoneyInput style={{ width: '100%' }} value={value} onChange={(next) => updateRow(record.id, { price: next })} />
),
},
{
title: <AutoHeaderTitle tip="默认 0,可在默认值中配置">安装费</AutoHeaderTitle>,
dataIndex: 'installFee',
width: 96,
onHeaderCell: () => inputHeader,
onCell: () => inputCell,
render: (value, record) => (
<MoneyInput style={{ width: '100%' }} value={value} onChange={(next) => updateRow(record.id, { installFee: next })} />
),
},
{
title: <AutoHeaderTitle tip="1688 SKU 运费,自动获取">运费</AutoHeaderTitle>,
dataIndex: 'shipping',
width: 96,
align: 'right',
sorter: numericSorter('shipping'),
onHeaderCell: () => autoHeader,
render: (value) => <span className="pricing-calc-value">{formatMoney(value)}</span>,
},
{
title: '毛利率',
dataIndex: 'marginRate',
width: 92,
align: 'right',
sorter: numericSorter('marginRate'),
onHeaderCell: () => formulaHeader,
onCell: () => formulaCell,
render: (value) => <span className="pricing-calc-value">{formatPercent(value)}</span>,
},
{
title: <AutoHeaderTitle tip="1688 代发固定使用非黑标店铺,平台佣金 1.5%">平台佣金</AutoHeaderTitle>,
dataIndex: 'commission',
width: 96,
align: 'right',
onHeaderCell: () => autoHeader,
render: (value) => <span className="pricing-calc-value">{formatPercent(value)}</span>,
},
{
title: <AutoHeaderTitle tip="毛利率减去固定费率、目标营业利润率及安装费占比">推广占比</AutoHeaderTitle>,
dataIndex: 'promoRate',
width: 104,
align: 'right',
sorter: numericSorter('promoRate'),
onHeaderCell: () => formulaHeader,
onCell: () => formulaCell,
render: (value) => <span className="pricing-calc-value">{formatPercent(value)}</span>,
},
{
title: <AutoHeaderTitle tip="固定 2.5%">营销广告费</AutoHeaderTitle>,
dataIndex: 'adFee',
width: 100,
align: 'right',
onHeaderCell: () => autoHeader,
render: (value) => <span className="pricing-calc-value">{formatPercent(value)}</span>,
},
{
title: <AutoHeaderTitle tip="固定 0%">种菜占比</AutoHeaderTitle>,
dataIndex: 'seeding',
width: 96,
align: 'right',
onHeaderCell: () => autoHeader,
render: (value) => <span className="pricing-calc-value">{formatPercent(value)}</span>,
},
{
title: <AutoHeaderTitle tip="固定 5%">售后费用</AutoHeaderTitle>,
dataIndex: 'afterSale',
width: 96,
align: 'right',
onHeaderCell: () => autoHeader,
render: (value) => <span className="pricing-calc-value">{formatPercent(value)}</span>,
},
{
title: <AutoHeaderTitle tip="固定 5%">保险费</AutoHeaderTitle>,
dataIndex: 'insurance',
width: 92,
align: 'right',
onHeaderCell: () => autoHeader,
render: (value) => <span className="pricing-calc-value">{formatPercent(value)}</span>,
},
{
title: <AutoHeaderTitle tip="1688 代发不收取品牌使用费,固定 0%">品牌使用费</AutoHeaderTitle>,
dataIndex: 'brandFee',
width: 100,
align: 'right',
onHeaderCell: () => autoHeader,
render: (value) => <span className="pricing-calc-value">{formatPercent(value)}</span>,
},
{
title: <AutoHeaderTitle tip="目标营业利润率默认 12%,可填写">营业利润率</AutoHeaderTitle>,
dataIndex: 'targetMargin',
width: 100,
align: 'right',
onHeaderCell: () => inputHeader,
onCell: () => inputCell,
render: (value, record) => (
<PercentInput style={{ width: '100%' }} value={value} onChange={(next) => updateRow(record.id, { targetMargin: next })} />
),
},
{
title: '营业利润额',
dataIndex: 'profitAmount',
width: 110,
align: 'right',
fixed: 'right',
sorter: numericSorter('profitAmount'),
onHeaderCell: () => formulaHeader,
onCell: (record) => ({
className: Number.isFinite(record.profitAmount) && record.profitAmount < 0
? 'pricing-calc-td--danger'
: 'pricing-calc-td--formula',
}),
render: (value) => <span className="pricing-calc-value">{formatMoney(value)}</span>,
},
{
title: <AutoHeaderTitle tip="优惠券金额默认 20,可填写">优惠券金额</AutoHeaderTitle>,
dataIndex: 'coupon',
width: 104,
align: 'right',
onHeaderCell: () => pddHeader,
onCell: () => pddInputCell,
render: (value, record) => (
<MoneyInput style={{ width: '100%' }} value={value} onChange={(next) => updateRow(record.id, { coupon: next })} />
),
},
{
title: <AutoHeaderTitle tip="活动折扣默认 0.6(即 6 折),可填写">活动折扣</AutoHeaderTitle>,
dataIndex: 'activityDiscount',
width: 96,
align: 'right',
onHeaderCell: () => pddHeader,
onCell: () => pddInputCell,
render: (value, record) => (
<DiscountInput style={{ width: '100%' }} value={value} onChange={(next) => updateRow(record.id, { activityDiscount: next })} />
),
},
{
title: <AutoHeaderTitle tip="公式:(到手价 + 优惠券金额) / 活动折扣">拼单价</AutoHeaderTitle>,
dataIndex: 'pddPrice',
width: 100,
align: 'right',
fixed: 'right',
sorter: numericSorter('pddPrice'),
onHeaderCell: () => pddHeader,
onCell: () => pddCell,
render: (value) => <span className="pricing-calc-value">{formatMoney(value)}</span>,
},
{
title: <AutoHeaderTitle tip="拼单价向上取整">最终拼单价</AutoHeaderTitle>,
dataIndex: 'pddFinalPrice',
width: 110,
align: 'right',
fixed: 'right',
sorter: numericSorter('pddFinalPrice'),
onHeaderCell: () => pddHeader,
onCell: () => pddCell,
render: (value) => <span className="pricing-calc-value">{formatMoney(value)}</span>,
},
];
const columnsByKey = new Map(columns.map((column) => [column.dataIndex, {
...column,
key: column.dataIndex,
}]));
const visibleColumns = fieldConfig.order
.filter((key) => !fieldConfig.hidden.includes(key) && columnsByKey.has(key))
.map((key) => columnsByKey.get(key));
const tableScrollX = visibleColumns.reduce((sum, column) => sum + (column.width || 100), 0) + 40;
const handleExport = async () => {
if (!productId || filteredRows.length === 0) {
message.warning('没有可导出的数据');
return;
}
setExporting(true);
try {
const exportColumns = visibleColumns.map((column) => ({
key: column.dataIndex,
label: DROPSHIP_FIELD_LABELS[column.dataIndex] || column.dataIndex,
percent: PERCENT_FIELD_KEYS.has(column.dataIndex),
}));
const exportRows = filteredRows.map((row) => Object.fromEntries(
exportColumns.map(({ key }) => [key, row[key]])
));
const blob = await exportPricingTable({ spu: productId, columns: exportColumns, rows: exportRows });
const stamp = new Date();
const pad = (value) => String(value).padStart(2, '0');
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = `1688代发测算_${productId}_${stamp.getFullYear()}${pad(stamp.getMonth() + 1)}${pad(stamp.getDate())}-${pad(stamp.getHours())}${pad(stamp.getMinutes())}.xlsx`;
link.click();
URL.revokeObjectURL(link.href);
message.success(`已导出 ${filteredRows.length} 行 × ${exportColumns.length} 列`);
} catch (error) {
message.error(getFriendlyError(error, '导出失败,请稍后重试'));
} finally {
setExporting(false);
}
};
const handleSave = async () => {
setSaving(true);
try {
await savePricingHistory({
calculatorType: 'alibaba1688',
spuCode: productId,
selectionContext: isSelectionPricingMode ? {
planId: selectionPlanId,
itemId: selectionItemId,
workflowProfile: 'alibaba_1688_pending',
} : undefined,
brand: productDetail?.storeName || product.storeName || '',
note: saveNote.trim(),
product: {
productId,
title: productTitle,
mainImage: productMainImage,
sourceUrl: productSourceUrl,
storeName: productDetail?.storeName || product.storeName || '',
category: productDetail?.category || product.category || '',
skuDimensions: product.skuDimensions || [],
},
summary: {
rowCount: computedRows.length,
negativeProfitCount: computedRows.filter((row) => (
Number.isFinite(row.profitAmount) && row.profitAmount < 0
)).length,
},
rows: computedRows,
});
setSaveModalOpen(false);
setHistoryRefreshKey((key) => key + 1);
if (isSelectionPricingMode && typeof onSelectionPricingSaved === 'function') {
await onSelectionPricingSaved();
}
message.success(isSelectionPricingMode ? '1688 定价表已保存' : '1688 代发测算记录已保存,可在测算历史中还原');
} catch (error) {
message.error(getFriendlyError(error, '保存测算记录失败'));
} finally {
setSaving(false);
}
};
const handleRestore = async (record) => {
setRestoring(true);
try {
const detail = await fetchPricingHistoryDetail(record.id);
const payload = detail?.payload;
if (!payload || payload.calculatorType !== 'alibaba1688' || !Array.isArray(payload.rows)) {
throw new Error('该记录不是 1688 代发测算记录,无法在当前页签还原');
}
const restoredProduct = payload.product || { productId: payload.spuCode || detail.spuCode };
setProductDetail({
...restoredProduct,
productId: restoredProduct.productId || payload.spuCode || detail.spuCode,
restoredAt: detail.createdAt,
product: restoredProduct,
});
setRows(normalizeRestoredRows(payload.rows, effectiveDefaults));
setInput(restoredProduct.productId || payload.spuCode || detail.spuCode || '');
setFilters(DEFAULT_FILTERS);
setHistoryOpen(false);
message.success(`已还原商品 ${detail.spuCode} 的测算记录`);
} catch (error) {
message.error(getFriendlyError(error, '还原测算记录失败'));
} finally {
setRestoring(false);
}
};
const renderHero = () => (
<div className="pricing-calc-hero pricing-calc-dropship-hero">
<div className="pricing-calc-hero__icon">
<ShopOutlined />
</div>
<h2 className="pricing-calc-hero__title">1688代发拼多多测算</h2>
<p className="pricing-calc-hero__subtitle">
输入 1688 商品 ID 或商品详情链接,读取全部 SKU 的采购价、规格、图片与运费,按非黑标拼多多店铺规则完成售价与利润测算。
</p>
<Input.Search
className="pricing-calc-hero__search"
size="large"
placeholder="输入商品 ID 或 detail.1688.com/offer/... 链接"
enterButton="开始测算"
loading={searching}
value={input}
readOnly={isSelectionPricingMode}
onChange={(event) => setInput(event.target.value)}
onSearch={runSearch}
/>
{searchError ? (
<Alert className="pricing-calc-hero__error" type="warning" showIcon message={searchError} />
) : null}
<div className="pricing-calc-hero__footer">
<span className="pricing-calc-hero__hint">平台佣金 1.5% · 营销广告 2.5% · 营业利润率默认 12%</span>
{!embedded ? <Button type="link" icon={<HistoryOutlined />} loading={restoring} onClick={() => setHistoryOpen(true)}>
测算历史
</Button> : null}
</div>
</div>
);
const renderResult = () => (
<div>
<div className="pricing-calc-spubar pricing-calc-dropship-spubar">
<div className="pricing-calc-product-summary">
<ProductThumb url={productMainImage} size={52} />
<div className="pricing-calc-product-summary__body">
<Tooltip title={productTitle}>
<strong className="pricing-calc-product-summary__title">{productTitle || '1688 商品'}</strong>
</Tooltip>
<div className="pricing-calc-spubar__meta">
<Tag color="orange" className="pricing-calc-spubar__spu">{productId}</Tag>
<span>{rows.length} 个 SKU</span>
<span>{dimensions.length} 个规格组</span>
{productDetail?.storeName || product.storeName ? (
<span>厂家:{productDetail?.storeName || product.storeName}</span>
) : null}
{productDetail?.restoredAt ? <span>历史还原</span> : null}
<span>佣金 1.5%</span>
<span>种菜 0%</span>
<span>品牌费 0%</span>
</div>
</div>
</div>
<div className="pricing-calc-spubar__content">
<div className="pricing-calc-spubar__controls">
<Input.Search
className="pricing-calc-spubar__search"
placeholder="商品 ID 或 1688 商品链接"
enterButton="测算"
loading={searching}
value={input}
readOnly={isSelectionPricingMode}
onChange={(event) => setInput(event.target.value)}
onSearch={runSearch}
/>
</div>
<div className="pricing-calc-spubar__actions">
{!embedded ? <Button icon={<ReloadOutlined />} onClick={resetToHero}>新的测算</Button> : null}
{!embedded ? <Button icon={<HistoryOutlined />} loading={restoring} onClick={() => setHistoryOpen(true)}>测算历史</Button> : null}
<Popover
trigger="click"
placement="bottomRight"
title="批量设置活动折扣"
content={(
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
<InputNumber
min={0.01}
max={1}
step={0.05}
precision={2}
placeholder="如 0.6 = 6 折"
style={{ width: 120 }}
value={batchDiscount}
onChange={(value) => setBatchDiscount(value)}
/>
<Button
type="primary"
size="small"
disabled={!batchDiscount || batchDiscount <= 0 || filteredRows.length === 0}
onClick={applyBatchDiscount}
>
应用到全部行({filteredRows.length})
</Button>
</div>
)}
>
<Button icon={<CalculatorOutlined />}>批量折扣</Button>
</Popover>
<Button icon={<ControlOutlined />} onClick={() => setDefaultsOpen(true)}>默认值</Button>
<Button icon={<DownloadOutlined />} loading={exporting} onClick={handleExport}>导出</Button>
<Button icon={<SettingOutlined />} onClick={() => setFieldConfigOpen(true)}>字段配置</Button>
{productSourceUrl ? (
<Button icon={<ExportOutlined />} href={productSourceUrl} target="_blank" rel="noreferrer">查看 1688 商品</Button>
) : null}
<Button
type="primary"
icon={<SaveOutlined />}
disabled={rows.length === 0}
onClick={() => {
setSaveNote('');
setSaveModalOpen(true);
}}
>
{isSelectionPricingMode ? '保存定价表' : '保存测算记录'}
</Button>
</div>
</div>
<div className="pricing-calc-summary" aria-label="1688 代发定价测算摘要">
{summaryMetrics.map((item) => (
<SycmDashboardMetric key={item.key} label={item.label} value={item.value} help={false} />
))}
</div>
</div>
<div className="pricing-calc-filter">
<span className="pricing-calc-filter__item">
<label>采购状态</label>
<Select
allowClear
style={{ width: 112 }}
placeholder="全部"
value={filters.available}
options={[
{ value: true, label: '可采购' },
{ value: false, label: '不可采购' },
]}
onChange={(value) => applyFilter({ available: value })}
/>
</span>
{dimensions.map((dimension) => (
<span key={dimension} className="pricing-calc-filter__item">
<label>{dimension}</label>
<Select
allowClear
showSearch
optionFilterProp="label"
style={{ width: 150 }}
placeholder="全部"
value={filters.specs[dimension]}
options={dimensionOptions[dimension] || []}
onChange={(value) => applySpecFilter(dimension, value)}
/>
</span>
))}
<Input
allowClear
className="pricing-calc-filter__keyword"
prefix={<SearchOutlined />}
placeholder="SKU 名称 / 规格"
value={filters.keyword}
onChange={(event) => applyFilter({ keyword: event.target.value })}
/>
<Button icon={<ReloadOutlined />} onClick={resetFilters}>重置</Button>
<span className="pricing-calc-legend">
{LEGEND_ITEMS.map((item) => (
<span key={item.className} className="pricing-calc-legend__item">
<i className={`pricing-calc-chip ${item.className}`} />
{item.label}
</span>
))}
</span>
</div>
<Table
className="pricing-calc-table"
rowKey="id"
size="small"
columns={visibleColumns}
dataSource={filteredRows}
scroll={{ x: tableScrollX, scrollToFirstRowOnChange: true }}
pagination={false}
locale={{ emptyText: <Empty description="没有匹配的 SKU,请调整筛选条件" /> }}
/>
</div>
);
return (
<>
{productDetail ? renderResult() : renderHero()}
<Modal
title={isSelectionPricingMode ? '保存 1688 上架定价表' : '保存 1688 代发测算记录'}
open={saveModalOpen}
okText="保存"
cancelText="取消"
confirmLoading={saving}
onOk={handleSave}
onCancel={() => setSaveModalOpen(false)}
>
<p className="pricing-calc-save__hint">
{isSelectionPricingMode
? `将保存商品「${productId}」共 ${rows.length} 行定价,并写回当前上架任务。`
: `将保存商品「${productId}」共 ${rows.length} 行的完整测算快照,仅自己可见。`}
</p>
<Input
value={saveNote}
maxLength={200}
placeholder="备注(可选),如:首轮代发定价"
onChange={(event) => setSaveNote(event.target.value)}
/>
</Modal>
<DefaultsConfigModal
open={defaultsOpen}
onClose={() => setDefaultsOpen(false)}
defaults={defaults}
systemDefaults={DROPSHIP_SYSTEM_DEFAULTS}
saving={defaultsSaving}
onSave={handleSaveDefaults}
title="1688 代发默认值配置"
/>
<FieldConfigDrawer
open={fieldConfigOpen}
onClose={() => setFieldConfigOpen(false)}
config={fieldConfig}
fieldLabels={DROPSHIP_FIELD_LABELS}
createDefaultConfig={createDefaultDropshipFieldConfig}
onChange={(next) => {
setFieldConfig(next);
saveDropshipFieldConfig(next);
}}
/>
<HistoryDrawer
open={historyOpen}
onClose={() => setHistoryOpen(false)}
onRestore={handleRestore}
refreshKey={historyRefreshKey}
calculatorType="alibaba1688"
title="1688 代发测算历史"
identityLabel="商品 ID"
/>
</>
);
};
export default DropshipCalculator;