完成40%
This commit is contained in:
+172
-43
@@ -13,10 +13,11 @@
|
||||
|
||||
<view class="login-tabs" role="tablist" aria-label="登录方式">
|
||||
<button
|
||||
class="auth-plain-button login-tab login-tab--unavailable"
|
||||
class="auth-plain-button login-tab"
|
||||
role="tab"
|
||||
:aria-selected="activeLoginMethod === 'password'"
|
||||
aria-disabled="true"
|
||||
:class="{ active: activeLoginMethod === 'password' }"
|
||||
:disabled="submitting || sendingCode || tacVisible"
|
||||
hover-class="tap-fade"
|
||||
@click="switchLoginMethod('password')"
|
||||
>密码登录</button
|
||||
@@ -26,6 +27,7 @@
|
||||
role="tab"
|
||||
:aria-selected="activeLoginMethod === 'sms'"
|
||||
:class="{ active: activeLoginMethod === 'sms' }"
|
||||
:disabled="submitting || sendingCode || tacVisible"
|
||||
hover-class="tap-fade"
|
||||
@click="switchLoginMethod('sms')"
|
||||
>验证码登录</button
|
||||
@@ -44,11 +46,12 @@
|
||||
class="auth-input"
|
||||
type="number"
|
||||
maxlength="11"
|
||||
:disabled="sendingCode || cooldownSeconds > 0 || submitting"
|
||||
:disabled="sendingCode || submitting || tacVisible || authenticationCommitted"
|
||||
placeholder="手机号"
|
||||
aria-label="手机号"
|
||||
placeholder-class="input-placeholder"
|
||||
confirm-type="next"
|
||||
@input="handlePhoneInput"
|
||||
/>
|
||||
</view>
|
||||
|
||||
@@ -63,7 +66,7 @@
|
||||
class="auth-input"
|
||||
:password="!passwordVisible"
|
||||
maxlength="32"
|
||||
:disabled="submitting"
|
||||
:disabled="submitting || tacVisible"
|
||||
placeholder="密码"
|
||||
aria-label="登录密码"
|
||||
placeholder-class="input-placeholder"
|
||||
@@ -74,6 +77,7 @@
|
||||
class="auth-plain-button password-toggle"
|
||||
:aria-label="passwordVisible ? '隐藏密码' : '显示密码'"
|
||||
:aria-pressed="passwordVisible"
|
||||
:disabled="submitting || tacVisible"
|
||||
hover-class="tap-fade"
|
||||
@click="togglePasswordVisibility"
|
||||
>
|
||||
@@ -140,7 +144,15 @@
|
||||
src="/static/assets/foundation/transparent/a01-scroll-primary-v3.png"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<text class="login-submit__copy">{{ submitting ? "登录中…" : "登录" }}</text>
|
||||
<text class="login-submit__copy">{{
|
||||
submitting
|
||||
? authenticationCommitted
|
||||
? "正在进入…"
|
||||
: "登录中…"
|
||||
: authenticationCommitted
|
||||
? "进入家谱"
|
||||
: "登录"
|
||||
}}</text>
|
||||
</button>
|
||||
|
||||
<view class="other-login-divider">
|
||||
@@ -249,7 +261,7 @@
|
||||
|
||||
<script setup>
|
||||
import { ref } from "vue";
|
||||
import { onBackPress, onUnload } from "@dcloudio/uni-app";
|
||||
import { onBackPress, onShow, onUnload } from "@dcloudio/uni-app";
|
||||
import AuthPageShell from "@/components/AuthPageShell.vue";
|
||||
import AppToast from "@/components/AppToast.vue";
|
||||
import TacVerification from "@/components/TacVerification.vue";
|
||||
@@ -260,13 +272,15 @@ import {
|
||||
} from "@/utils/api.js";
|
||||
import {
|
||||
AUTH_TAC_SCENE,
|
||||
PASSWORD_TAC_BLOCKED_MESSAGE,
|
||||
createTacRenderContext,
|
||||
isAuthPhone,
|
||||
isSmsDeliveryOutcomeUnknown,
|
||||
normalizeCaptchaRequirement,
|
||||
normalizeTacSuccess,
|
||||
} from "@/utils/auth-verification.js";
|
||||
import { createAuthSmsCooldown } from "@/utils/auth-sms-cooldown.js";
|
||||
import { runtimeConfig } from "@/utils/config.js";
|
||||
import { calcMD5 } from "@/utils/md5.js";
|
||||
import {
|
||||
goRoot,
|
||||
handleBackPress,
|
||||
@@ -274,7 +288,7 @@ import {
|
||||
runBackGuard,
|
||||
} from "@/utils/navigation.js";
|
||||
|
||||
const activeLoginMethod = ref("sms");
|
||||
const activeLoginMethod = ref("password");
|
||||
const passwordVisible = ref(false);
|
||||
const phone = ref("");
|
||||
const password = ref("");
|
||||
@@ -285,21 +299,32 @@ const tacVisible = ref(false);
|
||||
const tacContext = ref(null);
|
||||
const sendingCode = ref(false);
|
||||
const submitting = ref(false);
|
||||
const authenticationCommitted = ref(false);
|
||||
const cooldownSeconds = ref(0);
|
||||
const sentPhone = ref("");
|
||||
const feedbackVisible = ref(false);
|
||||
const feedbackMessage = ref("");
|
||||
let feedbackTimer = null;
|
||||
let cooldownTimer = null;
|
||||
let tacSequence = 0;
|
||||
let pendingTacAction = null;
|
||||
let pageActive = true;
|
||||
const authenticationNavigationFailure =
|
||||
"登录已完成,但暂时无法进入家谱,请再次点击进入";
|
||||
const authRequestController = createRequestController();
|
||||
const smsCooldown = createAuthSmsCooldown({
|
||||
sceneCode: AUTH_TAC_SCENE.SMS_LOGIN,
|
||||
onChange: (seconds) => {
|
||||
cooldownSeconds.value = seconds;
|
||||
},
|
||||
});
|
||||
|
||||
onUnload(() => {
|
||||
pageActive = false;
|
||||
authRequestController.abort();
|
||||
if (feedbackTimer) clearTimeout(feedbackTimer);
|
||||
if (cooldownTimer) clearInterval(cooldownTimer);
|
||||
smsCooldown.dispose();
|
||||
});
|
||||
onShow(() => smsCooldown.sync());
|
||||
|
||||
const showFeedback = (message) => {
|
||||
feedbackMessage.value = message;
|
||||
@@ -311,6 +336,21 @@ const showFeedback = (message) => {
|
||||
}, 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 blockBusyAction = () => {
|
||||
if (!sendingCode.value && !submitting.value) return false;
|
||||
showFeedback("请求处理中,请稍候");
|
||||
@@ -319,17 +359,19 @@ const blockBusyAction = () => {
|
||||
|
||||
const switchLoginMethod = (method) => {
|
||||
if (blockBusyAction()) return;
|
||||
if (method === "password") {
|
||||
showFeedback(PASSWORD_TAC_BLOCKED_MESSAGE);
|
||||
return;
|
||||
}
|
||||
if (method === "sms") activeLoginMethod.value = method;
|
||||
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;
|
||||
@@ -350,6 +392,7 @@ const requireAgreement = () => {
|
||||
const closeTac = () => {
|
||||
tacVisible.value = false;
|
||||
tacContext.value = null;
|
||||
pendingTacAction = null;
|
||||
};
|
||||
|
||||
const cancelPendingRequest = () => {
|
||||
@@ -380,18 +423,6 @@ onBackPress((event) => {
|
||||
return handleBackPress(event, requestBack);
|
||||
});
|
||||
|
||||
const startCooldown = () => {
|
||||
cooldownSeconds.value = 60;
|
||||
if (cooldownTimer) clearInterval(cooldownTimer);
|
||||
cooldownTimer = setInterval(() => {
|
||||
cooldownSeconds.value -= 1;
|
||||
if (cooldownSeconds.value <= 0) {
|
||||
clearInterval(cooldownTimer);
|
||||
cooldownTimer = null;
|
||||
}
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
const prepareGetCode = async () => {
|
||||
if (sendingCode.value || cooldownSeconds.value > 0) return;
|
||||
if (!validatePhone() || !requireAgreement()) return;
|
||||
@@ -416,6 +447,10 @@ const prepareGetCode = async () => {
|
||||
subject: requestedPhone,
|
||||
requirement,
|
||||
});
|
||||
pendingTacAction = {
|
||||
kind: "sms-code",
|
||||
phone: requestedPhone,
|
||||
};
|
||||
tacVisible.value = true;
|
||||
} catch (error) {
|
||||
if (pageActive && !isRequestCancelled(error)) {
|
||||
@@ -426,12 +461,83 @@ const prepareGetCode = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const preparePasswordLogin = async () => {
|
||||
const requestedPhone = phone.value;
|
||||
const passwordHash = calcMD5(password.value);
|
||||
submitting.value = true;
|
||||
try {
|
||||
// 产品规则:密码登录也必须先完成滑动验证。当前登录接口不消费
|
||||
// validToken,因此票据只作为本次前端验证成功的完成信号,不写入登录体。
|
||||
const sceneCode = AUTH_TAC_SCENE.SMS_LOGIN;
|
||||
const response = await appApi.getCaptchaRequirement(
|
||||
{ sceneCode, subject: requestedPhone },
|
||||
{ requestController: authRequestController },
|
||||
);
|
||||
if (!pageActive) return;
|
||||
if (phone.value !== requestedPhone) {
|
||||
throw new Error("手机号已变化,请重新登录");
|
||||
}
|
||||
const requirement = normalizeCaptchaRequirement(response, sceneCode);
|
||||
tacSequence += 1;
|
||||
tacContext.value = createTacRenderContext({
|
||||
requestId: `a01-password-${tacSequence}`,
|
||||
baseUrl: runtimeConfig.baseUrl,
|
||||
clientId: runtimeConfig.clientId,
|
||||
tenantId: runtimeConfig.tenantId,
|
||||
sceneCode,
|
||||
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;
|
||||
if (!expectedContext) return;
|
||||
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 appApi.loginWithPassword(
|
||||
{
|
||||
phone: action.phone,
|
||||
passwordHash: action.passwordHash,
|
||||
},
|
||||
{ requestController: authRequestController },
|
||||
);
|
||||
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 appApi.sendSmsCode(
|
||||
@@ -443,15 +549,33 @@ const completeTac = async (result) => {
|
||||
{ requestController: authRequestController },
|
||||
);
|
||||
if (!pageActive) return;
|
||||
startCooldown();
|
||||
sentPhone.value = expectedContext.subject;
|
||||
smsCooldown.start();
|
||||
showFeedback("验证码已发送");
|
||||
} catch (error) {
|
||||
closeTac();
|
||||
if (pageActive && !isRequestCancelled(error)) {
|
||||
showFeedback(error.message || "验证码发送失败");
|
||||
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;
|
||||
if (pageActive) {
|
||||
sendingCode.value = false;
|
||||
submitting.value = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -465,11 +589,19 @@ const handleTacError = ({ 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)
|
||||
@@ -479,10 +611,7 @@ const submitLogin = async () => {
|
||||
}
|
||||
if (!requireAgreement()) return;
|
||||
if (activeLoginMethod.value === "password") {
|
||||
// 受保护源合同明确禁止密码登录携带 TAC 票据。仅在客户端先滑动仍可被
|
||||
// 绕过,因此此入口必须失败关闭;短信登录已经具备 TAC→短信票据闭环。
|
||||
showFeedback(PASSWORD_TAC_BLOCKED_MESSAGE);
|
||||
return;
|
||||
return preparePasswordLogin();
|
||||
}
|
||||
submitting.value = true;
|
||||
try {
|
||||
@@ -494,7 +623,8 @@ const submitLogin = async () => {
|
||||
{ requestController: authRequestController },
|
||||
);
|
||||
if (!pageActive) return;
|
||||
await goRoot("G01");
|
||||
authenticationCommitted.value = true;
|
||||
await enterAuthenticatedRoot();
|
||||
} catch (error) {
|
||||
if (pageActive && !isRequestCancelled(error)) {
|
||||
showFeedback(error.message || "登录失败,请稍后重试");
|
||||
@@ -647,17 +777,16 @@ const prepareAgreement = () => showFeedback("协议页面准备中");
|
||||
|
||||
.get-code {
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
flex: 0 0 176rpx;
|
||||
box-sizing: border-box;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
min-width: 142rpx;
|
||||
min-height: var(--app-touch-min);
|
||||
padding: 0 8rpx;
|
||||
background: transparent !important;
|
||||
color: #a9160d;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.login-tab--unavailable {
|
||||
color: #9f968d;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.get-code--disabled {
|
||||
|
||||
Reference in New Issue
Block a user