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:
@@ -126,7 +126,7 @@
|
||||
{{ matching ? '匹配中…' : '匹配店铺' }}
|
||||
</button>
|
||||
<button type="button" class="btn-run btn-queue" :disabled="pushing || !hasValidMode" @click="pushToPythonLoopQueue">
|
||||
{{ pushing ? '推送中…' : '推送到 Python 队列' }}
|
||||
{{ pushing ? '启动中…' : '启动任务' }}
|
||||
</button>
|
||||
</div>
|
||||
<p class="loading-msg">上传ASIN文档和多选店铺<strong>可同时生效</strong>,跟价结果合并输出。全量和自定义的区别仅在于ASIN来源不同。</p>
|
||||
@@ -152,109 +152,53 @@
|
||||
</aside>
|
||||
|
||||
<section class="right-panel">
|
||||
<div class="panel-header">匹配与任务</div>
|
||||
<div class="task-list-wrap">
|
||||
<div class="clean-result-summary pr-summary">
|
||||
<div class="summary-card">
|
||||
<span class="summary-label">备选店铺数</span>
|
||||
<strong>{{ dashboard.candidateCount }}</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 v-if="!matchedItems.length" class="empty-tasks narrow">完成「匹配店铺」后,在此展示紫鸟索引匹配结果,供核对后再推送队列。</div>
|
||||
<el-table v-else :data="matchedItems" :row-key="rowKeyForMatch" :highlight-current-row="false"
|
||||
class="result-table match-table">
|
||||
<el-table-column prop="shopName" label="店铺名" min-width="100" />
|
||||
<el-table-column label="匹配" width="72" align="center">
|
||||
<template #default="{ row }">
|
||||
<span :class="row.matched ? 'ok' : 'fail'">{{ row.matched ? '是' : '否' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="shopId" label="店铺ID" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column prop="platform" label="平台" width="88" show-overflow-tooltip />
|
||||
<el-table-column prop="companyName" label="公司" min-width="100" show-overflow-tooltip />
|
||||
<el-table-column label="状态" width="100" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ formatMatchStatus(row.matchStatus) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="说明" min-width="140" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ formatMatchRemark(row) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="72" align="center">
|
||||
<template #default="{ row }">
|
||||
<button type="button" class="link-danger" @click="removeMatchedRow(row)">删除</button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="result-list-wrap">
|
||||
<div class="result-list-header">
|
||||
<span>处理记录</span>
|
||||
</div>
|
||||
|
||||
<div v-if="!hasVisibleList" class="empty-tasks">暂无处理记录。推送队列并等待 Python 回传后,将在此显示下载链接。</div>
|
||||
|
||||
<template v-else>
|
||||
<div v-if="currentSectionItems.length" class="result-subsection">
|
||||
<div class="result-subsection-title">当前任务</div>
|
||||
<ul class="task-list clean-result-list">
|
||||
<li v-for="item in currentSectionItems" :key="`cur-${item.resultId}-${item.taskId}`"
|
||||
class="task-item split-result-item">
|
||||
<div class="left split-result-main">
|
||||
<div v-if="item.roundIndex" class="files">轮次:第 {{ item.roundIndex }} 轮</div>
|
||||
<span class="id" :title="item.shopName || ''">{{ item.shopName || '-' }}</span>
|
||||
<div class="files">任务 ID:{{ item.taskId ?? '-' }}</div>
|
||||
<div v-if="item.platform" class="files">平台:{{ item.platform }}</div>
|
||||
<div v-if="item.error" class="files">错误:{{ item.error }}</div>
|
||||
</div>
|
||||
<div class="task-right split-result-actions">
|
||||
<span class="status" :class="statusClass(item)">{{ statusText(item) }}</span>
|
||||
<button v-if="canDownload(item)" type="button" class="download" @click="downloadResult(item)">
|
||||
下载
|
||||
</button>
|
||||
<button type="button" class="btn-delete" @click="deleteTaskRecord(item)">删除</button>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div v-if="historySectionItems.length" class="result-subsection">
|
||||
<div class="result-subsection-title">历史记录</div>
|
||||
<ul class="task-list clean-result-list">
|
||||
<li v-for="item in historySectionItems" :key="`his-${item.resultId}-${item.taskId}`"
|
||||
class="task-item split-result-item">
|
||||
<div class="left split-result-main">
|
||||
<div v-if="item.roundIndex" class="files">轮次:第 {{ item.roundIndex }} 轮</div>
|
||||
<span class="id" :title="item.shopName || ''">{{ item.shopName || '-' }}</span>
|
||||
<div class="files">任务 ID:{{ item.taskId ?? '-' }}</div>
|
||||
<div v-if="item.outputFilename" class="files">文件:{{ item.outputFilename }}</div>
|
||||
<div v-if="item.error" class="files">错误:{{ item.error }}</div>
|
||||
</div>
|
||||
<div class="task-right split-result-actions">
|
||||
<span class="status" :class="statusClass(item)">{{ statusText(item) }}</span>
|
||||
<button v-if="canDownload(item)" type="button" class="download" @click="downloadResult(item)">
|
||||
下载
|
||||
</button>
|
||||
<button type="button" class="btn-delete" @click="deleteTaskRecord(item)">删除</button>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<TaskCenterPanel
|
||||
:on-batch-delete="batchDeleteHistory"
|
||||
title="匹配与任务"
|
||||
:cards="priceTrackCards"
|
||||
:current-items="currentTaskViews"
|
||||
:history-items="historyTaskViews"
|
||||
current-empty-text="暂无当前任务。完成「匹配店铺」并启动任务后,将在此展示处理进度"
|
||||
history-empty-text="暂无历史记录"
|
||||
>
|
||||
<template #cards-extra>
|
||||
<div class="subsection-title">匹配结果</div>
|
||||
<div v-if="!matchedItems.length" class="match-empty">完成「匹配店铺」后,在此展示紫鸟索引匹配结果,供核对后再推送队列。</div>
|
||||
<el-table v-else :data="matchedItems" :row-key="rowKeyForMatch" :highlight-current-row="false"
|
||||
class="result-table match-table">
|
||||
<el-table-column prop="shopName" label="店铺名" min-width="100" />
|
||||
<el-table-column label="匹配" width="72" align="center">
|
||||
<template #default="{ row }">
|
||||
<span :class="row.matched ? 'ok' : 'fail'">{{ row.matched ? '是' : '否' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="shopId" label="店铺ID" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column prop="platform" label="平台" width="88" show-overflow-tooltip />
|
||||
<el-table-column prop="companyName" label="公司" min-width="100" show-overflow-tooltip />
|
||||
<el-table-column label="状态" width="100" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ formatMatchStatus(row.matchStatus) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="说明" min-width="140" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ formatMatchRemark(row) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="72" align="center">
|
||||
<template #default="{ row }">
|
||||
<button type="button" class="link-danger" @click="removeMatchedRow(row)">删除</button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</template>
|
||||
<template #item-actions="{ item }">
|
||||
<button v-if="canDownload(itemSource(item))" type="button" class="download"
|
||||
@click="downloadResult(itemSource(item))">下载</button>
|
||||
<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 type="button" class="btn-delete" @click="deleteTaskRecord(itemSource(item))">删除</button>
|
||||
</template>
|
||||
</TaskCenterPanel>
|
||||
</section>
|
||||
</div>
|
||||
</AmazonToolPageShell>
|
||||
@@ -267,6 +211,8 @@ import { ElMessage } from 'element-plus'
|
||||
import AmazonToolPageShell from '@/pages/amazon/components/AmazonToolPageShell.vue';
|
||||
import ModuleTemplateDownload from '@/shared/components/ModuleTemplateDownload.vue'
|
||||
import ZiniaoVersionSetting from '@/shared/components/ZiniaoVersionSetting.vue'
|
||||
import TaskCenterPanel from '@/shared/components/tasks/TaskCenterPanel.vue'
|
||||
import type { TaskItemView, TaskStatCard } from '@/shared/components/tasks/types'
|
||||
import { LISTING_FILTER_OPTIONS, type ListingFilterValue } from '@/pages/brand/components/listingFilters'
|
||||
import { expandBrandFolderRecursive } from '@/shared/api/brand'
|
||||
import { getPywebviewApi, type PywebviewApi, type UploadedJavaFile } from '@/shared/bridges/pywebview'
|
||||
@@ -386,6 +332,25 @@ const historyItems = ref<PriceTrackHistoryItem[]>([])
|
||||
const pollingTaskIds = ref<number[]>([])
|
||||
const taskDetails = ref<Record<number, string>>({})
|
||||
const taskSnapshots = ref<Record<number, PriceTrackTaskDetailVo>>({})
|
||||
|
||||
/** 任务快照的 task 时间(开始=创建,结束=完成;运行中结束显示进行中) */
|
||||
function snapTask(item: PriceTrackHistoryItem) {
|
||||
return item.taskId ? taskSnapshots.value[item.taskId]?.task : undefined
|
||||
}
|
||||
|
||||
function formatDateTime(value?: string) {
|
||||
if (!value) return '-'
|
||||
const date = new Date(value)
|
||||
if (Number.isNaN(date.getTime())) return value
|
||||
const year = date.getFullYear()
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
const hours = String(date.getHours()).padStart(2, '0')
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0')
|
||||
const seconds = String(date.getSeconds()).padStart(2, '0')
|
||||
return year + '-' + month + '-' + day + ' ' + hours + ':' + minutes + ':' + seconds
|
||||
}
|
||||
|
||||
const pollTimer = ref<number | null>(null)
|
||||
const pollingInFlight = ref(false)
|
||||
let disposed = false
|
||||
@@ -1050,7 +1015,7 @@ async function pushToPythonQueueLegacy() {
|
||||
queuePushResult.value = `任务 ${taskVo.taskId} 已入队,等待执行完成...`
|
||||
addPollingTask(taskVo.taskId)
|
||||
scheduleNextPoll(true)
|
||||
ElMessage.success('已推送到 Python 队列')
|
||||
ElMessage.success('已启动任务')
|
||||
} catch (e) {
|
||||
queuePushResult.value = e instanceof Error ? e.message : '推送异常'
|
||||
ElMessage.error(queuePushResult.value)
|
||||
@@ -1097,7 +1062,7 @@ async function enqueueCreatedTask(api: PywebviewApi, taskId: number, queuePayloa
|
||||
nonEmptyObjectKeys: mode === 'asin' ? ['asin_rows_by_country'] : [],
|
||||
})
|
||||
if (!(await passGuard(guard))) {
|
||||
const reason = `任务 ${taskId} 数据校验未通过,已阻止推送到 Python 队列`
|
||||
const reason = `任务 ${taskId} 数据校验未通过,已阻止启动任务`
|
||||
await compensateDispatchFailure(taskId, reason)
|
||||
throw new Error(reason)
|
||||
}
|
||||
@@ -1105,12 +1070,12 @@ async function enqueueCreatedTask(api: PywebviewApi, taskId: number, queuePayloa
|
||||
try {
|
||||
pushResult = await api.enqueue_json(queuePayload)
|
||||
} catch (error) {
|
||||
const reason = `Python 队列调用异常:${error instanceof Error ? error.message : String(error || '未知错误')}`
|
||||
const reason = `任务队列调用异常:${error instanceof Error ? error.message : String(error || '未知错误')}`
|
||||
await compensateDispatchFailure(taskId, reason)
|
||||
throw new Error(reason)
|
||||
}
|
||||
if (!pushResult?.success) {
|
||||
const reason = pushResult?.error || 'Python 队列拒绝接收任务'
|
||||
const reason = pushResult?.error || '任务队列拒绝接收任务'
|
||||
await compensateDispatchFailure(taskId, reason)
|
||||
throw new Error(reason)
|
||||
}
|
||||
@@ -1650,7 +1615,40 @@ const historySectionItems = computed(() =>
|
||||
historyItems.value.filter((row) => !row.taskId || !pollingTaskIds.value.includes(row.taskId) || isTaskTerminalById(row.taskId))
|
||||
)
|
||||
|
||||
const hasVisibleList = computed(() => currentSectionItems.value.length > 0 || historySectionItems.value.length > 0)
|
||||
/** 统一统计卡:运行中任务按页面当前列表实时计算,其余沿用后端 dashboard */
|
||||
const priceTrackCards = computed<TaskStatCard[]>(() => [
|
||||
{ label: '运行中任务', value: currentTaskViews.value.length },
|
||||
{ label: '已结束任务', value: dashboard.value.processedTaskCount },
|
||||
{ label: '成功任务', value: dashboard.value.successTaskCount },
|
||||
{ label: '失败任务', value: dashboard.value.failedTaskCount },
|
||||
])
|
||||
|
||||
const currentTaskViews = computed<TaskItemView[]>(() => currentSectionItems.value.map(toPriceTrackTaskView))
|
||||
const historyTaskViews = computed<TaskItemView[]>(() => historySectionItems.value.map(toPriceTrackTaskView))
|
||||
|
||||
function itemSource(item: TaskItemView) {
|
||||
return item.source as PriceTrackHistoryItem
|
||||
}
|
||||
|
||||
function toPriceTrackTaskView(item: PriceTrackHistoryItem): TaskItemView {
|
||||
const task = snapTask(item)
|
||||
return {
|
||||
key: `follow-${item.resultId ?? `t${item.taskId ?? ''}`}-${item.shopName ?? ''}`,
|
||||
title: item.shopName || '-',
|
||||
taskId: item.taskId ?? '-',
|
||||
startedAt: formatDateTime(task?.createdAt),
|
||||
finishedAt: formatDateTime(task?.finishedAt),
|
||||
statusText: statusText(item),
|
||||
statusClass: statusClass(item),
|
||||
extraLines: [
|
||||
...(item.roundIndex ? [`轮次:第 ${item.roundIndex} 轮`] : []),
|
||||
...(item.platform ? [`平台:${item.platform}`] : []),
|
||||
...(item.outputFilename ? [`文件:${item.outputFilename}`] : []),
|
||||
...(item.error ? [`错误:${item.error}`] : []),
|
||||
],
|
||||
source: item,
|
||||
}
|
||||
}
|
||||
|
||||
function taskStatusOf(taskId?: number) {
|
||||
if (!taskId) return ''
|
||||
@@ -1770,6 +1768,26 @@ onUnmounted(() => {
|
||||
timers.clearScope()
|
||||
if (countryPrefSaveTimer) timers.clearTimer('preference-save', countryPrefSaveTimer)
|
||||
})
|
||||
|
||||
/**
|
||||
* 历史记录批量删除:复用单条删除逻辑(删除后原函数各自刷新列表)
|
||||
*/
|
||||
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>
|
||||
@@ -2253,61 +2271,19 @@ onUnmounted(() => {
|
||||
}
|
||||
|
||||
/* Right panel */
|
||||
.panel-header {
|
||||
padding: 16px 20px;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #f5f8fc;
|
||||
border-bottom: 1px solid #2e3a52;
|
||||
}
|
||||
|
||||
.task-list-wrap {
|
||||
flex: 1;
|
||||
padding: 16px 20px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.pr-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: 10px;
|
||||
background: #1c2333;
|
||||
}
|
||||
|
||||
.summary-card strong {
|
||||
display: block;
|
||||
margin-top: 8px;
|
||||
font-size: 22px;
|
||||
color: #f5f8fc;
|
||||
}
|
||||
|
||||
.summary-label {
|
||||
font-size: 12px;
|
||||
color: #5e6878;
|
||||
}
|
||||
|
||||
.subsection-title {
|
||||
font-size: 13px;
|
||||
color: #a0acbe;
|
||||
margin: 8px 0 10px;
|
||||
}
|
||||
|
||||
.empty-tasks {
|
||||
/* 匹配结果空态(#cards-extra 区块内使用) */
|
||||
.match-empty {
|
||||
color: #5e6878;
|
||||
font-size: 13px;
|
||||
padding: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.empty-tasks.narrow {
|
||||
padding: 12px 8px;
|
||||
margin-bottom: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.match-table {
|
||||
@@ -2325,96 +2301,6 @@ onUnmounted(() => {
|
||||
.ok { color: #27ae60; }
|
||||
.fail { color: #e67e22; }
|
||||
|
||||
.result-list-wrap {
|
||||
border: 1px solid #2e3a52;
|
||||
border-radius: 10px;
|
||||
background: #1c2333;
|
||||
min-height: 200px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.result-list-header {
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid #2e3a52;
|
||||
font-size: 14px;
|
||||
color: #c8d2e2;
|
||||
}
|
||||
|
||||
.result-subsection {
|
||||
padding: 12px 12px 4px;
|
||||
}
|
||||
|
||||
.result-subsection-title {
|
||||
font-size: 12px;
|
||||
color: #5e6878;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.task-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.split-result-main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.id {
|
||||
font-weight: 600;
|
||||
color: #f5f8fc;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.files {
|
||||
font-size: 12px;
|
||||
color: #5e6878;
|
||||
}
|
||||
|
||||
.task-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.status {
|
||||
padding: 4px 10px;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.status.success {
|
||||
background: rgba(46, 204, 113, 0.18);
|
||||
color: #2ecc71;
|
||||
}
|
||||
|
||||
.status.failed {
|
||||
background: rgba(231, 76, 60, 0.18);
|
||||
color: #ff6b6b;
|
||||
}
|
||||
|
||||
.status.running {
|
||||
background: rgba(52, 152, 219, 0.18);
|
||||
color: #3498db;
|
||||
}
|
||||
|
||||
.download {
|
||||
padding: 6px 10px;
|
||||
border-radius: 6px;
|
||||
@@ -2454,10 +2340,6 @@ onUnmounted(() => {
|
||||
border-right: none;
|
||||
border-bottom: 1px solid #2e3a52;
|
||||
}
|
||||
|
||||
.pr-summary {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user