1130 lines
39 KiB
HTML
1130 lines
39 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>企业微信 · Layui版</title>
|
||
<link rel="stylesheet" href="/assets/layui/css/layui.css">
|
||
<style>
|
||
* {
|
||
margin: 0;
|
||
padding: 0;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
body {
|
||
font-family: "Microsoft YaHei", "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||
background: #f5f5f5;
|
||
}
|
||
|
||
.chat-container {
|
||
display: flex;
|
||
height: 100vh;
|
||
max-width: 1400px;
|
||
margin: 0 auto;
|
||
background: #fff;
|
||
box-shadow: 0 0 20px rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
/* ========== 左侧联系人栏 ========== */
|
||
.chat-sidebar {
|
||
width: 260px;
|
||
border-right: 1px solid #e7e7e7;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.sidebar-header {
|
||
padding: 15px;
|
||
border-bottom: 1px solid #e7e7e7;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
|
||
.sidebar-title {
|
||
font-size: 18px;
|
||
font-weight: bold;
|
||
color: #1aad19;
|
||
}
|
||
|
||
.sidebar-search {
|
||
padding: 10px 15px;
|
||
border-bottom: 1px solid #e7e7e7;
|
||
}
|
||
|
||
.sidebar-list {
|
||
flex: 1;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.contact-item {
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 12px 15px;
|
||
border-bottom: 1px solid #f5f5f5;
|
||
cursor: pointer;
|
||
transition: background 0.2s;
|
||
}
|
||
|
||
.contact-item:hover,
|
||
.contact-item.active {
|
||
background: #f5f9fd;
|
||
}
|
||
|
||
.contact-item.active {
|
||
border-left: 3px solid #1aad19;
|
||
padding-left: 12px;
|
||
}
|
||
|
||
.avatar {
|
||
width: 40px;
|
||
height: 40px;
|
||
border-radius: 50%;
|
||
background: #1aad19;
|
||
color: white;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-weight: bold;
|
||
margin-right: 10px;
|
||
font-size: 16px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.avatar.group {
|
||
background: #5fb878;
|
||
}
|
||
|
||
.contact-info {
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
|
||
.contact-name {
|
||
font-weight: bold;
|
||
font-size: 14px;
|
||
color: #333;
|
||
margin-bottom: 3px;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
.contact-msg {
|
||
font-size: 12px;
|
||
color: #999;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
.contact-time {
|
||
font-size: 11px;
|
||
color: #999;
|
||
}
|
||
|
||
.badge {
|
||
display: inline-block;
|
||
min-width: 18px;
|
||
height: 18px;
|
||
border-radius: 9px;
|
||
background: #ff4d4f;
|
||
color: white;
|
||
font-size: 12px;
|
||
text-align: center;
|
||
line-height: 18px;
|
||
padding: 0 4px;
|
||
margin-left: 5px;
|
||
}
|
||
|
||
.online-dot {
|
||
width: 8px;
|
||
height: 8px;
|
||
background: #52c41a;
|
||
border-radius: 50%;
|
||
display: inline-block;
|
||
margin-left: 5px;
|
||
vertical-align: middle;
|
||
}
|
||
|
||
/* ========== 右侧聊天区 ========== */
|
||
.chat-main {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.chat-header {
|
||
padding: 15px 20px;
|
||
border-bottom: 1px solid #e7e7e7;
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
|
||
.chat-avatar {
|
||
width: 45px;
|
||
height: 45px;
|
||
border-radius: 50%;
|
||
background: #1aad19;
|
||
color: white;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-weight: bold;
|
||
font-size: 18px;
|
||
margin-right: 15px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.chat-info {
|
||
flex: 1;
|
||
}
|
||
|
||
.chat-title {
|
||
font-size: 16px;
|
||
font-weight: bold;
|
||
color: #333;
|
||
}
|
||
|
||
.chat-status {
|
||
font-size: 12px;
|
||
color: #999;
|
||
margin-top: 3px;
|
||
}
|
||
|
||
.chat-status.online::after {
|
||
content: "";
|
||
display: inline-block;
|
||
width: 6px;
|
||
height: 6px;
|
||
background: #52c41a;
|
||
border-radius: 50%;
|
||
margin-left: 5px;
|
||
vertical-align: middle;
|
||
}
|
||
|
||
.chat-body {
|
||
flex: 1;
|
||
padding: 20px;
|
||
overflow-y: auto;
|
||
background: #f5f5f5;
|
||
}
|
||
|
||
.message-list {
|
||
max-width: 720px;
|
||
margin: 0 auto;
|
||
}
|
||
|
||
/* ========== 消息气泡 ========== */
|
||
.message-item {
|
||
margin-bottom: 15px;
|
||
display: flex;
|
||
}
|
||
|
||
.message-item.self {
|
||
justify-content: flex-end;
|
||
}
|
||
|
||
.message-avatar {
|
||
width: 40px;
|
||
height: 40px;
|
||
border-radius: 50%;
|
||
background: #d9d9d9;
|
||
margin: 0 10px 0 0;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-weight: bold;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.message-item.self .message-avatar {
|
||
margin: 0 0 0 10px;
|
||
}
|
||
|
||
.message-content {
|
||
max-width: 65%;
|
||
position: relative;
|
||
}
|
||
|
||
.message-item.self .message-content {
|
||
align-items: flex-end;
|
||
}
|
||
|
||
.msg-bubble {
|
||
padding: 10px 15px;
|
||
border-radius: 18px;
|
||
line-height: 1.5;
|
||
font-size: 14px;
|
||
word-break: break-word;
|
||
position: relative;
|
||
display: inline-block;
|
||
max-width: 100%;
|
||
}
|
||
|
||
.msg-bubble.text {
|
||
background: #9eea6a;
|
||
color: #000;
|
||
border-bottom-right-radius: 4px;
|
||
}
|
||
|
||
.message-item.self .msg-bubble.text {
|
||
background: #1aad19;
|
||
color: white;
|
||
border-bottom-right-radius: 18px;
|
||
border-bottom-left-radius: 4px;
|
||
}
|
||
|
||
.msg-bubble.image {
|
||
padding: 0;
|
||
border-radius: 8px;
|
||
overflow: hidden;
|
||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15);
|
||
}
|
||
|
||
.msg-bubble.image img {
|
||
display: block;
|
||
max-width: 300px;
|
||
max-height: 400px;
|
||
}
|
||
|
||
.msg-bubble.file {
|
||
background: #f7f7f7;
|
||
border: 1px solid #e5e5e5;
|
||
border-radius: 8px;
|
||
padding: 12px;
|
||
width: 280px;
|
||
}
|
||
|
||
.file-icon {
|
||
font-size: 24px;
|
||
color: #1aad19;
|
||
margin-bottom: 5px;
|
||
}
|
||
|
||
.file-name {
|
||
font-weight: bold;
|
||
color: #333;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
.file-meta {
|
||
font-size: 12px;
|
||
color: #999;
|
||
margin-top: 3px;
|
||
}
|
||
|
||
.file-size::before {
|
||
content: "· ";
|
||
}
|
||
|
||
/* 气泡三角 */
|
||
.msg-bubble::after {
|
||
content: "";
|
||
position: absolute;
|
||
bottom: -6px;
|
||
width: 0;
|
||
height: 0;
|
||
border: 6px solid transparent;
|
||
}
|
||
|
||
.message-item:not(.self) .msg-bubble.text::after {
|
||
left: -6px;
|
||
border-right-color: #9eea6a;
|
||
border-bottom-color: transparent;
|
||
}
|
||
|
||
.message-item.self .msg-bubble.text::after {
|
||
right: -6px;
|
||
border-left-color: #1aad19;
|
||
border-bottom-color: transparent;
|
||
}
|
||
|
||
.msg-time {
|
||
font-size: 11px;
|
||
color: #999;
|
||
text-align: center;
|
||
margin: 8px 0;
|
||
}
|
||
|
||
.msg-status {
|
||
font-size: 11px;
|
||
color: #999;
|
||
margin-top: 5px;
|
||
text-align: right;
|
||
}
|
||
|
||
.msg-status.sending::after {
|
||
content: "·";
|
||
animation: blink 1s infinite;
|
||
}
|
||
|
||
.msg-status.failed {
|
||
color: #ff4d4f;
|
||
}
|
||
|
||
@keyframes blink {
|
||
50% {
|
||
opacity: 0;
|
||
}
|
||
}
|
||
|
||
/* ========== 输入区域 ========== */
|
||
.chat-footer {
|
||
border-top: 1px solid #e7e7e7;
|
||
padding: 10px 15px;
|
||
background: white;
|
||
}
|
||
|
||
.chat-tools {
|
||
display: flex;
|
||
padding-bottom: 10px;
|
||
border-bottom: 1px dashed #e7e7e7;
|
||
}
|
||
|
||
.tool-btn {
|
||
width: 36px;
|
||
height: 36px;
|
||
border-radius: 50%;
|
||
background: #f5f5f5;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
margin-right: 10px;
|
||
cursor: pointer;
|
||
color: #666;
|
||
font-size: 18px;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.tool-btn:hover {
|
||
background: #e6e6e6;
|
||
color: #1aad19;
|
||
}
|
||
|
||
.tool-btn.active {
|
||
background: #e6f7ff;
|
||
color: #1890ff;
|
||
}
|
||
|
||
.tool-btn i {
|
||
font-size: 16px;
|
||
}
|
||
|
||
.chat-input-wrap {
|
||
display: flex;
|
||
margin-top: 10px;
|
||
}
|
||
|
||
.chat-input {
|
||
flex: 1;
|
||
border: 1px solid #d9d9d9;
|
||
border-radius: 20px;
|
||
padding: 0 20px;
|
||
height: 40px;
|
||
outline: none;
|
||
resize: none;
|
||
font-size: 14px;
|
||
line-height: 40px;
|
||
}
|
||
|
||
.chat-input:focus {
|
||
border-color: #1aad19;
|
||
box-shadow: 0 0 0 2px rgba(26, 173, 25, 0.15);
|
||
}
|
||
|
||
.chat-send {
|
||
width: 80px;
|
||
height: 40px;
|
||
background: #1aad19;
|
||
color: white;
|
||
border: none;
|
||
border-radius: 20px;
|
||
margin-left: 10px;
|
||
font-size: 15px;
|
||
font-weight: bold;
|
||
cursor: pointer;
|
||
transition: background 0.2s;
|
||
}
|
||
|
||
.chat-send:hover {
|
||
background: #179716;
|
||
}
|
||
|
||
.chat-send:active {
|
||
transform: scale(0.98);
|
||
}
|
||
|
||
/* ========== 表情面板 ========== */
|
||
.emoji-panel {
|
||
display: none;
|
||
position: absolute;
|
||
bottom: 100px;
|
||
left: 15px;
|
||
background: white;
|
||
border: 1px solid #d9d9d9;
|
||
border-radius: 8px;
|
||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.15);
|
||
padding: 10px;
|
||
z-index: 1000;
|
||
width: 360px;
|
||
}
|
||
|
||
.emoji-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(8, 1fr);
|
||
gap: 8px;
|
||
}
|
||
|
||
.emoji-item {
|
||
width: 36px;
|
||
height: 36px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
border-radius: 8px;
|
||
cursor: pointer;
|
||
font-size: 20px;
|
||
transition: background 0.2s;
|
||
}
|
||
|
||
.emoji-item:hover {
|
||
background: #f5f5f5;
|
||
}
|
||
|
||
/* ========== 系统消息 ========== */
|
||
.system-msg {
|
||
text-align: center;
|
||
color: #999;
|
||
font-size: 13px;
|
||
padding: 8px 15px;
|
||
background: #f9f9f9;
|
||
border-radius: 15px;
|
||
display: inline-block;
|
||
margin: 10px auto;
|
||
max-width: 80%;
|
||
}
|
||
|
||
/* ========== 响应式 ========== */
|
||
@media (max-width: 992px) {
|
||
.chat-container {
|
||
flex-direction: column;
|
||
}
|
||
|
||
.chat-sidebar {
|
||
width: 100%;
|
||
height: 40%;
|
||
border-right: none;
|
||
border-bottom: 1px solid #e7e7e7;
|
||
}
|
||
|
||
.contact-item {
|
||
padding: 10px;
|
||
}
|
||
|
||
.message-content {
|
||
max-width: 85%;
|
||
}
|
||
}
|
||
|
||
/* ========== 连接状态 ========== */
|
||
.conn-status {
|
||
position: fixed;
|
||
top: 15px;
|
||
right: 15px;
|
||
padding: 8px 15px;
|
||
border-radius: 20px;
|
||
color: white;
|
||
font-size: 13px;
|
||
z-index: 9999;
|
||
display: none;
|
||
}
|
||
|
||
.status-connected {
|
||
background: #52c41a;
|
||
}
|
||
|
||
.status-disconnected {
|
||
background: #ff4d4f;
|
||
animation: pulse 2s infinite;
|
||
}
|
||
|
||
@keyframes pulse {
|
||
0% {
|
||
box-shadow: 0 0 0 0 rgba(255, 77, 79, 0.4);
|
||
}
|
||
|
||
70% {
|
||
box-shadow: 0 0 0 10px rgba(255, 77, 79, 0);
|
||
}
|
||
|
||
100% {
|
||
box-shadow: 0 0 0 0 rgba(255, 77, 79, 0);
|
||
}
|
||
}
|
||
|
||
/* ========== 滚动条优化 ========== */
|
||
::-webkit-scrollbar {
|
||
width: 6px;
|
||
height: 6px;
|
||
}
|
||
|
||
::-webkit-scrollbar-track {
|
||
background: #f1f1f1;
|
||
border-radius: 3px;
|
||
}
|
||
|
||
::-webkit-scrollbar-thumb {
|
||
background: #c1c1c1;
|
||
border-radius: 3px;
|
||
}
|
||
|
||
::-webkit-scrollbar-thumb:hover {
|
||
background: #a8a8a8;
|
||
}
|
||
</style>
|
||
</head>
|
||
|
||
<body>
|
||
<div class="chat-container">
|
||
<!-- 左侧联系人栏 -->
|
||
<div class="chat-sidebar">
|
||
<div class="sidebar-header">
|
||
<div class="sidebar-title">💬 企业微信</div>
|
||
<div class="layui-badge-dot layui-bg-green" title="在线"></div>
|
||
</div>
|
||
<div class="sidebar-search">
|
||
<input type="text" id="contact-search" placeholder="搜索联系人/群聊" class="layui-input" style="padding-left: 30px;">
|
||
<i class="layui-icon layui-icon-search" style="position: absolute; left: 10px; top: 9px; color: #999;"></i>
|
||
</div>
|
||
<div class="sidebar-list" id="contact-list">
|
||
<!-- 联系人列表由JS动态生成 -->
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 右侧聊天区 -->
|
||
<div class="chat-main">
|
||
<div class="chat-header">
|
||
<div class="chat-avatar" id="current-avatar">T</div>
|
||
<div class="chat-info">
|
||
<div class="chat-title" id="current-title">选择联系人开始聊天</div>
|
||
<div class="chat-status" id="current-status">未连接</div>
|
||
</div>
|
||
<div>
|
||
<button class="layui-btn layui-btn-sm layui-btn-primary" id="conn-status-btn">
|
||
<i class="layui-icon layui-icon-refresh"></i> 重连
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="chat-body">
|
||
<div class="message-list" id="message-list">
|
||
<div class="system-msg">👈 请从左侧选择联系人或群聊</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="chat-footer">
|
||
<div class="chat-tools">
|
||
<div class="tool-btn" id="emoji-btn" title="表情">
|
||
<i class="layui-icon layui-icon-face-smile"></i>
|
||
</div>
|
||
<div class="tool-btn" id="image-btn" title="图片">
|
||
<i class="layui-icon layui-icon-picture"></i>
|
||
<input type="file" id="image-upload" accept="image/*" style="display:none;">
|
||
</div>
|
||
<div class="tool-btn" id="file-btn" title="文件">
|
||
<i class="layui-icon layui-icon-file"></i>
|
||
<input type="file" id="file-upload" style="display:none;">
|
||
</div>
|
||
<div class="tool-btn" title="语音"><i class="layui-icon layui-icon-voice"></i></div>
|
||
<div class="tool-btn" title="视频通话"><i class="layui-icon layui-icon-video"></i></div>
|
||
</div>
|
||
|
||
<div class="chat-input-wrap">
|
||
<textarea class="chat-input" id="chat-input" placeholder="输入消息... (Ctrl+Enter发送)" rows="1"></textarea>
|
||
<button class="chat-send" id="send-btn">发送</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 表情面板 -->
|
||
<div class="emoji-panel" id="emoji-panel">
|
||
<div class="emoji-grid" id="emoji-grid">
|
||
<!-- 表情由JS动态生成 -->
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 连接状态提示 -->
|
||
<div class="conn-status" id="conn-status">● 已连接</div>
|
||
|
||
<!-- 用户凭证(实际项目应通过AJAX获取) -->
|
||
<input type="hidden" id="user-token" value="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.xxxxx">
|
||
<input type="hidden" id="user-uid" value="u_10086">
|
||
<input type="hidden" id="user-nickname" value="唐">
|
||
<input type="hidden" id="user-avatar" value="T">
|
||
|
||
<script src="/assets/layui/layui.js"></script>
|
||
<script src="/assets/ywxapp/ywxapp.js" module="member"></script>
|
||
<script>
|
||
layui.use(['layer', 'element', 'util', 'upload', 'setter', 'chatClient'], function () {
|
||
var $ = layui.jquery;
|
||
var layer = layui.layer;
|
||
var util = layui.util;
|
||
var upload = layui.upload;
|
||
var ChatClient = layui.chatClient;
|
||
|
||
const { access_token } = layui.setter.getTokens();
|
||
// ========== 初始化变量 ==========
|
||
var currentSession = null; // { id: 'u_10086', type: 'single', name: '张三', avatar: 'Z' }
|
||
var messageQueue = []; // 未发送消息队列
|
||
var chatClient = null;
|
||
var emojiPanelVisible = false;
|
||
|
||
// ========== 模拟联系人数据(实际项目从服务端获取) ==========
|
||
var contacts = [
|
||
{ id: '88888891', type: 'single', name: '张三', avatar: 'Z', online: true, lastMsg: '好的,明天见', time: '10:23', unread: 2 },
|
||
{ id: 'u_10087', type: 'single', name: '李四', avatar: 'L', online: true, lastMsg: '文件已收到', time: '昨天', unread: 0 },
|
||
{ id: 'g_2024', type: 'group', name: '项目组', avatar: 'P', online: true, lastMsg: '[图片]', time: '14:15', unread: 9 },
|
||
{ id: 'u_10088', type: 'single', name: '王五', avatar: 'W', online: true, lastMsg: '在吗?', time: '周一', unread: 0 },
|
||
{ id: 'g_2025', type: 'group', name: '技术部', avatar: 'J', online: true, lastMsg: '会议推迟到下午', time: '09:30', unread: 1 }
|
||
];
|
||
|
||
// ========== 渲染联系人列表 ==========
|
||
function renderContacts(filter = '') {
|
||
var html = '';
|
||
contacts.forEach(contact => {
|
||
if (filter && !contact.name.includes(filter)) return;
|
||
|
||
var badge = contact.unread > 0 ? `<span class="badge">${contact.unread > 99 ? '99+' : contact.unread}</span>` : '';
|
||
var onlineDot = contact.online ? '<span class="online-dot"></span>' : '';
|
||
var activeClass = currentSession && currentSession.id === contact.id ? 'active' : '';
|
||
|
||
html += `
|
||
<div class="contact-item ${activeClass}" data-id="${contact.id}" data-type="${contact.type}" data-name="${contact.name}" data-avatar="${contact.avatar}">
|
||
<div class="avatar ${contact.type === 'group' ? 'group' : ''}">${contact.avatar}</div>
|
||
<div class="contact-info">
|
||
<div class="contact-name">${contact.name}${onlineDot}</div>
|
||
<div class="contact-msg">${contact.lastMsg}</div>
|
||
</div>
|
||
<div class="contact-time">${contact.time}${badge}</div>
|
||
</div>
|
||
`;
|
||
});
|
||
$('#contact-list').html(html || '<div style="text-align:center;padding:20px;color:#999">无匹配联系人</div>');
|
||
}
|
||
|
||
// ========== 渲染消息 ==========
|
||
function renderMessage(packet, isSelf = false) {
|
||
var header = packet.header || {};
|
||
var payload = packet.payload || {};
|
||
var ext = packet.ext || {};
|
||
var msgType = header.type || 'text';
|
||
var timeStr = util.toDateString(header.server_time || Date.now(), 'yyyy-MM-dd HH:mm');
|
||
// 消息容器
|
||
var msgClass = isSelf ? 'message-item self' : 'message-item';
|
||
var avatarText = isSelf ? $('#user-avatar').val() : (ext.from_avatar || header.from.substring(0, 1).toUpperCase());
|
||
var avatarClass = isSelf ? 'message-avatar' : 'message-avatar';
|
||
|
||
// 消息内容
|
||
var contentHtml = '';
|
||
switch (msgType) {
|
||
case 'text':
|
||
// XSS防护 + 表情转换
|
||
var text = util.escape(payload.content || '');
|
||
text = text.replace(/\[([^\]]+)\]/g, (match, p1) => {
|
||
var emojiMap = { '微笑': '😄', '大笑': '😂', '点赞': '👍', '爱心': '❤️', '哭': '😭', '生气': '😠' };
|
||
return emojiMap[p1] || match;
|
||
});
|
||
contentHtml = `<div class="msg-bubble text">${text}</div>`;
|
||
break;
|
||
case 'image':
|
||
var imgData = payload.content || {};
|
||
contentHtml = `
|
||
<div class="msg-bubble image">
|
||
<img src="${imgData.thumb || imgData.url}"
|
||
data-original="${imgData.url}"
|
||
onclick="previewImage(this)"
|
||
style="cursor: zoom-in;"
|
||
onerror="this.src='data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyMDAiIGhlaWdodD0iMTUwIj48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSIjZjVmNWY1Ii8+PHRleHQgeD0iNTAlIiB5PSI1MCUiIGZvbnQtZmFtaWx5PSJBcmlhbCIgZm9udC1zaXplPSIxNHB4IiBmaWxsPSIjOTk5IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIj5JbWFnZSBMb2FkaW5nPC90ZXh0Pjwvc3ZnPg=='">
|
||
${imgData.size ? `<div style="padding:5px 10px;font-size:12px;color:#999;text-align:center">${formatFileSize(imgData.size)}</div>` : ''}
|
||
</div>
|
||
`;
|
||
break;
|
||
case 'file':
|
||
var fileData = payload.content || {};
|
||
contentHtml = `
|
||
<div class="msg-bubble file" onclick="downloadFile('${fileData.url}', '${fileData.name}')">
|
||
<div class="file-icon">📄</div>
|
||
<div class="file-name" title="${fileData.name}">${fileData.name}</div>
|
||
<div class="file-meta">
|
||
<span>${fileData.type || '未知类型'}</span>
|
||
<span class="file-size">${formatFileSize(fileData.size)}</span>
|
||
</div>
|
||
</div>
|
||
`;
|
||
break;
|
||
case 'system':
|
||
$('#message-list').append(`<div class="system-msg">${util.escape(payload.content)}</div>`);
|
||
return;
|
||
}
|
||
|
||
// 消息状态(仅自己发送的消息)
|
||
var statusHtml = isSelf ? `<div class="msg-status ${packet.status || 'sent'}">${getStatusText(packet.status)}</div>` : '';
|
||
|
||
var msgHtml = `
|
||
<div class="${msgClass}" data-packet-id="${header.id || ''}">
|
||
${!isSelf ? `<div class="${avatarClass}">${avatarText}</div>` : ''}
|
||
<div class="message-content">
|
||
${contentHtml}
|
||
${statusHtml}
|
||
</div>
|
||
${isSelf ? `<div class="${avatarClass}">${avatarText}</div>` : ''}
|
||
</div>
|
||
`;
|
||
|
||
// 添加时间分隔(每5分钟一条)
|
||
var lastMsgTime = $('#message-list .msg-time:last').data('timestamp') || 0;
|
||
var currentTimestamp = header.server_time || Date.now();
|
||
if (currentTimestamp - lastMsgTime > 5 * 60 * 1000) {
|
||
$('#message-list').append(`<div class="msg-time" data-timestamp="${currentTimestamp}">${timeStr}</div>`);
|
||
}
|
||
|
||
$('#message-list').append(msgHtml);
|
||
scrollToBottom();
|
||
}
|
||
|
||
// ========== 工具函数 ==========
|
||
function scrollToBottom() {
|
||
var container = $('.chat-body');
|
||
container.scrollTop(container[0].scrollHeight);
|
||
}
|
||
|
||
function formatFileSize(bytes) {
|
||
if (bytes < 1024) return bytes + ' B';
|
||
else if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB';
|
||
else return (bytes / 1048576).toFixed(1) + ' MB';
|
||
}
|
||
|
||
function getStatusText(status) {
|
||
const map = { sending: '发送中', sent: '已发送', failed: '发送失败', received: '已读' };
|
||
return map[status] || '';
|
||
}
|
||
|
||
function showConnStatus(msg, type = 'connected') {
|
||
var $status = $('#conn-status');
|
||
$status.text(msg).removeClass().addClass('conn-status status-' + type).fadeIn();
|
||
setTimeout(() => $status.fadeOut(), type === 'connected' ? 2000 : 5000);
|
||
}
|
||
|
||
// ========== 初始化ChatClient ==========
|
||
function initChat() {
|
||
if (chatClient) return;
|
||
|
||
chatClient = new ChatClient({
|
||
token: access_token,
|
||
wsUrl: 'wss://dev.xixingwl.cn:2346',
|
||
uid: $('#user-uid').val(),
|
||
onMessage: function (packet) {
|
||
// 处理收到的消息
|
||
if (currentSession && packet.header &&
|
||
(packet.header.to === currentSession.id || packet.header.from === currentSession.id)) {
|
||
renderMessage(packet, false);
|
||
|
||
// 更新联系人最后消息(简化版)
|
||
if (packet.header.type === 'text') {
|
||
contacts.find(c => c.id === packet.header.from).lastMsg = packet.payload.content;
|
||
renderContacts();
|
||
}
|
||
}
|
||
},
|
||
onError: function (msg) {
|
||
layer.msg('❌ ' + msg, { icon: 2, time: 3000 });
|
||
showConnStatus('● 连接断开: ' + msg, 'disconnected');
|
||
$('#current-status').text('离线').removeClass('online');
|
||
}
|
||
});
|
||
|
||
// 连接状态监听
|
||
chatClient.onConnect = function () {
|
||
showConnStatus('● 已连接到服务器', 'connected');
|
||
$('#current-status').text('在线').addClass('online');
|
||
$('#conn-status-btn').html('<i class="layui-icon layui-icon-refresh"></i> 重连').prop('disabled', false);
|
||
|
||
// 重发队列中消息
|
||
messageQueue.forEach(msg => {
|
||
chatClient.send(msg.payload, msg.type);
|
||
});
|
||
messageQueue = [];
|
||
};
|
||
|
||
chatClient.onDisconnect = function () {
|
||
showConnStatus('● 连接已断开,正在重连...', 'disconnected');
|
||
$('#current-status').text('离线').removeClass('online');
|
||
$('#conn-status-btn').html('<i class="layui-icon layui-icon-loading layui-anim layui-anim-rotate layui-anim-loop"></i> 重连中').prop('disabled', true);
|
||
};
|
||
|
||
// 尝试连接
|
||
chatClient.connect();
|
||
}
|
||
|
||
// ========== 事件绑定 ==========
|
||
$(function () {
|
||
// 渲染初始联系人
|
||
renderContacts();
|
||
|
||
// 联系人点击
|
||
$(document).on('click', '.contact-item', function () {
|
||
$('.contact-item').removeClass('active');
|
||
$(this).addClass('active');
|
||
|
||
currentSession = {
|
||
id: $(this).data('id'),
|
||
type: $(this).data('type'),
|
||
name: $(this).data('name'),
|
||
avatar: $(this).data('avatar')
|
||
};
|
||
console.log('切换会话:', currentSession);
|
||
// 更新顶部信息
|
||
$('#current-title').text(currentSession.name);
|
||
$('#current-avatar').text(currentSession.avatar).css('background', currentSession.type === 'group' ? '#5fb878' : '#1aad19');
|
||
$('#current-status').text(currentSession.type === 'group' ? '群聊' : (contacts.find(c => c.id === currentSession.id)?.online ? '在线' : '离线'));
|
||
$('#message-list').html(`<div class="system-msg">与 ${currentSession.name} 的聊天记录</div>`);
|
||
if (!chatClient) initChat();
|
||
var contact = contacts.find(c => c.id === currentSession.id);
|
||
if (contact && contact.unread > 0) {
|
||
contact.unread = 0;
|
||
renderContacts();
|
||
}
|
||
chatClient.setCurrentSession(currentSession.id, currentSession.type);
|
||
});
|
||
|
||
// 搜索联系人
|
||
$('#contact-search').on('input', function () {
|
||
renderContacts($(this).val().trim());
|
||
});
|
||
|
||
// 发送消息
|
||
function sendMessage() {
|
||
var content = $.trim($('#chat-input').val());
|
||
if (!content || !currentSession) return;
|
||
if (!chatClient || !chatClient.isConnected) {
|
||
layer.msg('未连接服务器,请稍候...', { icon: 5 });
|
||
return;
|
||
}
|
||
console.log('发送消息:', currentSession);
|
||
// 本地预渲染(发送中状态)
|
||
var fakePacket = {
|
||
header: {
|
||
id: 'temp_' + Date.now(),
|
||
server_time: Date.now(),
|
||
from: $('#user-uid').val(),
|
||
to: currentSession.id,
|
||
type: 'text'
|
||
},
|
||
payload: { content: content },
|
||
status: 'sending'
|
||
};
|
||
renderMessage(fakePacket, true);
|
||
|
||
// 发送真实消息
|
||
var packetId = chatClient.send({ content: content }, 'text');
|
||
|
||
// 更新本地消息状态(简化版,实际用ACK回调更新)
|
||
setTimeout(() => {
|
||
$(`.message-item.self[data-packet-id="${fakePacket.header.id}"] .msg-status`)
|
||
.removeClass('sending')
|
||
.addClass('sent')
|
||
.text('已发送');
|
||
}, 300);
|
||
|
||
$('#chat-input').val('').focus();
|
||
}
|
||
|
||
$('#send-btn').click(sendMessage);
|
||
$('#chat-input').keypress(function (e) {
|
||
if (e.ctrlKey && e.which === 13) sendMessage(); // Ctrl+Enter
|
||
});
|
||
|
||
// 重连按钮
|
||
$('#conn-status-btn').click(function () {
|
||
if (chatClient) {
|
||
chatClient.disconnect();
|
||
setTimeout(() => chatClient.connect(), 500);
|
||
} else {
|
||
initChat();
|
||
}
|
||
});
|
||
|
||
// ========== 表情面板 ==========
|
||
var emojis = ['😄', '😂', '😊', '😍', '😭', '😡', '👍', '👏', '💪', '🎉', '❤️', '💯', '🙏', '🔥', '🚀', '💡'];
|
||
var emojiHtml = emojis.map(e => `<div class="emoji-item">${e}</div>`).join('');
|
||
$('#emoji-grid').html(emojiHtml);
|
||
|
||
$('#emoji-btn').click(function () {
|
||
emojiPanelVisible = !emojiPanelVisible;
|
||
$('#emoji-panel').toggle(emojiPanelVisible);
|
||
});
|
||
|
||
$(document).on('click', '.emoji-item', function () {
|
||
var currentVal = $('#chat-input').val();
|
||
$('#chat-input').val(currentVal + $(this).text()).focus();
|
||
});
|
||
|
||
// 点击外部关闭表情面板
|
||
$(document).click(function (e) {
|
||
if (emojiPanelVisible && !$(e.target).closest('#emoji-panel, #emoji-btn').length) {
|
||
$('#emoji-panel').hide();
|
||
emojiPanelVisible = false;
|
||
}
|
||
});
|
||
|
||
// ========== 图片上传 ==========
|
||
$('#image-btn').click(function () {
|
||
$('#image-upload').click();
|
||
});
|
||
|
||
$('#image-upload').change(function (e) {
|
||
var file = e.target.files[0];
|
||
if (!file) return;
|
||
|
||
if (file.size > 5 * 1024 * 1024) {
|
||
layer.msg('图片不能超过 5MB', { icon: 2 });
|
||
return;
|
||
}
|
||
|
||
// 本地预览 + 压缩(简化版,实际用browser-image-compression)
|
||
var reader = new FileReader();
|
||
reader.onload = function (ev) {
|
||
// 生成缩略图(Base64)
|
||
var img = new Image();
|
||
img.onload = function () {
|
||
var canvas = document.createElement('canvas');
|
||
var ctx = canvas.getContext('2d');
|
||
var maxWidth = 300;
|
||
var ratio = Math.min(maxWidth / img.width, 1);
|
||
canvas.width = img.width * ratio;
|
||
canvas.height = img.height * ratio;
|
||
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
|
||
|
||
var thumbBase64 = canvas.toDataURL('image/jpeg', 0.7);
|
||
|
||
// 模拟上传到CDN(实际调用上传接口)
|
||
layer.msg('正在上传图片...', { time: 0, shade: 0.3 });
|
||
|
||
setTimeout(() => {
|
||
layer.closeAll('msg');
|
||
// 模拟CDN URL
|
||
var cdnUrl = 'https://cdn.example.com/images/' + Date.now() + '.jpg';
|
||
|
||
// 发送消息(含缩略图+CDN URL)
|
||
if (chatClient && chatClient.isConnected && currentSession) {
|
||
chatClient.send({
|
||
content: {
|
||
type: file.type,
|
||
name: file.name,
|
||
size: file.size,
|
||
width: canvas.width,
|
||
height: canvas.height,
|
||
thumb: thumbBase64,
|
||
url: cdnUrl
|
||
}
|
||
}, 'image');
|
||
|
||
// 本地预渲染
|
||
renderMessage({
|
||
header: { server_time: Date.now(), type: 'image' },
|
||
payload: { content: { thumb: thumbBase64, url: cdnUrl, size: file.size } }
|
||
}, true);
|
||
}
|
||
}, 800);
|
||
};
|
||
img.src = ev.target.result;
|
||
};
|
||
reader.readAsDataURL(file);
|
||
this.value = null; // 重置input
|
||
});
|
||
|
||
// ========== 文件上传 ==========
|
||
$('#file-btn').click(function () {
|
||
$('#file-upload').click();
|
||
});
|
||
|
||
$('#file-upload').change(function (e) {
|
||
var file = e.target.files[0];
|
||
if (!file) return;
|
||
|
||
if (file.size > 50 * 1024 * 1024) {
|
||
layer.msg('文件不能超过 50MB', { icon: 2 });
|
||
return;
|
||
}
|
||
|
||
// 模拟上传
|
||
layer.msg('正在上传文件...', { time: 0, shade: 0.3 });
|
||
|
||
setTimeout(() => {
|
||
layer.closeAll('msg');
|
||
var cdnUrl = 'https://cdn.example.com/files/' + encodeURIComponent(file.name);
|
||
|
||
if (chatClient && chatClient.isConnected && currentSession) {
|
||
chatClient.send({
|
||
content: {
|
||
type: file.type || 'application/octet-stream',
|
||
name: file.name,
|
||
size: file.size,
|
||
url: cdnUrl
|
||
}
|
||
}, 'file');
|
||
|
||
// 本地预渲染
|
||
renderMessage({
|
||
header: { server_time: Date.now(), type: 'file' },
|
||
payload: {
|
||
content: {
|
||
type: file.type.split('/')[0],
|
||
name: file.name,
|
||
size: file.size,
|
||
url: cdnUrl
|
||
}
|
||
}
|
||
}, true);
|
||
}
|
||
}, 1200);
|
||
|
||
this.value = null;
|
||
});
|
||
|
||
// 初始化连接(可选:自动连接)
|
||
initChat();
|
||
|
||
// 页面卸载前清理
|
||
$(window).on('beforeunload', function () {
|
||
if (chatClient) chatClient.disconnect();
|
||
});
|
||
});
|
||
});
|
||
|
||
// ========== 全局函数(图片预览/文件下载) ==========
|
||
function previewImage(el) {
|
||
var src = el.dataset.original || el.src;
|
||
layer.photos({
|
||
photos: { "title": "", "id": Date.now(), "start": 0, "data": [{ "alt": "预览", "pid": 1, "src": src, "thumb": src }] },
|
||
anim: 5
|
||
});
|
||
}
|
||
|
||
function downloadFile(url, name) {
|
||
layer.confirm(`确定下载文件 "${name}"?`, { icon: 3, title: '文件下载' }, function (index) {
|
||
// 实际项目:跳转到下载链接或调用下载接口
|
||
layer.msg('模拟下载: ' + url, { icon: 1 });
|
||
layer.close(index);
|
||
});
|
||
}
|
||
</script>
|
||
</body>
|
||
|
||
</html> |