This commit is contained in:
rain
2026-07-24 18:03:02 +08:00
parent c7f278fe79
commit ec8486b035
86 changed files with 7943 additions and 1841 deletions
+192 -4
View File
@@ -1,11 +1,199 @@
<!-- 页面编号M-02用途编辑资料读取 DTO 与现有字段不一致时保持关闭 -->
<!-- 页面编号M-02用途编辑已由 Apifox 声明的个人资料字段 -->
<template>
<view class="profile-edit-page"><ModulePageBackground module="profile" /><view class="page-header"><PageHeader title="编辑资料" custom-back @back="backToProfile" /></view><view class="page-content"><view class="state-card"><text>个人资料编辑待字段合同</text><text>读取接口没有资料 DTO更新接口允许昵称头像性别生日地区和地址但当前页面原有真实姓名邮箱等字段不属于该合同页面不再用 mock 预填或提交未知字段</text><AppButton block label="返回个人中心" @click="backToProfile" /></view></view></view>
<view class="profile-edit-page">
<ModulePageBackground module="profile" />
<view class="page-header"><PageHeader title="编辑资料" custom-back @back="backToProfile" /></view>
<view class="page-content">
<view v-if="loading" class="state-card"><text>正在读取个人资料</text></view>
<view v-else-if="loadError" class="state-card">
<text>个人资料暂时无法读取</text>
<text>{{ loadError }}</text>
<AppButton block label="重新读取" @click="loadProfile" />
</view>
<template v-else>
<view class="form-panel">
<view class="avatar-row">
<view><text>头像</text><text>{{ avatarMessage }}</text></view>
<button class="upload-button" :disabled="uploading || saving" @click="uploadAvatar">{{ uploading ? "上传中" : "选择图片" }}</button>
</view>
<text v-if="avatarError" class="field-error">{{ avatarError }}</text>
<view class="form-row"><text>昵称</text><input v-model.trim="form.nickName" maxlength="30" placeholder="请输入昵称" placeholder-class="placeholder" :disabled="saving || uploading" /></view>
<view class="form-row"><text>真实姓名</text><input v-model.trim="form.realName" maxlength="30" placeholder="请输入真实姓名" placeholder-class="placeholder" :disabled="saving || uploading" /></view>
<view class="form-row"><text>性别</text><input v-model.trim="form.sex" placeholder="请输入性别" placeholder-class="placeholder" :disabled="saving || uploading" /></view>
<view class="form-row">
<text>生日</text>
<picker mode="date" :value="form.birthday" :disabled="saving || uploading" @change="changeBirthday"><view class="picker-value" :class="{ 'picker-value--placeholder': !form.birthday }">{{ form.birthday || "请选择生日" }}</view></picker>
</view>
<view class="form-row"><text>邮箱</text><input v-model.trim="form.email" maxlength="100" placeholder="请输入邮箱" placeholder-class="placeholder" :disabled="saving || uploading" /></view>
</view>
<text class="form-note">资料字段均可按需填写留空不会提交为修改</text>
<text v-if="saveError" class="save-error">{{ saveError }}</text>
<AppButton block :disabled="saving || uploading" :label="saving ? '保存中…' : '保存资料'" @click="saveProfile" />
</template>
</view>
<AppToast :visible="feedbackVisible" :message="feedbackMessage" />
</view>
</template>
<script setup>
import AppButton from "@/components/AppButton.vue"; import ModulePageBackground from "@/components/ModulePageBackground.vue"; import PageHeader from "@/components/PageHeader.vue"; import { returnTo } from "@/utils/navigation.js"; const backToProfile=()=>returnTo("M01");
import { computed, reactive, ref } from "vue";
import { onLoad, onUnload } from "@dcloudio/uni-app";
import AppButton from "@/components/AppButton.vue";
import AppToast from "@/components/AppToast.vue";
import ModulePageBackground from "@/components/ModulePageBackground.vue";
import PageHeader from "@/components/PageHeader.vue";
import { appApi, createRequestController, isRequestCancelled } from "@/utils/api.js";
import { isImagePickCancelled, pickAndUploadImage, toConsumerOssId } from "@/utils/resumable-image-upload.js";
import { finishPage, returnTo } from "@/utils/navigation.js";
const form = reactive({ nickName: "", realName: "", sex: "", birthday: "", email: "" });
const original = reactive({ nickName: "", realName: "", sex: "", birthday: "", email: "", avatar: null });
const avatarId = ref(null);
const avatarFileName = ref("");
const loading = ref(true);
const loadError = ref("");
const uploading = ref(false);
const avatarError = ref("");
const saving = ref(false);
const saveError = ref("");
const feedbackVisible = ref(false);
const feedbackMessage = ref("");
const requestController = createRequestController();
let feedbackTimer = null;
let pageActive = true;
const avatarMessage = computed(() => {
if (avatarFileName.value) return `已选择 ${avatarFileName.value}`;
return avatarId.value ? "当前头像已设置" : "尚未设置头像";
});
const showFeedback = (message) => {
feedbackMessage.value = message;
feedbackVisible.value = true;
if (feedbackTimer) clearTimeout(feedbackTimer);
feedbackTimer = setTimeout(() => {
feedbackVisible.value = false;
feedbackTimer = null;
}, 2200);
};
const toBirthdayDate = (value) => /^\d{4}-\d{2}-\d{2}/.test(value) ? value.slice(0, 10) : "";
const loadProfile = async () => {
if (loading.value === false && (uploading.value || saving.value)) return;
loading.value = true;
loadError.value = "";
try {
const profile = await appApi.getProfile({ requestController });
if (!pageActive) return;
Object.assign(form, {
nickName: profile.nickName,
realName: profile.realName,
sex: profile.sex,
birthday: toBirthdayDate(profile.birthday),
email: profile.email,
});
Object.assign(original, { ...form, avatar: profile.avatar });
avatarId.value = profile.avatar;
avatarFileName.value = "";
} catch (error) {
if (pageActive && !isRequestCancelled(error)) loadError.value = error?.message || "请稍后重试";
} finally {
if (pageActive) loading.value = false;
}
};
const changeBirthday = (event) => {
form.birthday = event?.detail?.value || "";
};
const uploadAvatar = async () => {
if (uploading.value || saving.value) return;
uploading.value = true;
avatarError.value = "";
try {
const receipt = await pickAndUploadImage({ requestController });
if (!pageActive) return;
avatarId.value = toConsumerOssId(receipt.ossId);
avatarFileName.value = receipt.fileName || "新头像";
} catch (error) {
if (pageActive && !isImagePickCancelled(error) && !isRequestCancelled(error)) {
avatarError.value = error?.message || "头像上传失败,请稍后重试";
}
} finally {
if (pageActive) uploading.value = false;
}
};
const buildUpdatePayload = () => {
const data = {};
for (const field of ["nickName", "realName", "sex", "email"]) {
if (form[field] && form[field] !== original[field]) data[field] = form[field];
}
if (form.birthday && form.birthday !== original.birthday) data.birthday = `${form.birthday}T00:00:00`;
if (avatarId.value && avatarId.value !== original.avatar) data.avatar = avatarId.value;
return data;
};
const saveProfile = async () => {
if (saving.value || uploading.value) return;
const payload = buildUpdatePayload();
if (Object.keys(payload).length === 0) {
showFeedback("没有需要保存的修改");
return;
}
saving.value = true;
saveError.value = "";
try {
await appApi.updateProfile(payload, { requestController });
if (!pageActive) return;
await finishPage("M01", {}, { operation: "profile-updated", refresh: true });
} catch (error) {
if (pageActive && !isRequestCancelled(error)) saveError.value = error?.message || "保存失败,请稍后重试";
} finally {
if (pageActive) saving.value = false;
}
};
const backToProfile = () => {
if (saving.value || uploading.value) return;
return returnTo("M01");
};
onLoad(loadProfile);
onUnload(() => {
pageActive = false;
requestController.abort();
if (feedbackTimer) clearTimeout(feedbackTimer);
});
</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-header,.page-content{z-index:1}.page-content{padding:18rpx 24rpx 72rpx}.state-card{box-sizing:border-box;min-height:340rpx;padding:78rpx 52rpx 50rpx;text-align:center;@include adaptive.adaptive-family-content}.state-card text{display:block}.state-card text:first-child{color:$ink;font-family:STKaiti,KaiTi,serif;font-size:35rpx;font-weight:700}.state-card text:nth-child(2){margin-top:18rpx;color:$ink-muted;font-size:24rpx;line-height:1.65}.state-card .app-button{margin-top:28rpx}
.profile-edit-page { display: flex; min-height: 100vh; flex-direction: column; background: $paper; }
.page-header, .page-content { z-index: 1; }
.page-content { flex: 1; padding: 24rpx 30rpx 72rpx; }
.state-card, .form-panel { @include adaptive.adaptive-profile-content; }
.state-card { box-sizing: border-box; min-height: 340rpx; padding: 78rpx 52rpx 50rpx; text-align: center; }
.state-card text { display: block; }
.state-card text:first-child { color: $ink; font-family: STKaiti, KaiTi, serif; font-size: 35rpx; font-weight: 700; }
.state-card text:nth-child(2) { margin-top: 18rpx; color: $ink-muted; font-size: 24rpx; line-height: 1.65; }
.state-card .app-button { margin-top: 28rpx; }
.form-panel { padding: 16rpx 30rpx 22rpx; }
.avatar-row, .form-row { display: grid; grid-template-columns: 142rpx minmax(0, 1fr); min-height: 92rpx; align-items: center; gap: 16rpx; border-bottom: 1rpx solid rgba(181, 137, 63, .42); }
.avatar-row > view text { display: block; }
.avatar-row > view text:first-child, .form-row > text { color: $ink; font-size: 23rpx; font-weight: 700; }
.avatar-row > view text:last-child { margin-top: 4rpx; color: $ink-muted; font-size: 19rpx; overflow-wrap: anywhere; }
.upload-button { display: flex; min-height: 64rpx; align-items: center; justify-content: center; border: 0; background: transparent; color: $brand-red; font-size: 22rpx; text-align: right; }
.form-row input, .picker-value { width: auto; min-width: 0; min-height: 68rpx; color: $ink; font-size: 23rpx; text-align: right; }
.picker-value { display: flex; align-items: center; justify-content: flex-end; }
.picker-value--placeholder, .placeholder { color: #ab9a86; }
.field-error, .save-error, .form-note { display: block; color: $ink-muted; font-size: 20rpx; line-height: 1.5; }
.field-error, .save-error { color: #b42318; }
.field-error { margin-top: 8rpx; text-align: right; }
.form-note { margin: 16rpx 4rpx 0; }
.save-error { margin: 12rpx 4rpx 0; }
.page-content > .app-button { margin-top: 24rpx; }
@media (max-width: 340px) { .page-content { padding-right: 22rpx; padding-left: 22rpx; } .form-panel { padding-right: 24rpx; padding-left: 24rpx; } .avatar-row, .form-row { grid-template-columns: 116rpx minmax(0, 1fr); gap: 12rpx; } }
</style>