Files
jiapuapp/pages/auth/a01-entry.vue
T
2026-07-15 17:23:33 +08:00

315 lines
17 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!-- 页面编号A-01用途APP 唯一登录入口承载密码验证码与微信登录入口 -->
<template>
<view class="auth-page login-page">
<image class="paper-background" src="/static/assets/foundation/opaque/auth-page-paper.jpg" mode="aspectFill" />
<image class="scenery-background" src="/static/assets/foundation/transparent/auth-ink-scenery.png" mode="aspectFill" />
<view class="auth-content">
<view class="auth-header">
<image class="header-background" src="/static/assets/modules/auth/opaque/a01-login-header-v1.png" mode="scaleToFill" />
</view>
<view class="login-scroll">
<image class="login-scroll__skin" src="/static/assets/modules/auth/transparent/a01-login-scroll-frame-v1.png" mode="scaleToFill" />
<view class="login-scroll__content">
<view class="login-heading">
<image class="heading-cloud" src="/static/assets/foundation/transparent/auth-title-cloud.png" mode="aspectFit" />
<text class="login-title">登录家谱</text>
<image class="heading-cloud heading-cloud--right" src="/static/assets/foundation/transparent/auth-title-cloud.png" mode="aspectFit" />
</view>
<image class="title-divider" src="/static/assets/modules/auth/opaque/a01-title-divider-v2.png" mode="scaleToFill" />
<view class="login-tabs">
<view class="login-tab" :class="{ active: activeLoginMethod === 'password' }" hover-class="tap-fade" @click="switchLoginMethod('password')">密码登录</view>
<view class="login-tab" :class="{ active: activeLoginMethod === 'sms' }" hover-class="tap-fade" @click="switchLoginMethod('sms')">验证码登录</view>
</view>
<view class="form-content">
<view class="input-row">
<image class="input-icon" src="/static/assets/modules/auth/transparent/a01-icon-phone-v1.png" mode="aspectFit" />
<input v-model.trim="phone" class="auth-input" type="number" maxlength="11" placeholder="手机号" placeholder-class="input-placeholder" confirm-type="next" />
</view>
<view v-if="activeLoginMethod === 'password'" class="input-row">
<image class="input-icon" src="/static/assets/modules/auth/transparent/a01-icon-lock-v1.png" mode="aspectFit" />
<input v-model="password" class="auth-input" :password="!passwordVisible" maxlength="32" placeholder="密码" placeholder-class="input-placeholder" confirm-type="done" @confirm="submitLogin" />
<view class="password-toggle" hover-class="tap-fade" @click="togglePasswordVisibility">
<image class="password-toggle__icon" :src="passwordVisible ? '/static/assets/modules/auth/transparent/a01-icon-eye-open-v1.png' : '/static/assets/modules/auth/transparent/a01-icon-eye-closed-pupil-v2.png'" mode="aspectFit" />
</view>
</view>
<view v-else class="input-row">
<image class="input-icon" src="/static/assets/modules/auth/transparent/a01-icon-sms-code-v2.png" mode="aspectFit" />
<input v-model.trim="verificationCode" class="auth-input" type="number" maxlength="6" placeholder="验证码" placeholder-class="input-placeholder" confirm-type="done" @confirm="submitLogin" />
<view class="get-code" hover-class="tap-fade" @click="prepareGetCode">获取验证码</view>
</view>
<view class="form-secondary-row">
<view v-if="activeLoginMethod === 'password'" class="forgot-password" hover-class="tap-fade" @click="prepareForgotPassword">忘记密码</view>
</view>
</view>
<view class="login-submit" hover-class="button-pressed" @click="submitLogin">
<image class="button-skin" src="/static/assets/foundation/opaque/a01-primary-button.png" mode="scaleToFill" />
<text class="login-submit__copy">登录</text>
</view>
<view class="other-login-divider">
<view class="divider-line" />
<image class="divider-knot" src="/static/assets/foundation/transparent/auth-divider-knot.png" mode="aspectFit" />
<text>其他登录方式</text>
<image class="divider-knot divider-knot--right" src="/static/assets/foundation/transparent/auth-divider-knot.png" mode="aspectFit" />
<view class="divider-line" />
</view>
<view class="wechat-login" hover-class="button-pressed" @click="prepareWechatLogin">
<image class="button-skin" src="/static/assets/foundation/opaque/a01-secondary-button.png" mode="scaleToFill" />
<view class="wechat-login__content">
<image class="wechat-icon" src="/static/assets/foundation/transparent/auth-wechat.png" mode="aspectFit" />
<text>微信登录</text>
</view>
</view>
<view class="register-entry">
<text>还没有账号</text>
<view class="register-link" hover-class="tap-fade" @click="prepareRegister">注册账号</view>
</view>
<view class="agreement-row" @click="toggleAgreement">
<image class="agreement-icon" :src="agreed ? '/static/assets/modules/auth/transparent/a02-agreement-checked.png' : '/static/assets/modules/auth/transparent/a02-agreement-unchecked.png'" mode="aspectFit" />
<view class="agreement-copy">
<text>我已阅读并同意</text>
<text class="agreement-link" @click.stop="prepareAgreement">用户协议</text>
<text></text>
<text class="agreement-link" @click.stop="prepareAgreement">隐私政策</text>
</view>
</view>
</view>
</view>
</view>
<image class="exact-header-overlay" src="/static/assets/modules/auth/opaque/a01-login-header-exact-v2.png" mode="scaleToFill" />
<!-- 先验收自定义容器真实滑块数据留到接口阶段接入 -->
<view v-if="verificationVisible" class="verification-layer" @click="closeVerification">
<view class="verification-dialog" @click.stop>
<image class="verification-dialog__skin" src="/static/assets/modules/auth/opaque/a02-login-panel.png" mode="scaleToFill" />
<view class="verification-dialog__content">
<text class="verification-title">安全验证</text>
<text class="verification-copy">{{ verificationPurpose }}前需要完成行为验证</text>
<text class="verification-note">验证数据将在接口接入后启用</text>
<view class="verification-actions">
<view class="verification-action" hover-class="tap-fade" @click="closeVerification">取消</view>
<view class="verification-action verification-action--primary" hover-class="tap-fade" @click="confirmVerification">知道了</view>
</view>
</view>
</view>
</view>
<view v-if="feedbackVisible" class="feedback-toast">
<image class="feedback-toast__skin" src="/static/assets/foundation/opaque/a01-secondary-button.png" mode="scaleToFill" />
<text class="feedback-toast__copy">{{ feedbackMessage }}</text>
</view>
</view>
</template>
<script setup>
import { ref } from 'vue'
import { onLoad, onUnload } from '@dcloudio/uni-app'
const LOGIN_METHOD_STORAGE_KEY = 'a01:last-login-method'
const activeLoginMethod = ref('password')
const passwordVisible = ref(false)
const phone = ref('')
const password = ref('')
const verificationCode = ref('')
const agreed = ref(false)
const verificationVisible = ref(false)
const verificationPurpose = ref('登录')
const feedbackVisible = ref(false)
const feedbackMessage = ref('')
let feedbackTimer = null
onLoad(() => {
const storedMethod = uni.getStorageSync(LOGIN_METHOD_STORAGE_KEY)
if (storedMethod === 'sms') activeLoginMethod.value = 'sms'
})
onUnload(() => {
if (feedbackTimer) clearTimeout(feedbackTimer)
})
const showFeedback = (message) => {
feedbackMessage.value = message
feedbackVisible.value = true
if (feedbackTimer) clearTimeout(feedbackTimer)
feedbackTimer = setTimeout(() => {
feedbackVisible.value = false
feedbackTimer = null
}, 2200)
}
const switchLoginMethod = (method) => {
if (method !== 'password' && method !== 'sms') return
activeLoginMethod.value = method
uni.setStorageSync(LOGIN_METHOD_STORAGE_KEY, method)
}
const togglePasswordVisibility = () => {
passwordVisible.value = !passwordVisible.value
}
const toggleAgreement = () => {
agreed.value = !agreed.value
}
const validatePhone = () => {
if (/^1\d{10}$/.test(phone.value)) return true
showFeedback('请输入正确的手机号')
return false
}
const requireAgreement = () => {
if (agreed.value) return true
showFeedback('请先阅读并同意用户协议与隐私政策')
return false
}
const openVerification = (purpose) => {
verificationPurpose.value = purpose
verificationVisible.value = true
}
const closeVerification = () => {
verificationVisible.value = false
}
const confirmVerification = () => {
closeVerification()
showFeedback('行为验证接口待接入')
}
const prepareGetCode = () => {
if (!validatePhone() || !requireAgreement()) return
openVerification('发送验证码')
}
const submitLogin = () => {
if (!validatePhone()) return
if (activeLoginMethod.value === 'password' && !password.value) {
showFeedback('请输入密码')
return
}
if (activeLoginMethod.value === 'sms' && !/^\d{6}$/.test(verificationCode.value)) {
showFeedback('请输入 6 位验证码')
return
}
if (!requireAgreement()) return
openVerification('登录')
}
const prepareForgotPassword = () => uni.navigateTo({ url: '/pages/auth/a05-reset-password' })
const prepareRegister = () => uni.navigateTo({ url: '/pages/auth/a04-register' })
const prepareWechatLogin = () => {
if (!requireAgreement()) return
showFeedback('微信登录功能准备中')
}
const prepareAgreement = () => showFeedback('协议页面准备中')
</script>
<style scoped lang="scss">
.auth-page {
position: relative;
box-sizing: border-box;
width: 100%;
height: 100dvh;
min-height: 100%;
overflow-x: hidden;
overflow-y: auto;
background: #f6efe2;
color: #493323;
}
.paper-background,
.scenery-background {
position: fixed;
inset: 0;
width: 100%;
height: 100%;
pointer-events: none;
}
.paper-background { opacity: 0.98; }
.scenery-background { opacity: 0.72; }
.exact-header-overlay { position: fixed; z-index: 5; top: 0; left: 0; width: 100%; height: 267rpx; pointer-events: none; }
.auth-content { position: relative; z-index: 1; min-height: 100%; padding-bottom: env(safe-area-inset-bottom); }
.auth-header { position: relative; height: 304rpx; }
.header-background { width: 100%; height: 100%; }
.login-scroll { position: relative; z-index: 4; box-sizing: border-box; height: 1520rpx; margin: -80rpx 12rpx 0; }
.login-scroll__skin { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; }
.login-scroll__content { position: relative; z-index: 1; box-sizing: border-box; height: 100%; padding: 224rpx 97rpx 66rpx; }
.login-heading { display: flex; align-items: center; justify-content: center; height: 74rpx; }
.login-title { margin: 0 8rpx; color: #6f140f; font-family: STKaiti, KaiTi, serif; font-size: 64rpx; font-weight: 700; letter-spacing: 8rpx; line-height: 1; }
.heading-cloud { width: 48rpx; height: 34rpx; }
.heading-cloud--right { transform: scaleX(-1); }
.title-divider { position: absolute; top: 310rpx; left: 50%; display: block; width: 286rpx; height: 38rpx; transform: translateX(-50%); }
.login-tabs { display: flex; align-items: flex-end; height: 104rpx; margin: 14rpx 0 0; border-bottom: 1rpx solid rgba(193, 142, 67, 0.48); }
.login-tab { position: relative; display: flex; flex: 1; align-items: center; justify-content: center; min-height: 88rpx; color: #7f6856; font-family: STKaiti, KaiTi, serif; font-size: 33rpx; letter-spacing: 2rpx; }
.login-tab:first-child::before { position: absolute; top: 50%; right: 0; width: 1rpx; height: 38rpx; background: rgba(193, 142, 67, 0.42); content: ''; transform: translateY(-50%); }
.login-tab.active { color: #a9160d; font-weight: 700; }
.login-tab.active::after { position: absolute; right: 102rpx; bottom: -2rpx; left: 102rpx; height: 5rpx; border-radius: 5rpx; background: #ad160d; content: ''; }
.form-content { margin-top: 16rpx; }
.input-row { display: flex; align-items: center; box-sizing: border-box; min-height: 128rpx; padding-left: 14rpx; border-bottom: 1rpx solid rgba(197, 145, 70, 0.62); }
.input-icon { flex: 0 0 auto; width: 58rpx; height: 58rpx; margin-right: 22rpx; }
.auth-input { flex: 1; min-width: 0; height: 116rpx; color: #493323; font-size: 32rpx; }
.input-placeholder { color: #a79e94; }
.password-toggle { display: flex; flex: 0 0 auto; align-items: center; justify-content: center; width: 88rpx; height: 88rpx; margin-right: 0; }
.password-toggle__icon { width: 56rpx; height: 56rpx; }
.get-code { display: flex; flex: 0 0 auto; align-items: center; min-height: 88rpx; padding-left: 18rpx; color: #a9160d; font-size: 24rpx; white-space: nowrap; }
.form-secondary-row { display: flex; align-items: center; justify-content: flex-end; min-height: 80rpx; }
.forgot-password { display: flex; align-items: center; min-height: 80rpx; color: #a9160d; font-size: 28rpx; }
.login-submit,
.wechat-login { position: relative; display: flex; align-items: center; justify-content: center; overflow: hidden; }
.login-submit { height: 112rpx; margin-top: 34rpx; }
.button-skin { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; }
.login-submit__copy { position: relative; z-index: 1; color: #fffaf1; font-family: STKaiti, KaiTi, serif; font-size: 42rpx; letter-spacing: 8rpx; }
.other-login-divider { display: flex; align-items: center; height: 74rpx; margin-top: 36rpx; color: #b07b31; font-family: STKaiti, KaiTi, serif; font-size: 27rpx; white-space: nowrap; }
.divider-line { flex: 1; height: 1rpx; margin: 0 10rpx; background: rgba(194, 139, 60, 0.58); }
.divider-knot { width: 18rpx; height: 18rpx; margin-right: 8rpx; }
.divider-knot--right { margin: 0 0 0 8rpx; transform: scaleX(-1); }
.wechat-login { height: 114rpx; margin-top: 8rpx; color: #493323; font-family: STKaiti, KaiTi, serif; font-size: 34rpx; letter-spacing: 4rpx; }
.wechat-login__content { position: relative; z-index: 1; display: flex; align-items: center; justify-content: center; }
.wechat-icon { width: 48rpx; height: 48rpx; margin-right: 14rpx; filter: sepia(1) saturate(2.7) hue-rotate(52deg) brightness(0.78); }
.register-entry { display: flex; align-items: center; justify-content: center; min-height: 66rpx; margin-top: 32rpx; color: #493323; font-size: 27rpx; }
.register-link { display: flex; align-items: center; min-height: 66rpx; margin-left: 10rpx; color: #a9160d; }
.agreement-row { display: flex; align-items: center; justify-content: center; min-height: 68rpx; margin-top: 24rpx; color: #58483c; font-size: 20rpx; line-height: 30rpx; }
.agreement-icon { flex: 0 0 auto; width: 32rpx; height: 32rpx; margin-right: 10rpx; }
.agreement-copy { display: flex; flex: 0 1 auto; align-items: center; min-width: 0; white-space: nowrap; }
.agreement-link { color: #a9160d; }
.tap-fade { opacity: 0.62; }
.button-pressed { opacity: 0.82; }
.feedback-toast { position: fixed; z-index: 30; top: calc(env(safe-area-inset-top) + 32rpx); left: 50%; display: flex; align-items: center; justify-content: center; width: 590rpx; min-height: 88rpx; transform: translateX(-50%); }
.feedback-toast__skin { position: absolute; inset: 0; width: 100%; height: 100%; }
.feedback-toast__copy { position: relative; z-index: 1; padding: 18rpx 40rpx; color: #5c4330; font-size: 25rpx; text-align: center; }
.verification-layer { position: fixed; z-index: 40; inset: 0; display: flex; align-items: center; justify-content: center; padding: 40rpx; background: rgba(31, 20, 13, 0.56); }
.verification-dialog { position: relative; width: 620rpx; height: 520rpx; }
.verification-dialog__skin { position: absolute; inset: 0; width: 100%; height: 100%; }
.verification-dialog__content { position: relative; z-index: 1; display: flex; flex-direction: column; align-items: center; box-sizing: border-box; height: 100%; padding: 90rpx 64rpx 54rpx; }
.verification-title { color: #6f140f; font-family: STKaiti, KaiTi, serif; font-size: 42rpx; font-weight: 700; }
.verification-copy { margin-top: 42rpx; color: #493323; font-size: 28rpx; text-align: center; }
.verification-note { margin-top: 20rpx; color: #8d7a68; font-size: 23rpx; }
.verification-actions { display: flex; width: 100%; margin-top: auto; }
.verification-action { display: flex; flex: 1; align-items: center; justify-content: center; min-height: 88rpx; color: #745b46; font-size: 28rpx; }
.verification-action--primary { color: #a9160d; font-weight: 700; }
</style>