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:
@@ -0,0 +1,254 @@
|
||||
<template>
|
||||
<div class="history-task-layer">
|
||||
<button type="button" class="history-btn" @click="open = true">
|
||||
历史任务<template v-if="count > 0">({{ count }})</template>
|
||||
</button>
|
||||
|
||||
<el-drawer v-model="open" :title="historyTitle" size="640px" class="task-history-drawer">
|
||||
<div class="history-body">
|
||||
<!-- 批量删除工具条(传入 onBatchDelete 时显示) -->
|
||||
<div v-if="onBatchDelete" class="history-toolbar">
|
||||
<label class="select-all">
|
||||
<input v-model="allSelected" type="checkbox" title="全选/取消全选" />
|
||||
<span>全选</span>
|
||||
</label>
|
||||
<span class="selected-count" v-if="selectedKeys.size > 0">已选 {{ selectedKeys.size }} 项</span>
|
||||
<button type="button" class="btn-delete" :disabled="!selectedKeys.size || batchDeleting"
|
||||
@click="confirmBatchDelete">
|
||||
{{ batchDeleting ? '删除中...' : `批量删除${selectedKeys.size ? `(${selectedKeys.size})` : ''}` }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="!items.length" class="empty-tasks">{{ emptyText }}</div>
|
||||
<ul v-else class="history-list">
|
||||
<li v-for="item in items" :key="item.key" class="history-item-row" :class="{ 'is-selected': isSelected(item.key) }">
|
||||
<label v-if="onBatchDelete" class="row-check" @click.stop>
|
||||
<input v-model="selectedSet" type="checkbox" :value="item.key" :disabled="batchDeleting" />
|
||||
</label>
|
||||
<div class="history-item-card">
|
||||
<TaskItemCard :item="item">
|
||||
<template #extra="scope">
|
||||
<slot name="history-item-extra" v-bind="scope" />
|
||||
</template>
|
||||
<template #actions="scope">
|
||||
<slot name="history-item-actions" v-bind="scope" />
|
||||
</template>
|
||||
</TaskItemCard>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { ElMessageBox } from 'element-plus'
|
||||
|
||||
import TaskItemCard from './TaskItemCard.vue'
|
||||
import type { TaskItemView } from './types'
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
/** 历史任务数量(按钮角标) */
|
||||
count: number
|
||||
/** 弹层标题 */
|
||||
historyTitle?: string
|
||||
/** 历史任务列表 */
|
||||
items: TaskItemView[]
|
||||
/** 空列表提示 */
|
||||
emptyText?: string
|
||||
/** 批量删除回调(传入后历史列表支持勾选批量删除;不传则不显示复选框) */
|
||||
onBatchDelete?: (items: TaskItemView[]) => Promise<void> | void
|
||||
}>(), {
|
||||
historyTitle: '历史任务',
|
||||
emptyText: '暂无历史任务',
|
||||
})
|
||||
|
||||
const open = ref(false)
|
||||
const batchDeleting = ref(false)
|
||||
/** 勾选集合:以 TaskItemView.key 为维度 */
|
||||
const selectedSet = ref<Set<string>>(new Set())
|
||||
|
||||
const selectedKeys = computed(() => selectedSet.value)
|
||||
const allSelected = computed({
|
||||
get: () => Boolean(props.items.length) && selectedSet.value.size === props.items.length,
|
||||
set: (checked: boolean) => {
|
||||
const next = new Set<string>()
|
||||
if (checked) {
|
||||
props.items.forEach((item) => next.add(item.key))
|
||||
}
|
||||
selectedSet.value = next
|
||||
},
|
||||
})
|
||||
|
||||
watch(open, (value) => {
|
||||
if (!value) {
|
||||
selectedSet.value = new Set()
|
||||
}
|
||||
})
|
||||
|
||||
function isSelected(key: string) {
|
||||
return selectedSet.value.has(key)
|
||||
}
|
||||
|
||||
async function confirmBatchDelete() {
|
||||
if (!props.onBatchDelete || !selectedSet.value.size || batchDeleting.value) return
|
||||
const target = props.items.filter((item) => selectedSet.value.has(item.key))
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定批量删除选中的 ${target.length} 条历史记录吗?删除后不可恢复。`,
|
||||
'批量删除历史记录',
|
||||
{ confirmButtonText: '删除', cancelButtonText: '取消', type: 'warning' },
|
||||
)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
batchDeleting.value = true
|
||||
try {
|
||||
await props.onBatchDelete(target)
|
||||
selectedSet.value = new Set()
|
||||
} finally {
|
||||
batchDeleting.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.history-task-layer {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.history-btn {
|
||||
padding: 5px 12px;
|
||||
border: 1px solid #3e4a62;
|
||||
border-radius: 6px;
|
||||
background: #222;
|
||||
color: #c8d2e2;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
transition: all .2s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.history-btn:hover {
|
||||
background: #2b3447;
|
||||
color: #f5f8fc;
|
||||
}
|
||||
|
||||
.history-body {
|
||||
padding: 4px 2px 12px;
|
||||
}
|
||||
|
||||
.history-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
padding: 0 4px 12px;
|
||||
border-bottom: 1px solid #2e3a52;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.select-all {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: #c8d2e2;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.selected-count {
|
||||
color: #a0acbe;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.btn-delete {
|
||||
margin-left: auto;
|
||||
padding: 6px 14px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
background: rgba(231, 76, 60, .18);
|
||||
color: #ff8f8f;
|
||||
font-size: 12px;
|
||||
transition: background .2s;
|
||||
}
|
||||
|
||||
.btn-delete:hover:not(:disabled) {
|
||||
background: rgba(231, 76, 60, .32);
|
||||
}
|
||||
|
||||
.btn-delete:disabled {
|
||||
opacity: .5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.empty-tasks {
|
||||
color: #5e6878;
|
||||
font-size: 13px;
|
||||
padding: 18px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.history-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.history-item-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.row-check {
|
||||
flex: 0 0 auto;
|
||||
padding-top: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.row-check input {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.history-item-card {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.history-item-row.is-selected .history-item-card {
|
||||
border-radius: 8px;
|
||||
outline: 2px solid rgba(52, 152, 219, .35);
|
||||
outline-offset: -1px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
/* el-drawer 深色主题(非 scoped:Element Plus 组件挂在 body 下) */
|
||||
.task-history-drawer .el-drawer__header {
|
||||
margin-bottom: 0;
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid #2e3a52;
|
||||
color: #f5f8fc;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.task-history-drawer .el-drawer__close-btn {
|
||||
color: #a0acbe;
|
||||
}
|
||||
|
||||
.task-history-drawer .el-drawer__body {
|
||||
background: #151a25;
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
.task-history-drawer .el-drawer {
|
||||
background: #151a25;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user