feat: add Claude AI skills for shadcn theming and image generation tools

This commit introduces comprehensive Claude AI skill configurations for:
- shadcn/ui theming with OKLCH color space support
- Gemini API integration for image generation and chat capabilities
- Batch processing and multi-turn conversation features
- File handling utilities for image processing workflows
This commit is contained in:
2026-03-18 02:56:05 +08:00
parent 69e96412ff
commit 791a523101
16 changed files with 1967 additions and 796 deletions

View File

@@ -0,0 +1,130 @@
<template>
<div class="task-page">
<!-- 筛选条件区域 -->
<div v-if="$slots.filters" class="task-page__filters">
<slot name="filters" />
</div>
<!-- 任务列表内容区域 -->
<div class="task-page__content">
<!-- 批量操作栏 -->
<div v-if="$slots['batch-actions']" class="batch-actions">
<slot name="batch-actions" />
</div>
<!-- 表格区域 -->
<div class="task-page__table">
<!-- 加载状态 -->
<div v-if="loading" class="task-page__loading">
<Spinner class="size-8" />
</div>
<!-- 表格内容 -->
<div class="task-page__table-wrapper">
<slot name="table" />
</div>
<!-- 分页 -->
<TablePagination
v-if="showPagination && total > 0"
:current="current"
:page-size="pageSize"
:total="total"
@change="$emit('page-change', $event)"
/>
</div>
</div>
<!-- 弹窗插槽 -->
<slot name="modals" />
</div>
</template>
<script setup>
import { Spinner } from '@/components/ui/spinner'
import { TablePagination } from '@/components/ui/pagination'
defineProps({
loading: {
type: Boolean,
default: false
},
showPagination: {
type: Boolean,
default: true
},
current: {
type: Number,
default: 1
},
pageSize: {
type: Number,
default: 10
},
total: {
type: Number,
default: 0
}
})
defineEmits(['page-change'])
</script>
<style scoped lang="less">
.task-page {
padding: 0;
height: 100%;
display: flex;
flex-direction: column;
gap: var(--space-4);
}
.task-page__filters {
flex-shrink: 0;
padding: var(--space-5);
background: var(--card);
border-radius: var(--radius-lg);
border: 1px solid var(--border);
}
.task-page__content {
flex: 1;
overflow: hidden;
background: var(--card);
border-radius: var(--radius-lg);
border: 1px solid var(--border);
padding: var(--space-5);
display: flex;
flex-direction: column;
}
.batch-actions {
flex-shrink: 0;
margin-bottom: var(--space-4);
}
.task-page__table {
flex: 1;
display: flex;
flex-direction: column;
position: relative;
min-height: 0;
}
.task-page__loading {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
background: var(--background);
opacity: 0.5;
z-index: 10;
}
.task-page__table-wrapper {
flex: 1;
overflow: auto;
min-height: 0;
}
</style>

View File

