1280 lines
48 KiB
Vue
1280 lines
48 KiB
Vue
<template>
|
||
<!-- 求职助手页面 -->
|
||
<div class="agent-page dflex">
|
||
<!-- 左侧导航栏 -->
|
||
<SideNav />
|
||
|
||
<!-- 主内容区域 -->
|
||
<div class="agent-page__content" v-loading="pageLoading" element-loading-text="加载中...">
|
||
<!-- 准备向导 — 第1步到第4步-->
|
||
<AgentSetupWizard
|
||
v-if="!pageLoading && showSetupWizard"
|
||
:initial-config="agentConfig"
|
||
@complete="handleSetupComplete"
|
||
@launch="handleLaunch"
|
||
/>
|
||
<!-- <AgentSetupWizard-->
|
||
<!-- v-if="true"-->
|
||
<!-- :initial-config="agentConfig"-->
|
||
<!-- @complete="handleSetupComplete"-->
|
||
<!-- @launch="handleLaunch"-->
|
||
<!-- />-->
|
||
|
||
|
||
<!-- 求职助手正式内容区域 -->
|
||
<div v-else-if="!pageLoading" class="agent-main">
|
||
<!-- 左侧主区域 -->
|
||
<div class="agent-main__left">
|
||
<!-- 顶部固定设置栏 -->
|
||
<div class="agent-main__top-bar">
|
||
<!-- 左侧状态信息区 -->
|
||
<div class="agent-main__top-bar-left">
|
||
<!-- 状态标签 -->
|
||
<span
|
||
class="agent-main__status-tag"
|
||
:class="{
|
||
'agent-main__status-tag--ready': applyJobList.length > 0 && !isApplying,
|
||
'agent-main__status-tag--applying': isApplying
|
||
}"
|
||
>{{ isApplying ? '投递中' : (applyJobList.length > 0 ? '准备就绪' : '待添加岗位') }}</span>
|
||
<!-- 提示文字 -->
|
||
<span class="agent-main__status-text">
|
||
<template v-if="isApplying">
|
||
当前进度
|
||
<span class="agent-main__status-progress">{{ applyProgressList.filter(p => p.step >= 4).length }} / {{ applyJobList.length + applyProgressList.length }}</span>
|
||
</template>
|
||
<template v-else-if="applyJobList.length > 0">
|
||
已添加 <span class="agent-main__status-count">{{ applyJobList.length }}</span> 个岗位
|
||
</template>
|
||
<template v-else>暂未添加岗位,请从下方推荐列表中选择</template>
|
||
</span>
|
||
</div>
|
||
<!-- 右侧按钮区 -->
|
||
<div class="agent-main__top-bar-right">
|
||
<!-- 查看任务按钮 -->
|
||
<div class="agent-main__tools">
|
||
<button class="agent-main__task-btn" title="查看任务" @click.stop="toggleTaskListDropdown">
|
||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||
<g clip-path="url(#clip0_421_1998)">
|
||
<path d="M8.33329 6.83301C8.33329 6.7004 8.38597 6.57322 8.47974 6.47945C8.57351 6.38569 8.70068 6.33301 8.83329 6.33301H11.1666C11.2992 6.33301 11.4264 6.38569 11.5202 6.47945C11.6139 6.57322 11.6666 6.7004 11.6666 6.83301C11.6666 6.96562 11.6139 7.09279 11.5202 7.18656C11.4264 7.28033 11.2992 7.33301 11.1666 7.33301H8.83329C8.70068 7.33301 8.57351 7.28033 8.47974 7.18656C8.38597 7.09279 8.33329 6.96562 8.33329 6.83301ZM8.83329 9.99967C8.70068 9.99967 8.57351 10.0524 8.47974 10.1461C8.38597 10.2399 8.33329 10.3671 8.33329 10.4997C8.33329 10.6323 8.38597 10.7595 8.47974 10.8532C8.57351 10.947 8.70068 10.9997 8.83329 10.9997H11.1666C11.2992 10.9997 11.4264 10.947 11.5202 10.8532C11.6139 10.7595 11.6666 10.6323 11.6666 10.4997C11.6666 10.3671 11.6139 10.2399 11.5202 10.1461C11.4264 10.0524 11.2992 9.99967 11.1666 9.99967H8.83329ZM7.18663 6.51967C7.23575 6.4739 7.27515 6.4187 7.30248 6.35737C7.32981 6.29603 7.3445 6.22982 7.34569 6.16269C7.34687 6.09555 7.33452 6.02887 7.30937 5.96661C7.28423 5.90435 7.2468 5.8478 7.19932 5.80032C7.15184 5.75284 7.09528 5.71541 7.03302 5.69026C6.97077 5.66511 6.90408 5.65276 6.83694 5.65395C6.76981 5.65513 6.7036 5.66983 6.64227 5.69715C6.58093 5.72448 6.52573 5.76388 6.47996 5.81301L5.49996 6.79301L5.18663 6.47967C5.09184 6.39136 4.96648 6.34327 4.83694 6.34556C4.70741 6.34784 4.58382 6.40032 4.49221 6.49193C4.4006 6.58354 4.34813 6.70713 4.34584 6.83666C4.34356 6.96619 4.39164 7.09156 4.47996 7.18634L5.14663 7.85301C5.24038 7.94664 5.36746 7.99923 5.49996 7.99923C5.63246 7.99923 5.75954 7.94664 5.85329 7.85301L7.18663 6.51967ZM7.18663 9.47968C7.28026 9.57343 7.33285 9.70051 7.33285 9.83301C7.33285 9.96551 7.28026 10.0926 7.18663 10.1863L5.85329 11.5197C5.75954 11.6133 5.63246 11.6659 5.49996 11.6659C5.36746 11.6659 5.24038 11.6133 5.14663 11.5197L4.47996 10.853C4.43083 10.8072 4.39143 10.752 4.36411 10.6907C4.33678 10.6294 4.32208 10.5632 4.3209 10.496C4.31971 10.4289 4.33206 10.3622 4.35721 10.2999C4.38236 10.2377 4.41979 10.1811 4.46727 10.1336C4.51475 10.0862 4.5713 10.0487 4.63356 10.0236C4.69582 9.99845 4.76251 9.9861 4.82964 9.98728C4.89678 9.98846 4.96299 10.0032 5.02432 10.0305C5.08565 10.0578 5.14085 10.0972 5.18663 10.1463L5.49996 10.4597L6.47996 9.47968C6.57371 9.38604 6.70079 9.33345 6.83329 9.33345C6.96579 9.33345 7.09288 9.38604 7.18663 9.47968ZM10.6626 2.72234C10.6347 2.34469 10.4649 1.99158 10.1875 1.73389C9.90999 1.47619 9.54531 1.33299 9.16663 1.33301H6.83329C6.46433 1.33302 6.10832 1.46901 5.83332 1.71499C5.55832 1.96097 5.38362 2.29967 5.34263 2.66634H4.16663C3.7688 2.66634 3.38727 2.82438 3.10597 3.10568C2.82466 3.38699 2.66663 3.76852 2.66663 4.16634V13.1663C2.66663 13.5642 2.82466 13.9457 3.10597 14.227C3.38727 14.5083 3.7688 14.6663 4.16663 14.6663H11.8333C12.0303 14.6663 12.2253 14.6275 12.4073 14.5522C12.5893 14.4768 12.7547 14.3663 12.894 14.227C13.0332 14.0877 13.1437 13.9224 13.2191 13.7404C13.2945 13.5584 13.3333 13.3633 13.3333 13.1663V4.16634C13.3333 3.96936 13.2945 3.7743 13.2191 3.59232C13.1437 3.41033 13.0332 3.24497 12.894 3.10568C12.7547 2.96639 12.5893 2.8559 12.4073 2.78052C12.2253 2.70514 12.0303 2.66634 11.8333 2.66634H10.6573L10.6626 2.72234ZM10.6626 2.73034L10.6666 2.83301C10.6666 2.79834 10.6648 2.76412 10.6626 2.73034ZM6.83329 4.33301H9.16663C9.68663 4.33301 10.1446 4.06834 10.414 3.66634H11.8333C11.9659 3.66634 12.0931 3.71902 12.1868 3.81279C12.2806 3.90656 12.3333 4.03373 12.3333 4.16634V13.1663C12.3333 13.299 12.2806 13.4261 12.1868 13.5199C12.0931 13.6137 11.9659 13.6663 11.8333 13.6663H4.16663C4.03402 13.6663 3.90684 13.6137 3.81307 13.5199C3.7193 13.4261 3.66663 13.299 3.66663 13.1663V4.16634C3.66663 4.03373 3.7193 3.90656 3.81307 3.81279C3.90684 3.71902 4.03402 3.66634 4.16663 3.66634H5.58596C5.85529 4.06834 6.31329 4.33301 6.83329 4.33301ZM6.83329 2.33301H9.16663C9.29923 2.33301 9.42641 2.38569 9.52018 2.47945C9.61395 2.57322 9.66663 2.7004 9.66663 2.83301C9.66663 2.96562 9.61395 3.09279 9.52018 3.18656C9.42641 3.28033 9.29923 3.33301 9.16663 3.33301H6.83329C6.70068 3.33301 6.57351 3.28033 6.47974 3.18656C6.38597 3.09279 6.33329 2.96562 6.33329 2.83301C6.33329 2.7004 6.38597 2.57322 6.47974 2.47945C6.57351 2.38569 6.70068 2.33301 6.83329 2.33301Z" fill="currentColor"/>
|
||
</g>
|
||
<defs>
|
||
<clipPath id="clip0_421_1998">
|
||
<rect width="16" height="16" fill="white"/>
|
||
</clipPath>
|
||
</defs>
|
||
</svg>
|
||
<span>查看任务</span>
|
||
</button>
|
||
<!-- 投递任务抽屉(从右侧弹出) -->
|
||
<AgentTaskListDropdown
|
||
:visible="showTaskListDrawer"
|
||
@close="handleCloseTaskListDrawer"
|
||
@update:visible="(val: boolean) => showTaskListDrawer = val"
|
||
@removed="handleTaskRemoved"
|
||
/>
|
||
</div>
|
||
<!-- 开始/暂停投递按钮 -->
|
||
<button
|
||
class="agent-main__action-btn"
|
||
:class="{
|
||
'agent-main__action-btn--start': !isApplying || isPaused,
|
||
'agent-main__action-btn--pause': isApplying && !isPaused
|
||
}"
|
||
@click="handleTopBarStartClick"
|
||
>
|
||
<!-- 开始投递 icon -->
|
||
<svg v-if="!isApplying || isPaused" width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||
<g clip-path="url(#clip0_421_2003)">
|
||
<path d="M3.33337 4.44455V11.5559C3.33337 12.1425 3.33337 12.4352 3.45671 12.6085C3.51007 12.6835 3.57837 12.7466 3.6573 12.7939C3.73624 12.8411 3.82411 12.8716 3.91537 12.8832C4.12604 12.9099 4.38404 12.7725 4.90204 12.4965L11.5687 8.94121C12.1414 8.63588 12.428 8.48255 12.522 8.27921C12.6034 8.10188 12.6034 7.89721 12.522 7.71988C12.428 7.51721 12.142 7.36455 11.5707 7.05988L11.568 7.05855L4.90137 3.50321C4.38471 3.22721 4.12604 3.08988 3.91471 3.11655C3.82356 3.12829 3.73582 3.15875 3.657 3.20602C3.57819 3.25328 3.50999 3.31633 3.45671 3.39121C3.33337 3.56455 3.33337 3.85788 3.33337 4.44455Z" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
|
||
</g>
|
||
<defs>
|
||
<clipPath id="clip0_421_2003">
|
||
<rect width="16" height="16" fill="white"/>
|
||
</clipPath>
|
||
</defs>
|
||
</svg>
|
||
|
||
<!-- 暂停投递 icon -->
|
||
<svg v-else width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||
<g clip-path="url(#clip0_409_2071)">
|
||
<path d="M6 3.33301H4.66667C4.29848 3.33301 4 3.63148 4 3.99967V11.9997C4 12.3679 4.29848 12.6663 4.66667 12.6663H6C6.36819 12.6663 6.66667 12.3679 6.66667 11.9997V3.99967C6.66667 3.63148 6.36819 3.33301 6 3.33301Z" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round"/>
|
||
<path d="M11.3334 3.33301H10C9.63185 3.33301 9.33337 3.63148 9.33337 3.99967V11.9997C9.33337 12.3679 9.63185 12.6663 10 12.6663H11.3334C11.7016 12.6663 12 12.3679 12 11.9997V3.99967C12 3.63148 11.7016 3.33301 11.3334 3.33301Z" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round"/>
|
||
</g>
|
||
<defs>
|
||
<clipPath id="clip0_409_2071">
|
||
<rect width="16" height="16" fill="white"/>
|
||
</clipPath>
|
||
</defs>
|
||
</svg>
|
||
<span>{{ isApplying ? (isPaused ? '开始投递' : '暂停投递') : '开始投递' }}</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 中间内容区域(可滚动) -->
|
||
<div ref="contentAreaRef" class="agent-main__chat-area">
|
||
<!-- 推荐岗位列表 -->
|
||
<AgentChatJobList
|
||
v-if="displayRecommendJobs.length > 0 || recommendSummary"
|
||
:summary="recommendSummary"
|
||
:jobs="displayRecommendJobs"
|
||
:loading-job-ids="loadingJobIds"
|
||
:refreshing="recommendRefreshing"
|
||
@click-job="handleChatJobClick"
|
||
@add="handleToggleJobApply"
|
||
@add-all="handleAddAllDisplayJobs"
|
||
@refresh="handleRefreshRecommend"
|
||
@dismiss="handleDismissJob"
|
||
/>
|
||
|
||
<!-- 投递进度列表 — 当前进行中 + 已完成的投递记录 -->
|
||
<div v-if="isApplying || completedApplyList.length > 0" class="agent-main__progress-list">
|
||
<!-- 标题区域 -->
|
||
<div class="agent-main__progress-list-header">
|
||
<div class="agent-main__progress-list-title">{{ isApplying ? '正在自动投递...' : '自动投递' }}</div>
|
||
<div class="agent-main__progress-list-desc">展示进入投递状态的岗位,岗位按时间顺序向下延展</div>
|
||
</div>
|
||
<!-- 可滚动列表区域 -->
|
||
<div class="agent-main__progress-list-body">
|
||
<!-- 当前进行中的岗位(只有一个) -->
|
||
<AgentApplyProgress
|
||
v-if="applyProgressList.length > 0"
|
||
:key="applyProgressList[0].id"
|
||
:job-info="applyProgressList[0].jobInfo"
|
||
:current-step="applyProgressList[0].step"
|
||
:step-phase="applyProgressList[0].stepPhase"
|
||
:match-score="applyProgressList[0].matchScore"
|
||
@view-resume="handleViewResumeStep"
|
||
@confirm-resume="handleConfirmResumeStep"
|
||
@not-apply="handleNotApplyStep"
|
||
@continue-apply="handleContinueApplyStep"
|
||
@go-apply="handleGoApplyStep"
|
||
@applied="handleAppliedStep"
|
||
/>
|
||
<!-- 已完成的投递记录(按时间降序) -->
|
||
<AgentApplyProgress
|
||
v-for="completed in completedApplyList"
|
||
:key="completed.id"
|
||
:job-info="completed.jobInfo"
|
||
:current-step="5"
|
||
:completed-time="completed.completedTime"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 右侧面板(常驻显示设置面板,其他面板覆盖其上) -->
|
||
<div class="agent-main__right">
|
||
<!-- 底层:设置面板(常驻) -->
|
||
<AgentSettingPanel />
|
||
|
||
<!-- 覆盖层已移除,简历预览改为独立抽屉(见页面底部) -->
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 求职目标设置弹窗 -->
|
||
<JobGoalDialog v-model="showJobGoalDialog" />
|
||
|
||
<!-- 岗位预览抽屉(从右侧弹出) -->
|
||
<AgentJobPreviewPanel
|
||
:visible="showJobPreviewDrawer"
|
||
:job-id="previewJobId"
|
||
:application-status="previewJobApplicationStatus"
|
||
@back="handleJobPreviewBack"
|
||
@add="handleJobPreviewAdd"
|
||
@remove="handleJobPreviewRemove"
|
||
@update:visible="(val: boolean) => showJobPreviewDrawer = val"
|
||
/>
|
||
|
||
<!-- 简历预览抽屉(从右侧弹出) -->
|
||
<Teleport to="body">
|
||
<Transition name="agent-resume-drawer">
|
||
<div v-if="showResumeDrawer" class="agent-resume-drawer-mask" @click.self="handleCloseResumeDrawer">
|
||
<div class="agent-resume-drawer">
|
||
<!-- 顶部标题栏 -->
|
||
<div class="agent-resume-drawer__header">
|
||
<div class="agent-resume-drawer__header-left">
|
||
<div class="agent-resume-drawer__close" @click="handleCloseResumeDrawer">
|
||
<svg viewBox="0 0 24 24" fill="none" class="agent-resume-drawer__close-icon">
|
||
<path d="M18 6L6 18M6 6l12 12" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||
</svg>
|
||
</div>
|
||
<span class="agent-resume-drawer__title">岗位专属优化简历</span>
|
||
</div>
|
||
<!-- 匹配提升提示 -->
|
||
<div class="agent-resume-drawer__score-info">
|
||
<span class="agent-resume-drawer__score-tip">
|
||
🎉 恭喜!你的简历匹配值
|
||
<br/>
|
||
从<span class="agent-resume-drawer__score-old">{{ resumeScoreBefore }}分</span>
|
||
提升到了<span class="agent-resume-drawer__score-new">{{ resumeScoreAfter }}分</span>!
|
||
</span>
|
||
<!-- 匹配度环 -->
|
||
<div class="agent-resume-drawer__score-ring">
|
||
<svg viewBox="0 0 60 60" class="agent-resume-drawer__ring-svg">
|
||
<circle cx="30" cy="30" r="25" stroke-width="3" stroke="#E8E8E8" fill="none" />
|
||
<circle cx="30" cy="30" r="25" stroke-width="3" fill="none"
|
||
:stroke="resumeScoreAfter >= 4 ? '#52CAD1' : '#F59E0B'" stroke-linecap="round"
|
||
:stroke-dasharray="2 * Math.PI * 25"
|
||
:stroke-dashoffset="2 * Math.PI * 25 * (1 - resumeScoreAfter / 10)"
|
||
transform="rotate(-90 30 30)"
|
||
/>
|
||
</svg>
|
||
<div class="agent-resume-drawer__ring-text">
|
||
<span class="agent-resume-drawer__ring-number">{{ resumeScoreAfter }}</span>
|
||
<span class="agent-resume-drawer__ring-label">{{ resumeScoreAfter >= 4 ? '高匹配度' : '匹配度' }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<!-- 简历内容 -->
|
||
<div class="agent-resume-drawer__body">
|
||
<JobResumeTemplate ref="applyResumeTemplateRef" :resume-data="applyResumeData" :mode="applyResumeMode" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</Transition>
|
||
</Teleport>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, onMounted, nextTick } from 'vue'
|
||
import { useRouter } from 'vue-router'
|
||
import { useStore } from 'vuex'
|
||
import SideNav from '@/components/SideNav.vue'
|
||
import AgentSetupWizard from '@/components/AgentSetupWizard.vue'
|
||
import AgentChatJobList from '@/components/AgentChatJobList.vue'
|
||
import AgentApplyProgress from '@/components/AgentApplyProgress.vue'
|
||
import AgentTaskListDropdown from '@/components/AgentTaskListDropdown.vue'
|
||
import JobResumeTemplate from '@/components/JobResumeTemplate.vue'
|
||
import AgentJobPreviewPanel from '@/components/AgentJobPreviewPanel.vue'
|
||
import AgentSettingPanel from '@/components/AgentSettingPanel.vue'
|
||
import type { ResumeTemplateData } from '@/components/JobResumeTemplate.vue'
|
||
import { fetchAgentConfig, saveAgentConfig, applyJob, cancelApplyJob, optimizeAgentResume, fetchMatchScore } from '@/api/agent'
|
||
import type { AgentConfig, AgentRecommendJob } from '@/api/agent'
|
||
import { fetchAgentTaskList, fetchJobList, fetchJobIntention, fetchCustomizeResume } from '@/api/jobs'
|
||
import type { JobListItem, JobListParams } from '@/api/jobs'
|
||
import { fetchResumeList } from '@/api/resume'
|
||
import { fetchMemberStatus } from '@/api/member'
|
||
import JobGoalDialog from '@/components/JobGoalDialog.vue'
|
||
|
||
// ==================== 路由 & Store ====================
|
||
const router = useRouter()
|
||
const store = useStore()
|
||
|
||
// ==================== sessionStorage 缓存 key ====================
|
||
const STORAGE_KEY = 'agent_apply_progress_list'
|
||
const COMPLETED_STORAGE_KEY = 'agent_completed_apply_list'
|
||
|
||
/** 投递进度数据项 */
|
||
interface ApplyProgressItem {
|
||
/** 唯一标识 */
|
||
id: number
|
||
/** 岗位信息 */
|
||
jobInfo: JobListItem
|
||
/** 当前步骤(1-4) */
|
||
step: number
|
||
/** 当前步骤阶段 */
|
||
stepPhase: 'loading' | 'done' | 'low-match' | 'ready' | 'confirm'
|
||
/** 简历名称 */
|
||
resumeName: string
|
||
/** 简历接口返回的原始数据(用于 ResumeTemplate 渲染) */
|
||
resumeData?: any
|
||
/** 简历缓存 storageKey */
|
||
resumeCacheKey?: string
|
||
/** 定制简历 ID(用于匹配度评分) */
|
||
customizeResumeId?: string
|
||
/** 匹配度分数 */
|
||
matchScore?: number
|
||
}
|
||
|
||
/** 已完成的投递记录项 */
|
||
interface CompletedApplyItem {
|
||
/** 唯一标识 */
|
||
id: number
|
||
/** 岗位信息 */
|
||
jobInfo: JobListItem
|
||
/** 投递完成时间 */
|
||
completedTime: string
|
||
}
|
||
|
||
/** 页面初始加载状态 */
|
||
const pageLoading = ref(true)
|
||
|
||
/** 是否显示准备向导 */
|
||
const showSetupWizard = ref(true)
|
||
|
||
/** 从接口查询到的配置数据 — 传给向导组件做初始值 */
|
||
const agentConfig = ref<AgentConfig | null>(null)
|
||
|
||
/** 推荐岗位全量列表(接口返回200个) */
|
||
const recommendJobs = ref<AgentRecommendJob[]>([])
|
||
|
||
/** 当前随机抽取展示的10个岗位 */
|
||
const displayRecommendJobs = ref<AgentRecommendJob[]>([])
|
||
|
||
/** 推荐说明文字 */
|
||
const recommendSummary = ref('')
|
||
|
||
/** 换一批加载状态 */
|
||
const recommendRefreshing = ref(false)
|
||
|
||
/** 待投递列表数据 */
|
||
const applyJobList = ref<JobListItem[]>([])
|
||
|
||
/** 正在请求中的岗位 ID 列表(传给子组件控制按钮 loading) */
|
||
const loadingJobIds = ref<(string | number)[]>([])
|
||
|
||
/** 默认简历 ID */
|
||
const defaultResumeId = ref('')
|
||
|
||
/** 是否正在投递流程中 */
|
||
const isApplying = ref(false)
|
||
|
||
/** 是否暂停投递 */
|
||
const isPaused = ref(false)
|
||
|
||
/** 是否显示求职目标设置弹窗 */
|
||
const showJobGoalDialog = ref(false)
|
||
|
||
/** 投递进度列表(当前进行中的,最多1个) */
|
||
const applyProgressList = ref<ApplyProgressItem[]>([])
|
||
|
||
/** 已完成的投递记录列表(按时间降序) */
|
||
const completedApplyList = ref<CompletedApplyItem[]>([])
|
||
|
||
/** 内容区域 DOM 引用 */
|
||
const contentAreaRef = ref<HTMLElement | null>(null)
|
||
|
||
/** 滚动内容区域到底部 */
|
||
function scrollContentToBottom() {
|
||
if (contentAreaRef.value) {
|
||
contentAreaRef.value.scrollTo({ top: contentAreaRef.value.scrollHeight, behavior: 'smooth' })
|
||
}
|
||
}
|
||
|
||
// ==================== sessionStorage 操作 ====================
|
||
|
||
/** 保存投递进度到 sessionStorage(只缓存已完成的记录,未完成的刷新后丢弃) */
|
||
function saveProgressToStorage() {
|
||
try {
|
||
const completedItems = applyProgressList.value.filter(p => p.step >= 4)
|
||
sessionStorage.setItem(STORAGE_KEY, JSON.stringify(completedItems))
|
||
} catch { /* 超容量静默 */ }
|
||
}
|
||
|
||
/** 保存已完成记录到 localStorage(持久存储) */
|
||
function saveCompletedToStorage() {
|
||
try {
|
||
localStorage.setItem(COMPLETED_STORAGE_KEY, JSON.stringify(completedApplyList.value))
|
||
} catch { /* 超容量静默 */ }
|
||
}
|
||
|
||
/** 从 sessionStorage 恢复投递进度 */
|
||
function loadProgressFromStorage() {
|
||
try {
|
||
const raw = sessionStorage.getItem(STORAGE_KEY)
|
||
if (raw) {
|
||
applyProgressList.value = JSON.parse(raw)
|
||
}
|
||
} catch { /* 解析失败静默 */ }
|
||
}
|
||
|
||
/** 从 localStorage 恢复已完成记录 */
|
||
function loadCompletedFromStorage() {
|
||
try {
|
||
const raw = localStorage.getItem(COMPLETED_STORAGE_KEY)
|
||
if (raw) {
|
||
completedApplyList.value = JSON.parse(raw)
|
||
}
|
||
} catch { /* 解析失败静默 */ }
|
||
}
|
||
|
||
// ==================== 页面初始化 ====================
|
||
|
||
/** 页面挂载时查询配置 */
|
||
onMounted(async () => {
|
||
// 检查会员权限:确保会员信息已加载,免费用户弹窗并跳回
|
||
if (!store.state.memberStatus) {
|
||
try {
|
||
const res = await fetchMemberStatus()
|
||
if (res.code === '0' && res.data) {
|
||
store.commit('SET_MEMBER_STATUS', res.data)
|
||
}
|
||
} catch { /* 异常时继续检查 */ }
|
||
}
|
||
const status = store.state.memberStatus
|
||
if (!status || !status.isMember) {
|
||
// 免费用户 — 弹出会员权限拦截弹窗,跳回上一页
|
||
store.commit('SET_SHOW_MEMBER_ACCESS_DIALOG', true)
|
||
router.back()
|
||
return
|
||
}
|
||
|
||
loadProgressFromStorage()
|
||
loadCompletedFromStorage()
|
||
await loadAgentConfig()
|
||
|
||
// 检查插件版本是否最新
|
||
if (store.state.plugIsOnline && !store.state.isLatestPlugin) {
|
||
ElMessage.warning(`当前安装的浏览器插件不是最新${store.state.plugTargetVersion}版,请去智能投递里下载最新版安装`)
|
||
}
|
||
})
|
||
|
||
/** 查询求职助手配置 */
|
||
async function loadAgentConfig() {
|
||
pageLoading.value = true
|
||
try {
|
||
const res = await fetchAgentConfig()
|
||
pageLoading.value = false
|
||
if (res.code === '0' && res.data) {
|
||
agentConfig.value = res.data
|
||
if (res.data.status === 1) {
|
||
showSetupWizard.value = false
|
||
await loadRecommendJobs()
|
||
await loadApplyList()
|
||
await loadDefaultResumeId()
|
||
}
|
||
}
|
||
} catch {
|
||
console.error('[Agent] 查询求职助手配置失败')
|
||
}
|
||
}
|
||
|
||
/** 将 JobListItem 适配为 AgentRecommendJob 格式(适配层) */
|
||
function jobListItemToRecommendJob(item: JobListItem): AgentRecommendJob {
|
||
return {
|
||
id: item.id,
|
||
title: item.title,
|
||
salary: '',
|
||
companyName: item.companyName,
|
||
companyShortName: item.companyShortName,
|
||
companyType: item.companyType,
|
||
companyLogoUrl: '',
|
||
regionName: item.regionName,
|
||
categoryName: item.categoryName,
|
||
tags: item.tags || [],
|
||
sourceUrl: item.sourceUrl,
|
||
isFavorite: item.isFavorite,
|
||
applicationStatus: null,
|
||
status: item.status,
|
||
matchScore: item.matchScore,
|
||
matchDetail: {
|
||
educationScore: item.matchDetail?.educationScore || 0,
|
||
skillScore: item.matchDetail?.skillScore || 0,
|
||
experienceScore: item.matchDetail?.experienceScore || 0,
|
||
},
|
||
education: item.education,
|
||
recruitCategory: item.recruitCategory,
|
||
}
|
||
}
|
||
|
||
/** 从全量列表中随机抽取指定数量的岗位 */
|
||
function pickRandomJobs(list: AgentRecommendJob[], count: number): AgentRecommendJob[] {
|
||
if (list.length <= count) return [...list]
|
||
const shuffled = [...list].sort(() => Math.random() - 0.5)
|
||
return shuffled.slice(0, count)
|
||
}
|
||
|
||
/** 加载推荐岗位列表(使用 job/list 接口) */
|
||
async function loadRecommendJobs() {
|
||
try {
|
||
// 获取用户求职意向作为筛选条件
|
||
const intentionRes = await fetchJobIntention()
|
||
const params: JobListParams = { pageNum: 1, pageSize: 50 }
|
||
if (intentionRes.code === '0' && intentionRes.data) {
|
||
if (intentionRes.data.regionCodes?.length) params.regionCodes = intentionRes.data.regionCodes
|
||
if (intentionRes.data.categoryIds?.length) params.categoryIds = intentionRes.data.categoryIds
|
||
if (intentionRes.data.industryIds?.length) params.industryIds = intentionRes.data.industryIds
|
||
if (intentionRes.data.recruitCategory !== null && intentionRes.data.recruitCategory !== undefined) {
|
||
params.recruitCategory = intentionRes.data.recruitCategory
|
||
}
|
||
}
|
||
const res = await fetchJobList(params)
|
||
if (res.code === '0' && res.data) {
|
||
recommendSummary.value = '根据求职目标和简历信息智能推荐'
|
||
recommendJobs.value = (res.data.list || []).map(jobListItemToRecommendJob)
|
||
displayRecommendJobs.value = pickRandomJobs(recommendJobs.value, 10)
|
||
}
|
||
} catch {
|
||
console.error('[Agent] 加载推荐岗位失败')
|
||
}
|
||
}
|
||
|
||
/** 换一批 — 重新请求 job/list 接口并随机抽取 */
|
||
async function handleRefreshRecommend() {
|
||
recommendRefreshing.value = true
|
||
try {
|
||
const intentionRes = await fetchJobIntention()
|
||
const params: JobListParams = { pageNum: 1, pageSize: 50 }
|
||
if (intentionRes.code === '0' && intentionRes.data) {
|
||
if (intentionRes.data.regionCodes?.length) params.regionCodes = intentionRes.data.regionCodes
|
||
if (intentionRes.data.categoryIds?.length) params.categoryIds = intentionRes.data.categoryIds
|
||
if (intentionRes.data.industryIds?.length) params.industryIds = intentionRes.data.industryIds
|
||
if (intentionRes.data.recruitCategory !== null && intentionRes.data.recruitCategory !== undefined) {
|
||
params.recruitCategory = intentionRes.data.recruitCategory
|
||
}
|
||
}
|
||
// 排除当前已展示的岗位
|
||
const excludeIds = recommendJobs.value.map(j => Number(j.id))
|
||
if (excludeIds.length) params.excludeJobIds = excludeIds
|
||
const res = await fetchJobList(params)
|
||
if (res.code === '0' && res.data) {
|
||
recommendSummary.value = '根据求职目标和简历信息智能推荐'
|
||
recommendJobs.value = (res.data.list || []).map(jobListItemToRecommendJob)
|
||
displayRecommendJobs.value = pickRandomJobs(recommendJobs.value, 10)
|
||
}
|
||
} catch {
|
||
console.error('[Agent] 换一批推荐岗位失败')
|
||
} finally {
|
||
recommendRefreshing.value = false
|
||
}
|
||
}
|
||
|
||
/** 不再推荐 — 从当前展示列表中移除 */
|
||
function handleDismissJob(job: AgentRecommendJob) {
|
||
displayRecommendJobs.value = displayRecommendJobs.value.filter(j => j.id !== job.id)
|
||
// 同时从全量列表中移除,避免换一批后重新出现
|
||
recommendJobs.value = recommendJobs.value.filter(j => j.id !== job.id)
|
||
}
|
||
|
||
/** 全部添加(展示的岗位) */
|
||
async function handleAddAllDisplayJobs() {
|
||
const pendingJobs = displayRecommendJobs.value.filter(j => j.applicationStatus === null || j.applicationStatus === undefined)
|
||
if (pendingJobs.length === 0) {
|
||
ElMessage.info('所有岗位已添加到待投递')
|
||
return
|
||
}
|
||
for (const job of pendingJobs) {
|
||
if (loadingJobIds.value.includes(job.id)) continue
|
||
loadingJobIds.value.push(job.id)
|
||
try {
|
||
await applyJob({ jobId: job.id, status: -1 })
|
||
updateJobStatusInRecommend(job.id, -1)
|
||
} catch { /* 单个失败不阻断 */ }
|
||
finally {
|
||
loadingJobIds.value = loadingJobIds.value.filter(id => id !== job.id)
|
||
}
|
||
}
|
||
await loadApplyList()
|
||
ElMessage.success('已全部添加到待投递')
|
||
}
|
||
|
||
/** 加载求职助手任务列表(tab=1 进行中/待投递) */
|
||
async function loadApplyList() {
|
||
try {
|
||
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] 加载求职助手任务列表失败')
|
||
}
|
||
}
|
||
|
||
/** 加载默认简历 ID */
|
||
async function loadDefaultResumeId() {
|
||
try {
|
||
const res = await fetchResumeList()
|
||
if (res.code === '0' && res.data && res.data.length > 0) {
|
||
const defaultResume = res.data.find(r => r.isDefault === 1)
|
||
defaultResumeId.value = defaultResume?.id || res.data[0]?.id || ''
|
||
}
|
||
} catch {
|
||
console.error('[Agent] 加载默认简历失败')
|
||
}
|
||
}
|
||
|
||
// ==================== 准备向导事件 ====================
|
||
|
||
/** 准备向导设置完成 */
|
||
async function handleSetupComplete(data: Record<string, any>) {
|
||
try {
|
||
await saveAgentConfig(data as AgentConfig)
|
||
ElMessage.success('配置保存成功')
|
||
} catch {
|
||
ElMessage.error('配置保存失败,请重试')
|
||
}
|
||
}
|
||
|
||
/** 启用求职助手 */
|
||
async function handleLaunch() {
|
||
showSetupWizard.value = false
|
||
ElMessage.success('求职助手已启用')
|
||
await loadRecommendJobs()
|
||
await loadApplyList()
|
||
await loadDefaultResumeId()
|
||
}
|
||
|
||
// ==================== 投递任务抽屉 ====================
|
||
|
||
/** 投递任务抽屉是否显示 */
|
||
const showTaskListDrawer = ref(false)
|
||
|
||
/** 切换投递任务抽屉显隐 */
|
||
function toggleTaskListDropdown() {
|
||
if (isApplying.value) {
|
||
ElMessage.warning('请完成或取消投递流程后再打开编辑待投递列表')
|
||
return
|
||
}
|
||
showTaskListDrawer.value = !showTaskListDrawer.value
|
||
}
|
||
|
||
/** 关闭投递任务抽屉 */
|
||
function handleCloseTaskListDrawer() {
|
||
showTaskListDrawer.value = false
|
||
}
|
||
|
||
/** 岗位从待投递列表中移除后的回调 */
|
||
function handleTaskRemoved(jobId: string) {
|
||
const idx = applyJobList.value.findIndex(j => j.id === jobId)
|
||
if (idx !== -1) {
|
||
applyJobList.value.splice(idx, 1)
|
||
}
|
||
}
|
||
|
||
// ==================== 推荐岗位面板 ====================
|
||
|
||
/** 更新推荐岗位中对应岗位的 applicationStatus */
|
||
function updateJobStatusInRecommend(jobId: string | number, newStatus: number | null) {
|
||
// 更新全量列表
|
||
const job = recommendJobs.value.find(j => j.id === jobId)
|
||
if (job) {
|
||
job.applicationStatus = newStatus
|
||
}
|
||
// 更新展示列表
|
||
const displayJob = displayRecommendJobs.value.find(j => j.id === jobId)
|
||
if (displayJob) {
|
||
displayJob.applicationStatus = newStatus
|
||
}
|
||
}
|
||
|
||
/** 单个岗位添加/移除 */
|
||
async function handleToggleJobApply(job: AgentRecommendJob) {
|
||
if (loadingJobIds.value.includes(job.id)) return
|
||
const isRemoving = job.applicationStatus === -1
|
||
loadingJobIds.value.push(job.id)
|
||
try {
|
||
if (isRemoving) {
|
||
await cancelApplyJob(job.id)
|
||
} else {
|
||
await applyJob({ jobId: job.id, status: -1 })
|
||
}
|
||
updateJobStatusInRecommend(job.id, isRemoving ? null : -1)
|
||
await loadApplyList()
|
||
ElMessage.success(isRemoving ? '岗位已从待投递移除' : '岗位已添加到待投递')
|
||
} catch {
|
||
ElMessage.error('操作失败,请重试')
|
||
} finally {
|
||
loadingJobIds.value = loadingJobIds.value.filter(id => id !== job.id)
|
||
}
|
||
}
|
||
|
||
// ==================== 岗位预览(模式4 → 改为独立抽屉) ====================
|
||
|
||
/** 岗位预览抽屉是否显示 */
|
||
const showJobPreviewDrawer = ref(false)
|
||
|
||
/** 当前预览的岗位 ID */
|
||
const previewJobId = ref('')
|
||
|
||
/** 当前预览岗位的投递状态 */
|
||
const previewJobApplicationStatus = ref<number | null>(null)
|
||
|
||
/** 打开岗位预览抽屉 */
|
||
function handleOpenJobPreview(jobId: string | number, applicationStatus?: number | null) {
|
||
previewJobId.value = String(jobId)
|
||
previewJobApplicationStatus.value = applicationStatus ?? null
|
||
showJobPreviewDrawer.value = true
|
||
}
|
||
|
||
/** 岗位预览关闭 */
|
||
function handleJobPreviewBack() {
|
||
showJobPreviewDrawer.value = false
|
||
}
|
||
|
||
/** 岗位预览中点击添加 */
|
||
async function handleJobPreviewAdd(jobId: string) {
|
||
try {
|
||
await applyJob({ jobId, status: -1 })
|
||
updateJobStatusInRecommend(Number(jobId), -1)
|
||
previewJobApplicationStatus.value = -1
|
||
await loadApplyList()
|
||
ElMessage.success('岗位已添加到待投递')
|
||
} catch {
|
||
ElMessage.error('操作失败,请重试')
|
||
}
|
||
}
|
||
|
||
/** 岗位预览中点击移出 */
|
||
async function handleJobPreviewRemove(jobId: string) {
|
||
try {
|
||
await cancelApplyJob(jobId)
|
||
updateJobStatusInRecommend(Number(jobId), null)
|
||
previewJobApplicationStatus.value = null
|
||
await loadApplyList()
|
||
ElMessage.success('岗位已从待投递移除')
|
||
} catch {
|
||
ElMessage.error('操作失败,请重试')
|
||
}
|
||
}
|
||
|
||
/** AgentChatJobList 中点击岗位 */
|
||
function handleChatJobClick(job: AgentRecommendJob) {
|
||
if (isApplying.value) {
|
||
ElMessage.warning('请完成或取消投递流程后再查看岗位详情')
|
||
return
|
||
}
|
||
handleOpenJobPreview(job.id, job.applicationStatus)
|
||
}
|
||
|
||
// ==================== 待投递列表面板(模式5) ====================
|
||
|
||
// ==================== 投递流程 ====================
|
||
|
||
/** 右侧面板显示模式(已废弃,保留仅兼容) */
|
||
|
||
/** 简历预览抽屉是否显示 */
|
||
const showResumeDrawer = ref(false)
|
||
|
||
/** 简历优化前分数(2-4之间随机) */
|
||
const resumeScoreBefore = ref(3.2)
|
||
|
||
/** 简历优化后分数(matchScore / 10) */
|
||
const resumeScoreAfter = ref(6.0)
|
||
|
||
/** 当前投递流程的简历数据 */
|
||
const applyResumeData = ref<ResumeTemplateData>({} as ResumeTemplateData)
|
||
|
||
/** 右侧面板简历模板组件引用 */
|
||
const applyResumeTemplateRef = ref<InstanceType<typeof JobResumeTemplate> | null>(null)
|
||
|
||
/** 简历模板模式:preview=预览,export=导出 */
|
||
const applyResumeMode = ref<'preview' | 'export'>('preview')
|
||
|
||
/** 关闭简历预览抽屉 */
|
||
function handleCloseResumeDrawer() {
|
||
showResumeDrawer.value = false
|
||
}
|
||
|
||
/** 学历文字转数字 */
|
||
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 || [],
|
||
}
|
||
}
|
||
|
||
/** 标记当前投递流程是否已被取消 */
|
||
const applyCancelled = ref(false)
|
||
|
||
/** 顶部栏按钮点击(开始/暂停投递) */
|
||
function handleTopBarStartClick() {
|
||
if (isApplying.value && !isPaused.value) {
|
||
// 暂停投递
|
||
isPaused.value = true
|
||
} else if (isApplying.value && isPaused.value) {
|
||
// 恢复投递
|
||
isPaused.value = false
|
||
} else {
|
||
// 开始投递
|
||
handleStartApply()
|
||
}
|
||
}
|
||
|
||
/** 暂停/恢复投递 */
|
||
function handleTogglePause() {
|
||
isPaused.value = !isPaused.value
|
||
}
|
||
|
||
/** 开始投递流程 */
|
||
async function handleStartApply() {
|
||
if (isApplying.value) return
|
||
isApplying.value = true
|
||
|
||
// 刷新最新配置(确保优化开关状态准确)
|
||
try {
|
||
const configRes = await fetchAgentConfig()
|
||
if (configRes.code === '0' && configRes.data) {
|
||
agentConfig.value = configRes.data
|
||
}
|
||
} catch { /* 静默 */ }
|
||
|
||
await loadApplyList()
|
||
|
||
if (applyJobList.value.length === 0) {
|
||
ElMessage.warning('暂无待投递岗位,请先添加岗位')
|
||
isApplying.value = false
|
||
return
|
||
}
|
||
|
||
// 取第一个岗位
|
||
const targetJob = applyJobList.value[0]
|
||
|
||
// 判断是否开启简历针对性优化(从配置中读取)
|
||
const isOptimizeEnabled = agentConfig.value?.autoOptimizeResume === 1
|
||
// 判断是否开启深度人岗匹配分析
|
||
const isMatchEnabled = agentConfig.value?.acceptDeptTransfer === '是'
|
||
|
||
if (isOptimizeEnabled) {
|
||
// 开启优化:从第1步开始
|
||
const progressItem: ApplyProgressItem = {
|
||
id: Date.now(),
|
||
jobInfo: targetJob,
|
||
step: 1,
|
||
stepPhase: 'loading',
|
||
resumeName: '岗位专属简历',
|
||
}
|
||
applyProgressList.value = [progressItem]
|
||
saveProgressToStorage()
|
||
|
||
await nextTick()
|
||
scrollContentToBottom()
|
||
|
||
// 开始第1步:优化简历
|
||
await startResumeOptimization(targetJob)
|
||
} else if (isMatchEnabled) {
|
||
// 关闭优化但开启匹配分析:跳过第1步,直接进入第2步
|
||
const progressItem: ApplyProgressItem = {
|
||
id: Date.now(),
|
||
jobInfo: targetJob,
|
||
step: 2,
|
||
stepPhase: 'loading',
|
||
resumeName: '默认简历',
|
||
}
|
||
applyProgressList.value = [progressItem]
|
||
saveProgressToStorage()
|
||
|
||
await nextTick()
|
||
scrollContentToBottom()
|
||
|
||
// 直接进入第2步:匹配分析(使用默认简历ID)
|
||
await startMatchAnalysisWithResumeId()
|
||
} else {
|
||
// 优化和匹配分析都关闭:跳过第1、2步,直接进入第3步
|
||
const progressItem: ApplyProgressItem = {
|
||
id: Date.now(),
|
||
jobInfo: targetJob,
|
||
step: 3,
|
||
stepPhase: 'ready',
|
||
resumeName: '默认简历',
|
||
}
|
||
applyProgressList.value = [progressItem]
|
||
saveProgressToStorage()
|
||
|
||
await nextTick()
|
||
scrollContentToBottom()
|
||
}
|
||
}
|
||
|
||
/** 执行简历优化(第1步) */
|
||
async function startResumeOptimization(targetJob: JobListItem) {
|
||
applyCancelled.value = false
|
||
|
||
try {
|
||
const res = await optimizeAgentResume({
|
||
resumeId: defaultResumeId.value,
|
||
jobId: targetJob.id,
|
||
})
|
||
|
||
if (applyCancelled.value) return
|
||
|
||
const apiData = res?.data || res
|
||
if (apiData) {
|
||
const resumeResult = mapOptimizeResumeToTemplate(apiData)
|
||
applyResumeData.value = resumeResult
|
||
|
||
// 直接操作数组中的对象确保响应式更新
|
||
const current = applyProgressList.value[0]
|
||
if (current) {
|
||
current.resumeName = resumeResult.name || '岗位专属简历'
|
||
current.resumeData = apiData
|
||
// 第1步完成,等待用户确认
|
||
current.step = 1
|
||
current.stepPhase = 'done'
|
||
}
|
||
// 触发数组引用更新
|
||
applyProgressList.value = [...applyProgressList.value]
|
||
saveProgressToStorage()
|
||
}
|
||
} catch (e) {
|
||
if (applyCancelled.value) return
|
||
console.error('[Agent] 优化简历失败', e)
|
||
ElMessage.error('简历优化失败,请重试')
|
||
isApplying.value = false
|
||
applyProgressList.value = []
|
||
saveProgressToStorage()
|
||
}
|
||
}
|
||
|
||
/** 第1步完成:查看优化简历 */
|
||
async function handleViewResumeStep() {
|
||
const progressItem = applyProgressList.value[0]
|
||
if (!progressItem) return
|
||
|
||
// 打开简历预览抽屉
|
||
if (progressItem.resumeData) {
|
||
applyResumeData.value = mapOptimizeResumeToTemplate(progressItem.resumeData)
|
||
}
|
||
showResumeDrawer.value = true
|
||
|
||
// 生成优化前随机分(2到4之间,保留1位小数)
|
||
resumeScoreBefore.value = Number((Math.random() * 2 + 2).toFixed(1))
|
||
|
||
// 查询匹配分
|
||
try {
|
||
const customizeRes = await fetchCustomizeResume(String(progressItem.jobInfo.id))
|
||
const customizeResumeId = customizeRes?.data?.id
|
||
if (customizeResumeId) {
|
||
const scoreRes = await fetchMatchScore({
|
||
customizeResumeId: String(customizeResumeId),
|
||
jobId: String(progressItem.jobInfo.id),
|
||
})
|
||
const rawScore = scoreRes?.data ?? 60
|
||
// 接口返回10-100,除以10得到1.0-10.0
|
||
resumeScoreAfter.value = Number((rawScore / 10).toFixed(1))
|
||
}
|
||
} catch {
|
||
// 查询失败时用默认值
|
||
resumeScoreAfter.value = 6.0
|
||
}
|
||
}
|
||
|
||
/** 第1步完成:确认简历 → 自动进入第2步匹配分析(或跳过) */
|
||
async function handleConfirmResumeStep() {
|
||
const progressItem = applyProgressList.value[0]
|
||
if (!progressItem) return
|
||
|
||
// 关闭简历预览抽屉
|
||
showResumeDrawer.value = false
|
||
|
||
// 判断是否开启深度人岗匹配分析
|
||
const isMatchEnabled = agentConfig.value?.acceptDeptTransfer === '是'
|
||
|
||
if (isMatchEnabled) {
|
||
// 进入第2步
|
||
progressItem.step = 2
|
||
progressItem.stepPhase = 'loading'
|
||
applyProgressList.value = [...applyProgressList.value]
|
||
saveProgressToStorage()
|
||
|
||
// 调用匹配分析流程
|
||
await startMatchAnalysis()
|
||
} else {
|
||
// 匹配分析关闭:跳过第2步直接进入第3步
|
||
progressItem.step = 3
|
||
progressItem.stepPhase = 'ready'
|
||
applyProgressList.value = [...applyProgressList.value]
|
||
saveProgressToStorage()
|
||
}
|
||
}
|
||
|
||
/** 执行匹配分析(第2步) */
|
||
async function startMatchAnalysis() {
|
||
const progressItem = applyProgressList.value[0]
|
||
if (!progressItem) return
|
||
|
||
try {
|
||
// 先调 /job/customize-resume 获取定制简历 ID
|
||
const customizeRes = await fetchCustomizeResume(String(progressItem.jobInfo.id))
|
||
if (applyCancelled.value) return
|
||
|
||
const customizeResumeId = customizeRes?.data?.id
|
||
if (!customizeResumeId) {
|
||
console.error('[Agent] 获取定制简历ID失败')
|
||
ElMessage.error('匹配分析失败,未获取到定制简历')
|
||
return
|
||
}
|
||
|
||
progressItem.customizeResumeId = String(customizeResumeId)
|
||
saveProgressToStorage()
|
||
|
||
// 再调 /job-agent/match-score 获取匹配度分数
|
||
const scoreRes = await fetchMatchScore({
|
||
customizeResumeId: String(customizeResumeId),
|
||
jobId: String(progressItem.jobInfo.id),
|
||
})
|
||
|
||
if (applyCancelled.value) return
|
||
|
||
const score = scoreRes?.data ?? 0
|
||
progressItem.matchScore = score
|
||
|
||
if (score >= 40) {
|
||
// 匹配度 >= 40,直接进入第3步
|
||
progressItem.step = 3
|
||
progressItem.stepPhase = 'ready'
|
||
} else {
|
||
// 匹配度 < 40,显示低匹配提示
|
||
progressItem.step = 2
|
||
progressItem.stepPhase = 'low-match'
|
||
}
|
||
// 触发响应式更新
|
||
applyProgressList.value = [...applyProgressList.value]
|
||
saveProgressToStorage()
|
||
} catch (e) {
|
||
if (applyCancelled.value) return
|
||
console.error('[Agent] 匹配分析失败', e)
|
||
ElMessage.error('匹配分析失败,请重试')
|
||
}
|
||
}
|
||
|
||
/** 执行匹配分析(第2步) — 关闭优化时使用默认简历ID */
|
||
async function startMatchAnalysisWithResumeId() {
|
||
const progressItem = applyProgressList.value[0]
|
||
if (!progressItem) return
|
||
|
||
try {
|
||
// 使用默认简历ID调用匹配分析接口
|
||
const scoreRes = await fetchMatchScore({
|
||
resumeId: defaultResumeId.value,
|
||
jobId: String(progressItem.jobInfo.id),
|
||
})
|
||
|
||
if (applyCancelled.value) return
|
||
|
||
const score = scoreRes?.data ?? 0
|
||
progressItem.matchScore = score
|
||
|
||
if (score >= 40) {
|
||
// 匹配度 >= 40,直接进入第3步
|
||
progressItem.step = 3
|
||
progressItem.stepPhase = 'ready'
|
||
} else {
|
||
// 匹配度 < 40,显示低匹配提示
|
||
progressItem.step = 2
|
||
progressItem.stepPhase = 'low-match'
|
||
}
|
||
// 触发响应式更新
|
||
applyProgressList.value = [...applyProgressList.value]
|
||
saveProgressToStorage()
|
||
} catch (e) {
|
||
if (applyCancelled.value) return
|
||
console.error('[Agent] 匹配分析失败(默认简历)', e)
|
||
ElMessage.error('匹配分析失败,请重试')
|
||
}
|
||
}
|
||
|
||
/** 第2步匹配度低:不投递 — 取消该岗位 */
|
||
async function handleNotApplyStep() {
|
||
const progressItem = applyProgressList.value[0]
|
||
if (!progressItem) return
|
||
|
||
try {
|
||
// 调用 DELETE /job/apply 从待投递列表移除
|
||
await cancelApplyJob(String(progressItem.jobInfo.id))
|
||
} catch (e) {
|
||
console.error('[Agent] 取消投递失败', e)
|
||
}
|
||
|
||
// 清除当前进度
|
||
applyProgressList.value = []
|
||
saveProgressToStorage()
|
||
|
||
// 自动开始下一个岗位
|
||
autoStartNextApply()
|
||
}
|
||
|
||
/** 第2步匹配度低:继续投递 → 进入第3步 */
|
||
function handleContinueApplyStep() {
|
||
const progressItem = applyProgressList.value[0]
|
||
if (!progressItem) return
|
||
|
||
progressItem.step = 3
|
||
progressItem.stepPhase = 'ready'
|
||
applyProgressList.value = [...applyProgressList.value]
|
||
saveProgressToStorage()
|
||
}
|
||
|
||
/** 第3步:前往投递 — 打开岗位来源网站 + 进入第4步 */
|
||
function handleGoApplyStep() {
|
||
const progressItem = applyProgressList.value[0]
|
||
if (!progressItem) return
|
||
|
||
const sourceUrl = (progressItem.jobInfo as any)?.sourceUrl
|
||
if (sourceUrl) {
|
||
window.open(sourceUrl, '_blank')
|
||
}
|
||
|
||
// 进入第4步
|
||
progressItem.step = 4
|
||
progressItem.stepPhase = 'confirm'
|
||
applyProgressList.value = [...applyProgressList.value]
|
||
saveProgressToStorage()
|
||
}
|
||
|
||
/** 第4步:已投递 — 调用接口确认 */
|
||
async function handleAppliedStep() {
|
||
const progressItem = applyProgressList.value[0]
|
||
if (!progressItem) return
|
||
|
||
try {
|
||
await applyJob({ jobId: String(progressItem.jobInfo.id), status: 0 })
|
||
ElMessage.success('投递成功')
|
||
|
||
// 记录完成时间
|
||
const now = new Date()
|
||
const timeStr = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')} ${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}:${String(now.getSeconds()).padStart(2, '0')}`
|
||
|
||
// 添加到已完成列表
|
||
completedApplyList.value.unshift({
|
||
id: progressItem.id,
|
||
jobInfo: progressItem.jobInfo,
|
||
completedTime: timeStr,
|
||
})
|
||
saveCompletedToStorage()
|
||
|
||
// 清除当前进度
|
||
applyProgressList.value = []
|
||
saveProgressToStorage()
|
||
|
||
// 自动开始下一个岗位
|
||
autoStartNextApply()
|
||
} catch (e) {
|
||
console.error('[Agent] 投递失败', e)
|
||
ElMessage.error('投递失败,请重试')
|
||
}
|
||
}
|
||
|
||
/** 取消投递流程(用于暂停后不再继续等) */
|
||
function handleCancelApply() {
|
||
applyCancelled.value = true
|
||
isPaused.value = false
|
||
|
||
applyProgressList.value = []
|
||
saveProgressToStorage()
|
||
|
||
showResumeDrawer.value = false
|
||
isApplying.value = false
|
||
}
|
||
|
||
/** 自动开始下一个岗位投递 */
|
||
async function autoStartNextApply() {
|
||
if (isPaused.value) {
|
||
isApplying.value = false
|
||
return
|
||
}
|
||
|
||
await loadApplyList()
|
||
|
||
if (applyJobList.value.length === 0) {
|
||
isApplying.value = false
|
||
showResumeDrawer.value = false
|
||
ElMessage.success('所有待投递岗位已处理完毕')
|
||
return
|
||
}
|
||
|
||
// 取下一个岗位
|
||
const targetJob = applyJobList.value[0]
|
||
|
||
// 判断是否开启简历针对性优化
|
||
const isOptimizeEnabled = agentConfig.value?.autoOptimizeResume === 1
|
||
// 判断是否开启深度人岗匹配分析
|
||
const isMatchEnabled = agentConfig.value?.acceptDeptTransfer === '是'
|
||
|
||
if (isOptimizeEnabled) {
|
||
const progressItem: ApplyProgressItem = {
|
||
id: Date.now(),
|
||
jobInfo: targetJob,
|
||
step: 1,
|
||
stepPhase: 'loading',
|
||
resumeName: '岗位专属简历',
|
||
}
|
||
applyProgressList.value = [progressItem]
|
||
saveProgressToStorage()
|
||
|
||
await nextTick()
|
||
scrollContentToBottom()
|
||
|
||
// 开始第1步:优化简历
|
||
await startResumeOptimization(targetJob)
|
||
} else if (isMatchEnabled) {
|
||
// 关闭优化但开启匹配分析:跳过第1步,直接进入第2步
|
||
const progressItem: ApplyProgressItem = {
|
||
id: Date.now(),
|
||
jobInfo: targetJob,
|
||
step: 2,
|
||
stepPhase: 'loading',
|
||
resumeName: '默认简历',
|
||
}
|
||
applyProgressList.value = [progressItem]
|
||
saveProgressToStorage()
|
||
|
||
await nextTick()
|
||
scrollContentToBottom()
|
||
|
||
await startMatchAnalysisWithResumeId()
|
||
} else {
|
||
// 优化和匹配分析都关闭:跳过第1、2步,直接进入第3步
|
||
const progressItem: ApplyProgressItem = {
|
||
id: Date.now(),
|
||
jobInfo: targetJob,
|
||
step: 3,
|
||
stepPhase: 'ready',
|
||
resumeName: '默认简历',
|
||
}
|
||
applyProgressList.value = [progressItem]
|
||
saveProgressToStorage()
|
||
|
||
await nextTick()
|
||
scrollContentToBottom()
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
@use '../assets/styles/pages/agent';
|
||
</style>
|