From 207f48f7d8dfe3b901b082f427ba06f9c0dfdefe Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E9=BB=84=E8=87=AA=E8=BE=BE?= <980324341@qq.com>
Date: Sat, 5 Sep 2026 23:24:11 +0800
Subject: [PATCH] =?UTF-8?q?align(=E8=A7=86=E9=A2=91=E4=BB=BB=E5=8A=A1):=20?=
=?UTF-8?q?=E5=8D=A1=E7=89=87=E8=A1=A5=E4=BB=BB=E5=8A=A1=E6=A8=A1=E5=BC=8F?=
=?UTF-8?q?/=E7=94=9F=E6=88=90=E6=97=B6=E9=97=B4(completed||submitted)/?=
=?UTF-8?q?=E8=B0=83=E8=AF=95=E9=93=BE=E6=8E=A5=E5=A4=8D=E5=88=B6=E4=BF=A1?=
=?UTF-8?q?=E6=81=AF=E8=A1=8C=E3=80=81=E4=B8=8B=E8=BD=BD=E6=8C=89=E9=92=AE?=
=?UTF-8?q?=E6=97=A0=E8=A7=86=E9=A2=91=E7=A6=81=E7=94=A8=E3=80=81=E6=98=8E?=
=?UTF-8?q?=E7=BB=86=E8=A1=A5=E6=A8=A1=E5=BC=8F/=E7=94=9F=E6=88=90?=
=?UTF-8?q?=E6=97=B6=E9=97=B4/=E8=B0=83=E8=AF=95=E9=93=BE=E6=8E=A5?=
=?UTF-8?q?=EF=BC=88=E5=AF=B9=E9=BD=90=20admin.js=20renderImageVideoCard/c?=
=?UTF-8?q?opyImageVideoDebugUrl=EF=BC=89?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../src/pages/tasks/ImageVideoTasksPage.vue | 31 ++++++---
.../src/pages/tasks/image-video-model.ts | 14 +++++
.../tests/align-image-video.test.ts | 63 +++++++++++++++++++
3 files changed, 101 insertions(+), 7 deletions(-)
create mode 100644 admin-frontend-vue/tests/align-image-video.test.ts
diff --git a/admin-frontend-vue/src/pages/tasks/ImageVideoTasksPage.vue b/admin-frontend-vue/src/pages/tasks/ImageVideoTasksPage.vue
index 35a06c5d..e1043913 100644
--- a/admin-frontend-vue/src/pages/tasks/ImageVideoTasksPage.vue
+++ b/admin-frontend-vue/src/pages/tasks/ImageVideoTasksPage.vue
@@ -6,7 +6,7 @@ import { ElMessage } from 'element-plus'
import { requestVideoZipDownload, fetchImageVideoTasks, fetchImageVideoTaskDetail } from './image-video-api.ts'
import { fetchImageVideoPermissionUsers, saveImageVideoPermissions } from './image-video-api.ts'
import { createImageVideoFilter, type ImageVideoFilter } from './image-video-filter.ts'
-import type { ImageVideoRow } from './image-video-model.ts'
+import { imageVideoGeneratedAt, type ImageVideoRow } from './image-video-model.ts'
import type { ImageVideoVideo, ImageVideoDetail } from './image-video-model.ts'
import {
allCardsSelected,
@@ -284,14 +284,23 @@ onMounted(load)
@@ -302,6 +311,7 @@ onMounted(load)
text
type="success"
size="small"
+ :disabled="!video.displayUrl"
:loading="downloadingKey === `${task.taskId}:${index}`"
@click="downloadVideo(task, index, video)"
>
@@ -323,7 +333,13 @@ onMounted(load)
{{ detail.taskId }}
{{ detail.username }}
{{ statusLabel(detail.status) }}
+ {{ detail.mode || '—' }}
{{ formatDateTime(detail.submittedAt) }}
+ {{ formatDateTime(imageVideoGeneratedAt(detail)) }}
+
+ 复制链接
+ —
+
{{ detail.cozeExecuteId || '—' }}
{{ detail.errorMessage || '—' }}
@@ -370,9 +386,10 @@ onMounted(load)
.task-card-head { display: flex; align-items: center; gap: 12px; padding: 12px 16px; border-bottom: 1px solid var(--el-border-color-lighter); }
.task-card-title { display: flex; align-items: center; gap: 10px; min-width: 0; }
.task-id { font-weight: 600; color: var(--el-text-color-primary); }
-.task-user { color: var(--el-text-color-regular); }
-.task-group { color: var(--el-text-color-secondary); font-size: 12px; }
-.task-time { margin-left: auto; color: var(--el-text-color-secondary); font-size: 12.5px; }
+.task-card-info { display: grid; grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); gap: 6px 14px; padding: 12px 16px; border-bottom: 1px dashed var(--el-border-color-lighter); }
+.task-info-row { display: flex; align-items: center; gap: 8px; font-size: 12.5px; min-width: 0; }
+.task-info-row label { flex: none; color: var(--el-text-color-secondary); min-width: 60px; }
+.task-info-row span { color: var(--el-text-color-regular); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.video-list { padding: 6px 16px; }
.video-row { display: flex; align-items: center; gap: 12px; padding: 10px 0; border-bottom: 1px dashed var(--el-border-color-lighter); }
.video-row:last-child { border-bottom: 0; }
diff --git a/admin-frontend-vue/src/pages/tasks/image-video-model.ts b/admin-frontend-vue/src/pages/tasks/image-video-model.ts
index 2f755166..1bb4d42f 100644
--- a/admin-frontend-vue/src/pages/tasks/image-video-model.ts
+++ b/admin-frontend-vue/src/pages/tasks/image-video-model.ts
@@ -22,6 +22,8 @@ export interface ImageVideoRow {
status: TaskStatus
cozeStatus: string
cozeExecuteId: string
+ /** 任务模式(对齐 admin.js 卡片「任务模式」信息行)。 */
+ mode?: string
videos: ImageVideoVideo[]
videoUrl: string
debugUrl: string
@@ -104,9 +106,21 @@ export function toImageVideoRow(raw: unknown): ImageVideoRow | null {
if (submittedAt) task.submittedAt = submittedAt
const completedAt = text(r.completed_at ?? r.completedAt)
if (completedAt) task.completedAt = completedAt
+ const mode = text(r.mode)
+ if (mode) task.mode = mode
return task
}
+/** 生成时间口径(对齐 admin.js:1331 completed_at || submitted_at || '-')。 */
+export function imageVideoGeneratedAt(task: Pick): string {
+ return task.completedAt || task.submittedAt || '-'
+}
+
+/** 卡片是否带可播放/可下载视频(对齐 admin.js hasVideo 判定)。 */
+export function imageVideoHasDisplayUrl(card: Pick): boolean {
+ return !!(card.video && card.video.displayUrl)
+}
+
/** 把任务行展平为“任务:视频下标”卡片;无 videos 的任务展为一张空卡。 */
export function flattenImageVideoTasks(items: readonly ImageVideoRow[]): ImageVideoCard[] {
const cards: ImageVideoCard[] = []
diff --git a/admin-frontend-vue/tests/align-image-video.test.ts b/admin-frontend-vue/tests/align-image-video.test.ts
new file mode 100644
index 00000000..89d4b09c
--- /dev/null
+++ b/admin-frontend-vue/tests/align-image-video.test.ts
@@ -0,0 +1,63 @@
+import test from 'node:test'
+import assert from 'node:assert/strict'
+import { readSource } from './helpers.ts'
+import {
+ imageVideoGeneratedAt,
+ imageVideoHasDisplayUrl,
+ toImageVideoRow,
+ type ImageVideoCard,
+ type ImageVideoRow,
+} from '../src/pages/tasks/image-video-model.ts'
+
+/** 对齐 admin.js:1328-1353 renderImageVideoCard:任务模式/生成时间(completed||submitted)/调试链接复制/下载禁用。 */
+
+function row(overrides: Partial = {}): ImageVideoRow {
+ return {
+ taskId: '1',
+ username: '张三',
+ groupName: 'A组',
+ status: 'RUNNING',
+ cozeStatus: '',
+ cozeExecuteId: '',
+ videos: [],
+ videoUrl: '',
+ debugUrl: '',
+ archiveStatus: '',
+ archiveError: '',
+ ...overrides,
+ }
+}
+
+test('align_image_video_mode_parsed_from_row', () => {
+ const task = toImageVideoRow({ task_id: 7, mode: 'text2video' })
+ assert.equal(task?.mode, 'text2video')
+ assert.equal(toImageVideoRow({ task_id: 8 })?.mode, undefined)
+})
+
+test('align_image_video_generated_at_completed_first', () => {
+ assert.equal(imageVideoGeneratedAt(row({ submittedAt: '2026-09-01 10:00:00', completedAt: '2026-09-01 10:05:00' })), '2026-09-01 10:05:00')
+ assert.equal(imageVideoGeneratedAt(row({ submittedAt: '2026-09-01 10:00:00' })), '2026-09-01 10:00:00')
+ assert.equal(imageVideoGeneratedAt(row()), '-')
+})
+
+test('align_image_video_has_display_url', () => {
+ const card: ImageVideoCard = {
+ key: '1:0',
+ task: row(),
+ video: { sourceUrl: '', archivedUrl: '', displayUrl: 'https://cdn/1.mp4', objectKey: '', archiveStatus: '', archiveError: '' },
+ videoIndex: 0,
+ }
+ assert.equal(imageVideoHasDisplayUrl(card), true)
+ assert.equal(imageVideoHasDisplayUrl({ ...card, video: null }), false)
+})
+
+test('align_image_video_page_card_info_wiring', () => {
+ const page = readSource('src/pages/tasks/ImageVideoTasksPage.vue')
+ assert.match(page, /任务模式/, '卡片补任务模式信息行')
+ assert.match(page, /所属分组/, '卡片信息行对齐参考')
+ assert.match(page, /生成时间/, '生成时间口径 completed||submitted')
+ assert.match(page, /imageVideoGeneratedAt/, '生成时间走纯模型')
+ assert.match(page, /调试链接/, '卡片补调试链接行')
+ assert.match(page, /copyLink\(task\.debugUrl\)/, '调试链接复制接线')
+ assert.match(page, /:disabled="!video\.displayUrl"|!hasDisplayUrl/, '下载按钮无视频禁用')
+})