完成40%

This commit is contained in:
rain
2026-07-23 17:21:27 +08:00
parent f1edc6b533
commit bb6431b319
114 changed files with 10931 additions and 877 deletions
+172 -43
View File
@@ -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 {