962 lines
42 KiB
Plaintext
962 lines
42 KiB
Plaintext
<!-- 机构端 - 消息中心(客服工作台) -->
|
||
<template>
|
||
<view class="messages-page">
|
||
<view class="wb-header-right">
|
||
<view class="wb-status-wrap">
|
||
<view class="wb-status-dot"></view>
|
||
<text class="wb-status-text">在线</text>
|
||
</view>
|
||
<view class="wb-pending-wrap" v-if="pendingCount > 0">
|
||
<text class="wb-pending-text">{{ pendingCount }}条待处理</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- ===== 三模块 Tab 切换栏 ===== -->
|
||
<view class="wb-tabs">
|
||
<view class="wb-tab-item" :class="{ 'wb-tab-active': activeTab === 0 }" @click="activeTab = 0">
|
||
<text class="wb-tab-icon">💬</text>
|
||
<text class="wb-tab-label">会话列表</text>
|
||
<view v-if="totalUnread > 0 && activeTab !== 0" class="wb-tab-badge">
|
||
<text class="wb-tab-badge-num">{{ totalUnread }}</text>
|
||
</view>
|
||
</view>
|
||
<view class="wb-tab-item" :class="{ 'wb-tab-active': activeTab === 1 }" @click="activeTab = 1">
|
||
<text class="wb-tab-icon">🗨</text>
|
||
<text class="wb-tab-label">聊天记录</text>
|
||
</view>
|
||
<view class="wb-tab-item" :class="{ 'wb-tab-active': activeTab === 2 }" @click="activeTab = 2">
|
||
<text class="wb-tab-icon">📋</text>
|
||
<text class="wb-tab-label">用户资料</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 当前会话提示条 -->
|
||
<view v-if="activeTab !== 0" class="session-hint-bar">
|
||
<text class="session-hint-avatar">{{ currentSession.avatar }}</text>
|
||
<text class="session-hint-name">{{ currentSession.userName }}</text>
|
||
<view class="session-hint-status" :class="'shs-' + currentSession.status">
|
||
<text class="session-hint-status-text">{{ currentSession.status === 'pending' ? '待回复' : currentSession.status === 'active' ? '服务中' : '已结束' }}</text>
|
||
</view>
|
||
<view class="session-hint-switch" @click="activeTab = 0">
|
||
<text class="session-hint-switch-text">切换会话</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- ===== 内容区 ===== -->
|
||
<view class="wb-content">
|
||
|
||
<!-- ──────────── 模块一:会话列表 ──────────── -->
|
||
<view v-if="activeTab === 0" class="module-sessions">
|
||
<view class="sessions-search-wrap">
|
||
<input class="sessions-search-input" placeholder="🔍 搜索用户或会话内容..." v-model="searchKey" />
|
||
</view>
|
||
<scroll-view scroll-y class="sessions-scroll">
|
||
<view
|
||
v-for="sess in filteredSessions"
|
||
:key="sess.id"
|
||
class="session-card"
|
||
:class="{ 'session-card-active': currentSessionId === sess.id }"
|
||
@click="selectSession(sess.id)"
|
||
>
|
||
<view class="sc-avatar-wrap">
|
||
<text class="sc-avatar-emoji">{{ sess.avatar }}</text>
|
||
<view v-if="sess.unread > 0" class="sc-unread-badge">
|
||
<text class="sc-unread-num">{{ sess.unread > 99 ? '99+' : sess.unread }}</text>
|
||
</view>
|
||
</view>
|
||
<view class="sc-info">
|
||
<view class="sc-row-top">
|
||
<text class="sc-name">{{ sess.userName }}</text>
|
||
<text class="sc-time">{{ sess.lastTime }}</text>
|
||
</view>
|
||
<text class="sc-consult-type">{{ sess.consultType }}</text>
|
||
<view class="sc-row-bottom">
|
||
<text class="sc-preview" :class="{ 'sc-preview-bold': sess.unread > 0 }">{{ sess.lastMsg }}</text>
|
||
<view class="sc-status-tag" :class="'sc-status-' + sess.status">
|
||
<text class="sc-status-text">{{ sess.status === 'pending' ? '待回复' : sess.status === 'active' ? '服务中' : '已结束' }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
<text class="sc-arrow">›</text>
|
||
</view>
|
||
<view v-if="filteredSessions.length === 0" class="sessions-empty">
|
||
<text class="sessions-empty-icon">🔍</text>
|
||
<text class="sessions-empty-text">暂无匹配会话</text>
|
||
</view>
|
||
<view class="sessions-safe-bottom"></view>
|
||
</scroll-view>
|
||
</view>
|
||
|
||
<!-- ──────────── 模块二:聊天记录 ──────────── -->
|
||
<view v-else-if="activeTab === 1" class="module-chat">
|
||
<scroll-view scroll-y class="chat-messages-scroll" :scroll-into-view="scrollTarget" :scroll-with-animation="true">
|
||
<view class="chat-messages-inner">
|
||
<view
|
||
v-for="msg in currentMessages"
|
||
:key="msg.id"
|
||
:id="'msg_' + msg.id"
|
||
class="msg-row"
|
||
:class="{ 'msg-row-system': msg.type === 'system' }"
|
||
>
|
||
<!-- 系统消息 -->
|
||
<view v-if="msg.type === 'system'" class="msg-system">
|
||
<text class="msg-system-text">{{ msg.content }}</text>
|
||
</view>
|
||
<!-- 用户消息(左) -->
|
||
<view v-else-if="msg.fromUser" class="msg-left">
|
||
<text class="msg-avatar">{{ currentSession.avatar }}</text>
|
||
<view class="msg-body">
|
||
<text class="msg-sender">{{ currentSession.userName }}</text>
|
||
<view v-if="msg.type === 'text'" class="bubble bubble-user">
|
||
<text class="bubble-text">{{ msg.content }}</text>
|
||
<text class="bubble-time">{{ msg.time }}</text>
|
||
</view>
|
||
<view v-else-if="msg.type === 'image'" class="bubble bubble-user bubble-image">
|
||
<text class="bubble-image-icon">🖼</text>
|
||
<text class="bubble-image-hint">[图片消息]</text>
|
||
<text class="bubble-time">{{ msg.time }}</text>
|
||
</view>
|
||
<view v-else-if="msg.type === 'product'" class="msg-card-wrap">
|
||
<view class="product-card">
|
||
<view class="pc-thumb">
|
||
<text class="pc-thumb-emoji">{{ msg.cardData != null ? (msg.cardData as ProductCardData).emoji : '📦' }}</text>
|
||
</view>
|
||
<view class="pc-info">
|
||
<text class="pc-name">{{ msg.cardData != null ? (msg.cardData as ProductCardData).name : '' }}</text>
|
||
<text class="pc-price">¥{{ msg.cardData != null ? (msg.cardData as ProductCardData).price : '' }}</text>
|
||
<text class="pc-tag">{{ msg.cardData != null ? (msg.cardData as ProductCardData).tag : '' }}</text>
|
||
</view>
|
||
</view>
|
||
<text class="card-time">{{ msg.time }}</text>
|
||
</view>
|
||
<view v-else-if="msg.type === 'order'" class="msg-card-wrap">
|
||
<view class="order-card">
|
||
<view class="oc-row1">
|
||
<text class="oc-label">订单号</text>
|
||
<text class="oc-no">{{ msg.cardData != null ? (msg.cardData as OrderCardData).orderNo : '' }}</text>
|
||
<view class="oc-status-tag" :class="'oc-' + (msg.cardData != null ? (msg.cardData as OrderCardData).statusCode : '')">
|
||
<text class="oc-status-text">{{ msg.cardData != null ? (msg.cardData as OrderCardData).status : '' }}</text>
|
||
</view>
|
||
</view>
|
||
<text class="oc-amount">¥{{ msg.cardData != null ? (msg.cardData as OrderCardData).amount : '' }}</text>
|
||
<text class="oc-items">{{ msg.cardData != null ? (msg.cardData as OrderCardData).itemsDesc : '' }}</text>
|
||
</view>
|
||
<text class="card-time">{{ msg.time }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
<!-- 商家消息(右) -->
|
||
<view v-else class="msg-right">
|
||
<view class="msg-body msg-body-right">
|
||
<view v-if="msg.type === 'text'" class="bubble bubble-me">
|
||
<text class="bubble-text">{{ msg.content }}</text>
|
||
<text class="bubble-time bubble-time-right">{{ msg.time }}</text>
|
||
</view>
|
||
<view v-else-if="msg.type === 'image'" class="bubble bubble-me bubble-image">
|
||
<text class="bubble-image-icon">🖼</text>
|
||
<text class="bubble-image-hint">[图片消息]</text>
|
||
<text class="bubble-time bubble-time-right">{{ msg.time }}</text>
|
||
</view>
|
||
<view v-else-if="msg.type === 'product'" class="msg-card-wrap msg-card-right">
|
||
<view class="product-card">
|
||
<view class="pc-thumb">
|
||
<text class="pc-thumb-emoji">{{ msg.cardData != null ? (msg.cardData as ProductCardData).emoji : '📦' }}</text>
|
||
</view>
|
||
<view class="pc-info">
|
||
<text class="pc-name">{{ msg.cardData != null ? (msg.cardData as ProductCardData).name : '' }}</text>
|
||
<text class="pc-price">¥{{ msg.cardData != null ? (msg.cardData as ProductCardData).price : '' }}</text>
|
||
<text class="pc-tag">{{ msg.cardData != null ? (msg.cardData as ProductCardData).tag : '' }}</text>
|
||
</view>
|
||
</view>
|
||
<text class="card-time card-time-right">{{ msg.time }}</text>
|
||
</view>
|
||
<view v-else-if="msg.type === 'order'" class="msg-card-wrap msg-card-right">
|
||
<view class="order-card">
|
||
<view class="oc-row1">
|
||
<text class="oc-label">订单号</text>
|
||
<text class="oc-no">{{ msg.cardData != null ? (msg.cardData as OrderCardData).orderNo : '' }}</text>
|
||
<view class="oc-status-tag" :class="'oc-' + (msg.cardData != null ? (msg.cardData as OrderCardData).statusCode : '')">
|
||
<text class="oc-status-text">{{ msg.cardData != null ? (msg.cardData as OrderCardData).status : '' }}</text>
|
||
</view>
|
||
</view>
|
||
<text class="oc-amount">¥{{ msg.cardData != null ? (msg.cardData as OrderCardData).amount : '' }}</text>
|
||
<text class="oc-items">{{ msg.cardData != null ? (msg.cardData as OrderCardData).itemsDesc : '' }}</text>
|
||
</view>
|
||
<text class="card-time card-time-right">{{ msg.time }}</text>
|
||
</view>
|
||
</view>
|
||
<text class="msg-avatar me-avatar">🏥</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</scroll-view>
|
||
|
||
<!-- 底部输入区 -->
|
||
<view class="chat-input-area">
|
||
<view v-if="showQuickReplies" class="quick-reply-panel">
|
||
<view class="qr-header">
|
||
<text class="qr-title">⚡ 快捷回复</text>
|
||
<text class="qr-close" @click="showQuickReplies = false">✕</text>
|
||
</view>
|
||
<scroll-view scroll-y class="qr-scroll">
|
||
<view v-for="qr in quickReplies" :key="qr" class="qr-item" @click="sendQuickReply(qr)">
|
||
<text class="qr-text">{{ qr }}</text>
|
||
</view>
|
||
</scroll-view>
|
||
</view>
|
||
<view class="chat-toolbar">
|
||
<view class="toolbar-btn" @click="insertImageMsg">
|
||
<text class="toolbar-icon">🖼</text>
|
||
<text class="toolbar-label">图片</text>
|
||
</view>
|
||
<view class="toolbar-btn" @click="insertProductMsg">
|
||
<text class="toolbar-icon">📦</text>
|
||
<text class="toolbar-label">商品</text>
|
||
</view>
|
||
<view class="toolbar-btn" @click="insertOrderMsg">
|
||
<text class="toolbar-icon">📋</text>
|
||
<text class="toolbar-label">订单</text>
|
||
</view>
|
||
<view class="toolbar-btn" @click="showQuickReplies = !showQuickReplies" :class="{ 'toolbar-btn-active': showQuickReplies }">
|
||
<text class="toolbar-icon">⚡</text>
|
||
<text class="toolbar-label">快捷</text>
|
||
</view>
|
||
</view>
|
||
<view class="input-row">
|
||
<textarea class="chat-textarea" v-model="inputText" placeholder="输入回复内容..." :maxlength="500" />
|
||
<view class="send-btn" :class="{ 'send-btn-active': inputText.trim() !== '' }" @click="sendTextMsg">
|
||
<text class="send-btn-text">发送</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
<!-- 为固定 TabBar 留出空间 -->
|
||
<view class="chat-tabbar-spacer"></view>
|
||
</view>
|
||
|
||
<!-- ──────────── 模块三:用户资料 ──────────── -->
|
||
<view v-else-if="activeTab === 2" class="module-info">
|
||
<scroll-view scroll-y class="info-scroll">
|
||
<view class="info-user-card">
|
||
<text class="info-big-avatar">{{ currentSession.avatar }}</text>
|
||
<view class="info-user-main">
|
||
<text class="info-user-name">{{ currentSession.userName }}</text>
|
||
<view class="info-tags-row">
|
||
<view v-for="tag in (currentUserInfo.tags as string[])" :key="tag" class="info-tag">
|
||
<text class="info-tag-text">{{ tag }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="info-section">
|
||
<view class="info-section-header">
|
||
<text class="info-section-icon">👤</text>
|
||
<text class="info-section-title">基本信息</text>
|
||
</view>
|
||
<view class="info-kv">
|
||
<text class="info-key">手机号</text>
|
||
<text class="info-val">{{ currentUserInfo.phone }}</text>
|
||
</view>
|
||
<view class="info-kv">
|
||
<text class="info-key">注册时间</text>
|
||
<text class="info-val">{{ currentUserInfo.registerDate }}</text>
|
||
</view>
|
||
<view class="info-kv">
|
||
<text class="info-key">历史订单</text>
|
||
<text class="info-val info-val-red">{{ currentUserInfo.totalOrders }} 单</text>
|
||
</view>
|
||
<view class="info-kv">
|
||
<text class="info-key">累计消费</text>
|
||
<text class="info-val info-val-red">¥{{ currentUserInfo.totalSpent }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="info-section" v-if="currentUserInfo.consultProduct != null">
|
||
<view class="info-section-header">
|
||
<text class="info-section-icon">🛒</text>
|
||
<text class="info-section-title">正在咨询</text>
|
||
</view>
|
||
<view class="consult-product">
|
||
<text class="cp-big-emoji">{{ (currentUserInfo.consultProduct as ConsultProduct).emoji }}</text>
|
||
<view class="cp-detail">
|
||
<text class="cp-pname">{{ (currentUserInfo.consultProduct as ConsultProduct).name }}</text>
|
||
<text class="cp-pprice">¥{{ (currentUserInfo.consultProduct as ConsultProduct).price }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="info-section">
|
||
<view class="info-section-header">
|
||
<text class="info-section-icon">👁</text>
|
||
<text class="info-section-title">最近浏览</text>
|
||
</view>
|
||
<view v-for="item in (currentUserInfo.recentViewed as RecentItem[])" :key="item.name" class="recent-row">
|
||
<text class="recent-emoji">{{ item.emoji }}</text>
|
||
<view class="recent-detail">
|
||
<text class="recent-name">{{ item.name }}</text>
|
||
<text class="recent-price">¥{{ item.price }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="info-section">
|
||
<view class="info-section-header">
|
||
<text class="info-section-icon">📋</text>
|
||
<text class="info-section-title">最近订单</text>
|
||
</view>
|
||
<view v-for="order in (currentUserInfo.recentOrders as RecentOrder[])" :key="order.no" class="order-mini-card">
|
||
<view class="omc-row1">
|
||
<text class="omc-no">{{ order.no }}</text>
|
||
<view class="omc-status-tag" :class="'omc-' + order.statusCode">
|
||
<text class="omc-status-text">{{ order.status }}</text>
|
||
</view>
|
||
</view>
|
||
<view class="omc-row2">
|
||
<text class="omc-items">{{ order.itemsDesc }}</text>
|
||
<text class="omc-amount">¥{{ order.amount }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="info-actions">
|
||
<view class="info-action-btn primary" @click="goToChat">
|
||
<text class="info-action-text">进入聊天</text>
|
||
</view>
|
||
<view class="info-action-btn" @click="closeSession">
|
||
<text class="info-action-text">结束会话</text>
|
||
</view>
|
||
</view>
|
||
<view class="info-safe-bottom"></view>
|
||
</scroll-view>
|
||
</view>
|
||
|
||
</view>
|
||
|
||
<!-- 商家端自定义 TabBar -->
|
||
<merchant-tab-bar :current="1"></merchant-tab-bar>
|
||
</view>
|
||
</template>
|
||
|
||
<script lang="uts">
|
||
import MerchantTabBar from '@/components/merchant-tabbar/MerchantTabBar.uvue'
|
||
import { requireMerchantAuth } from '@/utils/merchantAuth.uts'
|
||
|
||
type ProductCardData = {
|
||
emoji: string
|
||
name: string
|
||
price: string
|
||
tag: string
|
||
}
|
||
|
||
type OrderCardData = {
|
||
orderNo: string
|
||
status: string
|
||
statusCode: string
|
||
amount: string
|
||
itemsDesc: string
|
||
}
|
||
|
||
type ChatMessage = {
|
||
id: string
|
||
fromUser: boolean
|
||
type: string
|
||
content: string
|
||
time: string
|
||
cardData: ProductCardData | OrderCardData | null
|
||
}
|
||
|
||
type Session = {
|
||
id: string
|
||
userName: string
|
||
avatar: string
|
||
lastMsg: string
|
||
lastTime: string
|
||
unread: number
|
||
status: string
|
||
consultType: string
|
||
}
|
||
|
||
type ConsultProduct = {
|
||
emoji: string
|
||
name: string
|
||
price: string
|
||
}
|
||
|
||
type RecentItem = {
|
||
emoji: string
|
||
name: string
|
||
price: string
|
||
}
|
||
|
||
type RecentOrder = {
|
||
no: string
|
||
status: string
|
||
statusCode: string
|
||
amount: string
|
||
itemsDesc: string
|
||
}
|
||
|
||
type UserInfo = {
|
||
phone: string
|
||
registerDate: string
|
||
totalOrders: number
|
||
totalSpent: string
|
||
tags: string[]
|
||
consultProduct: ConsultProduct | null
|
||
recentViewed: RecentItem[]
|
||
recentOrders: RecentOrder[]
|
||
}
|
||
|
||
const MOCK_SESSIONS: Session[] = [
|
||
{ id: 'sess_001', userName: '李奶奶(家属:李女士)', avatar: '👩🦳', lastMsg: '请问居家护理服务是每天上门吗?', lastTime: '10:32', unread: 3, status: 'pending', consultType: '居家护理咨询' },
|
||
{ id: 'sess_002', userName: '王大爷', avatar: '👴', lastMsg: '好的,我明白了,谢谢!', lastTime: '09:15', unread: 0, status: 'active', consultType: '慢病管理套餐' },
|
||
{ id: 'sess_003', userName: '张先生(代父咨询)', avatar: '🧑', lastMsg: '这个订单什么时候能安排上门?', lastTime: '昨天', unread: 1, status: 'pending', consultType: '订单跟进' }
|
||
]
|
||
|
||
const MOCK_MESSAGES: Record<string, ChatMessage[]> = {
|
||
'sess_001': [
|
||
{ id: 'm001', fromUser: false, type: 'system', content: '会话开始 · 居家护理咨询 · 2026-03-24 10:20', time: '', cardData: null },
|
||
{ id: 'm002', fromUser: true, type: 'text', content: '您好,我是李奶奶的女儿,想咨询一下居家护理服务的安排。', time: '10:20', cardData: null },
|
||
{ id: 'm003', fromUser: false, type: 'text', content: '您好!感谢您的咨询,我们的居家护理服务可以每日或隔日上门,具体频次根据长者身体状况定制。', time: '10:21', cardData: null },
|
||
{ id: 'm004', fromUser: true, type: 'text', content: '请问上门的护理员都是专业持证的吗?', time: '10:22', cardData: null },
|
||
{ id: 'm005', fromUser: false, type: 'text', content: '是的,我们所有上门人员均持有护理员资格证,并经过机构内部培训和背景审查。', time: '10:23', cardData: null },
|
||
{ id: 'm006', fromUser: false, type: 'product', content: '', time: '10:25', cardData: { emoji: '🩺', name: '居家护理基础套餐(月服务)', price: '2800', tag: '每日上门 · 含基础生活护理' } as ProductCardData },
|
||
{ id: 'm007', fromUser: true, type: 'image', content: '', time: '10:28', cardData: null },
|
||
{ id: 'm008', fromUser: true, type: 'text', content: '请问居家护理服务是每天上门吗?', time: '10:32', cardData: null }
|
||
],
|
||
'sess_002': [
|
||
{ id: 'm101', fromUser: false, type: 'system', content: '会话开始 · 慢病管理套餐 · 2026-03-24 09:00', time: '', cardData: null },
|
||
{ id: 'm102', fromUser: true, type: 'text', content: '我父亲有高血压和糖尿病,想了解一下慢病管理套餐包含哪些内容?', time: '09:00', cardData: null },
|
||
{ id: 'm103', fromUser: false, type: 'text', content: '您好!我们慢病管理套餐包含:每月4次上门随访、血压血糖监测记录、用药指导和饮食建议,以及每季度体检报告解读。', time: '09:01', cardData: null },
|
||
{ id: 'm104', fromUser: false, type: 'product', content: '', time: '09:02', cardData: { emoji: '💊', name: '慢病综合管理套餐(季度)', price: '4200', tag: '含随访+监测+体检解读' } as ProductCardData },
|
||
{ id: 'm105', fromUser: true, type: 'text', content: '价格合理,这个套餐可以报销医保吗?', time: '09:05', cardData: null },
|
||
{ id: 'm106', fromUser: false, type: 'text', content: '目前部分项目可以使用长护险,具体需要根据您所在区域的长护险政策,我们可以协助您申请。', time: '09:06', cardData: null },
|
||
{ id: 'm107', fromUser: true, type: 'order', content: '', time: '09:10', cardData: { orderNo: 'SV20240315002', status: '服务中', statusCode: 'active', amount: '2800.00', itemsDesc: '居家护理基础套餐 × 1月' } as OrderCardData },
|
||
{ id: 'm108', fromUser: true, type: 'text', content: '好的,我明白了,谢谢!', time: '09:15', cardData: null }
|
||
],
|
||
'sess_003': [
|
||
{ id: 'm201', fromUser: false, type: 'system', content: '会话开始 · 订单跟进 · 2026-03-23 15:00', time: '', cardData: null },
|
||
{ id: 'm202', fromUser: true, type: 'text', content: '你好,我父亲上周下了一个陪诊服务的订单,现在是什么状态了?', time: '昨天 15:00', cardData: null },
|
||
{ id: 'm203', fromUser: false, type: 'text', content: '您好!请问您的订单号是多少?我来为您查询。', time: '昨天 15:01', cardData: null },
|
||
{ id: 'm204', fromUser: true, type: 'order', content: '', time: '昨天 15:02', cardData: { orderNo: 'SV20240320007', status: '待上门', statusCode: 'pending', amount: '380.00', itemsDesc: '专业陪诊服务(半日) × 1次' } as OrderCardData },
|
||
{ id: 'm205', fromUser: false, type: 'text', content: '已为您查到,该订单目前处于"待上门"状态,陪诊师将于明天上午9:00~10:00联系您父亲确认时间。', time: '昨天 15:05', cardData: null },
|
||
{ id: 'm206', fromUser: false, type: 'text', content: '您也可以在小程序"服务订单"中实时查看服务进度,如需备注特殊要求请告知。', time: '昨天 15:06', cardData: null },
|
||
{ id: 'm207', fromUser: true, type: 'text', content: '这个订单什么时候能安排上门?', time: '昨天 15:08', cardData: null }
|
||
]
|
||
}
|
||
|
||
const MOCK_USER_INFO: Record<string, UserInfo> = {
|
||
'sess_001': {
|
||
phone: '186****3421', registerDate: '2025-11-10', totalOrders: 4, totalSpent: '8,400',
|
||
tags: ['长者家属', '居家护理', '高意向'],
|
||
consultProduct: { emoji: '🩺', name: '居家护理基础套餐', price: '2800' } as ConsultProduct,
|
||
recentViewed: [
|
||
{ emoji: '🩺', name: '居家护理基础套餐', price: '2800' } as RecentItem,
|
||
{ emoji: '🛁', name: '洗浴护理服务', price: '188' } as RecentItem,
|
||
{ emoji: '🦽', name: '轮椅租赁月服务', price: '360' } as RecentItem
|
||
],
|
||
recentOrders: [
|
||
{ no: 'SV20240210003', status: '已完成', statusCode: 'done', amount: '2800', itemsDesc: '居家护理基础套餐' } as RecentOrder,
|
||
{ no: 'SV20240115001', status: '已完成', statusCode: 'done', amount: '188', itemsDesc: '洗浴护理服务 × 1次' } as RecentOrder
|
||
]
|
||
},
|
||
'sess_002': {
|
||
phone: '139****8802', registerDate: '2025-08-22', totalOrders: 7, totalSpent: '15,600',
|
||
tags: ['高消费', '慢病管理', '复购客户'],
|
||
consultProduct: { emoji: '💊', name: '慢病综合管理套餐', price: '4200' } as ConsultProduct,
|
||
recentViewed: [
|
||
{ emoji: '💊', name: '慢病综合管理套餐', price: '4200' } as RecentItem,
|
||
{ emoji: '🩸', name: '上门抽血检测服务', price: '98' } as RecentItem,
|
||
{ emoji: '❤️', name: '心电图上门检测', price: '128' } as RecentItem
|
||
],
|
||
recentOrders: [
|
||
{ no: 'SV20240315002', status: '服务中', statusCode: 'active', amount: '2800', itemsDesc: '居家护理基础套餐' } as RecentOrder,
|
||
{ no: 'SV20240201008', status: '已完成', statusCode: 'done', amount: '4200', itemsDesc: '慢病综合管理套餐' } as RecentOrder
|
||
]
|
||
},
|
||
'sess_003': {
|
||
phone: '158****7760', registerDate: '2026-01-05', totalOrders: 1, totalSpent: '380',
|
||
tags: ['新用户', '陪诊咨询'],
|
||
consultProduct: { emoji: '🏥', name: '专业陪诊服务(半日)', price: '380' } as ConsultProduct,
|
||
recentViewed: [
|
||
{ emoji: '🏥', name: '专业陪诊服务(半日)', price: '380' } as RecentItem,
|
||
{ emoji: '🚑', name: '专业陪诊服务(全日)', price: '680' } as RecentItem
|
||
],
|
||
recentOrders: [
|
||
{ no: 'SV20240320007', status: '待上门', statusCode: 'pending', amount: '380', itemsDesc: '专业陪诊服务(半日)× 1次' } as RecentOrder
|
||
]
|
||
}
|
||
}
|
||
|
||
const QUICK_REPLIES: string[] = [
|
||
'您好,感谢您的咨询!我是在线客服,请问有什么可以帮您?',
|
||
'好的,我马上为您查询,请稍候。',
|
||
'我们的服务人员均持有专业资格证,请放心。',
|
||
'您的订单已安排,服务人员将提前联系您确认时间。',
|
||
'如需进一步了解,欢迎预约免费上门评估服务。',
|
||
'感谢您的信任,祝长辈身体健康!'
|
||
]
|
||
|
||
export default {
|
||
components: { MerchantTabBar },
|
||
data() {
|
||
return {
|
||
sessions: MOCK_SESSIONS as Session[],
|
||
messageMap: MOCK_MESSAGES as Record<string, ChatMessage[]>,
|
||
userInfoMap: MOCK_USER_INFO as Record<string, UserInfo>,
|
||
quickReplies: QUICK_REPLIES as string[],
|
||
activeTab: 0,
|
||
currentSessionId: 'sess_001',
|
||
inputText: '',
|
||
scrollTarget: '',
|
||
showQuickReplies: false,
|
||
searchKey: ''
|
||
}
|
||
},
|
||
|
||
computed: {
|
||
filteredSessions(): Session[] {
|
||
if (this.searchKey.trim() === '') return this.sessions
|
||
const key = this.searchKey.toLowerCase()
|
||
return this.sessions.filter((s: Session) =>
|
||
s.userName.toLowerCase().includes(key) || s.lastMsg.toLowerCase().includes(key)
|
||
)
|
||
},
|
||
currentSession(): Session {
|
||
const found = this.sessions.find((s: Session) => s.id === this.currentSessionId)
|
||
return found ?? this.sessions[0]
|
||
},
|
||
currentMessages(): ChatMessage[] {
|
||
return this.messageMap[this.currentSessionId] ?? []
|
||
},
|
||
currentUserInfo(): UserInfo {
|
||
return this.userInfoMap[this.currentSessionId] ?? this.userInfoMap['sess_001']
|
||
},
|
||
pendingCount(): number {
|
||
let count = 0
|
||
for (let i = 0; i < this.sessions.length; i++) {
|
||
if (this.sessions[i].status === 'pending') count++
|
||
}
|
||
return count
|
||
},
|
||
totalUnread(): number {
|
||
let total = 0
|
||
for (let i = 0; i < this.sessions.length; i++) {
|
||
total += this.sessions[i].unread
|
||
}
|
||
return total
|
||
}
|
||
},
|
||
|
||
onShow() {
|
||
this.handlePageShow()
|
||
},
|
||
|
||
methods: {
|
||
async ensureMerchantAuth(): Promise<boolean> {
|
||
const result = await requireMerchantAuth({ redirectOnFail: true, toastOnFail: true })
|
||
return result.ok
|
||
},
|
||
|
||
async handlePageShow() {
|
||
const passed = await this.ensureMerchantAuth()
|
||
if (!passed) return
|
||
this.scrollToBottom()
|
||
},
|
||
|
||
selectSession(id: string) {
|
||
this.currentSessionId = id
|
||
this.showQuickReplies = false
|
||
const idx = this.sessions.findIndex((s: Session) => s.id === id)
|
||
if (idx >= 0) {
|
||
this.sessions[idx].unread = 0
|
||
if (this.sessions[idx].status === 'pending') this.sessions[idx].status = 'active'
|
||
}
|
||
this.activeTab = 1
|
||
setTimeout(() => { this.scrollToBottom() }, 150)
|
||
},
|
||
|
||
goToChat() {
|
||
this.activeTab = 1
|
||
setTimeout(() => { this.scrollToBottom() }, 150)
|
||
},
|
||
|
||
scrollToBottom() {
|
||
const msgs = this.currentMessages
|
||
if (msgs.length === 0) return
|
||
const last = msgs[msgs.length - 1]
|
||
this.scrollTarget = ''
|
||
setTimeout(() => { this.scrollTarget = 'msg_' + last.id }, 100)
|
||
},
|
||
|
||
nowTime(): string {
|
||
const d = new Date()
|
||
return d.getHours().toString().padStart(2, '0') + ':' + d.getMinutes().toString().padStart(2, '0')
|
||
},
|
||
|
||
pushMessage(msg: ChatMessage) {
|
||
if (this.messageMap[this.currentSessionId] == null) {
|
||
this.messageMap[this.currentSessionId] = []
|
||
}
|
||
this.messageMap[this.currentSessionId].push(msg)
|
||
const idx = this.sessions.findIndex((s: Session) => s.id === this.currentSessionId)
|
||
if (idx >= 0) {
|
||
this.sessions[idx].lastMsg = msg.type === 'text' ? msg.content :
|
||
msg.type === 'image' ? '[图片]' :
|
||
msg.type === 'product' ? '[商品推荐]' : '[订单信息]'
|
||
this.sessions[idx].lastTime = this.nowTime()
|
||
}
|
||
setTimeout(() => { this.scrollToBottom() }, 100)
|
||
},
|
||
|
||
sendTextMsg() {
|
||
const text = this.inputText.trim()
|
||
if (text === '') return
|
||
this.inputText = ''
|
||
this.showQuickReplies = false
|
||
this.pushMessage({ id: 'local_' + Date.now().toString(), fromUser: false, type: 'text', content: text, time: this.nowTime(), cardData: null } as ChatMessage)
|
||
},
|
||
|
||
sendQuickReply(text: string) {
|
||
this.showQuickReplies = false
|
||
this.pushMessage({ id: 'qr_' + Date.now().toString(), fromUser: false, type: 'text', content: text, time: this.nowTime(), cardData: null } as ChatMessage)
|
||
},
|
||
|
||
insertImageMsg() {
|
||
this.pushMessage({ id: 'img_' + Date.now().toString(), fromUser: false, type: 'image', content: '', time: this.nowTime(), cardData: null } as ChatMessage)
|
||
},
|
||
|
||
insertProductMsg() {
|
||
this.pushMessage({ id: 'prod_' + Date.now().toString(), fromUser: false, type: 'product', content: '', time: this.nowTime(), cardData: { emoji: '🩺', name: '居家护理定制套餐', price: '3200', tag: '可定制频次,支持长护险' } as ProductCardData } as ChatMessage)
|
||
},
|
||
|
||
insertOrderMsg() {
|
||
this.pushMessage({ id: 'ord_' + Date.now().toString(), fromUser: false, type: 'order', content: '', time: this.nowTime(), cardData: { orderNo: 'SV' + Date.now().toString().slice(-8), status: '待接单', statusCode: 'pending', amount: '2800.00', itemsDesc: '居家护理基础套餐 × 1月' } as OrderCardData } as ChatMessage)
|
||
},
|
||
|
||
closeSession() {
|
||
uni.showModal({
|
||
title: '结束会话',
|
||
content: '确认结束与该用户的当前咨询会话吗?',
|
||
success: (res) => {
|
||
if (res.confirm) {
|
||
const idx = this.sessions.findIndex((s: Session) => s.id === this.currentSessionId)
|
||
if (idx >= 0) this.sessions[idx].status = 'closed'
|
||
this.pushMessage({ id: 'sys_' + Date.now().toString(), fromUser: false, type: 'system', content: '会话已由客服结束 · ' + this.nowTime(), time: '', cardData: null } as ChatMessage)
|
||
this.activeTab = 0
|
||
}
|
||
}
|
||
})
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style>
|
||
/* ===== 页面容器 ===== */
|
||
.messages-page {
|
||
display: flex;
|
||
flex-direction: column;
|
||
height: 100vh;
|
||
background-color: #f4f6f9;
|
||
}
|
||
|
||
/* ===== 顶部导航(微信小程序) ===== */
|
||
.mp-tab-navbar {
|
||
background-color: #09C39D;
|
||
padding-top: var(--status-bar-height);
|
||
flex-shrink: 0;
|
||
}
|
||
.wb-navbar-content {
|
||
height: 88rpx;
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
padding: 0 24rpx;
|
||
}
|
||
.mp-tab-title {
|
||
font-size: 34rpx;
|
||
font-weight: bold;
|
||
color: #fff;
|
||
flex: 1;
|
||
}
|
||
.wb-header-right {
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
justify-content: flex-end;
|
||
gap: 16rpx;
|
||
padding: calc(16rpx + var(--status-bar-height)) 24rpx 16rpx;
|
||
background-color: #09C39D;
|
||
}
|
||
.wb-status-wrap {
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
background-color: rgba(255,255,255,0.15);
|
||
border-radius: 20rpx;
|
||
padding: 6rpx 16rpx;
|
||
}
|
||
.wb-status-dot {
|
||
width: 14rpx;
|
||
height: 14rpx;
|
||
border-radius: 7rpx;
|
||
background-color: #52c41a;
|
||
margin-right: 8rpx;
|
||
}
|
||
.wb-status-text { font-size: 22rpx; color: #fff; }
|
||
.wb-pending-wrap { background-color: #ff4d4f; border-radius: 20rpx; padding: 6rpx 16rpx; }
|
||
.wb-pending-text { font-size: 22rpx; color: #fff; font-weight: bold; }
|
||
|
||
/* ===== 三模块 Tab 切换栏 ===== */
|
||
.wb-tabs {
|
||
display: flex;
|
||
flex-direction: row;
|
||
background-color: #fff;
|
||
border-bottom-width: 2rpx;
|
||
border-bottom-style: solid;
|
||
border-bottom-color: #e8e8e8;
|
||
flex-shrink: 0;
|
||
}
|
||
.wb-tab-item {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
padding: 20rpx 0 16rpx;
|
||
position: relative;
|
||
}
|
||
.wb-tab-item:active { background-color: #f5f7fa; }
|
||
.wb-tab-icon { font-size: 38rpx; }
|
||
.wb-tab-label { font-size: 24rpx; color: #888; margin-top: 6rpx; }
|
||
.wb-tab-active .wb-tab-label { color: #09C39D; font-weight: bold; }
|
||
.wb-tab-active { border-bottom-width: 4rpx; border-bottom-style: solid; border-bottom-color: #09C39D; }
|
||
.wb-tab-badge { position: absolute; top: 10rpx; right: 16rpx; min-width: 28rpx; height: 28rpx; background-color: #ff4d4f; border-radius: 14rpx; display: flex; align-items: center; justify-content: center; padding: 0 6rpx; }
|
||
.wb-tab-badge-num { font-size: 18rpx; color: #fff; font-weight: bold; }
|
||
|
||
/* ===== 当前会话提示条 ===== */
|
||
.session-hint-bar {
|
||
background-color: #f0f7ff;
|
||
border-bottom-width: 1rpx;
|
||
border-bottom-style: solid;
|
||
border-bottom-color: #d0e8ff;
|
||
padding: 14rpx 24rpx;
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
flex-shrink: 0;
|
||
}
|
||
.session-hint-avatar { font-size: 32rpx; margin-right: 10rpx; }
|
||
.session-hint-name { font-size: 24rpx; color: #09C39D; font-weight: bold; flex: 1; }
|
||
.session-hint-status { border-radius: 8rpx; padding: 2rpx 12rpx; margin-right: 16rpx; }
|
||
.shs-pending { background-color: #fff3e0; }
|
||
.shs-pending .session-hint-status-text { font-size: 20rpx; color: #ff8c00; }
|
||
.shs-active { background-color: #e8f5e9; }
|
||
.shs-active .session-hint-status-text { font-size: 20rpx; color: #388e3c; }
|
||
.shs-closed { background-color: #f5f5f5; }
|
||
.shs-closed .session-hint-status-text { font-size: 20rpx; color: #999; }
|
||
.session-hint-switch { background-color: #09C39D; border-radius: 20rpx; padding: 6rpx 20rpx; }
|
||
.session-hint-switch-text { font-size: 20rpx; color: #fff; }
|
||
|
||
/* ===== 内容区(flex:1 填充剩余高度) ===== */
|
||
.wb-content {
|
||
flex: 1;
|
||
overflow: hidden;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
/* ====== 模块一:会话列表 ====== */
|
||
.module-sessions { flex: 1; display: flex; flex-direction: column; background-color: #fff; overflow: hidden; }
|
||
.sessions-search-wrap {
|
||
padding: 24rpx;
|
||
background-color: #f8f9fc;
|
||
border-bottom-width: 1rpx;
|
||
border-bottom-style: solid;
|
||
border-bottom-color: #eee;
|
||
flex-shrink: 0;
|
||
}
|
||
.sessions-search-input {
|
||
background-color: #fff;
|
||
border-width: 1rpx;
|
||
border-style: solid;
|
||
border-color: #d0d0d0;
|
||
border-radius: 44rpx;
|
||
height: 88rpx;
|
||
line-height: 88rpx;
|
||
padding: 0 32rpx;
|
||
font-size: 28rpx;
|
||
width: 100%;
|
||
}
|
||
.sessions-scroll { flex: 1; height: 0; }
|
||
|
||
.session-card { display: flex; flex-direction: row; align-items: center; padding: 28rpx 24rpx; border-bottom-width: 1rpx; border-bottom-style: solid; border-bottom-color: #f0f0f0; }
|
||
.session-card:active { background-color: #f0f7ff; }
|
||
.session-card-active { background-color: #E3F7ED; border-left-width: 6rpx; border-left-style: solid; border-left-color: #09C39D; }
|
||
.sc-avatar-wrap { position: relative; margin-right: 20rpx; flex-shrink: 0; }
|
||
.sc-avatar-emoji { font-size: 80rpx; }
|
||
.sc-unread-badge { position: absolute; top: -6rpx; right: -6rpx; min-width: 32rpx; height: 32rpx; background-color: #ff4d4f; border-radius: 16rpx; display: flex; align-items: center; justify-content: center; padding: 0 8rpx; border-width: 2rpx; border-style: solid; border-color: #fff; }
|
||
.sc-unread-num { font-size: 18rpx; color: #fff; font-weight: bold; }
|
||
.sc-info { flex: 1; overflow: hidden; }
|
||
.sc-row-top { display: flex; flex-direction: row; justify-content: space-between; align-items: center; margin-bottom: 4rpx; }
|
||
.sc-name { font-size: 30rpx; font-weight: bold; color: #1a1a1a; flex: 1; }
|
||
.sc-time { font-size: 22rpx; color: #aaa; flex-shrink: 0; margin-left: 16rpx; }
|
||
.sc-consult-type { font-size: 22rpx; color: #09C39D; display: block; margin-bottom: 8rpx; }
|
||
.sc-row-bottom { display: flex; flex-direction: row; align-items: center; }
|
||
.sc-preview { font-size: 26rpx; color: #999; flex: 1; }
|
||
.sc-preview-bold { color: #333; font-weight: bold; }
|
||
.sc-status-tag { border-radius: 8rpx; padding: 4rpx 14rpx; flex-shrink: 0; margin-left: 12rpx; }
|
||
.sc-status-pending { background-color: #fff3e0; }
|
||
.sc-status-pending .sc-status-text { color: #ff8c00; font-size: 22rpx; }
|
||
.sc-status-active { background-color: #e8f5e9; }
|
||
.sc-status-active .sc-status-text { color: #388e3c; font-size: 22rpx; }
|
||
.sc-status-closed { background-color: #f5f5f5; }
|
||
.sc-status-closed .sc-status-text { color: #999; font-size: 22rpx; }
|
||
.sc-arrow { font-size: 36rpx; color: #ccc; margin-left: 12rpx; flex-shrink: 0; }
|
||
.sessions-empty { padding: 100rpx 0; display: flex; flex-direction: column; align-items: center; }
|
||
.sessions-empty-icon { font-size: 80rpx; margin-bottom: 20rpx; }
|
||
.sessions-empty-text { font-size: 28rpx; color: #bbb; }
|
||
.sessions-safe-bottom { height: 140rpx; }
|
||
|
||
/* ====== 模块二:聊天记录 ====== */
|
||
.module-chat { flex: 1; display: flex; flex-direction: column; background-color: #f4f6f9; overflow: hidden; }
|
||
.chat-messages-scroll { flex: 1; height: 0; }
|
||
.chat-messages-inner { padding: 24rpx 20rpx 20rpx; display: flex; flex-direction: column; }
|
||
|
||
.msg-row { margin-bottom: 28rpx; }
|
||
.msg-row-system { display: flex; justify-content: center; }
|
||
.msg-system { background-color: rgba(0,0,0,0.05); border-radius: 10rpx; padding: 10rpx 24rpx; }
|
||
.msg-system-text { font-size: 22rpx; color: #999; }
|
||
|
||
.msg-left { display: flex; flex-direction: row; align-items: flex-start; }
|
||
.msg-right { display: flex; flex-direction: row; align-items: flex-start; justify-content: flex-end; }
|
||
.msg-avatar { font-size: 64rpx; flex-shrink: 0; }
|
||
.me-avatar { margin-left: 14rpx; }
|
||
.msg-body { margin-left: 14rpx; max-width: 80%; }
|
||
.msg-body-right { margin-left: 0; margin-right: 14rpx; display: flex; flex-direction: column; align-items: flex-end; }
|
||
.msg-sender { font-size: 22rpx; color: #999; margin-bottom: 8rpx; display: block; }
|
||
|
||
.bubble { border-radius: 18rpx; padding: 20rpx 24rpx; display: flex; flex-direction: column; }
|
||
.bubble-user { background-color: #fff; border-top-left-radius: 4rpx; box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.06); }
|
||
.bubble-me { background-color: #e8f0fe; border-top-right-radius: 4rpx; }
|
||
.bubble-text { font-size: 30rpx; color: #1a1a1a; line-height: 1.6; }
|
||
.bubble-time { font-size: 20rpx; color: #bbb; margin-top: 8rpx; }
|
||
.bubble-time-right { text-align: right; }
|
||
.bubble-image { flex-direction: row; align-items: center; gap: 12rpx; }
|
||
.bubble-image-icon { font-size: 44rpx; }
|
||
.bubble-image-hint { font-size: 26rpx; color: #666; }
|
||
|
||
.msg-card-wrap { display: flex; flex-direction: column; }
|
||
.msg-card-right { align-items: flex-end; }
|
||
.card-time { font-size: 20rpx; color: #bbb; margin-top: 8rpx; padding: 0 4rpx; }
|
||
.card-time-right { text-align: right; }
|
||
|
||
.product-card { background-color: #fff; border-radius: 16rpx; display: flex; flex-direction: row; align-items: center; padding: 20rpx; min-width: 500rpx; box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.06); border-width: 1rpx; border-style: solid; border-color: #f0f0f0; }
|
||
.pc-thumb { width: 96rpx; height: 96rpx; background-color: #f0f7ff; border-radius: 14rpx; display: flex; align-items: center; justify-content: center; margin-right: 20rpx; flex-shrink: 0; }
|
||
.pc-thumb-emoji { font-size: 56rpx; }
|
||
.pc-info { flex: 1; }
|
||
.pc-name { font-size: 28rpx; color: #1a1a1a; font-weight: bold; display: block; margin-bottom: 6rpx; }
|
||
.pc-price { font-size: 32rpx; color: #e84040; font-weight: bold; display: block; margin-bottom: 4rpx; }
|
||
.pc-tag { font-size: 22rpx; color: #888; display: block; }
|
||
|
||
.order-card { background-color: #fff; border-radius: 16rpx; padding: 20rpx 24rpx; min-width: 500rpx; box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.06); border-width: 1rpx; border-style: solid; border-color: #f0f0f0; }
|
||
.oc-row1 { display: flex; flex-direction: row; align-items: center; margin-bottom: 10rpx; }
|
||
.oc-label { font-size: 20rpx; color: #999; background-color: #f5f5f5; border-radius: 4rpx; padding: 2rpx 10rpx; margin-right: 12rpx; flex-shrink: 0; }
|
||
.oc-no { font-size: 22rpx; color: #555; flex: 1; }
|
||
.oc-status-tag { border-radius: 6rpx; padding: 4rpx 14rpx; flex-shrink: 0; }
|
||
.oc-pending { background-color: #fff3e0; }
|
||
.oc-pending .oc-status-text { color: #ff8c00; font-size: 22rpx; }
|
||
.oc-active { background-color: #e8f5e9; }
|
||
.oc-active .oc-status-text { color: #388e3c; font-size: 22rpx; }
|
||
.oc-done { background-color: #f5f5f5; }
|
||
.oc-done .oc-status-text { color: #888; font-size: 22rpx; }
|
||
.oc-amount { font-size: 36rpx; color: #e84040; font-weight: bold; display: block; margin-bottom: 4rpx; }
|
||
.oc-items { font-size: 24rpx; color: #888; display: block; }
|
||
|
||
.chat-input-area { background-color: #fff; border-top-width: 1rpx; border-top-style: solid; border-top-color: #e8e8e8; flex-shrink: 0; }
|
||
.quick-reply-panel { background-color: #f8f9fc; border-bottom-width: 1rpx; border-bottom-style: solid; border-bottom-color: #eee; padding: 16rpx 20rpx; }
|
||
.qr-header { display: flex; flex-direction: row; align-items: center; justify-content: space-between; margin-bottom: 14rpx; }
|
||
.qr-title { font-size: 24rpx; color: #555; font-weight: bold; }
|
||
.qr-close { font-size: 28rpx; color: #aaa; padding: 8rpx; }
|
||
.qr-scroll { max-height: 240rpx; }
|
||
.qr-item { background-color: #fff; border-radius: 10rpx; padding: 14rpx 20rpx; margin-bottom: 10rpx; border-width: 1rpx; border-style: solid; border-color: #e8e8e8; }
|
||
.qr-item:active { background-color: #E3F7ED; }
|
||
.qr-text { font-size: 26rpx; color: #333; line-height: 1.5; }
|
||
|
||
.chat-toolbar { display: flex; flex-direction: row; padding: 16rpx 20rpx 8rpx; gap: 20rpx; border-bottom-width: 1rpx; border-bottom-style: solid; border-bottom-color: #f0f0f0; }
|
||
.toolbar-btn { display: flex; flex-direction: column; align-items: center; padding: 10rpx 24rpx; border-radius: 12rpx; background-color: #f5f7fa; }
|
||
.toolbar-btn:active { background-color: #e0ecff; }
|
||
.toolbar-btn-active { background-color: #e8f0fe; }
|
||
.toolbar-icon { font-size: 40rpx; }
|
||
.toolbar-label { font-size: 20rpx; color: #555; margin-top: 4rpx; }
|
||
|
||
.input-row { display: flex; flex-direction: row; align-items: flex-end; padding: 12rpx 20rpx 16rpx; gap: 16rpx; }
|
||
.chat-textarea { flex: 1; background-color: #f5f7fa; border-radius: 16rpx; padding: 16rpx 20rpx; font-size: 30rpx; min-height: 80rpx; max-height: 200rpx; color: #333; border-width: 1rpx; border-style: solid; border-color: #e8e8e8; }
|
||
.send-btn { background-color: #ccc; border-radius: 16rpx; padding: 18rpx 28rpx; flex-shrink: 0; }
|
||
.send-btn-active { background-color: #09C39D; }
|
||
.send-btn-text { font-size: 30rpx; color: #fff; font-weight: bold; }
|
||
|
||
/* ====== 模块三:用户资料 ====== */
|
||
.module-info { flex: 1; overflow: hidden; }
|
||
.info-scroll { height: 100%; }
|
||
|
||
.info-user-card { background-color: #09C39D; padding: 40rpx 30rpx 30rpx; display: flex; flex-direction: row; align-items: center; }
|
||
.info-big-avatar { font-size: 100rpx; margin-right: 24rpx; }
|
||
.info-user-main { flex: 1; }
|
||
.info-user-name { font-size: 36rpx; font-weight: bold; color: #fff; display: block; margin-bottom: 16rpx; }
|
||
.info-tags-row { display: flex; flex-direction: row; flex-wrap: wrap; gap: 10rpx; }
|
||
.info-tag { background-color: rgba(255,255,255,0.2); border-radius: 20rpx; padding: 6rpx 18rpx; }
|
||
.info-tag-text { font-size: 22rpx; color: #fff; }
|
||
|
||
.info-section { background-color: #fff; margin: 16rpx; border-radius: 16rpx; overflow: hidden; }
|
||
.info-section-header { display: flex; flex-direction: row; align-items: center; padding: 20rpx 24rpx; border-bottom-width: 1rpx; border-bottom-style: solid; border-bottom-color: #f5f5f5; }
|
||
.info-section-icon { font-size: 32rpx; margin-right: 10rpx; }
|
||
.info-section-title { font-size: 28rpx; font-weight: bold; color: #333; }
|
||
.info-kv { display: flex; flex-direction: row; align-items: center; padding: 18rpx 24rpx; border-bottom-width: 1rpx; border-bottom-style: solid; border-bottom-color: #f5f5f5; }
|
||
.info-key { font-size: 28rpx; color: #888; width: 180rpx; flex-shrink: 0; }
|
||
.info-val { font-size: 28rpx; color: #333; flex: 1; }
|
||
.info-val-red { color: #e84040; font-weight: bold; }
|
||
|
||
.consult-product { display: flex; flex-direction: row; align-items: center; padding: 20rpx 24rpx; }
|
||
.cp-big-emoji { font-size: 60rpx; margin-right: 20rpx; }
|
||
.cp-detail { flex: 1; }
|
||
.cp-pname { font-size: 30rpx; color: #1a1a1a; font-weight: bold; display: block; margin-bottom: 8rpx; }
|
||
.cp-pprice { font-size: 32rpx; color: #e84040; font-weight: bold; display: block; }
|
||
|
||
.recent-row { display: flex; flex-direction: row; align-items: center; padding: 16rpx 24rpx; border-bottom-width: 1rpx; border-bottom-style: solid; border-bottom-color: #f5f5f5; }
|
||
.recent-emoji { font-size: 40rpx; margin-right: 16rpx; }
|
||
.recent-detail { flex: 1; }
|
||
.recent-name { font-size: 28rpx; color: #333; display: block; margin-bottom: 4rpx; }
|
||
.recent-price { font-size: 26rpx; color: #e84040; display: block; }
|
||
|
||
.order-mini-card { padding: 20rpx 24rpx; border-bottom-width: 1rpx; border-bottom-style: solid; border-bottom-color: #f5f5f5; }
|
||
.omc-row1 { display: flex; flex-direction: row; align-items: center; margin-bottom: 8rpx; }
|
||
.omc-no { font-size: 22rpx; color: #888; flex: 1; }
|
||
.omc-status-tag { border-radius: 6rpx; padding: 4rpx 14rpx; }
|
||
.omc-pending { background-color: #fff3e0; }
|
||
.omc-pending .omc-status-text { color: #ff8c00; font-size: 22rpx; }
|
||
.omc-active { background-color: #e8f5e9; }
|
||
.omc-active .omc-status-text { color: #388e3c; font-size: 22rpx; }
|
||
.omc-done { background-color: #f5f5f5; }
|
||
.omc-done .omc-status-text { color: #888; font-size: 22rpx; }
|
||
.omc-row2 { display: flex; flex-direction: row; align-items: center; justify-content: space-between; }
|
||
.omc-items { font-size: 26rpx; color: #555; flex: 1; }
|
||
.omc-amount { font-size: 28rpx; color: #e84040; font-weight: bold; }
|
||
|
||
.info-actions { display: flex; flex-direction: row; gap: 20rpx; padding: 24rpx; }
|
||
.info-action-btn { flex: 1; background-color: #f5f5f5; border-radius: 20rpx; padding: 22rpx 0; display: flex; align-items: center; justify-content: center; }
|
||
.info-action-btn:active { opacity: 0.8; }
|
||
.info-action-btn.primary { background-color: #09C39D; }
|
||
.info-action-btn.primary .info-action-text { color: #fff; }
|
||
.info-action-text { font-size: 30rpx; color: #555; font-weight: bold; }
|
||
|
||
.info-safe-bottom { height: 140rpx; }
|
||
|
||
/* 聊天模块底部 TabBar 占位 */
|
||
.chat-tabbar-spacer {
|
||
height: 120rpx;
|
||
flex-shrink: 0;
|
||
background-color: #fff;
|
||
}
|
||
</style>
|