新会员页面
This commit is contained in:
Vendored
+2
@@ -11,10 +11,12 @@ export {}
|
||||
/* prettier-ignore */
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
AgentApplyProgress: typeof import('./src/components/AgentApplyProgress.vue')['default']
|
||||
AgentChatJobList: typeof import('./src/components/AgentChatJobList.vue')['default']
|
||||
AgentMatchJobAdd: typeof import('./src/components/AgentMatchJobAdd.vue')['default']
|
||||
AgentSettingsPanel: typeof import('./src/components/AgentSettingsPanel.vue')['default']
|
||||
AgentSetupWizard: typeof import('./src/components/AgentSetupWizard.vue')['default']
|
||||
AgentTaskListDropdown: typeof import('./src/components/AgentTaskListDropdown.vue')['default']
|
||||
AiChat: typeof import('./src/components/AiChat.vue')['default']
|
||||
AiThinkingIndicator: typeof import('./src/components/tools/AiThinkingIndicator.vue')['default']
|
||||
ElButton: typeof import('element-plus/es')['ElButton']
|
||||
|
||||
+24
-2
@@ -131,8 +131,8 @@ export function fetchAgentRecommend(params: AgentRecommendParams = {}) {
|
||||
|
||||
/** 岗位投递请求参数 */
|
||||
export interface JobApplyParams {
|
||||
/** 岗位 ID */
|
||||
jobId: number
|
||||
/** 岗位 ID(支持字符串避免大整数精度丢失) */
|
||||
jobId: number | string
|
||||
/** 投递状态:-1=待投递 0=已投递 1=面试中 2=有Offer 3=未通过 4=已结束,null=取消 */
|
||||
status: number | null
|
||||
}
|
||||
@@ -278,3 +278,25 @@ export interface AgentChatResponse {
|
||||
export function sendAgentChat(params: AgentChatParams) {
|
||||
return aiService.post<any, { data: AiResult<AgentChatResponse> }>('/job-agent/chat', params).then(res => res.data)
|
||||
}
|
||||
|
||||
// ==================== 求职助手优化简历(Python 后端) ====================
|
||||
|
||||
/** 优化简历请求参数 */
|
||||
export interface OptimizeResumeParams {
|
||||
/** 简历 ID */
|
||||
resumeId: string | number
|
||||
/** 岗位 ID */
|
||||
jobId: string | number
|
||||
}
|
||||
|
||||
/**
|
||||
* 求职助手岗位优化简历
|
||||
* POST /job-agent/optimize-resume
|
||||
*/
|
||||
export function optimizeAgentResume(params: OptimizeResumeParams) {
|
||||
return aiService.post('/job-agent/optimize-resume', params, {
|
||||
headers: {
|
||||
Token: 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOjIwMzUyNTM4OTg5MTk4NzA0NjUsInV1SWQiOiI2MmQ5MDE2NTcyNzY0ZmNjODNjZTIyYjRjODA5ZmU5MiJ9.eE-Q5rio5J5kxkS-XPYdmk-1Tgvg6kj6NGoKWMFNU14',
|
||||
},
|
||||
}).then(res => res.data)
|
||||
}
|
||||
|
||||
@@ -220,6 +220,33 @@ export function fetchApplyList(params: ApplyListParams = {}) {
|
||||
})
|
||||
}
|
||||
|
||||
// ==================== 求职助手任务列表 ====================
|
||||
|
||||
/** 求职助手任务列表请求参数 */
|
||||
export interface AgentTaskListParams {
|
||||
/** 当前页码,从1开始,默认1 */
|
||||
pageNum?: number
|
||||
/** 每页条数,默认10 */
|
||||
pageSize?: number
|
||||
/** 搜索关键字 */
|
||||
keyword?: string
|
||||
/** tab类型 1=进行中(待投递) 2=已完成(已投递及之后状态),默认1 */
|
||||
tab?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取求职助手任务列表
|
||||
* POST /job/agent/task/list
|
||||
*/
|
||||
export function fetchAgentTaskList(params: AgentTaskListParams = {}) {
|
||||
return request.post<any, ApiResult<JobPageData>>('/job/agent/task/list', {
|
||||
pageNum: params.pageNum ?? 1,
|
||||
pageSize: params.pageSize ?? 10,
|
||||
tab: params.tab ?? 1,
|
||||
...(params.keyword ? { keyword: params.keyword } : {}),
|
||||
})
|
||||
}
|
||||
|
||||
// ==================== 投递统计 ====================
|
||||
|
||||
/** 投递统计结果 */
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
/** 站内信消息项 */
|
||||
export interface MessageDto {
|
||||
/** 消息ID */
|
||||
id: number
|
||||
/** 消息类型 1=系统消息 2=运营消息 3=订单消息 */
|
||||
type: number
|
||||
/** 消息标题 */
|
||||
title: string
|
||||
/** 消息内容 */
|
||||
content: string
|
||||
/** 关联业务类型 */
|
||||
bizType: string
|
||||
/** 关联业务ID */
|
||||
bizId: number
|
||||
/** 是否已读 */
|
||||
read: boolean
|
||||
/** 创建时间 */
|
||||
createTime: { seconds: number; nanos: number }
|
||||
}
|
||||
|
||||
/** 分页响应结构 */
|
||||
export interface MessagePageResult {
|
||||
pageNum: number
|
||||
pageSize: number
|
||||
total: number
|
||||
list: MessageDto[]
|
||||
}
|
||||
|
||||
/** 请求参数 */
|
||||
export interface MessageListParams {
|
||||
pageNum?: number
|
||||
pageSize?: number
|
||||
keyword?: string
|
||||
type?: number | null
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取站内信消息列表(分页)
|
||||
* POST /message/list
|
||||
*/
|
||||
export function fetchMessageList(params: MessageListParams) {
|
||||
return request.get<any, any>('/message/list', { params })
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取未读消息数量
|
||||
* GET /message/unread-count
|
||||
*/
|
||||
export function fetchUnreadCount() {
|
||||
return request.get<any, any>('/message/unread-count')
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记消息已读
|
||||
* POST /message/read/{messageId}
|
||||
*/
|
||||
export function markMessageRead(messageId: number | string) {
|
||||
return request.post<any, any>(`/message/read/${messageId}`)
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 543 B |
@@ -0,0 +1,317 @@
|
||||
@use '../variables' as *;
|
||||
|
||||
// 申请进度消息组件
|
||||
.agent-apply-progress {
|
||||
background: $bg-middle;
|
||||
border-radius: 0.12rem;
|
||||
padding: 0.2rem;
|
||||
|
||||
// 顶部标题栏
|
||||
&__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 0.16rem;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 0.15rem;
|
||||
font-weight: 600;
|
||||
color: $text-dark;
|
||||
|
||||
// 可点击状态(暂停/恢复)
|
||||
&--clickable {
|
||||
cursor: pointer;
|
||||
color: $accent;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__cancel {
|
||||
font-size: 0.13rem;
|
||||
color: $text-middle;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: $text-dark;
|
||||
}
|
||||
}
|
||||
|
||||
// 岗位信息卡片
|
||||
&__job-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.12rem;
|
||||
background: $bg-white;
|
||||
border-radius: 0.1rem;
|
||||
padding: 0.14rem 0.16rem;
|
||||
margin-bottom: 0.2rem;
|
||||
}
|
||||
|
||||
&__job-icon {
|
||||
width: 0.36rem;
|
||||
height: 0.36rem;
|
||||
border-radius: 0.08rem;
|
||||
background: $bg-main;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
color: $text-middle;
|
||||
|
||||
svg {
|
||||
width: 0.2rem;
|
||||
height: 0.2rem;
|
||||
}
|
||||
}
|
||||
|
||||
&__job-info {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&__job-company {
|
||||
font-size: 0.14rem;
|
||||
font-weight: 600;
|
||||
color: $text-dark;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
&__job-title {
|
||||
font-size: 0.12rem;
|
||||
color: $text-middle;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
// 步骤列表
|
||||
&__steps {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.14rem;
|
||||
}
|
||||
|
||||
&__step {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.1rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
// 勾选框
|
||||
&__step-check {
|
||||
width: 0.18rem;
|
||||
height: 0.18rem;
|
||||
border: 1.5px solid $text-light;
|
||||
border-radius: 0.03rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
margin-top: 0.01rem;
|
||||
|
||||
svg {
|
||||
width: 0.12rem;
|
||||
height: 0.12rem;
|
||||
}
|
||||
|
||||
// 已完成态
|
||||
&--done {
|
||||
border-color: $text-dark;
|
||||
background: $text-dark;
|
||||
color: $bg-white;
|
||||
}
|
||||
}
|
||||
|
||||
&__step-label {
|
||||
font-size: 0.14rem;
|
||||
color: $text-dark;
|
||||
line-height: 0.2rem;
|
||||
}
|
||||
|
||||
// 步骤展开内容
|
||||
&__step-content {
|
||||
width: 100%;
|
||||
margin-top: 0.08rem;
|
||||
padding-left: 0.28rem;
|
||||
}
|
||||
|
||||
// 第2步:简历确认区域
|
||||
&__resume-confirm {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background: $bg-white;
|
||||
border-radius: 0.08rem;
|
||||
padding: 0.1rem 0.14rem;
|
||||
}
|
||||
|
||||
&__resume-file {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.08rem;
|
||||
}
|
||||
|
||||
&__file-icon {
|
||||
width: 0.18rem;
|
||||
height: 0.18rem;
|
||||
color: $text-dark;
|
||||
}
|
||||
|
||||
&__resume-name {
|
||||
font-size: 0.13rem;
|
||||
color: $text-dark;
|
||||
}
|
||||
|
||||
&__confirm-btn {
|
||||
height: 0.28rem;
|
||||
padding: 0 0.16rem;
|
||||
background: $btn-dark;
|
||||
color: $bg-white;
|
||||
border: none;
|
||||
border-radius: 0.14rem;
|
||||
font-size: 0.12rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: $btn-dark-hover;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background: $text-light;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
// 第3步:投递操作区域
|
||||
&__apply-area {
|
||||
background: $bg-white;
|
||||
border-radius: 0.08rem;
|
||||
padding: 0.14rem 0.16rem;
|
||||
}
|
||||
|
||||
&__apply-desc {
|
||||
font-size: 0.12rem;
|
||||
color: $text-middle;
|
||||
line-height: 1.8;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&__apply-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 0.1rem;
|
||||
margin-top: 0.14rem;
|
||||
}
|
||||
|
||||
&__skip-btn {
|
||||
font-size: 0.12rem;
|
||||
color: $text-middle;
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
|
||||
&:hover {
|
||||
color: $text-dark;
|
||||
}
|
||||
|
||||
&--disabled {
|
||||
color: $text-light;
|
||||
cursor: not-allowed;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
&__applied-btn {
|
||||
height: 0.28rem;
|
||||
padding: 0 0.14rem;
|
||||
background: $btn-dark;
|
||||
color: $bg-white;
|
||||
border: none;
|
||||
border-radius: 0.14rem;
|
||||
font-size: 0.12rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: $btn-dark-hover;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background: $text-light;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
&__goto-btn {
|
||||
height: 0.28rem;
|
||||
padding: 0 0.14rem;
|
||||
background: $btn-dark;
|
||||
color: $bg-white;
|
||||
border: none;
|
||||
border-radius: 0.14rem;
|
||||
font-size: 0.12rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: $btn-dark-hover;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background: $text-light;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 右侧面板:简历生成进度
|
||||
.agent-resume-generating {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
padding: 0.4rem;
|
||||
|
||||
// 进度条容器
|
||||
&__progress-bar {
|
||||
width: 100%;
|
||||
max-width: 3rem;
|
||||
height: 0.04rem;
|
||||
background: $border-color;
|
||||
border-radius: 0.02rem;
|
||||
overflow: hidden;
|
||||
margin-bottom: 0.2rem;
|
||||
}
|
||||
|
||||
&__progress-fill {
|
||||
height: 100%;
|
||||
background: $text-dark;
|
||||
border-radius: 0.02rem;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 0.18rem;
|
||||
font-weight: 700;
|
||||
color: $text-dark;
|
||||
margin-bottom: 0.1rem;
|
||||
}
|
||||
|
||||
&__hint {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.06rem;
|
||||
font-size: 0.12rem;
|
||||
color: $text-middle;
|
||||
}
|
||||
|
||||
&__hint-icon {
|
||||
width: 0.14rem;
|
||||
height: 0.14rem;
|
||||
color: $accent;
|
||||
}
|
||||
}
|
||||
@@ -17,10 +17,11 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background: linear-gradient(90deg, #94EF9E , #53D9C8 );
|
||||
background: #0F172B;
|
||||
color: #fff;
|
||||
padding: 0.1rem 0.16rem;
|
||||
font-size: 0.13rem;
|
||||
padding: 0.12rem 0.18rem;
|
||||
font-size: 0.15rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
border-radius: 0.2rem;
|
||||
margin: 0.15rem;
|
||||
|
||||
@@ -275,7 +275,7 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.32rem;
|
||||
margin-bottom: 0.02rem;
|
||||
}
|
||||
|
||||
&__step {
|
||||
|
||||
@@ -81,17 +81,11 @@
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 4px;
|
||||
margin-bottom: 2px;
|
||||
break-inside: avoid;
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
|
||||
&__item-left {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
&__item-main {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
@@ -121,7 +115,8 @@
|
||||
&__item-desc {
|
||||
font-size: 12px;
|
||||
color: $text-dark;
|
||||
line-height: 1.5;
|
||||
line-height: 1.7;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
// 描述列表
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -151,6 +151,119 @@
|
||||
color: $text-dark;
|
||||
}
|
||||
|
||||
// ==================== 消息通知弹窗(左右分栏) ====================
|
||||
.side-nav__message-dialog {
|
||||
background: $bg-white;
|
||||
border-radius: 0.12rem;
|
||||
width: 8rem;
|
||||
height: 6rem;
|
||||
max-width: 90vw;
|
||||
max-height: 90vh;
|
||||
box-shadow: 0 0.04rem 0.2rem rgba(0, 0, 0, 0.15);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.side-nav__message-dialog-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.16rem 0.2rem;
|
||||
border-bottom: 1px solid $border-color;
|
||||
font-size: 0.16rem;
|
||||
font-weight: 600;
|
||||
color: $text-dark;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.side-nav__message-dialog-content {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
// 左侧消息列表
|
||||
.side-nav__message-list {
|
||||
width: 2rem;
|
||||
border-right: 1px solid $border-color;
|
||||
overflow-y: auto;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.side-nav__message-list-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.12rem 0.14rem;
|
||||
cursor: pointer;
|
||||
border-bottom: 1px solid $border-color;
|
||||
transition: background 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: $bg-main;
|
||||
}
|
||||
|
||||
&--active {
|
||||
background: $theme-color;
|
||||
}
|
||||
}
|
||||
|
||||
.side-nav__message-list-title {
|
||||
flex: 1;
|
||||
font-size: 0.13rem;
|
||||
color: $text-dark;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.side-nav__message-unread-dot {
|
||||
width: 0.08rem;
|
||||
height: 0.08rem;
|
||||
border-radius: 50%;
|
||||
background: $danger;
|
||||
margin-left: 0.06rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.side-nav__message-list-loading,
|
||||
.side-nav__message-list-empty {
|
||||
padding: 0.16rem;
|
||||
text-align: center;
|
||||
font-size: 0.12rem;
|
||||
color: $text-light;
|
||||
}
|
||||
|
||||
// 右侧消息详情
|
||||
.side-nav__message-detail {
|
||||
flex: 1;
|
||||
padding: 0.2rem;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.side-nav__message-detail-title {
|
||||
font-size: 0.16rem;
|
||||
font-weight: 600;
|
||||
color: $text-dark;
|
||||
margin-bottom: 0.16rem;
|
||||
}
|
||||
|
||||
.side-nav__message-detail-content {
|
||||
font-size: 0.14rem;
|
||||
color: $text-dark;
|
||||
line-height: 1.7;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.side-nav__message-detail-empty {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
font-size: 0.14rem;
|
||||
color: $text-light;
|
||||
}
|
||||
|
||||
// 反馈弹窗
|
||||
.feedback-dialog {
|
||||
padding: 0.28rem 0.24rem;
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
@use './components/resume-edit-name-dialog.scss';
|
||||
@use './components/agent-chat-job-list.scss';
|
||||
@use './components/agent-match-job-add.scss';
|
||||
@use './components/agent-apply-progress.scss';
|
||||
@use './components/ai-thinking-indicator.scss';
|
||||
|
||||
// 全局样式(优先级最高)
|
||||
|
||||
@@ -836,6 +836,30 @@
|
||||
height: calc(100vh - 0.6rem);
|
||||
margin-left: 0.2rem;
|
||||
max-width: 8rem;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
// 右侧简历预览容器
|
||||
&__right-resume {
|
||||
background: $bg-white;
|
||||
border-radius: 0.12rem;
|
||||
padding: 0.24rem;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
|
||||
// 自定义滚动条
|
||||
&::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: $border-color;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 顶部固定设置栏 ====================
|
||||
@@ -909,6 +933,12 @@
|
||||
&:hover {
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background: $text-light;
|
||||
cursor: not-allowed;
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
// 右侧工具按钮区
|
||||
@@ -917,6 +947,140 @@
|
||||
align-items: center;
|
||||
gap: 0.06rem;
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
// 待投递列表下拉弹窗
|
||||
&__task-dropdown {
|
||||
position: absolute;
|
||||
top: 0.36rem;
|
||||
right: 0;
|
||||
width: 5.1rem;
|
||||
background: $bg-white;
|
||||
border-radius: 0.12rem;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.12);
|
||||
z-index: 100;
|
||||
padding: 0.16rem;
|
||||
}
|
||||
|
||||
// Tab 切换
|
||||
&__task-tabs {
|
||||
display: flex;
|
||||
gap: 0.2rem;
|
||||
margin-bottom: 0.14rem;
|
||||
border-bottom: 1px solid $border-color;
|
||||
padding-bottom: 0.1rem;
|
||||
}
|
||||
|
||||
&__task-tab {
|
||||
font-size: 0.14rem;
|
||||
color: $text-middle;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
|
||||
&--active {
|
||||
color: $text-dark;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
// 列表区域
|
||||
&__task-list {
|
||||
max-height: 2.5rem;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
&__task-empty {
|
||||
font-size: 0.12rem;
|
||||
color: $text-light;
|
||||
text-align: center;
|
||||
padding: 0.2rem 0;
|
||||
}
|
||||
|
||||
// 单个岗位项
|
||||
&__task-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.1rem 0;
|
||||
border-bottom: 1px solid $border-color;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
&__task-item-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.08rem;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
&__task-item-name {
|
||||
font-size: 0.13rem;
|
||||
font-weight: 600;
|
||||
color: $text-dark;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
&__task-item-tag {
|
||||
font-size: 0.11rem;
|
||||
color: $bg-white;
|
||||
background: $text-middle;
|
||||
padding: 0.02rem 0.08rem;
|
||||
border-radius: 0.03rem;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__task-item-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.08rem;
|
||||
flex-shrink: 0;
|
||||
margin-left: 0.1rem;
|
||||
}
|
||||
|
||||
&__task-item-status {
|
||||
font-size: 0.12rem;
|
||||
color: $text-middle;
|
||||
|
||||
&--done {
|
||||
color: $accent;
|
||||
}
|
||||
}
|
||||
|
||||
&__task-item-remove {
|
||||
font-size: 0.16rem;
|
||||
color: $text-light;
|
||||
cursor: pointer;
|
||||
line-height: 1;
|
||||
|
||||
&:hover {
|
||||
color: $text-dark;
|
||||
}
|
||||
}
|
||||
|
||||
// 查看全部按钮
|
||||
&__task-view-all {
|
||||
text-align: center;
|
||||
font-size: 0.13rem;
|
||||
color: $text-middle;
|
||||
padding: 0.1rem 0 0;
|
||||
margin-top: 0.1rem;
|
||||
border-top: 1px solid $border-color;
|
||||
cursor: pointer;
|
||||
background: $bg-main;
|
||||
border-radius: 0.06rem;
|
||||
padding: 0.08rem;
|
||||
|
||||
&:hover {
|
||||
color: $text-dark;
|
||||
}
|
||||
}
|
||||
|
||||
// 工具按钮
|
||||
|
||||
@@ -170,6 +170,15 @@
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
// 视频播放器 — 16:9 比例,圆角,无控件
|
||||
&__video {
|
||||
width: 6.5rem;
|
||||
aspect-ratio: 16 / 9;
|
||||
border-radius: 0.24rem;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
// 模拟匹配卡片容器
|
||||
&__card {
|
||||
width: 6.5rem;
|
||||
|
||||
@@ -41,13 +41,17 @@ $border-color: #E8E8E8;
|
||||
// 遮罩层背景
|
||||
$overlay-bg: rgba(0, 0, 0, 0.5);
|
||||
|
||||
// 按钮深色背景(确认提交等)
|
||||
// $btn-dark: #1A1A2E;
|
||||
// 主按钮颜色背景(确认提交等)
|
||||
$btn-dark: #4FC2C9;
|
||||
|
||||
// 按钮深色悬停态
|
||||
// $btn-dark-hover: #2E3142;
|
||||
// 主按钮颜色悬停态
|
||||
$btn-dark-hover: #42A8B3;
|
||||
|
||||
// 次要深色按钮背景(除非特意指定使用否则不用这个按钮色)
|
||||
$btn-dark: #1A1A2E;
|
||||
|
||||
// 次要深色按钮悬停态(除非特意指定使用否则不用这个按钮色)
|
||||
$btn-dark-hover: #2E3142;
|
||||
|
||||
// 渐变色背景
|
||||
$gradient-bg: linear-gradient(to right, #4FC2C9, #42A8B3);
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
<template>
|
||||
<!-- 申请进度消息组件 -->
|
||||
<div class="agent-apply-progress">
|
||||
<!-- 顶部标题栏 -->
|
||||
<div class="agent-apply-progress__header">
|
||||
<!-- 三重状态:投递完成 / 暂停投递 / 开始投递 -->
|
||||
<span
|
||||
class="agent-apply-progress__title"
|
||||
:class="{ 'agent-apply-progress__title--clickable': titleClickable }"
|
||||
@click="handleTitleClick"
|
||||
>{{ titleText }}</span>
|
||||
<span v-if="currentStep < 4" class="agent-apply-progress__cancel" @click="handleCancel">取消</span>
|
||||
</div>
|
||||
|
||||
<!-- 岗位信息卡片 -->
|
||||
<div class="agent-apply-progress__job-card">
|
||||
<div class="agent-apply-progress__job-icon">
|
||||
<svg viewBox="0 0 24 24" fill="none">
|
||||
<rect x="3" y="3" width="7" height="7" rx="1" stroke="currentColor" stroke-width="1.2"/>
|
||||
<rect x="14" y="3" width="7" height="7" rx="1" stroke="currentColor" stroke-width="1.2"/>
|
||||
<rect x="3" y="14" width="7" height="7" rx="1" stroke="currentColor" stroke-width="1.2"/>
|
||||
<rect x="14" y="14" width="7" height="7" rx="1" stroke="currentColor" stroke-width="1.2"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="agent-apply-progress__job-info">
|
||||
<div class="agent-apply-progress__job-company">{{ jobInfo?.companyShortName || jobInfo?.companyName || '' }}</div>
|
||||
<div class="agent-apply-progress__job-title">{{ jobInfo?.title || '' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 步骤列表 -->
|
||||
<div class="agent-apply-progress__steps">
|
||||
<!-- 第1步:优化简历 -->
|
||||
<div class="agent-apply-progress__step">
|
||||
<div class="agent-apply-progress__step-check" :class="{ 'agent-apply-progress__step-check--done': currentStep > 1 }">
|
||||
<svg v-if="currentStep > 1" viewBox="0 0 16 16" fill="none"><path d="M3 8l4 4 6-7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||
</div>
|
||||
<span class="agent-apply-progress__step-label">优化简历</span>
|
||||
</div>
|
||||
|
||||
<!-- 第2步:确认简历 -->
|
||||
<div class="agent-apply-progress__step">
|
||||
<div class="agent-apply-progress__step-check" :class="{ 'agent-apply-progress__step-check--done': currentStep > 2 }">
|
||||
<svg v-if="currentStep > 2" viewBox="0 0 16 16" fill="none"><path d="M3 8l4 4 6-7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||
</div>
|
||||
<span class="agent-apply-progress__step-label">确认简历</span>
|
||||
<!-- 第2步展开内容:简历确认区域 -->
|
||||
<div v-if="currentStep === 2" class="agent-apply-progress__step-content">
|
||||
<div class="agent-apply-progress__resume-confirm">
|
||||
<div class="agent-apply-progress__resume-file">
|
||||
<svg viewBox="0 0 24 24" fill="none" class="agent-apply-progress__file-icon"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8l-6-6z" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/><path d="M14 2v6h6" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/></svg>
|
||||
<span class="agent-apply-progress__resume-name">{{ resumeName }}</span>
|
||||
</div>
|
||||
<button class="agent-apply-progress__confirm-btn" :disabled="paused" @click="handleConfirmResume">确定</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 第3步:分析投递页面 -->
|
||||
<div class="agent-apply-progress__step">
|
||||
<div class="agent-apply-progress__step-check" :class="{ 'agent-apply-progress__step-check--done': currentStep > 3 }">
|
||||
<svg v-if="currentStep > 3" viewBox="0 0 16 16" fill="none"><path d="M3 8l4 4 6-7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||
</div>
|
||||
<span class="agent-apply-progress__step-label">分析投递页面</span>
|
||||
<!-- 第3步展开内容:投递操作区域 -->
|
||||
<div v-if="currentStep === 3" class="agent-apply-progress__step-content">
|
||||
<div class="agent-apply-progress__apply-area">
|
||||
<p class="agent-apply-progress__apply-desc">该职位仅支持在申请网站原地址使用自动填充功能。</p>
|
||||
<p class="agent-apply-progress__apply-desc">请点击"去投递",打开申请网站,我们的自动填写插件将自动弹出。</p>
|
||||
<p class="agent-apply-progress__apply-desc">点击"自动填写",核对信息并提交申请,然后返回本页点击"我已申请"继续。</p>
|
||||
<div class="agent-apply-progress__apply-actions">
|
||||
<span class="agent-apply-progress__skip-btn" :class="{ 'agent-apply-progress__skip-btn--disabled': paused }" @click="handleSkip">跳过</span>
|
||||
<button class="agent-apply-progress__applied-btn" :disabled="paused" @click="handleApplied">我已投递</button>
|
||||
<button class="agent-apply-progress__goto-btn" :disabled="paused" @click="handleGoApply">去投递</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 第4步:提交申请 -->
|
||||
<div class="agent-apply-progress__step">
|
||||
<div class="agent-apply-progress__step-check" :class="{ 'agent-apply-progress__step-check--done': currentStep > 3 }">
|
||||
<svg v-if="currentStep > 3" viewBox="0 0 16 16" fill="none"><path d="M3 8l4 4 6-7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||
</div>
|
||||
<span class="agent-apply-progress__step-label">提交申请</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import type { JobListItem } from '@/api/jobs'
|
||||
|
||||
// ==================== Props ====================
|
||||
|
||||
const props = defineProps<{
|
||||
/** 岗位信息 */
|
||||
jobInfo: JobListItem | null
|
||||
/** 当前步骤(1-4,大于该步骤表示已完成) */
|
||||
currentStep: number
|
||||
/** 简历名称(第2步确认时显示) */
|
||||
resumeName?: string
|
||||
/** 是否暂停状态 */
|
||||
paused?: boolean
|
||||
}>()
|
||||
|
||||
// ==================== 计算属性 ====================
|
||||
|
||||
/** 标题文字:投递完成 / 暂停投递 / 开始投递 */
|
||||
const titleText = computed(() => {
|
||||
if (props.currentStep >= 4) return '投递完成'
|
||||
if (props.paused && props.currentStep > 1) return '开始投递'
|
||||
if (!props.paused && props.currentStep > 1) return '暂停投递'
|
||||
return '开始投递'
|
||||
})
|
||||
|
||||
/** 标题是否可点击(暂停投递 或 暂停后的开始投递) */
|
||||
const titleClickable = computed(() => {
|
||||
if (props.currentStep >= 4) return false
|
||||
return props.currentStep > 1
|
||||
})
|
||||
|
||||
// ==================== 事件 ====================
|
||||
|
||||
const emit = defineEmits<{
|
||||
/** 取消投递流程 */
|
||||
(e: 'cancel'): void
|
||||
/** 确认简历(第2步) */
|
||||
(e: 'confirmResume'): void
|
||||
/** 跳过投递(第3步) */
|
||||
(e: 'skip'): void
|
||||
/** 我已投递(第3步) */
|
||||
(e: 'applied'): void
|
||||
/** 去投递(第3步,打开原链接) */
|
||||
(e: 'goApply'): void
|
||||
/** 暂停/恢复投递 */
|
||||
(e: 'togglePause'): void
|
||||
}>()
|
||||
|
||||
// ==================== 事件处理 ====================
|
||||
|
||||
/** 点击标题(暂停/恢复) */
|
||||
function handleTitleClick() {
|
||||
if (!titleClickable.value) return
|
||||
emit('togglePause')
|
||||
}
|
||||
|
||||
/** 取消 */
|
||||
function handleCancel() {
|
||||
emit('cancel')
|
||||
}
|
||||
|
||||
/** 确认简历 */
|
||||
function handleConfirmResume() {
|
||||
if (props.paused) return
|
||||
emit('confirmResume')
|
||||
}
|
||||
|
||||
/** 跳过 */
|
||||
function handleSkip() {
|
||||
if (props.paused) return
|
||||
emit('skip')
|
||||
}
|
||||
|
||||
/** 我已投递 */
|
||||
function handleApplied() {
|
||||
if (props.paused) return
|
||||
emit('applied')
|
||||
}
|
||||
|
||||
/** 去投递 */
|
||||
function handleGoApply() {
|
||||
if (props.paused) return
|
||||
emit('goApply')
|
||||
}
|
||||
</script>
|
||||
@@ -9,6 +9,7 @@
|
||||
v-for="job in displayJobs"
|
||||
:key="job.id"
|
||||
class="agent-chat-job-list__item"
|
||||
v-if="displayJobs.length>0"
|
||||
>
|
||||
<!-- 左侧:公司图标 + 岗位信息 -->
|
||||
<div class="agent-chat-job-list__info">
|
||||
@@ -51,9 +52,12 @@
|
||||
<span class="agent-chat-job-list__score-text">{{ job.matchScore || 0 }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="color-9" v-if="displayJobs.length==0">
|
||||
暂无相关职位推荐
|
||||
</div>
|
||||
|
||||
<!-- 查看全部岗位按钮 -->
|
||||
<div class="agent-chat-job-list__footer">
|
||||
<div v-if="displayJobs.length>0" class="agent-chat-job-list__footer">
|
||||
<button class="agent-chat-job-list__view-all-btn" @click="handleViewAll">查看全部岗位</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
<template>
|
||||
<!-- 待投递列表下拉弹窗 -->
|
||||
<div class="agent-main__task-dropdown" @click.stop>
|
||||
<!-- Tab 切换 -->
|
||||
<div class="agent-main__task-tabs">
|
||||
<span
|
||||
class="agent-main__task-tab"
|
||||
:class="{ 'agent-main__task-tab--active': activeTab === 1 }"
|
||||
@click="switchTab(1)"
|
||||
>进行中({{ pendingList.length }})</span>
|
||||
<span
|
||||
class="agent-main__task-tab"
|
||||
:class="{ 'agent-main__task-tab--active': activeTab === 2 }"
|
||||
@click="switchTab(2)"
|
||||
>已完成({{ completedList.length }})</span>
|
||||
</div>
|
||||
<!-- 列表内容 -->
|
||||
<div class="agent-main__task-list">
|
||||
<template v-if="activeTab === 1">
|
||||
<div v-if="pendingList.length === 0" class="agent-main__task-empty">暂无进行中的岗位</div>
|
||||
<div v-for="job in pendingList" :key="job.id" class="agent-main__task-item">
|
||||
<div class="agent-main__task-item-left">
|
||||
<span class="agent-main__task-item-name">{{ (job.companyShortName || job.companyName) + '—' + job.title }}</span>
|
||||
<span class="agent-main__task-item-tag">自动投递</span>
|
||||
</div>
|
||||
<div class="agent-main__task-item-right">
|
||||
<span class="agent-main__task-item-status">未开始</span>
|
||||
<span class="agent-main__task-item-remove" @click="removeJob(job)">×</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div v-if="loading" class="agent-main__task-empty">加载中...</div>
|
||||
<div v-else-if="completedList.length === 0" class="agent-main__task-empty">暂无已完成的岗位</div>
|
||||
<div v-for="job in completedList" :key="job.id" class="agent-main__task-item">
|
||||
<div class="agent-main__task-item-left">
|
||||
<span class="agent-main__task-item-name">{{ (job.companyShortName || job.companyName) + '—' + job.title }}</span>
|
||||
<span class="agent-main__task-item-tag">自动投递</span>
|
||||
</div>
|
||||
<div class="agent-main__task-item-right">
|
||||
<span class="agent-main__task-item-status agent-main__task-item-status--done">已完成</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<!-- 查看全部 -->
|
||||
<div class="agent-main__task-view-all" @click="emit('viewAll')">查看全部</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { fetchAgentTaskList } from '@/api/jobs'
|
||||
import { cancelApplyJob } from '@/api/agent'
|
||||
import type { JobListItem } from '@/api/jobs'
|
||||
|
||||
// ==================== Props ====================
|
||||
|
||||
const props = defineProps<{
|
||||
/** 进行中列表(由父组件传入,保持同步) */
|
||||
pendingList: JobListItem[]
|
||||
}>()
|
||||
|
||||
// ==================== 事件 ====================
|
||||
|
||||
const emit = defineEmits<{
|
||||
/** 移除岗位后通知父组件刷新列表 */
|
||||
(e: 'removed', jobId: string): void
|
||||
/** 查看全部 */
|
||||
(e: 'viewAll'): void
|
||||
}>()
|
||||
|
||||
// ==================== 状态 ====================
|
||||
|
||||
/** 当前激活的 Tab(1=进行中 2=已完成) */
|
||||
const activeTab = ref(1)
|
||||
|
||||
/** 已完成列表数据 */
|
||||
const completedList = ref<JobListItem[]>([])
|
||||
|
||||
/** 加载状态 */
|
||||
const loading = ref(false)
|
||||
|
||||
// ==================== 生命周期 ====================
|
||||
|
||||
onMounted(() => {
|
||||
// 打开时同时加载已完成列表
|
||||
loadCompletedList()
|
||||
})
|
||||
|
||||
// ==================== 方法 ====================
|
||||
|
||||
/** 切换 Tab */
|
||||
function switchTab(tab: number) {
|
||||
activeTab.value = tab
|
||||
if (tab === 2) {
|
||||
loadCompletedList()
|
||||
}
|
||||
}
|
||||
|
||||
/** 加载已完成任务列表(tab=2) */
|
||||
async function loadCompletedList() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await fetchAgentTaskList({ pageNum: 1, pageSize: 100, tab: 2 })
|
||||
if (res.code === '0' && res.data) {
|
||||
completedList.value = res.data.list || []
|
||||
}
|
||||
} catch {
|
||||
console.error('[AgentTaskListDropdown] 加载已完成列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 移除岗位 — 调用 cancelApplyJob 接口并通知父组件 */
|
||||
async function removeJob(job: JobListItem) {
|
||||
try {
|
||||
await cancelApplyJob(Number(job.id))
|
||||
emit('removed', job.id)
|
||||
ElMessage.success('已移除')
|
||||
} catch {
|
||||
ElMessage.error('移除失败')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -2,7 +2,7 @@
|
||||
<div class="ai-chat">
|
||||
<!-- 顶部会员横幅 — 点击打开会员购买弹窗 -->
|
||||
<div class="ai-chat__banner" @click="showMemberDialog = true">
|
||||
<span>解锁会员,送快上岸!</span>
|
||||
<span class="dflex-start aliite-c"> <img src="@/assets/images/bule-flash.png" alt="Offer派" class="w16 h19 mr5" /> <span>升级 Pro 开启求职加速</span></span>
|
||||
<span class="ai-chat__banner-arrow">›</span>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -221,10 +221,14 @@
|
||||
</div>
|
||||
<!-- AI帮写内容 -->
|
||||
<div v-if="previewTab === 'ai'" class="job-resume-custom-dialog__preview-ai">
|
||||
|
||||
<!-- AI聊天消息区域 -->
|
||||
<div class="job-resume-custom-dialog__ai-messages" ref="aiMessagesRef">
|
||||
|
||||
<!-- 匹配度提升提示 -->
|
||||
<div class="job-resume-custom-dialog__ai-result">
|
||||
<div class="job-resume-custom-dialog__ai-result prl0">
|
||||
<div class="job-resume-custom-dialog__ai-result-text">
|
||||
<p class="job-resume-custom-dialog__ai-result-title">恭喜!你的简历匹配值从<br/>{{ jobInfo.matchScore }}分提升到了10分!</p>
|
||||
<p class="job-resume-custom-dialog__ai-result-title">恭喜!你的简历匹配值从<br/>{{ jobInfo.matchScore }}分提升到了{{ cachedOptimizedScore }}分!</p>
|
||||
<div class="job-resume-custom-dialog__ai-result-detail">
|
||||
<p class="job-resume-custom-dialog__ai-result-subtitle">做了哪些优化?</p>
|
||||
<ul class="job-resume-custom-dialog__ai-result-list">
|
||||
@@ -236,15 +240,15 @@
|
||||
<div class="job-resume-custom-dialog__match-ring job-resume-custom-dialog__match-ring--large">
|
||||
<svg viewBox="0 0 60 60" class="job-resume-custom-dialog__ring-svg">
|
||||
<circle cx="30" cy="30" r="24" stroke-width="4" stroke="#E8E8E8" fill="none" opacity="0.3"/>
|
||||
<circle cx="30" cy="30" r="24" stroke-width="4" fill="none" stroke="#4FC2C9" stroke-linecap="round" :stroke-dasharray="2*Math.PI*24" :stroke-dashoffset="0" transform="rotate(-90 30 30)"/>
|
||||
<circle cx="30" cy="30" r="24" stroke-width="4" fill="none" stroke="#4FC2C9" stroke-linecap="round" :stroke-dasharray="2*Math.PI*24" :stroke-dashoffset="2*Math.PI*24*(1-cachedOptimizedScore/10)" transform="rotate(-90 30 30)"/>
|
||||
</svg>
|
||||
<span class="job-resume-custom-dialog__match-score">10.0</span>
|
||||
<span class="job-resume-custom-dialog__match-score">{{ cachedOptimizedScore.toFixed(1) }}</span>
|
||||
</div>
|
||||
<span class="job-resume-custom-dialog__match-label job-resume-custom-dialog__match-label--high">非常匹配</span>
|
||||
<span class="job-resume-custom-dialog__match-label job-resume-custom-dialog__match-label--high">{{ cachedOptimizedScore >= 9 ? '非常匹配' : cachedOptimizedScore >= 6 ? '高匹配度' : '低匹配度' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 快捷操作按钮 -->
|
||||
<div class="job-resume-custom-dialog__ai-quick-actions">
|
||||
<div class="job-resume-custom-dialog__ai-quick-actions prl0">
|
||||
<button
|
||||
v-for="(action, i) in aiQuickActions"
|
||||
:key="i"
|
||||
@@ -252,8 +256,7 @@
|
||||
@click="sendAiMessage(action)"
|
||||
>{{ action }}</button>
|
||||
</div>
|
||||
<!-- AI聊天消息区域 -->
|
||||
<div class="job-resume-custom-dialog__ai-messages" ref="aiMessagesRef">
|
||||
|
||||
<div
|
||||
v-for="(msg, i) in aiMessages"
|
||||
:key="i"
|
||||
@@ -748,6 +751,42 @@ const aiOptimizeResults = computed<string[]>(() => {
|
||||
return results
|
||||
})
|
||||
|
||||
/**
|
||||
* 根据勾选的优化部分数量计算优化后的匹配评分
|
||||
* 0个勾选:3~5之间随机(一位小数)
|
||||
* 1个勾选:6~7之间随机
|
||||
* 2个勾选:8~9之间随机
|
||||
* 3个勾选:9~10之间随机,50%概率直接为10.0
|
||||
*/
|
||||
const optimizedMatchScore = computed(() => {
|
||||
const checkedCount = optimizeSections.value.filter(s => s.checked).length
|
||||
let min: number, max: number
|
||||
if (checkedCount === 0) {
|
||||
min = 3; max = 5
|
||||
} else if (checkedCount === 1) {
|
||||
min = 6; max = 7
|
||||
} else if (checkedCount === 2) {
|
||||
min = 8; max = 9
|
||||
} else {
|
||||
// 3个勾选:50%概率直接10.0
|
||||
if (Math.random() < 0.2) return 10.0
|
||||
min = 9; max = 10
|
||||
}
|
||||
// 生成 [min, max] 之间的一位小数随机值
|
||||
const raw = min + Math.random() * (max - min)
|
||||
return Math.round(raw * 10) / 10
|
||||
})
|
||||
|
||||
/** 缓存优化后评分(避免computed每次重新随机) */
|
||||
const cachedOptimizedScore = ref<number>(0)
|
||||
|
||||
/** 进入步骤4时缓存一次评分 */
|
||||
watch(currentStep, (val) => {
|
||||
if (val === 4) {
|
||||
cachedOptimizedScore.value = optimizedMatchScore.value
|
||||
}
|
||||
})
|
||||
|
||||
/** AI快捷操作按钮 */
|
||||
const aiQuickActions = ref<string[]>([
|
||||
'精简一下第一段工作经历',
|
||||
|
||||
@@ -44,7 +44,6 @@
|
||||
<div class="resume-html__divider"></div>
|
||||
<div v-for="(edu, idx) in resumeData.educations" :key="'edu-' + idx" class="resume-html__item">
|
||||
<div class="resume-html__item-header">
|
||||
<div class="resume-html__item-left">
|
||||
<span class="resume-html__item-main">
|
||||
<!-- 教育经历标题差异对比 -->
|
||||
<template v-if="showDiff">
|
||||
@@ -57,20 +56,19 @@
|
||||
</template>
|
||||
<template v-else>{{ edu.school }},{{ edu.major }},{{ degreeText(edu.degree) }}</template>
|
||||
</span>
|
||||
<span v-if="edu.description && edu.description.length" class="resume-html__item-desc">
|
||||
<!-- 教育经历描述差异对比 -->
|
||||
<div class="resume-html__item-right">
|
||||
<span class="resume-html__item-location" v-if="edu.location">{{ edu.location }}</span>
|
||||
<span class="resume-html__item-date">{{ edu.startDate }} — {{ edu.endDate || '至今' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 教育经历描述独立一行,避免被header的flex布局挤压 -->
|
||||
<div v-if="edu.description && edu.description.length" class="resume-html__item-desc">
|
||||
<template v-if="showDiff">
|
||||
主修课程:<template v-for="(desc, di) in edu.description" :key="'ed-' + di"><template v-if="di > 0">、</template><template v-for="(seg, si) in diffDescText(oldResumeData?.educations, idx, di, desc.text)" :key="'eds-' + si"><span v-if="seg.highlight" class="resume-html__diff-highlight">{{ seg.text }}</span><template v-else>{{ seg.text }}</template></template></template>等;
|
||||
</template>
|
||||
<template v-else>
|
||||
主修课程:{{ edu.description.map(d => d.text).join('、') }}等;
|
||||
</template>
|
||||
</span>
|
||||
</div>
|
||||
<div class="resume-html__item-right">
|
||||
<span class="resume-html__item-location" v-if="edu.location">{{ edu.location }}</span>
|
||||
<span class="resume-html__item-date">{{ edu.startDate }} — {{ edu.endDate || '至今' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
+428
-77
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<!-- 会员购买弹窗 — 通过 Teleport 挂载到 body -->
|
||||
<!-- 会员弹窗 — 通过 Teleport 挂载到 body -->
|
||||
<Teleport to="body">
|
||||
<!-- 遮罩层 — 点击关闭弹窗 -->
|
||||
<div v-if="modelValue" class="member-dialog-overlay" @click="$emit('update:modelValue', false)">
|
||||
@@ -8,8 +8,21 @@
|
||||
<!-- 右上角关闭按钮 -->
|
||||
<span class="member-dialog__close" @click="$emit('update:modelValue', false)">✕</span>
|
||||
|
||||
<!-- 顶部标语 -->
|
||||
<div class="member-dialog__slogan">每天不到一元,获得三倍面试机会!</div>
|
||||
<!-- ==================== 会员介绍步骤一:简介页 ==================== -->
|
||||
<div v-if="currentView === 'intro1'" class="member-dialog__intro-step1">
|
||||
<h1 class="member-dialog__intro-title">Offer 派 - AI 智能求职引擎</h1>
|
||||
<p class="member-dialog__intro-subtitle">全自动海投、简历精修、模拟面试,助你轻松拿高薪。</p>
|
||||
<button class="member-dialog__intro-btn" @click="currentView = 'intro2'">
|
||||
<span class="member-dialog__intro-btn-icon">⚡</span>
|
||||
升级 Pro 开启求职加速
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- ==================== 会员介绍步骤二:详细介绍页(可滚动) ==================== -->
|
||||
<div v-else-if="currentView === 'intro2'" class="member-dialog__intro-step2">
|
||||
<div class="member-dialog__step2-badge">⚡ 求职加速会员</div>
|
||||
<h1 class="member-dialog__step2-title">offer派,收 offer 就是快!</h1>
|
||||
<p class="member-dialog__step2-subtitle">每天不到 1 元,少投无效岗位,把时间花在更可能拿面试的机会。</p>
|
||||
|
||||
<!-- 套餐卡片区域 -->
|
||||
<div class="member-dialog__plans">
|
||||
@@ -20,61 +33,317 @@
|
||||
:class="{ 'member-dialog__plan-card--active': selectedPlan === plan.key }"
|
||||
@click="selectedPlan = plan.key"
|
||||
>
|
||||
<!-- 套餐名称 -->
|
||||
<div v-if="plan.recommend" class="member-dialog__plan-recommend">★ 推荐:覆盖求职周期,性价比最高</div>
|
||||
<div v-if="plan.tag" class="member-dialog__plan-tag">{{ plan.tag }}</div>
|
||||
<div class="member-dialog__plan-name">{{ plan.name }}</div>
|
||||
<!-- 原价(划线) -->
|
||||
<div class="member-dialog__plan-original">{{ plan.originalPrice }}</div>
|
||||
<!-- 现价 -->
|
||||
<div class="member-dialog__plan-price">
|
||||
<span class="member-dialog__plan-price-num">¥{{ plan.price }}</span>
|
||||
<span class="member-dialog__plan-price-symbol">¥</span>
|
||||
<span class="member-dialog__plan-price-num">{{ plan.price }}</span>
|
||||
<span class="member-dialog__plan-price-unit">/{{ plan.unit }}</span>
|
||||
<!-- 省钱标签 -->
|
||||
<span v-if="plan.discount" class="member-dialog__plan-discount">省{{ plan.discount }}</span>
|
||||
</div>
|
||||
<!-- 立刻升级按钮 -->
|
||||
<button class="member-dialog__plan-btn" @click.stop="handleUpgrade(plan)">立刻升级</button>
|
||||
<div v-if="plan.perMonth" class="member-dialog__plan-per-month">折合仅 ¥{{ plan.perMonth }}/月</div>
|
||||
<button class="member-dialog__plan-btn" @click.stop="handleUpgrade(plan)">{{ plan.btnText }}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 权益对比区域 -->
|
||||
<div class="member-dialog__benefits">
|
||||
<!-- 左列:面试率提升工具 -->
|
||||
<div class="member-dialog__benefit-col">
|
||||
<div class="member-dialog__benefit-title">面试率提升工具</div>
|
||||
<div
|
||||
v-for="item in toolList"
|
||||
:key="item"
|
||||
class="member-dialog__benefit-item"
|
||||
>{{ item }}</div>
|
||||
<div class="member-dialog__benefit-other">
|
||||
其他渠道购买 <span class="member-dialog__benefit-highlight">¥210/月</span>
|
||||
<!-- 提示信息 -->
|
||||
<div class="member-dialog__tip">
|
||||
<span class="member-dialog__tip-icon">⏰</span>
|
||||
<span class="member-dialog__tip-label">时间就是机会:</span>
|
||||
<span class="member-dialog__tip-text">热门岗位通常在发布后 24-48 小时内收到大量申请。使用 AI 加速工具,越早投递越容易被 HR 看到!</span>
|
||||
</div>
|
||||
|
||||
<!-- 底部内容:左侧能力列表 + 右侧对比表格 -->
|
||||
<div class="member-dialog__content-row">
|
||||
<div class="member-dialog__abilities">
|
||||
<h3 class="member-dialog__abilities-title">求职加速能力</h3>
|
||||
<div v-for="ability in abilities" :key="ability.text" class="member-dialog__ability-item">
|
||||
<span class="member-dialog__ability-icon" :style="{ color: ability.color }">{{ ability.icon }}</span>
|
||||
<span class="member-dialog__ability-text">{{ ability.text }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="member-dialog__compare">
|
||||
<h3 class="member-dialog__compare-title">会员能力对比</h3>
|
||||
<table class="member-dialog__compare-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>核心能力</th>
|
||||
<th>免费版</th>
|
||||
<th class="member-dialog__compare-th-pro">⚡ 会员版</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="row in compareData" :key="row.name">
|
||||
<td>{{ row.name }}</td>
|
||||
<td class="member-dialog__compare-free">{{ row.free }}</td>
|
||||
<td class="member-dialog__compare-pro">✓ {{ row.pro }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="member-dialog__compare-footer">
|
||||
<span>🔒 支付安全保障</span>
|
||||
<span>⏰ 支持随时取消</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 中列:会员权益 -->
|
||||
<div class="member-dialog__benefit-col member-dialog__benefit-col--center">
|
||||
<div class="member-dialog__benefit-title member-dialog__benefit-title--accent">会员权益</div>
|
||||
<!-- 用户评价 -->
|
||||
<div class="member-dialog__testimonial">
|
||||
<div class="member-dialog__testimonial-avatar">
|
||||
<img src="@/assets/images/home/avatar-temporary.png" alt="用户头像" />
|
||||
</div>
|
||||
<div class="member-dialog__testimonial-content">
|
||||
<div class="member-dialog__testimonial-stars">★★★★★</div>
|
||||
<p class="member-dialog__testimonial-text">"开通后我主要用 AI 简历优化和岗位匹配,投递效率明显提升,每次网申节省了十几分钟,终于知道该重点投哪些岗位了。"</p>
|
||||
<div class="member-dialog__testimonial-author">
|
||||
<span class="member-dialog__testimonial-name">张同学</span>
|
||||
<span class="member-dialog__testimonial-desc">· 成功入职某大厂运营</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ==================== 下单步骤一:选择套餐 ==================== -->
|
||||
<div v-else-if="currentView === 'order1'" class="member-dialog__order">
|
||||
<!-- 顶部导航 -->
|
||||
<div class="member-dialog__order-header">
|
||||
<span class="member-dialog__order-back" @click="currentView = 'intro2'">‹ 返回会员介绍</span>
|
||||
<!-- 步骤条 -->
|
||||
<div class="member-dialog__steps">
|
||||
<div class="member-dialog__step member-dialog__step--active">
|
||||
<span class="member-dialog__step-num">1</span>
|
||||
<span class="member-dialog__step-label">选择套餐</span>
|
||||
</div>
|
||||
<div class="member-dialog__step-line"></div>
|
||||
<div class="member-dialog__step">
|
||||
<span class="member-dialog__step-num">2</span>
|
||||
<span class="member-dialog__step-label">支付方式</span>
|
||||
</div>
|
||||
<div class="member-dialog__step-line"></div>
|
||||
<div class="member-dialog__step">
|
||||
<span class="member-dialog__step-num">3</span>
|
||||
<span class="member-dialog__step-label">完成</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 主内容区域(可滚动) -->
|
||||
<div class="member-dialog__order-body">
|
||||
<!-- 左侧内容 -->
|
||||
<div class="member-dialog__order-left">
|
||||
<h2 class="member-dialog__order-title">开启你的 AI 求职加速计划</h2>
|
||||
<p class="member-dialog__order-desc">解锁简历优化、岗位匹配、投递追踪、面试准备等能力,让你的求职过程更高效、更有方向。</p>
|
||||
|
||||
<!-- 套餐选择卡片 -->
|
||||
<div class="member-dialog__order-plans">
|
||||
<div
|
||||
v-for="item in memberBenefits"
|
||||
:key="item.label"
|
||||
class="member-dialog__benefit-item"
|
||||
v-for="plan in plans"
|
||||
:key="plan.key"
|
||||
class="member-dialog__order-plan-card"
|
||||
:class="{ 'member-dialog__order-plan-card--active': selectedPlan === plan.key }"
|
||||
@click="selectedPlan = plan.key"
|
||||
>
|
||||
<span class="member-dialog__benefit-badge">{{ item.badge }}</span>
|
||||
<span>{{ item.label }}</span>
|
||||
<div v-if="plan.recommend" class="member-dialog__order-plan-badge">★ 推荐</div>
|
||||
<!-- 选中圆圈 -->
|
||||
<div class="member-dialog__order-plan-radio">
|
||||
<div v-if="selectedPlan === plan.key" class="member-dialog__order-plan-radio-inner"></div>
|
||||
</div>
|
||||
<div class="member-dialog__order-plan-info">
|
||||
<div class="member-dialog__order-plan-name">{{ plan.name }}</div>
|
||||
<div class="member-dialog__order-plan-price">
|
||||
<span class="member-dialog__order-plan-price-symbol">¥</span>
|
||||
<span class="member-dialog__order-plan-price-num">{{ plan.price }}</span>
|
||||
<span class="member-dialog__order-plan-price-unit">/{{ plan.unit }}</span>
|
||||
</div>
|
||||
<div class="member-dialog__order-plan-desc">{{ plan.orderDesc }}</div>
|
||||
</div>
|
||||
<div class="member-dialog__benefit-other">
|
||||
打包价 <span class="member-dialog__benefit-highlight">¥19.99/月</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右列:免费权益(当前) -->
|
||||
<div class="member-dialog__benefit-col">
|
||||
<div class="member-dialog__benefit-title">免费权益(当前)</div>
|
||||
<!-- 支付方式 -->
|
||||
<div class="member-dialog__order-payment">
|
||||
<h3 class="member-dialog__order-payment-title">支付方式</h3>
|
||||
<div class="member-dialog__order-payment-methods">
|
||||
<div
|
||||
v-for="item in freeBenefits"
|
||||
:key="item"
|
||||
class="member-dialog__benefit-item"
|
||||
>{{ item }}</div>
|
||||
v-for="method in paymentMethods"
|
||||
:key="method.key"
|
||||
class="member-dialog__order-payment-item"
|
||||
:class="{ 'member-dialog__order-payment-item--active': selectedPayment === method.key }"
|
||||
@click="selectedPayment = method.key"
|
||||
>
|
||||
<span class="member-dialog__order-payment-icon">{{ method.icon }}</span>
|
||||
<span class="member-dialog__order-payment-name">{{ method.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 支付提示 -->
|
||||
<div class="member-dialog__order-payment-hint">
|
||||
点击右侧"立即开启求职加速"后,将弹出二维码完成支付。
|
||||
</div>
|
||||
<div class="member-dialog__order-payment-safe">支付信息将通过安全通道加密处理,请放心支付。</div>
|
||||
</div>
|
||||
|
||||
<!-- 底部保障标签 -->
|
||||
<div class="member-dialog__order-guarantees">
|
||||
<span class="member-dialog__order-guarantee-item">🔒 安全支付</span>
|
||||
<span class="member-dialog__order-guarantee-item">⚡ 立即解锁</span>
|
||||
<span class="member-dialog__order-guarantee-item">⏰ 支持取消续费</span>
|
||||
<span class="member-dialog__order-guarantee-item">🎧 客服支持</span>
|
||||
</div>
|
||||
|
||||
<!-- 常见问题 -->
|
||||
<div class="member-dialog__order-faq">
|
||||
<h3 class="member-dialog__order-faq-title">常见问题</h3>
|
||||
<div
|
||||
v-for="faq in faqList"
|
||||
:key="faq.question"
|
||||
class="member-dialog__order-faq-item"
|
||||
@click="faq.open = !faq.open"
|
||||
>
|
||||
<div class="member-dialog__order-faq-question">
|
||||
<span>{{ faq.question }}</span>
|
||||
<span class="member-dialog__order-faq-arrow" :class="{ 'member-dialog__order-faq-arrow--open': faq.open }">›</span>
|
||||
</div>
|
||||
<div v-if="faq.open" class="member-dialog__order-faq-answer">{{ faq.answer }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧订单摘要 -->
|
||||
<div class="member-dialog__order-right">
|
||||
<div class="member-dialog__order-summary">
|
||||
<h3 class="member-dialog__order-summary-title">订单摘要</h3>
|
||||
<!-- 套餐信息 -->
|
||||
<div class="member-dialog__order-summary-row">
|
||||
<div>
|
||||
<div class="member-dialog__order-summary-plan">{{ currentPlan.name }}</div>
|
||||
<div class="member-dialog__order-summary-duration">{{ currentPlan.unit }}</div>
|
||||
</div>
|
||||
<span class="member-dialog__order-summary-price">¥{{ currentPlan.price }}</span>
|
||||
</div>
|
||||
<!-- 分割线 -->
|
||||
<div class="member-dialog__order-summary-divider"></div>
|
||||
<!-- 今日支付 -->
|
||||
<div class="member-dialog__order-summary-row">
|
||||
<span class="member-dialog__order-summary-label">今日支付</span>
|
||||
<span class="member-dialog__order-summary-total">¥{{ currentPlan.price }}</span>
|
||||
</div>
|
||||
<!-- 协议勾选 -->
|
||||
<div class="member-dialog__order-summary-agree">
|
||||
<label class="member-dialog__order-checkbox">
|
||||
<input type="checkbox" v-model="agreeProtocol" />
|
||||
<span class="member-dialog__order-checkbox-mark"></span>
|
||||
</label>
|
||||
<span>我已阅读并同意 <a href="javascript:;">《会员服务协议》</a> <a href="javascript:;">《自动续费协议》</a></span>
|
||||
</div>
|
||||
<!-- 立即开启按钮 -->
|
||||
<button
|
||||
class="member-dialog__order-submit-btn"
|
||||
:disabled="!agreeProtocol"
|
||||
@click="handleShowQrCode"
|
||||
>立即开启求职加速</button>
|
||||
<div class="member-dialog__order-submit-tip">支付成功后立即解锁全部权益,可随时取消续费。</div>
|
||||
|
||||
<!-- 你将获得 -->
|
||||
<div class="member-dialog__order-benefits">
|
||||
<h4 class="member-dialog__order-benefits-title">你将获得</h4>
|
||||
<div v-for="item in orderBenefits" :key="item" class="member-dialog__order-benefits-item">
|
||||
<span class="member-dialog__order-benefits-check">✓</span>
|
||||
<span>{{ item }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ==================== 下单步骤二:确认支付结果 ==================== -->
|
||||
<div v-else-if="currentView === 'order2'" class="member-dialog__order">
|
||||
<div class="member-dialog__order-header">
|
||||
<span class="member-dialog__order-back"></span>
|
||||
<div class="member-dialog__steps">
|
||||
<div class="member-dialog__step member-dialog__step--done">
|
||||
<span class="member-dialog__step-num">✓</span>
|
||||
<span class="member-dialog__step-label">选择套餐</span>
|
||||
</div>
|
||||
<div class="member-dialog__step-line member-dialog__step-line--done"></div>
|
||||
<div class="member-dialog__step member-dialog__step--active">
|
||||
<span class="member-dialog__step-num">2</span>
|
||||
<span class="member-dialog__step-label">支付方式</span>
|
||||
</div>
|
||||
<div class="member-dialog__step-line"></div>
|
||||
<div class="member-dialog__step">
|
||||
<span class="member-dialog__step-num">3</span>
|
||||
<span class="member-dialog__step-label">完成</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 确认中内容 -->
|
||||
<div class="member-dialog__order-confirming">
|
||||
<div class="member-dialog__order-confirming-card">
|
||||
<!-- 加载动画 -->
|
||||
<div class="member-dialog__order-loading-spinner"></div>
|
||||
<h2 class="member-dialog__order-confirming-title">正在确认支付结果</h2>
|
||||
<p class="member-dialog__order-confirming-desc">请稍候,请勿关闭此页面。</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ==================== 下单步骤三:支付完成 ==================== -->
|
||||
<div v-else-if="currentView === 'order3'" class="member-dialog__order">
|
||||
<div class="member-dialog__order-header">
|
||||
<span class="member-dialog__order-back"></span>
|
||||
<div class="member-dialog__steps">
|
||||
<div class="member-dialog__step member-dialog__step--done">
|
||||
<span class="member-dialog__step-num">✓</span>
|
||||
<span class="member-dialog__step-label">选择套餐</span>
|
||||
</div>
|
||||
<div class="member-dialog__step-line member-dialog__step-line--done"></div>
|
||||
<div class="member-dialog__step member-dialog__step--done">
|
||||
<span class="member-dialog__step-num">✓</span>
|
||||
<span class="member-dialog__step-label">支付方式</span>
|
||||
</div>
|
||||
<div class="member-dialog__step-line member-dialog__step-line--done"></div>
|
||||
<div class="member-dialog__step member-dialog__step--active">
|
||||
<span class="member-dialog__step-num">3</span>
|
||||
<span class="member-dialog__step-label">完成</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 支付成功内容 -->
|
||||
<div class="member-dialog__order-success">
|
||||
<div class="member-dialog__order-success-card">
|
||||
<!-- 成功图标 -->
|
||||
<div class="member-dialog__order-success-icon">✓</div>
|
||||
<h2 class="member-dialog__order-success-title">支付成功,求职加速已开启</h2>
|
||||
<p class="member-dialog__order-success-desc">你已成功解锁 AI 求职加速权益,现在可以开始优化简历、匹配岗位并准备面试。</p>
|
||||
<button class="member-dialog__order-success-btn" @click="handleGoResume">开始优化我的简历</button>
|
||||
<button class="member-dialog__order-success-btn-secondary" @click="$emit('update:modelValue', false)">查看会员权益</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ==================== 付款二维码遮罩层 ==================== -->
|
||||
<div v-if="showQrCode" class="member-dialog__qrcode-overlay" @click="showQrCode = false">
|
||||
<div class="member-dialog__qrcode-modal" @click.stop>
|
||||
<!-- 关闭按钮 -->
|
||||
<span class="member-dialog__qrcode-close" @click="showQrCode = false">✕</span>
|
||||
<!-- 支付方式标题 -->
|
||||
<div class="member-dialog__qrcode-title">
|
||||
<span class="member-dialog__qrcode-title-icon">{{ selectedPayment === 'wechat' ? '💬' : '🔷' }}</span>
|
||||
<span>{{ selectedPayment === 'wechat' ? '微信支付' : '支付宝' }}</span>
|
||||
</div>
|
||||
<!-- 扫码提示 -->
|
||||
<h3 class="member-dialog__qrcode-subtitle">扫码完成支付</h3>
|
||||
<p class="member-dialog__qrcode-desc">请使用{{ selectedPayment === 'wechat' ? '微信' : '支付宝' }} App 扫描二维码完成支付,完成后此窗口会自动关闭。</p>
|
||||
<!-- 二维码占位 -->
|
||||
<div class="member-dialog__qrcode-image">
|
||||
<!-- TODO: 替换为真实二维码图片 -->
|
||||
<div class="member-dialog__qrcode-placeholder"></div>
|
||||
</div>
|
||||
<!-- 金额 -->
|
||||
<div class="member-dialog__qrcode-amount">¥{{ currentPlan.price }}</div>
|
||||
<!-- 我已完成支付按钮 -->
|
||||
<button class="member-dialog__qrcode-confirm-btn" @click="handlePaymentDone">我已完成支付</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -83,7 +352,8 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import { ref, computed, watch, reactive } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
/** 组件 Props — 控制弹窗显示/隐藏 */
|
||||
const props = defineProps<{ modelValue: boolean }>()
|
||||
@@ -91,72 +361,153 @@ const props = defineProps<{ modelValue: boolean }>()
|
||||
/** 组件 Emits — 通知父组件更新 modelValue */
|
||||
defineEmits<{ (e: 'update:modelValue', value: boolean): void }>()
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
// ==================== 类型定义 ====================
|
||||
|
||||
/** 套餐项类型 */
|
||||
interface PlanItem {
|
||||
key: string
|
||||
name: string
|
||||
originalPrice: string
|
||||
price: string
|
||||
unit: string
|
||||
discount: string
|
||||
tag?: string
|
||||
recommend?: boolean
|
||||
perMonth?: string
|
||||
btnText: string
|
||||
orderDesc: string
|
||||
}
|
||||
|
||||
/** 支付方式类型 */
|
||||
interface PaymentMethod {
|
||||
key: string
|
||||
name: string
|
||||
icon: string
|
||||
}
|
||||
|
||||
/** FAQ 项类型 */
|
||||
interface FaqItem {
|
||||
question: string
|
||||
answer: string
|
||||
open: boolean
|
||||
}
|
||||
|
||||
// ==================== 状态 ====================
|
||||
|
||||
/** 当前选中的套餐 */
|
||||
const selectedPlan = ref('monthly')
|
||||
/** 当前视图(intro1/intro2/order1/order2/order3) */
|
||||
const currentView = ref<'intro1' | 'intro2' | 'order1' | 'order2' | 'order3'>('intro2')
|
||||
|
||||
/** 监听弹窗开关 — 打开时锁定背景滚动,关闭时恢复 */
|
||||
/** 当前选中的套餐 */
|
||||
const selectedPlan = ref('quarterly')
|
||||
|
||||
/** 当前选中的支付方式 */
|
||||
const selectedPayment = ref('wechat')
|
||||
|
||||
/** 是否同意协议 */
|
||||
const agreeProtocol = ref(false)
|
||||
|
||||
/** 是否显示二维码弹窗 */
|
||||
const showQrCode = ref(false)
|
||||
|
||||
/** 监听弹窗开关 — 打开时锁定背景滚动,关闭时恢复并重置状态 */
|
||||
watch(() => props.modelValue, (val) => {
|
||||
document.body.style.overflow = val ? 'hidden' : ''
|
||||
if (!val) {
|
||||
currentView.value = 'intro2'
|
||||
showQrCode.value = false
|
||||
agreeProtocol.value = false
|
||||
}
|
||||
})
|
||||
|
||||
// ==================== 计算属性 ====================
|
||||
|
||||
/** 当前选中的套餐对象 */
|
||||
const currentPlan = computed(() => {
|
||||
return plans.find(p => p.key === selectedPlan.value) || plans[1]
|
||||
})
|
||||
|
||||
// ==================== 常量数据 ====================
|
||||
|
||||
/** 套餐列表 */
|
||||
const plans: PlanItem[] = [
|
||||
{ key: 'quarterly', name: '季度会员', originalPrice: '¥149.97 / 3个月', price: '49.99', unit: '3个月', discount: '67%' },
|
||||
{ key: 'monthly', name: '月度会员', originalPrice: '¥49.99 / 1个月', price: '19.99', unit: '月', discount: '60%' },
|
||||
{ key: 'weekly', name: '周会员', originalPrice: '¥71.96 / 1个月', price: '17.99', unit: '1周', discount: '' },
|
||||
{ key: 'weekly', name: '周会员', price: '17.99', unit: '周', tag: '临时体验 / 急用一次', btnText: '立即体验', orderDesc: '适合临时体验,急用一次的求职者' },
|
||||
{ key: 'quarterly', name: '季度会员', price: '49.99', unit: '3个月', recommend: true, perMonth: '16.66', btnText: '开启完整求职冲刺', orderDesc: '推荐正在集中投递、准备面试的求职者,性价比最高' },
|
||||
{ key: 'monthly', name: '月度会员', price: '19.99', unit: '月', tag: '标准求职', btnText: '开始求职加速', orderDesc: '标准求职周期,持续使用 AI 求职加速能力' },
|
||||
]
|
||||
|
||||
/** 面试率提升工具列表 */
|
||||
const toolList = [
|
||||
'AI 求职助手',
|
||||
'AI 针对性简历优化',
|
||||
'1V1真人导师',
|
||||
'一键自动填写网申信息',
|
||||
'内推码',
|
||||
'实时岗位更新提醒',
|
||||
/** 求职加速能力列表 */
|
||||
const abilities = [
|
||||
{ icon: '◉', color: '#4FC2C9', text: '个性化岗位匹配,量大极速又精准' },
|
||||
{ icon: '◉', color: '#4FC2C9', text: '针对岗位 JD 自动优化简历,提高简历通过率' },
|
||||
{ icon: '⚡', color: '#4FC2C9', text: '自动填写申请表,每次投递节省 10-15 分钟' },
|
||||
{ icon: '↗', color: '#E85635', text: '内推码,提高简历被 HR 看到的概率' },
|
||||
{ icon: '⏰', color: '#777777', text: '第一时间提醒高匹配岗位,抢占面试机会' },
|
||||
]
|
||||
|
||||
/** 会员权益列表 */
|
||||
const memberBenefits = [
|
||||
{ badge: '无限', label: '自动化网申流程' },
|
||||
{ badge: '无限', label: '简历过筛率提升' },
|
||||
{ badge: '✓', label: '找工作不孤单' },
|
||||
{ badge: '无限', label: '每一次网申节约15Min' },
|
||||
{ badge: '无限', label: '自动填写最新内推码' },
|
||||
{ badge: '无限', label: '第一时间投递高质量岗位' },
|
||||
/** 会员能力对比数据 */
|
||||
const compareData = [
|
||||
{ name: 'AI 岗位匹配', free: '免费 3 天', pro: '不限次数' },
|
||||
{ name: '简历针对性优化', free: '免费 3 天', pro: '不限次数' },
|
||||
{ name: '自动填写网申', free: '免费 3 天', pro: '不限次数' },
|
||||
{ name: '内推码获取', free: '免费 3 天', pro: '不限次数' },
|
||||
{ name: '高匹配岗位推送', free: '免费 3 天', pro: '不限次数' },
|
||||
]
|
||||
|
||||
/** 免费权益列表 */
|
||||
const freeBenefits = [
|
||||
'受限',
|
||||
'2次/天',
|
||||
'不支持',
|
||||
'4次/天',
|
||||
'4次/天',
|
||||
'1次/天',
|
||||
/** 支付方式列表 */
|
||||
const paymentMethods: PaymentMethod[] = [
|
||||
{ key: 'wechat', name: '微信支付', icon: '💬' },
|
||||
{ key: 'alipay', name: '支付宝', icon: '🔷' },
|
||||
]
|
||||
|
||||
/** 订单权益列表 */
|
||||
const orderBenefits = [
|
||||
'AI 简历深度优化',
|
||||
'智能岗位匹配分析',
|
||||
'求职信快速生成',
|
||||
'面试准备指导',
|
||||
'投递进度追踪',
|
||||
'求职策略建议',
|
||||
]
|
||||
|
||||
/** 常见问题列表 */
|
||||
const faqList = reactive<FaqItem[]>([
|
||||
{ question: '支付后如何开通?', answer: '支付成功后立即生效,无需等待,登录账号即可使用全部会员权益。', open: true },
|
||||
{ question: '可以随时取消吗?', answer: '可以,您可以在设置中随时取消自动续费,当前周期内的权益仍然有效。', open: false },
|
||||
{ question: '支持哪些支付方式?', answer: '目前支持微信支付和支付宝两种支付方式。', open: false },
|
||||
{ question: '可以开发票吗?', answer: '支持开具电子发票,请在支付完成后联系客服申请。', open: false },
|
||||
])
|
||||
|
||||
// ==================== 事件处理 ====================
|
||||
|
||||
/** 点击立刻升级按钮 */
|
||||
/** 从介绍页点击升级按钮 — 进入下单步骤一 */
|
||||
function handleUpgrade(plan: PlanItem) {
|
||||
// TODO: 接入支付接口
|
||||
ElMessage.success(`正在跳转 ${plan.name} 支付页面...`)
|
||||
selectedPlan.value = plan.key
|
||||
currentView.value = 'order1'
|
||||
}
|
||||
|
||||
/** 点击立即开启求职加速 — 显示二维码弹窗 */
|
||||
function handleShowQrCode() {
|
||||
if (!agreeProtocol.value) return
|
||||
showQrCode.value = true
|
||||
}
|
||||
|
||||
/** 点击我已完成支付 — 进入确认支付结果步骤 */
|
||||
function handlePaymentDone() {
|
||||
showQrCode.value = false
|
||||
currentView.value = 'order2'
|
||||
// 模拟接口确认支付结果,成功后进入步骤三
|
||||
confirmPayment()
|
||||
}
|
||||
|
||||
/** 模拟确认支付结果接口 */
|
||||
async function confirmPayment() {
|
||||
// TODO: 替换为真实支付确认接口
|
||||
await new Promise(resolve => setTimeout(resolve, 2000))
|
||||
currentView.value = 'order3'
|
||||
}
|
||||
|
||||
/** 支付成功后跳转简历页 */
|
||||
function handleGoResume() {
|
||||
router.push('/resume')
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -66,16 +66,17 @@
|
||||
<div class="settings-dialog__member-card">
|
||||
<div class="settings-dialog__member-header">
|
||||
<div class="settings-dialog__member-title-row">
|
||||
<span class="settings-dialog__member-name">月度会员</span>
|
||||
<span class="settings-dialog__member-name">会员</span>
|
||||
<span class="settings-dialog__member-badge">查看详情</span>
|
||||
</div>
|
||||
<span class="settings-dialog__member-terms" @click="handleMemberTerms">会员条款</span>
|
||||
</div>
|
||||
<div class="settings-dialog__member-info-row">
|
||||
<span class="settings-dialog__member-price">
|
||||
¥19.99/月<span>将于2026年3月27日续费</span>
|
||||
¥19.99/月
|
||||
<!-- <span>将于2026年3月27日续费</span>-->
|
||||
</span>
|
||||
<button class="settings-dialog__member-manage-btn" @click="handleManageSubscription">管理我的订阅</button>
|
||||
<!-- <button class="settings-dialog__member-manage-btn" @click="handleManageSubscription">管理我的订阅</button>-->
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-dialog__member-issue">
|
||||
@@ -92,7 +93,8 @@
|
||||
|
||||
<!-- Tab: 岗位更新提醒 — 目标岗位、即时提醒开关、提醒频率 -->
|
||||
<template v-if="activeTab === 'reminder'">
|
||||
<h2 class="settings-dialog__content-title">岗位更新提醒</h2>
|
||||
<!-- <h2 class="settings-dialog__content-title">岗位更新提醒</h2>-->
|
||||
<h2 class="settings-dialog__content-title">目标岗位设置</h2>
|
||||
<div class="settings-dialog__reminder-block">
|
||||
<div class="settings-dialog__reminder-block-title-row">
|
||||
<span class="settings-dialog__reminder-block-title">目标岗位</span>
|
||||
|
||||
+165
-6
@@ -35,13 +35,39 @@
|
||||
<!-- 消息通知弹窗 -->
|
||||
<Teleport to="body">
|
||||
<div v-if="showMessageDialog" class="side-nav__dialog-overlay" @click="showMessageDialog = false">
|
||||
<div class="side-nav__dialog" @click.stop>
|
||||
<div class="side-nav__dialog-header">
|
||||
<div class="side-nav__message-dialog" @click.stop>
|
||||
<!-- 弹窗头部 -->
|
||||
<div class="side-nav__message-dialog-header">
|
||||
<span>消息通知</span>
|
||||
<span class="side-nav__dialog-close" @click="showMessageDialog = false">✕</span>
|
||||
</div>
|
||||
<div class="side-nav__dialog-body">
|
||||
<p>暂无新消息</p>
|
||||
<!-- 弹窗内容区:左列表 + 右详情 -->
|
||||
<div class="side-nav__message-dialog-content">
|
||||
<!-- 左侧消息列表 -->
|
||||
<div class="side-nav__message-list" @scroll="handleMessageListScroll">
|
||||
<div
|
||||
v-for="(msg, idx) in messageList"
|
||||
:key="msg.id"
|
||||
class="side-nav__message-list-item"
|
||||
:class="{ 'side-nav__message-list-item--active': selectedMessageIdx === idx }"
|
||||
@click="selectedMessageIdx = idx"
|
||||
>
|
||||
<span class="side-nav__message-list-title">{{ msg.title }}</span>
|
||||
<span v-if="!msg.read" class="side-nav__message-unread-dot"></span>
|
||||
</div>
|
||||
<!-- 加载中提示 -->
|
||||
<div v-if="messageLoading" class="side-nav__message-list-loading">加载中...</div>
|
||||
<!-- 无消息提示 -->
|
||||
<div v-if="!messageLoading && messageList.length === 0" class="side-nav__message-list-empty">暂无消息</div>
|
||||
</div>
|
||||
<!-- 右侧消息详情 -->
|
||||
<div class="side-nav__message-detail">
|
||||
<template v-if="currentMessage">
|
||||
<div class="side-nav__message-detail-title">{{ currentMessage.title }}</div>
|
||||
<div class="side-nav__message-detail-content">{{ currentMessage.content }}</div>
|
||||
</template>
|
||||
<div v-else class="side-nav__message-detail-empty">请选择一条消息查看</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -91,11 +117,13 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import { computed, ref, watch, onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useStore } from 'vuex'
|
||||
import SettingsDialog from '@/components/SettingsDialog.vue'
|
||||
import { checkLogin } from '@/api/auth'
|
||||
import { fetchMessageList, fetchUnreadCount, markMessageRead } from '@/api/message'
|
||||
import type { MessageDto } from '@/api/message'
|
||||
import navJobsIcon from '@/assets/images/nav/nav-jobs-icon.png'
|
||||
import navResumeIcon from '@/assets/images/nav/nav-resume-icon.png'
|
||||
import navProfileIcon from '@/assets/images/nav/nav-profile-icon.png'
|
||||
@@ -167,6 +195,137 @@ const showSettingsDialog = ref(false)
|
||||
const feedbackType = ref('')
|
||||
const feedbackDetail = ref('')
|
||||
|
||||
// ==================== 站内信相关 ====================
|
||||
/** 未读消息数量 */
|
||||
const unreadCount = ref(0)
|
||||
/** 消息列表数据 */
|
||||
const messageList = ref<MessageDto[]>([])
|
||||
/** 当前选中的消息索引 */
|
||||
const selectedMessageIdx = ref(0)
|
||||
/** 当前页码 */
|
||||
const messagePageNum = ref(1)
|
||||
/** 是否还有更多数据 */
|
||||
const messageHasMore = ref(true)
|
||||
/** 加载状态 */
|
||||
const messageLoading = ref(false)
|
||||
|
||||
/** 当前选中的消息对象 */
|
||||
const currentMessage = computed(() => {
|
||||
return messageList.value[selectedMessageIdx.value] || null
|
||||
})
|
||||
|
||||
/**
|
||||
* 获取未读消息数量
|
||||
*/
|
||||
async function loadUnreadCount() {
|
||||
try {
|
||||
const res = await fetchUnreadCount()
|
||||
// 接口返回结构: { code, data: number }
|
||||
unreadCount.value = res.data ?? 0
|
||||
} catch (e) {
|
||||
console.error('获取未读消息数量失败', e)
|
||||
}
|
||||
}
|
||||
|
||||
// 组件挂载时获取未读消息数量
|
||||
onMounted(() => {
|
||||
loadUnreadCount()
|
||||
})
|
||||
|
||||
/**
|
||||
* 加载消息列表(分页追加)
|
||||
*/
|
||||
async function loadMessages() {
|
||||
if (messageLoading.value || !messageHasMore.value) return
|
||||
messageLoading.value = true
|
||||
try {
|
||||
const res = await fetchMessageList({ pageNum: messagePageNum.value, pageSize: 20 })
|
||||
// 接口返回结构: { code, msg, data: { pageNum, pageSize, total, list } }
|
||||
const pageData = res.data
|
||||
if (pageData && pageData.list && pageData.list.length > 0) {
|
||||
messageList.value.push(...pageData.list)
|
||||
// 判断是否还有更多
|
||||
messageHasMore.value = messageList.value.length < pageData.total
|
||||
messagePageNum.value++
|
||||
} else {
|
||||
messageHasMore.value = false
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('加载消息失败', e)
|
||||
} finally {
|
||||
messageLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 消息列表滚动到底部时加载下一页
|
||||
*/
|
||||
function handleMessageListScroll(e: Event) {
|
||||
const el = e.target as HTMLElement
|
||||
if (el.scrollTop + el.clientHeight >= el.scrollHeight - 10) {
|
||||
loadMessages()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新消息列表数据(保持当前选中的消息不变)
|
||||
*/
|
||||
async function refreshMessageList(keepMessageId: number | string) {
|
||||
try {
|
||||
// 重新请求已加载的所有页数据
|
||||
const totalLoaded = messageList.value.length
|
||||
const res = await fetchMessageList({ pageNum: 1, pageSize: totalLoaded })
|
||||
const pageData = res.data
|
||||
if (pageData && pageData.list) {
|
||||
messageList.value = pageData.list
|
||||
// 根据之前选中的消息ID恢复选中状态
|
||||
const idx = messageList.value.findIndex((m: MessageDto) => String(m.id) === String(keepMessageId))
|
||||
selectedMessageIdx.value = idx >= 0 ? idx : 0
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('刷新消息列表失败', e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 上报消息已读:当右侧显示某条消息时,如果该消息未读则标记已读
|
||||
* 标记成功后刷新列表数据和未读数量
|
||||
*/
|
||||
async function handleMarkRead(msg: MessageDto) {
|
||||
if (msg.read) return
|
||||
try {
|
||||
await markMessageRead(msg.id)
|
||||
// 刷新列表状态,保持当前选中
|
||||
await refreshMessageList(msg.id)
|
||||
// 更新未读数量
|
||||
loadUnreadCount()
|
||||
} catch (e) {
|
||||
console.error('标记已读失败', e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 监听当前选中消息变化,触发已读上报
|
||||
*/
|
||||
watch(currentMessage, (msg) => {
|
||||
if (msg && !msg.read) {
|
||||
handleMarkRead(msg)
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* 监听弹窗打开,重置并加载第一页消息
|
||||
*/
|
||||
watch(showMessageDialog, (val) => {
|
||||
if (val) {
|
||||
messageList.value = []
|
||||
messagePageNum.value = 1
|
||||
messageHasMore.value = true
|
||||
selectedMessageIdx.value = 0
|
||||
loadMessages()
|
||||
}
|
||||
})
|
||||
|
||||
const feedbackOptions = ['Bug反馈', '功能建议', '使用体验', '订阅及会员权益相关问题', '其它']
|
||||
|
||||
function handleFeedbackSubmit() {
|
||||
@@ -194,7 +353,7 @@ const settingsMenu = computed(() => {
|
||||
})
|
||||
|
||||
const footerMenus = computed(() => [
|
||||
{ iconImg: navMessageIcon, label: '消息通知', badge: 'NEW', action: () => { showMessageDialog.value = true } },
|
||||
{ iconImg: navMessageIcon, label: '消息通知', badge: unreadCount.value > 0 ? 'NEW' : '', action: () => { showMessageDialog.value = true } },
|
||||
{ iconImg: settingsMenu.value.iconImg, label: settingsMenu.value.label, action: () => { handleSettingsNav() } },
|
||||
{ iconImg: navFeedbackIcon, label: '反馈', action: () => { showFeedbackDialog.value = true } },
|
||||
])
|
||||
|
||||
+24
-1
@@ -27,6 +27,12 @@ const router = createRouter({
|
||||
routes: staticRoutes,
|
||||
})
|
||||
|
||||
/**
|
||||
* 上次静默校验登录状态的时间戳,用于节流(5 分钟内不重复请求)
|
||||
*/
|
||||
let lastCheckTime = 0
|
||||
const CHECK_INTERVAL = 5 * 60 * 1000 // 5 分钟
|
||||
|
||||
/**
|
||||
* 全局前置守卫 — 核心逻辑:
|
||||
*
|
||||
@@ -34,7 +40,7 @@ const router = createRouter({
|
||||
* 2. 需要鉴权的路由 → 调 checkLogin 接口验证 Cookie 是否有效
|
||||
* - 有效:同步 isAuthenticated = true,放行
|
||||
* - 无效:同步 isAuthenticated = false,弹登录框,阻止导航
|
||||
* 3. 不需要鉴权的路由 → 直接放行
|
||||
* 3. 不需要鉴权的路由 → 静默同步登录状态(不阻止导航、不弹登录框),5 分钟内节流
|
||||
*/
|
||||
router.beforeEach(async (to, _from, next) => {
|
||||
// 动态路由只需加载一次,与登录状态无关
|
||||
@@ -53,6 +59,7 @@ router.beforeEach(async (to, _from, next) => {
|
||||
if (to.meta?.requiresAuth) {
|
||||
try {
|
||||
const res = await checkLogin()
|
||||
lastCheckTime = Date.now()
|
||||
if (res.code === '0' && res.data === true) {
|
||||
// Cookie 有效,同步前端状态并放行
|
||||
store.commit('SET_AUTHENTICATED', true)
|
||||
@@ -72,6 +79,22 @@ router.beforeEach(async (to, _from, next) => {
|
||||
return
|
||||
}
|
||||
|
||||
// 非鉴权路由 — 静默同步登录状态(节流:5 分钟内不重复请求)
|
||||
if (Date.now() - lastCheckTime > CHECK_INTERVAL) {
|
||||
checkLogin()
|
||||
.then((res) => {
|
||||
lastCheckTime = Date.now()
|
||||
if (res.code === '0' && res.data === true) {
|
||||
store.commit('SET_AUTHENTICATED', true)
|
||||
} else {
|
||||
store.commit('SET_AUTHENTICATED', false)
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
store.commit('SET_AUTHENTICATED', false)
|
||||
})
|
||||
}
|
||||
|
||||
next()
|
||||
})
|
||||
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* 求职意向名称解析工具
|
||||
* 从 store 中的分类树数据里,根据意向 ID/Code 查找对应名称
|
||||
*/
|
||||
import store from '@/stores'
|
||||
|
||||
/**
|
||||
* 获取意向岗位类型名称列表
|
||||
* 根据 jobIntention.categoryIds 在 jobCategories 树中查找名称
|
||||
*/
|
||||
export function getIntentionCategoryNames(): string[] {
|
||||
const ids = store.state.jobIntention.categoryIds || []
|
||||
if (ids.length === 0) return []
|
||||
const names: string[] = []
|
||||
for (const group of store.state.jobCategories) {
|
||||
for (const child of group.children || []) {
|
||||
for (const leaf of child.children || []) {
|
||||
if (ids.includes(Number(leaf.id))) names.push(leaf.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取意向城市名称列表
|
||||
* 根据 jobIntention.regionCodes 在 regions 树中查找名称
|
||||
*/
|
||||
export function getIntentionRegionNames(): string[] {
|
||||
const codes = store.state.jobIntention.regionCodes || []
|
||||
if (codes.length === 0) return []
|
||||
const names: string[] = []
|
||||
for (const province of store.state.regions) {
|
||||
if (codes.includes(province.code)) { names.push(province.name); continue }
|
||||
for (const city of province.children || []) {
|
||||
if (codes.includes(city.code)) names.push(city.name)
|
||||
}
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取意向行业名称列表
|
||||
* 根据 jobIntention.industryIds 在 industries 树中查找名称
|
||||
*/
|
||||
export function getIntentionIndustryNames(): string[] {
|
||||
const ids = store.state.jobIntention.industryIds || []
|
||||
if (ids.length === 0) return []
|
||||
const names: string[] = []
|
||||
for (const group of store.state.industries) {
|
||||
if (ids.includes(Number(group.id))) { names.push(group.name); continue }
|
||||
for (const child of group.children || []) {
|
||||
if (ids.includes(Number(child.id))) names.push(child.name)
|
||||
}
|
||||
}
|
||||
return names
|
||||
}
|
||||
@@ -54,13 +54,12 @@ export function exportResumeWord(element: HTMLElement, fileName: string) {
|
||||
.resume-html__divider { height: 1.5px; background: #000; margin-bottom: 9.6pt; }
|
||||
.resume-html__summary { font-size: 12pt; line-height: 1.7; margin-bottom: 4.8pt; }
|
||||
.resume-html__item { margin-bottom: 9.6pt; }
|
||||
.resume-html__item-header { display: flex; align-items: flex-start; justify-content: space-between; margin-bottom: 3.6pt; }
|
||||
.resume-html__item-left { display: flex; flex-direction: column; gap: 1.2pt; }
|
||||
.resume-html__item-header { display: flex; align-items: flex-start; justify-content: space-between; margin-bottom: 2pt; }
|
||||
.resume-html__item-main { font-size: 13.2pt; font-weight: 600; color: #000; line-height: 1.4; }
|
||||
.resume-html__item-right { display: flex; align-items: center; gap: 4.8pt; flex-shrink: 0; text-align: right; }
|
||||
.resume-html__item-location { font-size: 12pt; color: #000; }
|
||||
.resume-html__item-date { font-size: 12pt; color: #000; white-space: nowrap; }
|
||||
.resume-html__item-desc { font-size: 12pt; color: #000; line-height: 1.5; }
|
||||
.resume-html__item-desc { font-size: 12pt; color: #000; line-height: 1.7; margin-top: 2pt; }
|
||||
.resume-html__desc-list { margin: 0; padding-left: 0; list-style: none; mso-list: none; }
|
||||
.resume-html__desc-list li { font-size: 12pt; line-height: 1.7; color: #000; list-style: none; mso-list: none; margin-left: 0; padding-left: 24pt; text-indent: 0; }
|
||||
.resume-html__skills { font-size: 12pt; line-height: 1.7; }
|
||||
|
||||
+579
-79
@@ -28,26 +28,37 @@
|
||||
</svg>
|
||||
</div>
|
||||
<!-- 状态标签 -->
|
||||
<span class="agent-main__status-tag">准备就绪</span>
|
||||
<span v-if="applyJobList.length > 0" class="agent-main__status-tag">准备就绪</span>
|
||||
<!-- 提示文字 -->
|
||||
<span class="agent-main__status-text">已添加一个岗位,随时可以开始投递</span>
|
||||
<!-- 开始按钮 -->
|
||||
<button class="agent-main__start-btn">开始 ▸</button>
|
||||
<span class="agent-main__status-text">{{ applyJobList.length > 0 ? `已添加${applyJobList.length}个岗位,随时可以开始投递` : '请添加岗位' }}</span>
|
||||
<!-- 开始按钮(投递中时置灰不可点击,暂停时显示继续可点击) -->
|
||||
<button
|
||||
class="agent-main__start-btn"
|
||||
:disabled="isApplying && !isPaused"
|
||||
@click="handleTopBarStartClick"
|
||||
>{{ isApplying ? (isPaused ? '继续' : '投递中...') : '开始 ▸' }}</button>
|
||||
<!-- 右侧工具按钮 -->
|
||||
<div class="agent-main__tools">
|
||||
<!-- 设置按钮 -->
|
||||
<button class="agent-main__tool-btn" title="设置">
|
||||
<!-- 待投递列表按钮 -->
|
||||
<button class="agent-main__tool-btn" title="待投递列表" @click.stop="toggleTaskListDropdown">
|
||||
<svg viewBox="0 0 24 24" fill="none">
|
||||
<path d="M4 6h16M4 12h16M4 18h16" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
|
||||
</svg>
|
||||
</button>
|
||||
<!-- 待投递列表下拉弹窗 -->
|
||||
<AgentTaskListDropdown
|
||||
v-if="showTaskListDropdown"
|
||||
:pending-list="applyJobList"
|
||||
@removed="handleTaskRemoved"
|
||||
@view-all="showTaskListDropdown = false"
|
||||
/>
|
||||
<!-- 齿轮按钮 -->
|
||||
<button class="agent-main__tool-btn" title="配置">
|
||||
<svg viewBox="0 0 24 24" fill="none">
|
||||
<circle cx="12" cy="12" r="3" stroke="currentColor" stroke-width="1.5" />
|
||||
<path d="M12 1v2m0 18v2m-9-11h2m18 0h2m-3.3-7.7-1.4 1.4M4.7 19.3l1.4-1.4m0-11.8L4.7 4.7m14.6 14.6-1.4-1.4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
|
||||
</svg>
|
||||
</button>
|
||||
<!-- <button class="agent-main__tool-btn" title="配置">-->
|
||||
<!-- <svg viewBox="0 0 24 24" fill="none">-->
|
||||
<!-- <circle cx="12" cy="12" r="3" stroke="currentColor" stroke-width="1.5" />-->
|
||||
<!-- <path d="M12 1v2m0 18v2m-9-11h2m18 0h2m-3.3-7.7-1.4 1.4M4.7 19.3l1.4-1.4m0-11.8L4.7 4.7m14.6 14.6-1.4-1.4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />-->
|
||||
<!-- </svg>-->
|
||||
<!-- </button>-->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -69,16 +80,26 @@
|
||||
<div v-else-if="msg.type === 'recommend'" class="agent-main__chat-row">
|
||||
<!-- 如果 extra 里有岗位数据,用岗位列表组件展示 -->
|
||||
<AgentChatJobList
|
||||
v-if="getRecommendJobsFromExtra(msg.extra).length > 0"
|
||||
:summary="getRecommendSummaryFromExtra(msg.extra)"
|
||||
:jobs="getRecommendJobsFromExtra(msg.extra)"
|
||||
@view-all="handleOpenRecommendPanel(msg.id)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 投递进度消息 -->
|
||||
<div v-else-if="msg.type === 'apply_progress'" class="agent-main__chat-row">
|
||||
<div class="agent-main__chat-bubble">{{ msg.content }}</div>
|
||||
<!-- 投递进度消息 — 仅显示最新 recommend 之后的 apply_progress -->
|
||||
<div v-else-if="msg.type === 'apply_progress' && isVisibleApplyProgress(msg.id)" class="agent-main__chat-row">
|
||||
<AgentApplyProgress
|
||||
:job-info="getApplyProgressJobInfo(msg.extra)"
|
||||
:current-step="getApplyProgressStep(msg.extra)"
|
||||
:resume-name="getApplyProgressResumeName(msg.extra)"
|
||||
:paused="isPaused"
|
||||
@cancel="handleCancelApply(msg)"
|
||||
@confirm-resume="handleConfirmResumeStep(msg)"
|
||||
@skip="handleSkipApply(msg)"
|
||||
@applied="handleAppliedStep(msg)"
|
||||
@go-apply="handleGoApplyStep(msg)"
|
||||
@toggle-pause="handleTogglePause"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -105,9 +126,11 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧匹配岗位添加面板 -->
|
||||
<!-- 右侧匹配岗位添加面板 / 简历生成进度 / 简历预览 -->
|
||||
<div v-if="showRightPanel" class="agent-main__right">
|
||||
<!-- 模式1:匹配岗位添加面板 -->
|
||||
<AgentMatchJobAdd
|
||||
v-if="rightPanelMode === 'recommend'"
|
||||
:jobs="activeRecommendJobs"
|
||||
:loading-job-ids="loadingJobIds"
|
||||
:panel-loading="panelLoading"
|
||||
@@ -116,6 +139,21 @@
|
||||
@toggle="handleToggleJobApply"
|
||||
@add-all="handleAddAllJobs"
|
||||
/>
|
||||
<!-- 模式2:简历生成进度 -->
|
||||
<div v-else-if="rightPanelMode === 'generating'" class="agent-resume-generating">
|
||||
<div class="agent-resume-generating__progress-bar">
|
||||
<div class="agent-resume-generating__progress-fill" :style="{ width: generateProgress + '%' }"></div>
|
||||
</div>
|
||||
<div class="agent-resume-generating__title">正在生成岗位专属简历</div>
|
||||
<div class="agent-resume-generating__hint">
|
||||
<svg viewBox="0 0 16 16" fill="none" class="agent-resume-generating__hint-icon"><circle cx="8" cy="8" r="7" stroke="currentColor" stroke-width="1.2"/><path d="M8 5v3M8 10.5v.5" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/></svg>
|
||||
<span>生成简历大约需要10-20秒</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 模式3:简历预览 -->
|
||||
<div v-else-if="rightPanelMode === 'resume'" class="agent-main__right-resume">
|
||||
<JobResumeTemplate :resume-data="applyResumeData" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -128,12 +166,16 @@ import SideNav from '@/components/SideNav.vue'
|
||||
import AgentSetupWizard from '@/components/AgentSetupWizard.vue'
|
||||
import AgentChatJobList from '@/components/AgentChatJobList.vue'
|
||||
import AgentMatchJobAdd from '@/components/AgentMatchJobAdd.vue'
|
||||
import { fetchAgentConfig, saveAgentConfig, fetchAgentRecommend, fetchAgentChatMessages, addAgentChatMessage, applyJob, cancelApplyJob, fetchApplyByJobIds, sendAgentChat } from '@/api/agent'
|
||||
import AgentApplyProgress from '@/components/AgentApplyProgress.vue'
|
||||
import AgentTaskListDropdown from '@/components/AgentTaskListDropdown.vue'
|
||||
import JobResumeTemplate from '@/components/JobResumeTemplate.vue'
|
||||
import type { ResumeTemplateData } from '@/components/JobResumeTemplate.vue'
|
||||
import { fetchAgentConfig, saveAgentConfig, fetchAgentRecommend, fetchAgentChatMessages, addAgentChatMessage, applyJob, cancelApplyJob, fetchApplyByJobIds, sendAgentChat, optimizeAgentResume } from '@/api/agent'
|
||||
import type { AgentConfig, AgentRecommendJob, AgentChatMessage, AgentChatHistoryItem } from '@/api/agent'
|
||||
import { fetchApplyList } from '@/api/jobs'
|
||||
import { fetchAgentTaskList } from '@/api/jobs'
|
||||
import type { JobListItem } from '@/api/jobs'
|
||||
import { fetchResumeList } from '@/api/resume'
|
||||
import store from '@/stores'
|
||||
import { getIntentionCategoryNames, getIntentionRegionNames, getIntentionIndustryNames } from '@/utils/intention'
|
||||
import AiThinkingIndicator from '@/components/tools/AiThinkingIndicator.vue'
|
||||
|
||||
/** 页面初始加载状态 */
|
||||
@@ -165,9 +207,12 @@ const activeRecommendJobs = computed(() => {
|
||||
/** 聊天输入框内容 */
|
||||
const chatInput = ref('')
|
||||
|
||||
/** 投递列表数据 */
|
||||
/** 待投递列表数据 */
|
||||
const applyJobList = ref<JobListItem[]>([])
|
||||
|
||||
/** 待投递列表下拉弹窗是否显示 */
|
||||
const showTaskListDropdown = ref(false)
|
||||
|
||||
/** 正在请求中的岗位 ID 列表(传给子组件控制按钮 loading) */
|
||||
const loadingJobIds = ref<number[]>([])
|
||||
|
||||
@@ -178,7 +223,15 @@ const panelLoading = ref(false)
|
||||
const defaultResumeId = ref('')
|
||||
|
||||
/** 是否正在发送消息(防止重复发送 + 显示等待状态) */
|
||||
const isSending = ref(false)/** 聊天区域 DOM 引用 */
|
||||
const isSending = ref(false)
|
||||
|
||||
/** 是否正在投递流程中(防止重复点击开始按钮) */
|
||||
const isApplying = ref(false)
|
||||
|
||||
/** 是否暂停投递 */
|
||||
const isPaused = ref(false)
|
||||
|
||||
/** 聊天区域 DOM 引用 */
|
||||
const chatAreaRef = ref<HTMLElement | null>(null)
|
||||
|
||||
/** 滚动聊天区域到底部(平滑滚动) */
|
||||
@@ -191,8 +244,15 @@ function scrollChatToBottom() {
|
||||
/** 页面挂载时查询配置 */
|
||||
onMounted(async () => {
|
||||
await loadAgentConfig()
|
||||
// 点击页面其他区域关闭待投递列表下拉
|
||||
document.addEventListener('click', closeTaskDropdownOnClickOutside)
|
||||
})
|
||||
|
||||
/** 点击外部关闭待投递列表下拉 */
|
||||
function closeTaskDropdownOnClickOutside() {
|
||||
showTaskListDropdown.value = false
|
||||
}
|
||||
|
||||
/** 查询求职助手配置 */
|
||||
async function loadAgentConfig() {
|
||||
pageLoading.value = true
|
||||
@@ -227,6 +287,20 @@ async function loadRecommendJobs() {
|
||||
if (res.code === '0' && res.data && res.data.list?.length > 0) {
|
||||
const summary = res.data.summary || ''
|
||||
const jobList = res.data.list
|
||||
|
||||
// if(jobList.length==0){
|
||||
// const aiNow = Math.floor(Date.now() / 1000)
|
||||
// chatMessages.value.push({
|
||||
// id: Date.now() + 1,
|
||||
// type: 'assistant',
|
||||
// content: '暂无相关职位推荐',
|
||||
// extra: '',
|
||||
// createTime: { seconds: aiNow, nanos: 0 },
|
||||
// })
|
||||
// try {
|
||||
// await addAgentChatMessage({ type: 'assistant', content: '暂无相关职位推荐' })
|
||||
// } catch { /* 静默 */ }
|
||||
// }
|
||||
/* 将完整岗位数据序列化为 extra JSON */
|
||||
const extraJson = JSON.stringify({ summary, list: jobList })
|
||||
|
||||
@@ -275,15 +349,31 @@ async function loadChatMessages() {
|
||||
}
|
||||
}
|
||||
|
||||
/** 加载岗位投递列表 */
|
||||
/** 加载求职助手任务列表(tab=1 进行中/待投递) */
|
||||
async function loadApplyList() {
|
||||
try {
|
||||
const res = await fetchApplyList({ pageNum: 1, pageSize: 100,status:0 })
|
||||
const res = await fetchAgentTaskList({ pageNum: 1, pageSize: 100, tab: 1 })
|
||||
if (res.code === '0' && res.data) {
|
||||
applyJobList.value = res.data.list || []
|
||||
}
|
||||
} catch {
|
||||
console.error('[Agent] 加载投递列表失败')
|
||||
console.error('[Agent] 加载求职助手任务列表失败')
|
||||
}
|
||||
}
|
||||
|
||||
/** 切换待投递列表下拉弹窗显隐 */
|
||||
function toggleTaskListDropdown() {
|
||||
showTaskListDropdown.value = !showTaskListDropdown.value
|
||||
if (showTaskListDropdown.value) {
|
||||
loadApplyList()
|
||||
}
|
||||
}
|
||||
|
||||
/** 岗位从待投递列表中移除后的回调 */
|
||||
function handleTaskRemoved(jobId: string) {
|
||||
const idx = applyJobList.value.findIndex(j => j.id === jobId)
|
||||
if (idx !== -1) {
|
||||
applyJobList.value.splice(idx, 1)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -320,58 +410,6 @@ function buildChatHistory(): AgentChatHistoryItem[] {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 store 中获取意向岗位类型名称列表
|
||||
* 根据 jobIntention.categoryIds 在 jobCategories 树中查找名称
|
||||
*/
|
||||
function getIntentionCategoryNames(): string[] {
|
||||
const ids = store.state.jobIntention.categoryIds || []
|
||||
if (ids.length === 0) return []
|
||||
const names: string[] = []
|
||||
for (const group of store.state.jobCategories) {
|
||||
for (const child of group.children || []) {
|
||||
for (const leaf of child.children || []) {
|
||||
if (ids.includes(Number(leaf.id))) names.push(leaf.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 store 中获取意向城市名称列表
|
||||
* 根据 jobIntention.regionCodes 在 regions 树中查找名称
|
||||
*/
|
||||
function getIntentionRegionNames(): string[] {
|
||||
const codes = store.state.jobIntention.regionCodes || []
|
||||
if (codes.length === 0) return []
|
||||
const names: string[] = []
|
||||
for (const province of store.state.regions) {
|
||||
if (codes.includes(province.code)) { names.push(province.name); continue }
|
||||
for (const city of province.children || []) {
|
||||
if (codes.includes(city.code)) names.push(city.name)
|
||||
}
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 store 中获取意向行业名称列表
|
||||
* 根据 jobIntention.industryIds 在 industries 树中查找名称
|
||||
*/
|
||||
function getIntentionIndustryNames(): string[] {
|
||||
const ids = store.state.jobIntention.industryIds || []
|
||||
if (ids.length === 0) return []
|
||||
const names: string[] = []
|
||||
for (const group of store.state.industries) {
|
||||
if (ids.includes(Number(group.id))) { names.push(group.name); continue }
|
||||
for (const child of group.children || []) {
|
||||
if (ids.includes(Number(child.id))) names.push(child.name)
|
||||
}
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
/**
|
||||
* 从消息的 extra JSON 中解析推荐岗位列表
|
||||
* extra 格式预期为 JSON 字符串,包含 list 字段
|
||||
@@ -478,6 +516,7 @@ async function handleToggleJobApply(job: AgentRecommendJob) {
|
||||
}
|
||||
/* 更新对话消息中的岗位状态 */
|
||||
updateJobStatusInMessages(job.id, isRemoving ? null : -1)
|
||||
await loadApplyList()
|
||||
ElMessage.success(isRemoving ? '岗位已从待投递移除' : '岗位已添加到待投递')
|
||||
} catch {
|
||||
ElMessage.error('操作失败,请重试')
|
||||
@@ -507,6 +546,7 @@ async function handleAddAllJobs() {
|
||||
loadingJobIds.value = loadingJobIds.value.filter(id => id !== job.id)
|
||||
}
|
||||
}
|
||||
await loadApplyList()
|
||||
ElMessage.success('已全部添加到待投递')
|
||||
}
|
||||
|
||||
@@ -568,6 +608,451 @@ async function handleOpenRecommendPanel(msgId: number) {
|
||||
function handleCloseRecommendPanel() {
|
||||
showRightPanel.value = false
|
||||
activeRecommendMsgId.value = null
|
||||
rightPanelMode.value = 'recommend'
|
||||
}
|
||||
|
||||
// ==================== 投递进度流程 ====================
|
||||
|
||||
/** 右侧面板显示模式:recommend-岗位推荐 / generating-简历生成中 / resume-简历预览 */
|
||||
const rightPanelMode = ref<'recommend' | 'generating' | 'resume'>('recommend')
|
||||
|
||||
/** 简历生成进度百分比(0-100) */
|
||||
const generateProgress = ref(0)
|
||||
|
||||
/** 当前投递流程的简历数据 */
|
||||
const applyResumeData = ref<ResumeTemplateData>({} as ResumeTemplateData)
|
||||
|
||||
/** 当前投递流程的简历名称 */
|
||||
const applyResumeName = ref('')
|
||||
|
||||
/** 进度条定时器 */
|
||||
let progressTimer: ReturnType<typeof setInterval> | null = null
|
||||
|
||||
/** 启动模拟进度条(慢慢增长到 90%,接口返回后快速涨满) */
|
||||
function startProgressSimulation() {
|
||||
generateProgress.value = 0
|
||||
let elapsed = 0
|
||||
progressTimer = setInterval(() => {
|
||||
elapsed += 300
|
||||
// 前 5 秒涨到 30%,5-15 秒涨到 70%,15-25 秒涨到 90%
|
||||
if (generateProgress.value < 30) {
|
||||
generateProgress.value += 2
|
||||
} else if (generateProgress.value < 70) {
|
||||
generateProgress.value += 1
|
||||
} else if (generateProgress.value < 90) {
|
||||
generateProgress.value += 0.5
|
||||
}
|
||||
// 最多到 90%
|
||||
if (generateProgress.value >= 90) {
|
||||
generateProgress.value = 90
|
||||
}
|
||||
}, 300)
|
||||
}
|
||||
|
||||
/** 停止进度条并涨满 */
|
||||
function finishProgress() {
|
||||
if (progressTimer) {
|
||||
clearInterval(progressTimer)
|
||||
progressTimer = null
|
||||
}
|
||||
generateProgress.value = 100
|
||||
}
|
||||
|
||||
/** 学历文字转数字(接口返回中文,模板需要数字) */
|
||||
function degreeToNumber(degree?: string): number {
|
||||
const map: Record<string, number> = { '大专': 1, '本科': 2, '硕士': 3, '博士': 4 }
|
||||
return map[degree || ''] || 2
|
||||
}
|
||||
|
||||
/** 将优化简历接口返回数据映射为 ResumeTemplateData 格式 */
|
||||
function mapOptimizeResumeToTemplate(data: any): ResumeTemplateData {
|
||||
const r = data.resume || {}
|
||||
return {
|
||||
name: r.name || '未填写姓名',
|
||||
email: r.email || '',
|
||||
mobileNumber: r.mobileNumber || '',
|
||||
wechatNumber: r.wechatNumber || '',
|
||||
summary: r.summary || '',
|
||||
educations: (data.education || []).map((e: any) => ({
|
||||
school: e.school || '',
|
||||
major: e.major || '',
|
||||
degree: degreeToNumber(e.degree),
|
||||
startDate: e.startDate || '',
|
||||
endDate: e.endDate || '',
|
||||
description: (e.description || []).map((d: any) => ({ id: d.id, text: d.text || '' })),
|
||||
})),
|
||||
workExperiences: (data.work || []).map((w: any) => ({
|
||||
companyName: w.companyName || '',
|
||||
position: w.position || '',
|
||||
startDate: w.startDate || '',
|
||||
endDate: w.endDate || '',
|
||||
description: (w.description || []).map((d: any) => ({ id: d.id, text: d.text || '' })),
|
||||
})),
|
||||
internships: (data.internship || []).map((i: any) => ({
|
||||
companyName: i.companyName || '',
|
||||
position: i.position || '',
|
||||
startDate: i.startDate || '',
|
||||
endDate: i.endDate || '',
|
||||
description: (i.description || []).map((d: any) => ({ id: d.id, text: d.text || '' })),
|
||||
})),
|
||||
projects: (data.project || []).map((p: any) => ({
|
||||
projectName: p.projectName || '',
|
||||
companyName: p.companyName || '',
|
||||
role: p.role || '',
|
||||
startDate: p.startDate || '',
|
||||
endDate: p.endDate || '',
|
||||
description: (p.description || []).map((d: any) => ({ id: d.id, text: d.text || '' })),
|
||||
})),
|
||||
competitions: (data.competition || []).map((c: any) => ({
|
||||
competitionName: c.competitionName || '',
|
||||
award: c.award || '',
|
||||
awardDate: c.awardDate || '',
|
||||
description: (c.description || []).map((d: any) => ({ id: d.id, text: d.text || '' })),
|
||||
})),
|
||||
skills: r.skills || [],
|
||||
certificates: r.certificates || [],
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 点击"开始"按钮 — 启动投递流程
|
||||
* 1. 刷新待投递列表
|
||||
* 2. 取第一个岗位
|
||||
* 3. 创建 apply_progress 消息
|
||||
* 4. 开始第1步:优化简历
|
||||
*/
|
||||
async function handleStartApply() {
|
||||
// 防抖:投递中不可重复点击
|
||||
if (isApplying.value) return
|
||||
isApplying.value = true
|
||||
|
||||
// 刷新待投递列表
|
||||
await loadApplyList()
|
||||
|
||||
if (applyJobList.value.length === 0) {
|
||||
ElMessage.warning('暂无待投递岗位,请先添加岗位')
|
||||
isApplying.value = false
|
||||
return
|
||||
}
|
||||
|
||||
// 取第一个岗位
|
||||
const targetJob = applyJobList.value[0]
|
||||
|
||||
// 构建 extra 数据
|
||||
const extraData = {
|
||||
jobInfo: targetJob,
|
||||
resumeInfo: null as any,
|
||||
step: 1,
|
||||
}
|
||||
|
||||
// 创建 apply_progress 消息并追加到对话列表
|
||||
const now = Math.floor(Date.now() / 1000)
|
||||
const applyMsg: AgentChatMessage = {
|
||||
id: Date.now(),
|
||||
type: 'apply_progress',
|
||||
content: '',
|
||||
extra: JSON.stringify(extraData),
|
||||
createTime: { seconds: now, nanos: 0 },
|
||||
}
|
||||
chatMessages.value.push(applyMsg)
|
||||
|
||||
await nextTick()
|
||||
scrollChatToBottom()
|
||||
|
||||
// ========== 第1步:优化简历 ==========
|
||||
// 打开右侧面板,显示生成进度
|
||||
rightPanelMode.value = 'generating'
|
||||
showRightPanel.value = true
|
||||
startProgressSimulation()
|
||||
|
||||
try {
|
||||
const res = await optimizeAgentResume({
|
||||
resumeId: defaultResumeId.value,
|
||||
jobId: targetJob.id,
|
||||
})
|
||||
|
||||
// 接口返回后涨满进度条
|
||||
finishProgress()
|
||||
|
||||
// 等待 1 秒让用户看到 100%
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
|
||||
// 解析简历数据 — 映射接口返回格式到 ResumeTemplateData
|
||||
const apiData = res?.data || res
|
||||
if (apiData) {
|
||||
const resumeResult = mapOptimizeResumeToTemplate(apiData)
|
||||
// 保存简历数据
|
||||
applyResumeData.value = resumeResult
|
||||
applyResumeName.value = resumeResult.name || '岗位专属简历'
|
||||
|
||||
// 更新 extra:存入 resumeInfo,step 进到 2
|
||||
extraData.resumeInfo = apiData
|
||||
extraData.step = 2
|
||||
applyMsg.extra = JSON.stringify(extraData)
|
||||
|
||||
// 切换右侧面板为简历预览
|
||||
rightPanelMode.value = 'resume'
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[Agent] 优化简历失败', e)
|
||||
ElMessage.error('简历优化失败,请重试')
|
||||
finishProgress()
|
||||
showRightPanel.value = false
|
||||
rightPanelMode.value = 'recommend'
|
||||
isApplying.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== apply_progress 消息辅助方法 ====================
|
||||
|
||||
/** 从 extra 中解析岗位信息 */
|
||||
function getApplyProgressJobInfo(extra: string): JobListItem | null {
|
||||
try {
|
||||
const parsed = JSON.parse(extra)
|
||||
return parsed.jobInfo || null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/** 从 extra 中解析当前步骤 */
|
||||
function getApplyProgressStep(extra: string): number {
|
||||
try {
|
||||
const parsed = JSON.parse(extra)
|
||||
return parsed.step || 1
|
||||
} catch {
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
/** 从 extra 中解析简历名称 */
|
||||
function getApplyProgressResumeName(extra: string): string {
|
||||
try {
|
||||
const parsed = JSON.parse(extra)
|
||||
return parsed.resumeInfo?.name || '岗位专属简历'
|
||||
} catch {
|
||||
return '岗位专属简历'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* apply_progress 显示模式
|
||||
* 'afterRecommend' — 显示最新 recommend 之后的所有 apply_progress
|
||||
* 'latestOnly' — 只显示最新一条 apply_progress
|
||||
* 切换此变量即可手动切换显示效果
|
||||
*/
|
||||
const applyProgressDisplayMode = ref<'afterRecommend' | 'latestOnly'>('latestOnly')
|
||||
|
||||
/**
|
||||
* 判断 apply_progress 消息是否应该显示
|
||||
*/
|
||||
function isVisibleApplyProgress(msgId: number): boolean {
|
||||
if (applyProgressDisplayMode.value === 'latestOnly') {
|
||||
// 只显示最新一条 apply_progress
|
||||
for (let i = chatMessages.value.length - 1; i >= 0; i--) {
|
||||
if (chatMessages.value[i].type === 'apply_progress') {
|
||||
return chatMessages.value[i].id === msgId
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// afterRecommend 模式:显示最新 recommend 之后的所有 apply_progress
|
||||
let lastRecommendIdx = -1
|
||||
for (let i = chatMessages.value.length - 1; i >= 0; i--) {
|
||||
if (chatMessages.value[i].type === 'recommend') {
|
||||
lastRecommendIdx = i
|
||||
break
|
||||
}
|
||||
}
|
||||
const msgIdx = chatMessages.value.findIndex(m => m.id === msgId)
|
||||
if (lastRecommendIdx === -1) return true
|
||||
return msgIdx > lastRecommendIdx
|
||||
}
|
||||
|
||||
// ==================== apply_progress 步骤事件处理 ====================
|
||||
|
||||
/** 顶部栏按钮点击 — 未投递时开始,暂停时恢复 */
|
||||
function handleTopBarStartClick() {
|
||||
if (isApplying.value && isPaused.value) {
|
||||
// 恢复投递
|
||||
isPaused.value = false
|
||||
} else if (!isApplying.value) {
|
||||
// 开始新的投递
|
||||
handleStartApply()
|
||||
}
|
||||
}
|
||||
|
||||
/** 暂停/恢复投递(由 AgentApplyProgress 组件触发) */
|
||||
function handleTogglePause() {
|
||||
isPaused.value = !isPaused.value
|
||||
}
|
||||
|
||||
/** 取消投递流程 */
|
||||
function handleCancelApply(msg: AgentChatMessage) {
|
||||
// 从消息列表中移除该条 apply_progress
|
||||
const idx = chatMessages.value.findIndex(m => m.id === msg.id)
|
||||
if (idx !== -1) {
|
||||
chatMessages.value.splice(idx, 1)
|
||||
}
|
||||
// 关闭右侧面板
|
||||
showRightPanel.value = false
|
||||
rightPanelMode.value = 'recommend'
|
||||
// 投递流程结束
|
||||
isApplying.value = false
|
||||
}
|
||||
|
||||
/** 第2步:确认简历 */
|
||||
function handleConfirmResumeStep(msg: AgentChatMessage) {
|
||||
try {
|
||||
const extraData = JSON.parse(msg.extra)
|
||||
// 勾选第2步,进入第3步
|
||||
extraData.step = 3
|
||||
msg.extra = JSON.stringify(extraData)
|
||||
} catch { /* 忽略 */ }
|
||||
}
|
||||
|
||||
/** 第3步:跳过投递 */
|
||||
function handleSkipApply(msg: AgentChatMessage) {
|
||||
try {
|
||||
const extraData = JSON.parse(msg.extra)
|
||||
// 勾选第3步,进入第4步(自动完成)
|
||||
extraData.step = 4
|
||||
msg.extra = JSON.stringify(extraData)
|
||||
// 持久化保存
|
||||
persistApplyProgressMessage(msg)
|
||||
// 自动开始下一个岗位投递(不关闭右侧面板,避免闪烁)
|
||||
autoStartNextApply()
|
||||
} catch { /* 忽略 */ }
|
||||
}
|
||||
|
||||
/** 第3步:我已投递 — 调用 /job/apply 接口 */
|
||||
async function handleAppliedStep(msg: AgentChatMessage) {
|
||||
try {
|
||||
const extraData = JSON.parse(msg.extra)
|
||||
const jobInfo = extraData.jobInfo
|
||||
if (!jobInfo) return
|
||||
|
||||
// 调用投递接口,status=0 表示已投递,jobId 传字符串避免大整数精度丢失
|
||||
const res = await applyJob({ jobId: String(jobInfo.id), status: 0 })
|
||||
if (res.code === '0') {
|
||||
// 勾选第3步,进入第4步
|
||||
extraData.step = 4
|
||||
msg.extra = JSON.stringify(extraData)
|
||||
ElMessage.success('投递成功')
|
||||
// 持久化保存
|
||||
persistApplyProgressMessage(msg)
|
||||
// 自动开始下一个岗位投递(不关闭右侧面板,避免闪烁)
|
||||
autoStartNextApply()
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[Agent] 投递失败', e)
|
||||
ElMessage.error('投递失败,请重试')
|
||||
}
|
||||
}
|
||||
|
||||
/** 第3步:去投递 — 打开原链接 */
|
||||
function handleGoApplyStep(msg: AgentChatMessage) {
|
||||
try {
|
||||
const extraData = JSON.parse(msg.extra)
|
||||
const sourceUrl = extraData.jobInfo?.sourceUrl
|
||||
if (sourceUrl) {
|
||||
window.open(sourceUrl, '_blank')
|
||||
}
|
||||
} catch { /* 忽略 */ }
|
||||
}
|
||||
|
||||
/** 持久化保存 apply_progress 消息到后端 */
|
||||
async function persistApplyProgressMessage(msg: AgentChatMessage) {
|
||||
try {
|
||||
await addAgentChatMessage({
|
||||
type: 'apply_progress',
|
||||
content: msg.content,
|
||||
extra: msg.extra,
|
||||
})
|
||||
} catch {
|
||||
console.error('[Agent] 保存投递进度消息失败')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 自动开始下一个岗位投递
|
||||
* 当前岗位完成(step=4)后,如果未暂停,自动刷新列表并开始下一个
|
||||
*/
|
||||
async function autoStartNextApply() {
|
||||
// 如果已暂停,不自动继续
|
||||
if (isPaused.value) {
|
||||
isApplying.value = false
|
||||
return
|
||||
}
|
||||
|
||||
// 刷新待投递列表
|
||||
await loadApplyList()
|
||||
|
||||
if (applyJobList.value.length === 0) {
|
||||
// 没有更多待投递岗位,结束流程
|
||||
isApplying.value = false
|
||||
showRightPanel.value = false
|
||||
rightPanelMode.value = 'recommend'
|
||||
ElMessage.success('所有待投递岗位已处理完毕')
|
||||
return
|
||||
}
|
||||
|
||||
// 取下一个岗位,开始新一轮投递
|
||||
const targetJob = applyJobList.value[0]
|
||||
const extraData = {
|
||||
jobInfo: targetJob,
|
||||
resumeInfo: null as any,
|
||||
step: 1,
|
||||
}
|
||||
|
||||
const now = Math.floor(Date.now() / 1000)
|
||||
const applyMsg: AgentChatMessage = {
|
||||
id: Date.now(),
|
||||
type: 'apply_progress',
|
||||
content: '',
|
||||
extra: JSON.stringify(extraData),
|
||||
createTime: { seconds: now, nanos: 0 },
|
||||
}
|
||||
chatMessages.value.push(applyMsg)
|
||||
|
||||
await nextTick()
|
||||
scrollChatToBottom()
|
||||
|
||||
// 第1步:优化简历
|
||||
rightPanelMode.value = 'generating'
|
||||
showRightPanel.value = true
|
||||
startProgressSimulation()
|
||||
|
||||
try {
|
||||
const res = await optimizeAgentResume({
|
||||
resumeId: defaultResumeId.value,
|
||||
jobId: targetJob.id,
|
||||
})
|
||||
|
||||
finishProgress()
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
|
||||
const apiData = res?.data || res
|
||||
if (apiData) {
|
||||
const resumeResult = mapOptimizeResumeToTemplate(apiData)
|
||||
applyResumeData.value = resumeResult
|
||||
applyResumeName.value = resumeResult.name || '岗位专属简历'
|
||||
|
||||
extraData.resumeInfo = apiData
|
||||
extraData.step = 2
|
||||
applyMsg.extra = JSON.stringify(extraData)
|
||||
|
||||
rightPanelMode.value = 'resume'
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[Agent] 优化简历失败', e)
|
||||
ElMessage.error('简历优化失败,请重试')
|
||||
finishProgress()
|
||||
showRightPanel.value = false
|
||||
rightPanelMode.value = 'recommend'
|
||||
isApplying.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 发送聊天消息 */
|
||||
@@ -644,11 +1129,11 @@ async function handleSendMessage() {
|
||||
const excludeJobIds = collectExcludeJobIds()
|
||||
try {
|
||||
const recRes = await fetchAgentRecommend({ preference, excludeJobIds })
|
||||
if (recRes.code === '0' && recRes.data && recRes.data.list?.length > 0) {
|
||||
if (recRes.code === '0' && recRes.data ) {
|
||||
const summary = recRes.data.summary || ''
|
||||
const jobList = recRes.data.list
|
||||
const extraJson = JSON.stringify({ summary, list: jobList })
|
||||
|
||||
// const contentV = recRes.data.list?.length==0?'':''
|
||||
/* 持久化 recommend 消息 */
|
||||
try {
|
||||
await addAgentChatMessage({ type: 'recommend', content: '', extra: extraJson })
|
||||
@@ -664,6 +1149,21 @@ async function handleSendMessage() {
|
||||
createTime: { seconds: recNow, nanos: 0 },
|
||||
})
|
||||
|
||||
// await nextTick()
|
||||
// scrollChatToBottom()
|
||||
// } else {
|
||||
// /* 推荐列表为空 — 插入提示消息 */
|
||||
// const emptyNow = Math.floor(Date.now() / 1000)
|
||||
// chatMessages.value.push({
|
||||
// id: Date.now() + 2,
|
||||
// type: 'assistant',
|
||||
// content: '暂无相关职位推荐',
|
||||
// extra: '',
|
||||
// createTime: { seconds: emptyNow, nanos: 0 },
|
||||
// })
|
||||
// try {
|
||||
// await addAgentChatMessage({ type: 'assistant', content: '暂无相关职位推荐' })
|
||||
// } catch { /* 静默 */ }
|
||||
await nextTick()
|
||||
scrollChatToBottom()
|
||||
}
|
||||
|
||||
+9
-25
@@ -28,32 +28,16 @@
|
||||
<p class="home-hero__desc">智能匹配职位、自动填写申请、量身定制简历、推荐内部人脉——不到1分钟,统统搞定!</p>
|
||||
<button class="home-hero__cta" @click="router.push('/jobs')">免费体验</button>
|
||||
</div>
|
||||
<!-- 右侧视频展示区 -->
|
||||
<div class="home-hero__right">
|
||||
<div class="home-hero__card">
|
||||
<div class="home-hero__card-dots">
|
||||
<span class="dot dot--red"></span>
|
||||
<span class="dot dot--yellow"></span>
|
||||
<span class="dot dot--green"></span>
|
||||
</div>
|
||||
<div class="home-hero__card-body">
|
||||
<div class="home-hero__card-user">
|
||||
<div class="home-hero__card-avatar"></div>
|
||||
<div class="home-hero__card-info">
|
||||
<h4>求职者 Alex</h4>
|
||||
<div class="home-hero__card-tags">
|
||||
<span class="tag">React</span>
|
||||
<span class="tag">Tailwind</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="home-hero__card-match">98%</div>
|
||||
</div>
|
||||
<div class="home-hero__card-lines">
|
||||
<div class="line-full"></div>
|
||||
<div class="line-3q"></div>
|
||||
</div>
|
||||
<div class="home-hero__card-result">为您匹配了 5 个高星职位</div>
|
||||
</div>
|
||||
</div>
|
||||
<video
|
||||
class="home-hero__video"
|
||||
src="https://jsxq-image-static.oss-cn-shenzhen.aliyuncs.com/aiJob/find/open-intention-video.mp4"
|
||||
autoplay
|
||||
loop
|
||||
muted
|
||||
playsinline
|
||||
></video>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
+1
-1
@@ -550,7 +550,7 @@ onBeforeUnmount(() => {
|
||||
})
|
||||
|
||||
/** 弹出菜单操作项 */
|
||||
const popupActions = ['从列表中移除', '已投递', '复制链接', '问题反馈']
|
||||
const popupActions = ['从列表中移除', '已投递', '复制链接']
|
||||
|
||||
// ==================== 职位列表项(扩展接口字段,增加前端交互状态) ====================
|
||||
|
||||
|
||||
+2
-1
@@ -41,7 +41,8 @@ export default defineConfig({
|
||||
changeOrigin: true,
|
||||
},
|
||||
'/ai-api': {
|
||||
target: 'http://192.168.31.133:8000',
|
||||
// target: 'http://192.168.31.133:8000',
|
||||
target: 'http://127.0.0.1:8000',
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/ai-api/, ''),
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user