Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Contribute to GitLab
Sign in
Toggle navigation
R
recruit-sys
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
李文光
recruit-sys
Commits
5f6b4553
Commit
5f6b4553
authored
Sep 01, 2026
by
李文光
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
fix: 修复快速匹配流程并优化评估报告展示
parent
8351ae19
Hide whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
265 additions
and
33 deletions
+265
-33
CandidateBatchModal.vue
vue-app/src/components/resumes/CandidateBatchModal.vue
+5
-3
CandidateCreateModal.vue
vue-app/src/components/resumes/CandidateCreateModal.vue
+1
-1
ResumeDetail.vue
vue-app/src/components/resumes/ResumeDetail.vue
+104
-29
report.js
vue-app/src/utils/report.js
+155
-0
No files found.
vue-app/src/components/resumes/CandidateBatchModal.vue
View file @
5f6b4553
...
@@ -183,20 +183,22 @@ const importBatch = () => {
...
@@ -183,20 +183,22 @@ const importBatch = () => {
tag: '新入库',
tag: '新入库',
evaluation: '批量上传入库',
evaluation: '批量上传入库',
})
})
store.localMatch(candidate.id)
created.push(candidate)
created.push(candidate)
store.logEvent('candidate_created', candidate, {
store.logEvent('candidate_created', candidate, {
stage: candidate.stage,
stage: candidate.stage,
source: candidate.source || '其他',
source: candidate.source || '其他',
batch: true,
batch: true,
})
})
})
store.candidates.unshift(...created)
store.reconcile()
created.forEach((candidate) => {
store.localMatch(candidate.id)
store.logEvent('local_match_generated', candidate, {
store.logEvent('local_match_generated', candidate, {
score: candidate.quickMatchScore || candidate.match || 0,
score: candidate.quickMatchScore || candidate.match || 0,
batch: true,
batch: true,
})
})
})
})
store.candidates.unshift(...created)
store.reconcile()
store.candidateBatchOpen = false
store.candidateBatchOpen = false
store.candidateBatchEntries = []
store.candidateBatchEntries = []
store.candidateBatchParsing = false
store.candidateBatchParsing = false
...
...
vue-app/src/components/resumes/CandidateCreateModal.vue
View file @
5f6b4553
...
@@ -151,9 +151,9 @@ const submit = async () => {
...
@@ -151,9 +151,9 @@ const submit = async () => {
tag: '新入库',
tag: '新入库',
evaluation: form.evaluation,
evaluation: form.evaluation,
})
})
store.localMatch(candidate.id)
store.candidates.unshift(candidate)
store.candidates.unshift(candidate)
store.reconcile()
store.reconcile()
store.localMatch(candidate.id)
store.logEvent('candidate_created', candidate, { stage: candidate.stage, source: candidate.source || '其他' })
store.logEvent('candidate_created', candidate, { stage: candidate.stage, source: candidate.source || '其他' })
store.logEvent('local_match_generated', candidate, { score: candidate.quickMatchScore || candidate.match || 0 })
store.logEvent('local_match_generated', candidate, { score: candidate.quickMatchScore || candidate.match || 0 })
store.selectedCandidateId = candidate.id
store.selectedCandidateId = candidate.id
...
...
vue-app/src/components/resumes/ResumeDetail.vue
View file @
5f6b4553
<
script
setup
>
<
script
setup
>
import
{
computed
,
reactive
,
ref
}
from
'vue'
import
{
computed
,
reactive
}
from
'vue'
import
{
useRecruitmentStore
}
from
'@/stores/recruitment'
import
{
useRecruitmentStore
}
from
'@/stores/recruitment'
import
{
candidateWorkflow
,
workflowStageClass
}
from
'@/utils/task'
import
{
candidateWorkflow
,
workflowStageClass
}
from
'@/utils/task'
import
{
candidateScreeningConclusion
,
quickScore
,
aiScore
}
from
'@/utils/resume'
import
{
candidateScreeningConclusion
,
quickScore
}
from
'@/utils/resume'
import
{
isAiEvaluationReport
}
from
'@/utils/scoring'
import
{
isAiEvaluationReport
}
from
'@/utils/scoring'
import
{
analysisNeedsRefresh
}
from
'@/utils/analysis'
import
{
analysisNeedsRefresh
}
from
'@/utils/analysis'
import
{
resumeDisplayName
,
candidateAge
,
candidateYears
}
from
'@/utils/candidate'
import
{
resumeDisplayName
,
candidateAge
,
candidateYears
}
from
'@/utils/candidate'
import
{
uid
}
from
'@/utils/normalize'
import
{
uid
}
from
'@/utils/normalize'
import
{
assessmentDecision
,
assessmentReportRows
,
assessmentScoreText
}
from
'@/utils/report'
import
{
showToast
}
from
'@/utils/toast'
import
{
showToast
}
from
'@/utils/toast'
const
store
=
useRecruitmentStore
()
const
store
=
useRecruitmentStore
()
...
@@ -21,6 +22,10 @@ const detailScore = computed(() => {
...
@@ -21,6 +22,10 @@ const detailScore = computed(() => {
return
store
.
resumeDetailMode
===
'ai'
?
Number
(
selected
.
value
.
analysis
?.
score
||
0
)
:
quickScore
(
selected
.
value
)
return
store
.
resumeDetailMode
===
'ai'
?
Number
(
selected
.
value
.
analysis
?.
score
||
0
)
:
quickScore
(
selected
.
value
)
})
})
const
scoreText
=
computed
(()
=>
assessmentScoreText
(
selected
.
value
,
store
.
resumeDetailMode
))
const
decisionInfo
=
computed
(()
=>
assessmentDecision
(
selected
.
value
,
store
.
resumeDetailMode
))
const
reportRows
=
computed
(()
=>
assessmentReportRows
(
selected
.
value
,
store
.
resumeDetailMode
))
const
decisionColor
=
computed
(()
=>
const
decisionColor
=
computed
(()
=>
detailScore
.
value
>=
85
?
'var(--green)'
:
detailScore
.
value
>=
75
?
'var(--yellow)'
:
'var(--rose)'
detailScore
.
value
>=
85
?
'var(--green)'
:
detailScore
.
value
>=
75
?
'var(--yellow)'
:
'var(--rose)'
)
)
...
@@ -29,7 +34,12 @@ const back = () => {
...
@@ -29,7 +34,12 @@ const back = () => {
store
.
resumeViewMode
=
'list'
store
.
resumeViewMode
=
'list'
}
}
const
tabs
=
ref
(
'overview'
)
const
tabs
=
computed
({
get
:
()
=>
store
.
resumeDetailSection
,
set
:
(
value
)
=>
{
store
.
resumeDetailSection
=
value
},
})
const
generateEval
=
async
(
candidate
)
=>
{
const
generateEval
=
async
(
candidate
)
=>
{
try
{
try
{
...
@@ -297,23 +307,42 @@ const saveCorrection = (candidate) => {
...
@@ -297,23 +307,42 @@ const saveCorrection = (candidate) => {
<div
class=
"assessment-head"
>
<div
class=
"assessment-head"
>
<div>
<div>
<span>
当前得分
</span>
<span>
当前得分
</span>
<strong
:style=
"
{ color: decisionColor }">
{{
detailScore
}}
</strong>
<strong
:style=
"
{ color: decisionColor }">
{{
scoreText
}}
</strong>
</div>
</div>
<el-tag
size=
"small"
>
{{
store
.
resumeDetailMode
===
'ai'
?
'AI 评估'
:
'本地快速匹配'
}}
</el-tag>
<el-tag
size=
"small"
>
{{
store
.
resumeDetailMode
===
'ai'
?
'AI 评估'
:
'本地快速匹配'
}}
</el-tag>
</div>
</div>
<div
class=
"assessment-decision"
:class=
"`tone-$
{decisionInfo.tone}`">
<div>
<span>
初筛建议
</span>
<strong>
{{
decisionInfo
.
text
}}
</strong>
<p>
{{
decisionInfo
.
note
}}
</p>
</div>
</div>
<div
class=
"assessment-summary"
>
<div
class=
"assessment-summary"
>
<p
v-if=
"selected.analysis?.summary"
>
{{
selected
.
analysis
.
summary
}}
</p>
<p
v-if=
"selected.analysis?.summary"
>
{{
selected
.
analysis
.
summary
}}
</p>
<p
v-else
>
快速匹配只做岗位关键词命中和缺口识别,不生成 HR 决策报告。
</p>
<p
v-else
>
快速匹配只做岗位关键词命中和缺口识别,不生成 HR 决策报告。
</p>
</div>
</div>
<div
v-if=
"selected.matchReport"
class=
"criteria-grid"
>
<div
v-if=
"selected.matchReport || selected.analysis"
class=
"assessment-table-wrap"
>
<div>
<el-table
:data=
"reportRows"
class=
"assessment-table"
>
<b>
命中
</b>
<el-table-column
label=
"维度"
width=
"130"
>
<p>
{{
(
selected
.
matchReport
.
matched
||
[]).
join
(
'、'
)
||
'暂无'
}}
</p>
<template
#
default=
"
{ row }">
</div>
<b
class=
"assessment-row-label"
>
{{
row
.
label
}}
</b>
<div>
</
template
>
<b>
缺口
</b>
</el-table-column>
<p>
{{
(
selected
.
matchReport
.
missing
||
[]).
slice
(
0
,
6
).
join
(
'、'
)
||
'暂无'
}}
</p>
<el-table-column
label=
"内容"
min-width=
"320"
>
</div>
<
template
#
default=
"{ row }"
>
<div
v-if=
"row.items.length"
class=
"assessment-items"
>
<span
v-for=
"(item, index) in row.items"
:key=
"`$
{row.label}-${index}`">
{{
item
}}
</span>
</div>
<span
v-else
class=
"assessment-empty"
>
暂无
</span>
</
template
>
</el-table-column>
<el-table-column
label=
"状态"
width=
"120"
align=
"center"
>
<
template
#
default=
"{ row }"
>
<el-tag
:type=
"row.tone"
size=
"small"
effect=
"light"
>
{{
row
.
status
}}
</el-tag>
</
template
>
</el-table-column>
</el-table>
</div>
</div>
</div>
</div>
</el-tab-pane>
</el-tab-pane>
...
@@ -560,32 +589,78 @@ const saveCorrection = (candidate) => {
...
@@ -560,32 +589,78 @@ const saveCorrection = (candidate) => {
}
}
}
}
.assessment-decision
{
display
:
flex
;
align-items
:
center
;
justify-content
:
space-between
;
gap
:
16px
;
margin-bottom
:
12px
;
padding
:
14px
16px
;
background
:
var
(
--
bg
);
border-left
:
4px
solid
var
(
--
blue
);
border-radius
:
8px
;
span
{
display
:
block
;
font-size
:
12px
;
color
:
var
(
--
muted
);
}
strong
{
display
:
block
;
margin
:
4px
0
;
font-size
:
18px
;
}
p
{
margin
:
0
;
color
:
var
(
--
muted
);
font-size
:
13px
;
}
&
.tone-success
{
border-color
:
var
(
--
green
);
}
&
.tone-warning
{
border-color
:
var
(
--
yellow
);
}
&
.tone-danger
{
border-color
:
var
(
--
rose
);
}
}
.assessment-summary
{
.assessment-summary
{
margin-bottom
:
12px
;
margin-bottom
:
12px
;
color
:
var
(
--
muted
);
color
:
var
(
--
muted
);
}
}
.criteria-grid
{
.assessment-table-wrap
{
display
:
grid
;
margin-top
:
4px
;
grid-template-columns
:
repeat
(
2
,
1fr
);
}
gap
:
12px
;
>
div
{
.assessment-row-label
{
background
:
var
(
--
bg
);
font-size
:
14px
;
border-radius
:
8px
;
}
padding
:
12px
;
b
{
.assessment-items
{
font-size
:
13px
;
display
:
flex
;
}
flex-direction
:
column
;
p
{
gap
:
6px
;
margin
:
6px
0
0
;
color
:
var
(
--
muted
);
span
{
font-size
:
13px
;
color
:
var
(
--
muted
);
}
font-size
:
13px
;
line-height
:
1
.5
;
}
}
}
}
.assessment-empty
{
color
:
var
(
--
muted
);
font-size
:
13px
;
}
.span-2
{
.span-2
{
grid-column
:
span
2
;
grid-column
:
span
2
;
}
}
...
...
vue-app/src/utils/report.js
0 → 100644
View file @
5f6b4553
function
listItems
(
value
)
{
return
Array
.
isArray
(
value
)
?
value
.
filter
((
item
)
=>
String
(
item
||
''
).
trim
())
:
[]
}
export
function
assessmentScoreText
(
candidate
=
{},
mode
=
'ai'
)
{
const
analysis
=
mode
===
'ai'
?
candidate
.
analysis
||
{}
:
{}
const
raw
=
mode
===
'ai'
&&
analysis
.
score
!=
null
?
analysis
.
score
:
(
candidate
.
quickMatchScore
??
candidate
.
match
??
analysis
.
score
??
null
)
if
(
raw
===
null
||
raw
===
''
||
Number
.
isNaN
(
Number
(
raw
)))
return
'--/100'
return
`
${
Math
.
round
(
Number
(
raw
))}
/100`
}
export
function
assessmentDecision
(
candidate
=
{},
mode
=
'ai'
)
{
const
analysis
=
mode
===
'ai'
?
candidate
.
analysis
||
{}
:
{}
const
hasReport
=
Boolean
(
candidate
.
matchReport
||
(
mode
===
'ai'
&&
analysis
.
score
!=
null
))
if
(
!
hasReport
)
{
return
{
text
:
'暂未生成评估'
,
tone
:
'info'
,
note
:
'请先为候选人选择关联岗位,并生成快速匹配或 AI 评估报告。'
,
}
}
const
decisionText
=
String
(
analysis
.
decision
||
''
)
const
score
=
Number
(
analysis
.
score
??
candidate
.
quickMatchScore
??
candidate
.
match
??
0
)
if
(
/人才池|暂缓|不建议/
.
test
(
decisionText
))
{
return
{
text
:
'暂缓,不建议进入初筛'
,
tone
:
'danger'
,
note
:
'当前匹配证据不足或存在硬性缺口,建议进入人才池后择机复评。'
,
}
}
if
(
/复核/
.
test
(
decisionText
))
{
return
{
text
:
'建议复核后进入初筛'
,
tone
:
'warning'
,
note
:
'总体条件可接受,但存在待确认项,建议 HR 复核后再推进。'
,
}
}
if
(
/优先/
.
test
(
decisionText
))
{
return
{
text
:
'建议进入初筛'
,
tone
:
'success'
,
note
:
'匹配证据较充分,可优先安排初筛或面试。'
,
}
}
if
(
score
>=
85
)
{
return
{
text
:
'建议进入初筛'
,
tone
:
'success'
,
note
:
'匹配度较高,可优先安排初筛或面试。'
,
}
}
if
(
score
>=
72
)
{
return
{
text
:
'建议复核后进入初筛'
,
tone
:
'warning'
,
note
:
'总体条件可接受,但存在待确认项,建议 HR 复核后再推进。'
,
}
}
return
{
text
:
'暂缓,不建议进入初筛'
,
tone
:
'danger'
,
note
:
'当前匹配证据不足或存在硬性缺口,建议进入人才池后择机复评。'
,
}
}
export
function
assessmentReportRows
(
candidate
=
{},
mode
=
'ai'
)
{
const
analysis
=
mode
===
'ai'
?
candidate
.
analysis
||
{}
:
{}
const
report
=
candidate
.
matchReport
||
{}
const
matched
=
listItems
(
report
.
matched
)
const
missing
=
listItems
(
report
.
missing
)
const
mustMatched
=
listItems
(
report
.
mustMatched
)
const
mustMissing
=
listItems
(
report
.
mustMissing
)
const
niceMatched
=
listItems
(
report
.
niceMatched
)
const
knockoutHit
=
listItems
(
report
.
knockoutHit
)
const
analysisBased
=
Boolean
(
analysis
.
reportVersion
||
analysis
.
hardMatch
||
analysis
.
risks
)
const
evalItems
=
[]
const
breakdown
=
analysis
.
scoreBreakdown
||
{}
const
breakdownParts
=
[]
if
(
breakdown
.
hardMatchScore
!=
null
)
breakdownParts
.
push
(
`硬性匹配
${
breakdown
.
hardMatchScore
}
/35`
)
if
(
breakdown
.
experienceScore
!=
null
)
breakdownParts
.
push
(
`经验匹配
${
breakdown
.
experienceScore
}
/25`
)
if
(
breakdown
.
evidenceScore
!=
null
)
breakdownParts
.
push
(
`项目证据
${
breakdown
.
evidenceScore
}
/20`
)
if
(
breakdown
.
competencyScore
!=
null
)
breakdownParts
.
push
(
`胜任力
${
breakdown
.
competencyScore
}
/10`
)
if
(
breakdown
.
riskAdjustment
!=
null
)
breakdownParts
.
push
(
`风险调整
${
breakdown
.
riskAdjustment
}
`
)
if
(
breakdownParts
.
length
)
evalItems
.
push
(
`得分拆解:
${
breakdownParts
.
join
(
'、'
)}
`
)
if
(
analysisBased
)
{
evalItems
.
push
(...
listItems
(
analysis
.
hardMatch
).
slice
(
0
,
6
))
evalItems
.
push
(...
listItems
(
analysis
.
experienceMatch
).
slice
(
0
,
3
))
evalItems
.
push
(
...
listItems
(
analysis
.
projectEvidence
)
.
slice
(
0
,
4
)
.
map
((
item
)
=>
`项目证据:
${
item
}
`
)
)
evalItems
.
push
(...
listItems
(
analysis
.
competencyFrame
).
slice
(
0
,
6
))
}
else
{
if
(
matched
.
length
)
evalItems
.
push
(
`关键词命中:
${
matched
.
slice
(
0
,
8
).
join
(
'、'
)}
`
)
if
(
report
.
sourceQuality
===
'filename_only'
)
evalItems
.
push
(
'仅解析到简历文件名,正文证据不足'
)
}
const
matchItems
=
[]
if
(
matched
.
length
)
matchItems
.
push
(
`岗位关键词命中(
${
matched
.
length
}
项):
${
matched
.
slice
(
0
,
8
).
join
(
'、'
)}
`
)
if
(
mustMatched
.
length
)
matchItems
.
push
(
`硬性条件命中:
${
mustMatched
.
join
(
'、'
)}
`
)
if
(
niceMatched
.
length
)
matchItems
.
push
(
`加分项命中:
${
niceMatched
.
join
(
'、'
)}
`
)
if
(
report
.
jobTitleMatched
)
matchItems
.
push
(
'岗位标题匹配'
)
if
(
report
.
sourceQuality
===
'filename_only'
)
matchItems
.
push
(
'仅有简历文件名,缺少正文匹配证据'
)
matchItems
.
push
(...
listItems
(
analysis
.
strengths
).
slice
(
0
,
4
))
if
(
!
matchItems
.
length
)
matchItems
.
push
(
'暂无匹配点'
)
const
riskItems
=
[]
const
risks
=
listItems
(
analysis
.
risks
)
if
(
risks
.
length
)
{
riskItems
.
push
(...
risks
.
slice
(
0
,
6
))
}
else
{
if
(
knockoutHit
.
length
)
riskItems
.
push
(
`触发淘汰项:
${
knockoutHit
.
join
(
'、'
)}
`
)
if
(
mustMissing
.
length
)
riskItems
.
push
(
`硬性条件缺口:
${
mustMissing
.
join
(
'、'
)}
`
)
if
(
missing
.
length
)
riskItems
.
push
(
`简历暂未明确:
${
missing
.
slice
(
0
,
6
).
join
(
'、'
)}
`
)
}
if
(
!
riskItems
.
length
)
riskItems
.
push
(
'暂无明确风险或待确认项'
)
let
questionItems
=
listItems
(
analysis
.
interviewQuestions
).
slice
(
0
,
5
)
if
(
!
questionItems
.
length
&&
missing
.
length
)
{
questionItems
=
[
`建议初筛时确认:
${
missing
.
slice
(
0
,
4
).
join
(
'、'
)}
`
]
}
const
hasRisk
=
Boolean
(
knockoutHit
.
length
||
mustMissing
.
length
||
risks
.
length
)
return
[
{
label
:
'评估点'
,
items
:
evalItems
,
status
:
evalItems
.
length
?
'评估依据'
:
'待补充'
,
tone
:
'info'
,
},
{
label
:
'匹配点'
,
items
:
matchItems
,
status
:
matched
.
length
?
`命中
${
matched
.
length
}
项`
:
'待补强'
,
tone
:
matched
.
length
?
'success'
:
'warning'
,
},
{
label
:
'风险/待确认点'
,
items
:
riskItems
,
status
:
hasRisk
?
'需关注'
:
'暂无'
,
tone
:
hasRisk
?
'danger'
:
'success'
,
},
{
label
:
'面试确认点'
,
items
:
questionItems
,
status
:
questionItems
.
length
?
'待核实'
:
'无需确认'
,
tone
:
questionItems
.
length
?
'warning'
:
'info'
,
},
]
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment