Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6b1037893e | ||
|
|
1fe8c133ad | ||
|
|
c80eef5112 | ||
|
|
e836fb33c8 | ||
|
|
a53b228f0b | ||
|
|
f69bd630a4 | ||
|
|
0cf11a4163 | ||
|
|
745f4bf5f1 |
@@ -2,3 +2,6 @@ unpackage/
|
|||||||
node_modules/
|
node_modules/
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.playwright-mcp/
|
.playwright-mcp/
|
||||||
|
|
||||||
|
# xuchao: 本地 agent 说明文档,不入库(与 CLAUDE.md 均保留在本地)
|
||||||
|
AGENTS.md
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
// xuchao: 账号级本地缓存集中管理。权益页首屏会读这些缓存做兜底展示,
|
||||||
|
// 换账号/退出时漏清就会把上一个账号的中奖状态、卡号、到期时间当成当前账号的数据展示。
|
||||||
|
const ACCOUNT_KEYS = ['token', 'phone', 'isAuth', 'hasWin', 'cardNumber', 'expiresTime',
|
||||||
|
'rightsCount', 'totalValueFen', 'authFlowStarted', 'authName', 'authIdNo']
|
||||||
|
|
||||||
|
export default {
|
||||||
|
// 清空当前账号的本地会话与权益缓存(定位 lng/lat 属于设备信息,不在此列)
|
||||||
|
clearAccountCache() {
|
||||||
|
ACCOUNT_KEYS.forEach(key => uni.removeStorageSync(key))
|
||||||
|
},
|
||||||
|
// 账号标识(手机号)
|
||||||
|
setAccountPhone(phone) {
|
||||||
|
uni.setStorageSync('phone', phone)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
// xuchao: 全站集中配置项 —— 后端基址/客服电话等只在这里维护一处
|
||||||
|
const config = {
|
||||||
|
// 后端 API 基址。当前指向 UAT 测试环境(见 AGENTS.md 部署信息),切生产改这一行即可
|
||||||
|
baseUrl: 'https://dscp-uat.hn12301.net',
|
||||||
|
// 对外统一客服电话,与《用户协议》《隐私协议》公示号码保持一致
|
||||||
|
servicePhone: '0731-88912301',
|
||||||
|
// xuchao: open-api/hhService/login 强制校验固定 password 字段(后端为 H5 开的旁路,
|
||||||
|
// 前端去掉即登录失败,故集中存放并在此说明;待后端改造 H5 免密登录后删除。
|
||||||
|
loginFixedPassword: '1232321'
|
||||||
|
}
|
||||||
|
|
||||||
|
export default config
|
||||||
+56
-69
@@ -1,87 +1,74 @@
|
|||||||
// 如果没有通过拦截器配置域名的话,可以在这里写上完整的URL(加上域名部分)
|
// xuchao: 怀化人社 H5 接口清单,与 docs/hhrs-api-doc.md 一一对应
|
||||||
let LoginUrl = 'open-api/pingXiang/login';
|
// 基址由 common/http.interceptor.js 统一注入(common/config.js.baseUrl)
|
||||||
let SendVerifyCodeUrl = 'open-api/pingXiang/sendLoginVerifyCode';
|
// 登录 / 云闪付入口
|
||||||
let VenueListUrl = 'mp-api/hhrs/venue/list';
|
// xuchao: hhrs 专属登录端点(旧地址 open-api/pingXiang/* 由后端兼容层转发,已弃用)
|
||||||
let VenueDetailUrl = 'mp-api/hhrs/venue/detail';
|
const LoginUrl = 'open-api/hhService/login'
|
||||||
let MerchantListUrl = 'mp-api/hhrs/merchant/list';
|
const SendVerifyCodeUrl = 'open-api/hhService/sendLoginVerifyCode'
|
||||||
let MerchantDetailUrl = 'mp-api/hhrs/merchant/detail';
|
const TokenDecodeUrl = 'open-api/hhService/tokenDecode'
|
||||||
let RightsInfoUrl = 'mp-api/hhrs/rights/info';
|
// 门店(文旅场馆 typeId=1 / 商户折扣 typeId=2)
|
||||||
let RightsDetailUrl = 'mp-api/hhrsRights/detail';
|
const StoreListUrl = 'mp-api/hhrsStore/list'
|
||||||
let RightsAuthUrl = 'mp-api/hhrs/rights/auth';
|
const StoreDetailUrl = 'mp-api/hhrsStore/detail'
|
||||||
let EligibilityUrl = 'mp-api/hhrsRights/eligibility';
|
// 权益包
|
||||||
let RightsBookUrl = 'mp-api/hhrs/rights/book';
|
const RightsListUrl = 'mp-api/hhrsRights/list'
|
||||||
let InitFaceVerifyUrl = 'open-api/aliyun/face/initFaceVerify';
|
const RightsDetailUrl = 'mp-api/hhrsRights/detail'
|
||||||
let DescribeFaceVerifyUrl = 'open-api/aliyun/face/describeFaceVerify';
|
const EligibilityUrl = 'mp-api/hhrsRights/eligibility'
|
||||||
let RightsListUrl = 'mp-api/hhrsRights/list';
|
const AppointmentListUrl = 'mp-api/hhrsRights/appointment/list'
|
||||||
let AppointmentListUrl = 'mp-api/hhrsRights/appointment/list';
|
const CheckListUrl = 'mp-api/hhrsRights/check/list'
|
||||||
let CheckListUrl = 'mp-api/hhrsRights/check/list';
|
// 实名人脸认证(阿里云)
|
||||||
let StoreListUrl = 'mp-api/hhrsStore/list';
|
const InitFaceVerifyUrl = 'open-api/aliyun/face/initFaceVerify'
|
||||||
let StoreDetailUrl = 'mp-api/hhrsStore/detail';
|
const DescribeFaceVerifyUrl = 'open-api/aliyun/face/describeFaceVerify'
|
||||||
let WinnerDatesUrl = 'mp-api/hhrsWinner/dates';
|
// 中奖公示
|
||||||
let WinnerListUrl = 'mp-api/hhrsWinner/list';
|
const WinnerDatesUrl = 'mp-api/hhrsWinner/dates'
|
||||||
let AppointUrl = 'zhy-api/cardBag/appoint';
|
const WinnerListUrl = 'mp-api/hhrsWinner/list'
|
||||||
let TokenDecodeUrl = 'open-api/hhService/tokenDecode';
|
// 预约下单(智慧游卡包)
|
||||||
|
const AppointUrl = 'zhy-api/cardBag/appoint'
|
||||||
|
|
||||||
// 此处第二个参数vm,就是我们在页面使用的this
|
// 此处第二个参数vm,就是我们在页面使用的this
|
||||||
const install = (Vue, vm) => {
|
const install = (Vue, vm) => {
|
||||||
let checkerLogin = (params = {}) => vm.$u.post(LoginUrl, params);
|
// 登录与凭证
|
||||||
let sendLoginVerifyCode = (params = {}) => vm.$u.post(SendVerifyCodeUrl, params);
|
const checkerLogin = (params = {}) => vm.$u.post(LoginUrl, params)
|
||||||
|
const sendLoginVerifyCode = (params = {}) => vm.$u.post(SendVerifyCodeUrl, params)
|
||||||
// xuchao: 云闪付入口 token 解码
|
// xuchao: 云闪付入口 token 解码
|
||||||
let tokenDecode = (params = {}) => vm.$u.post(TokenDecodeUrl, params);
|
const tokenDecode = (params = {}) => vm.$u.post(TokenDecodeUrl, params)
|
||||||
|
|
||||||
// 文旅场馆
|
// 门店列表/详情
|
||||||
let getVenueList = (params = {}) => vm.$u.get(VenueListUrl, params);
|
const getStoreList = (params = {}) => vm.$u.post(StoreListUrl, params)
|
||||||
let getVenueDetail = (params = {}) => vm.$u.post(VenueDetailUrl, params);
|
const getStoreDetail = (params = {}) => vm.$u.post(StoreDetailUrl, params)
|
||||||
|
|
||||||
// 商户折扣
|
|
||||||
let getMerchantList = (params = {}) => vm.$u.get(MerchantListUrl, params);
|
|
||||||
let getMerchantDetail = (params = {}) => vm.$u.post(MerchantDetailUrl, params);
|
|
||||||
|
|
||||||
// 中奖名单
|
|
||||||
let getWinnerDates = (params = {}) => vm.$u.get(WinnerDatesUrl, params);
|
|
||||||
let getWinnerList = (params = {}) => vm.$u.post(WinnerListUrl, params);
|
|
||||||
let doAppoint = (params = {}) => vm.$u.post(AppointUrl, params);
|
|
||||||
|
|
||||||
// 权益包
|
// 权益包
|
||||||
let getRightsInfo = (params = {}) => vm.$u.get(RightsInfoUrl, params);
|
const getRightsList = (params = {}) => vm.$u.post(RightsListUrl, params)
|
||||||
let getRightsDetail = (params = {}) => vm.$u.post(RightsDetailUrl, params);
|
const getRightsDetail = (params = {}) => vm.$u.post(RightsDetailUrl, params)
|
||||||
let doRightsAuth = (params = {}) => vm.$u.post(RightsAuthUrl, params);
|
// xuchao: 注意 identityNo 会触发后端“被动认证”回写,仅在认证流程中传
|
||||||
let doRightsBook = (params = {}) => vm.$u.post(RightsBookUrl, params);
|
const getEligibility = (params = {}) => vm.$u.post(EligibilityUrl, params)
|
||||||
let getEligibility = (params = {}) => vm.$u.post(EligibilityUrl, params);
|
const getAppointmentList = (params = {}) => vm.$u.post(AppointmentListUrl, params)
|
||||||
let initFaceVerify = (params = {}) => vm.$u.post(InitFaceVerifyUrl, params);
|
const getCheckList = (params = {}) => vm.$u.post(CheckListUrl, params)
|
||||||
let describeFaceVerify = (params = {}) => vm.$u.post(DescribeFaceVerifyUrl, params);
|
const doAppoint = (params = {}) => vm.$u.post(AppointUrl, params)
|
||||||
let getRightsList = (params = {}) => vm.$u.post(RightsListUrl, params);
|
|
||||||
let getAppointmentList = (params = {}) => vm.$u.post(AppointmentListUrl, params);
|
// 人脸核身
|
||||||
let getCheckList = (params = {}) => vm.$u.post(CheckListUrl, params);
|
const initFaceVerify = (params = {}) => vm.$u.post(InitFaceVerifyUrl, params)
|
||||||
let getStoreList = (params = {}) => vm.$u.post(StoreListUrl, params);
|
const describeFaceVerify = (params = {}) => vm.$u.post(DescribeFaceVerifyUrl, params)
|
||||||
let getStoreDetail = (params = {}) => vm.$u.post(StoreDetailUrl, params);
|
|
||||||
|
// 中奖名单
|
||||||
|
const getWinnerDates = (params = {}) => vm.$u.get(WinnerDatesUrl, params)
|
||||||
|
const getWinnerList = (params = {}) => vm.$u.post(WinnerListUrl, params)
|
||||||
|
|
||||||
vm.$u.api = {
|
vm.$u.api = {
|
||||||
checkerLogin,
|
checkerLogin,
|
||||||
sendLoginVerifyCode,
|
sendLoginVerifyCode,
|
||||||
tokenDecode,
|
tokenDecode,
|
||||||
getVenueList,
|
|
||||||
getVenueDetail,
|
|
||||||
getMerchantList,
|
|
||||||
getMerchantDetail,
|
|
||||||
getWinnerList,
|
|
||||||
getRightsInfo,
|
|
||||||
getRightsDetail,
|
|
||||||
doRightsAuth,
|
|
||||||
doRightsBook,
|
|
||||||
getEligibility,
|
|
||||||
initFaceVerify,
|
|
||||||
describeFaceVerify,
|
|
||||||
getRightsList,
|
|
||||||
getAppointmentList,
|
|
||||||
getCheckList,
|
|
||||||
getStoreList,
|
getStoreList,
|
||||||
getStoreDetail,
|
getStoreDetail,
|
||||||
getWinnerDates,
|
getRightsList,
|
||||||
getWinnerList,
|
getRightsDetail,
|
||||||
|
getEligibility,
|
||||||
|
getAppointmentList,
|
||||||
|
getCheckList,
|
||||||
doAppoint,
|
doAppoint,
|
||||||
};
|
initFaceVerify,
|
||||||
|
describeFaceVerify,
|
||||||
|
getWinnerDates,
|
||||||
|
getWinnerList
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
|
import config from '@/common/config.js'
|
||||||
|
|
||||||
// 这里的vm,就是我们在vue文件里面的this,所以我们能在这里获取vuex的变量,比如存放在里面的token
|
// 这里的vm,就是我们在vue文件里面的this,所以我们能在这里获取vuex的变量,比如存放在里面的token
|
||||||
// 同时,我们也可以在此使用getApp().globalData,如果你把token放在getApp().globalData的话,也是可以使用的
|
// 同时,我们也可以在此使用getApp().globalData,如果你把token放在getApp().globalData的话,也是可以使用的
|
||||||
const install = (Vue, vm) => {
|
const install = (Vue, vm) => {
|
||||||
Vue.prototype.$u.http.setConfig({
|
Vue.prototype.$u.http.setConfig({
|
||||||
baseUrl: 'https://dscp-uat.hn12301.net',
|
// xuchao: 基址统一取 common/config.js,避免多处硬编码
|
||||||
|
baseUrl: config.baseUrl,
|
||||||
// loadingText: '努力加载中~',
|
// loadingText: '努力加载中~',
|
||||||
// loadingTime: 18800,
|
// loadingTime: 18800,
|
||||||
// 如果将此值设置为true,拦截回调中将会返回服务端返回的所有数据response,而不是response.data
|
// 如果将此值设置为true,拦截回调中将会返回服务端返回的所有数据response,而不是response.data
|
||||||
@@ -41,10 +44,17 @@ const install = (Vue, vm) => {
|
|||||||
|
|
||||||
// 如果把originalData设置为了true,这里return回什么,this.$u.post的then回调中就会得到什么
|
// 如果把originalData设置为了true,这里return回什么,this.$u.post的then回调中就会得到什么
|
||||||
return res;
|
return res;
|
||||||
}else if(res.errcode==-99){
|
}else if(res.errcode==-99 || res.errcode==30002){
|
||||||
uni.navigateTo({
|
// xuchao: 未登录/会话失效(-99 自定义、30002 框架 SESSION_AUTH_FAIL)—— 已在登录页则不再入栈
|
||||||
url:'/pages/index/login'
|
const stack = getCurrentPages() || []
|
||||||
})
|
const top = stack.length ? stack[stack.length - 1].route : ''
|
||||||
|
if (top !== 'pages/index/login') {
|
||||||
|
uni.navigateTo({
|
||||||
|
url:'/pages/index/login'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// xuchao: 必须把 res 交回调用方,否则页面 .then(res => res.errcode) 会因 undefined 抛错
|
||||||
|
return res
|
||||||
}else {
|
}else {
|
||||||
|
|
||||||
// uni.showToast({
|
// uni.showToast({
|
||||||
|
|||||||
@@ -1,81 +0,0 @@
|
|||||||
<template>
|
|
||||||
<view class="tab-bar">
|
|
||||||
<view class="tab-bar-item" v-for="(item, index) in tabList" :key="index" @click="switchTab(index)">
|
|
||||||
<image class="tab-icon" :src="current === index ? item.selectedIconPath : item.iconPath"></image>
|
|
||||||
<text class="tab-text" :style="{ color: current === index ? selectedColor : color }">{{ item.text }}</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
name: 'TabBar',
|
|
||||||
props: {
|
|
||||||
current: {
|
|
||||||
type: Number,
|
|
||||||
default: 0
|
|
||||||
}
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
color: '#999999',
|
|
||||||
selectedColor: '#007aff',
|
|
||||||
backgroundColor: '#ffffff',
|
|
||||||
tabList: [
|
|
||||||
{
|
|
||||||
pagePath: '/pages/index/index',
|
|
||||||
iconPath: '/static/shouye.png',
|
|
||||||
selectedIconPath: '/static/shouye_select.png',
|
|
||||||
text: '首页'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
pagePath: '/pages/mine/mine',
|
|
||||||
iconPath: '/static/wode.png',
|
|
||||||
selectedIconPath: '/static/wode_select.png',
|
|
||||||
text: '我的'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
switchTab(index) {
|
|
||||||
if (this.current === index) return;
|
|
||||||
uni.switchTab({
|
|
||||||
url: this.tabList[index].pagePath
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.tab-bar {
|
|
||||||
position: fixed;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
height: 100rpx;
|
|
||||||
background-color: #ffffff;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-around;
|
|
||||||
border-top: 1rpx solid rgba(242, 242, 242, 0.5);
|
|
||||||
padding-bottom: env(safe-area-inset-bottom);
|
|
||||||
}
|
|
||||||
.tab-bar-item {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
flex: 1;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
.tab-icon {
|
|
||||||
width: 48rpx;
|
|
||||||
height: 48rpx;
|
|
||||||
margin-bottom: 4rpx;
|
|
||||||
}
|
|
||||||
.tab-text {
|
|
||||||
font-size: 22rpx;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
+23
-10
@@ -1,7 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="page">
|
<view class="page">
|
||||||
<view class="content">
|
<!-- xuchao: 参加指南改为整图展示,与使用指南一致;图片资源尚未上传 OSS,加载失败时给文字兜底避免白屏 -->
|
||||||
<text>参加指南</text>
|
<image v-if="!imgFailed" class="join-img" :src="imgUrl" mode="widthFix" @error="imgFailed = true"></image>
|
||||||
|
<view class="empty-tip" v-else>
|
||||||
|
<text class="empty-text">参加指南内容整理中,请稍后再来查看</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
@@ -9,7 +11,11 @@
|
|||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
data() {
|
data() {
|
||||||
return {}
|
return {
|
||||||
|
// 运营指定图片地址(OSS 上传后可与全站统一为 aliyunoss.hn12301.net/hhrs)
|
||||||
|
imgUrl: 'https://zhihuiyou.oss-cn-shenzhen.aliyuncs.com/hhrs/hhrs_zhinan.jpg',
|
||||||
|
imgFailed: false
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onLoad() {},
|
onLoad() {},
|
||||||
methods: {}
|
methods: {}
|
||||||
@@ -18,14 +24,21 @@
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.page {
|
.page {
|
||||||
height: 100vh;
|
min-height: 100vh;
|
||||||
background-color: #f5f5f5;
|
background-color: #f5f5f5;
|
||||||
overflow: hidden;
|
|
||||||
}
|
}
|
||||||
.content {
|
/* xuchao: 整图展示,widthFix 随页面向下滚动 */
|
||||||
display: flex;
|
.join-img {
|
||||||
align-items: center;
|
display: block;
|
||||||
justify-content: center;
|
width: 100%;
|
||||||
height: 100vh;
|
}
|
||||||
|
.empty-tip {
|
||||||
|
padding: 80px 24px 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.empty-text {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #999999;
|
||||||
|
line-height: 1.6;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="page">
|
<view class="page">
|
||||||
<image class="use-img" src="https://zhihuiyou.oss-cn-shenzhen.aliyuncs.com/hhrs/hhrs_use.jpg" mode="widthFix"></image>
|
<image class="use-img" src="https://aliyunoss.hn12301.net/hhrs/hhrs_use.jpg" mode="widthFix"></image>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
+46
-10
@@ -34,6 +34,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import account from '@/common/account.js'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@@ -56,21 +58,46 @@
|
|||||||
bg: 'https://aliyunoss.hn12301.net/hhrs/hhrs_syzn.png',
|
bg: 'https://aliyunoss.hn12301.net/hhrs/hhrs_syzn.png',
|
||||||
url: '/pages/guide/use'
|
url: '/pages/guide/use'
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
// xuchao: 云闪付免密登录是否仍在进行中 —— 返回前不放行任何依赖身份的跳转
|
||||||
|
tokenLoginPending: false,
|
||||||
|
tokenLoginTimer: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onLoad(options) {
|
onLoad(options) {
|
||||||
|
// xuchao: 云闪付入口带 token 进来 —— 直接免密换取会话,不经过手机号验证码
|
||||||
if (options.token) {
|
if (options.token) {
|
||||||
this.handleTokenLogin(options.token)
|
this.handleTokenLogin(options.token)
|
||||||
}
|
}
|
||||||
this.getLocation()
|
this.getLocation()
|
||||||
},
|
},
|
||||||
|
// xuchao: 离开首页时收尾,避免看门狗与遮罩残留
|
||||||
|
onUnload() {
|
||||||
|
this.finishTokenLogin()
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
// xuchao: 云闪付入口 token 自动登录
|
// xuchao: 云闪付入口 token 自动登录(免密换人)
|
||||||
handleTokenLogin(token) {
|
handleTokenLogin(token) {
|
||||||
|
// xuchao: 云闪付入口是"免密换人",本机上一个账号的会话与权益缓存必须先丢弃:
|
||||||
|
// 一是请求不能再带旧 accessToken(后端"会话优先"会把旧账号数据返给新用户),
|
||||||
|
// 二是权益页只在 onLoad 取一次账号快照、不会自我纠正,所以清缓存必须赶在
|
||||||
|
// 发请求之前 —— 解码返回前的窗口期里最多读到"未登录",绝不会读到别人的数据
|
||||||
|
account.clearAccountCache()
|
||||||
|
this.tokenLoginPending = true
|
||||||
|
uni.showLoading({ title: '登录中', mask: true })
|
||||||
|
// xuchao: 兜底看门狗,网络异常时不让遮罩卡死(只收遮罩,不改变登录结果)
|
||||||
|
this.tokenLoginTimer = setTimeout(() => {
|
||||||
|
this.finishTokenLogin()
|
||||||
|
}, 10000)
|
||||||
this.$u.api.tokenDecode({ token }).then(res => {
|
this.$u.api.tokenDecode({ token }).then(res => {
|
||||||
|
this.finishTokenLogin()
|
||||||
if (res.errcode === 0 && res.data && res.data.mobile) {
|
if (res.errcode === 0 && res.data && res.data.mobile) {
|
||||||
uni.setStorageSync('phone', res.data.mobile)
|
uni.setStorageSync('phone', res.data.mobile)
|
||||||
|
// xuchao: 后端已为云闪付入口签发会员会话,存下 accessToken,后续接口才认得这个用户。
|
||||||
|
// 只认"非空字符串":后端字段缺失/为 null 时会被序列化成 {},直接存进缓存
|
||||||
|
// 就会变成 Bearer [object Object],把所有鉴权接口全打挂
|
||||||
|
const issued = typeof res.data.accessToken === 'string' ? res.data.accessToken : ''
|
||||||
|
if (issued) uni.setStorageSync('token', issued)
|
||||||
this.$u.api.getEligibility({ phone: res.data.mobile, identityNo: res.data.idNo || '', name: res.data.name || '' }).then(eRes => {
|
this.$u.api.getEligibility({ phone: res.data.mobile, identityNo: res.data.idNo || '', name: res.data.name || '' }).then(eRes => {
|
||||||
if (eRes.errcode === 0) {
|
if (eRes.errcode === 0) {
|
||||||
uni.setStorageSync('isAuth', eRes.data.isAuth)
|
uni.setStorageSync('isAuth', eRes.data.isAuth)
|
||||||
@@ -85,9 +112,18 @@
|
|||||||
this.degradeToPhoneLogin()
|
this.degradeToPhoneLogin()
|
||||||
}
|
}
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
|
this.finishTokenLogin()
|
||||||
this.degradeToPhoneLogin()
|
this.degradeToPhoneLogin()
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
// xuchao: 免密登录收尾(收起遮罩、撤看门狗),成功/失败/超时/离开页面都会走到
|
||||||
|
finishTokenLogin() {
|
||||||
|
if (!this.tokenLoginPending) return
|
||||||
|
this.tokenLoginPending = false
|
||||||
|
clearTimeout(this.tokenLoginTimer)
|
||||||
|
this.tokenLoginTimer = null
|
||||||
|
uni.hideLoading()
|
||||||
|
},
|
||||||
// xuchao: 解码失败兜底,提示并跳手机号登录
|
// xuchao: 解码失败兜底,提示并跳手机号登录
|
||||||
degradeToPhoneLogin() {
|
degradeToPhoneLogin() {
|
||||||
uni.showToast({ title: '登录凭证已失效,请使用手机号登录', icon: 'none' })
|
uni.showToast({ title: '登录凭证已失效,请使用手机号登录', icon: 'none' })
|
||||||
@@ -122,6 +158,11 @@
|
|||||||
uni.navigateTo({ url })
|
uni.navigateTo({ url })
|
||||||
},
|
},
|
||||||
goRights() {
|
goRights() {
|
||||||
|
// xuchao: 免密登录还没返回时不放行,避免带着空/旧身份进权益页
|
||||||
|
if (this.tokenLoginPending) {
|
||||||
|
uni.showToast({ title: '正在登录,请稍候', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
const phone = uni.getStorageSync('phone')
|
const phone = uni.getStorageSync('phone')
|
||||||
if (!phone) {
|
if (!phone) {
|
||||||
uni.navigateTo({ url: '/pages/index/login?redirect=rights' })
|
uni.navigateTo({ url: '/pages/index/login?redirect=rights' })
|
||||||
@@ -142,15 +183,11 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
page {
|
/* xuchao: 原 page/.page 固定 100vh + overflow:hidden,小屏下活动指南第二张卡片会被裁掉且无法滚动;
|
||||||
height: 100%;
|
改为 min-height 撑满、超出可滚动,视觉在正常屏上保持不变 */
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page {
|
.page {
|
||||||
height: 100vh;
|
min-height: 100vh;
|
||||||
background-color: #f5f5f5;
|
background-color: #f5f5f5;
|
||||||
overflow: hidden;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
@@ -196,7 +233,6 @@
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
background-color: #ffffff;
|
background-color: #ffffff;
|
||||||
margin-top: 10rpx;
|
margin-top: 10rpx;
|
||||||
overflow: hidden;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|||||||
+43
-14
@@ -43,6 +43,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import config from '@/common/config.js'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@@ -63,6 +65,24 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
// xuchao: open-api/hhService/login、sendLoginVerifyCode 返回 OpenapiResponse{code,msg,data},
|
||||||
|
// 被网关再包一层后外层 errcode 恒为 0,真正的业务码在 data.code 里。
|
||||||
|
// 只判 errcode 会把「验证码错误/过期、短信被限流」全部当成成功。
|
||||||
|
// 宽松兜底:后端若改成不返回 data.code,仍按 errcode 判定,不会把成功误判为失败。
|
||||||
|
isBizOk(res) {
|
||||||
|
if (!res || res.errcode !== 0) return false
|
||||||
|
const d = res.data
|
||||||
|
if (!d || d.code === undefined || d.code === null) {
|
||||||
|
// xuchao: 没有业务码时,还要排除“网关把 404/错误页包成 HTTP 200”的情况
|
||||||
|
// (实测未部署的新地址会返回 {data:{timestamp,status:404,error:'Not Found'}} 且 errcode=0)
|
||||||
|
return !d || (d.status === undefined && d.error === undefined)
|
||||||
|
}
|
||||||
|
return d.code === 200
|
||||||
|
},
|
||||||
|
// xuchao: 失败原因优先取业务 msg,其次取网关 errmsg
|
||||||
|
bizMsg(res) {
|
||||||
|
return (res && res.data && res.data.msg) || (res && res.errmsg) || ''
|
||||||
|
},
|
||||||
sendCode() {
|
sendCode() {
|
||||||
if (this.countdown > 0) return
|
if (this.countdown > 0) return
|
||||||
if (!this.phone || !/^1\d{10}$/.test(this.phone)) {
|
if (!this.phone || !/^1\d{10}$/.test(this.phone)) {
|
||||||
@@ -70,22 +90,24 @@ export default {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.$u.api.sendLoginVerifyCode({ smsType: 'CIB_LOGIN', mobile: this.phone }).then(res => {
|
this.$u.api.sendLoginVerifyCode({ smsType: 'CIB_LOGIN', mobile: this.phone }).then(res => {
|
||||||
if (res.errcode === 0) {
|
if (this.isBizOk(res)) {
|
||||||
uni.showToast({ title: '验证码已发送', icon: 'success' })
|
uni.showToast({ title: '验证码已发送', icon: 'success' })
|
||||||
|
// xuchao: 倒计时移到发送成功之后(原来无条件启动,发送失败也会被白锁 60 秒无法重试)
|
||||||
|
this.countdown = 60
|
||||||
|
this.timer = setInterval(() => {
|
||||||
|
this.countdown--
|
||||||
|
if (this.countdown <= 0) {
|
||||||
|
clearInterval(this.timer)
|
||||||
|
this.timer = null
|
||||||
|
}
|
||||||
|
}, 1000)
|
||||||
} else {
|
} else {
|
||||||
uni.showToast({ title: res.errmsg || '发送失败', icon: 'none' })
|
// xuchao: 透出后端真实原因,如「操作频繁,请稍后再试」
|
||||||
|
uni.showToast({ title: this.bizMsg(res) || '发送失败', icon: 'none' })
|
||||||
}
|
}
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
uni.showToast({ title: '发送失败', icon: 'none' })
|
uni.showToast({ title: '发送失败', icon: 'none' })
|
||||||
})
|
})
|
||||||
this.countdown = 60
|
|
||||||
this.timer = setInterval(() => {
|
|
||||||
this.countdown--
|
|
||||||
if (this.countdown <= 0) {
|
|
||||||
clearInterval(this.timer)
|
|
||||||
this.timer = null
|
|
||||||
}
|
|
||||||
}, 1000)
|
|
||||||
},
|
},
|
||||||
onLogin() {
|
onLogin() {
|
||||||
if (!this.phone || !/^1\d{10}$/.test(this.phone)) {
|
if (!this.phone || !/^1\d{10}$/.test(this.phone)) {
|
||||||
@@ -100,9 +122,14 @@ export default {
|
|||||||
uni.showToast({ title: '请同意用户协议和隐私协议', icon: 'none' })
|
uni.showToast({ title: '请同意用户协议和隐私协议', icon: 'none' })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.$u.api.checkerLogin({ password: '1232321', mobile: this.phone, verifyCode: this.code }).then(res => {
|
// xuchao: password 为后端要求的固定旁路口令,集中取自 config.loginFixedPassword
|
||||||
if (res.errcode === 0) {
|
this.$u.api.checkerLogin({ password: config.loginFixedPassword, mobile: this.phone, verifyCode: this.code }).then(res => {
|
||||||
uni.setStorageSync('token', res.data.token)
|
if (this.isBizOk(res)) {
|
||||||
|
// xuchao: 只认"非空字符串"凭证。后端若把 null 序列化成 {}(实测 tokenDecode 就是这个形状),
|
||||||
|
// 用 || 兜底会把对象存进缓存,之后每个请求都变成 Bearer [object Object]
|
||||||
|
const payload = (res.data && res.data.data) || {}
|
||||||
|
const issued = [payload.accessToken, payload.token].find(v => typeof v === 'string' && v.length > 0)
|
||||||
|
uni.setStorageSync('token', issued || '')
|
||||||
uni.setStorageSync('phone', this.phone)
|
uni.setStorageSync('phone', this.phone)
|
||||||
this.$u.api.getEligibility({ phone: this.phone, identityNo: '', name: '' }).then(eRes => {
|
this.$u.api.getEligibility({ phone: this.phone, identityNo: '', name: '' }).then(eRes => {
|
||||||
if (eRes.errcode === 0) {
|
if (eRes.errcode === 0) {
|
||||||
@@ -125,7 +152,9 @@ export default {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
uni.showToast({ title: res.errmsg || '登录失败', icon: 'none' })
|
// xuchao: 透出后端真实原因,如「验证码错误」「验证码已过期,请重新获取」;
|
||||||
|
// 失败时绝不写 phone,避免留下“假登录态”让后续页面以为已登录
|
||||||
|
uni.showToast({ title: this.bizMsg(res) || '登录失败', icon: 'none' })
|
||||||
}
|
}
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
uni.showToast({ title: '登录失败', icon: 'none' })
|
uni.showToast({ title: '登录失败', icon: 'none' })
|
||||||
|
|||||||
+12
-7
@@ -38,16 +38,14 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 底部 TabBar -->
|
|
||||||
<!-- <tab-bar :current="1" /> -->
|
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import tabBar from '@/components/TabBar/TabBar.vue'
|
import config from '@/common/config.js'
|
||||||
|
import account from '@/common/account.js'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: { tabBar },
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
isLogin: false,
|
isLogin: false,
|
||||||
@@ -74,8 +72,9 @@ export default {
|
|||||||
content: '确定要退出登录吗?',
|
content: '确定要退出登录吗?',
|
||||||
success: (res) => {
|
success: (res) => {
|
||||||
if (res.confirm) {
|
if (res.confirm) {
|
||||||
uni.removeStorageSync('token')
|
// xuchao: 原来只清 token/phone,权益缓存(isAuth/hasWin/cardNumber/expiresTime/
|
||||||
uni.removeStorageSync('phone')
|
// rightsCount 等)残留会被下一个账号当成自己的数据展示
|
||||||
|
account.clearAccountCache()
|
||||||
this.isLogin = false
|
this.isLogin = false
|
||||||
this.phone = ''
|
this.phone = ''
|
||||||
uni.showToast({ title: '已退出登录', icon: 'success' })
|
uni.showToast({ title: '已退出登录', icon: 'success' })
|
||||||
@@ -90,7 +89,13 @@ export default {
|
|||||||
uni.navigateTo({ url: '/pages/guide/use' })
|
uni.navigateTo({ url: '/pages/guide/use' })
|
||||||
},
|
},
|
||||||
goService() {
|
goService() {
|
||||||
// TODO: 联系客服
|
// xuchao: 唤起拨号,号码与协议页公示的统一客服电话同源(common/config.js)
|
||||||
|
uni.makePhoneCall({
|
||||||
|
phoneNumber: config.servicePhone,
|
||||||
|
fail: () => {
|
||||||
|
uni.showToast({ title: '拨号失败,客服电话 ' + config.servicePhone, icon: 'none' })
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -143,10 +143,9 @@ export default {
|
|||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
uni.showToast({ title: '预约失败', icon: 'none' })
|
uni.showToast({ title: '预约失败', icon: 'none' })
|
||||||
})
|
})
|
||||||
} else {
|
|
||||||
// TODO: 调用使用接口
|
|
||||||
uni.showToast({ title: '使用成功', icon: 'success' })
|
|
||||||
}
|
}
|
||||||
|
// xuchao: 非预约权益没有“线上使用”动作——到店由商户核销,结果体现在权益包「已使用」列表;
|
||||||
|
// 底部按钮本身也只在 detail.isAppointment 时渲染,故删除原先只弹假 Toast 的不可达分支
|
||||||
},
|
},
|
||||||
loadDetail(ruleId) {
|
loadDetail(ruleId) {
|
||||||
const loc = uni.getStorageSync('lng') && uni.getStorageSync('lat')
|
const loc = uni.getStorageSync('lng') && uni.getStorageSync('lat')
|
||||||
|
|||||||
+133
-142
@@ -46,7 +46,7 @@
|
|||||||
<view class="tips-list">
|
<view class="tips-list">
|
||||||
<text class="tip-item">1. 请输入真实的身份信息用来进行活动权益资格认证</text>
|
<text class="tip-item">1. 请输入真实的身份信息用来进行活动权益资格认证</text>
|
||||||
<text class="tip-item">2. 认证通过后系统会显示出您的所获得的权益信息</text>
|
<text class="tip-item">2. 认证通过后系统会显示出您的所获得的权益信息</text>
|
||||||
<text class="tip-item">3. 手机号码为您登录的手机,无法修改。若号码对,请更换登录手机。</text>
|
<text class="tip-item">3. 手机号码为您登录的手机号,无法修改。若号码不对,请更换手机号重新登录。</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -107,8 +107,8 @@
|
|||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 权益列表 -->
|
<!-- 权益列表 -->
|
||||||
<scroll-view class="list-area" scroll-y @scrolltolower="loadMore" :lower-threshold="100"
|
<!-- xuchao: 三 Tab 列表统一改普通容器 + 页面级触底分页(scroll-view 无固定高度时 scrolltolower 永不触发) -->
|
||||||
v-if="currentTab==0">
|
<view class="list-area" v-if="currentTab==0">
|
||||||
<view class="rights-card" v-for="item in rightsList" :key="item.ruleId" @click="goDetail(item)">
|
<view class="rights-card" v-for="item in rightsList" :key="item.ruleId" @click="goDetail(item)">
|
||||||
<image class="card-img" :src="item.image" mode="aspectFill" />
|
<image class="card-img" :src="item.image" mode="aspectFill" />
|
||||||
<view class="card-info">
|
<view class="card-info">
|
||||||
@@ -124,28 +124,33 @@
|
|||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="empty-state" v-if="rightsList.length === 0 && !loading">
|
<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>
|
<image class="empty-state-img" src="https://aliyunoss.hn12301.net/hhrs/hhrs_wzj.png" mode="widthFix"></image>
|
||||||
<text class="empty-state-text">暂无内容</text>
|
<text class="empty-state-text">暂无内容</text>
|
||||||
</view>
|
</view>
|
||||||
</scroll-view>
|
<view v-if="pager[0].hasScrolled" class="nomore-area">
|
||||||
|
<u-loadmore :status="loadStatuses[0]" :load-text="{ loadmore: '', loading: '加载中...', nomore: '我也是有底线的' }" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
<scroll-view class="list-area" scroll-y @scrolltolower="loadAppointmentMore" :lower-threshold="100"
|
<view class="list-area" v-if="currentTab==1">
|
||||||
v-if="currentTab==1">
|
|
||||||
<view class="rights-card" style="margin-top: 24rpx;flex-direction: column;" v-for="item in appointmentList"
|
<view class="rights-card" style="margin-top: 24rpx;flex-direction: column;" v-for="item in appointmentList"
|
||||||
:key="item.id" >
|
:key="item.id" >
|
||||||
<view style="width: 654rpx;display: flex;justify-content: space-between;">
|
<view style="width: 654rpx;display: flex;justify-content: space-between;">
|
||||||
<text>{{ item.resourceName }}</text><text style="color: #34C759;">已预约</text></view>
|
<text>{{ item.resourceName }}</text><text style="color: #34C759;">已预约</text></view>
|
||||||
<view style="font-size: 24rpx;color: #727272;margin-top: 24rpx;">预约游玩日期:{{ item.appTime.split(' ')[0] }}
|
<!-- 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>
|
<text v-if="item.timeName">({{ item.timeName }})</text></view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="empty-state" v-if="appointmentList.length === 0 && !appointmentLoading">
|
<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>
|
<image class="empty-state-img" src="https://aliyunoss.hn12301.net/hhrs/hhrs_wzj.png" mode="widthFix"></image>
|
||||||
<text class="empty-state-text">暂无内容</text>
|
<text class="empty-state-text">暂无内容</text>
|
||||||
</view>
|
</view>
|
||||||
</scroll-view>
|
<view v-if="pager[1].hasScrolled" class="nomore-area">
|
||||||
<scroll-view class="list-area" scroll-y @scrolltolower="loadCheckMore" :lower-threshold="100"
|
<u-loadmore :status="loadStatuses[1]" :load-text="{ loadmore: '', loading: '加载中...', nomore: '我也是有底线的' }" />
|
||||||
v-if="currentTab==2">
|
</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"
|
<view class="rights-card" style="margin-top: 24rpx;flex-direction: column;" v-for="item in checkList"
|
||||||
:key="item.id" >
|
:key="item.id" >
|
||||||
<view style="width: 654rpx;display: flex;justify-content: space-between;">
|
<view style="width: 654rpx;display: flex;justify-content: space-between;">
|
||||||
@@ -153,16 +158,14 @@
|
|||||||
<view style="font-size: 24rpx;color: #727272;margin-top: 24rpx;">核销时间:{{ item.checkTime }}</view>
|
<view style="font-size: 24rpx;color: #727272;margin-top: 24rpx;">核销时间:{{ item.checkTime }}</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="empty-state" v-if="checkList.length === 0 && !checkLoading">
|
<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>
|
<image class="empty-state-img" src="https://aliyunoss.hn12301.net/hhrs/hhrs_wzj.png" mode="widthFix"></image>
|
||||||
<text class="empty-state-text">暂无内容</text>
|
<text class="empty-state-text">暂无内容</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="loading" v-if="checkLoading">
|
<!-- xuchao: 原先自写的“加载中/没有更多”与另两个 Tab 不一致,统一换成 u-loadmore -->
|
||||||
<text class="loading-text">加载中...</text>
|
<view v-if="pager[2].hasScrolled" class="nomore-area">
|
||||||
|
<u-loadmore :status="loadStatuses[2]" :load-text="{ loadmore: '', loading: '加载中...', nomore: '我也是有底线的' }" />
|
||||||
</view>
|
</view>
|
||||||
<view class="no-more" v-else-if="checkList.length >= checkTotal && checkList.length > 0">
|
</view>
|
||||||
<text class="no-more-text">没有更多了</text>
|
|
||||||
</view>
|
|
||||||
</scroll-view>
|
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
@@ -187,20 +190,18 @@
|
|||||||
currentTab: 0,
|
currentTab: 0,
|
||||||
keyword: '',
|
keyword: '',
|
||||||
rightsList: [],
|
rightsList: [],
|
||||||
pageNo: 1,
|
appointmentList: [],
|
||||||
pageSize: 1000,
|
checkList: [],
|
||||||
total: 0,
|
// 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: '',
|
lng: '',
|
||||||
lat: '',
|
lat: ''
|
||||||
loading: false,
|
|
||||||
appointmentList: [],
|
|
||||||
appointmentPageNo: 1,
|
|
||||||
appointmentTotal: 0,
|
|
||||||
appointmentLoading: false,
|
|
||||||
checkList: [],
|
|
||||||
checkPageNo: 1,
|
|
||||||
checkTotal: 0,
|
|
||||||
checkLoading: false
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -208,6 +209,24 @@
|
|||||||
totalValueText() {
|
totalValueText() {
|
||||||
const yuan = this.totalValueFen / 100
|
const yuan = this.totalValueFen / 100
|
||||||
return Number.isInteger(yuan) ? String(yuan) : yuan.toFixed(2)
|
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) {
|
onLoad(options) {
|
||||||
@@ -231,6 +250,17 @@
|
|||||||
this.loadRightsList()
|
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: {
|
methods: {
|
||||||
// xuchao: 缓存的权益状态可能过期(中奖/权益包会变化),每次进页面仅凭手机号向服务端
|
// xuchao: 缓存的权益状态可能过期(中奖/权益包会变化),每次进页面仅凭手机号向服务端
|
||||||
// 复查最新状态并覆盖缓存(不带证件号,避免触发被动认证)
|
// 复查最新状态并覆盖缓存(不带证件号,避免触发被动认证)
|
||||||
@@ -427,110 +457,86 @@
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
loadRightsList() {
|
// xuchao: Tab 索引 <-> 列表字段的唯一映射
|
||||||
const cardNumber = uni.getStorageSync('cardNumber') || ''
|
listOf(tab) {
|
||||||
this.fetchRightsList(cardNumber)
|
return tab === 1 ? this.appointmentList : tab === 2 ? this.checkList : this.rightsList
|
||||||
},
|
},
|
||||||
fetchRightsList(cardNumber) {
|
setList(tab, list) {
|
||||||
if (this.loading) return
|
if (tab === 1) {
|
||||||
this.loading = true
|
this.appointmentList = list
|
||||||
this.$u.api.getRightsList({
|
} else if (tab === 2) {
|
||||||
pageNo: this.pageNo,
|
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,
|
pageSize: this.pageSize,
|
||||||
keyword: this.keyword,
|
cardNumber: uni.getStorageSync('cardNumber') || ''
|
||||||
lng: this.lng,
|
}
|
||||||
lat: this.lat,
|
let request
|
||||||
cardNumber: cardNumber
|
if (tab === 1) {
|
||||||
}).then(res => {
|
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
|
||||||
|
request = this.$u.api.getRightsList(params)
|
||||||
|
}
|
||||||
|
request.then(res => {
|
||||||
|
if (seq !== p.reqSeq) return
|
||||||
if (res.errcode === 0 && res.data) {
|
if (res.errcode === 0 && res.data) {
|
||||||
const list = res.data.list || []
|
const list = res.data.list || []
|
||||||
if (this.pageNo === 1) {
|
const prev = this.listOf(tab)
|
||||||
this.rightsList = list
|
// 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 {
|
} else {
|
||||||
this.rightsList = this.rightsList.concat(list)
|
p.total = res.data.total || 0
|
||||||
}
|
}
|
||||||
this.total = res.data.total || 0
|
this.setList(tab, merged)
|
||||||
}
|
}
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
this.loading = false
|
if (seq !== p.reqSeq) return
|
||||||
|
p.loading = false
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
loadMore() {
|
loadRightsList() {
|
||||||
if (this.rightsList.length < this.total && !this.loading) {
|
this.loadList(0, true)
|
||||||
this.pageNo++
|
|
||||||
this.loadRightsList()
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
switchTab(index) {
|
switchTab(index) {
|
||||||
this.currentTab = index
|
this.currentTab = index
|
||||||
if (index === 1 && this.appointmentList.length === 0) {
|
// xuchao: 首次进入该 Tab 才拉首页;已有数据保留各自游标继续分页
|
||||||
this.appointmentPageNo = 1
|
if (this.listOf(index).length === 0 && !this.pager[index].loading) {
|
||||||
this.loadAppointmentList()
|
this.loadList(index, true)
|
||||||
}
|
|
||||||
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() {
|
onSearch() {
|
||||||
this.pageNo = 1
|
this.loadList(0, true)
|
||||||
this.loadRightsList()
|
|
||||||
},
|
},
|
||||||
goDetail(item) {
|
goDetail(item) {
|
||||||
uni.navigateTo({
|
uni.navigateTo({
|
||||||
@@ -847,12 +853,17 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* 权益列表 */
|
/* 权益列表 */
|
||||||
|
/* xuchao: 原 scroll-view 下 gap 不生效(卡片被包在 scroll-view-content 内),
|
||||||
|
改普通容器后去掉 gap,卡片间距仍由 .rights-card 的 margin-bottom 提供,视觉不变 */
|
||||||
.list-area {
|
.list-area {
|
||||||
|
|
||||||
width: 750rpx;
|
width: 750rpx;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 10px;
|
}
|
||||||
|
|
||||||
|
.nomore-area {
|
||||||
|
width: 100%;
|
||||||
|
padding-bottom: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rights-card {
|
.rights-card {
|
||||||
@@ -924,26 +935,6 @@
|
|||||||
color: #999999;
|
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 {
|
.empty-state {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
+23
-15
@@ -8,12 +8,12 @@
|
|||||||
<text class="back-text">返回</text>
|
<text class="back-text">返回</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="banner-header">
|
<view class="banner-header">
|
||||||
<image class="banner-icon" src="https://zhihuiyou.oss-cn-shenzhen.aliyuncs.com/hhrs/hhrs_zjgs.png" mode="aspectFit"></image>
|
<image class="banner-icon" src="https://aliyunoss.hn12301.net/hhrs/hhrs_zjgs.png" mode="aspectFit"></image>
|
||||||
<text class="banner-title">中奖名单公示</text>
|
<text class="banner-title">中奖名单公示</text>
|
||||||
</view>
|
</view>
|
||||||
<text class="banner-subtitle">总计 10,000份权益包,分三期公证抽取</text>
|
<text class="banner-subtitle">总计 10,000份权益包,分三期公证抽取</text>
|
||||||
<view class="stats-card">
|
<view class="stats-card">
|
||||||
<image class="stats-gift" src="https://zhihuiyou.oss-cn-shenzhen.aliyuncs.com/hhrs/hhrs_xyckr.png" mode="aspectFit"></image>
|
<image class="stats-gift" src="https://aliyunoss.hn12301.net/hhrs/hhrs_xyckr.png" mode="aspectFit"></image>
|
||||||
<text class="stats-label">已产生</text>
|
<text class="stats-label">已产生</text>
|
||||||
<text class="stats-number">{{ totalWinners.toLocaleString() }}</text>
|
<text class="stats-number">{{ totalWinners.toLocaleString() }}</text>
|
||||||
<text class="stats-label">名幸运持卡人</text>
|
<text class="stats-label">名幸运持卡人</text>
|
||||||
@@ -78,7 +78,8 @@ export default {
|
|||||||
currentTab: 0,
|
currentTab: 0,
|
||||||
tabs: [],
|
tabs: [],
|
||||||
winnerData: {},
|
winnerData: {},
|
||||||
pageNo: 1,
|
// xuchao: 页码改为按期维护(原单一 pageNo 在切期后会被沿用,导致同一页重复追加/跳页)
|
||||||
|
pageNos: {},
|
||||||
pageSize: 10,
|
pageSize: 10,
|
||||||
totals: {},
|
totals: {},
|
||||||
totalWinners: 0,
|
totalWinners: 0,
|
||||||
@@ -123,11 +124,13 @@ export default {
|
|||||||
data[index] = []
|
data[index] = []
|
||||||
})
|
})
|
||||||
this.winnerData = data
|
this.winnerData = data
|
||||||
// xuchao: 预置各期 key,保证 totals/loadStatusMap 的响应式
|
// xuchao: 预置各期 key,保证 totals/pageNos/loadStatusMap 的响应式
|
||||||
this.totals = {}
|
this.totals = {}
|
||||||
|
this.pageNos = {}
|
||||||
this.loadStatusMap = {}
|
this.loadStatusMap = {}
|
||||||
dates.forEach((date, index) => {
|
dates.forEach((date, index) => {
|
||||||
this.$set(this.totals, index, 0)
|
this.$set(this.totals, index, 0)
|
||||||
|
this.$set(this.pageNos, index, 1)
|
||||||
this.$set(this.loadStatusMap, index, 'loading')
|
this.$set(this.loadStatusMap, index, 'loading')
|
||||||
})
|
})
|
||||||
// xuchao: 预取各期第一页,汇总已产生总人数
|
// xuchao: 预取各期第一页,汇总已产生总人数
|
||||||
@@ -142,21 +145,24 @@ export default {
|
|||||||
loadWinnerList(tabIndex, pageNo, allowConcurrent) {
|
loadWinnerList(tabIndex, pageNo, allowConcurrent) {
|
||||||
if (!this.tabs[tabIndex]) return
|
if (!this.tabs[tabIndex]) return
|
||||||
if (this.inFlight > 0 && !allowConcurrent) return
|
if (this.inFlight > 0 && !allowConcurrent) return
|
||||||
|
// xuchao: 本期页码独立计算,缺省续拉该期下一页
|
||||||
|
const page = pageNo || (this.pageNos[tabIndex] || 1)
|
||||||
this.inFlight++
|
this.inFlight++
|
||||||
this.$set(this.loadStatusMap, tabIndex, 'loading')
|
this.$set(this.loadStatusMap, tabIndex, 'loading')
|
||||||
const drawDate = this.tabs[tabIndex].date
|
const drawDate = this.tabs[tabIndex].date
|
||||||
this.$u.api.getWinnerList({
|
this.$u.api.getWinnerList({
|
||||||
pageNo: pageNo || this.pageNo,
|
pageNo: page,
|
||||||
pageSize: this.pageSize,
|
pageSize: this.pageSize,
|
||||||
drawDate: drawDate
|
drawDate: drawDate
|
||||||
}).then(res => {
|
}).then(res => {
|
||||||
if (res.errcode === 0 && res.data) {
|
if (res.errcode === 0 && res.data) {
|
||||||
const list = res.data.list || []
|
const list = res.data.list || []
|
||||||
const page = pageNo || this.pageNo
|
const prev = this.winnerData[tabIndex] || []
|
||||||
|
this.$set(this.pageNos, tabIndex, page)
|
||||||
if (page === 1) {
|
if (page === 1) {
|
||||||
this.$set(this.winnerData, tabIndex, list)
|
this.$set(this.winnerData, tabIndex, list)
|
||||||
} else {
|
} else {
|
||||||
this.winnerData[tabIndex] = this.winnerData[tabIndex].concat(list)
|
this.$set(this.winnerData, tabIndex, prev.concat(list))
|
||||||
}
|
}
|
||||||
this.$set(this.totals, tabIndex, res.data.total || 0)
|
this.$set(this.totals, tabIndex, res.data.total || 0)
|
||||||
// xuchao: 各期总人数求和,用于顶部幸运持卡人数
|
// xuchao: 各期总人数求和,用于顶部幸运持卡人数
|
||||||
@@ -164,23 +170,25 @@ export default {
|
|||||||
}
|
}
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
this.inFlight--
|
this.inFlight--
|
||||||
const len = this.winnerData[tabIndex].length
|
const len = (this.winnerData[tabIndex] || []).length
|
||||||
const total = this.totals[tabIndex] || 0
|
const total = this.totals[tabIndex] || 0
|
||||||
this.$set(this.loadStatusMap, tabIndex, len === 0 || len >= total ? 'nomore' : 'loadmore')
|
this.$set(this.loadStatusMap, tabIndex, len === 0 || len >= total ? 'nomore' : 'loadmore')
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
loadMore() {
|
loadMore() {
|
||||||
const total = this.totals[this.currentTab] || 0
|
const tab = this.currentTab
|
||||||
if (this.winnerData[this.currentTab].length < total && this.inFlight === 0) {
|
const total = this.totals[tab] || 0
|
||||||
this.pageNo++
|
const len = (this.winnerData[tab] || []).length
|
||||||
this.loadWinnerList(this.currentTab)
|
if (len < total && this.inFlight === 0) {
|
||||||
|
this.loadWinnerList(tab, (this.pageNos[tab] || 1) + 1)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
switchTab(index) {
|
switchTab(index) {
|
||||||
this.currentTab = index
|
this.currentTab = index
|
||||||
this.pageNo = 1
|
// xuchao: 不再重置全局 pageNo(各期游标自带);空列表补拉首页时允许并发,
|
||||||
if (this.winnerData[index].length === 0) {
|
// 避免首屏预取仍在途时切期被丢弃后一直显示“暂无数据”
|
||||||
this.loadWinnerList(index, 1)
|
if ((this.winnerData[index] || []).length === 0) {
|
||||||
|
this.loadWinnerList(index, 1, true)
|
||||||
} else {
|
} else {
|
||||||
const total = this.totals[index] || 0
|
const total = this.totals[index] || 0
|
||||||
this.$set(this.loadStatusMap, index, this.winnerData[index].length >= total ? 'nomore' : 'loadmore')
|
this.$set(this.loadStatusMap, index, this.winnerData[index].length >= total ? 'nomore' : 'loadmore')
|
||||||
|
|||||||
Reference in New Issue
Block a user