Files
hhrs/pages/rights/rights.vue
T
jacobxu666andClaude Fable 5 eeb6b91e0a feat: 场馆/折扣/权益待使用列表新增「所属县区 + 所属类型」筛选条
- 新增 components/hhrs-filter-bar:两个等宽入口平分并占满容器宽、无底色、蓝字选中态;
  面板首项「全部 X」即清空该维度(因此没有独立重置按钮)
- 三页接入;选项与各选项命中条数由列表接口在响应里一并返回(后端已按关键词与另一维度算好)
- 筛选与关键词为 AND;切筛选回第 1 页,复用各页现有 reqSeq 竞态保护
- 某维度无可选项时(如商铺未录入所在地区)该入口自动不渲染,避免点开空面板
- 三页头部间距统一为「搜索栏↔筛选条 = 筛选条↔列表 = 10px」

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-28 15:12:38 +08:00

1049 lines
29 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" :class="{ 'page-auth': status === 'auth' }">
<!-- xuchao: 顶部导航(返回+页面名称,认证状态使用) -->
<view class="nav-bar" v-if="status === 'auth'">
<view class="nav-back" @click="goBack">
<u-icon name="arrow-left" color="#1a1818" size="40"></u-icon>
</view>
<text class="nav-title">权益资格认证</text>
</view>
<!-- 返回按钮(顶部,非auth状态) -->
<view class="back-btn" :class="{ 'back-btn--black': status === 'noWin' }" @click="goBack" v-if="status !== 'auth'">
<u-icon name="arrow-left" :color="status === 'noWin' ? '#000000' : '#ffffff'" size="40"></u-icon>
<text class="back-text">返回</text>
</view>
<!-- 状态1: 权益资格认证 -->
<view v-if="status === 'auth'" class="auth-container">
<view class="section-title">
<view class="title-bar"></view>
<text class="title-text">权益资格认证</text>
</view>
<view class="form-area">
<view class="form-item">
<text class="form-label required">真实姓名</text>
<input class="form-input" v-model="form.realName" placeholder="请输入真实姓名" />
</view>
<view class="form-item">
<text class="form-label required">手机号码</text>
<text class="form-value phone-value">{{ form.phone }}</text>
</view>
<view class="form-item" @click="showIdTypePicker = true">
<text class="form-label required">证件类型</text>
<text class="form-value">{{ form.idType }}</text>
<text class="form-arrow">&#xe602;</text>
</view>
<view class="form-item">
<text class="form-label required">证件号码</text>
<input class="form-input" v-model="form.idNumber" placeholder="请输入真实证件号码" />
</view>
</view>
<view class="auth-btn" @click="onAuth">开始认证</view>
<view class="tips-section">
<view class="section-title">
<view class="title-bar"></view>
<text class="title-text">温馨提示</text>
</view>
<view class="tips-list">
<text class="tip-item">1. 请输入真实的身份信息用来进行活动权益资格认证</text>
<text class="tip-item">2. 认证通过后系统会显示出您的所获得的权益信息</text>
<text class="tip-item">3. 手机号码为您登录的手机号,无法修改。若号码不对,请更换手机号重新登录。</text>
</view>
</view>
</view>
<!-- 状态2: 未中奖 -->
<view v-if="status === 'noWin'" class="empty-container">
<image class="empty-img" src="https://aliyunoss.hn12301.net/hhrs/hhrs_wzj.png" mode="widthFix" />
<text class="empty-text">您暂未中奖</text>
</view>
<!-- 状态3: 权益信息 -->
<view v-if="status === 'info'">
<!-- 蓝色头部横幅 -->
<view style="background: #fff;">
<view class="banner-area">
<text class="banner-title">恭喜您!获得怀化市社保卡权益包</text>
<view class="banner-expire" v-if="expiresTime">
<text class="expire-icon">!</text>
<text class="expire-text">{{ expiresTime }}</text>
<text class="expire-label">到期</text>
</view>
</view>
<!-- 权益统计卡片 -->
<view class="stats-card">
<view class="stats-item">
<text class="stats-label">总权益价值</text>
<view class="stats-value-row">
<text class="stats-currency">¥</text>
<text class="stats-number">{{ totalValueText }}</text>
<text class="stats-unit">元</text>
</view>
</view>
<view class="stats-divider"></view>
<view class="stats-item">
<text class="stats-label">可用权益数</text>
<view class="stats-value-row">
<text class="stats-number-large">{{ rightsCount }}</text>
<text class="stats-unit">个</text>
</view>
</view>
</view>
</view>
<!-- xuchao: Tab + 搜索 + 县区/类型筛选一起吸顶;筛选只作用于「待使用」(另两个 Tab 的接口不返回地址与分类) -->
<view class="info-sticky">
<!-- Tab 切换 -->
<view class="tab-bar">
<view v-for="(tab, index) in tabs" :key="index" class="tab-item"
:class="{ active: currentTab === index }" @click="switchTab(index)">
<text class="tab-text">{{ tab }}</text>
</view>
</view>
<!-- 搜索栏 -->
<view class="search-area" v-if="currentTab==0">
<view class="search-bar">
<u-icon name="search" color="#cccccc" size="24"></u-icon>
<input class="search-input" v-model="keyword" placeholder="搜索权益商家" @confirm="onSearch" />
</view>
<!-- xuchao: 所属县区 + 所属类型筛选,选项与联动条数由列表接口一并返回 -->
<hhrs-filter-bar :districts="filters.districts" :categories="filters.categories"
:district-id="districtId" :category="category" @change="onFilterChange" />
</view>
</view>
<!-- 权益列表 -->
<!-- xuchao: 三 Tab 列表统一改普通容器 + 页面级触底分页(scroll-view 无固定高度时 scrolltolower 永不触发) -->
<view class="list-area" v-if="currentTab==0">
<view class="rights-card" v-for="item in rightsList" :key="item.ruleId" @click="goDetail(item)">
<image class="card-img" :src="item.image" mode="aspectFill" />
<view class="card-info">
<view class="card-name">{{ item.name }}</view>
<view class="card-category">{{ item.category }}</view>
<view class="card-address">{{ item.address }}</view>
</view>
<view class="card-distance" v-if="item.distance !== null">
<u-icon name="map" color="#999999" size="24"></u-icon>
<text
class="distance-text">{{ item.distance < 1 ? Math.round(item.distance * 1000) + 'm' : Math.round(item.distance) + 'km' }}</text>
</view>
</view>
<view class="empty-state" v-if="rightsList.length === 0 && !loading">
<image class="empty-state-img" src="https://aliyunoss.hn12301.net/hhrs/hhrs_wzj.png" mode="widthFix"></image>
<text class="empty-state-text">暂无内容</text>
</view>
<view v-if="pager[0].hasScrolled" class="nomore-area">
<u-loadmore :status="loadStatuses[0]" :load-text="{ loadmore: '', loading: '加载中...', nomore: '我也是有底线的' }" />
</view>
</view>
<view class="list-area" v-if="currentTab==1">
<view class="rights-card" style="margin-top: 24rpx;flex-direction: column;" v-for="item in appointmentList"
:key="item.id" >
<view style="width: 654rpx;display: flex;justify-content: space-between;">
<text>{{ item.resourceName }}</text><text style="color: #34C759;">已预约</text></view>
<!-- xuchao: appTime 为空时不再抛错拖垮整页渲染 -->
<view style="font-size: 24rpx;color: #727272;margin-top: 24rpx;">预约游玩日期:{{ (item.appTime || '').split(' ')[0] }}
<text v-if="item.timeName">({{ item.timeName }})</text></view>
</view>
<view class="empty-state" v-if="appointmentList.length === 0 && !appointmentLoading">
<image class="empty-state-img" src="https://aliyunoss.hn12301.net/hhrs/hhrs_wzj.png" mode="widthFix"></image>
<text class="empty-state-text">暂无内容</text>
</view>
<view v-if="pager[1].hasScrolled" class="nomore-area">
<u-loadmore :status="loadStatuses[1]" :load-text="{ loadmore: '', loading: '加载中...', nomore: '我也是有底线的' }" />
</view>
</view>
<view class="list-area" v-if="currentTab==2">
<view class="rights-card" style="margin-top: 24rpx;flex-direction: column;" v-for="item in checkList"
:key="item.id" >
<view style="width: 654rpx;display: flex;justify-content: space-between;">
<text>{{ item.resourceName }}</text><text style="color: #FF383C;">已使用</text></view>
<view style="font-size: 24rpx;color: #727272;margin-top: 24rpx;">核销时间:{{ item.checkTime }}</view>
</view>
<view class="empty-state" v-if="checkList.length === 0 && !checkLoading">
<image class="empty-state-img" src="https://aliyunoss.hn12301.net/hhrs/hhrs_wzj.png" mode="widthFix"></image>
<text class="empty-state-text">暂无内容</text>
</view>
<!-- xuchao: 原先自写的“加载中/没有更多”与另两个 Tab 不一致,统一换成 u-loadmore -->
<view v-if="pager[2].hasScrolled" class="nomore-area">
<u-loadmore :status="loadStatuses[2]" :load-text="{ loadmore: '', loading: '加载中...', nomore: '我也是有底线的' }" />
</view>
</view>
</view>
</view>
</template>
<script>
// xuchao: 县区 + 类型筛选条,三个列表页共用(pages.json 的 easycom 只给 u- 前缀配了规则,这里显式引入)
import HhrsFilterBar from '@/components/hhrs-filter-bar/hhrs-filter-bar.vue'
export default {
components: { HhrsFilterBar },
data() {
return {
status: 'auth',
// xuchao: 权益概览三字段,首屏用缓存兜底,随后由服务端复查刷新
expiresTime: uni.getStorageSync('expiresTime') || '',
rightsCount: uni.getStorageSync('rightsCount') || 0,
totalValueFen: uni.getStorageSync('totalValueFen') || 0,
form: {
realName: '',
phone: '',
idType: '居民身份证',
idNumber: ''
},
showIdTypePicker: false,
tabs: ['待使用', '已预约', '已使用'],
currentTab: 0,
keyword: '',
// xuchao: 待使用列表的县区/类型筛选条件与面板选项(选项由列表响应带回)
districtId: '',
category: '',
filters: { districts: [], categories: [] },
// xuchao: 待使用列表的县区/类型筛选条件与面板选项(选项由列表响应带回)
districtId: '',
category: '',
filters: { districts: [], categories: [] },
rightsList: [],
appointmentList: [],
checkList: [],
// xuchao: 三个 Tab 各自独立的分页游标/加载态。原 pageSize=1000 是“一次拉全量 +
// scroll-view 永不触发的假分页”,现改真实分页(与场馆/折扣页口径一致)
pageSize: 10,
pager: [
{ pageNo: 1, total: 0, loading: false, reqSeq: 0, hasScrolled: false },
{ pageNo: 1, total: 0, loading: false, reqSeq: 0, hasScrolled: false },
{ pageNo: 1, total: 0, loading: false, reqSeq: 0, hasScrolled: false }
],
lng: '',
lat: ''
}
},
computed: {
// xuchao: 总权益价值分转元,整除去小数,否则保留两位
totalValueText() {
const yuan = this.totalValueFen / 100
return Number.isInteger(yuan) ? String(yuan) : yuan.toFixed(2)
},
// xuchao: 模板沿用原有 loading / appointmentLoading / checkLoading 字段名
loading() {
return this.pager[0].loading
},
appointmentLoading() {
return this.pager[1].loading
},
checkLoading() {
return this.pager[2].loading
},
// xuchao: 各 Tab 底部“加载中/没有更多”独立计算,避免并行请求互相覆盖
loadStatuses() {
return this.pager.map((p, i) => {
if (p.loading) return 'loading'
const len = this.listOf(i).length
return (len === 0 || len >= p.total) ? 'nomore' : 'loadmore'
})
}
},
onLoad(options) {
if (options.status) {
this.status = options.status
}
this.form.phone = uni.getStorageSync('phone') || ''
const loc = uni.getStorageSync('lng') && uni.getStorageSync('lat') ? {
lng: uni.getStorageSync('lng'),
lat: uni.getStorageSync('lat')
} :
null
if (loc) {
this.lng = loc.lng
this.lat = loc.lat
}
this.checkAuthResult()
// xuchao: 权益状态会变化,缓存只作首屏展示,进入页面一律向服务端复查最新状态
this.refreshStatus()
if (this.status === 'info') {
this.loadRightsList()
}
},
// xuchao: 页面级触底加载 —— 只作用于当前 Tab
onReachBottom() {
if (this.status !== 'info') return
const tab = this.currentTab
const p = this.pager[tab]
p.hasScrolled = true
if (!p.loading && this.listOf(tab).length < p.total) {
p.pageNo++
this.loadList(tab)
}
},
methods: {
// xuchao: 缓存的权益状态可能过期(中奖/权益包会变化),每次进页面仅凭手机号向服务端
// 复查最新状态并覆盖缓存(不带证件号,避免触发被动认证)
refreshStatus() {
// xuchao: 人脸认证回调场景由 checkAuthResult 负责查询,跳过避免重复请求
try {
if (new URL(window.location.href).searchParams.get('response')) return
} catch (e) {
return
}
// xuchao: 用户主动发起过认证流程时,取消后应停留在认证页,不自动恢复
if (this.status === 'auth' && uni.getStorageSync('authFlowStarted')) return
const phone = uni.getStorageSync('phone')
if (!phone) return
this.$u.api.getEligibility({
phone: phone
}).then(res => {
if (res.errcode !== 0 || !res.data) return
this.applyEligibility(res.data)
// xuchao: 服务端已认证则清除取消标记,后续按真实状态展示
if (res.data.isAuth) uni.removeStorageSync('authFlowStarted')
const newStatus = res.data.isAuth ? (res.data.hasWin ? 'info' : 'noWin') : 'auth'
const prevStatus = this.status
this.status = newStatus
// xuchao: 状态刷新为权益页时补拉权益列表,避免与首屏加载重复
if (newStatus === 'info' && prevStatus !== 'info') {
this.loadRightsList()
}
})
},
// xuchao: 资格查询结果统一写入缓存并回填页面(到期时间/总权益价值/可用权益数)
applyEligibility(data) {
uni.setStorageSync('isAuth', data.isAuth)
uni.setStorageSync('hasWin', data.hasWin)
uni.setStorageSync('cardNumber', data.cardNumber)
uni.setStorageSync('expiresTime', data.expiresTime)
uni.setStorageSync('rightsCount', data.rightsCount || 0)
uni.setStorageSync('totalValueFen', data.totalValueFen || 0)
this.expiresTime = data.expiresTime || ''
this.rightsCount = data.rightsCount || 0
this.totalValueFen = data.totalValueFen || 0
},
// xuchao: 认证取消/未通过:清理旧认证缓存并标记流程已发起,取消后应停留认证页
markAuthCanceled() {
uni.removeStorageSync('isAuth')
uni.removeStorageSync('hasWin')
uni.removeStorageSync('cardNumber')
uni.removeStorageSync('expiresTime')
uni.removeStorageSync('rightsCount')
uni.removeStorageSync('totalValueFen')
uni.setStorageSync('authFlowStarted', true)
},
checkAuthResult() {
const url = new URL(window.location.href)
let responseStr = url.searchParams.get('response')
console.log('responseStr:', responseStr)
if (!responseStr) return
let response
try {
let parms = JSON.parse(decodeURIComponent(responseStr))
console.log('parms:', parms)
response = typeof parms === 'string' ? JSON.parse(parms) : parms
console.log('response:', response)
} catch (e) {
try {
response = JSON.parse(responseStr)
} catch (e2) {
return
}
}
const code = response.code
const certifyId = response.extInfo ? response.extInfo.certifyId : ''
console.log('code:', code, 'certifyId:', certifyId)
if (code == 1000 && certifyId) {
this.$u.api.describeFaceVerify({
certifyId: certifyId
}).then(res => {
console.log('describeFaceVerify res:', res)
if (res.errcode === 0 && res.data.body && res.data.body.resultObject) {
const verifyResult = res.data.body.resultObject
console.log('verifyResult:', verifyResult)
if (String(verifyResult.passed) === 'T' || String(verifyResult.Passed) === 'T') {
const phone = uni.getStorageSync('phone') || ''
const name = uni.getStorageSync('authName') || ''
const identityNo = uni.getStorageSync('authIdNo') || ''
this.$u.api.getEligibility({
phone: phone,
identityNo: identityNo,
name: name
}).then(eRes => {
console.log('eligibility res:', eRes)
if (eRes.errcode === 0) {
this.applyEligibility(eRes.data)
// xuchao: 认证成功,清除流程标记
uni.removeStorageSync('authFlowStarted')
if (eRes.data.isAuth) {
this.status = eRes.data.hasWin ? 'info' : 'noWin'
if (this.status === 'info') {
this.loadRightsList()
}
}
}
})
} else {
// xuchao: 实名未通过视为取消认证,清理缓存并标记
this.markAuthCanceled()
uni.showToast({
title: '认证未通过',
icon: 'none'
})
}
} else {
uni.showToast({
title: '认证结果查询失败',
icon: 'none'
})
}
}).catch(() => {
uni.showToast({
title: '认证结果查询失败',
icon: 'none'
})
})
} else {
// xuchao: 未返回认证回调视为取消认证,清理缓存并标记
this.markAuthCanceled()
uni.showToast({
title: '认证未通过',
icon: 'none'
})
}
},
loadAliyunSDK() {
return new Promise((resolve, reject) => {
if (window.getMetaInfo) {
resolve()
return
}
const script = document.createElement('script')
script.src = 'https://o.alicdn.com/yd-cloudauth/cloudauth-cdn/jsvm_all.js'
script.onload = () => resolve()
script.onerror = () => reject(new Error('加载阿里云SDK失败'))
document.head.appendChild(script)
})
},
onAuth() {
if (!this.form.realName) {
uni.showToast({
title: '请输入真实姓名',
icon: 'none'
})
return
}
if (!this.form.idNumber) {
uni.showToast({
title: '请输入证件号码',
icon: 'none'
})
return
}
uni.setStorageSync('authName', this.form.realName)
uni.setStorageSync('authIdNo', this.form.idNumber)
// xuchao: 标记已发起认证流程,取消后停留在认证页,不自动恢复
uni.setStorageSync('authFlowStarted', true)
this.loadAliyunSDK().then(() => {
const metaInfo = JSON.stringify(window.getMetaInfo())
const returnUrl = window.location.origin + window.location.pathname +
'?response=%7B%7D#/pages/rights/rights?status=auth'
this.$u.api.initFaceVerify({
certName: this.form.realName,
certNo: this.form.idNumber,
metaInfo: metaInfo,
returnUrl: returnUrl
}).then(res => {
if (res.errcode === 0 && res.data.body && res.data.body.resultObject && res.data
.body.resultObject.certifyUrl) {
window.location.href = res.data.body.resultObject.certifyUrl
} else {
uni.showToast({
title: res.errmsg || '认证初始化失败',
icon: 'none'
})
}
}).catch(() => {
uni.showToast({
title: '认证初始化失败',
icon: 'none'
})
})
}).catch(() => {
uni.showToast({
title: '加载认证组件失败',
icon: 'none'
})
})
},
// xuchao: Tab 索引 <-> 列表字段的唯一映射
listOf(tab) {
return tab === 1 ? this.appointmentList : tab === 2 ? this.checkList : this.rightsList
},
setList(tab, list) {
if (tab === 1) {
this.appointmentList = list
} else if (tab === 2) {
this.checkList = list
} else {
this.rightsList = list
}
},
// xuchao: 三个 Tab 的统一分页加载器(reset=true 回到第一页重拉)
loadList(tab, reset) {
const p = this.pager[tab]
if (reset) {
p.pageNo = 1
p.total = 0
this.setList(tab, [])
}
// xuchao: 请求序号 —— 新请求取代在途请求,过期响应整体丢弃(治搜索/切 Tab 竞态)
const seq = ++p.reqSeq
p.loading = true
const params = {
pageNo: p.pageNo,
pageSize: this.pageSize,
cardNumber: uni.getStorageSync('cardNumber') || ''
}
let request
if (tab === 1) {
request = this.$u.api.getAppointmentList(params)
} else if (tab === 2) {
request = this.$u.api.getCheckList(params)
} else {
params.keyword = this.keyword
params.lng = this.lng
params.lat = this.lat
if (this.districtId) params.districtId = this.districtId
if (this.category) params.category = this.category
request = this.$u.api.getRightsList(params)
}
request.then(res => {
if (seq !== p.reqSeq) return
// xuchao: 筛选面板选项只随「待使用」列表返回,另两个 Tab 不覆盖
if (tab === 0 && res.errcode === 0 && res.data) {
this.filters = {
districts: res.data.districts || [],
categories: res.data.categories || []
}
}
if (res.errcode === 0 && res.data) {
const list = res.data.list || []
const prev = this.listOf(tab)
// xuchao: 按 id/ruleId 去重合并 —— 兜住后端忽略 pageNo 重复返回同一页的情况
const seen = {}
const merged = (p.pageNo === 1 ? list : prev.concat(list)).filter(item => {
const k = (item.id === undefined || item.id === null) ? item.ruleId : item.id
if (k === undefined || k === null) return true
if (seen[k]) return false
seen[k] = true
return true
})
// xuchao: 本页不足一页 或 整页都是重复 => 视为已到尾部,钉住 total,
// 防止 total 中途变大时反复空翻/空转
if (list.length < this.pageSize || (p.pageNo !== 1 && merged.length === prev.length)) {
p.total = merged.length
} else {
p.total = res.data.total || 0
}
this.setList(tab, merged)
}
}).finally(() => {
if (seq !== p.reqSeq) return
p.loading = false
})
},
loadRightsList() {
this.loadList(0, true)
},
switchTab(index) {
this.currentTab = index
// xuchao: 首次进入该 Tab 才拉首页;已有数据保留各自游标继续分页
if (this.listOf(index).length === 0 && !this.pager[index].loading) {
this.loadList(index, true)
}
},
onSearch() {
this.loadList(0, true)
},
// xuchao: 换筛选条件即回第 1 页重查(关键词保持不变,两者为 AND)
onFilterChange(e) {
this.districtId = e.districtId
this.category = e.category
this.loadList(0, true)
},
goDetail(item) {
uni.navigateTo({
url: '/pages/rights/detail?id=' + item.ruleId
})
},
goBack() {
// xuchao: 直接通过链接进入(无历史栈)时回到首页
const pages = getCurrentPages()
if (pages.length > 1) {
uni.navigateBack()
} else {
uni.reLaunch({ url: '/pages/index/index' })
}
}
}
}
</script>
<style>
page {
background-color: #f2f2f7;
}
</style>
<style scoped lang="less">
.page-auth {
background-color: #ffffff;
}
/* 认证表单 */
.auth-container {
min-height: 100vh;
padding: 16px;
}
.section-title {
display: flex;
align-items: center;
margin-bottom: 16px;
}
.title-bar {
width: 4px;
height: 16px;
background-color: #0088ff;
border-radius: 2px;
margin-right: 8px;
}
.title-text {
font-size: 16px;
font-weight: 600;
color: #1a1818;
}
.form-area {
background-color: #ffffff;
border-radius: 8px;
padding: 0 16px;
}
.form-item {
display: flex;
align-items: center;
height: 64px;
width: 100%;
border-bottom: 1px solid #f0f0f0;
position: relative;
}
.form-label {
font-size: 14px;
color: #333333;
width: 80px;
flex-shrink: 0;
}
.form-label.required::before {
content: '*';
color: #ff4d4f;
margin-right: 4px;
}
.form-input {
flex: 1;
font-size: 14px;
color: #333333;
}
.form-value {
flex: 1;
font-size: 14px;
color: #333333;
}
.phone-value {
color: #999999;
}
.form-arrow {
font-size: 14px;
color: #cccccc;
}
.auth-btn {
margin: 24px 0;
background-color: #0088ff;
color: #ffffff;
text-align: center;
font-size: 16px;
font-weight: 500;
height: 48px;
line-height: 48px;
border-radius: 24px;
}
.tips-section {
margin-top: 8px;
}
.tips-list {
display: flex;
flex-direction: column;
gap: 12px;
padding: 0 4px;
}
.tip-item {
font-size: 13px;
color: #ff4d4f;
line-height: 1.6;
}
/* 未中奖空状态 */
.empty-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding-top: 160px;
}
.empty-img {
width: 200px;
}
.empty-text {
font-size: 18px;
color: #999999;
margin-top: 24px;
}
/* 权益信息 - 蓝色横幅 */
.banner-area {
background-color: #0088ff;
padding: 80px 16px 96px;
border-radius: 0 0 50px 50px;
}
.banner-title {
font-size: 24px;
color: #ffffff;
font-weight: 700;
display: block;
text-align: center;
margin-bottom: 12px;
}
.banner-expire {
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
}
.expire-icon {
width: 18px;
height: 18px;
background-color: #ffffff;
color: #0088ff;
border-radius: 50%;
text-align: center;
line-height: 18px;
font-size: 12px;
font-weight: 700;
}
.expire-text {
font-size: 24px;
color: #ffffff;
font-weight: 600;
}
.expire-label {
font-size: 24px;
color: #ffffff;
}
/* 统计卡片 */
.stats-card {
display: flex;
align-items: center;
background: #FFFFFF;
margin: -60px 12px 0;
border-radius: 10px;
padding: 16px 24px;
box-shadow: 0px 4px 4px 0px #00000019;
position: relative;
z-index: 1;
}
.stats-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
}
.stats-label {
font-size: 12px;
color: #999999;
margin-bottom: 8px;
}
.stats-value-row {
display: flex;
align-items: baseline;
}
.stats-currency {
font-size: 14px;
color: #1a1818;
font-weight: 600;
}
.stats-number {
font-size: 28px;
color: #1a1818;
font-weight: 700;
}
.stats-number-large {
font-size: 28px;
color: #1a1818;
font-weight: 700;
}
.stats-unit {
font-size: 12px;
color: #999999;
margin-left: 2px;
}
.stats-divider {
width: 1px;
height: 36px;
background-color: #eeeeee;
}
/* xuchao: Tab + 搜索 + 筛选条一起吸顶。padding-top 替代原 tab-bar 的 margin-top,
否则吸顶后顶部会留一条能透出列表内容的缝隙;z-index 需高于筛选遮罩(90) */
.info-sticky {
background-color: #ffffff;
padding-top: 12px;
position: sticky;
top: 0;
z-index: 100;
}
/* Tab 切换 */
.tab-bar {
display: flex;
background-color: #ffffff;
padding: 0 16px;
}
.tab-item {
flex: 1;
text-align: center;
padding: 12px 0;
position: relative;
}
.tab-item.active::after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 24px;
height: 3px;
background-color: #0088ff;
border-radius: 2px;
}
.tab-text {
font-size: 14px;
color: #999999;
}
.tab-item.active .tab-text {
color: #0088ff;
font-weight: 600;
}
/* 搜索栏 */
.search-area {
/* xuchao: 底部留白交给筛选条的 20rpx 上内边距,保证三段间距一致 */
padding: 12px 12px 0;
}
.search-bar {
display: flex;
align-items: center;
height: 36px;
background-color: #ffffff;
border-radius: 18px;
padding: 0 12px;
gap: 8px;
}
.search-input {
flex: 1;
font-size: 14px;
color: #333333;
}
/* 权益列表 */
/* xuchao: 原 scroll-view 下 gap 不生效(卡片被包在 scroll-view-content 内),
改普通容器后去掉 gap,卡片间距仍由 .rights-card 的 margin-bottom 提供,视觉不变 */
.list-area {
width: 750rpx;
display: flex;
flex-direction: column;
}
.nomore-area {
width: 100%;
padding-bottom: 24px;
}
.rights-card {
width: 702rpx;
background-color: #fff;
margin-left: 24rpx;
padding: 24rpx;
display: flex;
position: relative;
margin-bottom: 24rpx;
}
.card-img {
width: 150rpx;
height: 150rpx;
border-radius: 6px;
flex-shrink: 0;
}
.card-info {
width: 490rpx;
margin-left: 14rpx;
display: flex;
flex-direction: column;
height: 150rpx;
justify-content: space-between;
}
.card-name {
font-size: 15px;
font-weight: 500;
color: #1a1818;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.card-category {
font-size: 13px;
color: #999999;
margin-top: 4px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.card-address {
width: 300rpx;
font-size: 13px;
color: #999999;
margin-top: 4px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.card-distance {
display: flex;
align-items: center;
gap: 2px;
flex-shrink: 0;
position: absolute;
bottom: 24rpx;
right: 24rpx;
}
.distance-text {
font-size: 12px;
color: #999999;
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding-top: 80px;
}
.empty-state-img {
width: 200px;
}
.empty-state-text {
font-size: 16px;
color: #999999;
margin-top: 16px;
}
/* 返回按钮 */
.back-btn {
position: fixed;
top: 24px;
left: 24px;
display: flex;
align-items: center;
gap: 2px;
z-index: 10;
}
.back-text {
font-size: 14px;
color: #ffffff;
}
/* xuchao: noWin 状态(无中奖空态页)返回按钮改黑色 */
.back-btn--black .back-text {
color: #000000;
}
/* xuchao: 顶部返回导航条(auth状态) */
.nav-bar {
display: flex;
align-items: center;
height: 56px;
background-color: #ffffff;
border-bottom: 1px solid #f0f0f0;
position: relative;
}
.nav-back {
position: absolute;
left: 12px;
top: 0;
bottom: 0;
display: flex;
align-items: center;
}
.nav-title {
flex: 1;
text-align: center;
font-size: 17px;
font-weight: 600;
color: #1a1818;
padding: 0 56px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
</style>