873 lines
49 KiB
HTML
873 lines
49 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||
<title>小财记账</title>
|
||
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
||
<meta name="theme-color" content="#1e3c72">
|
||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||
<style>
|
||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; height: 100vh; display: flex; justify-content: center; align-items: center; }
|
||
.app { width: 100%; max-width: 420px; height: 100vh; background: #fff; display: flex; flex-direction: column; box-shadow: 0 0 20px rgba(0,0,0,0.1); position: relative; }
|
||
.hidden { display: none !important; }
|
||
|
||
.header { background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%); color: #fff; padding: 0 16px; display: flex; justify-content: space-between; align-items: center; min-height: 48px; }
|
||
.header-left { display: flex; align-items: center; gap: 8px; }
|
||
.header-left h2 { font-size: 18px; font-weight: 500; }
|
||
.icon-btn { background: none; border: none; color: #fff; cursor: pointer; display: flex; align-items: center; justify-content: center; padding: 4px; border-radius: 50%; }
|
||
.icon-btn svg { width: 22px; height: 22px; stroke: #fff; fill: none; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
|
||
|
||
#create-room-btn { visibility: hidden; }
|
||
#create-room-btn.visible { visibility: visible; }
|
||
|
||
.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: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; }
|
||
.chat-name { font-size: 16px; font-weight: 500; margin-bottom: 4px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||
.last-msg { font-size: 14px; color: #888; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||
.chat-right { display: flex; flex-direction: column; align-items: flex-end; margin-left: 8px; flex-shrink: 0; min-width: 56px; }
|
||
.chat-time { font-size: 12px; color: #aaa; white-space: nowrap; }
|
||
.room-actions { display: flex; gap: 4px; margin-top: 4px; }
|
||
.edit-room-btn { background: none; border: none; cursor: pointer; display: flex; align-items: center; padding: 2px; }
|
||
.edit-room-btn svg { width: 16px; height: 16px; 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; }
|
||
|
||
.chat-window { display: flex; flex-direction: column; height: 100%; }
|
||
.chat-header { background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%); color: #fff; padding: 0 16px; display: flex; align-items: center; min-height: 48px; }
|
||
.chat-header .back-btn { margin-right: 12px; }
|
||
.chat-header .chat-title { font-size: 17px; font-weight: 500; flex: 1; }
|
||
.messages { flex: 1; overflow-y: auto; padding: 12px; background: #fafafa; }
|
||
.msg { margin-bottom: 12px; display: flex; flex-direction: column; }
|
||
.msg.me { align-items: flex-end; }
|
||
.msg-bubble { max-width: 75%; padding: 10px 14px; border-radius: 20px; font-size: 16px; line-height: 1.5; word-wrap: break-word; overflow-wrap: break-word; cursor: pointer; }
|
||
.msg.me .msg-bubble { background: #d1f0d1; border-bottom-right-radius: 4px; }
|
||
.msg.other .msg-bubble { background: #fff; border: 1px solid #eee; border-bottom-left-radius: 4px; }
|
||
.msg-user { font-size: 12px; color: #888; margin-bottom: 2px; margin-left: 4px; }
|
||
.msg.me .msg-user { text-align: right; margin-right: 4px; }
|
||
.msg-time { font-size: 11px; color: #aaa; margin-top: 2px; }
|
||
.msg-bubble h1, .msg-bubble h2, .msg-bubble h3 { font-size: 16px; margin: 5px 0; }
|
||
.msg-bubble table { width: 100%; border-collapse: collapse; margin: 8px 0; }
|
||
.msg-bubble th, .msg-bubble td { border: 1px solid #ddd; padding: 4px; text-align: left; font-size: 14px; }
|
||
.msg-bubble th { background: #f0f0f0; }
|
||
.msg-bubble pre { background: #f0f0f0; padding: 8px; border-radius: 4px; overflow-x: auto; }
|
||
.msg-bubble code { background: #f0f0f0; padding: 2px 4px; border-radius: 3px; font-size: 14px; }
|
||
|
||
.input-area { padding: 8px 12px; border-top: 1px solid #eee; display: flex; align-items: center; background: #fff; gap: 8px; }
|
||
.input-area textarea { flex: 1; border: 1px solid #ddd; border-radius: 12px; padding: 10px 12px; font-size: 16px; outline: none; resize: none; min-height: 40px; max-height: 120px; overflow-y: hidden; line-height: 1.4; }
|
||
.file-upload-btn { background: none; border: none; cursor: pointer; display: flex; align-items: center; justify-content: center; padding: 4px; }
|
||
.file-upload-btn svg { width: 24px; height: 24px; stroke: #666; fill: none; stroke-width: 2; }
|
||
.send-btn { background: #3b82f6; border: none; color: #fff; width: 32px; height: 32px; border-radius: 50%; display: flex; align-items: center; justify-content: center; cursor: pointer; flex-shrink: 0; }
|
||
.send-btn svg { width: 18px; height: 18px; fill: #fff; }
|
||
|
||
.purchase-panel-overlay { position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: #fff; z-index: 10; display: flex; flex-direction: column; }
|
||
.purchase-panel-header { background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%); color: #fff; padding: 0 16px; display: flex; justify-content: space-between; align-items: center; min-height: 48px; }
|
||
.purchase-panel-body { flex: 1; overflow-y: auto; padding: 12px; }
|
||
.purchase-month-group { margin-bottom: 20px; }
|
||
.purchase-month-header { background: #f5f5f5; font-size: 16px; font-weight: 600; padding: 10px 8px; border-radius: 4px; display: flex; justify-content: space-between; }
|
||
.purchase-item { padding: 12px 8px; border-bottom: 1px solid #f0f0f0; cursor: pointer; }
|
||
.purchase-item:active { background: #f9f9f9; }
|
||
.purchase-item .item-main { display: flex; justify-content: space-between; align-items: baseline; }
|
||
.purchase-item .item-name { font-size: 16px; font-weight: 500; }
|
||
.purchase-item .item-amount { font-size: 16px; font-weight: 600; }
|
||
.purchase-item .item-meta { display: flex; justify-content: space-between; margin-top: 4px; font-size: 13px; color: #888; flex-wrap: wrap; gap: 4px; }
|
||
.status-badge { background: #fef3c7; padding: 1px 6px; border-radius: 8px; font-size: 12px; }
|
||
.status-badge.done { background: #d1fae5; }
|
||
|
||
.modal-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); display: flex; justify-content: center; align-items: center; z-index: 100; }
|
||
.modal { background: #fff; width: 90%; max-width: 400px; max-height: 80vh; border-radius: 12px; overflow: auto; padding: 16px; position: relative; }
|
||
.modal .modal-close-btn { position: absolute; top: 10px; right: 12px; background: none; border: none; cursor: pointer; }
|
||
.modal .modal-close-btn svg { width: 20px; height: 20px; stroke: #888; fill: none; stroke-width: 2; }
|
||
.modal h3 { margin-bottom: 12px; font-size: 18px; padding-right: 30px; }
|
||
.modal input { width: 100%; padding: 8px; margin: 8px 0; border: 1px solid #ddd; border-radius: 8px; font-size: 16px; }
|
||
.modal button { margin-right: 8px; padding: 8px 16px; border-radius: 8px; border: none; cursor: pointer; font-size: 16px; }
|
||
.modal .primary-btn { background: #3b82f6; color: #fff; }
|
||
.history-list { list-style: none; font-size: 14px; }
|
||
.history-list li { padding: 6px 0; border-bottom: 1px solid #f0f0f0; display: flex; }
|
||
.history-time { min-width: 80px; color: #888; margin-right: 10px; font-size: 13px; }
|
||
.img-thumb { width: 60px; height: 60px; object-fit: cover; border-radius: 4px; margin: 4px; border: 1px solid #eee; }
|
||
.attachments { display: flex; flex-wrap: wrap; margin: 8px 0; }
|
||
.msg-img { max-width: 200px; border-radius: 8px; margin: 4px 0; }
|
||
.msg-placeholder .msg-bubble { opacity: 0.6; }
|
||
|
||
.version { text-align: center; font-size: 12px; color: #aaa; margin-top: 16px; }
|
||
|
||
.fullscreen-overlay {
|
||
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
|
||
background: rgba(0,0,0,0.9); display: flex; align-items: center; justify-content: center;
|
||
z-index: 200; cursor: pointer;
|
||
}
|
||
.fullscreen-overlay img { max-width: 100%; max-height: 100%; object-fit: contain; }
|
||
.fullscreen-overlay .fullscreen-text {
|
||
background: #fff; padding: 20px; border-radius: 8px; max-width: 90%; max-height: 80%;
|
||
overflow: auto; font-size: 16px; line-height: 1.5; color: #333;
|
||
}
|
||
.fullscreen-overlay .fullscreen-text img { max-width: 200px; border-radius: 8px; margin: 4px 0; }
|
||
.fullscreen-overlay .fullscreen-text table {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
margin: 8px 0;
|
||
}
|
||
.fullscreen-overlay .fullscreen-text th,
|
||
.fullscreen-overlay .fullscreen-text td {border: 1px solid #ddd;padding: 4px;text-align: left;font-size: 14px;}
|
||
.fullscreen-overlay .fullscreen-text th {background: #f0f0f0;}
|
||
|
||
.detail-row { margin-bottom: 8px; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="app" id="app">
|
||
<!-- 登录 -->
|
||
<div id="login-page" class="modal-overlay">
|
||
<div class="modal">
|
||
<h2>登录小财记账</h2>
|
||
<input type="text" id="login-user" placeholder="用户名">
|
||
<input type="password" id="login-pass" placeholder="密码">
|
||
<button class="primary-btn" onclick="login()" style="width:100%;">登录</button>
|
||
<p id="login-error" style="color:red; margin-top:8px;"></p>
|
||
<div class="version">v2.5</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 主界面 -->
|
||
<div id="main-page" class="hidden" style="display:flex; flex-direction:column; height:100%;">
|
||
<!-- 群聊列表页 -->
|
||
<div id="list-page" style="display:flex; flex-direction:column; height:100%;">
|
||
<div class="header">
|
||
<div class="header-left">
|
||
<svg width="24" height="24" viewBox="0 0 24 24" stroke="#fff" fill="none" stroke-width="1.5">
|
||
<rect x="3" y="3" width="18" height="18" rx="3"/><line x1="9" y1="9" x2="15" y2="9"/><line x1="9" y1="13" x2="15" y2="13"/><line x1="9" y1="17" x2="12" y2="17"/>
|
||
</svg>
|
||
<h2>小财记账</h2>
|
||
</div>
|
||
<div style="display:flex; gap:2px;">
|
||
<button class="icon-btn" id="create-room-btn" onclick="openCreateRoom()" title="新建群聊">
|
||
<svg viewBox="0 0 24 24"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg>
|
||
</button>
|
||
<button class="icon-btn" id="logout-btn" onclick="logout()" title="注销登录">
|
||
<svg viewBox="0 0 24 24"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/></svg>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div class="chat-list" id="chat-list-container"></div>
|
||
<div class="summary-card" onclick="openSummary()">
|
||
<div>
|
||
<h4>采购汇总</h4>
|
||
<div class="summary-preview" id="summary-preview">加载中...</div>
|
||
</div>
|
||
<svg width="20" height="20" viewBox="0 0 24 24" stroke="#888" fill="none" stroke-width="2"><polyline points="9 18 15 12 9 6"></polyline></svg>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 聊天窗口 -->
|
||
<div id="chat-page" class="chat-window hidden">
|
||
<div class="chat-header">
|
||
<button class="icon-btn back-btn" onclick="showList()">
|
||
<svg viewBox="0 0 24 24"><polyline points="15 18 9 12 15 6"></polyline></svg>
|
||
</button>
|
||
<span class="chat-title" id="current-chat-name"></span>
|
||
<button class="icon-btn" onclick="openPurchasePanel()" title="采购清单">
|
||
<svg viewBox="0 0 24 24"><line x1="8" y1="6" x2="21" y2="6"></line><line x1="8" y1="12" x2="21" y2="12"></line><line x1="8" y1="18" x2="21" y2="18"></line><line x1="3" y1="6" x2="3.01" y2="6"></line><line x1="3" y1="12" x2="3.01" y2="12"></line><line x1="3" y1="18" x2="3.01" y2="18"></line></svg>
|
||
</button>
|
||
</div>
|
||
<div class="messages" id="messages-container"></div>
|
||
<div class="input-area">
|
||
<label class="file-upload-btn" title="上传图片">
|
||
<input type="file" id="file-input" accept="image/*" multiple style="display:none;" onchange="handleFileSelect(event)">
|
||
<svg viewBox="0 0 24 24"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><circle cx="8.5" cy="8.5" r="1.5"></circle><polyline points="21 15 16 10 5 21"></polyline></svg>
|
||
</label>
|
||
<textarea id="msg-input" placeholder="输入消息..." rows="1"></textarea>
|
||
<button class="send-btn" onclick="sendMessage()">
|
||
<svg viewBox="0 0 24 24"><line x1="22" y1="2" x2="11" y2="13"></line><polygon points="22 2 15 22 11 13 2 9 22 2"></polygon></svg>
|
||
</button>
|
||
</div>
|
||
<!-- 采购清单覆盖层 -->
|
||
<div id="purchase-panel-overlay" class="purchase-panel-overlay hidden">
|
||
<div class="purchase-panel-header">
|
||
<span>采购清单</span>
|
||
<button class="icon-btn close-btn" onclick="closePurchasePanel()">
|
||
<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>
|
||
</div>
|
||
<div class="purchase-panel-body" id="purchase-panel-body"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 创建群聊弹窗 -->
|
||
<div id="create-modal" class="modal-overlay hidden">
|
||
<div class="modal">
|
||
<button class="modal-close-btn" onclick="closeCreateModal()">
|
||
<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>新建项目群聊</h3>
|
||
<input type="text" id="new-room-name" placeholder="群聊名称">
|
||
<input type="text" id="white-list" placeholder="白名单成员(逗号分隔,留空全员可见)">
|
||
<button class="primary-btn" onclick="confirmCreateRoom()">创建</button>
|
||
</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">
|
||
<button class="modal-close-btn" onclick="closeDetailModal()">
|
||
<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="detail-title"></h3>
|
||
<div id="detail-content"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
const API = '';
|
||
let ws;
|
||
let currentRoom = null;
|
||
let rooms = [];
|
||
let pendingUploads = [];
|
||
const messagesContainer = document.getElementById('messages-container');
|
||
let messageCache = [];
|
||
let tempBubbleTimer = null;
|
||
|
||
function getToken() { return localStorage.getItem('token'); }
|
||
function saveAuth(data) {
|
||
localStorage.setItem('token', data.token);
|
||
localStorage.setItem('currentUser', data.username);
|
||
localStorage.setItem('isAdmin', data.isAdmin);
|
||
}
|
||
function clearAuth() {
|
||
localStorage.removeItem('token');
|
||
localStorage.removeItem('currentUser');
|
||
localStorage.removeItem('isAdmin');
|
||
}
|
||
function logout() {
|
||
if (ws) { ws.close(); ws = null; }
|
||
clearAuth();
|
||
document.getElementById('login-page').classList.remove('hidden');
|
||
document.getElementById('main-page').classList.add('hidden');
|
||
currentRoom = null;
|
||
rooms = [];
|
||
document.getElementById('chat-list-container').innerHTML = '';
|
||
document.getElementById('messages-container').innerHTML = '';
|
||
}
|
||
|
||
if (typeof marked !== 'undefined') marked.setOptions({ breaks: true, gfm: true, sanitize: false });
|
||
|
||
function compressImage(file) {
|
||
return new Promise((resolve, reject) => {
|
||
const reader = new FileReader();
|
||
reader.onload = (e) => {
|
||
const img = new Image();
|
||
img.onload = () => {
|
||
const canvas = document.createElement('canvas');
|
||
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);
|
||
};
|
||
img.src = e.target.result;
|
||
};
|
||
reader.readAsDataURL(file);
|
||
});
|
||
}
|
||
|
||
function escapeHtml(text) {
|
||
const div = document.createElement('div');
|
||
div.textContent = text;
|
||
return div.innerHTML;
|
||
}
|
||
|
||
function renderMessageHTML(msg) {
|
||
const currentUser = localStorage.getItem('currentUser');
|
||
const isMe = msg.user === currentUser;
|
||
const name = msg.user === '小财' ? '小财' : msg.user;
|
||
let contentHtml = msg.text ? escapeHtml(msg.text) : '';
|
||
if (msg.user === '小财' && typeof marked !== 'undefined' && msg.text) {
|
||
try { contentHtml = marked.parse(msg.text); } catch(e) {}
|
||
}
|
||
let attachHtml = '';
|
||
if (msg.attachments && msg.attachments.length) {
|
||
try {
|
||
const files = typeof msg.attachments === 'string' ? JSON.parse(msg.attachments) : msg.attachments;
|
||
attachHtml = files.map(f => {
|
||
const isImage = /\.(jpg|jpeg|png|gif)$/i.test(f);
|
||
if (isImage) return `<img class="msg-img" src="${f}" alt="${f}" loading="lazy">`;
|
||
return `<div><a href="${f}" target="_blank">📎 ${f.split('/').pop()}</a></div>`;
|
||
}).join('');
|
||
} catch(e) {}
|
||
}
|
||
return `
|
||
<div class="msg ${isMe ? 'me' : 'other'}" data-msg-id="${msg.id}">
|
||
<div class="msg-user">${name}</div>
|
||
<div class="msg-bubble">${contentHtml}${attachHtml}</div>
|
||
<div class="msg-time">${msg.timestamp}</div>
|
||
</div>`;
|
||
}
|
||
|
||
function renderAllMessages(msgs) {
|
||
messageCache = msgs || [];
|
||
messagesContainer.innerHTML = messageCache.map(renderMessageHTML).join('');
|
||
scrollToBottom();
|
||
bindMessageEvents();
|
||
}
|
||
|
||
function appendMessage(msg) {
|
||
messageCache.push(msg);
|
||
messagesContainer.insertAdjacentHTML('beforeend', renderMessageHTML(msg));
|
||
scrollToBottom();
|
||
bindMessageEvents();
|
||
clearTempBubble(); // 收到新消息时移除气泡
|
||
}
|
||
|
||
function replaceMessage(msgId, newMsg) {
|
||
const idx = messageCache.findIndex(m => m.id == msgId);
|
||
if (idx !== -1) {
|
||
messageCache[idx] = newMsg;
|
||
const oldEl = messagesContainer.querySelector(`[data-msg-id="${msgId}"]`);
|
||
if (oldEl) oldEl.outerHTML = renderMessageHTML(newMsg);
|
||
} else {
|
||
appendMessage(newMsg);
|
||
}
|
||
bindMessageEvents();
|
||
}
|
||
|
||
function scrollToBottom() { messagesContainer.scrollTop = messagesContainer.scrollHeight; }
|
||
|
||
function bindMessageEvents() {
|
||
document.querySelectorAll('.msg-img').forEach(img => {
|
||
img.removeEventListener('dblclick', onImageDblClick);
|
||
img.addEventListener('dblclick', onImageDblClick);
|
||
});
|
||
document.querySelectorAll('.msg-bubble').forEach(bubble => {
|
||
bubble.removeEventListener('dblclick', onBubbleDblClick);
|
||
bubble.addEventListener('dblclick', onBubbleDblClick);
|
||
});
|
||
}
|
||
|
||
function onImageDblClick(e) {
|
||
e.stopPropagation();
|
||
const overlay = document.createElement('div');
|
||
overlay.className = 'fullscreen-overlay';
|
||
overlay.innerHTML = `<img src="${e.target.src}" alt="">`;
|
||
overlay.addEventListener('click', () => overlay.remove());
|
||
document.body.appendChild(overlay);
|
||
}
|
||
|
||
function onImgThumbDblClick(img) {
|
||
const overlay = document.createElement('div');
|
||
overlay.className = 'fullscreen-overlay';
|
||
overlay.innerHTML = `<img src="${img.src}" style="max-width:100%;max-height:100%;object-fit:contain" alt="">`;
|
||
overlay.addEventListener('click', () => overlay.remove());
|
||
document.body.appendChild(overlay);
|
||
}
|
||
|
||
function onBubbleDblClick(e) {
|
||
e.stopPropagation();
|
||
const bubble = e.currentTarget;
|
||
const clone = bubble.cloneNode(true);
|
||
const overlay = document.createElement('div');
|
||
overlay.className = 'fullscreen-overlay';
|
||
const container = document.createElement('div');
|
||
container.className = 'fullscreen-text';
|
||
container.innerHTML = clone.innerHTML;
|
||
overlay.appendChild(container);
|
||
overlay.addEventListener('click', () => overlay.remove());
|
||
document.body.appendChild(overlay);
|
||
}
|
||
|
||
// 临时"输入中..."气泡
|
||
function insertTempBubble() {
|
||
clearTempBubble();
|
||
const tempId = 'temp_' + Date.now();
|
||
const tempMsg = { id: tempId, user: '小财', text: '输入中...', attachments: [], timestamp: '', isTemp: true };
|
||
messageCache.push(tempMsg);
|
||
messagesContainer.insertAdjacentHTML('beforeend', renderMessageHTML(tempMsg));
|
||
scrollToBottom();
|
||
tempBubbleTimer = setTimeout(() => { clearTempBubble(); }, 8000);
|
||
// 降级轮询:如果 WebSocket 消息丢失,定时从服务端拉取
|
||
if (pollTimer) clearInterval(pollTimer);
|
||
let pollCount = 0;
|
||
pollTimer = setInterval(async () => {
|
||
pollCount++;
|
||
if (!currentRoom || pollCount > 5) { clearInterval(pollTimer); pollTimer = null; return; }
|
||
try {
|
||
const token = getToken();
|
||
const res = await fetch(API + `/api/rooms/${currentRoom}/messages`, { headers: { 'Authorization': `Bearer ${token}` } });
|
||
const msgs = await res.json();
|
||
const lastCacheId = messageCache.filter(m => !m.isTemp && !m.isPlaceholder).slice(-1)[0]?.id;
|
||
const newMsgs = msgs.filter(m => !lastCacheId || m.id > lastCacheId);
|
||
if (newMsgs.length) {
|
||
clearInterval(pollTimer); pollTimer = null;
|
||
clearTempBubble();
|
||
newMsgs.forEach(m => appendMessage(m));
|
||
}
|
||
} catch(e) {}
|
||
}, 2000);
|
||
}
|
||
|
||
function clearTempBubble() {
|
||
if (tempBubbleTimer) { clearTimeout(tempBubbleTimer); tempBubbleTimer = null; }
|
||
if (pollTimer) { clearInterval(pollTimer); pollTimer = null; }
|
||
messageCache = messageCache.filter(m => !m.isTemp);
|
||
const tempEls = messagesContainer.querySelectorAll('[data-msg-id^="temp_"]');
|
||
tempEls.forEach(el => el.remove());
|
||
}
|
||
|
||
// WebSocket 重连
|
||
let wsReconnectTimer = null;
|
||
let reconnectAttempts = 0;
|
||
const MAX_RECONNECT_DELAY = 30000;
|
||
let pollTimer = null; // 降级轮询定时器
|
||
|
||
function initWebSocket() {
|
||
const token = getToken();
|
||
if (!token) return;
|
||
const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||
ws = new WebSocket(`${protocol}//${location.host}?token=${token}`);
|
||
ws.onopen = () => {
|
||
console.log('WebSocket 已连接');
|
||
reconnectAttempts = 0;
|
||
if (currentRoom) ws.send(JSON.stringify({ type: 'join', roomId: currentRoom }));
|
||
};
|
||
ws.onmessage = (e) => {
|
||
const data = JSON.parse(e.data);
|
||
console.log('📩 WS 收到:', data.type);
|
||
if (data.type === 'new_message') {
|
||
const msg = data.message;
|
||
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(); }
|
||
}
|
||
} 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);
|
||
ws.onclose = (event) => {
|
||
console.log('WebSocket 关闭,代码:', event.code);
|
||
if (event.code === 1000) return;
|
||
scheduleReconnect();
|
||
};
|
||
}
|
||
|
||
function scheduleReconnect() {
|
||
if (wsReconnectTimer) clearTimeout(wsReconnectTimer);
|
||
const delay = Math.min(1000 * Math.pow(2, reconnectAttempts), MAX_RECONNECT_DELAY);
|
||
reconnectAttempts++;
|
||
wsReconnectTimer = setTimeout(() => { console.log('尝试重连...'); initWebSocket(); }, delay);
|
||
}
|
||
|
||
document.addEventListener('visibilitychange', () => {
|
||
if (document.visibilityState === 'visible') {
|
||
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
||
if (wsReconnectTimer) clearTimeout(wsReconnectTimer);
|
||
initWebSocket();
|
||
}
|
||
}
|
||
});
|
||
|
||
async function init() {
|
||
const token = getToken();
|
||
if (token) {
|
||
try {
|
||
const res = await fetch(API + '/api/user', { headers: { 'Authorization': `Bearer ${token}` } });
|
||
if (res.ok) {
|
||
const user = await res.json();
|
||
document.getElementById('login-page').classList.add('hidden');
|
||
document.getElementById('main-page').classList.remove('hidden');
|
||
if (localStorage.getItem('isAdmin') === 'true') {
|
||
document.getElementById('create-room-btn').classList.add('visible');
|
||
} else {
|
||
document.getElementById('create-room-btn').classList.remove('visible');
|
||
}
|
||
initWebSocket();
|
||
loadRooms();
|
||
updateSummaryPreview();
|
||
initSwipeGestures();
|
||
return;
|
||
}
|
||
} catch(e) {}
|
||
clearAuth();
|
||
}
|
||
document.getElementById('login-page').classList.remove('hidden');
|
||
document.getElementById('main-page').classList.add('hidden');
|
||
}
|
||
|
||
async function login() {
|
||
const u = document.getElementById('login-user').value;
|
||
const p = document.getElementById('login-pass').value;
|
||
const res = await fetch(API + '/api/login', {
|
||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ username: u, password: p })
|
||
});
|
||
if (res.ok) {
|
||
const data = await res.json();
|
||
saveAuth(data);
|
||
document.getElementById('login-page').classList.add('hidden');
|
||
document.getElementById('main-page').classList.remove('hidden');
|
||
if (data.isAdmin) {
|
||
document.getElementById('create-room-btn').classList.add('visible');
|
||
} else {
|
||
document.getElementById('create-room-btn').classList.remove('visible');
|
||
}
|
||
initWebSocket();
|
||
loadRooms();
|
||
updateSummaryPreview();
|
||
initSwipeGestures();
|
||
} else {
|
||
const err = await res.json();
|
||
document.getElementById('login-error').textContent = err.error || '登录失败';
|
||
}
|
||
}
|
||
|
||
function initSwipeGestures() {
|
||
const chatPage = document.getElementById('chat-page');
|
||
const purchaseOverlay = document.getElementById('purchase-panel-overlay');
|
||
let startX = 0, startY = 0;
|
||
|
||
function handleTouchStart(e) {
|
||
startX = e.touches[0].clientX;
|
||
startY = e.touches[0].clientY;
|
||
}
|
||
|
||
function handleTouchEnd(e, target) {
|
||
if (!startX) return;
|
||
const endX = e.changedTouches[0].clientX;
|
||
const endY = e.changedTouches[0].clientY;
|
||
const diffX = endX - startX;
|
||
const diffY = endY - startY;
|
||
if (Math.abs(diffX) > Math.abs(diffY) && Math.abs(diffX) > 50) {
|
||
if (target === chatPage && !purchaseOverlay.classList.contains('hidden')) {
|
||
if (Math.abs(diffX) > 30) closePurchasePanel();
|
||
} else if (target === chatPage) {
|
||
if (diffX > 30) showList();
|
||
else if (diffX < -30) openPurchasePanel();
|
||
} else if (target === purchaseOverlay) {
|
||
if (Math.abs(diffX) > 30) closePurchasePanel();
|
||
}
|
||
}
|
||
startX = 0; startY = 0;
|
||
}
|
||
|
||
chatPage.addEventListener('touchstart', handleTouchStart, { passive: true });
|
||
chatPage.addEventListener('touchend', (e) => handleTouchEnd(e, chatPage), { passive: true });
|
||
purchaseOverlay.addEventListener('touchstart', handleTouchStart, { passive: true });
|
||
purchaseOverlay.addEventListener('touchend', (e) => handleTouchEnd(e, purchaseOverlay), { passive: true });
|
||
}
|
||
|
||
async function loadRooms() {
|
||
const token = getToken();
|
||
const res = await fetch(API + '/api/rooms', { headers: { 'Authorization': `Bearer ${token}` } });
|
||
if (res.status === 401) { clearAuth(); location.reload(); return; }
|
||
rooms = await res.json();
|
||
renderChatList();
|
||
}
|
||
|
||
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" 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-right">
|
||
<div class="chat-time" onclick="openChat('${room.id}')">${lastTime}</div>
|
||
${isAdmin ? `<div class="room-actions">
|
||
<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>` : ''}
|
||
</div>
|
||
</div>`;
|
||
}).join('');
|
||
}
|
||
|
||
async function updateSummaryPreview() {
|
||
const token = getToken();
|
||
try {
|
||
const res = await fetch(API + '/api/summary', { headers: { 'Authorization': `Bearer ${token}` } });
|
||
const summary = await res.json();
|
||
const totalItems = summary.reduce((s, r) => s + r.purchase_count, 0);
|
||
const totalAmount = summary.reduce((s, r) => s + r.total_amount, 0);
|
||
const preview = document.getElementById('summary-preview');
|
||
if (preview) preview.innerText = `共 ${totalItems} 条,合计 ¥${totalAmount}`;
|
||
} catch(e) {}
|
||
}
|
||
|
||
async function openChat(roomId, autoOpenPurchase = false) {
|
||
currentRoom = roomId;
|
||
clearTempBubble();
|
||
document.getElementById('list-page').classList.add('hidden');
|
||
document.getElementById('chat-page').classList.remove('hidden');
|
||
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);
|
||
const token = getToken();
|
||
const res = await fetch(API + `/api/rooms/${roomId}/messages`, { headers: { 'Authorization': `Bearer ${token}` } });
|
||
const msgs = await res.json();
|
||
renderAllMessages(msgs);
|
||
if (ws && ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify({ type: 'join', roomId }));
|
||
}
|
||
|
||
function showList() {
|
||
clearTempBubble();
|
||
document.getElementById('chat-page').classList.add('hidden');
|
||
document.getElementById('list-page').classList.remove('hidden');
|
||
currentRoom = null;
|
||
loadRooms();
|
||
updateSummaryPreview();
|
||
}
|
||
|
||
async function sendMessage() {
|
||
const input = document.getElementById('msg-input');
|
||
const text = input.value.trim();
|
||
if (!text && pendingUploads.length === 0) return;
|
||
if (!currentRoom) return;
|
||
const token = getToken();
|
||
try {
|
||
const res = await fetch(API + `/api/rooms/${currentRoom}/messages`, {
|
||
method: 'POST',
|
||
headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ text: text || '', attachments: pendingUploads })
|
||
});
|
||
const newMsg = await res.json();
|
||
appendMessage(newMsg);
|
||
input.value = '';
|
||
pendingUploads = [];
|
||
insertTempBubble(); // 显示“输入中...”
|
||
} 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
|
||
};
|
||
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 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);
|
||
const placeholderEl = messagesContainer.querySelector(`[data-msg-id="${placeholderId}"]`);
|
||
if (placeholderEl) placeholderEl.remove();
|
||
alert('图片发送失败,请重试');
|
||
}
|
||
}
|
||
|
||
async function loadPurchases() {
|
||
if (!currentRoom) return;
|
||
const token = getToken();
|
||
const res = await fetch(API + `/api/rooms/${currentRoom}/purchases`, { headers: { 'Authorization': `Bearer ${token}` } });
|
||
const items = await res.json();
|
||
renderPurchasePanel(items);
|
||
}
|
||
|
||
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; }
|
||
const groups = {};
|
||
items.forEach(item => {
|
||
const month = formatMonth(item.created_at);
|
||
if (!groups[month]) groups[month] = [];
|
||
groups[month].push(item);
|
||
});
|
||
let html = '';
|
||
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>`;
|
||
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.quantity} ${item.invoice_type !== '无票' ? '· ' + item.invoice_type : ''}</span>
|
||
<span class="status-badge ${item.status==='已完成'?'done':''}">${item.status}</span>
|
||
</div>
|
||
</div>`;
|
||
});
|
||
html += '</div>';
|
||
});
|
||
container.innerHTML = html;
|
||
}
|
||
|
||
function formatMonth(dateStr) {
|
||
const clean = dateStr.replace(/-/g, '/');
|
||
const parts = clean.split('/');
|
||
if (parts.length >= 2) return `${parts[0]}年${parseInt(parts[1], 10)}月`;
|
||
return dateStr.substring(0,7);
|
||
}
|
||
|
||
function openPurchasePanel() {
|
||
document.getElementById('purchase-panel-body').innerHTML = '<p style="text-align:center;color:#888;">加载中...</p>';
|
||
document.getElementById('purchase-panel-overlay').classList.remove('hidden');
|
||
loadPurchases();
|
||
}
|
||
function closePurchasePanel() { document.getElementById('purchase-panel-overlay').classList.add('hidden'); }
|
||
|
||
async function openDetail(pid) {
|
||
const token = getToken();
|
||
const res = await fetch(API + `/api/purchases/${pid}`, { headers: { 'Authorization': `Bearer ${token}` } });
|
||
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}" ondblclick="onImgThumbDblClick(this)">`).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 = `
|
||
<div class="detail-row"><strong>数量:</strong>${p.quantity}</div>
|
||
<div class="detail-row"><strong>单价:</strong>¥${p.unit_price}</div>
|
||
<div class="detail-row"><strong>邮费:</strong>¥${p.freight}</div>
|
||
<div class="detail-row"><strong>总金额:</strong>¥${p.amount}</div>
|
||
<div class="detail-row"><strong>付款方式:</strong>${p.payment_method || '未指定'}</div>
|
||
<div class="detail-row"><strong>发票:</strong>${p.invoice_type || '无票'}</div>
|
||
<div class="detail-row"><strong>状态:</strong>${p.status}</div>
|
||
<div class="detail-row"><strong>申请人:</strong>${p.applicant}</div>
|
||
<div class="detail-row"><strong>采购时间:</strong>${p.created_at}</div>
|
||
<div class="detail-row"><strong>备注:</strong>${p.remarks || '无'}</div>
|
||
${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() {
|
||
const token = getToken();
|
||
const res = await fetch(API + '/api/summary', { headers: { 'Authorization': `Bearer ${token}` } });
|
||
const summary = await res.json();
|
||
let html = '';
|
||
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>`;
|
||
});
|
||
}
|
||
document.getElementById('detail-title').textContent = '采购汇总';
|
||
document.getElementById('detail-content').innerHTML = html;
|
||
document.getElementById('detail-modal').classList.remove('hidden');
|
||
}
|
||
|
||
function openCreateRoom() { document.getElementById('create-modal').classList.remove('hidden'); }
|
||
function closeCreateModal() { document.getElementById('create-modal').classList.add('hidden'); }
|
||
async function confirmCreateRoom() {
|
||
const name = document.getElementById('new-room-name').value.trim();
|
||
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 }) });
|
||
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('input', function(e) {
|
||
if (e.target.id === 'msg-input') {
|
||
e.target.style.height = 'auto';
|
||
e.target.style.height = e.target.scrollHeight + 'px';
|
||
}
|
||
});
|
||
|
||
document.addEventListener('keydown', function(e) {
|
||
if (e.target.id === 'msg-input' && e.key === 'Enter' && !e.ctrlKey) { e.preventDefault(); sendMessage(); }
|
||
});
|
||
|
||
init();
|
||
</script>
|
||
</body>
|
||
</html>
|