feat: build image optimizer page
This commit is contained in:
@@ -1,12 +1,255 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import {
|
||||
EyeOutlined,
|
||||
PlayCircleOutlined,
|
||||
ReloadOutlined,
|
||||
StopOutlined,
|
||||
UploadOutlined,
|
||||
} from '@ant-design/icons-vue'
|
||||
import ModulePageShell from '../../components/template/ModulePageShell.vue'
|
||||
import {
|
||||
CancelTask,
|
||||
CreateTask,
|
||||
GetTask,
|
||||
ListTasks,
|
||||
} from '../../../bindings/cmbone/internal/services/imageoptimizationservice'
|
||||
|
||||
const summary = [
|
||||
{ label: '待处理', value: '0', tone: 'slate' as const },
|
||||
{ label: '处理中', value: '0', tone: 'blue' as const },
|
||||
{ label: '成功', value: '0', tone: 'green' as const },
|
||||
{ label: '失败', value: '0', tone: 'red' as const },
|
||||
type ImageTask = {
|
||||
id: number
|
||||
sourcePath: string
|
||||
status: string
|
||||
operation: string
|
||||
durationMs: number
|
||||
errorMessage: string
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
type ImageTaskResult = {
|
||||
id: number
|
||||
taskId: number
|
||||
outputPath: string
|
||||
beforeSize: number
|
||||
afterSize: number
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
type ImageTaskDetail = {
|
||||
task: ImageTask
|
||||
result: ImageTaskResult
|
||||
}
|
||||
|
||||
const operationOptions = [
|
||||
{ value: 'enhance', label: '清晰度增强' },
|
||||
{ value: 'background', label: '背景优化' },
|
||||
{ value: 'compress', label: '压缩导出' },
|
||||
]
|
||||
|
||||
const statusOptions = [
|
||||
{ value: '', label: '全部状态' },
|
||||
{ value: 'pending', label: '待处理' },
|
||||
{ value: 'processing', label: '处理中' },
|
||||
{ value: 'succeeded', label: '成功' },
|
||||
{ value: 'failed', label: '失败' },
|
||||
{ value: 'canceled', label: '已取消' },
|
||||
]
|
||||
|
||||
const sourcePath = ref('')
|
||||
const selectedOperation = ref('enhance')
|
||||
const selectedFileName = ref('')
|
||||
const statusFilter = ref('')
|
||||
const operationFilter = ref('')
|
||||
const tasks = ref<ImageTask[]>([])
|
||||
const selectedDetail = ref<ImageTaskDetail | null>(null)
|
||||
const isLoading = ref(false)
|
||||
const isCreating = ref(false)
|
||||
const actionError = ref('')
|
||||
const actionMessage = ref('')
|
||||
|
||||
const summary = computed(() => [
|
||||
{ label: '待处理', value: String(countStatus('pending')), tone: 'slate' as const },
|
||||
{ label: '处理中', value: String(countStatus('processing')), tone: 'blue' as const },
|
||||
{ label: '成功', value: String(countStatus('succeeded')), tone: 'green' as const },
|
||||
{ label: '失败', value: String(countStatus('failed')), tone: 'red' as const },
|
||||
])
|
||||
|
||||
const selectedTask = computed(() => selectedDetail.value?.task)
|
||||
const selectedResult = computed(() => selectedDetail.value?.result)
|
||||
|
||||
onMounted(() => {
|
||||
void loadTasks()
|
||||
})
|
||||
|
||||
async function loadTasks() {
|
||||
isLoading.value = true
|
||||
actionError.value = ''
|
||||
try {
|
||||
const rows = await ListTasks({
|
||||
status: statusFilter.value,
|
||||
operation: operationFilter.value,
|
||||
limit: 50,
|
||||
offset: 0,
|
||||
}) as ImageTask[]
|
||||
tasks.value = rows
|
||||
if (rows.length === 0) {
|
||||
selectedDetail.value = null
|
||||
return
|
||||
}
|
||||
const selectedID = selectedDetail.value?.task.id
|
||||
const nextTask = rows.find((task) => task.id === selectedID) || rows[0]
|
||||
await selectTask(nextTask.id)
|
||||
} catch (error) {
|
||||
actionError.value = errorToMessage(error)
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function createTask() {
|
||||
actionError.value = ''
|
||||
actionMessage.value = ''
|
||||
if (!sourcePath.value.trim()) {
|
||||
actionError.value = '图片路径不能为空'
|
||||
return
|
||||
}
|
||||
|
||||
isCreating.value = true
|
||||
try {
|
||||
const detail = await CreateTask({
|
||||
sourcePath: sourcePath.value.trim(),
|
||||
operation: selectedOperation.value,
|
||||
}) as ImageTaskDetail
|
||||
selectedDetail.value = detail
|
||||
actionMessage.value = `任务 #${detail.task.id} 已创建`
|
||||
await loadTasks()
|
||||
} catch (error) {
|
||||
actionError.value = errorToMessage(error)
|
||||
} finally {
|
||||
isCreating.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function selectTask(id: number) {
|
||||
try {
|
||||
selectedDetail.value = await GetTask(id) as ImageTaskDetail
|
||||
} catch (error) {
|
||||
actionError.value = errorToMessage(error)
|
||||
}
|
||||
}
|
||||
|
||||
async function cancelTask(task: ImageTask) {
|
||||
actionError.value = ''
|
||||
actionMessage.value = ''
|
||||
try {
|
||||
await CancelTask(task.id)
|
||||
actionMessage.value = `任务 #${task.id} 已取消`
|
||||
await loadTasks()
|
||||
} catch (error) {
|
||||
actionError.value = errorToMessage(error)
|
||||
}
|
||||
}
|
||||
|
||||
function onFileChange(event: Event) {
|
||||
actionError.value = ''
|
||||
const input = event.target as HTMLInputElement
|
||||
const file = input.files?.[0]
|
||||
if (!file) {
|
||||
selectedFileName.value = ''
|
||||
return
|
||||
}
|
||||
|
||||
selectedFileName.value = file.name
|
||||
const fileWithPath = file as File & { path?: string }
|
||||
if (fileWithPath.path) {
|
||||
sourcePath.value = fileWithPath.path
|
||||
return
|
||||
}
|
||||
if (!sourcePath.value.trim()) {
|
||||
actionError.value = '当前文件选择器未返回完整路径,请在图片路径中粘贴绝对路径'
|
||||
}
|
||||
}
|
||||
|
||||
function countStatus(status: string) {
|
||||
return tasks.value.filter((task) => task.status === status).length
|
||||
}
|
||||
|
||||
function operationLabel(value: string) {
|
||||
return operationOptions.find((option) => option.value === value)?.label || value || '-'
|
||||
}
|
||||
|
||||
function statusLabel(status: string) {
|
||||
return statusOptions.find((option) => option.value === status)?.label || status || '-'
|
||||
}
|
||||
|
||||
function statusClass(status: string) {
|
||||
if (status === 'succeeded') {
|
||||
return 'border-emerald-200 bg-emerald-50 text-emerald-700 dark:border-emerald-900/60 dark:bg-emerald-950/40 dark:text-emerald-200'
|
||||
}
|
||||
if (status === 'failed') {
|
||||
return 'border-rose-200 bg-rose-50 text-rose-700 dark:border-rose-900/60 dark:bg-rose-950/40 dark:text-rose-200'
|
||||
}
|
||||
if (status === 'processing') {
|
||||
return 'border-blue-200 bg-blue-50 text-blue-700 dark:border-blue-900/60 dark:bg-blue-950/40 dark:text-blue-200'
|
||||
}
|
||||
if (status === 'canceled') {
|
||||
return 'border-slate-200 bg-slate-100 text-slate-600 dark:border-slate-700 dark:bg-slate-800 dark:text-slate-300'
|
||||
}
|
||||
return 'border-amber-200 bg-amber-50 text-amber-700 dark:border-amber-900/60 dark:bg-amber-950/40 dark:text-amber-200'
|
||||
}
|
||||
|
||||
function canCancel(task: ImageTask) {
|
||||
return task.status === 'pending' || task.status === 'processing'
|
||||
}
|
||||
|
||||
function formatDuration(value: number) {
|
||||
if (!value) {
|
||||
return '-'
|
||||
}
|
||||
if (value >= 1000) {
|
||||
return `${(value / 1000).toFixed(1)} s`
|
||||
}
|
||||
return `${value} ms`
|
||||
}
|
||||
|
||||
function formatSize(value: number) {
|
||||
if (!value) {
|
||||
return '-'
|
||||
}
|
||||
if (value >= 1024 * 1024) {
|
||||
return `${(value / 1024 / 1024).toFixed(2)} MB`
|
||||
}
|
||||
if (value >= 1024) {
|
||||
return `${(value / 1024).toFixed(1)} KB`
|
||||
}
|
||||
return `${value} B`
|
||||
}
|
||||
|
||||
function formatDate(value: string) {
|
||||
if (!value) {
|
||||
return '-'
|
||||
}
|
||||
const date = new Date(value)
|
||||
if (Number.isNaN(date.getTime())) {
|
||||
return value
|
||||
}
|
||||
return date.toLocaleString()
|
||||
}
|
||||
|
||||
function baseName(path: string) {
|
||||
if (!path) {
|
||||
return '-'
|
||||
}
|
||||
const normalized = path.replace(/\\/g, '/')
|
||||
return normalized.split('/').filter(Boolean).pop() || path
|
||||
}
|
||||
|
||||
function errorToMessage(error: unknown) {
|
||||
if (error instanceof Error) {
|
||||
return error.message
|
||||
}
|
||||
return String(error)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -14,63 +257,281 @@ const summary = [
|
||||
eyebrow="Business"
|
||||
title="商品图片 AI 优化"
|
||||
description="第一个主业务样例,用于沉淀任务创建、状态流转、结果预览和失败反馈。"
|
||||
status="路由可访问"
|
||||
status="后端已接入"
|
||||
:items="summary"
|
||||
>
|
||||
<div class="grid grid-cols-1 gap-4 xl:grid-cols-[360px_1fr]">
|
||||
<section class="rounded-lg border border-slate-200 bg-white p-4 dark:border-slate-800 dark:bg-slate-900">
|
||||
<h2 class="mb-4 text-base font-semibold">
|
||||
创建优化任务
|
||||
</h2>
|
||||
<div class="space-y-4">
|
||||
<label class="block">
|
||||
<span class="mb-1 block text-sm font-medium">图片文件</span>
|
||||
<input class="block w-full text-sm text-slate-600 file:mr-3 file:rounded-md file:border-0 file:bg-slate-950 file:px-3 file:py-2 file:text-sm file:font-medium file:text-white dark:text-slate-300 dark:file:bg-white dark:file:text-slate-950" type="file" accept="image/*" />
|
||||
</label>
|
||||
<label class="block">
|
||||
<span class="mb-1 block text-sm font-medium">优化类型</span>
|
||||
<select class="h-10 w-full rounded-md border border-slate-300 bg-white px-3 text-sm dark:border-slate-700 dark:bg-slate-950">
|
||||
<option>清晰度增强</option>
|
||||
<option>背景优化</option>
|
||||
<option>压缩导出</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="block">
|
||||
<span class="mb-1 block text-sm font-medium">备注</span>
|
||||
<textarea class="min-h-[96px] w-full rounded-md border border-slate-300 bg-white px-3 py-2 text-sm dark:border-slate-700 dark:bg-slate-950"></textarea>
|
||||
</label>
|
||||
<button class="h-10 w-full rounded-md bg-slate-950 text-sm font-medium text-white dark:bg-white dark:text-slate-950" type="button">
|
||||
创建任务
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="rounded-lg border border-slate-200 bg-white dark:border-slate-800 dark:bg-slate-900">
|
||||
<div class="border-b border-slate-200 p-4 dark:border-slate-800">
|
||||
<h2 class="mb-0 text-base font-semibold">
|
||||
任务列表
|
||||
<div class="grid grid-cols-1 gap-4 xl:grid-cols-[360px_minmax(0,1fr)]">
|
||||
<aside class="space-y-4">
|
||||
<section class="rounded-lg border border-slate-200 bg-white p-4 dark:border-slate-800 dark:bg-slate-900">
|
||||
<h2 class="mb-4 text-base font-semibold text-slate-950 dark:text-white">
|
||||
创建优化任务
|
||||
</h2>
|
||||
<div class="space-y-4">
|
||||
<label class="block">
|
||||
<span class="mb-1 block text-sm font-medium text-slate-700 dark:text-slate-200">选择图片</span>
|
||||
<input
|
||||
class="block w-full text-sm text-slate-600 file:mr-3 file:rounded-md file:border-0 file:bg-slate-950 file:px-3 file:py-2 file:text-sm file:font-medium file:text-white dark:text-slate-300 dark:file:bg-white dark:file:text-slate-950"
|
||||
type="file"
|
||||
accept="image/*"
|
||||
@change="onFileChange"
|
||||
>
|
||||
<span v-if="selectedFileName" class="mt-2 block truncate text-xs text-slate-500 dark:text-slate-400">
|
||||
{{ selectedFileName }}
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<label class="block">
|
||||
<span class="mb-1 block text-sm font-medium text-slate-700 dark:text-slate-200">图片路径</span>
|
||||
<input
|
||||
v-model="sourcePath"
|
||||
class="h-10 w-full rounded-md border border-slate-300 bg-white px-3 text-sm dark:border-slate-700 dark:bg-slate-950"
|
||||
placeholder="D:\images\sku-main.jpg"
|
||||
>
|
||||
</label>
|
||||
|
||||
<label class="block">
|
||||
<span class="mb-1 block text-sm font-medium text-slate-700 dark:text-slate-200">优化类型</span>
|
||||
<select
|
||||
v-model="selectedOperation"
|
||||
class="h-10 w-full rounded-md border border-slate-300 bg-white px-3 text-sm dark:border-slate-700 dark:bg-slate-950"
|
||||
>
|
||||
<option v-for="option in operationOptions" :key="option.value" :value="option.value">
|
||||
{{ option.label }}
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<button
|
||||
class="inline-flex h-10 w-full items-center justify-center gap-2 rounded-md bg-slate-950 px-4 text-sm font-medium text-white disabled:cursor-not-allowed disabled:opacity-50 dark:bg-white dark:text-slate-950"
|
||||
type="button"
|
||||
:disabled="isCreating"
|
||||
@click="createTask"
|
||||
>
|
||||
<PlayCircleOutlined />
|
||||
<span>{{ isCreating ? '创建中' : '创建任务' }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="rounded-lg border border-slate-200 bg-white p-4 dark:border-slate-800 dark:bg-slate-900">
|
||||
<h2 class="mb-4 text-base font-semibold text-slate-950 dark:text-white">
|
||||
任务筛选
|
||||
</h2>
|
||||
<div class="space-y-3">
|
||||
<select
|
||||
v-model="statusFilter"
|
||||
class="h-10 w-full rounded-md border border-slate-300 bg-white px-3 text-sm dark:border-slate-700 dark:bg-slate-950"
|
||||
>
|
||||
<option v-for="option in statusOptions" :key="option.value" :value="option.value">
|
||||
{{ option.label }}
|
||||
</option>
|
||||
</select>
|
||||
<select
|
||||
v-model="operationFilter"
|
||||
class="h-10 w-full rounded-md border border-slate-300 bg-white px-3 text-sm dark:border-slate-700 dark:bg-slate-950"
|
||||
>
|
||||
<option value="">全部类型</option>
|
||||
<option v-for="option in operationOptions" :key="option.value" :value="option.value">
|
||||
{{ option.label }}
|
||||
</option>
|
||||
</select>
|
||||
<button
|
||||
class="inline-flex h-10 w-full items-center justify-center gap-2 rounded-md border border-slate-300 bg-white px-4 text-sm font-medium text-slate-700 hover:bg-slate-50 dark:border-slate-700 dark:bg-slate-950 dark:text-slate-200 dark:hover:bg-slate-900"
|
||||
type="button"
|
||||
@click="loadTasks"
|
||||
>
|
||||
<ReloadOutlined />
|
||||
<span>刷新列表</span>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</aside>
|
||||
|
||||
<div class="min-w-0 space-y-4">
|
||||
<div
|
||||
v-if="actionError"
|
||||
class="rounded-lg border border-rose-200 bg-rose-50 px-4 py-3 text-sm text-rose-700 dark:border-rose-900/60 dark:bg-rose-950/40 dark:text-rose-200"
|
||||
>
|
||||
{{ actionError }}
|
||||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full text-left text-sm">
|
||||
<thead class="bg-slate-50 text-xs uppercase text-slate-500 dark:bg-slate-950 dark:text-slate-400">
|
||||
<tr>
|
||||
<th class="px-4 py-3 font-medium">任务</th>
|
||||
<th class="px-4 py-3 font-medium">类型</th>
|
||||
<th class="px-4 py-3 font-medium">状态</th>
|
||||
<th class="px-4 py-3 font-medium">耗时</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="px-4 py-6 text-slate-500 dark:text-slate-400" colspan="4">
|
||||
暂无任务
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div
|
||||
v-else-if="actionMessage"
|
||||
class="rounded-lg border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm text-emerald-700 dark:border-emerald-900/60 dark:bg-emerald-950/40 dark:text-emerald-200"
|
||||
>
|
||||
{{ actionMessage }}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="rounded-lg border border-slate-200 bg-white dark:border-slate-800 dark:bg-slate-900">
|
||||
<div class="flex flex-col gap-3 border-b border-slate-200 p-4 dark:border-slate-800 md:flex-row md:items-center md:justify-between">
|
||||
<div>
|
||||
<h2 class="mb-1 text-base font-semibold text-slate-950 dark:text-white">
|
||||
任务列表
|
||||
</h2>
|
||||
<p class="mb-0 text-sm text-slate-500 dark:text-slate-400">
|
||||
{{ isLoading ? '加载中' : `共 ${tasks.length} 个任务` }}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
class="inline-flex h-9 items-center justify-center gap-2 rounded-md border border-slate-300 bg-white px-3 text-sm font-medium text-slate-700 hover:bg-slate-50 dark:border-slate-700 dark:bg-slate-950 dark:text-slate-200 dark:hover:bg-slate-900"
|
||||
type="button"
|
||||
@click="loadTasks"
|
||||
>
|
||||
<ReloadOutlined />
|
||||
<span>刷新</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full text-left text-sm">
|
||||
<thead class="bg-slate-50 text-xs uppercase text-slate-500 dark:bg-slate-950 dark:text-slate-400">
|
||||
<tr>
|
||||
<th class="w-[90px] px-4 py-3 font-medium">任务</th>
|
||||
<th class="min-w-[220px] px-4 py-3 font-medium">图片</th>
|
||||
<th class="w-[140px] px-4 py-3 font-medium">类型</th>
|
||||
<th class="w-[120px] px-4 py-3 font-medium">状态</th>
|
||||
<th class="w-[110px] px-4 py-3 font-medium">耗时</th>
|
||||
<th class="w-[170px] px-4 py-3 font-medium">创建时间</th>
|
||||
<th class="w-[150px] px-4 py-3 text-right font-medium">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-200 dark:divide-slate-800">
|
||||
<tr v-if="!isLoading && tasks.length === 0">
|
||||
<td class="px-4 py-8 text-center text-slate-500 dark:text-slate-400" colspan="7">
|
||||
暂无任务
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
v-for="task in tasks"
|
||||
:key="task.id"
|
||||
class="cursor-pointer hover:bg-slate-50 dark:hover:bg-slate-950"
|
||||
:class="selectedTask?.id === task.id ? 'bg-slate-50 dark:bg-slate-950' : ''"
|
||||
@click="selectTask(task.id)"
|
||||
>
|
||||
<td class="px-4 py-3 font-medium text-slate-900 dark:text-white">
|
||||
#{{ task.id }}
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
<div class="max-w-[320px] truncate font-medium text-slate-900 dark:text-white">
|
||||
{{ baseName(task.sourcePath) }}
|
||||
</div>
|
||||
<div class="max-w-[320px] truncate text-xs text-slate-500 dark:text-slate-400">
|
||||
{{ task.sourcePath }}
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-slate-700 dark:text-slate-200">
|
||||
{{ operationLabel(task.operation) }}
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
<span class="inline-flex rounded-md border px-2 py-1 text-xs font-medium" :class="statusClass(task.status)">
|
||||
{{ statusLabel(task.status) }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-slate-600 dark:text-slate-300">
|
||||
{{ formatDuration(task.durationMs) }}
|
||||
</td>
|
||||
<td class="px-4 py-3 text-slate-500 dark:text-slate-400">
|
||||
{{ formatDate(task.createdAt) }}
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
<div class="flex justify-end gap-2" @click.stop>
|
||||
<button
|
||||
class="inline-flex h-8 w-8 items-center justify-center rounded-md border border-slate-300 text-slate-600 hover:bg-slate-50 dark:border-slate-700 dark:text-slate-300 dark:hover:bg-slate-800"
|
||||
type="button"
|
||||
title="查看详情"
|
||||
@click="selectTask(task.id)"
|
||||
>
|
||||
<EyeOutlined />
|
||||
</button>
|
||||
<button
|
||||
class="inline-flex h-8 w-8 items-center justify-center rounded-md border border-slate-300 text-slate-600 hover:bg-slate-50 disabled:cursor-not-allowed disabled:opacity-40 dark:border-slate-700 dark:text-slate-300 dark:hover:bg-slate-800"
|
||||
type="button"
|
||||
title="取消任务"
|
||||
:disabled="!canCancel(task)"
|
||||
@click="cancelTask(task)"
|
||||
>
|
||||
<StopOutlined />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="rounded-lg border border-slate-200 bg-white dark:border-slate-800 dark:bg-slate-900">
|
||||
<div class="border-b border-slate-200 p-4 dark:border-slate-800">
|
||||
<h2 class="mb-0 text-base font-semibold text-slate-950 dark:text-white">
|
||||
结果预览
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div v-if="!selectedDetail" class="p-4 text-sm text-slate-500 dark:text-slate-400">
|
||||
请选择一个任务
|
||||
</div>
|
||||
<div v-else class="grid grid-cols-1 gap-4 p-4 lg:grid-cols-[minmax(0,1fr)_320px]">
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<div class="rounded-lg border border-slate-200 p-3 dark:border-slate-800">
|
||||
<div class="mb-3 flex items-center gap-2 text-sm font-medium text-slate-900 dark:text-white">
|
||||
<UploadOutlined />
|
||||
<span>原图</span>
|
||||
</div>
|
||||
<div class="flex aspect-[4/3] items-center justify-center rounded-md bg-slate-100 p-4 text-center text-sm text-slate-500 dark:bg-slate-950 dark:text-slate-400">
|
||||
{{ baseName(selectedTask?.sourcePath || '') }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="rounded-lg border border-slate-200 p-3 dark:border-slate-800">
|
||||
<div class="mb-3 flex items-center gap-2 text-sm font-medium text-slate-900 dark:text-white">
|
||||
<EyeOutlined />
|
||||
<span>优化结果</span>
|
||||
</div>
|
||||
<div class="flex aspect-[4/3] items-center justify-center rounded-md bg-slate-100 p-4 text-center text-sm text-slate-500 dark:bg-slate-950 dark:text-slate-400">
|
||||
{{ baseName(selectedResult?.outputPath || '') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3 text-sm">
|
||||
<div class="flex justify-between gap-3">
|
||||
<span class="text-slate-500 dark:text-slate-400">任务编号</span>
|
||||
<span class="font-medium text-slate-900 dark:text-white">#{{ selectedTask?.id }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between gap-3">
|
||||
<span class="text-slate-500 dark:text-slate-400">状态</span>
|
||||
<span class="font-medium text-slate-900 dark:text-white">{{ statusLabel(selectedTask?.status || '') }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between gap-3">
|
||||
<span class="text-slate-500 dark:text-slate-400">类型</span>
|
||||
<span class="font-medium text-slate-900 dark:text-white">{{ operationLabel(selectedTask?.operation || '') }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between gap-3">
|
||||
<span class="text-slate-500 dark:text-slate-400">耗时</span>
|
||||
<span class="font-medium text-slate-900 dark:text-white">{{ formatDuration(selectedTask?.durationMs || 0) }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between gap-3">
|
||||
<span class="text-slate-500 dark:text-slate-400">原始大小</span>
|
||||
<span class="font-medium text-slate-900 dark:text-white">{{ formatSize(selectedResult?.beforeSize || 0) }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between gap-3">
|
||||
<span class="text-slate-500 dark:text-slate-400">结果大小</span>
|
||||
<span class="font-medium text-slate-900 dark:text-white">{{ formatSize(selectedResult?.afterSize || 0) }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="mb-1 text-slate-500 dark:text-slate-400">输出路径</p>
|
||||
<p class="break-all rounded-md bg-slate-50 p-2 text-slate-700 dark:bg-slate-950 dark:text-slate-200">
|
||||
{{ selectedResult?.outputPath || '-' }}
|
||||
</p>
|
||||
</div>
|
||||
<div v-if="selectedTask?.errorMessage">
|
||||
<p class="mb-1 text-rose-600 dark:text-rose-300">失败原因</p>
|
||||
<p class="break-all rounded-md bg-rose-50 p-2 text-rose-700 dark:bg-rose-950/40 dark:text-rose-200">
|
||||
{{ selectedTask.errorMessage }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</ModulePageShell>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user