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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user