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

203 lines
9.6 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用途编辑已由 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 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 form-row--readonly"><text>性别</text><view class="readonly-value"><text>{{ form.sex ? "待确认" : "未设置" }}</text><text>选项待提供</text></view></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 { 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 { 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: 88rpx; 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; }
.readonly-value { display:flex; min-height:68rpx; align-items:center; justify-content:flex-end; gap:12rpx; color:$ink-muted; }
.readonly-value text:first-child { color:$ink; font-size:23rpx; }
.readonly-value text:last-child { color:$ink-muted; font-size:19rpx; }
.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>