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
344 lines
12 KiB
Vue
344 lines
12 KiB
Vue
<template>
|
||
<TaskPageLayout
|
||
:loading="loading"
|
||
:current="paginationConfig.current"
|
||
:page-size="paginationConfig.pageSize"
|
||
:total="paginationConfig.total"
|
||
@page-change="handlePageChange"
|
||
>
|
||
<!-- 筛选条件 -->
|
||
<template #filters>
|
||
<div class="flex flex-wrap items-center gap-3">
|
||
<!-- 状态筛选 -->
|
||
<Select v-model="filters.status" @update:model-value="handleFilterChange">
|
||
<SelectTrigger class="w-[120px]">
|
||
<SelectValue placeholder="任务状态" />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
<SelectItem value="all">全部状态</SelectItem>
|
||
<SelectItem value="pending">待处理</SelectItem>
|
||
<SelectItem value="running">处理中</SelectItem>
|
||
<SelectItem value="success">已完成</SelectItem>
|
||
<SelectItem value="failed">失败</SelectItem>
|
||
<SelectItem value="canceled">已取消</SelectItem>
|
||
</SelectContent>
|
||
</Select>
|
||
|
||
<!-- 关键词搜索 -->
|
||
<div class="relative w-[200px]">
|
||
<Icon icon="lucide:search" class="absolute left-3 top-1/2 -translate-y-1/2 size-4 text-muted-foreground" />
|
||
<Input
|
||
v-model="filters.keyword"
|
||
placeholder="搜索任务名称"
|
||
class="pl-9"
|
||
@keyup.enter="handleFilterChange"
|
||
/>
|
||
</div>
|
||
|
||
<!-- 日期范围选择 -->
|
||
<Popover v-model:open="datePickerOpen">
|
||
<PopoverTrigger as-child>
|
||
<Button variant="outline" class="w-[280px] justify-start font-normal">
|
||
<Icon icon="lucide:calendar" class="mr-2 size-4" />
|
||
<template v-if="filters.dateRange?.length === 2">
|
||
{{ filters.dateRange[0] }} 至 {{ filters.dateRange[1] }}
|
||
</template>
|
||
<template v-else>
|
||
<span class="text-muted-foreground">选择日期范围</span>
|
||
</template>
|
||
</Button>
|
||
</PopoverTrigger>
|
||
<PopoverContent class="w-auto p-0" align="start">
|
||
<RangeCalendar
|
||
v-model="selectedDateRange"
|
||
:number-of-months="2"
|
||
@update:model-value="handleDateSelect"
|
||
/>
|
||
</PopoverContent>
|
||
</Popover>
|
||
|
||
<Button @click="handleFilterChange">
|
||
<Icon icon="lucide:search" class="mr-1 size-4" />
|
||
查询
|
||
</Button>
|
||
<Button variant="outline" @click="handleResetFilters">
|
||
<Icon icon="lucide:rotate-ccw" class="mr-1 size-4" />
|
||
重置
|
||
</Button>
|
||
</div>
|
||
</template>
|
||
|
||
<!-- 批量操作栏 -->
|
||
<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>
|
||
|
||
<!-- 表格 -->
|
||
<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>
|
||
|
||
<!-- 弹窗 -->
|
||
<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>
|
||
import { ref, computed, onMounted } from 'vue'
|
||
import { Icon } from '@iconify/vue'
|
||
import { toast } from 'vue-sonner'
|
||
import { Button } from '@/components/ui/button'
|
||
import { Input } from '@/components/ui/input'
|
||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
|
||
import { RangeCalendar } from '@/components/ui/range-calendar'
|
||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
|
||
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 {
|
||
AlertDialog,
|
||
AlertDialogAction,
|
||
AlertDialogCancel,
|
||
AlertDialogContent,
|
||
AlertDialogDescription,
|
||
AlertDialogFooter,
|
||
AlertDialogHeader,
|
||
AlertDialogTitle
|
||
} from '@/components/ui/alert-dialog'
|
||
import { getDigitalHumanTaskPage, cancelTask, deleteTask } from '@/api/digitalHuman'
|
||
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 } = useTaskList(getDigitalHumanTaskPage)
|
||
|
||
// 初始化 filters.status 为 'all'
|
||
if (!filters.status) {
|
||
filters.status = 'all'
|
||
}
|
||
|
||
const { handleDelete: deleteTaskById, handleCancel } = useTaskOperations({ deleteApi: deleteTask, cancelApi: cancelTask }, fetchList)
|
||
useTaskPolling(getDigitalHumanTaskPage, { onTaskUpdate: fetchList })
|
||
|
||
// 表格选择
|
||
const selectedRowKeys = ref([])
|
||
|
||
const isAllSelected = computed(() => {
|
||
return list.value.length > 0 && list.value.every(item => selectedRowKeys.value.includes(item.id))
|
||
})
|
||
|
||
const handleSelectAll = (checked) => {
|
||
if (checked) {
|
||
selectedRowKeys.value = list.value.map(item => item.id)
|
||
} else {
|
||
selectedRowKeys.value = []
|
||
}
|
||
}
|
||
|
||
const handleSelectRow = (id) => {
|
||
const index = selectedRowKeys.value.indexOf(id)
|
||
if (index > -1) {
|
||
selectedRowKeys.value.splice(index, 1)
|
||
} else {
|
||
selectedRowKeys.value.push(id)
|
||
}
|
||
}
|
||
|
||
// 分页处理
|
||
const handlePageChange = (page) => {
|
||
paginationConfig.current = page
|
||
fetchList()
|
||
}
|
||
|
||
// 状态判断
|
||
const isStatus = (status, target) => status === target || status === target.toUpperCase()
|
||
|
||
// 打开视频链接(预览/下载共用)
|
||
const openVideoUrl = (record) => {
|
||
if (!record.resultVideoUrl) {
|
||
toast.warning('该任务暂无视频结果,请稍后再试')
|
||
return
|
||
}
|
||
window.open(record.resultVideoUrl, '_blank')
|
||
}
|
||
|
||
// 删除对话框
|
||
const deleteDialogOpen = ref(false)
|
||
const deleteLoading = ref(false)
|
||
|
||
// 确认批量删除
|
||
const confirmBatchDelete = () => {
|
||
deleteDialogOpen.value = true
|
||
}
|
||
|
||
// 批量删除
|
||
const handleBatchDelete = async () => {
|
||
if (!selectedRowKeys.value.length) return
|
||
|
||
deleteLoading.value = true
|
||
try {
|
||
for (const id of selectedRowKeys.value) await deleteTask(id)
|
||
toast.success(`成功删除 ${selectedRowKeys.value.length} 个任务`)
|
||
selectedRowKeys.value = []
|
||
deleteDialogOpen.value = false
|
||
await fetchList()
|
||
} catch (e) {
|
||
console.error('批量删除失败:', e)
|
||
toast.error('批量删除失败,请重试')
|
||
} finally {
|
||
deleteLoading.value = false
|
||
}
|
||
}
|
||
|
||
// 处理单个删除
|
||
const handleDelete = (id) => {
|
||
selectedRowKeys.value = [id]
|
||
confirmBatchDelete()
|
||
}
|
||
|
||
// 处理日期选择
|
||
const handleDateSelect = (value) => {
|
||
if (value?.start && value?.end) {
|
||
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 = [formatDateStr(value.start), formatDateStr(value.end)]
|
||
datePickerOpen.value = false
|
||
handleFilterChange()
|
||
}
|
||
}
|
||
|
||
onMounted(fetchList)
|
||
</script>
|
||
|
||
<style scoped lang="less">
|
||
</style>
|