Files
rag-local/ui-src/src/views/DatasetDetail.vue
T
2026-08-05 13:39:37 +08:00

284 lines
8.7 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
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="dd-page">
<div class="dd-head">
<el-button link @click="$router.push('/datasets')">
<el-icon><ArrowLeft /></el-icon> 返回
</el-button>
<span class="dd-title">{{ datasetName }}</span>
</div>
<el-upload class="dd-upload" drag :show-file-list="false" :http-request="doUpload" :disabled="uploading"
accept=".txt,.md,.pdf,.docx,.doc,.html,.htm">
<el-icon class="el-icon--upload"><UploadFilled /></el-icon>
<div class="el-upload__text">拖拽文件到此处 <em>点击上传</em>txt/md/pdf/docx/html</div>
</el-upload>
<el-table :data="documents" v-loading="loading" empty-text="暂无文档">
<el-table-column prop="filename" label="文件名" min-width="200" show-overflow-tooltip />
<el-table-column label="大小" width="100">
<template #default="{ row }">{{ formatSize(row.file_size) }}</template>
</el-table-column>
<el-table-column label="状态" width="120">
<template #default="{ row }">
<el-tooltip :content="row.error_msg || ''" placement="top">
<el-tag :type="statusType(row.status)">{{ statusText(row.status) }}</el-tag>
</el-tooltip>
</template>
</el-table-column>
<el-table-column prop="chunk_count" label="分块数" width="90" />
<el-table-column prop="created_at" label="上传时间" width="170" />
<el-table-column label="操作" width="240" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="openChunks(row)">分块</el-button>
<el-button v-if="row.status === 3" link type="warning" @click="retry(row)">重试</el-button>
<el-button link type="primary" @click="reembed(row)">重新向量化</el-button>
<el-button link type="danger" @click="remove(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination class="dd-page-bar" layout="total, prev, pager, next" :total="total"
:page-size="pageSize" v-model:current-page="page" @current-change="load" />
<el-drawer v-model="chunkDrawer" :title="chunkDrawerTitle" size="55%">
<div class="chunk-toolbar">
<el-input v-model="chunkKeyword" placeholder="搜索分块内容" clearable style="width: 240px" @change="loadChunks" />
</div>
<el-table :data="chunks" v-loading="chunkLoading" size="small">
<el-table-column type="index" label="#" width="50" />
<el-table-column label="内容" min-width="300">
<template #default="{ row }">
<div class="chunk-content">{{ row.content }}</div>
</template>
</el-table-column>
<el-table-column label="操作" width="110" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="editChunk(row)">编辑</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination class="dd-page-bar" layout="total, prev, pager, next" :total="chunkTotal"
:page-size="chunkPageSize" v-model:current-page="chunkPage" @current-change="loadChunks" />
</el-drawer>
<el-dialog v-model="editDialog" title="编辑分块" width="640px">
<el-input v-model="editContent" type="textarea" :rows="10" />
<div class="edit-tip">保存后重新分词并向量化</div>
<template #footer>
<el-button @click="editDialog = false">取消</el-button>
<el-button type="primary" :loading="editSaving" @click="saveChunk">保存</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { computed, onMounted, onUnmounted, ref } from 'vue'
import { useRoute } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import { ArrowLeft, UploadFilled } from '@element-plus/icons-vue'
import { listDatasets } from '../api/dataset.js'
import { listDocuments, uploadDocument, deleteDocument, reembedDocument, retryParseTask } from '../api/document.js'
import { listChunks, updateChunk } from '../api/chunk.js'
const route = useRoute()
const datasetId = computed(() => Number(route.params.id))
const datasetName = ref('')
const documents = ref([])
const total = ref(0)
const page = ref(1)
const pageSize = 20
const loading = ref(false)
const uploading = ref(false)
const chunkDrawer = ref(false)
const chunkDrawerTitle = ref('')
const chunks = ref([])
const chunkTotal = ref(0)
const chunkPage = ref(1)
const chunkPageSize = 20
const chunkLoading = ref(false)
const chunkKeyword = ref('')
let currentDoc = null
const editDialog = ref(false)
const editContent = ref('')
const editSaving = ref(false)
let editingChunk = null
const statusMap = {
0: { text: '待处理', type: 'info' },
1: { text: '解析中', type: 'warning' },
2: { text: '已完成', type: 'success' },
3: { text: '失败', type: 'danger' }
}
function statusText(s) { return (statusMap[s] || {}).text || s }
function statusType(s) { return (statusMap[s] || {}).type || 'info' }
function formatSize(bytes) {
if (!bytes) return '-'
if (bytes < 1024) return bytes + ' B'
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'
return (bytes / 1024 / 1024).toFixed(1) + ' MB'
}
async function load() {
loading.value = true
try {
const d = await listDocuments({ dataset_id: datasetId.value, page: page.value, page_size: pageSize })
documents.value = d.list || []
total.value = d.total || 0
} finally {
loading.value = false
}
}
async function doUpload({ file }) {
uploading.value = true
try {
const formData = new FormData()
formData.append('dataset_id', datasetId.value)
formData.append('file', file)
await uploadDocument(formData)
ElMessage.success('上传成功,正在解析')
await load()
startPolling()
} finally {
uploading.value = false
}
}
// 有待处理/解析中的文档时轮询刷新状态,全部完成即停止
let pollTimer = null
async function startPolling() {
stopPolling()
pollTimer = setInterval(async () => {
await load()
if (!documents.value.some(d => d.status === 0 || d.status === 1)) stopPolling()
}, 3000)
}
function stopPolling() {
if (pollTimer) {
clearInterval(pollTimer)
pollTimer = null
}
}
onMounted(async () => {
try {
const dsRes = await listDatasets()
const ds = dsRes && dsRes.list ? dsRes.list : []
const cur = ds.find(x => x.id === datasetId.value)
datasetName.value = cur ? cur.name : '数据集'
} catch { /* 忽略 */ }
await load()
if (documents.value.some(d => d.status === 0 || d.status === 1)) startPolling()
})
onUnmounted(stopPolling)
async function remove(row) {
try {
await ElMessageBox.confirm(`删除文档「${row.filename}」?将同时删除分块、向量与全文索引。`, '删除确认', { type: 'warning' })
} catch {
return
}
await deleteDocument(row.id)
ElMessage.success('已删除')
await load()
}
async function retry(row) {
await retryParseTask(row.id)
ElMessage.success('已重新入队')
await load()
}
async function reembed(row) {
try {
await ElMessageBox.confirm(`重新向量化「${row.filename}」的全部分块?`, '确认', { type: 'info' })
} catch {
return
}
await reembedDocument(row.id)
ElMessage.success('重新向量化完成')
}
async function openChunks(row) {
currentDoc = row
chunkDrawerTitle.value = `分块列表 - ${row.filename}`
chunkDrawer.value = true
chunkPage.value = 1
chunkKeyword.value = ''
await loadChunks()
}
async function loadChunks() {
if (!currentDoc) return
chunkLoading.value = true
try {
const d = await listChunks({ document_id: currentDoc.id, page: chunkPage.value, page_size: chunkPageSize })
chunks.value = d.list
chunkTotal.value = d.total
} finally {
chunkLoading.value = false
}
}
function editChunk(row) {
editingChunk = row
editContent.value = row.content
editDialog.value = true
}
async function saveChunk() {
if (!editContent.value.trim()) {
ElMessage.warning('内容不能为空')
return
}
editSaving.value = true
try {
await updateChunk({ id: editingChunk.id, content: editContent.value })
ElMessage.success('已保存并重新向量化')
editDialog.value = false
await loadChunks()
} finally {
editSaving.value = false
}
}
</script>
<style scoped>
.dd-head {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 12px;
}
.dd-title {
font-size: 16px;
font-weight: 600;
}
.dd-upload {
margin-bottom: 12px;
}
.dd-page-bar {
margin-top: 10px;
justify-content: flex-end;
}
.chunk-toolbar {
margin-bottom: 10px;
}
.chunk-content {
max-height: 60px;
overflow: hidden;
font-size: 13px;
color: #606266;
line-height: 1.6;
}
.edit-tip {
margin-top: 6px;
font-size: 12px;
color: #909399;
}
</style>