599 lines
15 KiB
Vue
599 lines
15 KiB
Vue
<template>
|
|
<div style="width: 100%;display: flex;justify-content: center;align-items: center;transform: rotate(180deg);">
|
|
<div class="primary-box" :class="{ 'none-primary-box': props.item.status === 'none' }">
|
|
|
|
<div class="prompt-container" ref="promptContainerRef">
|
|
<div class="prompt-wrapper" ref="promptWrapperRef">
|
|
<div class="prompt" ref="promptRef" :class="{ 'expanded': isHovering }" @mouseenter="isHovering = true" @mouseleave="isHovering = false">
|
|
<span class="prompt-text">
|
|
{{ props.item.generateData.prompt || '生成图片' }}
|
|
<i-ep-DocumentCopy class="Copy" @click.stop="copyPrompt"/>
|
|
</span>
|
|
<div class="generate-data internal" v-show="!isHovering && !showExternalGenerateData">
|
|
<div class="detailed-data first-detailed-data">{{ props.item.generateData.model }}</div>
|
|
<div class="detailed-data">{{ props.item.generateData.proportion }}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="generate-data external" v-show="!isHovering && showExternalGenerateData">
|
|
<div class="detailed-data first-detailed-data">{{ props.item.generateData.model }}</div>
|
|
<div class="detailed-data">{{ props.item.generateData.proportion }}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 加载中 -->
|
|
<div v-if="props.item.status === 'none'" class="box none-box">
|
|
<!-- <img :src="primaryPicture" alt="无" class="img" /> -->
|
|
</div>
|
|
|
|
<!-- 生成失败 -->
|
|
<div v-if="props.item.status === 'error'" class="box none-box">
|
|
<img :src="primaryPicture" alt="无" class="img" />
|
|
<!-- <span>生成失败</span> -->
|
|
</div>
|
|
|
|
<!-- 生成中 -->
|
|
<div v-if="props.item.status === 'generate'" class="box generate-box">
|
|
<div class="generate-content">
|
|
<!-- 欢快的加载动画 -->
|
|
<div class="loading-animation">
|
|
<div class="bounce-dot"></div>
|
|
<div class="bounce-dot"></div>
|
|
<div class="bounce-dot"></div>
|
|
</div>
|
|
|
|
<!-- 状态文本 -->
|
|
<div class="status-text">{{ generateStatusText }}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 已完成 图片 -->
|
|
<div v-if="props.item.status === 'success' && props.item.type === 'Painting'" class="box success-box">
|
|
<div v-for="(file, index) in props.item.files" :key="index" class="one-box" :class="{ 'collected': isCollected(file) }" @mouseenter="hoverIndex = index" @mouseleave="hoverIndex = -1">
|
|
<!-- <img :src="file" alt="index" class="img" /> -->
|
|
<Img :src="file" alt="index" class="img" />
|
|
|
|
<div class="left-top">
|
|
<div v-show="hoverIndex === index" class="left-top-btn download-btn" @click="downloadImage(file, 'image')"><img src="@/assets/display/download.svg" /></div>
|
|
<span v-if="hoverIndex === index" class="line" />
|
|
<div class="left-top-btn collect-btn" @click="addCollection(file)"><img :src="isCollected(file) ? collectionActiveIcon : collectionIcon" /></div>
|
|
</div>
|
|
|
|
<el-tooltip
|
|
effect="dark"
|
|
content="画笔"
|
|
placement="top"
|
|
:hide-after="0"
|
|
>
|
|
<div @click.stop="AIbrush(file, index)" class="bottom-brush">
|
|
<img :src="brush" />
|
|
</div>
|
|
</el-tooltip>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 已完成 视频 -->
|
|
<div v-if="props.item.status === 'success' && props.item.type === 'Video'" class="box success-box">
|
|
<div class="one-box" :class="{ 'collected': isCollected(props.item.files[0]) }" @mouseenter="hoverIndex = 0" @mouseleave="hoverIndex = -1">
|
|
<!-- <img :src="file" alt="index" class="img" /> -->
|
|
<video :src="props.item.files[0]" class="video" controls playsinline />
|
|
|
|
<div class="left-top">
|
|
<div v-show="hoverIndex === 0" class="left-top-btn download-btn" @click="downloadImage(props.item.files[0], 'video')"><img src="@/assets/display/download.svg" /></div>
|
|
<span v-if="hoverIndex === 0" class="line" />
|
|
<div class="left-top-btn collect-btn" @click="addCollection(props.item.files[0])"><img :src="isCollected(props.item.files[0]) ? collectionActiveIcon : collectionIcon" /></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="props.item.status === 'success'" class="bottom-btn-group" style="margin-top: 8px;">
|
|
<div v-for="(item, index) in bottomBtnGroup" :key="index" class="bottom-btn" @click="item.click()">
|
|
<img :src="item.icon" />
|
|
<span>{{ item.name }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import brush from '@/assets/display/brush.svg'
|
|
import collectionIcon from '@/assets/display/collection.svg'
|
|
import collectionActiveIcon from '@/assets/display/collection-active.svg'
|
|
import { useDisplayStore, useParamStore, useUserStore } from '@/stores'
|
|
import { downloadImage } from '@/utils/downloadImage.js'
|
|
import reEditIcon from '@/assets/display/reEdit.svg'
|
|
import againGenerateIcon from '@/assets/display/againGenerate.svg'
|
|
import deleteImageIcon from '@/assets/display/deleteImage.svg'
|
|
import Img from '@/components/Img/index.vue'
|
|
import { cancelOrCollect, deleteGenerateHistory } from '@/apis/display'
|
|
|
|
const props = defineProps({
|
|
item: {
|
|
type: Object,
|
|
default: () => ({})
|
|
}
|
|
})
|
|
const emit = defineEmits(['open-canvas', 'delete-success'])
|
|
|
|
const useDisplay = useDisplayStore()
|
|
const useParams = useParamStore()
|
|
const useUser = useUserStore()
|
|
|
|
const localCollectStatus = ref({ ...props.item.collectStatus })
|
|
const hoverIndex = ref(-1)
|
|
const isHovering = ref(false)
|
|
const showExternalGenerateData = ref(false)
|
|
const promptContainerRef = ref(null)
|
|
const promptWrapperRef = ref(null)
|
|
const promptRef = ref(null)
|
|
|
|
const checkTextOverflow = () => {
|
|
nextTick(() => {
|
|
if (promptRef.value && promptWrapperRef.value && promptContainerRef.value) {
|
|
const lineHeight = 22.5
|
|
const padding = 8
|
|
const twoLineHeight = lineHeight * 2 + padding
|
|
|
|
promptRef.value.style.maxHeight = 'none'
|
|
promptRef.value.style.overflow = 'hidden'
|
|
|
|
const actualHeight = promptRef.value.scrollHeight
|
|
const lineCount = Math.ceil((actualHeight - padding) / lineHeight)
|
|
|
|
if (!isHovering.value) {
|
|
promptRef.value.style.maxHeight = `${twoLineHeight}px`
|
|
promptRef.value.style.overflow = 'hidden'
|
|
}
|
|
|
|
showExternalGenerateData.value = lineCount >= 3
|
|
|
|
if (!isHovering.value) {
|
|
if(lineCount >= 3){
|
|
promptContainerRef.value.style.height = `${twoLineHeight}px`
|
|
} else {
|
|
promptContainerRef.value.style.height = `${actualHeight}px`
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
watch(isHovering, (newVal) => {
|
|
if (promptRef.value) {
|
|
const lineHeight = 22.5
|
|
const padding = 8
|
|
const twoLineHeight = lineHeight * 2 + padding
|
|
|
|
if (newVal) {
|
|
promptRef.value.style.maxHeight = 'none'
|
|
promptRef.value.style.overflow = 'visible'
|
|
} else {
|
|
promptRef.value.style.maxHeight = `${twoLineHeight}px`
|
|
promptRef.value.style.overflow = 'hidden'
|
|
}
|
|
}
|
|
})
|
|
|
|
watch(() => props.item.generateData.prompt, () => {
|
|
checkTextOverflow()
|
|
}, { immediate: true })
|
|
|
|
onMounted(() => {
|
|
checkTextOverflow()
|
|
window.addEventListener('resize', checkTextOverflow)
|
|
})
|
|
|
|
onUnmounted(() => {
|
|
window.removeEventListener('resize', checkTextOverflow)
|
|
})
|
|
|
|
const isCollected = (url) => {
|
|
return localCollectStatus.value[url] === true
|
|
}
|
|
|
|
const generateStatusText = computed(() => {
|
|
if (props.item.status === 'generate') {
|
|
return '正在生成中...'
|
|
}
|
|
return ''
|
|
})
|
|
|
|
const AIbrush = (file, index) => {
|
|
emit('open-canvas', {
|
|
mainImage: { url: file, index: index + 1 },
|
|
referenceImages: [],
|
|
source: 'set'
|
|
})
|
|
}
|
|
|
|
const reEdit = () => {
|
|
if(props.item.generateData?.modelType === 'edit'){
|
|
ElMessage.error('画笔生成的任务不能重新编辑')
|
|
return
|
|
}
|
|
useDisplay.setResultData(props.item.generateData)
|
|
useDisplay.fillParamsForEdit()
|
|
}
|
|
|
|
const againGenerate = () => {
|
|
if(props.item.generateData?.modelType === 'edit'){
|
|
ElMessage.error('画笔生成的任务不能再次生成')
|
|
return
|
|
}
|
|
useDisplay.setResultData(props.item.generateData)
|
|
useDisplay.triggerGenerateWithResult()
|
|
}
|
|
|
|
const deleteImage = () => {
|
|
ElMessageBox.confirm(
|
|
'确定要删除该批次图片吗?此操作不可恢复!',
|
|
'删除确认',
|
|
{
|
|
confirmButtonText: '确定删除',
|
|
cancelButtonText: '取消',
|
|
type: 'warning',
|
|
confirmButtonClass: 'el-button--danger'
|
|
}
|
|
).then(async () => {
|
|
try {
|
|
const res = await deleteGenerateHistory({
|
|
id: props.item.id
|
|
})
|
|
if (res.success) {
|
|
ElMessage.success('删除成功')
|
|
emit('delete-success', props.item.id)
|
|
} else {
|
|
ElMessage.error(res.message || '删除失败')
|
|
}
|
|
} catch (error) {
|
|
console.error('删除操作失败:', error)
|
|
ElMessage.error('删除操作失败')
|
|
}
|
|
}).catch(() => {})
|
|
}
|
|
|
|
const bottomBtnGroup = computed(() => [
|
|
{
|
|
name: '重新编辑',
|
|
icon: reEditIcon,
|
|
click: reEdit
|
|
},
|
|
{
|
|
name: '再次生成',
|
|
icon: againGenerateIcon,
|
|
click: againGenerate
|
|
},
|
|
{
|
|
name: '删除该批次',
|
|
icon: deleteImageIcon,
|
|
click: deleteImage
|
|
}
|
|
])
|
|
|
|
const addCollection = async (url) => {
|
|
try {
|
|
const res = await cancelOrCollect({
|
|
taskId: props.item.id,
|
|
userId: useUser.userInfo.id,
|
|
url: url,
|
|
})
|
|
if (res.success) {
|
|
ElMessage.success(res.message || '操作成功')
|
|
localCollectStatus.value[url] = !localCollectStatus.value[url]
|
|
} else {
|
|
ElMessage.error(res.message || '操作失败')
|
|
}
|
|
} catch (error) {
|
|
console.error('收藏操作失败:', error)
|
|
ElMessage.error('收藏操作失败')
|
|
}
|
|
}
|
|
|
|
const copyPrompt = async () => {
|
|
try {
|
|
const promptText = props.item.generateData.prompt || '生成图片'
|
|
await navigator.clipboard.writeText(promptText)
|
|
ElMessage.success('提示词已复制到剪贴板')
|
|
} catch (error) {
|
|
console.error('复制失败:', error)
|
|
ElMessage.error('复制失败,请手动复制')
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style lang="less" scoped>
|
|
.primary-box{
|
|
width: 80%;
|
|
max-width: 1118px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 12px;
|
|
padding-bottom: 40px;
|
|
}
|
|
.none-primary-box{
|
|
height: 350px;
|
|
}
|
|
|
|
.prompt-container{
|
|
width: 100%;
|
|
position: relative;
|
|
overflow: visible;
|
|
}
|
|
|
|
.prompt-wrapper{
|
|
position: relative;
|
|
width: 100%;
|
|
}
|
|
|
|
.prompt{
|
|
color: #333;
|
|
font-family: "Microsoft YaHei";
|
|
font-size: 14px;
|
|
font-style: normal;
|
|
font-weight: 400;
|
|
line-height: 22.5px;
|
|
word-break: break-all;
|
|
padding: 8px;
|
|
background-color: #fff;
|
|
position: absolute;
|
|
left: -8px;
|
|
top: 0;
|
|
width: 102%;
|
|
z-index: 5;
|
|
width: auto;
|
|
|
|
&.expanded{
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
|
border-radius: 4px;
|
|
}
|
|
}
|
|
|
|
.prompt-text{
|
|
display: inline;
|
|
color: #333;
|
|
font-family: "Microsoft YaHei";
|
|
font-size: 14px;
|
|
font-style: normal;
|
|
font-weight: 400;
|
|
line-height: normal;
|
|
}
|
|
.Copy{
|
|
cursor: pointer;
|
|
margin-left: 5px;
|
|
color: #999;
|
|
text-align: center;
|
|
}
|
|
.Copy:hover{
|
|
color: #666;
|
|
}
|
|
.generate-data{
|
|
display: inline-flex;
|
|
align-items: center;
|
|
background-color: #fff;
|
|
height: 22.5px;
|
|
text-align: center;
|
|
padding: 0 12px;
|
|
|
|
color: #999;
|
|
font-family: "Microsoft YaHei";
|
|
font-size: 14px;
|
|
font-style: normal;
|
|
font-weight: 400;
|
|
line-height: normal;
|
|
|
|
&.internal{
|
|
display: inline-flex;
|
|
margin-left: 10px;
|
|
}
|
|
|
|
&.external{
|
|
position: absolute;
|
|
right: -16px;
|
|
bottom: 0;
|
|
z-index: 6;
|
|
// padding-right: 20px;
|
|
}
|
|
}
|
|
|
|
.detailed-data{
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
color: #999;
|
|
font-family: "Microsoft YaHei";
|
|
font-size: 14px;
|
|
font-style: normal;
|
|
font-weight: 400;
|
|
line-height: normal;
|
|
padding: 0 10px;
|
|
border-left: 1px solid #999;
|
|
white-space: nowrap;
|
|
height: 12px;
|
|
}
|
|
|
|
.first-detailed-data{
|
|
border-left: none;
|
|
padding-left: 0;
|
|
}
|
|
|
|
.box{
|
|
height: calc(100% - 37px);
|
|
width: 100%;
|
|
}
|
|
.none-box{
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
flex-direction: column;
|
|
.img{
|
|
width: 100%;
|
|
max-width: 300px;
|
|
}
|
|
}
|
|
|
|
// 生成中
|
|
.generate-box {
|
|
height: 300px;
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
flex-direction: column;
|
|
gap: 20px;
|
|
|
|
.generate-content {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 15px;
|
|
|
|
.status-text {
|
|
font-size: 14px;
|
|
color: #333;
|
|
font-weight: 600;
|
|
font-family: "Microsoft YaHei";
|
|
}
|
|
}
|
|
}
|
|
|
|
// 欢快的弹跳动画
|
|
.loading-animation {
|
|
display: flex;
|
|
gap: 8px;
|
|
justify-content: center;
|
|
align-items: center;
|
|
|
|
.bounce-dot {
|
|
width: 12px;
|
|
height: 12px;
|
|
border-radius: 50%;
|
|
background-color: #333;
|
|
animation: bounce 1.5s infinite ease-in-out;
|
|
|
|
&:nth-child(2) {
|
|
animation-delay: 0.2s;
|
|
background-color: #409eff;
|
|
}
|
|
|
|
&:nth-child(3) {
|
|
animation-delay: 0.4s;
|
|
background-color: #67c23a;
|
|
}
|
|
}
|
|
}
|
|
|
|
@keyframes bounce {
|
|
0%, 80%, 100% {
|
|
transform: translateY(0);
|
|
}
|
|
40% {
|
|
transform: translateY(-15px);
|
|
}
|
|
}
|
|
|
|
// 成功
|
|
.one-box{
|
|
position: relative;
|
|
display: flex;
|
|
justify-content: center; /* 水平居中 */
|
|
align-items: center; /* 垂直居中 */
|
|
border-radius: 10px;
|
|
// height: 100%;
|
|
}
|
|
.one-box:hover{
|
|
.left-top,.bottom-brush{
|
|
display:flex
|
|
}
|
|
}
|
|
.one-box.collected{
|
|
.left-top{
|
|
display:flex
|
|
}
|
|
}
|
|
.success-box{
|
|
display: grid;
|
|
grid-template-columns: repeat(4, 1fr);
|
|
border-radius: 10px;
|
|
gap: 10px;
|
|
|
|
.img{
|
|
width: 100%;
|
|
height: auto; /* 保持宽高比 */
|
|
object-fit: contain; /* 确保图片完整显示,不被裁剪 */
|
|
border-radius: 8px; /* 可选:给图片添加圆角 */
|
|
}
|
|
|
|
.video{
|
|
width: 380px;
|
|
height: auto; /* 保持宽高比 */
|
|
object-fit: contain; /* 确保图片完整显示,不被裁剪 */
|
|
border-radius: 8px; /* 可选:给图片添加圆角 */
|
|
}
|
|
}
|
|
|
|
.left-top,.bottom-brush{
|
|
display: none;
|
|
position: absolute;
|
|
z-index: 1;
|
|
cursor: pointer;
|
|
justify-content: center;
|
|
align-items: center;
|
|
}
|
|
.left-top{
|
|
padding: 4px 6px;
|
|
gap: 10px;
|
|
|
|
right: 10px;
|
|
top: 10px;
|
|
border-radius: 5px;
|
|
background: rgba(51, 51, 51, 0.80);
|
|
backdrop-filter: blur(2.5px);
|
|
|
|
.left-top-btn{
|
|
display: flex;
|
|
align-items: center;
|
|
padding: 4px;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.line{
|
|
width: 1px;
|
|
height: 12px;
|
|
background-color: #ccc;
|
|
}
|
|
}
|
|
.bottom-brush{
|
|
width: 60px;
|
|
height: 32px;
|
|
|
|
border-radius: 10px;
|
|
background: rgba(51, 51, 51, 0.80);
|
|
backdrop-filter: blur(2.5px);
|
|
bottom: 10px;
|
|
}
|
|
.left-top-btn:hover,.bottom-brush:hover {
|
|
background-color: rgb(112, 112, 112);
|
|
}
|
|
|
|
// 成功时底部按钮组
|
|
.bottom-btn-group{
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
}
|
|
.bottom-btn{
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 5px;
|
|
width: 120px;
|
|
height: 36px;
|
|
border-radius: 10px;
|
|
cursor: pointer;
|
|
background-color: #F8F9FA;
|
|
}
|
|
.bottom-btn:hover{
|
|
background-color: #e4e7ed;
|
|
}
|
|
</style>
|