实训任务编辑接入第一步选择题配置:教师端与平台端抽屉新增选择题入口(仅新产品调查/行业需求/竞争环境三个白名单任务),复用StepOneChoiceQuestionEditor,保存随任务配置持久化

fix/exam-question-access-material-excel-delete-draft
陈沅 2 weeks ago
parent b1fc16c6c1
commit ca1c5d5886

@ -394,6 +394,27 @@
</div> </div>
</div> </div>
<!-- 第一步选择题:仅三个白名单任务可配置,复用统一编辑器 -->
<div v-if="supportsStepOneChoice(taskForm.taskKey)" class="material-row">
<div class="material-info">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M9 11l3 3L22 4"/>
<path d="M21 12v7a2 2 0 01-2 2H5a2 2 0 01-2-2V5a2 2 0 012-2h11"/>
</svg>
<div class="material-text">
<span class="material-label">第一步选择题</span>
<span v-if="stepOneQuestionCount" class="material-link">已配置 {{ stepOneQuestionCount }} 道题</span>
<span v-else class="material-placeholder">暂未配置,学生端第一步将不显示选择题</span>
</div>
</div>
<div class="material-ops">
<el-button v-if="!isReadonly" type="primary" class="upload-btn" @click="choiceEditorVisible = true">
设置选择题
</el-button>
</div>
</div>
<StepOneChoiceQuestionEditor v-model:visible="choiceEditorVisible" v-model="taskForm.stepOneQuestions" :task-name="taskForm.title" />
<el-form-item label="步骤名称:"> <el-form-item label="步骤名称:">
<div class="step-list"> <div class="step-list">
<div v-for="(step, index) in taskForm.steps" :key="step.id" class="step-item"> <div v-for="(step, index) in taskForm.steps" :key="step.id" class="step-item">
@ -456,6 +477,8 @@ import {
updateTrainingTask, updateTrainingTask,
uploadTrainingTaskMaterial, uploadTrainingTaskMaterial,
} from "@/api/trainingTask"; } from "@/api/trainingTask";
import StepOneChoiceQuestionEditor from "@/views/components/StepOneChoiceQuestionEditor.vue";
import { parseStepOneChoiceQuestions, supportsStepOneChoiceQuestions } from "@/utils/stepOneChoiceQuestions";
const { proxy } = getCurrentInstance(); const { proxy } = getCurrentInstance();
@ -464,6 +487,7 @@ const keyword = ref("");
const loading = ref(false); const loading = ref(false);
const saving = ref(false); const saving = ref(false);
const uploadingField = ref(""); const uploadingField = ref("");
const choiceEditorVisible = ref(false);
// 四类资料槽位:key 对应 taskForm 的 `${key}Name` / `${key}Url` 字段 // 四类资料槽位:key 对应 taskForm 的 `${key}Name` / `${key}Url` 字段
const materialSlots = [ const materialSlots = [
{ key: "material", label: "案例资料" }, { key: "material", label: "案例资料" },
@ -477,6 +501,7 @@ const dialogMode = ref("view");
const currentTaskId = ref(""); const currentTaskId = ref("");
const taskForm = reactive(createEmptyTask()); const taskForm = reactive(createEmptyTask());
const isReadonly = computed(() => dialogMode.value === "view"); const isReadonly = computed(() => dialogMode.value === "view");
const stepOneQuestionCount = computed(() => taskForm.stepOneQuestions?.length || 0);
const enabledCount = computed(() => tasks.value.filter(t => t.enabled).length); const enabledCount = computed(() => tasks.value.filter(t => t.enabled).length);
@ -564,12 +589,17 @@ function createEmptyTask() {
guideManualUrl: "", guideManualUrl: "",
aiManualName: "", aiManualName: "",
aiManualUrl: "", aiManualUrl: "",
stepOneQuestions: [],
steps: createBuiltinSteps(), steps: createBuiltinSteps(),
sort: 0, sort: 0,
enabled: true, enabled: true,
}; };
} }
function supportsStepOneChoice(taskKey) {
return supportsStepOneChoiceQuestions(taskKey);
}
function hasMaterial(slotKey) { function hasMaterial(slotKey) {
return isRealMaterial(taskForm[`${slotKey}Name`], taskForm[`${slotKey}Url`]); return isRealMaterial(taskForm[`${slotKey}Name`], taskForm[`${slotKey}Url`]);
} }
@ -599,6 +629,7 @@ function normalizeTask(task) {
guideManualUrl: guideManual.url, guideManualUrl: guideManual.url,
aiManualName: aiManual.name, aiManualName: aiManual.name,
aiManualUrl: aiManual.url, aiManualUrl: aiManual.url,
stepOneQuestions: parseStepOneChoiceQuestions(task.stepOneQuestions),
steps: normalizeSteps(parseArray(task.steps, createBuiltinSteps())), steps: normalizeSteps(parseArray(task.steps, createBuiltinSteps())),
sort: task.sort || 0, sort: task.sort || 0,
enabled: task.enabled !== false, enabled: task.enabled !== false,
@ -771,6 +802,7 @@ function buildPayload() {
guideManualUrl: taskForm.guideManualUrl, guideManualUrl: taskForm.guideManualUrl,
aiManualName: taskForm.aiManualName, aiManualName: taskForm.aiManualName,
aiManualUrl: taskForm.aiManualUrl, aiManualUrl: taskForm.aiManualUrl,
stepOneQuestions: JSON.stringify(taskForm.stepOneQuestions || []),
sort: taskForm.sort, sort: taskForm.sort,
enabled: taskForm.enabled, enabled: taskForm.enabled,
}; };

