1124 lines
26 KiB
Vue
1124 lines
26 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">
|
||
<!-- 聊天安全提示 -->
|
||
<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>
|
||
|
||
<!-- 底部输入区域 -->
|
||
<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: {},
|
||
chatMessages: [],
|
||
inputText: '',
|
||
scrollTop: 0,
|
||
showMorePanel: false,
|
||
imUserId: '',
|
||
targetImUserId: '',
|
||
imReady: false,
|
||
nextReqMessageID: '',
|
||
historyCompleted: false,
|
||
loadingHistory: false,
|
||
showPriceChangePopup: false,
|
||
priceChangeType: '',
|
||
newPriceInput: '',
|
||
submittingPriceChange: false,
|
||
statusBarHeight: 0
|
||
}
|
||
},
|
||
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() {
|
||
// 如果 orderId 存在但 orderInfo 为空,重新加载
|
||
if (this.orderId && !this.orderInfo.id) {
|
||
this.loadOrderInfo()
|
||
}
|
||
},
|
||
methods: {
|
||
goBack() {
|
||
uni.navigateBack()
|
||
},
|
||
async loadOrderInfo() {
|
||
if (!this.orderId) return
|
||
try {
|
||
const res = await getOrderDetail(this.orderId);
|
||
console.log('[聊天] 订单详情:', res)
|
||
this.orderInfo = res || {}
|
||
} 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) {
|
||
this.chatMessages.push(formatIMMessage(msg, this.imUserId))
|
||
}
|
||
}
|
||
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 formatted = res.messageList.map(m => formatIMMessage(m, this.imUserId))
|
||
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.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.tempFilePaths[0])
|
||
this.chatMessages.push(formatIMMessage(msg, this.imUserId))
|
||
this.scrollToBottom()
|
||
} catch (e) {
|
||
uni.showToast({
|
||
title: '图片发送失败',
|
||
icon: 'none'
|
||
})
|
||
}
|
||
}
|
||
})
|
||
},
|
||
onChangeCommission() {
|
||
this.showMorePanel = false;
|
||
this.priceChangeType = 'Commission'
|
||
this.newPriceInput = '';
|
||
this.showPriceChangePopup = true
|
||
},
|
||
onChangeGoodsPrice() {
|
||
this.showMorePanel = false;
|
||
this.priceChangeType = 'GoodsAmount'
|
||
this.newPriceInput = '';
|
||
this.showPriceChangePopup = true
|
||
},
|
||
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.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}改价`
|
||
})
|
||
if (action === 'Accepted' && res.difference !== 0) {
|
||
if (res.difference < 0) this.chatMessages.push({
|
||
id: `sys_r_${Date.now()}`,
|
||
type: 'system',
|
||
content: `已退还您${Math.abs(res.difference).toFixed(2)}元`
|
||
})
|
||
await this.loadOrderInfo()
|
||
}
|
||
this.scrollToBottom()
|
||
} catch (e) {
|
||
uni.showToast({
|
||
title: e.message || '操作失败',
|
||
icon: 'none'
|
||
})
|
||
}
|
||
},
|
||
onCompleteOrder() {
|
||
this.showMorePanel = false
|
||
uni.navigateTo({
|
||
url: `/pages/order/complete-order?id=${this.orderId}`
|
||
})
|
||
},
|
||
goOrderDetail() {
|
||
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'
|
||
})
|
||
},
|
||
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;
|
||
}
|
||
|
||
/* 底部输入区域 */
|
||
.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> |