完成70%
This commit is contained in:
@@ -1,43 +1,23 @@
|
||||
<!-- 页面编号:F-02;用途:发布家族动态与提交结果。 -->
|
||||
<!-- 页面编号:F-02;用途:按 Apifox 已声明的 feedContent 发布家族动态。 -->
|
||||
<template>
|
||||
<view
|
||||
class="publish-page"
|
||||
:class="{
|
||||
'publish-state--form': publishState === 'form',
|
||||
'publish-state--preview': publishState === 'preview',
|
||||
'publish-state--error': publishState === 'error',
|
||||
'publish-state--invalid': publishState === 'invalid',
|
||||
}"
|
||||
><ModulePageBackground module="family" /><view class="publish-page__header"
|
||||
><PageHeader title="发布动态" custom-back @back="requestBack" /></view
|
||||
><view class="publish-panel"
|
||||
><view
|
||||
v-if="publishState === 'form'"
|
||||
class="publish-form"
|
||||
><text>记录此刻</text
|
||||
><text>分享通知、活动、家族故事或一段共同记忆。</text
|
||||
><view class="publish-field"
|
||||
><textarea
|
||||
v-model="content"
|
||||
auto-height
|
||||
maxlength="300"
|
||||
placeholder="写下想对家人说的话"
|
||||
placeholder-class="publish-placeholder"
|
||||
/></view
|
||||
><AppButton
|
||||
block
|
||||
:label="isSubmitting ? '正在校验' : '生成本地预览'"
|
||||
:disabled="isSubmitting"
|
||||
@click="submit"
|
||||
/></view
|
||||
><view v-else class="publish-result"
|
||||
><text>{{ resultCopy.title }}</text
|
||||
><text>{{ resultCopy.copy }}</text
|
||||
><AppButton
|
||||
block
|
||||
:label="resultCopy.action"
|
||||
@click="handleResultAction" /></view></view
|
||||
><AppDialog
|
||||
<view class="publish-page" :class="`publish-state--${publishState}`">
|
||||
<ModulePageBackground module="family" />
|
||||
<view class="publish-page__header"><PageHeader title="发布动态" custom-back @back="requestBack" /></view>
|
||||
<view class="publish-panel">
|
||||
<view v-if="publishState === 'form'" class="publish-form">
|
||||
<text>记录此刻</text>
|
||||
<text>当前接口只明确文本动态的 `feedContent` 请求字段;媒体、排序和状态均不由本页猜测或提交。</text>
|
||||
<view class="publish-field"><textarea v-model="content" auto-height maxlength="300" placeholder="写下想对家人说的话" placeholder-class="publish-placeholder" @input="submitError = ''" /></view>
|
||||
<text v-if="submitError" class="publish-error">{{ submitError }}</text>
|
||||
<AppButton block :label="isSubmitting ? '正在提交' : '提交动态'" :disabled="isSubmitting" @click="submit" />
|
||||
</view>
|
||||
<view v-else class="publish-result">
|
||||
<text>{{ resultCopy.title }}</text>
|
||||
<text>{{ resultCopy.copy }}</text>
|
||||
<AppButton block :label="resultCopy.action" @click="handleResultAction" />
|
||||
</view>
|
||||
</view>
|
||||
<AppDialog
|
||||
:visible="discardVisible"
|
||||
title="放弃动态草稿?"
|
||||
message="当前内容尚未提交服务器,确认返回后不会保留。"
|
||||
@@ -48,46 +28,34 @@
|
||||
@confirm="confirmDiscard"
|
||||
@cancel="cancelDiscard"
|
||||
/>
|
||||
><AppToast :visible="toastVisible" :message="toastMessage"
|
||||
/></view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, onUnmounted, ref } from "vue";
|
||||
import { onBackPress, onLoad } from "@dcloudio/uni-app";
|
||||
import AppButton from "@/components/AppButton.vue";
|
||||
import AppDialog from "@/components/AppDialog.vue";
|
||||
import AppToast from "@/components/AppToast.vue";
|
||||
import ModulePageBackground from "@/components/ModulePageBackground.vue";
|
||||
import PageHeader from "@/components/PageHeader.vue";
|
||||
import { getGenealogyFixtureAccess } from "@/data/mock.js";
|
||||
import { appApi, createRequestController, isRequestCancelled } from "@/utils/api.js";
|
||||
import { createDiscardConfirmation } from "@/utils/discard-confirmation.js";
|
||||
import {
|
||||
goBack,
|
||||
handleBackPress,
|
||||
returnTo,
|
||||
runBackGuard,
|
||||
} from "@/utils/navigation.js";
|
||||
import { goBack, handleBackPress, returnTo, runBackGuard } from "@/utils/navigation.js";
|
||||
|
||||
const genealogyId = ref("");
|
||||
const content = ref("");
|
||||
const publishState = ref("form");
|
||||
const isSubmitting = ref(false);
|
||||
const submitError = ref("");
|
||||
const discardVisible = ref(false);
|
||||
const toastVisible = ref(false);
|
||||
const toastMessage = ref("");
|
||||
let toastTimer = null;
|
||||
let submitTimer = null;
|
||||
const requestController = createRequestController();
|
||||
const isDirty = computed(() => Boolean(content.value.trim()));
|
||||
const hasValidContext = computed(() =>
|
||||
["owner", "member"].includes(
|
||||
getGenealogyFixtureAccess(genealogyId.value).accessRole,
|
||||
),
|
||||
);
|
||||
const hasValidContext = computed(() => /^[1-9]\d*$/.test(genealogyId.value));
|
||||
const resultCopy = computed(() => ({
|
||||
preview: {
|
||||
title: "动态内容已完成本地预览",
|
||||
copy: "当前尚未提交服务器,返回家族圈后不会出现这条动态。",
|
||||
action: "返回家族圈(不发布)",
|
||||
success: {
|
||||
title: "动态已提交服务端",
|
||||
copy: "服务端已返回成功信封。动态列表仍缺条目 DTO,返回后不会把本地内容伪装成列表项。",
|
||||
action: "返回家族动态",
|
||||
},
|
||||
error: {
|
||||
title: "动态未提交",
|
||||
@@ -96,7 +64,7 @@ const resultCopy = computed(() => ({
|
||||
},
|
||||
invalid: {
|
||||
title: "动态入口无效",
|
||||
copy: "没有找到可发布内容的成员家谱,页面不会创建无归属动态。",
|
||||
copy: "没有取得有效家谱标识,页面不会创建无归属动态。",
|
||||
action: "返回上一页",
|
||||
},
|
||||
}[publishState.value]));
|
||||
@@ -108,57 +76,45 @@ const confirmDiscard = discardConfirmation.confirm;
|
||||
const cancelDiscard = discardConfirmation.cancel;
|
||||
|
||||
onLoad((query) => {
|
||||
genealogyId.value = String(query.genealogyId || "");
|
||||
if (!genealogyId.value || !hasValidContext.value) {
|
||||
publishState.value = "invalid";
|
||||
return;
|
||||
}
|
||||
if (["preview", "error"].includes(query.state)) {
|
||||
content.value = "这是一段尚未提交服务器的家族动态预览。";
|
||||
publishState.value = query.state;
|
||||
}
|
||||
genealogyId.value = String(query?.genealogyId || "");
|
||||
if (!hasValidContext.value) publishState.value = "invalid";
|
||||
});
|
||||
const showToast = (message) => {
|
||||
toastMessage.value = message;
|
||||
toastVisible.value = true;
|
||||
if (toastTimer) clearTimeout(toastTimer);
|
||||
toastTimer = setTimeout(() => {
|
||||
toastVisible.value = false;
|
||||
toastTimer = null;
|
||||
}, 1800);
|
||||
};
|
||||
const submit = () => {
|
||||
const submit = async () => {
|
||||
if (isSubmitting.value || !hasValidContext.value) return;
|
||||
if (!content.value.trim()) {
|
||||
showToast("请先写下动态内容");
|
||||
const feedContent = content.value.trim();
|
||||
if (!feedContent) {
|
||||
submitError.value = "请先写下动态内容";
|
||||
return;
|
||||
}
|
||||
isSubmitting.value = true;
|
||||
const submittedContent = content.value.trim();
|
||||
const timer = setTimeout(() => {
|
||||
if (submitTimer !== timer) return;
|
||||
submitTimer = null;
|
||||
submitError.value = "";
|
||||
try {
|
||||
await appApi.createFeed(genealogyId.value, { feedContent }, { requestController });
|
||||
content.value = "";
|
||||
publishState.value = "success";
|
||||
} catch (error) {
|
||||
if (isRequestCancelled(error)) return;
|
||||
publishState.value = "error";
|
||||
submitError.value = error?.message || "动态提交失败,请稍后重试。";
|
||||
} finally {
|
||||
isSubmitting.value = false;
|
||||
publishState.value = submittedContent ? "preview" : "error";
|
||||
}, 280);
|
||||
submitTimer = timer;
|
||||
}
|
||||
};
|
||||
const requestBack = () =>
|
||||
runBackGuard({
|
||||
transientOpen: discardVisible.value,
|
||||
dirty: isDirty.value,
|
||||
submitting: isSubmitting.value,
|
||||
"close-transient": cancelDiscard,
|
||||
"block-submitting": () => true,
|
||||
"confirm-discard": requestDiscardConfirmation,
|
||||
});
|
||||
const requestBack = () => runBackGuard({
|
||||
transientOpen: discardVisible.value,
|
||||
dirty: isDirty.value,
|
||||
submitting: isSubmitting.value,
|
||||
"close-transient": cancelDiscard,
|
||||
"block-submitting": () => true,
|
||||
"confirm-discard": requestDiscardConfirmation,
|
||||
});
|
||||
const returnToFamily = async () => {
|
||||
const confirmed = isDirty.value ? await requestDiscardConfirmation() : true;
|
||||
if (!confirmed) return false;
|
||||
return returnTo("F01", { genealogyId: genealogyId.value });
|
||||
};
|
||||
const handleResultAction = () => {
|
||||
if (publishState.value === "preview") return returnToFamily();
|
||||
if (publishState.value === "success") return returnToFamily();
|
||||
if (publishState.value === "error") {
|
||||
publishState.value = "form";
|
||||
return;
|
||||
@@ -167,75 +123,24 @@ const handleResultAction = () => {
|
||||
};
|
||||
onBackPress((event) => handleBackPress(event, requestBack));
|
||||
onUnmounted(() => {
|
||||
if (toastTimer) clearTimeout(toastTimer);
|
||||
if (submitTimer) clearTimeout(submitTimer);
|
||||
requestController.abort();
|
||||
discardConfirmation.dispose();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@use "../../styles/adaptive-frame-profiles.scss" as adaptive;
|
||||
|
||||
.publish-page {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
flex-direction: column;
|
||||
background: $paper;
|
||||
}
|
||||
.publish-page__header,
|
||||
.publish-panel {
|
||||
@include adaptive.adaptive-family-content;
|
||||
z-index: 1;
|
||||
}
|
||||
.publish-panel {
|
||||
width: calc(100% - 32rpx);
|
||||
margin: 18rpx auto 0;
|
||||
padding: 9%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.publish-form > text,
|
||||
.publish-result > text {
|
||||
display: block;
|
||||
}
|
||||
.publish-form > text:first-child,
|
||||
.publish-result > text:first-child {
|
||||
color: $ink;
|
||||
font-family: STKaiti, KaiTi, serif;
|
||||
font-size: 36rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
.publish-form > text:nth-child(2),
|
||||
.publish-result > text:nth-child(2) {
|
||||
margin-top: 12rpx;
|
||||
color: $ink-muted;
|
||||
font-size: 23rpx;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.publish-field {
|
||||
@include adaptive.adaptive-family-field;
|
||||
display: flex;
|
||||
min-height: 250rpx;
|
||||
margin-top: 24rpx;
|
||||
padding: 25rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.publish-field textarea {
|
||||
width: 100%;
|
||||
min-height: 200rpx;
|
||||
color: $ink;
|
||||
font-size: 24rpx;
|
||||
line-height: 1.7;
|
||||
}
|
||||
.publish-placeholder {
|
||||
color: #8e806e;
|
||||
}
|
||||
.publish-form > .app-button {
|
||||
margin-top: 24rpx;
|
||||
}
|
||||
.publish-result {
|
||||
margin-top: 20%;
|
||||
text-align: center;
|
||||
}
|
||||
.publish-result > .app-button {
|
||||
margin: 30rpx auto 0;
|
||||
}
|
||||
.publish-page { display: flex; min-height: 100vh; flex-direction: column; background: $paper; }
|
||||
.publish-page__header, .publish-panel { @include adaptive.adaptive-family-content; z-index: 1; }
|
||||
.publish-panel { width: calc(100% - 32rpx); margin: 18rpx auto 0; padding: 9%; box-sizing: border-box; }
|
||||
.publish-form > text, .publish-result > text { display: block; }
|
||||
.publish-form > text:first-child, .publish-result > text:first-child { color: $ink; font-family: STKaiti, KaiTi, serif; font-size: 36rpx; font-weight: 700; }
|
||||
.publish-form > text:nth-child(2), .publish-result > text:nth-child(2) { margin-top: 12rpx; color: $ink-muted; font-size: 23rpx; line-height: 1.6; }
|
||||
.publish-field { @include adaptive.adaptive-family-field; display: flex; min-height: 250rpx; margin-top: 24rpx; padding: 25rpx; box-sizing: border-box; }
|
||||
.publish-field textarea { width: 100%; min-height: 200rpx; color: $ink; font-size: 24rpx; line-height: 1.7; }
|
||||
.publish-placeholder { color: #8e806e; }
|
||||
.publish-error { margin-top: 12rpx; color: $brand-red; font-size: 22rpx; line-height: 1.5; }
|
||||
.publish-form > .app-button { margin-top: 24rpx; }
|
||||
.publish-result { margin-top: 20%; text-align: center; }
|
||||
.publish-result > .app-button { margin: 30rpx auto 0; }
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user