Files
jiapuapp/pages/family/f06-article-editor.vue
T
2026-07-24 07:56:33 +08:00

157 lines
8.0 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.
<!-- 页面编号F-06用途按已声明 articleTitle/articleContent 创建谱文 -->
<template>
<view class="article-editor-page" :class="`article-editor-state--${editorState}`">
<ModulePageBackground module="family" />
<view class="article-editor-page__header"><PageHeader title="新建谱文" custom-back @back="requestBack" /></view>
<view class="article-editor-content">
<view v-if="editorState === 'form'" class="editor-panel">
<view class="editor-panel__body">
<text class="editor-eyebrow">服务端创建</text>
<text class="editor-title">把值得传承的故事写下来</text>
<text class="editor-intro">本页只发送 Apifox 已声明且可映射的文章标题与正文分类需要 `categoryId`当前没有可用分类 owner故不展示或猜测分类</text>
<view class="editor-field">
<text class="editor-field__label">文章标题</text>
<view class="editor-control"><input v-model="form.title" maxlength="40" placeholder="请输入文章标题" placeholder-class="editor-placeholder" @input="submitError = ''" /></view>
</view>
<view class="editor-field">
<text class="editor-field__label">正文内容</text>
<view class="editor-control editor-control--textarea"><textarea v-model="form.content" auto-height maxlength="1200" placeholder="记录家训、往事或想留给后人的话" placeholder-class="editor-placeholder" @input="submitError = ''" /></view>
</view>
<text v-if="submitError" class="editor-save-error">{{ submitError }}</text>
<AppButton block :label="isSubmitting ? '正在提交' : '提交谱文'" :disabled="isSubmitting" @click="submit" />
</view>
</view>
<view v-else class="editor-result-card">
<view class="editor-result-card__body">
<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>
</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, 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 editorState = ref("form");
const genealogyId = ref("");
const isSubmitting = ref(false);
const discardVisible = ref(false);
const submitError = ref("");
const form = reactive({ title: "", content: "" });
const requestController = createRequestController();
const isDirty = computed(() => Boolean(form.title.trim() || form.content.trim()));
const hasValidContext = computed(() => /^[1-9]\d*$/.test(genealogyId.value));
const resultCopy = computed(() => editorState.value === "success"
? {
eyebrow: "服务端已接受",
title: "谱文已提交",
copy: "服务端已返回成功信封。谱文列表与详情仍缺展示 DTO,返回后不会生成本地文章卡片。",
action: "返回谱文列表",
}
: {
eyebrow: "谱文入口无效",
title: "无法创建谱文",
copy: "当前只有新建操作可映射;编辑需要可靠的文章详情和文章 ID,暂不从 fixture 进入。",
action: "返回上一页",
},
);
const discardConfirmation = createDiscardConfirmation((visible) => {
discardVisible.value = visible;
});
const requestDiscardConfirmation = discardConfirmation.request;
const confirmDiscard = discardConfirmation.confirm;
const cancelDiscard = discardConfirmation.cancel;
onLoad((query) => {
genealogyId.value = String(query?.genealogyId || "");
if (!hasValidContext.value || query?.mode !== "create") editorState.value = "invalid";
});
const submit = async () => {
if (isSubmitting.value || !hasValidContext.value) return;
const articleTitle = form.title.trim();
const articleContent = form.content.trim();
if (!articleTitle || !articleContent) {
submitError.value = !articleTitle ? "请填写文章标题" : "请填写正文内容";
return;
}
isSubmitting.value = true;
submitError.value = "";
try {
await appApi.createArticle(genealogyId.value, { articleTitle, articleContent }, { requestController });
form.title = "";
form.content = "";
editorState.value = "success";
} catch (error) {
if (!isRequestCancelled(error)) submitError.value = error?.message || "谱文提交失败,请稍后重试。";
} finally {
isSubmitting.value = false;
}
};
const requestBack = () => runBackGuard({
transientOpen: discardVisible.value,
dirty: isDirty.value,
submitting: isSubmitting.value,
"close-transient": cancelDiscard,
"block-submitting": () => true,
"confirm-discard": requestDiscardConfirmation,
});
const handleResultAction = () => editorState.value === "success"
? returnTo("F04", { genealogyId: genealogyId.value })
: goBack();
onBackPress((event) => handleBackPress(event, requestBack));
onUnmounted(() => {
requestController.abort();
discardConfirmation.dispose();
});
</script>
<style scoped lang="scss">
@use "../../styles/adaptive-frame-profiles.scss" as adaptive;
.article-editor-page { display: flex; min-height: 100vh; flex-direction: column; background: $paper; }
.article-editor-page__header, .article-editor-content { z-index: 1; }
.article-editor-content { padding: 18rpx 20rpx calc(38rpx + env(safe-area-inset-bottom)); }
.editor-panel, .editor-result-card { @include adaptive.adaptive-family-panel; width: 100%; }
.editor-panel__body { padding: 38rpx 34rpx 42rpx; }
.editor-eyebrow { display: block; color: $brand-red; font-size: 23rpx; letter-spacing: 3rpx; text-align: center; }
.editor-title { display: block; margin-top: 10rpx; color: $ink; font-family: STKaiti, KaiTi, serif; font-size: 34rpx; font-weight: 700; text-align: center; }
.editor-intro { display: block; margin: 10rpx 8rpx 22rpx; color: $ink-muted; font-size: 23rpx; line-height: 1.55; text-align: center; }
.editor-field { margin-top: 18rpx; }
.editor-field__label { display: block; margin: 0 8rpx 8rpx; color: $ink; font-size: 25rpx; font-weight: 700; }
.editor-control { @include adaptive.adaptive-family-field; display: flex; min-height: 82rpx; align-items: center; }
.editor-control input, .editor-control textarea { box-sizing: border-box; width: 100%; color: $ink; font-size: 24rpx; }
.editor-control input { min-height: 82rpx; padding: 0 28rpx; }
.editor-control--textarea { min-height: 220rpx; padding: 20rpx 24rpx; }
.editor-control textarea { min-height: 180rpx; line-height: 1.65; }
.editor-placeholder { color: #9e8e79; }
.editor-save-error { display: block; margin: 12rpx 8rpx 0; color: $brand-red; font-size: 24rpx; line-height: 34rpx; text-align: center; }
.editor-panel__body > .app-button { margin-top: 22rpx; }
.editor-result-card { min-height: 420rpx; }
.editor-result-card__body { padding: 112rpx 52rpx 68rpx; text-align: center; }
.editor-result-card__title { display: block; margin-top: 14rpx; color: $ink; font-family: STKaiti, KaiTi, serif; font-size: 36rpx; font-weight: 700; }
.editor-result-card__copy { display: block; margin-top: 16rpx; color: $ink-muted; font-size: 24rpx; line-height: 1.65; }
.editor-result-card__body > .app-button { margin-top: 30rpx; }
</style>