回收站,软删除(三天自动清空),以及默认链接三十分钟失效

This commit is contained in:
amb
2026-09-02 18:59:42 +08:00
parent bfb7a1a8a2
commit ff3e2a7273
16 changed files with 797 additions and 38 deletions
+5
View File
@@ -61,6 +61,10 @@ function navigateTo(path: string) {
<el-icon><svg viewBox="0 0 24 24" fill="currentColor"><path d="M19.14 12.94c.04-.3.06-.61.06-.94 0-.32-.02-.64-.07-.94l2.03-1.58c.18-.14.23-.41.12-.61l-1.92-3.32c-.12-.22-.37-.29-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54c-.04-.24-.24-.41-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.05.3-.07.62-.07.94s.02.64.07.94l-2.03 1.58c-.18.14-.23.41-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z"/></svg></el-icon>
<template #title>设置</template>
</el-menu-item>
<el-menu-item index="/recycle-bin">
<el-icon><svg viewBox="0 0 24 24" fill="currentColor"><path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/></svg></el-icon>
<template #title>回收站</template>
</el-menu-item>
</el-menu>
</el-aside>
@@ -89,6 +93,7 @@ function navigateTo(path: string) {
<div style="padding: 12px 0; cursor: pointer; font-size: 16px" @click="navigateTo('/')">📊 仪表盘</div>
<div style="padding: 12px 0; cursor: pointer; font-size: 16px" @click="navigateTo('/knowledge-bases')">📚 知识库</div>
<div style="padding: 12px 0; cursor: pointer; font-size: 16px" @click="navigateTo('/settings')"> 设置</div>
<div style="padding: 12px 0; cursor: pointer; font-size: 16px" @click="navigateTo('/recycle-bin')">🗑 回收站</div>
</div>
</div>
+5
View File
@@ -37,6 +37,11 @@ const router = createRouter({
name: 'KbDetail',
component: () => import('@/views/KbDetail.vue'),
},
{
path: 'recycle-bin',
name: 'RecycleBin',
component: () => import('@/views/RecycleBin.vue'),
},
{
path: 'settings',
name: 'Settings',
+198
View File
@@ -0,0 +1,198 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import apiClient from '@/api/client'
const data = ref<{
documents: any[]
categories: any[]
knowledge_bases: any[]
retention_days: number
}>({
documents: [],
categories: [],
knowledge_bases: [],
retention_days: 3,
})
const loading = ref(false)
const isMobile = ref(window.innerWidth <= 768)
onMounted(() => {
load()
window.addEventListener('resize', () => {
isMobile.value = window.innerWidth <= 768
})
})
async function load() {
loading.value = true
try {
const { data: d } = await apiClient.get('/recycle-bin')
data.value = d
} catch { /* ignore */ }
loading.value = false
}
async function restoreDoc(item: any) {
try {
await apiClient.post(`/recycle-bin/documents/${item.id}/restore`)
ElMessage.success('文档已恢复。')
load()
} catch { /* ignore */ }
}
async function purgeDoc(item: any) {
try {
await ElMessageBox.confirm(`彻底删除「${item.name}」?文件与记录将不可恢复。`, '彻底删除', { type: 'warning' })
await apiClient.delete(`/recycle-bin/documents/${item.id}`)
ElMessage.success('已彻底删除。')
load()
} catch { /* ignore */ }
}
async function restoreCategory(item: any) {
try {
await apiClient.post(`/recycle-bin/categories/${item.id}/restore`)
ElMessage.success('目录及同批内容已恢复。')
load()
} catch { /* ignore */ }
}
async function purgeCategory(item: any) {
try {
await ElMessageBox.confirm(`彻底删除目录「${item.name}」及其同批删除的全部内容?`, '彻底删除', { type: 'warning' })
await apiClient.delete(`/recycle-bin/categories/${item.id}`)
ElMessage.success('已彻底删除。')
load()
} catch { /* ignore */ }
}
async function restoreKb(item: any) {
try {
await apiClient.post(`/recycle-bin/knowledge-bases/${item.id}/restore`)
ElMessage.success('知识库已恢复。')
load()
} catch { /* ignore */ }
}
async function purgeKb(item: any) {
try {
await ElMessageBox.confirm(`彻底删除知识库「${item.name}」及其全部文档?不可恢复。`, '彻底删除', { type: 'warning' })
await apiClient.delete(`/recycle-bin/knowledge-bases/${item.id}`)
ElMessage.success('已彻底删除。')
load()
} catch { /* ignore */ }
}
function statusText(item: any) {
if (item.days_left <= 0) return '即将自动清理'
return `${item.days_left} 天后自动清理`
}
function formatSize(bytes: number) {
if (bytes < 1024) return bytes + ' B'
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'
return (bytes / (1024 * 1024)).toFixed(1) + ' MB'
}
</script>
<template>
<div>
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; flex-wrap: wrap; gap: 8px">
<h1 style="margin: 0; font-size: 22px">🗑 回收站</h1>
<span style="color: #999; font-size: 13px">
删除的内容保留 {{ data.retention_days }} 超期自动彻底删除
</span>
</div>
<!-- 文档 -->
<el-card shadow="hover" style="margin-bottom: 16px">
<template #header><span style="font-weight: bold">📄 文档{{ data.documents.length }}</span></template>
<el-empty v-if="!data.documents.length" description="暂无" :image-size="48" />
<div v-for="item in data.documents" :key="item.id" class="rb-item">
<div class="rb-info">
<div class="rb-name">{{ item.name }}</div>
<div class="rb-meta">
所属{{ item.kb_name || '未知知识库' }} · {{ formatSize(item.file_size) }} ·
删除于 {{ item.deleted_at }} ·
<span :style="{ color: item.days_left <= 1 ? '#f56c6c' : '#999' }">{{ statusText(item) }}</span>
</div>
</div>
<div class="rb-actions">
<el-button size="small" type="primary" @click="restoreDoc(item)">恢复</el-button>
<el-button size="small" type="danger" @click="purgeDoc(item)">彻底删除</el-button>
</div>
</div>
</el-card>
<!-- 目录 -->
<el-card shadow="hover" style="margin-bottom: 16px">
<template #header><span style="font-weight: bold">📁 目录{{ data.categories.length }}</span></template>
<el-empty v-if="!data.categories.length" description="暂无" :image-size="48" />
<div v-for="item in data.categories" :key="item.id" class="rb-item">
<div class="rb-info">
<div class="rb-name">{{ item.name }}</div>
<div class="rb-meta">
所属{{ item.kb_name || '未知知识库' }} · 删除于 {{ item.deleted_at }} ·
<span :style="{ color: item.days_left <= 1 ? '#f56c6c' : '#999' }">{{ statusText(item) }}</span>
</div>
</div>
<div class="rb-actions">
<el-button size="small" type="primary" @click="restoreCategory(item)">恢复</el-button>
<el-button size="small" type="danger" @click="purgeCategory(item)">彻底删除</el-button>
</div>
</div>
</el-card>
<!-- 知识库 -->
<el-card shadow="hover">
<template #header><span style="font-weight: bold">📚 知识库{{ data.knowledge_bases.length }}</span></template>
<el-empty v-if="!data.knowledge_bases.length" description="暂无" :image-size="48" />
<div v-for="item in data.knowledge_bases" :key="item.id" class="rb-item">
<div class="rb-info">
<div class="rb-name">{{ item.name }}</div>
<div class="rb-meta">
删除于 {{ item.deleted_at }} ·
<span :style="{ color: item.days_left <= 1 ? '#f56c6c' : '#999' }">{{ statusText(item) }}</span>
</div>
</div>
<div class="rb-actions">
<el-button size="small" type="primary" @click="restoreKb(item)">恢复</el-button>
<el-button size="small" type="danger" @click="purgeKb(item)">彻底删除</el-button>
</div>
</div>
</el-card>
</div>
</template>
<style scoped>
.rb-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 0;
border-bottom: 1px solid #f0f0f0;
gap: 12px;
}
.rb-item:last-child {
border-bottom: none;
}
.rb-name {
font-weight: 500;
font-size: 14px;
}
.rb-meta {
color: #999;
font-size: 12px;
margin-top: 4px;
}
.rb-actions {
display: flex;
gap: 8px;
flex-shrink: 0;
}
</style>