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

708 lines
16 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"
:class="{ 'with-filter': showFilter }"
scroll-y
>
<view style="padding-inline: 8px">
<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>
</view>
</scroll-view>
<!-- 悬浮添加按钮 -->
<FabPlus @click="handleAddCustomer" />
</view>
</template>
<script setup>
import { ref, computed, onMounted, watch, onActivated } from 'vue';
import FabPlus from '@/components/FabPlus.vue';
import { useCustomerStore } from '@/store/customer';
// 使用客户管理 store
const customerStore = useCustomerStore();
// 筛选状态
const showFilter = ref(false);
const filterStatus = ref('');
// 客户列表数据(从 store 获取)
const customers = ref([]);
// 下拉刷新状态
const refreshing = ref(false);
// 过滤后的客户列表(使用前端筛选,避免重复请求)
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);
});
// 加载状态(从 store 获取)
const loading = computed(() => customerStore.loading);
// 获取状态样式类
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;
}
};
// 加载客户列表(使用 store 的缓存机制)
const loadCustomerList = async (forceRefresh = false) => {
try {
// 使用 store 获取客户列表(带缓存)
// 不传筛选参数,获取全部数据,然后在前端筛选
const data = await customerStore.fetchCustomerList({}, forceRefresh);
// 更新本地数据
customers.value = data || [];
} catch (error) {
console.error('加载客户列表失败:', error);
uni.$uv.toast('加载客户列表失败');
customers.value = [];
}
};
// 处理客户点击
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, () => {
// 不需要重新加载filteredCustomers computed 会自动更新
console.log('筛选状态变化:', filterStatus.value);
});
// 组件挂载时加载数据
onMounted(() => {
loadCustomerList(false); // 首次加载,使用缓存
});
// 下拉刷新处理
const onRefresh = async () => {
refreshing.value = true;
try {
// 强制刷新,忽略缓存
await loadCustomerList(true);
uni.$uv.toast('刷新成功');
} catch (error) {
console.error('刷新失败:', error);
uni.$uv.toast('刷新失败');
} finally {
// 延迟一下再关闭刷新状态,让用户看到刷新效果
setTimeout(() => {
refreshing.value = false;
}, 500);
}
};
// 组件激活时(使用 v-show 时触发)刷新数据(如果缓存过期)
onActivated(() => {
// 如果缓存无效,则刷新数据
if (!customerStore.isCacheValid) {
loadCustomerList(false);
}
});
</script>
<style lang="scss" scoped>
.customer-management {
display: flex;
flex-direction: column;
height: 100%;
width: 100%;
background-color: #f5f7fa;
position: relative;
}
/* 顶部标题栏 */
.header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
background-color: #fff;
border-bottom: 1px solid #e4e7ed;
position: fixed;
top: 0;
right: 0;
left: 0;
z-index: 100;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
}
.header-title {
font-size: 18px;
font-weight: 600;
color: #303133;
letter-spacing: 0.5px;
}
.filter-btn {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
border-radius: 6px;
background-color: #f5f7fa;
transition: all 0.3s ease;
cursor: pointer;
&:active {
background-color: #e4e7ed;
transform: scale(0.95);
}
}
.filter-text {
font-size: 14px;
color: #606266;
font-weight: 500;
}
.filter-icon {
font-size: 12px;
color: #909399;
transition: transform 0.3s ease;
&.rotate {
transform: rotate(180deg);
}
}
/* 筛选面板 */
.filter-panel {
background-color: #fff;
padding: 16px;
border-bottom: 1px solid #e4e7ed;
position: fixed;
top: 52px;
right: 0;
left: 0;
z-index: 99;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
animation: slideDown 0.3s ease;
}
@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.filter-item {
display: flex;
align-items: center;
gap: 16px;
}
.filter-label {
font-size: 14px;
color: #606266;
flex-shrink: 0;
font-weight: 500;
}
.filter-options {
display: flex;
gap: 10px;
flex-wrap: wrap;
flex: 1;
}
.filter-option {
padding: 6px 16px;
font-size: 14px;
color: #606266;
background-color: #f5f7fa;
border-radius: 20px;
transition: all 0.3s ease;
cursor: pointer;
border: 1px solid transparent;
&:active {
transform: scale(0.95);
}
&.active {
color: #2885ff;
background-color: #e6f2ff;
border-color: #2885ff;
font-weight: 500;
}
}
/* 客户列表 */
.customer-list {
flex: 1;
padding-top: 52px;
padding-bottom: 100px; /* 为底部导航栏和悬浮按钮留出空间 */
background-color: #f5f7fa;
overflow-y: auto;
transition: padding-top 0.3s ease;
&.with-filter {
padding-top: 108px; /* header(52px) + filter panel(约56px) */
}
}
.customer-card {
background-color: #fff;
border-radius: 12px;
padding: 16px;
margin: 0 12px 12px 12px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
transition: all 0.3s ease;
border: 1px solid #f0f0f0;
&:active {
transform: scale(0.98);
box-shadow: 0 1px 6px rgba(0, 0, 0, 0.12);
}
&:last-child {
margin-bottom: 20px;
}
}
.customer-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 14px;
padding-bottom: 12px;
border-bottom: 1px solid #f0f2f5;
}
.customer-info {
display: flex;
flex-direction: column;
gap: 8px;
flex: 1;
min-width: 0;
}
.customer-name {
font-size: 16px;
font-weight: 600;
color: #303133;
line-height: 1.4;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.last-followup {
font-size: 12px;
color: #909399;
line-height: 1.4;
}
.status-indicator {
display: flex;
align-items: center;
gap: 6px;
flex-shrink: 0;
padding: 4px 10px;
background-color: #f5f7fa;
border-radius: 12px;
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
flex-shrink: 0;
&.status-following {
background-color: #2885ff;
box-shadow: 0 0 4px rgba(40, 133, 255, 0.4);
}
&.status-pending {
background-color: #909399;
}
}
.status-text {
font-size: 12px;
color: #606266;
font-weight: 500;
}
/* 客户星级 */
.customer-rating {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 14px;
padding: 8px 12px;
background-color: #fafbfc;
border-radius: 8px;
}
.rating-label {
font-size: 13px;
color: #606266;
font-weight: 500;
}
.stars {
display: flex;
gap: 3px;
align-items: center;
}
.star {
font-size: 16px;
color: #e4e7ed;
line-height: 1;
transition: color 0.2s ease;
&.filled {
color: #ffc107;
text-shadow: 0 0 2px rgba(255, 193, 7, 0.3);
}
}
/* 分配用户 */
.assigned-user {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 14px;
padding: 8px 12px;
background-color: #fafbfc;
border-radius: 8px;
}
.user-avatar {
width: 28px;
height: 28px;
border-radius: 50%;
background-color: #e4e7ed;
border: 2px solid #fff;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.user-name {
font-size: 13px;
color: #606266;
font-weight: 500;
}
/* 操作按钮 */
.action-buttons {
display: flex;
justify-content: space-around;
align-items: center;
padding-top: 14px;
border-top: 1px solid #f0f2f5;
gap: 8px;
}
.action-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
padding: 8px 12px;
border-radius: 8px;
transition: all 0.3s ease;
cursor: pointer;
flex: 1;
min-width: 0;
&:active {
background-color: #f5f7fa;
transform: scale(0.95);
}
}
.action-icon {
font-size: 18px;
color: #606266;
font-weight: 500;
line-height: 1;
transition: all 0.3s ease;
&.checked {
color: #2885ff;
font-weight: 600;
transform: scale(1.1);
}
}
.action-text {
font-size: 12px;
color: #606266;
font-weight: 500;
white-space: nowrap;
}
.action-arrow {
font-size: 16px;
color: #909399;
margin-left: 4px;
}
/* 空状态 */
.empty-state {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 80px 20px;
min-height: 300px;
}
.empty-text {
font-size: 14px;
color: #909399;
margin-top: 12px;
}
/* 加载状态 */
.loading-state {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 60px 20px;
min-height: 200px;
}
.loading-text {
font-size: 14px;
color: #909399;
margin-top: 12px;
}
</style>