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:
@@ -65,7 +65,7 @@
|
||||
</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 回传字段按 id、asin、国家、价格、标题、图片链接数组提交;后端接收分片后按 10 条一批送检,结果区展示真实 LLM 批次进度。</p>
|
||||
@@ -80,104 +80,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>{{ displayFileProgressStage(item, '处理中') }}</span>
|
||||
<span>总进度 {{ fileProgressPercent(item) }}%</span>
|
||||
</div>
|
||||
<div class="file-progress-track">
|
||||
<div class="file-progress-bar" :style="{ width: `${fileProgressPercent(item)}%` }"></div>
|
||||
</div>
|
||||
<div v-if="hasFileProgressCount(item)" class="file-progress-count">
|
||||
{{ fileProgressCountLabel(item) }}
|
||||
</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>{{ displayFileProgressStage(item, '结果生成中') }}</span>
|
||||
<span>总进度 {{ fileProgressPercent(item) }}%</span>
|
||||
</div>
|
||||
<div class="file-progress-track">
|
||||
<div class="file-progress-bar" :style="{ width: `${fileProgressPercent(item)}%` }"></div>
|
||||
</div>
|
||||
<div v-if="hasFileProgressCount(item)" class="file-progress-count">
|
||||
{{ fileProgressCountLabel(item) }}
|
||||
</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="asinCards"
|
||||
: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>
|
||||
|
||||
@@ -190,6 +118,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 {
|
||||
activateSimilarAsinTask,
|
||||
deleteSimilarAsinHistory,
|
||||
@@ -301,6 +231,48 @@ const historyOnlyItems = computed(() =>
|
||||
}),
|
||||
)
|
||||
|
||||
const asinCards = 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(toAsinTaskView))
|
||||
|
||||
const historyTaskViews = computed<TaskItemView[]>(() => historyOnlyItems.value.map(toAsinTaskView))
|
||||
|
||||
function itemSource(item: TaskItemView): SimilarAsinHistoryItem {
|
||||
return item.source as SimilarAsinHistoryItem
|
||||
}
|
||||
|
||||
function toAsinTaskView(item: SimilarAsinHistoryItem): TaskItemView {
|
||||
const showProgress = showFileProgress(item)
|
||||
const isRunning = normalizeTaskStatus(item) === 'RUNNING'
|
||||
return {
|
||||
key: `asin-${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: fileProgressPercent(item),
|
||||
stage: displayFileProgressStage(item, isRunning ? '处理中' : '结果生成中'),
|
||||
countLabel: hasFileProgressCount(item) ? fileProgressCountLabel(item) : undefined,
|
||||
}
|
||||
: null,
|
||||
source: item,
|
||||
}
|
||||
}
|
||||
|
||||
function mergeCurrentTaskItem(item: SimilarAsinHistoryItem) {
|
||||
if (item.taskId == null) return item
|
||||
const liveItem = liveProgressItems.value[item.taskId]
|
||||
@@ -599,7 +571,7 @@ async function pushToPythonQueue() {
|
||||
clearParsedTask()
|
||||
await loadDashboard()
|
||||
await loadHistory({ force: true })
|
||||
ElMessage.success('已推送到 Python 队列')
|
||||
ElMessage.success('已启动任务')
|
||||
} finally {
|
||||
pushing.value = false
|
||||
}
|
||||
@@ -1122,6 +1094,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>
|
||||
@@ -1152,8 +1144,7 @@ onUnmounted(() => {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.section-title,
|
||||
.subsection-title {
|
||||
.section-title {
|
||||
font-size: 13px;
|
||||
color: #a0acbe;
|
||||
margin-bottom: 10px;
|
||||
@@ -1368,172 +1359,11 @@ onUnmounted(() => {
|
||||
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;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.file-progress-count {
|
||||
margin-top: 4px;
|
||||
color: #858585;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.download {
|
||||
padding: 6px 10px;
|
||||
color: #c8d2e2;
|
||||
@@ -1557,9 +1387,5 @@ onUnmounted(() => {
|
||||
border-right: none;
|
||||
border-bottom: 1px solid #2e3a52;
|
||||
}
|
||||
|
||||
.clean-result-summary {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user