first-update

This commit is contained in:
2026-03-17 14:36:31 +08:00
parent 72f08aee7c
commit 4eddf05e79
516 changed files with 115270 additions and 1 deletions

View 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>