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:
@@ -76,44 +76,36 @@
|
||||
</aside>
|
||||
|
||||
<section class="right-panel">
|
||||
<div class="panel-header">
|
||||
<span>品牌检测任务</span>
|
||||
<div class="panel-actions-row">
|
||||
<button type="button" class="btn-refresh" @click="loadTasks">刷新任务状态</button>
|
||||
</div>
|
||||
<div class="task-list-wrap">
|
||||
<div v-if="!tasks.length" class="empty-tasks">
|
||||
暂无品牌检测任务。选择待检文件后点击「运行」或「添加到任务队列」。
|
||||
</div>
|
||||
<ul v-else class="task-list">
|
||||
<li v-for="item in tasks" :key="`brand-${item.id}`" class="task-item">
|
||||
<div class="left">
|
||||
<span class="id" :title="taskDesc(item)">{{ taskDesc(item) }}</span>
|
||||
<div class="files">任务 ID:{{ item.id }}</div>
|
||||
<div v-if="item.file_paths?.length" class="files">文件:{{ item.file_paths.length }} 个</div>
|
||||
<div class="files">创建时间:{{ formatDateTime(item.created_at) }}</div>
|
||||
<template v-if="isRunning(item)">
|
||||
<div class="task-progress">
|
||||
<div class="task-progress-meta">
|
||||
<span>任务进度</span>
|
||||
<span>{{ taskProgress(item) }}%</span>
|
||||
</div>
|
||||
<div class="task-progress-track">
|
||||
<div class="task-progress-fill" :style="{ width: `${taskProgress(item)}%` }"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="errorMessage(item)" class="files error">{{ errorMessage(item) }}</div>
|
||||
</template>
|
||||
<div v-else-if="errorMessage(item)" class="files error">错误:{{ errorMessage(item) }}</div>
|
||||
</div>
|
||||
<div class="task-right">
|
||||
<span class="status" :class="statusClass(item.status)">{{ statusText(item.status) }}</span>
|
||||
<button v-if="isRunning(item)" type="button" class="act-btn danger" @click="cancelTask(item)">取消</button>
|
||||
<button v-if="canDownload(item)" type="button" class="act-btn ok" @click="downloadResult(item)">下载结果</button>
|
||||
<button v-if="canDelete(item)" type="button" class="act-btn" @click="deleteTask(item)">删除</button>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<TaskCenterPanel
|
||||
:on-batch-delete="batchDeleteHistory"
|
||||
title="品牌检测任务"
|
||||
:cards="brandCards"
|
||||
: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="isRunning(itemSource(item))" type="button" class="btn-delete"
|
||||
@click="cancelTask(itemSource(item))">取消</button>
|
||||
<button v-if="canDelete(itemSource(item))" type="button" class="btn-delete"
|
||||
@click="deleteTask(itemSource(item))">删除</button>
|
||||
</template>
|
||||
</template>
|
||||
<template #history-item-actions="{ item }">
|
||||
<template v-if="itemSource(item)">
|
||||
<button v-if="canDownload(itemSource(item))" type="button" class="download"
|
||||
@click="downloadResult(itemSource(item))">下载结果</button>
|
||||
<button v-if="canDelete(itemSource(item))" type="button" class="btn-delete"
|
||||
@click="deleteTask(itemSource(item))">删除</button>
|
||||
</template>
|
||||
</template>
|
||||
</TaskCenterPanel>
|
||||
</section>
|
||||
</div>
|
||||
</AmazonToolPageShell>
|
||||
@@ -124,18 +116,21 @@
|
||||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
import { ElMessage, ElMessageBox } 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 { getPywebviewApi } from '@/shared/bridges/pywebview'
|
||||
import { requestPostJson } from '@/shared/api/http'
|
||||
import {
|
||||
cancelBrandTask,
|
||||
createBrandTask,
|
||||
deleteBrandTask,
|
||||
expandBrandFolder,
|
||||
getBrandTaskDownloadUrl,
|
||||
getBrandTasks,
|
||||
runBrandNow,
|
||||
type BrandTaskItem,
|
||||
} from '@/shared/api/brand'
|
||||
|
||||
/** 扩展文件夹接口响应(向后端真实字段 paths 兼容,shared 类型仅声明 items 时以本接口为准) */
|
||||
interface BrandExpandFolderPathsResponse {
|
||||
success: boolean
|
||||
paths?: string[]
|
||||
@@ -172,6 +167,11 @@ function isRunning(item: BrandTaskItem) {
|
||||
return (item.status || '').toLowerCase() === 'running'
|
||||
}
|
||||
|
||||
function isBusyBrandTask(item: BrandTaskItem) {
|
||||
const status = (item.status || '').toLowerCase()
|
||||
return status === 'pending' || status === 'running'
|
||||
}
|
||||
|
||||
function isTerminal(item: BrandTaskItem) {
|
||||
const status = (item.status || '').toLowerCase()
|
||||
return status === 'success' || status === 'failed' || status === 'cancelled'
|
||||
@@ -193,11 +193,6 @@ function taskProgress(item: BrandTaskItem) {
|
||||
return Math.max(0, Math.min(100, Math.round((current / total) * 100)))
|
||||
}
|
||||
|
||||
function taskDesc(item: BrandTaskItem) {
|
||||
const desc = (item.desc || '').trim()
|
||||
return desc ? (desc.length > 60 ? `${desc.slice(0, 60)}…` : desc) : `任务 #${item.id}`
|
||||
}
|
||||
|
||||
function errorMessage(item: BrandTaskItem) {
|
||||
return (item.error_message || '').trim()
|
||||
}
|
||||
@@ -205,7 +200,7 @@ function errorMessage(item: BrandTaskItem) {
|
||||
function statusText(status?: BrandTaskItem['status']) {
|
||||
const value = (status || '').toLowerCase()
|
||||
const map: Record<string, string> = {
|
||||
pending: '等待中',
|
||||
pending: '排队中',
|
||||
running: '执行中',
|
||||
success: '已完成',
|
||||
failed: '失败',
|
||||
@@ -219,10 +214,57 @@ function statusClass(status?: BrandTaskItem['status']) {
|
||||
if (value === 'running') return 'running'
|
||||
if (value === 'success') return 'success'
|
||||
if (value === 'failed') return 'failed'
|
||||
if (value === 'cancelled') return 'cancelled'
|
||||
return 'pending'
|
||||
}
|
||||
|
||||
// ---- 右侧任务面板统一视图(TaskCenterPanel)----
|
||||
/** 当前任务:排队中 / 执行中的任务 */
|
||||
const currentTaskViews = computed<TaskItemView[]>(() =>
|
||||
tasks.value.filter(isBusyBrandTask).map(toBrandTaskView),
|
||||
)
|
||||
|
||||
/** 历史任务:终态(成功 / 失败 / 已取消 / 其他状态)的任务 */
|
||||
const historyTaskViews = computed<TaskItemView[]>(() =>
|
||||
tasks.value.filter((item) => !isBusyBrandTask(item)).map(toBrandTaskView),
|
||||
)
|
||||
|
||||
const brandCards = computed<TaskStatCard[]>(() => [
|
||||
{ label: '运行中任务', value: currentTaskViews.value.length },
|
||||
{ label: '已结束任务', value: historyTaskViews.value.length },
|
||||
{ label: '成功任务', value: historyTaskViews.value.filter((view) => view.statusClass === 'success').length },
|
||||
{ label: '失败任务', value: historyTaskViews.value.filter((view) => view.statusClass === 'failed').length },
|
||||
])
|
||||
|
||||
function itemSource(item: TaskItemView): BrandTaskItem {
|
||||
return item.source as BrandTaskItem
|
||||
}
|
||||
|
||||
function toBrandTaskView(item: BrandTaskItem): TaskItemView {
|
||||
const total = Number(item.progress_total) || 0
|
||||
const current = Number(item.progress_current) || 0
|
||||
return {
|
||||
key: `brand-${item.id}`,
|
||||
title: (item.desc || '').trim() || '品牌检测',
|
||||
taskId: item.id,
|
||||
startedAt: formatDateTime(item.created_at),
|
||||
finishedAt: isTerminal(item) ? formatDateTime(item.updated_at) : '',
|
||||
statusText: statusText(item.status),
|
||||
statusClass: statusClass(item.status),
|
||||
extraLines: [
|
||||
...(item.file_paths?.length ? [`文件:${item.file_paths.length} 个`] : []),
|
||||
...(errorMessage(item) ? [`错误:${errorMessage(item)}`] : []),
|
||||
],
|
||||
progress: isRunning(item)
|
||||
? {
|
||||
percent: taskProgress(item),
|
||||
stage: '任务进度',
|
||||
countLabel: total > 0 ? `${current}/${total}` : undefined,
|
||||
}
|
||||
: null,
|
||||
source: item,
|
||||
}
|
||||
}
|
||||
|
||||
function formatDateTime(value?: string) {
|
||||
if (!value) return '-'
|
||||
const date = new Date(value)
|
||||
@@ -267,7 +309,7 @@ async function selectFolder() {
|
||||
try {
|
||||
const folder = await bridge.select_brand_folder()
|
||||
if (!folder) return
|
||||
const res = await requestPostJson<BrandExpandFolderPathsResponse>('/api/brand/expand-folder', { folder })
|
||||
const res = (await expandBrandFolder(folder)) as BrandExpandFolderPathsResponse
|
||||
if (!res.success || !res.paths?.length) {
|
||||
ElMessage.warning(res.error || '该文件夹下没有 xlsx 文件')
|
||||
return
|
||||
@@ -433,6 +475,26 @@ onBeforeUnmount(() => {
|
||||
pollTimer = null
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* 历史记录批量删除:复用单条删除逻辑(删除后原函数各自刷新列表)
|
||||
*/
|
||||
async function batchDeleteHistory(views: TaskItemView[]) {
|
||||
if (!views.length) return
|
||||
let failed = 0
|
||||
for (const view of views) {
|
||||
try {
|
||||
await deleteTask(itemSource(view))
|
||||
} catch {
|
||||
failed += 1
|
||||
}
|
||||
}
|
||||
if (failed > 0) {
|
||||
ElMessage.warning(`删除完成,${failed} 条失败`)
|
||||
} else {
|
||||
ElMessage.success(`已删除 ${views.length} 条历史记录`)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -466,29 +528,12 @@ onBeforeUnmount(() => {
|
||||
.btn-run { padding: 10px 18px; color: #f5f8fc; background: #3498db; border: none; border-radius: 8px; font-weight: 600; cursor: pointer; }
|
||||
.btn-run:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
.loading-msg { margin-top: 10px; }
|
||||
.panel-header { display: flex; justify-content: space-between; align-items: center; padding: 14px 20px; border-bottom: 1px solid #2e3a52; color: #f5f8fc; font-size: 15px; font-weight: 600; }
|
||||
.panel-actions-row { display: flex; justify-content: flex-end; padding: 10px 20px 0; }
|
||||
.btn-refresh { padding: 6px 12px; color: #c8d2e2; background: #2e3a52; border: 1px solid #3e4a62; border-radius: 6px; cursor: pointer; font-size: 12px; }
|
||||
.task-list-wrap { flex: 1; padding: 16px 20px; overflow: auto; }
|
||||
.empty-tasks { color: #5e6878; font-size: 13px; padding: 28px; text-align: center; }
|
||||
.task-list { list-style: none; margin: 0; padding: 0; }
|
||||
.task-item { display: flex; justify-content: space-between; align-items: flex-start; gap: 12px; padding: 12px 14px; margin-bottom: 8px; border: 1px solid #2e3a52; border-radius: 8px; background: #1c2333; }
|
||||
.left { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 4px; }
|
||||
.id { color: #f5f8fc; font-size: 13px; font-weight: 600; }
|
||||
.files.error, .error { color: #ff6b6b; }
|
||||
.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; }
|
||||
.status.pending { background: rgba(149, 165, 166, 0.18); color: #a0acbe; }
|
||||
.status.running { background: rgba(52, 152, 219, 0.18); color: #3498db; }
|
||||
.status.success { background: rgba(46, 204, 113, 0.18); color: #2ecc71; }
|
||||
.status.failed { background: rgba(231, 76, 60, 0.18); color: #ff6b6b; }
|
||||
.status.cancelled { background: rgba(149, 165, 166, 0.18); color: #a0acbe; }
|
||||
.act-btn { padding: 5px 10px; border-radius: 6px; font-size: 12px; background: #2e3a52; color: #c8d2e2; border: 1px solid #3e4a62; cursor: pointer; }
|
||||
.act-btn.ok { background: rgba(52, 152, 219, 0.18); color: #69b6ff; border-color: transparent; }
|
||||
.act-btn.danger { background: rgba(231, 76, 60, 0.15); color: #ff8f8f; border-color: transparent; }
|
||||
.task-progress { margin-top: 6px; max-width: 460px; }
|
||||
.task-progress-meta { display: flex; justify-content: space-between; color: #5e6878; font-size: 11px; margin-bottom: 4px; }
|
||||
.task-progress-track { height: 5px; border-radius: 3px; background: #333; overflow: hidden; }
|
||||
.task-progress-fill { height: 100%; background: #3498db; transition: width 0.25s ease; }
|
||||
.download { padding: 6px 10px; border-radius: 6px; font-size: 12px; background: rgba(52, 152, 219, 0.18); color: #69b6ff; border: none; cursor: pointer; }
|
||||
.download:hover { background: rgba(52, 152, 219, 0.28); }
|
||||
.btn-delete { padding: 6px 10px; border-radius: 6px; font-size: 12px; background: rgba(231, 76, 60, 0.12); color: #ff8f8f; border: none; cursor: pointer; }
|
||||
.btn-delete:hover { background: rgba(231, 76, 60, 0.22); }
|
||||
@media (max-width: 1100px) {
|
||||
.main-content { flex-direction: column; height: auto; }
|
||||
.left-panel { width: 100%; border-right: none; border-bottom: 1px solid #2e3a52; }
|
||||
|
||||
Reference in New Issue
Block a user