feat(dataset): 支持数据任务批量删除

This commit is contained in:
caoxiaozhu
2026-07-27 11:36:30 +08:00
parent d6e325fe9e
commit e486d36a80
3 changed files with 155 additions and 26 deletions

View File

@@ -7,11 +7,13 @@ const scriptDir = path.dirname(fileURLToPath(import.meta.url))
const typesPath = path.resolve(scriptDir, '../src/types/index.ts')
const dataPath = path.resolve(scriptDir, '../src/mock/data.ts')
const viewPath = path.resolve(scriptDir, '../src/views/dataset/DatasetListView.vue')
const tablePagePath = path.resolve(scriptDir, '../src/components/DataTablePage.vue')
const [typesSource, dataSource, viewSource] = await Promise.all([
const [typesSource, dataSource, viewSource, tablePageSource] = await Promise.all([
readFile(typesPath, 'utf8'),
readFile(dataPath, 'utf8'),
readFile(viewPath, 'utf8'),
readFile(tablePagePath, 'utf8'),
])
assert.match(typesSource, /export type DatasetSource = 'upload' \| 'task'/)
@@ -40,5 +42,16 @@ assert.match(
)
assert.match(viewSource, /return dataList\.value\.filter\(\(item\) => item\.source !== 'task'\)/)
assert.doesNotMatch(viewSource, /数据任务产生的数据集[\s\S]*?return \[\]/)
assert.match(viewSource, /:search-fields="\['task_id', 'name', 'description'\]"/, '数据任务搜索应支持任务 ID')
assert.match(viewSource, /:multi-select="activeTab === 'task' && batchMode"/, '多选框只能在数据任务的批量模式显示')
assert.match(viewSource, /:show-batch-bar="false"/, '数据任务应使用搜索框旁的批量删除入口')
assert.match(viewSource, /<template #toolbar-extra>[\s\S]*?批量删除[\s\S]*?v-if="selectedCount > 0"[\s\S]*?删除(\{\{ selectedCount \}\}/, '批量删除按钮没有按选择状态切换')
assert.match(viewSource, /const completed = await tablePageRef\.value\?\.handleBatchDelete\(\)/, '批量删除按钮没有调用通用批量删除流程')
assert.match(viewSource, /ElMessageBox\.confirm\([\s\S]*?删除后无法恢复/, '单条删除仍缺少二次确认')
assert.match(tablePageSource, /emit\('selectionChange', rows\)/, '通用表格没有向外同步多选状态')
assert.match(tablePageSource, /function clearSelection\(\)[\s\S]*?emit\('selectionChange', \[\]\)/, '清空选择后没有同步重置外部选择状态')
assert.match(tablePageSource, /确认批量删除[\s\S]*?confirmButtonText: '删除'[\s\S]*?confirmButtonClass: 'el-button--danger'/, '批量删除确认弹窗缺少危险操作样式')
assert.match(tablePageSource, /let ok = 0[\s\S]*?let fail = 0[\s\S]*?for \(const row of rows\)/, '批量删除没有汇总逐条删除结果')
assert.match(tablePageSource, /defineExpose\(\{ handleDelete, handleBatchDelete, clearSelection \}\)/, '页面无法触发通用批量删除')
console.log('数据任务 Mock 数据与页签分流回归检查通过')

View File

@@ -1,6 +1,6 @@
<script setup lang="ts" generic="T extends Record<string, any>">
import { ref, computed, watch } from 'vue'
import { ElMessageBox } from 'element-plus'
import { ElMessage, ElMessageBox } from 'element-plus'
import type { TableInstance } from 'element-plus'
import { useRouter } from 'vue-router'
@@ -19,6 +19,8 @@ const props = withDefaults(
searchFields?: string[]
/** 是否支持多选 */
multiSelect?: boolean
/** 选中后是否展示组件内置的批量操作栏 */
showBatchBar?: boolean
/** 创建按钮文本,不传则不显示 */
createText?: string
/** 创建按钮跳转路径 */
@@ -38,6 +40,7 @@ const props = withDefaults(
loading: false,
searchable: false,
multiSelect: false,
showBatchBar: true,
rowKey: 'id',
emptyText: '暂无数据',
pageSize: 10,
@@ -49,6 +52,7 @@ const emit = defineEmits<{
create: []
refresh: []
expandChange: [row: T, expandedRows: T[]]
selectionChange: [rows: T[]]
}>()
const router = useRouter()
@@ -57,6 +61,7 @@ const selectedIds = ref<Set<string | number>>(new Set())
const keyword = ref('')
const currentPage = ref(1)
const currentPageSize = ref(props.pageSize)
const batchDeleting = ref(false)
/** 行唯一标识取值 */
function rowId(row: T): string | number {
@@ -81,6 +86,7 @@ const pagedData = computed(() => {
/** 处理多选变化 */
function handleSelectionChange(rows: T[]) {
selectedIds.value = new Set(rows.map(rowId))
emit('selectionChange', rows)
}
/** 单个删除(子组件或操作列调用) */
@@ -100,14 +106,25 @@ async function handleDelete(row: any, confirmText = '确定要删除这条记录
}
/** 批量删除 */
async function handleBatchDelete() {
if (selectedIds.value.size === 0 || !props.deleteFn) return
async function handleBatchDelete(): Promise<boolean> {
if (selectedIds.value.size === 0 || !props.deleteFn || batchDeleting.value) return false
try {
await ElMessageBox.confirm(
`确定删除选中的 ${selectedIds.value.size} 条记录吗?`,
'批量删除',
{ type: 'warning' },
`确定删除选中的 ${selectedIds.value.size} 条记录吗?删除后无法恢复。`,
'确认批量删除',
{
type: 'warning',
confirmButtonText: '删除',
cancelButtonText: '取消',
confirmButtonClass: 'el-button--danger',
},
)
} catch {
return false
}
batchDeleting.value = true
try {
const rows = props.data.filter((row) => selectedIds.value.has(rowId(row)))
let ok = 0
let fail = 0
@@ -119,21 +136,23 @@ async function handleBatchDelete() {
fail++
}
}
selectedIds.value.clear()
clearSelection()
emit('refresh')
if (fail === 0) {
ElMessageBox.alert(`成功删除 ${ok} 条记录`, '成功', { type: 'success' })
ElMessage.success(`成功删除 ${ok} 条记录`)
} else {
ElMessageBox.alert(`成功 ${ok} 条,失败 ${fail}`, '部分失败', { type: 'warning' })
ElMessage.warning(`批量删除完成:成功 ${ok} 条,失败 ${fail}`)
}
} catch {
// 取消
return true
} finally {
batchDeleting.value = false
}
}
function clearSelection() {
selectedIds.value.clear()
tableRef.value?.clearSelection()
emit('selectionChange', [])
}
function handleCreate() {
@@ -152,20 +171,27 @@ function handleExpandChange(row: T, expandedRows: T[] | boolean) {
watch(
() => props.data,
() => {
selectedIds.value.clear()
clearSelection()
currentPage.value = 1
tableRef.value?.clearSelection()
},
)
watch(
() => props.multiSelect,
(enabled) => {
if (!enabled) clearSelection()
},
)
watch(
() => keyword.value,
() => {
if (selectedIds.value.size > 0) clearSelection()
currentPage.value = 1
},
)
defineExpose({ handleDelete, clearSelection })
defineExpose({ handleDelete, handleBatchDelete, clearSelection })
</script>
<template>
@@ -196,12 +222,12 @@ defineExpose({ handleDelete, clearSelection })
<!-- 批量操作栏 -->
<transition name="el-zoom-in-top">
<div v-if="multiSelect && selectedIds.size > 0" class="batch-bar">
<div v-if="multiSelect && showBatchBar && selectedIds.size > 0" class="batch-bar">
<span class="batch-info">
已选择 <strong>{{ selectedIds.size }}</strong>
<el-button link type="primary" @click="clearSelection">取消选择</el-button>
</span>
<el-button type="danger" size="small" @click="handleBatchDelete">
<el-button type="danger" size="small" :loading="batchDeleting" @click="handleBatchDelete">
<i class="fa fa-trash" style="margin-right: 4px" />
批量删除 ({{ selectedIds.size }})
</el-button>

View File

@@ -1,7 +1,7 @@
<script setup lang="ts">
import { ref, onMounted, computed } from 'vue'
import { ref, onMounted, computed, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import { ElMessage, ElMessageBox } from 'element-plus'
import DataTablePage from '@/components/DataTablePage.vue'
import { getDatasetList, deleteDataset, downloadDatasetUrl } from '@/api/modules/dataset'
import { DATASET_TYPE_MAP, STORAGE_MAP } from '@/constants'
@@ -13,6 +13,14 @@ const route = useRoute()
const loading = ref(false)
const dataList = ref<DatasetItem[]>([])
const activeTab = ref<DatasetSource>(route.query.tab === 'task' ? 'task' : 'upload')
const batchMode = ref(false)
const selectedCount = ref(0)
const batchDeleting = ref(false)
const deletingId = ref<string | number | null>(null)
const tablePageRef = ref<{
handleBatchDelete: () => Promise<boolean>
clearSelection: () => void
} | null>(null)
const filteredDataList = computed(() => {
if (activeTab.value === 'task') {
@@ -33,11 +41,62 @@ async function loadData() {
}
}
async function handleDelete(row: any) {
async function deleteDatasetItem(row: DatasetItem) {
await deleteDataset(row.id)
dataList.value = dataList.value.filter((item) => item.id !== row.id)
await loadData()
ElMessage.success('删除成功')
}
async function handleDelete(row: DatasetItem) {
try {
await ElMessageBox.confirm(
`确定删除数据集“${row.name}”吗?删除后无法恢复。`,
'确认删除',
{
type: 'warning',
confirmButtonText: '删除',
cancelButtonText: '取消',
confirmButtonClass: 'el-button--danger',
},
)
} catch {
return
}
deletingId.value = row.id
try {
await deleteDatasetItem(row)
await loadData()
ElMessage.success('数据集已删除')
} catch {
// 统一请求层已展示后端返回的失败原因。
} finally {
deletingId.value = null
}
}
function enterBatchMode() {
batchMode.value = true
selectedCount.value = 0
}
function exitBatchMode() {
batchMode.value = false
selectedCount.value = 0
tablePageRef.value?.clearSelection()
}
function handleSelectionChange(rows: DatasetItem[]) {
selectedCount.value = rows.length
}
async function handleBatchDelete() {
if (!selectedCount.value || batchDeleting.value) return
batchDeleting.value = true
try {
const completed = await tablePageRef.value?.handleBatchDelete()
if (completed) exitBatchMode()
} finally {
batchDeleting.value = false
}
}
function handlePreview(row: any) {
@@ -48,21 +107,26 @@ function handleDownload(row: any) {
window.open(downloadDatasetUrl(row.id), '_blank')
}
watch(activeTab, exitBatchMode)
onMounted(loadData)
</script>
<template>
<DataTablePage
ref="tablePageRef"
title="数据集管理"
:data="filteredDataList"
:loading="loading"
searchable
:search-fields="['name', 'description']"
:search-fields="['task_id', 'name', 'description']"
:multi-select="activeTab === 'task' && batchMode"
:show-batch-bar="false"
:create-text="activeTab === 'upload' ? '上传数据集' : ''"
create-to="/dataset/create"
:delete-fn="handleDelete"
:delete-fn="deleteDatasetItem"
row-key="id"
@refresh="loadData"
@selection-change="handleSelectionChange"
>
<template #title>
<div class="capsule-tabs">
@@ -83,6 +147,25 @@ onMounted(loadData)
</div>
</template>
<template #toolbar-extra>
<template v-if="activeTab === 'task'">
<el-button v-if="!batchMode" type="danger" plain @click="enterBatchMode">
<i class="fa fa-trash-o" style="margin-right: 4px" />批量删除
</el-button>
<template v-else>
<el-button :disabled="batchDeleting" @click="exitBatchMode">取消</el-button>
<el-button
v-if="selectedCount > 0"
type="danger"
:loading="batchDeleting"
@click="handleBatchDelete"
>
<i class="fa fa-trash-o" style="margin-right: 4px" />删除{{ selectedCount }}
</el-button>
</template>
</template>
</template>
<template #columns>
<el-table-column v-if="activeTab === 'task'" label="任务ID" prop="task_id" align="center" width="100" />
<el-table-column label="数据集名称" prop="name" align="center" />
@@ -124,7 +207,14 @@ onMounted(loadData)
<el-button type="success" link size="small" @click="handleDownload(row)">
<i class="fa fa-download" style="margin-right: 4px" />下载
</el-button>
<el-button type="danger" link size="small" @click="handleDelete(row)">
<el-button
type="danger"
link
size="small"
:loading="deletingId === row.id"
:disabled="batchDeleting || batchMode"
@click="handleDelete(row as DatasetItem)"
>
<i class="fa fa-trash-o" style="margin-right: 4px" />删除
</el-button>
</div>