feat: restore learning feedback and knowledge capabilities

This commit is contained in:
2026-07-18 00:50:11 +08:00
parent 876a23a123
commit 6bdc86e3fc
152 changed files with 30337 additions and 953 deletions
+192 -28
View File
@@ -156,6 +156,26 @@
<view v-else-if="!onboardingLoading" class="empty">暂无正式岗前/入职任务,等待 HR 或组织系统派发</view>
</view>
<view v-if="loggedIn" class="panel exam-panel">
<view class="panel-head">
<view>
<text class="panel-title">考试任务</text>
<text class="panel-caption">主管或经理发布的岗位考试,提交后自动评分并保留题目版本</text>
</view>
<button class="ghost-button compact-button" :disabled="examLoading" @click="loadExams">{{ examLoading ? '加载中' : '刷新' }}</button>
</view>
<view v-if="exams.length" class="exam-list">
<view v-for="item in exams" :key="item.id" class="exam-row">
<view class="exam-copy">
<text class="exam-title">{{ item.title }}</text>
<text class="exam-meta">{{ item.status }} · {{ item.questionCount }}题 · 及格{{ item.passingScore }}分{{ item.dueDate ? ` · 截止${item.dueDate}` : '' }}</text>
</view>
<button class="secondary-button compact-button" @click="openExam(item.id)">{{ item.submissionCount ? '再次查看' : '去考试' }}</button>
</view>
</view>
<view v-else-if="!examLoading" class="empty">暂无考试任务</view>
</view>
<view v-if="loggedIn" class="panel qualification-panel">
<view class="panel-head">
<view>
@@ -258,7 +278,7 @@
<script setup lang="ts">
import { computed, ref } from 'vue';
import { onShow } from '@dcloudio/uni-app';
import { onHide, onShow, onUnload } from '@dcloudio/uni-app';
import type {
KnowledgeSearchResponse,
OnboardTask,
@@ -266,7 +286,8 @@ import type {
PracticeAssignment,
PracticeMistake,
PracticePrepCard,
QualificationSummary
QualificationSummary,
ExamSummary
} from '@/types/api';
import { completeOnboardTask, getOnboardTasks, getPracticeAssignments, getPracticeMistakes, getPracticePrepCard, getQualification } from '@/services/practice';
import { getPositionSopSummary, searchKnowledge } from '@/services/knowledge';
@@ -274,6 +295,8 @@ import { getSelectedPositionProfile, isLoggedIn, rememberLoginRedirect } from '@
import { buildLearningTopics, selectLearningTopic } from '@/services/learning';
import { navigateBackToToday, resetPageScroll } from '@/services/navigation';
import { ensureEmployeePosition } from '@/services/position';
import { listMyExams } from '@/services/exam';
import { createPageRequestScope, currentAccountKey, type RequestScopeTicket } from '@/services/request-scope';
const defaultScenarioId = 'complaint-water';
const pendingAssignmentKey = 'aihr_pending_assignment_id';
@@ -299,6 +322,10 @@ const qualification = ref<QualificationSummary | null>(null);
const onboardingLoading = ref(false);
const onboardingCompletingId = ref<number | null>(null);
const qualificationLoading = ref(false);
const exams = ref<ExamSummary[]>([]);
const examLoading = ref(false);
const requests = createPageRequestScope();
let visibleAccountKey = '';
const defaultTopic = computed(() => `${positionProfile.value.value} 岗前 SOP`);
const topics = computed(() => buildLearningTopics(
@@ -390,112 +417,166 @@ const cleanOrderedText = (text = '') =>
.replace(/^(?:[一二三四五六七八九十]+[、..]|[0-9]+[.)、..])\s*/, '')
.trim();
const loadAssignments = async () => {
const loadAssignmentsRequest = async (request: RequestScopeTicket) => {
if (!loggedIn.value) return;
assignmentLoading.value = true;
try {
assignments.value = await getPracticeAssignments(true);
const nextAssignments = await getPracticeAssignments(true);
if (!requests.isCurrent(request)) return;
assignments.value = nextAssignments;
} catch (error) {
if (!requests.isCurrent(request)) return;
assignments.value = [];
handleRequestError(error, '学习任务加载失败');
} finally {
assignmentLoading.value = false;
if (requests.isCurrent(request)) assignmentLoading.value = false;
}
};
const loadMistakes = async () => {
const loadAssignments = () => loadAssignmentsRequest(requests.begin('assignments'));
const loadMistakesRequest = async (request: RequestScopeTicket) => {
if (!loggedIn.value) return;
mistakeLoading.value = true;
try {
mistakes.value = await getPracticeMistakes();
const nextMistakes = await getPracticeMistakes();
if (!requests.isCurrent(request)) return;
mistakes.value = nextMistakes;
} catch (error) {
if (!requests.isCurrent(request)) return;
mistakes.value = [];
handleRequestError(error, '错题本加载失败');
} finally {
mistakeLoading.value = false;
if (requests.isCurrent(request)) mistakeLoading.value = false;
}
};
const loadOnboarding = async () => {
const loadMistakes = () => loadMistakesRequest(requests.begin('mistakes'));
const loadOnboardingRequest = async (request: RequestScopeTicket) => {
if (!loggedIn.value) return;
onboardingLoading.value = true;
try {
onboardTasks.value = await getOnboardTasks();
const nextTasks = await getOnboardTasks();
if (!requests.isCurrent(request)) return;
onboardTasks.value = nextTasks;
} catch (error) {
if (!requests.isCurrent(request)) return;
onboardTasks.value = [];
exams.value = [];
handleRequestError(error, '岗前任务加载失败');
} finally {
onboardingLoading.value = false;
if (requests.isCurrent(request)) onboardingLoading.value = false;
}
};
const loadOnboarding = () => loadOnboardingRequest(requests.begin('onboarding'));
const completeOnboarding = async (task: OnboardTask) => {
if (onboardingCompletingId.value !== null || task.status === '已完成') return;
const request = requests.begin('complete-onboarding');
onboardingCompletingId.value = task.id;
try {
await completeOnboardTask(task.id);
if (!requests.isCurrent(request)) return;
onboardTasks.value = onboardTasks.value.map((item) => item.id === task.id
? { ...item, status: '已完成', completedTime: new Date().toISOString() }
: item);
message.value = '任务已记录为完成,正式资格仍以 HR 复核为准';
} catch (error) {
if (!requests.isCurrent(request)) return;
handleRequestError(error, '任务完成记录失败');
} finally {
onboardingCompletingId.value = null;
if (requests.isCurrent(request)) onboardingCompletingId.value = null;
}
};
const loadQualification = async () => {
const loadQualificationRequest = async (request: RequestScopeTicket) => {
if (!loggedIn.value) return;
qualificationLoading.value = true;
try {
qualification.value = await getQualification();
const nextQualification = await getQualification();
if (!requests.isCurrent(request)) return;
qualification.value = nextQualification;
} catch (error) {
if (!requests.isCurrent(request)) return;
qualification.value = { status: 'NOT_CONFIGURED', positionCode: '', gates: [] };
handleRequestError(error, '上岗资格加载失败');
} finally {
qualificationLoading.value = false;
if (requests.isCurrent(request)) qualificationLoading.value = false;
}
};
const loadPrepCard = async () => {
const loadQualification = () => loadQualificationRequest(requests.begin('qualification'));
const loadExamsRequest = async (request: RequestScopeTicket) => {
if (!loggedIn.value) return;
examLoading.value = true;
try {
const nextExams = await listMyExams();
if (!requests.isCurrent(request)) return;
exams.value = nextExams;
} catch (error) {
if (!requests.isCurrent(request)) return;
exams.value = [];
handleRequestError(error, '考试任务加载失败');
} finally {
if (requests.isCurrent(request)) examLoading.value = false;
}
};
const loadExams = () => loadExamsRequest(requests.begin('exams'));
const loadPrepCardRequest = async (request: RequestScopeTicket) => {
if (!loggedIn.value) return;
prepLoading.value = true;
try {
prepCard.value = await getPracticePrepCard(prepScenarioId.value);
const nextPrepCard = await getPracticePrepCard(prepScenarioId.value);
if (!requests.isCurrent(request)) return;
prepCard.value = nextPrepCard;
} catch (error) {
if (!requests.isCurrent(request)) return;
prepCard.value = null;
handleRequestError(error, '预习卡加载失败');
} finally {
prepLoading.value = false;
if (requests.isCurrent(request)) prepLoading.value = false;
}
};
const loadKnowledge = async (topic: string) => {
const loadPrepCard = () => loadPrepCardRequest(requests.begin('prep-card'));
const loadKnowledge = async (topic: string, request: RequestScopeTicket = requests.begin('knowledge')) => {
if (!loggedIn.value || knowledgeLoading.value) return;
activeTopic.value = topic;
knowledgeLoading.value = true;
try {
knowledgeResult.value = await searchKnowledge(topic);
const nextKnowledge = await searchKnowledge(topic);
if (!requests.isCurrent(request)) return;
knowledgeResult.value = nextKnowledge;
} catch (error) {
if (!requests.isCurrent(request)) return;
knowledgeResult.value = null;
handleRequestError(error, '岗位 SOP 检索失败');
} finally {
knowledgeLoading.value = false;
if (requests.isCurrent(request)) knowledgeLoading.value = false;
}
};
const loadPositionSopSummary = async () => {
const loadPositionSopSummary = async (request: RequestScopeTicket = requests.begin('position-sop')) => {
if (!loggedIn.value) return;
try {
positionSopSummary.value = await getPositionSopSummary(positionProfile.value.value);
const nextSummary = await getPositionSopSummary(positionProfile.value.value);
if (!requests.isCurrent(request)) return;
positionSopSummary.value = nextSummary;
} catch (error) {
if (!requests.isCurrent(request)) return;
positionSopSummary.value = null;
handleRequestError(error, '岗位学习范围加载失败');
}
};
const loadPage = async () => {
const pageRequest = requests.begin('page');
refreshAuthState();
message.value = '';
if (!loggedIn.value) {
@@ -508,19 +589,49 @@ const loadPage = async () => {
positionSopSummary.value = null;
return;
}
if (!(await ensureEmployeePosition('/pages/user/learning/index'))) {
if (!(await ensureEmployeePosition('/pages/user/learning/index')) || !requests.isCurrent(pageRequest)) {
return;
}
refreshAuthState();
await Promise.all([loadAssignments(), loadMistakes(), loadOnboarding(), loadQualification()]);
await Promise.all([
loadAssignmentsRequest(requests.begin('assignments')),
loadMistakesRequest(requests.begin('mistakes')),
loadOnboardingRequest(requests.begin('onboarding')),
loadQualificationRequest(requests.begin('qualification')),
loadExamsRequest(requests.begin('exams'))
]);
if (!requests.isCurrent(pageRequest)) return;
activeTopic.value = selectLearningTopic(assignments.value, defaultTopic.value);
await Promise.all([
loadPositionSopSummary(),
loadKnowledge(activeTopic.value),
loadPrepCard()
loadPositionSopSummary(requests.begin('position-sop')),
loadKnowledge(activeTopic.value, requests.begin('knowledge')),
loadPrepCardRequest(requests.begin('prep-card'))
]);
};
const invalidateRequests = () => {
requests.invalidate();
assignmentLoading.value = false;
mistakeLoading.value = false;
prepLoading.value = false;
knowledgeLoading.value = false;
onboardingLoading.value = false;
onboardingCompletingId.value = null;
qualificationLoading.value = false;
examLoading.value = false;
};
const clearPageData = () => {
assignments.value = [];
mistakes.value = [];
onboardTasks.value = [];
qualification.value = null;
prepCard.value = null;
knowledgeResult.value = null;
positionSopSummary.value = null;
exams.value = [];
};
const goLogin = () => {
rememberLoginRedirect('/pages/user/learning/index');
uni.navigateTo({ url: '/pages/auth/login/index' });
@@ -537,6 +648,10 @@ const openMistake = (item: PracticeMistake) => {
uni.switchTab({ url: '/pages/user/practice/index' });
};
const openExam = (id: number) => {
uni.navigateTo({ url: `/pages/user/exam/index?id=${id}` });
};
const goPractice = () => {
if (firstPracticeAssignment.value) {
uni.setStorageSync(pendingAssignmentKey, String(firstPracticeAssignment.value.id));
@@ -554,9 +669,19 @@ const goCases = () => {
};
onShow(() => {
const nextAccountKey = currentAccountKey();
if (visibleAccountKey && visibleAccountKey !== nextAccountKey) {
clearPageData();
message.value = '';
}
visibleAccountKey = nextAccountKey;
requests.activate();
resetPageScroll();
void loadPage();
});
onHide(invalidateRequests);
onUnload(invalidateRequests);
</script>
<style scoped>
@@ -571,6 +696,45 @@ onShow(() => {
font-size: 12px;
}
.exam-list {
display: grid;
gap: 10px;
margin-top: 12px;
}
.exam-row {
display: flex;
gap: 12px;
align-items: center;
justify-content: space-between;
padding: 12px;
border: 1px solid var(--line);
border-radius: 10px;
background: #fafcff;
}
.exam-copy {
min-width: 0;
flex: 1;
}
.exam-title,
.exam-meta {
display: block;
}
.exam-title {
font-weight: 900;
line-height: 1.4;
}
.exam-meta {
margin-top: 5px;
color: var(--muted);
font-size: 12px;
line-height: 1.5;
}
.position-sop-status {
display: grid;
gap: 4px;