first-update
This commit is contained in:
470
frontend/src/views/project/TextSplit.vue
Normal file
470
frontend/src/views/project/TextSplit.vue
Normal file
@@ -0,0 +1,470 @@
|
||||
<template>
|
||||
<div class="text-split">
|
||||
<!-- Page Header -->
|
||||
<div class="page-header">
|
||||
<div class="header-left">
|
||||
<h2>文本分割</h2>
|
||||
<p class="header-desc">将文档内容智能分割为文本块</p>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<el-button @click="refreshChunks" class="action-btn">
|
||||
<el-icon><Refresh /></el-icon>
|
||||
刷新
|
||||
</el-button>
|
||||
<el-button type="primary" @click="showSplitDialog = true" class="action-btn">
|
||||
<el-icon><Plus /></el-icon>
|
||||
新建分割
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Split Config Card -->
|
||||
<div class="config-card" v-if="selectedFile">
|
||||
<div class="config-header">
|
||||
<div class="config-title">
|
||||
<div class="file-badge">
|
||||
<el-icon><Document /></el-icon>
|
||||
</div>
|
||||
<span>{{ selectedFile.filename }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="config-form">
|
||||
<div class="form-row">
|
||||
<div class="form-item">
|
||||
<label>分割算法</label>
|
||||
<el-select v-model="splitConfig.method" placeholder="选择算法">
|
||||
<el-option label="Markdown 结构分割" value="markdown_structure" />
|
||||
<el-option label="递归字符分割" value="recursive" />
|
||||
<el-option label="Token 数量分割" value="token" />
|
||||
<el-option label="代码感知分割" value="code" />
|
||||
<el-option label="自定义分隔符" value="custom" />
|
||||
</el-select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row sliders">
|
||||
<div class="form-item">
|
||||
<label>块大小: {{ splitConfig.chunk_size }}</label>
|
||||
<el-slider v-model="splitConfig.chunk_size" :min="100" :max="2000" :step="100" :marks="{100: '100', 500: '500', 1000: '1k', 2000: '2k'}" />
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<label>重叠字符: {{ splitConfig.overlap }}</label>
|
||||
<el-slider v-model="splitConfig.overlap" :min="0" :max="500" :step="50" :marks="{0: '0', 250: '250', 500: '500'}" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-item" v-if="splitConfig.method === 'custom'">
|
||||
<label>自定义分隔符</label>
|
||||
<el-input v-model="splitConfig.separator" placeholder="例如: \n\n 或 || 或 ---" />
|
||||
</div>
|
||||
|
||||
<div class="config-action">
|
||||
<el-button type="primary" size="large" @click="handleSplit" :loading="splitting" class="start-btn">
|
||||
<el-icon><CaretRight /></el-icon>
|
||||
开始分割
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="empty-state">
|
||||
<div class="empty-illustration">
|
||||
<div class="ring"></div>
|
||||
<el-icon size="56"><Document /></el-icon>
|
||||
</div>
|
||||
<h3>未选择文件</h3>
|
||||
<p>请从文件管理中选择一个文件开始分割</p>
|
||||
</div>
|
||||
|
||||
<!-- Chunk List -->
|
||||
<div class="chunks-card" v-if="selectedFile && chunks.length > 0">
|
||||
<div class="chunks-header">
|
||||
<div class="chunks-title">
|
||||
<el-icon><List /></el-icon>
|
||||
<span>文本块 ({{ chunks.length }})</span>
|
||||
</div>
|
||||
<el-tag type="primary" effect="dark">
|
||||
总计 {{ totalWords }} 字
|
||||
</el-tag>
|
||||
</div>
|
||||
|
||||
<div class="chunk-list" v-loading="loading">
|
||||
<div
|
||||
v-for="(chunk, index) in chunks"
|
||||
:key="chunk.id"
|
||||
class="chunk-item"
|
||||
:style="{ '--delay': index * 0.03 + 's' }"
|
||||
>
|
||||
<div class="chunk-header">
|
||||
<div class="chunk-badge">{{ index + 1 }}</div>
|
||||
<span class="chunk-title">{{ chunk.name || '未命名' }}</span>
|
||||
<span class="chunk-meta">{{ chunk.word_count || 0 }} 字</span>
|
||||
</div>
|
||||
<div class="chunk-content">{{ chunk.content }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Select File Dialog -->
|
||||
<el-dialog v-model="showSplitDialog" title="选择文件" width="500px" class="select-dialog">
|
||||
<el-select v-model="selectedFileId" placeholder="选择要分割的文件" style="width: 100%" size="large">
|
||||
<el-option
|
||||
v-for="file in files"
|
||||
:key="file.id"
|
||||
:label="file.filename"
|
||||
:value="file.id"
|
||||
>
|
||||
<div class="file-option">
|
||||
<el-icon><Document /></el-icon>
|
||||
<span>{{ file.filename }}</span>
|
||||
</div>
|
||||
</el-option>
|
||||
</el-select>
|
||||
<template #footer>
|
||||
<el-button @click="showSplitDialog = false">取消</el-button>
|
||||
<el-button type="primary" @click="confirmSelectFile">确认</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { fileApi, chunkApi } from '@/api'
|
||||
|
||||
const route = useRoute()
|
||||
const projectId = computed(() => route.params.id)
|
||||
|
||||
const loading = ref(false)
|
||||
const splitting = ref(false)
|
||||
const files = ref([])
|
||||
const chunks = ref([])
|
||||
const selectedFileId = ref('')
|
||||
const selectedFile = ref(null)
|
||||
const showSplitDialog = ref(false)
|
||||
|
||||
const splitConfig = reactive({
|
||||
method: 'recursive',
|
||||
chunk_size: 500,
|
||||
overlap: 50,
|
||||
separator: '\n\n'
|
||||
})
|
||||
|
||||
const totalWords = computed(() => {
|
||||
return chunks.value.reduce((sum, c) => sum + (c.word_count || 0), 0)
|
||||
})
|
||||
|
||||
const fetchFiles = async () => {
|
||||
try {
|
||||
const res = await fileApi.list(projectId.value)
|
||||
files.value = res.data.files || []
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
const fetchChunks = async () => {
|
||||
if (!selectedFile.value) return
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await chunkApi.list(projectId.value, { file_id: selectedFile.value.id })
|
||||
chunks.value = res.data.chunks || []
|
||||
} catch (error) {
|
||||
ElMessage.error('获取分割结果失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const confirmSelectFile = () => {
|
||||
selectedFile.value = files.value.find(f => f.id === selectedFileId.value)
|
||||
showSplitDialog.value = false
|
||||
fetchChunks()
|
||||
}
|
||||
|
||||
const handleSplit = async () => {
|
||||
if (!selectedFile.value) {
|
||||
ElMessage.warning('请先选择文件')
|
||||
return
|
||||
}
|
||||
splitting.value = true
|
||||
try {
|
||||
await chunkApi.split(projectId.value, { file_id: selectedFile.value.id, ...splitConfig })
|
||||
ElMessage.success('分割任务已启动')
|
||||
fetchChunks()
|
||||
} catch (error) {
|
||||
ElMessage.error('分割失败')
|
||||
} finally {
|
||||
splitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const refreshChunks = () => fetchChunks()
|
||||
|
||||
onMounted(() => {
|
||||
fetchFiles()
|
||||
const fileId = route.query.fileId
|
||||
if (fileId) {
|
||||
selectedFileId.value = fileId
|
||||
setTimeout(() => {
|
||||
selectedFile.value = files.value.find(f => f.id === fileId)
|
||||
if (selectedFile.value) fetchChunks()
|
||||
}, 500)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.text-split {
|
||||
padding: 32px;
|
||||
max-width: 1200px;
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
|
||||
.header-left h2 {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.header-desc {
|
||||
font-size: 14px;
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
padding: 10px 18px;
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
/* Config Card */
|
||||
.config-card {
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border-subtle);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 24px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.config-header {
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 20px;
|
||||
border-bottom: 1px solid var(--border-subtle);
|
||||
}
|
||||
|
||||
.config-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.file-badge {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--accent-primary-muted);
|
||||
border-radius: var(--radius-md);
|
||||
color: var(--accent-primary);
|
||||
}
|
||||
|
||||
.config-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.form-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.form-item label {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.config-action {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid var(--border-subtle);
|
||||
}
|
||||
|
||||
.start-btn {
|
||||
padding: 12px 28px;
|
||||
}
|
||||
|
||||
/* Empty State */
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 80px 20px;
|
||||
background: var(--bg-secondary);
|
||||
border: 1px dashed var(--border-default);
|
||||
border-radius: var(--radius-lg);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.empty-illustration {
|
||||
position: relative;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.ring {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border: 2px dashed var(--border-default);
|
||||
border-radius: 50%;
|
||||
animation: spin 15s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.empty-illustration .el-icon {
|
||||
color: var(--text-muted);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.empty-state h3 {
|
||||
font-size: 18px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.empty-state p {
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
/* Chunks Card */
|
||||
.chunks-card {
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border-subtle);
|
||||
border-radius: var(--radius-lg);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.chunks-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid var(--border-subtle);
|
||||
}
|
||||
|
||||
.chunks-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.chunk-list {
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.chunk-item {
|
||||
padding: 20px;
|
||||
border-bottom: 1px solid var(--border-subtle);
|
||||
transition: background var(--transition-fast);
|
||||
animation: slideIn 0.3s ease backwards;
|
||||
animation-delay: var(--delay);
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(-10px);
|
||||
}
|
||||
}
|
||||
|
||||
.chunk-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.chunk-item:hover {
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
.chunk-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.chunk-badge {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--accent-gradient);
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.chunk-title {
|
||||
font-weight: 600;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.chunk-meta {
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.chunk-content {
|
||||
font-size: 13px;
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.7;
|
||||
white-space: pre-wrap;
|
||||
max-height: 120px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* File Option */
|
||||
.file-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user