From 8ef989ade4ff45b34841c4955c79c9748a304640 Mon Sep 17 00:00:00 2001 From: mike Date: Fri, 7 Aug 2026 17:53:37 +0800 Subject: [PATCH] Update App.jsx via upload script - 2026-08-07 17:53:15 --- App.jsx | 391 +++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 386 insertions(+), 5 deletions(-) diff --git a/App.jsx b/App.jsx index 2f2037c..3e1eed5 100644 --- a/App.jsx +++ b/App.jsx @@ -248,7 +248,7 @@ const COUNTRIES = [ const COUNTRY_BY_NAME = Object.fromEntries(COUNTRIES.map(c => [c.name, c])); -const STORAGE_KEYS = { customers: 'crm:customers', products: 'crm:products', settings: 'crm:settings', quotes: 'crm:quotes', sellerProfiles: 'crm:sellerProfiles', certifications: 'crm:certifications', productCategories: 'crm:productCategories', packingLists: 'crm:packingLists', catalogDocs: 'crm:catalogDocs', jmsLines: 'crm:jmsLines' }; +const STORAGE_KEYS = { customers: 'crm:customers', products: 'crm:products', settings: 'crm:settings', quotes: 'crm:quotes', sellerProfiles: 'crm:sellerProfiles', certifications: 'crm:certifications', productCategories: 'crm:productCategories', packingLists: 'crm:packingLists', catalogDocs: 'crm:catalogDocs', jmsLines: 'crm:jmsLines', backPanelTemplates: 'crm:backPanelTemplates' }; const blankCustomer = () => ({ id: 'c_' + Date.now() + '_' + Math.random().toString(36).slice(2, 7), @@ -278,6 +278,135 @@ const EXPORT_LANGUAGES = [ // 多语言PDF导出用的内置专业翻译词典:固定字段名 + 已知的品类/机型名称,人工核对过的翻译, // 不依赖AI、不占用DeepSeek额度、永远一致准确。词典里没有的词(比如用户自己新建的品类/品牌名、 // 或者产品备注这种自由文本)才会走AI翻译并缓存(见 resolveExportTranslations)。 +/* ---------------------------- 后板图图标库 ---------------------------- */ +// 后板图编辑器用到的固定图标集:基础元件(交流输入口/保险丝/输出插座/USB/RJ45/风扇) +// + 常见插座类型(可选配插座变体)。每个图标是一段简约线条风格的SVG,viewBox统一 +// 0 0 40 40,用currentColor描边,方便跟着容器的文字颜色走、也方便被选中时变色高亮。 +const BACK_PANEL_ICON_LIBRARY = [ + { type: 'ac_input', label: '交流输入口', group: '基础元件' }, + { type: 'fuse_breaker', label: '保险丝/断路器', group: '基础元件' }, + { type: 'output_generic', label: '输出插座(通用)', group: '基础元件' }, + { type: 'usb', label: 'USB', group: '基础元件' }, + { type: 'rj45', label: 'RJ45', group: '基础元件' }, + { type: 'fan', label: '风扇', group: '基础元件' }, + { type: 'socket_cn', label: '国标插座', group: '可选配插座' }, + { type: 'socket_schuko', label: '德标插座(Schuko)', group: '可选配插座' }, + { type: 'socket_uk', label: '英标插座(BS1363)', group: '可选配插座' }, + { type: 'socket_us', label: '美标插座(NEMA)', group: '可选配插座' }, + { type: 'socket_au', label: '澳标插座', group: '可选配插座' }, + { type: 'socket_za', label: '南非/印度插座', group: '可选配插座' }, + { type: 'socket_it', label: '意标插座', group: '可选配插座' }, + { type: 'socket_iec', label: 'IEC C13/C14', group: '可选配插座' }, +]; +const BACK_PANEL_ICON_LABELS = BACK_PANEL_ICON_LIBRARY.reduce((m, i) => ({ ...m, [i.type]: i.label }), {}); + +function BackPanelIconSVG({ type, size = 40 }) { + const stroke = { stroke: 'currentColor', strokeWidth: 2, fill: 'none' }; + const body = (() => { + switch (type) { + case 'ac_input': + return (<> + + + + + ); + case 'fuse_breaker': + return (<> + + + + ); + case 'output_generic': + return (<> + + + + ); + case 'usb': + return (<> + + + + ); + case 'rj45': + return (<> + + + {[0, 1, 2, 3, 4, 5].map(i => ())} + ); + case 'fan': + return (<> + + {[0, 90, 180, 270].map(a => ( + + ))} + + ); + case 'socket_cn': + return (<> + + + + + ); + case 'socket_schuko': + return (<> + + + + + + ); + case 'socket_uk': + return (<> + + + + + ); + case 'socket_us': + return (<> + + + + + ); + case 'socket_au': + return (<> + + + + + ); + case 'socket_za': + return (<> + + + + + ); + case 'socket_it': + return (<> + + + + + ); + case 'socket_iec': + return (<> + + + + + ); + default: + return ; + } + })(); + return {body}; +} + const TRANSLATION_DICTIONARY = { // 文档通用 '产品规格书': { en: 'Specifications', fr: 'Spécifications', ru: 'Технические характеристики', es: 'Especificaciones', ar: 'المواصفات' }, @@ -802,6 +931,7 @@ const blankProduct = () => ({ applicationScenarios: '', // 应用场景文本,如 "电脑、投影仪、打印机、摄像机、收银机" container20ft: '', container40ft: '', container40hq: '', // 装箱数量(20尺柜/40尺柜/40高柜,无栈板台数) extraSpecs: [], // 动态兜底参数:AI识别到但不在上面任何具名字段里的参数,原样保留 {label, value},如 [{label:'浮充电压', value:'13.7VDC/节'}] + backPanelTemplateId: '', // 引用的后板图模板id(可跨产品共用同一个模板),迷你UPS不适用 productImageFileName: '', productImageOriginalName: '', // 产品主图,导出多语言PDF时展示 sellingPoints: '', // 对客卖点/主要特点,每行一条,专门给客户看的(和下面的costPrice/notes内部信息彻底分开,不会一起导出) costPrice: '', notes: '', @@ -1322,7 +1452,12 @@ async function loadAll() { const jl = await storageGet(STORAGE_KEYS.jmsLines); if (jl && jl.value) jmsLines = JSON.parse(jl.value); } catch (e) {} - return { customers, products, settings, quotes, sellerProfiles, certifications, productCategories, packingLists, catalogDocs, jmsLines }; + let backPanelTemplates = []; + try { + const bpt = await storageGet(STORAGE_KEYS.backPanelTemplates); + if (bpt && bpt.value) backPanelTemplates = JSON.parse(bpt.value); + } catch (e) {} + return { customers, products, settings, quotes, sellerProfiles, certifications, productCategories, packingLists, catalogDocs, jmsLines, backPanelTemplates }; } /* -------------------------------- styles -------------------------------- */ @@ -1816,6 +1951,7 @@ const NAV_GROUPS = [ { label: '产品', items: [ { id: 'groups', label: '产品分组', icon: FolderTree }, { id: 'products', label: '产品管理', icon: Package }, + { id: 'backpanel', label: '后板图模板', icon: Settings2 }, ] }, ]; const STANDALONE_NAV = [ @@ -1997,6 +2133,7 @@ function MainApp({ username, onLogout }) { const [catalogEditDraft, setCatalogEditDraft] = useState({ title: '', category: '', brand: '', type: '' }); const [catalogCategoryFilter, setCatalogCategoryFilter] = useState('all'); const [jmsLines, setJmsLines] = useState([]); + const [backPanelTemplates, setBackPanelTemplates] = useState([]); const [telegramConfigHasToken, setTelegramConfigHasToken] = useState(false); const [telegramConfigChatId, setTelegramConfigChatId] = useState(''); @@ -2023,6 +2160,7 @@ function MainApp({ username, onLogout }) { setPackingLists(data.packingLists); setCatalogDocs((data.catalogDocs || []).filter(d => d.fileName)); setJmsLines(data.jmsLines || []); + setBackPanelTemplates(data.backPanelTemplates || []); setLoaded(true); })(); }, []); @@ -2042,6 +2180,7 @@ function MainApp({ username, onLogout }) { useEffect(() => { if (loaded) storageSet(STORAGE_KEYS.packingLists, JSON.stringify(packingLists)).catch(() => {}); }, [packingLists, loaded]); useEffect(() => { if (loaded) storageSet(STORAGE_KEYS.catalogDocs, JSON.stringify(catalogDocs)).catch(() => {}); }, [catalogDocs, loaded]); useEffect(() => { if (loaded) storageSet(STORAGE_KEYS.jmsLines, JSON.stringify(jmsLines)).catch(() => {}); }, [jmsLines, loaded]); + useEffect(() => { if (loaded) storageSet(STORAGE_KEYS.backPanelTemplates, JSON.stringify(backPanelTemplates)).catch(() => {}); }, [backPanelTemplates, loaded]); function notify(msg) { setToast(msg); setTimeout(() => setToast(''), 2200); } @@ -3311,7 +3450,7 @@ function MainApp({ username, onLogout }) {
{productDraft ? ( - setTaxManageMode(mode)} onNotify={notify} /> + setTaxManageMode(mode)} onNotify={notify} backPanelTemplates={backPanelTemplates} /> ) : selectedProduct ? ( startEditProduct(selectedProduct)} onDelete={() => deleteProduct(selectedProduct.id)} @@ -3319,6 +3458,7 @@ function MainApp({ username, onLogout }) { exportLang={productExportLang} onExportLangChange={setProductExportLang} exportLabels={productExportLabels} exportLoading={productExportLoading} onExportPdf={handleExportProductPdf} exportPdfBusy={productExportPdfBusy} + backPanelTemplates={backPanelTemplates} /> ) : (
选择左侧产品查看详情,或新建一个产品档案
@@ -3327,6 +3467,10 @@ function MainApp({ username, onLogout }) {
)} + {view === 'backpanel' && ( + + )} + {view === 'catalog' && (
t.id === editingId) || null; + + function updateEditing(patch) { + setTemplates(prev => prev.map(t => (t.id === editingId ? { ...t, ...patch } : t))); + } + function addElement(type, x, y) { + const el = { id: 'el_' + Date.now() + '_' + Math.random().toString(36).slice(2, 6), type, x, y, size: 40, rotation: 0 }; + updateEditing({ elements: [...(editing.elements || []), el] }); + setSelectedElId(el.id); + } + function updateElement(id, patch) { + updateEditing({ elements: (editing.elements || []).map(el => (el.id === id ? { ...el, ...patch } : el)) }); + } + function removeElement(id) { + updateEditing({ elements: (editing.elements || []).filter(el => el.id !== id) }); + if (selectedElId === id) setSelectedElId(null); + } + + function handleCanvasDrop(e) { + e.preventDefault(); + const type = e.dataTransfer.getData('text/plain'); + if (!type) return; + const rect = e.currentTarget.getBoundingClientRect(); + const x = Math.round(e.clientX - rect.left); + const y = Math.round(e.clientY - rect.top); + addElement(type, x, y); + } + + function handleElementPointerDown(e, el) { + e.stopPropagation(); + setSelectedElId(el.id); + const canvasEl = e.currentTarget.closest('.backpanel-canvas'); + const canvasRect = canvasEl.getBoundingClientRect(); + const startOffsetX = e.clientX - canvasRect.left - el.x; + const startOffsetY = e.clientY - canvasRect.top - el.y; + e.currentTarget.setPointerCapture(e.pointerId); + const onMove = (moveEvt) => { + const nx = Math.round(moveEvt.clientX - canvasRect.left - startOffsetX); + const ny = Math.round(moveEvt.clientY - canvasRect.top - startOffsetY); + updateElement(el.id, { x: nx, y: ny }); + }; + const onUp = (upEvt) => { + e.currentTarget.releasePointerCapture(e.pointerId); + e.currentTarget.removeEventListener('pointermove', onMove); + e.currentTarget.removeEventListener('pointerup', onUp); + }; + e.currentTarget.addEventListener('pointermove', onMove); + e.currentTarget.addEventListener('pointerup', onUp); + } + + const selectedEl = editing && (editing.elements || []).find(el => el.id === selectedElId); + + // ---------- 模板列表视图 ---------- + if (!editing) { + return ( +
+
+
后板图模板
+ +
+
+ {(!templates || templates.length === 0) ? ( +
还没有后板图模板,点击右上角新建一个
+ ) : templates.map(t => ( +
{ setEditingId(t.id); setSelectedElId(null); }}> +
+
+
{t.name || '(未命名模板)'}
+
{(t.elements || []).length} 个元素
+
+ +
+ ))} +
+
+ ); + } + + // ---------- 编辑画布视图 ---------- + return ( +
+
+ updateEditing({ name: e.target.value })} + /> +
+ +
+
+
+
+
拖拽图标到右侧画布
+ {['基础元件', '可选配插座'].map(group => ( +
+
{group}
+
+ {BACK_PANEL_ICON_LIBRARY.filter(i => i.group === group).map(icon => ( +
e.dataTransfer.setData('text/plain', icon.type)} + title={icon.label} + style={{ + border: '1px solid var(--line)', borderRadius: 8, padding: 6, cursor: 'grab', + display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 3, background: 'var(--panel)', + }} + > + + {icon.label} +
+ ))} +
+
+ ))} +
+ +
+
e.preventDefault()} + onDrop={handleCanvasDrop} + onPointerDown={() => setSelectedElId(null)} + style={{ + position: 'relative', width: editing.width, height: editing.height, maxWidth: '100%', + background: '#3a3f47', borderRadius: 8, border: '1px solid var(--line)', overflow: 'hidden', + }} + > + {(editing.elements || []).map(el => ( +
handleElementPointerDown(e, el)} + title={BACK_PANEL_ICON_LABELS[el.type] || el.type} + style={{ + position: 'absolute', left: el.x, top: el.y, cursor: 'grab', + transform: `translate(-50%, -50%) rotate(${el.rotation || 0}deg)`, + color: selectedElId === el.id ? 'var(--blue)' : '#e8eaed', + outline: selectedElId === el.id ? '2px dashed var(--blue)' : 'none', outlineOffset: 3, borderRadius: 4, + }} + > + +
+ ))} + {(editing.elements || []).length === 0 && ( +
+ 这里代表机身后板轮廓,把左边的图标拖进来开始摆放 +
+ )} +
+ + {selectedEl && ( +
+ {BACK_PANEL_ICON_LABELS[selectedEl.type] || selectedEl.type} + + + + +
+ )} +
+
+
+ ); +} + +// 只读渲染(产品详情页/PDF导出用):同一套图标定义,不带交互,纯展示 +function BackPanelReadOnly({ template }) { + if (!template) return null; + return ( +
+ {(template.elements || []).map(el => ( +
+ +
+ ))} +
+ ); +} + +function ProductForm({ draft, setDraft, onSave, onCancel, categories, onManage, onNotify, backPanelTemplates }) { const set = (patch) => setDraft(d => ({ ...d, ...patch })); const cat = (categories || []).find(c => c.id === draft.category); const brandOptions = cat ? cat.brands : []; @@ -5068,6 +5432,16 @@ function ProductForm({ draft, setDraft, onSave, onCancel, categories, onManage,
)} + {isUps && !isMiniUps && ( +
+ 后板图模板(可选,跨产品共用同一个布局) + +
+ )} +
产品主图(可选,用于多语言PDF导出) @@ -5156,13 +5530,14 @@ function ProductForm({ draft, setDraft, onSave, onCancel, categories, onManage, ); } -function ProductDetail({ product: p, onEdit, onDelete, categories, exportLang, onExportLangChange, exportLabels, exportLoading, onExportPdf, exportPdfBusy }) { +function ProductDetail({ product: p, onEdit, onDelete, categories, exportLang, onExportLangChange, exportLabels, exportLoading, onExportPdf, exportPdfBusy, backPanelTemplates }) { const cat = (categories || []).find(c => c.id === p.category); const showMachineVariant = isUpsCategory(cat) && p.machineVariant; const L = (key) => (exportLabels && exportLabels[key]) || key; const V = (value) => (containsChinese(value) ? L(value) : value); const langInfo = EXPORT_LANGUAGES.find(l => l.code === exportLang) || EXPORT_LANGUAGES[0]; const exportRows = productExportRows(p, categories); + const backPanelTemplate = (backPanelTemplates || []).find(t => t.id === p.backPanelTemplateId); return ( <>
@@ -5184,6 +5559,12 @@ function ProductDetail({ product: p, onEdit, onDelete, categories, exportLang, o } label="电压" value={p.voltage || '—'} /> } label="正本价格(元)" value={p.costPrice || '—'} />
+ {backPanelTemplate && ( +
+ 后板图({backPanelTemplate.name}) + +
+ )} {p.notes &&
备注
{p.notes}
}