feat: restore learning feedback and knowledge capabilities
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user