631 lines
25 KiB
Vue
631 lines
25 KiB
Vue
<template>
|
||
<div class="job-detail dflex">
|
||
<SideNav />
|
||
<div class="job-detail__content">
|
||
<!-- 页面标题 + Tab 切换 -->
|
||
<JobPageHeader :activeTab="''" />
|
||
<div class="bg-white p20 border-ra20">
|
||
<div class="bg-main p20 border-ra20">
|
||
<!-- 顶部操作栏 -->
|
||
<div class="job-detail__toolbar">
|
||
<button class="job-detail__close-btn" @click="goBack" aria-label="关闭">
|
||
<svg viewBox="0 0 16 16" fill="none" class="job-detail__close-icon">
|
||
<path d="M12 4L4 12M4 4l8 8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
||
</svg>
|
||
</button>
|
||
<div class="job-detail__toolbar-right">
|
||
<button class="job-detail__tool-btn" aria-label="编辑" @click="openDislikeDialog">
|
||
<svg viewBox="0 0 16 16" fill="none" class="job-detail__tool-icon">
|
||
<path d="M11.5 2.5l2 2L5 13H3v-2l8.5-8.5z" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/>
|
||
</svg>
|
||
</button>
|
||
<button class="jobs-page__action-icon-btn" :class="{ 'job-detail__tool-btn--liked': job.isFavorite }" aria-label="收藏" @click="toggleFavorite">
|
||
<svg viewBox="0 0 16 16" :fill="job.isFavorite ? 'currentColor' : 'none'" class="job-detail__tool-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"/>
|
||
</svg>
|
||
</button>
|
||
<button class="job-detail__apply-btn" @click="handleApply">去投递</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 内容区域(可滚动) -->
|
||
<div class="job-detail__body">
|
||
<!-- 导航 Tab:岗位详情 / 公司概况 -->
|
||
<div class="job-detail__nav-tabs">
|
||
<div
|
||
class="job-detail__nav-tab"
|
||
:class="{ 'job-detail__nav-tab--active': activeSection === 'job' }"
|
||
@click="activeSection = 'job'"
|
||
>岗位详情</div>
|
||
<div
|
||
class="job-detail__nav-tab"
|
||
:class="{ 'job-detail__nav-tab--active': activeSection === 'company' }"
|
||
@click="scrollToCompany"
|
||
>公司概况</div>
|
||
<div class="job-detail__nav-tab-right">
|
||
<span class="job-detail__link-btn" @click="handleFeedback">问题反馈</span>
|
||
<span class="job-detail__link-btn" @click="handleReport">原链接</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 岗位详情内容 -->
|
||
<template v-if="activeSection === 'job'">
|
||
<!-- 公司 & 职位头部 -->
|
||
<div class="job-detail__card">
|
||
<div class="job-detail__card-top">
|
||
<div class="job-detail__card-left">
|
||
<div class="job-detail__company-row">
|
||
<div class="job-detail__company-icon">
|
||
<img v-if="job.companyLogoUrl" :src="job.companyLogoUrl" :alt="job.company" class="job-detail__company-logo-img" />
|
||
<svg v-else viewBox="0 0 24 24" fill="none" class="job-detail__company-svg">
|
||
<rect x="3" y="7" width="18" height="14" rx="2" stroke="currentColor" stroke-width="1.5"/>
|
||
<path d="M7 7V5a2 2 0 012-2h6a2 2 0 012 2v2" stroke="currentColor" stroke-width="1.5"/>
|
||
<path d="M3 13h18" stroke="currentColor" stroke-width="1.5"/>
|
||
</svg>
|
||
</div>
|
||
<span class="job-detail__company-name">{{ job.company }}</span>
|
||
</div>
|
||
<h3 class="job-detail__job-title">{{ job.title }}</h3>
|
||
<div class="job-detail__job-meta">
|
||
<span class="job-detail__meta-item">
|
||
<svg viewBox="0 0 16 16" fill="none" class="job-detail__meta-icon">
|
||
<circle cx="8" cy="6.5" r="2.5" stroke="currentColor" stroke-width="1.2"/>
|
||
<path d="M8 14s-5-4-5-7.5a5 5 0 0110 0C13 10 8 14 8 14z" stroke="currentColor" stroke-width="1.2"/>
|
||
</svg>
|
||
{{ job.location }}
|
||
</span>
|
||
<span class="job-detail__meta-item">
|
||
<svg viewBox="0 0 16 16" fill="none" class="job-detail__meta-icon">
|
||
<rect x="2" y="3" width="12" height="11" rx="1.5" stroke="currentColor" stroke-width="1.2"/>
|
||
<path d="M2 6.5h12" stroke="currentColor" stroke-width="1.2"/>
|
||
</svg>
|
||
{{ job.experience }}
|
||
</span>
|
||
<span class="job-detail__meta-item">
|
||
<svg viewBox="0 0 16 16" fill="none" class="job-detail__meta-icon">
|
||
<rect x="2" y="2" width="12" height="12" rx="2" stroke="currentColor" stroke-width="1.2"/>
|
||
<path d="M5 8h6M8 5v6" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/>
|
||
</svg>
|
||
{{ job.type }}
|
||
</span>
|
||
<span v-if="job.education" class="job-detail__meta-item">
|
||
<svg viewBox="0 0 16 16" fill="none" class="job-detail__meta-icon">
|
||
<path d="M8 2L1 6l7 4 7-4-7-4z" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/>
|
||
<path d="M3 7.5v4l5 3 5-3v-4" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/>
|
||
</svg>
|
||
{{ job.education }}
|
||
</span>
|
||
<span v-if="job.salary" class="job-detail__meta-item">
|
||
<svg viewBox="0 0 16 16" fill="none" class="job-detail__meta-icon">
|
||
<path d="M8 1v14M4 4h8M3 8h10M5 12h6" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/>
|
||
</svg>
|
||
{{ job.salary }}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
<!-- 匹配度环 -->
|
||
<div class="job-detail__match-area">
|
||
<div class="job-detail__match-ring">
|
||
<svg viewBox="0 0 80 80" class="job-detail__ring-svg">
|
||
<circle cx="40" cy="40" r="34" stroke-width="5" stroke="#E8E8E8" fill="none" opacity="0.3"/>
|
||
<circle cx="40" cy="40" r="34" stroke-width="5" fill="none"
|
||
stroke="#4FC2C9"
|
||
stroke-linecap="round"
|
||
:stroke-dasharray="2 * Math.PI * 34"
|
||
:stroke-dashoffset="2 * Math.PI * 34 * (1 - job.matchScore / 100)"
|
||
transform="rotate(-90 40 40)"
|
||
/>
|
||
</svg>
|
||
<div class="job-detail__match-score">{{ job.matchScore }}%</div>
|
||
</div>
|
||
<div class="job-detail__match-label">岗位匹配值</div>
|
||
<div class="job-detail__match-details">
|
||
<div class="job-detail__match-item" v-for="m in matchItems" :key="m.label">
|
||
<div class="job-detail__match-mini-ring">
|
||
<svg viewBox="0 0 40 40" class="job-detail__mini-ring-svg">
|
||
<circle cx="20" cy="20" r="16" stroke-width="3" stroke="#E8E8E8" fill="none" opacity="0.3"/>
|
||
<circle cx="20" cy="20" r="16" stroke-width="3" fill="none"
|
||
stroke="#BFBFBF"
|
||
stroke-linecap="round"
|
||
:stroke-dasharray="2 * Math.PI * 16"
|
||
:stroke-dashoffset="2 * Math.PI * 16 * (1 - m.score / 100)"
|
||
transform="rotate(-90 20 20)"
|
||
/>
|
||
</svg>
|
||
<span class="job-detail__mini-score">{{ m.score }}%</span>
|
||
</div>
|
||
<span class="job-detail__match-item-label">{{ m.label }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 优化简历提示 -->
|
||
<div class="job-detail__optimize-bar">
|
||
<span>优化简历大幅提升面试成功率!</span>
|
||
<button class="job-detail__optimize-btn" @click="handleGenerateResume">生成岗位专属简历</button>
|
||
</div>
|
||
|
||
<!-- 岗位描述 -->
|
||
<div class="job-detail__card">
|
||
<p class="job-detail__desc-text">{{ job.companyInfo.summary }}</p>
|
||
<div class="job-detail__tag-list">
|
||
<span v-for="tag in job.companyInfo.tags" :key="tag" class="job-detail__tag">{{ tag }}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 岗位职责 -->
|
||
<div class="job-detail__card">
|
||
<h3 class="job-detail__section-title">岗位职责</h3>
|
||
<ol class="job-detail__list">
|
||
<li v-for="(item, i) in job.responsibilities" :key="i">{{ item }}</li>
|
||
</ol>
|
||
</div>
|
||
|
||
<!-- 任职要求 -->
|
||
<div class="job-detail__card">
|
||
<div class="job-detail__section-header">
|
||
<h3 class="job-detail__section-title">任职要求</h3>
|
||
<span class="job-detail__skill-hint">
|
||
|
||
<!-- 深色为你拥有的技能-->
|
||
</span>
|
||
</div>
|
||
<div class="job-detail__skill-tags">
|
||
|
||
<!-- 先不加点击选中 @click="skill.matched = !skill.matched"-->
|
||
<span
|
||
v-for="skill in job.requiredSkills"
|
||
:key="skill.name"
|
||
class="job-detail__skill-tag cursor-po"
|
||
:class="{ 'job-detail__skill-tag--matched': skill.matched }"
|
||
|
||
>
|
||
{{ skill.name }}
|
||
<svg v-if="skill.matched" viewBox="0 0 12 12" fill="none" class="job-detail__skill-close">
|
||
<path d="M9 3L3 9M3 3l6 6" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/>
|
||
</svg>
|
||
</span>
|
||
</div>
|
||
<ol class="job-detail__list">
|
||
<li v-for="(item, i) in job.requirements" :key="i">{{ item }}</li>
|
||
</ol>
|
||
</div>
|
||
|
||
<!-- 加分项 -->
|
||
<div class="job-detail__card">
|
||
<h3 class="job-detail__section-title">加分项</h3>
|
||
<p class="job-detail__desc-text">{{ job.bonus }}</p>
|
||
</div>
|
||
|
||
<!-- 公司概况 -->
|
||
<div ref="companySectionRef" class="job-detail__card">
|
||
<h3 class="job-detail__section-title">公司概况</h3>
|
||
<div class="job-detail__company-info">
|
||
<div class="job-detail__company-info-left">
|
||
<div class="job-detail__company-header">
|
||
<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 24 24" fill="none" class="job-detail__company-svg">
|
||
<rect x="3" y="7" width="18" height="14" rx="2" stroke="currentColor" stroke-width="1.5"/>
|
||
<path d="M7 7V5a2 2 0 012-2h6a2 2 0 012 2v2" stroke="currentColor" stroke-width="1.5"/>
|
||
</svg>
|
||
</div>
|
||
<span class="job-detail__company-info-name">{{ job.company }}</span>
|
||
</div>
|
||
<p class="job-detail__company-desc">{{ job.companyInfo.description }}</p>
|
||
</div>
|
||
<div class="job-detail__company-info-right pr50">
|
||
<div class="job-detail__company-meta-item">
|
||
<span class="job-detail__meta-label">成立时间:</span>
|
||
<span>{{ job.companyInfo.founded }}</span>
|
||
</div>
|
||
<div class="job-detail__company-meta-item">
|
||
<span class="job-detail__meta-label">公司地址:</span>
|
||
<span>{{ job.companyInfo.address }}</span>
|
||
</div>
|
||
<div class="job-detail__company-meta-item">
|
||
<span class="job-detail__meta-label">企业规模:</span>
|
||
<span>{{ job.companyInfo.size }}</span>
|
||
</div>
|
||
<div class="job-detail__company-meta-item">
|
||
<span class="job-detail__meta-label">官网:</span>
|
||
<a :href="job.companyInfo.website" target="_blank" class="job-detail__company-link">{{ job.companyInfo.website }}</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 融资 -->
|
||
<div class="job-detail__card">
|
||
<h3 class="job-detail__section-title">融资</h3>
|
||
<p class="job-detail__desc-text">
|
||
<span class="job-detail__funding-label">当前融资阶段:</span>{{ job.companyInfo.fundingStage }}
|
||
<span class="job-detail__funding-label" style="margin-left: 0.3rem;">最新估值:</span>{{ job.companyInfo.valuation }}
|
||
</p>
|
||
</div>
|
||
|
||
<!-- 最新动态 -->
|
||
<div class="job-detail__card" v-if="job.companyInfo.news.length">
|
||
<h3 class="job-detail__section-title">最新动态</h3>
|
||
<div class="job-detail__news-list">
|
||
<div v-for="(news, i) in job.companyInfo.news" :key="i" class="job-detail__news-item">
|
||
<p class="job-detail__news-desc">{{ news }}</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<!-- 公司概况 Tab 已移除,点击直接滚动到岗位详情中的公司概况部分 -->
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
|
||
|
||
</div>
|
||
<AiChat :job-id="jobId" />
|
||
|
||
<!-- 职位不感兴趣反馈弹窗 -->
|
||
<JobDislikeDialog ref="dislikeDialogRef" v-model="showDislikeDialog" :job-id="jobId" />
|
||
|
||
<!-- 职位问题反馈弹窗 -->
|
||
<JobFeedbackDialog ref="feedbackDialogRef" v-model="showFeedbackDialog" :job-id="jobId" />
|
||
|
||
<!-- 岗位专属简历定制弹窗 -->
|
||
<JobResumeCustomDialog v-model="showResumeCustomDialog" :job-info="resumeCustomJobInfo" :job-id="jobId" @skip="handleSkipToApply" />
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, reactive, computed, nextTick, onMounted } from 'vue'
|
||
import { useRouter, useRoute } from 'vue-router'
|
||
import { useStore } from 'vuex'
|
||
import SideNav from '@/components/SideNav.vue'
|
||
import AiChat from '@/components/AiChat.vue'
|
||
import JobPageHeader from '@/components/JobPageHeader.vue'
|
||
import JobDislikeDialog from '@/components/JobDislikeDialog.vue'
|
||
import JobFeedbackDialog from '@/components/JobFeedbackDialog.vue'
|
||
import JobResumeCustomDialog from '@/components/JobResumeCustomDialog.vue'
|
||
import { fetchJobDetail, toggleJobFavorite, removeJobFavorite, fetchSkillGap } from '@/api/jobs'
|
||
import type { JobDetailData, SkillGapData } from '@/api/jobs'
|
||
|
||
// ==================== 路由相关 ====================
|
||
|
||
const router = useRouter()
|
||
const route = useRoute()
|
||
const store = useStore()
|
||
|
||
/** 当前岗位 ID,从路由参数中获取 */
|
||
const jobId = route.params.id as string
|
||
|
||
// ==================== 工具函数 ====================
|
||
|
||
/** 工作类型映射:数字 → 中文 */
|
||
function formatEmploymentType(type: number | undefined): string {
|
||
const map: Record<number, string> = { 0: '全职', 1: '兼职' }
|
||
return map[type ?? -1] ?? '未知'
|
||
}
|
||
|
||
/** 学历要求映射:数字 → 中文 */
|
||
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:job-岗位详情 / company-公司概况 */
|
||
const activeSection = ref<'job' | 'company'>('job')
|
||
|
||
/** 公司概况卡片 ref,用于滚动定位 */
|
||
const companySectionRef = ref<HTMLElement | null>(null)
|
||
|
||
/** 是否正在加载 */
|
||
const loading = ref(false)
|
||
|
||
/** 点击公司概况 tab 时,确保显示岗位详情内容并滚动到公司概况卡片 */
|
||
function scrollToCompany() {
|
||
activeSection.value = 'job'
|
||
nextTick(() => {
|
||
companySectionRef.value?.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
||
})
|
||
}
|
||
|
||
// ==================== 岗位详情数据 ====================
|
||
|
||
/** 匹配度维度列表(岗位匹配环下方的子维度) */
|
||
const matchItems = ref([
|
||
{ label: '教育背景', score: 0 },
|
||
{ label: '核心技能', score: 0 },
|
||
{ label: '过往经历', score: 0 },
|
||
])
|
||
|
||
/** 技能标签项(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.employmentType)
|
||
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()
|
||
})
|
||
|
||
// ==================== 事件处理 ====================
|
||
|
||
/** 不感兴趣弹窗状态 */
|
||
const showDislikeDialog = ref(false)
|
||
const dislikeDialogRef = ref<InstanceType<typeof JobDislikeDialog> | null>(null)
|
||
|
||
/** 问题反馈弹窗状态 */
|
||
const showFeedbackDialog = ref(false)
|
||
const feedbackDialogRef = ref<InstanceType<typeof JobFeedbackDialog> | null>(null)
|
||
|
||
/** 打开不感兴趣弹窗 */
|
||
function openDislikeDialog() {
|
||
dislikeDialogRef.value?.resetForm()
|
||
showDislikeDialog.value = true
|
||
}
|
||
|
||
/** 返回职位列表页 */
|
||
function goBack() {
|
||
// 使用 back() 回退,保证 Jobs 页能正确恢复缓存
|
||
// 如果没有历史记录(直接访问详情页),则 push 到列表页
|
||
if (window.history.length > 1) {
|
||
router.back()
|
||
} else {
|
||
router.push('/jobs')
|
||
}
|
||
}
|
||
|
||
/** 问题反馈 */
|
||
function handleFeedback() {
|
||
feedbackDialogRef.value?.resetForm()
|
||
showFeedbackDialog.value = true
|
||
}
|
||
|
||
/** 跳转到原链接 */
|
||
function handleReport() {
|
||
if (job.sourceUrl) {
|
||
window.open(job.sourceUrl, '_blank')
|
||
}
|
||
}
|
||
|
||
// ==================== 岗位专属简历定制弹窗 ====================
|
||
|
||
/** 简历定制弹窗显隐 */
|
||
const showResumeCustomDialog = 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 || [],
|
||
keywords: skillGapData.value?.job?.skillTags || job.requiredSkills.map(s => s.name),
|
||
sourceUrl: job.sourceUrl,
|
||
/** 默认简历信息(来自 skill-gap 接口) */
|
||
defaultResume: skillGapData.value?.resume || null,
|
||
}))
|
||
|
||
/** 生成岗位专属简历 — 调用 skill-gap 接口后打开定制弹窗 */
|
||
async function handleGenerateResume() {
|
||
const loadingInstance = ElLoading.service({
|
||
text: '正在分析岗位匹配度...',
|
||
background: 'rgba(0, 0, 0, 0.5)',
|
||
})
|
||
try {
|
||
const res = await fetchSkillGap(jobId)
|
||
if (res.code === 0 && res.data) {
|
||
skillGapData.value = res.data
|
||
}
|
||
} catch (e) {
|
||
console.error('技能差距分析失败', e)
|
||
ElMessage.error('分析失败,请稍后重试')
|
||
return
|
||
} finally {
|
||
loadingInstance.close()
|
||
}
|
||
showResumeCustomDialog.value = true
|
||
}
|
||
|
||
/** 不优化直接投递 */
|
||
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>
|