完成70%
This commit is contained in:
@@ -1,251 +1,13 @@
|
||||
<!-- 页面编号:R-10;用途:当前家谱备忘列表与不写库的本地预览。 -->
|
||||
<!-- 页面编号:R-10;用途:家族备忘创建。列表 DTO 缺失时不展示 fixture。 -->
|
||||
<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>
|
||||
<view class="memo-page"><ModulePageBackground module="records" /><view class="page-header"><PageHeader title="家族备忘" custom-back @back="requestBack" /></view><view class="page-content"><view v-if="state === 'form'" class="form-card"><text>新建家族备忘</text><text class="form-copy">列表和详情没有可消费字段;本页仅提交标题、提醒时间和内容,不猜测完成状态或媒体。</text><view class="field"><text>备忘标题</text><input v-model="form.title" maxlength="40" placeholder="请输入备忘标题" @input="error = ''" /></view><view class="field"><text>提醒时间</text><input v-model="form.time" placeholder="例如:2026-07-24 09:00" @input="error = ''" /></view><view class="field"><text>备忘内容</text><textarea v-model="form.content" auto-height maxlength="1200" placeholder="记录需要提醒的事情" @input="error = ''" /></view><text v-if="error" class="error">{{ error }}</text><AppButton block :disabled="submitting" :label="submitting ? '正在提交' : '提交备忘'" @click="submit" /></view><view v-else class="state-card"><text>{{ result.title }}</text><text>{{ result.copy }}</text><AppButton block :label="result.action" @click="resultAction" /></view></view><AppDialog :visible="discardVisible" title="放弃家族备忘?" message="尚未提交的内容将被清除。" confirm-text="放弃并返回" cancel-text="继续填写" show-cancel :close-on-mask="false" @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 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();
|
||||
});
|
||||
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 { goBack, handleBackPress, returnTo, runBackGuard } from "@/utils/navigation.js";
|
||||
const genealogyId=ref("");const state=ref("form");const submitting=ref(false);const error=ref("");const discardVisible=ref(false);const form=reactive({title:"",time:"",content:""});const controller=createRequestController();const valid=computed(()=>/^[1-9]\d*$/.test(genealogyId.value));const dirty=computed(()=>Object.values(form).some((value)=>value.trim()));const result=computed(()=>state.value==="success"?{title:"备忘已提交服务端",copy:"服务端已返回成功信封;列表仍缺条目 DTO,不生成本地备忘。",action:"返回备忘"}:{title:"备忘入口无效",copy:"没有取得有效家谱标识。",action:"返回上一页"});const confirmation=createDiscardConfirmation((visible)=>{discardVisible.value=visible;});const confirmDiscard=confirmation.confirm;const cancelDiscard=confirmation.cancel;
|
||||
onLoad((query)=>{genealogyId.value=String(query?.genealogyId||"");if(!valid.value)state.value="invalid";});const submit=async()=>{if(submitting.value||!valid.value)return;const memoTitle=form.title.trim();if(!memoTitle){error.value="请填写备忘标题";return;}submitting.value=true;error.value="";try{await appApi.createMemo(genealogyId.value,{memoTitle,remindTime:form.time,memoContent:form.content},{requestController:controller});Object.keys(form).forEach((key)=>{form[key]="";});state.value="success";}catch(cause){if(!isRequestCancelled(cause))error.value=cause?.message||"备忘提交失败,请稍后重试。";}finally{submitting.value=false;}};const requestBack=()=>runBackGuard({transientOpen:discardVisible.value,dirty:dirty.value,submitting:submitting.value,"close-transient":cancelDiscard,"block-submitting":()=>true,"confirm-discard":confirmation.request});const resultAction=()=>state.value==="success"?returnTo("R10",{genealogyId:genealogyId.value}):goBack();onBackPress((event)=>handleBackPress(event,requestBack));onUnmounted(()=>{controller.abort();confirmation.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; }
|
||||
.memo-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{margin-top:16rpx}.field>text{display:block;margin:0 8rpx 8rpx;color:$ink;font-size:23rpx;font-weight:700}.field input,.field textarea{@include adaptive.adaptive-records-field;box-sizing:border-box;width:100%;min-height:76rpx;padding:16rpx 22rpx;color:$ink;font-size:23rpx}.field textarea{min-height:150rpx}.error{display:block;margin-top:12rpx;color:$brand-red;font-size:22rpx}.form-card .app-button{margin-top:20rpx}.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>
|
||||
|
||||
Reference in New Issue
Block a user