feat(aigc): add image preview and improve detail layout
- restructure aigc detail page flex layout to separate preview and control sections - update ResultPreview styles to fill full width and height - add click-to-preview functionality for result images - add auto-hide delay and fix state management for GiftPointsToast component - adjust video component handling for fullscreen display mode
This commit is contained in:
@@ -18,16 +18,23 @@
|
|||||||
auto-pause-if-navigate
|
auto-pause-if-navigate
|
||||||
auto-pause-if-open-native
|
auto-pause-if-open-native
|
||||||
play-btn-position="center"
|
play-btn-position="center"
|
||||||
object-fit="cover"
|
:object-fit="videoObjectFit"
|
||||||
@play="handleVideoPlay"
|
@play="handleVideoPlay"
|
||||||
@waiting="handleVideoWaiting"
|
@waiting="handleVideoWaiting"
|
||||||
@timeupdate="handleVideoTimeUpdate"
|
@timeupdate="handleVideoTimeUpdate"
|
||||||
@progress="handleVideoProgress"
|
@progress="handleVideoProgress"
|
||||||
@loadedmetadata="handleVideoLoaded"
|
@loadedmetadata="handleVideoLoaded"
|
||||||
|
@fullscreenchange="handleVideoFullscreenChange"
|
||||||
@ended="handleVideoEnded"
|
@ended="handleVideoEnded"
|
||||||
@error="handleVideoError"
|
@error="handleVideoError"
|
||||||
/>
|
/>
|
||||||
<image v-else class="result-preview-image" :src="result.imageResultUrl" mode="aspectFill" />
|
<image
|
||||||
|
v-else
|
||||||
|
class="result-preview-image"
|
||||||
|
:src="result.imageResultUrl"
|
||||||
|
mode="aspectFill"
|
||||||
|
@tap="handlePreviewImage"
|
||||||
|
/>
|
||||||
<view v-if="showCaptionOverlay" class="result-preview-shade" />
|
<view v-if="showCaptionOverlay" class="result-preview-shade" />
|
||||||
|
|
||||||
<view v-if="showCaptionOverlay" class="result-preview-caption">
|
<view v-if="showCaptionOverlay" class="result-preview-caption">
|
||||||
@@ -76,10 +83,12 @@ const videoLoadVersion = ref(0);
|
|||||||
const isVideoLoading = ref(true);
|
const isVideoLoading = ref(true);
|
||||||
const isVideoEnded = ref(false);
|
const isVideoEnded = ref(false);
|
||||||
const isVideoError = ref(false);
|
const isVideoError = ref(false);
|
||||||
|
const isVideoFullscreen = ref(false);
|
||||||
const hasVideoStarted = ref(false);
|
const hasVideoStarted = ref(false);
|
||||||
const bufferedProgress = ref(0);
|
const bufferedProgress = ref(0);
|
||||||
|
|
||||||
const isVideo = computed(() => props.result.mediaType === "video");
|
const isVideo = computed(() => props.result.mediaType === "video");
|
||||||
|
const videoObjectFit = computed(() => (isVideoFullscreen.value ? "contain" : "cover"));
|
||||||
const videoRenderKey = computed(() => `${props.result.videoResultUrl || "video"}-${videoLoadVersion.value}`);
|
const videoRenderKey = computed(() => `${props.result.videoResultUrl || "video"}-${videoLoadVersion.value}`);
|
||||||
const showCaptionOverlay = computed(
|
const showCaptionOverlay = computed(
|
||||||
() => !isVideo.value || (!hasVideoStarted.value && !isVideoEnded.value && !isVideoError.value)
|
() => !isVideo.value || (!hasVideoStarted.value && !isVideoEnded.value && !isVideoError.value)
|
||||||
@@ -90,6 +99,16 @@ const loadingText = computed(() =>
|
|||||||
|
|
||||||
const getVideoContext = () => uni.createVideoContext(videoId, instance?.proxy || instance);
|
const getVideoContext = () => uni.createVideoContext(videoId, instance?.proxy || instance);
|
||||||
|
|
||||||
|
const handlePreviewImage = () => {
|
||||||
|
const imageUrl = String(props.result.imageResultUrl || "").trim();
|
||||||
|
if (isVideo.value || !imageUrl) return;
|
||||||
|
|
||||||
|
uni.previewImage({
|
||||||
|
current: imageUrl,
|
||||||
|
urls: [imageUrl],
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const resetVideoState = () => {
|
const resetVideoState = () => {
|
||||||
isVideoLoading.value = isVideo.value;
|
isVideoLoading.value = isVideo.value;
|
||||||
isVideoEnded.value = false;
|
isVideoEnded.value = false;
|
||||||
@@ -129,6 +148,10 @@ const handleVideoProgress = (event) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleVideoFullscreenChange = (event) => {
|
||||||
|
isVideoFullscreen.value = Boolean(event?.detail?.fullScreen);
|
||||||
|
};
|
||||||
|
|
||||||
const handleVideoEnded = () => {
|
const handleVideoEnded = () => {
|
||||||
isVideoLoading.value = false;
|
isVideoLoading.value = false;
|
||||||
isVideoEnded.value = true;
|
isVideoEnded.value = true;
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
.result-preview {
|
.result-preview {
|
||||||
position: relative;
|
position: relative;
|
||||||
height: 360px;
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
box-sizing: border-box;
|
||||||
border-radius: 26px;
|
border-radius: 26px;
|
||||||
background: var(--result-accent, #0a4d46);
|
background: var(--result-accent, #0a4d46);
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
|
|||||||
@@ -4,7 +4,8 @@
|
|||||||
|
|
||||||
<view class="aigc-detail-content">
|
<view class="aigc-detail-content">
|
||||||
<template v-if="taskDetail">
|
<template v-if="taskDetail">
|
||||||
<ResultPreview v-if="result.cover" :result="result" />
|
<ResultPreview v-if="result.cover" class="aigc-detail-preview" :result="result" />
|
||||||
|
<view class="aigc-detail-controls">
|
||||||
<ResultMeta :items="metaItems" />
|
<ResultMeta :items="metaItems" />
|
||||||
<ResultActions :saving="isSaving" :media-type="result.mediaType" :share-ready="Boolean(shareKey)"
|
<ResultActions :saving="isSaving" :media-type="result.mediaType" :share-ready="Boolean(shareKey)"
|
||||||
:share-preparing="isPreparingShare" @save="handleSave" @prepare-share="handlePrepareShare" />
|
:share-preparing="isPreparingShare" @save="handleSave" @prepare-share="handlePrepareShare" />
|
||||||
@@ -14,6 +15,7 @@
|
|||||||
<view class="aigc-detail-text-action" @tap="handleChangeTemplate">换个模板</view>
|
<view class="aigc-detail-text-action" @tap="handleChangeTemplate">换个模板</view>
|
||||||
<view class="aigc-detail-text-action" @tap="handleOpenRecords">历史记录</view>
|
<view class="aigc-detail-text-action" @tap="handleOpenRecords">历史记录</view>
|
||||||
</view>
|
</view>
|
||||||
|
</view>
|
||||||
</template>
|
</template>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
|||||||
@@ -19,11 +19,25 @@
|
|||||||
.aigc-detail-content {
|
.aigc-detail-content {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
padding: 0 12px;
|
padding: 0 12px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.aigc-detail-preview {
|
||||||
|
display: block;
|
||||||
|
flex: 1 1 500px;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 360px;
|
||||||
|
max-height: 500px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aigc-detail-controls {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
.aigc-detail-text-actions {
|
.aigc-detail-text-actions {
|
||||||
height: 50px;
|
height: 50px;
|
||||||
display: grid;
|
display: grid;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<view v-if="visible" class="gift-points-toast" role="status">
|
<view v-if="displayVisible" class="gift-points-toast" role="status">
|
||||||
<text class="gift-points-badge">{{ title }}</text>
|
<text class="gift-points-badge">{{ title }}</text>
|
||||||
<text class="gift-points-message">{{ message }}</text>
|
<text class="gift-points-message">{{ message }}</text>
|
||||||
<view class="gift-points-close" @tap="handleClose">
|
<view class="gift-points-close" @tap="handleClose">
|
||||||
@@ -9,11 +9,17 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
defineProps({
|
import { onBeforeUnmount, onMounted, ref, watch } from "vue";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
visible: {
|
visible: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
|
delay: {
|
||||||
|
type: Number,
|
||||||
|
default: 3000,
|
||||||
|
},
|
||||||
title: {
|
title: {
|
||||||
type: String,
|
type: String,
|
||||||
default: "400积分已到账",
|
default: "400积分已到账",
|
||||||
@@ -25,11 +31,50 @@ defineProps({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits(["close", "update:visible"]);
|
const emit = defineEmits(["close", "update:visible"]);
|
||||||
|
const displayVisible = ref(false);
|
||||||
|
const mounted = ref(false);
|
||||||
|
let showTimer = null;
|
||||||
|
|
||||||
|
const clearShowTimer = () => {
|
||||||
|
if (showTimer) {
|
||||||
|
clearTimeout(showTimer);
|
||||||
|
showTimer = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const syncVisible = (visible) => {
|
||||||
|
clearShowTimer();
|
||||||
|
displayVisible.value = false;
|
||||||
|
if (!visible || !mounted.value) return;
|
||||||
|
|
||||||
|
showTimer = setTimeout(() => {
|
||||||
|
showTimer = null;
|
||||||
|
displayVisible.value = props.visible;
|
||||||
|
}, Math.max(0, props.delay));
|
||||||
|
};
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
|
clearShowTimer();
|
||||||
|
displayVisible.value = false;
|
||||||
emit("update:visible", false);
|
emit("update:visible", false);
|
||||||
emit("close");
|
emit("close");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.visible,
|
||||||
|
(visible) => {
|
||||||
|
syncVisible(visible);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
mounted.value = true;
|
||||||
|
syncVisible(props.visible);
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
clearShowTimer();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
|
|||||||
Reference in New Issue
Block a user