Files
YwxAppThink/addon/wxchat/view/index/index.html
T

1010 lines
35 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>UniChat - Layui 组件化聊天系统</title>
<!-- Layui CSS -->
<link rel="stylesheet" href="/assets/layui/css/layui.css">
<style>
/* 仅保留必要布局样式(Layui 无法覆盖的部分) */
:root {
--primary: #1e9fff;
--success: #5fb878;
--dark-bg: #2a2e36;
--dark-card: #323842;
}
body {
background: #f5f7fa;
font-family: "Microsoft YaHei", sans-serif;
overflow-x: hidden;
}
body.dark-mode {
background: #1a1e25;
color: #e6e9ed;
}
.chat-container {
display: flex;
height: 100vh;
padding: 3px;
}
.chat-sidebar {
width: 320px;
border-right: 1px solid #e6e6e6;
background: white;
transition: all 0.3s;
position: sticky;
}
body.dark-mode .chat-sidebar {
background: var(--dark-bg);
border-right-color: #3a3f4b;
}
.chat-main {
flex: 1;
display: flex;
flex-direction: column;
background: #f9fbfd;
}
body.dark-mode .chat-main {
background: #242830;
}
.chat-messages {
flex: 1;
padding: 20px;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 15px;
}
.message {
max-width: 70%;
padding: 10px 15px;
border-radius: 18px;
line-height: 1.5;
position: relative;
animation: fadeIn 0.3s;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.message.received {
align-self: flex-start;
background: white;
border-radius: 18px 18px 18px 5px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
}
body.dark-mode .message.received {
background: var(--dark-card);
}
.message.sent {
align-self: flex-end;
background: var(--primary);
color: white;
border-radius: 18px 18px 5px 18px;
}
.message-time {
font-size: 11px;
color: #999;
margin-top: 5px;
display: flex;
justify-content: space-between;
}
.read-status {
font-size: 10px;
opacity: 0.7;
}
.message.sent .read-status {
color: rgba(255, 255, 255, 0.8);
}
.online-badge {
position: absolute;
bottom: 8px;
right: 8px;
width: 10px;
height: 10px;
border-radius: 50%;
background: var(--success);
border: 2px solid white;
display: none;
}
.list-item.online .online-badge {
display: block;
}
body.dark-mode .online-badge {
border-color: var(--dark-bg);
}
.badge {
position: absolute;
right: 15px;
top: 50%;
transform: translateY(-50%);
min-width: 18px;
height: 18px;
border-radius: 9px;
background: #ff5722;
color: white;
font-size: 12px;
display: flex;
align-items: center;
justify-content: center;
padding: 0 4px;
display: none;
}
.badge.show {
display: flex;
}
.chat-input {
padding: 15px 20px;
border-top: 1px solid #e6e6e6;
background: white;
display: flex;
gap: 10px;
}
body.dark-mode .chat-input {
background: var(--dark-card);
border-top-color: #3a3f4b;
}
.chat-input textarea {
flex: 1;
border: 1px solid #ddd;
border-radius: 20px;
padding: 10px 15px;
resize: none;
min-height: 40px;
max-height: 120px;
font-family: inherit;
transition: all 0.3s;
}
body.dark-mode .chat-input textarea {
background: #2a2e36;
border-color: #3a3f4b;
color: #e6e9ed;
}
.chat-btn {
width: 40px;
height: 40px;
border-radius: 50%;
background: var(--primary);
color: white;
border: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
transition: all 0.3s;
}
.chat-btn:hover {
transform: scale(1.05);
}
.chat-btn.send {
background: var(--success);
}
.theme-toggle {
cursor: pointer;
font-size: 18px;
}
.layui-side-scroll {
width: 320px;
}
.layui-card {
height: 100%;
}
.layui-card-body {
padding: 0;
height: calc(100%- 200px);
}
.layui-card-footer {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 20px;
border-top: 1px solid #e6e6e6;
background: white;
bottom: 0px;
position: relative;
padding: 10px 15px;
}
@media (max-width: 768px) {
.chat-container {
flex-direction: column;
}
.chat-sidebar {
width: 100%;
max-height: 40vh;
}
.chat-main {
height: 60vh;
}
.message {
max-width: 85%;
}
}
</style>
<link rel="stylesheet" href="/assets/ywxapp/css/ywxapp.css">
<style>
body {
display: flex;
align-items: center;
height: 100vh;
justify-content: center;
}
.chat-container {
max-height: 720px;
min-width: 1024px;
}
.chat-input {
display: flex;
flex-direction: column;
}
.chat-actions {
display: flex;
gap: 10px;
justify-content: flex-start;
flex-direction: row;
width: -webkit-fill-available;
}
.chat-btn {
width: 36px;
height: 36px;
margin-left: 5px;
}
</style>
</head>
<body class="layui-layout-body">
<!-- 主容器 -->
<div class="chat-container">
<!-- 侧边栏 -->
<div class="layui-side chat-sidebar ">
<div class="layui-side-scroll">
<div class="layui-card">
<div class="layui-card-body">
<div class="layui-tab layui-tab-brief" lay-filter="sidebar-tabs">
<ul class="layui-tab-title">
<li class="layui-this" data-tab="chats"><i class="layui-icon layui-icon-chat"></i> 聊天</li>
<li data-tab="friends"><i class="layui-icon layui-icon-friends"></i> 好友</li>
<li data-tab="groups"><i class="layui-icon layui-icon-group"></i> 群组</li>
</ul>
<div class="layui-tab-content">
<!-- 聊天列表 -->
<div class="layui-tab-item layui-show">
<div class="layui-input-block" style="margin: 10px 0;">
<input type="text" id="chat-search" placeholder="搜索聊天..." class="layui-input">
</div>
<div class="chat-list" id="chat-list"></div>
</div>
<!-- 好友列表 -->
<div class="layui-tab-item">
<div class="layui-input-block" style="margin: 10px 0; display: flex; gap: 8px;">
<input type="text" id="friend-search" placeholder="搜索好友" class="layui-input" style="flex:1;">
<button class="layui-btn layui-btn-sm" id="add-friend-btn"><i class="layui-icon layui-icon-add-1"></i></button>
</div>
<div class="friend-list" id="friend-list"></div>
</div>
<!-- 群组列表 -->
<div class="layui-tab-item">
<div class="layui-input-block" style="margin: 10px 0; display: flex; gap: 8px;">
<input type="text" id="group-search" placeholder="搜索群组" class="layui-input" style="flex:1;">
<button class="layui-btn layui-btn-sm" id="add-group-btn"><i class="layui-icon layui-icon-add-1"></i></button>
</div>
<div class="group-list" id="group-list"></div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 主聊天区域 -->
<div class="chat-main">
<div class="ywxapp-card">
<div class="ywxapp-card-header" id="chat-title">
<span>请选择聊天对象</span>
<span class="layui-badge layui-bg-gray" id="chat-status"></span>
<div class="layui-btn-group layui-fr">
<button class="layui-btn layui-btn-sm layui-btn-primary" id="clear-chat"><i class="layui-icon layui-icon-delete"></i> 清空</button>
</div>
</div>
<div class="ywxapp-card-body chat-messages" id="message-container">
<div class="message message-system">✨ 欢迎使用 UniChat!请选择左侧聊天对象开始对话</div>
</div>
<div class="ywxapp-card-footer chat-input">
<div class="chat-actions">
<button class="chat-btn" id="face-btn" title="表情"><i class="layui-icon layui-icon-face-smile"></i></button>
<button class="chat-btn" id="image-btn" title="图片"><i class="layui-icon layui-icon-picture"></i></button>
</div>
<div style=" display: flex;flex: 1; flex-direction: row;width: 100%; align-items: center;">
<textarea id="message-input" placeholder="输入消息... (Ctrl+Enter发送)" class="layui-textarea" style="lex: 1;"></textarea>
<button class="chat-btn send" id="send-btn" title="发送"><i class="layui-icon layui-icon-send"></i></button>
</div>
</div>
</div>
</div>
</div>
<!-- 表情面板模板 -->
<script type="text/html" id="face-panel">
<div class="face-panel" style="padding: 15px; max-height: 300px; overflow-y: auto;">
{{# layui.each(d.faces, function(index, item){ }}
<span class="face-item" data-face="{{item}}" style="display: inline-block; width: 30px; height: 30px; line-height: 30px; text-align: center; cursor: pointer; margin: 3px; border-radius: 4px; transition: all 0.2s;" onmouseover="this.style.background='#f2f2f2'" onmouseout="this.style.background='transparent'">
{{item}}
</span>
{{# }); }}
</div>
</script>
<!-- 消息模板 -->
<script type="text/html" id="message-tpl">
{{# if(d.type === 'system'){ }}
<div class="message message-system">{{d.content}}</div>
{{# } else { }}
<div class="message {{d.direction}} {{d.read_status === 'read' && d.direction === 'sent' ? 'read' : ''}}" data-msgid="{{d.msg_id}}">
<div>{{d.content}}</div>
<div class="message-time">
<span>{{layui.util.toDateString(d.create_time, 'HH:mm')}}</span>
{{# if(d.direction === 'sent'){ }}
<span class="read-status">{{ d.read_status === 'read' ? '已读' : (d.read_status === 'sending' ? '发送中...' : '未读') }}</span>
{{# } }}
</div>
</div>
{{# } }}
</script>
<!-- 列表项模板 -->
<script type="text/html" id="list-item-tpl">
<div class="list-item {{d.online ? 'online' : ''}}" data-id="{{d.id}}" data-type="{{d.type}}" style="padding: 12px 15px; border-bottom: 1px solid #f0f0f0; display: flex; align-items: center; gap: 12px; cursor: pointer; position: relative;">
<div class="layui-badge-dot {{d.online ? 'layui-bg-green' : ''}}" style="position: absolute; bottom: 8px; right: 8px; {{d.online ? '' : 'display:none'}}"></div>
<div class="layui-nav-img" style="background: {{d.type === 'group' ? '#ff9800' : '#1e9fff'}}; color: white; width: 40px; height: 40px; line-height: 40px; text-align: center; border-radius: 50%;">
{{d.avatar}}
</div>
<div style="flex:1; min-width:0;">
<div class="layui-elip" style="font-weight:500; margin-bottom:3px;">{{d.name}}</div>
<div class="layui-elip" style="font-size:12px; color:#888;">{{d.last_msg}}</div>
</div>
<div style="text-align:right; min-width:60px;">
<div style="font-size:11px; color:#999;">{{d.time}}</div>
{{# if(d.unread > 0){ }}
<span class="layui-badge">{{d.unread > 99 ? '99+' : d.unread}}</span>
{{# } }}
</div>
</div>
</script>
<!-- 引入 Layui -->
<script src="/assets/layui/layui.js"></script>
<script src="/assets/ywxapp/ywxapp.js" module="member"></script>
<!-- 定义 ChatClient Layui 模块 -->
<script>
// ==================== 页面主逻辑 ====================
layui.use(['element', 'jquery', 'layer', 'laytpl', 'util', 'upload', 'setter', 'chatClient'], function () {
const element = layui.element;
const $ = layui.jquery;
const layer = layui.layer;
const laytpl = layui.laytpl;
const util = layui.util;
const upload = layui.upload;
const ChatClient = layui.chatClient;
const { access_token } = layui.setter.getTokens();
// 初始化主题
const initTheme = () => {
const isDark = localStorage.getItem('uni-chat-theme') === 'dark' ||
(!localStorage.getItem('uni-chat-theme') && window.matchMedia('(prefers-color-scheme: dark)').matches);
if (isDark) {
$('body').addClass('dark-mode');
$('#theme-toggle').html('<i class="layui-icon layui-icon-dark"></i>');
localStorage.setItem('uni-chat-theme', 'dark');
} else {
$('body').removeClass('dark-mode');
$('#theme-toggle').html('<i class="layui-icon layui-icon-light"></i>');
localStorage.setItem('uni-chat-theme', 'light');
}
};
// 切换主题
const toggleTheme = () => {
const isDark = $('body').toggleClass('dark-mode').hasClass('dark-mode');
$('#theme-toggle').html(isDark ?
'<i class="layui-icon layui-icon-dark"></i>' :
'<i class="layui-icon layui-icon-light"></i>');
localStorage.setItem('uni-chat-theme', isDark ? 'dark' : 'light');
layer.msg(`✨ 已切换至${isDark ? '深色' : '浅色'}模式`, {
icon: isDark ? 16 : 15,
time: 1500
});
};
// 初始化用户数据
const userData = {
uid: 'user_1001',
nickname: '前端开发者',
avatar: 'U',
token: 'mock_token_123456'
};
$('#current-nickname').text(userData.nickname);
// 初始化 ChatClient
const chatClient = new ChatClient({
wsUrl: 'wss://dev.xixingwl.cn:2346', // 实际替换为你的 GatewayWorker 地址
token: access_token,
});
// ==================== 事件绑定 ====================
// 主题切换
$('#theme-toggle').on('click', toggleTheme);
// 消息输入
$('#message-input').on('keydown', function (e) {
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
sendMessage();
}
});
// 发送消息
$('#send-btn').on('click', sendMessage);
// 清空聊天
$('#clear-chat').on('click', function () {
$('#message-container').html('');
renderMessage({ type: 'system', content: '💬 聊天记录已清空' });
});
// 添加好友
$('#add-friend-btn').on('click', function () {
showSearchModal('user');
});
// 添加群组
$('#add-group-btn').on('click', function () {
showSearchModal('group');
});
// 搜索过滤
$('#chat-search, #friend-search, #group-search').on('input', function () {
const keyword = $(this).val().toLowerCase();
const containerId = $(this).attr('id').replace('-search', '-list');
$(`#${containerId} .list-item`).each(function () {
const name = $(this).find('.layui-elip:first').text().toLowerCase();
const desc = $(this).find('.layui-elip:last').text().toLowerCase();
$(this).toggle(name.includes(keyword) || desc.includes(keyword));
});
});
// ==================== 表情面板 ====================
const initFacePanel = () => {
// 表情数据(常用emoji
const faces = [
'😀', '😁', '😂', '🤣', '😃', '😄', '😅', '😆', '😉', '😊',
'😋', '😎', '😍', '😘', '😗', '😙', '😚', '🙂', '🤗', '🤔',
'😐', '😑', '😶', '🙄', '😏', '😣', '😥', '😮', '🤐', '😯',
'😪', '😫', '😴', '😌', '🤓', '😛', '😜', '😝', '🤤', '😒',
'😓', '😔', '😕', '🙃', '🤑', '😲', '🙁', '😖', '😞', '😟'
];
// 编译模板
const getTpl = document.getElementById('face-panel').innerHTML;
const faceView = laytpl(getTpl).render({ faces: faces });
// 绑定表情按钮
$('#face-btn').on('click', function () {
layer.open({
type: 1,
title: '😄 选择表情',
area: ['400px', '350px'],
shadeClose: true,
content: faceView,
success: function (layero) {
// 表情点击事件
$(layero).find('.face-item').on('click', function () {
const face = $(this).data('face');
const input = $('#message-input');
input.val(input.val() + face).focus();
layer.closeAll('page');
});
}
});
});
};
// ==================== 图片上传 ====================
const initImageUpload = () => {
upload.render({
elem: '#image-btn',
url: '/upload/', // 实际替换为你的上传接口
accept: 'images',
exts: 'jpg|png|gif|bmp|jpeg',
size: 2048, // 限制2MB
done: function (res) {
if (res.code === 0) {
// 发送图片消息
if (chatClient.currentSession.id) {
chatClient.sendImage(
chatClient.currentSession.id,
chatClient.currentSession.type,
res.data.src
);
} else {
layer.msg('请先选择聊天对象', { icon: 2 });
}
} else {
layer.msg('上传失败: ' + res.msg, { icon: 2 });
}
},
error: function () {
layer.msg('上传异常', { icon: 2 });
},
// 模拟上传(实际项目中移除)
auto: false,
bindAction: '#image-btn',
choose: function (obj) {
// 预览图片
obj.preview(function (index, file, result) {
layer.confirm('确认发送此图片?', {
btn: ['发送', '取消'],
title: '📷 图片预览'
}, function (index) {
layer.close(index);
// 模拟上传成功
setTimeout(() => {
if (chatClient.currentSession.id) {
chatClient.sendImage(
chatClient.currentSession.id,
chatClient.currentSession.type,
result // 实际应为服务器返回的URL
);
}
}, 300);
});
});
}
});
};
// ==================== 消息渲染 ====================
const renderMessage = (msg) => {
const direction = msg.from === userData.uid ? 'sent' : 'received';
const getTpl = document.getElementById('message-tpl').innerHTML;
const view = laytpl(getTpl).render({
...msg,
direction: direction,
create_time: msg.create_time || new Date().toISOString()
});
$('#message-container').append(view);
// 滚动到底部
const container = document.getElementById('message-container');
container.scrollTop = container.scrollHeight;
// 如果是收到的消息且是当前会话,发送已读回执
if (direction === 'received' &&
chatClient.currentSession.id === msg.session_id &&
chatClient.currentSession.type === msg.session_type) {
chatClient.sendReadReceipt(msg.msg_id, msg.session_id, msg.session_type);
}
};
// 渲染列表项
const renderListItem = (item, container) => {
const getTpl = document.getElementById('list-item-tpl').innerHTML;
const view = laytpl(getTpl).render(item);
$(container).append(view);
// 绑定点击事件
$(container).find(`[data-id="${item.id}"]`).off('click').on('click', function () {
selectChat(item.id, item.type);
});
};
// ==================== 会话选择 ====================
const selectChat = (sessionId, sessionType) => {
// 更新当前会话
chatClient.setCurrentSession(sessionId, sessionType);
// 更新UI
$('.list-item').removeClass('layui-this');
$(`.list-item[data-id="${sessionId}"]`).addClass('layui-this');
// 更新标题
const name = $(`.list-item[data-id="${sessionId}"] .layui-elip:first`).text();
const title = sessionType === 'group' ? name : `与 ${name} 的对话`;
$('#chat-title span:first').text(title);
$('#chat-status').text(sessionType === 'group' ? '群组' : '好友').removeClass('layui-bg-gray').addClass(sessionType === 'group' ? 'layui-bg-orange' : 'layui-bg-blue');
// 清空消息区域
$('#message-container').html('<div class="message message-system">🕗 正在加载历史消息...</div>');
// 模拟加载历史消息
setTimeout(() => {
$('#message-container').html('');
// 模拟历史消息
const mockMessages = [
{ msg_id: '1', from: sessionId, content: '你好,这个需求什么时候能完成?', type: 'text', create_time: '2026-02-27T14:30:00' },
{ msg_id: '2', from: userData.uid, content: '预计明天下午可以交付', type: 'text', create_time: '2026-02-27T14:32:00' },
{ msg_id: '3', from: sessionId, content: '好的,辛苦了!', type: 'text', create_time: '2026-02-27T14:33:00' },
{ msg_id: '4', from: sessionId, content: '另外有个小问题需要确认', type: 'text', create_time: '2026-02-27T14:35:00' }
];
mockMessages.forEach(msg => renderMessage(msg));
renderMessage({ type: 'system', content: '📌 以上是历史消息' });
}, 500);
};
// ==================== 搜索模态框 ====================
const showSearchModal = (type) => {
const title = type === 'user' ? '🔍 搜索用户' : '🔍 搜索群组';
const placeholder = type === 'user' ? '输入昵称或ID' : '输入群组名称或ID';
layer.open({
type: 1,
title: title,
area: ['450px', '400px'],
shadeClose: true,
content: `
<div style="padding:15px;">
<input type="text" id="search-modal-input" placeholder="${placeholder}" class="layui-input" style="margin-bottom:15px;">
<div id="search-results" style="max-height:320px;overflow-y:auto;">
<div style="text-align:center;padding:20px;color:#999;">请输入关键词搜索</div>
</div>
</div>
`,
success: function () {
const input = $('#search-modal-input');
const results = $('#search-results');
input.focus();
input.on('input', util.debounce(function () {
const keyword = $(this).val().trim();
if (keyword.length < 2) {
results.html('<div style="text-align:center;padding:20px;color:#999;">请输入至少2个字符</div>');
return;
}
results.html('<div style="text-align:center;padding:20px;"><i class="layui-icon layui-icon-loading layui-anim layui-anim-rotate layui-anim-loop"></i> 搜索中...</div>');
if (type === 'user') {
chatClient.searchUsers(keyword, (res) => {
if (res.code === 0 && res.data.length > 0) {
renderSearchResults(res.data, type, results);
} else {
results.html('<div style="text-align:center;padding:20px;color:#999;">未找到相关用户</div>');
}
});
} else {
// 模拟群组搜索
setTimeout(() => {
const mockGroups = [
{ id: 'g101', name: '前端技术交流', avatar: 'F', member_count: 128, is_member: false },
{ id: 'g102', name: '全栈开发联盟', avatar: 'A', member_count: 256, is_member: true },
{ id: 'g103', name: '设计与产品', avatar: 'D', member_count: 89, is_member: false }
].filter(g => g.name.includes(keyword) || g.id.includes(keyword));
if (mockGroups.length > 0) {
renderSearchResults(mockGroups, type, results);
} else {
results.html('<div style="text-align:center;padding:20px;color:#999;">未找到相关群组</div>');
}
}, 300);
}
}, 300));
}
});
};
const renderSearchResults = (items, type, container) => {
container.html('');
items.forEach(item => {
const isFriend = type === 'user' && item.is_friend;
const isMember = type === 'group' && item.is_member;
const actionText = isFriend ? '已是好友' : (isMember ? '已在群组' : (type === 'user' ? '添加' : '申请加入'));
const actionClass = isFriend || isMember ? 'layui-btn-disabled' : 'layui-btn-primary';
const html = `
<div class="list-item" data-id="${item.uid || item.id}" style="padding:12px 15px; border-bottom:1px solid #f0f0f0; display:flex; align-items:center; gap:12px;">
<div class="layui-nav-img" style="background:${isFriend || isMember ? '#5fb878' : (type === 'group' ? '#ff9800' : '#1e9fff')}; color:white; width:40px; height:40px; line-height:40px; text-align:center; border-radius:50%;">
${(item.avatar || (type === 'user' ? item.nickname : item.name)).charAt(0).toUpperCase()}
</div>
<div style="flex:1;">
<div style="font-weight:500;">${type === 'user' ? item.nickname : item.name}</div>
<div style="font-size:12px; color:#888;">${type === 'user' ? 'UID: ' + item.uid : `${item.member_count} 成员`}</div>
</div>
<button class="layui-btn layui-btn-sm ${actionClass} action-btn" ${isFriend || isMember ? 'disabled' : ''}>
${actionText}
</button>
</div>
`;
container.append(html);
// 绑定添加事件
if (!isFriend && !isMember) {
container.find(`[data-id="${item.uid || item.id}"] .action-btn`).off('click').on('click', function () {
if (type === 'user') {
layer.prompt({
title: '添加好友备注',
value: '你好,加个好友吧~',
formType: 2
}, function (value, index) {
chatClient.addFriend(item.uid, value, (res) => {
if (res.payload?.success) {
layer.close(index);
layer.msg('✅ 好友申请已发送', { icon: 1 });
$(this).addClass('layui-btn-disabled').text('已申请').prop('disabled', true);
} else {
layer.msg('❌ ' + (res.payload?.message || '操作失败'), { icon: 2 });
}
});
});
} else {
layer.confirm(`确定申请加入群组 "${item.name}"`, { icon: 3 }, function () {
layer.msg('✅ 申请已提交,请等待管理员审核', { icon: 1 });
container.find(`[data-id="${item.id}"] .action-btn`)
.addClass('layui-btn-disabled')
.text('已申请')
.prop('disabled', true);
});
}
});
}
});
};
// ==================== ChatClient 事件处理 ====================
// 连接成功
chatClient.on('connect', () => {
layer.msg('🟢 WebSocket 连接成功', { icon: 1, time: 1500 });
loadChatList();
loadFriends();
loadGroups();
});
// 消息接收
chatClient.on('new-message', (payload) => {
const { message } = payload;
// 更新未读数
updateUnreadCount(message.session_id, 1);
// 如果是当前会话,直接渲染并标记已读
if (chatClient.currentSession.id === message.session_id &&
chatClient.currentSession.type === message.session_type) {
renderMessage(message);
// 发送已读回执
chatClient.sendReadReceipt(message.msg_id, message.session_id, message.session_type);
} else {
// 非当前会话,提示新消息
if (!document.hidden) {
layer.msg(`新消息: ${message.content.substring(0, 20)}...`, { time: 2000 });
}
}
});
// 消息已读回执
chatClient.on('message-read', (payload) => {
const { msg_id } = payload;
// 更新消息状态
$(`.message[data-msgid="${msg_id}"] .read-status`).text('已读');
});
// 本地消息(刚发送的消息)
chatClient.on('local-message', (message) => {
renderMessage(message);
});
// 消息发送成功
chatClient.on('message-sent', (message) => {
$(`.message[data-msgid="${message.msg_id}"] .read-status`).text('未读');
});
// 好友申请通知
chatClient.on('friend-apply', (payload) => {
const { apply_id, applicant_info, remark } = payload;
layer.confirm(
`<div style="padding:10px 0;">
<div style="display:flex;align-items:center;margin-bottom:10px;">
<div class="layui-nav-img" style="background:#1e9fff;color:white;width:40px;height:40px;line-height:40px;text-align:center;border-radius:50%;margin-right:10px;">
${applicant_info.nickname.charAt(0)}
</div>
<div>
<div style="font-weight:bold">${applicant_info.nickname}</div>
<div style="color:#888;font-size:13px">请求添加你为好友</div>
</div>
</div>
<div style="background:#f8f9fa;padding:8px;border-radius:4px;margin-top:5px;">"${remark}"</div>
</div>`,
{
title: '🤝 好友申请',
btn: ['同意', '拒绝'],
shadeClose: false
},
function (index) {
chatClient.handleFriendApply(apply_id, true, (res) => {
layer.close(index);
if (res.payload?.success) {
layer.msg(`已添加 ${applicant_info.nickname} 为好友`, { icon: 1 });
loadFriends();
loadChatList();
}
});
},
function () {
chatClient.handleFriendApply(apply_id, false);
layer.msg('已拒绝申请', { icon: 2 });
}
);
});
// 好友申请结果
chatClient.on('friend-apply-result', (payload) => {
const { result, target_info } = payload;
const msg = result === 'accepted'
? `✅ 已成功添加 ${target_info.nickname} 为好友!`
: `❌ 对方拒绝了你的好友申请`;
layer.msg(msg, { icon: result === 'accepted' ? 1 : 2, time: 3000 });
if (result === 'accepted') {
loadFriends();
loadChatList();
}
});
// 连接关闭
chatClient.on('close', () => {
if (!document.hidden) {
layer.msg('🔴 连接已断开,尝试重连...', { icon: 2, time: 2000 });
}
});
// 重连失败
chatClient.on('reconnect-failed', () => {
layer.msg('连接失败,请检查网络', { icon: 2 });
});
// ==================== 数据加载(模拟) ====================
const loadChatList = () => {
const chats = [
{ id: 'u2001', type: 'single', name: '设计师小王', avatar: 'W', last_msg: '原型图已更新,请查收', time: '15:30', unread: 2, online: true },
{ id: 'g101', type: 'group', name: '前端技术交流', avatar: 'F', last_msg: '[图片] 新版UI设计稿', time: '14:20', unread: 5, online: false },
{ id: 'u2003', type: 'single', name: '后端工程师赵', avatar: 'Z', last_msg: '接口文档已发布', time: '昨天', unread: 0, online: false },
{ id: 'g102', type: 'group', name: '全栈开发联盟', avatar: 'A', last_msg: '今晚8点技术分享', time: '09:15', unread: 0, online: false }
];
$('#chat-list').html('');
chats.forEach(chat => renderListItem(chat, '#chat-list'));
};
const loadFriends = () => {
const friends = [
{ id: 'u2001', type: 'single', name: '设计师小王', avatar: 'W', last_msg: '在线', time: '', unread: 0, online: true },
{ id: 'u2002', type: 'single', name: '产品经理李', avatar: 'L', last_msg: '最后在线: 昨天 15:30', time: '', unread: 0, online: false },
{ id: 'u2003', type: 'single', name: '后端工程师赵', avatar: 'Z', last_msg: '最后在线: 2026-02-26', time: '', unread: 0, online: false },
{ id: 'u2004', type: 'single', name: '测试工程师周', avatar: 'Z', last_msg: '在线', time: '', unread: 0, online: true }
];
$('#friend-list').html('');
friends.forEach(friend => renderListItem(friend, '#friend-list'));
};
const loadGroups = () => {
const groups = [
{ id: 'g101', type: 'group', name: '前端技术交流', avatar: 'F', last_msg: '128成员', time: '', unread: 3, online: false },
{ id: 'g102', type: 'group', name: '全栈开发联盟', avatar: 'A', last_msg: '256成员', time: '', unread: 0, online: false },
{ id: 'g103', type: 'group', name: '设计与产品', avatar: 'D', last_msg: '89成员', time: '', unread: 0, online: false },
{ id: 'g104', type: 'group', name: '项目管理组', avatar: 'P', last_msg: '42成员', time: '', unread: 1, online: false }
];
$('#group-list').html('');
groups.forEach(group => renderListItem(group, '#group-list'));
};
const updateUnreadCount = (sessionId, delta) => {
const item = $(`.list-item[data-id="${sessionId}"]`);
const badge = item.find('.layui-badge');
let count = parseInt(badge.text()) || 0;
count = Math.max(0, count + delta);
if (count > 0) {
badge.text(count > 99 ? '99+' : count).show();
} else {
badge.hide();
}
};
// ==================== 初始化 ====================
const init = () => {
initTheme();
initFacePanel();
initImageUpload();
// 模拟初始数据加载
setTimeout(() => {
loadChatList();
loadFriends();
loadGroups();
}, 500);
};
// 启动应用
init();
// ==================== 辅助函数 ====================
function sendMessage() {
const content = $('#message-input').val().trim();
if (!content || !chatClient.currentSession.id) {
if (!chatClient.currentSession.id) {
layer.msg('请先选择聊天对象', { icon: 2 });
}
return;
}
// 发送消息(ChatClient 会触发 local-message 事件)
chatClient.sendMessage(
chatClient.currentSession.id,
chatClient.currentSession.type,
content
);
// 清空输入框
$('#message-input').val('').focus();
}
});
</script>
</body>
</html>