自己动手实现聊天APP
成果
开始
时隔两年多,想再次看看 app 的开发。还记得两年前辛苦使用 andro studio 写 xml 的日子,五味杂陈。网上走了一圈,发现 dcloud 公司推出了 uni-app 和 5 + app 的方式开发 app , 为了知道这些方式和 andro studio 开发 app 的区别。我开始了探寻。
uni-app 和 5 + app
uni-app 最大的特点便是 编写一套代码,可发布到iOS、Android、H5、以及各种小程序(微信/支付宝/百度/头条/QQ/钉钉/淘宝)、快应用 ,并且 基于vue 。听上去方便不少哦。
5 + app 是 HTML5 Plus移动App的意思。HTML5 plus(即HTML5 +)是W3C提供一套规范,属于工信部。扩展了 JavaScript 对象 plus,使得js可以调用摄像头、陀螺仪、文件系统等。HBuilder内置HTML5 + APP开发环境,可以在云端将代码打包为 apk 等文件。
经过测试发现 uni-app 编译过程稍微有点漫长,对于开发来说测试有点费时间。最终我选择 5 + app 的方式开发APP。
mui
打开 hbuilder ,新建项目可以发现有 mui 项目模板。
mui 是 dcloud 公司推出的 5 + app 开发的一款 ui 框架 ,听说性能最接近原生APP。 官网是 【mui】,官网提供了大量示例,可以参考(也可以粘贴复制哦)。但总的来说文档不是很丰富吧,有些功能我还是在百度解决。
尬聊APP
来源
在学习了两天的 mui api后我想尝试写个 app 来锻炼下自己,也为了对比 5 + app 和 android studio 开发上的深层区别和感受,再加上之前对于 即时聊天 app 的 实现感兴趣,想摸索一番,于是取了这个名字便开始了开发。
技术选型
后端大致选择 springboot + netty + mybatisplus
前端大致选择 mui + vue
数据库依旧选择 mysql(当然后边可以换其他数据库)
项目注意点
前端代码需要注意几个问题 :
1 mui 配合 vue 一起玩的时候 @click 会不起作用,只能用 mui 去绑定 tap 事件后调用 this.click() 方法,因为 mui 禁用了 click 事件 ,点击会触发 tap 事件。当然本页面暂未使用到 vue , 其他页面注意。解决代码如下:mui(document.body).on('tap', '#btn_clcik', function(e) { this.click() })
2 mui 配合 vue 一起玩的时候 v-model 双向绑定不起作用。暂时我使用 document 去获取值。比如获取输入框内容时:
document.getElementById('galiao_userId').value
3 如果自定义的 js 文件用到 mui 、vue、axios 中的对象或方法时,尽量后引入。比如:
<script src="https://cdn.bootcss.com/vue/2.6.11/vue.min.js"></script> <script src="https://cdn.bootcss.com/axios/0.19.2/axios.min.js"></script> <script src="js/mui.min.js"></script> <script src="js/app.js"></script>
4 尽量使用 v-text 代替 {{ }} 避免插值闪烁。
后端项目
构建
新建maven工程,pom.xml:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.6.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.junan</groupId>
<artifactId>galiao</artifactId>
<version>1.0.0-SNAPSHOT</version>
<name>galiao</name>
<description>尬聊 v1 服务端</description>
<properties>
<java.version>1.8</java.version>
<fastjson.version>1.2.58</fastjson.version>
<mybatis-plus.version>3.2.0</mybatis-plus.version>
<mybatisplus-generation.version>3.2.0</mybatisplus-generation.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.2</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.1.0</version>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>${fastjson.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>${mybatisplus-generation.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
项目结构
核心
核心功能便是 netty 提供的服务了,在 netty 包下三个类:
GaLiaoServer 表示 Netty 服务的启动类。
GaLiaoServerInitializer 表示 Netty 服务的初始化类。
GaLiaoHandler 表示Netty 服务的处理器类。(这里面包含消息的接受和发送,它就是核心)
至于代码方面大家感兴趣可以去【尬聊开源后端项目】
项目启动页(index.html)
前端代码:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no" />
<title>首页</title>
<link href="css/mui.min.css" rel="stylesheet" />
</head>
<body>
</body>
<script src="js/mui.min.js"></script>
<script type="text/javascript" charset="utf-8">
mui.init();
mui.plusReady(function() {
// 弹到登录页面
mui.openWindow({
url: 'login.html',
id: 'login'
});
})
</script>
</html>
这个页面暂时只是负责打开了登录页面,后期可以做成判断是否登录或自动登录,根据情况跳转到不同页面。
登录(login.html)
架构图
主要还是使用 mvc ,这里暂时不用 netty 。登录成功返回 token,客户端保存 token ,携带 token 访问服务端资源,包含后期使用 netty 发送消息也需要携带 token 。
前端登录页面代码:
<!doctype html>
<html lang="en">
<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 rel="stylesheet" type="text/css" href="css/mui.min.css" />
<link rel="stylesheet" type="text/css" href="css/app.css" />
<style>
body {
background-color: #FFFFFF;
}
.title {
margin: 0;
background-color: #FFFFFF;
}
.logo {
width: 100px;
height: 100px;
border-radius: 10px;
}
.form {
margin-left: 10%;
margin-right: 10%;
background-color: #FFFFFF;
}
.form .galiao-user-id, .form .galiao-user-password {
margin-bottom: 30px;
border: none;
border-radius: 50px;
background-color: #F3F3F3;
}
.btn-ok {
width: 70px;
height: 70px;
border-radius: 35px;
}
.btn-img {
width: 40px;
height: 40px;
padding-top: 5px;
}
</style>
</head>
<body>
<div class="mui-content">
<div style="margin-top: 40%; text-align: center; background-color: #FFFFFF;">
<h2 class="title">尬聊 v1.0</h2>
<div style="background-color: #FFFFFF;margin: 30px 0;">
<img class="logo" src="img/logo.png" />
</div>
<div class="form">
<div>
<input type="text" class="mui-input-clear galiao-user-id" value="1000" id="galiao_userId" placeholder="尬聊号">
</div>
<div class="mui-input-row">
<input type="password" class="mui-input-password galiao-user-password" value="123456" id="galiao_password"
placeholder="密码">
</div>
<div>
<button id="btn_clcik" type="button" class="mui-btn mui-btn-success btn-ok">
<img class="btn-img" src="img/ok.png" />
</button>
</div>
</div>
</div>
</div>
<script type="text/javascript" src="js/config.js"></script>
<script src="https://cdn.bootcss.com/vue/2.6.11/vue.min.js"></script>
<script src="https://cdn.bootcss.com/axios/0.19.2/axios.min.js"></script>
<script src="js/mui.min.js" type="text/javascript" charset="utf-8"></script>
<script type="text/javascript" src="js/app.js"></script>
<script type="text/javascript">
mui.init()
mui.plusReady(function() {
// 关闭 index 页面
plus.webview.currentWebview().opener().close()
})
// 登录操作
mui(document.body).on('tap', '#btn_clcik', function(e) {
// 关闭软键盘(否则老弹出来)
document.activeElement.blur();
let userId = document.getElementById('galiao_userId').value
let userPwd = document.getElementById('galiao_password').value
// 验证用户名密码是否输入
if (userId === '') {
mui.alert("请输入尬聊号!")
}
if (userId === '') return // 直接在上面if里面return alert就会没反应,一切为了用户体验
if (userPwd === '') {
mui.alert("请输入密码!")
}
if (userPwd === '') return // 直接在上面if里面return alert就会没反应 一切为了用户体验
// 显示加载中动画
mui.showLoading("正在登录....","div")
// 发起登陆请求
axios({
url: config.app.baseUrl + '/user/login',
method: 'post',
headers: {
'Content-Type': 'application/json'
},
data: {
userId: userId,
userPwd: userPwd
}
})
.then(function(res) {
var data = res.data
if (data.code != 1) {
mui.alert(data.data)
return
}
// 登陆成功
// 1 记录 登录信息 (user里面包含token)
data.data.token = data.token
let user = JSON.stringify(data.data)
plus.storage.setItem("user", user)
// 2 跳转页面
mui.openWindow({
url: 'main.html',
id: 'main'
});
// 关闭加载
mui.hideLoading()
})
.catch(function(error) {
// 关闭加载
mui.hideLoading()
mui.alert("网络故障!")
});
})
// 关闭其他页面
window.addEventListener('clearOtherHtml', function(event) {
// 获取当前webview窗口对象
var curr = plus.webview.currentWebview()
//获取所有已经打开的webview窗口
var wvs = plus.webview.all()
for (var i = 0, len = wvs.length; i < len; i++) {
//关闭除当前页面外的其他页面
if (wvs[i].id == curr.id)
//遇到当前页跳过
continue
// 先hide,避免闪一下
wvs[i].hide()
//非当前页执行关闭
wvs[i].close()
}
});
</script>
</body>
</html>
登录页面效果:
后端主要代码:
@Override
public Message login(User user) {
User byId = getById(user.getUserId());
if (byId == null) {
return new Message()
.code(MessageCode.FAIL.value())
.data("用户不存在!");
}
if (!encoder.matches(user.getUserPwd(), byId.getUserPwd())) {
return new Message()
.code(MessageCode.FAIL.value())
.data("用户名或密码错误!");
}
// 密码正确,产生token并返回
user.setUserPwd(null); // 清理掉密码
byId.setUserPwd(null); // 清理掉密码
String token = JwtTokenUtil.createToken(user);
// redis 存储登录的用户的信息 (存储一天)
redisUtil.set(token, FastJsonUtil.toJSONString(byId), 24 * 60 * 60);
// 获取好友
String[] friendIds = byId.getUserFriends().split(",");
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.in("user_id", friendIds);
wrapper.select("user_id", "user_avatar", "user_nickname");
List<User> users = userMapper.selectList(wrapper);
byId.setFriends(users);
return new Message().code(MessageCode.OK.value()).data(byId).token(token);
}
后端主要用到 spring security 的 BCryptPasswordEncoder 对密码加密和解密,用的 sha256 算法,因此不可逆的,保证密码的安全性。登录成功后会将用户的基本信息(除了密码)放入redis ,key 为用户的token, 暂时默认存一天也是 token 的过期时间。然后把用户信息返回客户端,客户端保存在 localstorage 供页面之间共享。
主页面(main.html)
主页面主要是 main.html 页面,这个页面有一个底部导航栏来控制显示其他几个页面。
前端代码如下:
<!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.css" rel="stylesheet" />
<style>
.mui-bar-tab .mui-tab-item.mui-active {
color: #4DB361; /* 这里放你底部导航栏颜色 */
}
</style>
</head>
<body>
<!-- 底部导航栏 -->
<nav class="mui-bar mui-bar-tab">
<a class="mui-tab-item mui-active" tabindex="0">
<span class="mui-icon mui-icon-chat"></span>
<span class="mui-tab-label">消息</span>
</a>
<a class="mui-tab-item" tabindex="1">
<span class="mui-icon mui-icon-list"></span>
<span class="mui-tab-label">联系人</span>
</a>
<a class="mui-tab-item" tabindex="2">
<span class="mui-icon mui-icon-paperplane"></span>
<span class="mui-tab-label">发现</span>
</a>
<a class="mui-tab-item" tabindex="3">
<span class="mui-icon mui-icon-home"></span>
<span class="mui-tab-label">我的</span>
</a>
</nav>
<div id="app">
</div>
<script src="js/mui.js"></script>
<script src="https://cdn.bootcss.com/vue/2.6.11/vue.min.js"></script>
<script type="text/javascript">
mui.init()
var vue = new Vue({
el: '#app',
created() {
mui.plusReady(function() {
if(plus.webview.currentWebview().opener()) {
var curr = plus.webview.currentWebview()
var wvs = plus.webview.all()
for (var i = 0, len = wvs.length; i < len; i++) {
if (wvs[i].id == 'login' || wvs[i].id == 'HBuilder') {
// 先hide再关闭 ,避免闪一下
wvs[i].hide()
wvs[i].close()
}
}
}
init();
});
}
})
/**
* main页面初始化
*/
function init() {
var sixinArray = [{
pageUrl: 'message.html',
pageId: 'message'
},
{
pageUrl: 'friends.html',
pageId: 'friends'
},
{
pageUrl: 'found.html',
pageId: 'found'
},
{
pageUrl: 'mine.html',
pageId: 'mine'
}
]
var sixinStyle = {
top: '0px',
bottom: '51px'
}
//获取当前的webview对象
var indexWebview = plus.webview.currentWebview();
//向当前的主页webview追加子页的4张webview对象
for (var i = 0; i < sixinArray.length; i++) {
var sixinPage = plus.webview.create(sixinArray[i].pageUrl,
sixinArray[i].pageId, sixinStyle);
//创建完后不需要马上显示,只有点击的时候才显示,所以隐藏webview窗口
sixinPage.hide();
//追加每一个子页面到当前主页面
indexWebview.append(sixinPage);
}
//设置默认显示页面
plus.webview.show(sixinArray[0].pageId);
//批量绑定tap(点击)事件,展示不同的页面
//通过mui选择器选择唯一的class ,on是表示触发的事件,tap表示手指触摸点击事件类型
//第二个参数表示link的对象,也可以用共同的a标签代替
mui(".mui-bar-tab").on("tap", ".mui-tab-item", function() {
//或者使用a标签选择器 mui(".mui-bar-tab").on("tap", "a", function() {
//在要点击的标签处添加事件名 并且获取到相应的对象
var tabindex = this.getAttribute("tabindex");
//显示tap点击的页面,第一个id,第二个动画效果,第三个延迟时间
plus.webview.show(sixinArray[tabindex].pageId, "none", 400);
//隐藏不需要的页面
for (var i = 0; i < sixinArray.length; i++) {
if (i != tabindex) {
plus.webview.hide(sixinArray[i].pageId, "none", 400);
}
}
});
}
</script>
</body>
</html>
该页面效果如下:
该页面难点在于底部导航栏每个 a 标签控制每个页面显示与隐藏,而官网只提供了基于a 标签 href 的代码块的显示与隐藏,这种方式需要将其他页面以 div 的方式写在 main 页面,会导致 main 页面代码过于冗杂。本次使用的切换 html 的方式核心代码在于 init 方法,通过 a 标签上的 tabindex 的值来显示不同页面。
消息页面(message.html)
前端代码:
<!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>
.mui-bar-nav {
background-color: #4DB361;
}
.galiao-title {
color: #FFFFFF;
}
.galiao-message img {
border-radius: 5px;
}
</style>
</head>
<body>
<!-- 头部 -->
<header class="mui-bar mui-bar-nav">
<h1 id="title" class="mui-title galiao-title">消息</h1>
</header>
<div class="mui-content" id="app">
<div>
<ul class="mui-table-view">
<li v-for="receiver in receivers" :key="receiver.userId" class="mui-table-view-cell mui-media" id="openChat" @click="openChat(receiver)">
<div class="galiao-message">
<img class="mui-media-object mui-pull-left" :src="receiver.userAvatar">
<div class="mui-media-body">
<span v-text="receiver.userNickname"></span>
<p class="mui-ellipsis" v-text="receiver.messages[receiver.messages.length - 1].data"></p>
</div>
</div>
</li>
</ul>
</div>
</div>
<script src="https://cdn.bootcss.com/vue/2.6.11/vue.min.js"></script>
<script src="js/config.js"></script>
<script src="js/mui.min.js"></script>
<script type="text/javascript">
mui.init()
document.galiao = {
// 后端接口
url: config.app.sockUrl
}
/**
* 尬聊app初始化
*/
function galiao_init() {
// 创建 socket
if (window.WebSocket) {
// 构建socket
window.sock = new WebSocket(document.galiao.url)
// 刚建立连接时需要mark一下(让服务端把你的channel和尬聊号建立联系)
window.sock.onopen = function(event) {
window.sock.send(JSON.stringify({
action: 'mark',
token: vue.$data.user.token
}))
}
// 服务端有消息推送
window.sock.onmessage = function(event) {
var chat = plus.webview.getWebviewById('chat');
if (chat) {
mui.fire(chat, 'receiveMessage', {
message: event.data
})
// 添加到消息
vue.receiveNewMessage(event.data)
}
}
}
}
/**
* 调用本页面的 socket 发送消息
* @param {Object} message
*/
function sendMessage(message) {
window.sock.send(message)
addNewMessage(JSON.parse(message))
}
/**
* 新增消息栏位
*/
function addReceiver(receiver) {
vue.addReceiver(JSON.parse(receiver))
}
// 打开聊天窗口
mui(document.body).on('tap', '#openChat', function(e) {
this.click()
})
// 添加最新的消息
function addNewMessage(message) {
vue.addNewMessage(message)
}
/**
* vue实例
*/
var vue = new Vue({
el: '#app',
data: {
user: {},
receivers: []
},
methods: {
addReceiver(receiver) {
// 先判断是否已经存在
for(receiver1 of this.receivers) {
if(receiver1.userId === receiver.userId)
this.openChat(receiver1)
return
}
// 添加到消息列表
receiver.messages = []
this.receivers.push(receiver)
this.openChat(receiver)
},
openChat(receiver) {
mui.openWindow({
url: 'chat.html',
id: 'chat',
extras: {
receiver: receiver
}
});
},
// 添加最新消息
addNewMessage(message) {
for(receiver of this.receivers) {
if(receiver.userId === message.receiver) {
receiver.messages.push(message)
}
}
},
// 收到新消息
receiveNewMessage(message) {
let inMessage = JSON.parse(message)
for(receiver of this.receivers) {
if(receiver.userId === inMessage.sender) {
receiver.messages.push(inMessage)
}
}
}
},
created() {
let that = this
mui.plusReady(function() {
// 初始化
galiao_init();
that.user = JSON.parse(plus.storage.getItem('user'))
})
}
})
</script>
</body>
</html>
消息页面效果:
该页面构建了一个websocket对象供聊天信息的发送和接受,但该对象只能在本页面使用,其他页面(chat.html)需要调用本页面方法来发送消息。该页面收到消息直接传给chat页面,chat页面进行判断后选择显示或忽略。该页面涉及到消息数据的保存功能请参见代码。
联系人页面(friends.html)
前端代码:
<!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">
.mui-bar-nav {
background-color: #4DB361;
}
.galiao-title {
color: #FFFFFF;
}
.galiao-friend img {
border-radius: 5px;
}
#topPopover {
position: fixed;
top: 16px;
right: 6px;
}
#topPopover .mui-popover-arrow {
left: auto;
right: 6px;
}
.mui-popover {
width: 140px;
height: 100px;
}
</style>
</head>
<body>
<!-- 头部 -->
<header class="mui-bar mui-bar-nav">
<h1 id="title" class="mui-title galiao-title">联系人</h1>
<a href="#topPopover" class="mui-icon mui-icon-plus mui-pull-right galiao-title"></a>
</header>
<div class="mui-content" id="app">
<div>
<ul class="mui-table-view">
<li v-for="friend in user.friends" v-if="friend.userId != user.userId" @click="openFriendInfo(friend)" :key="friend.userId" class="mui-table-view-cell mui-media"
id="open_friend_info">
<div class="galiao-friend">
<img class="mui-media-object mui-pull-left" :src="friend.userAvatar">
<div style="padding-top: 10px;" v-text="friend.userNickname">
</div>
</div>
</li>
</ul>
</div>
<!--右上角弹出菜单-->
<div id="topPopover" class="mui-popover">
<div class="mui-popover-arrow"></div>
<div class="mui-scroll-wrapper">
<div class="mui-scroll">
<ul class="mui-table-view">
<li class="mui-table-view-cell" id="btn_sao" @click="saoClick">
<a href="javascript:;">
<img class="mui-media-object mui-pull-left" style="width: 18px; height: 18px;" src="img/sao.png" />
扫一扫
</a>
</li>
<li class="mui-table-view-cell" id="btn_ssou" @click="souClick">
<a href="javascript:;">
<img class="mui-media-object mui-pull-left" style="width: 18px; height: 18px;" src="img/sou.png" />
搜索好友
</a>
</li>
</ul>
</div>
</div>
</div>
</div>
<script src="https://cdn.bootcss.com/vue/2.6.11/vue.min.js"></script>
<script src="js/mui.min.js"></script>
<script type="text/javascript">
mui.init()
// 点击事件
mui(document.body).on('tap', '#open_friend_info', function(e) {
this.click()
})
// 点击扫一扫
mui(document.body).on('tap', '#btn_sao', function(e) {
this.click()
})
// 点击搜索
mui(document.body).on('tap', '#btn_sou', function(e) {
this.click()
})
/**
* vue实例
*/
var vue = new Vue({
el: '#app',
data: {
user: {}
},
methods: {
init() {
this.user = JSON.parse(plus.storage.getItem('user'))
},
openFriendInfo(friend) {
mui.openWindow({
url: 'friend_info.html',
id: 'friend_info',
extras: {
userId: friend.userId
}
});
},
saoClick() {
mui.openWindow({
url: 'sao.html',
id: 'sao'
});
mui('#topPopover').popover('toggle'); // 关闭弹出菜单
},
souClick() {
mui.openWindow({
url: 'sou.html',
id: 'sou'
});
mui('#topPopover').popover('toggle');
}
},
created() {
let that = this
mui.plusReady(function() {
that.init()
})
}
})
</script>
</body>
</html>
页面效果:
联系人页面和消息页面比较相似,可以 copy 一部分。
发现页面 (found.html)
前端代码:
<!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">
.mui-bar-nav {
background-color: #4DB361 ;
}
.galiao-title {
color: #FFFFFF;
}
.galiao-li {
height: 45px;
}
.galiao-li img {
width: 25px;
padding-bottom: 15px;
}
</style>
</head>
<body>
<!-- 头部 -->
<header class="mui-bar mui-bar-nav">
<h1 id="title" class="mui-title galiao-title">发现</h1>
</header>
<div class="mui-content">
<div>
<ul class="mui-table-view">
<!-- 毒鸡汤栏目 -->
<li id="dujitang" class="mui-table-view-cell mui-media galiao-li">
<div class="mui-navigate-right">
<img class="mui-media-object mui-pull-left" src="img/du.png">
<div style="padding-top: 3px;">
毒鸡汤(每日一毒)
</div>
</div>
</li>
<!-- 代做决定栏目 -->
<li id="decision" class="mui-table-view-cell mui-media galiao-li">
<div class="mui-navigate-right">
<img class="mui-media-object mui-pull-left" src="img/decision.png">
<div style="padding-top: 3px;">
代做决定
</div>
</div>
</li>
</ul>
</div>
</div>
<script src="js/mui.min.js"></script>
<script type="text/javascript">
mui.init()
// 打开毒鸡汤页面
mui(document.body).on('tap', '#dujitang', function(e) {
mui.openWindow({
url: 'du.html',
id:'du'
});
})
// 打开代做决定页面
mui(document.body).on('tap', '#decision', function(e) {
mui.openWindow({
url: 'decision.html',
id:'decision'
});
})
</script>
</body>
</html>
这个页面主要负责打开其他页面,目前主要打开 du.html 和 decision.html,这两个页面目前主要还是 js 在玩,没有 ajax 操作。可在【尬聊开源地址】查看。
我的页面(mine.html)
前端代码:
<!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">
.mui-bar-nav {
background-color: #4DB361;
}
.galiao-li {
height: 45px;
}
.galiao-title {
color: #FFFFFF;
}
.galiao-mine-header {
width: 100%;
height: 150px;
background-color: #FFFFFF;
padding-top: 16%;
padding-left: 10%;
}
.galiao-mine-header img {
width: 60px;
height: 60px;
border-radius: 6px;
margin-right: 22px;
}
.galiao-mine-header div {
padding-top: 10px;
}
.galiao-mine-header-title {
color: #000000;
font-size: 24px;
}
.galiao-mine-header-desc {
font-size: 14px;
color: #8F8F94;
}
.galiao-mine ul,
.galiao-mine div {
margin-bottom: 20px;
}
.galiao-quit-login {
width: 100%;
height: 40px;
}
</style>
</head>
<body>
<!-- 头部 -->
<header class="mui-bar mui-bar-nav">
<h1 id="title" class="mui-title galiao-title">我的</h1>
</header>
<div class="mui-content">
<div class="galiao-mine" id="galiao_mine">
<div class="galiao-mine-header">
<img style="float: left;" :src="user.userAvatar" />
<div>
<p class="galiao-mine-header-title" v-text="user.userNickname"></p>
<span class="galiao-mine-header-desc">尬聊号:<span v-text="user.userId">></span></span>
</div>
</div>
<ul class="mui-table-view">
<li class="mui-table-view-cell" id="btn_replace_avatar">
<a class="mui-navigate-right">头像
<img class="mui-pull-right" style="width: 35px; height: 35px; margin-right: 20px;" :src="user.userAvatar" />
</a>
</li>
<li class="mui-table-view-cell" id="btn_replace_nickname">
<a class="mui-navigate-right">昵称
<p class="mui-pull-right" style=" margin-right: 20px;" v-text="user.userNickname"></p>
</a>
</li>
<li class="mui-table-view-cell">
<a class="mui-navigate-right">尬聊号
<p class="mui-pull-right" style=" margin-right: 20px;" v-text="user.userId"></p>
</a>
</li>
<li class="mui-table-view-cell" id="btn_show_qrcode" @click="showQrcode">
<a class="mui-navigate-right">我的二维码</a>
</li>
</ul>
<ul class="mui-table-view">
<li class="mui-table-view-cell" id="btn_show_admire">
<a class="mui-navigate-right">赞赏</a>
</li>
<li class="mui-table-view-cell" id="btn_show_about">
<a class="mui-navigate-right">关于</a>
</li>
</ul>
<button type="button" id="btn_quit_login" class="mui-btn mui-btn-danger galiao-quit-login">退出登录</button>
</div>
</div>
<script src="https://cdn.bootcss.com/vue/2.6.11/vue.min.js"></script>
<script src="https://cdn.bootcss.com/axios/0.19.2/axios.min.js"></script>
<script src="js/mui.min.js"></script>
<script src="js/app.js"></script>
<script type="text/javascript">
mui.init()
// 更换头像
mui(document.body).on('tap', '#btn_replace_avatar', function(e) {
mui.alert("该功能暂无法使用哦!")
})
// 更换昵称
mui(document.body).on('tap', '#btn_replace_nickname', function(e) {
mui.alert("该功能暂无法使用哦!")
})
// 展示二维码
mui(document.body).on('tap', '#btn_show_qrcode', function(e) {
this.click()
})
// 赞赏
mui(document.body).on('tap', '#btn_show_admire', function(e) {
mui.alert("小朋友,省点钱买辣条吃!")
})
// 关于
mui(document.body).on('tap', '#btn_show_about', function(e) {
//打开about页面
mui.openWindow({
url: 'about.html',
id: 'about'
});
})
// 退出登录
mui(document.body).on('tap', '#btn_quit_login', function(e) {
// 清理缓存
clearCache()
//打开login页面
mui.openWindow({
url: 'login.html',
id: 'login'
});
// 调用login的方法清除其他页面
var login = plus.webview.getWebviewById('login');
mui.fire(login, 'clearOtherHtml')
})
var vue = new Vue({
el: '#galiao_mine',
data: {
user: {
userId: '',
userNickname: '',
userAvatar: ''
}
},
methods: {
showQrcode() {
let that = this
mui.openWindow({
url: 'mine_qrcode.html',
id: 'mine_qrcode',
extras: {
userId: that.user.userId
}
});
}
},
created() {
let that = this
mui.plusReady(function() {
that.user = JSON.parse(plus.storage.getItem('user'))
})
}
})
</script>
</body>
</html>
页面效果:
这个页面目前主要实现了我的二维码展示、关于、退出三个功能,后续可以做其他功能。
好友信息页面(friend_info.html)
<!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">
.mui-bar-nav {
background-color: #4DB361;
}
.galiao-btn-return {
color: #FFFFFF;
}
.galiao-li {
height: 45px;
}
.galiao-title {
color: #FFFFFF;
}
.galiao-mine-header {
width: 100%;
height: 150px;
background-color: #FFFFFF;
padding-top: 16%;
padding-left: 10%;
}
.galiao-mine-header img {
width: 60px;
height: 60px;
border-radius: 6px;
margin-right: 22px;
}
.galiao-mine-header div {
padding-top: 10px;
}
.galiao-mine-header-title {
color: #000000;
font-size: 24px;
}
.galiao-mine ul {
margin-bottom: 20px;
}
.galiao-mine-header-desc {
font-size: 14px;
color: #8F8F94;
}
.galiao-quit-login {
width: 100%;
height: 40px;
}
</style>
</head>
<body>
<header class="mui-bar mui-bar-nav">
<a class="mui-action-back mui-icon mui-icon-left-nav mui-pull-left galiao-btn-return"></a>
</header>
<div class="mui-content" id="app">
<div class="galiao-mine" id="galiao_mine">
<div class="galiao-mine-header">
<img style="float: left;" :src="user.userAvatar" />
<div>
<p class="galiao-mine-header-title" v-text="user.userNickname"></p>
<span class="galiao-mine-header-desc">尬聊号:<span v-text="user.userId"></span> </span>
</div>
</div>
<!-- 已经是好友显示 -->
<ul class="mui-table-view" v-if="user.isFriend">
<li class="mui-table-view-cell" id="btn_set_remark">
<a class="mui-navigate-right">
设置备注和标签
</a>
</li>
</ul>
<ul class="mui-table-view" v-if="user.isFriend">
<li class="mui-table-view-cell" id="btn_send_message" @click="openChat">
<a style="text-align: center; color: dimgray;">
<span class="mui-icon mui-icon-chatbubble" style="width: 25px; height: 25px;"></span>
发消息
</a>
</li>
</ul>
<!-- 不是好友显示 -->
<ul class="mui-table-view" v-if="user.isFriend == false" style="margin-top: 20px;">
<li class="mui-table-view-cell" id="btn_add_friend" @click="addFriend">
<a style="text-align: center; color: dimgray;">
<img src="img/add_friend.png" class="mui-icon mui-icon-chatbubble" style="width: 20px; height: 20px; margin-bottom: -3px;"></img>
添加好友
</a>
</li>
</ul>
</div>
</div>
<script type="text/javascript" src="js/config.js"></script>
<script src="https://cdn.bootcss.com/vue/2.6.11/vue.min.js"></script>
<script src="https://cdn.bootcss.com/axios/0.19.2/axios.min.js"></script>
<script src="js/mui.min.js"></script>
<script type="text/javascript">
mui.init()
// 点击发消息按钮
mui(document.body).on('tap', '#btn_send_message', function(e) {
this.click()
})
// 点击设置备注按钮
mui(document.body).on('tap', '#btn_set_remark', function(e) {
mui.alert("该功能暂无法使用哦!")
})
// 点击添加好友
mui(document.body).on('tap', '#btn_add_friend', function(e) {
// this.click()
mui.alert("该功能暂无法使用哦!")
})
/**
* vue实例
*/
var vue = new Vue({
el: '#app',
data: {
user: {
userId: '',
userNickname: '',
userAvatar: '',
isFriend: false
}
},
methods: {
// 打开聊天界面
openChat() {
let that = this
// 不能和自己聊天
if (that.user.userId == JSON.parse(plus.storage.getItem('user')).userId) {
mui.alert("无法和自己聊天!")
return
}
// message 页面新增消息栏
this.messageView.evalJS("addReceiver('" + JSON.stringify(that.user) + "')")
},
// 添加好友
addFriend() {
let that = this
axios.post(config.app.baseUrl + '/user/friend/' + that.user.userId, null, {
params: {
token: JSON.parse(plus.storage.getItem('user')).token
}
})
.then(function(res) {
var data = res.data
if (data.code != 1) {
mui.alert(data.data)
return
}
mui.alert("添加成功!")
that.openChat()
// 更新本地数据
})
.catch(function(error) {
mui.alert("网络异常!")
});
}
},
created() {
let that = this
mui.plusReady(function() {
// 获取上个页面传来的参数
that.user.userId = plus.webview.currentWebview().userId
//获取 message 页面
var wvs = plus.webview.all()
for (var i = 0, len = wvs.length; i < len; i++) {
// 获取建立socket连接的 message 页面
if (wvs[i].id == 'message') {
vue.messageView = wvs[i]
break
}
}
// 获取好友信息
if (that.user.userId) {
axios.get(config.app.baseUrl + '/user/' + that.user.userId, {
params: {
token: JSON.parse(plus.storage.getItem('user')).token
}
})
.then(function(res) {
var data = res.data
if (data.code != 1) {
mui.alert(data.data)
return
}
that.user = data.data
})
.catch(function(error) {
console.log(error)
});
}
})
}
})
</script>
</body>
</html>
页面效果:
这个页面会根据情况判断,如果不是自己的好友,会显示 添加好友 按钮,如果是自己的好友就显示 发消息 按钮。这一块的判断在服务端,主要逻辑如下:
@Override
public Message userById(Long userId, String token) {
// 验证token
if(checkToken(token)) {
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.eq("user_id", userId);
wrapper.select("user_id", "user_avatar", "user_nickname");
User user = userMapper.selectOne(wrapper);
if(user == null)
return new Message().code(MessageCode.USER_NOT_FOUND.value()).data("该用户不存在!");
// 用户存在,检查是否是自己的好友
boolean isFriend = false;
// 获取自己的好友列表
String[] split = FastJsonUtil.parseObject(
(String) redisUtil.get(token), User.class)
.getUserFriends()
.split(",");
for (String s : split) {
// 判断是否是自己好友
if(user.getUserId().equals(Long.valueOf(s))) {
isFriend = true;
break;
}
}
user.setIsFriend(isFriend);
return new Message().code(MessageCode.OK.value()).data(user);
}
// token不合法
return new Message().code(MessageCode.FAIL.value()).data("token不合法");
}
聊天页面(chat.html)
前端代码:
<!doctype html>
<html lang="en">
<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 rel="stylesheet" type="text/css" href="css/mui.min.css" />
<style>
body {
background-color: #F2F2F2;
}
.mui-bar-nav {
background-color: #4DB361 ;
}
.galiao-btn-return {
color: #FFFFFF;
}
.galiao-chat-title {
color: #FFFFFF;
}
.galiao-chat-tab {
padding: 7px 12px 3px 12px;
}
.galiao-avator {
width: 50px;
height: 50px;
}
.mui-table-view {
background-color: #F2F2F2;
}
.mui-table-view:after{ height:0}
.mui-table-view:before{ height:0}
.mui-table-view-cell:after{ height:0}
.mui-table-view-cell:before{ height:0}
.galiao-message-bubble-left {
position: relative;
display: inline-block;
padding: 10px 15px 10px 20px;
margin-top: 5px;
background-color: #FFFFFF;
font-size: 14px;
border-radius: 5px;
margin-left: 20px;
max-width: 260px;
}
.galiao-message-bubble-left:before, .galiao-message-bubble-left:after {
content: ""; /*:before和:after必带技能,重要性为满5颗星*/
display: block;
position: absolute; /*日常绝对定位*/
top: 15px;
left: -12px;
border: 6px solid transparent;
border-right-color: #FFFFFF;
width: 0px;
height: 0px;
}
.galiao-message-bubble-right {
position: relative;
display: inline-block;
padding: 10px 15px 10px 20px;
margin-top: 5px;
background-color: #9fe766;
font-size: 14px;
border-radius: 5px;
margin-right: 20px;
max-width: 260px;
}
.galiao-message-bubble-right:before, .galiao-message-bubble-right:after {
content: ""; /*:before和:after必带技能,重要性为满5颗星*/
display: block;
position: absolute; /*日常绝对定位*/
top: 15px;
right: -10px;
border: 6px solid transparent;
border-left-color: #9fe766;
width: 0px;
height: 0px;
}
.galiao-avator{
border-radius: 5px;
}
</style>
</head>
<body>
<div id="app">
<header class="mui-bar mui-bar-nav">
<a class="mui-action-back mui-icon mui-icon-left-nav mui-pull-left galiao-btn-return"></a>
<h1 class="mui-title galiao-chat-title" v-text="receiver.userNickname"></h1>
</header>
<div class="mui-scroll-wrapper" style="margin: 50px 0 70px 0;">
<div class="mui-scroll">
<!-- 左气泡 -->
<ul class="mui-table-view" class="galiao-chat-message">
<li v-for="(message, index) in messageData" :key="index" class="mui-table-view-cell">
<img v-if="message.action != 'out'" class="galiao-avator mui-pull-left" :src="receiver.userAvatar" />
<div v-if="message.action != 'out'" class="galiao-message-bubble-left mui-pull-left" v-text="message.data"></div>
<img v-if="message.action == 'out'" class="galiao-avator mui-pull-right" :src="user.userAvatar" />
<div v-if="message.action == 'out'" class="galiao-message-bubble-right mui-pull-right" v-text="message.data"></div>
</li>
</ul>
</div>
</div>
<nav class="mui-bar mui-bar-tab galiao-chat-tab">
<div>
<span class="mui-icon mui-icon-pengyouquan" style="width: 10%; padding-right: 3px;"></span>
<input id="concurrentMessage" type="text" style="width: 60%;border: none;">
<span class="mui-icon mui-icon-star" style="width: 10%; padding-left: 7px;"></span>
<button type="button" id="btn_send_message" @click="sendMessage" class="mui-btn mui-btn-success" style="width: 14%;">发送</button>
</div>
</nav>
</div>
<script src="https://cdn.bootcss.com/vue/2.6.11/vue.min.js"></script>
<script src="js/mui.min.js" type="text/javascript" charset="utf-8"></script>
<script type="text/javascript">
mui.init()
/**
* 收到消息
* @param {Object} event
*/
window.addEventListener('receiveMessage', function(event) {
//获得事件参数
var message = event.detail.message;
vue.receiveMessage(message)
});
// 按钮点击事件
mui(document.body).on('tap', '#btn_send_message', function(e) {
this.click()
})
/**
* vue实例
*/
var vue = new Vue({
el: '#app',
data: {
messageData: [],
user: {},
receiver: {}
},
methods: {
// 发消息
sendMessage() {
let message = document.getElementById('concurrentMessage').value
if (message != '') {
// 构建传输的消息格式
let msg = {
action: 'out', // 操作类型
receiver: this.receiver.userId, // 接受者尬聊号
data: message, // 信息
token: this.user.token // token
}
// 调用父页面发送消息
this.socketView.evalJS("sendMessage('" + JSON.stringify(msg) + "')")
this.messageData.push(msg)
document.getElementById('concurrentMessage').value = ''
let that = this
setTimeout(function() {
that.movieToBottom()
}, 100)
}
},
// 接收消息
receiveMessage(message) {
let inMessage = JSON.parse(message)
if (inMessage.sender === this.receiver.userId) {
let that = this
that.messageData.push({
action: 'in',
data: inMessage.data
})
// 如果直接调用是没效果的,可能还没挂载吧,等个100ms后已经挂载完成
setTimeout(function() {
that.movieToBottom()
}, 100)
}
},
// 窗口移动到底部(每次发完消息时和初始化时)
movieToBottom() {
// 让页面一直显示最底部
var scroll = mui('.mui-scroll-wrapper').scroll();
scroll.reLayout();
//滚动到底部
scroll.scrollToBottom(100);
}
},
created() {
let that = this
mui.plusReady(function() {
// 获取登录信息
that.user = JSON.parse(plus.storage.getItem('user'))
// 获取接收人信息
that.receiver = plus.webview.currentWebview().receiver
// 渲染最新消息数据
that.messageData = that.receiver.messages != undefined ? that.receiver.messages : []
// 设置scroll
mui('.mui-scroll-wrapper').scroll({
scrollY: true, //是否竖向滚动
scrollX: false, //是否横向滚动
startX: 0, //初始化时滚动至x
startY: 0, //初始化时滚动至y
indicators: true, //是否显示滚动条
deceleration: 0.0005, //阻尼系数,系数越小滑动越灵敏
bounce: true //是否启用回弹
})
//获取 message 页面
var wvs = plus.webview.all()
for (var i = 0, len = wvs.length; i < len; i++) {
// 获取建立socket连接的 message 页面
if (wvs[i].id == 'message') {
// 有socket连接的view
vue.socketView = wvs[i]
break
}
}
})
}
})
</script>
</body>
</html>
页面效果如下:
这个页面的聊天气泡功能稍微有点复杂,可以参考项目中 chat_bubble.html 的页面的简化代码实现。另外,这个页面在发消息时需要调用 message 页面来发,因为只有 message 建立了 websocket 对象。调用其他页面的方法有两种方式,一种是 mui.fire() 方法,另一种是 evalJS() 方法。注意 evalJS() 方式只能传字符串参数,无法直接传对象,可以使用 JSON.stringify() 转为 json 串传参,被调用页面使用JSON.parse() 方法将 json 串转对象。
二维码页面(mine_qrcode.html)
前端代码:
<!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">
.mui-bar-nav {
background-color: #4DB361;
}
.galiao-qrcode-header {
color: #FFFFFF;
}
.galiao-qrcode {
margin: 30% 0 20% 0;
text-align: center;
}
.qrcode_bg {
width: 340px;
height: 340px;
background-image: url(img/qrcode_bg.png);
background-size: cover;
}
.qrcode {
padding-top: 50%;
}
</style>
</head>
<body>
<header class="mui-bar mui-bar-nav">
<a class="mui-action-back mui-icon mui-icon-left-nav mui-pull-left galiao-qrcode-header"></a>
<h1 class="mui-title galiao-qrcode-header">我的二维码</h1>
</header>
<div class="mui-content galiao-qrcode" id="app">
<div class="mui-card">
<!--页眉,放置标题-->
<div class="mui-card-header">
<img :src="user.userAvatar" /> <span v-text="user.userNickname"></span>
</div>
<div class="mui-card-content" style="padding: 10px;">
<div class="qrcode_bg" id="qrcode_bg">
<div class="qrcode" id="qrcode">
</div>
</div>
</div>
</div>
</div>
<script src="https://cdn.bootcss.com/vue/2.6.11/vue.min.js"></script>
<script src="js/qrcode.min.js"></script>
<script src="js/mui.min.js"></script>
<script type="text/javascript">
mui.init()
var vue = new Vue({
el: '#app',
data: {
user: {}
},
created() {
let that = this
mui.plusReady(function() {
that.user = JSON.parse(plus.storage.getItem('user'))
// 设置参数方式
var qrcode = new QRCode('qrcode', {
text: that.user.userId + "",
width: 130,
height: 130,
colorDark: '#000000',
colorLight: '#569362',
correctLevel: QRCode.CorrectLevel.H
});
})
}
})
</script>
</body>
</html>
页面效果:
这个页面主要使用【qrcode.js】对尬聊号生成二维码。
扫一扫页面(sao.html)
前端代码:
<!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">
.mui-bar-nav {
background-color: #4DB361;
}
.galiao-sao-header {
color: #FFFFFF;
}
.sao-qrcode {
width: 350px;
height: 350px;
margin: 25% auto;
}
</style>
</head>
<body>
<header class="mui-bar mui-bar-nav">
<a class="mui-action-back mui-icon mui-icon-left-nav mui-pull-left galiao-sao-header"></a>
<h1 class="mui-title galiao-sao-header">扫一扫识别</h1>
</header>
<div class="mui-content">
<div id="saoQrcode" class="sao-qrcode">
<!--盛放扫描控件的div-->
</div>
</div>
<script src="js/mui.min.js"></script>
<script type="text/javascript">
mui.init()
mui.plusReady(function() {
startRecognize(); //开始扫描
});
// 扫描组件
var scan;
//开启扫描方法
function startRecognize() {
try {
var filter;
//自定义的扫描控件样式
var styles = {
frameColor: "#4DB361",
scanbarColor: "#4DB361",
background: "",
width: '80%',
height: '80%'
}
//扫描控件构造
scan = new plus.barcode.Barcode('saoQrcode', filter, styles);
scan.onmarked = onmarked // 扫描成功
scan.onerror = onerror; // 扫描错误
scan.start();
} catch (e) {
onerror(e)
}
};
// 扫描成功回调 result是返回的结果
function onmarked(type, result, file) {
switch (type) {
case plus.barcode.QR:
type = 'QR';
break;
default:
type = '其它' + type;
break;
}
// 跳转页面
mui.openWindow({
url: 'friend_info.html',
id: 'friend_info',
extras: {
userId: result
}
});
setTimeout(function() {
scan.start()
}, 1000)
};
// 扫描失败回调
function onerror(error) {
mui.alert("扫描失败!")
scan.start()
};
</script>
</body>
</html>
页面效果:
该页面主要使用 plus.barcode.Barcode 开启一个扫一扫的效果,设置回调函数。扫描成功就弹到好友信息页面,这个页面会发 ajax 去查好友信息。
搜索页面(sou.html)
前端代码:
<!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">
.mui-bar-nav {
background-color: #4DB361;
}
.galiao-sou-header {
color: #FFFFFF;
}
.galiao-sou-search {
margin: 10px 10px 0 10px;
}
</style>
</head>
<body>
<header class="mui-bar mui-bar-nav">
<a class="mui-action-back mui-icon mui-icon-left-nav mui-pull-left galiao-sou-header"></a>
<h1 class="mui-title galiao-sou-header">搜索</h1>
</header>
<div class="mui-content" id="app">
<div class="mui-input-row mui-search galiao-sou-search">
<input type="search" id="searchValue" class="mui-input-clear mui-input-speech" placeholder="请输入尬聊号" @click="search">
</div>
<ul v-show="stranger.userId != null" class="mui-table-view">
<li class="mui-table-view-cell" id="btn_open_info" style="height: 60px; text-align: center;" @click="openInfo">
<img class="mui-media-object mui-pull-left" :src="stranger.userAvatar" style="border-radius: 5px;">
<div class="mui-media-body mui-pull-left" style="padding-top: 10px;" v-text="stranger.userNickname">
</div>
</li>
</ul>
<ul v-show="notFindUser" class="mui-table-view">
<li class="mui-table-view-cell" style="height: 60px; text-align: center; padding-top: 20px;">
用户不存在
</li>
</ul>
<p class="galiao-sou-search">注:输入好友的尬聊号搜索添加好友。尬聊号可以在 ‘我的’ -> ‘尬聊号’ 处查看。</p>
</div>
<script type="text/javascript" src="js/config.js"></script>
<script src="https://cdn.bootcss.com/vue/2.6.11/vue.min.js"></script>
<script src="https://cdn.bootcss.com/axios/0.19.2/axios.min.js"></script>
<script src="js/mui.min.js"></script>
<script type="text/javascript">
mui.init()
// 点击开始搜索
mui(document.body).on('keypress', '#searchValue', function(e) {
this.click();
})
// 点击打开详情
mui(document.body).on('tap', '#btn_open_info', function(e) {
this.click();
})
// vue实例
var vue = new Vue({
el: '#app',
data: {
notFindUser: false,
stranger: {
userId: null,
userAvatar: '',
userNickname: ''
}
},
methods: {
// 搜索
search() {
let that = this
that.notFindUser = false
let searchValue = document.getElementById('searchValue').value
if(searchValue === '') return
axios.get(config.app.baseUrl + '/user/search/' + searchValue, {
params: {
token: JSON.parse(plus.storage.getItem('user')).token
}
})
.then(function(res) {
var data = res.data
if (data.code == 3) {
that.stranger = {
userId: null,
userAvatar: '',
userNickname: ''
}
that.notFindUser = true
return
}
if (data.code != 1) {
mui.alert(data.data)
return
}
that.stranger = data.data
})
.catch(function(error) {
console.log(error)
});
},
// 打开详情页
openInfo() {
mui.openWindow({
url: 'friend_info.html',
id: 'friend_info',
extras: {
userId: this.stranger.userId
}
});
}
},
created() {
mui.plusReady(function() {
});
}
})
</script>
</body>
</html>
页面效果:
这个页面难点在于输入框的回车事件的监听,如果是 vue 的 @keyup.enter.native 就监听不到,需要使用 mui 监听 keypress 事件。输入框 添加 mui-input-speech 的 class ,mui 实现了语音输入。
其他
展望
虽然开发了一些功能,但是还有很多问题需要去发现和解决。比如消息无法保存等 问题。
后边我希望增加有趣的功能,比如过滤 不良消息词汇,用美好的词代替,净化网络暴力。比如 : 你是傻逼 代替为 你是靓仔。让我们的交流更加美好。
当然了,如果你想到了有趣的功能欢迎留言(说不定哪天就去实现了)。要是愿意参与到开源项目的开发当然更好了。走过路过,留个 star 呗。
博客地址:https://www.anlazy.top/index.php/archives/308/
本人公众号:一只小安仔