@ -344,6 +344,28 @@
</div> </div>
</div> </div>
<!-- 第一步选择题:仅三个白名单任务可配置,复用统一编辑器 -->
<div v-if="supportsStepOneChoice(taskForm.taskKey)" class="material-row">
<div class="material-info">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2">
<path d="M9 11l3 3L22 4" />
<path d="M21 12v7a2 2 0 01-2 2H5a2 2 0 01-2-2V5a2 2 0 012-2h11" />
</svg>
<div class="material-text">
<span class="material-label">第一步选择题</span>
<span v-if="stepOneQuestionCount" class="material-link">已配置 {{ stepOneQuestionCount }} 道题</span>
<span v-else class="material-placeholder">暂未配置,学生端第一步将不显示选择题</span>
</div>
</div>
<div class="material-ops">
<el-button v-if="!isReadonly" type="primary" class="upload-btn" @click="choiceEditorVisible = true">
设置选择题
</el-button>
</div>
</div>
<StepOneChoiceQuestionEditor v-model:visible="choiceEditorVisible" v-model="taskForm.stepOneQuestions" :task-name="taskForm.title" />
<!-- 步骤数量与顺序固定,仅允许修改名称 --> <!-- 步骤数量与顺序固定,仅允许修改名称 -->
<el-form-item label="步骤名称:"> <el-form-item label="步骤名称:">
<div class="step-list"> <div class="step-list">
@ -397,6 +419,8 @@ import { listTrainingTasks, listClassTrainingTasks, saveClassTrainingTask, resto
import { createStudentDemoSession } from "@/api/studentDemo" import { createStudentDemoSession } from "@/api/studentDemo"
import * as indexApi from "@/api/teacher" import * as indexApi from "@/api/teacher"
import useUserStore from "@/store/modules/user" import useUserStore from "@/store/modules/user"
import StepOneChoiceQuestionEditor from "@/views/components/StepOneChoiceQuestionEditor.vue"
import { parseStepOneChoiceQuestions, supportsStepOneChoiceQuestions } from "@/utils/stepOneChoiceQuestions"
const { proxy } = getCurrentInstance() const { proxy } = getCurrentInstance()
const userStore = useUserStore() const userStore = useUserStore()
@ -408,6 +432,7 @@ const classesLoading = ref(false)
const saving = ref(false) const saving = ref(false)
const restoring = ref(false) const restoring = ref(false)
const uploadingField = ref("") const uploadingField = ref("")
const choiceEditorVisible = ref(false)
// 四类资料槽位:key 对应 taskForm 的 `${key}Name` / `${key}Url` 字段 // 四类资料槽位:key 对应 taskForm 的 `${key}Name` / `${key}Url` 字段
const materialSlots = [ const materialSlots = [
{ key: "material", label: "案例资料" }, { key: "material", label: "案例资料" },
@ -423,6 +448,7 @@ const dialogMode = ref("view")
const taskForm = reactive(createEmptyTask()) const taskForm = reactive(createEmptyTask())
const currentUserId = computed(() => userStore.userInfo.userId) const currentUserId = computed(() => userStore.userInfo.userId)
const isReadonly = computed(() => dialogMode.value === "view") const isReadonly = computed(() => dialogMode.value === "view")
const stepOneQuestionCount = computed(() => taskForm.stepOneQuestions?.length || 0)
const projects = computed(() => [...new Set(tasks.value.map((task) => task.project).filter(Boolean))]) const projects = computed(() => [...new Set(tasks.value.map((task) => task.project).filter(Boolean))])
const projectOptions = computed(() => { const projectOptions = computed(() => {
return projects.value.map((project) => ({ return projects.value.map((project) => ({
@ -566,12 +592,17 @@ function createEmptyTask() {
guideManualUrl: "", guideManualUrl: "",
aiManualName: "", aiManualName: "",
aiManualUrl: "", aiManualUrl: "",
stepOneQuestions: [],
steps: createBuiltinSteps(), steps: createBuiltinSteps(),
sort: 0, sort: 0,
enabled: true, enabled: true,
} }
} }
function supportsStepOneChoice(taskKey) {
return supportsStepOneChoiceQuestions(taskKey)
}
function hasMaterial(slotKey) { function hasMaterial(slotKey) {
return isRealMaterial(taskForm[`${slotKey}Name`], taskForm[`${slotKey}Url`]) return isRealMaterial(taskForm[`${slotKey}Name`], taskForm[`${slotKey}Url`])
} }
@ -601,6 +632,7 @@ function normalizeTask(task) {
guideManualUrl: guideManual.url, guideManualUrl: guideManual.url,
aiManualName: aiManual.name, aiManualName: aiManual.name,
aiManualUrl: aiManual.url, aiManualUrl: aiManual.url,
stepOneQuestions: parseStepOneChoiceQuestions(task.stepOneQuestions),
steps: normalizeSteps(parseArray(task.steps, createBuiltinSteps())), steps: normalizeSteps(parseArray(task.steps, createBuiltinSteps())),
sort: task.sort || 0, sort: task.sort || 0,
enabled: task.enabled !== false, enabled: task.enabled !== false,
@ -833,6 +865,7 @@ function buildPayload() {
guideManualUrl: taskForm.guideManualUrl, guideManualUrl: taskForm.guideManualUrl,
aiManualName: taskForm.aiManualName, aiManualName: taskForm.aiManualName,
aiManualUrl: taskForm.aiManualUrl, aiManualUrl: taskForm.aiManualUrl,
stepOneQuestions: JSON.stringify(taskForm.stepOneQuestions || []),
sort: taskForm.sort, sort: taskForm.sort,
enabled: taskForm.enabled, enabled: taskForm.enabled,
} }

Loading…
Cancel
Save