Files
hhrs/pages/rights/rights.vue
T
2026-08-27 16:56:33 +08:00

979 lines
24 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' }">
<!-- 返回按钮(顶部,非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>
<!-- 返回按钮(底部,auth状态) -->
<view class="back-btn-bottom" @click="goBack" v-if="status === 'auth'">
<image class="back-icon" src="https://zhihuiyou.oss-cn-shenzhen.aliyuncs.com/hhrs/hhrs_back.png" mode="aspectFit"></image>
<text class="back-text-bottom">返回上一页</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">
<text class="expire-icon">!</text>
<text class="expire-text">2026年12月31日</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">1500</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">10</text>
<text class="stats-unit">个</text>
</view>
</view>
</view>
<!-- 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>
<!-- 搜索栏 -->
<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>
</view>
<!-- 权益列表 -->
<scroll-view class="list-area" scroll-y @scrolltolower="loadMore" :lower-threshold="100"
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://zhihuiyou.oss-cn-shenzhen.aliyuncs.com/hhrs/hhrs_wzj.png" mode="widthFix"></image>
<text class="empty-state-text">暂无内容</text>
</view>
</scroll-view>
<scroll-view class="list-area" scroll-y @scrolltolower="loadAppointmentMore" :lower-threshold="100"
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>
<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://zhihuiyou.oss-cn-shenzhen.aliyuncs.com/hhrs/hhrs_wzj.png" mode="widthFix"></image>
<text class="empty-state-text">暂无内容</text>
</view>
</scroll-view>
<scroll-view class="list-area" scroll-y @scrolltolower="loadCheckMore" :lower-threshold="100"
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://zhihuiyou.oss-cn-shenzhen.aliyuncs.com/hhrs/hhrs_wzj.png" mode="widthFix"></image>
<text class="empty-state-text">暂无内容</text>
</view>
<view class="loading" v-if="checkLoading">
<text class="loading-text">加载中...</text>
</view>
<view class="no-more" v-else-if="checkList.length >= checkTotal && checkList.length > 0">
<text class="no-more-text">没有更多了</text>
</view>
</scroll-view>
</view>
</view>
</template>
<script>
export default {
data() {
return {
status: 'auth',
form: {
realName: '',
phone: '',
idType: '居民身份证',
idNumber: ''
},
showIdTypePicker: false,
tabs: ['待使用', '已预约', '已使用'],
currentTab: 0,
keyword: '',
rightsList: [],
pageNo: 1,
pageSize: 1000,
total: 0,
lng: '',
lat: '',
loading: false,
appointmentList: [],
appointmentPageNo: 1,
appointmentTotal: 0,
appointmentLoading: false,
checkList: [],
checkPageNo: 1,
checkTotal: 0,
checkLoading: false
}
},
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.autoRecoverStatus()
if (this.status === 'info') {
this.loadRightsList()
}
},
methods: {
// xuchao: 已登录但本地无认证缓存时,仅传手机号查询资格以恢复状态(不带证件号,避免触发被动认证)
autoRecoverStatus() {
if (this.status !== 'auth') return
// xuchao: 用户主动发起过认证流程时,取消后应停留在认证页,不自动恢复
if (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
uni.setStorageSync('isAuth', res.data.isAuth)
uni.setStorageSync('hasWin', res.data.hasWin)
uni.setStorageSync('cardNumber', res.data.cardNumber)
uni.setStorageSync('expiresTime', res.data.expiresTime)
// xuchao: 自动恢复成功,清除流程标记
uni.removeStorageSync('authFlowStarted')
if (res.data.isAuth) {
this.status = res.data.hasWin ? 'info' : 'noWin'
if (this.status === 'info') {
this.loadRightsList()
}
}
})
},
// xuchao: 认证取消/未通过:清理旧认证缓存并标记流程已发起,取消后应停留认证页
markAuthCanceled() {
uni.removeStorageSync('isAuth')
uni.removeStorageSync('hasWin')
uni.removeStorageSync('cardNumber')
uni.removeStorageSync('expiresTime')
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) {
uni.setStorageSync('isAuth', eRes.data.isAuth)
uni.setStorageSync('hasWin', eRes.data.hasWin)
uni.setStorageSync('cardNumber', eRes.data.cardNumber)
uni.setStorageSync('expiresTime', eRes.data.expiresTime)
// 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'
})
})
},
loadRightsList() {
const cardNumber = uni.getStorageSync('cardNumber') || ''
this.fetchRightsList(cardNumber)
},
fetchRightsList(cardNumber) {
if (this.loading) return
this.loading = true
this.$u.api.getRightsList({
pageNo: this.pageNo,
pageSize: this.pageSize,
keyword: this.keyword,
lng: this.lng,
lat: this.lat,
cardNumber: cardNumber
}).then(res => {
if (res.errcode === 0 && res.data) {
const list = res.data.list || []
if (this.pageNo === 1) {
this.rightsList = list
} else {
this.rightsList = this.rightsList.concat(list)
}
this.total = res.data.total || 0
}
}).finally(() => {
this.loading = false
})
},
loadMore() {
if (this.rightsList.length < this.total && !this.loading) {
this.pageNo++
this.loadRightsList()
}
},
switchTab(index) {
this.currentTab = index
if (index === 1 && this.appointmentList.length === 0) {
this.appointmentPageNo = 1
this.loadAppointmentList()
}
if (index === 2 && this.checkList.length === 0) {
this.checkPageNo = 1
this.loadCheckList()
}
},
loadAppointmentList() {
const cardNumber = uni.getStorageSync('cardNumber') || ''
if (this.appointmentLoading) return
this.appointmentLoading = true
this.$u.api.getAppointmentList({
pageNo: this.appointmentPageNo,
pageSize: this.pageSize,
cardNumber: cardNumber
}).then(res => {
if (res.errcode === 0 && res.data) {
const list = res.data.list || []
if (this.appointmentPageNo === 1) {
this.appointmentList = list
} else {
this.appointmentList = this.appointmentList.concat(list)
}
this.appointmentTotal = res.data.total || 0
}
}).finally(() => {
this.appointmentLoading = false
})
},
loadAppointmentMore() {
if (this.appointmentList.length < this.appointmentTotal && !this.appointmentLoading) {
this.appointmentPageNo++
this.loadAppointmentList()
}
},
loadCheckList() {
const cardNumber = uni.getStorageSync('cardNumber') || ''
if (this.checkLoading) return
this.checkLoading = true
this.$u.api.getCheckList({
pageNo: this.checkPageNo,
pageSize: this.pageSize,
cardNumber: cardNumber
}).then(res => {
if (res.errcode === 0 && res.data) {
const list = res.data.list || []
if (this.checkPageNo === 1) {
this.checkList = list
} else {
this.checkList = this.checkList.concat(list)
}
this.checkTotal = res.data.total || 0
}
}).finally(() => {
this.checkLoading = false
})
},
loadCheckMore() {
if (this.checkList.length < this.checkTotal && !this.checkLoading) {
this.checkPageNo++
this.loadCheckList()
}
},
onSearch() {
this.pageNo = 1
this.loadRightsList()
},
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: 500;
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;
}
/* Tab 切换 */
.tab-bar {
display: flex;
background-color: #ffffff;
margin-top: 12px;
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 {
padding: 12px;
}
.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;
}
/* 权益列表 */
.list-area {
width: 750rpx;
display: flex;
flex-direction: column;
gap: 10px;
}
.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;
}
.loading {
text-align: center;
padding: 16px 0;
}
.loading-text {
font-size: 14px;
color: #999999;
}
.no-more {
text-align: center;
padding: 16px 0;
}
.no-more-text {
font-size: 14px;
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;
}
/* 底部返回按钮 */
.back-btn-bottom {
position: fixed;
left: 24px;
bottom: 50px;
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
z-index: 10;
}
.back-icon {
width: 44px;
height: 44px;
}
.back-text-bottom {
font-size: 12px;
color: #666666;
}
</style>