Update App.jsx via upload script - 2026-08-24 09:30:33
This commit is contained in:
parent
7084c2d8fe
commit
c02aff38b3
152
App.jsx
152
App.jsx
|
|
@ -2087,7 +2087,7 @@ function MainApp({ username, onLogout }) {
|
||||||
const [regionFilter, setRegionFilter] = useState('all');
|
const [regionFilter, setRegionFilter] = useState('all');
|
||||||
const [attachmentNote, setAttachmentNote] = useState('');
|
const [attachmentNote, setAttachmentNote] = useState('');
|
||||||
const [attachmentCategory, setAttachmentCategory] = useState(FILE_CATEGORIES[0]);
|
const [attachmentCategory, setAttachmentCategory] = useState(FILE_CATEGORIES[0]);
|
||||||
const [attachmentFile, setAttachmentFile] = useState(null);
|
const [attachmentFiles, setAttachmentFiles] = useState([]);
|
||||||
const [uploading, setUploading] = useState(false);
|
const [uploading, setUploading] = useState(false);
|
||||||
const [fileInputKey, setFileInputKey] = useState(0);
|
const [fileInputKey, setFileInputKey] = useState(0);
|
||||||
const [fileDragActive, setFileDragActive] = useState(false);
|
const [fileDragActive, setFileDragActive] = useState(false);
|
||||||
|
|
@ -2138,7 +2138,7 @@ function MainApp({ username, onLogout }) {
|
||||||
const [supplierDraft, setSupplierDraft] = useState(null);
|
const [supplierDraft, setSupplierDraft] = useState(null);
|
||||||
const [supplierSearch, setSupplierSearch] = useState('');
|
const [supplierSearch, setSupplierSearch] = useState('');
|
||||||
const [supplierAttachmentNote, setSupplierAttachmentNote] = useState('');
|
const [supplierAttachmentNote, setSupplierAttachmentNote] = useState('');
|
||||||
const [supplierAttachmentFile, setSupplierAttachmentFile] = useState(null);
|
const [supplierAttachmentFiles, setSupplierAttachmentFiles] = useState([]);
|
||||||
const [supplierUploading, setSupplierUploading] = useState(false);
|
const [supplierUploading, setSupplierUploading] = useState(false);
|
||||||
const [supplierFileInputKey, setSupplierFileInputKey] = useState(0);
|
const [supplierFileInputKey, setSupplierFileInputKey] = useState(0);
|
||||||
const [supplierFileDragActive, setSupplierFileDragActive] = useState(false);
|
const [supplierFileDragActive, setSupplierFileDragActive] = useState(false);
|
||||||
|
|
@ -2471,33 +2471,36 @@ function MainApp({ username, onLogout }) {
|
||||||
function handleFileDrop(e) {
|
function handleFileDrop(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setFileDragActive(false);
|
setFileDragActive(false);
|
||||||
const f = e.dataTransfer.files && e.dataTransfer.files[0];
|
const files = e.dataTransfer.files && Array.from(e.dataTransfer.files);
|
||||||
if (f) setAttachmentFile(f);
|
if (files && files.length) setAttachmentFiles(files);
|
||||||
}
|
}
|
||||||
async function handleUploadAttachment(customerId, folderId) {
|
async function handleUploadAttachment(customerId, folderId) {
|
||||||
if (!attachmentFile) { notify('请先选择文件'); return; }
|
if (!attachmentFiles.length) { notify('请先选择文件'); return; }
|
||||||
setUploading(true);
|
setUploading(true);
|
||||||
|
const failed = [];
|
||||||
try {
|
try {
|
||||||
const res = await uploadFile(customerId, attachmentFile);
|
for (const file of attachmentFiles) {
|
||||||
setCustomers(prev => prev.map(c => {
|
try {
|
||||||
if (c.id !== customerId) return c;
|
const res = await uploadFile(customerId, file);
|
||||||
const entry = {
|
const entry = {
|
||||||
fileName: res.fileName,
|
fileName: res.fileName,
|
||||||
originalName: res.originalName,
|
originalName: res.originalName,
|
||||||
category: attachmentCategory,
|
category: attachmentCategory,
|
||||||
note: attachmentNote.trim(),
|
note: attachmentNote.trim(),
|
||||||
date: todayStr(),
|
date: todayStr(),
|
||||||
folderId: folderId || null,
|
folderId: folderId || null,
|
||||||
};
|
};
|
||||||
return { ...c, attachments: [entry, ...(c.attachments || [])] };
|
setCustomers(prev => prev.map(c => (c.id === customerId ? { ...c, attachments: [entry, ...(c.attachments || [])] } : c)));
|
||||||
}));
|
} catch (e) {
|
||||||
setAttachmentFile(null);
|
failed.push(`${file.name}:${e.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setAttachmentFiles([]);
|
||||||
setAttachmentNote('');
|
setAttachmentNote('');
|
||||||
setAttachmentCategory(FILE_CATEGORIES[0]);
|
setAttachmentCategory(FILE_CATEGORIES[0]);
|
||||||
setFileInputKey(k => k + 1);
|
setFileInputKey(k => k + 1);
|
||||||
notify('已上传');
|
if (failed.length === 0) notify('已上传');
|
||||||
} catch (e) {
|
else notify(`${attachmentFiles.length - failed.length}个成功,失败:${failed.join(';')}`);
|
||||||
notify('上传失败,请重试');
|
|
||||||
} finally {
|
} finally {
|
||||||
setUploading(false);
|
setUploading(false);
|
||||||
}
|
}
|
||||||
|
|
@ -2617,25 +2620,28 @@ function MainApp({ username, onLogout }) {
|
||||||
function handleSupplierFileDrop(e) {
|
function handleSupplierFileDrop(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setSupplierFileDragActive(false);
|
setSupplierFileDragActive(false);
|
||||||
const f = e.dataTransfer.files && e.dataTransfer.files[0];
|
const files = e.dataTransfer.files && Array.from(e.dataTransfer.files);
|
||||||
if (f) setSupplierAttachmentFile(f);
|
if (files && files.length) setSupplierAttachmentFiles(files);
|
||||||
}
|
}
|
||||||
async function handleUploadSupplierAttachment(supplierId) {
|
async function handleUploadSupplierAttachment(supplierId) {
|
||||||
if (!supplierAttachmentFile) { notify('请先选择文件'); return; }
|
if (!supplierAttachmentFiles.length) { notify('请先选择文件'); return; }
|
||||||
setSupplierUploading(true);
|
setSupplierUploading(true);
|
||||||
|
const failed = [];
|
||||||
try {
|
try {
|
||||||
const res = await uploadFile(supplierId, supplierAttachmentFile);
|
for (const file of supplierAttachmentFiles) {
|
||||||
setSuppliers(prev => prev.map(s => {
|
try {
|
||||||
if (s.id !== supplierId) return s;
|
const res = await uploadFile(supplierId, file);
|
||||||
const entry = { fileName: res.fileName, originalName: res.originalName, note: supplierAttachmentNote.trim(), date: todayStr() };
|
const entry = { fileName: res.fileName, originalName: res.originalName, note: supplierAttachmentNote.trim(), date: todayStr() };
|
||||||
return { ...s, attachments: [entry, ...(s.attachments || [])] };
|
setSuppliers(prev => prev.map(s => (s.id === supplierId ? { ...s, attachments: [entry, ...(s.attachments || [])] } : s)));
|
||||||
}));
|
} catch (e) {
|
||||||
setSupplierAttachmentFile(null);
|
failed.push(`${file.name}:${e.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setSupplierAttachmentFiles([]);
|
||||||
setSupplierAttachmentNote('');
|
setSupplierAttachmentNote('');
|
||||||
setSupplierFileInputKey(k => k + 1);
|
setSupplierFileInputKey(k => k + 1);
|
||||||
notify('已上传');
|
if (failed.length === 0) notify('已上传');
|
||||||
} catch (e) {
|
else notify(`${supplierAttachmentFiles.length - failed.length}个成功,失败:${failed.join(';')}`);
|
||||||
notify('上传失败,请重试');
|
|
||||||
} finally {
|
} finally {
|
||||||
setSupplierUploading(false);
|
setSupplierUploading(false);
|
||||||
}
|
}
|
||||||
|
|
@ -3344,8 +3350,8 @@ function MainApp({ username, onLogout }) {
|
||||||
setAttachmentNote={setAttachmentNote}
|
setAttachmentNote={setAttachmentNote}
|
||||||
attachmentCategory={attachmentCategory}
|
attachmentCategory={attachmentCategory}
|
||||||
setAttachmentCategory={setAttachmentCategory}
|
setAttachmentCategory={setAttachmentCategory}
|
||||||
attachmentFileName={attachmentFile ? attachmentFile.name : ''}
|
attachmentFileNames={attachmentFiles.map(f => f.name)}
|
||||||
onPickAttachment={(f) => setAttachmentFile(f)}
|
onPickAttachment={(files) => setAttachmentFiles(files)}
|
||||||
uploading={uploading}
|
uploading={uploading}
|
||||||
fileInputKey={fileInputKey}
|
fileInputKey={fileInputKey}
|
||||||
onUploadAttachment={(folderId) => handleUploadAttachment(selected.id, folderId)}
|
onUploadAttachment={(folderId) => handleUploadAttachment(selected.id, folderId)}
|
||||||
|
|
@ -3707,8 +3713,8 @@ function MainApp({ username, onLogout }) {
|
||||||
onDelete={() => deleteSupplier(selectedSupplier.id)}
|
onDelete={() => deleteSupplier(selectedSupplier.id)}
|
||||||
attachmentNote={supplierAttachmentNote}
|
attachmentNote={supplierAttachmentNote}
|
||||||
setAttachmentNote={setSupplierAttachmentNote}
|
setAttachmentNote={setSupplierAttachmentNote}
|
||||||
attachmentFileName={supplierAttachmentFile ? supplierAttachmentFile.name : ''}
|
attachmentFileNames={supplierAttachmentFiles.map(f => f.name)}
|
||||||
onPickAttachment={(f) => setSupplierAttachmentFile(f)}
|
onPickAttachment={(files) => setSupplierAttachmentFiles(files)}
|
||||||
uploading={supplierUploading}
|
uploading={supplierUploading}
|
||||||
fileInputKey={supplierFileInputKey}
|
fileInputKey={supplierFileInputKey}
|
||||||
onUploadAttachment={() => handleUploadSupplierAttachment(selectedSupplier.id)}
|
onUploadAttachment={() => handleUploadSupplierAttachment(selectedSupplier.id)}
|
||||||
|
|
@ -4171,10 +4177,11 @@ function SupplierForm({ draft, setDraft, onSave, onCancel }) {
|
||||||
|
|
||||||
function SupplierDetail({
|
function SupplierDetail({
|
||||||
supplier: s, onEdit, onDelete,
|
supplier: s, onEdit, onDelete,
|
||||||
attachmentNote, setAttachmentNote, attachmentFileName, onPickAttachment, uploading, fileInputKey,
|
attachmentNote, setAttachmentNote, attachmentFileNames, onPickAttachment, uploading, fileInputKey,
|
||||||
onUploadAttachment, onDeleteAttachment, fileDragActive, onFileDragOver, onFileDragLeave, onFileDrop,
|
onUploadAttachment, onDeleteAttachment, fileDragActive, onFileDragOver, onFileDragLeave, onFileDrop,
|
||||||
}) {
|
}) {
|
||||||
const attachments = s.attachments || [];
|
const attachments = s.attachments || [];
|
||||||
|
const [previewFile, setPreviewFile] = useState(null); // { url, name }
|
||||||
return (
|
return (
|
||||||
<div className="panel">
|
<div className="panel">
|
||||||
<div className="panel-header">
|
<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>}
|
{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">
|
<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 }}>
|
<div style={{ display: 'flex', gap: 8, alignItems: 'stretch', flexWrap: 'wrap', marginBottom: 8 }}>
|
||||||
<label
|
<label
|
||||||
className={`dropzone ${fileDragActive ? 'drag-active' : ''}`}
|
className={`dropzone ${fileDragActive ? 'drag-active' : ''}`}
|
||||||
onDragOver={onFileDragOver} onDragLeave={onFileDragLeave} onDrop={onFileDrop}
|
onDragOver={onFileDragOver} onDragLeave={onFileDragLeave} onDrop={onFileDrop}
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
key={fileInputKey} type="file"
|
key={fileInputKey} type="file" multiple
|
||||||
onChange={e => onPickAttachment(e.target.files && e.target.files[0])}
|
onChange={e => onPickAttachment(e.target.files ? Array.from(e.target.files) : [])}
|
||||||
style={{ display: 'none' }}
|
style={{ display: 'none' }}
|
||||||
/>
|
/>
|
||||||
<Paperclip size={15} style={{ marginBottom: 4 }} />
|
<Paperclip size={15} style={{ marginBottom: 4 }} />
|
||||||
<div>{fileDragActive ? '松开鼠标完成选择' : attachmentFileName ? `已选择:${attachmentFileName}` : '点击选择文件,或把文件拖到这里'}</div>
|
<div>{fileDragActive ? '松开鼠标完成选择' : attachmentFileNames.length ? `已选择 ${attachmentFileNames.length} 个文件:${attachmentFileNames.join('、')}` : '点击选择文件(可多选),或把文件拖到这里'}</div>
|
||||||
</label>
|
</label>
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8, flex: '1 1 160px' }}>
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 8, flex: '1 1 160px' }}>
|
||||||
<input
|
<input
|
||||||
|
|
@ -4231,15 +4238,20 @@ function SupplierDetail({
|
||||||
<div style={{ flex: 1, minWidth: 0 }}>
|
<div style={{ flex: 1, minWidth: 0 }}>
|
||||||
<span className="crm-mono" style={{ fontSize: 11.5, color: 'var(--ink-soft)' }}>{a.date}</span>
|
<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 }}>
|
<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>}
|
{a.note && <span style={{ color: 'var(--ink-soft)' }}> — {a.note}</span>}
|
||||||
</div>
|
</div>
|
||||||
</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>
|
<button className="btn btn-sm btn-ghost btn-danger" onClick={() => onDeleteAttachment(a.fileName)}><Trash2 size={12} /></button>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</CollapsibleSection>
|
</CollapsibleSection>
|
||||||
</div>
|
</div>
|
||||||
|
{previewFile && <FilePreviewModal url={previewFile.url} name={previewFile.name} onClose={() => setPreviewFile(null)} />}
|
||||||
</div>
|
</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 }) {
|
function CollapsibleSection({ title, count, defaultOpen, children }) {
|
||||||
const [open, setOpen] = useState(!!defaultOpen);
|
const [open, setOpen] = useState(!!defaultOpen);
|
||||||
return (
|
return (
|
||||||
|
|
@ -4383,7 +4429,7 @@ function CustomerDetail({
|
||||||
customer: c, now, onEdit, onDelete,
|
customer: c, now, onEdit, onDelete,
|
||||||
onTogglePreferred, onToggleVip, onToggleOrdered,
|
onTogglePreferred, onToggleVip, onToggleOrdered,
|
||||||
attachmentNote, setAttachmentNote, attachmentCategory, setAttachmentCategory,
|
attachmentNote, setAttachmentNote, attachmentCategory, setAttachmentCategory,
|
||||||
attachmentFileName, onPickAttachment, uploading, fileInputKey, onUploadAttachment, onDeleteAttachment, onMoveAttachment,
|
attachmentFileNames, onPickAttachment, uploading, fileInputKey, onUploadAttachment, onDeleteAttachment, onMoveAttachment,
|
||||||
onCreateFolder, onRenameFolder, onDeleteFolder,
|
onCreateFolder, onRenameFolder, onDeleteFolder,
|
||||||
fileDragActive, onFileDragOver, onFileDragLeave, onFileDrop,
|
fileDragActive, onFileDragOver, onFileDragLeave, onFileDrop,
|
||||||
}) {
|
}) {
|
||||||
|
|
@ -4392,6 +4438,7 @@ function CustomerDetail({
|
||||||
const [fileSearch, setFileSearch] = useState('');
|
const [fileSearch, setFileSearch] = useState('');
|
||||||
const [fileCategoryFilter, setFileCategoryFilter] = useState('all');
|
const [fileCategoryFilter, setFileCategoryFilter] = useState('all');
|
||||||
const [currentFolderId, setCurrentFolderId] = useState(null);
|
const [currentFolderId, setCurrentFolderId] = useState(null);
|
||||||
|
const [previewFile, setPreviewFile] = useState(null); // { url, name }
|
||||||
|
|
||||||
const folders = c.fileFolders || [];
|
const folders = c.fileFolders || [];
|
||||||
const attachments = c.attachments || [];
|
const attachments = c.attachments || [];
|
||||||
|
|
@ -4519,12 +4566,12 @@ function CustomerDetail({
|
||||||
onDragOver={onFileDragOver} onDragLeave={onFileDragLeave} onDrop={onFileDrop}
|
onDragOver={onFileDragOver} onDragLeave={onFileDragLeave} onDrop={onFileDrop}
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
key={fileInputKey} type="file"
|
key={fileInputKey} type="file" multiple
|
||||||
onChange={e => onPickAttachment(e.target.files && e.target.files[0])}
|
onChange={e => onPickAttachment(e.target.files ? Array.from(e.target.files) : [])}
|
||||||
style={{ display: 'none' }}
|
style={{ display: 'none' }}
|
||||||
/>
|
/>
|
||||||
<Paperclip size={15} style={{ marginBottom: 4 }} />
|
<Paperclip size={15} style={{ marginBottom: 4 }} />
|
||||||
<div>{fileDragActive ? '松开鼠标完成选择' : attachmentFileName ? `已选择:${attachmentFileName}` : '点击选择文件,或把文件拖到这里'}</div>
|
<div>{fileDragActive ? '松开鼠标完成选择' : attachmentFileNames.length ? `已选择 ${attachmentFileNames.length} 个文件:${attachmentFileNames.join('、')}` : '点击选择文件(可多选),或把文件拖到这里'}</div>
|
||||||
</label>
|
</label>
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8, flex: '1 1 160px' }}>
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 8, flex: '1 1 160px' }}>
|
||||||
<select className="select" value={attachmentCategory} onChange={e => setAttachmentCategory(e.target.value)}>
|
<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="crm-mono" style={{ fontSize: 11.5, color: 'var(--ink-soft)' }}>{a.date}</span>
|
||||||
{' '}<span className="badge badge-region">{a.category || 'Other'}</span>
|
{' '}<span className="badge badge-region">{a.category || 'Other'}</span>
|
||||||
<div style={{ fontSize: 12.5, wordBreak: 'break-word', marginTop: 2 }}>
|
<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>}
|
{a.note && <span style={{ color: 'var(--ink-soft)' }}> — {a.note}</span>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -4577,6 +4627,7 @@ function CustomerDetail({
|
||||||
<option value="">根目录</option>
|
<option value="">根目录</option>
|
||||||
{folders.map(f => <option key={f.id} value={f.id}>{folderPathLabel(folders, f.id)}</option>)}
|
{folders.map(f => <option key={f.id} value={f.id}>{folderPathLabel(folders, f.id)}</option>)}
|
||||||
</select>
|
</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>
|
<button className="btn btn-sm btn-ghost btn-danger" onClick={() => onDeleteAttachment(a.fileName)}><Trash2 size={12} /></button>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
@ -4584,6 +4635,7 @@ function CustomerDetail({
|
||||||
)}
|
)}
|
||||||
</CollapsibleSection>
|
</CollapsibleSection>
|
||||||
</div>
|
</div>
|
||||||
|
{previewFile && <FilePreviewModal url={previewFile.url} name={previewFile.name} onClose={() => setPreviewFile(null)} />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue