跟进详细初版
This commit is contained in:
parent
b57b90eaf5
commit
f8d41c7f3b
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -89,6 +89,13 @@
|
|||
"navigationBarTitleText": "编辑客户",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/customer/follow/detail/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "跟进详情",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
|
|
|
|||
566
pages/customer/follow/detail/index.vue
Normal file
566
pages/customer/follow/detail/index.vue
Normal 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>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user