diff --git a/App.jsx b/App.jsx index 5ce1989..8c5620e 100644 --- a/App.jsx +++ b/App.jsx @@ -4,13 +4,13 @@ import { Settings as SettingsIcon, Copy, ChevronRight, 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, Download, Wifi, Radio, QrCode + Menu, LogOut, GripVertical, BookOpen, FileText, FolderTree, Lock, Sparkles, Printer, Image as ImageIcon, + ShieldCheck, Settings2, Boxes, Download, Wifi, Radio, QrCode, Folder, FolderPlus } from 'lucide-react'; import { storageGet, storageSet, uploadFile, deleteFile, authCheck, authLogin, authLogout, authChangePassword, - getAiConfig, saveAiConfig, deleteAiConfig, testAiConfig, generateAiDraft, + getAiConfig, saveAiConfig, deleteAiConfig, testAiConfig, parseCatalogFiles, translateTerms, } from './storage'; @@ -79,7 +79,6 @@ function buildDefaultProductCategories() { })); } -const DEFAULT_SETTINGS = { defaultFollowUpDays: 14, dueSoonWindow: 7 }; const CUSTOMER_SOURCES = ['Made-in-China', '官网', '展会', 'Other']; const FILE_CATEGORIES = ['报价单', '合同', '聊天记录', '产品资料', '物流单据', 'Other']; @@ -246,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', settings: 'crm:settings', 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' }; const blankCustomer = () => ({ id: 'c_' + Date.now() + '_' + Math.random().toString(36).slice(2, 7), @@ -257,9 +256,10 @@ const blankCustomer = () => ({ website: '', productLines: [], intendedModel: '', intendedQty: '', source: '', - tags: '', notes: '', lastContact: '', followUpDays: '', + tags: '', notes: '', warehouseAddress: '', freightContact: '', shippingMarkNote: '', attachments: [], + fileFolders: [], // 虚拟文件夹:{id, name, parentId},parentId为null表示根目录下的文件夹 }); const OUTPUT_PORT_TYPES = ['USB', 'DC', 'DC1', 'DC2', 'DC3', 'POE', 'Type-C', 'DC interface', 'POE interface']; @@ -1277,23 +1277,6 @@ function todayStr() { const d = new Date(); return d.toISOString().slice(0, 10); } -function daysSince(dateStr) { - if (!dateStr) return Infinity; - const last = new Date(dateStr + 'T00:00:00'); - const today = new Date(); today.setHours(0, 0, 0, 0); - return Math.floor((today - last) / 86400000); -} -function statusOf(customer, settings) { - if (!customer.lastContact) return 'overdue'; - const d = daysSince(customer.lastContact); - const interval = Number(customer.followUpDays) || settings.defaultFollowUpDays || 30; - if (d >= interval) return 'overdue'; - if (d >= interval - (settings.dueSoonWindow || 7)) return 'due'; - return 'ok'; -} -function statusLabel(s) { - return s === 'overdue' ? '已逾期' : s === 'due' ? '即将到期' : '状态良好'; -} function regionLabel(id) { return (REGIONS.find(r => r.id === id) || {}).label || id; } @@ -1336,9 +1319,13 @@ function reorderWithinCategory(products, category, fromIndex, toIndex) { let i = 0; return products.map(p => (p.category === category ? reordered[i++] : p)); } -function fmtDate(d) { - if (!d) return '—'; - return d; +// VIP客户和非VIP客户在"客户档案"看板里分两组展示,拖拽排序只在各自组内生效—— +// 取出该组成员按新顺序重排,再按原来在整个customers数组里的位置依次塞回去,不影响另一组的顺序。 +function reorderCustomerGroup(customers, vipFlag, fromIndex, toIndex) { + const groupItems = customers.filter(c => !!c.isVip === vipFlag); + const reordered = reorderArray(groupItems, fromIndex, toIndex); + let i = 0; + return customers.map(c => (!!c.isVip === vipFlag ? reordered[i++] : c)); } function categoryBreadcrumb(p, categories) { const cat = (categories || []).find(c => c.id === p.category); @@ -1380,29 +1367,12 @@ function normalizeProductTaxonomy(products, categories) { return { products: migratedProducts, categories: cats }; } -function generateFollowUpEmail(customer) { - const name = customer.contact || customer.company || 'there'; - const product = customer.productLines && customer.productLines.length ? customer.productLines.join(', ') : 'our products'; - const subject = `Following up – ${customer.company || name}`; - const body = `Dear ${name}, - -I hope this message finds you well. It has been a little while since we last connected, and I wanted to check in regarding ${product}. - -If you have any upcoming projects or requirements, I would be happy to share our latest pricing, lead times, and technical updates. Please let me know if there is anything I can help with. - -Looking forward to hearing from you. - -Best regards, -[Your name]`; - return { subject, body }; -} /* -------------------------------- storage ------------------------------ */ async function loadAll() { let customers = []; let products = []; - let settings = DEFAULT_SETTINGS; let quotes = []; let sellerProfiles = DEFAULT_SELLER_PROFILES; let certifications = []; @@ -1417,10 +1387,6 @@ async function loadAll() { const p = await storageGet(STORAGE_KEYS.products); if (p && p.value) products = JSON.parse(p.value); } catch (e) {} - try { - const s = await storageGet(STORAGE_KEYS.settings); - if (s && s.value) settings = { ...DEFAULT_SETTINGS, ...JSON.parse(s.value) }; - } catch (e) {} try { const q = await storageGet(STORAGE_KEYS.quotes); if (q && q.value) quotes = JSON.parse(q.value); @@ -1455,7 +1421,7 @@ async function loadAll() { const bpt = await storageGet(STORAGE_KEYS.backPanelTemplates); if (bpt && bpt.value) backPanelTemplates = JSON.parse(bpt.value); } catch (e) {} - return { customers, products, settings, quotes, sellerProfiles, certifications, productCategories, packingLists, catalogDocs, jmsLines, backPanelTemplates }; + return { customers, products, quotes, sellerProfiles, certifications, productCategories, packingLists, catalogDocs, jmsLines, backPanelTemplates }; } /* -------------------------------- styles -------------------------------- */ @@ -1830,11 +1796,28 @@ const Styles = () => ( .doc-preview { position: absolute; left: 0; top: 0; width: 100%; max-width: none; border: none; box-shadow: none; padding: 24px 40px; } } - /* ---------- vip ---------- */ - .vip-toggle { cursor: pointer; color: var(--line); display: inline-flex; align-items: center; transition: opacity 0.15s; } - .vip-toggle:hover { opacity: 0.65; } - .vip-toggle.active { color: #C9942E; } - .vip-toggle.active-red { color: var(--led-red); } + /* ---------- vip / status badges ---------- */ + .tiny-badge { display: inline-flex; align-items: center; font-size: 10px; font-weight: 700; padding: 2px 7px; border-radius: 100px; letter-spacing: 0.02em; flex-shrink: 0; } + .tiny-badge-vip { background: #FBF1DD; color: #C9942E; } + .tiny-badge-red { background: var(--led-red-bg); color: var(--led-red); } + .tiny-badge-toggle { cursor: pointer; display: inline-flex; align-items: center; font-size: 11.5px; font-weight: 600; padding: 4px 11px; border-radius: 100px; border: 1.5px solid var(--line); color: var(--ink-soft); transition: all 0.15s; user-select: none; } + .tiny-badge-toggle:hover { border-color: var(--ink-soft); } + .tiny-badge-toggle.tiny-badge-vip.active { border-color: #C9942E; background: #FBF1DD; color: #C9942E; } + .tiny-badge-toggle.tiny-badge-red.active { border-color: var(--led-red); background: var(--led-red-bg); color: var(--led-red); } + + /* ---------- customer file explorer (virtual folders) ---------- */ + .file-breadcrumb { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; font-size: 12px; margin-bottom: 10px; padding-bottom: 8px; border-bottom: 1px solid var(--line); } + .file-breadcrumb-item { cursor: pointer; color: var(--ink-soft); padding: 2px 4px; border-radius: 4px; } + .file-breadcrumb-item:hover { background: var(--bg); color: var(--ink); } + .file-breadcrumb-item.current { color: var(--ink); font-weight: 700; cursor: default; } + .file-breadcrumb-item.current:hover { background: none; } + .folder-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(110px, 1fr)); gap: 8px; margin-bottom: 14px; } + .folder-item { position: relative; border: 1.5px solid var(--line); border-radius: 10px; padding: 12px 8px 8px; text-align: center; cursor: pointer; transition: border-color 0.15s, background 0.15s; } + .folder-item:hover { border-color: var(--copper); background: var(--copper-soft); } + .folder-item-name { font-size: 12px; font-weight: 600; margin-top: 6px; word-break: break-word; line-height: 1.3; } + .folder-item-count { font-size: 10.5px; color: var(--ink-soft); margin-top: 2px; } + .folder-item-actions { display: flex; justify-content: center; gap: 4px; margin-top: 6px; opacity: 0; transition: opacity 0.15s; } + .folder-item:hover .folder-item-actions { opacity: 1; } `} ); @@ -1884,6 +1867,51 @@ function StatCard({ num, label, tone }) { ); } +function CustomerMonitorGrid({ list, vipFlag, dragCustomerIdx, dragCustomerVip, onDragStart, onDrop, onDragEnd, now, onView }) { + if (list.length === 0) { + return
{vipFlag ? '还没有VIP客户' : '暂无客户'}
; + } + return ( +
+ {list.map((c, idx) => { + const lt = localTimeInfo(c.country, now); + const modelQty = [c.intendedModel, c.intendedQty ? `× ${c.intendedQty}` : ''].filter(Boolean).join(' '); + const cardTitle = customerTitle(c); + const showCompanyLine = !!(modelQty && c.company); + const { showEmail, showPhone } = contactDisplayFlags(c); + const isDragging = dragCustomerIdx === idx && dragCustomerVip === vipFlag; + return ( +
onDragStart(vipFlag, idx)} + onDragOver={(e) => e.preventDefault()} + onDrop={() => onDrop(vipFlag, idx)} + onDragEnd={onDragEnd} + > +
+ +
{cardTitle}
+ {c.hasOrdered && 已下单} +
+
{regionLabel(c.region)} · {c.country || '—'}
+ {showCompanyLine &&
{c.company}
} + {c.contact &&
{c.contact}
} + {showEmail &&
{c.email}
} + {showPhone &&
{c.phone}
} + {lt &&
当地时间 {lt.time}
} +
+ +
+
+ ); + })} +
+ ); +} + function ProductPills({ value, onChange }) { const toggle = (p) => { const has = value.includes(p); @@ -1922,7 +1950,7 @@ function RegionPills({ value, onChange }) { const NAV_GROUPS = [ { label: '客户', items: [ - { id: 'monitor', label: '客户监控', icon: LayoutDashboard }, + { id: 'monitor', label: '客户档案', icon: LayoutDashboard }, { id: 'manage', label: '客户管理', icon: Users }, { id: 'jms', label: 'Just My Socks', icon: Wifi }, ] }, @@ -2033,7 +2061,6 @@ function MainApp({ username, onLogout }) { const [loaded, setLoaded] = useState(false); const [customers, setCustomers] = useState([]); const [products, setProducts] = useState([]); - const [settings, setSettings] = useState(DEFAULT_SETTINGS); const [view, setView] = useState('monitor'); const [mobileMenuOpen, setMobileMenuOpen] = useState(false); @@ -2061,12 +2088,12 @@ function MainApp({ username, onLogout }) { const [aiImportHintType, setAiImportHintType] = useState(''); const [aiImportTruncatedWarning, setAiImportTruncatedWarning] = useState(false); - const [emailModal, setEmailModal] = useState(null); const [exportOpen, setExportOpen] = useState(false); const [toast, setToast] = useState(''); const [now, setNow] = useState(() => new Date()); const [dragCustomerIdx, setDragCustomerIdx] = useState(null); + const [dragCustomerVip, setDragCustomerVip] = useState(false); // 正在拖拽的卡片属于VIP组还是普通组 const [dragGroupState, setDragGroupState] = useState(null); // { category, index } const [pwCurrent, setPwCurrent] = useState(''); @@ -2077,8 +2104,6 @@ function MainApp({ username, onLogout }) { const [aiKeyInput, setAiKeyInput] = useState(''); const [aiSaving, setAiSaving] = useState(false); const [aiTesting, setAiTesting] = useState(false); - const [aiGenerating, setAiGenerating] = useState(false); - const [aiDraftModal, setAiDraftModal] = useState(null); // { channel, customerId, subject, body } const [quotes, setQuotes] = useState([]); const [sellerProfiles, setSellerProfiles] = useState(DEFAULT_SELLER_PROFILES); @@ -2125,7 +2150,6 @@ function MainApp({ username, onLogout }) { setCustomers(data.customers); setProducts(normalized.products); setProductCategories(normalized.categories); - setSettings(data.settings); setQuotes(data.quotes); setSellerProfiles(data.sellerProfiles); setCertifications(data.certifications); @@ -2138,7 +2162,6 @@ function MainApp({ username, onLogout }) { }, []); useEffect(() => { if (loaded) storageSet(STORAGE_KEYS.customers, JSON.stringify(customers)).catch(() => {}); }, [customers, loaded]); useEffect(() => { if (loaded) storageSet(STORAGE_KEYS.products, JSON.stringify(products)).catch(() => {}); }, [products, loaded]); - useEffect(() => { if (loaded) storageSet(STORAGE_KEYS.settings, JSON.stringify(settings)).catch(() => {}); }, [settings, loaded]); useEffect(() => { if (loaded) storageSet(STORAGE_KEYS.quotes, JSON.stringify(quotes)).catch(() => {}); }, [quotes, loaded]); useEffect(() => { if (loaded) storageSet(STORAGE_KEYS.sellerProfiles, JSON.stringify(sellerProfiles)).catch(() => {}); }, [sellerProfiles, loaded]); useEffect(() => { if (loaded) storageSet(STORAGE_KEYS.certifications, JSON.stringify(certifications)).catch(() => {}); }, [certifications, loaded]); @@ -2187,9 +2210,11 @@ function MainApp({ username, onLogout }) { (c.country || '').toLowerCase().includes(q) ); } - const order = { overdue: 0, due: 1, ok: 2 }; - return [...list].sort((a, b) => order[statusOf(a, settings)] - order[statusOf(b, settings)] || (a.company || '').localeCompare(b.company || '')); - }, [customers, search, regionFilter, settings]); + return [...list].sort((a, b) => (b.isVip ? 1 : 0) - (a.isVip ? 1 : 0) || (a.company || '').localeCompare(b.company || '')); + }, [customers, search, regionFilter]); + + const vipCustomers = useMemo(() => customers.filter(c => c.isVip), [customers]); + const normalCustomers = useMemo(() => customers.filter(c => !c.isVip), [customers]); const filteredProducts = useMemo(() => { let list = products; @@ -2206,11 +2231,9 @@ function MainApp({ username, onLogout }) { }, [products, productSearch, productCategoryFilter, productCategories]); const counts = useMemo(() => ({ - overdue: customers.filter(c => statusOf(c, settings) === 'overdue').length, - due: customers.filter(c => statusOf(c, settings) === 'due').length, total: customers.length, products: products.length, - }), [customers, settings, products]); + }), [customers, products]); const priceHistory = useMemo(() => buildPriceHistory(quotes), [quotes]); @@ -2219,9 +2242,9 @@ function MainApp({ username, onLogout }) { 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)); + function handleCustomerDrop(vipFlag, dropIdx) { + if (dragCustomerIdx === null || (dragCustomerIdx === dropIdx && dragCustomerVip === vipFlag)) { setDragCustomerIdx(null); return; } + setCustomers(prev => reorderCustomerGroup(prev, vipFlag, dragCustomerIdx, dropIdx)); setDragCustomerIdx(null); } async function handleAddCatalogFile(file) { @@ -2367,18 +2390,6 @@ function MainApp({ username, onLogout }) { setAiConfigHasKey(false); notify('已清除'); } - async function handleGenerateAi(customerId, channel) { - if (!aiConfigHasKey) { notify('请先在设置中配置DeepSeek API Key'); return; } - setAiGenerating(true); - try { - const result = await generateAiDraft(customerId, channel); - setAiDraftModal({ channel, customerId, subject: result.subject || '', body: result.body || '' }); - } catch (e) { - notify('生成失败:' + (e.message || '请检查API Key和网络')); - } finally { - setAiGenerating(false); - } - } function startNewCustomer() { setDraft(blankCustomer()); setSelectedId(null); } function startEditCustomer(c) { setDraft({ ...c, productLines: [...(c.productLines || [])] }); setSelectedId(c.id); } @@ -2398,10 +2409,6 @@ function MainApp({ username, onLogout }) { if (selectedId === id) setSelectedId(null); notify('已删除'); } - function markContacted(id) { - setCustomers(prev => prev.map(c => (c.id === id ? { ...c, lastContact: todayStr() } : c))); - notify('已标记为今日已联系'); - } function togglePreferredContact(id, channel) { setCustomers(prev => prev.map(c => { if (c.id !== id) return c; @@ -2423,7 +2430,7 @@ function MainApp({ username, onLogout }) { const f = e.dataTransfer.files && e.dataTransfer.files[0]; if (f) setAttachmentFile(f); } - async function handleUploadAttachment(customerId) { + async function handleUploadAttachment(customerId, folderId) { if (!attachmentFile) { notify('请先选择文件'); return; } setUploading(true); try { @@ -2436,6 +2443,7 @@ function MainApp({ username, onLogout }) { category: attachmentCategory, note: attachmentNote.trim(), date: todayStr(), + folderId: folderId || null, }; return { ...c, attachments: [entry, ...(c.attachments || [])] }; })); @@ -2461,7 +2469,42 @@ function MainApp({ username, onLogout }) { c.id === customerId ? { ...c, attachments: (c.attachments || []).filter(a => a.fileName !== fileName) } : c ))); } - function openFollowUpEmail(c) { setEmailModal({ ...generateFollowUpEmail(c), customerId: c.id }); } + function moveAttachmentToFolder(customerId, fileName, folderId) { + setCustomers(prev => prev.map(c => ( + c.id === customerId + ? { ...c, attachments: (c.attachments || []).map(a => (a.fileName === fileName ? { ...a, folderId: folderId || null } : a)) } + : c + ))); + } + // 虚拟文件夹:不改变服务器上的实际文件存储位置,只是给客户记录挂一份文件夹树 + // {id, name, parentId},附件通过folderId关联到某个文件夹(null=根目录),前端做成 + // 资源管理器的样子——新建/重命名/删除文件夹、点进去浏览、面包屑导航。 + function createFolder(customerId, parentId, name) { + const trimmed = (name || '').trim(); + if (!trimmed) return; + const folder = { id: 'folder_' + Date.now() + '_' + Math.random().toString(36).slice(2, 7), name: trimmed, parentId: parentId || null }; + setCustomers(prev => prev.map(c => ( + c.id === customerId ? { ...c, fileFolders: [...(c.fileFolders || []), folder] } : c + ))); + } + function renameFolder(customerId, folderId, name) { + const trimmed = (name || '').trim(); + if (!trimmed) return; + setCustomers(prev => prev.map(c => ( + c.id === customerId ? { ...c, fileFolders: (c.fileFolders || []).map(f => (f.id === folderId ? { ...f, name: trimmed } : f)) } : c + ))); + } + function deleteFolder(customerId, folderId) { + const customer = customers.find(c => c.id === customerId); + if (!customer) return; + const hasSubfolder = (customer.fileFolders || []).some(f => f.parentId === folderId); + const hasFile = (customer.attachments || []).some(a => a.folderId === folderId); + if (hasSubfolder || hasFile) { notify('文件夹不是空的,先移走或删除里面的文件/子文件夹'); return; } + if (!window.confirm('确定删除这个文件夹?')) return; + setCustomers(prev => prev.map(c => ( + c.id === customerId ? { ...c, fileFolders: (c.fileFolders || []).filter(f => f.id !== folderId) } : c + ))); + } function startNewProduct() { setProductDraft(blankProduct()); setSelectedProductId(null); } function startEditProduct(p) { setProductDraft({ ...p, extraNames: [] }); setSelectedProductId(p.id); } @@ -3111,61 +3154,42 @@ function MainApp({ username, onLogout }) { {view === 'monitor' && ( <>
- - +
-
全部客户
- 拖动卡片可调整顺序 +
VIP客户
+ 拖动卡片可调整组内顺序
- {customers.length === 0 ? ( -
暂无客户,去"客户管理"添加第一个
- ) : ( -
- {customers.map((c, idx) => { - const s = statusOf(c, settings); - const lt = localTimeInfo(c.country, now); - const modelQty = [c.intendedModel, c.intendedQty ? `× ${c.intendedQty}` : ''].filter(Boolean).join(' '); - const cardTitle = customerTitle(c); - const showCompanyLine = !!(modelQty && c.company); - const { showEmail, showPhone } = contactDisplayFlags(c); - return ( -
setDragCustomerIdx(idx)} - onDragOver={(e) => e.preventDefault()} - onDrop={() => handleCustomerDrop(idx)} - onDragEnd={() => setDragCustomerIdx(null)} - > -
- -
-
{cardTitle}
- {c.isVip && } - {c.hasOrdered && } -
-
{regionLabel(c.region)} · {c.country || '—'}
- {showCompanyLine &&
{c.company}
} - {c.contact &&
{c.contact}
} - {showEmail &&
{c.email}
} - {showPhone &&
{c.phone}
} - {lt &&
当地时间 {lt.time}
} -
上次联系 {fmtDate(c.lastContact)}
-
- - -
-
- ); - })} -
- )} + { setDragCustomerIdx(idx); setDragCustomerVip(vipFlag); }} + onDrop={handleCustomerDrop} + onDragEnd={() => setDragCustomerIdx(null)} + now={now} + onView={(id) => { setView('manage'); setDraft(null); setSelectedId(id); }} + /> + +
+
全部客户
+ 拖动卡片可调整组内顺序 +
+ { setDragCustomerIdx(idx); setDragCustomerVip(vipFlag); }} + onDrop={handleCustomerDrop} + onDragEnd={() => setDragCustomerIdx(null)} + now={now} + onView={(id) => { setView('manage'); setDraft(null); setSelectedId(id); }} + /> )} @@ -3185,13 +3209,15 @@ function MainApp({ username, onLogout }) { {filteredCustomers.length === 0 ? (
暂无客户,点击上方按钮添加第一个
) : filteredCustomers.map(c => { - const s = statusOf(c, settings); const showCompanyInSub = customerTitleIsModel(c) && c.company; return (
{ setDraft(null); setSelectedId(c.id); }}> -
-
{c.isVip && }{c.hasOrdered && }{customerTitle(c)}
+
+ {c.isVip && VIP} + {c.hasOrdered && 已下单} + {customerTitle(c)} +
{showCompanyInSub && `${c.company} · `}{regionLabel(c.region)} · {c.country || '—'}{localTimeInfo(c.country, now) && ` · 当地 ${localTimeInfo(c.country, now).time}`}
@@ -3208,12 +3234,9 @@ function MainApp({ username, onLogout }) { ) : selected ? ( startEditCustomer(selected)} onDelete={() => deleteCustomer(selected.id)} - onMarkContacted={() => markContacted(selected.id)} - onEmail={() => openFollowUpEmail(selected)} onTogglePreferred={(channel) => togglePreferredContact(selected.id, channel)} onToggleVip={() => toggleVip(selected.id)} onToggleOrdered={() => toggleOrdered(selected.id)} @@ -3225,15 +3248,16 @@ function MainApp({ username, onLogout }) { onPickAttachment={(f) => setAttachmentFile(f)} uploading={uploading} fileInputKey={fileInputKey} - onUploadAttachment={() => handleUploadAttachment(selected.id)} + onUploadAttachment={(folderId) => handleUploadAttachment(selected.id, folderId)} onDeleteAttachment={(fileName) => handleDeleteAttachment(selected.id, fileName)} + onMoveAttachment={(fileName, folderId) => moveAttachmentToFolder(selected.id, fileName, folderId)} + onCreateFolder={(parentId, name) => createFolder(selected.id, parentId, name)} + onRenameFolder={(folderId, name) => renameFolder(selected.id, folderId, name)} + onDeleteFolder={(folderId) => deleteFolder(selected.id, folderId)} fileDragActive={fileDragActive} onFileDragOver={handleFileDragOver} onFileDragLeave={handleFileDragLeave} onFileDrop={handleFileDrop} - aiConfigHasKey={aiConfigHasKey} - aiGenerating={aiGenerating} - onGenerateAi={(channel) => handleGenerateAi(selected.id, channel)} /> ) : (
选择左侧客户查看详情,或新建一个客户档案
@@ -3665,29 +3689,19 @@ function MainApp({ username, onLogout }) { {view === 'settings' && ( <>
-
跟进与提醒设置
+
数据管理
-
-
- 默认跟进周期(天) - setSettings(s => ({ ...s, defaultFollowUpDays: Number(e.target.value) || 1 }))} /> -
-
- "即将到期"提前提醒天数 - setSettings(s => ({ ...s, dueSoonWindow: Number(e.target.value) || 1 }))} /> -
-
- 说明:邮件功能用于生成可复制的英文邮件文案,需要你手动粘贴到自己的邮箱发送。报价单制作正在开发中,下一轮会上线。 + 说明:报价单制作正在开发中,下一轮会上线。
@@ -3726,7 +3740,7 @@ function MainApp({ username, onLogout }) { {aiConfigHasKey && }
- 用于客户详情页的"AI生成邮件 / AI生成WhatsApp话术"功能,调用DeepSeek官方API生成。成本很低(单次生成大概几分钱以内人民币,按你自己DeepSeek账户余额实际扣费),Key只会加密保存在服务器本地,不会显示给任何人。 + 用于"产品管理"里的产品目录批量导入功能,调用DeepSeek官方API识别目录文档生成产品字段。成本很低(单次生成大概几分钱以内人民币,按你自己DeepSeek账户余额实际扣费),Key只会加密保存在服务器本地,不会显示给任何人。
@@ -3738,58 +3752,15 @@ function MainApp({ username, onLogout }) { - {aiDraftModal && ( -
setAiDraftModal(null)}> -
e.stopPropagation()}> -
-
{aiDraftModal.channel === 'email' ? 'AI生成邮件' : 'AI生成WhatsApp话术'}
- -
-
- {aiDraftModal.channel === 'email' && ( -
主题 Subject setAiDraftModal(m => ({ ...m, subject: e.target.value }))} />
- )} -
- {aiDraftModal.channel === 'email' ? '正文 Body' : '消息内容'} -