Update App.jsx via upload script - 2026-07-26 22:07:53
This commit is contained in:
parent
b140948f4d
commit
b9bde850c0
157
App.jsx
157
App.jsx
|
|
@ -411,7 +411,10 @@ function gatherExportTerms(p, categories) {
|
||||||
else terms.add(r.labelKey);
|
else terms.add(r.labelKey);
|
||||||
});
|
});
|
||||||
if (hasPortRow) terms.add('输出');
|
if (hasPortRow) terms.add('输出');
|
||||||
if (p.notes && p.notes.trim()) { terms.add('备注'); terms.add(p.notes.trim()); }
|
// 对客卖点/主要特点:每行一条,都是要给客户看的文字,需要翻译
|
||||||
|
(p.sellingPoints || '').split('\n').map(l => l.trim()).filter(Boolean).forEach(line => terms.add(line));
|
||||||
|
// 注意:备注(notes)字段故意不收集、不导出——这里经常被用来记内部信息(成本价、
|
||||||
|
// 采购渠道、库存数量这类),一旦跟着PDF发给客户后果很麻烦,所以规格书导出永远不碰这个字段。
|
||||||
return Array.from(terms);
|
return Array.from(terms);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -430,6 +433,8 @@ const blankProduct = () => ({
|
||||||
batterySpec: '', // 迷你UPS专属:电池数量和容量,如 "5000mAh*4"
|
batterySpec: '', // 迷你UPS专属:电池数量和容量,如 "5000mAh*4"
|
||||||
dimensions: '', // 迷你UPS专属:尺寸(mm),如 "210*130*40"
|
dimensions: '', // 迷你UPS专属:尺寸(mm),如 "210*130*40"
|
||||||
netWeight: '', // 迷你UPS专属:净重(kg)
|
netWeight: '', // 迷你UPS专属:净重(kg)
|
||||||
|
productImageFileName: '', productImageOriginalName: '', // 产品主图,导出多语言PDF时展示
|
||||||
|
sellingPoints: '', // 对客卖点/主要特点,每行一条,专门给客户看的(和下面的costPrice/notes内部信息彻底分开,不会一起导出)
|
||||||
costPrice: '', notes: '',
|
costPrice: '', notes: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -1112,6 +1117,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-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-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; }
|
||||||
|
|
||||||
|
|
||||||
@media print {
|
@media print {
|
||||||
|
|
@ -2506,7 +2519,7 @@ function MainApp({ username, onLogout }) {
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
{productDraft ? (
|
{productDraft ? (
|
||||||
<ProductForm draft={productDraft} setDraft={setProductDraft} onSave={saveProductDraft} onCancel={cancelProductDraft} categories={productCategories} onManage={(mode) => setTaxManageMode(mode)} />
|
<ProductForm draft={productDraft} setDraft={setProductDraft} onSave={saveProductDraft} onCancel={cancelProductDraft} categories={productCategories} onManage={(mode) => setTaxManageMode(mode)} onNotify={notify} />
|
||||||
) : selectedProduct ? (
|
) : selectedProduct ? (
|
||||||
<ProductDetail
|
<ProductDetail
|
||||||
product={selectedProduct} onEdit={() => startEditProduct(selectedProduct)} onDelete={() => deleteProduct(selectedProduct.id)}
|
product={selectedProduct} onEdit={() => startEditProduct(selectedProduct)} onDelete={() => deleteProduct(selectedProduct.id)}
|
||||||
|
|
@ -3442,7 +3455,7 @@ function CustomerDetail({
|
||||||
|
|
||||||
/* ----------------------------- product form / detail ----------------------------- */
|
/* ----------------------------- product form / detail ----------------------------- */
|
||||||
|
|
||||||
function ProductForm({ draft, setDraft, onSave, onCancel, categories, onManage }) {
|
function ProductForm({ draft, setDraft, onSave, onCancel, categories, onManage, onNotify }) {
|
||||||
const set = (patch) => setDraft(d => ({ ...d, ...patch }));
|
const set = (patch) => setDraft(d => ({ ...d, ...patch }));
|
||||||
const cat = (categories || []).find(c => c.id === draft.category);
|
const cat = (categories || []).find(c => c.id === draft.category);
|
||||||
const brandOptions = cat ? cat.brands : [];
|
const brandOptions = cat ? cat.brands : [];
|
||||||
|
|
@ -3451,6 +3464,24 @@ function ProductForm({ draft, setDraft, onSave, onCancel, categories, onManage }
|
||||||
const isMiniUps = isUps && draft.type === '迷你UPS';
|
const isMiniUps = isUps && draft.type === '迷你UPS';
|
||||||
const maxKW = computeMaxPowerKW(draft.ratedPower, draft.ratedPowerUnit, draft.powerFactor);
|
const maxKW = computeMaxPowerKW(draft.ratedPower, draft.ratedPowerUnit, draft.powerFactor);
|
||||||
const extraNames = draft.extraNames || [];
|
const extraNames = draft.extraNames || [];
|
||||||
|
const [imageUploading, setImageUploading] = useState(false);
|
||||||
|
async function handleImagePick(file) {
|
||||||
|
if (!file) return;
|
||||||
|
setImageUploading(true);
|
||||||
|
try {
|
||||||
|
const res = await uploadFile(draft.id, file);
|
||||||
|
set({ productImageFileName: res.fileName, productImageOriginalName: res.originalName });
|
||||||
|
} catch (e) {
|
||||||
|
if (onNotify) onNotify('图片上传失败,请重试');
|
||||||
|
} finally {
|
||||||
|
setImageUploading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function handleImageRemove() {
|
||||||
|
if (!draft.productImageFileName) return;
|
||||||
|
try { await deleteFile(draft.id, draft.productImageFileName); } catch (e) {}
|
||||||
|
set({ productImageFileName: '', productImageOriginalName: '' });
|
||||||
|
}
|
||||||
function addExtraName() { set({ extraNames: [...extraNames, ''] }); }
|
function addExtraName() { set({ extraNames: [...extraNames, ''] }); }
|
||||||
function updateExtraName(idx, val) { const arr = [...extraNames]; arr[idx] = val; set({ extraNames: arr }); }
|
function updateExtraName(idx, val) { const arr = [...extraNames]; arr[idx] = val; set({ extraNames: arr }); }
|
||||||
function removeExtraName(idx) { set({ extraNames: extraNames.filter((_, i) => i !== idx) }); }
|
function removeExtraName(idx) { set({ extraNames: extraNames.filter((_, i) => i !== idx) }); }
|
||||||
|
|
@ -3646,8 +3677,45 @@ function ProductForm({ draft, setDraft, onSave, onCancel, categories, onManage }
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<div className="field">
|
||||||
|
<div className="field-label-row">
|
||||||
|
<span className="field-label">产品主图(可选,用于多语言PDF导出)</span>
|
||||||
|
</div>
|
||||||
|
{draft.productImageFileName ? (
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
||||||
|
<img
|
||||||
|
src={`/uploads/${draft.id}/${draft.productImageFileName}`} alt={draft.name}
|
||||||
|
style={{ width: 96, height: 96, objectFit: 'cover', borderRadius: 8, border: '1px solid var(--line)' }}
|
||||||
|
/>
|
||||||
|
<div style={{ fontSize: 12.5, color: 'var(--ink-soft)', flex: 1 }}>{draft.productImageOriginalName}</div>
|
||||||
|
<label className="btn btn-sm" style={{ cursor: imageUploading ? 'default' : 'pointer' }}>
|
||||||
|
{imageUploading ? '上传中…' : '替换'}
|
||||||
|
<input type="file" accept="image/*" style={{ display: 'none' }} disabled={imageUploading} onChange={e => handleImagePick(e.target.files && e.target.files[0])} />
|
||||||
|
</label>
|
||||||
|
<button className="btn btn-sm btn-ghost btn-danger" onClick={handleImageRemove}><Trash2 size={12} /></button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<label className="tax-add-category" style={{ display: 'block', cursor: imageUploading ? 'default' : 'pointer' }}>
|
||||||
|
<input type="file" accept="image/*" style={{ display: 'none' }} disabled={imageUploading} onChange={e => handleImagePick(e.target.files && e.target.files[0])} />
|
||||||
|
<ImageIcon size={16} style={{ verticalAlign: -3, marginRight: 4 }} />{imageUploading ? '上传中…' : '点击上传产品图片'}
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="field">
|
||||||
|
<span className="field-label">对客卖点 / 主要特点(可选,每行一条,会显示在多语言PDF导出的资料里)</span>
|
||||||
|
<textarea
|
||||||
|
className="textarea" style={{ fontFamily: 'inherit', fontSize: 13 }} rows={4}
|
||||||
|
value={draft.sellingPoints} onChange={e => set({ sellingPoints: e.target.value })}
|
||||||
|
placeholder={'如:\nHigh capacity provides longer back-up time\nSupports wide AC voltage range (100-240Vac)'}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="field"><span className="field-label">正本价格(元)</span><input className="input" value={draft.costPrice} onChange={e => set({ costPrice: e.target.value })} placeholder="采购/成本价,人民币" /></div>
|
<div className="field"><span className="field-label">正本价格(元)</span><input className="input" value={draft.costPrice} onChange={e => set({ costPrice: e.target.value })} placeholder="采购/成本价,人民币" /></div>
|
||||||
<div className="field"><span className="field-label">备注 / 卖点说明</span><textarea className="textarea" style={{ fontFamily: 'inherit', fontSize: 13 }} value={draft.notes} onChange={e => set({ notes: e.target.value })} /></div>
|
<div className="field">
|
||||||
|
<span className="field-label">内部备注(仅自己可见,不会出现在任何导出的文档里)</span>
|
||||||
|
<textarea className="textarea" style={{ fontFamily: 'inherit', fontSize: 13 }} value={draft.notes} onChange={e => set({ notes: e.target.value })} placeholder="成本、货期、库存这类内部信息写这里" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="modal-foot"><button className="btn" onClick={onCancel}>取消</button><button className="btn btn-primary" onClick={onSave}><Save size={13} />保存</button></div>
|
<div className="modal-foot"><button className="btn" onClick={onCancel}>取消</button><button className="btn btn-primary" onClick={onSave}><Save size={13} />保存</button></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -3701,28 +3769,65 @@ function ProductDetail({ product: p, onEdit, onDelete, categories, exportLang, o
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<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' }}>
|
||||||
<div style={{ marginBottom: 20 }}>
|
{(() => {
|
||||||
<div style={{ fontSize: 26, fontWeight: 800 }}>{p.name || '—'}</div>
|
const sellingPointLines = (p.sellingPoints || '').split('\n').map(l => l.trim()).filter(Boolean);
|
||||||
{p.brand && <div style={{ color: '#888', marginTop: 4 }}>{p.brand}</div>}
|
const specTable = (
|
||||||
</div>
|
<table className="doc-table spec-sheet-table">
|
||||||
<div className="spec-sheet-bar">{L('产品规格书')}</div>
|
<tbody>
|
||||||
<table className="doc-table spec-sheet-table">
|
<tr><td className="spec-label">{L('型号')}</td><td>{p.name || '—'}</td></tr>
|
||||||
<tbody>
|
{p.brand && <tr><td className="spec-label">{L('品牌')}</td><td>{p.brand}</td></tr>}
|
||||||
<tr><td className="spec-label">{L('型号')}</td><td>{p.name || '—'}</td></tr>
|
<tr><td className="spec-label">{L('产品大类')}</td><td>{cat ? L(cat.name) : '—'}</td></tr>
|
||||||
{p.brand && <tr><td className="spec-label">{L('品牌')}</td><td>{p.brand}</td></tr>}
|
{p.type && <tr><td className="spec-label">{L('细分品类')}</td><td>{L(p.type)}</td></tr>}
|
||||||
<tr><td className="spec-label">{L('产品大类')}</td><td>{cat ? L(cat.name) : '—'}</td></tr>
|
{showMachineVariant && <tr><td className="spec-label">{L('机型')}</td><td>{L(p.machineVariant)}</td></tr>}
|
||||||
{p.type && <tr><td className="spec-label">{L('细分品类')}</td><td>{L(p.type)}</td></tr>}
|
{exportRows.map((r, i) => (
|
||||||
{showMachineVariant && <tr><td className="spec-label">{L('机型')}</td><td>{L(p.machineVariant)}</td></tr>}
|
<tr key={i}>
|
||||||
{exportRows.map((r, i) => (
|
<td className="spec-label">{r.labelKey === '__port__' ? `${r.portType} ${L('输出')}` : L(r.labelKey)}</td>
|
||||||
<tr key={i}>
|
<td>{r.value}</td>
|
||||||
<td className="spec-label">{r.labelKey === '__port__' ? `${r.portType} ${L('输出')}` : L(r.labelKey)}</td>
|
</tr>
|
||||||
<td>{r.value}</td>
|
))}
|
||||||
</tr>
|
</tbody>
|
||||||
))}
|
</table>
|
||||||
{p.notes && p.notes.trim() && <tr><td className="spec-label">{L('备注')}</td><td>{L(p.notes.trim())}</td></tr>}
|
);
|
||||||
</tbody>
|
const disclaimer = <div style={{ marginTop: 18, fontSize: 11, color: '#999' }}>{L('* 产品规格如有变动,恕不另行通知')}</div>;
|
||||||
</table>
|
|
||||||
<div style={{ marginTop: 18, fontSize: 11, color: '#999' }}>{L('* 产品规格如有变动,恕不另行通知')}</div>
|
if (p.productImageFileName) {
|
||||||
|
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>}
|
||||||
|
{sellingPointLines.length > 0 && (
|
||||||
|
<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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<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>
|
||||||
|
{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}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue