feat: add agreement-gated login state
This commit is contained in:
@@ -11,7 +11,8 @@
|
||||
"verify:tabbar": "node scripts/verify-venue-tabbar.mjs",
|
||||
"verify:startup": "node scripts/verify-venue-startup.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": {
|
||||
"@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) {
|
||||
const content = readFileSync(resolve(srcRoot, page), 'utf8')
|
||||
expectIncludes(content, 'v-if="!session.isLoggedIn"', `${page} unauth condition`)
|
||||
expectIncludes(content, '<StatePanel', `${page} unauth component`)
|
||||
expectIncludes(content, 'type="login"', `${page} login state type`)
|
||||
expectIncludes(content, ':show-mark="false"', `${page} login state mark`)
|
||||
expectIncludes(content, "import StatePanel from '@/components/ice/StatePanel.vue'", `${page} StatePanel import`)
|
||||
expectIncludes(content, '<UnauthLoginPanel', `${page} unauth component`)
|
||||
expectIncludes(content, '@login="login"', `${page} login event`)
|
||||
expectIncludes(content, "import UnauthLoginPanel from '@/components/UnauthLoginPanel.vue'", `${page} UnauthLoginPanel import`)
|
||||
expectExcludes(content, '星河综合运动中心', `${page} unauth copy`)
|
||||
expectExcludes(content, '登录后查看', `${page} unauth copy`)
|
||||
expectExcludes(content, 'class="brand"', `${page} brand style`)
|
||||
expectExcludes(content, 'class="desc"', `${page} desc style`)
|
||||
expectExcludes(content, 'primary-text="微信一键登录"', `${page} direct StatePanel login`)
|
||||
}
|
||||
|
||||
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>
|
||||
<view class="bookings-page">
|
||||
<StatePanel
|
||||
<UnauthLoginPanel
|
||||
v-if="!session.isLoggedIn"
|
||||
primary-text="微信一键登录"
|
||||
type="login"
|
||||
:show-mark="false"
|
||||
@primary="login"
|
||||
@login="login"
|
||||
/>
|
||||
|
||||
<block v-else>
|
||||
@@ -44,7 +41,7 @@ import { ticketApi } from '@/modules/comprehensive/api/ticket'
|
||||
import { useComprehensiveSessionStore } from '@/modules/comprehensive/stores/session'
|
||||
import { formatDateTime, formatTimeRange } from '@/modules/comprehensive/utils/date'
|
||||
import VenueTabBar from '@/components/VenueTabBar.vue'
|
||||
import StatePanel from '@/components/ice/StatePanel.vue'
|
||||
import UnauthLoginPanel from '@/components/UnauthLoginPanel.vue'
|
||||
|
||||
const session = useComprehensiveSessionStore()
|
||||
const businessType = ref('course')
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
<template>
|
||||
<view class="profile-page container">
|
||||
<StatePanel
|
||||
<UnauthLoginPanel
|
||||
v-if="!session.isLoggedIn"
|
||||
primary-text="微信一键登录"
|
||||
type="login"
|
||||
:show-mark="false"
|
||||
@primary="login"
|
||||
@login="login"
|
||||
/>
|
||||
|
||||
<block v-else>
|
||||
@@ -68,6 +65,7 @@
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
<AgreementLinks v-if="session.isLoggedIn" class="profile-agreement-links" compact />
|
||||
<VenueTabBar venue="comprehensive" active="profile" />
|
||||
</view>
|
||||
</template>
|
||||
@@ -76,7 +74,8 @@
|
||||
import { computed, onMounted } from 'vue'
|
||||
import { useComprehensiveSessionStore } from '@/modules/comprehensive/stores/session'
|
||||
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 avatarInitial = computed(() => (session.member?.nickname || '用户').slice(0, 1))
|
||||
|
||||
@@ -7,24 +7,21 @@
|
||||
type="loading"
|
||||
/>
|
||||
|
||||
<StatePanel
|
||||
<UnauthLoginPanel
|
||||
v-else-if="sessionStatus === SESSION_STATUS.GUEST"
|
||||
title="登录后查看预约详情"
|
||||
description="请先登录并完善会员资料。"
|
||||
primary-text="微信一键登录"
|
||||
type="login"
|
||||
:loading="isLoginSubmitting"
|
||||
@primary="onWechatLogin"
|
||||
@login="onWechatLogin"
|
||||
/>
|
||||
|
||||
<StatePanel
|
||||
<UnauthLoginPanel
|
||||
v-else-if="sessionStatus === SESSION_STATUS.EXPIRED"
|
||||
title="登录状态已过期"
|
||||
description="为了保护会员资料,请重新登录后继续查看。"
|
||||
primary-text="重新登录"
|
||||
type="expired"
|
||||
:loading="isLoginSubmitting"
|
||||
@primary="onWechatLogin"
|
||||
@login="onWechatLogin"
|
||||
/>
|
||||
|
||||
<StatePanel
|
||||
@@ -147,6 +144,7 @@ import { onShow } from '@dcloudio/uni-app'
|
||||
import { bookingApi } from '@/modules/ice/api/booking'
|
||||
import { errorMessage, getMemberSession, loginAndStoreToken, sessionStatusFromError, SESSION_STATUS } from '@/modules/ice/api/session'
|
||||
import StatePanel from '@/components/ice/StatePanel.vue'
|
||||
import UnauthLoginPanel from '@/components/UnauthLoginPanel.vue'
|
||||
|
||||
const booking = ref({
|
||||
id: null,
|
||||
|
||||
@@ -7,23 +7,19 @@
|
||||
type="loading"
|
||||
/>
|
||||
|
||||
<StatePanel
|
||||
<UnauthLoginPanel
|
||||
v-else-if="sessionStatus === SESSION_STATUS.GUEST"
|
||||
primary-text="微信一键登录"
|
||||
type="login"
|
||||
:show-mark="false"
|
||||
:loading="isLoginSubmitting"
|
||||
@primary="onWechatLogin"
|
||||
@login="onWechatLogin"
|
||||
/>
|
||||
|
||||
<StatePanel
|
||||
<UnauthLoginPanel
|
||||
v-else-if="sessionStatus === SESSION_STATUS.EXPIRED"
|
||||
title="登录状态已过期"
|
||||
description="为了保护会员资料,请重新登录后继续查看预约行程。"
|
||||
primary-text="重新登录"
|
||||
type="expired"
|
||||
:loading="isLoginSubmitting"
|
||||
@primary="onWechatLogin"
|
||||
@login="onWechatLogin"
|
||||
/>
|
||||
|
||||
<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 VenueTabBar from '@/components/VenueTabBar.vue'
|
||||
import StatePanel from '@/components/ice/StatePanel.vue'
|
||||
import UnauthLoginPanel from '@/components/UnauthLoginPanel.vue'
|
||||
|
||||
const isLoggedIn = ref(false)
|
||||
const isLoginSubmitting = ref(false)
|
||||
|
||||
@@ -7,24 +7,21 @@
|
||||
type="loading"
|
||||
/>
|
||||
|
||||
<StatePanel
|
||||
<UnauthLoginPanel
|
||||
v-else-if="sessionStatus === SESSION_STATUS.GUEST"
|
||||
title="登录后查看会员卡流水"
|
||||
description="请先登录并完善会员资料。"
|
||||
primary-text="微信一键登录"
|
||||
type="login"
|
||||
:loading="isLoginSubmitting"
|
||||
@primary="onWechatLogin"
|
||||
@login="onWechatLogin"
|
||||
/>
|
||||
|
||||
<StatePanel
|
||||
<UnauthLoginPanel
|
||||
v-else-if="sessionStatus === SESSION_STATUS.EXPIRED"
|
||||
title="登录状态已过期"
|
||||
description="为了保护会员资料,请重新登录后继续查看。"
|
||||
primary-text="重新登录"
|
||||
type="expired"
|
||||
:loading="isLoginSubmitting"
|
||||
@primary="onWechatLogin"
|
||||
@login="onWechatLogin"
|
||||
/>
|
||||
|
||||
<StatePanel
|
||||
@@ -148,6 +145,7 @@ import { onPullDownRefresh, onShow } from '@dcloudio/uni-app'
|
||||
import { cardApi } from '@/modules/ice/api/card'
|
||||
import { errorMessage, getMemberSession, loginAndStoreToken, sessionStatusFromError, SESSION_STATUS } from '@/modules/ice/api/session'
|
||||
import StatePanel from '@/components/ice/StatePanel.vue'
|
||||
import UnauthLoginPanel from '@/components/UnauthLoginPanel.vue'
|
||||
|
||||
const card = ref({
|
||||
id: null,
|
||||
|
||||
@@ -7,24 +7,21 @@
|
||||
type="loading"
|
||||
/>
|
||||
|
||||
<StatePanel
|
||||
<UnauthLoginPanel
|
||||
v-else-if="sessionStatus === SESSION_STATUS.GUEST"
|
||||
title="登录后查看会员卡"
|
||||
description="登录并完善会员资料后,可查看场馆为你登记的会员卡。"
|
||||
primary-text="微信一键登录"
|
||||
type="login"
|
||||
:loading="isLoginSubmitting"
|
||||
@primary="onWechatLogin"
|
||||
@login="onWechatLogin"
|
||||
/>
|
||||
|
||||
<StatePanel
|
||||
<UnauthLoginPanel
|
||||
v-else-if="sessionStatus === SESSION_STATUS.EXPIRED"
|
||||
title="登录状态已过期"
|
||||
description="为了保护会员资料,请重新登录后继续查看会员卡。"
|
||||
primary-text="重新登录"
|
||||
type="expired"
|
||||
:loading="isLoginSubmitting"
|
||||
@primary="onWechatLogin"
|
||||
@login="onWechatLogin"
|
||||
/>
|
||||
|
||||
<StatePanel
|
||||
@@ -137,6 +134,7 @@ import { cardApi } from '@/modules/ice/api/card'
|
||||
import { authStorage } from '@/modules/ice/api/member'
|
||||
import { errorMessage, getMemberSession, loginAndStoreToken, sessionStatusFromError, SESSION_STATUS } from '@/modules/ice/api/session'
|
||||
import StatePanel from '@/components/ice/StatePanel.vue'
|
||||
import UnauthLoginPanel from '@/components/UnauthLoginPanel.vue'
|
||||
|
||||
const cards = ref([])
|
||||
const isLoading = ref(false)
|
||||
|
||||
@@ -7,24 +7,21 @@
|
||||
type="loading"
|
||||
/>
|
||||
|
||||
<StatePanel
|
||||
<UnauthLoginPanel
|
||||
v-else-if="sessionStatus === SESSION_STATUS.GUEST"
|
||||
title="登录后查看个人信息"
|
||||
description="请先登录并完善会员资料。"
|
||||
primary-text="微信一键登录"
|
||||
type="login"
|
||||
:loading="isLoginSubmitting"
|
||||
@primary="onWechatLogin"
|
||||
@login="onWechatLogin"
|
||||
/>
|
||||
|
||||
<StatePanel
|
||||
<UnauthLoginPanel
|
||||
v-else-if="sessionStatus === SESSION_STATUS.EXPIRED"
|
||||
title="登录状态已过期"
|
||||
description="为了保护会员资料,请重新登录后继续查看。"
|
||||
primary-text="重新登录"
|
||||
type="expired"
|
||||
:loading="isLoginSubmitting"
|
||||
@primary="onWechatLogin"
|
||||
@login="onWechatLogin"
|
||||
/>
|
||||
|
||||
<StatePanel
|
||||
@@ -101,6 +98,7 @@ import { ref } from 'vue'
|
||||
import { onShow } from '@dcloudio/uni-app'
|
||||
import { errorMessage, getMemberSession, loginAndStoreToken, SESSION_STATUS } from '@/modules/ice/api/session'
|
||||
import StatePanel from '@/components/ice/StatePanel.vue'
|
||||
import UnauthLoginPanel from '@/components/UnauthLoginPanel.vue'
|
||||
|
||||
const member = ref({
|
||||
name: '',
|
||||
|
||||
@@ -7,23 +7,19 @@
|
||||
type="loading"
|
||||
/>
|
||||
|
||||
<StatePanel
|
||||
<UnauthLoginPanel
|
||||
v-else-if="sessionStatus === SESSION_STATUS.GUEST"
|
||||
primary-text="微信一键登录"
|
||||
type="login"
|
||||
:show-mark="false"
|
||||
:loading="isLoginSubmitting"
|
||||
@primary="onWechatLogin"
|
||||
@login="onWechatLogin"
|
||||
/>
|
||||
|
||||
<StatePanel
|
||||
<UnauthLoginPanel
|
||||
v-else-if="sessionStatus === SESSION_STATUS.EXPIRED"
|
||||
title="登录状态已过期"
|
||||
description="为了保护会员资料,请重新登录后继续查看。"
|
||||
primary-text="重新登录"
|
||||
type="expired"
|
||||
:loading="isLoginSubmitting"
|
||||
@primary="onWechatLogin"
|
||||
@login="onWechatLogin"
|
||||
/>
|
||||
|
||||
<StatePanel
|
||||
@@ -212,6 +208,7 @@
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
<AgreementLinks v-if="isLoggedIn" class="profile-agreement-links" compact />
|
||||
<VenueTabBar venue="ice" active="profile" />
|
||||
</view>
|
||||
</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 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 isLoggedIn = ref(false)
|
||||
const isBound = ref(false)
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
Reference in New Issue
Block a user