|
|
@@ -0,0 +1,542 @@
|
|
|
+<!DOCTYPE html>
|
|
|
+<html lang="zh-CN">
|
|
|
+
|
|
|
+<head>
|
|
|
+ <meta charset="UTF-8">
|
|
|
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
|
+ <title>消息发送管理</title>
|
|
|
+ <style>
|
|
|
+ * {
|
|
|
+ margin: 0;
|
|
|
+ padding: 0;
|
|
|
+ box-sizing: border-box;
|
|
|
+ }
|
|
|
+
|
|
|
+ body {
|
|
|
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
|
|
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
|
+ min-height: 100vh;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ padding: 20px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .container {
|
|
|
+ max-width: 1400px;
|
|
|
+ width: 95%;
|
|
|
+ margin: 0 auto;
|
|
|
+ background: white;
|
|
|
+ border-radius: 12px;
|
|
|
+ box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
|
|
|
+ overflow: hidden;
|
|
|
+ display: flex;
|
|
|
+ min-height: 700px;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* 左侧用户列表区域 */
|
|
|
+ .user-panel {
|
|
|
+ width: 400px;
|
|
|
+ background: #f8f9fa;
|
|
|
+ border-right: 1px solid #e0e0e0;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ overflow: hidden;
|
|
|
+ }
|
|
|
+
|
|
|
+ .user-panel-header {
|
|
|
+ padding: 20px;
|
|
|
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
|
+ color: white;
|
|
|
+ }
|
|
|
+
|
|
|
+ .user-panel-header h2 {
|
|
|
+ font-size: 20px;
|
|
|
+ margin-bottom: 15px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .search-box {
|
|
|
+ position: relative;
|
|
|
+ }
|
|
|
+
|
|
|
+ .search-box input {
|
|
|
+ width: 100%;
|
|
|
+ padding: 10px 40px 10px 15px;
|
|
|
+ border: none;
|
|
|
+ border-radius: 6px;
|
|
|
+ font-size: 14px;
|
|
|
+ outline: none;
|
|
|
+ transition: all 0.3s;
|
|
|
+ }
|
|
|
+
|
|
|
+ .search-box input:focus {
|
|
|
+ box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.3);
|
|
|
+ }
|
|
|
+
|
|
|
+ .search-box button {
|
|
|
+ position: absolute;
|
|
|
+ right: 5px;
|
|
|
+ top: 50%;
|
|
|
+ transform: translateY(-50%);
|
|
|
+ background: #667eea;
|
|
|
+ color: white;
|
|
|
+ border: none;
|
|
|
+ padding: 6px 12px;
|
|
|
+ border-radius: 4px;
|
|
|
+ cursor: pointer;
|
|
|
+ font-size: 13px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .search-box button:hover {
|
|
|
+ background: #5568d3;
|
|
|
+ }
|
|
|
+
|
|
|
+ .user-list-container {
|
|
|
+ flex: 1;
|
|
|
+ overflow-y: auto;
|
|
|
+ padding: 10px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .user-item {
|
|
|
+ padding: 15px;
|
|
|
+ background: white;
|
|
|
+ margin-bottom: 8px;
|
|
|
+ border-radius: 8px;
|
|
|
+ cursor: pointer;
|
|
|
+ transition: all 0.2s;
|
|
|
+ border: 2px solid transparent;
|
|
|
+ }
|
|
|
+
|
|
|
+ .user-item:hover {
|
|
|
+ transform: translateX(5px);
|
|
|
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
|
+ }
|
|
|
+
|
|
|
+ .user-item.active {
|
|
|
+ border-color: #667eea;
|
|
|
+ background: #f0f3ff;
|
|
|
+ }
|
|
|
+
|
|
|
+ .user-name {
|
|
|
+ font-weight: 600;
|
|
|
+ color: #333;
|
|
|
+ margin-bottom: 5px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .user-openid {
|
|
|
+ font-size: 12px;
|
|
|
+ color: #888;
|
|
|
+ word-break: break-all;
|
|
|
+ }
|
|
|
+
|
|
|
+ .pagination {
|
|
|
+ padding: 15px;
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ border-top: 1px solid #e0e0e0;
|
|
|
+ background: white;
|
|
|
+ }
|
|
|
+
|
|
|
+ .pagination button {
|
|
|
+ padding: 8px 16px;
|
|
|
+ background: #667eea;
|
|
|
+ color: white;
|
|
|
+ border: none;
|
|
|
+ border-radius: 4px;
|
|
|
+ cursor: pointer;
|
|
|
+ font-size: 13px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .pagination button:disabled {
|
|
|
+ background: #ccc;
|
|
|
+ cursor: not-allowed;
|
|
|
+ }
|
|
|
+
|
|
|
+ .pagination button:hover:not(:disabled) {
|
|
|
+ background: #5568d3;
|
|
|
+ }
|
|
|
+
|
|
|
+ .page-info {
|
|
|
+ font-size: 13px;
|
|
|
+ color: #666;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* 右侧消息输入区域 */
|
|
|
+ .message-panel {
|
|
|
+ flex: 1;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ padding: 30px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .message-panel h2 {
|
|
|
+ color: #333;
|
|
|
+ margin-bottom: 20px;
|
|
|
+ font-size: 24px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .selected-user-info {
|
|
|
+ background: #f0f3ff;
|
|
|
+ padding: 15px;
|
|
|
+ border-radius: 8px;
|
|
|
+ margin-bottom: 20px;
|
|
|
+ border-left: 4px solid #667eea;
|
|
|
+ }
|
|
|
+
|
|
|
+ .selected-user-info p {
|
|
|
+ margin: 5px 0;
|
|
|
+ color: #555;
|
|
|
+ }
|
|
|
+
|
|
|
+ .selected-user-info strong {
|
|
|
+ color: #333;
|
|
|
+ }
|
|
|
+
|
|
|
+ .message-input-area {
|
|
|
+ flex: 1;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ }
|
|
|
+
|
|
|
+ .message-input-area label {
|
|
|
+ font-weight: 600;
|
|
|
+ color: #333;
|
|
|
+ margin-bottom: 10px;
|
|
|
+ display: block;
|
|
|
+ }
|
|
|
+
|
|
|
+ .message-input-area textarea {
|
|
|
+ flex: 1;
|
|
|
+ padding: 15px;
|
|
|
+ border: 2px solid #e0e0e0;
|
|
|
+ border-radius: 8px;
|
|
|
+ font-size: 14px;
|
|
|
+ font-family: inherit;
|
|
|
+ resize: none;
|
|
|
+ outline: none;
|
|
|
+ transition: border-color 0.3s;
|
|
|
+ }
|
|
|
+
|
|
|
+ .message-input-area textarea:focus {
|
|
|
+ border-color: #667eea;
|
|
|
+ }
|
|
|
+
|
|
|
+ .template-option {
|
|
|
+ margin-top: 15px;
|
|
|
+ padding: 12px;
|
|
|
+ background: #f8f9fa;
|
|
|
+ border-radius: 6px;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ }
|
|
|
+
|
|
|
+ .template-option input[type="checkbox"] {
|
|
|
+ width: 18px;
|
|
|
+ height: 18px;
|
|
|
+ margin-right: 10px;
|
|
|
+ cursor: pointer;
|
|
|
+ }
|
|
|
+
|
|
|
+ .template-option label {
|
|
|
+ cursor: pointer;
|
|
|
+ margin: 0;
|
|
|
+ color: #555;
|
|
|
+ font-weight: 500;
|
|
|
+ }
|
|
|
+
|
|
|
+ .send-button {
|
|
|
+ margin-top: 20px;
|
|
|
+ padding: 15px 30px;
|
|
|
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
|
+ color: white;
|
|
|
+ border: none;
|
|
|
+ border-radius: 8px;
|
|
|
+ font-size: 16px;
|
|
|
+ font-weight: 600;
|
|
|
+ cursor: pointer;
|
|
|
+ transition: all 0.3s;
|
|
|
+ }
|
|
|
+
|
|
|
+ .send-button:hover:not(:disabled) {
|
|
|
+ transform: translateY(-2px);
|
|
|
+ box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
|
|
|
+ }
|
|
|
+
|
|
|
+ .send-button:disabled {
|
|
|
+ background: #ccc;
|
|
|
+ cursor: not-allowed;
|
|
|
+ }
|
|
|
+
|
|
|
+ .empty-state {
|
|
|
+ text-align: center;
|
|
|
+ padding: 40px 20px;
|
|
|
+ color: #999;
|
|
|
+ }
|
|
|
+
|
|
|
+ .loading {
|
|
|
+ text-align: center;
|
|
|
+ padding: 20px;
|
|
|
+ color: #667eea;
|
|
|
+ }
|
|
|
+
|
|
|
+ .alert {
|
|
|
+ padding: 12px 20px;
|
|
|
+ border-radius: 6px;
|
|
|
+ margin-bottom: 20px;
|
|
|
+ animation: slideDown 0.3s;
|
|
|
+ }
|
|
|
+
|
|
|
+ .alert-success {
|
|
|
+ background: #d4edda;
|
|
|
+ color: #155724;
|
|
|
+ border: 1px solid #c3e6cb;
|
|
|
+ }
|
|
|
+
|
|
|
+ .alert-error {
|
|
|
+ background: #f8d7da;
|
|
|
+ color: #721c24;
|
|
|
+ border: 1px solid #f5c6cb;
|
|
|
+ }
|
|
|
+
|
|
|
+ @keyframes slideDown {
|
|
|
+ from {
|
|
|
+ opacity: 0;
|
|
|
+ transform: translateY(-10px);
|
|
|
+ }
|
|
|
+
|
|
|
+ to {
|
|
|
+ opacity: 1;
|
|
|
+ transform: translateY(0);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ </style>
|
|
|
+</head>
|
|
|
+
|
|
|
+<body>
|
|
|
+ <div class="container">
|
|
|
+ <!-- 左侧用户列表 -->
|
|
|
+ <div class="user-panel">
|
|
|
+ <div class="user-panel-header">
|
|
|
+ <h2>用户列表</h2>
|
|
|
+ <div class="search-box">
|
|
|
+ <input type="text" id="searchInput" placeholder="搜索用户名称...">
|
|
|
+ <button onclick="searchUsers()">搜索</button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="user-list-container" id="userList">
|
|
|
+ <div class="loading">加载中...</div>
|
|
|
+ </div>
|
|
|
+ <div class="pagination">
|
|
|
+ <button id="prevBtn" onclick="previousPage()" disabled>上一页</button>
|
|
|
+ <span class="page-info" id="pageInfo">第 1 页</span>
|
|
|
+ <button id="nextBtn" onclick="nextPage()">下一页</button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 右侧消息输入 -->
|
|
|
+ <div class="message-panel">
|
|
|
+ <h2>发送消息</h2>
|
|
|
+ <div id="alertContainer"></div>
|
|
|
+ <div id="selectedUserInfo" style="display: none;" class="selected-user-info">
|
|
|
+ <p><strong>选中用户:</strong> <span id="selectedUserName"></span></p>
|
|
|
+ <p><strong>OpenID:</strong> <span id="selectedUserOpenId"></span></p>
|
|
|
+ </div>
|
|
|
+ <div class="message-input-area">
|
|
|
+ <label for="messageContent">消息内容</label>
|
|
|
+ <textarea id="messageContent" placeholder="请输入要发送的消息..."></textarea>
|
|
|
+ </div>
|
|
|
+ <div class="template-option">
|
|
|
+ <input type="checkbox" id="useTemplate">
|
|
|
+ <label for="useTemplate">发送模板消息(将使用预设模板发送)</label>
|
|
|
+ </div>
|
|
|
+ <button class="send-button" id="sendBtn" onclick="sendMessage()" disabled>发送消息</button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <script>
|
|
|
+ let currentPage = 1;
|
|
|
+ const pageSize = 10;
|
|
|
+ let selectedUser = null;
|
|
|
+ let totalPages = 1;
|
|
|
+ let searchName = '';
|
|
|
+
|
|
|
+ // 页面加载时获取用户列表
|
|
|
+ window.onload = function () {
|
|
|
+ loadUsers();
|
|
|
+ };
|
|
|
+
|
|
|
+ // 加载用户列表
|
|
|
+ async function loadUsers() {
|
|
|
+ try {
|
|
|
+ const params = new URLSearchParams({
|
|
|
+ page: currentPage,
|
|
|
+ size: pageSize
|
|
|
+ });
|
|
|
+ if (searchName) {
|
|
|
+ params.append('name', searchName);
|
|
|
+ }
|
|
|
+
|
|
|
+ const response = await fetch(`/wechat/api/users?${params}`);
|
|
|
+ const data = await response.json();
|
|
|
+
|
|
|
+ if (response.ok) {
|
|
|
+ displayUsers(data.records || []);
|
|
|
+ totalPages = data.pages || 1;
|
|
|
+ updatePagination();
|
|
|
+ } else {
|
|
|
+ showError('加载用户列表失败');
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('加载用户列表出错:', error);
|
|
|
+ showError('加载用户列表出错: ' + error.message);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 显示用户列表
|
|
|
+ function displayUsers(users) {
|
|
|
+ const userList = document.getElementById('userList');
|
|
|
+
|
|
|
+ if (users.length === 0) {
|
|
|
+ userList.innerHTML = '<div class="empty-state">暂无用户数据</div>';
|
|
|
+ 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('');
|
|
|
+ }
|
|
|
+
|
|
|
+ // 选择用户
|
|
|
+ function selectUser(openId, name) {
|
|
|
+ selectedUser = { openId, name };
|
|
|
+
|
|
|
+ // 更新UI
|
|
|
+ document.querySelectorAll('.user-item').forEach(item => {
|
|
|
+ item.classList.remove('active');
|
|
|
+ });
|
|
|
+ event.currentTarget.classList.add('active');
|
|
|
+
|
|
|
+ document.getElementById('selectedUserInfo').style.display = 'block';
|
|
|
+ document.getElementById('selectedUserName').textContent = name;
|
|
|
+ document.getElementById('selectedUserOpenId').textContent = openId;
|
|
|
+ document.getElementById('sendBtn').disabled = false;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 搜索用户
|
|
|
+ function searchUsers() {
|
|
|
+ searchName = document.getElementById('searchInput').value.trim();
|
|
|
+ currentPage = 1;
|
|
|
+ loadUsers();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 允许回车搜索
|
|
|
+ document.getElementById('searchInput').addEventListener('keypress', function (e) {
|
|
|
+ if (e.key === 'Enter') {
|
|
|
+ searchUsers();
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // 上一页
|
|
|
+ function previousPage() {
|
|
|
+ if (currentPage > 1) {
|
|
|
+ currentPage--;
|
|
|
+ loadUsers();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 下一页
|
|
|
+ function nextPage() {
|
|
|
+ if (currentPage < totalPages) {
|
|
|
+ currentPage++;
|
|
|
+ loadUsers();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 更新分页按钮状态
|
|
|
+ function updatePagination() {
|
|
|
+ document.getElementById('prevBtn').disabled = currentPage <= 1;
|
|
|
+ document.getElementById('nextBtn').disabled = currentPage >= totalPages;
|
|
|
+ document.getElementById('pageInfo').textContent = `第 ${currentPage} / ${totalPages} 页`;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 发送消息
|
|
|
+ async function sendMessage() {
|
|
|
+ if (!selectedUser) {
|
|
|
+ showError('请先选择一个用户');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const content = document.getElementById('messageContent').value.trim();
|
|
|
+ if (!content) {
|
|
|
+ showError('请输入消息内容');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const useTemplate = document.getElementById('useTemplate').checked;
|
|
|
+ const sendBtn = document.getElementById('sendBtn');
|
|
|
+ sendBtn.disabled = true;
|
|
|
+ sendBtn.textContent = '发送中...';
|
|
|
+
|
|
|
+ try {
|
|
|
+ const endpoint = useTemplate ? '/wechat/api/message/sendTemplate' : '/wechat/api/message/send';
|
|
|
+ const response = await fetch(endpoint, {
|
|
|
+ method: 'POST',
|
|
|
+ headers: {
|
|
|
+ 'Content-Type': 'application/json'
|
|
|
+ },
|
|
|
+ body: JSON.stringify({
|
|
|
+ openId: selectedUser.openId,
|
|
|
+ content: content,
|
|
|
+ name: selectedUser.name
|
|
|
+ })
|
|
|
+ });
|
|
|
+
|
|
|
+ const result = await response.text();
|
|
|
+
|
|
|
+ if (response.ok) {
|
|
|
+ showSuccess(useTemplate ? '模板消息发送成功!' : '消息发送成功!');
|
|
|
+ document.getElementById('messageContent').value = '';
|
|
|
+ } else {
|
|
|
+ showError('发送失败: ' + result);
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('发送消息出错:', error);
|
|
|
+ showError('发送消息出错: ' + error.message);
|
|
|
+ } finally {
|
|
|
+ sendBtn.disabled = false;
|
|
|
+ sendBtn.textContent = '发送消息';
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 显示成功提示
|
|
|
+ function showSuccess(message) {
|
|
|
+ const alertContainer = document.getElementById('alertContainer');
|
|
|
+ alertContainer.innerHTML = `<div class="alert alert-success">${escapeHtml(message)}</div>`;
|
|
|
+ setTimeout(() => {
|
|
|
+ alertContainer.innerHTML = '';
|
|
|
+ }, 3000);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 显示错误提示
|
|
|
+ function showError(message) {
|
|
|
+ const alertContainer = document.getElementById('alertContainer');
|
|
|
+ alertContainer.innerHTML = `<div class="alert alert-error">${escapeHtml(message)}</div>`;
|
|
|
+ setTimeout(() => {
|
|
|
+ alertContainer.innerHTML = '';
|
|
|
+ }, 5000);
|
|
|
+ }
|
|
|
+
|
|
|
+ // HTML转义,防止XSS
|
|
|
+ function escapeHtml(text) {
|
|
|
+ const div = document.createElement('div');
|
|
|
+ div.textContent = text;
|
|
|
+ return div.innerHTML;
|
|
|
+ }
|
|
|
+ </script>
|
|
|
+</body>
|
|
|
+
|
|
|
+</html>
|