跟进详细初版

This commit is contained in:
WindowBird 2025-11-10 09:43:29 +08:00
parent b57b90eaf5
commit f8d41c7f3b
4 changed files with 591 additions and 0 deletions

View File

@ -129,6 +129,19 @@ export const getCustomerFollowupList = (customerId) => {
});
};
/**
* 获取跟进详情
* @param {string} followId 跟进ID
* @returns {Promise} 返回跟进详情
*/
export const getFollowupDetail = (followId) => {
return uni.$uv.http.get(`bst/customerFollow/${followId}`, {
custom: {
auth: true
}
});
};
/**
* 获取客户项目列表
* @param {string} customerId 客户ID

View File

@ -123,6 +123,11 @@ const formatTimeOnly = (dateTime) => {
//
const handleFollowupClick = (item) => {
if (item && item.followId) {
uni.navigateTo({
url: `/pages/customer/follow/detail/index?followId=${item.followId}`
});
}
emit('followup-click', item);
};
</script>

View File

@ -89,6 +89,13 @@
"navigationBarTitleText": "编辑客户",
"navigationStyle": "custom"
}
},
{
"path": "pages/customer/follow/detail/index",
"style": {
"navigationBarTitleText": "跟进详情",
"navigationStyle": "custom"
}
}
],

View File

@ -0,0 +1,566 @@
<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 class="follow-time">
<text class="time-text">{{ formatDateTime(followupDetail.followTime) }}</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.nextFollowTime) }}</text>
</view>
<view class="info-row" v-if="followupDetail.nextFollowContent">
<text class="info-label">跟进内容</text>
<text class="info-value">{{ followupDetail.nextFollowContent }}</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>
<!-- 备注信息 -->
<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 class="info-card">
<view class="card-title">时间信息</view>
<view class="info-row" v-if="followupDetail.createTime">
<text class="info-label">创建时间</text>
<text class="info-value">{{ formatDateTime(followupDetail.createTime) }}</text>
</view>
<view class="info-row" v-if="followupDetail.updateTime">
<text class="info-label">更新时间</text>
<text class="info-value">{{ formatDateTime(followupDetail.updateTime) }}</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 } from '@/common/api/customer';
//
const followId = ref('');
const followupDetail = ref({});
const loading = ref(false);
const error = 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 loadFollowupDetail = async () => {
if (!followId.value) return;
loading.value = true;
error.value = '';
try {
const res = await getFollowupDetail(followId.value);
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-following': status === '1',
'status-pending': status === '2'
};
};
//
const getStatusText = (status) => {
const statusMap = {
'1': '正在跟进',
'2': '待跟进',
'3': '其他',
'4': '无效客户'
};
return statusMap[status] || '未知';
};
//
const getCustomerStatusClass = (status) => {
return {
'status-following': status === '1',
'status-pending': status === '2'
};
};
//
const getCustomerStatusText = (status) => {
const statusMap = {
'1': '正在跟进',
'2': '待跟进',
'3': '其他',
'4': '无效客户'
};
return statusMap[status] || '未知';
};
//
const getIntentStrengthText = (intentLevel) => {
const levelMap = {
'1': '高',
'2': '中',
'3': '低'
};
return levelMap[intentLevel] || '--';
};
//
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-following {
background-color: #e3f2fd;
color: #1976d2;
}
&.status-pending {
background-color: #f5f5f5;
color: #666;
}
}
.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>