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

151 lines
11 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-07用途创建礼仪活动 -->
<template>
<view class="ritual-editor-page" :class="`ritual-state--${pageState}`">
<ModulePageBackground module="records" />
<view class="page-header"><PageHeader title="新建礼仪活动" custom-back @back="requestBack" /></view>
<view class="page-content">
<view v-if="pageState === 'form'" class="editor-card">
<text class="editor-card__title">记录一场家族礼仪</text>
<text class="editor-card__note">带红色星号的内容不能为空其余内容可按需要补充</text>
<view class="field-row">
<text class="field-row__label"><text class="required-mark">*</text>活动类型</text>
<input v-model="form.ceremonyType" maxlength="30" placeholder="例如:祭祖、家宴" placeholder-class="placeholder" @input="clearError" />
</view>
<view class="field-row">
<text class="field-row__label"><text class="required-mark">*</text>活动标题</text>
<input v-model="form.ceremonyTitle" maxlength="60" placeholder="请输入活动标题" placeholder-class="placeholder" @input="clearError" />
</view>
<view class="textarea-field">
<text class="textarea-field__label">活动说明</text>
<textarea v-model="form.ceremonyDesc" maxlength="500" auto-height placeholder="补充活动安排或说明" placeholder-class="placeholder" @input="clearError" />
</view>
<view class="field-row field-row--picker">
<text class="field-row__label">活动日期</text>
<picker mode="date" :value="form.ceremonyDate" @change="selectCeremonyDate"><view :class="{ placeholder: !form.ceremonyDate }">{{ form.ceremonyDate || '请选择' }}</view></picker>
</view>
<view class="field-row field-row--picker">
<text class="field-row__label">活动时间</text>
<picker mode="time" :value="form.ceremonyClock" @change="selectCeremonyClock"><view :class="{ placeholder: !form.ceremonyClock }">{{ form.ceremonyClock || '请选择' }}</view></picker>
</view>
<view class="field-row">
<text class="field-row__label">地点</text>
<input v-model="form.location" maxlength="60" placeholder="请输入地点" placeholder-class="placeholder" @input="clearError" />
</view>
<view class="cover-field">
<view>
<text class="cover-field__label">封面图片</text>
<text class="cover-field__hint">选择图片后会取得真实上传回执并在保存时关联</text>
</view>
<button class="upload-button" :disabled="uploading || submitting" @click="uploadCover">{{ uploading ? '上传中…' : '选择图片' }}</button>
<text v-if="coverFileName" class="upload-receipt">已上传{{ coverFileName }}</text>
</view>
<text v-if="uploadError" class="form-error">{{ uploadError }}</text>
<view class="field-row">
<text class="field-row__label">排序值</text>
<input v-model="form.sortOrder" type="number" placeholder="数值越小越靠前" placeholder-class="placeholder" @input="clearError" />
</view>
<text v-if="submitError" class="form-error">{{ submitError }}</text>
<AppButton block :disabled="uploading || submitting" :label="submitting ? '正在保存…' : '保存活动'" @click="submit" />
</view>
<view v-else class="state-card">
<text>{{ stateCopy.title }}</text>
<text>{{ stateCopy.copy }}</text>
<AppButton block :label="stateCopy.action" @click="handleStateAction" />
</view>
</view>
</view>
<AppDialog :visible="discardVisible" title="放弃活动草稿?" message="当前内容尚未保存到服务端,返回后不会保留。" confirm-text="放弃并返回" cancel-text="继续填写" show-cancel :close-on-mask="false" @confirm="confirmation.confirm" @cancel="confirmation.cancel" />
</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 { isImagePickCancelled, pickAndUploadImage, toConsumerOssId } from "@/utils/resumable-image-upload.js";
import { createDiscardConfirmation } from "@/utils/discard-confirmation.js";
import { goBack, handleBackPress, returnTo, runBackGuard } from "@/utils/navigation.js";
const genealogyId = ref("");
const pageState = ref("form");
const form = reactive({ ceremonyType: "", ceremonyTitle: "", ceremonyDesc: "", ceremonyDate: "", ceremonyClock: "", location: "", sortOrder: "" });
const coverOssId = ref(null);
const coverFileName = ref("");
const uploadError = ref("");
const submitError = ref("");
const uploading = ref(false);
const submitting = ref(false);
const controller = createRequestController();
const discardVisible = ref(false);
const confirmation = createDiscardConfirmation((visible) => { discardVisible.value = visible; });
const hasValidContext = computed(() => /^[1-9]\d*$/.test(genealogyId.value));
const isDirty = computed(() => Boolean(coverOssId.value) || Object.values(form).some((value) => value.trim()));
const ceremonyTime = computed(() => form.ceremonyDate ? `${form.ceremonyDate} ${form.ceremonyClock || "00:00"}:00` : "");
const stateCopy = computed(() => pageState.value === "success"
? { title: "礼仪活动已提交服务端", copy: "服务端已返回成功结果。", action: "返回礼仪活动" }
: { title: "礼仪活动入口无效", copy: "没有取得有效家谱标识。", action: "返回上一页" });
onLoad((query) => {
genealogyId.value = String(query?.genealogyId || "");
if (!hasValidContext.value || query?.mode !== "create") pageState.value = "invalid";
});
const clearError = () => { submitError.value = ""; };
const selectCeremonyDate = (event) => { form.ceremonyDate = event.detail.value || ""; clearError(); };
const selectCeremonyClock = (event) => { form.ceremonyClock = event.detail.value || ""; clearError(); };
const uploadCover = async () => {
if (uploading.value || submitting.value) return;
uploading.value = true;
uploadError.value = "";
try {
const receipt = await pickAndUploadImage({ requestController: controller });
coverOssId.value = toConsumerOssId(receipt.ossId);
coverFileName.value = receipt.fileName || "活动封面";
} catch (error) {
if (!isImagePickCancelled(error) && !isRequestCancelled(error)) uploadError.value = error?.message || "封面图片上传失败,请稍后重试";
} finally {
uploading.value = false;
}
};
const submit = async () => {
if (uploading.value || submitting.value || !hasValidContext.value) return;
if (!form.ceremonyType.trim() || !form.ceremonyTitle.trim()) {
submitError.value = !form.ceremonyType.trim() ? "请填写活动类型" : "请填写活动标题";
return;
}
submitting.value = true;
submitError.value = "";
try {
const { ceremonyDate, ceremonyClock, ...ceremonyForm } = form;
await appApi.createCeremony(genealogyId.value, { ...ceremonyForm, ceremonyTime: ceremonyTime.value, ...(coverOssId.value ? { coverOssId: coverOssId.value } : {}) }, { requestController: controller });
pageState.value = "success";
} catch (error) {
if (!isRequestCancelled(error)) submitError.value = error?.message || "礼仪活动保存失败,请稍后重试";
} finally {
submitting.value = false;
}
};
const returnToList = () => hasValidContext.value ? returnTo("R05", { genealogyId: genealogyId.value }) : goBack();
const requestBack = () => runBackGuard({ transientOpen: discardVisible.value, dirty: isDirty.value, submitting: uploading.value || submitting.value, "close-transient": confirmation.cancel, "block-submitting": () => true, "confirm-discard": confirmation.request });
const handleStateAction = () => pageState.value === "success" ? returnToList() : goBack();
onBackPress((event) => handleBackPress(event, requestBack));
onUnmounted(() => { controller.abort(); confirmation.dispose(); });
</script>
<style scoped lang="scss">
@use "../../styles/adaptive-frame-profiles.scss" as adaptive;
.ritual-editor-page { display:flex; min-height:100vh; flex-direction:column; background:$paper; }
.page-header,.page-content { z-index:1; }.page-content { flex:1; padding:22rpx 28rpx 72rpx; }
.editor-card,.state-card { @include adaptive.adaptive-records-content; box-sizing:border-box; }
.editor-card { padding:38rpx 32rpx 42rpx; }.editor-card__title,.editor-card__note,.field-row__label,.textarea-field__label,.form-error { display:block; }
.editor-card__title { color:$ink; font-family:STKaiti,KaiTi,serif; font-size:38rpx; font-weight:700; }.editor-card__note { margin-top:10rpx; color:$ink-muted; font-size:23rpx; line-height:1.55; }
.field-row { display:flex; min-height:92rpx; align-items:center; margin-top:20rpx; padding:0 18rpx; border:1rpx solid rgba(128,89,49,.34); border-radius:12rpx; background:rgba(255,252,245,.7); }.field-row__label { width:150rpx; flex:0 0 auto; color:$ink; font-size:26rpx; font-weight:700; }.field-row input { min-width:0; flex:1; color:$ink; font-size:25rpx; }.field-row--picker picker { min-width:0; flex:1; }.field-row--picker picker > view { min-height:92rpx; display:flex; align-items:center; color:$ink; font-size:25rpx; }.required-mark { display:inline-block; margin-right:4rpx; color:$brand-red; font-size:26rpx; transform:translateY(-3rpx); }.placeholder { color:#9e8e79; }
.textarea-field,.cover-field { margin-top:20rpx; padding:18rpx; border:1rpx solid rgba(128,89,49,.34); border-radius:12rpx; background:rgba(255,252,245,.7); }.textarea-field__label,.cover-field__label { color:$ink; font-size:26rpx; font-weight:700; }.textarea-field textarea { display:block; width:100%; min-height:122rpx; margin-top:12rpx; color:$ink; font-size:25rpx; line-height:1.55; }
.cover-field { display:grid; gap:12rpx; }.cover-field__hint { display:block; margin-top:5rpx; color:$ink-muted; font-size:20rpx; line-height:1.45; }.upload-button { justify-self:start; min-height:88rpx; 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-receipt { color:$ink-muted; font-size:21rpx; overflow-wrap:anywhere; }.form-error { margin-top:10rpx; color:$brand-red; font-size:22rpx; line-height:1.45; }.editor-card .app-button { margin-top:28rpx; }
.state-card { min-height:340rpx; padding:78rpx 52rpx 50rpx; text-align:center; }.state-card text { display:block; }.state-card text:first-child { color:$ink; 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; }
</style>