完成50%
This commit is contained in:
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user