235 lines
11 KiB
Vue
235 lines
11 KiB
Vue
<!-- 页面编号: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>
|