修改模拟面试的相关内容

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

View File

@@ -111,6 +111,15 @@
</template> </template>
</el-upload> </el-upload>
</el-form-item> </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-form-item>
<el-button <el-button
@@ -173,14 +182,14 @@ const startInterviewAction = async () => {
isLoading.value = true; isLoading.value = true;
const sendFormData = new FormData(); const sendFormData = new FormData();
const selectionResult = questionBankSectionRef.value.getSelectionResult() const selectionResult = questionBankSectionRef.value.getSelectionResult()
if (!selectionResult.selectedNodeList) { if (!selectionResult.selectedNodes) {
selectionResult.selectedNodeList = [] selectionResult.selectedNodes = []
} }
console.log(selectionResult) console.log(selectionResult)
sendFormData.append('candidateName', formData.value.candidateName); sendFormData.append('candidateName', formData.value.candidateName);
sendFormData.append('model', selectedMode.value); sendFormData.append('model', selectedMode.value);
if (selectionResult.selectedNodeList && selectionResult.selectedNodeList.length > 0) { if (selectionResult.selectedNodes && selectionResult.selectedNodes.length > 0) {
sendFormData.append('selectedNodes', selectionResult.selectedNodeList); sendFormData.append('selectedNodes', selectionResult.selectedNodes);
} }
sendFormData.append('resume', formData.value.resumeFiles); sendFormData.append('resume', formData.value.resumeFiles);