@@ -1,7 +1,13 @@
<template>
<div class="task-page">
<TaskPageLayout
:loading="loading"
:current="paginationConfig.current"
:page-size="paginationConfig.pageSize"
:total="paginationConfig.total"
@page-change="handlePageChange"
>
<!-- 筛选条件 -->
<div class="task-page__filters">
<template #filters>
<div class="flex flex-wrap items-center gap-3">
<!-- 状态筛选 -->
<Select v-model="filters.status" @update:model-value="handleFilterChange">
@@ -60,148 +66,135 @@
重置
</Button>
</div>
</div>
</template>
<!-- 任务列表 -->
<div class="task-page__content">
<!-- 批量操作栏 -->
<div v-if="selectedRowKeys.length > 0" class="batch-actions">
<Alert class="flex items-center justify-between">
<div class="flex items-center gap-2">
<Icon icon="lucide:info" class="size-4" />
<span>已选中 {{ selectedRowKeys.length }} </span>
</div>
<Button variant="destructive" size="sm" @click="confirmBatchDelete">
<Icon icon="lucide:trash-2" class="mr-1 size-4" />
批量删除
</Button>
</Alert>
</div>
<div class="relative">
<div v-if="loading" class="absolute inset-0 flex items-center justify-center bg-background/50 z-10">
<Spinner class="size-8" />
<!-- 批量操作栏 -->
<template #batch-actions>
<Alert v-if="selectedRowKeys.length > 0" class="flex items-center justify-between">
<div class="flex items-center gap-2">
<Icon icon="lucide:info" class="size-4" />
<span>已选中 {{ selectedRowKeys.length }} </span>
</div>
<Button variant="destructive" size="sm" @click="confirmBatchDelete">
<Icon icon="lucide:trash-2" class="mr-1 size-4" />
批量删除
</Button>
</Alert>
</template>
<div class="overflow-auto">
<Table>
<TableHeader>
<TableRow>
<TableHead class="w-[50px]">
<Checkbox
:checked="isAllSelected"
@update:checked="handleSelectAll"
/>
</TableHead>
<TableHead class="w-[80px]">ID</TableHead>
<TableHead class="min-w-[200px]">任务名称</TableHead>
<TableHead class="w-[100px]">状态</TableHead>
<TableHead class="w-[150px]">进度</TableHead>
<TableHead class="w-[180px]">创建时间</TableHead>
<TableHead class="w-[180px] sticky right-0 bg-background">操作</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow v-for="record in list" :key="record.id">
<TableCell>
<Checkbox
:checked="selectedRowKeys.includes(record.id)"
@update:checked="handleSelectRow(record.id)"
/>
</TableCell>
<TableCell>{{ record.id }}</TableCell>
<TableCell>
<span class="font-medium">{{ record.taskName }}</span>
</TableCell>
<TableCell>
<TaskStatusTag :status="record.status" />
</TableCell>
<TableCell>
<div class="w-[120px]">
<Progress :model-value="record.progress" class="h-2" />
</div>
</TableCell>
<TableCell>{{ formatDate(record.createTime) }}</TableCell>
<TableCell class="sticky right-0 bg-background">
<div class="flex items-center gap-2">
<Button
v-if="isStatus(record.status, 'success')"
variant="ghost"
size="sm"
class="h-7 px-2 text-primary hover:text-primary/80"
@click="openVideoUrl(record)"
>
<Icon icon="lucide:play-circle" class="mr-1 size-4" />
预览
</Button>
<Button
v-if="isStatus(record.status, 'success')"
variant="ghost"
size="sm"
class="h-7 px-2 text-success hover:text-success/80"
@click="openVideoUrl(record)"
>
<Icon icon="lucide:download" class="mr-1 size-4" />
下载
</Button>
<Button
v-if="isStatus(record.status, 'running')"
variant="ghost"
size="sm"
class="h-7 px-2"
@click="handleCancel(record.id)"
>
取消
</Button>
<Button
variant="ghost"
size="sm"
class="h-7 px-2 text-destructive hover:text-destructive/80 hover:bg-destructive/10"
@click="handleDelete(record.id)"
>
删除
</Button>
</div>
</TableCell>
</TableRow>
<TableRow v-if="list.length === 0 && !loading">
<TableCell colspan="7" class="h-32 text-center text-muted-foreground">
暂无数据
</TableCell>
</TableRow>
</TableBody>
</Table>
</div>
<!-- 表格 -->
<template #table>
<Table>
<TableHeader>
<TableRow>
<TableHead class="w-[50px]">
<Checkbox
:checked="isAllSelected"
@update:checked="handleSelectAll"
/>
</TableHead>
<TableHead class="w-[80px]">ID</TableHead>
<TableHead class="min-w-[200px]">任务名称</TableHead>
<TableHead class="w-[100px]">状态</TableHead>
<TableHead class="w-[150px]">进度</TableHead>
<TableHead class="w-[180px]">创建时间</TableHead>
<TableHead class="w-[180px] sticky right-0 bg-background">操作</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow v-for="record in list" :key="record.id">
<TableCell>
<Checkbox
:checked="selectedRowKeys.includes(record.id)"
@update:checked="handleSelectRow(record.id)"
/>
</TableCell>
<TableCell>{{ record.id }}</TableCell>
<TableCell>
<span class="font-medium">{{ record.taskName }}</span>
</TableCell>
<TableCell>
<TaskStatusTag :status="record.status" />
</TableCell>
<TableCell>
<div class="w-[120px]">
<Progress :model-value="record.progress" class="h-2" />
</div>
</TableCell>
<TableCell>{{ formatDate(record.createTime) }}</TableCell>
<TableCell class="sticky right-0 bg-background">
<div class="flex items-center gap-2">
<Button
v-if="isStatus(record.status, 'success')"
variant="ghost"
size="sm"
class="h-7 px-2 text-primary hover:text-primary/80"
@click="openVideoUrl(record)"
>
<Icon icon="lucide:play-circle" class="mr-1 size-4" />
预览
</Button>
<Button
v-if="isStatus(record.status, 'success')"
variant="ghost"
size="sm"
class="h-7 px-2 text-success hover:text-success/80"
@click="openVideoUrl(record)"
>
<Icon icon="lucide:download" class="mr-1 size-4" />
下载
</Button>
<Button
v-if="isStatus(record.status, 'running')"
variant="ghost"
size="sm"
class="h-7 px-2"
@click="handleCancel(record.id)"
>
取消
</Button>
<Button
variant="ghost"
size="sm"
class="h-7 px-2 text-destructive hover:text-destructive/80 hover:bg-destructive/10"
@click="handleDelete(record.id)"
>
删除
</Button>
</div>
</TableCell>
</TableRow>
<TableRow v-if="list.length === 0 && !loading">
<TableCell colspan="7" class="h-32 text-center text-muted-foreground">
暂无数据
</TableCell>
</TableRow>
</TableBody>
</Table>
</template>
<!-- 分页 -->
<TablePagination
:current="paginationConfig.current"
:page-size="paginationConfig.pageSize"
:total="paginationConfig.total"
@change="handlePageChange"
/>
</div>
</div>
<!-- 确认删除对话框 -->
<AlertDialog :open="deleteDialogOpen" @update:open="deleteDialogOpen = $event">
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>确认删除</AlertDialogTitle>
<AlertDialogDescription>
确定要删除选中的任务吗此操作无法撤销
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel @click="deleteDialogOpen = false">取消</AlertDialogCancel>
<AlertDialogAction @click="handleBatchDelete" :disabled="deleteLoading">
<Spinner v-if="deleteLoading" class="mr-2 size-4" />
确认删除
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
<!-- 弹窗 -->
<template #modals>
<!-- 确认删除对话框 -->
<AlertDialog :open="deleteDialogOpen" @update:open="deleteDialogOpen = $event">
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>确认删除</AlertDialogTitle>
<AlertDialogDescription>
确定要删除选中的任务吗此操作无法撤销
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel @click="deleteDialogOpen = false">取消</AlertDialogCancel>
<AlertDialogAction @click="handleBatchDelete" :disabled="deleteLoading">
<Spinner v-if="deleteLoading" class="mr-2 size-4" />
确认删除
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</template>
</TaskPageLayout>
</template>
<script setup>
@@ -218,7 +211,6 @@ import { Progress } from '@/components/ui/progress'
import { Alert } from '@/components/ui/alert'
import { Spinner } from '@/components/ui/spinner'
import { Checkbox } from '@/components/ui/checkbox'
import { TablePagination } from '@/components/ui/pagination'
import {
AlertDialog,
AlertDialogAction,
@@ -235,27 +227,20 @@ import { useTaskList } from '@/views/system/task-management/composables/useTaskL
import { useTaskOperations } from '@/views/system/task-management/composables/useTaskOperations'
import { useTaskPolling } from '@/views/system/task-management/composables/useTaskPolling'
import TaskStatusTag from '@/views/system/task-management/components/TaskStatusTag.vue'
import TaskPageLayout from '@/views/system/task-management/components/TaskPageLayout.vue'
// 日期选择器开关
const datePickerOpen = ref(false)
const selectedDateRange = ref(null)
// Composables
const { loading, list, filters, paginationConfig, fetchList, handleFilterChange, handleResetFilters, handleTableChange } = useTaskList(getDigitalHumanTaskPage)
const { loading, list, filters, paginationConfig, fetchList, handleFilterChange, handleResetFilters } = useTaskList(getDigitalHumanTaskPage)
// 初始化 filters.status 为 'all'
if (!filters.status) {
filters.status = 'all'
}
// 包装 handleFilterChange 处理 'all' 值
const wrappedHandleFilterChange = () => {
const params = { ...filters }
if (params.status === 'all') {
params.status = undefined
}
handleFilterChange()
}
const { handleDelete: deleteTaskById, handleCancel } = useTaskOperations({ deleteApi: deleteTask, cancelApi: cancelTask }, fetchList)
useTaskPolling(getDigitalHumanTaskPage, { onTaskUpdate: fetchList })
@@ -338,14 +323,14 @@ const handleDelete = (id) => {
// 处理日期选择
const handleDateSelect = (value) => {
if (value?.start && value?.end) {
const formatDate = (date) => {
const formatDateStr = (date) => {
const d = new Date(date)
const year = d.getFullYear()
const month = String(d.getMonth() + 1).padStart(2, '0')
const day = String(d.getDate()).padStart(2, '0')
return `${year}-${month}-${day}`
}
filters.dateRange = [formatDate(value.start), formatDate(value.end)]
filters.dateRange = [formatDateStr(value.start), formatDateStr(value.end)]
datePickerOpen.value = false
handleFilterChange()
}
@@ -355,34 +340,4 @@ onMounted(fetchList)
</script>
<style scoped lang="less">
.task-page {
padding: 0;
height: 100%;
display: flex;
flex-direction: column;
gap: var(--space-4);
}
.task-page__filters {
flex-shrink: 0;
padding: var(--space-5);
background: var(--card);
border-radius: var(--radius-lg);
border: 1px solid var(--border);
}
.task-page__content {
flex: 1;
overflow: hidden;
background: var(--card);
border-radius: var(--radius-lg);
border: 1px solid var(--border);
padding: var(--space-5);
display: flex;
flex-direction: column;
}
.batch-actions {
margin-bottom: var(--space-4);
}
</style>

View File

@@ -1,5 +1,5 @@
<template>
<div class="task-layout">
<div class="task-layout p-4">
<!-- 顶部Tab栏 -->
<div class="task-layout__header">
<div class="flex items-center justify-between">
@@ -59,7 +59,7 @@ const NAV_ITEMS = [
type: 'style-task',
label: '风格任务',
icon: 'lucide:palette',
component: markRaw(defineAsyncComponent(() => import('../../../task-center/BenchmarkTaskList.vue')))
component: markRaw(defineAsyncComponent(() => import('../task-center/BenchmarkTaskList.vue')))
}
]

View File

@@ -1,7 +1,13 @@
<template>
<div class="task-page">
<TaskPageLayout
:loading="loading"
:current="paginationConfig.current"
:page-size="paginationConfig.pageSize"
:total="paginationConfig.total"
@page-change="handlePageChange"
>
<!-- 筛选条件 -->
<div class="task-page__filters">
<template #filters>
<div class="flex flex-wrap items-center gap-3">
<!-- 状态筛选 -->
<Select v-model="filters.status" @update:model-value="handleFilterChange">
@@ -59,246 +65,236 @@
重置
</Button>
</div>
</div>
</template>
<!-- 任务列表 -->
<div class="task-page__content">
<!-- 批量操作栏 -->
<div v-if="selectedRowKeys.length > 0" class="batch-actions">
<Alert class="flex items-center justify-between">
<div class="flex items-center gap-2">
<Icon icon="lucide:info" class="size-4" />
<span>已选中 {{ selectedRowKeys.length }} </span>
</div>
<div class="flex items-center gap-2">
<Button
:disabled="!hasDownloadableSelected"
:loading="batchDownloading"
size="sm"
@click="handleBatchDownloadSelected"
>
<Icon icon="lucide:download" class="mr-1 size-4" />
批量下载 ({{ downloadableCount }})
</Button>
<Button variant="destructive" size="sm" @click="handleBatchDeleteSelected">
<Icon icon="lucide:trash-2" class="mr-1 size-4" />
批量删除
</Button>
</div>
</Alert>
</div>
<div class="relative min-h-[200px]">
<Spinner v-if="loading" class="absolute inset-0 z-10 m-auto size-8" />
<div class="overflow-auto">
<Table>
<TableHeader>
<TableRow>
<TableHead class="w-[50px]">
<Checkbox
:checked="isAllSelected"
@update:checked="handleSelectAll"
/>
</TableHead>
<TableHead class="w-[70px]">ID</TableHead>
<TableHead>标题</TableHead>
<TableHead class="w-[90px]">状态</TableHead>
<TableHead class="w-[100px]">生成结果</TableHead>
<TableHead class="w-[160px]">创建时间</TableHead>
<TableHead class="w-[160px]">完成时间</TableHead>
<TableHead class="w-[240px] sticky right-0 bg-background">操作</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<template v-for="record in list" :key="record.id">
<TableRow
class="cursor-pointer hover:bg-muted/50"
@click="toggleExpand(record.id)"
>
<TableCell>
<Checkbox
:checked="selectedRowKeys.includes(record.id)"
@update:checked.stop="handleSelectRow(record.id)"
/>
</TableCell>
<TableCell>{{ record.id }}</TableCell>
<TableCell>
<div class="flex items-center gap-2">
<Icon
:icon="expandedRowKeys.includes(record.id) ? 'lucide:chevron-down' : 'lucide:chevron-right'"
class="size-4 text-muted-foreground"
/>
<span class="font-medium">{{ record.title }}</span>
<Badge v-if="record.text" variant="secondary" class="text-xs">有文案</Badge>
</div>
</TableCell>
<TableCell>
<TaskStatusTag :status="record.status" />
</TableCell>
<TableCell>
<Badge v-if="record.outputUrls?.length" variant="success">
{{ record.outputUrls.length }} 个视频
</Badge>
<span v-else class="text-muted-foreground">-</span>
</TableCell>
<TableCell>{{ formatDate(record.createTime) }}</TableCell>
<TableCell>{{ record.finishTime ? formatDate(record.finishTime) : '-' }}</TableCell>
<TableCell @click.stop>
<div class="flex items-center gap-1">
<Button
v-if="canOperate(record, 'preview')"
variant="ghost"
size="sm"
class="h-7 px-2 text-primary hover:text-primary/80"
@click="openPreview(record)"
>
<Icon icon="lucide:play-circle" class="mr-1 size-4" />
预览
</Button>
<Button
v-if="canOperate(record, 'download')"
variant="ghost"
size="sm"
class="h-7 px-2 text-success hover:text-success/80"
@click="handleDownload(record)"
>
<Icon icon="lucide:download" class="mr-1 size-4" />
下载
</Button>
<Button
v-if="canOperate(record, 'cancel')"
variant="outline"
size="sm"
class="h-7 px-2"
@click="handleCancel(record.id)"
>
取消
</Button>
<Button
v-if="canOperate(record, 'retry')"
variant="outline"
size="sm"
class="h-7 px-2"
@click="handleRetry(record.id)"
>
重试
</Button>
<Button
variant="ghost"
size="sm"
class="h-7 px-2 text-destructive hover:text-destructive/80 hover:bg-destructive/10"
@click="handleDeleteClick(record.id)"
>
删除
</Button>
</div>
</TableCell>
</TableRow>
<!-- 展开行 -->
<TableRow v-if="expandedRowKeys.includes(record.id)" class="bg-muted/30">
<TableCell colspan="8" class="p-0">
<div class="expanded-content">
<div v-if="record.text" class="task-text">
<strong>文案内容</strong>
<p>{{ record.text }}</p>
</div>
<div v-if="record.outputUrls?.length" class="task-results">
<div class="result-header">
<strong>生成结果</strong>
<span class="result-count">{{ record.outputUrls.length }} 个视频</span>
</div>
<div class="result-list">
<div v-for="(_, index) in record.outputUrls" :key="index" class="result-item">
<Button
v-if="isStatus(record.status, 'success')"
variant="ghost"
size="sm"
class="h-7 px-2"
@click="previewVideo(record, index)"
>
<Icon icon="lucide:play-circle" class="mr-1 size-4" />
视频 {{ index + 1 }}
</Button>
<Button
v-if="isStatus(record.status, 'success')"
variant="ghost"
size="sm"
class="h-7 px-2"
@click="downloadVideo(record.id, index)"
>
<Icon icon="lucide:download" class="size-4" />
</Button>
<span v-else class="text-muted-foreground text-sm">视频 {{ index + 1 }} (处理中...)</span>
</div>
</div>
</div>
<Alert v-if="record.errorMsg" variant="destructive" class="mt-3">
<Icon icon="lucide:alert-circle" class="size-4" />
<AlertDescription>{{ record.errorMsg }}</AlertDescription>
</Alert>
</div>
</TableCell>
</TableRow>
</template>
<TableRow v-if="list.length === 0 && !loading">
<TableCell colspan="8" class="h-32 text-center text-muted-foreground">
暂无数据
</TableCell>
</TableRow>
</TableBody>
</Table>
<!-- 批量操作栏 -->
<template #batch-actions>
<Alert v-if="selectedRowKeys.length > 0" class="flex items-center justify-between">
<div class="flex items-center gap-2">
<Icon icon="lucide:info" class="size-4" />
<span>已选中 {{ selectedRowKeys.length }} </span>
</div>
<!-- 分页 -->
<TablePagination
:current="paginationConfig.current"
:page-size="paginationConfig.pageSize"
:total="paginationConfig.total"
@change="handlePageChange"
/>
</div>
</div>
<!-- 预览模态框 -->
<Dialog v-model:open="preview.visible">
<DialogContent class="max-w-[800px]">
<DialogHeader>
<DialogTitle>{{ preview.title }}</DialogTitle>
</DialogHeader>
<div v-if="preview.url" class="preview-container">
<video :src="preview.url" controls autoplay class="preview-video">
您的浏览器不支持视频播放
</video>
</div>
<div v-else class="preview-loading">
<Spinner class="size-6" />
<span class="text-muted-foreground mt-2">正在加载预览...</span>
</div>
</DialogContent>
</Dialog>
<!-- 删除确认对话框 -->
<AlertDialog v-model:open="deleteDialogOpen">
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>确认批量删除</AlertDialogTitle>
<AlertDialogDescription>
确定要删除选中的 {{ selectedRowKeys.length }} 个任务吗删除后无法恢复
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>取消</AlertDialogCancel>
<AlertDialogAction
:disabled="deleteLoading"
class="bg-destructive hover:bg-destructive/90"
@click="confirmBatchDelete"
<div class="flex items-center gap-2">
<Button
:disabled="!hasDownloadableSelected"
:loading="batchDownloading"
size="sm"
@click="handleBatchDownloadSelected"
>
确定删除
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
<Icon icon="lucide:download" class="mr-1 size-4" />
批量下载 ({{ downloadableCount }})
</Button>
<Button variant="destructive" size="sm" @click="handleBatchDeleteSelected">
<Icon icon="lucide:trash-2" class="mr-1 size-4" />
批量删除
</Button>
</div>
</Alert>
</template>
<!-- 表格 -->
<template #table>
<Table>
<TableHeader>
<TableRow>
<TableHead class="w-[50px]">
<Checkbox
:checked="isAllSelected"
@update:checked="handleSelectAll"
/>
</TableHead>
<TableHead class="w-[70px]">ID</TableHead>
<TableHead>标题</TableHead>
<TableHead class="w-[90px]">状态</TableHead>
<TableHead class="w-[100px]">生成结果</TableHead>
<TableHead class="w-[160px]">创建时间</TableHead>
<TableHead class="w-[160px]">完成时间</TableHead>
<TableHead class="w-[240px] sticky right-0 bg-background">操作</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<template v-for="record in list" :key="record.id">
<TableRow
class="cursor-pointer hover:bg-muted/50"
@click="toggleExpand(record.id)"
>
<TableCell>
<Checkbox
:checked="selectedRowKeys.includes(record.id)"
@update:checked.stop="handleSelectRow(record.id)"
/>
</TableCell>
<TableCell>{{ record.id }}</TableCell>
<TableCell>
<div class="flex items-center gap-2">
<Icon
:icon="expandedRowKeys.includes(record.id) ? 'lucide:chevron-down' : 'lucide:chevron-right'"
class="size-4 text-muted-foreground"
/>
<span class="font-medium">{{ record.title }}</span>
<Badge v-if="record.text" variant="secondary" class="text-xs">有文案</Badge>
</div>
</TableCell>
<TableCell>
<TaskStatusTag :status="record.status" />
</TableCell>
<TableCell>
<Badge v-if="record.outputUrls?.length" variant="success">
{{ record.outputUrls.length }} 个视频
</Badge>
<span v-else class="text-muted-foreground">-</span>
</TableCell>
<TableCell>{{ formatDate(record.createTime) }}</TableCell>
<TableCell>{{ record.finishTime ? formatDate(record.finishTime) : '-' }}</TableCell>
<TableCell @click.stop>
<div class="flex items-center gap-1">
<Button
v-if="canOperate(record, 'preview')"
variant="ghost"
size="sm"
class="h-7 px-2 text-primary hover:text-primary/80"
@click="openPreview(record)"
>
<Icon icon="lucide:play-circle" class="mr-1 size-4" />
预览
</Button>
<Button
v-if="canOperate(record, 'download')"
variant="ghost"
size="sm"
class="h-7 px-2 text-success hover:text-success/80"
@click="handleDownload(record)"
>
<Icon icon="lucide:download" class="mr-1 size-4" />
下载
</Button>
<Button
v-if="canOperate(record, 'cancel')"
variant="outline"
size="sm"
class="h-7 px-2"
@click="handleCancel(record.id)"
>
取消
</Button>
<Button
v-if="canOperate(record, 'retry')"
variant="outline"
size="sm"
class="h-7 px-2"
@click="handleRetry(record.id)"
>
重试
</Button>
<Button
variant="ghost"
size="sm"
class="h-7 px-2 text-destructive hover:text-destructive/80 hover:bg-destructive/10"
@click="handleDeleteClick(record.id)"
>
删除
</Button>
</div>
</TableCell>
</TableRow>
<!-- 展开行 -->
<TableRow v-if="expandedRowKeys.includes(record.id)" class="bg-muted/30">
<TableCell colspan="8" class="p-0">
<div class="expanded-content">
<div v-if="record.text" class="task-text">
<strong>文案内容</strong>
<p>{{ record.text }}</p>
</div>
<div v-if="record.outputUrls?.length" class="task-results">
<div class="result-header">
<strong>生成结果</strong>
<span class="result-count">{{ record.outputUrls.length }} 个视频</span>
</div>
<div class="result-list">
<div v-for="(_, index) in record.outputUrls" :key="index" class="result-item">
<Button
v-if="isStatus(record.status, 'success')"
variant="ghost"
size="sm"
class="h-7 px-2"
@click="previewVideo(record, index)"
>
<Icon icon="lucide:play-circle" class="mr-1 size-4" />
视频 {{ index + 1 }}
</Button>
<Button
v-if="isStatus(record.status, 'success')"
variant="ghost"
size="sm"
class="h-7 px-2"
@click="downloadVideo(record.id, index)"
>
<Icon icon="lucide:download" class="size-4" />
</Button>
<span v-else class="text-muted-foreground text-sm">视频 {{ index + 1 }} (处理中...)</span>
</div>
</div>
</div>
<Alert v-if="record.errorMsg" variant="destructive" class="mt-3">
<Icon icon="lucide:alert-circle" class="size-4" />
<AlertDescription>{{ record.errorMsg }}</AlertDescription>
</Alert>
</div>
</TableCell>
</TableRow>
</template>
<TableRow v-if="list.length === 0 && !loading">
<TableCell colspan="8" class="h-32 text-center text-muted-foreground">
暂无数据
</TableCell>
</TableRow>
</TableBody>
</Table>
</template>
<!-- 弹窗 -->
<template #modals>
<!-- 预览模态框 -->
<Dialog v-model:open="preview.visible">
<DialogContent class="max-w-[800px]">
<DialogHeader>
<DialogTitle>{{ preview.title }}</DialogTitle>
</DialogHeader>
<div v-if="preview.url" class="preview-container">
<video :src="preview.url" controls autoplay class="preview-video">
您的浏览器不支持视频播放
</video>
</div>
<div v-else class="preview-loading">
<Spinner class="size-6" />
<span class="text-muted-foreground mt-2">正在加载预览...</span>
</div>
</DialogContent>
</Dialog>
<!-- 删除确认对话框 -->
<AlertDialog v-model:open="deleteDialogOpen">
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>确认批量删除</AlertDialogTitle>
<AlertDialogDescription>
确定要删除选中的 {{ selectedRowKeys.length }} 个任务吗删除后无法恢复
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>取消</AlertDialogCancel>
<AlertDialogAction
:disabled="deleteLoading"
class="bg-destructive hover:bg-destructive/90"
@click="confirmBatchDelete"
>
确定删除
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</template>
</TaskPageLayout>
</template>
<script setup>
@@ -331,26 +327,26 @@ import {
AlertDialogHeader,
AlertDialogTitle
} from '@/components/ui/alert-dialog'
import { TablePagination } from '@/components/ui/pagination'
import { MixTaskService } from '@/api/mixTask'
import { formatDate } from '@/utils/file'
import { useTaskList } from '@/views/system/task-management/composables/useTaskList'
import { useTaskOperations } from '@/views/system/task-management/composables/useTaskOperations'
import { useTaskPolling } from '@/views/system/task-management/composables/useTaskPolling'
import TaskStatusTag from '@/views/system/task-management/components/TaskStatusTag.vue'
import TaskPageLayout from '@/views/system/task-management/components/TaskPageLayout.vue'
// 日期选择器开关
const datePickerOpen = ref(false)
const selectedDateRange = ref(null)
// Composables
const { loading, list, filters, paginationConfig, fetchList, handleFilterChange, handleResetFilters, handleTableChange } = useTaskList(MixTaskService.getTaskPage)
const { loading, list, filters, paginationConfig, fetchList, handleFilterChange, handleResetFilters } = useTaskList(MixTaskService.getTaskPage)
// 初始化 filters.status 为 'all'
if (!filters.status) {
filters.status = 'all'
}
const { handleDelete, handleCancel, handleRetry, handleBatchDownload } = useTaskOperations(
const { handleCancel, handleRetry, handleBatchDownload } = useTaskOperations(
{ deleteApi: MixTaskService.deleteTask, cancelApi: MixTaskService.cancelTask, retryApi: MixTaskService.retryTask, getSignedUrlsApi: MixTaskService.getSignedUrls },
fetchList
)
@@ -555,14 +551,14 @@ const handleDownload = (record) => {
// 处理日期选择
const handleDateSelect = (value) => {
if (value?.start && value?.end) {
const formatDate = (date) => {
const formatDateStr = (date) => {
const d = new Date(date)
const year = d.getFullYear()
const month = String(d.getMonth() + 1).padStart(2, '0')
const day = String(d.getDate()).padStart(2, '0')
return `${year}-${month}-${day}`
}
filters.dateRange = [formatDate(value.start), formatDate(value.end)]
filters.dateRange = [formatDateStr(value.start), formatDateStr(value.end)]
datePickerOpen.value = false
handleFilterChange()
}
@@ -578,34 +574,6 @@ onMounted(fetchList)
</script>
<style scoped lang="less">
.task-page {
padding: var(--space-4);
height: 100%;
display: flex;
flex-direction: column;
gap: var(--space-4);
}
.task-page__filters {
padding: var(--space-4);
background: var(--color-bg-card);
border-radius: var(--radius-lg);
}
.task-page__content {
flex: 1;
overflow: hidden;
background: var(--color-bg-card);
border-radius: var(--radius-lg);
padding: var(--space-4);
display: flex;
flex-direction: column;
}
.batch-actions {
margin-bottom: var(--space-4);
}
.expanded-content {
padding: var(--space-5);
background: var(--muted);

View File

@@ -0,0 +1,287 @@
<template>
<TaskPageLayout
:loading="loading"
:current="pagination.current"
:page-size="pagination.pageSize"
:total="pagination.total"
@page-change="handlePageChange"
>
<!-- 筛选条件 -->
<template #filters>
<div class="flex flex-wrap items-center gap-3">
<Select v-model="filterStatus" @update:model-value="handleFilterChange">
<SelectTrigger class="w-[150px]">
<SelectValue placeholder="全部状态" />
</SelectTrigger>
<SelectContent>
<SelectItem :value="null">全部状态</SelectItem>
<SelectItem :value="0">待处理</SelectItem>
<SelectItem :value="1">处理中</SelectItem>
<SelectItem :value="2">成功</SelectItem>
<SelectItem :value="3">失败</SelectItem>
</SelectContent>
</Select>
<Button @click="handleRefresh" :disabled="loading">
<Icon v-if="loading" icon="lucide:loader-2" class="size-4 animate-spin" />
<Icon v-else icon="lucide:refresh-cw" class="size-4" />
刷新
</Button>
</div>
</template>
<!-- 表格 -->
<template #table>
<Table>
<TableHeader>
<TableRow>
<TableHead class="min-w-[200px]">任务名称</TableHead>
<TableHead class="w-[100px]">视频数量</TableHead>
<TableHead class="w-[100px]">状态</TableHead>
<TableHead class="w-[150px]">进度</TableHead>
<TableHead class="w-[180px]">创建时间</TableHead>
<TableHead class="w-[180px]">操作</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow v-for="record in taskList" :key="record.id">
<TableCell class="font-medium">
<span class="truncate block max-w-[200px]" :title="record.taskName">
{{ record.taskName }}
</span>
</TableCell>
<TableCell>{{ record.videoCount }}</TableCell>
<TableCell>
<Badge :variant="getStatusVariant(record.status)" :class="STATUS_MAP.class[record.status]">
{{ getStatusText(record.status) }}
</Badge>
</TableCell>
<TableCell>
<Progress :model-value="record.progress" class="h-2" />
</TableCell>
<TableCell class="text-muted-foreground">
{{ formatTime(record.createTime) }}
</TableCell>
<TableCell>
<div class="flex gap-1">
<Button
v-if="record.status === 2 && record.generatedPrompt"
variant="link"
size="sm"
class="h-auto p-0"
@click="handleViewPrompt(record)"
>
查看
</Button>
<Button
v-if="record.status === 2 && record.generatedPrompt"
variant="link"
size="sm"
class="h-auto p-0"
@click="handleCopyPrompt(record)"
>
复制
</Button>
<AlertDialog v-if="record.status !== 1">
<AlertDialogTrigger as-child>
<Button variant="link" size="sm" class="h-auto p-0 text-destructive">
删除
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>确认删除</AlertDialogTitle>
<AlertDialogDescription>
确定要删除这个任务吗此操作无法撤销
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>取消</AlertDialogCancel>
<AlertDialogAction @click="handleDelete(record)">确认删除</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
</TableCell>
</TableRow>
<TableRow v-if="taskList.length === 0 && !loading">
<TableCell colspan="6" class="h-32 text-center text-muted-foreground">
暂无任务数据
</TableCell>
</TableRow>
</TableBody>
</Table>
</template>
<!-- 弹窗 -->
<template #modals>
<!-- 提示词弹窗 -->
<Dialog v-model:open="promptModalVisible">
<DialogContent class="sm:max-w-[700px]">
<DialogHeader>
<DialogTitle>生成的提示词</DialogTitle>
</DialogHeader>
<div class="prompt-content">{{ currentPrompt }}</div>
<DialogFooter>
<Button @click="handleCopyCurrentPrompt">
<Icon icon="lucide:copy" class="size-4" />
复制到剪贴板
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</template>
</TaskPageLayout>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { toast } from 'vue-sonner'
import { Icon } from '@iconify/vue'
import dayjs from 'dayjs'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { Progress } from '@/components/ui/progress'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow
} from '@/components/ui/table'
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle
} from '@/components/ui/dialog'
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger
} from '@/components/ui/alert-dialog'
import { BenchmarkTaskApi } from '@/api/benchmarkTask'
import { copyToClipboard } from '@/utils/clipboard'
import TaskPageLayout from '@/views/system/task-management/components/TaskPageLayout.vue'
const loading = ref(false)
const taskList = ref([])
const filterStatus = ref(null)
const promptModalVisible = ref(false)
const currentPrompt = ref('')
const pagination = reactive({
current: 1,
pageSize: 10,
total: 0
})
const STATUS_MAP = {
// 0: 待处理(灰), 1: 处理中(蓝), 2: 成功(绿), 3: 失败(红)
variant: { 0: 'secondary', 1: 'default', 2: 'outline', 3: 'destructive' },
class: {
2: 'border-green-500 text-green-600 bg-green-50'
},
text: { 0: '待处理', 1: '处理中', 2: '成功', 3: '失败' }
}
function formatTime(time) {
if (!time) return '-'
return dayjs(time).format('YYYY-MM-DD HH:mm:ss')
}
async function loadTaskList() {
loading.value = true
try {
const response = await BenchmarkTaskApi.getTaskPage({
pageNo: pagination.current,
pageSize: pagination.pageSize,
status: filterStatus.value,
})
if (response?.data) {
taskList.value = response.data.list || []
pagination.total = response.data.total || 0
}
} catch (error) {
console.error('加载失败:', error)
toast.error('加载任务列表失败')
} finally {
loading.value = false
}
}
function handleRefresh() { loadTaskList() }
function handleFilterChange() {
pagination.current = 1
loadTaskList()
}
function handlePageChange(page) {
pagination.current = page
loadTaskList()
}
function getStatusVariant(status) {
return STATUS_MAP.variant[status] || 'secondary'
}
function getStatusText(status) {
return STATUS_MAP.text[status] || '未知'
}
function handleViewPrompt(record) {
currentPrompt.value = record.generatedPrompt
promptModalVisible.value = true
}
async function copyPromptText(text) {
const success = await copyToClipboard(text)
toast[success ? 'success' : 'error'](success ? '已复制' : '复制失败')
}
function handleCopyPrompt(record) {
copyPromptText(record.generatedPrompt)
}
function handleCopyCurrentPrompt() {
copyPromptText(currentPrompt.value)
}
async function handleDelete(record) {
try {
await BenchmarkTaskApi.deleteTask(record.id)
toast.success('删除成功')
loadTaskList()
} catch {
toast.error('删除失败')
}
}
onMounted(() => {
loadTaskList()
})
</script>
<style scoped lang="less">
.prompt-content {
padding: var(--space-4);
background: var(--muted);
border-radius: var(--radius);
white-space: pre-wrap;
max-height: 400px;
overflow-y: auto;
font-size: var(--font-size-base);
line-height: 1.6;
}
</style>