Files
jiapuapp/pages/profile/m02-edit-profile.vue
T
2026-07-23 08:24:07 +08:00

125 lines
6.7 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-02用途编辑个人资料 -->
<template>
<view class="profile-edit-page" :class="{ 'profile-state--ready': profileState === 'ready', 'profile-state--saving': profileState === 'saving' }">
<ModulePageBackground module="profile" />
<view class="page-layer"><PageHeader title="个人资料" custom-back @back="requestBack" /></view>
<view class="page-content page-layer">
<view class="profile-avatar-card">
<image class="avatar-seal" src="/static/assets/foundation/transparent/brand-seal.png" mode="aspectFit" />
<view class="avatar-copy"><text>{{ profileForm.nickName || "未填写昵称" }}</text><text>头像将在上传接口批次接入</text></view>
<view class="text-action" role="button" aria-label="选择头像" @click="chooseAvatar">选择头像</view>
</view>
<view class="form-panel">
<view class="form-row"><text>昵称</text><input v-model.trim="profileForm.nickName" maxlength="30" aria-label="昵称" placeholder="请输入昵称" @input="errors.nickName = ''" /></view>
<text v-if="errors.nickName" class="field-error">{{ errors.nickName }}</text>
<view class="form-row"><text>真实姓名</text><input v-model.trim="profileForm.realName" maxlength="30" aria-label="真实姓名" placeholder="请输入真实姓名" /></view>
<view class="form-row"><text>邮箱</text><input v-model.trim="profileForm.email" maxlength="100" aria-label="邮箱" placeholder="选填,用于接收通知" @input="errors.email = ''" /></view>
<text v-if="errors.email" class="field-error">{{ errors.email }}</text>
</view>
<AppButton block :disabled="profileState === 'saving'" :label="profileState === 'saving' ? '正在校验' : '生成本地校验预览'" @click="saveProfile" />
</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 profileForm = reactive({ nickName: currentUser.name, realName: currentUser.name, email: "" });
const errors = reactive({ nickName: "", email: "" });
const profileState = ref("ready");
const toastVisible = ref(false);
const toastMessage = ref("");
const discardVisible = ref(false);
let timer = null;
const formSnapshot = computed(() => JSON.stringify(profileForm));
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(timer); timer = setTimeout(() => (toastVisible.value = false), 1800); };
const chooseAvatar = () => showToast("头像选择将在相册权限接入后开放");
const validateProfile = () => {
errors.nickName = profileForm.nickName ? "" : "请填写昵称";
errors.email = !profileForm.email || /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(profileForm.email)
? ""
: "请输入正确的邮箱地址";
return !errors.nickName && !errors.email;
};
const saveProfile = () => {
if (!validateProfile() || profileState.value === "saving") return;
profileState.value = "saving";
clearTimeout(timer);
timer = setTimeout(() => {
profileState.value = "ready";
showToast("本地校验通过,尚未提交服务器");
}, 500);
};
const requestBack = () =>
runBackGuard({
transientOpen: discardVisible.value,
dirty: isDirty.value,
submitting: profileState.value === "saving",
"close-transient": cancelDiscard,
"block-submitting": () => true,
"confirm-discard": requestDiscardConfirmation,
});
onBackPress((event) => handleBackPress(event, requestBack));
onUnload(() => {
clearTimeout(timer);
discardConfirmation.dispose();
});
</script>
<style scoped lang="scss">
@use "../../styles/adaptive-frame-profiles.scss" as adaptive;
.profile-edit-page { display: flex; min-height: 100vh; flex-direction: column; background: $paper; }
.page-layer { z-index: 1; }
.page-content { flex: 1; padding: 26rpx 30rpx 70rpx; }
.profile-avatar-card, .form-panel { @include adaptive.adaptive-profile-field; }
.profile-avatar-card { display: grid; grid-template-columns: 92rpx minmax(0,1fr) auto; align-items: center; gap: 20rpx; min-height: 150rpx; padding: 24rpx 34rpx; box-sizing: border-box; }
.avatar-seal { width: 82rpx; height: auto; max-height: 92rpx; aspect-ratio: 82 / 92; }
.avatar-copy { min-width: 0; }
.avatar-copy text { display: block; overflow-wrap: anywhere; }
.avatar-copy text:first-child { color: $ink; font-size: 29rpx; font-weight: 700; }
.avatar-copy text:last-child { margin-top: 6rpx; color: $ink-muted; font-size: 20rpx; line-height: 1.45; }
.text-action { min-height: 72rpx; display: flex; align-items: center; color: $brand-red; font-size: 22rpx; }
.form-panel { margin-top: 22rpx; padding: 20rpx 34rpx 30rpx; box-sizing: border-box; }
.form-row { display: grid; grid-template-columns: 150rpx minmax(0,1fr); min-height: 92rpx; align-items: center; border-bottom: 1px solid rgba(181,137,63,.42); gap: 18rpx; }
.form-row > text { color: $ink; font-size: 24rpx; font-weight: 700; }
.form-row input { width: auto; min-width: 0; min-height: 70rpx; color: $ink; font-size: 24rpx; text-align: right; }
.field-error { display: block; padding-top: 7rpx; color: #b42318; font-size: 21rpx; text-align: right; }
.page-content > .app-button { margin-top: 26rpx; }
@media(max-width:340px){.page-content{padding-right:22rpx;padding-left:22rpx}.profile-avatar-card{grid-template-columns:72rpx minmax(0,1fr);padding-right:26rpx;padding-left:26rpx}.text-action{grid-column:2}.avatar-seal{width:68rpx;max-height:78rpx}.form-row{grid-template-columns:126rpx minmax(0,1fr)}}
</style>