OfficeSystem/components/CustomerManagement.vue
2025-11-07 11:40:13 +08:00

580 lines
13 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-management">
<!-- 顶部标题栏 -->
<view class="header">
<text class="header-title">客户管理</text>
<view class="filter-btn" @click="showFilter = !showFilter">
<text class="filter-text">筛选</text>
<text class="filter-icon" :class="{ 'rotate': showFilter }"></text>
</view>
</view>
<!-- 筛选区域可选 -->
<view class="filter-panel" v-if="showFilter">
<view class="filter-item">
<text class="filter-label">状态</text>
<view class="filter-options">
<text
class="filter-option"
:class="{ 'active': filterStatus === '' }"
@click="filterStatus = ''"
>全部</text>
<text
class="filter-option"
:class="{ 'active': filterStatus === 'following' }"
@click="filterStatus = 'following'"
>正在跟进</text>
<text
class="filter-option"
:class="{ 'active': filterStatus === 'pending' }"
@click="filterStatus = 'pending'"
>待跟进</text>
</view>
</view>
</view>
<!-- 客户列表 -->
<scroll-view class="customer-list" scroll-y>
<view
class="customer-card"
v-for="customer in filteredCustomers"
:key="customer.id"
@click="handleCustomerClick(customer)"
>
<!-- 客户信息区域 -->
<view class="customer-header">
<view class="customer-info">
<text class="customer-name">{{ customer.name }}</text>
<text class="last-followup">最后跟进: {{ formatDateTime(customer.lastFollowTime) }}</text>
</view>
<view class="status-indicator">
<view
class="status-dot"
:class="getStatusClass(customer.status)"
></view>
<text class="status-text">{{ getStatusText(customer.status) }}</text>
</view>
</view>
<!-- 客户星级 -->
<view class="customer-rating">
<text class="rating-label">客户星级:</text>
<view class="stars">
<text
class="star"
v-for="i in 5"
:key="i"
:class="{ 'filled': i <= getRatingFromIntentLevel(customer.intentLevel) }"
>★</text>
</view>
</view>
<!-- 分配用户 -->
<view class="assigned-user">
<image
class="user-avatar"
:src="customer.assignedUserAvatar || '/static/default-avatar.png'"
mode="aspectFill"
/>
<text class="user-name">{{ customer.followName || '未分配' }}</text>
</view>
<!-- 操作按钮 -->
<view class="action-buttons">
<view class="action-item" @click.stop="handleFollowup(customer)">
<text class="action-icon" :class="{ 'checked': customer.status === '1' }">✓</text>
<text class="action-text">跟进</text>
</view>
<view class="action-item" @click.stop="handleTasks(customer)">
<text class="action-icon">☰</text>
<text class="action-text">任务</text>
</view>
<view class="action-item" @click.stop="handleCall(customer)">
<text class="action-icon">☎</text>
<text class="action-text">电话</text>
</view>
<view class="action-item" @click.stop="handleMore(customer)">
<text class="action-text">更多</text>
<text class="action-arrow"></text>
</view>
</view>
</view>
<!-- 空状态 -->
<view class="empty-state" v-if="filteredCustomers.length === 0 && !loading">
<text class="empty-text">暂无客户数据</text>
</view>
<!-- 加载状态 -->
<view class="loading-state" v-if="loading">
<text class="loading-text">加载中...</text>
</view>
</scroll-view>
<!-- 悬浮添加按钮 -->
<FabPlus @click="handleAddCustomer" />
</view>
</template>
<script setup>
import { ref, computed, onMounted, watch } from 'vue';
import FabPlus from '@/components/FabPlus.vue';
import { getCustomerList } from '@/common/api';
// 筛选状态
const showFilter = ref(false);
const filterStatus = ref('');
// 加载状态
const loading = ref(false);
// 客户列表数据
const customers = ref([]);
// 过滤后的客户列表
const filteredCustomers = computed(() => {
if (!filterStatus.value) {
return customers.value;
}
// 将筛选状态映射到API状态值
const statusMap = {
'following': '1', // 正在跟进
'pending': '2' // 待跟进
};
const targetStatus = statusMap[filterStatus.value];
if (!targetStatus) {
return customers.value;
}
return customers.value.filter(customer => customer.status === targetStatus);
});
// 获取状态样式类
const getStatusClass = (status) => {
return {
'status-following': status === '1', // 正在跟进
'status-pending': status === '2' // 待跟进
};
};
// 获取状态文本
const getStatusText = (status) => {
const statusMap = {
'1': '正在跟进',
'2': '待跟进',
'3': '其他',
'4': '无效客户'
};
return statusMap[status] || '未知';
};
// 根据意向等级获取星级1=高=5星2=中=3星
const getRatingFromIntentLevel = (intentLevel) => {
const levelMap = {
'1': 5, // 高意向 = 5星
'2': 3 // 中意向 = 3星
};
return levelMap[intentLevel] || 0;
};
// 格式化日期时间
const formatDateTime = (dateTime) => {
if (!dateTime) return '暂无';
// 将 "2025-10-29 09:00:00" 格式化为 "2025-10-29 09:00"
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 loadCustomerList = async () => {
loading.value = true;
try {
// 构建请求参数
const params = {};
// 根据筛选状态设置statusList参数
if (filterStatus.value) {
const statusMap = {
'following': ['1'], // 正在跟进
'pending': ['2'] // 待跟进
};
if (statusMap[filterStatus.value]) {
params.statusList = statusMap[filterStatus.value];
}
}
// 调用 API 获取客户列表
const res = await getCustomerList(params);
// API返回格式: { total: number, rows: array }
if (res && res.rows && Array.isArray(res.rows)) {
customers.value = res.rows;
} else if (res && Array.isArray(res)) {
// 兼容直接返回数组的情况
customers.value = res;
} else {
customers.value = [];
}
} catch (error) {
console.error('加载客户列表失败:', error);
uni.$uv.toast('加载客户列表失败');
customers.value = [];
} finally {
loading.value = false;
}
};
// 处理客户点击
const handleCustomerClick = (customer) => {
console.log('点击客户:', customer);
// 跳转到客户详情页
uni.navigateTo({
url: `/pages/customer/detail/index?id=${customer.id}`
});
};
// 处理跟进
const handleFollowup = (customer) => {
console.log('跟进客户:', customer);
// TODO: 调用 API 更新跟进状态
// 这里可以跳转到跟进记录页面或打开跟进对话框
uni.navigateTo({
url: `/pages/customer-follow/index?customerId=${customer.id}&customerName=${customer.name}`
});
};
// 处理任务
const handleTasks = (customer) => {
console.log('查看客户任务:', customer);
// 可以跳转到客户任务列表页
uni.navigateTo({
url: `/pages/customer-tasks/index?customerId=${customer.id}&customerName=${customer.name}`
});
};
// 处理电话
const handleCall = (customer) => {
console.log('拨打客户电话:', customer);
// 使用 mobile 字段
if (customer.mobile) {
uni.makePhoneCall({
phoneNumber: customer.mobile,
fail: (err) => {
console.error('拨打电话失败:', err);
uni.$uv.toast('拨打电话失败');
}
});
} else {
uni.$uv.toast('客户未设置电话号码');
}
};
// 处理更多
const handleMore = (customer) => {
console.log('更多操作:', customer);
// 可以显示操作菜单
uni.showActionSheet({
itemList: ['编辑客户', '删除客户', '查看详情'],
success: (res) => {
console.log('选择了第' + (res.tapIndex + 1) + '个选项');
// 根据选择执行相应操作
}
});
};
// 处理添加客户
const handleAddCustomer = () => {
console.log('添加新客户');
// 跳转到添加客户页面
uni.navigateTo({
url: '/pages/customer/add/index'
});
};
// 监听筛选状态变化,重新加载数据
watch(filterStatus, () => {
loadCustomerList();
});
// 组件挂载时加载数据
onMounted(() => {
loadCustomerList();
});
</script>
<style lang="scss" scoped>
.customer-management {
display: flex;
flex-direction: column;
height: 100%;
background-color: #f5f5f5;
}
/* 顶部标题栏 */
.header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
background-color: #fff;
border-bottom: 1px solid #eee;
position: sticky;
top: 0;
z-index: 100;
}
.header-title {
font-size: 18px;
font-weight: 600;
color: #333;
}
.filter-btn {
display: flex;
align-items: center;
gap: 4px;
padding: 4px 8px;
cursor: pointer;
}
.filter-text {
font-size: 14px;
color: #666;
}
.filter-icon {
font-size: 12px;
color: #666;
transition: transform 0.3s;
&.rotate {
transform: rotate(180deg);
}
}
/* 筛选面板 */
.filter-panel {
background-color: #fff;
padding: 12px 16px;
border-bottom: 1px solid #eee;
}
.filter-item {
display: flex;
align-items: center;
gap: 12px;
}
.filter-label {
font-size: 14px;
color: #666;
flex-shrink: 0;
}
.filter-options {
display: flex;
gap: 12px;
flex-wrap: wrap;
}
.filter-option {
padding: 4px 12px;
font-size: 14px;
color: #666;
background-color: #f5f5f5;
border-radius: 4px;
&.active {
color: #1976d2;
background-color: #e3f2fd;
}
}
/* 客户列表 */
.customer-list {
flex: 1;
padding: 12px;
padding-bottom: 80px; /* 为底部导航栏留出空间 */
}
.customer-card {
background-color: #fff;
border-radius: 8px;
padding: 16px;
margin-bottom: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}
.customer-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 12px;
}
.customer-info {
display: flex;
flex-direction: column;
gap: 6px;
flex: 1;
}
.customer-name {
font-size: 16px;
font-weight: 600;
color: #333;
}
.last-followup {
font-size: 12px;
color: #999;
}
.status-indicator {
display: flex;
align-items: center;
gap: 4px;
flex-shrink: 0;
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
&.status-following {
background-color: #1976d2;
}
&.status-pending {
background-color: #999;
}
}
.status-text {
font-size: 12px;
color: #666;
}
/* 客户星级 */
.customer-rating {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 12px;
}
.rating-label {
font-size: 14px;
color: #666;
}
.stars {
display: flex;
gap: 2px;
}
.star {
font-size: 16px;
color: #ddd;
&.filled {
color: #999;
}
}
/* 分配用户 */
.assigned-user {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 12px;
}
.user-avatar {
width: 24px;
height: 24px;
border-radius: 50%;
background-color: #f0f0f0;
}
.user-name {
font-size: 12px;
color: #999;
}
/* 操作按钮 */
.action-buttons {
display: flex;
justify-content: space-around;
align-items: center;
padding-top: 12px;
border-top: 1px solid #f0f0f0;
}
.action-item {
display: flex;
align-items: center;
gap: 4px;
padding: 4px 8px;
cursor: pointer;
}
.action-icon {
font-size: 14px;
color: #666;
font-weight: 500;
&.checked {
color: #1976d2;
font-weight: 600;
}
}
.action-text {
font-size: 14px;
color: #666;
}
.action-arrow {
font-size: 18px;
color: #999;
margin-left: 4px;
}
/* 空状态 */
.empty-state {
display: flex;
justify-content: center;
align-items: center;
padding: 60px 0;
}
.empty-text {
font-size: 14px;
color: #999;
}
/* 加载状态 */
.loading-state {
display: flex;
justify-content: center;
align-items: center;
padding: 40px 0;
}
.loading-text {
font-size: 14px;
color: #999;
}
</style>