修改模拟面试的相关内容

This commit is contained in:
2025-09-20 21:43:20 +08:00
parent 7e6cf25295
commit 84ae32adc1
2 changed files with 113 additions and 133 deletions

View File

@@ -3,7 +3,7 @@
<div class="section-header">
<h3>题库选择</h3>
<div class="selection-actions">
<el-button link @click="selectAllValid">全选</el-button>
<!-- <el-button link @click="selectAllValid">全选</el-button>-->
<el-button link @click="clearAll">清空</el-button>
</div>
</div>
@@ -13,16 +13,14 @@
ref="categoryTree"
:data="categoryTreeData"
:props="treeProps"
node-key="id"
node-key="nodeKey"
show-checkbox
:default-expand-all="false"
:expand-on-click-node="true"
@check="handleTreeCheck"
:default-checked-keys="defaultCheckedKeys"
:filter-node-method="filterNode"
>
<template #default="{ node, data }">
<span class="tree-node" :class="{ 'disabled-node': isCategoryEmpty(data) }">
<span class="tree-node" :class="{ 'disabled-node': data.disabled }">
<span class="node-label">{{ node.label }}</span>
<span v-if="data.type === 'question'" class="difficulty-tag" :class="data.difficulty?.toLowerCase()">
{{ data.difficulty }}
@@ -48,7 +46,7 @@
</div>
<div class="summary-item">
<span class="summary-label">总计题目</span>
<span class="summary-value">{{ categoryTreeData && categoryTreeData[0].count }} 道题目</span>
<span class="summary-value">{{ totalQuestionCount }} 道题目</span>
</div>
</div>
</div>
@@ -75,46 +73,40 @@
</template>
<script setup>
import {onMounted, ref, computed, watch, nextTick} from 'vue'
import {ElMessage, ElLoading} from 'element-plus'
import { onMounted, ref, computed } from 'vue'
import { ElMessage } from 'element-plus'
import { getTreeListByCategory } from "@/api/question.js"
// 组件内部状态
// --- 状态管理 ---
const categoryTreeData = ref([])
const categoryTree = ref(null)
const selectedDifficulty = ref('ALL')
const defaultCheckedKeys = ref([])
// 存储所有选中的节点ID包括分类和题目
const selectedNodeIds = ref(new Set())
const selectedNodeList = ref([])
// 选中的节点列表,作为唯一的信任源,存储完整的节点对象。
const selectedNodes = ref([])
const treeProps = {
children: 'children',
label: 'name'
label: 'name',
// 利用节点数据上的 'disabled' 属性来禁用复选框,这是 Element Plus 的标准用法
disabled: 'disabled'
}
// 计算属性
const hasSelection = computed(() => selectedNodeIds.value.size > 0)
// --- 计算属性 ---
// 简化的计算属性,直接从 `selectedNodes` 派生,无需复杂的查找。
const hasSelection = computed(() => selectedNodes.value.length > 0)
const totalQuestionCount = computed(() => {
// 假设数据数组的顶层节点(通常是第一个)包含了总数统计
return categoryTreeData.value[0]?.count || 0
})
const selectedCategories = computed(() => {
return Array.from(selectedNodeIds.value)
.map(id => {
const node = findNodeById(id)
return node && node.type === 'category' && !isCategoryEmpty(node) ?
{id: node.id, name: node.name} : null
})
.filter(Boolean)
return selectedNodes.value.filter(node => node.type === 'category')
})
const selectedQuestions = computed(() => {
return Array.from(selectedNodeIds.value)
.map(id => {
const node = findNodeById(id)
return node && node.type === 'question' ?
{id: node.id, name: node.name} : null
})
.filter(Boolean)
return selectedNodes.value.filter(node => node.type === 'question')
})
const selectionSummary = computed(() => {
@@ -122,148 +114,127 @@ const selectionSummary = computed(() => {
const queCount = selectedQuestions.value.length
if (catCount === 0 && queCount === 0) {
return '请选择题目或分类(分类不可选)'
return '请选择题目或分类(包含0题的分类不可选)'
}
return `已选择 ${catCount} 个分类,${queCount} 道单独题目,共计 ${categoryTreeData.value.length} 道题目`
// 显示一个更精确的统计摘要
return `已选择 ${catCount} 个分类和 ${queCount} 道单独题目。`
})
// 方法
// 检查分类是否为空(没有题目)
const isCategoryEmpty = (category) => {
return category.type === 'category' && category.count === 0
}
// --- 方法 ---
// 扁平化树结构(缓存结果)
let flattenedTreeCache = []
const flattenTree = (nodes) => {
let result = []
/**
* 递归处理树形数据,为题目数量为 0 的分类添加 'disabled' 属性。
* 这是 Element Plus 中禁用树节点的标准方式。
* @param {Array} nodes - 需要处理的节点数组。
*/
const processTreeData = (nodes) => {
nodes.forEach(node => {
result.push(node)
if (node.children && node.children.length > 0) {
result = result.concat(flattenTree(node.children))
if (node.type === 'category') {
// 如果一个分类的题目数量为 0则禁用它
node.disabled = node.count === 0
if (node.children?.length > 0) {
processTreeData(node.children)
}
}
})
return result
}
// 更新缓存
const updateTreeCache = () => {
flattenedTreeCache = flattenTree(categoryTreeData.value)
}
// 根据ID查找节点使用缓存
const findNodeById = (id) => {
return flattenedTreeCache.find(node => node.id === id)
}
// 过滤空分类节点
const filterNode = (value, data) => {
if (data.type === 'category') {
return !isCategoryEmpty(data)
}
return true
}
// 处理树节点选择(使用批量处理)
const handleTreeCheck = (node, {checkedKeys, halfCheckedKeys}) => {
// 使用防抖处理频繁选择
clearTimeout(debounceTimer)
debounceTimer = setTimeout(() => {
processTreeSelection(checkedKeys)
}, 100)
selectedNodeList.value = categoryTree.value.getCheckedNodes()
}
let debounceTimer = null
// 处理树选择结果
const processTreeSelection = (checkedKeys) => {
// 更新选中节点
selectedNodeIds.value = new Set(checkedKeys)
}
// 选择所有有效内容
const selectAllValid = async () => {
// 获取所有非空节点ID
const allValidNodeIds = flattenedTreeCache
.filter(node => node.type === 'question' || (node.type === 'category' && !isCategoryEmpty(node)))
.map(node => node.id)
// 更新选择
selectedNodeIds.value = new Set(allValidNodeIds)
// 更新树选择状态
/**
* 处理树节点选中事件,这是更新选择状态的核心。
*/
const handleTreeCheck = () => {
// getCheckedNodes() 是获取所有选中项最直接的方法。
// 这取代了原先管理 ID 和扁平化缓存的复杂逻辑。
if (categoryTree.value) {
categoryTree.value.setCheckedKeys(allValidNodeIds)
selectedNodes.value = categoryTree.value.getCheckedNodes()
}
}
/**
* 选中所有有效(未被禁用)的节点。
*/
const selectAllValid = () => {
if (!categoryTree.value) return
// 递归辅助函数,用于收集所有未被禁用的节点 ID。
const getAllValidIds = (nodes) => {
let ids = []
nodes.forEach(node => {
if (!node.disabled) {
ids.push(node.id)
if (node.children) {
ids = ids.concat(getAllValidIds(node.children))
}
}
})
return ids
}
// 清空所有选择
const clearAll = async () => {
selectedNodeIds.value.clear()
const allValidIds = getAllValidIds(categoryTreeData.value)
categoryTree.value.setCheckedKeys(allValidIds)
// 使用 setCheckedKeys 后,需要手动同步我们的状态
handleTreeCheck()
}
/**
* 清空所有选择。
*/
const clearAll = () => {
if (categoryTree.value) {
categoryTree.value.setCheckedKeys([])
// 同步清空本地状态
selectedNodes.value = []
}
}
// 处理难度筛选变化
/**
* 处理难度筛选器的变更。
*/
const handleDifficultyChange = () => {
selectedNodeIds.value.clear()
selectedNodeList.value = []
// 清空现有选择,并为新的难度重新加载数据。
selectedNodes.value = []
loadCategories()
}
// 加载分类数据(使用分页或虚拟滚动优化大数据量)
/**
* 从 API 加载分类和题目数据。
*/
const loadCategories = async () => {
try {
const res = await getTreeListByCategory({
difficulty: selectedDifficulty.value === 'ALL' ? null : selectedDifficulty.value
})
categoryTreeData.value = res.data || []
const data = res.data || []
// 在渲染前处理数据,添加 'disabled' 标记。
processTreeData(data)
categoryTreeData.value = data
// 更新树缓存
updateTreeCache()
// 重新应用选择状态
if (categoryTree.value && selectedNodeIds.value.size > 0) {
const currentSelected = Array.from(selectedNodeIds.value)
categoryTree.value.setCheckedKeys(currentSelected)
}
} catch (error) {
ElMessage.error('加载分类数据失败:' + error.message)
}
}
// 获取最终选择结果(供父组件调用)
const getSelectionResult = () => {
return {
nodeIds: Array.from(selectedNodeIds.value),
totalQuestions: categoryTreeData.value.length,
selectedNodeList: selectedNodeList.value ? selectedNodeList.value : []
}
}
// --- 生命周期钩子 ---
// 组件挂载时加载数据
onMounted(() => {
loadCategories()
})
// 暴露方法给父组件
// --- 暴露方法 ---
// 暴露方法给父组件,用于获取选择结果。
defineExpose({
getSelectionResult
getSelectionResult: () => ({
selectedNodes: selectedNodes.value,
totalQuestions: totalQuestionCount.value
})
})
</script>
<style scoped>
/* 样式部分保持不变 */
.question-bank-section {
background: #fff;
border-radius: 16px;

View File

@@ -111,6 +111,15 @@
</template>
</el-upload>
</el-form-item>
<el-form-item label="AI模型">
<el-select v-model="formData.model" placeholder="请选择AI模型">
<el-option label="GPT-3.5" value="gpt-3.5-turbo"></el-option>
<el-option label="GPT-4" value="gpt-4"></el-option>
</el-select>
</el-form-item>
<el-form-item label="面试题目数量">
<el-input-number v-model="formData.questionCount" :min="1" :max="100"></el-input-number>
</el-form-item>
<el-form-item>
<el-button
@@ -173,14 +182,14 @@ const startInterviewAction = async () => {
isLoading.value = true;
const sendFormData = new FormData();
const selectionResult = questionBankSectionRef.value.getSelectionResult()
if (!selectionResult.selectedNodeList) {
selectionResult.selectedNodeList = []
if (!selectionResult.selectedNodes) {
selectionResult.selectedNodes = []
}
console.log(selectionResult)
sendFormData.append('candidateName', formData.value.candidateName);
sendFormData.append('model', selectedMode.value);
if (selectionResult.selectedNodeList && selectionResult.selectedNodeList.length > 0) {
sendFormData.append('selectedNodes', selectionResult.selectedNodeList);
if (selectionResult.selectedNodes && selectionResult.selectedNodes.length > 0) {
sendFormData.append('selectedNodes', selectionResult.selectedNodes);
}
sendFormData.append('resume', formData.value.resumeFiles);