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-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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user