完成50%
This commit is contained in:
+131
-158
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user