Files
jiapuapp/pages/auth/a01-entry.vue
T
2026-07-21 11:13:26 +08:00

835 lines
19 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">
<view class="auth-header">
<image
class="auth-header__image"
src="/static/assets/modules/auth/opaque/a01-vnext-header-v1.png"
mode="widthFix"
/>
<image
class="brand-seal"
src="/static/assets/foundation/transparent/brand-seal.png"
mode="aspectFit"
/>
</view>
<view class="auth-paper">
<view class="login-content">
<view class="login-heading">
<text class="login-title">登录家谱</text>
<image
class="title-divider"
src="/static/assets/modules/auth/transparent/a01-vnext-divider-v1.png"
mode="aspectFit"
/>
</view>
<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 input-icon--sms"
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/transparent/a01-scroll-primary-v3.png"
mode="aspectFit"
/>
<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/transparent/a01-scroll-secondary-v3.png"
mode="aspectFit"
/>
<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-area"
:class="{ 'agreement-area--error': agreementError }"
>
<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>
<text v-if="agreementError" class="agreement-error"
>请先阅读并同意用户协议与隐私政策</text
>
</view>
</view>
</view>
<!-- 当前只验收自定义容器外观真实行为验证数据留到接口阶段接入 -->
<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/transparent/a01-scroll-dialog-v3.png"
mode="aspectFit"
/>
<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">
<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 agreementError = 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;
if (agreed.value) agreementError.value = false;
};
const validatePhone = () => {
if (/^1\d{10}$/.test(phone.value)) return true;
showFeedback("请输入正确的手机号");
return false;
};
const requireAgreement = () => {
if (agreed.value) return true;
agreementError.value = true;
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 {
display: flex;
flex-direction: column;
box-sizing: border-box;
width: 100%;
min-height: var(--app-viewport-height);
overflow-x: hidden;
background: #940400;
color: #493323;
}
.auth-header {
position: relative;
flex: 0 0 auto;
box-sizing: border-box;
width: 100%;
height: calc(var(--app-safe-top) + min(36.5vw, 175px));
overflow: hidden;
padding-top: var(--app-safe-top);
background: #940400;
}
.auth-header__image {
display: block;
width: 100%;
height: auto;
pointer-events: none;
}
.brand-seal {
position: absolute;
top: calc(var(--app-safe-top) + clamp(46px, 11.5vw, 58px));
left: 50%;
width: clamp(56px, 17vw, 78px);
height: clamp(67px, 20.4vw, 94px);
transform: translateX(-50%);
pointer-events: none;
}
.auth-paper {
display: flex;
flex: 1;
box-sizing: border-box;
width: 100%;
padding-bottom: var(--app-safe-bottom);
background: #f7f0e5 url("/static/assets/foundation/opaque/auth-page-paper.jpg") center top / 100% auto repeat-y;
}
.login-content {
display: flex;
flex: 1;
flex-direction: column;
justify-content: space-between;
box-sizing: border-box;
width: 100%;
max-width: 480px;
min-width: 0;
margin: 0 auto;
padding: clamp(10px, 2.2vh, 20px) clamp(24px, 8.5vw, 44px);
}
.login-heading {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 50px;
}
.login-title {
color: #9f170f;
font-size: 58rpx;
font-weight: 700;
letter-spacing: 5rpx;
line-height: 1;
}
.title-divider {
flex: none;
width: 280rpx;
height: 46rpx;
filter: brightness(0.68) saturate(1.5) contrast(1.2);
}
.login-tabs {
display: flex;
align-items: flex-end;
min-height: var(--app-touch-min);
}
.login-tab {
display: flex;
flex: 1;
align-items: center;
justify-content: center;
min-height: 88rpx;
color: #80684f;
font-size: 34rpx;
letter-spacing: 2rpx;
}
.login-tab:first-child::before {
content: none;
}
.login-tab:first-child {
border-right: 1rpx solid rgba(176, 120, 52, 0.58);
}
.login-tab.active {
background: linear-gradient(#ad160d, #ad160d) center bottom /
calc(100% - 116rpx) 5rpx no-repeat;
color: #a9160d;
font-weight: 700;
}
.login-tab.active::after {
content: none;
}
.form-content {
margin-top: 10rpx;
}
.input-row {
display: flex;
align-items: center;
box-sizing: border-box;
min-height: var(--app-touch-min);
padding-left: 12rpx;
border-bottom: 1rpx solid rgba(182, 116, 43, 0.68);
}
.input-icon {
flex: 0 0 auto;
width: 64rpx;
height: 64rpx;
margin-right: 22rpx;
transform: translateX(-12rpx);
}
.input-icon--sms {
transform: translateX(-12rpx) scale(1.45);
}
.auth-input {
flex: 1;
min-width: 0;
min-height: var(--app-touch-min);
color: #493323;
font-size: 30rpx;
}
.input-placeholder {
color: #9f968d;
}
.password-toggle {
display: flex;
flex: 0 0 auto;
align-items: center;
justify-content: center;
width: 88rpx;
height: var(--app-touch-min);
}
.password-toggle__icon {
width: 64rpx;
height: 64rpx;
}
.get-code {
display: flex;
flex: 0 0 auto;
align-items: center;
justify-content: flex-end;
min-width: 142rpx;
min-height: var(--app-touch-min);
color: #a9160d;
font-size: 28rpx;
}
.form-secondary-row {
display: flex;
align-items: center;
justify-content: flex-end;
min-height: var(--app-touch-min);
}
.forgot-password {
display: flex;
align-items: center;
min-height: var(--app-touch-min);
color: #a9160d;
font-size: 28rpx;
}
.login-submit,
.wechat-login {
display: grid;
place-items: center;
}
.login-submit {
min-height: var(--app-touch-min);
height: var(--app-touch-min);
}
.button-skin {
grid-area: 1 / 1;
width: 100%;
height: 100%;
pointer-events: none;
}
.login-submit__copy {
z-index: 1;
grid-area: 1 / 1;
color: #fffaf1;
font-size: 36rpx;
letter-spacing: 5rpx;
}
.other-login-divider {
display: flex;
align-items: center;
min-height: 30px;
color: #9f6a27;
font-size: 28rpx;
white-space: nowrap;
}
.divider-line {
flex: 1;
height: 1rpx;
margin: 0 8rpx;
background: rgba(180, 113, 38, 0.64);
}
.divider-knot {
width: 17rpx;
height: 17rpx;
margin-right: 7rpx;
}
.divider-knot--right {
margin: 0 0 0 7rpx;
transform: scaleX(-1);
}
.wechat-login {
min-height: var(--app-touch-min);
height: var(--app-touch-min);
color: #493323;
font-size: 30rpx;
letter-spacing: 2rpx;
}
.wechat-login__content {
z-index: 1;
display: flex;
grid-area: 1 / 1;
align-items: center;
justify-content: center;
}
.wechat-icon {
width: 48rpx;
height: 48rpx;
margin-right: 14rpx;
transform: translateX(-8rpx) scale(1.22);
}
.register-entry {
display: flex;
align-items: center;
justify-content: center;
min-height: var(--app-touch-min);
color: #493323;
font-size: 28rpx;
}
.register-link {
display: flex;
align-items: center;
min-height: var(--app-touch-min);
margin-left: 8rpx;
color: #a9160d;
}
.agreement-area {
color: #58483c;
}
.agreement-row {
display: flex;
align-items: center;
justify-content: center;
min-height: var(--app-touch-min);
font-size: 24rpx;
line-height: 32rpx;
}
.agreement-icon {
flex: 0 0 auto;
width: 34rpx;
height: 34rpx;
margin-right: 10rpx;
}
.agreement-copy {
display: flex;
flex: 0 1 auto;
align-items: center;
min-width: 0;
}
.agreement-link {
color: #a9160d;
}
.agreement-error {
display: block;
color: #b42318;
font-size: 20rpx;
line-height: 30rpx;
text-align: center;
}
.agreement-area--error {
animation: agreement-shake 240ms ease-out;
}
@keyframes agreement-shake {
0%,
100% {
transform: translateX(0);
}
33% {
transform: translateX(-8rpx);
}
66% {
transform: translateX(8rpx);
}
}
.tap-fade {
opacity: 0.62;
}
.button-pressed {
opacity: 0.82;
}
.feedback-toast {
position: fixed;
z-index: 30;
top: calc(env(safe-area-inset-top) + 24rpx);
left: 50%;
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
width: 590rpx;
min-height: 82rpx;
border: 16rpx solid transparent;
border-width: 16rpx 74rpx;
border-image-source: url("/static/assets/foundation/transparent/a01-scroll-toast-v3.png");
border-image-slice: 58 280 fill;
border-image-width: 16rpx 74rpx;
border-image-repeat: stretch;
background: transparent;
transform: translateX(-50%);
}
.feedback-toast__copy {
z-index: 1;
padding: 16rpx 36rpx;
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 {
display: grid;
width: 620rpx;
min-height: 520rpx;
max-height: calc(100vh - 80rpx);
}
.verification-dialog__skin {
grid-area: 1 / 1;
width: 100%;
height: 100%;
}
.verification-dialog__content {
z-index: 1;
display: flex;
grid-area: 1 / 1;
flex-direction: column;
align-items: center;
box-sizing: border-box;
min-height: 100%;
overflow-y: auto;
padding: 90rpx 64rpx 54rpx;
}
.verification-title {
color: #6f140f;
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>