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