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

@@ -12,7 +12,7 @@ import SidebarNav from '@/components/SidebarNav.vue'
<TopNav />
<div class="flex flex-1 pt-[70px]">
<SidebarNav />
<main class="flex-1 h-[calc(100vh-70px)] overflow-auto p-4">
<main class="flex-1 h-[calc(100vh-70px)] overflow-auto">
<RouterView v-slot="{ Component }">
<keep-alive>
<component :is="Component" />

View File

@@ -341,6 +341,28 @@
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
/* 品牌色阶 */
--color-primary-50: var(--color-primary-50);
--color-primary-100: var(--color-primary-100);
--color-primary-200: var(--color-primary-200);
--color-primary-300: var(--color-primary-300);
--color-primary-400: var(--color-primary-400);
--color-primary-500: var(--color-primary-500);
--color-primary-600: var(--color-primary-600);
--color-primary-700: var(--color-primary-700);
/* 灰色系 */
--color-gray-50: var(--color-gray-50);
--color-gray-100: var(--color-gray-100);
--color-gray-200: var(--color-gray-200);
--color-gray-300: var(--color-gray-300);
--color-gray-400: var(--color-gray-400);
--color-gray-500: var(--color-gray-500);
--color-gray-600: var(--color-gray-600);
--color-gray-700: var(--color-gray-700);
--color-gray-800: var(--color-gray-800);
--color-gray-900: var(--color-gray-900);
--font-sans: var(--font-sans);
--font-mono: var(--font-mono);
--font-serif: var(--font-serif);

View File

