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
9f16374c
Commit
9f16374c
authored
Sep 04, 2026
by
李文光
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: 需求22 补全——简历库/人才池批量移入认领与打标签, 批量入库按 file_hash 库级去重
parent
22f1e111
Show whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
262 additions
and
12 deletions
+262
-12
CONTEXT.md
CONTEXT.md
+3
-2
requirements_from_interview.md
docs/requirements_from_interview.md
+7
-5
CandidateBatchModal.vue
vue-app/src/components/resumes/CandidateBatchModal.vue
+21
-3
ResumeList.vue
vue-app/src/components/resumes/ResumeList.vue
+136
-2
PoolView.vue
vue-app/src/views/PoolView.vue
+95
-0
No files found.
CONTEXT.md
View file @
9f16374c
...
...
@@ -18,8 +18,9 @@
## 人才池 / 总库
-
**人才池(Talent Pool)= 总库(Company Pool)**
:公司级共享候选人池,不属于任何个人工作区,所有招聘账号可见——即需求 1 的「总库」。
-
**移入人才池(Move to Pool)**
:把候选人从个人工作区迁入公司人才池,原流程与待办随之结束。
-
**认领 / 复活(Claim / Resurrect)**
:把人才池中的候选人迁入本人工作区,重新开始招聘流程。
-
**移入人才池(Move to Pool)**
:把候选人从个人工作区迁入公司人才池,原流程与待办随之结束;支持单条或
**批量移入**
(简历库多选)。
-
**认领 / 复活(Claim / Resurrect)**
:把人才池中的候选人迁入本人工作区,重新开始招聘流程;支持单条或
**批量认领**
(人才池多选)。
-
**简历库级去重(Resume Dedup)**
:批量入库/上传时按文件哈希(file hash)+ 姓名比对——本人简历库已存在 → 跳过并计数;同一份简历在公司人才池 → 不重复建档,引导去人才池认领;不同账号工作区之间不互斥。
-
**入池备注 / 入池时间 / 放入人**
:记录候选人进入人才池的原因、时间与操作账号,便于复用与溯源。
## 岗位与 JD
...
...
docs/requirements_from_interview.md
View file @
9f16374c
...
...
@@ -10,6 +10,8 @@
>
> **2026-09-04 增量(需求 9)**:粘贴 JD 自动结构化 + 招聘需求模板已实现——新增 backend/app/services/jd_import.py(需求模板生成 + LLM 优先/本地小节切分解析),接口 /api/jd/parse-pasted 与 /api/jd/requirement-template,JobEdit「JD 与匹配标准」页新增「粘贴 JD 自动结构化 / 生成需求模板」入口;模板含对内约束(性别/年龄/试用期/竞业/验证),解析后回填岗位对内字段,绝不上对外 JD。详见需求 9 条目。
>
> **2026-09-04 增量(需求 22 补全)**:批量移入人才池 / 批量打标签 / 批量认领(复活)已实现(简历库与人才池页多选 + 批量工具条),批量入库/上传增加 **file_hash 库级去重**(命中本人简历库→跳过计数;命中公司人才池→不重复建档、引导认领;跨账号不互斥);候选人 resumeMeta 落 hash 供后续去重。详见需求 22 条目。
>
> 本次复核相对上一版(810e280)新增提交:`ddc5340`(候选人详情抽屉+档案编辑)、`07310dd`(简历库重构+保存原简历+防重复)、`6248f27`/`fc0aa5c`(简历解析真正 LLM 优先并移除脱敏)、`ec9499f`(弹窗蒙层/ESC 不关闭)。
>
> 依据代码现状核实:
...
...
@@ -32,7 +34,7 @@ HR 原话要点:
**结论**
:延续现有系统架构(整包状态 + 前后端分离),按模块迭代。
**核实情况(2026-09,最新)**
:「下推联动」痛点已基本解决——候选人和岗位的每个状态动作都会同时写事件日志、增删待办(见需求 20);企微智能体产出的 JD 可经
`/api/jd/sync-external`
自动落为岗位并置为「待确认」状态(见需求 2)。
**候选人详情已于近期改为「右侧抽屉 + 可收缩 PDF 档案预览」**
(提交
`ddc5340`
/
`07310dd`
,见需求 10),HR「弹窗/页面级收敛、简历可收缩预览」的交互诉求已落地。
**候选人标记/人才池体系已大部落地(需求 1 总库=人才池 + 需求 12 标签筛选 + 需求 22 人才池独立视图,2026-09-02 工作区补充)**
,需求 14(自述优势约束)已实现(2026-09-03,见条目);需求 17(风险点固定维度)亦已实现(2026-09-03,见条目);
余下缺口为批量移入/复活(需求 22
);岗位维简历量统计已于 2026-09-03 落地;另简历解析自
`fc0aa5c`
起改为
**原文直传大模型提取字段(不再本地脱敏)**
,采集插件仍不抓「沟通职位/期望薪资」。
**核实情况(2026-09,最新)**
:「下推联动」痛点已基本解决——候选人和岗位的每个状态动作都会同时写事件日志、增删待办(见需求 20);企微智能体产出的 JD 可经
`/api/jd/sync-external`
自动落为岗位并置为「待确认」状态(见需求 2)。
**候选人详情已于近期改为「右侧抽屉 + 可收缩 PDF 档案预览」**
(提交
`ddc5340`
/
`07310dd`
,见需求 10),HR「弹窗/页面级收敛、简历可收缩预览」的交互诉求已落地。
**候选人标记/人才池体系已大部落地(需求 1 总库=人才池 + 需求 12 标签筛选 + 需求 22 人才池独立视图,2026-09-02 工作区补充)**
,需求 14(自述优势约束)已实现(2026-09-03,见条目);需求 17(风险点固定维度)亦已实现(2026-09-03,见条目);
需求 22 批量移入/复活与 file_hash 库级去重已实现(2026-09-04,见条目
);岗位维简历量统计已于 2026-09-03 落地;另简历解析自
`fc0aa5c`
起改为
**原文直传大模型提取字段(不再本地脱敏)**
,采集插件仍不抓「沟通职位/期望薪资」。
---
...
...
@@ -197,8 +199,8 @@ HR 原话要点:
**需求 22:简历库 = 简历存储 + 标签 + 统计**
-
原话:「简历库涵盖所有简历,几十上百份可打标签;要看总量、通过面试人数、各岗位简历量(如 java 岗多少份、人力岗多少份);面完没过的放人才池」(P19)
-
现状:🔄 近期重构(提交
`07310dd`
):简历库为
**卡片网格列表**
(头像/姓名/阶段/来源/快筛与 AI 分/求职岗位下拉/「查看档案·快速匹配·更多」操作),顶部 4 张卡(简历总数 / 高匹配=快筛≥85 / 待 AI 评估 / 待补全),筛选为 关键词/阶段/来源/匹配档位——
**仍无标签筛选**
;漏斗/来源/趋势/ROI 看板在
**独立 `/dashboard` 路由**
(可按岗位范围过滤),当时的「各岗位简历量」统计缺口已于 2026-09-03 补齐(见下)。
**新增「保存原简历 + 档案预览/防重复」**
:后端
`/api/upload-resume`
把原文件落库(
`ResumeFile`
表含
`file_hash`
唯一字段,原始文本存
`ResumeText`
,结构化档案存
`ResumeProfile`
),候选人带
`resumeFileDataUrl`
(
`/api/resumes/{name}`
)与
`resumeMeta`
,详情抽屉左栏可预览/下载;
**批量入库增加前端防重复**
(按 简历文件名+清洗后姓名 跳过并计数,后端
`IngestItem`
仍以唯一
`idempotency_key`
兜底)。
**人才池(总库)独立视图已实现(2026-09-02 补充)**
:左侧「人才池」页 = 公司共享总库(跨账号),卡片列表 + 关键词/标签/来源筛选 + 统计卡(总库人数/本周新入池/待跟进)+ 详情抽屉(档案/简历正文/原简历链接)+「认领到我的工作区」;简历库卡片与详情抽屉均可一键「移入人才池」。
**岗位维简历量统计已落地(2026-09-03)**
:简历库页顶部新增「各岗位简历量」chips,按岗位显示 简历量 / 已入职 / 高匹配(快筛≥85),点击聚焦该岗位候选人(复用
`?job=`
聚焦),未绑定岗位单列提示。
**
残余**
:批量移入/复活仍未做
。
-
建议(
🔄):标签筛选、人才池独立视图、岗位维简历量统计均已落地;待做:批量放入/复活、后端按
`file_hash`
库级去重
。
-
现状:🔄 近期重构(提交
`07310dd`
):简历库为
**卡片网格列表**
(头像/姓名/阶段/来源/快筛与 AI 分/求职岗位下拉/「查看档案·快速匹配·更多」操作),顶部 4 张卡(简历总数 / 高匹配=快筛≥85 / 待 AI 评估 / 待补全),筛选为 关键词/阶段/来源/匹配档位——
**仍无标签筛选**
;漏斗/来源/趋势/ROI 看板在
**独立 `/dashboard` 路由**
(可按岗位范围过滤),当时的「各岗位简历量」统计缺口已于 2026-09-03 补齐(见下)。
**新增「保存原简历 + 档案预览/防重复」**
:后端
`/api/upload-resume`
把原文件落库(
`ResumeFile`
表含
`file_hash`
唯一字段,原始文本存
`ResumeText`
,结构化档案存
`ResumeProfile`
),候选人带
`resumeFileDataUrl`
(
`/api/resumes/{name}`
)与
`resumeMeta`
,详情抽屉左栏可预览/下载;
**批量入库增加前端防重复**
(按 简历文件名+清洗后姓名 跳过并计数,后端
`IngestItem`
仍以唯一
`idempotency_key`
兜底)。
**人才池(总库)独立视图已实现(2026-09-02 补充)**
:左侧「人才池」页 = 公司共享总库(跨账号),卡片列表 + 关键词/标签/来源筛选 + 统计卡(总库人数/本周新入池/待跟进)+ 详情抽屉(档案/简历正文/原简历链接)+「认领到我的工作区」;简历库卡片与详情抽屉均可一键「移入人才池」。
**岗位维简历量统计已落地(2026-09-03)**
:简历库页顶部新增「各岗位简历量」chips,按岗位显示 简历量 / 已入职 / 高匹配(快筛≥85),点击聚焦该岗位候选人(复用
`?job=`
聚焦),未绑定岗位单列提示。
**
批量操作已实现(2026-09-04)**
:简历库卡片多选(勾选框)→ 批量工具条「批量移入人才池(可统一备注)/批量打标签」;人才池页列表/卡片多选 →「批量认领到我的工作区」(=批量复活),均复用单条逻辑循环并汇总成功/失败。
**file_hash 库级去重已实现(2026-09-04)**
:批量入库先上传取 hash,再与「本人简历库 + 公司人才池」比对——命中本人库跳过计数「工作区已存在」,命中人才池不重复建档并提示「可去人才池认领」,跨账号不互斥;候选人
`resumeMeta`
新增
`hash`
落档供后续去重
。
-
建议(
✅ 已落地):标签筛选、人才池独立视图、岗位维简历量统计、批量移入/复活、file_hash 库级去重均已实现。可选增强:人才池批量「重新绑定岗位/批量打标」、跨账号重复提示(当前按隔离不互斥)、把去重下沉到后端行级建档(当前在入库/上传环节按 hash 比对)
。
---
...
...
@@ -234,7 +236,7 @@ HR 原话要点:
| P1 | 需求 12 候选人标记 | 🔄 基本落地 | tags 枚举 + 详情编辑 + 简历库/人才池标签筛选 + 待跟进快捷筛选;独立周报视图待做 |
| P2 | 需求 4 岗位自动关闭 | ✅ 已实现(2026-09-03) | hired>=headcount 自动置「已招满」并记事件日志 |
| P2 | 需求 9 粘贴 JD 自动结构化 + 需求模板 | ✅ 已实现(2026-09-04) | 粘贴解析(LLM/本地)+ 需求模板(含对内约束)闭环 |
| P2 | 需求 22 简历库标签/统计
| 🔄 大部分落地 | 标签筛选/人才池视图/岗位维简历量已落地;批量移入待做
|
| P2 | 需求 22 简历库标签/统计
/批量 | ✅ 已实现(2026-09-04) | 批量移入/打标签/批量认领 + file_hash 库级去重
|
| P3 | 需求 23 插件字段修正 | 🔄 部分 | 补抓沟通职位/期望薪资 |
| P3 | 需求 21 入职材料三件套 | 🔄 部分 | 三张模板化 |
| P3 | 需求 3 岗位类型枚举 / 需求 25 期望薪资展示 | 🆕 | 小改动,按需做 |
...
...
@@ -254,4 +256,4 @@ HR 原话要点:
---
*整理自 20 页访谈记录,结合当前代码现状标注;最近一次代码核对 2026-09-02(main @ ec9499f,较 810e280 起新增 ddc5340/07310dd/6248f27/fc0aa5c/ec9499f 均已复核)。需求 14/17(自述优势约束、风险点固定维度)、需求 4 自动招满、岗位维简历量统计已于 2026-09-03 落地;需求 9(粘贴 JD 自动结构化 + 招聘需求模板
,含对内约束)已于 2026-09-04 落地;建议优先补齐:批量移入/复活(需求 22),并按需做需求 3/25 等小改动
。*
*整理自 20 页访谈记录,结合当前代码现状标注;最近一次代码核对 2026-09-02(main @ ec9499f,较 810e280 起新增 ddc5340/07310dd/6248f27/fc0aa5c/ec9499f 均已复核)。需求 14/17(自述优势约束、风险点固定维度)、需求 4 自动招满、岗位维简历量统计已于 2026-09-03 落地;需求 9(粘贴 JD 自动结构化 + 招聘需求模板
)与需求 22 补全(批量移入/复活、file_hash 库级去重)已于 2026-09-04 落地;后续按需做需求 3/25 等小改动(岗位类型枚举、期望薪资展示)
。*
vue-app/src/components/resumes/CandidateBatchModal.vue
View file @
9f16374c
...
...
@@ -142,18 +142,32 @@ const importBatch = async () => {
return
}
}
// 需求 22:按 file_hash 库级去重——对比本人简历库与公司人才池已有简历的 hash
await
store
.
loadPool
()
const
workspaceHashes
=
new
Set
(
store
.
candidates
.
map
((
candidate
)
=>
candidate
.
resumeMeta
?.
hash
).
filter
(
Boolean
))
const
poolHashes
=
new
Set
(
store
.
poolCandidates
.
map
((
candidate
)
=>
candidate
.
resumeMeta
?.
hash
).
filter
(
Boolean
))
const
created
=
[]
let
duplicateCount
=
0
let
poolDuplicateCount
=
0
importable
.
forEach
((
entry
)
=>
{
const
upload
=
entry
.
upload
||
{}
const
parsed
=
entry
.
parsed
||
upload
.
parsed
||
{}
const
job
=
store
.
jobs
.
find
((
item
)
=>
item
.
id
===
entry
.
jobId
)
const
name
=
cleanCandidateName
(
parsed
.
name
||
''
,
entry
.
file
?.
name
)
||
'未命名候选人'
const
duplicate
=
store
.
candidates
.
some
(
const
hash
=
String
(
upload
.
file
?.
fileHash
||
''
)
const
poolDuplicate
=
Boolean
(
hash
&&
poolHashes
.
has
(
hash
))
const
nameDuplicate
=
store
.
candidates
.
some
(
(
candidate
)
=>
candidate
.
resumeName
===
entry
.
file
?.
name
&&
cleanCandidateName
(
candidate
.
name
,
candidate
.
resumeName
)
===
name
)
if
(
duplicate
)
{
const
workspaceDuplicate
=
Boolean
(
hash
&&
workspaceHashes
.
has
(
hash
))
if
(
poolDuplicate
)
{
// 同一份简历已在公司人才池:不重复建档,引导去人才池认领
poolDuplicateCount
+=
1
return
}
if
(
workspaceDuplicate
||
nameDuplicate
)
{
duplicateCount
+=
1
return
}
...
...
@@ -163,6 +177,7 @@ const importBatch = async () => {
name
:
upload
.
file
.
originalName
||
entry
.
file
?.
name
,
type
:
upload
.
file
.
fileType
||
entry
.
file
?.
type
||
''
,
size
:
upload
.
file
.
sizeBytes
||
entry
.
file
?.
size
||
0
,
hash
:
upload
.
file
.
fileHash
||
''
,
}
:
entry
.
file
?
{
name
:
entry
.
file
.
name
,
type
:
entry
.
file
.
type
,
size
:
entry
.
file
.
size
}
...
...
@@ -219,7 +234,10 @@ const importBatch = async () => {
if (created[0]) store.selectedCandidateId = created[0].id
store.resumeViewMode = 'list'
store.persist('批量简历已入库')
showToast(`
已批量入库
$
{
created
.
length
}
人
$
{
duplicateCount
?
`,跳过重复
${
duplicateCount
}
份`
:
''
}
`)
let message = `
已批量入库
$
{
created
.
length
}
人
`
if (duplicateCount) message += `
,工作区已存在跳过
$
{
duplicateCount
}
份
`
if (poolDuplicateCount) message += `
,人才池已有
$
{
poolDuplicateCount
}
份(可去人才池认领)
`
showToast(message, 'success')
} finally {
importing.value = false
}
...
...
vue-app/src/components/resumes/ResumeList.vue
View file @
9f16374c
...
...
@@ -19,6 +19,69 @@ const router = useRouter()
const
batchMatchRunning
=
ref
(
false
)
const
batchMatchResult
=
ref
(
null
)
// 需求 22 残余:批量移入人才池 / 批量打标签(多选卡片)
const
selectedIds
=
ref
([])
const
selectedCandidates
=
computed
(()
=>
store
.
candidates
.
filter
((
candidate
)
=>
selectedIds
.
value
.
includes
(
candidate
.
id
))
)
const
isSelected
=
(
candidate
)
=>
selectedIds
.
value
.
includes
(
candidate
.
id
)
const
toggleSelect
=
(
candidate
)
=>
{
const
index
=
selectedIds
.
value
.
indexOf
(
candidate
.
id
)
if
(
index
>=
0
)
selectedIds
.
value
.
splice
(
index
,
1
)
else
selectedIds
.
value
.
push
(
candidate
.
id
)
}
const
clearSelection
=
()
=>
{
selectedIds
.
value
=
[]
}
const
batchMoveToPool
=
async
()
=>
{
if
(
!
selectedCandidates
.
value
.
length
)
return
let
note
=
''
try
{
const
{
value
}
=
await
ElMessageBox
.
prompt
(
'可统一备注移入原因(留空也可)'
,
'批量移入人才池'
,
{
confirmButtonText
:
'移入人才池'
,
cancelButtonText
:
'取消'
,
inputPlaceholder
:
'如:二面未通过 / 暂缓 / 储备'
,
inputValue
:
''
,
})
note
=
(
value
||
''
).
trim
()
}
catch
{
return
}
const
list
=
[...
selectedCandidates
.
value
]
let
moved
=
0
let
failed
=
0
for
(
const
candidate
of
list
)
{
try
{
await
store
.
moveToPoolCandidate
(
candidate
,
note
)
moved
+=
1
}
catch
{
failed
+=
1
}
}
clearSelection
()
showToast
(
failed
?
`已移入
${
moved
}
人,失败
${
failed
}
人`
:
`已批量移入人才池
${
moved
}
人`
,
'success'
)
}
const
batchTagOpen
=
ref
(
false
)
const
batchTagValue
=
ref
([])
const
openBatchTag
=
()
=>
{
const
first
=
selectedCandidates
.
value
[
0
]
batchTagValue
.
value
=
first
?.
tags
?
[...
first
.
tags
]
:
[]
batchTagOpen
.
value
=
true
}
const
applyBatchTag
=
()
=>
{
const
tags
=
[...
batchTagValue
.
value
]
const
count
=
selectedCandidates
.
value
.
length
selectedCandidates
.
value
.
forEach
((
candidate
)
=>
{
candidate
.
tags
=
tags
})
batchTagOpen
.
value
=
false
clearSelection
()
store
.
persist
(
'已批量更新候选人标记'
)
showToast
(
tags
.
length
?
`已为
${
count
}
人更新标记:
${
tags
.
join
(
'、'
)}
`
:
`已清除
${
count
}
人的标记`
,
'success'
)
}
const
stageOptions
=
[
'全部阶段'
,
'未筛选'
,
'初筛通过'
,
'初筛未通过'
,
'面试中'
,
'Offer 中'
,
'待入职'
,
'已入职'
]
const
tagOptions
=
[
'全部标签'
,
'待跟进'
,
...
CANDIDATE_TAG_OPTIONS
]
const
sourceOptions
=
computed
(()
=>
[
...
...
@@ -311,6 +374,15 @@ const openBatch = () => {
<
/div
>
<
/header
>
<
div
v
-
if
=
"selectedIds.length"
class
=
"selection-bar"
>
<
span
class
=
"selection-count"
>
已选
<
b
>
{{
selectedIds
.
length
}}
<
/b> 位候选人</
span
>
<
el
-
button
size
=
"small"
type
=
"primary"
plain
@
click
=
"batchMoveToPool"
>
批量移入人才池
<
/el-button
>
<
el
-
button
size
=
"small"
plain
@
click
=
"openBatchTag"
>
批量打标签
<
/el-button
>
<
el
-
button
size
=
"small"
text
@
click
=
"clearSelection"
>
取消选择
<
/el-button
>
<
/div
>
<
div
class
=
"filter-row"
>
<
el
-
input
v
-
model
=
"store.resumeFilters.q"
...
...
@@ -380,10 +452,18 @@ const openBatch = () => {
v
-
for
=
"candidate in filtered"
:
key
=
"candidate.id"
class
=
"candidate-card"
:
class
=
"{ 'is-selected': isSelected(candidate)
}
"
tabindex
=
"0"
@
click
=
"openDetail(candidate)"
@
keydown
.
enter
=
"openDetail(candidate)"
>
<
div
class
=
"candidate-check-cell"
@
click
.
stop
>
<
el
-
checkbox
:
model
-
value
=
"isSelected(candidate)"
aria
-
label
=
"选择候选人"
@
change
=
"toggleSelect(candidate)"
/>
<
/div
>
<
div
class
=
"candidate-person"
>
<
div
class
=
"candidate-avatar"
:
style
=
"{ '--avatar-accent': scoreColor(quickScore(candidate))
}
"
>
{{
candidateInitial
(
candidate
)
}}
...
...
@@ -483,6 +563,17 @@ const openBatch = () => {
<
el
-
button
v
-
if
=
"hasFilters"
round
@
click
=
"clearFilters"
>
清除筛选
<
/el-button
>
<
/div
>
<
/section
>
<
el
-
dialog
v
-
model
=
"batchTagOpen"
title
=
"批量打标签"
width
=
"440px"
>
<
p
class
=
"batch-tag-tip"
>
将覆盖所选候选人的现有标记;全部取消勾选
=
清除标记。
<
/p
>
<
el
-
checkbox
-
group
v
-
model
=
"batchTagValue"
class
=
"batch-tag-group"
>
<
el
-
checkbox
v
-
for
=
"tag in CANDIDATE_TAG_OPTIONS"
:
key
=
"tag"
:
label
=
"tag"
>
{{
tag
}}
<
/el-checkbox
>
<
/el-checkbox-group
>
<
template
#
footer
>
<
el
-
button
@
click
=
"batchTagOpen = false"
>
取消
<
/el-button
>
<
el
-
button
type
=
"primary"
@
click
=
"applyBatchTag"
>
应用到已选
{{
selectedIds
.
length
}}
人
<
/el-button
>
<
/template
>
<
/el-dialog
>
<
/div
>
<
/template
>
...
...
@@ -716,10 +807,11 @@ const openBatch = () => {
}
.
candidate
-
card
{
position
:
relative
;
display
:
grid
;
grid
-
template
-
columns
:
minmax
(
240
px
,
1.5f
r
)
minmax
(
150
px
,
0.7f
r
)
minmax
(
220
px
,
0.95f
r
)
auto
;
grid
-
template
-
columns
:
24
px
minmax
(
240
px
,
1.5f
r
)
minmax
(
150
px
,
0.7f
r
)
minmax
(
220
px
,
0.95f
r
)
auto
;
align
-
items
:
center
;
gap
:
1
6
px
;
gap
:
1
2
px
;
padding
:
13
px
14
px
;
border
:
1
px
solid
var
(
--
line
);
border
-
radius
:
12
px
;
...
...
@@ -1103,4 +1195,46 @@ const openBatch = () => {
transition
:
none
;
}
}
.
candidate
-
card
.
is
-
selected
{
border
-
color
:
var
(
--
blue
);
box
-
shadow
:
0
0
0
2
px
color
-
mix
(
in
srgb
,
var
(
--
blue
)
30
%
,
transparent
);
}
.
candidate
-
check
-
cell
{
display
:
flex
;
align
-
items
:
center
;
}
.
selection
-
bar
{
display
:
flex
;
align
-
items
:
center
;
gap
:
10
px
;
margin
:
10
px
0
;
padding
:
8
px
12
px
;
background
:
color
-
mix
(
in
srgb
,
var
(
--
blue
)
10
%
,
#
fff
);
border
:
1
px
solid
color
-
mix
(
in
srgb
,
var
(
--
blue
)
35
%
,
var
(
--
line
));
border
-
radius
:
10
px
;
.
selection
-
count
{
margin
-
right
:
auto
;
font
-
size
:
13
px
;
b
{
color
:
var
(
--
blue
);
}
}
}
.
batch
-
tag
-
tip
{
margin
-
bottom
:
10
px
;
font
-
size
:
12
px
;
color
:
var
(
--
muted
);
line
-
height
:
1.5
;
}
.
batch
-
tag
-
group
{
display
:
flex
;
flex
-
wrap
:
wrap
;
gap
:
6
px
;
}
<
/style
>
vue-app/src/views/PoolView.vue
View file @
9f16374c
...
...
@@ -22,6 +22,45 @@ const selectedId = ref('')
const
LOAD_STEP
=
60
const
visibleCount
=
ref
(
LOAD_STEP
)
// 需求 22:批量认领(复活)——多选人才池候选人认领到我的工作区
const
selectedPoolIds
=
ref
([])
const
selectedPool
=
computed
(()
=>
poolView
.
value
.
filter
((
candidate
)
=>
selectedPoolIds
.
value
.
includes
(
candidate
.
id
)))
const
poolIsSelected
=
(
candidate
)
=>
selectedPoolIds
.
value
.
includes
(
candidate
.
id
)
const
togglePoolSelect
=
(
candidate
)
=>
{
const
index
=
selectedPoolIds
.
value
.
indexOf
(
candidate
.
id
)
if
(
index
>=
0
)
selectedPoolIds
.
value
.
splice
(
index
,
1
)
else
selectedPoolIds
.
value
.
push
(
candidate
.
id
)
}
const
clearPoolSelection
=
()
=>
{
selectedPoolIds
.
value
=
[]
}
const
batchClaim
=
async
()
=>
{
const
count
=
selectedPool
.
value
.
length
if
(
!
count
)
return
try
{
await
ElMessageBox
.
confirm
(
`确认把选中的
${
count
}
位候选人认领到你的工作区?认领后重置为「未筛选」,可重新绑定岗位继续推进。`
,
'批量认领到我的工作区'
,
{
confirmButtonText
:
'认领到我的工作区'
,
cancelButtonText
:
'取消'
,
type
:
'info'
}
)
}
catch
{
return
}
const
list
=
[...
selectedPool
.
value
]
let
ok
=
0
let
failed
=
0
for
(
const
candidate
of
list
)
{
try
{
await
store
.
claimPoolCandidate
(
candidate
)
ok
+=
1
}
catch
{
failed
+=
1
}
}
clearPoolSelection
()
showToast
(
failed
?
`已认领
${
ok
}
人,失败
${
failed
}
人`
:
`已批量认领
${
ok
}
人到我的工作区`
,
'success'
)
}
function
readDensity
()
{
try
{
return
localStorage
.
getItem
(
'pool-density-v1'
)
===
'grid'
?
'grid'
:
'list'
...
...
@@ -444,6 +483,15 @@ onMounted(() => {
<
/button
>
<
/div
>
<
/div
>
<
div
v
-
if
=
"selectedPoolIds.length"
class
=
"pool-selection-bar"
>
<
span
class
=
"pool-selection-count"
>
已选
<
b
>
{{
selectedPoolIds
.
length
}}
<
/b> 人</
span
>
<
el
-
button
size
=
"small"
round
type
=
"primary"
plain
@
click
=
"batchClaim"
>
批量认领到我的工作区
<
/el-button
>
<
el
-
button
size
=
"small"
round
text
@
click
=
"clearPoolSelection"
>
取消选择
<
/el-button
>
<
/div
>
<!--
结果区
-->
<
div
class
=
"results"
>
<!--
骨架屏
-->
...
...
@@ -464,12 +512,20 @@ onMounted(() => {
v
-
for
=
"candidate in visible"
:
key
=
"candidate.id"
class
=
"ledger-row"
:
class
=
"{ 'is-selected': poolIsSelected(candidate)
}
"
:
style
=
"{ '--row-tone': firstTone(candidate)
}
"
tabindex
=
"0"
@
click
=
"openDetail(candidate)"
@
keydown
.
enter
=
"openDetail(candidate)"
>
<
div
class
=
"lr-person"
>
<
el
-
checkbox
class
=
"pool-check"
:
model
-
value
=
"poolIsSelected(candidate)"
aria
-
label
=
"选择认领"
@
click
.
stop
@
change
=
"togglePoolSelect(candidate)"
/>
<
span
class
=
"person-tile"
>
{{
avatarText
(
candidate
)
}}
<
/span
>
<
span
class
=
"person-copy"
>
<
strong
class
=
"person-name"
>
{{
candidate
.
name
||
'未命名候选人'
}}
<
/strong
>
...
...
@@ -515,12 +571,20 @@ onMounted(() => {
v
-
for
=
"candidate in visible"
:
key
=
"candidate.id"
class
=
"pool-card"
:
class
=
"{ 'is-selected': poolIsSelected(candidate)
}
"
:
style
=
"{ '--row-tone': firstTone(candidate)
}
"
tabindex
=
"0"
@
click
=
"openDetail(candidate)"
@
keydown
.
enter
=
"openDetail(candidate)"
>
<
div
class
=
"pc-head"
>
<
el
-
checkbox
class
=
"pool-check"
:
model
-
value
=
"poolIsSelected(candidate)"
aria
-
label
=
"选择认领"
@
click
.
stop
@
change
=
"togglePoolSelect(candidate)"
/>
<
span
class
=
"person-tile"
>
{{
avatarText
(
candidate
)
}}
<
/span
>
<
span
class
=
"pc-head-copy"
>
<
strong
>
{{
candidate
.
name
||
'未命名候选人'
}}
<
/strong
>
...
...
@@ -1625,4 +1689,35 @@ onMounted(() => {
background
:
#
f3f5f9
;
}
}
.
pool
-
check
{
flex
-
shrink
:
0
;
margin
-
right
:
4
px
;
}
.
ledger
-
row
.
is
-
selected
,
.
pool
-
card
.
is
-
selected
{
outline
:
2
px
solid
var
(
--
blue
);
outline
-
offset
:
-
2
px
;
}
.
pool
-
selection
-
bar
{
display
:
flex
;
align
-
items
:
center
;
gap
:
10
px
;
margin
:
10
px
0
4
px
;
padding
:
8
px
12
px
;
background
:
color
-
mix
(
in
srgb
,
var
(
--
blue
)
10
%
,
#
fff
);
border
:
1
px
solid
color
-
mix
(
in
srgb
,
var
(
--
blue
)
35
%
,
var
(
--
line
));
border
-
radius
:
10
px
;
.
pool
-
selection
-
count
{
margin
-
right
:
auto
;
font
-
size
:
13
px
;
b
{
color
:
var
(
--
blue
);
}
}
}
<
/style
>
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