feat: add agreement-gated login state

This commit is contained in:
gqt
2026-06-10 11:46:39 +08:00
parent 11fe7399f3
commit 0d5fe7d1cf
14 changed files with 662 additions and 61 deletions
+2 -1
View File
@@ -11,7 +11,8 @@
"verify:tabbar": "node scripts/verify-venue-tabbar.mjs", "verify:tabbar": "node scripts/verify-venue-tabbar.mjs",
"verify:startup": "node scripts/verify-venue-startup.mjs", "verify:startup": "node scripts/verify-venue-startup.mjs",
"verify:home-switch": "node scripts/verify-home-venue-switch.mjs", "verify:home-switch": "node scripts/verify-home-venue-switch.mjs",
"verify:comprehensive-auth": "node scripts/verify-comprehensive-auth-state.mjs" "verify:comprehensive-auth": "node scripts/verify-comprehensive-auth-state.mjs",
"verify:auth-agreements": "node scripts/verify-auth-agreements.mjs"
}, },
"dependencies": { "dependencies": {
"@dcloudio/uni-app": "3.0.0-alpha-5000120260211001", "@dcloudio/uni-app": "3.0.0-alpha-5000120260211001",
@@ -0,0 +1,100 @@
import { existsSync, readFileSync } from 'node:fs'
import { fileURLToPath } from 'node:url'
import { dirname, resolve } from 'node:path'
const root = resolve(dirname(fileURLToPath(import.meta.url)), '..')
const srcRoot = resolve(root, 'src')
const failures = []
const read = (relativePath) => readFileSync(resolve(srcRoot, relativePath), 'utf8')
const expectFile = (relativePath) => {
const path = resolve(srcRoot, relativePath)
if (!existsSync(path)) {
failures.push(`${relativePath} is missing`)
return ''
}
return readFileSync(path, 'utf8')
}
const expectIncludes = (content, expected, label) => {
if (!content.includes(expected)) failures.push(`${label} is missing: ${expected}`)
}
const expectExcludes = (content, unexpected, label) => {
if (content.includes(unexpected)) failures.push(`${label} should not contain: ${unexpected}`)
}
const agreements = expectFile('components/AgreementLinks.vue')
expectIncludes(agreements, '用户协议', 'AgreementLinks user agreement link')
expectIncludes(agreements, '隐私政策', 'AgreementLinks privacy policy link')
expectIncludes(agreements, 'u-popup', 'AgreementLinks popup')
expectIncludes(agreements, '场馆小程序用户服务协议', 'AgreementLinks user agreement content')
expectIncludes(agreements, '场馆小程序隐私政策', 'AgreementLinks privacy policy content')
expectIncludes(agreements, 'defineExpose', 'AgreementLinks exposed popup methods')
const unauthPanel = expectFile('components/UnauthLoginPanel.vue')
expectIncludes(unauthPanel, '/static/brand/logo.png', 'UnauthLoginPanel brand logo')
expectIncludes(unauthPanel, '大兴区冰上运动中心', 'UnauthLoginPanel app name')
expectIncludes(unauthPanel, 'brand-section', 'UnauthLoginPanel brand section')
expectIncludes(unauthPanel, 'login-card', 'UnauthLoginPanel login card')
expectIncludes(unauthPanel, 'justify-content: center;', 'UnauthLoginPanel centered layout')
expectIncludes(unauthPanel, 'calc(100vh - 176rpx - env(safe-area-inset-bottom))', 'UnauthLoginPanel available viewport height')
expectIncludes(unauthPanel, 'align-self: center;', 'UnauthLoginPanel centered agreement rows')
expectIncludes(unauthPanel, 'hasAcceptedUserAgreement = ref(false)', 'UnauthLoginPanel user agreement manual state')
expectIncludes(unauthPanel, 'hasAcceptedPrivacyPolicy = ref(false)', 'UnauthLoginPanel privacy policy manual state')
expectIncludes(unauthPanel, 'canLogin', 'UnauthLoginPanel login gate')
expectIncludes(unauthPanel, 'acceptance-user', 'UnauthLoginPanel user agreement checkbox')
expectIncludes(unauthPanel, 'acceptance-privacy', 'UnauthLoginPanel privacy policy checkbox')
expectIncludes(unauthPanel, "emit('login')", 'UnauthLoginPanel login emit')
expectIncludes(unauthPanel, '请先勾选用户协议和隐私政策', 'UnauthLoginPanel unchecked feedback')
expectIncludes(unauthPanel, 'AgreementLinks', 'UnauthLoginPanel shared agreement popup')
expectExcludes(unauthPanel, '预约课程、查看', 'UnauthLoginPanel extra marketing copy')
expectExcludes(unauthPanel, '冰上课程', 'UnauthLoginPanel extra feature chip')
expectExcludes(unauthPanel, '预约行程', 'UnauthLoginPanel extra feature chip')
expectExcludes(unauthPanel, '会员权益', 'UnauthLoginPanel extra feature chip')
expectExcludes(unauthPanel, '登录仅用于', 'UnauthLoginPanel extra safe tip')
expectExcludes(unauthPanel, 'brand-logo-shell', 'UnauthLoginPanel logo shell decoration')
expectExcludes(unauthPanel, 'radial-gradient', 'UnauthLoginPanel background decoration')
expectExcludes(unauthPanel, 'linear-gradient(180deg, #EEF8FF', 'UnauthLoginPanel blue block background')
expectExcludes(unauthPanel, 'background: #FFFFFF;', 'UnauthLoginPanel nested white card background')
expectExcludes(unauthPanel, 'space-between', 'UnauthLoginPanel scattered vertical layout')
expectExcludes(unauthPanel, 'width: 100%;\n max-width: 420rpx;', 'UnauthLoginPanel left-aligned agreement rows')
const loginPages = [
['pages/ice/bookings/index.vue', 'v-else-if="sessionStatus === SESSION_STATUS.GUEST"', '@login="onWechatLogin"'],
['pages/ice/bookings/detail.vue', 'v-else-if="sessionStatus === SESSION_STATUS.GUEST"', '@login="onWechatLogin"'],
['pages/ice/profile/index.vue', 'v-else-if="sessionStatus === SESSION_STATUS.GUEST"', '@login="onWechatLogin"'],
['pages/ice/profile/detail.vue', 'v-else-if="sessionStatus === SESSION_STATUS.GUEST"', '@login="onWechatLogin"'],
['pages/ice/profile/cards.vue', 'v-else-if="sessionStatus === SESSION_STATUS.GUEST"', '@login="onWechatLogin"'],
['pages/ice/profile/card-history.vue', 'v-else-if="sessionStatus === SESSION_STATUS.GUEST"', '@login="onWechatLogin"'],
['pages/comprehensive/bookings/index.vue', 'v-if="!session.isLoggedIn"', '@login="login"'],
['pages/comprehensive/profile/index.vue', 'v-if="!session.isLoggedIn"', '@login="login"']
]
for (const [page, condition, handler] of loginPages) {
const content = read(page)
expectIncludes(content, '<UnauthLoginPanel', `${page} unauth login component`)
expectIncludes(content, condition, `${page} unauth condition`)
expectIncludes(content, handler, `${page} login handler`)
expectIncludes(content, "import UnauthLoginPanel from '@/components/UnauthLoginPanel.vue'", `${page} UnauthLoginPanel import`)
expectExcludes(content, 'primary-text="微信一键登录"', `${page} direct login StatePanel`)
expectExcludes(content, '@primary="onWechatLogin"', `${page} direct ice login handler`)
expectExcludes(content, '@primary="login"', `${page} direct comprehensive login handler`)
}
for (const page of ['pages/ice/profile/index.vue', 'pages/comprehensive/profile/index.vue']) {
const content = read(page)
expectIncludes(content, '<AgreementLinks', `${page} bottom agreement links`)
expectIncludes(content, '<AgreementLinks v-if=', `${page} bottom agreement links auth guard`)
expectIncludes(content, 'profile-agreement-links', `${page} bottom agreement links style`)
expectIncludes(content, "import AgreementLinks from '@/components/AgreementLinks.vue'", `${page} AgreementLinks import`)
}
if (failures.length) {
console.error(`Auth agreement verification failed with ${failures.length} issue(s):`)
for (const failure of failures) console.error(`- ${failure}`)
process.exit(1)
}
console.log('Auth agreement verification passed.')
@@ -22,14 +22,14 @@ const expectExcludes = (content, unexpected, label) => {
for (const page of pages) { for (const page of pages) {
const content = readFileSync(resolve(srcRoot, page), 'utf8') const content = readFileSync(resolve(srcRoot, page), 'utf8')
expectIncludes(content, 'v-if="!session.isLoggedIn"', `${page} unauth condition`) expectIncludes(content, 'v-if="!session.isLoggedIn"', `${page} unauth condition`)
expectIncludes(content, '<StatePanel', `${page} unauth component`) expectIncludes(content, '<UnauthLoginPanel', `${page} unauth component`)
expectIncludes(content, 'type="login"', `${page} login state type`) expectIncludes(content, '@login="login"', `${page} login event`)
expectIncludes(content, ':show-mark="false"', `${page} login state mark`) expectIncludes(content, "import UnauthLoginPanel from '@/components/UnauthLoginPanel.vue'", `${page} UnauthLoginPanel import`)
expectIncludes(content, "import StatePanel from '@/components/ice/StatePanel.vue'", `${page} StatePanel import`)
expectExcludes(content, '星河综合运动中心', `${page} unauth copy`) expectExcludes(content, '星河综合运动中心', `${page} unauth copy`)
expectExcludes(content, '登录后查看', `${page} unauth copy`) expectExcludes(content, '登录后查看', `${page} unauth copy`)
expectExcludes(content, 'class="brand"', `${page} brand style`) expectExcludes(content, 'class="brand"', `${page} brand style`)
expectExcludes(content, 'class="desc"', `${page} desc style`) expectExcludes(content, 'class="desc"', `${page} desc style`)
expectExcludes(content, 'primary-text="微信一键登录"', `${page} direct StatePanel login`)
} }
if (failures.length) { if (failures.length) {
@@ -0,0 +1,256 @@
<template>
<view class="agreement-links">
<view v-if="showEntry" class="agreement-entry" :class="{ compact }">
<text class="agreement-link" @tap="openUserAgreement">用户协议</text>
<text class="agreement-divider">|</text>
<text class="agreement-link" @tap="openPrivacyPolicy">隐私政策</text>
</view>
<u-popup :show="Boolean(popupType)" mode="bottom" :round="24" @close="closePopup">
<view class="agreement-popup">
<view class="agreement-popup__header">
<text class="agreement-popup__title">{{ currentAgreement.popupTitle }}</text>
<view class="agreement-popup__close" @tap="closePopup">
<u-icon name="close" size="36rpx" color="#666666"></u-icon>
</view>
</view>
<scroll-view scroll-y class="agreement-popup__body">
<view class="agreement-content">
<text class="agreement-h1">{{ currentAgreement.title }}</text>
<text class="agreement-date">更新日期2026年6月9日</text>
<text class="agreement-p">{{ currentAgreement.intro }}</text>
<block v-for="section in currentAgreement.sections" :key="section.title">
<text class="agreement-h2">{{ section.title }}</text>
<text v-for="item in section.items" :key="item" class="agreement-p">{{ item }}</text>
</block>
</view>
</scroll-view>
</view>
</u-popup>
</view>
</template>
<script setup>
import { computed, ref } from 'vue'
defineProps({
showEntry: {
type: Boolean,
default: true
},
compact: {
type: Boolean,
default: false
}
})
const popupType = ref('')
const agreementMap = {
user: {
popupTitle: '用户协议',
title: '场馆小程序用户服务协议',
intro: '欢迎您使用场馆小程序。使用预约、购票、会员卡、订单和个人中心等服务前,请您仔细阅读并充分理解本协议。',
sections: [
{
title: '一、服务说明',
items: [
'1. 本平台为用户提供场馆信息浏览、课程预约、包场预约、门票购买、会员卡查询和订单管理等服务。',
'2. 您可以通过微信授权登录使用平台服务,部分功能需要完成会员资料绑定后方可使用。'
]
},
{
title: '二、用户账户',
items: [
'1. 您应保证授权登录和提交的个人资料真实、准确、完整。',
'2. 因您未妥善保管账户或授权信息造成的损失,由您自行承担。'
]
},
{
title: '三、预约、门票与交易',
items: [
'1. 课程、包场、门票和会员权益以下单、预约或场馆确认时展示的信息为准。',
'2. 订单提交后,请按页面提示完成支付或到场核验;逾期、取消、退款等规则以页面说明和场馆实际规则为准。'
]
},
{
title: '四、免责声明',
items: [
'因不可抗力、系统维护、网络异常、场馆临时调整或政策变化导致服务中断、预约调整或无法履行的,平台将在合理范围内协助处理。'
]
},
{
title: '五、协议变更',
items: [
'平台可根据业务和法律法规要求更新本协议。更新后的协议将在小程序内展示,您继续使用平台服务即表示接受更新内容。'
]
}
]
},
privacy: {
popupTitle: '隐私政策',
title: '场馆小程序隐私政策',
intro: '本平台重视您的隐私保护。本政策说明我们如何收集、使用、存储和保护您的个人信息。',
sections: [
{
title: '一、信息收集',
items: [
'1. 我们会收集您的微信授权信息,用于创建账户、识别登录状态和展示个人资料。',
'2. 您主动提供或授权的姓名、手机号等信息用于匹配场馆会员档案、预约记录和门票权益。',
'3. 您的预约、订单、支付、核销和会员卡记录用于提供服务、售后处理和体验优化。'
]
},
{
title: '二、信息使用',
items: [
'1. 处理您的预约、购票、订单支付、会员权益查询和消息通知。',
'2. 改善和优化平台产品、服务流程与安全风控。',
'3. 未经您的同意,我们不会向无关第三方提供您的个人信息,法律法规另有规定的除外。'
]
},
{
title: '三、信息存储与保护',
items: [
'1. 您的个人信息将依法存储在中国境内服务器。',
'2. 我们采用访问控制、传输加密和权限管理等措施保护您的个人信息安全。',
'3. 当您注销账户或服务目的不再需要时,我们将依法删除或匿名化处理相关信息。'
]
},
{
title: '四、用户权利',
items: [
'1. 您有权查看、更正、删除您的个人信息,或撤回对个人信息处理的授权同意。',
'2. 如需行使相关权利,可通过场馆客服或小程序内联系方式与我们联系。'
]
},
{
title: '五、政策更新',
items: [
'我们可能会不时更新本隐私政策,更新后的政策将在小程序内展示,请您定期查看。'
]
}
]
}
}
const currentAgreement = computed(() => agreementMap[popupType.value] || agreementMap.user)
const openUserAgreement = () => {
popupType.value = 'user'
}
const openPrivacyPolicy = () => {
popupType.value = 'privacy'
}
const closePopup = () => {
popupType.value = ''
}
defineExpose({
openUserAgreement,
openPrivacyPolicy
})
</script>
<style lang="scss" scoped>
.agreement-entry {
display: flex;
justify-content: center;
align-items: center;
padding: 40rpx 0 60rpx;
gap: 16rpx;
&.compact {
padding: 16rpx 0 0;
}
}
.agreement-link {
font-size: 24rpx;
color: $text-color-muted;
}
.agreement-divider {
font-size: 24rpx;
color: $border-color;
}
.agreement-popup {
height: 80vh;
display: flex;
flex-direction: column;
background-color: #FFFFFF;
}
.agreement-popup__header {
display: flex;
align-items: center;
justify-content: center;
padding: 32rpx;
position: relative;
flex-shrink: 0;
}
.agreement-popup__title {
font-size: 34rpx;
font-weight: 700;
color: $text-color-main;
}
.agreement-popup__close {
position: absolute;
right: 32rpx;
top: 32rpx;
width: 56rpx;
height: 56rpx;
display: flex;
align-items: center;
justify-content: center;
}
.agreement-popup__body {
flex: 1;
overflow: hidden;
}
.agreement-content {
padding: 0 40rpx 60rpx;
display: flex;
flex-direction: column;
}
.agreement-h1 {
font-size: 34rpx;
font-weight: 800;
color: $text-color-main;
text-align: center;
margin-bottom: 8rpx;
display: block;
}
.agreement-date {
font-size: 22rpx;
color: $text-color-light;
text-align: center;
margin-bottom: 32rpx;
display: block;
}
.agreement-h2 {
font-size: 30rpx;
font-weight: 800;
color: $text-color-main;
margin-top: 32rpx;
margin-bottom: 16rpx;
display: block;
}
.agreement-p {
font-size: 26rpx;
color: $text-color-regular;
line-height: 1.8;
margin-bottom: 8rpx;
display: block;
}
</style>
@@ -0,0 +1,260 @@
<template>
<view class="unauth-login-panel" :class="{ compact }">
<view class="brand-section">
<image class="brand-logo" src="/static/brand/logo.png" mode="aspectFit" />
<text class="brand-name">大兴区冰上运动中心</text>
</view>
<view class="login-card">
<view class="unauth-copy" v-if="title || description">
<text class="unauth-title" v-if="title">{{ title }}</text>
<text class="unauth-description" v-if="description">{{ description }}</text>
</view>
<button
class="login-button"
:class="{ disabled: loading }"
:loading="loading"
:disabled="loading"
@tap="handleLoginTap"
>
{{ primaryText }}
</button>
<view class="agreement-checks">
<view class="acceptance-row acceptance-user" @tap="toggleUserAgreement">
<view class="checkbox" :class="{ checked: hasAcceptedUserAgreement }">
<text v-if="hasAcceptedUserAgreement"></text>
</view>
<text class="acceptance-text">我已阅读并同意</text>
<text class="agreement-inline-link" @tap.stop="openUserAgreement">用户协议</text>
</view>
<view class="acceptance-row acceptance-privacy" @tap="togglePrivacyPolicy">
<view class="checkbox" :class="{ checked: hasAcceptedPrivacyPolicy }">
<text v-if="hasAcceptedPrivacyPolicy"></text>
</view>
<text class="acceptance-text">我已阅读并同意</text>
<text class="agreement-inline-link" @tap.stop="openPrivacyPolicy">隐私政策</text>
</view>
</view>
</view>
<AgreementLinks ref="agreementLinksRef" :show-entry="false" />
</view>
</template>
<script setup>
import { computed, ref } from 'vue'
import AgreementLinks from '@/components/AgreementLinks.vue'
defineProps({
title: {
type: String,
default: ''
},
description: {
type: String,
default: ''
},
primaryText: {
type: String,
default: '微信一键登录'
},
loading: {
type: Boolean,
default: false
},
compact: {
type: Boolean,
default: false
}
})
const emit = defineEmits(['login'])
const hasAcceptedUserAgreement = ref(false)
const hasAcceptedPrivacyPolicy = ref(false)
const agreementLinksRef = ref(null)
const canLogin = computed(() => hasAcceptedUserAgreement.value && hasAcceptedPrivacyPolicy.value)
const toggleUserAgreement = () => {
hasAcceptedUserAgreement.value = !hasAcceptedUserAgreement.value
}
const togglePrivacyPolicy = () => {
hasAcceptedPrivacyPolicy.value = !hasAcceptedPrivacyPolicy.value
}
const openUserAgreement = () => {
agreementLinksRef.value?.openUserAgreement()
}
const openPrivacyPolicy = () => {
agreementLinksRef.value?.openPrivacyPolicy()
}
const handleLoginTap = () => {
if (!canLogin.value) {
uni.showToast({
title: '请先勾选用户协议和隐私政策',
icon: 'none'
})
return
}
emit('login')
}
</script>
<style lang="scss" scoped>
.unauth-login-panel {
min-height: calc(100vh - 176rpx - env(safe-area-inset-bottom));
padding: 0 $spacing-lg 48rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
gap: 58rpx;
position: relative;
overflow: hidden;
background: transparent;
&.compact {
min-height: 640rpx;
padding: 0 $spacing-lg 40rpx;
gap: 44rpx;
}
}
.brand-section {
width: 100%;
position: relative;
z-index: 1;
display: flex;
flex-direction: column;
align-items: center;
}
.brand-logo {
width: 144rpx;
height: 144rpx;
border-radius: 50%;
}
.brand-name {
margin-top: 34rpx;
font-size: 40rpx;
line-height: 1.26;
font-weight: 900;
color: $text-color-main;
}
.login-card {
width: 100%;
max-width: 560rpx;
position: relative;
z-index: 1;
display: flex;
flex-direction: column;
align-items: stretch;
}
.unauth-copy {
margin-bottom: 24rpx;
display: flex;
flex-direction: column;
align-items: center;
gap: $spacing-sm;
max-width: 560rpx;
}
.unauth-title {
font-size: 30rpx;
line-height: 1.35;
font-weight: 800;
color: $text-color-main;
}
.unauth-description {
font-size: 24rpx;
line-height: 1.6;
color: $text-color-muted;
}
.login-button {
width: 100%;
height: 88rpx;
border-radius: $border-radius-base;
color: #FFFFFF;
background-color: $text-color-main;
font-size: 28rpx;
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
&::after {
border: none;
}
&.disabled,
&[disabled] {
color: #FFFFFF;
background-color: $text-color-light;
}
}
.agreement-checks {
margin-top: 24rpx;
display: flex;
flex-direction: column;
gap: 18rpx;
align-items: center;
}
.acceptance-row {
display: flex;
align-items: center;
min-height: 38rpx;
text-align: left;
align-self: center;
}
.checkbox {
width: 30rpx;
height: 30rpx;
margin-right: 12rpx;
flex-shrink: 0;
border: 2rpx solid $border-color;
border-radius: 8rpx;
display: flex;
align-items: center;
justify-content: center;
color: #FFFFFF;
font-size: 20rpx;
line-height: 1;
font-weight: 900;
background-color: #FFFFFF;
&.checked {
border-color: $text-color-main;
background-color: $text-color-main;
}
}
.acceptance-text,
.agreement-inline-link {
font-size: 24rpx;
line-height: 1.5;
}
.acceptance-text {
color: $text-color-muted;
}
.agreement-inline-link {
color: $text-color-main;
font-weight: 700;
}
</style>
@@ -1,11 +1,8 @@
<template> <template>
<view class="bookings-page"> <view class="bookings-page">
<StatePanel <UnauthLoginPanel
v-if="!session.isLoggedIn" v-if="!session.isLoggedIn"
primary-text="微信一键登录" @login="login"
type="login"
:show-mark="false"
@primary="login"
/> />
<block v-else> <block v-else>
@@ -44,7 +41,7 @@ import { ticketApi } from '@/modules/comprehensive/api/ticket'
import { useComprehensiveSessionStore } from '@/modules/comprehensive/stores/session' import { useComprehensiveSessionStore } from '@/modules/comprehensive/stores/session'
import { formatDateTime, formatTimeRange } from '@/modules/comprehensive/utils/date' import { formatDateTime, formatTimeRange } from '@/modules/comprehensive/utils/date'
import VenueTabBar from '@/components/VenueTabBar.vue' import VenueTabBar from '@/components/VenueTabBar.vue'
import StatePanel from '@/components/ice/StatePanel.vue' import UnauthLoginPanel from '@/components/UnauthLoginPanel.vue'
const session = useComprehensiveSessionStore() const session = useComprehensiveSessionStore()
const businessType = ref('course') const businessType = ref('course')
@@ -1,11 +1,8 @@
<template> <template>
<view class="profile-page container"> <view class="profile-page container">
<StatePanel <UnauthLoginPanel
v-if="!session.isLoggedIn" v-if="!session.isLoggedIn"
primary-text="微信一键登录" @login="login"
type="login"
:show-mark="false"
@primary="login"
/> />
<block v-else> <block v-else>
@@ -68,6 +65,7 @@
</view> </view>
</view> </view>
</block> </block>
<AgreementLinks v-if="session.isLoggedIn" class="profile-agreement-links" compact />
<VenueTabBar venue="comprehensive" active="profile" /> <VenueTabBar venue="comprehensive" active="profile" />
</view> </view>
</template> </template>
@@ -76,7 +74,8 @@
import { computed, onMounted } from 'vue' import { computed, onMounted } from 'vue'
import { useComprehensiveSessionStore } from '@/modules/comprehensive/stores/session' import { useComprehensiveSessionStore } from '@/modules/comprehensive/stores/session'
import VenueTabBar from '@/components/VenueTabBar.vue' import VenueTabBar from '@/components/VenueTabBar.vue'
import StatePanel from '@/components/ice/StatePanel.vue' import AgreementLinks from '@/components/AgreementLinks.vue'
import UnauthLoginPanel from '@/components/UnauthLoginPanel.vue'
const session = useComprehensiveSessionStore() const session = useComprehensiveSessionStore()
const avatarInitial = computed(() => (session.member?.nickname || '用户').slice(0, 1)) const avatarInitial = computed(() => (session.member?.nickname || '用户').slice(0, 1))
@@ -7,24 +7,21 @@
type="loading" type="loading"
/> />
<StatePanel <UnauthLoginPanel
v-else-if="sessionStatus === SESSION_STATUS.GUEST" v-else-if="sessionStatus === SESSION_STATUS.GUEST"
title="登录后查看预约详情" title="登录后查看预约详情"
description="请先登录并完善会员资料。" description="请先登录并完善会员资料。"
primary-text="微信一键登录"
type="login"
:loading="isLoginSubmitting" :loading="isLoginSubmitting"
@primary="onWechatLogin" @login="onWechatLogin"
/> />
<StatePanel <UnauthLoginPanel
v-else-if="sessionStatus === SESSION_STATUS.EXPIRED" v-else-if="sessionStatus === SESSION_STATUS.EXPIRED"
title="登录状态已过期" title="登录状态已过期"
description="为了保护会员资料,请重新登录后继续查看。" description="为了保护会员资料,请重新登录后继续查看。"
primary-text="重新登录" primary-text="重新登录"
type="expired"
:loading="isLoginSubmitting" :loading="isLoginSubmitting"
@primary="onWechatLogin" @login="onWechatLogin"
/> />
<StatePanel <StatePanel
@@ -147,6 +144,7 @@ import { onShow } from '@dcloudio/uni-app'
import { bookingApi } from '@/modules/ice/api/booking' import { bookingApi } from '@/modules/ice/api/booking'
import { errorMessage, getMemberSession, loginAndStoreToken, sessionStatusFromError, SESSION_STATUS } from '@/modules/ice/api/session' import { errorMessage, getMemberSession, loginAndStoreToken, sessionStatusFromError, SESSION_STATUS } from '@/modules/ice/api/session'
import StatePanel from '@/components/ice/StatePanel.vue' import StatePanel from '@/components/ice/StatePanel.vue'
import UnauthLoginPanel from '@/components/UnauthLoginPanel.vue'
const booking = ref({ const booking = ref({
id: null, id: null,
+5 -8
View File
@@ -7,23 +7,19 @@
type="loading" type="loading"
/> />
<StatePanel <UnauthLoginPanel
v-else-if="sessionStatus === SESSION_STATUS.GUEST" v-else-if="sessionStatus === SESSION_STATUS.GUEST"
primary-text="微信一键登录"
type="login"
:show-mark="false"
:loading="isLoginSubmitting" :loading="isLoginSubmitting"
@primary="onWechatLogin" @login="onWechatLogin"
/> />
<StatePanel <UnauthLoginPanel
v-else-if="sessionStatus === SESSION_STATUS.EXPIRED" v-else-if="sessionStatus === SESSION_STATUS.EXPIRED"
title="登录状态已过期" title="登录状态已过期"
description="为了保护会员资料,请重新登录后继续查看预约行程。" description="为了保护会员资料,请重新登录后继续查看预约行程。"
primary-text="重新登录" primary-text="重新登录"
type="expired"
:loading="isLoginSubmitting" :loading="isLoginSubmitting"
@primary="onWechatLogin" @login="onWechatLogin"
/> />
<StatePanel <StatePanel
@@ -123,6 +119,7 @@ import { authStorage } from '@/modules/ice/api/member'
import { errorMessage, getMemberSession, loginAndStoreToken, sessionStatusFromError, SESSION_STATUS } from '@/modules/ice/api/session' import { errorMessage, getMemberSession, loginAndStoreToken, sessionStatusFromError, SESSION_STATUS } from '@/modules/ice/api/session'
import VenueTabBar from '@/components/VenueTabBar.vue' import VenueTabBar from '@/components/VenueTabBar.vue'
import StatePanel from '@/components/ice/StatePanel.vue' import StatePanel from '@/components/ice/StatePanel.vue'
import UnauthLoginPanel from '@/components/UnauthLoginPanel.vue'
const isLoggedIn = ref(false) const isLoggedIn = ref(false)
const isLoginSubmitting = ref(false) const isLoginSubmitting = ref(false)
@@ -7,24 +7,21 @@
type="loading" type="loading"
/> />
<StatePanel <UnauthLoginPanel
v-else-if="sessionStatus === SESSION_STATUS.GUEST" v-else-if="sessionStatus === SESSION_STATUS.GUEST"
title="登录后查看会员卡流水" title="登录后查看会员卡流水"
description="请先登录并完善会员资料。" description="请先登录并完善会员资料。"
primary-text="微信一键登录"
type="login"
:loading="isLoginSubmitting" :loading="isLoginSubmitting"
@primary="onWechatLogin" @login="onWechatLogin"
/> />
<StatePanel <UnauthLoginPanel
v-else-if="sessionStatus === SESSION_STATUS.EXPIRED" v-else-if="sessionStatus === SESSION_STATUS.EXPIRED"
title="登录状态已过期" title="登录状态已过期"
description="为了保护会员资料,请重新登录后继续查看。" description="为了保护会员资料,请重新登录后继续查看。"
primary-text="重新登录" primary-text="重新登录"
type="expired"
:loading="isLoginSubmitting" :loading="isLoginSubmitting"
@primary="onWechatLogin" @login="onWechatLogin"
/> />
<StatePanel <StatePanel
@@ -148,6 +145,7 @@ import { onPullDownRefresh, onShow } from '@dcloudio/uni-app'
import { cardApi } from '@/modules/ice/api/card' import { cardApi } from '@/modules/ice/api/card'
import { errorMessage, getMemberSession, loginAndStoreToken, sessionStatusFromError, SESSION_STATUS } from '@/modules/ice/api/session' import { errorMessage, getMemberSession, loginAndStoreToken, sessionStatusFromError, SESSION_STATUS } from '@/modules/ice/api/session'
import StatePanel from '@/components/ice/StatePanel.vue' import StatePanel from '@/components/ice/StatePanel.vue'
import UnauthLoginPanel from '@/components/UnauthLoginPanel.vue'
const card = ref({ const card = ref({
id: null, id: null,
+5 -7
View File
@@ -7,24 +7,21 @@
type="loading" type="loading"
/> />
<StatePanel <UnauthLoginPanel
v-else-if="sessionStatus === SESSION_STATUS.GUEST" v-else-if="sessionStatus === SESSION_STATUS.GUEST"
title="登录后查看会员卡" title="登录后查看会员卡"
description="登录并完善会员资料后,可查看场馆为你登记的会员卡。" description="登录并完善会员资料后,可查看场馆为你登记的会员卡。"
primary-text="微信一键登录"
type="login"
:loading="isLoginSubmitting" :loading="isLoginSubmitting"
@primary="onWechatLogin" @login="onWechatLogin"
/> />
<StatePanel <UnauthLoginPanel
v-else-if="sessionStatus === SESSION_STATUS.EXPIRED" v-else-if="sessionStatus === SESSION_STATUS.EXPIRED"
title="登录状态已过期" title="登录状态已过期"
description="为了保护会员资料,请重新登录后继续查看会员卡。" description="为了保护会员资料,请重新登录后继续查看会员卡。"
primary-text="重新登录" primary-text="重新登录"
type="expired"
:loading="isLoginSubmitting" :loading="isLoginSubmitting"
@primary="onWechatLogin" @login="onWechatLogin"
/> />
<StatePanel <StatePanel
@@ -137,6 +134,7 @@ import { cardApi } from '@/modules/ice/api/card'
import { authStorage } from '@/modules/ice/api/member' import { authStorage } from '@/modules/ice/api/member'
import { errorMessage, getMemberSession, loginAndStoreToken, sessionStatusFromError, SESSION_STATUS } from '@/modules/ice/api/session' import { errorMessage, getMemberSession, loginAndStoreToken, sessionStatusFromError, SESSION_STATUS } from '@/modules/ice/api/session'
import StatePanel from '@/components/ice/StatePanel.vue' import StatePanel from '@/components/ice/StatePanel.vue'
import UnauthLoginPanel from '@/components/UnauthLoginPanel.vue'
const cards = ref([]) const cards = ref([])
const isLoading = ref(false) const isLoading = ref(false)
+5 -7
View File
@@ -7,24 +7,21 @@
type="loading" type="loading"
/> />
<StatePanel <UnauthLoginPanel
v-else-if="sessionStatus === SESSION_STATUS.GUEST" v-else-if="sessionStatus === SESSION_STATUS.GUEST"
title="登录后查看个人信息" title="登录后查看个人信息"
description="请先登录并完善会员资料。" description="请先登录并完善会员资料。"
primary-text="微信一键登录"
type="login"
:loading="isLoginSubmitting" :loading="isLoginSubmitting"
@primary="onWechatLogin" @login="onWechatLogin"
/> />
<StatePanel <UnauthLoginPanel
v-else-if="sessionStatus === SESSION_STATUS.EXPIRED" v-else-if="sessionStatus === SESSION_STATUS.EXPIRED"
title="登录状态已过期" title="登录状态已过期"
description="为了保护会员资料,请重新登录后继续查看。" description="为了保护会员资料,请重新登录后继续查看。"
primary-text="重新登录" primary-text="重新登录"
type="expired"
:loading="isLoginSubmitting" :loading="isLoginSubmitting"
@primary="onWechatLogin" @login="onWechatLogin"
/> />
<StatePanel <StatePanel
@@ -101,6 +98,7 @@ import { ref } from 'vue'
import { onShow } from '@dcloudio/uni-app' import { onShow } from '@dcloudio/uni-app'
import { errorMessage, getMemberSession, loginAndStoreToken, SESSION_STATUS } from '@/modules/ice/api/session' import { errorMessage, getMemberSession, loginAndStoreToken, SESSION_STATUS } from '@/modules/ice/api/session'
import StatePanel from '@/components/ice/StatePanel.vue' import StatePanel from '@/components/ice/StatePanel.vue'
import UnauthLoginPanel from '@/components/UnauthLoginPanel.vue'
const member = ref({ const member = ref({
name: '', name: '',
+7 -8
View File
@@ -7,23 +7,19 @@
type="loading" type="loading"
/> />
<StatePanel <UnauthLoginPanel
v-else-if="sessionStatus === SESSION_STATUS.GUEST" v-else-if="sessionStatus === SESSION_STATUS.GUEST"
primary-text="微信一键登录"
type="login"
:show-mark="false"
:loading="isLoginSubmitting" :loading="isLoginSubmitting"
@primary="onWechatLogin" @login="onWechatLogin"
/> />
<StatePanel <UnauthLoginPanel
v-else-if="sessionStatus === SESSION_STATUS.EXPIRED" v-else-if="sessionStatus === SESSION_STATUS.EXPIRED"
title="登录状态已过期" title="登录状态已过期"
description="为了保护会员资料,请重新登录后继续查看。" description="为了保护会员资料,请重新登录后继续查看。"
primary-text="重新登录" primary-text="重新登录"
type="expired"
:loading="isLoginSubmitting" :loading="isLoginSubmitting"
@primary="onWechatLogin" @login="onWechatLogin"
/> />
<StatePanel <StatePanel
@@ -212,6 +208,7 @@
</view> </view>
</view> </view>
</block> </block>
<AgreementLinks v-if="isLoggedIn" class="profile-agreement-links" compact />
<VenueTabBar venue="ice" active="profile" /> <VenueTabBar venue="ice" active="profile" />
</view> </view>
</template> </template>
@@ -223,6 +220,8 @@ import { api, authStorage } from '@/modules/ice/api/member'
import { errorMessage, getMemberSession, loginAndStoreToken, SESSION_STATUS } from '@/modules/ice/api/session' import { errorMessage, getMemberSession, loginAndStoreToken, SESSION_STATUS } from '@/modules/ice/api/session'
import VenueTabBar from '@/components/VenueTabBar.vue' import VenueTabBar from '@/components/VenueTabBar.vue'
import StatePanel from '@/components/ice/StatePanel.vue' import StatePanel from '@/components/ice/StatePanel.vue'
import AgreementLinks from '@/components/AgreementLinks.vue'
import UnauthLoginPanel from '@/components/UnauthLoginPanel.vue'
const isLoggedIn = ref(false) const isLoggedIn = ref(false)
const isBound = ref(false) const isBound = ref(false)
Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB