Update App.jsx via upload script - 2026-07-29 02:06:53

This commit is contained in:
mike 2026-07-29 02:07:16 +08:00
parent 160f9ee109
commit 49ca411592
1 changed files with 58 additions and 40 deletions

98
App.jsx
View File

@ -291,8 +291,8 @@ const TRANSLATION_DICTIONARY = {
'产品大类': { en: 'Category', fr: 'Catégorie', ru: 'Категория', es: 'Categoría', ar: 'الفئة' }, '产品大类': { en: 'Category', fr: 'Catégorie', ru: 'Категория', es: 'Categoría', ar: 'الفئة' },
'细分品类': { en: 'Type', fr: 'Type', ru: 'Тип', es: 'Tipo', ar: 'النوع' }, '细分品类': { en: 'Type', fr: 'Type', ru: 'Тип', es: 'Tipo', ar: 'النوع' },
'机型': { en: 'Machine Type', fr: "Type d'appareil", ru: 'Тип модели', es: 'Tipo de equipo', ar: 'طراز الجهاز' }, '机型': { en: 'Machine Type', fr: "Type d'appareil", ru: 'Тип модели', es: 'Tipo de equipo', ar: 'طراز الجهاز' },
'标机': { en: 'Standard', fr: 'Standard', ru: 'Стандартный', es: 'Estándar', ar: 'قياسي' }, '标': { en: 'Standard Model', fr: 'Modèle standard', ru: 'Стандартная модель', es: 'Modelo estándar', ar: 'الطراز القياسي' },
'长机': { en: 'Extended', fr: 'Prolongé', ru: 'Увеличенный', es: 'Extendido', ar: 'ممتد' }, '长延时': { en: 'Long Backup Time Model', fr: 'Modèle longue autonomie', ru: 'Модель увеличенной автономности', es: 'Modelo de larga autonomía', ar: 'طراز الدعم الطويل' },
'输出功率': { en: 'Output Power', fr: 'Puissance de sortie', ru: 'Выходная мощность', es: 'Potencia de salida', 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: '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: 'Input Voltage', fr: "Tension d'entrée", ru: 'Входное напряжение', es: 'Voltaje de entrada', ar: 'جهد الدخل' },
@ -315,6 +315,7 @@ const TRANSLATION_DICTIONARY = {
'控制管理': { en: 'Interface', fr: 'Interface', ru: 'Интерфейс', es: 'Interfaz', ar: 'الواجهة' }, '控制管理': { en: 'Interface', fr: 'Interface', ru: 'Интерфейс', es: 'Interfaz', ar: 'الواجهة' },
'湿度': { en: 'Humidity', fr: 'Humidité', ru: 'Влажность', es: 'Humedad', ar: 'الرطوبة' }, '湿度': { en: 'Humidity', fr: 'Humidité', ru: 'Влажность', es: 'Humedad', ar: 'الرطوبة' },
'噪音': { en: 'Noise Level', fr: 'Niveau sonore', ru: 'Уровень шума', es: 'Nivel de ruido', ar: 'مستوى الضوضاء' }, '噪音': { en: 'Noise Level', fr: 'Niveau sonore', ru: 'Уровень шума', es: 'Nivel de ruido', ar: 'مستوى الضوضاء' },
'主要特点': { en: 'Main Features', fr: 'Caractéristiques principales', ru: 'Основные характеристики', es: 'Características principales', ar: 'الميزات الرئيسية' },
'相数': { en: 'Phase', fr: 'Phase', ru: 'Фаза', es: 'Fase', ar: 'الطور' }, '相数': { en: 'Phase', fr: 'Phase', ru: 'Фаза', es: 'Fase', ar: 'الطور' },
'输入功率因数': { en: 'Input Power Factor', fr: "Facteur de puissance d'entrée", ru: 'Входной коэффициент мощности', es: 'Factor de potencia de entrada', ar: 'معامل قدرة الدخل' }, '输入功率因数': { en: 'Input Power Factor', fr: "Facteur de puissance d'entrée", ru: 'Входной коэффициент мощности', es: 'Factor de potencia de entrada', ar: 'معامل قدرة الدخل' },
'转换时间(市电到电池)': { en: 'Transfer Time (AC to Battery)', fr: 'Temps de transfert (secteur vers batterie)', ru: 'Время переключения (сеть на батарею)', es: 'Tiempo de transferencia (red a batería)', ar: 'وقت التحويل (من الشبكة إلى البطارية)' }, '转换时间(市电到电池)': { en: 'Transfer Time (AC to Battery)', fr: 'Temps de transfert (secteur vers batterie)', ru: 'Время переключения (сеть на батарею)', es: 'Tiempo de transferencia (red a batería)', ar: 'وقت التحويل (من الشبكة إلى البطارية)' },
@ -525,12 +526,26 @@ function containsChinese(str) {
return /[\u4e00-\u9fa5]/.test(String(str || '')); return /[\u4e00-\u9fa5]/.test(String(str || ''));
} }
// ""/""""/""
//
function normalizeMachineVariant(v) {
if (v === '标机') return '标准机型';
if (v === '长机') return '长延时机型';
return v;
}
//
//
const MACHINE_VARIANT_NOTES = {
'标准机型': '(*with built-in battery)',
'长延时机型': '(*external battery pack required)',
};
function gatherExportTerms(p, categories) { function gatherExportTerms(p, categories) {
const cat = (categories || []).find(c => c.id === p.category); const cat = (categories || []).find(c => c.id === p.category);
const terms = new Set(['产品规格书', '* 产品规格如有变动,恕不另行通知', '型号', '品牌', '产品大类']); const terms = new Set(['产品规格书', '* 产品规格如有变动,恕不另行通知', '型号', '品牌', '产品大类']);
if (cat) terms.add(cat.name); if (cat) terms.add(cat.name);
if (p.type) { terms.add('细分品类'); terms.add(p.type); } if (p.type) { terms.add('细分品类'); terms.add(p.type); }
if (isUpsCategory(cat) && p.machineVariant) { terms.add('机型'); terms.add(p.machineVariant); } if (isUpsCategory(cat) && p.machineVariant) { terms.add('机型'); terms.add(normalizeMachineVariant(p.machineVariant)); }
const rows = productExportRows(p, categories); const rows = productExportRows(p, categories);
let hasPortRow = false; let hasPortRow = false;
rows.forEach(r => { rows.forEach(r => {
@ -542,7 +557,9 @@ function gatherExportTerms(p, categories) {
}); });
if (hasPortRow) terms.add('输出'); if (hasPortRow) terms.add('输出');
// / // /
(p.sellingPoints || '').split('\n').map(l => l.trim()).filter(Boolean).forEach(line => terms.add(line)); const sellingPointLines = (p.sellingPoints || '').split('\n').map(l => l.trim()).filter(Boolean);
if (sellingPointLines.length > 0) terms.add('主要特点');
sellingPointLines.forEach(line => terms.add(line));
// (notes) // (notes)
// PDF // PDF
return Array.from(terms); return Array.from(terms);
@ -948,9 +965,9 @@ function normalizeProductTaxonomy(products, categories) {
if (p.type && !cat.types.includes(p.type)) cat.types.push(p.type); if (p.type && !cat.types.includes(p.type)) cat.types.push(p.type);
}); });
// "/" types // "/""/"
// /machineVariant // types machineVariant
cats.forEach(c => { c.types = c.types.filter(t => t !== '标机' && t !== '长机'); }); cats.forEach(c => { c.types = c.types.filter(t => !['标机', '长机', '标准机型', '长延时机型'].includes(t)); });
return { products: migratedProducts, categories: cats }; return { products: migratedProducts, categories: cats };
} }
@ -1341,14 +1358,14 @@ const Styles = () => (
.spec-sheet-table { margin-top: 0; border: 1px solid var(--copper-soft); border-top: none; } .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 td.spec-label { background: #F7F1E8; font-weight: 600; width: 38%; color: #6b5a3e; }
.spec-sheet-table tr:nth-child(even) td { background: #FBF8F3; } .spec-sheet-table tr:nth-child(even) td { background: #FBF8F3; }
.spec-sheet-two-col { display: flex; gap: 32px; align-items: flex-start; } .spec-sheet-cover { text-align: center; padding: 20px 0 8px; }
.spec-sheet-left { flex: 0 0 240px; } .spec-sheet-cover .spec-sheet-bar { text-align: left; }
.spec-sheet-right { flex: 1; min-width: 0; } .spec-sheet-image-full { max-width: 70%; max-height: 320px; object-fit: contain; border-radius: 6px; }
.spec-sheet-image { width: 100%; aspect-ratio: 1 / 1; object-fit: cover; border-radius: 6px; border: 1px solid var(--copper-soft); } .spec-sheet-points { margin: 14px 0 0; padding-left: 18px; font-size: 12.5px; color: #444; line-height: 1.7; text-align: left; }
.spec-sheet-points { margin: 14px 0 0; padding-left: 18px; font-size: 12.5px; color: #444; line-height: 1.7; }
.spec-sheet-points li { margin-bottom: 4px; } .spec-sheet-points li { margin-bottom: 4px; }
[dir="rtl"] .spec-sheet-two-col { flex-direction: row-reverse; } .spec-sheet-page-break { break-before: page; page-break-before: always; }
[dir="rtl"] .spec-sheet-points { padding-left: 0; padding-right: 18px; } [dir="rtl"] .spec-sheet-points { padding-left: 0; padding-right: 18px; text-align: right; }
[dir="rtl"] .spec-sheet-cover .spec-sheet-bar { text-align: right; }
@media print { @media print {
@ -2045,6 +2062,7 @@ function MainApp({ username, onLogout }) {
image: { type: 'jpeg', quality: 0.98 }, image: { type: 'jpeg', quality: 0.98 },
html2canvas: { scale: 2 }, html2canvas: { scale: 2 },
jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' }, jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' },
pagebreak: { mode: ['css', 'legacy'], before: '.spec-sheet-page-break' },
}).from(el).save(); }).from(el).save();
} catch (e) { } catch (e) {
notify('导出PDF失败请重试'); notify('导出PDF失败请重试');
@ -3857,7 +3875,7 @@ function ProductForm({ draft, setDraft, onSave, onCancel, categories, onManage,
if (newType === '迷你UPS') { if (newType === '迷你UPS') {
set({ set({
type: newType, type: newType,
machineVariant: '标机', machineVariant: '标',
voltage: draft.voltage || '100~240Vac/50-60Hz', voltage: draft.voltage || '100~240Vac/50-60Hz',
outputPortCount: draft.outputPortCount || '5', outputPortCount: draft.outputPortCount || '5',
outputPortTypes: (draft.outputPortTypes && draft.outputPortTypes.length) ? draft.outputPortTypes : [...DEFAULT_MINI_UPS_OUTPUT_TYPES], outputPortTypes: (draft.outputPortTypes && draft.outputPortTypes.length) ? draft.outputPortTypes : [...DEFAULT_MINI_UPS_OUTPUT_TYPES],
@ -3878,12 +3896,12 @@ function ProductForm({ draft, setDraft, onSave, onCancel, categories, onManage,
<div className="field"> <div className="field">
<span className="field-label">机型</span> <span className="field-label">机型</span>
{isMiniUps ? ( {isMiniUps ? (
<input className="input" value="标机" disabled readOnly /> <input className="input" value="标" disabled readOnly />
) : ( ) : (
<select className="select" value={draft.machineVariant} onChange={e => set({ machineVariant: e.target.value })}> <select className="select" value={draft.machineVariant} onChange={e => set({ machineVariant: e.target.value })}>
<option value="">请选择</option> <option value="">请选择</option>
<option value="标机"></option> <option value="标">内置电池</option>
<option value="长机"></option> <option value="长延时">延时需外接电池</option>
</select> </select>
)} )}
</div> </div>
@ -4127,7 +4145,7 @@ function ProductDetail({ product: p, onEdit, onDelete, categories, exportLang, o
<div className="grid-2" style={{ marginBottom: 16 }}> <div className="grid-2" style={{ marginBottom: 16 }}>
<Info icon={<Layers size={13} />} label="分类" value={categoryBreadcrumb(p, categories)} /> <Info icon={<Layers size={13} />} label="分类" value={categoryBreadcrumb(p, categories)} />
<Info icon={<Building2 size={13} />} label="品牌" value={p.brand || '—'} /> <Info icon={<Building2 size={13} />} label="品牌" value={p.brand || '—'} />
{showMachineVariant && <Info icon={<Tag size={13} />} label="机型" value={p.machineVariant} />} {showMachineVariant && <Info icon={<Tag size={13} />} label="机型" value={p.machineVariant ? `${normalizeMachineVariant(p.machineVariant)} ${MACHINE_VARIANT_NOTES[normalizeMachineVariant(p.machineVariant)] || ''}`.trim() : '—'} />}
{powerInfoItems(p, categories).map(item => ( {powerInfoItems(p, categories).map(item => (
<Info key={item.key} icon={<Zap size={13} />} label={item.label} value={item.value} /> <Info key={item.key} icon={<Zap size={13} />} label={item.label} value={item.value} />
))} ))}
@ -4156,6 +4174,7 @@ function ProductDetail({ product: p, onEdit, onDelete, categories, exportLang, o
<div className="doc-preview" dir={langInfo.rtl ? 'rtl' : 'ltr'} style={{ textAlign: langInfo.rtl ? 'right' : 'left' }}> <div className="doc-preview" dir={langInfo.rtl ? 'rtl' : 'ltr'} style={{ textAlign: langInfo.rtl ? 'right' : 'left' }}>
{(() => { {(() => {
const sellingPointLines = (p.sellingPoints || '').split('\n').map(l => l.trim()).filter(Boolean); const sellingPointLines = (p.sellingPoints || '').split('\n').map(l => l.trim()).filter(Boolean);
const hasCoverPage = !!(p.productImageFileName || sellingPointLines.length > 0);
const specTable = ( const specTable = (
<table className="doc-table spec-sheet-table"> <table className="doc-table spec-sheet-table">
<tbody> <tbody>
@ -4163,7 +4182,7 @@ function ProductDetail({ product: p, onEdit, onDelete, categories, exportLang, o
{p.brand && <tr><td className="spec-label">{L('品牌')}</td><td>{p.brand}</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> <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>} {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>} {showMachineVariant && <tr><td className="spec-label">{L('机型')}</td><td>{`${L(normalizeMachineVariant(p.machineVariant))} ${MACHINE_VARIANT_NOTES[normalizeMachineVariant(p.machineVariant)] || ''}`.trim()}</td></tr>}
{exportRows.map((r, i) => ( {exportRows.map((r, i) => (
<tr key={i}> <tr key={i}>
<td className="spec-label">{r.labelKey === '__port__' ? `${r.portType} ${L('输出')}` : L(r.labelKey)}</td> <td className="spec-label">{r.labelKey === '__port__' ? `${r.portType} ${L('输出')}` : L(r.labelKey)}</td>
@ -4175,25 +4194,29 @@ function ProductDetail({ product: p, onEdit, onDelete, categories, exportLang, o
); );
const disclaimer = <div style={{ marginTop: 18, fontSize: 11, color: '#999' }}>{L('* 产品规格如有变动,恕不另行通知')}</div>; const disclaimer = <div style={{ marginTop: 18, fontSize: 11, color: '#999' }}>{L('* 产品规格如有变动,恕不另行通知')}</div>;
if (p.productImageFileName) { if (hasCoverPage) {
return ( return (
<div className="spec-sheet-two-col"> <>
<div className="spec-sheet-left"> <div className="spec-sheet-cover">
<img className="spec-sheet-image" src={`/uploads/${p.id}/${p.productImageFileName}`} alt={p.name || ''} /> {p.productImageFileName && (
<div style={{ fontSize: 22, fontWeight: 800, marginTop: 14 }}>{p.name || '—'}</div> <img className="spec-sheet-image-full" src={`/uploads/${p.id}/${p.productImageFileName}`} alt={p.name || ''} />
{p.brand && <div style={{ color: '#888', marginTop: 2 }}>{p.brand}</div>} )}
<div style={{ fontSize: 28, fontWeight: 800, marginTop: 20 }}>{p.name || '—'}</div>
{p.brand && <div style={{ color: '#888', marginTop: 4, fontSize: 15 }}>{p.brand}</div>}
{sellingPointLines.length > 0 && ( {sellingPointLines.length > 0 && (
<ul className="spec-sheet-points"> <>
{sellingPointLines.map((line, i) => <li key={i}>{L(line)}</li>)} <div className="spec-sheet-bar" style={{ marginTop: 24 }}>{L('主要特点')}</div>
</ul> <ul className="spec-sheet-points">
{sellingPointLines.map((line, i) => <li key={i}>{L(line)}</li>)}
</ul>
</>
)} )}
</div> </div>
<div className="spec-sheet-right"> <div className="spec-sheet-page-break" />
<div className="spec-sheet-bar">{L('产品规格书')}</div> <div className="spec-sheet-bar">{L('产品规格书')}</div>
{specTable} {specTable}
{disclaimer} {disclaimer}
</div> </>
</div>
); );
} }
return ( return (
@ -4202,11 +4225,6 @@ function ProductDetail({ product: p, onEdit, onDelete, categories, exportLang, o
<div style={{ fontSize: 26, fontWeight: 800 }}>{p.name || '—'}</div> <div style={{ fontSize: 26, fontWeight: 800 }}>{p.name || '—'}</div>
{p.brand && <div style={{ color: '#888', marginTop: 4 }}>{p.brand}</div>} {p.brand && <div style={{ color: '#888', marginTop: 4 }}>{p.brand}</div>}
</div> </div>
{sellingPointLines.length > 0 && (
<ul className="spec-sheet-points" style={{ marginBottom: 18 }}>
{sellingPointLines.map((line, i) => <li key={i}>{L(line)}</li>)}
</ul>
)}
<div className="spec-sheet-bar">{L('产品规格书')}</div> <div className="spec-sheet-bar">{L('产品规格书')}</div>
{specTable} {specTable}
{disclaimer} {disclaimer}