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
f3f85b35
Commit
f3f85b35
authored
Sep 02, 2026
by
李文光
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
style: 人才池改为档案导卡筛选与台账列表,支撑大库浏览
parent
6b8b2e2f
Hide whitespace changes
Inline
Side-by-side
Showing
1 changed file
with
1241 additions
and
247 deletions
+1241
-247
PoolView.vue
vue-app/src/views/PoolView.vue
+1241
-247
No files found.
vue-app/src/views/PoolView.vue
View file @
f3f85b35
<
script
setup
>
import
{
computed
,
onMounted
,
ref
}
from
'vue'
import
{
computed
,
onMounted
,
ref
,
watch
}
from
'vue'
import
{
useRouter
}
from
'vue-router'
import
{
ElMessageBox
}
from
'element-plus'
import
{
useRecruitmentStore
}
from
'@/stores/recruitment'
import
{
CANDIDATE_TAG_OPTIONS
,
FOLLOW_UP_TAGS
,
candidateTagTone
,
normalizeCandidateTags
}
from
'@/utils/constants'
import
{
CANDIDATE_TAG_OPTIONS
,
FOLLOW_UP_TAGS
,
normalizeCandidateTags
}
from
'@/utils/constants'
import
{
enrichCandidateFields
,
candidateAge
,
candidateYears
}
from
'@/utils/candidate'
import
{
candidatePassesTagFilter
}
from
'@/utils/matching'
import
{
showToast
}
from
'@/utils/toast'
const
store
=
useRecruitmentStore
()
const
router
=
useRouter
()
const
filters
=
ref
({
q
:
''
,
tag
:
'全部标签'
,
source
:
'全部来源'
})
const
ALL_TAG
=
'全部标签'
const
FOLLOW_TAG
=
'待跟进'
const
NONE_TAG
=
'__none__'
const
filters
=
ref
({
q
:
''
,
tag
:
ALL_TAG
,
source
:
'全部来源'
,
sort
:
'recent'
})
const
density
=
ref
(
readDensity
())
const
detailOpen
=
ref
(
false
)
const
selectedId
=
ref
(
''
)
const
LOAD_STEP
=
60
const
visibleCount
=
ref
(
LOAD_STEP
)
const
tagOptions
=
[
'全部标签'
,
'待跟进'
,
...
CANDIDATE_TAG_OPTIONS
]
function
readDensity
()
{
try
{
return
localStorage
.
getItem
(
'pool-density-v1'
)
===
'grid'
?
'grid'
:
'list'
}
catch
{
return
'list'
}
}
const
sourceOptions
=
computed
(()
=>
[
'全部来源'
,
...
new
Set
((
store
.
poolCandidates
||
[]).
map
((
candidate
)
=>
candidate
.
source
).
filter
(
Boolean
)),
])
function
persistDensity
(
value
)
{
density
.
value
=
value
try
{
localStorage
.
setItem
(
'pool-density-v1'
,
value
)
}
catch
{
/* 忽略隐私模式下的写入失败 */
}
}
// 池内候选人不属于当前工作区,只做轻量档案补全,不绑定本区岗位。
const
poolView
=
computed
(()
=>
...
...
@@ -31,11 +47,94 @@ const poolView = computed(() =>
})
)
const
sourceOptions
=
computed
(()
=>
[
'全部来源'
,
...
new
Set
(
poolView
.
value
.
map
((
candidate
)
=>
candidate
.
source
).
filter
(
Boolean
)),
])
// ---- 导卡(guide-tab) 标签过滤体系 ----
// 两个检索区:视图(全部/待跟进) 与 标记(预设+自定义+未标记),单点切换、再点取消。
const
guideSections
=
computed
(()
=>
{
const
all
=
poolView
.
value
const
tagSet
=
(
candidate
)
=>
candidate
.
tags
||
[]
const
countBy
=
(
predicate
)
=>
all
.
filter
(
predicate
).
length
const
preset
=
CANDIDATE_TAG_OPTIONS
const
custom
=
new
Map
()
all
.
forEach
((
candidate
)
=>
{
tagSet
(
candidate
).
forEach
((
tag
)
=>
{
if
(
!
preset
.
includes
(
tag
))
custom
.
set
(
tag
,
(
custom
.
get
(
tag
)
||
0
)
+
1
)
})
})
return
[
{
caption
:
'视图'
,
tabs
:
[
{
key
:
ALL_TAG
,
label
:
'全部'
,
tone
:
'#4569c9'
,
count
:
all
.
length
},
{
key
:
FOLLOW_TAG
,
label
:
FOLLOW_TAG
,
tone
:
'#9a6a12'
,
count
:
countBy
((
candidate
)
=>
tagSet
(
candidate
).
some
((
tag
)
=>
FOLLOW_UP_TAGS
.
includes
(
tag
))),
hint
:
'重点跟进 · 待二面 · 待沟通 · 需维护'
,
},
],
},
{
caption
:
'标记'
,
tabs
:
[
...
preset
.
map
((
tag
)
=>
({
key
:
tag
,
label
:
tag
,
tone
:
tagColor
(
tag
),
count
:
countBy
((
candidate
)
=>
tagSet
(
candidate
).
includes
(
tag
)),
})),
...[...
custom
.
entries
()]
.
sort
((
a
,
b
)
=>
b
[
1
]
-
a
[
1
]
||
String
(
a
[
0
]).
localeCompare
(
String
(
b
[
0
]),
'zh-CN'
))
.
map
(([
label
,
count
])
=>
({
key
:
label
,
label
,
tone
:
tagColor
(
label
),
count
})),
{
key
:
NONE_TAG
,
label
:
'未标记'
,
tone
:
'#9aa3b2'
,
count
:
countBy
((
candidate
)
=>
tagSet
(
candidate
).
length
===
0
),
hint
:
'还没有任何标记的档案'
,
},
],
},
]
})
function
passesTag
(
candidate
,
key
)
{
const
tags
=
normalizeCandidateTags
(
candidate
)
if
(
key
===
ALL_TAG
)
return
true
if
(
key
===
FOLLOW_TAG
)
return
tags
.
some
((
tag
)
=>
FOLLOW_UP_TAGS
.
includes
(
tag
))
if
(
key
===
NONE_TAG
)
return
tags
.
length
===
0
return
tags
.
includes
(
key
)
}
function
selectTag
(
tab
)
{
filters
.
value
.
tag
=
filters
.
value
.
tag
===
tab
.
key
?
ALL_TAG
:
tab
.
key
}
function
isTagActive
(
tab
)
{
return
filters
.
value
.
tag
===
tab
.
key
}
// ---- 统计 ----
const
followUpCount
=
computed
(()
=>
poolView
.
value
.
filter
((
candidate
)
=>
passesTag
(
candidate
,
FOLLOW_TAG
)).
length
)
const
unmarkedCount
=
computed
(()
=>
poolView
.
value
.
filter
((
candidate
)
=>
passesTag
(
candidate
,
NONE_TAG
)).
length
)
const
newThisWeek
=
computed
(()
=>
{
const
weekAgo
=
Date
.
now
()
-
7
*
24
*
60
*
60
*
1000
return
poolView
.
value
.
filter
((
candidate
)
=>
Date
.
parse
(
candidate
.
pooledAt
||
''
)
>=
weekAgo
).
length
})
// ---- 检索与排序 ----
const
filtered
=
computed
(()
=>
{
const
q
=
filters
.
value
.
q
.
toLowerCase
().
trim
()
const
list
=
poolView
.
value
.
filter
((
candidate
)
=>
{
return
poolView
.
value
.
filter
((
candidate
)
=>
{
if
(
filters
.
value
.
source
!==
'全部来源'
&&
(
candidate
.
source
||
'其他'
)
!==
filters
.
value
.
source
)
return
false
if
(
!
candidatePassesTagFilter
(
candidate
,
filters
.
value
.
tag
))
return
false
if
(
!
passesTag
(
candidate
,
filters
.
value
.
tag
))
return
false
if
(
q
)
{
const
haystack
=
[
candidate
.
name
,
...
...
@@ -53,28 +152,113 @@ const filtered = computed(() => {
}
return
true
})
})
const
sorted
=
computed
(()
=>
{
const
list
=
[...
filtered
.
value
]
const
poolTs
=
(
candidate
)
=>
Date
.
parse
(
candidate
.
pooledAt
||
''
)
||
0
switch
(
filters
.
value
.
sort
)
{
case
'oldest'
:
list
.
sort
((
a
,
b
)
=>
poolTs
(
a
)
-
poolTs
(
b
)
||
String
(
a
.
name
||
''
).
localeCompare
(
String
(
b
.
name
||
''
),
'zh-CN'
))
break
case
'name'
:
list
.
sort
((
a
,
b
)
=>
String
(
a
.
name
||
''
).
localeCompare
(
String
(
b
.
name
||
''
),
'zh-CN'
))
break
case
'years'
:
list
.
sort
((
a
,
b
)
=>
Number
(
b
.
years
||
0
)
-
Number
(
a
.
years
||
0
)
||
poolTs
(
b
)
-
poolTs
(
a
))
break
default
:
list
.
sort
((
a
,
b
)
=>
poolTs
(
b
)
-
poolTs
(
a
)
||
String
(
a
.
name
||
''
).
localeCompare
(
String
(
b
.
name
||
''
),
'zh-CN'
))
}
return
list
})
const
followUpCount
=
computed
(
()
=>
poolView
.
value
.
filter
((
candidate
)
=>
(
candidate
.
tags
||
[]).
some
((
tag
)
=>
FOLLOW_UP_TAGS
.
includes
(
tag
))).
length
const
visible
=
computed
(()
=>
sorted
.
value
.
slice
(
0
,
visibleCount
.
value
))
const
hasFilters
=
computed
(
()
=>
Boolean
(
filters
.
value
.
q
.
trim
())
||
filters
.
value
.
tag
!==
ALL_TAG
||
filters
.
value
.
source
!==
'全部来源'
)
const
newThisWeek
=
computed
(()
=>
{
const
weekAgo
=
Date
.
now
()
-
7
*
24
*
60
*
60
*
1000
return
poolView
.
value
.
filter
((
candidate
)
=>
Date
.
parse
(
candidate
.
pooledAt
||
''
)
>=
weekAgo
).
length
})
function
resetFilters
()
{
filters
.
value
=
{
q
:
''
,
tag
:
ALL_TAG
,
source
:
'全部来源'
,
sort
:
filters
.
value
.
sort
}
}
watch
(
[
()
=>
filters
.
value
.
q
,
()
=>
filters
.
value
.
tag
,
()
=>
filters
.
value
.
source
,
()
=>
filters
.
value
.
sort
,
()
=>
poolView
.
value
.
length
,
],
()
=>
{
visibleCount
.
value
=
LOAD_STEP
}
)
// ---- 展示 ----
const
sortOptions
=
[
{
value
:
'recent'
,
label
:
'最近入池'
},
{
value
:
'oldest'
,
label
:
'最早入池'
},
{
value
:
'name'
,
label
:
'姓名 A-Z'
},
{
value
:
'years'
,
label
:
'经验年限'
},
]
const
selected
=
computed
(()
=>
poolView
.
value
.
find
((
candidate
)
=>
candidate
.
id
===
selectedId
.
value
)
||
null
)
const
pad
=
(
num
)
=>
String
(
num
).
padStart
(
2
,
'0'
)
const
formatPooledAt
=
(
iso
)
=>
{
if
(
!
iso
)
return
''
const
date
=
new
Date
(
iso
)
if
(
Number
.
isNaN
(
date
.
getTime
()))
return
''
const
pad
=
(
num
)
=>
String
(
num
).
padStart
(
2
,
'0'
)
return
`
${
date
.
getFullYear
()}
-
${
pad
(
date
.
getMonth
()
+
1
)}
-
${
pad
(
date
.
getDate
())}
${
pad
(
date
.
getHours
())}
:
${
pad
(
date
.
getMinutes
())}
`
}
const
formatPooledShort
=
(
iso
)
=>
{
const
date
=
new
Date
(
iso
||
''
)
if
(
Number
.
isNaN
(
date
.
getTime
()))
return
''
return
`
${
pad
(
date
.
getMonth
()
+
1
)}
-
${
pad
(
date
.
getDate
())}
${
pad
(
date
.
getHours
())}
:
${
pad
(
date
.
getMinutes
())}
`
}
function
avatarText
(
candidate
)
{
return
String
(
candidate
.
name
||
'候'
)
.
trim
()
.
slice
(
0
,
1
)
.
toUpperCase
()
}
function
roleLine
(
candidate
)
{
const
role
=
candidate
.
jobTitle
||
'待匹配岗位'
const
stage
=
candidate
.
stage
&&
candidate
.
stage
!==
'未筛选'
?
candidate
.
stage
:
''
return
stage
?
`
${
role
}
·
${
stage
}
`
:
role
}
function
pooledByText
(
candidate
)
{
return
candidate
.
pooledByLabel
||
candidate
.
pooledBy
||
'未知'
}
function
firstTone
(
candidate
)
{
const
tags
=
candidate
.
tags
||
[]
return
tags
.
length
?
tagColor
(
tags
[
0
])
:
'#a6aebf'
}
const
tagColor
=
(
tag
=
''
)
=>
{
const
map
=
{
新入库
:
'#1f7a5a'
,
重点跟进
:
'#9a6a12'
,
待二面
:
'#9a6a12'
,
待沟通
:
'#9a6a12'
,
需维护
:
'#9a6a12'
,
储备
:
'#4d5c75'
,
已淘汰
:
'#b24848'
,
}
return
map
[
tag
]
||
'#3d4759'
}
function
visibleTags
(
candidate
)
{
const
tags
=
candidate
.
tags
||
[]
return
{
shown
:
tags
.
slice
(
0
,
2
),
extra
:
Math
.
max
(
0
,
tags
.
length
-
2
)
}
}
// ---- 操作 ----
const
openDetail
=
(
candidate
)
=>
{
selectedId
.
value
=
candidate
.
id
detailOpen
.
value
=
true
...
...
@@ -132,112 +316,266 @@ onMounted(() => {
store
.
loadPool
()
})
</
script
>
<
template
>
<div
class=
"pool-page"
>
<
div
class=
"metric-grid"
>
<article
class=
"metric-card"
style=
"--accent: var(--blue)
"
>
<div
class=
"metric
-copy"
>
<span>
总库人数
</span
>
<strong>
{{
store
.
poolCandidates
.
length
}}
</strong
>
<small>
公司共享人才池(跨账号)
</small
>
</div>
<
/article
>
<article
class=
"metric-card"
style=
"--accent: var(--green)
"
>
<div
class=
"metric-copy"
>
<
span>
本周新入池
</span
>
<strong>
{{
newThisWeek
}}
</strong>
<
small>
近 7 天放入总库
</small
>
<
!-- 台账头:池况一句话 + 关键数字 --
>
<section
class=
"hero-strip
"
>
<div
class=
"hero
-copy"
>
<p
class=
"hero-eyebrow"
>
COMPANY POOL · 共享总库
</p
>
<h2>
公司人才池
</h2
>
<p
class=
"hero-sub"
>
淘汰、暂缓、储备的候选档案在这里沉淀——跨账号可见,检索到谁,谁认领回自己的流程。
</p
>
</div>
<
dl
class=
"hero-stats"
>
<div
class=
"stat-cell
"
>
<dt>
在库
</dt
>
<
dd
class=
"stat-num"
:class=
"
{ 'is-empty': store.poolLoading }"
>
{{
store
.
poolLoading
?
'—'
:
store
.
poolCandidates
.
length
}}
<
/dd
>
</div>
</article>
<article
class=
"metric-card"
style=
"--accent: var(--yellow)"
>
<div
class=
"metric-copy"
>
<span>
待跟进
</span>
<strong>
{{
followUpCount
}}
</strong>
<small>
重点跟进 / 待二面 / 待沟通 / 需维护
</small>
<div
class=
"stat-cell"
>
<dt>
本周入池
</dt>
<dd
class=
"stat-num accent-up"
:class=
"
{ 'is-empty': store.poolLoading }">
{{
store
.
poolLoading
?
'—'
:
`+${newThisWeek
}
`
}}
<
/dd
>
<
/div
>
</article>
</div>
<section
class=
"card pool-card"
>
<header
class=
"list-head"
>
<div
class=
"list-head-copy"
>
<span
class=
"section-kicker"
>
总库 · 人才池
</span>
<h2>
公司共享人才池
</h2>
<p>
面试淘汰 / 暂缓 / 储备的候选人都汇聚到这里,任一招聘账号都可查看并认领。
</p>
<
div
class
=
"stat-cell"
>
<
dt
>
待跟进
<
/dt
>
<
dd
class
=
"stat-num"
:
class
=
"{ 'is-empty': store.poolLoading
}
"
>
{{
store
.
poolLoading
?
'—'
:
followUpCount
}}
<
/dd
>
<
/div
>
<div
class=
"head-actions"
>
<el-button
round
@
click=
"goResumes"
>
回简历库
</el-button>
<
div
class
=
"stat-cell"
>
<
dt
>
未标记
<
/dt
>
<
dd
class
=
"stat-num"
:
class
=
"{ 'is-empty': store.poolLoading
}
"
>
{{
store
.
poolLoading
?
'—'
:
unmarkedCount
}}
<
/dd
>
<
/div
>
</header>
<div
class=
"filter-row"
>
<el-input
v-model=
"filters.q"
class=
"filter-search"
placeholder=
"搜索姓名、岗位、学校、技能、备注"
clearable
/>
<el-select
v-model=
"filters.tag"
placeholder=
"标记"
>
<el-option
v-for=
"option in tagOptions"
:key=
"option"
:label=
"option"
:value=
"option"
/>
</el-select>
<el-select
v-model=
"filters.source"
placeholder=
"来源"
>
<el-option
v-for=
"option in sourceOptions"
:key=
"option"
:label=
"option"
:value=
"option"
/>
</el-select>
<el-button
v-if=
"store.poolLoading"
round
disabled
>
加载中…
</el-button>
<
/dl
>
<
div
class
=
"hero-action"
>
<
el
-
button
round
class
=
"ghost-btn"
@
click
=
"goResumes"
>
回简历库
<
/el-button
>
<
/div
>
<
/section
>
<div
v-if=
"filtered.length"
class=
"pool-grid
"
>
<article
v-for=
"candidate in filtered"
:key=
"candidate.id"
class=
"pool-card-item
"
tabindex=
"0
"
@
click=
"openDetail(candidate)
"
@
keydown
.
enter=
"openDetail(candidate)
"
<
section
class
=
"pool-panel
"
>
<!--
检索导卡带
(
签名元素
):
像档案目录的导卡
,
一张卡一种检索口径
-->
<
div
class
=
"guide-rail"
>
<
div
v
-
for
=
"section in guideSections
"
:
key
=
"section.caption
"
class
=
"guide-zone
"
:
class
=
"{ 'is-divider': section.caption === '标记'
}
"
>
<div
class=
"pool-person"
>
<div
class=
"pool-avatar"
>
{{
(
candidate
.
name
||
'候'
).
slice
(
0
,
1
).
toUpperCase
()
}}
</div>
<div
class=
"pool-copy"
>
<div
class=
"pool-name-row"
>
<strong>
{{
candidate
.
name
||
'未命名候选人'
}}
</strong>
<span
v-if=
"candidate.stage"
class=
"stage-chip"
>
{{
candidate
.
stage
}}
</span>
</div>
<div
class=
"pool-sub"
>
{{
candidate
.
jobTitle
||
'待匹配岗位'
}}
</div>
<div
class=
"pool-meta"
>
<span
v-if=
"candidate.source && candidate.source !== '其他'"
>
{{
candidate
.
source
}}
</span>
<span
v-if=
"candidate.pooledByLabel || candidate.pooledBy"
>
入池:
{{
candidate
.
pooledByLabel
||
candidate
.
pooledBy
}}
</span
>
<span
v-if=
"candidate.pooledAt"
>
于
{{
formatPooledAt
(
candidate
.
pooledAt
)
}}
</span>
</div>
</div>
<
span
class
=
"guide-caption"
>
{{
section
.
caption
}}
<
/span
>
<
div
class
=
"guide-tabs"
>
<
button
v
-
for
=
"tab in section.tabs"
:
key
=
"tab.key"
class
=
"guide-tab"
:
class
=
"{ 'is-active': isTagActive(tab)
}
"
type
=
"button"
:
title
=
"tab.hint || tab.label"
:
aria
-
pressed
=
"isTagActive(tab)"
@
click
=
"selectTag(tab)"
>
<
span
class
=
"gt-line"
>
<
i
class
=
"gt-dot"
:
style
=
"{ background: tab.tone
}
"
><
/i
>
<
span
class
=
"gt-label"
>
{{
tab
.
label
}}
<
/span
>
<
b
class
=
"gt-count"
:
style
=
"{ color: tab.tone
}
"
>
{{
tab
.
count
}}
<
/b
>
<
/span
>
<
span
class
=
"gt-gauge"
:
class
=
"{ 'is-none': tab.key === NONE_TAG
}
"
>
<
i
class
=
"gt-gauge-fill"
:
style
=
"{
width: `${Math.max(3, Math.round((tab.count / Math.max(1, poolView.length)) * 100))
}
%`,
background: tab.tone,
}
"
><
/i
>
<
/span
>
<
/button
>
<
/div
>
<
/div
>
<
/div
>
<div
class=
"pool-profile"
:class=
"
{ 'is-empty': !profileSummary(candidate) }">
{{
profileSummary
(
candidate
)
||
'档案字段待补全'
}}
</div>
<!--
检索工具行
-->
<
div
class
=
"toolbar"
>
<
el
-
input
v
-
model
=
"filters.q"
class
=
"tool-search"
placeholder
=
"姓名、岗位、学校、技能、备注"
clearable
>
<
template
#
prefix
><
el
-
icon
class
=
"tool-search-icon"
><
Search
/><
/el-ico
n
><
/template
>
<
/el-input
>
<
span
class
=
"ctl"
>
<
span
class
=
"ctl-label"
>
来源
<
/span
>
<
el
-
select
v
-
model
=
"filters.source"
class
=
"ctl-select"
placeholder
=
"来源"
>
<
el
-
option
v
-
for
=
"option in sourceOptions"
:
key
=
"option"
:
label
=
"option"
:
value
=
"option"
/>
<
/el-select
>
<
/span
>
<
span
class
=
"ctl"
>
<
span
class
=
"ctl-label"
>
排序
<
/span
>
<
el
-
select
v
-
model
=
"filters.sort"
class
=
"ctl-select"
placeholder
=
"排序"
>
<
el
-
option
v
-
for
=
"option in sortOptions"
:
key
=
"option.value"
:
label
=
"option.label"
:
value
=
"option.value"
/>
<
/el-select
>
<
/span
>
<
span
class
=
"toolbar-spacer"
><
/span
>
<
span
v
-
if
=
"hasFilters"
class
=
"hit-meta"
>
命中
<
b
>
{{
sorted
.
length
}}
<
/b> /
{{
poolView
.
length
}}
<
button
type
=
"button"
class
=
"clear-filters"
@
click
=
"resetFilters"
>
清除筛选
<
/button
>
<
/span
>
<
span
v
-
else
class
=
"hit-meta"
>
共
<
b
>
{{
poolView
.
length
}}
<
/b> 人</
span
>
<
div
class
=
"density"
role
=
"group"
aria
-
label
=
"展示密度"
>
<
button
type
=
"button"
class
=
"density-btn"
:
class
=
"{ 'is-active': density === 'list'
}
"
title
=
"目录列表"
@
click
=
"persistDensity('list')"
>
<
el
-
icon
><
List
/><
/el-icon
>
<
/button
>
<
button
type
=
"button"
class
=
"density-btn"
:
class
=
"{ 'is-active': density === 'grid'
}
"
title
=
"卡片视图"
@
click
=
"persistDensity('grid')"
>
<
el
-
icon
><
Grid
/><
/el-icon
>
<
/button
>
<
/div
>
<
/div
>
<!--
结果区
-->
<
div
class
=
"results"
>
<!--
骨架屏
-->
<
div
v
-
if
=
"store.poolLoading"
class
=
"skeleton"
aria
-
hidden
=
"true"
>
<
div
v
-
for
=
"i in 5"
:
key
=
"i"
class
=
"skel-row"
><
/div
>
<
/div
>
<div
v-if=
"candidate.tags && candidate.tags.length"
class=
"pool-tags"
>
<el-tag
v-for=
"tag in candidate.tags"
:key=
"tag"
size=
"small"
:type=
"candidateTagTone(tag)"
effect=
"light"
>
{{
tag
}}
</el-tag
>
<!--
列表
(
台账目录
,
适合大库
)
-->
<
div
v
-
else
-
if
=
"visible.length && density === 'list'"
class
=
"ledger"
>
<
div
class
=
"ledger-head"
>
<
span
>
候选人
<
/span
>
<
span
>
档案速览
<
/span
>
<
span
>
标记
<
/span
>
<
span
>
来源
·
入池
<
/span
>
<
span
class
=
"col-actions"
>
操作
<
/span
>
<
/div
>
<
article
v
-
for
=
"candidate in visible"
:
key
=
"candidate.id"
class
=
"ledger-row"
:
style
=
"{ '--row-tone': firstTone(candidate)
}
"
tabindex
=
"0"
@
click
=
"openDetail(candidate)"
@
keydown
.
enter
=
"openDetail(candidate)"
>
<
div
class
=
"lr-person"
>
<
span
class
=
"person-tile"
>
{{
avatarText
(
candidate
)
}}
<
/span
>
<
span
class
=
"person-copy"
>
<
strong
class
=
"person-name"
>
{{
candidate
.
name
||
'未命名候选人'
}}
<
/strong
>
<
span
class
=
"person-role"
>
{{
roleLine
(
candidate
)
}}
<
/span
>
<
/span
>
<
/div
>
<
div
class
=
"lr-profile"
:
class
=
"{ 'is-empty': !profileSummary(candidate)
}
"
:
title
=
"profileSummary(candidate)"
>
{{
profileSummary
(
candidate
)
||
'档案待补全'
}}
<
/div
>
<
div
class
=
"lr-tags"
>
<
template
v
-
if
=
"candidate.tags && candidate.tags.length"
>
<
span
v
-
for
=
"tag in visibleTags(candidate).shown"
:
key
=
"tag"
class
=
"tag-chip"
:
style
=
"{ '--tone': tagColor(tag)
}
"
>
{{
tag
}}
<
/spa
n
>
<
span
v
-
if
=
"visibleTags(candidate).extra"
class
=
"tag-more"
>+
{{
visibleTags
(
candidate
).
extra
}}
<
/span
>
<
/template
>
<
span
v
-
else
class
=
"tag-chip is-none"
>
未标记
<
/span
>
<
/div
>
<
div
class
=
"lr-meta"
>
<
span
class
=
"lr-source"
>
{{
candidate
.
source
||
'其他'
}}
<
/span
>
<
span
class
=
"lr-by"
:
title
=
"`${pooledByText(candidate)
}
放入 · ${formatPooledAt(candidate.pooledAt)
}
`"
>
<
b
>
{{
pooledByText
(
candidate
)
}}
<
/b> · {{ formatPooledShort
(
candidate.pooledAt
)
|| '—'
}}
<
/span
>
<
/div
>
<
div
class
=
"lr-actions col-actions"
@
click
.
stop
>
<
button
type
=
"button"
class
=
"row-link"
@
click
=
"openDetail(candidate)"
>
查看
<
/button
>
<
el
-
button
size
=
"small"
round
type
=
"primary"
@
click
=
"claim(candidate)"
>
认领
<
/el-button
>
<
/div
>
<
/article
>
<
/div
>
<div
class=
"pool-actions"
@
click
.
stop
>
<el-button
size=
"small"
round
plain
@
click=
"openDetail(candidate)"
>
查看
</el-button>
<el-button
size=
"small"
round
type=
"primary"
@
click=
"claim(candidate)"
>
认领
</el-button>
</div>
</article>
</div>
<!--
卡片视图
(
小库浏览
)
-->
<
div
v
-
else
-
if
=
"visible.length && density === 'grid'"
class
=
"pool-grid"
>
<
article
v
-
for
=
"candidate in visible"
:
key
=
"candidate.id"
class
=
"pool-card"
:
style
=
"{ '--row-tone': firstTone(candidate)
}
"
tabindex
=
"0"
@
click
=
"openDetail(candidate)"
@
keydown
.
enter
=
"openDetail(candidate)"
>
<
div
class
=
"pc-head"
>
<
span
class
=
"person-tile"
>
{{
avatarText
(
candidate
)
}}
<
/span
>
<
span
class
=
"pc-head-copy"
>
<
strong
>
{{
candidate
.
name
||
'未命名候选人'
}}
<
/strong
>
<
span
class
=
"pc-role"
>
{{
roleLine
(
candidate
)
}}
<
/span
>
<
/span
>
<
/div
>
<
div
class
=
"pc-tags"
>
<
template
v
-
if
=
"candidate.tags && candidate.tags.length"
>
<
span
v
-
for
=
"tag in candidate.tags.slice(0, 3)"
:
key
=
"tag"
class
=
"tag-chip"
:
style
=
"{ '--tone': tagColor(tag)
}
"
>
{{
tag
}}
<
/spa
n
>
<
span
v
-
if
=
"candidate.tags.length > 3"
class
=
"tag-more"
>+
{{
candidate
.
tags
.
length
-
3
}}
<
/span
>
<
/template
>
<
span
v
-
else
class
=
"tag-chip is-none"
>
未标记
<
/span
>
<
/div
>
<
p
class
=
"pc-profile"
:
class
=
"{ 'is-empty': !profileSummary(candidate)
}
"
>
{{
profileSummary
(
candidate
)
||
'档案待补全'
}}
<
/p
>
<
footer
class
=
"pc-foot"
>
<
span
class
=
"lr-source"
>
{{
candidate
.
source
||
'其他'
}}
<
/span
>
<
span
class
=
"lr-by"
:
title
=
"`${pooledByText(candidate)
}
放入 · ${formatPooledAt(candidate.pooledAt)
}
`"
>
{{
pooledByText
(
candidate
)
}}
·
{{
formatPooledShort
(
candidate
.
pooledAt
)
||
'—'
}}
<
/span
>
<
span
class
=
"pc-actions"
@
click
.
stop
>
<
el
-
button
size
=
"small"
round
plain
@
click
=
"openDetail(candidate)"
>
查看
<
/el-button
>
<
el
-
button
size
=
"small"
round
type
=
"primary"
@
click
=
"claim(candidate)"
>
认领
<
/el-button
>
<
/span
>
<
/footer
>
<
/article
>
<
/div
>
<!--
空态
-->
<
div
v
-
else
class
=
"empty-state"
>
<
template
v
-
if
=
"poolView.length"
>
<
strong
>
没有命中结果
<
/strong
>
<
p
>
换一个关键词、来源或导卡试试;池子里的其他人可能更适合你现在的岗位。
<
/p
>
<
button
v
-
if
=
"hasFilters"
type
=
"button"
class
=
"empty-btn"
@
click
=
"resetFilters"
>
清除全部筛选
<
/button
>
<
/template
>
<
template
v
-
else
>
<
strong
>
人才池还没有档案
<
/strong
>
<
p
>
在简历库中把「面完未通过
/
暂缓
/
储备」的候选人移入人才池,公司共享总库就会在这里展示。
<
/p
>
<
button
type
=
"button"
class
=
"empty-btn"
@
click
=
"goResumes"
>
去简历库移入候选人
<
/button
>
<
/template
>
<
/div
>
<div
v-else
class=
"empty-state"
>
<
strong>
{{
store
.
poolCandidates
.
length
?
'没有找到匹配的候选人'
:
'人才池还是空的'
}}
</strong
>
<p
v-if=
"store.poolCandidates.length"
>
换个关键词或清除筛选条件试试。
</p
>
<p
v-else
>
在简历库中把「面完未通过 / 暂缓 / 储备」的候选人移入人才池,公司共享总库就会在这里展示。
</p
>
<el-button
v-if=
"!store.poolCandidates.length"
round
type=
"primary"
@
click=
"goResumes"
>
去简历库移入候选人
</el-button
>
<!--
渐进加载
:
数量大时按批渲染
,
保持滚动顺畅
--
>
<
div
v
-
if
=
"!store.poolLoading && visible.length < sorted.length"
class
=
"load-more"
>
<
span
class
=
"load-caption"
>
已显示
{{
visible
.
length
}}
/
{{
sorted
.
length
}}
<
/span
>
<
button
type
=
"button"
class
=
"load-more-btn"
@
click
=
"visibleCount += LOAD_STEP"
>
再显示
{{
Math
.
min
(
LOAD_STEP
,
sorted
.
length
-
visible
.
length
)
}}
人
<
/button
>
<
/div
>
<
/div
>
<
/section
>
...
...
@@ -246,14 +584,14 @@ onMounted(() => {
<
header
class
=
"detail-head"
>
<
div
>
<
h3
>
{{
selected
.
name
||
'未命名候选人'
}}
<
/h3
>
<p>
{{
selected
.
jobTitle
||
'待匹配岗位'
}}
</p>
<
p
>
{{
roleLine
(
selected
)
}}
<
/p
>
<
/div
>
<
div
class
=
"detail-head-side"
>
<
div
class
=
"detail-head-meta"
>
<
el-tag
size=
"small"
effect=
"plain"
>
{{
selected
.
source
||
'其他'
}}
</el-tag
>
<
el-tag
v-if=
"selected.pooledByLabel || selected.pooledBy"
size=
"small"
type=
"info"
effect=
"plain
"
>
{{
selected
.
pooledByLabel
||
selected
.
pooledBy
}}
放入
</
el-tag
>
<
span
class
=
"tag-chip is-source"
>
{{
selected
.
source
||
'其他'
}}
<
/span
>
<
span
v
-
if
=
"selected.pooledByLabel || selected.pooledBy"
class
=
"tag-chip is-put
"
>
{{
pooledByText
(
selected
)
}}
放入
<
/
span
>
<
/div
>
<
el
-
button
text
circle
aria
-
label
=
"关闭"
title
=
"关闭"
@
click
=
"closeDetail"
>
<
el
-
icon
><
Close
/><
/el-icon
>
...
...
@@ -292,9 +630,9 @@ onMounted(() => {
<
/div
>
<
div
v
-
if
=
"selected.tags && selected.tags.length"
class
=
"detail-tags"
>
<
el-tag
v-for=
"tag in selected.tags"
:key=
"tag"
size=
"small"
:type=
"candidateTagTone(tag)"
effect=
"light
"
>
{{
<
span
v
-
for
=
"tag in selected.tags"
:
key
=
"tag"
class
=
"tag-chip"
:
style
=
"{ '--tone': tagColor(tag)
}
"
>
{{
tag
}}
</
el-tag
>
}}
<
/
span
>
<
/div
>
<
div
class
=
"detail-resume"
>
...
...
@@ -320,217 +658,784 @@ onMounted(() => {
<
/el-drawer
>
<
/div
>
<
/template
>
<
style
lang
=
"scss"
scoped
>
.
pool
-
page
{
--
p
-
ink
:
#
16203
a
;
--
p
-
muted
:
#
667187
;
--
p
-
line
:
#
e4e9f2
;
--
p
-
rail
:
#
f1f5fa
;
display
:
flex
;
flex
-
direction
:
column
;
gap
:
16px
;
}
.metric-grid
{
display
:
grid
;
grid-template-columns
:
repeat
(
3
,
minmax
(
0
,
1fr
));
gap
:
14
px
;
}
.metric-card
{
/* ---------- 台账头 ---------- */
.
hero
-
strip
{
display
:
flex
;
align
-
items
:
center
;
min-height
:
9
2px
;
padding
:
1
6px
18
px
;
gap
:
2
2
px
;
padding
:
1
8
px
20
px
;
background
:
var
(
--
panel
);
border
:
1px
solid
var
(
--
line
);
border-radius
:
14px
;
box-shadow
:
0
8px
20px
rgba
(
39
,
49
,
70
,
0
.06
);
border-top
:
3px
solid
var
(
--
accent
);
border
:
1
px
solid
var
(
--
p
-
line
);
border
-
radius
:
16
px
;
box
-
shadow
:
0
1
px
2
px
rgba
(
16
,
26
,
48
,
0.04
),
0
14
px
34
px
-
22
px
rgba
(
16
,
26
,
48
,
0.22
);
.
hero
-
copy
{
min
-
width
:
0
;
flex
:
1
1
auto
;
.
hero
-
eyebrow
{
margin
:
0
0
5
px
;
font
-
size
:
10
px
;
font
-
weight
:
700
;
letter
-
spacing
:
2.5
px
;
color
:
var
(
--
blue
);
}
.metric-copy
{
display
:
flex
;
flex-direction
:
column
;
h2
{
margin
:
0
0
4
px
;
font
-
size
:
21
px
;
letter
-
spacing
:
0.3
px
;
color
:
var
(
--
p
-
ink
);
}
span
{
font-size
:
13px
;
color
:
var
(
--
muted
);
.
hero
-
sub
{
margin
:
0
;
font
-
size
:
12.5
px
;
color
:
var
(
--
p
-
muted
);
line
-
height
:
1.6
;
}
}
strong
{
font-size
:
28px
;
line-height
:
1
.2
;
.
hero
-
stats
{
display
:
flex
;
margin
:
0
;
flex
:
0
0
auto
;
.
stat
-
cell
{
min
-
width
:
86
px
;
padding
:
2
px
20
px
0
;
border
-
left
:
1
px
solid
var
(
--
p
-
line
);
dt
{
font
-
size
:
10.5
px
;
letter
-
spacing
:
1.5
px
;
color
:
var
(
--
p
-
muted
);
margin
-
bottom
:
5
px
;
}
.
stat
-
num
{
margin
:
0
;
font
-
size
:
25
px
;
font
-
weight
:
700
;
line
-
height
:
1
;
font
-
variant
-
numeric
:
tabular
-
nums
;
color
:
var
(
--
p
-
ink
);
&
.
accent
-
up
{
color
:
#
1
f7a5a
;
}
&
.
is
-
empty
{
color
:
var
(
--
p
-
muted
);
}
}
}
}
small
{
font-size
:
12px
;
color
:
var
(
--
muted
);
.
hero
-
action
{
flex
:
0
0
auto
;
padding
-
left
:
4
px
;
.
ghost
-
btn
{
border
-
color
:
var
(
--
p
-
line
);
color
:
var
(
--
p
-
muted
);
background
:
#
fff
;
font
-
size
:
12.5
px
;
&
:
hover
{
border
-
color
:
var
(
--
blue
);
color
:
var
(
--
blue
);
}
}
}
}
.card
{
/* ---------- 面板 ---------- */
.
pool
-
panel
{
background
:
var
(
--
panel
);
border
:
1px
solid
var
(
--
line
);
border-radius
:
14px
;
box-shadow
:
0
8px
20px
rgba
(
39
,
49
,
70
,
0
.06
);
border
:
1
px
solid
var
(
--
p
-
line
);
border
-
radius
:
16
px
;
box
-
shadow
:
0
1
px
2
px
rgba
(
16
,
26
,
48
,
0.04
),
0
14
px
34
px
-
22
px
rgba
(
16
,
26
,
48
,
0.22
);
overflow
:
hidden
;
}
.pool-card
{
padding
:
18px
;
/* ---------- 检索导卡带(签名元素) ---------- */
.
guide
-
rail
{
display
:
flex
;
flex
-
direction
:
column
;
gap
:
8
px
;
padding
:
12
px
16
px
;
background
:
var
(
--
p
-
rail
);
border
-
bottom
:
1
px
solid
var
(
--
p
-
line
);
}
.
list-head
{
.
guide
-
zone
{
display
:
flex
;
align
-
items
:
flex
-
start
;
justify-content
:
space-between
;
gap
:
12px
;
margin-bottom
:
14px
;
.section-kicker
{
font-size
:
12px
;
letter-spacing
:
1px
;
color
:
var
(
--
blue
);
font-weight
:
600
;
}
gap
:
6
px
;
padding
-
top
:
2
px
;
h2
{
margin
:
2px
0
4px
;
font-size
:
18
px
;
&
.
is
-
divider
{
border
-
top
:
1
px
dashed
var
(
--
p
-
line
)
;
padding
-
top
:
10
px
;
}
p
{
margin
:
0
;
color
:
var
(
--
muted
);
font-size
:
13px
;
.
guide
-
caption
{
flex
:
0
0
auto
;
width
:
34
px
;
padding
-
top
:
9
px
;
font
-
size
:
10
px
;
letter
-
spacing
:
2
px
;
color
:
var
(
--
p
-
muted
);
user
-
select
:
none
;
}
}
.filter-row
{
display
:
flex
;
flex-wrap
:
wrap
;
gap
:
10px
;
margin-bottom
:
14px
;
.filter-search
{
width
:
260px
;
.
guide
-
tabs
{
flex
:
1
;
display
:
flex
;
flex
-
wrap
:
wrap
;
gap
:
6
px
;
}
}
.pool-grid
{
display
:
grid
;
grid-template-columns
:
repeat
(
auto-fill
,
minmax
(
300px
,
1fr
));
gap
:
12px
;
}
.pool-card-item
{
.
guide
-
tab
{
display
:
flex
;
flex
-
direction
:
column
;
gap
:
8px
;
padding
:
14px
;
background
:
#fff
;
border
:
1px
solid
var
(
--
line
);
border-radius
:
12px
;
gap
:
4
px
;
min
-
width
:
64
px
;
padding
:
7
px
11
px
6
px
;
border
:
1
px
solid
transparent
;
border
-
radius
:
10
px
;
background
:
transparent
;
color
:
var
(
--
p
-
muted
);
cursor
:
pointer
;
text
-
align
:
left
;
transition
:
box-shadow
0
.2s
,
transform
0
.2s
;
background
0.15
s
ease
,
border
-
color
0.15
s
ease
,
box
-
shadow
0.15
s
ease
;
&
:
hover
{
box-shadow
:
0
10px
24px
rgba
(
39
,
49
,
70
,
0
.1
);
transform
:
translateY
(
-1px
);
background
:
rgba
(
255
,
255
,
255
,
0.72
);
}
}
.pool-person
{
&
:
focus
-
visible
{
outline
:
2
px
solid
var
(
--
cyan
);
outline
-
offset
:
1
px
;
}
&
.
is
-
active
{
background
:
#
fff
;
border
-
color
:
var
(
--
p
-
line
);
box
-
shadow
:
0
3
px
10
px
-
3
px
rgba
(
16
,
26
,
48
,
0.14
);
color
:
var
(
--
p
-
ink
);
.
gt
-
label
{
font
-
weight
:
700
;
}
}
.
gt
-
line
{
display
:
flex
;
align
-
items
:
center
;
gap
:
6
px
;
line
-
height
:
1
;
.
gt
-
dot
{
width
:
6
px
;
height
:
6
px
;
border
-
radius
:
50
%
;
flex
-
shrink
:
0
;
}
.
gt
-
label
{
font
-
size
:
12.5
px
;
}
.
gt
-
count
{
margin
-
left
:
auto
;
font
-
size
:
12
px
;
font
-
variant
-
numeric
:
tabular
-
nums
;
font
-
weight
:
700
;
}
}
.
gt
-
gauge
{
display
:
block
;
height
:
3
px
;
border
-
radius
:
2
px
;
background
:
rgba
(
16
,
26
,
48
,
0.08
);
overflow
:
hidden
;
.
gt
-
gauge
-
fill
{
display
:
block
;
height
:
100
%
;
border
-
radius
:
2
px
;
}
&
.
is
-
none
{
background
:
repeating
-
linear
-
gradient
(
90
deg
,
rgba
(
16
,
26
,
48
,
0.08
)
0
3
px
,
transparent
3
px
6
px
);
.
gt
-
gauge
-
fill
{
display
:
none
;
}
}
}
}
/* ---------- 工具行 ---------- */
.
toolbar
{
display
:
flex
;
align
-
items
:
center
;
flex
-
wrap
:
wrap
;
gap
:
10
px
;
padding
:
12
px
16
px
;
border
-
bottom
:
1
px
solid
var
(
--
p
-
line
);
.
tool
-
search
{
width
:
300
px
;
max
-
width
:
40
vw
;
}
.
tool
-
search
-
icon
{
color
:
var
(
--
p
-
muted
);
}
.
ctl
{
display
:
inline
-
flex
;
align
-
items
:
center
;
gap
:
6
px
;
.
ctl
-
label
{
font
-
size
:
12
px
;
color
:
var
(
--
p
-
muted
);
}
.
ctl
-
select
{
width
:
128
px
;
}
}
.
toolbar
-
spacer
{
flex
:
1
1
auto
;
}
.
hit
-
meta
{
display
:
inline
-
flex
;
align
-
items
:
center
;
gap
:
8
px
;
font
-
size
:
12
px
;
color
:
var
(
--
p
-
muted
);
font
-
variant
-
numeric
:
tabular
-
nums
;
white
-
space
:
nowrap
;
b
{
color
:
var
(
--
p
-
ink
);
}
.
clear
-
filters
{
border
:
none
;
background
:
none
;
color
:
var
(
--
blue
);
font
-
size
:
12
px
;
cursor
:
pointer
;
padding
:
2
px
4
px
;
border
-
radius
:
6
px
;
&
:
hover
{
background
:
rgba
(
69
,
105
,
201
,
0.08
);
}
&
:
focus
-
visible
{
outline
:
2
px
solid
var
(
--
cyan
);
outline
-
offset
:
1
px
;
}
}
}
.
density
{
display
:
inline
-
flex
;
padding
:
2
px
;
background
:
var
(
--
p
-
rail
);
border
:
1
px
solid
var
(
--
p
-
line
);
border
-
radius
:
9
px
;
.
density
-
btn
{
display
:
inline
-
flex
;
align
-
items
:
center
;
justify
-
content
:
center
;
width
:
26
px
;
height
:
24
px
;
border
:
none
;
border
-
radius
:
7
px
;
background
:
transparent
;
color
:
var
(
--
p
-
muted
);
cursor
:
pointer
;
font
-
size
:
14
px
;
&
:
hover
{
color
:
var
(
--
p
-
ink
);
}
&
.
is
-
active
{
background
:
#
fff
;
color
:
var
(
--
blue
);
box
-
shadow
:
0
1
px
4
px
rgba
(
16
,
26
,
48
,
0.12
);
}
&
:
focus
-
visible
{
outline
:
2
px
solid
var
(
--
cyan
);
outline
-
offset
:
1
px
;
}
}
}
}
.pool-avatar
{
width
:
40px
;
height
:
40px
;
border-radius
:
50%
;
display
:
flex
;
/* ---------- 通用标记 ---------- */
.
tag
-
chip
{
display
:
inline
-
flex
;
align
-
items
:
center
;
justify-content
:
center
;
color
:
#fff
;
font-weight
:
700
;
background
:
linear-gradient
(
135deg
,
#5b8def
,
#4569c9
);
height
:
20
px
;
padding
:
0
8
px
;
border
-
radius
:
6
px
;
font
-
size
:
11
px
;
line
-
height
:
1
;
white
-
space
:
nowrap
;
background
:
color
-
mix
(
in
srgb
,
var
(
--
tone
,
#
4569
c9
)
10
%
,
#
fff
);
color
:
var
(
--
tone
,
#
4569
c9
);
border
:
1
px
solid
color
-
mix
(
in
srgb
,
var
(
--
tone
,
#
4569
c9
)
26
%
,
transparent
);
&
.
is
-
none
{
--
tone
:
#
9
aa3b2
;
border
-
style
:
dashed
;
background
:
transparent
;
}
&
.
is
-
source
{
--
tone
:
#
4
d5c75
;
}
&
.
is
-
put
{
--
tone
:
#
5
f6b80
;
}
}
.
pool-copy
{
min-width
:
0
;
flex
:
1
;
.
tag
-
more
{
font
-
size
:
11
px
;
color
:
var
(
--
p
-
muted
)
;
}
.pool-name-row
{
/* ---------- 台账目录列表 ---------- */
.
ledger
{
display
:
flex
;
flex
-
direction
:
column
;
}
.
ledger
-
head
,
.
ledger
-
row
{
display
:
grid
;
grid
-
template
-
columns
:
minmax
(
210
px
,
1.55f
r
)
minmax
(
150
px
,
1.05f
r
)
minmax
(
108
px
,
auto
)
minmax
(
150
px
,
0.9f
r
)
minmax
(
132
px
,
auto
);
gap
:
14
px
;
align
-
items
:
center
;
gap
:
6px
;
padding
:
0
16
px
;
}
strong
{
font-size
:
15px
;
.
ledger
-
head
{
min
-
height
:
32
px
;
background
:
#
f8fafc
;
border
-
bottom
:
1
px
solid
var
(
--
p
-
line
);
font
-
size
:
10
px
;
letter
-
spacing
:
1.5
px
;
color
:
var
(
--
p
-
muted
);
.
col
-
actions
{
text
-
align
:
right
;
}
}
.stage-chip
{
font-size
:
11px
;
padding
:
1px
6px
;
border-radius
:
999px
;
background
:
rgba
(
69
,
105
,
201
,
0
.1
);
color
:
#4569c9
;
.
ledger
-
row
{
position
:
relative
;
min
-
height
:
62
px
;
border
-
bottom
:
1
px
solid
#
eef1f6
;
cursor
:
pointer
;
transition
:
background
0.15
s
ease
;
&
::
before
{
content
:
''
;
position
:
absolute
;
left
:
0
;
top
:
0
;
bottom
:
0
;
width
:
3
px
;
background
:
transparent
;
transition
:
background
0.15
s
ease
;
}
&
:
hover
{
background
:
#
f7faff
;
&
::
before
{
background
:
var
(
--
row
-
tone
,
#
4569
c9
);
}
}
&
:
focus
-
visible
{
outline
:
2
px
solid
var
(
--
cyan
);
outline
-
offset
:
-
2
px
;
}
&
:
last
-
child
{
border
-
bottom
:
none
;
}
}
.
lr
-
person
{
display
:
flex
;
align
-
items
:
center
;
gap
:
11
px
;
min
-
width
:
0
;
}
.pool-sub
{
font-size
:
13px
;
color
:
#4569c9
;
margin-top
:
2px
;
.
person
-
tile
{
display
:
inline
-
flex
;
align
-
items
:
center
;
justify
-
content
:
center
;
flex
-
shrink
:
0
;
width
:
36
px
;
height
:
36
px
;
border
-
radius
:
10
px
;
background
:
color
-
mix
(
in
srgb
,
var
(
--
row
-
tone
,
#
4569
c9
)
11
%
,
#
fff
);
color
:
var
(
--
row
-
tone
,
#
4569
c9
);
border
:
1
px
solid
color
-
mix
(
in
srgb
,
var
(
--
row
-
tone
,
#
4569
c9
)
24
%
,
transparent
);
font
-
size
:
14
px
;
font
-
weight
:
700
;
}
.pool-meta
{
.
person
-
copy
{
min
-
width
:
0
;
display
:
flex
;
flex-wrap
:
wrap
;
gap
:
8px
;
font-size
:
12px
;
color
:
var
(
--
muted
);
margin-top
:
4px
;
flex
-
direction
:
column
;
gap
:
2
px
;
.
person
-
name
{
font
-
size
:
13.5
px
;
color
:
var
(
--
p
-
ink
);
overflow
:
hidden
;
text
-
overflow
:
ellipsis
;
white
-
space
:
nowrap
;
}
.
person
-
role
{
font
-
size
:
12
px
;
color
:
var
(
--
p
-
muted
);
overflow
:
hidden
;
text
-
overflow
:
ellipsis
;
white
-
space
:
nowrap
;
}
}
.
pool
-profile
{
.
lr
-
profile
{
font
-
size
:
12
px
;
color
:
var
(
--
ink
);
color
:
color
-
mix
(
in
srgb
,
var
(
--
p
-
ink
)
82
%
,
#
4569
c9
);
overflow
:
hidden
;
text
-
overflow
:
ellipsis
;
white
-
space
:
nowrap
;
&
.
is
-
empty
{
color
:
var
(
--
muted
);
color
:
var
(
--
p
-
muted
);
}
}
.
pool
-tags
{
.
lr
-
tags
{
display
:
flex
;
align
-
items
:
center
;
flex
-
wrap
:
wrap
;
gap
:
4
px
;
min
-
width
:
0
;
}
.
pool-actions
{
.
lr
-
meta
{
display
:
flex
;
flex
-
direction
:
column
;
gap
:
4
px
;
min
-
width
:
0
;
.
lr
-
source
{
align
-
self
:
flex
-
start
;
display
:
inline
-
flex
;
align
-
items
:
center
;
height
:
18
px
;
padding
:
0
7
px
;
border
-
radius
:
5
px
;
background
:
#
eef3fa
;
color
:
#
4
d5c75
;
font
-
size
:
10.5
px
;
letter
-
spacing
:
0.5
px
;
}
.
lr
-
by
{
font
-
size
:
11.5
px
;
color
:
var
(
--
p
-
muted
);
font
-
variant
-
numeric
:
tabular
-
nums
;
overflow
:
hidden
;
text
-
overflow
:
ellipsis
;
white
-
space
:
nowrap
;
b
{
color
:
#
44506
a
;
font
-
weight
:
600
;
}
}
}
.
lr
-
actions
{
display
:
inline
-
flex
;
align
-
items
:
center
;
justify
-
content
:
flex
-
end
;
gap
:
8px
;
margin-top
:
auto
;
gap
:
6
px
;
.
row
-
link
{
border
:
none
;
background
:
none
;
color
:
var
(
--
p
-
muted
);
font
-
size
:
12.5
px
;
cursor
:
pointer
;
padding
:
4
px
2
px
;
border
-
radius
:
6
px
;
&
:
hover
{
color
:
var
(
--
blue
);
}
&
:
focus
-
visible
{
outline
:
2
px
solid
var
(
--
cyan
);
outline
-
offset
:
1
px
;
}
}
}
/* ---------- 卡片视图 ---------- */
.
pool
-
grid
{
display
:
grid
;
grid
-
template
-
columns
:
repeat
(
auto
-
fill
,
minmax
(
300
px
,
1
fr
));
gap
:
12
px
;
padding
:
16
px
;
}
.
pool
-
card
{
display
:
flex
;
flex
-
direction
:
column
;
gap
:
10
px
;
padding
:
14
px
;
border
:
1
px
solid
var
(
--
p
-
line
);
border
-
radius
:
13
px
;
background
:
#
fff
;
cursor
:
pointer
;
transition
:
box
-
shadow
0.18
s
ease
,
transform
0.18
s
ease
,
border
-
color
0.18
s
ease
;
&
:
hover
{
border
-
color
:
color
-
mix
(
in
srgb
,
var
(
--
row
-
tone
,
#
4569
c9
)
34
%
,
var
(
--
p
-
line
));
box
-
shadow
:
0
10
px
26
px
-
14
px
rgba
(
16
,
26
,
48
,
0.22
);
transform
:
translateY
(
-
2
px
);
}
&
:
focus
-
visible
{
outline
:
2
px
solid
var
(
--
cyan
);
outline
-
offset
:
1
px
;
}
.
pc
-
head
{
display
:
flex
;
align
-
items
:
center
;
gap
:
10
px
;
.
pc
-
head
-
copy
{
min
-
width
:
0
;
display
:
flex
;
flex
-
direction
:
column
;
gap
:
2
px
;
strong
{
font
-
size
:
14
px
;
color
:
var
(
--
p
-
ink
);
}
.
pc
-
role
{
font
-
size
:
12
px
;
color
:
var
(
--
p
-
muted
);
overflow
:
hidden
;
text
-
overflow
:
ellipsis
;
white
-
space
:
nowrap
;
}
}
}
.
pc
-
tags
{
display
:
flex
;
flex
-
wrap
:
wrap
;
gap
:
4
px
;
}
.
pc
-
profile
{
margin
:
0
;
font
-
size
:
12
px
;
color
:
color
-
mix
(
in
srgb
,
var
(
--
p
-
ink
)
82
%
,
#
4569
c9
);
line
-
height
:
1.55
;
display
:
-
webkit
-
box
;
-
webkit
-
line
-
clamp
:
2
;
-
webkit
-
box
-
orient
:
vertical
;
overflow
:
hidden
;
&
.
is
-
empty
{
color
:
var
(
--
p
-
muted
);
}
}
.
pc
-
foot
{
display
:
flex
;
align
-
items
:
center
;
flex
-
wrap
:
wrap
;
gap
:
6
px
10
px
;
margin
-
top
:
auto
;
padding
-
top
:
10
px
;
border
-
top
:
1
px
dashed
var
(
--
p
-
line
);
.
lr
-
by
{
font
-
size
:
11
px
;
color
:
var
(
--
p
-
muted
);
font
-
variant
-
numeric
:
tabular
-
nums
;
}
.
pc
-
actions
{
margin
-
left
:
auto
;
display
:
inline
-
flex
;
gap
:
6
px
;
}
}
}
/* ---------- 空态 / 加载 ---------- */
.
empty
-
state
{
padding
:
40px
16
px
;
padding
:
64
px
20
px
;
text
-
align
:
center
;
strong
{
display
:
block
;
font
-
size
:
16
px
;
color
:
var
(
--
p
-
ink
);
margin
-
bottom
:
7
px
;
}
p
{
color
:
var
(
--
muted
);
margin
:
0
auto
18
px
;
max
-
width
:
460
px
;
color
:
var
(
--
p
-
muted
);
font
-
size
:
13
px
;
margin
:
6px
0
14px
;
line
-
height
:
1.7
;
}
.
empty
-
btn
{
border
:
1
px
solid
var
(
--
p
-
line
);
background
:
#
fff
;
border
-
radius
:
999
px
;
padding
:
8
px
18
px
;
font
-
size
:
13
px
;
color
:
var
(
--
p
-
ink
);
cursor
:
pointer
;
transition
:
border
-
color
0.15
s
ease
,
color
0.15
s
ease
;
&
:
hover
{
border
-
color
:
var
(
--
blue
);
color
:
var
(
--
blue
);
}
&
:
focus
-
visible
{
outline
:
2
px
solid
var
(
--
cyan
);
outline
-
offset
:
1
px
;
}
}
}
.
skeleton
{
padding
:
8
px
16
px
18
px
;
.
skel
-
row
{
height
:
60
px
;
border
-
bottom
:
1
px
solid
#
eef1f6
;
background
:
linear
-
gradient
(
100
deg
,
#
f3f5f9
40
%
,
#
fafbfd
50
%
,
#
f3f5f9
60
%
);
background
-
size
:
200
%
100
%
;
animation
:
skel
-
shimmer
1.4
s
ease
-
in
-
out
infinite
;
}
}
@
keyframes
skel
-
shimmer
{
from
{
background
-
position
:
120
%
0
;
}
to
{
background
-
position
:
-
80
%
0
;
}
}
.
load
-
more
{
display
:
flex
;
align
-
items
:
center
;
justify
-
content
:
center
;
gap
:
14
px
;
padding
:
14
px
16
px
18
px
;
border
-
top
:
1
px
solid
#
eef1f6
;
.
load
-
caption
{
font
-
size
:
12
px
;
color
:
var
(
--
p
-
muted
);
font
-
variant
-
numeric
:
tabular
-
nums
;
}
.
load
-
more
-
btn
{
border
:
1
px
solid
var
(
--
p
-
line
);
background
:
#
fff
;
border
-
radius
:
999
px
;
padding
:
7
px
18
px
;
font
-
size
:
12.5
px
;
color
:
var
(
--
p
-
ink
);
cursor
:
pointer
;
transition
:
border
-
color
0.15
s
ease
,
color
0.15
s
ease
;
&
:
hover
{
border
-
color
:
var
(
--
blue
);
color
:
var
(
--
blue
);
}
&
:
focus
-
visible
{
outline
:
2
px
solid
var
(
--
cyan
);
outline
-
offset
:
1
px
;
}
}
}
/* ---------- 详情抽屉 ---------- */
.
pool
-
detail
{
padding
:
18
px
20
px
24
px
;
display
:
flex
;
...
...
@@ -548,11 +1453,12 @@ onMounted(() => {
h3
{
margin
:
0
;
font
-
size
:
20
px
;
color
:
var
(
--
p
-
ink
);
}
p
{
margin
:
2
px
0
0
;
color
:
var
(
--
muted
);
margin
:
3
px
0
0
;
color
:
var
(
--
p
-
muted
);
}
.
detail
-
head
-
side
{
...
...
@@ -576,11 +1482,11 @@ onMounted(() => {
display
:
flex
;
gap
:
8
px
;
font
-
size
:
13
px
;
border-bottom
:
1px
dashed
var
(
--
line
);
padding
:
4
px
0
;
border
-
bottom
:
1
px
dashed
var
(
--
p
-
line
);
padding
:
5
px
0
;
span
{
color
:
var
(
--
muted
);
color
:
var
(
--
p
-
muted
);
min
-
width
:
64
px
;
}
}
...
...
@@ -589,13 +1495,13 @@ onMounted(() => {
.
detail
-
tags
{
display
:
flex
;
flex
-
wrap
:
wrap
;
gap
:
4
px
;
gap
:
5
px
;
}
.
detail
-
resume
{
background
:
#f
6f8
fc
;
border
:
1px
solid
var
(
--
line
);
border-radius
:
1
0
px
;
background
:
#
f
7f9
fc
;
border
:
1
px
solid
var
(
--
p
-
line
);
border
-
radius
:
1
1
px
;
padding
:
12
px
;
.
detail
-
resume
-
head
{
...
...
@@ -610,7 +1516,8 @@ onMounted(() => {
white
-
space
:
pre
-
wrap
;
word
-
break
:
break
-
word
;
font
-
size
:
13
px
;
line-height
:
1
.6
;
line
-
height
:
1.65
;
color
:
color
-
mix
(
in
srgb
,
var
(
--
p
-
ink
)
90
%
,
#
4569
c9
);
max
-
height
:
40
vh
;
overflow
:
auto
;
}
...
...
@@ -623,12 +1530,99 @@ onMounted(() => {
gap
:
12
px
;
margin
-
top
:
auto
;
padding
-
top
:
12
px
;
border-top
:
1px
solid
var
(
--
line
);
border
-
top
:
1
px
solid
var
(
--
p
-
line
);
.
foot
-
hint
{
font
-
size
:
12
px
;
color
:
var
(
--
muted
);
color
:
var
(
--
p
-
muted
);
margin
-
right
:
auto
;
}
}
/* ---------- 响应式 ---------- */
@
media
(
max
-
width
:
1320
px
)
{
.
hero
-
strip
{
flex
-
wrap
:
wrap
;
}
.
hero
-
action
{
margin
-
left
:
auto
;
}
.
hero
-
stats
{
order
:
3
;
width
:
100
%
;
padding
-
top
:
12
px
;
border
-
top
:
1
px
solid
var
(
--
p
-
line
);
.
stat
-
cell
{
flex
:
1
;
border
-
left
:
none
;
padding
:
0
20
px
0
0
;
&
:
not
(:
first
-
child
)
{
padding
-
left
:
20
px
;
border
-
left
:
1
px
solid
var
(
--
p
-
line
);
}
}
}
}
@
media
(
max
-
width
:
1180
px
)
{
.
ledger
-
head
,
.
ledger
-
row
{
grid
-
template
-
columns
:
minmax
(
200
px
,
1.5f
r
)
minmax
(
96
px
,
auto
)
minmax
(
150
px
,
0.9f
r
)
minmax
(
132
px
,
auto
);
}
.
ledger
-
row
.
lr
-
profile
,
.
ledger
-
head
span
:
nth
-
child
(
2
)
{
display
:
none
;
}
}
@
media
(
max
-
width
:
940
px
)
{
.
ledger
-
head
,
.
ledger
-
row
{
grid
-
template
-
columns
:
minmax
(
180
px
,
1.4f
r
)
minmax
(
132
px
,
auto
)
minmax
(
132
px
,
auto
);
}
.
ledger
-
row
.
lr
-
meta
,
.
ledger
-
head
span
:
nth
-
child
(
4
)
{
display
:
none
;
}
}
@
media
(
max
-
width
:
720
px
)
{
.
ledger
-
head
,
.
ledger
-
row
{
grid
-
template
-
columns
:
minmax
(
0
,
1
fr
)
auto
;
}
.
ledger
-
row
.
lr
-
tags
,
.
ledger
-
head
span
:
nth
-
child
(
3
)
{
display
:
none
;
}
.
toolbar
.
tool
-
search
{
width
:
100
%
;
max
-
width
:
none
;
}
.
hit
-
meta
{
order
:
5
;
width
:
100
%
;
}
}
@
media
(
prefers
-
reduced
-
motion
:
reduce
)
{
.
ledger
-
row
,
.
pool
-
card
,
.
guide
-
tab
{
transition
:
none
;
}
.
skel
-
row
{
animation
:
none
;
background
:
#
f3f5f9
;
}
}
<
/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