feat(aihr): persist interview human review
This commit is contained in:
@@ -64,14 +64,25 @@ export type InterviewFinishResponse = {
|
||||
};
|
||||
|
||||
export type 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;
|
||||
};
|
||||
|
||||
export type InterviewReviewRequest = {
|
||||
reviewedScore: number;
|
||||
reviewNote?: string;
|
||||
};
|
||||
|
||||
export type CandidateLinkResponse = {
|
||||
candidateId: string;
|
||||
extPartyId: string;
|
||||
@@ -126,6 +137,15 @@ export function listInterviewRecords(limit = 20): Promise<ApiResult<InterviewRec
|
||||
});
|
||||
}
|
||||
|
||||
export function reviewInterviewRecord(sessionId: string, data: InterviewReviewRequest): Promise<ApiResult<null>> {
|
||||
return request({
|
||||
url: `/api/recruit/interview/records/${encodeURIComponent(sessionId)}/review`,
|
||||
method: 'post',
|
||||
data,
|
||||
headers: { repeatSubmit: false }
|
||||
});
|
||||
}
|
||||
|
||||
export function getCandidateEmployeeLink(candidateId: string): Promise<ApiResult<CandidateLinkResponse | null>> {
|
||||
return request({
|
||||
url: '/api/recruit/interview/candidate-links',
|
||||
|
||||
@@ -174,12 +174,56 @@
|
||||
<el-table-column prop="time" label="时间" width="128" />
|
||||
<el-table-column prop="candidate" label="候选人" width="120" />
|
||||
<el-table-column prop="position" label="岗位" min-width="150" />
|
||||
<el-table-column prop="score" label="得分" width="80" />
|
||||
<el-table-column prop="score" label="AI得分" width="90" />
|
||||
<el-table-column label="人工复核分" width="110">
|
||||
<template #default="{ row }">{{ row.reviewedScore ?? '—' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="最终采用分" width="110">
|
||||
<template #default="{ row }">{{ row.finalScore ?? row.score }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="result" label="结论" width="120" />
|
||||
<el-table-column label="复核状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.reviewStatus === '已复核' ? 'success' : 'warning'" size="small">
|
||||
{{ row.reviewStatus || '待复核' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="summary" label="AI 摘要" min-width="260" />
|
||||
<el-table-column label="操作" width="100" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" :disabled="!row.sessionId" @click="openInterviewReview(row)">人工复核</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</section>
|
||||
|
||||
<el-dialog v-model="interviewReviewVisible" title="人工复核 AI 面试结果" width="520px">
|
||||
<div v-if="reviewingRecord" class="interview-review-context">
|
||||
<strong>{{ reviewingRecord.candidate }} · {{ reviewingRecord.position }}</strong>
|
||||
<span>AI 得分 {{ reviewingRecord.score }},当前最终采用分 {{ reviewingRecord.finalScore ?? reviewingRecord.score }}</span>
|
||||
</div>
|
||||
<el-form label-width="104px" class="interview-review-form">
|
||||
<el-form-item label="人工复核分" required>
|
||||
<el-input-number v-model="reviewScore" :min="0" :max="100" :precision="0" controls-position="right" />
|
||||
</el-form-item>
|
||||
<el-form-item label="复核意见">
|
||||
<el-input
|
||||
v-model="reviewNote"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
maxlength="1000"
|
||||
show-word-limit
|
||||
placeholder="记录人工判断依据,可说明是否覆盖 AI 建议"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="interviewReviewVisible = false">取消</el-button>
|
||||
<el-button type="danger" :loading="interviewReviewSaving" @click="submitInterviewReview">保存复核</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<section class="panel record-panel material-review-panel">
|
||||
<div class="panel-title title-with-extra">
|
||||
<div>
|
||||
@@ -232,6 +276,7 @@ import {
|
||||
linkCandidateEmployee,
|
||||
listCandidateMaterialsForReview,
|
||||
listInterviewRecords,
|
||||
reviewInterviewRecord,
|
||||
reviewCandidateMaterial,
|
||||
startInterview,
|
||||
submitInterviewAnswer,
|
||||
@@ -426,6 +471,11 @@ const apiQuestions = ref<InterviewQuestion[]>([]);
|
||||
const apiScoreResult = ref<CandidateSeed['scoreResult'] | null>(null);
|
||||
const answers = reactive<Record<string, string>>({});
|
||||
const records = ref<InterviewRecord[]>([...baseRecords]);
|
||||
const interviewReviewVisible = ref(false);
|
||||
const interviewReviewSaving = ref(false);
|
||||
const reviewingRecord = ref<InterviewRecord | null>(null);
|
||||
const reviewScore = ref<number | undefined>(undefined);
|
||||
const reviewNote = ref('');
|
||||
const materialReviews = ref<CandidateMaterialReview[]>([]);
|
||||
const materialStatus = ref('待审核');
|
||||
const materialLoading = ref(false);
|
||||
@@ -626,6 +676,38 @@ const loadInterviewRecords = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const openInterviewReview = (row: InterviewRecord) => {
|
||||
if (!row.sessionId) {
|
||||
ElMessage.warning('当前记录没有可复核的后端会话');
|
||||
return;
|
||||
}
|
||||
reviewingRecord.value = row;
|
||||
reviewScore.value = row.reviewedScore ?? row.finalScore ?? row.score;
|
||||
reviewNote.value = row.reviewNote ?? '';
|
||||
interviewReviewVisible.value = true;
|
||||
};
|
||||
|
||||
const submitInterviewReview = async () => {
|
||||
if (!reviewingRecord.value?.sessionId || reviewScore.value === undefined) {
|
||||
ElMessage.warning('请输入人工复核分');
|
||||
return;
|
||||
}
|
||||
interviewReviewSaving.value = true;
|
||||
try {
|
||||
await reviewInterviewRecord(reviewingRecord.value.sessionId, {
|
||||
reviewedScore: reviewScore.value,
|
||||
reviewNote: reviewNote.value.trim()
|
||||
});
|
||||
interviewReviewVisible.value = false;
|
||||
await loadInterviewRecords();
|
||||
ElMessage.success('面试结果人工复核已保存');
|
||||
} catch {
|
||||
ElMessage.error('面试结果人工复核失败');
|
||||
} finally {
|
||||
interviewReviewSaving.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const loadCandidateLink = async () => {
|
||||
candidateLink.value = null;
|
||||
linkExtPartyId.value = '';
|
||||
@@ -1196,6 +1278,30 @@ watch(candidateId, loadCandidateLink);
|
||||
width: 110px;
|
||||
}
|
||||
|
||||
.interview-review-context {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
margin-bottom: 18px;
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
background: #fff8ed;
|
||||
color: #46566e;
|
||||
|
||||
strong {
|
||||
color: #26354c;
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.interview-review-form {
|
||||
:deep(.el-input-number) {
|
||||
width: 180px;
|
||||
}
|
||||
}
|
||||
|
||||
.material-name {
|
||||
display: grid;
|
||||
gap: 2px;
|
||||
|
||||
Reference in New Issue
Block a user