OfficeSystem/components/CustomerManagement.vue
2025-11-12 15:34:06 +08:00

836 lines
20 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 v-if="useUserStore().getUserInfo?.roles?.some(r => ['admin','sys_admin'].includes(r))" style="display: flex;align-items: center;gap: 6px">
<view>私有</view><uv-switch v-model="filterSelf"></uv-switch>
</view>
<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 === 'potential' }"
@click="filterStatus = 'potential'"
>潜在</text>
<text
class="filter-option"
:class="{ 'active': filterStatus === 'intent' }"
@click="filterStatus = 'intent'"
>意向</text>
<text
class="filter-option"
:class="{ 'active': filterStatus === 'deal' }"
@click="filterStatus = 'deal'"
>成交</text>
<text
class="filter-option"
:class="{ 'active': filterStatus === 'invalid' }"
@click="filterStatus = 'invalid'"
>失效</text>
</view>
</view>
</view>
<!-- 客户列表 -->
<view
class="customer-list"
:class="{ 'with-filter': showFilter }"
>
<view style="padding-inline: 8px">
<view
class="customer-card"
v-for="customer in list"
:key="customer.id"
@click="handleCustomerClick(customer)"
>
<CustomerSummaryBrief
:name="customer.name"
:intents="customer.intents"
:status="customer.status"
:show-edit="true"
@edit="handleEdit(customer)"
/>
<!-- 客户详细信息区域 -->
<view class="customer-details">
<!-- 备注 -->
<view class="detail-row" v-if="customer.remark">
<text class="detail-label">备注:</text>
<text class="detail-value">{{ customer.remark || '--' }}</text>
</view>
<!-- 微信号 -->
<!-- <view class="detail-row">-->
<!-- <text class="detail-label">微信号:</text>-->
<!-- <text class="detail-value">{{ customer.wechat || '&#45;&#45;' }}</text>-->
<!-- </view>-->
<!-- 手机号 -->
<view class="detail-row" v-if="customer.mobile">
<text class="detail-label">手机号:</text>
<text class="detail-value">{{ customer.mobile || '--' }}</text>
</view>
<!-- 跟进内容 -->
<view class="detail-row" v-if="customer.lastFollowRecord && customer.lastFollowRecord.content">
<text class="detail-label">跟进内容:</text>
<text class="detail-value follow-content">{{ truncateText(customer.lastFollowRecord.content, 30) }}</text>
</view>
<!-- 最后跟进时间 -->
<!-- <view class="detail-row" v-if="customer.lastFollowTime">-->
<!-- <text class="detail-label">最后跟进:</text>-->
<!-- <text class="detail-value">{{ formatDateTime(customer.lastFollowTime) }}</text>-->
<!-- </view>-->
<!-- 下次跟进时间 -->
<view class="detail-row" v-if="customer.nextFollowTime">
<text class="detail-label">下次跟进:</text>
<text class="detail-value">{{ formatDateTime(customer.nextFollowTime) }}</text>
</view>
<!-- 分配用户 -->
<view class="detail-row" v-if="customer.followName">
<text class="detail-label">跟进人:</text>
<text class="detail-value">{{ customer.followName || '--' }}</text>
</view>
</view>
<!-- 操作按钮 -->
<view class="action-buttons">
<view class="action-item" @click.stop="handleFollowup(customer)">
<text class="action-icon">✓</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-icon">+</text>
<text class="action-text">更多</text>
</view>
</view>
</view>
<!-- 空状态 -->
<view class="empty-state" v-if="isEmpty">
<text class="empty-text">暂无客户数据</text>
</view>
<!-- 加载状态 -->
<view class="loading-state" v-if="loading && list.length === 0">
<text class="loading-text">加载中...</text>
</view>
<!-- 加载更多提示 -->
<view class="load-more" v-if="list.length > 0">
<text class="load-more-text" v-if="loading">加载中...</text>
<text class="load-more-text" v-else-if="noMore">没有更多数据了</text>
<text class="load-more-text" v-else>上拉加载更多</text>
</view>
</view>
</view>
<!-- 悬浮添加按钮 -->
<FabPlus @click="handleAddCustomer" />
</view>
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted, watch } from 'vue';
import FabPlus from '@/components/FabPlus.vue';
import CustomerSummaryBrief from '@/components/customer/CustomerSummaryBrief.vue';
import { usePagination } from '@/composables/usePagination';
import { getCustomerList, deleteCustomer } from '@/api/customer';
import{useUserStore} from "@/store/user";
import {
getStatusListByFilter
} from '@/utils/customerMappings';
// 筛选状态
const filterSelf =ref(false);
const showFilter = ref(false);
const filterStatus = ref('');
// 使用分页组合式函数
const {
list,
loading,
noMore,
isEmpty,
getList,
loadMore,
updateParams,
refresh,
queryParams,
reset
} = usePagination({
fetchData: getCustomerList,
mode: 'loadMore',
pageSize: 10,
defaultParams: {}
});
// 本页状态映射与样式由 CustomerSummaryBrief 统一处理
// 处理编辑
const handleEdit = (customer) => {
console.log('编辑客户:', customer);
uni.navigateTo({
url: `/pages/customer/edit/index?id=${customer.id}`
});
};
// 截断文本
const truncateText = (text, maxLength) => {
if (!text) return '--';
if (text.length <= maxLength) return text;
return text.substring(0, maxLength) + '...';
};
// 格式化日期时间
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 buildQueryParams = () => {
const params = {};
params.joinUserId = filterSelf.value? useUserStore().getUserInfo.user.userId : null
console.log(filterSelf.value? useUserStore().getUserInfo.user.userId : null)
// 只有有效的筛选状态才添加statusList参数
if (filterStatus.value) {
const statusList = getStatusListByFilter(filterStatus.value);
if (statusList && statusList.length > 0) {
params.statusList = statusList;
console.log(`筛选状态: ${filterStatus.value} -> statusList:`, statusList);
} else {
console.log(`未知的筛选状态: ${filterStatus.value},跳过状态筛选`);
}
} else {
console.log('无筛选状态,返回空参数');
}
return params;
};
// 处理客户点击
const handleCustomerClick = (customer) => {
console.log('点击客户:', customer);
// 跳转到客户详情页
uni.navigateTo({
url: `/pages/customer/detail/index?id=${customer.id}`
});
};
// 处理跟进
const handleFollowup = (customer) => {
console.log('跟进客户:', customer);
// 跳转到跟进新增页
uni.navigateTo({
url: `/pages/customer/follow/add/index?customerId=${customer.id}&customerName=${encodeURIComponent(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) => {
if (res.tapIndex === 0) {
// 编辑客户
uni.navigateTo({
url: `/pages/customer/edit/index?id=${customer.id}`
});
} else if (res.tapIndex === 1) {
// 删除客户
uni.showModal({
title: '确认删除',
content: `确定要删除客户"${customer.name}"吗?`,
success: async (modalRes) => {
if (modalRes.confirm) {
try {
// 显示加载提示
uni.showLoading({
title: '删除中...',
mask: true
});
// 调用删除API
await deleteCustomer(customer.id);
// 隐藏加载提示
uni.hideLoading();
// 显示成功提示
uni.$uv.toast('删除成功');
// 刷新列表
refresh();
} catch (error) {
// 隐藏加载提示
uni.hideLoading();
// 显示错误提示
console.error('删除客户失败:', error);
uni.$uv.toast(error?.message || '删除失败,请重试');
}
}
}
});
} else if (res.tapIndex === 2) {
// 查看详情
uni.navigateTo({
url: `/pages/customer/detail/index?id=${customer.id}`
});
}
}
});
};
// 处理添加客户
const handleAddCustomer = () => {
console.log('添加新客户');
// 跳转到添加客户页面
uni.navigateTo({
url: '/pages/customer/add/index'
});
};
// 监听筛选状态变化,更新查询参数并重新加载
watch(filterStatus, () => {
console.log('筛选状态变化:', filterStatus.value);
// 点击"全部"时,直接重置并刷新,清除所有缓存参数
if (filterStatus.value === '') {
// 重置分页状态
reset();
// 清除所有查询参数只保留基础分页参数保留用户id过滤参数
queryParams.value = {
joinUserId: filterSelf.value? useUserStore().getUserInfo.user.userId : null,
pageNum: 1,
pageSize: 10
};
// 直接刷新列表
refresh();
} else {
// 其他筛选状态,使用 updateParams 更新参数
const params = buildQueryParams();
updateParams(params);
}
});
watch(filterSelf, () => {
console.log('筛选是否自己变化:', filterSelf.value);
const params = buildQueryParams();
console.log('<UNK>:当前参数', params);
updateParams(params);
})
// 监听客户列表刷新事件
const handleCustomerListRefresh = () => {
console.log('收到客户列表刷新事件');
refresh();
// 清除存储标志,避免 onShow 时重复刷新
uni.removeStorageSync('customerListNeedRefresh');
};
// 组件挂载时加载数据
onMounted(() => {
const params = buildQueryParams();
// updateParams 内部会调用 getList(),所以不需要重复调用
updateParams(params);
// 监听客户列表刷新事件
uni.$on('customerListRefresh', handleCustomerListRefresh);
});
// 组件卸载时移除事件监听
onUnmounted(() => {
uni.$off('customerListRefresh', handleCustomerListRefresh);
});
// 暴露方法供父组件调用(用于 onReachBottom
const winB_LoadMore = () => {
if (!loading.value && !noMore.value) {
loadMore();
}
};
// 使用 defineExpose 暴露方法
defineExpose({
winB_LoadMore,
refresh // 暴露 refresh 方法,供外部调用
});
</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: 28px;
padding-bottom: 100px; /* 为底部导航栏和悬浮按钮留出空间 */
background-color: #f5f7fa;
/* 移除 overflow-y: auto让页面本身滚动以支持 onReachBottom */
transition: padding-top 0.3s ease;
&.with-filter {
padding-top: 132px; /* 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;
align-items: center;
margin-bottom: 12px;
}
.customer-name-wrapper {
display: flex;
align-items: center;
gap: 8px;
}
.customer-name {
font-size: 16px;
font-weight: 600;
color: #2885ff;
line-height: 1.4;
}
.edit-icon {
color: #909399;
cursor: pointer;
opacity: 0.6;
transition: opacity 0.3s ease;
&:active {
opacity: 1;
}
}
/* 标签区域 */
.tags-section {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 12px;
}
/* 状态标签 */
.status-tag {
display: inline-block;
padding: 4px 12px;
font-size: 13px;
font-weight: 500;
border-radius: 12px;
white-space: nowrap;
&.status-tag-potential {
color: #e6a23c;
background-color: #fdf6ec;
border: 1px solid #f5dab1;
}
&.status-tag-intent {
color: #409eff;
background-color: #ecf5ff;
border: 1px solid #b3d8ff;
}
&.status-tag-deal {
color: #67c23a;
background-color: #f0f9ff;
border: 1px solid #b3e19d;
}
&.status-tag-invalid {
color: #f56c6c;
background-color: #fef0f0;
border: 1px solid #fbc4c4;
}
}
/* 意向强度标签 */
.intent-level-tag {
display: inline-block;
padding: 4px 12px;
font-size: 13px;
font-weight: 500;
color: #e6a23c;
background-color: #fef9e7;
border: 1px solid #f5dab1;
border-radius: 12px;
white-space: nowrap;
}
/* 客户意向标签 */
.intent-tag {
display: inline-block;
padding: 4px 12px;
font-size: 13px;
font-weight: 500;
color: #67c23a;
background-color: #f0f9ff;
border: 1px solid #b3e19d;
border-radius: 12px;
white-space: nowrap;
}
/* 客户详细信息区域 */
.customer-details {
margin-top: 12px;
margin-bottom: 12px;
padding-top: 12px;
border-top: 1px solid #f0f2f5;
}
.detail-row {
display: flex;
align-items: flex-start;
margin-bottom: 8px;
font-size: 14px;
line-height: 1.5;
&:last-child {
margin-bottom: 0;
}
}
.detail-label {
color: #909399;
margin-right: 8px;
flex-shrink: 0;
min-width: 70px;
}
.detail-value {
color: #303133;
flex: 1;
word-break: break-all;
}
/* 操作按钮 */
.action-buttons {
display: flex;
justify-content: space-around;
align-items: center;
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;
}
/* 加载更多提示 */
.load-more {
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
margin-bottom: 20px;
}
.load-more-text {
font-size: 13px;
color: #909399;
}
</style>