修改模拟面试的相关内容
This commit is contained in:
@@ -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 {getTreeListByCategory} from "@/api/question.js"
|
||||
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 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) {
|
||||
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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user