From fb9bf6dbe27673a0af132988a20a48a83e296c2e Mon Sep 17 00:00:00 2001 From: mike Date: Fri, 21 Aug 2026 17:24:08 +0800 Subject: [PATCH] Update App.jsx via upload script - 2026-08-21 17:23:49 --- App.jsx | 257 ++++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 252 insertions(+), 5 deletions(-) diff --git a/App.jsx b/App.jsx index 5916b6e..9049fb4 100644 --- a/App.jsx +++ b/App.jsx @@ -5,7 +5,7 @@ import { Building2, Globe, Phone, Save, RefreshCw, LayoutDashboard, Users, Package, Layers, Zap, Tag, Inbox, Paperclip, Menu, LogOut, GripVertical, BookOpen, FileText, FolderTree, Lock, Sparkles, Printer, Image as ImageIcon, - ShieldCheck, Settings2, Boxes, Download, Wifi, Radio, QrCode, Folder, FolderPlus + ShieldCheck, Settings2, Boxes, Download, Wifi, Radio, QrCode, Folder, FolderPlus, Factory } from 'lucide-react'; import { storageGet, storageSet, uploadFile, deleteFile, @@ -245,7 +245,7 @@ const COUNTRIES = [ const COUNTRY_BY_NAME = Object.fromEntries(COUNTRIES.map(c => [c.name, c])); -const STORAGE_KEYS = { customers: 'crm:customers', products: 'crm:products', 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 STORAGE_KEYS = { customers: 'crm:customers', products: 'crm:products', quotes: 'crm:quotes', sellerProfiles: 'crm:sellerProfiles', certifications: 'crm:certifications', productCategories: 'crm:productCategories', packingLists: 'crm:packingLists', catalogDocs: 'crm:catalogDocs', jmsLines: 'crm:jmsLines', backPanelTemplates: 'crm:backPanelTemplates', suppliers: 'crm:suppliers' }; const blankCustomer = () => ({ id: 'c_' + Date.now() + '_' + Math.random().toString(36).slice(2, 7), @@ -262,6 +262,13 @@ const blankCustomer = () => ({ fileFolders: [], // 虚拟文件夹:{id, name, parentId},parentId为null表示根目录下的文件夹 }); +const blankSupplier = () => ({ + id: 'sup_' + Date.now() + '_' + Math.random().toString(36).slice(2, 7), + name: '', contact: '', phone: '', email: '', wechatOrWhatsapp: '', address: '', website: '', + mainProducts: '', notes: '', + attachments: [], // 厂家资料:PDF/图片等,{fileName, originalName, note, date} +}); + const OUTPUT_PORT_TYPES = ['USB', 'DC', 'DC1', 'DC2', 'DC3', 'POE', 'Type-C', 'DC interface', 'POE interface']; const EXPORT_LANGUAGES = [ @@ -1424,7 +1431,12 @@ async function loadAll() { const bpt = await storageGet(STORAGE_KEYS.backPanelTemplates); if (bpt && bpt.value) backPanelTemplates = JSON.parse(bpt.value); } catch (e) {} - return { customers, products, quotes, sellerProfiles, certifications, productCategories, packingLists, catalogDocs, jmsLines, backPanelTemplates }; + let suppliers = []; + try { + const sup = await storageGet(STORAGE_KEYS.suppliers); + if (sup && sup.value) suppliers = JSON.parse(sup.value); + } catch (e) {} + return { customers, products, quotes, sellerProfiles, certifications, productCategories, packingLists, catalogDocs, jmsLines, backPanelTemplates, suppliers }; } /* -------------------------------- styles -------------------------------- */ @@ -1966,6 +1978,7 @@ const NAV_GROUPS = [ ]; const STANDALONE_NAV = [ { id: 'certs', label: '授权管理', icon: ShieldCheck }, + { id: 'suppliers', label: '供应商名录', icon: Factory }, { id: 'catalog', label: '产品目录', icon: BookOpen }, { id: 'quotes', label: '报价单', icon: FileText }, { id: 'packing', label: '箱单', icon: Boxes }, @@ -2119,6 +2132,17 @@ function MainApp({ username, onLogout }) { const [certUploading, setCertUploading] = useState(false); const [editingCertId, setEditingCertId] = useState(null); const [certBrandDraft, setCertBrandDraft] = useState(''); + + const [suppliers, setSuppliers] = useState([]); + const [selectedSupplierId, setSelectedSupplierId] = useState(null); + const [supplierDraft, setSupplierDraft] = useState(null); + const [supplierSearch, setSupplierSearch] = useState(''); + const [supplierAttachmentNote, setSupplierAttachmentNote] = useState(''); + const [supplierAttachmentFile, setSupplierAttachmentFile] = useState(null); + const [supplierUploading, setSupplierUploading] = useState(false); + const [supplierFileInputKey, setSupplierFileInputKey] = useState(0); + const [supplierFileDragActive, setSupplierFileDragActive] = useState(false); + const [quoteSubView, setQuoteSubView] = useState('list'); // 'list' | 'editor' const [quoteDraft, setQuoteDraft] = useState(null); const [quoteTypeFilter, setQuoteTypeFilter] = useState('all'); @@ -2161,6 +2185,7 @@ function MainApp({ username, onLogout }) { setCatalogDocs((data.catalogDocs || []).filter(d => d.fileName)); setJmsLines(data.jmsLines || []); setBackPanelTemplates(data.backPanelTemplates || []); + setSuppliers(data.suppliers || []); setLoaded(true); })(); }, []); @@ -2174,6 +2199,7 @@ function MainApp({ username, onLogout }) { 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]); + useEffect(() => { if (loaded) storageSet(STORAGE_KEYS.suppliers, JSON.stringify(suppliers)).catch(() => {}); }, [suppliers, loaded]); function notify(msg) { setToast(msg); setTimeout(() => setToast(''), 2200); } @@ -2186,6 +2212,7 @@ function MainApp({ username, onLogout }) { const selected = customers.find(c => c.id === selectedId) || null; const selectedProduct = products.find(p => p.id === selectedProductId) || null; + const selectedSupplier = suppliers.find(s => s.id === selectedSupplierId) || null; const [productExportLang, setProductExportLang] = useState('zh'); const [productExportLabels, setProductExportLabels] = useState({}); const [productExportLoading, setProductExportLoading] = useState(false); @@ -2234,6 +2261,19 @@ function MainApp({ username, onLogout }) { return [...list].sort((a, b) => categoryBreadcrumb(a, productCategories).localeCompare(categoryBreadcrumb(b, productCategories)) || (a.name || '').localeCompare(b.name || '')); }, [products, productSearch, productCategoryFilter, productCategories]); + const filteredSuppliers = useMemo(() => { + let list = suppliers; + if (supplierSearch.trim()) { + const q = supplierSearch.trim().toLowerCase(); + list = list.filter(s => + (s.name || '').toLowerCase().includes(q) || + (s.contact || '').toLowerCase().includes(q) || + (s.mainProducts || '').toLowerCase().includes(q) + ); + } + return [...list].sort((a, b) => (a.name || '').localeCompare(b.name || '')); + }, [suppliers, supplierSearch]); + const counts = useMemo(() => ({ total: customers.length, products: products.length, @@ -2552,6 +2592,62 @@ function MainApp({ username, onLogout }) { notify('已删除'); } + /* --------------------------------- 供应商名录 --------------------------------- */ + + function startNewSupplier() { setSupplierDraft(blankSupplier()); setSelectedSupplierId(null); } + function startEditSupplier(s) { setSupplierDraft({ ...s }); setSelectedSupplierId(null); } + function cancelSupplierDraft() { setSupplierDraft(null); } + function saveSupplierDraft(s) { + setSuppliers(prev => { + const exists = prev.some(x => x.id === s.id); + return exists ? prev.map(x => (x.id === s.id ? s : x)) : [s, ...prev]; + }); + setSupplierDraft(null); + setSelectedSupplierId(s.id); + notify('已保存'); + } + function deleteSupplier(id) { + if (!window.confirm('确定删除这家供应商?此操作不可恢复。')) return; + setSuppliers(prev => prev.filter(s => s.id !== id)); + if (selectedSupplierId === id) setSelectedSupplierId(null); + notify('已删除'); + } + function handleSupplierFileDragOver(e) { e.preventDefault(); setSupplierFileDragActive(true); } + function handleSupplierFileDragLeave() { setSupplierFileDragActive(false); } + function handleSupplierFileDrop(e) { + e.preventDefault(); + setSupplierFileDragActive(false); + const f = e.dataTransfer.files && e.dataTransfer.files[0]; + if (f) setSupplierAttachmentFile(f); + } + async function handleUploadSupplierAttachment(supplierId) { + if (!supplierAttachmentFile) { notify('请先选择文件'); return; } + setSupplierUploading(true); + try { + const res = await uploadFile(supplierId, supplierAttachmentFile); + setSuppliers(prev => prev.map(s => { + if (s.id !== supplierId) return s; + const entry = { fileName: res.fileName, originalName: res.originalName, note: supplierAttachmentNote.trim(), date: todayStr() }; + return { ...s, attachments: [entry, ...(s.attachments || [])] }; + })); + setSupplierAttachmentFile(null); + setSupplierAttachmentNote(''); + setSupplierFileInputKey(k => k + 1); + notify('已上传'); + } catch (e) { + notify('上传失败,请重试'); + } finally { + setSupplierUploading(false); + } + } + async function handleDeleteSupplierAttachment(supplierId, fileName) { + if (!window.confirm('确定删除这个文件?此操作不可恢复。')) return; + try { await deleteFile(supplierId, fileName); } catch (e) {} + setSuppliers(prev => prev.map(s => ( + s.id === supplierId ? { ...s, attachments: (s.attachments || []).filter(a => a.fileName !== fileName) } : s + ))); + } + /* --------------------------------- Just My Socks --------------------------------- */ function saveJmsLine(line, editingId) { @@ -3579,6 +3675,56 @@ function MainApp({ username, onLogout }) { )} + {view === 'suppliers' && ( +
+
+
setSupplierSearch(e.target.value)} />
+ +
+
+ {filteredSuppliers.length === 0 ? ( +
暂无供应商,点击上方按钮添加第一个
+ ) : filteredSuppliers.map(s => ( +
{ setSupplierDraft(null); setSelectedSupplierId(s.id); }}> +
+
{s.name || '(未命名)'}
+
{s.contact && `${s.contact} · `}{s.mainProducts || '—'}
+
+ +
+ ))} +
+
+
+ +
+ {supplierDraft ? ( + + ) : selectedSupplier ? ( + startEditSupplier(selectedSupplier)} + onDelete={() => deleteSupplier(selectedSupplier.id)} + attachmentNote={supplierAttachmentNote} + setAttachmentNote={setSupplierAttachmentNote} + attachmentFileName={supplierAttachmentFile ? supplierAttachmentFile.name : ''} + onPickAttachment={(f) => setSupplierAttachmentFile(f)} + uploading={supplierUploading} + fileInputKey={supplierFileInputKey} + onUploadAttachment={() => handleUploadSupplierAttachment(selectedSupplier.id)} + onDeleteAttachment={(fileName) => handleDeleteSupplierAttachment(selectedSupplier.id, fileName)} + fileDragActive={supplierFileDragActive} + onFileDragOver={handleSupplierFileDragOver} + onFileDragLeave={handleSupplierFileDragLeave} + onFileDrop={handleSupplierFileDrop} + /> + ) : ( +
选择左侧供应商查看详情,或新建一个供应商档案
+ )} +
+
+ )} + {view === 'quotes' && quoteSubView === 'list' && ( <>
@@ -3761,10 +3907,10 @@ function MainApp({ username, onLogout }) {
e.stopPropagation()}>
数据备份(JSON)
-