Files
jiapuapp/pages/records/r04-gift-editor.vue
T
2026-07-27 06:50:23 +08:00

123 lines
10 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.
<!-- 页面编号R-04用途 Apifox 的完整 AppRelativeRecordBody 创建亲友往来记录 -->
<template>
<view class="gift-editor-page" :class="`relative-editor-state--${editorState}`">
<ModulePageBackground module="records" />
<view class="page-header"><PageHeader title="新建往来记录" custom-back @back="requestBack" /></view>
<view class="page-content">
<view v-if="editorState === 'form'" class="form-card">
<text>记录一份家人往来</text>
<text class="form-copy">除亲友姓名外其他字段均可按需填写留空时不提交该字段</text>
<view class="field-row"><text><text class="required-mark">*</text>亲友姓名</text><input v-model="form.relativeName" placeholder="请输入亲友姓名" @input="submitError = ''" /></view>
<view class="field-row"><text>关系称谓</text><input v-model="form.relationName" placeholder="请输入关系称谓" @input="submitError = ''" /></view>
<view class="field-row"><text>礼仪事项</text><input v-model="form.eventName" placeholder="请输入礼仪事项" @input="submitError = ''" /></view>
<view class="field-row field-row--picker"><text>事项日期</text><picker mode="date" :value="form.eventDate" @change="selectEventDate"><view>{{ form.eventDate || '请选择' }}</view></picker></view>
<view class="field-row field-row--picker"><text>事项时间</text><picker mode="time" :value="form.eventClock" @change="selectEventClock"><view>{{ form.eventClock || '请选择' }}</view></picker></view>
<view class="field-row"><text>礼金金额</text><input v-model="form.giftAmount" type="digit" placeholder="请输入礼金金额" @input="submitError = ''" /></view>
<view class="field-row field-row--textarea"><text>往来备注</text><textarea v-model="form.recordContent" auto-height placeholder="记录往来内容" @input="submitError = ''" /></view>
<view class="upload-field">
<view><text>相关图片</text><text>图片选定后会先取得真实上传回执并在提交记录时关联</text></view>
<button class="upload-button" :disabled="isUploading || isSubmitting" @click="uploadImage">{{ isUploading ? '上传中…' : '添加图片' }}</button>
<text v-for="(receipt, index) in mediaReceipts" :key="`${receipt.ossId}-${index}`">已上传{{ receipt.fileName || '图片' }}</text>
<text v-if="uploadError" class="save-error">{{ uploadError }}</text>
</view>
<view class="field-row"><text>排序值</text><input v-model="form.sortOrder" type="number" placeholder="数值越小越靠前" @input="submitError = ''" /></view>
<text v-if="submitError" class="save-error">{{ submitError }}</text>
<AppButton block :disabled="isSubmitting || isUploading" :label="isSubmitting ? '正在提交' : '提交往来记录'" @click="saveRelative" />
</view>
<view v-else class="state-card"><text>{{ resultCopy.title }}</text><text>{{ resultCopy.copy }}</text><AppButton block :label="resultCopy.action" @click="handleResultAction" /></view>
</view>
<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, onUnmounted, reactive, ref } from "vue";
import { onBackPress, onLoad } from "@dcloudio/uni-app";
import AppButton from "@/components/AppButton.vue";
import AppDialog from "@/components/AppDialog.vue";
import ModulePageBackground from "@/components/ModulePageBackground.vue";
import PageHeader from "@/components/PageHeader.vue";
import { appApi, createRequestController, isRequestCancelled } from "@/utils/api.js";
import { createDiscardConfirmation } from "@/utils/discard-confirmation.js";
import { isImagePickCancelled, pickAndUploadImage } from "@/utils/resumable-image-upload.js";
import { goBack, handleBackPress, returnTo, runBackGuard } from "@/utils/navigation.js";
const genealogyId = ref("");
const editorState = ref("form");
const isSubmitting = ref(false);
const isUploading = ref(false);
const discardVisible = ref(false);
const submitError = ref("");
const uploadError = ref("");
const form = reactive({ relativeName: "", relationName: "", eventName: "", eventDate: "", eventClock: "", giftAmount: "", recordContent: "", sortOrder: "" });
const mediaReceipts = ref([]);
const requestController = createRequestController();
const mediaOssIds = computed(() => mediaReceipts.value.map((item) => item.ossId).join(","));
const eventTime = computed(() => form.eventDate ? `${form.eventDate} ${form.eventClock || "00:00"}:00` : "");
const isDirty = computed(() => Object.values(form).some((value) => String(value).trim()) || mediaReceipts.value.length > 0);
const hasValidContext = computed(() => /^[1-9]\d*$/.test(genealogyId.value));
const resultCopy = computed(() => editorState.value === "success"
? { title: "往来记录已提交服务端", copy: "返回贺礼簿后会重新读取服务端记录。", action: "返回贺礼簿" }
: { title: "往来记录入口无效", copy: "没有取得有效家谱标识,页面不会创建无归属记录。", action: "返回上一页" },
);
const discardConfirmation = createDiscardConfirmation((visible) => { discardVisible.value = visible; });
const requestDiscardConfirmation = discardConfirmation.request;
const confirmDiscard = discardConfirmation.confirm;
const cancelDiscard = discardConfirmation.cancel;
onLoad((query) => {
genealogyId.value = String(query?.genealogyId || "");
if (!hasValidContext.value || query?.mode !== "create") editorState.value = "invalid";
});
const selectEventDate = (event) => { form.eventDate = event.detail.value || ""; submitError.value = ""; };
const selectEventClock = (event) => { form.eventClock = event.detail.value || ""; submitError.value = ""; };
const uploadImage = async () => {
if (isUploading.value || isSubmitting.value) return;
isUploading.value = true;
uploadError.value = "";
try {
mediaReceipts.value = [...mediaReceipts.value, await pickAndUploadImage({ requestController })];
} catch (error) {
if (!isImagePickCancelled(error) && !isRequestCancelled(error)) uploadError.value = error?.message || "图片上传失败,请稍后重试。";
} finally {
isUploading.value = false;
}
};
const saveRelative = async () => {
if (isSubmitting.value || isUploading.value || !hasValidContext.value) return;
if (!form.relativeName.trim()) { submitError.value = "请填写亲友姓名"; return; }
isSubmitting.value = true;
submitError.value = "";
try {
const { eventDate, eventClock, ...recordForm } = form;
await appApi.createRelativeRecord(genealogyId.value, { ...recordForm, eventTime: eventTime.value, mediaOssIds: mediaOssIds.value }, { requestController });
Object.assign(form, { relativeName: "", relationName: "", eventName: "", eventDate: "", eventClock: "", giftAmount: "", recordContent: "", sortOrder: "" });
mediaReceipts.value = [];
editorState.value = "success";
} catch (error) {
if (!isRequestCancelled(error)) submitError.value = error?.message || "往来记录提交失败,请稍后重试。";
} finally {
isSubmitting.value = false;
}
};
const requestBack = () => runBackGuard({ transientOpen: discardVisible.value, dirty: isDirty.value, submitting: isSubmitting.value || isUploading.value, "close-transient": cancelDiscard, "block-submitting": () => true, "confirm-discard": requestDiscardConfirmation });
const handleResultAction = () => editorState.value === "success" ? returnTo("R03", { genealogyId: genealogyId.value }) : goBack();
onBackPress((event) => handleBackPress(event, requestBack));
onUnmounted(() => { requestController.abort(); discardConfirmation.dispose(); });
</script>
<style scoped lang="scss">
@use "../../styles/adaptive-frame-profiles.scss" as adaptive;
.gift-editor-page { display: flex; min-height: 100vh; flex-direction: column; background: $paper; }
.page-header, .page-content { z-index: 1; }.page-content { padding: 18rpx 24rpx 72rpx; }
.form-card, .state-card { box-sizing: border-box; padding: 46rpx; @include adaptive.adaptive-records-content; }
.form-card > text:first-child, .state-card > text:first-child { display: block; color: $ink; font-size: 34rpx; font-weight: 700; }.form-copy { display: block; margin-top: 12rpx; color: $ink-muted; font-size: 22rpx; line-height: 1.55; }
.field-row { display: grid; grid-template-columns: 142rpx minmax(0, 1fr); align-items: center; gap: 12rpx 20rpx; min-height: 82rpx; margin-top: 14rpx; padding: 14rpx 24rpx; box-sizing: border-box; @include adaptive.adaptive-records-field; }
.field-row > text, .upload-field > view > text:first-child { color: $ink; font-size: 23rpx; font-weight: 700; }.required-mark { display: inline-block; margin-right: 4rpx; color: $brand-red; font-size: 26rpx; transform: translateY(-3rpx); }
.field-row input, .field-row textarea { min-width: 0; color: $ink; font-size: 23rpx; text-align: right; }.field-row--textarea { align-items: start; }.field-row textarea { min-height: 72rpx; text-align: left; }
.field-row--picker picker { min-width: 0; }.field-row--picker picker > view { min-height: 48rpx; display: flex; align-items: center; justify-content: flex-end; color: $ink; font-size: 23rpx; }
.upload-field { display: grid; gap: 12rpx; margin-top: 14rpx; padding: 18rpx 24rpx; @include adaptive.adaptive-records-field; }.upload-field > view > text { display: block; }.upload-field > view > text:last-child { margin-top: 6rpx; color: $ink-muted; font-size: 20rpx; line-height: 1.45; }
.upload-button { justify-self: start; min-height: 60rpx; margin: 0; padding: 0 20rpx; border: 1rpx solid rgba(159, 23, 15, .42); border-radius: 8rpx; background: transparent; color: $brand-red; font-size: 22rpx; }.upload-field > text { color: $ink-muted; font-size: 21rpx; overflow-wrap: anywhere; }
.save-error { display: block; margin-top: 14rpx; color: $brand-red; font-size: 22rpx; }.form-card .app-button { margin-top: 18rpx; }.state-card { min-height: 340rpx; padding-top: 80rpx; text-align: center; }.state-card > text:nth-child(2) { display: block; margin-top: 18rpx; color: $ink-muted; font-size: 24rpx; line-height: 1.65; }.state-card .app-button { margin-top: 28rpx; }
</style>