Update App.jsx via upload script - 2026-07-29 02:06:53
This commit is contained in:
parent
160f9ee109
commit
49ca411592
98
App.jsx
98
App.jsx
|
|
@ -291,8 +291,8 @@ const TRANSLATION_DICTIONARY = {
|
|||
'产品大类': { 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: 'Standard Model', fr: 'Modèle standard', ru: 'Стандартная модель', es: 'Modelo estándar', 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: 'Power / Capacity', fr: 'Puissance / Capacité', ru: 'Мощность / Ёмкость', es: 'Potencia / Capacidad', 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: 'Humidity', fr: 'Humidité', ru: 'Влажность', es: 'Humedad', 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: '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: 'وقت التحويل (من الشبكة إلى البطارية)' },
|
||||
|
|
@ -525,12 +526,26 @@ function containsChinese(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) {
|
||||
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); }
|
||||
if (isUpsCategory(cat) && p.machineVariant) { terms.add('机型'); terms.add(normalizeMachineVariant(p.machineVariant)); }
|
||||
const rows = productExportRows(p, categories);
|
||||
let hasPortRow = false;
|
||||
rows.forEach(r => {
|
||||
|
|
@ -542,7 +557,9 @@ function gatherExportTerms(p, categories) {
|
|||
});
|
||||
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)字段故意不收集、不导出——这里经常被用来记内部信息(成本价、
|
||||
// 采购渠道、库存数量这类),一旦跟着PDF发给客户后果很麻烦,所以规格书导出永远不碰这个字段。
|
||||
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);
|
||||
});
|
||||
|
||||
// 老数据兼容:如果之前版本把"标机/长机"误存进了某个类目的 types 列表里,这里清掉,
|
||||
// 现在标机/长机是独立字段(machineVariant),不再作为细分品类的一个选项。
|
||||
cats.forEach(c => { c.types = c.types.filter(t => t !== '标机' && t !== '长机'); });
|
||||
// 老数据兼容:如果之前版本把"标机/长机"(或者新命名"标准机型/长延时机型")误存进了某个类目的
|
||||
// types 列表里,这里清掉,机型是独立字段(machineVariant),不再作为细分品类的一个选项。
|
||||
cats.forEach(c => { c.types = c.types.filter(t => !['标机', '长机', '标准机型', '长延时机型'].includes(t)); });
|
||||
|
||||
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 td.spec-label { background: #F7F1E8; font-weight: 600; width: 38%; color: #6b5a3e; }
|
||||
.spec-sheet-table tr:nth-child(even) td { background: #FBF8F3; }
|
||||
.spec-sheet-two-col { display: flex; gap: 32px; align-items: flex-start; }
|
||||
.spec-sheet-left { flex: 0 0 240px; }
|
||||
.spec-sheet-right { flex: 1; min-width: 0; }
|
||||
.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; }
|
||||
.spec-sheet-cover { text-align: center; padding: 20px 0 8px; }
|
||||
.spec-sheet-cover .spec-sheet-bar { text-align: left; }
|
||||
.spec-sheet-image-full { max-width: 70%; max-height: 320px; object-fit: contain; border-radius: 6px; }
|
||||
.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 li { margin-bottom: 4px; }
|
||||
[dir="rtl"] .spec-sheet-two-col { flex-direction: row-reverse; }
|
||||
[dir="rtl"] .spec-sheet-points { padding-left: 0; padding-right: 18px; }
|
||||
.spec-sheet-page-break { break-before: page; page-break-before: always; }
|
||||
[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 {
|
||||
|
|
@ -2045,6 +2062,7 @@ function MainApp({ username, onLogout }) {
|
|||
image: { type: 'jpeg', quality: 0.98 },
|
||||
html2canvas: { scale: 2 },
|
||||
jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' },
|
||||
pagebreak: { mode: ['css', 'legacy'], before: '.spec-sheet-page-break' },
|
||||
}).from(el).save();
|
||||
} catch (e) {
|
||||
notify('导出PDF失败,请重试');
|
||||
|
|
@ -3857,7 +3875,7 @@ function ProductForm({ draft, setDraft, onSave, onCancel, categories, onManage,
|
|||
if (newType === '迷你UPS') {
|
||||
set({
|
||||
type: newType,
|
||||
machineVariant: '标机',
|
||||
machineVariant: '标准机型',
|
||||
voltage: draft.voltage || '100~240Vac/50-60Hz',
|
||||
outputPortCount: draft.outputPortCount || '5',
|
||||
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">
|
||||
<span className="field-label">机型</span>
|
||||
{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 })}>
|
||||
<option value="">请选择</option>
|
||||
<option value="标机">标机</option>
|
||||
<option value="长机">长机</option>
|
||||
<option value="标准机型">标准机型(内置电池)</option>
|
||||
<option value="长延时机型">长延时机型(需外接电池)</option>
|
||||
</select>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -4127,7 +4145,7 @@ function ProductDetail({ product: p, onEdit, onDelete, categories, exportLang, o
|
|||
<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} />}
|
||||
{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 => (
|
||||
<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' }}>
|
||||
{(() => {
|
||||
const sellingPointLines = (p.sellingPoints || '').split('\n').map(l => l.trim()).filter(Boolean);
|
||||
const hasCoverPage = !!(p.productImageFileName || sellingPointLines.length > 0);
|
||||
const specTable = (
|
||||
<table className="doc-table spec-sheet-table">
|
||||
<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>}
|
||||
<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>}
|
||||
{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) => (
|
||||
<tr key={i}>
|
||||
<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>;
|
||||
|
||||
if (p.productImageFileName) {
|
||||
if (hasCoverPage) {
|
||||
return (
|
||||
<div className="spec-sheet-two-col">
|
||||
<div className="spec-sheet-left">
|
||||
<img className="spec-sheet-image" src={`/uploads/${p.id}/${p.productImageFileName}`} alt={p.name || ''} />
|
||||
<div style={{ fontSize: 22, fontWeight: 800, marginTop: 14 }}>{p.name || '—'}</div>
|
||||
{p.brand && <div style={{ color: '#888', marginTop: 2 }}>{p.brand}</div>}
|
||||
<>
|
||||
<div className="spec-sheet-cover">
|
||||
{p.productImageFileName && (
|
||||
<img className="spec-sheet-image-full" src={`/uploads/${p.id}/${p.productImageFileName}`} alt={p.name || ''} />
|
||||
)}
|
||||
<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 && (
|
||||
<ul className="spec-sheet-points">
|
||||
{sellingPointLines.map((line, i) => <li key={i}>{L(line)}</li>)}
|
||||
</ul>
|
||||
<>
|
||||
<div className="spec-sheet-bar" style={{ marginTop: 24 }}>{L('主要特点')}</div>
|
||||
<ul className="spec-sheet-points">
|
||||
{sellingPointLines.map((line, i) => <li key={i}>{L(line)}</li>)}
|
||||
</ul>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="spec-sheet-right">
|
||||
<div className="spec-sheet-bar">{L('产品规格书')}</div>
|
||||
{specTable}
|
||||
{disclaimer}
|
||||
</div>
|
||||
</div>
|
||||
<div className="spec-sheet-page-break" />
|
||||
<div className="spec-sheet-bar">{L('产品规格书')}</div>
|
||||
{specTable}
|
||||
{disclaimer}
|
||||
</>
|
||||
);
|
||||
}
|
||||
return (
|
||||
|
|
@ -4202,11 +4225,6 @@ function ProductDetail({ product: p, onEdit, onDelete, categories, exportLang, o
|
|||
<div style={{ fontSize: 26, fontWeight: 800 }}>{p.name || '—'}</div>
|
||||
{p.brand && <div style={{ color: '#888', marginTop: 4 }}>{p.brand}</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>
|
||||
{specTable}
|
||||
{disclaimer}
|
||||
|
|
|
|||
Loading…
Reference in New Issue