feat(dataset): 支持数据任务批量删除
This commit is contained in:
@@ -7,11 +7,13 @@ const scriptDir = path.dirname(fileURLToPath(import.meta.url))
|
|||||||
const typesPath = path.resolve(scriptDir, '../src/types/index.ts')
|
const typesPath = path.resolve(scriptDir, '../src/types/index.ts')
|
||||||
const dataPath = path.resolve(scriptDir, '../src/mock/data.ts')
|
const dataPath = path.resolve(scriptDir, '../src/mock/data.ts')
|
||||||
const viewPath = path.resolve(scriptDir, '../src/views/dataset/DatasetListView.vue')
|
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(typesPath, 'utf8'),
|
||||||
readFile(dataPath, 'utf8'),
|
readFile(dataPath, 'utf8'),
|
||||||
readFile(viewPath, 'utf8'),
|
readFile(viewPath, 'utf8'),
|
||||||
|
readFile(tablePagePath, 'utf8'),
|
||||||
])
|
])
|
||||||
|
|
||||||
assert.match(typesSource, /export type DatasetSource = 'upload' \| 'task'/)
|
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.match(viewSource, /return dataList\.value\.filter\(\(item\) => item\.source !== 'task'\)/)
|
||||||
assert.doesNotMatch(viewSource, /数据任务产生的数据集[\s\S]*?return \[\]/)
|
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 数据与页签分流回归检查通过')
|
console.log('数据任务 Mock 数据与页签分流回归检查通过')
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts" generic="T extends Record<string, any>">
|
<script setup lang="ts" generic="T extends Record<string, any>">
|
||||||
import { ref, computed, watch } from 'vue'
|
import { ref, computed, watch } from 'vue'
|
||||||
import { ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import type { TableInstance } from 'element-plus'
|
import type { TableInstance } from 'element-plus'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
@@ -19,6 +19,8 @@ const props = withDefaults(
|
|||||||
searchFields?: string[]
|
searchFields?: string[]
|
||||||
/** 是否支持多选 */
|
/** 是否支持多选 */
|
||||||
multiSelect?: boolean
|
multiSelect?: boolean
|
||||||
|
/** 选中后是否展示组件内置的批量操作栏 */
|
||||||
|
showBatchBar?: boolean
|
||||||
/** 创建按钮文本,不传则不显示 */
|
/** 创建按钮文本,不传则不显示 */
|
||||||
createText?: string
|
createText?: string
|
||||||
/** 创建按钮跳转路径 */
|
/** 创建按钮跳转路径 */
|
||||||
@@ -38,6 +40,7 @@ const props = withDefaults(
|
|||||||
loading: false,
|
loading: false,
|
||||||
searchable: false,
|
searchable: false,
|
||||||
multiSelect: false,
|
multiSelect: false,
|
||||||
|
showBatchBar: true,
|
||||||
rowKey: 'id',
|
rowKey: 'id',
|
||||||
emptyText: '暂无数据',
|
emptyText: '暂无数据',
|
||||||
pageSize: 10,
|
pageSize: 10,
|
||||||
@@ -49,6 +52,7 @@ const emit = defineEmits<{
|
|||||||
create: []
|
create: []
|
||||||
refresh: []
|
refresh: []
|
||||||
expandChange: [row: T, expandedRows: T[]]
|
expandChange: [row: T, expandedRows: T[]]
|
||||||
|
selectionChange: [rows: T[]]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -57,6 +61,7 @@ const selectedIds = ref<Set<string | number>>(new Set())
|
|||||||
const keyword = ref('')
|
const keyword = ref('')
|
||||||
const currentPage = ref(1)
|
const currentPage = ref(1)
|
||||||
const currentPageSize = ref(props.pageSize)
|
const currentPageSize = ref(props.pageSize)
|
||||||
|
const batchDeleting = ref(false)
|
||||||
|
|
||||||
/** 行唯一标识取值 */
|
/** 行唯一标识取值 */
|
||||||
function rowId(row: T): string | number {
|
function rowId(row: T): string | number {
|
||||||
@@ -81,6 +86,7 @@ const pagedData = computed(() => {
|
|||||||
/** 处理多选变化 */
|
/** 处理多选变化 */
|
||||||
function handleSelectionChange(rows: T[]) {
|
function handleSelectionChange(rows: T[]) {
|
||||||
selectedIds.value = new Set(rows.map(rowId))
|
selectedIds.value = new Set(rows.map(rowId))
|
||||||
|
emit('selectionChange', rows)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 单个删除(子组件或操作列调用) */
|
/** 单个删除(子组件或操作列调用) */
|
||||||
@@ -100,14 +106,25 @@ async function handleDelete(row: any, confirmText = '确定要删除这条记录
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** 批量删除 */
|
/** 批量删除 */
|
||||||
async function handleBatchDelete() {
|
async function handleBatchDelete(): Promise<boolean> {
|
||||||
if (selectedIds.value.size === 0 || !props.deleteFn) return
|
if (selectedIds.value.size === 0 || !props.deleteFn || batchDeleting.value) return false
|
||||||
try {
|
try {
|
||||||
await ElMessageBox.confirm(
|
await ElMessageBox.confirm(
|
||||||
`确定要删除选中的 ${selectedIds.value.size} 条记录吗?`,
|
`确定删除选中的 ${selectedIds.value.size} 条记录吗?删除后无法恢复。`,
|
||||||
'批量删除',
|
'确认批量删除',
|
||||||
{ type: 'warning' },
|
{
|
||||||
|
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)))
|
const rows = props.data.filter((row) => selectedIds.value.has(rowId(row)))
|
||||||
let ok = 0
|
let ok = 0
|
||||||
let fail = 0
|
let fail = 0
|
||||||
@@ -119,21 +136,23 @@ async function handleBatchDelete() {
|
|||||||
fail++
|
fail++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
selectedIds.value.clear()
|
clearSelection()
|
||||||
emit('refresh')
|
emit('refresh')
|
||||||
if (fail === 0) {
|
if (fail === 0) {
|
||||||
ElMessageBox.alert(`成功删除 ${ok} 条记录`, '成功', { type: 'success' })
|
ElMessage.success(`成功删除 ${ok} 条记录`)
|
||||||
} else {
|
} else {
|
||||||
ElMessageBox.alert(`成功 ${ok} 条,失败 ${fail} 条`, '部分失败', { type: 'warning' })
|
ElMessage.warning(`批量删除完成:成功 ${ok} 条,失败 ${fail} 条`)
|
||||||
}
|
}
|
||||||
} catch {
|
return true
|
||||||
// 取消
|
} finally {
|
||||||
|
batchDeleting.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearSelection() {
|
function clearSelection() {
|
||||||
selectedIds.value.clear()
|
selectedIds.value.clear()
|
||||||
tableRef.value?.clearSelection()
|
tableRef.value?.clearSelection()
|
||||||
|
emit('selectionChange', [])
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleCreate() {
|
function handleCreate() {
|
||||||
@@ -152,20 +171,27 @@ function handleExpandChange(row: T, expandedRows: T[] | boolean) {
|
|||||||
watch(
|
watch(
|
||||||
() => props.data,
|
() => props.data,
|
||||||
() => {
|
() => {
|
||||||
selectedIds.value.clear()
|
clearSelection()
|
||||||
currentPage.value = 1
|
currentPage.value = 1
|
||||||
tableRef.value?.clearSelection()
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.multiSelect,
|
||||||
|
(enabled) => {
|
||||||
|
if (!enabled) clearSelection()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => keyword.value,
|
() => keyword.value,
|
||||||
() => {
|
() => {
|
||||||
|
if (selectedIds.value.size > 0) clearSelection()
|
||||||
currentPage.value = 1
|
currentPage.value = 1
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
defineExpose({ handleDelete, clearSelection })
|
defineExpose({ handleDelete, handleBatchDelete, clearSelection })
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -196,12 +222,12 @@ defineExpose({ handleDelete, clearSelection })
|
|||||||
|
|
||||||
<!-- 批量操作栏 -->
|
<!-- 批量操作栏 -->
|
||||||
<transition name="el-zoom-in-top">
|
<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">
|
<span class="batch-info">
|
||||||
已选择 <strong>{{ selectedIds.size }}</strong> 项
|
已选择 <strong>{{ selectedIds.size }}</strong> 项
|
||||||
<el-button link type="primary" @click="clearSelection">取消选择</el-button>
|
<el-button link type="primary" @click="clearSelection">取消选择</el-button>
|
||||||
</span>
|
</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" />
|
<i class="fa fa-trash" style="margin-right: 4px" />
|
||||||
批量删除 ({{ selectedIds.size }})
|
批量删除 ({{ selectedIds.size }})
|
||||||
</el-button>
|
</el-button>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, computed } from 'vue'
|
import { ref, onMounted, computed, watch } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import DataTablePage from '@/components/DataTablePage.vue'
|
import DataTablePage from '@/components/DataTablePage.vue'
|
||||||
import { getDatasetList, deleteDataset, downloadDatasetUrl } from '@/api/modules/dataset'
|
import { getDatasetList, deleteDataset, downloadDatasetUrl } from '@/api/modules/dataset'
|
||||||
import { DATASET_TYPE_MAP, STORAGE_MAP } from '@/constants'
|
import { DATASET_TYPE_MAP, STORAGE_MAP } from '@/constants'
|
||||||
@@ -13,6 +13,14 @@ const route = useRoute()
|
|||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const dataList = ref<DatasetItem[]>([])
|
const dataList = ref<DatasetItem[]>([])
|
||||||
const activeTab = ref<DatasetSource>(route.query.tab === 'task' ? 'task' : 'upload')
|
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(() => {
|
const filteredDataList = computed(() => {
|
||||||
if (activeTab.value === 'task') {
|
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)
|
await deleteDataset(row.id)
|
||||||
dataList.value = dataList.value.filter((item) => item.id !== row.id)
|
}
|
||||||
|
|
||||||
|
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()
|
await loadData()
|
||||||
ElMessage.success('删除成功')
|
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) {
|
function handlePreview(row: any) {
|
||||||
@@ -48,21 +107,26 @@ function handleDownload(row: any) {
|
|||||||
window.open(downloadDatasetUrl(row.id), '_blank')
|
window.open(downloadDatasetUrl(row.id), '_blank')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
watch(activeTab, exitBatchMode)
|
||||||
onMounted(loadData)
|
onMounted(loadData)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<DataTablePage
|
<DataTablePage
|
||||||
|
ref="tablePageRef"
|
||||||
title="数据集管理"
|
title="数据集管理"
|
||||||
:data="filteredDataList"
|
:data="filteredDataList"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
searchable
|
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-text="activeTab === 'upload' ? '上传数据集' : ''"
|
||||||
create-to="/dataset/create"
|
create-to="/dataset/create"
|
||||||
:delete-fn="handleDelete"
|
:delete-fn="deleteDatasetItem"
|
||||||
row-key="id"
|
row-key="id"
|
||||||
@refresh="loadData"
|
@refresh="loadData"
|
||||||
|
@selection-change="handleSelectionChange"
|
||||||
>
|
>
|
||||||
<template #title>
|
<template #title>
|
||||||
<div class="capsule-tabs">
|
<div class="capsule-tabs">
|
||||||
@@ -83,6 +147,25 @@ onMounted(loadData)
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</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>
|
<template #columns>
|
||||||
<el-table-column v-if="activeTab === 'task'" label="任务ID" prop="task_id" align="center" width="100" />
|
<el-table-column v-if="activeTab === 'task'" label="任务ID" prop="task_id" align="center" width="100" />
|
||||||
<el-table-column label="数据集名称" prop="name" align="center" />
|
<el-table-column label="数据集名称" prop="name" align="center" />
|
||||||
@@ -124,7 +207,14 @@ onMounted(loadData)
|
|||||||
<el-button type="success" link size="small" @click="handleDownload(row)">
|
<el-button type="success" link size="small" @click="handleDownload(row)">
|
||||||
<i class="fa fa-download" style="margin-right: 4px" />下载
|
<i class="fa fa-download" style="margin-right: 4px" />下载
|
||||||
</el-button>
|
</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" />删除
|
<i class="fa fa-trash-o" style="margin-right: 4px" />删除
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user