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

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 {number} params.pageNum 页码从1开始
* @param {number} params.pageSize 每页数量
* @param {string[]} params.statusList 客户状态列表["1"] 正在跟进, ["2"] 待跟进
* @param {string} params.excludeld 排除id
* @param {string[]} params.ids id列表
@ -22,6 +24,15 @@
export const getCustomerList = (params = {}) => {
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) {
params.statusList.forEach(status => {

View File

@ -34,15 +34,14 @@
</view>
<!-- 客户列表 -->
<scroll-view
<view
class="customer-list"
:class="{ 'with-filter': showFilter }"
scroll-y
>
<view style="padding-inline: 8px">
<view
class="customer-card"
v-for="customer in filteredCustomers"
v-for="customer in list"
:key="customer.id"
@click="handleCustomerClick(customer)"
>
@ -106,16 +105,23 @@
</view>
<!-- 空状态 -->
<view class="empty-state" v-if="filteredCustomers.length === 0 && !loading">
<view class="empty-state" v-if="isEmpty">
<text class="empty-text">暂无客户数据</text>
</view>
<!-- 加载状态 -->
<view class="loading-state" v-if="loading">
<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>
</scroll-view>
</view>
</view>
<!-- 悬浮添加按钮 -->
<FabPlus @click="handleAddCustomer" />
@ -123,43 +129,32 @@
</template>
<script setup>
import { ref, computed, onMounted, watch, onActivated } from 'vue';
import { ref, computed, onMounted, watch } from 'vue';
import FabPlus from '@/components/FabPlus.vue';
import { useCustomerStore } from '@/store/customer';
// 使 store
const customerStore = useCustomerStore();
import { usePagination } from '@/composables/usePagination';
import { getCustomerList } from '@/common/api/customer';
//
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);
// 使
const {
list,
loading,
noMore,
isEmpty,
getList,
loadMore,
updateParams,
refresh
} = usePagination({
fetchData: getCustomerList,
mode: 'loadMore',
pageSize: 10,
defaultParams: {}
});
// store
const loading = computed(() => customerStore.loading);
//
const getStatusClass = (status) => {
return {
@ -205,19 +200,22 @@ const formatDateTime = (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 buildQueryParams = () => {
const params = {};
// statusList
if (filterStatus.value) {
const statusMap = {
'following': ['1'], //
'pending': ['2'] //
};
if (statusMap[filterStatus.value]) {
params.statusList = statusMap[filterStatus.value];
}
}
return params;
};
//
@ -287,43 +285,32 @@ const handleAddCustomer = () => {
});
};
//
//
//
watch(filterStatus, () => {
// filteredCustomers computed
console.log('筛选状态变化:', filterStatus.value);
const params = buildQueryParams();
updateParams(params);
});
//
onMounted(() => {
loadCustomerList(false); // 使
const params = buildQueryParams();
// updateParams getList()
updateParams(params);
});
//
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);
// onReachBottom
const winB_LoadMore = () => {
if (!loading.value && !noMore.value) {
loadMore();
}
};
// 使 v-show
onActivated(() => {
//
if (!customerStore.isCacheValid) {
loadCustomerList(false);
}
// 使 defineExpose
defineExpose({
winB_LoadMore
});
</script>
<style lang="scss" scoped>
@ -464,7 +451,7 @@ onActivated(() => {
padding-top: 52px;
padding-bottom: 100px; /* 为底部导航栏和悬浮按钮留出空间 */
background-color: #f5f7fa;
overflow-y: auto;
/* 移除 overflow-y: auto让页面本身滚动以支持 onReachBottom */
transition: padding-top 0.3s ease;
&.with-filter {
@ -703,5 +690,19 @@ onActivated(() => {
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>

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">
<!-- 客户管理底部导航栏选中时显示使用 v-show 避免组件销毁重建 -->
<CustomerManagement v-if="value === 3" />
<CustomerManagement v-if="value === 3" ref="customerManagementRef" />
<!-- 其他内容底部导航栏未选中客户管理时显示 -->
<template v-else>
@ -49,7 +49,7 @@
<script setup>
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 { getUserInfo } from '@/common/api';
@ -109,6 +109,9 @@ const showAdd = ref(false);
// ScheduleEditor
const scheduleEditorRef = ref(null);
// CustomerManagement
const customerManagementRef = ref(null);
//
const handleAddClick = () => {
// ScheduleEditor
@ -161,6 +164,14 @@ onShow(() => {
console.error('读取新日程数据失败:', e);
}
});
//
onReachBottom(() => {
// value === 3
if (value.value === 3 && customerManagementRef.value) {
customerManagementRef.value.winB_LoadMore();
}
});
</script>
<style lang="scss" scoped>