This commit is contained in:
625
src/views/polling-management/data-cleanup/index.vue
Normal file
625
src/views/polling-management/data-cleanup/index.vue
Normal file
@@ -0,0 +1,625 @@
|
||||
<template>
|
||||
<ArtTableFullScreen>
|
||||
<div class="data-cleanup-page" id="table-full-screen">
|
||||
<!-- 进度提示 -->
|
||||
<ElAlert
|
||||
v-if="progress?.is_running"
|
||||
title="数据清理进行中"
|
||||
type="info"
|
||||
:closable="false"
|
||||
style="margin-bottom: 16px"
|
||||
>
|
||||
<template #default>
|
||||
<div>
|
||||
当前清理表: {{ progress.current_table }} | 已处理: {{ progress.processed_tables }}/{{
|
||||
progress.total_tables
|
||||
}}
|
||||
| 已删除: {{ progress.total_deleted }} 条记录
|
||||
</div>
|
||||
</template>
|
||||
</ElAlert>
|
||||
|
||||
<ElCard shadow="never" class="art-table-card">
|
||||
<!-- 表格头部 -->
|
||||
<ArtTableHeader
|
||||
:columnList="columnOptions"
|
||||
v-model:columns="columnChecks"
|
||||
@refresh="handleRefresh"
|
||||
>
|
||||
<template #left>
|
||||
<ElButton type="primary" @click="showCreateDialog">新增配置</ElButton>
|
||||
<ElButton @click="handlePreview">预览清理</ElButton>
|
||||
<ElButton type="warning" @click="handleTriggerCleanup" :loading="triggering">
|
||||
手动清理
|
||||
</ElButton>
|
||||
</template>
|
||||
</ArtTableHeader>
|
||||
|
||||
<!-- 表格 -->
|
||||
<ArtTable
|
||||
ref="tableRef"
|
||||
row-key="id"
|
||||
:loading="loading"
|
||||
:data="configList"
|
||||
:marginTop="10"
|
||||
:row-class-name="getRowClassName"
|
||||
:show-pagination="false"
|
||||
@row-contextmenu="handleRowContextMenu"
|
||||
@cell-mouse-enter="handleCellMouseEnter"
|
||||
@cell-mouse-leave="handleCellMouseLeave"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
|
||||
</template>
|
||||
</ArtTable>
|
||||
|
||||
<!-- 鼠标悬浮提示 -->
|
||||
<TableContextMenuHint :visible="showContextMenuHint" :position="hintPosition" />
|
||||
|
||||
<!-- 右键菜单 -->
|
||||
<ArtMenuRight
|
||||
ref="contextMenuRef"
|
||||
:menu-items="contextMenuItems"
|
||||
:menu-width="140"
|
||||
@select="handleContextMenuSelect"
|
||||
/>
|
||||
|
||||
<!-- 新增/编辑对话框 -->
|
||||
<ElDialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogType === 'add' ? '新增配置' : '编辑配置'"
|
||||
width="600px"
|
||||
:close-on-click-modal="false"
|
||||
@closed="handleDialogClosed"
|
||||
>
|
||||
<ElForm ref="formRef" :model="form" :rules="rules" label-width="120px">
|
||||
<ElFormItem label="表名" prop="table_name">
|
||||
<ElInput
|
||||
v-model="form.table_name"
|
||||
placeholder="请输入表名"
|
||||
:disabled="dialogType === 'edit'"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="配置说明" prop="description">
|
||||
<ElInput
|
||||
v-model="form.description"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
placeholder="请输入配置说明"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="保留天数" prop="retention_days">
|
||||
<ElInputNumber
|
||||
v-model="form.retention_days"
|
||||
:min="7"
|
||||
:max="3650"
|
||||
placeholder="最少7天"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="每批删除条数" prop="batch_size">
|
||||
<ElInputNumber
|
||||
v-model="form.batch_size"
|
||||
:min="100"
|
||||
:max="100000"
|
||||
placeholder="默认10000"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem v-if="dialogType === 'edit'" label="是否启用" prop="enabled">
|
||||
<ElSwitch
|
||||
v-model="form.enabled"
|
||||
:active-value="1"
|
||||
:inactive-value="0"
|
||||
active-text="启用"
|
||||
inactive-text="禁用"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="dialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" @click="handleSubmit" :loading="submitLoading">
|
||||
提交
|
||||
</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
|
||||
<!-- 预览对话框 -->
|
||||
<ElDialog v-model="previewVisible" title="预览待清理数据" width="700px">
|
||||
<ElTable :data="previewData" border>
|
||||
<ElTableColumn prop="table_name" label="表名" width="150" />
|
||||
<ElTableColumn prop="description" label="说明" min-width="180" />
|
||||
<ElTableColumn prop="retention_days" label="保留天数" width="100" align="center" />
|
||||
<ElTableColumn prop="record_count" label="待清理记录数" width="130" align="right">
|
||||
<template #default="{ row }">
|
||||
<span :class="{ 'text-danger': row.record_count > 10000 }">
|
||||
{{ row.record_count.toLocaleString() }}
|
||||
</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</ElTable>
|
||||
<template #footer>
|
||||
<ElButton @click="previewVisible = false">关闭</ElButton>
|
||||
</template>
|
||||
</ElDialog>
|
||||
|
||||
<!-- 清理日志对话框 -->
|
||||
<ElDialog v-model="logVisible" title="清理日志" width="1000px">
|
||||
<div style="margin-bottom: 16px">
|
||||
<ElInput
|
||||
v-model="logFilter.table_name"
|
||||
placeholder="表名筛选"
|
||||
clearable
|
||||
style="width: 200px; margin-right: 10px"
|
||||
@clear="getCleanupLogs"
|
||||
/>
|
||||
<ElButton type="primary" @click="getCleanupLogs">查询</ElButton>
|
||||
</div>
|
||||
<ElTable :data="logList" border>
|
||||
<ElTableColumn prop="table_name" label="表名" width="120" />
|
||||
<ElTableColumn prop="cleanup_type" label="清理类型" width="100">
|
||||
<template #default="{ row }">
|
||||
<ElTag :type="row.cleanup_type === 'scheduled' ? 'success' : 'warning'">
|
||||
{{ row.cleanup_type === 'scheduled' ? '定时' : '手动' }}
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn prop="status" label="状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<ElTag
|
||||
:type="
|
||||
row.status === 'success'
|
||||
? 'success'
|
||||
: row.status === 'running'
|
||||
? 'info'
|
||||
: 'danger'
|
||||
"
|
||||
>
|
||||
{{
|
||||
row.status === 'success' ? '成功' : row.status === 'running' ? '运行中' : '失败'
|
||||
}}
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn prop="retention_days" label="保留天数" width="100" align="center" />
|
||||
<ElTableColumn prop="deleted_count" label="删除记录数" width="110" align="right">
|
||||
<template #default="{ row }">
|
||||
{{ row.deleted_count.toLocaleString() }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn prop="duration_ms" label="耗时(ms)" width="100" align="right" />
|
||||
<ElTableColumn prop="started_at" label="开始时间" width="160">
|
||||
<template #default="{ row }">
|
||||
{{ formatDateTime(row.started_at) }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn
|
||||
prop="error_message"
|
||||
label="错误信息"
|
||||
min-width="150"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
</ElTable>
|
||||
<ElPagination
|
||||
v-model:current-page="logPagination.page"
|
||||
v-model:page-size="logPagination.page_size"
|
||||
:total="logPagination.total"
|
||||
:page-sizes="[10, 20, 50]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
style="margin-top: 16px; justify-content: flex-end"
|
||||
@size-change="getCleanupLogs"
|
||||
@current-change="getCleanupLogs"
|
||||
/>
|
||||
<template #footer>
|
||||
<ElButton @click="logVisible = false">关闭</ElButton>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</ElCard>
|
||||
</div>
|
||||
</ArtTableFullScreen>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { h } from 'vue'
|
||||
import { DataCleanupService } from '@/api/modules'
|
||||
import { ElMessage, ElTag, ElSwitch, ElMessageBox } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import type {
|
||||
DataCleanupConfig,
|
||||
CreateDataCleanupConfigRequest,
|
||||
UpdateDataCleanupConfigRequest,
|
||||
DataCleanupLog,
|
||||
DataCleanupPreviewItem,
|
||||
DataCleanupProgress
|
||||
} from '@/types/api'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import { useTableContextMenu } from '@/composables/useTableContextMenu'
|
||||
import ArtMenuRight from '@/components/core/others/ArtMenuRight.vue'
|
||||
import TableContextMenuHint from '@/components/core/others/TableContextMenuHint.vue'
|
||||
import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
|
||||
defineOptions({ name: 'DataCleanup' })
|
||||
|
||||
const loading = ref(false)
|
||||
const submitLoading = ref(false)
|
||||
const triggering = ref(false)
|
||||
const tableRef = ref()
|
||||
const dialogVisible = ref(false)
|
||||
const previewVisible = ref(false)
|
||||
const logVisible = ref(false)
|
||||
const dialogType = ref<'add' | 'edit'>('add')
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
// 右键菜单
|
||||
const contextMenuRef = ref<InstanceType<typeof ArtMenuRight>>()
|
||||
const currentRow = ref<DataCleanupConfig | null>(null)
|
||||
|
||||
// 列配置
|
||||
const columnOptions = [
|
||||
{ label: '表名', prop: 'table_name' },
|
||||
{ label: '配置说明', prop: 'description' },
|
||||
{ label: '保留天数', prop: 'retention_days' },
|
||||
{ label: '每批删除条数', prop: 'batch_size' },
|
||||
{ label: '是否启用', prop: 'enabled' },
|
||||
{ label: '创建时间', prop: 'created_at' },
|
||||
{ label: '更新时间', prop: 'updated_at' }
|
||||
]
|
||||
|
||||
// 验证规则
|
||||
const rules: FormRules = {
|
||||
table_name: [{ required: true, message: '请输入表名', trigger: 'blur' }],
|
||||
retention_days: [
|
||||
{ required: true, message: '请输入保留天数', trigger: 'blur' },
|
||||
{ type: 'number', min: 7, message: '保留天数最少7天', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
const form = reactive({
|
||||
id: 0,
|
||||
table_name: '',
|
||||
description: '',
|
||||
retention_days: 30,
|
||||
batch_size: 10000,
|
||||
enabled: 1
|
||||
})
|
||||
|
||||
// 重置表单
|
||||
const resetForm = () => {
|
||||
form.id = 0
|
||||
form.table_name = ''
|
||||
form.description = ''
|
||||
form.retention_days = 30
|
||||
form.batch_size = 10000
|
||||
form.enabled = 1
|
||||
}
|
||||
|
||||
const configList = ref<DataCleanupConfig[]>([])
|
||||
const previewData = ref<DataCleanupPreviewItem[]>([])
|
||||
const logList = ref<DataCleanupLog[]>([])
|
||||
const progress = ref<DataCleanupProgress | null>(null)
|
||||
|
||||
// 日志筛选和分页
|
||||
const logFilter = reactive({
|
||||
table_name: ''
|
||||
})
|
||||
|
||||
const logPagination = reactive({
|
||||
page: 1,
|
||||
page_size: 10,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 动态列配置
|
||||
const { columnChecks, columns } = useCheckedColumns(() => [
|
||||
{
|
||||
prop: 'table_name',
|
||||
label: '表名',
|
||||
width: 200,
|
||||
fixed: 'left'
|
||||
},
|
||||
{
|
||||
prop: 'description',
|
||||
label: '配置说明',
|
||||
minWidth: 180,
|
||||
showOverflowTooltip: true
|
||||
},
|
||||
{
|
||||
prop: 'retention_days',
|
||||
label: '保留天数',
|
||||
width: 100,
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
prop: 'batch_size',
|
||||
label: '每批删除条数',
|
||||
width: 130,
|
||||
align: 'right',
|
||||
formatter: (row: DataCleanupConfig) => row.batch_size.toLocaleString()
|
||||
},
|
||||
{
|
||||
prop: 'enabled',
|
||||
label: '是否启用',
|
||||
width: 100,
|
||||
formatter: (row: DataCleanupConfig) => {
|
||||
return h(ElTag, { type: row.enabled === 1 ? 'success' : 'info' }, () =>
|
||||
row.enabled === 1 ? '启用' : '禁用'
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'created_at',
|
||||
label: '创建时间',
|
||||
width: 180,
|
||||
formatter: (row: DataCleanupConfig) => formatDateTime(row.created_at)
|
||||
},
|
||||
{
|
||||
prop: 'updated_at',
|
||||
label: '更新时间',
|
||||
width: 180,
|
||||
formatter: (row: DataCleanupConfig) => formatDateTime(row.updated_at)
|
||||
}
|
||||
])
|
||||
|
||||
onMounted(() => {
|
||||
getTableData()
|
||||
getProgress()
|
||||
// 每30秒刷新一次进度
|
||||
setInterval(getProgress, 30000)
|
||||
})
|
||||
|
||||
// 获取配置列表
|
||||
const getTableData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await DataCleanupService.getDataCleanupConfigs()
|
||||
if (res.code === 0) {
|
||||
configList.value = res.data.items || []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 获取进度
|
||||
const getProgress = async () => {
|
||||
try {
|
||||
const res = await DataCleanupService.getDataCleanupProgress()
|
||||
if (res.code === 0) {
|
||||
progress.value = res.data
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新表格
|
||||
const handleRefresh = () => {
|
||||
getTableData()
|
||||
getProgress()
|
||||
}
|
||||
|
||||
// 显示创建对话框
|
||||
const showCreateDialog = () => {
|
||||
resetForm()
|
||||
dialogType.value = 'add'
|
||||
dialogVisible.value = true
|
||||
nextTick(() => {
|
||||
formRef.value?.clearValidate()
|
||||
})
|
||||
}
|
||||
|
||||
// 显示编辑对话框
|
||||
const showEditDialog = async (row: DataCleanupConfig) => {
|
||||
try {
|
||||
// 先调用详情接口获取完整数据
|
||||
const res = await DataCleanupService.getDataCleanupConfigById(row.id)
|
||||
if (res.code === 0 && res.data) {
|
||||
Object.assign(form, {
|
||||
id: res.data.id,
|
||||
table_name: res.data.table_name,
|
||||
description: res.data.description || '',
|
||||
retention_days: res.data.retention_days,
|
||||
batch_size: res.data.batch_size,
|
||||
enabled: res.data.enabled
|
||||
})
|
||||
dialogType.value = 'edit'
|
||||
dialogVisible.value = true
|
||||
nextTick(() => {
|
||||
formRef.value?.clearValidate()
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取详情失败:', error)
|
||||
ElMessage.error('获取配置详情失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 对话框关闭
|
||||
const handleDialogClosed = () => {
|
||||
// 对话框关闭后不做任何操作
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
await formRef.value.validate(async (valid) => {
|
||||
if (valid) {
|
||||
submitLoading.value = true
|
||||
try {
|
||||
if (dialogType.value === 'add') {
|
||||
const data: CreateDataCleanupConfigRequest = {
|
||||
table_name: form.table_name,
|
||||
retention_days: form.retention_days,
|
||||
batch_size: form.batch_size || undefined,
|
||||
description: form.description || undefined
|
||||
}
|
||||
await DataCleanupService.createDataCleanupConfig(data)
|
||||
ElMessage.success('创建成功')
|
||||
} else {
|
||||
const data: UpdateDataCleanupConfigRequest = {
|
||||
retention_days: form.retention_days,
|
||||
batch_size: form.batch_size,
|
||||
description: form.description || undefined,
|
||||
enabled: form.enabled
|
||||
}
|
||||
await DataCleanupService.updateDataCleanupConfig(form.id, data)
|
||||
ElMessage.success('更新成功')
|
||||
}
|
||||
|
||||
dialogVisible.value = false
|
||||
await getTableData()
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 删除配置
|
||||
const handleDelete = async (row: DataCleanupConfig) => {
|
||||
try {
|
||||
await ElMessageBox.confirm(`确定要删除配置"${row.table_name}"吗?`, '删除确认', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
|
||||
await DataCleanupService.deleteDataCleanupConfig(row.id)
|
||||
ElMessage.success('删除成功')
|
||||
await getTableData()
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 预览清理
|
||||
const handlePreview = async () => {
|
||||
try {
|
||||
const res = await DataCleanupService.previewDataCleanup()
|
||||
if (res.code === 0) {
|
||||
previewData.value = res.data.items || []
|
||||
previewVisible.value = true
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 手动触发清理
|
||||
const handleTriggerCleanup = async () => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要手动触发数据清理吗?', '确认', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
|
||||
triggering.value = true
|
||||
await DataCleanupService.triggerDataCleanup()
|
||||
ElMessage.success('已触发清理任务')
|
||||
await getProgress()
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
console.error(error)
|
||||
}
|
||||
} finally {
|
||||
triggering.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 查看日志
|
||||
const handleViewLogs = () => {
|
||||
logFilter.table_name = ''
|
||||
logPagination.page = 1
|
||||
logVisible.value = true
|
||||
getCleanupLogs()
|
||||
}
|
||||
|
||||
// 获取清理日志
|
||||
const getCleanupLogs = async () => {
|
||||
try {
|
||||
const res = await DataCleanupService.getDataCleanupLogs({
|
||||
table_name: logFilter.table_name || undefined,
|
||||
page: logPagination.page,
|
||||
page_size: logPagination.page_size
|
||||
})
|
||||
if (res.code === 0) {
|
||||
logList.value = res.data.items || []
|
||||
logPagination.total = res.data.total || 0
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 右键菜单项
|
||||
const contextMenuItems = computed((): MenuItemType[] => {
|
||||
return [
|
||||
{ key: 'edit', label: '编辑' },
|
||||
{ key: 'logs', label: '查看日志' },
|
||||
{ key: 'delete', label: '删除' }
|
||||
]
|
||||
})
|
||||
|
||||
// 处理表格行右键菜单
|
||||
const handleRowContextMenu = (row: DataCleanupConfig, column: any, event: MouseEvent) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
currentRow.value = row
|
||||
contextMenuRef.value?.show(event)
|
||||
}
|
||||
|
||||
// 处理右键菜单选择
|
||||
const handleContextMenuSelect = (item: MenuItemType) => {
|
||||
if (!currentRow.value) return
|
||||
|
||||
switch (item.key) {
|
||||
case 'edit':
|
||||
showEditDialog(currentRow.value)
|
||||
break
|
||||
case 'logs':
|
||||
logFilter.table_name = currentRow.value.table_name
|
||||
logPagination.page = 1
|
||||
logVisible.value = true
|
||||
getCleanupLogs()
|
||||
break
|
||||
case 'delete':
|
||||
handleDelete(currentRow.value)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 使用表格右键菜单功能
|
||||
const {
|
||||
showContextMenuHint,
|
||||
hintPosition,
|
||||
getRowClassName,
|
||||
handleCellMouseEnter,
|
||||
handleCellMouseLeave
|
||||
} = useTableContextMenu()
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.data-cleanup-page {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
:deep(.el-table__row.table-row-with-context-menu) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.text-danger {
|
||||
color: var(--el-color-danger);
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user