新增: 轮询管理
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 5m17s

This commit is contained in:
sexygoat
2026-03-30 14:59:56 +08:00
parent dbe6070207
commit a05717c2ed
28 changed files with 6865 additions and 103 deletions

View 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>