All checks were successful
continuous-integration/drone/push Build is passing
1338 lines
33 KiB
Vue
1338 lines
33 KiB
Vue
<template>
|
||
<view class="chat-page">
|
||
<!-- 自定义导航栏 -->
|
||
<view class="custom-navbar" :style="{ paddingTop: statusBarHeight + 'px' }">
|
||
<view class="navbar-content">
|
||
<view class="nav-back" @click="goBack">
|
||
<image class="back-icon" src="/static/ic_back.png" mode="aspectFit"></image>
|
||
</view>
|
||
<text class="navbar-title">聊天</text>
|
||
<view class="nav-placeholder"></view>
|
||
</view>
|
||
</view>
|
||
<view :style="{ height: (statusBarHeight + 44) + 'px' }"></view>
|
||
|
||
<!-- 顶部订单信息卡片 -->
|
||
<view class="order-card" v-if="orderInfo.id">
|
||
<view class="order-card-body">
|
||
<view class="order-card-left">
|
||
<view class="order-info-row">
|
||
<text class="order-info-label">订单类型:</text>
|
||
<text class="order-info-val">{{ formatOrderType(orderInfo.orderType) }}</text>
|
||
</view>
|
||
<view class="order-info-row" v-if="orderInfo.itemName">
|
||
<text class="order-info-label">{{ getItemLabel(orderInfo.orderType) }}:</text>
|
||
<text class="order-info-val">{{ orderInfo.itemName }}</text>
|
||
</view>
|
||
<view class="order-info-row" v-if="orderInfo.pickupLocation">
|
||
<text class="order-info-label">取货地址:</text>
|
||
<text class="order-info-val">{{ orderInfo.pickupLocation }}</text>
|
||
</view>
|
||
<view class="order-info-row" v-if="orderInfo.deliveryLocation">
|
||
<text class="order-info-label">送达地址:</text>
|
||
<text class="order-info-val">{{ orderInfo.deliveryLocation }}</text>
|
||
</view>
|
||
<view class="order-info-row">
|
||
<text class="order-info-label">备注信息:</text>
|
||
<text class="order-info-val">{{ orderInfo.remark || '无' }}</text>
|
||
</view>
|
||
</view>
|
||
<view class="order-card-right">
|
||
<view class="order-commission">
|
||
<text class="commission-label">跑腿费:</text>
|
||
<text class="commission-value">{{ orderInfo.commission }}元</text>
|
||
</view>
|
||
<view class="order-detail-btn" @click="goOrderDetail">
|
||
<text>查看详情</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 聊天记录区域 -->
|
||
<scroll-view class="chat-body" scroll-y :scroll-top="scrollTop" scroll-with-animation
|
||
@scrolltoupper="loadMoreHistory" :show-scrollbar="false">
|
||
<!-- 聊天安全提示 -->
|
||
<view class="chat-safety-tip">
|
||
<text>聊天安全提示:请友好和谐沟通。请友好和谐沟通。请友好和谐沟通。请友好和谐沟通。</text>
|
||
</view>
|
||
<view v-if="loadingHistory" class="loading-history">
|
||
<text>加载中...</text>
|
||
</view>
|
||
<view v-for="(msg, index) in chatMessages" :key="msg.id || index">
|
||
<!-- 时间戳 -->
|
||
<view v-if="msg.showTime" class="msg-time">
|
||
<text>{{ msg.timeLabel }}</text>
|
||
</view>
|
||
|
||
<!-- 系统提示消息 -->
|
||
<view v-if="msg.type === 'system'" class="msg-system-wrap">
|
||
<view class="msg-system">
|
||
<text>{{ msg.content }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 改价申请消息 -->
|
||
<view v-else-if="msg.type === 'price-change'" class="msg-system-wrap">
|
||
<view class="price-change-card">
|
||
<view class="pc-header">
|
||
<text class="pc-title">{{ msg.isSelf ? '您' : '对方' }}发起了{{ msg.changeTypeLabel }}改价申请</text>
|
||
</view>
|
||
<view class="pc-body">
|
||
<view class="pc-row">
|
||
<text class="pc-label">原价</text>
|
||
<text class="pc-value">¥{{ msg.originalPrice }}</text>
|
||
</view>
|
||
<view class="pc-row">
|
||
<text class="pc-label">新价</text>
|
||
<text class="pc-value pc-new-price">¥{{ msg.newPrice }}</text>
|
||
</view>
|
||
<view class="pc-row" v-if="msg.difference !== 0">
|
||
<text class="pc-label">{{ msg.difference > 0 ? '需补缴' : '将退款' }}</text>
|
||
<text class="pc-value" :class="msg.difference > 0 ? 'pc-pay' : 'pc-refund'">
|
||
¥{{ Math.abs(msg.difference).toFixed(2) }}
|
||
</text>
|
||
</view>
|
||
</view>
|
||
<view v-if="msg.status === 'Pending' && !msg.isSelf" class="pc-actions">
|
||
<view class="pc-btn pc-accept" @click="respondPriceChange(msg.priceChangeId, 'Accepted')">
|
||
<text>同意</text>
|
||
</view>
|
||
<view class="pc-btn pc-reject" @click="respondPriceChange(msg.priceChangeId, 'Rejected')">
|
||
<text>拒绝</text>
|
||
</view>
|
||
</view>
|
||
<view v-else-if="msg.status === 'Pending' && msg.isSelf" class="pc-waiting">
|
||
<text>等待对方确认中</text>
|
||
</view>
|
||
<view v-else class="pc-result">
|
||
<text :class="msg.status === 'Accepted' ? 'pc-accepted' : 'pc-rejected'">
|
||
{{ msg.status === 'Accepted' ? '已同意' : '已拒绝' }}
|
||
</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 普通消息 -->
|
||
<view v-else class="msg-row" :class="{ 'msg-self': msg.isSelf }">
|
||
<image class="msg-avatar" :src="msg.avatar || '/static/logo.png'" mode="aspectFill"></image>
|
||
<view class="msg-bubble">
|
||
<image v-if="msg.type === 'image'" class="msg-image" :src="msg.content" mode="widthFix"
|
||
@click="previewImage(msg.originUrl || msg.content)"></image>
|
||
<text v-else class="msg-text">{{ msg.content }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</scroll-view>
|
||
|
||
<!-- 快捷操作栏 -->
|
||
<view class="quick-actions" v-if="orderInfo.id">
|
||
<view class="quick-btn" @click="onCallPhone">
|
||
<text>拨打电话</text>
|
||
</view>
|
||
<view class="quick-btn" @click="onContactService">
|
||
<text>联系客服</text>
|
||
</view>
|
||
<view class="quick-btn btn-pay" v-if="pendingPayment" @click="retryPayment">
|
||
<text>补缴支付</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 底部输入区域 -->
|
||
<view class="chat-bottom">
|
||
<view class="footer-input-row">
|
||
<input class="chat-input" v-model="inputText" placeholder="请输入..." confirm-type="send"
|
||
@confirm="sendMessage" @focus="showMorePanel = false" />
|
||
<view class="send-btn" @click="sendMessage">
|
||
<text>发送</text>
|
||
</view>
|
||
<view class="plus-btn" :class="{ active: showMorePanel }" @click="toggleMorePanel">
|
||
<image class="plus-icon" src="/static/ic_add.png" mode="aspectFit"></image>
|
||
</view>
|
||
</view>
|
||
<!-- 内嵌更多面板 -->
|
||
<view class="more-panel" v-if="showMorePanel">
|
||
<view class="panel-item" @click="onSendImage">
|
||
<view class="panel-icon-wrap"><image class="panel-icon-img" src="/static/ic_picture.png" mode="aspectFit"></image></view>
|
||
<text class="panel-label">发送图片</text>
|
||
</view>
|
||
<view class="panel-item" v-if="showGoodsPriceBtn" @click="onChangeGoodsPrice">
|
||
<view class="panel-icon-wrap"><image class="panel-icon-img" src="/static/ic_commodity.png" mode="aspectFit"></image></view>
|
||
<text class="panel-label">更改商品价格</text>
|
||
</view>
|
||
<view class="panel-item" @click="onChangeCommission">
|
||
<view class="panel-icon-wrap"><image class="panel-icon-img" src="/static/ic_errand.png" mode="aspectFit"></image></view>
|
||
<text class="panel-label">更改跑腿价格</text>
|
||
</view>
|
||
<view class="panel-item" v-if="isRunner && orderInfo.status === 'InProgress'" @click="onCompleteOrder">
|
||
<view class="panel-icon-wrap"><image class="panel-icon-img" src="/static/ic_complete.png" mode="aspectFit"></image></view>
|
||
<text class="panel-label">完成订单</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 改价弹窗 -->
|
||
<view v-if="showPriceChangePopup" class="popup-mask" @click="showPriceChangePopup = false">
|
||
<view class="popup-content" @click.stop>
|
||
<text class="popup-title">{{ priceChangeTypeLabel }}</text>
|
||
<view class="popup-field">
|
||
<text class="popup-label">当前价格</text>
|
||
<text class="popup-current-price">¥{{ currentPriceForChange }}</text>
|
||
</view>
|
||
<view class="popup-field">
|
||
<text class="popup-label">新价格</text>
|
||
<input class="popup-input" type="digit" v-model="newPriceInput" placeholder="请输入新价格" />
|
||
</view>
|
||
<view v-if="priceDifference !== 0" class="popup-diff">
|
||
<text v-if="priceDifference > 0 && isOwner" class="pc-pay">需补缴
|
||
¥{{ priceDifference.toFixed(2) }}</text>
|
||
<text v-else-if="priceDifference < 0" class="pc-refund">将退款
|
||
¥{{ Math.abs(priceDifference).toFixed(2) }}</text>
|
||
<text v-else-if="priceDifference > 0 && !isOwner" class="pc-pay">对方需补缴
|
||
¥{{ priceDifference.toFixed(2) }}</text>
|
||
</view>
|
||
<view class="popup-actions">
|
||
<view class="popup-btn popup-cancel" @click="showPriceChangePopup = false"><text>取消</text></view>
|
||
<view class="popup-btn popup-confirm" @click="submitPriceChange"><text>发起修改申请</text></view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
import {
|
||
getOrderDetail,
|
||
createPriceChange,
|
||
respondPriceChange as respondPriceChangeApi
|
||
} from '../../utils/api'
|
||
import {
|
||
useUserStore
|
||
} from '../../stores/user'
|
||
import {
|
||
initIM,
|
||
onNewMessage,
|
||
offNewMessage,
|
||
getMessageList,
|
||
sendTextMessage,
|
||
sendImageMessage,
|
||
sendCustomMessage,
|
||
formatIMMessage,
|
||
setMessageRead
|
||
} from '../../utils/im'
|
||
|
||
export default {
|
||
data() {
|
||
return {
|
||
orderId: null,
|
||
orderInfo: {},
|
||
lastOrderStatus: null,
|
||
chatMessages: [],
|
||
inputText: '',
|
||
scrollTop: 0,
|
||
showMorePanel: false,
|
||
imUserId: '',
|
||
imGroupId: null,
|
||
imReady: false,
|
||
nextReqMessageID: '',
|
||
historyCompleted: false,
|
||
loadingHistory: false,
|
||
showPriceChangePopup: false,
|
||
priceChangeType: '',
|
||
newPriceInput: '',
|
||
submittingPriceChange: false,
|
||
statusBarHeight: 0,
|
||
pendingPayment: null
|
||
}
|
||
},
|
||
computed: {
|
||
isRunner() {
|
||
const userStore = useUserStore()
|
||
return this.orderInfo.runnerId === userStore.userId
|
||
},
|
||
isOwner() {
|
||
const userStore = useUserStore()
|
||
return this.orderInfo.ownerId === userStore.userId
|
||
},
|
||
/** 构建 IM userId -> 头像 的映射 */
|
||
avatarMap() {
|
||
const map = {}
|
||
if (this.orderInfo.ownerId) {
|
||
map[`user_${this.orderInfo.ownerId}`] = this.orderInfo.ownerAvatar || ''
|
||
}
|
||
if (this.orderInfo.runnerId) {
|
||
map[`user_${this.orderInfo.runnerId}`] = this.orderInfo.runnerAvatar || ''
|
||
}
|
||
return map
|
||
},
|
||
showGoodsPriceBtn() {
|
||
const type = this.orderInfo.orderType
|
||
return type === 'Purchase' || type === 'Food'
|
||
},
|
||
priceChangeTypeLabel() {
|
||
return this.priceChangeType === 'Commission' ? '更改跑腿价格' : '更改商品价格'
|
||
},
|
||
currentPriceForChange() {
|
||
if (this.priceChangeType === 'Commission') return (this.orderInfo.commission || 0).toFixed(2)
|
||
return (this.orderInfo.goodsAmount || 0).toFixed(2)
|
||
},
|
||
priceDifference() {
|
||
const newPrice = parseFloat(this.newPriceInput)
|
||
if (isNaN(newPrice) || newPrice < 0) return 0
|
||
const original = this.priceChangeType === 'Commission' ?
|
||
(this.orderInfo.commission || 0) : (this.orderInfo.goodsAmount || 0)
|
||
return newPrice - original
|
||
}
|
||
},
|
||
async onLoad(options) {
|
||
const sysInfo = uni.getSystemInfoSync()
|
||
this.statusBarHeight = sysInfo.statusBarHeight || 0
|
||
console.log('[聊天] onLoad options:', JSON.stringify(options))
|
||
this.orderId = options.orderId || null
|
||
this.targetImUserIdFromParam = options.targetUserId || null
|
||
if (this.orderId) await this.loadOrderInfo()
|
||
await this.loginIM()
|
||
},
|
||
onUnload() {
|
||
offNewMessage()
|
||
},
|
||
onShow() {
|
||
// 每次回到聊天页都检测订单状态变化
|
||
if (this.orderId) {
|
||
this.checkOrderStatusChange()
|
||
}
|
||
},
|
||
methods: {
|
||
goBack() {
|
||
uni.navigateBack()
|
||
},
|
||
async loadOrderInfo() {
|
||
if (!this.orderId) return
|
||
try {
|
||
const res = await getOrderDetail(this.orderId);
|
||
console.log('[聊天] 订单详情:', res)
|
||
this.orderInfo = res || {}
|
||
if (!this.lastOrderStatus) {
|
||
this.lastOrderStatus = this.orderInfo.status
|
||
}
|
||
} catch (e) {
|
||
console.error('[聊天] 加载订单详情失败:', e)
|
||
}
|
||
},
|
||
async loginIM() {
|
||
try {
|
||
const {
|
||
userId
|
||
} = await initIM()
|
||
this.imUserId = userId
|
||
|
||
// 从订单信息获取群组ID
|
||
if (this.orderInfo.imGroupId) {
|
||
this.imGroupId = this.orderInfo.imGroupId
|
||
} else if (this.orderId) {
|
||
// 兼容旧订单:群ID格式为 order_{orderId}
|
||
this.imGroupId = `order_${this.orderId}`
|
||
}
|
||
|
||
this.imReady = true
|
||
|
||
onNewMessage((msgList) => {
|
||
for (const msg of msgList) {
|
||
// 只处理当前群的消息
|
||
if (msg.conversationID === `GROUP${this.imGroupId}`) {
|
||
const formatted = formatIMMessage(msg, this.imUserId, this.avatarMap)
|
||
// 收到改价响应消息,更新对应改价卡片状态
|
||
if (formatted.type === 'price-change-response') {
|
||
const card = this.chatMessages.find(m => m.type === 'price-change' && m.priceChangeId === formatted.priceChangeId)
|
||
if (card) card.status = formatted.status
|
||
this.chatMessages.push({
|
||
id: formatted.id,
|
||
type: 'system',
|
||
content: `对方${formatted.action === 'Accepted' ? '同意' : '拒绝'}了${formatted.changeTypeLabel}改价`
|
||
})
|
||
if (formatted.action === 'Accepted') this.loadOrderInfo()
|
||
} else {
|
||
this.chatMessages.push(formatted)
|
||
}
|
||
}
|
||
}
|
||
if (this.imGroupId) setMessageRead(this.imGroupId)
|
||
})
|
||
|
||
await this.loadHistory()
|
||
if (this.imGroupId) setMessageRead(this.imGroupId)
|
||
} catch (e) {
|
||
console.error('[IM] 初始化失败:', e)
|
||
uni.showToast({
|
||
title: 'IM连接失败',
|
||
icon: 'none'
|
||
})
|
||
}
|
||
},
|
||
async loadHistory(scrollToEnd = true) {
|
||
if (!this.imReady || !this.imGroupId) return
|
||
this.loadingHistory = true
|
||
try {
|
||
const res = await getMessageList(this.imGroupId, this.nextReqMessageID)
|
||
const allFormatted = res.messageList.map(m => formatIMMessage(m, this.imUserId, this.avatarMap))
|
||
// 处理历史消息中的改价响应
|
||
const formatted = []
|
||
for (const m of allFormatted) {
|
||
if (m.type === 'price-change-response') {
|
||
const card = allFormatted.find(c => c.type === 'price-change' && c.priceChangeId === m.priceChangeId)
|
||
if (card) card.status = m.status
|
||
formatted.push({ id: m.id, type: 'system', content: `${m.isSelf ? '您' : '对方'}${m.action === 'Accepted' ? '同意' : '拒绝'}了${m.changeTypeLabel}改价` })
|
||
} else {
|
||
formatted.push(m)
|
||
}
|
||
}
|
||
this.chatMessages = [...formatted, ...this.chatMessages]
|
||
this.nextReqMessageID = res.nextReqMessageID
|
||
this.historyCompleted = res.isCompleted
|
||
if (scrollToEnd) this.scrollToBottom()
|
||
} catch (e) {
|
||
console.error('[IM] 拉取历史消息失败:', e)
|
||
} finally {
|
||
this.loadingHistory = false
|
||
}
|
||
},
|
||
async loadMoreHistory() {
|
||
if (this.historyCompleted || this.loadingHistory) return
|
||
await this.loadHistory(false)
|
||
},
|
||
async sendMessage() {
|
||
if (!this.inputText.trim()) return
|
||
if (!this.imReady) {
|
||
uni.showToast({
|
||
title: 'IM未就绪',
|
||
icon: 'none'
|
||
});
|
||
return
|
||
}
|
||
const text = this.inputText
|
||
this.inputText = ''
|
||
try {
|
||
const msg = await sendTextMessage(this.imGroupId, text)
|
||
this.chatMessages.push(formatIMMessage(msg, this.imUserId, this.avatarMap))
|
||
this.scrollToBottom()
|
||
} catch (e) {
|
||
uni.showToast({
|
||
title: '发送失败',
|
||
icon: 'none'
|
||
});
|
||
this.inputText = text
|
||
}
|
||
},
|
||
scrollToBottom() {
|
||
this.$nextTick(() => {
|
||
this.scrollTop = this.scrollTop === 99999 ? 99998 : 99999
|
||
})
|
||
},
|
||
toggleMorePanel() {
|
||
this.showMorePanel = !this.showMorePanel
|
||
},
|
||
onSendImage() {
|
||
this.showMorePanel = false
|
||
if (!this.imReady) {
|
||
uni.showToast({
|
||
title: 'IM未就绪',
|
||
icon: 'none'
|
||
});
|
||
return
|
||
}
|
||
uni.chooseImage({
|
||
count: 1,
|
||
sourceType: ['album', 'camera'],
|
||
success: async (res) => {
|
||
try {
|
||
const msg = await sendImageMessage(this.imGroupId, res)
|
||
this.chatMessages.push(formatIMMessage(msg, this.imUserId, this.avatarMap))
|
||
this.scrollToBottom()
|
||
} catch (e) {
|
||
uni.showToast({
|
||
title: '图片发送失败',
|
||
icon: 'none'
|
||
})
|
||
}
|
||
}
|
||
})
|
||
},
|
||
onChangeCommission() {
|
||
this.showMorePanel = false
|
||
if (this.hasPendingPriceChange()) return
|
||
this.priceChangeType = 'Commission'
|
||
this.newPriceInput = ''
|
||
this.showPriceChangePopup = true
|
||
},
|
||
onChangeGoodsPrice() {
|
||
this.showMorePanel = false
|
||
if (this.hasPendingPriceChange()) return
|
||
this.priceChangeType = 'GoodsAmount'
|
||
this.newPriceInput = ''
|
||
this.showPriceChangePopup = true
|
||
},
|
||
/** 检查是否有待处理的改价申请 */
|
||
hasPendingPriceChange() {
|
||
const pending = this.chatMessages.find(m => m.type === 'price-change' && m.status === 'Pending')
|
||
if (pending) {
|
||
uni.showToast({ title: '已有等待处理的改价申请,无法同时申请', icon: 'none' })
|
||
return true
|
||
}
|
||
return false
|
||
},
|
||
async submitPriceChange() {
|
||
const newPrice = parseFloat(this.newPriceInput)
|
||
if (isNaN(newPrice) || newPrice < 0) {
|
||
uni.showToast({
|
||
title: '请输入有效的价格',
|
||
icon: 'none'
|
||
});
|
||
return
|
||
}
|
||
if (this.submittingPriceChange) return
|
||
this.submittingPriceChange = true
|
||
if (this.isOwner && this.priceDifference > 0) {
|
||
uni.showModal({
|
||
title: '补缴支付',
|
||
content: `需补缴 ¥${this.priceDifference.toFixed(2)},确认?`,
|
||
success: async (r) => {
|
||
if (r.confirm) await this.doSubmitPriceChange(newPrice)
|
||
this.submittingPriceChange = false
|
||
}
|
||
});
|
||
return
|
||
}
|
||
await this.doSubmitPriceChange(newPrice)
|
||
this.submittingPriceChange = false
|
||
},
|
||
async doSubmitPriceChange(newPrice) {
|
||
try {
|
||
const res = await createPriceChange(this.orderId, {
|
||
changeType: this.priceChangeType,
|
||
newPrice
|
||
})
|
||
this.showPriceChangePopup = false
|
||
const changeTypeLabel = this.priceChangeType === 'Commission' ? '跑腿佣金' : '商品总额'
|
||
if (this.imReady) {
|
||
await sendCustomMessage(this.imGroupId, {
|
||
bizType: 'price-change',
|
||
priceChangeId: res.id,
|
||
changeTypeLabel,
|
||
originalPrice: res.originalPrice.toFixed(2),
|
||
newPrice: res.newPrice.toFixed(2),
|
||
difference: res.difference,
|
||
status: res.status,
|
||
description: `发起了${changeTypeLabel}改价申请`
|
||
})
|
||
}
|
||
this.chatMessages.push({
|
||
id: `pc_${res.id}`,
|
||
type: 'price-change',
|
||
isSelf: true,
|
||
priceChangeId: res.id,
|
||
changeTypeLabel,
|
||
originalPrice: res.originalPrice.toFixed(2),
|
||
newPrice: res.newPrice.toFixed(2),
|
||
difference: res.difference,
|
||
status: res.status
|
||
})
|
||
this.scrollToBottom()
|
||
uni.showToast({
|
||
title: '改价申请已发送',
|
||
icon: 'none'
|
||
})
|
||
} catch (e) {
|
||
uni.showToast({
|
||
title: e.message || '改价申请失败',
|
||
icon: 'none'
|
||
})
|
||
}
|
||
},
|
||
async respondPriceChange(priceChangeId, action) {
|
||
try {
|
||
console.log('[聊天] 响应改价:', this.orderId, priceChangeId, action)
|
||
if (!this.orderId) {
|
||
uni.showToast({ title: '订单信息未加载', icon: 'none' })
|
||
return
|
||
}
|
||
const res = await respondPriceChangeApi(this.orderId, priceChangeId, {
|
||
action
|
||
})
|
||
const msg = this.chatMessages.find(m => m.type === 'price-change' && m.priceChangeId ===
|
||
priceChangeId)
|
||
if (msg) msg.status = res.status
|
||
const actionLabel = action === 'Accepted' ? '同意' : '拒绝'
|
||
const changeTypeLabel = res.changeType === 'Commission' ? '跑腿佣金' : '商品总额'
|
||
|
||
// 通过 IM 通知对方改价结果(对方收到后显示为系统提示)
|
||
if (this.imReady) {
|
||
// 发送到群
|
||
await sendCustomMessage(this.imGroupId, {
|
||
bizType: 'price-change-response',
|
||
priceChangeId: priceChangeId,
|
||
action: action,
|
||
status: res.status,
|
||
changeTypeLabel,
|
||
description: `${actionLabel}了${changeTypeLabel}改价申请`
|
||
})
|
||
// 发一条文本消息记录操作(群内可见,持久化)
|
||
try {
|
||
const imMsg = await sendTextMessage(this.imGroupId, `[系统提示] 单主已${actionLabel}${changeTypeLabel}改价`)
|
||
this.chatMessages.push(formatIMMessage(imMsg, this.imUserId, this.avatarMap))
|
||
} catch (ex) {}
|
||
} else {
|
||
this.chatMessages.push({
|
||
id: `sys_${Date.now()}`,
|
||
type: 'system',
|
||
content: `您已${actionLabel}${changeTypeLabel}改价`
|
||
})
|
||
}
|
||
|
||
if (action === 'Accepted') {
|
||
// 需要补价:拉起支付
|
||
if (res.paymentParams) {
|
||
try {
|
||
await this.wxPay(res.paymentParams)
|
||
// 补缴成功,通过IM通知群内
|
||
if (this.imReady && this.imGroupId) {
|
||
try {
|
||
const imMsg = await sendTextMessage(this.imGroupId, '[系统提示] 单主已完成补缴支付')
|
||
this.chatMessages.push(formatIMMessage(imMsg, this.imUserId, this.avatarMap))
|
||
} catch (ex) {}
|
||
}
|
||
} catch (e) {
|
||
// 支付取消或失败,保存待支付信息供再次支付
|
||
this.pendingPayment = res.paymentParams
|
||
this.chatMessages.push({
|
||
id: `sys_pay_${Date.now()}`,
|
||
type: 'system',
|
||
content: `需补缴¥${res.difference.toFixed(2)},点击下方"补缴支付"完成支付`
|
||
})
|
||
}
|
||
}
|
||
// 需要退款:后端已自动退款
|
||
if (res.difference < 0) {
|
||
const refundText = res.refundSuccess
|
||
? `改价退款¥${Math.abs(res.difference).toFixed(2)}已原路返回`
|
||
: '退款处理中,请稍后查看'
|
||
// 通过IM发送(持久化,群内可见)
|
||
if (this.imReady && this.imGroupId) {
|
||
try {
|
||
await sendCustomMessage(this.imGroupId, {
|
||
bizType: 'order-status',
|
||
description: refundText
|
||
})
|
||
} catch (ex) {}
|
||
}
|
||
this.chatMessages.push({ id: `sys_r_${Date.now()}`, type: 'system', content: refundText })
|
||
}
|
||
await this.loadOrderInfo()
|
||
}
|
||
this.scrollToBottom()
|
||
} catch (e) {
|
||
uni.showToast({
|
||
title: e.message || '操作失败',
|
||
icon: 'none'
|
||
})
|
||
}
|
||
},
|
||
async checkOrderStatusChange() {
|
||
const oldStatus = this.lastOrderStatus
|
||
try {
|
||
await this.loadOrderInfo()
|
||
} catch (e) {
|
||
return
|
||
}
|
||
const newStatus = this.orderInfo.status
|
||
// 首次加载只记录状态,不提示
|
||
if (!oldStatus) {
|
||
this.lastOrderStatus = newStatus
|
||
return
|
||
}
|
||
if (oldStatus === newStatus) return
|
||
this.lastOrderStatus = newStatus
|
||
|
||
const statusMessages = {
|
||
'InProgress→WaitConfirm': '跑腿已提交完成,请在订单详情中确认',
|
||
'WaitConfirm→Completed': '订单已完成',
|
||
'WaitConfirm→InProgress': '单主已拒绝完成,订单继续进行'
|
||
}
|
||
const key = `${oldStatus}→${newStatus}`
|
||
const msg = statusMessages[key]
|
||
if (!msg) return
|
||
|
||
// 避免重复:检查聊天消息中是否已有相同内容
|
||
const isDuplicate = this.chatMessages.some(m =>
|
||
(m.type === 'system' && m.content === msg) ||
|
||
(m.type === 'text' && m.content && m.content.includes(msg))
|
||
)
|
||
if (isDuplicate) return
|
||
|
||
this.chatMessages.push({
|
||
id: `sys_status_${Date.now()}`,
|
||
type: 'system',
|
||
content: msg
|
||
})
|
||
},
|
||
onCompleteOrder() {
|
||
this.showMorePanel = false
|
||
uni.navigateTo({
|
||
url: `/pages/order/complete-order?id=${this.orderId}`
|
||
})
|
||
},
|
||
goOrderDetail() {
|
||
const pages = getCurrentPages()
|
||
if (pages.length >= 2) {
|
||
const prevPage = pages[pages.length - 2]
|
||
if (prevPage.route === 'pages/order/order-detail') {
|
||
uni.navigateBack()
|
||
return
|
||
}
|
||
}
|
||
uni.navigateTo({
|
||
url: `/pages/order/order-detail?id=${this.orderId}`
|
||
})
|
||
},
|
||
onCallPhone() {
|
||
const phone = this.isOwner ? this.orderInfo.runnerPhone : this.orderInfo.phone
|
||
if (!phone) {
|
||
uni.showToast({
|
||
title: '暂无对方手机号',
|
||
icon: 'none'
|
||
});
|
||
return
|
||
}
|
||
uni.makePhoneCall({
|
||
phoneNumber: phone,
|
||
fail: () => {}
|
||
})
|
||
},
|
||
onContactService() {
|
||
uni.navigateTo({
|
||
url: '/pages/config/qrcode'
|
||
})
|
||
},
|
||
/** 微信支付 */
|
||
wxPay(params) {
|
||
return new Promise((resolve, reject) => {
|
||
uni.requestPayment({
|
||
provider: 'wxpay',
|
||
timeStamp: params.timeStamp,
|
||
nonceStr: params.nonceStr,
|
||
package: params.package_,
|
||
signType: params.signType,
|
||
paySign: params.paySign,
|
||
success: () => {
|
||
this.pendingPayment = null
|
||
resolve()
|
||
},
|
||
fail: (err) => {
|
||
if (err.errMsg !== 'requestPayment:fail cancel')
|
||
uni.showToast({ title: '支付失败', icon: 'none' })
|
||
reject(err)
|
||
}
|
||
})
|
||
})
|
||
},
|
||
/** 重新补缴支付 */
|
||
async retryPayment() {
|
||
if (!this.pendingPayment) return
|
||
try {
|
||
await this.wxPay(this.pendingPayment)
|
||
uni.showToast({ title: '补缴成功', icon: 'success' })
|
||
// 发送补缴成功的提示消息
|
||
if (this.imReady && this.imGroupId) {
|
||
try {
|
||
const imMsg = await sendTextMessage(this.imGroupId, '[系统提示] 单主已完成补缴支付')
|
||
this.chatMessages.push(formatIMMessage(imMsg, this.imUserId, this.avatarMap))
|
||
this.scrollToBottom()
|
||
} catch (ex) {}
|
||
}
|
||
await this.loadOrderInfo()
|
||
} catch (e) {}
|
||
},
|
||
previewImage(url) {
|
||
uni.previewImage({
|
||
urls: [url]
|
||
})
|
||
},
|
||
formatOrderType(type) {
|
||
const map = {
|
||
Pickup: '代取',
|
||
Delivery: '代送',
|
||
Help: '万能帮',
|
||
Purchase: '代购',
|
||
Food: '美食街'
|
||
}
|
||
return map[type] || type
|
||
},
|
||
getItemLabel(type) {
|
||
const map = {
|
||
Pickup: '代取物品',
|
||
Delivery: '送货物品',
|
||
Help: '帮忙事项',
|
||
Purchase: '代购物品',
|
||
Food: '美食订单'
|
||
}
|
||
return map[type] || '物品'
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style scoped>
|
||
.custom-navbar {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
z-index: 999;
|
||
background: #FFB700;
|
||
}
|
||
|
||
.navbar-content {
|
||
height: 44px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 0 20rpx;
|
||
}
|
||
|
||
.nav-back {
|
||
width: 60rpx;
|
||
height: 60rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.back-icon {
|
||
width: 40rpx;
|
||
height: 40rpx;
|
||
}
|
||
|
||
.navbar-title {
|
||
font-size: 34rpx;
|
||
font-weight: bold;
|
||
color: #363636;
|
||
}
|
||
|
||
.nav-placeholder {
|
||
width: 60rpx;
|
||
}
|
||
|
||
.chat-page {
|
||
display: flex;
|
||
flex-direction: column;
|
||
height: 100vh;
|
||
background-color: #f5f5f5;
|
||
}
|
||
|
||
/* 订单信息卡片 */
|
||
.order-card {
|
||
margin: 16rpx 24rpx 0;
|
||
background: #fff;
|
||
border-radius: 16rpx;
|
||
padding: 24rpx;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.order-card-body {
|
||
display: flex;
|
||
}
|
||
|
||
.order-card-left {
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
|
||
.order-info-row {
|
||
display: flex;
|
||
margin-bottom: 6rpx;
|
||
line-height: 1.6;
|
||
}
|
||
|
||
.order-info-label {
|
||
font-size: 26rpx;
|
||
color: #666;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.order-info-val {
|
||
font-size: 26rpx;
|
||
color: #333;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.order-card-right {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: flex-end;
|
||
justify-content: space-between;
|
||
margin-left: 20rpx;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.order-commission {
|
||
display: flex;
|
||
align-items: baseline;
|
||
}
|
||
|
||
.commission-label {
|
||
font-size: 24rpx;
|
||
color: #FF6600;
|
||
}
|
||
|
||
.commission-value {
|
||
font-size: 36rpx;
|
||
color: #FF6600;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.order-detail-btn {
|
||
background: linear-gradient(135deg, #FFB700, #FF9500);
|
||
padding: 12rpx 28rpx;
|
||
border-radius: 8rpx;
|
||
}
|
||
|
||
.order-detail-btn text {
|
||
font-size: 24rpx;
|
||
color: #fff;
|
||
font-weight: 500;
|
||
}
|
||
|
||
/* 聊天安全提示 */
|
||
.chat-safety-tip {
|
||
text-align: center;
|
||
padding: 16rpx 40rpx;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.chat-safety-tip text {
|
||
font-size: 22rpx;
|
||
color: #ccc;
|
||
line-height: 1.6;
|
||
}
|
||
|
||
/* 聊天记录 */
|
||
.chat-body {
|
||
flex: 1;
|
||
width: 96%;
|
||
padding: 16rpx 0;
|
||
margin: 0 auto 0;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.chat-body ::-webkit-scrollbar {
|
||
display: none;
|
||
width: 0;
|
||
height: 0;
|
||
}
|
||
|
||
.loading-history {
|
||
text-align: center;
|
||
padding: 16rpx 0;
|
||
font-size: 24rpx;
|
||
color: #999;
|
||
}
|
||
|
||
.msg-time {
|
||
text-align: center;
|
||
margin: 16rpx 0;
|
||
}
|
||
|
||
.msg-time text {
|
||
font-size: 22rpx;
|
||
color: #bbb;
|
||
}
|
||
|
||
/* 消息行 */
|
||
.msg-row {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
margin-bottom: 24rpx;
|
||
padding: 0 8rpx;
|
||
}
|
||
|
||
.msg-row.msg-self {
|
||
flex-direction: row-reverse;
|
||
}
|
||
|
||
.msg-avatar {
|
||
width: 72rpx;
|
||
height: 72rpx;
|
||
border-radius: 50%;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.msg-bubble {
|
||
max-width: 60%;
|
||
margin: 0 16rpx;
|
||
}
|
||
|
||
.msg-text {
|
||
background-color: #fff;
|
||
padding: 20rpx 24rpx;
|
||
border-radius: 20rpx;
|
||
font-size: 28rpx;
|
||
color: #333;
|
||
word-break: break-all;
|
||
display: block;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.msg-self .msg-text {
|
||
background-color: #FFB700;
|
||
color: #fff;
|
||
}
|
||
|
||
.msg-image {
|
||
max-width: 100%;
|
||
border-radius: 12rpx;
|
||
}
|
||
|
||
/* 系统消息居中 */
|
||
.msg-system-wrap {
|
||
display: flex;
|
||
justify-content: center;
|
||
margin-bottom: 20rpx;
|
||
}
|
||
|
||
.msg-system {
|
||
background-color: #f0f0f0;
|
||
padding: 10rpx 20rpx;
|
||
border-radius: 8rpx;
|
||
}
|
||
|
||
.msg-system text {
|
||
font-size: 24rpx;
|
||
color: #999;
|
||
}
|
||
|
||
/* 快捷操作栏 */
|
||
.quick-actions {
|
||
display: flex;
|
||
gap: 20rpx;
|
||
padding: 12rpx 24rpx;
|
||
flex-shrink: 0;
|
||
background: #f5f5f5;
|
||
}
|
||
|
||
.quick-btn {
|
||
padding: 10rpx 24rpx;
|
||
border: 1rpx solid #ddd;
|
||
border-radius: 8rpx;
|
||
background: #fff;
|
||
}
|
||
|
||
.quick-btn text {
|
||
font-size: 22rpx;
|
||
color: #333;
|
||
}
|
||
|
||
.btn-pay {
|
||
background: #FAD146;
|
||
border-color: #FAD146;
|
||
}
|
||
|
||
.btn-pay text {
|
||
color: #fff;
|
||
}
|
||
|
||
/* 底部输入区域 */
|
||
.chat-bottom {
|
||
background: #fff;
|
||
border-top: 1rpx solid #eee;
|
||
flex-shrink: 0;
|
||
padding-bottom: env(safe-area-inset-bottom);
|
||
}
|
||
|
||
.footer-input-row {
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 16rpx 20rpx;
|
||
gap: 12rpx;
|
||
}
|
||
|
||
.chat-input {
|
||
flex: 1;
|
||
height: 72rpx;
|
||
background-color: #f5f5f5;
|
||
border-radius: 36rpx;
|
||
padding: 0 24rpx;
|
||
font-size: 28rpx;
|
||
}
|
||
|
||
.send-btn {
|
||
background: linear-gradient(135deg, #FFB700, #FF9500);
|
||
padding: 0 32rpx;
|
||
height: 72rpx;
|
||
line-height: 72rpx;
|
||
border-radius: 36rpx;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.send-btn text {
|
||
font-size: 28rpx;
|
||
color: #fff;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.plus-btn {
|
||
width: 64rpx;
|
||
height: 64rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
flex-shrink: 0;
|
||
transition: transform 0.2s;
|
||
}
|
||
|
||
.plus-btn.active {
|
||
transform: rotate(45deg);
|
||
}
|
||
|
||
.plus-icon {
|
||
width: 48rpx;
|
||
height: 48rpx;
|
||
}
|
||
|
||
/* 内嵌更多面板 */
|
||
.more-panel {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
padding: 20rpx 10rpx 10rpx;
|
||
border-top: 1rpx solid #f0f0f0;
|
||
}
|
||
|
||
.panel-item {
|
||
width: 25%;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
padding: 16rpx 0;
|
||
}
|
||
|
||
.panel-icon-wrap {
|
||
width: 96rpx;
|
||
height: 96rpx;
|
||
background: #f5f5f5;
|
||
border-radius: 20rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
margin-bottom: 10rpx;
|
||
}
|
||
|
||
.panel-icon-img {
|
||
width: 48rpx;
|
||
height: 48rpx;
|
||
}
|
||
|
||
.panel-label {
|
||
font-size: 22rpx;
|
||
color: #666;
|
||
}
|
||
|
||
/* 改价消息卡片 */
|
||
.price-change-card {
|
||
background: #fff;
|
||
border-radius: 16rpx;
|
||
padding: 24rpx;
|
||
width: 80%;
|
||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.08);
|
||
}
|
||
|
||
.pc-header {
|
||
margin-bottom: 16rpx;
|
||
}
|
||
|
||
.pc-title {
|
||
font-size: 26rpx;
|
||
color: #333;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.pc-body {
|
||
margin-bottom: 16rpx;
|
||
}
|
||
|
||
.pc-row {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
padding: 8rpx 0;
|
||
}
|
||
|
||
.pc-label {
|
||
font-size: 24rpx;
|
||
color: #999;
|
||
}
|
||
|
||
.pc-value {
|
||
font-size: 26rpx;
|
||
color: #333;
|
||
}
|
||
|
||
.pc-new-price {
|
||
color: #FFB700;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.pc-pay {
|
||
color: #ff6600;
|
||
}
|
||
|
||
.pc-refund {
|
||
color: #52c41a;
|
||
}
|
||
|
||
.pc-actions {
|
||
display: flex;
|
||
gap: 16rpx;
|
||
}
|
||
|
||
.pc-btn {
|
||
flex: 1;
|
||
padding: 16rpx 0;
|
||
border-radius: 8rpx;
|
||
text-align: center;
|
||
font-size: 26rpx;
|
||
}
|
||
|
||
.pc-accept {
|
||
background: #FFB700;
|
||
color: #fff;
|
||
}
|
||
|
||
.pc-reject {
|
||
background: #f5f5f5;
|
||
color: #666;
|
||
}
|
||
|
||
.pc-waiting {
|
||
text-align: center;
|
||
padding: 12rpx 0;
|
||
}
|
||
|
||
.pc-waiting text {
|
||
font-size: 24rpx;
|
||
color: #ff9900;
|
||
}
|
||
|
||
.pc-result {
|
||
text-align: center;
|
||
padding: 12rpx 0;
|
||
}
|
||
|
||
.pc-accepted {
|
||
font-size: 24rpx;
|
||
color: #52c41a;
|
||
}
|
||
|
||
.pc-rejected {
|
||
font-size: 24rpx;
|
||
color: #ff4d4f;
|
||
}
|
||
|
||
/* 改价弹窗 */
|
||
.popup-mask {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background: rgba(0, 0, 0, 0.5);
|
||
z-index: 200;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.popup-content {
|
||
width: 80%;
|
||
background: #fff;
|
||
border-radius: 20rpx;
|
||
padding: 40rpx;
|
||
}
|
||
|
||
.popup-title {
|
||
font-size: 32rpx;
|
||
color: #333;
|
||
font-weight: 500;
|
||
display: block;
|
||
text-align: center;
|
||
margin-bottom: 30rpx;
|
||
}
|
||
|
||
.popup-field {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 20rpx 0;
|
||
border-bottom: 1rpx solid #f0f0f0;
|
||
}
|
||
|
||
.popup-label {
|
||
font-size: 28rpx;
|
||
color: #666;
|
||
}
|
||
|
||
.popup-current-price {
|
||
font-size: 28rpx;
|
||
color: #333;
|
||
}
|
||
|
||
.popup-input {
|
||
width: 50%;
|
||
height: 64rpx;
|
||
border: 1rpx solid #ddd;
|
||
border-radius: 8rpx;
|
||
padding: 0 16rpx;
|
||
font-size: 28rpx;
|
||
text-align: right;
|
||
}
|
||
|
||
.popup-diff {
|
||
padding: 16rpx 0;
|
||
text-align: center;
|
||
}
|
||
|
||
.popup-diff text {
|
||
font-size: 24rpx;
|
||
}
|
||
|
||
.popup-actions {
|
||
display: flex;
|
||
gap: 20rpx;
|
||
margin-top: 30rpx;
|
||
}
|
||
|
||
.popup-btn {
|
||
flex: 1;
|
||
padding: 20rpx 0;
|
||
border-radius: 12rpx;
|
||
text-align: center;
|
||
font-size: 28rpx;
|
||
}
|
||
|
||
.popup-cancel {
|
||
background: #f5f5f5;
|
||
color: #666;
|
||
}
|
||
|
||
.popup-confirm {
|
||
background: #FAD146;
|
||
color: #fff;
|
||
}
|
||
</style>
|
||
|
||
<style>
|
||
/* 隐藏聊天页滚动条(非scoped才能覆盖scroll-view内部) */
|
||
.chat-body ::-webkit-scrollbar {
|
||
display: none !important;
|
||
width: 0 !important;
|
||
height: 0 !important;
|
||
}
|
||
</style> |