Files
jiapuapp/pages/auth/sign-in.vue
T
2026-09-13 17:45:52 +08:00

1140 lines
30 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.
<template>
<AuthPageShell class="auth-page login-page">
<view class="login-content">
<view class="login-heading">
<text class="login-title">登录家谱</text>
<image
class="title-divider"
src="/static/assets/modules/auth/transparent/title-divider.png"
mode="aspectFit"
/>
</view>
<view class="login-tabs" role="tablist" aria-label="登录方式">
<button
class="auth-plain-button login-tab"
role="tab"
:aria-selected="activeLoginMethod === 'password'"
:class="{ active: activeLoginMethod === 'password' }"
:disabled="submitting || sendingCode || tacVisible"
hover-class="tap-fade"
@click="switchLoginMethod('password')"
>
密码登录
</button>
<button
class="auth-plain-button login-tab"
role="tab"
:aria-selected="activeLoginMethod === 'sms'"
:class="{ active: activeLoginMethod === 'sms' }"
:disabled="submitting || sendingCode || tacVisible"
hover-class="tap-fade"
@click="switchLoginMethod('sms')"
>
验证码登录
</button>
</view>
<view class="form-content">
<view class="input-row input-row--required">
<image
class="input-icon"
src="/static/assets/modules/auth/transparent/icon-phone.png"
mode="aspectFit"
/>
<input
v-model.trim="phone"
class="auth-input"
type="number"
maxlength="11"
:disabled="
sendingCode ||
submitting ||
tacVisible ||
authenticationCommitted ||
(cooldownSeconds > 0 && phone.length > 0)
"
placeholder="手机号"
aria-label="手机号"
placeholder-class="input-placeholder"
confirm-type="next"
@input="handlePhoneInput"
/>
</view>
<view
v-if="activeLoginMethod === 'password'"
class="input-row input-row--required"
>
<image
class="input-icon"
src="/static/assets/modules/auth/transparent/icon-password.png"
mode="aspectFit"
/>
<input
v-model="password"
class="auth-input"
:password="!passwordVisible"
maxlength="32"
:disabled="submitting || tacVisible"
placeholder="密码"
aria-label="登录密码"
placeholder-class="input-placeholder"
confirm-type="done"
@confirm="submitLogin"
/>
<button
class="auth-plain-button password-toggle"
:aria-label="passwordVisible ? '隐藏密码' : '显示密码'"
:aria-pressed="passwordVisible"
:disabled="submitting || tacVisible"
hover-class="tap-fade"
@click="togglePasswordVisibility"
>
<image
class="password-toggle__icon"
:src="
passwordVisible
? '/static/assets/modules/auth/transparent/password-visible.png'
: '/static/assets/modules/auth/transparent/password-hidden.png'
"
mode="aspectFit"
aria-hidden="true"
/>
</button>
</view>
<view v-else class="input-row input-row--required">
<image
class="input-icon input-icon--sms"
src="/static/assets/modules/auth/transparent/icon-verification-code.png"
mode="aspectFit"
/>
<input
v-model.trim="verificationCode"
class="auth-input"
type="number"
maxlength="4"
:disabled="submitting"
placeholder="短信验证码"
aria-label="短信验证码"
placeholder-class="input-placeholder"
confirm-type="done"
@confirm="submitLogin"
/>
<button
class="auth-plain-button get-code"
:class="{
'get-code--disabled': sendingCode || cooldownSeconds > 0,
}"
:disabled="sendingCode || submitting || cooldownSeconds > 0"
hover-class="tap-fade"
@click="prepareGetCode"
>
{{
sendingCode
? "请求中…"
: cooldownSeconds > 0
? `${cooldownSeconds}s 后重发`
: "获取验证码"
}}
</button>
</view>
<view class="form-secondary-row">
<button
class="auth-plain-button forgot-password"
hover-class="tap-fade"
@click="prepareForgotPassword"
>
忘记密码
</button>
</view>
</view>
<button
class="auth-plain-button login-submit"
:disabled="submitting || sendingCode || tacVisible"
:aria-busy="submitting"
hover-class="button-pressed"
@click="submitLogin"
>
<image
class="button-skin"
src="/static/assets/foundation/transparent/scroll-primary.png"
mode="aspectFit"
/>
<text class="login-submit__copy">{{
submitting
? authenticationCommitted
? "正在进入…"
: "登录中…"
: authenticationCommitted
? "进入家谱"
: "登录"
}}</text>
</button>
<!-- #ifdef APP-PLUS -->
<view class="third-party-login">
<text class="third-party-login__divider">其他登录方式</text>
<button
class="auth-plain-button wechat-login"
:disabled="submitting || sendingCode || tacVisible || wechatProviderState !== 'ready'"
:aria-busy="submitting"
hover-class="tap-fade"
@click="submitWechatLogin"
>
<text class="wechat-login__mark"></text>
<text>{{
wechatProviderState === "checking"
? "正在检查微信登录"
: wechatProviderState === "ready"
? "微信登录"
: "微信登录尚未配置"
}}</text>
</button>
</view>
<!-- #endif -->
<LoginAdvertisement />
<view class="register-entry">
<text>还没有账号</text>
<button
class="auth-plain-button register-link"
:disabled="submitting || sendingCode || tacVisible"
hover-class="tap-fade"
@click="prepareRegister"
>
注册账号
</button>
</view>
<view
:key="agreementShakeVersion"
class="agreement-area"
:class="{ 'agreement-area--error': agreementError }"
>
<view class="agreement-row">
<button
class="auth-plain-button agreement-toggle"
role="checkbox"
:aria-checked="agreed"
:aria-label="
agreed ? '取消同意用户协议与隐私政策' : '同意用户协议与隐私政策'
"
:disabled="submitting || sendingCode || tacVisible"
@click="toggleAgreement"
>
<image
class="agreement-icon"
:src="
agreed
? '/static/assets/modules/auth/transparent/agreement-checked.png'
: '/static/assets/modules/auth/transparent/agreement-unchecked.png'
"
mode="aspectFit"
aria-hidden="true"
/>
</button>
<view class="agreement-copy">
<text>我已阅读并同意</text>
<button
class="auth-plain-button agreement-link"
@click="openComplianceDocument('user_agreement')"
>
用户协议
</button>
<text></text>
<button
class="auth-plain-button agreement-link"
@click="openComplianceDocument('privacy_policy')"
>
隐私政策
</button>
</view>
</view>
<text v-if="agreementError" class="agreement-error" role="alert"
>请先阅读并同意用户协议与隐私政策</text
>
</view>
</view>
<template #overlay>
<TacVerification
:visible="tacVisible"
:context="tacContext"
@success="completeTac"
@failure="handleTacFailure"
@error="handleTacError"
@cancel="closeTac"
/>
<AppToast :visible="feedbackVisible" :message="feedbackMessage" />
</template>
</AuthPageShell>
</template>
<script setup>
import { ref } from "vue";
import { onBackPress, onShow, onUnload } from "@dcloudio/uni-app";
import AuthPageShell from "@/components/auth/PageShell.vue";
import LoginAdvertisement from "@/components/auth/LoginAdvertisement.vue";
import AppToast from "@/components/AppToast.vue";
import TacVerification from "@/components/auth/TacVerification.vue";
import {
createRequestController,
isRequestCancelled
} from "@/services/api/request-controller.js";
import { authApi } from "@/services/api/auth-service.js";
import {
AUTH_VERIFICATION_OPERATION,
createTacRenderContext,
isAuthPhone,
isSmsDeliveryOutcomeUnknown,
normalizeCaptchaRequirement,
normalizeTacSuccess,
} from "@/utils/auth/verification.js";
import { createAuthSmsCooldown } from "@/utils/auth/sms-cooldown.js";
import { runtimeConfig } from "@/utils/runtime-config.js";
import { calcMD5 } from "@/utils/md5.js";
import { session } from "@/utils/session.js";
import {
goRoot,
handleBackPress,
openPage,
runBackGuard,
} from "@/utils/navigation/gateway.js";
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 agreementShakeVersion = ref(0);
const tacVisible = ref(false);
const tacContext = ref(null);
const sendingCode = ref(false);
const submitting = ref(false);
const wechatProviderState = ref("unknown");
const authenticationCommitted = ref(false);
const cooldownSeconds = ref(0);
const sentPhone = ref("");
const feedbackVisible = ref(false);
const feedbackMessage = ref("");
let feedbackTimer = null;
let tacSequence = 0;
let pendingTacAction = null;
let pageActive = true;
const authenticationNavigationFailure =
"登录已完成,但暂时无法进入家谱,请再次点击进入";
const captchaRequirementRequestController = createRequestController();
const smsDeliveryRequestController = createRequestController();
const signInSubmissionRequestController = createRequestController();
const smsCooldown = createAuthSmsCooldown({
operationCode: AUTH_VERIFICATION_OPERATION.SMS_LOGIN,
onChange: (seconds) => {
cooldownSeconds.value = seconds;
},
});
onUnload(() => {
pageActive = false;
captchaRequirementRequestController.abort();
smsDeliveryRequestController.abort();
signInSubmissionRequestController.abort();
if (feedbackTimer) clearTimeout(feedbackTimer);
smsCooldown.dispose();
});
const showFeedback = (message) => {
feedbackMessage.value = message;
feedbackVisible.value = true;
if (feedbackTimer) clearTimeout(feedbackTimer);
feedbackTimer = setTimeout(() => {
feedbackVisible.value = false;
feedbackTimer = null;
}, 2200);
};
const enterAuthenticatedRoot = async () => {
if (!pageActive) return false;
submitting.value = true;
try {
const opened = await goRoot("G01");
if (opened !== true) throw new Error("NAVIGATION_RETRY_REQUIRED");
return true;
} catch {
if (pageActive) showFeedback(authenticationNavigationFailure);
return false;
} finally {
if (pageActive) submitting.value = false;
}
};
const restoreAuthenticatedSession = () => {
smsCooldown.sync();
if (
authenticationCommitted.value ||
submitting.value ||
sendingCode.value ||
tacVisible.value ||
!session.getToken()
) {
return;
}
authenticationCommitted.value = true;
void enterAuthenticatedRoot();
};
const detectWechatProvider = () => {
if (wechatProviderState.value !== "unknown") return;
// #ifdef APP-PLUS
if (typeof uni?.getProvider !== "function") {
wechatProviderState.value = "unavailable";
return;
}
wechatProviderState.value = "checking";
uni.getProvider({
service: "oauth",
success: ({ provider = [] } = {}) => {
if (!pageActive) return;
wechatProviderState.value = provider.includes("weixin")
? "ready"
: "unavailable";
},
fail: () => {
if (pageActive) wechatProviderState.value = "unavailable";
},
});
// #endif
// #ifndef APP-PLUS
wechatProviderState.value = "unavailable";
// #endif
};
onShow(() => {
restoreAuthenticatedSession();
detectWechatProvider();
});
const blockBusyAction = () => {
if (!sendingCode.value && !submitting.value) return false;
showFeedback("请求处理中,请稍候");
return true;
};
const switchLoginMethod = (method) => {
if (blockBusyAction()) return;
if (method === "password" || method === "sms")
activeLoginMethod.value = method;
};
const togglePasswordVisibility = () => {
passwordVisible.value = !passwordVisible.value;
};
const handlePhoneInput = () => {
if (!sentPhone.value || phone.value === sentPhone.value) return;
verificationCode.value = "";
sentPhone.value = "";
};
const toggleAgreement = () => {
agreed.value = !agreed.value;
if (agreed.value) agreementError.value = false;
};
const validatePhone = () => {
if (isAuthPhone(phone.value)) return true;
showFeedback("请输入正确的手机号");
return false;
};
const requireAgreement = () => {
if (agreed.value) return true;
agreementError.value = true;
agreementShakeVersion.value += 1;
return false;
};
const closeTac = () => {
tacVisible.value = false;
tacContext.value = null;
pendingTacAction = null;
};
const cancelPendingRequest = () => {
captchaRequirementRequestController.abort();
smsDeliveryRequestController.abort();
signInSubmissionRequestController.abort();
sendingCode.value = false;
submitting.value = false;
};
const requestBack = () =>
runBackGuard({
transientOpen: tacVisible.value,
submitting: submitting.value || sendingCode.value,
"close-transient": closeTac,
"block-submitting": () => {
cancelPendingRequest();
return true;
},
});
// 登录页是认证根页:没有浮层时必须把系统返回交还 Android,只在行为验证浮层
// 可见时消费返回键,避免根页被守卫误拦而无法正常退出应用。
onBackPress((event) => {
if (!tacVisible.value && (submitting.value || sendingCode.value)) {
cancelPendingRequest();
return false;
}
if (!tacVisible.value) return false;
return handleBackPress(event, requestBack);
});
const prepareGetCode = async () => {
if (sendingCode.value || cooldownSeconds.value > 0) return;
if (!validatePhone() || !requireAgreement()) return;
const requestedPhone = phone.value;
let smsDeliveryStarted = false;
sendingCode.value = true;
try {
const operationCode = AUTH_VERIFICATION_OPERATION.SMS_LOGIN;
const response = await authApi.getCaptchaRequirement(
{ operationCode, subject: requestedPhone },
{ requestController: captchaRequirementRequestController },
);
if (!pageActive) return;
if (phone.value !== requestedPhone)
throw new Error("手机号已变化,请重新获取验证码");
const requirement = normalizeCaptchaRequirement(response);
if (!requirement.required) {
smsDeliveryStarted = true;
await authApi.sendSmsCode(
{ operationCode, phone: requestedPhone },
{ requestController: smsDeliveryRequestController },
);
if (!pageActive) return;
sentPhone.value = requestedPhone;
smsCooldown.start();
showFeedback("验证码已发送");
return;
}
tacSequence += 1;
tacContext.value = createTacRenderContext({
requestId: `sign-in-sms-${tacSequence}`,
baseUrl: runtimeConfig.baseUrl,
clientId: runtimeConfig.clientId,
tenantId: runtimeConfig.tenantId,
operationCode,
subject: requestedPhone,
requirement,
});
pendingTacAction = {
kind: "sms-code",
phone: requestedPhone,
};
tacVisible.value = true;
} catch (error) {
if (pageActive) {
if (smsDeliveryStarted && isSmsDeliveryOutcomeUnknown(error)) {
sentPhone.value = requestedPhone;
smsCooldown.start();
showFeedback("发送结果未知,如收到短信可直接填写;60 秒后可重试");
} else if (!isRequestCancelled(error)) {
showFeedback(error.message || "安全验证暂不可用");
}
}
} finally {
if (pageActive) sendingCode.value = false;
}
};
const preparePasswordLogin = async () => {
const requestedPhone = phone.value;
const passwordHash = calcMD5(password.value);
submitting.value = true;
try {
const operationCode = AUTH_VERIFICATION_OPERATION.PASSWORD_LOGIN;
const response = await authApi.getCaptchaRequirement(
{ operationCode, subject: requestedPhone },
{ requestController: captchaRequirementRequestController },
);
if (!pageActive) return;
if (phone.value !== requestedPhone) {
throw new Error("手机号已变化,请重新登录");
}
const requirement = normalizeCaptchaRequirement(response);
if (!requirement.required) {
await authApi.loginWithPassword(
{ phone: requestedPhone, passwordHash },
{ requestController: signInSubmissionRequestController },
);
if (!pageActive) return;
authenticationCommitted.value = true;
await enterAuthenticatedRoot();
return;
}
tacSequence += 1;
tacContext.value = createTacRenderContext({
requestId: `sign-in-password-${tacSequence}`,
baseUrl: runtimeConfig.baseUrl,
clientId: runtimeConfig.clientId,
tenantId: runtimeConfig.tenantId,
operationCode,
subject: requestedPhone,
requirement,
});
pendingTacAction = {
kind: "password-login",
phone: requestedPhone,
passwordHash,
};
tacVisible.value = true;
} catch (error) {
if (pageActive && !isRequestCancelled(error)) {
showFeedback(error.message || "登录失败,请稍后重试");
}
} finally {
if (pageActive) submitting.value = false;
}
};
const completeTac = async (result) => {
const expectedContext = tacContext.value;
const action = pendingTacAction;
if (!expectedContext || !action) return;
try {
const ticket = normalizeTacSuccess(result, expectedContext.requestId);
if (phone.value !== expectedContext.subject)
throw new Error("手机号已变化,请重新验证");
if (action.kind === "password-login") {
if (
phone.value !== action.phone ||
calcMD5(password.value) !== action.passwordHash
) {
throw new Error("登录信息已变化,请重新验证");
}
tacVisible.value = false;
tacContext.value = null;
pendingTacAction = null;
submitting.value = true;
await authApi.loginWithPassword(
{
phone: action.phone,
passwordHash: action.passwordHash,
validToken: ticket.validToken,
},
{ requestController: signInSubmissionRequestController },
);
if (!pageActive) return;
authenticationCommitted.value = true;
await enterAuthenticatedRoot();
return;
}
if (action.kind !== "sms-code") {
const error = new Error("安全验证状态无效,请重新操作");
error.code = "AUTH_TAC_ACTION_INVALID";
throw error;
}
closeTac();
sendingCode.value = true;
await authApi.sendSmsCode(
{
operationCode: AUTH_VERIFICATION_OPERATION.SMS_LOGIN,
phone: expectedContext.subject,
validToken: ticket.validToken,
},
{ requestController: smsDeliveryRequestController },
);
if (!pageActive) return;
sentPhone.value = expectedContext.subject;
smsCooldown.start();
showFeedback("验证码已发送");
} catch (error) {
closeTac();
if (pageActive) {
if (action?.kind === "sms-code" && isSmsDeliveryOutcomeUnknown(error)) {
sentPhone.value = expectedContext.subject;
smsCooldown.start();
showFeedback("发送结果未知,如收到短信可直接填写;60 秒后可重试");
} else if (!isRequestCancelled(error)) {
showFeedback(
error.message ||
(action?.kind === "password-login"
? "登录失败,请稍后重试"
: "验证码发送失败"),
);
}
}
} finally {
if (pageActive) {
sendingCode.value = false;
submitting.value = false;
}
}
};
const handleTacFailure = ({ message } = {}) =>
showFeedback(message || "行为验证未通过,请重试");
const handleTacError = ({ message } = {}) => {
closeTac();
showFeedback(message || "安全验证暂不可用");
};
const submitLogin = async () => {
if (submitting.value) return;
if (authenticationCommitted.value) return enterAuthenticatedRoot();
if (!validatePhone()) return;
if (activeLoginMethod.value === "password" && !password.value) {
showFeedback("请输入密码");
return;
}
if (activeLoginMethod.value === "sms" && sentPhone.value !== phone.value) {
showFeedback("请先获取当前手机号的验证码");
return;
}
if (
activeLoginMethod.value === "sms" &&
!/^\d{4}$/.test(verificationCode.value)
) {
showFeedback("请输入 4 位验证码");
return;
}
if (!requireAgreement()) return;
if (activeLoginMethod.value === "password") {
return preparePasswordLogin();
}
submitting.value = true;
try {
await authApi.loginWithSms(
{
phone: phone.value,
smsCode: verificationCode.value,
},
{ requestController: signInSubmissionRequestController },
);
if (!pageActive) return;
authenticationCommitted.value = true;
await enterAuthenticatedRoot();
} catch (error) {
if (pageActive && !isRequestCancelled(error)) {
showFeedback(error.message || "登录失败,请稍后重试");
}
} finally {
if (pageActive) submitting.value = false;
}
};
const prepareForgotPassword = () => {
if (blockBusyAction()) return;
return openPage("A05", {}, "A01");
};
const prepareRegister = () => {
if (blockBusyAction()) return;
return openPage("A04", {}, "A01");
};
const openComplianceDocument = (documentKey) => {
if (blockBusyAction()) return;
return openPage("M13", { documentKey }, "A01");
};
const requestWechatAuthorizationCode = () =>
new Promise((resolve, reject) => {
uni.login({
provider: "weixin",
onlyAuthorize: true,
success: ({ code } = {}) => resolve(code),
fail: reject,
});
});
const submitWechatLogin = async () => {
if (submitting.value || wechatProviderState.value !== "ready") return;
if (authenticationCommitted.value) return enterAuthenticatedRoot();
if (!requireAgreement()) return;
submitting.value = true;
try {
const code = await requestWechatAuthorizationCode();
await authApi.loginWithWechat(
{ code },
{ requestController: signInSubmissionRequestController },
);
if (!pageActive) return;
authenticationCommitted.value = true;
await enterAuthenticatedRoot();
} catch (error) {
if (!pageActive || isRequestCancelled(error)) return;
const errorText = String(error?.errMsg || error?.message || "");
showFeedback(/cancel|取消/i.test(errorText) ? "已取消微信登录" : errorText || "微信登录失败,请稍后重试");
} finally {
if (pageActive) submitting.value = false;
}
};
</script>
<style scoped lang="scss">
.login-page {
height: var(--app-viewport-height);
min-height: 0;
max-height: var(--app-viewport-height);
overflow: hidden;
}
.login-page :deep(.auth-shell__paper) {
min-height: 0;
overflow: hidden;
}
.login-content {
display: flex;
flex: 1;
flex-direction: column;
justify-content: flex-start;
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: clamp(29px, 58rpx, 36px);
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);
margin-top: 6rpx;
}
.login-tab {
display: flex;
flex: 1;
align-items: center;
justify-content: center;
min-height: 88rpx;
color: #5d493b;
font-size: clamp(19px, 34rpx, 24px);
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 {
position: relative;
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-row--required::before {
position: absolute;
top: 8rpx;
left: 0;
content: "*";
color: #b42318;
font-size: clamp(16px, 26rpx, 20px);
line-height: 1;
}
.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: clamp(17px, 30rpx, 22px);
}
.input-placeholder {
color: #766c63;
}
.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 176rpx;
box-sizing: border-box;
align-items: center;
justify-content: flex-end;
min-height: var(--app-touch-min);
padding: 0 8rpx;
background: transparent !important;
color: #a9160d;
font-size: clamp(16px, 28rpx, 20px);
white-space: nowrap;
}
.get-code--disabled {
color: #766c63;
}
.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: clamp(16px, 28rpx, 20px);
}
.login-submit {
display: grid;
margin-top: 26rpx;
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: clamp(19px, 36rpx, 24px);
letter-spacing: 5rpx;
}
.register-entry {
display: flex;
align-items: center;
justify-content: center;
min-height: var(--app-touch-min);
margin-top: 8rpx;
color: #493323;
font-size: clamp(16px, 28rpx, 20px);
}
.register-link {
display: flex;
align-items: center;
min-height: var(--app-touch-min);
margin-left: 8rpx;
color: #a9160d;
}
.agreement-area {
position: relative;
margin-top: 2rpx;
margin-bottom: 48rpx;
color: #493323;
}
.agreement-row {
display: flex;
align-items: center;
flex-wrap: wrap;
justify-content: center;
min-height: var(--app-touch-min);
font-size: clamp(12px, 22rpx, 14px);
line-height: 1.45;
}
.agreement-icon {
flex: 0 0 auto;
width: 34rpx;
height: 34rpx;
}
.agreement-toggle {
display: inline-flex;
flex: 0 0 auto;
align-items: center;
justify-content: center;
min-width: var(--app-touch-min);
min-height: var(--app-touch-min);
box-sizing: border-box;
margin: 0 -12rpx 0 0;
padding-top: 0;
}
.agreement-copy {
display: flex;
flex: 0 0 auto;
flex-wrap: wrap;
align-items: center;
justify-content: center;
min-width: 0;
padding-top: 2rpx;
}
.agreement-copy text {
white-space: normal;
}
.third-party-login {
display: flex;
flex-direction: column;
align-items: center;
margin-top: 22rpx;
gap: 16rpx;
}
.third-party-login__divider {
color: #5f4b3c;
font-size: clamp(12px, 21rpx, 15px);
}
.wechat-login {
display: inline-flex;
min-height: 72rpx;
align-items: center;
justify-content: center;
padding: 0 30rpx;
border: 1rpx solid rgba(66, 107, 88, 0.38);
border-radius: 999rpx;
color: #315943;
font-size: clamp(14px, 23rpx, 17px);
gap: 12rpx;
}
.wechat-login[disabled] {
opacity: 0.68;
}
.wechat-login__mark {
display: inline-flex;
width: 38rpx;
height: 38rpx;
align-items: center;
justify-content: center;
border-radius: 50%;
background: #2f9e57;
color: #fff;
font-size: clamp(11px, 18rpx, 14px);
}
.agreement-link {
display: inline-flex;
align-items: center;
min-height: var(--app-touch-min);
color: #a9160d;
white-space: nowrap;
}
.agreement-error {
position: absolute;
top: calc(100% + 4rpx);
right: 0;
left: 0;
display: block;
color: #b42318;
font-size: clamp(13px, 20rpx, 16px);
line-height: max(1.35em, clamp(17px, 30rpx, 22px));
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;
}
</style>