campus-errand/miniapp/pages/message/chat.vue
2026-04-01 12:50:17 +08:00

1292 lines
32 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">
<!-- 聊天安全提示 -->
<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.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" @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,
getOrderByChatUser,
createPriceChange,
respondPriceChange as respondPriceChangeApi
} from '../../utils/api'
import {
useUserStore
} from '../../stores/user'
import {
initIM,
logoutIM,
onNewMessage,
offNewMessage,
getMessageList,
sendTextMessage,
sendImageMessage,
sendCustomMessage,
formatIMMessage
} from '../../utils/im'
export default {
data() {
return {
orderId: null,
orderInfo: {},
lastOrderStatus: null,
chatMessages: [],
inputText: '',
scrollTop: 0,
showMorePanel: false,
imUserId: '',
targetImUserId: '',
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
},
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
if (this.targetImUserIdFromParam) {
this.targetImUserId = this.targetImUserIdFromParam
} else if (this.orderInfo.id) {
const userStore = useUserStore()
this.targetImUserId = this.orderInfo.ownerId === userStore.userId ?
`user_${this.orderInfo.runnerId}` : `user_${this.orderInfo.ownerId}`
}
this.imReady = true
// 如果没有 orderId 但有对方用户ID查找关联订单
if (!this.orderId && this.targetImUserId) {
try {
const targetUid = this.targetImUserId.replace('user_', '')
const res = await getOrderByChatUser(targetUid)
if (res && res.found && res.orderId) {
this.orderId = res.orderId
await this.loadOrderInfo()
}
} catch (e) {
console.error('[聊天] 查找关联订单失败:', e)
}
}
onNewMessage((msgList) => {
for (const msg of msgList) {
if (msg.from === this.targetImUserId || msg.from === this.imUserId) {
const formatted = formatIMMessage(msg, this.imUserId)
// 按订单ID过滤
if (this.orderId && formatted.orderId && String(formatted.orderId) !== String(this.orderId)) continue
// 收到改价响应消息,更新对应改价卡片状态
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)
}
}
}
this.scrollToBottom()
})
await this.loadHistory()
} catch (e) {
console.error('[IM] 初始化失败:', e)
uni.showToast({
title: 'IM连接失败',
icon: 'none'
})
}
},
async loadHistory() {
if (!this.imReady || !this.targetImUserId) return
this.loadingHistory = true
try {
const res = await getMessageList(this.targetImUserId, this.nextReqMessageID)
const allFormatted = res.messageList.map(m => formatIMMessage(m, this.imUserId))
// 按订单ID过滤只显示当前订单的消息或没有orderId的旧消息
const filtered = this.orderId
? allFormatted.filter(m => !m.orderId || String(m.orderId) === String(this.orderId))
: allFormatted
// 处理历史消息中的改价响应,更新对应改价卡片状态
const formatted = []
for (const m of filtered) {
if (m.type === 'price-change-response') {
const card = filtered.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
this.scrollToBottom()
} catch (e) {
console.error('[IM] 拉取历史消息失败:', e)
} finally {
this.loadingHistory = false
}
},
async loadMoreHistory() {
if (this.historyCompleted || this.loadingHistory) return
await this.loadHistory()
},
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.targetImUserId, text, this.orderId)
this.chatMessages.push(formatIMMessage(msg, this.imUserId))
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.targetImUserId, res, this.orderId)
this.chatMessages.push(formatIMMessage(msg, this.imUserId))
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.targetImUserId, {
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.orderId)
}
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 {
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' ? '跑腿佣金' : '商品总额'
this.chatMessages.push({
id: `sys_${Date.now()}`,
type: 'system',
content: `您已${actionLabel}${changeTypeLabel}改价`
})
// 通过 IM 通知对方改价结果
if (this.imReady) {
await sendCustomMessage(this.targetImUserId, {
bizType: 'price-change-response',
priceChangeId: priceChangeId,
action: action,
status: res.status,
changeTypeLabel,
description: `${actionLabel}${changeTypeLabel}改价申请`
}, this.orderId)
}
if (action === 'Accepted') {
// 需要补价:拉起支付
if (res.paymentParams) {
try {
await this.wxPay(res.paymentParams)
this.chatMessages.push({
id: `sys_pay_${Date.now()}`,
type: 'system',
content: `已补缴¥${res.difference.toFixed(2)}`
})
} 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) {
this.chatMessages.push({
id: `sys_r_${Date.now()}`,
type: 'system',
content: res.refundSuccess ? `已退还您¥${Math.abs(res.difference).toFixed(2)}` : '退款处理中,请稍后查看'
})
}
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 lastMsg = [...this.chatMessages].reverse().find(m => m.type === 'system')
if (lastMsg && lastMsg.content === msg) return
this.chatMessages.push({
id: `sys_status_${Date.now()}`,
type: 'system',
content: msg
})
this.scrollToBottom()
},
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' })
} 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;
padding: 16rpx 0;
overflow-y: auto;
}
.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>