You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

500 lines
12 KiB

2 months ago
<template>
1 week ago
<view class="fish-chat-page">
<view class="nav" :style="{ paddingTop: statusBarTop + 'px' }">
<view class="back" @tap="back"><uni-icons type="left" size="24" color="#15362f"></uni-icons></view>
<view class="nav-main">
<view class="nav-title">{{peerName}}</view>
<view class="nav-sub">校园二手咨询 · 独立聊天</view>
2 months ago
</view>
</view>
1 week ago
<view class="goods-card" v-if="goods" @tap="goGoods">
<image class="goods-cover" :src="goods.coverImage || imageList[0] || defaultAvatar" mode="aspectFill"></image>
<view class="goods-main">
<view class="goods-title">{{goods.title}}</view>
<view class="goods-price">¥{{goods.price}}</view>
2 months ago
</view>
1 week ago
<view class="goods-link">查看 </view>
2 months ago
</view>
1 week ago
<scroll-view
class="message-scroll"
scroll-y
:scroll-into-view="scrollIntoView"
:scroll-with-animation="true"
@scrolltoupper="loadOlder"
>
<view class="load-tip" v-if="loadingOlder">加载中...</view>
<view class="safe-tip">请勿发送涉黄涉暴辱骂诈骗等内容文字和图片会进行内容审核</view>
<view
class="message-row"
:class="{ mine: item.mine }"
v-for="item in messages"
:key="item.id"
:id="'msg-' + item.id"
>
<image class="avatar" :src="item.mine ? myAvatar : peerAvatar" mode="aspectFill"></image>
<view class="message-main">
<view class="bubble text-bubble" :class="{ failed: item.localState == 'failed' }" v-if="item.messageType == 1">{{item.content}}</view>
<view class="bubble image-bubble" :class="{ failed: item.localState == 'failed' }" v-else>
<image :src="item.content" mode="aspectFill" @tap="previewImage(item.content)"></image>
</view>
<view class="message-status" v-if="item.mine && item.localState">{{statusText(item)}}</view>
2 months ago
</view>
</view>
1 week ago
<view id="chat-bottom" class="bottom-anchor"></view>
</scroll-view>
<view class="input-bar">
<view class="image-btn" @tap="chooseImage">
<uni-icons type="image" size="24" color="#61736e"></uni-icons>
2 months ago
</view>
1 week ago
<input
v-model.trim="draft"
confirm-type="send"
placeholder="请输入消息..."
@confirm="sendText"
/>
<view class="send-btn" @tap="sendText">发送</view>
2 months ago
</view>
</view>
2 months ago
</template>
<script>
1 week ago
import fishMarketSocket from '@/common/fishMarketSocket.js'
import {
getFishGoodsDetail,
getFishChatSession,
pageFishChatMessages,
uploadFishImage
} from '@/common/fishMarketApi.js'
2 months ago
export default {
data() {
return {
1 week ago
statusBarTop: 24,
sessionId: '',
goodsId: '',
session: null,
goods: null,
messages: [],
pageNum: 1,
pageSize: 20,
totalPages: 1,
loadingOlder: false,
draft: '',
scrollIntoView: 'chat-bottom',
selfId: String(uni.getStorageSync('id') || ''),
myAvatar: uni.getStorageSync('avatar') || 'https://jewel-shop.oss-cn-beijing.aliyuncs.com/801c569079da4540990c1cc634186fdd.png',
defaultAvatar: 'https://jewel-shop.oss-cn-beijing.aliyuncs.com/801c569079da4540990c1cc634186fdd.png',
socketReady: false
2 months ago
}
},
1 week ago
computed: {
imageList() {
if (!this.goods || !this.goods.imageList) return []
return this.goods.imageList
},
peerName() {
if (!this.goods || !this.session) return '校园同学'
return this.selfId == String(this.session.sellerId) ? '买家同学' : (this.goods.userName || '卖家同学')
},
peerAvatar() {
return (this.goods && this.goods.userAvatar) || this.defaultAvatar
}
2 months ago
},
1 week ago
onLoad(options) {
this.sessionId = options && options.sessionId ? options.sessionId : ''
this.goodsId = options && options.goodsId ? options.goodsId : ''
if (uni.getMenuButtonBoundingClientRect) {
this.statusBarTop = uni.getMenuButtonBoundingClientRect().top || this.statusBarTop
}
this.load()
},
onUnload() {
if (this.sessionId) {
fishMarketSocket.unsubscribeSession(this.sessionId)
}
fishMarketSocket.resetHandlers()
fishMarketSocket.close()
2 months ago
},
methods: {
1 week ago
back() {
uni.navigateBack()
},
load() {
if (!this.sessionId) return
getFishChatSession(this.sessionId).then(session => {
this.session = session
const goodsId = this.goodsId || (session && session.goodsId)
if (goodsId) {
this.goodsId = goodsId
getFishGoodsDetail(goodsId).then(goods => {
this.goods = goods
})
}
})
this.loadMessages(true).then(() => {
this.initSocket()
})
},
loadMessages(reset) {
if (reset) {
this.pageNum = 1
this.totalPages = 1
2 months ago
}
1 week ago
return pageFishChatMessages(this.sessionId, this.pageNum, this.pageSize).then(page => {
const records = page && (page.records || page.content) ? (page.records || page.content) : []
const normalized = records.reverse().map(this.normalizeMessage)
this.totalPages = Number(page && (page.pages || page.totalPages)) || 1
this.messages = reset ? normalized : normalized.concat(this.messages)
this.scrollToBottom()
})
2 months ago
},
1 week ago
loadOlder() {
if (this.loadingOlder || this.pageNum >= this.totalPages) return
this.loadingOlder = true
this.pageNum += 1
this.loadMessages(false).finally(() => {
this.loadingOlder = false
})
},
normalizeMessage(item) {
return {
...item,
mine: String(item.senderId) == this.selfId,
localState: item.localState || ''
}
},
initSocket() {
if (!this.sessionId) return
fishMarketSocket.resetHandlers()
fishMarketSocket.onMessage((message) => {
if (!message || String(message.sessionId) !== String(this.sessionId)) return
this.handleSocketMessage(message)
})
fishMarketSocket.connect().then(() => {
this.socketReady = fishMarketSocket.subscribeSession(this.sessionId)
}).catch(() => {
this.socketReady = false
})
},
handleSocketMessage(message) {
if (message.type == 'audit_failed' || Number(message.auditStatus) == 2) {
this.markMessageFailed(message.clientMsgId, message.auditReason || '内容审核未通过')
uni.showToast({ title: message.auditReason || '内容审核未通过,请修改后重试', icon: 'none', duration: 3000 })
return
2 months ago
}
1 week ago
this.appendMessage(message)
},
appendMessage(message) {
if (!message || !message.id) return
if (message.clientMsgId) {
const local = this.messages.find(item => item.clientMsgId && item.clientMsgId == message.clientMsgId)
if (local) {
Object.assign(local, this.normalizeMessage(message), { localState: '' })
this.scrollToBottom()
return
}
}
const exists = this.messages.some(item => String(item.id) == String(message.id))
if (exists) return
this.messages.push(this.normalizeMessage(message))
this.scrollToBottom()
},
statusText(item) {
if (item.localState == 'failed') return item.auditReason || '内容审核未通过'
if (item.localState == 'sending') return '发送中'
return ''
},
pushLocalMessage(messageType, content, clientMsgId) {
this.messages.push({
id: clientMsgId,
clientMsgId,
sessionId: this.sessionId,
senderId: this.selfId,
messageType,
content,
mine: true,
localState: 'sending'
})
this.scrollToBottom()
},
markMessageFailed(clientMsgId, reason) {
const local = this.messages.find(item => item.clientMsgId && item.clientMsgId == clientMsgId)
if (!local) return
local.localState = 'failed'
local.auditStatus = 2
local.auditReason = reason || '内容审核未通过'
},
sendText() {
if (!this.draft) return
const content = this.draft
this.draft = ''
this.sendMessage(1, content)
},
sendMessage(messageType, content) {
const clientMsgId = 'fish-' + Date.now() + '-' + Math.random().toString(16).slice(2)
this.pushLocalMessage(messageType, content, clientMsgId)
const payload = {
sessionId: this.sessionId,
messageType,
content,
clientMsgId
}
if (fishMarketSocket.sendMessage(payload)) return
this.markMessageFailed(clientMsgId, '聊天连接未就绪,请稍后重试')
uni.showToast({ title: '聊天连接未就绪,请稍后重试', icon: 'none' })
},
chooseImage() {
uni.chooseImage({
count: 1,
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
success: async (res) => {
const path = res.tempFilePaths && res.tempFilePaths[0]
if (!path) return
const url = await uploadFishImage(path)
if (url) this.sendMessage(2, url)
}
})
},
scrollToBottom() {
this.$nextTick(() => {
this.scrollIntoView = ''
setTimeout(() => {
this.scrollIntoView = 'chat-bottom'
}, 30)
})
},
previewImage(url) {
if (!url) return
uni.previewImage({ urls: [url], current: url })
},
goGoods() {
if (!this.goodsId) return
uni.navigateTo({ url: '/package2/IdleTrad/detail?id=' + this.goodsId })
2 months ago
}
}
}
2 months ago
</script>
2 months ago
<style lang="scss">
page {
width: 100%;
height: 100%;
1 week ago
background: #f7f9f7;
2 months ago
}
1 week ago
.fish-chat-page {
2 months ago
width: 100%;
height: 100%;
1 week ago
display: flex;
flex-direction: column;
background: linear-gradient(180deg, #e8fbf3 0%, #f7f9f7 360rpx);
color: #15362f;
2 months ago
}
1 week ago
.nav {
padding-left: 24rpx;
padding-right: 24rpx;
display: flex;
align-items: center;
background: rgba(255, 255, 255, 0.88);
box-shadow: 0 8rpx 20rpx rgba(20, 68, 52, 0.06);
2 months ago
}
1 week ago
.back {
width: 68rpx;
height: 68rpx;
display: flex;
2 months ago
align-items: center;
}
1 week ago
.nav-main {
flex: 1;
2 months ago
}
1 week ago
.nav-title {
font-size: 30rpx;
font-weight: 900;
2 months ago
}
1 week ago
.nav-sub {
margin-top: 4rpx;
color: #7d8b86;
font-size: 20rpx;
2 months ago
}
1 week ago
.goods-card {
margin: 18rpx 24rpx 0;
padding: 16rpx;
2 months ago
display: flex;
align-items: center;
1 week ago
border-radius: 24rpx;
background: #fff;
box-shadow: 0 8rpx 22rpx rgba(20, 60, 48, 0.06);
2 months ago
}
1 week ago
.goods-cover {
width: 104rpx;
height: 104rpx;
border-radius: 18rpx;
background: #edf4f1;
2 months ago
}
1 week ago
.goods-main {
flex: 1;
min-width: 0;
margin-left: 16rpx;
2 months ago
}
1 week ago
.goods-title {
overflow: hidden;
font-size: 26rpx;
font-weight: 900;
text-overflow: ellipsis;
white-space: nowrap;
2 months ago
}
1 week ago
.goods-price {
margin-top: 8rpx;
color: #ef4c3f;
font-size: 28rpx;
font-weight: 900;
2 months ago
}
1 week ago
.goods-link {
color: #7f8d88;
font-size: 22rpx;
2 months ago
}
1 week ago
.message-scroll {
2 months ago
flex: 1;
1 week ago
min-height: 0;
padding: 20rpx 24rpx;
box-sizing: border-box;
2 months ago
}
1 week ago
.safe-tip,
.load-tip {
margin: 10rpx auto 24rpx;
color: #8a9994;
font-size: 21rpx;
text-align: center;
}
.message-row {
margin-bottom: 26rpx;
2 months ago
display: flex;
1 week ago
align-items: flex-start;
2 months ago
}
1 week ago
.message-row.mine {
flex-direction: row-reverse;
}
2 months ago
1 week ago
.avatar {
width: 70rpx;
height: 70rpx;
border-radius: 50%;
background: #edf4f1;
2 months ago
}
1 week ago
.message-main {
2 months ago
display: flex;
flex-direction: column;
1 week ago
align-items: flex-start;
2 months ago
}
1 week ago
.mine .message-main {
align-items: flex-end;
2 months ago
}
1 week ago
.bubble {
max-width: 520rpx;
margin: 0 16rpx;
border-radius: 24rpx;
font-size: 27rpx;
line-height: 42rpx;
word-break: break-all;
2 months ago
}
1 week ago
.text-bubble {
padding: 18rpx 22rpx;
background: #fff;
color: #253b35;
2 months ago
}
1 week ago
.mine .text-bubble {
background: linear-gradient(135deg, #37c990, #19af7e);
color: #fff;
}
2 months ago
1 week ago
.bubble.failed {
opacity: .58;
2 months ago
}
1 week ago
.image-bubble {
overflow: hidden;
2 months ago
background: #fff;
1 week ago
}
2 months ago
1 week ago
.image-bubble image {
width: 260rpx;
height: 260rpx;
display: block;
2 months ago
}
1 week ago
.message-status {
max-width: 520rpx;
margin: 8rpx 18rpx 0;
color: #ef4c3f;
font-size: 21rpx;
line-height: 30rpx;
2 months ago
}
1 week ago
.bottom-anchor {
height: 20rpx;
}
2 months ago
1 week ago
.input-bar {
padding: 16rpx 22rpx calc(16rpx + env(safe-area-inset-bottom));
display: flex;
align-items: center;
background: rgba(255, 255, 255, 0.96);
box-shadow: 0 -8rpx 22rpx rgba(20, 60, 48, 0.06);
2 months ago
}
1 week ago
.image-btn {
width: 70rpx;
height: 70rpx;
2 months ago
display: flex;
1 week ago
align-items: center;
justify-content: center;
2 months ago
}
1 week ago
.input-bar input {
2 months ago
flex: 1;
1 week ago
height: 72rpx;
padding: 0 22rpx;
border-radius: 38rpx;
background: #f2f7f4;
color: #213832;
font-size: 26rpx;
2 months ago
}
1 week ago
.send-btn {
width: 104rpx;
height: 72rpx;
margin-left: 14rpx;
border-radius: 38rpx;
background: linear-gradient(135deg, #37c990, #19af7e);
color: #fff;
2 months ago
font-size: 26rpx;
1 week ago
font-weight: 900;
text-align: center;
line-height: 72rpx;
2 months ago
}
1 week ago
</style>