Files
CloudSearch/source_clean/frontend-src/src/pages/admin/Cleanup.vue

356 lines
13 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="cleanup-section">
<el-card class="config-card">
<template #header><span>🧹 存储清理</span></template>
<div class="cleanup-grid">
<!-- 列1: 基础清理策略 -->
<div class="cleanup-group">
<div class="cleanup-group-label"> 基础清理策略</div>
<el-form label-width="120px" label-position="left" size="small">
<el-form-item label="自动清理">
<div style="display: flex; align-items: center; gap: 10px; flex-wrap: wrap;">
<el-switch v-model="cleanupEnabled" size="small" />
<span class="cleanup-hint">每天自动检查一次删除过期日志移入回收站文件</span>
</div>
</el-form-item>
<el-form-item label="清空回收站">
<div style="display: flex; align-items: center; gap: 10px;">
<el-switch v-model="cleanupEmptyTrash" size="small" />
<span class="cleanup-hint">清理时一并清空各网盘回收站</span>
</div>
</el-form-item>
<el-form-item label="白名单目录">
<div style="width: 100%;">
<div style="display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 6px;" v-if="whitelistDirs.length">
<el-tag v-for="(dir, i) in whitelistDirs" :key="i" closable size="small" @close="removeWhitelistDir(i)">{{ dir }}</el-tag>
</div>
<div style="display: flex; gap: 6px;">
<el-input v-model="newWhitelistDir" placeholder="输入目录名" size="small" style="width: 160px" @keyup.enter="addWhitelistDir" />
<el-button type="primary" size="small" @click="addWhitelistDir">添加</el-button>
</div>
</div>
</el-form-item>
</el-form>
</div>
<!-- 列2: 📦 保留设置 -->
<div class="cleanup-group">
<div class="cleanup-group-label">📦 保留设置</div>
<el-form label-width="120px" label-position="left" size="small">
<el-form-item label="文件保留">
<div style="display: flex; align-items: center; gap: 8px;">
<el-input-number v-model="cleanupFileRetentionDays" :min="1" :max="365" style="width: 100px" size="small" />
<span></span>
</div>
</el-form-item>
<el-form-item label="日志保留">
<div style="display: flex; align-items: center; gap: 8px;">
<el-input-number v-model="cleanupLogRetentionDays" :min="1" :max="365" style="width: 100px" size="small" />
<span></span>
</div>
</el-form-item>
<el-form-item label="Cookie检测">
<div style="display: flex; align-items: center; gap: 8px;">
<el-input-number v-model="verifyIntervalMinutes" :min="5" :max="1440" :step="5" style="width: 100px" size="small" />
<span>分钟</span>
</div>
</el-form-item>
<el-form-item label="空间校准">
<div style="display: flex; align-items: center; gap: 8px;">
<el-input-number v-model="storageRefreshIntervalMinutes" :min="5" :max="1440" :step="5" style="width: 100px" size="small" />
<span>分钟</span>
</div>
</el-form-item>
</el-form>
</div>
<!-- 列3: 📊 空间阈值自动清理 -->
<div class="cleanup-group">
<div class="cleanup-group-label">📊 空间阈值自动清理</div>
<el-form label-width="120px" label-position="left" size="small">
<el-form-item label="启用">
<el-switch v-model="cleanupSpaceThresholdEnabled" size="small" />
<span class="cleanup-hint" style="margin-left: 8px;">已用空间超过阈值时按比例删除最旧的转存文件</span>
</el-form-item>
<el-form-item v-if="cleanupSpaceThresholdEnabled" label="使用阈值">
<el-slider v-model="cleanupSpaceThresholdPercent" :min="50" :max="99" style="width: 140px" show-input size="small" />
</el-form-item>
<el-form-item v-if="cleanupSpaceThresholdEnabled" label="删除比例">
<el-slider v-model="cleanupSpaceThresholdDeletePercent" :min="5" :max="50" :step="5" style="width: 140px" show-input size="small" />
</el-form-item>
</el-form>
</div>
<!-- 列4: 🔗 分享链接复用 -->
<div class="cleanup-group">
<div class="cleanup-group-label">🔗 分享链接复用</div>
<el-form label-width="120px" label-position="left" size="small">
<el-form-item label="复用">
<el-switch v-model="saveReuseEnabled" size="small" />
<span class="cleanup-hint" style="margin-left: 8px;">相同原始链接不再重复转存复用已有分享链接会自动验证原链接有效性60秒内重复请求直接返回已有链接</span>
</el-form-item>
</el-form>
</div>
</div>
<!-- 底部手动操作跨列全宽 -->
<div class="cleanup-actions">
<div class="cleanup-actions-buttons">
<el-button type="primary" :loading="cleanupSaving" @click="handleSaveCleanupConfigs">💾 保存清理配置</el-button>
<el-button type="danger" :loading="cleanupRunning" @click="handleRunCleanup">{{ cleanupRunning ? '清理中...' : '🗑 立即清理' }}</el-button>
<el-button type="warning" :loading="emptyTrashRunning" @click="handleEmptyTrash">{{ emptyTrashRunning ? '清空中...' : '🧹 清空回收站' }}</el-button>
</div>
<div v-if="lastCleanupTime" class="cleanup-info">
上次清理{{ lastCleanupTime }}
<span v-if="lastCleanupStats" style="margin-left: 12px;">📊 {{ lastCleanupStats }}</span>
</div>
</div>
</el-card>
</div>
</template>
<script setup lang="ts">
import { ref, computed, reactive, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { getSystemConfigs, updateSystemConfigs, runCleanup, emptyAllTrash } from '../../api'
interface ConfigMap {
[key: string]: string | number
}
const sysConfigs = reactive<ConfigMap>({})
const cleanupRunning = ref(false)
const emptyTrashRunning = ref(false)
const cleanupSaving = ref(false)
const lastCleanupTime = computed(() => String(sysConfigs.cleanup_last_run || ''))
const lastCleanupStats = computed(() => {
const raw = String(sysConfigs.cleanup_last_stats || '')
if (!raw) return ''
try {
const s = JSON.parse(raw)
const parts = []
if (s.filesTrashed > 0) parts.push(`移入回收站 ${s.filesTrashed} 个文件夹`)
if (s.logsDeleted > 0) parts.push(`删除 ${s.logsDeleted} 条日志`)
if (s.trashEmptied) parts.push(`已清空回收站`)
if (s.errors > 0) parts.push(`⚠️ ${s.errors} 个错误`)
return parts.join(' / ') || '无操作'
} catch { return '' }
})
const cleanupEnabled = computed({
get: () => String(sysConfigs.cleanup_enabled) === 'true',
set: (val: boolean) => { sysConfigs.cleanup_enabled = val ? 'true' : 'false' },
})
const cleanupEmptyTrash = computed({
get: () => String(sysConfigs.cleanup_empty_trash) !== 'false',
set: (val: boolean) => { sysConfigs.cleanup_empty_trash = val ? 'true' : 'false' },
})
const cleanupFileRetentionDays = computed({
get: () => Number(sysConfigs.cleanup_file_retention_days ?? 7),
set: (val: number) => { sysConfigs.cleanup_file_retention_days = val },
})
const cleanupLogRetentionDays = computed({
get: () => Number(sysConfigs.cleanup_log_retention_days ?? 30),
set: (val: number) => { sysConfigs.cleanup_log_retention_days = val },
})
const cleanupSpaceThresholdEnabled = computed({
get: () => String(sysConfigs.cleanup_space_threshold_enabled) === 'true',
set: (val: boolean) => { sysConfigs.cleanup_space_threshold_enabled = val ? 'true' : 'false' },
})
const cleanupSpaceThresholdPercent = computed({
get: () => Number(sysConfigs.cleanup_space_threshold_percent ?? 90),
set: (val: number) => { sysConfigs.cleanup_space_threshold_percent = val },
})
const cleanupSpaceThresholdDeletePercent = computed({
get: () => Number(sysConfigs.cleanup_space_threshold_delete_percent ?? 10),
set: (val: number) => { sysConfigs.cleanup_space_threshold_delete_percent = val },
})
const saveReuseEnabled = computed({
get: () => String(sysConfigs.save_reuse_enabled) !== 'false',
set: (val: boolean) => { sysConfigs.save_reuse_enabled = val ? 'true' : 'false' },
})
// 白名单
const whitelistDirs = ref<string[]>([])
const newWhitelistDir = ref('')
function loadWhitelistDirs() {
try {
const raw = String(sysConfigs.cleanup_whitelist_dirs || '[]')
whitelistDirs.value = JSON.parse(raw)
} catch {
whitelistDirs.value = []
}
}
function addWhitelistDir() {
const name = newWhitelistDir.value.trim()
if (!name) return
if (whitelistDirs.value.includes(name)) {
ElMessage.warning('该目录已在白名单中')
return
}
whitelistDirs.value.push(name)
newWhitelistDir.value = ''
}
function removeWhitelistDir(index: number) {
whitelistDirs.value.splice(index, 1)
}
// Cookie检测间隔 + 空间校准间隔
const verifyIntervalMinutes = computed({
get: () => Number(sysConfigs.cleanup_verify_interval ?? 30),
set: (val: number) => { sysConfigs.cleanup_verify_interval = val },
})
const storageRefreshIntervalMinutes = computed({
get: () => Number(sysConfigs.storage_refresh_interval ?? 180),
set: (val: number) => { sysConfigs.storage_refresh_interval = val },
})
async function loadCleanupConfigs() {
try {
const raw = await getSystemConfigs()
for (const cfg of raw) {
sysConfigs[cfg.key] = cfg.value
}
loadWhitelistDirs()
} catch (e) {
console.error('加载清理配置失败', e)
}
}
async function handleSaveCleanupConfigs() {
cleanupSaving.value = true
try {
const keys = [
'cleanup_enabled', 'cleanup_file_retention_days', 'cleanup_log_retention_days',
'cleanup_empty_trash',
'cleanup_space_threshold_enabled', 'cleanup_space_threshold_percent', 'cleanup_space_threshold_delete_percent',
'save_reuse_enabled',
'cleanup_verify_interval', 'storage_refresh_interval',
]
const entries = keys.map(key => ({ key, value: String(sysConfigs[key] ?? '') }))
entries.push({ key: 'cleanup_whitelist_dirs', value: JSON.stringify(whitelistDirs.value) })
await updateSystemConfigs(entries)
ElMessage.success('清理配置已保存')
} catch (e: any) {
ElMessage.error(e.response?.data?.error || '保存失败')
} finally {
cleanupSaving.value = false
}
}
async function handleRunCleanup() {
cleanupRunning.value = true
try {
const result = await runCleanup()
if (result.success) {
ElMessage.success(result.message)
} else {
ElMessage.warning(result.message)
}
await loadCleanupConfigs()
} catch (e: any) {
ElMessage.error(e.response?.data?.error || '清理失败')
} finally {
cleanupRunning.value = false
}
}
async function handleEmptyTrash() {
emptyTrashRunning.value = true
try {
const result = await emptyAllTrash()
if (result.success) {
ElMessage.success(result.message)
} else {
ElMessage.warning(result.message)
}
} catch (e: any) {
ElMessage.error(e.response?.data?.error || '清空回收站失败')
} finally {
emptyTrashRunning.value = false
}
}
onMounted(() => {
loadCleanupConfigs()
})
</script>
<style scoped>
.cleanup-section .config-card {
/* 全宽展示没有max-width限制 */
}
.cleanup-section :deep(.el-card__header) {
font-size: 16px;
font-weight: 600;
letter-spacing: 0.5px;
border-bottom: 1px solid var(--border);
}
/* ── 2列网格布局 ── */
.cleanup-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
.cleanup-group {
border: 1px solid var(--border);
border-radius: var(--radius-md);
padding: 16px 18px;
background: var(--bg-card);
transition: box-shadow 0.2s;
}
.cleanup-group:hover {
box-shadow: var(--shadow-sm);
}
.cleanup-group-label {
font-size: 14px;
font-weight: 600;
color: var(--primary);
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 1px dashed var(--border-light);
}
.cleanup-hint {
color: var(--text-tertiary);
font-size: 12px;
line-height: 1.5;
}
/* ── 底部操作栏(跨列全宽) ── */
.cleanup-actions {
margin-top: 20px;
padding: 16px 18px;
border-top: 1px solid var(--border);
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 12px;
}
.cleanup-actions-buttons {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.cleanup-info {
font-size: 13px;
color: var(--text-tertiary);
display: flex;
flex-wrap: wrap;
gap: 4px;
}
/* ── 响应式:窄屏时改为单列 ── */
@media (max-width: 900px) {
.cleanup-grid {
grid-template-columns: 1fr;
}
}
</style>