Files
sionrui/frontend/app/web-gold/src/views/system/task-management/digital-human-task/index.vue
sion123 791a523101 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
2026-03-18 02:56:05 +08:00

344 lines
12 KiB
Vue
Raw Blame History

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