refactor(TaskStatusTag): replace a-tag with span element and improve status configuration

- Replace a-tag component with semantic span element for better accessibility
- Introduce centralized STATUS_CONFIG object for consistent status mapping
- Add isRunning computed property for cleaner conditional logic
- Remove redundant statusMap handling and normalize status values
- Add proper CSS class bindings for styling consistency
- Update component structure to use
This commit is contained in:
2026-02-26 20:45:51 +08:00
parent 72fa2c63a1
commit 1e5a1d422b
6 changed files with 386 additions and 1223 deletions

View File

@@ -1,277 +0,0 @@
# 设计系统迁移指南
> 将现有前端代码迁移到统一设计系统
---
## 一、迁移步骤
### Step 1: 引入设计令牌
`src/main.ts` 中添加设计令牌导入:
```typescript
// main.ts
import './styles/design-tokens.css' // 添加在最先
import './style.less'
// ... 其他导入
```
### Step 2: 配置 TailwindCSS
更新 `tailwind.config.js``tailwind.config.ts`
```bash
# 删除旧配置
rm tailwind.config.js
# 使用新配置
# 已创建: tailwind.config.ts
```
### Step 3: 配置 Ant Design Vue 主题
更新 `src/App.vue`
```vue
<script setup lang="ts">
import { ConfigProvider, theme } from 'ant-design-vue'
import { antdThemeConfig, themeManager } from '@gold/styles/antd-theme'
const isDark = ref(themeManager.initTheme())
const currentTheme = computed(() => themeManager.getAntdTheme(isDark.value))
// 监听系统主题变化
watchEffect(() => {
const media = window.matchMedia('(prefers-color-scheme: dark)')
const handler = (e: MediaQueryListEvent) => {
if (!localStorage.getItem('theme')) {
isDark.value = e.matches
themeManager.setTheme(e.matches)
}
}
media.addEventListener('change', handler)
})
</script>
<template>
<ConfigProvider :theme="currentTheme">
<router-view />
</ConfigProvider>
</template>
```
---
## 二、组件迁移示例
### 2.1 颜色迁移
**Before (问题代码)**:
```less
// ❌ 组件内定义变量
@primary: #6366f1;
@primary-gold: #D4A853;
.button {
background: #1890FF;
}
```
**After (规范代码)**:
```less
// ✅ 使用全局设计令牌
.button {
background: var(--color-primary-500);
&:hover {
background: var(--color-primary-400);
}
}
```
### 2.2 字体迁移
**Before**:
```css
/* ❌ 硬编码字体大小 */
.title { font-size: 24px; }
.desc { font-size: 12px; }
```
**After**:
```css
/* ✅ 使用设计令牌 */
.title { font-size: var(--font-size-2xl); }
.desc { font-size: var(--font-size-xs); }
```
### 2.3 间距迁移
**Before**:
```css
/* ❌ 魔法数字 */
.card {
padding: 20px;
margin-top: 24px;
gap: 16px;
}
```
**After**:
```css
/* ✅ 语义化间距 */
.card {
padding: var(--space-5);
margin-top: var(--space-6);
gap: var(--space-4);
}
```
### 2.4 使用 TailwindCSS
```vue
<template>
<!-- 使用 TailwindCSS 工具类 -->
<div class="p-5 rounded-lg shadow-base bg-white">
<h2 class="text-lg font-medium text-gray-900">标题</h2>
<p class="mt-2 text-sm text-gray-500">描述文字</p>
<button class="mt-4 px-4 py-2 bg-primary-500 text-white rounded-base
hover:bg-primary-400 transition-duration-fast">
操作按钮
</button>
</div>
</template>
```
---
## 三、需要修改的文件清单
### 优先级 P0 - 立即修改
| 文件 | 问题 | 修改内容 |
|------|------|----------|
| `src/components/agents/ChatDrawer.vue` | 局部定义 `@primary: #6366f1` | 改用 `var(--color-primary-500)` |
| `src/components/agents/HistoryPanel.vue` | 局部定义 `@primary: #6366f1` | 改用 `var(--color-primary-500)` |
| `src/views/auth/Login.vue` | 金色主题 `@primary-gold: #D4A853` | 改用品牌蓝 |
| `src/views/home/Home.vue` | 硬编码颜色 `#1890FF` | 改用 `var(--color-primary-500)` |
### 优先级 P1 - 短期修改
| 文件 | 问题 |
|------|------|
| `src/views/content-style/Copywriting.vue` | 混用 `var(--color-primary)``#1677ff` |
| `src/components/GradientButton.vue` | 需要验证与设计系统一致 |
---
## 四、批量替换脚本
### 4.1 颜色替换
```bash
# 在 frontend/app/web-gold 目录执行
# 替换硬编码的主色
find src -name "*.vue" -o -name "*.less" | xargs sed -i 's/#1890FF/var(--color-primary-500)/g'
find src -name "*.vue" -o -name "*.less" | xargs sed -i 's/#40A9FF/var(--color-primary-400)/g'
find src -name "*.vue" -o -name "*.less" | xargs sed -i 's/#6366f1/var(--color-primary-500)/g'
find src -name "*.vue" -o -name "*.less" | xargs sed -i 's/#1677ff/var(--color-primary-500)/g'
```
### 4.2 间距替换
```bash
# padding: 16px -> padding: var(--space-4)
find src -name "*.vue" -o -name "*.less" | xargs sed -i 's/padding: 16px/padding: var(--space-4)/g'
find src -name "*.vue" -o -name "*.less" | xargs sed -i 's/padding: 20px/padding: var(--space-5)/g'
find src -name "*.vue" -o -name "*.less" | xargs sed -i 's/padding: 24px/padding: var(--space-6)/g'
```
---
## 五、验证清单
完成迁移后,检查以下项目:
- [ ] 所有颜色使用 CSS 变量或 TailwindCSS 类
- [ ] 没有硬编码的 `#xxxxxx` 颜色值(除特殊情况)
- [ ] 字体大小使用 `var(--font-size-*)` 或 TailwindCSS
- [ ] 间距使用 `var(--space-*)` 或 TailwindCSS
- [ ] 组件内没有重复定义 `@primary` 等 Less 变量
- [ ] 页面视觉效果一致
- [ ] 深色模式切换正常工作
---
## 六、设计令牌速查表
### 颜色
| 用途 | 变量 | 值 |
|------|------|-----|
| 主色 | `--color-primary-500` | #3B82F6 |
| 主色悬浮 | `--color-primary-400` | #60A5FA |
| 成功 | `--color-success-500` | #22C55E |
| 警告 | `--color-warning-500` | #F59E0B |
| 错误 | `--color-error-500` | #EF4444 |
| 正文 | `--color-text-primary` | #111827 |
| 次要文字 | `--color-text-secondary` | #4B5563 |
| 边框 | `--color-border` | #E5E7EB |
### 间距
| 变量 | 值 | 用途 |
|------|-----|------|
| `--space-2` | 8px | 图标与文字 |
| `--space-3` | 12px | 小间距 |
| `--space-4` | 16px | 标准间距 |
| `--space-5` | 20px | 卡片内边距 |
| `--space-6` | 24px | 区块间距 |
### 圆角
| 变量 | 值 | 用途 |
|------|-----|------|
| `--radius-sm` | 4px | 标签 |
| `--radius-base` | 6px | 按钮、输入框 |
| `--radius-md` | 8px | 下拉框 |
| `--radius-lg` | 12px | 卡片、弹窗 |
---
## 七、常见问题
### Q: 旧组件样式不生效?
确保设计令牌在 `main.ts` 中最先导入。
### Q: Ant Design 组件样式不一致?
检查 `App.vue` 中是否正确配置了 `ConfigProvider``theme` 属性。
### Q: TailwindCSS 类不生效?
1. 确保 `tailwind.config.ts` 文件存在
2. 确保 `postcss.config.js` 配置正确
3. 重启开发服务器
### Q: 深色模式切换有闪烁?
`index.html``<head>` 中添加:
```html
<script>
(function() {
const theme = localStorage.getItem('theme');
if (theme === 'dark' || (!theme && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.setAttribute('data-theme', 'dark');
}
})();
</script>
```
---
> **迁移完成后**,项目将拥有统一的设计语言,后续开发效率将显著提升。

