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.
This commit is contained in:
@@ -29,6 +29,15 @@
|
|||||||
<text v-if="!members.length" class="muted">当前项目暂无可派发的在职员工。</text>
|
<text v-if="!members.length" class="muted">当前项目暂无可派发的在职员工。</text>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
<view class="panel ai-panel">
|
||||||
|
<view class="section-head"><text class="panel-title">AI 出题</text></view>
|
||||||
|
<text class="muted">输入考察提示,AI 按岗位生成题目与答案,请逐题核对后发布</text>
|
||||||
|
<input v-model="aiHints" class="input" maxlength="120" placeholder="例如:投诉接待、停车费催缴、电梯故障应对" />
|
||||||
|
<view class="quick-row ai-chip-row"><button v-for="hint in aiHintPresets" :key="hint" class="quick-chip ai-chip" @click="appendAiHint(hint)">{{ hint }}</button></view>
|
||||||
|
<view class="quick-row ai-chip-row"><button v-for="option in aiCountOptions" :key="option" class="quick-chip ai-chip" :class="{ active: aiCount === option }" @click="aiCount = option">{{ option }} 题</button></view>
|
||||||
|
<button class="primary-button ai-generate-button" :disabled="saving || aiGenerating" @click="generateAiQuestions">{{ aiGenerating ? '生成中…约需10-30秒' : 'AI 生成题目' }}</button>
|
||||||
|
</view>
|
||||||
|
|
||||||
<view class="panel question-editor">
|
<view class="panel question-editor">
|
||||||
<view class="section-head"><text class="panel-title">题目({{ questions.length }})· 共 {{ totalScore }} 分</text><button class="ghost-button compact-button" @click="addQuestion">添加题目</button></view>
|
<view class="section-head"><text class="panel-title">题目({{ questions.length }})· 共 {{ totalScore }} 分</text><button class="ghost-button compact-button" @click="addQuestion">添加题目</button></view>
|
||||||
<text v-if="totalScore !== 100" class="score-warning">发布前题目分值合计必须为 100 分</text>
|
<text v-if="totalScore !== 100" class="score-warning">发布前题目分值合计必须为 100 分</text>
|
||||||
@@ -60,7 +69,7 @@ import { computed, ref } from 'vue';
|
|||||||
import { onLoad } from '@dcloudio/uni-app';
|
import { onLoad } from '@dcloudio/uni-app';
|
||||||
import type { ExamQuestion, PracticeTeamMember, SupervisorExamDraft } from '@/types/api';
|
import type { ExamQuestion, PracticeTeamMember, SupervisorExamDraft } from '@/types/api';
|
||||||
import { getPracticeTeamSnapshot } from '@/services/practice';
|
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';
|
import SupervisorTabBar from '@/components/SupervisorTabBar.vue';
|
||||||
|
|
||||||
const positions = ['生活顾问', '客服', '保安', '保洁', '保修'];
|
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 examId = ref(0); const title = ref(''); const positionIndex = ref(0); const passingScore = ref(80); const dueDate = ref('');
|
||||||
const members = ref<PracticeTeamMember[]>([]); const selectedMemberIds = ref<string[]>([]); const questions = ref<ExamQuestion[]>([]); const singleAnswerModels = ref<string[]>([]);
|
const members = ref<PracticeTeamMember[]>([]); const selectedMemberIds = ref<string[]>([]); const questions = ref<ExamQuestion[]>([]); const singleAnswerModels = ref<string[]>([]);
|
||||||
const saving = ref(false); const message = 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 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 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);
|
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 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 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 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 => ({
|
const draft = (): SupervisorExamDraft => ({
|
||||||
title: title.value.trim(),
|
title: title.value.trim(),
|
||||||
projectCode: '',
|
projectCode: '',
|
||||||
@@ -168,4 +197,9 @@ onLoad((options) => { examId.value = Number(options?.id || 0); void load(); });
|
|||||||
.compact-textarea { min-height:82px; }
|
.compact-textarea { min-height:82px; }
|
||||||
.score-warning { color:#a86100; font-size:12px; font-weight:800; }
|
.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); }
|
.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; }
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -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';
|
import { apiRequest } from './api';
|
||||||
|
|
||||||
type RawExamQuestion = {
|
type RawExamQuestion = {
|
||||||
@@ -149,3 +149,35 @@ export const getSupervisorExamResults = (id: number) =>
|
|||||||
method: 'GET',
|
method: 'GET',
|
||||||
timeout: 15000
|
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<RawAiGenerateResponse>({
|
||||||
|
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 || ''
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user