feat(aihr): add employee mistake book audit

This commit is contained in:
2026-07-14 03:23:49 +08:00
parent 5c0f792f37
commit d98ff0ff8b
12 changed files with 317 additions and 10 deletions
+124 -2
View File
@@ -68,6 +68,39 @@
<view v-else class="empty">暂无今日任务</view>
</view>
<view v-if="loggedIn" class="panel mistake-panel">
<view class="panel-head">
<view>
<text class="panel-title">错题本</text>
<text class="panel-caption">从低分和红线回合中自动归集</text>
</view>
<button class="ghost-button compact-button" :disabled="mistakeLoading" @click="loadMistakes">
{{ mistakeLoading ? '加载中' : '刷新' }}
</button>
</view>
<view v-if="mistakes.length" class="mistake-list">
<view v-for="item in mistakes" :key="item.id" class="mistake-row">
<view class="mistake-copy">
<view class="mistake-meta">
<text class="mistake-cause">{{ item.cause }}</text>
<text class="mistake-score">{{ item.score }} 分</text>
</view>
<text class="mistake-title">{{ item.scenarioName }}</text>
<text class="mistake-desc">{{ item.time }} · {{ item.summary }}</text>
<text class="mistake-status">再练状态:{{ item.retryStatus }}</text>
</view>
<button
v-if="item.retryAssignmentId"
class="secondary-button compact-button"
@click="openMistake(item)"
>
去再练
</button>
</view>
</view>
<view v-else-if="!mistakeLoading" class="empty">完成训练后,低分和红线回合会自动出现在这里</view>
</view>
<view v-if="loggedIn" class="panel">
<view class="panel-head">
<text class="panel-title">岗位 SOP</text>
@@ -145,9 +178,10 @@ import { onShow } from '@dcloudio/uni-app';
import type {
KnowledgeSearchResponse,
PracticeAssignment,
PracticeMistake,
PracticePrepCard
} from '@/types/api';
import { getPracticeAssignments, getPracticePrepCard } from '@/services/practice';
import { getPracticeAssignments, getPracticeMistakes, getPracticePrepCard } from '@/services/practice';
import { searchKnowledge } from '@/services/knowledge';
import { getSelectedPositionProfile, isLoggedIn, rememberLoginRedirect } from '@/services/auth';
import { buildLearningTopics, selectLearningTopic } from '@/services/learning';
@@ -161,10 +195,12 @@ const pendingSopQueryKey = 'aihr_sop_pending_query';
const loggedIn = ref(false);
const message = ref('');
const assignments = ref<PracticeAssignment[]>([]);
const mistakes = ref<PracticeMistake[]>([]);
const prepCard = ref<PracticePrepCard | null>(null);
const knowledgeResult = ref<KnowledgeSearchResponse | null>(null);
const activeTopic = ref('岗前 SOP');
const assignmentLoading = ref(false);
const mistakeLoading = ref(false);
const prepLoading = ref(false);
const knowledgeLoading = ref(false);
const positionProfile = ref(getSelectedPositionProfile());
@@ -235,6 +271,19 @@ const loadAssignments = async () => {
}
};
const loadMistakes = async () => {
if (!loggedIn.value) return;
mistakeLoading.value = true;
try {
mistakes.value = await getPracticeMistakes();
} catch (error) {
mistakes.value = [];
handleRequestError(error, '错题本加载失败');
} finally {
mistakeLoading.value = false;
}
};
const loadPrepCard = async () => {
if (!loggedIn.value) return;
prepLoading.value = true;
@@ -267,6 +316,7 @@ const loadPage = async () => {
message.value = '';
if (!loggedIn.value) {
assignments.value = [];
mistakes.value = [];
prepCard.value = null;
knowledgeResult.value = null;
return;
@@ -275,7 +325,7 @@ const loadPage = async () => {
return;
}
refreshAuthState();
await loadAssignments();
await Promise.all([loadAssignments(), loadMistakes()]);
activeTopic.value = selectLearningTopic(assignments.value, defaultTopic.value);
await Promise.all([
loadKnowledge(activeTopic.value),
@@ -293,6 +343,12 @@ const openAssignment = (item: PracticeAssignment) => {
uni.switchTab({ url: '/pages/user/practice/index' });
};
const openMistake = (item: PracticeMistake) => {
if (!item.retryAssignmentId) return;
uni.setStorageSync(pendingAssignmentKey, String(item.retryAssignmentId));
uni.switchTab({ url: '/pages/user/practice/index' });
};
const goPractice = () => {
if (firstPracticeAssignment.value) {
uni.setStorageSync(pendingAssignmentKey, String(firstPracticeAssignment.value.id));
@@ -320,6 +376,72 @@ onShow(() => {
padding-bottom: 42px;
}
.panel-caption {
display: block;
margin-top: 4px;
color: #7b8494;
font-size: 12px;
}
.mistake-list {
display: grid;
gap: 10px;
}
.mistake-row {
display: flex;
gap: 12px;
align-items: center;
justify-content: space-between;
padding: 12px;
border: 1px solid #fee2e2;
border-radius: 12px;
background: #fff8f8;
}
.mistake-copy {
min-width: 0;
flex: 1;
}
.mistake-meta {
display: flex;
gap: 8px;
align-items: center;
}
.mistake-cause {
color: #bd0010;
font-size: 12px;
font-weight: 700;
}
.mistake-score {
color: #7b8494;
font-size: 12px;
}
.mistake-title,
.mistake-desc,
.mistake-status {
display: block;
}
.mistake-title {
margin-top: 4px;
color: #1f2937;
font-size: 14px;
font-weight: 700;
}
.mistake-desc,
.mistake-status {
margin-top: 4px;
color: #7b8494;
font-size: 12px;
line-height: 1.5;
}
.login-panel {
display: flex;
gap: 14px;
+10
View File
@@ -2,6 +2,7 @@ import type {
PracticeFinishResponse,
PracticePrepCard,
PracticeRecord,
PracticeMistake,
PracticeTeamSnapshot,
PracticeStartResponse,
PracticeTurnResponse,
@@ -88,6 +89,15 @@ export const getPracticeHistoryDetail = (id: number) =>
timeout: 15000
});
export const getPracticeMistakes = () => {
const extPartyId = encodeURIComponent(currentExtPartyId());
return apiRequest<PracticeMistake[]>({
url: `/api/aihr/mobile/practice/mistakes?extPartyId=${extPartyId}`,
method: 'GET',
timeout: 15000
});
};
export const getCompetencyProfile = () => {
const extPartyId = encodeURIComponent(currentExtPartyId());
return apiRequest<CompetencyProfile>({
+13
View File
@@ -193,6 +193,19 @@ export interface PracticeRecord {
summary: string;
}
export interface PracticeMistake {
id: number;
sessionId: string;
scenarioId: string;
scenarioName: string;
score: number;
cause: string;
retryAssignmentId?: number | null;
retryStatus: string;
time: string;
summary: string;
}
export interface PracticeTeamMember {
extPartyId: string;
name: string;
@@ -135,3 +135,16 @@ test('训练页保留任务卡、对话区、回复区和结果区', async () =>
assert.match(source, /reply-card/);
assert.match(source, /result-card/);
});
test('学习页展示真实错题本并可进入已有再练任务', async () => {
const [source, practiceService] = await Promise.all([
pageSource('../src/pages/user/learning/index.vue'),
pageSource('../src/services/practice.ts')
]);
assert.match(source, /错题本/);
assert.match(source, /getPracticeMistakes/);
assert.match(source, /retryAssignmentId/);
assert.match(source, /pendingAssignmentKey/);
assert.match(practiceService, /\/api\/aihr\/mobile\/practice\/mistakes/);
});