左侧菜单,邀请,设置,和会员相关

This commit is contained in:
2026-07-07 23:40:20 +08:00
parent b740e0cd39
commit 851310a901
18 changed files with 2090 additions and 2169 deletions
+53 -2
View File
@@ -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
View File
@@ -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,