714 lines
38 KiB
Vue
714 lines
38 KiB
Vue
<template>
|
||
<!-- 求职助手准备向导 — 第1步到第4步 -->
|
||
<div class="bg-f ptb16 prl24 mb16 fs18 fw600">投递准备</div>
|
||
<div>
|
||
<div class="agent-page__wizard bg-f prl32 ptb24 border-ra12 mrl24">
|
||
|
||
<!-- 个人资料编辑抽屉 -->
|
||
<ProfileEditDrawer
|
||
v-model="showEditDrawer"
|
||
:module="editModule"
|
||
:initial-data="editInitialData"
|
||
@save="handleSaveEdit"
|
||
/>
|
||
|
||
<!-- 顶部步骤导航条 -->
|
||
<div class="agent-page__steps">
|
||
<template v-for="(step, index) in steps" :key="index">
|
||
<div
|
||
class="agent-page__step"
|
||
:class="{ 'agent-page__step--active': currentStep === index + 1 }"
|
||
>
|
||
<span class="agent-page__step-number">{{ index + 1 }}</span>
|
||
<span class="agent-page__step-label">{{ step }}</span>
|
||
</div>
|
||
<span v-if="index < steps.length - 1" class="agent-page__step-line"></span>
|
||
</template>
|
||
</div>
|
||
|
||
<!-- 主体内容区域 -->
|
||
<div class="agent-page__main">
|
||
<!-- ========== 第1步:确认个人资料 ========== -->
|
||
<template v-if="currentStep === 1">
|
||
<div class="agent-page__right">
|
||
<div class="agent-page__profile-wrapper">
|
||
<ProfilePageContent :profile="profile" @edit="handleEdit" @save="handleInlineSave" @exp-update="handleExpUpdate" @exp-add="handleExpAdd" @exp-delete="handleExpDelete" />
|
||
</div>
|
||
</div>
|
||
<div class="agent-page__left">
|
||
<div class="agent-page__intro-card agent-page__intro-card--gradient">
|
||
<!-- Nova 头像 + 名称 -->
|
||
<div class="agent-page__intro-avatar-row">
|
||
<img src="@/assets/images/nova-logo-2.png" alt="Nova" class="agent-page__intro-avatar-img" />
|
||
<span class="agent-page__intro-avatar-name">Nova</span>
|
||
</div>
|
||
<!-- 标题 -->
|
||
<h2 class="agent-page__intro-title">请先确认你的个人资料是否无误</h2>
|
||
<!-- 描述 -->
|
||
<p class="agent-page__intro-desc">请仔细对你的资料评估(包括个人信息、教育背景、工作履历及技能),确保资料齐全</p>
|
||
<!-- 按钮 -->
|
||
<button class="agent-page__intro-gradient-btn" @click="handleNext">下一步</button>
|
||
|
||
</div>
|
||
</div>
|
||
|
||
</template>
|
||
|
||
<!-- ========== 第2步:确认目标 ========== -->
|
||
<template v-if="currentStep === 2">
|
||
<!-- 左侧:求职偏好 + 简历选择 -->
|
||
<div class="agent-page__right">
|
||
<div class="agent-page__step2-panel">
|
||
<!-- 求职偏好区域 -->
|
||
<div class="agent-page__step2-section">
|
||
<h3 class="agent-page__step2-section-title">求职偏好</h3>
|
||
<div class="agent-page__step2-filters">
|
||
<!-- 城市选择 -->
|
||
<RegionSelector
|
||
:regionCodes="step2RegionCodes"
|
||
:level="2"
|
||
:maxSelect="3"
|
||
:triggerStyle="prefFilterTriggerStyle"
|
||
:displayStyle="prefFilterDisplayStyle"
|
||
@update:regionCodes="onStep2RegionChange"
|
||
/>
|
||
<!-- 岗位选择 -->
|
||
<JobCategorySelector
|
||
:categoryIds="step2CategoryIds"
|
||
:maxSelect="3"
|
||
:level="3"
|
||
:allowParentSelect="false"
|
||
:allowParentSelectClick="true"
|
||
:triggerStyle="prefFilterTriggerStyle"
|
||
:displayStyle="prefFilterDisplayStyle"
|
||
@update:categoryIds="onStep2CategoryChange"
|
||
/>
|
||
<!-- 行业选择 -->
|
||
<IndustrySelector
|
||
:industryIds="step2IndustryIds"
|
||
:maxSelect="3"
|
||
:level="2"
|
||
:allowParentSelect="false"
|
||
:allowParentSelectClick="true"
|
||
:triggerStyle="prefFilterTriggerStyle"
|
||
:displayStyle="prefFilterDisplayStyle"
|
||
@update:industryIds="onStep2IndustryChange"
|
||
/>
|
||
<!-- 招聘分类下拉选择 -->
|
||
<div class="agent-page__pref-type-dropdown" @click.stop="showJobTypeDropdown = !showJobTypeDropdown">
|
||
<span>{{ intentionEmploymentLabel || '招聘类型' }}</span>
|
||
<svg class="agent-page__pref-type-arrow" viewBox="0 0 12 12" fill="none">
|
||
<path d="M3 4.5L6 7.5L9 4.5" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||
</svg>
|
||
<!-- 下拉菜单 -->
|
||
<div v-if="showJobTypeDropdown" class="agent-page__pref-type-menu" @click.stop>
|
||
<div
|
||
v-for="option in jobTypeOptions"
|
||
:key="option.value"
|
||
class="agent-page__pref-type-menu-item"
|
||
:class="{ 'agent-page__pref-type-menu-item--active': store.state.jobIntention.recruitCategory === option.value }"
|
||
@click.stop="selectJobType(option)"
|
||
>{{ option.label }}</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<!-- 简历选择区域 -->
|
||
<div class="agent-page__step2-section">
|
||
<h3 class="agent-page__step2-section-title">简历选择</h3>
|
||
<div class="agent-page__step2-filters">
|
||
<!-- 自定义简历下拉 -->
|
||
<div class="agent-page__step2-resume-dropdown" @click.stop="showResumeDropdown = !showResumeDropdown">
|
||
<span class="agent-page__step2-resume-dropdown-text">{{ selectedResumeName || '请选择简历' }}</span>
|
||
<svg class="agent-page__step2-resume-dropdown-arrow" viewBox="0 0 12 12" fill="none">
|
||
<path d="M3 4.5L6 7.5L9 4.5" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||
</svg>
|
||
<!-- 下拉菜单 -->
|
||
<div v-if="showResumeDropdown" class="agent-page__step2-resume-menu" @click.stop>
|
||
<div
|
||
v-for="r in resumeList"
|
||
:key="r.id"
|
||
class="agent-page__step2-resume-menu-item"
|
||
:class="{ 'agent-page__step2-resume-menu-item--active': selectedResumeId === r.id }"
|
||
@click.stop="selectResume(r)"
|
||
>{{ r.resumeName }}</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<!-- 右侧:确认卡片 -->
|
||
<div class="agent-page__left">
|
||
<div class="agent-page__intro-card agent-page__intro-card--gradient">
|
||
<!-- Nova 头像 + 名称 -->
|
||
<div class="agent-page__intro-avatar-row">
|
||
<img src="@/assets/images/nova-logo-2.png" alt="Nova" class="agent-page__intro-avatar-img" />
|
||
<span class="agent-page__intro-avatar-name">Nova</span>
|
||
</div>
|
||
<!-- 标题 -->
|
||
<h2 class="agent-page__intro-title">请配置你的投递方案</h2>
|
||
<!-- 描述 -->
|
||
<p class="agent-page__intro-desc">我们会根据你的选择,为你推荐最合适的投递岗位</p>
|
||
<!-- 按钮 -->
|
||
<button class="agent-page__intro-gradient-btn" @click="handleNext">下一步</button>
|
||
<button class="agent-page__intro-back-btn" @click="handleBack">上一步</button>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
|
||
|
||
<!-- ========== 第3步:安装插件 ========== -->
|
||
<template v-if="currentStep === 3">
|
||
<!-- 左侧:安装教程内容 -->
|
||
<div class="agent-page__right">
|
||
<div class="agent-page__profile-wrapper bg-f">
|
||
<AgentBrowserInstallGuide ref="browserInstallGuideRef" />
|
||
</div>
|
||
</div>
|
||
<!-- 右侧:确认卡片 -->
|
||
<div class="agent-page__left">
|
||
<div class="agent-page__intro-card agent-page__intro-card--gradient">
|
||
<!-- Nova 头像 + 名称 -->
|
||
<div class="agent-page__intro-avatar-row">
|
||
<img src="@/assets/images/nova-logo-2.png" alt="Nova" class="agent-page__intro-avatar-img" />
|
||
<span class="agent-page__intro-avatar-name">Nova</span>
|
||
</div>
|
||
<!-- 标题 -->
|
||
<h2 class="agent-page__intro-title">安装浏览器自动投递插件</h2>
|
||
<!-- 描述 -->
|
||
<p class="agent-page__intro-desc">按照左侧步骤完成插件安装,即可开启自动投递功能</p>
|
||
<!-- 按钮:在线显示下一步,离线显示安装确认按钮 -->
|
||
<button v-if="plugIsOnline" class="agent-page__intro-gradient-btn" @click="handleNext">下一步</button>
|
||
<button v-else class="agent-page__intro-gradient-btn" @click="handleInstallConfirm">
|
||
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||
<path d="M0.625 10.2117V11.0417C0.625 11.7047 0.888392 12.3406 1.35723 12.8094C1.82607 13.2783 2.46196 13.5417 3.125 13.5417H11.4583C12.1214 13.5417 12.7573 13.2783 13.2261 12.8094C13.6949 12.3406 13.9583 11.7047 13.9583 11.0417V10.2083M7.29167 0.625V9.79167M4.375 6.875L7.29167 9.79167L10.2083 6.875" stroke="white" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
|
||
</svg>
|
||
下载插件
|
||
</button>
|
||
<button class="agent-page__intro-back-btn" @click="handleBack">上一步</button>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<!-- ========== 第4步:完成配置 ========== -->
|
||
<template v-if="currentStep === 4">
|
||
<div class="agent-page__complete">
|
||
<div class="agent-page__complete-icon">
|
||
<img class="w160 " src="@/assets/images/agent-setting/setting-success.gif" alt="">
|
||
</div>
|
||
<h2 class="agent-page__complete-title">恭喜!求职助手已经准备好</h2>
|
||
<p class="agent-page__complete-desc">启用求职助手后,你可以随时调整求职偏好和简历信息</p>
|
||
<button class="agent-page__complete-btn" @click="handleLaunchAgent">启用求职助手开始投递</button>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
|
||
<!-- 求职目标设置弹窗 -->
|
||
<JobGoalDialog v-model="showJobGoalDialog" />
|
||
|
||
<!-- 插件安装确认弹窗 -->
|
||
<Teleport to="body">
|
||
<div v-if="showInstallDialog" class="agent-page__install-dialog-overlay" @click.self="showInstallDialog = false">
|
||
<div class="agent-page__install-dialog">
|
||
<!-- 渐变背景 -->
|
||
<div class="agent-page__install-dialog-bg"></div>
|
||
<!-- 关闭按钮 -->
|
||
<div class="agent-page__install-dialog-close" @click="showInstallDialog = false">
|
||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
|
||
<path d="M18 6L6 18M6 6l12 12" stroke="#6A7282" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||
</svg>
|
||
</div>
|
||
<!-- 标题 -->
|
||
<h3 class="agent-page__install-dialog-title">插件是否已安装成功?</h3>
|
||
<!-- 描述 -->
|
||
<p class="agent-page__install-dialog-desc">智能投递助手需要配合浏览器插件使用<br/>请确认插件已成功安装并启用<br/>否则将无法正常进行自动填写和投递</p>
|
||
<!-- 底部按钮 -->
|
||
<div class="agent-page__install-dialog-btns">
|
||
<button class="agent-page__install-dialog-btn agent-page__install-dialog-btn--outline" @click="showInstallDialog = false">还未安装</button>
|
||
<button class="agent-page__install-dialog-btn agent-page__install-dialog-btn--primary" @click="handleInstallConfirmReload">已安装,继续</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</Teleport>
|
||
</div>
|
||
<!-- 功能说明 — 独立区块,仅第2步显示 -->
|
||
<div v-if="currentStep === 2" class="agent-page__step2-faq mrl24 mt16">
|
||
<h3 class="agent-page__step2-faq-title">功能说明</h3>
|
||
<div class="agent-page__step2-faq-item">
|
||
<div class="agent-page__step2-faq-item-title">为什么要选择求职偏好?</div>
|
||
<div class="agent-page__step2-faq-item-desc">城市、岗位、行业和招聘类型会影响岗位推荐范围,系统会优先筛选更符合你目标的投递机会。</div>
|
||
</div>
|
||
<div class="agent-page__step2-faq-item">
|
||
<div class="agent-page__step2-faq-item-title">为什么要选择简历?</div>
|
||
<div class="agent-page__step2-faq-item-desc">AI会基于你选择的简历进行岗位匹配度分析,差距识别和岗位专属优化,帮助提升投递成功率。</div>
|
||
</div>
|
||
<div class="agent-page__step2-faq-item">
|
||
<div class="agent-page__step2-faq-item-title">这些设置会影响后续哪些功能?</div>
|
||
<div class="agent-page__step2-faq-item-desc">会影响推荐岗位、匹配度评分、简历优化方向、自动投递队列和投递进度记录。</div>
|
||
</div>
|
||
<div class="agent-page__step2-faq-item">
|
||
<div class="agent-page__step2-faq-item-title">设置后还能修改吗?</div>
|
||
<div class="agent-page__step2-faq-item-desc">可以随时调整,修改后,系统会根据新的偏好和简历重新推荐结果。</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, computed, watch, onMounted } from 'vue'
|
||
import { useStore } from 'vuex'
|
||
import { formatEmploymentType, JOB_TYPE_OPTIONS } from '@/stores/index'
|
||
import ProfilePageContent from '@/components/ProfilePageContent.vue'
|
||
import ProfileEditDrawer from '@/components/ProfileEditDrawer.vue'
|
||
import JobGoalDialog from '@/components/JobGoalDialog.vue'
|
||
import RegionSelector from '@/components/tools/RegionSelector.vue'
|
||
import JobCategorySelector from '@/components/tools/JobCategorySelector.vue'
|
||
import IndustrySelector from '@/components/tools/IndustrySelector.vue'
|
||
import AgentBrowserInstallGuide from '@/components/AgentBrowserInstallGuide.vue'
|
||
import { saveProfile, saveEducation, saveWork, saveInternship, saveProject, saveCompetition, fetchProfile, fetchEducation, fetchWork, fetchInternship, fetchProject, fetchCompetition } from '@/api/profile'
|
||
import type { SaveEducationItem, SaveWorkItem, SaveProjectItem, SaveCompetitionItem } from '@/api/profile'
|
||
import { fetchResumeList, setDefaultResume } from '@/api/resume'
|
||
import type { ResumeListItem } from '@/api/resume'
|
||
import type { AgentConfig } from '@/api/agent'
|
||
|
||
const store = useStore()
|
||
|
||
/** 组件 Props — 接收父组件传入的初始配置数据 */
|
||
const props = defineProps<{
|
||
/** 从接口查询到的初始配置,可能为空 */
|
||
initialConfig?: AgentConfig | null
|
||
}>()
|
||
|
||
/** 组件事件 — complete: 设置完成时传回完整配置数据;launch: 点击启用按钮 */
|
||
const emit = defineEmits<{
|
||
(e: 'complete', data: Record<string, any>): void
|
||
(e: 'launch'): void
|
||
}>()
|
||
|
||
// ==================== 步骤导航 ====================
|
||
const steps = ['确认个人资料', '配置投递方案', '安装浏览器插件', '开启投递助手']
|
||
const currentStep = ref(1)
|
||
|
||
/** 进入下一步 */
|
||
function handleNext() {
|
||
if (currentStep.value < steps.length) currentStep.value++
|
||
}
|
||
|
||
/** 返回上一步 */
|
||
function handleBack() {
|
||
if (currentStep.value > 1) {
|
||
currentStep.value--
|
||
}
|
||
}
|
||
|
||
// ==================== 编辑抽屉状态 ====================
|
||
const showEditDrawer = ref(false)
|
||
const editModule = ref('info')
|
||
const editInitialData = ref<Record<string, any>>({})
|
||
|
||
// ==================== 个人档案数据 ====================
|
||
onMounted(async () => {
|
||
// 恢复刷新前保存的步骤号
|
||
const savedStep = sessionStorage.getItem('wizardStep')
|
||
if (savedStep) {
|
||
currentStep.value = Number(savedStep)
|
||
sessionStorage.removeItem('wizardStep')
|
||
}
|
||
|
||
// 刷新后检测插件是否在线
|
||
const needCheck = sessionStorage.getItem('wizardCheckPlug')
|
||
if (needCheck) {
|
||
sessionStorage.removeItem('wizardCheckPlug')
|
||
// 立即打开弹窗
|
||
showInstallDialog.value = true
|
||
// 等待心跳检测完成(最多等 3.5 秒)
|
||
await new Promise(resolve => setTimeout(resolve, 3500))
|
||
if (store.state.plugIsOnline) {
|
||
// 插件在线,关闭弹窗并直接进入下一步
|
||
showInstallDialog.value = false
|
||
handleNext()
|
||
} else {
|
||
// 插件离线,弹窗保持展示 + 提示
|
||
ElMessage.warning('未检测到插件安装')
|
||
}
|
||
}
|
||
|
||
if (!store.state.regions.length) store.dispatch('loadCommonData')
|
||
store.dispatch('loadJobIntention')
|
||
await loadProfile()
|
||
await loadEducation()
|
||
await loadWork()
|
||
await loadInternship()
|
||
await loadProject()
|
||
await loadCompetition()
|
||
})
|
||
|
||
// ==================== 通信桥接:插件在线状态从 store 全局获取 ====================
|
||
/** 插件是否在线(从 vuex 全局状态读取) */
|
||
const plugIsOnline = computed(() => store.state.plugIsOnline)
|
||
|
||
async function loadProfile() {
|
||
try {
|
||
const res = await fetchProfile()
|
||
if (res.code === '0' && res.data) {
|
||
const d = res.data
|
||
profile.value.name = d.name || ''; profile.value.phone = d.mobileNumber || ''
|
||
profile.value.email = d.email || ''; profile.value.idNumber = d.idCard || ''
|
||
profile.value.regionCode = d.regionCode || ''; profile.value.wechat = d.wechatNumber || ''
|
||
profile.value.skills = d.skills || []; profile.value.certificates = d.certificates || []
|
||
profile.value.portfolioUrl = d.portfolioUrl || ''
|
||
}
|
||
} catch { console.error('[AgentSetupWizard] 加载个人资料失败') }
|
||
}
|
||
async function loadEducation() {
|
||
try {
|
||
const res = await fetchEducation()
|
||
if (res.code === '0' && res.data) {
|
||
profile.value.education = res.data.map(item => ({ school: item.school || '', major: item.major || '', studyType: item.studyType ?? 0, degree: item.degree ?? 2, startDate: item.startDate || '', endDate: item.endDate || '', description: (item.description || []).map(d => ({ id: d.id || '', text: d.text || '' })) }))
|
||
}
|
||
} catch { console.error('[AgentSetupWizard] 加载教育经历失败') }
|
||
}
|
||
async function loadWork() {
|
||
try {
|
||
const res = await fetchWork()
|
||
if (res.code === '0' && res.data) {
|
||
profile.value.works = res.data.map(item => ({ companyName: item.companyName || '', position: item.position || '', startDate: item.startDate || '', endDate: item.endDate || '', description: (item.description || []).map(d => ({ id: d.id || '', text: d.text || '' })) }))
|
||
}
|
||
} catch { console.error('[AgentSetupWizard] 加载工作经历失败') }
|
||
}
|
||
async function loadInternship() {
|
||
try {
|
||
const res = await fetchInternship()
|
||
if (res.code === '0' && res.data) {
|
||
profile.value.internships = res.data.map(item => ({ companyName: item.companyName || '', position: item.position || '', startDate: item.startDate || '', endDate: item.endDate || '', description: (item.description || []).map(d => ({ id: d.id || '', text: d.text || '' })) }))
|
||
}
|
||
} catch { console.error('[AgentSetupWizard] 加载实习经历失败') }
|
||
}
|
||
async function loadProject() {
|
||
try {
|
||
const res = await fetchProject()
|
||
if (res.code === '0' && res.data) {
|
||
profile.value.projects = res.data.map(item => ({ projectName: item.projectName || '', companyName: item.companyName || '', role: item.role || '', startDate: item.startDate || '', endDate: item.endDate || '', description: (item.description || []).map(d => ({ id: d.id || '', text: d.text || '' })) }))
|
||
}
|
||
} catch { console.error('[AgentSetupWizard] 加载项目经历失败') }
|
||
}
|
||
async function loadCompetition() {
|
||
try {
|
||
const res = await fetchCompetition()
|
||
if (res.code === '0' && res.data) {
|
||
profile.value.competitions = res.data.map(item => ({ competitionName: item.competitionName || '', award: item.award || '', awardDate: item.awardDate || '', description: (item.description || []).map(d => ({ id: d.id || '', text: d.text || '' })) }))
|
||
}
|
||
} catch { console.error('[AgentSetupWizard] 加载竞赛经历失败') }
|
||
}
|
||
|
||
const profile = ref({
|
||
name: '', phone: '', email: '', idNumber: '', regionCode: '', portfolioUrl: '', wechat: '',
|
||
skills: [] as string[], certificates: [] as string[],
|
||
education: [] as Array<{ school: string; major: string; studyType: number; degree: number; startDate: string; endDate: string; description: Array<{ id: string; text: string }> }>,
|
||
works: [] as Array<{ companyName: string; position: string; startDate: string; endDate: string; description: Array<{ id: string; text: string }> }>,
|
||
internships: [] as Array<{ companyName: string; position: string; startDate: string; endDate: string; description: Array<{ id: string; text: string }> }>,
|
||
projects: [] as Array<{ projectName: string; companyName: string; role: string; startDate: string; endDate: string; description: Array<{ id: string; text: string }> }>,
|
||
competitions: [] as Array<{ competitionName: string; award: string; awardDate: string; description: Array<{ id: string; text: string }> }>,
|
||
})
|
||
|
||
// ==================== 编辑抽屉事件处理 ====================
|
||
function handleEdit(section: string) {
|
||
editModule.value = section
|
||
if (section === 'info') { editInitialData.value = { name: profile.value.name, email: profile.value.email, phone: profile.value.phone, location: profile.value.regionCode, wechat: profile.value.wechat } }
|
||
else if (section === 'education') { editInitialData.value = { education: profile.value.education.map(edu => ({ ...edu, description: edu.description.map(d => ({ ...d })) })) } }
|
||
else if (section === 'work') { editInitialData.value = { works: profile.value.works.map(exp => ({ ...exp, description: exp.description.map(d => ({ ...d })) })) } }
|
||
else if (section === 'internship') { editInitialData.value = { internships: (profile.value.internships || []).map(exp => ({ ...exp, description: exp.description.map(d => ({ ...d })) })) } }
|
||
else if (section === 'project') { editInitialData.value = { projects: (profile.value.projects || []).map(proj => ({ ...proj, description: proj.description.map(d => ({ ...d })) })) } }
|
||
else if (section === 'competition') { editInitialData.value = { competitions: profile.value.competitions.map(comp => ({ ...comp, description: comp.description.map(d => ({ ...d })) })) } }
|
||
else if (section === 'portfolio') { editInitialData.value = { portfolioUrl: profile.value.portfolioUrl } }
|
||
else if (section === 'skills') { editInitialData.value = { skills: [...profile.value.skills] } }
|
||
else if (section === 'certificate') { editInitialData.value = { certificates: [...(profile.value.certificates || [])] } }
|
||
else { editInitialData.value = {} }
|
||
showEditDrawer.value = true
|
||
}
|
||
|
||
/** 保存中的加载状态 */
|
||
const saving = ref(false)
|
||
|
||
/** 保存编辑数据 — 调用接口持久化 */
|
||
async function handleSaveEdit(data: Record<string, any>) {
|
||
if (editModule.value === 'info') {
|
||
try { saving.value = true; await saveProfile({ name: data.name, email: data.email, mobileNumber: data.phone, regionCode: data.location, wechatNumber: data.wechat }); profile.value.name = data.name; profile.value.email = data.email; profile.value.phone = data.phone; profile.value.wechat = data.wechat; profile.value.regionCode = data.location || ''; ElMessage.success('个人信息保存成功') } catch { ElMessage.error('个人信息保存失败,请重试') } finally { saving.value = false }
|
||
} else if (editModule.value === 'education') {
|
||
try { saving.value = true; const payload: SaveEducationItem[] = data.education.map((edu: any) => ({ school: edu.school, major: edu.major, degree: edu.degree, studyType: edu.studyType, startDate: edu.startDate, endDate: edu.endDate, description: edu.description.map((d: any) => ({ id: d.id, text: d.text })) })); await saveEducation(payload); profile.value.education = data.education.map((edu: any) => ({ school: edu.school, major: edu.major, studyType: edu.studyType, degree: edu.degree, startDate: edu.startDate, endDate: edu.endDate, description: edu.description.map((d: any) => ({ ...d })) })); ElMessage.success('教育经历保存成功') } catch { ElMessage.error('教育经历保存失败,请重试') } finally { saving.value = false }
|
||
} else if (editModule.value === 'work') {
|
||
try { saving.value = true; const payload: SaveWorkItem[] = data.works.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 })) })); await saveWork(payload); profile.value.works = data.works.map((w: any) => ({ companyName: w.companyName, position: w.position, startDate: w.startDate, endDate: w.endDate, description: w.description.map((d: any) => ({ ...d })) })); ElMessage.success('工作经历保存成功') } catch { ElMessage.error('工作经历保存失败,请重试') } finally { saving.value = false }
|
||
} else if (editModule.value === 'internship') {
|
||
try { saving.value = true; const payload: SaveWorkItem[] = data.internships.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 })) })); await saveInternship(payload); profile.value.internships = data.internships.map((i: any) => ({ companyName: i.companyName, position: i.position, startDate: i.startDate, endDate: i.endDate, description: i.description.map((d: any) => ({ ...d })) })); ElMessage.success('实习经历保存成功') } catch { ElMessage.error('实习经历保存失败,请重试') } finally { saving.value = false }
|
||
} else if (editModule.value === 'project') {
|
||
try { saving.value = true; const payload: SaveProjectItem[] = data.projects.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 })) })); await saveProject(payload); profile.value.projects = data.projects.map((p: any) => ({ projectName: p.projectName, companyName: p.companyName, role: p.role, startDate: p.startDate, endDate: p.endDate, description: p.description.map((d: any) => ({ ...d })) })); ElMessage.success('项目经历保存成功') } catch { ElMessage.error('项目经历保存失败,请重试') } finally { saving.value = false }
|
||
} else if (editModule.value === 'competition') {
|
||
try { saving.value = true; const payload: SaveCompetitionItem[] = data.competitions.map((c: any) => ({ competitionName: c.competitionName, award: c.award || '', awardDate: c.awardDate || '', description: c.description.map((d: any) => ({ id: d.id, text: d.text })) })); await saveCompetition(payload); profile.value.competitions = data.competitions.map((c: any) => ({ competitionName: c.competitionName, award: c.award, awardDate: c.awardDate, description: c.description.map((d: any) => ({ ...d })) })); ElMessage.success('竞赛经历保存成功') } catch { ElMessage.error('竞赛经历保存失败,请重试') } finally { saving.value = false }
|
||
} else if (editModule.value === 'portfolio') {
|
||
try { saving.value = true; await saveProfile({ name: profile.value.name, email: profile.value.email, mobileNumber: profile.value.phone, regionCode: profile.value.regionCode, wechatNumber: profile.value.wechat, skills: profile.value.skills, certificates: profile.value.certificates, portfolioUrl: data.portfolioUrl }); profile.value.portfolioUrl = data.portfolioUrl; ElMessage.success('作品集保存成功') } catch { ElMessage.error('作品集保存失败,请重试') } finally { saving.value = false }
|
||
} else if (editModule.value === 'skills') {
|
||
try { saving.value = true; await saveProfile({ name: profile.value.name, email: profile.value.email, mobileNumber: profile.value.phone, regionCode: profile.value.regionCode, wechatNumber: profile.value.wechat, skills: [...data.skills], certificates: profile.value.certificates }); profile.value.skills = [...data.skills]; ElMessage.success('技能保存成功') } catch { ElMessage.error('技能保存失败,请重试') } finally { saving.value = false }
|
||
} else if (editModule.value === 'certificate') {
|
||
try { saving.value = true; await saveProfile({ name: profile.value.name, email: profile.value.email, mobileNumber: profile.value.phone, regionCode: profile.value.regionCode, wechatNumber: profile.value.wechat, skills: profile.value.skills, certificates: [...data.certificates] }); profile.value.certificates = [...data.certificates]; ElMessage.success('证书保存成功') } catch { ElMessage.error('证书保存失败,请重试') } finally { saving.value = false }
|
||
}
|
||
}
|
||
|
||
/** 内联保存处理(个人信息/技能/证书模块从组件内直接保存) */
|
||
async function handleInlineSave(section: string, data: Record<string, any>) {
|
||
editModule.value = section
|
||
await handleSaveEdit(data)
|
||
}
|
||
|
||
/** 单条经历更新 — 替换到完整列表后整体保存 */
|
||
async function handleExpUpdate(moduleType: string, index: number, data: Record<string, any>) {
|
||
try {
|
||
if (moduleType === 'education') {
|
||
const list = profile.value.education.map((item, i) => i === index ? data : item) as any[]
|
||
await saveEducation(list)
|
||
profile.value.education = list
|
||
} else if (moduleType === 'work') {
|
||
const list = profile.value.works.map((item, i) => i === index ? data : item) as any[]
|
||
await saveWork(list)
|
||
profile.value.works = list
|
||
} else if (moduleType === 'internship') {
|
||
const list = profile.value.internships.map((item, i) => i === index ? data : item) as any[]
|
||
await saveInternship(list)
|
||
profile.value.internships = list
|
||
} else if (moduleType === 'project') {
|
||
const list = profile.value.projects.map((item, i) => i === index ? data : item) as any[]
|
||
await saveProject(list)
|
||
profile.value.projects = list
|
||
} else if (moduleType === 'competition') {
|
||
const list = profile.value.competitions.map((item, i) => i === index ? data : item) as any[]
|
||
await saveCompetition(list)
|
||
profile.value.competitions = list
|
||
}
|
||
ElMessage.success('保存成功')
|
||
} catch {
|
||
ElMessage.error('保存失败')
|
||
}
|
||
}
|
||
|
||
/** 新增经历 */
|
||
async function handleExpAdd(moduleType: string, data: Record<string, any>) {
|
||
try {
|
||
if (moduleType === 'education') {
|
||
const list = [...profile.value.education, data] as any[]
|
||
await saveEducation(list)
|
||
profile.value.education = list
|
||
} else if (moduleType === 'work') {
|
||
const list = [...profile.value.works, data] as any[]
|
||
await saveWork(list)
|
||
profile.value.works = list
|
||
} else if (moduleType === 'internship') {
|
||
const list = [...profile.value.internships, data] as any[]
|
||
await saveInternship(list)
|
||
profile.value.internships = list
|
||
} else if (moduleType === 'project') {
|
||
const list = [...profile.value.projects, data] as any[]
|
||
await saveProject(list)
|
||
profile.value.projects = list
|
||
} else if (moduleType === 'competition') {
|
||
const list = [...profile.value.competitions, data] as any[]
|
||
await saveCompetition(list)
|
||
profile.value.competitions = list
|
||
}
|
||
ElMessage.success('添加成功')
|
||
} catch {
|
||
ElMessage.error('添加失败')
|
||
}
|
||
}
|
||
|
||
/** 删除单条经历 — 确认后从列表移除并全量保存 */
|
||
async function handleExpDelete(moduleType: string, index: number) {
|
||
try {
|
||
await ElMessageBox.confirm('确定要删除这条经历吗?', '提示', {
|
||
confirmButtonText: '确定',
|
||
cancelButtonText: '取消',
|
||
type: 'warning',
|
||
})
|
||
} catch {
|
||
return
|
||
}
|
||
try {
|
||
if (moduleType === 'education') {
|
||
const list = profile.value.education.filter((_, i) => i !== index) as any[]
|
||
await saveEducation(list)
|
||
profile.value.education = list
|
||
} else if (moduleType === 'work') {
|
||
const list = profile.value.works.filter((_, i) => i !== index) as any[]
|
||
await saveWork(list)
|
||
profile.value.works = list
|
||
} else if (moduleType === 'internship') {
|
||
const list = profile.value.internships.filter((_, i) => i !== index) as any[]
|
||
await saveInternship(list)
|
||
profile.value.internships = list
|
||
} else if (moduleType === 'project') {
|
||
const list = profile.value.projects.filter((_, i) => i !== index) as any[]
|
||
await saveProject(list)
|
||
profile.value.projects = list
|
||
} else if (moduleType === 'competition') {
|
||
const list = profile.value.competitions.filter((_, i) => i !== index) as any[]
|
||
await saveCompetition(list)
|
||
profile.value.competitions = list
|
||
}
|
||
ElMessage.success('删除成功')
|
||
} catch {
|
||
ElMessage.error('删除失败')
|
||
}
|
||
}
|
||
|
||
// ==================== 第2步:确认目标 ====================
|
||
const showJobGoalDialog = ref(false)
|
||
const intentionEmploymentLabel = computed(() => formatEmploymentType(store.state.jobIntention.recruitCategory))
|
||
|
||
/** 招聘分类选项 */
|
||
const jobTypeOptions = JOB_TYPE_OPTIONS
|
||
/** 招聘分类下拉是否展开 */
|
||
const showJobTypeDropdown = ref(false)
|
||
/** 简历下拉是否展开 */
|
||
const showResumeDropdown = ref(false)
|
||
|
||
/** 当前选中简历名称 */
|
||
const selectedResumeName = computed(() => {
|
||
const item = resumeList.value.find(r => r.id === selectedResumeId.value)
|
||
return item ? item.resumeName : ''
|
||
})
|
||
|
||
/** 选中简历 */
|
||
function selectResume(r: { id?: string; resumeName?: string }) {
|
||
selectedResumeId.value = r.id || ''
|
||
showResumeDropdown.value = false
|
||
}
|
||
|
||
/** 选中招聘分类 */
|
||
function selectJobType(option: { label: string; value: number }) {
|
||
showJobTypeDropdown.value = false
|
||
store.dispatch('saveJobIntention', {
|
||
...store.state.jobIntention,
|
||
recruitCategory: option.value,
|
||
})
|
||
}
|
||
|
||
/** 第2步:求职偏好筛选组件 — 读写 store.jobIntention */
|
||
const step2RegionCodes = computed<string[]>(() => store.state.jobIntention.regionCodes || [])
|
||
const step2CategoryIds = computed<number[]>(() => store.state.jobIntention.categoryIds || [])
|
||
const step2IndustryIds = computed<number[]>(() => store.state.jobIntention.industryIds || [])
|
||
|
||
/** 第2步:筛选组件统一触发按钮样式 — 大圆角胶囊按钮 */
|
||
const prefFilterTriggerStyle = {
|
||
padding: '0.12rem 0.16rem',
|
||
borderRadius: '0.12rem',
|
||
fontSize: '0.14rem',
|
||
background: '#F3F4F6',
|
||
border: 'none',
|
||
minWidth: '0.9rem',
|
||
justifyContent: 'center',
|
||
height: '0.43rem',
|
||
boxSizing: 'border-box',
|
||
}
|
||
/** 第2步:筛选组件显示文字样式 */
|
||
const prefFilterDisplayStyle = {
|
||
maxWidth: 'none',
|
||
color: '#6A7282',
|
||
}
|
||
|
||
/** 第2步:地区选择变更 */
|
||
function onStep2RegionChange(codes: string[]) {
|
||
store.dispatch('saveJobIntention', { ...store.state.jobIntention, regionCodes: codes })
|
||
}
|
||
|
||
/** 第2步:岗位选择变更 */
|
||
function onStep2CategoryChange(ids: number[]) {
|
||
store.dispatch('saveJobIntention', { ...store.state.jobIntention, categoryIds: ids })
|
||
}
|
||
|
||
/** 第2步:行业选择变更 */
|
||
function onStep2IndustryChange(ids: number[]) {
|
||
store.dispatch('saveJobIntention', { ...store.state.jobIntention, industryIds: ids })
|
||
}
|
||
|
||
/** 第2步:简历列表 */
|
||
const resumeList = ref<ResumeListItem[]>([])
|
||
/** 选中的简历 ID */
|
||
const selectedResumeId = ref('')
|
||
/** 自动优化简历开关 */
|
||
const autoOptimizeSwitch = ref(true)
|
||
/** 岗位匹配分析开关 */
|
||
const autoMatchAnalysisSwitch = ref(true)
|
||
|
||
/** 加载简历列表 */
|
||
async function loadResumeList() {
|
||
try {
|
||
const res = await fetchResumeList()
|
||
if (res.code === '0' && res.data) {
|
||
resumeList.value = res.data
|
||
const defaultResume = res.data.find(r => r.isDefault === 1)
|
||
if (defaultResume && defaultResume.id) {
|
||
selectedResumeId.value = defaultResume.id
|
||
} else if (res.data.length > 0 && res.data[0].id) {
|
||
selectedResumeId.value = res.data[0].id
|
||
}
|
||
}
|
||
} catch { console.error('[AgentSetupWizard] 加载简历列表失败') }
|
||
}
|
||
|
||
/** 进入第2步时加载简历列表 */
|
||
watch(currentStep, (val) => { if (val === 2) loadResumeList() })
|
||
|
||
/** 选择简历后设为默认简历 */
|
||
watch(selectedResumeId, async (newId, oldId) => {
|
||
// 仅在用户主动切换时调用接口(非初始化加载)
|
||
if (!newId || !oldId) return
|
||
try {
|
||
await setDefaultResume(newId)
|
||
} catch { console.error('[AgentSetupWizard] 设置默认简历失败') }
|
||
})
|
||
|
||
// ==================== 第3步:安装插件 ====================
|
||
|
||
/** 浏览器插件安装指引组件引用 */
|
||
const browserInstallGuideRef = ref<InstanceType<typeof AgentBrowserInstallGuide> | null>(null)
|
||
|
||
/** 插件安装确认弹窗 */
|
||
const showInstallDialog = ref(false)
|
||
|
||
/** 点击"下载插件":下载插件文件 + 弹出安装确认弹窗 */
|
||
function handleInstallConfirm() {
|
||
// 触发下载插件文件
|
||
browserInstallGuideRef.value?.downloadExtension()
|
||
// 显示安装确认弹窗
|
||
showInstallDialog.value = true
|
||
}
|
||
|
||
/** 点击"已安装,继续":保存步骤 + 标记需要检测 + 刷新页面 */
|
||
function handleInstallConfirmReload() {
|
||
sessionStorage.setItem('wizardStep', String(currentStep.value))
|
||
sessionStorage.setItem('wizardCheckPlug', '1')
|
||
location.reload()
|
||
}
|
||
|
||
// ==================== 第4步:配置求职助手 ====================
|
||
|
||
/** 启用求职助手 — 收集第2步数据,通过 emit 传回父组件并触发 launch */
|
||
function handleLaunchAgent() {
|
||
const allSettings = {
|
||
jobType: store.state.jobIntention.recruitCategory === 2 ? 1 : 2,
|
||
agentMode: 1,
|
||
weeklyTarget: 2,
|
||
autoOptimizeResume: autoOptimizeSwitch.value ? 1 : 0,
|
||
autoMatchAnalysis: autoMatchAnalysisSwitch.value ? 1 : 0,
|
||
defaultResumeId: selectedResumeId.value,
|
||
status: 1,
|
||
}
|
||
emit('complete', allSettings)
|
||
emit('launch')
|
||
}
|
||
</script>
|
||
|
||
<style lang="scss">
|
||
@use '../assets/styles/pages/agent';
|
||
</style>
|