Files
klp-mono/apps/hand-factory/pages/todo/index.vue
王文昊 5890b1d114 refactor(todo): 替换滑动触发加载为点击加载更多
重构了待办页面的加载更多逻辑:移除滚动监听和自动加载逻辑,改为点击按钮触发加载;优化了加载状态提示和UI样式,简化了滚动视图高度计算逻辑,移除了多余的状态变量和防抖冷却逻辑。
2026-05-25 10:10:53 +08:00

613 lines
15 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="todo-container">
<!-- 自定义导航栏 -->
<view class="custom-nav-bar" :style="{ paddingTop: statusBarHeight + 'px' }">
<view class="nav-content">
<text class="nav-title">待办事项</text>
</view>
</view>
<!-- Tab切换 -->
<view class="tab-bar">
<view
v-for="tab in tabs"
:key="tab.key"
class="tab-item"
:class="{ active: activeTab === tab.key }"
@click="handleTabChange(tab.key)"
>
<text class="tab-text">{{ tab.label }}</text>
<text v-if="tab.badge > 0" class="tab-badge">{{ tab.badge }}</text>
</view>
</view>
<!-- 筛选栏 -->
<filter-bar
:loading="loading"
:total="total"
@search="handleSearch"
@reset="handleReset"
/>
<!-- 列表内容 -->
<scroll-view
scroll-y
class="list-container"
:style="{ height: scrollViewHeight + 'px' }"
:refresher-enabled="true"
:refresher-triggered="refreshing"
@refresherrefresh="onRefresh"
>
<!-- 待贴标签列表 -->
<view v-if="activeTab === 'label'" class="coil-list">
<coil-card
v-for="item in list"
:key="item.coilId"
:data="item"
@relabel="handleRelabel"
@view-record="handleViewRecord"
@view-detail="handleViewDetail"
/>
</view>
<!-- 其他Tab占位 -->
<view v-else class="placeholder-page">
<text class="placeholder-icon">🚧</text>
<text class="placeholder-text">功能开发中</text>
</view>
<!-- 空状态 -->
<view v-if="activeTab === 'label' && list.length === 0 && !loading" class="empty-state">
<text class="empty-icon">📭</text>
<text class="empty-text">暂无待办事项</text>
</view>
<!-- 加载状态 -->
<view v-if="loading && list.length === 0" class="loading-state">
<uni-load-more status="loading" />
</view>
<!-- 加载更多 -->
<view v-if="activeTab === 'label' && list.length > 0 && list.length < total" class="load-more-wrapper" @click="onLoadMore">
<view v-if="loading" class="load-more-loading">
<text class="loading-text">加载中...</text>
</view>
<view v-else class="load-more-btn">
<text class="btn-text">点击加载更多</text>
<text class="btn-hint">(已加载 {{ list.length }} / {{ total }} )</text>
</view>
</view>
<!-- 已加载全部 -->
<view v-if="activeTab === 'label' && list.length > 0 && list.length >= total" class="load-more-wrapper no-more">
<text class="no-more-text">已加载全部 {{ total }} 条数据</text>
</view>
</scroll-view>
<!-- 记录弹窗 -->
<record-popup ref="recordPopup" />
<!-- 重贴标签弹窗 -->
<relabel-popup ref="relabelPopup" @success="handleRelabelSuccess" />
</view>
</template>
<script>
import { listMaterialCoil } from '@/api/wms/coil'
import FilterBar from './components/filter-bar.vue'
import CoilCard from './components/coil-card.vue'
import RecordPopup from './components/record-popup.vue'
import RelabelPopup from './components/relabel-popup.vue'
export default {
components: {
FilterBar,
CoilCard,
RecordPopup,
RelabelPopup
},
data() {
return {
statusBarHeight: 0,
scrollViewHeight: 0, // scroll-view 计算高度
tabs: [
{ key: 'label', label: '待贴标签', badge: 0 },
{ key: 'inspect', label: '检验任务', badge: 0 },
{ key: 'approval', label: '质保书审批', badge: 0 },
{ key: 'other', label: '其他代办', badge: 0 }
],
activeTab: 'label',
list: [],
loading: false,
refreshing: false,
query: {
pageNum: 1,
pageSize: 10,
enterCoilNo: '',
currentCoilNo: '',
itemName: '',
itemSpecification: '',
itemMaterial: '',
itemManufacturer: '',
hasTransferType: true // 待贴标签只显示有调拨类型的钢卷
},
total: 0,
loadMoreTimer: null // 加载更多防抖定时器
}
},
created() {
// 获取状态栏高度
const systemInfo = uni.getSystemInfoSync()
this.statusBarHeight = systemInfo.statusBarHeight || 0
// 计算 scroll-view 高度
this.calcScrollViewHeight()
},
onReady() {
// 页面就绪后再次计算高度(确保布局完成)
this.calcScrollViewHeight()
},
computed: {
loadMoreStatus() {
if (this.loading) return 'loading'
if (this.list.length >= this.total) return 'noMore'
return 'more'
}
},
onLoad() {
this.fetchList()
},
methods: {
// 计算 scroll-view 高度
calcScrollViewHeight() {
const systemInfo = uni.getSystemInfoSync()
const windowHeight = systemInfo.windowHeight
const statusBarHeight = systemInfo.statusBarHeight || 0
// 各区域高度单位px
const navBarHeight = 44 // 导航栏高度
const tabBarHeight = 50 // Tab栏高度
const filterBarHeight = 50 // 筛选栏高度(收起状态)
const bottomSafeArea = systemInfo.safeAreaInsets?.bottom || 0 // 底部安全区域
// 计算 scroll-view 可用高度
this.scrollViewHeight = windowHeight - statusBarHeight - navBarHeight - tabBarHeight - filterBarHeight - bottomSafeArea
console.log('scroll-view 高度计算:', {
windowHeight,
statusBarHeight,
navBarHeight,
tabBarHeight,
filterBarHeight,
bottomSafeArea,
scrollViewHeight: this.scrollViewHeight
})
},
// 切换Tab
handleTabChange(key) {
this.activeTab = key
if (key === 'label') {
this.fetchList()
}
},
// 数据字段映射 - 将后端字段映射到前端使用的字段
// 后端 WmsMaterialCoilVo 字段coilId, enterCoilNo, currentCoilNo,
// itemName, specification, material, manufacturer,
// actualWarehouseName, warehouseName, remark, transferType
mapDataFields(row) {
return {
// 钢卷ID
coilId: row.coilId,
// 钢卷号
enterCoilNo: row.enterCoilNo || '-',
currentCoilNo: row.currentCoilNo || '-',
// 产品信息(注意后端使用 specification/material/manufacturer非 item 前缀)
itemName: row.itemName || '-',
itemSpecification: row.specification || '-',
itemMaterial: row.material || '-',
itemManufacturer: row.manufacturer || '-',
// 库区信息
actualWarehouseName: row.actualWarehouseName || row.warehouseName || '-',
// 其他字段
remark: row.remark || '-',
transferType: row.transferType || '',
// 改判原因(后端 WmsMaterialCoilVo 无此字段,仅通过 listWithRejudge 接口传入)
changeReason: row.changeReason || ''
}
},
// 获取列表数据
async fetchList(isLoadMore = false) {
if (this.loading) return
this.loading = true
try {
console.log('开始获取列表,查询参数:', this.query)
const res = await listMaterialCoil(this.query)
console.log('获取列表响应:', res)
const rows = res.rows || []
this.total = res.total || 0
// 映射数据字段
const mappedRows = rows.map(row => this.mapDataFields(row))
console.log('映射后的数据:', { mappedRows, total: this.total })
if (isLoadMore) {
this.list = [...this.list, ...mappedRows]
} else {
this.list = mappedRows
}
// 更新待贴标签数量
const labelTab = this.tabs.find(t => t.key === 'label')
if (labelTab) {
labelTab.badge = this.total
}
} catch (error) {
console.error('获取列表失败:', error)
uni.showToast({
title: '获取数据失败: ' + (error.message || '未知错误'),
icon: 'none'
})
} finally {
this.loading = false
this.refreshing = false
}
},
// 搜索
handleSearch(form) {
this.query = {
...this.query,
...form,
pageNum: 1
}
this.fetchList()
},
// 重置
handleReset() {
this.query = {
pageNum: 1,
pageSize: 10,
enterCoilNo: '',
currentCoilNo: '',
itemName: '',
itemSpecification: '',
itemMaterial: '',
itemManufacturer: '',
hasTransferType: true
}
this.fetchList()
},
// 下拉刷新
onRefresh() {
this.refreshing = true
this.query.pageNum = 1
this.fetchList()
},
// 加载更多
onLoadMore() {
console.log('触发加载更多', {
listLength: this.list.length,
total: this.total,
loading: this.loading,
pageNum: this.query.pageNum
})
// 检查是否已加载全部数据
if (this.list.length >= this.total) {
console.log('加载更多被阻止:已加载全部数据')
uni.showToast({
title: '已加载全部数据',
icon: 'none',
duration: 1500
})
return
}
// 检查是否正在加载中
if (this.loading) {
console.log('加载更多被阻止:正在加载中')
return
}
// 防抖保护:防止重复点击
if (this.loadMoreTimer) {
console.log('加载更多被阻止:防抖保护中')
return
}
// 设置防抖定时器
this.loadMoreTimer = setTimeout(() => {
this.loadMoreTimer = null
}, 800)
// 执行加载
this.query.pageNum++
console.log('加载第', this.query.pageNum, '页')
this.fetchList(true)
},
// 重贴标签
handleRelabel(coilInfo) {
this.$refs.relabelPopup.open(coilInfo)
},
// 重贴标签成功回调
handleRelabelSuccess() {
this.fetchList()
},
// 查看记录
handleViewRecord(coilInfo) {
const coilId = coilInfo.coilId
if (!coilId) {
uni.showToast({
title: '无法获取钢卷ID',
icon: 'none'
})
return
}
this.$refs.recordPopup.open(coilId)
},
// 查看详情
handleViewDetail(coilInfo) {
const coilId = coilInfo.coilId
if (!coilId) {
uni.showToast({
title: '无法获取钢卷ID',
icon: 'none'
})
return
}
uni.navigateTo({
url: `/pages/todo/coil-detail?coilId=${coilId}`
})
}
}
}
</script>
<style scoped lang="scss">
.todo-container {
min-height: 100vh;
background: #f5f7fa;
display: flex;
flex-direction: column;
.custom-nav-bar {
background: #ffffff;
border-bottom: 1rpx solid #f0f0f0;
.nav-content {
height: 88rpx;
display: flex;
align-items: center;
justify-content: center;
.nav-title {
font-size: 34rpx;
font-weight: 600;
color: #333333;
}
}
}
.tab-bar {
display: flex;
background: #ffffff;
padding: 0 10rpx;
border-bottom: 1rpx solid #f0f0f0;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
scrollbar-width: none;
&::-webkit-scrollbar {
display: none;
}
.tab-item {
flex: 0 0 auto;
min-width: 140rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 20rpx 16rpx;
position: relative;
.tab-text {
font-size: 26rpx;
color: #666666;
white-space: nowrap;
line-height: 1.2;
}
.tab-badge {
position: absolute;
top: 8rpx;
right: 8rpx;
min-width: 32rpx;
height: 32rpx;
line-height: 32rpx;
text-align: center;
background: #ff4d4f;
color: #ffffff;
font-size: 20rpx;
border-radius: 16rpx;
padding: 0 8rpx;
}
&.active {
.tab-text {
color: #1a73e8;
font-weight: 600;
}
&::after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 40rpx;
height: 4rpx;
background: #1a73e8;
border-radius: 2rpx;
}
}
}
}
.list-container {
padding: 20rpx;
box-sizing: border-box;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
.coil-list {
padding-bottom: 20rpx;
}
.load-more-wrapper {
padding: 16rpx 0;
display: flex;
justify-content: center;
align-items: center;
&:active {
opacity: 0.7;
}
.load-more-btn {
display: flex;
flex-direction: column;
align-items: center;
padding: 12rpx 32rpx;
background: #ffffff;
border-radius: 8rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.08);
.btn-text {
font-size: 26rpx;
color: #1a73e8;
font-weight: 500;
}
.btn-hint {
font-size: 20rpx;
color: #999999;
margin-top: 4rpx;
}
}
.load-more-loading {
display: flex;
align-items: center;
padding: 12rpx 32rpx;
.loading-text {
font-size: 26rpx;
color: #999999;
}
}
&.no-more {
padding: 16rpx 0;
.no-more-text {
font-size: 22rpx;
color: #bbbbbb;
}
}
}
.placeholder-page {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 200rpx 0;
.placeholder-icon {
font-size: 120rpx;
margin-bottom: 30rpx;
}
.placeholder-text {
font-size: 32rpx;
color: #999999;
}
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 200rpx 0;
.empty-icon {
font-size: 120rpx;
margin-bottom: 30rpx;
}
.empty-text {
font-size: 32rpx;
color: #999999;
}
}
.loading-state {
padding: 100rpx 0;
}
}
}
/* 浏览器环境适配 */
@media screen and (min-width: 768px) {
.todo-container {
max-width: 750rpx;
margin: 0 auto;
.tab-bar {
.tab-item {
min-width: 160rpx;
.tab-text {
font-size: 28rpx;
}
}
}
}
}
/* H5 浏览器特定样式 */
/* #ifdef H5 */
.todo-container {
.tab-bar {
.tab-item {
cursor: pointer;
&:hover {
background: rgba(26, 115, 232, 0.05);
}
}
}
.coil-card {
cursor: pointer;
&:hover {
box-shadow: 0 6rpx 20rpx rgba(0, 0, 0, 0.1);
}
}
}
/* #endif */
</style>