Update App.jsx via upload script - 2026-07-26 22:07:53

This commit is contained in:
mike 2026-07-26 22:08:17 +08:00
parent b140948f4d
commit b9bde850c0
1 changed files with 131 additions and 26 deletions

157
App.jsx
View File

@ -411,7 +411,10 @@ function gatherExportTerms(p, categories) {
else terms.add(r.labelKey);
});
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);
}
@ -430,6 +433,8 @@ const blankProduct = () => ({
batterySpec: '', // UPS "5000mAh*4"
dimensions: '', // UPS(mm) "210*130*40"
netWeight: '', // UPS(kg)
productImageFileName: '', productImageOriginalName: '', // PDF
sellingPoints: '', // /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 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-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 {
@ -2506,7 +2519,7 @@ function MainApp({ username, onLogout }) {
<div>
{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 ? (
<ProductDetail
product={selectedProduct} onEdit={() => startEditProduct(selectedProduct)} onDelete={() => deleteProduct(selectedProduct.id)}
@ -3442,7 +3455,7 @@ function CustomerDetail({
/* ----------------------------- 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 cat = (categories || []).find(c => c.id === draft.category);
const brandOptions = cat ? cat.brands : [];
@ -3451,6 +3464,24 @@ function ProductForm({ draft, setDraft, onSave, onCancel, categories, onManage }
const isMiniUps = isUps && draft.type === '迷你UPS';
const maxKW = computeMaxPowerKW(draft.ratedPower, draft.ratedPowerUnit, draft.powerFactor);
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 updateExtraName(idx, val) { const arr = [...extraNames]; arr[idx] = val; set({ extraNames: arr }); }
function removeExtraName(idx) { set({ extraNames: extraNames.filter((_, i) => i !== idx) }); }
@ -3646,8 +3677,45 @@ function ProductForm({ draft, setDraft, onSave, onCancel, categories, onManage }
</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><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 className="modal-foot"><button className="btn" onClick={onCancel}>取消</button><button className="btn btn-primary" onClick={onSave}><Save size={13} />保存</button></div>
</div>
@ -3701,28 +3769,65 @@ function ProductDetail({ product: p, onEdit, onDelete, categories, exportLang, o
</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>
{(() => {
const sellingPointLines = (p.sellingPoints || '').split('\n').map(l => l.trim()).filter(Boolean);
const specTable = (
<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>
))}
</tbody>
</table>
);
const disclaimer = <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>