feat: 完成前端业务闭环与后端联调

This commit is contained in:
2026-08-24 23:37:56 +08:00
parent c59e36f933
commit 9ad572907b
175 changed files with 10711 additions and 1740 deletions
+58 -8
View File
@@ -109,14 +109,27 @@
</template>
</view>
<AppToast :visible="feedbackVisible" :message="feedbackMessage" />
<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 { onLoad, onUnload } from "@dcloudio/uni-app";
import { onBackPress, onLoad, onUnload } from "@dcloudio/uni-app";
import AppAvatar from "@/components/AppAvatar.vue";
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";
@@ -129,11 +142,16 @@ import {
} from "@/services/api/request-controller.js";
import { profileApi } from "@/services/api/profile-service.js";
import { getRequestErrorMessage } from "@/services/api/request-error-message.js";
import { createDiscardConfirmation } from "@/utils/discard-confirmation.js";
import {
isImagePickCancelled,
pickAndUploadImage,
} from "@/utils/media-upload.js";
import { finishPage, returnTo } from "@/utils/navigation/gateway.js";
import {
finishPage,
handleBackPress,
runBackGuard,
} from "@/utils/navigation/gateway.js";
const form = reactive({
nickName: "",
@@ -162,11 +180,18 @@ const saveError = ref("");
const committedProfilePayload = ref("");
const feedbackVisible = ref(false);
const feedbackMessage = ref("");
const discardVisible = ref(false);
const profileReadController = createRequestController();
const avatarUploadController = createRequestController();
const profileSaveController = createRequestController();
let feedbackTimer = null;
let isPageActive = true;
const discardConfirmation = createDiscardConfirmation((visible) => {
discardVisible.value = visible;
});
const requestDiscardConfirmation = discardConfirmation.request;
const confirmDiscard = discardConfirmation.confirm;
const cancelDiscard = discardConfirmation.cancel;
const sexOptions = Object.freeze([
Object.freeze({ value: "", label: "请选择" }),
...PROFILE_SEX_OPTIONS,
@@ -181,6 +206,16 @@ const avatarMessage = computed(() => {
if (avatarFileName.value) return `已选择 ${avatarFileName.value}`;
return avatarPreviewUrl.value ? "当前头像已设置" : "当前使用默认头像";
});
const profileSnapshot = computed(() => JSON.stringify({
nickName: form.nickName,
realName: form.realName,
sex: form.sex,
birthday: form.birthday,
email: form.email,
avatar: avatarId.value || original.avatar || null,
}));
const originalSnapshot = computed(() => JSON.stringify(original));
const isDirty = computed(() => profileSnapshot.value !== originalSnapshot.value);
const showFeedback = (message) => {
feedbackMessage.value = message;
@@ -291,6 +326,15 @@ const saveProfile = async () => {
});
if (!isPageActive) return;
committedProfilePayload.value = payloadFingerprint;
for (const field of ["nickName", "realName", "sex", "birthday", "email"]) {
if (Object.prototype.hasOwnProperty.call(payload, field)) {
original[field] = payload[field];
}
}
if (Object.prototype.hasOwnProperty.call(payload, "avatar")) {
original.avatar = payload.avatar;
avatarId.value = null;
}
}
await finishPage(
"M01",
@@ -307,18 +351,24 @@ const saveProfile = async () => {
}
};
const backToProfile = () => {
if (saving.value || uploading.value) return;
return returnTo("M01");
};
const backToProfile = () => runBackGuard({
transientOpen: discardVisible.value,
dirty: isDirty.value,
submitting: saving.value || uploading.value,
"close-transient": cancelDiscard,
"block-submitting": () => true,
"confirm-discard": requestDiscardConfirmation,
});
onLoad(loadProfile);
onBackPress((event) => handleBackPress(event, backToProfile));
onUnload(() => {
isPageActive = false;
profileReadController.abort();
avatarUploadController.abort();
profileSaveController.abort();
if (feedbackTimer) clearTimeout(feedbackTimer);
discardConfirmation.dispose();
});
</script>
@@ -336,7 +386,7 @@ onUnload(() => {
}
.page-content {
flex: 1;
padding: 24rpx 30rpx 72rpx;
padding: 24rpx 30rpx calc(72rpx + env(safe-area-inset-bottom));
}
.state-card,
.form-panel {
@@ -435,7 +485,7 @@ onUnload(() => {
.picker-value {
width: auto;
min-width: 0;
min-height: 68rpx;
min-height: 80rpx;
color: $ink;
font-size: clamp(14px, 23rpx, 17px);
text-align: right;