完成50%

This commit is contained in:
2026-07-23 08:23:59 +08:00
parent 9b0ad62df4
commit f1edc6b533
218 changed files with 24318 additions and 5514 deletions
+135 -36
View File
@@ -6,7 +6,7 @@
>
<ModulePageBackground module="family" />
<view class="article-editor-page__header">
<PageHeader title="编辑谱文" />
<PageHeader title="编辑谱文" custom-back @back="requestBack" />
</view>
<view v-if="editorState === 'loading'" class="article-editor-loading">
@@ -17,17 +17,15 @@
</view>
<view
v-else-if="editorState === 'success'"
v-else-if="editorState === 'preview' || editorState === 'invalid'"
class="article-editor-content"
>
<view class="editor-result-card">
<view class="editor-result-card__body">
<text class="editor-eyebrow">保存结果</text>
<text class="editor-result-card__title">谱文已保存</text>
<text class="editor-result-card__copy"
>这篇谱文已收录可返回谱文列表继续查看</text
>
<AppButton block label="返回谱文列表" @click="returnToList" />
<text class="editor-eyebrow">{{ resultCopy.eyebrow }}</text>
<text class="editor-result-card__title">{{ resultCopy.title }}</text>
<text class="editor-result-card__copy">{{ resultCopy.copy }}</text>
<AppButton block :label="resultCopy.action" @click="handleResultAction" />
</view>
</view>
</view>
@@ -38,7 +36,7 @@
<text class="editor-eyebrow">草稿 · 未发布</text>
<text class="editor-title">把值得传承的故事写下来</text>
<text class="editor-intro"
>补充标题分类和正文保存后可返回谱文列表继续查看</text
>补充标题分类和正文当前只校验并生成本地预览</text
>
<view class="editor-field">
@@ -108,22 +106,45 @@
<AppButton
block
:label="actionLabel"
:disabled="editorState === 'saving'"
:disabled="isSubmitting"
@click="submit"
/>
</view>
</view>
</view>
</view>
<AppDialog
:visible="discardVisible"
title="放弃谱文草稿?"
message="当前内容尚未提交服务器,确认返回后不会保留。"
confirm-text="放弃并返回"
cancel-text="继续编辑"
show-cancel
:close-on-mask="false"
@confirm="confirmDiscard"
@cancel="cancelDiscard"
/>
</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 AppButton from "@/components/AppButton.vue";
import AppDialog from "@/components/AppDialog.vue";
import AppLoading from "@/components/AppLoading.vue";
import ModulePageBackground from "@/components/ModulePageBackground.vue";
import PageHeader from "@/components/PageHeader.vue";
import {
findFamilyArticleFixture,
getGenealogyFixtureAccess,
} from "@/data/mock.js";
import { createDiscardConfirmation } from "@/utils/discard-confirmation.js";
import {
goBack,
handleBackPress,
returnTo,
runBackGuard,
} from "@/utils/navigation.js";
const draftFixture = {
title: "汤氏家训辑录",
@@ -132,28 +153,68 @@ const draftFixture = {
"孝友传家,勤俭立业;敬祖睦宗,诚实待人。愿后人常怀感恩,彼此扶持。",
};
const editorState = ref("form");
const genealogyId = ref("");
const articleId = ref("");
const editorMode = ref("create");
const simulateSaveFailure = ref(false);
const isSubmitting = ref(false);
const discardVisible = ref(false);
const allowedStates = new Set([
"draft",
"loading",
"validation",
"saving",
"error",
"success",
"preview",
]);
const form = reactive({ title: "", category: "", content: "" });
const fieldErrors = reactive({ title: "", category: "", content: "" });
const baseline = ref("");
let saveTimer = null;
const actionLabel = computed(() =>
editorState.value === "saving"
? "正在保存…"
isSubmitting.value
? "正在校验…"
: editorState.value === "error"
? "重新保存"
: "保存谱文",
? "重新校验"
: "生成本地预览",
);
const formSnapshot = computed(() => JSON.stringify(form));
const isDirty = computed(() =>
Boolean(baseline.value) && formSnapshot.value !== baseline.value,
);
const hasValidContext = computed(() =>
Boolean(
genealogyId.value &&
["owner", "member"].includes(
getGenealogyFixtureAccess(genealogyId.value).accessRole,
) &&
(editorMode.value === "create" ||
(editorMode.value === "edit" && articleId.value)),
),
);
const resultCopy = computed(() =>
editorState.value === "preview"
? {
eyebrow: "本地流程预览",
title: "谱文内容已通过本地校验",
copy: "当前尚未提交服务器,返回后不会新增或修改谱文。",
action:
editorMode.value === "edit"
? "返回原谱文(不保存)"
: "返回谱文列表(不保存)",
}
: {
eyebrow: "谱文入口无效",
title: "没有找到要编辑的谱文上下文",
copy: "请从当前家谱的谱文列表重新进入,页面不会创建无归属内容。",
action: "返回上一页",
},
);
const discardConfirmation = createDiscardConfirmation((visible) => {
discardVisible.value = visible;
});
const requestDiscardConfirmation = discardConfirmation.request;
const confirmDiscard = discardConfirmation.confirm;
const cancelDiscard = discardConfirmation.cancel;
const fillDraft = () => {
Object.assign(form, draftFixture);
@@ -165,16 +226,34 @@ const showAllFieldErrors = () => {
};
onLoad((query) => {
articleId.value = query.articleId || "";
editorMode.value = articleId.value || query.mode === "edit" ? "edit" : "create";
genealogyId.value = String(query.genealogyId || "");
articleId.value = String(query.articleId || "");
editorMode.value = String(query.mode || "");
const article =
editorMode.value === "edit"
? findFamilyArticleFixture(genealogyId.value, articleId.value)
: null;
const modeIsValid =
(editorMode.value === "create" && !articleId.value) ||
(editorMode.value === "edit" && Boolean(article));
if (!modeIsValid || !hasValidContext.value) {
editorState.value = "invalid";
return;
}
const requestedState = allowedStates.has(query.state) ? query.state : "form";
simulateSaveFailure.value = requestedState === "error";
if (
editorMode.value === "edit" ||
["draft", "saving", "error"].includes(requestedState)
) {
if (article) {
Object.assign(form, {
title: article.title,
category: article.category,
content: article.paragraphs.join("\n\n"),
});
} else if (["draft", "error", "preview"].includes(requestedState)) {
fillDraft();
}
baseline.value = formSnapshot.value;
if (editorMode.value === "create" && requestedState === "draft") {
baseline.value = JSON.stringify({ title: "", category: "", content: "" });
}
if (requestedState === "validation") showAllFieldErrors();
editorState.value = requestedState;
});
@@ -195,28 +274,48 @@ const validate = () => {
return !fieldErrors.title && !fieldErrors.category && !fieldErrors.content;
};
const submit = () => {
if (editorState.value === "saving") return;
if (isSubmitting.value || !hasValidContext.value) return;
if (!validate()) {
editorState.value = "validation";
return;
}
editorState.value = "saving";
saveTimer = setTimeout(() => {
editorState.value = simulateSaveFailure.value ? "error" : "success";
simulateSaveFailure.value = false;
isSubmitting.value = true;
const submitSnapshot = formSnapshot.value;
const timer = setTimeout(() => {
if (saveTimer !== timer) return;
editorState.value = submitSnapshot ? "preview" : "error";
isSubmitting.value = false;
saveTimer = null;
}, 320);
saveTimer = timer;
};
const returnToList = () =>
uni.redirectTo({
url:
editorMode.value === "edit" && articleId.value
? `/pages/family/f05-article-detail?articleId=${articleId.value}`
: "/pages/family/f04-article-list",
const requestBack = () =>
runBackGuard({
transientOpen: discardVisible.value,
dirty: isDirty.value,
submitting: isSubmitting.value,
"close-transient": cancelDiscard,
"block-submitting": () => true,
"confirm-discard": requestDiscardConfirmation,
});
const returnToTarget = async () => {
const confirmed = isDirty.value ? await requestDiscardConfirmation() : true;
if (!confirmed) return false;
return editorMode.value === "edit"
? returnTo("F05", {
genealogyId: genealogyId.value,
articleId: articleId.value,
})
: returnTo("F04", { genealogyId: genealogyId.value });
};
const handleResultAction = () =>
editorState.value === "preview" ? returnToTarget() : goBack();
onBackPress((event) => handleBackPress(event, requestBack));
onUnload(() => {
if (saveTimer) clearTimeout(saveTimer);
discardConfirmation.dispose();
});
</script>