(() => { let sessionToken = ''; const api = async (url, options = {}) => { const response = await fetch(url, { headers: { 'Content-Type': 'application/json', ...(sessionToken ? { Authorization: `Bearer ${sessionToken}` } : {}) }, ...options }); const body = await response.json(); if (Array.isArray(body.items)) body.filter = (...args) => body.items.filter(...args); if (!response.ok) throw new Error(body.error || '操作失败'); return body; }; const modal = (title, fields, submit) => { const wrap = document.createElement('div'); wrap.style = 'position:fixed;inset:0;background:#17212b66;display:grid;place-items:center;z-index:50'; wrap.innerHTML = `

${title}

${fields}
`; document.body.appendChild(wrap); wrap.querySelector('.cancel').onclick = () => wrap.remove(); wrap.querySelector('.confirm').onclick = async () => { try { await submit(wrap); wrap.remove(); } catch (error) { wrap.querySelector('.error').textContent = error.message; } }; }; document.querySelectorAll('[data-view="erp"], #erp').forEach((element) => element.remove()); document.querySelector('[data-view="groups"]')?.remove(); document.getElementById('groups')?.style && (document.getElementById('groups').style.display = 'none'); let currentUser = null; let groups = []; let activeGroupId = null; const applyRoleUI = () => { const isAdmin = currentUser?.role === '管理员'; document.querySelector('[data-view="agents"]')?.classList.toggle('hidden', !isAdmin); document.getElementById('agents')?.classList.toggle('hidden', !isAdmin); document.querySelector('[data-view="admin"]')?.classList.toggle('hidden', !isAdmin); document.getElementById('admin')?.classList.toggle('hidden', !isAdmin); const agentCard = [...document.querySelectorAll('#home .card')].find((card) => card.textContent.includes('已上架智能体')); if (agentCard && isAdmin) { agentCard.querySelector('.stat').textContent = '4'; agentCard.querySelector('.muted').textContent = '单号查询与三类价格助手'; } }; const accountAvatar = document.querySelector('.account .avatar'); if (accountAvatar) { accountAvatar.title = '点击更换头像'; accountAvatar.style.cursor = 'pointer'; accountAvatar.onclick = () => { const input = document.createElement('input'); input.type = 'file'; input.accept = 'image/png,image/jpeg,image/webp'; input.onchange = async () => { const file = input.files[0]; if (!file) return; await fetch('/api/profile/avatar', { method: 'POST', headers: { Authorization: `Bearer ${sessionToken}`, 'X-File-Name': file.name }, body: await file.arrayBuffer() }); accountAvatar.textContent = ''; accountAvatar.style.backgroundImage = `url('/api/profile/avatar/${encodeURIComponent(currentUser.name)}?v=${Date.now()}')`; accountAvatar.style.backgroundSize = 'cover'; }; input.click(); }; } document.getElementById('loginBtn').addEventListener('click', () => setTimeout(() => { if (currentUser && accountAvatar) { accountAvatar.textContent = ''; accountAvatar.style.backgroundImage = `url('/api/profile/avatar/${encodeURIComponent(currentUser.name)}?v=${Date.now()}')`; accountAvatar.style.backgroundSize = 'cover'; } }, 120)); let navMessages = document.querySelector('[data-view="messages"]'); const renderCleanMessages = async () => { const [direct, groups, groupMessages, pins] = await Promise.all([api('/api/direct-messages'), api('/api/groups'), api('/api/messages'), api('/api/pins')]); const sessions = []; const people = new Set(); direct.items.forEach((item) => people.add(item.from === currentUser.name ? item.to : item.from)); people.forEach((name) => { const last = direct.items.filter((item) => item.from === name || item.to === name).at(-1); sessions.push({ key:`direct:${name}`, type:'direct', id:name, name, time:last?.createdAt || '', preview:last?.text || '暂无消息' }); }); groups.items.forEach((group) => { const last = groupMessages.filter((item) => item.groupId === group.id).at(-1); sessions.push({ key:`group:${group.id}`, type:'group', id:group.id, name:group.name, time:last?.createdAt || '', preview:last?.text || '暂无消息' }); }); sessions.sort((a,b) => { const ap=pins.items.includes(a.key), bp=pins.items.includes(b.key); return ap !== bp ? (bp-ap) : new Date(b.time||0)-new Date(a.time||0); }); let selected = null; const render = async () => { const list = sessions.map((item) => ``).join(''); let history='
请选择左侧群聊或联系人
'; let title='消息'; if (selected) { const items = selected.type==='direct' ? (await api(`/api/direct-messages?with=${encodeURIComponent(selected.id)}`)).items : groupMessages.filter((item) => item.groupId===selected.id); title=selected.name; history=items.map((item)=>`
${selected.type==='direct'?item.from:item.sender}
${item.fileName||item.text}
`).join('')||'
暂无消息
'; } messagesView.innerHTML=`
${title}
${history}
${selected?'
':''}
`; messagesView.querySelectorAll('.clean-session').forEach((node)=>node.onclick=()=>{selected=sessions.find((item)=>item.key===node.dataset.key);render();}); if(selected) document.getElementById('cleanSend').onclick=async()=>{const text=document.getElementById('cleanInput').value.trim();if(!text)return;if(selected.type==='direct')await api('/api/direct-messages',{method:'POST',body:JSON.stringify({to:selected.id,text})});else await api('/api/messages',{method:'POST',body:JSON.stringify({groupId:selected.id,text})});await render();}; }; render(); }; if (false) navMessages.addEventListener('click', (event) => { event.stopImmediatePropagation(); document.querySelectorAll('.nav button').forEach((button) => button.classList.remove('active')); navMessages.classList.add('active'); document.querySelectorAll('.view').forEach((view) => view.classList.remove('active')); document.getElementById('messages').classList.add('active'); document.getElementById('pageTitle').textContent = '消息'; setTimeout(renderCleanMessages, 0); }, true); const list = document.querySelector('#groups .list'); const createButton = document.getElementById('createGroup'); list.querySelectorAll('[data-room]').forEach((element) => element.remove()); const renderMessages = async () => { const items = (await api('/api/messages')).items.filter((item) => item.groupId === activeGroupId); document.getElementById('roomChat').innerHTML = items.length ? items.map((item) => `
${item.sender}
${item.text}
`).join('') : '
系统:单号查询员已加入本群。发送单号后会自动查询。
'; }; const openGroup = async (id) => { activeGroupId = id; list.querySelectorAll('[data-group-id]').forEach((button) => button.classList.toggle('active', button.dataset.groupId === id)); const group = groups.find((item) => item.id === id); document.getElementById('roomTitle').firstChild.textContent = group?.name || ''; document.getElementById('roomInput').disabled = !group || group.dissolved; document.getElementById('roomSend').disabled = !group || group.dissolved; await renderMessages(); }; const renderGroups = async () => { groups = (await api('/api/groups')).items.filter((group) => !group.dissolved); list.querySelectorAll('[data-group-id]').forEach((element) => element.remove()); groups.forEach((group) => { const button = document.createElement('button'); button.dataset.groupId = group.id; button.innerHTML = `${group.name} ${group.members.length} 人`; button.onclick = () => openGroup(group.id); list.insertBefore(button, createButton); }); if (!groups.some((group) => group.id === activeGroupId)) activeGroupId = groups[0]?.id || null; if (activeGroupId) await openGroup(activeGroupId); bindGroupAdmin(); }; const bindGroupAdmin = () => { const rename = document.getElementById('renameGroup'); const dissolve = document.getElementById('dissolveGroup'); if (rename) rename.onclick = () => { const group = groups.find((item) => item.id === activeGroupId); if (!group) return; modal('修改群名称', ``, async (wrap) => { await api(`/api/groups/${activeGroupId}`, { method: 'PATCH', body: JSON.stringify({ name: wrap.querySelector('.group-name').value }) }); await renderGroups(); }); }; if (dissolve) dissolve.onclick = () => { const group = groups.find((item) => item.id === activeGroupId); if (!group) return; modal('解散群聊', `

只会解散“${group.name}”,其他群聊不受影响。请输入“解散”确认。

`, async (wrap) => { if (wrap.querySelector('.dissolve-confirm').value.trim() !== '解散') throw new Error('请输入“解散”确认'); await api(`/api/groups/${activeGroupId}`, { method: 'PATCH', body: JSON.stringify({ dissolved: true }) }); activeGroupId = null; await renderGroups(); }); }; }; document.getElementById('loginBtn').onclick = async () => { try { const result = await api('/api/auth/login', { method: 'POST', body: JSON.stringify({ name: document.getElementById('loginName').value.trim(), password: document.getElementById('loginPass').value }) }); sessionToken = result.token; currentUser = result.user; window.__manyiToken = sessionToken; window.__manyiUser = currentUser; applyRoleUI(); document.getElementById('login').style.display = 'none'; document.getElementById('app').style.display = 'flex'; await renderGroups(); } catch (error) { alert(error.message); } }; document.getElementById('loginPass').onkeydown = (event) => { if (event.key === 'Enter') document.getElementById('loginBtn').click(); }; createButton.onclick = () => modal('新建群聊', '', async (wrap) => { const created = await api('/api/groups', { method: 'POST', body: JSON.stringify({ name: wrap.querySelector('.group-name').value }) }); activeGroupId = created.id; await renderGroups(); }); document.getElementById('roomSend').onclick = async () => { const input = document.getElementById('roomInput'); const text = input.value.trim(); if (!text || !activeGroupId) return; input.value = ''; try { await api('/api/messages', { method: 'POST', body: JSON.stringify({ groupId: activeGroupId, sender: currentUser.name, text }) }); await renderMessages(); } catch (error) { document.getElementById('roomChat').insertAdjacentHTML('beforeend', `
发送失败:${error.message}
`); } }; document.getElementById('invite').onclick = async () => { try { const contacts = (await api('/api/contacts')).items; const group = groups.find((item) => item.id === activeGroupId); const users = contacts.filter((user) => !group.members.includes(user.name)); if (!users.length) throw new Error('没有可邀请的联系人'); modal('邀请成员', ``, async (wrap) => { await api(`/api/groups/${activeGroupId}/members`, { method: 'POST', body: JSON.stringify({ name: wrap.querySelector('.invite-user').value }) }); await renderGroups(); }); } catch (error) { alert(error.message); } }; const contactsView = document.getElementById('contacts').querySelector('.card'); const renderContacts = async () => { const contacts = (await api('/api/contacts')).items; contactsView.innerHTML = `

联系人

${contacts.length ? contacts.map((user) => `
${user.name.slice(0,1)}
${user.name}
${user.role}
`).join('') : '

暂无其他联系人

'}`; contactsView.querySelectorAll('.chat-contact').forEach((button) => button.onclick = () => openDirect(button.dataset.name)); }; const openDirect = async (name) => { const items = (await api(`/api/direct-messages?with=${encodeURIComponent(name)}`)).items; modal(`与 ${name} 私聊`, `
${items.map((item) => `
${item.from}
${item.fileName ? `文件:${item.fileName}` : item.text}
`).join('') || '
暂无消息
'}
`, async (wrap) => { const text = wrap.querySelector('.direct-text').value.trim(); const file = wrap.querySelector('.direct-file').files[0]; if (text) await api('/api/direct-messages', { method: 'POST', body: JSON.stringify({ to: name, text }) }); if (file) { const data = await file.arrayBuffer(); await fetch('/api/direct-files', { method: 'POST', headers: { Authorization: `Bearer ${sessionToken}`, 'X-To': name, 'X-File-Name': file.name }, body: data }); } await renderContacts(); }); }; document.querySelector('[data-view="contacts"]').addEventListener('click', renderContacts); const messagesView = document.getElementById('messages').querySelector('.card'); let unifiedOpen = null; const openUnifiedConversation = async (kind, id) => { unifiedOpen = { kind, id }; const title = kind === 'direct' ? id : (await api('/api/groups')).items.find((group) => group.id === id)?.name || '群聊'; const items = kind === 'direct' ? (await api(`/api/direct-messages?with=${encodeURIComponent(id)}`)).items : (await api('/api/messages')).items.filter((item) => item.groupId === id); messagesView.innerHTML = `

${title}

${items.map((item) => `
${kind === 'direct' ? item.from : item.sender}
${item.fileName ? item.fileName : item.text}
`).join('') || '
暂无消息
'}
`; document.getElementById('backInbox').onclick = renderInbox; document.getElementById('unifiedSend').onclick = async () => { const input = document.getElementById('unifiedInput'); if (!input.value.trim()) return; if (kind === 'direct') await api('/api/direct-messages', { method: 'POST', body: JSON.stringify({ to: id, text: input.value.trim() }) }); else await api('/api/messages', { method: 'POST', body: JSON.stringify({ groupId: id, text: input.value.trim() }) }); await openUnifiedConversation(kind, id); } }; const renderInbox = async () => { const [direct, groupData, pinData] = await Promise.all([api('/api/direct-messages'), api('/api/groups'), api('/api/pins')]); const pins = pinData.items; const groupsById = new Map(groupData.items.map((group) => [group.id, group])); const groupMessages = (await api('/api/messages')).items; const conversations = []; const directMap = new Map(); direct.items.forEach((item) => { const person = item.from === currentUser.name ? item.to : item.from; if (!directMap.has(person)) directMap.set(person, []); directMap.get(person).push(item); }); directMap.forEach((items, person) => conversations.push({ key:`direct:${person}`, html:`` })); const groupMap = new Map(); groupMessages.forEach((item) => { if (!groupMap.has(item.groupId)) groupMap.set(item.groupId, []); groupMap.get(item.groupId).push(item); }); groupMap.forEach((items, groupId) => { const group = groupsById.get(groupId); if (group) conversations.push({ key:`group:${groupId}`, html:`` }); }); conversations.sort((a,b)=>(pins.includes(a.key)?-1:0)-(pins.includes(b.key)?-1:0)); messagesView.innerHTML = `

消息

${conversations.length ? conversations.map((item) => `${item.html}${pins.includes(item.key) ? '已置顶 · 右键取消置顶' : '右键置顶'}`).join('') : '

暂无新消息

'}`; messagesView.querySelectorAll('.inbox-direct').forEach((button) => { button.onclick = () => openUnifiedConversation('direct', button.dataset.name); button.oncontextmenu = async (event) => { event.preventDefault(); await api('/api/pins',{method:'POST',body:JSON.stringify({key:`direct:${button.dataset.name}`})}); await renderInbox(); }; }); messagesView.querySelectorAll('.inbox-group').forEach((button) => { button.onclick = () => openUnifiedConversation('group', button.dataset.groupId); button.oncontextmenu = async (event) => { event.preventDefault(); await api('/api/pins',{method:'POST',body:JSON.stringify({key:`group:${button.dataset.groupId}`})}); await renderInbox(); }; }); }; if (false) document.querySelector('[data-view="messages"]').addEventListener('click', renderInbox); if (false) document.querySelector('[data-view="messages"]').addEventListener('click', () => setTimeout(() => { if (!document.getElementById('messageCreateGroup')) { const button = document.createElement('button'); button.id = 'messageCreateGroup'; button.className = 'btn'; button.textContent = '+ 新建群聊'; button.style.marginBottom = '12px'; messagesView.insertBefore(button, messagesView.firstChild); } }, 50)); const admin = document.getElementById('admin').querySelector('.card'); admin.innerHTML = '

系统设置

每天早上 8 点自动清理过期表格并更新查询索引。

账号与权限

'; const roles = ['管理员','运营','客服','跟单','设计师','工厂','财务']; const renderUsers = async () => { const users = (await api('/api/users')).items; document.getElementById('userList').innerHTML = `${users.map((user) => ``).join('')}
账号角色状态操作
${user.name}${user.role}${user.enabled === false ? '停用' : '启用'}${user.name === '伊青荣' ? '' : ``}
`; document.querySelectorAll('.edit-user').forEach((button) => button.onclick = () => modal('修改账号权限', ``, async (wrap) => { await api('/api/users/permissions', { method: 'POST', body: JSON.stringify({ name: button.dataset.name, role: wrap.querySelector('.role').value, enabled: wrap.querySelector('.enabled').checked }) }); await renderUsers(); })); document.querySelectorAll('.delete-user').forEach((button) => button.onclick = () => modal('删除账号', `

删除后该账号将无法登录,且不能恢复。

${button.dataset.name}

`, async (wrap) => { if (wrap.querySelector('.delete-confirm').value.trim() !== button.dataset.name) throw new Error('请输入完全一致的账号名'); await api('/api/users/delete', { method: 'POST', body: JSON.stringify({ name: button.dataset.name }) }); await renderUsers(); })); }; document.getElementById('addUser').onclick = () => modal('创建账号', ``, async (wrap) => { await api('/api/users', { method: 'POST', body: JSON.stringify({ name: wrap.querySelector('.username').value.trim(), password: wrap.querySelector('.password').value, role: wrap.querySelector('.role').value }) }); await renderUsers(); }); document.getElementById('refreshUsers').onclick = renderUsers; const renderSettings = async () => { const settings = await api('/api/settings'); document.getElementById('retentionDays').value = settings.orderRetentionDays; }; document.getElementById('saveRetention').onclick = async () => { const status = document.getElementById('retentionStatus'); try { const settings = await api('/api/settings', { method: 'PATCH', body: JSON.stringify({ orderRetentionDays: Number(document.getElementById('retentionDays').value) }) }); status.textContent = `已保存:单号表格保留 ${settings.orderRetentionDays} 天,每天早上 8 点自动清理。`; } catch (error) { status.textContent = error.message; } }; document.querySelector('[data-view="admin"]').addEventListener('click', () => { renderUsers(); renderSettings(); }); const renderWechatInbox = async () => { const [direct, groupData, pinData] = await Promise.all([api('/api/direct-messages'), api('/api/groups'), api('/api/pins')]); const groupMessages = (await api('/api/messages')).items; const sessions = []; const people = new Set(); direct.items.forEach((item) => people.add(item.from === currentUser.name ? item.to : item.from)); people.forEach((name) => sessions.push({ key: `direct:${name}`, kind: 'direct', id: name, name, preview: direct.items.filter((item) => item.from === name || item.to === name).at(-1)?.text || '暂无消息' })); groupData.items.forEach((group) => sessions.push({ key: `group:${group.id}`, kind: 'group', id: group.id, name: group.name, preview: groupMessages.filter((item) => item.groupId === group.id).at(-1)?.text || '暂无消息' })); sessions.sort((a, b) => (pinData.items.includes(a.key) ? -1 : 0) - (pinData.items.includes(b.key) ? -1 : 0)); let selected = sessions[0]; const render = async () => { if (!selected) return; const items = selected.kind === 'direct' ? (await api(`/api/direct-messages?with=${encodeURIComponent(selected.id)}`)).items : (await api('/api/messages')).items.filter((item) => item.groupId === selected.id); messagesView.innerHTML = `
${sessions.map((item) => `
${item.preview || '暂无消息'}
`).join('')}
${selected.name}
${items.map((item) => `
${selected.kind === 'direct' ? item.from : item.sender}
${item.fileName ? `文件:${item.fileName}` : item.text}
`).join('') || '
暂无消息
'}
`; messagesView.querySelectorAll('[data-session]').forEach((button) => button.onclick = (event) => { if (event.target.closest('[data-pin]')) return; selected = sessions.find((item) => item.key === button.dataset.session) || selected; render(); }); messagesView.querySelectorAll('[data-pin]').forEach((button) => button.onclick = async (event) => { event.stopPropagation(); await api('/api/pins', { method: 'POST', body: JSON.stringify({ key: button.dataset.pin }) }); await renderWechatInbox(); }); document.getElementById('wechatNewGroup').onclick = () => createButton.click(); document.getElementById('wechatSend').onclick = async () => { const input = document.getElementById('wechatInput'); const text = input.value.trim(); const file = document.getElementById('wechatFile').files[0]; if (text) { if (selected.kind === 'direct') await api('/api/direct-messages', { method: 'POST', body: JSON.stringify({ to: selected.id, text }) }); else await api('/api/messages', { method: 'POST', body: JSON.stringify({ groupId: selected.id, text }) }); } if (file && selected.kind === 'direct') { await fetch('/api/direct-files', { method: 'POST', headers: { Authorization: `Bearer ${sessionToken}`, 'X-To': selected.id, 'X-File-Name': file.name }, body: await file.arrayBuffer() }); } await renderWechatInbox(); }; }; render(); }; if (false) document.querySelector('[data-view="messages"]').addEventListener('click', () => setTimeout(renderWechatInbox, 80)); document.addEventListener('contextmenu', (event) => { const item = event.target.closest('.wechat-item'); if (!item) return; event.preventDefault(); document.querySelector('.wechat-context-menu')?.remove(); const key = item.dataset.session; const menu = document.createElement('div'); menu.className = 'wechat-context-menu'; const pinned = item.querySelector('.wechat-pin')?.textContent === '★'; menu.innerHTML = ``; menu.style = `position:fixed;left:${Math.min(event.clientX, window.innerWidth - 190)}px;top:${Math.min(event.clientY, window.innerHeight - 280)}px;z-index:100;background:#fff;border:1px solid #dce3ea;border-radius:8px;box-shadow:0 8px 24px #17212b33;padding:6px 0;width:180px`; menu.querySelectorAll('button').forEach((button) => { button.style = 'display:block;width:100%;padding:9px 14px;border:0;background:#fff;text-align:left;cursor:pointer;font-size:14px'; button.onmouseenter = () => button.style.background = '#f2f6fa'; button.onmouseleave = () => button.style.background = '#fff'; }); menu.querySelector('.menu-delete').style.color = '#d64545'; menu.querySelector('[data-action="pin"]').onclick = async () => { await api('/api/pins', { method: 'POST', body: JSON.stringify({ key }) }); menu.remove(); await renderWechatInbox(); }; document.body.appendChild(menu); }); document.addEventListener('click', (event) => { if (!event.target.closest('.wechat-context-menu')) document.querySelector('.wechat-context-menu')?.remove(); }); const renderWechatInboxV2 = async () => { const [direct, groupData, pinData, state] = await Promise.all([api('/api/direct-messages'), api('/api/groups'), api('/api/pins'), api('/api/conversation-state')]); const sessions = []; const people = new Set(); direct.items.forEach((item) => people.add(item.from === currentUser.name ? item.to : item.from)); people.forEach((name) => sessions.push({ key: `direct:${name}`, kind: 'direct', id: name, name, preview: direct.items.filter((item) => item.from === name || item.to === name).at(-1)?.text || '暂无消息' })); groupData.items.forEach((group) => sessions.push({ key: `group:${group.id}`, kind: 'group', id: group.id, name: group.name, preview: '群聊' })); sessions.sort((a, b) => (pinData.items.includes(a.key) ? -1 : 0) - (pinData.items.includes(b.key) ? -1 : 0)); const active = sessions.find((item) => !state[item.key]?.hidden && !state[item.key]?.deleted) || sessions[0]; const open = async (selected) => { if (!selected) { messagesView.innerHTML = '
暂无会话
'; return; } const items = selected.kind === 'direct' ? (await api(`/api/direct-messages?with=${encodeURIComponent(selected.id)}`)).items : (await api('/api/messages')).items.filter((item) => item.groupId === selected.id); messagesView.innerHTML = `
${selected.name}
${items.map((item) => `
${selected.kind === 'direct' ? item.from : item.sender}
${item.fileName ? item.fileName : item.text}
`).join('') || '
暂无消息
'}
`; messagesView.querySelectorAll('.wechat-session').forEach((node) => node.onclick = () => open(sessions.find((item) => item.key === node.dataset.sessionKey))); messagesView.querySelectorAll('.pin-action').forEach((button) => button.onclick = async (event) => { event.stopPropagation(); await api('/api/pins', { method: 'POST', body: JSON.stringify({ key: button.dataset.key }) }); await renderWechatInboxV2(); }); document.getElementById('wechatSend').onclick = async () => { const input = document.getElementById('wechatInput'); const text = input.value.trim(); if (!text) return; if (selected.kind === 'direct') await api('/api/direct-messages', { method: 'POST', body: JSON.stringify({ to: selected.id, text }) }); else await api('/api/messages', { method: 'POST', body: JSON.stringify({ groupId: selected.id, text }) }); await open(selected); }; }; open(active); }; if (false) document.querySelector('[data-view="messages"]').addEventListener('click', () => setTimeout(renderWechatInboxV2, 120)); const organizeWechatSubLists = () => { const list = document.querySelector('.wechat-sessions'); if (!list || list.dataset.organized === '1') return; list.dataset.organized = '1'; const search = list.querySelector('.wechat-search'); const nodes = [...list.querySelectorAll('.wechat-session')]; const groups = nodes.filter((node) => node.dataset.sessionKey?.startsWith('group:')); const contacts = nodes.filter((node) => node.dataset.sessionKey?.startsWith('direct:')); list.innerHTML = ''; if (search) list.appendChild(search); const addSection = (title, items) => { const heading = document.createElement('div'); heading.textContent = title; heading.style = 'padding:10px 14px;color:#6b7785;font-size:12px;font-weight:700;border-top:1px solid #e1e7ee'; list.appendChild(heading); items.forEach((item) => list.appendChild(item)); }; addSection('群聊', groups); addSection('联系人', contacts); }; const inboxObserver = new MutationObserver(() => organizeWechatSubLists()); inboxObserver.observe(messagesView, { childList: true, subtree: true }); const renderChronologicalInbox = async () => { const [direct, groupData, pinData] = await Promise.all([api('/api/direct-messages'), api('/api/groups'), api('/api/pins')]); const groupMessages = (await api('/api/messages')).items; const sessions = []; const people = new Set(); direct.items.forEach((item) => people.add(item.from === currentUser.name ? item.to : item.from)); people.forEach((name) => { const items = direct.items.filter((item) => item.from === name || item.to === name); const last = items.at(-1); sessions.push({ key:`direct:${name}`, kind:'direct', id:name, name, preview:last?.fileName ? `文件:${last.fileName}` : (last?.text || '暂无消息'), time:last?.createdAt || '' }); }); groupData.items.forEach((group) => { const items = groupMessages.filter((item) => item.groupId === group.id); const last = items.at(-1); sessions.push({ key:`group:${group.id}`, kind:'group', id:group.id, name:group.name, preview:last?.text || '暂无消息', time:last?.createdAt || '' }); }); sessions.sort((a,b) => { const ap=pinData.items.includes(a.key), bp=pinData.items.includes(b.key); if (ap !== bp) return bp - ap; return new Date(b.time || 0) - new Date(a.time || 0); }); const selected=sessions[0]; messagesView.innerHTML=`
${selected?.name || '消息'}
`; messagesView.querySelectorAll('.wechat-session').forEach((node)=>node.onclick=async()=>{const item=sessions.find((entry)=>entry.key===node.dataset.sessionKey); if(item) await openUnifiedConversation(item.kind,item.id);}); }; if (false) document.querySelector('[data-view="messages"]').addEventListener('click', () => setTimeout(renderChronologicalInbox, 260)); const cleanStyle = document.createElement('style'); cleanStyle.textContent = '.clean-layout{display:grid;grid-template-columns:300px minmax(0,1fr);height:620px;border:1px solid #e1e7ee;border-radius:8px;overflow:hidden;background:#fff}.clean-list{background:#f4f6f8;border-right:1px solid #e1e7ee;overflow:auto}.clean-title{padding:16px;font-size:18px;font-weight:700}.clean-session{display:flex;align-items:center;width:100%;padding:12px;border:0;border-bottom:1px solid #e5e9ee;background:transparent;text-align:left;cursor:pointer}.clean-session.selected{background:#dcecff}.clean-meta{display:flex;flex-direction:column;flex:1;min-width:0}.clean-meta small{color:#7b8795;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.clean-session time{color:#8b96a3;font-size:11px}.clean-chat{display:flex;flex-direction:column;min-width:0}.clean-chat header{padding:16px 20px;border-bottom:1px solid #e1e7ee;font-weight:700}.clean-history{flex:1;overflow:auto;padding:16px;background:#f7f9fb}.clean-chat .composer{padding:12px;border-top:1px solid #e1e7ee}'; document.head.appendChild(cleanStyle); navMessages = document.querySelector('[data-view="messages"]'); const renderMessageSublist = async () => { let sub = document.getElementById('messageSublist'); if (!sub) { sub = document.createElement('div'); sub.id = 'messageSublist'; sub.style = 'margin:0 8px 8px 18px;max-height:260px;overflow:auto'; navMessages.insertAdjacentElement('afterend', sub); } const [direct, groupData] = await Promise.all([api('/api/direct-messages'), api('/api/groups')]); const names = new Set(); direct.items.forEach((item) => names.add(item.from === currentUser.name ? item.to : item.from)); const items = [...groupData.items.map((group) => ({ key:`group:${group.id}`, name:group.name, type:'group' })), ...[...names].map((name) => ({ key:`direct:${name}`, name, type:'direct' }))]; sub.innerHTML = items.map((item) => ``).join(''); sub.querySelectorAll('.message-subitem').forEach((button) => button.onclick = () => { const [type, id] = button.dataset.key.split(':'); document.querySelector('[data-view="messages"]').click(); setTimeout(() => type === 'group' ? openUnifiedConversation('group', id) : openUnifiedConversation('direct', id), 300); }); }; if (false) navMessages.addEventListener('click', () => setTimeout(renderMessageSublist, 140)); document.addEventListener('contextmenu', (event) => { const node = event.target.closest('.wechat-session'); if (!node) return; event.preventDefault(); event.stopImmediatePropagation(); const key = node.dataset.sessionKey; document.querySelector('.wechat-context-menu')?.remove(); const menu = document.createElement('div'); menu.className = 'wechat-context-menu'; menu.innerHTML = ``; menu.style = `position:fixed;left:${Math.min(event.clientX, window.innerWidth - 190)}px;top:${Math.min(event.clientY, window.innerHeight - 240)}px;z-index:100;background:#fff;border:1px solid #dce3ea;border-radius:8px;box-shadow:0 8px 24px #17212b33;padding:6px 0;width:180px`; menu.querySelectorAll('button').forEach((button) => { button.style = 'display:block;width:100%;padding:9px 14px;border:0;background:#fff;text-align:left;cursor:pointer;font-size:14px'; button.onclick = async () => { await api('/api/conversation-state', { method: 'POST', body: JSON.stringify({ key, action: button.dataset.action }) }); if (button.dataset.action === 'pin') await api('/api/pins', { method: 'POST', body: JSON.stringify({ key }) }); menu.remove(); await renderWechatInboxV2(); }; }); document.body.appendChild(menu); }, true); const fileView = document.getElementById('files'); if (false && fileView) { const card = fileView.querySelector('.card'); card.innerHTML = `

本地表格工作区

文件保存在 D:\\满意ERP数据,网页只读取索引,不复制文件。

上传后会归档、读取全部工作表并建立索引。

已收录表格

查询结果

输入关键词后显示匹配行及来源。

`; const escLibrary = (value) => String(value ?? '').replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c])); const formatSize = (bytes) => Number(bytes || 0) < 1048576 ? `${(Number(bytes || 0) / 1024).toFixed(1)} KB` : `${(Number(bytes || 0) / 1048576).toFixed(1)} MB`; const loadLibraryFiles = async (query = '') => { const result = await api(`/api/library/files?q=${encodeURIComponent(query)}`); card.querySelector('#libraryFiles').innerHTML = result.items.length ? result.items.map((file) => `
${escLibrary(file.name)}${formatSize(file.size)} · ${new Date(file.modifiedAt).toLocaleString()} · ${file.status === 'failed' ? '解析失败' : file.status === 'indexed' ? `已索引 ${file.records} 行` : '等待索引'}${escLibrary(file.path)}
`).join('') : '

没有匹配的已上传表格。

'; }; const searchLibrary = async () => { const query = card.querySelector('#libraryQuery').value.trim(); if (!query) { await loadLibraryFiles(); card.querySelector('#libraryResults').innerHTML = '

请输入单号、客户、文件名或关键词。

'; return; } const [matches] = await Promise.all([api(`/api/orders?q=${encodeURIComponent(query)}`), loadLibraryFiles(query)]); const rows = [...(matches.items || []).map((item) => ({ text: Object.values(item.data || {}).join('|'), file: item.source?.file, sheet: item.source?.sheet || 'default', row: item.source?.row })), ...(matches.textHits || [])]; card.querySelector('#libraryResults').innerHTML = rows.length ? rows.slice(0, 100).map((row) => `
${escLibrary(cleanFileName(row.file))}${escLibrary(row.sheet || '未知工作表')} · 第 ${escLibrary(row.row || '-')} 行${escLibrary(row.text)}
`).join('') : '

未找到匹配内容,请确认表格已成功录入。

'; }; const cleanFileName = (name) => String(name || '未知文件').replace(/^(?:[a-f0-9]{64}-)+/i, ''); card.querySelector('#librarySearch').onclick = searchLibrary; card.querySelector('#libraryQuery').onkeydown = (event) => { if (event.key === 'Enter') searchLibrary(); }; card.querySelector('#attachmentInput').onchange = async (event) => { const file = event.target.files?.[0]; if (!file) return; const status = card.querySelector('#attachmentStatus'); const summary = card.querySelector('#attachmentSummary'); status.textContent = '正在保存、解析全部工作表并建立索引…'; summary.textContent = ''; try { const response = await fetch('/api/attachments', { method: 'POST', headers: { Authorization: `Bearer ${sessionToken}`, 'X-File-Name-Encoded': encodeURIComponent(file.name), 'X-Uploader-Encoded': encodeURIComponent(currentUser?.name || '上传者') }, body: await file.arrayBuffer() }); const result = await response.json(); if (!response.ok || !result.ok) throw new Error(result.error || result.reply || '表格录入失败'); status.textContent = result.reply; summary.textContent = `扫描 ${result.summary?.files || 0} 个文件,收录 ${result.summary?.records || 0} 行,解析错误 ${result.summary?.errors || 0} 个。`; await loadLibraryFiles(); } catch (error) { status.textContent = '表格录入失败。'; summary.textContent = `失败原因:${error.message}`; } finally { event.target.value = ''; } }; document.querySelector('[data-view="files"]')?.addEventListener('click', () => loadLibraryFiles().catch((error) => { card.querySelector('#libraryFiles').innerHTML = `

${escLibrary(error.message)}

`; })); const libraryStyle = document.createElement('style'); libraryStyle.textContent = '.library-toolbar{display:grid;grid-template-columns:minmax(0,1fr) auto auto;gap:8px;margin:18px 0 10px}.library-toolbar .input{margin:0}.library-upload{display:flex;align-items:center}.library-status{padding:10px 12px;background:#f3f6f9;border-radius:6px}.library-grid{display:grid;grid-template-columns:minmax(280px,.8fr) minmax(0,1.2fr);gap:18px;margin-top:18px}.library-grid section{min-width:0}.library-list{max-height:470px;overflow:auto;border:1px solid #e1e7ee;border-radius:6px}.library-item{display:flex;flex-direction:column;gap:4px;padding:12px;border-bottom:1px solid #edf1f5}.library-item:last-child{border-bottom:0}.library-item small{color:#788696}.library-item span{color:#566575;word-break:break-all;font-size:12px}@media(max-width:900px){.library-grid{grid-template-columns:1fr}.library-toolbar{grid-template-columns:1fr auto}.library-upload{grid-column:1/3;justify-content:center}}'; document.head.appendChild(libraryStyle); } if (fileView) fileView.querySelector('.card').innerHTML = ''; })();