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:
252
frontend/src/page-logic/ProjectQuestionPage.ts
Normal file
252
frontend/src/page-logic/ProjectQuestionPage.ts
Normal 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
|
||||
}
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user