Update App.jsx via upload script - 2026-07-26 21:14:12

This commit is contained in:
mike 2026-07-26 21:14:37 +08:00
parent 9fa916dbfc
commit b140948f4d
1 changed files with 291 additions and 20 deletions

311
App.jsx
View File

@ -261,6 +261,160 @@ const blankCustomer = () => ({
});
const OUTPUT_PORT_TYPES = ['USB', 'DC', 'DC1', 'DC2', 'DC3', 'POE', 'Type-C', 'DC interface', 'POE interface'];
const EXPORT_LANGUAGES = [
{ code: 'zh', label: '中文', rtl: false },
{ code: 'en', label: 'English', rtl: false },
{ code: 'fr', label: 'Français', rtl: false },
{ code: 'ru', label: 'Русский', rtl: false },
{ code: 'es', label: 'Español', rtl: false },
{ code: 'ar', label: 'العربية', rtl: true },
];
// PDF + /
// AIDeepSeek/
// AI resolveExportTranslations
const TRANSLATION_DICTIONARY = {
//
'产品规格书': { en: 'Specifications', fr: 'Spécifications', ru: 'Технические характеристики', es: 'Especificaciones', ar: 'المواصفات' },
'* 产品规格如有变动,恕不另行通知': {
en: 'Product specifications are subject to change without notice.',
fr: "Les spécifications du produit peuvent être modifiées sans préavis.",
ru: 'Технические характеристики могут быть изменены без предварительного уведомления.',
es: 'Las especificaciones del producto están sujetas a cambios sin previo aviso.',
ar: 'تخضع مواصفات المنتج للتغيير دون إشعار مسبق.',
},
//
'型号': { en: 'Model', fr: 'Modèle', ru: 'Модель', es: 'Modelo', ar: 'الطراز' },
'品牌': { en: 'Brand', fr: 'Marque', ru: 'Бренд', es: 'Marca', ar: 'العلامة التجارية' },
'产品大类': { en: 'Category', fr: 'Catégorie', ru: 'Категория', es: 'Categoría', ar: 'الفئة' },
'细分品类': { en: 'Type', fr: 'Type', ru: 'Тип', es: 'Tipo', ar: 'النوع' },
'机型': { en: 'Machine Type', fr: "Type d'appareil", ru: 'Тип модели', es: 'Tipo de equipo', ar: 'طراز الجهاز' },
'标机': { en: 'Standard', fr: 'Standard', ru: 'Стандартный', es: 'Estándar', ar: 'قياسي' },
'长机': { en: 'Extended', fr: 'Prolongé', ru: 'Увеличенный', es: 'Extendido', ar: 'ممتد' },
'输出功率': { en: 'Output Power', fr: 'Puissance de sortie', ru: 'Выходная мощность', es: 'Potencia de salida', ar: 'طاقة الخرج' },
'功率 / 容量': { en: 'Power / Capacity', fr: 'Puissance / Capacité', ru: 'Мощность / Ёмкость', es: 'Potencia / Capacidad', ar: 'الطاقة / السعة' },
'输入电压': { en: 'Input Voltage', fr: "Tension d'entrée", ru: 'Входное напряжение', es: 'Voltaje de entrada', ar: 'جهد الدخل' },
'电压': { en: 'Voltage', fr: 'Tension', ru: 'Напряжение', es: 'Voltaje', ar: 'الجهد' },
'视载功率': { en: 'Rated Power (VA)', fr: 'Puissance nominale (VA)', ru: 'Номинальная мощность (ВА)', es: 'Potencia nominal (VA)', ar: 'الطاقة الاسمية (فولت أمبير)' },
'功率因数': { en: 'Power Factor', fr: 'Facteur de puissance', ru: 'Коэффициент мощности', es: 'Factor de potencia', ar: 'معامل القدرة' },
'最大功率': { en: 'Max Power', fr: 'Puissance maximale', ru: 'Максимальная мощность', es: 'Potencia máxima', ar: 'أقصى طاقة' },
'输出口数量': { en: 'Output Ports', fr: 'Nombre de sorties', ru: 'Количество выходов', es: 'Número de salidas', ar: 'عدد منافذ الخرج' },
'输出口类型': { en: 'Output Types', fr: 'Types de sortie', ru: 'Типы выходов', es: 'Tipos de salida', ar: 'أنواع منافذ الخرج' },
'电池数量和容量': { en: 'Battery Qty & Capacity', fr: 'Nombre et capacité de batterie', ru: 'Кол-во и ёмкость батарей', es: 'Cantidad y capacidad de batería', ar: 'عدد وسعة البطارية' },
'尺寸(mm)': { en: 'Dimensions (mm)', fr: 'Dimensions (mm)', ru: 'Габариты (мм)', es: 'Dimensiones (mm)', ar: 'الأبعاد (مم)' },
'净重(kg)': { en: 'Net Weight (kg)', fr: 'Poids net (kg)', ru: 'Вес нетто (кг)', es: 'Peso neto (kg)', ar: 'الوزن الصافي (كجم)' },
'备注': { en: 'Notes', fr: 'Remarques', ru: 'Примечания', es: 'Notas', ar: 'ملاحظات' },
'输出': { en: 'Output', fr: 'Sortie', ru: 'Выход', es: 'Salida', ar: 'الخرج' },
//
'UPS不间断电源': { en: 'UPS (Uninterruptible Power Supply)', fr: 'Onduleur (UPS)', ru: 'ИБП (Источник бесперебойного питания)', es: 'SAI (Sistema de Alimentación Ininterrumpida)', ar: 'مزود طاقة غير منقطعة (UPS)' },
'光伏逆变器': { en: 'Solar Inverter', fr: 'Onduleur solaire', ru: 'Солнечный инвертор', es: 'Inversor solar', ar: 'عاكس شمسي' },
'储能电池': { en: 'Energy Storage Battery', fr: "Batterie de stockage d'énergie", ru: 'Аккумулятор для хранения энергии', es: 'Batería de almacenamiento de energía', ar: 'بطارية تخزين الطاقة' },
//
'迷你UPS': { en: 'Mini UPS', fr: 'Mini onduleur', ru: 'Мини-ИБП', es: 'Mini SAI', ar: 'يو بي إس صغير' },
'离线式UPS': { en: 'Offline UPS', fr: 'Onduleur hors ligne', ru: 'Резервный ИБП (Offline)', es: 'SAI fuera de línea', ar: 'يو بي إس غير متصل' },
'在线互动式UPS': { en: 'Line-Interactive UPS', fr: 'Onduleur interactif', ru: 'Линейно-интерактивный ИБП', es: 'SAI interactivo de línea', ar: 'يو بي إس تفاعلي الخط' },
'高频塔式UPS': { en: 'High-Frequency Tower UPS', fr: 'Onduleur tour haute fréquence', ru: 'Высокочастотный башенный ИБП', es: 'SAI torre de alta frecuencia', ar: 'يو بي إس برجي عالي التردد' },
'工频UPS': { en: 'Low-Frequency UPS', fr: 'Onduleur basse fréquence', ru: 'Низкочастотный ИБП', es: 'SAI de baja frecuencia', ar: 'يو بي إس منخفض التردد' },
'模块化UPS': { en: 'Modular UPS', fr: 'Onduleur modulaire', ru: 'Модульный ИБП', es: 'SAI modular', ar: 'يو بي إس معياري' },
'机架式UPS': { en: 'Rack-mount UPS', fr: 'Onduleur rackable', ru: 'Стоечный ИБП', es: 'SAI para rack', ar: 'يو بي إس للتركيب في الرف' },
'太阳能混网逆变器': { en: 'Hybrid Solar Inverter', fr: 'Onduleur solaire hybride', ru: 'Гибридный солнечный инвертор', es: 'Inversor solar híbrido', ar: 'عاكس شمسي هجين' },
'并网/离网逆变器': { en: 'Grid-tied / Off-grid Inverter', fr: 'Onduleur raccordé / hors réseau', ru: 'Сетевой / автономный инвертор', es: 'Inversor conectado a la red / aislado', ar: 'عاكس متصل بالشبكة / خارج الشبكة' },
'铅酸电池': { en: 'Lead-acid Battery', fr: 'Batterie au plomb', ru: 'Свинцово-кислотная батарея', es: 'Batería de plomo-ácido', ar: 'بطارية الرصاص الحمضية' },
'磷酸铁锂电池': { en: 'LiFePO4 Battery', fr: 'Batterie LiFePO4', ru: 'Аккумулятор LiFePO4', es: 'Batería LiFePO4', ar: 'بطارية ليثيوم فوسفات الحديد' },
};
// AI
// AI crm:translationGlossary
// DeepSeek
async function resolveExportTranslations(terms, lang) {
if (lang === 'zh' || !terms.length) {
const map = {};
terms.forEach(t => { map[t] = t; });
return map;
}
let glossary = {};
try {
const res = await storageGet('crm:translationGlossary');
glossary = JSON.parse(res.value) || {};
} catch (e) {}
const map = {};
const misses = [];
terms.forEach(t => {
if (TRANSLATION_DICTIONARY[t] && TRANSLATION_DICTIONARY[t][lang]) {
map[t] = TRANSLATION_DICTIONARY[t][lang];
} else if (glossary[t] && glossary[t][lang]) {
map[t] = glossary[t][lang];
} else {
misses.push(t);
}
});
if (misses.length) {
try {
const translations = await translateTerms(misses, lang);
const newGlossary = { ...glossary };
misses.forEach(t => {
const translated = (translations && translations[t]) || t;
map[t] = translated;
newGlossary[t] = { ...(newGlossary[t] || {}), [lang]: translated };
});
try { await storageSet('crm:translationGlossary', JSON.stringify(newGlossary)); } catch (e) {}
} catch (e) {
misses.forEach(t => { map[t] = t; }); //
}
}
return map;
}
// ProductDetailpowerInfoItems
// label/value便""""
function productExportRows(p, categories) {
const cat = (categories || []).find(c => c.id === p.category);
const rows = [];
if (isUpsCategory(cat) && p.type !== '迷你UPS') {
const maxKW = computeMaxPowerKW(p.ratedPower, p.ratedPowerUnit, p.powerFactor);
if (p.ratedPower) rows.push({ labelKey: '视载功率', value: `${p.ratedPower} ${p.ratedPowerUnit || 'VA'}` });
if (p.powerFactor) rows.push({ labelKey: '功率因数', value: p.powerFactor });
if (maxKW !== null) rows.push({ labelKey: '最大功率', value: `${maxKW} kW` });
if (p.voltage) rows.push({ labelKey: '电压', value: p.voltage });
} else if (isUpsCategory(cat) && p.type === '迷你UPS') {
if (p.powerOrCapacity) rows.push({ labelKey: '输出功率', value: p.powerOrCapacity });
if (p.voltage) rows.push({ labelKey: '输入电压', value: p.voltage });
if (p.outputPortCount) rows.push({ labelKey: '输出口数量', value: p.outputPortCount });
if (p.outputPortTypes && p.outputPortTypes.length) rows.push({ labelKey: '输出口类型', value: p.outputPortTypes.join(' / ') });
(p.outputPorts || []).forEach(port => {
if (port.voltage || port.current) rows.push({ labelKey: '__port__', portType: port.type, value: [port.voltage, port.current].filter(Boolean).join(' / ') });
});
if (p.batterySpec) rows.push({ labelKey: '电池数量和容量', value: p.batterySpec });
if (p.dimensions) rows.push({ labelKey: '尺寸(mm)', value: p.dimensions });
if (p.netWeight) rows.push({ labelKey: '净重(kg)', value: p.netWeight });
} else {
if (p.powerOrCapacity) rows.push({ labelKey: '功率 / 容量', value: p.powerOrCapacity });
if (p.voltage) rows.push({ labelKey: '电压', value: p.voltage });
}
return rows;
}
// + / +
function gatherExportTerms(p, categories) {
const cat = (categories || []).find(c => c.id === p.category);
const terms = new Set(['产品规格书', '* 产品规格如有变动,恕不另行通知', '型号', '品牌', '产品大类']);
if (cat) terms.add(cat.name);
if (p.type) { terms.add('细分品类'); terms.add(p.type); }
if (isUpsCategory(cat) && p.machineVariant) { terms.add('机型'); terms.add(p.machineVariant); }
const rows = productExportRows(p, categories);
let hasPortRow = false;
rows.forEach(r => {
if (r.labelKey === '__port__') hasPortRow = true;
else terms.add(r.labelKey);
});
if (hasPortRow) terms.add('输出');
if (p.notes && p.notes.trim()) { terms.add('备注'); terms.add(p.notes.trim()); }
return Array.from(terms);
}
const DEFAULT_MINI_UPS_OUTPUT_TYPES = ['USB', 'DC1', 'DC2', 'DC3', 'POE'];
const blankProduct = () => ({
@ -950,6 +1104,16 @@ const Styles = () => (
.doc-totals-row.total { font-weight: 700; color: #222; border-top: 1px solid #ddd; margin-top: 4px; padding-top: 8px; }
.doc-balance-due { background: #f2f2f2; border-radius: 6px; padding: 10px 14px; margin-top: 6px; }
/* ---------- multi-language product spec sheet export ---------- */
.spec-sheet-bar {
background: var(--copper); color: #fff; font-weight: 700; font-size: 15px;
padding: 10px 16px; border-radius: 4px 4px 0 0; letter-spacing: 0.02em;
}
.spec-sheet-table { margin-top: 0; border: 1px solid var(--copper-soft); border-top: none; }
.spec-sheet-table td.spec-label { background: #F7F1E8; font-weight: 600; width: 38%; color: #6b5a3e; }
.spec-sheet-table tr:nth-child(even) td { background: #FBF8F3; }
@media print {
body * { visibility: hidden; }
.doc-preview, .doc-preview * { visibility: visible; }
@ -1183,6 +1347,7 @@ function MainApp({ username, onLogout }) {
const [aiImportHintCategory, setAiImportHintCategory] = useState('');
const [aiImportHintBrand, setAiImportHintBrand] = useState('');
const [aiImportHintType, setAiImportHintType] = useState('');
const [aiImportTruncatedWarning, setAiImportTruncatedWarning] = useState(false);
const [emailModal, setEmailModal] = useState(null);
const [exportOpen, setExportOpen] = useState(false);
@ -1276,6 +1441,21 @@ function MainApp({ username, onLogout }) {
const selected = customers.find(c => c.id === selectedId) || null;
const selectedProduct = products.find(p => p.id === selectedProductId) || null;
const [productExportLang, setProductExportLang] = useState('zh');
const [productExportLabels, setProductExportLabels] = useState({});
const [productExportLoading, setProductExportLoading] = useState(false);
const [productExportPdfBusy, setProductExportPdfBusy] = useState(false);
useEffect(() => {
if (!selectedProduct) { setProductExportLabels({}); return; }
let cancelled = false;
setProductExportLoading(true);
const terms = gatherExportTerms(selectedProduct, productCategories);
resolveExportTranslations(terms, productExportLang)
.then(map => { if (!cancelled) setProductExportLabels(map); })
.finally(() => { if (!cancelled) setProductExportLoading(false); });
return () => { cancelled = true; };
}, [selectedProduct && selectedProduct.id, productExportLang, productCategories]);
const filteredCustomers = useMemo(() => {
let list = customers;
@ -1594,11 +1774,42 @@ function MainApp({ username, onLogout }) {
notify('已删除');
}
async function handleExportProductPdf() {
if (!selectedProduct) return;
setProductExportPdfBusy(true);
const el = document.querySelector('.doc-preview');
let originalBorder, originalBoxShadow;
try {
const mod = await import('html2pdf.js');
const html2pdf = mod.default || mod;
if (!el) { notify('找不到预览内容'); return; }
originalBorder = el.style.border;
originalBoxShadow = el.style.boxShadow;
el.style.border = 'none';
el.style.boxShadow = 'none';
const langLabel = (EXPORT_LANGUAGES.find(l => l.code === productExportLang) || {}).code || 'zh';
const filename = `${selectedProduct.name || 'product'}_${langLabel}.pdf`;
await html2pdf().set({
margin: 10,
filename,
image: { type: 'jpeg', quality: 0.98 },
html2canvas: { scale: 2 },
jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' },
}).from(el).save();
} catch (e) {
notify('导出PDF失败请重试');
} finally {
if (el) { el.style.border = originalBorder || ''; el.style.boxShadow = originalBoxShadow || ''; }
setProductExportPdfBusy(false);
}
}
/* ------------------------------ ai bulk import ----------------------------- */
function openAiImport() {
setAiImportOpen(true); setAiImportDrafts(null); setAiImportError('');
setAiImportHintCategory(''); setAiImportHintBrand(''); setAiImportHintType('');
setAiImportTruncatedWarning(false);
}
function closeAiImport() { setAiImportOpen(false); setAiImportDrafts(null); setAiImportError(''); setAiImportBusy(false); }
@ -1614,6 +1825,7 @@ function MainApp({ username, onLogout }) {
});
const parsed = (res && res.products) || [];
if (!parsed.length) { setAiImportError('没有从文件里识别出任何产品,换一份更清晰的文件试试'); return; }
setAiImportTruncatedWarning(!!(res && res.truncated));
const drafts = parsed.map((p, i) => ({
_localId: 'aidraft_' + i + '_' + Math.random().toString(36).slice(2, 6),
include: true,
@ -2296,7 +2508,13 @@ function MainApp({ username, onLogout }) {
{productDraft ? (
<ProductForm draft={productDraft} setDraft={setProductDraft} onSave={saveProductDraft} onCancel={cancelProductDraft} categories={productCategories} onManage={(mode) => setTaxManageMode(mode)} />
) : selectedProduct ? (
<ProductDetail product={selectedProduct} onEdit={() => startEditProduct(selectedProduct)} onDelete={() => deleteProduct(selectedProduct.id)} categories={productCategories} />
<ProductDetail
product={selectedProduct} onEdit={() => startEditProduct(selectedProduct)} onDelete={() => deleteProduct(selectedProduct.id)}
categories={productCategories}
exportLang={productExportLang} onExportLangChange={setProductExportLang}
exportLabels={productExportLabels} exportLoading={productExportLoading}
onExportPdf={handleExportProductPdf} exportPdfBusy={productExportPdfBusy}
/>
) : (
<div className="panel"><div className="empty-state"><Package size={28} />选择左侧产品查看详情或新建一个产品档案</div></div>
)}
@ -2882,6 +3100,12 @@ function MainApp({ username, onLogout }) {
识别出 {aiImportDrafts.length} 个产品请检查型号/品牌/大类/细分品类勾掉不需要导入的行确认后点"确认导入"
其余规格字段功率/电压/输出口/电池/尺寸等已经一并识别好导入后可以在产品详情里继续核对修改
</div>
{aiImportTruncatedWarning && (
<div className="disclaimer" style={{ background: '#FDECEC', color: 'var(--led-red)', marginTop: -6 }}>
AI这次输出的内容超出了长度限制可能还有没识别完的产品被截断丢弃了建议核对一下上面这 {aiImportDrafts.length} 个是不是齐了
如果少了把剩下的文件单独再上传一次比如分成更小的批次每批3-5个文件
</div>
)}
<div style={{ display: 'flex', flexDirection: 'column', gap: 10, maxHeight: 420, overflowY: 'auto' }}>
{aiImportDrafts.map(d => (
<div key={d._localId} className="tax-manage-list-row" style={{ flexDirection: 'column', alignItems: 'stretch', gap: 8 }}>
@ -3430,32 +3654,79 @@ function ProductForm({ draft, setDraft, onSave, onCancel, categories, onManage }
);
}
function ProductDetail({ product: p, onEdit, onDelete, categories }) {
function ProductDetail({ product: p, onEdit, onDelete, categories, exportLang, onExportLangChange, exportLabels, exportLoading, onExportPdf, exportPdfBusy }) {
const cat = (categories || []).find(c => c.id === p.category);
const showMachineVariant = isUpsCategory(cat) && p.machineVariant;
const L = (key) => (exportLabels && exportLabels[key]) || key;
const langInfo = EXPORT_LANGUAGES.find(l => l.code === exportLang) || EXPORT_LANGUAGES[0];
const exportRows = productExportRows(p, categories);
return (
<div className="panel">
<div className="panel-header">
<div className="panel-title"><div className="led-dot blue" />{p.name || '(未命名)'}</div>
<div style={{ display: 'flex', gap: 8 }}>
<button className="btn btn-sm" onClick={onEdit}><Edit2 size={12} />编辑</button>
<button className="btn btn-sm btn-ghost btn-danger" onClick={onDelete}><Trash2 size={12} /></button>
<>
<div className="panel section-gap">
<div className="panel-header">
<div className="panel-title"><div className="led-dot blue" />{p.name || '(未命名)'}</div>
<div style={{ display: 'flex', gap: 8 }}>
<button className="btn btn-sm" onClick={onEdit}><Edit2 size={12} />编辑</button>
<button className="btn btn-sm btn-ghost btn-danger" onClick={onDelete}><Trash2 size={12} /></button>
</div>
</div>
<div className="panel-body" style={{ padding: 18 }}>
<div className="grid-2" style={{ marginBottom: 16 }}>
<Info icon={<Layers size={13} />} label="分类" value={categoryBreadcrumb(p, categories)} />
<Info icon={<Building2 size={13} />} label="品牌" value={p.brand || '—'} />
{showMachineVariant && <Info icon={<Tag size={13} />} label="机型" value={p.machineVariant} />}
{powerInfoItems(p, categories).map(item => (
<Info key={item.key} icon={<Zap size={13} />} label={item.label} value={item.value} />
))}
<Info icon={<Zap size={13} />} label="电压" value={p.voltage || '—'} />
<Info icon={<Tag size={13} />} label="正本价格(元)" value={p.costPrice || '—'} />
</div>
{p.notes && <div className="field"><span className="field-label">备注</span><div style={{ fontSize: 12.5, whiteSpace: 'pre-wrap' }}>{p.notes}</div></div>}
</div>
</div>
<div className="panel-body" style={{ padding: 18 }}>
<div className="grid-2" style={{ marginBottom: 16 }}>
<Info icon={<Layers size={13} />} label="分类" value={categoryBreadcrumb(p, categories)} />
<Info icon={<Building2 size={13} />} label="品牌" value={p.brand || '—'} />
{showMachineVariant && <Info icon={<Tag size={13} />} label="机型" value={p.machineVariant} />}
{powerInfoItems(p, categories).map(item => (
<Info key={item.key} icon={<Zap size={13} />} label={item.label} value={item.value} />
))}
<Info icon={<Zap size={13} />} label="电压" value={p.voltage || '—'} />
<Info icon={<Tag size={13} />} label="正本价格(元)" value={p.costPrice || '—'} />
<div className="panel section-gap">
<div className="panel-header"><div className="panel-title"><Globe size={15} />多语言PDF资料导出</div></div>
<div className="panel-body" style={{ padding: 18 }}>
<div className="grid-2" style={{ alignItems: 'flex-end', marginBottom: 16 }}>
<div className="field" style={{ marginBottom: 0 }}>
<span className="field-label">导出语言</span>
<select className="select" value={exportLang} onChange={e => onExportLangChange(e.target.value)}>
{EXPORT_LANGUAGES.map(l => <option key={l.code} value={l.code}>{l.label}</option>)}
</select>
</div>
<button className="btn btn-primary" disabled={exportPdfBusy || exportLoading} onClick={onExportPdf}>
<FileText size={13} />{exportPdfBusy ? '导出中…' : exportLoading ? '翻译准备中…' : '导出PDF资料'}
</button>
</div>
<div className="doc-preview" dir={langInfo.rtl ? 'rtl' : 'ltr'} style={{ textAlign: langInfo.rtl ? 'right' : 'left' }}>
<div style={{ marginBottom: 20 }}>
<div style={{ fontSize: 26, fontWeight: 800 }}>{p.name || '—'}</div>
{p.brand && <div style={{ color: '#888', marginTop: 4 }}>{p.brand}</div>}
</div>
<div className="spec-sheet-bar">{L('产品规格书')}</div>
<table className="doc-table spec-sheet-table">
<tbody>
<tr><td className="spec-label">{L('型号')}</td><td>{p.name || '—'}</td></tr>
{p.brand && <tr><td className="spec-label">{L('品牌')}</td><td>{p.brand}</td></tr>}
<tr><td className="spec-label">{L('产品大类')}</td><td>{cat ? L(cat.name) : '—'}</td></tr>
{p.type && <tr><td className="spec-label">{L('细分品类')}</td><td>{L(p.type)}</td></tr>}
{showMachineVariant && <tr><td className="spec-label">{L('机型')}</td><td>{L(p.machineVariant)}</td></tr>}
{exportRows.map((r, i) => (
<tr key={i}>
<td className="spec-label">{r.labelKey === '__port__' ? `${r.portType} ${L('输出')}` : L(r.labelKey)}</td>
<td>{r.value}</td>
</tr>
))}
{p.notes && p.notes.trim() && <tr><td className="spec-label">{L('备注')}</td><td>{L(p.notes.trim())}</td></tr>}
</tbody>
</table>
<div style={{ marginTop: 18, fontSize: 11, color: '#999' }}>{L('* 产品规格如有变动,恕不另行通知')}</div>
</div>
</div>
{p.notes && <div className="field"><span className="field-label">备注</span><div style={{ fontSize: 12.5, whiteSpace: 'pre-wrap' }}>{p.notes}</div></div>}
</div>
</div>
</>
);
}