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
@@ -27,7 +27,7 @@
{{ parsing ? '解析中...' : '解析并创建任务' }}
</button>
<button type="button" class="btn-run btn-queue" :disabled="pushing || !parseResult?.taskId" @click="pushToPythonQueue">
{{ pushing ? '推送中...' : '推送到 Python 队列' }}
{{ pushing ? '启动中...' : '启动任务' }}
</button>
</div>
<p class="loading-msg">Python 回传字段按 asin国家url标题提交后端接收分片后累计 50 条调用 LLM当前任务区展示 LLM 回流和结果组装进度</p>
@@ -41,96 +41,32 @@
</aside>
<section class="right-panel">
<div class="panel-header">外观专利检测</div>
<div class="task-list-wrap">
<div class="clean-result-summary">
<div class="summary-card">
<span class="summary-label">运行中任务</span>
<strong>{{ dashboard.pendingTaskCount }}</strong>
</div>
<div class="summary-card">
<span class="summary-label">已结束任务</span>
<strong>{{ dashboard.processedTaskCount }}</strong>
</div>
<div class="summary-card">
<span class="summary-label">成功任务</span>
<strong>{{ dashboard.successTaskCount }}</strong>
</div>
<div class="summary-card">
<span class="summary-label">失败任务</span>
<strong>{{ dashboard.failedTaskCount }}</strong>
</div>
</div>
<div class="subsection-title">匹配任务</div>
<div class="result-list-wrap">
<div class="result-list-header">
<span>当前任务</span>
</div>
<div v-if="!currentItems.length" class="empty-tasks">暂无当前任务</div>
<ul v-else class="task-list clean-result-list">
<li v-for="item in currentItems" :key="`cur-${item.taskId}`" class="task-item">
<div class="left">
<span class="id">{{ item.sourceFilename || '外观专利检测' }}</span>
<div class="files">任务 ID{{ item.taskId }}</div>
<div class="files">开始时间{{ formatDateTime(item.startedAt || item.createdAt) }}</div>
<div class="files">结束时间{{ formatDateTime(item.finishedAt) }}</div>
<div class="files">行数{{ item.rowCount ?? '-' }}</div>
<div v-if="pendingResultHint(item)" class="files result-hint">{{ pendingResultHint(item) }}</div>
<div v-if="showFileProgress(item)" class="file-progress">
<div class="file-progress-meta">
<span>{{ displayFileProgressMessage(item, '处理中') }}</span>
<span>{{ fileProgressPercent(item) }}%</span>
</div>
<div class="file-progress-track">
<div class="file-progress-bar" :style="{ width: `${fileProgressPercent(item)}%` }"></div>
</div>
</div>
</div>
<div class="task-right">
<span class="status running">{{ statusText(item) }}</span>
<button type="button" class="btn-delete" @click="deleteTaskRecord(item)">删除</button>
</div>
</li>
</ul>
</div>
<div class="result-list-wrap">
<div class="result-list-header">
<span>历史记录</span>
</div>
<div v-if="!historyOnlyItems.length" class="empty-tasks">暂无历史记录</div>
<ul v-else class="task-list clean-result-list">
<li v-for="item in historyOnlyItems" :key="`his-${item.resultId}-${item.taskId}`" class="task-item">
<div class="left">
<span class="id">{{ item.sourceFilename || '外观专利检测' }}</span>
<div class="files">任务 ID{{ item.taskId ?? '-' }}</div>
<div class="files">开始时间{{ formatDateTime(item.startedAt || item.createdAt) }}</div>
<div class="files">结束时间{{ formatDateTime(item.finishedAt) }}</div>
<div v-if="item.resultFilename" class="files">
{{ item.resultFilename || '下载结果' }}
</div>
<div v-if="pendingResultHint(item)" class="files result-hint">{{ pendingResultHint(item) }}</div>
<div v-if="showFileProgress(item)" class="file-progress">
<div class="file-progress-meta">
<span>{{ displayFileProgressMessage(item, '结果生成中') }}</span>
<span>{{ fileProgressPercent(item) }}%</span>
</div>
<div class="file-progress-track">
<div class="file-progress-bar" :style="{ width: `${fileProgressPercent(item)}%` }"></div>
</div>
</div>
<div v-if="item.error" class="files">错误{{ item.error }}</div>
</div>
<div class="task-right">
<span class="status" :class="statusClass(item)">{{ statusText(item) }}</span>
<button v-if="canDownload(item)" type="button" class="download" @click="downloadResult(item)">下载</button>
<button v-if="item.resultId" type="button" class="btn-delete" @click="deleteTaskRecord(item)">删除</button>
</div>
</li>
</ul>
</div>
</div>
<TaskCenterPanel
:on-batch-delete="batchDeleteHistory"
title="外观专利检测"
:cards="patentCards"
:current-items="currentTaskViews"
:history-items="historyTaskViews"
current-title="当前任务"
current-empty-text="暂无当前任务"
history-empty-text="暂无历史记录"
>
<template #item-extra="{ item }">
<div v-if="pendingResultHint(itemSource(item))" class="files result-hint">{{ pendingResultHint(itemSource(item)) }}</div>
</template>
<template #history-item-extra="{ item }">
<div v-if="pendingResultHint(itemSource(item))" class="files result-hint">{{ pendingResultHint(itemSource(item)) }}</div>
</template>
<template #item-actions="{ item }">
<button type="button" class="btn-delete" @click="deleteTaskRecord(itemSource(item))">删除</button>
</template>
<template #history-item-actions="{ item }">
<button v-if="canDownload(itemSource(item))" type="button" class="download"
@click="downloadResult(itemSource(item))">下载</button>
<button v-if="itemSource(item).resultId" type="button" class="btn-delete"
@click="deleteTaskRecord(itemSource(item))">删除</button>
</template>
</TaskCenterPanel>
</section>
</div>
</AmazonToolPageShell>
@@ -142,6 +78,8 @@ import { computed, onMounted, onUnmounted, ref } from 'vue'
import { ElMessage } from 'element-plus'
import AmazonToolPageShell from '@/pages/amazon/components/AmazonToolPageShell.vue';
import ModuleTemplateDownload from '@/shared/components/ModuleTemplateDownload.vue'
import TaskCenterPanel from '@/shared/components/tasks/TaskCenterPanel.vue'
import type { TaskItemView, TaskStatCard } from '@/shared/components/tasks/types'
import {
activateAppearancePatentTask,
deleteAppearancePatentHistory,
@@ -244,6 +182,51 @@ const historyOnlyItems = computed(() =>
}),
)
const patentCards = computed<TaskStatCard[]>(() => [
{ label: '运行中任务', value: dashboard.value.pendingTaskCount },
{ label: '已结束任务', value: dashboard.value.processedTaskCount },
{ label: '成功任务', value: dashboard.value.successTaskCount },
{ label: '失败任务', value: dashboard.value.failedTaskCount },
])
const currentTaskViews = computed<TaskItemView[]>(() =>
currentItems.value.map(toPatentTaskView),
)
const historyTaskViews = computed<TaskItemView[]>(() =>
historyOnlyItems.value.map(toPatentTaskView),
)
function itemSource(item: TaskItemView) {
return item.source as AppearancePatentHistoryItem
}
function toPatentTaskView(item: AppearancePatentHistoryItem): TaskItemView {
const showProgress = showFileProgress(item)
const progressPercent = fileProgressPercent(item)
return {
key: `patent-${item.taskId ?? item.resultId ?? item.sourceFilename}`,
title: item.sourceFilename || '外观专利检测',
taskId: item.taskId ?? '-',
startedAt: formatDateTime(item.startedAt || item.createdAt),
finishedAt: formatDateTime(item.finishedAt),
statusText: statusText(item),
statusClass: statusClass(item),
extraLines: [
...(item.rowCount != null ? [`行数:${item.rowCount}`] : []),
...(item.resultFilename && !showProgress ? [item.resultFilename] : []),
...(item.error ? [`错误:${item.error}`] : []),
],
progress: showProgress
? {
percent: progressPercent,
stage: displayFileProgressMessage(item, '处理中'),
}
: null,
source: item,
}
}
function mergeCurrentTaskItem(item: AppearancePatentHistoryItem) {
if (item.taskId == null) return item
const liveItem = liveProgressItems.value[item.taskId]
@@ -531,7 +514,7 @@ async function pushToPythonQueue() {
clearParsedTask()
await loadDashboard()
await loadHistory({ force: true })
ElMessage.success('已推送到 Python 队列')
ElMessage.success('已启动任务')
} finally {
pushing.value = false
}
@@ -979,6 +962,26 @@ onUnmounted(() => {
stopPolling()
timers.clearScope()
})
/**
* 历史记录批量删除:复用单条删除逻辑(删除后原函数各自刷新列表)
*/
async function batchDeleteHistory(views: TaskItemView[]) {
if (!views.length) return
let failed = 0
for (const view of views) {
try {
await deleteTaskRecord(itemSource(view))
} catch {
failed += 1
}
}
if (failed > 0) {
ElMessage.warning(`删除完成,${failed} 条失败`)
} else {
ElMessage.success(`已删除 ${views.length} 条历史记录`)
}
}
</script>
<style scoped>
@@ -1006,36 +1009,12 @@ onUnmounted(() => {
.prompt-input:focus, .secret-input:focus { border-color: #3498db; }
.parse-card, .queue-payload { margin-top: 14px; padding: 12px; border: 1px solid #2e3a52; border-radius: 8px; background: #222b3d; color: #a0acbe; font-size: 12px; }
.queue-payload { max-height: 220px; overflow: auto; color: #8fd3ff; white-space: pre-wrap; }
.panel-header { padding: 16px 20px; border-bottom: 1px solid #2e3a52; font-size: 15px; font-weight: 600; color: #c8d2e2; }
.task-list-wrap { flex: 1; padding: 16px 20px; overflow: auto; }
.clean-result-summary { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 12px; margin-bottom: 16px; }
.summary-card { padding: 14px 16px; border: 1px solid #2e3a52; border-radius: 8px; background: #1c2333; }
.summary-card strong { display: block; margin-top: 8px; color: #f5f8fc; font-size: 22px; }
.summary-label { color: #5e6878; font-size: 12px; }
.result-list-wrap { border: 1px solid #2e3a52; border-radius: 8px; background: #1c2333; min-height: 180px; margin: 0 0 16px; }
.result-list-header { display: flex; justify-content: space-between; padding: 12px 16px; border-bottom: 1px solid #2e3a52; color: #c8d2e2; font-size: 14px; }
.empty-tasks { color: #5e6878; font-size: 13px; padding: 18px; text-align: center; }
.result-table { --el-table-bg-color: #222; --el-table-tr-bg-color: #222; --el-table-header-bg-color: #2e3a52; --el-table-text-color: #c8d2e2; --el-table-border-color: #333; }
.task-list { list-style: none; margin: 0; padding: 12px; }
.task-item { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; padding: 12px 14px; border: 1px solid #2e3a52; border-radius: 8px; margin-bottom: 8px; background: #222; }
.left { flex: 1; min-width: 0; }
.id { color: #f5f8fc; font-size: 13px; font-weight: 600; }
.task-right { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
.status { padding: 4px 10px; border-radius: 6px; font-size: 12px; }
.status.success { background: rgba(46, 204, 113, .18); color: #2ecc71; }
.status.failed { background: rgba(231, 76, 60, .18); color: #ff6b6b; }
.status.running { background: rgba(52, 152, 219, .18); color: #3498db; }
.status.pending { background: rgba(149, 165, 166, .18); color: #a0acbe; }
.result-hint { margin-top: 6px; color: #e0b96d; }
.file-progress { margin-top: 8px; max-width: 520px; }
.file-progress-meta { display: flex; justify-content: space-between; gap: 12px; color: #d8c278; font-size: 12px; }
.file-progress-track { margin-top: 5px; height: 8px; border-radius: 999px; overflow: hidden; background: #333f55; border: 1px solid #3b3b3b; }
.file-progress-bar { height: 100%; border-radius: inherit; background: linear-gradient(90deg, #4aa3ff, #f0c75e); transition: width .25s ease; }
.download { padding: 6px 10px; color: #c8d2e2; background: rgba(52, 152, 219, .18); }
.btn-delete { padding: 6px 10px; color: #ff8f8f; background: rgba(231, 76, 60, .12); }
@media (max-width: 1100px) {
.main-content { flex-direction: column; height: auto; }
.left-panel { width: 100%; border-right: none; border-bottom: 1px solid #2e3a52; }
.clean-result-summary { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
</style>