387 lines
10 KiB
Vue
387 lines
10 KiB
Vue
<!-- 页面编号: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>
|