个人资料和岗位列表联调,简历优化部分页面
This commit is contained in:
+429
-4
@@ -1,20 +1,445 @@
|
||||
<template>
|
||||
<div class="home-page">
|
||||
<h1>首页</h1>
|
||||
<el-button type="primary" size="large" @click="router.push('/jobs')">浏览职位</el-button>
|
||||
<!-- SEO: 语义化 header -->
|
||||
<header class="home-nav">
|
||||
<div class="home-nav__inner">
|
||||
<div class="home-nav__logo">
|
||||
<span class="home-nav__logo-text">Offer派</span>
|
||||
</div>
|
||||
<button class="home-nav__btn" @click="router.push('/jobs')">免费领取 offer</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Hero Section -->
|
||||
<section class="home-hero">
|
||||
<div class="home-hero__inner">
|
||||
<div class="home-hero__left">
|
||||
<h1 class="home-hero__title">Offer派<br/>收offer就是快!</h1>
|
||||
<p class="home-hero__desc">智能匹配职位、自动填写申请、量身定制简历、推荐内部人脉——不到1分钟,统统搞定!</p>
|
||||
<button class="home-hero__cta" @click="router.push('/jobs')">免费体验</button>
|
||||
</div>
|
||||
<div class="home-hero__right">
|
||||
<div class="home-hero__card">
|
||||
<div class="home-hero__card-dots">
|
||||
<span class="dot dot--red"></span>
|
||||
<span class="dot dot--yellow"></span>
|
||||
<span class="dot dot--green"></span>
|
||||
</div>
|
||||
<div class="home-hero__card-body">
|
||||
<div class="home-hero__card-user">
|
||||
<div class="home-hero__card-avatar"></div>
|
||||
<div class="home-hero__card-info">
|
||||
<h4>求职者 Alex</h4>
|
||||
<div class="home-hero__card-tags">
|
||||
<span class="tag">React</span>
|
||||
<span class="tag">Tailwind</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="home-hero__card-match">98%</div>
|
||||
</div>
|
||||
<div class="home-hero__card-lines">
|
||||
<div class="line-full"></div>
|
||||
<div class="line-3q"></div>
|
||||
</div>
|
||||
<div class="home-hero__card-result">为您匹配了 5 个高星职位</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Stats Section -->
|
||||
<section class="home-stats">
|
||||
<div class="home-stats__inner">
|
||||
<div class="home-stats__header">
|
||||
<h2 class="fs48">让每一次求职努力<br/>都成为看得见的成果</h2>
|
||||
<div class="home-stats__divider"></div>
|
||||
</div>
|
||||
<div class="home-stats__cards">
|
||||
<article class="stat-card">
|
||||
<div class="stat-card__num">第一</div>
|
||||
<p class="stat-card__label">80%大学生求职首选</p>
|
||||
</article>
|
||||
<article class="stat-card">
|
||||
<div class="stat-card__num">3倍</div>
|
||||
<p class="stat-card__label">面试邀约率提升</p>
|
||||
</article>
|
||||
<article class="stat-card">
|
||||
<div class="stat-card__num">82%</div>
|
||||
<p class="stat-card__label">用户成功拿到offer</p>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Jobs Showcase Section -->
|
||||
<section class="home-jobs-showcase">
|
||||
<div class="home-jobs-showcase__inner">
|
||||
<h2>海量优质校招岗位,尽在Offer派!</h2>
|
||||
<p class="home-jobs-showcase__sub">实时汇集海量超10000+名校校招职位</p>
|
||||
<div class="home-jobs-showcase__box">
|
||||
<div class="home-jobs-showcase__stats">
|
||||
<div class="showcase-stat">
|
||||
<div class="showcase-stat__num"><span class="accent">40</span>万<span class="accent">+</span></div>
|
||||
<div class="showcase-stat__label">岗位总数</div>
|
||||
</div>
|
||||
<div class="showcase-stat">
|
||||
<div class="showcase-stat__num"><span class="accent">3120</span>今日新增</div>
|
||||
<div class="showcase-stat__label">今日新增</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="home-jobs-showcase__scroll">
|
||||
<div class="job-ticker" v-for="(job, i) in tickerJobs" :key="i">
|
||||
<span class="job-ticker__company">{{ job.company }}·{{ job.time }}</span>
|
||||
<span class="job-ticker__title">{{ job.title }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Feature 1: 个性化岗位匹配 -->
|
||||
<section class="home-feature">
|
||||
<div class="home-feature__inner">
|
||||
<div class="home-feature__text">
|
||||
<h2>个性化<br/>岗位匹配</h2>
|
||||
<p>第一时间发现真正适合你的岗位,精准匹配你的真实技能,杜绝虚假信息。</p>
|
||||
<button class="home-feature__btn" @click="router.push('/jobs')">
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="8" stroke="currentColor" stroke-width="2"/><circle cx="10" cy="10" r="4" stroke="currentColor" stroke-width="2"/><circle cx="10" cy="10" r="1.5" fill="currentColor"/></svg>
|
||||
<span>立即匹配</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="home-feature__visual home-feature__visual--match">
|
||||
<div class="feature-match-card">
|
||||
<div class="feature-match-card__header">
|
||||
<div class="feature-match-card__avatar"></div>
|
||||
<div class="feature-match-card__info">
|
||||
<h4>求职者 Alex</h4>
|
||||
<div class="feature-match-card__tags">
|
||||
<span>React</span><span>Tailwind</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="feature-match-card__score">98%</div>
|
||||
</div>
|
||||
<div class="feature-match-card__bars">
|
||||
<div class="bar bar--full"></div>
|
||||
<div class="bar bar--3q"></div>
|
||||
</div>
|
||||
<div class="feature-match-card__result">为您匹配了 5 个高星职位</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Feature 2: 一键自动网申 -->
|
||||
<section class="home-feature home-feature--reverse">
|
||||
<div class="home-feature__inner">
|
||||
<div class="home-feature__visual home-feature__visual--apply">
|
||||
<div class="feature-apply-card">
|
||||
<div class="feature-apply-card__header">
|
||||
<span class="feature-apply-card__status">正在执行投递程序...</span>
|
||||
<div class="feature-apply-card__dots">
|
||||
<span class="dot dot--red"></span>
|
||||
<span class="dot dot--yellow"></span>
|
||||
<span class="dot dot--green"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="feature-apply-card__list">
|
||||
<div class="apply-item">
|
||||
<div class="apply-item__icon">✉</div>
|
||||
<div class="apply-item__info">
|
||||
<p class="apply-item__title">腾讯科技 - 校招投递成功</p>
|
||||
<p class="apply-item__sub">耗时 0.8s · 自动识别填写完毕</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="apply-item apply-item--active">
|
||||
<div class="apply-item__icon apply-item__icon--pulse">📋</div>
|
||||
<div class="apply-item__info">
|
||||
<p class="apply-item__title">京东校招 - 自动填写中...</p>
|
||||
<p class="apply-item__sub">进度 65%</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="home-feature__text">
|
||||
<h2>一键<br/>自动网申</h2>
|
||||
<p>每日向数百岗位一键投递,覆盖各大企业网申系统。告别重复填写,节省 80% 的宝贵时间。</p>
|
||||
<button class="home-feature__btn" @click="router.push('/jobs')">
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M3 10l2-8h10l2 8-7 7-7-7z" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/></svg>
|
||||
<span>开启自动网申</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Feature 3: 岗位定制简历 -->
|
||||
<section class="home-feature">
|
||||
<div class="home-feature__inner">
|
||||
<div class="home-feature__text">
|
||||
<h2>岗位定制简历</h2>
|
||||
<p>6秒内生成针对特定岗位优化的专业简历,通过ATS系统,突出你的核心优势。</p>
|
||||
<button class="home-feature__btn" @click="router.push('/jobs')">
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M4 2h8l4 4v12H4V2z" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/><path d="M12 2v4h4" stroke="currentColor" stroke-width="2"/><path d="M4 11h8" stroke="currentColor" stroke-width="2"/></svg>
|
||||
<span>优化我的简历</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="home-feature__visual home-feature__visual--resume">
|
||||
<div class="feature-resume-card">
|
||||
<div class="feature-resume-card__doc">
|
||||
<div class="doc-header">
|
||||
<div class="doc-avatar"></div>
|
||||
<div class="doc-lines">
|
||||
<div class="doc-line doc-line--half"></div>
|
||||
<div class="doc-line doc-line--third"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="doc-bars">
|
||||
<div class="doc-bar doc-bar--1"></div>
|
||||
<div class="doc-bar doc-bar--2"></div>
|
||||
<div class="doc-bar doc-bar--3"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="feature-resume-card__badge">
|
||||
<svg width="15" height="15" viewBox="0 0 15 15" fill="none"><path d="M3 2h7l3 3v8H3V2z" stroke="#4FC2C9" stroke-width="1.2"/><path d="M6 7h3" stroke="#4FC2C9" stroke-width="1.2"/></svg>
|
||||
<span>ATS OPTIMIZED</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Feature 4: 内推人脉 -->
|
||||
<section class="home-feature home-feature--reverse">
|
||||
<div class="home-feature__inner">
|
||||
<div class="home-feature__visual home-feature__visual--referral">
|
||||
<div class="referral-grid">
|
||||
<div class="referral-card" v-for="(ref, i) in referralCards" :key="i" :class="{ 'referral-card--offset': i % 2 === 1 }">
|
||||
<div class="referral-card__icon" :style="{ background: ref.color }">{{ ref.letter }}</div>
|
||||
<p class="referral-card__company">{{ ref.company }}</p>
|
||||
<p class="referral-card__role">{{ ref.role }}</p>
|
||||
<span class="referral-card__code">内推码: {{ ref.code }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="home-feature__text">
|
||||
<h2>内推<br/>人脉直通</h2>
|
||||
<p>实时获取名企最新内推信息,自动填写网申内推码,简历更快到达HR。</p>
|
||||
<button class="home-feature__btn" @click="router.push('/jobs')">
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M3 3h10l4 4v10H3V3z" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/><path d="M6 12l2 2 4-4" stroke="currentColor" stroke-width="2"/></svg>
|
||||
<span>立即投递</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Feature 5: AI求职助手 -->
|
||||
<section class="home-feature">
|
||||
<div class="home-feature__inner">
|
||||
<div class="home-feature__text">
|
||||
<h2>24h全天候<br/>AI求职助手</h2>
|
||||
<p>随时提供求职指导,从岗位筛选到面试技巧,你的专属职业规划顾问。</p>
|
||||
<button class="home-feature__btn" @click="router.push('/jobs')">
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M2 3h12v10H6l-4 4V3z" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/></svg>
|
||||
<span>立即咨询</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="home-feature__visual home-feature__visual--ai">
|
||||
<div class="feature-ai-chat">
|
||||
<div class="ai-bubble ai-bubble--bot">👋 哈喽!我是你的 AI 职业导师。正在为你分析即将到来的面试流程。</div>
|
||||
<div class="ai-bubble ai-bubble--user">能帮我模拟一下明天的腾讯面试吗?</div>
|
||||
<div class="ai-bubble ai-bubble--bot">
|
||||
<p class="ai-bubble__title">准备就绪!面试官画像已生成:</p>
|
||||
<div class="ai-bubble__tags">
|
||||
<span>项目深度与工程化能力</span>
|
||||
<span>计算机网络与操作系统</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Testimonials Section -->
|
||||
<section class="home-testimonials">
|
||||
<div class="home-testimonials__header">
|
||||
<h2>万千毕业生的信赖之选</h2>
|
||||
<p>REAL VOICES FROM THE COMMUNITY</p>
|
||||
</div>
|
||||
<div class="home-testimonials__founder">
|
||||
<div class="home-testimonials__founder-img">👩💼</div>
|
||||
<blockquote>"很多大学生还在用传统方式找工作,海投、反复改简历,效率很低。Offer派利用AI技术让整个校招流程更丝滑,节省了80%的繁琐过程。"</blockquote>
|
||||
<cite><span class="cite-name">创始人</span> <span class="cite-role">Offer派</span></cite>
|
||||
</div>
|
||||
<div class="home-testimonials__scroll">
|
||||
<div class="testimonial-card" v-for="(t, i) in testimonials" :key="i">
|
||||
<svg class="testimonial-card__quote" width="48" height="48" viewBox="0 0 48 48" fill="none"><path d="M6 6h14v36" stroke="rgba(82,202,209,0.2)" stroke-width="2"/><path d="M28 6h14v36" stroke="rgba(82,202,209,0.2)" stroke-width="2"/></svg>
|
||||
<p class="testimonial-card__text">"{{ t.text }}"</p>
|
||||
<div class="testimonial-card__author">
|
||||
<div class="testimonial-card__avatar"></div>
|
||||
<div>
|
||||
<p class="testimonial-card__name">{{ t.name }}</p>
|
||||
<p class="testimonial-card__school">{{ t.school }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Job Search Section -->
|
||||
<section class="home-job-search">
|
||||
<div class="home-job-search__inner">
|
||||
<h3>一键找到最适合你的工作</h3>
|
||||
<div class="home-job-search__filters">
|
||||
<div class="filter-select">
|
||||
<span>行业</span>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none"><path d="M6 9l6 6 6-6" stroke="currentColor" stroke-width="2"/></svg>
|
||||
</div>
|
||||
<div class="filter-select">
|
||||
<span>地区</span>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none"><path d="M6 9l6 6 6-6" stroke="currentColor" stroke-width="2"/></svg>
|
||||
</div>
|
||||
<div class="filter-select">
|
||||
<span>岗位</span>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none"><path d="M6 9l6 6 6-6" stroke="currentColor" stroke-width="2"/></svg>
|
||||
</div>
|
||||
<button class="filter-btn" @click="router.push('/jobs')">搜索职位</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- FAQ Section -->
|
||||
<section class="home-faq">
|
||||
<div class="home-faq__inner">
|
||||
<div class="home-faq__header">
|
||||
<h2>常见问题</h2>
|
||||
<p>FREQUENTLY ASKED QUESTIONS</p>
|
||||
</div>
|
||||
<div class="home-faq__list">
|
||||
<div
|
||||
v-for="(faq, i) in faqs"
|
||||
:key="i"
|
||||
class="faq-item"
|
||||
:class="{ 'faq-item--open': faqOpen === i }"
|
||||
@click="faqOpen = faqOpen === i ? -1 : i"
|
||||
>
|
||||
<div class="faq-item__header">
|
||||
<span class="faq-item__question">{{ faq.q }}</span>
|
||||
<div class="faq-item__icon">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
|
||||
<line x1="5" y1="12" x2="19" y2="12" stroke="currentColor" stroke-width="2"/>
|
||||
<line v-if="faqOpen !== i" x1="12" y1="5" x2="12" y2="19" stroke="currentColor" stroke-width="2"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="faq-item__answer" v-show="faqOpen === i">
|
||||
<p>{{ faq.a }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- CTA Section -->
|
||||
<section class="home-cta">
|
||||
<div class="home-cta__inner">
|
||||
<h2>让Offer不再遥不可及<br/>大学生一站式AI求职</h2>
|
||||
<button class="home-cta__btn" @click="router.push('/jobs')">免费体验</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="home-footer">
|
||||
<div class="home-footer__inner">
|
||||
<div class="home-footer__col">
|
||||
<div class="home-footer__logo">
|
||||
<span class="home-footer__logo-text">Offer派</span>
|
||||
</div>
|
||||
<p class="home-footer__slogan">大学生AI求职平台</p>
|
||||
</div>
|
||||
<div class="home-footer__col">
|
||||
<h5>核心功能</h5>
|
||||
<ul>
|
||||
<li>智能岗位匹配</li>
|
||||
<li>AI简历优化</li>
|
||||
<li>AI求职助手</li>
|
||||
<li>一键投递</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="home-footer__col">
|
||||
<h5>求职资源</h5>
|
||||
<ul>
|
||||
<li>求职指南</li>
|
||||
<li>面试技巧</li>
|
||||
<li>简历模版</li>
|
||||
<li>行业分析</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="home-footer__col">
|
||||
<h5>其他信息</h5>
|
||||
<ul>
|
||||
<li>隐私协议</li>
|
||||
<li>服务条款</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="home-footer__bottom">
|
||||
<p>©2016-2026 - 广州油梨信息科技有限公司 版权所有</p>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted } from 'vue'
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useStore } from 'vuex'
|
||||
|
||||
/** 路由实例 */
|
||||
const router = useRouter()
|
||||
/** Vuex 状态管理实例 */
|
||||
const store = useStore()
|
||||
|
||||
/** 当前展开的 FAQ 索引,-1 表示全部收起 */
|
||||
const faqOpen = ref(-1)
|
||||
|
||||
/** 岗位滚动展示数据 — 模拟最新发布的校招岗位 */
|
||||
const tickerJobs = [
|
||||
{ company: '字节跳动', time: '15分钟前', title: '前端开发工程师' },
|
||||
{ company: '腾讯', time: '30分钟前', title: '后端开发工程师' },
|
||||
{ company: '阿里巴巴', time: '1小时前', title: '产品经理' },
|
||||
{ company: '华为', time: '2小时前', title: '算法工程师' },
|
||||
{ company: '京东', time: '3小时前', title: '运营管培生' },
|
||||
]
|
||||
|
||||
/** 内推人脉卡片数据 — letter: 公司首字母, color: 图标背景色 */
|
||||
const referralCards = [
|
||||
{ letter: 'b', company: '字节跳动', role: '前端工程师', code: 'r7u9xx', color: '#2563eb' },
|
||||
{ letter: 'a', company: '阿里巴巴', role: '运营管培生', code: 'alicc2', color: '#ea580c' },
|
||||
{ letter: 't', company: '腾讯', role: '后端开发工程师', code: 'tx9901', color: '#111827' },
|
||||
{ letter: 'h', company: '华为', role: '通用软件研究员', code: 'hwl92', color: '#dc2626' },
|
||||
]
|
||||
|
||||
/** 用户评价数据 — 展示真实用户的使用反馈 */
|
||||
const testimonials = [
|
||||
{ text: 'Offer派求职体验特别好!岗位信息丰富、质量高,AI岗位匹配也很精准,求职助手给的面试建议很专业,一周就拿到了Offer!', name: '李同学', school: '西安交通大学' },
|
||||
{ text: '简历优化功能太强了,针对不同岗位自动调整内容,面试邀约率直接翻倍。', name: '王同学', school: '浙江大学' },
|
||||
{ text: '一键投递省了我大量时间,再也不用一个个填网申表格了,效率提升太明显。', name: '张同学', school: '北京大学' },
|
||||
]
|
||||
|
||||
/** 常见问题列表 — q: 问题, a: 答案,点击可展开/收起 */
|
||||
const faqs = [
|
||||
{ q: '这个平台与其他求职网站有什么不同?', a: 'Offer派专注于大学生校招场景,利用AI技术实现智能岗位匹配、一键自动网申、岗位定制简历和内推人脉直通,让求职效率提升80%以上。' },
|
||||
{ q: '平台会分享我的个人信息吗?', a: '绝对不会。我们严格遵守隐私保护法规,您的个人信息仅用于岗位匹配和简历投递,不会分享给任何第三方。' },
|
||||
{ q: '如何收费?', a: '基础功能完全免费,包括岗位浏览、AI匹配和求职助手。高级功能如一键批量投递、定制简历等提供会员订阅服务。' },
|
||||
{ q: '平台的岗位来源是什么?', a: '我们的岗位信息来自各大企业官方校招渠道、合作高校就业中心以及企业HR直接发布,确保信息真实可靠。' },
|
||||
{ q: '支持哪些企业?', a: '目前已覆盖互联网、金融、制造、快消等行业的数千家企业,包括字节跳动、腾讯、阿里巴巴、华为等头部企业。' },
|
||||
]
|
||||
|
||||
onMounted(() => {
|
||||
// 进入首页时加载公共工具数据(行业分类等)
|
||||
// 加载公共工具数据(行业分类、岗位分类、地区分类等)
|
||||
store.dispatch('loadCommonData')
|
||||
// 触发预渲染事件 — 配合 vite.config.ts 中 PrerenderPlugin 的 renderAfterDocumentEvent 设置
|
||||
document.dispatchEvent(new Event('prerender-trigger'))
|
||||
})
|
||||
</script>
|
||||
|
||||
+270
-207
@@ -4,68 +4,69 @@
|
||||
<div class="job-detail__content">
|
||||
<!-- 页面标题 + Tab 切换 -->
|
||||
<JobPageHeader :activeTab="''" />
|
||||
|
||||
<!-- 顶部操作栏 -->
|
||||
<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="收藏">
|
||||
<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 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>
|
||||
|
||||
<!-- 岗位详情内容 -->
|
||||
<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">
|
||||
<!-- 内容区域(可滚动) -->
|
||||
<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"/>
|
||||
@@ -73,193 +74,197 @@
|
||||
</svg>
|
||||
{{ job.location }}
|
||||
</span>
|
||||
<span class="job-detail__meta-item">
|
||||
<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">
|
||||
<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">
|
||||
<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">
|
||||
<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)"
|
||||
</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>
|
||||
<span class="job-detail__mini-score">{{ m.score }}%</span>
|
||||
<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>
|
||||
<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__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.tags" :key="tag" class="job-detail__tag">{{ tag }}</span>
|
||||
</div>
|
||||
</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">
|
||||
<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">
|
||||
<svg viewBox="0 0 14 14" fill="none" class="job-detail__hint-icon">
|
||||
<circle cx="7" cy="7" r="6" stroke="currentColor" stroke-width="1"/>
|
||||
<path d="M7 6.5V10" stroke="currentColor" stroke-width="1" stroke-linecap="round"/>
|
||||
<circle cx="7" cy="4.5" r="0.5" fill="currentColor"/>
|
||||
</svg>
|
||||
查看您的技能与岗位要求的匹配情况
|
||||
</span>
|
||||
</div>
|
||||
<div class="job-detail__skill-tags">
|
||||
<!-- 任职要求 -->
|
||||
<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 }"
|
||||
@click="skill.matched = !skill.matched"
|
||||
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>
|
||||
<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 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 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>
|
||||
<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">
|
||||
<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 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>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 公司概况 Tab 已移除,点击直接滚动到岗位详情中的公司概况部分 -->
|
||||
<!-- 最新动态 -->
|
||||
<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 />
|
||||
|
||||
@@ -268,24 +273,30 @@
|
||||
|
||||
<!-- 职位问题反馈弹窗 -->
|
||||
<JobFeedbackDialog ref="feedbackDialogRef" v-model="showFeedbackDialog" :job-id="jobId" />
|
||||
|
||||
<!-- 岗位专属简历定制弹窗 -->
|
||||
<JobResumeCustomDialog v-model="showResumeCustomDialog" :job-info="resumeCustomJobInfo" @skip="handleSkipToApply" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, nextTick, onMounted } from 'vue'
|
||||
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 { fetchJobDetail } from '@/api/jobs'
|
||||
import JobResumeCustomDialog from '@/components/JobResumeCustomDialog.vue'
|
||||
import { fetchJobDetail, toggleJobFavorite, removeJobFavorite } from '@/api/jobs'
|
||||
import type { JobDetailData } from '@/api/jobs'
|
||||
|
||||
// ==================== 路由相关 ====================
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const store = useStore()
|
||||
|
||||
/** 当前岗位 ID,从路由参数中获取 */
|
||||
const jobId = route.params.id as string
|
||||
@@ -345,9 +356,9 @@ function scrollToCompany() {
|
||||
|
||||
/** 匹配度维度列表(岗位匹配环下方的子维度) */
|
||||
const matchItems = ref([
|
||||
{ label: '行业经验', score: 0 },
|
||||
{ label: '教育背景', score: 0 },
|
||||
{ label: '核心技能', score: 0 },
|
||||
{ label: '工作经历', score: 0 },
|
||||
{ label: '过往经历', score: 0 },
|
||||
])
|
||||
|
||||
/** 技能标签项(matched 表示用户是否具备该技能,可点击切换) */
|
||||
@@ -455,9 +466,9 @@ function fillJobData(data: JobDetailData) {
|
||||
// 匹配度详情
|
||||
if (data.matchDetail) {
|
||||
matchItems.value = [
|
||||
{ label: '行业经验', score: data.matchDetail.industryScore ?? 0 },
|
||||
{ label: '教育背景', score: data.matchDetail.educationScore ?? 0 },
|
||||
{ label: '核心技能', score: data.matchDetail.skillScore ?? 0 },
|
||||
{ label: '工作经历', score: data.matchDetail.experienceScore ?? 0 },
|
||||
{ label: '过往经历', score: data.matchDetail.experienceScore ?? 0 },
|
||||
]
|
||||
}
|
||||
|
||||
@@ -516,7 +527,13 @@ function openDislikeDialog() {
|
||||
|
||||
/** 返回职位列表页 */
|
||||
function goBack() {
|
||||
router.push('/jobs')
|
||||
// 使用 back() 回退,保证 Jobs 页能正确恢复缓存
|
||||
// 如果没有历史记录(直接访问详情页),则 push 到列表页
|
||||
if (window.history.length > 1) {
|
||||
router.back()
|
||||
} else {
|
||||
router.push('/jobs')
|
||||
}
|
||||
}
|
||||
|
||||
/** 问题反馈 */
|
||||
@@ -532,9 +549,55 @@ function handleReport() {
|
||||
}
|
||||
}
|
||||
|
||||
/** 生成岗位专属简历 */
|
||||
// ==================== 岗位专属简历定制弹窗 ====================
|
||||
|
||||
/** 简历定制弹窗显隐 */
|
||||
const showResumeCustomDialog = ref(false)
|
||||
|
||||
/** 传递给简历定制弹窗的岗位信息 */
|
||||
const resumeCustomJobInfo = computed(() => ({
|
||||
title: job.title,
|
||||
company: job.company,
|
||||
companyLogoUrl: job.companyLogoUrl,
|
||||
location: job.location,
|
||||
matchScore: +(job.matchScore / 10).toFixed(1),
|
||||
missingSkills: job.requiredSkills.filter(s => !s.matched).map(s => s.name),
|
||||
keywords: job.requiredSkills.map(s => s.name),
|
||||
sourceUrl: job.sourceUrl,
|
||||
}))
|
||||
|
||||
/** 生成岗位专属简历 — 打开定制弹窗 */
|
||||
function handleGenerateResume() {
|
||||
console.log('生成岗位专属简历')
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
/** 去投递 — 跳转到来源链接 */
|
||||
|
||||
+568
-129
@@ -3,9 +3,28 @@
|
||||
<SideNav />
|
||||
<div class="jobs-page__content">
|
||||
<!-- 页面标题 + Tab 切换 -->
|
||||
<JobPageHeader v-model:activeTab="activeTab" />
|
||||
<JobPageHeader v-model:activeTab="activeTab" :favoriteCount="favoriteTotal" :applyCount="applyTotal" />
|
||||
<!-- 收藏统计栏 -->
|
||||
<div v-if="activeTab === 'collected'" class="jobs-page__filters-bar">
|
||||
<div class="jobs-page__fav-stats">
|
||||
<span class="jobs-page__fav-stats-item">有效({{ favoriteValidCount }})</span>
|
||||
<span class="jobs-page__fav-stats-item">失效({{ favoriteInvalidCount }})</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 投递状态筛选栏 -->
|
||||
<div v-if="activeTab === 'applied'" class="jobs-page__filters-bar">
|
||||
<div class="jobs-page__fav-stats">
|
||||
<span
|
||||
v-for="tab in applyStatusTabs"
|
||||
:key="tab.status"
|
||||
class="jobs-page__fav-stats-item"
|
||||
:class="{ 'jobs-page__fav-stats-item--active': applyStatusFilter === tab.status }"
|
||||
@click="switchApplyStatus(tab.status)"
|
||||
>{{ tab.label }}({{ tab.count }})</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 筛选条件 -->
|
||||
<div class="jobs-page__filters-bar">
|
||||
<div v-if="isRecommendTab" class="jobs-page__filters-bar">
|
||||
<div class="jobs-page__filters">
|
||||
<div class="jobs-page__filter-group">
|
||||
<!-- 筛选条件按钮列表(行业单独用组件) -->
|
||||
@@ -18,20 +37,24 @@
|
||||
:maxSelect="3"
|
||||
@update:regionCodes="onRegionChange"
|
||||
/>
|
||||
<!-- 行业筛选:使用行业选择组件 -->
|
||||
<IndustrySelector
|
||||
v-else-if="filter.key === 'industry'"
|
||||
:industryIds="selectedIndustryIds"
|
||||
:maxSelect="3"
|
||||
@update:industryIds="onIndustryChange"
|
||||
/>
|
||||
<!-- 岗位筛选:使用岗位选择组件 -->
|
||||
<JobCategorySelector
|
||||
v-else-if="filter.key === 'position'"
|
||||
:categoryIds="selectedCategoryIds"
|
||||
:maxSelect="3"
|
||||
:level="3"
|
||||
:allowParentSelect="false"
|
||||
@update:categoryIds="onCategoryChange"
|
||||
/>
|
||||
<!-- 行业筛选:使用行业选择组件 -->
|
||||
<IndustrySelector
|
||||
v-else-if="filter.key === 'industry'"
|
||||
:industryIds="selectedIndustryIds"
|
||||
:maxSelect="3"
|
||||
:level="2"
|
||||
:allowParentSelect="false"
|
||||
@update:industryIds="onIndustryChange"
|
||||
/>
|
||||
<!-- 其他筛选条件 -->
|
||||
<div
|
||||
v-else
|
||||
@@ -62,21 +85,22 @@
|
||||
</template>
|
||||
</div>
|
||||
<div class="jobs-page__search-box">
|
||||
<svg class="jobs-page__search-svg" viewBox="0 0 16 16" fill="none">
|
||||
<svg class="jobs-page__search-svg" viewBox="0 0 16 16" fill="none" @click="reloadFirstPage">
|
||||
<circle cx="7" cy="7" r="5.5" stroke="currentColor" stroke-width="1.2"/>
|
||||
<path d="M11 11L14 14" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
<input
|
||||
v-model="searchText"
|
||||
v-model="keyword"
|
||||
class="jobs-page__search-input"
|
||||
placeholder="搜索职位、公司或关键词"
|
||||
@keyup.enter="reloadFirstPage"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 职位列表 -->
|
||||
<div ref="jobListRef" class="jobs-page__list pr5" @scroll="onListScroll">
|
||||
<div ref="jobListRef" class="jobs-page__list pr5" :style="restoring ? { visibility: 'hidden' } : {}" @scroll="onListScroll">
|
||||
<div
|
||||
v-for="(job, index) in jobList"
|
||||
:key="index"
|
||||
@@ -87,105 +111,124 @@
|
||||
<div class="jobs-page__job-main">
|
||||
<!-- 左侧:公司图标 + 职位信息 -->
|
||||
<div class="jobs-page__job-left">
|
||||
<div class="jobs-page__job-icon">
|
||||
<svg viewBox="0 0 24 24" fill="none" class="jobs-page__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"/>
|
||||
<rect x="10" y="11" width="4" height="4" rx="0.5" stroke="currentColor" stroke-width="1"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="jobs-page__job-info">
|
||||
<div class="jobs-page__job-title-row">
|
||||
<span class="jobs-page__job-name">{{ job.title }}</span>
|
||||
<button class="jobs-page__job-more" aria-label="更多操作" @click.stop="toggleMenu(index)">
|
||||
<svg viewBox="0 0 16 16" fill="currentColor" class="jobs-page__more-svg">
|
||||
<circle cx="3" cy="8" r="1.5"/><circle cx="8" cy="8" r="1.5"/><circle cx="13" cy="8" r="1.5"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="jobs-page__job-meta pt5">
|
||||
<span>{{ job.regionName }}</span>
|
||||
<span class="jobs-page__job-dot">·</span>
|
||||
<span>{{ job.companyShortName || job.companyName }}</span>
|
||||
<span class="jobs-page__job-dot">·</span>
|
||||
<span>{{ job.categoryName }}</span>
|
||||
</div>
|
||||
<!-- 提示信息 -->
|
||||
<div v-if="(job as any).tip" class="jobs-page__job-tip">
|
||||
<svg viewBox="0 0 14 14" fill="none" class="jobs-page__tip-svg">
|
||||
<circle cx="7" cy="7" r="6" stroke="currentColor" stroke-width="1"/>
|
||||
<path d="M7 6.5V10" stroke="currentColor" stroke-width="1" stroke-linecap="round"/>
|
||||
<circle cx="7" cy="4.5" r="0.5" fill="currentColor"/>
|
||||
<div class="dflex ">
|
||||
<div class="jobs-page__job-icon mr16">
|
||||
<svg viewBox="0 0 24 24" fill="none" class="jobs-page__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"/>
|
||||
<rect x="10" y="11" width="4" height="4" rx="0.5" stroke="currentColor" stroke-width="1"/>
|
||||
</svg>
|
||||
{{ (job as any).tip }}
|
||||
</div>
|
||||
<div class="jobs-page__job-info">
|
||||
<div class="jobs-page__job-title-row">
|
||||
<span class="jobs-page__job-name">{{ job.title }}</span>
|
||||
<button v-if="isRecommendTab" class="jobs-page__job-more" aria-label="更多操作" @click.stop="toggleMenu(index)">
|
||||
<svg viewBox="0 0 16 16" fill="currentColor" class="jobs-page__more-svg">
|
||||
<circle cx="3" cy="8" r="1.5"/><circle cx="8" cy="8" r="1.5"/><circle cx="13" cy="8" r="1.5"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button v-if="activeTab === 'collected'" class="jobs-page__job-remove" aria-label="取消收藏" @click.stop="removeFavoriteFromList(job, index)">
|
||||
<svg viewBox="0 0 16 16" fill="none" class="jobs-page__remove-svg">
|
||||
<path d="M4 4l8 8M12 4l-8 8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="jobs-page__job-meta pt5">
|
||||
<span>{{ job.regionName }}</span>
|
||||
<span class="jobs-page__job-dot">·</span>
|
||||
<span>{{ job.companyShortName || job.companyName }}</span>
|
||||
<span class="jobs-page__job-dot">·</span>
|
||||
<span>{{ job.categoryName }}</span>
|
||||
</div>
|
||||
<!-- 提示信息 -->
|
||||
<div v-if="(job as any).tip" class="jobs-page__job-tip">
|
||||
<svg viewBox="0 0 14 14" fill="none" class="jobs-page__tip-svg">
|
||||
<circle cx="7" cy="7" r="6" stroke="currentColor" stroke-width="1"/>
|
||||
<path d="M7 6.5V10" stroke="currentColor" stroke-width="1" stroke-linecap="round"/>
|
||||
<circle cx="7" cy="4.5" r="0.5" fill="currentColor"/>
|
||||
</svg>
|
||||
{{ (job as any).tip }}
|
||||
</div>
|
||||
|
||||
<!-- 底部操作栏 -->
|
||||
<div class="jobs-page__job-actions pt20">
|
||||
<div class="dflex fgrow2 aliite-c flex-warp" >
|
||||
<!-- 标签 -->
|
||||
<div class="jobs-page__job-tags mt10">
|
||||
<span v-for="(tag, ti) in job.tags" :key="ti" class="jobs-page__job-tag">{{ tag }}</span>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<!-- 底部操作栏 -->
|
||||
<div class="jobs-page__job-actions pt20">
|
||||
<div class="dflex fgrow2 aliite-c flex-warp" >
|
||||
<!-- 标签 -->
|
||||
<div class="jobs-page__job-tags mt10">
|
||||
<span v-for="(tag, ti) in job.tags" :key="ti" class="jobs-page__job-tag">{{ tag }}</span>
|
||||
</div>
|
||||
<div class="dflex-end mt10">
|
||||
<div v-if="isRecommendTab" class="jobs-page__job-action-left">
|
||||
<button class="jobs-page__action-icon-btn" aria-label="不感兴趣" @click.stop="openDislikeDialog(job)">
|
||||
<svg viewBox="0 0 16 16" fill="none" class="jobs-page__action-svg">
|
||||
<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="{ 'jobs-page__action-icon-btn--liked': job.isFavorite }" aria-label="收藏" @click.stop="toggleFavorite(job)">
|
||||
<svg viewBox="0 0 16 16" :fill="job.isFavorite ? 'currentColor' : 'none'" class="jobs-page__action-svg">
|
||||
<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>
|
||||
</div>
|
||||
<div class="dflex-end mt10">
|
||||
<div class="jobs-page__job-action-left">
|
||||
<button class="jobs-page__action-icon-btn" aria-label="不感兴趣" @click.stop="openDislikeDialog(job)">
|
||||
<svg viewBox="0 0 16 16" fill="none" class="jobs-page__action-svg">
|
||||
<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="{ 'jobs-page__action-icon-btn--liked': job.isFavorite }" aria-label="收藏">
|
||||
<svg viewBox="0 0 16 16" :fill="job.isFavorite ? 'currentColor' : 'none'" class="jobs-page__action-svg">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<div class="jobs-page__job-action-right ">
|
||||
<button class="jobs-page__job-helper">
|
||||
<svg viewBox="0 0 14 14" fill="none" class="jobs-page__helper-svg">
|
||||
<circle cx="7" cy="7" r="6" stroke="currentColor" stroke-width="1"/>
|
||||
<path d="M5.5 5.5a1.5 1.5 0 113 0c0 .8-.7 1-1.5 1.5V9" stroke="currentColor" stroke-width="1" stroke-linecap="round"/>
|
||||
<circle cx="7" cy="11" r="0.5" fill="currentColor"/>
|
||||
</svg>
|
||||
问助手
|
||||
</button>
|
||||
<button class="jobs-page__job-apply-btn" :class="{ 'jobs-page__job-apply-btn--active': job.applied }">
|
||||
自动投递
|
||||
</button>
|
||||
</div>
|
||||
<div class="jobs-page__job-action-right ">
|
||||
<button class="jobs-page__job-helper ml5">
|
||||
<svg viewBox="0 0 14 14" fill="none" class="jobs-page__helper-svg">
|
||||
<circle cx="7" cy="7" r="6" stroke="currentColor" stroke-width="1"/>
|
||||
<path d="M5.5 5.5a1.5 1.5 0 113 0c0 .8-.7 1-1.5 1.5V9" stroke="currentColor" stroke-width="1" stroke-linecap="round"/>
|
||||
<circle cx="7" cy="11" r="0.5" fill="currentColor"/>
|
||||
</svg>
|
||||
问助手
|
||||
</button>
|
||||
<button @click="handleReport(job.sourceUrl)" class="jobs-page__job-apply-btn" :class="{ 'jobs-page__job-apply-btn--active': job.applied }">
|
||||
去投递
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 弹出菜单 -->
|
||||
<div v-if="job.showMenu" class="jobs-page__job-popup" @click.stop>
|
||||
<div
|
||||
</div>
|
||||
<!-- 弹出菜单 -->
|
||||
<div v-if="isRecommendTab && job.showMenu" class="jobs-page__job-popup" @click.stop>
|
||||
<div
|
||||
class="jobs-page__job-popup-item"
|
||||
v-for="action in popupActions"
|
||||
:key="action"
|
||||
@click="handlePopupAction(action, job)"
|
||||
>{{ action }}</div>
|
||||
</div>
|
||||
>{{ action }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
<!-- 右侧:匹配度 -->
|
||||
<div class="jobs-page__job-match" :class="matchClass(job.matchScore)">
|
||||
<div class="jobs-page__match-ring">
|
||||
<svg viewBox="0 0 80 80" class="jobs-page__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="job.matchScore >= 80 ? '#4FC2C9' : '#BFBFBF'"
|
||||
stroke-linecap="round"
|
||||
:stroke-dasharray="2 * Math.PI * 34"
|
||||
:stroke-dashoffset="2 * Math.PI * 34 * (1 - job.matchScore / 100)"
|
||||
transform="rotate(-90 40 40)"
|
||||
/>
|
||||
<div class="jobs-page__job-match" :class="isAuthenticated ? matchClass(job.matchScore) : 'jobs-page__job-match--locked'">
|
||||
<template v-if="isAuthenticated">
|
||||
<div class="jobs-page__match-ring">
|
||||
<svg viewBox="0 0 80 80" class="jobs-page__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="job.matchScore >= 80 ? '#4FC2C9' : '#BFBFBF'"
|
||||
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="jobs-page__match-score">{{ job.matchScore }}%</div>
|
||||
</div>
|
||||
<div class="jobs-page__match-label">匹配值</div>
|
||||
<div class="jobs-page__match-level">{{ matchLevelText(job.matchScore) }}</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<svg class="jobs-page__lock-icon" viewBox="0 0 24 24" fill="none">
|
||||
<rect x="5" y="11" width="14" height="10" rx="2" stroke="currentColor" stroke-width="1.5"/>
|
||||
<path d="M8 11V7a4 4 0 118 0v4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
||||
</svg>
|
||||
<div class="jobs-page__match-score">{{ job.matchScore }}%</div>
|
||||
</div>
|
||||
<div class="jobs-page__match-label">匹配值</div>
|
||||
<div class="jobs-page__match-level">{{ matchLevelText(job.matchScore) }}</div>
|
||||
<div class="jobs-page__match-lock-text">登录查看匹配值</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -197,7 +240,7 @@
|
||||
<AiChat />
|
||||
|
||||
<!-- 职位不感兴趣反馈弹窗 -->
|
||||
<JobDislikeDialog ref="dislikeDialogRef" v-model="showDislikeDialog" :job-id="dislikeJobId" />
|
||||
<JobDislikeDialog ref="dislikeDialogRef" v-model="showDislikeDialog" :job-id="dislikeJobId" @disliked="removeDislikedJob" />
|
||||
|
||||
<!-- 职位问题反馈弹窗 -->
|
||||
<JobFeedbackDialog ref="feedbackDialogRef" v-model="showFeedbackDialog" :job-id="feedbackJobId" />
|
||||
@@ -216,8 +259,8 @@ import JobFeedbackDialog from '@/components/JobFeedbackDialog.vue'
|
||||
import IndustrySelector from '@/components/tools/IndustrySelector.vue'
|
||||
import JobCategorySelector from '@/components/tools/JobCategorySelector.vue'
|
||||
import RegionSelector from '@/components/tools/RegionSelector.vue'
|
||||
import { fetchJobList } from '@/api/jobs'
|
||||
import type { JobListItem, JobListParams } from '@/api/jobs'
|
||||
import { fetchJobList, fetchFavoriteList, toggleJobFavorite, removeJobFavorite, fetchFavoriteCount, fetchApplyList, fetchApplyCount, removeJobFromList } from '@/api/jobs'
|
||||
import type { JobListItem, JobListParams, FavoriteListParams, ApplyListParams, ApplyCountData } from '@/api/jobs'
|
||||
|
||||
// ==================== 路由相关 ====================
|
||||
|
||||
@@ -225,13 +268,19 @@ const router = useRouter()
|
||||
const route = useRoute()
|
||||
const store = useStore()
|
||||
|
||||
/** 登录状态 — 从 store 读取 */
|
||||
const isAuthenticated = computed(() => store.state.isAuthenticated)
|
||||
|
||||
// ==================== 页面状态 ====================
|
||||
|
||||
/** 当前激活的 Tab,从 URL query 参数读取,默认"推荐" */
|
||||
const activeTab = ref((route.query.tab as string) || 'recommend')
|
||||
|
||||
/** 是否为推荐列表 Tab */
|
||||
const isRecommendTab = computed(() => activeTab.value === 'recommend')
|
||||
|
||||
/** 搜索框输入内容 */
|
||||
const searchText = ref('')
|
||||
const keyword = ref('')
|
||||
|
||||
/** 当前选中的职位卡片索引 */
|
||||
const selectedIndex = ref(0)
|
||||
@@ -248,6 +297,78 @@ const showFeedbackDialog = ref(false)
|
||||
/** 当前操作的职位 ID(用于提交问题反馈) */
|
||||
const feedbackJobId = ref<string | null>(null)
|
||||
|
||||
// ==================== 收藏统计 ====================
|
||||
|
||||
/** 收藏总数(用于 Tab 标签显示) */
|
||||
const favoriteTotal = ref(0)
|
||||
|
||||
/** 有效收藏数 */
|
||||
const favoriteValidCount = ref(0)
|
||||
|
||||
/** 失效收藏数 */
|
||||
const favoriteInvalidCount = ref(0)
|
||||
|
||||
/** 加载收藏统计 */
|
||||
async function loadFavoriteCount() {
|
||||
try {
|
||||
const res = await fetchFavoriteCount()
|
||||
if (res.code === '0' && res.data) {
|
||||
favoriteTotal.value = res.data.totalCount
|
||||
favoriteValidCount.value = res.data.validCount
|
||||
favoriteInvalidCount.value = res.data.invalidCount
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('加载收藏统计失败', e)
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 投递统计 ====================
|
||||
|
||||
/** 投递总数(用于 Tab 标签显示) */
|
||||
const applyTotal = ref(0)
|
||||
|
||||
/** 投递统计详情 */
|
||||
const applyCountData = ref<ApplyCountData>({
|
||||
totalCount: 0,
|
||||
appliedCount: 0,
|
||||
interviewingCount: 0,
|
||||
offerCount: 0,
|
||||
rejectedCount: 0,
|
||||
closedCount: 0,
|
||||
})
|
||||
|
||||
/** 当前投递状态筛选(null=全部) */
|
||||
const applyStatusFilter = ref<number | null>(null)
|
||||
|
||||
/** 投递状态 Tab 列表 */
|
||||
const applyStatusTabs = computed(() => [
|
||||
{ status: 0, label: '已投递', count: applyCountData.value.appliedCount },
|
||||
{ status: 1, label: '面试中', count: applyCountData.value.interviewingCount },
|
||||
{ status: 2, label: '有Offer', count: applyCountData.value.offerCount },
|
||||
{ status: 3, label: '未通过', count: applyCountData.value.rejectedCount },
|
||||
{ status: 4, label: '已结束', count: applyCountData.value.closedCount },
|
||||
])
|
||||
|
||||
/** 切换投递状态筛选 */
|
||||
function switchApplyStatus(status: number) {
|
||||
applyStatusFilter.value = applyStatusFilter.value === status ? null : status
|
||||
pageNum.value = 1
|
||||
loadApplyList()
|
||||
}
|
||||
|
||||
/** 加载投递统计 */
|
||||
async function loadApplyCount() {
|
||||
try {
|
||||
const res = await fetchApplyCount()
|
||||
if (res.code === '0' && res.data) {
|
||||
applyCountData.value = res.data
|
||||
applyTotal.value = res.data.totalCount
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('加载投递统计失败', e)
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 常量数据 ====================
|
||||
|
||||
/** 筛选条件项类型 */
|
||||
@@ -274,31 +395,48 @@ const jobTypeOptions: { label: string; value: number }[] = [
|
||||
/** 工作类型下拉菜单是否显示 */
|
||||
const showJobTypeDropdown = ref(false)
|
||||
|
||||
/** 当前选中的工作类型(integer,对应接口参数 employmentType,null 表示未选) */
|
||||
const selectedEmploymentType = ref<number | null>(null)
|
||||
/** 当前选中的工作类型 — 直接读 store.jobIntention.employmentType */
|
||||
const selectedEmploymentType = computed<number | null>(
|
||||
() => store.state.jobIntention.employmentType ?? null,
|
||||
)
|
||||
|
||||
/** 选中的行业 id 数组(integer,对应接口参数 industryIds) */
|
||||
const selectedIndustryIds = ref<number[]>([])
|
||||
/** 选中的行业 id 数组 — 直接读 store.jobIntention.industryIds */
|
||||
const selectedIndustryIds = computed<number[]>(
|
||||
() => store.state.jobIntention.industryIds || [],
|
||||
)
|
||||
|
||||
/** 选中的岗位 id 数组(integer,对应接口参数 categoryIds) */
|
||||
const selectedCategoryIds = ref<number[]>([])
|
||||
/** 选中的岗位 id 数组 — 直接读 store.jobIntention.categoryIds */
|
||||
const selectedCategoryIds = computed<number[]>(
|
||||
() => store.state.jobIntention.categoryIds || [],
|
||||
)
|
||||
|
||||
/** 选中的地区编码数组(string,对应接口参数 regionCodes) */
|
||||
const selectedRegionCodes = ref<string[]>([])
|
||||
/** 选中的地区编码数组 — 直接读 store.jobIntention.regionCodes */
|
||||
const selectedRegionCodes = computed<string[]>(
|
||||
() => store.state.jobIntention.regionCodes || [],
|
||||
)
|
||||
|
||||
/** 行业选择变更回调 */
|
||||
function onIndustryChange(ids: number[]) {
|
||||
selectedIndustryIds.value = ids
|
||||
store.dispatch('saveJobIntention', {
|
||||
...store.state.jobIntention,
|
||||
industryIds: ids,
|
||||
})
|
||||
}
|
||||
|
||||
/** 岗位选择变更回调 */
|
||||
function onCategoryChange(ids: number[]) {
|
||||
selectedCategoryIds.value = ids
|
||||
store.dispatch('saveJobIntention', {
|
||||
...store.state.jobIntention,
|
||||
categoryIds: ids,
|
||||
})
|
||||
}
|
||||
|
||||
/** 地区选择变更回调 */
|
||||
function onRegionChange(codes: string[]) {
|
||||
selectedRegionCodes.value = codes
|
||||
store.dispatch('saveJobIntention', {
|
||||
...store.state.jobIntention,
|
||||
regionCodes: codes,
|
||||
})
|
||||
}
|
||||
|
||||
/** 点击筛选条件按钮 — 仅工作类型展开下拉 */
|
||||
@@ -311,10 +449,22 @@ function handleFilterClick(filter: FilterItem) {
|
||||
/** 选中工作类型选项 */
|
||||
function selectJobType(filter: FilterItem, option: { label: string; value: number }) {
|
||||
filter.selected = option.label
|
||||
selectedEmploymentType.value = option.value
|
||||
showJobTypeDropdown.value = false
|
||||
store.dispatch('saveJobIntention', {
|
||||
...store.state.jobIntention,
|
||||
employmentType: option.value,
|
||||
})
|
||||
}
|
||||
|
||||
/** 监听 store 中 employmentType 变化,同步工作类型筛选按钮的显示文字 */
|
||||
watch(selectedEmploymentType, (val) => {
|
||||
const jobTypeFilter = filters.value.find(f => f.key === 'jobType')
|
||||
if (jobTypeFilter && val !== null) {
|
||||
const matched = jobTypeOptions.find(o => o.value === val)
|
||||
if (matched) jobTypeFilter.selected = matched.label
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
/** 点击页面其他区域时关闭下拉菜单 */
|
||||
function closeDropdownOnClickOutside(e: MouseEvent) {
|
||||
const target = e.target as HTMLElement
|
||||
@@ -323,30 +473,66 @@ function closeDropdownOnClickOutside(e: MouseEvent) {
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
/** 跳转到原链接 */
|
||||
function handleReport( url:string ) {
|
||||
if (url) {
|
||||
window.open(url, '_blank')
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
document.addEventListener('click', closeDropdownOnClickOutside)
|
||||
|
||||
// 先判断是否有缓存(从详情页返回),在任何异步操作之前取出
|
||||
const cache = store.state.jobListCache
|
||||
const hasCache = !!(cache && cache.list.length > 0)
|
||||
|
||||
if (hasCache) {
|
||||
// 立即恢复缓存数据,隐藏列表避免闪烁
|
||||
restoring.value = true
|
||||
jobList.value = cache!.list
|
||||
pageNum.value = cache!.pageNum
|
||||
total.value = cache!.total
|
||||
savedScrollTop = cache!.scrollTop
|
||||
// 清除缓存,避免下次非详情页返回时误用
|
||||
store.commit('SET_JOB_LIST_CACHE', null)
|
||||
}
|
||||
|
||||
// 进入 Jobs 页时加载公共工具数据(行业分类等)
|
||||
store.dispatch('loadCommonData')
|
||||
|
||||
// 尝试从缓存恢复列表数据(从详情页返回时不重新请求)
|
||||
const cache = store.state.jobListCache
|
||||
if (cache && cache.list.length > 0) {
|
||||
jobList.value = cache.list
|
||||
pageNum.value = cache.pageNum
|
||||
total.value = cache.total
|
||||
savedScrollTop = cache.scrollTop
|
||||
// 清除缓存,避免下次非详情页返回时误用
|
||||
store.commit('SET_JOB_LIST_CACHE', null)
|
||||
// 等 DOM 渲染完成后恢复滚动位置
|
||||
// 仅登录状态下才加载需要鉴权的数据
|
||||
if (isAuthenticated.value) {
|
||||
// 加载收藏统计(用于 Tab 标签显示)
|
||||
loadFavoriteCount()
|
||||
|
||||
// 加载投递统计(用于 Tab 标签显示)
|
||||
loadApplyCount()
|
||||
|
||||
// 加载求职意向数据,computed 会自动反映到筛选条件
|
||||
await store.dispatch('loadJobIntention')
|
||||
}
|
||||
|
||||
if (hasCache) {
|
||||
// 从详情页返回,恢复滚动位置后再显示列表
|
||||
nextTick(() => {
|
||||
if (jobListRef.value) {
|
||||
jobListRef.value.scrollTop = savedScrollTop
|
||||
}
|
||||
restoring.value = false
|
||||
})
|
||||
} else {
|
||||
// 没有缓存,正常加载
|
||||
loadJobList()
|
||||
loadCurrentTab()
|
||||
}
|
||||
|
||||
// 初始化完成,允许 watcher 正常工作
|
||||
// 使用双重 nextTick 确保 loadJobIntention 引起的所有响应式更新都已 flush
|
||||
nextTick(() => {
|
||||
nextTick(() => {
|
||||
initializing = false
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
@@ -394,6 +580,12 @@ const jobListRef = ref<HTMLElement | null>(null)
|
||||
|
||||
/** 记录离开页面前的滚动位置 */
|
||||
let savedScrollTop = 0
|
||||
|
||||
/** 初始化标志 — onMounted 期间为 true,防止 watcher 误触发 reloadFirstPage */
|
||||
let initializing = true
|
||||
|
||||
/** 恢复缓存中 — 为 true 时列表容器 visibility:hidden,避免滚动位置闪烁 */
|
||||
const restoring = ref(false)
|
||||
// ==================== 加载岗位列表 ====================
|
||||
|
||||
/** 组装请求参数 */
|
||||
@@ -418,6 +610,10 @@ function buildParams(): JobListParams {
|
||||
if (selectedEmploymentType.value !== null) {
|
||||
params.employmentType = selectedEmploymentType.value
|
||||
}
|
||||
// 搜索关键词
|
||||
if (keyword.value.trim()) {
|
||||
params.keyword = keyword.value.trim()
|
||||
}
|
||||
return params
|
||||
}
|
||||
|
||||
@@ -474,7 +670,13 @@ function onListScroll() {
|
||||
// 距离底部小于 100px 时触发加载
|
||||
const threshold = 100
|
||||
if (el.scrollHeight - el.scrollTop - el.clientHeight < threshold) {
|
||||
loadNextPage()
|
||||
if (isRecommendTab.value) {
|
||||
loadNextPage()
|
||||
} else if (activeTab.value === 'collected') {
|
||||
loadFavoriteNextPage()
|
||||
} else if (activeTab.value === 'applied') {
|
||||
loadApplyNextPage()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -483,15 +685,173 @@ function reloadFirstPage() {
|
||||
pageNum.value = 1
|
||||
// 筛选条件变化时清除缓存
|
||||
store.commit('SET_JOB_LIST_CACHE', null)
|
||||
loadJobList()
|
||||
loadCurrentTab()
|
||||
}
|
||||
|
||||
/** 根据当前 Tab 加载对应列表 */
|
||||
function loadCurrentTab() {
|
||||
if (isRecommendTab.value) {
|
||||
loadJobList()
|
||||
} else if (activeTab.value === 'collected') {
|
||||
loadFavoriteList()
|
||||
} else if (activeTab.value === 'applied') {
|
||||
loadApplyList()
|
||||
}
|
||||
}
|
||||
|
||||
/** 加载收藏列表 */
|
||||
async function loadFavoriteList() {
|
||||
loading.value = true
|
||||
try {
|
||||
const params: FavoriteListParams = {
|
||||
pageNum: pageNum.value,
|
||||
pageSize: pageSize.value,
|
||||
valid: true,
|
||||
}
|
||||
const res = await fetchFavoriteList(params)
|
||||
if (res.code === '0' && res.data) {
|
||||
jobList.value = res.data.list.map((item) => ({
|
||||
...item,
|
||||
applied: false,
|
||||
showMenu: false,
|
||||
}))
|
||||
total.value = Number(res.data.total)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('加载收藏列表失败', e)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 加载收藏列表下一页 */
|
||||
async function loadFavoriteNextPage() {
|
||||
if (loadingMore.value || noMore.value) return
|
||||
loadingMore.value = true
|
||||
pageNum.value++
|
||||
try {
|
||||
const params: FavoriteListParams = {
|
||||
pageNum: pageNum.value,
|
||||
pageSize: pageSize.value,
|
||||
valid: true,
|
||||
}
|
||||
const res = await fetchFavoriteList(params)
|
||||
if (res.code === '0' && res.data) {
|
||||
const newItems = res.data.list.map((item) => ({
|
||||
...item,
|
||||
applied: false,
|
||||
showMenu: false,
|
||||
}))
|
||||
jobList.value.push(...newItems)
|
||||
total.value = Number(res.data.total)
|
||||
}
|
||||
} catch (e) {
|
||||
pageNum.value--
|
||||
console.error('加载收藏列表下一页失败', e)
|
||||
} finally {
|
||||
loadingMore.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 投递列表加载 ====================
|
||||
|
||||
/** 加载投递列表 */
|
||||
async function loadApplyList() {
|
||||
loading.value = true
|
||||
try {
|
||||
const params: ApplyListParams = {
|
||||
pageNum: pageNum.value,
|
||||
pageSize: pageSize.value,
|
||||
}
|
||||
if (applyStatusFilter.value !== null) {
|
||||
params.status = applyStatusFilter.value
|
||||
}
|
||||
const res = await fetchApplyList(params)
|
||||
if (res.code === '0' && res.data) {
|
||||
jobList.value = res.data.list.map((item) => ({
|
||||
...item,
|
||||
applied: true,
|
||||
showMenu: false,
|
||||
}))
|
||||
total.value = Number(res.data.total)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('加载投递列表失败', e)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 加载投递列表下一页 */
|
||||
async function loadApplyNextPage() {
|
||||
if (loadingMore.value || noMore.value) return
|
||||
loadingMore.value = true
|
||||
pageNum.value++
|
||||
try {
|
||||
const params: ApplyListParams = {
|
||||
pageNum: pageNum.value,
|
||||
pageSize: pageSize.value,
|
||||
}
|
||||
if (applyStatusFilter.value !== null) {
|
||||
params.status = applyStatusFilter.value
|
||||
}
|
||||
const res = await fetchApplyList(params)
|
||||
if (res.code === '0' && res.data) {
|
||||
const newItems = res.data.list.map((item) => ({
|
||||
...item,
|
||||
applied: true,
|
||||
showMenu: false,
|
||||
}))
|
||||
jobList.value.push(...newItems)
|
||||
total.value = Number(res.data.total)
|
||||
}
|
||||
} catch (e) {
|
||||
pageNum.value--
|
||||
console.error('加载投递列表下一页失败', e)
|
||||
} finally {
|
||||
loadingMore.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 监听筛选条件变化,自动重新加载
|
||||
watch(
|
||||
[selectedRegionCodes, selectedCategoryIds, selectedIndustryIds, selectedEmploymentType],
|
||||
() => reloadFirstPage(),
|
||||
() => {
|
||||
// 初始化阶段(loadJobIntention 导致的变化)跳过,避免清掉缓存
|
||||
if (initializing) return
|
||||
reloadFirstPage()
|
||||
},
|
||||
{ deep: true },
|
||||
)
|
||||
|
||||
// 监听 Tab 切换,重置列表状态并加载对应数据
|
||||
watch(activeTab, (newTab, oldTab) => {
|
||||
if (initializing) return
|
||||
|
||||
// 收藏和投递 Tab 需要登录
|
||||
if ((newTab === 'collected' || newTab === 'applied') && !isAuthenticated.value) {
|
||||
// 回退到之前的 Tab,弹登录框
|
||||
nextTick(() => { activeTab.value = oldTab })
|
||||
store.dispatch('openLogin')
|
||||
return
|
||||
}
|
||||
|
||||
jobList.value = []
|
||||
keyword.value = ''
|
||||
pageNum.value = 1
|
||||
total.value = 0
|
||||
loadCurrentTab()
|
||||
// 切换到收藏 Tab 时刷新统计
|
||||
if (newTab === 'collected') {
|
||||
loadFavoriteCount()
|
||||
}
|
||||
// 切换到投递 Tab 时刷新统计并重置筛选
|
||||
if (newTab === 'applied') {
|
||||
applyStatusFilter.value = null
|
||||
loadApplyCount()
|
||||
}
|
||||
})
|
||||
|
||||
// ==================== 事件处理 ====================
|
||||
|
||||
/** 切换职位卡片的弹出菜单(同时关闭其他已打开的菜单) */
|
||||
@@ -501,14 +861,46 @@ function toggleMenu(index: number) {
|
||||
})
|
||||
}
|
||||
|
||||
/** 处理弹出菜单操作项点击 — 问题反馈打开反馈弹窗,其他直接关闭菜单 */
|
||||
/** 处理弹出菜单操作项点击 */
|
||||
function handlePopupAction(action: string, job: JobItem) {
|
||||
job.showMenu = false
|
||||
if (action === '问题反馈') {
|
||||
if (action === '从列表中移除') {
|
||||
handleRemoveFromList(job)
|
||||
} else if (action === '复制链接') {
|
||||
handleCopyLink(job)
|
||||
} else if (action === '问题反馈') {
|
||||
openFeedbackDialog(job)
|
||||
}
|
||||
}
|
||||
|
||||
/** 复制职位原链接到剪贴板 */
|
||||
async function handleCopyLink(job: JobItem) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(job.sourceUrl)
|
||||
ElMessage.success('复制链接成功')
|
||||
} catch {
|
||||
ElMessage.error('复制链接失败')
|
||||
}
|
||||
}
|
||||
|
||||
/** 从推荐列表中移除职位(前端移除 + 预留接口调用) */
|
||||
async function handleRemoveFromList(job: JobItem) {
|
||||
try {
|
||||
// TODO: 接口对接后此处会真正调用后端,当前为模拟成功
|
||||
const res = await removeJobFromList(job.id)
|
||||
if (res.code === '0') {
|
||||
const index = jobList.value.findIndex(j => j.id === job.id)
|
||||
if (index !== -1) {
|
||||
jobList.value.splice(index, 1)
|
||||
total.value = Math.max(0, total.value - 1)
|
||||
}
|
||||
ElMessage.success('已从列表中移除')
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('移除职位失败', e)
|
||||
}
|
||||
}
|
||||
|
||||
/** 根据匹配分数返回对应的 CSS 类名 */
|
||||
function matchClass(score: number) {
|
||||
return score >= 80 ? 'jobs-page__job-match--high' : 'jobs-page__job-match--low'
|
||||
@@ -521,8 +913,12 @@ function matchLevelText(score: number) {
|
||||
return '匹配度偏低'
|
||||
}
|
||||
|
||||
/** 跳转到岗位详情页 — 先缓存当前列表和滚动位置 */
|
||||
/** 跳转到岗位详情页 — 未登录弹登录框,登录后再跳转 */
|
||||
function goToDetail(job: JobItem) {
|
||||
if (!isAuthenticated.value) {
|
||||
store.dispatch('openLogin', `/jobs/${job.id}`)
|
||||
return
|
||||
}
|
||||
// 保存当前列表状态到 store,返回时恢复
|
||||
store.commit('SET_JOB_LIST_CACHE', {
|
||||
list: jobList.value,
|
||||
@@ -537,6 +933,39 @@ function goToDetail(job: JobItem) {
|
||||
const dislikeDialogRef = ref<InstanceType<typeof JobDislikeDialog> | null>(null)
|
||||
const feedbackDialogRef = ref<InstanceType<typeof JobFeedbackDialog> | null>(null)
|
||||
|
||||
/** 收藏/取消收藏岗位 */
|
||||
async function toggleFavorite(job: JobItem) {
|
||||
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 ? '已取消收藏' : '收藏成功')
|
||||
// 刷新收藏统计
|
||||
loadFavoriteCount()
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('收藏操作失败', e)
|
||||
}
|
||||
}
|
||||
|
||||
/** 收藏列表中取消收藏并移除该项 */
|
||||
async function removeFavoriteFromList(job: JobItem, index: number) {
|
||||
try {
|
||||
const res = await removeJobFavorite(job.id)
|
||||
if (res.code === '0') {
|
||||
jobList.value.splice(index, 1)
|
||||
total.value = Math.max(0, total.value - 1)
|
||||
ElMessage.success('已取消收藏')
|
||||
loadFavoriteCount()
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('取消收藏失败', e)
|
||||
}
|
||||
}
|
||||
|
||||
/** 打开不感兴趣弹窗 — 记录当前职位 ID 并重置表单 */
|
||||
function openDislikeDialog(job: JobItem) {
|
||||
dislikeJobId.value = job.id
|
||||
@@ -544,6 +973,16 @@ function openDislikeDialog(job: JobItem) {
|
||||
showDislikeDialog.value = true
|
||||
}
|
||||
|
||||
/** 不感兴趣提交成功后,从列表中移除该职位 */
|
||||
function removeDislikedJob() {
|
||||
if (!dislikeJobId.value) return
|
||||
const index = jobList.value.findIndex(j => j.id === dislikeJobId.value)
|
||||
if (index !== -1) {
|
||||
jobList.value.splice(index, 1)
|
||||
total.value = Math.max(0, total.value - 1)
|
||||
}
|
||||
}
|
||||
|
||||
/** 打开问题反馈弹窗 — 记录当前职位 ID 并重置表单 */
|
||||
function openFeedbackDialog(job: JobItem) {
|
||||
feedbackJobId.value = job.id
|
||||
|
||||
@@ -96,6 +96,7 @@ async function loadProfile() {
|
||||
profile.value.wechat = d.wechatNumber || ''
|
||||
profile.value.skills = d.skills || []
|
||||
profile.value.certificates = d.certificates || []
|
||||
profile.value.portfolioUrl = d.portfolioUrl || ''
|
||||
}
|
||||
} catch {
|
||||
console.error('[Profile] 加载个人资料失败')
|
||||
@@ -239,6 +240,8 @@ const profile = ref({
|
||||
skills: [] as string[],
|
||||
/** 证书标签列表 — 接口字段 certificates */
|
||||
certificates: [] as string[],
|
||||
/** 作品集链接 — 接口字段 portfolioUrl */
|
||||
portfolioUrl: '',
|
||||
/** 教育经历 — 对应数据库 bg_user_profile_education */
|
||||
education: [] as Array<{
|
||||
school: string
|
||||
@@ -349,6 +352,10 @@ function handleEdit(section: string) {
|
||||
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],
|
||||
@@ -524,6 +531,28 @@ async function handleSaveEdit(data: Record<string, any>) {
|
||||
} 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,
|
||||
idCard: profile.value.idNumber,
|
||||
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 {
|
||||
|
||||
Reference in New Issue
Block a user