导师列表和导师详情

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
+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>