实现客户管理客户列表下拉刷新

This commit is contained in:
WindowBird 2025-11-08 13:36:04 +08:00
parent bc6f488cd0
commit cf8af5a1af
4 changed files with 299 additions and 75 deletions

View File

@ -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 => {

View File

@ -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>

View 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,
}
}

View File

@ -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>