feat: 完成前端业务闭环与后端联调
This commit is contained in:
@@ -26,6 +26,7 @@
|
||||
:label="protectionSubmitting ? '正在验证' : '解锁并查看'"
|
||||
@click="unlockContent"
|
||||
/>
|
||||
<button class="detail-recovery-link" @click="passwordRecoveryVisible = true">忘记内容密码?</button>
|
||||
<text v-if="detailError" class="detail-error">{{ detailError }}</text>
|
||||
</template>
|
||||
<template v-else>
|
||||
@@ -33,18 +34,26 @@
|
||||
<text>关联人物:{{ detailRecord?.personName || "未关联人物" }}</text>
|
||||
<text>记录类型:{{ detailRecord?.typeLabel || "未填写" }}</text>
|
||||
<text>记录日期:{{ detailRecord?.recordDate || "未填写" }}</text>
|
||||
<text v-if="detailRecord?.createTime">创建时间:{{ formatMinuteTimestamp(detailRecord.createTime) }}</text>
|
||||
<text v-if="detailRecord?.remindTime">提醒时间:{{ detailRecord.remindTime }}</text>
|
||||
<text class="detail-content__body">{{ detailRecord?.content || "未填写记录内容" }}</text>
|
||||
<view v-if="detailRecord?.mediaFiles?.length" class="detail-media">
|
||||
<image
|
||||
v-for="file in detailRecord.mediaFiles"
|
||||
:key="file.fileId"
|
||||
:src="file.accessUrl"
|
||||
mode="aspectFill"
|
||||
role="button"
|
||||
aria-label="查看成长记录图片"
|
||||
@click="previewMedia(file)"
|
||||
/>
|
||||
<template v-for="file in detailRecord.mediaFiles" :key="file.fileId">
|
||||
<video
|
||||
v-if="isVideoFile(file)"
|
||||
:src="file.accessUrl"
|
||||
controls
|
||||
:aria-label="file.fileName || '播放成长记录视频'"
|
||||
/>
|
||||
<image
|
||||
v-else
|
||||
:src="file.accessUrl"
|
||||
mode="aspectFill"
|
||||
role="button"
|
||||
aria-label="查看成长记录图片"
|
||||
@click="previewMedia(file)"
|
||||
/>
|
||||
</template>
|
||||
</view>
|
||||
<view v-if="detailRecord?.canManageProtection" class="detail-protection-actions">
|
||||
<AppButton
|
||||
@@ -87,6 +96,15 @@
|
||||
/>
|
||||
<text v-if="detailError" class="detail-error">{{ detailError }}</text>
|
||||
</AppDialog>
|
||||
<ContentPasswordRecoveryDialog
|
||||
:visible="passwordRecoveryVisible"
|
||||
:genealogy-id="genealogyId"
|
||||
resource-type="GROWTH_RECORD"
|
||||
:resource-id="String(detailRecord?.id || '')"
|
||||
@close="passwordRecoveryVisible = false"
|
||||
@complete="completePasswordRecovery"
|
||||
@busy-change="passwordRecoveryBusy = $event"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
@@ -94,6 +112,8 @@ import { computed, onUnmounted, ref, watch } from "vue";
|
||||
import AppButton from "@/components/AppButton.vue";
|
||||
import AppDialog from "@/components/AppDialog.vue";
|
||||
import AppLoading from "@/components/AppLoading.vue";
|
||||
import { formatMinuteTimestamp } from "@/utils/display-time.js";
|
||||
import ContentPasswordRecoveryDialog from "@/components/ContentPasswordRecoveryDialog.vue";
|
||||
import {
|
||||
createRequestController,
|
||||
isRequestCancelled
|
||||
@@ -117,6 +137,8 @@ const contentPassword = ref("");
|
||||
const protectionSubmitting = ref(false);
|
||||
const protectionVisible = ref(false);
|
||||
const protectionMode = ref("set");
|
||||
const passwordRecoveryVisible = ref(false);
|
||||
const passwordRecoveryBusy = ref(false);
|
||||
const growthDetailReadRequestController = createRequestController();
|
||||
const growthDetailUnlockRequestController = createRequestController();
|
||||
const growthProtectionMutationRequestController = createRequestController();
|
||||
@@ -136,7 +158,7 @@ const protectionConfirmText = computed(() => {
|
||||
return protectionMode.value === "disable" ? "确认关闭" : "确认保存";
|
||||
});
|
||||
const isBusy = computed(() =>
|
||||
detailState.value === "loading" || protectionSubmitting.value,
|
||||
detailState.value === "loading" || protectionSubmitting.value || passwordRecoveryBusy.value,
|
||||
);
|
||||
const hasTransient = computed(() =>
|
||||
Boolean(detailRecord.value) || protectionVisible.value,
|
||||
@@ -155,6 +177,7 @@ const clearDetailState = () => {
|
||||
detailError.value = "";
|
||||
contentPassword.value = "";
|
||||
protectionVisible.value = false;
|
||||
passwordRecoveryVisible.value = false;
|
||||
};
|
||||
const applyRecordDetail = (recordDetail) => {
|
||||
detailRecord.value = {
|
||||
@@ -231,6 +254,10 @@ const unlockContent = async () => {
|
||||
if (isCurrentRecord(recordId)) protectionSubmitting.value = false;
|
||||
}
|
||||
};
|
||||
const completePasswordRecovery = (newPassword) => {
|
||||
contentPassword.value = newPassword;
|
||||
detailError.value = "内容密码已重置,请点击“解锁并查看”确认。";
|
||||
};
|
||||
|
||||
const openProtection = (mode) => {
|
||||
if (!detailRecord.value?.canManageProtection || protectionSubmitting.value) return;
|
||||
@@ -315,6 +342,11 @@ const close = () => {
|
||||
return true;
|
||||
};
|
||||
const closeTransient = () => {
|
||||
if (passwordRecoveryVisible.value) {
|
||||
if (passwordRecoveryBusy.value) return false;
|
||||
passwordRecoveryVisible.value = false;
|
||||
return true;
|
||||
}
|
||||
if (protectionVisible.value) {
|
||||
closeProtection();
|
||||
return !protectionSubmitting.value;
|
||||
@@ -323,11 +355,17 @@ const closeTransient = () => {
|
||||
};
|
||||
const previewMedia = (file) => {
|
||||
const urls = detailRecord.value?.mediaFiles
|
||||
?.filter((mediaFile) => !isVideoFile(mediaFile))
|
||||
?.map((mediaFile) => mediaFile.accessUrl)
|
||||
.filter(Boolean) || [];
|
||||
if (!file?.accessUrl || !urls.length || typeof uni?.previewImage !== "function") return;
|
||||
uni.previewImage({ current: file.accessUrl, urls });
|
||||
};
|
||||
const isVideoFile = (file) => {
|
||||
const mediaType = String(file?.mediaType || "").toLowerCase();
|
||||
const fileName = String(file?.fileName || "");
|
||||
return mediaType === "video" || mediaType.startsWith("video/") || /\.(mp4|mov|m4v|webm|avi|mkv)$/i.test(fileName);
|
||||
};
|
||||
|
||||
defineExpose({ closeTransient, open });
|
||||
|
||||
@@ -365,7 +403,8 @@ onUnmounted(() => {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 10rpx;
|
||||
}
|
||||
.detail-media image {
|
||||
.detail-media image,
|
||||
.detail-media video {
|
||||
width: 100%;
|
||||
height: 150rpx;
|
||||
border-radius: 8rpx;
|
||||
@@ -383,6 +422,8 @@ onUnmounted(() => {
|
||||
color: $ink;
|
||||
font-size: clamp(14px, 22rpx, 17px);
|
||||
}
|
||||
.detail-recovery-link { display: block; min-height: var(--app-touch-min); margin: 4rpx auto 0; padding: 0 16rpx; border: 0; background: transparent; color: $brand-red; font-size: clamp(13px, 21rpx, 16px); }
|
||||
.detail-recovery-link::after { border: 0; }
|
||||
.detail-protection-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
Reference in New Issue
Block a user