Files
jiapuapp/pages/records/r10-memo-list.vue
T
2026-07-23 08:24:07 +08:00

252 lines
9.7 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-10用途当前家谱备忘列表与不写库的本地预览 -->
<template>
<view class="memo-page" :class="stateClasses">
<ModulePageBackground module="records" />
<view class="page-header">
<PageHeader
title="家族备忘"
:action="hasValidContext ? '填写预览' : ''"
custom-back
@back="requestBack"
@action="startMemoPreview"
/>
</view>
<view v-if="memoState === 'loading'" class="page-loading">
<AppLoading text="正在读取家族备忘" description="请稍候,正在核对当前家谱的备忘记录。" />
</view>
<view v-else class="page-content">
<view v-if="localMemoPreview" class="preview-card">
<text>本地预览 · 尚未提交</text>
<text>{{ localMemoPreview.memoTitle }}</text>
<text>{{ localMemoPreview.remindTime || "提醒时间未填写" }}</text>
<text>{{ localMemoPreview.memoContent || "内容未填写" }}</text>
</view>
<template v-if="memoState === 'ready' && memos.length">
<view v-for="memo in memos" :key="memo.memoId" class="memo-card">
<view>
<text>{{ memo.completedLabel }}</text>
<text>{{ memo.remindTime || "未设置提醒" }}</text>
</view>
<text>{{ memo.memoTitle }}</text>
<text>{{ memo.memoContent || "暂无备忘内容" }}</text>
<text>状态仅展示线上切换接口尚未确认</text>
</view>
<AppButton block label="填写备忘预览" @click="startMemoPreview" />
</template>
<view v-else class="state-card">
<text>{{ stateCopy.title }}</text>
<text>{{ stateCopy.copy }}</text>
<AppButton
:type="memoState === 'empty' ? 'primary' : 'secondary'"
block
:label="stateCopy.action"
@click="handleStateAction"
/>
</view>
</view>
<AppDialog
:visible="dialogVisible"
:close-on-mask="false"
eyebrow="家族备忘"
title="填写一份备忘预览"
confirm-text="生成预览"
cancel-text="取消"
show-cancel
@confirm="createMemoPreview"
@cancel="requestCloseEditor"
>
<view class="dialog-form">
<input v-model="memoForm.memoTitle" placeholder="备忘标题" />
<input v-model="memoForm.remindTime" placeholder="提醒时间(选填)" />
<textarea v-model="memoForm.memoContent" auto-height placeholder="补充具体事项(选填)" />
<text v-if="formError">{{ formError }}</text>
</view>
</AppDialog>
<AppDialog
:visible="discardVisible"
:close-on-mask="false"
eyebrow="放弃确认"
title="放弃当前填写?"
message="尚未提交的预览内容将从当前页面清除。"
confirm-text="确认放弃"
cancel-text="继续填写"
show-cancel
@confirm="confirmDiscard"
@cancel="cancelDiscard"
/>
<AppToast :visible="toastVisible" message="已生成本地预览,尚未保存" />
</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 AppLoading from "@/components/AppLoading.vue";
import AppToast from "@/components/AppToast.vue";
import ModulePageBackground from "@/components/ModulePageBackground.vue";
import PageHeader from "@/components/PageHeader.vue";
import {
getGenealogyFixtureAccess,
listMemoFixtures,
} from "@/data/mock.js";
import { createDiscardConfirmation } from "@/utils/discard-confirmation.js";
import { goBack, handleBackPress, runBackGuard } from "@/utils/navigation.js";
const genealogyId = ref("");
const memos = ref([]);
const memoState = ref("loading");
const dialogVisible = ref(false);
const discardVisible = ref(false);
const toastVisible = ref(false);
const formError = ref("");
const localMemoPreview = ref(null);
const memoForm = reactive({ memoTitle: "", remindTime: "", memoContent: "" });
const editorBaseline = ref("");
let timer = null;
const stateClasses = computed(() => ({
"memo-state--loading": memoState.value === "loading",
"memo-state--empty": memoState.value === "empty",
"memo-state--error": memoState.value === "error",
"memo-state--invalid": memoState.value === "invalid",
}));
const hasValidContext = computed(() => ["ready", "empty"].includes(memoState.value));
const formSnapshot = computed(() => JSON.stringify({ ...memoForm }));
const memoDraftDirty = computed(() =>
dialogVisible.value && formSnapshot.value !== editorBaseline.value,
);
const stateCopy = computed(() => ({
error: {
title: "家族备忘暂不可用",
copy: "请稍后重新查看,已有备忘不会受到影响。",
action: "重新查看",
},
invalid: {
title: "家族备忘入口无效",
copy: "没有找到可访问的成员家谱,页面不会展示其他家谱备忘。",
action: "返回上一页",
},
empty: {
title: "还没有备忘",
copy: "可以先生成一份本地预览;正式创建仍需等待线上写接口启用。",
action: "填写备忘预览",
},
})[memoState.value] || {
title: "家族备忘暂不可用",
copy: "请返回上一页重新进入。",
action: "返回上一页",
});
onLoad((query) => {
genealogyId.value = String(query.genealogyId || "");
const access = getGenealogyFixtureAccess(genealogyId.value);
if (!["owner", "member"].includes(access.accessRole)) {
memoState.value = "invalid";
return;
}
memos.value = listMemoFixtures(genealogyId.value);
memoState.value = ["loading", "empty", "error"].includes(query.state)
? query.state
: memos.value.length
? "ready"
: "empty";
});
const startMemoPreview = () => {
if (!hasValidContext.value) return false;
Object.assign(memoForm, { memoTitle: "", remindTime: "", memoContent: "" });
formError.value = "";
editorBaseline.value = formSnapshot.value;
dialogVisible.value = true;
return true;
};
const createMemoPreview = () => {
formError.value = memoForm.memoTitle.trim() ? "" : "请填写备忘标题";
if (formError.value) return false;
// 正式列表来自只读选择器;预览不生成 ID,也不会改变完成状态或记录数量。
localMemoPreview.value = Object.freeze({
memoTitle: memoForm.memoTitle.trim(),
remindTime: memoForm.remindTime.trim(),
memoContent: memoForm.memoContent.trim(),
});
dialogVisible.value = false;
toastVisible.value = true;
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
toastVisible.value = false;
timer = null;
}, 1800);
return true;
};
const closeEditor = () => {
dialogVisible.value = false;
formError.value = "";
};
const discardConfirmation = createDiscardConfirmation(
(visible) => { discardVisible.value = visible; },
);
const confirmDiscard = () => {
discardConfirmation.confirm();
closeEditor();
};
const cancelDiscard = discardConfirmation.cancel;
const requestCloseEditor = async () => {
if (!memoDraftDirty.value) {
closeEditor();
return true;
}
const confirmed = await discardConfirmation.request();
if (confirmed) closeEditor();
return confirmed;
};
const restoreMemos = () => {
memos.value = listMemoFixtures(genealogyId.value);
memoState.value = memos.value.length ? "ready" : "empty";
};
const handleStateAction = () => {
if (memoState.value === "invalid") return goBack();
if (memoState.value === "error") return restoreMemos();
return startMemoPreview();
};
const requestBack = () => {
if (discardVisible.value) {
cancelDiscard();
return Promise.resolve(true);
}
if (dialogVisible.value) return requestCloseEditor();
return runBackGuard({
dirty: Boolean(localMemoPreview.value),
"confirm-discard": discardConfirmation.request,
});
};
onBackPress((event) => handleBackPress(event, requestBack));
onUnmounted(() => {
if (timer) clearTimeout(timer);
discardConfirmation.dispose();
});
</script>
<style scoped lang="scss">
@use "../../styles/adaptive-frame-profiles.scss" as adaptive;
.memo-page { display: flex; min-height: 100vh; flex-direction: column; background: $paper; }
.page-header,.page-loading,.page-content { z-index: 1; }
.page-loading { min-height: calc(100vh - 100rpx); }
.page-content { display: flex; flex-direction: column; gap: 16rpx; padding: 18rpx 24rpx 72rpx; }
.memo-card,.preview-card,.state-card { box-sizing: border-box; padding: 34rpx 46rpx; @include adaptive.adaptive-records-content; }
.memo-card > view { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 6rpx 18rpx; color: $brand-red; font-size: 20rpx; }
.memo-card > text,.preview-card > text,.state-card > text { display: block; }
.memo-card > text:nth-child(2),.preview-card > text:nth-child(2) { margin-top: 8rpx; color: $ink; font-size: 31rpx; font-weight: 700; }
.memo-card > text:nth-child(3),.preview-card > text:nth-child(n + 3) { margin-top: 9rpx; color: $ink-muted; font-size: 23rpx; line-height: 1.55; }
.memo-card > text:last-child,.preview-card > text:first-child { margin-top: 10rpx; color: $brand-red; font-size: 20rpx; }
.state-card { min-height: 340rpx; padding-top: 78rpx; text-align: center; }
.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; }
.dialog-form { width: 100%; margin: 18rpx 0; }
.dialog-form input,.dialog-form textarea { width: 100%; min-height: 68rpx; margin-top: 9rpx; padding: 13rpx 20rpx; box-sizing: border-box; color: $ink; font-size: 23rpx; @include adaptive.adaptive-records-field; }
.dialog-form text { display: block; margin-top: 8rpx; color: $brand-red; font-size: 20rpx; }
</style>