View File

@@ -1,97 +1,92 @@
<template> <template>
<a-tag :color="color" :class="statusClass"> <span class="task-status-tag" :class="statusClass">
<template v-if="showIcon && (status === 'running' || status === 'RUNNING')"> <template v-if="showIcon && isRunning">
<LoadingOutlined :spin="true" /> <LoadingOutlined class="task-status-tag__icon" :spin="true" />
</template> </template>
{{ text }} {{ text }}
</a-tag> </span>
</template> </template>
<script setup> <script setup>
import { computed } from 'vue' import { computed } from 'vue'
import { LoadingOutlined } from '@ant-design/icons-vue' import { LoadingOutlined } from '@ant-design/icons-vue'
// Props
const props = defineProps({ const props = defineProps({
// 状态值 status: { type: String, required: true },
status: { showIcon: { type: Boolean, default: true },
type: String, statusMap: { type: Object, default: () => ({}) }
required: true
},
// 是否显示图标
showIcon: {
type: Boolean,
default: true
},
// 自定义状态映射
statusMap: {
type: Object,
default: () => ({})
}
}) })
// 计算属性:状态文本 const STATUS_CONFIG = {
const text = computed(() => { pending: { text: '待处理', class: 'task-status-tag--pending' },
// 使用自定义映射或默认映射(同时支持大小写) running: { text: '处理中', class: 'task-status-tag--running' },
const map = { success: { text: '已完成', class: 'task-status-tag--success' },
pending: '待处理', failed: { text: '失败', class: 'task-status-tag--failed' },
running: '处理中', canceled: { text: '已取消', class: 'task-status-tag--canceled' }
success: '已完成', }
failed: '失败',
canceled: '已取消', const normalizedStatus = computed(() => props.status?.toLowerCase() || '')
// 大写状态支持
PENDING: '待处理', const config = computed(() => {
RUNNING: '处理中', const custom = props.statusMap[props.status]
SUCCESS: '已完成', if (custom) return { text: custom, class: `task-status-tag--${normalizedStatus.value}` }
PROCESSING: '处理中', return STATUS_CONFIG[normalizedStatus.value] || { text: props.status, class: 'task-status-tag--default' }
FAILED: '失败',
CANCELED: '已取消',
...props.statusMap
}
return map[props.status] || props.status
}) })
// 计算属性:状态颜色 const text = computed(() => config.value.text)
const color = computed(() => { const statusClass = computed(() => config.value.class)
const colorMap = { const isRunning = computed(() => normalizedStatus.value === 'running')
pending: 'default',
running: 'processing',
success: 'success',
failed: 'error',
canceled: 'warning',
// 大写状态支持
PENDING: 'default',
RUNNING: 'processing',
SUCCESS: 'success',
FAILED: 'error',
CANCELED: 'warning'
}
return colorMap[props.status] || 'default'
})
// 计算属性:状态样式类
const statusClass = computed(() => {
// 将状态标准化为小写用于CSS类名
const normalizedStatus = props.status.toLowerCase()
return `task-status-tag--${normalizedStatus}`
})
</script> </script>
<style scoped> <style scoped lang="less">
/* 状态标签动画效果 */ .task-status-tag {
.task-status-tag--running { display: inline-flex;
animation: pulse 1.5s ease-in-out infinite; align-items: center;
gap: 4px;
padding: 2px 8px;
font-size: var(--font-size-xs);
font-weight: var(--font-weight-medium);
border-radius: var(--radius-sm);
line-height: 1.5;
&__icon {
font-size: 12px;
}
&--pending {
background: var(--color-gray-100);
color: var(--color-gray-600);
}
&--running {
background: var(--color-primary-50);
color: var(--color-primary-600);
animation: pulse 1.5s ease-in-out infinite;
}
&--success {
background: var(--color-success-50);
color: var(--color-success-600);
}
&--failed {
background: var(--color-error-50);
color: var(--color-error-600);
}
&--canceled {
background: var(--color-warning-50);
color: var(--color-warning-600);
}
&--default {
background: var(--color-gray-100);
color: var(--color-gray-600);
}
} }
@keyframes pulse { @keyframes pulse {
0% { 0%, 100% { opacity: 1; }
opacity: 1; 50% { opacity: 0.6; }
}
50% {
opacity: 0.6;
}
100% {
opacity: 1;
}
} }
</style> </style>

View File

