完成70%
This commit is contained in:
+58
-188
@@ -1,63 +1,22 @@
|
||||
<!-- 页面编号:F-07;用途:家族相册列表、创建与受控状态。 -->
|
||||
<!-- 页面编号:F-07;用途:相册入口。列表 DTO 缺失时不展示本地相册。 -->
|
||||
<template>
|
||||
<view class="album-list-page" :class="albumStateClasses">
|
||||
<view class="album-list-page">
|
||||
<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">
|
||||
<view class="album-list-header"><PageHeader title="家族相册" :action="hasValidContext ? '新建' : ''" custom-back @back="requestBack" @action="openCreateDialog" /></view>
|
||||
<view class="album-list-content">
|
||||
<view class="album-state-card">
|
||||
<text>{{ stateCopy.title }}</text>
|
||||
<text>{{ stateCopy.copy }}</text>
|
||||
<AppButton :type="albumState === 'error' ? 'secondary' : 'primary'" block :label="stateCopy.action" @click="handleStateAction" />
|
||||
<AppButton 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">
|
||||
<AppDialog :visible="dialogVisible" eyebrow="新建相册" title="为家人整理一段影像" message="仅提交已声明的相册名称;封面、排序和状态没有可用 owner,不会猜测提交。" :confirm-text="isSubmitting ? '正在提交' : '提交相册'" cancel-text="取消" show-cancel :close-on-mask="false" @confirm="submitAlbum" @cancel="closeCreateDialog">
|
||||
<view class="album-dialog-field">
|
||||
<text>相册名称</text>
|
||||
<input v-model="albumNameDraft" maxlength="30" placeholder="例如:春节团圆" />
|
||||
<text v-if="albumNameError">{{ albumNameError }}</text>
|
||||
<input v-model="albumNameDraft" maxlength="30" placeholder="例如:春节团圆" @input="submitError = ''" />
|
||||
<text v-if="submitError">{{ submitError }}</text>
|
||||
</view>
|
||||
</AppDialog>
|
||||
<AppDialog
|
||||
:visible="discardVisible"
|
||||
title="放弃相册草稿?"
|
||||
message="当前相册尚未提交服务器,确认后不会保留。"
|
||||
confirm-text="放弃"
|
||||
cancel-text="继续整理"
|
||||
show-cancel
|
||||
:close-on-mask="false"
|
||||
@confirm="confirmDiscard"
|
||||
@cancel="cancelDiscard"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
@@ -66,162 +25,74 @@ 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";
|
||||
import { appApi, createRequestController, isRequestCancelled } from "@/utils/api.js";
|
||||
import { goBack, handleBackPress, 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 submitError = ref("");
|
||||
const isSubmitting = ref(false);
|
||||
const created = ref(false);
|
||||
const requestController = createRequestController();
|
||||
const stateCopy = computed(() => !hasValidContext.value
|
||||
? { title: "相册入口无效", copy: "没有取得有效家谱标识,页面不会展示其他家谱相册。", action: "返回上一页" }
|
||||
: created.value
|
||||
? { title: "相册已提交服务端", copy: "服务端已返回成功信封;相册列表仍没有条目 DTO,页面不会生成本地相册卡片。", action: "新建相册" }
|
||||
: { title: "相册列表待后端字段合同", copy: "列表响应没有声明相册 ID、封面、名称、照片数、描述或更新时间字段;页面已停止展示本地相册。", action: "新建相册" },
|
||||
);
|
||||
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";
|
||||
genealogyId.value = String(query?.genealogyId || "");
|
||||
hasValidContext.value = /^[1-9]\d*$/.test(genealogyId.value);
|
||||
});
|
||||
const openCreateDialog = () => {
|
||||
if (!hasValidContext.value || isSubmitting.value) return;
|
||||
albumNameDraft.value = "";
|
||||
submitError.value = "";
|
||||
dialogVisible.value = true;
|
||||
};
|
||||
const closeCreateDialog = () => {
|
||||
if (!isSubmitting.value) dialogVisible.value = false;
|
||||
};
|
||||
const submitAlbum = async () => {
|
||||
if (isSubmitting.value) return;
|
||||
const albumName = albumNameDraft.value.trim();
|
||||
if (!albumName) {
|
||||
submitError.value = "请填写相册名称";
|
||||
return;
|
||||
}
|
||||
albums.value = listFamilyAlbumFixtures(genealogyId.value);
|
||||
albumState.value = ["loading", "empty", "error"].includes(query.state)
|
||||
? query.state
|
||||
: albums.value.length
|
||||
? "ready"
|
||||
: "empty";
|
||||
isSubmitting.value = true;
|
||||
submitError.value = "";
|
||||
try {
|
||||
await appApi.createAlbum(genealogyId.value, { albumName }, { requestController });
|
||||
dialogVisible.value = false;
|
||||
created.value = true;
|
||||
} catch (error) {
|
||||
if (!isRequestCancelled(error)) submitError.value = error?.message || "相册提交失败,请稍后重试。";
|
||||
} finally {
|
||||
isSubmitting.value = false;
|
||||
}
|
||||
};
|
||||
const requestBack = () => runBackGuard({
|
||||
transientOpen: dialogVisible.value,
|
||||
submitting: isSubmitting.value,
|
||||
"close-transient": closeCreateDialog,
|
||||
"block-submitting": () => true,
|
||||
});
|
||||
|
||||
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,
|
||||
});
|
||||
};
|
||||
const handleStateAction = () => hasValidContext.value ? openCreateDialog() : goBack();
|
||||
onBackPress((event) => handleBackPress(event, requestBack));
|
||||
onUnmounted(() => { discardConfirmation.dispose(); });
|
||||
onUnmounted(() => requestController.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-loading, .album-list-content { z-index: 1; }
|
||||
.album-list-loading { min-height: calc(100vh - 100rpx); }
|
||||
.album-list-header, .album-list-content { z-index: 1; }
|
||||
.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 { @include adaptive.adaptive-family-content; width: 100%; 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; }
|
||||
@@ -230,5 +101,4 @@ onUnmounted(() => { discardConfirmation.dispose(); });
|
||||
.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>
|
||||
|
||||
Reference in New Issue
Block a user