fix(aihr): separate onboarding and candidate review states
This commit is contained in:
@@ -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 = [];
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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[];
|
||||
|
||||
Reference in New Issue
Block a user