导师列表和导师详情
This commit is contained in:
@@ -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>
|
||||
Reference in New Issue
Block a user