1010 lines
35 KiB
HTML
1010 lines
35 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>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> |