修改模拟面试的相关内容
This commit is contained in:
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user