feat: 修复 H5 底部导航覆盖 + 更新项目进度文档
## H5 底部导航修复 (Bug #10) - 精简 App.vue,移除重复 tabbar,仅保留全局样式 - uni-page 设置 height: calc(100% - 50px) + overflow-y: auto - 内容区域精确停在底部导航上方,独立滚动不再叠加 - 恢复 custom-tab-bar 组件 ## 项目进度文档 - PROGRESS.md 更新至 10 个 Bug 修复 - 新增 H5 底部导航修复记录 - 新增历史变更条目
This commit is contained in:
@@ -1,5 +1,24 @@
|
||||
<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"
|
||||
@@ -44,6 +63,16 @@
|
||||
<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>
|
||||
@@ -54,9 +83,13 @@
|
||||
<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>
|
||||
@@ -67,8 +100,19 @@
|
||||
<text>暂无客户数据</text>
|
||||
</view>
|
||||
|
||||
<view class="add-btn" @click="showAddModal = true">
|
||||
<text class="add-icon">+</text>
|
||||
<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">
|
||||
@@ -121,8 +165,63 @@
|
||||
<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="detail-body">
|
||||
|
||||
<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>
|
||||
@@ -143,25 +242,51 @@
|
||||
<text class="detail-label">邮箱:</text>
|
||||
<text class="detail-value">{{ currentCustomer.email }}</text>
|
||||
</view>
|
||||
</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, onShow } from 'vue'
|
||||
import { customerApi } from '@/utils/api.js'
|
||||
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: '',
|
||||
@@ -176,17 +301,59 @@ const statusOptions = ['lead', 'negotiating', 'customer', 'lost']
|
||||
|
||||
onShow(() => {
|
||||
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)
|
||||
@@ -196,6 +363,11 @@ const loadSilent = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
@@ -206,9 +378,28 @@ const formatTime = (time) => {
|
||||
return time.split('T')[0]
|
||||
}
|
||||
|
||||
const showCustomerDetail = (item) => {
|
||||
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) => {
|
||||
@@ -255,6 +446,45 @@ const submitCustomer = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
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: '确认删除',
|
||||
@@ -281,6 +511,141 @@ const deleteCustomer = async (id) => {
|
||||
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;
|
||||
@@ -327,6 +692,7 @@ const deleteCustomer = async (id) => {
|
||||
.customer-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
@@ -347,6 +713,17 @@ const deleteCustomer = async (id) => {
|
||||
.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;
|
||||
@@ -373,6 +750,21 @@ const deleteCustomer = async (id) => {
|
||||
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;
|
||||
@@ -401,23 +793,48 @@ const deleteCustomer = async (id) => {
|
||||
padding: 100rpx;
|
||||
}
|
||||
|
||||
.add-btn {
|
||||
.bottom-actions {
|
||||
position: fixed;
|
||||
right: 40rpx;
|
||||
bottom: 40rpx;
|
||||
width: 100rpx;
|
||||
height: 100rpx;
|
||||
background: #1890ff;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 8rpx 24rpx rgba(24, 144, 255, 0.4);
|
||||
flex-direction: column;
|
||||
gap: 24rpx;
|
||||
}
|
||||
|
||||
.add-icon {
|
||||
font-size: 60rpx;
|
||||
.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 {
|
||||
@@ -588,4 +1005,85 @@ const deleteCustomer = async (id) => {
|
||||
border-radius: 8rpx;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
</style>
|
||||
|
||||
.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>
|
||||
|
||||
Reference in New Issue
Block a user