feat: 前端新增图片生成功能

This commit is contained in:
Gsh
2026-01-03 15:16:18 +08:00
parent 5bb7dfb7cd
commit a3259ad36f
11 changed files with 1343 additions and 21 deletions

View File

@@ -0,0 +1,275 @@
<template>
<div class="h-full flex flex-col bg-gray-50">
<div
class="flex-1 overflow-y-auto p-4 custom-scrollbar"
v-infinite-scroll="loadMore"
:infinite-scroll-disabled="disabled"
:infinite-scroll-distance="50"
>
<div v-if="taskList.length > 0" class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-4">
<TaskCard
v-for="task in taskList"
:key="task.id"
:task="task"
show-publish-status
@click="handleCardClick(task)"
@use-prompt="$emit('use-prompt', $event)"
@use-reference="$emit('use-reference', $event)"
@publish="handlePublish($event)"
/>
</div>
<!-- Empty State -->
<div v-else-if="!loading && taskList.length === 0" class="h-full flex flex-col items-center justify-center text-gray-400">
<el-icon class="text-6xl mb-4"><Picture /></el-icon>
<p>您还没有生成过图片</p>
</div>
<!-- Loading State -->
<div v-if="loading" class="py-6 flex justify-center">
<div class="flex items-center gap-2 text-gray-500">
<el-icon class="is-loading"><Loading /></el-icon>
<span>加载中...</span>
</div>
</div>
<!-- No More State -->
<div v-if="noMore && taskList.length > 0" class="py-6 text-center text-gray-400 text-sm">
- 到底了没有更多图片了 -
</div>
</div>
<!-- Dialog for details -->
<el-dialog
v-model="dialogVisible"
title="图片详情"
width="900px"
append-to-body
class="image-detail-dialog"
align-center
>
<div v-if="currentTask" class="flex flex-col md:flex-row gap-6 h-[600px]">
<!-- Left Image -->
<div class="flex-1 bg-black/5 rounded-lg flex items-center justify-center overflow-hidden relative">
<el-image
v-if="currentTask.storeUrl"
:src="currentTask.storeUrl"
fit="contain"
class="w-full h-full"
:preview-src-list="[currentTask.storeUrl]"
/>
<div v-else class="flex flex-col items-center text-gray-400 p-4 text-center">
<span v-if="currentTask.taskStatus === 'Processing'">生成中...</span>
<div v-else-if="currentTask.taskStatus === 'Fail'" class="text-red-500 flex flex-col items-center">
<el-icon class="text-4xl mb-2"><CircleCloseFilled /></el-icon>
<span class="font-bold mb-1">生成失败</span>
<span class="text-sm opacity-80">{{ currentTask.errorInfo || '未知错误' }}</span>
</div>
</div>
</div>
<!-- Right Info -->
<div class="w-full md:w-[300px] flex flex-col gap-4 overflow-hidden">
<div class="flex-1 flex flex-col min-h-0">
<h3 class="font-bold text-gray-800 mb-2 flex items-center gap-2 shrink-0">
<el-icon><MagicStick /></el-icon> 提示词
</h3>
<div class="bg-gray-50 p-4 rounded-lg border border-gray-100 text-sm text-gray-600 leading-relaxed relative group/prompt overflow-y-auto custom-scrollbar flex-1">
{{ currentTask.prompt }}
<el-button
class="absolute top-2 right-2 opacity-0 group-hover/prompt:opacity-100 transition-opacity shadow-sm z-10"
size="small"
circle
:icon="CopyDocument"
@click="copyPrompt(currentTask.prompt)"
title="复制提示词"
/>
</div>
</div>
<div class="mt-auto space-y-3 pt-4 border-t border-gray-100 shrink-0">
<div class="flex justify-between text-sm">
<span class="text-gray-500">创建时间</span>
<span class="text-gray-800">{{ formatTime(currentTask.creationTime) }}</span>
</div>
<div class="flex justify-between text-sm items-center">
<span class="text-gray-500">状态</span>
<div class="flex gap-2">
<el-tag v-if="currentTask.taskStatus === 'Success'" size="small" type="success">成功</el-tag>
<el-tag v-else-if="currentTask.taskStatus === 'Processing'" size="small" type="primary">进行中</el-tag>
<el-tag v-else size="small" type="danger">失败</el-tag>
<el-tag v-if="currentTask.publishStatus === 'Published'" size="small" type="warning" effect="dark">已发布</el-tag>
</div>
</div>
<div v-if="currentTask.taskStatus === 'Success'" class="pt-2 space-y-2">
<div class="grid grid-cols-2 gap-2">
<el-button
type="primary"
plain
:icon="MagicStick"
@click="$emit('use-prompt', currentTask.prompt)"
>
使用提示词
</el-button>
<el-button
type="primary"
plain
:icon="Picture"
@click="$emit('use-reference', currentTask.storeUrl)"
>
做参考图
</el-button>
</div>
<el-button
v-if="currentTask.publishStatus === 'Unpublished'"
type="success"
class="w-full"
:icon="Share"
@click="handlePublish(currentTask)"
>
发布到广场
</el-button>
<el-button v-else type="info" disabled class="w-full">
已发布
</el-button>
</div>
</div>
</div>
</div>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue';
import { getMyTasks, publishImage } from '@/api/aiImage';
import type { TaskItem } from '@/api/aiImage/types';
import TaskCard from './TaskCard.vue';
import { format } from 'date-fns';
import { ElMessage, ElMessageBox } from 'element-plus';
import { useClipboard } from '@vueuse/core';
import { Picture, Loading, MagicStick, CopyDocument, Share } from '@element-plus/icons-vue';
const emit = defineEmits(['use-prompt', 'use-reference']);
const taskList = ref<TaskItem[]>([]);
const pageIndex = ref(1);
const pageSize = ref(20);
const loading = ref(false);
const noMore = ref(false);
const dialogVisible = ref(false);
const currentTask = ref<TaskItem | null>(null);
const { copy } = useClipboard();
const disabled = computed(() => loading.value || noMore.value);
const loadMore = async () => {
if (loading.value || noMore.value) return;
loading.value = true;
try {
const res = await getMyTasks({
PageIndex: pageIndex.value,
PageSize: pageSize.value
});
// Handle potential wrapper
const data = (res as any).data || res;
const items = data.items || [];
const total = data.total || 0;
if (items.length < pageSize.value) {
noMore.value = true;
}
if (pageIndex.value === 1) {
taskList.value = items;
} else {
// Avoid duplicates if any
const newItems = items.filter((item: TaskItem) => !taskList.value.some(t => t.id === item.id));
taskList.value.push(...newItems);
}
// Check if we reached total
if (taskList.value.length >= total) {
noMore.value = true;
}
if (items.length > 0) {
pageIndex.value++;
} else {
noMore.value = true;
}
} catch (error) {
console.error(error);
// Stop trying if error occurs to prevent loop
noMore.value = true;
} finally {
loading.value = false;
}
};
const handleCardClick = (task: TaskItem) => {
currentTask.value = task;
dialogVisible.value = true;
};
const formatTime = (time: string) => {
try {
return format(new Date(time), 'yyyy-MM-dd HH:mm');
} catch (e) {
return time;
}
};
const copyPrompt = async (text: string) => {
await copy(text);
ElMessage.success('提示词已复制');
};
const handlePublish = async (task: TaskItem) => {
try {
await ElMessageBox.confirm('确定要发布这张图片到广场吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'info',
});
await publishImage({
taskId: task.id,
categories: []
});
ElMessage.success('发布成功');
task.publishStatus = 'Published';
} catch (e) {
// Cancelled or error
}
};
// Expose refresh method
defineExpose({
refresh: () => {
pageIndex.value = 1;
taskList.value = [];
noMore.value = false;
loadMore();
}
});
</script>
<style scoped>
.custom-scrollbar::-webkit-scrollbar {
width: 6px;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background-color: #e5e7eb;
border-radius: 3px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background-color: transparent;
}
</style>