fix(aihr): separate onboarding and candidate review states

This commit is contained in:
2026-07-15 02:39:29 +08:00
parent 21a13d806c
commit 40574c204d
13 changed files with 487 additions and 13 deletions
@@ -25,7 +25,7 @@
<text class="score-value">{{ latestRecord.score }} 分</text>
<text class="score-meta">AI 初评 · {{ latestRecord.result }}</text>
</view>
<text class="review-badge">{{ reviewStatusLabel }}</text>
<text class="review-badge">{{ interviewReviewLabel }}</text>
</view>
<text class="score-summary">{{ latestRecord.summary }}</text>
<text class="score-evidence">{{ questionRecords.length }} 道题 · 已保存真实回答</text>
@@ -45,8 +45,13 @@
</view>
<view class="review-status-card">
<text class="review-status-title">人工复核状态</text>
<text class="review-status-desc">{{ reviewStatusDescription }}</text>
<text class="review-status-title">面试人工复核</text>
<text class="review-status-desc">{{ interviewReviewDescription }}</text>
</view>
<view class="review-status-card material-review-card">
<text class="review-status-title">补充资料审核</text>
<text class="review-status-desc">{{ materialReviewDescription }}</text>
</view>
<button class="primary-button home-button" @click="goBack">返回候选人中心</button>
@@ -78,10 +83,16 @@ const message = ref('');
const latestRecord = computed(() => records.value[0] || null);
const latestMaterial = computed(() => materials.value[0] || null);
const questionRecords = computed(() => latestRecord.value?.questions || []);
const reviewStatusLabel = computed(() => latestMaterial.value?.status === '已通过' ? '人工复核完成' : '人工复核中');
const reviewStatusDescription = computed(() => latestMaterial.value?.status === '已通过'
? 'HR 已完成 AI 评分与补充资料复核,后续通知会同步更新。'
: 'HR 正在复核 AI 评分与补充资料,预计 1-2 个工作日更新。');
const interviewReviewLabel = computed(() => latestRecord.value?.reviewStatus === '已复核' ? '面试已复核' : '面试待复核');
const interviewReviewDescription = computed(() => latestRecord.value?.reviewStatus === '已复核'
? 'HR 已完成本次 AI 面试的人工复核,最终采用分已写入记录。'
: 'AI 初评仅作辅助参考,HR 尚未完成本次面试的人工复核。');
const materialReviewDescription = computed(() => {
if (!latestMaterial.value) return '暂无补充资料,完成上传后由 HR 进行审核。';
if (latestMaterial.value.status === '已通过') return 'HR 已通过最近一份补充资料审核。';
if (latestMaterial.value.status === '已驳回') return '最近一份补充资料未通过,请按审核意见重新提交。';
return '最近一份补充资料正在等待 HR 审核。';
});
const clearData = () => {
records.value = [];
+194 -3
View File
@@ -123,6 +123,51 @@
</view>
</view>
<view v-if="loggedIn" class="panel onboarding-panel">
<view class="panel-head">
<view>
<text class="panel-title">岗前 / 入职任务</text>
<text class="panel-caption">只展示组织系统已正式派发的任务</text>
</view>
<button class="ghost-button compact-button" :disabled="onboardingLoading" @click="loadOnboarding">
{{ onboardingLoading ? '加载中' : '刷新' }}
</button>
</view>
<view v-if="onboardTasks.length" class="onboarding-list">
<view v-for="item in onboardTasks" :key="item.id" class="onboarding-row">
<view class="onboarding-copy">
<text class="onboarding-title">{{ item.taskType }} · {{ item.positionCode || positionProfile.value }}</text>
<text class="onboarding-meta">{{ item.status }}{{ item.projectCode ? ` · ${item.projectCode}` : '' }}</text>
<text v-if="item.dueTime" class="onboarding-meta">截止:{{ item.dueTime }}</text>
</view>
<text class="onboarding-status" :class="item.status === '已完成' ? 'done' : 'pending'">{{ item.status }}</text>
</view>
</view>
<view v-else-if="!onboardingLoading" class="empty">暂无正式岗前/入职任务,等待 HR 或组织系统派发</view>
</view>
<view v-if="loggedIn" class="panel qualification-panel">
<view class="panel-head">
<view>
<text class="panel-title">上岗资格</text>
<text class="panel-caption">资格结论必须来自正式证据,不以 AI 训练分数代替</text>
</view>
<button class="ghost-button compact-button" :disabled="qualificationLoading" @click="loadQualification">
{{ qualificationLoading ? '加载中' : '刷新' }}
</button>
</view>
<view class="qualification-status" :class="qualificationTone">
<text class="qualification-label">{{ qualificationStatusLabel }}</text>
<text class="qualification-note">{{ qualificationNote }}</text>
</view>
<view v-if="qualification?.gates?.length" class="qualification-list">
<view v-for="gate in qualification.gates" :key="gate.id" class="qualification-row">
<text class="qualification-cert">{{ gate.certId }}</text>
<text class="qualification-meta">{{ gate.passed ? '已通过' : '未通过' }}{{ gate.validThru ? ` · 有效至 ${gate.validThru}` : '' }}</text>
</view>
</view>
</view>
<view v-if="loggedIn" class="panel">
<view class="panel-head">
<text class="panel-title">岗位 SOP</text>
@@ -206,12 +251,14 @@ import { computed, ref } from 'vue';
import { onShow } from '@dcloudio/uni-app';
import type {
KnowledgeSearchResponse,
OnboardTask,
PositionSopSummary,
PracticeAssignment,
PracticeMistake,
PracticePrepCard
PracticePrepCard,
QualificationSummary
} from '@/types/api';
import { getPracticeAssignments, getPracticeMistakes, getPracticePrepCard } from '@/services/practice';
import { getOnboardTasks, getPracticeAssignments, getPracticeMistakes, getPracticePrepCard, getQualification } from '@/services/practice';
import { getPositionSopSummary, searchKnowledge } from '@/services/knowledge';
import { getSelectedPositionProfile, isLoggedIn, rememberLoginRedirect } from '@/services/auth';
import { buildLearningTopics, selectLearningTopic } from '@/services/learning';
@@ -237,6 +284,10 @@ const activeMistakeCause = ref('全部');
const prepLoading = ref(false);
const knowledgeLoading = ref(false);
const positionProfile = ref(getSelectedPositionProfile());
const onboardTasks = ref<OnboardTask[]>([]);
const qualification = ref<QualificationSummary | null>(null);
const onboardingLoading = ref(false);
const qualificationLoading = ref(false);
const defaultTopic = computed(() => `${positionProfile.value.value} 岗前 SOP`);
const topics = computed(() => buildLearningTopics(
@@ -258,6 +309,33 @@ const visibleMistakes = computed(() => activeMistakeCause.value === '全部'
? mistakes.value
: mistakes.value.filter((item) => item.cause === activeMistakeCause.value));
const qualificationStatusLabel = computed(() => {
switch (qualification.value?.status) {
case 'PASSED': return '已通过';
case 'BLOCKED': return '未通过';
case 'EXPIRED': return '已过期';
default: return '未配置';
}
});
const qualificationTone = computed(() => {
switch (qualification.value?.status) {
case 'PASSED': return 'passed';
case 'BLOCKED': return 'blocked';
case 'EXPIRED': return 'expired';
default: return 'not-configured';
}
});
const qualificationNote = computed(() => {
switch (qualification.value?.status) {
case 'PASSED': return '当前资格记录已通过正式复核。';
case 'BLOCKED': return '存在未通过的资格门禁,请联系主管或 HR。';
case 'EXPIRED': return '资格记录已过有效期,请重新提交证据复核。';
default: return '当前没有正式资格规则或证据记录,不能据此判断已具备上岗资格。';
}
});
const refreshAuthState = () => {
loggedIn.value = isLoggedIn();
positionProfile.value = getSelectedPositionProfile();
@@ -327,6 +405,32 @@ const loadMistakes = async () => {
}
};
const loadOnboarding = async () => {
if (!loggedIn.value) return;
onboardingLoading.value = true;
try {
onboardTasks.value = await getOnboardTasks();
} catch (error) {
onboardTasks.value = [];
handleRequestError(error, '岗前任务加载失败');
} finally {
onboardingLoading.value = false;
}
};
const loadQualification = async () => {
if (!loggedIn.value) return;
qualificationLoading.value = true;
try {
qualification.value = await getQualification();
} catch (error) {
qualification.value = { status: 'NOT_CONFIGURED', positionCode: '', gates: [] };
handleRequestError(error, '上岗资格加载失败');
} finally {
qualificationLoading.value = false;
}
};
const loadPrepCard = async () => {
if (!loggedIn.value) return;
prepLoading.value = true;
@@ -370,6 +474,8 @@ const loadPage = async () => {
if (!loggedIn.value) {
assignments.value = [];
mistakes.value = [];
onboardTasks.value = [];
qualification.value = null;
prepCard.value = null;
knowledgeResult.value = null;
positionSopSummary.value = null;
@@ -379,7 +485,7 @@ const loadPage = async () => {
return;
}
refreshAuthState();
await Promise.all([loadAssignments(), loadMistakes()]);
await Promise.all([loadAssignments(), loadMistakes(), loadOnboarding(), loadQualification()]);
activeTopic.value = selectLearningTopic(assignments.value, defaultTopic.value);
await Promise.all([
loadPositionSopSummary(),
@@ -465,6 +571,91 @@ onShow(() => {
line-height: 1.5;
}
.onboarding-list,
.qualification-list {
display: grid;
gap: 8px;
}
.onboarding-row,
.qualification-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 10px 12px;
border-radius: 8px;
background: #f7f9fc;
}
.onboarding-copy {
display: grid;
gap: 3px;
min-width: 0;
}
.onboarding-title,
.qualification-cert {
color: #1f2d3d;
font-size: 13px;
font-weight: 700;
}
.onboarding-meta,
.qualification-meta {
color: #7b8494;
font-size: 12px;
line-height: 1.45;
}
.onboarding-status {
flex: none;
font-size: 12px;
font-weight: 700;
}
.onboarding-status.pending {
color: #9a6700;
}
.onboarding-status.done {
color: #16845b;
}
.qualification-status {
display: grid;
gap: 3px;
margin-bottom: 10px;
padding: 10px 12px;
border-radius: 8px;
}
.qualification-status.not-configured {
background: #fff8e6;
color: #8a5a00;
}
.qualification-status.passed {
background: #ecf9f2;
color: #16764f;
}
.qualification-status.blocked,
.qualification-status.expired {
background: #fff0f0;
color: #b42318;
}
.qualification-label {
font-size: 13px;
font-weight: 700;
}
.qualification-note {
font-size: 12px;
line-height: 1.5;
}
.mistake-list {
display: grid;
gap: 10px;
+16
View File
@@ -10,6 +10,8 @@ import type {
ReviewAdviceRequest,
ReviewDetail,
PracticeAssignment,
OnboardTask,
QualificationSummary,
PracticeAssignmentRequest,
DailyDrillAnswerResponse,
PracticeAlert,
@@ -170,6 +172,20 @@ export const getPracticeAssignments = (ownOnly = false) => {
});
};
export const getOnboardTasks = () =>
apiRequest<OnboardTask[]>({
url: '/api/aihr/mobile/onboard/tasks',
method: 'GET',
timeout: 15000
});
export const getQualification = () =>
apiRequest<QualificationSummary>({
url: '/api/aihr/mobile/qualification',
method: 'GET',
timeout: 15000
});
export const createPracticeAssignment = (data: PracticeAssignmentRequest) =>
apiRequest<PracticeAssignment>({
url: '/api/aihr/mobile/practice/assignments',
+39
View File
@@ -315,6 +315,39 @@ export interface PracticeAssignment {
audioOssId?: number | string;
}
export interface OnboardTask {
id: number;
projectCode?: string | null;
positionCode?: string | null;
sopId?: number | null;
courseId?: number | null;
taskType: string;
status: string;
assignBy?: string | null;
assignedTime?: string | null;
dueTime?: string | null;
completedTime?: string | null;
evidenceRef?: string | null;
}
export interface QualificationGate {
id: number;
projectCode?: string | null;
positionCode: string;
certId: string;
passed: boolean;
validThru?: string | null;
evidenceRef?: string | null;
reviewer?: string | null;
reviewedTime?: string | null;
}
export interface QualificationSummary {
status: 'NOT_CONFIGURED' | 'BLOCKED' | 'EXPIRED' | 'PASSED' | string;
positionCode?: string | null;
gates: QualificationGate[];
}
export interface PracticeAssignmentRequest {
extPartyId: string;
traineeName?: string;
@@ -458,10 +491,16 @@ export interface InterviewDimension {
}
export interface InterviewRecord {
sessionId?: string;
time: string;
candidate: string;
position: string;
score: number;
reviewedScore?: number | null;
finalScore?: number;
reviewStatus?: string;
reviewer?: string;
reviewNote?: string;
result: string;
summary: string;
questions?: InterviewQuestionRecord[];