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:
duanshuwen
2026-07-23 21:50:09 +08:00
parent 34711330bd
commit f21bd70b08
5 changed files with 99 additions and 13 deletions

View File

@@ -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;

View File

@@ -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;

View File

@@ -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>

View File

@@ -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;

View File

@@ -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">