395 lines
13 KiB
Vue
395 lines
13 KiB
Vue
<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)"
|
||
><span>查看详情</span> <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.51612 4.07575C7.38279 3.94242 7.31879 3.78686 7.32412 3.60908C7.32945 3.43131 7.39901 3.27575 7.53278 3.14241C7.66612 3.02019 7.82167 2.95619 7.99946 2.95041C8.17723 2.94464 8.33278 3.00864 8.46612 3.14241L12.8661 7.54241C12.9328 7.60908 12.9801 7.6813 13.0081 7.75908C13.0361 7.83686 13.0497 7.92019 13.0488 8.00908C13.0479 8.09797 13.0339 8.18131 13.0068 8.25908C12.9797 8.33686 12.9326 8.40908 12.8654 8.47575L8.46545 12.8757C8.34323 12.998 8.19034 13.0591 8.00679 13.0591C7.82323 13.0591 7.66501 12.998 7.53212 12.8757C7.39879 12.7424 7.33212 12.584 7.33212 12.4004C7.33212 12.2169 7.39879 12.0586 7.53212 11.9257L10.7828 8.67575H3.33279C3.1439 8.67575 2.98546 8.61175 2.85745 8.48375C2.72945 8.35575 2.66567 8.19753 2.66612 8.00908C2.66656 7.82064 2.73056 7.6622 2.85812 7.53375C2.98568 7.40531 3.1439 7.34153 3.33279 7.34242L10.7828 7.34242L7.51612 4.07575Z" fill="#52CAD1"/>
|
||
</svg>
|
||
</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>
|