导师列表和导师详情
This commit is contained in:
@@ -1,2 +1,5 @@
|
||||
# 开发环境 — 请求走 vite proxy
|
||||
VITE_API_BASE_URL=/api
|
||||
|
||||
# 见识星球接口 — 开发环境走 vite proxy 中转解决跨域
|
||||
VITE_JIANSHI_BASE_URL=/jianshi-api
|
||||
|
||||
@@ -1,2 +1,5 @@
|
||||
# 生产环境 — 同域路径代理,不需要完整域名
|
||||
VITE_API_BASE_URL=/api
|
||||
|
||||
# 见识星球接口 — 生产环境直接请求真实域名
|
||||
VITE_JIANSHI_BASE_URL=https://www.jianshixingqiu.com/proxy-api
|
||||
|
||||
@@ -17,7 +17,7 @@ inclusion: always
|
||||
|
||||
- 颜色用尽量用variables.scss里的统一颜色变量,特别是背景,按钮,文字的颜色
|
||||
- 除了Home.vue首页外,其他页面我上传了截图要写页面或组件的,需要参考图片里文字和布局结构,在保证美观前提上自由发挥(如果我粘贴了图片内容里的完整样式过来,那就要参考我给的样式结合图片里的结构和样式代码保证还原度)
|
||||
- 全局用1rem=100px的格式并注意对某些特殊元素组件的line-height行高影响,纵布局如非必要不用flex-direction: column布局
|
||||
- 全局用1rem=100px的格式并注意对某些特殊元素组件的line-height行高影响,纵布局能用div自动排列加上下margin的就用div排列,不要用display:flex加flex-direction: column布局,除非是像这个盒子里有个子元素需要固定在底部这样需要明确高度的情况就用flex-direction: column,否则没有高度要求内部子元素只需要正常从上到下排列纵布局只用div
|
||||
- 如果是建一个组件,这个组件看我说是用在views里哪个页面的,比如用在Profile.vue里的组件,组件名字最前面要加Profile,而且整个组件的命名不能过度简化,要容易看懂组件的用途;如果检测到某种名字开头的组件数量比如Profile开头的超过15个,就在components里新建个类似profile这样的页面名字的文件夹,把这类命名的组件都移到文件夹里并查找更新组件所有被引用地方的文件地址
|
||||
|
||||
## 注意事项
|
||||
|
||||
@@ -29,6 +29,8 @@ export interface JobListItem {
|
||||
companyShortName: string
|
||||
/** 公司类型(如"上市企业") */
|
||||
companyType: string
|
||||
/** 公司标签列表(如 ["新能源汽车", "乘用车制造", "10000人以上"]) */
|
||||
companyTags: string[]
|
||||
/** 地区名称 */
|
||||
regionName: string
|
||||
/** 岗位分类名称 */
|
||||
@@ -45,6 +47,8 @@ export interface JobListItem {
|
||||
matchScore: number
|
||||
/** 匹配度详情 */
|
||||
matchDetail: MatchDetail
|
||||
/** 招聘分类:0=社招 1=校招 2=实习 */
|
||||
recruitCategory?: number
|
||||
}
|
||||
|
||||
// ==================== 分页结构 ====================
|
||||
|
||||
@@ -0,0 +1,204 @@
|
||||
import jianshiRequest from '@/utils/jianshiRequest'
|
||||
|
||||
/**
|
||||
* 分页查询导师列表请求参数
|
||||
*/
|
||||
export interface MentorListParams {
|
||||
/** 页码,>= 1 */
|
||||
page: number
|
||||
/** 每页条数,1~100 */
|
||||
pageSize: number
|
||||
/** 搜索关键词,可选 */
|
||||
keyword?: string
|
||||
/** 行业ids,可选 */
|
||||
industryIds?: number[]
|
||||
/** 导师ids,可选 */
|
||||
mentorIds?: number[]
|
||||
/** 身份类型:0企业在职 1生涯规划师 2心理咨询师 3教职工 4在校学生,可选 */
|
||||
identityType?: number | null
|
||||
/** 标签ids,可选 */
|
||||
labelIds?: number[]
|
||||
/** 约聊方式:0语音电话 1线下约见,可选 */
|
||||
chatType?: number
|
||||
/** 价格最大值,可选 */
|
||||
maxChatPrice?: number
|
||||
/** 价格最小值,可选 */
|
||||
minChatPrice?: number
|
||||
/** 公司名,可选 */
|
||||
agencyName?: string
|
||||
/** 机构类型,可选 */
|
||||
type?: string | null
|
||||
/** 排序:0默认 1金牌导师优先 2好评优先 3价格升序 4价格降序 */
|
||||
sort: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 导师列表项数据
|
||||
*/
|
||||
export interface MentorItem {
|
||||
/** 导师id(字符串,避免大数精度丢失) */
|
||||
id: string
|
||||
/** 用户id */
|
||||
userId: number
|
||||
/** 平台名 */
|
||||
platformName: string
|
||||
/** 头像 */
|
||||
picture: string
|
||||
/** 话题标题字符串 */
|
||||
titleStr: string
|
||||
/** 话题列表 */
|
||||
titles: string[]
|
||||
/** 标签字符串 */
|
||||
labelStr: string
|
||||
/** 标签列表 */
|
||||
labels: string[]
|
||||
/** 最低价格 */
|
||||
price: number
|
||||
/** 机构名 */
|
||||
agencyName: string
|
||||
/** 岗位 */
|
||||
position: string
|
||||
/** 机构logo */
|
||||
agencyLogo: string
|
||||
/** 排序 */
|
||||
sort: number
|
||||
/** 认证类型 */
|
||||
type: string
|
||||
/** 身份类型 */
|
||||
identityType: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页查询导师列表响应
|
||||
*/
|
||||
export interface MentorPageResult {
|
||||
records: MentorItem[]
|
||||
total: number
|
||||
size: number
|
||||
current: number
|
||||
pages: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页查询导师列表
|
||||
* POST /public/website/pageSelectMentor
|
||||
*/
|
||||
export function fetchMentorList(params: MentorListParams): Promise<MentorPageResult> {
|
||||
return jianshiRequest.post('/public/website/pageSelectMentor', params)
|
||||
}
|
||||
|
||||
// ==================== 导师详情接口 ====================
|
||||
|
||||
/**
|
||||
* 导师详情 — 在职人士认证信息
|
||||
*/
|
||||
export interface MentorWorking {
|
||||
companyName: string
|
||||
type: number
|
||||
companyLogo: string
|
||||
industryId: number
|
||||
position: string
|
||||
currentPositionTerm: number
|
||||
term: number
|
||||
images: string[]
|
||||
link: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 导师详情 — 认证信息
|
||||
*/
|
||||
export interface MentorVerification {
|
||||
career?: { agencyName: string; position: string; type: number; term: number; agencyLogo: string; images: string[] }
|
||||
psychology?: { agencyName: string; position: string; type: number; agencyLogo: string; term: number; images: string[] }
|
||||
staff?: { schoolName: string; type: number; schoolLogo: string; position: string; term: number; images: string[] }
|
||||
student?: { schoolName: string; type: number; schoolLogo: string; major: string; startTime: string; endTime: string; degree: number; images: string[] }
|
||||
working?: MentorWorking
|
||||
}
|
||||
|
||||
/**
|
||||
* 导师详情 — 基本信息
|
||||
*/
|
||||
export interface MentorInfo {
|
||||
platformName: string
|
||||
realName: string
|
||||
nickName: string
|
||||
picture: string
|
||||
identityType: number
|
||||
country?: string
|
||||
province?: string
|
||||
everCompany?: string
|
||||
city?: string
|
||||
phone: string
|
||||
mail?: string
|
||||
introduce?: string
|
||||
introduceImages?: string[]
|
||||
wechat?: string
|
||||
cooperationCount?: number
|
||||
starLevel?: number
|
||||
remark?: string
|
||||
sort?: number
|
||||
status: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 导师详情 — 支持服务项
|
||||
*/
|
||||
export interface MentorSupport {
|
||||
type: number
|
||||
introduce?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 导师详情 — 标签关系
|
||||
*/
|
||||
export interface MentorLabelRelation {
|
||||
labelId: number
|
||||
alias?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 导师详情完整数据
|
||||
*/
|
||||
export interface MentorFullInfo {
|
||||
id: number
|
||||
mentorInfo: MentorInfo
|
||||
verification?: MentorVerification
|
||||
supports?: MentorSupport[]
|
||||
labelRelations?: MentorLabelRelation[]
|
||||
}
|
||||
|
||||
/**
|
||||
* 导师话题项
|
||||
*/
|
||||
export interface MentorTalk {
|
||||
id: number
|
||||
teacherUserId: number
|
||||
title: string
|
||||
content: string
|
||||
chatType: string
|
||||
seePrice: number
|
||||
seeSalePrice: number
|
||||
chatPrice: number
|
||||
chatSalePrice: number
|
||||
tags: string
|
||||
status: string
|
||||
talkTimes: number
|
||||
jobBusinessType: string
|
||||
caseImages?: string[]
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取导师详情信息
|
||||
* GET /public/website/selectMentorFullInfo?mentorId=xxx
|
||||
*/
|
||||
export function fetchMentorFullInfo(mentorId: string): Promise<MentorFullInfo> {
|
||||
return jianshiRequest.get('/public/website/selectMentorFullInfo', { params: { mentorId } })
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取导师话题列表
|
||||
* GET /public/website/selectTalk?teacherId=xxx
|
||||
*/
|
||||
export function fetchMentorTalks(teacherId: string): Promise<MentorTalk[]> {
|
||||
return jianshiRequest.get('/public/website/selectTalk', { params: { teacherId } })
|
||||
}
|
||||
@@ -391,6 +391,12 @@ body:not(#_) {
|
||||
.flex-warp{
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.flex1{
|
||||
flex: 1;
|
||||
}
|
||||
.flex2{
|
||||
flex: 2;
|
||||
}
|
||||
|
||||
//字体粗细
|
||||
.fw600{
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
&__item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.08rem;
|
||||
gap: 0.04rem;
|
||||
padding: 0.1rem 0.12rem;
|
||||
border-radius: 0.08rem;
|
||||
color: #9598ab;
|
||||
@@ -65,13 +65,17 @@
|
||||
font-size: 0.14rem;
|
||||
|
||||
&:hover {
|
||||
background: $btn-dark;
|
||||
color: #fff;
|
||||
background: #EDF9FA;
|
||||
color: $accent;
|
||||
|
||||
svg{
|
||||
color: $btn-dark;
|
||||
}
|
||||
}
|
||||
|
||||
&--active {
|
||||
background: $btn-dark;
|
||||
color: #fff;
|
||||
background: #EDF9FA;
|
||||
color: $accent;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,6 +83,8 @@
|
||||
width: 0.2rem;
|
||||
height: 0.2rem;
|
||||
object-fit: contain;
|
||||
font-size: 0.16rem;
|
||||
|
||||
}
|
||||
|
||||
&__item-label {
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
@use './pages/job-detail.scss';
|
||||
@use './pages/profile.scss';
|
||||
@use './pages/login.scss';
|
||||
@use './pages/mentor.scss';
|
||||
@use './pages/mentor-detail.scss';
|
||||
|
||||
// 组件样式
|
||||
@use './components/hello-world.scss';
|
||||
|
||||
+242
-198
@@ -12,9 +12,9 @@
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
background: $bg-main;
|
||||
font-size: 0.14rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-size: 0.14rem;
|
||||
}
|
||||
|
||||
// 页面标题
|
||||
@@ -203,26 +203,33 @@
|
||||
|
||||
// 职位列表
|
||||
&__list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.12rem;
|
||||
gap: 0;
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding-bottom: 0.2rem;
|
||||
|
||||
// 列表中每张卡片间距用 margin-bottom 替代 gap
|
||||
& > .jobs-page__job-card {
|
||||
margin-bottom: 0.12rem;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__job-card {
|
||||
background: $bg-white;
|
||||
border-radius: 0.12rem;
|
||||
padding: 0.16rem 0.14rem;
|
||||
padding: 0.2rem 0.24rem;
|
||||
transition: all 0.25s ease;
|
||||
border: 0.02rem solid transparent;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 0.04rem 0.16rem rgba(0, 0, 0, 0.06);
|
||||
background: #fbfbfb;
|
||||
//transform: translateY(-0.01rem);
|
||||
}
|
||||
|
||||
&--selected {
|
||||
@@ -238,65 +245,48 @@
|
||||
gap: 0.2rem;
|
||||
}
|
||||
|
||||
&__job-left {
|
||||
// ========== 左栏:职位信息 ==========
|
||||
&__col-left {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&__job-icon {
|
||||
width: 0.54rem;
|
||||
height: 0.54rem;
|
||||
border-radius: 0.1rem;
|
||||
background: $theme-color;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
border: 1px solid #E8E8E8;
|
||||
}
|
||||
|
||||
&__company-svg {
|
||||
width: 0.24rem;
|
||||
height: 0.24rem;
|
||||
color: $accent;
|
||||
}
|
||||
|
||||
&__job-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
flex-direction: column; // 纵排,配合 meta-bottom 的 margin-top: auto 把它推到底部
|
||||
}
|
||||
|
||||
&__job-title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.08rem;
|
||||
}
|
||||
|
||||
&__job-name {
|
||||
font-size: 0.15rem;
|
||||
font-weight: 600;
|
||||
color: $text-dark;
|
||||
line-height: 1.4;
|
||||
font-size: 0.18rem;
|
||||
font-weight: 520;
|
||||
color: #000000;
|
||||
line-height: 0.24rem;
|
||||
}
|
||||
|
||||
&__job-more {
|
||||
color: $text-light;
|
||||
cursor: pointer;
|
||||
// 收藏红心
|
||||
&__favorite-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0.04rem;
|
||||
border-radius: 0.04rem;
|
||||
transition: all 0.2s;
|
||||
cursor: pointer;
|
||||
padding: 0.02rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: #8C8C8C;
|
||||
transition: color 0.2s;
|
||||
|
||||
&:hover {
|
||||
color: $text-dark;
|
||||
background: $bg-main;
|
||||
color: $danger;
|
||||
}
|
||||
|
||||
&--liked {
|
||||
color: $danger;
|
||||
}
|
||||
}
|
||||
|
||||
&__more-svg {
|
||||
&__favorite-svg {
|
||||
width: 0.16rem;
|
||||
height: 0.16rem;
|
||||
}
|
||||
@@ -323,168 +313,218 @@
|
||||
height: 0.14rem;
|
||||
}
|
||||
|
||||
&__job-meta {
|
||||
font-size: 0.14rem;
|
||||
color: $text-middle;
|
||||
margin-top: 0.04rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
&__job-dot {
|
||||
margin: 0 0.04rem;
|
||||
color: #D9D9D9;
|
||||
}
|
||||
|
||||
&__job-tip {
|
||||
font-size: 0.11rem;
|
||||
color: $accent-hover;
|
||||
background: $theme-color;
|
||||
border-radius: 0.04rem;
|
||||
padding: 0.05rem 0.1rem;
|
||||
margin-top: 0.1rem;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.04rem;
|
||||
border: 1px solid rgba(79, 194, 201, 0.2);
|
||||
}
|
||||
|
||||
&__tip-svg {
|
||||
width: 0.12rem;
|
||||
height: 0.12rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
// 职位标签
|
||||
&__job-tags {
|
||||
display: flex;
|
||||
gap: 0.08rem;
|
||||
margin-top: 0.1rem;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 0.08rem;
|
||||
}
|
||||
|
||||
&__job-tag {
|
||||
font-size: 0.11rem;
|
||||
color: #888;
|
||||
background: $bg-main;
|
||||
border-radius: 0.04rem;
|
||||
padding: 0.03rem 0.1rem;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
color: $accent-hover;
|
||||
background: $theme-color;
|
||||
}
|
||||
}
|
||||
|
||||
// 底部操作栏
|
||||
&__job-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: 0.14rem;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
&__job-action-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.08rem;
|
||||
}
|
||||
|
||||
&__action-icon-btn {
|
||||
width: 0.28rem;
|
||||
height: 0.28rem;
|
||||
border-radius: 50%;
|
||||
border: none;
|
||||
background: $bg-main;
|
||||
color: $text-light;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s;
|
||||
padding: 0.04rem 0.06rem;
|
||||
background: #F4F4F4;
|
||||
border-radius: 0.04rem;
|
||||
font-size: 0.12rem;
|
||||
font-weight: 330;
|
||||
color: #929292;
|
||||
line-height: 0.16rem;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: $theme-color;
|
||||
// 底部:地区 + 学历 + 招聘分类,margin-top: auto 把它推到左栏底部
|
||||
&__job-meta-bottom {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.12rem;
|
||||
margin-top: auto;
|
||||
padding-top: 0.08rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
&__meta-location {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.02rem;
|
||||
font-size: 0.14rem;
|
||||
font-weight: 330;
|
||||
color: #8C8C8C;
|
||||
line-height: 0.19rem;
|
||||
}
|
||||
|
||||
&__location-icon {
|
||||
width: 0.18rem;
|
||||
height: 0.18rem;
|
||||
color: #8C8C8C;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__meta-edu {
|
||||
font-size: 0.14rem;
|
||||
font-weight: 330;
|
||||
color: #8C8C8C;
|
||||
line-height: 0.19rem;
|
||||
}
|
||||
|
||||
&__meta-recruit {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0.02rem 0.06rem;
|
||||
background: #EDF9FA;
|
||||
border-radius: 0.04rem;
|
||||
font-size: 0.12rem;
|
||||
font-weight: 330;
|
||||
color: $accent;
|
||||
line-height: 0.16rem;
|
||||
}
|
||||
|
||||
&--liked {
|
||||
background: #FFF0ED;
|
||||
color: $danger;
|
||||
|
||||
&:hover {
|
||||
background: #FFE4DE;
|
||||
}
|
||||
}
|
||||
// ========== 中栏:公司信息(居中) ==========
|
||||
&__col-center {
|
||||
flex: 1; // 参与 flex 空间分配,获得真实宽度约束
|
||||
min-width: 0; // 防止内容撑破 flex 容器
|
||||
}
|
||||
|
||||
&__action-svg {
|
||||
width: 0.14rem;
|
||||
height: 0.14rem;
|
||||
// 公司信息容器,需要 min-width 才能让内部标签换行
|
||||
&__company-info {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&__company-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.06rem;
|
||||
margin-bottom: 0.06rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
&__company-logo {
|
||||
width: 0.24rem;
|
||||
height: 0.24rem;
|
||||
border-radius: 0.04rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
font-size: 0.24rem;
|
||||
color: #8C8C8C;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&__company-name {
|
||||
font-size: 0.18rem;
|
||||
font-weight: 330;
|
||||
color: #000000;
|
||||
line-height: 0.24rem;
|
||||
}
|
||||
|
||||
&__company-tags {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.08rem;
|
||||
flex-wrap: wrap;
|
||||
min-width: 0; // 允许内容收缩触发换行
|
||||
margin-top: 0.06rem;
|
||||
}
|
||||
|
||||
&__company-tag {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0.04rem 0.06rem;
|
||||
background: #F4F4F4;
|
||||
border-radius: 0.04rem;
|
||||
font-size: 0.12rem;
|
||||
font-weight: 330;
|
||||
color: #929292;
|
||||
line-height: 0.16rem;
|
||||
}
|
||||
|
||||
// ========== 右栏:操作按钮 ==========
|
||||
&__col-right {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
&__job-action-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.12rem;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.1rem;
|
||||
}
|
||||
|
||||
&__job-helper {
|
||||
font-size: 0.12rem;
|
||||
color: $text-light;
|
||||
cursor: pointer;
|
||||
transition: color 0.2s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.04rem;
|
||||
background: $bg-main;
|
||||
border: 1px solid #E8E8E8;
|
||||
padding: 0.04rem 0.08rem;
|
||||
border-radius: 0.14rem;
|
||||
justify-content: center;
|
||||
padding: 0.08rem 0.16rem;
|
||||
border: 1.23px solid $accent;
|
||||
border-radius: 0.08rem;
|
||||
background: none;
|
||||
font-size: 0.14rem;
|
||||
font-weight: 450;
|
||||
color: $accent;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
line-height: 0.19rem;
|
||||
white-space: nowrap;
|
||||
|
||||
&:hover {
|
||||
color: $accent;
|
||||
background: $theme-color;
|
||||
}
|
||||
}
|
||||
|
||||
&__helper-svg {
|
||||
width: 0.14rem;
|
||||
height: 0.14rem;
|
||||
}
|
||||
|
||||
&__job-apply-btn {
|
||||
background: $btn-dark;
|
||||
border: 0.02rem solid $btn-dark;
|
||||
border-radius: 0.2rem;
|
||||
padding: 0.04rem 0.2rem;
|
||||
color: $bg-white;
|
||||
font-size: 0.12rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0.08rem 0.16rem;
|
||||
background: $accent;
|
||||
border: none;
|
||||
border-radius: 0.08rem;
|
||||
font-size: 0.14rem;
|
||||
font-weight: 450;
|
||||
color: #FFFFFF;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
font-weight: 500;
|
||||
line-height: 0.19rem;
|
||||
white-space: nowrap;
|
||||
|
||||
&:hover {
|
||||
background: $btn-dark-hover;
|
||||
background: $accent-hover;
|
||||
}
|
||||
|
||||
&--active {
|
||||
background: $accent;
|
||||
border-color: $accent;
|
||||
color: $bg-white;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
}
|
||||
|
||||
// 不再显示
|
||||
&__hide-btn {
|
||||
display: block;
|
||||
margin-top: auto;
|
||||
font-size: 0.14rem;
|
||||
font-weight: 330;
|
||||
color: #8C8C8C;
|
||||
cursor: pointer;
|
||||
transition: color 0.2s;
|
||||
line-height: 0.19rem;
|
||||
white-space: nowrap;
|
||||
|
||||
&:hover {
|
||||
background: $accent-hover;
|
||||
border-color: $accent-hover;
|
||||
}
|
||||
color: $text-dark;
|
||||
}
|
||||
}
|
||||
|
||||
// 弹出菜单
|
||||
&__job-popup {
|
||||
position: absolute;
|
||||
right: 0.26rem;
|
||||
top: -0.64rem;
|
||||
right: 1.4rem;
|
||||
top: 0.5rem;
|
||||
background: $bg-white;
|
||||
border: 1px solid #E8E8E8;
|
||||
border-radius: 0.1rem;
|
||||
@@ -507,38 +547,29 @@
|
||||
}
|
||||
}
|
||||
|
||||
// 匹配度卡片
|
||||
// ========== 最右侧:匹配度 ==========
|
||||
&__job-match {
|
||||
width: 1.1rem;
|
||||
width: 1.01rem;
|
||||
text-align: center;
|
||||
flex-shrink: 0;
|
||||
border-radius: 0.12rem;
|
||||
border-radius: 0.08rem;
|
||||
padding: 0.14rem 0.1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.02rem;
|
||||
transition: all 0.25s ease;
|
||||
|
||||
&--high {
|
||||
background: linear-gradient(135deg, $theme-color 0%, rgba(79, 194, 201, 0.15) 100%);
|
||||
border: 1px solid rgba(79, 194, 201, 0.25);
|
||||
background: #FBFDF7;
|
||||
}
|
||||
|
||||
&--low {
|
||||
background: $bg-main;
|
||||
border: 1px solid #E8E8E8;
|
||||
background: #FBFDF7;
|
||||
}
|
||||
}
|
||||
|
||||
&__match-ring {
|
||||
position: relative;
|
||||
width: 0.64rem;
|
||||
height: 0.64rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 0.62rem;
|
||||
height: 0.62rem;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
&__ring-svg {
|
||||
@@ -551,23 +582,39 @@
|
||||
|
||||
&__match-score {
|
||||
font-size: 0.18rem;
|
||||
font-weight: 700;
|
||||
color: $text-dark;
|
||||
line-height: 1;
|
||||
position: relative;
|
||||
font-weight: 520;
|
||||
line-height: 0.62rem;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
&__match-label {
|
||||
font-size: 0.1rem;
|
||||
color: $text-light;
|
||||
&__match-level {
|
||||
font-size: 0.13rem;
|
||||
font-weight: 520;
|
||||
line-height: 0.19rem;
|
||||
margin-top: 0.04rem;
|
||||
}
|
||||
|
||||
&__match-level {
|
||||
font-size: 0.1rem;
|
||||
color: $text-light;
|
||||
margin-top: 0.01rem;
|
||||
// 高匹配度颜色
|
||||
&__job-match--high &__match-score {
|
||||
color: #9FD051;
|
||||
}
|
||||
|
||||
&__job-match--high &__match-level {
|
||||
color: #9FD051;
|
||||
}
|
||||
|
||||
// 低匹配度颜色
|
||||
&__job-match--low &__match-score {
|
||||
color: #9FD051;
|
||||
}
|
||||
|
||||
&__job-match--low &__match-level {
|
||||
color: #9FD051;
|
||||
}
|
||||
|
||||
// 未登录锁定态
|
||||
@@ -580,7 +627,8 @@
|
||||
width: 0.32rem;
|
||||
height: 0.32rem;
|
||||
color: $text-light;
|
||||
margin-bottom: 0.06rem;
|
||||
display: block;
|
||||
margin: 0 auto 0.06rem;
|
||||
}
|
||||
|
||||
&__match-lock-text {
|
||||
@@ -658,16 +706,6 @@
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
// 高匹配度特殊样式
|
||||
&__job-match--high &__match-score {
|
||||
color: $accent-hover;
|
||||
}
|
||||
|
||||
&__job-match--high &__match-level {
|
||||
color: $accent;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
// ==================== AI助手投递提醒弹窗 ====================
|
||||
|
||||
// 遮罩层
|
||||
@@ -766,10 +804,16 @@
|
||||
|
||||
// 选项列表容器
|
||||
&__options {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.1rem;
|
||||
margin-bottom: 0.16rem;
|
||||
|
||||
// 选项间距用 margin-bottom
|
||||
.dislike-dialog__option {
|
||||
margin-bottom: 0.1rem;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 单个选项行
|
||||
|
||||
@@ -0,0 +1,247 @@
|
||||
@use '../variables' as *;
|
||||
|
||||
// ==================== 导师详情页样式 ====================
|
||||
|
||||
.mentor-detail {
|
||||
font-size: 0.14rem;
|
||||
width: 100%;
|
||||
height: var(--app-height, 100vh);
|
||||
background: $bg-main;
|
||||
overflow: hidden;
|
||||
|
||||
// 主体区域
|
||||
&__body {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
margin-left: 2rem;
|
||||
padding: 0.24rem 0.4rem 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
// 页面标题
|
||||
&__page-title {
|
||||
font-size: 0.18rem;
|
||||
font-weight: 600;
|
||||
color: $text-dark;
|
||||
margin-bottom: 0.12rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
// 返回按钮
|
||||
&__back {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.04rem;
|
||||
font-size: 0.13rem;
|
||||
color: $text-middle;
|
||||
cursor: pointer;
|
||||
margin-bottom: 0.16rem;
|
||||
flex-shrink: 0;
|
||||
|
||||
&:hover {
|
||||
color: $accent;
|
||||
}
|
||||
}
|
||||
|
||||
&__back-icon {
|
||||
width: 0.14rem;
|
||||
height: 0.14rem;
|
||||
}
|
||||
|
||||
// 滚动容器
|
||||
&__scroll {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding-bottom: 0.24rem;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: $border-color;
|
||||
border-radius: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
// 通用卡片
|
||||
&__card {
|
||||
background: $bg-white;
|
||||
border-radius: 0.08rem;
|
||||
padding: 0.4rem 0.32rem;
|
||||
margin-bottom: 0.16rem;
|
||||
}
|
||||
|
||||
// ==================== 导师信息卡片 ====================
|
||||
|
||||
&__info-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.2rem;
|
||||
}
|
||||
|
||||
&__info-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.16rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
// 头像
|
||||
&__avatar-wrap {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__avatar {
|
||||
width: 1.04rem;
|
||||
height: 1.04rem;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
&__avatar-placeholder {
|
||||
width: 0.6rem;
|
||||
height: 0.6rem;
|
||||
border-radius: 50%;
|
||||
background: $accent;
|
||||
color: $bg-white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.2rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
// 导师文字信息
|
||||
&__info-text {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&__name {
|
||||
font-size: 0.24rem;
|
||||
font-weight: 600;
|
||||
color: $text-dark;
|
||||
margin-bottom: 0.04rem;
|
||||
}
|
||||
|
||||
&__meta {
|
||||
font-size: 0.18rem;
|
||||
color: $text-middle;
|
||||
margin-bottom: 0.04rem;
|
||||
display: inline-block;
|
||||
font-weight: 400;
|
||||
margin-left: 0.1rem;
|
||||
}
|
||||
|
||||
&__location {
|
||||
font-size: 0.14rem;
|
||||
color: $text-middle;
|
||||
margin-bottom: 0.08rem;
|
||||
|
||||
>span{
|
||||
color: $text-dark;
|
||||
}
|
||||
}
|
||||
|
||||
&__services {
|
||||
font-size: 0.14rem;
|
||||
color: $text-middle;
|
||||
>span{
|
||||
color: $text-dark;
|
||||
}
|
||||
}
|
||||
|
||||
// 立即咨询按钮
|
||||
&__consult-btn {
|
||||
width: 0.92rem;
|
||||
text-align: center;
|
||||
height: 0.32rem;
|
||||
line-height: 0.30rem;
|
||||
background: #fff;
|
||||
border: 0.01rem solid $accent;
|
||||
color: $accent;
|
||||
border-radius: 0.08rem;
|
||||
font-size: 0.12rem;
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
transition: background 0.15s;
|
||||
|
||||
&:hover {
|
||||
background: $btn-dark;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 区域标题 ====================
|
||||
|
||||
&__section-title {
|
||||
font-size: 0.18rem;
|
||||
font-weight: 600;
|
||||
color: $accent;
|
||||
margin-bottom: 0.14rem;
|
||||
}
|
||||
|
||||
// ==================== 话题列表 ====================
|
||||
|
||||
&__talk-item {
|
||||
margin-bottom: 0.16rem;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__talk-title {
|
||||
font-size: 0.16rem;
|
||||
font-weight: 600;
|
||||
color: $text-dark;
|
||||
margin-bottom: 0.06rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
&__talk-content {
|
||||
font-size: 0.14rem;
|
||||
color: $text-middle;
|
||||
line-height: 1.7;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
// ==================== 个人介绍 ====================
|
||||
|
||||
&__introduce {
|
||||
font-size: 0.12rem;
|
||||
color: $text-middle;
|
||||
line-height: 1.7;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
// ==================== 加载状态 ====================
|
||||
|
||||
&__loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.08rem;
|
||||
padding: 0.3rem;
|
||||
color: $text-middle;
|
||||
font-size: 0.13rem;
|
||||
}
|
||||
|
||||
&__loading-spinner {
|
||||
width: 0.18rem;
|
||||
height: 0.18rem;
|
||||
border: 2px solid $border-color;
|
||||
border-top-color: $accent;
|
||||
border-radius: 50%;
|
||||
animation: mentorDetailSpin 0.7s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes mentorDetailSpin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,375 @@
|
||||
@use '../variables' as *;
|
||||
|
||||
// ==================== Mentor 导师页面样式 ====================
|
||||
|
||||
.mentor-page {
|
||||
font-size: 0.14rem;
|
||||
width: 100%;
|
||||
height: var(--app-height, 100vh);
|
||||
background: $bg-main;
|
||||
overflow: hidden;
|
||||
|
||||
// 主体区域(SideNav 固定宽 2rem,主内容左移让出空间)
|
||||
&__body {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
margin-left: 2rem;
|
||||
}
|
||||
|
||||
// 左侧主内容区
|
||||
&__main {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
// 顶部标题 + 搜索框
|
||||
&__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 0.16rem;
|
||||
flex-shrink: 0;
|
||||
background: #fff;
|
||||
padding: 0.16rem 0.24rem;
|
||||
}
|
||||
|
||||
&__title-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.08rem;
|
||||
}
|
||||
|
||||
&__title-icon {
|
||||
color: $accent;
|
||||
font-size: 0.18rem;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 0.18rem;
|
||||
font-weight: 600;
|
||||
color: $text-dark;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
&__subtitle {
|
||||
font-size: 0.12rem;
|
||||
color: $text-middle;
|
||||
margin-top: 0.02rem;
|
||||
}
|
||||
|
||||
// 搜索框
|
||||
&__search-wrap {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 2.8rem;
|
||||
background: $bg-white;
|
||||
border: 1px solid $border-color;
|
||||
border-radius: 0.2rem;
|
||||
padding: 0 0.12rem;
|
||||
height: 0.36rem;
|
||||
|
||||
&:focus-within {
|
||||
border-color: $accent;
|
||||
}
|
||||
}
|
||||
|
||||
&__search-icon {
|
||||
width: 0.16rem;
|
||||
height: 0.16rem;
|
||||
color: $text-light;
|
||||
flex-shrink: 0;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: $accent;
|
||||
}
|
||||
}
|
||||
|
||||
&__search-input {
|
||||
flex: 1;
|
||||
border: none;
|
||||
outline: none;
|
||||
font-size: 0.13rem;
|
||||
color: $text-dark;
|
||||
background: transparent;
|
||||
padding: 0 0.08rem;
|
||||
line-height: 1;
|
||||
|
||||
&::placeholder {
|
||||
color: $text-light;
|
||||
}
|
||||
}
|
||||
|
||||
&__search-clear {
|
||||
font-size: 0.12rem;
|
||||
color: $text-light;
|
||||
cursor: pointer;
|
||||
padding: 0.02rem 0.04rem;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
line-height: 1;
|
||||
|
||||
&:hover {
|
||||
color: $text-middle;
|
||||
background: $bg-main;
|
||||
}
|
||||
}
|
||||
|
||||
// 行业 Tab 筛选栏
|
||||
&__tabs {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.00rem;
|
||||
flex-wrap: wrap;
|
||||
margin: 0 0.24rem;
|
||||
margin-bottom: 0.16rem;
|
||||
flex-shrink: 0;
|
||||
background: #fff;
|
||||
padding: 0.06rem;
|
||||
border-radius: 0.08rem;
|
||||
|
||||
}
|
||||
|
||||
&__tab {
|
||||
height: 0.35rem;
|
||||
text-align: center;
|
||||
padding: 0 0.16rem;
|
||||
line-height: 0.35rem;
|
||||
border-radius: 0.08rem;
|
||||
min-width: 0.8rem;
|
||||
font-size: 0.13rem;
|
||||
color: $text-middle;
|
||||
cursor: pointer;
|
||||
background: $bg-white;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid transparent;
|
||||
transition: all 0.15s;
|
||||
white-space: nowrap;
|
||||
|
||||
&:not(&--active):hover {
|
||||
color: $accent;
|
||||
border: 1px solid $accent;
|
||||
}
|
||||
|
||||
&--active {
|
||||
background: $accent;
|
||||
color: $bg-white;
|
||||
border: 1px solid $accent;
|
||||
|
||||
&:hover {
|
||||
color: $bg-white;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 导师列表滚动容器
|
||||
&__list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding-bottom: 0.24rem;
|
||||
margin: 0 0.17rem 0 0.24rem;
|
||||
padding-right: 0.04rem;
|
||||
&::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: $border-color;
|
||||
border-radius: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
// 导师卡片
|
||||
&__card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: $bg-white;
|
||||
border-radius: 0.12rem;
|
||||
padding: 0.18rem 0.2rem;
|
||||
margin-bottom: 0.12rem;
|
||||
gap: 0.2rem;
|
||||
transition: box-shadow 0.15s;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
}
|
||||
|
||||
// 卡片左侧 — 限制最大宽度让右侧话题纵向对齐
|
||||
&__card-left {
|
||||
flex: 1 1 0;
|
||||
max-width: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.14rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
// 头像
|
||||
&__avatar-wrap {
|
||||
flex-shrink: 0;
|
||||
width: 0.56rem;
|
||||
}
|
||||
|
||||
&__avatar {
|
||||
width: 0.5rem;
|
||||
height: 0.5rem;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
&__avatar-placeholder {
|
||||
width: 0.5rem;
|
||||
height: 0.5rem;
|
||||
border-radius: 50%;
|
||||
background: $accent;
|
||||
color: $bg-white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.18rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
// 导师信息文字区
|
||||
&__card-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&__card-name {
|
||||
font-size: 0.15rem;
|
||||
font-weight: 600;
|
||||
color: $text-dark;
|
||||
margin-bottom: 0.04rem;
|
||||
}
|
||||
|
||||
&__card-meta {
|
||||
font-size: 0.12rem;
|
||||
color: $text-middle;
|
||||
margin-bottom: 0.08rem;
|
||||
}
|
||||
|
||||
&__card-sep {
|
||||
margin: 0 0.04rem;
|
||||
color: $border-color;
|
||||
}
|
||||
|
||||
&__card-labels {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.06rem;
|
||||
}
|
||||
|
||||
&__card-label {
|
||||
font-size: 0.11rem;
|
||||
color: $accent;
|
||||
background: rgba(18, 199, 190, 0.1);
|
||||
border-radius: 0.1rem;
|
||||
padding: 0.02rem 0.08rem;
|
||||
}
|
||||
|
||||
// 卡片右侧 — flex:1 让话题区自适应剩余空间,窄屏不会撑出滚动条
|
||||
&__card-right {
|
||||
flex: 1 1 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.12rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
// 话题列表 — 占据右侧剩余空间,自动截断
|
||||
&__card-topics {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
// 单条话题,跟随容器宽度自动截断,不设死宽度
|
||||
&__card-topic {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-size: 0.12rem;
|
||||
color: $text-middle;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
// 查看详情按钮
|
||||
&__detail-btn {
|
||||
height: 0.32rem;
|
||||
line-height: 0.32rem;
|
||||
width: 0.92rem;
|
||||
text-align: center;
|
||||
background: $bg-white;
|
||||
border: 0.01rem solid $btn-dark;
|
||||
color: $btn-dark;
|
||||
border-radius: 0.08rem;
|
||||
font-size: 0.12rem;
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
|
||||
&:hover {
|
||||
background: $btn-dark;
|
||||
color: $bg-white;
|
||||
}
|
||||
}
|
||||
|
||||
// 加载中提示
|
||||
&__loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.08rem;
|
||||
padding: 0.2rem;
|
||||
color: $text-middle;
|
||||
font-size: 0.13rem;
|
||||
}
|
||||
|
||||
&__loading-spinner {
|
||||
width: 0.18rem;
|
||||
height: 0.18rem;
|
||||
border: 2px solid $border-color;
|
||||
border-top-color: $accent;
|
||||
border-radius: 50%;
|
||||
animation: mentorSpin 0.7s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes mentorSpin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
// 没有更多
|
||||
&__no-more {
|
||||
text-align: center;
|
||||
font-size: 0.12rem;
|
||||
color: $text-light;
|
||||
padding: 0.16rem;
|
||||
}
|
||||
|
||||
// 空列表
|
||||
&__empty {
|
||||
text-align: center;
|
||||
font-size: 0.13rem;
|
||||
color: $text-light;
|
||||
padding: 0.4rem;
|
||||
}
|
||||
|
||||
// 右侧广告区域(固定 4rem)
|
||||
&__aside {
|
||||
width: 3rem;
|
||||
flex-shrink: 0;
|
||||
height: 100%;
|
||||
background: $bg-white;
|
||||
border-left: 1px solid $border-color;
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
@@ -18,13 +18,13 @@ $bg-middle: #E6E6E6;
|
||||
$bg-white: #FFFFFF;
|
||||
|
||||
// 深色文字(标题、正文)
|
||||
$text-dark: #000000;
|
||||
$text-dark: #1A1A1A;
|
||||
|
||||
// 浅色文字(副标题、占位符、辅助信息)
|
||||
$text-light: #BFBFBF;
|
||||
|
||||
// 中间色文字(使用优先级高于浅色文字,稍微要深色一点的副标题、占位符、辅助信息)
|
||||
$text-middle: #777777;
|
||||
$text-middle: #6A7282;
|
||||
|
||||
// 强调色 / 品牌色
|
||||
//$accent: #4FC2C9;
|
||||
|
||||
+88
-12
@@ -17,16 +17,77 @@
|
||||
:class="{ 'side-nav__item--active': isActive(item.name) }"
|
||||
@click="handleNav(item)"
|
||||
>
|
||||
<img class="side-nav__item-icon" :src="item.iconImg" :alt="item.label" />
|
||||
<!-- SVG图标,通过v-html渲染以支持currentColor继承颜色 -->
|
||||
<span class="side-nav__item-icon" v-html="item.iconImg"></span>
|
||||
<span class="side-nav__item-label">{{ item.label }}</span>
|
||||
<span v-if="item.badge" class="side-nav__badge">{{ item.badge }}</span>
|
||||
</a>
|
||||
</nav>
|
||||
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_151_276)">
|
||||
<path fill="currentColor" stroke="currentColor" stroke-width="0.2" d="M7.66664 2.16675C6.78388 2.16681 5.91411 2.37934 5.13081 2.7864C4.34751 3.19346 3.6737 3.78309 3.16631 4.50545C2.65891 5.22782 2.33285 6.0617 2.21567 6.93664C2.09849 7.81158 2.19362 8.70188 2.49305 9.5323C2.79247 10.3627 3.28738 11.1089 3.93596 11.7077C4.58454 12.3065 5.36773 12.7405 6.21936 12.9728C7.07099 13.2052 7.96603 13.2291 8.82886 13.0426C9.6917 12.8561 10.497 12.4647 11.1766 11.9014L12.962 13.6867C13.0568 13.7751 13.1821 13.8232 13.3117 13.8209C13.4412 13.8186 13.5648 13.7661 13.6564 13.6745C13.748 13.5829 13.8005 13.4593 13.8028 13.3298C13.805 13.2002 13.757 13.0749 13.6686 12.9801L11.8853 11.1961C12.5567 10.3936 12.9853 9.41624 13.1208 8.37872C13.2562 7.34121 13.0929 6.28658 12.65 5.33862C12.2071 4.39066 11.503 3.58871 10.6203 3.02688C9.73759 2.46506 8.71296 2.16667 7.66664 2.16675Z" />
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_151_276">
|
||||
<rect width="16" height="16" fill="none"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_151_281)">
|
||||
<path fill="currentColor" stroke="currentColor" stroke-width="0.2" d="M10.5 8.66675C10.5 8.53414 10.4473 8.40696 10.3536 8.31319C10.2598 8.21943 10.1326 8.16675 10 8.16675H6C5.86739 8.16675 5.74022 8.21943 5.64645 8.31319C5.55268 8.40696 5.5 8.53414 5.5 8.66675C5.5 8.79936 5.55268 8.92653 5.64645 9.0203C5.74022 9.11407 5.86739 9.16675 6 9.16675H10C10.1326 9.16675 10.2598 9.11407 10.3536 9.0203C10.4473 8.92653 10.5 8.79936 10.5 8.66675ZM10.5 11.3334C10.5 11.2008 10.4473 11.0736 10.3536 10.9799C10.2598 10.8861 10.1326 10.8334 10 10.8334H6C5.86739 10.8334 5.74022 10.8861 5.64645 10.9799C5.55268 11.0736 5.5 11.2008 5.5 11.3334C5.5 11.466 5.55268 11.5932 5.64645 11.687C5.74022 11.7807 5.86739 11.8334 6 11.8334H10C10.1326 11.8334 10.2598 11.7807 10.3536 11.687C10.4473 11.5932 10.5 11.466 10.5 11.3334Z"/>
|
||||
<path fill="currentColor" stroke="currentColor" stroke-width="0.1" fill-rule="evenodd" clip-rule="evenodd" d="M4.66671 1.5C4.18048 1.5 3.71416 1.69315 3.37034 2.03697C3.02653 2.38079 2.83337 2.8471 2.83337 3.33333V12.6667C2.83337 13.1529 3.02653 13.6192 3.37034 13.963C3.71416 14.3068 4.18048 14.5 4.66671 14.5H11.3334C11.8196 14.5 12.2859 14.3068 12.6297 13.963C12.9736 13.6192 13.1667 13.1529 13.1667 12.6667V5.312C13.1667 5.058 13.084 4.81133 12.9307 4.60867L10.932 1.96333C10.8233 1.81941 10.6827 1.70267 10.5212 1.62227C10.3597 1.54188 10.1818 1.50002 10.0014 1.5H4.66671ZM3.83337 3.33333C3.83337 2.87333 4.20671 2.5 4.66671 2.5H9.50004V5.43133C9.50004 5.70733 9.72404 5.93133 10 5.93133H12.1667V12.6667C12.1667 13.1267 11.7934 13.5 11.3334 13.5H4.66671C4.20671 13.5 3.83337 13.1267 3.83337 12.6667V3.33333Z" />
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_151_281">
|
||||
<rect width="16" height="16" fill="currentColor"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_151_287)">
|
||||
<path fill="currentColor" stroke="currentColor" stroke-width="0.2" d="M13.8087 5.41874L13.6447 5.79607C13.6191 5.85751 13.5758 5.91 13.5204 5.94692C13.465 5.98384 13.4 6.00354 13.3334 6.00354C13.2668 6.00354 13.2017 5.98384 13.1463 5.94692C13.0909 5.91 13.0477 5.85751 13.022 5.79607L12.858 5.41874C12.5697 4.75122 12.0416 4.21602 11.378 3.91874L10.872 3.69274C10.8107 3.6645 10.7586 3.61925 10.7222 3.56236C10.6857 3.50546 10.6664 3.43931 10.6664 3.37174C10.6664 3.30417 10.6857 3.23801 10.7222 3.18112C10.7586 3.12422 10.8107 3.07897 10.872 3.05074L11.35 2.83807C12.0303 2.53232 12.5674 1.97732 12.8507 1.2874L13.0194 0.88007C13.0441 0.816972 13.0874 0.762802 13.1434 0.72462C13.1994 0.686438 13.2656 0.666016 13.3334 0.666016C13.4012 0.666016 13.4674 0.686438 13.5234 0.72462C13.5794 0.762802 13.6226 0.816972 13.6474 0.88007L13.816 1.28674C14.099 1.97679 14.6359 2.53202 15.316 2.83807L15.7947 3.0514C15.8559 3.07972 15.9077 3.12496 15.944 3.18179C15.9804 3.23861 15.9997 3.30463 15.9997 3.37207C15.9997 3.43951 15.9804 3.50553 15.944 3.56235C15.9077 3.61917 15.8559 3.66442 15.7947 3.69274L15.288 3.91807C14.6246 4.21565 14.0968 4.75109 13.8087 5.41874ZM6.66671 2.00007H9.33337V3.3334H6.66671C5.60584 3.3334 4.58843 3.75483 3.83828 4.50498C3.08813 5.25512 2.66671 6.27254 2.66671 7.3334C2.66671 9.74007 4.30804 11.3107 8.00004 12.9867V11.3334H9.33337C10.3942 11.3334 11.4117 10.912 12.1618 10.1618C12.9119 9.41168 13.3334 8.39427 13.3334 7.3334H14.6667C14.6667 8.74789 14.1048 10.1044 13.1046 11.1046C12.1044 12.1048 10.7479 12.6667 9.33337 12.6667V15.0001C6.00004 13.6667 1.33337 11.6667 1.33337 7.3334C1.33337 5.91892 1.89528 4.56236 2.89547 3.56217C3.89567 2.56197 5.25222 2.00007 6.66671 2.00007Z"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_151_287">
|
||||
<rect width="16" height="16" fill="currentColor"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_151_292)">
|
||||
<path fill="none" stroke="currentColor" stroke-width="0.2" d="M14.3333 5.29864V10.7013C14.3328 11.0668 14.2604 11.4286 14.1189 11.7656C13.9775 12.1027 13.7705 12.4083 13.51 12.6646C12.982 13.1846 12.2666 13.478 11.52 13.478H4.48063C3.7362 13.4788 3.02136 13.1866 2.49063 12.6646C1.96329 12.144 1.66663 11.438 1.66663 10.7013V5.29864C1.66704 4.93317 1.74003 4.57143 1.88137 4.23439C2.0227 3.89736 2.22957 3.59175 2.48996 3.33531C3.02069 2.8133 3.73554 2.52113 4.47996 2.52197H11.5193C12.2637 2.52113 12.9786 2.8133 13.5093 3.33531C13.7698 3.5917 13.9768 3.89728 14.1182 4.23432C14.2597 4.57135 14.3328 4.93313 14.3333 5.29864Z" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path fill="none" stroke="currentColor" stroke-width="0.2" d="M7.75534 10.6454C7.75534 10.1041 7.53934 9.5854 7.15401 9.20207C6.76801 8.81817 6.24574 8.60267 5.70134 8.60267C5.15694 8.60267 4.63467 8.81817 4.24867 9.20207C4.05815 9.39107 3.90693 9.6159 3.80372 9.86362C3.70052 10.1113 3.64737 10.377 3.64734 10.6454M9.71201 5.94607H12.108M9.71201 8.00007H10.7387M9.71201 10.0541H12.108M5.70134 8.0934C6.06451 8.0934 6.4128 7.94914 6.6696 7.69234C6.9264 7.43554 7.07067 7.08724 7.07067 6.72407C7.07067 6.3609 6.9264 6.01261 6.6696 5.75581C6.4128 5.49901 6.06451 5.35474 5.70134 5.35474C5.33817 5.35474 4.98987 5.49901 4.73307 5.75581C4.47627 6.01261 4.33201 6.3609 4.33201 6.72407C4.33201 7.08724 4.47627 7.43554 4.73307 7.69234C4.98987 7.94914 5.33817 8.0934 5.70134 8.0934Z" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_151_292">
|
||||
<rect width="16" height="16" fill="currentColor"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_151_299)">
|
||||
<path fill="currentColor" stroke="currentColor" stroke-width="0.2" d="M6.07128 3.63192C6.46995 2.46525 8.08195 2.42992 8.55461 3.52592L8.59461 3.63259L9.13262 5.20592C9.25591 5.56674 9.45515 5.89693 9.7169 6.1742C9.97865 6.45148 10.2968 6.66939 10.6499 6.81325L10.7946 6.86725L12.3679 7.40459C13.5346 7.80325 13.5699 9.41525 12.4746 9.88792L12.3679 9.92792L10.7946 10.4659C10.4337 10.5891 10.1034 10.7883 9.82596 11.0501C9.54856 11.3118 9.33055 11.6301 9.18661 11.9833L9.13262 12.1273L8.59528 13.7013C8.19661 14.8679 6.58461 14.9033 6.11261 13.8079L6.07128 13.7013L5.53395 12.1279C5.41073 11.767 5.21153 11.4367 4.94978 11.1593C4.68802 10.8819 4.36981 10.6638 4.01661 10.5199L3.87261 10.4659L2.29928 9.92859C1.13195 9.52992 1.09661 7.91792 2.19261 7.44592L2.29928 7.40459L3.87261 6.86725C4.23344 6.74396 4.56362 6.54471 4.8409 6.28297C5.11817 6.02122 5.33609 5.70305 5.47995 5.34992L5.53395 5.20592L6.07128 3.63192ZM7.33328 4.06259L6.79595 5.63592C6.60821 6.18612 6.30279 6.68879 5.90097 7.10892C5.49915 7.52904 5.01058 7.85654 4.46928 8.06859L4.30261 8.12925L2.72928 8.66659L4.30261 9.20392C4.85282 9.39166 5.35548 9.69707 5.77561 10.0989C6.19574 10.5007 6.52323 10.9893 6.73528 11.5306L6.79595 11.6973L7.33328 13.2706L7.87061 11.6973C8.05835 11.147 8.36377 10.6444 8.76559 10.2243C9.16741 9.80413 9.65598 9.47664 10.1973 9.26459L10.3639 9.20459L11.9373 8.66659L10.3639 8.12925C9.81375 7.94151 9.31108 7.6361 8.89095 7.23428C8.47082 6.83245 8.14333 6.34388 7.93128 5.80259L7.87128 5.63592L7.33328 4.06259ZM12.6666 1.33325C12.7913 1.33325 12.9136 1.36824 13.0194 1.43423C13.1252 1.50023 13.2104 1.59459 13.2653 1.70659L13.2973 1.78459L13.5306 2.46859L14.2153 2.70192C14.3403 2.74438 14.4498 2.823 14.5301 2.9278C14.6104 3.0326 14.6577 3.15886 14.6661 3.2906C14.6746 3.42233 14.6437 3.55361 14.5774 3.66778C14.5112 3.78195 14.4125 3.87388 14.2939 3.93192L14.2153 3.96392L13.5313 4.19725L13.2979 4.88192C13.2554 5.00687 13.1767 5.11638 13.0719 5.19658C12.9671 5.27678 12.8408 5.32405 12.709 5.3324C12.5773 5.34075 12.4461 5.30981 12.3319 5.2435C12.2178 5.17719 12.1259 5.0785 12.0679 4.95992L12.0359 4.88192L11.8026 4.19792L11.1179 3.96459C10.993 3.92212 10.8834 3.84351 10.8031 3.73871C10.7229 3.63391 10.6755 3.50764 10.6671 3.37591C10.6587 3.24417 10.6895 3.1129 10.7558 2.99873C10.8221 2.88456 10.9207 2.79263 11.0393 2.73459L11.1179 2.70259L11.8019 2.46925L12.0353 1.78459C12.0802 1.65287 12.1653 1.53852 12.2785 1.45758C12.3917 1.37664 12.5274 1.33317 12.6666 1.33325Z"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_151_299">
|
||||
<rect width="16" height="16" fill="currentColor"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<!-- 底部菜单 -->
|
||||
<div class="side-nav__footer">
|
||||
<div class="side-nav__item" v-for="item in footerMenus" :key="item.label" @click="item.action?.()">
|
||||
<img class="side-nav__item-icon" :src="item.iconImg" :alt="item.label" />
|
||||
<span class="side-nav__item-label">{{ item.label }}</span>
|
||||
<span v-if="item.badge" class="side-nav__badge">{{ item.badge }}</span>
|
||||
</div>
|
||||
@@ -150,6 +211,8 @@ import navMessageIcon from '@/assets/images/nav/nav-message-icon.png'
|
||||
import navSettingIcon from '@/assets/images/nav/nav-setting-icon.png'
|
||||
import navFeedbackIcon from '@/assets/images/nav/nav-feedback-icon.png'
|
||||
import navShareIcon from '@/assets/images/nav/nav-share-icon.png'
|
||||
// 导师页图标(暂用 profile 图标占位,有设计稿后替换为专属图标)
|
||||
import navMentorIcon from '@/assets/images/nav/nav-profile-icon.png'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
@@ -168,9 +231,8 @@ interface MenuItem {
|
||||
}
|
||||
|
||||
/**
|
||||
* 图标映射表
|
||||
* 后端 meta.icon 返回的是字符串 key,这里映射到实际的图片资源
|
||||
* 【新增图标时】在这里加一条即可
|
||||
* 图标映射表(兼容旧版字符串 key,新版后端直接返回 SVG 字符串)
|
||||
* 如果 meta.icon 是一个已知的 key,则使用对应图片;否则直接当作 SVG 字符串使用
|
||||
*/
|
||||
const iconMap: Record<string, string> = {
|
||||
'nav-jobs-icon': navJobsIcon,
|
||||
@@ -178,6 +240,20 @@ const iconMap: Record<string, string> = {
|
||||
'nav-profile-icon': navProfileIcon,
|
||||
'nav-agent-icon': navAgentIcon,
|
||||
'nav-setting-icon': navSettingIcon,
|
||||
'nav-mentor-icon': navMentorIcon,
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析图标:如果是已知的 key 则返回 img 标签,如果是 SVG 字符串则直接返回
|
||||
*/
|
||||
function resolveIcon(iconValue: string | undefined): string {
|
||||
if (!iconValue) return ''
|
||||
// 如果在映射表中找到了,说明是旧版 key,包装成 img 标签
|
||||
if (iconMap[iconValue]) {
|
||||
return `<img src="${iconMap[iconValue]}" alt="" />`
|
||||
}
|
||||
// 否则认为是 SVG 字符串,直接返回
|
||||
return iconValue
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -186,7 +262,7 @@ const iconMap: Record<string, string> = {
|
||||
* sortOrder 设为 0 确保在未获取到后端数据时也能正常显示
|
||||
*/
|
||||
const staticMenus: MenuItem[] = [
|
||||
{ name: 'Jobs', path: '/jobs', iconImg: navJobsIcon, label: '职位', sortOrder: 0, accessible: true },
|
||||
{ name: 'Jobs', path: '/jobs', iconImg: `<img src="${navJobsIcon}" alt="" />`, label: '职位', sortOrder: 0, accessible: true },
|
||||
]
|
||||
|
||||
/**
|
||||
@@ -201,7 +277,7 @@ const dynamicMenuItems = computed<MenuItem[]>(() => {
|
||||
.map((item: any) => ({
|
||||
name: item.name,
|
||||
path: item.path,
|
||||
iconImg: iconMap[item.meta?.icon] || '',
|
||||
iconImg: resolveIcon(item.meta?.icon),
|
||||
label: item.meta?.label || item.name,
|
||||
badge: item.meta?.badge,
|
||||
sortOrder: item.sortOrder ?? 99,
|
||||
@@ -405,19 +481,19 @@ const settingsMenu = computed(() => {
|
||||
const item = store.state.dynamicMenus.find((m: any) => m.meta?.position === 'footer')
|
||||
if (item) {
|
||||
return {
|
||||
iconImg: iconMap[item.meta?.icon] || navSettingIcon,
|
||||
iconImg: resolveIcon(item.meta?.icon) || `<img src="${navSettingIcon}" alt="" />`,
|
||||
label: item.meta?.label || '设置',
|
||||
path: item.path,
|
||||
}
|
||||
}
|
||||
return { iconImg: navSettingIcon, label: '设置', path: '/settings' }
|
||||
return { iconImg: `<img src="${navSettingIcon}" alt="" />`, label: '设置', path: '/settings' }
|
||||
})
|
||||
|
||||
const footerMenus = computed(() => [
|
||||
{ iconImg: navShareIcon, label: '分享送会员', action: () => { showShareDialog.value = true } },
|
||||
{ iconImg: navMessageIcon, label: '消息通知', badge: unreadCount.value > 0 ? 'NEW' : '', action: () => { showMessageDialog.value = true } },
|
||||
{ iconImg: `<img src="${navShareIcon}" alt="" />`, label: '分享送会员', action: () => { showShareDialog.value = true } },
|
||||
{ iconImg: `<img src="${navMessageIcon}" alt="" />`, label: '消息通知', badge: unreadCount.value > 0 ? 'NEW' : '', action: () => { showMessageDialog.value = true } },
|
||||
{ iconImg: settingsMenu.value.iconImg, label: settingsMenu.value.label, action: () => { handleSettingsNav() } },
|
||||
{ iconImg: navFeedbackIcon, label: '反馈', action: () => { showFeedbackDialog.value = true } },
|
||||
{ iconImg: `<img src="${navFeedbackIcon}" alt="" />`, label: '反馈', action: () => { showFeedbackDialog.value = true } },
|
||||
])
|
||||
|
||||
/**
|
||||
|
||||
@@ -14,6 +14,7 @@ const componentMap: Record<string, () => Promise<any>> = {
|
||||
'Agent': () => import('@/views/Agent.vue'),
|
||||
'Profile': () => import('@/views/Profile.vue'),
|
||||
'Resume': () => import('@/views/Resume.vue'),
|
||||
'Mentor': () => import('@/views/Mentor.vue'),
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -22,7 +23,7 @@ const componentMap: Record<string, () => Promise<any>> = {
|
||||
* 如果后端返回了一个 component 字符串在映射表里找不到,会跳过该条路由并打印警告
|
||||
* 已在静态路由中注册的路径(如 /jobs)不再重复注册
|
||||
*/
|
||||
const STATIC_PATHS = ['/', '/jobs', '/resume/:id', '/jobs/:id']
|
||||
const STATIC_PATHS = ['/', '/jobs', '/resume/:id', '/jobs/:id', '/mentor/:id']
|
||||
|
||||
export function buildDynamicRoutes(menus: MenuItemRaw[]): RouteRecordRaw[] {
|
||||
const routes: RouteRecordRaw[] = []
|
||||
|
||||
@@ -26,6 +26,11 @@ const staticRoutes: RouteRecordRaw[] = [
|
||||
name: 'JobDetail',
|
||||
component: () => import('@/views/JobDetail.vue'),
|
||||
},
|
||||
{
|
||||
path: '/mentor/:id',
|
||||
name: 'MentorDetail',
|
||||
component: () => import('@/views/MentorDetail.vue'),
|
||||
},
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
|
||||
@@ -41,6 +41,16 @@ export interface JobListCache {
|
||||
scrollTop: number
|
||||
}
|
||||
|
||||
/** 导师列表页缓存数据(从详情页返回时恢复用) */
|
||||
export interface MentorListCache {
|
||||
/** 缓存的导师列表 */
|
||||
list: any[]
|
||||
/** 当前已加载到的页码 */
|
||||
pageNum: number
|
||||
/** 列表滚动位置 */
|
||||
scrollTop: number
|
||||
}
|
||||
|
||||
export interface RootState {
|
||||
appName: string
|
||||
showLogin: boolean
|
||||
@@ -95,6 +105,12 @@ export interface RootState {
|
||||
*/
|
||||
jobListCache: JobListCache | null
|
||||
|
||||
/**
|
||||
* 导师列表页缓存 — 从详情页返回时恢复列表和滚动位置
|
||||
* 为 null 表示没有缓存,需要重新请求
|
||||
*/
|
||||
mentorListCache: MentorListCache | null
|
||||
|
||||
/**
|
||||
* 求职意向数据 — JobGoalDialog 和 Jobs 页面共享
|
||||
* 由 loadJobIntention action 从接口加载,saveJobIntention action 保存后更新
|
||||
@@ -133,6 +149,7 @@ export default createStore<RootState>({
|
||||
jobCategories: [],
|
||||
regions: [],
|
||||
jobListCache: null,
|
||||
mentorListCache: null,
|
||||
jobIntention: {
|
||||
categoryIds: [],
|
||||
regionCodes: [],
|
||||
@@ -187,6 +204,9 @@ export default createStore<RootState>({
|
||||
SET_JOB_LIST_CACHE(state, cache: JobListCache | null) {
|
||||
state.jobListCache = cache
|
||||
},
|
||||
SET_MENTOR_LIST_CACHE(state, cache: MentorListCache | null) {
|
||||
state.mentorListCache = cache
|
||||
},
|
||||
SET_JOB_INTENTION(state, data: JobIntention) {
|
||||
state.jobIntention = {
|
||||
categoryIds: data.categoryIds ?? [],
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* 图片地址处理工具
|
||||
* 将后端返回的内部文件路径转为 OSS 公开访问地址
|
||||
*/
|
||||
|
||||
/** OSS 基础地址 */
|
||||
const OSS_BASE = 'https://jsxq-service-read.oss-cn-shenzhen.aliyuncs.com/uploads/'
|
||||
|
||||
/** 后端返回的内部路径前缀 */
|
||||
const INTERNAL_PREFIX = '/admin/sys-file/jsxq-service-read/uploads/'
|
||||
|
||||
/**
|
||||
* 将后端图片路径转为 OSS 公开地址
|
||||
* - 空值安全:传入 null / undefined / 空字符串 返回空字符串
|
||||
* - 如果已经是完整 http(s) 链接则直接返回,不做二次拼接
|
||||
* - 如果是内部路径则替换为 OSS 地址
|
||||
*/
|
||||
export function toOssUrl(path: string | null | undefined): string {
|
||||
if (!path) return ''
|
||||
// 已经是完整链接,直接返回
|
||||
if (path.startsWith('http://') || path.startsWith('https://')) return path
|
||||
// 内部路径前缀替换
|
||||
if (path.startsWith(INTERNAL_PREFIX)) {
|
||||
const filename = path.slice(INTERNAL_PREFIX.length)
|
||||
return OSS_BASE + filename
|
||||
}
|
||||
// 兜底:可能只是文件名,拼上 OSS 基础地址
|
||||
return OSS_BASE + path
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import axios from 'axios'
|
||||
import type { AxiosResponse } from 'axios'
|
||||
|
||||
/**
|
||||
* 自定义 JSON 解析:将超过安全整数范围的数字转为字符串,避免精度丢失
|
||||
* 原理:用正则把 JSON 中超长数字(≥16位)加上引号变成字符串再交给 JSON.parse
|
||||
*/
|
||||
function parseBigIntJson(text: string): any {
|
||||
// 匹配 JSON 中独立的长数字(非字符串内的),将 ≥16 位整数用引号包裹
|
||||
const processed = text.replace(/:\s*(\d{16,})/g, ':"$1"')
|
||||
return JSON.parse(processed)
|
||||
}
|
||||
|
||||
/**
|
||||
* 见识星球专用 axios 实例
|
||||
* 开发环境:走 Vite proxy(/jianshi-api → https://api.jianshixingqiu.com)解决跨域
|
||||
* 生产环境:直接请求 https://api.jianshixingqiu.com
|
||||
* 接口完全公开,不需要 token 和登录鉴权
|
||||
*/
|
||||
const jianshiRequest = axios.create({
|
||||
baseURL: import.meta.env.VITE_JIANSHI_BASE_URL as string,
|
||||
timeout: 15000,
|
||||
// 见识星球接口固定请求头
|
||||
headers: {
|
||||
Authorization: 'Bearer f69b5086-97b0-4e5e-b172-453e444d448c',
|
||||
'TENANT-ID': '1',
|
||||
'CLIENT-TOC': 'Y',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
// 自定义响应数据转换:防止大数 id 精度丢失
|
||||
transformResponse: [(data: string) => {
|
||||
if (!data) return data
|
||||
try {
|
||||
return parseBigIntJson(data)
|
||||
} catch {
|
||||
return data
|
||||
}
|
||||
}],
|
||||
})
|
||||
|
||||
/**
|
||||
* 响应拦截器
|
||||
*/
|
||||
jianshiRequest.interceptors.response.use(
|
||||
(response: AxiosResponse) => {
|
||||
return response.data
|
||||
},
|
||||
(error) => {
|
||||
console.error('[见识星球接口错误]', error)
|
||||
return Promise.reject(error)
|
||||
},
|
||||
)
|
||||
|
||||
export default jianshiRequest
|
||||
+69
-84
@@ -121,108 +121,81 @@
|
||||
@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"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="jobs-page__job-info">
|
||||
<!-- 左栏:职位信息 -->
|
||||
<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__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"/>
|
||||
<!-- 收藏红心 -->
|
||||
<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>
|
||||
</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"/>
|
||||
</svg>
|
||||
{{ (job as any).tip }}
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<!-- 底部操作栏 -->
|
||||
<div class="jobs-page__job-actions pt20">
|
||||
<div class="dflex fgrow2 aliite-c flex-warp" >
|
||||
<!-- 标签 -->
|
||||
<div class="jobs-page__job-tags mt10">
|
||||
<!-- 职位标签 -->
|
||||
<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="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"/>
|
||||
<!-- 底部:地区 + 学历 + 招聘分类 -->
|
||||
<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>
|
||||
</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>
|
||||
{{ 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>
|
||||
</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>
|
||||
|
||||
<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 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>
|
||||
<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>
|
||||
<!-- 右侧:匹配度 -->
|
||||
<!-- 最右侧:匹配度 -->
|
||||
<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" stroke="#9FD051" fill="none" opacity="0.3"/>
|
||||
<circle cx="40" cy="40" r="34" stroke-width="5" fill="none"
|
||||
:stroke="job.matchScore >= 80 ? '#4FC2C9' : '#BFBFBF'"
|
||||
:stroke="matchRingColor(job.matchScore)"
|
||||
stroke-linecap="round"
|
||||
:stroke-dasharray="2 * Math.PI * 34"
|
||||
:stroke-dashoffset="2 * Math.PI * 34 * (1 - job.matchScore / 100)"
|
||||
@@ -231,7 +204,6 @@
|
||||
</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>
|
||||
@@ -243,6 +215,16 @@
|
||||
</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>
|
||||
<!-- 首次加载中提示 -->
|
||||
<div v-if="loading" class="jobs-page__loading-box">
|
||||
@@ -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>
|
||||
+9
-4
@@ -27,13 +27,18 @@ export default defineConfig({
|
||||
host: '0.0.0.0',
|
||||
proxy: {
|
||||
'/api': {
|
||||
// target: 'http://192.168.31.133:8080',
|
||||
target: 'http://127.0.0.1:8080',
|
||||
target: 'https://test.offerpai.com.cn/',
|
||||
// target: 'http://127.0.0.1:8080',
|
||||
changeOrigin: true,
|
||||
},
|
||||
'/jianshi-api': {
|
||||
target: 'https://www.jianshixingqiu.com',
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/jianshi-api/, '/proxy-api'),
|
||||
},
|
||||
'/ai-api': {
|
||||
// target: 'http://192.168.31.133:8000',
|
||||
target: 'http://127.0.0.1:8000',
|
||||
target: 'https://test.offerpai.com.cn/',
|
||||
// target: 'http://127.0.0.1:8000',
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/ai-api/, ''),
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user