Files
offerpai_web/src/views/AnnouncementDetail.vue
T

568 lines
26 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="announcement-detail dflex">
<SideNav />
<div class="announcement-detail__content">
<!-- 页面标题 -->
<div class="bg-f mb16 prl24 ptb16 dflex aliite-c">
<div class="dflex-start aliite-c">
<h2 class="announcement-detail__page-title">校招公告</h2>
<span class="announcement-detail__page-subtitle ml12">每日更新最新企业校招信息助力你的求职之路</span>
</div>
</div>
<!-- 返回按钮 -->
<div class="announcement-detail__back-row">
<button class="announcement-detail__back-btn" @click="goBack">
<svg viewBox="0 0 16 16" fill="none" class="announcement-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="announcement-detail__body scrollbar-thin">
<!-- 加载中骨架屏 -->
<el-skeleton :loading="loading" animated :count="1">
<template #template>
<div style="padding: 0.24rem 0.32rem;">
<el-skeleton-item variant="h1" style="width: 40%; height: 0.28rem; margin-bottom: 0.16rem;" />
<el-skeleton-item variant="text" style="width: 80%; height: 0.16rem; margin-bottom: 0.12rem;" />
<el-skeleton-item variant="text" style="width: 60%; height: 0.16rem; margin-bottom: 0.24rem;" />
<el-skeleton-item variant="rect" style="width: 100%; height: 2rem; margin-bottom: 0.24rem;" />
</div>
</template>
<template #default>
<!-- 顶部公司信息卡片独占一行 -->
<div class="announcement-detail__company-card">
<div class="announcement-detail__company-left">
<!-- 公司logo -->
<img
v-if="detail.logoUrl"
:src="detail.logoUrl"
class="announcement-detail__company-logo"
:alt="detail.companyName"
/>
<div v-else class="announcement-detail__company-logo announcement-detail__company-logo--default">
<svg width="40" height="40" viewBox="0 0 40 40" fill="none">
<rect width="40" height="40" rx="20" fill="#EDF9FA"/>
<path d="M10 27H30" stroke="#52CAD1" stroke-width="2" stroke-linecap="round"/>
<rect x="14" y="13" width="12" height="14" rx="1" stroke="#52CAD1" stroke-width="1.5"/>
</svg>
</div>
<!-- 公司名称+标签 -->
<div class="announcement-detail__company-info">
<div class="announcement-detail__company-name-row">
<span class="announcement-detail__company-name">{{ detail.companyName }}</span>
</div>
<div class="announcement-detail__company-tags">
<span v-if="detail.companyType" class="announcement-detail__tag announcement-detail__tag--type">{{ detail.companyType }}</span>
<span v-if="detail.categoryNames && detail.categoryNames.length" class="announcement-detail__tag announcement-detail__tag--gray">{{ detail.categoryNames.join('/') }}</span>
<span v-if="detail.companyScale" class="announcement-detail__tag announcement-detail__tag--gray">{{ detail.companyScale}}</span>
</div>
</div>
</div>
<!-- 右侧按钮 -->
<div class="announcement-detail__company-right">
<button class="announcement-detail__btn-outline" @click="handleViewAnnouncement">查看公告</button>
<button class="announcement-detail__btn-primary" @click="handleApplyUrl">投递地址</button>
</div>
</div>
<!-- 下方两列布局左侧详情 + 右侧边栏 -->
<div class="announcement-detail__layout">
<!-- 左侧主详情 -->
<div class="announcement-detail__left">
<!-- 岗位速览卡片 -->
<div class="announcement-detail__overview-card">
<!-- 速览标题行 -->
<div class="announcement-detail__overview-header">
<div class="announcement-detail__overview-title-row">
<span class="announcement-detail__overview-title">岗位速览</span>
<span class="announcement-detail__overview-date">{{ publishDateText }}发布</span>
</div>
<span class="announcement-detail__overview-deadline">{{ applyEndText }}截止</span>
</div>
<!-- 速览内容 -->
<div class="announcement-detail__overview-body">
<!-- 左侧字段区域 -->
<div class="announcement-detail__overview-fields">
<!-- 第一行招聘对象 + 工作地点 -->
<div class="announcement-detail__overview-row">
<div class="announcement-detail__overview-field flex1">
<span class="announcement-detail__overview-label">招聘对象</span>
<span class="announcement-detail__overview-value">{{ detail.targetAudience || '--' }}</span>
</div>
<div class="announcement-detail__overview-field flex2">
<span class="announcement-detail__overview-label">工作地点</span>
<span class="announcement-detail__overview-value">{{ detail.cityNames && detail.cityNames.length ? detail.cityNames.join('、') : '--' }}</span>
</div>
<!-- 右侧岗位方向 -->
<div class="announcement-detail__overview-direction flex2">
<div class="announcement-detail__overview-label">岗位方向</div>
<div class="announcement-detail__overview-value">{{ detail.recruitPosition || '--' }}</div>
</div>
</div>
<!-- 第二行学历要求 + 专业要求 -->
<div class="announcement-detail__overview-row">
<div class="announcement-detail__overview-field flex1">
<span class="announcement-detail__overview-label">学历要求</span>
<span class="announcement-detail__overview-value">{{ detail.educationNames && detail.educationNames.length ? detail.educationNames.join('、') : '--' }}</span>
</div>
<div class="announcement-detail__overview-field flex2">
<span class="announcement-detail__overview-label">专业要求</span>
<span class="announcement-detail__overview-value">{{ detail.majorRequire || '--' }}</span>
</div>
<div class="flex2"></div>
</div>
</div>
</div>
</div>
<!-- 公告标题 -->
<h2 class="announcement-detail__main-title">{{ detail.title }}</h2>
<!-- 详情正文各段落 -->
<div v-if="detail.companyIntro" class="announcement-detail__section">
<h3 class="announcement-detail__section-title">企业介绍</h3>
<p class="announcement-detail__section-content">{{ detail.companyIntro }}</p>
</div>
<div v-if="detail.recruitPosition" class="announcement-detail__section">
<h3 class="announcement-detail__section-title">招聘岗位</h3>
<p class="announcement-detail__section-content">{{ detail.recruitPosition }}</p>
</div>
<div v-if="detail.cityNames && detail.cityNames.length" class="announcement-detail__section">
<h3 class="announcement-detail__section-title">招聘城市</h3>
<p class="announcement-detail__section-content">{{ detail.cityNames.join('、') }}</p>
</div>
<div v-if="detail.majorRequire" class="announcement-detail__section">
<h3 class="announcement-detail__section-title">专业要求</h3>
<p class="announcement-detail__section-content">{{ detail.majorRequire }}</p>
</div>
<div v-if="detail.targetAudience" class="announcement-detail__section">
<h3 class="announcement-detail__section-title">招聘对象</h3>
<p class="announcement-detail__section-content">{{ detail.targetAudience }}</p>
</div>
<div v-if="detail.remark" class="announcement-detail__section">
<h3 class="announcement-detail__section-title">备注</h3>
<p class="announcement-detail__section-content">{{ detail.remark }}</p>
</div>
<div v-if="detail.writtenExam" class="announcement-detail__section">
<h3 class="announcement-detail__section-title">笔试</h3>
<p class="announcement-detail__section-content">{{ detail.writtenExam }}</p>
</div>
<!-- <div v-if="detail.source" class="announcement-detail__section">
<h3 class="announcement-detail__section-title">信息来源</h3>
<p class="announcement-detail__section-content">{{ detail.source }}</p>
</div> -->
</div>
<!-- 右侧边栏 -->
<div class="announcement-detail__right">
<!-- 企业其他岗位 -->
<div class="announcement-detail__aside-card">
<div class="announcement-detail__aside-header">
<span class="announcement-detail__aside-title">企业其他岗位</span>
<span class="announcement-detail__aside-more" @click="refreshOtherJobs">
<svg width="19" height="19" viewBox="0 0 19 19" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_2812_6238)">
<path d="M9.50008 15.8332C7.73203 15.8332 6.23446 15.2196 5.00737 13.9925C3.78029 12.7655 3.16675 11.2679 3.16675 9.49984C3.16675 7.73178 3.78029 6.23421 5.00737 5.00713C6.23446 3.78005 7.73203 3.16651 9.50008 3.16651C10.4105 3.16651 11.2813 3.3544 12.1126 3.73017C12.9438 4.10595 13.6563 4.64376 14.2501 5.34359V3.95817C14.2501 3.73387 14.3261 3.54598 14.4781 3.39451C14.6301 3.24303 14.818 3.16703 15.0417 3.16651C15.2655 3.16598 15.4537 3.24198 15.6062 3.39451C15.7587 3.54703 15.8345 3.73492 15.8334 3.95817V7.91651C15.8334 8.14081 15.7574 8.32897 15.6054 8.48097C15.4534 8.63297 15.2655 8.7087 15.0417 8.70817H11.0834C10.8591 8.70817 10.6712 8.63217 10.5197 8.48017C10.3683 8.32817 10.2923 8.14028 10.2917 7.91651C10.2912 7.69273 10.3672 7.50484 10.5197 7.35284C10.6723 7.20084 10.8602 7.12484 11.0834 7.12484H13.6167C13.1945 6.38595 12.6174 5.8054 11.8854 5.38317C11.1533 4.96095 10.3582 4.74984 9.50008 4.74984C8.18064 4.74984 7.05911 5.21165 6.1355 6.13526C5.21189 7.05887 4.75008 8.1804 4.75008 9.49984C4.75008 10.8193 5.21189 11.9408 6.1355 12.8644C7.05911 13.788 8.18064 14.2498 9.50008 14.2498C10.3973 14.2498 11.2188 14.0224 11.9645 13.5674C12.7103 13.1125 13.2874 12.5021 13.6959 11.7363C13.8015 11.5516 13.95 11.4231 14.1416 11.3508C14.3332 11.2785 14.5277 11.275 14.7251 11.3405C14.9362 11.4064 15.0879 11.545 15.1803 11.7561C15.2727 11.9672 15.2661 12.1651 15.1605 12.3498C14.6195 13.4054 13.8477 14.2498 12.8449 14.8832C11.8421 15.5165 10.7272 15.8332 9.50008 15.8332Z" fill="currentColor"/>
</g>
<defs>
<clipPath id="clip0_2812_6238">
<rect width="19" height="19" fill="white"/>
</clipPath>
</defs>
</svg>
换一批
</span>
</div>
<div class="announcement-detail__aside-list">
<div
v-for="(job, idx) in otherJobs"
:key="'other-' + idx"
class="announcement-detail__job-item"
style="cursor: pointer;"
@click="router.push('/jobs/' + job.id)"
>
<div class="announcement-detail__job-item-main">
<span class="announcement-detail__job-item-title">{{ job.title }}</span>
<div class="announcement-detail__job-item-tags">
<span v-if="job.city" class="announcement-detail__chip">{{ job.city }}</span>
<span v-if="job.type" class="announcement-detail__chip">{{ job.type }}</span>
<span v-if="job.education" class="announcement-detail__chip">{{ job.education }}</span>
</div>
</div>
<div class="announcement-detail__job-item-score" :style="{ color: job.scoreColor }">
<span class="announcement-detail__job-item-score-num">{{ job.score }}%</span>
<span class="announcement-detail__job-item-score-label" :style="{ color: job.scoreColor }">{{ job.scoreLabel }}</span>
</div>
<div v-if="idx < otherJobs.length - 1" class="announcement-detail__job-divider"></div>
</div>
</div>
</div>
<!-- 校招推荐 -->
<div class="announcement-detail__aside-card">
<div class="announcement-detail__aside-header">
<span class="announcement-detail__aside-title">校招推荐</span>
<span class="announcement-detail__aside-more" @click="refreshRecommend">
<svg width="19" height="19" viewBox="0 0 19 19" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_2812_6238)">
<path d="M9.50008 15.8332C7.73203 15.8332 6.23446 15.2196 5.00737 13.9925C3.78029 12.7655 3.16675 11.2679 3.16675 9.49984C3.16675 7.73178 3.78029 6.23421 5.00737 5.00713C6.23446 3.78005 7.73203 3.16651 9.50008 3.16651C10.4105 3.16651 11.2813 3.3544 12.1126 3.73017C12.9438 4.10595 13.6563 4.64376 14.2501 5.34359V3.95817C14.2501 3.73387 14.3261 3.54598 14.4781 3.39451C14.6301 3.24303 14.818 3.16703 15.0417 3.16651C15.2655 3.16598 15.4537 3.24198 15.6062 3.39451C15.7587 3.54703 15.8345 3.73492 15.8334 3.95817V7.91651C15.8334 8.14081 15.7574 8.32897 15.6054 8.48097C15.4534 8.63297 15.2655 8.7087 15.0417 8.70817H11.0834C10.8591 8.70817 10.6712 8.63217 10.5197 8.48017C10.3683 8.32817 10.2923 8.14028 10.2917 7.91651C10.2912 7.69273 10.3672 7.50484 10.5197 7.35284C10.6723 7.20084 10.8602 7.12484 11.0834 7.12484H13.6167C13.1945 6.38595 12.6174 5.8054 11.8854 5.38317C11.1533 4.96095 10.3582 4.74984 9.50008 4.74984C8.18064 4.74984 7.05911 5.21165 6.1355 6.13526C5.21189 7.05887 4.75008 8.1804 4.75008 9.49984C4.75008 10.8193 5.21189 11.9408 6.1355 12.8644C7.05911 13.788 8.18064 14.2498 9.50008 14.2498C10.3973 14.2498 11.2188 14.0224 11.9645 13.5674C12.7103 13.1125 13.2874 12.5021 13.6959 11.7363C13.8015 11.5516 13.95 11.4231 14.1416 11.3508C14.3332 11.2785 14.5277 11.275 14.7251 11.3405C14.9362 11.4064 15.0879 11.545 15.1803 11.7561C15.2727 11.9672 15.2661 12.1651 15.1605 12.3498C14.6195 13.4054 13.8477 14.2498 12.8449 14.8832C11.8421 15.5165 10.7272 15.8332 9.50008 15.8332Z" fill="currentColor"/>
</g>
<defs>
<clipPath id="clip0_2812_6238">
<rect width="19" height="19" fill="white"/>
</clipPath>
</defs>
</svg>
换一批
</span>
</div>
<div class="announcement-detail__aside-list">
<div
v-for="(rec, idx) in recommendList"
:key="'rec-' + idx"
class="announcement-detail__rec-item"
style="cursor: pointer;"
@click="router.push('/announcement/' + rec.id)"
>
<img
v-if="rec.logoUrl"
:src="rec.logoUrl"
class="announcement-detail__rec-logo"
:alt="rec.companyName"
/>
<div v-else class="announcement-detail__rec-logo announcement-detail__rec-logo--default">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none"><rect width="24" height="24" rx="12" fill="#EDF9FA"/><path d="M6 17H18" stroke="#52CAD1" stroke-width="1.5" stroke-linecap="round"/></svg>
</div>
<div class="announcement-detail__rec-info">
<span class="announcement-detail__rec-company">{{ rec.companyName }}</span>
<span class="announcement-detail__rec-positions">{{ rec.positions }}</span>
<div class="announcement-detail__rec-tags">
<span v-if="rec.industry" class="announcement-detail__chip">{{ rec.industry }}</span>
<span v-if="rec.cities" class="announcement-detail__chip">{{ rec.cities }}</span>
</div>
</div>
<div v-if="idx < recommendList.length - 1" class="announcement-detail__job-divider"></div>
</div>
</div>
</div>
</div>
</div>
</template>
</el-skeleton>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, computed, onMounted, watch } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import SideNav from '@/components/SideNav.vue'
import { fetchAnnouncementDetail, fetchAnnouncementRecommend } from '@/api/announcement'
import type { AnnouncementDetail, AnnouncementRecommendItem } from '@/api/announcement'
import { fetchJobList } from '@/api/jobs'
import type { JobListItem } from '@/api/jobs'
// ==================== 路由相关 ====================
const router = useRouter()
const route = useRoute()
/** 当前公告ID(大数字符串,避免精度丢失) */
let announcementId = route.params.id as string
// ==================== 页面状态 ====================
/** 是否正在加载 */
const loading = ref(false)
/** 公告详情数据 */
const detail = reactive<AnnouncementDetail>({
id: 0,
companyId: 0,
companyName: '',
logoUrl: '',
companyType: '',
title: '',
companyIntro: '',
companyScale: '',
targetAudience: '',
majorRequire: '',
recruitPosition: '',
remark: '',
writtenExam: '',
applyStartTime: null,
applyEndTime: null,
applyEndDesc: '',
inviteCode: '',
source: '',
announcementUrl: '',
applyUrl: '',
publishTime: null,
cleanStatus: 0,
status: 0,
createTime: null,
updateTime: null,
industryNames: [],
categoryNames: [],
cityNames: [],
educationNames: [],
recruitYears: [],
batchNames: [],
tagNames: [],
})
// ==================== 计算属性 ====================
/** 发布日期格式化文本 */
const publishDateText = computed(() => {
if (!detail.publishTime || !detail.publishTime.seconds) return ''
const d = new Date(detail.publishTime.seconds * 1000)
const y = d.getFullYear()
const m = String(d.getMonth() + 1).padStart(2, '0')
const day = String(d.getDate()).padStart(2, '0')
return `${y}-${m}-${day}`
})
/** 投递截止时间格式化文本 */
const applyEndText = computed(() => {
if (detail.applyEndDesc) return detail.applyEndDesc
if (!detail.applyEndTime || !detail.applyEndTime.seconds) return '招满即止'
const d = new Date(detail.applyEndTime.seconds * 1000)
const y = d.getFullYear()
const m = String(d.getMonth() + 1).padStart(2, '0')
const day = String(d.getDate()).padStart(2, '0')
return `${y}-${m}-${day}`
})
// ==================== 右侧企业其他岗位 ====================
/** 学历枚举映射 */
const educationMap: Record<number, string> = {
0: '不限',
1: '大专',
2: '本科',
3: '硕士',
4: '博士',
}
/** 招聘分类映射 */
const recruitCategoryMap: Record<number, string> = {
0: '社招',
1: '校招',
2: '实习',
3: '其他',
}
interface OtherJobItem {
id: string
title: string
city: string
type: string
education: string
score: number
scoreColor: string
scoreLabel: string
}
/** 企业全部岗位列表(缓存,避免重复请求) */
const companyAllJobs = ref<JobListItem[]>([])
/** 当前展示的企业其他岗位 */
const otherJobs = ref<OtherJobItem[]>([])
/**
* 根据匹配分数获取颜色和标签
*/
function getScoreStyle(score: number) {
if (score >= 80) return { color: '#9FD051', label: '高匹配度' }
if (score >= 40) return { color: '#F59E0B', label: '中匹配度' }
return { color: '#FE6D52', label: '低匹配度' }
}
/**
* 从全部岗位中随机选取最多4个并转换为展示格式
*/
function pickRandomJobs(list: JobListItem[]): OtherJobItem[] {
if (!list.length) return []
// 随机打乱后取前4个
const shuffled = [...list].sort(() => Math.random() - 0.5)
const picked = shuffled.slice(0, 4)
return picked.map(job => {
const style = getScoreStyle(job.matchScore ?? 0)
return {
id: job.id,
title: job.title,
city: job.regionName || '',
type: recruitCategoryMap[job.recruitCategory ?? 3] || '',
education: educationMap[job.education ?? 0] || '',
score: job.matchScore ?? 0,
scoreColor: style.color,
scoreLabel: style.label,
}
})
}
/**
* 加载企业其他岗位(用公司名作为关键词查询)
*/
async function loadCompanyJobs() {
if (!detail.companyName) return
try {
const res = await fetchJobList({
keyword: detail.companyName,
pageNum: 1,
pageSize: 30,
})
if (res.code === '0' && res.data?.list) {
companyAllJobs.value = res.data.list
otherJobs.value = pickRandomJobs(companyAllJobs.value)
}
} catch (e) {
console.error('加载企业其他岗位失败', e)
}
}
/** 换一批(企业其他岗位)—— 重新调接口随机展示4个 */
async function refreshOtherJobs() {
await loadCompanyJobs()
}
// ==================== 右侧校招推荐 ====================
interface RecommendItem {
id: string
logoUrl: string
companyName: string
positions: string
industry: string
cities: string
}
/** 推荐列表全量缓存(一次请求100条) */
const recommendAllList = ref<AnnouncementRecommendItem[]>([])
/** 当前展示的推荐列表(随机取4条) */
const recommendList = ref<RecommendItem[]>([])
/**
* 从全量推荐列表中随机选取最多4条,转换为展示格式
*/
function pickRandomRecommend(list: AnnouncementRecommendItem[]): RecommendItem[] {
if (!list.length) return []
const shuffled = [...list].sort(() => Math.random() - 0.5)
const picked = shuffled.slice(0, 4)
return picked.map(item => ({
id: String(item.id),
logoUrl: item.logoUrl || '',
companyName: item.companyName || '',
positions: item.categoryNames?.join('/') || '',
industry: '',
cities: item.cityNames?.join(', ') || '',
}))
}
/**
* 加载校招推荐列表(请求100条缓存,随机展示4条)
*/
async function loadRecommendList() {
if (!announcementId) return
try {
const res = await fetchAnnouncementRecommend({
announcementId: announcementId,
pageNum: 1,
pageSize: 100,
})
if (res.code === '0' && res.data?.list) {
recommendAllList.value = res.data.list
recommendList.value = pickRandomRecommend(recommendAllList.value)
}
} catch (e) {
console.error('加载校招推荐列表失败', e)
}
}
/** 换一批(校招推荐)—— 重新请求接口随机展示4条 */
async function refreshRecommend() {
await loadRecommendList()
}
// ==================== 事件处理 ====================
/** 返回列表页 */
function goBack() {
// 从首页跳转过来时,直接去列表页而非回首页
if (route.query.from === 'home') {
router.push('/announcement')
return
}
if (window.history.length > 1) {
router.back()
} else {
router.push('/announcement')
}
}
/** 查看公告 */
function handleViewAnnouncement() {
if (detail.source) {
window.open(detail.announcementUrl, '_blank')
}
}
/** 投递地址 */
function handleApplyUrl() {
if (detail.source) {
window.open(detail.applyUrl, '_blank')
}
}
// ==================== 加载详情数据 ====================
async function loadDetail() {
if (!announcementId) return
loading.value = true
try {
const res = await fetchAnnouncementDetail(announcementId)
if (res.code === '0' && res.data) {
Object.assign(detail, res.data)
// 详情加载完毕后,加载企业其他岗位和校招推荐
loadCompanyJobs()
loadRecommendList()
}
} catch (e) {
console.error('加载校招公告详情失败', e)
} finally {
loading.value = false
}
}
onMounted(() => {
loadDetail()
})
// 监听路由参数变化(同路由组件内跳转时刷新数据)
watch(
() => route.params.id,
(newId) => {
if (newId && newId !== announcementId) {
announcementId = newId as string
loadDetail()
}
}
)
</script>