Files
trade-assistant/uni-app/src/pages/customers/customers.vue
T

1093 lines
28 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="customers-container">
<view class="health-overview" v-if="healthOverview">
<view class="health-stat active" @click="filter = 'all'; loadCustomers()">
<text class="health-stat-value">{{ healthOverview.active }}</text>
<text class="health-stat-label">活跃客户</text>
</view>
<view class="health-stat watch" @click="filter = 'all'; loadCustomers()">
<text class="health-stat-value">{{ healthOverview.watch }}</text>
<text class="health-stat-label">需关注</text>
</view>
<view class="health-stat critical" @click="filter = 'silent'; loadSilent()">
<text class="health-stat-value">{{ healthOverview.critical }}</text>
<text class="health-stat-label">高危流失</text>
</view>
<view class="health-stat total">
<text class="health-stat-value">{{ healthOverview.total }}</text>
<text class="health-stat-label">共计</text>
</view>
</view>
<view class="filter-tabs">
<view
class="tab-item"
:class="{ active: filter === 'all' }"
@click="filter = 'all'; loadCustomers()"
>
全部
</view>
<view
class="tab-item"
:class="{ active: filter === 'lead' }"
@click="filter = 'lead'; loadCustomers()"
>
潜在
</view>
<view
class="tab-item"
:class="{ active: filter === 'negotiating' }"
@click="filter = 'negotiating'; loadCustomers()"
>
谈判中
</view>
<view
class="tab-item"
:class="{ active: filter === 'customer' }"
@click="filter = 'customer'; loadCustomers()"
>
已成交
</view>
<view
class="tab-item warning"
:class="{ active: filter === 'silent' }"
@click="filter = 'silent'; loadSilent()"
>
沉默
</view>
</view>
<view class="customer-list" v-if="customers.length > 0">
<view class="customer-item" v-for="item in customers" :key="item.id">
<view class="customer-info">
<view class="customer-header">
<text class="customer-name">{{ item.name }}</text>
<text class="customer-status" :class="item.status">{{ getStatusText(item.status) }}</text>
<text
class="risk-badge"
:class="item.risk_level"
v-if="item.risk_level"
>{{ getRiskText(item.risk_level) }}</text>
<text
class="health-badge"
:class="getHealthGrade(item.id)"
v-if="getHealthGrade(item.id)"
>{{ getHealthLabel(getHealthGrade(item.id)) }}</text>
</view>
<view class="customer-detail">
<text class="detail-item" v-if="item.company">{{ item.company }}</text>
<text class="detail-item" v-if="item.country">{{ item.country }}</text>
<text class="detail-item" v-if="item.phone">{{ item.phone }}</text>
</view>
<view class="customer-contact" v-if="item.last_contact_at">
<text class="contact-time">最后联系: {{ formatTime(item.last_contact_at) }}</text>
<text class="silence-days" v-if="item.silence_days > 0">沉默 {{ item.silence_days }} </text>
</view>
<view class="risk-reasons" v-if="item.reasons && item.reasons.length > 0">
<text class="risk-reason" v-for="(r, i) in item.reasons" :key="i">{{ r }}</text>
</view>
</view>
<view class="customer-actions">
<view class="action-icon" @click="showCustomerDetail(item)"></view>
<view class="action-icon" @click="showConversation(item)"></view>
<view class="action-icon" @click="editCustomer(item)"></view>
<view class="action-icon delete" @click="deleteCustomer(item.id)"></view>
</view>
</view>
</view>
<view class="empty" v-else>
<text>暂无客户数据</text>
</view>
<view class="bottom-actions">
<view class="action-btn export-btn" @click="exportCsv">
<text class="btn-icon">CSV</text>
<text class="btn-text">导出</text>
</view>
<view class="action-btn import-btn" @click="importCustomers">
<text class="btn-icon"></text>
<text class="btn-text">导入</text>
</view>
<view class="action-btn add-btn" @click="showAddModal = true">
<text class="btn-icon">+</text>
<text class="btn-text">新增</text>
</view>
</view>
<view class="modal" v-if="showAddModal || showEditModal" @click="closeModal">
<view class="modal-content" @click.stop>
<view class="modal-header">
<text class="modal-title">{{ showEditModal ? '编辑客户' : '新增客户' }}</text>
<text class="modal-close" @click="closeModal">×</text>
</view>
<view class="modal-body">
<view class="form-item">
<text class="form-label">姓名 *</text>
<input class="form-input" v-model="formData.name" placeholder="客户姓名" />
</view>
<view class="form-item">
<text class="form-label">公司</text>
<input class="form-input" v-model="formData.company" placeholder="公司名称" />
</view>
<view class="form-item">
<text class="form-label">国家</text>
<input class="form-input" v-model="formData.country" placeholder="国家" />
</view>
<view class="form-item">
<text class="form-label">电话</text>
<input class="form-input" v-model="formData.phone" placeholder="电话" />
</view>
<view class="form-item">
<text class="form-label">WhatsApp</text>
<input class="form-input" v-model="formData.whatsapp_id" placeholder="WhatsApp ID" />
</view>
<view class="form-item">
<text class="form-label">邮箱</text>
<input class="form-input" v-model="formData.email" placeholder="邮箱" />
</view>
<view class="form-item">
<text class="form-label">状态</text>
<picker :range="statusOptions" @change="onStatusChange">
<view class="picker-value">{{ getStatusText(formData.status) || '选择状态' }}</view>
</picker>
</view>
</view>
<view class="modal-footer">
<button class="cancel-btn" @click="closeModal">取消</button>
<button class="submit-btn" @click="submitCustomer">保存</button>
</view>
</view>
</view>
<view class="detail-modal" v-if="showDetailModal" @click="showDetailModal = false">
<view class="detail-content" @click.stop>
<view class="detail-header">
<text class="detail-name">{{ currentCustomer.name }}</text>
<text class="detail-status" :class="currentCustomer.status">{{ getStatusText(currentCustomer.status) }}</text>
<text
class="health-badge large"
:class="currentHealth?.grade"
v-if="currentHealth"
>{{ getHealthLabel(currentHealth.grade) }}</text>
<text class="health-score" v-if="currentHealth">{{ currentHealth.total_score }}</text>
</view>
<view class="health-detail-section" v-if="currentHealth">
<view class="health-detail-title">健康度评分</view>
<view class="dimension-row">
<text class="dimension-label">响应趋势</text>
<view class="dimension-bar">
<view class="dimension-fill" :style="{ width: (currentHealth.dimensions?.response_trend?.score || 0) + '%' }"></view>
</view>
<text class="dimension-score">{{ currentHealth.dimensions?.response_trend?.score || 0 }}</text>
<text class="dimension-trend" :class="currentHealth.dimensions?.response_trend?.trend">{{ trendLabel(currentHealth.dimensions?.response_trend?.trend) }}</text>
</view>
<view class="dimension-row">
<text class="dimension-label">情感轨迹</text>
<view class="dimension-bar">
<view class="dimension-fill" :style="{ width: (currentHealth.dimensions?.sentiment?.score || 0) + '%' }"></view>
</view>
<text class="dimension-score">{{ currentHealth.dimensions?.sentiment?.score || 0 }}</text>
<text class="dimension-label small">{{ currentHealth.dimensions?.sentiment?.label }}</text>
</view>
<view class="dimension-row">
<text class="dimension-label">询盘深度</text>
<view class="dimension-bar">
<view class="dimension-fill" :style="{ width: (currentHealth.dimensions?.inquiry_depth?.score || 0) + '%' }"></view>
</view>
<text class="dimension-score">{{ currentHealth.dimensions?.inquiry_depth?.score || 0 }}</text>
<text class="dimension-label small" v-if="currentHealth.dimensions?.inquiry_depth?.signal_count">信号 {{ currentHealth.dimensions.inquiry_depth.signal_count }}</text>
</view>
<view class="dimension-row">
<text class="dimension-label">沉默天数</text>
<view class="dimension-bar">
<view class="dimension-fill" :style="{ width: (currentHealth.dimensions?.silence?.score || 0) + '%' }"></view>
</view>
<text class="dimension-score">{{ currentHealth.dimensions?.silence?.score || 0 }}</text>
<text class="dimension-label small">{{ currentHealth.dimensions?.silence?.days || 0 }}</text>
</view>
<view class="dimension-row">
<text class="dimension-label">商业价值</text>
<view class="dimension-bar">
<view class="dimension-fill" :style="{ width: (currentHealth.dimensions?.business_value?.score || 0) + '%' }"></view>
</view>
<text class="dimension-score">{{ currentHealth.dimensions?.business_value?.score || 0 }}</text>
<text class="dimension-label small" v-if="currentHealth.dimensions?.business_value?.total_value">${{ formatValue(currentHealth.dimensions.business_value.total_value) }}</text>
</view>
<view class="suggestion-box" v-if="currentHealth.suggestion">
<text class="suggestion-icon">💡</text>
<text class="suggestion-text">{{ currentHealth.suggestion }}</text>
</view>
</view>
<scroll-view class="detail-body" scroll-y>
<view class="detail-row" v-if="currentCustomer.company">
<text class="detail-label">公司:</text>
<text class="detail-value">{{ currentCustomer.company }}</text>
</view>
<view class="detail-row" v-if="currentCustomer.country">
<text class="detail-label">国家:</text>
<text class="detail-value">{{ currentCustomer.country }}</text>
</view>
<view class="detail-row" v-if="currentCustomer.phone">
<text class="detail-label">电话:</text>
<text class="detail-value">{{ currentCustomer.phone }}</text>
</view>
<view class="detail-row" v-if="currentCustomer.whatsapp_id">
<text class="detail-label">WhatsApp:</text>
<text class="detail-value">{{ currentCustomer.whatsapp_id }}</text>
</view>
<view class="detail-row" v-if="currentCustomer.email">
<text class="detail-label">邮箱:</text>
<text class="detail-value">{{ currentCustomer.email }}</text>
</view>
</scroll-view>
<view class="detail-footer">
<button class="close-btn" @click="showDetailModal = false">关闭</button>
</view>
</view>
</view>
<view class="conversation-modal" v-if="showConversationModal" @click="showConversationModal = false">
<view class="conversation-content" @click.stop>
<view class="conversation-header">
<text class="conversation-title">沟通记录 - {{ conversationCustomer?.name }}</text>
<text class="conversation-close" @click="showConversationModal = false">×</text>
</view>
<scroll-view class="conversation-body" scroll-y>
<view class="msg-item" v-for="(msg, i) in conversation" :key="i">
<text class="msg-direction" :class="msg.direction">{{ msg.direction === 'inbound' ? '客户' : '我' }}</text>
<text class="msg-content">{{ msg.content }}</text>
<text class="msg-time">{{ formatTime(msg.created_at) }}</text>
</view>
<view class="empty-msg" v-if="conversation.length === 0">
<text>暂无沟通记录</text>
</view>
</scroll-view>
</view>
</view>
</view>
</template>
<script setup>
import { ref } from 'vue'
import { onShow } from '@dcloudio/uni-app'
import { customerApi, healthApi, silentPatternApi } from '@/utils/api.js'
const filter = ref('all')
const customers = ref([])
const showAddModal = ref(false)
const showEditModal = ref(false)
const showDetailModal = ref(false)
const showConversationModal = ref(false)
const currentCustomer = ref(null)
const currentHealth = ref(null)
const conversationCustomer = ref(null)
const conversation = ref([])
const healthOverview = ref(null)
const healthScores = ref({})
const formData = ref({
name: '',
company: '',
country: '',
phone: '',
whatsapp_id: '',
email: '',
status: 'lead',
})
const statusOptions = ['lead', 'negotiating', 'customer', 'lost']
onShow(() => {
const token = uni.getStorageSync('token')
const isGuest = uni.getStorageSync('isGuest')
if (!token || isGuest) return
loadCustomers()
loadHealthOverview()
})
const loadCustomers = async () => {
try {
const res = await customerApi.list(1, 20, filter.value === 'all' ? undefined : filter.value)
customers.value = res.items || []
loadHealthScores()
} catch (err) {
uni.showToast({ title: err.message || '加载失败', icon: 'none' })
}
}
const loadHealthOverview = async () => {
try {
const res = await healthApi.overview()
healthOverview.value = res
} catch (err) {
console.error('加载健康概览失败', err)
}
}
const loadHealthScores = async () => {
try {
const res = await healthApi.allScores()
const map = {}
;(res.items || []).forEach(h => { map[h.customer_id] = h })
healthScores.value = map
} catch (err) {
console.error('加载健康评分失败', err)
}
}
const getHealthGrade = (customerId) => {
return healthScores.value[customerId]?.grade || null
}
const getHealthLabel = (grade) => {
const map = { active: '活跃', watch: '关注', critical: '高危' }
return map[grade] || grade
}
const trendLabel = (trend) => {
const map = { improving: '↑', declining: '↓', stable: '→' }
return map[trend] || ''
}
const formatValue = (val) => {
if (!val) return '0'
if (val >= 10000) return (val / 10000).toFixed(1) + '万'
return val.toLocaleString()
}
const loadSilent = async () => {
try {
const res = await customerApi.getSilent(7)
customers.value = res.customers || []
} catch (err) {
uni.showToast({ title: err.message || '加载失败', icon: 'none' })
}
}
const getRiskText = (level) => {
const map = { high: '高风险', medium: '中风险', low: '低风险' }
return map[level] || level
}
const getStatusText = (status) => {
const map = { lead: '潜在', negotiating: '谈判中', customer: '已成交', lost: '已丢失' }
return map[status] || status
}
const formatTime = (time) => {
if (!time) return ''
return time.split('T')[0]
}
const showCustomerDetail = async (item) => {
currentCustomer.value = item
showDetailModal.value = true
try {
const res = await healthApi.customerHealth(item.id)
currentHealth.value = res
} catch (err) {
currentHealth.value = null
console.error('加载健康详情失败', err)
}
}
const showConversation = async (item) => {
conversationCustomer.value = item
showConversationModal.value = true
try {
const res = await customerApi.getConversation(item.id)
conversation.value = res.items || res.messages || []
} catch (err) {
conversation.value = []
uni.showToast({ title: '加载沟通记录失败', icon: 'none' })
}
}
const editCustomer = (item) => {
formData.value = { ...item }
showEditModal.value = true
}
const closeModal = () => {
showAddModal.value = false
showEditModal.value = false
formData.value = {
name: '',
company: '',
country: '',
phone: '',
whatsapp_id: '',
email: '',
status: 'lead',
}
}
const onStatusChange = (e) => {
formData.value.status = statusOptions[e.detail.value]
}
const submitCustomer = async () => {
if (!formData.value.name) {
uni.showToast({ title: '请填写姓名', icon: 'none' })
return
}
try {
if (showEditModal.value) {
await customerApi.update(formData.value.id, formData.value)
uni.showToast({ title: '更新成功', icon: 'success' })
} else {
await customerApi.create(formData.value)
uni.showToast({ title: '创建成功', icon: 'success' })
}
closeModal()
loadCustomers()
} catch (err) {
uni.showToast({ title: err.message || '操作失败', icon: 'none' })
}
}
const exportCsv = () => {
const url = customerApi.exportCsv()
const token = uni.getStorageSync('token')
uni.downloadFile({
url,
header: { Authorization: `Bearer ${token}` },
success: (res) => {
if (res.statusCode === 200) {
uni.showToast({ title: '导出成功', icon: 'success' })
} else {
uni.showToast({ title: '导出失败', icon: 'none' })
}
},
fail: () => { uni.showToast({ title: '导出失败', icon: 'none' }) },
})
}
const importCustomers = () => {
uni.chooseImage({
count: 1,
success: async (res) => {
const file = res.tempFilePaths[0]
uni.showLoading({ title: '导入中...' })
try {
const result = await customerApi.importCustomers(file)
uni.hideLoading()
uni.showModal({
title: '导入完成',
content: `成功导入 ${result.imported || 0}\n失败 ${(result.errors || []).length}`,
success: () => loadCustomers(),
})
} catch (err) {
uni.hideLoading()
uni.showToast({ title: err.message || '导入失败', icon: 'none' })
}
},
})
}
const deleteCustomer = async (id) => {
uni.showModal({
title: '确认删除',
content: '确定要删除该客户吗?',
success: async (res) => {
if (res.confirm) {
try {
await customerApi.delete(id)
uni.showToast({ title: '删除成功', icon: 'success' })
loadCustomers()
} catch (err) {
uni.showToast({ title: err.message || '删除失败', icon: 'none' })
}
}
},
})
}
</script>
<style lang="scss" scoped>
.customers-container {
min-height: 100vh;
background: #f5f5f5;
padding: 20rpx;
}
.health-overview {
display: flex;
gap: 12rpx;
margin-bottom: 20rpx;
}
.health-stat {
flex: 1;
background: #fff;
border-radius: 12rpx;
padding: 16rpx 8rpx;
text-align: center;
}
.health-stat-value {
font-size: 36rpx;
font-weight: 700;
display: block;
}
.health-stat-label {
font-size: 20rpx;
color: #999;
margin-top: 4rpx;
}
.health-stat.active .health-stat-value { color: #52c41a; }
.health-stat.watch .health-stat-value { color: #fa8c16; }
.health-stat.critical .health-stat-value { color: #ff4d4f; }
.health-stat.total .health-stat-value { color: #1890ff; }
.health-badge {
font-size: 20rpx;
padding: 2rpx 10rpx;
border-radius: 4rpx;
margin-left: 8rpx;
}
.health-badge.active { background: #f6ffed; color: #52c41a; }
.health-badge.watch { background: #fff7e6; color: #fa8c16; }
.health-badge.critical { background: #fff1f0; color: #ff4d4f; }
.health-badge.large { padding: 4rpx 16rpx; font-size: 24rpx; }
.health-score {
font-size: 40rpx;
font-weight: 700;
color: #1890ff;
margin-left: auto;
}
.health-detail-section {
padding: 20rpx;
background: #f9f9f9;
border-radius: 12rpx;
margin: 0 30rpx 20rpx;
}
.health-detail-title {
font-size: 26rpx;
font-weight: 600;
color: #333;
margin-bottom: 16rpx;
}
.dimension-row {
display: flex;
align-items: center;
gap: 12rpx;
margin-bottom: 12rpx;
}
.dimension-label {
font-size: 22rpx;
color: #666;
min-width: 80rpx;
}
.dimension-label.small {
min-width: auto;
font-size: 20rpx;
}
.dimension-bar {
flex: 1;
height: 12rpx;
background: #e8e8e8;
border-radius: 6rpx;
overflow: hidden;
}
.dimension-fill {
height: 100%;
background: linear-gradient(90deg, #52c41a, #1890ff);
border-radius: 6rpx;
transition: width 0.3s;
}
.dimension-score {
font-size: 22rpx;
font-weight: 600;
color: #333;
min-width: 32rpx;
text-align: right;
}
.dimension-trend {
font-size: 20rpx;
min-width: 24rpx;
}
.dimension-trend.improving { color: #52c41a; }
.dimension-trend.declining { color: #ff4d4f; }
.dimension-trend.stable { color: #999; }
.suggestion-box {
display: flex;
align-items: flex-start;
gap: 8rpx;
margin-top: 16rpx;
padding: 12rpx;
background: #e6f7ff;
border-radius: 8rpx;
}
.suggestion-icon {
font-size: 24rpx;
}
.suggestion-text {
font-size: 24rpx;
color: #1890ff;
line-height: 1.4;
flex: 1;
}
.filter-tabs {
display: flex;
background: #fff;
border-radius: 12rpx;
margin-bottom: 20rpx;
overflow: hidden;
}
.tab-item {
flex: 1;
text-align: center;
padding: 24rpx;
font-size: 26rpx;
color: #666;
}
.tab-item.active {
color: #1890ff;
background: #e6f7ff;
}
.tab-item.warning {
color: #ff4d4f;
}
.customer-list {
display: flex;
flex-direction: column;
gap: 16rpx;
}
.customer-item {
background: #fff;
border-radius: 16rpx;
padding: 24rpx;
display: flex;
justify-content: space-between;
}
.customer-info {
flex: 1;
}
.customer-header {
display: flex;
align-items: center;
flex-wrap: wrap;
margin-bottom: 12rpx;
}
.customer-name {
font-size: 30rpx;
font-weight: 600;
margin-right: 12rpx;
}
.customer-status {
font-size: 22rpx;
padding: 4rpx 12rpx;
border-radius: 6rpx;
}
.customer-status.lead { background: #fff7e6; color: #fa8c16; }
.customer-status.negotiating { background: #e6f7ff; color: #1890ff; }
.customer-status.customer { background: #f6ffed; color: #52c41a; }
.customer-status.lost { background: #fff1f0; color: #ff4d4f; }
.risk-badge {
font-size: 20rpx;
padding: 2rpx 10rpx;
border-radius: 4rpx;
margin-left: 8rpx;
}
.risk-badge.high { background: #fff1f0; color: #ff4d4f; }
.risk-badge.medium { background: #fff7e6; color: #fa8c16; }
.risk-badge.low { background: #f6ffed; color: #52c41a; }
.customer-detail {
display: flex;
flex-wrap: wrap;
gap: 12rpx;
margin-bottom: 12rpx;
}
.detail-item {
font-size: 24rpx;
color: #666;
background: #f5f5f5;
padding: 4rpx 12rpx;
border-radius: 6rpx;
}
.customer-contact {
display: flex;
justify-content: space-between;
font-size: 22rpx;
color: #999;
}
.silence-days {
color: #ff4d4f;
}
.risk-reasons {
display: flex;
flex-wrap: wrap;
gap: 8rpx;
margin-top: 8rpx;
}
.risk-reason {
font-size: 20rpx;
color: #ff4d4f;
background: #fff1f0;
padding: 2rpx 10rpx;
border-radius: 4rpx;
}
.customer-actions {
display: flex;
flex-direction: column;
gap: 12rpx;
}
.action-icon {
width: 60rpx;
height: 60rpx;
background: #f5f5f5;
border-radius: 8rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 24rpx;
color: #666;
}
.action-icon.delete {
color: #ff4d4f;
}
.empty {
text-align: center;
color: #999;
padding: 100rpx;
}
.bottom-actions {
position: fixed;
right: 40rpx;
bottom: 100px;
display: flex;
flex-direction: column;
gap: 24rpx;
}
.action-btn {
width: 100rpx;
height: 100rpx;
border-radius: 50%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.15);
}
.add-btn {
background: #1890ff;
}
.import-btn {
background: #52c41a;
}
.export-btn {
background: #722ed1;
}
.btn-icon {
font-size: 32rpx;
color: #fff;
line-height: 1;
}
.btn-text {
font-size: 18rpx;
color: #fff;
margin-top: 2rpx;
}
.modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 999;
}
.modal-content {
width: 80%;
background: #fff;
border-radius: 16rpx;
overflow: hidden;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 30rpx;
border-bottom: 2rpx solid #f5f5f5;
}
.modal-title {
font-size: 32rpx;
font-weight: 600;
}
.modal-close {
font-size: 44rpx;
color: #999;
}
.modal-body {
padding: 30rpx;
max-height: 60vh;
overflow-y: auto;
}
.form-item {
margin-bottom: 24rpx;
}
.form-label {
font-size: 26rpx;
color: #666;
display: block;
margin-bottom: 8rpx;
}
.form-input {
width: 100%;
height: 72rpx;
background: #f5f5f5;
border-radius: 8rpx;
padding: 0 20rpx;
font-size: 28rpx;
}
.picker-value {
height: 72rpx;
background: #f5f5f5;
border-radius: 8rpx;
padding: 0 20rpx;
display: flex;
align-items: center;
font-size: 28rpx;
}
.modal-footer {
display: flex;
gap: 20rpx;
padding: 30rpx;
border-top: 2rpx solid #f5f5f5;
}
.cancel-btn, .submit-btn {
flex: 1;
height: 80rpx;
border-radius: 8rpx;
font-size: 28rpx;
display: flex;
align-items: center;
justify-content: center;
}
.cancel-btn {
background: #f5f5f5;
color: #666;
}
.submit-btn {
background: #1890ff;
color: #fff;
}
.detail-modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 999;
}
.detail-content {
width: 80%;
background: #fff;
border-radius: 16rpx;
padding: 30rpx;
}
.detail-header {
display: flex;
align-items: center;
margin-bottom: 30rpx;
}
.detail-name {
font-size: 34rpx;
font-weight: 600;
margin-right: 16rpx;
}
.detail-status {
font-size: 22rpx;
padding: 4rpx 12rpx;
border-radius: 6rpx;
}
.detail-body {
margin-bottom: 30rpx;
}
.detail-row {
display: flex;
padding: 16rpx 0;
border-bottom: 2rpx solid #f5f5f5;
}
.detail-label {
width: 160rpx;
color: #999;
font-size: 26rpx;
}
.detail-value {
flex: 1;
font-size: 26rpx;
}
.close-btn {
width: 100%;
height: 80rpx;
background: #1890ff;
color: #fff;
border-radius: 8rpx;
font-size: 28rpx;
}
.conversation-modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 999;
}
.conversation-content {
width: 90%;
height: 70%;
background: #fff;
border-radius: 16rpx;
display: flex;
flex-direction: column;
}
.conversation-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 30rpx;
border-bottom: 2rpx solid #f5f5f5;
}
.conversation-title {
font-size: 32rpx;
font-weight: 600;
}
.conversation-close {
font-size: 44rpx;
color: #999;
}
.conversation-body {
flex: 1;
padding: 30rpx;
}
.msg-item {
padding: 16rpx;
margin-bottom: 16rpx;
background: #f9f9f9;
border-radius: 12rpx;
}
.msg-direction {
font-size: 22rpx;
padding: 2rpx 10rpx;
border-radius: 4rpx;
margin-bottom: 8rpx;
display: inline-block;
}
.msg-direction.inbound { background: #e6f7ff; color: #1890ff; }
.msg-direction.outbound { background: #f6ffed; color: #52c41a; }
.msg-content {
font-size: 26rpx;
line-height: 1.5;
display: block;
margin-bottom: 8rpx;
}
.msg-time {
font-size: 20rpx;
color: #999;
}
.empty-msg {
text-align: center;
color: #999;
padding: 60rpx;
}
</style>