858 lines
22 KiB
Vue
858 lines
22 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-bar" v-if="orderInfo.id">
|
||
<view class="order-bar-info" @click="goOrderDetail">
|
||
<text class="order-bar-text">
|
||
{{ formatOrderType(orderInfo.orderType) }}订单 #{{ orderInfo.orderNo }}
|
||
</text>
|
||
<text class="order-bar-link">查看详情 ›</text>
|
||
</view>
|
||
<view class="order-bar-actions">
|
||
<view class="bar-action-btn" @click="onCallPhone">
|
||
<text>📞 拨打电话</text>
|
||
</view>
|
||
<view class="bar-action-btn" @click="onContactService">
|
||
<text>💬 联系客服</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 聊天记录区域 -->
|
||
<scroll-view
|
||
class="chat-body"
|
||
scroll-y
|
||
:scroll-top="scrollTop"
|
||
scroll-with-animation
|
||
@scrolltoupper="loadMoreHistory"
|
||
>
|
||
<view v-if="loadingHistory" class="loading-history">
|
||
<text>加载中...</text>
|
||
</view>
|
||
<view
|
||
class="msg-row"
|
||
:class="{ 'msg-self': msg.isSelf, 'msg-center': msg.type === 'system' || msg.type === 'price-change' }"
|
||
v-for="(msg, index) in chatMessages"
|
||
:key="msg.id || index"
|
||
>
|
||
<!-- 系统提示消息(居中显示) -->
|
||
<view v-if="msg.type === 'system'" class="msg-system">
|
||
<text>{{ msg.content }}</text>
|
||
</view>
|
||
|
||
<!-- 改价申请消息 -->
|
||
<view v-else-if="msg.type === 'price-change'" 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>
|
||
|
||
<!-- 普通消息(文本/图片) -->
|
||
<template v-else>
|
||
<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>
|
||
</template>
|
||
</view>
|
||
</scroll-view>
|
||
|
||
<!-- 底部输入区域 -->
|
||
<view class="chat-footer">
|
||
<input
|
||
class="chat-input"
|
||
v-model="inputText"
|
||
placeholder="输入消息..."
|
||
confirm-type="send"
|
||
@confirm="sendMessage"
|
||
/>
|
||
<view class="more-btn" @click="toggleMoreMenu">
|
||
<text class="more-icon">+</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 更多功能菜单 -->
|
||
<view v-if="showMoreMenu" class="more-menu-mask" @click="showMoreMenu = false">
|
||
<view class="more-menu" @click.stop>
|
||
<view class="menu-item" @click="onSendImage">
|
||
<text class="menu-icon">🖼️</text>
|
||
<text class="menu-label">发送图片</text>
|
||
</view>
|
||
<view class="menu-item" @click="onChangeCommission">
|
||
<text class="menu-icon">💰</text>
|
||
<text class="menu-label">更改跑腿价格</text>
|
||
</view>
|
||
<view
|
||
class="menu-item"
|
||
v-if="showGoodsPriceBtn"
|
||
@click="onChangeGoodsPrice"
|
||
>
|
||
<text class="menu-icon">🏷️</text>
|
||
<text class="menu-label">更改商品价格</text>
|
||
</view>
|
||
<view
|
||
class="menu-item"
|
||
v-if="isRunner"
|
||
@click="onCompleteOrder"
|
||
>
|
||
<text class="menu-icon">✅</text>
|
||
<text class="menu-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, logoutIM, onNewMessage, offNewMessage,
|
||
getMessageList, sendTextMessage, sendImageMessage,
|
||
sendCustomMessage, formatIMMessage
|
||
} from '../../utils/im'
|
||
|
||
export default {
|
||
data() {
|
||
return {
|
||
orderId: null,
|
||
orderInfo: {},
|
||
chatMessages: [],
|
||
inputText: '',
|
||
scrollTop: 0,
|
||
showMoreMenu: false,
|
||
// IM 相关
|
||
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
|
||
this.orderId = options.orderId || null
|
||
// 从消息列表跳转时可能只有 targetUserId
|
||
this.targetImUserIdFromParam = options.targetUserId || null
|
||
if (this.orderId) {
|
||
await this.loadOrderInfo()
|
||
}
|
||
await this.loginIM()
|
||
},
|
||
onUnload() {
|
||
// 离开页面时移除消息监听
|
||
offNewMessage()
|
||
},
|
||
methods: {
|
||
goBack() { uni.navigateBack() },
|
||
|
||
/** 加载订单信息 */
|
||
async loadOrderInfo() {
|
||
if (!this.orderId) return
|
||
try {
|
||
const res = await getOrderDetail(this.orderId)
|
||
this.orderInfo = res || {}
|
||
} catch (e) {}
|
||
},
|
||
|
||
/** 登录 IM 并加载历史消息 */
|
||
async loginIM() {
|
||
try {
|
||
const { userId } = await initIM()
|
||
this.imUserId = userId
|
||
|
||
// 确定对方 IM 用户 ID
|
||
if (this.targetImUserIdFromParam) {
|
||
// 从消息列表跳转,直接使用传入的 targetUserId
|
||
this.targetImUserId = this.targetImUserIdFromParam
|
||
} else if (this.orderInfo.id) {
|
||
const userStore = useUserStore()
|
||
if (this.orderInfo.ownerId === userStore.userId) {
|
||
// 当前用户是单主,对方是跑腿
|
||
this.targetImUserId = `user_${this.orderInfo.runnerId}`
|
||
} else {
|
||
// 当前用户是跑腿,对方是单主
|
||
this.targetImUserId = `user_${this.orderInfo.ownerId}`
|
||
}
|
||
}
|
||
|
||
this.imReady = true
|
||
|
||
// 监听新消息
|
||
onNewMessage((msgList) => {
|
||
for (const msg of msgList) {
|
||
// 只处理当前会话的消息
|
||
if (msg.from === this.targetImUserId || msg.from === this.imUserId) {
|
||
const formatted = formatIMMessage(msg, this.imUserId)
|
||
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 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
|
||
})
|
||
},
|
||
|
||
toggleMoreMenu() {
|
||
this.showMoreMenu = !this.showMoreMenu
|
||
},
|
||
|
||
/** 发送图片 */
|
||
onSendImage() {
|
||
this.showMoreMenu = false
|
||
if (!this.imReady) {
|
||
uni.showToast({ title: 'IM未就绪', icon: 'none' })
|
||
return
|
||
}
|
||
uni.chooseImage({
|
||
count: 1,
|
||
sourceType: ['album', 'camera'],
|
||
success: async (res) => {
|
||
const tempPath = res.tempFilePaths[0]
|
||
try {
|
||
const msg = await sendImageMessage(this.targetImUserId, tempPath)
|
||
this.chatMessages.push(formatIMMessage(msg, this.imUserId))
|
||
this.scrollToBottom()
|
||
} catch (e) {
|
||
uni.showToast({ title: '图片发送失败', icon: 'none' })
|
||
}
|
||
}
|
||
})
|
||
},
|
||
|
||
onChangeCommission() {
|
||
this.showMoreMenu = false
|
||
this.priceChangeType = 'Commission'
|
||
this.newPriceInput = ''
|
||
this.showPriceChangePopup = true
|
||
},
|
||
|
||
onChangeGoodsPrice() {
|
||
this.showMoreMenu = 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
|
||
|
||
const difference = this.priceDifference
|
||
|
||
if (this.isOwner && difference > 0) {
|
||
uni.showModal({
|
||
title: '补缴支付',
|
||
content: `需补缴 ¥${difference.toFixed(2)},确认支付后将发送改价申请`,
|
||
success: async (modalRes) => {
|
||
if (modalRes.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' ? '跑腿佣金' : '商品总额'
|
||
|
||
// 通过 IM 发送自定义消息通知对方
|
||
if (this.imReady) {
|
||
const customData = {
|
||
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}改价申请`
|
||
}
|
||
await sendCustomMessage(this.targetImUserId, customData)
|
||
}
|
||
|
||
// 本地显示
|
||
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_refund_${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.showMoreMenu = 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.showModal({
|
||
title: '对方手机号',
|
||
content: phone,
|
||
confirmText: '复制电话',
|
||
success: (res) => {
|
||
if (res.confirm) {
|
||
uni.setClipboardData({
|
||
data: phone,
|
||
success: () => {
|
||
uni.showToast({ title: '手机号已复制', icon: 'success' })
|
||
}
|
||
})
|
||
}
|
||
}
|
||
})
|
||
},
|
||
|
||
/** 联系客服 */
|
||
onContactService() {
|
||
// 跳转微信小程序自带客服页(需在小程序后台配置客服)
|
||
// 小程序中使用 button open-type="contact" 才能打开客服
|
||
// 这里用 navigateTo 跳转客服二维码页作为替代
|
||
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
|
||
}
|
||
}
|
||
}
|
||
</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;
|
||
}
|
||
.order-bar {
|
||
background-color: #f0f7ff;
|
||
flex-shrink: 0;
|
||
}
|
||
.order-bar-info {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 20rpx 30rpx 10rpx;
|
||
}
|
||
.order-bar-text {
|
||
font-size: 26rpx;
|
||
color: #333333;
|
||
}
|
||
.order-bar-link {
|
||
font-size: 26rpx;
|
||
color: #FFB700;
|
||
}
|
||
.order-bar-actions {
|
||
display: flex;
|
||
gap: 16rpx;
|
||
padding: 10rpx 30rpx 20rpx;
|
||
}
|
||
.bar-action-btn {
|
||
flex: 1;
|
||
text-align: center;
|
||
padding: 12rpx 0;
|
||
background-color: #ffffff;
|
||
border-radius: 8rpx;
|
||
border: 1rpx solid #e0e0e0;
|
||
}
|
||
.bar-action-btn text {
|
||
font-size: 24rpx;
|
||
color: #666666;
|
||
}
|
||
.chat-body {
|
||
flex: 1;
|
||
padding: 20rpx 24rpx;
|
||
overflow-y: auto;
|
||
}
|
||
.loading-history {
|
||
text-align: center;
|
||
padding: 16rpx 0;
|
||
font-size: 24rpx;
|
||
color: #999;
|
||
}
|
||
.msg-row {
|
||
display: flex;
|
||
margin-bottom: 24rpx;
|
||
align-items: flex-start;
|
||
}
|
||
.msg-row.msg-self {
|
||
flex-direction: row-reverse;
|
||
}
|
||
.msg-row.msg-center {
|
||
justify-content: center;
|
||
}
|
||
.msg-avatar {
|
||
width: 72rpx;
|
||
height: 72rpx;
|
||
border-radius: 50%;
|
||
flex-shrink: 0;
|
||
}
|
||
.msg-bubble {
|
||
max-width: 65%;
|
||
margin: 0 16rpx;
|
||
}
|
||
.msg-text {
|
||
background-color: #ffffff;
|
||
padding: 20rpx 24rpx;
|
||
border-radius: 16rpx;
|
||
font-size: 28rpx;
|
||
color: #333333;
|
||
word-break: break-all;
|
||
display: block;
|
||
}
|
||
.msg-self .msg-text {
|
||
background-color: #FFB700;
|
||
color: #ffffff;
|
||
}
|
||
.msg-image {
|
||
max-width: 100%;
|
||
border-radius: 12rpx;
|
||
}
|
||
.msg-system {
|
||
background-color: #f5f5f5;
|
||
padding: 12rpx 20rpx;
|
||
border-radius: 8rpx;
|
||
font-size: 24rpx;
|
||
color: #999999;
|
||
text-align: center;
|
||
max-width: 80%;
|
||
}
|
||
.chat-footer {
|
||
display: flex;
|
||
align-items: center;
|
||
background-color: #ffffff;
|
||
padding: 16rpx 24rpx;
|
||
padding-bottom: calc(16rpx + env(safe-area-inset-bottom));
|
||
border-top: 1rpx solid #eeeeee;
|
||
flex-shrink: 0;
|
||
}
|
||
.chat-input {
|
||
flex: 1;
|
||
height: 72rpx;
|
||
background-color: #f5f5f5;
|
||
border-radius: 36rpx;
|
||
padding: 0 30rpx;
|
||
font-size: 28rpx;
|
||
}
|
||
.more-btn {
|
||
width: 72rpx;
|
||
height: 72rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
margin-left: 16rpx;
|
||
flex-shrink: 0;
|
||
}
|
||
.more-icon {
|
||
font-size: 48rpx;
|
||
color: #666666;
|
||
font-weight: 300;
|
||
}
|
||
/* 更多功能菜单 */
|
||
.more-menu-mask {
|
||
position: fixed;
|
||
top: 0; left: 0; right: 0; bottom: 0;
|
||
background-color: rgba(0, 0, 0, 0.3);
|
||
z-index: 100;
|
||
display: flex;
|
||
align-items: flex-end;
|
||
}
|
||
.more-menu {
|
||
width: 100%;
|
||
background-color: #ffffff;
|
||
border-radius: 24rpx 24rpx 0 0;
|
||
padding: 30rpx;
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
}
|
||
.menu-item {
|
||
width: 25%;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
padding: 24rpx 0;
|
||
}
|
||
.menu-icon {
|
||
font-size: 48rpx;
|
||
margin-bottom: 12rpx;
|
||
}
|
||
.menu-label {
|
||
font-size: 24rpx;
|
||
color: #666666;
|
||
}
|
||
/* 改价消息卡片 */
|
||
.price-change-card {
|
||
background-color: #ffffff;
|
||
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: #333333; font-weight: 500; }
|
||
.pc-body { margin-bottom: 16rpx; }
|
||
.pc-row { display: flex; justify-content: space-between; align-items: center; padding: 8rpx 0; }
|
||
.pc-label { font-size: 24rpx; color: #999999; }
|
||
.pc-value { font-size: 26rpx; color: #333333; }
|
||
.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-color: #FFB700; color: #ffffff; }
|
||
.pc-reject { background-color: #f5f5f5; color: #666666; }
|
||
.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-color: rgba(0, 0, 0, 0.5);
|
||
z-index: 200;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
.popup-content {
|
||
width: 80%;
|
||
background-color: #ffffff;
|
||
border-radius: 20rpx;
|
||
padding: 40rpx;
|
||
}
|
||
.popup-title {
|
||
font-size: 32rpx;
|
||
color: #333333;
|
||
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: #666666; }
|
||
.popup-current-price { font-size: 28rpx; color: #333333; }
|
||
.popup-input {
|
||
width: 50%;
|
||
height: 64rpx;
|
||
border: 1rpx solid #dddddd;
|
||
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-color: #f5f5f5; color: #666666; }
|
||
.popup-confirm { background-color: #FAD146; color: #ffffff; }
|
||
</style>
|