@@ -1,14 +1,14 @@
<template> <template>
<div class="digital-human-task-page"> <div class="task-page">
<!-- 筛选条件 --> <!-- 筛选条件 -->
<div class="digital-human-task-page__filters"> <div class="task-page__filters">
<a-space :size="16"> <a-space :size="16">
<a-select <a-select
v-model:value="filters.status" v-model:value="filters.status"
class="filter-select" class="filter-select"
placeholder="任务状态" placeholder="任务状态"
@change="handleFilterChange"
allow-clear allow-clear
@change="handleFilterChange"
> >
<a-select-option value="">全部状态</a-select-option> <a-select-option value="">全部状态</a-select-option>
<a-select-option value="pending">待处理</a-select-option> <a-select-option value="pending">待处理</a-select-option>
@@ -25,9 +25,7 @@
allow-clear allow-clear
@press-enter="handleFilterChange" @press-enter="handleFilterChange"
> >
<template #prefix> <template #prefix><SearchOutlined /></template>
<SearchOutlined />
</template>
</a-input> </a-input>
<a-range-picker <a-range-picker
@@ -39,39 +37,22 @@
@change="handleFilterChange" @change="handleFilterChange"
/> />
<a-button type="primary" class="filter-button" @click="handleFilterChange"> <a-button type="primary" @click="handleFilterChange">查询</a-button>
查询 <a-button @click="handleResetFilters">重置</a-button>
</a-button>
<a-button class="filter-button" @click="handleResetFilters">
重置
</a-button>
</a-space> </a-space>
</div> </div>
<!-- 任务列表 --> <!-- 任务列表 -->
<div class="digital-human-task-page__content"> <div class="task-page__content">
<!-- 批量操作栏 --> <!-- 批量操作栏 -->
<div v-if="selectedRowKeys.length > 0" class="batch-actions"> <div v-if="selectedRowKeys.length > 0" class="batch-actions">
<a-alert <a-alert :message="`已选中 ${selectedRowKeys.length} 项`" type="info" show-icon>
:message="`已选中 ${selectedRowKeys.length} 项`"
type="info"
show-icon
>
<template #action> <template #action>
<a-space> <a-popconfirm title="确定要删除选中的任务吗?" @confirm="handleBatchDelete">
<a-button size="small" danger>
<a-popconfirm <DeleteOutlined /> 批量删除
title="确定要删除选中的任务吗?删除后无法恢复。" </a-button>
@confirm="handleBatchDelete" </a-popconfirm>
>
<a-button size="small" danger>
<template #icon>
<DeleteOutlined />
</template>
批量删除
</a-button>
</a-popconfirm>
</a-space>
</template> </template>
</a-alert> </a-alert>
</div> </div>
@@ -82,21 +63,14 @@
:columns="columns" :columns="columns"
:row-key="record => record.id" :row-key="record => record.id"
:pagination="paginationConfig" :pagination="paginationConfig"
@change="handleTableChange"
:row-selection="rowSelection" :row-selection="rowSelection"
:scroll="{ x: 1000 }" :scroll="{ x: 1000 }"
@change="handleTableChange"
> >
<!-- 任务名称列 -->
<template #bodyCell="{ column, record }"> <template #bodyCell="{ column, record }">
<!-- 任务名称列 -->
<template v-if="column.key === 'taskName'"> <template v-if="column.key === 'taskName'">
<div class="task-name-cell"> <strong>{{ record.taskName }}</strong>
<strong>{{ record.taskName }}</strong>
</div>
</template>
<!-- 音色列 -->
<template v-else-if="column.key === 'voiceId'">
<span>{{ record.voiceId || '-' }}</span>
</template> </template>
<!-- 状态列 --> <!-- 状态列 -->
@@ -106,10 +80,10 @@
<!-- 进度列 --> <!-- 进度列 -->
<template v-else-if="column.key === 'progress'"> <template v-else-if="column.key === 'progress'">
<div style="min-width: 100px"> <div class="progress-cell">
<a-progress <a-progress
:percent="record.progress" :percent="record.progress"
:status="getProgressStatus(record.status)" :status="PROGRESS_STATUS[record.status]"
size="small" size="small"
:show-info="false" :show-info="false"
/> />
@@ -118,27 +92,22 @@
<!-- 创建时间列 --> <!-- 创建时间列 -->
<template v-else-if="column.key === 'createTime'"> <template v-else-if="column.key === 'createTime'">
{{ formatDateTime(record.createTime) }} {{ formatDate(record.createTime) }}
</template> </template>
<!-- 操作列 --> <!-- 操作列 -->
<template v-else-if="column.key === 'actions'"> <template v-else-if="column.key === 'actions'">
<a-space> <a-space>
<!-- 下载按钮 -->
<a-button <a-button
v-if="isStatus(record.status, 'success')" v-if="isStatus(record.status, 'success')"
type="link" type="link"
size="small" size="small"
class="action-btn action-btn--success"
@click="handleDownload(record)" @click="handleDownload(record)"
class="action-btn-download"
> >
<template #icon> <DownloadOutlined /> 下载
<DownloadOutlined />
</template>
下载
</a-button> </a-button>
<!-- 取消按钮 -->
<a-button <a-button
v-if="isStatus(record.status, 'running')" v-if="isStatus(record.status, 'running')"
type="link" type="link"
@@ -148,11 +117,8 @@
取消 取消
</a-button> </a-button>
<a-popconfirm <a-popconfirm title="确定删除删除后无法恢复" @confirm="handleDelete(record.id)">
title="确定删除这个任务吗删除后无法恢复" <a-button type="link" size="small" class="action-btn action-btn--danger">删除</a-button>
@confirm="() => handleDelete(record.id)"
>
<a-button size="small" type="link" class="action-btn-delete">删除</a-button>
</a-popconfirm> </a-popconfirm>
</a-space> </a-space>
</template> </template>
@@ -166,364 +132,145 @@
<script setup> <script setup>
import { ref, onMounted } from 'vue' import { ref, onMounted } from 'vue'
import { message } from 'ant-design-vue' import { message } from 'ant-design-vue'
import { import { SearchOutlined, DownloadOutlined, DeleteOutlined } from '@ant-design/icons-vue'
SearchOutlined, import { getDigitalHumanTaskPage, cancelTask, deleteTask } from '@/api/digitalHuman'
PlayCircleOutlined,
DownloadOutlined,
DeleteOutlined
} from '@ant-design/icons-vue'
import {
getDigitalHumanTaskPage,
cancelTask,
deleteTask
} from '@/api/digitalHuman'
import { formatDate } from '@/utils/file' import { formatDate } from '@/utils/file'
import { useTaskList } from '@/views/system/task-management/composables/useTaskList' import { useTaskList } from '@/views/system/task-management/composables/useTaskList'
import { useTaskOperations } from '@/views/system/task-management/composables/useTaskOperations' import { useTaskOperations } from '@/views/system/task-management/composables/useTaskOperations'
import { useTaskPolling } from '@/views/system/task-management/composables/useTaskPolling' import { useTaskPolling } from '@/views/system/task-management/composables/useTaskPolling'
import TaskStatusTag from '@/views/system/task-management/components/TaskStatusTag.vue' import TaskStatusTag from '@/views/system/task-management/components/TaskStatusTag.vue'
// 使用 Composable // 进度状态映射
const { const PROGRESS_STATUS = {
loading, pending: 'normal', running: 'active', success: 'success', failed: 'exception', canceled: 'normal',
list, PENDING: 'normal', RUNNING: 'active', SUCCESS: 'success', FAILED: 'exception', CANCELED: 'normal'
filters, }
paginationConfig,
fetchList,
handleFilterChange,
handleResetFilters,
handleTableChange
} = useTaskList(getDigitalHumanTaskPage)
// 使用任务操作 Composable // Composables
const { const { loading, list, filters, paginationConfig, fetchList, handleFilterChange, handleResetFilters, handleTableChange } = useTaskList(getDigitalHumanTaskPage)
handleDelete, const { handleDelete, handleCancel } = useTaskOperations({ deleteApi: deleteTask, cancelApi: cancelTask }, fetchList)
handleCancel, useTaskPolling(getDigitalHumanTaskPage, { onTaskUpdate: fetchList })
} = useTaskOperations(
{
deleteApi: deleteTask,
cancelApi: cancelTask,
},
fetchList
)
// 使用轮询 Composable // 表格选择
useTaskPolling(getDigitalHumanTaskPage, {
onTaskUpdate: () => {
fetchList()
}
})
// 表格选择相关
const selectedRowKeys = ref([]) const selectedRowKeys = ref([])
// 表格行选择配置
const rowSelection = { const rowSelection = {
selectedRowKeys, selectedRowKeys,
onChange: (keys) => { onChange: (keys) => { selectedRowKeys.value = keys }
selectedRowKeys.value = keys
},
onSelectAll: (selected, selectedRows, changeRows) => {
// 全选逻辑
console.log('全选状态:', selected, '选中行数:', selectedRows.length, '变化行数:', changeRows.length)
}
} }
// 格式化日期时间 // 状态判断
const formatDateTime = (dateStr) => { const isStatus = (status, target) => status === target || status === target.toUpperCase()
return formatDate(dateStr)
}
// 表格列定义 // 下载视频
const columns = [
{
title: 'ID',
dataIndex: 'id',
key: 'id',
width: 80,
fixed: 'left'
},
{
title: '任务名称',
dataIndex: 'taskName',
key: 'taskName',
width: 250,
ellipsis: true,
fixed: 'left'
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
width: 100
},
{
title: '进度',
dataIndex: 'progress',
key: 'progress',
width: 150
},
{
title: '创建时间',
dataIndex: 'createTime',
key: 'createTime',
width: 180
},
{
title: '操作',
key: 'actions',
width: 240,
fixed: 'right'
}
]
// 获取进度条状态
const getProgressStatus = (status) => {
const statusMap = {
pending: 'normal',
running: 'active',
success: 'success',
failed: 'exception',
canceled: 'normal',
// 大写状态支持
PENDING: 'normal',
RUNNING: 'active',
SUCCESS: 'success',
FAILED: 'exception',
CANCELED: 'normal'
}
return statusMap[status] || 'normal'
}
// 检查状态(同时支持大小写)
const isStatus = (status, targetStatus) => {
return status === targetStatus || status === targetStatus.toUpperCase()
}
// 下载视频 - 新窗口打开(浏览器自动处理下载)
const handleDownload = (record) => { const handleDownload = (record) => {
console.log(record)
if (!record.resultVideoUrl) { if (!record.resultVideoUrl) {
message.warning('该任务暂无视频结果,请稍后再试') message.warning('该任务暂无视频结果,请稍后再试')
return return
} }
window.open(record.resultVideoUrl, '_blank') window.open(record.resultVideoUrl, '_blank')
} }
// 批量删除任务 // 批量删除
const handleBatchDelete = async () => { const handleBatchDelete = async () => {
if (selectedRowKeys.value.length === 0) { if (!selectedRowKeys.value.length) return
message.warning('请选择要删除的任务')
return
}
try { try {
// 逐个删除选中的任务 for (const id of selectedRowKeys.value) await deleteTask(id)
for (const id of selectedRowKeys.value) {
await deleteTask(id)
}
message.success(`成功删除 ${selectedRowKeys.value.length} 个任务`) message.success(`成功删除 ${selectedRowKeys.value.length} 个任务`)
// 清空选择并刷新列表
selectedRowKeys.value = [] selectedRowKeys.value = []
await fetchList() await fetchList()
} catch (error) { } catch (e) {
console.error('批量删除失败:', error) console.error('批量删除失败:', e)
message.error('批量删除失败,请重试') message.error('批量删除失败,请重试')
} }
} }
// 初始化 // 表格列定义
onMounted(() => { const columns = [
fetchList() { title: 'ID', dataIndex: 'id', key: 'id', width: 80, fixed: 'left' },
}) { title: '任务名称', dataIndex: 'taskName', key: 'taskName', width: 250, ellipsis: true, fixed: 'left' },
{ title: '状态', dataIndex: 'status', key: 'status', width: 100 },
{ title: '进度', dataIndex: 'progress', key: 'progress', width: 150 },
{ title: '创建时间', dataIndex: 'createTime', key: 'createTime', width: 180 },
{ title: '操作', key: 'actions', width: 200, fixed: 'right' }
]
onMounted(fetchList)
</script> </script>
<style scoped lang="less"> <style scoped lang="less">
.digital-human-task-page { .task-page {
padding: 0 var(--space-3); padding: var(--space-4);
height: 100%; height: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: var(--space-3); gap: var(--space-4);
}
&__filters { .task-page__filters {
padding: var(--space-3); padding: var(--space-4);
background: var(--color-surface); background: var(--color-bg-card);
border-radius: var(--radius-card); border-radius: var(--radius-lg);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03); box-shadow: var(--shadow-sm);
.filter-select,
.filter-input {
width: 200px;
}
.filter-date-picker {
width: 280px;
}
.filter-select,
.filter-input {
width: 200px;
} }
&__content { .filter-date-picker {
flex: 1; width: 280px;
overflow: auto;
background: var(--color-surface);
border-radius: var(--radius-card);
padding: var(--space-3);
display: flex;
flex-direction: column;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03);
} }
} }
/* 批量操作栏 */ .task-page__content {
flex: 1;
overflow: auto;
background: var(--color-bg-card);
border-radius: var(--radius-lg);
padding: var(--space-4);
display: flex;
flex-direction: column;
box-shadow: var(--shadow-sm);
}
.batch-actions { .batch-actions {
margin-bottom: var(--space-3); margin-bottom: var(--space-4);
+ .ant-spin { + :deep(.ant-spin) {
flex: 1; flex: 1;
display: flex;
flex-direction: column;
:deep(.ant-spin-container) { .ant-spin-container {
flex: 1; flex: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
:deep(.ant-table) { .ant-table {
flex: 1; flex: 1;
} }
} }
} }
/* 任务名称单元格 */ .progress-cell {
.task-name-cell { min-width: 100px;
display: flex;
align-items: center;
gap: var(--space-1);
} }
/* 操作按钮样式 */ .action-btn {
.action-btn-preview { &--success { color: var(--color-success-500); &:hover { color: var(--color-success-600); } }
color: var(--color-primary); &--danger { color: var(--color-error-500); &:hover { color: var(--color-error-600); } }
&:hover {
color: var(--color-primary-hover, var(--color-blue-600));
}
} }
.action-btn-download {
color: var(--color-success);
&:hover {
color: #059669;
}
}
.action-btn-delete {
color: var(--color-error);
&:hover {
color: #dc2626;
}
}
/* 文本截断 */
.text-ellipsis {
display: inline-block;
max-width: 180px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* 展开内容 */
.expanded-content {
padding: var(--space-3);
background: var(--color-bg-2);
border-radius: var(--radius-card);
margin: var(--space-2);
}
.task-text {
margin-bottom: var(--space-3);
p {
margin: var(--space-2) 0 0 0;
padding: var(--space-2);
background: var(--color-surface);
border-radius: var(--radius-card);
line-height: 1.6;
}
}
.task-params {
margin-bottom: var(--space-3);
.params-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: var(--space-2);
margin-top: var(--space-2);
.param-item {
display: flex;
align-items: center;
padding: var(--space-2);
background: var(--color-surface);
border-radius: var(--radius-card);
transition: all 0.2s;
&:hover {
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
}
.param-label {
font-weight: 600;
margin-right: var(--space-2);
color: var(--color-text-2);
}
.param-value {
color: var(--color-text);
}
}
}
.task-result {
margin-bottom: var(--space-3);
}
.processing-tip {
color: var(--color-text-3);
font-size: 12px;
}
.task-error {
margin-bottom: var(--space-2);
}
/* 确保按钮内的图标和文字对齐 */
:deep(.ant-btn .anticon) {
line-height: 0;
}
/* 表格样式 */
:deep(.ant-table-tbody > tr > td) { :deep(.ant-table-tbody > tr > td) {
padding: 12px 8px; padding: var(--space-3) var(--space-2);
} }
:deep(.ant-table-thead > tr > th) { :deep(.ant-table-thead > tr > th) {
background: var(--color-bg-2); background: var(--color-gray-50);
font-weight: 600; font-weight: var(--font-weight-semibold);
} }
/* 桌面端样式优化 */ :deep(.ant-btn .anticon) {
line-height: 0;
}
</style> </style>

View File

@@ -8,22 +8,13 @@
</div> </div>
<ul class="task-layout__nav-list"> <ul class="task-layout__nav-list">
<li <li
v-for="item in navItems" v-for="item in NAV_ITEMS"
:key="item.type" :key="item.type"
class="task-layout__nav-item" class="task-layout__nav-item"
:class="{ :class="{ 'is-active': currentType === item.type }"
'is-active': currentType === item.type
}"
> >
<a <a class="task-layout__nav-link" @click="navigateTo(item.type)">
href="javascript:void(0)" <component :is="item.icon" class="nav-icon" />
class="task-layout__nav-link"
@click="navigateTo(item.type)"
>
<span class="nav-icon">
<VideoCameraOutlined v-if="item.icon === 'video'" />
<UserOutlined v-else-if="item.icon === 'user'" />
</span>
<span class="nav-text">{{ item.label }}</span> <span class="nav-text">{{ item.label }}</span>
</a> </a>
</li> </li>
@@ -41,55 +32,37 @@
</template> </template>
<script setup> <script setup>
import { computed, defineAsyncComponent } from 'vue' import { computed, defineAsyncComponent, markRaw } from 'vue'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import { VideoCameraOutlined, UserOutlined } from '@ant-design/icons-vue' import { VideoCameraOutlined, UserOutlined } from '@ant-design/icons-vue'
// 响应式数据
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
// 当前任务类型
const currentType = computed(() => { const currentType = computed(() => {
const type = route.params.type const { type } = route.params
return !type || type === 'task-management' ? 'mix-task' : type
if (!type || type === 'task-management') {
return 'mix-task'
}
return type
}) })
// 动态导入组件 const NAV_ITEMS = [
const MixTaskList = defineAsyncComponent(() => import('../mix-task/index.vue'))
const DigitalHumanTaskList = defineAsyncComponent(() => import('../digital-human-task/index.vue'))
// 导航项配置
const navItems = [
{ {
type: 'mix-task', type: 'mix-task',
label: '混剪视频任务', label: '混剪视频任务',
icon: 'video', icon: VideoCameraOutlined,
component: MixTaskList component: markRaw(defineAsyncComponent(() => import('../mix-task/index.vue')))
}, },
{ {
type: 'digital-human-task', type: 'digital-human-task',
label: '数字人视频任务', label: '数字人视频任务',
icon: 'user', icon: UserOutlined,
component: DigitalHumanTaskList component: markRaw(defineAsyncComponent(() => import('../digital-human-task/index.vue')))
} }
] ]
// 当前组件
const currentComponent = computed(() => { const currentComponent = computed(() => {
const item = navItems.find(item => item.type === currentType.value) return NAV_ITEMS.find(item => item.type === currentType.value)?.component ?? NAV_ITEMS[0].component
if (!item) {
return navItems[0].component
}
return item.component
}) })
// 导航到指定类型
const navigateTo = (type) => { const navigateTo = (type) => {
router.push(`/system/task-management/${type}`) router.push(`/system/task-management/${type}`)
} }
@@ -99,141 +72,96 @@ const navigateTo = (type) => {
.task-layout { .task-layout {
display: flex; display: flex;
height: 100%; height: 100%;
width: 100%;
position: relative;
overflow: hidden; overflow: hidden;
background: var(--color-bg-card);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-sm);
} }
/* 左侧导航 */
.task-layout__sidebar { .task-layout__sidebar {
width: 220px; width: 220px;
background: var(--color-surface); background: transparent;
border-right: 1px solid var(--color-border); border-right: 1px solid var(--color-gray-200);
flex-shrink: 0; flex-shrink: 0;
overflow-y: auto; overflow-y: auto;
@media (max-width: 1199px) {
width: 200px;
}
@media (max-width: 767px) {
width: 100%;
position: absolute;
top: 0;
left: 0;
z-index: 10;
transform: translateX(-100%);
transition: transform 0.3s ease;
&.is-mobile-open {
transform: translateX(0);
}
}
} }
/* 导航头部 */
.task-layout__nav-header { .task-layout__nav-header {
height: 64px; height: 56px;
display: flex; display: flex;
align-items: center; align-items: center;
padding: 0 24px; padding: 0 var(--space-6);
border-bottom: 1px solid var(--color-border); border-bottom: 1px solid var(--color-gray-200);
flex-shrink: 0;
} }
.task-layout__nav-title { .task-layout__nav-title {
margin: 0; margin: 0;
font-size: 18px; font-size: var(--font-size-lg);
font-weight: 600; font-weight: var(--font-weight-semibold);
color: var(--color-text); color: var(--color-gray-800);
line-height: 1.4;
} }
/* 导航列表 */
.task-layout__nav-list { .task-layout__nav-list {
list-style: none; list-style: none;
padding: var(--space-1) 0; padding: var(--space-2) 0;
margin: 0; margin: 0;
} }
/* 导航项 */
.task-layout__nav-item { .task-layout__nav-item {
margin: 4px var(--space-2); margin: var(--space-1) var(--space-3);
&.is-active { &.is-active .task-layout__nav-link {
.task-layout__nav-link { background: var(--color-primary-500);
background: var(--color-primary); color: #fff;
box-shadow: var(--shadow-sm);
.nav-icon {
color: #fff; color: #fff;
.nav-icon {
color: #fff;
}
} }
} }
} }
/* 导航链接 */
.task-layout__nav-link { .task-layout__nav-link {
display: flex; display: flex;
align-items: center; align-items: center;
padding: var(--space-2) var(--space-2); padding: var(--space-3);
border-radius: var(--radius-card); border-radius: var(--radius-md);
color: var(--color-text-secondary); color: var(--color-gray-600);
text-decoration: none;
transition: all 0.2s;
cursor: pointer; cursor: pointer;
transition: all var(--duration-fast) var(--ease-out);
&:hover { &:hover {
background: var(--color-bg-2); background: var(--color-gray-100);
color: var(--color-primary); color: var(--color-primary-500);
} }
.is-active & { .is-active &:hover {
&:hover { background: var(--color-primary-600);
background: var(--color-primary);
color: #fff;
}
}
}
/* 导航图标 */
.nav-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 18px;
height: 18px;
margin-right: var(--space-2);
color: var(--color-text-3);
transition: color 0.2s;
.task-layout__nav-item.is-active & {
color: #fff; color: #fff;
} }
} }
/* 导航文本 */ .nav-icon {
.nav-text { width: 18px;
font-size: 14px; height: 18px;
font-weight: 500; margin-right: var(--space-2);
color: var(--color-gray-500);
}
.nav-text {
font-size: var(--font-size-base);
font-weight: var(--font-weight-medium);
} }
/* 右侧内容 */
.task-layout__content { .task-layout__content {
flex: 1; flex: 1;
overflow: auto; overflow: auto;
background: var(--color-bg); background: var(--color-bg-page);
padding: 0;
@media (max-width: 767px) {
padding: 0;
}
} }
/* 过渡动画 */
.fade-enter-active, .fade-enter-active,
.fade-leave-active { .fade-leave-active {
transition: opacity 0.25s ease; transition: opacity var(--duration-base) var(--ease-out);
} }
.fade-enter-from, .fade-enter-from,

