导师列表和导师详情
This commit is contained in:
+103
-118
@@ -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 '匹配度偏低'
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user