605 lines
15 KiB
Vue
605 lines
15 KiB
Vue
<template>
|
||
<view class="followup-detail-page">
|
||
<!-- 自定义导航栏 -->
|
||
<view class="custom-navbar">
|
||
<view class="navbar-content">
|
||
<text class="nav-btn" @click="handleBack">‹</text>
|
||
<text class="nav-title">跟进详情</text>
|
||
<text class="nav-btn" style="opacity: 0;">占位</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 内容区域 -->
|
||
<scroll-view class="content-scroll" scroll-y v-if="!loading">
|
||
<view style="padding: 16px">
|
||
<!-- 跟进人信息卡片 -->
|
||
<view class="info-card">
|
||
<view class="card-header">
|
||
<image
|
||
class="user-avatar"
|
||
:src="followupDetail.userAvatar || '/static/default-avatar.png'"
|
||
mode="aspectFill"
|
||
/>
|
||
<view class="user-info">
|
||
<text class="user-name">{{ followupDetail.userName || '--' }}</text>
|
||
<text class="user-role">销售经理</text>
|
||
</view>
|
||
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 客户信息 -->
|
||
<view class="info-card" v-if="followupDetail.customerName">
|
||
<view class="card-title">客户信息</view>
|
||
<view class="info-row">
|
||
<text class="info-label">客户名称</text>
|
||
<text class="info-value">{{ followupDetail.customerName }}</text>
|
||
</view>
|
||
<view class="info-row" v-if="followupDetail.customerId">
|
||
<text class="info-label">客户ID</text>
|
||
<text class="info-value">{{ followupDetail.customerId }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 跟进内容 -->
|
||
<view class="info-card">
|
||
<view class="card-title">跟进内容</view>
|
||
<view class="content-text">{{ followupDetail.content || '暂无内容' }}</view>
|
||
<!-- 图片展示(一行最多三个) -->
|
||
<view class="followup-images-wrapper" v-if="followupImages && followupImages.length > 0">
|
||
<view
|
||
class="followup-image-item"
|
||
v-for="(imageUrl, imgIndex) in followupImages"
|
||
:key="imgIndex"
|
||
@click="previewFollowupImages(followupImages, imgIndex)"
|
||
>
|
||
<image
|
||
:src="imageUrl"
|
||
mode="aspectFill"
|
||
class="followup-image"
|
||
/>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 下次跟进 -->
|
||
<view class="info-card" v-if="followupDetail.nextFollowTime">
|
||
<view class="card-title">时间信息</view>
|
||
<view class="info-row">
|
||
<text class="info-label">跟进时间</text>
|
||
<text class="info-value">{{ formatDateTime(followupDetail.followTime) }}</text>
|
||
</view>
|
||
<view class="info-row">
|
||
<text class="info-label">下次跟进</text>
|
||
<text class="info-value">{{ formatDateTime(followupDetail.nextFollowTime) }}</text>
|
||
</view>
|
||
<view class="info-row" v-if="followupDetail.createTime">
|
||
<text class="info-label">创建时间</text>
|
||
<text class="info-value">{{ followupDetail.createTime }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 客户状态信息 -->
|
||
<view class="info-card" v-if="followupDetail.status || followupDetail.customerStatus">
|
||
<view class="card-title">状态信息</view>
|
||
<view class="info-row" v-if="followupDetail.status">
|
||
<text class="info-label">跟进状态</text>
|
||
<view class="status-badge" :class="getStatusClass(followupDetail.status)">
|
||
<text>{{ getStatusText(followupDetail.status) }}</text>
|
||
</view>
|
||
</view>
|
||
<view class="info-row" v-if="followupDetail.customerStatus">
|
||
<text class="info-label">客户状态</text>
|
||
<view class="status-badge" :class="getCustomerStatusClass(followupDetail.customerStatus)">
|
||
<text>{{ getCustomerStatusText(followupDetail.customerStatus) }}</text>
|
||
</view>
|
||
</view>
|
||
<view class="info-row" v-if="followupDetail.intentLevel">
|
||
<text class="info-label">意向强度</text>
|
||
<text class="info-value">{{ getIntentStrengthText(followupDetail.intentLevel) }}</text>
|
||
</view>
|
||
<view class="info-row" v-if="followupDetail.customerIntentLevel">
|
||
<text class="info-label">客户意向强度</text>
|
||
<text class="info-value">{{ getIntentStrengthText(followupDetail.customerIntentLevel) }}</text>
|
||
</view>
|
||
<view class="info-row" v-if="followupDetail.intents">
|
||
<text class="info-label">客户意向</text>
|
||
<text class="info-value">{{ formatIntents(followupDetail.intents) }}</text>
|
||
</view>
|
||
<view class="info-row" v-if="followupDetail.followType || followupDetail.followMethod || followupDetail.type">
|
||
<text class="info-label">跟进方式</text>
|
||
<text class="info-value">{{ getFollowTypeText(followupDetail.followType || followupDetail.followMethod || followupDetail.type) }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 备注信息 -->
|
||
<view class="info-card" v-if="followupDetail.remark">
|
||
<view class="card-title">备注</view>
|
||
<view class="content-text">{{ followupDetail.remark }}</view>
|
||
</view>
|
||
|
||
<!-- 客户分析 -->
|
||
<view class="info-card" v-if="hasAnalysis">
|
||
<view class="card-title">客户分析</view>
|
||
<view class="info-row" v-if="followupDetail.concern">
|
||
<text class="info-label">顾虑点</text>
|
||
<text class="info-value">{{ followupDetail.concern }}</text>
|
||
</view>
|
||
<view class="info-row" v-if="followupDetail.pain">
|
||
<text class="info-label">痛点</text>
|
||
<text class="info-value">{{ followupDetail.pain }}</text>
|
||
</view>
|
||
<view class="info-row" v-if="followupDetail.attention">
|
||
<text class="info-label">关注点</text>
|
||
<text class="info-value">{{ followupDetail.attention }}</text>
|
||
</view>
|
||
<view class="info-row" v-if="followupDetail.demand">
|
||
<text class="info-label">需求点</text>
|
||
<text class="info-value">{{ followupDetail.demand }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
|
||
</view>
|
||
</scroll-view>
|
||
|
||
<!-- 加载状态 -->
|
||
<view class="loading-container" v-if="loading">
|
||
<text class="loading-text">加载中...</text>
|
||
</view>
|
||
|
||
<!-- 错误状态 -->
|
||
<view class="error-container" v-if="error">
|
||
<text class="error-text">{{ error }}</text>
|
||
<view class="retry-btn" @click="loadFollowupDetail">
|
||
<text>重试</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, computed, onMounted } from 'vue';
|
||
import { onLoad } from '@dcloudio/uni-app';
|
||
import { getFollowupDetail, getCustomerFollowTypeDict } from '@/common/api/customer';
|
||
|
||
// 页面参数
|
||
const followId = ref('');
|
||
const followupDetail = ref({});
|
||
const loading = ref(false);
|
||
const error = ref('');
|
||
|
||
// 跟进方式字典数据
|
||
const followTypeOptions = ref([]);
|
||
|
||
// 计算是否有客户分析信息
|
||
const hasAnalysis = computed(() => {
|
||
return followupDetail.value.concern ||
|
||
followupDetail.value.pain ||
|
||
followupDetail.value.attention ||
|
||
followupDetail.value.demand;
|
||
});
|
||
|
||
// 计算图片列表(支持多种字段名和格式)
|
||
const followupImages = computed(() => {
|
||
const detail = followupDetail.value;
|
||
|
||
// 处理 picture 字段(字符串格式,逗号分隔)
|
||
if (detail.picture && typeof detail.picture === 'string' && detail.picture.trim()) {
|
||
const images = detail.picture.split(',').map(url => url.trim()).filter(url => url);
|
||
if (images.length > 0) {
|
||
return images;
|
||
}
|
||
}
|
||
|
||
// 支持数组格式的图片字段
|
||
if (detail.pictures && Array.isArray(detail.pictures) && detail.pictures.length > 0) {
|
||
return detail.pictures;
|
||
}
|
||
if (detail.images && Array.isArray(detail.images) && detail.images.length > 0) {
|
||
return detail.images;
|
||
}
|
||
if (detail.imageAttachments && Array.isArray(detail.imageAttachments) && detail.imageAttachments.length > 0) {
|
||
return detail.imageAttachments;
|
||
}
|
||
|
||
return [];
|
||
});
|
||
|
||
// 获取页面参数
|
||
onLoad((options) => {
|
||
if (options && options.followId) {
|
||
followupDetail.value.followId = options.followId;
|
||
followId.value = options.followId;
|
||
loadFollowupDetail();
|
||
} else if (options && options.id) {
|
||
// 兼容 id 参数
|
||
followupDetail.value.followId = options.id;
|
||
followId.value = options.id;
|
||
loadFollowupDetail();
|
||
} else {
|
||
error.value = '缺少跟进ID参数';
|
||
}
|
||
});
|
||
|
||
// 加载跟进方式字典数据
|
||
const loadFollowTypeDict = async () => {
|
||
try {
|
||
const res = await getCustomerFollowTypeDict();
|
||
if (res && Array.isArray(res)) {
|
||
followTypeOptions.value = res;
|
||
}
|
||
} catch (err) {
|
||
console.error('加载跟进方式字典失败:', err);
|
||
}
|
||
};
|
||
|
||
// 加载跟进详情
|
||
const loadFollowupDetail = async () => {
|
||
if (!followId.value) return;
|
||
|
||
loading.value = true;
|
||
error.value = '';
|
||
|
||
try {
|
||
// 并行加载跟进详情和字典数据
|
||
const [res] = await Promise.all([
|
||
getFollowupDetail(followId.value),
|
||
loadFollowTypeDict()
|
||
]);
|
||
|
||
if (res) {
|
||
followupDetail.value = res;
|
||
} else {
|
||
error.value = '获取跟进详情失败';
|
||
}
|
||
} catch (err) {
|
||
console.error('加载跟进详情失败:', err);
|
||
error.value = err?.message || '加载跟进详情失败,请重试';
|
||
uni.$uv.toast(error.value);
|
||
} finally {
|
||
loading.value = false;
|
||
}
|
||
};
|
||
|
||
// 格式化日期时间
|
||
const formatDateTime = (dateTime) => {
|
||
if (!dateTime) return '--';
|
||
try {
|
||
const date = new Date(dateTime);
|
||
const year = date.getFullYear();
|
||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||
const day = String(date.getDate()).padStart(2, '0');
|
||
const hours = String(date.getHours()).padStart(2, '0');
|
||
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||
return `${year}-${month}-${day} ${hours}:${minutes}`;
|
||
} catch (e) {
|
||
return dateTime;
|
||
}
|
||
};
|
||
|
||
// 格式化客户意向
|
||
const formatIntents = (intents) => {
|
||
if (!intents) return '--';
|
||
if (Array.isArray(intents)) {
|
||
return intents.length > 0 ? intents.join('、') : '--';
|
||
}
|
||
if (typeof intents === 'string') {
|
||
return intents || '--';
|
||
}
|
||
return '--';
|
||
};
|
||
|
||
// 获取状态样式类
|
||
const getStatusClass = (status) => {
|
||
return {
|
||
'status-potential': status === '1', // 潜在
|
||
'status-intent': status === '2', // 意向
|
||
'status-deal': status === '3', // 成交
|
||
'status-invalid': status === '4' // 失效
|
||
};
|
||
};
|
||
|
||
// 获取状态文本
|
||
const getStatusText = (status) => {
|
||
const statusMap = {
|
||
'1': '潜在',
|
||
'2': '意向',
|
||
'3': '成交',
|
||
'4': '失效'
|
||
};
|
||
return statusMap[status] || '未知';
|
||
};
|
||
|
||
// 获取客户状态样式类
|
||
const getCustomerStatusClass = (status) => {
|
||
return {
|
||
'status-potential': status === '1', // 潜在
|
||
'status-intent': status === '2', // 意向
|
||
'status-deal': status === '3', // 成交
|
||
'status-invalid': status === '4' // 失效
|
||
};
|
||
};
|
||
|
||
// 获取客户状态文本
|
||
const getCustomerStatusText = (status) => {
|
||
const statusMap = {
|
||
'1': '潜在',
|
||
'2': '意向',
|
||
'3': '成交',
|
||
'4': '失效'
|
||
};
|
||
return statusMap[status] || '未知';
|
||
};
|
||
|
||
// 获取意向强度文本
|
||
const getIntentStrengthText = (intentLevel) => {
|
||
const levelMap = {
|
||
'1': '高',
|
||
'2': '中',
|
||
'3': '低'
|
||
};
|
||
return levelMap[intentLevel] || '--';
|
||
};
|
||
|
||
// 获取跟进方式文本
|
||
const getFollowTypeText = (followTypeValue) => {
|
||
if (!followTypeValue) return '--';
|
||
|
||
// 从字典数据中查找对应的标签
|
||
const option = followTypeOptions.value.find(item => item.dictValue === String(followTypeValue));
|
||
return option ? option.dictLabel : '--';
|
||
};
|
||
|
||
// 预览图片
|
||
const previewFollowupImages = (images, currentIndex) => {
|
||
if (!images || images.length === 0) return;
|
||
uni.previewImage({
|
||
urls: images,
|
||
current: currentIndex
|
||
});
|
||
};
|
||
|
||
// 返回
|
||
const handleBack = () => {
|
||
uni.navigateBack();
|
||
};
|
||
|
||
// 组件挂载
|
||
onMounted(() => {
|
||
// 数据已在 onLoad 中加载
|
||
});
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.followup-detail-page {
|
||
display: flex;
|
||
flex-direction: column;
|
||
height: 100vh;
|
||
background-color: #f5f5f5;
|
||
}
|
||
|
||
.custom-navbar {
|
||
background-color: #fff;
|
||
padding-top: var(--status-bar-height, 0);
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||
position: relative;
|
||
z-index: 100;
|
||
}
|
||
|
||
.navbar-content {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
height: 44px;
|
||
padding: 0 16px;
|
||
}
|
||
|
||
.nav-btn {
|
||
font-size: 24px;
|
||
color: #333;
|
||
font-weight: bold;
|
||
min-width: 44px;
|
||
text-align: center;
|
||
}
|
||
|
||
.nav-title {
|
||
font-size: 18px;
|
||
font-weight: 600;
|
||
color: #333;
|
||
flex: 1;
|
||
text-align: center;
|
||
}
|
||
|
||
.content-scroll {
|
||
flex: 1;
|
||
overflow-y: auto;
|
||
|
||
}
|
||
|
||
.info-card {
|
||
background-color: #fff;
|
||
border-radius: 12px;
|
||
padding: 16px;
|
||
margin-bottom: 12px;
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||
}
|
||
|
||
.card-header {
|
||
display: flex;
|
||
align-items: center;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.user-avatar {
|
||
width: 48px;
|
||
height: 48px;
|
||
border-radius: 24px;
|
||
margin-right: 12px;
|
||
background-color: #e0e0e0;
|
||
border: 2px solid #f5f5f5;
|
||
}
|
||
|
||
.user-info {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
}
|
||
|
||
.user-name {
|
||
font-size: 16px;
|
||
color: #333;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.user-role {
|
||
font-size: 12px;
|
||
color: #999;
|
||
}
|
||
|
||
.follow-time {
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
|
||
.time-text {
|
||
font-size: 12px;
|
||
color: #999;
|
||
}
|
||
|
||
.card-title {
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
color: #333;
|
||
margin-bottom: 12px;
|
||
padding-bottom: 8px;
|
||
border-bottom: 1px solid #f0f0f0;
|
||
}
|
||
|
||
.info-row {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
margin-bottom: 12px;
|
||
|
||
&:last-child {
|
||
margin-bottom: 0;
|
||
}
|
||
}
|
||
|
||
.info-label {
|
||
font-size: 14px;
|
||
color: #999;
|
||
min-width: 80px;
|
||
margin-right: 12px;
|
||
}
|
||
|
||
.info-value {
|
||
flex: 1;
|
||
font-size: 14px;
|
||
color: #333;
|
||
word-break: break-word;
|
||
}
|
||
|
||
.content-text {
|
||
font-size: 15px;
|
||
color: #555;
|
||
line-height: 1.7;
|
||
word-break: break-word;
|
||
white-space: pre-wrap;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
/* 图片展示(一行最多三个) */
|
||
.followup-images-wrapper {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
margin-top: 12px;
|
||
}
|
||
|
||
.followup-image-item {
|
||
/* 一行三个:每个图片宽度 = (100% - 2个gap) / 3 */
|
||
width: calc((100% - 16px) / 3);
|
||
aspect-ratio: 1;
|
||
border-radius: 4px;
|
||
overflow: hidden;
|
||
background-color: #e0e0e0;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.followup-image {
|
||
width: 100%;
|
||
height: 100%;
|
||
object-fit: cover;
|
||
}
|
||
|
||
.status-badge {
|
||
display: inline-block;
|
||
padding: 4px 12px;
|
||
border-radius: 4px;
|
||
font-size: 12px;
|
||
|
||
&.status-potential {
|
||
background-color: #fdf6ec;
|
||
color: #e6a23c;
|
||
}
|
||
|
||
&.status-intent {
|
||
background-color: #ecf5ff;
|
||
color: #409eff;
|
||
}
|
||
|
||
&.status-deal {
|
||
background-color: #f0f9ff;
|
||
color: #67c23a;
|
||
}
|
||
|
||
&.status-invalid {
|
||
background-color: #fef0f0;
|
||
color: #f56c6c;
|
||
}
|
||
}
|
||
|
||
.loading-container {
|
||
flex: 1;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.loading-text {
|
||
font-size: 14px;
|
||
color: #999;
|
||
}
|
||
|
||
.error-container {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 40px 20px;
|
||
}
|
||
|
||
.error-text {
|
||
font-size: 14px;
|
||
color: #f56c6c;
|
||
margin-bottom: 20px;
|
||
text-align: center;
|
||
}
|
||
|
||
.retry-btn {
|
||
padding: 8px 24px;
|
||
background-color: #1976d2;
|
||
color: #fff;
|
||
border-radius: 4px;
|
||
font-size: 14px;
|
||
|
||
&:active {
|
||
opacity: 0.8;
|
||
}
|
||
}
|
||
</style>
|
||
|