Files
jiapuapp/pages/family/f06-article-editor.vue
T
2026-07-20 17:23:13 +08:00

387 lines
10 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用途新建与编辑谱文 -->
<template>
<view
class="article-editor-page"
:class="`article-editor-state--${editorState}`"
>
<ModulePageBackground module="family" />
<view class="article-editor-page__header">
<PageHeader title="编辑谱文" />
</view>
<view v-if="editorState === 'loading'" class="article-editor-loading">
<AppLoading
text="正在打开谱文编辑器"
description="请稍候,草稿正在展开。"
/>
</view>
<view
v-else-if="editorState === 'success'"
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" />
</view>
</view>
</view>
<view v-else class="article-editor-content">
<view class="editor-panel">
<view class="editor-panel__body">
<text class="editor-eyebrow">草稿 · 未发布</text>
<text class="editor-title">把值得传承的故事写下来</text>
<text class="editor-intro"
>补充标题分类和正文保存后可返回谱文列表继续查看</text
>
<view class="editor-field">
<text class="editor-field__label">文章标题</text>
<view
class="editor-control"
:class="{ 'editor-control--error': fieldErrors.title }"
>
<image :src="fieldFrame" mode="scaleToFill" />
<input
v-model="form.title"
maxlength="40"
placeholder="请输入文章标题"
placeholder-class="editor-placeholder"
@input="clearFieldError('title')"
/>
</view>
<text v-if="fieldErrors.title" class="editor-field__error">{{
fieldErrors.title
}}</text>
</view>
<view class="editor-field">
<text class="editor-field__label">文章分类</text>
<view
class="editor-control"
:class="{ 'editor-control--error': fieldErrors.category }"
>
<image :src="fieldFrame" mode="scaleToFill" />
<input
v-model="form.category"
maxlength="20"
placeholder="如:家风家训"
placeholder-class="editor-placeholder"
@input="clearFieldError('category')"
/>
</view>
<text v-if="fieldErrors.category" class="editor-field__error">{{
fieldErrors.category
}}</text>
</view>
<view class="editor-field">
<text class="editor-field__label">正文内容</text>
<view
class="editor-control editor-control--textarea"
:class="{ 'editor-control--error': fieldErrors.content }"
>
<image :src="fieldFrame" mode="scaleToFill" />
<textarea
v-model="form.content"
maxlength="1200"
placeholder="记录家训、往事或想留给后人的话"
placeholder-class="editor-placeholder"
@input="clearFieldError('content')"
/>
</view>
<text v-if="fieldErrors.content" class="editor-field__error">{{
fieldErrors.content
}}</text>
</view>
<text class="editor-draft-note"
>当前为未发布草稿真实草稿保存将在接口阶段接入</text
>
<text v-if="editorState === 'error'" class="editor-save-error"
>保存失败已填写内容仍保留请稍后重试</text
>
<AppButton
block
:label="actionLabel"
:disabled="editorState === 'saving'"
@click="submit"
/>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { computed, reactive, ref } from "vue";
import { onLoad, onUnload } from "@dcloudio/uni-app";
import AppButton from "@/components/AppButton.vue";
import AppLoading from "@/components/AppLoading.vue";
import ModulePageBackground from "@/components/ModulePageBackground.vue";
import PageHeader from "@/components/PageHeader.vue";
const fieldFrame =
"/static/assets/modules/family/transparent/module-field-frame.png";
const draftFixture = {
title: "汤氏家训辑录",
category: "家风家训",
content:
"孝友传家,勤俭立业;敬祖睦宗,诚实待人。愿后人常怀感恩,彼此扶持。",
};
const editorState = ref("form");
const allowedStates = new Set([
"draft",
"loading",
"validation",
"saving",
"error",
"success",
]);
const form = reactive({ title: "", category: "", content: "" });
const fieldErrors = reactive({ title: "", category: "", content: "" });
let saveTimer = null;
const actionLabel = computed(() =>
editorState.value === "saving"
? "正在保存…"
: editorState.value === "error"
? "重新保存"
: "保存谱文",
);
const fillDraft = (failed = false) => {
Object.assign(form, draftFixture, failed ? { title: "保存失败" } : {});
};
const showAllFieldErrors = () => {
fieldErrors.title = "请填写文章标题";
fieldErrors.category = "请填写文章分类";
fieldErrors.content = "请填写正文内容";
};
onLoad((query) => {
const requestedState = allowedStates.has(query.state) ? query.state : "form";
if (["draft", "saving", "error"].includes(requestedState)) {
fillDraft(requestedState === "error");
}
if (requestedState === "validation") showAllFieldErrors();
editorState.value = requestedState;
});
const clearFieldError = (field) => {
fieldErrors[field] = "";
if (
editorState.value === "error" ||
editorState.value === "validation"
) {
editorState.value = "form";
}
};
const validate = () => {
fieldErrors.title = form.title.trim() ? "" : "请填写文章标题";
fieldErrors.category = form.category.trim() ? "" : "请填写文章分类";
fieldErrors.content = form.content.trim() ? "" : "请填写正文内容";
return !fieldErrors.title && !fieldErrors.category && !fieldErrors.content;
};
const submit = () => {
if (editorState.value === "saving") return;
if (!validate()) {
editorState.value = "validation";
return;
}
editorState.value = "saving";
saveTimer = setTimeout(() => {
editorState.value =
form.title.trim() === "保存失败" ? "error" : "success";
saveTimer = null;
}, 320);
};
const returnToList = () => uni.navigateBack();
onUnload(() => {
if (saveTimer) clearTimeout(saveTimer);
});
</script>
<style scoped lang="scss">
.article-editor-page {
position: relative;
min-height: 100vh;
overflow-x: hidden;
background: $paper;
}
.article-editor-page__header,
.article-editor-loading,
.article-editor-content {
position: relative;
z-index: 2;
}
.article-editor-loading {
padding-top: 150rpx;
}
.article-editor-content {
padding: 18rpx 20rpx calc(38rpx + env(safe-area-inset-bottom));
}
.editor-panel,
.editor-result-card {
position: relative;
width: 100%;
border: 20rpx solid transparent;
border-image-source: url("/static/assets/modules/family/transparent/module-content-frame.png");
border-image-slice: 72 fill;
border-image-width: 64rpx;
border-image-repeat: stretch;
box-sizing: border-box;
}
.editor-panel__body {
position: relative;
z-index: 1;
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 {
position: relative;
display: flex;
height: 82rpx;
align-items: center;
}
.editor-control > image {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
pointer-events: none;
}
.editor-control input,
.editor-control textarea {
position: relative;
z-index: 1;
box-sizing: border-box;
width: 100%;
color: $ink;
font-size: 24rpx;
}
.editor-control input {
width: 100%;
height: 82rpx;
padding: 0 28rpx;
}
.editor-control--textarea {
height: 220rpx;
padding: 20rpx 24rpx;
box-sizing: border-box;
}
.editor-control textarea {
width: 100%;
height: 180rpx;
line-height: 1.65;
}
.editor-control--error {
filter: sepia(0.18) saturate(1.2);
}
.editor-placeholder {
color: #9e8e79;
}
.editor-field__error,
.editor-save-error {
display: block;
color: $brand-red;
font-size: 24rpx;
line-height: 34rpx;
}
.editor-field__error {
margin: 5rpx 8rpx 0;
}
.editor-draft-note {
display: block;
margin: 20rpx 10rpx 0;
color: $ink-muted;
font-size: 22rpx;
line-height: 1.55;
text-align: center;
}
.editor-save-error {
margin: 12rpx 8rpx 0;
text-align: center;
}
.editor-panel__body > .app-button {
margin-top: 22rpx;
}
.editor-result-card {
min-height: 420rpx;
}
.editor-result-card__body {
position: relative;
z-index: 1;
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;
}
@media (max-width: 340px) {
.article-editor-content {
padding-right: 14rpx;
padding-left: 14rpx;
}
.editor-panel__body {
padding-right: 26rpx;
padding-left: 26rpx;
}
}
</style>