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,213 @@
<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"
@click="handleCardClick(task)"
@use-prompt="$emit('use-prompt', $event)"
@use-reference="$emit('use-reference', $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 group">
<el-image
:src="currentTask.storeUrl"
fit="contain"
class="w-full h-full"
:preview-src-list="[currentTask.storeUrl]"
/>
</div>
<!-- Right Info -->
<div class="w-full md:w-[300px] flex flex-col gap-4 overflow-y-auto">
<div>
<h3 class="font-bold text-gray-800 mb-2 flex items-center gap-2">
<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">
{{ currentTask.prompt }}
<el-button
class="absolute top-2 right-2 opacity-0 group-hover/prompt:opacity-100 transition-opacity shadow-sm"
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">
<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">
<span class="text-gray-500">状态</span>
<el-tag size="small" type="success">生成成功</el-tag>
</div>
<div class="grid grid-cols-2 gap-2 mt-2">
<el-button
type="primary"
: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>
</div>
</div>
</div>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue';
import { getImagePlaza } from '@/api/aiImage';
import type { TaskItem } from '@/api/aiImage/types';
import TaskCard from './TaskCard.vue';
import { format } from 'date-fns';
import { ElMessage } from 'element-plus';
import { useClipboard } from '@vueuse/core';
import { Picture, Loading, MagicStick, CopyDocument } 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 getImagePlaza({
PageIndex: pageIndex.value,
PageSize: pageSize.value,
TaskStatus: 'Success'
});
// 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
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);
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('提示词已复制');
};
</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>