Files
hhrs/pages/winners/winners.vue
T
jacobxu666andClaude Haiku 4.5 f2c8364d87 feat: 中奖名单页动态人数、分页提示与空态
- 顶部幸运持卡人数由各期中奖总数动态求和
- 名单不足一页时隐藏加载更多,各期加载状态独立管理
- 无名单时展示暂无数据空态

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-08-27 15:17:08 +08:00

456 lines
10 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="page">
<!-- 顶部公告区 -->
<view class="banner-area">
<!-- 返回按钮 -->
<view class="back-btn" @click="goBack">
<u-icon name="arrow-left" color="#333333" size="40"></u-icon>
<text class="back-text">返回</text>
</view>
<view class="banner-header">
<image class="banner-icon" src="https://zhihuiyou.oss-cn-shenzhen.aliyuncs.com/hhrs/hhrs_zjgs.png" mode="aspectFit"></image>
<text class="banner-title">中奖名单公示</text>
</view>
<text class="banner-subtitle">总计 10,000份权益包,分三期公证抽取</text>
<view class="stats-card">
<image class="stats-gift" src="https://zhihuiyou.oss-cn-shenzhen.aliyuncs.com/hhrs/hhrs_xyckr.png" mode="aspectFit"></image>
<text class="stats-label">已产生</text>
<text class="stats-number">{{ totalWinners.toLocaleString() }}</text>
<text class="stats-label">名幸运持卡人</text>
</view>
</view>
<!-- 三期 Tab -->
<view class="tab-bar">
<view
class="tab-item"
:class="{ active: currentTab === index }"
v-for="(tab, index) in tabs"
:key="index"
@click="switchTab(index)"
>
<text class="tab-label">{{ tab.name }}</text>
<text class="tab-date">{{ tab.date }}</text>
<view class="tab-indicator" v-if="currentTab === index"></view>
</view>
</view>
<!-- 搜索栏 -->
<view class="search-area">
<view class="search-bar">
<u-icon name="search" color="#8e8e93" size="36"></u-icon>
<input class="search-input" v-model="keyword" placeholder="搜索姓名/手机号" confirm-type="search" @confirm="onSearch" />
<view class="search-btn" @click="onSearch">搜索</view>
</view>
</view>
<!-- 中奖者表格 -->
<view class="table-area">
<template v-if="currentList.length > 0">
<view class="table-header">
<text class="table-col col-name">姓名</text>
<text class="table-col col-idcard">身份证</text>
<text class="table-col col-phone">手机号码</text>
</view>
<view class="table-row" v-for="(item, index) in currentList" :key="index">
<text class="table-col col-name">{{ item.memberName }}</text>
<text class="table-col col-idcard">{{ item.memberIdentityNo }}</text>
<text class="table-col col-phone">{{ item.memberPhone }}</text>
</view>
<!-- xuchao: 名单不足一页时隐藏加载更多 -->
<view class="loadmore-area" v-if="(totals[currentTab] || 0) > pageSize">
<u-loadmore :status="loadStatus" :loading-text="'加载中...'" :loadmore-text="'上拉加载更多'" :nomore-text="'没有更多了'" />
</view>
</template>
<!-- xuchao: 无名单时展示空态 -->
<view class="empty-area" v-else>
<u-icon name="empty" size="60" color="#c8c8c8"></u-icon>
<text class="empty-text">暂无数据</text>
</view>
</view>
</view>
</template>
<script>
export default {
data() {
return {
currentTab: 0,
tabs: [],
winnerData: {},
pageNo: 1,
pageSize: 10,
totals: {},
totalWinners: 0,
inFlight: 0,
loadStatusMap: {},
keyword: ''
}
},
computed: {
currentList() {
const list = this.winnerData[this.currentTab] || []
if (!this.keyword) return list
const kw = this.keyword.trim().toLowerCase()
if (!kw) return list
return list.filter(item =>
(item.memberName && item.memberName.toLowerCase().includes(kw)) ||
(item.memberPhone && item.memberPhone.includes(kw))
)
},
// xuchao: 各期独立的加载状态,避免并行请求互相覆盖
loadStatus() {
return this.loadStatusMap[this.currentTab] || 'nomore'
}
},
onLoad() {
this.loadDates()
},
onReachBottom() {
this.loadMore()
},
methods: {
loadDates() {
this.$u.api.getWinnerDates().then(res => {
if (res.errcode === 0 && res.data) {
const dates = res.data
this.tabs = dates.map((date, index) => ({
name: `第${index + 1}期`,
date: date
}))
const data = {}
dates.forEach((date, index) => {
data[index] = []
})
this.winnerData = data
// xuchao: 预置各期 key,保证 totals/loadStatusMap 的响应式
this.totals = {}
this.loadStatusMap = {}
dates.forEach((date, index) => {
this.$set(this.totals, index, 0)
this.$set(this.loadStatusMap, index, 'loading')
})
// xuchao: 预取各期第一页,汇总已产生总人数
if (this.tabs.length > 0) {
this.tabs.forEach((tab, index) => {
this.loadWinnerList(index, 1, true)
})
}
}
})
},
loadWinnerList(tabIndex, pageNo, allowConcurrent) {
if (!this.tabs[tabIndex]) return
if (this.inFlight > 0 && !allowConcurrent) return
this.inFlight++
this.$set(this.loadStatusMap, tabIndex, 'loading')
const drawDate = this.tabs[tabIndex].date
this.$u.api.getWinnerList({
pageNo: pageNo || this.pageNo,
pageSize: this.pageSize,
drawDate: drawDate
}).then(res => {
if (res.errcode === 0 && res.data) {
const list = res.data.list || []
const page = pageNo || this.pageNo
if (page === 1) {
this.$set(this.winnerData, tabIndex, list)
} else {
this.winnerData[tabIndex] = this.winnerData[tabIndex].concat(list)
}
this.$set(this.totals, tabIndex, res.data.total || 0)
// xuchao: 各期总人数求和,用于顶部幸运持卡人数
this.totalWinners = Object.values(this.totals).reduce((sum, n) => sum + n, 0)
}
}).finally(() => {
this.inFlight--
const len = this.winnerData[tabIndex].length
const total = this.totals[tabIndex] || 0
this.$set(this.loadStatusMap, tabIndex, len === 0 || len >= total ? 'nomore' : 'loadmore')
})
},
loadMore() {
const total = this.totals[this.currentTab] || 0
if (this.winnerData[this.currentTab].length < total && this.inFlight === 0) {
this.pageNo++
this.loadWinnerList(this.currentTab)
}
},
switchTab(index) {
this.currentTab = index
this.pageNo = 1
if (this.winnerData[index].length === 0) {
this.loadWinnerList(index, 1)
} else {
const total = this.totals[index] || 0
this.$set(this.loadStatusMap, index, this.winnerData[index].length >= total ? 'nomore' : 'loadmore')
}
},
goBack() {
// xuchao: 直接通过链接进入(无历史栈)时回到首页
const pages = getCurrentPages()
if (pages.length > 1) {
uni.navigateBack()
} else {
uni.reLaunch({ url: '/pages/index/index' })
}
},
onSearch() {
// 前端过滤,无需重新请求
}
}
}
</script>
<style>
page {
background-color: #f2f2f7;
}
</style>
<style scoped lang="less">
/* 返回按钮 */
.back-btn {
align-self: flex-start;
display: flex;
align-items: center;
gap: 2px;
margin-top: 48px;
}
.back-text {
font-size: 14px;
color: #333333;
}
/* 顶部公告区 */
.banner-area {
background-color: #ffffff;
padding: 0 24px 24px;
margin-top: -24px;
display: flex;
flex-direction: column;
align-items: center;
gap: 17px;
}
.banner-header {
display: flex;
align-items: center;
gap: 4px;
margin-top: -12px;
}
.banner-icon {
width: 50px;
height: 50px;
}
.banner-title {
color: #0D387D;
font-family: 'Source Han Serif SC VF', 'Source Han Serif SC', serif;
font-weight: bold;
font-size: 32px;
line-height: normal;
letter-spacing: 0px;
text-align: left;
}
.banner-subtitle {
color: #8E8E93;
font-family: 'Source Han Serif SC VF', 'Source Han Serif SC', serif;
font-weight: 600;
font-size: 14px;
line-height: normal;
letter-spacing: 0px;
text-align: center;
}
.stats-card {
width: 100%;
display: flex;
align-items: center;
gap: 12px;
padding: 12px;
border-radius: 10px;
background: linear-gradient(-135deg, #4981D2 0%, #1853AF 100%);
}
.stats-gift {
width: 36px;
height: 36px;
}
.stats-label {
color: #FFFFFF;
font-family: 'Source Han Sans CN', 'Source Han Sans SC', sans-serif;
font-weight: 400;
font-size: 14px;
line-height: normal;
letter-spacing: 0px;
text-align: left;
}
.stats-number {
flex: 1;
font-family: 'Source Han Serif SC VF', 'Source Han Serif SC', serif;
font-weight: 600;
font-size: 36px;
line-height: normal;
letter-spacing: 0px;
text-align: center;
color: #fce7b0;
}
/* 三期 Tab */
.tab-bar {
display: flex;
background-color: #ffffff;
padding-top: 12px;
justify-content: space-around;
position: sticky;
top: 0;
z-index: 5;
border-bottom: 1px solid #f2f2f7;
}
.tab-item {
display: flex;
flex-direction: column;
align-items: center;
width: 88px;
position: relative;
padding-bottom: 12px;
}
.tab-label {
color: #B1B1B1;
font-family: 'Source Han Serif SC VF', 'Source Han Serif SC', serif;
font-weight: 600;
font-size: 16px;
line-height: 22px;
letter-spacing: 0px;
text-align: center;
}
.tab-date {
font-size: 14px;
font-weight: 600;
color: #B1B1B1;
line-height: 22px;
}
.tab-item.active .tab-label,
.tab-item.active .tab-date {
color: #3d85ff;
}
.tab-indicator {
position: absolute;
bottom: 0;
width: 24px;
height: 4px;
background-color: #3d85ff;
border-radius: 100px;
}
/* 搜索栏 */
.search-area {
background-color: #ffffff;
padding: 10px 12px 12px;
}
.search-bar {
display: flex;
align-items: center;
height: 44px;
background-color: #ffffff;
border: 1px solid #f2f2f7;
border-radius: 100px;
padding: 4px 8px;
gap: 12px;
}
.search-input {
flex: 1;
font-size: 16px;
color: #333;
}
.search-btn {
background-color: #0088ff;
color: #ffffff;
font-size: 14px;
padding: 4px 10px;
border-radius: 50px;
white-space: nowrap;
}
/* 中奖者表格 */
.table-area {
background-color: #ffffff;
width: 100%;
margin: 12px 0;
padding: 0 12px;
box-sizing: border-box;
}
.table-header,
.table-row {
display: flex;
align-items: center;
padding: 12px 0;
border-bottom: 1px solid #f2f2f7;
}
.table-header {
color: #8e8e93;
font-size: 14px;
}
.table-row {
color: #000000;
font-size: 14px;
height: 64px;
}
.table-col {
flex: 1;
text-align: center;
}
.col-name {
width: 80px;
flex: none;
text-align: center;
}
.col-idcard {
flex: 1;
text-align: center;
}
.col-phone {
flex: 1;
text-align: center;
}
/* 加载更多 */
.loadmore-area {
padding: 24px 0;
color: #999999;
}
/* 空态 */
.empty-area {
display: flex;
flex-direction: column;
align-items: center;
padding: 80px 0;
color: #999999;
}
.empty-text {
margin-top: 12px;
font-size: 14px;
}
</style>