|
|
@@ -105,6 +105,7 @@
|
|
|
cursor: pointer;
|
|
|
transition: all 0.2s;
|
|
|
border: 2px solid transparent;
|
|
|
+ position: relative;
|
|
|
}
|
|
|
|
|
|
.user-item:hover {
|
|
|
@@ -121,6 +122,20 @@
|
|
|
font-weight: 600;
|
|
|
color: #333;
|
|
|
margin-bottom: 5px;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ }
|
|
|
+
|
|
|
+ .unread-badge {
|
|
|
+ background: #ff4444;
|
|
|
+ color: white;
|
|
|
+ font-size: 11px;
|
|
|
+ padding: 2px 6px;
|
|
|
+ border-radius: 10px;
|
|
|
+ font-weight: 600;
|
|
|
+ min-width: 18px;
|
|
|
+ text-align: center;
|
|
|
}
|
|
|
|
|
|
.user-openid {
|
|
|
@@ -310,6 +325,105 @@
|
|
|
transform: translateY(0);
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+ /* 消息历史模态框 */
|
|
|
+ .modal {
|
|
|
+ display: none;
|
|
|
+ position: fixed;
|
|
|
+ z-index: 1000;
|
|
|
+ left: 0;
|
|
|
+ top: 0;
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ background-color: rgba(0, 0, 0, 0.5);
|
|
|
+ animation: fadeIn 0.3s;
|
|
|
+ }
|
|
|
+
|
|
|
+ .modal.show {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ }
|
|
|
+
|
|
|
+ .modal-content {
|
|
|
+ background: white;
|
|
|
+ border-radius: 12px;
|
|
|
+ width: 90%;
|
|
|
+ max-width: 600px;
|
|
|
+ max-height: 80vh;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
|
|
|
+ }
|
|
|
+
|
|
|
+ .modal-header {
|
|
|
+ padding: 20px;
|
|
|
+ border-bottom: 1px solid #e0e0e0;
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ }
|
|
|
+
|
|
|
+ .modal-header h3 {
|
|
|
+ margin: 0;
|
|
|
+ color: #333;
|
|
|
+ }
|
|
|
+
|
|
|
+ .close-btn {
|
|
|
+ background: none;
|
|
|
+ border: none;
|
|
|
+ font-size: 24px;
|
|
|
+ color: #999;
|
|
|
+ cursor: pointer;
|
|
|
+ padding: 0;
|
|
|
+ width: 30px;
|
|
|
+ height: 30px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .close-btn:hover {
|
|
|
+ color: #333;
|
|
|
+ }
|
|
|
+
|
|
|
+ .modal-body {
|
|
|
+ flex: 1;
|
|
|
+ overflow-y: auto;
|
|
|
+ padding: 20px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .message-item {
|
|
|
+ margin-bottom: 15px;
|
|
|
+ padding: 12px;
|
|
|
+ background: #f8f9fa;
|
|
|
+ border-radius: 8px;
|
|
|
+ border-left: 3px solid #667eea;
|
|
|
+ }
|
|
|
+
|
|
|
+ .message-time {
|
|
|
+ font-size: 12px;
|
|
|
+ color: #888;
|
|
|
+ margin-bottom: 5px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .message-text {
|
|
|
+ color: #333;
|
|
|
+ line-height: 1.5;
|
|
|
+ }
|
|
|
+
|
|
|
+ .no-history {
|
|
|
+ text-align: center;
|
|
|
+ padding: 40px;
|
|
|
+ color: #999;
|
|
|
+ }
|
|
|
+
|
|
|
+ @keyframes fadeIn {
|
|
|
+ from {
|
|
|
+ opacity: 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ to {
|
|
|
+ opacity: 1;
|
|
|
+ }
|
|
|
+ }
|
|
|
</style>
|
|
|
</head>
|
|
|
|
|
|
@@ -354,18 +468,49 @@
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
+ <!-- 消息历史模态框 -->
|
|
|
+ <div class="modal" id="historyModal">
|
|
|
+ <div class="modal-content">
|
|
|
+ <div class="modal-header">
|
|
|
+ <h3 id="modalTitle">消息历史</h3>
|
|
|
+ <button class="close-btn" onclick="closeHistoryModal()">×</button>
|
|
|
+ </div>
|
|
|
+ <div class="modal-body" id="historyContent">
|
|
|
+ <div class="loading">加载中...</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
<script>
|
|
|
let currentPage = 1;
|
|
|
const pageSize = 10;
|
|
|
+ let unreadCounts = {};
|
|
|
let selectedUser = null;
|
|
|
let totalPages = 1;
|
|
|
let searchName = '';
|
|
|
|
|
|
- // 页面加载时获取用户列表
|
|
|
+ // 页面加载时获取用户列表和未读消息数量
|
|
|
window.onload = function () {
|
|
|
loadUsers();
|
|
|
+ loadUnreadCounts();
|
|
|
};
|
|
|
|
|
|
+ // 加载未读消息数量
|
|
|
+ async function loadUnreadCounts() {
|
|
|
+ try {
|
|
|
+ const response = await fetch('/wechat/api/message/unreadCounts');
|
|
|
+ if (response.ok) {
|
|
|
+ unreadCounts = await response.json();
|
|
|
+ // 重新渲染用户列表以显示未读徽章
|
|
|
+ if (document.querySelectorAll('.user-item').length > 0) {
|
|
|
+ loadUsers();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('加载未读数量失败:', error);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
// 加载用户列表
|
|
|
async function loadUsers() {
|
|
|
try {
|
|
|
@@ -402,12 +547,23 @@
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
- userList.innerHTML = users.map(user => `
|
|
|
- <div class="user-item" onclick="selectUser('${user.openId}', '${escapeHtml(user.name || '未知用户')}')">
|
|
|
- <div class="user-name">${escapeHtml(user.name || '未知用户')}</div>
|
|
|
- <div class="user-openid">${escapeHtml(user.openId)}</div>
|
|
|
- </div>
|
|
|
- `).join('');
|
|
|
+ userList.innerHTML = users.map(user => {
|
|
|
+ const unreadCount = unreadCounts[user.openId] || 0;
|
|
|
+ const unreadBadge = unreadCount > 0 ?
|
|
|
+ `<span class="unread-badge">${unreadCount}</span>` : '';
|
|
|
+
|
|
|
+ return `
|
|
|
+ <div class="user-item"
|
|
|
+ onclick="selectUser('${user.openId}', '${escapeHtml(user.name || '未知用户')}')"
|
|
|
+ ondblclick="viewMessageHistory('${user.openId}', '${escapeHtml(user.name || '未知用户')}')">
|
|
|
+ <div class="user-name">
|
|
|
+ <span>${escapeHtml(user.name || '未知用户')}</span>
|
|
|
+ ${unreadBadge}
|
|
|
+ </div>
|
|
|
+ <div class="user-openid">${escapeHtml(user.openId)}</div>
|
|
|
+ </div>
|
|
|
+ `;
|
|
|
+ }).join('');
|
|
|
}
|
|
|
|
|
|
// 选择用户
|
|
|
@@ -536,6 +692,86 @@
|
|
|
div.textContent = text;
|
|
|
return div.innerHTML;
|
|
|
}
|
|
|
+
|
|
|
+ // 查看用户的消息历史
|
|
|
+ async function viewMessageHistory(openId, name) {
|
|
|
+ const modal = document.getElementById('historyModal');
|
|
|
+ const modalTitle = document.getElementById('modalTitle');
|
|
|
+ const historyContent = document.getElementById('historyContent');
|
|
|
+
|
|
|
+ modalTitle.textContent = `${name} 的消息历史`;
|
|
|
+ historyContent.innerHTML = '<div class="loading">加载中...</div>';
|
|
|
+ modal.classList.add('show');
|
|
|
+
|
|
|
+ try {
|
|
|
+ const response = await fetch(`/wechat/api/message/history?openId=${openId}&limit=50`);
|
|
|
+
|
|
|
+ if (response.ok) {
|
|
|
+ const messages = await response.json();
|
|
|
+
|
|
|
+ if (messages.length === 0) {
|
|
|
+ historyContent.innerHTML = '<div class="no-history">暂无历史消息</div>';
|
|
|
+ } else {
|
|
|
+ historyContent.innerHTML = messages.map(msg => `
|
|
|
+ <div class="message-item">
|
|
|
+ <div class="message-time">${formatDateTime(msg.createTime)}</div>
|
|
|
+ <div class="message-text">${escapeHtml(msg.content)}</div>
|
|
|
+ </div>
|
|
|
+ `).join('');
|
|
|
+
|
|
|
+ // 标记消息为已读
|
|
|
+ await markAsRead(openId);
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ historyContent.innerHTML = '<div class="no-history">加载历史消息失败</div>';
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('加载历史消息失败:', error);
|
|
|
+ historyContent.innerHTML = '<div class="no-history">加载历史消息失败</div>';
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 关闭消息历史模态框
|
|
|
+ function closeHistoryModal() {
|
|
|
+ const modal = document.getElementById('historyModal');
|
|
|
+ modal.classList.remove('show');
|
|
|
+ }
|
|
|
+
|
|
|
+ // 点击模态框背景关闭
|
|
|
+ document.getElementById('historyModal')?.addEventListener('click', function (e) {
|
|
|
+ if (e.target === this) {
|
|
|
+ closeHistoryModal();
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // 标记消息为已读
|
|
|
+ async function markAsRead(openId) {
|
|
|
+ try {
|
|
|
+ const response = await fetch(`/wechat/api/message/markAsRead?openId=${openId}`, {
|
|
|
+ method: 'POST'
|
|
|
+ });
|
|
|
+
|
|
|
+ if (response.ok) {
|
|
|
+ // 更新未读数量
|
|
|
+ delete unreadCounts[openId];
|
|
|
+ loadUsers(); // 重新加载用户列表以更新徽章
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('标记已读失败:', error);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 格式化日期时间
|
|
|
+ function formatDateTime(dateTimeStr) {
|
|
|
+ if (!dateTimeStr) return '';
|
|
|
+ const date = new Date(dateTimeStr);
|
|
|
+ const year = date.getFullYear();
|
|
|
+ const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
|
+ const day = String(date.getDate()).padStart(2, '0');
|
|
|
+ const hours = String(date.getHours()).padStart(2, '0');
|
|
|
+ const minutes = String(date.getMinutes()).padStart(2, '0');
|
|
|
+ return `${year}-${month}-${day} ${hours}:${minutes}`;
|
|
|
+ }
|
|
|
</script>
|
|
|
</body>
|
|
|
|