feat: 完成前端业务闭环与后端联调
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
<view class="article-detail-page" :class="`article-state--${articleState}`">
|
||||
<ModulePageBackground module="family" />
|
||||
<view class="article-detail-header"
|
||||
><PageHeader title="谱文详情" custom-back @back="backToArticles"
|
||||
><PageHeader title="谱文详情" custom-back @back="requestBack"
|
||||
/></view>
|
||||
|
||||
<view class="article-detail-content">
|
||||
@@ -12,6 +12,15 @@
|
||||
description="请稍候,正在同步谱文正文。"
|
||||
/>
|
||||
<view v-else-if="articleState === 'ready'" class="article-card">
|
||||
<image
|
||||
v-if="article.coverFile?.accessUrl"
|
||||
class="article-card__cover"
|
||||
:src="article.coverFile.accessUrl"
|
||||
mode="aspectFill"
|
||||
role="button"
|
||||
aria-label="查看谱文封面"
|
||||
@click="previewCover"
|
||||
/>
|
||||
<text v-if="article.category" class="article-card__category">{{
|
||||
article.category
|
||||
}}</text>
|
||||
@@ -27,13 +36,17 @@
|
||||
<text>这篇谱文已设置内容密码</text>
|
||||
<input v-model="protectionPassword" password maxlength="128" placeholder="请输入8至128位内容密码" />
|
||||
<AppButton block :disabled="protectionSubmitting" :label="protectionSubmitting ? '正在验证' : '解锁并查看'" @click="unlockArticle" />
|
||||
<button class="article-lock-card__recovery" @click="passwordRecoveryVisible = true">忘记内容密码?</button>
|
||||
<text v-if="protectionError">{{ protectionError }}</text>
|
||||
</view>
|
||||
<text class="article-card__content">{{
|
||||
article.contentProtected && !article.contentUnlocked ? "" : article.content || "作者暂未填写正文。"
|
||||
}}</text>
|
||||
<text class="article-card__views">阅读 {{ article.viewCount }} 次</text>
|
||||
<view v-if="article.canEdit || article.canDelete" class="article-card__actions">
|
||||
<view
|
||||
v-if="article.canEdit || article.canDelete || article.canManageProtection"
|
||||
class="article-card__actions"
|
||||
>
|
||||
<AppButton
|
||||
v-if="article.canEdit && article.content"
|
||||
compact
|
||||
@@ -75,9 +88,9 @@
|
||||
:visible="deleteConfirmationVisible"
|
||||
:close-on-mask="false"
|
||||
eyebrow="删除确认"
|
||||
title="删除这篇谱文?"
|
||||
message="删除后无法恢复,请确认当前内容不再需要。"
|
||||
confirm-text="确认删除"
|
||||
title="将这篇谱文移至回收站?"
|
||||
message="移入回收站后不再展示,家谱管理员可在保留期内恢复。"
|
||||
confirm-text="移至回收站"
|
||||
cancel-text="保留谱文"
|
||||
show-cancel
|
||||
@confirm="deleteArticle"
|
||||
@@ -98,15 +111,25 @@
|
||||
<input v-if="protectionMode === 'set'" v-model="protectionPassword" class="protection-dialog-input" password maxlength="128" placeholder="请输入8至128位内容密码" />
|
||||
<text v-if="protectionError" class="article-card__error">{{ protectionError }}</text>
|
||||
</AppDialog>
|
||||
<ContentPasswordRecoveryDialog
|
||||
:visible="passwordRecoveryVisible"
|
||||
:genealogy-id="genealogyId"
|
||||
resource-type="ARTICLE"
|
||||
:resource-id="articleId"
|
||||
@close="passwordRecoveryVisible = false"
|
||||
@complete="completePasswordRecovery"
|
||||
@busy-change="passwordRecoveryBusy = $event"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from "vue";
|
||||
import { onLoad, onShow, onUnload } from "@dcloudio/uni-app";
|
||||
import { onBackPress, onLoad, onShow, onUnload } from "@dcloudio/uni-app";
|
||||
import AppButton from "@/components/AppButton.vue";
|
||||
import AppDialog from "@/components/AppDialog.vue";
|
||||
import AppLoading from "@/components/AppLoading.vue";
|
||||
import ContentPasswordRecoveryDialog from "@/components/ContentPasswordRecoveryDialog.vue";
|
||||
import ModulePageBackground from "@/components/ModulePageBackground.vue";
|
||||
import PageHeader from "@/components/PageHeader.vue";
|
||||
import {
|
||||
@@ -115,7 +138,7 @@ import {
|
||||
} from "@/services/api/request-controller.js";
|
||||
import { familyArticleApi } from "@/services/api/family-article-service.js";
|
||||
import { getRequestErrorMessage } from "@/services/api/request-error-message.js";
|
||||
import { goBack, openPage, returnTo } from "@/utils/navigation/gateway.js";
|
||||
import { goBack, handleBackPress, openPage, returnTo } from "@/utils/navigation/gateway.js";
|
||||
|
||||
const genealogyId = ref("");
|
||||
const articleId = ref("");
|
||||
@@ -130,6 +153,8 @@ const protectionError = ref("");
|
||||
const protectionDialogVisible = ref(false);
|
||||
const protectionMode = ref("set");
|
||||
const protectionSubmitting = ref(false);
|
||||
const passwordRecoveryVisible = ref(false);
|
||||
const passwordRecoveryBusy = ref(false);
|
||||
const articleReadController = createRequestController();
|
||||
const articleProtectionController = createRequestController();
|
||||
const articleDeleteController = createRequestController();
|
||||
@@ -140,6 +165,11 @@ const hasValidContext = computed(
|
||||
() =>
|
||||
/^[1-9]\d*$/.test(genealogyId.value) && /^[1-9]\d*$/.test(articleId.value),
|
||||
);
|
||||
const previewCover = () => {
|
||||
const url = article.value?.coverFile?.accessUrl;
|
||||
if (!url || typeof uni?.previewImage !== "function") return;
|
||||
uni.previewImage({ current: url, urls: [url] });
|
||||
};
|
||||
const stateCopy = computed(() => {
|
||||
if (!hasValidContext.value) {
|
||||
return {
|
||||
@@ -241,6 +271,10 @@ const unlockArticle = async () => {
|
||||
protectionSubmitting.value = false;
|
||||
}
|
||||
};
|
||||
const completePasswordRecovery = (newPassword) => {
|
||||
protectionPassword.value = newPassword;
|
||||
protectionError.value = "内容密码已重置,请点击“解锁并查看”确认。";
|
||||
};
|
||||
const openProtectionDialog = (mode) => {
|
||||
if (!article.value?.canManageProtection || protectionSubmitting.value) return;
|
||||
protectionMode.value = mode;
|
||||
@@ -305,6 +339,23 @@ const editArticle = () =>
|
||||
const closeDeleteConfirmation = () => {
|
||||
if (!deleting.value) deleteConfirmationVisible.value = false;
|
||||
};
|
||||
const requestBack = () => {
|
||||
if (deleting.value || protectionSubmitting.value || passwordRecoveryBusy.value) return true;
|
||||
if (passwordRecoveryVisible.value) {
|
||||
passwordRecoveryVisible.value = false;
|
||||
return true;
|
||||
}
|
||||
if (protectionDialogVisible.value) {
|
||||
closeProtectionDialog();
|
||||
return true;
|
||||
}
|
||||
if (deleteConfirmationVisible.value) {
|
||||
closeDeleteConfirmation();
|
||||
return true;
|
||||
}
|
||||
return backToArticles();
|
||||
};
|
||||
onBackPress((event) => handleBackPress(event, requestBack));
|
||||
const deleteArticle = async () => {
|
||||
if (!article.value?.canDelete || deleting.value) return;
|
||||
deleting.value = true;
|
||||
@@ -352,12 +403,13 @@ const deleteArticle = async () => {
|
||||
}
|
||||
.article-detail-content {
|
||||
flex: 1;
|
||||
padding: 18rpx 24rpx 72rpx;
|
||||
padding: 18rpx 24rpx calc(72rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
.article-card,
|
||||
.article-state-card {
|
||||
@include adaptive-family-content;
|
||||
box-sizing: border-box;
|
||||
background-color: rgba($paper, 0.82);
|
||||
}
|
||||
.article-card {
|
||||
margin-top: 18rpx;
|
||||
@@ -383,6 +435,7 @@ const deleteArticle = async () => {
|
||||
font-size: clamp(20px, 40rpx, 26px);
|
||||
font-weight: 700;
|
||||
line-height: 1.32;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.article-card__meta {
|
||||
margin-top: 16rpx;
|
||||
@@ -404,6 +457,7 @@ const deleteArticle = async () => {
|
||||
color: $ink;
|
||||
font-size: clamp(16px, 28rpx, 20px);
|
||||
line-height: 1.85;
|
||||
overflow-wrap: anywhere;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
.article-card__views {
|
||||
@@ -412,6 +466,13 @@ const deleteArticle = async () => {
|
||||
font-size: clamp(13px, 21rpx, 16px);
|
||||
text-align: right;
|
||||
}
|
||||
.article-card__cover {
|
||||
width: 100%;
|
||||
height: 360rpx;
|
||||
margin-bottom: 22rpx;
|
||||
border-radius: 10rpx;
|
||||
background: rgba(128, 89, 49, 0.12);
|
||||
}
|
||||
.article-card__actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@@ -421,8 +482,10 @@ const deleteArticle = async () => {
|
||||
}
|
||||
.article-lock-card { margin: 16rpx 0; padding: 24rpx; border: 1rpx solid rgba(159, 23, 15, 0.3); border-radius: 10rpx; background: rgba(159, 23, 15, 0.05); }
|
||||
.article-lock-card > text { display: block; color: $ink; font-size: clamp(14px, 22rpx, 17px); }
|
||||
.article-lock-card__recovery { min-height: var(--app-touch-min); margin: 4rpx auto 0; padding: 0 16rpx; border: 0; background: transparent; color: #9e251b; font-size: clamp(13px, 22rpx, 16px); }
|
||||
.article-lock-card__recovery::after { border: 0; }
|
||||
.article-lock-card input,
|
||||
.protection-dialog-input { box-sizing: border-box; width: 100%; min-height: 76rpx; margin: 16rpx 0; padding: 14rpx 18rpx; border: 1rpx solid rgba(128, 89, 49, 0.32); border-radius: 8rpx; background: #fffdf8; color: $ink; font-size: clamp(14px, 22rpx, 17px); }
|
||||
.protection-dialog-input { box-sizing: border-box; width: 100%; min-height: 80rpx; margin: 16rpx 0; padding: 14rpx 18rpx; border: 1rpx solid rgba(128, 89, 49, 0.32); border-radius: 8rpx; background: #fffdf8; color: $ink; font-size: clamp(14px, 22rpx, 17px); }
|
||||
.article-card__error {
|
||||
margin-top: 12rpx;
|
||||
color: $brand-red;
|
||||
|
||||
Reference in New Issue
Block a user