feat: 前端新增图片生成功能
This commit is contained in:
213
Yi.Ai.Vue3/src/pages/chat/image/components/ImagePlaza.vue
Normal file
213
Yi.Ai.Vue3/src/pages/chat/image/components/ImagePlaza.vue
Normal 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>
|
||||
Reference in New Issue
Block a user