导师相关接口换offer派接口,职位列表和职位详情,Nova聊天侧边栏UI重写

This commit is contained in:
2026-07-03 19:55:53 +08:00
parent a1a27073e4
commit f90c8be2a9
26 changed files with 896 additions and 1129 deletions
+109 -291
View File
@@ -4,275 +4,128 @@
<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 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">
<!-- 岗位基本信息区 -->
<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">
<div class="job-detail__match-top">
<span class="job-detail__match-score">{{ job.matchScore }}%</span>
<span class="job-detail__match-label">高匹配度</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__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 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>
<!-- 岗位详情内容 -->
<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"-->
<!-- 任职要求 -->
<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 cursor-po"
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>
>
{{ 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 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>
<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 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>
<!-- 融资 -->
<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 已移除点击直接滚动到岗位详情中的公司概况部分 -->
<!-- 公司描述 -->
<p class="job-detail__company-desc">{{ job.companyInfo.description }}</p>
</div>
</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" />
<AiChat ref="aiChatRef" :job-id="jobId" />
<!-- 岗位专属简历定制弹窗 -->
<JobResumeCustomDialog v-model="showResumeCustomDialog" :job-info="resumeCustomJobInfo" :job-id="jobId" @skip="handleSkipToApply" />
@@ -287,15 +140,13 @@
</template>
<script setup lang="ts">
import { ref, reactive, computed, nextTick, onMounted } from 'vue'
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 JobDislikeDialog from '@/components/JobDislikeDialog.vue'
import JobFeedbackDialog from '@/components/JobFeedbackDialog.vue'
import JobResumeCustomDialog from '@/components/JobResumeCustomDialog.vue'
import MemberAccessDialog from '@/components/MemberAccessDialog.vue'
import FullscreenLoading from '@/components/FullscreenLoading.vue'
@@ -349,23 +200,11 @@ function splitNumberedText(text: string | undefined): string[] {
// ==================== 页面状态 ====================
/** 当前激活的内容区 Tabjob-岗位详情 / company-公司概况 */
const activeSection = ref<'job' | 'company'>('job')
/** 公司概况卡片 ref,用于滚动定位 */
const companySectionRef = ref<HTMLElement | null>(null)
/** 当前激活的内容区 Tab(保留兼容性但不再使用) */
/** 是否正在加载 */
const loading = ref(false)
/** 点击公司概况 tab 时,确保显示岗位详情内容并滚动到公司概况卡片 */
function scrollToCompany() {
activeSection.value = 'job'
nextTick(() => {
companySectionRef.value?.scrollIntoView({ behavior: 'smooth', block: 'start' })
})
}
// ==================== 岗位详情数据 ====================
/** 匹配度维度列表(岗位匹配环下方的子维度) */
@@ -525,24 +364,8 @@ onMounted(() => {
// ==================== 事件处理 ====================
/** 不感兴趣弹窗状态 */
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 {
@@ -550,17 +373,12 @@ function goBack() {
}
}
/** 问题反馈 */
function handleFeedback() {
feedbackDialogRef.value?.resetForm()
showFeedbackDialog.value = true
}
/** AiChat 组件引用 */
const aiChatRef = ref<InstanceType<typeof AiChat> | null>(null)
/** 跳转到原链接 */
function handleReport() {
if (job.sourceUrl) {
window.open(job.sourceUrl, '_blank')
}
/** 问 Nova — 触发 AI 聊天,推送提示消息 */
function handleAskNova() {
aiChatRef.value?.triggerAsk()
}
// ==================== 岗位专属简历定制弹窗 ====================