前言
前段时间使用MUI做了个APP,客户提出想法,能否增加一个可以聊天的功能,用来保证工单系统中所有参与角色的交流。
好的,安排!
聊天布局分析
需要实现一个可供聊天的窗口,需要优先分析其布局的构成。
参考QQ或者微信的聊天群,大致可以看出具有以下的共通点:
- 1、群聊窗口需要有信息展示窗口。
- 2、自己发送的消息显示在右边,别人发送的聊天消息显示在左边。
- 3、需要有一个触发软键盘的输入框,和一个发送一个按钮。
- 4、每个人有不同的信息,需要采取token进行判断。
除此之外,像语音聊天和动画表情等,现目前暂不做考虑,先实现简单的聊天窗口和信息展示吧。
页面布局
在MUI的界面模板中,存在一个im-chat.html
的聊天页面
布局案例,参照其功能,去掉繁琐的其他功能选项。
消息展示模板
在案例中,存在一个模板类,当消息是自己发送时,将信息展示并显示在屏幕的右方,否则就显示于屏幕左方。
<script id='msg-template' type="text/template">
<!--遍历消息数组对象-->
<% for(var i in record){ var item=record[i]; %>
<!-- 获取对象中的属性参数信息,判断当前消息的产生者,同时保存其他数据信息 -->
<div class="msg-item <%= (item.sender=='self'?' msg-item-self':'') %>" msg-type='<%=(item.type)%>' msg-content='<%=(item.content)%>'>
<!--消息时间显示-->
<% if(item.time){ %>
<div class="" style="text-align: center;">
<span style="color: #C7C7CC;font-size: 12px;" id="chatTime">
<%= item.time %>
</span>
</div>
<% } %>
<!--聊天内容头像-->
<% if(item.sender=='self' ) { %>
<i class="msg-user">
<span style="display: block;font-size: 10px;overflow:hidden;line-height: 30px;" >
<%= item.username %>
</span>
</i>
<% } else { %>
<!--<img class="msg-user-img" src="../images/logo.png" alt="" />-->
<i class="msg-user-img">
<span style="display: block;font-size: 10px;overflow:hidden;line-height: 30px;">
<%= item.username %>
</span>
</i>
<% } %>
<!--聊天内容-->
<div class="msg-content">
<div class="msg-content-inner">
<% if(item.type=='text' ) { %>
<%=( item.content|| ' ') %>
<% } %>
</div>
<!--div聊天内容边框布局-->
<div class="msg-content-arrow"></div>
</div>
<!--每条聊天消息之间空格开-->
<div class="mui-item-clear"></div>
</div>
<% } %>
</script>
消息展示和消息输入框,发送按钮布局
<!--中间内容显示-->
<div class="mui-content">
<div id='msg-list'>
</div>
</div>
<!--底部-->
<footer>
<!--图片-->
<div class="footer-left">
<!--去除照片功能,保留布局-->
</div>
<!--输入框-->
<div class="footer-center">
<textarea id='msg-text' type="text" class='input-text'></textarea>
</div>
<!--图标 点击事件是label标签-->
<label for="" class="footer-right">
<i id='msg-type' class="mui-icon mui-icon-paperplane"></i>
</label>
</footer>
完整页面布局和逻辑分析
上面截取部分ui显示布局的注意点,接下来展示完整的布局和其他js操作逻辑。
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<title></title>
<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no" />
<link href="../css/mui.min.css" rel="stylesheet" />
<style type="text/css">
html,
body {
height: 100%;
margin: 0px;
padding: 0px;
overflow: hidden;
-webkit-touch-callout: none;
-webkit-user-select: none;
}
footer {
position: fixed;
width: 100%;
height: 50px;
min-height: 50px;
border-top: solid 1px #bbb;
left: 0px;
bottom: 0px;
overflow: hidden;
padding: 0px 50px;
background-color: #fafafa;
}
.footer-left {
position: absolute;
width: 50px;
height: 50px;
left: 0px;
bottom: 0px;
text-align: center;
vertical-align: middle;
line-height: 100%;
padding: 12px 4px;
}
.footer-right {
position: absolute;
width: 50px;
height: 50px;
right: 0px;
bottom: 0px;
text-align: center;
vertical-align: middle;
line-height: 100%;
padding: 12px 5px;
display: inline-block;
}
.footer-center {
height: 100%;
padding: 5px 0px;
}
.footer-center [class*=input] {
width: 100%;
height: 100%;
border-radius: 5px;
}
.footer-center .input-text {
background: #fff;
border: solid 1px #ddd;
padding: 10px !important;
font-size: 16px !important;
line-height: 18px !important;
font-family: verdana !important;
overflow: hidden;
}
.footer-center .input-sound {
background-color: #eee;
}
.mui-content {
height: 100%;
padding: 44px 0px 50px 0px;
/*消息较多时,增加滚动条*/
overflow: auto;
background-color: #eaeaea;
}
#msg-list {
height: 100%;
overflow: auto;
-webkit-overflow-scrolling: touch;
}
.msg-item {
padding: 8px;
clear: both;
}
.msg-item .mui-item-clear {
clear: both;
}
/*.msg-item .msg-user {
width: 38px;
height: 38px;
border: solid 1px #d3d3d3;
display: inline-block;
background: #fff;
border-radius: 3px;
vertical-align: top;
text-align: center;*/
/*不是自己发送的消息 向左浮动显示*/
/*float: left;
padding: 3px;
color: #ddd;
}*/
.msg-item .msg-user {
width: 40px;
height: 40px;
border: solid 1px #d3d3d3;
display: inline-block;
background: #fff;
border-radius: 4px;
vertical-align: top;
text-align: center;
/*不是自己发送的消息 向左浮动显示*/
float: left;
padding: 3px;
background-color: #ddd;
}
.msg-item .msg-user-img {
width: 40px;
height: 40px;
border: solid 1px #d3d3d3;
display: inline-block;
border-radius: 4px;
vertical-align: top;
text-align: center;
float: left;
padding: 3px;
background-color: #ddd;
}
.msg-item .msg-content {
display: inline-block;
border-radius: 5px;
border: solid 1px #d3d3d3;
background-color: #FFFFFF;
color: #333;
padding: 8px;
vertical-align: top;
font-size: 15px;
position: relative;
margin: 0px 8px;
max-width: 75%;
min-width: 35px;
float: left;
}
.msg-item .msg-content .msg-content-inner {
overflow-x: hidden;
}
.msg-item .msg-content .msg-content-arrow {
position: absolute;
border: solid 1px #d3d3d3;
border-right: none;
border-top: none;
background-color: #FFFFFF;
width: 10px;
height: 10px;
left: -5px;
top: 12px;
-webkit-transform: rotateZ(45deg);
transform: rotateZ(45deg);
}
/*自身发送小心 最后都进行了向右浮动显示*/
.msg-item-self .msg-user,
.msg-item-self .msg-content {
float: right;
}
.msg-item-self .msg-content .msg-content-arrow {
left: auto;
right: -5px;
-webkit-transform: rotateZ(225deg);
transform: rotateZ(225deg);
}
.msg-item-self .msg-content,
.msg-item-self .msg-content .msg-content-arrow {
background-color: #4CD964;
color: #fff;
border-color: #2AC845;
}
footer .mui-icon {
color: #000;
}
footer .mui-icon:active {
color: #007AFF !important;
}
/*伪类 before 表示将content内容放在class内容之前*/
footer .mui-icon-paperplane:before {
content: "发送";
}
footer .mui-icon-paperplane {
/*-webkit-transform: rotateZ(45deg);
transform: rotateZ(45deg);*/
font-size: 16px;
word-break: keep-all;
line-height: 100%;
padding-top: 6px;
color: rgba(0, 135, 250, 1);
}
/*语音*/
#msg-sound {
-webkit-user-select: none !important;
user-select: none !important;
}
@-webkit-keyframes spin {
0% {
-webkit-transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
}
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
#h {
background: #fff;
border: solid 1px #ddd;
padding: 10px !important;
font-size: 16px !important;
font-family: verdana !important;
line-height: 18px !important;
overflow: visible;
position: absolute;
left: -1000px;
right: 0px;
word-break: break-all;
word-wrap: break-word;
}
.cancel {
background-color: darkred;
}
</style>
<header class="mui-bar mui-bar-nav">
<a class="mui-action-back mui-icon mui-icon-left-nav mui-pull-left"></a>
<!--聊天组中各个角色名称信息(长度超长使用"..."代替)-->
<h1 class="mui-title" id="headerText"></h1>
</header>
</head>
<body contextmenu="return false;">
<pre id='h'></pre>
<script id='msg-template' type="text/template">
<% for(var i in record){ var item=record[i]; %>
<div class="msg-item <%= (item.sender=='self'?' msg-item-self':'') %>" msg-type='<%=(item.type)%>' msg-content='<%=(item.content)%>'>
<!--消息时间显示-->
<% if(item.time){ %>
<div class="" style="text-align: center;">
<span style="color: #C7C7CC;font-size: 12px;" id="chatTime">
<%= item.time %>
</span>
</div>
<% } %>
<!--聊天内容头像-->
<% if(item.sender=='self' ) { %>
<i class="msg-user">
<span style="display: block;font-size: 10px;overflow:hidden;line-height: 30px;" >
<%= item.username %>
</span>
</i>
<% } else { %>
<!--<img class="msg-user-img" src="../images/logo.png" alt="" />-->
<i class="msg-user-img">
<span style="display: block;font-size: 10px;overflow:hidden;line-height: 30px;">
<%= item.username %>
</span>
</i>
<% } %>
<!--聊天内容-->
<div class="msg-content">
<div class="msg-content-inner">
<% if(item.type=='text' ) { %>
<%=( item.content|| ' ') %>
<% } %>
</div>
<!--div聊天内容边框布局-->
<div class="msg-content-arrow"></div>
</div>
<!--每条聊天消息之间空格开-->
<div class="mui-item-clear"></div>
</div>
<% } %>
</script>
<!--中间内容显示-->
<div class="mui-content">
<div id='msg-list'>
</div>
</div>
<!--底部-->
<footer>
<!--图片-->
<div class="footer-left">
<!--去除照片功能,保留布局-->
</div>
<!--输入框-->
<div class="footer-center">
<textarea id='msg-text' type="text" class='input-text'></textarea>
</div>
<!--图标 点击事件是label标签-->
<label for="" class="footer-right">
<i id='msg-type' class="mui-icon mui-icon-paperplane"></i>
</label>
</footer>
<script src="../js/apps.js" type="text/javascript" charset="utf-8"></script>
<script src="../js/moment.min.js" type="text/javascript" charset="utf-8"></script>
<script src="../js/mui.min.js"></script>
<script src="../js/arttmpl.js"></script>
<script type="text/javascript" charset="utf-8">
(function($, doc) {
$.init({
//手势事件
gestureConfig: {
tap: true, //默认为true
doubletap: true, //默认为false
longtap: true, //默认为false
swipe: true, //默认为true
drag: true, //默认为true
hold: true, //默认为false,不监听
release: true //默认为false,不监听
}
});
//js/arttmpl.js 必须引入该js文件
template.config('escape', false);
$.plusReady(function() {
var self = plus.webview.currentWebview();
var workId = self.workId;
var workName = self.workName;
//聊天框标题部分显示任务名称
if(typeof(workName) == "undefined") {
mui.alert("任务名称获取失败,请重新进入此页面", '温馨提示','确定',function(){},'div');
return;
}
if(workName.length > 6) {
workName = workName.substring(0, 5) + "...";
console.log("修改拼接后的任务名称:" + workName);
}
document.getElementById("headerText").innerText = workName;
//定义全局数组,每次消息接收或发送成功 都存储id至数组中(避免不断请求服务器获取数据信息重复显示)
var chatMsgIdArray = new Array();
// 输入框软键盘样式
plus.webview.currentWebview().setStyle({
softinputMode: "adjustResize"
});
//服务器先请求消息记录
getChatMsg(workId, "");
//定时刷新(每次传入的是当前最大对话id值)
// 这里原则上使用 websocket 技术比较好
setInterval(function() {
getChatMsg(workId, "");
}, 3500);
//显示键盘
// var showKeyboard = function() {
// var Context = plus.android.importClass("android.content.Context");
// var InputMethodManager = plus.android.importClass("android.view.inputmethod.InputMethodManager");
// var main = plus.android.runtimeMainActivity();
// var imm = main.getSystemService(Context.INPUT_METHOD_SERVICE);
// imm.toggleSoftInput(0, InputMethodManager.SHOW_FORCED);
// //var view = ((ViewGroup)main.findViewById(android.R.id.content)).getChildAt(0);
// imm.showSoftInput(main.getWindow().getDecorView(), InputMethodManager.SHOW_IMPLICIT);
// //alert("ll");
// };
//记录信息(消息对象体,包含消息发送者和发送的内容,消息样式等,目前只考虑 text)
var record = [
//显示格式
/*{
sender: 'zs',
type: 'text',
content: 'Hi,我是 MUI 小管家!'
}*/
];
// 有点类似面向对象,这里是将各个节点对象进行获取、保存、封装
var ui = {
body: doc.querySelector('body'),
footer: doc.querySelector('footer'),
footerRight: doc.querySelector('.footer-right'),
btnMsgType: doc.querySelector('#msg-type'),
boxMsgText: doc.querySelector('#msg-text'),
areaMsgList: doc.querySelector('#msg-list'),
h: doc.querySelector('#h'),
content: doc.querySelector('.mui-content')
};
// 设置每条消息的分割
ui.h.style.width = ui.boxMsgText.offsetWidth + 'px';
//alert(ui.boxMsgText.offsetWidth );--261
var footerPadding = ui.footer.offsetHeight - ui.boxMsgText.offsetHeight;
//显示消息内容函数,同时重新计算消息显示区域的 scrollTop 属性值
var bindMsgList = function() {
//先调用template处理(避免乱码) 将信息显示在areaMsgList中
//msg-template 遍历循环的script的id
ui.areaMsgList.innerHTML = template('msg-template', {
"record": record
});
//每次发送数据后 都应该显示至最新消息处
ui.areaMsgList.scrollTop = ui.areaMsgList.scrollHeight + ui.areaMsgList.offsetHeight;
};
// 调用函数,显示内容
bindMsgList();
//界面窗口改变时的监听
window.addEventListener('resize', function() {
ui.areaMsgList.scrollTop = ui.areaMsgList.scrollHeight + ui.areaMsgList.offsetHeight;
}, false);
//发送消息函数
var send = function(msg) {
// 消息对象数组保存新的消息信息
record.push(msg);
// 发送消息,同时重新计算 scrollTop 属性
bindMsgList();
//服务器通信 (多热群聊,需要将消息传递给服务器)
toRobot(msg.content);
};
//服务器请求 发送聊天信息
var toRobot = function(info) {
//发送对话
console.log("发送对话内容:" + info);
mui.ajax(config.host1 + "/work/workdialog/chat", {
data: {
workid: workId,
content: info
},
dataType: "json",
type: "post",
timeout: 10000,
headers: {
'Content-Type': 'application/json',
'authorization': localStorage.getItem('accessToken')
},
success: function(res) {
console.log("发送成功返回:" + JSON.stringify(res));
// 这里应该还需要考虑重发机制
},
error: function(xhr, type, errorThrown) {
console.log(type);
if(type == "abort") {
mui.toast("请检查您的网络!");
} else if(type == "timeout") {
mui.toast("消息发送超时!");
}
}
})
};
// 消息输入框 收到焦点
function msgTextFocus() {
ui.boxMsgText.focus();
// 延迟双收,保证输入框一定会获取到焦点
setTimeout(function() {
ui.boxMsgText.focus();
}, 100);
}
//解决长按“发送”按钮,导致键盘关闭的问题;
ui.footerRight.addEventListener('touchstart', function(event) {
if(ui.btnMsgType.classList.contains('mui-icon-paperplane')) {
msgTextFocus();
event.preventDefault();
}
});
//解决长按“发送”按钮,导致键盘关闭的问题;
ui.footerRight.addEventListener('touchmove', function(event) {
if(ui.btnMsgType.classList.contains('mui-icon-paperplane')) {
msgTextFocus();
event.preventDefault();
}
});
//发送按钮 长按后释放 手势监听事件
ui.footerRight.addEventListener('release', function(event) {
if(ui.btnMsgType.classList.contains('mui-icon-paperplane')) {
//showKeyboard();
ui.boxMsgText.focus();
setTimeout(function() {
ui.boxMsgText.focus();
}, 150);
//event.detail.gesture.preventDefault();
// 获取当前缓存中的登录账户用户名信息
var userName = localStorage.getItem('userName');
if(userName.length >= 2){
userName = userName.substring(0,2)+"...";
}
//本机输入的先显示到界面上
send({
sender: 'self',
type: 'text',
//g全局匹配 i区分大小写 m多行匹配
content: ui.boxMsgText.value.replace(new RegExp('\n', 'gm'), '<br/>'),
username: userName,
time: moment().format('HH:mm:ss')
});
//清空输入框中的数据
ui.boxMsgText.value = '';
$.trigger(ui.boxMsgText, 'input', null);
}
}, false);
//监听输入框中内容改变时的事件信息
ui.boxMsgText.addEventListener('input', function(event) {
//原代码
//ui.btnMsgType.classList[ui.boxMsgText.value == '' ? 'remove' : 'add']('mui-icon-paperplane');
//将remove更改为add 不管input标签内是否有值 都显示"伪类" ---- 即显示 "发送" 字样
ui.btnMsgType.classList['add']('mui-icon-paperplane');
ui.btnMsgType.setAttribute("for", ui.boxMsgText.value == '' ? '' : 'msg-text');
ui.h.innerText = ui.boxMsgText.value.replace(new RegExp('\n', 'gm'), '\n-') || '-';
ui.footer.style.height = (ui.h.offsetHeight + footerPadding) + 'px';
console.log("重要参数offsetHeight:" + ui.h.offsetHeight);
console.log("重要参数:" + footerPadding);
ui.content.style.paddingBottom = ui.footer.style.height;
});
var focus = false;
ui.boxMsgText.addEventListener('tap', function(event) {
ui.boxMsgText.focus();
setTimeout(function() {
ui.boxMsgText.focus();
}, 0);
focus = true;
setTimeout(function() {
focus = false;
}, 1000);
event.detail.gesture.preventDefault();
}, false);
//点击消息列表,关闭键盘
ui.areaMsgList.addEventListener('click', function(event) {
if(!focus) {
// 发送后消息输入框失去焦点
ui.boxMsgText.blur();
}
})
/**
* 请求服务器聊天消息记录
* @param {Object} taskIDs 任务id
* @param {Object} dialogIDs 上次最大对话id(每次获取都需要存本地数据库并在此读取)
*/
function getChatMsg(taskIDs, dialogIDs) {
mui.ajax(config.host1 + "/work/workdialog/chat", {
data: {
workid: taskIDs
},
dataType: "json",
type: "get",
timeout: 10000,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'authorization': localStorage.getItem('accessToken')
},
success: function(res) {
console.log(JSON.stringify(res));
if(res.code != 200) {
//发送失败(后续在做处理)
return;
}
// 解析数据信息
var datas = res.data;
// 遍历数据信息
for (var i = 0; i < datas.length; i++) {
var obj = datas[i];
// 获取消息id信息
var msgId = obj.id;
// 判断消息是否已经存在数组中
if(idWhetherInArray(msgId)){
return;
}
// 如果不存在数组中,则将id信息保存至数组
chatMsgIdArray.push(msgId);
// 获取发送者名字
var user_name = obj.user_name?obj.user_name:"暂无";
if(user_name.length >= 2){
user_name = user_name.substring(0,2)+"...";
}
var content = obj.content;
var create_time = obj.create_time;
var nowDate = moment().format('YYYY-MM-DD');
var serviceDate = moment.unix(create_time).utcOffset(8).format('YYYY-MM-DD');
//今日显示时间 他日显示具体日期和时间
var times;
if(nowDate == serviceDate) {
times = moment.unix(create_time).utcOffset(8).format('HH:mm:ss');
} else {
times = moment.unix(create_time).utcOffset(8).format('YYYY-MM-DD HH:mm:ss');
}
// 获取区别标识,判断消息是自己发送的还是别人发送的
var is_mine = obj.is_mine;
if(1 == is_mine){
// 自己发送的消息
record.push({
sender: 'self',
type: 'text',
content: content,
username: user_name,
time: times
});
}else{
// 不是自己发送的,显示在视窗左边
record.push({
sender: 'zs',
type: 'text',
content: content,
username: user_name,
time: times
});
}
//没有这个 不会显示(放在判断里面--避免每次成功回调会让布局重新计算下,导致不能向上看历史消息)
bindMsgList();
}
},
error: function(xhr, type, errorThrown) {
}
})
}
//判断ajax回调中的id是否在数组中有同样的值
function idWhetherInArray(id) {
var isExist = false;
for(var i = 0; i < chatMsgIdArray.length; i++) {
if(chatMsgIdArray[i] == id) {
isExist = true;
break;
}
}
return isExist;
}
});
}(mui, document));
</script>
</body>
</html>
注意事项
必须保证导入arttmpl.js
文件。
获取详细库
Hbuiler
编辑器右键创建APP
应用,选择MUI 样式模板
即可。