Files
Yi.Framework/Yi.Ai.Vue3/src/pages/chat/image/components/MyImages.vue
2026-01-04 21:58:11 +08:00

931 lines
30 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>