Files
offerpai_web/src/components/AgentSetupWizard.vue
T
2026-07-15 16:36:20 +08:00

714 lines
38 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>