Files
jiapuapp/pages/profile/m05-change-phone.vue
T
2026-07-23 08:24:07 +08:00

99 lines
6.3 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.
<!-- 页面编号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>