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

235 lines
11 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" :class="albumStateClasses">
<ModulePageBackground module="family" />
<view class="album-list-header"><PageHeader title="家族相册" :action="hasValidContext ? '新建' : ''" custom-back @back="requestBack" @action="createAlbum" /></view>
<view v-if="albumState === 'loading'" class="album-list-loading">
<AppLoading text="正在整理家族相册" description="请稍候,正在读取照片与更新时间。" />
</view>
<view v-else class="album-list-content">
<template v-if="albumState === 'ready'">
<view class="album-list-lead"><text>让每一张照片都回到家人身边</text><text> {{ albums.length }} 本相册</text></view>
<view v-if="localAlbumPreview" class="album-local-preview">
<text>本地预览 · 尚未提交服务器</text>
<text>{{ localAlbumPreview.name }}</text>
<text>这本相册不会加入正式列表离开页面后不保存</text>
</view>
<view v-if="albums.length" class="album-list">
<view v-for="album in albums" :key="album.id" class="album-card" role="button" :aria-label="`打开相册${album.name}`" @click="openAlbum(album)">
<view class="album-card__media"><image class="album-card__cover" :src="album.cover" mode="aspectFill" :alt="album.name" /></view>
<view class="album-card__copy">
<text>{{ album.name }}</text>
<text>{{ album.photoCount }} 张照片 · {{ album.updatedAt }}</text>
<text>{{ album.description }}</text>
</view>
</view>
<AppButton block label="新建相册" @click="createAlbum" />
</view>
<view v-else class="album-state-card">
<text>还没有相册</text><text>创建一本相册把团圆成长与祖居旧影整理在一起</text>
<AppButton block label="新建相册" @click="createAlbum" />
</view>
</template>
<view v-else class="album-state-card">
<text>{{ stateCopy.title }}</text>
<text>{{ stateCopy.copy }}</text>
<AppButton :type="albumState === 'error' ? 'secondary' : 'primary'" block :label="stateCopy.action" @click="handleStateAction" />
</view>
</view>
<AppDialog :visible="dialogVisible" eyebrow="新建相册预览" title="为家人整理一段影像" message="当前只生成本地预览,不会创建服务器相册。" confirm-text="生成本地预览" cancel-text="取消" show-cancel :close-on-mask="false" @confirm="confirmCreateAlbum" @cancel="requestCloseCreateDialog">
<view class="album-dialog-field">
<text>相册名称</text>
<input v-model="albumNameDraft" maxlength="30" placeholder="例如:春节团圆" />
<text v-if="albumNameError">{{ albumNameError }}</text>
</view>
</AppDialog>
<AppDialog
:visible="discardVisible"
title="放弃相册草稿?"
message="当前相册尚未提交服务器,确认后不会保留。"
confirm-text="放弃"
cancel-text="继续整理"
show-cancel
:close-on-mask="false"
@confirm="confirmDiscard"
@cancel="cancelDiscard"
/>
</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 AppLoading from "@/components/AppLoading.vue";
import ModulePageBackground from "@/components/ModulePageBackground.vue";
import PageHeader from "@/components/PageHeader.vue";
import {
getGenealogyFixtureAccess,
listFamilyAlbumFixtures,
} from "@/data/mock.js";
import { createDiscardConfirmation } from "@/utils/discard-confirmation.js";
import {
goBack,
handleBackPress,
openPage,
runBackGuard,
} from "@/utils/navigation.js";
const genealogyId = ref("");
const hasValidContext = ref(false);
const albums = ref([]);
const albumState = ref("loading");
const dialogVisible = ref(false);
const albumNameDraft = ref("");
const albumNameError = ref("");
const localAlbumPreview = ref(null);
const discardVisible = ref(false);
const albumStateClasses = computed(() => ({
[`album-list-state--${albumState.value}`]: true,
"album-state--empty": albumState.value === "empty",
"album-list-state--loading": albumState.value === "loading",
"album-list-state--error": albumState.value === "error",
}));
const isDirty = computed(() =>
Boolean(albumNameDraft.value.trim() || localAlbumPreview.value),
);
const stateCopy = computed(() => ({
empty: {
title: "还没有相册",
copy: "当前家谱尚无可读取相册,可先生成不会保存的本地预览。",
action: "填写相册预览",
},
error: {
title: "相册暂不可用",
copy: "请稍后重新查看,已有照片不会受到影响。",
action: "重新查看",
},
invalid: {
title: "相册入口无效",
copy: "没有找到可访问的成员家谱,页面不会展示其他家谱相册。",
action: "返回上一页",
},
}[albumState.value]));
const discardConfirmation = createDiscardConfirmation((visible) => {
discardVisible.value = visible;
});
const requestDiscardConfirmation = discardConfirmation.request;
const confirmDiscard = discardConfirmation.confirm;
const cancelDiscard = discardConfirmation.cancel;
onLoad((query) => {
genealogyId.value = String(query.genealogyId || "");
hasValidContext.value = ["owner", "member"].includes(
getGenealogyFixtureAccess(genealogyId.value).accessRole,
);
if (!genealogyId.value || !hasValidContext.value) {
albumState.value = "invalid";
return;
}
albums.value = listFamilyAlbumFixtures(genealogyId.value);
albumState.value = ["loading", "empty", "error"].includes(query.state)
? query.state
: albums.value.length
? "ready"
: "empty";
});
const openAlbum = (album) =>
openPage(
"F08",
{ genealogyId: genealogyId.value, albumId: String(album.id) },
"F07",
);
const createAlbum = () => { albumNameDraft.value = ""; albumNameError.value = ""; dialogVisible.value = true; };
const closeCreateDialog = () => { dialogVisible.value = false; };
const requestCloseCreateDialog = async () => {
if (!albumNameDraft.value.trim()) {
closeCreateDialog();
return true;
}
const confirmed = await requestDiscardConfirmation();
if (!confirmed) return false;
albumNameDraft.value = "";
albumNameError.value = "";
closeCreateDialog();
return true;
};
const confirmCreateAlbum = () => {
const name = albumNameDraft.value.trim();
if (!name) { albumNameError.value = "请填写相册名称"; return; }
localAlbumPreview.value = Object.freeze({ name });
albumNameDraft.value = "";
dialogVisible.value = false;
};
const restoreAlbums = () => {
albums.value = listFamilyAlbumFixtures(genealogyId.value);
albumState.value = albums.value.length ? "ready" : "empty";
};
const handleStateAction = () => {
if (albumState.value === "invalid") return goBack();
if (albumState.value === "error") return restoreAlbums();
return createAlbum();
};
const requestBack = () => {
if (discardVisible.value) {
return runBackGuard({
transientOpen: true,
"close-transient": cancelDiscard,
});
}
if (dialogVisible.value) {
return runBackGuard({
transientOpen: true,
"close-transient": requestCloseCreateDialog,
});
}
return runBackGuard({
dirty: isDirty.value,
"confirm-discard": requestDiscardConfirmation,
});
};
onBackPress((event) => handleBackPress(event, requestBack));
onUnmounted(() => { discardConfirmation.dispose(); });
</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-loading, .album-list-content { z-index: 1; }
.album-list-loading { min-height: calc(100vh - 100rpx); }
.album-list-content { padding: 18rpx 24rpx 72rpx; }
.album-list-lead { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 8rpx 18rpx; min-height: 62rpx; padding: 0 20rpx; color: $ink-muted; font-size: 22rpx; background: url("/static/assets/modules/genealogy/transparent/section-divider.png") center / 100% auto no-repeat; }
.album-list { display: flex; flex-direction: column; gap: 16rpx; margin-top: 16rpx; }
.album-local-preview { @include adaptive.adaptive-family-field; margin-top: 16rpx; padding: 22rpx 24rpx; }
.album-local-preview text { display: block; color: $ink-muted; font-size: 22rpx; line-height: 1.5; }
.album-local-preview text:first-child { color: $brand-red; font-weight: 700; }
.album-local-preview text:nth-child(2) { margin-top: 6rpx; color: $ink; font-family: STKaiti, KaiTi, serif; font-size: 29rpx; font-weight: 700; }
.album-card, .album-state-card { @include adaptive.adaptive-family-content; width: 100%; }
.album-card { display: grid; grid-template-columns: minmax(150rpx, 0.7fr) minmax(0, 1.3fr); min-height: 210rpx; gap: 22rpx; padding: 30rpx 38rpx; }
.album-card__media { width: 100%; aspect-ratio: 4 / 3; align-self: center; }
.album-card__cover { display: block; width: 100%; height: 100%; }
.album-card__copy { align-self: center; min-width: 0; }
.album-card__copy text { display: block; overflow-wrap: anywhere; }
.album-card__copy text:first-child { color: $ink; font-family: STKaiti, KaiTi, serif; font-size: 30rpx; font-weight: 700; }
.album-card__copy text:nth-child(2) { margin-top: 9rpx; color: $brand-red; font-size: 21rpx; }
.album-card__copy text:last-child { margin-top: 8rpx; color: $ink-muted; font-size: 22rpx; line-height: 1.5; }
.album-list > .app-button { margin-top: 8rpx; }
.album-state-card { min-height: 340rpx; margin-top: 30rpx; padding: 78rpx 52rpx 50rpx; text-align: center; }
.album-state-card > text { display: block; }
.album-state-card > text:first-child { color: $ink; font-family: STKaiti, KaiTi, serif; font-size: 35rpx; font-weight: 700; }
.album-state-card > text:nth-child(2) { margin-top: 18rpx; color: $ink-muted; font-size: 24rpx; line-height: 1.65; }
.album-state-card .app-button { margin-top: 28rpx; }
.album-dialog-field { width: 100%; margin: 24rpx 0; text-align: left; }
.album-dialog-field > text:first-child { display: block; color: $ink; font-size: 23rpx; font-weight: 700; }
.album-dialog-field input { @include adaptive.adaptive-family-field; width: 100%; min-height: 76rpx; margin-top: 10rpx; padding: 0 22rpx; color: $ink; font-size: 24rpx; }
.album-dialog-field > text:last-child { display: block; margin-top: 8rpx; color: $brand-red; font-size: 21rpx; }
@media (max-width: 340px) { .album-card { grid-template-columns: 130rpx minmax(0, 1fr); gap: 16rpx; padding-right: 30rpx; padding-left: 30rpx; } }
</style>