first commit

This commit is contained in:
2026-02-13 17:36:42 +08:00
commit f067e1bb78
155 changed files with 46676 additions and 0 deletions

View File

@@ -0,0 +1,427 @@
<template>
<view v-if="visible" class="preview-overlay" @click="handleOverlayClick">
<view class="preview-container">
<!-- 返回按钮与微信胶囊对齐 -->
<view
class="back-btn"
:style="{ top: navButtonTop + 'px' }"
@click.stop="handleClose"
>
<image src="/static/icons/Left (左).png" class="back-icon" mode="aspectFit" />
</view>
<!-- 媒体内容 -->
<view class="media-wrapper" @click.stop>
<!-- 视频预览 -->
<video
v-if="currentMedia.isVideo"
:src="currentMedia.url"
class="preview-video"
controls
autoplay
object-fit="contain"
/>
<!-- 图片预览 -->
<swiper
v-else
class="preview-swiper"
:current="currentIndex"
@change="handleSwiperChange"
>
<swiper-item v-for="(item, index) in mediaList" :key="index">
<view class="swiper-item-wrapper">
<image
:src="item.url"
class="preview-image"
mode="aspectFit"
@click.stop
/>
</view>
</swiper-item>
</swiper>
</view>
<!-- 底部信息栏 -->
<view class="info-bar" @click.stop>
<view class="info-left">
<text class="model-name">{{ currentMedia.modelName }}</text>
<text class="create-time">{{ currentMedia.createTime }}</text>
</view>
<!-- 图片计数 -->
<view class="page-indicator" v-if="!currentMedia.isVideo && mediaList.length > 1">
<text class="indicator-text">{{ currentIndex + 1 }} / {{ mediaList.length }}</text>
</view>
<!-- 操作按钮 -->
<view class="action-buttons">
<view class="icon-btn" @click.stop="handleDownload">
<image src="/static/icons/To-bottom.png" class="btn-icon" mode="aspectFit" />
</view>
<view class="icon-btn" @click.stop="handleDelete">
<image src="/static/icons/del.png" class="btn-icon" mode="aspectFit" />
</view>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref, computed, watch, onMounted } from 'vue'
// 计算返回按钮的top位置与微信胶囊对齐
const navButtonTop = ref(20)
onMounted(() => {
// #ifdef MP-WEIXIN
try {
// 获取微信胶囊按钮位置信息
const menuButtonInfo = uni.getMenuButtonBoundingClientRect()
if (menuButtonInfo) {
// 返回按钮与胶囊按钮垂直居中对齐
// 胶囊top + (胶囊高度 - 按钮高度) / 2
navButtonTop.value = menuButtonInfo.top + (menuButtonInfo.height - 40) / 2
}
} catch (e) {
// 获取失败时使用默认值
const systemInfo = uni.getSystemInfoSync()
navButtonTop.value = (systemInfo.statusBarHeight || 20) + 6
}
// #endif
// #ifndef MP-WEIXIN
// 非微信小程序使用状态栏高度
try {
const systemInfo = uni.getSystemInfoSync()
navButtonTop.value = (systemInfo.statusBarHeight || 20) + 10
} catch (e) {
navButtonTop.value = 30
}
// #endif
})
const props = defineProps({
visible: {
type: Boolean,
default: false
},
task: {
type: Object,
default: null
},
taskList: {
type: Array,
default: () => []
}
})
const emit = defineEmits(['close', 'download', 'delete'])
const currentIndex = ref(0)
// 当前媒体信息
const currentMedia = computed(() => {
if (!props.task) return {}
const isVideoTask = isVideo(props.task)
// 视频任务使用视频URL图片任务使用图片URL
const url = isVideoTask ? getVideoUrl(props.task) : getTaskImage(props.task)
return {
url: url,
isVideo: isVideoTask,
modelName: props.task.modelName || '未知模型',
createTime: formatTime(props.task.createdAt),
taskId: props.task.id
}
})
// 媒体列表(仅图片)
const mediaList = computed(() => {
if (!props.task || isVideo(props.task)) return []
// 获取同一日期的所有图片任务
const currentDate = formatDate(props.task.createdAt)
return props.taskList
.filter(t => {
const taskDate = formatDate(t.createdAt)
return taskDate === currentDate && !isVideo(t) && getTaskImage(t)
})
.map(t => ({
url: getTaskImage(t),
taskId: t.id,
modelName: t.modelName,
createTime: formatTime(t.createdAt)
}))
})
// 监听任务变化,更新当前索引
watch(() => props.task, (newTask) => {
if (newTask && !isVideo(newTask)) {
const index = mediaList.value.findIndex(item => item.taskId === newTask.id)
currentIndex.value = index >= 0 ? index : 0
}
}, { immediate: true })
// 格式化日期
const formatDate = (dateStr) => {
if (!dateStr) return ''
const date = new Date(dateStr)
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
return `${year}-${month}-${day}`
}
// 格式化时间
const formatTime = (dateStr) => {
if (!dateStr) return ''
const date = new Date(dateStr)
const month = date.getMonth() + 1
const day = date.getDate()
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
return `${month}${day}${hours}:${minutes}`
}
// 视频模型编码列表
const videoModelCodes = ['grok-video', 'tencent-sora2-video', 'tencent-aigc-video', 'sora2-video']
// 判断是否为视频
const isVideo = (task) => {
// 优先通过模型编码判断
if (task.modelCode && videoModelCodes.includes(task.modelCode)) {
return true
}
if (!task.outputResult) return false
try {
const result = JSON.parse(task.outputResult)
if (result.result && (result.result.endsWith('.mp4') || result.result.includes('.mp4') || result.result.includes('video'))) {
return true
}
return !!result.videoUrl
} catch (e) {
return false
}
}
// 获取视频URL与图片分开
const getVideoUrl = (task) => {
if (!task.outputResult) return null
try {
const result = JSON.parse(task.outputResult)
return result.result || result.videoUrl || result.url || null
} catch (e) {
return null
}
}
// 获取任务图片/视频
const getTaskImage = (task) => {
if (!task.outputResult) return null
try {
const result = JSON.parse(task.outputResult)
if (result.coverUrl) return result.coverUrl
if (result.result) return result.result
if (result.videoUrl) return result.videoUrl
if (result.imageUrl) return result.imageUrl
if (result.images && result.images.length > 0) return result.images[0]
if (result.url) return result.url
if (result.image) return result.image
} catch (e) {
console.error('解析任务结果失败:', e)
}
return null
}
// 轮播图切换
const handleSwiperChange = (e) => {
currentIndex.value = e.detail.current
}
// 关闭预览
const handleClose = () => {
emit('close')
}
// 点击遮罩关闭
const handleOverlayClick = () => {
handleClose()
}
// 下载
const handleDownload = () => {
const media = currentMedia.value
if (!currentMedia.value.isVideo && mediaList.value.length > 0) {
// 图片模式,下载当前图片
const currentItem = mediaList.value[currentIndex.value]
emit('download', currentItem.taskId)
} else {
// 视频模式
emit('download', media.taskId)
}
}
// 删除
const handleDelete = () => {
const media = currentMedia.value
if (!currentMedia.value.isVideo && mediaList.value.length > 0) {
// 图片模式,删除当前图片
const currentItem = mediaList.value[currentIndex.value]
emit('delete', currentItem.taskId)
} else {
// 视频模式
emit('delete', media.taskId)
}
}
</script>
<style scoped>
.preview-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.95);
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
}
.preview-container {
width: 100%;
height: 100%;
position: relative;
display: flex;
flex-direction: column;
}
/* 返回按钮top通过:style动态设置与微信胶囊对齐 */
.back-btn {
position: absolute;
top: 20px; /* 默认值,实际由:style覆盖 */
left: 16px;
width: 40px;
height: 40px;
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
z-index: 10;
}
.back-icon {
width: 20px;
height: 20px;
}
/* 媒体容器 */
.media-wrapper {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
/* 视频预览 */
.preview-video {
width: 100%;
height: 100%;
}
/* 图片轮播 */
.preview-swiper {
width: 100%;
height: 100%;
}
.swiper-item-wrapper {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.preview-image {
width: 100%;
height: 100%;
}
/* 底部信息栏 */
.info-bar {
position: absolute;
bottom: 0;
left: 0;
right: 0;
padding: 20px;
background: linear-gradient(to top, rgba(0, 0, 0, 0.8), transparent);
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.info-left {
flex: 1;
display: flex;
flex-direction: column;
gap: 4px;
}
.model-name {
color: #ffffff;
font-size: 15px;
font-weight: 500;
}
.create-time {
color: #a1a1aa;
font-size: 13px;
}
/* 页码指示器 */
.page-indicator {
padding: 6px 12px;
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border-radius: 16px;
}
.indicator-text {
color: #ffffff;
font-size: 13px;
}
/* 操作按钮 */
.action-buttons {
display: flex;
gap: 12px;
}
.icon-btn {
width: 40px;
height: 40px;
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.btn-icon {
width: 20px;
height: 20px;
}
</style>