(() => {
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 = `
`;
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=``; 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)} `).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) => `
`).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) => `| ${user.name} | ${user.role} | ${user.enabled === false ? '停用' : '启用'} | ${user.name === '伊青荣' ? '' : ``} |
`).join('')}
`; 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) => `
`).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 = `${pinned ? '取消置顶' : '置顶'}标为已读允许消息通知收进“折叠的聊天”独立窗口显示不显示`; 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 = `${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) => `${item.type === 'group' ? '群 · ' : ''}${item.name}`).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 = `${document.querySelector(`[data-key="${CSS.escape(key)}"]`)?.textContent || '置顶'}标为已读${'允许消息通知'}不显示删除`; 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 = '';
})();