Update App.jsx via upload script - 2026-07-22 17:33:42
This commit is contained in:
parent
9806f23008
commit
257ec1f11d
196
App.jsx
196
App.jsx
|
|
@ -837,19 +837,21 @@ const Styles = () => (
|
|||
.cert-card-brand input { text-align: center; font-weight: 700; }
|
||||
|
||||
/* ---------- catalog docs (PDF gallery: upload progress, actions, dnd reorder) ---------- */
|
||||
.catalog-card-grid { grid-template-columns: repeat(auto-fill, minmax(170px, 1fr)); }
|
||||
.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-img-wrap { aspect-ratio: unset; height: 92px; }
|
||||
.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;
|
||||
display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 6px;
|
||||
width: 100%; height: 100%; padding: 10px; text-align: center; color: inherit; text-decoration: none;
|
||||
}
|
||||
.catalog-doc-filename { font-size: 11px; color: var(--ink-soft); word-break: break-word; }
|
||||
.catalog-doc-filename { font-size: 10.5px; color: var(--ink-soft); word-break: break-word; line-height: 1.3; }
|
||||
.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;
|
||||
display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 6px;
|
||||
width: 100%; height: 100%; color: var(--ink-soft); font-size: 11px; padding: 10px; text-align: center;
|
||||
}
|
||||
.catalog-doc-spinner {
|
||||
width: 26px; height: 26px; border-radius: 50%; border: 3px solid var(--line); border-top-color: var(--copper);
|
||||
width: 20px; height: 20px; 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); } }
|
||||
|
|
@ -859,6 +861,11 @@ const Styles = () => (
|
|||
.catalog-dropzone.drag-active { outline: 2px dashed var(--blue); outline-offset: 6px; background: #F0F6FA; }
|
||||
.catalog-doc-card { cursor: grab; }
|
||||
.catalog-doc-card.dragging { opacity: 0.5; }
|
||||
.catalog-doc-meta { font-size: 10.5px; color: var(--ink-soft); margin-top: 3px; text-align: center; word-break: break-word; }
|
||||
.catalog-doc-edit-fields { display: flex; flex-direction: column; gap: 6px; }
|
||||
.catalog-doc-edit-fields select { font-size: 11.5px; padding: 5px 8px; }
|
||||
.catalog-filter-row { display: flex; align-items: center; gap: 10px; margin-bottom: 14px; flex-wrap: wrap; }
|
||||
.catalog-filter-row .select { width: auto; min-width: 160px; }
|
||||
.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;
|
||||
|
|
@ -895,6 +902,7 @@ const Styles = () => (
|
|||
color: var(--ink-soft); font-size: 13px; cursor: pointer; transition: border-color 0.15s, color 0.15s;
|
||||
}
|
||||
.tax-add-category:hover { border-color: var(--blue); color: var(--blue); }
|
||||
.catalog-view-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 14px; flex-wrap: wrap; gap: 10px; }
|
||||
.cert-view-head { display: flex; align-items: center; justify-content: flex-end; margin-bottom: 14px; }
|
||||
.cert-add-btn {
|
||||
width: 36px; height: 36px; border-radius: 50%; padding: 0;
|
||||
|
|
@ -1188,7 +1196,8 @@ function MainApp({ username, onLogout }) {
|
|||
const [catalogDragActive, setCatalogDragActive] = useState(false);
|
||||
const [dragCatalogDocIdx, setDragCatalogDocIdx] = useState(null);
|
||||
const [editingCatalogId, setEditingCatalogId] = useState(null);
|
||||
const [catalogTitleDraft, setCatalogTitleDraft] = useState('');
|
||||
const [catalogEditDraft, setCatalogEditDraft] = useState({ title: '', category: '', brand: '', type: '' });
|
||||
const [catalogCategoryFilter, setCatalogCategoryFilter] = useState('all');
|
||||
|
||||
useEffect(() => {
|
||||
getAiConfig().then((res) => setAiConfigHasKey(!!res.hasKey)).catch(() => {});
|
||||
|
|
@ -1276,6 +1285,11 @@ function MainApp({ username, onLogout }) {
|
|||
|
||||
const priceHistory = useMemo(() => buildPriceHistory(quotes), [quotes]);
|
||||
|
||||
const filteredCatalogDocs = useMemo(() => {
|
||||
if (catalogCategoryFilter === 'all') return catalogDocs;
|
||||
return catalogDocs.filter(d => d.uploading || d.category === catalogCategoryFilter);
|
||||
}, [catalogDocs, catalogCategoryFilter]);
|
||||
|
||||
function handleCustomerDrop(dropIdx) {
|
||||
if (dragCustomerIdx === null || dragCustomerIdx === dropIdx) { setDragCustomerIdx(null); return; }
|
||||
setCustomers(prev => reorderArray(prev, dragCustomerIdx, dropIdx));
|
||||
|
|
@ -1285,12 +1299,12 @@ function MainApp({ username, onLogout }) {
|
|||
if (!file) return;
|
||||
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]);
|
||||
setCatalogDocs(prev => [{ id: tempId, title: '', fileName: '', originalName: file.name, category: '', brand: '', type: '', uploading: true }, ...prev]);
|
||||
try {
|
||||
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('');
|
||||
setCatalogEditDraft({ title: '', category: '', brand: '', type: '' });
|
||||
} catch (e) {
|
||||
notify(`《${file.name}》上传失败,请重试`);
|
||||
setCatalogDocs(prev => prev.filter(d => d.id !== tempId));
|
||||
|
|
@ -1311,14 +1325,19 @@ function MainApp({ username, onLogout }) {
|
|||
if (files.length === 0) { notify('只能上传PDF文件'); return; }
|
||||
handleCatalogFilesSelected(files);
|
||||
}
|
||||
function startEditCatalogTitle(doc) {
|
||||
function startEditCatalogDoc(doc) {
|
||||
setEditingCatalogId(doc.id);
|
||||
setCatalogTitleDraft(doc.title || '');
|
||||
setCatalogEditDraft({ title: doc.title || '', category: doc.category || '', brand: doc.brand || '', type: doc.type || '' });
|
||||
}
|
||||
function saveCatalogTitle() {
|
||||
setCatalogDocs(prev => prev.map(d => (d.id === editingCatalogId ? { ...d, title: catalogTitleDraft.trim() } : d)));
|
||||
function updateCatalogEditDraft(patch) {
|
||||
setCatalogEditDraft(d => ({ ...d, ...patch }));
|
||||
}
|
||||
function saveCatalogEdit() {
|
||||
setCatalogDocs(prev => prev.map(d => (d.id === editingCatalogId
|
||||
? { ...d, title: catalogEditDraft.title.trim(), category: catalogEditDraft.category, brand: catalogEditDraft.brand, type: catalogEditDraft.type }
|
||||
: d)));
|
||||
setEditingCatalogId(null);
|
||||
setCatalogTitleDraft('');
|
||||
setCatalogEditDraft({ title: '', category: '', brand: '', type: '' });
|
||||
}
|
||||
async function deleteCatalogDoc(doc) {
|
||||
if (!window.confirm('确定删除这份产品目录PDF?此操作不可恢复。')) return;
|
||||
|
|
@ -2173,7 +2192,13 @@ function MainApp({ username, onLogout }) {
|
|||
className={`catalog-dropzone ${catalogDragActive ? 'drag-active' : ''}`}
|
||||
onDragOver={handleCatalogDragOver} onDragLeave={handleCatalogDragLeave} onDrop={handleCatalogDrop}
|
||||
>
|
||||
<div className="cert-view-head">
|
||||
<div className="catalog-view-head">
|
||||
<div className="catalog-filter-row" style={{ marginBottom: 0 }}>
|
||||
<select className="select" value={catalogCategoryFilter} onChange={e => setCatalogCategoryFilter(e.target.value)}>
|
||||
<option value="all">全部类别</option>
|
||||
{productCategories.map(c => <option key={c.id} value={c.id}>{c.name}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
<label className="btn btn-primary cert-add-btn" title="上传产品目录PDF(也可以把PDF直接拖到本页任意位置)">
|
||||
<input
|
||||
type="file" accept="application/pdf" multiple style={{ display: 'none' }}
|
||||
|
|
@ -2184,63 +2209,98 @@ function MainApp({ username, onLogout }) {
|
|||
</div>
|
||||
{catalogDocs.length === 0 ? (
|
||||
<div className="panel"><div className="empty-state"><BookOpen size={28} />暂无产品目录,点击右上角"+"或把PDF拖到这里上传第一份样本</div></div>
|
||||
) : filteredCatalogDocs.length === 0 ? (
|
||||
<div className="panel"><div className="empty-state"><BookOpen size={28} />这个类别下还没有产品目录</div></div>
|
||||
) : (
|
||||
<div className="card-grid">
|
||||
{catalogDocs.map((doc, idx) => (
|
||||
<div
|
||||
key={doc.id}
|
||||
className={`cert-card catalog-doc-card ${dragCatalogDocIdx === idx ? 'dragging' : ''}`}
|
||||
draggable={!doc.uploading}
|
||||
onDragStart={(e) => { e.stopPropagation(); setDragCatalogDocIdx(idx); }}
|
||||
onDragOver={(e) => { e.preventDefault(); e.stopPropagation(); }}
|
||||
onDrop={(e) => { e.stopPropagation(); handleCatalogDocDrop(idx); }}
|
||||
onDragEnd={() => setDragCatalogDocIdx(null)}
|
||||
>
|
||||
<div className="cert-card-img-wrap">
|
||||
{doc.uploading ? (
|
||||
<div className="catalog-doc-uploading">
|
||||
<div className="catalog-doc-spinner" />
|
||||
<div>正在上传…</div>
|
||||
<div className="catalog-doc-filename">{doc.originalName}</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<a href={`/uploads/${doc.id}/${doc.fileName}`} target="_blank" rel="noreferrer" className="catalog-doc-link">
|
||||
<FileText size={40} color="var(--copper)" />
|
||||
<div className="card-grid catalog-card-grid">
|
||||
{filteredCatalogDocs.map((doc, idx) => {
|
||||
const isEditing = editingCatalogId === doc.id;
|
||||
const activeCategoryId = isEditing ? catalogEditDraft.category : doc.category;
|
||||
const cat = productCategories.find(c => c.id === activeCategoryId);
|
||||
const brandOptions = cat ? cat.brands : [];
|
||||
const typeOptions = cat ? cat.types : [];
|
||||
return (
|
||||
<div
|
||||
key={doc.id}
|
||||
className={`cert-card catalog-doc-card ${dragCatalogDocIdx === idx ? 'dragging' : ''}`}
|
||||
draggable={!doc.uploading}
|
||||
onDragStart={(e) => { e.stopPropagation(); setDragCatalogDocIdx(idx); }}
|
||||
onDragOver={(e) => { e.preventDefault(); e.stopPropagation(); }}
|
||||
onDrop={(e) => { e.stopPropagation(); handleCatalogDocDrop(idx); }}
|
||||
onDragEnd={() => setDragCatalogDocIdx(null)}
|
||||
>
|
||||
<div className="cert-card-img-wrap catalog-doc-img-wrap">
|
||||
{doc.uploading ? (
|
||||
<div className="catalog-doc-uploading">
|
||||
<div className="catalog-doc-spinner" />
|
||||
<div>正在上传…</div>
|
||||
<div className="catalog-doc-filename">{doc.originalName}</div>
|
||||
</a>
|
||||
<div className="catalog-doc-actions">
|
||||
<a className="btn btn-sm btn-ghost" title="下载" href={`/uploads/${doc.id}/${doc.fileName}`} download={doc.originalName}>
|
||||
<Download size={12} />
|
||||
</a>
|
||||
<button className="btn btn-sm btn-ghost btn-danger" title="删除" onClick={() => deleteCatalogDoc(doc)}>
|
||||
<Trash2 size={12} />
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
) : (
|
||||
<>
|
||||
<a href={`/uploads/${doc.id}/${doc.fileName}`} target="_blank" rel="noreferrer" className="catalog-doc-link">
|
||||
<FileText size={22} color="var(--copper)" />
|
||||
<div className="catalog-doc-filename">{doc.originalName}</div>
|
||||
</a>
|
||||
<div className="catalog-doc-actions">
|
||||
<a className="btn btn-sm btn-ghost" title="下载" href={`/uploads/${doc.id}/${doc.fileName}`} download={doc.originalName}>
|
||||
<Download size={12} />
|
||||
</a>
|
||||
<button className="btn btn-sm btn-ghost btn-danger" title="删除" onClick={() => deleteCatalogDoc(doc)}>
|
||||
<Trash2 size={12} />
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="cert-card-brand">
|
||||
{isEditing ? (
|
||||
<div className="catalog-doc-edit-fields">
|
||||
<input
|
||||
className="input" autoFocus
|
||||
value={catalogEditDraft.title}
|
||||
onChange={e => updateCatalogEditDraft({ title: e.target.value })}
|
||||
onKeyDown={e => { if (e.key === 'Enter') saveCatalogEdit(); }}
|
||||
placeholder="目录名称,如:SKE产品样本"
|
||||
/>
|
||||
<select
|
||||
className="select"
|
||||
value={catalogEditDraft.category}
|
||||
onChange={e => updateCatalogEditDraft({ category: e.target.value, brand: '', type: '' })}
|
||||
>
|
||||
<option value="">不选择大类</option>
|
||||
{productCategories.map(c => <option key={c.id} value={c.id}>{c.name}</option>)}
|
||||
</select>
|
||||
{catalogEditDraft.category && (
|
||||
<>
|
||||
<select className="select" value={catalogEditDraft.brand} onChange={e => updateCatalogEditDraft({ brand: e.target.value })}>
|
||||
<option value="">不选择品牌</option>
|
||||
{brandOptions.map(b => <option key={b} value={b}>{b}</option>)}
|
||||
</select>
|
||||
<select className="select" value={catalogEditDraft.type} onChange={e => updateCatalogEditDraft({ type: e.target.value })}>
|
||||
<option value="">不选择细分品类</option>
|
||||
{typeOptions.map(t => <option key={t} value={t}>{t}</option>)}
|
||||
</select>
|
||||
</>
|
||||
)}
|
||||
<button className="btn btn-sm btn-primary" onClick={saveCatalogEdit}><Check size={12} />完成</button>
|
||||
</div>
|
||||
) : (
|
||||
<div onClick={() => !doc.uploading && startEditCatalogDoc(doc)} style={{ cursor: doc.uploading ? 'default' : 'pointer' }}>
|
||||
<div className={`cert-card-brand-text ${doc.title ? '' : 'empty'}`}>
|
||||
{doc.uploading ? '上传完成后可填写' : (doc.title || '点击填写目录名称')}
|
||||
</div>
|
||||
{(doc.category || doc.brand || doc.type) && (
|
||||
<div className="catalog-doc-meta">
|
||||
{[cat && cat.name, doc.brand, doc.type].filter(Boolean).join(' · ')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="cert-card-brand">
|
||||
{editingCatalogId === doc.id ? (
|
||||
<input
|
||||
className="input" autoFocus
|
||||
value={catalogTitleDraft}
|
||||
onChange={e => setCatalogTitleDraft(e.target.value)}
|
||||
onBlur={saveCatalogTitle}
|
||||
onKeyDown={e => { if (e.key === 'Enter') e.currentTarget.blur(); }}
|
||||
placeholder="目录名称,如:SKE产品样本"
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
className={`cert-card-brand-text ${doc.title ? '' : 'empty'}`}
|
||||
onClick={() => !doc.uploading && startEditCatalogTitle(doc)}
|
||||
>
|
||||
{doc.uploading ? '上传完成后可填写' : (doc.title || '点击填写目录名称')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in New Issue