Files
hhrs/pages/winners/winners.vue
T
jacobxu666 745f4bf5f1 fix: 修复登录失败被当成功、列表假分页与首页裁切等隐患
1. 登录/验证码改判业务码 data.code(外层 errcode 恒为0),失败不再写入 phone 造成假登录态,并透出后端真实原因;倒计时改为发送成功后才启动;token 按 accessToken 兜底读取,不再写入 undefined

2. 权益包三 Tab 由 pageSize=1000 的假分页改为真实分页 + 页面级触底加载,各 Tab 独立游标与请求序号,合并去重兜底;中奖名单页码改为按期维护,修复切期后重复追加

3. 首页去掉整页 overflow:hidden,小屏不再裁切活动指南卡片;接口表去重并清理 7 个废弃端点;基址/客服电话集中到 common/config.js;删除权益详情不可达的假成功分支
2026-09-14 23:38:13 +08:00

458 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://aliyunoss.hn12301.net/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://aliyunoss.hn12301.net/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: {},
// xuchao: 页码改为按期维护(原单一 pageNo 在切期后会被沿用,导致同一页重复追加/跳页)
pageNos: {},
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/pageNos/loadStatusMap 的响应式
this.totals = {}
this.pageNos = {}
this.loadStatusMap = {}
dates.forEach((date, index) => {
this.$set(this.totals, index, 0)
this.$set(this.pageNos, index, 1)
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
// xuchao: 本期页码独立计算,缺省续拉该期下一页
const page = pageNo || (this.pageNos[tabIndex] || 1)
this.inFlight++
this.$set(this.loadStatusMap, tabIndex, 'loading')
const drawDate = this.tabs[tabIndex].date
this.$u.api.getWinnerList({
pageNo: page,
pageSize: this.pageSize,
drawDate: drawDate
}).then(res => {
if (res.errcode === 0 && res.data) {
const list = res.data.list || []
const prev = this.winnerData[tabIndex] || []
this.$set(this.pageNos, tabIndex, page)
if (page === 1) {
this.$set(this.winnerData, tabIndex, list)
} else {
this.$set(this.winnerData, tabIndex, prev.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 tab = this.currentTab
const total = this.totals[tab] || 0
const len = (this.winnerData[tab] || []).length
if (len < total && this.inFlight === 0) {
this.loadWinnerList(tab, (this.pageNos[tab] || 1) + 1)
}
},
switchTab(index) {
this.currentTab = index
// xuchao: 不再重置全局 pageNo(各期游标自带);空列表补拉首页时允许并发,
// 避免首屏预取仍在途时切期被丢弃后一直显示“暂无数据”
if ((this.winnerData[index] || []).length === 0) {
this.loadWinnerList(index, 1, true)
} 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-weight: 700;
font-size: 32px;
line-height: normal;
letter-spacing: 0px;
text-align: left;
}
.banner-subtitle {
color: #8E8E93;
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-size: 14px;
line-height: normal;
letter-spacing: 0px;
text-align: left;
}
.stats-number {
flex: 1;
font-weight: 700;
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-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>