Update App.jsx via upload script - 2026-08-24 09:30:33

This commit is contained in:
mike 2026-08-24 09:30:47 +08:00
parent 7084c2d8fe
commit c02aff38b3
1 changed files with 102 additions and 50 deletions

152
App.jsx
View File

@ -2087,7 +2087,7 @@ function MainApp({ username, onLogout }) {
const [regionFilter, setRegionFilter] = useState('all');
const [attachmentNote, setAttachmentNote] = useState('');
const [attachmentCategory, setAttachmentCategory] = useState(FILE_CATEGORIES[0]);
const [attachmentFile, setAttachmentFile] = useState(null);
const [attachmentFiles, setAttachmentFiles] = useState([]);
const [uploading, setUploading] = useState(false);
const [fileInputKey, setFileInputKey] = useState(0);
const [fileDragActive, setFileDragActive] = useState(false);
@ -2138,7 +2138,7 @@ function MainApp({ username, onLogout }) {
const [supplierDraft, setSupplierDraft] = useState(null);
const [supplierSearch, setSupplierSearch] = useState('');
const [supplierAttachmentNote, setSupplierAttachmentNote] = useState('');
const [supplierAttachmentFile, setSupplierAttachmentFile] = useState(null);
const [supplierAttachmentFiles, setSupplierAttachmentFiles] = useState([]);
const [supplierUploading, setSupplierUploading] = useState(false);
const [supplierFileInputKey, setSupplierFileInputKey] = useState(0);
const [supplierFileDragActive, setSupplierFileDragActive] = useState(false);
@ -2471,33 +2471,36 @@ function MainApp({ username, onLogout }) {
function handleFileDrop(e) {
e.preventDefault();
setFileDragActive(false);
const f = e.dataTransfer.files && e.dataTransfer.files[0];
if (f) setAttachmentFile(f);
const files = e.dataTransfer.files && Array.from(e.dataTransfer.files);
if (files && files.length) setAttachmentFiles(files);
}
async function handleUploadAttachment(customerId, folderId) {
if (!attachmentFile) { notify('请先选择文件'); return; }
if (!attachmentFiles.length) { notify('请先选择文件'); return; }
setUploading(true);
const failed = [];
try {
const res = await uploadFile(customerId, attachmentFile);
setCustomers(prev => prev.map(c => {
if (c.id !== customerId) return c;
const entry = {
fileName: res.fileName,
originalName: res.originalName,
category: attachmentCategory,
note: attachmentNote.trim(),
date: todayStr(),
folderId: folderId || null,
};
return { ...c, attachments: [entry, ...(c.attachments || [])] };
}));
setAttachmentFile(null);
for (const file of attachmentFiles) {
try {
const res = await uploadFile(customerId, file);
const entry = {
fileName: res.fileName,
originalName: res.originalName,
category: attachmentCategory,
note: attachmentNote.trim(),
date: todayStr(),
folderId: folderId || null,
};
setCustomers(prev => prev.map(c => (c.id === customerId ? { ...c, attachments: [entry, ...(c.attachments || [])] } : c)));
} catch (e) {
failed.push(`${file.name}${e.message}`);
}
}
setAttachmentFiles([]);
setAttachmentNote('');
setAttachmentCategory(FILE_CATEGORIES[0]);
setFileInputKey(k => k + 1);
notify('已上传');
} catch (e) {
notify('上传失败,请重试');
if (failed.length === 0) notify('已上传');
else notify(`${attachmentFiles.length - failed.length}个成功,失败:${failed.join('')}`);
} finally {
setUploading(false);
}
@ -2617,25 +2620,28 @@ function MainApp({ username, onLogout }) {
function handleSupplierFileDrop(e) {
e.preventDefault();
setSupplierFileDragActive(false);
const f = e.dataTransfer.files && e.dataTransfer.files[0];
if (f) setSupplierAttachmentFile(f);
const files = e.dataTransfer.files && Array.from(e.dataTransfer.files);
if (files && files.length) setSupplierAttachmentFiles(files);
}
async function handleUploadSupplierAttachment(supplierId) {
if (!supplierAttachmentFile) { notify('请先选择文件'); return; }
if (!supplierAttachmentFiles.length) { notify('请先选择文件'); return; }
setSupplierUploading(true);
const failed = [];
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);
for (const file of supplierAttachmentFiles) {
try {
const res = await uploadFile(supplierId, file);
const entry = { fileName: res.fileName, originalName: res.originalName, note: supplierAttachmentNote.trim(), date: todayStr() };
setSuppliers(prev => prev.map(s => (s.id === supplierId ? { ...s, attachments: [entry, ...(s.attachments || [])] } : s)));
} catch (e) {
failed.push(`${file.name}${e.message}`);
}
}
setSupplierAttachmentFiles([]);
setSupplierAttachmentNote('');
setSupplierFileInputKey(k => k + 1);
notify('已上传');
} catch (e) {
notify('上传失败,请重试');
if (failed.length === 0) notify('已上传');
else notify(`${supplierAttachmentFiles.length - failed.length}个成功,失败:${failed.join('')}`);
} finally {
setSupplierUploading(false);
}
@ -3344,8 +3350,8 @@ function MainApp({ username, onLogout }) {
setAttachmentNote={setAttachmentNote}
attachmentCategory={attachmentCategory}
setAttachmentCategory={setAttachmentCategory}
attachmentFileName={attachmentFile ? attachmentFile.name : ''}
onPickAttachment={(f) => setAttachmentFile(f)}
attachmentFileNames={attachmentFiles.map(f => f.name)}
onPickAttachment={(files) => setAttachmentFiles(files)}
uploading={uploading}
fileInputKey={fileInputKey}
onUploadAttachment={(folderId) => handleUploadAttachment(selected.id, folderId)}
@ -3707,8 +3713,8 @@ function MainApp({ username, onLogout }) {
onDelete={() => deleteSupplier(selectedSupplier.id)}
attachmentNote={supplierAttachmentNote}
setAttachmentNote={setSupplierAttachmentNote}
attachmentFileName={supplierAttachmentFile ? supplierAttachmentFile.name : ''}
onPickAttachment={(f) => setSupplierAttachmentFile(f)}
attachmentFileNames={supplierAttachmentFiles.map(f => f.name)}
onPickAttachment={(files) => setSupplierAttachmentFiles(files)}
uploading={supplierUploading}
fileInputKey={supplierFileInputKey}
onUploadAttachment={() => handleUploadSupplierAttachment(selectedSupplier.id)}
@ -4171,10 +4177,11 @@ function SupplierForm({ draft, setDraft, onSave, onCancel }) {
function SupplierDetail({
supplier: s, onEdit, onDelete,
attachmentNote, setAttachmentNote, attachmentFileName, onPickAttachment, uploading, fileInputKey,
attachmentNote, setAttachmentNote, attachmentFileNames, onPickAttachment, uploading, fileInputKey,
onUploadAttachment, onDeleteAttachment, fileDragActive, onFileDragOver, onFileDragLeave, onFileDrop,
}) {
const attachments = s.attachments || [];
const [previewFile, setPreviewFile] = useState(null); // { url, name }
return (
<div className="panel">
<div className="panel-header">
@ -4197,19 +4204,19 @@ function SupplierDetail({
{s.notes && <div className="field"><span className="field-label">备注</span><div style={{ fontSize: 12.5, whiteSpace: 'pre-wrap' }}>{s.notes}</div></div>}
<div className="field">
<span className="field-label">厂家资料PDF / 图片等</span>
<span className="field-label">厂家资料PDF / 图片等支持多选</span>
<div style={{ display: 'flex', gap: 8, alignItems: 'stretch', flexWrap: 'wrap', marginBottom: 8 }}>
<label
className={`dropzone ${fileDragActive ? 'drag-active' : ''}`}
onDragOver={onFileDragOver} onDragLeave={onFileDragLeave} onDrop={onFileDrop}
>
<input
key={fileInputKey} type="file"
onChange={e => onPickAttachment(e.target.files && e.target.files[0])}
key={fileInputKey} type="file" multiple
onChange={e => onPickAttachment(e.target.files ? Array.from(e.target.files) : [])}
style={{ display: 'none' }}
/>
<Paperclip size={15} style={{ marginBottom: 4 }} />
<div>{fileDragActive ? '松开鼠标完成选择' : attachmentFileName ? `已选择${attachmentFileName}` : '点击选择文件,或把文件拖到这里'}</div>
<div>{fileDragActive ? '松开鼠标完成选择' : attachmentFileNames.length ? `已选择 ${attachmentFileNames.length} 个文件${attachmentFileNames.join('、')}` : '点击选择文件(可多选),或把文件拖到这里'}</div>
</label>
<div style={{ display: 'flex', flexDirection: 'column', gap: 8, flex: '1 1 160px' }}>
<input
@ -4231,15 +4238,20 @@ function SupplierDetail({
<div style={{ flex: 1, minWidth: 0 }}>
<span className="crm-mono" style={{ fontSize: 11.5, color: 'var(--ink-soft)' }}>{a.date}</span>
<div style={{ fontSize: 12.5, wordBreak: 'break-word', marginTop: 2 }}>
<a href={`/uploads/${s.id}/${a.fileName}`} target="_blank" rel="noreferrer" download={a.originalName} style={{ color: 'var(--blue)' }}>{a.originalName}</a>
<span
onClick={() => setPreviewFile({ url: `/uploads/${s.id}/${a.fileName}`, name: a.originalName })}
style={{ color: 'var(--blue)', cursor: 'pointer' }}
>{a.originalName}</span>
{a.note && <span style={{ color: 'var(--ink-soft)' }}> {a.note}</span>}
</div>
</div>
<a className="btn btn-sm btn-ghost" href={`/uploads/${s.id}/${a.fileName}`} download={a.originalName} title="下载"><Download size={12} /></a>
<button className="btn btn-sm btn-ghost btn-danger" onClick={() => onDeleteAttachment(a.fileName)}><Trash2 size={12} /></button>
</div>
))}
</CollapsibleSection>
</div>
{previewFile && <FilePreviewModal url={previewFile.url} name={previewFile.name} onClose={() => setPreviewFile(null)} />}
</div>
);
}
@ -4350,6 +4362,40 @@ function ContactInfo({ icon, label, value, active, onToggle }) {
);
}
function isPreviewableImage(name) { return /\.(jpe?g|png|gif|webp|svg|bmp)$/i.test(name || ''); }
function isPreviewablePdf(name) { return /\.pdf$/i.test(name || ''); }
function FilePreviewModal({ url, name, onClose }) {
const isImage = isPreviewableImage(name);
const isPdf = isPreviewablePdf(name);
const wide = isImage || isPdf;
return (
<div className="modal-overlay" onClick={onClose}>
<div className="modal-box" style={{ maxWidth: wide ? 820 : 420, width: wide ? '90vw' : undefined }} onClick={e => e.stopPropagation()}>
<div className="modal-head">
<div className="panel-title" style={{ wordBreak: 'break-word', fontSize: 13.5 }}>{name}</div>
<div style={{ display: 'flex', gap: 8, flexShrink: 0 }}>
<a className="btn btn-sm" href={url} download={name} title="下载"><Download size={13} /></a>
<button className="btn btn-ghost btn-sm" onClick={onClose}><X size={14} /></button>
</div>
</div>
<div className="modal-body" style={{ padding: wide ? 0 : 18 }}>
{isImage ? (
<img src={url} alt={name} style={{ width: '100%', maxHeight: '78vh', objectFit: 'contain', display: 'block', background: 'var(--bg)' }} />
) : isPdf ? (
<iframe src={url} title={name} style={{ width: '100%', height: '78vh', border: 'none', display: 'block' }} />
) : (
<div style={{ textAlign: 'center', padding: '20px 10px' }}>
<FileText size={32} color="var(--ink-soft)" style={{ marginBottom: 10 }} />
<div style={{ fontSize: 13, color: 'var(--ink-soft)' }}>这个文件类型不支持在线预览点右上角下载按钮打开</div>
</div>
)}
</div>
</div>
</div>
);
}
function CollapsibleSection({ title, count, defaultOpen, children }) {
const [open, setOpen] = useState(!!defaultOpen);
return (
@ -4383,7 +4429,7 @@ function CustomerDetail({
customer: c, now, onEdit, onDelete,
onTogglePreferred, onToggleVip, onToggleOrdered,
attachmentNote, setAttachmentNote, attachmentCategory, setAttachmentCategory,
attachmentFileName, onPickAttachment, uploading, fileInputKey, onUploadAttachment, onDeleteAttachment, onMoveAttachment,
attachmentFileNames, onPickAttachment, uploading, fileInputKey, onUploadAttachment, onDeleteAttachment, onMoveAttachment,
onCreateFolder, onRenameFolder, onDeleteFolder,
fileDragActive, onFileDragOver, onFileDragLeave, onFileDrop,
}) {
@ -4392,6 +4438,7 @@ function CustomerDetail({
const [fileSearch, setFileSearch] = useState('');
const [fileCategoryFilter, setFileCategoryFilter] = useState('all');
const [currentFolderId, setCurrentFolderId] = useState(null);
const [previewFile, setPreviewFile] = useState(null); // { url, name }
const folders = c.fileFolders || [];
const attachments = c.attachments || [];
@ -4519,12 +4566,12 @@ function CustomerDetail({
onDragOver={onFileDragOver} onDragLeave={onFileDragLeave} onDrop={onFileDrop}
>
<input
key={fileInputKey} type="file"
onChange={e => onPickAttachment(e.target.files && e.target.files[0])}
key={fileInputKey} type="file" multiple
onChange={e => onPickAttachment(e.target.files ? Array.from(e.target.files) : [])}
style={{ display: 'none' }}
/>
<Paperclip size={15} style={{ marginBottom: 4 }} />
<div>{fileDragActive ? '松开鼠标完成选择' : attachmentFileName ? `已选择${attachmentFileName}` : '点击选择文件,或把文件拖到这里'}</div>
<div>{fileDragActive ? '松开鼠标完成选择' : attachmentFileNames.length ? `已选择 ${attachmentFileNames.length} 个文件${attachmentFileNames.join('、')}` : '点击选择文件(可多选),或把文件拖到这里'}</div>
</label>
<div style={{ display: 'flex', flexDirection: 'column', gap: 8, flex: '1 1 160px' }}>
<select className="select" value={attachmentCategory} onChange={e => setAttachmentCategory(e.target.value)}>
@ -4564,7 +4611,10 @@ function CustomerDetail({
<span className="crm-mono" style={{ fontSize: 11.5, color: 'var(--ink-soft)' }}>{a.date}</span>
{' '}<span className="badge badge-region">{a.category || 'Other'}</span>
<div style={{ fontSize: 12.5, wordBreak: 'break-word', marginTop: 2 }}>
<a href={`/uploads/${c.id}/${a.fileName}`} target="_blank" rel="noreferrer" style={{ color: 'var(--blue)' }}>{a.originalName}</a>
<span
onClick={() => setPreviewFile({ url: `/uploads/${c.id}/${a.fileName}`, name: a.originalName })}
style={{ color: 'var(--blue)', cursor: 'pointer' }}
>{a.originalName}</span>
{a.note && <span style={{ color: 'var(--ink-soft)' }}> {a.note}</span>}
</div>
</div>
@ -4577,6 +4627,7 @@ function CustomerDetail({
<option value="">根目录</option>
{folders.map(f => <option key={f.id} value={f.id}>{folderPathLabel(folders, f.id)}</option>)}
</select>
<a className="btn btn-sm btn-ghost" href={`/uploads/${c.id}/${a.fileName}`} download={a.originalName} title="下载"><Download size={12} /></a>
<button className="btn btn-sm btn-ghost btn-danger" onClick={() => onDeleteAttachment(a.fileName)}><Trash2 size={12} /></button>
</div>
))}
@ -4584,6 +4635,7 @@ function CustomerDetail({
)}
</CollapsibleSection>
</div>
{previewFile && <FilePreviewModal url={previewFile.url} name={previewFile.name} onClose={() => setPreviewFile(null)} />}
</div>
);
}