一.聊天模块页面列表设计与开发
1.开发设计
-
开发内容:开发聊天用户的列表布局与绑定
-
ion-item上使用click事件不是太好,故直接navPush属性设置点击跳转到的组件,通过navParam属性设置点击跳转组件时传递的参数
<ion-item [navPush]="ChatdetailsPage" [navParam]="userInfo.userId">
-
创建聊天详情页面
ionic g page chatdetails
2.实例代码
-
chatbubbles.html
<ion-header> <ion-navbar> <ion-title>冒泡</ion-title> </ion-navbar> </ion-header> <ion-content> <ion-list> <ion-item [navPush]="ChatdetailsPage" [navParams]="userInfo"> <ion-avatar item-left> <img src=""> </ion-avatar> <h2>迪丽热巴</h2> <p>热巴小姐姐Q你了一下哦~</p> </ion-item> </ion-list> </ion-content>
-
chatbubbles.ts
import { Component } from '@angular/core'; import { NavController, NavParams } from 'ionic-angular'; import {ChatdetailsPage} from '../chatdetails/chatdetails'; @Component({ selector: 'page-chatbubbles', templateUrl: 'chatbubbles.html', }) export class ChatbubblesPage { userInfo:any; ChatdetailsPage:any; constructor( public navCtrl: NavController, public navParams: NavParams ) { //模拟返回聊天用户列表 this.userInfo = { userId:'1', userName:'迪丽热巴', } this.ChatdetailsPage = ChatdetailsPage; } }
二.聊天对话页面布局
1.开发设计
- 获得传递过来的username并显示在最上方
navParams.get('userName')
- 编写消息聊天布局内容
2.实例代码
-
chatdeatils.html
<ion-header> <ion-navbar> <ion-title>{{chatUserName}}</ion-title> </ion-navbar> </ion-header> <ion-content> <div class="message-wrap"> <div class="message right"> <img src="../../assets/imgs/logo.png" class="user-img"/> <div class="msg-detail"> <div class="msg-info"> <p>Jack 1分钟前</p> </div> <div class="msg-content"> <p class="line-breaker">自己发的消息内容</p> </div> </div> </div> </div> </ion-content>
-
chatdetails.ts
import { Component } from '@angular/core'; import { IonicPage, NavController, NavParams, ViewController } from 'ionic-angular'; @Component({ selector: 'page-chatdetails', templateUrl: 'chatdetails.html', }) export class ChatdetailsPage { chatUserName:string; constructor( public navCtrl: NavController, public navParams: NavParams, public viewCtrl: ViewController) { //获得传递过来的username this.chatUserName = navParams.get('userName') } }
-
chatdetails.scss
page-chatdetails { $userBackgroundColor: #387ef5; $toUserBackgroundColor: #fff; ion-content .scroll-content { background-color: #f5f5f5; } ion-footer { box-shadow: 0 0 4px rgba(0, 0, 0, 0.11); background-color: #fff; height: 255px; } .line-breaker { white-space: pre-line; } .input-wrap { padding: 0 5px; ion-textarea { position: static; } ion-col.col { padding: 0; } button { width: 100%; height: 55px; font-size: 1.3em; margin: 0; } textarea { border-bottom: 1px #387ef5; border-style: solid; } } .message-wrap { padding: 0 10px; .message { position: relative; padding: 7px 0; .user-img { position: absolute; border-radius: 45px; width: 45px; height: 45px; box-shadow: 0 0 2px rgba(0, 0, 0, 0.36); } .msg-detail { width: 100%; display: inline-block; p { margin: 0; } .msg-info { p { font-size: .8em; color: #888; } } .msg-content { position: relative; margin-top: 5px; border-radius: 5px; padding: 8px; border: 1px solid #ddd; color: #fff; width: auto; span.triangle { background-color: #fff; border-radius: 2px; height: 8px; width: 8px; top: 12px; display: block; border-style: solid; border-color: #ddd; border-width: 1px; -webkit-transform: rotate(45deg); transform: rotate(45deg); position: absolute; } } } } .message.left { .msg-content { background-color: $toUserBackgroundColor; float: left; } .msg-detail { padding-left: 60px; } .user-img { left: 0; } .msg-content { color: #343434; span.triangle { border-top-width: 0; border-right-width: 0; left: -5px; } } } .message.right { .msg-detail { padding-right: 60px; .msg-info { text-align: right; } } .user-img { right: 0; } ion-spinner { position: absolute; right: 10px; top: 50px; } .msg-content { background-color: $userBackgroundColor; float: right; span.triangle { background-color: $userBackgroundColor; border-bottom-width: 0; border-left-width: 0; right: -5px; } } } } }
三.聊天对话页面底部输入框设计
1.开发设计
- 输入框以及表情选择页面的输入与开发
- 开发技巧
- 开发底栏组件使用ion-footer
- ionic没有边框的属性:no-border
- 使用ion-grid/ion-row/ion-col实现响应式编程
2.实例代码
-
chatdetails.html
<ion-header> <ion-navbar> <ion-title>{{chatUserName}}</ion-title> </ion-navbar> </ion-header> <ion-content> <div class="message-wrap"> <div class="message right"> <img src="../../assets/imgs/logo.png" class="user-img"/> <div class="msg-detail"> <div class="msg-info"> <p>Jack 1分钟前</p> </div> <div class="msg-content"> <p class="line-breaker">Hi,热巴小姐姐</p> </div> </div> </div> </div> </ion-content> <!-- ionic没有边框的属性:no-border --> <ion-footer no-border style="height: 55px;"> <ion-grid class="input-wrap"> <ion-row> <ion-col col-2> <button ion-button clear ion-only item-right> <ion-icon name="md-happy"></ion-icon> </button> </ion-col> <ion-col col-8> <ion-textarea placeholder="请输入内容"></ion-textarea> </ion-col> <ion-col col-2> <button ion-button clear ion-only item-right> <ion-icon name="send"></ion-icon> </button> </ion-col> </ion-row> </ion-grid> </ion-footer>
四.聊天对话页面表情输入模块的开发
1.开发思路
- 创建providers用于获得emoji表情
ionic g provider emoji
- 生成component
ionic g component emojipicker
,注意自动创建module,需要添加imports并注册到app.modules中 - 编写emojipicker组件(Angular表单——实现接口ControlValueAccessor,通过ControlValueAccessor可以将原生dom节点内容转换成表单内容;使用ion-sliders组件可以实现左右滑动切换
<ion-slides pager>
)和chatdetails页面内容
2.实例代码
-
emoji.ts
import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; @Injectable() export class EmojiProvider { constructor(public http: HttpClient) { console.log('Hello EmojiProvider Provider'); } //获取所有表情的数组(已经分组好了的) getEmojis() { const EMOJIS = "? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?" + " ☹️ ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?" + " ? ? ? ? ? ☠️ ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ✊ ? ? ? ✌️ ? ? ? ? ? ? ☝️ ✋ ?" + " ? ? ? ? ? ? ✍️ ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?♀️ ? ? ? ? ?♀️ ? ?♀️ ? ?♀️ ?" + " ?♀️ ? ?️♀️ ?️ ?⚕️ ?⚕️ ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ??" + " ?? ?? ?? ?? ?✈️ ?✈️ ?? ?? ?⚖️ ?⚖️ ? ? ? ? ? ? ? ? ?♀️ ? ? ?♂️ ? ?♂️ ? ?♂️ ? ?♂️ ?♀️ ?♂️ ?♀" + "️ ?♂️ ? ?♂️ ? ?♂️ ? ?♂️ ? ?♂️ ? ? ? ? ?♂️ ?♀️ ? ?♀️ ? ? ? ? ? ?❤️? ?❤️? ? ?❤️?? ?❤️?? ? ???" + " ???? ???? ???? ??? ??? ???? ???? ???? ??? ??? ???? ???? ???? ?? ??" + " ??? ??? ??? ?? ?? ??? ??? ??? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ⛑ ? ? ? ? ? ?" + " ? ? ☂️"; //进行分组的操作 let array = EMOJIS.split(' '); let groupNumber = Math.ceil(array.length / 24); //四舍五入,尽量取大数 15.1->16 , 15.6->16 let items = []; //分组填充表情 for (let i = 0; i < groupNumber; i++) { items.push(array.slice(24 * i, 24 * (i + 1))) } return items; } }
-
emojipicker.ts
import { Component, forwardRef } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { EmojiProvider } from '../../providers/emoji/emoji'; import { _ParseAST } from '@angular/compiler'; //实现EmojipickerComponent的providers export const EMOJI_ACCESSOR:any = { provide:NG_VALUE_ACCESSOR, useExisting:forwardRef(()=>EmojipickerComponent), multi:true } @Component({ selector: 'emojipicker', templateUrl: 'emojipicker.html', providers:[EMOJI_ACCESSOR] }) //Angular表单——实现接口ControlValueAccessor //可以将dom转成angular form export class EmojipickerComponent implements ControlValueAccessor{ //emoji表情 emojiArray = []; //输入信息内容 content:string; onChange:Function; onTouched:Function; constructor( emojiProvider:EmojiProvider ) { this.emojiArray = emojiProvider.getEmojis();//获得所有emoji表情 } //写一个值到element上面 writeValue(obj: any): void { this.content = obj; } registerOnChange(fn: any): void { this.onChange = fn; this.setValue(this.content); } registerOnTouched(fn: any): void { this.onTouched = fn; } //在此处新的内容的赋值以及函数的绑定 setValue(val:any){ this.content += val; if(this.content){ this.onChange(this.content); } } }
-
emojipicker.html
<div class="emoji-picker"> <div class="emoji-items"> <!-- 使用Sliders实现分页左右滑动 --> <ion-slides pager> <ion-slide *ngFor="let items of emojiArray"> <span class="emoji-item" (click)="setValue(item)" *ngFor="let item of items"> {{item}} </span> </ion-slide> </ion-slides> </div> </div>
-
emojipicker.scss
emojipicker { .emoji-picker{ height: 195px; border-top:1px solid #999; .emoji-items{ padding: 10px; width: 100%; height: 100%; .emoji-item{ display: block; float: left; width: 12.5%; height: 42px; font-size: 1.2em; line-height: 42px; text-align: center; margin-bottom: 10px; } } } }
-
component.module.ts
import { NgModule } from '@angular/core'; import { EmojipickerComponent } from './emojipicker/emojipicker'; import { IonicPageModule } from 'ionic-angular'; @NgModule({ declarations: [EmojipickerComponent], imports: [IonicPageModule.forChild(EmojipickerComponent)], exports: [EmojipickerComponent] }) export class ComponentsModule {}
-
chatdetails.ts
import { Component } from '@angular/core'; import { IonicPage, NavController, NavParams, ViewController } from 'ionic-angular'; @Component({ selector: 'page-chatdetails', templateUrl: 'chatdetails.html', }) export class ChatdetailsPage { chatUserName:string; //决定emoji是否显示 isOpenEmojiPicker = false; constructor( public navCtrl: NavController, public navParams: NavParams, public viewCtrl: ViewController) { //获得传递过来的username this.chatUserName = navParams.get('userName') } //切换emoji是否显示 swichEmojiPicker() { this.isOpenEmojiPicker = !this.isOpenEmojiPicker; } }
-
chatdetails.html
<ion-header> <ion-navbar> <ion-title>{{chatUserName}}</ion-title> </ion-navbar> </ion-header> <ion-content> <div class="message-wrap"> <div class="message right"> <img src="../../assets/imgs/logo.png" class="user-img"/> <div class="msg-detail"> <div class="msg-info"> <p>Jack 1分钟前</p> </div> <div class="msg-content"> <p class="line-breaker">Hi,热巴小姐姐</p> </div> </div> </div> </div> </ion-content> <!-- ionic没有边框的属性:no-border --> <ion-footer no-border [style.height]="isOpenEmojiPicker?'255px':'55px'"> <ion-grid class="input-wrap"> <ion-row> <ion-col col-2> <button ion-button clear ion-only item-right (click)="swichEmojiPicker()"> <ion-icon name="md-happy"></ion-icon> </button> </ion-col> <ion-col col-8> <ion-textarea placeholder="请输入内容"></ion-textarea> </ion-col> <ion-col col-2> <button ion-button clear ion-only item-right> <ion-icon name="send"></ion-icon> </button> </ion-col> </ion-row> </ion-grid> <!-- 通过是否点击上方emoji按钮决定是否显示下方组件 --> <emojipicker *ngIf="isOpenEmojiPicker"></emojipicker> </ion-footer>
-
chatdetails.scss
page-chatdetails { $userBackgroundColor: #387ef5; $toUserBackgroundColor: #fff; ion-content .scroll-content { background-color: #f5f5f5; } ion-footer { box-shadow: 0 0 4px rgba(0, 0, 0, 0.11); background-color: #fff; height: 255px; } .line-breaker { white-space: pre-line; } .input-wrap { padding: 0 5px; ion-textarea { position: static; } ion-col.col { padding: 0; } button { width: 100%; height: 55px; font-size: 1.3em; margin: 0; } textarea { border-bottom: 1px #387ef5; border-style: solid; } } .message-wrap { padding: 0 10px; .message { position: relative; padding: 7px 0; .user-img { position: absolute; border-radius: 45px; width: 45px; height: 45px; box-shadow: 0 0 2px rgba(0, 0, 0, 0.36); } .msg-detail { width: 100%; display: inline-block; p { margin: 0; } .msg-info { p { font-size: .8em; color: #888; } } .msg-content { position: relative; margin-top: 5px; border-radius: 5px; padding: 8px; border: 1px solid #ddd; color: #fff; width: auto; span.triangle { background-color: #fff; border-radius: 2px; height: 8px; width: 8px; top: 12px; display: block; border-style: solid; border-color: #ddd; border-width: 1px; -webkit-transform: rotate(45deg); transform: rotate(45deg); position: absolute; } } } } .message.left { .msg-content { background-color: $toUserBackgroundColor; float: left; } .msg-detail { padding-left: 60px; } .user-img { left: 0; } .msg-content { color: #343434; span.triangle { border-top-width: 0; border-right-width: 0; left: -5px; } } } .message.right { .msg-detail { padding-right: 60px; .msg-info { text-align: right; } } .user-img { right: 0; } ion-spinner { position: absolute; right: 10px; top: 50px; } .msg-content { background-color: $userBackgroundColor; float: right; span.triangle { background-color: $userBackgroundColor; border-bottom-width: 0; border-left-width: 0; right: -5px; } } } } }
五.聊天对话页面自动回复逻辑开发
1.开发设计
-
实现进入冒泡页面查看详情,通过mock的json数据查看历史消息,创建emojipicker组件实现发送表情,通过消息订阅实现消息返回并被用户监听查看
-
使用mock的json模拟后台的返回数据
-
如何在开发中mock数据
-
创建mock的json文件
-
请求返回数据的内容与发送请求访问服务一样
getMessageList():Promise<ChatMessage[]>{ const url = '../../assets/mock/msg-list.json'; return this.http.get(url) .toPromise() .then((response:any) => response.array as ChatMessage[]) .catch(error => Promise.reject(error || '错误信息')); }
-
-
-
使用ViewChild获取页面DOM节点
- 使用Content可以获取整个页面内容,调用
this.content.scrollToBottom();
可以将页面滚到底部 - 使用具体值可以定位具体DOM节点,在html中通过
#名称
标记DOM,在ts中通过@ViewChild('名称') messageInput:TextInput;
获取
- 使用Content可以获取整个页面内容,调用
-
Events模块实现消息订阅[订阅者模式]
-
订阅消息使用:subscribe方法
//参数:订阅topic、回调函数(消息内容,时间) this.events.subscribe('chat.received',(msg,time)=>{ this.messageList.push(msg); this.scrollToBottom(); })
-
发布消息使用:publish方法
//将消息发布出去,参数: 事件topic,消息内容,时间 this.events.publish("chat.received",messageSend,Date.now());
-
取消订阅使用:unsubscribe方法
//取消订阅,参数: 事件topic this.events.unsubscribe('chat.received');
-
-
安装moment:
npm install moment --save
,moment是npm轻量级的js数据库,引入moment的方式import * as moment from 'moment';
[所有引入javascript包都是如此] -
管道(像管子一样将你定义的方法“流到”所有你需要的地方去),创建管道
ionic g pipe relativtime
2.实例代码
-
处理时间,获得与当前时间差情况的pipe文件,relativetime.ts
import { Pipe, PipeTransform } from '@angular/core'; import * as moment from 'moment'; @Pipe({ name: 'relativetime', }) export class RelativetimePipe implements PipeTransform { transform(value: string, ...args) { //使用moment(时间戳).toNow()则可以得到时间戳距现在多久了 return moment(value).toNow(); } }
-
聊天消息处理,用于查看、发送、模拟消息处理与订阅发布的service,chatservice.ts
import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { Events } from 'ionic-angular'; //聊天信息的属性 export class ChatMessage { messageId:string; userId:string; userName:string; userImgUrl:string; toUserId:string;//发送给谁的Id time:number | string; message:string; status:string; } //用户信息的属性 export class UserInfo { userId:string; userName:string; userImgUrl:string; } @Injectable() export class ChatserviceProvider { constructor( public http: HttpClient, public events: Events ) {} /** * 获取消息列表 * 从获取的JSON中获取 */ getMessageList():Promise<ChatMessage[]>{ const url = '../../assets/mock/msg-list.json'; return this.http.get(url) .toPromise() .then((response:any) => response.array as ChatMessage[]) .catch(error => Promise.reject(error || '错误信息')); } /** * 发送消息 * @param message */ sendMessage(message:ChatMessage) { return new Promise(resolve => setTimeout(()=>{ resolve(message) },Math.random()*1000)) .then(()=>{ //模拟返回消息 this.mockNewMessage(message); }); } /** * 模拟对方返回消息 * 使用发布订阅模式实现即时接受消息 * 引入Events模块 * Events is a public-subscribe style event system for sending and responding to application-level event across your app * * @param message */ mockNewMessage(message:ChatMessage) { const id = Date.now().toString(); let messageSend:ChatMessage = { messageId : id, userId : '123321', userName : '迪丽热巴', userImgUrl : '../../assets/imgs/girl.jpg', toUserId : message.userId, time : Date.now(), message : '不要跟我说:'+message.message+'我只想给你一个么么哒!! ?', status : 'seccess' } //模拟网络请求卡一下 setTimeout(()=>{ //将消息发布出去,参数: 事件主题,消息内容,时间 this.events.publish("chat.received",messageSend,Date.now()); }, Math.random()*1000); } }
-
聊天详情页面布局,chatdetails.html
<ion-header> <ion-navbar> <ion-title>{{chatUserName}}</ion-title> </ion-navbar> </ion-header> <ion-content> <div class="message-wrap"> <div class="message" *ngFor="let m of messageList" [class.left]="m.userId === chatUserId" [class.right]="m.userId === userId"> <img [src]="m.userImgUrl" class="user-img"/> <!-- 因为做了模拟延迟发送效果,发送期间消息的status是pending,故当发送但没有完成时头像下方会显示 ... 的加载效果 --> <ion-spinner name="dots" *ngIf="m.status === 'pending'"></ion-spinner> <div class="msg-detail"> <div class="msg-info"> <p>{{m.userName}} {{m.time | relativetime}}</p> </div> <div class="msg-content"> <p class="line-breaker">{{m.message}}</p> </div> </div> </div> </div> </ion-content> <!-- ionic没有边框的属性:no-border --> <ion-footer no-border [style.height]="isOpenEmojiPicker?'255px':'55px'"> <ion-grid class="input-wrap"> <ion-row> <ion-col col-2> <button ion-button clear ion-only item-right (click)="swichEmojiPicker()"> <ion-icon name="md-happy"></ion-icon> </button> </ion-col> <ion-col col-8> <ion-textarea #chatInput [(ngModel)]="editorMessage" (keyup.enter)="sendMessage()" (focus)="focus()" placeholder="请输入内容"></ion-textarea> </ion-col> <ion-col col-2> <button ion-button clear ion-only item-right (click)="sendMessage()"> <ion-icon name="send"></ion-icon> </button> </ion-col> </ion-row> </ion-grid> <emojipicker *ngIf="isOpenEmojiPicker" [(ngModel)]="editorMessage"></emojipicker> </ion-footer>
-
聊天详情页面控制器,chatdetails.ts
import { Component, ViewChild } from '@angular/core'; import { NavController, NavParams, ViewController, Content, TextInput, Events } from 'ionic-angular'; import { ChatserviceProvider, ChatMessage } from '../../providers/chatservice/chatservice'; import { Storage } from '@ionic/storage'; @Component({ selector: 'page-chatdetails', templateUrl: 'chatdetails.html', }) export class ChatdetailsPage { //和哪个username聊天 chatUserName:string; //和哪个userid聊天 chatUserId:string; //当前用户信息 userId:string; userName:string; userImgUrl:string; //决定emoji是否显示 isOpenEmojiPicker = false; //对话消息列表 messageList:ChatMessage[] = []; //获取页面DOM节点 //获取整个页面是Content @ViewChild(Content) content:Content; //用户获取用户输入的输入框 @ViewChild('chatInput') messageInput:TextInput; editorMessage:string=''; constructor( public navCtrl: NavController, public navParams: NavParams, public viewCtrl: ViewController, public chatService: ChatserviceProvider, public storage: Storage, public events: Events) { //获得传递过来的username this.chatUserName = navParams.get('userName'); //获取当前和谁聊天的userId this.chatUserId = navParams.get('userId'); } //页面进来时发送的请求ionViewDidEnter[注意不能使用ionViewDidLoad,如果使用页面会闪一下,ionViewDidLoad是页面渲染后完成的内容] ionViewDidEnter() { this.storage.get('token').then((val)=>{ if(val !== null){ this.userId = '140000198202211138'; this.userName = 'Jack汪喆'; this.userImgUrl = '../../assets/imgs/avatar.jpg'; } }) this.getMessage() .then(()=>{ this.scrollToBottom();//消息显示完成后需要将页面滚到最下面 }) //听取消息的发布和订阅 //此处通过消息主题实现订阅 this.events.subscribe('chat.received',(msg,time)=>{ this.messageList.push(msg); this.scrollToBottom(); }) } //在将要离开页面时,退订订阅的内容 ionViewWillLeave() { this.events.unsubscribe('chat.received'); } //切换emoji是否显示 swichEmojiPicker() { this.isOpenEmojiPicker = !this.isOpenEmojiPicker; } //获得消息内容,调用service中属性的方法实现属性的赋值 getMessage() { //返回的是promise return this.chatService.getMessageList() .then(res=>{ this.messageList = res; }) .catch((error)=>{ console.log(error); }); } //页面滚动到最下面 scrollToBottom() { setTimeout(()=>{ if(this.content.scrollToBottom){ this.content.scrollToBottom(); } },400); } //发送消息 sendMessage() { //如果为空则不做任何处理,即不发送消息 if(!this.editorMessage.trim()){ return; } const id = Date.now().toString(); let messageSend:ChatMessage = { messageId : id, userId : this.userId, userName : this.userName, userImgUrl : this.userImgUrl, toUserId : this.chatUserId, time : Date.now(), message : this.editorMessage, status : 'pending'//表示消息的状态,pending表示正在发送中 } this.messageList.push(messageSend); this.scrollToBottom(); this.editorMessage = ''; if(!this.isOpenEmojiPicker){ //如果表情窗口没被打开,则将输入信息窗口聚焦 this.messageInput.setFocus(); } this.chatService.sendMessage(messageSend) .then(()=>{ let index = this.getMessageIndex(id); if(index!==-1){ this.messageList[index].status = 'success'; } }); } //聚焦事件:当光标移到textarea时触发 focus() { this.isOpenEmojiPicker = false; this.content.resize();//重新计算整个页面DOM节点的内容 this.scrollToBottom(); } //从当前消息列表中获取对应messageId的下标的message getMessageIndex(id:string){ return this.messageList.findIndex(e=>{ return e.messageId === id; }) } }