575 lines
22 KiB
Vue
575 lines
22 KiB
Vue
<template>
|
||
<div class="job-detail dflex">
|
||
<SideNav />
|
||
<div class="job-detail__content">
|
||
<!-- 页面标题 + Tab 切换 -->
|
||
<JobPageHeader :activeTab="''" />
|
||
<!-- 主内容卡片 -->
|
||
<div class="job-detail__main-card">
|
||
<!-- 返回按钮 -->
|
||
<div class="job-detail__back-row">
|
||
<button class="job-detail__back-btn" @click="goBack">
|
||
<svg viewBox="0 0 16 16" fill="none" class="job-detail__back-icon">
|
||
<path d="M10 12L6 8l4-4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||
</svg>
|
||
<span>返回</span>
|
||
</button>
|
||
</div>
|
||
|
||
<!-- 内容区域(可滚动) -->
|
||
<div class="job-detail__body scrollbar-thin">
|
||
<!-- 加载中骨架屏 -->
|
||
<el-skeleton :loading="loading" animated :count="1">
|
||
<template #template>
|
||
<div class="job-detail__skeleton">
|
||
<!-- 模拟岗位标题行 -->
|
||
<el-skeleton-item variant="h1" style="width: 40%; height: 0.28rem; margin-bottom: 0.16rem;" />
|
||
<!-- 模拟标签行 -->
|
||
<div style="display: flex; gap: 0.1rem; margin-bottom: 0.2rem;">
|
||
<el-skeleton-item variant="text" style="width: 0.6rem; height: 0.2rem;" />
|
||
<el-skeleton-item variant="text" style="width: 0.8rem; height: 0.2rem;" />
|
||
<el-skeleton-item variant="text" style="width: 0.5rem; height: 0.2rem;" />
|
||
</div>
|
||
<!-- 模拟信息区 -->
|
||
<el-skeleton-item variant="text" style="width: 80%; height: 0.16rem; margin-bottom: 0.12rem;" />
|
||
<el-skeleton-item variant="text" style="width: 60%; height: 0.16rem; margin-bottom: 0.24rem;" />
|
||
<!-- 模拟描述区 -->
|
||
<el-skeleton-item variant="h3" style="width: 20%; height: 0.2rem; margin-bottom: 0.16rem;" />
|
||
<el-skeleton-item variant="text" style="width: 100%; height: 0.14rem; margin-bottom: 0.1rem;" />
|
||
<el-skeleton-item variant="text" style="width: 95%; height: 0.14rem; margin-bottom: 0.1rem;" />
|
||
<el-skeleton-item variant="text" style="width: 90%; height: 0.14rem; margin-bottom: 0.1rem;" />
|
||
<el-skeleton-item variant="text" style="width: 85%; height: 0.14rem; margin-bottom: 0.1rem;" />
|
||
<el-skeleton-item variant="text" style="width: 70%; height: 0.14rem; margin-bottom: 0.24rem;" />
|
||
<!-- 模拟公司信息区 -->
|
||
<el-skeleton-item variant="h3" style="width: 25%; height: 0.2rem; margin-bottom: 0.16rem;" />
|
||
<div style="display: flex; align-items: center; gap: 0.12rem; margin-bottom: 0.12rem;">
|
||
<el-skeleton-item variant="circle" style="width: 0.4rem; height: 0.4rem;" />
|
||
<el-skeleton-item variant="text" style="width: 1.2rem; height: 0.18rem;" />
|
||
</div>
|
||
<el-skeleton-item variant="text" style="width: 100%; height: 0.14rem; margin-bottom: 0.1rem;" />
|
||
<el-skeleton-item variant="text" style="width: 80%; height: 0.14rem;" />
|
||
</div>
|
||
</template>
|
||
<template #default>
|
||
<!-- 岗位基本信息区 -->
|
||
<div class="job-detail__header-section">
|
||
<!-- 左侧:岗位信息 -->
|
||
<div class="job-detail__basic-info">
|
||
<!-- 岗位标题 + 收藏 -->
|
||
<div class="job-detail__title-row">
|
||
<h1 class="job-detail__job-title">{{ job.title }}</h1>
|
||
<button class="job-detail__fav-btn" :class="{ 'job-detail__fav-btn--active': job.isFavorite }" aria-label="收藏" @click="toggleFavorite">
|
||
<svg viewBox="0 0 16 16" class="job-detail__fav-icon">
|
||
<path d="M8 13.7l-1.1-1C3.6 9.8 1.5 7.9 1.5 5.7 1.5 3.9 2.9 2.5 4.7 2.5c1 0 2 .5 2.6 1.2h1.4c.6-.7 1.6-1.2 2.6-1.2 1.8 0 3.2 1.4 3.2 3.2 0 2.2-2.1 4.1-5.4 6.9L8 13.7z" stroke="currentColor" stroke-width="1" fill="none"/>
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
<!-- 岗位 meta 信息 -->
|
||
<div class="job-detail__meta-row">
|
||
<span>{{ job.location }}</span>
|
||
<span class="job-detail__meta-dot">·</span>
|
||
<span>{{ job.experience }}</span>
|
||
<span class="job-detail__meta-dot">·</span>
|
||
<span>{{ job.education }}</span>
|
||
<span class="job-detail__meta-dot">·</span>
|
||
<span>{{ job.salary }}</span>
|
||
</div>
|
||
<!-- 操作按钮 -->
|
||
<div class="job-detail__action-btns">
|
||
<button class="job-detail__nova-btn" @click="handleAskNova">问Nova</button>
|
||
<button class="job-detail__apply-btn" @click="handleApply">去投递</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 右侧:匹配度卡片 -->
|
||
<div class="job-detail__match-card" :class="matchCardClass">
|
||
<div class="job-detail__match-top">
|
||
<span class="job-detail__match-score">{{ job.matchScore }}%</span>
|
||
<span class="job-detail__match-label">{{ matchLevelText }}</span>
|
||
</div>
|
||
<div class="job-detail__match-list">
|
||
<div class="job-detail__match-row" v-for="m in matchItems" :key="m.label">
|
||
<span class="job-detail__match-row-label">{{ m.label }}</span>
|
||
<span class="job-detail__match-row-score">{{ m.score }}%</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 优化简历提示条 -->
|
||
<div class="job-detail__optimize-bar">
|
||
<span class="job-detail__optimize-text">优化简历大幅提升面试成功率</span>
|
||
<button class="job-detail__optimize-btn" @click="handleGenerateResume">生成岗位专属简历</button>
|
||
</div>
|
||
|
||
<!-- 岗位职责 -->
|
||
<div class="job-detail__section">
|
||
<h3 class="job-detail__section-title">岗位职责</h3>
|
||
<div class="job-detail__section-list">
|
||
<div class="job-detail__section-item" v-for="(item, i) in job.responsibilities" :key="i">{{ item }}</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 任职要求 -->
|
||
<div class="job-detail__section">
|
||
<h3 class="job-detail__section-title">任职要求</h3>
|
||
<!-- 技能标签 -->
|
||
<div class="job-detail__skill-tags">
|
||
<span
|
||
v-for="skill in job.requiredSkills"
|
||
:key="skill.name"
|
||
class="job-detail__skill-tag"
|
||
:class="{ 'job-detail__skill-tag--matched': skill.matched }"
|
||
>{{ skill.name }}</span>
|
||
</div>
|
||
<!-- 要求列表 -->
|
||
<div class="job-detail__section-list">
|
||
<div class="job-detail__section-item" v-for="(item, i) in job.requirements" :key="i">{{ item }}</div>
|
||
</div>
|
||
<!-- 加分项 -->
|
||
<div v-if="job.bonus" class="job-detail__bonus-item">加分项:{{ job.bonus }}</div>
|
||
</div>
|
||
|
||
<!-- 公司概况 -->
|
||
<div class="job-detail__section">
|
||
<h3 class="job-detail__section-title">公司概况</h3>
|
||
<div class="job-detail__company-block">
|
||
<!-- 公司 logo + 名称 + 标签 -->
|
||
<div class="job-detail__company-head">
|
||
<div class="job-detail__company-logo">
|
||
<img v-if="job.companyLogoUrl" :src="job.companyLogoUrl" :alt="job.company" class="job-detail__company-logo-img" />
|
||
<svg v-else viewBox="0 0 32 32" fill="none" class="job-detail__company-svg">
|
||
<rect x="4" y="8" width="24" height="18" rx="3" stroke="currentColor" stroke-width="1.5"/>
|
||
<path d="M10 8V6a3 3 0 013-3h6a3 3 0 013 3v2" stroke="currentColor" stroke-width="1.5"/>
|
||
</svg>
|
||
</div>
|
||
<div class="job-detail__company-name-info">
|
||
<span class="job-detail__company-name">{{ job.company }}</span>
|
||
<span class="job-detail__company-sub">{{ job.companyInfo.industryName }}{{ job.companyInfo.size ? ' · ' + job.companyInfo.size : '' }}</span>
|
||
</div>
|
||
</div>
|
||
<!-- 公司描述 -->
|
||
<p class="job-detail__company-desc">{{ job.companyInfo.description }}</p>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
</el-skeleton>
|
||
</div>
|
||
</div>
|
||
|
||
|
||
|
||
</div>
|
||
<AiChat ref="aiChatRef" :job-id="jobId" />
|
||
|
||
<!-- 岗位专属简历定制弹窗 -->
|
||
<JobResumeCustomDialog v-model="showResumeCustomDialog" :job-info="resumeCustomJobInfo" :job-id="jobId" @skip="handleSkipToApply" />
|
||
|
||
<!-- 会员权限拦截弹窗 -->
|
||
<MemberAccessDialog v-model="showMemberAccessDialog" @open-member="showMemberDialog = true" />
|
||
<!-- 会员购买弹窗 -->
|
||
<MemberDialog v-model="showMemberDialog" />
|
||
<!-- 全屏加载遮罩 -->
|
||
<FullscreenLoading :visible="fullLoading" :text="fullLoadingText" title="Nova 正在帮您努力处理~" />
|
||
|
||
<!-- 岗位简历生成步骤进度遮罩 -->
|
||
<StepProgressOverlay
|
||
v-if="showStepProgress"
|
||
:title="stepProgressTitle"
|
||
:subtitle="stepProgressSubtitle"
|
||
:steps="stepProgressSteps"
|
||
:durations="stepProgressDurations"
|
||
:done="stepProgressDone"
|
||
@finished="handleStepProgressFinished"
|
||
@close="handleStepProgressClose"
|
||
/>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, reactive, computed, onMounted } from 'vue'
|
||
import { useRouter, useRoute } from 'vue-router'
|
||
import { useStore } from 'vuex'
|
||
import { formatEmploymentType } from '@/stores/index'
|
||
import SideNav from '@/components/SideNav.vue'
|
||
import AiChat from '@/components/AiChat.vue'
|
||
import JobPageHeader from '@/components/JobPageHeader.vue'
|
||
import JobResumeCustomDialog from '@/components/JobResumeCustomDialog.vue'
|
||
import MemberAccessDialog from '@/components/MemberAccessDialog.vue'
|
||
import FullscreenLoading from '@/components/FullscreenLoading.vue'
|
||
import MemberDialog from '@/components/MemberDialog.vue'
|
||
import StepProgressOverlay from '@/components/StepProgressOverlay.vue'
|
||
import { fetchJobDetail, toggleJobFavorite, removeJobFavorite, fetchSkillGap } from '@/api/jobs'
|
||
import { fetchMemberStatus } from '@/api/member'
|
||
import type { JobDetailData, SkillGapData } from '@/api/jobs'
|
||
|
||
// ==================== 路由相关 ====================
|
||
|
||
const router = useRouter()
|
||
const route = useRoute()
|
||
const store = useStore()
|
||
|
||
// ==================== 全屏加载状态 ====================
|
||
/** 全屏加载是否显示 */
|
||
const fullLoading = ref(false)
|
||
/** 全屏加载文案 */
|
||
const fullLoadingText = ref('')
|
||
|
||
/** 当前岗位 ID,从路由参数中获取 */
|
||
const jobId = route.params.id as string
|
||
|
||
// ==================== 工具函数 ====================
|
||
|
||
/** 工作类型映射:使用全局统一的 formatEmploymentType */
|
||
|
||
/** 学历要求映射:数字 → 中文 */
|
||
function formatEducation(edu: number | undefined): string {
|
||
const map: Record<number, string> = { 0: '不限', 1: '大专', 2: '本科', 3: '硕士', 4: '博士' }
|
||
return map[edu ?? -1] ?? '未知'
|
||
}
|
||
|
||
/** 最低工作年限格式化 */
|
||
function formatExperience(min: number | undefined): string {
|
||
if (min === undefined || min === null) return '经验不限'
|
||
if (min === 0) return '经验不限'
|
||
return `${min}年以上`
|
||
}
|
||
|
||
/**
|
||
* 将带序号的文本拆分为列表项
|
||
* 例如 "1.xxx;2.yyy;3.zzz" → ['xxx', 'yyy', 'zzz']
|
||
*/
|
||
function splitNumberedText(text: string | undefined): string[] {
|
||
if (!text) return []
|
||
// 按 "数字." 或 "数字、" 分割,过滤空项
|
||
const items = text.split(/\d+[.、]/).filter((s) => s.trim())
|
||
return items.map((s) => s.trim().replace(/[;;]$/, ''))
|
||
}
|
||
|
||
// ==================== 页面状态 ====================
|
||
|
||
/** 当前激活的内容区 Tab(保留兼容性但不再使用) */
|
||
|
||
/** 是否正在加载 */
|
||
const loading = ref(false)
|
||
|
||
// ==================== 岗位详情数据 ====================
|
||
|
||
/** 匹配度维度列表(岗位匹配环下方的子维度) */
|
||
const matchItems = ref([
|
||
{ label: '教育背景', score: 0 },
|
||
{ label: '核心技能', score: 0 },
|
||
{ label: '过往经历', score: 0 },
|
||
])
|
||
|
||
/** 匹配度卡片动态class:根据分数区间切换高/中/低样式 */
|
||
const matchCardClass = computed(() => {
|
||
const score = job.matchScore
|
||
if (score >= 80) return 'job-detail__match-card--high'
|
||
if (score >= 40) return 'job-detail__match-card--medium'
|
||
return 'job-detail__match-card--low'
|
||
})
|
||
|
||
/** 匹配度等级文字 */
|
||
const matchLevelText = computed(() => {
|
||
const score = job.matchScore
|
||
if (score >= 80) return '高匹配度'
|
||
if (score >= 40) return '中匹配度'
|
||
return '低匹配度'
|
||
})
|
||
|
||
/** 技能标签项(matched 表示用户是否具备该技能,可点击切换) */
|
||
interface SkillTag {
|
||
/** 技能名称 */
|
||
name: string
|
||
/** 是否匹配 */
|
||
matched: boolean
|
||
}
|
||
|
||
/** 岗位详情响应式数据 */
|
||
const job = reactive({
|
||
/** 岗位 ID */
|
||
id: jobId,
|
||
/** 公司名称(优先简称) */
|
||
company: '',
|
||
/** 公司 Logo URL */
|
||
companyLogoUrl: '',
|
||
/** 岗位标题 */
|
||
title: '',
|
||
/** 地区名称 */
|
||
location: '',
|
||
/** 工作经验要求(格式化后的文案) */
|
||
experience: '',
|
||
/** 工作类型(格式化后的文案) */
|
||
type: '',
|
||
/** 学历要求(格式化后的文案) */
|
||
education: '',
|
||
/** 薪资描述 */
|
||
salary: '',
|
||
/** 匹配总分 */
|
||
matchScore: 0,
|
||
/** 岗位描述(原始文本) */
|
||
description: '',
|
||
/** 岗位标签 */
|
||
tags: [] as string[],
|
||
/** 岗位职责列表(从 description 拆分) */
|
||
responsibilities: [] as string[],
|
||
/** 技能标签 */
|
||
requiredSkills: [] as SkillTag[],
|
||
/** 任职要求列表(从 requirement 拆分) */
|
||
requirements: [] as string[],
|
||
/** 加分项 */
|
||
bonus: '',
|
||
/** 来源链接 */
|
||
sourceUrl: '',
|
||
/** 是否已收藏 */
|
||
isFavorite: false,
|
||
/** 公司信息 */
|
||
companyInfo: {
|
||
/** 公司类型 */
|
||
type: '',
|
||
/** 公司所属行业 */
|
||
industryName: '',
|
||
/** 公司简介 */
|
||
summary: '',
|
||
/** 公司描述 */
|
||
description: '',
|
||
/** 成立时间 */
|
||
founded: '',
|
||
/** 公司地址 */
|
||
address: '',
|
||
/** 公司规模 */
|
||
size: '',
|
||
/** 公司官网 */
|
||
website: '',
|
||
/** 融资状态 */
|
||
fundingStage: '',
|
||
/** 最新估值 */
|
||
valuation: '',
|
||
/** 公司标签 */
|
||
tags: [] as string[],
|
||
/** 公司新闻列表 */
|
||
news: [] as string[],
|
||
},
|
||
})
|
||
|
||
/** 将接口返回数据填充到页面响应式对象 */
|
||
function fillJobData(data: JobDetailData) {
|
||
job.id = data.jobId
|
||
job.company = data.companyShortName || data.companyName || ''
|
||
job.companyLogoUrl = data.companyLogoUrl || ''
|
||
job.title = data.jobTitle || ''
|
||
job.location = data.regionName || ''
|
||
job.experience = formatExperience(data.minExperience)
|
||
job.type = formatEmploymentType(data.recruitCategory)
|
||
job.education = formatEducation(data.education)
|
||
job.salary = data.salary || ''
|
||
job.matchScore = data.matchScore ?? 0
|
||
job.description = data.description || ''
|
||
job.tags = data.tags || []
|
||
job.sourceUrl = data.sourceUrl || ''
|
||
job.isFavorite = data.isFavorite ?? false
|
||
job.bonus = data.bonus || ''
|
||
|
||
// 岗位职责:从 description 中按序号拆分
|
||
job.responsibilities = splitNumberedText(data.description)
|
||
|
||
// 任职要求:从 requirement 中按序号拆分
|
||
job.requirements = splitNumberedText(data.requirement)
|
||
|
||
// 技能标签:默认 matched 为 false
|
||
job.requiredSkills = (data.skillTags || []).map((name) => ({ name, matched: false }))
|
||
|
||
// 匹配度详情
|
||
if (data.matchDetail) {
|
||
matchItems.value = [
|
||
{ label: '教育背景', score: data.matchDetail.educationScore ?? 0 },
|
||
{ label: '核心技能', score: data.matchDetail.skillScore ?? 0 },
|
||
{ label: '过往经历', score: data.matchDetail.experienceScore ?? 0 },
|
||
]
|
||
}
|
||
|
||
// 公司信息
|
||
job.companyInfo.type = data.companyType || ''
|
||
job.companyInfo.industryName = data.companyIndustryName || ''
|
||
job.companyInfo.summary = data.companySummary || ''
|
||
job.companyInfo.description = data.companyDescription || ''
|
||
job.companyInfo.founded = data.companyFoundedYear ? `${data.companyFoundedYear}年` : ''
|
||
job.companyInfo.address = data.companyAddress || ''
|
||
job.companyInfo.size = data.companyScale || ''
|
||
job.companyInfo.website = data.companyWebsite || ''
|
||
job.companyInfo.fundingStage = data.companyFinancingStage || ''
|
||
job.companyInfo.valuation = data.companyLatestValuation || ''
|
||
job.companyInfo.tags = data.companyTags || []
|
||
job.companyInfo.news = data.companyNews || []
|
||
}
|
||
|
||
// ==================== 加载岗位详情 ====================
|
||
|
||
/** 调用接口获取岗位详情 */
|
||
async function loadJobDetail() {
|
||
if (!jobId) return
|
||
loading.value = true
|
||
try {
|
||
const res = await fetchJobDetail(jobId)
|
||
if (res.code === '0' && res.data) {
|
||
fillJobData(res.data)
|
||
}
|
||
} catch (e) {
|
||
console.error('加载岗位详情失败', e)
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
onMounted(() => {
|
||
loadJobDetail()
|
||
})
|
||
|
||
// ==================== 事件处理 ====================
|
||
|
||
/** 返回职位列表页 */
|
||
function goBack() {
|
||
if (window.history.length > 1) {
|
||
router.back()
|
||
} else {
|
||
router.push('/jobs')
|
||
}
|
||
}
|
||
|
||
/** AiChat 组件引用 */
|
||
const aiChatRef = ref<InstanceType<typeof AiChat> | null>(null)
|
||
|
||
/** 问 Nova — 触发 AI 聊天,推送提示消息 */
|
||
function handleAskNova() {
|
||
aiChatRef.value?.triggerAsk()
|
||
}
|
||
|
||
// ==================== 岗位专属简历定制弹窗 ====================
|
||
|
||
/** 简历定制弹窗显隐 */
|
||
const showResumeCustomDialog = ref(false)
|
||
|
||
/** 会员权限拦截弹窗 */
|
||
const showMemberAccessDialog = ref(false)
|
||
/** 会员购买弹窗 */
|
||
const showMemberDialog = ref(false)
|
||
|
||
/** 技能差距分析数据 */
|
||
const skillGapData = ref<SkillGapData | null>(null)
|
||
|
||
/** 传递给简历定制弹窗的岗位信息(从 skill-gap 接口获取) */
|
||
const resumeCustomJobInfo = computed(() => ({
|
||
title: skillGapData.value?.job?.title || job.title,
|
||
company: job.company,
|
||
companyLogoUrl: job.companyLogoUrl,
|
||
location: job.location,
|
||
matchScore: skillGapData.value?.score ?? +(job.matchScore / 10).toFixed(1),
|
||
missingSkills: skillGapData.value?.missingSkills || skillGapData.value?.gaps?.map(g => g.keyword) || [],
|
||
keywords: skillGapData.value?.job?.skillTags || job.requiredSkills.map(s => s.name),
|
||
sourceUrl: job.sourceUrl,
|
||
/** 差距分析列表(来自 skill-gap 接口) */
|
||
gaps: skillGapData.value?.gaps || [],
|
||
/** 默认简历信息(来自 skill-gap 接口) */
|
||
defaultResume: skillGapData.value?.resume || null,
|
||
}))
|
||
|
||
/** 生成岗位专属简历 — 先检查会员状态,非会员弹权限拦截弹窗 */
|
||
async function handleGenerateResume() {
|
||
// 先检查会员状态
|
||
try {
|
||
const statusRes = await fetchMemberStatus()
|
||
if (statusRes.code === '0' && statusRes.data && !statusRes.data.isMember) {
|
||
// 非会员 — 弹出会员权限拦截弹窗
|
||
showMemberAccessDialog.value = true
|
||
return
|
||
}
|
||
} catch {
|
||
// 接口异常时不阻断,继续执行
|
||
}
|
||
|
||
// 显示步骤进度遮罩
|
||
showStepProgress.value = true
|
||
stepProgressDone.value = false
|
||
|
||
try {
|
||
const res = await fetchSkillGap(jobId)
|
||
if (res.code === 0 && res.data) {
|
||
skillGapData.value = res.data
|
||
// 通知组件接口已完成
|
||
stepProgressDone.value = true
|
||
} else {
|
||
// 接口返回异常,关闭遮罩提示错误
|
||
showStepProgress.value = false
|
||
ElMessage.error('分析失败,请稍后重试')
|
||
}
|
||
} catch (e) {
|
||
console.error('技能差距分析失败', e)
|
||
showStepProgress.value = false
|
||
ElMessage.error('分析失败,请稍后重试')
|
||
}
|
||
}
|
||
|
||
// ==================== 步骤进度遮罩相关 ====================
|
||
|
||
/** 步骤进度遮罩是否显示 */
|
||
const showStepProgress = ref(false)
|
||
/** 接口是否已完成 */
|
||
const stepProgressDone = ref(false)
|
||
/** 步骤进度大标题 */
|
||
const stepProgressTitle = '分析简历中,请耐心等待'
|
||
/** 步骤进度副标题 */
|
||
const stepProgressSubtitle = '正在根据岗位所需技能、简历详情发现问题'
|
||
/** 步骤名称 */
|
||
const stepProgressSteps = ['岗位技能解析...', '岗位职责解析...', '简历经历技能匹配...', '优化建议最终生成...']
|
||
/** 每步模拟时长(毫秒) */
|
||
const stepProgressDurations = [2000, 2000, 2000, 2000]
|
||
|
||
/** 步骤进度走完后打开简历定制弹窗 */
|
||
function handleStepProgressFinished() {
|
||
showStepProgress.value = false
|
||
showResumeCustomDialog.value = true
|
||
}
|
||
|
||
/** 关闭步骤进度遮罩 */
|
||
function handleStepProgressClose() {
|
||
showStepProgress.value = false
|
||
}
|
||
|
||
/** 不优化直接投递 */
|
||
function handleSkipToApply() {
|
||
if (job.sourceUrl) {
|
||
window.open(job.sourceUrl, '_blank')
|
||
}
|
||
}
|
||
|
||
/** 收藏/取消收藏 */
|
||
async function toggleFavorite() {
|
||
try {
|
||
const res = job.isFavorite
|
||
? await removeJobFavorite(job.id)
|
||
: await toggleJobFavorite(job.id)
|
||
if (res.code === '0') {
|
||
const wasFavorite = job.isFavorite
|
||
job.isFavorite = !job.isFavorite
|
||
ElMessage.success(wasFavorite ? '已取消收藏' : '收藏成功')
|
||
// 同步收藏状态到 Jobs 页面缓存,返回时无需刷新列表即可看到最新状态
|
||
const cache = store.state.jobListCache
|
||
if (cache) {
|
||
const cached = cache.list.find((j: any) => j.id === job.id)
|
||
if (cached) cached.isFavorite = job.isFavorite
|
||
}
|
||
}
|
||
} catch (e) {
|
||
console.error('收藏操作失败', e)
|
||
}
|
||
}
|
||
|
||
/** 去投递 — 跳转到来源链接 */
|
||
function handleApply() {
|
||
if (job.sourceUrl) {
|
||
window.open(job.sourceUrl, '_blank')
|
||
}
|
||
}
|
||
</script>
|