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);
|
||||
});
|
||||
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>
|
||||
|
|
|
|||
Loading…
Reference in New Issue