左侧菜单,邀请,设置,和会员相关
This commit is contained in:
+53
-2
@@ -18,6 +18,39 @@
|
||||
|
||||
<!-- 内容区域(可滚动) -->
|
||||
<div class="job-detail__body scrollbar-thin">
|
||||
<!-- 加载中骨架屏 -->
|
||||
<el-skeleton :loading="loading" animated :count="1">
|
||||
<template #template>
|
||||
<div class="job-detail__skeleton">
|
||||
<!-- 模拟岗位标题行 -->
|
||||
<el-skeleton-item variant="h1" style="width: 40%; height: 0.28rem; margin-bottom: 0.16rem;" />
|
||||
<!-- 模拟标签行 -->
|
||||
<div style="display: flex; gap: 0.1rem; margin-bottom: 0.2rem;">
|
||||
<el-skeleton-item variant="text" style="width: 0.6rem; height: 0.2rem;" />
|
||||
<el-skeleton-item variant="text" style="width: 0.8rem; height: 0.2rem;" />
|
||||
<el-skeleton-item variant="text" style="width: 0.5rem; height: 0.2rem;" />
|
||||
</div>
|
||||
<!-- 模拟信息区 -->
|
||||
<el-skeleton-item variant="text" style="width: 80%; height: 0.16rem; margin-bottom: 0.12rem;" />
|
||||
<el-skeleton-item variant="text" style="width: 60%; height: 0.16rem; margin-bottom: 0.24rem;" />
|
||||
<!-- 模拟描述区 -->
|
||||
<el-skeleton-item variant="h3" style="width: 20%; height: 0.2rem; margin-bottom: 0.16rem;" />
|
||||
<el-skeleton-item variant="text" style="width: 100%; height: 0.14rem; margin-bottom: 0.1rem;" />
|
||||
<el-skeleton-item variant="text" style="width: 95%; height: 0.14rem; margin-bottom: 0.1rem;" />
|
||||
<el-skeleton-item variant="text" style="width: 90%; height: 0.14rem; margin-bottom: 0.1rem;" />
|
||||
<el-skeleton-item variant="text" style="width: 85%; height: 0.14rem; margin-bottom: 0.1rem;" />
|
||||
<el-skeleton-item variant="text" style="width: 70%; height: 0.14rem; margin-bottom: 0.24rem;" />
|
||||
<!-- 模拟公司信息区 -->
|
||||
<el-skeleton-item variant="h3" style="width: 25%; height: 0.2rem; margin-bottom: 0.16rem;" />
|
||||
<div style="display: flex; align-items: center; gap: 0.12rem; margin-bottom: 0.12rem;">
|
||||
<el-skeleton-item variant="circle" style="width: 0.4rem; height: 0.4rem;" />
|
||||
<el-skeleton-item variant="text" style="width: 1.2rem; height: 0.18rem;" />
|
||||
</div>
|
||||
<el-skeleton-item variant="text" style="width: 100%; height: 0.14rem; margin-bottom: 0.1rem;" />
|
||||
<el-skeleton-item variant="text" style="width: 80%; height: 0.14rem;" />
|
||||
</div>
|
||||
</template>
|
||||
<template #default>
|
||||
<!-- 岗位基本信息区 -->
|
||||
<div class="job-detail__header-section">
|
||||
<!-- 左侧:岗位信息 -->
|
||||
@@ -49,10 +82,10 @@
|
||||
</div>
|
||||
|
||||
<!-- 右侧:匹配度卡片 -->
|
||||
<div class="job-detail__match-card">
|
||||
<div class="job-detail__match-card" :class="matchCardClass">
|
||||
<div class="job-detail__match-top">
|
||||
<span class="job-detail__match-score">{{ job.matchScore }}%</span>
|
||||
<span class="job-detail__match-label">高匹配度</span>
|
||||
<span class="job-detail__match-label">{{ matchLevelText }}</span>
|
||||
</div>
|
||||
<div class="job-detail__match-list">
|
||||
<div class="job-detail__match-row" v-for="m in matchItems" :key="m.label">
|
||||
@@ -119,6 +152,8 @@
|
||||
<p class="job-detail__company-desc">{{ job.companyInfo.description }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-skeleton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -214,6 +249,22 @@ const matchItems = ref([
|
||||
{ label: '过往经历', score: 0 },
|
||||
])
|
||||
|
||||
/** 匹配度卡片动态class:根据分数区间切换高/中/低样式 */
|
||||
const matchCardClass = computed(() => {
|
||||
const score = job.matchScore
|
||||
if (score >= 80) return 'job-detail__match-card--high'
|
||||
if (score >= 40) return 'job-detail__match-card--medium'
|
||||
return 'job-detail__match-card--low'
|
||||
})
|
||||
|
||||
/** 匹配度等级文字 */
|
||||
const matchLevelText = computed(() => {
|
||||
const score = job.matchScore
|
||||
if (score >= 80) return '高匹配度'
|
||||
if (score >= 40) return '中匹配度'
|
||||
return '低匹配度'
|
||||
})
|
||||
|
||||
/** 技能标签项(matched 表示用户是否具备该技能,可点击切换) */
|
||||
interface SkillTag {
|
||||
/** 技能名称 */
|
||||
|
||||
+105
-19
@@ -18,7 +18,7 @@
|
||||
<JobPageHeader v-model:activeTab="activeTab" />
|
||||
|
||||
<!-- 筛选条件 -->
|
||||
<div v-if="isRecommendTab" class="jobs-page__filters-bar">
|
||||
<div v-if="isRecommendTab" class="jobs-page__filters-bar" :style="showAiRecommendTip?'':'margin-bottom:0.2rem'">
|
||||
<div class="jobs-page__filters">
|
||||
<div ref="filterGroupRef" class="jobs-page__filter-group">
|
||||
<!-- 筛选条件按钮列表(行业单独用组件) -->
|
||||
@@ -114,7 +114,7 @@
|
||||
<div class="jobs-page__ai-tip-content">
|
||||
<img class="jobs-page__ai-tip-logo" src="@/assets/images/logo-offerpai-2.png" alt="Nova" />
|
||||
<span class="jobs-page__ai-tip-text">
|
||||
Nova根据你的个人背景建议的行业:{{ aiIndustryNames }}<template v-if="aiCategoryNames">;岗位:{{ aiCategoryNames }}</template>
|
||||
Nova根据你的个人背景建议的<template v-if="aiCategoryNames">岗位:{{ aiCategoryNames }}</template><template v-if="aiIndustryNames">;行业:{{ aiIndustryNames }}</template>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -302,8 +302,8 @@ import RegionSelector from '@/components/tools/RegionSelector.vue'
|
||||
import { fetchJobList, fetchFavoriteList, toggleJobFavorite, removeJobFavorite, fetchFavoriteCount, fetchApplyList, fetchApplyCount, removeJobFromList } from '@/api/jobs'
|
||||
import type { JobListItem, JobListParams, FavoriteListParams, ApplyListParams, ApplyCountData } from '@/api/jobs'
|
||||
import { getTimeEvent, setTimeEvent, timestampToLocalDateTime } from '@/utils/time'
|
||||
import { resolveJobCategoryName } from '@/utils/jobCategory'
|
||||
import { resolveIndustryName } from '@/utils/industry'
|
||||
import { fetchIndustryTree, fetchJobCategoryTree } from '@/api/common'
|
||||
import type { IndustryItem, JobCategoryItem } from '@/api/common'
|
||||
|
||||
|
||||
// 2. 注意:这里的字符串不需要再手动转义双引号了
|
||||
@@ -516,27 +516,98 @@ const aiCategoryIds = computed<string[]>(
|
||||
() => store.state.jobIntention.aiCategoryIds || [],
|
||||
)
|
||||
|
||||
/** AI 推荐的行业名称(ID 转文字) */
|
||||
/** ====== AI 推荐提示条专用:完整分类树(含空分类) ====== */
|
||||
const fullIndustryTree = ref<IndustryItem[]>([])
|
||||
const fullJobCategoryTree = ref<JobCategoryItem[]>([])
|
||||
|
||||
/** 加载完整分类树数据(includeEmpty=true),仅用于 AI 推荐提示条 ID 转名称 */
|
||||
async function loadFullCategoryTrees() {
|
||||
const [indRes, catRes] = await Promise.all([
|
||||
fetchIndustryTree({ includeEmpty: true }),
|
||||
fetchJobCategoryTree({ includeEmpty: true }),
|
||||
])
|
||||
if (indRes && indRes.code === '0' && indRes.data) {
|
||||
fullIndustryTree.value = indRes.data
|
||||
}
|
||||
if (catRes && catRes.code === '0' && catRes.data) {
|
||||
fullJobCategoryTree.value = catRes.data
|
||||
}
|
||||
}
|
||||
|
||||
/** 从完整行业树中根据 ID 查找名称 */
|
||||
function resolveFullIndustryName(id: string): string {
|
||||
for (const parent of fullIndustryTree.value) {
|
||||
if (String(parent.id) === id) return parent.name
|
||||
for (const child of parent.children) {
|
||||
if (String(child.id) === id) return child.name
|
||||
}
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
/** 从完整岗位树中根据 ID 查找名称 */
|
||||
function resolveFullCategoryName(id: string): string {
|
||||
for (const lv1 of fullJobCategoryTree.value) {
|
||||
if (String(lv1.id) === id) return lv1.name
|
||||
for (const lv2 of lv1.children) {
|
||||
if (String(lv2.id) === id) return lv2.name
|
||||
if (lv2.children) {
|
||||
for (const lv3 of lv2.children) {
|
||||
if (String(lv3.id) === id) return lv3.name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
/** AI 推荐的行业名称(ID 转文字,基于完整行业树) */
|
||||
const aiIndustryNames = computed<string>(() => {
|
||||
if (!aiIndustryIds.value.length) return ''
|
||||
return aiIndustryIds.value
|
||||
.map(id => resolveIndustryName(Number(id)))
|
||||
.filter(name => !!name)
|
||||
.join('、')
|
||||
if (!aiIndustryIds.value.length || !fullIndustryTree.value.length) return ''
|
||||
const idSet = new Set(aiIndustryIds.value.map(String))
|
||||
const names: string[] = []
|
||||
for (const id of aiIndustryIds.value) {
|
||||
// 如果该 ID 是一级父节点且子节点也在列表中,跳过父级避免重复
|
||||
const parent = fullIndustryTree.value.find(p => String(p.id) === id)
|
||||
if (parent && parent.children?.some(c => idSet.has(String(c.id)))) {
|
||||
continue
|
||||
}
|
||||
const name = resolveFullIndustryName(id)
|
||||
if (name) names.push(name)
|
||||
}
|
||||
return names.join('、')
|
||||
})
|
||||
|
||||
/** AI 推荐的岗位名称(ID 转文字) */
|
||||
/** AI 推荐的岗位名称(ID 转文字,基于完整岗位树) */
|
||||
const aiCategoryNames = computed<string>(() => {
|
||||
if (!aiCategoryIds.value.length) return ''
|
||||
return aiCategoryIds.value
|
||||
.map(id => resolveJobCategoryName(Number(id)))
|
||||
.filter(name => !!name)
|
||||
.join('、')
|
||||
if (!aiCategoryIds.value.length || !fullJobCategoryTree.value.length) return ''
|
||||
const idSet = new Set(aiCategoryIds.value.map(String))
|
||||
const names: string[] = []
|
||||
for (const id of aiCategoryIds.value) {
|
||||
// 如果该 ID 是一级父节点且下级子节点也在列表中,跳过
|
||||
const lv1 = fullJobCategoryTree.value.find(p => String(p.id) === id)
|
||||
if (lv1 && lv1.children?.some(c => idSet.has(String(c.id)) || c.children?.some(leaf => idSet.has(String(leaf.id))))) {
|
||||
continue
|
||||
}
|
||||
// 如果该 ID 是二级节点且三级子节点也在列表中,跳过
|
||||
let isParentWithChild = false
|
||||
for (const cat of fullJobCategoryTree.value) {
|
||||
const lv2 = cat.children?.find(c => String(c.id) === id)
|
||||
if (lv2 && lv2.children?.some(leaf => idSet.has(String(leaf.id)))) {
|
||||
isParentWithChild = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if (isParentWithChild) continue
|
||||
const name = resolveFullCategoryName(id)
|
||||
if (name) names.push(name)
|
||||
}
|
||||
return names.join('、')
|
||||
})
|
||||
|
||||
/** 是否显示 AI 推荐提示条 */
|
||||
const showAiRecommendTip = computed(() => {
|
||||
return isRecommendTab.value && (aiIndustryNames.value || aiCategoryNames.value)
|
||||
return isRecommendTab.value && (aiIndustryNames.value !== null || aiCategoryNames.value !== null)
|
||||
})
|
||||
|
||||
/** 行业选择变更回调 */
|
||||
@@ -699,9 +770,12 @@ onMounted(async () => {
|
||||
// 窗口 resize 时更新尖角位置
|
||||
window.addEventListener('resize', updateAiTipArrowPosition)
|
||||
|
||||
// 加载完整分类树(含空分类),用于 AI 推荐提示条 ID 转名称
|
||||
loadFullCategoryTrees()
|
||||
|
||||
// 先判断是否有缓存(从详情页返回),在任何异步操作之前取出
|
||||
const cache = store.state.jobListCache
|
||||
const hasCache = !!(cache && cache.list.length > 0)
|
||||
const hasCache = !!(cache && cache.list.length > 0 && cache.tab === activeTab.value)
|
||||
|
||||
if (hasCache) {
|
||||
// 立即恢复缓存数据,隐藏列表避免闪烁
|
||||
@@ -710,7 +784,9 @@ onMounted(async () => {
|
||||
pageNum.value = cache!.pageNum
|
||||
total.value = cache!.total
|
||||
savedScrollTop = cache!.scrollTop
|
||||
// 清除缓存,避免下次非详情页返回时误用
|
||||
}
|
||||
// 无论是否命中缓存,都清除,避免下次非详情页返回时误用
|
||||
if (cache) {
|
||||
store.commit('SET_JOB_LIST_CACHE', null)
|
||||
}
|
||||
|
||||
@@ -1126,6 +1202,15 @@ watch(activeTab, (newTab, oldTab) => {
|
||||
return
|
||||
}
|
||||
|
||||
// 同步 tab 到 URL query,刷新浏览器时能恢复正确的 Tab
|
||||
const query = { ...route.query }
|
||||
if (newTab === 'recommend') {
|
||||
delete query.tab
|
||||
} else {
|
||||
query.tab = newTab
|
||||
}
|
||||
router.replace({ query })
|
||||
|
||||
jobList.value = []
|
||||
keyword.value = ''
|
||||
pageNum.value = 1
|
||||
@@ -1221,6 +1306,7 @@ function goToDetail(job: JobItem) {
|
||||
}
|
||||
// 保存当前列表状态到 store,返回时恢复
|
||||
store.commit('SET_JOB_LIST_CACHE', {
|
||||
tab: activeTab.value,
|
||||
list: jobList.value,
|
||||
pageNum: pageNum.value,
|
||||
total: total.value,
|
||||
|
||||
Reference in New Issue
Block a user