refactor: 前端架构重构 - 提取 CSS 和逻辑到独立模块

前端重构:
- 删除旧的大体积 Vue 组件(HomeView, FileManage, TextSplit 等)
- 删除旧的 composables(useFormatters, useModels, useProjects)
- 新增 core/, page-logic/, pages/, shared/ 模块化目录结构
- 提取 CSS 到 styles/pages/ 目录
- 添加全局样式 variables.css 和 common.css

后端 API 更新:
- chunks: 语义分割 API 增强
- files: 文件处理 API 更新
- models: 模型管理 API 更新
- questions: 问答管理 API 更新
- database: 数据库连接优化
- semantic_embedding: 语义嵌入服务优化

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Developer
2026-03-19 14:23:34 +08:00
parent a280b4f014
commit 6aa271c4f7
75 changed files with 22636 additions and 6519 deletions

View File

@@ -0,0 +1,252 @@
import { defineComponent } from 'vue'
import { ref, reactive, computed, onMounted } from 'vue'
import { useRoute } from 'vue-router'
import { ElMessage } from 'element-plus'
import { chunkApi, questionApi, modelApi } from '@/core/api'
export default defineComponent({
name: 'QuestionManage',
setup() {
const route = useRoute()
const projectId = computed(() => route.params.id)
const loading = ref(false)
const isInitialLoad = ref(true)
const generating = ref(false)
const questions = ref([])
const chunks = ref([])
const availableModels = ref([])
const showGenerateDialog = ref(false)
const filterStatus = ref('')
const chunkMap = ref({})
const DEFAULT_GENERATE_PROMPT = '你是一名高质量中文问答数据构建助手。请基于给定 chunk 内容生成准确、自然、可用于训练的数据集问答对。问题必须清晰具体,答案必须直接来自内容或基于内容做合理概括,不要编造原文没有的信息,不要输出与目录、导航、页眉页脚、噪声文字相关的问题。'
const generateConfig = reactive({
model_id: '',
chunk_ids: [],
count: 3,
dirty_data_filter: true,
thinking_mode: true,
preset_prompt: DEFAULT_GENERATE_PROMPT
})
// Multi-select
const selectedQuestions = ref([])
const filteredQuestions = computed(() => {
if (!filterStatus.value) return questions.value
return questions.value.filter(q => q.source === filterStatus.value)
})
const generatedCount = computed(() => questions.value.filter(q => q.source === 'generated').length)
const manualCount = computed(() => questions.value.filter(q => q.source === 'manual').length)
const failedCount = computed(() => questions.value.filter(q => q.status === 'failed').length)
const generateModels = computed(() => {
return availableModels.value.filter(model => {
const type = normalizeModelType(model.model_type, model.model_name)
return type === 'chat' || type === 'vlm'
})
})
const isAllSelected = computed(() => filteredQuestions.value.length > 0 && selectedQuestions.value.length === filteredQuestions.value.length)
const selectedCount = computed(() => selectedQuestions.value.length)
const toggleSelectAll = () => {
if (isAllSelected.value) {
selectedQuestions.value = []
} else {
selectedQuestions.value = filteredQuestions.value.map(q => q.id)
}
}
const toggleSelect = (id) => {
const index = selectedQuestions.value.indexOf(id)
if (index === -1) {
selectedQuestions.value.push(id)
} else {
selectedQuestions.value.splice(index, 1)
}
}
const isSelected = (id) => selectedQuestions.value.includes(id)
const clearSelection = () => {
selectedQuestions.value = []
}
const batchDelete = async () => {
if (selectedQuestions.value.length === 0) return
try {
for (const id of selectedQuestions.value) {
await questionApi.delete(projectId.value, id)
}
ElMessage.success(`已删除 ${selectedQuestions.value.length} 个问题`)
selectedQuestions.value = []
fetchQuestions()
} catch (error) {
ElMessage.error('删除失败')
}
}
const normalizeModelType = (modelType, modelName = '') => {
if (modelType && modelType !== 'chat') {
return modelType
}
const normalizedName = String(modelName).trim().toLowerCase()
if (['rerank', 'bce-reranker', 'gte-rerank'].some(keyword => normalizedName.includes(keyword))) return 'rerank'
if (['embedding', 'embed', 'text-embedding', 'bge-', 'gte-', 'm3e', 'e5-', 'jina-embeddings'].some(keyword => normalizedName.includes(keyword))) return 'embedding'
if (['vl', 'vision', 'visual', 'multimodal', 'qwen-vl', 'gpt-4o'].some(keyword => normalizedName.includes(keyword))) return 'vlm'
return 'chat'
}
const getProviderLabel = (provider) => {
const map = {
openai: 'OpenAI Compatible',
minimax: 'MiniMax',
glm: 'GLM',
ali: '阿里云百炼'
}
return map[provider] || provider
}
const fetchAvailableModels = async () => {
try {
const res = await modelApi.list()
availableModels.value = Array.isArray(res) ? res : (res?.data || [])
if (!generateConfig.model_id && generateModels.value.length) {
const defaultModel = generateModels.value.find(model => model.is_default === 'true') || generateModels.value[0]
generateConfig.model_id = defaultModel?.id || ''
}
} catch (error) {
availableModels.value = []
}
}
const fetchAllChunks = async () => {
const allChunks = []
let page = 1
let total = 0
do {
const res = await chunkApi.list(projectId.value, { page, page_size: 100 })
const items = res.items || res.data || []
total = res.total || res.pagination?.total || items.length
allChunks.push(...items)
page += 1
} while (allChunks.length < total)
return allChunks
}
const fetchQuestions = async () => {
const wasInitial = isInitialLoad.value
loading.value = true
try {
const [chunkList, questionRes] = await Promise.all([
fetchAllChunks(),
questionApi.list(projectId.value, { page: 1, page_size: 500 })
])
chunks.value = chunkList
chunkMap.value = Object.fromEntries(chunkList.map(chunk => [chunk.id, chunk]))
questions.value = questionRes.items || questionRes.data || []
} catch (error) {
questions.value = []
} finally {
loading.value = false
if (wasInitial) {
isInitialLoad.value = false
}
}
}
const handleGenerate = async () => {
if (generateConfig.chunk_ids.length === 0) {
ElMessage.warning('请选择文本块')
return
}
if (!generateConfig.model_id) {
ElMessage.warning('请选择生成模型')
return
}
generating.value = true
try {
await questionApi.generate(projectId.value, generateConfig)
ElMessage.success('问题生成任务已启动')
showGenerateDialog.value = false
setTimeout(fetchQuestions, 2000)
} catch (error) {
ElMessage.error('生成失败')
} finally {
generating.value = false
}
}
const handleDelete = async (question) => {
try {
await questionApi.delete(projectId.value, question.id)
ElMessage.success('删除成功')
fetchQuestions()
} catch (error) {
ElMessage.error('删除失败')
}
}
const getTypeColor = (type) => {
const map = { 'fact': '#22c55e', 'summary': '#818cf8', 'reasoning': '#f59e0b' }
return map[type] || '#818cf8'
}
const getTypeName = (type) => {
const map = { 'fact': '事实性', 'summary': '总结性', 'reasoning': '推理性' }
return map[type] || type
}
const getSourceName = (source) => {
const map = { 'generated': 'AI生成', 'manual': '手动', 'failed': '失败' }
return map[source] || source
}
onMounted(() => {
fetchAvailableModels()
fetchQuestions()
})
return {
route,
projectId,
loading,
isInitialLoad,
generating,
questions,
chunks,
availableModels,
showGenerateDialog,
filterStatus,
chunkMap,
DEFAULT_GENERATE_PROMPT,
generateConfig,
selectedQuestions,
filteredQuestions,
generatedCount,
manualCount,
failedCount,
generateModels,
isAllSelected,
selectedCount,
toggleSelectAll,
toggleSelect,
isSelected,
clearSelection,
batchDelete,
normalizeModelType,
getProviderLabel,
fetchAvailableModels,
fetchAllChunks,
fetchQuestions,
handleGenerate,
handleDelete,
getTypeColor,
getTypeName,
getSourceName
}
}
})