203 lines
9.6 KiB
Vue
203 lines
9.6 KiB
Vue
<!-- 页面编号: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>
|