@@ -43,7 +43,7 @@ function handleReset() {
</script>
<template>
<Card class="border-0 shadow-sm bg-white/80 backdrop-blur-sm">
<Card class="border-0 shadow-sm bg-white/80 backdrop-blur-sm p-0">
<CardContent class="p-5 space-y-5">
<!-- 平台选择 -->
<div class="space-y-2">
@@ -66,28 +66,6 @@ function handleReset() {
/>
</div>
<!-- 排序方式 -->
<div class="space-y-2">
<Label class="text-sm font-medium text-gray-700">排序方式</Label>
<RadioGroup v-model="form.sort_type" class="flex gap-2">
<div class="inline-flex items-center justify-center px-4 h-9 text-sm font-medium rounded-lg cursor-pointer transition-all"
:class="form.sort_type === 0 ? 'bg-primary text-primary-foreground' : 'bg-muted text-muted-foreground hover:bg-muted/80'">
<RadioGroupItem :value="0" id="sort-default" class="hidden" />
<label for="sort-default" class="cursor-pointer">综合排序</label>
</div>
<div class="inline-flex items-center justify-center px-4 h-9 text-sm font-medium rounded-lg cursor-pointer transition-all"
:class="form.sort_type === 1 ? 'bg-primary text-primary-foreground' : 'bg-muted text-muted-foreground hover:bg-muted/80'">
<RadioGroupItem :value="1" id="sort-likes" class="hidden" />
<label for="sort-likes" class="cursor-pointer">最多点赞</label>
</div>
<div class="inline-flex items-center justify-center px-4 h-9 text-sm font-medium rounded-lg cursor-pointer transition-all"
:class="form.sort_type === 2 ? 'bg-primary text-primary-foreground' : 'bg-muted text-muted-foreground hover:bg-muted/80'">
<RadioGroupItem :value="2" id="sort-latest" class="hidden" />
<label for="sort-latest" class="cursor-pointer">最新发布</label>
</div>
</RadioGroup>
</div>
<!-- 分析数量 -->
<div class="space-y-2">
<Label class="text-sm font-medium text-gray-700">分析数量</Label>

View File

@@ -46,6 +46,12 @@ const isAllSelected = computed(() => {
return props.data.length > 0 && props.selectedRowKeys.length === props.data.length
})
// 半选状态(部分选中)
const isIndeterminate = computed(() => {
const selectedLen = props.selectedRowKeys.length
return selectedLen > 0 && selectedLen < props.data.length
})
// 切换排序
function handleSort(key) {
if (sortKey.value === key) {
@@ -59,10 +65,10 @@ function handleSort(key) {
// 选择切换
function handleSelectAll(checked) {
if (isAllSelected.value) {
emit('update:selectedRowKeys', [])
} else {
if (checked) {
emit('update:selectedRowKeys', props.data.map(item => String(item.id)))
} else {
emit('update:selectedRowKeys', [])
}
}
@@ -133,6 +139,7 @@ function formatNumber(value) {
<div class="flex items-center gap-2">
<Checkbox
:checked="isAllSelected"
:indeterminate="isIndeterminate"
@update:checked="handleSelectAll"
class="scale-110"
/>

View File

@@ -3,7 +3,6 @@ import { ref, reactive, computed, onMounted } from 'vue'
import { toast } from 'vue-sonner'
import { Icon } from '@iconify/vue'
import dayjs from 'dayjs'
import BasicLayout from '@/layouts/components/BasicLayout.vue'
import { MaterialService } from '@/api/material'
import { VoiceService } from '@/api/voice'
import { useUpload } from '@/composables/useUpload'
@@ -42,6 +41,7 @@ import {
} from '@/components/ui/alert-dialog'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import { Progress } from '@/components/ui/progress'
import TaskPageLayout from '@/views/system/task-management/components/TaskPageLayout.vue'
// ========== 常量 ==========
const MAX_FILE_SIZE = 5 * 1024 * 1024
@@ -369,32 +369,41 @@ onMounted(() => loadVoiceList())
</script>
<template>
<BasicLayout :show-back="false" :show-title="false">
<!-- 工具栏 -->
<div class="toolbar">
<div class="toolbar-left">
<Button @click="handleCreate">
新建配音
</Button>
<div class="p-4">
<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 justify-between gap-3">
<div class="flex items-center gap-3">
<Button @click="handleCreate">
新建配音
</Button>
</div>
<div class="flex items-center gap-3">
<Input
v-model="searchParams.name"
placeholder="搜索配音名称..."
class="w-64"
@keypress.enter="handleSearch"
>
<template #prefix>
<Icon icon="lucide:search" class="size-4 text-muted-foreground" />
</template>
</Input>
<Button variant="outline" @click="handleSearch">搜索</Button>
<Button variant="ghost" @click="handleReset">重置</Button>
</div>
</div>
<div class="toolbar-right">
<Input
v-model="searchParams.name"
placeholder="搜索配音名称..."
class="w-64"
@keypress.enter="handleSearch"
>
<template #prefix>
<Icon icon="lucide:search" class="size-4 text-muted-foreground" />
</template>
</Input>
<Button variant="outline" @click="handleSearch">搜索</Button>
<Button variant="ghost" @click="handleReset">重置</Button>
</div>
</div>
</template>
<!-- 表格区域 -->
<div class="table-wrapper">
<!-- 表格 -->
<template #table>
<Table>
<TableHeader>
<TableRow class="hover:bg-transparent">
@@ -405,16 +414,9 @@ onMounted(() => loadVoiceList())
</TableRow>
</TableHeader>
<TableBody>
<!-- 加载状态 -->
<TableRow v-if="loading">
<TableCell :col-span="4" class="h-48 text-center">
<Spinner class="mx-auto size-6" />
</TableCell>
</TableRow>
<!-- 空状态 -->
<TableRow v-else-if="voiceList.length === 0">
<TableCell :col-span="4" class="h-48 text-center">
<TableRow v-if="voiceList.length === 0 && !loading">
<TableCell :colspan="4" class="h-48 text-center">
<div class="flex flex-col items-center gap-3 text-muted-foreground">
<Icon icon="lucide:mic-off" class="size-10 opacity-50" />
<span>暂无配音数据</span>
@@ -460,190 +462,137 @@ onMounted(() => loadVoiceList())
</TableRow>
</TableBody>
</Table>
</template>
<!-- 分页 -->
<div v-if="pagination.total > pagination.pageSize" class="pagination-bar">
<span class="text-sm text-muted-foreground">
{{ pagination.total }}
</span>
<div class="flex gap-1">
<Button
variant="outline"
size="sm"
:disabled="pagination.current === 1"
@click="handlePageChange(pagination.current - 1)"
>
上一页
</Button>
<Button
variant="outline"
size="sm"
:disabled="pagination.current * pagination.pageSize >= pagination.total"
@click="handlePageChange(pagination.current + 1)"
>
下一页
</Button>
</div>
</div>
</div>
<!-- 弹窗 -->
<template #modals>
<!-- 新建/编辑弹窗 -->
<Dialog :open="modalVisible" @update:open="(v) => modalVisible = v">
<DialogContent class="sm:max-w-lg">
<DialogHeader>
<DialogTitle>{{ isCreateMode ? '新建配音' : '编辑配音' }}</DialogTitle>
</DialogHeader>
<!-- 新建/编辑弹窗 -->
<Dialog :open="modalVisible" @update:open="(v) => modalVisible = v">
<DialogContent class="sm:max-w-lg">
<DialogHeader>
<DialogTitle>{{ isCreateMode ? '新建配音' : '编辑配音' }}</DialogTitle>
</DialogHeader>
<div class="space-y-5 py-2">
<!-- 名称 -->
<div class="space-y-2">
<Label for="name">配音名称 <span class="text-destructive">*</span></Label>
<Input id="name" v-model="formData.name" placeholder="请输入配音名称" />
</div>
<div class="space-y-5 py-2">
<!-- 名称 -->
<div class="space-y-2">
<Label for="name">配音名称 <span class="text-destructive">*</span></Label>
<Input id="name" v-model="formData.name" placeholder="请输入配音名称" />
<!-- 上传区域 -->
<div v-if="isCreateMode" class="space-y-2">
<Label>音频文件 <span class="text-destructive">*</span></Label>
<!-- 未上传状态 -->
<div
v-if="fileList.length === 0 && !uploadState.uploading && !extractingText"
class="upload-zone"
:class="{ 'upload-zone--dragging': isDragging }"
@click="triggerFileInput"
@dragover="handleDragOver"
@dragleave="handleDragLeave"
@drop="handleDrop"
>
<div class="upload-zone__icon">
<Icon icon="lucide:cloud-upload" />
</div>
<p class="upload-zone__title">点击或拖拽音频文件到此区域</p>
<p class="upload-zone__hint">支持 MP3WAVAACM4AFLACOGG最大 5MB</p>
</div>
<!-- 上传中 -->
<div v-else-if="uploadState.uploading" class="upload-status">
<Progress :value="50" class="w-16" />
<p class="mt-3 text-sm text-muted-foreground">正在上传...</p>
</div>
<!-- 识别中 -->
<div v-else-if="extractingText" class="upload-status">
<Progress :value="50" class="w-16" />
<p class="mt-3 text-sm text-muted-foreground">正在识别语音...</p>
</div>
<!-- 已上传 -->
<div v-else class="upload-preview">
<div class="upload-preview__icon">
<Icon icon="lucide:file-audio" />
</div>
<div class="upload-preview__info">
<span class="upload-preview__name">{{ fileList[0]?.name || '音频文件' }}</span>
<Badge v-if="formData.text" variant="secondary" class="gap-1">
<Icon icon="lucide:check-circle" class="size-3" />
已识别语音
</Badge>
<Badge v-else variant="outline" class="gap-1 text-amber-600">
<Icon icon="lucide:alert-circle" class="size-3" />
未识别到语音
</Badge>
</div>
<Button variant="ghost" size="sm" class="text-destructive" @click="handleRemoveFile">
<Icon icon="lucide:x" class="size-4" />
</Button>
</div>
<input
ref="fileInputRef"
type="file"
accept="audio/*,.mp3,.wav,.aac,.m4a,.flac,.ogg"
class="hidden"
@change="handleFileSelect"
/>
</div>
<!-- 备注 -->
<div class="space-y-2">
<Label for="note">备注</Label>
<Textarea
id="note"
v-model="formData.note"
placeholder="备注信息(选填)"
:rows="2"
/>
</div>
</div>
<!-- 上传区域 -->
<div v-if="isCreateMode" class="space-y-2">
<Label>音频文件 <span class="text-destructive">*</span></Label>
<DialogFooter>
<Button variant="outline" @click="handleCancel">取消</Button>
<Button :disabled="isSubmitDisabled" :loading="submitting" @click="handleSubmit">
保存
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<!-- 未上传状态 -->
<div
v-if="fileList.length === 0 && !uploadState.uploading && !extractingText"
class="upload-zone"
:class="{ 'upload-zone--dragging': isDragging }"
@click="triggerFileInput"
@dragover="handleDragOver"
@dragleave="handleDragLeave"
@drop="handleDrop"
<!-- 删除确认 -->
<AlertDialog :open="deleteDialogVisible" @update:open="(v) => deleteDialogVisible = v">
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>确认删除</AlertDialogTitle>
<AlertDialogDescription>
确定要删除配音{{ deleteTarget?.name }}此操作不可恢复
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>取消</AlertDialogCancel>
<AlertDialogAction
class="bg-destructive text-destructive-foreground hover:bg-destructive/90"
@click="confirmDelete"
>
<div class="upload-zone__icon">
<Icon icon="lucide:cloud-upload" />
</div>
<p class="upload-zone__title">点击或拖拽音频文件到此区域</p>
<p class="upload-zone__hint">支持 MP3WAVAACM4AFLACOGG最大 5MB</p>
</div>
删除
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</template>
</TaskPageLayout>
</div>
<!-- 上传中 -->
<div v-else-if="uploadState.uploading" class="upload-status">
<Progress :value="50" class="w-16" />
<p class="mt-3 text-sm text-muted-foreground">正在上传...</p>
</div>
<!-- 识别中 -->
<div v-else-if="extractingText" class="upload-status">
<Progress :value="50" class="w-16" />
<p class="mt-3 text-sm text-muted-foreground">正在识别语音...</p>
</div>
<!-- 已上传 -->
<div v-else class="upload-preview">
<div class="upload-preview__icon">
<Icon icon="lucide:file-audio" />
</div>
<div class="upload-preview__info">
<span class="upload-preview__name">{{ fileList[0]?.name || '音频文件' }}</span>
<Badge v-if="formData.text" variant="secondary" class="gap-1">
<Icon icon="lucide:check-circle" class="size-3" />
已识别语音
</Badge>
<Badge v-else variant="outline" class="gap-1 text-amber-600">
<Icon icon="lucide:alert-circle" class="size-3" />
未识别到语音
</Badge>
</div>
<Button variant="ghost" size="sm" class="text-destructive" @click="handleRemoveFile">
<Icon icon="lucide:x" class="size-4" />
</Button>
</div>
<input
ref="fileInputRef"
type="file"
accept="audio/*,.mp3,.wav,.aac,.m4a,.flac,.ogg"
class="hidden"
@change="handleFileSelect"
/>
</div>
<!-- 备注 -->
<div class="space-y-2">
<Label for="note">备注</Label>
<Textarea
id="note"
v-model="formData.note"
placeholder="备注信息(选填)"
:rows="2"
/>
</div>
</div>
<DialogFooter>
<Button variant="outline" @click="handleCancel">取消</Button>
<Button :disabled="isSubmitDisabled" :loading="submitting" @click="handleSubmit">
保存
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<!-- 删除确认 -->
<AlertDialog :open="deleteDialogVisible" @update:open="(v) => deleteDialogVisible = v">
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>确认删除</AlertDialogTitle>
<AlertDialogDescription>
确定要删除配音{{ deleteTarget?.name }}此操作不可恢复
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>取消</AlertDialogCancel>
<AlertDialogAction
class="bg-destructive text-destructive-foreground hover:bg-destructive/90"
@click="confirmDelete"
>
删除
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<audio ref="audioPlayer" class="hidden" />
</BasicLayout>
<audio ref="audioPlayer" class="hidden" />
</template>
<style scoped lang="less">
.toolbar {
display: flex;
justify-content: space-between;
align-items: center;
padding: var(--space-4);
background: var(--card);
border-radius: var(--radius-lg);
margin-bottom: var(--space-4);
box-shadow: var(--shadow-sm);
}
.toolbar-right {
display: flex;
align-items: center;
gap: var(--space-2);
}
.table-wrapper {
background: var(--card);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-sm);
overflow: hidden;
}
.pagination-bar {
display: flex;
justify-content: space-between;
align-items: center;
padding: var(--space-3) var(--space-4);
border-top: 1px solid var(--border);
background: var(--card);
}
// 上传区域
.upload-zone {
display: flex;

View File

@@ -206,7 +206,7 @@
<label class="input-label">语速调节</label>
<div class="rate-control">
<Slider
v-model="store.speechRate"
v-model="speechRateArray"
:min="0.5"
:max="2.0"
:step="0.1"
@@ -283,6 +283,16 @@ const rateMarks = {
2.0: '2.0x',
}
// Slider 组件需要数组形式的 v-model
const speechRateArray = computed({
get: () => [store.speechRate],
set: (val: number[]) => {
if (val?.[0] !== undefined) {
store.speechRate = val[0]
}
}
})
function triggerFileSelect() {
fileInput.value?.click()
}
@@ -378,7 +388,6 @@ onMounted(async () => {
min-height: 100vh;
background: @bg-page;
padding: 48px 64px;
max-width: 1400px;
margin: 0 auto;
@media (max-width: 1024px) {

View File

@@ -28,7 +28,7 @@
<div class="flex items-center min-w-0 flex-1">
<Icon
icon="lucide:folder"
class="mr-2.5 text-base shrink-0"
class="mr-2.5 size-5 shrink-0"
:class="selectedGroupId === group.id ? 'text-primary' : 'text-muted-foreground'"
/>
<template v-if="editingGroupId !== group.id">
@@ -51,13 +51,13 @@
<span class="text-xs mr-1" :class="selectedGroupId === group.id ? 'text-primary/80' : 'text-muted-foreground'">{{ group.fileCount }}</span>
<Icon
icon="lucide:pencil"
class="opacity-0 group-hover:opacity-100 p-1 cursor-pointer text-xs transition-all"
class="opacity-0 group-hover:opacity-100 p-0.5 cursor-pointer size-4 transition-all"
:class="selectedGroupId === group.id ? 'text-primary/70 hover:text-primary' : 'text-muted-foreground hover:text-primary'"
@click.stop="handleEditGroup(group, $event)"
/>
<Icon
icon="lucide:trash-2"
class="opacity-0 group-hover:opacity-100 p-1 cursor-pointer text-xs transition-all"
class="opacity-0 group-hover:opacity-100 p-0.5 cursor-pointer size-4 transition-all"
:class="selectedGroupId === group.id ? 'text-primary/70 hover:text-destructive' : 'text-muted-foreground hover:text-destructive'"
@click.stop="handleDeleteGroup(group, $event)"
/>
@@ -831,7 +831,6 @@ onMounted(async () => {
display: flex;
height: 100%;
background: var(--background);
padding: var(--space-4);
gap: var(--space-4);
}

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

@@ -1,28 +1,36 @@
<template>
<div class="task-list-container">
<!-- 筛选 -->
<div class="filter-section">
<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>
<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>
<!-- -->
<div class="table-wrapper">
<!-- -->
<template #table>
<Table>
<TableHeader>
<TableRow>
@@ -102,49 +110,27 @@
</TableRow>
</TableBody>
</Table>
</div>
</template>
<!-- 分页 -->
<div v-if="pagination.total > 0" class="pagination-section">
<span class="text-sm text-muted-foreground">
{{ pagination.total }}
</span>
<div class="flex gap-1">
<Button
variant="outline"
size="sm"
:disabled="pagination.current === 1"
@click="handlePageChange(pagination.current - 1)"
>
上一页
</Button>
<Button
variant="outline"
size="sm"
:disabled="pagination.current * pagination.pageSize >= pagination.total"
@click="handlePageChange(pagination.current + 1)"
>
下一页
</Button>
</div>
</div>
<!-- 提示词弹窗 -->
<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>
</div>
<!-- 弹窗 -->
<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>
@@ -186,6 +172,7 @@ import {
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([])
@@ -287,42 +274,6 @@ onMounted(() => {
</script>
<style scoped lang="less">
.task-list-container {
padding: 0;
height: 100%;
display: flex;
flex-direction: column;
gap: var(--space-4);
}
.filter-section {
flex-shrink: 0;
padding: var(--space-5);
background: var(--card);
border-radius: var(--radius-lg);
border: 1px solid var(--border);
display: flex;
gap: var(--space-3);
margin-bottom: 0;
}
.table-wrapper {
flex: 1;
background: var(--card);
border-radius: var(--radius-lg);
border: 1px solid var(--border);
overflow: hidden;
}
.pagination-section {
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--space-4);
border-top: 1px solid var(--border);
}
.prompt-content {
padding: var(--space-4);
background: var(--muted);