feat:聊天页面代码移植
This commit is contained in:
1 parent
6d26839aee
commit
3aaa1cb425
113 files changed
+7726
-166
No files matched your search
@@ -0,0 +1,64 @@
|
||||
<template>
|
||||
<view class="arrow-bar">
|
||||
<text class="icon iconfont" :class="icon" :style="{color: textColor}"></text>
|
||||
<text class="title">{{ title }}</text>
|
||||
<uni-icons class="arrow" type="right" size="16"></uni-icons>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "arrow-bar",
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
colors: ["#5daa31", "#c7515a", "#e03697", "#85029b",
|
||||
"#c9b455", "#326eb6"]
|
||||
}
|
||||
},
|
||||
computed:{
|
||||
textColor() {
|
||||
let hash = 0;
|
||||
for (var i = 0; i < this.title.length; i++) {
|
||||
hash += this.title.charCodeAt(i);
|
||||
}
|
||||
return this.colors[hash % this.colors.length];
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.arrow-bar {
|
||||
width: 100%;
|
||||
height: 90rpx;
|
||||
font-size: $im-font-size;
|
||||
color: $im-text-color;
|
||||
margin-top: 5rpx;
|
||||
background-color: white;
|
||||
line-height: 90rpx;
|
||||
display: flex;
|
||||
|
||||
.icon {
|
||||
margin-left: 40rpx;
|
||||
}
|
||||
|
||||
.title {
|
||||
flex: 1;
|
||||
margin-left: 10rpx;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
margin-right: 40rpx;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,17 @@
|
||||
<template>
|
||||
<view class="bar-group">
|
||||
<slot></slot>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "bar-group"
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.bar-group {
|
||||
margin: 20rpx 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,68 @@
|
||||
<template>
|
||||
<view class="btn-bar" :style="style">
|
||||
<text v-if="icon" class="icon iconfont" :class="icon"></text>
|
||||
<text class="title">{{ title }}</text>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "btn-bar",
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
required: false
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: "normal"
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: "#000"
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
style() {
|
||||
let color = "#000";
|
||||
switch (this.type) {
|
||||
case 'danger':
|
||||
color = "#f14747";
|
||||
break;
|
||||
case 'primary':
|
||||
color = "#35567f";
|
||||
break;
|
||||
}
|
||||
return `color: ${color};`
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.btn-bar {
|
||||
width: 100%;
|
||||
height: 100rpx;
|
||||
margin-top: 5rpx;
|
||||
background-color: white;
|
||||
line-height: 100rpx;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
.icon {
|
||||
font-size: 40rpx;
|
||||
font-weight: 600;
|
||||
margin-right: 10rpx;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,61 @@
|
||||
<template>
|
||||
<view class="switch-bar">
|
||||
<text class="title">{{ title }}</text>
|
||||
<switch class="switch" :checked="checked" color="#18bc37" @change="onChange"></switch>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "switch-bar",
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
checked: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
value: this.checked
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onChange(e) {
|
||||
this.value = true;
|
||||
setTimeout(() => {
|
||||
this.value = false;
|
||||
}, 100)
|
||||
//this.value = false;
|
||||
|
||||
this.$emit('change', e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.switch-bar {
|
||||
width: 100%;
|
||||
height: 100rpx;
|
||||
font-size: 34rpx;
|
||||
color: black;
|
||||
margin-top: 5rpx;
|
||||
background-color: white;
|
||||
line-height: 100rpx;
|
||||
display: flex;
|
||||
|
||||
.title {
|
||||
flex: 1;
|
||||
margin-left: 40rpx;
|
||||
}
|
||||
|
||||
.switch {
|
||||
margin-right: 40rpx;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -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,133 @@
|
||||
<template>
|
||||
<uni-popup ref="popup" type="bottom">
|
||||
<view class="chat-group-readed">
|
||||
<view class="uni-padding-wrap uni-common-mt">
|
||||
<uni-segmented-control :current="current" :values="items" style-type="button"
|
||||
@clickItem="onClickItem" />
|
||||
</view>
|
||||
<view class="content">
|
||||
<view v-if="current === 0">
|
||||
<virtual-scroller :items="readedMembers">
|
||||
<template v-slot="{ item }">
|
||||
<view class="member-item">
|
||||
<head-image :name="item.showNickName" :online="item.online" :url="item.headImage"
|
||||
:size="90"></head-image>
|
||||
<view class="member-name">{{ item.showNickName }}</view>
|
||||
</view>
|
||||
</template>
|
||||
</virtual-scroller>
|
||||
</view>
|
||||
<view v-if="current === 1">
|
||||
<virtual-scroller :items="unreadMembers">
|
||||
<template v-slot="{ item }">
|
||||
<view class="member-item">
|
||||
<head-image :name="item.showNickName" :online="item.online" :url="item.headImage"
|
||||
:size="90"></head-image>
|
||||
<view class="member-name">{{ item.showNickName }}</view>
|
||||
</view>
|
||||
</template>
|
||||
</virtual-scroller>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</uni-popup>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "chat-group-readed",
|
||||
data() {
|
||||
return {
|
||||
items: ['已读', '未读'],
|
||||
current: 0,
|
||||
readedMembers: [],
|
||||
unreadMembers: []
|
||||
};
|
||||
},
|
||||
props: {
|
||||
msgInfo: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
groupMembers: {
|
||||
type: Array
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
open() {
|
||||
this.$refs.popup.open();
|
||||
this.loadReadedUser();
|
||||
},
|
||||
loadReadedUser() {
|
||||
this.readedMembers = [];
|
||||
this.unreadMembers = [];
|
||||
this.$http({
|
||||
url: `/message/group/findReadedUsers?groupId=${this.msgInfo.groupId}&messageId=${this.msgInfo.id}`,
|
||||
method: 'Get'
|
||||
}).then(userIds => {
|
||||
this.groupMembers.forEach(member => {
|
||||
// 发送者和已退群的不显示
|
||||
if (member.userId == this.msgInfo.sendId || member.quit) {
|
||||
return;
|
||||
}
|
||||
// 区分已读还是未读
|
||||
if (userIds.find(userId => member.userId == userId)) {
|
||||
this.readedMembers.push(member);
|
||||
} else {
|
||||
this.unreadMembers.push(member);
|
||||
}
|
||||
})
|
||||
this.items[0] = `已读(${this.readedMembers.length})`;
|
||||
this.items[1] = `未读(${this.unreadMembers.length})`;
|
||||
|
||||
let chatInfo = {
|
||||
type: 'GROUP',
|
||||
targetId: this.msgInfo.groupId
|
||||
}
|
||||
let msgInfo = {
|
||||
id: this.msgInfo.id,
|
||||
groupId: this.msgInfo.groupId,
|
||||
readedCount: this.readedMembers.length
|
||||
}
|
||||
// 更新已读人数
|
||||
this.chatStore.updateMessage(msgInfo, chatInfo)
|
||||
})
|
||||
},
|
||||
onClickItem(e) {
|
||||
this.current = e.currentIndex;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.chat-group-readed {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: white;
|
||||
padding: 10rpx;
|
||||
|
||||
.member-item {
|
||||
height: 120rpx;
|
||||
display: flex;
|
||||
position: relative;
|
||||
padding: 0 30rpx;
|
||||
align-items: center;
|
||||
background-color: white;
|
||||
white-space: nowrap;
|
||||
|
||||
.member-name {
|
||||
flex: 1;
|
||||
padding-left: 20rpx;
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
line-height: 60rpx;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,202 @@
|
||||
<template>
|
||||
<view class="chat-item" :class="active ? 'active' : ''">
|
||||
<!--rich-text中的表情包会屏蔽事件,所以这里用一个遮罩层捕获点击事件 -->
|
||||
<view class="mask" @tap="showChatBox()"></view>
|
||||
<view class="left">
|
||||
<head-image :url="chat.headImage" :name="chat.showName" :online="online"></head-image>
|
||||
</view>
|
||||
<view class="chat-right">
|
||||
<view class="chat-name">
|
||||
<view class="chat-name-text">
|
||||
<view>{{ chat.showName }}</view>
|
||||
</view>
|
||||
<view class="chat-time">{{ $date.toTimeText(chat.lastSendTime, true) }}</view>
|
||||
</view>
|
||||
<view class="chat-content">
|
||||
<view class="chat-at-text">{{ atText }}</view>
|
||||
<view class="chat-send-name" v-if="isShowSendName">{{
|
||||
chat.sendNickName + ': '
|
||||
}}</view>
|
||||
<view v-if="!isTextMessage" class="chat-content-text">{{ chat.lastContent }}</view>
|
||||
<rich-text v-else class="chat-content-text" :nodes="nodesText"></rich-text>
|
||||
<view v-if="chat.isDnd" class="icon iconfont icon-dnd"></view>
|
||||
<uni-badge v-else-if="chat.unreadCount > 0" :max-num="99" :text="chat.unreadCount" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'chatItem',
|
||||
data() {
|
||||
return {};
|
||||
},
|
||||
props: {
|
||||
chat: {
|
||||
type: Object,
|
||||
},
|
||||
index: {
|
||||
type: Number,
|
||||
},
|
||||
active: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
showChatBox() {
|
||||
// 初始化期间进入会话会导致消息不刷新
|
||||
if (!this.configStore.appInit || this.chatStore.loading) {
|
||||
uni.showToast({
|
||||
title: '正在初始化页面,请稍后...',
|
||||
icon: 'none',
|
||||
});
|
||||
return;
|
||||
}
|
||||
uni.navigateTo({
|
||||
url: '/pages/message/chat-box?chatIdx=' + this.index,
|
||||
});
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
isShowSendName() {
|
||||
if (!this.chat.sendNickName) {
|
||||
return false;
|
||||
}
|
||||
let size = this.chat.messages.length;
|
||||
if (size == 0) {
|
||||
return false;
|
||||
}
|
||||
// 只有群聊的普通消息需要显示名称
|
||||
let lastMsg = this.chat.messages[size - 1];
|
||||
return this.$msgType.isNormal(lastMsg.type);
|
||||
},
|
||||
atText() {
|
||||
if (this.chat.atMe) {
|
||||
return '[有人@我]';
|
||||
} else if (this.chat.atAll) {
|
||||
return '[@全体成员]';
|
||||
}
|
||||
return '';
|
||||
},
|
||||
isTextMessage() {
|
||||
if (this.chat.messages.length == 0) {
|
||||
return false;
|
||||
}
|
||||
let idx = this.chat.messages.length - 1;
|
||||
let messageType = this.chat.messages[idx].type;
|
||||
return messageType == this.$enums.MESSAGE_TYPE.TEXT;
|
||||
},
|
||||
nodesText() {
|
||||
let text = this.$str.html2Escape(this.chat.lastContent);
|
||||
return this.$emo.transform(text, 'emoji-small');
|
||||
},
|
||||
online() {
|
||||
if (this.chat.type == 'PRIVATE') {
|
||||
let friend = this.friendStore.findFriend(this.chat.targetId);
|
||||
return friend && friend.online;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.chat-item {
|
||||
height: 96rpx;
|
||||
display: flex;
|
||||
margin-bottom: 2rpx;
|
||||
position: relative;
|
||||
padding: 18rpx 20rpx;
|
||||
align-items: center;
|
||||
background-color: white;
|
||||
white-space: nowrap;
|
||||
|
||||
&:hover {
|
||||
background-color: $im-bg-active;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: $im-bg-active;
|
||||
}
|
||||
|
||||
.mask {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 99;
|
||||
}
|
||||
|
||||
.left {
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100rpx;
|
||||
height: 100rpx;
|
||||
}
|
||||
|
||||
.chat-right {
|
||||
height: 100%;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
padding-left: 20rpx;
|
||||
text-align: left;
|
||||
overflow: hidden;
|
||||
|
||||
.chat-name {
|
||||
display: flex;
|
||||
|
||||
.chat-name-text {
|
||||
flex: 1;
|
||||
font-size: $im-font-size-large;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.chat-time {
|
||||
font-size: $im-font-size-smaller-extra;
|
||||
color: $im-text-color-lighter;
|
||||
text-align: right;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-content {
|
||||
display: flex;
|
||||
font-size: $im-font-size-smaller;
|
||||
color: $im-text-color-lighter;
|
||||
padding-top: 8rpx;
|
||||
align-items: center;
|
||||
|
||||
.chat-at-text {
|
||||
color: $im-color-danger;
|
||||
}
|
||||
|
||||
.chat-send-name {
|
||||
font-size: $im-font-size-smaller;
|
||||
}
|
||||
|
||||
.chat-content-text {
|
||||
flex: 1;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: $im-font-size;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,546 @@
|
||||
<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 class="name">{{ showName }}</text>
|
||||
</view>
|
||||
<view class="bottom">
|
||||
<view class="message-content-wrapper">
|
||||
<view v-if="msgInfo.type == $enums.MESSAGE_TYPE.TEXT">
|
||||
<long-press-menu :items="menuItems" @select="onSelectMenu">
|
||||
<!-- up-parse支持点击a标签,但是不支持显示emo表情,也不支持换行 -->
|
||||
<up-parse v-if="$url.containUrl(msgInfo.content)&&!$emo.containEmoji(msgInfo.content)"
|
||||
class="message-text" :showImgMenu="false" :content="nodesText"></up-parse>
|
||||
<!-- rich-text支持显示emo表情以及消息换行,但是不支持点击a标签 -->
|
||||
<rich-text v-else class="message-text" :nodes="nodesText"></rich-text>
|
||||
</long-press-menu>
|
||||
</view>
|
||||
<view class="message-image" v-else-if="msgInfo.type == $enums.MESSAGE_TYPE.IMAGE">
|
||||
<long-press-menu :items="menuItems" @select="onSelectMenu">
|
||||
<view class="image-box">
|
||||
<image class="send-image" :style="imageStyle" mode="aspectFill"
|
||||
:src="contentData.thumbUrl" lazy-load="true" @click.stop="onShowFullImage()">
|
||||
</image>
|
||||
<loading v-if="sending"></loading>
|
||||
</view>
|
||||
</long-press-menu>
|
||||
</view>
|
||||
<view class="message-file" v-else-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="contentData.name" showUnderLine="true"
|
||||
color="#007BFF" :href="contentData.url"></uni-link>
|
||||
<view class="file-size">{{ fileSize }}</view>
|
||||
</view>
|
||||
<view class="file-icon iconfont icon-file"></view>
|
||||
<loading v-if="sending"></loading>
|
||||
</view>
|
||||
</long-press-menu>
|
||||
</view>
|
||||
<long-press-menu v-else-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">{{ contentData.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 v-if="sending&&(isTextMessage||isAudioMessage)" class="sending">
|
||||
<loading :size="40" icon-color="#656adf" :mask="false"></loading>
|
||||
</view>
|
||||
<view v-else-if="sendFail" @click="onSendFail"
|
||||
class="send-fail iconfont icon-warning-circle-fill"></view>
|
||||
</view>
|
||||
<view class="message-status" v-if="!isAction && msgInfo.selfSend && !msgInfo.groupId">
|
||||
<text class="chat-readed" v-if="msgInfo.status == $enums.MESSAGE_STATUS.READED">已读</text>
|
||||
<text class="chat-unread" v-else>未读</text>
|
||||
</view>
|
||||
<view class="chat-receipt" v-if="msgInfo.receipt" @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>
|
||||
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
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
audioPlayState: 'STOP',
|
||||
innerAudioContext: null,
|
||||
menu: {
|
||||
show: false,
|
||||
style: ""
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onSendFail() {
|
||||
this.$emit("resend", this.msgInfo);
|
||||
},
|
||||
onPlayAudio() {
|
||||
// 初始化音频播放器
|
||||
if (!this.innerAudioContext) {
|
||||
this.innerAudioContext = uni.createInnerAudioContext();
|
||||
let url = this.contentData.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 = this.contentData.originUrl;
|
||||
uni.previewImage({
|
||||
urls: [imageUrl]
|
||||
})
|
||||
},
|
||||
onShowReadedBox() {
|
||||
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"
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
sending() {
|
||||
return this.msgInfo.status == this.$enums.MESSAGE_STATUS.SENDING;
|
||||
},
|
||||
sendFail() {
|
||||
return this.msgInfo.status == this.$enums.MESSAGE_STATUS.FAILED;
|
||||
},
|
||||
contentData() {
|
||||
return JSON.parse(this.msgInfo.content)
|
||||
},
|
||||
fileSize() {
|
||||
let size = this.contentData.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;
|
||||
},
|
||||
isTextMessage() {
|
||||
return this.msgInfo.type == this.$enums.MESSAGE_TYPE.TEXT
|
||||
},
|
||||
isAudioMessage() {
|
||||
return this.msgInfo.type == this.$enums.MESSAGE_TYPE.AUDIO
|
||||
},
|
||||
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.$str.html2Escape(this.msgInfo.content)
|
||||
text = this.$url.replaceURLWithHTMLLinks(text, color)
|
||||
return this.$emo.transform(text, 'emoji-normal')
|
||||
},
|
||||
imageStyle() {
|
||||
console.log(uni.getSystemInfo())
|
||||
let maxSize = uni.getSystemInfoSync().windowWidth * 0.6;
|
||||
let minSize = uni.getSystemInfoSync().windowWidth * 0.2;
|
||||
let width = this.contentData.width;
|
||||
let height = this.contentData.height;
|
||||
if (width && height) {
|
||||
let ratio = Math.min(width, height) / Math.max(width, height);
|
||||
let w = Math.max(width > height ? maxSize : ratio * maxSize, minSize);
|
||||
let h = Math.max(width > height ? ratio * maxSize : maxSize, minSize);
|
||||
return `width: ${w}px;height:${h}px;`
|
||||
} else {
|
||||
// 兼容历史版本,历史数据没有记录宽高
|
||||
return `max-width: ${maxSize}px;min-width:100px;max-height: ${maxSize}px;min-height:100px;`
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</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;
|
||||
align-items: center;
|
||||
|
||||
.name {
|
||||
color: $im-text-color-lighter;
|
||||
font-size: $im-font-size-smaller;
|
||||
line-height: $im-font-size-smaller;
|
||||
height: $im-font-size-smaller;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
.bottom {
|
||||
display: inline-block;
|
||||
padding-right: 80rpx;
|
||||
margin-top: 5rpx;
|
||||
|
||||
.message-content-wrapper {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.sending {
|
||||
position: relative;
|
||||
margin: 0 6rpx;
|
||||
|
||||
.icon-loading {
|
||||
color: $im-color-primary;
|
||||
}
|
||||
}
|
||||
|
||||
.send-fail {
|
||||
color: #e60c0c;
|
||||
font-size: 50rpx;
|
||||
margin: 0 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-word;
|
||||
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 {
|
||||
cursor: pointer;
|
||||
border-radius: 10rpx;
|
||||
background: $im-bg;
|
||||
border: 6rpx solid $im-color-primary-light-5;
|
||||
}
|
||||
}
|
||||
|
||||
.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-content-wrapper {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
.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-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,243 @@
|
||||
<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" style="--d: 0"></text>
|
||||
<text class="note" style="--d: 1"></text>
|
||||
<text class="note" style="--d: 2"></text>
|
||||
<text class="note" style="--d: 3"></text>
|
||||
<text class="note" style="--d: 4"></text>
|
||||
<text class="note" style="--d: 5"></text>
|
||||
<text class="note" style="--d: 6"></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,
|
||||
druation: 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() {
|
||||
/* 用户第一次使用语音会唤醒录音权限请求,此时会导致@touchend失效,
|
||||
一直处于录音状态,这里允许用户再次点击发送语音并结束录音 */
|
||||
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;
|
||||
}
|
||||
// 大于1秒才发送
|
||||
if (this.druation <= 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.druation = 0;
|
||||
this.stopTimer();
|
||||
this.rcTimer = setInterval(() => {
|
||||
this.druation++;
|
||||
// 大于60s,直接结束
|
||||
if (this.druation >= 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.druation > 50) {
|
||||
return `${60 - this.druation}s后将停止录音`;
|
||||
}
|
||||
return `录音时长:${this.druation}s`;
|
||||
}
|
||||
},
|
||||
unmounted() {
|
||||
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;
|
||||
animation-delay: calc(0.1s * var(--d));
|
||||
|
||||
@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>
|
||||
@@ -0,0 +1,122 @@
|
||||
<template>
|
||||
<view>
|
||||
<lsj-upload ref="lsjUpload" :height="'100%'" :option="option" @uploadEnd="onUploadEnd" @change="onChange"
|
||||
:size="maxSize" :instantly="true">
|
||||
<slot></slot>
|
||||
</lsj-upload>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import UNI_APP from '@/.env.js';
|
||||
|
||||
export default {
|
||||
name: "file-upload",
|
||||
data() {
|
||||
return {
|
||||
fileMap: new Map(),
|
||||
option: {
|
||||
url: UNI_APP.BASE_URL + '/file/upload',
|
||||
name: 'file',
|
||||
header: {
|
||||
accessToken: uni.getStorageSync('loginInfo').accessToken
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
props: {
|
||||
maxSize: {
|
||||
type: Number,
|
||||
default: 10
|
||||
},
|
||||
onBefore: {
|
||||
type: Function,
|
||||
default: null
|
||||
},
|
||||
onSuccess: {
|
||||
type: Function,
|
||||
default: null
|
||||
},
|
||||
onError: {
|
||||
type: Function,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
show() {
|
||||
this.$refs.lsjUpload.show();
|
||||
},
|
||||
hide() {
|
||||
this.$refs.lsjUpload.hide();
|
||||
},
|
||||
onUploadEnd(item) {
|
||||
let file = this.fileMap.get(item.path);
|
||||
if (item.type == 'fail') {
|
||||
this.onError(file)
|
||||
return;
|
||||
}
|
||||
let res = JSON.parse(item.responseText);
|
||||
if (res.code == 200) {
|
||||
// 上传成功
|
||||
this.onOk(file, res);
|
||||
} else if (res.code == 401) {
|
||||
// token已过期,重新获取token
|
||||
this.refreshToken().then((res) => {
|
||||
let newToken = res.data.accessToken;
|
||||
this.option.header.accessToken = newToken;
|
||||
this.$refs.lsjUpload.setData(this.option);
|
||||
// 重新上传
|
||||
this.$refs.lsjUpload.upload(file.name);
|
||||
}).catch(() => {
|
||||
this.onError(file, res);
|
||||
})
|
||||
} else {
|
||||
// 上传失败
|
||||
this.onError(file, res);
|
||||
}
|
||||
},
|
||||
onChange(files) {
|
||||
if (!files.size) {
|
||||
return;
|
||||
}
|
||||
files.forEach((file, name) => {
|
||||
if (!this.fileMap.has(file.path)) {
|
||||
this.onBefore && this.onBefore(file)
|
||||
this.fileMap.set(file.path, file);
|
||||
}
|
||||
})
|
||||
},
|
||||
onOk(file, res) {
|
||||
this.fileMap.delete(file.path);
|
||||
this.$refs.lsjUpload.clear(file.name);
|
||||
this.onSuccess && this.onSuccess(file, res);
|
||||
},
|
||||
onFailed(file, res) {
|
||||
this.fileMap.delete(file.path);
|
||||
this.$refs.lsjUpload.clear(file.name);
|
||||
this.onError && this.onError(file, res);
|
||||
},
|
||||
refreshToken() {
|
||||
return new Promise((resolve, reject) => {
|
||||
let loginInfo = uni.getStorageSync('loginInfo')
|
||||
uni.request({
|
||||
method: 'PUT',
|
||||
url: UNI_APP.BASE_URL + '/refreshToken',
|
||||
header: {
|
||||
refreshToken: loginInfo.refreshToken
|
||||
},
|
||||
success: (res) => {
|
||||
resolve(res.data);
|
||||
},
|
||||
fail: (res) => {
|
||||
reject(res);
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
@@ -0,0 +1,82 @@
|
||||
<template>
|
||||
<view class="friend-item" @click="showFriendInfo()">
|
||||
<head-image :name="friend.nickName" :online="friend.online" :url="friend.headImage" size="small"></head-image>
|
||||
<view class="friend-info">
|
||||
<view class="friend-name">{{ friend.nickName }}</view>
|
||||
<view class="friend-online">
|
||||
<image v-show="friend.onlineWeb" class="online" src="/static/image/online_web.png" title="电脑设备在线" />
|
||||
<image v-show="friend.onlineApp" class="online" src="/static/image/online_app.png" title="移动设备在线" />
|
||||
</view>
|
||||
<slot></slot>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "frined-item",
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
methods: {
|
||||
showFriendInfo() {
|
||||
if (this.detail) {
|
||||
uni.navigateTo({
|
||||
url: "/pages/common/user-info?id=" + this.friend.id
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
props: {
|
||||
friend: {
|
||||
type: Object
|
||||
},
|
||||
detail: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scope lang="scss">
|
||||
.friend-item {
|
||||
height: 90rpx;
|
||||
display: flex;
|
||||
margin-bottom: 1rpx;
|
||||
position: relative;
|
||||
padding: 10rpx;
|
||||
padding-left: 20rpx;
|
||||
align-items: center;
|
||||
background-color: white;
|
||||
white-space: nowrap;
|
||||
|
||||
&:hover {
|
||||
background-color: $im-bg;
|
||||
}
|
||||
|
||||
.friend-info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
padding-left: 20rpx;
|
||||
text-align: left;
|
||||
|
||||
.friend-name {
|
||||
flex: 1;
|
||||
font-size: $im-font-size;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.friend-online {
|
||||
margin-top: 4rpx;
|
||||
|
||||
.online {
|
||||
padding-right: 4rpx;
|
||||
width: 24rpx;
|
||||
height: 24rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,58 @@
|
||||
<template>
|
||||
<view class="group-item" @click="showGroupInfo()">
|
||||
<head-image :name="group.showGroupName" :url="group.headImageThumb" size="small"></head-image>
|
||||
<view class="group-name">
|
||||
<view>{{ group.showGroupName }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "group-item",
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
methods: {
|
||||
showGroupInfo() {
|
||||
uni.navigateTo({
|
||||
url: "/pages/group/group-info?id=" + this.group.id
|
||||
})
|
||||
},
|
||||
},
|
||||
props: {
|
||||
group: {
|
||||
type: Object
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scope lang="scss">
|
||||
.group-item {
|
||||
height: 90rpx;
|
||||
display: flex;
|
||||
margin-bottom: 2rpx;
|
||||
position: relative;
|
||||
padding: 18rpx 20rpx;
|
||||
align-items: center;
|
||||
background-color: white;
|
||||
white-space: nowrap;
|
||||
|
||||
&:hover {
|
||||
background-color: $im-bg-active;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: $im-bg-active;
|
||||
}
|
||||
|
||||
.group-name {
|
||||
font-size: $im-font-size;
|
||||
padding-left: 20rpx;
|
||||
text-align: left;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -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,129 @@
|
||||
<template>
|
||||
<view class="head-image none-pointer-events" @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?.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: Number
|
||||
},
|
||||
size: {
|
||||
type: [Number, String],
|
||||
default: 'default'
|
||||
},
|
||||
url: {
|
||||
type: String
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
radius: {
|
||||
type: String,
|
||||
default: "50%"
|
||||
},
|
||||
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') {
|
||||
return {
|
||||
'default': 96,
|
||||
'small': 84,
|
||||
'smaller': 72,
|
||||
'mini': 60,
|
||||
'minier': 48,
|
||||
'lage': 108,
|
||||
'lager': 120,
|
||||
} [this.size]
|
||||
}
|
||||
},
|
||||
avatarImageStyle() {
|
||||
return `width:${this._size}rpx;
|
||||
height:${this._size}rpx;`
|
||||
},
|
||||
avatarTextStyle() {
|
||||
return `width: ${this._size}rpx;
|
||||
height:${this._size}rpx;
|
||||
background: linear-gradient(145deg,#ffffff20 25%,#00000060),${this.textColor};
|
||||
font-size:${this._size * 0.45}rpx;
|
||||
border-radius: ${this.radius};
|
||||
`
|
||||
},
|
||||
textColor() {
|
||||
if (!this.name) {
|
||||
return '#fff';
|
||||
}
|
||||
let hash = 0;
|
||||
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,100 @@
|
||||
<template>
|
||||
<view @click="selectAndUpload()">
|
||||
<slot></slot>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import UNI_APP from '@/.env.js'
|
||||
|
||||
export default {
|
||||
name: "image-upload",
|
||||
data() {
|
||||
return {
|
||||
uploadHeaders: {
|
||||
"accessToken": uni.getStorageSync('loginInfo').accessToken
|
||||
}
|
||||
}
|
||||
},
|
||||
props: {
|
||||
maxCount: {
|
||||
type: Number,
|
||||
default: 1
|
||||
},
|
||||
maxSize: {
|
||||
type: Number,
|
||||
default: 5 * 1024 * 1024
|
||||
},
|
||||
sourceType: {
|
||||
type: String,
|
||||
default: 'album'
|
||||
},
|
||||
isPermanent: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
thumbSize: {
|
||||
type: Number,
|
||||
default: 50
|
||||
},
|
||||
onBefore: {
|
||||
type: Function,
|
||||
default: null
|
||||
},
|
||||
onSuccess: {
|
||||
type: Function,
|
||||
default: null
|
||||
},
|
||||
onError: {
|
||||
type: Function,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
selectAndUpload() {
|
||||
uni.chooseImage({
|
||||
count: this.maxCount, //最多可以选择的图片张数,默认9
|
||||
sourceType: [this.sourceType], //album 从相册选图,camera 使用相机,默认二者都有。如需直接开相机或直接选相册,请只使用一个选项
|
||||
sizeType: ['original'], //original 原图,compressed 压缩图,默认二者都有
|
||||
success: (res) => {
|
||||
res.tempFiles.forEach((file) => {
|
||||
if (!this.onBefore || this.onBefore(file)) {
|
||||
// 调用上传图片的接口
|
||||
this.uploadImage(file);
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
uploadImage(file) {
|
||||
let action = `/image/upload?isPermanent=${this.isPermanent}&thumbSize=${this.thumbSize}`
|
||||
uni.uploadFile({
|
||||
url: UNI_APP.BASE_URL + action,
|
||||
header: {
|
||||
accessToken: uni.getStorageSync("loginInfo").accessToken
|
||||
},
|
||||
filePath: file.path, // 要上传文件资源的路径
|
||||
name: 'file',
|
||||
success: (res) => {
|
||||
let data = JSON.parse(res.data);
|
||||
if (data.code != 200) {
|
||||
uni.showToast({
|
||||
icon: "none",
|
||||
title: data.message,
|
||||
})
|
||||
this.onError && this.onError(file, data);
|
||||
} else {
|
||||
this.onSuccess && this.onSuccess(file, data);
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
console.log(err);
|
||||
this.onError && this.onError(file, err);
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
@@ -0,0 +1,71 @@
|
||||
<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
|
||||
},
|
||||
iconColor: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
mask: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
icontStyle() {
|
||||
let style = `font-size:${this.size}rpx;`;
|
||||
if(this.iconColor){
|
||||
style += `color: ${this.iconColor};`
|
||||
}else if(this.mask){
|
||||
style += 'color: #eee;'
|
||||
}
|
||||
return style;
|
||||
},
|
||||
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,129 @@
|
||||
<template>
|
||||
<view class="long-press-menu none-pointer-events">
|
||||
<view @longpress.prevent.stop="onLongPress($event)" @touchmove="onTouchMove" @touchend="onTouchEnd">
|
||||
<slot></slot>
|
||||
</view>
|
||||
<view v-if="isShowMenu" class="menu-mask" @touchstart="onClose()" @contextmenu.prevent=""></view>
|
||||
<view v-if="isShowMenu" class="menu" :style="menuStyle">
|
||||
<view class="menu-item" v-for="(item) in items" :key="item.key" @click.prevent="onSelectMenu(item)">
|
||||
<text :style="itemStyle(item)"> {{ item.name }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "long-press-menu",
|
||||
data() {
|
||||
return {
|
||||
isShowMenu: false,
|
||||
isTouchMove: false,
|
||||
style: ""
|
||||
}
|
||||
},
|
||||
props: {
|
||||
items: {
|
||||
type: Array
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onLongPress(e) {
|
||||
if (this.isTouchMove) {
|
||||
// 屏幕移动时不弹出
|
||||
return;
|
||||
}
|
||||
uni.getSystemInfo({
|
||||
success: (res) => {
|
||||
let touches = e.touches[0];
|
||||
let style = "";
|
||||
/* 因 非H5端不兼容 style 属性绑定 Object ,所以拼接字符 */
|
||||
if (touches.clientY > (res.windowHeight / 2)) {
|
||||
style = `bottom:${res.windowHeight - touches.clientY + 20}px;`;
|
||||
} else {
|
||||
style = `top:${touches.clientY + 20}px;`;
|
||||
}
|
||||
if (touches.clientX > (res.windowWidth / 2)) {
|
||||
style += `right:${res.windowWidth - touches.clientX}px;`;
|
||||
} else {
|
||||
style += `left:${touches.clientX}px;`;
|
||||
}
|
||||
this.menuStyle = style;
|
||||
//
|
||||
this.$nextTick(() => {
|
||||
this.isShowMenu = true;
|
||||
});
|
||||
this.menuTouch = touches
|
||||
}
|
||||
})
|
||||
},
|
||||
onTouchMove(e) {
|
||||
this.isTouchMove = true;
|
||||
let touches = e.touches[0];
|
||||
// 屏幕拖动大于50px时,取消菜单
|
||||
if (Math.abs(touches.clientX - this.menuTouch.clientX) > 50 ||
|
||||
Math.abs(touches.clientY - this.menuTouch.clientY) > 50) {
|
||||
this.onClose();
|
||||
}
|
||||
},
|
||||
onTouchEnd() {
|
||||
this.isTouchMove = false;
|
||||
},
|
||||
onSelectMenu(item) {
|
||||
this.$emit("select", item);
|
||||
this.isShowMenu = false;
|
||||
},
|
||||
onClose() {
|
||||
this.isShowMenu = false;
|
||||
},
|
||||
itemStyle(item) {
|
||||
if (item.color) {
|
||||
return `color:${item.color};`
|
||||
}
|
||||
return `color:#000;`;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.long-press-menu {
|
||||
.menu-mask {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
.menu {
|
||||
position: fixed;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
background-color: #fff;
|
||||
z-index: 1000;
|
||||
box-shadow: $im-box-shadow-dark;
|
||||
|
||||
.menu-item {
|
||||
height: 28px;
|
||||
min-width: 120rpx;
|
||||
line-height: 28px;
|
||||
font-size: $im-font-size-small;
|
||||
display: flex;
|
||||
padding: 6px 20px;
|
||||
justify-content: flex-start;
|
||||
|
||||
&:hover {
|
||||
background: $im-bg-active;
|
||||
}
|
||||
|
||||
.menu-icon {
|
||||
margin-right: 10rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,121 @@
|
||||
<template>
|
||||
<view class="im-nav-bar">
|
||||
<!-- #ifdef APP-PLUS -->
|
||||
<view style="height: var(--status-bar-height)"></view>
|
||||
<!-- #endif -->
|
||||
<view class="im-nav-bar-content">
|
||||
<!-- #ifndef MP-WEIXIN -->
|
||||
<view class="back" @click="handleBackClick" v-if="back">
|
||||
<uni-icons type="back" :size="iconFontSize"></uni-icons>
|
||||
</view>
|
||||
<!-- #endif -->
|
||||
<view class="title" v-if="title">
|
||||
<slot></slot>
|
||||
</view>
|
||||
<view class="btn">
|
||||
<uni-icons class="btn-item" v-if="search" type="search" :size="iconFontSize"
|
||||
@click="$emit('search')"></uni-icons>
|
||||
<uni-icons class="btn-item" v-if="add" type="plusempty" :size="iconFontSize"
|
||||
@click="$emit('add')"></uni-icons>
|
||||
<uni-icons class="btn-item" v-if="more" type="more-filled" :size="iconFontSize"
|
||||
@click="$emit('more')"></uni-icons>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "nav-bar",
|
||||
props: {
|
||||
back: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
title: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
search: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
add: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
more: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
iconFontSize: {
|
||||
type: Number,
|
||||
default: 24
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
computed: {
|
||||
height() {
|
||||
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleBackClick() {
|
||||
uni.navigateBack({
|
||||
delta: 1
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.im-nav-bar {
|
||||
background: $im-bg-linear;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
color: $im-text-color;
|
||||
border-bottom: 1px solid $im-border-light;
|
||||
font-size: $im-font-size-large;
|
||||
z-index: 99;
|
||||
|
||||
.im-nav-bar-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
height: $im-nav-bar-height;
|
||||
|
||||
.title {}
|
||||
|
||||
.back {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px;
|
||||
font-size: 22px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.btn {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
padding: 12px;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
|
||||
.btn-item {
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,60 @@
|
||||
<template>
|
||||
<scroll-view scroll-y="true" :style="{height: height}" upper-threshold="200" @scrolltolower="onScrollToBottom"
|
||||
scroll-with-animation="true">
|
||||
<view v-for="(item, idx) in showItems" :key="idx">
|
||||
<slot :item="item">
|
||||
</slot>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "virtual-scroller",
|
||||
data() {
|
||||
return {
|
||||
page: 1,
|
||||
isInitEvent: false,
|
||||
lockTip: false
|
||||
}
|
||||
},
|
||||
props: {
|
||||
height: {
|
||||
type: String,
|
||||
default: '60vh'
|
||||
},
|
||||
items: {
|
||||
type: Array
|
||||
},
|
||||
size: {
|
||||
type: Number,
|
||||
default: 30
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onScrollToBottom(e) {
|
||||
if (this.showMaxIdx >= this.items.length) {
|
||||
this.showTip();
|
||||
} else {
|
||||
this.page++;
|
||||
}
|
||||
},
|
||||
showTip() {
|
||||
uni.showToast({
|
||||
title: "已滚动至底部",
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
showMaxIdx() {
|
||||
return Math.min(this.page * this.size, this.items.length);
|
||||
},
|
||||
showItems() {
|
||||
return this.items.slice(0, this.showMaxIdx);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
Reference in new issue
Block a user