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-open-native
play-btn-position="center"
object-fit="cover"
:object-fit="videoObjectFit"
@play="handleVideoPlay"
@waiting="handleVideoWaiting"
@timeupdate="handleVideoTimeUpdate"
@progress="handleVideoProgress"
@loadedmetadata="handleVideoLoaded"
@fullscreenchange="handleVideoFullscreenChange"
@ended="handleVideoEnded"
@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-caption">
@@ -76,10 +83,12 @@ const videoLoadVersion = ref(0);
const isVideoLoading = ref(true);
const isVideoEnded = ref(false);
const isVideoError = ref(false);
const isVideoFullscreen = ref(false);
const hasVideoStarted = ref(false);
const bufferedProgress = ref(0);
const isVideo = computed(() => props.result.mediaType === "video");
const videoObjectFit = computed(() => (isVideoFullscreen.value ? "contain" : "cover"));
const videoRenderKey = computed(() => `${props.result.videoResultUrl || "video"}-${videoLoadVersion.value}`);
const showCaptionOverlay = computed(
() => !isVideo.value || (!hasVideoStarted.value && !isVideoEnded.value && !isVideoError.value)
@@ -90,6 +99,16 @@ const loadingText = computed(() =>
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 = () => {
isVideoLoading.value = isVideo.value;
isVideoEnded.value = false;
@@ -129,6 +148,10 @@ const handleVideoProgress = (event) => {
}
};
const handleVideoFullscreenChange = (event) => {
isVideoFullscreen.value = Boolean(event?.detail?.fullScreen);
};
const handleVideoEnded = () => {
isVideoLoading.value = false;
isVideoEnded.value = true;

View File

@@ -1,7 +1,9 @@
.result-preview {
position: relative;
height: 360px;
width: 100%;
height: 100%;
overflow: hidden;
box-sizing: border-box;
border-radius: 26px;
background: var(--result-accent, #0a4d46);
color: #ffffff;

View File

@@ -4,15 +4,17 @@
<view class="aigc-detail-content">
<template v-if="taskDetail">
<ResultPreview v-if="result.cover" :result="result" />
<ResultMeta :items="metaItems" />
<ResultActions :saving="isSaving" :media-type="result.mediaType" :share-ready="Boolean(shareKey)"
:share-preparing="isPreparingShare" @save="handleSave" @prepare-share="handlePrepareShare" />
<ResultPreview v-if="result.cover" class="aigc-detail-preview" :result="result" />
<view class="aigc-detail-controls">
<ResultMeta :items="metaItems" />
<ResultActions :saving="isSaving" :media-type="result.mediaType" :share-ready="Boolean(shareKey)"
:share-preparing="isPreparingShare" @save="handleSave" @prepare-share="handlePrepareShare" />
<view class="aigc-detail-text-actions">
<view class="aigc-detail-text-action" @tap="handleRegenerate">再生成一版</view>
<view class="aigc-detail-text-action" @tap="handleChangeTemplate">换个模板</view>
<view class="aigc-detail-text-action" @tap="handleOpenRecords">历史记录</view>
<view class="aigc-detail-text-actions">
<view class="aigc-detail-text-action" @tap="handleRegenerate">再生成一版</view>
<view class="aigc-detail-text-action" @tap="handleChangeTemplate">换个模板</view>
<view class="aigc-detail-text-action" @tap="handleOpenRecords">历史记录</view>
</view>
</view>
</template>
</view>

View File

@@ -19,11 +19,25 @@
.aigc-detail-content {
flex: 1 1 auto;
min-height: 0;
display: flex;
flex-direction: column;
overflow: hidden;
padding: 0 12px;
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 {
height: 50px;
display: grid;

View File

@@ -1,5 +1,5 @@
<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-message">{{ message }}</text>
<view class="gift-points-close" @tap="handleClose">
@@ -9,11 +9,17 @@
</template>
<script setup>
defineProps({
import { onBeforeUnmount, onMounted, ref, watch } from "vue";
const props = defineProps({
visible: {
type: Boolean,
default: true,
},
delay: {
type: Number,
default: 3000,
},
title: {
type: String,
default: "400积分已到账",
@@ -25,11 +31,50 @@ defineProps({
});
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 = () => {
clearShowTimer();
displayVisible.value = false;
emit("update:visible", false);
emit("close");
};
watch(
() => props.visible,
(visible) => {
syncVisible(visible);
}
);
onMounted(() => {
mounted.value = true;
syncVisible(props.visible);
});
onBeforeUnmount(() => {
clearShowTimer();
});
</script>
<style scoped lang="scss">