1093 lines
28 KiB
Vue
1093 lines
28 KiB
Vue
<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>
|