个人资料,简历
This commit is contained in:
Vendored
+1
@@ -69,6 +69,7 @@ declare module 'vue' {
|
||||
SettingsDialog: typeof import('./src/components/SettingsDialog.vue')['default']
|
||||
SettingsInviteDialog: typeof import('./src/components/SettingsInviteDialog.vue')['default']
|
||||
SideNav: typeof import('./src/components/SideNav.vue')['default']
|
||||
StepProgressOverlay: typeof import('./src/components/StepProgressOverlay.vue')['default']
|
||||
}
|
||||
export interface GlobalDirectives {
|
||||
vLoading: typeof import('element-plus/es')['ElLoadingDirective']
|
||||
|
||||
@@ -117,3 +117,27 @@ export function fetchOrderDetail(orderId: string) {
|
||||
params: { orderId },
|
||||
})
|
||||
}
|
||||
|
||||
// ==================== 邀请统计相关 ====================
|
||||
|
||||
/** 用户邀请统计DTO */
|
||||
export interface InviteStatsDto {
|
||||
/** 累计邀请人数 */
|
||||
inviteCount?: number
|
||||
/** 当前周期已邀请人数 */
|
||||
milestoneProgress?: number
|
||||
/** 累计获得会员天数 */
|
||||
rewardDays?: number
|
||||
/** 里程碑目标人数 */
|
||||
milestoneTarget?: number
|
||||
/** 里程碑奖励天数 */
|
||||
milestoneDays?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户邀请统计
|
||||
* GET /user/manage/inviteStats
|
||||
*/
|
||||
export function fetchInviteStats() {
|
||||
return request.get<any, ApiResult<InviteStatsDto>>('/user/manage/inviteStats')
|
||||
}
|
||||
|
||||
+13
-2
@@ -32,9 +32,12 @@ export interface ResumeListItem {
|
||||
/**
|
||||
* 获取简历列表
|
||||
* GET /resume/list
|
||||
* @param pageSize 每页条数,默认 50
|
||||
*/
|
||||
export function fetchResumeList() {
|
||||
return request.get<any, ApiResult<ResumeListItem[]>>('/resume/list')
|
||||
export function fetchResumeList(pageSize = 50) {
|
||||
return request.get<any, ApiResult<ResumeListItem[]>>('/resume/list', {
|
||||
params: { pageSize },
|
||||
})
|
||||
}
|
||||
|
||||
// ==================== 描述段落(通用) ====================
|
||||
@@ -735,3 +738,11 @@ export interface UpdateResumeInfoParams {
|
||||
export function updateResumeInfo(data: UpdateResumeInfoParams) {
|
||||
return request.post<any, ApiResult>('/resume/updateInfo', data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据当前用户个人资料创建一份新简历
|
||||
* POST /user/profile/createResume
|
||||
*/
|
||||
export function createResumeFromProfile() {
|
||||
return request.post<any, ApiResult>('/user/profile/createResume')
|
||||
}
|
||||
|
||||
@@ -584,6 +584,27 @@ body:not(#_) {
|
||||
.color-accent{
|
||||
color:$accent;
|
||||
}
|
||||
.color-dark{
|
||||
color:$text-dark;
|
||||
}
|
||||
.color-middle{
|
||||
color:$text-middle;
|
||||
}
|
||||
.color-light{
|
||||
color:$text-light;
|
||||
}
|
||||
.color-accent-ho:hover{
|
||||
color:$accent;
|
||||
}
|
||||
.color-dark-ho:hover{
|
||||
color:$text-dark;
|
||||
}
|
||||
.color-middle-ho:hover{
|
||||
color:$text-middle;
|
||||
}
|
||||
.color-light-ho:hover{
|
||||
color:$text-light;
|
||||
}
|
||||
|
||||
|
||||
/*border*/
|
||||
@@ -623,6 +644,9 @@ body:not(#_) {
|
||||
.border-ra19{border-radius: 0.19rem;}
|
||||
.border-ra20{border-radius: 0.2rem;}
|
||||
|
||||
.border-ra50p{
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.bor-ra50p{
|
||||
border-radius: 50%;
|
||||
@@ -791,6 +815,9 @@ body:not(#_) {
|
||||
.mb0 {margin-bottom: 0rem;}
|
||||
.mb1 {margin-bottom: 0.01rem;} .mb2 {margin-bottom: 0.02rem;} .mb3 {margin-bottom: 0.03rem;} .mb4 {margin-bottom: 0.04rem;} .mb5 {margin-bottom: 0.05rem;} .mb6 {margin-bottom: 0.06rem;} .mb7 {margin-bottom: 0.07rem;} .mb8 {margin-bottom: 0.08rem;} .mb9 {margin-bottom: 0.09rem;} .mb10 {margin-bottom: 0.1rem;} .mb11 {margin-bottom: 0.11rem;} .mb12 {margin-bottom: 0.12rem;} .mb13 {margin-bottom: 0.13rem;} .mb14 {margin-bottom: 0.14rem;} .mb15 {margin-bottom: 0.15rem;} .mb16 {margin-bottom: 0.16rem;} .mb17 {margin-bottom: 0.17rem;} .mb18 {margin-bottom: 0.18rem;} .mb19 {margin-bottom: 0.19rem;} .mb20 {margin-bottom: 0.2rem;} .mb21 {margin-bottom: 0.21rem;} .mb22 {margin-bottom: 0.22rem;} .mb23 {margin-bottom: 0.23rem;} .mb24 {margin-bottom: 0.24rem;} .mb25 {margin-bottom: 0.25rem;} .mb26 {margin-bottom: 0.26rem;} .mb27 {margin-bottom: 0.27rem;} .mb28 {margin-bottom: 0.28rem;} .mb29 {margin-bottom: 0.29rem;} .mb30 {margin-bottom: 0.3rem;} .mb31 {margin-bottom: 0.31rem;} .mb32 {margin-bottom: 0.32rem;} .mb33 {margin-bottom: 0.33rem;} .mb34 {margin-bottom: 0.34rem;} .mb35 {margin-bottom: 0.35rem;} .mb36 {margin-bottom: 0.36rem;} .mb37 {margin-bottom: 0.37rem;} .mb38 {margin-bottom: 0.38rem;} .mb39 {margin-bottom: 0.39rem;} .mb40 {margin-bottom: 0.4rem;} .mb41 {margin-bottom: 0.41rem;} .mb42 {margin-bottom: 0.42rem;} .mb43 {margin-bottom: 0.43rem;} .mb44 {margin-bottom: 0.44rem;} .mb45 {margin-bottom: 0.45rem;} .mb46 {margin-bottom: 0.46rem;} .mb47 {margin-bottom: 0.47rem;} .mb48 {margin-bottom: 0.48rem;} .mb49 {margin-bottom: 0.49rem;} .mb50 {margin-bottom: 0.5rem;} .mb51 {margin-bottom: 0.51rem;} .mb52 {margin-bottom: 0.52rem;} .mb53 {margin-bottom: 0.53rem;} .mb54 {margin-bottom: 0.54rem;} .mb55 {margin-bottom: 0.55rem;} .mb56 {margin-bottom: 0.56rem;} .mb57 {margin-bottom: 0.57rem;} .mb58 {margin-bottom: 0.58rem;} .mb59 {margin-bottom: 0.59rem;} .mb60 {margin-bottom: 0.6rem;} .mb61 {margin-bottom: 0.61rem;} .mb62 {margin-bottom: 0.62rem;} .mb63 {margin-bottom: 0.63rem;} .mb64 {margin-bottom: 0.64rem;} .mb65 {margin-bottom: 0.65rem;} .mb66 {margin-bottom: 0.66rem;} .mb67 {margin-bottom: 0.67rem;} .mb68 {margin-bottom: 0.68rem;} .mb69 {margin-bottom: 0.69rem;} .mb70 {margin-bottom: 0.7rem;} .mb71 {margin-bottom: 0.71rem;} .mb72 {margin-bottom: 0.72rem;} .mb73 {margin-bottom: 0.73rem;} .mb74 {margin-bottom: 0.74rem;} .mb75 {margin-bottom: 0.75rem;} .mb76 {margin-bottom: 0.76rem;} .mb77 {margin-bottom: 0.77rem;} .mb78 {margin-bottom: 0.78rem;} .mb79 {margin-bottom: 0.79rem;} .mb80 {margin-bottom: 0.8rem;} .mb81 {margin-bottom: 0.81rem;} .mb82 {margin-bottom: 0.82rem;} .mb83 {margin-bottom: 0.83rem;} .mb84 {margin-bottom: 0.84rem;} .mb85 {margin-bottom: 0.85rem;} .mb86 {margin-bottom: 0.86rem;} .mb87 {margin-bottom: 0.87rem;} .mb88 {margin-bottom: 0.88rem;} .mb89 {margin-bottom: 0.89rem;} .mb90 {margin-bottom: 0.9rem;} .mb91 {margin-bottom: 0.91rem;} .mb92 {margin-bottom: 0.92rem;} .mb93 {margin-bottom: 0.93rem;} .mb94 {margin-bottom: 0.94rem;} .mb95 {margin-bottom: 0.95rem;} .mb96 {margin-bottom: 0.96rem;} .mb97 {margin-bottom: 0.97rem;} .mb98 {margin-bottom: 0.98rem;} .mb99 {margin-bottom: 0.99rem;}
|
||||
.mb100 {margin-bottom: 1rem;}
|
||||
.mb-20{
|
||||
margin-bottom: -0.2rem;
|
||||
}
|
||||
|
||||
/* margin-top-bottom */
|
||||
.mtb0 {margin-top: 0rem;margin-bottom: 0rem;}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
padding: 0.2rem 0.24rem 0;
|
||||
padding-bottom: 0.2rem ;
|
||||
background: $bg-white;
|
||||
border-radius: 0.12rem;
|
||||
font-size: 0.14rem;
|
||||
@@ -18,16 +18,15 @@
|
||||
align-items: center;
|
||||
gap: 0.24rem;
|
||||
padding-bottom: 0.14rem;
|
||||
border-bottom: 1px solid $border-color;
|
||||
flex-shrink: 0;
|
||||
margin-bottom:0.14rem
|
||||
}
|
||||
|
||||
&__tab-item {
|
||||
font-size: 0.18rem;
|
||||
font-size: 0.14rem;
|
||||
color: $text-middle;
|
||||
cursor: pointer;
|
||||
padding-bottom: 0.06rem;
|
||||
padding: 0.16rem 0.2rem;
|
||||
border-bottom: 2px solid transparent;
|
||||
transition: all 0.2s;
|
||||
white-space: nowrap;
|
||||
@@ -37,9 +36,9 @@
|
||||
}
|
||||
|
||||
&--active {
|
||||
color: $text-dark;
|
||||
color: $accent;
|
||||
font-weight: 600;
|
||||
border-bottom-color: $text-dark;
|
||||
border-bottom-color: $accent;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +46,7 @@
|
||||
&__body {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding-bottom: 0.2rem;
|
||||
padding: 0 0.24rem 0.2rem 0.24rem;
|
||||
|
||||
// 隐藏滚动条
|
||||
&::-webkit-scrollbar {
|
||||
@@ -64,7 +63,7 @@
|
||||
border-radius: 0.08rem;
|
||||
|
||||
&:hover .profile-page-content__card-header{
|
||||
background: #F7F7F7;
|
||||
background: #F0FAFA;
|
||||
.profile-page-content__edit-btn-inline{
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
@@ -301,7 +300,7 @@
|
||||
}
|
||||
|
||||
&__card-hover:hover {
|
||||
background: #F7F7F7;
|
||||
background: #F0FAFA;
|
||||
}
|
||||
|
||||
// 标题行hover显示添加按钮(5大经历)
|
||||
@@ -319,7 +318,7 @@
|
||||
border-radius: 0.08rem;
|
||||
|
||||
&:hover {
|
||||
background: #F7F7F7;
|
||||
background: #F0FAFA;
|
||||
}
|
||||
|
||||
&:not(:last-child) {
|
||||
|
||||
@@ -562,11 +562,18 @@
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
background: #F3F4F6;
|
||||
background: #fff;
|
||||
border-radius: 0.08rem;
|
||||
padding: 0.12rem 0.16rem;
|
||||
box-sizing: border-box;
|
||||
margin-top: 0.12rem;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover,
|
||||
&:active,
|
||||
&--active {
|
||||
background: #F3F4F6;
|
||||
}
|
||||
}
|
||||
|
||||
.side-nav__user-left {
|
||||
|
||||
@@ -0,0 +1,241 @@
|
||||
@use '../variables' as *;
|
||||
|
||||
/* 步骤进度加载遮罩组件 */
|
||||
.step-progress-overlay {
|
||||
font-size: 0.14rem;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 2000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* 半透明遮罩 */
|
||||
.step-progress-overlay__mask {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: $overlay-bg;
|
||||
}
|
||||
|
||||
/* 内容面板 */
|
||||
.step-progress-overlay__panel {
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
padding: 0.24rem 0.32rem;
|
||||
gap: 0.24rem;
|
||||
width: 4.80rem;
|
||||
background: linear-gradient(180deg, #EDF9FA 0%, #FFFFFF 20.63%);
|
||||
border: 1px solid #F0F0F0;
|
||||
box-shadow: 0 0 0.16rem rgba(0, 0, 0, 0.05);
|
||||
border-radius: 0.12rem;
|
||||
}
|
||||
|
||||
/* 顶部标题行 */
|
||||
.step-progress-overlay__header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.step-progress-overlay__titles {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.04rem;
|
||||
}
|
||||
|
||||
.step-progress-overlay__title {
|
||||
font-weight: 520;
|
||||
font-size: 0.18rem;
|
||||
line-height: 0.24rem;
|
||||
color: $text-dark;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.step-progress-overlay__subtitle {
|
||||
font-weight: 330;
|
||||
font-size: 0.12rem;
|
||||
line-height: 0.16rem;
|
||||
color: $text-middle;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* 关闭按钮 */
|
||||
.step-progress-overlay__close {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 0.24rem;
|
||||
height: 0.24rem;
|
||||
border: none;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
border-radius: 0.04rem;
|
||||
transition: background 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: #F0F0F0;
|
||||
}
|
||||
}
|
||||
|
||||
/* 进度面板(带边框的内框) */
|
||||
.step-progress-overlay__progress-box {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
padding: 0.16rem;
|
||||
gap: 0.1rem;
|
||||
width: 100%;
|
||||
border: 1px solid #AEE6EA;
|
||||
border-radius: 0.08rem;
|
||||
}
|
||||
|
||||
/* 进度条头部 */
|
||||
.step-progress-overlay__progress-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 0.08rem;
|
||||
}
|
||||
|
||||
.step-progress-overlay__progress-label {
|
||||
font-weight: 450;
|
||||
font-size: 0.14rem;
|
||||
line-height: 0.19rem;
|
||||
color: #616367;
|
||||
}
|
||||
|
||||
.step-progress-overlay__progress-percent {
|
||||
font-weight: 450;
|
||||
font-size: 0.14rem;
|
||||
line-height: 0.19rem;
|
||||
color: $accent;
|
||||
}
|
||||
|
||||
/* 进度轨道 */
|
||||
.step-progress-overlay__progress-rail {
|
||||
width: 100%;
|
||||
height: 0.06rem;
|
||||
background: #EDF9FA;
|
||||
border-radius: 0.03rem;
|
||||
overflow: hidden;
|
||||
margin-bottom: 0.20rem;
|
||||
}
|
||||
|
||||
.step-progress-overlay__progress-fill {
|
||||
height: 100%;
|
||||
background: $accent;
|
||||
border-radius: 0.03rem;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
/* 步骤列表 */
|
||||
.step-progress-overlay__steps {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.step-progress-overlay__step-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.step-progress-overlay__step-left {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-right: 0.08rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* 步骤圆点 — 通用 */
|
||||
.step-progress-overlay__dot {
|
||||
width: 0.16rem;
|
||||
height: 0.16rem;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* 完成状态:品牌色实心 + 对勾 */
|
||||
.step-progress-overlay__dot--done {
|
||||
background: $accent;
|
||||
}
|
||||
|
||||
/* 进行中:浅色光晕 + 呼吸内圆 */
|
||||
.step-progress-overlay__dot--active {
|
||||
background: #EDF9FA;
|
||||
}
|
||||
|
||||
.step-progress-overlay__dot-pulse {
|
||||
width: 0.08rem;
|
||||
height: 0.08rem;
|
||||
background: $accent;
|
||||
border-radius: 50%;
|
||||
animation: step-overlay-breathe 1.2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes step-overlay-breathe {
|
||||
0%, 100% { transform: scale(0.7); opacity: 0.7; }
|
||||
50% { transform: scale(1.2); opacity: 1; }
|
||||
}
|
||||
|
||||
/* 等待状态:浅色圆 */
|
||||
.step-progress-overlay__dot--pending {
|
||||
background: #EDF9FA;
|
||||
}
|
||||
|
||||
/* 连接线 */
|
||||
.step-progress-overlay__connector {
|
||||
width: 0.02rem;
|
||||
height: 0.22rem;
|
||||
background: #EDF9FA;
|
||||
border-radius: 0.01rem;
|
||||
}
|
||||
|
||||
/* 步骤文字 */
|
||||
.step-progress-overlay__step-text {
|
||||
flex: 1;
|
||||
font-weight: 380;
|
||||
font-size: 0.12rem;
|
||||
line-height: 0.16rem;
|
||||
color: #18181A;
|
||||
}
|
||||
|
||||
.step-progress-overlay__step-text--pending {
|
||||
font-weight: 330;
|
||||
color: #616367;
|
||||
}
|
||||
|
||||
/* 步骤状态文字 */
|
||||
.step-progress-overlay__step-state {
|
||||
font-size: 0.12rem;
|
||||
line-height: 0.16rem;
|
||||
flex-shrink: 0;
|
||||
margin-left: 0.10rem;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.step-progress-overlay__step-state--done {
|
||||
font-weight: 450;
|
||||
color: $accent;
|
||||
}
|
||||
|
||||
.step-progress-overlay__step-state--active {
|
||||
font-weight: 450;
|
||||
color: $accent;
|
||||
}
|
||||
|
||||
.step-progress-overlay__step-state--pending {
|
||||
font-weight: 330;
|
||||
color: #939599;
|
||||
}
|
||||
@@ -42,6 +42,7 @@
|
||||
@use './components/resume-upload-dialog.scss';
|
||||
@use './components/agreement-preview-dialog.scss';
|
||||
@use './components/member-access-dialog.scss';
|
||||
@use './components/step-progress-overlay.scss';
|
||||
|
||||
// 全局样式(优先级最高)
|
||||
@use './auto.scss';
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
padding-bottom: 0.24rem;
|
||||
height: var(--app-height, 100vh);
|
||||
box-sizing: border-box;
|
||||
overflow-y: auto;
|
||||
overflow: hidden;
|
||||
background: $bg-main;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -17,8 +17,9 @@
|
||||
|
||||
&__page-title {
|
||||
|
||||
margin: 0 0 0.16rem 0;
|
||||
padding: 0 0.24rem;
|
||||
padding: 0.19rem 0.24rem;
|
||||
margin-bottom: 0.16rem;
|
||||
background: #fff;
|
||||
>h2{
|
||||
font-size: 0.24rem;
|
||||
font-weight: 700;
|
||||
@@ -116,7 +117,7 @@
|
||||
background: $bg-white;
|
||||
border-radius: 0.12rem;
|
||||
padding: 0.18rem 0.24rem;
|
||||
padding-top: 0.4rem;
|
||||
padding-top: 0.3rem;
|
||||
margin-bottom: 0.16rem;
|
||||
|
||||
/* B/C/D 评级渐变背景 */
|
||||
@@ -133,17 +134,12 @@
|
||||
&__score-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.2rem;
|
||||
gap: 0.16rem;
|
||||
}
|
||||
|
||||
/* 星星+底座容器 */
|
||||
&__star-wrapper {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 0.7rem;
|
||||
flex-shrink: 0;
|
||||
|
||||
}
|
||||
|
||||
/* SVG 五角星 */
|
||||
@@ -156,44 +152,40 @@
|
||||
|
||||
/* 底座长方形 */
|
||||
&__star-base {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
|
||||
margin-top: -0.08rem;
|
||||
width: 0.52rem;
|
||||
height: 0.22rem;
|
||||
border: 1.5px solid #858585;
|
||||
border-radius: 0.03rem;
|
||||
background: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.14rem;
|
||||
|
||||
> span {
|
||||
font-size: 0.12rem;
|
||||
font-size: 0.14rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
/* 右侧操作按钮区 */
|
||||
&__score-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.16rem;
|
||||
|
||||
}
|
||||
|
||||
/* 暂无等级提示文字 */
|
||||
&__score-hint {
|
||||
font-size: 0.13rem;
|
||||
color: $text-light;
|
||||
font-size: 0.14rem;
|
||||
color: $text-middle;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
&__score-links{
|
||||
height: 0.14rem;
|
||||
}
|
||||
&__score-link {
|
||||
font-size: 0.13rem;
|
||||
font-size: 0.12rem;
|
||||
color: $text-light;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: color 0.2s;
|
||||
margin-right: 0.16rem;
|
||||
|
||||
&:hover {
|
||||
color: $accent;
|
||||
@@ -217,7 +209,7 @@
|
||||
font-size: 0.30rem;
|
||||
font-weight: 700;
|
||||
color: $text-dark;
|
||||
line-height: 1.2;
|
||||
padding-bottom: 0.1rem;
|
||||
}
|
||||
|
||||
&__score-label {
|
||||
@@ -231,8 +223,8 @@
|
||||
margin-left: 0.16rem;
|
||||
color: $bg-white;
|
||||
border: none;
|
||||
border-radius: 0.18rem;
|
||||
width: 0.93rem;
|
||||
border-radius: 0.08rem;
|
||||
width: 1.03rem;
|
||||
height: 0.36rem;
|
||||
line-height: 0.37rem;
|
||||
font-size: 0.13rem;
|
||||
@@ -240,7 +232,8 @@
|
||||
cursor: pointer;
|
||||
box-sizing: border-box;
|
||||
//transition: background 0.2s;
|
||||
white-space: nowrap;
|
||||
|
||||
|
||||
|
||||
&:hover {
|
||||
background: $btn-dark-hover;
|
||||
@@ -255,6 +248,58 @@
|
||||
border-radius: 0.16rem;
|
||||
background: #fff;
|
||||
margin: 0 0.24rem;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
// ---- 固定顶部区域(评分+Tab,不滚动) ----
|
||||
&__fixed-top {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
// ---- Tab 导航栏 ----
|
||||
&__tabs {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-bottom: 0.14rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__tab-item {
|
||||
font-size: 0.14rem;
|
||||
color: $text-middle;
|
||||
cursor: pointer;
|
||||
padding: 0.12rem 0.2rem;
|
||||
border-bottom: 2px solid transparent;
|
||||
transition: all 0.2s;
|
||||
white-space: nowrap;
|
||||
|
||||
&:hover {
|
||||
color: $text-dark;
|
||||
}
|
||||
|
||||
&--active {
|
||||
color: $accent;
|
||||
font-weight: 600;
|
||||
border-bottom-color: $accent;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 可滚动内容区域 ----
|
||||
&__scroll-body {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding-bottom: 0.3rem;
|
||||
|
||||
// 隐藏滚动条
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
&__card {
|
||||
@@ -263,7 +308,7 @@
|
||||
margin: 0.4rem 0.18rem;
|
||||
|
||||
&:hover .resume-detail__section-header {
|
||||
background: #F7F7F7;
|
||||
background: #F0FAFA;
|
||||
.resume-detail__edit-btn-inline {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
@@ -275,7 +320,7 @@
|
||||
&__card-hover {
|
||||
padding: 0.1rem 0.14rem;
|
||||
&:hover {
|
||||
background: #F7F7F7;
|
||||
background: #F0FAFA;
|
||||
|
||||
}
|
||||
|
||||
@@ -290,7 +335,7 @@
|
||||
}
|
||||
|
||||
&__user-name {
|
||||
font-size: 0.2rem;
|
||||
font-size: 0.28rem;
|
||||
font-weight: 700;
|
||||
color: $text-dark;
|
||||
margin: 0 0 0.04rem 0;
|
||||
@@ -513,7 +558,7 @@
|
||||
padding: 0.1rem 0.14rem;
|
||||
border-radius: 0.09rem;
|
||||
&:hover {
|
||||
background: #F7F7F7;
|
||||
background: #F0FAFA;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
&__content {
|
||||
margin-left: 2rem;
|
||||
flex: 1;
|
||||
padding: 0.12rem 0.36rem;
|
||||
height: var(--app-height, 100vh);
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
@@ -19,7 +18,8 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 0.2rem;
|
||||
background: #fff;
|
||||
padding: 0.19rem 0.24rem;
|
||||
}
|
||||
|
||||
&__title {
|
||||
@@ -36,8 +36,8 @@
|
||||
background: $btn-dark;
|
||||
color: $bg-white;
|
||||
border: none;
|
||||
border-radius: 0.2rem;
|
||||
padding: 0.08rem 0.2rem;
|
||||
border-radius: 0.08rem;
|
||||
padding: 0.08rem 0.14rem;
|
||||
font-size: 0.13rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
@@ -48,20 +48,50 @@
|
||||
}
|
||||
}
|
||||
|
||||
// 从个人资料创建按钮(描边样式)
|
||||
&__create-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.06rem;
|
||||
background: $bg-white;
|
||||
color: $accent;
|
||||
border: 1px solid $accent;
|
||||
border-radius: 0.08rem;
|
||||
padding: 0.08rem 0.14rem;
|
||||
font-size: 0.13rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: rgba($accent, 0.06);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
&__upload-icon {
|
||||
width: 0.14rem;
|
||||
height: 0.14rem;
|
||||
}
|
||||
|
||||
// 表格容器
|
||||
// 表格容器(超出时可滚动)
|
||||
&__table-wrap {
|
||||
background: $bg-white;
|
||||
border-radius: 0.12rem;
|
||||
margin: 0.18rem 0.17rem;
|
||||
overflow-y: auto;
|
||||
flex: 0 1 auto;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
&__table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
&__th {
|
||||
@@ -72,9 +102,14 @@
|
||||
color: $text-dark;
|
||||
border-bottom: 1px solid $border-color;
|
||||
white-space: nowrap;
|
||||
width: 15%;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background: $bg-white;
|
||||
z-index: 1;
|
||||
|
||||
&--action {
|
||||
width: 2rem;
|
||||
width: 25%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,9 +135,15 @@
|
||||
&--action {
|
||||
position: relative;
|
||||
text-align: center;
|
||||
opacity: 0;
|
||||
transition: opacity 0.15s;
|
||||
}
|
||||
}
|
||||
|
||||
&__row:hover &__td--action {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
// 简历名称单元格
|
||||
&__name-cell {
|
||||
display: flex;
|
||||
@@ -130,9 +171,9 @@
|
||||
&__default-tag {
|
||||
font-size: 0.09rem;
|
||||
color: $accent;
|
||||
background: #D9F5F0;
|
||||
border-radius: 0.14rem;
|
||||
padding: 0.01rem 0.06rem;
|
||||
background: #EDF9FA;
|
||||
border-radius: 0.04rem;
|
||||
padding: 0.014rem 0.08rem;
|
||||
padding-bottom: 0.02rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
@@ -164,6 +205,7 @@
|
||||
align-items: center;
|
||||
gap: 0.16rem;
|
||||
justify-content: flex-end;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
// 行内操作按钮
|
||||
@@ -171,15 +213,17 @@
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 0.12rem;
|
||||
color: $text-dark;
|
||||
color: $text-middle;
|
||||
cursor: pointer;
|
||||
padding: 0.04rem 0.08rem;
|
||||
border-radius: 0.04rem;
|
||||
white-space: nowrap;
|
||||
transition: all 0.15s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&:hover {
|
||||
color: $accent;
|
||||
color: $text-dark;
|
||||
}
|
||||
|
||||
// 删除按钮红色
|
||||
|
||||
@@ -345,6 +345,17 @@
|
||||
</div>
|
||||
<!-- 全屏加载遮罩 -->
|
||||
<FullscreenLoading :visible="fullLoading" :text="fullLoadingText" title="Nova 正在帮您努力处理~" />
|
||||
<!-- 简历定制步骤进度遮罩 -->
|
||||
<StepProgressOverlay
|
||||
v-if="showCustomStepProgress"
|
||||
:title="customStepTitle"
|
||||
:subtitle="customStepSubtitle"
|
||||
:steps="customStepNames"
|
||||
:durations="customStepDurations"
|
||||
:done="customStepDone"
|
||||
@finished="handleCustomStepFinished"
|
||||
@close="handleCustomStepClose"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -359,6 +370,7 @@ import { fetchCustomizeResume, generateCustomizeResume, aiEditResume, rollbackCu
|
||||
import type { CustomizeResumeData, AiEditChatMessage } from '@/api/jobs'
|
||||
import AiThinkingIndicator from '@/components/tools/AiThinkingIndicator.vue'
|
||||
import FullscreenLoading from '@/components/FullscreenLoading.vue'
|
||||
import StepProgressOverlay from '@/components/StepProgressOverlay.vue'
|
||||
|
||||
// ==================== 类型定义 ====================
|
||||
|
||||
@@ -428,6 +440,38 @@ const fullLoading = ref(false)
|
||||
/** 全屏加载文案 */
|
||||
const fullLoadingText = ref('')
|
||||
|
||||
// ==================== 简历定制步骤进度遮罩 ====================
|
||||
|
||||
/** 步骤进度遮罩是否显示 */
|
||||
const showCustomStepProgress = ref(false)
|
||||
/** 接口是否已完成 */
|
||||
const customStepDone = ref(false)
|
||||
/** 大标题 */
|
||||
const customStepTitle = '简历定制中,请耐心等待'
|
||||
/** 副标题 */
|
||||
const customStepSubtitle = '根据STAR法则,快速定制补齐你的简历缺失部分'
|
||||
/** 4个步骤名称 */
|
||||
const customStepNames = ['个人概述优化...', '岗位技能优化...', '工作经验优化...', '最终简历生成中...']
|
||||
/** 每步时长 */
|
||||
const customStepDurations = [2000, 2000, 2000, 2000]
|
||||
/** 缓存的定制简历数据(等进度走完后填充) */
|
||||
const pendingCustomResumeData = ref<any>(null)
|
||||
|
||||
/** 步骤进度走完后填充数据并跳转预览 */
|
||||
function handleCustomStepFinished() {
|
||||
showCustomStepProgress.value = false
|
||||
if (pendingCustomResumeData.value) {
|
||||
fillCustomResumeData(pendingCustomResumeData.value)
|
||||
currentStep.value = 4
|
||||
pendingCustomResumeData.value = null
|
||||
}
|
||||
}
|
||||
|
||||
/** 关闭步骤进度遮罩 */
|
||||
function handleCustomStepClose() {
|
||||
showCustomStepProgress.value = false
|
||||
}
|
||||
|
||||
/** 当前步骤:2-差距分析(右侧抽屉) 3-定制简历 4-预览 */
|
||||
const currentStep = ref(2)
|
||||
|
||||
@@ -504,17 +548,15 @@ async function handleDrawerNext() {
|
||||
* 流程:先 GET 查询 → 有数据直接用 → 无数据则 POST 生成 → 再 GET 查询
|
||||
*/
|
||||
async function fetchAndLoadCustomResume() {
|
||||
fullLoadingText.value = '正在生成定制简历...'
|
||||
fullLoading.value = true
|
||||
// 显示步骤进度遮罩
|
||||
showCustomStepProgress.value = true
|
||||
customStepDone.value = false
|
||||
// 缓存数据用于进度完成后填充
|
||||
let resumeData: CustomizeResumeData | null = null
|
||||
|
||||
try {
|
||||
// 第一步:查询是否已有定制简历
|
||||
let queryRes = await fetchCustomizeResume(props.jobId)
|
||||
// if (queryRes.code === 0 && queryRes.data) {
|
||||
// // 已有定制简历,直接填充数据并跳转预览
|
||||
// fillCustomResumeData(queryRes.data)
|
||||
// currentStep.value = 4
|
||||
// return
|
||||
// }
|
||||
|
||||
// 第二步:没有定制简历,调用生成接口
|
||||
const genRes = await generateCustomizeResume({
|
||||
@@ -525,6 +567,7 @@ async function fetchAndLoadCustomResume() {
|
||||
})
|
||||
|
||||
if (genRes.code !== 0 || !genRes.data?.success) {
|
||||
showCustomStepProgress.value = false
|
||||
ElMessage.error('生成定制简历失败,请稍后重试')
|
||||
return
|
||||
}
|
||||
@@ -532,16 +575,19 @@ async function fetchAndLoadCustomResume() {
|
||||
// 第三步:生成成功后再次查询获取简历数据
|
||||
queryRes = await fetchCustomizeResume(props.jobId)
|
||||
if (queryRes.code === 0 && queryRes.data) {
|
||||
fillCustomResumeData(queryRes.data)
|
||||
currentStep.value = 4
|
||||
resumeData = queryRes.data
|
||||
// 通知步骤进度组件接口已完成
|
||||
customStepDone.value = true
|
||||
// 缓存数据,等进度走完后再填充
|
||||
pendingCustomResumeData.value = resumeData
|
||||
} else {
|
||||
showCustomStepProgress.value = false
|
||||
ElMessage.error('获取定制简历数据失败')
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[JobResumeCustomDialog] 定制简历流程失败', e)
|
||||
showCustomStepProgress.value = false
|
||||
ElMessage.error('定制简历失败,请稍后重试')
|
||||
} finally {
|
||||
fullLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -52,10 +52,10 @@
|
||||
<div class="member-dialog__plan-price">
|
||||
<span class="member-dialog__plan-price-symbol">¥</span>
|
||||
<span class="member-dialog__plan-price-num">{{ plan.priceInt }}</span>
|
||||
<span v-if="plan.originalPrice" class="member-dialog__plan-original-price">¥{{ plan.originalPrice }}</span>
|
||||
<span v-if="plan.originalPrice" class="member-dialog__plan-original-price">{{ plan.originalPrice }}</span>
|
||||
</div>
|
||||
<!-- 每日价格 -->
|
||||
<div class="member-dialog__plan-daily">{{ plan.dailyDesc }}</div>
|
||||
<!-- 标签描述(接口tag字段) -->
|
||||
<div class="member-dialog__plan-daily mt4">{{ plan.tag }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -211,8 +211,8 @@ interface PlanItem {
|
||||
priceInt: string
|
||||
/** 划线价(元) */
|
||||
originalPrice: string
|
||||
/** 每日价格描述 */
|
||||
dailyDesc: string
|
||||
/** 标签描述(接口tag字段,如"轻量体验"、"低至¥0.56/天") */
|
||||
tag: string
|
||||
/** 是否推荐 */
|
||||
recommend?: boolean
|
||||
/** 后端原始数据,下单时使用 */
|
||||
@@ -269,7 +269,7 @@ watch(() => props.modelValue, (val) => {
|
||||
|
||||
/** 当前选中的套餐对象 */
|
||||
const currentPlan = computed(() => {
|
||||
return plans.value.find(p => p.key === selectedPlan.value) || plans.value[0] || { priceInt: '0', name: '', key: '', originalPrice: '', dailyDesc: '', raw: {} as MemberProduct }
|
||||
return plans.value.find(p => p.key === selectedPlan.value) || plans.value[0] || { priceInt: '0', name: '', key: '', originalPrice: '', tag: '', raw: {} as MemberProduct }
|
||||
})
|
||||
|
||||
// ==================== 数据 ====================
|
||||
@@ -284,25 +284,6 @@ function mapProductToPlan(product: MemberProduct): PlanItem {
|
||||
const originalPriceYuan = product.originalPrice ? product.originalPrice / 100 : 0
|
||||
// 直接使用接口返回的 productName 作为套餐名称
|
||||
const name = product.productName
|
||||
// 根据天数计算每日价格描述
|
||||
let dailyDesc = '轻量体验'
|
||||
if (product.durationDays <= 7) {
|
||||
dailyDesc = '轻量体验'
|
||||
} else if (product.durationDays <= 31) {
|
||||
// 月卡除以30天,四舍五入到小数点后2位
|
||||
const dailyPrice = (priceYuan / 30).toFixed(2)
|
||||
dailyDesc = `低至 ¥${dailyPrice} /天`
|
||||
} else if (product.durationDays <= 93) {
|
||||
// 季卡除以90天,四舍五入到小数点后2位
|
||||
const dailyPrice = (priceYuan / 90).toFixed(2)
|
||||
dailyDesc = `低至 ¥${dailyPrice} /天`
|
||||
} else if (product.durationDays <= 186) {
|
||||
const dailyPrice = (priceYuan / 180).toFixed(2)
|
||||
dailyDesc = `低至 ¥${dailyPrice} /天`
|
||||
} else {
|
||||
const dailyPrice = (priceYuan / 365).toFixed(2)
|
||||
dailyDesc = `低至 ¥${dailyPrice} /天`
|
||||
}
|
||||
|
||||
// 价格显示:如果是整数就不显示小数,否则保留实际小数
|
||||
const priceDisplay = Number.isInteger(priceYuan) ? String(priceYuan) : priceYuan.toFixed(2)
|
||||
@@ -315,7 +296,7 @@ function mapProductToPlan(product: MemberProduct): PlanItem {
|
||||
name,
|
||||
priceInt: priceDisplay,
|
||||
originalPrice: originalDisplay,
|
||||
dailyDesc,
|
||||
tag: product.tag || '',
|
||||
recommend: product.isFeatured === 1,
|
||||
raw: product,
|
||||
}
|
||||
|
||||
@@ -16,13 +16,13 @@
|
||||
<!-- 个人信息 -->
|
||||
<div ref="infoRef" class="profile-page-content__card p10" :class="{ 'profile-page-content__card-hover': editingModule !== 'info' }">
|
||||
<template v-if="editingModule !== 'info'">
|
||||
<div class="profile-page-content__card-header pl0">
|
||||
<h3 class="profile-page-content__card-title">个人信息</h3>
|
||||
<div class="profile-page-content__card-header pl0 mb-20">
|
||||
<h3 class="profile-page-content__card-title"></h3>
|
||||
<button class="profile-page-content__edit-btn-inline" @click="startEdit('info')">
|
||||
<el-icon><EditPen /></el-icon><span>编辑</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="profile-page-content__info-name dflex-start aliite-c"><span>{{ profile.name }}</span><span class="fs13 color-6 ml10 dflex-start aliite-c"><svg viewBox="0 0 16 16" fill="none" class="resume-detail__contact-icon">
|
||||
<div class="profile-page-content__info-name dflex-start aliite-e"><span class="fs28">{{ profile.name }}</span><span class="fs13 color-6 ml10 dflex-start aliite-c pb5"><svg viewBox="0 0 16 16" fill="none" class="resume-detail__contact-icon">
|
||||
<circle cx="8" cy="6.5" r="2.5" stroke="currentColor" stroke-width="1.2"/>
|
||||
<path d="M8 14s-5-4-5-7.5a5 5 0 0110 0C13 10 8 14 8 14z" stroke="currentColor" stroke-width="1.2"/>
|
||||
</svg> {{ resolveRegionName(profile.regionCode) }}</span></div>
|
||||
|
||||
@@ -61,13 +61,14 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { useStore } from 'vuex'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import MemberDialog from './MemberDialog.vue'
|
||||
import { fetchInviteStats } from '@/api/member'
|
||||
|
||||
/** 组件 Props */
|
||||
defineProps<{ modelValue: boolean }>()
|
||||
const props = defineProps<{ modelValue: boolean }>()
|
||||
|
||||
/** 组件 Emits */
|
||||
defineEmits<{
|
||||
@@ -79,17 +80,32 @@ const store = useStore()
|
||||
/** 会员弹窗显示状态 */
|
||||
const showMemberDialog = ref(false)
|
||||
|
||||
/** 累计获得会员天数(暂用模拟数据) */
|
||||
const totalDays = ref(7)
|
||||
/** 累计获得会员天数 */
|
||||
const totalDays = ref(0)
|
||||
|
||||
/** 累计邀请好友数(暂用模拟数据) */
|
||||
const totalFriends = ref(1)
|
||||
/** 累计邀请好友数 */
|
||||
const totalFriends = ref(0)
|
||||
|
||||
/**
|
||||
* 弹窗打开时请求邀请统计接口
|
||||
*/
|
||||
watch(() => props.modelValue, async (val) => {
|
||||
if (val) {
|
||||
try {
|
||||
const res = await fetchInviteStats()
|
||||
totalFriends.value = res.data?.inviteCount ?? 0
|
||||
totalDays.value = res.data?.rewardDays ?? 0
|
||||
} catch (e) {
|
||||
console.error('获取邀请统计失败', e)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
/** 用户邀请码 — 从全局 store 读取 */
|
||||
const inviteCode = computed(() => store.state.userInfo?.inviteCode || 'NOVA2026')
|
||||
|
||||
/** 邀请链接地址 */
|
||||
const inviteUrl = computed(() => `https://offerpai.com/invite?code=${inviteCode.value}`)
|
||||
const inviteUrl = computed(() => `https://www.offerpai.com.cn/invite?code=${inviteCode.value}`)
|
||||
|
||||
/** 复制链接到剪贴板 */
|
||||
async function handleCopy() {
|
||||
|
||||
@@ -42,14 +42,14 @@
|
||||
<!-- 邀请进度行 -->
|
||||
<div class="side-nav__referral-row">
|
||||
<span class="side-nav__referral-label">邀请进度</span>
|
||||
<span class="side-nav__referral-value">12/30</span>
|
||||
<span class="side-nav__referral-value">{{ milestoneProgress }}/{{ milestoneTarget }}</span>
|
||||
</div>
|
||||
<!-- 进度条 -->
|
||||
<div class="side-nav__referral-track">
|
||||
<div class="side-nav__referral-fill" style="width: 40%"></div>
|
||||
<div class="side-nav__referral-fill" :style="{ width: inviteProgressPercent + '%' }"></div>
|
||||
</div>
|
||||
<!-- 提示文字 -->
|
||||
<div class="side-nav__referral-tip">再邀请 <strong>18</strong> 人,可获得年会员</div>
|
||||
<div class="side-nav__referral-tip">再邀请 <strong>{{ inviteRemaining }}</strong> 人,可获得年会员</div>
|
||||
<!-- 分隔线 -->
|
||||
<div class="side-nav__referral-divider"></div>
|
||||
<!-- 去邀请按钮 -->
|
||||
@@ -78,7 +78,7 @@
|
||||
</div>
|
||||
|
||||
<!-- 用户信息卡片(点击弹出/关闭账户弹窗) -->
|
||||
<div class="side-nav__user-info" @click.stop="showAccountPopup = !showAccountPopup">
|
||||
<div class="side-nav__user-info" :class="{ 'side-nav__user-info--active': showAccountPopup }" @click.stop="showAccountPopup = !showAccountPopup">
|
||||
<div class="side-nav__user-left">
|
||||
<!-- 手机号(脱敏显示) -->
|
||||
<div class="side-nav__user-phone">{{ maskedPhone }}</div>
|
||||
@@ -230,6 +230,7 @@ import MemberDialog from '@/components/MemberDialog.vue'
|
||||
import { checkLogin } from '@/api/auth'
|
||||
import { logout } from '@/api/auth'
|
||||
import { fetchMessageList, fetchUnreadCount, markMessageRead } from '@/api/message'
|
||||
import { fetchInviteStats } from '@/api/member'
|
||||
import { timestampToLocalDateTime } from '@/utils/time'
|
||||
|
||||
/**
|
||||
@@ -368,6 +369,34 @@ import SettingsInviteDialog from "@/components/SettingsInviteDialog.vue";
|
||||
// ==================== 站内信相关 ====================
|
||||
/** 未读消息数量 */
|
||||
const unreadCount = ref(0)
|
||||
|
||||
// ==================== 邀请统计相关 ====================
|
||||
/** 当前周期已邀请人数 */
|
||||
const milestoneProgress = ref(0)
|
||||
/** 里程碑目标总人数(从接口获取) */
|
||||
const milestoneTarget = ref(30)
|
||||
/** 进度条百分比 */
|
||||
const inviteProgressPercent = computed(() => {
|
||||
if (milestoneTarget.value <= 0) return 0
|
||||
return Math.min(Math.round((milestoneProgress.value / milestoneTarget.value) * 100), 100)
|
||||
})
|
||||
/** 还需邀请人数 */
|
||||
const inviteRemaining = computed(() => {
|
||||
return Math.max(milestoneTarget.value - milestoneProgress.value, 0)
|
||||
})
|
||||
|
||||
/**
|
||||
* 获取邀请统计数据
|
||||
*/
|
||||
async function loadInviteStats() {
|
||||
try {
|
||||
const res = await fetchInviteStats()
|
||||
milestoneProgress.value = res.data?.milestoneProgress ?? 0
|
||||
milestoneTarget.value = res.data?.milestoneTarget ?? 30
|
||||
} catch (e) {
|
||||
console.error('获取邀请统计失败', e)
|
||||
}
|
||||
}
|
||||
/** 消息列表数据 */
|
||||
const messageList = ref<MessageDto[]>([])
|
||||
/** 当前选中的消息索引 */
|
||||
@@ -650,6 +679,7 @@ function handleGlobalClick() {
|
||||
|
||||
onMounted(() => {
|
||||
loadUnreadCount()
|
||||
loadInviteStats()
|
||||
document.addEventListener('click', handleGlobalClick)
|
||||
})
|
||||
|
||||
|
||||
@@ -0,0 +1,229 @@
|
||||
<template>
|
||||
<!-- 步骤进度加载遮罩组件 — 居中面板 + 半透明遮罩 -->
|
||||
<div class="step-progress-overlay">
|
||||
<!-- 半透明遮罩背景 -->
|
||||
<div class="step-progress-overlay__mask"></div>
|
||||
<!-- 内容面板 -->
|
||||
<div class="step-progress-overlay__panel">
|
||||
<!-- 顶部标题行 -->
|
||||
<div class="step-progress-overlay__header">
|
||||
<div class="step-progress-overlay__titles">
|
||||
<h2 class="step-progress-overlay__title">{{ title }}</h2>
|
||||
<p class="step-progress-overlay__subtitle">{{ subtitle }}</p>
|
||||
</div>
|
||||
<!-- 关闭按钮 -->
|
||||
<button class="step-progress-overlay__close" @click="handleClose" aria-label="关闭">
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1 1L13 13M13 1L1 13" stroke="#6A7282" stroke-width="1.6" stroke-linecap="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 进度面板 -->
|
||||
<div class="step-progress-overlay__progress-box">
|
||||
<!-- 进度条头部 -->
|
||||
<div class="step-progress-overlay__progress-header">
|
||||
<span class="step-progress-overlay__progress-label">处理进度</span>
|
||||
<span class="step-progress-overlay__progress-percent">{{ percent }}%</span>
|
||||
</div>
|
||||
<!-- 进度轨道 -->
|
||||
<div class="step-progress-overlay__progress-rail">
|
||||
<div class="step-progress-overlay__progress-fill" :style="{ width: percent + '%' }"></div>
|
||||
</div>
|
||||
|
||||
<!-- 步骤列表 -->
|
||||
<div class="step-progress-overlay__steps">
|
||||
<div v-for="(item, idx) in stepList" :key="idx" class="step-progress-overlay__step-item">
|
||||
<!-- 左侧圆点 + 连接线 -->
|
||||
<div class="step-progress-overlay__step-left">
|
||||
<!-- 完成状态:对勾圆 -->
|
||||
<div v-if="item.status === 'done'" class="step-progress-overlay__dot step-progress-overlay__dot--done">
|
||||
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M2.5 5.2L4.2 6.8L7.5 3.5" stroke="#fff" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
<!-- 进行中:光晕 + 呼吸内圆 -->
|
||||
<div v-else-if="item.status === 'active'" class="step-progress-overlay__dot step-progress-overlay__dot--active">
|
||||
<div class="step-progress-overlay__dot-pulse"></div>
|
||||
</div>
|
||||
<!-- 等待状态:浅色圆 -->
|
||||
<div v-else class="step-progress-overlay__dot step-progress-overlay__dot--pending"></div>
|
||||
<!-- 连接线(最后一项不显示) -->
|
||||
<div v-if="idx < stepList.length - 1" class="step-progress-overlay__connector"></div>
|
||||
</div>
|
||||
<!-- 步骤名称 -->
|
||||
<span class="step-progress-overlay__step-text" :class="{ 'step-progress-overlay__step-text--pending': item.status === 'pending' }">{{ item.label }}</span>
|
||||
<!-- 步骤状态文字 -->
|
||||
<span class="step-progress-overlay__step-state" :class="{
|
||||
'step-progress-overlay__step-state--done': item.status === 'done',
|
||||
'step-progress-overlay__step-state--active': item.status === 'active',
|
||||
'step-progress-overlay__step-state--pending': item.status === 'pending'
|
||||
}">{{ item.stateText }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, onBeforeUnmount } from 'vue'
|
||||
|
||||
// ==================== Props ====================
|
||||
|
||||
interface Props {
|
||||
/** 大标题 */
|
||||
title: string
|
||||
/** 副标题 */
|
||||
subtitle: string
|
||||
/** 4个步骤名称 */
|
||||
steps: string[]
|
||||
/** 4个步骤对应的模拟时长(毫秒) */
|
||||
durations: number[]
|
||||
/** 接口是否已完成,父组件设为 true 后走完剩余进度 */
|
||||
done: boolean
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
done: false,
|
||||
})
|
||||
|
||||
// ==================== Emits ====================
|
||||
|
||||
const emit = defineEmits<{
|
||||
/** 进度走完100%后通知父组件 */
|
||||
(e: 'finished'): void
|
||||
/** 点击关闭按钮 */
|
||||
(e: 'close'): void
|
||||
}>()
|
||||
|
||||
// ==================== 内部状态 ====================
|
||||
|
||||
/** 步骤项接口 */
|
||||
interface StepItem {
|
||||
label: string
|
||||
status: 'pending' | 'active' | 'done'
|
||||
stateText: string
|
||||
}
|
||||
|
||||
/** 当前进度百分比 */
|
||||
const percent = ref(0)
|
||||
|
||||
/** 步骤列表 */
|
||||
const stepList = ref<StepItem[]>(
|
||||
props.steps.map((label) => ({ label, status: 'pending' as const, stateText: '等待' }))
|
||||
)
|
||||
|
||||
/** 进度动画定时器 */
|
||||
let progressTimer: ReturnType<typeof setInterval> | null = null
|
||||
/** 标记接口是否已返回 */
|
||||
let uploadDone = false
|
||||
/** 标记动画是否已完成(防止重复 emit) */
|
||||
let hasFinished = false
|
||||
|
||||
// ==================== 核心动画逻辑 ====================
|
||||
|
||||
/** 每个步骤对应的进度百分比段 */
|
||||
function getStepPercents(): number[] {
|
||||
const total = props.steps.length
|
||||
// 均分到95%,如4步则为 [23, 47, 71, 95]
|
||||
return props.steps.map((_, i) => Math.floor(((i + 1) / total) * 95))
|
||||
}
|
||||
|
||||
/** 开始步骤动画 */
|
||||
function startAnimation() {
|
||||
const totalSteps = stepList.value.length
|
||||
const stepPercents = getStepPercents()
|
||||
let currentStep = 0
|
||||
|
||||
// 第一个步骤立即进入进行中
|
||||
stepList.value[0].status = 'active'
|
||||
stepList.value[0].stateText = '进行中'
|
||||
|
||||
let targetPercent = stepPercents[0]
|
||||
let lastStepTime = Date.now()
|
||||
|
||||
progressTimer = setInterval(() => {
|
||||
const elapsed = Date.now() - lastStepTime
|
||||
const stepDuration = props.durations[currentStep] || 2000
|
||||
|
||||
if (elapsed >= stepDuration && currentStep < totalSteps - 1) {
|
||||
// 当前步骤完成
|
||||
stepList.value[currentStep].status = 'done'
|
||||
stepList.value[currentStep].stateText = '完成'
|
||||
currentStep++
|
||||
// 下一个步骤进入进行中
|
||||
stepList.value[currentStep].status = 'active'
|
||||
stepList.value[currentStep].stateText = '进行中'
|
||||
targetPercent = stepPercents[currentStep]
|
||||
lastStepTime = Date.now()
|
||||
} else if (currentStep >= totalSteps - 1 && elapsed >= stepDuration) {
|
||||
// 最后一个步骤时间到
|
||||
if (uploadDone) {
|
||||
// 接口已返回,完成最后一步
|
||||
stepList.value[currentStep].status = 'done'
|
||||
stepList.value[currentStep].stateText = '完成'
|
||||
if (progressTimer) { clearInterval(progressTimer); progressTimer = null }
|
||||
// 进度条跳到95-99再走完到100
|
||||
const finalPercent = Math.floor(95 + Math.random() * 4)
|
||||
percent.value = finalPercent
|
||||
setTimeout(() => {
|
||||
percent.value = 100
|
||||
setTimeout(() => {
|
||||
if (!hasFinished) { hasFinished = true; emit('finished') }
|
||||
}, 600)
|
||||
}, 1000)
|
||||
return
|
||||
}
|
||||
// 接口未返回,缓慢爬进度,上限94%
|
||||
percent.value = Math.min(percent.value + 0.1, 94)
|
||||
return
|
||||
}
|
||||
|
||||
// 平滑增长进度
|
||||
const progress = Math.min(elapsed / stepDuration, 1)
|
||||
const prevPercent = currentStep > 0 ? stepPercents[currentStep - 1] : 0
|
||||
percent.value = Math.floor(prevPercent + (targetPercent - prevPercent) * progress)
|
||||
}, 50)
|
||||
}
|
||||
|
||||
/** 接口返回后完成动画(定时器已停时手动触发) */
|
||||
function finishAnimation() {
|
||||
if (!progressTimer) {
|
||||
const lastIdx = stepList.value.length - 1
|
||||
stepList.value[lastIdx].status = 'done'
|
||||
stepList.value[lastIdx].stateText = '完成'
|
||||
const finalPercent = Math.floor(95 + Math.random() * 4)
|
||||
percent.value = finalPercent
|
||||
setTimeout(() => {
|
||||
percent.value = 100
|
||||
setTimeout(() => {
|
||||
if (!hasFinished) { hasFinished = true; emit('finished') }
|
||||
}, 600)
|
||||
}, 1000)
|
||||
}
|
||||
}
|
||||
|
||||
/** 点击关闭按钮 */
|
||||
function handleClose() {
|
||||
emit('close')
|
||||
}
|
||||
|
||||
// ==================== 监听 done prop ====================
|
||||
|
||||
watch(() => props.done, (val) => {
|
||||
if (val) {
|
||||
uploadDone = true
|
||||
finishAnimation()
|
||||
}
|
||||
})
|
||||
|
||||
// ==================== 生命周期 ====================
|
||||
|
||||
// 组件挂载后立即开始动画
|
||||
startAnimation()
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (progressTimer) { clearInterval(progressTimer); progressTimer = null }
|
||||
})
|
||||
</script>
|
||||
+49
-5
@@ -171,6 +171,18 @@
|
||||
<MemberDialog v-model="showMemberDialog" />
|
||||
<!-- 全屏加载遮罩 -->
|
||||
<FullscreenLoading :visible="fullLoading" :text="fullLoadingText" title="Nova 正在帮您努力处理~" />
|
||||
|
||||
<!-- 岗位简历生成步骤进度遮罩 -->
|
||||
<StepProgressOverlay
|
||||
v-if="showStepProgress"
|
||||
:title="stepProgressTitle"
|
||||
:subtitle="stepProgressSubtitle"
|
||||
:steps="stepProgressSteps"
|
||||
:durations="stepProgressDurations"
|
||||
:done="stepProgressDone"
|
||||
@finished="handleStepProgressFinished"
|
||||
@close="handleStepProgressClose"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -186,6 +198,7 @@ import JobResumeCustomDialog from '@/components/JobResumeCustomDialog.vue'
|
||||
import MemberAccessDialog from '@/components/MemberAccessDialog.vue'
|
||||
import FullscreenLoading from '@/components/FullscreenLoading.vue'
|
||||
import MemberDialog from '@/components/MemberDialog.vue'
|
||||
import StepProgressOverlay from '@/components/StepProgressOverlay.vue'
|
||||
import { fetchJobDetail, toggleJobFavorite, removeJobFavorite, fetchSkillGap } from '@/api/jobs'
|
||||
import { fetchMemberStatus } from '@/api/member'
|
||||
import type { JobDetailData, SkillGapData } from '@/api/jobs'
|
||||
@@ -475,23 +488,54 @@ async function handleGenerateResume() {
|
||||
// 接口异常时不阻断,继续执行
|
||||
}
|
||||
|
||||
fullLoadingText.value = '正在分析岗位匹配度...'
|
||||
fullLoading.value = true
|
||||
// 显示步骤进度遮罩
|
||||
showStepProgress.value = true
|
||||
stepProgressDone.value = false
|
||||
|
||||
try {
|
||||
const res = await fetchSkillGap(jobId)
|
||||
if (res.code === 0 && res.data) {
|
||||
skillGapData.value = res.data
|
||||
// 通知组件接口已完成
|
||||
stepProgressDone.value = true
|
||||
} else {
|
||||
// 接口返回异常,关闭遮罩提示错误
|
||||
showStepProgress.value = false
|
||||
ElMessage.error('分析失败,请稍后重试')
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('技能差距分析失败', e)
|
||||
showStepProgress.value = false
|
||||
ElMessage.error('分析失败,请稍后重试')
|
||||
return
|
||||
} finally {
|
||||
fullLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 步骤进度遮罩相关 ====================
|
||||
|
||||
/** 步骤进度遮罩是否显示 */
|
||||
const showStepProgress = ref(false)
|
||||
/** 接口是否已完成 */
|
||||
const stepProgressDone = ref(false)
|
||||
/** 步骤进度大标题 */
|
||||
const stepProgressTitle = '分析简历中,请耐心等待'
|
||||
/** 步骤进度副标题 */
|
||||
const stepProgressSubtitle = '正在根据岗位所需技能、简历详情发现问题'
|
||||
/** 步骤名称 */
|
||||
const stepProgressSteps = ['岗位技能解析...', '岗位职责解析...', '简历经历技能匹配...', '优化建议最终生成...']
|
||||
/** 每步模拟时长(毫秒) */
|
||||
const stepProgressDurations = [2000, 2000, 2000, 2000]
|
||||
|
||||
/** 步骤进度走完后打开简历定制弹窗 */
|
||||
function handleStepProgressFinished() {
|
||||
showStepProgress.value = false
|
||||
showResumeCustomDialog.value = true
|
||||
}
|
||||
|
||||
/** 关闭步骤进度遮罩 */
|
||||
function handleStepProgressClose() {
|
||||
showStepProgress.value = false
|
||||
}
|
||||
|
||||
/** 不优化直接投递 */
|
||||
function handleSkipToApply() {
|
||||
if (job.sourceUrl) {
|
||||
|
||||
+97
-18
@@ -5,6 +5,14 @@
|
||||
<!-- 页面标题 + 上传按钮 -->
|
||||
<div class="resume-page__header">
|
||||
<h2 class="resume-page__title">我的简历</h2>
|
||||
<div class="dflex-end">
|
||||
<!-- 从个人资料创建按钮 -->
|
||||
<button class="resume-page__create-btn mr16" :disabled="creatingFromProfile" @click="handleCreateFromProfile">
|
||||
<svg viewBox="0 0 16 16" fill="none" class="resume-page__upload-icon">
|
||||
<path d="M8 3v10M3 8h10" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
||||
</svg>
|
||||
从个人资料创建
|
||||
</button>
|
||||
<button class="resume-page__upload-btn" @click="handleUpload">
|
||||
<svg viewBox="0 0 16 16" fill="none" class="resume-page__upload-icon">
|
||||
<path d="M8 3v10M3 8h10" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
||||
@@ -12,6 +20,7 @@
|
||||
上传简历
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 简历表格 -->
|
||||
<div class="resume-page__table-wrap">
|
||||
@@ -39,14 +48,6 @@
|
||||
<div class="resume-page__name-cell">
|
||||
<span class="resume-page__name">{{ item.name }}</span>
|
||||
<span v-if="item.isDefault" class="resume-page__default-tag">默认</span>
|
||||
<!-- 编辑名称岗位图标 -->
|
||||
<button
|
||||
class="resume-page__edit-btn"
|
||||
aria-label="编辑名称岗位"
|
||||
@click.stop="handleEditName(item)"
|
||||
>
|
||||
<el-icon class="resume-page__edit-icon"><EditPen /></el-icon>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
<td class="resume-page__td">{{ item.targetJob }}</td>
|
||||
@@ -54,6 +55,16 @@
|
||||
<!-- 操作按钮列 -->
|
||||
<td class="resume-page__td resume-page__td--action">
|
||||
<div class="resume-page__actions">
|
||||
<button
|
||||
class="resume-page__action-btn"
|
||||
@click.stop="handleEditName(item)"
|
||||
>
|
||||
<svg width="12" height="12" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.0523 1.82016C11.7781 1.54295 11.4366 1.34358 11.062 1.24212C10.6875 1.14065 10.2931 1.14065 9.91851 1.24212C9.54396 1.34358 9.2024 1.54295 8.92822 1.82016L8.29348 2.46298C8.2617 2.4886 8.23269 2.51753 8.20693 2.54932L1.90565 8.92043C1.82508 9.00168 1.77008 9.10517 1.74754 9.21793L1.16589 12.1346C1.14691 12.2288 1.1513 12.3264 1.17863 12.4185C1.20598 12.5107 1.25544 12.5945 1.32261 12.6627C1.38979 12.7308 1.47258 12.7811 1.56364 12.809C1.65458 12.837 1.75127 12.8417 1.84449 12.8229L4.7343 12.2396C4.84585 12.2168 4.94823 12.1612 5.02859 12.0797L12.0523 4.97947C12.3265 4.70229 12.5237 4.35703 12.6241 3.97838C12.7245 3.59974 12.7245 3.20105 12.6241 2.8224C12.5237 2.44375 12.3265 2.09849 12.0523 1.82133" fill="currentColor"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.3353 11.1303L2.46814 11.5083L2.84319 9.62298L8.65699 3.74536L10.1491 5.25385L4.3353 11.1303ZM9.7439 2.64628C9.8742 2.51131 10.0376 2.41368 10.2174 2.36336C10.3972 2.31304 10.587 2.31183 10.7675 2.35985C10.9479 2.40787 11.1125 2.5034 11.2445 2.63669C11.3765 2.76999 11.4711 2.93629 11.5187 3.11863C11.5664 3.30096 11.5653 3.4928 11.5157 3.67459C11.466 3.85638 11.3696 4.0216 11.2362 4.15341L10.9661 4.42871L9.47384 2.91925L9.7439 2.64628Z" fill="white"/>
|
||||
</svg>
|
||||
<span class="fs12 color-middle ml3 color-dark-ho">修改简历信息</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="!item.isDefault"
|
||||
class="resume-page__action-btn"
|
||||
@@ -97,6 +108,17 @@
|
||||
/>
|
||||
<!-- 全屏加载遮罩 -->
|
||||
<FullscreenLoading :visible="fullLoading" :text="fullLoadingText" title="Nova 正在帮您努力处理~" />
|
||||
<!-- 简历解析步骤进度遮罩 -->
|
||||
<StepProgressOverlay
|
||||
v-if="showUploadProgress"
|
||||
:title="uploadProgressTitle"
|
||||
:subtitle="uploadProgressSubtitle"
|
||||
:steps="uploadProgressSteps"
|
||||
:durations="uploadProgressDurations"
|
||||
:done="uploadProgressDone"
|
||||
@finished="handleUploadProgressFinished"
|
||||
@close="handleUploadProgressClose"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -110,12 +132,13 @@ import ResumeExportDialog from '@/components/ResumeExportDialog.vue'
|
||||
import ResumeUploadDialog from '@/components/ResumeUploadDialog.vue'
|
||||
import { uploadResume } from '@/utils/aiRequest'
|
||||
import {
|
||||
fetchResumeList, deleteResume, setDefaultResume, type ResumeListItem,
|
||||
fetchResumeList, deleteResume, setDefaultResume, createResumeFromProfile, type ResumeListItem,
|
||||
} from '@/api/resume'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
// 命令式调用的组件,按需引入插件不会自动加载其样式,需手动引入
|
||||
import 'element-plus/es/components/message-box/style/css'
|
||||
import FullscreenLoading from '@/components/FullscreenLoading.vue'
|
||||
import StepProgressOverlay from '@/components/StepProgressOverlay.vue'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
@@ -207,6 +230,30 @@ onMounted(() => {
|
||||
loadResumeList()
|
||||
})
|
||||
|
||||
// ==================== 从个人资料创建简历 ====================
|
||||
|
||||
/** 防抖锁:防止多次点击 */
|
||||
const creatingFromProfile = ref(false)
|
||||
|
||||
/**
|
||||
* 从个人资料创建简历(带防抖)
|
||||
*/
|
||||
async function handleCreateFromProfile() {
|
||||
if (creatingFromProfile.value) return
|
||||
creatingFromProfile.value = true
|
||||
try {
|
||||
const res = await createResumeFromProfile()
|
||||
if (res.code === '0') {
|
||||
ElMessage.success('创建成功')
|
||||
await loadResumeList()
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('从个人资料创建简历失败', e)
|
||||
} finally {
|
||||
creatingFromProfile.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 页面状态 ====================
|
||||
|
||||
// ==================== 编辑简历名称弹窗状态 ====================
|
||||
@@ -301,29 +348,61 @@ function handleUpload() {
|
||||
|
||||
/** 上传弹窗确认回调 — 接收 File 对象,调用接口上传 */
|
||||
async function handleUploadConfirm(file: File) {
|
||||
// 全屏加载提示,AI 接口响应较慢
|
||||
fullLoadingText.value = '简历解析中,请耐心等待…'
|
||||
fullLoading.value = true
|
||||
// 显示步骤进度遮罩
|
||||
showUploadProgress.value = true
|
||||
uploadProgressDone.value = false
|
||||
pendingResumeId.value = ''
|
||||
|
||||
try {
|
||||
const res = await uploadResume(file)
|
||||
if (res.code === 0) {
|
||||
// 上传成功,刷新列表
|
||||
loadResumeList()
|
||||
// 等待让后端异步处理数据,再关闭加载动画并跳转详情页
|
||||
await new Promise(resolve => setTimeout(resolve, 2000))
|
||||
fullLoading.value = false
|
||||
goDetail(res.data.resumeId)
|
||||
// 缓存简历ID,等进度走完后跳转
|
||||
pendingResumeId.value = res.data.resumeId
|
||||
// 通知进度组件接口已完成
|
||||
uploadProgressDone.value = true
|
||||
} else {
|
||||
fullLoading.value = false
|
||||
showUploadProgress.value = false
|
||||
ElMessage.error(res.msg || '上传失败')
|
||||
}
|
||||
} catch {
|
||||
fullLoading.value = false
|
||||
showUploadProgress.value = false
|
||||
ElMessage.error('上传失败,请稍后重试')
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 简历解析步骤进度遮罩 ====================
|
||||
|
||||
/** 解析步骤进度遮罩是否显示 */
|
||||
const showUploadProgress = ref(false)
|
||||
/** 解析接口是否已完成 */
|
||||
const uploadProgressDone = ref(false)
|
||||
/** 大标题 */
|
||||
const uploadProgressTitle = '正在解析简历'
|
||||
/** 副标题 */
|
||||
const uploadProgressSubtitle = '正在识别教育背景、工作经历与技能关键词'
|
||||
/** 4个步骤名称 */
|
||||
const uploadProgressSteps = ['教育背景解析...', '工作经历解析...', '技能特长解析...', '个人资料最终确认']
|
||||
/** 每步时长4600毫秒 */
|
||||
const uploadProgressDurations = [4200, 4200, 4200, 4200]
|
||||
/** 缓存的简历ID(等进度走完后跳转) */
|
||||
const pendingResumeId = ref('')
|
||||
|
||||
/** 解析进度走完后跳转详情页 */
|
||||
function handleUploadProgressFinished() {
|
||||
showUploadProgress.value = false
|
||||
if (pendingResumeId.value) {
|
||||
goDetail(pendingResumeId.value)
|
||||
pendingResumeId.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
/** 关闭解析进度遮罩 */
|
||||
function handleUploadProgressClose() {
|
||||
showUploadProgress.value = false
|
||||
}
|
||||
|
||||
/** 跳转到简历详情页 */
|
||||
function goDetail(id: string) {
|
||||
router.push(`/resume/${id}`)
|
||||
|
||||
+174
-40
@@ -80,38 +80,40 @@
|
||||
<!-- 简历主体内容 -->
|
||||
<div class="resume-detail__body">
|
||||
|
||||
<!-- 简历评分区域(不滚动) -->
|
||||
<div class="resume-detail__fixed-top">
|
||||
|
||||
<!-- 简历评分区域 -->
|
||||
<div class="resume-detail__score-bar" :class="scoreBarClass">
|
||||
<div class="resume-detail__score-left">
|
||||
<!-- 星星+底座组合 -->
|
||||
<div class="resume-detail__star-wrapper">
|
||||
<!-- SVG 五角星(胖型,短角) -->
|
||||
<svg class="resume-detail__star-svg" viewBox="0 0 54 53" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M24.9956 1.45584C25.7174 0.181609 27.553 0.181662 28.2749 1.45584L35.4253 14.0769C35.8275 14.7869 36.5116 15.2941 37.3081 15.4715L51.2944 18.5847C52.6882 18.8952 53.2438 20.5831 52.3071 21.6609L42.7036 32.7117C42.1833 33.3104 41.9326 34.0971 42.0103 34.8865L43.4556 49.5652C43.5975 51.0087 42.1235 52.0649 40.8022 51.4666L27.8257 45.5867C27.0692 45.2441 26.2013 45.244 25.4448 45.5867L12.4692 51.4666C11.1479 52.0653 9.67395 51.0089 9.81592 49.5652L11.2612 34.8865C11.3389 34.0971 11.0882 33.3104 10.5679 32.7117L0.964355 21.6609C0.0276138 20.583 0.583121 18.8951 1.97705 18.5847L15.9624 15.4715C16.759 15.2941 17.444 14.787 17.8462 14.0769L24.9956 1.45584Z"
|
||||
:fill="starFillColor"
|
||||
:stroke="starStrokeColor"
|
||||
stroke-width="1.3"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<!-- 星星中间的文字(等级字母或问号) -->
|
||||
<text x="26" y="37" text-anchor="middle" :fill="starStrokeColor" font-size="24" font-weight="500">
|
||||
{{ hasDiagnosis ? diagnosisReport.grade : '?' }}
|
||||
</text>
|
||||
</svg>
|
||||
<!-- 底座长方形(遮住星星下方两角) -->
|
||||
<div class="resume-detail__star-base" :style="{ borderColor: starStrokeColor }">
|
||||
<span :style="{ color: starStrokeColor }">{{ hasDiagnosis ? gradeLabel : '未知' }}</span>
|
||||
|
||||
|
||||
<div class="w40 tac h40 lh38 border-ra50p " :style="{ background: starStrokeColor }">
|
||||
<span class="color-f fs18 fw600 dib">{{ hasDiagnosis ? diagnosisReport.grade : '?' }}</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<!-- 右侧操作按钮区 -->
|
||||
<div class="resume-detail__score-actions">
|
||||
<template v-if="hasDiagnosis">
|
||||
<div class="hp100">
|
||||
<!-- 底座长方形(遮住星星下方两角) -->
|
||||
<div class="resume-detail__star-base ">
|
||||
<span :style="{ color: starStrokeColor }">{{ hasDiagnosis ? gradeLabel : '未知' }}</span>
|
||||
</div>
|
||||
<div class="mt0 resume-detail__score-links">
|
||||
<button class="resume-detail__score-link" @click="handleViewReport">查看评估报告</button>
|
||||
<button class="resume-detail__score-link" @click="handleDiagnose">重新诊断</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div>
|
||||
<span class="resume-detail__score-hint">暂无等级,点击开始诊断简历</span>
|
||||
<div class="fs12 color-light mt4">AI 诊断将深度分析您的简历,给出评分和优化建议</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
@@ -121,24 +123,60 @@
|
||||
<div class="resume-detail__score-item">
|
||||
<span class="resume-detail__score-num" :style="{ color: starStrokeColor }">{{ issuesUrgentTotal+issuesImportantTotal+issuesExpressionTotal }}</span>
|
||||
<span class="resume-detail__score-label" :style="{ color: starStrokeColor }">项待修复</span>
|
||||
<button class="resume-detail__diagnose-btn" :style="{ background: starStrokeColor }" @click="handleFixAll">一键修复</button>
|
||||
<button class="resume-detail__diagnose-btn" :style="{ background: starStrokeColor }" @click="handleFixAll"><svg width="19" height="19" viewBox="0 0 19 19" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_76_1547)">
|
||||
<path d="M5.93736 4.4335L3.9582 5.54183L5.06653 3.56266L3.9582 1.5835L5.93736 2.69183L7.91653 1.5835L6.8082 3.56266L7.91653 5.54183L5.93736 4.4335ZM15.4374 12.1918L17.4165 11.0835L16.3082 13.0627L17.4165 15.0418L15.4374 13.9335L13.4582 15.0418L14.5665 13.0627L13.4582 11.0835L15.4374 12.1918ZM17.4165 1.5835L16.3082 3.56266L17.4165 5.54183L15.4374 4.4335L13.4582 5.54183L14.5665 3.56266L13.4582 1.5835L15.4374 2.69183L17.4165 1.5835ZM10.5607 10.1177L12.4924 8.186L10.814 6.50766L8.88236 8.43933L10.5607 10.1177ZM11.3761 5.77141L13.2286 7.62391C13.5374 7.91683 13.5374 8.43141 13.2286 8.74016L3.98986 17.9789C3.68111 18.2877 3.16653 18.2877 2.87361 17.9789L1.02111 16.1264C0.712363 15.8335 0.712363 15.3189 1.02111 15.0102L10.2599 5.77141C10.5686 5.46266 11.0832 5.46266 11.3761 5.77141Z" fill="white"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_76_1547">
|
||||
<rect width="19" height="19" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
<span class="verali-su fw600 ml5">一键修复</span></button>
|
||||
</div>
|
||||
</template>
|
||||
<!-- 没有诊断报告时显示开始诊断按钮 -->
|
||||
<template v-else>
|
||||
<button class="resume-detail__diagnose-btn" @click="handleDiagnose">开始诊断</button>
|
||||
<button class="resume-detail__diagnose-btn dflex-center aliite-c" @click="handleDiagnose">
|
||||
<svg width="20" height="20" viewBox="0 0 19 21" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_76_1376)">
|
||||
<path d="M16.753 15.5746L13.4589 12.2793C14.4466 10.9923 14.9077 9.37781 14.7488 7.76335C14.5898 6.14889 13.8226 4.65534 12.6029 3.58569C11.3832 2.51603 9.80222 1.95036 8.18074 2.00342C6.55927 2.05649 5.01869 2.72431 3.87152 3.87143C2.72435 5.01855 2.05649 6.55906 2.00342 8.18046C1.95036 9.80186 2.51605 11.3827 3.58576 12.6024C4.65547 13.8221 6.14908 14.5892 7.76362 14.7482C9.37816 14.9071 10.9927 14.446 12.2798 13.4584L15.5766 16.7558C15.654 16.8332 15.746 16.8946 15.8471 16.9365C15.9483 16.9784 16.0567 17 16.1662 17C16.2757 17 16.3841 16.9784 16.4853 16.9365C16.5864 16.8946 16.6784 16.8332 16.7558 16.7558C16.8332 16.6784 16.8946 16.5865 16.9365 16.4853C16.9784 16.3841 17 16.2757 17 16.1662C17 16.0567 16.9784 15.9483 16.9365 15.8472C16.8946 15.746 16.8332 15.6541 16.7558 15.5767L16.753 15.5746ZM3.67871 8.39513C3.67871 7.46229 3.95534 6.55041 4.47362 5.77478C4.9919 4.99916 5.72855 4.39464 6.59042 4.03766C7.45228 3.68068 8.40065 3.58728 9.31561 3.76926C10.2306 3.95125 11.071 4.40045 11.7306 5.06006C12.3903 5.71968 12.8395 6.56008 13.0215 7.47498C13.2035 8.38989 13.1101 9.33822 12.7531 10.2C12.3961 11.0619 11.7915 11.7985 11.0159 12.3167C10.2402 12.835 9.3283 13.1116 8.39542 13.1116C7.14487 13.1103 5.9459 12.613 5.06163 11.7288C4.17735 10.8445 3.68 9.64562 3.67871 8.39513Z" fill="white"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_76_1376">
|
||||
<rect width="19" height="19" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
<span class=" fs14 fw600 ml3">开始诊断</span></button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab 导航栏(固定不动) -->
|
||||
<div class="resume-detail__tabs">
|
||||
<span
|
||||
v-for="tab in resumeTabList"
|
||||
:key="tab.key"
|
||||
class="resume-detail__tab-item"
|
||||
:class="{ 'resume-detail__tab-item--active': resumeActiveTab === tab.key }"
|
||||
@click="scrollToResumeSection(tab.key)"
|
||||
>{{ tab.label }}</span>
|
||||
</div>
|
||||
|
||||
</div><!-- 固定顶部区域结束 -->
|
||||
|
||||
<!-- 可滚动内容区域 -->
|
||||
<div ref="resumeScrollRef" class="resume-detail__scroll-body">
|
||||
|
||||
<!-- 个人信息卡片 -->
|
||||
<div class="resume-detail__card" :class="{ 'resume-detail__card-hover': editModule !== 'info' || !showEditDrawer }">
|
||||
<div ref="resumeSectionInfoRef" class="resume-detail__card" :class="{ 'resume-detail__card-hover': editModule !== 'info' || !showEditDrawer }">
|
||||
<!-- 展示模式 -->
|
||||
<template v-if="editModule !== 'info' || !showEditDrawer">
|
||||
<div class="resume-detail__card-header color-6">
|
||||
<div class="dflex-start aliite-c">
|
||||
<h3 class="resume-detail__user-name">{{ resumeMain.name || '未填写姓名' }}</h3>
|
||||
<p class="resume-detail__user-title">{{ resumeMain.targetPosition || '' }}</p>
|
||||
<span v-if="resumeMain.city" class="ml10 dflex-start aliite-c fs13">
|
||||
<svg viewBox="0 0 16 16" fill="none" class="resume-detail__contact-icon">
|
||||
<circle cx="8" cy="6.5" r="2.5" stroke="currentColor" stroke-width="1.2"/>
|
||||
@@ -200,7 +238,7 @@
|
||||
</div>
|
||||
|
||||
<!-- 个人概述 -->
|
||||
<div class="resume-detail__card" :class="{ 'resume-detail__card-hover': editModule !== 'summary' || !showEditDrawer }">
|
||||
<div ref="resumeSectionSummaryRef" class="resume-detail__card" :class="{ 'resume-detail__card-hover': editModule !== 'summary' || !showEditDrawer }">
|
||||
<!-- 展示模式 -->
|
||||
<template v-if="editModule !== 'summary' || !showEditDrawer">
|
||||
<div class="resume-detail__section-header pl0">
|
||||
@@ -233,7 +271,7 @@
|
||||
</div>
|
||||
|
||||
<!-- 教育背景经历 -->
|
||||
<div class="resume-detail__card">
|
||||
<div ref="resumeSectionEducationRef" class="resume-detail__card">
|
||||
<div class="resume-detail__section-header">
|
||||
<h3 class="resume-detail__section-title resume-detail__section-title--underline">教育背景</h3>
|
||||
<!-- 添加按钮 — hover模块时显示 -->
|
||||
@@ -257,7 +295,7 @@
|
||||
<span v-if="edu.major || edu.degree" class="resume-detail__exp-role">{{ edu.major }}{{ edu.degree ? '·' + edu.degree : '' }}</span>
|
||||
<span v-if="hasDiagnosis && getIssueByRecord('education', edu.id!) && getIssueByRecord('education', edu.id!)!.status === 0" class="resume-detail__fix-tag">
|
||||
<svg viewBox="0 0 16 16" class="resume-detail__fix-tag-icon"><circle cx="8" cy="8" r="7" fill="#FF8D0B"/><text x="8" y="12" text-anchor="middle" font-size="11" fill="#fff" font-weight="bold">!</text></svg>
|
||||
建议使用AI润色修复 >>
|
||||
建议使用AI润色修复
|
||||
<span class="resume-detail__fix-tooltip">{{ getIssueByRecord('education', edu.id!)!.finding }}</span>
|
||||
</span>
|
||||
<div class="mlauto dflex-end">
|
||||
@@ -291,7 +329,7 @@
|
||||
</div>
|
||||
|
||||
<!-- 工作经历 -->
|
||||
<div class="resume-detail__card">
|
||||
<div ref="resumeSectionWorkRef" class="resume-detail__card">
|
||||
<div class="resume-detail__section-header">
|
||||
<h3 class="resume-detail__section-title resume-detail__section-title--underline">工作经历</h3>
|
||||
<button class="resume-detail__edit-btn-inline" @click="startAddExp('work')" aria-label="添加工作经历">
|
||||
@@ -307,7 +345,7 @@
|
||||
<span v-if="exp.position" class="resume-detail__exp-role">{{ exp.position }}</span>
|
||||
<span v-if="hasDiagnosis && getIssueByRecord('work', exp.id!) && getIssueByRecord('work', exp.id!)!.status === 0" class="resume-detail__fix-tag">
|
||||
<svg viewBox="0 0 16 16" class="resume-detail__fix-tag-icon"><circle cx="8" cy="8" r="7" fill="#FF8D0B"/><text x="8" y="12" text-anchor="middle" font-size="11" fill="#fff" font-weight="bold">!</text></svg>
|
||||
建议使用AI润色修复 >>
|
||||
建议使用AI润色修复
|
||||
<span class="resume-detail__fix-tooltip">{{ getIssueByRecord('work', exp.id!)!.finding }}</span>
|
||||
</span>
|
||||
<div class="mlauto dflex-end">
|
||||
@@ -330,7 +368,7 @@
|
||||
</div>
|
||||
|
||||
<!-- 实习经历 -->
|
||||
<div class="resume-detail__card">
|
||||
<div ref="resumeSectionInternshipRef" class="resume-detail__card">
|
||||
<div class="resume-detail__section-header">
|
||||
<h3 class="resume-detail__section-title resume-detail__section-title--underline">实习经历</h3>
|
||||
<button class="resume-detail__edit-btn-inline" @click="startAddExp('internship')" aria-label="添加实习经历">
|
||||
@@ -345,7 +383,7 @@
|
||||
<span v-if="intern.position" class="resume-detail__exp-role">{{ intern.position }}</span>
|
||||
<span v-if="hasDiagnosis && getIssueByRecord('internship', intern.id!) && getIssueByRecord('internship', intern.id!)!.status === 0" class="resume-detail__fix-tag">
|
||||
<svg viewBox="0 0 16 16" class="resume-detail__fix-tag-icon"><circle cx="8" cy="8" r="7" fill="#FF8D0B"/><text x="8" y="12" text-anchor="middle" font-size="11" fill="#fff" font-weight="bold">!</text></svg>
|
||||
建议使用AI润色修复 >>
|
||||
建议使用AI润色修复
|
||||
<span class="resume-detail__fix-tooltip">{{ getIssueByRecord('internship', intern.id!)!.finding }}</span>
|
||||
</span>
|
||||
<div class="mlauto dflex-end">
|
||||
@@ -368,7 +406,7 @@
|
||||
</div>
|
||||
|
||||
<!-- 项目经历 -->
|
||||
<div class="resume-detail__card">
|
||||
<div ref="resumeSectionProjectRef" class="resume-detail__card">
|
||||
<div class="resume-detail__section-header">
|
||||
<h3 class="resume-detail__section-title resume-detail__section-title--underline">项目经历</h3>
|
||||
<button class="resume-detail__edit-btn-inline" @click="startAddExp('project')" aria-label="添加项目经历">
|
||||
@@ -383,7 +421,7 @@
|
||||
<span v-if="proj.role" class="resume-detail__exp-role">{{ proj.role }}</span>
|
||||
<span v-if="hasDiagnosis && getIssueByRecord('project', proj.id!) && getIssueByRecord('project', proj.id!)!.status === 0" class="resume-detail__fix-tag">
|
||||
<svg viewBox="0 0 16 16" class="resume-detail__fix-tag-icon"><circle cx="8" cy="8" r="7" fill="#FF8D0B"/><text x="8" y="12" text-anchor="middle" font-size="11" fill="#fff" font-weight="bold">!</text></svg>
|
||||
建议使用AI润色修复 >>
|
||||
建议使用AI润色修复
|
||||
<span class="resume-detail__fix-tooltip">{{ getIssueByRecord('project', proj.id!)!.finding }}</span>
|
||||
</span>
|
||||
<div class="mlauto dflex-end">
|
||||
@@ -406,7 +444,7 @@
|
||||
</div>
|
||||
|
||||
<!-- 竞赛经历 -->
|
||||
<div class="resume-detail__card">
|
||||
<div ref="resumeSectionCompetitionRef" class="resume-detail__card">
|
||||
<div class="resume-detail__section-header">
|
||||
<h3 class="resume-detail__section-title resume-detail__section-title--underline">竞赛经历</h3>
|
||||
<button class="resume-detail__edit-btn-inline" @click="startAddExp('competition')" aria-label="添加竞赛经历">
|
||||
@@ -421,7 +459,7 @@
|
||||
<span v-if="comp.award" class="resume-detail__exp-role">{{ comp.award }}</span>
|
||||
<span v-if="hasDiagnosis && getIssueByRecord('competition', comp.id!) && getIssueByRecord('competition', comp.id!)!.status === 0" class="resume-detail__fix-tag">
|
||||
<svg viewBox="0 0 16 16" class="resume-detail__fix-tag-icon"><circle cx="8" cy="8" r="7" fill="#FF8D0B"/><text x="8" y="12" text-anchor="middle" font-size="11" fill="#fff" font-weight="bold">!</text></svg>
|
||||
建议使用AI润色修复 >>
|
||||
建议使用AI润色修复
|
||||
<span class="resume-detail__fix-tooltip">{{ getIssueByRecord('competition', comp.id!)!.finding }}</span>
|
||||
</span>
|
||||
<div class="mlauto dflex-end">
|
||||
@@ -444,7 +482,7 @@
|
||||
</div>
|
||||
|
||||
<!-- 技能 -->
|
||||
<div class="resume-detail__card" :class="{ 'resume-detail__card-hover': editModule !== 'skills' || !showEditDrawer }">
|
||||
<div ref="resumeSectionSkillsRef" class="resume-detail__card" :class="{ 'resume-detail__card-hover': editModule !== 'skills' || !showEditDrawer }">
|
||||
<template v-if="editModule !== 'skills' || !showEditDrawer">
|
||||
<div class="resume-detail__section-header">
|
||||
<h3 class="resume-detail__section-title">技能</h3>
|
||||
@@ -473,7 +511,7 @@
|
||||
</div>
|
||||
|
||||
<!-- 证书 -->
|
||||
<div class="resume-detail__card" :class="{ 'resume-detail__card-hover': editModule !== 'certificate' || !showEditDrawer }">
|
||||
<div ref="resumeSectionCertificateRef" class="resume-detail__card" :class="{ 'resume-detail__card-hover': editModule !== 'certificate' || !showEditDrawer }">
|
||||
<template v-if="editModule !== 'certificate' || !showEditDrawer">
|
||||
<div class="resume-detail__section-header">
|
||||
<h3 class="resume-detail__section-title">证书</h3>
|
||||
@@ -500,6 +538,7 @@
|
||||
@cancel="showEditDrawer = false"
|
||||
/>
|
||||
</div>
|
||||
</div><!-- 可滚动内容区域结束 -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -509,6 +548,17 @@
|
||||
<MemberDialog v-model="showMemberDialog" />
|
||||
<!-- 全屏加载遮罩 -->
|
||||
<FullscreenLoading :visible="fullLoading" :text="fullLoadingText" title="Nova 正在帮您努力处理~" />
|
||||
<!-- 简历诊断步骤进度遮罩 -->
|
||||
<StepProgressOverlay
|
||||
v-if="showDiagnoseProgress"
|
||||
:title="diagnoseProgressTitle"
|
||||
:subtitle="diagnoseProgressSubtitle"
|
||||
:steps="diagnoseProgressSteps"
|
||||
:durations="diagnoseProgressDurations"
|
||||
:done="diagnoseProgressDone"
|
||||
@finished="handleDiagnoseProgressFinished"
|
||||
@close="handleDiagnoseProgressClose"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -568,10 +618,67 @@ import {
|
||||
import {ArrowLeft, EditPen, Plus, DeleteFilled, ChatLineRound} from "@element-plus/icons-vue";
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import FullscreenLoading from '@/components/FullscreenLoading.vue'
|
||||
import StepProgressOverlay from '@/components/StepProgressOverlay.vue'
|
||||
|
||||
// ==================== Store 实例 ====================
|
||||
const store = useStore()
|
||||
|
||||
// ==================== Tab 导航栏相关 ====================
|
||||
|
||||
/** 简历详情 Tab 导航列表 */
|
||||
const resumeTabList = [
|
||||
{ key: 'info', label: '个人信息' },
|
||||
{ key: 'summary', label: '个人概述' },
|
||||
{ key: 'education', label: '教育背景' },
|
||||
{ key: 'work', label: '工作经历' },
|
||||
{ key: 'internship', label: '实习经历' },
|
||||
{ key: 'project', label: '项目经历' },
|
||||
{ key: 'competition', label: '竞赛经历' },
|
||||
{ key: 'skills', label: '技能' },
|
||||
{ key: 'certificate', label: '证书' },
|
||||
]
|
||||
|
||||
/** 当前激活的 Tab */
|
||||
const resumeActiveTab = ref('info')
|
||||
|
||||
/** 可滚动区域的 DOM 引用 */
|
||||
const resumeScrollRef = ref<HTMLElement | null>(null)
|
||||
|
||||
/** 各模块的 DOM 引用 */
|
||||
const resumeSectionInfoRef = ref<HTMLElement | null>(null)
|
||||
const resumeSectionSummaryRef = ref<HTMLElement | null>(null)
|
||||
const resumeSectionEducationRef = ref<HTMLElement | null>(null)
|
||||
const resumeSectionWorkRef = ref<HTMLElement | null>(null)
|
||||
const resumeSectionInternshipRef = ref<HTMLElement | null>(null)
|
||||
const resumeSectionProjectRef = ref<HTMLElement | null>(null)
|
||||
const resumeSectionCompetitionRef = ref<HTMLElement | null>(null)
|
||||
const resumeSectionSkillsRef = ref<HTMLElement | null>(null)
|
||||
const resumeSectionCertificateRef = ref<HTMLElement | null>(null)
|
||||
|
||||
/** 模块 key 与 ref 的映射 */
|
||||
const resumeSectionRefMap: Record<string, typeof resumeSectionInfoRef> = {
|
||||
info: resumeSectionInfoRef,
|
||||
summary: resumeSectionSummaryRef,
|
||||
education: resumeSectionEducationRef,
|
||||
work: resumeSectionWorkRef,
|
||||
internship: resumeSectionInternshipRef,
|
||||
project: resumeSectionProjectRef,
|
||||
competition: resumeSectionCompetitionRef,
|
||||
skills: resumeSectionSkillsRef,
|
||||
certificate: resumeSectionCertificateRef,
|
||||
}
|
||||
|
||||
/** 点击 Tab 滚动到对应模块 */
|
||||
function scrollToResumeSection(key: string) {
|
||||
resumeActiveTab.value = key
|
||||
const targetRef = resumeSectionRefMap[key]
|
||||
const container = resumeScrollRef.value
|
||||
if (targetRef?.value && container) {
|
||||
const offsetTop = targetRef.value.offsetTop - container.offsetTop
|
||||
container.scrollTo({ top: offsetTop, behavior: 'smooth' })
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 全屏加载状态 ====================
|
||||
/** 全屏加载是否显示 */
|
||||
const fullLoading = ref(false)
|
||||
@@ -951,25 +1058,52 @@ async function handleDiagnose() {
|
||||
// 接口异常时不阻断(可能是网络问题),继续执行诊断
|
||||
}
|
||||
|
||||
// 全屏加载提示,AI 接口响应较慢
|
||||
fullLoadingText.value = '简历诊断中,请耐心等待…'
|
||||
fullLoading.value = true
|
||||
// 显示步骤进度遮罩
|
||||
showDiagnoseProgress.value = true
|
||||
diagnoseProgressDone.value = false
|
||||
try {
|
||||
const res = await triggerResumeDiagnosis(resumeId)
|
||||
if (res.code === 0) {
|
||||
// 诊断完成,重新拉取诊断报告数据
|
||||
await loadDiagnosis()
|
||||
ElMessage.success('诊断完成')
|
||||
// 通知进度组件接口已完成
|
||||
diagnoseProgressDone.value = true
|
||||
} else {
|
||||
showDiagnoseProgress.value = false
|
||||
ElMessage.error(res.msg || '诊断失败,请稍后重试')
|
||||
}
|
||||
} catch {
|
||||
showDiagnoseProgress.value = false
|
||||
ElMessage.error('诊断请求失败,请稍后重试')
|
||||
} finally {
|
||||
fullLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 简历诊断步骤进度遮罩 ====================
|
||||
|
||||
/** 诊断步骤进度遮罩是否显示 */
|
||||
const showDiagnoseProgress = ref(false)
|
||||
/** 诊断接口是否已完成 */
|
||||
const diagnoseProgressDone = ref(false)
|
||||
/** 诊断大标题 */
|
||||
const diagnoseProgressTitle = '简历诊断中,请耐心等待'
|
||||
/** 诊断副标题 */
|
||||
const diagnoseProgressSubtitle = '根据STAR法则,快速校验你的简历缺失部分并且评分'
|
||||
/** 诊断4个步骤名称 */
|
||||
const diagnoseProgressSteps = ['教育背景解析...', '工作经历解析...', '技能特长解析...', '评分结果最终确认...']
|
||||
/** 每步时长6000毫秒 */
|
||||
const diagnoseProgressDurations = [6000, 6000, 6000, 6000]
|
||||
|
||||
/** 诊断进度走完后提示成功 */
|
||||
function handleDiagnoseProgressFinished() {
|
||||
showDiagnoseProgress.value = false
|
||||
ElMessage.success('诊断完成')
|
||||
}
|
||||
|
||||
/** 关闭诊断进度遮罩 */
|
||||
function handleDiagnoseProgressClose() {
|
||||
showDiagnoseProgress.value = false
|
||||
}
|
||||
|
||||
/** 修复指定模块的问题 */
|
||||
function handleFixIssue(moduleType: string, recordId?: string) {
|
||||
fixModuleType.value = moduleType
|
||||
|
||||
Reference in New Issue
Block a user