feat(web): 前端 SPA 化 + 工具页任务面板统一与历史批量删除

- SPA 化:22 个 MPA html 入口与 *-main.ts 合并为 index.html + vue-router(URL 无 .html 后缀),
  页面跳转全部 router-link,/new_web_source/xxx.html 旧路径归一为 /xxx
- 任务面板统一:共享 TaskCenterPanel/TaskItemCard/TaskStatCards/HistoryTaskLayer,
  16 个工具页右侧统一为统计卡 + 当前任务 + 历史任务弹层(任务ID/开始/结束/状态必展示)
- 历史记录支持单条删除 + 批量勾选删除(确认框/全选/失败提示)
- Java 7 模块(dedupe/convert/split/productrisk/shopmatch/pricetrack/deletebrand)
  history 接口补齐任务时间字段(VO+Service,复用 biz_file_task 列)
- 图片工作台/API 层(brand/permission/user)既有未提交改动一并提交
This commit is contained in:
2026-09-08 10:02:55 +08:00
parent abcfa5bef7
commit e248b6e43a
125 changed files with 7482 additions and 4304 deletions
@@ -86,45 +86,9 @@
</aside>
<section class="right-panel">
<div class="panel-header">
<span>变体采集任务</span>
<div class="panel-actions">
<button type="button" class="btn-refresh" @click="refreshList">刷新任务状态</button>
</div>
</div>
<div class="task-list-wrap">
<div v-if="!currentTasks.length" class="empty-tasks">暂无变体采集任务</div>
<ul v-else class="task-list">
<li v-for="item in currentTasks" :key="`vc-${item.task_id}`" class="task-item">
<div class="left">
<span class="id">任务 {{ item.task_id }}</span>
<div v-if="taskMeta(item).source" class="files">来源{{ taskMeta(item).source }}</div>
<div class="files">采集国家{{ countryText(item) || '-' }}</div>
<div class="files">更新时间{{ item.update_time || formatDateTime(item.updated_at) }}</div>
<div v-if="latestMessage(item)" class="files">{{ latestMessage(item) }}</div>
</div>
<div class="task-right">
<span class="status" :class="statusClass(item.status)">{{ statusText(item.status) }}</span>
<button type="button" class="act-btn" @click="refreshTask(item)">更新</button>
<button type="button" class="act-btn" @click="showDetail(item)">详情</button>
<button
v-if="hasResultFiles(item)"
type="button"
class="act-btn ok"
@click="downloadResult(item)"
>下载结果</button>
<button
v-if="hasSourceFile(item)"
type="button"
class="act-btn"
@click="downloadSource(item)"
>下载源文件</button>
<button type="button" class="act-btn warn" @click="reexportTask(item)">重新导出</button>
</div>
</li>
</ul>
<div v-if="currentTasks.length" class="pagination">
<div class="panel-toolbar">
<button type="button" class="btn-refresh" @click="refreshList">刷新任务状态</button>
<div v-if="totalCount" class="toolbar-pagination">
<span class="page-info"> {{ totalCount }} · {{ currentPage }} / {{ totalPages }} </span>
<div class="page-btns">
<button type="button" class="page-btn" :disabled="currentPage <= 1" @click="goPage(currentPage - 1)">上一页</button>
@@ -132,6 +96,38 @@
</div>
</div>
</div>
<TaskCenterPanel
title="变体采集任务"
:cards="variantCards"
:current-items="currentTaskViews"
:history-items="historyTaskViews"
current-title="当前任务"
current-empty-text="暂无运行中的变体采集任务"
history-empty-text="暂无历史记录"
>
<template #item-actions="{ item }">
<template v-if="itemSource(item)">
<button type="button" class="act-btn" @click="refreshTask(itemSource(item))">更新</button>
<button type="button" class="act-btn" @click="showDetail(itemSource(item))">详情</button>
<button v-if="hasResultFiles(itemSource(item))" type="button" class="act-btn ok"
@click="downloadResult(itemSource(item))">下载结果</button>
<button v-if="hasSourceFile(itemSource(item))" type="button" class="act-btn"
@click="downloadSource(itemSource(item))">下载源文件</button>
<button type="button" class="act-btn warn" @click="reexportTask(itemSource(item))">重新导出</button>
</template>
</template>
<template #history-item-actions="{ item }">
<template v-if="itemSource(item)">
<button type="button" class="act-btn" @click="refreshTask(itemSource(item))">更新</button>
<button type="button" class="act-btn" @click="showDetail(itemSource(item))">详情</button>
<button v-if="hasResultFiles(itemSource(item))" type="button" class="act-btn ok"
@click="downloadResult(itemSource(item))">下载结果</button>
<button v-if="hasSourceFile(itemSource(item))" type="button" class="act-btn"
@click="downloadSource(itemSource(item))">下载源文件</button>
<button type="button" class="act-btn warn" @click="reexportTask(itemSource(item))">重新导出</button>
</template>
</template>
</TaskCenterPanel>
</section>
</div>
</AmazonToolPageShell>
@@ -166,6 +162,8 @@
import { computed, onBeforeUnmount, onMounted, reactive, ref } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import AmazonToolPageShell from '@/pages/amazon/components/AmazonToolPageShell.vue'
import TaskCenterPanel from '@/shared/components/tasks/TaskCenterPanel.vue'
import type { TaskItemView, TaskStatCard } from '@/shared/components/tasks/types'
import {
getPywebviewApi,
type VariantTaskListItem,
@@ -214,6 +212,59 @@ const canSubmit = computed(() =>
inputMode.value === 'asin' ? asinLines.value.length > 0 : selectedFiles.value.length > 0,
)
// ---- 右侧任务面板统一视图(TaskCenterPanel----
/** 当前任务:排队中(0) / 执行中(1) 的任务(当前分页内) */
const currentTaskViews = computed<TaskItemView[]>(() =>
currentTasks.value.filter((item) => isBusy(item.status)).map(toVariantTaskView),
)
/** 历史任务:已完成(2) / 失败(3) 的任务(当前分页内) */
const historyTaskViews = computed<TaskItemView[]>(() =>
currentTasks.value.filter((item) => !isBusy(item.status)).map(toVariantTaskView),
)
const variantCards = computed<TaskStatCard[]>(() => [
{ label: '运行中任务', value: currentTaskViews.value.length },
{ label: '已结束任务', value: historyTaskViews.value.length },
{ label: '成功任务', value: historyTaskViews.value.filter((view) => view.statusClass === 'success').length },
{ label: '失败任务', value: historyTaskViews.value.filter((view) => view.statusClass === 'failed').length },
])
function itemSource(item: TaskItemView): VariantEntry {
return item.source as VariantEntry
}
function variantStatusClass(status?: number | string) {
const numeric = Number(status)
if (numeric === 1 || /running|processing/i.test(String(status))) return 'running'
if (numeric === 2 || /success|completed|done/i.test(String(status))) return 'success'
if (numeric === 3 || /failed|error/i.test(String(status))) return 'failed'
return 'pending'
}
function toVariantTaskView(item: VariantEntry): TaskItemView {
const extraLines: string[] = []
const source = item.meta?.source || ''
if (source) extraLines.push(`来源:${source}`)
extraLines.push(`采集国家:${countryText(item) || '-'}`)
const updateText = item.update_time || formatDateTime(item.updated_at)
if (updateText) extraLines.push(`更新时间:${updateText}`)
extraLines.push(latestMessage(item))
return {
key: `vc-${item.task_id}`,
title: `任务 ${item.task_id}`,
taskId: item.task_id,
// 桥接列表未提供任务开始时间,开始时间占位为 '-'
startedAt: '-',
// 桥接仅提供最近更新时间,作为结束时间的近似展示
finishedAt: formatDateTime(item.update_time),
statusText: statusText(item.status),
statusClass: variantStatusClass(item.status),
extraLines,
source: item,
}
}
let autoTimer: number | null = null
let refreshTimer: number | null = null
@@ -651,26 +702,12 @@ onBeforeUnmount(() => {
.btn-run { padding: 10px 18px; color: #f5f8fc; background: #27ae60; border: none; border-radius: 8px; font-weight: 600; cursor: pointer; }
.btn-run:disabled { opacity: 0.5; cursor: not-allowed; }
.loading-msg { margin-top: 10px; }
.panel-header { display: flex; justify-content: space-between; align-items: center; padding: 14px 20px; border-bottom: 1px solid #2e3a52; color: #f5f8fc; font-size: 15px; font-weight: 600; }
.panel-actions { display: flex; gap: 8px; }
.panel-toolbar { display: flex; justify-content: space-between; align-items: center; gap: 12px; flex-wrap: wrap; padding: 12px 20px 0; }
.btn-refresh { padding: 6px 12px; color: #c8d2e2; background: #2e3a52; border: 1px solid #3e4a62; border-radius: 6px; cursor: pointer; font-size: 12px; }
.task-list-wrap { flex: 1; padding: 16px 20px; overflow: auto; }
.empty-tasks { color: #5e6878; font-size: 13px; padding: 28px; text-align: center; }
.task-list { list-style: none; margin: 0; padding: 0; }
.task-item { display: flex; justify-content: space-between; align-items: flex-start; gap: 12px; padding: 12px 14px; margin-bottom: 8px; border: 1px solid #2e3a52; border-radius: 8px; background: #1c2333; }
.left { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 3px; }
.id { color: #f5f8fc; font-size: 13px; font-weight: 600; }
.task-right { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; justify-content: flex-end; }
.status { padding: 4px 10px; border-radius: 6px; font-size: 12px; white-space: nowrap; }
.status.pending { background: rgba(149, 165, 166, 0.18); color: #a0acbe; }
.status.running { background: rgba(52, 152, 219, 0.18); color: #3498db; }
.status.success { background: rgba(46, 204, 113, 0.18); color: #2ecc71; }
.status.failed { background: rgba(231, 76, 60, 0.18); color: #ff6b6b; }
.status.cancelled { background: rgba(149, 165, 166, 0.18); color: #a0acbe; }
.toolbar-pagination { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; justify-content: flex-end; }
.act-btn { padding: 4px 9px; border-radius: 6px; font-size: 12px; background: #2e3a52; color: #c8d2e2; border: 1px solid #3e4a62; cursor: pointer; }
.act-btn.ok { background: rgba(52, 152, 219, 0.18); color: #69b6ff; border-color: transparent; }
.act-btn.warn { background: rgba(230, 162, 60, 0.14); color: #e6a23c; border-color: transparent; }
.pagination { display: flex; justify-content: space-between; align-items: center; padding: 10px 0 4px; }
.page-info { color: #5e6878; font-size: 12px; }
.page-btns { display: flex; gap: 8px; }
.page-btn { padding: 5px 12px; border-radius: 6px; background: #2e3a52; color: #c8d2e2; border: 1px solid #3e4a62; cursor: pointer; font-size: 12px; }