OfficeSystem/pages/customer/follow/detail/index.vue
2025-11-10 14:31:42 +08:00

605 lines
15 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="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>