99 lines
6.3 KiB
Vue
99 lines
6.3 KiB
Vue
<!-- 页面编号:M-05;用途:验证并更换绑定手机号。 -->
|
||
<template>
|
||
<view class="phone-page" :class="{ 'phone-state--ready': phoneState === 'ready', 'phone-state--saving': phoneState === 'saving' }">
|
||
<ModulePageBackground module="profile" />
|
||
<view class="page-layer"><PageHeader title="修改手机号" custom-back @back="requestBack" /></view>
|
||
<view class="page-content page-layer">
|
||
<view class="current-phone"><text>当前绑定手机号</text><text>{{ phoneForm.currentPhone }}</text><text>更换后,新手机号将用于登录与安全验证。</text></view>
|
||
<view class="form-panel">
|
||
<view class="form-row"><text>新手机号</text><input v-model.trim="phoneForm.newPhone" type="number" maxlength="11" aria-label="新手机号" placeholder="请输入新手机号" @input="errors.newPhone = ''" /></view>
|
||
<text v-if="errors.newPhone" class="field-error">{{ errors.newPhone }}</text>
|
||
<view class="form-row code-row"><text>验证码</text><input v-model.trim="phoneForm.code" type="number" maxlength="4" aria-label="短信验证码" placeholder="4 位验证码" @input="errors.code = ''" /><view class="code-action" role="button" aria-label="检查验证码发送条件" @click="sendCode">发送条件</view></view>
|
||
<text v-if="errors.code" class="field-error">{{ errors.code }}</text>
|
||
</view>
|
||
<AppButton block :disabled="phoneState === 'saving'" :label="phoneState === 'saving' ? '正在校验' : '校验换绑信息(不提交)'" @click="savePhone" />
|
||
</view>
|
||
<AppToast :visible="toastVisible" :message="toastMessage" />
|
||
<AppDialog
|
||
:visible="discardVisible"
|
||
:close-on-mask="false"
|
||
eyebrow="放弃确认"
|
||
title="放弃换绑填写?"
|
||
message="新手机号和验证码尚未提交服务器。"
|
||
confirm-text="确认放弃"
|
||
cancel-text="继续填写"
|
||
show-cancel
|
||
@confirm="confirmDiscard"
|
||
@cancel="cancelDiscard"
|
||
/>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { computed, reactive, ref } from "vue";
|
||
import { onBackPress, onUnload } from "@dcloudio/uni-app";
|
||
import AppButton from "@/components/AppButton.vue";
|
||
import AppDialog from "@/components/AppDialog.vue";
|
||
import AppToast from "@/components/AppToast.vue";
|
||
import ModulePageBackground from "@/components/ModulePageBackground.vue";
|
||
import PageHeader from "@/components/PageHeader.vue";
|
||
import { currentUser } from "@/data/mock.js";
|
||
import { createDiscardConfirmation } from "@/utils/discard-confirmation.js";
|
||
import { handleBackPress, runBackGuard } from "@/utils/navigation.js";
|
||
|
||
const phoneForm = reactive({ currentPhone: currentUser.phone, newPhone: "", code: "" });
|
||
const errors = reactive({ newPhone: "", code: "" });
|
||
const phoneState = ref("ready");
|
||
const discardVisible = ref(false);
|
||
const toastVisible = ref(false); const toastMessage = ref("");
|
||
let stateTimer = null; let toastTimer = null;
|
||
const formSnapshot = computed(() => JSON.stringify({ newPhone: phoneForm.newPhone, code: phoneForm.code }));
|
||
const baseline = ref(formSnapshot.value);
|
||
const isDirty = computed(() => formSnapshot.value !== baseline.value);
|
||
const discardConfirmation = createDiscardConfirmation((visible) => {
|
||
discardVisible.value = visible;
|
||
});
|
||
const requestDiscardConfirmation = discardConfirmation.request;
|
||
const confirmDiscard = discardConfirmation.confirm;
|
||
const cancelDiscard = discardConfirmation.cancel;
|
||
const showToast = (message) => { toastMessage.value = message; toastVisible.value = true; clearTimeout(toastTimer); toastTimer = setTimeout(() => (toastVisible.value = false), 1800); };
|
||
const sendCode = () => {
|
||
if (!/^1\d{10}$/.test(phoneForm.newPhone)) { errors.newPhone = "请输入正确的新手机号"; return; }
|
||
errors.newPhone = "";
|
||
showToast("需先完成滑动行为验证;当前未发送验证码");
|
||
};
|
||
const validatePhone = () => {
|
||
errors.newPhone = /^1\d{10}$/.test(phoneForm.newPhone) ? "" : "请输入正确的新手机号";
|
||
errors.code = /^\d{4}$/.test(phoneForm.code) ? "" : "请输入 4 位验证码";
|
||
return !errors.newPhone && !errors.code;
|
||
};
|
||
const savePhone = () => {
|
||
if (!validatePhone() || phoneState.value === "saving") return;
|
||
phoneState.value = "saving";
|
||
stateTimer = setTimeout(() => {
|
||
phoneState.value = "ready";
|
||
showToast("本地校验通过,尚未提交服务器");
|
||
}, 500);
|
||
};
|
||
const requestBack = () =>
|
||
runBackGuard({
|
||
transientOpen: discardVisible.value,
|
||
dirty: isDirty.value,
|
||
submitting: phoneState.value === "saving",
|
||
"close-transient": cancelDiscard,
|
||
"block-submitting": () => true,
|
||
"confirm-discard": requestDiscardConfirmation,
|
||
});
|
||
onBackPress((event) => handleBackPress(event, requestBack));
|
||
onUnload(() => {
|
||
clearTimeout(stateTimer);
|
||
clearTimeout(toastTimer);
|
||
discardConfirmation.dispose();
|
||
});
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
@use "../../styles/adaptive-frame-profiles.scss" as adaptive;
|
||
.phone-page{display:flex;min-height:100vh;flex-direction:column;background:$paper}.page-layer{z-index:1}.page-content{flex:1;padding:28rpx 30rpx 72rpx}.current-phone,.form-panel{@include adaptive.adaptive-profile-content}.current-phone{min-height:210rpx;padding:40rpx 48rpx;text-align:center}.current-phone text{display:block}.current-phone text:first-child{color:$ink-muted;font-size:22rpx}.current-phone text:nth-child(2){margin-top:8rpx;color:$ink;font-size:38rpx;font-weight:700;letter-spacing:3rpx}.current-phone text:last-child{margin-top:12rpx;color:$ink-muted;font-size:21rpx;line-height:1.5}.form-panel{margin-top:22rpx;padding:24rpx 34rpx 32rpx}.form-row{display:grid;grid-template-columns:130rpx minmax(0,1fr);min-height:94rpx;align-items:center;gap:16rpx;border-bottom:1px solid rgba(181,137,63,.42)}.code-row{grid-template-columns:130rpx minmax(0,1fr) auto}.form-row>text{color:$ink;font-size:23rpx;font-weight:700}.form-row input{width:auto;min-width:0;min-height:70rpx;color:$ink;font-size:23rpx}.code-action{display:flex;min-width:126rpx;min-height:70rpx;align-items:center;justify-content:flex-end;color:$brand-red;font-size:21rpx}.field-error{display:block;padding-top:7rpx;color:#b42318;font-size:20rpx;text-align:right}.page-content>.app-button{margin-top:28rpx}@media(max-width:340px){.page-content{padding-right:22rpx;padding-left:22rpx}.form-panel{padding-right:26rpx;padding-left:26rpx}.form-row{grid-template-columns:112rpx minmax(0,1fr)}.code-row{grid-template-columns:112rpx minmax(0,1fr);}.code-action{grid-column:2;justify-content:flex-start}}
|
||
</style>
|