campus-errand/miniapp/pages/message/chat.vue
18631081161 681d2b5fe8
All checks were successful
continuous-integration/drone/push Build is passing
提现
2026-04-02 16:55:18 +08:00

1338 lines
33 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>