feat: 完成前端业务闭环与后端联调
This commit is contained in:
+58
-7
@@ -78,6 +78,34 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view
|
||||
class="field-block"
|
||||
:class="{ 'field-block--error': fieldErrors.referralCode }"
|
||||
>
|
||||
<view class="input-row">
|
||||
<label class="input-label" for="register-referral-code">推荐码</label>
|
||||
<input
|
||||
id="register-referral-code"
|
||||
v-model.trim="referralCode"
|
||||
class="auth-input"
|
||||
maxlength="64"
|
||||
:disabled="submitting || registrationCommitted"
|
||||
placeholder="选填,来自邀请人的推荐码"
|
||||
placeholder-class="placeholder"
|
||||
:aria-invalid="Boolean(fieldErrors.referralCode)"
|
||||
:aria-describedby="fieldErrors.referralCode ? 'register-referral-code-error' : undefined"
|
||||
@input="clearFieldError('referralCode')"
|
||||
/>
|
||||
</view>
|
||||
<text
|
||||
v-if="fieldErrors.referralCode"
|
||||
id="register-referral-code-error"
|
||||
class="field-error"
|
||||
role="alert"
|
||||
>{{ fieldErrors.referralCode }}</text
|
||||
>
|
||||
</view>
|
||||
|
||||
<view
|
||||
class="field-block"
|
||||
:class="{ 'field-block--error': fieldErrors.verificationCode }"
|
||||
@@ -259,14 +287,14 @@
|
||||
<text>我已阅读并同意</text>
|
||||
<button
|
||||
class="auth-plain-button agreement-link"
|
||||
@click="prepareAgreement"
|
||||
@click="openComplianceDocument('user_agreement')"
|
||||
>
|
||||
《用户协议》
|
||||
</button>
|
||||
<text>与</text>
|
||||
<button
|
||||
class="auth-plain-button agreement-link"
|
||||
@click="prepareAgreement"
|
||||
@click="openComplianceDocument('privacy_policy')"
|
||||
>
|
||||
《隐私政策》
|
||||
</button>
|
||||
@@ -318,7 +346,7 @@
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from "vue";
|
||||
import { onBackPress, onShow, onUnload } from "@dcloudio/uni-app";
|
||||
import { onBackPress, onLoad, onShow, onUnload } from "@dcloudio/uni-app";
|
||||
import AppDialog from "@/components/AppDialog.vue";
|
||||
import AppToast from "@/components/AppToast.vue";
|
||||
import AuthPageShell from "@/components/auth/PageShell.vue";
|
||||
@@ -336,7 +364,12 @@ import {
|
||||
import { createDiscardConfirmation } from "@/utils/discard-confirmation.js";
|
||||
import { calcMD5 } from "@/utils/md5.js";
|
||||
import { createNonIdempotentWriteGuard } from "@/utils/non-idempotent-write-guard.js";
|
||||
import { goRoot, handleBackPress, runBackGuard } from "@/utils/navigation/gateway.js";
|
||||
import {
|
||||
goRoot,
|
||||
handleBackPress,
|
||||
openPage,
|
||||
runBackGuard,
|
||||
} from "@/utils/navigation/gateway.js";
|
||||
import {
|
||||
PASSWORD_POLICY_MESSAGE,
|
||||
validatePassword,
|
||||
@@ -344,6 +377,7 @@ import {
|
||||
|
||||
const phone = ref("");
|
||||
const nickName = ref("");
|
||||
const referralCode = ref("");
|
||||
const verificationCode = ref("");
|
||||
const password = ref("");
|
||||
const confirmPassword = ref("");
|
||||
@@ -351,6 +385,7 @@ const agreed = ref(false);
|
||||
const agreementError = ref(false);
|
||||
const fieldErrors = ref({
|
||||
phone: "",
|
||||
referralCode: "",
|
||||
verificationCode: "",
|
||||
password: "",
|
||||
confirmPassword: "",
|
||||
@@ -366,6 +401,7 @@ const isDirty = computed(
|
||||
Boolean(
|
||||
phone.value ||
|
||||
nickName.value ||
|
||||
referralCode.value ||
|
||||
verificationCode.value ||
|
||||
password.value ||
|
||||
confirmPassword.value ||
|
||||
@@ -374,6 +410,10 @@ const isDirty = computed(
|
||||
);
|
||||
let feedbackTimer = null;
|
||||
let pageActive = true;
|
||||
|
||||
onLoad((options = {}) => {
|
||||
referralCode.value = String(options.referralCode || "").trim().slice(0, 64);
|
||||
});
|
||||
const registrationNavigationFailure =
|
||||
"注册已完成,但暂时无法进入家谱,请再次点击进入";
|
||||
const registrationSubmissionRequestController = createRequestController();
|
||||
@@ -452,7 +492,11 @@ const enterAuthenticatedRoot = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const prepareAgreement = () => showFeedback("协议页面准备中");
|
||||
const openComplianceDocument = (documentKey) => {
|
||||
if (sendingCode.value || submitting.value || registrationCommitted.value)
|
||||
return;
|
||||
return openPage("M13", { documentKey }, "A04");
|
||||
};
|
||||
|
||||
const toggleAgreement = () => {
|
||||
if (sendingCode.value || submitting.value || registrationCommitted.value)
|
||||
@@ -490,11 +534,15 @@ const prepareGetCode = async () => {
|
||||
const validateForm = () => {
|
||||
const nextErrors = {
|
||||
phone: "",
|
||||
referralCode: "",
|
||||
verificationCode: "",
|
||||
password: "",
|
||||
confirmPassword: "",
|
||||
};
|
||||
if (!isAuthPhone(phone.value)) nextErrors.phone = "请输入正确手机号";
|
||||
if (referralCode.value && !/^[A-Za-z0-9_-]{4,64}$/.test(referralCode.value)) {
|
||||
nextErrors.referralCode = "推荐码应为 4 至 64 位字母、数字、短横线或下划线";
|
||||
}
|
||||
if (sentPhone.value !== phone.value)
|
||||
nextErrors.verificationCode = "请先获取当前手机号的验证码";
|
||||
else if (!/^\d{4}$/.test(verificationCode.value))
|
||||
@@ -517,6 +565,7 @@ const submitRegister = async () => {
|
||||
const registrationPayload = {
|
||||
phone: phone.value,
|
||||
nickName: nickName.value,
|
||||
referralCode: referralCode.value,
|
||||
passwordHash: calcMD5(password.value),
|
||||
smsCode: verificationCode.value,
|
||||
};
|
||||
@@ -732,7 +781,7 @@ const submitRegister = async () => {
|
||||
.agreement-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: nowrap;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
min-height: var(--app-touch-min);
|
||||
font-size: clamp(12px, 22rpx, 14px);
|
||||
@@ -761,12 +810,14 @@ const submitRegister = async () => {
|
||||
.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: nowrap;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.agreement-link {
|
||||
|
||||
+138
-6
@@ -175,6 +175,28 @@
|
||||
}}</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 -->
|
||||
|
||||
<view class="register-entry">
|
||||
<text>还没有账号?</text>
|
||||
<button
|
||||
@@ -218,14 +240,14 @@
|
||||
<text>我已阅读并同意</text>
|
||||
<button
|
||||
class="auth-plain-button agreement-link"
|
||||
@click="prepareAgreement"
|
||||
@click="openComplianceDocument('user_agreement')"
|
||||
>
|
||||
《用户协议》
|
||||
</button>
|
||||
<text>与</text>
|
||||
<button
|
||||
class="auth-plain-button agreement-link"
|
||||
@click="prepareAgreement"
|
||||
@click="openComplianceDocument('privacy_policy')"
|
||||
>
|
||||
《隐私政策》
|
||||
</button>
|
||||
@@ -294,6 +316,7 @@ 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("");
|
||||
@@ -363,7 +386,36 @@ const restoreAuthenticatedSession = () => {
|
||||
void enterAuthenticatedRoot();
|
||||
};
|
||||
|
||||
onShow(restoreAuthenticatedSession);
|
||||
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;
|
||||
@@ -685,7 +737,43 @@ const prepareRegister = () => {
|
||||
return openPage("A04", {}, "A01");
|
||||
};
|
||||
|
||||
const prepareAgreement = () => showFeedback("协议页面准备中");
|
||||
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">
|
||||
@@ -906,7 +994,7 @@ const prepareAgreement = () => showFeedback("协议页面准备中");
|
||||
.agreement-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: nowrap;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
min-height: var(--app-touch-min);
|
||||
font-size: clamp(12px, 22rpx, 14px);
|
||||
@@ -934,12 +1022,56 @@ const prepareAgreement = () => showFeedback("协议页面准备中");
|
||||
.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: nowrap;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.third-party-login {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-top: 22rpx;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.third-party-login__divider {
|
||||
color: rgba(76, 57, 41, 0.62);
|
||||
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.52;
|
||||
}
|
||||
|
||||
.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 {
|
||||
|
||||
Reference in New Issue
Block a user