OfficeSystem/pages/customer/detail/index.vue
2025-11-07 11:40:13 +08:00

961 lines
22 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="customer-detail-page">
<!-- 自定义导航栏 -->
<view class="custom-navbar">
<view class="navbar-content">
<text class="nav-btn" @click="handleBack"></text>
<text class="nav-title">{{ customerDetail.name || '客户详情' }}</text>
<text class="nav-btn" style="opacity: 0;">占位</text>
</view>
</view>
<!-- 客户摘要卡片 -->
<view class="customer-summary-card">
<view class="summary-row">
<view class="summary-item">
<text class="summary-label">客户状态</text>
<view class="status-badge" :class="getStatusClass(customerDetail.status)">
<text>{{ getStatusText(customerDetail.status) }}</text>
</view>
</view>
<view class="summary-item">
<text class="summary-label">客户星级</text>
<view class="stars">
<text
class="star"
v-for="i in 5"
:key="i"
:class="{ 'filled': i <= getRatingFromIntentLevel(customerDetail.intentLevel) }"
>★</text>
</view>
</view>
</view>
<view class="summary-row">
<view class="summary-item">
<text class="summary-label">客户归属</text>
<text class="summary-value">{{ customerDetail.followName || '未分配' }}</text>
</view>
<view class="summary-item">
<text class="summary-label">客户类型</text>
<text class="summary-value">{{ customerDetail.customerType || '--' }}</text>
</view>
</view>
<view class="summary-row">
<view class="summary-item">
<text class="summary-label">联系人</text>
<text class="summary-value">{{ customerDetail.contactName || customerDetail.name || '--' }}</text>
</view>
<view class="summary-item">
<text class="summary-label">最近跟进</text>
<text class="summary-value">{{ formatDateTime(customerDetail.lastFollowTime) }}</text>
</view>
</view>
</view>
<!-- 标签页导航 -->
<view class="tab-navigation">
<view
class="tab-item"
:class="{ active: activeTab === 'followup' }"
@click="switchTab('followup')"
>
<text>跟进动态</text>
</view>
<view
class="tab-item"
:class="{ active: activeTab === 'projects' }"
@click="switchTab('projects')"
>
<text>项目列表</text>
</view>
<view
class="tab-item"
:class="{ active: activeTab === 'info' }"
@click="switchTab('info')"
>
<text>客户信息</text>
</view>
</view>
<!-- 内容区域 -->
<scroll-view class="content-scroll" scroll-y>
<!-- 跟进动态标签页 -->
<view class="tab-content" v-if="activeTab === 'followup'">
<view class="followup-timeline">
<view
class="followup-item"
v-for="(item, index) in followupList"
:key="index"
@click="handleFollowupClick(item)"
>
<view class="timeline-dot">
<text class="dot-date">{{ formatDate(item.createTime) }}</text>
</view>
<view class="followup-content">
<view class="followup-header">
<image
class="followup-avatar"
:src="item.userAvatar || '/static/default-avatar.png'"
mode="aspectFill"
/>
<view class="followup-user-info">
<text class="followup-user-name">{{ item.userName }}</text>
<text class="followup-user-role">{{ item.userRole || '销售经理' }}</text>
</view>
<text class="followup-arrow"></text>
</view>
<text class="followup-text">{{ item.content }}</text>
<text class="followup-time">{{ formatDateTime(item.createTime) }}</text>
</view>
</view>
<view class="empty-state" v-if="followupList.length === 0">
<text>暂无跟进记录</text>
</view>
</view>
</view>
<!-- 项目列表标签页 -->
<view class="tab-content" v-if="activeTab === 'projects'">
<view class="project-list">
<view
class="project-card"
v-for="(project, index) in projectList"
:key="index"
>
<view class="project-header">
<view class="project-status-tag" :class="getProjectStatusClass(project.status)">
<text>{{ getProjectStatusText(project.status) }}</text>
</view>
<text class="project-more" @click.stop="handleProjectMore(project)">⋮</text>
</view>
<text class="project-name">{{ project.name }}</text>
<view class="project-progress">
<view class="progress-bar">
<view
class="progress-fill"
:style="{ width: project.progress + '%' }"
></view>
</view>
<text class="progress-text">{{ project.progress }}%</text>
</view>
<view class="project-participants">
<text class="participants-text">{{ formatParticipants(project.participants) }}</text>
</view>
<view class="project-deadline" :class="{ 'overdue': project.isOverdue }">
<text class="deadline-icon">🕐</text>
<text class="deadline-text">{{ formatDeadline(project) }}</text>
</view>
</view>
<view class="empty-state" v-if="projectList.length === 0">
<text>暂无项目</text>
</view>
</view>
</view>
<!-- 客户信息标签页 -->
<view class="tab-content" v-if="activeTab === 'info'">
<view class="info-section">
<view class="section-title">
<view class="title-line"></view>
<text>基础信息</text>
</view>
<view class="info-item">
<text class="info-label">客户名称</text>
<text class="info-value">{{ customerDetail.name || '--' }}</text>
</view>
<view class="info-item">
<text class="info-label">联系电话</text>
<text class="info-value">{{ customerDetail.mobile || '--' }}</text>
</view>
<view class="info-item">
<text class="info-label">微信号</text>
<text class="info-value">{{ customerDetail.wechat || '--' }}</text>
</view>
<view class="info-item">
<text class="info-label">客户来源</text>
<text class="info-value">{{ customerDetail.source || '--' }}</text>
</view>
<view class="info-item">
<text class="info-label">客户意向</text>
<text class="info-value">{{ customerDetail.intent || '--' }}</text>
</view>
<view class="info-item">
<text class="info-label">意向强度</text>
<text class="info-value">{{ getIntentStrengthText(customerDetail.intentLevel) }}</text>
</view>
<view class="info-item">
<text class="info-label">客户地区</text>
<text class="info-value">{{ formatRegion(customerDetail.region, customerDetail.city) }}</text>
</view>
<view class="info-item">
<text class="info-label">工作微信</text>
<text class="info-value">{{ customerDetail.workWechat || '--' }}</text>
</view>
</view>
<view class="info-section">
<view class="section-title">
<view class="title-line"></view>
<text>其他信息</text>
</view>
<view class="info-item">
<text class="info-label">客户星级</text>
<view class="info-value-stars">
<text
class="star"
v-for="i in 5"
:key="i"
:class="{ 'filled': i <= getRatingFromIntentLevel(customerDetail.intentLevel) }"
>★</text>
</view>
</view>
<view class="info-item">
<text class="info-label">备注</text>
<text class="info-value">{{ customerDetail.remark || '--' }}</text>
</view>
<view class="info-item">
<text class="info-label">顾虑点</text>
<text class="info-value">{{ customerDetail.concerns || '--' }}</text>
</view>
<view class="info-item">
<text class="info-label">痛点</text>
<text class="info-value">{{ customerDetail.painPoints || '--' }}</text>
</view>
<view class="info-item">
<text class="info-label">关注点</text>
<text class="info-value">{{ customerDetail.focusPoints || '--' }}</text>
</view>
<view class="info-item">
<text class="info-label">需求点</text>
<text class="info-value">{{ customerDetail.requirements || '--' }}</text>
</view>
</view>
</view>
</scroll-view>
<!-- 底部操作栏 -->
<view class="bottom-actions">
<view class="action-btn" @click="handleNewFollowup">
<text class="action-icon">✎</text>
<text class="action-text">写新跟进</text>
</view>
<view class="action-btn" @click="handleNewTask">
<text class="action-icon">☑</text>
<text class="action-text">新建任务</text>
</view>
<view class="action-btn" @click="handleCall">
<text class="action-icon">☎</text>
<text class="action-text">拨打电话</text>
</view>
<view class="action-btn" @click="handleMore">
<text class="action-icon">⋯</text>
<text class="action-text">更多操作</text>
</view>
</view>
</view>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import { onLoad } from '@dcloudio/uni-app';
import { getCustomerDetail, getCustomerFollowupList, getCustomerProjects } from '@/common/api';
// 页面参数
const customerId = ref('');
const customerDetail = ref({});
const activeTab = ref('followup');
const followupList = ref([]);
const projectList = ref([]);
const loading = ref(false);
// 获取页面参数
onLoad((options) => {
if (options && options.id) {
customerId.value = options.id;
loadCustomerDetail();
loadFollowupList();
}
});
// 切换标签页
const switchTab = (tab) => {
activeTab.value = tab;
if (tab === 'followup' && followupList.value.length === 0) {
loadFollowupList();
} else if (tab === 'projects' && projectList.value.length === 0) {
loadProjectList();
}
};
// 加载客户详情
const loadCustomerDetail = async () => {
if (!customerId.value) return;
loading.value = true;
try {
const res = await getCustomerDetail(customerId.value);
if (res) {
customerDetail.value = res;
}
} catch (error) {
console.error('加载客户详情失败:', error);
uni.$uv.toast('加载客户详情失败');
} finally {
loading.value = false;
}
};
// 加载跟进动态列表
const loadFollowupList = async () => {
if (!customerId.value) return;
try {
const res = await getCustomerFollowupList(customerId.value);
if (res && Array.isArray(res)) {
followupList.value = res;
} else if (res && res.rows && Array.isArray(res.rows)) {
followupList.value = res.rows;
}
} catch (error) {
console.error('加载跟进动态失败:', error);
}
};
// 加载项目列表
const loadProjectList = async () => {
if (!customerId.value) return;
try {
const res = await getCustomerProjects(customerId.value);
if (res && Array.isArray(res)) {
projectList.value = res;
} else if (res && res.rows && Array.isArray(res.rows)) {
projectList.value = res.rows;
}
} catch (error) {
console.error('加载项目列表失败:', error);
}
};
// 获取状态样式类
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 getRatingFromIntentLevel = (intentLevel) => {
const levelMap = {
'1': 5,
'2': 3
};
return levelMap[intentLevel] || 0;
};
// 格式化日期时间
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 formatDate = (dateTime) => {
if (!dateTime) return '';
try {
const date = new Date(dateTime);
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${month}-${day}`;
} catch (e) {
return '';
}
};
// 获取项目状态样式类
const getProjectStatusClass = (status) => {
return {
'status-developing': status === 'developing' || status === '1',
'status-expiring': status === 'expiring' || status === '2'
};
};
// 获取项目状态文本
const getProjectStatusText = (status) => {
const statusMap = {
'developing': '开发中',
'expiring': '即将到期',
'1': '开发中',
'2': '即将到期'
};
return statusMap[status] || '未知';
};
// 格式化参与人
const formatParticipants = (participants) => {
if (!participants || !Array.isArray(participants)) return '--';
if (participants.length <= 4) {
return participants.join('、');
}
const firstFour = participants.slice(0, 4).join('、');
return `${firstFour}${participants.length}`;
};
// 格式化截止日期
const formatDeadline = (project) => {
if (!project.deadline) return '--';
const deadline = new Date(project.deadline);
const now = new Date();
const diffTime = deadline - now;
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
if (diffDays < 0) {
return `逾期${Math.abs(diffDays)}${formatDateTime(project.deadline)}`;
} else {
return `剩余${diffDays}${formatDateTime(project.deadline)}`;
}
};
// 格式化地区
const formatRegion = (region, city) => {
if (!region && !city) return '--';
return [region, city].filter(Boolean).join('/');
};
// 获取意向强度文本
const getIntentStrengthText = (intentLevel) => {
const levelMap = {
'1': '高',
'2': '中',
'3': '低'
};
return levelMap[intentLevel] || '--';
};
// 返回
const handleBack = () => {
uni.navigateBack();
};
// 跟进项点击
const handleFollowupClick = (item) => {
// 可以跳转到跟进详情
console.log('点击跟进:', item);
};
// 项目更多操作
const handleProjectMore = (project) => {
uni.showActionSheet({
itemList: ['查看详情', '编辑项目', '删除项目'],
success: (res) => {
console.log('选择了第' + (res.tapIndex + 1) + '个选项');
}
});
};
// 写新跟进
const handleNewFollowup = () => {
uni.navigateTo({
url: `/pages/customer-follow/index?customerId=${customerId.value}&customerName=${customerDetail.value.name}`
});
};
// 新建任务
const handleNewTask = () => {
uni.navigateTo({
url: `/pages/customer-tasks/index?customerId=${customerId.value}&customerName=${customerDetail.value.name}`
});
};
// 拨打电话
const handleCall = () => {
if (customerDetail.value.mobile) {
uni.makePhoneCall({
phoneNumber: customerDetail.value.mobile,
fail: (err) => {
console.error('拨打电话失败:', err);
uni.$uv.toast('拨打电话失败');
}
});
} else {
uni.$uv.toast('客户未设置电话号码');
}
};
// 更多操作
const handleMore = () => {
uni.showActionSheet({
itemList: ['编辑客户', '删除客户', '分享客户'],
success: (res) => {
console.log('选择了第' + (res.tapIndex + 1) + '个选项');
if (res.tapIndex === 0) {
// 编辑客户
uni.navigateTo({
url: `/pages/edit-customer/index?id=${customerId.value}`
});
}
}
});
};
// 组件挂载时的初始化(如果需要)
onMounted(() => {
// 数据已在 onLoad 中加载
});
</script>
<style lang="scss" scoped>
.customer-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;
}
.customer-summary-card {
background-color: #fff;
padding: 16px;
margin-bottom: 8px;
}
.summary-row {
display: flex;
margin-bottom: 12px;
&:last-child {
margin-bottom: 0;
}
}
.summary-item {
flex: 1;
display: flex;
flex-direction: column;
&:first-child {
margin-right: 16px;
}
}
.summary-label {
font-size: 12px;
color: #999;
margin-bottom: 4px;
}
.summary-value {
font-size: 14px;
color: #333;
}
.status-badge {
display: inline-block;
padding: 2px 8px;
border-radius: 4px;
font-size: 12px;
&.status-following {
background-color: #e3f2fd;
color: #1976d2;
}
&.status-pending {
background-color: #f5f5f5;
color: #666;
}
}
.stars {
display: flex;
gap: 2px;
}
.star {
font-size: 14px;
color: #ddd;
&.filled {
color: #ffc107;
}
}
.tab-navigation {
display: flex;
background-color: #fff;
border-bottom: 1px solid #eee;
padding: 0 16px;
}
.tab-item {
flex: 1;
padding: 12px 0;
text-align: center;
position: relative;
text {
font-size: 14px;
color: #666;
}
&.active {
text {
color: #1976d2;
font-weight: 600;
}
&::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 2px;
background-color: #1976d2;
}
}
}
.content-scroll {
flex: 1;
overflow-y: auto;
}
.tab-content {
padding: 16px;
}
// 跟进动态样式
.followup-timeline {
position: relative;
}
.followup-item {
display: flex;
margin-bottom: 16px;
background-color: #fff;
border-radius: 8px;
padding: 12px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05);
}
.timeline-dot {
width: 50px;
display: flex;
align-items: flex-start;
justify-content: center;
padding-top: 4px;
}
.dot-date {
font-size: 12px;
color: #1976d2;
font-weight: 600;
}
.followup-content {
flex: 1;
display: flex;
flex-direction: column;
}
.followup-header {
display: flex;
align-items: center;
margin-bottom: 8px;
}
.followup-avatar {
width: 32px;
height: 32px;
border-radius: 16px;
margin-right: 8px;
}
.followup-user-info {
flex: 1;
display: flex;
flex-direction: column;
}
.followup-user-name {
font-size: 14px;
color: #333;
font-weight: 500;
}
.followup-user-role {
font-size: 12px;
color: #999;
}
.followup-arrow {
font-size: 18px;
color: #ccc;
}
.followup-text {
font-size: 14px;
color: #666;
line-height: 1.5;
margin-bottom: 8px;
}
.followup-time {
font-size: 12px;
color: #999;
}
// 项目列表样式
.project-card {
background-color: #fff;
border-radius: 8px;
padding: 16px;
margin-bottom: 12px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05);
}
.project-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.project-status-tag {
padding: 4px 12px;
border-radius: 4px;
font-size: 12px;
&.status-developing {
background-color: #e3f2fd;
color: #1976d2;
}
&.status-expiring {
background-color: #fff3e0;
color: #f57c00;
}
}
.project-more {
font-size: 18px;
color: #999;
transform: rotate(90deg);
}
.project-name {
font-size: 16px;
font-weight: 600;
color: #333;
margin-bottom: 12px;
display: block;
}
.project-progress {
display: flex;
align-items: center;
margin-bottom: 12px;
}
.progress-bar {
flex: 1;
height: 6px;
background-color: #f0f0f0;
border-radius: 3px;
overflow: hidden;
margin-right: 8px;
}
.progress-fill {
height: 100%;
background-color: #1976d2;
border-radius: 3px;
transition: width 0.3s;
}
.progress-text {
font-size: 12px;
color: #1976d2;
font-weight: 600;
}
.project-participants {
margin-bottom: 8px;
}
.participants-text {
font-size: 12px;
color: #666;
}
.project-deadline {
display: flex;
align-items: center;
&.overdue {
.deadline-text {
color: #f44336;
}
}
}
.deadline-icon {
font-size: 14px;
margin-right: 4px;
}
.deadline-text {
font-size: 12px;
color: #666;
}
// 客户信息样式
.info-section {
background-color: #fff;
border-radius: 8px;
padding: 16px;
margin-bottom: 12px;
}
.section-title {
display: flex;
align-items: center;
margin-bottom: 16px;
}
.title-line {
width: 3px;
height: 16px;
background-color: #1976d2;
margin-right: 8px;
}
.section-title text {
font-size: 16px;
font-weight: 600;
color: #333;
}
.info-item {
display: flex;
padding: 12px 0;
border-bottom: 1px solid #f5f5f5;
&:last-child {
border-bottom: none;
}
}
.info-label {
width: 100px;
font-size: 14px;
color: #666;
flex-shrink: 0;
}
.info-value {
flex: 1;
font-size: 14px;
color: #333;
text-align: right;
}
.info-value-stars {
flex: 1;
display: flex;
justify-content: flex-end;
gap: 2px;
}
.empty-state {
text-align: center;
padding: 40px 0;
color: #999;
font-size: 14px;
}
// 底部操作栏
.bottom-actions {
display: flex;
background-color: #fff;
border-top: 1px solid #eee;
padding: 8px 0;
padding-bottom: calc(8px + env(safe-area-inset-bottom));
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.05);
}
.action-btn {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 8px;
}
.action-icon {
font-size: 20px;
color: #1976d2;
margin-bottom: 4px;
}
.action-text {
font-size: 12px;
color: #666;
}
</style>