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
ddc5340f
Commit
ddc5340f
authored
Sep 02, 2026
by
李文光
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: 候选人详情抽屉与档案编辑,重构概览及导航侧栏
parent
810e2805
Expand all
Show whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
1680 additions
and
439 deletions
+1680
-439
main.scss
vue-app/src/assets/styles/main.scss
+187
-49
NavItem.vue
vue-app/src/components/common/NavItem.vue
+74
-12
ResumeDetail.vue
vue-app/src/components/resumes/ResumeDetail.vue
+1324
-336
AppShell.vue
vue-app/src/layouts/AppShell.vue
+68
-32
nav.js
vue-app/src/router/nav.js
+11
-5
ResumesView.vue
vue-app/src/views/ResumesView.vue
+16
-5
No files found.
vue-app/src/assets/styles/main.scss
View file @
ddc5340f
...
@@ -47,65 +47,139 @@ textarea {
...
@@ -47,65 +47,139 @@ textarea {
position
:
sticky
;
position
:
sticky
;
top
:
0
;
top
:
0
;
height
:
100vh
;
height
:
100vh
;
padding
:
1
8px
14
px
;
padding
:
1
6px
12
px
;
display
:
grid
;
display
:
grid
;
grid-template-rows
:
auto
1fr
auto
;
grid-template-rows
:
auto
1fr
auto
;
gap
:
1
8
px
;
gap
:
1
4
px
;
background
:
#111722
;
background
:
linear-gradient
(
180deg
,
#1a2340
0%
,
#0f1526
100%
)
;
color
:
#fff
;
color
:
#fff
;
.brand
{
.brand
{
display
:
flex
;
display
:
flex
;
align-items
:
center
;
align-items
:
center
;
gap
:
12px
;
gap
:
12px
;
padding
:
10px
;
padding
:
6px
8px
12px
;
border-bottom
:
1px
solid
rgba
(
255
,
255
,
255
,
0
.08
);
.brand-mark
{
.brand-mark
{
display
:
flex
;
display
:
flex
;
align-items
:
center
;
align-items
:
center
;
justify-content
:
center
;
justify-content
:
center
;
width
:
3
4
px
;
width
:
3
8
px
;
height
:
3
4
px
;
height
:
3
8
px
;
border-radius
:
9
px
;
border-radius
:
11
px
;
background
:
var
(
--
blue
);
background
:
linear-gradient
(
135deg
,
var
(
--
blue
)
,
var
(
--
cyan
)
);
color
:
#fff
;
color
:
#fff
;
font-weight
:
700
;
font-weight
:
700
;
font-size
:
15px
;
font-size
:
15px
;
box-shadow
:
0
6px
16px
rgba
(
69
,
105
,
201
,
0
.4
);
letter-spacing
:
0
.5px
;
}
}
.brand-copy
{
min-width
:
0
;
strong
{
strong
{
display
:
block
;
display
:
block
;
font-size
:
16px
;
font-size
:
16px
;
font-weight
:
600
;
letter-spacing
:
0
.5px
;
}
}
span
{
.brand-sub
{
display
:
block
;
display
:
flex
;
font-size
:
12px
;
align-items
:
center
;
gap
:
6px
;
margin-top
:
3px
;
font-size
:
11px
;
color
:
rgba
(
255
,
255
,
255
,
0
.5
);
color
:
rgba
(
255
,
255
,
255
,
0
.5
);
}
}
}
}
}
.nav
{
.nav
{
min-height
:
0
;
overflow-y
:
auto
;
display
:
flex
;
display
:
flex
;
flex-direction
:
column
;
flex-direction
:
column
;
gap
:
4px
;
gap
:
14px
;
padding-right
:
2px
;
.nav-group
{
display
:
flex
;
flex-direction
:
column
;
gap
:
3px
;
}
.nav-group-title
{
font-size
:
10px
;
letter-spacing
:
2px
;
color
:
rgba
(
255
,
255
,
255
,
0
.4
);
padding
:
0
8px
;
margin-bottom
:
4px
;
}
}
}
.side-actions
{
.side-foot
{
border-top
:
1px
solid
rgba
(
255
,
255
,
255
,
0
.08
);
padding-top
:
12px
;
.side-foot-title
{
font-size
:
10px
;
letter-spacing
:
2px
;
color
:
rgba
(
255
,
255
,
255
,
0
.4
);
padding
:
0
8px
;
margin-bottom
:
8px
;
}
.data-actions
{
display
:
flex
;
display
:
flex
;
flex-direction
:
column
;
flex-direction
:
column
;
gap
:
8px
;
gap
:
6px
;
align-items
:
stretch
;
.el-button
{
.data-btn
{
margin-left
:
0
;
display
:
flex
;
align-items
:
center
;
gap
:
10px
;
width
:
100%
;
width
:
100%
;
padding
:
8px
12px
;
border
:
1px
solid
rgba
(
255
,
255
,
255
,
0
.08
);
border-radius
:
9px
;
background
:
rgba
(
255
,
255
,
255
,
0
.04
);
color
:
rgba
(
255
,
255
,
255
,
0
.72
);
font-size
:
12px
;
cursor
:
pointer
;
transition
:
background
0
.18s
ease
,
border-color
0
.18s
ease
,
color
0
.18s
ease
;
.el-icon
{
font-size
:
14px
;
color
:
rgba
(
255
,
255
,
255
,
0
.6
);
}
&
:hover
{
background
:
rgba
(
255
,
255
,
255
,
0
.08
);
border-color
:
rgba
(
117
,
195
,
221
,
0
.4
);
color
:
#fff
;
.el-icon
{
color
:
var
(
--
cyan
);
}
}
&
:focus-visible
{
outline
:
2px
solid
var
(
--
cyan
);
outline-offset
:
1px
;
}
}
}
}
}
.import-input
{
.import-input
{
display
:
none
;
display
:
none
;
}
}
}
}
}
.main
{
.main
{
...
@@ -116,61 +190,114 @@ textarea {
...
@@ -116,61 +190,114 @@ textarea {
.top
{
.top
{
display
:
flex
;
display
:
flex
;
align-items
:
flex-start
;
align-items
:
center
;
justify-content
:
space-between
;
justify-content
:
space-between
;
gap
:
16px
;
gap
:
16px
;
padding
:
20px
28px
;
padding
:
14px
28px
;
background
:
var
(
--
panel
);
background
:
rgba
(
255
,
255
,
255
,
0
.86
);
backdrop-filter
:
blur
(
14px
)
saturate
(
1
.2
);
-webkit-backdrop-filter
:
blur
(
14px
)
saturate
(
1
.2
);
border-bottom
:
1px
solid
var
(
--
line
);
border-bottom
:
1px
solid
var
(
--
line
);
position
:
sticky
;
position
:
sticky
;
top
:
0
;
top
:
0
;
z-index
:
10
;
z-index
:
10
;
h1
{
h1
{
margin
:
0
0
4
px
;
margin
:
0
0
3
px
;
font-size
:
2
2
px
;
font-size
:
2
0
px
;
}
}
p
{
p
{
margin
:
0
;
margin
:
0
;
color
:
var
(
--
muted
);
color
:
var
(
--
muted
);
font-size
:
1
4
px
;
font-size
:
1
3
px
;
}
}
.top-meta
{
.top-meta
{
display
:
flex
;
display
:
flex
;
align-items
:
center
;
align-items
:
center
;
gap
:
1
2
px
;
gap
:
1
4
px
;
color
:
var
(
--
muted
);
color
:
var
(
--
muted
);
font-size
:
1
3
px
;
font-size
:
1
2
px
;
strong
{
.save-status
{
color
:
var
(
--
ink
);
display
:
inline-flex
;
align-items
:
center
;
gap
:
7px
;
}
.today
{
font-variant-numeric
:
tabular-nums
;
}
}
.workspace-switch
{
.workspace-switch
{
width
:
13
0
px
;
width
:
13
2
px
;
}
}
.user-
entry
{
.user-
chip
{
display
:
inline-flex
;
display
:
inline-flex
;
align-items
:
center
;
align-items
:
center
;
gap
:
8px
;
gap
:
10px
;
cursor
:
pointer
;
padding
:
5px
10px
5px
6px
;
padding
:
4px
8px
;
border
:
1px
solid
var
(
--
line
);
border-radius
:
8px
;
border-radius
:
999px
;
background
:
#fff
;
color
:
var
(
--
ink
);
color
:
var
(
--
ink
);
cursor
:
pointer
;
transition
:
border-color
0
.18s
ease
,
box-shadow
0
.18s
ease
;
&
:hover
{
&
:hover
{
background
:
var
(
--
line
);
border-color
:
var
(
--
blue
);
box-shadow
:
0
4px
14px
rgba
(
88
,
116
,
200
,
0
.18
);
}
&
:focus-visible
{
outline
:
2px
solid
var
(
--
cyan
);
outline-offset
:
1px
;
}
}
.user-name
{
.user-avatar
{
max-width
:
140px
;
display
:
inline-flex
;
align-items
:
center
;
justify-content
:
center
;
width
:
28px
;
height
:
28px
;
border-radius
:
50%
;
background
:
linear-gradient
(
135deg
,
var
(
--
blue
)
,
var
(
--
cyan
));
color
:
#fff
;
font-size
:
13px
;
font-weight
:
600
;
box-shadow
:
inset
0
0
0
2px
rgba
(
255
,
255
,
255
,
0
.55
);
}
.user-chip-copy
{
display
:
flex
;
flex-direction
:
column
;
align-items
:
flex-start
;
line-height
:
1
.2
;
strong
{
font-size
:
13px
;
color
:
var
(
--
ink
);
max-width
:
130px
;
overflow
:
hidden
;
overflow
:
hidden
;
text-overflow
:
ellipsis
;
text-overflow
:
ellipsis
;
white-space
:
nowrap
;
white-space
:
nowrap
;
}
}
em
{
font-style
:
normal
;
font-size
:
10px
;
color
:
var
(
--
muted
);
}
}
.user-chevron
{
font-size
:
12px
;
color
:
var
(
--
muted
);
}
}
}
}
}
}
}
...
@@ -181,6 +308,17 @@ textarea {
...
@@ -181,6 +308,17 @@ textarea {
min-height
:
0
;
min-height
:
0
;
}
}
// 通用状态圆点(侧栏品牌 / 顶栏保存状态)
.status-dot
{
display
:
inline-block
;
width
:
7px
;
height
:
7px
;
border-radius
:
50%
;
background
:
var
(
--
green
);
box-shadow
:
0
0
0
3px
rgba
(
99
,
200
,
155
,
0
.18
);
flex-shrink
:
0
;
}
// 通用卡片式容器
// 通用卡片式容器
.panel
{
.panel
{
padding
:
22px
;
padding
:
22px
;
...
...
vue-app/src/components/common/NavItem.vue
View file @
ddc5340f
...
@@ -4,40 +4,102 @@ defineProps({
...
@@ -4,40 +4,102 @@ defineProps({
active
:
{
type
:
Boolean
,
default
:
false
},
active
:
{
type
:
Boolean
,
default
:
false
},
count
:
{
type
:
Number
,
default
:
0
},
count
:
{
type
:
Number
,
default
:
0
},
})
})
defineEmits
([
'click'
])
</
script
>
</
script
>
<
template
>
<
template
>
<button
type=
"button"
class=
"nav-item"
:class=
"
{ active }" @click="$emit('click')">
<button
type=
"button"
class=
"nav-item"
:class=
"
{ active }" @click="$emit('click')">
<span>
{{
item
.
label
}}
</span>
<span
class=
"nav-icon"
>
<span
class=
"count"
>
{{
count
}}
</span>
<el-icon><component
:is=
"item.icon"
/></el-icon>
</span>
<span
class=
"nav-label"
>
{{
item
.
label
}}
</span>
<span
v-if=
"count"
class=
"nav-count"
>
{{
count
}}
</span>
</button>
</button>
</
template
>
</
template
>
<
style
lang=
"scss"
scoped
>
<
style
lang=
"scss"
scoped
>
.nav-item
{
.nav-item
{
position
:
relative
;
display
:
flex
;
display
:
flex
;
align-items
:
center
;
align-items
:
center
;
justify-content
:
space-between
;
gap
:
10px
;
gap
:
8px
;
width
:
100%
;
width
:
100%
;
padding
:
10px
12px
;
padding
:
9px
11px
;
border-radius
:
8px
;
border
:
0
;
border-radius
:
10px
;
background
:
transparent
;
background
:
transparent
;
color
:
rgba
(
255
,
255
,
255
,
0
.
72
);
color
:
rgba
(
255
,
255
,
255
,
0
.
66
);
text-align
:
left
;
text-align
:
left
;
cursor
:
pointer
;
transition
:
background
0
.18s
ease
,
color
0
.18s
ease
,
transform
0
.12s
ease
;
.nav-icon
{
display
:
inline-flex
;
align-items
:
center
;
justify-content
:
center
;
width
:
22px
;
height
:
22px
;
font-size
:
17px
;
flex-shrink
:
0
;
color
:
rgba
(
255
,
255
,
255
,
0
.78
);
transition
:
color
0
.18s
ease
;
}
.nav-label
{
flex
:
1
;
min-width
:
0
;
font-size
:
13px
;
white-space
:
nowrap
;
overflow
:
hidden
;
text-overflow
:
ellipsis
;
}
.nav-count
{
min-width
:
22px
;
height
:
20px
;
padding
:
0
6px
;
border-radius
:
999px
;
background
:
rgba
(
255
,
255
,
255
,
0
.14
);
color
:
rgba
(
255
,
255
,
255
,
0
.85
);
font-size
:
11px
;
font-variant-numeric
:
tabular-nums
;
display
:
inline-flex
;
align-items
:
center
;
justify-content
:
center
;
flex-shrink
:
0
;
}
&
:hover
{
&
:hover
{
background
:
rgba
(
255
,
255
,
255
,
0
.06
);
background
:
rgba
(
255
,
255
,
255
,
0
.06
);
color
:
#fff
;
.nav-icon
{
color
:
#fff
;
}
}
&
:focus-visible
{
outline
:
2px
solid
var
(
--
cyan
);
outline-offset
:
1px
;
}
}
&
.active
{
&
.active
{
background
:
var
(
--
blue
);
background
:
linear-gradient
(
120deg
,
rgba
(
88
,
116
,
200
,
0
.95
)
,
rgba
(
117
,
195
,
221
,
0
.82
));
color
:
#fff
;
box-shadow
:
0
6px
18px
rgba
(
69
,
105
,
201
,
0
.35
);
.nav-icon
{
color
:
#fff
;
color
:
#fff
;
}
}
.count
{
.nav-count
{
font-size
:
12px
;
background
:
rgba
(
255
,
255
,
255
,
0
.24
);
opacity
:
0
.8
;
color
:
#fff
;
}
}
}
}
}
</
style
>
</
style
>
vue-app/src/components/resumes/ResumeDetail.vue
View file @
ddc5340f
This diff is collapsed.
Click to expand it.
vue-app/src/layouts/AppShell.vue
View file @
ddc5340f
...
@@ -27,6 +27,8 @@ watch(
...
@@ -27,6 +27,8 @@ watch(
activeRouteKey
,
activeRouteKey
,
(
key
)
=>
{
(
key
)
=>
{
store
.
activePage
=
key
store
.
activePage
=
key
// 离开简历库时关闭候选人抽屉,避免返回时残留弹层
if
(
key
!==
'resumes'
)
store
.
resumeViewMode
=
'list'
},
},
{
immediate
:
true
}
{
immediate
:
true
}
)
)
...
@@ -39,6 +41,19 @@ const todayText = computed(() =>
...
@@ -39,6 +41,19 @@ const todayText = computed(() =>
const
displayName
=
computed
(()
=>
userStore
.
user
?.
name
||
userStore
.
user
?.
username
||
'未登录'
)
const
displayName
=
computed
(()
=>
userStore
.
user
?.
name
||
userStore
.
user
?.
username
||
'未登录'
)
const
isAdmin
=
computed
(()
=>
userStore
.
isAdmin
)
const
isAdmin
=
computed
(()
=>
userStore
.
isAdmin
)
// 侧栏导航按工作流分组
const
navGroups
=
computed
(()
=>
{
const
byKey
=
Object
.
fromEntries
(
navItems
.
map
((
item
)
=>
[
item
.
key
,
item
]))
const
groups
=
[
{
title
:
'工作台'
,
items
:
[
byKey
.
todo
,
byKey
.
dashboard
].
filter
(
Boolean
)
},
{
title
:
'招聘流程'
,
items
:
[
byKey
.
jobs
,
byKey
.
resumes
,
byKey
.
offer
].
filter
(
Boolean
)
},
]
if
(
isAdmin
.
value
)
{
groups
.
push
({
title
:
'系统'
,
items
:
[{
key
:
'user-management'
,
label
:
'用户管理'
,
icon
:
'Setting'
}]
})
}
return
groups
})
const
navigate
=
(
key
)
=>
{
const
navigate
=
(
key
)
=>
{
store
.
setPage
(
key
)
store
.
setPage
(
key
)
if
(
key
===
'user-management'
)
router
.
push
({
name
:
'user-management'
})
if
(
key
===
'user-management'
)
router
.
push
({
name
:
'user-management'
})
...
@@ -81,6 +96,12 @@ const handleReset = () => {
...
@@ -81,6 +96,12 @@ const handleReset = () => {
if
(
ok
)
store
.
resetData
()
if
(
ok
)
store
.
resetData
()
}
}
// ---- 侧栏数据工具 ----
const
importInput
=
ref
(
null
)
const
triggerImport
=
()
=>
{
importInput
.
value
?.
click
()
}
// ---- 用户菜单 ----
// ---- 用户菜单 ----
const
changePwdOpen
=
ref
(
false
)
const
changePwdOpen
=
ref
(
false
)
const
pwdForm
=
ref
({
oldPassword
:
''
,
newPassword
:
''
,
confirm
:
''
})
const
pwdForm
=
ref
({
oldPassword
:
''
,
newPassword
:
''
,
confirm
:
''
})
...
@@ -172,33 +193,43 @@ onMounted(() => {
...
@@ -172,33 +193,43 @@ onMounted(() => {
<aside
class=
"side"
>
<aside
class=
"side"
>
<div
class=
"brand"
>
<div
class=
"brand"
>
<div
class=
"brand-mark"
>
HR
</div>
<div
class=
"brand-mark"
>
HR
</div>
<div><strong>
招聘系统
</strong><span>
本地可用版
</span></div>
<div
class=
"brand-copy"
>
<strong>
招聘系统
</strong>
<span
class=
"brand-sub"
><i
class=
"status-dot"
></i>
本地可用版
</span>
</div>
</div>
</div>
<nav
class=
"nav"
>
<nav
class=
"nav"
>
<div
v-for=
"group in navGroups"
:key=
"group.title"
class=
"nav-group"
>
<div
class=
"nav-group-title"
>
{{
group
.
title
}}
</div>
<NavItem
<NavItem
v-for=
"item in navI
tems"
v-for=
"item in group.i
tems"
:key=
"item.key"
:key=
"item.key"
:item=
"item"
:item=
"item"
:active=
"store.activePage === item.key"
:active=
"store.activePage === item.key"
:count=
"navCount(item.key)"
:count=
"navCount(item.key)"
@
click=
"navigate(item.key)"
@
click=
"navigate(item.key)"
/>
/>
<NavItem
</div>
v-if=
"isAdmin"
key=
"user-management"
:item=
"
{ key: 'user-management', label: '用户管理' }"
:active="store.activePage === 'user-management'"
:count="0"
@click="navigate('user-management')"
/>
</nav>
</nav>
<div
class=
"side-actions"
>
<el-button
type=
"primary"
size=
"small"
@
click=
"handleExport"
>
导出备份
</el-button>
<div
class=
"side-foot"
>
<el-button
size=
"small"
>
<div
class=
"side-foot-title"
>
数据
</div>
导入备份
<div
class=
"data-actions"
>
<input
class=
"import-input"
type=
"file"
accept=
"application/json"
@
change=
"handleImport"
/>
<button
type=
"button"
class=
"data-btn"
@
click=
"handleExport"
>
</el-button>
<el-icon><Download
/></el-icon>
<el-button
size=
"small"
@
click=
"handleReset"
>
重置示例
</el-button>
<span>
导出备份
</span>
</button>
<button
type=
"button"
class=
"data-btn"
@
click=
"triggerImport"
>
<el-icon><Upload
/></el-icon>
<span>
导入备份
</span>
</button>
<input
ref=
"importInput"
class=
"import-input"
type=
"file"
accept=
"application/json"
@
change=
"handleImport"
/>
<button
type=
"button"
class=
"data-btn"
@
click=
"handleReset"
>
<el-icon><Refresh
/></el-icon>
<span>
重置示例
</span>
</button>
</div>
</div>
</div>
</aside>
</aside>
...
@@ -209,8 +240,11 @@ onMounted(() => {
...
@@ -209,8 +240,11 @@ onMounted(() => {
<p>
{{
pageMeta
.
desc
}}
</p>
<p>
{{
pageMeta
.
desc
}}
</p>
</div>
</div>
<div
class=
"top-meta"
>
<div
class=
"top-meta"
>
<span
class=
"save-status"
>
<i
class=
"status-dot"
></i>
<span>
{{
store
.
saveStateText
}}
</span>
<span>
{{
store
.
saveStateText
}}
</span>
<strong>
{{
todayText
}}
</strong>
</span>
<span
class=
"today"
>
{{
todayText
}}
</span>
<el-select
v-if=
"isAdmin"
v-model=
"workspaceValue"
class=
"workspace-switch"
size=
"small"
placeholder=
"工作区"
>
<el-select
v-if=
"isAdmin"
v-model=
"workspaceValue"
class=
"workspace-switch"
size=
"small"
placeholder=
"工作区"
>
<el-option
<el-option
v-for=
"option in workspaceOptions"
v-for=
"option in workspaceOptions"
...
@@ -220,12 +254,14 @@ onMounted(() => {
...
@@ -220,12 +254,14 @@ onMounted(() => {
/>
/>
</el-select>
</el-select>
<el-dropdown
trigger=
"click"
@
command=
"handleUserCommand"
>
<el-dropdown
trigger=
"click"
@
command=
"handleUserCommand"
>
<
span
class=
"user-entry
"
>
<
button
type=
"button"
class=
"user-chip
"
>
<
el-avatar
:size=
"26"
>
{{
displayName
.
slice
(
0
,
1
).
toUpperCase
()
}}
</el-avatar
>
<
span
class=
"user-avatar"
>
{{
displayName
.
slice
(
0
,
1
).
toUpperCase
()
}}
</span
>
<span
class=
"user-
name"
>
{{
displayName
}}
</span
>
<span
class=
"user-
chip-copy"
>
<el-tag
v-if=
"isAdmin"
size=
"small"
type=
"danger"
>
管理员
</el-ta
g>
<strong>
{{
displayName
}}
</stron
g>
<el-icon><arrow-down
/></el-icon
>
<em>
{{
isAdmin
?
'管理员'
:
'招聘专员'
}}
</em
>
</span>
</span>
<el-icon
class=
"user-chevron"
><ArrowDown
/></el-icon>
</button>
<template
#
dropdown
>
<template
#
dropdown
>
<el-dropdown-menu>
<el-dropdown-menu>
<el-dropdown-item
command=
"change-pwd"
>
修改密码
</el-dropdown-item>
<el-dropdown-item
command=
"change-pwd"
>
修改密码
</el-dropdown-item>
...
...
vue-app/src/router/nav.js
View file @
ddc5340f
export
const
navItems
=
[
export
const
navItems
=
[
{
key
:
'todo'
,
label
:
'待办项'
,
desc
:
'处理需要 HR 跟进的事项。'
,
path
:
'/'
},
{
key
:
'todo'
,
label
:
'待办项'
,
desc
:
'处理需要 HR 跟进的事项。'
,
path
:
'/'
,
icon
:
'Bell'
},
{
key
:
'dashboard'
,
label
:
'招聘数据看板'
,
desc
:
'用本地数据查看招聘漏斗、来源和趋势。'
,
path
:
'/dashboard'
},
{
{
key
:
'jobs'
,
label
:
'岗位管理'
,
desc
:
'维护岗位、JD、招聘目标和岗位状态。'
,
path
:
'/jobs'
},
key
:
'dashboard'
,
{
key
:
'resumes'
,
label
:
'简历库'
,
desc
:
'管理候选人、简历来源、匹配度和流程状态。'
,
path
:
'/resumes'
},
label
:
'招聘数据看板'
,
{
key
:
'offer'
,
label
:
'录用入职管理'
,
desc
:
'生成 Offer 审批草稿并跟进待入职。'
,
path
:
'/offer'
},
desc
:
'用本地数据查看招聘漏斗、来源和趋势。'
,
path
:
'/dashboard'
,
icon
:
'DataAnalysis'
,
},
{
key
:
'jobs'
,
label
:
'岗位管理'
,
desc
:
'维护岗位、JD、招聘目标和岗位状态。'
,
path
:
'/jobs'
,
icon
:
'Briefcase'
},
{
key
:
'resumes'
,
label
:
'简历库'
,
desc
:
'管理候选人、简历来源、匹配度和流程状态。'
,
path
:
'/resumes'
,
icon
:
'User'
},
{
key
:
'offer'
,
label
:
'录用入职管理'
,
desc
:
'生成 Offer 审批草稿并跟进待入职。'
,
path
:
'/offer'
,
icon
:
'Stamp'
},
]
]
export
const
navPageMeta
=
Object
.
fromEntries
(
export
const
navPageMeta
=
Object
.
fromEntries
(
...
...
vue-app/src/views/ResumesView.vue
View file @
ddc5340f
<
script
setup
>
<
script
setup
>
import
{
computed
}
from
'vue'
import
{
computed
,
watch
}
from
'vue'
import
{
useRecruitmentStore
}
from
'@/stores/recruitment'
import
{
useRecruitmentStore
}
from
'@/stores/recruitment'
import
ResumeList
from
'@/components/resumes/ResumeList.vue'
import
ResumeList
from
'@/components/resumes/ResumeList.vue'
...
@@ -9,16 +9,27 @@ import CandidateBatchModal from '@/components/resumes/CandidateBatchModal.vue'
...
@@ -9,16 +9,27 @@ import CandidateBatchModal from '@/components/resumes/CandidateBatchModal.vue'
const
store
=
useRecruitmentStore
()
const
store
=
useRecruitmentStore
()
const
mode
=
computed
(()
=>
store
.
resumeViewMode
)
const
createOpen
=
computed
(()
=>
store
.
candidateCreateOpen
)
const
createOpen
=
computed
(()
=>
store
.
candidateCreateOpen
)
const
batchOpen
=
computed
(()
=>
store
.
candidateBatchOpen
)
const
batchOpen
=
computed
(()
=>
store
.
candidateBatchOpen
)
// 待办「定位候选人」:自动打开候选人抽屉
watch
(
()
=>
store
.
focusedTarget
?.
type
,
(
type
)
=>
{
if
(
type
===
'candidate'
)
{
store
.
resumeViewMode
=
'detail'
store
.
resumeDetailSection
=
'overview'
store
.
focusedTarget
=
null
}
},
{
immediate
:
true
}
)
</
script
>
</
script
>
<
template
>
<
template
>
<div
class=
"resume-workbench"
>
<div
class=
"resume-workbench"
>
<ResumeList
v-show=
"mode !== 'detail'"
/>
<ResumeList
/>
<ResumeDetail
v-if=
"mode === 'detail'"
/>
<ResumeDetail
/>
<CandidateCreateModal
v-if=
"createOpen"
/>
<CandidateCreateModal
v-if=
"createOpen"
/>
<CandidateBatchModal
v-if=
"batchOpen"
/>
<CandidateBatchModal
v-if=
"batchOpen"
/>
...
...
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