mirror of
https://github.com/Awuqing/BackupX.git
synced 2026-07-09 22:44:20 +08:00
feat(backup): 备份保留锁定 / 法律保留(豁免清理删除 + 记录页锁定) (#84)
新增保留锁定:锁定的备份豁免保留期清理与手动删除(迁移基线/合规快照/取证)。model+Locked、retention 剔除锁定记录、DeleteRecord 拒绝删除、PUT /backup/records/:id/lock、记录页锁定/解锁操作与标识。go test、tsc+vite、运行时路由验证均通过。
This commit is contained in:
@@ -2,7 +2,7 @@ import { Button, Card, Empty, Message, Select, Space, Table, Tag, Typography } f
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { useSearchParams } from 'react-router-dom'
|
||||
import { BackupRecordLogDrawer } from '../../components/backup-records/BackupRecordLogDrawer'
|
||||
import { listBackupRecords } from '../../services/backup-records'
|
||||
import { listBackupRecords, setBackupRecordLock } from '../../services/backup-records'
|
||||
import { listBackupTasks } from '../../services/backup-tasks'
|
||||
import type { BackupRecordStatus, BackupRecordSummary } from '../../types/backup-records'
|
||||
import type { BackupTaskSummary } from '../../types/backup-tasks'
|
||||
@@ -33,6 +33,7 @@ export function BackupRecordsPage() {
|
||||
const [tasks, setTasks] = useState<BackupTaskSummary[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState('')
|
||||
const [lockBusyId, setLockBusyId] = useState<number | null>(null)
|
||||
|
||||
const selectedTaskId = Number(searchParams.get('taskId') ?? 0) || undefined
|
||||
const selectedRecordId = Number(searchParams.get('recordId') ?? 0) || undefined
|
||||
@@ -64,6 +65,19 @@ export function BackupRecordsPage() {
|
||||
void loadData()
|
||||
}, [loadData])
|
||||
|
||||
async function handleToggleLock(record: BackupRecordSummary) {
|
||||
setLockBusyId(record.id)
|
||||
try {
|
||||
await setBackupRecordLock(record.id, !record.locked)
|
||||
Message.success(record.locked ? '已解除保留锁定' : '已锁定:该备份将豁免保留清理与删除')
|
||||
await loadData()
|
||||
} catch (e) {
|
||||
Message.error(resolveErrorMessage(e, '操作失败'))
|
||||
} finally {
|
||||
setLockBusyId(null)
|
||||
}
|
||||
}
|
||||
|
||||
function updateSearchParam(key: 'taskId' | 'status' | 'recordId', value?: string) {
|
||||
const nextParams = new URLSearchParams(searchParams)
|
||||
if (!value || value === '0') {
|
||||
@@ -96,7 +110,10 @@ export function BackupRecordsPage() {
|
||||
dataIndex: 'fileName',
|
||||
render: (_: unknown, record: BackupRecordSummary) => (
|
||||
<Space direction="vertical" size={2}>
|
||||
<Typography.Text>{record.fileName || '-'}</Typography.Text>
|
||||
<Space size={4}>
|
||||
<Typography.Text>{record.fileName || '-'}</Typography.Text>
|
||||
{record.locked && <Tag color="orange" size="small" bordered>已锁定</Tag>}
|
||||
</Space>
|
||||
<Typography.Text type="secondary">{formatBytes(record.fileSize)}</Typography.Text>
|
||||
{record.checksum && (
|
||||
<Typography.Text type="secondary" copyable style={{ fontSize: 11 }}>
|
||||
@@ -129,11 +146,24 @@ export function BackupRecordsPage() {
|
||||
{
|
||||
title: '操作',
|
||||
dataIndex: 'actions',
|
||||
width: 120,
|
||||
width: 180,
|
||||
render: (_: unknown, record: BackupRecordSummary) => (
|
||||
<Button size="small" type="text" onClick={() => updateSearchParam('recordId', String(record.id))}>
|
||||
查看日志
|
||||
</Button>
|
||||
<Space size={4}>
|
||||
<Button size="small" type="text" onClick={() => updateSearchParam('recordId', String(record.id))}>
|
||||
查看日志
|
||||
</Button>
|
||||
{record.status === 'success' && (
|
||||
<Button
|
||||
size="small"
|
||||
type="text"
|
||||
status={record.locked ? 'warning' : 'default'}
|
||||
loading={lockBusyId === record.id}
|
||||
onClick={() => void handleToggleLock(record)}
|
||||
>
|
||||
{record.locked ? '解锁' : '锁定'}
|
||||
</Button>
|
||||
)}
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
@@ -89,6 +89,12 @@ export async function deleteBackupRecord(id: number) {
|
||||
return unwrapApiEnvelope(response.data)
|
||||
}
|
||||
|
||||
// setBackupRecordLock 设置/解除备份记录的保留锁定(法律保留)。
|
||||
export async function setBackupRecordLock(id: number, locked: boolean) {
|
||||
const response = await http.put<ApiEnvelope<BackupRecordDetail>>(`/backup/records/${id}/lock`, { locked })
|
||||
return unwrapApiEnvelope(response.data)
|
||||
}
|
||||
|
||||
export function streamBackupRecordLogs(recordId: number, handlers: RecordLogStreamHandlers) {
|
||||
const controller = new AbortController()
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ export interface BackupRecordSummary {
|
||||
errorMessage: string
|
||||
startedAt: string
|
||||
completedAt?: string
|
||||
locked: boolean
|
||||
}
|
||||
|
||||
export interface StorageUploadResultItem {
|
||||
|
||||
Reference in New Issue
Block a user