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

1277 lines
47 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">
<!-- Layui CSS -->
<link rel="stylesheet" href="/assets/layui/css/layui.css">
<title>纯 CSS 垂直选项卡 | 左侧标题 + 右侧内容</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', 'Microsoft YaHei', sans-serif;
background: linear-gradient(135deg, #f0f4f8 0%, #e6e9ff 100%);
color: #334155;
line-height: 1.6;
padding: 20px;
min-height: 100vh;
}
.wxapp-tab-container {
max-width: 1024px;
margin: 30px auto;
background: white;
border-radius: 16px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.12);
overflow: hidden;
}
header {
text-align: center;
padding: 10px;
background: linear-gradient(120deg, #4361ee, #3a0ca3);
color: white;
}
h1 {
font-size: 2.4rem;
margin-bottom: 12px;
letter-spacing: -0.5px;
}
.subtitle {
font-size: 0.8rem;
opacity: 0.9;
max-width: 600px;
margin: 0 auto;
}
/* ===== 核心:垂直选项卡布局 ===== */
.vertical-tabs {
display: flex;
min-height: 500px;
position: relative;
}
/* 隐藏 radio 按钮,但保留可访问性 */
.vertical-tabs input[type="radio"] {
position: absolute;
opacity: 0;
width: 0;
height: 0;
}
/* 左侧标题区域 */
.wxapp-tab-titles {
display: flex;
flex-direction: column;
width: 60px;
background: linear-gradient(180deg, #f8fafc 0%, #eef2f7 100%);
border-right: 1px solid #e2e8f0;
padding: 20px 0;
position: relative;
overflow: hidden;
}
/* 标题项样式 */
.wxapp-tab-titles label {
padding: 18px 25px;
cursor: pointer;
font-weight: 500;
font-size: 1.05rem;
color: #4a5568;
transition: all 0.35s cubic-bezier(0.165, 0.84, 0.44, 1);
position: relative;
border-left: 4px solid transparent;
display: flex;
align-items: center;
gap: 12px;
}
.wxapp-tab-titles label::before {
content: "";
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 4px;
background: #4361ee;
transform: scaleY(0);
transform-origin: bottom;
transition: transform 0.4s cubic-bezier(0.23, 1, 0.32, 1);
}
.wxapp-tab-titles label:hover {
background: #edf2f7;
color: #2b6cb0;
transform: translateX(4px);
}
.wxapp-tab-titles label i {
font-size: 1.3rem;
width: 24px;
text-align: center;
}
/* 激活状态样式 */
#tab1:checked~.wxapp-tab-titles label[for="tab1"],
#tab2:checked~.wxapp-tab-titles label[for="tab2"],
#tab3:checked~.wxapp-tab-titles label[for="tab3"],
#tab4:checked~.wxapp-tab-titles label[for="tab4"],
#tab5:checked~.wxapp-tab-titles label[for="tab5"] {
color: #4361ee;
background: white;
font-weight: 600;
border-left: 4px solid #4361ee;
box-shadow: 4px 0 12px rgba(67, 97, 238, 0.15);
transform: translateX(4px);
}
#tab1:checked~.wxapp-tab-titles label[for="tab1"]::before,
#tab2:checked~.wxapp-tab-titles label[for="tab2"]::before,
#tab3:checked~.wxapp-tab-titles label[for="tab3"]::before,
#tab4:checked~.wxapp-tab-titles label[for="tab4"]::before,
#tab5:checked~.wxapp-tab-titles label[for="tab5"]::before {
transform: scaleY(1);
}
/* 右侧内容区域 */
.wxapp-tab-contents {
flex: 1;
position: relative;
overflow: hidden;
background: white;
padding: 35px;
}
.wxapp-tab-content {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
padding: 5px;
opacity: 0;
transform: translateX(20px);
transition: all 0.5s cubic-bezier(0.165, 0.84, 0.44, 1);
pointer-events: none;
}
/* 显示激活的内容 */
#tab1:checked~.wxapp-tab-contents .content-1,
#tab2:checked~.wxapp-tab-contents .content-2,
#tab3:checked~.wxapp-tab-contents .content-3,
#tab4:checked~.wxapp-tab-contents .content-4,
#tab5:checked~.wxapp-tab-contents .content-5 {
opacity: 1;
transform: translateX(0);
pointer-events: all;
z-index: 10;
}
/* 内容样式 */
.wxapp-tab-content h2 {
font-size: 1.9rem;
color: #1e293b;
margin-bottom: 20px;
padding-bottom: 12px;
border-bottom: 2px solid #f1f5f9;
display: flex;
align-items: center;
gap: 12px;
}
.wxapp-tab-content h2 i {
color: #4361ee;
font-size: 1.8rem;
}
.wxapp-tab-content p {
margin: 15px 0;
font-size: 1.05rem;
color: #475569;
line-height: 1.7;
}
.features-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin-top: 25px;
}
.feature-card {
background: #f8fafc;
border-radius: 12px;
padding: 20px;
border-left: 3px solid #4361ee;
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.06);
transition: transform 0.3s ease;
}
.feature-card:hover {
transform: translateY(-3px);
}
.feature-card h3 {
display: flex;
align-items: center;
gap: 10px;
color: #1e3a8a;
margin-bottom: 10px;
font-size: 1.3rem;
}
.feature-card h3 i {
font-size: 1.4rem;
}
/* 响应式设计 */
@media (max-width: 768px) {
.vertical-tabs {
flex-direction: column;
min-height: auto;
}
.wxapp-tab-titles {
width: 100%;
border-right: none;
border-bottom: 1px solid #e2e8f0;
flex-direction: row;
padding: 10px 0;
overflow-x: auto;
}
.wxapp-tab-titles label {
padding: 15px;
font-size: 0.95rem;
white-space: nowrap;
border-left: none;
border-bottom: 3px solid transparent;
}
.wxapp-tab-titles label::before {
display: none;
}
#tab1:checked~.wxapp-tab-titles label[for="tab1"],
#tab2:checked~.wxapp-tab-titles label[for="tab2"],
#tab3:checked~.wxapp-tab-titles label[for="tab3"],
#tab4:checked~.wxapp-tab-titles label[for="tab4"],
#tab5:checked~.wxapp-tab-titles label[for="tab5"] {
border-bottom: 3px solid #4361ee;
border-left: none;
transform: none;
box-shadow: none;
}
.wxapp-tab-contents {
padding: 5px;
}
.wxapp-tab-content {
position: relative;
padding: 15px 0;
transform: none !important;
}
}
footer {
text-align: center;
padding: 5px;
color: #64748b;
font-size: 0.95rem;
border-top: 1px solid #f1f5f9;
background: #f8fafc;
}
/* 图标字体(简化版) */
.icon::before {
font-family: 'Segoe UI Emoji', sans-serif;
margin-right: 8px;
}
.icon-home::before {
content: "🏠";
}
.icon-code::before {
content: "💻";
}
.icon-design::before {
content: "🎨";
}
.icon-data::before {
content: "📊";
}
.icon-check::before {
content: "✓";
color: #10b981;
}
</style>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,
body {
height: 100%;
}
/* 左右结构容器 */
.container {
display: flex;
height: 100%;
}
/* 左侧定宽区域 */
.left {
width: 200px;
background-color: #4CAF50;
color: white;
padding: 20px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
/* 右侧自适应区域 */
.right {
flex: 1;
background-color: #f1f1f1;
display: flex;
flex-direction: column;
}
/* 右侧内部上中下结构 */
.right-header {
height: 60px;
background-color: #2196F3;
color: white;
display: flex;
align-items: center;
justify-content: center;
}
.right-content {
flex: 1;
background-color: #ffffff;
padding: 20px;
overflow-y: auto;
}
.right-footer {
height: 40px;
background-color: #ff9800;
color: white;
display: flex;
align-items: center;
justify-content: center;
}
</style>
</head>
<body>
<div class="wxapp-tab-container">
<header>
<h1>✨ 纯 CSS 垂直选项卡</h1>
<p class="subtitle">左侧导航 + 右侧内容区域 | 无 JavaScript | Flexbox 布局 | 响应式设计 | 平滑动画</p>
</header>
<div class="vertical-tabs">
<!-- 隐藏的 radio 按钮(控制选项卡切换) -->
<input type="radio" name="tabs" id="tab1" checked>
<input type="radio" name="tabs" id="tab2">
<input type="radio" name="tabs" id="tab3">
<input type="radio" name="tabs" id="tab4">
<input type="radio" name="tabs" id="tab5">
<!-- 左侧标题区域 -->
<div class="wxapp-tab-titles">
<label for="tab1" class="icon icon-home">消息</label>
<label for="tab2" class="icon icon-code">通讯录</label>
<label for="tab3" class="icon icon-design">圈子</label>
<label for="tab4" class="icon icon-data">场景</label>
<label for="tab5" class="icon icon-data">其它</label>
</div>
<!-- 右侧内容区域 -->
<div class="wxapp-tab-contents">
<div class="wxapp-tab-content content-1">
<h2><span class="icon icon-home"></span>首页概览</h2>
<p>这是一个完全使用 <strong>纯 CSS 实现</strong> 的垂直选项卡组件,无需任何 JavaScript 代码。</p>
<p>左侧为垂直排列的导航标题,右侧为对应的内容区域。通过 CSS 的 <code>:checked</code> 伪类和兄弟选择器实现切换逻辑。</p>
<p>组件特点:</p>
<ul style="padding-left: 25px; margin: 15px 0;">
<li>✅ 100% 纯 CSS 实现,无 JavaScript 依赖</li>
<li>✅ 语义化 HTML 结构,无障碍友好</li>
<li>✅ Flexbox 布局,响应式设计</li>
<li>✅ 平滑动画过渡效果</li>
<li>✅ 激活状态视觉反馈清晰</li>
</ul>
</div>
<div class="wxapp-tab-content content-2">
<h2><span class="icon icon-code"></span>技术实现</h2>
<p><strong>核心原理:</strong> 利用隐藏的 radio 按钮的 <code>:checked</code> 状态,通过 CSS 兄弟选择器控制内容显示。</p>
<div class="features-grid">
<div class="feature-card">
<h3 class="icon icon-check">HTML 结构</h3>
<p>radio 按钮 + label 作为标题,内容区域使用绝对定位</p>
</div>
<div class="feature-card">
<h3 class="icon icon-check">CSS 选择器</h3>
<p><code>#tab1:checked ~ .tab-contents .content-1</code></p>
</div>
<div class="feature-card">
<h3 class="icon icon-check">Flexbox 布局</h3>
<p>容器 <code>display: flex</code>,标题区域 <code>flex-direction: column</code></p>
</div>
<div class="feature-card">
<h3 class="icon icon-check">动画效果</h3>
<p>使用 <code>opacity</code> + <code>transform</code> 实现平滑过渡</p>
</div>
</div>
<p style="margin-top: 25px; padding-top: 15px; border-top: 1px dashed #cbd5e1;">
<strong>关键 CSS 片段:</strong>
</p>
<pre style="background: #f8fafc; border-radius: 8px; padding: 15px; margin-top: 10px; font-family: monospace; font-size: 0.9rem; line-height: 1.5; overflow-x: auto;">
#tab1:checked ~ .tab-contents .content-1 {
opacity: 1;
transform: translateX(0);
pointer-events: all;
}</pre>
</div>
<div class="wxapp-tab-content content-3">
<h2><span class="icon icon-design"></span>设计特点</h2>
<p>本组件在视觉设计上注重用户体验和现代感:</p>
<div class="features-grid">
<div class="feature-card">
<h3 class="icon icon-check">激活状态反馈</h3>
<p>左侧蓝色边框 + 背景高亮 + 微妙的右移动画</p>
</div>
<div class="feature-card">
<h3 class="icon icon-check">内容过渡动画</h3>
<p>内容从右侧滑入,配合淡入效果,流畅自然</p>
</div>
<div class="feature-card">
<h3 class="icon icon-check">视觉层次</h3>
<p>卡片阴影、分隔线、图标系统构建清晰信息层级</p>
</div>
<div class="feature-card">
<h3 class="icon icon-check">响应式优化</h3>
<p>小屏设备自动切换为水平选项卡布局</p>
</div>
</div>
<p style="margin-top: 25px; background: #f0f7ff; padding: 18px; border-radius: 10px; border-left: 3px solid #4361ee;">
<strong>设计细节:</strong> 激活项使用 <code>transform-origin</code> 实现从底部生长的边框动画,配合 <code>cubic-bezier</code> 贝塞尔曲线创造更自然的运动感。
</p>
</div>
<div class="wxapp-tab-content content-4">
<h2><span class="icon icon-data"></span>使用场景</h2>
<p>此垂直选项卡组件适用于多种场景:</p>
<div class="features-grid">
<div class="feature-card">
<h3 class="icon icon-check">管理后台</h3>
<p>设置面板、用户资料编辑、系统配置等</p>
</div>
<div class="feature-card">
<h3 class="icon icon-check">产品介绍页</h3>
<p>功能特性分组展示,提升信息可读性</p>
</div>
<div class="feature-card">
<h3 class="icon icon-check">文档系统</h3>
<p>API 文档分类、帮助中心内容组织</p>
</div>
<div class="feature-card">
<h3 class="icon icon-check">数据仪表盘</h3>
<p>不同数据视图切换,保持界面整洁</p>
</div>
</div>
<p style="margin-top: 25px; font-weight: 500; color: #1e293b;">
💡 <strong>最佳实践建议:</strong>
</p>
<ul style="padding-left: 25px; margin-top: 10px;">
<li>标题文字简洁明了(建议 2-4 个汉字)</li>
<li>选项卡数量控制在 3-6 个,避免滚动</li>
<li>重要内容放在前两个选项卡(用户可见区域)</li>
<li>为 radio 按钮添加 <code>aria-label</code> 提升无障碍体验</li>
</ul>
</div>
<div class="wxapp-tab-content content-5">
<div class="container">
<div class="left">
<h2>左侧导航</h2>
<p>固定宽度 200px</p>
</div>
<div class="right">
<div class="right-header">头部区域</div>
<div class="right-content">
<h2>中间内容区域</h2>
<p>这里是主要内容区域,会自动填充剩余空间。</p>
<p>左侧区域固定宽度,右侧区域自适应填充剩余空间。</p>
<p>右侧内部采用上中下结构:</p>
<ul>
<li>头部区域固定高度 60px</li>
<li>中间内容区域自动填充剩余空间</li>
<li>底部区域固定高度 40px</li>
</ul>
</div>
<div class="right-footer">底部区域</div>
</div>
</div>
</div>
</div>
</div>
<footer>
<p>✨ 纯 CSS 垂直选项卡组件 | 无 JavaScript 依赖 | 响应式设计 | 无障碍友好</p>
<p style="margin-top: 8px; font-size: 0.9rem; color: #475569;">适用于管理后台、产品介绍、文档系统等场景 • 复制代码即可直接使用</p>
</footer>
</div>
<!-- 修复后的主容器结构 -->
<div class="chat-container" style="display: none;">
<!-- 侧边栏 -->
<div class="chat-sidebar layui-side" style="position: inherit;">
<div class="layui-side-scroll" style="width: auto;">
<div class="layui-card" style="height: inherit;">
<div class="layui-card-header">
<div class="layui-layout-admin">
<div class="layui-header">
<ul class="layui-nav layui-layout-left">
<li class="layui-nav-item">
<a href="javascript:;">
<img src="//tva1.sinaimg.cn/crop.0.0.118.118.180/5db11ff4gw1e77d3nqrv8j203b03cweg.jpg" class="layui-nav-img">
<span id="current-nickname">用户</span>
</a>
</li>
</ul>
<ul class="layui-nav layui-layout-right">
<li class="layui-nav-item">
<a href="javascript:;" id="theme-toggle" class="theme-toggle">
<i class="layui-icon layui-icon-light"></i>
</a>
</li>
</ul>
</div>
</div>
</div>
<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="layui-card" style=" height: -webkit-fill-available;">
<div class="layui-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="layui-card-body chat-messages" id="message-container">
<div class="message message-system">✨ 欢迎使用 UniChat!请选择左侧聊天对象开始对话</div>
</div>
<div class="layui-card-footer chat-input">
<textarea id="message-input" placeholder="输入消息... (Ctrl+Enter发送)" class="layui-textarea"></textarea>
<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>
<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>
<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',
// 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_uid === 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_uid: sessionId, content: '你好,这个需求什么时候能完成?', type: 'text', create_time: '2026-02-27T14:30:00' },
{ msg_id: '2', from_uid: userData.uid, content: '预计明天下午可以交付', type: 'text', create_time: '2026-02-27T14:32:00' },
{ msg_id: '3', from_uid: sessionId, content: '好的,辛苦了!', type: 'text', create_time: '2026-02-27T14:33:00' },
{ msg_id: '4', from_uid: 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:280px;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>