/** * 7 入库与导出(对齐 v1 web/ozonSeller.html 上品登记表): * 「录入当前商品」把当前试算商品登记进当日登记表(localStorage,v1 同款交互), * 表格展示当日录入的商品(17 列,物流费/平台总抽成/重量/尺寸/状态为可选列), * 支持导出组合码 / 导出记录 CSV / 清空记录;页面右下角固定第二个录入按钮。 */ import { useCallback, useEffect, useState } from 'react'; import { Button, Input, message, Modal, Popconfirm, Table, Tooltip, Typography } from 'antd'; import type { ColumnsType } from 'antd/es/table'; import { CopyOutlined, DownloadOutlined, FileTextOutlined, PlusOutlined, RestOutlined } from '@ant-design/icons'; import { ProductDetail } from '@/services/product'; import { copyText, downloadCsv } from '@/utils/file'; const { Text } = Typography; /** v1 按钮/金额配色 */ const COLOR_SECONDARY = '#10b981'; // v1 bg-secondary(录入按钮) const RECORDS_KEY = 'trialRegisterRecords'; interface RegisterRecord { registeredAt: string; // 录入时间(当日过滤用) sku: string; modelCode: string; productName: string; status: string; purchaseUrl: string; sellingPrice: string; sellingPriceReserved: string; // 划线价 ¥(v1 预留价位) sellingPriceRub: string; sellingPriceRubReserved: string; // 划线价 ₽ lineDiscountRate: number; // 划线折扣率 % exchangeRate: string; logisticsFee: string; receivedPrice: string; purchasePrice: string; profit: string; profitRate: string; fullCommission: string; totalCost: string; weight: string; dimensions: string; logisticsLevel: string; // 低 / 高 / Premium } function loadRecords(): RegisterRecord[] { try { const s = localStorage.getItem(RECORDS_KEY); if (s) return JSON.parse(s) as RegisterRecord[]; } catch { /* 忽略坏数据 */ } return []; } function saveRecords(records: RegisterRecord[]) { try { localStorage.setItem(RECORDS_KEY, JSON.stringify(records)); } catch { /* 忽略存储失败 */ } } /** 是否今天(本地时区) */ function isToday(iso: string): boolean { if (!iso) return false; const d = new Date(iso); const now = new Date(); return d.getFullYear() === now.getFullYear() && d.getMonth() === now.getMonth() && d.getDate() === now.getDate(); } const cny = (v: unknown) => { const n = parseFloat(String(v ?? '')); return isNaN(n) ? '--' : `¥ ${n.toFixed(2)}`; }; const rub = (v: unknown) => { const n = parseFloat(String(v ?? '')); return isNaN(n) ? '--' : `₽ ${n.toFixed(2)}`; }; // eslint-disable-next-line @typescript-eslint/no-explicit-any function toRecord(product: ProductDetail): RegisterRecord | null { // eslint-disable-next-line @typescript-eslint/no-explicit-any const pr = (product.pricing ?? {}) as Record; const raw = (product.raw ?? {}) as Record; if (!pr.calculatedAt) return null; const scale = product.dimension_unit === 'cm' ? 1 : 0.1; const dim = (v: number | null | undefined) => (v == null ? '' : String(v * scale)); const level = (pr.logisticsLevel as string) ?? 'low'; const f = (v: unknown, digits = 2) => (v == null ? '' : Number(v).toFixed(digits)); return { registeredAt: new Date().toISOString(), sku: product.offer_id || '', modelCode: (raw.model_code as string) ?? '', productName: ((raw.title_zh as string) ?? (raw.title as string) ?? product.name ?? '').trim(), status: '在售', purchaseUrl: (raw.purchase_url as string) ?? '', sellingPrice: f(pr.sellingPriceCny), sellingPriceReserved: f(pr.linePriceCny), sellingPriceRub: f(pr.sellingPriceRub), sellingPriceRubReserved: f(pr.linePriceRub), lineDiscountRate: pr.lineDiscountRate ?? 50, exchangeRate: pr.fxRate != null ? Number(pr.fxRate).toFixed(4) : '', logisticsFee: f(pr.logisticsFee), receivedPrice: f(pr.receivedPrice), purchasePrice: f(pr.purchasePrice), profit: f(pr.profitPrice), profitRate: pr.profitRate != null ? String(Math.round(Number(pr.profitRate))) : '', fullCommission: f(pr.fullCommission), totalCost: f(pr.totalCost), weight: product.weight != null ? String(product.weight) : '', dimensions: product.depth != null && product.width != null && product.height != null ? `${dim(product.depth)}x${dim(product.width)}x${dim(product.height)}` : '', logisticsLevel: level === 'high' ? '高' : level === 'high2' ? 'Premium' : '低', }; } const CSV_HEADER = [ '货号(sku)', '商品名', '进货价', '物流费', '平台总抽成', '实收价', '完全成本', '销售价', '净利润', '净利率', '卢布销价', '重量', '尺寸', '状态', 'Ozon地址', '采买地址', ]; function recordCsvRow(r: RegisterRecord): Array { return [ r.sku || '', r.productName || '', r.purchasePrice || '', r.logisticsFee || '', r.fullCommission || '', r.receivedPrice || '', r.totalCost || '', r.sellingPrice || '', r.profit || '', r.profitRate ? `${r.profitRate}%` : '', r.sellingPriceRub || '', r.weight || '', r.dimensions || '', r.status || '在售', r.sku ? `https://www.ozon.ru/product/${r.sku}` : '', r.purchaseUrl || '', ]; } /** 组合码一行:`货号 划线₽价(两位)`,无划线价回退销售价₽ */ function comboLine(r: RegisterRecord): string { const rubPrice = r.sellingPriceRubReserved || r.sellingPriceRub; if (!r.sku || !rubPrice) return ''; return `${r.sku} ${Number(rubPrice).toFixed(2)}`; } interface Props { product: ProductDetail; } export default function TrialExportPanel({ product }: Props) { const [records, setRecords] = useState([]); const [showOptional, setShowOptional] = useState(false); const [comboOpen, setComboOpen] = useState(false); const [comboText, setComboText] = useState(''); useEffect(() => { setRecords(loadRecords().filter((r) => isToday(r.registeredAt))); }, []); const currentRecord = toRecord(product); /** 录入当前商品(对齐 v1 recordData:货号必填、先计价、当日货号去重) */ const registerCurrent = useCallback(() => { if (!currentRecord) { message.warning('请先在「2 价格试算」完成计价,再录入商品'); return; } if (!currentRecord.sku) { message.warning('请先填写货号(SKU),再录入商品'); return; } const list = loadRecords().filter((r) => isToday(r.registeredAt)); const dup = list.findIndex((r) => r.sku.trim().toLowerCase() === currentRecord.sku.trim().toLowerCase()); if (dup !== -1) { message.warning(`货号「${currentRecord.sku}」已存在于当日登记表(第 ${dup + 1} 条),请勿重复录入`); return; } const next = [currentRecord, ...list]; saveRecords(next); setRecords(next); message.success(`商品「${currentRecord.productName || currentRecord.sku}」录入成功`); }, [currentRecord]); const deleteRecord = (sku: string) => { const next = loadRecords().filter((r) => r.sku !== sku); saveRecords(next); setRecords(next.filter((r) => isToday(r.registeredAt))); }; const clearRecords = () => { saveRecords([]); setRecords([]); message.success('登记表已清空'); }; const openCombo = () => { if (records.length === 0) { message.warning('登记表为空,请先录入商品'); return; } const lines = records.map(comboLine).filter(Boolean); setComboText(lines.join('\n')); setComboOpen(true); }; const exportCsv = () => { if (records.length === 0) { message.warning('登记表为空,请先录入商品'); return; } const d = new Date(); const p = (v: number) => String(v).padStart(2, '0'); downloadCsv( [CSV_HEADER, ...records.map(recordCsvRow)], `${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}.csv`, ); message.success(`已导出 ${records.length} 条记录`); }; const buildColumns = (): ColumnsType => { const money = (field: keyof RegisterRecord, fmt: (v: unknown) => string) => ({ dataIndex: field, render: (v: unknown) => {fmt(v)}, }); const renderLink = (url: string, maxWidth: number) => ( {url} ); const base: ColumnsType = [ { title: '操作', width: 70, render: (_, r) => ( deleteRecord(r.sku)}> ), }, { title: '货号(sku)', dataIndex: 'sku', width: 110, render: (v: string) => {v || '--'} }, { title: '商品名', dataIndex: 'productName', ellipsis: true, render: (v: string) => {v || '--'}, }, { title: '进货价', width: 90, ...money('purchasePrice', cny) }, ]; const optional: ColumnsType = [ { title: '物流费', width: 95, ...money('logisticsFee', cny) }, { title: '平台总抽成', width: 105, ...money('fullCommission', cny) }, ]; const rest: ColumnsType = [ { title: '实收价', width: 90, ...money('receivedPrice', cny) }, { title: '完全成本', width: 95, ...money('totalCost', cny) }, { title: '销售价', width: 95, ...money('sellingPrice', cny) }, { title: '净利润', width: 90, ...money('profit', cny) }, { title: '净利率', dataIndex: 'profitRate', width: 80, render: (v: string) => {v ? `${v}%` : '--'} }, { title: '卢布销价', width: 100, ...money('sellingPriceRub', rub) }, ]; const optional2: ColumnsType = [ { title: '重量', dataIndex: 'weight', width: 85, render: (v: string) => {v ? `${v} g` : '--'} }, { title: '尺寸', dataIndex: 'dimensions', width: 120, render: (v: string) => {v ? `${v} cm` : '--'} }, { title: '状态', dataIndex: 'status', width: 75, render: (v: string) => {v || '在售'} }, ]; const links: ColumnsType = [ { title: 'Ozon地址', dataIndex: 'sku', width: 160, render: (sku: string) => { const url = sku ? `https://www.ozon.ru/product/${sku}` : ''; return url ? renderLink(url, 128) : --; }, }, { title: '采买地址', dataIndex: 'purchaseUrl', width: 180, render: (v: string) => v ? renderLink(v, 148) : --, }, ]; return showOptional ? [...base, ...optional, ...rest, ...optional2, ...links] : [...base, ...rest, ...links]; }; const registerButton = (fixed = false) => ( ); return (
{/* 工具条:左=录入按钮;右=可选列切换/导出/清空(按钮配色对齐 v1) */}
{registerButton()}
{/* 组合码弹窗(对齐 v1 上品组合码 textarea) */} setComboOpen(false)} footer={[ , , ]} > {/* 页面右下角固定录入按钮(不随页面滚动) */} {registerButton(true)} ); }