实现客户管理客户列表下拉刷新
This commit is contained in:
parent
bc6f488cd0
commit
cf8af5a1af
|
|
@ -5,6 +5,8 @@
|
||||||
/**
|
/**
|
||||||
* 获取客户列表
|
* 获取客户列表
|
||||||
* @param {Object} params 请求参数(可选)
|
* @param {Object} params 请求参数(可选)
|
||||||
|
* @param {number} params.pageNum 页码(从1开始)
|
||||||
|
* @param {number} params.pageSize 每页数量
|
||||||
* @param {string[]} params.statusList 客户状态列表:["1"] 正在跟进, ["2"] 待跟进
|
* @param {string[]} params.statusList 客户状态列表:["1"] 正在跟进, ["2"] 待跟进
|
||||||
* @param {string} params.excludeld 排除id
|
* @param {string} params.excludeld 排除id
|
||||||
* @param {string[]} params.ids id列表
|
* @param {string[]} params.ids id列表
|
||||||
|
|
@ -22,6 +24,15 @@
|
||||||
export const getCustomerList = (params = {}) => {
|
export const getCustomerList = (params = {}) => {
|
||||||
const queryParams = [];
|
const queryParams = [];
|
||||||
|
|
||||||
|
// 处理分页参数
|
||||||
|
if (params.pageNum !== undefined && params.pageNum !== null) {
|
||||||
|
queryParams.push(`pageNum=${encodeURIComponent(params.pageNum)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.pageSize !== undefined && params.pageSize !== null) {
|
||||||
|
queryParams.push(`pageSize=${encodeURIComponent(params.pageSize)}`);
|
||||||
|
}
|
||||||
|
|
||||||
// 处理数组参数
|
// 处理数组参数
|
||||||
if (params.statusList && Array.isArray(params.statusList) && params.statusList.length > 0) {
|
if (params.statusList && Array.isArray(params.statusList) && params.statusList.length > 0) {
|
||||||
params.statusList.forEach(status => {
|
params.statusList.forEach(status => {
|
||||||
|
|
|
||||||
|
|
@ -34,15 +34,14 @@
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 客户列表 -->
|
<!-- 客户列表 -->
|
||||||
<scroll-view
|
<view
|
||||||
class="customer-list"
|
class="customer-list"
|
||||||
:class="{ 'with-filter': showFilter }"
|
:class="{ 'with-filter': showFilter }"
|
||||||
scroll-y
|
|
||||||
>
|
>
|
||||||
<view style="padding-inline: 8px">
|
<view style="padding-inline: 8px">
|
||||||
<view
|
<view
|
||||||
class="customer-card"
|
class="customer-card"
|
||||||
v-for="customer in filteredCustomers"
|
v-for="customer in list"
|
||||||
:key="customer.id"
|
:key="customer.id"
|
||||||
@click="handleCustomerClick(customer)"
|
@click="handleCustomerClick(customer)"
|
||||||
>
|
>
|
||||||
|
|
@ -106,16 +105,23 @@
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 空状态 -->
|
<!-- 空状态 -->
|
||||||
<view class="empty-state" v-if="filteredCustomers.length === 0 && !loading">
|
<view class="empty-state" v-if="isEmpty">
|
||||||
<text class="empty-text">暂无客户数据</text>
|
<text class="empty-text">暂无客户数据</text>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 加载状态 -->
|
<!-- 加载状态 -->
|
||||||
<view class="loading-state" v-if="loading">
|
<view class="loading-state" v-if="loading && list.length === 0">
|
||||||
<text class="loading-text">加载中...</text>
|
<text class="loading-text">加载中...</text>
|
||||||
</view>
|
</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>
|
||||||
</scroll-view>
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
<!-- 悬浮添加按钮 -->
|
<!-- 悬浮添加按钮 -->
|
||||||
<FabPlus @click="handleAddCustomer" />
|
<FabPlus @click="handleAddCustomer" />
|
||||||
|
|
@ -123,43 +129,32 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onMounted, watch, onActivated } from 'vue';
|
import { ref, computed, onMounted, watch } from 'vue';
|
||||||
import FabPlus from '@/components/FabPlus.vue';
|
import FabPlus from '@/components/FabPlus.vue';
|
||||||
import { useCustomerStore } from '@/store/customer';
|
import { usePagination } from '@/composables/usePagination';
|
||||||
|
import { getCustomerList } from '@/common/api/customer';
|
||||||
// 使用客户管理 store
|
|
||||||
const customerStore = useCustomerStore();
|
|
||||||
|
|
||||||
// 筛选状态
|
// 筛选状态
|
||||||
const showFilter = ref(false);
|
const showFilter = ref(false);
|
||||||
const filterStatus = ref('');
|
const filterStatus = ref('');
|
||||||
|
|
||||||
// 客户列表数据(从 store 获取)
|
// 使用分页组合式函数
|
||||||
const customers = ref([]);
|
const {
|
||||||
|
list,
|
||||||
// 下拉刷新状态
|
loading,
|
||||||
const refreshing = ref(false);
|
noMore,
|
||||||
|
isEmpty,
|
||||||
// 过滤后的客户列表(使用前端筛选,避免重复请求)
|
getList,
|
||||||
const filteredCustomers = computed(() => {
|
loadMore,
|
||||||
if (!filterStatus.value) {
|
updateParams,
|
||||||
return customers.value;
|
refresh
|
||||||
}
|
} = usePagination({
|
||||||
// 将筛选状态映射到API状态值
|
fetchData: getCustomerList,
|
||||||
const statusMap = {
|
mode: 'loadMore',
|
||||||
'following': '1', // 正在跟进
|
pageSize: 10,
|
||||||
'pending': '2' // 待跟进
|
defaultParams: {}
|
||||||
};
|
|
||||||
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) => {
|
const getStatusClass = (status) => {
|
||||||
return {
|
return {
|
||||||
|
|
@ -205,19 +200,22 @@ const formatDateTime = (dateTime) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 加载客户列表(使用 store 的缓存机制)
|
// 构建查询参数(包含筛选条件)
|
||||||
const loadCustomerList = async (forceRefresh = false) => {
|
const buildQueryParams = () => {
|
||||||
try {
|
const params = {};
|
||||||
// 使用 store 获取客户列表(带缓存)
|
|
||||||
// 不传筛选参数,获取全部数据,然后在前端筛选
|
// 根据筛选状态添加 statusList 参数
|
||||||
const data = await customerStore.fetchCustomerList({}, forceRefresh);
|
if (filterStatus.value) {
|
||||||
// 更新本地数据
|
const statusMap = {
|
||||||
customers.value = data || [];
|
'following': ['1'], // 正在跟进
|
||||||
} catch (error) {
|
'pending': ['2'] // 待跟进
|
||||||
console.error('加载客户列表失败:', error);
|
};
|
||||||
uni.$uv.toast('加载客户列表失败');
|
if (statusMap[filterStatus.value]) {
|
||||||
customers.value = [];
|
params.statusList = statusMap[filterStatus.value];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return params;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 处理客户点击
|
// 处理客户点击
|
||||||
|
|
@ -287,43 +285,32 @@ const handleAddCustomer = () => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// 监听筛选状态变化(不需要重新请求,只做前端筛选)
|
// 监听筛选状态变化,更新查询参数并重新加载
|
||||||
// 因为我们已经缓存了全部数据,切换筛选时只需要前端过滤即可
|
|
||||||
watch(filterStatus, () => {
|
watch(filterStatus, () => {
|
||||||
// 不需要重新加载,filteredCustomers computed 会自动更新
|
|
||||||
console.log('筛选状态变化:', filterStatus.value);
|
console.log('筛选状态变化:', filterStatus.value);
|
||||||
|
const params = buildQueryParams();
|
||||||
|
updateParams(params);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 组件挂载时加载数据
|
// 组件挂载时加载数据
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadCustomerList(false); // 首次加载,使用缓存
|
const params = buildQueryParams();
|
||||||
|
// updateParams 内部会调用 getList(),所以不需要重复调用
|
||||||
|
updateParams(params);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 下拉刷新处理
|
// 暴露方法供父组件调用(用于 onReachBottom)
|
||||||
const onRefresh = async () => {
|
const winB_LoadMore = () => {
|
||||||
refreshing.value = true;
|
if (!loading.value && !noMore.value) {
|
||||||
try {
|
loadMore();
|
||||||
// 强制刷新,忽略缓存
|
|
||||||
await loadCustomerList(true);
|
|
||||||
uni.$uv.toast('刷新成功');
|
|
||||||
} catch (error) {
|
|
||||||
console.error('刷新失败:', error);
|
|
||||||
uni.$uv.toast('刷新失败');
|
|
||||||
} finally {
|
|
||||||
// 延迟一下再关闭刷新状态,让用户看到刷新效果
|
|
||||||
setTimeout(() => {
|
|
||||||
refreshing.value = false;
|
|
||||||
}, 500);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 组件激活时(使用 v-show 时触发)刷新数据(如果缓存过期)
|
// 使用 defineExpose 暴露方法
|
||||||
onActivated(() => {
|
defineExpose({
|
||||||
// 如果缓存无效,则刷新数据
|
winB_LoadMore
|
||||||
if (!customerStore.isCacheValid) {
|
|
||||||
loadCustomerList(false);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|
@ -464,7 +451,7 @@ onActivated(() => {
|
||||||
padding-top: 52px;
|
padding-top: 52px;
|
||||||
padding-bottom: 100px; /* 为底部导航栏和悬浮按钮留出空间 */
|
padding-bottom: 100px; /* 为底部导航栏和悬浮按钮留出空间 */
|
||||||
background-color: #f5f7fa;
|
background-color: #f5f7fa;
|
||||||
overflow-y: auto;
|
/* 移除 overflow-y: auto,让页面本身滚动以支持 onReachBottom */
|
||||||
transition: padding-top 0.3s ease;
|
transition: padding-top 0.3s ease;
|
||||||
|
|
||||||
&.with-filter {
|
&.with-filter {
|
||||||
|
|
@ -703,5 +690,19 @@ onActivated(() => {
|
||||||
color: #909399;
|
color: #909399;
|
||||||
margin-top: 12px;
|
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>
|
</style>
|
||||||
|
|
||||||
|
|
|
||||||
201
composables/usePagination.js
Normal file
201
composables/usePagination.js
Normal file
|
|
@ -0,0 +1,201 @@
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页管理组合式函数
|
||||||
|
* @param {Object} options - 配置选项
|
||||||
|
* @param {Function} options.fetchData - 获取数据的API函数
|
||||||
|
* @param {Object} options.defaultParams - 默认查询参数
|
||||||
|
* @param {string} options.mode - 分页模式:'loadMore' 或 'pager'
|
||||||
|
* @param {number} options.pageSize - 每页数量
|
||||||
|
* @returns {Object} 分页相关的状态和方法
|
||||||
|
*/
|
||||||
|
export function usePagination(options = {}) {
|
||||||
|
const { fetchData, defaultParams = {}, mode = 'loadMore', pageSize = 10 } = options
|
||||||
|
|
||||||
|
// 基础状态
|
||||||
|
const list = ref([])
|
||||||
|
const loading = ref(false)
|
||||||
|
const error = ref(null)
|
||||||
|
|
||||||
|
// 分页参数
|
||||||
|
const queryParams = ref({
|
||||||
|
pageNum: 1,
|
||||||
|
pageSize,
|
||||||
|
...defaultParams,
|
||||||
|
})
|
||||||
|
|
||||||
|
// 分页信息
|
||||||
|
const pagination = ref({
|
||||||
|
total: 0,
|
||||||
|
currentPage: 1,
|
||||||
|
pageSize,
|
||||||
|
totalPages: 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
// 上拉加载相关
|
||||||
|
const noMore = ref(false)
|
||||||
|
|
||||||
|
// 计算属性
|
||||||
|
const hasData = computed(() => list.value.length > 0)
|
||||||
|
const isEmpty = computed(() => !loading.value && list.value.length === 0)
|
||||||
|
const canLoadMore = computed(() => mode === 'loadMore' && !noMore.value && !loading.value)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取数据
|
||||||
|
* @param {boolean} reset - 是否重置列表
|
||||||
|
*/
|
||||||
|
const getList = async (reset = false) => {
|
||||||
|
if (loading.value) return
|
||||||
|
|
||||||
|
if (reset) {
|
||||||
|
list.value = []
|
||||||
|
queryParams.value.pageNum = 1
|
||||||
|
noMore.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.value = true
|
||||||
|
error.value = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetchData(queryParams.value)
|
||||||
|
|
||||||
|
// 响应拦截器已经处理了错误情况,如果请求成功返回,response 就是数据对象
|
||||||
|
// 不再包含 code 和 msg 字段(已被拦截器过滤)
|
||||||
|
if (!response) {
|
||||||
|
throw new Error('获取数据失败:响应为空')
|
||||||
|
}
|
||||||
|
|
||||||
|
const { rows = [], total = 0 } = response
|
||||||
|
|
||||||
|
if (reset) {
|
||||||
|
list.value = rows
|
||||||
|
} else {
|
||||||
|
list.value.push(...rows)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新分页信息
|
||||||
|
pagination.value = {
|
||||||
|
total,
|
||||||
|
currentPage: queryParams.value.pageNum,
|
||||||
|
pageSize,
|
||||||
|
totalPages: Math.ceil(total / pageSize),
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否还有更多数据
|
||||||
|
if (mode === 'loadMore') {
|
||||||
|
noMore.value = queryParams.value.pageNum * pageSize >= total
|
||||||
|
console.log(
|
||||||
|
`noMore状态: ${noMore.value}, 当前页: ${queryParams.value.pageNum}, 每页: ${pageSize}, 总数: ${total}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`获取数据成功: 第${queryParams.value.pageNum}页,共${rows.length}条`)
|
||||||
|
} catch (err) {
|
||||||
|
console.error('获取数据失败:', err)
|
||||||
|
error.value = err
|
||||||
|
|
||||||
|
// 显示错误提示
|
||||||
|
uni.showToast({
|
||||||
|
title: '数据加载失败',
|
||||||
|
icon: 'none',
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 刷新数据
|
||||||
|
*/
|
||||||
|
const refresh = () => {
|
||||||
|
getList(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载下一页(上拉加载模式)
|
||||||
|
*/
|
||||||
|
const loadMore = () => {
|
||||||
|
if (!canLoadMore.value) return
|
||||||
|
|
||||||
|
queryParams.value.pageNum++
|
||||||
|
getList()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 跳转到指定页(分页器模式)
|
||||||
|
* @param {number} page - 目标页码
|
||||||
|
*/
|
||||||
|
const goToPage = page => {
|
||||||
|
if (page < 1 || page > pagination.value.totalPages || page === queryParams.value.pageNum) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
queryParams.value.pageNum = page
|
||||||
|
getList(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重置分页状态
|
||||||
|
*/
|
||||||
|
const reset = () => {
|
||||||
|
list.value = []
|
||||||
|
loading.value = false
|
||||||
|
error.value = null
|
||||||
|
noMore.value = false
|
||||||
|
queryParams.value.pageNum = 1
|
||||||
|
pagination.value = {
|
||||||
|
total: 0,
|
||||||
|
currentPage: 1,
|
||||||
|
pageSize,
|
||||||
|
totalPages: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新查询参数
|
||||||
|
* @param {Object} newParams - 新的查询参数
|
||||||
|
*/
|
||||||
|
const updateParams = newParams => {
|
||||||
|
queryParams.value = {
|
||||||
|
...queryParams.value,
|
||||||
|
...newParams,
|
||||||
|
pageNum: 1, // 重置页码
|
||||||
|
}
|
||||||
|
reset()
|
||||||
|
getList()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化日期
|
||||||
|
* @param {string} dateString - 日期字符串
|
||||||
|
* @returns {string} 格式化后的日期
|
||||||
|
*/
|
||||||
|
const formatDate = dateString => {
|
||||||
|
if (!dateString) return '未知'
|
||||||
|
const date = new Date(dateString)
|
||||||
|
return `${date.getFullYear()}.${String(date.getMonth() + 1).padStart(2, '0')}.${String(date.getDate()).padStart(2, '0')}`
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
// 状态
|
||||||
|
list,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
noMore,
|
||||||
|
pagination,
|
||||||
|
queryParams,
|
||||||
|
|
||||||
|
// 计算属性
|
||||||
|
hasData,
|
||||||
|
isEmpty,
|
||||||
|
canLoadMore,
|
||||||
|
|
||||||
|
// 方法
|
||||||
|
getList,
|
||||||
|
refresh,
|
||||||
|
loadMore,
|
||||||
|
goToPage,
|
||||||
|
reset,
|
||||||
|
updateParams,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
<!-- 内容区域 -->
|
<!-- 内容区域 -->
|
||||||
<view class="content-wrapper">
|
<view class="content-wrapper">
|
||||||
<!-- 客户管理(底部导航栏选中时显示,使用 v-show 避免组件销毁重建) -->
|
<!-- 客户管理(底部导航栏选中时显示,使用 v-show 避免组件销毁重建) -->
|
||||||
<CustomerManagement v-if="value === 3" />
|
<CustomerManagement v-if="value === 3" ref="customerManagementRef" />
|
||||||
|
|
||||||
<!-- 其他内容(底部导航栏未选中客户管理时显示) -->
|
<!-- 其他内容(底部导航栏未选中客户管理时显示) -->
|
||||||
<template v-else>
|
<template v-else>
|
||||||
|
|
@ -49,7 +49,7 @@
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, watch, onMounted } from 'vue';
|
import { ref, computed, watch, onMounted } from 'vue';
|
||||||
import { onShow } from '@dcloudio/uni-app';
|
import { onShow, onReachBottom } from '@dcloudio/uni-app';
|
||||||
import { useUserStore } from '@/store/user';
|
import { useUserStore } from '@/store/user';
|
||||||
import { getUserInfo } from '@/common/api';
|
import { getUserInfo } from '@/common/api';
|
||||||
|
|
||||||
|
|
@ -109,6 +109,9 @@ const showAdd = ref(false);
|
||||||
// ScheduleEditor 组件引用
|
// ScheduleEditor 组件引用
|
||||||
const scheduleEditorRef = ref(null);
|
const scheduleEditorRef = ref(null);
|
||||||
|
|
||||||
|
// CustomerManagement 组件引用
|
||||||
|
const customerManagementRef = ref(null);
|
||||||
|
|
||||||
// 处理添加按钮点击
|
// 处理添加按钮点击
|
||||||
const handleAddClick = () => {
|
const handleAddClick = () => {
|
||||||
// 从 ScheduleEditor 组件获取当前选择的日期
|
// 从 ScheduleEditor 组件获取当前选择的日期
|
||||||
|
|
@ -161,6 +164,14 @@ onShow(() => {
|
||||||
console.error('读取新日程数据失败:', e);
|
console.error('读取新日程数据失败:', e);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 触底加载更多(仅在客户管理页面时生效)
|
||||||
|
onReachBottom(() => {
|
||||||
|
// 只有在客户管理页面(value === 3)时才触发加载更多
|
||||||
|
if (value.value === 3 && customerManagementRef.value) {
|
||||||
|
customerManagementRef.value.winB_LoadMore();
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user