send_message.html 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778
  1. <!DOCTYPE html>
  2. <html lang="zh-CN">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6. <title>消息发送管理</title>
  7. <style>
  8. * {
  9. margin: 0;
  10. padding: 0;
  11. box-sizing: border-box;
  12. }
  13. body {
  14. font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
  15. background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  16. min-height: 100vh;
  17. display: flex;
  18. align-items: center;
  19. justify-content: center;
  20. padding: 20px;
  21. }
  22. .container {
  23. max-width: 1400px;
  24. width: 95%;
  25. margin: 0 auto;
  26. background: white;
  27. border-radius: 12px;
  28. box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
  29. overflow: hidden;
  30. display: flex;
  31. min-height: 700px;
  32. }
  33. /* 左侧用户列表区域 */
  34. .user-panel {
  35. width: 400px;
  36. background: #f8f9fa;
  37. border-right: 1px solid #e0e0e0;
  38. display: flex;
  39. flex-direction: column;
  40. overflow: hidden;
  41. }
  42. .user-panel-header {
  43. padding: 20px;
  44. background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  45. color: white;
  46. }
  47. .user-panel-header h2 {
  48. font-size: 20px;
  49. margin-bottom: 15px;
  50. }
  51. .search-box {
  52. position: relative;
  53. }
  54. .search-box input {
  55. width: 100%;
  56. padding: 10px 40px 10px 15px;
  57. border: none;
  58. border-radius: 6px;
  59. font-size: 14px;
  60. outline: none;
  61. transition: all 0.3s;
  62. }
  63. .search-box input:focus {
  64. box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.3);
  65. }
  66. .search-box button {
  67. position: absolute;
  68. right: 5px;
  69. top: 50%;
  70. transform: translateY(-50%);
  71. background: #667eea;
  72. color: white;
  73. border: none;
  74. padding: 6px 12px;
  75. border-radius: 4px;
  76. cursor: pointer;
  77. font-size: 13px;
  78. }
  79. .search-box button:hover {
  80. background: #5568d3;
  81. }
  82. .user-list-container {
  83. flex: 1;
  84. overflow-y: auto;
  85. padding: 10px;
  86. }
  87. .user-item {
  88. padding: 15px;
  89. background: white;
  90. margin-bottom: 8px;
  91. border-radius: 8px;
  92. cursor: pointer;
  93. transition: all 0.2s;
  94. border: 2px solid transparent;
  95. position: relative;
  96. }
  97. .user-item:hover {
  98. transform: translateX(5px);
  99. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  100. }
  101. .user-item.active {
  102. border-color: #667eea;
  103. background: #f0f3ff;
  104. }
  105. .user-name {
  106. font-weight: 600;
  107. color: #333;
  108. margin-bottom: 5px;
  109. display: flex;
  110. align-items: center;
  111. justify-content: space-between;
  112. }
  113. .unread-badge {
  114. background: #ff4444;
  115. color: white;
  116. font-size: 11px;
  117. padding: 2px 6px;
  118. border-radius: 10px;
  119. font-weight: 600;
  120. min-width: 18px;
  121. text-align: center;
  122. }
  123. .user-openid {
  124. font-size: 12px;
  125. color: #888;
  126. word-break: break-all;
  127. }
  128. .pagination {
  129. padding: 15px;
  130. display: flex;
  131. justify-content: space-between;
  132. align-items: center;
  133. border-top: 1px solid #e0e0e0;
  134. background: white;
  135. }
  136. .pagination button {
  137. padding: 8px 16px;
  138. background: #667eea;
  139. color: white;
  140. border: none;
  141. border-radius: 4px;
  142. cursor: pointer;
  143. font-size: 13px;
  144. }
  145. .pagination button:disabled {
  146. background: #ccc;
  147. cursor: not-allowed;
  148. }
  149. .pagination button:hover:not(:disabled) {
  150. background: #5568d3;
  151. }
  152. .page-info {
  153. font-size: 13px;
  154. color: #666;
  155. }
  156. /* 右侧消息输入区域 */
  157. .message-panel {
  158. flex: 1;
  159. display: flex;
  160. flex-direction: column;
  161. padding: 30px;
  162. }
  163. .message-panel h2 {
  164. color: #333;
  165. margin-bottom: 20px;
  166. font-size: 24px;
  167. }
  168. .selected-user-info {
  169. background: #f0f3ff;
  170. padding: 15px;
  171. border-radius: 8px;
  172. margin-bottom: 20px;
  173. border-left: 4px solid #667eea;
  174. }
  175. .selected-user-info p {
  176. margin: 5px 0;
  177. color: #555;
  178. }
  179. .selected-user-info strong {
  180. color: #333;
  181. }
  182. .message-input-area {
  183. flex: 1;
  184. display: flex;
  185. flex-direction: column;
  186. }
  187. .message-input-area label {
  188. font-weight: 600;
  189. color: #333;
  190. margin-bottom: 10px;
  191. display: block;
  192. }
  193. .message-input-area textarea {
  194. flex: 1;
  195. padding: 15px;
  196. border: 2px solid #e0e0e0;
  197. border-radius: 8px;
  198. font-size: 14px;
  199. font-family: inherit;
  200. resize: none;
  201. outline: none;
  202. transition: border-color 0.3s;
  203. }
  204. .message-input-area textarea:focus {
  205. border-color: #667eea;
  206. }
  207. .template-option {
  208. margin-top: 15px;
  209. padding: 12px;
  210. background: #f8f9fa;
  211. border-radius: 6px;
  212. display: flex;
  213. align-items: center;
  214. }
  215. .template-option input[type="checkbox"] {
  216. width: 18px;
  217. height: 18px;
  218. margin-right: 10px;
  219. cursor: pointer;
  220. }
  221. .template-option label {
  222. cursor: pointer;
  223. margin: 0;
  224. color: #555;
  225. font-weight: 500;
  226. }
  227. .send-button {
  228. margin-top: 20px;
  229. padding: 15px 30px;
  230. background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  231. color: white;
  232. border: none;
  233. border-radius: 8px;
  234. font-size: 16px;
  235. font-weight: 600;
  236. cursor: pointer;
  237. transition: all 0.3s;
  238. }
  239. .send-button:hover:not(:disabled) {
  240. transform: translateY(-2px);
  241. box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
  242. }
  243. .send-button:disabled {
  244. background: #ccc;
  245. cursor: not-allowed;
  246. }
  247. .empty-state {
  248. text-align: center;
  249. padding: 40px 20px;
  250. color: #999;
  251. }
  252. .loading {
  253. text-align: center;
  254. padding: 20px;
  255. color: #667eea;
  256. }
  257. .alert {
  258. padding: 12px 20px;
  259. border-radius: 6px;
  260. margin-bottom: 20px;
  261. animation: slideDown 0.3s;
  262. }
  263. .alert-success {
  264. background: #d4edda;
  265. color: #155724;
  266. border: 1px solid #c3e6cb;
  267. }
  268. .alert-error {
  269. background: #f8d7da;
  270. color: #721c24;
  271. border: 1px solid #f5c6cb;
  272. }
  273. @keyframes slideDown {
  274. from {
  275. opacity: 0;
  276. transform: translateY(-10px);
  277. }
  278. to {
  279. opacity: 1;
  280. transform: translateY(0);
  281. }
  282. }
  283. /* 消息历史模态框 */
  284. .modal {
  285. display: none;
  286. position: fixed;
  287. z-index: 1000;
  288. left: 0;
  289. top: 0;
  290. width: 100%;
  291. height: 100%;
  292. background-color: rgba(0, 0, 0, 0.5);
  293. animation: fadeIn 0.3s;
  294. }
  295. .modal.show {
  296. display: flex;
  297. align-items: center;
  298. justify-content: center;
  299. }
  300. .modal-content {
  301. background: white;
  302. border-radius: 12px;
  303. width: 90%;
  304. max-width: 600px;
  305. max-height: 80vh;
  306. display: flex;
  307. flex-direction: column;
  308. box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
  309. }
  310. .modal-header {
  311. padding: 20px;
  312. border-bottom: 1px solid #e0e0e0;
  313. display: flex;
  314. justify-content: space-between;
  315. align-items: center;
  316. }
  317. .modal-header h3 {
  318. margin: 0;
  319. color: #333;
  320. }
  321. .close-btn {
  322. background: none;
  323. border: none;
  324. font-size: 24px;
  325. color: #999;
  326. cursor: pointer;
  327. padding: 0;
  328. width: 30px;
  329. height: 30px;
  330. }
  331. .close-btn:hover {
  332. color: #333;
  333. }
  334. .modal-body {
  335. flex: 1;
  336. overflow-y: auto;
  337. padding: 20px;
  338. }
  339. .message-item {
  340. margin-bottom: 15px;
  341. padding: 12px;
  342. background: #f8f9fa;
  343. border-radius: 8px;
  344. border-left: 3px solid #667eea;
  345. }
  346. .message-time {
  347. font-size: 12px;
  348. color: #888;
  349. margin-bottom: 5px;
  350. }
  351. .message-text {
  352. color: #333;
  353. line-height: 1.5;
  354. }
  355. .no-history {
  356. text-align: center;
  357. padding: 40px;
  358. color: #999;
  359. }
  360. @keyframes fadeIn {
  361. from {
  362. opacity: 0;
  363. }
  364. to {
  365. opacity: 1;
  366. }
  367. }
  368. </style>
  369. </head>
  370. <body>
  371. <div class="container">
  372. <!-- 左侧用户列表 -->
  373. <div class="user-panel">
  374. <div class="user-panel-header">
  375. <h2>用户列表</h2>
  376. <div class="search-box">
  377. <input type="text" id="searchInput" placeholder="搜索用户名称...">
  378. <button onclick="searchUsers()">搜索</button>
  379. </div>
  380. </div>
  381. <div class="user-list-container" id="userList">
  382. <div class="loading">加载中...</div>
  383. </div>
  384. <div class="pagination">
  385. <button id="prevBtn" onclick="previousPage()" disabled>上一页</button>
  386. <span class="page-info" id="pageInfo">第 1 页</span>
  387. <button id="nextBtn" onclick="nextPage()">下一页</button>
  388. </div>
  389. </div>
  390. <!-- 右侧消息输入 -->
  391. <div class="message-panel">
  392. <h2>发送消息</h2>
  393. <div id="alertContainer"></div>
  394. <div id="selectedUserInfo" style="display: none;" class="selected-user-info">
  395. <p><strong>选中用户:</strong> <span id="selectedUserName"></span></p>
  396. <p><strong>OpenID:</strong> <span id="selectedUserOpenId"></span></p>
  397. </div>
  398. <div class="message-input-area">
  399. <label for="messageContent">消息内容</label>
  400. <textarea id="messageContent" placeholder="请输入要发送的消息..."></textarea>
  401. </div>
  402. <div class="template-option">
  403. <input type="checkbox" id="useTemplate">
  404. <label for="useTemplate">发送模板消息(将使用预设模板发送)</label>
  405. </div>
  406. <button class="send-button" id="sendBtn" onclick="sendMessage()" disabled>发送消息</button>
  407. </div>
  408. </div>
  409. <!-- 消息历史模态框 -->
  410. <div class="modal" id="historyModal">
  411. <div class="modal-content">
  412. <div class="modal-header">
  413. <h3 id="modalTitle">消息历史</h3>
  414. <button class="close-btn" onclick="closeHistoryModal()">&times;</button>
  415. </div>
  416. <div class="modal-body" id="historyContent">
  417. <div class="loading">加载中...</div>
  418. </div>
  419. </div>
  420. </div>
  421. <script>
  422. let currentPage = 1;
  423. const pageSize = 10;
  424. let unreadCounts = {};
  425. let selectedUser = null;
  426. let totalPages = 1;
  427. let searchName = '';
  428. // 页面加载时获取用户列表和未读消息数量
  429. window.onload = function () {
  430. loadUsers();
  431. loadUnreadCounts();
  432. };
  433. // 加载未读消息数量
  434. async function loadUnreadCounts() {
  435. try {
  436. const response = await fetch('/wechat/api/message/unreadCounts');
  437. if (response.ok) {
  438. unreadCounts = await response.json();
  439. // 重新渲染用户列表以显示未读徽章
  440. if (document.querySelectorAll('.user-item').length > 0) {
  441. loadUsers();
  442. }
  443. }
  444. } catch (error) {
  445. console.error('加载未读数量失败:', error);
  446. }
  447. }
  448. // 加载用户列表
  449. async function loadUsers() {
  450. try {
  451. const params = new URLSearchParams({
  452. page: currentPage,
  453. size: pageSize
  454. });
  455. if (searchName) {
  456. params.append('name', searchName);
  457. }
  458. const response = await fetch(`/wechat/api/users?${params}`);
  459. const data = await response.json();
  460. if (response.ok) {
  461. displayUsers(data.records || []);
  462. totalPages = data.pages || 1;
  463. updatePagination();
  464. } else {
  465. showError('加载用户列表失败');
  466. }
  467. } catch (error) {
  468. console.error('加载用户列表出错:', error);
  469. showError('加载用户列表出错: ' + error.message);
  470. }
  471. }
  472. // 显示用户列表
  473. function displayUsers(users) {
  474. const userList = document.getElementById('userList');
  475. if (users.length === 0) {
  476. userList.innerHTML = '<div class="empty-state">暂无用户数据</div>';
  477. return;
  478. }
  479. userList.innerHTML = users.map(user => {
  480. const unreadCount = unreadCounts[user.openId] || 0;
  481. const unreadBadge = unreadCount > 0 ?
  482. `<span class="unread-badge">${unreadCount}</span>` : '';
  483. return `
  484. <div class="user-item"
  485. onclick="selectUser('${user.openId}', '${escapeHtml(user.name || '未知用户')}')"
  486. ondblclick="viewMessageHistory('${user.openId}', '${escapeHtml(user.name || '未知用户')}')">
  487. <div class="user-name">
  488. <span>${escapeHtml(user.name || '未知用户')}</span>
  489. ${unreadBadge}
  490. </div>
  491. <div class="user-openid">${escapeHtml(user.openId)}</div>
  492. </div>
  493. `;
  494. }).join('');
  495. }
  496. // 选择用户
  497. function selectUser(openId, name) {
  498. selectedUser = { openId, name };
  499. // 更新UI
  500. document.querySelectorAll('.user-item').forEach(item => {
  501. item.classList.remove('active');
  502. });
  503. event.currentTarget.classList.add('active');
  504. document.getElementById('selectedUserInfo').style.display = 'block';
  505. document.getElementById('selectedUserName').textContent = name;
  506. document.getElementById('selectedUserOpenId').textContent = openId;
  507. document.getElementById('sendBtn').disabled = false;
  508. }
  509. // 搜索用户
  510. function searchUsers() {
  511. searchName = document.getElementById('searchInput').value.trim();
  512. currentPage = 1;
  513. loadUsers();
  514. }
  515. // 允许回车搜索
  516. document.getElementById('searchInput').addEventListener('keypress', function (e) {
  517. if (e.key === 'Enter') {
  518. searchUsers();
  519. }
  520. });
  521. // 上一页
  522. function previousPage() {
  523. if (currentPage > 1) {
  524. currentPage--;
  525. loadUsers();
  526. }
  527. }
  528. // 下一页
  529. function nextPage() {
  530. if (currentPage < totalPages) {
  531. currentPage++;
  532. loadUsers();
  533. }
  534. }
  535. // 更新分页按钮状态
  536. function updatePagination() {
  537. document.getElementById('prevBtn').disabled = currentPage <= 1;
  538. document.getElementById('nextBtn').disabled = currentPage >= totalPages;
  539. document.getElementById('pageInfo').textContent = `第 ${currentPage} / ${totalPages} 页`;
  540. }
  541. // 发送消息
  542. async function sendMessage() {
  543. if (!selectedUser) {
  544. showError('请先选择一个用户');
  545. return;
  546. }
  547. const content = document.getElementById('messageContent').value.trim();
  548. if (!content) {
  549. showError('请输入消息内容');
  550. return;
  551. }
  552. const useTemplate = document.getElementById('useTemplate').checked;
  553. const sendBtn = document.getElementById('sendBtn');
  554. sendBtn.disabled = true;
  555. sendBtn.textContent = '发送中...';
  556. try {
  557. const endpoint = useTemplate ? '/wechat/api/message/sendTemplate' : '/wechat/api/message/send';
  558. const response = await fetch(endpoint, {
  559. method: 'POST',
  560. headers: {
  561. 'Content-Type': 'application/json'
  562. },
  563. body: JSON.stringify({
  564. openId: selectedUser.openId,
  565. content: content,
  566. name: selectedUser.name
  567. })
  568. });
  569. const result = await response.text();
  570. if (response.ok) {
  571. showSuccess(useTemplate ? '模板消息发送成功!' : '消息发送成功!');
  572. document.getElementById('messageContent').value = '';
  573. } else {
  574. showError('发送失败: ' + result);
  575. }
  576. } catch (error) {
  577. console.error('发送消息出错:', error);
  578. showError('发送消息出错: ' + error.message);
  579. } finally {
  580. sendBtn.disabled = false;
  581. sendBtn.textContent = '发送消息';
  582. }
  583. }
  584. // 显示成功提示
  585. function showSuccess(message) {
  586. const alertContainer = document.getElementById('alertContainer');
  587. alertContainer.innerHTML = `<div class="alert alert-success">${escapeHtml(message)}</div>`;
  588. setTimeout(() => {
  589. alertContainer.innerHTML = '';
  590. }, 3000);
  591. }
  592. // 显示错误提示
  593. function showError(message) {
  594. const alertContainer = document.getElementById('alertContainer');
  595. alertContainer.innerHTML = `<div class="alert alert-error">${escapeHtml(message)}</div>`;
  596. setTimeout(() => {
  597. alertContainer.innerHTML = '';
  598. }, 5000);
  599. }
  600. // HTML转义,防止XSS
  601. function escapeHtml(text) {
  602. const div = document.createElement('div');
  603. div.textContent = text;
  604. return div.innerHTML;
  605. }
  606. // 查看用户的消息历史
  607. async function viewMessageHistory(openId, name) {
  608. const modal = document.getElementById('historyModal');
  609. const modalTitle = document.getElementById('modalTitle');
  610. const historyContent = document.getElementById('historyContent');
  611. modalTitle.textContent = `${name} 的消息历史`;
  612. historyContent.innerHTML = '<div class="loading">加载中...</div>';
  613. modal.classList.add('show');
  614. try {
  615. const response = await fetch(`/wechat/api/message/history?openId=${openId}&limit=50`);
  616. if (response.ok) {
  617. const messages = await response.json();
  618. if (messages.length === 0) {
  619. historyContent.innerHTML = '<div class="no-history">暂无历史消息</div>';
  620. } else {
  621. historyContent.innerHTML = messages.map(msg => `
  622. <div class="message-item">
  623. <div class="message-time">${formatDateTime(msg.createTime)}</div>
  624. <div class="message-text">${escapeHtml(msg.content)}</div>
  625. </div>
  626. `).join('');
  627. // 标记消息为已读
  628. await markAsRead(openId);
  629. }
  630. } else {
  631. historyContent.innerHTML = '<div class="no-history">加载历史消息失败</div>';
  632. }
  633. } catch (error) {
  634. console.error('加载历史消息失败:', error);
  635. historyContent.innerHTML = '<div class="no-history">加载历史消息失败</div>';
  636. }
  637. }
  638. // 关闭消息历史模态框
  639. function closeHistoryModal() {
  640. const modal = document.getElementById('historyModal');
  641. modal.classList.remove('show');
  642. }
  643. // 点击模态框背景关闭
  644. document.getElementById('historyModal')?.addEventListener('click', function (e) {
  645. if (e.target === this) {
  646. closeHistoryModal();
  647. }
  648. });
  649. // 标记消息为已读
  650. async function markAsRead(openId) {
  651. try {
  652. const response = await fetch(`/wechat/api/message/markAsRead?openId=${openId}`, {
  653. method: 'POST'
  654. });
  655. if (response.ok) {
  656. // 更新未读数量
  657. delete unreadCounts[openId];
  658. loadUsers(); // 重新加载用户列表以更新徽章
  659. }
  660. } catch (error) {
  661. console.error('标记已读失败:', error);
  662. }
  663. }
  664. // 格式化日期时间
  665. function formatDateTime(dateTimeStr) {
  666. if (!dateTimeStr) return '';
  667. const date = new Date(dateTimeStr);
  668. const year = date.getFullYear();
  669. const month = String(date.getMonth() + 1).padStart(2, '0');
  670. const day = String(date.getDate()).padStart(2, '0');
  671. const hours = String(date.getHours()).padStart(2, '0');
  672. const minutes = String(date.getMinutes()).padStart(2, '0');
  673. return `${year}-${month}-${day} ${hours}:${minutes}`;
  674. }
  675. </script>
  676. </body>
  677. </html>