931 lines
30 KiB
Vue
931 lines
30 KiB
Vue
<script setup lang="ts">
|
||
import type { InputInstance } from 'element-plus';
|
||
import type { TaskItem, TaskListRequest } from '@/api/aiImage/types';
|
||
import { Check, CircleCloseFilled, CollectionTag, CopyDocument, Download, Filter, Loading, MagicStick, Picture, Refresh, Search, Share, User, WarningFilled, ZoomIn } from '@element-plus/icons-vue';
|
||
import { useClipboard } from '@vueuse/core';
|
||
import { format } from 'date-fns';
|
||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||
import { computed, nextTick, onMounted, onUnmounted, reactive, ref, watch } from 'vue';
|
||
import { deleteMyTasks, getMyTasks, publishImage } from '@/api/aiImage';
|
||
import TaskCard from './TaskCard.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);
|
||
|
||
// Batch selection
|
||
const batchMode = ref(false);
|
||
const selectedTaskIds = ref<string[]>([]);
|
||
|
||
// Mobile detection
|
||
const isMobile = ref(false);
|
||
|
||
function checkMobile() {
|
||
isMobile.value = window.innerWidth < 768;
|
||
}
|
||
|
||
onMounted(() => {
|
||
checkMobile();
|
||
window.addEventListener('resize', checkMobile);
|
||
loadMore();
|
||
});
|
||
|
||
onUnmounted(() => {
|
||
window.removeEventListener('resize', checkMobile);
|
||
});
|
||
|
||
// Mobile filter drawer
|
||
const showMobileFilter = ref(false);
|
||
|
||
// Viewer State
|
||
const showViewer = ref(false);
|
||
const previewUrl = ref('');
|
||
|
||
// Filter State
|
||
const searchForm = reactive({
|
||
Prompt: '',
|
||
TaskStatus: '' as 'Processing' | 'Success' | 'Fail' | '',
|
||
PublishStatus: '' as 'Unpublished' | 'Published' | '',
|
||
OrderByColumn: '',
|
||
IsAscending: true,
|
||
});
|
||
const dateRange = ref<[string, string] | null>(null);
|
||
|
||
// Publish Dialog State
|
||
const publishDialogVisible = ref(false);
|
||
const publishing = ref(false);
|
||
const publishTags = ref<string[]>([]);
|
||
const inputValue = ref('');
|
||
const inputVisible = ref(false);
|
||
const InputRef = ref<InputInstance>();
|
||
const taskToPublish = ref<TaskItem | null>(null);
|
||
const isAnonymousPublish = ref(false);
|
||
|
||
const { copy } = useClipboard();
|
||
|
||
const disabled = computed(() => loading.value || noMore.value);
|
||
|
||
async function loadMore() {
|
||
if (loading.value || noMore.value)
|
||
return;
|
||
|
||
loading.value = true;
|
||
try {
|
||
const params: TaskListRequest = {
|
||
SkipCount: pageIndex.value,
|
||
MaxResultCount: pageSize.value,
|
||
Prompt: searchForm.Prompt || undefined,
|
||
TaskStatus: searchForm.TaskStatus || undefined,
|
||
PublishStatus: searchForm.PublishStatus || undefined,
|
||
StartTime: dateRange.value ? dateRange.value[0] : undefined,
|
||
EndTime: dateRange.value ? dateRange.value[1] : undefined,
|
||
OrderByColumn: searchForm.OrderByColumn || undefined,
|
||
IsAscending: searchForm.IsAscending,
|
||
};
|
||
|
||
const res = await getMyTasks(params);
|
||
|
||
// Handle potential wrapper
|
||
const data = (res as any).data || res;
|
||
const items = data.items || [];
|
||
const total = data.total || 0;
|
||
|
||
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 should stop loading more
|
||
if (items.length === 0 || items.length < pageSize.value || taskList.value.length >= total) {
|
||
noMore.value = true;
|
||
}
|
||
else {
|
||
// Increment pageIndex for next load only if there's more data
|
||
pageIndex.value++;
|
||
}
|
||
}
|
||
catch (error) {
|
||
console.error(error);
|
||
// Stop trying if error occurs to prevent loop
|
||
noMore.value = true;
|
||
}
|
||
finally {
|
||
loading.value = false;
|
||
}
|
||
}
|
||
|
||
function handleSearch() {
|
||
pageIndex.value = 1;
|
||
taskList.value = [];
|
||
noMore.value = false;
|
||
loadMore();
|
||
}
|
||
|
||
function handleReset() {
|
||
searchForm.Prompt = '';
|
||
searchForm.TaskStatus = '';
|
||
searchForm.PublishStatus = '';
|
||
dateRange.value = null;
|
||
handleSearch();
|
||
}
|
||
|
||
function handleCardClick(task: TaskItem) {
|
||
currentTask.value = task;
|
||
dialogVisible.value = true;
|
||
}
|
||
|
||
function handlePreview(url: string) {
|
||
previewUrl.value = url;
|
||
showViewer.value = true;
|
||
}
|
||
|
||
function closeViewer() {
|
||
showViewer.value = false;
|
||
}
|
||
|
||
function formatTime(time: string) {
|
||
try {
|
||
return format(new Date(time), 'yyyy-MM-dd HH:mm');
|
||
}
|
||
catch (e) {
|
||
return time;
|
||
}
|
||
}
|
||
|
||
async function copyPrompt(text: string) {
|
||
await copy(text);
|
||
ElMessage.success('提示词已复制');
|
||
}
|
||
|
||
async function copyErrorInfo(text: string) {
|
||
await copy(text);
|
||
ElMessage.success('错误信息已复制');
|
||
}
|
||
|
||
function openPublishDialog(task: TaskItem) {
|
||
taskToPublish.value = task;
|
||
publishTags.value = [];
|
||
inputValue.value = '';
|
||
inputVisible.value = false;
|
||
isAnonymousPublish.value = false;
|
||
publishDialogVisible.value = true;
|
||
}
|
||
|
||
function handleCloseTag(tag: string) {
|
||
publishTags.value.splice(publishTags.value.indexOf(tag), 1);
|
||
}
|
||
|
||
function showInput() {
|
||
inputVisible.value = true;
|
||
nextTick(() => {
|
||
InputRef.value!.input!.focus();
|
||
});
|
||
}
|
||
|
||
function handleInputConfirm() {
|
||
if (inputValue.value) {
|
||
if (publishTags.value.length >= 3) {
|
||
ElMessage.warning('最多只能添加3个标签');
|
||
inputValue.value = '';
|
||
inputVisible.value = false;
|
||
return;
|
||
}
|
||
if (!publishTags.value.includes(inputValue.value)) {
|
||
publishTags.value.push(inputValue.value);
|
||
}
|
||
inputValue.value = '';
|
||
// 保持焦点
|
||
nextTick(() => {
|
||
InputRef.value?.focus();
|
||
});
|
||
}
|
||
}
|
||
|
||
async function confirmPublish() {
|
||
if (!taskToPublish.value)
|
||
return;
|
||
|
||
publishing.value = true;
|
||
try {
|
||
await publishImage({
|
||
taskId: taskToPublish.value.id,
|
||
categories: publishTags.value,
|
||
isAnonymous: isAnonymousPublish.value,
|
||
});
|
||
|
||
ElMessage.success('发布成功');
|
||
taskToPublish.value.publishStatus = 'Published';
|
||
publishDialogVisible.value = false;
|
||
}
|
||
catch (e) {
|
||
console.error(e);
|
||
ElMessage.error('发布失败');
|
||
}
|
||
finally {
|
||
publishing.value = false;
|
||
}
|
||
}
|
||
|
||
async function downloadImage(url: string) {
|
||
try {
|
||
const response = await fetch(url);
|
||
const blob = await response.blob();
|
||
const blobUrl = window.URL.createObjectURL(blob);
|
||
const link = document.createElement('a');
|
||
link.href = blobUrl;
|
||
link.download = `image-${Date.now()}.png`;
|
||
document.body.appendChild(link);
|
||
link.click();
|
||
document.body.removeChild(link);
|
||
window.URL.revokeObjectURL(blobUrl);
|
||
}
|
||
catch (e) {
|
||
console.error('Download failed', e);
|
||
window.open(url, '_blank');
|
||
}
|
||
}
|
||
|
||
// Batch selection functions
|
||
function toggleBatchMode() {
|
||
batchMode.value = !batchMode.value;
|
||
if (!batchMode.value) {
|
||
selectedTaskIds.value = [];
|
||
}
|
||
}
|
||
|
||
function toggleTaskSelection(taskId: string) {
|
||
const index = selectedTaskIds.value.indexOf(taskId);
|
||
if (index > -1) {
|
||
selectedTaskIds.value.splice(index, 1);
|
||
}
|
||
else {
|
||
selectedTaskIds.value.push(taskId);
|
||
}
|
||
}
|
||
|
||
function selectAll() {
|
||
selectedTaskIds.value = taskList.value.map(task => task.id);
|
||
}
|
||
|
||
function clearSelection() {
|
||
selectedTaskIds.value = [];
|
||
}
|
||
|
||
async function batchDelete() {
|
||
if (selectedTaskIds.value.length === 0) {
|
||
ElMessage.warning('请先选择要删除的图片');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
await ElMessageBox.confirm(
|
||
`确定要删除选中的 ${selectedTaskIds.value.length} 张图片吗?`,
|
||
'批量删除',
|
||
{
|
||
confirmButtonText: '确定',
|
||
cancelButtonText: '取消',
|
||
type: 'warning',
|
||
},
|
||
);
|
||
|
||
const ids = selectedTaskIds.value;
|
||
await deleteMyTasks(ids);
|
||
ElMessage.success('删除成功');
|
||
selectedTaskIds.value = [];
|
||
batchMode.value = false;
|
||
handleSearch();
|
||
}
|
||
catch (e: any) {
|
||
if (e !== 'cancel') {
|
||
console.error(e);
|
||
ElMessage.error('删除失败');
|
||
}
|
||
}
|
||
}
|
||
|
||
// Expose refresh method
|
||
defineExpose({
|
||
refresh: () => {
|
||
handleSearch();
|
||
},
|
||
});
|
||
|
||
// Watch for filter changes - debounce for prompt, immediate for others
|
||
let searchTimer: ReturnType<typeof setTimeout> | null = null;
|
||
watch(() => searchForm.Prompt, () => {
|
||
if (searchTimer) {
|
||
clearTimeout(searchTimer);
|
||
}
|
||
searchTimer = setTimeout(() => {
|
||
handleSearch();
|
||
}, 500);
|
||
});
|
||
|
||
watch([() => searchForm.TaskStatus, () => searchForm.PublishStatus, dateRange], () => {
|
||
handleSearch();
|
||
});
|
||
</script>
|
||
|
||
<template>
|
||
<div class="h-full flex flex-col md:flex-row bg-gray-50 overflow-hidden">
|
||
<!-- Mobile Filter Button -->
|
||
<div class="md:hidden p-4 bg-white border-b border-gray-200 flex items-center justify-between">
|
||
<h2 class="text-lg font-bold text-gray-800">
|
||
我的图库
|
||
</h2>
|
||
<el-button type="primary" @click="showMobileFilter = true">
|
||
<el-icon class="mr-1">
|
||
<Filter />
|
||
</el-icon>
|
||
筛选
|
||
</el-button>
|
||
</div>
|
||
|
||
<!-- Left Sidebar - Filters (Desktop) -->
|
||
<div class="hidden md:flex w-72 bg-white border-r border-gray-200 flex-col shadow-sm">
|
||
<div class="p-6 border-b border-gray-100">
|
||
<h2 class="text-lg font-bold text-gray-800 flex items-center gap-2">
|
||
<el-icon><Filter /></el-icon>
|
||
筛选条件
|
||
</h2>
|
||
</div>
|
||
|
||
<div class=" overflow-y-auto p-6 custom-scrollbar">
|
||
<el-form :model="searchForm" label-position="top" class="space-y-4">
|
||
<el-form-item label="提示词">
|
||
<el-input
|
||
v-model="searchForm.Prompt"
|
||
placeholder="搜索提示词..."
|
||
clearable
|
||
@keyup.enter="handleSearch"
|
||
>
|
||
<template #prefix>
|
||
<el-icon><Search /></el-icon>
|
||
</template>
|
||
</el-input>
|
||
</el-form-item>
|
||
|
||
<el-form-item label="任务状态">
|
||
<el-select
|
||
v-model="searchForm.TaskStatus"
|
||
placeholder="全部状态"
|
||
class="w-full"
|
||
clearable
|
||
>
|
||
<el-option label="进行中" value="Processing">
|
||
<div class="flex items-center gap-2">
|
||
<el-icon class="text-blue-500">
|
||
<Loading />
|
||
</el-icon>
|
||
<span>进行中</span>
|
||
</div>
|
||
</el-option>
|
||
<el-option label="成功" value="Success">
|
||
<div class="flex items-center gap-2">
|
||
<el-icon class="text-green-500">
|
||
<Check />
|
||
</el-icon>
|
||
<span>成功</span>
|
||
</div>
|
||
</el-option>
|
||
<el-option label="失败" value="Fail">
|
||
<div class="flex items-center gap-2">
|
||
<el-icon class="text-red-500">
|
||
<CircleCloseFilled />
|
||
</el-icon>
|
||
<span>失败</span>
|
||
</div>
|
||
</el-option>
|
||
</el-select>
|
||
</el-form-item>
|
||
|
||
<el-form-item label="发布状态">
|
||
<el-select
|
||
v-model="searchForm.PublishStatus"
|
||
placeholder="全部状态"
|
||
class="w-full"
|
||
clearable
|
||
>
|
||
<el-option label="未发布" value="Unpublished" />
|
||
<el-option label="已发布" value="Published" />
|
||
</el-select>
|
||
</el-form-item>
|
||
|
||
<el-form-item label="创建时间">
|
||
<el-date-picker
|
||
v-model="dateRange"
|
||
type="daterange"
|
||
range-separator="至"
|
||
start-placeholder="开始日期"
|
||
end-placeholder="结束日期"
|
||
value-format="YYYY-MM-DD"
|
||
class="w-full"
|
||
/>
|
||
</el-form-item>
|
||
|
||
<el-form-item v-if="false" label="排序方式">
|
||
<el-select
|
||
v-model="searchForm.OrderByColumn"
|
||
placeholder="默认排序"
|
||
class="w-full"
|
||
clearable
|
||
>
|
||
<el-option label="创建时间" value="CreationTime" />
|
||
<el-option label="任务状态" value="TaskStatus" />
|
||
<el-option label="发布状态" value="PublishStatus" />
|
||
</el-select>
|
||
</el-form-item>
|
||
|
||
<el-form-item v-if="searchForm.OrderByColumn" label="排序方向">
|
||
<el-radio-group v-model="searchForm.IsAscending">
|
||
<el-radio :value="true">
|
||
升序
|
||
</el-radio>
|
||
<el-radio :value="false">
|
||
降序
|
||
</el-radio>
|
||
</el-radio-group>
|
||
</el-form-item>
|
||
</el-form>
|
||
</div>
|
||
|
||
<div class="p-6 border-t border-gray-100 space-y-2">
|
||
<el-button type="primary" class="w-full" @click="handleSearch">
|
||
<el-icon class="mr-1">
|
||
<Search />
|
||
</el-icon>
|
||
搜索
|
||
</el-button>
|
||
<el-button class="w-full refresh-btn" @click="handleReset">
|
||
<el-icon class="mr-1">
|
||
<Refresh />
|
||
</el-icon>
|
||
重置
|
||
</el-button>
|
||
|
||
<!-- Batch Operations -->
|
||
<div class="pt-4 border-t border-gray-100 space-y-2">
|
||
<el-button
|
||
:type="batchMode ? 'primary' : 'default'"
|
||
class="w-full "
|
||
@click="toggleBatchMode"
|
||
>
|
||
{{ batchMode ? '取消批量' : '批量管理' }}
|
||
</el-button>
|
||
<template v-if="batchMode">
|
||
<el-button class="w-full refresh-btn" @click="selectAll">
|
||
全选
|
||
</el-button>
|
||
<el-button class="w-full refresh-btn" @click="clearSelection">
|
||
清空
|
||
</el-button>
|
||
<el-button
|
||
type="danger"
|
||
class="w-full refresh-btn"
|
||
:disabled="selectedTaskIds.length === 0"
|
||
@click="batchDelete"
|
||
>
|
||
删除选中 ({{ selectedTaskIds.length }})
|
||
</el-button>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Mobile Filter Drawer -->
|
||
<el-drawer
|
||
v-model="showMobileFilter"
|
||
title="筛选条件"
|
||
direction="ltr"
|
||
size="80%"
|
||
>
|
||
<el-form :model="searchForm" label-position="top" class="space-y-4">
|
||
<el-form-item label="提示词">
|
||
<el-input
|
||
v-model="searchForm.Prompt"
|
||
placeholder="搜索提示词..."
|
||
clearable
|
||
>
|
||
<template #prefix>
|
||
<el-icon><Search /></el-icon>
|
||
</template>
|
||
</el-input>
|
||
</el-form-item>
|
||
|
||
<el-form-item label="任务状态">
|
||
<el-select
|
||
v-model="searchForm.TaskStatus"
|
||
placeholder="全部状态"
|
||
class="w-full"
|
||
clearable
|
||
>
|
||
<el-option label="进行中" value="Processing" />
|
||
<el-option label="成功" value="Success" />
|
||
<el-option label="失败" value="Fail" />
|
||
</el-select>
|
||
</el-form-item>
|
||
|
||
<el-form-item label="发布状态">
|
||
<el-select
|
||
v-model="searchForm.PublishStatus"
|
||
placeholder="全部状态"
|
||
class="w-full"
|
||
clearable
|
||
>
|
||
<el-option label="未发布" value="Unpublished" />
|
||
<el-option label="已发布" value="Published" />
|
||
</el-select>
|
||
</el-form-item>
|
||
|
||
<el-form-item label="创建时间">
|
||
<el-date-picker
|
||
v-model="dateRange"
|
||
type="daterange"
|
||
range-separator="至"
|
||
start-placeholder="开始日期"
|
||
end-placeholder="结束日期"
|
||
value-format="YYYY-MM-DD"
|
||
class="w-full"
|
||
/>
|
||
</el-form-item>
|
||
|
||
<el-form-item label="排序方式">
|
||
<el-select
|
||
v-model="searchForm.OrderByColumn"
|
||
placeholder="默认排序"
|
||
class="w-full"
|
||
clearable
|
||
>
|
||
<el-option label="创建时间" value="CreationTime" />
|
||
<el-option label="任务状态" value="TaskStatus" />
|
||
<el-option label="发布状态" value="PublishStatus" />
|
||
</el-select>
|
||
</el-form-item>
|
||
|
||
<el-form-item v-if="searchForm.OrderByColumn" label="排序方向">
|
||
<el-radio-group v-model="searchForm.IsAscending">
|
||
<el-radio :value="true">
|
||
升序
|
||
</el-radio>
|
||
<el-radio :value="false">
|
||
降序
|
||
</el-radio>
|
||
</el-radio-group>
|
||
</el-form-item>
|
||
</el-form>
|
||
|
||
<template #footer>
|
||
<div class="space-y-2">
|
||
<el-button type="primary" class="w-full" @click="handleSearch(); showMobileFilter = false">
|
||
<el-icon class="mr-1">
|
||
<Search />
|
||
</el-icon>
|
||
搜索
|
||
</el-button>
|
||
<el-button class="w-full" @click="handleReset(); showMobileFilter = false">
|
||
<el-icon class="mr-1">
|
||
<Refresh />
|
||
</el-icon>
|
||
重置
|
||
</el-button>
|
||
|
||
<!-- Batch Operations -->
|
||
<div class="pt-4 border-t border-gray-100 space-y-2">
|
||
<el-button
|
||
:type="batchMode ? 'primary' : 'default'"
|
||
class="w-full"
|
||
@click="toggleBatchMode(); showMobileFilter = false"
|
||
>
|
||
{{ batchMode ? '取消批量' : '批量管理' }}
|
||
</el-button>
|
||
<template v-if="batchMode">
|
||
<el-button class="w-full" @click="selectAll(); showMobileFilter = false">
|
||
全选
|
||
</el-button>
|
||
<el-button class="w-full" @click="clearSelection(); showMobileFilter = false">
|
||
清空
|
||
</el-button>
|
||
<el-button
|
||
type="danger"
|
||
class="w-full"
|
||
:disabled="selectedTaskIds.length === 0"
|
||
@click="batchDelete(); showMobileFilter = false"
|
||
>
|
||
删除选中 ({{ selectedTaskIds.length }})
|
||
</el-button>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
</el-drawer>
|
||
|
||
<!-- Right Content Area -->
|
||
<div class="flex-1 flex flex-col overflow-hidden">
|
||
<div
|
||
v-infinite-scroll="loadMore"
|
||
class="flex-1 overflow-y-auto p-4 md:p-6 custom-scrollbar"
|
||
:infinite-scroll-disabled="disabled"
|
||
:infinite-scroll-distance="50"
|
||
>
|
||
<div v-if="taskList.length > 0" class="grid grid-cols-1 sm: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
|
||
:batch-mode="batchMode"
|
||
:selected="selectedTaskIds.includes(task.id)"
|
||
@click="batchMode ? toggleTaskSelection(task.id) : handleCardClick(task)"
|
||
@use-prompt="$emit('use-prompt', $event)"
|
||
@use-reference="$emit('use-reference', $event)"
|
||
@publish="openPublishDialog(task)"
|
||
@preview="handlePreview"
|
||
/>
|
||
</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>
|
||
</div>
|
||
|
||
<!-- Dialog for details -->
|
||
<el-dialog
|
||
v-model="dialogVisible"
|
||
title="图片详情"
|
||
:width="isMobile ? '95%' : '900px'"
|
||
:fullscreen="isMobile"
|
||
append-to-body
|
||
class="image-detail-dialog"
|
||
align-center
|
||
>
|
||
<div v-if="currentTask" class="flex flex-col md:flex-row gap-4 md:gap-6 h-auto md:h-[600px]">
|
||
<!-- Left Image -->
|
||
<div class="flex-1 bg-black/5 rounded-lg flex items-center justify-center overflow-hidden relative group">
|
||
<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 justify-center text-gray-400 p-4 text-center w-full h-full">
|
||
<span v-if="currentTask.taskStatus === 'Processing'" class="flex flex-col items-center">
|
||
<el-icon class="text-4xl mb-3 is-loading text-blue-500"><Loading /></el-icon>
|
||
<span class="text-blue-500 font-medium">生成中...</span>
|
||
</span>
|
||
<div v-else-if="currentTask.taskStatus === 'Fail'" class="flex flex-col items-center w-full max-w-md px-4">
|
||
<el-icon class="text-5xl mb-4 text-red-500">
|
||
<CircleCloseFilled />
|
||
</el-icon>
|
||
<span class="font-bold mb-4 text-lg text-red-600">生成失败</span>
|
||
<div class="w-full bg-gradient-to-br from-red-50 to-red-100 border-2 border-red-200 rounded-xl p-4 relative shadow-sm">
|
||
<div class="flex items-start gap-3">
|
||
<el-icon class="text-red-500 mt-0.5 flex-shrink-0">
|
||
<WarningFilled />
|
||
</el-icon>
|
||
<div class="flex-1 text-sm text-red-800 max-h-40 overflow-y-auto custom-scrollbar break-words text-left leading-relaxed">
|
||
{{ currentTask.errorInfo || '未知错误,请稍后重试' }}
|
||
</div>
|
||
</div>
|
||
<el-button
|
||
class="absolute top-2 right-2"
|
||
size="small"
|
||
circle
|
||
type="danger"
|
||
:icon="CopyDocument"
|
||
title="复制错误信息"
|
||
@click="copyErrorInfo(currentTask.errorInfo || '未知错误')"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Desktop Button Overlay -->
|
||
<div v-if="currentTask.storeUrl" class="hidden md:flex absolute bottom-4 right-4 gap-2 opacity-0 group-hover:opacity-100 transition-opacity z-10">
|
||
<el-button circle type="primary" :icon="ZoomIn" @click="handlePreview(currentTask.storeUrl)" />
|
||
<el-button circle type="primary" :icon="Download" @click="downloadImage(currentTask.storeUrl)" />
|
||
</div>
|
||
|
||
<!-- Mobile Button Overlay -->
|
||
<div v-if="currentTask.storeUrl" class="md:hidden absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/70 to-transparent p-3 z-10">
|
||
<div class="flex items-center justify-center gap-2">
|
||
<el-button circle type="primary" :icon="ZoomIn" @click="handlePreview(currentTask.storeUrl)" />
|
||
<el-button circle type="primary" :icon="Download" @click="downloadImage(currentTask.storeUrl)" />
|
||
</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"
|
||
title="复制提示词"
|
||
@click="copyPrompt(currentTask.prompt)"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="mt-auto space-y-3 pt-4 border-t border-gray-100 shrink-0">
|
||
<!-- 分类标签 -->
|
||
<div v-if="currentTask.categories && currentTask.categories.length > 0" class="space-y-2">
|
||
<div class="flex items-center gap-2 text-sm text-gray-500">
|
||
<el-icon><CollectionTag /></el-icon>
|
||
<span>分类标签</span>
|
||
</div>
|
||
<div class="flex flex-wrap gap-1">
|
||
<el-tag
|
||
v-for="tag in currentTask.categories"
|
||
:key="tag"
|
||
size="small"
|
||
type="info"
|
||
effect="plain"
|
||
>
|
||
{{ tag }}
|
||
</el-tag>
|
||
</div>
|
||
</div>
|
||
|
||
<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>
|
||
<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>
|
||
</div>
|
||
|
||
<!-- 发布状态 -->
|
||
<div v-if="currentTask.publishStatus === 'Published'" class="flex justify-between text-sm items-center">
|
||
<span class="text-gray-500">发布状态</span>
|
||
<div class="flex items-center gap-1">
|
||
<el-tag size="small" type="warning" effect="dark">
|
||
已发布
|
||
</el-tag>
|
||
<span v-if="!currentTask.isAnonymous && currentTask.userName" class="text-blue-500 text-xs flex items-center gap-1">
|
||
<el-icon><User /></el-icon> {{ currentTask.userName }}
|
||
</span>
|
||
<span v-else class="text-gray-400 text-xs flex items-center gap-1">
|
||
<el-icon><User /></el-icon> 匿名
|
||
</span>
|
||
</div>
|
||
</div>
|
||
<div v-else class="flex justify-between text-sm items-center">
|
||
<span class="text-gray-500">发布状态</span>
|
||
<el-tag size="small" type="info" effect="plain">
|
||
未发布
|
||
</el-tag>
|
||
</div>
|
||
|
||
<!-- 操作按钮 - 所有状态都可以使用提示词 -->
|
||
<div class="pt-2 space-y-2">
|
||
<el-button
|
||
type="primary"
|
||
plain
|
||
:icon="MagicStick"
|
||
class="w-full"
|
||
@click="$emit('use-prompt', currentTask.prompt); dialogVisible = false"
|
||
>
|
||
使用提示词
|
||
</el-button>
|
||
|
||
<!-- 成功状态才显示发布按钮 -->
|
||
<el-button
|
||
v-if="currentTask.taskStatus === 'Success' && currentTask.publishStatus === 'Unpublished'"
|
||
type="success"
|
||
class="w-full refresh-btn"
|
||
:icon="Share"
|
||
@click="openPublishDialog(currentTask)"
|
||
>
|
||
发布到广场
|
||
</el-button>
|
||
<el-button v-else-if="currentTask.taskStatus === 'Success'" type="info" disabled class="w-full">
|
||
已发布
|
||
</el-button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</el-dialog>
|
||
|
||
<!-- Publish Dialog -->
|
||
<el-dialog
|
||
v-model="publishDialogVisible"
|
||
title="发布到广场"
|
||
width="500px"
|
||
append-to-body
|
||
align-center
|
||
>
|
||
<el-form label-position="top">
|
||
<el-form-item label="标签 (回车添加)">
|
||
<div class="flex gap-2 flex-wrap w-full p-2 border border-gray-200 rounded-md min-h-[40px]">
|
||
<el-tag
|
||
v-for="tag in publishTags"
|
||
:key="tag"
|
||
closable
|
||
:disable-transitions="false"
|
||
@close="handleCloseTag(tag)"
|
||
>
|
||
{{ tag }}
|
||
</el-tag>
|
||
<el-input
|
||
v-if="inputVisible"
|
||
ref="InputRef"
|
||
v-model="inputValue"
|
||
class="w-24"
|
||
size="small"
|
||
@keydown.enter.prevent="handleInputConfirm"
|
||
@blur="handleInputConfirm"
|
||
/>
|
||
<el-button v-else-if="publishTags.length < 3" class="button-new-tag" size="small" @click="showInput">
|
||
+ New Tag
|
||
</el-button>
|
||
<span v-else class="text-xs text-gray-400">最多3个标签</span>
|
||
</div>
|
||
</el-form-item>
|
||
<el-form-item>
|
||
<el-checkbox v-model="isAnonymousPublish" label="匿名发布" />
|
||
</el-form-item>
|
||
</el-form>
|
||
<template #footer>
|
||
<span class="dialog-footer">
|
||
<el-button @click="publishDialogVisible = false">取消</el-button>
|
||
<el-button type="primary" :loading="publishing" @click="confirmPublish">
|
||
发布
|
||
</el-button>
|
||
</span>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<!-- Global Image Viewer -->
|
||
<el-image-viewer
|
||
v-if="showViewer && previewUrl"
|
||
:url-list="[previewUrl]"
|
||
@close="closeViewer"
|
||
/>
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.refresh-btn {
|
||
margin: 20px 0;
|
||
}
|
||
.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>
|