Browse Source

编辑框支持显示表情

master
xsx 1 year ago
parent
commit
624af46dd9
  1. 29
      im-uniapp/common/emotion.js
  2. 2
      im-uniapp/components/chat-item/chat-item.vue
  3. 2
      im-uniapp/components/chat-message-item/chat-message-item.vue
  4. 6
      im-uniapp/components/file-upload/file-upload.vue
  5. 18
      im-uniapp/im.scss
  6. 3
      im-uniapp/manifest.json
  7. 15
      im-uniapp/pages.json
  8. 460
      im-uniapp/pages/chat/chat-box.vue

29
im-uniapp/common/emotion.js

@ -7,25 +7,21 @@ const emoTextList = ['憨笑', '媚眼', '开心', '坏笑', '可怜', '爱心',
];
let transform = (content) => {
return content.replace(/\#[\u4E00-\u9FA5]{1,3}\;/gi, textToImg);
let transform = (content, extClass) => {
return content.replace(/\#[\u4E00-\u9FA5]{1,3}\;/gi, (emoText)=>{
// 将匹配结果替换表情图片
let word = emoText.replace(/\#|\;/gi, '');
let idx = emoTextList.indexOf(word);
if (idx == -1) {
return emoText;
}
let path = textToPath(emoText);
let img = `<img src="${path}" class="${extClass}"/>`;
return img;
});
}
// 将匹配结果替换表情图片
let textToImg = (emoText) => {
let word = emoText.replace(/\#|\;/gi, '');
let idx = emoTextList.indexOf(word);
if (idx == -1) {
return emoText;
}
let path = textToPath(emoText);
let img = `<img src="${path}" style="with:30px;height:30px;
margin: 0 -2px;vertical-align:bottom;"/>`;
return img;
}
let textToPath = (emoText) => {
let word = emoText.replace(/\#|\;/gi, '');
@ -38,6 +34,5 @@ let textToPath = (emoText) => {
export default {
emoTextList,
transform,
textToImg,
textToPath
}

2
im-uniapp/components/chat-item/chat-item.vue

@ -16,7 +16,7 @@
<view class="chat-content">
<view class="chat-at-text">{{ atText }}</view>
<view class="chat-send-name" v-if="isShowSendName">{{ chat.sendNickName + ':&nbsp;' }}</view>
<rich-text class="chat-content-text" :nodes="$emo.transform(chat.lastContent)"></rich-text>
<rich-text class="chat-content-text" :nodes="$emo.transform(chat.lastContent,'emoji-small')"></rich-text>
<uni-badge v-if="chat.unreadCount > 0" :max-num="99" :text="chat.unreadCount" />
</view>
</view>

2
im-uniapp/components/chat-message-item/chat-message-item.vue

@ -17,7 +17,7 @@
<view class="chat-msg-bottom">
<view v-if="msgInfo.type == $enums.MESSAGE_TYPE.TEXT">
<long-press-menu :items="menuItems" @select="onSelectMenu">
<rich-text class="chat-msg-text" :nodes="$emo.transform(msgInfo.content)"></rich-text>
<rich-text class="chat-msg-text" :nodes="$emo.transform(msgInfo.content, 'emoji-normal')"></rich-text>
</long-press-menu>
</view>
<view class="chat-msg-image" v-if="msgInfo.type == $enums.MESSAGE_TYPE.IMAGE">

6
im-uniapp/components/file-upload/file-upload.vue

@ -43,6 +43,12 @@ export default {
}
},
methods: {
show() {
this.$refs.lsjUpload.show();
},
hide() {
this.$refs.lsjUpload.hide();
},
onUploadEnd(item) {
let file = this.fileMap.get(item.path);
if (item.type == 'fail') {

18
im-uniapp/im.scss

@ -171,4 +171,22 @@ button[size='mini'] {
uni-button + uni-button {
margin-top: 20rpx;
}
}
.emoji-large {
width: 64rpx;
height: 64rpx;
vertical-align: bottom;
}
.emoji-normal {
width: 54rpx;
height: 54rpx;
vertical-align: bottom;
}
.emoji-small {
width: 36rpx;
height: 36rpx;
vertical-align: bottom;
}

3
im-uniapp/manifest.json

@ -22,6 +22,9 @@
"Record" : {},
"Bluetooth" : {}
},
"softinput" : {
"mode" : "adjustResize"
},
/* */
"distribute" : {
/* android */

15
im-uniapp/pages.json

@ -7,8 +7,7 @@
"^u-([^-].*)": "@/uni_modules/uview-plus/components/u-$1/u-$1.vue"
}
},
"pages": [
{
"pages": [{
"path": "pages/login/login"
},
{
@ -30,7 +29,14 @@
"path": "pages/common/user-info"
},
{
"path": "pages/chat/chat-box"
"path": "pages/chat/chat-box",
"style": {
"navigationStyle": "custom",
"app-plus": {
// adjustPanadjustResize=webview+
"softinputMode": "adjustResize"
}
}
},
{
"path": "pages/chat/chat-private-video"
@ -71,8 +77,7 @@
"selectedColor": "#587ff0",
"borderStyle": "black",
"backgroundColor": "#ffffff",
"list": [
{
"list": [{
"pagePath": "pages/chat/chat",
"iconPath": "static/tarbar/chat.png",
"selectedIconPath": "static/tarbar/chat_active.png",

460
im-uniapp/pages/chat/chat-box.vue

@ -1,49 +1,54 @@
<template>
<view class="page chat-box">
<nav-bar back more @more="onShowMore">{{ title }}</nav-bar>
<view class="chat-msg" @click="switchChatTabBox('none', true)">
<scroll-view class="scroll-box" scroll-y="true" upper-threshold="200" @scrolltoupper="onScrollToTop"
:scroll-into-view="'chat-item-' + scrollMsgIdx">
<view v-if="chat" v-for="(msgInfo, idx) in chat.messages" :key="idx">
<chat-message-item v-if="idx >= showMinIdx" :headImage="headImage(msgInfo)" @call="onRtCall(msgInfo)"
:showName="showName(msgInfo)" @recall="onRecallMessage" @copy="onCopyMessage"
@delete="onDeleteMessage" @longPressHead="onLongPressHead(msgInfo)" @download="onDownloadFile"
:id="'chat-item-' + idx" :msgInfo="msgInfo" :groupMembers="groupMembers">
</chat-message-item>
</view>
</scroll-view>
</view>
<view v-if="atUserIds.length > 0" class="chat-at-bar" @click="openAtBox()">
<view class="iconfont icon-at">:&nbsp;</view>
<scroll-view v-if="atUserIds.length > 0" class="chat-at-scroll-box" scroll-x="true" scroll-left="120">
<view class="chat-at-items">
<view v-for="m in atUserItems" class="chat-at-item">
<head-image :name="m.showNickName" :url="m.headImage" size="minier"></head-image>
<view class="chat-main-box" :style="{height: chatMainHeight+'px'}">
<view class="chat-msg" @click="switchChatTabBox('none')">
<scroll-view class="scroll-box" scroll-y="true" upper-threshold="200" @scrolltoupper="onScrollToTop"
:scroll-into-view="'chat-item-' + scrollMsgIdx">
<view v-if="chat" v-for="(msgInfo, idx) in chat.messages" :key="idx">
<chat-message-item v-if="idx >= showMinIdx" :headImage="headImage(msgInfo)"
@call="onRtCall(msgInfo)" :showName="showName(msgInfo)" @recall="onRecallMessage"
@copy="onCopyMessage" @delete="onDeleteMessage" @longPressHead="onLongPressHead(msgInfo)"
@download="onDownloadFile" :id="'chat-item-' + idx" :msgInfo="msgInfo"
:groupMembers="groupMembers">
</chat-message-item>
</view>
</view>
</scroll-view>
</view>
<view class="send-bar">
<view v-if="!showRecord" class="iconfont icon-voice-circle" @click="onRecorderInput()"></view>
<view v-else class="iconfont icon-keyboard" @click="onKeyboardInput()"></view>
<chat-record v-if="showRecord" class="chat-record" @send="onSendRecord"></chat-record>
<view v-else class="send-text">
<textarea class="send-text-area" v-model="sendText" auto-height :show-confirm-bar="false"
:placeholder="isReceipt ? '[回执消息]' : ''" :adjust-position="false" @confirm="sendTextMessage()"
@keyboardheightchange="onKeyboardheightchange" @input="onTextInput" confirm-type="send" confirm-hold
:hold-keyboard="true"></textarea>
</scroll-view>
</view>
<view v-if="chat && chat.type == 'GROUP'" class="iconfont icon-at" @click="openAtBox()"></view>
<view class="iconfont icon-icon_emoji" @click="onShowEmoChatTab()"></view>
<view v-if="sendText == ''" class="iconfont icon-add" @click="onShowToolsChatTab()">
<view v-if="atUserIds.length > 0" class="chat-at-bar" @click="openAtBox()">
<view class="iconfont icon-at">:&nbsp;</view>
<scroll-view v-if="atUserIds.length > 0" class="chat-at-scroll-box" scroll-x="true" scroll-left="120">
<view class="chat-at-items">
<view v-for="m in atUserItems" class="chat-at-item">
<head-image :name="m.showNickName" :url="m.headImage" size="minier"></head-image>
</view>
</view>
</scroll-view>
</view>
<view class="send-bar">
<view v-if="!showRecord" class="iconfont icon-voice-circle" @click="onRecorderInput()"></view>
<view v-else class="iconfont icon-keyboard" @click="onKeyboardInput()"></view>
<chat-record v-if="showRecord" class="chat-record" @send="onSendRecord"></chat-record>
<view v-else class="send-text">
<editor id="editor" class="send-text-area" :placeholder="isReceipt ? '[回执消息]' : ''"
:read-only="isReadOnly" @focus="onEditorFocus" @blur="onEditorBlur" @ready="onEditorReady"
@input="onTextInput">
</editor>
<!-- <textarea class="send-text-area" v-model="sendText" auto-height :show-confirm-bar="false"
:placeholder="isReceipt ? '[回执消息]' : ''" :adjust-position="false" @confirm="sendTextMessage()"
@keyboardheightchange="onKeyboardheightchange" @input="onTextInput" confirm-type="send"
confirm-hold :hold-keyboard="true"></textarea> -->
</view>
<view v-if="chat && chat.type == 'GROUP'" class="iconfont icon-at" @click="openAtBox()"></view>
<view class="iconfont icon-icon_emoji" @click="onShowEmoChatTab()"></view>
<view v-if="isEmpty" class="iconfont icon-add" @click="onShowToolsChatTab()">
</view>
<button v-if="!isEmpty || atUserIds.length > 0" class="btn-send" type="primary"
@touchend.prevent="sendTextMessage()" size="mini">发送</button>
</view>
<button v-if="sendText != '' || atUserIds.length > 0" class="btn-send" type="primary"
@touchend.prevent="sendTextMessage()" size="mini">发送</button>
</view>
<view class="chat-tab-bar" v-show="chatTabBox != 'none' || (showKeyBoard && !isH5)"
:style="{ height: `${keyboardHeight}px` }">
<view v-if="chatTabBox == 'tools'" class="chat-tools">
<view class="chat-tab-bar">
<view v-if="chatTabBox == 'tools'" class="chat-tools" :style="{height: keyboardHeight+'px'}">
<view class="chat-tools-item">
<image-upload :maxCount="9" sourceType="album" :onBefore="onUploadImageBefore"
:onSuccess="onUploadImageSuccess" :onError="onUploadImageFail">
@ -60,7 +65,7 @@
</view>
<view class="chat-tools-item">
<file-upload :onBefore="onUploadFileBefore" :onSuccess="onUploadFileSuccess"
<file-upload ref="fileUpload" :onBefore="onUploadFileBefore" :onSuccess="onUploadFileSuccess"
:onError="onUploadFileFail">
<view class="tool-icon iconfont icon-folder"></view>
</file-upload>
@ -91,9 +96,10 @@
</view>
<!-- #endif -->
</view>
<scroll-view v-if="chatTabBox === 'emo'" class="chat-emotion" scroll-y="true">
<scroll-view v-if="chatTabBox === 'emo'" class="chat-emotion" scroll-y="true"
:style="{height: keyboardHeight+'px'}">
<view class="emotion-item-list">
<image class="emotion-item" :title="emoText" :src="$emo.textToPath(emoText)"
<image class="emotion-item emoji-large" :title="emoText" :src="$emo.textToPath(emoText)"
v-for="(emoText, i) in $emo.emoTextList" :key="i" @click="selectEmoji(emoText)" mode="aspectFit"
lazy-load="true"></image>
</view>
@ -122,29 +128,31 @@ export default {
friend: {},
group: {},
groupMembers: [],
sendText: "",
isReceipt: false, //
scrollMsgIdx: 0, //
chatTabBox: 'none',
showKeyBoard: false,
showRecord: false,
keyboardHeight: 322,
keyboardHeight: 300,
atUserIds: [],
needScrollToBottom: false, //
showMinIdx: 0, // showMinIdx
reqQueue: [], //
isSending: false, //
isH5: false // h5
isShowKeyBoard: false, //
editorCtx: null, //
isEmpty: true, //
isFocus: false, //
isReadOnly: false //
}
},
methods: {
onRecorderInput() {
this.showRecord = true;
this.switchChatTabBox('none', true);
this.switchChatTabBox('none');
},
onKeyboardInput() {
this.showRecord = false;
this.switchChatTabBox('none', false);
this.switchChatTabBox('none');
},
onSendRecord(data) {
let msgInfo = {
@ -250,35 +258,49 @@ export default {
}
},
sendTextMessage() {
if (!this.sendText.trim() && this.atUserIds.length == 0) {
return uni.showToast({
title: "不能发送空白信息",
icon: "none"
});
}
let receiptText = this.isReceipt ? "【回执消息】" : "";
let atText = this.createAtText();
let msgInfo = {
content: receiptText + this.sendText + atText,
atUserIds: this.atUserIds,
receipt: this.isReceipt,
type: 0
}
this.sendText = "";
// id
this.fillTargetId(msgInfo, this.chat.targetId);
this.sendMessageRequest(msgInfo).then((m) => {
m.selfSend = true;
this.chatStore.insertMessage(m);
//
this.moveChatToTop();
}).finally(() => {
//
this.scrollToBottom();
// @
this.atUserIds = [];
this.isReceipt = false;
});
this.editorCtx.getContents({
success: (e) => {
let sendText = this.isReceipt ? "【回执消息】" : "";
e.delta.ops.forEach((op) => {
if (op.insert.image) {
// emo
sendText += `#${op.attributes.alt};`
} else(
//
sendText += op.insert
)
})
if (!sendText.trim() && this.atUserIds.length == 0) {
return uni.showToast({
title: "不能发送空白信息",
icon: "none"
});
}
let receiptText = this.isReceipt ? "【回执消息】" : "";
let atText = this.createAtText();
let msgInfo = {
content: receiptText + sendText + atText,
atUserIds: this.atUserIds,
receipt: this.isReceipt,
type: 0
}
// id
this.fillTargetId(msgInfo, this.chat.targetId);
this.sendMessageRequest(msgInfo).then((m) => {
m.selfSend = true;
this.chatStore.insertMessage(m, this.chat);
//
this.moveChatToTop();
}).finally(() => {
//
this.scrollToBottom();
//
this.atUserIds = [];
this.isReceipt = false;
this.editorCtx.clear();
});
}
})
},
createAtText() {
let atText = "";
@ -325,31 +347,35 @@ export default {
},
onShowEmoChatTab() {
this.showRecord = false;
this.switchChatTabBox('emo', true)
this.switchChatTabBox('emo')
},
onShowToolsChatTab() {
this.showRecord = false;
this.switchChatTabBox('tools', true)
this.switchChatTabBox('tools')
},
switchChatTabBox(chatTabBox, hideKeyBoard) {
switchChatTabBox(chatTabBox) {
this.chatTabBox = chatTabBox;
if (hideKeyBoard) {
uni.hideKeyboard();
this.showKeyBoard = false;
if (chatTabBox != 'tools' && this.$refs.fileUpload) {
this.$refs.fileUpload.hide()
}
},
selectEmoji(emoText) {
this.sendText += `#${emoText};`;
},
onKeyboardheightchange(e) {
if (e.detail.height > 0) {
this.showKeyBoard = true;
this.switchChatTabBox('none', false)
this.keyboardHeight = this.rpxTopx(e.detail.height);
this.scrollToBottom();
} else {
this.showKeyBoard = false;
}
let path = this.$emo.textToPath(emoText)
//
this.isReadOnly = true;
this.isEmpty = false;
this.$nextTick(() => {
this.editorCtx.insertImage({
src: path,
alt: emoText,
extClass: 'emoji-small',
nowrap: true,
complete: () => {
this.isReadOnly = false;
this.editorCtx.blur();
}
});
})
},
onUploadImageBefore(file) {
let data = {
@ -540,14 +566,22 @@ export default {
}
},
onTextInput(e) {
let idx = e.detail.cursor - 1;
if (this.chat.type == 'GROUP' && e.detail.value[idx] == '@') {
this.openAtBox();
let sendText = e.detail.value.replace("@", '');
this.$nextTick(() => {
this.sendText = sendText;
})
}
this.isEmpty = e.detail.html == '<p><br></p>'
},
onEditorReady() {
const query = uni.createSelectorQuery().in(this);
query.select('#editor').context((res) => {
this.editorCtx = res.context
}).exec()
},
onEditorFocus(e) {
this.isFocus = true;
this.scrollToBottom()
this.switchChatTabBox('none')
},
onEditorBlur(e) {
this.isFocus = false;
},
loadReaded(fid) {
this.$http({
@ -638,21 +672,27 @@ export default {
})
}
},
listenKeyBoardForH5() {
listenKeyBoard() {
// #ifdef H5
// H5TextArea@keyboardheightchange
//
let initHeight = window.innerHeight;
window.addEventListener('resize', () => {
let keyboardHeight = initHeight - window.innerHeight;
if (keyboardHeight > 0) {
this.keyboardHeight = keyboardHeight - 20;
this.showKeyBoard = true;
this.switchChatTabBox('none', false)
this.scrollToBottom();
} else {
this.showKeyBoard = false;
this.isShowKeyBoard = keyboardHeight > 0;
if (this.isShowKeyBoard) {
this.keyboardHeight = keyboardHeight;
}
});
// #endif
// #ifndef H5
uni.onKeyboardHeightChange((res) => {
this.isShowKeyBoard = res.height > 0;
if (this.isShowKeyBoard) {
this.keyboardHeight = res.height; //
}
});
// #endif
},
generateId() {
// id
@ -705,10 +745,30 @@ export default {
}
})
return atUsers;
},
chatMainHeight() {
const sysInfo = uni.getSystemInfoSync();
let h = sysInfo.windowHeight;
//
h -= 50;
// #ifdef H5
// h5sysInfo.windowHeight
if (this.chatTabBox != 'none') {
h -= this.keyboardHeight;
}
// #endif
// #ifndef H5
//
h -= sysInfo.statusBarHeight;
if (this.isShowKeyBoard || this.chatTabBox != 'none') {
h -= this.keyboardHeight;
}
// #endif
return h;
}
},
watch: {
messageSize: function (newSize, oldSize) {
messageSize: function(newSize, oldSize) {
//
if (newSize > oldSize) {
let pages = getCurrentPages();
@ -730,10 +790,6 @@ export default {
}
},
onLoad(options) {
// #ifdef H5
this.isH5 = true;
this.listenKeyBoardForH5();
// #endif
//
this.chat = this.chatStore.chats[options.chatIdx];
// 20
@ -752,6 +808,8 @@ export default {
this.chatStore.activeChat(options.chatIdx);
//
this.isReceipt = false;
//
this.listenKeyBoard();
},
onShow() {
if (this.needScrollToBottom) {
@ -765,9 +823,11 @@ export default {
<style lang="scss" scoped>
.chat-box {
$icon-color: rgba(0, 0, 0, 0.88);
position: relative;
display: flex;
flex-direction: column;
background-color: #fafafa;
.header {
display: flex;
@ -775,7 +835,7 @@ export default {
align-items: center;
height: 60rpx;
padding: 5px;
background-color: #f9f9f9;
background-color: #fafafa;
line-height: 50px;
font-size: $im-font-size-large;
box-shadow: $im-box-shadow-lighter;
@ -792,99 +852,118 @@ export default {
}
}
.chat-msg {
flex: 1;
padding: 0;
overflow: hidden;
position: relative;
background-color: white;
.scroll-box {
height: 100%;
}
}
.chat-at-bar {
.chat-main-box {
// #ifdef H5
top: $im-nav-bar-height;
// #endif
// #ifndef H5
top: calc($im-nav-bar-height + var(--status-bar-height));
// #endif
position: fixed;
width: 100%;
display: flex;
align-items: center;
padding: 0 10rpx;
flex-direction: column;
z-index: 9;
.icon-at {
font-size: $im-font-size-larger;
color: $im-color-primary;
font-weight: bold;
}
.chat-at-scroll-box {
.chat-msg {
flex: 1;
width: 80%;
.chat-at-items {
display: flex;
align-items: center;
height: 70rpx;
padding: 0;
overflow: hidden;
position: relative;
background-color: white;
.chat-at-item {
padding: 0 3rpx;
}
.scroll-box {
height: 100%;
}
}
}
.chat-at-bar {
display: flex;
align-items: center;
padding: 0 10rpx;
$icon-color: rgba(0, 0, 0, 0.88);
.icon-at {
font-size: $im-font-size-larger;
color: $im-color-primary;
font-weight: bold;
}
.send-bar {
display: flex;
align-items: center;
padding: 10rpx;
//margin-bottom: 10rpx;
border-top: $im-border solid 1px;
background-color: $im-bg;
height: 80rpx;
//box-shadow: $im-box-shadow-lighter;
z-index: 1;
.chat-at-scroll-box {
flex: 1;
width: 80%;
.iconfont {
font-size: 60rpx;
margin: 0 10rpx;
color: $icon-color;
}
.chat-at-items {
display: flex;
align-items: center;
height: 70rpx;
.chat-at-item {
padding: 0 3rpx;
}
}
}
.chat-record {
flex: 1;
}
.send-text {
flex: 1;
overflow: auto;
padding: 14rpx 20rpx;
background-color: #fff;
border-radius: 8rpx;
font-size: $im-font-size;
box-sizing: border-box;
margin: 0 10rpx;
.send-text-area {
width: 100%;
.send-bar {
display: flex;
align-items: center;
padding: 10rpx;
border-top: $im-border solid 1px;
background-color: $im-bg;
min-height: 80rpx;
margin-bottom: 14rpx;
.iconfont {
font-size: 60rpx;
margin: 0 10rpx;
color: $icon-color;
}
.chat-record {
flex: 1;
}
.send-text {
flex: 1;
overflow: auto;
padding: 14rpx 20rpx;
background-color: #fff;
border-radius: 8rpx;
font-size: $im-font-size;
box-sizing: border-box;
margin: 0 10rpx;
position: relative;
.send-text-area {
width: 100%;
height: 100%;
min-height: 40rpx;
max-height: 200rpx;
font-size: 30rpx;
}
}
}
.btn-send {
margin: 5rpx;
.btn-send {
margin: 5rpx;
}
}
}
.chat-tab-bar {
height: 500rpx;
padding: 20rpx;
position: fixed;
bottom: 0;
background-color: $im-bg;
.chat-tools {
display: flex;
flex-wrap: wrap;
padding-top: 20rpx;
align-items: top;
height: 310px;
padding: 40rpx;
box-sizing: border-box;
.chat-tools-item {
width: 25%;
@ -915,19 +994,20 @@ export default {
}
.chat-emotion {
height: 100%;
height: 310px;
padding: 20rpx;
box-sizing: border-box;
.emotion-item-list {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
align-content: center;
.emotion-item {
width: 34px;
height: 34px;
text-align: center;
cursor: pointer;
padding: 6px;
padding: 5px;
}
}
}

Loading…
Cancel
Save