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
@@ -85,59 +85,29 @@
</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>{{ cleanSummary.total }}</strong>
</div>
<div class="summary-card">
<span class="summary-label">成功结果</span>
<strong>{{ cleanSummary.successCount }}</strong>
</div>
<div class="summary-card">
<span class="summary-label">失败文件</span>
<strong>{{ cleanSummary.failedCount }}</strong>
</div>
</div>
<div class="result-list-wrap">
<div class="result-list-header">
<span>处理后的 Excel 列表</span>
</div>
<div v-if="cleanResultItems.length === 0" class="empty-tasks">
暂无去重结果完成数据去重后会在这里展示输出文件
</div>
<ul v-else class="task-list clean-result-list">
<li v-for="item in cleanResultItems"
:key="`${item.resultId || item.outputFilename || item.sourceFilename}`" class="task-item">
<div class="left">
<span class="id" :title="item.sourceFilename">{{ item.sourceFilename || '-' }}</span>
<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">
<span class="status" :class="item.success ? 'success' : 'failed'">
{{ item.success ? '已完成' : '失败' }}
</span>
<button v-if="item.success && (item.downloadUrl || item.resultId)" type="button" class="download"
@click="downloadCleanResult(item)">
下载文件
</button>
<button v-if="item.resultId" type="button" class="btn-delete"
@click="deleteCleanHistoryRecord(item.resultId)">
删除
</button>
</div>
</li>
</ul>
</div>
</div>
<TaskCenterPanel
:on-batch-delete="batchDeleteHistory"
title="去重结果"
:cards="dedupeCards"
:current-items="currentTaskViews"
:history-items="historyTaskViews"
current-title="当前任务"
current-empty-text="暂无当前任务完成数据去重后会在这里展示"
history-empty-text="暂无历史记录"
>
<template #item-actions="{ item }">
<template v-if="itemSource(item)">
<button v-if="itemSource(item).success && (itemSource(item).downloadUrl || itemSource(item).resultId)"
type="button" class="download" @click="downloadCleanResult(itemSource(item))">
下载文件
</button>
<button v-if="itemSource(item).resultId" type="button" class="btn-delete"
@click="deleteCleanHistoryRecord(itemSource(item).resultId!)">
删除
</button>
</template>
</template>
</TaskCenterPanel>
</section>
</div>
</AmazonToolPageShell>
@@ -148,6 +118,8 @@
import { computed, onMounted, ref } from 'vue'
import { ElMessage } 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 { expandBrandFolderRecursive, type BrandExpandFolderItem } from '@/shared/api/brand'
import { deleteDedupeHistory, getDedupeHistory, getDedupeResultDownloadUrl, getDedupeRunProgress, getExcelInfo, runDedupe, type DedupeResultItem, type DedupeRunProgressVo, type DedupeRunVo } from '@/shared/api/java-modules'
import { getPywebviewApi, type UploadedJavaFile } from '@/shared/bridges/pywebview'
@@ -168,6 +140,88 @@ const cleanProgressProcessed = ref(0)
const cleanResultItems = ref<DedupeResultItem[]>([])
const cleanSummary = ref<DedupeRunVo>({ total: 0, successCount: 0, failedCount: 0, items: [] })
const cleanDisplayPaths = computed(() => cleanSelectedPaths.value.slice(0, 8))
// 最近一次去重运行信息:用于右侧"当前任务"卡的占位展示(开始时间/运行ID/进度)
const latestRunId = ref('')
const cleanRunStartedAt = ref('')
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}`
}
function itemSource(item: TaskItemView): DedupeResultItem {
return item.source as DedupeResultItem
}
const dedupeCards = computed<TaskStatCard[]>(() => [
{ label: '运行中任务', value: currentTaskViews.value.length },
{ label: '已结束任务', value: cleanSummary.value.total },
{ label: '成功任务', value: cleanSummary.value.successCount },
{ label: '失败任务', value: cleanSummary.value.failedCount },
])
/** 当前任务:运行中的去重 run(占位卡) + 历史列表中非终态任务 */
const currentTaskViews = computed<TaskItemView[]>(() => {
const views: TaskItemView[] = []
if (cleanRunning.value && latestRunId.value) {
views.push({
key: `run-${latestRunId.value}`,
title: cleanArchiveName.value || '数据去重',
taskId: latestRunId.value,
startedAt: cleanRunStartedAt.value,
finishedAt: '',
statusText: '执行中',
statusClass: 'running',
progress: {
percent: cleanSummary.value.total
? Math.round((cleanProgressProcessed.value / cleanSummary.value.total) * 100)
: 0,
stage: `正在处理文件 ${cleanProgressProcessed.value}/${cleanSummary.value.total}`,
},
})
}
for (const item of cleanResultItems.value) {
const status = (item.taskStatus || '').toUpperCase()
if (status === 'RUNNING' || status === 'PENDING') {
views.push(toDedupeTaskView(item))
}
}
return views
})
const historyTaskViews = computed<TaskItemView[]>(() =>
cleanResultItems.value
.filter((item) => {
const status = (item.taskStatus || '').toUpperCase()
return status !== 'RUNNING' && status !== 'PENDING'
})
.map(toDedupeTaskView),
)
function toDedupeTaskView(item: DedupeResultItem): TaskItemView {
return {
key: `dedupe-${item.resultId ?? item.sourceFilename}`,
title: item.sourceFilename || '-',
taskId: item.taskId ?? '-',
startedAt: formatDateTime(item.startedAt || item.createdAt),
finishedAt: formatDateTime(item.finishedAt),
statusText: item.success ? '已完成' : '失败',
statusClass: item.success ? 'success' : 'failed',
extraLines: [
...(item.outputFilename ? [`输出文件:${item.outputFilename}`] : []),
...(item.error ? [`错误信息:${item.error}`] : []),
],
source: item,
}
}
function selectAllCleanColumns() {
cleanSelectedColumns.value = [...cleanAvailableColumns.value]
@@ -292,6 +346,8 @@ async function submitCleanRun() {
try {
cleanRunning.value = true
latestRunId.value = ''
cleanRunStartedAt.value = formatDateTime(new Date().toISOString())
const progress = await runDedupe({
files: cleanUploadedFiles.value.map((item) => ({ fileKey: item.fileKey, originalFilename: item.originalFilename, relativePath: item.relativePath })),
selectedColumns: cleanSelectedColumns.value,
@@ -312,6 +368,7 @@ async function submitCleanRun() {
items: cleanResultItems.value,
}
})
latestRunId.value = progress.runId
cleanSummary.value = result
cleanResultItems.value = result.items || []
await loadCleanHistory()
@@ -410,6 +467,26 @@ async function downloadCleanResult(item: DedupeResultItem) {
onMounted(() => {
loadCleanHistory().catch(() => undefined)
})
/**
* 历史记录批量删除:复用单条删除逻辑(删除后原函数各自刷新列表)
*/
async function batchDeleteHistory(views: TaskItemView[]) {
if (!views.length) return
let failed = 0
for (const view of views) {
try {
await deleteCleanHistoryRecord(itemSource(view).resultId!)
} catch {
failed += 1
}
}
if (failed > 0) {
ElMessage.warning(`删除完成,${failed} 条失败`)
} else {
ElMessage.success(`已删除 ${views.length} 条历史记录`)
}
}
</script>
<style scoped>
@@ -666,81 +743,6 @@ onMounted(() => {
font-size: 13px;
}
.panel-header {
padding: 16px 20px;
border-bottom: 1px solid #2e3a52;
font-size: 15px;
font-weight: 600;
color: #c8d2e2;
}
.task-list-wrap {
flex: 1;
overflow-y: auto;
padding: 16px;
}
.task-list {
list-style: none;
margin: 0;
padding: 0;
}
.task-item {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
padding: 14px 16px;
border: 1px solid #2e3a52;
border-radius: 8px;
margin-bottom: 10px;
background: #1c2333;
}
.left {
flex: 1;
min-width: 0;
}
.id {
display: inline-block;
font-weight: 600;
color: #f5f8fc;
font-size: 13px;
}
.files {
font-size: 12px;
color: #5e6878;
margin-top: 4px;
}
.task-right {
display: flex;
align-items: center;
gap: 12px;
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;
}
.download {
padding: 6px 10px;
border-radius: 6px;
@@ -775,50 +777,6 @@ onMounted(() => {
max-height: none;
}
.clean-result-summary {
display: grid;
grid-template-columns: repeat(3, minmax(0, 220px));
gap: 16px;
margin-bottom: 18px;
}
.summary-card {
padding: 16px 18px;
border: 1px solid #2e3a52;
border-radius: 10px;
background: #1c2333;
}
.summary-card strong {
display: block;
margin-top: 8px;
font-size: 24px;
color: #f5f8fc;
}
.summary-label {
font-size: 12px;
color: #5e6878;
}
.result-list-wrap {
border: 1px solid #2e3a52;
border-radius: 10px;
background: #1c2333;
min-height: 260px;
}
.result-list-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 14px 16px;
border-bottom: 1px solid #2e3a52;
font-size: 14px;
color: #c8d2e2;
}
@media (max-width: 1100px) {
.main-content {
flex-direction: column;
@@ -834,19 +792,5 @@ onMounted(() => {
.right-panel {
min-height: 420px;
}
.task-item {
flex-direction: column;
align-items: flex-start;
}
.task-right {
width: 100%;
justify-content: flex-start;
}
.clean-result-summary {
grid-template-columns: 1fr;
}
}
</style>