Compare commits

3 Commits
Author SHA1 Message Date
xuxin ed959e6e6d 一键修复样式微调 2026-07-08 23:20:22 +08:00
xuxin f0b29d9abb 导师列表导师详情右侧广告,去投递弹窗等 2026-07-08 22:48:58 +08:00
xuxin c94a96d14e 个人资料,简历 2026-07-08 18:58:27 +08:00
31 changed files with 2735 additions and 409 deletions
+2
View File
@@ -52,6 +52,7 @@ declare module 'vue' {
LoginDialog: typeof import('./src/components/LoginDialog.vue')['default']
MemberAccessDialog: typeof import('./src/components/MemberAccessDialog.vue')['default']
MemberDialog: typeof import('./src/components/MemberDialog.vue')['default']
MentorAsidePanel: typeof import('./src/components/MentorAsidePanel.vue')['default']
ProfileEditDrawer: typeof import('./src/components/ProfileEditDrawer.vue')['default']
ProfilePageContent: typeof import('./src/components/ProfilePageContent.vue')['default']
ProfileWelcomeDialog: typeof import('./src/components/ProfileWelcomeDialog.vue')['default']
@@ -69,6 +70,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']
+24
View File
@@ -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')
}
+49 -1
View File
@@ -152,7 +152,8 @@ export interface MentorSupport {
* 导师详情 — 标签关系
*/
export interface MentorLabelRelation {
labelId: number
labelId: string
labelName: string
alias?: string
}
@@ -202,3 +203,50 @@ export function fetchMentorFullInfo(mentorId: string): Promise<MentorFullInfo> {
export function fetchMentorTalks(teacherId: string): Promise<MentorTalk[]> {
return request.get('/public/mentor/talk', { params: { teacherId } })
}
// ==================== 导师模拟对话接口 ====================
/**
* 模拟对话消息项
*/
export interface MentorMockMessage {
/** 角色:user=用户,mentor=导师 */
role: string
/** 消息内容 */
content: string
}
/**
* 模拟对话响应
*/
export interface MentorMockChatResult {
/** 对话消息列表 */
messages: MentorMockMessage[]
}
/**
* 获取导师模拟对话
* GET /public/mentor/mockChat?mentorId=xxx
* 注意:mentorId 使用字符串传递,避免大数精度丢失
*/
export function fetchMentorMockChat(mentorId: string): Promise<MentorMockChatResult> {
return request.get('/public/mentor/mockChat', { params: { mentorId } })
}
// ==================== 导师小程序二维码接口 ====================
/**
* 获取导师小程序二维码(base64 图片)
* POST /public/mentor/getQrCode
* @param teacherId 导师ID(字符串,避免精度丢失)
* @returns base64 格式的图片数据
*/
export function fetchMentorQrCode(teacherId: string): Promise<any> {
return request.post('/public/mentor/getQrCode', {
key: {
path: 'pages/teacher/teacher',
teacherId,
loginAppid: 'wx3adfa65f9d8db17d',
},
})
}
+13 -2
View File
@@ -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')
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

+27
View File
@@ -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;}
@@ -0,0 +1,485 @@
@use '../variables' as *;
// ==================== 导师页右侧广告面板样式 ====================
.mentor-aside-panel {
font-size: 0.14rem;
width: 100%;
height: 100%;
position: relative;
background: $bg-white;
display: flex;
flex-direction: column;
// 顶部渐变区域
&__header {
padding: 0.24rem;
background: linear-gradient(180deg, #EDF9FA 0%, #FFFFFF 100%);
}
&__title {
font-weight: 630;
font-size: 0.20rem;
line-height: 0.27rem;
color: $text-dark;
}
&__subtitle {
font-weight: 330;
font-size: 0.14rem;
line-height: 0.19rem;
color: $text-middle;
margin-top: 0.04rem;
}
// 统计数字区域
&__stats {
display: flex;
align-items: center;
margin: 0 0.24rem;
height: 1rem;
background: linear-gradient(270deg, rgba(82, 202, 209, 0.05) 0%, rgba(159, 208, 81, 0.05) 100%);
border-radius: 0.12rem;
position: relative;
// 渐变边框:从上到下 #9FD051 → #52CAD1
&::before {
content: '';
position: absolute;
inset: 0;
border-radius: 0.12rem;
padding: 0.012rem;
background: linear-gradient(180deg, #9FD051 0%, #52CAD1 100%);
-webkit-mask:
linear-gradient(#fff 0 0) content-box,
linear-gradient(#fff 0 0);
-webkit-mask-composite: xor;
mask-composite: exclude;
pointer-events: none;
}
}
&__stat-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 0.08rem;
padding: 0.20rem 0;
}
&__stat-num {
font-weight: 520;
font-size: 0.24rem;
line-height: 0.32rem;
background: linear-gradient(128.17deg, #52CAD1 0%, #82D79B 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
&__stat-label {
font-weight: 450;
font-size: 0.14rem;
line-height: 0.19rem;
color: $text-dark;
}
&__stat-divider {
width: 0;
height: 0.98rem;
border-left: 1px solid $border-color;
}
// 小节标题(共用)
&__section-title {
display: flex;
align-items: center;
gap: 0.08rem;
}
&__section-icon {
font-size: 0.16rem;
background: linear-gradient(128.17deg, #52CAD1 0%, #82D79B 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
&__section-text {
font-weight: 520;
font-size: 0.16rem;
line-height: 0.21rem;
background: linear-gradient(128.17deg, #52CAD1 0%, #82D79B 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
// 为什么选择我们的导师
&__reasons {
margin: 0.24rem 0.24rem 0;
}
&__reason-list {
margin-top: 0.24rem;
}
&__reason-item {
display: flex;
align-items: center;
gap: 0.08rem;
margin-bottom: 0.20rem;
&:last-child {
margin-bottom: 0;
}
}
&__reason-icon {
width: 0.42rem;
height: 0.42rem;
background: #EDF9FA;
border-radius: 0.08rem;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
svg {
width: 0.24rem;
height: 0.24rem;
}
}
&__reason-info {
display: flex;
flex-direction: column;
gap: 0.04rem;
}
&__reason-title {
font-weight: 520;
font-size: 0.14rem;
line-height: 0.19rem;
color: $text-dark;
}
&__reason-desc {
font-weight: 330;
font-size: 0.14rem;
line-height: 0.19rem;
color: $text-middle;
}
// 热门导师方向
&__directions {
margin: 0.24rem 0.24rem 0;
}
&__direction-list {
margin-top: 0.24rem;
}
&__direction-card {
box-sizing: border-box;
padding: 0.12rem 0.16rem;
background: $bg-white;
border: 1px solid $border-color;
border-radius: 0.12rem;
margin-bottom: 0.20rem;
&:last-child {
margin-bottom: 0;
}
}
&__direction-name {
font-weight: 520;
font-size: 0.14rem;
line-height: 0.19rem;
color: $text-dark;
margin-bottom: 0.08rem;
}
&__direction-tags {
display: flex;
align-items: center;
gap: 0.08rem;
}
&__direction-tag {
display: inline-flex;
justify-content: center;
align-items: center;
padding: 0.04rem 0.08rem;
background: #EDF9FA;
border-radius: 0.04rem;
font-weight: 330;
font-size: 0.12rem;
line-height: 0.16rem;
color: $accent;
}
// 底部按钮区域
&__footer {
margin-top: auto;
padding: 0.24rem;
text-align: center;
}
&__cta-btn {
width: 100%;
height: 0.40rem;
background: linear-gradient(128.17deg, #52CAD1 0%, #82D79B 100%);
border-radius: 0.12rem;
border: none;
font-weight: 520;
font-size: 0.14rem;
line-height: 0.19rem;
color: $bg-white;
cursor: pointer;
transition: opacity 0.15s;
&:hover {
opacity: 0.9;
}
}
&__footer-tip {
font-weight: 330;
font-size: 0.12rem;
line-height: 0.16rem;
color: $text-middle;
margin-top: 0.08rem;
}
// ==================== 模式二:模拟对话样式 ====================
&__chat {
flex: 1;
overflow-y: auto;
padding: 0.16rem 0.24rem;
&::-webkit-scrollbar {
width: 4px;
}
&::-webkit-scrollbar-thumb {
background: $border-color;
border-radius: 2px;
}
}
// 对话加载中
&__chat-loading {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0.12rem;
padding: 0.4rem 0;
color: $text-middle;
font-size: 0.13rem;
}
&__chat-spinner {
width: 0.24rem;
height: 0.24rem;
border: 2px solid $border-color;
border-top-color: $accent;
border-radius: 50%;
animation: mentorChatSpin 0.7s linear infinite;
}
@keyframes mentorChatSpin {
to { transform: rotate(360deg); }
}
// 对话气泡
&__chat-bubble {
display: flex;
margin-bottom: 0.16rem;
&:last-child {
margin-bottom: 0;
}
// 用户消息 — 右对齐,品牌色背景
&--user {
justify-content: flex-end;
.mentor-aside-panel__chat-text {
background: $accent;
color: #FFFFFF;
border-radius: 0.12rem 0.04rem 0.12rem 0.12rem;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.04);
}
}
// 导师消息 — 左对齐,白色背景
&--mentor {
justify-content: flex-start;
.mentor-aside-panel__chat-text {
background: #FFFFFF;
color: $text-dark;
border-radius: 0.04rem 0.12rem 0.12rem 0.12rem;
box-shadow: 0 2px 4px rgba(82, 202, 209, 0.1);
}
}
}
&__chat-text {
display: inline-block;
max-width: 2.38rem;
padding: 0.08rem 0.12rem;
font-weight: 330;
font-size: 0.14rem;
line-height: 0.24rem;
word-break: break-word;
}
}
// ==================== 二维码弹窗样式 ====================
/* 遮罩层 */
.mentor-qr-overlay {
position: fixed;
inset: 0;
z-index: 9999;
background: rgba(0, 0, 0, 0.4);
display: flex;
align-items: center;
justify-content: center;
}
/* 弹窗卡片 */
.mentor-qr-card {
font-size: 0.14rem;
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: 0px 0px 16px rgba(0, 0, 0, 0.05);
border-radius: 0.12rem;
// 头部:标题 + 关闭按钮
&__header {
display: flex;
justify-content: space-between;
align-items: flex-start;
width: 100%;
}
&__title-wrap {
display: flex;
flex-direction: column;
gap: 0.04rem;
}
&__title {
font-weight: 520;
font-size: 0.18rem;
line-height: 0.24rem;
color: #1A1A1A;
}
&__desc {
font-weight: 330;
font-size: 0.12rem;
line-height: 0.16rem;
color: #6A7282;
}
&__close {
width: 0.24rem;
height: 0.24rem;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
&:hover {
opacity: 0.7;
}
svg {
width: 0.24rem;
height: 0.24rem;
}
}
// 二维码展示区域
&__qr-area {
box-sizing: border-box;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 0.16rem 0;
width: 100%;
min-height: 2.72rem;
background: #F4FBFB;
border: 1px solid #AEE6EA;
border-radius: 0.08rem;
}
&__qr-img {
width: 2.40rem;
height: 2.40rem;
object-fit: contain;
}
&__placeholder {
font-size: 0.14rem;
color: #6A7282;
}
&__loading {
display: flex;
align-items: center;
justify-content: center;
width: 2.40rem;
height: 2.40rem;
}
&__spinner {
width: 0.32rem;
height: 0.32rem;
border: 3px solid #AEE6EA;
border-top-color: #52CAD1;
border-radius: 50%;
animation: mentorQrSpin 0.7s linear infinite;
}
@keyframes mentorQrSpin {
to { transform: rotate(360deg); }
}
// 底部提示
&__footer {
width: 100%;
}
&__footer-text {
box-sizing: border-box;
display: flex;
justify-content: center;
align-items: center;
padding: 0.12rem 0.16rem;
width: 100%;
background: #FFFFFF;
border: 1px solid #F0F0F0;
border-radius: 0.08rem;
font-weight: 330;
font-size: 0.14rem;
line-height: 0.19rem;
color: #6A7282;
}
}
@@ -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;
@@ -16,18 +16,16 @@
&__tabs {
display: flex;
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 +35,9 @@
}
&--active {
color: $text-dark;
color: $accent;
font-weight: 600;
border-bottom-color: $text-dark;
border-bottom-color: $accent;
}
}
@@ -47,7 +45,7 @@
&__body {
flex: 1;
overflow-y: auto;
padding-bottom: 0.2rem;
padding: 0 0.24rem 0.2rem 0.24rem;
// 隐藏滚动条
&::-webkit-scrollbar {
@@ -64,7 +62,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 +299,7 @@
}
&__card-hover:hover {
background: #F7F7F7;
background: #F0FAFA;
}
// 标题行hover显示添加按钮(5大经历)
@@ -319,7 +317,7 @@
border-radius: 0.08rem;
&:hover {
background: #F7F7F7;
background: #F0FAFA;
}
&:not(:last-child) {
@@ -27,7 +27,7 @@
right: 0;
width: 8rem;
height: var(--app-height, 100vh);
background: $bg-main;
background: #fff;
z-index: 2001;
display: flex;
flex-direction: column;
@@ -85,7 +85,7 @@
// ---- 模块标题 ----
&__module-title {
font-size: 0.16rem;
font-size: 0.18rem;
font-weight: 700;
color: $text-dark;
margin: 0.24rem 0 0.12rem 0;
@@ -98,6 +98,9 @@
// ---- 单条经历记录 ----
&__record {
margin-bottom: 0.2rem;
background: $bg-main;
padding: 0.16rem;
border-radius: 0.12rem;
}
&__record-label {
@@ -109,14 +112,34 @@
// ---- 卡片容器 ----
&__card {
background: $bg-white;
background: transparent;
border-radius: 0.1rem;
padding: 0.16rem;
border: 1px solid $border-color;
position: relative;
overflow: hidden;
&--ai {
border: 1px solid rgba($accent, 0.3);
background: rgba($accent, 0.03);
}
/* 顶部渐变背景层,不遮挡内容 */
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border-radius: 0.1rem;
background: linear-gradient(to bottom, #EDF9FA 0, #fff 0.8rem);
z-index: 0;
}
/* 让卡片内所有内容在渐变之上 */
> * {
position: relative;
z-index: 1;
}
}
@@ -152,6 +175,8 @@
font-weight: 600;
color: $accent;
margin: 0 0 0.08rem 0;
display: flex;
align-items: flex-start;
}
&__ai-text {
@@ -175,17 +200,19 @@
font-size: 0.12rem;
font-weight: 500;
border: 1px solid $accent;
border-radius: 0.14rem;
padding: 0.04rem 0.12rem;
border-radius: 0.04rem;
padding: 0.04rem 0.06rem;
cursor: pointer;
transition: all 0.2s;
display: flex;
align-items: center;
gap: 0.04rem;
&--replace {
color: $accent;
background: $bg-white;
color: #fff;
background: $accent;
&:hover {
background: rgba($accent, 0.08);
}
}
+8 -1
View File
@@ -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;
}
+2
View File
@@ -42,6 +42,8 @@
@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 './components/mentor-aside-panel.scss';
// 全局样式(优先级最高)
@use './auto.scss';
+173 -35
View File
@@ -798,10 +798,10 @@
to { transform: rotate(360deg); }
}
// ==================== AI助手投递提醒弹窗 ====================
// ==================== 选择投递方式弹窗 ====================
// 遮罩层
&__agent-remind-overlay {
&__apply-method-overlay {
position: fixed;
top: 0;
left: 0;
@@ -815,54 +815,192 @@
}
// 弹窗主体
&__agent-remind-dialog {
&__apply-method-dialog {
position: relative;
width: 4.8rem;
background: $bg-white;
border: 1px solid #F0F0F0;
box-shadow: 0 0 0.16rem rgba(0, 0, 0, 0.05);
border-radius: 0.12rem;
padding: 0.32rem 0.36rem 0.24rem;
min-width: 3.2rem;
text-align: center;
padding: 0.28rem 0.32rem 0.32rem;
box-sizing: border-box;
overflow: hidden;
}
// 提示文字
&__agent-remind-text {
font-size: 0.15rem;
// 渐变背景层
&__apply-method-bg {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(180deg, #EDF9FA 0%, #FFFFFF 20.63%);
border-radius: 0.12rem;
pointer-events: none;
}
// 标题
&__apply-method-title {
position: relative;
font-size: 0.18rem;
font-weight: 520;
color: $text-dark;
font-weight: 500;
margin-bottom: 0.28rem;
line-height: 1.5;
text-align: center;
margin: 0 0 0.2rem 0;
line-height: 0.24rem;
}
// 按钮区域
&__agent-remind-actions {
display: flex;
gap: 0.16rem;
}
// 按钮通用
&__agent-remind-btn {
flex: 1;
padding: 0.1rem 0;
border-radius: 0.08rem;
font-size: 0.14rem;
font-weight: 500;
// 关闭按钮
&__apply-method-close {
position: absolute;
top: 0.24rem;
right: 0.24rem;
width: 0.24rem;
height: 0.24rem;
cursor: pointer;
border: none;
transition: opacity 0.2s;
display: flex;
align-items: center;
justify-content: center;
z-index: 1;
&:hover {
opacity: 0.85;
opacity: 0.7;
}
}
// 选项卡容器
&__apply-method-options {
position: relative;
display: flex;
gap: 0.16rem;
margin-bottom: 0.24rem;
}
// 选项卡
&__apply-method-card {
position: relative;
flex: 1;
height: 1.06rem;
background: $bg-white;
border: 1px solid #F0F0F0;
border-radius: 0.08rem;
padding: 0.2rem 0;
box-sizing: border-box;
cursor: pointer;
text-align: center;
transition: border-color 0.2s, background 0.2s;
overflow: hidden;
&:hover {
border-color: #D0D0D0;
}
// 灰色按钮 — 直接投
&--secondary {
background: $bg-main;
color: $text-middle;
// 选中态
&--active {
border-color: $accent;
background: #EDF9FA;
}
}
// 选项卡标题
&__apply-method-card-title {
font-size: 0.14rem;
font-weight: 450;
color: #000;
margin: 0;
line-height: 0.19rem;
// 品牌色标题
&--accent {
color: $accent;
}
}
// 选项卡描述文字
&__apply-method-card-desc {
font-size: 0.12rem;
font-weight: 330;
color: $text-light;
line-height: 0.2rem;
margin: 0.06rem 0 0 0;
white-space: pre-line;
}
// 选项卡头部(icon + 标题)
&__apply-method-card-header {
display: flex;
align-items: center;
justify-content: center;
gap: 0.04rem;
}
// 左下角圆形装饰
&__apply-method-card-circle {
position: absolute;
width: 0.46rem;
height: 0.46rem;
left: -0.05rem;
bottom: -0.08rem;
background: #AEE6EA;
border-radius: 50%;
opacity: 1;
transition: opacity 0.2s;
.jobs-page__apply-method-card--active & {
opacity: 1;
}
// 主题色按钮 — 去AI助手
&--primary {
background: $btn-dark;
// 圆形上的两个白色小圆点(眼睛)
&::before,
&::after {
content: '';
position: absolute;
width: 0.04rem;
height: 0.09rem;
background: $bg-white;
border-radius: 0.02rem;
top: 40%;
}
&::before {
left: 42%;
}
&::after {
left: 60%;
}
}
// 底部操作按钮
&__apply-method-action {
position: relative;
width: 100%;
height: 0.35rem;
border: none;
border-radius: 0.08rem;
font-size: 0.14rem;
font-weight: 450;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: background 0.2s, color 0.2s;
// 未选择状态 — 浅色背景
&--disabled {
background: #EDF9FA;
color: #AEE6EA;
cursor: not-allowed;
}
// 已选择状态 — 品牌色背景
&--active {
background: $accent;
color: $bg-white;
&:hover {
background: $accent-hover;
}
}
}
}
+55 -4
View File
@@ -15,9 +15,26 @@
height: 100%;
overflow: hidden;
margin-left: 2rem;
padding: 0.24rem 0.4rem 0;
display: flex;
}
// 左侧主内容区
&__main {
flex: 1;
height: 100%;
overflow: hidden;
display: flex;
flex-direction: column;
min-width: 0;
}
// 右侧广告区域
&__aside {
width: 3.6rem;
flex-shrink: 0;
height: 100%;
background: #fff;
overflow-y: auto;
}
// 页面标题
@@ -25,8 +42,9 @@
font-size: 0.18rem;
font-weight: 600;
color: $text-dark;
margin-bottom: 0.12rem;
flex-shrink: 0;
padding: 0.16rem 0.24rem;
background: #fff;
}
// 返回按钮
@@ -37,7 +55,7 @@
font-size: 0.13rem;
color: $text-middle;
cursor: pointer;
margin-bottom: 0.16rem;
margin: 0.16rem 0.24rem;
flex-shrink: 0;
&:hover {
@@ -55,6 +73,7 @@
flex: 1;
overflow-y: auto;
padding-bottom: 0.24rem;
margin:0 0.24rem 0.16rem 0.24rem;
&::-webkit-scrollbar {
width: 4px;
@@ -77,7 +96,7 @@
&__info-card {
display: flex;
align-items: center;
align-items: stretch;
justify-content: space-between;
gap: 0.2rem;
}
@@ -153,6 +172,38 @@
}
}
// 右侧咨询区域容器
&__consult-wrap {
display: flex;
flex-direction: column;
align-items: flex-end;
justify-content: space-between;
flex-shrink: 0;
}
// 咨询价格
&__price {
display: flex;
align-items: baseline;
gap: 0.04rem;
}
&__price-label {
font-size: 0.13rem;
color: $text-middle;
}
&__price-value {
font-size: 0.22rem;
font-weight: 700;
color: #f5a623;
}
&__price-suffix {
font-size: 0.13rem;
color: $text-middle;
}
// 立即咨询按钮
&__consult-btn {
width: 0.92rem;
+27 -3
View File
@@ -249,12 +249,13 @@
font-weight: 600;
color: $text-dark;
margin-bottom: 0.04rem;
white-space: nowrap;
}
&__card-meta {
font-size: 0.12rem;
color: $text-middle;
margin-bottom: 0.08rem;
margin-bottom: 0.06rem;
}
&__card-sep {
@@ -276,6 +277,30 @@
padding: 0.02rem 0.08rem;
}
// 咨询价格
&__card-price {
display: flex;
align-items: baseline;
gap: 0.04rem;
margin-top: 0.06rem;
}
&__card-price-label {
font-size: 0.12rem;
color: $text-middle;
}
&__card-price-value {
font-size: 0.18rem;
font-weight: 700;
color: #f5a623;
}
&__card-price-suffix {
font-size: 0.12rem;
color: $text-middle;
}
// 卡片右侧 — flex:1 让话题区自适应剩余空间,窄屏不会撑出滚动条
&__card-right {
flex: 1 1 0;
@@ -369,11 +394,10 @@
// 右侧广告区域(固定 4rem
&__aside {
width: 3rem;
width: 3.6rem;
flex-shrink: 0;
height: 100%;
background: $bg-white;
border-left: 1px solid $border-color;
overflow-y: auto;
}
}
+80 -35
View File
@@ -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;
}
+55 -11
View File
@@ -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;
}
// 删除按钮红色
+58 -12
View File
@@ -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
}
}
+7 -26
View File
@@ -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) {
// 302
const dailyPrice = (priceYuan / 30).toFixed(2)
dailyDesc = `低至 ¥${dailyPrice} /天`
} else if (product.durationDays <= 93) {
// 902
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,
}
+320
View File
@@ -0,0 +1,320 @@
<template>
<!-- 导师页右侧广告面板模式一列表页/ 模式二详情页 -->
<div class="mentor-aside-panel">
<!-- ==================== 模式一导师列表页展示 ==================== -->
<template v-if="mode === 'list'">
<!-- 顶部渐变区域标题 + 副标题 -->
<div class="mentor-aside-panel__header">
<div class="mentor-aside-panel__title">和真实职场导师聊聊</div>
<div class="mentor-aside-panel__subtitle">让职业选择更清晰</div>
</div>
<!-- 统计数字区域 -->
<div class="mentor-aside-panel__stats">
<div class="mentor-aside-panel__stat-item">
<span class="mentor-aside-panel__stat-num">3000+</span>
<span class="mentor-aside-panel__stat-label">名企导师</span>
</div>
<div class="mentor-aside-panel__stat-divider"></div>
<div class="mentor-aside-panel__stat-item">
<span class="mentor-aside-panel__stat-num">5000+</span>
<span class="mentor-aside-panel__stat-label">服务学员</span>
</div>
</div>
<!-- 为什么选择我们的导师 -->
<div class="mentor-aside-panel__reasons">
<div class="mentor-aside-panel__section-title">
<span class="mentor-aside-panel__section-icon"></span>
<span class="mentor-aside-panel__section-text">为什么选择我们的导师</span>
</div>
<div class="mentor-aside-panel__reason-list">
<!-- 真实在职导师 -->
<div class="mentor-aside-panel__reason-item">
<div class="mentor-aside-panel__reason-icon">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
<path d="M12 12C14.21 12 16 10.21 16 8C16 5.79 14.21 4 12 4C9.79 4 8 5.79 8 8C8 10.21 9.79 12 12 12ZM12 14C9.33 14 4 15.34 4 18V20H20V18C20 15.34 14.67 14 12 14Z" fill="#52CAD1"/>
</svg>
</div>
<div class="mentor-aside-panel__reason-info">
<div class="mentor-aside-panel__reason-title">真实在职导师</div>
<div class="mentor-aside-panel__reason-desc">信息真实经验更落地</div>
</div>
</div>
<!-- 名企背景严选 -->
<div class="mentor-aside-panel__reason-item">
<div class="mentor-aside-panel__reason-icon">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
<path d="M12 2L4 6V12C4 16.42 7.44 20.74 12 22C16.56 20.74 20 16.42 20 12V6L12 2Z" fill="#52CAD1"/>
</svg>
</div>
<div class="mentor-aside-panel__reason-info">
<div class="mentor-aside-panel__reason-title">名企背景严选</div>
<div class="mentor-aside-panel__reason-desc">覆盖大厂外企央国企等类型</div>
</div>
</div>
<!-- 企业一线视角 -->
<div class="mentor-aside-panel__reason-item">
<div class="mentor-aside-panel__reason-icon">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
<path d="M20 6H16V4C16 2.89 15.11 2 14 2H10C8.89 2 8 2.89 8 4V6H4C2.89 6 2 6.89 2 8V19C2 20.11 2.89 21 4 21H20C21.11 21 22 20.11 22 19V8C22 6.89 21.11 6 20 6ZM10 4H14V6H10V4Z" fill="#52CAD1"/>
</svg>
</div>
<div class="mentor-aside-panel__reason-info">
<div class="mentor-aside-panel__reason-title">企业一线视角</div>
<div class="mentor-aside-panel__reason-desc">不讲空话只讲真实经验</div>
</div>
</div>
<!-- 个性化辅导方案 -->
<div class="mentor-aside-panel__reason-item">
<div class="mentor-aside-panel__reason-icon">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
<path d="M12 3L1 9L5 11.18V17.18L12 21L19 17.18V11.18L21 10.09V17H23V9L12 3ZM18.82 9L12 12.72L5.18 9L12 5.28L18.82 9ZM17 15.99L12 18.72L7 15.99V12.27L12 15L17 12.27V15.99Z" fill="#52CAD1"/>
</svg>
</div>
<div class="mentor-aside-panel__reason-info">
<div class="mentor-aside-panel__reason-title">个性化辅导方案</div>
<div class="mentor-aside-panel__reason-desc">针对不同需求量身定制学习路径</div>
</div>
</div>
</div>
</div>
<!-- 热门导师方向 -->
<div class="mentor-aside-panel__directions">
<div class="mentor-aside-panel__section-title">
<span class="mentor-aside-panel__section-icon"></span>
<span class="mentor-aside-panel__section-text">热门导师方向</span>
</div>
<div class="mentor-aside-panel__direction-list">
<div class="mentor-aside-panel__direction-card">
<div class="mentor-aside-panel__direction-name">互联网产品导师</div>
<div class="mentor-aside-panel__direction-tags">
<span class="mentor-aside-panel__direction-tag">需求分析</span>
<span class="mentor-aside-panel__direction-tag">PRD撰写</span>
<span class="mentor-aside-panel__direction-tag">产品设计</span>
</div>
</div>
<div class="mentor-aside-panel__direction-card">
<div class="mentor-aside-panel__direction-name">金融/财务导师</div>
<div class="mentor-aside-panel__direction-tags">
<span class="mentor-aside-panel__direction-tag">财务分析</span>
<span class="mentor-aside-panel__direction-tag">估值建模</span>
<span class="mentor-aside-panel__direction-tag">行业研究</span>
</div>
</div>
<div class="mentor-aside-panel__direction-card">
<div class="mentor-aside-panel__direction-name">数据分析导师</div>
<div class="mentor-aside-panel__direction-tags">
<span class="mentor-aside-panel__direction-tag">数据收集</span>
<span class="mentor-aside-panel__direction-tag">分析报告</span>
<span class="mentor-aside-panel__direction-tag">指标监测</span>
</div>
</div>
<div class="mentor-aside-panel__direction-card">
<div class="mentor-aside-panel__direction-name">市场营销导师</div>
<div class="mentor-aside-panel__direction-tags">
<span class="mentor-aside-panel__direction-tag">市场调研</span>
<span class="mentor-aside-panel__direction-tag">品牌策略</span>
<span class="mentor-aside-panel__direction-tag">推广活动</span>
</div>
</div>
</div>
</div>
<!-- 底部按钮 + 提示文字 -->
<div class="mentor-aside-panel__footer">
<button class="mentor-aside-panel__cta-btn" @click="openQrDialog">预约导师咨询</button>
<div class="mentor-aside-panel__footer-tip">已有5000+学员获得帮助</div>
</div>
</template>
<!-- ==================== 模式二导师详情页 辅导场景预览 ==================== -->
<template v-if="mode === 'detail'">
<!-- 顶部渐变区域 -->
<div class="mentor-aside-panel__header">
<div class="mentor-aside-panel__title">辅导场景预览</div>
<div class="mentor-aside-panel__subtitle">看看这位导师能如何帮到你</div>
</div>
<!-- 模拟对话区域 -->
<div class="mentor-aside-panel__chat">
<!-- 加载中 -->
<div v-if="chatLoading" class="mentor-aside-panel__chat-loading">
<div class="mentor-aside-panel__chat-spinner"></div>
<span>正在生成对话预览...</span>
</div>
<!-- 对话消息列表 -->
<template v-else>
<div
v-for="(msg, index) in chatMessages"
:key="index"
class="mentor-aside-panel__chat-bubble"
:class="{
'mentor-aside-panel__chat-bubble--user': msg.role === 'user',
'mentor-aside-panel__chat-bubble--mentor': msg.role === 'mentor'
}"
>
<span class="mentor-aside-panel__chat-text">{{ msg.content }}</span>
</div>
</template>
</div>
<!-- 底部按钮 + 提示文字 -->
<div class="mentor-aside-panel__footer">
<button class="mentor-aside-panel__cta-btn" @click="openQrDialog">立即咨询导师</button>
<div class="mentor-aside-panel__footer-tip">对话内容根据导师服务类型与行业背景动态生成</div>
</div>
</template>
<!-- ==================== 二维码弹窗遮罩 + 居中卡片 ==================== -->
<teleport to="body">
<div v-if="showQrDialog" class="mentor-qr-overlay" @click.self="closeQrDialog">
<div class="mentor-qr-card">
<!-- 头部标题 + 关闭按钮 -->
<div class="mentor-qr-card__header">
<div class="mentor-qr-card__title-wrap">
<div class="mentor-qr-card__title">扫码进入小程序预约导师咨询</div>
<div class="mentor-qr-card__desc">查看导师价格过往评价并完成预约</div>
</div>
<!-- 关闭按钮 -->
<div class="mentor-qr-card__close" @click="closeQrDialog">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
<path d="M18 6L6 18M6 6l12 12" stroke="#6A7282" stroke-width="2" stroke-linecap="round"/>
</svg>
</div>
</div>
<!-- 二维码展示区域 -->
<div class="mentor-qr-card__qr-area">
<div v-if="qrLoading" class="mentor-qr-card__loading">
<div class="mentor-qr-card__spinner"></div>
</div>
<img
v-else-if="qrCodeBase64"
:src="qrCodeBase64"
alt="小程序二维码"
class="mentor-qr-card__qr-img"
/>
<div v-else class="mentor-qr-card__placeholder">二维码加载失败</div>
</div>
<!-- 底部提示 -->
<div class="mentor-qr-card__footer">
<div class="mentor-qr-card__footer-text">小程序名称见识星球 | 大学生求职神器</div>
</div>
</div>
</div>
</teleport>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { fetchMentorMockChat, fetchMentorQrCode } from '@/api/mentor'
import type { MentorMockMessage } from '@/api/mentor'
//
import fixedQrCodeImg from '@/assets/images/mentor/jsxqQrCode-mentors-page.png'
// ==================== Props ====================
const props = withDefaults(defineProps<{
/** 显示模式:list=列表页,detail=详情页 */
mode?: 'list' | 'detail'
/** 导师ID(模式二需要,用于请求模拟对话) */
mentorId?: string
}>(), {
mode: 'list',
mentorId: '',
})
// ==================== ====================
/** 对话消息列表 */
const chatMessages = ref<MentorMockMessage[]>([])
/** 对话加载状态 */
const chatLoading = ref(false)
/**
* 加载导师模拟对话
*/
async function loadMockChat() {
if (!props.mentorId) return
chatLoading.value = true
try {
const res: any = await fetchMentorMockChat(props.mentorId)
const data = res?.data ?? res
chatMessages.value = data?.messages ?? []
} catch (e) {
console.error('[模拟对话] 加载失败', e)
} finally {
chatLoading.value = false
}
}
// ==================== ====================
onMounted(() => {
//
if (props.mode === 'detail' && props.mentorId) {
loadMockChat()
}
})
// ==================== ====================
/** 弹窗是否显示 */
const showQrDialog = ref(false)
/** 二维码图片 base64 */
const qrCodeBase64 = ref('')
/** 二维码加载状态 */
const qrLoading = ref(false)
/**
* 打开二维码弹窗
* 模式一展示本地固定二维码图片
* 模式二请求后端生成导师专属二维码
*/
async function openQrDialog() {
showQrDialog.value = true
if (props.mode === 'list') {
// 使
qrLoading.value = false
qrCodeBase64.value = fixedQrCodeImg
return
}
// base64
qrLoading.value = true
qrCodeBase64.value = ''
try {
const res: any = await fetchMentorQrCode(props.mentorId)
// base64
const base64 = res?.data ?? res
if (typeof base64 === 'string' && base64.length > 0) {
// data:image
qrCodeBase64.value = base64.startsWith('data:image')
? base64
: `data:image/png;base64,${base64}`
}
} catch (e) {
console.error('[二维码] 获取失败', e)
} finally {
qrLoading.value = false
}
}
/**
* 关闭弹窗
*/
function closeQrDialog() {
showQrDialog.value = false
}
</script>
+3 -3
View File
@@ -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>
+53 -8
View File
@@ -16,8 +16,15 @@
<!-- 顶部栏 -->
<div class="fix-all-drawer__header">
<button class="fix-all-drawer__close-btn" @click="handleClose" aria-label="收起">
<svg viewBox="0 0 16 16" fill="none" class="fix-all-drawer__close-icon">
<path d="M10 3l-5 5 5 5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_154_1595)">
<path d="M11.9998 13.3998L7.0998 18.2998C6.91647 18.4831 6.68314 18.5748 6.3998 18.5748C6.11647 18.5748 5.88314 18.4831 5.6998 18.2998C5.51647 18.1165 5.4248 17.8831 5.4248 17.5998C5.4248 17.3165 5.51647 17.0831 5.6998 16.8998L10.5998 11.9998L5.6998 7.0998C5.51647 6.91647 5.4248 6.68314 5.4248 6.3998C5.4248 6.11647 5.51647 5.88314 5.6998 5.6998C5.88314 5.51647 6.11647 5.4248 6.3998 5.4248C6.68314 5.4248 6.91647 5.51647 7.0998 5.6998L11.9998 10.5998L16.8998 5.6998C17.0831 5.51647 17.3165 5.4248 17.5998 5.4248C17.8831 5.4248 18.1165 5.51647 18.2998 5.6998C18.4831 5.88314 18.5748 6.11647 18.5748 6.3998C18.5748 6.68314 18.4831 6.91647 18.2998 7.0998L13.3998 11.9998L18.2998 16.8998C18.4831 17.0831 18.5748 17.3165 18.5748 17.5998C18.5748 17.8831 18.4831 18.1165 18.2998 18.2998C18.1165 18.4831 17.8831 18.5748 17.5998 18.5748C17.3165 18.5748 17.0831 18.4831 16.8998 18.2998L11.9998 13.3998Z" fill="#6A7282"/>
</g>
<defs>
<clipPath id="clip0_154_1595">
<rect width="24" height="24" fill="white"/>
</clipPath>
</defs>
</svg>
</button>
<h3 class="fix-all-drawer__title">一键修复</h3>
@@ -51,15 +58,53 @@
<!-- AI 润色结果区 -->
<div class="fix-all-drawer__ai-section">
<p class="fix-all-drawer__ai-label"> AI润色结果</p>
<div class="fix-all-drawer__card fix-all-drawer__card--ai">
<p class="fix-all-drawer__ai-label">
<svg class="pt2" width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_154_1838)">
<path d="M6.0714 3.63216C6.47007 2.4655 8.08207 2.43016 8.55474 3.52616L8.59474 3.63283L9.13274 5.20616C9.25603 5.56699 9.45527 5.89717 9.71702 6.17445C9.97877 6.45172 10.2969 6.66964 10.6501 6.8135L10.7947 6.8675L12.3681 7.40483C13.5347 7.8035 13.5701 9.4155 12.4747 9.88816L12.3681 9.92816L10.7947 10.4662C10.4338 10.5894 10.1035 10.7886 9.82608 11.0503C9.54869 11.3121 9.33067 11.6303 9.18674 11.9835L9.13274 12.1275L8.5954 13.7015C8.19674 14.8682 6.58474 14.9035 6.11274 13.8082L6.0714 13.7015L5.53407 12.1282C5.41086 11.7672 5.21165 11.4369 4.9499 11.1595C4.68814 10.8821 4.36993 10.6641 4.01674 10.5202L3.87274 10.4662L2.2994 9.92883C1.13207 9.53016 1.09674 7.91816 2.19274 7.44616L2.2994 7.40483L3.87274 6.8675C4.23356 6.7442 4.56374 6.54496 4.84102 6.28321C5.11829 6.02146 5.33621 5.70329 5.48007 5.35016L5.53407 5.20616L6.0714 3.63216ZM7.3334 4.06283L6.79607 5.63616C6.60833 6.18637 6.30292 6.68903 5.90109 7.10916C5.49927 7.52929 5.0107 7.85678 4.4694 8.06883L4.30274 8.1295L2.7294 8.66683L4.30274 9.20416C4.85294 9.3919 5.35561 9.69732 5.77573 10.0991C6.19586 10.501 6.52335 10.9895 6.7354 11.5308L6.79607 11.6975L7.3334 13.2708L7.87074 11.6975C8.05848 11.1473 8.36389 10.6446 8.76571 10.2245C9.16753 9.80437 9.65611 9.47688 10.1974 9.26483L10.3641 9.20483L11.9374 8.66683L10.3641 8.1295C9.81387 7.94176 9.3112 7.63634 8.89107 7.23452C8.47094 6.8327 8.14345 6.34413 7.9314 5.80283L7.8714 5.63616L7.3334 4.06283ZM12.6667 1.3335C12.7915 1.3335 12.9137 1.36848 13.0195 1.43448C13.1253 1.50047 13.2105 1.59483 13.2654 1.70683L13.2974 1.78483L13.5307 2.46883L14.2154 2.70216C14.3404 2.74463 14.45 2.82324 14.5302 2.92804C14.6105 3.03284 14.6578 3.15911 14.6663 3.29084C14.6747 3.42258 14.6438 3.55385 14.5775 3.66802C14.5113 3.78219 14.4126 3.87412 14.2941 3.93216L14.2154 3.96416L13.5314 4.1975L13.2981 4.88216C13.2555 5.00712 13.1769 5.11663 13.072 5.19682C12.9672 5.27702 12.8409 5.32429 12.7092 5.33264C12.5774 5.341 12.4462 5.31006 12.3321 5.24375C12.2179 5.17744 12.1261 5.07874 12.0681 4.96016L12.0361 4.88216L11.8027 4.19816L11.1181 3.96483C10.9931 3.92237 10.8835 3.84375 10.8033 3.73895C10.723 3.63415 10.6756 3.50788 10.6672 3.37615C10.6588 3.24441 10.6897 3.11314 10.7559 2.99897C10.8222 2.8848 10.9208 2.79287 11.0394 2.73483L11.1181 2.70283L11.8021 2.4695L12.0354 1.78483C12.0804 1.65311 12.1654 1.53877 12.2786 1.45783C12.3918 1.37689 12.5276 1.33341 12.6667 1.3335Z" fill="#52CAD1"/>
</g>
<defs>
<clipPath id="clip0_154_1838">
<rect width="16" height="16" fill="white"/>
</clipPath>
</defs>
</svg>
<span>AI润色结果</span>
</p>
<p class="fix-all-drawer__ai-text">{{ record.polishText }}</p>
<!-- 操作按钮 -->
<div class="fix-all-drawer__ai-actions">
<button class="fix-all-drawer__action-btn fix-all-drawer__action-btn--replace" @click="handleReplaceToOriginal(record)">
<svg width="19" height="19" viewBox="0 0 19 19" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_154_1847)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.79167 10.3333C4.56737 10.3333 4.37947 10.2573 4.22801 10.1053C4.07653 9.95334 4.00053 9.76544 4 9.54167C3.99948 9.31789 4.07548 9.13 4.22801 8.978C4.38054 8.82601 4.56842 8.75 4.79167 8.75H8.75V4.79167C8.75 4.56737 8.82601 4.37947 8.978 4.22801C9.13 4.07653 9.31789 4.00053 9.54167 4C9.76544 3.99948 9.9536 4.07548 10.1061 4.22801C10.2587 4.38054 10.3344 4.56842 10.3333 4.79167V8.75H14.2917C14.516 8.75 14.7041 8.82601 14.8561 8.978C15.0081 9.13 15.0839 9.31789 15.0833 9.54167C15.0828 9.76544 15.0068 9.9536 14.8553 10.1061C14.7039 10.2587 14.516 10.3344 14.2917 10.3333H10.3333V14.2917C10.3333 14.516 10.2573 14.7041 10.1053 14.8561C9.95334 15.0081 9.76544 15.0839 9.54167 15.0833C9.31789 15.0828 9.13 15.0068 8.978 14.8553C8.82601 14.7039 8.75 14.516 8.75 14.2917V10.3333H4.79167Z" fill="white"/>
</g>
<defs>
<clipPath id="clip0_154_1847">
<rect width="19" height="19" fill="white"/>
</clipPath>
</defs>
</svg>
<span>替换为此内容</span>
</button>
<button class="fix-all-drawer__action-btn fix-all-drawer__action-btn--regenerate" @click="handleRegenerate(record)">
<svg width="19" height="19" viewBox="0 0 19 19" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_154_1852)">
<path d="M9.50033 15.8332C7.73227 15.8332 6.2347 15.2196 5.00762 13.9925C3.78053 12.7655 3.16699 11.2679 3.16699 9.49984C3.16699 7.73178 3.78053 6.23421 5.00762 5.00713C6.2347 3.78005 7.73227 3.16651 9.50033 3.16651C10.4107 3.16651 11.2816 3.3544 12.1128 3.73017C12.9441 4.10595 13.6566 4.64376 14.2503 5.34359V3.95817C14.2503 3.73387 14.3263 3.54598 14.4783 3.39451C14.6303 3.24303 14.8182 3.16703 15.042 3.16651C15.2658 3.16598 15.4539 3.24198 15.6065 3.39451C15.759 3.54703 15.8347 3.73492 15.8337 3.95817V7.91651C15.8337 8.14081 15.7577 8.32897 15.6057 8.48097C15.4537 8.63297 15.2658 8.7087 15.042 8.70817H11.0837C10.8594 8.70817 10.6715 8.63217 10.52 8.48017C10.3685 8.32817 10.2925 8.14028 10.292 7.91651C10.2915 7.69273 10.3675 7.50484 10.52 7.35284C10.6725 7.20084 10.8604 7.12484 11.0837 7.12484H13.617C13.1948 6.38595 12.6176 5.8054 11.8856 5.38317C11.1536 4.96095 10.3585 4.74984 9.50033 4.74984C8.18088 4.74984 7.05935 5.21165 6.13574 6.13526C5.21213 7.05887 4.75033 8.1804 4.75033 9.49984C4.75033 10.8193 5.21213 11.9408 6.13574 12.8644C7.05935 13.788 8.18088 14.2498 9.50033 14.2498C10.3975 14.2498 11.219 14.0224 11.9648 13.5674C12.7105 13.1125 13.2877 12.5021 13.6962 11.7363C13.8017 11.5516 13.9503 11.4231 14.1419 11.3508C14.3335 11.2785 14.5279 11.275 14.7253 11.3405C14.9364 11.4064 15.0882 11.545 15.1805 11.7561C15.2729 11.9672 15.2663 12.1651 15.1607 12.3498C14.6198 13.4054 13.8479 14.2498 12.8451 14.8832C11.8423 15.5165 10.7274 15.8332 9.50033 15.8332Z" fill="#52CAD1"/>
</g>
<defs>
<clipPath id="clip0_154_1852">
<rect width="19" height="19" fill="white"/>
</clipPath>
</defs>
</svg>
<span>重新生成</span>
</button>
</div>
</div>
<!-- 操作按钮 -->
<div class="fix-all-drawer__ai-actions">
<button class="fix-all-drawer__action-btn fix-all-drawer__action-btn--replace" @click="handleReplaceToOriginal(record)">+替换为此内容</button>
<button class="fix-all-drawer__action-btn fix-all-drawer__action-btn--regenerate" @click="handleRegenerate(record)"> 重新生成</button>
</div>
</div>
</div>
</template>
+23 -7
View File
@@ -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() {
+34 -4
View File
@@ -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)
})
+229
View File
@@ -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-99100
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
View File
@@ -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) {
+208 -75
View File
@@ -203,7 +203,7 @@
<!-- 按钮行 -->
<div class="jobs-page__job-action-right">
<button class="jobs-page__job-helper" @click.stop="askAssistant(job)">问Nova</button>
<button @click.stop="handleReport(job.sourceUrl)" class="jobs-page__job-apply-btn" :class="{ 'jobs-page__job-apply-btn--active': job.applied }">去投递</button>
<button @click.stop="handleReport(job)" class="jobs-page__job-apply-btn" :class="{ 'jobs-page__job-apply-btn--active': job.applied }">去投递</button>
</div>
<!-- 不再显示 -->
<span v-if="isRecommendTab" class="jobs-page__hide-btn" @click.stop="openDislikeDialog(job)">不再显示</span>
@@ -270,17 +270,55 @@
<!-- 职位问题反馈弹窗 -->
<JobFeedbackDialog ref="feedbackDialogRef" v-model="showFeedbackDialog" :job-id="feedbackJobId" />
<!-- AI助手投递提醒弹窗 -->
<div v-if="showAgentRemindDialog" class="jobs-page__agent-remind-overlay" @click.self="closeAgentRemind">
<div class="jobs-page__agent-remind-dialog">
<p class="jobs-page__agent-remind-text">去AI助手投递岗位效果更好哦</p>
<div class="jobs-page__agent-remind-actions">
<button class="jobs-page__agent-remind-btn jobs-page__agent-remind-btn--secondary" @click="directApply">直接投</button>
<button class="jobs-page__agent-remind-btn jobs-page__agent-remind-btn--primary" @click="goToAgent">去AI助手</button>
<!-- 选择投递方式弹窗 -->
<div v-if="showAgentRemindDialog" class="jobs-page__apply-method-overlay" @click.self="closeAgentRemind">
<div class="jobs-page__apply-method-dialog">
<!-- 渐变背景层 -->
<div class="jobs-page__apply-method-bg"></div>
<!-- 标题 -->
<p class="jobs-page__apply-method-title">选择投递方式</p>
<!-- 关闭按钮 -->
<span class="jobs-page__apply-method-close" @click="closeAgentRemind">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none"><path d="M18 6L6 18M6 6l12 12" stroke="#6A7282" stroke-width="1.5" stroke-linecap="round"/></svg>
</span>
<!-- 两个选项卡 -->
<div class="jobs-page__apply-method-options">
<!-- 直接前往官网 -->
<div class="jobs-page__apply-method-card" :class="{ 'jobs-page__apply-method-card--active': applyMethodChoice === 'direct' }" @click="applyMethodChoice = 'direct'">
<p class="jobs-page__apply-method-card-title">直接前往官网</p>
<p class="jobs-page__apply-method-card-desc">手动填写网申信息<br>自行记录投递进度</p>
</div>
<!-- 智能投递助手 -->
<div class="jobs-page__apply-method-card" :class="{ 'jobs-page__apply-method-card--active': applyMethodChoice === 'agent' }" @click="applyMethodChoice = 'agent'">
<!-- 左下角圆形装饰 -->
<div class="jobs-page__apply-method-card-circle"></div>
<div class="jobs-page__apply-method-card-header">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M8 1L10 5.5L15 6.5L11.5 10L12.5 15L8 12.5L3.5 15L4.5 10L1 6.5L6 5.5L8 1Z" fill="#52CAD1"/></svg>
<span class="jobs-page__apply-method-card-title jobs-page__apply-method-card-title--accent">智能投递助手</span>
</div>
<p class="jobs-page__apply-method-card-desc">{{ agentCardDesc }}</p>
</div>
</div>
<!-- 底部操作按钮 -->
<button class="jobs-page__apply-method-action" :class="applyMethodActionClass" :disabled="!applyMethodChoice" @click="handleApplyMethodAction">
<svg v-if="applyMethodActionIcon" width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_166_1554)">
<path d="M6.0714 3.63204C6.47007 2.46537 8.08207 2.43004 8.55474 3.52604L8.59474 3.63271L9.13274 5.20604C9.25603 5.56686 9.45527 5.89705 9.71702 6.17432C9.97877 6.4516 10.2969 6.66952 10.6501 6.81337L10.7947 6.86737L12.3681 7.40471C13.5347 7.80337 13.5701 9.41537 12.4747 9.88804L12.3681 9.92804L10.7947 10.466C10.4338 10.5893 10.1035 10.7885 9.82608 11.0502C9.54869 11.312 9.33067 11.6302 9.18674 11.9834L9.13274 12.1274L8.5954 13.7014C8.19674 14.868 6.58474 14.9034 6.11274 13.808L6.0714 13.7014L5.53407 12.128C5.41086 11.7671 5.21165 11.4368 4.9499 11.1594C4.68814 10.882 4.36993 10.664 4.01674 10.52L3.87274 10.466L2.2994 9.92871C1.13207 9.53004 1.09674 7.91804 2.19274 7.44604L2.2994 7.40471L3.87274 6.86737C4.23356 6.74408 4.56374 6.54484 4.84102 6.28309C5.11829 6.02134 5.33621 5.70317 5.48007 5.35004L5.53407 5.20604L6.0714 3.63204ZM7.3334 4.06271L6.79607 5.63604C6.60833 6.18624 6.30292 6.68891 5.90109 7.10904C5.49927 7.52917 5.0107 7.85666 4.4694 8.06871L4.30274 8.12937L2.7294 8.66671L4.30274 9.20404C4.85294 9.39178 5.35561 9.6972 5.77573 10.099C6.19586 10.5008 6.52335 10.9894 6.7354 11.5307L6.79607 11.6974L7.3334 13.2707L7.87074 11.6974C8.05848 11.1472 8.36389 10.6445 8.76571 10.2244C9.16753 9.80425 9.65611 9.47676 10.1974 9.26471L10.3641 9.20471L11.9374 8.66671L10.3641 8.12937C9.81387 7.94163 9.3112 7.63622 8.89107 7.2344C8.47094 6.83258 8.14345 6.34401 7.9314 5.80271L7.8714 5.63604L7.3334 4.06271ZM12.6667 1.33337C12.7915 1.33337 12.9137 1.36836 13.0195 1.43436C13.1253 1.50035 13.2105 1.59471 13.2654 1.70671L13.2974 1.78471L13.5307 2.46871L14.2154 2.70204C14.3404 2.7445 14.45 2.82312 14.5302 2.92792C14.6105 3.03272 14.6578 3.15899 14.6663 3.29072C14.6747 3.42246 14.6438 3.55373 14.5775 3.6679C14.5113 3.78207 14.4126 3.874 14.2941 3.93204L14.2154 3.96404L13.5314 4.19737L13.2981 4.88204C13.2555 5.00699 13.1769 5.11651 13.072 5.1967C12.9672 5.2769 12.8409 5.32417 12.7092 5.33252C12.5774 5.34088 12.4462 5.30994 12.3321 5.24363C12.2179 5.17732 12.1261 5.07862 12.0681 4.96004L12.0361 4.88204L11.8027 4.19804L11.1181 3.96471C10.9931 3.92224 10.8835 3.84363 10.8033 3.73883C10.723 3.63403 10.6756 3.50776 10.6672 3.37603C10.6588 3.24429 10.6897 3.11302 10.7559 2.99885C10.8222 2.88468 10.9208 2.79275 11.0394 2.73471L11.1181 2.70271L11.8021 2.46937L12.0354 1.78471C12.0804 1.65299 12.1654 1.53865 12.2786 1.45771C12.3918 1.37677 12.5276 1.33329 12.6667 1.33337Z" fill="white"/>
</g>
<defs>
<clipPath id="clip0_166_1554">
<rect width="16" height="16" fill="white"/>
</clipPath>
</defs>
</svg>
{{ applyMethodActionText }}
</button>
</div>
</div>
<!-- 会员购买弹窗 -->
<MemberDialog v-model="showMemberDialog" />
</div>
</template>
@@ -294,6 +332,7 @@ import AiChat from '@/components/AiChat.vue'
import JobPageHeader from '@/components/JobPageHeader.vue'
import JobDislikeDialog from '@/components/JobDislikeDialog.vue'
import JobFeedbackDialog from '@/components/JobFeedbackDialog.vue'
import MemberDialog from '@/components/MemberDialog.vue'
import IndustrySelector from '@/components/tools/IndustrySelector.vue'
import JobCategorySelector from '@/components/tools/JobCategorySelector.vue'
import RegionSelector from '@/components/tools/RegionSelector.vue'
@@ -304,6 +343,8 @@ import type { JobListItem, JobListParams, FavoriteListParams, ApplyListParams, A
import { getTimeEvent, setTimeEvent, timestampToLocalDateTime } from '@/utils/time'
import { fetchIndustryTree, fetchJobCategoryTree } from '@/api/common'
import type { IndustryItem, JobCategoryItem } from '@/api/common'
import { fetchAgentConfig, applyJob } from '@/api/agent'
import { ElMessage } from 'element-plus'
// 2.
@@ -668,9 +709,9 @@ function closeDropdownOnClickOutside(e: MouseEvent) {
}
}
/** 跳转到原链接 — 带 AI 助手提醒弹窗频率控制 */
/** 跳转到原链接 — 选择投递方式弹窗 */
/**
* 提醒弹窗频率逻辑说明
* 旧的提醒弹窗频率逻辑已注销保留不删
* 1. 未登录用户直接跳转外部链接不弹窗
* 2. 已登录用户首次点击 ai_agent_remind_first 缓存弹窗提醒记录第一次提醒时间
* 3. 有第一次记录但无第二次记录
@@ -680,89 +721,181 @@ function closeDropdownOnClickOutside(e: MouseEvent) {
* - 距第二次记录 7直接跳转不弹窗
* - 距第二次记录 > 7弹窗提醒更新第二次记录时间为当前时间
*/
function handleReport(url: string) {
//
pendingApplyUrl.value = url
// function handleReportOld(url: string) {
// pendingApplyUrl.value = url
// if (!isAuthenticated.value) { window.open(url, '_blank'); return }
// const userId = store.state.userInfo?.id
// if (!userId) { window.open(url, '_blank'); return }
// const now = Date.now()
// const firstTime = getTimeEvent('ai_agent_remind_first', userId)
// const secondTime = getTimeEvent('ai_agent_remind_second', userId)
// if (!firstTime) {
// setTimeEvent('ai_agent_remind_first', userId, timestampToLocalDateTime(now))
// showAgentRemindDialog.value = true; return
// }
// if (!secondTime) {
// const firstTimestamp = new Date(firstTime).getTime()
// const diffHours = (now - firstTimestamp) / (1000 * 60 * 60)
// if (diffHours <= 1) { window.open(url, '_blank') }
// else { setTimeEvent('ai_agent_remind_second', userId, timestampToLocalDateTime(now)); showAgentRemindDialog.value = true }
// return
// }
// const secondTimestamp = new Date(secondTime).getTime()
// const diffDays = (now - secondTimestamp) / (1000 * 60 * 60 * 24)
// if (diffDays <= 7) { window.open(url, '_blank') }
// else { setTimeEvent('ai_agent_remind_second', userId, timestampToLocalDateTime(now)); showAgentRemindDialog.value = true }
// }
//
if (!isAuthenticated.value) {
window.open(url, '_blank')
return
}
const userId = store.state.userInfo?.id
if (!userId) {
window.open(url, '_blank')
return
}
const now = Date.now()
const firstTime = getTimeEvent('ai_agent_remind_first', userId)
const secondTime = getTimeEvent('ai_agent_remind_second', userId)
if (!firstTime) {
// 1
setTimeEvent('ai_agent_remind_first', userId, timestampToLocalDateTime(now))
showAgentRemindDialog.value = true
return
}
if (!secondTime) {
// 2 1
const firstTimestamp = new Date(firstTime).getTime()
const diffHours = (now - firstTimestamp) / (1000 * 60 * 60)
if (diffHours <= 1) {
// 1
window.open(url, '_blank')
} else {
// 1
setTimeEvent('ai_agent_remind_second', userId, timestampToLocalDateTime(now))
showAgentRemindDialog.value = true
}
return
}
// 3 7
const secondTimestamp = new Date(secondTime).getTime()
const diffDays = (now - secondTimestamp) / (1000 * 60 * 60 * 24)
if (diffDays <= 7) {
// 7
window.open(url, '_blank')
} else {
// 7
setTimeEvent('ai_agent_remind_second', userId, timestampToLocalDateTime(now))
showAgentRemindDialog.value = true
}
/** 点击"去投递"按钮 — 每次都弹出选择投递方式弹窗 */
function handleReport(job: JobListItem) {
//
pendingApplyUrl.value = job.sourceUrl
pendingApplyJobId.value = job.id
//
applyMethodChoice.value = null
//
showAgentRemindDialog.value = true
// AI
loadAgentConfigStatus()
}
// ==================== AI ====================
// ==================== ====================
/** AI助手提醒弹窗是否显示 */
/** 弹窗是否显示 */
const showAgentRemindDialog = ref(false)
/** 暂存待跳转的外部链接 */
const pendingApplyUrl = ref('')
/** 关闭AI助手提醒弹窗 */
/** 暂存当前触发弹窗的岗位 ID */
const pendingApplyJobId = ref('')
/** 用户在弹窗中选择的投递方式:null=未选择, 'direct'=直接前往官网, 'agent'=智能投递助手 */
const applyMethodChoice = ref<'direct' | 'agent' | null>(null)
/** AI助手是否已完成配置(status === 1 */
const agentConfigReady = ref(false)
/** 是否正在加载 AI 助手配置 */
const agentConfigLoading = ref(false)
/** 会员购买弹窗显示状态 */
const showMemberDialog = ref(false)
/** 用户会员状态 — 从 store 读取 */
const memberStatus = computed(() => store.state.memberStatus)
/** 用户是否是会员(正式或试用) */
const isMember = computed(() => !!memberStatus.value?.isMember)
/** 智能投递助手选项卡内的描述文字 — 根据会员和配置状态动态显示 */
const agentCardDesc = computed(() => {
if (!isMember.value) {
return '自动填写网申信息\n统一管理投递进度'
}
if (!agentConfigReady.value) {
return '完成助手设置后\n可使用智能投递'
}
return '将该岗位加入待投递列表\n在投递助手中统一处理'
})
/** 底部操作按钮文字 */
const applyMethodActionText = computed(() => {
if (!applyMethodChoice.value) return '请选择投递方式'
if (applyMethodChoice.value === 'direct') return '去官网投递'
// agent
if (!isMember.value) return '开通会员使用'
if (!agentConfigReady.value) return '去完成设置'
return '加入待投递列表'
})
/** 底部操作按钮是否显示 icon */
const applyMethodActionIcon = computed(() => {
if (!applyMethodChoice.value) return false
if (applyMethodChoice.value === 'direct') return false
// agent icon
return true
})
/** 底部操作按钮样式 class */
const applyMethodActionClass = computed(() => {
if (!applyMethodChoice.value) return 'jobs-page__apply-method-action--disabled'
return 'jobs-page__apply-method-action--active'
})
/** 加载 AI 助手配置状态 */
async function loadAgentConfigStatus() {
if (agentConfigLoading.value) return
agentConfigLoading.value = true
try {
const res = await fetchAgentConfig()
if (res.code === '0' && res.data && res.data.status === 1) {
agentConfigReady.value = true
} else {
agentConfigReady.value = false
}
} catch {
agentConfigReady.value = false
} finally {
agentConfigLoading.value = false
}
}
/** 关闭选择投递方式弹窗 */
function closeAgentRemind() {
showAgentRemindDialog.value = false
pendingApplyUrl.value = ''
pendingApplyJobId.value = ''
}
/** 点击"直接投"按钮 — 关闭弹窗并跳转外部链接 */
function directApply() {
showAgentRemindDialog.value = false
if (pendingApplyUrl.value) {
window.open(pendingApplyUrl.value, '_blank')
/** 底部操作按钮点击处理 */
async function handleApplyMethodAction() {
if (!applyMethodChoice.value) return
if (applyMethodChoice.value === 'direct') {
//
showAgentRemindDialog.value = false
if (pendingApplyUrl.value) {
window.open(pendingApplyUrl.value, '_blank')
}
pendingApplyUrl.value = ''
pendingApplyJobId.value = ''
return
}
pendingApplyUrl.value = ''
}
/** 点击"去AI助手"按钮 — 关闭弹窗并跳转到 Agent 页面 */
function goToAgent() {
//
if (!isMember.value) {
//
showAgentRemindDialog.value = false
showMemberDialog.value = true
pendingApplyUrl.value = ''
pendingApplyJobId.value = ''
return
}
if (!agentConfigReady.value) {
// AI Agent
showAgentRemindDialog.value = false
pendingApplyUrl.value = ''
pendingApplyJobId.value = ''
router.push('/agent')
return
}
//
try {
const res = await applyJob({ jobId: pendingApplyJobId.value, status: -1 })
if (res.code === '0') {
ElMessage.success('已加入待投递列表')
} else {
ElMessage.error(res.msg || '加入待投递列表失败')
}
} catch {
ElMessage.error('加入待投递列表失败')
}
showAgentRemindDialog.value = false
pendingApplyUrl.value = ''
router.push('/agent')
pendingApplyJobId.value = ''
}
onMounted(async () => {
+18 -16
View File
@@ -69,19 +69,20 @@
</div>
<!-- 导师基本信息 -->
<div class="mentor-page__card-info">
<div class="mentor-page__card-name">{{ mentor.platformName }}</div>
<div class="mentor-page__card-meta">
<span v-if="mentor.agencyName" class="mentor-page__card-agency">{{ mentor.agencyName }}</span>
<span v-if="mentor.agencyName && mentor.position" class="mentor-page__card-sep"></span>
<span v-if="mentor.position" class="mentor-page__card-position">{{ mentor.position }}</span>
<div class="dflex-start aliite-e flex-warp">
<div class="mentor-page__card-name mr10">{{ mentor.platformName }}</div>
<div class="mentor-page__card-meta">
<span v-if="mentor.agencyName" class="mentor-page__card-agency">{{ mentor.agencyName }}</span>
<span v-if="mentor.agencyName && mentor.position" class="mentor-page__card-sep"></span>
<span v-if="mentor.position" class="mentor-page__card-position">{{ mentor.position }}</span>
</div>
</div>
<!-- 标签 -->
<div class="mentor-page__card-labels" v-if="mentor.labels && mentor.labels.length">
<span
v-for="(label, li) in mentor.labels.slice(0, 4)"
:key="li"
class="mentor-page__card-label"
>{{ label }}</span>
<!-- 咨询价格 -->
<div class="mentor-page__card-price" v-if="mentor.price">
<span class="mentor-page__card-price-label">咨询价</span>
<span class="mentor-page__card-price-value">¥{{ mentor.price }}</span>
<span class="mentor-page__card-price-suffix"></span>
</div>
</div>
</div>
@@ -101,7 +102,7 @@
class="mentor-page__detail-btn"
@click="goToDetail(mentor)"
><span>查看详情</span> <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.51612 4.07575C7.38279 3.94242 7.31879 3.78686 7.32412 3.60908C7.32945 3.43131 7.39901 3.27575 7.53278 3.14241C7.66612 3.02019 7.82167 2.95619 7.99946 2.95041C8.17723 2.94464 8.33278 3.00864 8.46612 3.14241L12.8661 7.54241C12.9328 7.60908 12.9801 7.6813 13.0081 7.75908C13.0361 7.83686 13.0497 7.92019 13.0488 8.00908C13.0479 8.09797 13.0339 8.18131 13.0068 8.25908C12.9797 8.33686 12.9326 8.40908 12.8654 8.47575L8.46545 12.8757C8.34323 12.998 8.19034 13.0591 8.00679 13.0591C7.82323 13.0591 7.66501 12.998 7.53212 12.8757C7.39879 12.7424 7.33212 12.584 7.33212 12.4004C7.33212 12.2169 7.39879 12.0586 7.53212 11.9257L10.7828 8.67575H3.33279C3.1439 8.67575 2.98546 8.61175 2.85745 8.48375C2.72945 8.35575 2.66567 8.19753 2.66612 8.00908C2.66656 7.82064 2.73056 7.6622 2.85812 7.53375C2.98568 7.40531 3.1439 7.34153 3.33279 7.34242L10.7828 7.34242L7.51612 4.07575Z" fill="#52CAD1"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.51612 4.07575C7.38279 3.94242 7.31879 3.78686 7.32412 3.60908C7.32945 3.43131 7.39901 3.27575 7.53278 3.14241C7.66612 3.02019 7.82167 2.95619 7.99946 2.95041C8.17723 2.94464 8.33278 3.00864 8.46612 3.14241L12.8661 7.54241C12.9328 7.60908 12.9801 7.6813 13.0081 7.75908C13.0361 7.83686 13.0497 7.92019 13.0488 8.00908C13.0479 8.09797 13.0339 8.18131 13.0068 8.25908C12.9797 8.33686 12.9326 8.40908 12.8654 8.47575L8.46545 12.8757C8.34323 12.998 8.19034 13.0591 8.00679 13.0591C7.82323 13.0591 7.66501 12.998 7.53212 12.8757C7.39879 12.7424 7.33212 12.584 7.33212 12.4004C7.33212 12.2169 7.39879 12.0586 7.53212 11.9257L10.7828 8.67575H3.33279C3.1439 8.67575 2.98546 8.61175 2.85745 8.48375C2.72945 8.35575 2.66567 8.19753 2.66612 8.00908C2.66656 7.82064 2.73056 7.6622 2.85812 7.53375C2.98568 7.40531 3.1439 7.34153 3.33279 7.34242L10.7828 7.34242L7.51612 4.07575Z" fill="currentColor"/>
</svg>
</button>
</div>
@@ -125,9 +126,9 @@
</div>
</div>
<!-- 右侧广告区域固定宽度 4rem -->
<!-- 右侧广告区域 -->
<div class="mentor-page__aside">
<!-- 广告内容占位后续填充 -->
<MentorAsidePanel />
</div>
</div>
@@ -139,6 +140,7 @@ import { ref, computed, onMounted, nextTick } from 'vue'
import { useRouter } from 'vue-router'
import { useStore } from 'vuex'
import SideNav from '@/components/SideNav.vue'
import MentorAsidePanel from '@/components/MentorAsidePanel.vue'
import { fetchMentorList } from '@/api/mentor'
import type { MentorItem, MentorListParams } from '@/api/mentor'
import { toOssUrl } from '@/utils/image'
@@ -361,7 +363,7 @@ function goToDetail(mentor: MentorItem) {
pageNum: currentPage.value,
scrollTop: listRef.value?.scrollTop ?? 0,
})
router.push(`/mentor/${mentor.id}`)
router.push({ path: `/mentor/${mentor.id}`, query: { price: String(mentor.price) } })
}
// ==================== ====================
+167 -74
View File
@@ -1,90 +1,142 @@
<template>
<div class="mentor-detail dflex">
<SideNav />
<div class="mentor-detail__body">
<!-- 页面标题 -->
<div class="mentor-detail__page-title">导师详情</div>
<div class="mentor-detail__body dflex">
<!-- 左侧主内容区 -->
<div class="mentor-detail__main">
<!-- 页面标题 -->
<div class="mentor-detail__page-title">导师详情</div>
<!-- 返回按钮 -->
<div class="mentor-detail__back" @click="goBack">
<svg viewBox="0 0 16 16" fill="none" class="mentor-detail__back-icon">
<path d="M10 12L6 8l4-4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<span>返回</span>
</div>
<!-- 返回按钮 -->
<div class="mentor-detail__back" @click="goBack">
<svg viewBox="0 0 16 16" fill="none" class="mentor-detail__back-icon">
<path d="M10 12L6 8l4-4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<span>返回</span>
</div>
<!-- 主内容滚动区域 -->
<div class="mentor-detail__scroll">
<!-- 主内容滚动区域 -->
<div class="mentor-detail__scroll">
<!-- 导师信息卡片 -->
<div class="mentor-detail__card mentor-detail__info-card">
<div class="mentor-detail__info-left">
<!-- 头像 -->
<div class="mentor-detail__avatar-wrap">
<img
v-if="mentorInfo?.picture"
:src="toOssUrl(mentorInfo.picture)"
class="mentor-detail__avatar"
:alt="mentorInfo.platformName"
/>
<div v-else class="mentor-detail__avatar-placeholder">
{{ mentorInfo?.platformName?.charAt(0) || '师' }}
</div>
</div>
<!-- 基本信息 -->
<div class="mentor-detail__info-text">
<div class="mentor-detail__name">
<span>{{ mentorInfo?.platformName }}</span>
<div class="mentor-detail__meta">
<span v-if="agencyName">{{ agencyName }}</span>
<span v-if="agencyName && position"> | </span>
<span v-if="position">{{ position }}</span>
<!-- 导师信息卡片 -->
<div class="mentor-detail__card mentor-detail__info-card">
<div class="mentor-detail__info-left">
<!-- 头像 -->
<div class="mentor-detail__avatar-wrap">
<img
v-if="mentorInfo?.picture"
:src="toOssUrl(mentorInfo.picture)"
class="mentor-detail__avatar"
:alt="mentorInfo.platformName"
/>
<div v-else class="mentor-detail__avatar-placeholder">
{{ mentorInfo?.platformName?.charAt(0) || '师' }}
</div>
</div>
<!-- 基本信息 -->
<div class="mentor-detail__info-text">
<div class="mentor-detail__name">
<span>{{ mentorInfo?.platformName }}</span>
<div class="mentor-detail__meta">
<span v-if="agencyName">{{ agencyName }}</span>
<span v-if="agencyName && position"> | </span>
<span v-if="position">{{ position }}</span>
</div>
</div>
<div class="mentor-detail__location" v-if="locationText">
所在城市 <span class="">{{ locationText }}</span>
</div>
<div class="mentor-detail__services" v-if="serviceLabels.length">
服务类型 <span>{{ serviceLabels.join('、') }}</span>
<div class="mentor-detail__location" v-if="locationText">
所在城市 <span class="">{{ locationText }}</span>
</div>
<div class="mentor-detail__services" v-if="serviceLabels.length">
服务类型 <span class="ml10">{{ serviceLabels.join('、') }}</span>
</div>
</div>
</div>
<!-- 右侧咨询价 + 立即咨询按钮 -->
<div class="mentor-detail__consult-wrap">
<!-- 咨询价格从列表页传入 -->
<div class="mentor-detail__price" v-if="mentorPrice">
<span class="mentor-detail__price-label">咨询价</span>
<span class="mentor-detail__price-value">¥{{ mentorPrice }}</span>
<span class="mentor-detail__price-suffix"></span>
</div>
<!-- 立即咨询按钮 -->
<button
class="mentor-detail__consult-btn"
@click="openQrDialog"
>立即咨询 </button>
</div>
</div>
<!-- 立即咨询按钮 -->
<a
href="https://www.jianshixingqiu.com"
target="_blank"
rel="noopener noreferrer"
class="mentor-detail__consult-btn"
>立即咨询 </a>
</div>
<!-- 擅长话题卡片 -->
<div class="mentor-detail__card" v-if="talks.length">
<div class="mentor-detail__section-title">擅长话题</div>
<div
v-for="(talk, index) in talks"
:key="talk.id"
class="mentor-detail__talk-item"
>
<div class="mentor-detail__talk-title">{{ index + 1 }}.{{ talk.title }}</div>
<div class="mentor-detail__talk-content" v-if="talk.content">{{ talk.content }}</div>
<!-- 擅长话题卡片 -->
<div class="mentor-detail__card" v-if="talks.length">
<div class="mentor-detail__section-title">擅长话题</div>
<div
v-for="(talk, index) in talks"
:key="talk.id"
class="mentor-detail__talk-item"
>
<div class="mentor-detail__talk-title">{{ index + 1 }}.{{ talk.title }}</div>
<div class="mentor-detail__talk-content" v-if="talk.content">{{ talk.content }}</div>
</div>
</div>
</div>
<!-- 个人介绍卡片 -->
<div class="mentor-detail__card" v-if="mentorInfo?.introduce">
<div class="mentor-detail__section-title">个人介绍</div>
<div class="mentor-detail__introduce">{{ mentorInfo.introduce }}</div>
</div>
<!-- 个人介绍卡片 -->
<div class="mentor-detail__card" v-if="mentorInfo?.introduce">
<div class="mentor-detail__section-title">个人介绍</div>
<div class="mentor-detail__introduce">{{ mentorInfo.introduce }}</div>
</div>
<!-- 加载中 -->
<div v-if="loading" class="mentor-detail__loading">
<div class="mentor-detail__loading-spinner"></div>
<span>加载中...</span>
<!-- 加载中 -->
<div v-if="loading" class="mentor-detail__loading">
<div class="mentor-detail__loading-spinner"></div>
<span>加载中...</span>
</div>
</div>
</div>
<!-- 右侧广告区域 -->
<div class="mentor-detail__aside">
<MentorAsidePanel mode="detail" :mentorId="mentorId" />
</div>
</div>
<!-- ==================== 二维码弹窗遮罩 + 居中卡片 ==================== -->
<teleport to="body">
<div v-if="showQrDialog" class="mentor-qr-overlay" @click.self="closeQrDialog">
<div class="mentor-qr-card">
<!-- 头部标题 + 关闭按钮 -->
<div class="mentor-qr-card__header">
<div class="mentor-qr-card__title-wrap">
<div class="mentor-qr-card__title">扫码进入小程序预约导师咨询</div>
<div class="mentor-qr-card__desc">查看导师价格过往评价并完成预约</div>
</div>
<div class="mentor-qr-card__close" @click="closeQrDialog">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
<path d="M18 6L6 18M6 6l12 12" stroke="#6A7282" stroke-width="2" stroke-linecap="round"/>
</svg>
</div>
</div>
<!-- 二维码展示区域 -->
<div class="mentor-qr-card__qr-area">
<div v-if="qrLoading" class="mentor-qr-card__loading">
<div class="mentor-qr-card__spinner"></div>
</div>
<img
v-else-if="qrCodeBase64"
:src="qrCodeBase64"
alt="小程序二维码"
class="mentor-qr-card__qr-img"
/>
<div v-else class="mentor-qr-card__placeholder">二维码加载失败</div>
</div>
<!-- 底部提示 -->
<div class="mentor-qr-card__footer">
<div class="mentor-qr-card__footer-text">小程序名称见识星球 | 大学生求职神器</div>
</div>
</div>
</div>
</teleport>
</div>
</template>
@@ -92,7 +144,8 @@
import { ref, computed, onMounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import SideNav from '@/components/SideNav.vue'
import { fetchMentorFullInfo, fetchMentorTalks } from '@/api/mentor'
import MentorAsidePanel from '@/components/MentorAsidePanel.vue'
import { fetchMentorFullInfo, fetchMentorTalks, fetchMentorQrCode } from '@/api/mentor'
import type { MentorFullInfo, MentorTalk } from '@/api/mentor'
import { toOssUrl } from '@/utils/image'
@@ -104,6 +157,9 @@ const route = useRoute()
/** 导师 ID,从路由参数获取(字符串,避免大数精度丢失) */
const mentorId = route.params.id as string
/** 咨询价格,从列表页 query 传入 */
const mentorPrice = route.query.price as string || ''
// ==================== ====================
/** 是否正在加载 */
@@ -149,12 +205,11 @@ const locationText = computed(() => {
return parts.join(' · ')
})
/** 服务类型标签 */
/** 服务类型标签 — 取 labelRelations 中每项的 labelName */
const serviceLabels = computed(() => {
const supports = fullInfo.value?.supports
if (!supports || !supports.length) return []
const typeMap: Record<number, string> = { 0: '远程实习', 1: '简历内推', 2: '定向内推辅导' }
return supports.map(s => typeMap[s.type] || '').filter(Boolean)
const labels = fullInfo.value?.labelRelations
if (!labels || !labels.length) return []
return labels.map(l => l.labelName).filter(Boolean)
})
// ==================== ====================
@@ -198,4 +253,42 @@ function goBack() {
onMounted(() => {
loadDetail()
})
// ==================== ====================
/** 弹窗是否显示 */
const showQrDialog = ref(false)
/** 二维码图片 base64 */
const qrCodeBase64 = ref('')
/** 二维码加载状态 */
const qrLoading = ref(false)
/**
* 打开二维码弹窗调接口获取当前导师的小程序二维码
*/
async function openQrDialog() {
showQrDialog.value = true
qrLoading.value = true
qrCodeBase64.value = ''
try {
const res: any = await fetchMentorQrCode(mentorId)
const base64 = res?.data ?? res
if (typeof base64 === 'string' && base64.length > 0) {
qrCodeBase64.value = base64.startsWith('data:image')
? base64
: `data:image/png;base64,${base64}`
}
} catch (e) {
console.error('[二维码] 获取失败', e)
} finally {
qrLoading.value = false
}
}
/** 关闭弹窗 */
function closeQrDialog() {
showQrDialog.value = false
}
</script>
+103 -24
View File
@@ -5,12 +5,21 @@
<!-- 页面标题 + 上传按钮 -->
<div class="resume-page__header">
<h2 class="resume-page__title">我的简历</h2>
<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"/>
</svg>
上传简历
</button>
<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"/>
</svg>
上传简历
</button>
</div>
</div>
<!-- 简历表格 -->
@@ -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}`)
+177 -43
View File
@@ -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">
<button class="resume-detail__score-link" @click="handleViewReport">查看评估报告</button>
<button class="resume-detail__score-link" @click="handleDiagnose">重新诊断</button>
<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>
<span class="resume-detail__score-hint">暂无等级点击开始诊断简历</span>
<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润色修复 &gt;&gt;
建议使用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润色修复 &gt;&gt;
建议使用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润色修复 &gt;&gt;
建议使用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润色修复 &gt;&gt;
建议使用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润色修复 &gt;&gt;
建议使用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