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:
@@ -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" />
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
|
||||
@@ -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">支持 MP3、WAV、AAC、M4A、FLAC、OGG,最大 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">支持 MP3、WAV、AAC、M4A、FLAC、OGG,最大 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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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')))
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
Reference in New Issue
Block a user