实现客户管理客户列表下拉刷新
This commit is contained in:
parent
bc6f488cd0
commit
cf8af5a1af
|
|
@ -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 => {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
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">
|
||||
<!-- 客户管理(底部导航栏选中时显示,使用 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>
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user