From 7716f0040d88bfd1a3a86227178a790b00161f22 Mon Sep 17 00:00:00 2001 From: let5sne Date: Mon, 20 Jul 2026 10:16:40 +0800 Subject: [PATCH] feat(mobile): AI question generation panel in supervisor exam editor Hints input + preset chips + count picker; generated questions land in the existing editor blocks for the supervisor to review before publish. --- .../src/pages/supervisor/exams/edit.vue | 36 ++++++++++++++++++- mobile-uni/src/services/exam.ts | 34 +++++++++++++++++- 2 files changed, 68 insertions(+), 2 deletions(-) diff --git a/mobile-uni/src/pages/supervisor/exams/edit.vue b/mobile-uni/src/pages/supervisor/exams/edit.vue index eca34428..c5bd94e8 100644 --- a/mobile-uni/src/pages/supervisor/exams/edit.vue +++ b/mobile-uni/src/pages/supervisor/exams/edit.vue @@ -29,6 +29,15 @@ 当前项目暂无可派发的在职员工。 + + AI 出题 + 输入考察提示,AI 按岗位生成题目与答案,请逐题核对后发布 + + + + + + 题目({{ questions.length }})· 共 {{ totalScore }} 分 发布前题目分值合计必须为 100 分 @@ -60,7 +69,7 @@ import { computed, ref } from 'vue'; import { onLoad } from '@dcloudio/uni-app'; import type { ExamQuestion, PracticeTeamMember, SupervisorExamDraft } from '@/types/api'; import { getPracticeTeamSnapshot } from '@/services/practice'; -import { getSupervisorExam, publishSupervisorExam, saveSupervisorExam, updateSupervisorExam } from '@/services/exam'; +import { generateSupervisorExamQuestions, getSupervisorExam, publishSupervisorExam, saveSupervisorExam, updateSupervisorExam } from '@/services/exam'; import SupervisorTabBar from '@/components/SupervisorTabBar.vue'; const positions = ['生活顾问', '客服', '保安', '保洁', '保修']; @@ -69,6 +78,9 @@ const quickDates = [{ label: '当天', days: 0 }, { label: '明天', days: 1 }, const examId = ref(0); const title = ref(''); const positionIndex = ref(0); const passingScore = ref(80); const dueDate = ref(''); const members = ref([]); const selectedMemberIds = ref([]); const questions = ref([]); const singleAnswerModels = ref([]); const saving = ref(false); const message = ref(''); +const aiHints = ref(''); const aiCount = ref(5); const aiGenerating = ref(false); +const aiHintPresets = ['投诉接待', '催缴沟通', '电梯故障', '服务态度', '消防安全']; +const aiCountOptions = [5, 8, 10]; const formatDate = (date: Date) => `${date.getFullYear()}-${String(date.getMonth()+1).padStart(2,'0')}-${String(date.getDate()).padStart(2,'0')}`; const dateAfter = (days: number) => { const date = new Date(); date.setHours(12,0,0,0); date.setDate(date.getDate()+days); return formatDate(date); }; const today = dateAfter(0); @@ -104,6 +116,23 @@ const selectFormat = (index: number, event: { detail: { value: string } }) => { const syncAnswer = (index: number) => { questions.value[index].answer = questions.value[index].format === 'multiple' ? singleAnswerModels.value[index].split('|').map((value) => value.trim()).filter(Boolean) : singleAnswerModels.value[index]; }; const toggleMember = (id: string) => { selectedMemberIds.value = selectedMemberIds.value.includes(id) ? selectedMemberIds.value.filter((item) => item !== id) : selectedMemberIds.value.length < 100 ? [...selectedMemberIds.value,id] : selectedMemberIds.value; }; const selectAll = () => { selectedMemberIds.value = members.value.slice(0,100).map((item) => item.extPartyId); }; +const appendAiHint = (hint: string) => { + const parts = aiHints.value.split(/[、,,\s]+/).map((value) => value.trim()).filter(Boolean); + if (!parts.includes(hint)) parts.push(hint); + aiHints.value = parts.join('、'); +}; +const generateAiQuestions = async () => { + if (saving.value || aiGenerating.value) return; aiGenerating.value = true; message.value = ''; + try { + const result = await generateSupervisorExamQuestions({ positionCode: positions[positionIndex.value], hints: aiHints.value.trim(), count: aiCount.value, formats: ['single','multiple','judge'] }); + questions.value = result.questions; + singleAnswerModels.value = result.questions.map((item) => Array.isArray(item.answer) ? item.answer.join('|') : String(item.answer || '')); + message.value = result.groundedFragments > 0 + ? `已生成 ${result.questions.length} 题(参考 ${result.groundedFragments} 条 SOP 片段),请逐题核对修改后再发布` + : `已生成 ${result.questions.length} 题,请逐题核对修改后再发布`; + uni.pageScrollTo({ selector: '.question-editor', duration: 200 }); + } catch (error) { message.value = error instanceof Error ? error.message : 'AI 出题失败,请稍后重试'; } finally { aiGenerating.value = false; } +}; const draft = (): SupervisorExamDraft => ({ title: title.value.trim(), projectCode: '', @@ -168,4 +197,9 @@ onLoad((options) => { examId.value = Number(options?.id || 0); void load(); }); .compact-textarea { min-height:82px; } .score-warning { color:#a86100; font-size:12px; font-weight:800; } .publish-dock { position:fixed; z-index:10; right:0; bottom:calc(76px + env(safe-area-inset-bottom)); left:0; display:grid; grid-template-columns:1fr 1fr; gap:10px; max-width:var(--mobile-shell-max-width); margin:0 auto; padding:12px 16px; border-top:1px solid var(--line); background:rgba(255,255,255,.97); } +.ai-panel { display:grid; gap:10px; } +.ai-chip-row { justify-content:flex-start; flex-wrap:wrap; } +.ai-chip { display:flex; align-items:center; justify-content:center; min-height:34px; padding:0 12px; line-height:1; } +.ai-chip.active { border-color:var(--brand); color:var(--brand); font-weight:900; } +.ai-generate-button { display:flex; align-items:center; justify-content:center; min-height:44px; line-height:1; } diff --git a/mobile-uni/src/services/exam.ts b/mobile-uni/src/services/exam.ts index 7f58cef6..e6f7cbd6 100644 --- a/mobile-uni/src/services/exam.ts +++ b/mobile-uni/src/services/exam.ts @@ -1,4 +1,4 @@ -import type { ExamDetail, ExamResultItem, ExamSubmitResult, ExamSummary, SupervisorExamDraft } from '@/types/api'; +import type { ExamDetail, ExamQuestion, ExamResultItem, ExamSubmitResult, ExamSummary, SupervisorExamDraft } from '@/types/api'; import { apiRequest } from './api'; type RawExamQuestion = { @@ -149,3 +149,35 @@ export const getSupervisorExamResults = (id: number) => method: 'GET', timeout: 15000 }); + + +type RawAiGenerateResponse = { + questions: RawExamQuestion[]; + groundedFragments?: number; +}; + +export const generateSupervisorExamQuestions = async (data: { + positionCode: string; + hints: string; + count: number; + formats: string[]; +}): Promise<{ questions: ExamQuestion[]; groundedFragments: number }> => { + const response = await apiRequest({ + url: '/api/aihr/mobile/exams/supervisor/ai-generate', + method: 'POST', + data, + timeout: 60000 + }); + return { + groundedFragments: response.groundedFragments || 0, + questions: (response.questions || []).map((question, index) => ({ + id: Date.now() + index, + format: question.questionType, + stem: question.stem, + options: question.options || [], + score: question.score, + answer: question.questionType === 'multiple' ? (question.correctAnswers || []) : (question.correctAnswers?.[0] || ''), + explanation: question.explanation || '' + })) + }; +};