Files
jiapuapp/pages/family/f07-album-list.vue
T
2026-07-27 06:50:23 +08:00

106 lines
9.6 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-07用途读取并创建当前家谱的相册 -->
<template>
<view class="album-list-page">
<ModulePageBackground module="family" />
<view class="album-list-header"><PageHeader title="家族相册" :action="hasValidContext ? '新建' : ''" custom-back @back="requestBack" @action="openCreateDialog" /></view>
<view class="album-list-content">
<view v-if="!hasValidContext" class="album-state-card"><text>相册入口无效</text><text>没有取得有效家谱标识。</text><AppButton block label="返回上一页" @click="goBack" /></view>
<view v-else-if="listState === 'loading'" class="album-state-card"><AppLoading text="正在读取家族相册" /></view>
<view v-else-if="listState === 'error'" class="album-state-card"><text>暂时无法读取家族相册</text><text>{{ listError }}</text><AppButton block type="secondary" label="重新加载" @click="loadAlbums" /></view>
<view v-else-if="listState === 'empty'" class="album-state-card"><text>还没有相册</text><text>新建第一本相册,整理家族影像。</text><AppButton block label="新建相册" @click="openCreateDialog" /></view>
<view v-else class="album-list">
<view v-for="item in albums" :key="item.id" class="album-card" @click="openAlbum(item)"><text>{{ item.name }}</text><text v-if="item.description">{{ item.description }}</text><text>{{ item.photoCount }} 张照片</text></view>
</view>
</view>
<AppDialog :visible="dialogVisible" eyebrow="新建相册" title="为家人整理一段影像" :confirm-text="isSubmitting ? '正在提交' : '提交相册'" cancel-text="取消" show-cancel :close-on-mask="false" @confirm="submitAlbum" @cancel="closeCreateDialog">
<view class="album-dialog-field"><text><text class="required-mark">*</text>相册名称</text><input v-model="form.albumName" placeholder="例如:春节团圆" @input="submitError = ''" /></view>
<view class="album-dialog-field"><text>相册说明</text><textarea v-model="form.albumDesc" auto-height placeholder="介绍这组影像" @input="submitError = ''" /></view>
<view class="album-dialog-field album-dialog-field--cover"><view><text>封面图片</text><text class="album-field-hint">选择图片后会取得真实上传回执并作为封面关联</text></view><button class="upload-button" :disabled="uploading || isSubmitting" @click="uploadCover">{{ uploading ? '上传中…' : '选择图片' }}</button><text v-if="coverFileName" class="upload-receipt">已上传:{{ coverFileName }}</text><text v-if="uploadError" class="album-field-error">{{ uploadError }}</text></view>
<view class="album-dialog-field"><text>排序值</text><input v-model="form.sortOrder" type="number" placeholder="数值越小越靠前" @input="submitError = ''" /></view>
<text v-if="submitError" class="album-field-error">{{ submitError }}</text>
</AppDialog>
</view>
</template>
<script setup>
import { computed, onUnmounted, reactive, ref } from "vue";
import { onBackPress, onLoad, onShow } from "@dcloudio/uni-app";
import AppButton from "@/components/AppButton.vue";
import AppDialog from "@/components/AppDialog.vue";
import AppLoading from "@/components/AppLoading.vue";
import ModulePageBackground from "@/components/ModulePageBackground.vue";
import PageHeader from "@/components/PageHeader.vue";
import { appApi, createRequestController, isRequestCancelled } from "@/utils/api.js";
import { isImagePickCancelled, pickAndUploadImage, toConsumerOssId } from "@/utils/resumable-image-upload.js";
import { goBack, handleBackPress, openPage, runBackGuard } from "@/utils/navigation.js";
const genealogyId = ref("");
const albums = ref([]);
const listState = ref("loading");
const listError = ref("");
const dialogVisible = ref(false);
const form = reactive({ albumName: "", albumDesc: "", sortOrder: "" });
const coverOssId = ref(null);
const coverFileName = ref("");
const submitError = ref("");
const uploadError = ref("");
const uploading = ref(false);
const isSubmitting = ref(false);
const controller = createRequestController();
let active = true;
const hasValidContext = computed(() => /^[1-9]\d*$/.test(genealogyId.value));
const text = (value) => typeof value === "string" || typeof value === "number" ? String(value).trim() : "";
const toAlbum = (item) => {
const id = text(item?.albumId);
if (!/^[1-9]\d*$/.test(id)) return null;
return { id, name: text(item.albumName) || "未命名相册", description: text(item.albumDesc), photoCount: Number.isSafeInteger(item.photoCount) ? item.photoCount : 0 };
};
const loadAlbums = async () => {
if (!hasValidContext.value) return;
controller.abort();
listState.value = "loading";
listError.value = "";
try {
const rows = await appApi.getAlbums(genealogyId.value, { requestController: controller });
if (!active) return;
albums.value = rows.map(toAlbum).filter(Boolean);
listState.value = albums.value.length ? "list" : "empty";
} catch (error) {
if (!active || isRequestCancelled(error)) return;
listState.value = "error";
listError.value = error?.message || "请稍后重试。";
}
};
const resetDraft = () => { Object.assign(form, { albumName: "", albumDesc: "", sortOrder: "" }); coverOssId.value = null; coverFileName.value = ""; submitError.value = ""; uploadError.value = ""; };
const openCreateDialog = () => { if (!hasValidContext.value || isSubmitting.value || uploading.value) return; resetDraft(); dialogVisible.value = true; };
const closeCreateDialog = () => { if (!isSubmitting.value && !uploading.value) dialogVisible.value = false; };
const uploadCover = async () => {
if (uploading.value || isSubmitting.value) return;
uploading.value = true;
uploadError.value = "";
try { const receipt = await pickAndUploadImage({ requestController: controller }); coverOssId.value = toConsumerOssId(receipt.ossId); coverFileName.value = receipt.fileName || "封面图片"; }
catch (error) { if (!isImagePickCancelled(error) && !isRequestCancelled(error)) uploadError.value = error?.message || "封面图片上传失败,请稍后重试。"; }
finally { uploading.value = false; }
};
const submitAlbum = async () => {
if (isSubmitting.value || uploading.value) return;
if (!form.albumName.trim()) { submitError.value = "请填写相册名称"; return; }
isSubmitting.value = true;
submitError.value = "";
try { await appApi.createAlbum(genealogyId.value, { ...form, coverOssId: coverOssId.value }, { requestController: controller }); dialogVisible.value = false; await loadAlbums(); }
catch (error) { if (!isRequestCancelled(error)) submitError.value = error?.message || "相册提交失败,请稍后重试。"; }
finally { isSubmitting.value = false; }
};
const openAlbum = (item) => openPage("F08", { genealogyId: genealogyId.value, albumId: item.id }, "F07");
const requestBack = () => runBackGuard({ transientOpen: dialogVisible.value, submitting: isSubmitting.value || uploading.value, "close-transient": closeCreateDialog, "block-submitting": () => true });
onLoad((query) => { genealogyId.value = String(query?.genealogyId || ""); if (hasValidContext.value) loadAlbums(); else listState.value = "invalid"; });
onShow(() => { if (hasValidContext.value && !dialogVisible.value && listState.value !== "loading") loadAlbums(); });
onBackPress((event) => handleBackPress(event, requestBack));
onUnmounted(() => { active = false; controller.abort(); });
</script>
<style scoped lang="scss">
@use "../../styles/adaptive-frame-profiles.scss" as adaptive;
.album-list-page { display: flex; min-height: 100vh; flex-direction: column; background: $paper; }.album-list-header, .album-list-content { z-index: 1; }.album-list-content { padding: 18rpx 24rpx 72rpx; }.album-state-card, .album-card { @include adaptive.adaptive-family-content; }.album-state-card { min-height: 340rpx; padding: 78rpx 52rpx 50rpx; text-align: center; }.album-state-card text, .album-card text { display: block; }.album-state-card text:first-child, .album-card text:first-child { color: $ink; font-family: STKaiti, KaiTi, serif; font-size: 35rpx; font-weight: 700; }.album-state-card text:nth-child(2), .album-card text:not(:first-child) { margin-top: 12rpx; color: $ink-muted; font-size: 23rpx; line-height: 1.55; }.album-state-card .app-button { margin-top: 28rpx; }.album-list { display: flex; flex-direction: column; gap: 16rpx; }.album-card { padding: 28rpx 30rpx; }.album-dialog-field { width: 100%; margin: 20rpx 0; text-align: left; }.album-dialog-field > text:first-child, .album-dialog-field--cover > view > text:first-child { display: block; color: $ink; font-size: 23rpx; font-weight: 700; }.required-mark { display: inline-block; margin-right: 4rpx; color: $brand-red; font-size: 26rpx; }.album-dialog-field input, .album-dialog-field textarea { @include adaptive.adaptive-family-field; box-sizing: border-box; display: block; width: 100%; margin-top: 10rpx; padding: 16rpx 22rpx; color: $ink; font-size: 24rpx; }.album-dialog-field input { min-height: 76rpx; padding-top: 0; padding-bottom: 0; }.album-dialog-field textarea { min-height: 112rpx; line-height: 1.55; }.album-dialog-field--cover { display: grid; gap: 12rpx; padding: 18rpx; border: 1rpx solid rgba(128, 89, 49, .34); border-radius: 12rpx; background: rgba(255, 252, 245, .7); }.album-field-hint { display: block; margin-top: 6rpx; color: $ink-muted; font-size: 20rpx; line-height: 1.45; }.upload-button { justify-self: start; min-height: 88rpx; margin: 0; padding: 0 20rpx; border: 1rpx solid rgba(159, 23, 15, .42); border-radius: 8rpx; background: transparent; color: $brand-red; font-size: 22rpx; }.upload-receipt { color: $ink-muted; font-size: 21rpx; overflow-wrap: anywhere; }.album-field-error { display: block; margin-top: 8rpx; color: $brand-red; font-size: 21rpx; line-height: 1.45; }
</style>