导师列表和导师详情

This commit is contained in:
2026-07-03 11:55:44 +08:00
parent 51be3e5585
commit a1a27073e4
22 changed files with 2007 additions and 346 deletions
+103 -118
View File
@@ -121,127 +121,109 @@
@click="goToDetail(job)"
>
<div class="jobs-page__job-main">
<!-- 公司图标(后面一定要换成正式的公司logo) + 职位信息 -->
<div class="jobs-page__job-left">
<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"/>
<!-- 职位信息 -->
<div class="jobs-page__col-left">
<!-- 职位名称 + 收藏红心 -->
<div class="jobs-page__job-title-row">
<span class="jobs-page__job-name">{{ job.title }}</span>
<!-- 收藏红心 -->
<button v-if="isRecommendTab" class="jobs-page__favorite-btn" :class="{ 'jobs-page__favorite-btn--liked': job.isFavorite }" aria-label="收藏" @click.stop="toggleFavorite(job)">
<svg viewBox="0 0 16 16" :fill="job.isFavorite ? 'currentColor' : 'none'" class="jobs-page__favorite-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>
</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"/>
</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-tags">
<span v-for="(tag, ti) in job.tags" :key="ti" class="jobs-page__job-tag">{{ tag }}</span>
</div>
<!-- 底部地区 + 学历 + 招聘分类 -->
<div class="jobs-page__job-meta-bottom">
<span class="jobs-page__meta-location">
<svg viewBox="0 0 18 18" fill="none" class="jobs-page__location-icon">
<path d="M9 1.5C5.96 1.5 3.5 3.96 3.5 7c0 4.13 5.5 9.5 5.5 9.5s5.5-5.37 5.5-9.5c0-3.04-2.46-5.5-5.5-5.5zm0 7.5a2 2 0 110-4 2 2 0 010 4z" fill="currentColor"/>
</svg>
{{ job.regionName }}
</span>
<span class="jobs-page__meta-edu">本科</span>
<span v-if="formatEmploymentType(job.recruitCategory)" class="jobs-page__meta-recruit">{{ formatEmploymentType(job.recruitCategory) }}</span>
</div>
</div>
<!-- 中栏公司信息居中 -->
<div class="jobs-page__col-center">
<div class="jobs-page__company-info">
<!-- 公司logo + 公司名 -->
<div class="jobs-page__company-row">
<div class="jobs-page__company-logo">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
<path d="M0 0h24v24H0z" fill="none" />
<path fill="currentColor" d="M18 15h-2v2h2m0-6h-2v2h2m2 6h-8v-2h2v-2h-2v-2h2v-2h-2V9h8M10 7H8V5h2m0 6H8V9h2m0 6H8v-2h2m0 6H8v-2h2M6 7H4V5h2m0 6H4V9h2m0 6H4v-2h2m0 6H4v-2h2m6-10V3H2v18h20V7z" />
</svg>
{{ (job as any).tip }}
</div>
<span class="jobs-page__company-name">{{ job.companyShortName || job.companyName }}</span>
</div>
<!-- 公司标签 -->
<div v-if="job.companyTags && job.companyTags.length" class="jobs-page__company-tags">
<span v-for="(ctag, ci) in job.companyTags" :key="ci" class="jobs-page__company-tag">{{ ctag }}</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="jobs-page__job-action-right ">
<button class="jobs-page__job-helper ml5" @click.stop="askAssistant(job)">
<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.stop="handleReport(job.sourceUrl)" class="jobs-page__job-apply-btn" :class="{ 'jobs-page__job-apply-btn--active': job.applied }">
去投递
</button>
</div>
</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.stop="handlePopupAction(action, job)"
>{{ action }}</div>
</div>
</div>
</div>
<!-- 右侧匹配度 -->
<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)"
/>
<div class="dflex-end flex1">
<!-- 右栏操作按钮 + 不再显示 -->
<div class="jobs-page__col-right mr10">
<!-- 按钮行 -->
<div class="jobs-page__job-action-right">
<button class="jobs-page__job-helper" @click.stop="askAssistant(job)">问Nova</button>
<button @click.stop="handleReport(job.sourceUrl)" class="jobs-page__job-apply-btn" :class="{ 'jobs-page__job-apply-btn--active': job.applied }">去投递</button>
</div>
<!-- 不再显示 -->
<span v-if="isRecommendTab" class="jobs-page__hide-btn" @click.stop="openDislikeDialog(job)">不再显示</span>
</div>
<!-- 最右侧匹配度 -->
<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="#9FD051" fill="none" opacity="0.3"/>
<circle cx="40" cy="40" r="34" stroke-width="5" fill="none"
:stroke="matchRingColor(job.matchScore)"
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-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>
</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-lock-text">登录查看匹配值</div>
</template>
<div class="jobs-page__match-lock-text">登录查看匹配值</div>
</template>
</div>
</div>
</div>
<!-- 弹出菜单 -->
<div v-if="isRecommendTab && job.showMenu" class="jobs-page__job-popup" @click.stop>
<div
class="jobs-page__job-popup-item"
@click.stop="handlePopupAction('从列表中移除', job)"
>不再显示</div>
</div>
</div>
<!-- 首次加载中提示 -->
@@ -285,7 +267,7 @@
import { ref, watch, onMounted, onBeforeUnmount, nextTick, computed } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { useStore } from 'vuex'
import { JOB_TYPE_OPTIONS } from '@/stores/index'
import { JOB_TYPE_OPTIONS, formatEmploymentType } from '@/stores/index'
import SideNav from '@/components/SideNav.vue'
import AiChat from '@/components/AiChat.vue'
import JobPageHeader from '@/components/JobPageHeader.vue'
@@ -695,9 +677,6 @@ onBeforeUnmount(() => {
document.removeEventListener('click', closeDropdownOnClickOutside)
})
/** 弹出菜单操作项 */
const popupActions = ['从列表中移除', '已投递', '复制链接']
// ==================== 职位列表项(扩展接口字段,增加前端交互状态) ====================
/** 职位列表项类型(在接口返回基础上扩展前端交互字段) */
@@ -1089,9 +1068,15 @@ function matchClass(score: number) {
return score >= 80 ? 'jobs-page__job-match--high' : 'jobs-page__job-match--low'
}
/** 根据匹配分数返回环形颜色 */
function matchRingColor(score: number) {
if (score >= 80) return '#9FD051'
return '#9FD051'
}
/** 根据匹配分数返回匹配等级文案 */
function matchLevelText(score: number) {
if (score >= 80) return '匹配'
if (score >= 80) return '匹配'
if (score >= 60) return '一般匹配'
return '匹配度偏低'
}
+391
View File
@@ -0,0 +1,391 @@
<template>
<div class="mentor-page dflex">
<SideNav />
<div class="mentor-page__body dflex">
<!-- 左侧导师列表区域 -->
<div class="mentor-page__main">
<!-- 顶部标题 + 搜索框 -->
<div class="mentor-page__header">
<div class="mentor-page__title-wrap">
<!-- <span class="mentor-page__title-icon"></span>-->
<div class=" ">
<div class="mentor-page__title dib mr10">导师推荐</div>
<div class="mentor-page__subtitle dib">自有合作导师真实在职身份全部实名认证</div>
</div>
</div>
<div class="mentor-page__search-wrap">
<svg class="mentor-page__search-icon" viewBox="0 0 16 16" fill="none">
<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="keyword"
class="mentor-page__search-input"
placeholder="搜索导师、公司、服务类型..."
@keyup.enter="handleSearch"
/>
<!-- 清空按钮有输入内容时鼠标 hover 搜索框才显示 -->
<span
v-if="keyword.length > 0"
class="mentor-page__search-clear"
@click="handleClearKeyword"
></span>
</div>
</div>
<!-- 行业分类 Tab -->
<div class="mentor-page__tabs">
<span
v-for="(tab, index) in industryTabs"
:key="index"
class="mentor-page__tab"
:class="{ 'mentor-page__tab--active': activeTab === index }"
@click="handleTabChange(index)"
>{{ tab }}</span>
</div>
<!-- 导师列表 -->
<div ref="listRef" class="mentor-page__list" :style="restoring ? { visibility: 'hidden' } : {}" @scroll="onListScroll">
<div
v-for="mentor in mentorList"
:key="mentor.id"
class="mentor-page__card"
>
<!-- 左侧头像 + 基本信息 -->
<div class="mentor-page__card-left">
<!-- 头像 -->
<div class="mentor-page__avatar-wrap">
<img
v-if="mentor.picture"
:src="toOssUrl(mentor.picture)"
class="mentor-page__avatar"
:alt="mentor.platformName"
/>
<div v-else class="mentor-page__avatar-placeholder">
{{ mentor.platformName?.charAt(0) || '师' }}
</div>
</div>
<!-- 导师基本信息 -->
<div class="mentor-page__card-info">
<div class="mentor-page__card-name">{{ mentor.platformName }}</div>
<div class="mentor-page__card-meta">
<span v-if="mentor.agencyName" class="mentor-page__card-agency">{{ mentor.agencyName }}</span>
<span v-if="mentor.agencyName && mentor.position" class="mentor-page__card-sep"></span>
<span v-if="mentor.position" class="mentor-page__card-position">{{ mentor.position }}</span>
</div>
<!-- 标签 -->
<div class="mentor-page__card-labels" v-if="mentor.labels && mentor.labels.length">
<span
v-for="(label, li) in mentor.labels.slice(0, 4)"
:key="li"
class="mentor-page__card-label"
>{{ label }}</span>
</div>
</div>
</div>
<!-- 右侧话题 + 查看详情 -->
<div class="mentor-page__card-right">
<!-- 话题列表 -->
<div class="mentor-page__card-topics">
<div
v-for="(title, ti) in mentor.titles.slice(0, 3)"
:key="ti"
class="mentor-page__card-topic"
>{{ title }}</div>
</div>
<!-- 查看详情按钮 -->
<button
class="mentor-page__detail-btn"
@click="goToDetail(mentor)"
>查看详情 </button>
</div>
</div>
<!-- 加载中提示 -->
<div v-if="loading" class="mentor-page__loading">
<div class="mentor-page__loading-spinner"></div>
<span>加载中...</span>
</div>
<!-- 没有更多了 -->
<div v-if="noMore && mentorList.length > 0" class="mentor-page__no-more">
没有更多了~
</div>
<!-- 空列表 -->
<div v-if="!loading && mentorList.length === 0 && noMore" class="mentor-page__empty">
暂无相关导师
</div>
</div>
</div>
<!-- 右侧广告区域固定宽度 4rem -->
<div class="mentor-page__aside">
<!-- 广告内容占位后续填充 -->
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, nextTick } from 'vue'
import { useRouter } from 'vue-router'
import { useStore } from 'vuex'
import SideNav from '@/components/SideNav.vue'
import { fetchMentorList } from '@/api/mentor'
import type { MentorItem, MentorListParams } from '@/api/mentor'
import { toOssUrl } from '@/utils/image'
// ==================== 行业 Tab 数据 ====================
const router = useRouter()
const store = useStore()
/** 行业 Tab 标签列表 */
const industryTabs = ['推荐', '互联网', '金融', '快消', '咨询', '央国企', '硬科技', '生涯规划', '名校导师', '心理咨询', '其他']
/** 当前选中的 Tab 索引 */
const activeTab = ref(0)
/**
* 生成 min~max 之间的整数数组
*/
function createIntegerRange(min: number, max: number): number[] {
min = Math.floor(min)
max = Math.floor(max)
if (min > max) return []
const result: number[] = []
for (let i = min; i <= max; i++) result.push(i)
return result
}
/**
* 根据当前 Tab 计算 industryIds 参数
*/
const industryIds = computed<number[]>(() => {
if (activeTab.value === 0) return []
if (activeTab.value === 1) return createIntegerRange(1000, 1016)
if (activeTab.value === 2) return createIntegerRange(1131, 1139)
if (activeTab.value === 3) return createIntegerRange(1038, 1047)
if (activeTab.value === 4) return createIntegerRange(1087, 1093)
if (activeTab.value === 5) return []
if (activeTab.value === 6) return [
1012, 1014, 1017, 1018, 1019, 1020, 1021, 1022, 1023,
1074, 1078, 1079, 1084, 1085,
1095, 1097, 1098,
1119, 1120, 1121, 1122, 1123, 1124, 1125, 1126, 1127, 1128, 1129, 1130,
]
if (activeTab.value === 7) return []
if (activeTab.value === 8) return []
if (activeTab.value === 9) return []
if (activeTab.value === 10) {
// 其他 = 全部行业 id 减去已分类的
const all = createIntegerRange(1000, 1143)
const used = new Set([
...createIntegerRange(1000, 1016),
...createIntegerRange(1131, 1139),
...createIntegerRange(1038, 1047),
...createIntegerRange(1087, 1093),
1012, 1014, 1017, 1018, 1019, 1020, 1021, 1022, 1023,
1074, 1078, 1079, 1084, 1085,
1095, 1097, 1098,
1119, 1120, 1121, 1122, 1123, 1124, 1125, 1126, 1127, 1128, 1129, 1130,
])
return all.filter(id => !used.has(id))
}
return []
})
/**
* 根据当前 Tab 计算机构类型 type 参数
*/
const mentorType = computed<string | null>(() => {
return activeTab.value === 5 ? '央国企' : null
})
/**
* 根据当前 Tab 计算身份类型 identityType 参数
*/
const identityType = computed<number | null>(() => {
if (activeTab.value === 5) return 0 // 企业在职
if (activeTab.value === 7) return 1 // 生涯规划师
if (activeTab.value === 8) return 4 // 在校学生(名校导师)
if (activeTab.value === 9) return 2 // 心理咨询师
return null
})
// ==================== 列表 & 分页状态 ====================
/** 导师列表数据 */
const mentorList = ref<MentorItem[]>([])
/** 当前页码 */
const currentPage = ref(1)
/** 每页条数 */
const pageSize = 20
/** 是否正在加载 */
const loading = ref(false)
/** 是否已经没有更多数据 */
const noMore = ref(false)
/** 搜索关键词 */
const keyword = ref('')
/** 列表容器 ref */
const listRef = ref<HTMLElement | null>(null)
// ==================== 接口请求 ====================
/**
* 构建接口请求参数
*/
function buildParams(): MentorListParams {
const params: MentorListParams = {
page: currentPage.value,
pageSize,
sort: 0,
}
if (industryIds.value.length) params.industryIds = industryIds.value
if (mentorType.value !== null) params.type = mentorType.value
if (identityType.value !== null) params.identityType = identityType.value
if (keyword.value.trim().length > 0) params.keyword = keyword.value.trim()
return params
}
/**
* 加载导师列表(追加模式)
*/
async function loadMentors() {
if (loading.value || noMore.value) return
loading.value = true
try {
const res: any = await fetchMentorList(buildParams())
const pageData = res?.data ?? res
const records = pageData?.records ?? []
if (records.length === 0) {
noMore.value = true
} else {
mentorList.value.push(...records)
currentPage.value++
// 返回条数 < pageSize 说明已到最后一页
if (records.length < pageSize) {
noMore.value = true
}
}
} catch (e) {
console.error('[导师列表] 加载失败', e)
} finally {
loading.value = false
}
}
/**
* 重置列表并从第一页开始加载
*/
function resetAndLoad() {
mentorList.value = []
currentPage.value = 1
noMore.value = false
loadMentors()
}
// ==================== 交互事件 ====================
/**
* 切换行业 Tab — 同时重置滚动缓存
*/
function handleTabChange(index: number) {
if (activeTab.value === index) return
activeTab.value = index
// 切换 tab 时清除缓存
store.commit('SET_MENTOR_LIST_CACHE', null)
resetAndLoad()
}
/**
* 点击搜索按钮 / 按 Enter 搜索 — 同时重置滚动缓存
*/
function handleSearch() {
if (keyword.value.trim().length === 0) return
// 搜索时清除缓存
store.commit('SET_MENTOR_LIST_CACHE', null)
resetAndLoad()
}
/**
* 点击清空按钮,清空关键词并重新加载 — 同时重置滚动缓存
*/
function handleClearKeyword() {
keyword.value = ''
// 清空搜索时清除缓存
store.commit('SET_MENTOR_LIST_CACHE', null)
resetAndLoad()
}
/**
* 列表滚动事件 — 距离底部 150px 时提前加载
*/
function onListScroll() {
const el = listRef.value
if (!el || loading.value || noMore.value) return
if (el.scrollHeight - el.scrollTop - el.clientHeight < 150) {
loadMentors()
}
}
// ==================== 跳转详情页 & 滚动缓存 ====================
/** 恢复缓存中 — 隐藏列表避免闪烁 */
const restoring = ref(false)
/** 记录离开页面前的滚动位置 */
let savedScrollTop = 0
/**
* 跳转到导师详情页,保存当前列表缓存
*/
function goToDetail(mentor: MentorItem) {
// 保存当前列表状态到 store,返回时恢复
store.commit('SET_MENTOR_LIST_CACHE', {
list: mentorList.value,
pageNum: currentPage.value,
scrollTop: listRef.value?.scrollTop ?? 0,
})
router.push(`/mentor/${mentor.id}`)
}
// ==================== 生命周期 ====================
onMounted(() => {
// 先判断是否有缓存(从详情页返回)
const cache = store.state.mentorListCache
const hasCache = !!(cache && cache.list.length > 0)
if (hasCache) {
// 恢复缓存数据,隐藏列表避免闪烁
restoring.value = true
mentorList.value = cache!.list
currentPage.value = cache!.pageNum
savedScrollTop = cache!.scrollTop
// 清除缓存,避免下次误用
store.commit('SET_MENTOR_LIST_CACHE', null)
// 恢复滚动位置后显示列表
nextTick(() => {
if (listRef.value) {
listRef.value.scrollTop = savedScrollTop
}
restoring.value = false
})
} else {
// 没有缓存,正常加载
loadMentors()
}
})
</script>
+201
View File
@@ -0,0 +1,201 @@
<template>
<div class="mentor-detail dflex">
<SideNav />
<div class="mentor-detail__body">
<!-- 页面标题 -->
<div class="mentor-detail__page-title">导师详情</div>
<!-- 返回按钮 -->
<div class="mentor-detail__back" @click="goBack">
<svg viewBox="0 0 16 16" fill="none" class="mentor-detail__back-icon">
<path d="M10 12L6 8l4-4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<span>返回</span>
</div>
<!-- 主内容滚动区域 -->
<div class="mentor-detail__scroll">
<!-- 导师信息卡片 -->
<div class="mentor-detail__card mentor-detail__info-card">
<div class="mentor-detail__info-left">
<!-- 头像 -->
<div class="mentor-detail__avatar-wrap">
<img
v-if="mentorInfo?.picture"
:src="toOssUrl(mentorInfo.picture)"
class="mentor-detail__avatar"
:alt="mentorInfo.platformName"
/>
<div v-else class="mentor-detail__avatar-placeholder">
{{ mentorInfo?.platformName?.charAt(0) || '师' }}
</div>
</div>
<!-- 基本信息 -->
<div class="mentor-detail__info-text">
<div class="mentor-detail__name">
<span>{{ mentorInfo?.platformName }}</span>
<div class="mentor-detail__meta">
<span v-if="agencyName">{{ agencyName }}</span>
<span v-if="agencyName && position"> | </span>
<span v-if="position">{{ position }}</span>
</div>
</div>
<div class="mentor-detail__location" v-if="locationText">
所在城市 <span class="">{{ locationText }}</span>
</div>
<div class="mentor-detail__services" v-if="serviceLabels.length">
服务类型 <span>{{ serviceLabels.join('、') }}</span>
</div>
</div>
</div>
<!-- 立即咨询按钮 -->
<a
href="https://www.jianshixingqiu.com"
target="_blank"
rel="noopener noreferrer"
class="mentor-detail__consult-btn"
>立即咨询 </a>
</div>
<!-- 擅长话题卡片 -->
<div class="mentor-detail__card" v-if="talks.length">
<div class="mentor-detail__section-title">擅长话题</div>
<div
v-for="(talk, index) in talks"
:key="talk.id"
class="mentor-detail__talk-item"
>
<div class="mentor-detail__talk-title">{{ index + 1 }}.{{ talk.title }}</div>
<div class="mentor-detail__talk-content" v-if="talk.content">{{ talk.content }}</div>
</div>
</div>
<!-- 个人介绍卡片 -->
<div class="mentor-detail__card" v-if="mentorInfo?.introduce">
<div class="mentor-detail__section-title">个人介绍</div>
<div class="mentor-detail__introduce">{{ mentorInfo.introduce }}</div>
</div>
<!-- 加载中 -->
<div v-if="loading" class="mentor-detail__loading">
<div class="mentor-detail__loading-spinner"></div>
<span>加载中...</span>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import SideNav from '@/components/SideNav.vue'
import { fetchMentorFullInfo, fetchMentorTalks } from '@/api/mentor'
import type { MentorFullInfo, MentorTalk } from '@/api/mentor'
import { toOssUrl } from '@/utils/image'
// ==================== 路由相关 ====================
const router = useRouter()
const route = useRoute()
/** 导师 ID,从路由参数获取(字符串,避免大数精度丢失) */
const mentorId = route.params.id as string
// ==================== 页面状态 ====================
/** 是否正在加载 */
const loading = ref(false)
/** 导师完整信息 */
const fullInfo = ref<MentorFullInfo | null>(null)
/** 导师话题列表 */
const talks = ref<MentorTalk[]>([])
/** 导师基本信息快捷访问 */
const mentorInfo = computed(() => fullInfo.value?.mentorInfo || null)
/** 机构/公司名 — 根据身份类型从不同认证信息中获取 */
const agencyName = computed(() => {
const v = fullInfo.value?.verification
if (!v) return ''
if (v.working?.companyName) return v.working.companyName
if (v.career?.agencyName) return v.career.agencyName
if (v.staff?.schoolName) return v.staff.schoolName
if (v.student?.schoolName) return v.student.schoolName
if (v.psychology?.agencyName) return v.psychology.agencyName
return ''
})
/** 岗位/职位 */
const position = computed(() => {
const v = fullInfo.value?.verification
if (!v) return ''
if (v.working?.position) return v.working.position
if (v.career?.position) return v.career.position
if (v.staff?.position) return v.staff.position
if (v.psychology?.position) return v.psychology.position
return ''
})
/** 所在城市文本 */
const locationText = computed(() => {
const info = mentorInfo.value
if (!info) return ''
const parts = [info.province, info.city].filter(Boolean)
return parts.join(' · ')
})
/** 服务类型标签 */
const serviceLabels = computed(() => {
const supports = fullInfo.value?.supports
if (!supports || !supports.length) return []
const typeMap: Record<number, string> = { 0: '远程实习', 1: '简历内推', 2: '定向内推辅导' }
return supports.map(s => typeMap[s.type] || '').filter(Boolean)
})
// ==================== 数据加载 ====================
/** 加载导师详情和话题 */
async function loadDetail() {
if (!mentorId) return
loading.value = true
try {
// 并行请求导师信息和话题
const [infoRes, talksRes] = await Promise.all([
fetchMentorFullInfo(mentorId),
fetchMentorTalks(mentorId),
])
// 信息接口返回的可能是 { data: ... } 或直接数据
const infoData = (infoRes as any)?.data ?? infoRes
fullInfo.value = infoData
// 话题接口返回数组或 { data: [...] }
const talksData = (talksRes as any)?.data ?? talksRes
talks.value = Array.isArray(talksData) ? talksData : []
} catch (e) {
console.error('[导师详情] 加载失败', e)
} finally {
loading.value = false
}
}
// ==================== 事件处理 ====================
/** 返回导师列表页 */
function goBack() {
if (window.history.length > 1) {
router.back()
} else {
router.push('/mentor')
}
}
// ==================== 生命周期 ====================
onMounted(() => {
loadDetail()
})
</script>