diff --git a/App.jsx b/App.jsx index 771be6c..fec93e9 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, Crown, Sparkles, Printer, Image as ImageIcon, - ShieldCheck, Settings2, Boxes + ShieldCheck, Settings2, Boxes, Download } from 'lucide-react'; import { storageGet, storageSet, uploadFile, deleteFile, @@ -835,6 +835,30 @@ const Styles = () => ( .cert-card-brand-text { font-size: 13px; font-weight: 700; cursor: pointer; word-break: break-word; } .cert-card-brand-text.empty { font-weight: 400; color: var(--ink-soft); } .cert-card-brand input { text-align: center; font-weight: 700; } + + /* ---------- catalog docs (PDF gallery: upload progress, actions, dnd reorder) ---------- */ + .catalog-doc-actions { position: absolute; top: 6px; right: 6px; display: flex; gap: 4px; } + .catalog-doc-actions .btn { padding: 5px 6px; background: rgba(255,255,255,0.92); border-color: var(--line); } + .catalog-doc-link { + display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 10px; + width: 100%; height: 100%; padding: 18px; text-align: center; color: inherit; text-decoration: none; + } + .catalog-doc-filename { font-size: 11px; color: var(--ink-soft); word-break: break-word; } + .catalog-doc-uploading { + display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 10px; + width: 100%; height: 100%; color: var(--ink-soft); font-size: 12px; padding: 18px; text-align: center; + } + .catalog-doc-spinner { + width: 26px; height: 26px; border-radius: 50%; border: 3px solid var(--line); border-top-color: var(--copper); + animation: catalog-doc-spin 0.8s linear infinite; + } + @keyframes catalog-doc-spin { to { transform: rotate(360deg); } } + .catalog-dropzone { + border-radius: 12px; transition: background 0.15s, outline 0.15s; outline: 2px dashed transparent; outline-offset: 6px; + } + .catalog-dropzone.drag-active { outline-color: var(--blue); background: #F0F6FA; } + .catalog-doc-card { cursor: grab; } + .catalog-doc-card.dragging { opacity: 0.5; } .field-label-row { display: flex; align-items: center; justify-content: space-between; } .field-manage-link { font-size: 11.5px; font-weight: 600; color: var(--blue); cursor: pointer; @@ -1161,7 +1185,8 @@ function MainApp({ username, onLogout }) { const [packingDraft, setPackingDraft] = useState(null); const [packingPdfExporting, setPackingPdfExporting] = useState(false); const [catalogDocs, setCatalogDocs] = useState([]); - const [catalogUploading, setCatalogUploading] = useState(false); + const [catalogDragActive, setCatalogDragActive] = useState(false); + const [dragCatalogDocIdx, setDragCatalogDocIdx] = useState(null); const [editingCatalogId, setEditingCatalogId] = useState(null); const [catalogTitleDraft, setCatalogTitleDraft] = useState(''); @@ -1186,7 +1211,7 @@ function MainApp({ username, onLogout }) { setSellerProfiles(data.sellerProfiles); setCertifications(data.certifications); setPackingLists(data.packingLists); - setCatalogDocs(data.catalogDocs); + setCatalogDocs((data.catalogDocs || []).filter(d => d.fileName)); setLoaded(true); })(); }, []); @@ -1258,23 +1283,34 @@ function MainApp({ username, onLogout }) { } async function handleAddCatalogFile(file) { if (!file) return; - setCatalogUploading(true); + const tempId = 'catdoc_' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6); + // 先插入一张"上传中"的占位卡片,让用户能实时看到这个文件正在传,传完再替换成真实数据 + setCatalogDocs(prev => [{ id: tempId, title: '', fileName: '', originalName: file.name, uploading: true }, ...prev]); try { - const id = 'catdoc_' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6); - const res = await uploadFile(id, file); - const entry = { id, title: '', fileName: res.fileName, originalName: res.originalName }; - setCatalogDocs(prev => [entry, ...prev]); - setEditingCatalogId(id); + const res = await uploadFile(tempId, file); + setCatalogDocs(prev => prev.map(d => (d.id === tempId ? { ...d, fileName: res.fileName, originalName: res.originalName, uploading: false } : d))); + setEditingCatalogId(tempId); setCatalogTitleDraft(''); } catch (e) { - notify('上传失败,请重试'); - } finally { - setCatalogUploading(false); + notify(`《${file.name}》上传失败,请重试`); + setCatalogDocs(prev => prev.filter(d => d.id !== tempId)); } } function handleCatalogFilesSelected(files) { Array.from(files || []).forEach(f => handleAddCatalogFile(f)); } + function handleCatalogDragOver(e) { e.preventDefault(); setCatalogDragActive(true); } + function handleCatalogDragLeave(e) { + if (e.currentTarget.contains(e.relatedTarget)) return; + setCatalogDragActive(false); + } + function handleCatalogDrop(e) { + e.preventDefault(); + setCatalogDragActive(false); + const files = Array.from((e.dataTransfer && e.dataTransfer.files) || []).filter(f => f.type === 'application/pdf' || /\.pdf$/i.test(f.name)); + if (files.length === 0) { notify('只能上传PDF文件'); return; } + handleCatalogFilesSelected(files); + } function startEditCatalogTitle(doc) { setEditingCatalogId(doc.id); setCatalogTitleDraft(doc.title || ''); @@ -1290,6 +1326,11 @@ function MainApp({ username, onLogout }) { setCatalogDocs(prev => prev.filter(d => d.id !== doc.id)); if (editingCatalogId === doc.id) setEditingCatalogId(null); } + function handleCatalogDocDrop(dropIdx) { + if (dragCatalogDocIdx === null || dragCatalogDocIdx === dropIdx) { setDragCatalogDocIdx(null); return; } + setCatalogDocs(prev => reorderArray(prev, dragCatalogDocIdx, dropIdx)); + setDragCatalogDocIdx(null); + } function handleGroupDrop(category, dropIdx) { if (!dragGroupState || dragGroupState.category !== category || dragGroupState.index === dropIdx) { setDragGroupState(null); return; } setProducts(prev => reorderWithinCategory(prev, category, dragGroupState.index, dropIdx)); @@ -2128,9 +2169,12 @@ function MainApp({ username, onLogout }) { )} {view === 'catalog' && ( - <> +