完成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 -158
View File
@@ -5,46 +5,55 @@
:class="`media-upload-state--${uploadState}`"
>
<ModulePageBackground module="family" />
<view class="media-upload-header"><PageHeader title="上传照片" /></view>
<view class="media-upload-header"><PageHeader title="添加照片" custom-back @back="requestBack" /></view>
<view class="media-upload-content">
<view class="media-album-card">
<view v-if="uploadState === 'invalid'" class="media-invalid-card">
<text class="media-state-card__eyebrow">相册入口无效</text>
<text class="media-state-card__title">没有找到当前相册</text>
<text class="media-state-card__copy">页面不会把照片归入其他家谱或其他相册</text>
<view class="media-preview-action" @click="returnFromInvalid">
<AppButton block :label="genealogyId ? '返回相册列表' : '返回上一页'" />
</view>
</view>
<view v-else class="media-album-card">
<image
class="media-album-card__cover"
:src="mockLibrary[0].src"
:alt="mockLibrary[0].alt"
:src="album.cover"
:alt="album.name"
mode="aspectFill"
/>
<view class="media-album-card__copy">
<text class="media-album-card__eyebrow">当前相册</text>
<text class="media-album-card__title">2024 春节团圆</text>
<text class="media-album-card__title">{{ album.name }}</text>
<text class="media-album-card__limit">最多可选择 9 </text>
</view>
</view>
<view v-if="uploadState === 'permission'" class="media-permission-card">
<view v-if="uploadState === 'permission' && album" class="media-permission-card">
<text class="media-state-card__eyebrow">照片访问权限</text>
<text class="media-state-card__title">需要照片访问权限</text>
<text class="media-state-card__copy"
>授权后才能选择要加入当前相册的照片当前仅展示 H5 审核状态</text
>授权后才能选择照片当前只验证选择与说明流程不会上传</text
>
<view class="media-permission-action" @click="selectMockPhotos">
<AppButton block label="重新授权" />
</view>
</view>
<view v-else-if="uploadState === 'success'" class="media-success-card">
<text class="media-state-card__eyebrow">上传结果</text>
<text class="media-success-title">4 张照片已上传</text>
<view v-else-if="uploadState === 'preview'" class="media-preview-card">
<text class="media-state-card__eyebrow">本地流程预览</text>
<text class="media-preview-title">{{ selectedPhotos.length }} 张照片已完成本地校验</text>
<text class="media-state-card__copy"
>照片已加入2024 春节团圆可以返回相册继续查看</text
>照片尚未上传也没有加入{{ album.name }}返回后不会保存</text
>
<view class="media-success-action" @click="returnToAlbum">
<AppButton block label="返回当前相册" />
<view class="media-preview-action" @click="returnToAlbum">
<AppButton block label="返回当前相册(不上传)" />
</view>
</view>
<template v-else>
<template v-else-if="album">
<view class="media-section-heading">
<text>选择照片</text>
<text>{{ selectedPhotos.length }} / {{ MAX_PHOTOS }}</text>
@@ -57,7 +66,6 @@
class="media-photo-tile"
:class="{
'media-photo-tile--active': index === activePhotoIndex,
'media-photo-tile--failed': photo.status === 'error',
}"
@click="selectPhoto(index)"
>
@@ -80,21 +88,6 @@
>
<text>删除</text>
</view>
<text
v-if="photo.status === 'error'"
class="media-photo-status media-photo-status--error"
>上传失败</text
>
<text
v-else-if="photo.status === 'uploading'"
class="media-photo-status media-photo-status--uploading"
>{{ photo.progress }}%</text
>
<text
v-else-if="photo.status === 'uploaded'"
class="media-photo-status media-photo-status--uploaded"
>已上传</text
>
</view>
<view
@@ -162,104 +155,95 @@
</view>
</view>
<template v-if="uploadState === 'uploading'">
<text class="media-progress-copy"
>正在上传 2/{{ selectedPhotos.length }}</text
>
<view class="media-progress-action">
<AppButton block label="正在上传" />
</view>
</template>
<view
v-else-if="uploadState === 'error'"
class="media-retry-action"
@click="retryFailed"
>
<AppButton block label="重试 2 张失败照片" />
</view>
<view v-else class="media-primary-action" @click="startUpload">
<view class="media-primary-action" @click="generatePreview">
<AppButton
block
:disabled="isSubmitting"
:label="
selectedPhotos.length
? `上传 ${selectedPhotos.length} 张照片`
isSubmitting
? '正在校验'
: selectedPhotos.length
? `生成 ${selectedPhotos.length} 张照片预览`
: '选择照片'
"
/>
</view>
</template>
</view>
<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, ref } from "vue";
import { onLoad } from "@dcloudio/uni-app";
import { onBackPress, onLoad, onUnload } from "@dcloudio/uni-app";
import AppButton from "@/components/AppButton.vue";
import AppDialog from "@/components/AppDialog.vue";
import ModulePageBackground from "@/components/ModulePageBackground.vue";
import PageHeader from "@/components/PageHeader.vue";
import { findFamilyAlbumFixture } from "@/data/mock.js";
import { createDiscardConfirmation } from "@/utils/discard-confirmation.js";
import {
goBack,
handleBackPress,
returnTo,
runBackGuard,
} from "@/utils/navigation.js";
const MAX_PHOTOS = 9;
const genealogyId = ref("");
const albumId = ref("");
const album = ref(null);
const uploadState = ref("initial");
const activePhotoIndex = ref(0);
const batchDescription = ref("");
const validationMessage = ref("");
const selectedPhotos = ref([]);
const isSubmitting = ref(false);
const discardVisible = ref(false);
let previewTimer = null;
const mockLibrary = [
{
id: "reunion",
src: "/static/assets/modules/family/f08/f08-reunion-hero.png",
alt: "春节团圆时三代家人的合影",
note: "",
},
{
id: "portrait",
src: "/static/assets/modules/family/f08/f08-family-portrait.png",
alt: "家人在院落前的春节合影",
note: "",
},
{
id: "table",
src: "/static/assets/modules/family/f08/f08-reunion-table.png",
alt: "家人围坐吃年夜饭",
note: "",
},
{
id: "home",
src: "/static/assets/modules/family/f08/f08-ancestral-home.png",
alt: "祖居院落的复古旧照",
note: "",
},
{
id: "ancestor",
src: "/static/assets/modules/family/f08/f08-ancestral-portrait.png",
alt: "老一辈家人在祖居门前的合影",
note: "",
},
{ id: "reunion", src: "/static/assets/modules/family/f08/f08-reunion-hero.png", alt: "春节团圆时三代家人的合影", note: "" },
{ id: "portrait", src: "/static/assets/modules/family/f08/f08-family-portrait.png", alt: "家人在院落前的春节合影", note: "" },
{ id: "table", src: "/static/assets/modules/family/f08/f08-reunion-table.png", alt: "家人围坐吃年夜饭", note: "" },
{ id: "home", src: "/static/assets/modules/family/f08/f08-ancestral-home.png", alt: "祖居院落的复古旧照", note: "" },
{ id: "ancestor", src: "/static/assets/modules/family/f08/f08-ancestral-portrait.png", alt: "老一辈家人在祖居门前的合影", note: "" },
];
const selectedPhotos = ref([]);
const albumId = ref("");
const activePhoto = computed(
() => selectedPhotos.value[activePhotoIndex.value] || null,
);
const isLocked = computed(() => uploadState.value === "uploading");
const isLocked = computed(() => isSubmitting.value);
const isDirty = computed(() =>
Boolean(
selectedPhotos.value.length ||
batchDescription.value.trim() ||
selectedPhotos.value.some((photo) => photo.note.trim()),
),
);
const discardConfirmation = createDiscardConfirmation((visible) => {
discardVisible.value = visible;
});
const requestDiscardConfirmation = discardConfirmation.request;
const confirmDiscard = discardConfirmation.confirm;
const cancelDiscard = discardConfirmation.cancel;
const makeSelectedPhotos = (state) =>
mockLibrary.slice(0, 4).map((photo, index) => ({
...photo,
note: "",
progress: state === "uploading" ? [100, 62, 35, 0][index] : 0,
status:
state === "error"
? index < 2
? "error"
: "uploaded"
: state,
}));
const makeSelectedPhotos = () =>
mockLibrary.slice(0, 4).map((photo) => ({ ...photo, note: "" }));
const selectMockPhotos = () => {
selectedPhotos.value = makeSelectedPhotos("selected");
selectedPhotos.value = makeSelectedPhotos();
activePhotoIndex.value = 0;
validationMessage.value = "";
uploadState.value = "selected";
@@ -286,8 +270,6 @@ const addMockPhoto = () => {
...next,
id: `${next.id}-${nextIndex + 1}`,
note: "",
progress: 0,
status: "selected",
});
};
@@ -297,7 +279,8 @@ const updateActiveNote = (event) => {
}
};
const startUpload = () => {
const generatePreview = () => {
if (isSubmitting.value || !album.value) return;
if (!selectedPhotos.value.length) {
selectMockPhotos();
return;
@@ -307,40 +290,59 @@ const startUpload = () => {
return;
}
validationMessage.value = "";
selectedPhotos.value = selectedPhotos.value.map((photo, index) => ({
...photo,
progress: [100, 62, 35, 0][index] || 0,
status: "uploading",
}));
uploadState.value = "uploading";
isSubmitting.value = true;
const selectedCount = selectedPhotos.value.length;
const timer = setTimeout(() => {
if (previewTimer !== timer) return;
previewTimer = null;
isSubmitting.value = false;
uploadState.value = selectedCount > 0 ? "preview" : "selected";
}, 280);
previewTimer = timer;
};
const retryFailed = () => {
selectedPhotos.value = selectedPhotos.value.map((photo, index) => ({
...photo,
progress: [100, 62, 35, 0][index] || 0,
status: "uploading",
}));
uploadState.value = "uploading";
};
const returnToAlbum = () => {
uni.redirectTo({
url: `/pages/family/f08-album-detail?albumId=${albumId.value}`,
const returnToAlbum = () =>
returnTo("F08", {
genealogyId: genealogyId.value,
albumId: albumId.value,
});
const returnFromInvalid = () =>
genealogyId.value
? returnTo("F07", { genealogyId: genealogyId.value })
: goBack();
const requestBack = () =>
runBackGuard({
transientOpen: discardVisible.value,
dirty: isDirty.value,
submitting: isSubmitting.value,
"close-transient": cancelDiscard,
"block-submitting": () => true,
"confirm-discard": requestDiscardConfirmation,
});
};
onLoad((query) => {
albumId.value = query.albumId || "reunion";
uploadState.value = ["permission", "selected", "uploading", "error", "success"].includes(query.state)
genealogyId.value = String(query.genealogyId || "");
albumId.value = String(query.albumId || "");
album.value = findFamilyAlbumFixture(genealogyId.value, albumId.value);
if (!album.value) {
uploadState.value = "invalid";
return;
}
uploadState.value = ["permission", "selected", "preview"].includes(query.state)
? query.state
: "initial";
if (!["initial", "permission"].includes(uploadState.value)) {
selectedPhotos.value = makeSelectedPhotos(uploadState.value);
if (["selected", "preview"].includes(uploadState.value)) {
selectedPhotos.value = makeSelectedPhotos();
batchDescription.value = "春节团圆照片整理";
}
});
onBackPress((event) => handleBackPress(event, requestBack));
onUnload(() => {
if (previewTimer) clearTimeout(previewTimer);
discardConfirmation.dispose();
});
</script>
<style scoped lang="scss">
@@ -379,9 +381,7 @@ onLoad((query) => {
.media-state-card__eyebrow,
.media-state-card__title,
.media-state-card__copy,
.media-success-title,
.media-photo-status,
.media-progress-copy {
.media-preview-title {
display: block;
}
.media-album-card__eyebrow {
@@ -437,17 +437,12 @@ onLoad((query) => {
outline: 4rpx solid rgba(159, 44, 35, 0.78);
outline-offset: -4rpx;
}
.media-photo-tile--failed {
border-style: dashed;
border-color: #9f2c23;
}
.media-photo-tile__image {
width: 100%;
height: 100%;
}
.media-photo-order,
.media-photo-current,
.media-photo-status {
.media-photo-current {
position: absolute;
z-index: 2;
color: #fffaf0;
@@ -485,21 +480,6 @@ onLoad((query) => {
padding: 7rpx 8rpx;
background: rgba(145, 36, 29, 0.88);
}
.media-photo-status {
right: 0;
bottom: 0;
left: 0;
padding: 7rpx 8rpx;
background: rgba(58, 43, 32, 0.82);
text-align: center;
}
.media-photo-status--error {
background: rgba(132, 35, 29, 0.92);
font-weight: 700;
}
.media-photo-status--uploaded {
background: rgba(55, 82, 51, 0.88);
}
.media-add-tile {
display: flex;
min-height: 44px;
@@ -591,19 +571,12 @@ onLoad((query) => {
font-size: 21rpx;
line-height: 1.45;
}
.media-primary-action,
.media-retry-action,
.media-progress-action {
.media-primary-action {
margin-top: 24rpx;
}
.media-progress-copy {
margin-top: 22rpx;
color: $ink;
font-size: 23rpx;
text-align: center;
}
.media-permission-card,
.media-success-card {
.media-preview-card,
.media-invalid-card {
@include adaptive.adaptive-family-panel;
min-height: 410rpx;
margin-top: 24rpx;
@@ -617,7 +590,7 @@ onLoad((query) => {
letter-spacing: 2rpx;
}
.media-state-card__title,
.media-success-title {
.media-preview-title {
margin-top: 12rpx;
color: $ink;
font-family: STKaiti, KaiTi, serif;
@@ -631,7 +604,7 @@ onLoad((query) => {
line-height: 1.65;
}
.media-permission-action,
.media-success-action {
.media-preview-action {
margin-top: 30rpx;
}
@media (max-width: 340px) {