更改消息时间
This commit is contained in:
@@ -18,7 +18,7 @@
|
||||
.icon-btn svg { width: 22px; height: 22px; stroke: #fff; fill: none; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
|
||||
|
||||
.chat-list { flex: 1; overflow-y: auto; padding-bottom: 8px; }
|
||||
.chat-item { padding: 14px 16px; border-bottom: 1px solid #f0f0f0; display: flex; align-items: center; cursor: pointer; }
|
||||
.chat-item { padding: 14px 16px; border-bottom: 1px solid #f0f0f0; display: flex; align-items: center; cursor: pointer; position: relative; }
|
||||
.chat-item:active { background: #f9f9f9; }
|
||||
.avatar { width: 44px; height: 44px; border-radius: 50%; background: #e0e0e0; margin-right: 12px; display: flex; align-items: center; justify-content: center; font-size: 18px; font-weight: 600; color: #555; flex-shrink: 0; }
|
||||
.chat-info { flex: 1; min-width: 0; }
|
||||
@@ -26,6 +26,12 @@
|
||||
.last-msg { font-size: 14px; color: #888; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.chat-time { font-size: 12px; color: #aaa; margin-left: 8px; white-space: nowrap; }
|
||||
|
||||
.edit-room-btn {
|
||||
position: absolute; right: 40px; top: 50%; transform: translateY(-50%);
|
||||
background: none; border: none; cursor: pointer; display: flex; align-items: center; padding: 8px;
|
||||
}
|
||||
.edit-room-btn svg { width: 18px; height: 18px; stroke: #888; fill: none; stroke-width: 2; }
|
||||
|
||||
.summary-card { background: #f0f7ff; margin: 8px; border-radius: 12px; padding: 14px 16px; cursor: pointer; display: flex; justify-content: space-between; align-items: center; }
|
||||
.summary-card h4 { font-size: 16px; font-weight: 500; }
|
||||
.summary-card .summary-preview { font-size: 14px; color: #555; }
|
||||
@@ -174,6 +180,20 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 管理群聊弹窗 -->
|
||||
<div id="manage-modal" class="modal-overlay hidden">
|
||||
<div class="modal">
|
||||
<button class="modal-close-btn" onclick="closeManageModal()">
|
||||
<svg viewBox="0 0 24 24"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
|
||||
</button>
|
||||
<h3 id="manage-title">编辑群聊</h3>
|
||||
<input type="text" id="manage-room-name" placeholder="群聊名称">
|
||||
<input type="text" id="manage-white-list" placeholder="白名单成员(逗号分隔)">
|
||||
<button class="primary-btn" onclick="saveRoom()">保存</button>
|
||||
<button class="close-modal" style="background:#e53e3e;color:#fff;margin-top:8px;" onclick="deleteRoom()">删除群聊</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 采购详情弹窗 -->
|
||||
<div id="detail-modal" class="modal-overlay hidden">
|
||||
<div class="modal">
|
||||
@@ -211,7 +231,6 @@
|
||||
marked.setOptions({ breaks: true, gfm: true, sanitize: false });
|
||||
}
|
||||
|
||||
// 图片压缩
|
||||
function compressImage(file) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
@@ -219,14 +238,9 @@
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
const canvas = document.createElement('canvas');
|
||||
let width = img.width;
|
||||
let height = img.height;
|
||||
if (width > 1200) {
|
||||
height = height * (1200 / width);
|
||||
width = 1200;
|
||||
}
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
let width = img.width, height = img.height;
|
||||
if (width > 1200) { height = height * (1200 / width); width = 1200; }
|
||||
canvas.width = width; canvas.height = height;
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.drawImage(img, 0, 0, width, height);
|
||||
canvas.toBlob((blob) => resolve(blob), 'image/jpeg', 0.8);
|
||||
@@ -237,14 +251,12 @@
|
||||
});
|
||||
}
|
||||
|
||||
// HTML 转义
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
// 渲染单条消息 HTML
|
||||
function renderMessageHTML(msg) {
|
||||
const currentUser = localStorage.getItem('currentUser');
|
||||
const isMe = msg.user === currentUser;
|
||||
@@ -272,21 +284,18 @@
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// 全量渲染(切换房间时)
|
||||
function renderAllMessages(msgs) {
|
||||
messageCache = msgs || [];
|
||||
messagesContainer.innerHTML = messageCache.map(renderMessageHTML).join('');
|
||||
scrollToBottom();
|
||||
}
|
||||
|
||||
// 追加一条消息
|
||||
function appendMessage(msg) {
|
||||
messageCache.push(msg);
|
||||
messagesContainer.insertAdjacentHTML('beforeend', renderMessageHTML(msg));
|
||||
scrollToBottom();
|
||||
}
|
||||
|
||||
// 替换指定消息(占位更新)
|
||||
function replaceMessage(msgId, newMsg) {
|
||||
const idx = messageCache.findIndex(m => m.id == msgId);
|
||||
if (idx !== -1) {
|
||||
@@ -298,11 +307,9 @@
|
||||
}
|
||||
}
|
||||
|
||||
function scrollToBottom() {
|
||||
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
||||
}
|
||||
function scrollToBottom() { messagesContainer.scrollTop = messagesContainer.scrollHeight; }
|
||||
|
||||
// WebSocket 及重连
|
||||
// WebSocket
|
||||
let wsReconnectTimer = null;
|
||||
let reconnectAttempts = 0;
|
||||
const MAX_RECONNECT_DELAY = 30000;
|
||||
@@ -321,22 +328,23 @@
|
||||
const data = JSON.parse(e.data);
|
||||
if (data.type === 'new_message') {
|
||||
const msg = data.message;
|
||||
if (currentRoom === msg.room_id) {
|
||||
appendMessage(msg);
|
||||
}
|
||||
if (currentRoom === msg.room_id) appendMessage(msg);
|
||||
if (data.room_preview) {
|
||||
const room = rooms.find(r => r.id === data.room_preview.room_id);
|
||||
if (room) {
|
||||
room.last_message = data.room_preview.last_message;
|
||||
room.last_time = data.room_preview.last_time;
|
||||
renderChatList();
|
||||
}
|
||||
if (room) { room.last_message = data.room_preview.last_message; room.last_time = data.room_preview.last_time; renderChatList(); }
|
||||
}
|
||||
} else if (data.type === 'purchase_updated') {
|
||||
if (currentRoom) loadPurchases();
|
||||
updateSummaryPreview();
|
||||
} else if (data.type === 'room_created') {
|
||||
loadRooms();
|
||||
} else if (data.type === 'room_updated') {
|
||||
const room = rooms.find(r => r.id === data.room.id);
|
||||
if (room) { room.name = data.room.name; room.white_list = data.room.white_list; renderChatList(); }
|
||||
} else if (data.type === 'room_deleted') {
|
||||
rooms = rooms.filter(r => r.id !== data.roomId);
|
||||
renderChatList();
|
||||
if (currentRoom === data.roomId) showList();
|
||||
}
|
||||
};
|
||||
ws.onerror = (e) => console.error('WebSocket 错误', e);
|
||||
@@ -351,10 +359,7 @@
|
||||
if (wsReconnectTimer) clearTimeout(wsReconnectTimer);
|
||||
const delay = Math.min(1000 * Math.pow(2, reconnectAttempts), MAX_RECONNECT_DELAY);
|
||||
reconnectAttempts++;
|
||||
wsReconnectTimer = setTimeout(() => {
|
||||
console.log('尝试重连...');
|
||||
initWebSocket();
|
||||
}, delay);
|
||||
wsReconnectTimer = setTimeout(() => { console.log('尝试重连...'); initWebSocket(); }, delay);
|
||||
}
|
||||
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
@@ -366,7 +371,6 @@
|
||||
}
|
||||
});
|
||||
|
||||
// 初始化
|
||||
async function init() {
|
||||
const token = getToken();
|
||||
if (token) {
|
||||
@@ -413,7 +417,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
// 群聊列表
|
||||
async function loadRooms() {
|
||||
const token = getToken();
|
||||
const res = await fetch(API + '/api/rooms', { headers: { 'Authorization': `Bearer ${token}` } });
|
||||
@@ -424,17 +427,22 @@
|
||||
|
||||
function renderChatList() {
|
||||
const container = document.getElementById('chat-list-container');
|
||||
const isAdmin = localStorage.getItem('isAdmin') === 'true';
|
||||
container.innerHTML = rooms.map(room => {
|
||||
const lastMsg = room.last_message || '暂无消息';
|
||||
const lastTime = room.last_time || '';
|
||||
return `
|
||||
<div class="chat-item" onclick="openChat('${room.id}')">
|
||||
<div class="avatar">${room.name.charAt(0)}</div>
|
||||
<div class="chat-info">
|
||||
<div class="chat-item" data-room-id="${room.id}">
|
||||
<div class="avatar" onclick="openChat('${room.id}')">${room.name.charAt(0)}</div>
|
||||
<div class="chat-info" onclick="openChat('${room.id}')">
|
||||
<div class="chat-name">${room.name}</div>
|
||||
<div class="last-msg">${escapeHtml(lastMsg)}</div>
|
||||
</div>
|
||||
<div class="chat-time">${lastTime}</div>
|
||||
<div class="chat-time" onclick="openChat('${room.id}')">${lastTime}</div>
|
||||
${isAdmin ? `
|
||||
<button class="edit-room-btn" onclick="event.stopPropagation(); openManageRoom('${room.id}')" title="编辑群聊">
|
||||
<svg viewBox="0 0 24 24"><path d="M12 20h9"></path><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"></path></svg>
|
||||
</button>` : ''}
|
||||
</div>`;
|
||||
}).join('');
|
||||
}
|
||||
@@ -451,7 +459,6 @@
|
||||
} catch(e) {}
|
||||
}
|
||||
|
||||
// 进入聊天
|
||||
async function openChat(roomId, autoOpenPurchase = false) {
|
||||
currentRoom = roomId;
|
||||
document.getElementById('list-page').classList.add('hidden');
|
||||
@@ -459,9 +466,7 @@
|
||||
const room = rooms.find(r => r.id === roomId);
|
||||
document.getElementById('current-chat-name').textContent = room?.name || '';
|
||||
document.getElementById('purchase-panel-overlay').classList.add('hidden');
|
||||
if (autoOpenPurchase) {
|
||||
setTimeout(() => openPurchasePanel(), 200);
|
||||
}
|
||||
if (autoOpenPurchase) setTimeout(() => openPurchasePanel(), 200);
|
||||
const token = getToken();
|
||||
const res = await fetch(API + `/api/rooms/${roomId}/messages`, { headers: { 'Authorization': `Bearer ${token}` } });
|
||||
const msgs = await res.json();
|
||||
@@ -477,7 +482,6 @@
|
||||
updateSummaryPreview();
|
||||
}
|
||||
|
||||
// 发送文本消息
|
||||
async function sendMessage() {
|
||||
const input = document.getElementById('msg-input');
|
||||
const text = input.value.trim();
|
||||
@@ -494,45 +498,31 @@
|
||||
appendMessage(newMsg);
|
||||
input.value = '';
|
||||
pendingUploads = [];
|
||||
} catch(e) {
|
||||
alert('发送失败,请重试');
|
||||
}
|
||||
} catch(e) { alert('发送失败,请重试'); }
|
||||
}
|
||||
|
||||
// 图片选择上传
|
||||
async function handleFileSelect(event) {
|
||||
const files = event.target.files;
|
||||
if (!files.length) return;
|
||||
const token = getToken();
|
||||
const placeholderId = 'placeholder_' + Date.now();
|
||||
const placeholderMsg = {
|
||||
id: placeholderId,
|
||||
room_id: currentRoom,
|
||||
user: localStorage.getItem('currentUser'),
|
||||
text: '图片发送中...',
|
||||
attachments: [],
|
||||
timestamp: new Date().toLocaleTimeString('zh-CN', { hour12: false }),
|
||||
isPlaceholder: true
|
||||
id: placeholderId, room_id: currentRoom, user: localStorage.getItem('currentUser'),
|
||||
text: '图片发送中...', attachments: [], timestamp: new Date().toLocaleTimeString('zh-CN', { hour12: false }), isPlaceholder: true
|
||||
};
|
||||
appendMessage(placeholderMsg);
|
||||
|
||||
try {
|
||||
for (let file of files) {
|
||||
let blob = file;
|
||||
if (file.size > 1 * 1024 * 1024) blob = await compressImage(file);
|
||||
const formData = new FormData();
|
||||
formData.append('file', blob, file.name || 'image.jpg');
|
||||
const res = await fetch(API + '/api/upload', {
|
||||
method: 'POST', headers: { 'Authorization': `Bearer ${token}` }, body: formData
|
||||
});
|
||||
const formData = new FormData(); formData.append('file', blob, file.name || 'image.jpg');
|
||||
const res = await fetch(API + '/api/upload', { method: 'POST', headers: { 'Authorization': `Bearer ${token}` }, body: formData });
|
||||
const data = await res.json();
|
||||
if (data.path) pendingUploads.push(data.path);
|
||||
}
|
||||
// 移除占位
|
||||
messageCache = messageCache.filter(m => m.id !== placeholderId);
|
||||
const placeholderEl = messagesContainer.querySelector(`[data-msg-id="${placeholderId}"]`);
|
||||
if (placeholderEl) placeholderEl.remove();
|
||||
// 发送正式图片消息
|
||||
sendMessage();
|
||||
} catch(e) {
|
||||
messageCache = messageCache.filter(m => m.id !== placeholderId);
|
||||
@@ -542,7 +532,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
// 采购清单
|
||||
async function loadPurchases() {
|
||||
if (!currentRoom) return;
|
||||
const token = getToken();
|
||||
@@ -553,10 +542,7 @@
|
||||
|
||||
function renderPurchasePanel(items) {
|
||||
const container = document.getElementById('purchase-panel-body');
|
||||
if (items.length === 0) {
|
||||
container.innerHTML = '<p style="text-align:center;color:#888;padding:40px;">暂无采购记录</p>';
|
||||
return;
|
||||
}
|
||||
if (items.length === 0) { container.innerHTML = '<p style="text-align:center;color:#888;padding:40px;">暂无采购记录</p>'; return; }
|
||||
const groups = {};
|
||||
items.forEach(item => {
|
||||
const month = formatMonth(item.created_at);
|
||||
@@ -567,19 +553,14 @@
|
||||
Object.keys(groups).sort().reverse().forEach(month => {
|
||||
const monthItems = groups[month];
|
||||
const monthTotal = monthItems.reduce((s, i) => s + (i.amount||0), 0);
|
||||
html += `<div class="purchase-month-group">
|
||||
<div class="purchase-month-header"><span>${month}</span><span>¥${monthTotal}</span></div>`;
|
||||
html += `<div class="purchase-month-group"><div class="purchase-month-header"><span>${month}</span><span>¥${monthTotal}</span></div>`;
|
||||
monthItems.forEach(item => {
|
||||
const timeShort = item.created_at.split(' ')[1]?.substring(0,5) || '';
|
||||
const dateShort = item.created_at.split(' ')[0]?.substring(5) || '';
|
||||
html += `
|
||||
<div class="purchase-item" onclick="openDetail('${item.id}')">
|
||||
<div class="item-main"><span class="item-name">${item.item}</span><span class="item-amount">¥${item.amount}</span></div>
|
||||
<div class="item-meta">
|
||||
<span>${dateShort} ${timeShort}</span>
|
||||
<span>${item.applicant || ''} / ${item.method || ''}</span>
|
||||
<span class="status-badge ${item.status==='已完成'?'done':''}">${item.status}</span>
|
||||
</div>
|
||||
<div class="item-meta"><span>${dateShort} ${timeShort}</span><span>${item.applicant || ''} / ${item.method || ''}</span><span class="status-badge ${item.status==='已完成'?'done':''}">${item.status}</span></div>
|
||||
</div>`;
|
||||
});
|
||||
html += '</div>';
|
||||
@@ -589,11 +570,7 @@
|
||||
|
||||
function formatMonth(dateStr) {
|
||||
const parts = dateStr.split('/');
|
||||
if (parts.length >= 2) {
|
||||
const year = parts[0];
|
||||
const month = parseInt(parts[1], 10);
|
||||
return `${year}年${month}月`;
|
||||
}
|
||||
if (parts.length >= 2) return `${parts[0]}年${parseInt(parts[1], 10)}月`;
|
||||
return dateStr.substring(0,7);
|
||||
}
|
||||
|
||||
@@ -602,10 +579,7 @@
|
||||
document.getElementById('purchase-panel-overlay').classList.remove('hidden');
|
||||
loadPurchases();
|
||||
}
|
||||
|
||||
function closePurchasePanel() {
|
||||
document.getElementById('purchase-panel-overlay').classList.add('hidden');
|
||||
}
|
||||
function closePurchasePanel() { document.getElementById('purchase-panel-overlay').classList.add('hidden'); }
|
||||
|
||||
async function openDetail(pid) {
|
||||
const token = getToken();
|
||||
@@ -613,19 +587,11 @@
|
||||
const p = await res.json();
|
||||
document.getElementById('detail-title').textContent = p.item;
|
||||
let attachHtml = '';
|
||||
if (p.attachments?.length) {
|
||||
attachHtml = '<div class="attachments">' + p.attachments.map(a => `<img class="img-thumb" src="${a.file_path}" alt="${a.file_path}">`).join('') + '</div>';
|
||||
}
|
||||
if (p.attachments?.length) attachHtml = '<div class="attachments">' + p.attachments.map(a => `<img class="img-thumb" src="${a.file_path}" alt="${a.file_path}">`).join('') + '</div>';
|
||||
const historyHtml = p.history?.map(h => `<li><span class="history-time">${h.timestamp}</span> ${h.user}: ${h.action}</li>`).join('') || '';
|
||||
document.getElementById('detail-content').innerHTML = `
|
||||
<p><strong>金额:</strong>¥${p.amount} | <strong>方式:</strong>${p.method}</p>
|
||||
<p><strong>申请人:</strong>${p.applicant}</p>
|
||||
${attachHtml}
|
||||
<h4 style="margin-top:12px;">操作历史</h4>
|
||||
<ul class="history-list">${historyHtml}</ul>`;
|
||||
document.getElementById('detail-content').innerHTML = `<p><strong>金额:</strong>¥${p.amount} | <strong>方式:</strong>${p.method}</p><p><strong>申请人:</strong>${p.applicant}</p>${attachHtml}<h4 style="margin-top:12px;">操作历史</h4><ul class="history-list">${historyHtml}</ul>`;
|
||||
document.getElementById('detail-modal').classList.remove('hidden');
|
||||
}
|
||||
|
||||
function closeDetailModal() { document.getElementById('detail-modal').classList.add('hidden'); }
|
||||
|
||||
async function openSummary() {
|
||||
@@ -636,10 +602,7 @@
|
||||
if (summary.length === 0) html = '<p>暂无采购记录</p>';
|
||||
else {
|
||||
summary.forEach(s => {
|
||||
html += `<div class="summary-card" onclick="event.stopPropagation(); closeDetailModal(); openChat('${s.room_id}', true)" style="margin:8px 0;">
|
||||
<h4>${s.room_name}</h4>
|
||||
<p>${s.purchase_count} 条,合计 ¥${s.total_amount}</p>
|
||||
</div>`;
|
||||
html += `<div class="summary-card" onclick="event.stopPropagation(); closeDetailModal(); openChat('${s.room_id}', true)" style="margin:8px 0;"><h4>${s.room_name}</h4><p>${s.purchase_count} 条,合计 ¥${s.total_amount}</p></div>`;
|
||||
});
|
||||
}
|
||||
document.getElementById('detail-title').textContent = '采购汇总';
|
||||
@@ -654,20 +617,49 @@
|
||||
if (!name) return alert('请输入名称');
|
||||
const whiteList = document.getElementById('white-list').value.trim();
|
||||
const token = getToken();
|
||||
await fetch(API + '/api/rooms', {
|
||||
method: 'POST',
|
||||
headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name, whiteList })
|
||||
});
|
||||
await fetch(API + '/api/rooms', { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ name, whiteList }) });
|
||||
closeCreateModal();
|
||||
loadRooms();
|
||||
}
|
||||
|
||||
// 群聊管理
|
||||
function openManageRoom(roomId) {
|
||||
const room = rooms.find(r => r.id === roomId);
|
||||
if (!room) return;
|
||||
document.getElementById('manage-title').textContent = '编辑群聊';
|
||||
document.getElementById('manage-room-name').value = room.name;
|
||||
document.getElementById('manage-white-list').value = room.white_list || '';
|
||||
document.getElementById('manage-modal').dataset.roomId = roomId;
|
||||
document.getElementById('manage-modal').classList.remove('hidden');
|
||||
}
|
||||
function closeManageModal() { document.getElementById('manage-modal').classList.add('hidden'); }
|
||||
async function saveRoom() {
|
||||
const roomId = document.getElementById('manage-modal').dataset.roomId;
|
||||
const name = document.getElementById('manage-room-name').value.trim();
|
||||
const whiteList = document.getElementById('manage-white-list').value.trim();
|
||||
if (!name) return alert('请输入群聊名称');
|
||||
const token = getToken();
|
||||
const res = await fetch(API + '/api/rooms/' + roomId, {
|
||||
method: 'PUT', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name, whiteList })
|
||||
});
|
||||
if (res.ok) { closeManageModal(); loadRooms(); }
|
||||
else { const err = await res.json(); alert(err.error || '更新失败'); }
|
||||
}
|
||||
async function deleteRoom() {
|
||||
if (!confirm('确定要删除该群聊吗?所有消息和采购数据将被永久删除。')) return;
|
||||
const roomId = document.getElementById('manage-modal').dataset.roomId;
|
||||
const token = getToken();
|
||||
const res = await fetch(API + '/api/rooms/' + roomId, { method: 'DELETE', headers: { 'Authorization': `Bearer ${token}` } });
|
||||
if (res.ok) {
|
||||
closeManageModal();
|
||||
loadRooms();
|
||||
if (currentRoom === roomId) showList();
|
||||
} else { const err = await res.json(); alert(err.error || '删除失败'); }
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.target.id === 'msg-input' && e.key === 'Enter' && !e.ctrlKey) {
|
||||
e.preventDefault();
|
||||
sendMessage();
|
||||
}
|
||||
if (e.target.id === 'msg-input' && e.key === 'Enter' && !e.ctrlKey) { e.preventDefault(); sendMessage(); }
|
||||
});
|
||||
|
||||
init();
|
||||
|
||||
Reference in New Issue
Block a user