完成50%

This commit is contained in:
2026-07-23 08:23:59 +08:00
parent 9b0ad62df4
commit f1edc6b533
218 changed files with 24318 additions and 5514 deletions
+131 -30
View File
@@ -2,7 +2,7 @@
<template>
<view class="album-list-page" :class="albumStateClasses">
<ModulePageBackground module="family" />
<view class="album-list-header"><PageHeader title="家族相册" action="新建" @action="createAlbum" /></view>
<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="请稍候,正在读取照片与更新时间。" />
@@ -11,6 +11,11 @@
<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>
@@ -29,76 +34,168 @@
</template>
<view v-else class="album-state-card">
<text>{{ albumState === 'empty' ? '还没有相册' : '相册暂不可用' }}</text>
<text>{{ albumState === 'empty' ? '从第一本团圆相册开始收集家族影像。' : '请稍后重新查看,已有照片不会受到影响。' }}</text>
<AppButton :type="albumState === 'error' ? 'secondary' : 'primary'" block :label="albumState === 'error' ? '重新查看' : '新建相册'" @click="albumState === 'error' ? restoreAlbums() : createAlbum()" />
<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 @confirm="confirmCreateAlbum" @cancel="closeCreateDialog">
<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>
<AppToast :visible="toastVisible" message="相册已创建" />
<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 { onLoad } from "@dcloudio/uni-app";
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 AppToast from "@/components/AppToast.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 baseAlbums = [
{ id: "201", name: "2024 春节团圆", photoCount: 18, updatedAt: "今天更新", description: "三代家人的团圆饭与院前合影", cover: "/static/assets/modules/family/f08/f08-reunion-hero.png" },
{ id: "202", name: "祖居旧影", photoCount: 32, updatedAt: "5 月 10 日更新", description: "祖居、旧物与长辈珍藏的老照片", cover: "/static/assets/modules/family/f08/f08-ancestral-home.png" },
{ id: "203", name: "儿童成长", photoCount: 46, updatedAt: "持续更新", description: "记录孩子们每一个值得珍藏的瞬间", cover: "/static/assets/modules/family/f08/f08-family-portrait.png" },
];
const albums = ref([...baseAlbums]);
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 toastVisible = ref(false);
let toastTimer = null;
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) => {
const count = Math.max(1, Math.min(Number(query.count) || baseAlbums.length, 30));
albums.value = Array.from({ length: count }, (_, index) => ({
...baseAlbums[index % baseAlbums.length],
id: String(201 + index),
name: count > baseAlbums.length ? `${baseAlbums[index % baseAlbums.length].name}${index + 1}` : baseAlbums[index].name,
}));
albumState.value = ["loading", "empty", "error"].includes(query.state) ? query.state : "ready";
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) => uni.navigateTo({ url: `/pages/family/f08-album-detail?albumId=${album.id}` });
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; }
albums.value.unshift({ id: String(Date.now()), name, photoCount: 0, updatedAt: "刚刚创建", description: "等待添加第一张照片", cover: "/static/assets/modules/family/f08/f08-reunion-hero.png" });
albumState.value = "ready";
localAlbumPreview.value = Object.freeze({ name });
albumNameDraft.value = "";
dialogVisible.value = false;
toastVisible.value = true;
toastTimer = setTimeout(() => { toastVisible.value = false; }, 1800);
};
const restoreAlbums = () => { albumState.value = "ready"; };
onUnmounted(() => { if (toastTimer) clearTimeout(toastTimer); });
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">
@@ -110,6 +207,10 @@ onUnmounted(() => { if (toastTimer) clearTimeout(toastTimer); });
.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; }