Update App.jsx via upload script - 2026-07-26 21:14:12
This commit is contained in:
parent
9fa916dbfc
commit
b140948f4d
311
App.jsx
311
App.jsx
|
|
@ -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导出用的内置专业翻译词典:固定字段名 + 已知的品类/机型名称,人工核对过的翻译,
|
||||
// 不依赖AI、不占用DeepSeek额度、永远一致准确。词典里没有的词(比如用户自己新建的品类/品牌名、
|
||||
// 或者产品备注这种自由文本)才会走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;
|
||||
}
|
||||
|
||||
// 把某个产品需要展示的规格行整理出来(和ProductDetail的powerInfoItems逻辑对齐,但拆分出
|
||||
// 独立的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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue