|
|
<template>
|
|
|
<view class="content">
|
|
|
<!-- 聊天内容 -->
|
|
|
<scroll-view
|
|
|
class="chat"
|
|
|
scroll-y="true"
|
|
|
scroll-with-animation="true"
|
|
|
:scroll-into-view="scrollToView"
|
|
|
refresher-enabled="true"
|
|
|
:refresher-triggered="triggered"
|
|
|
:refresher-threshold="100"
|
|
|
refresher-background="#f0f0f0"
|
|
|
@refresherpulling="onPulling"
|
|
|
@refresherrefresh="onRefresh"
|
|
|
@refresherrestore="onRestore"
|
|
|
@refresherabort="onAbort"
|
|
|
>
|
|
|
<view class="chat-main" :style="{ paddingBottom: inputh + 'px' }">
|
|
|
<view
|
|
|
class="chat-ls"
|
|
|
v-for="(item, index) in messageList"
|
|
|
:key="index"
|
|
|
:id="'msg' + index"
|
|
|
>
|
|
|
<!-- 时间分隔 -->
|
|
|
<view class="chat-time" v-if="item.fromtime != ''">
|
|
|
{{ changeTime(item.fromtime) }}
|
|
|
</view>
|
|
|
|
|
|
<!-- 消息体:左侧(对方) -->
|
|
|
<view class="msg-m msg-left" v-if="item.fromuser !== userId">
|
|
|
<view class="user-img">
|
|
|
<image
|
|
|
class="uimg"
|
|
|
:src="item.headimg"
|
|
|
@error="handleImageError($event, index)"
|
|
|
mode="aspectFill"
|
|
|
/>
|
|
|
<text class="uname">{{ item.fromname }}</text>
|
|
|
</view>
|
|
|
|
|
|
<!-- 文本 -->
|
|
|
<view class="message" v-if="item.type == 'txt'">
|
|
|
<view class="msg-text"><text selectable>{{ item.content }}</text></view>
|
|
|
</view>
|
|
|
|
|
|
<!-- 图片 -->
|
|
|
<view class="message" v-if="item.type == 'image'" @tap="previewImg(item.content)">
|
|
|
<image :src="item.content" class="msg-img" mode="widthFix" />
|
|
|
</view>
|
|
|
|
|
|
<!-- 视频 -->
|
|
|
<view class="messagevideo" v-if="item.type == 'video'">
|
|
|
<myVideo :videoUrl="item.content" />
|
|
|
</view>
|
|
|
|
|
|
<!-- 音频 -->
|
|
|
<view class="message" v-if="item.type == 'audio'" @click="playVoice(item.content, item.ifaudio, index)">
|
|
|
<view class="msg-text voice" :style="{ width: item.time * 40 + 'rpx' }">
|
|
|
<view class="voicel" v-if="item.ifaudio"><div class="bg voicePlay"></div></view>
|
|
|
<image v-else src="../../static/icon/horn.png" class="voice-img" />
|
|
|
{{ item.time }}″
|
|
|
</view>
|
|
|
</view>
|
|
|
|
|
|
<!-- 商品/订单 -->
|
|
|
<view class="message" v-if="item.type == 'product'">
|
|
|
<view v-if="item.send" class="msg-product" @click="gotoDetail(item)">
|
|
|
<view v-if="item.send.type == 3" class="zx">
|
|
|
<view class="zxname">订单咨询</view>
|
|
|
<view class="zxview">查看订单</view>
|
|
|
</view>
|
|
|
<view class="msg-con">
|
|
|
<image class="img" :src="item.send.pic" mode="aspectFit" />
|
|
|
<view class="info">
|
|
|
<view class="name">
|
|
|
<text class="ntxt">{{ item.send.name }}</text>
|
|
|
<text
|
|
|
v-if="item.send.spData"
|
|
|
v-for="(value, key) in item.send.spData"
|
|
|
:key="key"
|
|
|
class="txt"
|
|
|
>
|
|
|
{{ key }}: {{ value }}
|
|
|
</text>
|
|
|
</view>
|
|
|
<view class="xq">
|
|
|
<view class="list">
|
|
|
<view v-if="item.send.type == 3" class="ord">
|
|
|
购买金额:¥ <text style="font-size: 30rpx;">{{ item.send.price }}</text>
|
|
|
{{ item.send.unit ? '/' + item.send.unit : '' }}
|
|
|
</view>
|
|
|
<view v-else class="price">
|
|
|
¥{{ item.send.price }}{{ item.send.unit ? '/' + item.send.unit : '' }}
|
|
|
</view>
|
|
|
<view v-if="item.send.quantity" class="qty">
|
|
|
共({{ item.send.quantity }}{{ item.send.unit ? item.send.unit : '件' }})
|
|
|
</view>
|
|
|
</view>
|
|
|
<button class="btn" type="primary" v-if="item.send.type == 1 || item.send.type == 2">
|
|
|
去购买
|
|
|
</button>
|
|
|
</view>
|
|
|
</view>
|
|
|
</view>
|
|
|
</view>
|
|
|
</view>
|
|
|
</view>
|
|
|
|
|
|
<!-- 消息体:右侧(自己) -->
|
|
|
<view class="msg-m msg-right" v-if="item.fromuser == userId">
|
|
|
<view class="user-img">
|
|
|
<image class="uimg" :src="item.headimg" @error="handleImageError($event, index)" mode="aspectFill" />
|
|
|
<text class="uname">{{ item.fromname }}</text>
|
|
|
</view>
|
|
|
|
|
|
<!-- 群聊才显示已读/未读按钮 -->
|
|
|
<template v-if="chatType === 'group'">
|
|
|
<view v-if="item.type == 'txt' || item.type == 'image' || item.type == 'video' || item.type == 'audio' || item.type == 'product'" class="message" style="flex-direction: column; align-items: flex-end;">
|
|
|
<view class="rcon">
|
|
|
<view class="read" @click="openRead(item)">已读{{ item.read ? item.read.length : 0 }}</view>
|
|
|
<view class="noread" @click="openNoRead(item)">未读{{ item.read ? 3 - item.read.length : 3 }}</view>
|
|
|
</view>
|
|
|
<!-- 内容显示与左侧类似,但文字颜色为白色,背景为绿色,故复用部分结构 -->
|
|
|
<view v-if="item.type == 'txt'" class="msg-text" style="margin-right: 0;">
|
|
|
<text selectable>{{ item.content }}</text>
|
|
|
</view>
|
|
|
<image v-if="item.type == 'image'" :src="item.content" class="msg-img" mode="widthFix" style="margin-right: 0;" />
|
|
|
<myVideo v-if="item.type == 'video'" :videoUrl="item.content" style="margin-right: 0;" />
|
|
|
<view v-if="item.type == 'audio'" class="msg-text voice" :style="{ width: item.time * 40 + 'rpx' }">
|
|
|
{{ item.time }}″
|
|
|
<view class="voicel" v-if="item.ifaudio"><div class="bg voicePlay"></div></view>
|
|
|
<image v-else src="../../static/icon/hornrw.png" class="voice-img" />
|
|
|
</view>
|
|
|
<view v-if="item.type == 'product'" class="msg-product" style="margin-right: 0;">
|
|
|
<!-- 商品卡片复用左侧相同结构,但右侧需调整样式,因为父级flex方向不同,此处省略重复代码 -->
|
|
|
<!-- 实际开发中应抽离为组件,这里为简洁略 -->
|
|
|
</view>
|
|
|
</view>
|
|
|
</template>
|
|
|
|
|
|
<!-- 私聊显示已读/未读状态(仅文字和图片) -->
|
|
|
<template v-else>
|
|
|
<view v-if="item.type == 'txt'" class="message">
|
|
|
<view v-if="item.ifread" class="read">已读</view>
|
|
|
<view v-else class="noread">未读</view>
|
|
|
<view class="msg-text"><text selectable>{{ item.content }}</text></view>
|
|
|
</view>
|
|
|
<view v-if="item.type == 'image'" @tap="previewImg(item.content)" class="message">
|
|
|
<view v-if="item.ifread" class="read">已读</view>
|
|
|
<view v-else class="noread">未读</view>
|
|
|
<image :src="item.content" class="msg-img" mode="widthFix" />
|
|
|
</view>
|
|
|
<view v-if="item.type == 'video'" class="messagevideo">
|
|
|
<view v-if="item.ifread" class="read">已读</view>
|
|
|
<view v-else class="noread">未读</view>
|
|
|
<myVideo :videoUrl="item.content" />
|
|
|
</view>
|
|
|
<view v-if="item.type == 'audio'" @click="playVoice(item.content, item.ifaudio, index)" class="message">
|
|
|
<view v-if="item.ifread" class="read">已读</view>
|
|
|
<view v-else class="noread">未读</view>
|
|
|
<view class="msg-text voice" :style="{ width: item.time > 2 ? item.time * 40 + 'rpx' : 'auto' }">
|
|
|
{{ item.time }}″
|
|
|
<view class="voicel" v-if="item.ifaudio"><div class="bg voicePlay"></div></view>
|
|
|
<image v-else src="../../static/icon/hornrw.png" class="voice-img" />
|
|
|
</view>
|
|
|
</view>
|
|
|
<view v-if="item.type == 'product'" class="message">
|
|
|
<view v-if="item.ifread" class="read">已读</view>
|
|
|
<view v-else class="noread">未读</view>
|
|
|
<view v-if="item.send" class="msg-product" @click="gotoDetail(item)">
|
|
|
<!-- 商品卡片,略 -->
|
|
|
</view>
|
|
|
</view>
|
|
|
</template>
|
|
|
</view>
|
|
|
</view>
|
|
|
</view>
|
|
|
</scroll-view>
|
|
|
|
|
|
<!-- 商品/订单发送弹窗 -->
|
|
|
<view class="popup" v-if="ifSend && sendInfo">
|
|
|
<view class="tipcon">
|
|
|
<image class="img" :src="sendInfo.pic" mode="aspectFit" />
|
|
|
<view class="info">
|
|
|
<view class="name">
|
|
|
<text class="ntxt">{{ sendInfo.name }}</text>
|
|
|
<text v-if="sendInfo.spData" v-for="(value, key) in sendInfo.spData" :key="key" class="txt">
|
|
|
{{ key }}: {{ value }}
|
|
|
</text>
|
|
|
</view>
|
|
|
<view class="xq">
|
|
|
<view class="list">
|
|
|
<view v-if="sendInfo.type == 3" class="ord">
|
|
|
购买金额:¥ <text style="font-size: 30rpx;">{{ sendInfo.price }}</text>
|
|
|
{{ sendInfo.unit ? '/' + sendInfo.unit : '' }}
|
|
|
</view>
|
|
|
<view v-else class="price">
|
|
|
¥{{ sendInfo.price }}{{ sendInfo.unit ? '/' + sendInfo.unit : '' }}
|
|
|
</view>
|
|
|
<view class="qty">
|
|
|
{{ sendInfo.quantity ? '共(' + sendInfo.quantity + (sendInfo.unit ? sendInfo.unit : '件') + ')' : '' }}
|
|
|
</view>
|
|
|
</view>
|
|
|
<button class="btn" type="primary" @click="sendServer">发送{{ sendInfo.type == 1 ? '商品' : sendInfo.type == 2 ? '课程' : '订单' }}</button>
|
|
|
</view>
|
|
|
<view class="close">
|
|
|
<uni-icons type="closeempty" color="#bebdbd" size="16" @click="closeSendPopup" />
|
|
|
</view>
|
|
|
</view>
|
|
|
</view>
|
|
|
</view>
|
|
|
|
|
|
<!-- 输入组件 -->
|
|
|
<submit @inputs="inputs" @heights="heights" />
|
|
|
|
|
|
<!-- 提示 Toast -->
|
|
|
<u-toast ref="uToast" />
|
|
|
|
|
|
<!-- 群公告弹窗(仅群聊) -->
|
|
|
<uni-popup ref="popup" background-color="#fff" v-if="chatType === 'group'">
|
|
|
<view class="popup-content">
|
|
|
<view class="example">
|
|
|
<view class="order">
|
|
|
<view class="chatlist">
|
|
|
<view class="chatcell" v-for="(person, key) in groupPersons" :key="key">
|
|
|
<image class="user-img" :src="person.img" />
|
|
|
<text class="user-name">{{ person.name }}</text>
|
|
|
</view>
|
|
|
</view>
|
|
|
<view class="lcon">
|
|
|
<view class="lhtxt">群聊名称</view>
|
|
|
<view class="ltxt">{{ chatInfo.chatName || '未设置' }}</view>
|
|
|
</view>
|
|
|
<view class="lcon">
|
|
|
<view class="lhtxt">群公告</view>
|
|
|
<view class="ltxt">{{ chatInfo.notice || '未设置' }}</view>
|
|
|
</view>
|
|
|
<view class="lcon">
|
|
|
<view class="lhtxt">备注</view>
|
|
|
<view class="ltxt">{{ chatInfo.reason ? chatInfo.reason : '无' }}</view>
|
|
|
</view>
|
|
|
<view class="lcon">
|
|
|
<view class="lhtxt">在本群昵称</view>
|
|
|
<view class="ltxt">{{ userName || '' }}</view>
|
|
|
</view>
|
|
|
</view>
|
|
|
</view>
|
|
|
</view>
|
|
|
</uni-popup>
|
|
|
|
|
|
<!-- 已读/未读列表弹窗(仅群聊) -->
|
|
|
<uni-popup ref="popupRead" background-color="#fff" v-if="chatType === 'group'">
|
|
|
<view class="popup-content">
|
|
|
<view class="example">
|
|
|
<view class="order">
|
|
|
<view class="qhbtn">
|
|
|
<view :class="ifRead ? 'btn cur' : 'btn'" @click="switchReadTab(1)">已读({{ readInfo.read.length }})</view>
|
|
|
<view :class="ifRead ? 'btn' : 'btn cur'" @click="switchReadTab(2)">未读({{ readInfo.noread.length }})</view>
|
|
|
</view>
|
|
|
<view class="readlist" v-if="ifRead">
|
|
|
<view class="readcell" v-for="(item, idx) in readInfo.read" :key="idx">
|
|
|
<text class="readname">{{ item.name }}</text>
|
|
|
<text class="readtime">{{ item.time }}</text>
|
|
|
</view>
|
|
|
</view>
|
|
|
<view class="readlist" v-else>
|
|
|
<view class="readcell" v-for="(item, idx) in readInfo.noread" :key="idx">
|
|
|
<text class="readname">{{ item.name }}</text>
|
|
|
</view>
|
|
|
</view>
|
|
|
</view>
|
|
|
</view>
|
|
|
</view>
|
|
|
</uni-popup>
|
|
|
</view>
|
|
|
</template>
|
|
|
|
|
|
<script>
|
|
|
import { myCache } from '../../utils/utils.js';
|
|
|
import dateTime from '@/common/dateTime.js';
|
|
|
import submit from '@/components/chat/submit.vue';
|
|
|
import myVideo from '@/components/myVideo.vue';
|
|
|
|
|
|
const innerAudioContext = uni.createInnerAudioContext();
|
|
|
|
|
|
export default {
|
|
|
components: { submit, myVideo },
|
|
|
data() {
|
|
|
return {
|
|
|
triggered: false,
|
|
|
userName: '',
|
|
|
userId: '',
|
|
|
userHeadImg: '',
|
|
|
chatInfo: {}, // 会话基本信息
|
|
|
// 聊天类型:'private' 或 'group'
|
|
|
chatType: 'private',
|
|
|
// 消息列表
|
|
|
messageList: [],
|
|
|
// 图片预览用
|
|
|
imgMsg: [],
|
|
|
scrollToView: '',
|
|
|
inputh: '90',
|
|
|
ifaudio: false,
|
|
|
// 商品/订单发送
|
|
|
ifSend: false,
|
|
|
sendInfo: null,
|
|
|
// 群聊特有
|
|
|
ifRead: true,
|
|
|
readInfo: { read: [], noread: [] },
|
|
|
// WebSocket相关(页面内独立管理,也可抽离为全局)
|
|
|
isConnected: false,
|
|
|
heartbeatInterval: null,
|
|
|
heartbeatTimeout: 20000,
|
|
|
};
|
|
|
},
|
|
|
onLoad(options) {
|
|
|
// 从缓存获取用户信息
|
|
|
const user = myCache('user');
|
|
|
this.userName = user.nickName || '';
|
|
|
this.userId = user.userid || '';
|
|
|
this.userHeadImg = user.avatar || require('@/static/image/girl.png');
|
|
|
|
|
|
if (options.data) {
|
|
|
this.chatInfo = JSON.parse(decodeURIComponent(options.data));
|
|
|
// 根据会话类型设置 chatType
|
|
|
this.chatType = this.chatInfo.sort === 'groupchat' ? 'group' : 'private';
|
|
|
// 设置标题
|
|
|
uni.setNavigationBarTitle({ title: this.chatInfo.chatName || '聊天' });
|
|
|
|
|
|
// 是否有待发送的商品/订单
|
|
|
if (this.chatInfo.sendinfo) {
|
|
|
this.ifSend = true;
|
|
|
this.sendInfo = this.chatInfo.sendinfo;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 群聊时获取教练、客服等头像
|
|
|
if (this.chatType === 'group') {
|
|
|
this.getGroupMemberInfo();
|
|
|
}
|
|
|
},
|
|
|
onShow() {
|
|
|
this.getMessageList();
|
|
|
// 重新连接WebSocket
|
|
|
this.reconnect();
|
|
|
// 滚动到底部
|
|
|
setTimeout(() => this.scrollToBottom(), 300);
|
|
|
},
|
|
|
methods: {
|
|
|
// ---------- 消息列表加载 ----------
|
|
|
getMessageList() {
|
|
|
const list = myCache(this.chatInfo.chatId) || [];
|
|
|
if (list.length === 0) {
|
|
|
// 首次会话显示欢迎语
|
|
|
const welcome = `Hi,${this.userName},欢迎您!这里是${this.chatInfo.chatName},请问有什么可以帮助您的呢?`;
|
|
|
this.messageList = [{
|
|
|
fromname: this.chatInfo.chatName,
|
|
|
fromuser: this.chatInfo.friendId || this.chatInfo.ownerId,
|
|
|
headimg: this.chatInfo.chatAvatar,
|
|
|
toname: this.userName,
|
|
|
touser: this.userId,
|
|
|
content: welcome,
|
|
|
fromtime: new Date().toISOString(),
|
|
|
type: 'txt',
|
|
|
}];
|
|
|
// 立即缓存
|
|
|
myCache(this.chatInfo.chatId, this.messageList);
|
|
|
} else {
|
|
|
this.messageList = list;
|
|
|
// 处理时间显示(已由缓存保存时处理好)
|
|
|
}
|
|
|
this.$forceUpdate();
|
|
|
// 加载完成后滚动到底部
|
|
|
this.$nextTick(() => this.scrollToBottom());
|
|
|
},
|
|
|
|
|
|
// ---------- WebSocket 连接(合并后统一管理) ----------
|
|
|
socketInit() {
|
|
|
const that = this;
|
|
|
if (this.isConnected) return;
|
|
|
uni.connectSocket({
|
|
|
url: 'wss://www.sanduolantoyoga.com/yoga-imserver',
|
|
|
header: { Authorization: uni.getStorageSync('token') },
|
|
|
success: (res) => {
|
|
|
console.log('socket连接成功', res);
|
|
|
// 拉取离线消息
|
|
|
this.getReadedId();
|
|
|
},
|
|
|
fail: (err) => console.log('socket连接失败', err),
|
|
|
});
|
|
|
uni.onSocketOpen(() => {
|
|
|
that.isConnected = true;
|
|
|
// 登录包
|
|
|
uni.sendSocketMessage({
|
|
|
data: JSON.stringify({ cmd: 0, data: { accessToken: uni.getStorageSync('token') } }),
|
|
|
success: () => console.log('登录包发送成功'),
|
|
|
fail: () => console.log('登录包发送失败'),
|
|
|
});
|
|
|
that.startHeartbeat();
|
|
|
});
|
|
|
uni.onSocketClose(() => {
|
|
|
that.isConnected = false;
|
|
|
that.reconnect();
|
|
|
});
|
|
|
uni.onSocketError(() => {
|
|
|
that.isConnected = false;
|
|
|
that.reconnect();
|
|
|
});
|
|
|
uni.onSocketMessage((res) => {
|
|
|
if (!res.data) return;
|
|
|
const data = JSON.parse(res.data);
|
|
|
console.log('收到消息', data);
|
|
|
// 根据 cmd 分发
|
|
|
if (data.cmd === 3) {
|
|
|
// 私聊
|
|
|
this.handlePrivateMessage(data.data);
|
|
|
} else if (data.cmd === 4) {
|
|
|
// 群聊
|
|
|
this.handleGroupMessage(data.data);
|
|
|
}
|
|
|
});
|
|
|
},
|
|
|
|
|
|
handlePrivateMessage(data) {
|
|
|
// 仅处理发给自己的消息
|
|
|
if (data.recvId != this.userId) return;
|
|
|
if (!this.ifMsgExist(data.id)) {
|
|
|
this.appendMessage(data);
|
|
|
// 已读上报
|
|
|
this.readedPrivate(data.id);
|
|
|
// 更新会话列表
|
|
|
this.updateChatList(data);
|
|
|
}
|
|
|
},
|
|
|
|
|
|
handleGroupMessage(data) {
|
|
|
if (data.groupId != this.chatInfo.groupId) return;
|
|
|
if (!this.ifMsgExist(data.id)) {
|
|
|
this.appendMessage(data);
|
|
|
this.readedGroup();
|
|
|
this.updateChatList(data);
|
|
|
}
|
|
|
},
|
|
|
|
|
|
ifMsgExist(id) {
|
|
|
return this.messageList.some(item => item.id === id);
|
|
|
},
|
|
|
|
|
|
appendMessage(data) {
|
|
|
// 将消息转换为显示格式
|
|
|
const msg = {
|
|
|
fromname: data.sendNickName || '匿名',
|
|
|
fromuser: data.sendId,
|
|
|
headimg: data.headImage || require('@/static/image/girl.png'),
|
|
|
toname: this.userName,
|
|
|
touser: this.userId,
|
|
|
content: data.type === 0 ? decodeURIComponent(data.content) : data.content,
|
|
|
fromtime: data.sendTime,
|
|
|
type: this.mapMsgType(data.type),
|
|
|
id: data.id,
|
|
|
sendId: data.sendId,
|
|
|
recvId: data.recvId,
|
|
|
time: data.type === 3 ? 5 : 0,
|
|
|
ifaudio: false,
|
|
|
read: data.readedCount ? [] : undefined, // 群聊使用
|
|
|
ifread: false, // 私聊使用
|
|
|
send: data.type === 6 ? this.getProduct(data.content) : (data.type === 5 ? this.getOrder(data.content) : null),
|
|
|
};
|
|
|
this.messageList.push(msg);
|
|
|
this.$forceUpdate();
|
|
|
// 缓存
|
|
|
myCache(this.chatInfo.chatId, this.messageList);
|
|
|
this.scrollToBottom();
|
|
|
},
|
|
|
|
|
|
mapMsgType(type) {
|
|
|
const map = {
|
|
|
0: 'txt',
|
|
|
1: 'image',
|
|
|
3: 'audio',
|
|
|
4: 'video',
|
|
|
5: 'product',
|
|
|
6: 'product',
|
|
|
};
|
|
|
return map[type] || 'txt';
|
|
|
},
|
|
|
|
|
|
// ---------- 读写已读状态 ----------
|
|
|
async readedPrivate() {
|
|
|
await uni.$http.put(`/api/message/private/readed?friendId=${this.chatInfo.friendId}`);
|
|
|
},
|
|
|
async readedGroup() {
|
|
|
await uni.$http.put(`/api/message/group/readed?groupId=${this.chatInfo.groupId}`);
|
|
|
},
|
|
|
|
|
|
// ---------- 拉取离线消息 ----------
|
|
|
async getReadedId() {
|
|
|
let url, param;
|
|
|
if (this.chatType === 'private') {
|
|
|
url = '/api/message/private/maxReadedId';
|
|
|
param = { friendId: this.chatInfo.friendId };
|
|
|
} else {
|
|
|
url = '/api/message/group/maxReadedId';
|
|
|
param = { groupId: this.chatInfo.groupId };
|
|
|
}
|
|
|
const { data: res } = await uni.$http.get(url, param);
|
|
|
if (res.data) {
|
|
|
this.chatInfo.minId = res.data;
|
|
|
this.pullOfflineMessages();
|
|
|
}
|
|
|
},
|
|
|
|
|
|
async pullOfflineMessages() {
|
|
|
let url, param;
|
|
|
if (this.chatType === 'private') {
|
|
|
url = '/api/message/private/pullOfflineMessage';
|
|
|
param = { minId: this.chatInfo.minId };
|
|
|
} else {
|
|
|
url = '/api/message/group/pullOfflineMessage';
|
|
|
param = { minId: this.chatInfo.minId };
|
|
|
}
|
|
|
const { data: res } = await uni.$http.get(url, param);
|
|
|
if (res.data && res.data.length) {
|
|
|
// 倒序插入(因为消息按时间升序)
|
|
|
res.data.forEach(item => {
|
|
|
if (!this.ifMsgExist(item.id)) {
|
|
|
const msg = this.convertMsg(item);
|
|
|
this.messageList.push(msg);
|
|
|
}
|
|
|
});
|
|
|
this.messageList.sort((a, b) => a.fromtime - b.fromtime);
|
|
|
myCache(this.chatInfo.chatId, this.messageList);
|
|
|
this.$forceUpdate();
|
|
|
this.scrollToBottom();
|
|
|
}
|
|
|
},
|
|
|
|
|
|
// ---------- 发送消息 ----------
|
|
|
async inputs(e) {
|
|
|
const data = {
|
|
|
fromname: this.userName,
|
|
|
fromuser: this.userId,
|
|
|
headimg: this.userHeadImg,
|
|
|
toname: this.chatInfo.chatName,
|
|
|
touser: this.chatInfo.friendId || this.chatInfo.groupId,
|
|
|
content: e.type === 'audio' ? e.message.voice : e.message,
|
|
|
time: e.type === 'audio' ? e.message.time : 0,
|
|
|
ifaudio: false,
|
|
|
fromtime: new Date(),
|
|
|
type: e.type,
|
|
|
};
|
|
|
if (e.type !== 'video') {
|
|
|
this.messageList.push(data);
|
|
|
this.scrollToBottom();
|
|
|
myCache(this.chatInfo.chatId, this.messageList);
|
|
|
}
|
|
|
// 根据类型上传文件并发送
|
|
|
if (['txt', 'audio', 'image', 'video'].includes(e.type)) {
|
|
|
let content = data.content;
|
|
|
if (e.type !== 'txt') {
|
|
|
// 上传文件
|
|
|
const url = await this.uploadFile(e.message, e.type);
|
|
|
content = url;
|
|
|
}
|
|
|
// 发送消息
|
|
|
await this.sendMsg(content, e.type);
|
|
|
}
|
|
|
},
|
|
|
|
|
|
async uploadFile(filePath, type) {
|
|
|
let url = '';
|
|
|
const token = uni.getStorageSync('token');
|
|
|
const uploadUrl = uni.$http.baseUrl + (type === 'image' ? '/api/image/upload' : '/api/file/upload');
|
|
|
uni.showLoading({ title: '上传中...' });
|
|
|
try {
|
|
|
const res = await new Promise((resolve, reject) => {
|
|
|
uni.uploadFile({
|
|
|
url: uploadUrl,
|
|
|
filePath,
|
|
|
name: 'file',
|
|
|
header: { Authorization: token },
|
|
|
success: (res) => {
|
|
|
const data = JSON.parse(res.data);
|
|
|
if (data.data) resolve(data.data.originUrl || data.data);
|
|
|
else reject(data.msg);
|
|
|
},
|
|
|
fail: reject,
|
|
|
});
|
|
|
});
|
|
|
uni.hideLoading();
|
|
|
return res;
|
|
|
} catch (err) {
|
|
|
uni.hideLoading();
|
|
|
this.$refs.uToast.show({ title: err || '上传失败', type: 'error' });
|
|
|
throw err;
|
|
|
}
|
|
|
},
|
|
|
|
|
|
async sendMsg(content, type) {
|
|
|
const mtypeMap = { txt: '0', image: '1', audio: '3', video: '4' };
|
|
|
const mtype = mtypeMap[type] || '0';
|
|
|
let param = { content };
|
|
|
if (this.chatType === 'private') {
|
|
|
param.recvId = this.chatInfo.friendId;
|
|
|
} else {
|
|
|
param.groupId = this.chatInfo.groupId;
|
|
|
param.atUserIds = [this.chatInfo.friendId, this.chatInfo.teacherId].filter(Boolean);
|
|
|
}
|
|
|
param.type = mtype;
|
|
|
const url = this.chatType === 'private' ? '/api/message/private/send' : '/api/message/group/send';
|
|
|
try {
|
|
|
const res = await uni.$http.post(url, param);
|
|
|
if (res.data) {
|
|
|
const msg = res.data;
|
|
|
// 更新已发送的消息
|
|
|
const last = this.messageList[this.messageList.length - 1];
|
|
|
if (last && last.content === param.content) {
|
|
|
last.id = msg.id;
|
|
|
last.fromtime = msg.sendTime;
|
|
|
last.sendId = msg.sendId;
|
|
|
last.recvId = msg.recvId;
|
|
|
last.ifread = false;
|
|
|
myCache(this.chatInfo.chatId, this.messageList);
|
|
|
}
|
|
|
}
|
|
|
} catch (err) {
|
|
|
this.messageList.pop();
|
|
|
uni.showToast({ title: err.msg || '发送失败', icon: 'none' });
|
|
|
}
|
|
|
},
|
|
|
|
|
|
// ---------- 聊天列表更新(缓存) ----------
|
|
|
updateChatList(data) {
|
|
|
const list = myCache('chatlist-' + this.userId) || [];
|
|
|
const idx = list.findIndex(item => item.id === this.chatInfo.chatId);
|
|
|
const entry = {
|
|
|
id: this.chatInfo.chatId,
|
|
|
content: data.content || '',
|
|
|
type: data.type,
|
|
|
minId: data.id,
|
|
|
datetime: data.sendTime,
|
|
|
name: this.chatInfo.chatName,
|
|
|
userid: this.userId,
|
|
|
fromuser: data.sendId,
|
|
|
img: this.chatInfo.chatAvatar,
|
|
|
sort: this.chatType === 'private' ? 'privatechat' : 'groupchat',
|
|
|
groupId: this.chatInfo.groupId,
|
|
|
friendId: this.chatInfo.friendId,
|
|
|
teacherId: this.chatInfo.teacherId,
|
|
|
};
|
|
|
if (idx >= 0) {
|
|
|
list[idx] = entry;
|
|
|
} else {
|
|
|
list.unshift(entry);
|
|
|
}
|
|
|
myCache('chatlist-' + this.userId, list);
|
|
|
},
|
|
|
|
|
|
// ---------- 工具方法 ----------
|
|
|
changeTime(date) {
|
|
|
return dateTime.dateTime1(date);
|
|
|
},
|
|
|
|
|
|
previewImg(url) {
|
|
|
const idx = this.imgMsg.indexOf(url);
|
|
|
uni.previewImage({
|
|
|
current: idx >= 0 ? idx : 0,
|
|
|
urls: this.imgMsg,
|
|
|
});
|
|
|
},
|
|
|
|
|
|
playVoice(url, ifplay, index) {
|
|
|
innerAudioContext.stop();
|
|
|
this.messageList.forEach((item, i) => (item.ifaudio = false));
|
|
|
ifplay = !ifplay;
|
|
|
this.messageList[index].ifaudio = ifplay;
|
|
|
this.$forceUpdate();
|
|
|
if (ifplay) {
|
|
|
innerAudioContext.src = url;
|
|
|
innerAudioContext.play();
|
|
|
innerAudioContext.onEnded(() => {
|
|
|
this.messageList[index].ifaudio = false;
|
|
|
this.$forceUpdate();
|
|
|
});
|
|
|
}
|
|
|
},
|
|
|
|
|
|
handleImageError(e, index) {
|
|
|
this.messageList[index].headimg = require('@/static/image/girl.png');
|
|
|
},
|
|
|
|
|
|
scrollToBottom() {
|
|
|
const len = this.messageList.length;
|
|
|
if (len) {
|
|
|
this.scrollToView = '';
|
|
|
this.$nextTick(() => {
|
|
|
this.scrollToView = 'msg' + (len - 1);
|
|
|
});
|
|
|
}
|
|
|
},
|
|
|
|
|
|
// 下拉刷新
|
|
|
onPulling() {
|
|
|
this.triggered = true;
|
|
|
setTimeout(() => (this.triggered = false), 2000);
|
|
|
},
|
|
|
onRefresh() {
|
|
|
this.pullOfflineMessages();
|
|
|
},
|
|
|
onRestore() { this.triggered = false; },
|
|
|
onAbort() { this.triggered = false; },
|
|
|
|
|
|
// 输入框高度
|
|
|
heights(h) {
|
|
|
this.inputh = h;
|
|
|
this.scrollToBottom();
|
|
|
},
|
|
|
|
|
|
// 商品/订单弹窗
|
|
|
closeSendPopup() {
|
|
|
this.ifSend = false;
|
|
|
this.sendInfo = null;
|
|
|
},
|
|
|
sendServer() {
|
|
|
// 触发发送商品/订单消息,实际调用 inputs 或直接 sendMsg
|
|
|
const data = {
|
|
|
type: 'product',
|
|
|
message: this.sendInfo.id,
|
|
|
};
|
|
|
this.inputs(data);
|
|
|
},
|
|
|
|
|
|
// 跳转详情
|
|
|
gotoDetail(item) {
|
|
|
const send = item.send;
|
|
|
if (!send) return;
|
|
|
let url = '';
|
|
|
if (send.type === 1) url = `/pages/product/detail?id=${send.id}`;
|
|
|
else if (send.type === 2) url = `/pages/product/cdetail?id=${send.id}`;
|
|
|
else if (send.type === 3) url = `/pages/order/orderinfo?id=${send.orderId}`;
|
|
|
if (url) uni.navigateTo({ url });
|
|
|
},
|
|
|
|
|
|
// 群聊成员信息(客服、教练、咨询用户)
|
|
|
async getGroupMemberInfo() {
|
|
|
const ids = [this.chatInfo.customerService, this.chatInfo.instructor, this.chatInfo.ownerId];
|
|
|
// 实际项目中可批量请求,此处简化,只取当前用户信息
|
|
|
// 为了演示,直接设置占位
|
|
|
this.groupPersons = [
|
|
|
{ name: this.chatInfo.customername || '客服', img: this.chatInfo.customerimg || '/static/image/kfr.png' },
|
|
|
{ name: this.chatInfo.instructorname || '教练', img: this.chatInfo.instructorimg || '/static/image/jltx.png' },
|
|
|
{ name: this.chatInfo.ownername || '咨询用户', img: this.chatInfo.ownerimg || '/static/image/mr.png' },
|
|
|
];
|
|
|
},
|
|
|
|
|
|
// 群聊已读未读弹窗
|
|
|
openRead(item) {
|
|
|
this.ifRead = true;
|
|
|
this.buildReadInfo(item);
|
|
|
this.$refs.popupRead.open('right');
|
|
|
},
|
|
|
openNoRead(item) {
|
|
|
this.ifRead = false;
|
|
|
this.buildReadInfo(item);
|
|
|
this.$refs.popupRead.open('right');
|
|
|
},
|
|
|
buildReadInfo(item) {
|
|
|
// 假设 item.read 存储已读用户id列表,此处仅为示例
|
|
|
// 实际需从服务器获取或本地维护,此处仅演示界面
|
|
|
const readList = item.read || [];
|
|
|
const allUsers = [
|
|
|
{ id: this.chatInfo.customerService, name: this.chatInfo.customername || '客服' },
|
|
|
{ id: this.chatInfo.instructor, name: this.chatInfo.instructorname || '教练' },
|
|
|
{ id: this.chatInfo.ownerId, name: this.chatInfo.ownername || '咨询用户' },
|
|
|
];
|
|
|
this.readInfo.read = allUsers.filter(u => readList.includes(u.id)).map(u => ({ ...u, time: new Date() }));
|
|
|
this.readInfo.noread = allUsers.filter(u => !readList.includes(u.id));
|
|
|
},
|
|
|
switchReadTab(type) {
|
|
|
this.ifRead = type === 1;
|
|
|
},
|
|
|
|
|
|
// WebSocket 心跳/重连(合并,与上述 socketInit 配合)
|
|
|
startHeartbeat() {
|
|
|
clearInterval(this.heartbeatInterval);
|
|
|
this.heartbeatInterval = setInterval(() => {
|
|
|
if (this.isConnected) {
|
|
|
uni.sendSocketMessage({
|
|
|
data: JSON.stringify({ cmd: 1, data: { accessToken: uni.getStorageSync('token') } }),
|
|
|
fail: () => this.reconnect(),
|
|
|
});
|
|
|
}
|
|
|
}, this.heartbeatTimeout);
|
|
|
},
|
|
|
reconnect() {
|
|
|
if (!this.isConnected) {
|
|
|
clearTimeout(this.heartbeatInterval);
|
|
|
this.heartbeatInterval = setTimeout(() => this.socketInit(), this.heartbeatTimeout);
|
|
|
}
|
|
|
},
|
|
|
|
|
|
// 获取商品/订单详情(略,实际调用接口)
|
|
|
async getProduct(id) {
|
|
|
// 模拟
|
|
|
return { type: 1, id, name: '商品名', pic: '/static/image/nopic.png', price: 100, unit: '元' };
|
|
|
},
|
|
|
async getOrder(id) {
|
|
|
return { type: 3, id, orderId: '123', name: '订单', pic: '/static/image/nopic.png', price: 200, quantity: 1 };
|
|
|
},
|
|
|
},
|
|
|
};
|
|
|
</script>
|
|
|
|
|
|
<style lang="scss">
|
|
|
page {
|
|
|
height: 100%;
|
|
|
}
|
|
|
|
|
|
.content {
|
|
|
height: 100%;
|
|
|
background-color: rgba(244, 244, 244, 1);
|
|
|
position: relative;
|
|
|
}
|
|
|
::v-deep .uni-noticebar{
|
|
|
margin-bottom: 0 !important;
|
|
|
padding: 10rpx 12rpx !important;
|
|
|
border-radius: 10rpx;
|
|
|
}
|
|
|
|
|
|
.chat {
|
|
|
height: 100%;
|
|
|
|
|
|
.chat-main {
|
|
|
padding-left: 20rpx;
|
|
|
padding-right: 20rpx;
|
|
|
padding-top: 20rpx;
|
|
|
// padding-bottom: 120rpx; //获取动态高度
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
}
|
|
|
|
|
|
.chat-ls {
|
|
|
.chat-time {
|
|
|
font-size: 24rpx;
|
|
|
color: rgba(39, 40, 50, 0.3);
|
|
|
line-height: 34rpx;
|
|
|
padding: 10rpx 0rpx;
|
|
|
text-align: center;
|
|
|
}
|
|
|
|
|
|
.msg-m {
|
|
|
display: flex;
|
|
|
padding: 20rpx 0;
|
|
|
|
|
|
.user-img {
|
|
|
flex: none;
|
|
|
width: 92rpx;
|
|
|
height: 90rpx;
|
|
|
border-radius: 20rpx;
|
|
|
text-align: center;
|
|
|
.uimg{
|
|
|
width: 70rpx;
|
|
|
height: 70rpx;
|
|
|
margin: 0 auto;
|
|
|
border-radius: 35rpx;
|
|
|
}
|
|
|
.uname{
|
|
|
height: 30rpx;
|
|
|
display: block;
|
|
|
margin: 0 auto;
|
|
|
line-height: 30rpx;
|
|
|
color: rgba(39, 40, 50, 0.5);
|
|
|
font-size: 24rpx;
|
|
|
}
|
|
|
}
|
|
|
.message {
|
|
|
flex: none;
|
|
|
max-width: 540rpx;
|
|
|
display: flex;
|
|
|
flex-direction: row;
|
|
|
}
|
|
|
.messagevideo {
|
|
|
max-width: 540rpx;
|
|
|
height: 300rpx;
|
|
|
display: flex;
|
|
|
flex-direction: row;
|
|
|
}
|
|
|
.msg-text {
|
|
|
font-size: 32rpx;
|
|
|
color: rgba(39, 40, 50, 1);
|
|
|
line-height: 44rpx;
|
|
|
padding: 18rpx 24rpx;
|
|
|
}
|
|
|
|
|
|
.msg-img {
|
|
|
max-width: 400rpx;
|
|
|
border-radius: 20rpx;
|
|
|
}
|
|
|
.msg-video{
|
|
|
width: 400rpx;
|
|
|
height: 300rpx;
|
|
|
border-radius: 20rpx;
|
|
|
}
|
|
|
.msg-map {
|
|
|
background: #fff;
|
|
|
width: 464rpx;
|
|
|
height: 284rpx;
|
|
|
overflow: hidden;
|
|
|
margin: 0 20rpx;
|
|
|
.map-name {
|
|
|
font-size: 32rpx;
|
|
|
color: rgba(39, 40, 50, 1);
|
|
|
line-height: 44rpx;
|
|
|
padding: 18rpx 24rpx 0 24rpx;
|
|
|
//下面四行是单行文字的样式
|
|
|
display: -webkit-box;
|
|
|
-webkit-box-orient: vertical;
|
|
|
-webkit-line-clamp: 1;
|
|
|
overflow: hidden;
|
|
|
}
|
|
|
.map-address {
|
|
|
font-size: 24rpx;
|
|
|
color: rgba(39, 40, 50, 0.4);
|
|
|
padding: 0 24rpx;
|
|
|
//下面四行是单行文字的样式
|
|
|
display: -webkit-box;
|
|
|
-webkit-box-orient: vertical;
|
|
|
-webkit-line-clamp: 1;
|
|
|
overflow: hidden;
|
|
|
}
|
|
|
|
|
|
.map {
|
|
|
padding-top: 8rpx;
|
|
|
width: 464rpx;
|
|
|
height: 190rpx;
|
|
|
}
|
|
|
}
|
|
|
.voice {
|
|
|
// width: 200rpx;
|
|
|
min-width: 100rpx;
|
|
|
max-width: 400rpx;
|
|
|
}
|
|
|
.voice-img {
|
|
|
width: 36rpx;
|
|
|
height: 36rpx;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.msg-left {
|
|
|
flex-direction: row;
|
|
|
.feed-imgy{
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
justify-content: center;
|
|
|
align-self: center;
|
|
|
margin-right: 0rpx;
|
|
|
width: 95rpx;
|
|
|
height: 75rpx;
|
|
|
}
|
|
|
.feed-img{
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
justify-content: center;
|
|
|
align-self: center;
|
|
|
margin-right: 0rpx;
|
|
|
width: 75rpx;
|
|
|
height: 75rpx;
|
|
|
}
|
|
|
.msg-text {
|
|
|
margin-left: 16rpx;
|
|
|
background-color: #fff;
|
|
|
border-radius: 0rpx 20rpx 20rpx 20rpx;
|
|
|
}
|
|
|
|
|
|
.ms-img {
|
|
|
margin-left: 16rpx;
|
|
|
}
|
|
|
.msg-video{
|
|
|
margin-left: 16rpx;
|
|
|
}
|
|
|
.msh-map {
|
|
|
margin-left: 16rpx;
|
|
|
border-radius: 0rpx 20rpx 20rpx 20rpx;
|
|
|
}
|
|
|
|
|
|
.msg-img {
|
|
|
margin: 0 0 0 20rpx;
|
|
|
}
|
|
|
|
|
|
.voice {
|
|
|
text-align: left;
|
|
|
display: flex;
|
|
|
}
|
|
|
.voice-img {
|
|
|
transform: rotate(180deg);
|
|
|
width: 36rpx;
|
|
|
height: 36rpx;
|
|
|
padding-bottom: 4rpx;
|
|
|
margin-right: 10rpx;
|
|
|
}
|
|
|
.voicel{
|
|
|
height: 32rpx;
|
|
|
width: 32rpx;
|
|
|
}
|
|
|
.bg {
|
|
|
background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAAAYCAYAAAAF6fiUAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoV2luZG93cykiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6MEM0NzgwODc1MDY0MTFFRjk1QjhFRUUxQjYyOUQ5NDQiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6MEM0NzgwODg1MDY0MTFFRjk1QjhFRUUxQjYyOUQ5NDQiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDowQzQ3ODA4NTUwNjQxMUVGOTVCOEVFRTFCNjI5RDk0NCIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDowQzQ3ODA4NjUwNjQxMUVGOTVCOEVFRTFCNjI5RDk0NCIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PqxjhsEAAANzSURBVHjazJlLaNRQFIaTVIcRQUELoggVKYIgQvGFr40giLMRBAtjXblwUSmIK1vBTQUREUFB8NGdGwUXgoIVFwq6EGSkOAguXKiUQQq6kKK0Gv+LJ+HkTpLJfYUc+DnJ3M436Tn3eeKHYeiZ2NDQkGfZfCj03Nka6DzUgqZMYa1WS45HzIf15C/xqmeuE7ATOs3up1zxRefslYSgggn465j/GLpJ13ehfS75SEIu36/AFCQ6wQHoBbTgIOBZfBGow9AvaIXBb8d89PYFFpcEn7e5GAG+wXfPQc+g91DNQQKy+A3oB1SnkWDMR9BrbG0oxLeVgNAgCeLhF6FN0CuLiS3CP07+BDTgko/kbHC9BugG6w20ja63QxNSYk0tj/8EatP1uOYuKMFHoCdYW09+kDKnT0Kz0KjGKChik9AsxPkz0FnWvtIgsar8y2wUBDp8BDrBR+zS+CP4PCgyApZDa6Eb+ELDQQJiPs3DkV2FOnQ9atDjVfkPyS+DduvykQQtfpAypM7A3aPbB0iC7bNCgi+dRW6RP2owDanyf0Lv6HqPKl+KTxcf8czlBxnz2gjcPGXtiIOdSRb/Ofmt1KY7DanyZ8gP2uIjMYX4eXPeNPm9jg5Eafwv7IS+rkT+N/Kry+bnJeA3+T5HCUjjhxa3oCr8UOM3rfDzEnCQ/Fu2Q7K5be3i0+IWWadEfj/572Xzg4zywhW4VWwhs128y+LvJ9+mxbEs/mbyn2zxafHtyU87B4yzPe1JgOZZ8x8LwU/waTGL7BT5RwZTkSq/Rge06NCmxJfi08Wn8kQmP20ErKc56xIvpQJUx72NBMR8L1kKbkIb6fq6FKDQIb9BI1v8by91+YiPFj+1GgpYP4I9x+593IcZ05VOEsScOMfuxUuMr/Sg4uHHWNtSjUqlCv81HZDuQ8OqfPFChl7CxHx8NsbiE/Px+XChBKiYhXL0IPUMsYB9lopiddpthI74O9i0sIXVbbT5CPIAi02Cj7a2y2KcrjXp4UXpdpfUtmihIJfHjxbQp5rBL8xPC77NXY2JXaOeOS1t3fooAa74t9loaNrgI8gd1vsL8f0KvpSPdj4u3wuLncxFuj4mbSWVTH4pz3aSMR9/k8mv4jthz3HwD7HgXDAJfkaHTPDzgl+VKahs+wjdgT54/0vUzvhUos61fwIMANiWariLn8/jAAAAAElFTkSuQmCC);
|
|
|
width: 32rpx;
|
|
|
height: 32rpx;
|
|
|
background-size: 400%;
|
|
|
}
|
|
|
|
|
|
.msg-product{
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
width: 100%;
|
|
|
background-color: #FFFFFF;
|
|
|
padding: 10rpx;
|
|
|
border-radius: 16rpx;
|
|
|
width: 480rpx;
|
|
|
margin-left: 12rpx;
|
|
|
.zx{
|
|
|
display: flex;
|
|
|
justify-content: space-between;
|
|
|
width: 100%;
|
|
|
font-size: 28rpx;
|
|
|
margin: 10rpx;
|
|
|
.zxview{
|
|
|
color: darkgrey;
|
|
|
font-size: 26rpx;
|
|
|
margin-right: 20rpx;
|
|
|
}
|
|
|
}
|
|
|
.msg-con{
|
|
|
display: flex;
|
|
|
justify-content: space-between;
|
|
|
width: 100%;
|
|
|
padding: 10rpx;
|
|
|
border-radius: 16rpx;
|
|
|
}
|
|
|
.img{
|
|
|
width: 120rpx;
|
|
|
height: 120rpx;
|
|
|
}
|
|
|
.info{
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
flex: 1;
|
|
|
padding: 6rpx 8rpx;
|
|
|
position: relative;
|
|
|
margin-left: 10rpx;
|
|
|
background-color: #f4f4f4;
|
|
|
border-radius: 0 10rpx 10rpx 0;
|
|
|
.name{
|
|
|
width: 330rpx;
|
|
|
// white-space: nowrap; /* 防止文本换行 */
|
|
|
// overflow: hidden; /* 隐藏溢出的内容 */
|
|
|
// text-overflow: ellipsis; /* 溢出部分显示省略号 */
|
|
|
.txt{
|
|
|
font-size: 26rpx;
|
|
|
color: darkgrey;
|
|
|
margin-left: 6rpx;
|
|
|
}
|
|
|
.ntxt{
|
|
|
font-size: 28rpx;
|
|
|
color: #000;
|
|
|
}
|
|
|
}
|
|
|
.sku{
|
|
|
display: flex;
|
|
|
flex-direction: row;
|
|
|
.txt{
|
|
|
font-size: 26rpx;
|
|
|
color: darkgrey;
|
|
|
margin-right: 6rpx;
|
|
|
}
|
|
|
}
|
|
|
.xq{
|
|
|
width: 100%;
|
|
|
display: flex;
|
|
|
flex-direction: row;
|
|
|
.list{
|
|
|
display: flex;
|
|
|
flex: 1;
|
|
|
}
|
|
|
.price{
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
font-size: 28rpx;
|
|
|
color: #ff007f;
|
|
|
}
|
|
|
.ord{
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
font-size: 26rpx;
|
|
|
color: #232323;
|
|
|
}
|
|
|
.qty{
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
font-size: 26rpx;
|
|
|
color: darkgrey;
|
|
|
margin-left: 8rpx;
|
|
|
}
|
|
|
.btn{
|
|
|
font-size: 26rpx;
|
|
|
font-family: PingFang SC-Medium, PingFang SC;
|
|
|
font-weight: 500;
|
|
|
color: #FCFCFD;
|
|
|
width: 110rpx;
|
|
|
height: 50rpx;
|
|
|
line-height: 50rpx;
|
|
|
background: #ff007f;
|
|
|
box-shadow: 0rpx 10rpx 30rpx 2rpx rgba(255,0,127,0.2);
|
|
|
border-radius: 60rpx;
|
|
|
opacity: 1;
|
|
|
margin-right: 10rpx;
|
|
|
margin-top: 10rpx;
|
|
|
margin-bottom: 10rpx;
|
|
|
padding-left:0 !important;
|
|
|
padding-right:0 !important;
|
|
|
&::after{
|
|
|
border:none;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.msg-right {
|
|
|
flex-direction: row-reverse;
|
|
|
.rcon{
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
justify-content: flex-end;
|
|
|
align-items: flex-end;
|
|
|
}
|
|
|
.read{
|
|
|
font-size: 24rpx;
|
|
|
color: #888;
|
|
|
margin-right: 10rpx;
|
|
|
display: flex;
|
|
|
justify-content: flex-end;
|
|
|
align-items: flex-end;
|
|
|
}
|
|
|
.noread{
|
|
|
font-size: 24rpx;
|
|
|
color: #00a89b;
|
|
|
margin-right: 10rpx;
|
|
|
display: flex;
|
|
|
justify-content: flex-end;
|
|
|
align-items: flex-end;
|
|
|
}
|
|
|
|
|
|
.feed-imgy{
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
justify-content: center;
|
|
|
align-self: center;
|
|
|
margin-right: 0rpx;
|
|
|
width: 95rpx;
|
|
|
height: 75rpx;
|
|
|
}
|
|
|
.feed-img{
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
justify-content: center;
|
|
|
align-self: center;
|
|
|
margin-right: 0rpx;
|
|
|
width: 75rpx;
|
|
|
height: 75rpx;
|
|
|
}
|
|
|
.msg-text {
|
|
|
margin-right: 16rpx;
|
|
|
background-color: #00a89b;
|
|
|
font-family: PingFang SC, PingFang SC;
|
|
|
font-weight: 400;
|
|
|
font-size: 32rpx;
|
|
|
color: #FFFFFF;
|
|
|
line-height: 48rpx;
|
|
|
border-radius: 20rpx 0rpx 20rpx 20rpx;
|
|
|
}
|
|
|
|
|
|
.ms-img {
|
|
|
margin-right: 16rpx;
|
|
|
}
|
|
|
|
|
|
.msg-img {
|
|
|
margin: 0 20rpx 0 0;
|
|
|
}
|
|
|
.msg-video{
|
|
|
margin: 0 20rpx 0 0;
|
|
|
}
|
|
|
.msh-map {
|
|
|
margin-left: 16rpx;
|
|
|
border-radius: 20rpx 0rpx 20rpx 20rpx;
|
|
|
}
|
|
|
|
|
|
.voice {
|
|
|
display: flex;
|
|
|
flex-direction: row;
|
|
|
align-items: center;
|
|
|
justify-content: flex-end;
|
|
|
}
|
|
|
|
|
|
.voice-img {
|
|
|
display: block;
|
|
|
width: 36rpx;
|
|
|
height: 36rpx;
|
|
|
margin-left: 10rpx;
|
|
|
}
|
|
|
.voicel{
|
|
|
height: 32rpx;
|
|
|
width: 32rpx;
|
|
|
}
|
|
|
.bg {
|
|
|
background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAAAYCAYAAAAF6fiUAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoV2luZG93cykiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6REJBQzQ1RjQ1MDYzMTFFRjk5QjZEQkI5MDlENzE3RTQiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6REJBQzQ1RjU1MDYzMTFFRjk5QjZEQkI5MDlENzE3RTQiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDpEQkFDNDVGMjUwNjMxMUVGOTlCNkRCQjkwOUQ3MTdFNCIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDpEQkFDNDVGMzUwNjMxMUVGOTlCNkRCQjkwOUQ3MTdFNCIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/Pm5p+OoAAAKmSURBVHja1Jk/SBxBFMbdy6mHSEARNYIkhU0ICCEkEEynEYmkUEiTxmBtZZHCgGVsAgEhQozYaAo7G0GwjgemC1ZBgzbaKKnEIsb1HbwjH8PM7szdfEQffMzyzexvhn2zM/snSdO0wSOmRfdFc6JfDfGDzb++UUlAjmbTfzHi0T5UbP61Vl6DV3BxZggDYPNRyU1LQDtcnC9GXY/ojeh2HZ2z+ahbZD4lAZt6cQ4Mv1v0W+tm6+icza+qSOZTEvAAZudjo+5Q/SNRX40ds/m47JSIfFoC1nSQ24Y/r/4fUZdR1xHQMZtfVSOZH7LndPgmoEl0oQMdA/8OzNrXxjlzokvRgsfA2HxUM5nvUsmXbzt5CGZJE/jv1d832k8GPsmw+TgDEyLfd8PP5NsAM9q4bPi76r8FrwXgHzwHyOa7loDY/Kw73JtvAyzpCcvgtQLoKfgT6p0GDJDNtyWAwXepEMIvWF6O27Q8Aa8bjo/h+JGWWwEv32y+Ldh8jMsQvi0B1Y9DicUz/b9aNod8/SDzs/pk8V2Ry7cl4FTLTvCORBd63Av+Ny2HAwbF5tuCzXdFLt+WgD0t+8E7F/3Q40Hw17WuRbTqOSg23zbrGXyfyOdbNoYB2LBaLU8vx0b7UWj/0WOTYvNdGzGD76NMvu0OKGvWKjEO/ifY0KbB3xBN6eZ25jEr2HxXxOQnAf1m8x1ZW9SM7Rr+O8hmfx2z4jOZ71IsfoH9LegeDPSFUfcd3mSf1NjxXTI/64UsBj/av4WC47Y5EK3o8VejbkD0U1QUPa9xOTgk812bcSx+GrgM1fRLsvIl8Vxny4albqjOW5HNz+ubyY/2S/IZLBULhAGz+Tf+n7D5Ne8lYRBsPmXzjKWixyq1rOVD0Q7hZYXNz9oP/ntcCTAAHScbp0OSlTgAAAAASUVORK5CYII=);
|
|
|
width: 32rpx;
|
|
|
height: 32rpx;
|
|
|
background-size: 400%;
|
|
|
}
|
|
|
|
|
|
.msg-product{
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
width: 100%;
|
|
|
background-color: #FFFFFF;
|
|
|
padding: 10rpx;
|
|
|
border-radius: 16rpx;
|
|
|
width: 480rpx;
|
|
|
margin-right: 12rpx;
|
|
|
.zx{
|
|
|
display: flex;
|
|
|
justify-content: space-between;
|
|
|
width: 100%;
|
|
|
font-size: 28rpx;
|
|
|
margin: 10rpx;
|
|
|
.zxview{
|
|
|
color: darkgrey;
|
|
|
font-size: 26rpx;
|
|
|
margin-right: 20rpx;
|
|
|
}
|
|
|
}
|
|
|
.msg-con{
|
|
|
display: flex;
|
|
|
justify-content: space-between;
|
|
|
width: 100%;
|
|
|
padding: 10rpx;
|
|
|
border-radius: 16rpx;
|
|
|
}
|
|
|
.img{
|
|
|
width: 120rpx;
|
|
|
height: 120rpx;
|
|
|
}
|
|
|
.info{
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
flex: 1;
|
|
|
padding: 6rpx 8rpx;
|
|
|
position: relative;
|
|
|
margin-left: 10rpx;
|
|
|
background-color: #f4f4f4;
|
|
|
border-radius: 0 10rpx 10rpx 0;
|
|
|
.name{
|
|
|
width: 330rpx;
|
|
|
// white-space: nowrap; /* 防止文本换行 */
|
|
|
// overflow: hidden; /* 隐藏溢出的内容 */
|
|
|
// text-overflow: ellipsis; /* 溢出部分显示省略号 */
|
|
|
.txt{
|
|
|
font-size: 26rpx;
|
|
|
color: darkgrey;
|
|
|
margin-left: 6rpx;
|
|
|
}
|
|
|
.ntxt{
|
|
|
font-size: 28rpx;
|
|
|
color: #000;
|
|
|
}
|
|
|
}
|
|
|
.sku{
|
|
|
display: flex;
|
|
|
flex-direction: row;
|
|
|
.txt{
|
|
|
font-size: 26rpx;
|
|
|
color: darkgrey;
|
|
|
margin-right: 6rpx;
|
|
|
}
|
|
|
}
|
|
|
.xq{
|
|
|
width: 100%;
|
|
|
display: flex;
|
|
|
flex-direction: row;
|
|
|
.list{
|
|
|
display: flex;
|
|
|
flex: 1;
|
|
|
}
|
|
|
.price{
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
font-size: 28rpx;
|
|
|
color: #ff007f;
|
|
|
}
|
|
|
.ord{
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
font-size: 26rpx;
|
|
|
color: #232323;
|
|
|
}
|
|
|
.qty{
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
font-size: 26rpx;
|
|
|
color: darkgrey;
|
|
|
margin-left: 8rpx;
|
|
|
}
|
|
|
.btn{
|
|
|
font-size: 26rpx;
|
|
|
font-family: PingFang SC-Medium, PingFang SC;
|
|
|
font-weight: 500;
|
|
|
color: #FCFCFD;
|
|
|
width: 110rpx;
|
|
|
height: 50rpx;
|
|
|
line-height: 50rpx;
|
|
|
background: #ff007f;
|
|
|
box-shadow: 0rpx 10rpx 30rpx 2rpx rgba(255,0,127,0.2);
|
|
|
border-radius: 60rpx;
|
|
|
opacity: 1;
|
|
|
margin-right: 10rpx;
|
|
|
margin-top: 10rpx;
|
|
|
margin-bottom: 10rpx;
|
|
|
padding-left:0 !important;
|
|
|
padding-right:0 !important;
|
|
|
&::after{
|
|
|
border:none;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.voicePlay {
|
|
|
animation-name: voicePlay;
|
|
|
animation-duration: 1s;
|
|
|
animation-direction: normal;
|
|
|
animation-iteration-count: infinite;
|
|
|
animation-timing-function: steps(3);
|
|
|
}
|
|
|
|
|
|
@keyframes voicePlay {
|
|
|
0% {
|
|
|
background-position: 0;
|
|
|
}
|
|
|
100% {
|
|
|
background-position: 100%;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.popup{
|
|
|
position: fixed;
|
|
|
bottom: 180rpx;
|
|
|
/* #ifdef H5 */
|
|
|
bottom: 100rpx;
|
|
|
/* #endif */
|
|
|
left: 0;
|
|
|
width: 100%;
|
|
|
padding: 18rpx;
|
|
|
border-radius: 16rpx;
|
|
|
.tipcon{
|
|
|
display: flex;
|
|
|
justify-content: space-between;
|
|
|
width: 100%;
|
|
|
background-color: #FFFFFF;
|
|
|
padding: 16rpx;
|
|
|
border-radius: 16rpx;
|
|
|
.img{
|
|
|
width: 100rpx;
|
|
|
height: 100rpx;
|
|
|
}
|
|
|
.info{
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
flex: 1;
|
|
|
margin-left: 16rpx;
|
|
|
position: relative;
|
|
|
.name{
|
|
|
width: 550rpx;
|
|
|
// white-space: nowrap; /* 防止文本换行 */
|
|
|
// overflow: hidden; /* 隐藏溢出的内容 */
|
|
|
// text-overflow: ellipsis; /* 溢出部分显示省略号 */
|
|
|
margin-right: 50rpx;
|
|
|
.txt{
|
|
|
font-size: 26rpx;
|
|
|
color: darkgrey;
|
|
|
margin-left: 6rpx;
|
|
|
}
|
|
|
.ntxt{
|
|
|
font-size: 28rpx;
|
|
|
color: #000;
|
|
|
}
|
|
|
}
|
|
|
.sku{
|
|
|
display: flex;
|
|
|
flex-direction: row;
|
|
|
.txt{
|
|
|
font-size: 26rpx;
|
|
|
color: darkgrey;
|
|
|
margin-right: 6rpx;
|
|
|
}
|
|
|
}
|
|
|
.xq{
|
|
|
width: 100%;
|
|
|
display: flex;
|
|
|
flex-direction: row;
|
|
|
.list{
|
|
|
display: flex;
|
|
|
flex: 1;
|
|
|
align-items: flex-end;
|
|
|
}
|
|
|
.price{
|
|
|
font-size: 28rpx;
|
|
|
color: #ff007f;
|
|
|
}
|
|
|
.ord{
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
font-size: 26rpx;
|
|
|
color: #232323;
|
|
|
}
|
|
|
.qty{
|
|
|
font-size: 26rpx;
|
|
|
color: darkgrey;
|
|
|
margin-left: 6rpx;
|
|
|
}
|
|
|
.btn{
|
|
|
font-size: 26rpx;
|
|
|
font-family: PingFang SC-Medium, PingFang SC;
|
|
|
font-weight: 500;
|
|
|
color: #FCFCFD;
|
|
|
width: 150rpx;
|
|
|
height: 50rpx;
|
|
|
line-height: 50rpx;
|
|
|
background: #ff007f;
|
|
|
box-shadow: 0rpx 10rpx 30rpx 2rpx rgba(255,0,127,0.2);
|
|
|
border-radius: 10rpx;
|
|
|
opacity: 1;
|
|
|
margin-right: 10rpx;
|
|
|
padding-left:0 !important;
|
|
|
padding-right:0 !important;
|
|
|
&::after{
|
|
|
border:none;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
.close{
|
|
|
position: absolute;
|
|
|
top: 0;
|
|
|
right: 6rpx;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.example {
|
|
|
width: 550rpx;
|
|
|
opacity: 1;
|
|
|
padding: 0;
|
|
|
box-sizing: border-box;
|
|
|
.order {
|
|
|
position: relative;
|
|
|
overflow: hidden;
|
|
|
display: block;
|
|
|
background-color: #ffffff;
|
|
|
margin: 0 0 10rpx;
|
|
|
box-sizing: border-box;
|
|
|
padding: 0rpx 20rpx;
|
|
|
}
|
|
|
.qhbtn{
|
|
|
width: 100%;
|
|
|
display: flex;
|
|
|
flex-direction: row;
|
|
|
.btn{
|
|
|
width: 120rpx;
|
|
|
height: 60rpx;
|
|
|
border-radius: 20rpx;
|
|
|
background-color: #eee;
|
|
|
color: #000;
|
|
|
font-size: 26rpx;
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
justify-content: center;
|
|
|
margin-right: 16rpx;
|
|
|
margin-top: 16rpx;
|
|
|
}
|
|
|
.cur{
|
|
|
background-color: #00a89b;
|
|
|
color: #fff;
|
|
|
}
|
|
|
}
|
|
|
.readlist{
|
|
|
display: flex;
|
|
|
flex-wrap: wrap;
|
|
|
padding: 20rpx 0 0 10rpx ;
|
|
|
}
|
|
|
.readcell{
|
|
|
display: flex;
|
|
|
justify-content: space-between;
|
|
|
width: 100%;
|
|
|
padding: 16rpx 0;
|
|
|
border-bottom: 1rpx solid #eee;
|
|
|
.readtime{
|
|
|
font-size: 24rpx;
|
|
|
color: #888;
|
|
|
}
|
|
|
.readname{
|
|
|
font-size: 26rpx;
|
|
|
color: #33383c;
|
|
|
display: flex;
|
|
|
flex: 1;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.chatlist{
|
|
|
display: flex;
|
|
|
flex-wrap: wrap;
|
|
|
padding: 20rpx 0 0 10rpx ;
|
|
|
}
|
|
|
.chatcell{
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
min-width: 90rpx;
|
|
|
height: 150rpx;
|
|
|
align-items: center;
|
|
|
justify-content: center;
|
|
|
margin-right: 30rpx;
|
|
|
.user-img{
|
|
|
width: 90rpx;
|
|
|
height: 90rpx;
|
|
|
border-radius: 5rpx;
|
|
|
}
|
|
|
.user-name{
|
|
|
font-size: 26rpx;
|
|
|
color: #33383c;
|
|
|
margin-top: 16rpx;
|
|
|
}
|
|
|
}
|
|
|
.lcon{
|
|
|
padding: 20rpx 10rpx;
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
border-bottom: 1rpx solid #eee;
|
|
|
.lhtxt{
|
|
|
font-size: 30rpx;
|
|
|
color: #33383c;
|
|
|
line-height: 40rpx;
|
|
|
}
|
|
|
.ltxt{
|
|
|
margin-top: 16rpx;
|
|
|
font-size: 28rpx;
|
|
|
color: #888;
|
|
|
line-height: 30rpx;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
</style>
|