View File

@@ -1,14 +1,14 @@
<template> <template>
<div class="mix-task-page"> <div class="task-page">
<!-- 筛选条件 --> <!-- 筛选条件 -->
<div class="mix-task-page__filters"> <div class="task-page__filters">
<a-space :size="16"> <a-space :size="16">
<a-select <a-select
v-model:value="filters.status" v-model:value="filters.status"
class="filter-select" class="filter-select"
placeholder="任务状态" placeholder="任务状态"
@change="handleFilterChange"
allow-clear allow-clear
@change="handleFilterChange"
> >
<a-select-option value="">全部状态</a-select-option> <a-select-option value="">全部状态</a-select-option>
<a-select-option value="pending">待处理</a-select-option> <a-select-option value="pending">待处理</a-select-option>
@@ -24,9 +24,7 @@
allow-clear allow-clear
@press-enter="handleFilterChange" @press-enter="handleFilterChange"
> >
<template #prefix> <template #prefix><SearchOutlined /></template>
<SearchOutlined />
</template>
</a-input> </a-input>
<a-range-picker <a-range-picker
@@ -38,177 +36,138 @@
@change="handleFilterChange" @change="handleFilterChange"
/> />
<a-button type="primary" class="filter-button" @click="handleFilterChange"> <a-button type="primary" @click="handleFilterChange">查询</a-button>
查询 <a-button @click="handleResetFilters">重置</a-button>
</a-button>
<a-button class="filter-button" @click="handleResetFilters">
重置
</a-button>
</a-space> </a-space>
</div> </div>
<!-- 任务列表 --> <!-- 任务列表 -->
<div class="mix-task-page__content"> <div class="task-page__content">
<a-spin :spinning="loading" tip="加载中..."> <a-spin :spinning="loading" tip="加载中...">
<a-table <a-table
:data-source="list" :data-source="list"
:columns="columns" :columns="columns"
:row-key="record => record.id" :row-key="record => record.id"
:pagination="paginationConfig" :pagination="paginationConfig"
@change="handleTableChange"
:expanded-row-keys="expandedRowKeys" :expanded-row-keys="expandedRowKeys"
@expandedRowsChange="handleExpandedRowsChange"
:scroll="{ x: 'max-content' }" :scroll="{ x: 'max-content' }"
@change="handleTableChange"
@expandedRowsChange="handleExpandedRowsChange"
> >
<!-- 标题列 -->
<template #bodyCell="{ column, record }"> <template #bodyCell="{ column, record }">
<!-- 标题列 -->
<template v-if="column.key === 'title'"> <template v-if="column.key === 'title'">
<div class="title-cell"> <div class="title-cell">
<strong>{{ record.title }}</strong> <strong>{{ record.title }}</strong>
<a-tag v-if="record.text" size="small" style="margin-left: 8px">有文案</a-tag> <a-tag v-if="record.text" size="small">有文案</a-tag>
</div> </div>
</template> </template>
<!-- 状态列 --> <!-- 状态列 -->
<template v-else-if="column.key === 'status'"> <template v-else-if="column.key === 'status'">
<a-tag :color="getStatusColor(record.status)" class="status-tag"> <TaskStatusTag :status="record.status" />
{{ getStatusText(record.status) }}
</a-tag>
</template> </template>
<!-- 创建时间列 --> <!-- 时间列 -->
<template v-else-if="column.key === 'createTime'"> <template v-else-if="column.key === 'createTime'">
{{ formatDate(record.createTime) }} {{ formatDate(record.createTime) }}
</template> </template>
<!-- 完成时间列 -->
<template v-else-if="column.key === 'finishTime'"> <template v-else-if="column.key === 'finishTime'">
{{ record.finishTime ? formatDate(record.finishTime) : '-' }} {{ record.finishTime ? formatDate(record.finishTime) : '-' }}
</template> </template>
<!-- 生成结果列 --> <!-- 生成结果列 -->
<template v-else-if="column.key === 'outputUrls'"> <template v-else-if="column.key === 'outputUrls'">
<div v-if="record.outputUrls && record.outputUrls.length > 0"> <a-tag v-if="record.outputUrls?.length" color="success">
<a-tag color="success">{{ record.outputUrls.length }} 个视频</a-tag> {{ record.outputUrls.length }} 个视频
</div> </a-tag>
<span v-else>-</span> <span v-else class="text-muted">-</span>
</template> </template>
<!-- 操作列 (增强版:预览+下载+其他操作) --> <!-- 操作列 -->
<template v-else-if="column.key === 'actions'"> <template v-else-if="column.key === 'actions'">
<a-space> <a-space>
<!-- 预览按钮 -->
<a-button <a-button
v-if="isStatus(record.status, 'success') && record.outputUrls && record.outputUrls.length > 0" v-if="canOperate(record, 'preview')"
type="link" type="link"
size="small" size="small"
@click="openPreviewModal(record)" class="action-btn action-btn--primary"
class="action-btn-preview" @click="openPreview(record)"
> >
<template #icon> <PlayCircleOutlined /> 预览
<PlayCircleOutlined />
</template>
预览
</a-button> </a-button>
<!-- 下载按钮 -->
<a-button <a-button
v-if="isStatus(record.status, 'success') && record.outputUrls && record.outputUrls.length > 0" v-if="canOperate(record, 'download')"
type="link" type="link"
size="small" size="small"
class="action-btn action-btn--success"
@click="handleDownload(record)" @click="handleDownload(record)"
class="action-btn-download"
> >
<template #icon> <DownloadOutlined /> 下载
<DownloadOutlined />
</template>
下载
</a-button> </a-button>
<!-- 取消按钮 -->
<a-button <a-button
v-if="isStatus(record.status, 'running')" v-if="canOperate(record, 'cancel')"
size="small" size="small"
@click="handleCancel(record.id)" @click="handleCancel(record.id)"
> >
取消 取消
</a-button> </a-button>
<!-- 重试按钮 -->
<a-button <a-button
v-if="isStatus(record.status, 'failed')" v-if="canOperate(record, 'retry')"
size="small" size="small"
@click="handleRetry(record.id)" @click="handleRetry(record.id)"
> >
重试 重试
</a-button> </a-button>
<!-- 删除按钮 --> <a-popconfirm title="确定删除删除后无法恢复" @confirm="handleDelete(record.id)">
<a-popconfirm <a-button type="link" size="small" class="action-btn action-btn--danger">删除</a-button>
title="确定删除这个任务吗删除后无法恢复"
@confirm="() => handleDelete(record.id)"
>
<a-button size="small" type="link" class="action-btn-delete">删除</a-button>
</a-popconfirm> </a-popconfirm>
</a-space> </a-space>
</template> </template>
</template> </template>
<!-- 展开行内容 (优化版) --> <!-- 展开行内容 -->
<template #expandedRowRender="{ record }"> <template #expandedRowRender="{ record }">
<div class="expanded-content"> <div class="expanded-content">
<!-- 任务详情 -->
<div v-if="record.text" class="task-text"> <div v-if="record.text" class="task-text">
<strong>文案内容:</strong> <strong>文案内容:</strong>
<p>{{ record.text }}</p> <p>{{ record.text }}</p>
</div> </div>
<!-- 生成结果 --> <div v-if="record.outputUrls?.length" class="task-results">
<div v-if="record.outputUrls && record.outputUrls.length > 0" class="task-results">
<div class="result-header"> <div class="result-header">
<strong>生成结果:</strong> <strong>生成结果:</strong>
<span class="result-count">{{ record.outputUrls.length }} 个视频</span> <span class="result-count">{{ record.outputUrls.length }} 个视频</span>
</div> </div>
<div class="result-list"> <div class="result-list">
<div <div v-for="(_, index) in record.outputUrls" :key="index" class="result-item">
v-for="(url, index) in record.outputUrls"
:key="index"
class="result-item"
>
<a-button <a-button
v-if="isStatus(record.status, 'success')" v-if="isStatus(record.status, 'success')"
type="link" type="link"
size="small" size="small"
@click="handlePreviewSingle(record, index)" @click="previewVideo(record, index)"
class="result-preview-btn"
> >
<PlayCircleOutlined /> <PlayCircleOutlined /> 视频 {{ index + 1 }}
视频 {{ index + 1 }}
</a-button> </a-button>
<a-button <a-button
v-if="isStatus(record.status, 'success')" v-if="isStatus(record.status, 'success')"
type="link" type="link"
size="small" size="small"
@click="handleDownloadSingle(record.id, index)" @click="downloadVideo(record.id, index)"
class="result-download-btn"
> >
<DownloadOutlined /> <DownloadOutlined />
</a-button> </a-button>
<span v-else class="processing-tip"> <span v-else class="text-muted">视频 {{ index + 1 }} (处理中...)</span>
视频 {{ index + 1 }} (处理中...)
</span>
</div> </div>
</div> </div>
</div> </div>
<!-- 错误信息 --> <a-alert v-if="record.errorMsg" type="error" :message="record.errorMsg" show-icon />
<div v-if="record.errorMsg" class="task-error">
<a-alert
type="error"
:message="record.errorMsg"
show-icon
/>
</div>
</div> </div>
</template> </template>
</a-table> </a-table>
@@ -216,21 +175,9 @@
</div> </div>
<!-- 预览模态框 --> <!-- 预览模态框 -->
<a-modal <a-modal v-model:open="preview.visible" :title="preview.title" width="800px" :footer="null" centered>
v-model:open="previewVisible" <div v-if="preview.url" class="preview-container">
:title="previewTitle" <video :src="preview.url" controls autoplay class="preview-video">
width="800px"
:footer="null"
:centered="true"
class="preview-modal"
>
<div v-if="previewUrl" class="preview-container">
<video
:src="previewUrl"
controls
autoplay
style="width: 100%; max-height: 600px; border-radius: 8px;"
>
您的浏览器不支持视频播放 您的浏览器不支持视频播放
</video> </video>
</div> </div>
@@ -242,76 +189,64 @@
</template> </template>
<script setup> <script setup>
import { ref, onMounted } from 'vue' import { ref, reactive, onMounted } from 'vue'
import { import { SearchOutlined, PlayCircleOutlined, DownloadOutlined } from '@ant-design/icons-vue'
SearchOutlined,
PlayCircleOutlined,
DownloadOutlined
} from '@ant-design/icons-vue'
import { MixTaskService } from '@/api/mixTask' import { MixTaskService } from '@/api/mixTask'
import { formatDate } from '@/utils/file' import { formatDate } from '@/utils/file'
import { useTaskList } from '@/views/system/task-management/composables/useTaskList' import { useTaskList } from '@/views/system/task-management/composables/useTaskList'
import { useTaskOperations } from '@/views/system/task-management/composables/useTaskOperations' import { useTaskOperations } from '@/views/system/task-management/composables/useTaskOperations'
import { useTaskPolling } from '@/views/system/task-management/composables/useTaskPolling' import { useTaskPolling } from '@/views/system/task-management/composables/useTaskPolling'
import TaskStatusTag from '@/views/system/task-management/components/TaskStatusTag.vue'
// 使用 Composable // Composables
const { const { loading, list, filters, paginationConfig, fetchList, handleFilterChange, handleResetFilters, handleTableChange } = useTaskList(MixTaskService.getTaskPage)
loading, const { handleDelete, handleCancel, handleRetry, handleBatchDownload } = useTaskOperations(
list, { deleteApi: MixTaskService.deleteTask, cancelApi: MixTaskService.cancelTask, retryApi: MixTaskService.retryTask, getSignedUrlsApi: MixTaskService.getSignedUrls },
filters,
paginationConfig,
fetchList,
handleFilterChange,
handleResetFilters,
handleTableChange,
buildParams
} = useTaskList(MixTaskService.getTaskPage)
// 使用任务操作 Composable
const {
handleDelete,
handleCancel,
handleRetry,
handleBatchDownload
} = useTaskOperations(
{
deleteApi: MixTaskService.deleteTask,
cancelApi: MixTaskService.cancelTask,
retryApi: MixTaskService.retryTask,
getSignedUrlsApi: MixTaskService.getSignedUrls
},
fetchList fetchList
) )
useTaskPolling(MixTaskService.getTaskPage, { onTaskUpdate: fetchList })
// 预览相关状态 // 展开行
const previewVisible = ref(false) const expandedRowKeys = ref([])
const previewUrl = ref('') const handleExpandedRowsChange = (keys) => { expandedRowKeys.value = keys }
const previewTitle = ref('')
// 预览单个视频 // 预览状态
const handlePreviewSingle = async (record, index) => { const preview = reactive({ visible: false, title: '', url: '' })
// 状态判断
const isStatus = (status, target) => status === target || status === target.toUpperCase()
const canOperate = (record, action) => {
const isSuccess = isStatus(record.status, 'success')
const hasUrls = record.outputUrls?.length > 0
const actions = {
preview: isSuccess && hasUrls,
download: isSuccess && hasUrls,
cancel: isStatus(record.status, 'running'),
retry: isStatus(record.status, 'failed')
}
return actions[action]
}
// 预览视频
const previewVideo = async (record, index) => {
preview.title = `${record.title} - 视频 ${index + 1}`
preview.visible = true
preview.url = ''
try { try {
previewTitle.value = `${record.title} - 视频 ${index + 1}`
previewVisible.value = true
previewUrl.value = ''
// 获取签名URL
const res = await MixTaskService.getSignedUrls(record.id) const res = await MixTaskService.getSignedUrls(record.id)
if (res.code === 0 && res.data && res.data[index]) { if (res.code === 0 && res.data?.[index]) preview.url = res.data[index]
previewUrl.value = res.data[index] } catch (e) {
} else { console.error('获取预览链接失败:', e)
console.warn('获取预览链接失败')
}
} catch (error) {
console.error('获取预览链接失败:', error)
} }
} }
// 下载单个视频 const openPreview = (record) => previewVideo(record, 0)
const handleDownloadSingle = async (taskId, index) => {
// 下载视频
const downloadVideo = async (taskId, index) => {
try { try {
const res = await MixTaskService.getSignedUrls(taskId) const res = await MixTaskService.getSignedUrls(taskId)
if (res.code === 0 && res.data && res.data[index]) { if (res.code === 0 && res.data?.[index]) {
const link = document.createElement('a') const link = document.createElement('a')
link.href = res.data[index] link.href = res.data[index]
link.download = `video_${taskId}_${index + 1}.mp4` link.download = `video_${taskId}_${index + 1}.mp4`
@@ -319,242 +254,112 @@ const handleDownloadSingle = async (taskId, index) => {
document.body.appendChild(link) document.body.appendChild(link)
link.click() link.click()
document.body.removeChild(link) document.body.removeChild(link)
} else {
console.warn('获取下载链接失败')
} }
} catch (error) { } catch (e) {
console.error('获取下载链接失败:', error) console.error('获取下载链接失败:', e)
} }
} }
// 预览任务(主列表) const handleDownload = (record) => {
const openPreviewModal = async (record) => { if (record.outputUrls?.length) handleBatchDownload([], MixTaskService.getSignedUrls, record.id)
await handlePreviewSingle(record, 0)
}
// 下载任务
const handleDownload = async (record) => {
if (record.outputUrls && record.outputUrls.length > 0) {
await handleBatchDownload(
[],
MixTaskService.getSignedUrls,
record.id
)
}
}
// 使用轮询 Composable
useTaskPolling(MixTaskService.getTaskPage, {
onTaskUpdate: () => {
fetchList()
}
})
// 扩展行键
const expandedRowKeys = ref([])
// 处理展开行变化
const handleExpandedRowsChange = (keys) => {
expandedRowKeys.value = keys
} }
// 表格列定义 // 表格列定义
const columns = [ const columns = [
{ { title: 'ID', dataIndex: 'id', key: 'id', width: 70, fixed: 'left' },
title: 'ID', { title: '标题', dataIndex: 'title', key: 'title', ellipsis: true },
dataIndex: 'id', { title: '状态', dataIndex: 'status', key: 'status', width: 90 },
key: 'id', { title: '生成结果', dataIndex: 'outputUrls', key: 'outputUrls', width: 100 },
width: 70, { title: '创建时间', dataIndex: 'createTime', key: 'createTime', width: 160 },
fixed: 'left' { title: '完成时间', dataIndex: 'finishTime', key: 'finishTime', width: 160 },
}, { title: '操作', key: 'actions', width: 240, fixed: 'right' }
{
title: '标题',
dataIndex: 'title',
key: 'title',
ellipsis: true
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
width: 90
},
{
title: '生成结果',
dataIndex: 'outputUrls',
key: 'outputUrls',
width: 100
},
{
title: '创建时间',
dataIndex: 'createTime',
key: 'createTime',
width: 160
},
{
title: '完成时间',
dataIndex: 'finishTime',
key: 'finishTime',
width: 160
},
{
title: '操作',
key: 'actions',
width: 240,
fixed: 'right'
}
] ]
// 状态映射函数 onMounted(fetchList)
const getStatusText = (status) => {
const statusMap = {
pending: '待处理',
running: '处理中',
success: '已完成',
failed: '失败'
}
return statusMap[status] || status
}
const getStatusColor = (status) => {
const colorMap = {
pending: 'default',
running: 'processing',
success: 'success',
failed: 'error'
}
return colorMap[status] || 'default'
}
const getProgressStatus = (status) => {
const statusMap = {
pending: 'normal',
running: 'active',
success: 'success',
failed: 'exception',
// 大写状态支持
PENDING: 'normal',
RUNNING: 'active',
SUCCESS: 'success',
FAILED: 'exception'
}
return statusMap[status] || 'normal'
}
// 检查状态(同时支持大小写)
const isStatus = (status, targetStatus) => {
return status === targetStatus || status === targetStatus.toUpperCase()
}
// 删除未使用的方法
// handleDownloadSignedUrl 和 handleDownloadAll 已被移除
// 初始化
onMounted(() => {
fetchList()
})
</script> </script>
<style scoped lang="less"> <style scoped lang="less">
.mix-task-page { .task-page {
padding: 0 var(--space-3); padding: var(--space-4);
height: 100%; height: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: var(--space-3); gap: var(--space-4);
}
&__filters { .task-page__filters {
padding: var(--space-3); padding: var(--space-4);
background: var(--color-surface); background: var(--color-bg-card);
border-radius: var(--radius-card); border-radius: var(--radius-lg);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03); box-shadow: var(--shadow-sm);
.filter-select,
.filter-input {
width: 200px;
}
.filter-date-picker {
width: 280px;
}
.filter-select,
.filter-input {
width: 200px;
} }
&__content { .filter-date-picker {
flex: 1; width: 280px;
overflow: auto;
background: var(--color-surface);
border-radius: var(--radius-card);
padding: var(--space-3);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03);
} }
} }
/* 标题单元格 */ .task-page__content {
flex: 1;
overflow: auto;
background: var(--color-bg-card);
border-radius: var(--radius-lg);
padding: var(--space-4);
box-shadow: var(--shadow-sm);
}
.title-cell { .title-cell {
display: flex; display: flex;
align-items: center; align-items: center;
gap: var(--space-1); gap: var(--space-2);
} }
/* 操作按钮样式 */ .text-muted {
.action-btn-preview { color: var(--color-gray-400);
color: var(--color-primary);
&:hover {
color: var(--color-primary-hover, var(--color-blue-600));
}
} }
.action-btn-download { .action-btn {
color: var(--color-success); &--primary { color: var(--color-primary-500); &:hover { color: var(--color-primary-600); } }
&--success { color: var(--color-success-500); &:hover { color: var(--color-success-600); } }
&:hover { &--danger { color: var(--color-error-500); &:hover { color: var(--color-error-600); } }
color: #059669;
}
} }
.action-btn-delete {
color: var(--color-error);
&:hover {
color: #dc2626;
}
}
/* 展开内容 */
.expanded-content { .expanded-content {
padding: var(--space-3); padding: var(--space-4);
background: var(--color-bg-2); background: var(--color-gray-50);
border-radius: var(--radius-card); border-radius: var(--radius-md);
margin: var(--space-2); margin: var(--space-2);
} }
.task-text { .task-text {
margin-bottom: var(--space-3); margin-bottom: var(--space-4);
p { p {
margin: var(--space-2) 0 0 0; margin: var(--space-2) 0 0;
padding: var(--space-2); padding: var(--space-3);
background: var(--color-surface); background: var(--color-gray-100);
border-radius: var(--radius-card); border-radius: var(--radius-md);
line-height: 1.6; line-height: var(--line-height-base);
} }
} }
.task-results { .task-results {
margin-bottom: var(--space-3); margin-bottom: var(--space-4);
.result-header { .result-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
margin-bottom: var(--space-2); margin-bottom: var(--space-2);
}
.result-count { .result-count {
font-size: 12px; font-size: var(--font-size-xs);
color: var(--color-text-3, #8c8c8c); color: var(--color-gray-500);
}
} }
.result-list { .result-list {
@@ -562,72 +367,20 @@ onMounted(() => {
flex-wrap: wrap; flex-wrap: wrap;
gap: var(--space-2); gap: var(--space-2);
margin-top: var(--space-2); margin-top: var(--space-2);
.result-item {
display: flex;
align-items: center;
gap: var(--space-2);
padding: var(--space-1) var(--space-2);
background: var(--color-surface);
border-radius: var(--radius-card);
font-size: 13px;
transition: all 0.2s;
&:hover {
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.result-preview-btn {
color: var(--color-primary);
padding: 0;
height: auto;
&:hover {
color: var(--color-primary-hover, var(--color-blue-600));
}
}
.result-download-btn {
color: var(--color-success);
padding: 0;
height: auto;
&:hover {
color: #059669;
}
}
}
} }
}
.processing-tip { .result-item {
color: var(--color-text-3); display: flex;
font-size: 12px; align-items: center;
} gap: var(--space-2);
padding: var(--space-2) var(--space-3);
background: var(--color-gray-100);
border-radius: var(--radius-md);
transition: box-shadow var(--duration-fast) var(--ease-out);
.task-error { &:hover {
margin-bottom: var(--space-2); box-shadow: var(--shadow-sm);
} }
/* 确保按钮内的图标和文字对齐 */
:deep(.ant-btn .anticon) {
line-height: 0;
}
/* 表格样式 */
:deep(.ant-table-tbody > tr > td) {
padding: 12px 8px;
}
:deep(.ant-table-thead > tr > th) {
background: var(--color-bg-2);
font-weight: 600;
}
/* 预览模态框样式 */
.preview-modal {
:deep(.ant-modal-body) {
padding: var(--space-3);
} }
} }
@@ -638,6 +391,12 @@ onMounted(() => {
min-height: 200px; min-height: 200px;
} }
.preview-video {
width: 100%;
max-height: 600px;
border-radius: var(--radius-lg);
}
.preview-loading { .preview-loading {
display: flex; display: flex;
justify-content: center; justify-content: center;
@@ -645,5 +404,16 @@ onMounted(() => {
min-height: 200px; min-height: 200px;
} }
/* 桌面端样式优化 */ :deep(.ant-table-tbody > tr > td) {
padding: var(--space-3) var(--space-2);
}
:deep(.ant-table-thead > tr > th) {
background: var(--color-gray-50);
font-weight: var(--font-weight-semibold);
}
:deep(.ant-btn .anticon) {
line-height: 0;
}
</style> </style>

View File

@@ -216,7 +216,7 @@ onMounted(async () => {
<div class="stat-content"> <div class="stat-content">
<div class="stat-label">剩余积分</div> <div class="stat-label">剩余积分</div>
<div class="stat-value">{{ formatCredits(userStore.remainingPoints) }}</div> <div class="stat-value">{{ formatCredits(userStore.remainingPoints) }}</div>
<div class="stat-desc">用于AI生成消耗</div> <div class="stat-desc">用于生成消耗</div>
</div> </div>
</a-card> </a-card>
</a-col> </a-col>