main
parent
c2903e3b2a
commit
95e91728a0
@ -0,0 +1,66 @@
|
||||
let toTimeText = (timeStamp, simple) => {
|
||||
var dateTime = new Date(timeStamp)
|
||||
var currentTime = Date.parse(new Date()); //当前时间
|
||||
var timeDiff = currentTime - dateTime; //与当前时间误差
|
||||
var timeText = '';
|
||||
if (timeDiff <= 60000) { //一分钟内
|
||||
timeText = '刚刚';
|
||||
} else if (timeDiff > 60000 && timeDiff < 3600000) {
|
||||
//1小时内
|
||||
timeText = Math.floor(timeDiff / 60000) + '分钟前';
|
||||
} else if (timeDiff >= 3600000 && timeDiff < 86400000 && !isYestday(dateTime)) {
|
||||
//今日
|
||||
timeText = formatDateTime(dateTime).substr(11, 5);
|
||||
} else if (isYestday(dateTime)) {
|
||||
//昨天
|
||||
timeText = '昨天' + formatDateTime(dateTime).substr(11, 5);
|
||||
} else if (isYear(dateTime)) {
|
||||
//今年
|
||||
timeText = formatDateTime(dateTime).substr(5, simple ? 5 : 14);
|
||||
} else {
|
||||
//不属于今年
|
||||
timeText = formatDateTime(dateTime);
|
||||
if (simple) {
|
||||
timeText = timeText.substr(2, 8);
|
||||
}
|
||||
}
|
||||
return timeText;
|
||||
}
|
||||
|
||||
let isYestday = (date) => {
|
||||
var yesterday = new Date(new Date() - 1000 * 60 * 60 * 24);
|
||||
return yesterday.getYear() === date.getYear() &&
|
||||
yesterday.getMonth() === date.getMonth() &&
|
||||
yesterday.getDate() === date.getDate();
|
||||
}
|
||||
|
||||
let isYear = (date) => {
|
||||
return date.getYear() === new Date().getYear();
|
||||
}
|
||||
|
||||
let formatDateTime = (date) => {
|
||||
if (date === '' || !date) {
|
||||
return ''
|
||||
}
|
||||
var dateObject = new Date(date)
|
||||
var y = dateObject.getFullYear()
|
||||
var m = dateObject.getMonth() + 1
|
||||
m = m < 10 ? ('0' + m) : m
|
||||
var d = dateObject.getDate()
|
||||
d = d < 10 ? ('0' + d) : d
|
||||
var h = dateObject.getHours()
|
||||
h = h < 10 ? ('0' + h) : h
|
||||
var minute = dateObject.getMinutes()
|
||||
minute = minute < 10 ? ('0' + minute) : minute
|
||||
var second = dateObject.getSeconds()
|
||||
second = second < 10 ? ('0' + second) : second
|
||||
return y + '/' + m + '/' + d + ' ' + h + ':' + minute + ':' + second
|
||||
}
|
||||
|
||||
|
||||
export {
|
||||
toTimeText,
|
||||
isYestday,
|
||||
isYear,
|
||||
formatDateTime
|
||||
}
|
||||
@ -0,0 +1,68 @@
|
||||
|
||||
const MESSAGE_TYPE = {
|
||||
TEXT: 0,
|
||||
IMAGE: 1,
|
||||
FILE: 2,
|
||||
AUDIO: 3,
|
||||
VIDEO: 4,
|
||||
ORDER: 5,
|
||||
PRODUCT: 6,
|
||||
RECALL: 10,
|
||||
READED: 11,
|
||||
RECEIPT: 12,
|
||||
TIP_TIME: 20,
|
||||
TIP_TEXT: 21,
|
||||
LOADING: 30,
|
||||
ACT_RT_VOICE: 40,
|
||||
ACT_RT_VIDEO: 41,
|
||||
USER_BANNED: 50,
|
||||
FRIEND_NEW: 80,
|
||||
FRIEND_DEL: 81,
|
||||
GROUP_NEW: 90,
|
||||
GROUP_DEL: 91,
|
||||
RTC_CALL_VOICE: 100,
|
||||
RTC_CALL_VIDEO: 101,
|
||||
RTC_ACCEPT: 102,
|
||||
RTC_REJECT: 103,
|
||||
RTC_CANCEL: 104,
|
||||
RTC_FAILED: 105,
|
||||
RTC_HANDUP: 106,
|
||||
RTC_CANDIDATE: 107,
|
||||
RTC_GROUP_SETUP: 200,
|
||||
RTC_GROUP_ACCEPT: 201,
|
||||
RTC_GROUP_REJECT: 202,
|
||||
RTC_GROUP_FAILED: 203,
|
||||
RTC_GROUP_CANCEL: 204,
|
||||
RTC_GROUP_QUIT: 205,
|
||||
RTC_GROUP_INVITE: 206,
|
||||
RTC_GROUP_JOIN: 207,
|
||||
RTC_GROUP_OFFER: 208,
|
||||
RTC_GROUP_ANSWER: 209,
|
||||
RTC_GROUP_CANDIDATE: 210,
|
||||
RTC_GROUP_DEVICE: 211
|
||||
}
|
||||
|
||||
const USER_STATE = {
|
||||
OFFLINE: 0,
|
||||
FREE: 1,
|
||||
BUSY: 2
|
||||
}
|
||||
|
||||
const TERMINAL_TYPE = {
|
||||
WEB: 0,
|
||||
APP: 1
|
||||
}
|
||||
|
||||
const MESSAGE_STATUS = {
|
||||
UNSEND: 0,
|
||||
SENDED: 1,
|
||||
RECALL: 2,
|
||||
READED: 3
|
||||
}
|
||||
|
||||
export {
|
||||
MESSAGE_TYPE,
|
||||
USER_STATE,
|
||||
TERMINAL_TYPE,
|
||||
MESSAGE_STATUS
|
||||
}
|
||||
@ -0,0 +1,40 @@
|
||||
|
||||
// 是否普通消息
|
||||
let isNormal = function (type) {
|
||||
return type >= 0 && type < 10;
|
||||
}
|
||||
|
||||
// 是否状态消息
|
||||
let isStatus = function (type) {
|
||||
return type >= 10 && type < 20;
|
||||
}
|
||||
|
||||
// 是否提示消息
|
||||
let isTip = function (type) {
|
||||
return type >= 20 && type < 30;
|
||||
}
|
||||
|
||||
// 操作交互类消息
|
||||
let isAction = function (type) {
|
||||
return type >= 40 && type < 50;
|
||||
}
|
||||
|
||||
// 单人通话信令
|
||||
let isRtcPrivate = function (type) {
|
||||
return type >= 100 && type < 200;
|
||||
}
|
||||
|
||||
// 多人通话信令
|
||||
let isRtcGroup = function (type) {
|
||||
return type >= 200 && type < 300;
|
||||
}
|
||||
|
||||
|
||||
export {
|
||||
isNormal,
|
||||
isStatus,
|
||||
isTip,
|
||||
isAction,
|
||||
isRtcPrivate,
|
||||
isRtcGroup
|
||||
}
|
||||
@ -0,0 +1,173 @@
|
||||
<template>
|
||||
<uni-popup ref="popup" type="bottom" @change="onChange">
|
||||
<view class="chat-at-box">
|
||||
<view class="chat-at-top">
|
||||
<view class="chat-at-tip"> 选择要提醒的人</view>
|
||||
<button class="chat-at-btn" type="warn" size="mini" @click="onClean()">清空 </button>
|
||||
<button class="chat-at-btn" type="primary" size="mini" @click="onOk()">确定({{ atUserIds.length }})
|
||||
</button>
|
||||
</view>
|
||||
<scroll-view v-show="atUserIds.length > 0" scroll-x="true" scroll-left="120">
|
||||
<view class="at-user-items">
|
||||
<view v-for="m in checkedMembers" class="at-user-item" :key="m.userId">
|
||||
<head-image :name="m.showNickName" :url="m.headImage" size="mini"></head-image>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
<view class="search-bar">
|
||||
<uni-search-bar v-model="searchText" cancelButton="none" radius="100" placeholder="搜索"></uni-search-bar>
|
||||
</view>
|
||||
<view class="member-items">
|
||||
<virtual-scroller :items="memberItems">
|
||||
<template v-slot="{ item }">
|
||||
<view class="member-item" @click="onSwitchChecked(item)">
|
||||
<head-image :name="item.showNickName" :online="item.online" :url="item.headImage"
|
||||
size="small"></head-image>
|
||||
<view class="member-name">{{ item.showNickName }}</view>
|
||||
<radio :checked="item.checked" :disabled="item.locked"
|
||||
@click.stop="onSwitchChecked(item)" />
|
||||
</view>
|
||||
</template>
|
||||
</virtual-scroller>
|
||||
</view>
|
||||
</view>
|
||||
</uni-popup>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "chat-at-box",
|
||||
props: {
|
||||
ownerId: {
|
||||
type: Number,
|
||||
},
|
||||
members: {
|
||||
type: Array
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
searchText: "",
|
||||
showMembers: []
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
init(atUserIds) {
|
||||
this.showMembers = [];
|
||||
let userId = this.userStore.userInfo.id;
|
||||
if (this.ownerId == userId) {
|
||||
this.showMembers.push({
|
||||
userId: -1,
|
||||
showNickName: "全体成员"
|
||||
})
|
||||
}
|
||||
this.members.forEach((m) => {
|
||||
if (!m.quit && m.userId != userId) {
|
||||
m.checked = atUserIds.indexOf(m.userId) >= 0;
|
||||
this.showMembers.push(m);
|
||||
}
|
||||
});
|
||||
},
|
||||
open() {
|
||||
this.$refs.popup.open();
|
||||
},
|
||||
onSwitchChecked(member) {
|
||||
member.checked = !member.checked;
|
||||
},
|
||||
onClean() {
|
||||
this.showMembers.forEach((m) => {
|
||||
m.checked = false;
|
||||
})
|
||||
},
|
||||
onOk() {
|
||||
this.$refs.popup.close();
|
||||
},
|
||||
onChange(e) {
|
||||
if (!e.show) {
|
||||
this.$emit("complete", this.atUserIds)
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
atUserIds() {
|
||||
return this.showMembers.filter(m => m.checked).map(m => m.userId);
|
||||
},
|
||||
checkedMembers() {
|
||||
return this.showMembers.filter(m => m.checked);
|
||||
},
|
||||
memberItems() {
|
||||
return this.showMembers.filter(m => m.showNickName.includes(this.searchText));
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.chat-at-box {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: white;
|
||||
padding: 10rpx;
|
||||
//border-radius: 15rpx;
|
||||
|
||||
.chat-at-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 70rpx;
|
||||
padding: 10rpx;
|
||||
|
||||
.chat-at-tip {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.chat-at-btn {
|
||||
margin-left: 10rpx;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.at-user-items {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 90rpx;
|
||||
|
||||
.at-user-item {
|
||||
padding: 3rpx;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
.member-items {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
|
||||
.member-item {
|
||||
height: 110rpx;
|
||||
display: flex;
|
||||
position: relative;
|
||||
padding: 0 30rpx;
|
||||
align-items: center;
|
||||
background-color: white;
|
||||
white-space: nowrap;
|
||||
margin-bottom: 1px;
|
||||
|
||||
&:hover {
|
||||
background-color: $im-bg-active;
|
||||
}
|
||||
|
||||
.member-name {
|
||||
flex: 1;
|
||||
padding-left: 20rpx;
|
||||
font-size: $im-font-size;
|
||||
line-height: 60rpx;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,559 @@
|
||||
<template>
|
||||
<view class="chat-message-item">
|
||||
<view class="message-tip" v-if="msgInfo.type == $enums.MESSAGE_TYPE.TIP_TEXT">
|
||||
{{ msgInfo.content }}
|
||||
</view>
|
||||
<view class="message-tip" v-else-if="msgInfo.type == $enums.MESSAGE_TYPE.TIP_TIME">
|
||||
{{ $date.toTimeText(msgInfo.sendTime) }}
|
||||
</view>
|
||||
<view class="message-normal" v-else-if="isNormal" :class="{ 'message-mine': msgInfo.selfSend }">
|
||||
<head-image class="avatar" @longpress.prevent="$emit('longPressHead')" :id="msgInfo.sendId" :url="headImage"
|
||||
:name="showName" size="small"></head-image>
|
||||
<view class="content">
|
||||
<view v-if="msgInfo.groupId && !msgInfo.selfSend" class="top">
|
||||
<text>{{ showName }}</text>
|
||||
</view>
|
||||
|
||||
<view class="bottom">
|
||||
<view v-if="msgInfo.type == $enums.MESSAGE_TYPE.TEXT">
|
||||
<long-press-menu :items="menuItems" @select="onSelectMenu">
|
||||
<rich-text v-if="$emo.containEmoji(msgInfo.content)" class="message-text"
|
||||
:nodes="nodesText"></rich-text>
|
||||
<u-parse v-else class="message-text" :showImgMenu="false" :html="nodesText"></u-parse>
|
||||
</long-press-menu>
|
||||
</view>
|
||||
<view class="message-image" v-if="msgInfo.type == $enums.MESSAGE_TYPE.IMAGE">
|
||||
<long-press-menu :items="menuItems" @select="onSelectMenu">
|
||||
<view class="image-box">
|
||||
<image class="send-image" mode="heightFix" :src="JSON.parse(msgInfo.content).thumbUrl"
|
||||
lazy-load="true" @click.stop="onShowFullImage()">
|
||||
</image>
|
||||
<loading v-if="loading"></loading>
|
||||
</view>
|
||||
</long-press-menu>
|
||||
<text title="发送失败" v-if="loadFail" @click="onSendFail"
|
||||
class="send-fail iconfont icon-warning-circle-fill"></text>
|
||||
</view>
|
||||
<view class="message-video" v-if="msgInfo.type == $enums.MESSAGE_TYPE.VIDEO">
|
||||
<long-press-menu :items="menuItems" @select="onSelectMenu">
|
||||
<view class="video-box">
|
||||
<myVideo :videoUrl="videoUrl" />
|
||||
</view>
|
||||
</long-press-menu>
|
||||
|
||||
<loading v-if="loading"></loading>
|
||||
<text title="发送失败" v-if="loadFail" @click="onSendFail"
|
||||
class="send-fail iconfont icon-warning-circle-fill"></text>
|
||||
</view>
|
||||
|
||||
<!-- <view class="message-file" v-if="msgInfo.type == $enums.MESSAGE_TYPE.FILE">-->
|
||||
<!-- <long-press-menu :items="menuItems" @select="onSelectMenu">-->
|
||||
<!-- <view class="file-box">-->
|
||||
<!-- <view class="file-info">-->
|
||||
<!-- <uni-link class="file-name" :text="data.name" showUnderLine="true"-->
|
||||
<!-- color="#007BFF" :href="data.url"></uni-link>-->
|
||||
<!-- <view class="file-size">{{ fileSize }}</view>-->
|
||||
<!-- </view>-->
|
||||
<!-- <view class="file-icon iconfont icon-file"></view>-->
|
||||
<!-- <loading v-if="loading"></loading>-->
|
||||
<!-- </view>-->
|
||||
<!-- </long-press-menu>-->
|
||||
<!-- <text title="发送失败" v-if="loadFail" @click="onSendFail"-->
|
||||
<!-- class="send-fail iconfont icon-warning-circle-fill"></text>-->
|
||||
<!-- </view>-->
|
||||
<long-press-menu v-if="msgInfo.type == $enums.MESSAGE_TYPE.AUDIO" :items="menuItems"
|
||||
@select="onSelectMenu">
|
||||
<view class="message-audio message-text" @click="onPlayAudio()">
|
||||
<text class="iconfont icon-voice-play"></text>
|
||||
<text class="chat-audio-text">{{ JSON.parse(msgInfo.content).duration + '"' }}</text>
|
||||
<text v-if="audioPlayState == 'PAUSE'" class="iconfont icon-play"></text>
|
||||
<text v-if="audioPlayState == 'PLAYING'" class="iconfont icon-pause"></text>
|
||||
</view>
|
||||
</long-press-menu>
|
||||
<!-- <long-press-menu v-if="isAction" :items="menuItems" @select="onSelectMenu">-->
|
||||
<!-- <view class="chat-realtime message-text" @click="$emit('call')">-->
|
||||
<!-- <text v-if="msgInfo.type == $enums.MESSAGE_TYPE.ACT_RT_VOICE"-->
|
||||
<!-- class="iconfont icon-chat-voice"></text>-->
|
||||
<!-- <text v-if="msgInfo.type == $enums.MESSAGE_TYPE.ACT_RT_VIDEO"-->
|
||||
<!-- class="iconfont icon-chat-video"></text>-->
|
||||
<!-- <text>{{ msgInfo.content }}</text>-->
|
||||
<!-- </view>-->
|
||||
<!-- </long-press-menu>-->
|
||||
<view class="message-status" v-if="!isAction">
|
||||
<text class="chat-readed" v-if="msgInfo.selfSend && !msgInfo.groupId
|
||||
&& msgInfo.status == $enums.MESSAGE_STATUS.READED">已读</text>
|
||||
<text class="chat-unread" v-if="msgInfo.selfSend && !msgInfo.groupId
|
||||
&& msgInfo.status != $enums.MESSAGE_STATUS.READED">未读</text>
|
||||
</view>
|
||||
<view class="chat-receipt" v-if="msgInfo.receipt && msgInfo.groupId" @click="onShowReadedBox">
|
||||
<text v-if="msgInfo.receiptOk" class="tool-icon iconfont icon-ok"></text>
|
||||
<text v-else>{{ msgInfo.readedCount }}人已读</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<chat-group-readed ref="chatGroupReaded" :groupMembers="groupMembers" :msgInfo="msgInfo"></chat-group-readed>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import myVideo from "@/components/myVideo.vue";
|
||||
export default {
|
||||
name: "chat-message-item",
|
||||
props: {
|
||||
headImage: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
showName: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
msgInfo: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
groupMembers: {
|
||||
type: Array
|
||||
}
|
||||
},
|
||||
components: {
|
||||
myVideo
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
audioPlayState: 'STOP',
|
||||
innerAudioContext: null,
|
||||
menu: {
|
||||
show: false,
|
||||
style: ""
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
loading() {
|
||||
|
||||
return this.msgInfo.loadStatus && this.msgInfo.loadStatus === "loading";
|
||||
},
|
||||
loadFail() {
|
||||
return this.msgInfo.loadStatus && this.msgInfo.loadStatus === "fail";
|
||||
},
|
||||
data() {
|
||||
return JSON.parse(this.msgInfo.content)
|
||||
},
|
||||
videoUrl() {
|
||||
try {
|
||||
const content = JSON.parse(this.msgInfo.content);
|
||||
return content.url || content.originUrl || '';
|
||||
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
},
|
||||
videoPoster() {
|
||||
try {
|
||||
const content = JSON.parse(this.msgInfo.content);
|
||||
return content.thumbUrl || content.poster || '';
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
},
|
||||
fileSize() {
|
||||
let size = this.data.size;
|
||||
if (size > 1024 * 1024) {
|
||||
return Math.round(size / 1024 / 1024) + "M";
|
||||
}
|
||||
if (size > 1024) {
|
||||
return Math.round(size / 1024) + "KB";
|
||||
}
|
||||
return size + "B";
|
||||
},
|
||||
menuItems() {
|
||||
let items = [];
|
||||
if (this.msgInfo.type == this.$enums.MESSAGE_TYPE.TEXT) {
|
||||
items.push({
|
||||
key: 'COPY',
|
||||
name: '复制',
|
||||
icon: 'bars'
|
||||
});
|
||||
}
|
||||
// if (this.msgInfo.selfSend && this.msgInfo.id > 0) {
|
||||
// items.push({
|
||||
// key: 'RECALL',
|
||||
// name: '撤回',
|
||||
// icon: 'refreshempty'
|
||||
// });
|
||||
// }
|
||||
items.push({
|
||||
key: 'DELETE',
|
||||
name: '删除',
|
||||
icon: 'trash',
|
||||
color: '#e64e4e'
|
||||
});
|
||||
// if (this.msgInfo.type == this.$enums.MESSAGE_TYPE.FILE) {
|
||||
// items.push({
|
||||
// key: 'DOWNLOAD',
|
||||
// name: '下载并打开',
|
||||
// icon: 'download'
|
||||
// });
|
||||
// }
|
||||
return items;
|
||||
},
|
||||
isAction() {
|
||||
return this.$msgType.isAction(this.msgInfo.type);
|
||||
},
|
||||
isNormal() {
|
||||
const type = this.msgInfo.type;
|
||||
return this.$msgType.isNormal(type) || this.$msgType.isAction(type)
|
||||
},
|
||||
nodesText() {
|
||||
let color = this.msgInfo.selfSend ? 'white' : '';
|
||||
let text = this.$url.replaceURLWithHTMLLinks(this.msgInfo.content, color)
|
||||
return this.$emo.transform(text, 'emoji-normal')
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onSendFail() {
|
||||
uni.showToast({
|
||||
title: "该文件已发送失败,目前不支持自动重新发送,建议手动重新发送",
|
||||
icon: "none"
|
||||
})
|
||||
},
|
||||
onPlayAudio() {
|
||||
if (!this.innerAudioContext) {
|
||||
this.innerAudioContext = uni.createInnerAudioContext();
|
||||
let url = JSON.parse(this.msgInfo.content).url;
|
||||
this.innerAudioContext.src = url;
|
||||
this.innerAudioContext.onEnded((e) => {
|
||||
console.log('停止')
|
||||
this.audioPlayState = "STOP"
|
||||
this.emit();
|
||||
})
|
||||
this.innerAudioContext.onError((e) => {
|
||||
this.audioPlayState = "STOP"
|
||||
console.log("播放音频出错");
|
||||
console.log(e)
|
||||
this.emit();
|
||||
});
|
||||
}
|
||||
if (this.audioPlayState == 'STOP') {
|
||||
this.innerAudioContext.play();
|
||||
this.audioPlayState = "PLAYING";
|
||||
} else if (this.audioPlayState == 'PLAYING') {
|
||||
this.innerAudioContext.pause();
|
||||
this.audioPlayState = "PAUSE"
|
||||
} else if (this.audioPlayState == 'PAUSE') {
|
||||
this.innerAudioContext.play();
|
||||
this.audioPlayState = "PLAYING"
|
||||
}
|
||||
this.emit();
|
||||
},
|
||||
onSelectMenu(item) {
|
||||
this.$emit(item.key.toLowerCase(), this.msgInfo);
|
||||
this.menu.show = false;
|
||||
},
|
||||
onShowFullImage() {
|
||||
let imageUrl = JSON.parse(this.msgInfo.content).originUrl;
|
||||
uni.previewImage({
|
||||
urls: [imageUrl]
|
||||
})
|
||||
},
|
||||
async onShowReadedBox() {
|
||||
await this.$refs.chatGroupReaded.loadReadedUser();
|
||||
this.$refs.chatGroupReaded.open();
|
||||
},
|
||||
emit() {
|
||||
this.$emit("audioStateChange", this.audioPlayState, this.msgInfo);
|
||||
},
|
||||
stopPlayAudio() {
|
||||
if (this.innerAudioContext) {
|
||||
this.innerAudioContext.stop();
|
||||
this.innerAudioContext = null;
|
||||
this.audioPlayState = "STOP"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.chat-message-item {
|
||||
padding: 2rpx 20rpx;
|
||||
|
||||
.message-tip {
|
||||
line-height: 60rpx;
|
||||
text-align: center;
|
||||
color: $im-text-color-lighter;
|
||||
font-size: $im-font-size-smaller-extra;
|
||||
padding: 10rpx;
|
||||
}
|
||||
|
||||
.message-normal {
|
||||
position: relative;
|
||||
margin-bottom: 22rpx;
|
||||
padding-left: 110rpx;
|
||||
min-height: 80rpx;
|
||||
|
||||
.avatar {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.content {
|
||||
text-align: left;
|
||||
|
||||
.top {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
color: $im-text-color-lighter;
|
||||
font-size: $im-font-size-smaller;
|
||||
line-height: $im-font-size-smaller;
|
||||
height: $im-font-size-smaller;
|
||||
}
|
||||
|
||||
.bottom {
|
||||
display: inline-block;
|
||||
padding-right: 80rpx;
|
||||
margin-top: 5rpx;
|
||||
|
||||
.message-text {
|
||||
position: relative;
|
||||
line-height: 1.6;
|
||||
margin-top: 10rpx;
|
||||
padding: 16rpx 24rpx;
|
||||
background-color: $im-bg;
|
||||
border-radius: 20rpx;
|
||||
color: $im-text-color;
|
||||
font-size: $im-font-size;
|
||||
text-align: left;
|
||||
display: block;
|
||||
word-break: break-all;
|
||||
white-space: pre-line;
|
||||
|
||||
&:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: -20rpx;
|
||||
top: 26rpx;
|
||||
width: 6rpx;
|
||||
height: 6rpx;
|
||||
border-style: solid dashed dashed;
|
||||
border-color: $im-bg transparent transparent;
|
||||
overflow: hidden;
|
||||
border-width: 18rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.message-image {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
.image-box {
|
||||
position: relative;
|
||||
|
||||
.send-image {
|
||||
min-width: 200rpx;
|
||||
max-width: 420rpx;
|
||||
height: 350rpx;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.send-fail {
|
||||
color: $im-color-danger;
|
||||
font-size: $im-font-size;
|
||||
cursor: pointer;
|
||||
margin: 0 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.message-video {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
.video-box {
|
||||
position: relative;
|
||||
max-width: 540rpx;
|
||||
height: 300rpx;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.send-fail {
|
||||
color: $im-color-danger;
|
||||
font-size: $im-font-size;
|
||||
cursor: pointer;
|
||||
margin: 0 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.message-file {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
|
||||
.file-box {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
align-items: center;
|
||||
min-height: 60px;
|
||||
border-radius: 4px;
|
||||
padding: 10px 15px;
|
||||
box-shadow: $im-box-shadow-dark;
|
||||
|
||||
.file-info {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
text-align: left;
|
||||
font-size: 14px;
|
||||
width: 300rpx;
|
||||
|
||||
.file-name {
|
||||
font-weight: 600;
|
||||
margin-bottom: 15px;
|
||||
word-break: break-all;
|
||||
}
|
||||
}
|
||||
|
||||
.file-icon {
|
||||
font-size: 80rpx;
|
||||
color: #d42e07;
|
||||
}
|
||||
}
|
||||
|
||||
.send-fail {
|
||||
color: #e60c0c;
|
||||
font-size: 50rpx;
|
||||
cursor: pointer;
|
||||
margin: 0 20rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.message-audio {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.chat-audio-text {
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
.icon-voice-play {
|
||||
font-size: 18px;
|
||||
padding-right: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-realtime {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.iconfont {
|
||||
font-size: 20px;
|
||||
padding-right: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.message-status {
|
||||
line-height: $im-font-size-smaller-extra;
|
||||
font-size: $im-font-size-smaller-extra;
|
||||
padding-top: 2rpx;
|
||||
|
||||
.chat-readed {
|
||||
display: block;
|
||||
padding-top: 2rpx;
|
||||
color: $im-text-color-lighter;
|
||||
}
|
||||
|
||||
.chat-unread {
|
||||
color: $im-color-danger;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-receipt {
|
||||
font-size: $im-font-size-smaller;
|
||||
color: $im-text-color-lighter;
|
||||
font-weight: 600;
|
||||
|
||||
.icon-ok {
|
||||
font-size: 20px;
|
||||
color: $im-color-success;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.message-mine {
|
||||
text-align: right;
|
||||
padding-left: 0;
|
||||
padding-right: 110rpx;
|
||||
|
||||
.avatar {
|
||||
left: auto;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.content {
|
||||
text-align: right;
|
||||
|
||||
.bottom {
|
||||
padding-left: 80rpx;
|
||||
padding-right: 0;
|
||||
|
||||
.message-text {
|
||||
margin-left: 10px;
|
||||
background-color: $im-color-primary-light-2;
|
||||
color: #fff;
|
||||
|
||||
&:after {
|
||||
left: auto;
|
||||
right: -9px;
|
||||
border-top-color: $im-color-primary-light-2;
|
||||
}
|
||||
}
|
||||
|
||||
.message-image {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
.message-file {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
.message-audio {
|
||||
flex-direction: row-reverse;
|
||||
|
||||
.chat-audio-text {
|
||||
padding-right: 0;
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
.icon-voice-play {
|
||||
transform: rotateY(180deg);
|
||||
}
|
||||
}
|
||||
|
||||
.chat-realtime {
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
|
||||
.iconfont {
|
||||
transform: rotateY(180deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,221 @@
|
||||
<template>
|
||||
<view class="chat-record">
|
||||
<view class="chat-record-bar" :class="{ recording: recording }" id="chat-record-bar" @click.stop=""
|
||||
@touchstart.prevent="onStartRecord" @touchmove.prevent="onTouchMove" @touchend.prevent="onEndRecord">
|
||||
{{ recording ? '正在录音' : '长按 说话' }}</view>
|
||||
<view v-if="recording" class="chat-record-window" :style="recordWindowStyle">
|
||||
<view class="rc-wave">
|
||||
<text class="note" v-for="i in 7" :key="i" :style="{ animationDelay: (0.1 * (i - 1)) + 's' }"></text>
|
||||
</view>
|
||||
<view class="rc-tip">{{ recordTip }}</view>
|
||||
<view class="cancel-btn" @click="onCancel">
|
||||
<uni-icons :class="moveToCancel ? 'red' : 'black'" type="clear" :size="moveToCancel ? 45 : 40"></uni-icons>
|
||||
</view>
|
||||
<view class="opt-tip" :class="moveToCancel ? 'red' : 'black'">{{ moveToCancel ? '松手取消' : '松手发送,上划取消' }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "chat-record",
|
||||
data() {
|
||||
return {
|
||||
recording: false,
|
||||
moveToCancel: false,
|
||||
recordBarTop: 0,
|
||||
duration: 0,
|
||||
rcTimer: null
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onTouchMove(e) {
|
||||
const moveY = e.touches[0].clientY;
|
||||
this.moveToCancel = moveY < this.recordBarTop - 40;
|
||||
},
|
||||
onCancel() {
|
||||
if (this.recording) {
|
||||
this.moveToCancel = true;
|
||||
this.onEndRecord();
|
||||
}
|
||||
},
|
||||
onStartRecord() {
|
||||
if (this.recording) {
|
||||
return;
|
||||
}
|
||||
console.log("开始录音")
|
||||
this.moveToCancel = false;
|
||||
this.initRecordBar();
|
||||
if (!this.$rc.checkIsEnable()) {
|
||||
return;
|
||||
}
|
||||
this.$rc.start().then(() => {
|
||||
this.recording = true;
|
||||
console.log("开始录音成功")
|
||||
this.startTimer();
|
||||
}).catch((e) => {
|
||||
console.log("录音失败" + JSON.stringify(e))
|
||||
uni.showToast({
|
||||
title: "录音失败",
|
||||
icon: "none"
|
||||
});
|
||||
});
|
||||
},
|
||||
onEndRecord() {
|
||||
if (!this.recording) {
|
||||
return;
|
||||
}
|
||||
this.recording = false;
|
||||
this.stopTimer();
|
||||
this.$rc.close();
|
||||
if (this.moveToCancel) {
|
||||
console.log("录音取消")
|
||||
return;
|
||||
}
|
||||
if (this.duration <= 1) {
|
||||
uni.showToast({
|
||||
title: "说话时间太短",
|
||||
icon: 'none'
|
||||
})
|
||||
return;
|
||||
}
|
||||
this.$rc.upload().then((data) => {
|
||||
this.$emit("send", data);
|
||||
}).catch((e) => {
|
||||
uni.showToast({
|
||||
title: e,
|
||||
icon: 'none'
|
||||
})
|
||||
})
|
||||
},
|
||||
startTimer() {
|
||||
this.duration = 0;
|
||||
this.stopTimer();
|
||||
this.rcTimer = setInterval(() => {
|
||||
this.duration++;
|
||||
if (this.duration >= 60) {
|
||||
this.onEndRecord();
|
||||
}
|
||||
}, 1000)
|
||||
},
|
||||
stopTimer() {
|
||||
this.rcTimer && clearInterval(this.rcTimer);
|
||||
this.rcTimer = null;
|
||||
},
|
||||
initRecordBar() {
|
||||
const query = uni.createSelectorQuery().in(this);
|
||||
query.select('#chat-record-bar').boundingClientRect((rect) => {
|
||||
this.recordBarTop = rect.top;
|
||||
}).exec()
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
recordWindowStyle() {
|
||||
const windowHeight = uni.getSystemInfoSync().windowHeight;
|
||||
const bottom = windowHeight - this.recordBarTop + 12;
|
||||
return `bottom:${bottom}px;`
|
||||
},
|
||||
recordTip() {
|
||||
if (this.duration > 50) {
|
||||
return `${60 - this.duration}s后将停止录音`;
|
||||
}
|
||||
return `录音时长:${this.duration}s`;
|
||||
}
|
||||
},
|
||||
// Vue 2 生命周期钩子
|
||||
beforeDestroy() {
|
||||
this.stopTimer();
|
||||
this.recording = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.chat-record {
|
||||
.rc-wave {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
height: 80rpx;
|
||||
|
||||
.note {
|
||||
background: linear-gradient(to top, $im-color-primary-light-1 0%, $im-color-primary-light-6 100%);
|
||||
width: 4px;
|
||||
height: 50%;
|
||||
border-radius: 5rpx;
|
||||
margin-right: 4px;
|
||||
animation: loading 0.5s infinite linear;
|
||||
|
||||
@keyframes loading {
|
||||
0% {
|
||||
background-image: linear-gradient(to right, $im-color-primary-light-1 0%, $im-color-primary-light-6 100%);
|
||||
height: 20%;
|
||||
border-radius: 5rpx;
|
||||
}
|
||||
50% {
|
||||
background-image: linear-gradient(to top, $im-color-primary-light-1 0%, $im-color-primary-light-6 100%);
|
||||
height: 80%;
|
||||
border-radius: 5rpx;
|
||||
}
|
||||
100% {
|
||||
background-image: linear-gradient(to top, $im-color-primary-light-1 0%, $im-color-primary-light-6 100%);
|
||||
height: 20%;
|
||||
border-radius: 5rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chat-record-bar {
|
||||
padding: 10rpx;
|
||||
margin: 10rpx;
|
||||
border-radius: 10rpx;
|
||||
text-align: center;
|
||||
box-shadow: $im-box-shadow;
|
||||
|
||||
&.recording {
|
||||
background-color: $im-color-primary;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-record-window {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 360rpx;
|
||||
background-color: rgba(255, 255, 255, 0.95);
|
||||
padding: 30rpx;
|
||||
|
||||
.icon-microphone {
|
||||
text-align: center;
|
||||
font-size: 80rpx;
|
||||
padding: 10rpx;
|
||||
}
|
||||
|
||||
.rc-tip {
|
||||
text-align: center;
|
||||
font-size: $im-font-size-small;
|
||||
color: $im-text-color-light;
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
|
||||
.cancel-btn {
|
||||
text-align: center;
|
||||
margin-top: 40rpx;
|
||||
height: 80rpx;
|
||||
}
|
||||
|
||||
.opt-tip {
|
||||
text-align: center;
|
||||
font-size: 30rpx;
|
||||
padding: 20rpx;
|
||||
}
|
||||
|
||||
.red {
|
||||
color: $im-color-danger !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,180 @@
|
||||
<template>
|
||||
<uni-popup ref="popup" type="bottom">
|
||||
<view class="group-member-selector">
|
||||
<view class="top-bar">
|
||||
<view class="top-tip">选择成员</view>
|
||||
<button class="top-btn" type="warn" size="mini" @click="onClean()">清空 </button>
|
||||
<button class="top-btn" type="primary" size="mini" @click="onOk()">确定({{ checkedIds.length }})
|
||||
</button>
|
||||
</view>
|
||||
<scroll-view v-show="checkedIds.length > 0" scroll-x="true" scroll-left="120">
|
||||
<view class="checked-users">
|
||||
<view v-for="m in checkedMembers" class="user-item" :key="m.userId">
|
||||
<head-image :name="m.showNickName" :url="m.headImage" :size="60"></head-image>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
<view class="search-bar">
|
||||
<uni-search-bar v-model="searchText" cancelButton="none" placeholder="搜索"></uni-search-bar>
|
||||
</view>
|
||||
<view class="member-items">
|
||||
<virtual-scroller :items="showMembers">
|
||||
<template v-slot="{ item }">
|
||||
<view class="member-item" @click="onSwitchChecked(item)">
|
||||
<head-image :name="item.showNickName" :online="item.online" :url="item.headImage"
|
||||
:size="90"></head-image>
|
||||
<view class="member-name">{{ item.showNickName }}
|
||||
</view>
|
||||
<view class="member-checked">
|
||||
<radio :checked="item.checked" :disabled="item.locked"
|
||||
@click.stop="onSwitchChecked(item)" />
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
</virtual-scroller>
|
||||
</view>
|
||||
</view>
|
||||
</uni-popup>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "group-member-selector",
|
||||
props: {
|
||||
group: {
|
||||
type: Object
|
||||
},
|
||||
members: {
|
||||
type: Array
|
||||
},
|
||||
maxSize: {
|
||||
type: Number,
|
||||
default: 50
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
searchText: "",
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
init(checkedIds, lockedIds, hideIds) {
|
||||
this.members.forEach((m) => {
|
||||
m.checked = checkedIds.indexOf(m.userId) >= 0;
|
||||
m.locked = lockedIds.indexOf(m.userId) >= 0;
|
||||
m.hide = hideIds.indexOf(m.userId) >= 0;
|
||||
});
|
||||
},
|
||||
open() {
|
||||
this.$refs.popup.open();
|
||||
},
|
||||
onSwitchChecked(m) {
|
||||
if (!m.locked) {
|
||||
m.checked = !m.checked;
|
||||
}
|
||||
// 达到选择上限
|
||||
if (this.maxSize > 0 && this.checkedIds.length > this.maxSize) {
|
||||
m.checked = false;
|
||||
uni.showToast({
|
||||
title: `最多选择${this.maxSize}位用户`,
|
||||
icon: "none"
|
||||
})
|
||||
}
|
||||
},
|
||||
onClean() {
|
||||
this.members.forEach((m) => {
|
||||
if (!m.locked && m.checked) {
|
||||
m.checked = false;
|
||||
}
|
||||
})
|
||||
},
|
||||
onOk() {
|
||||
this.$refs.popup.close();
|
||||
this.$emit("complete", this.checkedIds)
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
checkedIds() {
|
||||
return this.checkedMembers.map(m => m.userId)
|
||||
},
|
||||
checkedMembers() {
|
||||
return this.members.filter((m) => !m.quit && !m.hide && m.checked);
|
||||
},
|
||||
showMembers() {
|
||||
return this.members.filter(m => !m.quit && !m.hide && m.showNickName.includes(this.searchText))
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.group-member-selector {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: white;
|
||||
padding: 10rpx;
|
||||
border-radius: 15rpx 15rpx 0 0;
|
||||
overflow: hidden;
|
||||
|
||||
.top-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 70rpx;
|
||||
padding: 10rpx 30rpx;
|
||||
|
||||
.top-tip {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.top-btn {
|
||||
margin-left: 10rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.checked-users {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 90rpx;
|
||||
padding: 0 30rpx;
|
||||
|
||||
.user-item {
|
||||
padding: 3rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.member-items {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
|
||||
.member-item {
|
||||
height: 120rpx;
|
||||
display: flex;
|
||||
position: relative;
|
||||
padding: 0 30rpx;
|
||||
align-items: center;
|
||||
background-color: white;
|
||||
white-space: nowrap;
|
||||
|
||||
.member-name {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
padding-left: 20rpx;
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
line-height: 60rpx;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
|
||||
.uni-tag {
|
||||
margin-left: 5rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,88 @@
|
||||
<template>
|
||||
<uni-popup ref="popup" type="center">
|
||||
<uni-popup-dialog mode="base" :duration="2000" title="是否加入通话?" confirmText="加入" @confirm="onOk">
|
||||
<div class="group-rtc-join">
|
||||
<div class="host-info">
|
||||
<div>发起人</div>
|
||||
<head-image :name="rtcInfo.host.nickName" :url="rtcInfo.host.headImage" :size="80"></head-image>
|
||||
</div>
|
||||
<div class="user-info">
|
||||
<div>{{ rtcInfo.userInfos.length + '人正在通话中' }}</div>
|
||||
<scroll-view scroll-x="true" scroll-left="120">
|
||||
<view class="user-list">
|
||||
<view v-for="user in rtcInfo.userInfos" class="user-item" :key="user.id">
|
||||
<head-image :name="user.nickName" :url="user.headImage" :size="80"></head-image>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</div>
|
||||
</div>
|
||||
</uni-popup-dialog>
|
||||
</uni-popup>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
rtcInfo: {}
|
||||
}
|
||||
},
|
||||
props: {
|
||||
groupId: {
|
||||
type: Number
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
open(rtcInfo) {
|
||||
this.rtcInfo = rtcInfo;
|
||||
this.$refs.popup.open();
|
||||
},
|
||||
onOk() {
|
||||
let users = this.rtcInfo.userInfos;
|
||||
let mine = this.userStore.userInfo;
|
||||
// 加入自己的信息
|
||||
if (!users.find((user) => user.id == mine.id)) {
|
||||
users.push({
|
||||
id: mine.id,
|
||||
nickName: mine.nickName,
|
||||
headImage: mine.headImageThumb,
|
||||
isCamera: false,
|
||||
isMicroPhone: true
|
||||
})
|
||||
}
|
||||
const userInfos = encodeURIComponent(JSON.stringify(users));
|
||||
uni.navigateTo({
|
||||
url: `/pages/chat/chat-group-video?groupId=${this.groupId}&isHost=false
|
||||
&inviterId=${mine.id}&userInfos=${userInfos}`
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.group-rtc-join {
|
||||
width: 100%;
|
||||
|
||||
.host-info {
|
||||
font-size: 16px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
font-size: 16px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.user-list {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 90rpx;
|
||||
|
||||
.user-item {
|
||||
padding: 3rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,124 @@
|
||||
<template>
|
||||
<view class="head-image" @click="showUserInfo($event)" :title="name">
|
||||
<image class="avatar-image" v-if="url" :src="url" :style="avatarImageStyle" lazy-load="true"
|
||||
mode="aspectFill" />
|
||||
<view class="avatar-text" v-if="!url" :style="avatarTextStyle">
|
||||
{{ (name ? name.substring(0, 1) : '').toUpperCase() }}
|
||||
</view>
|
||||
<view v-if="online" class="online" title="用户当前在线">
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "head-image",
|
||||
data() {
|
||||
return {
|
||||
colors: ["#5daa31", "#c7515a", "#e03697", "#85029b",
|
||||
"#c9b455", "#326eb6"]
|
||||
}
|
||||
},
|
||||
props: {
|
||||
id: {
|
||||
type: String
|
||||
},
|
||||
size: {
|
||||
type: [Number, String],
|
||||
default: 'default'
|
||||
},
|
||||
url: {
|
||||
type: String
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
online: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
showUserInfo(e) {
|
||||
if (this.id && this.id > 0) {
|
||||
uni.navigateTo({
|
||||
url: "/pages/common/user-info?id=" + this.id
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
_size() {
|
||||
if (typeof this.size === 'number') {
|
||||
return this.size;
|
||||
} else if (typeof this.size === 'string') {
|
||||
const sizeMap = {
|
||||
'default': 96,
|
||||
'small': 84,
|
||||
'smaller': 72,
|
||||
'mini': 60,
|
||||
'minier': 48,
|
||||
'lage': 108,
|
||||
'lager': 120,
|
||||
};
|
||||
return sizeMap[this.size] || 96;
|
||||
}
|
||||
return 96;
|
||||
},
|
||||
avatarImageStyle() {
|
||||
return `width:${this._size}rpx;
|
||||
height:${this._size}rpx;`
|
||||
},
|
||||
avatarTextStyle() {
|
||||
return `width: ${this._size}rpx;
|
||||
height:${this._size}rpx;
|
||||
background-color:${this.name ? this.textColor : '#fff'};
|
||||
font-size:${this._size * 0.5}rpx;
|
||||
`
|
||||
},
|
||||
textColor() {
|
||||
let hash = 0;
|
||||
if (this.name) {
|
||||
for (var i = 0; i < this.name.length; i++) {
|
||||
hash += this.name.charCodeAt(i);
|
||||
}
|
||||
}
|
||||
return this.colors[hash % this.colors.length];
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.head-image {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
|
||||
.avatar-image {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border-radius: 50%;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
|
||||
.avatar-text {
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.online {
|
||||
position: absolute;
|
||||
right: -10%;
|
||||
bottom: 0;
|
||||
width: 24rpx;
|
||||
height: 24rpx;
|
||||
background: limegreen;
|
||||
border-radius: 50%;
|
||||
border: 6rpx solid white;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,62 @@
|
||||
<template>
|
||||
<view class="loading-box" :style="loadingStyle">
|
||||
<view class="rotate iconfont icon-loading" :style="icontStyle"></view>
|
||||
<slot></slot>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
props: {
|
||||
size: {
|
||||
type: Number,
|
||||
default: 100
|
||||
},
|
||||
mask: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
icontStyle() {
|
||||
return `font-size:${this.size}rpx`;
|
||||
},
|
||||
loadingStyle() {
|
||||
return this.mask ? "background: rgba(0, 0, 0, 0.3);" : "";
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.loading-box {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
z-index: 10000;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.rotate {
|
||||
animation: rotate 2s ease-in-out infinite;
|
||||
|
||||
}
|
||||
|
||||
@keyframes rotate {
|
||||
from {
|
||||
transform: rotate(0deg)
|
||||
}
|
||||
|
||||
to {
|
||||
transform: rotate(360deg)
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,60 @@
|
||||
// 颜色
|
||||
$im-color-primary: #00a89b;
|
||||
$im-color-primary-light-1: mix(#fff, $im-color-primary, 10%);
|
||||
$im-color-primary-light-2: mix(#fff, $im-color-primary, 20%);
|
||||
$im-color-primary-light-3: mix(#fff, $im-color-primary, 30%);
|
||||
$im-color-primary-light-4: mix(#fff, $im-color-primary, 40%);
|
||||
$im-color-primary-light-5: mix(#fff, $im-color-primary, 50%);
|
||||
$im-color-primary-light-6: mix(#fff, $im-color-primary, 60%);
|
||||
$im-color-primary-light-7: mix(#fff, $im-color-primary, 70%);
|
||||
$im-color-primary-light-8: mix(#fff, $im-color-primary, 80%);
|
||||
$im-color-primary-light-9: mix(#fff, $im-color-primary, 90%);
|
||||
$im-color-primary-dark-1: mix(#000, $im-color-primary, 10%);
|
||||
$im-color-primary-dark-2: mix(#000, $im-color-primary, 20%);
|
||||
$im-color-primary-dark-3: mix(#000, $im-color-primary, 30%);
|
||||
$im-color-primary-dark-4: mix(#000, $im-color-primary, 40%);
|
||||
|
||||
$im-color-success: #18bc37;
|
||||
$im-color-warning: #f3a73f;
|
||||
$im-color-danger: #e43d33;
|
||||
$im-color-info: #8f939c;
|
||||
|
||||
// 文字颜色
|
||||
$im-text-color: #000000;
|
||||
$im-text-color-light: #6a6a6a;
|
||||
$im-text-color-lighter: #909399;
|
||||
$im-text-color-lighter-extra: #c7c7c7;
|
||||
|
||||
// 边框颜色
|
||||
$im-border: #F0F0F0;
|
||||
$im-border-light: #EDEDED;
|
||||
$im-border-lighter: #DCDCDC;
|
||||
$im-border-lighter-extra: #B9B9B9;
|
||||
|
||||
// 文字大小
|
||||
$im-font-size: 30rpx;
|
||||
$im-font-size-small: 28rpx;
|
||||
$im-font-size-smaller: 26rpx;
|
||||
$im-font-size-smaller-extra: 24rpx;
|
||||
$im-font-size-large: 32rpx;
|
||||
$im-font-size-larger: 34rpx;
|
||||
$im-font-size-larger-extra: 36rpx;
|
||||
|
||||
// 阴影
|
||||
$im-box-shadow: 0 2px 4px rgba(0, 0, 0, .12), 0 0 6px rgba(0, 0, 0, .04);
|
||||
$im-box-shadow-light: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||
$im-box-shadow-lighter: 0px 0px 6px rgba(0, 0, 0, .12);
|
||||
$im-box-shadow-dark: 0px 1px 10px 2px rgba($color: #a5a4a4, $alpha: 0.5);
|
||||
|
||||
// 背景
|
||||
$im-bg: #f7f7f7;
|
||||
$im-bg-active: #f1f1f1;
|
||||
|
||||
// 标题
|
||||
$im-title-size: 26px;
|
||||
$im-title-size-1: 22px;
|
||||
$im-title-size-2: 18px;
|
||||
|
||||
$font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, SimSun, sans-serif;
|
||||
|
||||
$im-nav-bar-height: 45px;
|
||||
@ -0,0 +1,217 @@
|
||||
/** 原生button样式 **/
|
||||
uni-button {
|
||||
font-size: $im-font-size !important;
|
||||
}
|
||||
uni-button[type='primary'] {
|
||||
color: #fff !important;
|
||||
background-color: $im-color-primary !important;
|
||||
}
|
||||
|
||||
uni-button[type='primary'][plain] {
|
||||
color: $im-color-primary !important;
|
||||
border: 1px solid $im-color-primary;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
uni-button[type='warn'] {
|
||||
color: #fff !important;
|
||||
background-color: $im-color-danger !important;
|
||||
}
|
||||
|
||||
uni-button[type='warn'][plain] {
|
||||
color: $im-color-danger !important;
|
||||
border: 1px solid $im-color-danger !important;
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
uni-button[size='mini'] {
|
||||
font-size: $im-font-size-smaller !important;
|
||||
}
|
||||
|
||||
// #ifdef MP-WEIXIN
|
||||
// wx小程序只有button,没有uni-botton
|
||||
button {
|
||||
font-size: $im-font-size !important;
|
||||
}
|
||||
|
||||
button[type='primary'] {
|
||||
color: #fff !important;
|
||||
background-color: $im-color-primary !important;
|
||||
}
|
||||
|
||||
button[type='primary'][plain] {
|
||||
color: $im-color-primary !important;
|
||||
border: 1px solid $im-color-primary;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
button[type='warn'] {
|
||||
color: #fff !important;
|
||||
background-color: $im-color-danger !important;
|
||||
}
|
||||
|
||||
button[type='warn'][plain] {
|
||||
color: $im-color-danger !important;
|
||||
border: 1px solid $im-color-danger !important;
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
button[size='mini'] {
|
||||
font-size: $im-font-size-smaller !important;
|
||||
}
|
||||
// #endif
|
||||
|
||||
.button-hover[type='primary'] {
|
||||
color: #fff !important;
|
||||
background-color: $im-color-primary-dark-1 !important;
|
||||
}
|
||||
|
||||
/** uni-ui input激活后边框、图标颜色 **/
|
||||
.uni-easyinput__content.is-focused:not(.is-input-error-border) {
|
||||
border-color: $im-color-primary-light-2 !important;
|
||||
|
||||
.content-clear-icon {
|
||||
color: $im-color-primary-light-2 !important;
|
||||
}
|
||||
}
|
||||
|
||||
/** 底部导航 **/
|
||||
.uni-tabbar-bottom .uni-tabbar {
|
||||
box-shadow: $im-box-shadow;
|
||||
}
|
||||
|
||||
.uni-tabbar-border {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.segmented-control {
|
||||
border-color: $im-color-primary !important;
|
||||
|
||||
.segmented-control__item--button {
|
||||
border-color: $im-color-primary !important;
|
||||
}
|
||||
.segmented-control__item--button--active {
|
||||
background-color: $im-color-primary !important;
|
||||
|
||||
.segmented-control__text{
|
||||
color: #fff !important;
|
||||
}
|
||||
}
|
||||
.segmented-control__text{
|
||||
color: $im-color-primary !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.uni-radio-input svg{
|
||||
border-color: white !important;
|
||||
background-color: $im-color-primary !important;
|
||||
}
|
||||
|
||||
.uni-radio-input svg {
|
||||
background-color: $im-color-primary !important;
|
||||
border-color: $im-color-primary !important;
|
||||
background-clip: content-box !important;
|
||||
box-sizing: border-box;
|
||||
border-radius: 50%;
|
||||
transform: translate(-50%, -50%) scale(0.7)!important;
|
||||
}
|
||||
|
||||
.uni-radio-input svg path{
|
||||
fill: $im-color-primary !important;
|
||||
}
|
||||
|
||||
.uni-radio-input {
|
||||
background-color: white !important;
|
||||
border-color: $im-color-primary !important;
|
||||
}
|
||||
|
||||
.uni-radio-input-disabled {
|
||||
background-color: rgb(225, 225, 225) !important;
|
||||
border-color: rgb(209, 209, 209) !important;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.uni-section__content-title {
|
||||
font-size: $im-font-size !important;
|
||||
color: $im-text-color-light;
|
||||
}
|
||||
|
||||
.uni-forms-item__label {
|
||||
color: $im-text-color;
|
||||
font-size: $im-font-size !important;
|
||||
}
|
||||
.uni-forms-item {
|
||||
//margin-bottom: 8px !important;
|
||||
}
|
||||
.uni-easyinput__content-input {
|
||||
font-size: $im-font-size !important;
|
||||
}
|
||||
.uni-easyinput__placeholder-class {
|
||||
color: $im-text-color-lighter;
|
||||
font-size: $im-font-size !important;;
|
||||
}
|
||||
.uni-easyinput__content-textarea {
|
||||
font-size: $im-font-size !important;;
|
||||
}
|
||||
.uni-input-input:disabled {
|
||||
color: $im-text-color-light;
|
||||
}
|
||||
.uni-forms-item.is-direction-top .uni-forms-item__label {
|
||||
padding: 0 !important;
|
||||
}
|
||||
.uni-data-checklist .checklist-group .checklist-box .checklist-content .checklist-text {
|
||||
font-size: $im-font-size !important;
|
||||
}
|
||||
.uni-card .uni-card__content {
|
||||
color: unset !important;
|
||||
padding: 10px 0 !important;
|
||||
}
|
||||
.uni-tag-text--small{
|
||||
font-size: 10px !important;
|
||||
font-weight: bolder !important;
|
||||
}
|
||||
|
||||
.nav-bar {
|
||||
height: 100rpx;
|
||||
padding: 0 20rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: white;
|
||||
border-bottom: 1px solid $im-border;
|
||||
|
||||
.nav-search {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.nav-add {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.bottom-btn {
|
||||
margin: 40rpx 40rpx;
|
||||
|
||||
uni-button + uni-button {
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.emoji-large {
|
||||
width: 64rpx !important;
|
||||
height: 64rpx !important;
|
||||
vertical-align: bottom !important;
|
||||
}
|
||||
|
||||
.emoji-normal {
|
||||
width: 54rpx !important;
|
||||
height: 54rpx !important;
|
||||
vertical-align: bottom !important;
|
||||
}
|
||||
|
||||
.emoji-small {
|
||||
width: 36rpx !important;
|
||||
height: 36rpx !important;
|
||||
vertical-align: bottom !important;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,177 @@
|
||||
@font-face {
|
||||
font-family: "iconfont"; /* Project id 4272106 */
|
||||
src: url('./static/icon/iconfont.ttf?t=1746119818070') format('truetype');
|
||||
}
|
||||
|
||||
.iconfont {
|
||||
font-family: "iconfont" !important;
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.icon-remove:before {
|
||||
content: "\e603";
|
||||
}
|
||||
|
||||
.icon-doc:before {
|
||||
content: "\e61c";
|
||||
}
|
||||
|
||||
.icon-image:before {
|
||||
content: "\e7f7";
|
||||
}
|
||||
|
||||
.icon-top-message:before {
|
||||
content: "\e6ff";
|
||||
}
|
||||
|
||||
.icon-setting:before {
|
||||
content: "\e851";
|
||||
}
|
||||
|
||||
.icon-phone:before {
|
||||
content: "\e692";
|
||||
}
|
||||
|
||||
.icon-email:before {
|
||||
content: "\e611";
|
||||
}
|
||||
|
||||
.icon-username:before {
|
||||
content: "\e60f";
|
||||
}
|
||||
|
||||
.icon-chat-muted:before {
|
||||
content: "\e634";
|
||||
}
|
||||
|
||||
.icon-chat-unmuted:before {
|
||||
content: "\ec44";
|
||||
}
|
||||
|
||||
.icon-privacy-protocol:before {
|
||||
content: "\e70a";
|
||||
}
|
||||
|
||||
.icon-un-register:before {
|
||||
content: "\e656";
|
||||
}
|
||||
|
||||
.icon-modify-pwd:before {
|
||||
content: "\e63c";
|
||||
}
|
||||
|
||||
.icon-user-protocol:before {
|
||||
content: "\e61a";
|
||||
}
|
||||
|
||||
.icon-film:before {
|
||||
content: "\e66b";
|
||||
}
|
||||
|
||||
.icon-chat:before {
|
||||
content: "\e624";
|
||||
}
|
||||
|
||||
.icon-delete:before {
|
||||
content: "\e605";
|
||||
}
|
||||
|
||||
.icon-receipt:before {
|
||||
content: "\e601";
|
||||
}
|
||||
|
||||
.icon-pause:before {
|
||||
content: "\e669";
|
||||
}
|
||||
|
||||
.icon-play:before {
|
||||
content: "\e620";
|
||||
}
|
||||
|
||||
.icon-voice-play:before {
|
||||
content: "\e675";
|
||||
}
|
||||
|
||||
.icon-chat-video:before {
|
||||
content: "\e73b";
|
||||
}
|
||||
|
||||
.icon-chat-voice:before {
|
||||
content: "\e633";
|
||||
}
|
||||
|
||||
.icon-video:before {
|
||||
content: "\e685";
|
||||
}
|
||||
|
||||
.icon-ok:before {
|
||||
content: "\e65a";
|
||||
}
|
||||
|
||||
.icon-at:before {
|
||||
content: "\e7de";
|
||||
}
|
||||
|
||||
.icon-man:before {
|
||||
content: "\e615";
|
||||
}
|
||||
|
||||
.icon-girl:before {
|
||||
content: "\e602";
|
||||
}
|
||||
|
||||
.icon-file:before {
|
||||
content: "\e671";
|
||||
}
|
||||
|
||||
.icon-add:before {
|
||||
content: "\e66c";
|
||||
}
|
||||
|
||||
.icon-warning-circle-fill:before {
|
||||
content: "\e848";
|
||||
}
|
||||
|
||||
.icon-loading:before {
|
||||
content: "\e93d";
|
||||
}
|
||||
|
||||
.icon-camera:before {
|
||||
content: "\e600";
|
||||
}
|
||||
|
||||
.icon-folder:before {
|
||||
content: "\e6a0";
|
||||
}
|
||||
|
||||
.icon-microphone:before {
|
||||
content: "\e63b";
|
||||
}
|
||||
|
||||
.icon-icon_emoji:before {
|
||||
content: "\e619";
|
||||
}
|
||||
|
||||
.icon-call:before {
|
||||
content: "\e610";
|
||||
}
|
||||
|
||||
.icon-keyboard:before {
|
||||
content: "\e679";
|
||||
}
|
||||
|
||||
.icon-voice-circle:before {
|
||||
content: "\e67f";
|
||||
}
|
||||
|
||||
.icon-picture:before {
|
||||
content: "\e653";
|
||||
}
|
||||
|
||||
.icon-search:before {
|
||||
content: "\e648";
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 6.3 KiB |
@ -0,0 +1,81 @@
|
||||
// store/groupStore.js
|
||||
const groupModule = {
|
||||
namespaced: true,
|
||||
|
||||
state: () => ({
|
||||
groups: []
|
||||
}),
|
||||
|
||||
mutations: {
|
||||
SET_GROUPS(state, groups) {
|
||||
state.groups = groups
|
||||
},
|
||||
ADD_GROUP(state, group) {
|
||||
// 使用 unshift 插入到列表头部(与 Pinia 保持一致)
|
||||
state.groups.unshift(group)
|
||||
},
|
||||
UPDATE_GROUP(state, group) {
|
||||
const g = state.groups.find(item => item.id === group.id)
|
||||
if (g) {
|
||||
Object.assign(g, group)
|
||||
}
|
||||
},
|
||||
REMOVE_GROUP(state, id) {
|
||||
state.groups.filter(g => g.id === id).forEach(g => g.quit = true)
|
||||
},
|
||||
CLEAR_GROUPS(state) {
|
||||
state.groups = []
|
||||
}
|
||||
},
|
||||
|
||||
actions: {
|
||||
// 设置群组列表
|
||||
setGroups({ commit }, groups) {
|
||||
commit('SET_GROUPS', groups)
|
||||
},
|
||||
|
||||
// 添加或更新群组
|
||||
addGroup({ commit, state }, group) {
|
||||
const exists = state.groups.some(g => g.id === group.id)
|
||||
if (exists) {
|
||||
commit('UPDATE_GROUP', group)
|
||||
} else {
|
||||
commit('ADD_GROUP', group)
|
||||
}
|
||||
},
|
||||
|
||||
// 删除群组(逻辑删除)
|
||||
removeGroup({ commit }, id) {
|
||||
commit('REMOVE_GROUP', id)
|
||||
},
|
||||
|
||||
// 更新群组信息
|
||||
updateGroup({ commit }, group) {
|
||||
commit('UPDATE_GROUP', group)
|
||||
},
|
||||
|
||||
// 清空群组列表
|
||||
clear({ commit }) {
|
||||
commit('CLEAR_GROUPS')
|
||||
},
|
||||
|
||||
// 加载群组列表(异步)
|
||||
async loadGroup({ dispatch }) {
|
||||
try {
|
||||
const groups = await uni.$http.get('/api/group/list')
|
||||
dispatch('setGroups', groups.data.data)
|
||||
} catch (error) {
|
||||
// 将错误抛出,让调用方处理
|
||||
throw error
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
getters: {
|
||||
findGroup: (state) => (id) => {
|
||||
return state.groups.find(g => g.id === id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default groupModule
|
||||
Loading…
Reference in new issue