校招公告,未登陆状态,免登录岗位列表接口
This commit is contained in:
Vendored
+1
@@ -48,6 +48,7 @@ declare module 'vue' {
|
||||
JobResumeCustomDialog: typeof import('./src/components/JobResumeCustomDialog.vue')['default']
|
||||
JobResumeCustomEditPanel: typeof import('./src/components/JobResumeCustomEditPanel.vue')['default']
|
||||
JobResumeTemplate: typeof import('./src/components/JobResumeTemplate.vue')['default']
|
||||
JobStatsOverview: typeof import('./src/components/JobStatsOverview.vue')['default']
|
||||
LoginDialog: typeof import('./src/components/LoginDialog.vue')['default']
|
||||
MemberAccessDialog: typeof import('./src/components/MemberAccessDialog.vue')['default']
|
||||
MemberDialog: typeof import('./src/components/MemberDialog.vue')['default']
|
||||
|
||||
+4
-1
@@ -1,12 +1,15 @@
|
||||
<template>
|
||||
<el-config-provider :locale="zhCn">
|
||||
<RouterView />
|
||||
<!-- 全局登录弹窗 -->
|
||||
<LoginDialog />
|
||||
</el-config-provider>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { RouterView } from 'vue-router'
|
||||
import zhCn from 'element-plus/es/locale/lang/zh-cn'
|
||||
import LoginDialog from '@/components/LoginDialog.vue'
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@@ -15,4 +18,4 @@ import zhCn from 'element-plus/es/locale/lang/zh-cn'
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
import request from '@/utils/request'
|
||||
import type { ApiResult } from '@/api/auth'
|
||||
|
||||
// ==================== 校招公告列表接口 ====================
|
||||
|
||||
/** Instant 时间结构(Java Instant 序列化格式) */
|
||||
export interface InstantTime {
|
||||
/** 从 1970-01-01T00:00:00Z 起的秒数 */
|
||||
seconds: number
|
||||
/** 纳秒偏移(0 ~ 999999999) */
|
||||
nanos?: number
|
||||
}
|
||||
|
||||
/** 校招公告列表请求参数 */
|
||||
export interface AnnouncementListParams {
|
||||
/** 当前页码,从1开始 */
|
||||
pageNum?: number
|
||||
/** 每页条数 */
|
||||
pageSize?: number
|
||||
/** 关键字 */
|
||||
keyword?: string
|
||||
/** 发布时间起(不含) */
|
||||
publishTimeStart?: InstantTime | null
|
||||
/** 发布时间止(含) */
|
||||
publishTimeEnd?: InstantTime | null
|
||||
/** 投递截止时间起(不含) */
|
||||
applyEndTimeStart?: InstantTime | null
|
||||
/** 投递截止时间止(含) */
|
||||
applyEndTimeEnd?: InstantTime | null
|
||||
/** 公司类型列表 */
|
||||
companyTypes?: string[]
|
||||
/** 学历要求列表 */
|
||||
educationNames?: string[]
|
||||
/** 招聘届数列表 */
|
||||
recruitYears?: number[]
|
||||
/** 批次列表 */
|
||||
batchNames?: string[]
|
||||
/** 岗位大类列表 */
|
||||
categoryNames?: string[]
|
||||
/** 热招城市列表 */
|
||||
cityNames?: string[]
|
||||
/** 行业列表 */
|
||||
industryNames?: string[]
|
||||
/** 标签列表 */
|
||||
tagNames?: string[]
|
||||
}
|
||||
|
||||
/** 校招公告列表项 */
|
||||
export interface AnnouncementItem {
|
||||
/** 公告ID */
|
||||
id: number
|
||||
/** 公司ID */
|
||||
companyId: number
|
||||
/** 公司名称 */
|
||||
companyName: string
|
||||
/** 公司logo */
|
||||
logoUrl: string
|
||||
/** 公告标题 */
|
||||
title: string
|
||||
/** 专业要求 */
|
||||
majorRequire: string
|
||||
/** 发布时间(毫秒时间戳) */
|
||||
publishTime: number | null
|
||||
/** 投递结束时间 */
|
||||
applyEndTime: InstantTime | null
|
||||
/** 投递截止描述 */
|
||||
applyEndDesc: string
|
||||
/** 行业列表 */
|
||||
industryNames: string[]
|
||||
/** 岗位大类列表 */
|
||||
categoryNames: string[]
|
||||
/** 热招城市列表 */
|
||||
cityNames: string[]
|
||||
/** 学历要求列表 */
|
||||
educationNames: string[]
|
||||
/** 招聘届数列表 */
|
||||
recruitYears: number[]
|
||||
/** 批次列表 */
|
||||
batchNames: string[]
|
||||
}
|
||||
|
||||
/** 校招公告分页数据 */
|
||||
export interface AnnouncementPageData {
|
||||
/** 当前页码 */
|
||||
pageNum: number
|
||||
/** 每页条数 */
|
||||
pageSize: number
|
||||
/** 总记录数 */
|
||||
total: number
|
||||
/** 数据列表 */
|
||||
list: AnnouncementItem[]
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取校招公告分页列表
|
||||
* POST /public/recruitAnnouncement/page
|
||||
*/
|
||||
export function fetchAnnouncementList(params: AnnouncementListParams = {}) {
|
||||
return request.post<any, ApiResult<AnnouncementPageData>>('/public/recruitAnnouncement/page', {
|
||||
pageNum: params.pageNum ?? 1,
|
||||
pageSize: params.pageSize ?? 10,
|
||||
...params,
|
||||
})
|
||||
}
|
||||
|
||||
// ==================== 校招公告详情接口 ====================
|
||||
|
||||
/** 校招公告详情数据 */
|
||||
export interface AnnouncementDetail {
|
||||
/** 公告ID */
|
||||
id: number
|
||||
/** 公司ID */
|
||||
companyId: number
|
||||
/** 公司名称 */
|
||||
companyName: string
|
||||
/** 公司logo */
|
||||
logoUrl: string
|
||||
/** 公司类型 */
|
||||
companyType: string
|
||||
/** 公告标题 */
|
||||
title: string
|
||||
/** 公司简介 */
|
||||
companyIntro: string
|
||||
/** 面向对象 */
|
||||
targetAudience: string
|
||||
/** 专业要求 */
|
||||
majorRequire: string
|
||||
/** 招聘岗位 */
|
||||
recruitPosition: string
|
||||
/** 备注 */
|
||||
remark: string
|
||||
/** 是否笔试 */
|
||||
writtenExam: string
|
||||
/** 投递开始时间 */
|
||||
applyStartTime: InstantTime | null
|
||||
/** 投递结束时间 */
|
||||
applyEndTime: InstantTime | null
|
||||
/** 投递截止描述 */
|
||||
applyEndDesc: string
|
||||
/** 专属内推码 */
|
||||
inviteCode: string
|
||||
/** 信息来源说明 */
|
||||
source: string
|
||||
/** 发布时间 */
|
||||
publishTime: InstantTime | null
|
||||
/** 清洗状态 */
|
||||
cleanStatus: number
|
||||
/** 状态 */
|
||||
status: number
|
||||
/** 创建时间 */
|
||||
createTime: InstantTime | null
|
||||
/** 更新时间 */
|
||||
updateTime: InstantTime | null
|
||||
/** 行业列表 */
|
||||
industryNames: string[]
|
||||
/** 岗位大类列表 */
|
||||
categoryNames: string[]
|
||||
/** 热招城市列表 */
|
||||
cityNames: string[]
|
||||
/** 学历要求列表 */
|
||||
educationNames: string[]
|
||||
/** 招聘届数列表 */
|
||||
recruitYears: number[]
|
||||
/** 批次列表 */
|
||||
batchNames: string[]
|
||||
/** 标签列表 */
|
||||
tagNames: string[]
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取校招公告详情
|
||||
* GET /public/recruitAnnouncement/detail
|
||||
*/
|
||||
export function fetchAnnouncementDetail(id: string) {
|
||||
return request.get<any, ApiResult<AnnouncementDetail>>('/public/recruitAnnouncement/detail', {
|
||||
params: { id },
|
||||
})
|
||||
}
|
||||
+1
-1
@@ -209,7 +209,7 @@ export function saveJobIntention(data: JobIntention) {
|
||||
* @param params 岗位列表查询参数
|
||||
*/
|
||||
export function fetchJobList(params: JobListParams = {}) {
|
||||
return request.post<any, ApiResult<JobPageData>>('/job/list', {
|
||||
return request.post<any, ApiResult<JobPageData>>('/public/job/list', {
|
||||
pageNum: params.pageNum ?? 1,
|
||||
pageSize: params.pageSize ?? 15,
|
||||
...params,
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 458 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 504 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 448 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 524 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 79 KiB |
@@ -450,6 +450,9 @@ body:not(#_) {
|
||||
.flex-warp{
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.flex0{
|
||||
flex: 0;
|
||||
}
|
||||
.flex1{
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,67 @@
|
||||
// Agent浏览器插件安装步骤指引组件样式
|
||||
@use '../variables' as *;
|
||||
|
||||
/* 组件最外层容器 */
|
||||
.agent-browser-guide {
|
||||
font-size: 0.14rem;
|
||||
}
|
||||
|
||||
/* Tab切换栏 */
|
||||
.agent-browser-guide__tabs {
|
||||
display: flex;
|
||||
border-bottom: 1px solid #F0F0F0;
|
||||
margin-bottom: 0.16rem;
|
||||
padding: 0 0.32rem;
|
||||
}
|
||||
|
||||
/* 单个Tab */
|
||||
.agent-browser-guide__tab {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.06rem;
|
||||
padding: 0.14rem 0.24rem;
|
||||
font-size: 0.14rem;
|
||||
font-weight: 450;
|
||||
color: $text-middle;
|
||||
cursor: pointer;
|
||||
border-bottom: 2px solid transparent;
|
||||
transition: color 0.2s, border-color 0.2s;
|
||||
flex: 1;
|
||||
justify-content: center;
|
||||
|
||||
&:hover {
|
||||
color: $accent;
|
||||
}
|
||||
|
||||
&--active {
|
||||
color: $accent;
|
||||
font-weight: 600;
|
||||
border-bottom-color: $accent;
|
||||
}
|
||||
|
||||
svg {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* 链接样式 */
|
||||
.agent-browser-guide__link {
|
||||
color: $accent;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
/* URL截断文本 */
|
||||
.agent-browser-guide__url-text {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: 2.8rem;
|
||||
}
|
||||
|
||||
/* 步骤内容区域 */
|
||||
.agent-browser-guide-drawer__body {
|
||||
flex: 1;
|
||||
@@ -74,6 +135,11 @@
|
||||
border-radius: 0.12rem;
|
||||
height: 0.44rem;
|
||||
margin-left: 0.4rem;
|
||||
|
||||
&--wide {
|
||||
width: auto;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.agent-browser-guide-drawer__download-name {
|
||||
@@ -96,6 +162,7 @@
|
||||
font-weight: 620;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
white-space: nowrap;
|
||||
|
||||
&:hover {
|
||||
background: $accent-hover;
|
||||
|
||||
@@ -1,86 +1,5 @@
|
||||
@use '../variables' as *;
|
||||
|
||||
/* ==================== 岗位统计卡片 ==================== */
|
||||
.job-stats-cards {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.16rem;
|
||||
margin-left: auto;
|
||||
filter: drop-shadow(0px 2px 10px rgba(0, 221, 179, 0.16));
|
||||
|
||||
/* 全网实时岗位卡片 */
|
||||
&__count-card {
|
||||
position: relative;
|
||||
height: 0.43rem;
|
||||
background: #0A171C;
|
||||
border: 1px solid #1C4C55;
|
||||
border-radius: 0.05rem;
|
||||
}
|
||||
|
||||
/* 状态指示圆点 */
|
||||
&__dot {
|
||||
//position: absolute;
|
||||
width: 0.08rem;
|
||||
height: 0.08rem;
|
||||
background: #2FE9F4;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0px 0px 4px rgba(47, 233, 244, 0.75);
|
||||
}
|
||||
|
||||
/* 岗位数字 */
|
||||
&__count-num {
|
||||
|
||||
font-family: 'MiSans';
|
||||
font-weight: 630;
|
||||
font-size: 0.16rem;
|
||||
line-height: 0.21rem;
|
||||
color: #F4FFFF;
|
||||
}
|
||||
|
||||
/* 岗位标签文字 */
|
||||
&__count-label {
|
||||
font-family: 'MiSans';
|
||||
font-weight: 330;
|
||||
font-size: 0.10rem;
|
||||
line-height: 0.13rem;
|
||||
color: #8AA8AF;
|
||||
}
|
||||
|
||||
/* 可投率卡片 */
|
||||
&__match-card {
|
||||
position: relative;
|
||||
width: 0.52rem;
|
||||
height: 0.42rem;
|
||||
background: #08161B;
|
||||
border: 1px solid #20626D;
|
||||
border-radius: 0.05rem;
|
||||
}
|
||||
|
||||
/* 可投率数字 */
|
||||
&__match-num {
|
||||
position: absolute;
|
||||
left: 0.08rem;
|
||||
top: 0.04rem;
|
||||
font-family: 'MiSans';
|
||||
font-weight: 630;
|
||||
font-size: 0.16rem;
|
||||
line-height: 0.21rem;
|
||||
color: #2FE9F4;
|
||||
}
|
||||
|
||||
/* 可投率标签文字 */
|
||||
&__match-label {
|
||||
position: absolute;
|
||||
left: 0.08rem;
|
||||
top: 0.245rem;
|
||||
font-family: 'MiSans';
|
||||
font-weight: 620;
|
||||
font-size: 0.10rem;
|
||||
line-height: 0.13rem;
|
||||
color: #8FB7BE;
|
||||
}
|
||||
}
|
||||
|
||||
.job-page-header {
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
@use '../variables' as *;
|
||||
|
||||
.job-stats-overview {
|
||||
/* 固定宽度卡片 */
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
width: 3.25rem;
|
||||
padding: 0.24rem 0.20rem;
|
||||
background: linear-gradient(180deg, #EDF9FA 0%, #FFFFFF 20.63%);
|
||||
border: 1px solid #F0F0F0;
|
||||
box-shadow: 0 0.02rem 0.06rem rgba(15, 23, 42, 0.025);
|
||||
border-radius: 0.12rem;
|
||||
font-size: 0.14rem;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: 1.8rem;
|
||||
|
||||
/* 右上角装饰图片 */
|
||||
&__icon {
|
||||
position: absolute;
|
||||
top: 0.10rem;
|
||||
right: 0.10rem;
|
||||
width: 0.86rem;
|
||||
height: 0.72rem;
|
||||
object-fit: contain;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* 内容纵向居中容器 */
|
||||
&__inner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
gap: 0.32rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* 标题区域 */
|
||||
&__header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 0.04rem;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-weight: 600;
|
||||
font-size: 0.18rem;
|
||||
line-height: 0.24rem;
|
||||
color: #1A1A1A;
|
||||
}
|
||||
|
||||
&__date {
|
||||
font-weight: 330;
|
||||
font-size: 0.14rem;
|
||||
line-height: 0.19rem;
|
||||
color: #64748B;
|
||||
}
|
||||
|
||||
/* 数据指标行 */
|
||||
&__metrics {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 0.20rem;
|
||||
}
|
||||
|
||||
/* 单个指标 */
|
||||
&__metric-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.04rem;
|
||||
}
|
||||
|
||||
&__metric-num {
|
||||
font-weight: 600;
|
||||
font-size: 0.30rem;
|
||||
line-height: 0.40rem;
|
||||
color: $accent;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&__metric-label {
|
||||
font-weight: 330;
|
||||
font-size: 0.14rem;
|
||||
line-height: 0.19rem;
|
||||
color: #6A7282;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* 竖线分隔 */
|
||||
&__divider {
|
||||
width: 0;
|
||||
height: 0.52rem;
|
||||
border-left: 1px solid #F0F0F0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,376 @@
|
||||
@use '../variables' as *;
|
||||
|
||||
/* 登录弹窗遮罩层 */
|
||||
.login-dialog-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: var(--app-height, 100vh);
|
||||
background: rgba(0, 0, 0, 0.45);
|
||||
z-index: 2050;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* 弹窗底座盒子 */
|
||||
.login-dialog-box {
|
||||
font-size: 0.14rem;
|
||||
position: relative;
|
||||
width: 5.6rem;
|
||||
min-height: 4.57rem;
|
||||
background: radial-gradient(58.14% 30.59% at 100% 0%, #D2F2F3 0%, #FFFFFF 100%);
|
||||
border: 1px solid #F0F0F0;
|
||||
border-radius: 0.32rem;
|
||||
padding: 0.48rem 0.6rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* 关闭按钮 */
|
||||
.login-dialog-box__close {
|
||||
position: absolute;
|
||||
top: 0.2rem;
|
||||
right: 0.24rem;
|
||||
width: 0.32rem;
|
||||
height: 0.32rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
border-radius: 50%;
|
||||
transition: background 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 0.2rem;
|
||||
height: 0.2rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Logo 行 */
|
||||
.login-dialog-box__logo-row {
|
||||
width: 100%;
|
||||
margin-bottom: 0.4rem;
|
||||
|
||||
img {
|
||||
width: 1.6rem;
|
||||
height: 0.4rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* 表单包裹 */
|
||||
.login-dialog-box__form-wrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.24rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* 标题 */
|
||||
.login-dialog-box__heading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.05rem;
|
||||
}
|
||||
|
||||
.login-dialog-box__title {
|
||||
font-weight: 700;
|
||||
font-size: 0.24rem;
|
||||
line-height: 0.22rem;
|
||||
color: #18181A;
|
||||
}
|
||||
|
||||
.login-dialog-box__subtitle {
|
||||
font-weight: 400;
|
||||
font-size: 0.14rem;
|
||||
margin-top: 0.08rem;
|
||||
margin-bottom: 0.08rem;
|
||||
color: #616367;
|
||||
|
||||
> span:nth-child(2) {
|
||||
color: $accent;
|
||||
}
|
||||
}
|
||||
|
||||
/* 手机号输入行 */
|
||||
.login-dialog-box__phone-row {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 0.56rem;
|
||||
background: #FFFFFF;
|
||||
border: 1px solid #C8C9CD;
|
||||
box-shadow: 0 0.08rem 0.20rem rgba(15, 83, 87, 0.05);
|
||||
border-radius: 0.12rem;
|
||||
}
|
||||
|
||||
.login-dialog-box__phone-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 0.18rem;
|
||||
height: 0.18rem;
|
||||
margin-left: 0.16rem;
|
||||
flex-shrink: 0;
|
||||
|
||||
svg {
|
||||
width: 0.18rem;
|
||||
height: 0.18rem;
|
||||
}
|
||||
}
|
||||
|
||||
.login-dialog-box__country-label {
|
||||
margin-left: 0.06rem;
|
||||
font-weight: 500;
|
||||
font-size: 0.14rem;
|
||||
line-height: 0.15rem;
|
||||
color: #202124;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.login-dialog-box__country-chevron {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 0.20rem;
|
||||
height: 0.20rem;
|
||||
flex-shrink: 0;
|
||||
cursor: pointer;
|
||||
|
||||
svg {
|
||||
width: 0.20rem;
|
||||
height: 0.20rem;
|
||||
}
|
||||
}
|
||||
|
||||
.login-dialog-box__input-divider {
|
||||
width: 1px;
|
||||
height: 0.30rem;
|
||||
background: #E7F2F3;
|
||||
margin: 0 0.16rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.login-dialog-box__phone-input {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
border: none;
|
||||
background: transparent;
|
||||
font-weight: 400;
|
||||
font-size: 0.14rem;
|
||||
line-height: 0.15rem;
|
||||
color: #202124;
|
||||
outline: none;
|
||||
|
||||
&::placeholder {
|
||||
color: #939599;
|
||||
}
|
||||
}
|
||||
|
||||
/* 发送验证码按钮 */
|
||||
.login-dialog-box__send-btn {
|
||||
width: 100%;
|
||||
height: 0.43rem;
|
||||
background: linear-gradient(90deg, #4FC2C9 0%, #42A8B3 100%);
|
||||
box-shadow: 0 0.16rem 0.30rem rgba(15, 83, 87, 0.15);
|
||||
border-radius: 0.12rem;
|
||||
border: none;
|
||||
font-weight: 600;
|
||||
font-size: 0.14rem;
|
||||
line-height: 0.15rem;
|
||||
color: #FFFFFF;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.2s;
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&:not(:disabled):hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
}
|
||||
|
||||
/* OTP 验证码输入 */
|
||||
.login-dialog-box__otp-wrap {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.1rem;
|
||||
width: 100%;
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
.login-dialog-box__otp-hidden-input {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 0;
|
||||
z-index: 2;
|
||||
font-size: 0.16rem;
|
||||
caret-color: transparent;
|
||||
}
|
||||
|
||||
.login-dialog-box__otp-box {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 0.56rem;
|
||||
height: 0.56rem;
|
||||
background: #FAFBFC;
|
||||
border: 1px solid #E2E8F0;
|
||||
border-radius: 0.12rem;
|
||||
transition: border-color 0.2s, background 0.2s;
|
||||
}
|
||||
|
||||
.login-dialog-box__otp-box--active {
|
||||
background: #F3FFFD;
|
||||
border: 1px solid $accent;
|
||||
}
|
||||
|
||||
.login-dialog-box__otp-box--filled {
|
||||
background: #F3FFFD;
|
||||
border: 2px solid $accent;
|
||||
}
|
||||
|
||||
.login-dialog-box__otp-digit {
|
||||
font-weight: 700;
|
||||
font-size: 0.24rem;
|
||||
line-height: 0.29rem;
|
||||
color: #132034;
|
||||
}
|
||||
|
||||
/* 闪烁光标 */
|
||||
.login-dialog-box__otp-cursor {
|
||||
display: inline-block;
|
||||
width: 2px;
|
||||
height: 0.28rem;
|
||||
background: $accent;
|
||||
border-radius: 1px;
|
||||
animation: login-dialog-otp-blink 1s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes login-dialog-otp-blink {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0; }
|
||||
}
|
||||
|
||||
/* 验证码状态提示 */
|
||||
.login-dialog-box__sms-status {
|
||||
width: 100%;
|
||||
font-size: 0.13rem;
|
||||
line-height: 0.16rem;
|
||||
color: #64748B;
|
||||
text-align: left;
|
||||
margin-top: -0.14rem;
|
||||
margin-bottom: -0.06rem;
|
||||
}
|
||||
|
||||
.login-dialog-box__sms-status--error {
|
||||
color: #EF4444;
|
||||
}
|
||||
|
||||
/* 状态按钮(步骤二) */
|
||||
.login-dialog-box__status-btn {
|
||||
width: 100%;
|
||||
height: 0.43rem;
|
||||
background: linear-gradient(90deg, $btn-dark 0%, $btn-dark-hover 100%);
|
||||
border-radius: 0.12rem;
|
||||
border: none;
|
||||
font-weight: 600;
|
||||
font-size: 0.14rem;
|
||||
color: #FFFFFF;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.08rem;
|
||||
transition: opacity 0.2s;
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.7;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&:not(:disabled):hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
}
|
||||
|
||||
/* 登录中转圈动画 */
|
||||
.login-dialog-box__spinner {
|
||||
display: inline-block;
|
||||
width: 0.18rem;
|
||||
height: 0.18rem;
|
||||
border: 2px solid rgba(255, 255, 255, 0.3);
|
||||
border-top-color: #FFFFFF;
|
||||
border-radius: 50%;
|
||||
animation: login-dialog-spin 0.7s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes login-dialog-spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* 协议勾选 */
|
||||
.login-dialog-box__agreement {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.06rem;
|
||||
}
|
||||
|
||||
.login-dialog-box__checkbox {
|
||||
box-sizing: border-box;
|
||||
width: 0.18rem;
|
||||
height: 0.18rem;
|
||||
background: #FFFFFF;
|
||||
border: 1.5px solid #CBD5E1;
|
||||
border-radius: 0.04rem;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.login-dialog-box__checkbox--checked {
|
||||
background: #4FC2C9;
|
||||
border-color: #4FC2C9;
|
||||
}
|
||||
|
||||
.login-dialog-box__check-icon {
|
||||
font-size: 0.11rem;
|
||||
color: #FFFFFF;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.login-dialog-box__agreement-text {
|
||||
font-weight: 400;
|
||||
font-size: 0.12rem;
|
||||
line-height: 0.15rem;
|
||||
color: #616367;
|
||||
}
|
||||
|
||||
.login-dialog-box__agreement-link {
|
||||
font-weight: 500;
|
||||
font-size: 0.12rem;
|
||||
line-height: 0.15rem;
|
||||
color: #42A8B3;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
@@ -349,4 +349,14 @@
|
||||
color: #64748B;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
// 个人概述文本
|
||||
&__summary-text {
|
||||
font-size: 0.14rem;
|
||||
line-height: 1.7;
|
||||
color: #374151;
|
||||
padding: 0 0.14rem 0.12rem;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,12 +12,15 @@
|
||||
@use './pages/login.scss';
|
||||
@use './pages/mentor.scss';
|
||||
@use './pages/mentor-detail.scss';
|
||||
@use './pages/announcement.scss';
|
||||
@use './pages/announcement-detail.scss';
|
||||
|
||||
// 组件样式
|
||||
@use './components/hello-world.scss';
|
||||
@use './components/side-nav.scss';
|
||||
@use './components/ai-chat.scss';
|
||||
@use './components/job-page-header.scss';
|
||||
@use './components/job-stats-overview.scss';
|
||||
@use './components/job-goal-dialog.scss';
|
||||
@use './components/settings-dialog.scss';
|
||||
@use './components/member-dialog.scss';
|
||||
@@ -45,6 +48,7 @@
|
||||
@use './components/step-progress-overlay.scss';
|
||||
@use './components/mentor-aside-panel.scss';
|
||||
@use './components/agent-browser-install-guide.scss';
|
||||
@use './components/login-dialog.scss';
|
||||
|
||||
// 全局样式(优先级最高)
|
||||
@use './auto.scss';
|
||||
|
||||
@@ -677,6 +677,78 @@
|
||||
margin-bottom: 0.16rem;
|
||||
}
|
||||
|
||||
// 第2步投递偏好开关容器
|
||||
&__step2-pref-switches {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.12rem;
|
||||
}
|
||||
|
||||
// 第2步开关项
|
||||
&__step2-switch-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.2rem 0.24rem;
|
||||
background: #F9FAFB;
|
||||
border-radius: 0.12rem;
|
||||
}
|
||||
|
||||
// 第2步开关文字区
|
||||
&__step2-switch-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.04rem;
|
||||
}
|
||||
|
||||
// 第2步开关标签
|
||||
&__step2-switch-label {
|
||||
font-size: 0.15rem;
|
||||
font-weight: 500;
|
||||
line-height: 0.21rem;
|
||||
color: #1A1A1A;
|
||||
}
|
||||
|
||||
// 第2步开关描述
|
||||
&__step2-switch-desc {
|
||||
font-size: 0.13rem;
|
||||
font-weight: 330;
|
||||
line-height: 0.18rem;
|
||||
color: $text-middle;
|
||||
}
|
||||
|
||||
// 第2步自定义开关
|
||||
&__step2-toggle {
|
||||
position: relative;
|
||||
width: 0.44rem;
|
||||
height: 0.24rem;
|
||||
background: #E5E7EB;
|
||||
border-radius: 0.12rem;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
flex-shrink: 0;
|
||||
|
||||
&--on {
|
||||
background: $accent;
|
||||
|
||||
.agent-page__step2-toggle-dot {
|
||||
left: 0.22rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 第2步开关圆点
|
||||
&__step2-toggle-dot {
|
||||
position: absolute;
|
||||
width: 0.18rem;
|
||||
height: 0.18rem;
|
||||
left: 0.04rem;
|
||||
top: 0.03rem;
|
||||
background: #FFFFFF;
|
||||
border-radius: 50%;
|
||||
transition: left 0.2s;
|
||||
}
|
||||
|
||||
// 第2步功能说明区域 — 全宽
|
||||
&__step2-faq {
|
||||
width: 100%;
|
||||
@@ -744,13 +816,14 @@
|
||||
padding: 0.12rem 0.16rem;
|
||||
min-width: 1.6rem;
|
||||
height: 0.43rem;
|
||||
background: #F3F4F6;
|
||||
background: #fff;
|
||||
border-radius: 0.12rem;
|
||||
cursor: pointer;
|
||||
box-sizing: border-box;
|
||||
user-select: none;
|
||||
transition: border-color 0.2s ease;
|
||||
width: 1.6rem;
|
||||
width: 100%;
|
||||
border: 1px solid #F0F0F0;
|
||||
|
||||
&:hover { border-color: darken($border-color, 10%); }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,532 @@
|
||||
@use '../variables' as *;
|
||||
|
||||
// ==================== 校招公告详情页 ====================
|
||||
|
||||
.announcement-detail {
|
||||
font-size: 0.14rem;
|
||||
width: 100%;
|
||||
height: var(--app-height, 100vh);
|
||||
overflow: hidden;
|
||||
min-width: 12.8rem;
|
||||
|
||||
// 主内容区(与 JobDetail 一致,通过 margin-left 避开侧边栏)
|
||||
&__content {
|
||||
margin-left: var(--nav-width, 2.2rem);
|
||||
flex: 1;
|
||||
height: var(--app-height, 100vh);
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
background: $bg-main;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-size: 0.14rem;
|
||||
}
|
||||
|
||||
// 页面标题
|
||||
&__page-title {
|
||||
font-size: 0.18rem;
|
||||
font-weight: 600;
|
||||
color: $text-dark;
|
||||
line-height: 0.24rem;
|
||||
}
|
||||
|
||||
&__page-subtitle {
|
||||
font-size: 0.12rem;
|
||||
color: $text-middle;
|
||||
line-height: 0.16rem;
|
||||
}
|
||||
|
||||
// 返回按钮行
|
||||
&__back-row {
|
||||
padding: 0 0.24rem;
|
||||
margin-bottom: 0.12rem;
|
||||
}
|
||||
|
||||
&__back-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.04rem;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 0.14rem;
|
||||
color: $text-middle;
|
||||
padding: 0;
|
||||
|
||||
&:hover {
|
||||
color: $accent;
|
||||
}
|
||||
}
|
||||
|
||||
&__back-icon {
|
||||
width: 0.16rem;
|
||||
height: 0.16rem;
|
||||
}
|
||||
|
||||
// 主体内容可滚动区域
|
||||
&__body {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 0 0.24rem 0.24rem;
|
||||
}
|
||||
|
||||
// 两列布局(公司卡片下方)
|
||||
&__layout {
|
||||
display: flex;
|
||||
gap: 0.12rem;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
// 左侧主详情
|
||||
&__left {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
background: #fff;
|
||||
padding: 0.24rem 0.32rem;
|
||||
border-radius: 0.08rem;
|
||||
}
|
||||
|
||||
// 右侧边栏
|
||||
&__right {
|
||||
width: 4.24rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 顶部公司信息卡片(独占一行) ====================
|
||||
|
||||
.announcement-detail__company-card {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.24rem 0.32rem;
|
||||
background: $bg-white;
|
||||
border: 1px solid #F0F0F0;
|
||||
box-shadow: 0px 1px 4px rgba(0, 0, 0, 0.04);
|
||||
border-radius: 0.08rem;
|
||||
margin-bottom: 0.12rem;
|
||||
}
|
||||
|
||||
.announcement-detail__company-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.16rem;
|
||||
}
|
||||
|
||||
.announcement-detail__company-logo {
|
||||
width: 0.66rem;
|
||||
height: 0.66rem;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
flex-shrink: 0;
|
||||
|
||||
&--default {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #EDF9FA;
|
||||
}
|
||||
}
|
||||
|
||||
.announcement-detail__company-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.12rem;
|
||||
}
|
||||
|
||||
.announcement-detail__company-name-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.16rem;
|
||||
}
|
||||
|
||||
.announcement-detail__company-name {
|
||||
font-size: 0.24rem;
|
||||
font-weight: 600;
|
||||
color: $text-dark;
|
||||
line-height: 0.32rem;
|
||||
}
|
||||
|
||||
.announcement-detail__company-tags {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.12rem;
|
||||
}
|
||||
|
||||
.announcement-detail__tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0.03rem 0.06rem;
|
||||
font-size: 0.12rem;
|
||||
line-height: 0.16rem;
|
||||
border-radius: 0.04rem;
|
||||
|
||||
&--type {
|
||||
background: #FEF3F2;
|
||||
color: #B42318;
|
||||
}
|
||||
|
||||
&--gray {
|
||||
background: #F3F4F6;
|
||||
color: $text-middle;
|
||||
}
|
||||
}
|
||||
|
||||
.announcement-detail__company-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.16rem;
|
||||
}
|
||||
|
||||
.announcement-detail__btn-outline {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0.08rem 0.16rem;
|
||||
font-size: 0.14rem;
|
||||
font-weight: 450;
|
||||
color: $accent;
|
||||
background: transparent;
|
||||
border: 1px solid $accent;
|
||||
border-radius: 0.08rem;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
|
||||
&:hover {
|
||||
background: #EDF9FA;
|
||||
}
|
||||
}
|
||||
|
||||
.announcement-detail__btn-primary {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0.08rem 0.16rem;
|
||||
font-size: 0.14rem;
|
||||
font-weight: 450;
|
||||
color: #FFFFFF;
|
||||
background: $accent;
|
||||
border: 1px solid $accent;
|
||||
border-radius: 0.08rem;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
|
||||
&:hover {
|
||||
background: $accent-hover;
|
||||
border-color: $accent-hover;
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 岗位速览卡片 ====================
|
||||
|
||||
.announcement-detail__overview-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0.16rem 0.24rem;
|
||||
gap: 0.16rem;
|
||||
background: linear-gradient(180deg, #EDF9FA 0%, #FFFFFF 20.63%);
|
||||
border: 1px solid #AEE6EA;
|
||||
border-radius: 0.08rem;
|
||||
margin-bottom: 0.24rem;
|
||||
}
|
||||
|
||||
.announcement-detail__overview-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.announcement-detail__overview-title-row {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: 0.12rem;
|
||||
}
|
||||
|
||||
.announcement-detail__overview-title {
|
||||
font-size: 0.16rem;
|
||||
font-weight: 600;
|
||||
color: $text-dark;
|
||||
line-height: 0.21rem;
|
||||
}
|
||||
|
||||
.announcement-detail__overview-date {
|
||||
font-size: 0.12rem;
|
||||
font-weight: 330;
|
||||
color: $text-middle;
|
||||
line-height: 0.16rem;
|
||||
}
|
||||
|
||||
.announcement-detail__overview-deadline {
|
||||
font-size: 0.14rem;
|
||||
font-weight: 380;
|
||||
color: $text-dark;
|
||||
line-height: 0.19rem;
|
||||
}
|
||||
|
||||
.announcement-detail__overview-body {
|
||||
display: flex;
|
||||
gap: 0.24rem;
|
||||
}
|
||||
|
||||
.announcement-detail__overview-fields {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
// 每行两个字段横排,align-items: stretch 保证同行等高
|
||||
.announcement-detail__overview-row {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
gap: 0.16rem;
|
||||
}
|
||||
|
||||
.announcement-detail__overview-direction {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.04rem;
|
||||
}
|
||||
|
||||
.announcement-detail__overview-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.04rem;
|
||||
}
|
||||
|
||||
.announcement-detail__overview-label {
|
||||
font-size: 0.12rem;
|
||||
font-weight: 330;
|
||||
color: $text-middle;
|
||||
line-height: 0.16rem;
|
||||
}
|
||||
|
||||
.announcement-detail__overview-value {
|
||||
font-size: 0.14rem;
|
||||
font-weight: 380;
|
||||
color: $text-dark;
|
||||
line-height: 0.19rem;
|
||||
}
|
||||
|
||||
// ==================== 公告标题 ====================
|
||||
|
||||
.announcement-detail__main-title {
|
||||
font-size: 0.24rem;
|
||||
font-weight: 600;
|
||||
color: $text-dark;
|
||||
line-height: 0.32rem;
|
||||
margin-bottom: 0.24rem;
|
||||
}
|
||||
|
||||
// ==================== 详情正文段落 ====================
|
||||
|
||||
.announcement-detail__section {
|
||||
margin-bottom: 0.16rem;
|
||||
}
|
||||
|
||||
.announcement-detail__section-title {
|
||||
font-size: 0.18rem;
|
||||
font-weight: 600;
|
||||
color: $accent;
|
||||
line-height: 0.24rem;
|
||||
margin-bottom: 0.16rem;
|
||||
}
|
||||
|
||||
.announcement-detail__section-content {
|
||||
font-size: 0.14rem;
|
||||
font-weight: 330;
|
||||
color: $text-dark;
|
||||
line-height: 0.19rem;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
// ==================== 右侧边栏卡片 ====================
|
||||
|
||||
.announcement-detail__aside-card {
|
||||
background: $bg-white;
|
||||
border: 1px solid #F0F0F0;
|
||||
box-shadow: 0px 1px 4px rgba(0, 0, 0, 0.04);
|
||||
border-radius: 0.08rem;
|
||||
padding: 0.24rem 0.32rem;
|
||||
margin-bottom: 0.12rem;
|
||||
}
|
||||
|
||||
.announcement-detail__aside-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 0.24rem;
|
||||
}
|
||||
|
||||
.announcement-detail__aside-title {
|
||||
font-size: 0.18rem;
|
||||
font-weight: 600;
|
||||
color: $accent;
|
||||
line-height: 0.24rem;
|
||||
}
|
||||
|
||||
.announcement-detail__aside-more {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.04rem;
|
||||
font-size: 0.14rem;
|
||||
font-weight: 450;
|
||||
color: $text-middle;
|
||||
cursor: pointer;
|
||||
|
||||
svg {
|
||||
width: 0.19rem;
|
||||
height: 0.19rem;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: $accent;
|
||||
}
|
||||
}
|
||||
|
||||
.announcement-detail__aside-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.16rem;
|
||||
}
|
||||
|
||||
// ==================== 企业其他岗位列表项 ====================
|
||||
|
||||
.announcement-detail__job-item {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
padding-bottom: 0.16rem;
|
||||
}
|
||||
|
||||
.announcement-detail__job-item-main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.announcement-detail__job-item-title {
|
||||
font-size: 0.16rem;
|
||||
font-weight: 600;
|
||||
color: $text-dark;
|
||||
line-height: 0.21rem;
|
||||
display: block;
|
||||
margin-bottom: 0.08rem;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 2.56rem;
|
||||
}
|
||||
|
||||
.announcement-detail__job-item-tags {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.08rem;
|
||||
}
|
||||
|
||||
.announcement-detail__job-item-score {
|
||||
text-align: right;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.announcement-detail__job-item-score-num {
|
||||
display: block;
|
||||
font-size: 0.22rem;
|
||||
font-weight: 600;
|
||||
line-height: 0.29rem;
|
||||
}
|
||||
|
||||
.announcement-detail__job-item-score-label {
|
||||
display: block;
|
||||
font-size: 0.12rem;
|
||||
font-weight: 450;
|
||||
line-height: 0.16rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.announcement-detail__job-divider {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 0;
|
||||
border-bottom: 1px solid #F0F0F0;
|
||||
}
|
||||
|
||||
// ==================== 通用灰色chip ====================
|
||||
|
||||
.announcement-detail__chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0.03rem 0.06rem;
|
||||
background: #F3F4F6;
|
||||
border-radius: 0.04rem;
|
||||
font-size: 0.12rem;
|
||||
font-weight: 380;
|
||||
color: $text-middle;
|
||||
line-height: 0.16rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
// ==================== 校招推荐列表项 ====================
|
||||
|
||||
.announcement-detail__rec-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.1rem;
|
||||
position: relative;
|
||||
padding-bottom: 0.16rem;
|
||||
}
|
||||
|
||||
.announcement-detail__rec-logo {
|
||||
width: 0.4rem;
|
||||
height: 0.4rem;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
flex-shrink: 0;
|
||||
|
||||
&--default {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #EDF9FA;
|
||||
}
|
||||
}
|
||||
|
||||
.announcement-detail__rec-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.04rem;
|
||||
}
|
||||
|
||||
.announcement-detail__rec-company {
|
||||
font-size: 0.16rem;
|
||||
font-weight: 600;
|
||||
color: #111827;
|
||||
line-height: 0.21rem;
|
||||
}
|
||||
|
||||
.announcement-detail__rec-positions {
|
||||
font-size: 0.14rem;
|
||||
font-weight: 330;
|
||||
color: $text-middle;
|
||||
line-height: 0.19rem;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.announcement-detail__rec-tags {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.04rem;
|
||||
}
|
||||
@@ -0,0 +1,555 @@
|
||||
// 校招公告页面样式
|
||||
@use '../variables' as *;
|
||||
|
||||
.announcement-page {
|
||||
display: flex;
|
||||
|
||||
&__content {
|
||||
margin-left: var(--nav-width, 2.2rem);
|
||||
flex: 1;
|
||||
padding-bottom: 0.18rem;
|
||||
height: var(--app-height, 100vh);
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
background: $bg-main;
|
||||
font-size: 0.14rem;
|
||||
}
|
||||
|
||||
// 页面标题
|
||||
&__title {
|
||||
font-size: 0.24rem;
|
||||
font-weight: 700;
|
||||
color: $text-dark;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
&__subtitle {
|
||||
font-size: 0.13rem;
|
||||
color: $text-light;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
// 筛选条件外层
|
||||
&__filters-bar {
|
||||
border-radius: 0.12rem;
|
||||
margin: 0 0.24rem 0.12rem 0.24rem;
|
||||
}
|
||||
|
||||
// 筛选面板 + 统计卡片横向排列
|
||||
&__filters-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
gap: 0.12rem;
|
||||
}
|
||||
|
||||
// 筛选面板
|
||||
&__filters-panel {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
background: $bg-white;
|
||||
border: 1px solid #F0F0F0;
|
||||
box-shadow: 0px 2px 6px rgba(15, 23, 42, 0.025);
|
||||
border-radius: 0.12rem;
|
||||
padding: 0.12rem 0.16rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.12rem;
|
||||
}
|
||||
|
||||
// 筛选行
|
||||
&__filter-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 0.08rem 0.16rem;
|
||||
background: $bg-white;
|
||||
}
|
||||
|
||||
&__filter-row-inner {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 0.16rem;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
// 筛选行标题
|
||||
&__filter-row-label {
|
||||
font-size: 0.12rem;
|
||||
line-height: 0.16rem;
|
||||
font-weight: 380;
|
||||
color: $text-middle;
|
||||
flex-shrink: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
// chip组外层
|
||||
&__filter-chips-wrap {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.08rem;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
||||
> .industry-selector,
|
||||
> .job-category-selector,
|
||||
> .region-selector {
|
||||
flex-shrink: 0;
|
||||
margin-left: auto;
|
||||
align-self: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
// chip组
|
||||
&__filter-chips {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
align-content: center;
|
||||
gap: 0.08rem;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
// 筛选chip
|
||||
&__filter-chip {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 0.04rem 0.08rem;
|
||||
background: #F3F4F6;
|
||||
border-radius: 0.04rem;
|
||||
font-size: 0.12rem;
|
||||
line-height: 0.16rem;
|
||||
font-weight: 330;
|
||||
color: $text-dark;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
user-select: none;
|
||||
transition: all 0.15s;
|
||||
border: 1px solid transparent;
|
||||
|
||||
&:hover {
|
||||
background: #E8F8F9;
|
||||
color: $accent;
|
||||
}
|
||||
|
||||
&--active {
|
||||
background: #F4FBFB;
|
||||
border-color: $accent;
|
||||
color: $accent;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
&--active-solid {
|
||||
background: $accent;
|
||||
color: #FFFFFF;
|
||||
font-weight: 600;
|
||||
border-color: $accent;
|
||||
}
|
||||
}
|
||||
|
||||
// 底部筛选行
|
||||
&__filter-row--bottom {}
|
||||
|
||||
// 下拉按钮组容器
|
||||
&__filter-dropdowns {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.08rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
// 下拉按钮项
|
||||
&__dropdown-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.02rem;
|
||||
padding: 0.04rem 0.08rem;
|
||||
border: 1px solid #F0F0F0;
|
||||
border-radius: 0.04rem;
|
||||
font-size: 0.12rem;
|
||||
line-height: 0.16rem;
|
||||
font-weight: 330;
|
||||
color: $text-dark;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
position: relative;
|
||||
user-select: none;
|
||||
transition: all 0.15s;
|
||||
|
||||
&:hover {
|
||||
border-color: $accent;
|
||||
color: $accent;
|
||||
}
|
||||
|
||||
.el-range-separator {
|
||||
padding-bottom: 0.03rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
&__dropdown-arrow {
|
||||
width: 0.12rem;
|
||||
height: 0.12rem;
|
||||
color: $text-middle;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
// 下拉菜单面板
|
||||
&__filter-dropdown {
|
||||
position: absolute;
|
||||
top: calc(100% + 0.06rem);
|
||||
left: 0;
|
||||
background: $bg-white;
|
||||
border: 1px solid #E8E8E8;
|
||||
border-radius: 0.08rem;
|
||||
padding: 0.04rem 0;
|
||||
box-shadow: 0 0.04rem 0.12rem rgba(0, 0, 0, 0.08);
|
||||
z-index: 20;
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
// 下拉菜单选项
|
||||
&__filter-dropdown-item {
|
||||
padding: 0.06rem 0.16rem;
|
||||
font-size: 0.12rem;
|
||||
color: $text-dark;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
transition: all 0.15s;
|
||||
|
||||
&:hover {
|
||||
background: $theme-color;
|
||||
color: $accent-hover;
|
||||
}
|
||||
|
||||
&--active {
|
||||
color: $accent;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
// 清空筛选按钮
|
||||
&__clear-filters {
|
||||
font-size: 0.12rem;
|
||||
color: $text-middle;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
|
||||
&:hover {
|
||||
color: $accent;
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索框
|
||||
&__search-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: $bg-white;
|
||||
border-radius: 0.08rem;
|
||||
padding: 0.10rem 0.14rem;
|
||||
border: 1px solid #F0F0F0;
|
||||
|
||||
&:focus-within {
|
||||
border-color: $accent;
|
||||
}
|
||||
}
|
||||
|
||||
&__search-svg {
|
||||
width: 0.14rem;
|
||||
height: 0.14rem;
|
||||
color: $text-light;
|
||||
margin-right: 0.08rem;
|
||||
flex-shrink: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&__search-input {
|
||||
flex: 1;
|
||||
background: transparent;
|
||||
border: none;
|
||||
outline: none;
|
||||
color: $text-dark;
|
||||
font-size: 0.12rem;
|
||||
|
||||
&::placeholder {
|
||||
color: $text-light;
|
||||
}
|
||||
}
|
||||
|
||||
// 热门搜索标签
|
||||
&__hot-search-label {
|
||||
font-size: 0.12rem;
|
||||
color: $text-middle;
|
||||
white-space: nowrap;
|
||||
margin-right: 0.07rem;
|
||||
}
|
||||
|
||||
&__hot-search-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 0.24rem;
|
||||
padding: 0 0.1rem;
|
||||
border-radius: 0.04rem;
|
||||
font-size: 0.12rem;
|
||||
color: $accent;
|
||||
background: $bg-white;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
user-select: none;
|
||||
transition: all 0.15s;
|
||||
margin-right: 0.07rem;
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&--active {
|
||||
background: $accent;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
}
|
||||
|
||||
// 列表区域
|
||||
&__list {
|
||||
overflow-y: auto;
|
||||
height: calc(var(--app-height, 100vh) - 3.4rem);
|
||||
padding: 0 0.24rem 0.2rem;
|
||||
}
|
||||
|
||||
// 表头
|
||||
&__table-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.10rem 0.12rem;
|
||||
background: #F4FBFB;
|
||||
border-radius: 0.08rem 0.08rem 0 0;
|
||||
border-bottom: 1px solid #F0F0F0;
|
||||
font-size: 0.12rem;
|
||||
font-weight: 600;
|
||||
color: $text-dark;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 5;
|
||||
gap: 0.2rem;
|
||||
>span{
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
// 表格行
|
||||
&__table-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.12rem;
|
||||
background: $bg-white;
|
||||
border-bottom: 1px solid #F6F6F6;
|
||||
font-size: 0.12rem;
|
||||
color: $text-dark;
|
||||
transition: background 0.15s;
|
||||
gap: 0.2rem;
|
||||
|
||||
&:hover {
|
||||
background: $theme-color;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
>span{
|
||||
flex: 1;
|
||||
}
|
||||
>div{
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
// 表头列宽
|
||||
&__th, &__td {
|
||||
flex-shrink: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&__th--time, &__td--time { min-width: 0.7rem; }
|
||||
&__th--company, &__td--company { width: 1.8rem; flex-shrink: 1; }
|
||||
&__th--title, &__td--title { width: 1.8rem; flex: 1; min-width: 1rem; }
|
||||
&__th--position, &__td--position { width: 1.5rem; }
|
||||
&__th--target, &__td--target { width: 1.2rem; }
|
||||
&__th--major, &__td--major { width: 1.6rem; }
|
||||
&__th--batch, &__td--batch { width: 1rem; }
|
||||
&__th--deadline, &__td--deadline { width: 0.9rem; }
|
||||
|
||||
// 公司信息列
|
||||
&__td--company {
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
&__company-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.08rem;
|
||||
}
|
||||
|
||||
&__company-logo {
|
||||
width: 0.32rem;
|
||||
height: 0.32rem;
|
||||
border-radius: 0.06rem;
|
||||
border: 1px solid #F4F4F4;
|
||||
flex-shrink: 0;
|
||||
object-fit: cover;
|
||||
|
||||
&--default {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: $bg-white;
|
||||
}
|
||||
}
|
||||
|
||||
&__company-text {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&__company-name {
|
||||
font-size: 0.13rem;
|
||||
font-weight: 500;
|
||||
color: $text-dark;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&__company-tags {
|
||||
display: flex;
|
||||
gap: 0.04rem;
|
||||
margin-top: 0.02rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
&__company-tag {
|
||||
font-size: 0.11rem;
|
||||
padding: 0.01rem 0.04rem;
|
||||
border-radius: 0.02rem;
|
||||
white-space: nowrap;
|
||||
|
||||
&--industry {
|
||||
background: #EDF9FA;
|
||||
color: $accent;
|
||||
}
|
||||
}
|
||||
|
||||
// 多行单元格
|
||||
&__td--position, &__td--target {
|
||||
white-space: normal;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.02rem;
|
||||
}
|
||||
|
||||
&__cell-line {
|
||||
font-size: 0.12rem;
|
||||
color: $text-dark;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&__cell-sub {
|
||||
font-size: 0.11rem;
|
||||
color: $text-light;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&__recruit-year {
|
||||
font-size: 0.12rem;
|
||||
color: #175CD3;
|
||||
background: #EFF8FF;
|
||||
padding: 0 0.08rem;
|
||||
border-radius: 0.04rem;
|
||||
display: inline-block;
|
||||
align-self: flex-start;
|
||||
height: 0.24rem;
|
||||
line-height: 0.24rem;
|
||||
}
|
||||
|
||||
// 空状态
|
||||
&__empty {
|
||||
text-align: center;
|
||||
font-size: 0.14rem;
|
||||
color: $text-light;
|
||||
}
|
||||
|
||||
&__empty-img {
|
||||
width: 1.24rem;
|
||||
height: 1.3rem;
|
||||
display: block;
|
||||
margin: 1rem auto 0.2rem;
|
||||
}
|
||||
|
||||
&__empty-title {
|
||||
font-size: 0.14rem;
|
||||
color: $text-light;
|
||||
margin-bottom: 0.08rem;
|
||||
}
|
||||
|
||||
&__empty-subtitle {
|
||||
font-size: 0.14rem;
|
||||
color: $text-light;
|
||||
}
|
||||
|
||||
&__empty-link {
|
||||
color: $accent;
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.75;
|
||||
}
|
||||
}
|
||||
|
||||
// 加载状态
|
||||
&__loading-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.08rem;
|
||||
padding: 0.24rem 0;
|
||||
font-size: 0.13rem;
|
||||
color: $text-light;
|
||||
}
|
||||
|
||||
&__loading-spinner {
|
||||
width: 0.18rem;
|
||||
height: 0.18rem;
|
||||
border: 2px solid rgba(0, 0, 0, 0.1);
|
||||
border-top-color: $accent;
|
||||
border-radius: 50%;
|
||||
animation: announcement-spin 0.6s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes announcement-spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
&__loading-more {
|
||||
text-align: center;
|
||||
padding: 0.16rem 0;
|
||||
font-size: 0.12rem;
|
||||
color: $text-light;
|
||||
}
|
||||
}
|
||||
@@ -83,6 +83,14 @@
|
||||
margin: 0 0.24rem 0.12rem 0.24rem;
|
||||
}
|
||||
|
||||
// 筛选面板 + 统计卡片横向排列容器
|
||||
&__filters-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
gap: 0.12rem;
|
||||
}
|
||||
|
||||
// AI 推荐提示条外层
|
||||
&__ai-tip-wrapper {
|
||||
position: relative;
|
||||
@@ -198,6 +206,8 @@
|
||||
|
||||
// 筛选面板
|
||||
&__filters-panel {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
background: $bg-white;
|
||||
border: 1px solid #F0F0F0;
|
||||
box-shadow: 0px 2px 6px rgba(15, 23, 42, 0.025);
|
||||
@@ -295,14 +305,14 @@
|
||||
background: #F4FBFB;
|
||||
border-color: $accent;
|
||||
color: $accent;
|
||||
font-weight: 520;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
// 选中态(实心品牌色背景,用于"全部/全国"按钮选中时)
|
||||
&--active-solid {
|
||||
background: $accent;
|
||||
color: #FFFFFF;
|
||||
font-weight: 520;
|
||||
font-weight: 600;
|
||||
border-color: $accent;
|
||||
}
|
||||
}
|
||||
@@ -390,7 +400,7 @@
|
||||
&__deep-match-btn {
|
||||
padding: 0 0.08rem;
|
||||
height: 0.20rem;
|
||||
line-height: 0.16rem;
|
||||
line-height: 0.20rem;
|
||||
border: none;
|
||||
border-radius: 0.04rem;
|
||||
background: linear-gradient(128.17deg, #52CAD1 0%, #82D79B 100%);
|
||||
@@ -398,8 +408,6 @@
|
||||
font-size: 0.12rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
transition: opacity 0.2s;
|
||||
|
||||
&:hover {
|
||||
@@ -529,7 +537,6 @@
|
||||
border-radius: 0.08rem;
|
||||
padding: 0.10rem 0.14rem;
|
||||
border: 1px solid #F0F0F0;
|
||||
margin-top: 0.12rem;
|
||||
|
||||
&:focus-within {
|
||||
border-color: $accent;
|
||||
@@ -558,6 +565,42 @@
|
||||
}
|
||||
}
|
||||
|
||||
// 热门搜索标签 label
|
||||
&__hot-search-label {
|
||||
font-size: 0.12rem;
|
||||
color: $text-middle;
|
||||
white-space: nowrap;
|
||||
margin-right: 0.07rem;
|
||||
}
|
||||
|
||||
// 热门搜索标签
|
||||
&__hot-search-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 0.24rem;
|
||||
padding: 0 0.1rem;
|
||||
border-radius: 0.04rem;
|
||||
font-size: 0.12rem;
|
||||
color: $accent;
|
||||
background: $bg-white;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
user-select: none;
|
||||
transition: all 0.15s;
|
||||
margin-right: 0.07rem;
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&--active {
|
||||
background: $accent;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
}
|
||||
|
||||
// 职位列表(用 calc 减去上方头部+筛选栏大致高度,保证列表独立滚动)
|
||||
&__list {
|
||||
gap: 0;
|
||||
@@ -619,7 +662,7 @@
|
||||
// 岗位标题
|
||||
&__card-title {
|
||||
font-size: 0.16rem;
|
||||
font-weight: 520;
|
||||
font-weight: 600;
|
||||
color: #1A1A1A;
|
||||
line-height: 0.21rem;
|
||||
white-space: nowrap;
|
||||
@@ -662,7 +705,7 @@
|
||||
&__card-match-score {
|
||||
display: block;
|
||||
font-size: 0.22rem;
|
||||
font-weight: 520;
|
||||
font-weight: 600;
|
||||
line-height: 0.29rem;
|
||||
text-align: center;
|
||||
}
|
||||
@@ -957,6 +1000,53 @@
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
// 未登录时底部登录提示
|
||||
&__login-prompt {
|
||||
text-align: center;
|
||||
margin-top: -0.3rem;
|
||||
padding-top: 0.4rem;
|
||||
padding-bottom: 1.6rem;
|
||||
background: linear-gradient(180deg, rgba(255, 255, 255, 0) 0%, #FFFFFF 25%);
|
||||
position: relative;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
&__login-prompt-title {
|
||||
font-size: 0.18rem;
|
||||
font-weight: 520;
|
||||
color: $accent;
|
||||
margin-bottom: 0.12rem;
|
||||
}
|
||||
|
||||
&__login-prompt-desc {
|
||||
font-size: 0.14rem;
|
||||
font-weight: 330;
|
||||
color: #6A7282;
|
||||
margin-bottom: 0.2rem;
|
||||
}
|
||||
|
||||
&__login-prompt-btn {
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 0.08rem 0.32rem;
|
||||
width: 1.8rem;
|
||||
height: 0.37rem;
|
||||
font-size: 0.14rem;
|
||||
font-weight: 450;
|
||||
color: #fff;
|
||||
background: $btn-dark;
|
||||
border: 1px solid $btn-dark;
|
||||
border-radius: 0.08rem;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: $btn-dark-hover;
|
||||
border-color: $btn-dark-hover;
|
||||
}
|
||||
}
|
||||
|
||||
// 列表底部加载盒子
|
||||
&__loading-box {
|
||||
display: flex;
|
||||
|
||||
@@ -1,158 +1,225 @@
|
||||
<!-- Agent页面 - 浏览器插件安装步骤指引组件 -->
|
||||
<!-- Agent页面 - 浏览器插件安装步骤指引组件(双Tab切换) -->
|
||||
<template>
|
||||
<div class="agent-browser-guide-drawer__body">
|
||||
<!-- 步骤1:下载插件包 -->
|
||||
<div class="agent-browser-guide-drawer__card">
|
||||
<div class="mr100">
|
||||
<div class="agent-browser-guide-drawer__step-row mr0">
|
||||
<span class="agent-browser-guide-drawer__step-num">1</span>
|
||||
<div class="agent-browser-guide-drawer__step-info">
|
||||
<h4 class="agent-browser-guide-drawer__step-title">下载并解压插件压缩包</h4>
|
||||
<p class="agent-browser-guide-drawer__step-desc">下载 Offer派自动投递助手安装包,并将下载好的插件压缩包解压</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="agent-browser-guide-drawer__download-box w263">
|
||||
<span class="agent-browser-guide-drawer__download-name">Offer派智能投递助手.zip</span>
|
||||
<button class="agent-browser-guide-drawer__download-btn" @click="downloadExtension">
|
||||
<svg viewBox="0 0 16 16" fill="none" width="14" height="14"><path d="M8 2v8M8 10L5 7M8 10l3-3M3 13h10" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||
<span>下载</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="agent-browser-guide">
|
||||
<!-- 顶部Tab切换 -->
|
||||
<div class="agent-browser-guide__tabs">
|
||||
<div
|
||||
class="agent-browser-guide__tab"
|
||||
:class="{ 'agent-browser-guide__tab--active': activeTab === 'edge' }"
|
||||
@click="activeTab = 'edge'"
|
||||
>
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip_tab_edge)">
|
||||
<path d="M14.4375 11.9059C14.225 12.0184 14.0062 12.1184 13.7812 12.1996C13.0625 12.4684 12.3062 12.6059 11.5375 12.6059C8.5812 12.6059 6.0062 10.5746 6.0062 7.96211C6.01245 7.24961 6.4062 6.59336 7.0312 6.25586C4.3562 6.36836 3.6687 9.15586 3.6687 10.7871C3.6687 15.4059 7.92495 15.8746 8.8437 15.8746C9.33745 15.8746 10.0812 15.7309 10.5312 15.5871L10.6125 15.5621C12.3375 14.9684 13.8 13.8059 14.775 12.2621C14.85 12.1434 14.8125 11.9934 14.7 11.9184C14.6187 11.8684 14.5187 11.8621 14.4375 11.9059Z" fill="#0C59A4"/>
|
||||
<path d="M9.51875 9.30625C9.46875 9.36875 9.3125 9.4625 9.3125 9.6625C9.3125 9.825 9.41875 9.98125 9.60625 10.1125C10.5062 10.7375 12.2 10.6562 12.2063 10.6562C12.875 10.6562 13.525 10.475 14.1 10.1375C15.275 9.45 16 8.19375 16 6.83125C16.0187 5.43125 15.5 4.5 15.2937 4.0875C13.9688 1.49375 11.1062 0 8 0C3.625 0 0.0625 3.5125 0 7.8875C0.03125 5.60625 2.3 3.7625 5 3.7625C5.21875 3.7625 6.46875 3.78125 7.625 4.39375C8.64375 4.93125 9.18125 5.575 9.55 6.21875C9.9375 6.8875 10.0063 7.725 10.0063 8.0625C10.0063 8.39375 9.8375 8.89375 9.51875 9.30625Z" fill="#35C1F1"/>
|
||||
</g>
|
||||
<defs><clipPath id="clip_tab_edge"><rect width="16" height="16" fill="white"/></clipPath></defs>
|
||||
</svg>
|
||||
<span>Edge商店下载</span>
|
||||
</div>
|
||||
<div
|
||||
class="agent-browser-guide__tab"
|
||||
:class="{ 'agent-browser-guide__tab--active': activeTab === 'manual' }"
|
||||
@click="activeTab = 'manual'"
|
||||
>
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip_tab_chrome)">
|
||||
<path d="M8 12.45C10.46 12.45 12.45 10.46 12.45 8C12.45 5.54 10.46 3.55 8 3.55C5.54 3.55 3.55 5.54 3.55 8C3.55 10.46 5.54 12.45 8 12.45" fill="white"/>
|
||||
<path d="M2.24 5.81C1.91 5.24 1.52 4.63 1.07 4C0.37 5.22 0 6.6 0 8C0 9.41 0.37 10.78 1.07 12C1.77 13.22 2.78 14.23 4 14.93C5.22 15.63 6.6 16 8 16C8.74 14.97 9.24 14.22 9.5 13.77C10.01 12.89 10.66 11.63 11.46 10V10C11.11 10.61 10.61 11.11 10 11.46C9.39 11.82 8.7 12 8 12C7.3 12 6.61 11.82 6 11.46C5.39 11.11 4.89 10.61 4.54 10C3.44 7.97 2.68 6.57 2.24 5.81Z" fill="#229342"/>
|
||||
<path d="M8 16C9.05 16 10.09 15.79 11.06 15.39C12.03 14.99 12.91 14.4 13.66 13.66C14.4 12.91 14.99 12.03 15.39 11.06C15.79 10.09 16 9.05 16 8C16 6.6 15.63 5.22 14.93 4C13.41 3.85 12.29 3.78 11.57 3.78C10.75 3.78 9.56 3.85 8 4L8 4C8.7 4 9.39 4.19 10 4.54C10.61 4.89 11.11 5.39 11.46 6C11.82 6.61 12 7.3 12 8C12 8.7 11.82 9.39 11.46 10L8 16Z" fill="#FBC116"/>
|
||||
<path d="M8 11.17C9.75 11.17 11.17 9.75 11.17 8C11.17 6.25 9.75 4.83 8 4.83C6.25 4.83 4.83 6.25 4.83 8C4.83 9.75 6.25 11.17 8 11.17Z" fill="#1A73E8"/>
|
||||
<path d="M8 4H14.93C14.23 2.78 13.22 1.77 12 1.07C10.78 0.37 9.4 0 8 0C6.6 0 5.22 0.37 4 1.07C2.78 1.77 1.77 2.78 1.07 4L4.54 10L4.54 10C4.19 9.39 4 8.7 4 8C4 7.3 4.18 6.61 4.54 6C4.89 5.39 5.39 4.89 6 4.54C6.61 4.18 7.3 4 8 4Z" fill="#E33B2E"/>
|
||||
</g>
|
||||
<defs><clipPath id="clip_tab_chrome"><rect width="16" height="16" fill="white"/></clipPath></defs>
|
||||
</svg>
|
||||
<span>Chrome/Edge手动安装</span>
|
||||
</div>
|
||||
<!-- 步骤1示意图 -->
|
||||
<img src="@/assets/images/agent-setting/1-1.png" alt="打开开发者模式" class="agent-browser-guide-drawer__step-img" />
|
||||
</div>
|
||||
|
||||
<!-- 步骤2:打开扩展管理页面 -->
|
||||
<div class="agent-browser-guide-drawer__card">
|
||||
<!-- Tab1: Edge商店下载(3步) -->
|
||||
<div v-if="activeTab === 'edge'" class="agent-browser-guide-drawer__body">
|
||||
<!-- 步骤1:点击链接跳转商店 -->
|
||||
<div class="agent-browser-guide-drawer__card">
|
||||
<div class="mr100">
|
||||
<div class="agent-browser-guide-drawer__step-row mr0">
|
||||
<span class="agent-browser-guide-drawer__step-num">1</span>
|
||||
<div class="agent-browser-guide-drawer__step-info">
|
||||
<h4 class="agent-browser-guide-drawer__step-title">点击下方链接跳转商店,获取插件</h4>
|
||||
<p class="agent-browser-guide-drawer__step-desc">打开 Edge 扩展商店中的「<a class="agent-browser-guide__link" :href="edgeStoreUrl" target="_blank">Offer派智能投递助手</a>」页面,点击右上角「获取」。</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 商店链接 + 跳转按钮 -->
|
||||
<div class="agent-browser-guide-drawer__download-box agent-browser-guide-drawer__download-box--wide">
|
||||
<span class="agent-browser-guide-drawer__download-name agent-browser-guide__url-text">{{ edgeStoreUrlDisplay }}</span>
|
||||
<button class="agent-browser-guide-drawer__download-btn" @click="openEdgeStore">
|
||||
<svg viewBox="0 0 16 16" fill="none" width="14" height="14"><path d="M6 3h7v7M13 3L3 13" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||
<span>跳转下载</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 步骤1示意图 -->
|
||||
<img src="@/assets/images/agent-setting/edge-step1.png" alt="Edge商店获取插件" class="agent-browser-guide-drawer__step-img" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<!-- 步骤2:添加扩展 -->
|
||||
<div class="agent-browser-guide-drawer__card">
|
||||
<div>
|
||||
<div class="agent-browser-guide-drawer__step-row">
|
||||
<span class="agent-browser-guide-drawer__step-num">2</span>
|
||||
<div class="agent-browser-guide-drawer__step-info">
|
||||
<h4 class="agent-browser-guide-drawer__step-title">添加扩展</h4>
|
||||
<p class="agent-browser-guide-drawer__step-desc">在浏览器弹出的确认窗口中,点击「添加扩展」,等待插件自动安装完成。</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 步骤2示意图 -->
|
||||
<img src="@/assets/images/agent-setting/edge-step2.png" alt="添加扩展" class="agent-browser-guide-drawer__step-img" />
|
||||
</div>
|
||||
|
||||
<!-- 步骤3:固定插件 -->
|
||||
<div class="agent-browser-guide-drawer__card">
|
||||
<div>
|
||||
<div class="agent-browser-guide-drawer__step-row">
|
||||
<span class="agent-browser-guide-drawer__step-num">3</span>
|
||||
<div class="agent-browser-guide-drawer__step-info">
|
||||
<h4 class="agent-browser-guide-drawer__step-title">固定插件</h4>
|
||||
<p class="agent-browser-guide-drawer__step-desc">点击浏览器右上角的「扩展」图标,找到「Offer派智能投递助手」,点击右侧的固钉图标,将插件固定到工具栏,方便后续使用。</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 步骤3示意图 -->
|
||||
<img src="@/assets/images/agent-setting/edge-step3.png" alt="固定插件" class="agent-browser-guide-drawer__step-img" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab2: Chrome/Edge手动安装(4步,保留原内容) -->
|
||||
<div v-if="activeTab === 'manual'" class="agent-browser-guide-drawer__body">
|
||||
<!-- 步骤1:下载插件包 -->
|
||||
<div class="agent-browser-guide-drawer__card">
|
||||
<div class="mr100">
|
||||
<div class="agent-browser-guide-drawer__step-row mr0">
|
||||
<span class="agent-browser-guide-drawer__step-num">1</span>
|
||||
<div class="agent-browser-guide-drawer__step-info">
|
||||
<h4 class="agent-browser-guide-drawer__step-title">下载并解压插件压缩包</h4>
|
||||
<p class="agent-browser-guide-drawer__step-desc">下载 Offer派智能投递助手安装包,并将下载好的插件压缩包解压</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="agent-browser-guide-drawer__download-box w263">
|
||||
<span class="agent-browser-guide-drawer__download-name">Offer派智能投递助手.zip</span>
|
||||
<button class="agent-browser-guide-drawer__download-btn" @click="downloadExtension">
|
||||
<svg viewBox="0 0 16 16" fill="none" width="14" height="14"><path d="M8 2v8M8 10L5 7M8 10l3-3M3 13h10" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||
<span>下载</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 步骤1示意图 -->
|
||||
<img src="@/assets/images/agent-setting/1-1.png" alt="打开开发者模式" class="agent-browser-guide-drawer__step-img" />
|
||||
</div>
|
||||
|
||||
<!-- 步骤2:打开扩展管理页面 -->
|
||||
<div class="agent-browser-guide-drawer__card">
|
||||
<div>
|
||||
<div class="agent-browser-guide-drawer__step-row">
|
||||
<span class="agent-browser-guide-drawer__step-num">2</span>
|
||||
<div class="agent-browser-guide-drawer__step-info">
|
||||
<h4 class="agent-browser-guide-drawer__step-title">打开扩展管理页面</h4>
|
||||
<p class="agent-browser-guide-drawer__step-desc">在浏览器地址栏输入对应地址并回车,打开扩展管理页面</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 浏览器地址链接 -->
|
||||
<div class="agent-browser-guide-drawer__browser-links">
|
||||
<div class="agent-browser-guide-drawer__browser-link" @click="copyToClipboard('chrome://extensions')">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_guide_chrome2)">
|
||||
<path d="M8 12.45C10.46 12.45 12.45 10.46 12.45 8C12.45 5.54 10.46 3.55 8 3.55C5.54 3.55 3.55 5.54 3.55 8C3.55 10.46 5.54 12.45 8 12.45" fill="white"/>
|
||||
<path d="M2.24 5.81C1.91 5.24 1.52 4.63 1.07 4C0.37 5.22 0 6.6 0 8C0 9.41 0.37 10.78 1.07 12C1.77 13.22 2.78 14.23 4 14.93C5.22 15.63 6.6 16 8 16C8.74 14.97 9.24 14.22 9.5 13.77C10.01 12.89 10.66 11.63 11.46 10V10C11.11 10.61 10.61 11.11 10 11.46C9.39 11.82 8.7 12 8 12C7.3 12 6.61 11.82 6 11.46C5.39 11.11 4.89 10.61 4.54 10C3.44 7.97 2.68 6.57 2.24 5.81Z" fill="#229342"/>
|
||||
<path d="M8 16C9.05 16 10.09 15.79 11.06 15.39C12.03 14.99 12.91 14.4 13.66 13.66C14.4 12.91 14.99 12.03 15.39 11.06C15.79 10.09 16 9.05 16 8C16 6.6 15.63 5.22 14.93 4C13.41 3.85 12.29 3.78 11.57 3.78C10.75 3.78 9.56 3.85 8 4L8 4C8.7 4 9.39 4.19 10 4.54C10.61 4.89 11.11 5.39 11.46 6C11.82 6.61 12 7.3 12 8C12 8.7 11.82 9.39 11.46 10L8 16Z" fill="#FBC116"/>
|
||||
<path d="M8 11.17C9.75 11.17 11.17 9.75 11.17 8C11.17 6.25 9.75 4.83 8 4.83C6.25 4.83 4.83 6.25 4.83 8C4.83 9.75 6.25 11.17 8 11.17Z" fill="#1A73E8"/>
|
||||
<path d="M8 4H14.93C14.23 2.78 13.22 1.77 12 1.07C10.78 0.37 9.4 0 8 0C6.6 0 5.22 0.37 4 1.07C2.78 1.77 1.77 2.78 1.07 4L4.54 10L4.54 10C4.19 9.39 4 8.7 4 8C4 7.3 4.18 6.61 4.54 6C4.89 5.39 5.39 4.89 6 4.54C6.61 4.18 7.3 4 8 4Z" fill="#E33B2E"/>
|
||||
</g>
|
||||
<defs><clipPath id="clip0_guide_chrome2"><rect width="16" height="16" fill="white"/></clipPath></defs>
|
||||
</svg>
|
||||
<span>Chrome浏览器:chrome://extensions</span>
|
||||
<span class="agent-browser-guide-drawer__copy-btn">
|
||||
<svg viewBox="0 0 14 14" fill="none" width="12" height="12"><rect x="4" y="4" width="8" height="8" rx="1" stroke="currentColor" stroke-width="1"/><path d="M10 4V3a1 1 0 00-1-1H3a1 1 0 00-1 1v6a1 1 0 001 1h1" stroke="currentColor" stroke-width="1"/></svg>
|
||||
复制
|
||||
</span>
|
||||
</div>
|
||||
<div class="agent-browser-guide-drawer__browser-link" @click="copyToClipboard('edge://extensions')">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_guide_edge2)">
|
||||
<path d="M14.4375 11.9059C14.225 12.0184 14.0062 12.1184 13.7812 12.1996C13.0625 12.4684 12.3062 12.6059 11.5375 12.6059C8.5812 12.6059 6.0062 10.5746 6.0062 7.96211C6.01245 7.24961 6.4062 6.59336 7.0312 6.25586C4.3562 6.36836 3.6687 9.15586 3.6687 10.7871C3.6687 15.4059 7.92495 15.8746 8.8437 15.8746C9.33745 15.8746 10.0812 15.7309 10.5312 15.5871L10.6125 15.5621C12.3375 14.9684 13.8 13.8059 14.775 12.2621C14.85 12.1434 14.8125 11.9934 14.7 11.9184C14.6187 11.8684 14.5187 11.8621 14.4375 11.9059Z" fill="#0C59A4"/>
|
||||
<path d="M9.51875 9.30625C9.46875 9.36875 9.3125 9.4625 9.3125 9.6625C9.3125 9.825 9.41875 9.98125 9.60625 10.1125C10.5062 10.7375 12.2 10.6562 12.2063 10.6562C12.875 10.6562 13.525 10.475 14.1 10.1375C15.275 9.45 16 8.19375 16 6.83125C16.0187 5.43125 15.5 4.5 15.2937 4.0875C13.9688 1.49375 11.1062 0 8 0C3.625 0 0.0625 3.5125 0 7.8875C0.03125 5.60625 2.3 3.7625 5 3.7625C5.21875 3.7625 6.46875 3.78125 7.625 4.39375C8.64375 4.93125 9.18125 5.575 9.55 6.21875C9.9375 6.8875 10.0063 7.725 10.0063 8.0625C10.0063 8.39375 9.8375 8.89375 9.51875 9.30625Z" fill="#35C1F1"/>
|
||||
</g>
|
||||
<defs><clipPath id="clip0_guide_edge2"><rect width="16" height="16" fill="white"/></clipPath></defs>
|
||||
</svg>
|
||||
<span>Edge浏览器:edge://extensions</span>
|
||||
<span class="agent-browser-guide-drawer__copy-btn">
|
||||
<svg viewBox="0 0 14 14" fill="none" width="12" height="12"><rect x="4" y="4" width="8" height="8" rx="1" stroke="currentColor" stroke-width="1"/><path d="M10 4V3a1 1 0 00-1-1H3a1 1 0 00-1 1v6a1 1 0 001 1h1" stroke="currentColor" stroke-width="1"/></svg>
|
||||
复制
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 步骤2示意图 -->
|
||||
<img src="@/assets/images/agent-setting/2-1.png" alt="输入扩展地址" class="agent-browser-guide-drawer__step-img" />
|
||||
</div>
|
||||
|
||||
<!-- 步骤3:打开开发者模式 -->
|
||||
<div class="agent-browser-guide-drawer__card">
|
||||
<div class="agent-browser-guide-drawer__step-row">
|
||||
<span class="agent-browser-guide-drawer__step-num">2</span>
|
||||
<span class="agent-browser-guide-drawer__step-num">3</span>
|
||||
<div class="agent-browser-guide-drawer__step-info">
|
||||
<h4 class="agent-browser-guide-drawer__step-title">打开扩展管理页面</h4>
|
||||
<p class="agent-browser-guide-drawer__step-desc">在浏览器地址栏输入对应地址并回车,打开扩展管理页面</p>
|
||||
<h4 class="agent-browser-guide-drawer__step-title">打开开发者模式</h4>
|
||||
<p class="agent-browser-guide-drawer__step-desc"><span class="color-D31414">Chrome浏览器</span>在扩展管理页面的右上角,找到「开发者模式」开关并打开</p>
|
||||
<p class="agent-browser-guide-drawer__step-desc mt3"><span class="color-2F7DEA">Edge浏览器</span>在扩展管理页面左下角,找到「开发者模式」开关并打开</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 浏览器地址链接 -->
|
||||
<div class="agent-browser-guide-drawer__browser-links">
|
||||
<div class="agent-browser-guide-drawer__browser-link" @click="copyToClipboard('chrome://extensions')">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_guide_chrome)">
|
||||
<path d="M8.00027 12.4507C10.4587 12.4507 12.4516 10.4577 12.4516 7.99929C12.4516 5.54085 10.4587 3.54785 8.00027 3.54785C5.54183 3.54785 3.54883 5.54085 3.54883 7.99929C3.54883 10.4577 5.54177 12.4507 8.0002 12.4507" fill="white"/>
|
||||
<path d="M2.24303 5.81204C1.91094 5.23683 1.52061 4.63314 1.07203 4.00098C0.369757 5.21706 2.38548e-05 6.59661 1.15428e-09 8.0009C-2.38525e-05 9.4052 0.369663 10.7848 1.07189 12.0009C1.77412 13.217 2.78414 14.2268 4.0004 14.9288C5.21666 15.6307 6.59629 16.0001 8.00059 15.9998C8.73688 14.9671 9.2368 14.2225 9.50034 13.766C10.0065 12.8893 10.6612 11.6341 11.4643 10.0004V9.99941C11.1134 10.6077 10.6086 11.113 10.0005 11.4643C9.39241 11.8156 8.70256 12.0006 8.0003 12.0007C7.29804 12.0008 6.60813 11.816 5.99995 11.4649C5.39178 11.1138 4.88677 10.6087 4.53571 10.0005C3.44496 7.96623 2.68073 6.57004 2.24303 5.81204Z" fill="#229342"/>
|
||||
<path d="M8.00046 16.0002C9.05106 16.0004 10.0914 15.7936 11.0621 15.3916C12.0327 14.9896 12.9147 14.4003 13.6575 13.6574C14.4004 12.9145 14.9896 12.0325 15.3915 11.0619C15.7935 10.0912 16.0002 9.05084 16 8.00024C15.9997 6.59593 15.6297 5.21644 14.9273 4.00043C13.4118 3.85105 12.2933 3.77637 11.5719 3.77637C10.7539 3.77637 9.56323 3.85105 7.9999 4.00043L7.99902 4.00105C8.7013 4.00071 9.39128 4.18529 9.99958 4.53623C10.6079 4.88717 11.113 5.39211 11.4643 6.00024C11.8155 6.60838 12.0004 7.29828 12.0004 8.00056C12.0004 8.70284 11.8155 9.39274 11.4642 10.0009L8.00046 16.0002Z" fill="#FBC116"/>
|
||||
<path d="M8.00013 11.1677C9.74913 11.1677 11.1669 9.74992 11.1669 8.00086C11.1669 6.2518 9.74913 4.83398 8.00006 4.83398C6.25113 4.83398 4.83325 6.25186 4.83325 8.00086C4.83325 9.74986 6.25113 11.1677 8.00013 11.1677Z" fill="#1A73E8"/>
|
||||
<path d="M8.00027 4.00027H14.9276C14.2256 2.78399 13.2158 1.77397 11.9997 1.07176C10.7835 0.369547 9.40396 -9.50021e-05 7.99965 1.83149e-08C6.59534 9.50388e-05 5.21581 0.369924 3.99978 1.0723C2.78374 1.77467 1.77406 2.78484 1.07227 4.00121L4.53595 10.0006L4.53689 10.0011C4.18549 9.39306 4.00037 8.70322 4.00017 8.00095C3.99996 7.29868 4.18467 6.60873 4.53571 6.0005C4.88676 5.39226 5.39177 4.88718 5.99995 4.53605C6.60814 4.18491 7.29806 4.0001 8.00033 4.00021L8.00027 4.00027Z" fill="#E33B2E"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_guide_chrome">
|
||||
<rect width="16" height="16" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
<span>Chrome浏览器:chrome://extensions</span>
|
||||
<span class="agent-browser-guide-drawer__copy-btn">
|
||||
<svg viewBox="0 0 14 14" fill="none" width="12" height="12"><rect x="4" y="4" width="8" height="8" rx="1" stroke="currentColor" stroke-width="1"/><path d="M10 4V3a1 1 0 00-1-1H3a1 1 0 00-1 1v6a1 1 0 001 1h1" stroke="currentColor" stroke-width="1"/></svg>
|
||||
复制
|
||||
</span>
|
||||
</div>
|
||||
<div class="agent-browser-guide-drawer__browser-link" @click="copyToClipboard('edge://extensions')">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_guide_edge)">
|
||||
<path d="M14.4375 11.9059C14.225 12.0184 14.0062 12.1184 13.7812 12.1996C13.0625 12.4684 12.3062 12.6059 11.5375 12.6059C8.5812 12.6059 6.0062 10.5746 6.0062 7.96211C6.01245 7.24961 6.4062 6.59336 7.0312 6.25586C4.3562 6.36836 3.6687 9.15586 3.6687 10.7871C3.6687 15.4059 7.92495 15.8746 8.8437 15.8746C9.33745 15.8746 10.0812 15.7309 10.5312 15.5871L10.6125 15.5621C12.3375 14.9684 13.8 13.8059 14.775 12.2621C14.85 12.1434 14.8125 11.9934 14.7 11.9184C14.6187 11.8684 14.5187 11.8621 14.4375 11.9059Z" fill="url(#paint0_linear_guide_edge)"/>
|
||||
<path opacity="0.35" d="M14.4375 11.9059C14.225 12.0184 14.0062 12.1184 13.7812 12.1996C13.0625 12.4684 12.3062 12.6059 11.5375 12.6059C8.5812 12.6059 6.0062 10.5746 6.0062 7.96211C6.01245 7.24961 6.4062 6.59336 7.0312 6.25586C4.3562 6.36836 3.6687 9.15586 3.6687 10.7871C3.6687 15.4059 7.92495 15.8746 8.8437 15.8746C9.33745 15.8746 10.0812 15.7309 10.5312 15.5871L10.6125 15.5621C12.3375 14.9684 13.8 13.8059 14.775 12.2621C14.85 12.1434 14.8125 11.9934 14.7 11.9184C14.6187 11.8684 14.5187 11.8621 14.4375 11.9059Z" fill="url(#paint1_radial_guide_edge)"/>
|
||||
<path d="M6.60629 15.0877C6.05004 14.7439 5.56879 14.2877 5.18754 13.7564C3.54379 11.5064 4.03754 8.3502 6.28754 6.70645C6.52504 6.5377 6.76879 6.38145 7.03129 6.25645C7.22504 6.1627 7.55629 6.0002 8.00004 6.00645C8.63129 6.0127 9.22504 6.3127 9.60629 6.81895C9.85629 7.15645 10 7.5627 10.0063 7.9877C10.0063 7.9752 11.5375 3.0127 5.00629 3.0127C2.26254 3.0127 0.00628588 5.61895 0.00628588 7.9002C-0.00621412 9.10645 0.256286 10.3064 0.762536 11.4002C2.48754 15.0752 6.68754 16.8752 10.5375 15.5939C9.21879 16.0064 7.78129 15.8252 6.60629 15.0877Z" fill="url(#paint2_linear_guide_edge)"/>
|
||||
<path opacity="0.41" d="M6.60629 15.0877C6.05004 14.7439 5.56879 14.2877 5.18754 13.7564C3.54379 11.5064 4.03754 8.3502 6.28754 6.70645C6.52504 6.5377 6.76879 6.38145 7.03129 6.25645C7.22504 6.1627 7.55629 6.0002 8.00004 6.00645C8.63129 6.0127 9.22504 6.3127 9.60629 6.81895C9.85629 7.15645 10 7.5627 10.0063 7.9877C10.0063 7.9752 11.5375 3.0127 5.00629 3.0127C2.26254 3.0127 0.00628588 5.61895 0.00628588 7.9002C-0.00621412 9.10645 0.256286 10.3064 0.762536 11.4002C2.48754 15.0752 6.68754 16.8752 10.5375 15.5939C9.21879 16.0064 7.78129 15.8252 6.60629 15.0877Z" fill="url(#paint3_radial_guide_edge)"/>
|
||||
<path d="M9.51875 9.30625C9.46875 9.36875 9.3125 9.4625 9.3125 9.6625C9.3125 9.825 9.41875 9.98125 9.60625 10.1125C10.5062 10.7375 12.2 10.6562 12.2063 10.6562C12.875 10.6562 13.525 10.475 14.1 10.1375C15.275 9.45 16 8.19375 16 6.83125C16.0187 5.43125 15.5 4.5 15.2937 4.0875C13.9688 1.49375 11.1062 0 8 0C3.625 0 0.0625 3.5125 0 7.8875C0.03125 5.60625 2.3 3.7625 5 3.7625C5.21875 3.7625 6.46875 3.78125 7.625 4.39375C8.64375 4.93125 9.18125 5.575 9.55 6.21875C9.9375 6.8875 10.0063 7.725 10.0063 8.0625C10.0063 8.39375 9.8375 8.89375 9.51875 9.30625Z" fill="url(#paint4_radial_guide_edge)"/>
|
||||
<path d="M9.51875 9.30625C9.46875 9.36875 9.3125 9.4625 9.3125 9.6625C9.3125 9.825 9.41875 9.98125 9.60625 10.1125C10.5062 10.7375 12.2 10.6562 12.2063 10.6562C12.875 10.6562 13.525 10.475 14.1 10.1375C15.275 9.45 16 8.19375 16 6.83125C16.0187 5.43125 15.5 4.5 15.2937 4.0875C13.9688 1.49375 11.1062 0 8 0C3.625 0 0.0625 3.5125 0 7.8875C0.03125 5.60625 2.3 3.7625 5 3.7625C5.21875 3.7625 6.46875 3.78125 7.625 4.39375C8.64375 4.93125 9.18125 5.575 9.55 6.21875C9.9375 6.8875 10.0063 7.725 10.0063 8.0625C10.0063 8.39375 9.8375 8.89375 9.51875 9.30625Z" fill="url(#paint5_radial_guide_edge)"/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_guide_edge" x1="3.66895" y1="11.0634" x2="14.8116" y2="11.0634" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#0C59A4"/>
|
||||
<stop offset="1" stop-color="#114A8B"/>
|
||||
</linearGradient>
|
||||
<radialGradient id="paint1_radial_guide_edge" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(9.82444 11.1519) scale(5.96065 5.662)">
|
||||
<stop offset="0.72" stop-opacity="0"/>
|
||||
<stop offset="0.95" stop-opacity="0.53"/>
|
||||
<stop offset="1"/>
|
||||
</radialGradient>
|
||||
<linearGradient id="paint2_linear_guide_edge" x1="9.54822" y1="6.23432" x2="2.58741" y2="13.8163" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#1B9DE2"/>
|
||||
<stop offset="0.16" stop-color="#1595DF"/>
|
||||
<stop offset="0.67" stop-color="#0680D7"/>
|
||||
<stop offset="1" stop-color="#0078D4"/>
|
||||
</linearGradient>
|
||||
<radialGradient id="paint3_radial_guide_edge" cx="0" cy="0" r="1" gradientTransform="matrix(1.34287 -8.86118 7.162 1.0743 4.41307 12.4383)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.76" stop-opacity="0"/>
|
||||
<stop offset="0.95" stop-opacity="0.5"/>
|
||||
<stop offset="1"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="paint4_radial_guide_edge" cx="0" cy="0" r="1" gradientTransform="matrix(-0.506075 12.6493 -26.9472 -1.0119 1.61573 2.96205)" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#35C1F1"/>
|
||||
<stop offset="0.11" stop-color="#34C1ED"/>
|
||||
<stop offset="0.23" stop-color="#2FC2DF"/>
|
||||
<stop offset="0.31" stop-color="#2BC3D2"/>
|
||||
<stop offset="0.67" stop-color="#36C752"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="paint5_radial_guide_edge" cx="0" cy="0" r="1" gradientTransform="matrix(1.70345 5.83918 -4.74533 1.39926 15.0038 4.83786)" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#66EB6E"/>
|
||||
<stop offset="1" stop-color="#66EB6E" stop-opacity="0"/>
|
||||
</radialGradient>
|
||||
<clipPath id="clip0_guide_edge">
|
||||
<rect width="16" height="16" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
<span>Edge浏览器:edge://extensions</span>
|
||||
<span class="agent-browser-guide-drawer__copy-btn">
|
||||
<svg viewBox="0 0 14 14" fill="none" width="12" height="12"><rect x="4" y="4" width="8" height="8" rx="1" stroke="currentColor" stroke-width="1"/><path d="M10 4V3a1 1 0 00-1-1H3a1 1 0 00-1 1v6a1 1 0 001 1h1" stroke="currentColor" stroke-width="1"/></svg>
|
||||
复制
|
||||
</span>
|
||||
<!-- 步骤3示意图 -->
|
||||
<img src="@/assets/images/agent-setting/3-1.png" alt="打开开发者模式" class="agent-browser-guide-drawer__step-img max-w602" />
|
||||
</div>
|
||||
|
||||
<!-- 步骤4:加载未打包的扩展程序 -->
|
||||
<div class="agent-browser-guide-drawer__card">
|
||||
<div class="agent-browser-guide-drawer__step-row">
|
||||
<span class="agent-browser-guide-drawer__step-num">4</span>
|
||||
<div class="agent-browser-guide-drawer__step-info dflex flex-warp">
|
||||
<div>
|
||||
<h4 class="agent-browser-guide-drawer__step-title">点击加载未打包的扩展程序</h4>
|
||||
<p class="agent-browser-guide-drawer__step-desc">点击「加载未打包的扩展程序」,选择直接包含 static 文件夹的「Offer派智能投递助手」文件夹</p>
|
||||
</div>
|
||||
<img src="@/assets/images/agent-setting/4-1.png" alt="加载扩展" class="agent-browser-guide-drawer__step-img" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- 步骤4示意图 -->
|
||||
|
||||
<img src="@/assets/images/agent-setting/4-2.png" alt="加载扩展" class="agent-browser-guide-drawer__step-img" />
|
||||
</div>
|
||||
<!-- 步骤2示意图 -->
|
||||
<img src="@/assets/images/agent-setting/2-1.png" alt="输入扩展地址" class="agent-browser-guide-drawer__step-img" />
|
||||
</div>
|
||||
|
||||
<!-- 步骤3:打开开发者模式 -->
|
||||
<div class="agent-browser-guide-drawer__card">
|
||||
<div class="agent-browser-guide-drawer__step-row">
|
||||
<span class="agent-browser-guide-drawer__step-num">3</span>
|
||||
<div class="agent-browser-guide-drawer__step-info">
|
||||
<h4 class="agent-browser-guide-drawer__step-title">打开开发者模式</h4>
|
||||
<p class="agent-browser-guide-drawer__step-desc"><span class="color-D31414">Chrome浏览器</span>在扩展管理页面的右上角,找到「开发者模式」开关并打开</p>
|
||||
<p class="agent-browser-guide-drawer__step-desc mt3"><span class="color-2F7DEA">Edge浏览器</span>在扩展管理页面左下角,找到「开发者模式」开关并打开</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 步骤3示意图 -->
|
||||
<img src="@/assets/images/agent-setting/3-1.png" alt="打开开发者模式" class="agent-browser-guide-drawer__step-img max-w602" />
|
||||
</div>
|
||||
|
||||
<!-- 步骤4:将CRX安装包拖入安装 -->
|
||||
<div class="agent-browser-guide-drawer__card">
|
||||
<div class="agent-browser-guide-drawer__step-row">
|
||||
<span class="agent-browser-guide-drawer__step-num">4</span>
|
||||
<div class="agent-browser-guide-drawer__step-info">
|
||||
<h4 class="agent-browser-guide-drawer__step-title">点击加载未打包的扩展程序</h4>
|
||||
<p class="agent-browser-guide-drawer__step-desc">点击"加载未打包的扩展程序",选择解压后的插件文件夹,完成安装</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 步骤4示意图 -->
|
||||
<img src="@/assets/images/agent-setting/4-1.png" alt="拖入安装" class="agent-browser-guide-drawer__step-img" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
/** 插件下载地址 */
|
||||
/** 当前激活的Tab */
|
||||
const activeTab = ref<'edge' | 'manual'>('edge')
|
||||
|
||||
/** Edge扩展商店地址 */
|
||||
const edgeStoreUrl = 'https://microsoftedge.microsoft.com/addons/detail/offer派智能投递助手/jianshixingqiu'
|
||||
/** Edge商店地址展示文本(截断显示) */
|
||||
const edgeStoreUrlDisplay = 'https://microsoftedge.microsoft.com/addons/detai...'
|
||||
|
||||
/** 插件zip下载地址 */
|
||||
const extensionDownloadUrl = 'https://offerpie.oss-cn-guangzhou.aliyuncs.com/extension/Offer派智能投递助手.zip'
|
||||
|
||||
/** 下载插件 */
|
||||
/** 跳转Edge商店 */
|
||||
function openEdgeStore() {
|
||||
window.open(edgeStoreUrl, '_blank')
|
||||
}
|
||||
|
||||
/** 下载插件zip包 */
|
||||
function downloadExtension() {
|
||||
window.open(extensionDownloadUrl, '_blank')
|
||||
}
|
||||
|
||||
+455
-140
@@ -43,9 +43,28 @@
|
||||
<span class="agent-page__intro-avatar-name">Nova</span>
|
||||
</div>
|
||||
<!-- 标题 -->
|
||||
<h2 class="agent-page__intro-title">请先确认你的个人资料是否无误</h2>
|
||||
<h2 class="agent-page__intro-title">请先选择并确认你的投递简历</h2>
|
||||
<!-- 描述 -->
|
||||
<p class="agent-page__intro-desc">请仔细对你的资料评估(包括个人信息、教育背景、工作履历及技能),确保资料齐全</p>
|
||||
<p class="agent-page__intro-desc">请选择智能投递使用的简历,并仔细核对简历内容,确保各项信息准确、完整。</p>
|
||||
<div class="agent-page__step2-filters">
|
||||
<!-- 自定义简历下拉 -->
|
||||
<div class="agent-page__step2-resume-dropdown" @click.stop="showResumeDropdown = !showResumeDropdown">
|
||||
<span class="agent-page__step2-resume-dropdown-text">{{ selectedResumeName || '请选择简历' }}</span>
|
||||
<svg class="agent-page__step2-resume-dropdown-arrow" viewBox="0 0 12 12" fill="none">
|
||||
<path d="M3 4.5L6 7.5L9 4.5" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
<!-- 下拉菜单 -->
|
||||
<div v-if="showResumeDropdown" class="agent-page__step2-resume-menu" @click.stop>
|
||||
<div
|
||||
v-for="r in resumeList"
|
||||
:key="r.id"
|
||||
class="agent-page__step2-resume-menu-item"
|
||||
:class="{ 'agent-page__step2-resume-menu-item--active': selectedResumeId === r.id }"
|
||||
@click.stop="selectResume(r)"
|
||||
>{{ r.resumeName }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 按钮 -->
|
||||
<button class="agent-page__intro-gradient-btn" @click="handleNext">下一步</button>
|
||||
|
||||
@@ -54,7 +73,7 @@
|
||||
|
||||
</template>
|
||||
|
||||
<!-- ========== 第2步:确认目标 ========== -->
|
||||
<!-- ========== 第2步:配置投递方案 ========== -->
|
||||
<template v-if="currentStep === 2">
|
||||
<!-- 左侧:求职偏好 + 简历选择 -->
|
||||
<div class="agent-page__right">
|
||||
@@ -113,25 +132,28 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 简历选择区域 -->
|
||||
<!-- 投递偏好区域 -->
|
||||
<div class="agent-page__step2-section">
|
||||
<h3 class="agent-page__step2-section-title">简历选择</h3>
|
||||
<div class="agent-page__step2-filters">
|
||||
<!-- 自定义简历下拉 -->
|
||||
<div class="agent-page__step2-resume-dropdown" @click.stop="showResumeDropdown = !showResumeDropdown">
|
||||
<span class="agent-page__step2-resume-dropdown-text">{{ selectedResumeName || '请选择简历' }}</span>
|
||||
<svg class="agent-page__step2-resume-dropdown-arrow" viewBox="0 0 12 12" fill="none">
|
||||
<path d="M3 4.5L6 7.5L9 4.5" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
<!-- 下拉菜单 -->
|
||||
<div v-if="showResumeDropdown" class="agent-page__step2-resume-menu" @click.stop>
|
||||
<div
|
||||
v-for="r in resumeList"
|
||||
:key="r.id"
|
||||
class="agent-page__step2-resume-menu-item"
|
||||
:class="{ 'agent-page__step2-resume-menu-item--active': selectedResumeId === r.id }"
|
||||
@click.stop="selectResume(r)"
|
||||
>{{ r.resumeName }}</div>
|
||||
<h3 class="agent-page__step2-section-title">投递偏好</h3>
|
||||
<div class="agent-page__step2-pref-switches">
|
||||
<!-- 开启简历针对性优化 -->
|
||||
<div class="agent-page__step2-switch-item">
|
||||
<div class="agent-page__step2-switch-text">
|
||||
<span class="agent-page__step2-switch-label">开启简历针对性优化</span>
|
||||
<span class="agent-page__step2-switch-desc">结合岗位要求,自动优化简历表达</span>
|
||||
</div>
|
||||
<div class="agent-page__step2-toggle" :class="{ 'agent-page__step2-toggle--on': autoOptimizeSwitch }" @click="toggleOptimize">
|
||||
<div class="agent-page__step2-toggle-dot"></div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 开启深度人岗匹配分析 -->
|
||||
<div class="agent-page__step2-switch-item">
|
||||
<div class="agent-page__step2-switch-text">
|
||||
<span class="agent-page__step2-switch-label">开启深度人岗匹配分析</span>
|
||||
<span class="agent-page__step2-switch-desc">分析你的匹配优势与差距</span>
|
||||
</div>
|
||||
<div class="agent-page__step2-toggle" :class="{ 'agent-page__step2-toggle--on': autoMatchAnalysisSwitch }" @click="toggleMatchAnalysis">
|
||||
<div class="agent-page__step2-toggle-dot"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -176,9 +198,9 @@
|
||||
<span class="agent-page__intro-avatar-name">Nova</span>
|
||||
</div>
|
||||
<!-- 标题 -->
|
||||
<h2 class="agent-page__intro-title">安装浏览器自动投递插件</h2>
|
||||
<h2 class="agent-page__intro-title">请按左侧步骤安装浏览器插件</h2>
|
||||
<!-- 描述 -->
|
||||
<p class="agent-page__intro-desc">按照左侧步骤完成插件安装,即可开启自动投递功能</p>
|
||||
<p class="agent-page__intro-desc">只需4步,完成 Chrome / Edge 插件安装</p>
|
||||
<!-- 按钮:在线显示下一步,离线显示安装确认按钮 -->
|
||||
<button v-if="plugIsOnline" class="agent-page__intro-gradient-btn" @click="handleNext">下一步</button>
|
||||
<button v-else class="agent-page__intro-gradient-btn" @click="handleInstallConfirm">
|
||||
@@ -234,7 +256,7 @@
|
||||
</Teleport>
|
||||
</div>
|
||||
<!-- 功能说明 — 独立区块,仅第2步显示 -->
|
||||
<div v-if="currentStep === 2" class="agent-page__step2-faq mrl24 mt16">
|
||||
<div v-if="currentStep === 2||currentStep === 3" class="agent-page__step2-faq mrl24 mt16">
|
||||
<h3 class="agent-page__step2-faq-title">功能说明</h3>
|
||||
<div class="agent-page__step2-faq-item">
|
||||
<div class="agent-page__step2-faq-item-title">为什么要选择求职偏好?</div>
|
||||
@@ -267,11 +289,19 @@ import RegionSelector from '@/components/tools/RegionSelector.vue'
|
||||
import JobCategorySelector from '@/components/tools/JobCategorySelector.vue'
|
||||
import IndustrySelector from '@/components/tools/IndustrySelector.vue'
|
||||
import AgentBrowserInstallGuide from '@/components/AgentBrowserInstallGuide.vue'
|
||||
import { saveProfile, saveEducation, saveWork, saveInternship, saveProject, saveCompetition, fetchProfile, fetchEducation, fetchWork, fetchInternship, fetchProject, fetchCompetition } from '@/api/profile'
|
||||
import type { SaveEducationItem, SaveWorkItem, SaveProjectItem, SaveCompetitionItem } from '@/api/profile'
|
||||
import { fetchResumeList, setDefaultResume } from '@/api/resume'
|
||||
import { saveProfile, fetchProfile } from '@/api/profile'
|
||||
import {
|
||||
fetchResumeList, setDefaultResume, fetchResumeMain, saveResumeMain,
|
||||
fetchResumeEducation, fetchResumeWork, fetchResumeInternship, fetchResumeProject, fetchResumeCompetition,
|
||||
addResumeEducation, updateResumeEducation, deleteResumeEducation,
|
||||
addResumeWork, updateResumeWork, deleteResumeWork,
|
||||
addResumeInternship, updateResumeInternship, deleteResumeInternship,
|
||||
addResumeProject, updateResumeProject, deleteResumeProject,
|
||||
addResumeCompetition, updateResumeCompetition, deleteResumeCompetition,
|
||||
} from '@/api/resume'
|
||||
import type { ResumeListItem } from '@/api/resume'
|
||||
import type { AgentConfig } from '@/api/agent'
|
||||
import { fetchAgentConfig, saveAgentConfig } from '@/api/agent'
|
||||
|
||||
const store = useStore()
|
||||
|
||||
@@ -309,6 +339,10 @@ const editModule = ref('info')
|
||||
const editInitialData = ref<Record<string, any>>({})
|
||||
|
||||
// ==================== 个人档案数据 ====================
|
||||
|
||||
/** 默认简历 ID(从简历列表中确定) */
|
||||
const defaultResumeId = ref('')
|
||||
|
||||
onMounted(async () => {
|
||||
// 恢复刷新前保存的步骤号
|
||||
const savedStep = sessionStorage.getItem('wizardStep')
|
||||
@@ -337,92 +371,160 @@ onMounted(async () => {
|
||||
|
||||
if (!store.state.regions.length) store.dispatch('loadCommonData')
|
||||
store.dispatch('loadJobIntention')
|
||||
// 个人信息仍从 /user/profile 获取
|
||||
await loadProfile()
|
||||
await loadEducation()
|
||||
await loadWork()
|
||||
await loadInternship()
|
||||
await loadProject()
|
||||
await loadCompetition()
|
||||
// 其他数据(技能、证书、个人概述、5大经历)从默认简历获取
|
||||
await loadDefaultResumeData()
|
||||
//加载简历列表
|
||||
loadResumeList()
|
||||
// 加载投递偏好开关状态
|
||||
loadPreferenceSwitches()
|
||||
})
|
||||
|
||||
// ==================== 通信桥接:插件在线状态从 store 全局获取 ====================
|
||||
/** 插件是否在线(从 vuex 全局状态读取) */
|
||||
const plugIsOnline = computed(() => store.state.plugIsOnline)
|
||||
|
||||
/** 加载个人资料主表 — 仅取 name/email/mobileNumber/regionCode/wechatNumber/portfolioUrl */
|
||||
async function loadProfile() {
|
||||
try {
|
||||
const res = await fetchProfile()
|
||||
if (res.code === '0' && res.data) {
|
||||
const d = res.data
|
||||
profile.value.name = d.name || ''; profile.value.phone = d.mobileNumber || ''
|
||||
profile.value.email = d.email || ''; profile.value.idNumber = d.idCard || ''
|
||||
profile.value.regionCode = d.regionCode || ''; profile.value.wechat = d.wechatNumber || ''
|
||||
profile.value.skills = d.skills || []; profile.value.certificates = d.certificates || []
|
||||
profile.value.name = d.name || ''
|
||||
profile.value.phone = d.mobileNumber || ''
|
||||
profile.value.email = d.email || ''
|
||||
profile.value.regionCode = d.regionCode || ''
|
||||
profile.value.wechat = d.wechatNumber || ''
|
||||
profile.value.portfolioUrl = d.portfolioUrl || ''
|
||||
}
|
||||
} catch { console.error('[AgentSetupWizard] 加载个人资料失败') }
|
||||
}
|
||||
async function loadEducation() {
|
||||
|
||||
/**
|
||||
* 确定默认简历并加载简历主表 + 5大经历
|
||||
* 规则:优先 isDefault===1 的简历,否则取 createTime 最新的
|
||||
*/
|
||||
async function loadDefaultResumeData() {
|
||||
try {
|
||||
const res = await fetchEducation()
|
||||
if (res.code === '0' && res.data) {
|
||||
profile.value.education = res.data.map(item => ({ school: item.school || '', major: item.major || '', studyType: item.studyType ?? 0, degree: item.degree ?? 2, startDate: item.startDate || '', endDate: item.endDate || '', description: (item.description || []).map(d => ({ id: d.id || '', text: d.text || '' })) }))
|
||||
const listRes = await fetchResumeList()
|
||||
if (listRes.code !== '0' || !listRes.data || listRes.data.length === 0) {
|
||||
console.warn('[AgentSetupWizard] 无简历数据')
|
||||
return
|
||||
}
|
||||
} catch { console.error('[AgentSetupWizard] 加载教育经历失败') }
|
||||
}
|
||||
async function loadWork() {
|
||||
try {
|
||||
const res = await fetchWork()
|
||||
if (res.code === '0' && res.data) {
|
||||
profile.value.works = res.data.map(item => ({ companyName: item.companyName || '', position: item.position || '', startDate: item.startDate || '', endDate: item.endDate || '', description: (item.description || []).map(d => ({ id: d.id || '', text: d.text || '' })) }))
|
||||
const list = listRes.data
|
||||
// 确定默认简历
|
||||
let target = list.find(r => r.isDefault === 1)
|
||||
if (!target) {
|
||||
// 按 createTime 降序取第一个
|
||||
target = [...list].sort((a, b) => (b.createTime || 0) - (a.createTime || 0))[0]
|
||||
}
|
||||
} catch { console.error('[AgentSetupWizard] 加载工作经历失败') }
|
||||
}
|
||||
async function loadInternship() {
|
||||
try {
|
||||
const res = await fetchInternship()
|
||||
if (res.code === '0' && res.data) {
|
||||
profile.value.internships = res.data.map(item => ({ companyName: item.companyName || '', position: item.position || '', startDate: item.startDate || '', endDate: item.endDate || '', description: (item.description || []).map(d => ({ id: d.id || '', text: d.text || '' })) }))
|
||||
if (!target || !target.id) return
|
||||
defaultResumeId.value = target.id
|
||||
|
||||
// 并发加载简历主表 + 5大经历
|
||||
const [mainRes, eduRes, workRes, internRes, projRes, compRes] = await Promise.all([
|
||||
fetchResumeMain(defaultResumeId.value),
|
||||
fetchResumeEducation(defaultResumeId.value),
|
||||
fetchResumeWork(defaultResumeId.value),
|
||||
fetchResumeInternship(defaultResumeId.value),
|
||||
fetchResumeProject(defaultResumeId.value),
|
||||
fetchResumeCompetition(defaultResumeId.value),
|
||||
])
|
||||
|
||||
// 从简历主表获取技能、证书、个人概述
|
||||
if (mainRes.code === '0' && mainRes.data) {
|
||||
profile.value.skills = mainRes.data.skills || []
|
||||
profile.value.certificates = mainRes.data.certificates || []
|
||||
profile.value.summary = mainRes.data.summary || ''
|
||||
}
|
||||
} catch { console.error('[AgentSetupWizard] 加载实习经历失败') }
|
||||
}
|
||||
async function loadProject() {
|
||||
try {
|
||||
const res = await fetchProject()
|
||||
if (res.code === '0' && res.data) {
|
||||
profile.value.projects = res.data.map(item => ({ projectName: item.projectName || '', companyName: item.companyName || '', role: item.role || '', startDate: item.startDate || '', endDate: item.endDate || '', description: (item.description || []).map(d => ({ id: d.id || '', text: d.text || '' })) }))
|
||||
// 教育经历
|
||||
if (eduRes.code === '0' && eduRes.data) {
|
||||
profile.value.education = eduRes.data.map(item => ({
|
||||
id: item.id || '',
|
||||
school: item.school || '', major: item.major || '',
|
||||
studyType: item.studyType || '',
|
||||
degree: item.degree || '',
|
||||
startDate: item.startDate || '', endDate: item.endDate || '',
|
||||
description: (item.description || []).map(d => ({ id: d.id || '', text: d.text || '' })),
|
||||
}))
|
||||
}
|
||||
} catch { console.error('[AgentSetupWizard] 加载项目经历失败') }
|
||||
}
|
||||
async function loadCompetition() {
|
||||
try {
|
||||
const res = await fetchCompetition()
|
||||
if (res.code === '0' && res.data) {
|
||||
profile.value.competitions = res.data.map(item => ({ competitionName: item.competitionName || '', award: item.award || '', awardDate: item.awardDate || '', description: (item.description || []).map(d => ({ id: d.id || '', text: d.text || '' })) }))
|
||||
// 工作经历
|
||||
if (workRes.code === '0' && workRes.data) {
|
||||
profile.value.works = workRes.data.map(item => ({
|
||||
id: item.id || '',
|
||||
companyName: item.companyName || '', position: item.position || '',
|
||||
startDate: item.startDate || '', endDate: item.endDate || '',
|
||||
description: (item.description || []).map(d => ({ id: d.id || '', text: d.text || '' })),
|
||||
}))
|
||||
}
|
||||
} catch { console.error('[AgentSetupWizard] 加载竞赛经历失败') }
|
||||
// 实习经历
|
||||
if (internRes.code === '0' && internRes.data) {
|
||||
profile.value.internships = internRes.data.map(item => ({
|
||||
id: item.id || '',
|
||||
companyName: item.companyName || '', position: item.position || '',
|
||||
startDate: item.startDate || '', endDate: item.endDate || '',
|
||||
description: (item.description || []).map(d => ({ id: d.id || '', text: d.text || '' })),
|
||||
}))
|
||||
}
|
||||
// 项目经历
|
||||
if (projRes.code === '0' && projRes.data) {
|
||||
profile.value.projects = projRes.data.map(item => ({
|
||||
id: item.id || '',
|
||||
projectName: item.projectName || '', companyName: item.companyName || '',
|
||||
role: item.role || '', startDate: item.startDate || '', endDate: item.endDate || '',
|
||||
description: (item.description || []).map(d => ({ id: d.id || '', text: d.text || '' })),
|
||||
}))
|
||||
}
|
||||
// 竞赛经历
|
||||
if (compRes.code === '0' && compRes.data) {
|
||||
profile.value.competitions = compRes.data.map(item => ({
|
||||
id: item.id || '',
|
||||
competitionName: item.competitionName || '', award: item.award || '',
|
||||
awardDate: item.awardDate || '',
|
||||
description: (item.description || []).map(d => ({ id: d.id || '', text: d.text || '' })),
|
||||
}))
|
||||
}
|
||||
} catch { console.error('[AgentSetupWizard] 加载默认简历数据失败') }
|
||||
}
|
||||
|
||||
const profile = ref({
|
||||
name: '', phone: '', email: '', idNumber: '', regionCode: '', portfolioUrl: '', wechat: '',
|
||||
skills: [] as string[], certificates: [] as string[],
|
||||
education: [] as Array<{ school: string; major: string; studyType: number; degree: number; startDate: string; endDate: string; description: Array<{ id: string; text: string }> }>,
|
||||
works: [] as Array<{ companyName: string; position: string; startDate: string; endDate: string; description: Array<{ id: string; text: string }> }>,
|
||||
internships: [] as Array<{ companyName: string; position: string; startDate: string; endDate: string; description: Array<{ id: string; text: string }> }>,
|
||||
projects: [] as Array<{ projectName: string; companyName: string; role: string; startDate: string; endDate: string; description: Array<{ id: string; text: string }> }>,
|
||||
competitions: [] as Array<{ competitionName: string; award: string; awardDate: string; description: Array<{ id: string; text: string }> }>,
|
||||
skills: [] as string[], certificates: [] as string[], summary: '',
|
||||
education: [] as Array<{ id: string; school: string; major: string; studyType: string | number; degree: string | number; startDate: string; endDate: string; description: Array<{ id: string; text: string }> }>,
|
||||
works: [] as Array<{ id: string; companyName: string; position: string; startDate: string; endDate: string; description: Array<{ id: string; text: string }> }>,
|
||||
internships: [] as Array<{ id: string; companyName: string; position: string; startDate: string; endDate: string; description: Array<{ id: string; text: string }> }>,
|
||||
projects: [] as Array<{ id: string; projectName: string; companyName: string; role: string; startDate: string; endDate: string; description: Array<{ id: string; text: string }> }>,
|
||||
competitions: [] as Array<{ id: string; competitionName: string; award: string; awardDate: string; description: Array<{ id: string; text: string }> }>,
|
||||
})
|
||||
|
||||
// ==================== 编辑抽屉事件处理 ====================
|
||||
function handleEdit(section: string) {
|
||||
editModule.value = section
|
||||
if (section === 'info') { editInitialData.value = { name: profile.value.name, email: profile.value.email, phone: profile.value.phone, location: profile.value.regionCode, wechat: profile.value.wechat } }
|
||||
else if (section === 'education') { editInitialData.value = { education: profile.value.education.map(edu => ({ ...edu, description: edu.description.map(d => ({ ...d })) })) } }
|
||||
else if (section === 'work') { editInitialData.value = { works: profile.value.works.map(exp => ({ ...exp, description: exp.description.map(d => ({ ...d })) })) } }
|
||||
else if (section === 'internship') { editInitialData.value = { internships: (profile.value.internships || []).map(exp => ({ ...exp, description: exp.description.map(d => ({ ...d })) })) } }
|
||||
else if (section === 'project') { editInitialData.value = { projects: (profile.value.projects || []).map(proj => ({ ...proj, description: proj.description.map(d => ({ ...d })) })) } }
|
||||
else if (section === 'competition') { editInitialData.value = { competitions: profile.value.competitions.map(comp => ({ ...comp, description: comp.description.map(d => ({ ...d })) })) } }
|
||||
else if (section === 'education') {
|
||||
// 记录当前教育经历ID快照,用于保存时做diff
|
||||
editSnapshotEducationIds.value = profile.value.education.map(e => e.id).filter(Boolean)
|
||||
editInitialData.value = { education: profile.value.education.map(edu => ({ ...edu, description: edu.description.map(d => ({ ...d })) })) }
|
||||
}
|
||||
else if (section === 'work') {
|
||||
editSnapshotWorkIds.value = profile.value.works.map(w => w.id).filter(Boolean)
|
||||
editInitialData.value = { works: profile.value.works.map(exp => ({ ...exp, description: exp.description.map(d => ({ ...d })) })) }
|
||||
}
|
||||
else if (section === 'internship') {
|
||||
editSnapshotInternshipIds.value = profile.value.internships.map(i => i.id).filter(Boolean)
|
||||
editInitialData.value = { internships: (profile.value.internships || []).map(exp => ({ ...exp, description: exp.description.map(d => ({ ...d })) })) }
|
||||
}
|
||||
else if (section === 'project') {
|
||||
editSnapshotProjectIds.value = profile.value.projects.map(p => p.id).filter(Boolean)
|
||||
editInitialData.value = { projects: (profile.value.projects || []).map(proj => ({ ...proj, description: proj.description.map(d => ({ ...d })) })) }
|
||||
}
|
||||
else if (section === 'competition') {
|
||||
editSnapshotCompetitionIds.value = profile.value.competitions.map(c => c.id).filter(Boolean)
|
||||
editInitialData.value = { competitions: profile.value.competitions.map(comp => ({ ...comp, description: comp.description.map(d => ({ ...d })) })) }
|
||||
}
|
||||
else if (section === 'portfolio') { editInitialData.value = { portfolioUrl: profile.value.portfolioUrl } }
|
||||
else if (section === 'summary') { editInitialData.value = { summary: profile.value.summary || '' } }
|
||||
else if (section === 'skills') { editInitialData.value = { skills: [...profile.value.skills] } }
|
||||
else if (section === 'certificate') { editInitialData.value = { certificates: [...(profile.value.certificates || [])] } }
|
||||
else { editInitialData.value = {} }
|
||||
@@ -432,58 +534,226 @@ function handleEdit(section: string) {
|
||||
/** 保存中的加载状态 */
|
||||
const saving = ref(false)
|
||||
|
||||
/** 保存编辑数据 — 调用接口持久化 */
|
||||
/** 保存编辑数据 — 个人信息走 profile 接口,其余走简历接口 */
|
||||
async function handleSaveEdit(data: Record<string, any>) {
|
||||
if (editModule.value === 'info') {
|
||||
try { saving.value = true; await saveProfile({ name: data.name, email: data.email, mobileNumber: data.phone, regionCode: data.location, wechatNumber: data.wechat }); profile.value.name = data.name; profile.value.email = data.email; profile.value.phone = data.phone; profile.value.wechat = data.wechat; profile.value.regionCode = data.location || ''; ElMessage.success('个人信息保存成功') } catch { ElMessage.error('个人信息保存失败,请重试') } finally { saving.value = false }
|
||||
// 个人信息 — 保存到 /user/profile
|
||||
try {
|
||||
saving.value = true
|
||||
await saveProfile({ name: data.name, email: data.email, mobileNumber: data.phone, regionCode: data.location, wechatNumber: data.wechat })
|
||||
profile.value.name = data.name; profile.value.email = data.email
|
||||
profile.value.phone = data.phone; profile.value.wechat = data.wechat
|
||||
profile.value.regionCode = data.location || ''
|
||||
ElMessage.success('个人信息保存成功')
|
||||
} catch { ElMessage.error('个人信息保存失败,请重试') } finally { saving.value = false }
|
||||
} else if (editModule.value === 'education') {
|
||||
try { saving.value = true; const payload: SaveEducationItem[] = data.education.map((edu: any) => ({ school: edu.school, major: edu.major, degree: edu.degree, studyType: edu.studyType, startDate: edu.startDate, endDate: edu.endDate, description: edu.description.map((d: any) => ({ id: d.id, text: d.text })) })); await saveEducation(payload); profile.value.education = data.education.map((edu: any) => ({ school: edu.school, major: edu.major, studyType: edu.studyType, degree: edu.degree, startDate: edu.startDate, endDate: edu.endDate, description: edu.description.map((d: any) => ({ ...d })) })); ElMessage.success('教育经历保存成功') } catch { ElMessage.error('教育经历保存失败,请重试') } finally { saving.value = false }
|
||||
// 教育经历 — diff增删改(简历接口)
|
||||
if (!defaultResumeId.value) { ElMessage.error('未找到默认简历'); return }
|
||||
try {
|
||||
saving.value = true
|
||||
const oldIds = new Set(editSnapshotEducationIds.value)
|
||||
const newItems: any[] = data.education
|
||||
const newIds = new Set(newItems.filter((e: any) => e.id).map((e: any) => e.id))
|
||||
const deletedIds = editSnapshotEducationIds.value.filter(id => !newIds.has(id))
|
||||
const addPromises: Promise<any>[] = []
|
||||
const updatePromises: Promise<any>[] = []
|
||||
const deletePromises = deletedIds.map(id => deleteResumeEducation(id))
|
||||
for (const edu of newItems) {
|
||||
const itemData = { school: edu.school, major: edu.major, degree: edu.degree, studyType: edu.studyType, startDate: edu.startDate, endDate: edu.endDate, description: edu.description }
|
||||
if (edu.id && oldIds.has(edu.id)) {
|
||||
updatePromises.push(updateResumeEducation({ ...itemData, id: edu.id }))
|
||||
} else {
|
||||
addPromises.push(addResumeEducation(defaultResumeId.value, itemData))
|
||||
}
|
||||
}
|
||||
await Promise.all([...addPromises, ...updatePromises, ...deletePromises])
|
||||
// 重新加载教育经历以获取最新ID
|
||||
const eduRes = await fetchResumeEducation(defaultResumeId.value)
|
||||
if (eduRes.code === '0' && eduRes.data) {
|
||||
profile.value.education = eduRes.data.map(item => ({ id: item.id || '', school: item.school || '', major: item.major || '', studyType: item.studyType || '', degree: item.degree || '', startDate: item.startDate || '', endDate: item.endDate || '', description: (item.description || []).map(d => ({ id: d.id || '', text: d.text || '' })) }))
|
||||
}
|
||||
ElMessage.success('教育经历保存成功')
|
||||
} catch { ElMessage.error('教育经历保存失败,请重试') } finally { saving.value = false }
|
||||
} else if (editModule.value === 'work') {
|
||||
try { saving.value = true; const payload: SaveWorkItem[] = data.works.map((w: any) => ({ companyName: w.companyName, position: w.position, startDate: w.startDate, endDate: w.endDate || '', description: w.description.map((d: any) => ({ id: d.id, text: d.text })) })); await saveWork(payload); profile.value.works = data.works.map((w: any) => ({ companyName: w.companyName, position: w.position, startDate: w.startDate, endDate: w.endDate, description: w.description.map((d: any) => ({ ...d })) })); ElMessage.success('工作经历保存成功') } catch { ElMessage.error('工作经历保存失败,请重试') } finally { saving.value = false }
|
||||
// 工作经历 — diff增删改(简历接口)
|
||||
if (!defaultResumeId.value) { ElMessage.error('未找到默认简历'); return }
|
||||
try {
|
||||
saving.value = true
|
||||
const oldIds = new Set(editSnapshotWorkIds.value)
|
||||
const newItems: any[] = data.works
|
||||
const newIds = new Set(newItems.filter((w: any) => w.id).map((w: any) => w.id))
|
||||
const deletedIds = editSnapshotWorkIds.value.filter(id => !newIds.has(id))
|
||||
const addPromises: Promise<any>[] = []
|
||||
const updatePromises: Promise<any>[] = []
|
||||
const deletePromises = deletedIds.map(id => deleteResumeWork(id))
|
||||
for (const w of newItems) {
|
||||
const itemData = { companyName: w.companyName, position: w.position, startDate: w.startDate, endDate: w.endDate || '', description: w.description }
|
||||
if (w.id && oldIds.has(w.id)) {
|
||||
updatePromises.push(updateResumeWork({ ...itemData, id: w.id }))
|
||||
} else {
|
||||
addPromises.push(addResumeWork(defaultResumeId.value, itemData))
|
||||
}
|
||||
}
|
||||
await Promise.all([...addPromises, ...updatePromises, ...deletePromises])
|
||||
const workRes = await fetchResumeWork(defaultResumeId.value)
|
||||
if (workRes.code === '0' && workRes.data) {
|
||||
profile.value.works = workRes.data.map(item => ({ id: item.id || '', companyName: item.companyName || '', position: item.position || '', startDate: item.startDate || '', endDate: item.endDate || '', description: (item.description || []).map(d => ({ id: d.id || '', text: d.text || '' })) }))
|
||||
}
|
||||
ElMessage.success('工作经历保存成功')
|
||||
} catch { ElMessage.error('工作经历保存失败,请重试') } finally { saving.value = false }
|
||||
} else if (editModule.value === 'internship') {
|
||||
try { saving.value = true; const payload: SaveWorkItem[] = data.internships.map((i: any) => ({ companyName: i.companyName, position: i.position, startDate: i.startDate, endDate: i.endDate || '', description: i.description.map((d: any) => ({ id: d.id, text: d.text })) })); await saveInternship(payload); profile.value.internships = data.internships.map((i: any) => ({ companyName: i.companyName, position: i.position, startDate: i.startDate, endDate: i.endDate, description: i.description.map((d: any) => ({ ...d })) })); ElMessage.success('实习经历保存成功') } catch { ElMessage.error('实习经历保存失败,请重试') } finally { saving.value = false }
|
||||
// 实习经历 — diff增删改(简历接口)
|
||||
if (!defaultResumeId.value) { ElMessage.error('未找到默认简历'); return }
|
||||
try {
|
||||
saving.value = true
|
||||
const oldIds = new Set(editSnapshotInternshipIds.value)
|
||||
const newItems: any[] = data.internships
|
||||
const newIds = new Set(newItems.filter((i: any) => i.id).map((i: any) => i.id))
|
||||
const deletedIds = editSnapshotInternshipIds.value.filter(id => !newIds.has(id))
|
||||
const addPromises: Promise<any>[] = []
|
||||
const updatePromises: Promise<any>[] = []
|
||||
const deletePromises = deletedIds.map(id => deleteResumeInternship(id))
|
||||
for (const i of newItems) {
|
||||
const itemData = { companyName: i.companyName, position: i.position, startDate: i.startDate, endDate: i.endDate || '', description: i.description }
|
||||
if (i.id && oldIds.has(i.id)) {
|
||||
updatePromises.push(updateResumeInternship({ ...itemData, id: i.id }))
|
||||
} else {
|
||||
addPromises.push(addResumeInternship(defaultResumeId.value, itemData))
|
||||
}
|
||||
}
|
||||
await Promise.all([...addPromises, ...updatePromises, ...deletePromises])
|
||||
const internRes = await fetchResumeInternship(defaultResumeId.value)
|
||||
if (internRes.code === '0' && internRes.data) {
|
||||
profile.value.internships = internRes.data.map(item => ({ id: item.id || '', companyName: item.companyName || '', position: item.position || '', startDate: item.startDate || '', endDate: item.endDate || '', description: (item.description || []).map(d => ({ id: d.id || '', text: d.text || '' })) }))
|
||||
}
|
||||
ElMessage.success('实习经历保存成功')
|
||||
} catch { ElMessage.error('实习经历保存失败,请重试') } finally { saving.value = false }
|
||||
} else if (editModule.value === 'project') {
|
||||
try { saving.value = true; const payload: SaveProjectItem[] = data.projects.map((p: any) => ({ projectName: p.projectName, companyName: p.companyName || '', role: p.role || '', startDate: p.startDate, endDate: p.endDate || '', description: p.description.map((d: any) => ({ id: d.id, text: d.text })) })); await saveProject(payload); profile.value.projects = data.projects.map((p: any) => ({ projectName: p.projectName, companyName: p.companyName, role: p.role, startDate: p.startDate, endDate: p.endDate, description: p.description.map((d: any) => ({ ...d })) })); ElMessage.success('项目经历保存成功') } catch { ElMessage.error('项目经历保存失败,请重试') } finally { saving.value = false }
|
||||
// 项目经历 — diff增删改(简历接口)
|
||||
if (!defaultResumeId.value) { ElMessage.error('未找到默认简历'); return }
|
||||
try {
|
||||
saving.value = true
|
||||
const oldIds = new Set(editSnapshotProjectIds.value)
|
||||
const newItems: any[] = data.projects
|
||||
const newIds = new Set(newItems.filter((p: any) => p.id).map((p: any) => p.id))
|
||||
const deletedIds = editSnapshotProjectIds.value.filter(id => !newIds.has(id))
|
||||
const addPromises: Promise<any>[] = []
|
||||
const updatePromises: Promise<any>[] = []
|
||||
const deletePromises = deletedIds.map(id => deleteResumeProject(id))
|
||||
for (const p of newItems) {
|
||||
const itemData = { projectName: p.projectName, companyName: p.companyName || '', role: p.role || '', startDate: p.startDate, endDate: p.endDate || '', description: p.description }
|
||||
if (p.id && oldIds.has(p.id)) {
|
||||
updatePromises.push(updateResumeProject({ ...itemData, id: p.id }))
|
||||
} else {
|
||||
addPromises.push(addResumeProject(defaultResumeId.value, itemData))
|
||||
}
|
||||
}
|
||||
await Promise.all([...addPromises, ...updatePromises, ...deletePromises])
|
||||
const projRes = await fetchResumeProject(defaultResumeId.value)
|
||||
if (projRes.code === '0' && projRes.data) {
|
||||
profile.value.projects = projRes.data.map(item => ({ id: item.id || '', projectName: item.projectName || '', companyName: item.companyName || '', role: item.role || '', startDate: item.startDate || '', endDate: item.endDate || '', description: (item.description || []).map(d => ({ id: d.id || '', text: d.text || '' })) }))
|
||||
}
|
||||
ElMessage.success('项目经历保存成功')
|
||||
} catch { ElMessage.error('项目经历保存失败,请重试') } finally { saving.value = false }
|
||||
} else if (editModule.value === 'competition') {
|
||||
try { saving.value = true; const payload: SaveCompetitionItem[] = data.competitions.map((c: any) => ({ competitionName: c.competitionName, award: c.award || '', awardDate: c.awardDate || '', description: c.description.map((d: any) => ({ id: d.id, text: d.text })) })); await saveCompetition(payload); profile.value.competitions = data.competitions.map((c: any) => ({ competitionName: c.competitionName, award: c.award, awardDate: c.awardDate, description: c.description.map((d: any) => ({ ...d })) })); ElMessage.success('竞赛经历保存成功') } catch { ElMessage.error('竞赛经历保存失败,请重试') } finally { saving.value = false }
|
||||
// 竞赛经历 — diff增删改(简历接口)
|
||||
if (!defaultResumeId.value) { ElMessage.error('未找到默认简历'); return }
|
||||
try {
|
||||
saving.value = true
|
||||
const oldIds = new Set(editSnapshotCompetitionIds.value)
|
||||
const newItems: any[] = data.competitions
|
||||
const newIds = new Set(newItems.filter((c: any) => c.id).map((c: any) => c.id))
|
||||
const deletedIds = editSnapshotCompetitionIds.value.filter(id => !newIds.has(id))
|
||||
const addPromises: Promise<any>[] = []
|
||||
const updatePromises: Promise<any>[] = []
|
||||
const deletePromises = deletedIds.map(id => deleteResumeCompetition(id))
|
||||
for (const c of newItems) {
|
||||
const itemData = { competitionName: c.competitionName, award: c.award || '', awardDate: c.awardDate || '', description: c.description }
|
||||
if (c.id && oldIds.has(c.id)) {
|
||||
updatePromises.push(updateResumeCompetition({ ...itemData, id: c.id }))
|
||||
} else {
|
||||
addPromises.push(addResumeCompetition(defaultResumeId.value, itemData))
|
||||
}
|
||||
}
|
||||
await Promise.all([...addPromises, ...updatePromises, ...deletePromises])
|
||||
const compRes = await fetchResumeCompetition(defaultResumeId.value)
|
||||
if (compRes.code === '0' && compRes.data) {
|
||||
profile.value.competitions = compRes.data.map(item => ({ id: item.id || '', competitionName: item.competitionName || '', award: item.award || '', awardDate: item.awardDate || '', description: (item.description || []).map(d => ({ id: d.id || '', text: d.text || '' })) }))
|
||||
}
|
||||
ElMessage.success('竞赛经历保存成功')
|
||||
} catch { ElMessage.error('竞赛经历保存失败,请重试') } finally { saving.value = false }
|
||||
} else if (editModule.value === 'summary') {
|
||||
// 个人概述 — 保存到简历主表
|
||||
if (!defaultResumeId.value) { ElMessage.error('未找到默认简历'); return }
|
||||
try {
|
||||
saving.value = true
|
||||
await saveResumeMain({ resumeId: defaultResumeId.value, skills: profile.value.skills, certificates: profile.value.certificates, summary: data.summary })
|
||||
profile.value.summary = data.summary
|
||||
ElMessage.success('个人概述保存成功')
|
||||
} catch { ElMessage.error('个人概述保存失败,请重试') } finally { saving.value = false }
|
||||
} else if (editModule.value === 'portfolio') {
|
||||
try { saving.value = true; await saveProfile({ name: profile.value.name, email: profile.value.email, mobileNumber: profile.value.phone, regionCode: profile.value.regionCode, wechatNumber: profile.value.wechat, skills: profile.value.skills, certificates: profile.value.certificates, portfolioUrl: data.portfolioUrl }); profile.value.portfolioUrl = data.portfolioUrl; ElMessage.success('作品集保存成功') } catch { ElMessage.error('作品集保存失败,请重试') } finally { saving.value = false }
|
||||
// 作品集链接 — 保存到 /user/profile
|
||||
try {
|
||||
saving.value = true
|
||||
await saveProfile({ name: profile.value.name, email: profile.value.email, mobileNumber: profile.value.phone, regionCode: profile.value.regionCode, wechatNumber: profile.value.wechat, portfolioUrl: data.portfolioUrl })
|
||||
profile.value.portfolioUrl = data.portfolioUrl
|
||||
ElMessage.success('作品集保存成功')
|
||||
} catch { ElMessage.error('作品集保存失败,请重试') } finally { saving.value = false }
|
||||
} else if (editModule.value === 'skills') {
|
||||
try { saving.value = true; await saveProfile({ name: profile.value.name, email: profile.value.email, mobileNumber: profile.value.phone, regionCode: profile.value.regionCode, wechatNumber: profile.value.wechat, skills: [...data.skills], certificates: profile.value.certificates }); profile.value.skills = [...data.skills]; ElMessage.success('技能保存成功') } catch { ElMessage.error('技能保存失败,请重试') } finally { saving.value = false }
|
||||
// 技能 — 保存到简历主表
|
||||
if (!defaultResumeId.value) { ElMessage.error('未找到默认简历'); return }
|
||||
try {
|
||||
saving.value = true
|
||||
await saveResumeMain({ resumeId: defaultResumeId.value, skills: [...data.skills], certificates: profile.value.certificates, summary: profile.value.summary })
|
||||
profile.value.skills = [...data.skills]
|
||||
ElMessage.success('技能保存成功')
|
||||
} catch { ElMessage.error('技能保存失败,请重试') } finally { saving.value = false }
|
||||
} else if (editModule.value === 'certificate') {
|
||||
try { saving.value = true; await saveProfile({ name: profile.value.name, email: profile.value.email, mobileNumber: profile.value.phone, regionCode: profile.value.regionCode, wechatNumber: profile.value.wechat, skills: profile.value.skills, certificates: [...data.certificates] }); profile.value.certificates = [...data.certificates]; ElMessage.success('证书保存成功') } catch { ElMessage.error('证书保存失败,请重试') } finally { saving.value = false }
|
||||
// 证书 — 保存到简历主表
|
||||
if (!defaultResumeId.value) { ElMessage.error('未找到默认简历'); return }
|
||||
try {
|
||||
saving.value = true
|
||||
await saveResumeMain({ resumeId: defaultResumeId.value, skills: profile.value.skills, certificates: [...data.certificates], summary: profile.value.summary })
|
||||
profile.value.certificates = [...data.certificates]
|
||||
ElMessage.success('证书保存成功')
|
||||
} catch { ElMessage.error('证书保存失败,请重试') } finally { saving.value = false }
|
||||
}
|
||||
}
|
||||
|
||||
/** 编辑抽屉打开时的经历ID快照 — 用于保存时做增删改diff */
|
||||
const editSnapshotEducationIds = ref<string[]>([])
|
||||
const editSnapshotWorkIds = ref<string[]>([])
|
||||
const editSnapshotInternshipIds = ref<string[]>([])
|
||||
const editSnapshotProjectIds = ref<string[]>([])
|
||||
const editSnapshotCompetitionIds = ref<string[]>([])
|
||||
|
||||
/** 内联保存处理(个人信息/技能/证书模块从组件内直接保存) */
|
||||
async function handleInlineSave(section: string, data: Record<string, any>) {
|
||||
editModule.value = section
|
||||
await handleSaveEdit(data)
|
||||
}
|
||||
|
||||
/** 单条经历更新 — 替换到完整列表后整体保存 */
|
||||
/** 单条经历更新 — 调用简历单条编辑接口 */
|
||||
async function handleExpUpdate(moduleType: string, index: number, data: Record<string, any>) {
|
||||
if (!defaultResumeId.value) { ElMessage.error('未找到默认简历'); return }
|
||||
try {
|
||||
const record = (profile.value as any)[moduleType === 'work' ? 'works' : moduleType === 'internship' ? 'internships' : moduleType === 'project' ? 'projects' : moduleType === 'competition' ? 'competitions' : 'education'][index]
|
||||
const recordId = record?.id
|
||||
if (moduleType === 'education') {
|
||||
const list = profile.value.education.map((item, i) => i === index ? data : item) as any[]
|
||||
await saveEducation(list)
|
||||
profile.value.education = list
|
||||
await updateResumeEducation({ id: recordId, school: data.school, major: data.major, degree: data.degree, studyType: data.studyType, startDate: data.startDate, endDate: data.endDate, description: data.description })
|
||||
profile.value.education[index] = { id: recordId, ...data } as any
|
||||
} else if (moduleType === 'work') {
|
||||
const list = profile.value.works.map((item, i) => i === index ? data : item) as any[]
|
||||
await saveWork(list)
|
||||
profile.value.works = list
|
||||
await updateResumeWork({ id: recordId, companyName: data.companyName, position: data.position, startDate: data.startDate, endDate: data.endDate, description: data.description })
|
||||
profile.value.works[index] = { id: recordId, ...data } as any
|
||||
} else if (moduleType === 'internship') {
|
||||
const list = profile.value.internships.map((item, i) => i === index ? data : item) as any[]
|
||||
await saveInternship(list)
|
||||
profile.value.internships = list
|
||||
await updateResumeInternship({ id: recordId, companyName: data.companyName, position: data.position, startDate: data.startDate, endDate: data.endDate, description: data.description })
|
||||
profile.value.internships[index] = { id: recordId, ...data } as any
|
||||
} else if (moduleType === 'project') {
|
||||
const list = profile.value.projects.map((item, i) => i === index ? data : item) as any[]
|
||||
await saveProject(list)
|
||||
profile.value.projects = list
|
||||
await updateResumeProject({ id: recordId, projectName: data.projectName, companyName: data.companyName, role: data.role, startDate: data.startDate, endDate: data.endDate, description: data.description })
|
||||
profile.value.projects[index] = { id: recordId, ...data } as any
|
||||
} else if (moduleType === 'competition') {
|
||||
const list = profile.value.competitions.map((item, i) => i === index ? data : item) as any[]
|
||||
await saveCompetition(list)
|
||||
profile.value.competitions = list
|
||||
await updateResumeCompetition({ id: recordId, competitionName: data.competitionName, award: data.award, awardDate: data.awardDate, description: data.description })
|
||||
profile.value.competitions[index] = { id: recordId, ...data } as any
|
||||
}
|
||||
ElMessage.success('保存成功')
|
||||
} catch {
|
||||
@@ -491,29 +761,31 @@ async function handleExpUpdate(moduleType: string, index: number, data: Record<s
|
||||
}
|
||||
}
|
||||
|
||||
/** 新增经历 */
|
||||
/** 新增经历 — 调用简历单条添加接口 */
|
||||
async function handleExpAdd(moduleType: string, data: Record<string, any>) {
|
||||
if (!defaultResumeId.value) { ElMessage.error('未找到默认简历'); return }
|
||||
try {
|
||||
let newId = ''
|
||||
if (moduleType === 'education') {
|
||||
const list = [...profile.value.education, data] as any[]
|
||||
await saveEducation(list)
|
||||
profile.value.education = list
|
||||
const res = await addResumeEducation(defaultResumeId.value, { school: data.school, major: data.major, degree: data.degree, studyType: data.studyType, startDate: data.startDate, endDate: data.endDate, description: data.description })
|
||||
newId = res.data || ''
|
||||
profile.value.education.push({ id: newId, ...data } as any)
|
||||
} else if (moduleType === 'work') {
|
||||
const list = [...profile.value.works, data] as any[]
|
||||
await saveWork(list)
|
||||
profile.value.works = list
|
||||
const res = await addResumeWork(defaultResumeId.value, { companyName: data.companyName, position: data.position, startDate: data.startDate, endDate: data.endDate, description: data.description })
|
||||
newId = res.data || ''
|
||||
profile.value.works.push({ id: newId, ...data } as any)
|
||||
} else if (moduleType === 'internship') {
|
||||
const list = [...profile.value.internships, data] as any[]
|
||||
await saveInternship(list)
|
||||
profile.value.internships = list
|
||||
const res = await addResumeInternship(defaultResumeId.value, { companyName: data.companyName, position: data.position, startDate: data.startDate, endDate: data.endDate, description: data.description })
|
||||
newId = res.data || ''
|
||||
profile.value.internships.push({ id: newId, ...data } as any)
|
||||
} else if (moduleType === 'project') {
|
||||
const list = [...profile.value.projects, data] as any[]
|
||||
await saveProject(list)
|
||||
profile.value.projects = list
|
||||
const res = await addResumeProject(defaultResumeId.value, { projectName: data.projectName, companyName: data.companyName, role: data.role, startDate: data.startDate, endDate: data.endDate, description: data.description })
|
||||
newId = res.data || ''
|
||||
profile.value.projects.push({ id: newId, ...data } as any)
|
||||
} else if (moduleType === 'competition') {
|
||||
const list = [...profile.value.competitions, data] as any[]
|
||||
await saveCompetition(list)
|
||||
profile.value.competitions = list
|
||||
const res = await addResumeCompetition(defaultResumeId.value, { competitionName: data.competitionName, award: data.award, awardDate: data.awardDate, description: data.description })
|
||||
newId = res.data || ''
|
||||
profile.value.competitions.push({ id: newId, ...data } as any)
|
||||
}
|
||||
ElMessage.success('添加成功')
|
||||
} catch {
|
||||
@@ -521,7 +793,7 @@ async function handleExpAdd(moduleType: string, data: Record<string, any>) {
|
||||
}
|
||||
}
|
||||
|
||||
/** 删除单条经历 — 确认后从列表移除并全量保存 */
|
||||
/** 删除单条经历 — 调用简历单条删除接口 */
|
||||
async function handleExpDelete(moduleType: string, index: number) {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要删除这条经历吗?', '提示', {
|
||||
@@ -532,27 +804,28 @@ async function handleExpDelete(moduleType: string, index: number) {
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
if (!defaultResumeId.value) { ElMessage.error('未找到默认简历'); return }
|
||||
try {
|
||||
if (moduleType === 'education') {
|
||||
const list = profile.value.education.filter((_, i) => i !== index) as any[]
|
||||
await saveEducation(list)
|
||||
profile.value.education = list
|
||||
const id = profile.value.education[index]?.id
|
||||
if (id) await deleteResumeEducation(id)
|
||||
profile.value.education.splice(index, 1)
|
||||
} else if (moduleType === 'work') {
|
||||
const list = profile.value.works.filter((_, i) => i !== index) as any[]
|
||||
await saveWork(list)
|
||||
profile.value.works = list
|
||||
const id = profile.value.works[index]?.id
|
||||
if (id) await deleteResumeWork(id)
|
||||
profile.value.works.splice(index, 1)
|
||||
} else if (moduleType === 'internship') {
|
||||
const list = profile.value.internships.filter((_, i) => i !== index) as any[]
|
||||
await saveInternship(list)
|
||||
profile.value.internships = list
|
||||
const id = profile.value.internships[index]?.id
|
||||
if (id) await deleteResumeInternship(id)
|
||||
profile.value.internships.splice(index, 1)
|
||||
} else if (moduleType === 'project') {
|
||||
const list = profile.value.projects.filter((_, i) => i !== index) as any[]
|
||||
await saveProject(list)
|
||||
profile.value.projects = list
|
||||
const id = profile.value.projects[index]?.id
|
||||
if (id) await deleteResumeProject(id)
|
||||
profile.value.projects.splice(index, 1)
|
||||
} else if (moduleType === 'competition') {
|
||||
const list = profile.value.competitions.filter((_, i) => i !== index) as any[]
|
||||
await saveCompetition(list)
|
||||
profile.value.competitions = list
|
||||
const id = profile.value.competitions[index]?.id
|
||||
if (id) await deleteResumeCompetition(id)
|
||||
profile.value.competitions.splice(index, 1)
|
||||
}
|
||||
ElMessage.success('删除成功')
|
||||
} catch {
|
||||
@@ -581,6 +854,11 @@ const selectedResumeName = computed(() => {
|
||||
function selectResume(r: { id?: string; resumeName?: string }) {
|
||||
selectedResumeId.value = r.id || ''
|
||||
showResumeDropdown.value = false
|
||||
|
||||
// 个人信息仍从 /user/profile 获取
|
||||
loadProfile()
|
||||
// 其他数据(技能、证书、个人概述、5大经历)从默认简历获取
|
||||
loadDefaultResumeData()
|
||||
}
|
||||
|
||||
/** 选中招聘分类 */
|
||||
@@ -639,13 +917,50 @@ const autoOptimizeSwitch = ref(true)
|
||||
/** 岗位匹配分析开关 */
|
||||
const autoMatchAnalysisSwitch = ref(true)
|
||||
|
||||
/** 切换简历针对性优化开关 */
|
||||
function toggleOptimize() {
|
||||
autoOptimizeSwitch.value = !autoOptimizeSwitch.value
|
||||
savePreferenceSwitches()
|
||||
}
|
||||
|
||||
/** 切换深度人岗匹配分析开关 */
|
||||
function toggleMatchAnalysis() {
|
||||
autoMatchAnalysisSwitch.value = !autoMatchAnalysisSwitch.value
|
||||
savePreferenceSwitches()
|
||||
}
|
||||
|
||||
/** 保存投递偏好开关到接口 */
|
||||
async function savePreferenceSwitches() {
|
||||
try {
|
||||
await saveAgentConfig({
|
||||
autoOptimizeResume: autoOptimizeSwitch.value ? 1 : 0,
|
||||
acceptDeptTransfer: autoMatchAnalysisSwitch.value ? '是' : '否',
|
||||
})
|
||||
} catch { /* 静默 */ }
|
||||
}
|
||||
|
||||
/** 加载投递偏好开关状态 */
|
||||
async function loadPreferenceSwitches() {
|
||||
try {
|
||||
const res = await fetchAgentConfig()
|
||||
if (res.code === '0' && res.data) {
|
||||
if (res.data.autoOptimizeResume !== undefined) autoOptimizeSwitch.value = res.data.autoOptimizeResume === 1
|
||||
autoMatchAnalysisSwitch.value = res.data.acceptDeptTransfer === '是'
|
||||
}
|
||||
} catch { /* 静默 */ }
|
||||
}
|
||||
|
||||
/** 加载简历列表 */
|
||||
async function loadResumeList() {
|
||||
try {
|
||||
const res = await fetchResumeList()
|
||||
if (res.code === '0' && res.data) {
|
||||
resumeList.value = res.data
|
||||
const defaultResume = res.data.find(r => r.isDefault === 1)
|
||||
let defaultResume = res.data.find(r => r.isDefault === 1)
|
||||
if(!defaultResume){
|
||||
// 按 createTime 降序取第一个
|
||||
defaultResume = [...resumeList.value].sort((a, b) => (b.createTime || 0) - (a.createTime || 0))[0]
|
||||
}
|
||||
if (defaultResume && defaultResume.id) {
|
||||
selectedResumeId.value = defaultResume.id
|
||||
} else if (res.data.length > 0 && res.data[0].id) {
|
||||
@@ -656,7 +971,7 @@ async function loadResumeList() {
|
||||
}
|
||||
|
||||
/** 进入第2步时加载简历列表 */
|
||||
watch(currentStep, (val) => { if (val === 2) loadResumeList() })
|
||||
// watch(currentStep, (val) => { if (val === 1) loadResumeList() })
|
||||
|
||||
/** 选择简历后设为默认简历 */
|
||||
watch(selectedResumeId, async (newId, oldId) => {
|
||||
|
||||
@@ -167,11 +167,11 @@ async function loadPendingList() {
|
||||
}
|
||||
}
|
||||
|
||||
/** 加载已完成列表(pageSize=300) */
|
||||
/** 加载已完成列表(pageSize=200) */
|
||||
async function loadCompletedList() {
|
||||
completedLoading.value = true
|
||||
try {
|
||||
const res = await fetchAgentTaskList({ pageNum: 1, pageSize: 300, tab: 2 })
|
||||
const res = await fetchAgentTaskList({ pageNum: 1, pageSize: 200, tab: 2 })
|
||||
if (res.code === '0' && res.data) {
|
||||
completedList.value = res.data.list || []
|
||||
}
|
||||
|
||||
@@ -17,22 +17,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 岗位统计卡片区域 -->
|
||||
<div class="job-stats-cards">
|
||||
<!-- 全网实时岗位卡片 -->
|
||||
<div class="job-stats-cards__count-card prl10 dflex-center aliite-c">
|
||||
<span class="job-stats-cards__dot"></span>
|
||||
<div class="ml8">
|
||||
<div class="job-stats-cards__count-num">{{ formattedJobCount }}</div>
|
||||
<div class="job-stats-cards__count-label">全网实时岗位</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 可投率卡片 -->
|
||||
<div class="job-stats-cards__match-card">
|
||||
<span class="job-stats-cards__match-num">92%</span>
|
||||
<span class="job-stats-cards__match-label">可投率</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</template>
|
||||
@@ -41,7 +26,7 @@
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { useStore } from 'vuex'
|
||||
import { fetchFavoriteCount, fetchApplyCount, fetchJobCount } from '@/api/jobs'
|
||||
import { fetchFavoriteCount, fetchApplyCount } from '@/api/jobs'
|
||||
|
||||
// ==================== Props & Emits ====================
|
||||
|
||||
@@ -66,32 +51,12 @@ const isAuthenticated = computed(() => store.state.isAuthenticated)
|
||||
|
||||
// ==================== 收藏 & 投递统计 ====================
|
||||
|
||||
/** 全网岗位总数 */
|
||||
const jobCount = ref(0)
|
||||
|
||||
/** 格式化岗位总数(每3位加逗号) */
|
||||
const formattedJobCount = computed(() => {
|
||||
return jobCount.value.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',')
|
||||
})
|
||||
|
||||
/** 收藏总数 */
|
||||
const favoriteTotal = ref(0)
|
||||
|
||||
/** 投递总数 */
|
||||
const applyTotal = ref(0)
|
||||
|
||||
/** 加载岗位总数 */
|
||||
async function loadJobCount() {
|
||||
try {
|
||||
const res = await fetchJobCount()
|
||||
if (res.code === '0' && res.data !== undefined) {
|
||||
jobCount.value = Number(res.data)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('加载岗位总数失败', e)
|
||||
}
|
||||
}
|
||||
|
||||
/** 加载收藏统计 */
|
||||
async function loadFavoriteCount() {
|
||||
try {
|
||||
@@ -118,8 +83,6 @@ async function loadApplyCount() {
|
||||
|
||||
/** 组件挂载时请求收藏和投递统计 */
|
||||
onMounted(() => {
|
||||
// 岗位总数不需要登录态
|
||||
loadJobCount()
|
||||
if (isAuthenticated.value) {
|
||||
loadFavoriteCount()
|
||||
loadApplyCount()
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
<template>
|
||||
<!-- 岗位概览统计卡片 -->
|
||||
<div class="job-stats-overview" :style="{ height: cardHeight }">
|
||||
<!-- 右上角装饰图片 -->
|
||||
<img class="job-stats-overview__icon" src="@/assets/images/job-count-card-icon01.png" alt="" />
|
||||
<!-- 内容居中容器 -->
|
||||
<div class="job-stats-overview__inner">
|
||||
<!-- 标题区域 -->
|
||||
<div class="job-stats-overview__header">
|
||||
<div class="job-stats-overview__title">{{ title }}</div>
|
||||
<div class="job-stats-overview__date">{{ subtitle }}</div>
|
||||
</div>
|
||||
<!-- 数据指标区域 -->
|
||||
<div class="job-stats-overview__metrics">
|
||||
<!-- 今日新增 -->
|
||||
<div class="job-stats-overview__metric-item">
|
||||
<div class="job-stats-overview__metric-num">{{ todayCount }}</div>
|
||||
<div class="job-stats-overview__metric-label">{{ todayLabel }}</div>
|
||||
</div>
|
||||
<!-- 分隔线 -->
|
||||
<div class="job-stats-overview__divider"></div>
|
||||
<!-- 累计收录 -->
|
||||
<div class="job-stats-overview__metric-item">
|
||||
<div class="job-stats-overview__metric-num">{{ totalCount }}</div>
|
||||
<div class="job-stats-overview__metric-label">{{ totalLabel }}</div>
|
||||
</div>
|
||||
<!-- 分隔线 -->
|
||||
<div class="job-stats-overview__divider"></div>
|
||||
<!-- 可投率 -->
|
||||
<div class="job-stats-overview__metric-item">
|
||||
<div class="job-stats-overview__metric-num">{{ rateValue }}</div>
|
||||
<div class="job-stats-overview__metric-label">{{ rateLabel }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
/** 岗位概览统计卡片 - 所有展示数据由父组件传入 */
|
||||
|
||||
defineProps<{
|
||||
/** 卡片标题,如"岗位概览" */
|
||||
title: string
|
||||
/** 副标题,如"数据截至 07.28" */
|
||||
subtitle: string
|
||||
/** 今日新增数字 */
|
||||
todayCount: string | number
|
||||
/** 今日新增标签 */
|
||||
todayLabel: string
|
||||
/** 累计收录数字 */
|
||||
totalCount: string | number
|
||||
/** 累计收录标签 */
|
||||
totalLabel: string
|
||||
/** 可投率数字 */
|
||||
rateValue: string | number
|
||||
/** 可投率标签 */
|
||||
rateLabel: string
|
||||
/** 卡片高度(从父级传入,与筛选面板同步) */
|
||||
cardHeight?: string
|
||||
}>()
|
||||
</script>
|
||||
+236
-106
@@ -1,152 +1,282 @@
|
||||
<template>
|
||||
<!--
|
||||
⚠️ 该弹窗登录组件已搁置,登录功能已改为独立页面 /login (src/views/Login.vue)。
|
||||
如需恢复弹窗登录,重新在 App.vue 中引用此组件即可。
|
||||
-->
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
width="480px"
|
||||
:show-close="true"
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="true"
|
||||
class="login-dialog"
|
||||
@close="handleClose"
|
||||
>
|
||||
<div class="login-page">
|
||||
<h1 class="login-title">欢迎使用Offer派</h1>
|
||||
|
||||
<div class="login-form">
|
||||
<el-input
|
||||
v-model="phone"
|
||||
placeholder="输入手机号"
|
||||
size="large"
|
||||
class="login-input mt20"
|
||||
/>
|
||||
<div class="code-row mt12 mb12">
|
||||
<el-input
|
||||
v-model="code"
|
||||
placeholder="输入验证码"
|
||||
size="large"
|
||||
class="login-input"
|
||||
/>
|
||||
<el-button
|
||||
class="send-code-btn"
|
||||
:disabled="countdown > 0"
|
||||
@click="sendCode"
|
||||
>
|
||||
{{ countdown > 0 ? countdownText : '发送验证码' }}
|
||||
</el-button>
|
||||
<!-- 登录弹窗 — 使用登录页 step1&2 的样式,弹窗形式 -->
|
||||
<Teleport to="body">
|
||||
<div v-if="visible" class="login-dialog-overlay" @click.self="handleClose">
|
||||
<div class="login-dialog-box">
|
||||
<!-- 关闭按钮 -->
|
||||
<div class="login-dialog-box__close" @click="handleClose">
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5 5L15 15M15 5L5 15" stroke="#999" stroke-width="1.8" stroke-linecap="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<el-button
|
||||
type="primary"
|
||||
size="large"
|
||||
class="login-btn h50 border-ra20"
|
||||
@click="handleLogin"
|
||||
>
|
||||
登录
|
||||
</el-button>
|
||||
<!-- Logo -->
|
||||
<div class="login-dialog-box__logo-row">
|
||||
<img src="@/assets/images/logo.png" alt="">
|
||||
</div>
|
||||
|
||||
<div class="login-dialog-box__form-wrap">
|
||||
<!-- 步骤一:输入手机号发送验证码 -->
|
||||
<template v-if="step === 1">
|
||||
<div class="login-dialog-box__heading">
|
||||
<h2 class="login-dialog-box__title">手机号登录/注册</h2>
|
||||
<p class="login-dialog-box__subtitle">未注册用户验证后将自动注册并登录</p>
|
||||
</div>
|
||||
<!-- 手机号输入组 -->
|
||||
<div class="login-dialog-box__phone-row">
|
||||
<!-- 手机图标 -->
|
||||
<div class="login-dialog-box__phone-icon">
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="4.5" y="2" width="9" height="14" rx="1.5" stroke="#0F6B66" stroke-width="1.5"/>
|
||||
<line x1="9" y1="2" x2="9" y2="3.5" stroke="#0F6B66" stroke-width="1.5" stroke-linecap="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
<!-- 国家码标签 -->
|
||||
<span class="login-dialog-box__country-label">中国 +86</span>
|
||||
<!-- 下拉箭头 -->
|
||||
<div class="login-dialog-box__country-chevron">
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6 8L10 12L14 8" stroke="#6F8586" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
<!-- 分隔线 -->
|
||||
<div class="login-dialog-box__input-divider"></div>
|
||||
<!-- 手机号输入框 -->
|
||||
<input v-model="phone" type="tel" class="login-dialog-box__phone-input" placeholder="请输入手机号码" maxlength="11" />
|
||||
</div>
|
||||
<!-- 发送验证码按钮 -->
|
||||
<button class="login-dialog-box__send-btn" :disabled="!canSend" @click="handleSendCode">发送验证码</button>
|
||||
</template>
|
||||
|
||||
<!-- 步骤二:输入验证码登录 -->
|
||||
<template v-if="step === 2">
|
||||
<div class="login-dialog-box__heading">
|
||||
<h2 class="login-dialog-box__title">请输入验证码</h2>
|
||||
<p class="login-dialog-box__subtitle dflex"><span class="dib">短信验证码已发送至 +86{{ phone }} </span><span class="fw600 ml5 dib cursor-po mlauto" @click="handleChangePhone">修改手机号</span></p>
|
||||
</div>
|
||||
<!-- OTP 输入框 -->
|
||||
<div class="login-dialog-box__otp-wrap" @click="focusOtpInput">
|
||||
<input ref="otpInputRef" v-model="otpValue" type="text" inputmode="numeric" class="login-dialog-box__otp-hidden-input" maxlength="6" :disabled="isLoggingIn" @input="handleOtpInput" />
|
||||
<div v-for="(_, idx) in 6" :key="idx" class="login-dialog-box__otp-box" :class="{ 'login-dialog-box__otp-box--active': idx === otpValue.length && !isLoggingIn, 'login-dialog-box__otp-box--filled': idx < otpValue.length }">
|
||||
<span v-if="idx < otpValue.length" class="login-dialog-box__otp-digit">{{ otpValue[idx] }}</span>
|
||||
<span v-else-if="idx === otpValue.length && !isLoggingIn" class="login-dialog-box__otp-cursor"></span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 状态提示 -->
|
||||
<p v-if="smsStatusMsg" class="login-dialog-box__sms-status" :class="{ 'login-dialog-box__sms-status--error': smsStatusIsError }">{{ smsStatusMsg }}</p>
|
||||
<!-- 重新发送 / 登录中按钮 -->
|
||||
<button class="login-dialog-box__status-btn" :disabled="isLoggingIn || countdown > 0" @click="handleResendCode">
|
||||
<template v-if="isLoggingIn"><span class="login-dialog-box__spinner"></span><span>登录中</span></template>
|
||||
<template v-else-if="countdown > 0">{{ countdown }} 秒后可继续发送</template>
|
||||
<template v-else>再次发送验证码</template>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<!-- 协议勾选(步骤一二通用) -->
|
||||
<div class="login-dialog-box__agreement">
|
||||
<div class="login-dialog-box__checkbox" :class="{ 'login-dialog-box__checkbox--checked': agreedTerms }" @click="agreedTerms = !agreedTerms">
|
||||
<span v-if="agreedTerms" class="login-dialog-box__check-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" width="0.13rem" height="0.13rem"><path stroke="currentColor" fill="currentColor" d="M77.248 415.04a64 64 0 0 1 90.496 0l226.304 226.304L846.528 188.8a64 64 0 1 1 90.56 90.496l-543.04 543.04-316.8-316.8a64 64 0 0 1 0-90.496"></path></svg></span>
|
||||
</div>
|
||||
<span class="login-dialog-box__agreement-text">登录即表示同意</span>
|
||||
<span class="login-dialog-box__agreement-link" @click="openAgreement('ae8065i3')">《用户协议》</span>
|
||||
<span class="login-dialog-box__agreement-text">与</span>
|
||||
<span class="login-dialog-box__agreement-link" @click="openAgreement('hf8375i8')">《隐私政策》</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</Teleport>
|
||||
|
||||
<!-- 协议预览弹窗 -->
|
||||
<AgreementPreviewDialog v-model="showAgreementDialog" :code="agreementCode" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { ref, computed, nextTick, watch } from 'vue'
|
||||
import { useStore } from 'vuex'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { sendSmsCode, smsLogin } from '@/api/auth'
|
||||
import AgreementPreviewDialog from '@/components/tools/AgreementPreviewDialog.vue'
|
||||
|
||||
const store = useStore()
|
||||
const router = useRouter()
|
||||
|
||||
// ==================== 弹窗显隐控制 ====================
|
||||
/** 弹窗可见性,绑定 store.state.showLogin */
|
||||
const visible = computed(() => store.state.showLogin)
|
||||
|
||||
// ==================== 登录步骤 ====================
|
||||
/** 当前步骤:1=输入手机号 2=输入验证码 */
|
||||
const step = ref(1)
|
||||
/** 手机号 */
|
||||
const phone = ref('')
|
||||
const code = ref('')
|
||||
/** OTP 验证码值 */
|
||||
const otpValue = ref('')
|
||||
/** 是否已勾选协议 */
|
||||
const agreedTerms = ref(true)
|
||||
/** 倒计时秒数 */
|
||||
const countdown = ref(0)
|
||||
let timer: ReturnType<typeof setInterval> | null = null
|
||||
let countdownTimer: ReturnType<typeof setInterval> | null = null
|
||||
/** 是否正在登录中 */
|
||||
const isLoggingIn = ref(false)
|
||||
/** 验证码状态提示文字 */
|
||||
const smsStatusMsg = ref('')
|
||||
/** 是否为错误样式 */
|
||||
const smsStatusIsError = ref(false)
|
||||
/** OTP 输入框引用 */
|
||||
const otpInputRef = ref<HTMLInputElement | null>(null)
|
||||
/** 协议弹窗状态 */
|
||||
const showAgreementDialog = ref(false)
|
||||
const agreementCode = ref('')
|
||||
|
||||
/** 倒计时显示文本,格式 m:ss */
|
||||
const countdownText = computed(() => {
|
||||
const m = Math.floor(countdown.value / 60)
|
||||
const s = countdown.value % 60
|
||||
return `${m}:${s.toString().padStart(2, '0')}`
|
||||
})
|
||||
/** 发送按钮是否可用(手机号11位 + 勾选协议) */
|
||||
const canSend = computed(() => phone.value.length === 11 && agreedTerms.value)
|
||||
|
||||
const visible = computed({
|
||||
get: () => store.state.showLogin,
|
||||
set: (val: boolean) => {
|
||||
if (!val) store.dispatch('closeLogin')
|
||||
},
|
||||
})
|
||||
|
||||
//发送验证码
|
||||
async function sendCode() {
|
||||
if (!phone.value) {
|
||||
ElMessage.warning('请输入手机号')
|
||||
return
|
||||
// ==================== 弹窗打开时重置状态 ====================
|
||||
watch(visible, (val) => {
|
||||
if (val) {
|
||||
step.value = 1
|
||||
phone.value = ''
|
||||
otpValue.value = ''
|
||||
countdown.value = 0
|
||||
smsStatusMsg.value = ''
|
||||
smsStatusIsError.value = false
|
||||
isLoggingIn.value = false
|
||||
if (countdownTimer) { clearInterval(countdownTimer); countdownTimer = null }
|
||||
}
|
||||
})
|
||||
|
||||
// ==================== 方法 ====================
|
||||
|
||||
/** 关闭弹窗 */
|
||||
function handleClose() {
|
||||
store.dispatch('closeLogin')
|
||||
}
|
||||
|
||||
/** 打开协议预览 */
|
||||
function openAgreement(code: string) {
|
||||
agreementCode.value = code
|
||||
showAgreementDialog.value = true
|
||||
}
|
||||
|
||||
/** 聚焦 OTP 隐藏输入框 */
|
||||
function focusOtpInput() {
|
||||
otpInputRef.value?.focus()
|
||||
}
|
||||
|
||||
/** 修改手机号 — 回到步骤一 */
|
||||
function handleChangePhone() {
|
||||
step.value = 1
|
||||
phone.value = ''
|
||||
otpValue.value = ''
|
||||
countdown.value = 0
|
||||
smsStatusMsg.value = ''
|
||||
smsStatusIsError.value = false
|
||||
isLoggingIn.value = false
|
||||
if (countdownTimer) { clearInterval(countdownTimer); countdownTimer = null }
|
||||
}
|
||||
|
||||
/** 开始60秒倒计时 */
|
||||
function startCountdown() {
|
||||
countdown.value = 60
|
||||
countdownTimer = setInterval(() => {
|
||||
countdown.value--
|
||||
if (countdown.value <= 0 && countdownTimer) { clearInterval(countdownTimer); countdownTimer = null }
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
/** 步骤一:发送验证码 */
|
||||
async function handleSendCode() {
|
||||
if (!phone.value) { ElMessage.warning('请输入手机号'); return }
|
||||
if (!agreedTerms.value) { ElMessage.warning('请先同意用户协议与隐私政策'); return }
|
||||
try {
|
||||
const res = await sendSmsCode(phone.value)
|
||||
if (res.code === '0' && res.data === true) {
|
||||
ElMessage.success('验证码已发送')
|
||||
countdown.value = 60
|
||||
timer = setInterval(() => {
|
||||
countdown.value--
|
||||
if (countdown.value <= 0 && timer) {
|
||||
clearInterval(timer)
|
||||
timer = null
|
||||
}
|
||||
}, 1000)
|
||||
if (res.code === '0') {
|
||||
smsStatusMsg.value = '验证码发送成功,请输入'
|
||||
smsStatusIsError.value = false
|
||||
step.value = 2
|
||||
startCountdown()
|
||||
nextTick(() => focusOtpInput())
|
||||
} else if (res.msg && res.msg.includes('请勿重复发送')) {
|
||||
smsStatusMsg.value = '验证码还在有效期内,请输入'
|
||||
smsStatusIsError.value = false
|
||||
step.value = 2
|
||||
startCountdown()
|
||||
nextTick(() => focusOtpInput())
|
||||
} else {
|
||||
ElMessage.error(res.msg || '验证码发送失败')
|
||||
smsStatusMsg.value = res.msg || '验证码发送失败'
|
||||
smsStatusIsError.value = true
|
||||
}
|
||||
} catch {
|
||||
// 错误已在 request 拦截器中统一处理
|
||||
} catch (err: any) {
|
||||
const msg = err?.response?.data?.msg || '验证码发送失败'
|
||||
smsStatusMsg.value = msg
|
||||
smsStatusIsError.value = true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 登录流程:
|
||||
* 1. 调 smsLogin 接口,后端成功会 Set-Cookie: Token=xxx
|
||||
* 2. Cookie 自动保持登录状态,不再存 localStorage
|
||||
* 3. 关闭弹窗,跳转到登录前想去的页面
|
||||
*/
|
||||
async function handleLogin() {
|
||||
if (!phone.value || !code.value) {
|
||||
ElMessage.warning('请输入手机号和验证码')
|
||||
return
|
||||
}
|
||||
/** OTP 输入处理 — 6位自动登录 */
|
||||
function handleOtpInput() {
|
||||
otpValue.value = otpValue.value.replace(/\D/g, '').slice(0, 6)
|
||||
if (otpValue.value.length === 6) { handleLogin() }
|
||||
}
|
||||
|
||||
/** 步骤二:再次发送验证码 */
|
||||
async function handleResendCode() {
|
||||
if (isLoggingIn.value || countdown.value > 0) return
|
||||
try {
|
||||
const res = await smsLogin(phone.value, code.value)
|
||||
const res = await sendSmsCode(phone.value)
|
||||
if (res.code === '0') {
|
||||
smsStatusMsg.value = '验证码发送成功,请输入'
|
||||
smsStatusIsError.value = false
|
||||
otpValue.value = ''
|
||||
startCountdown()
|
||||
nextTick(() => focusOtpInput())
|
||||
} else if (res.msg && res.msg.includes('请勿重复发送')) {
|
||||
smsStatusMsg.value = '验证码还在有效期内,请输入'
|
||||
smsStatusIsError.value = false
|
||||
} else {
|
||||
smsStatusMsg.value = res.msg || '验证码发送失败'
|
||||
smsStatusIsError.value = true
|
||||
}
|
||||
} catch (err: any) {
|
||||
const msg = err?.response?.data?.msg || '验证码发送失败'
|
||||
smsStatusMsg.value = msg
|
||||
smsStatusIsError.value = true
|
||||
}
|
||||
}
|
||||
|
||||
/** 登录接口调用 */
|
||||
async function handleLogin() {
|
||||
isLoggingIn.value = true
|
||||
try {
|
||||
const res = await smsLogin(phone.value, otpValue.value)
|
||||
if (res.code !== '0') {
|
||||
ElMessage.error(res.msg || '登录失败')
|
||||
smsStatusMsg.value = '验证码有误,请重新输入'
|
||||
smsStatusIsError.value = true
|
||||
otpValue.value = ''
|
||||
isLoggingIn.value = false
|
||||
nextTick(() => focusOtpInput())
|
||||
return
|
||||
}
|
||||
|
||||
// 登录成功,Cookie Token 已由后端 Set-Cookie 写入
|
||||
// 登录成功
|
||||
ElMessage.success(`欢迎回来,${res.data?.nick || ''}`)
|
||||
|
||||
// 清除验证码倒计时,登录成功后解除发送限制
|
||||
if (timer) {
|
||||
clearInterval(timer)
|
||||
timer = null
|
||||
}
|
||||
if (countdownTimer) { clearInterval(countdownTimer); countdownTimer = null }
|
||||
countdown.value = 0
|
||||
|
||||
store.commit('SET_AUTHENTICATED', true)
|
||||
|
||||
// 关闭弹窗并跳转
|
||||
const redirect = store.state.loginRedirect
|
||||
store.dispatch('closeLogin')
|
||||
if (redirect) {
|
||||
router.push(redirect)
|
||||
}
|
||||
} catch {
|
||||
// 错误已在 request 拦截器中统一处理
|
||||
smsStatusMsg.value = '验证码有误,请重新输入'
|
||||
smsStatusIsError.value = true
|
||||
otpValue.value = ''
|
||||
isLoggingIn.value = false
|
||||
nextTick(() => focusOtpInput())
|
||||
}
|
||||
}
|
||||
|
||||
function handleClose() {
|
||||
store.dispatch('closeLogin')
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -64,6 +64,30 @@
|
||||
|
||||
<div class="h1 border-EDF9FA-bottom-2 mrl14 mt14"></div>
|
||||
|
||||
<!-- 个人概述 -->
|
||||
<div ref="summaryRef" class="profile-page-content__card mtb16" :class="{ 'profile-page-content__card-hover': editingModule !== 'summary' }">
|
||||
<template v-if="editingModule !== 'summary'">
|
||||
<div class="profile-page-content__card-header">
|
||||
<h3 class="profile-page-content__card-title">个人概述</h3>
|
||||
<button class="profile-page-content__edit-btn-inline" @click="startEdit('summary')">
|
||||
<el-icon><EditPen /></el-icon><span>编辑</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="profile-page-content__summary-text" v-if="profile.summary">{{ profile.summary }}</div>
|
||||
<div class="profile-page-content__summary-text color-9" v-else>暂无个人概述</div>
|
||||
</template>
|
||||
<ProfileEditDrawer
|
||||
v-if="editingModule === 'summary'"
|
||||
v-model="editVisible"
|
||||
module="summary"
|
||||
:initial-data="editInitialData"
|
||||
@save="handleSave"
|
||||
@cancel="cancelEdit"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="h1 border-EDF9FA-bottom-2 mrl14"></div>
|
||||
|
||||
<!-- 教育经历 -->
|
||||
<div ref="educationRef" class="profile-page-content__card">
|
||||
<div class="profile-page-content__card-header profile-page-content__header-hover">
|
||||
@@ -305,6 +329,7 @@ import ResumeExpInlineEdit from '@/components/ResumeExpInlineEdit.vue'
|
||||
/** Tab 导航列表 */
|
||||
const tabList = [
|
||||
{ key: 'info', label: '个人信息' },
|
||||
{ key: 'summary', label: '个人概述' },
|
||||
{ key: 'education', label: '教育经历' },
|
||||
{ key: 'work', label: '工作经历' },
|
||||
{ key: 'internship', label: '实习经历' },
|
||||
@@ -320,6 +345,7 @@ const activeTab = ref('info')
|
||||
/** 各模块的 DOM 引用 */
|
||||
const scrollContainerRef = ref<HTMLElement | null>(null)
|
||||
const infoRef = ref<HTMLElement | null>(null)
|
||||
const summaryRef = ref<HTMLElement | null>(null)
|
||||
const educationRef = ref<HTMLElement | null>(null)
|
||||
const workRef = ref<HTMLElement | null>(null)
|
||||
const internshipRef = ref<HTMLElement | null>(null)
|
||||
@@ -331,6 +357,7 @@ const certificateRef = ref<HTMLElement | null>(null)
|
||||
/** 模块 key 与 ref 的映射 */
|
||||
const sectionRefMap: Record<string, typeof infoRef> = {
|
||||
info: infoRef,
|
||||
summary: summaryRef,
|
||||
education: educationRef,
|
||||
work: workRef,
|
||||
internship: internshipRef,
|
||||
@@ -360,7 +387,8 @@ interface ProfileData {
|
||||
regionCode: string
|
||||
portfolioUrl: string
|
||||
wechat?: string
|
||||
education: Array<{ school: string; major: string; studyType: number; degree: number; startDate: string; endDate: string; description: Array<{ id: string; text: string }> }>
|
||||
summary?: string
|
||||
education: Array<{ school: string; major: string; studyType: string | number; degree: string | number; startDate: string; endDate: string; description: Array<{ id: string; text: string }> }>
|
||||
works: Array<{ companyName: string; position: string; startDate: string; endDate: string; description: Array<{ id: string; text: string }> }>
|
||||
internships: Array<{ companyName: string; position: string; startDate: string; endDate: string; description: Array<{ id: string; text: string }> }>
|
||||
projects: Array<{ projectName: string; companyName: string; role: string; startDate: string; endDate: string; description: Array<{ id: string; text: string }> }>
|
||||
@@ -401,11 +429,15 @@ const editingExpIndex = ref('')
|
||||
/** 当前添加经历的模块 */
|
||||
const addingModule = ref('')
|
||||
|
||||
/** 获取学历文本 */
|
||||
function getDegreeText(studyType: number, degree: number): string {
|
||||
const studyTypeText = studyType === 0 ? '全日制' : '非全日制'
|
||||
/** 获取学历文本 — 兼容数字和中文字符串 */
|
||||
function getDegreeText(studyType: string | number, degree: string | number): string {
|
||||
// studyType 可能是 0/1 数字 或 "全日制"/"非全日制" 字符串
|
||||
const studyTypeMap: Record<number, string> = { 0: '全日制', 1: '非全日制' }
|
||||
const studyTypeText = typeof studyType === 'number' ? (studyTypeMap[studyType] || '') : (studyType || '')
|
||||
// degree 可能是 1/2/3/4 数字 或 "大专"/"本科"/"硕士"/"博士" 字符串
|
||||
const degreeMap: Record<number, string> = { 1: '大专', 2: '本科', 3: '硕士', 4: '博士' }
|
||||
return `${studyTypeText}${degreeMap[degree] || ''}`
|
||||
const degreeText = typeof degree === 'number' ? (degreeMap[degree] || '') : (degree || '')
|
||||
return `${studyTypeText}${degreeText}`
|
||||
}
|
||||
|
||||
/** 打开模块编辑(个人信息/技能/证书) */
|
||||
@@ -427,6 +459,8 @@ function startEdit(module: string) {
|
||||
editInitialData.value = { skills: [...(props.profile.skills || [])] }
|
||||
} else if (module === 'certificate') {
|
||||
editInitialData.value = { certificates: [...(props.profile.certificates || [])] }
|
||||
} else if (module === 'summary') {
|
||||
editInitialData.value = { summary: props.profile.summary || '' }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
>
|
||||
<!-- SVG图标:hover/active 状态显示选中图标,否则显示默认图标 -->
|
||||
<span class="side-nav__item-icon" v-if="index==9">
|
||||
|
||||
|
||||
</span>
|
||||
<span v-else
|
||||
@@ -291,16 +292,16 @@ import navMentorIcon from '@/assets/images/nav/nav-profile-icon.png'
|
||||
const activeIconSvgs: string[] = [
|
||||
// 索引0 — 职位(Jobs)
|
||||
'<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><g clip-path="url(#clip0_2834_6536)"><path d="M11.5 3.25C10.1759 3.25009 8.87121 3.56889 7.69626 4.17948C6.52131 4.79007 5.51059 5.67451 4.7495 6.75805C3.98842 7.8416 3.49932 9.09242 3.32355 10.4048C3.14777 11.7173 3.29048 13.0527 3.73962 14.2983C4.18875 15.544 4.93112 16.6632 5.90398 17.5614C6.87685 18.4597 8.05164 19.1106 9.32908 19.4591C10.6065 19.8076 11.9491 19.8435 13.2433 19.5638C14.5376 19.2841 15.7455 18.697 16.765 17.852L19.443 20.53C19.5852 20.6625 19.7732 20.7346 19.9675 20.7312C20.1618 20.7277 20.3472 20.649 20.4846 20.5116C20.622 20.3742 20.7008 20.1888 20.7042 19.9945C20.7076 19.8002 20.6355 19.6122 20.503 19.47L17.828 16.794C18.8351 15.5903 19.478 14.1242 19.6812 12.568C19.8843 11.0117 19.6394 9.42975 18.975 8.00781C18.3107 6.58587 17.2545 5.38294 15.9305 4.54021C14.6064 3.69747 13.0695 3.24989 11.5 3.25Z" fill="#52CAD1"/></g><defs><clipPath id="clip0_2834_6536"><rect width="24" height="24" fill="white"/></clipPath></defs></svg>',
|
||||
// 索引3 — 第四个菜单项(待替换)
|
||||
'<svg width="16" height="16" viewBox="0 0 26 24" fill="none" xmlns="http://www.w3.org/2000/svg"><mask id="mask0_2975_5519" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="2" y="1" width="20" height="22"><path d="M11 22L10.5 18" stroke="white" stroke-width="1.66667" stroke-linecap="round" stroke-linejoin="round"/><path d="M21 22V6H13L13.5 10L14 14L14.5 18L11 22H21Z" fill="white" stroke="white" stroke-width="1.66667" stroke-linecap="round" stroke-linejoin="round"/><path d="M14 14H16.5M13.5 10H16.5" stroke="black" stroke-width="1.66667" stroke-linecap="round" stroke-linejoin="round"/><path d="M3 2H12.5L13 6L13.5 10L14 14L14.5 18H3V2Z" fill="white" stroke="white" stroke-width="1.66667" stroke-linecap="round" stroke-linejoin="round"/><path d="M6 6H9.5M6 10H10M6 14H10.5" stroke="black" stroke-width="1.66667" stroke-linecap="round" stroke-linejoin="round"/></mask><g mask="url(#mask0_2975_5519)"><path d="M0 0H24V24H0V0Z" fill="#52CAD1"/></g></svg>',
|
||||
// 索引1 — 第二个菜单项(待替换)
|
||||
'<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"><g clip-path="url(#clip0_268_1540)"><path fill-rule="evenodd" clip-rule="evenodd" d="M9.49998 1.66667C9.49998 1.62246 9.48242 1.58007 9.45116 1.54882C9.41991 1.51756 9.37752 1.5 9.33331 1.5H4.66665C4.18042 1.5 3.7141 1.69315 3.37028 2.03697C3.02647 2.38079 2.83331 2.8471 2.83331 3.33333V12.6667C2.83331 13.1529 3.02647 13.6192 3.37028 13.963C3.7141 14.3068 4.18042 14.5 4.66665 14.5H11.3333C11.8195 14.5 12.2859 14.3068 12.6297 13.963C12.9735 13.6192 13.1666 13.1529 13.1666 12.6667V6.098C13.1666 6.0538 13.1491 6.0114 13.1178 5.98015C13.0866 5.94889 13.0442 5.93133 13 5.93133H9.99998C9.86737 5.93133 9.74019 5.87865 9.64643 5.78489C9.55266 5.69112 9.49998 5.56394 9.49998 5.43133V1.66667ZM9.99998 8.16667C10.1326 8.16667 10.2598 8.21935 10.3535 8.31311C10.4473 8.40688 10.5 8.53406 10.5 8.66667C10.5 8.79927 10.4473 8.92645 10.3535 9.02022C10.2598 9.11399 10.1326 9.16667 9.99998 9.16667H5.99998C5.86737 9.16667 5.74019 9.11399 5.64643 9.02022C5.55266 8.92645 5.49998 8.79927 5.49998 8.66667C5.49998 8.53406 5.55266 8.40688 5.64643 8.31311C5.74019 8.21935 5.86737 8.16667 5.99998 8.16667H9.99998ZM9.99998 10.8333C10.1326 10.8333 10.2598 10.886 10.3535 10.9798C10.4473 11.0735 10.5 11.2007 10.5 11.3333C10.5 11.4659 10.4473 11.5931 10.3535 11.6869C10.2598 11.7807 10.1326 11.8333 9.99998 11.8333H5.99998C5.86737 11.8333 5.74019 11.7807 5.64643 11.6869C5.55266 11.5931 5.49998 11.4659 5.49998 11.3333C5.49998 11.2007 5.55266 11.0735 5.64643 10.9798C5.74019 10.886 5.86737 10.8333 5.99998 10.8333H9.99998Z" fill="#52CAD1"/><path d="M10.5 1.88266C10.5 1.75999 10.6287 1.68199 10.724 1.75866C10.8049 1.82399 10.8767 1.89999 10.9393 1.98666L12.948 4.78466C12.9933 4.84866 12.944 4.93132 12.8653 4.93132H10.6667C10.6225 4.93132 10.5801 4.91377 10.5488 4.88251C10.5176 4.85125 10.5 4.80886 10.5 4.76466V1.88266Z" fill="#52CAD1"/></g><defs><clipPath id="clip0_268_1540"><rect width="16" height="16" fill="white"/></clipPath></defs></svg>',
|
||||
// 索引2 — 第三个菜单项(待替换)
|
||||
'<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"><g clip-path="url(#clip0_268_1537)"><path d="M13.8086 5.41868L13.6446 5.79601C13.619 5.85745 13.5758 5.90994 13.5204 5.94686C13.465 5.98378 13.3999 6.00348 13.3333 6.00348C13.2667 6.00348 13.2017 5.98378 13.1462 5.94686C13.0908 5.90994 13.0476 5.85745 13.022 5.79601L12.858 5.41868C12.5696 4.75116 12.0416 4.21596 11.378 3.91868L10.872 3.69268C10.8106 3.66444 10.7586 3.61919 10.7221 3.56229C10.6857 3.5054 10.6663 3.43925 10.6663 3.37168C10.6663 3.3041 10.6857 3.23795 10.7221 3.18106C10.7586 3.12416 10.8106 3.07891 10.872 3.05068L11.35 2.83801C12.0302 2.53226 12.5674 1.97726 12.8506 1.28734L13.0193 0.880009C13.0441 0.816911 13.0873 0.762741 13.1433 0.724559C13.1993 0.686377 13.2655 0.665955 13.3333 0.665955C13.4011 0.665955 13.4673 0.686377 13.5233 0.724559C13.5793 0.762741 13.6225 0.816911 13.6473 0.880009L13.816 1.28668C14.099 1.97673 14.6358 2.53196 15.316 2.83801L15.7946 3.05134C15.8558 3.07966 15.9077 3.1249 15.944 3.18172C15.9803 3.23855 15.9996 3.30457 15.9996 3.37201C15.9996 3.43944 15.9803 3.50547 15.944 3.56229C15.9077 3.61911 15.8558 3.66436 15.7946 3.69268L15.288 3.91801C14.6245 4.21559 14.0967 4.75103 13.8086 5.41868ZM13.3333 7.33334C13.7858 7.3337 14.2351 7.25728 14.662 7.10734C14.6651 7.1829 14.6666 7.25823 14.6666 7.33334C14.6666 8.74783 14.1047 10.1044 13.1045 11.1046C12.1044 12.1048 10.7478 12.6667 9.33331 12.6667V15C5.99998 13.6667 1.33331 11.6667 1.33331 7.33334C1.33331 5.91885 1.89522 4.5623 2.89541 3.56211C3.8956 2.56191 5.25216 2.00001 6.66665 2.00001H9.33331C9.40887 2.00001 9.4842 2.00156 9.55931 2.00468C9.34686 2.60796 9.28217 3.25339 9.37068 3.88684C9.45918 4.52028 9.69829 5.12327 10.068 5.64522C10.4376 6.16717 10.9271 6.59286 11.4952 6.88659C12.0634 7.18032 12.6937 7.33352 13.3333 7.33334Z" fill="#52CAD1"/></g><defs><clipPath id="clip0_268_1537"><rect width="16" height="16" fill="white"/></clipPath></defs></svg>',
|
||||
// 索引3 — 第四个菜单项(待替换)
|
||||
'<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"><g clip-path="url(#clip0_268_1539)"><path d="M13.88 2.97668C13.253 2.36774 12.4141 2.02596 11.54 2.02334H4.50002C4.06562 2.01717 3.6343 2.09718 3.23102 2.25875C2.82773 2.42032 2.46048 2.66025 2.1505 2.96464C1.84053 3.26904 1.59398 3.63187 1.42511 4.03216C1.25624 4.43244 1.16841 4.86223 1.16669 5.29668V10.7033C1.1684 11.1341 1.25528 11.5602 1.42233 11.9572C1.58937 12.3542 1.83328 12.7143 2.14002 13.0167C2.76361 13.6317 3.60419 13.9765 4.48002 13.9767H11.52C12.3959 13.9765 13.2364 13.6317 13.86 13.0167C14.1689 12.7159 14.4142 12.3561 14.5813 11.9587C14.7485 11.5614 14.8342 11.1345 14.8334 10.7033V5.29668C14.8362 4.86607 14.7534 4.43919 14.5898 4.0409C14.4261 3.6426 14.1848 3.28084 13.88 2.97668ZM7.77335 11.1433C7.64075 11.1433 7.51357 11.0907 7.4198 10.9969C7.32603 10.9031 7.27335 10.776 7.27335 10.6433C7.27312 10.4413 7.23295 10.2412 7.15515 10.0548C7.07735 9.86827 6.96346 9.699 6.82002 9.55667C6.52064 9.27977 6.12783 9.12596 5.72002 9.12596C5.31221 9.12596 4.9194 9.27977 4.62002 9.55667C4.47599 9.69855 4.3617 9.86774 4.28386 10.0543C4.20602 10.2409 4.16618 10.4412 4.16669 10.6433C4.16669 10.776 4.11401 10.9031 4.02024 10.9969C3.92647 11.0907 3.7993 11.1433 3.66669 11.1433C3.53408 11.1433 3.4069 11.0907 3.31313 10.9969C3.21937 10.9031 3.16669 10.776 3.16669 10.6433C3.16616 10.3089 3.23187 9.97762 3.36002 9.66868C3.48817 9.35973 3.67623 9.07923 3.91335 8.84334C4.14167 8.60543 4.41317 8.41313 4.71335 8.27668C4.44349 8.11088 4.22081 7.87847 4.0667 7.60178C3.91259 7.32508 3.83223 7.01339 3.83335 6.69668C3.83335 6.2016 4.03002 5.72681 4.38009 5.37674C4.73016 5.02667 5.20495 4.83001 5.70002 4.83001C6.19509 4.83001 6.66989 5.02667 7.01995 5.37674C7.37002 5.72681 7.56669 6.2016 7.56669 6.69668C7.57063 7.01088 7.49433 7.3209 7.345 7.59738C7.19568 7.87387 6.97827 8.10767 6.71335 8.27668C7.01802 8.40334 7.29669 8.58668 7.53335 8.81668C8.00738 9.29639 8.2753 9.94228 8.28002 10.6167C8.2847 10.6853 8.27496 10.7542 8.25142 10.8189C8.22789 10.8836 8.19107 10.9426 8.14335 10.9922C8.09563 11.0418 8.03806 11.0809 7.97434 11.1069C7.91062 11.133 7.84215 11.1454 7.77335 11.1433ZM12.1267 10.5567H9.73335C9.60075 10.5567 9.47357 10.504 9.3798 10.4102C9.28603 10.3165 9.23335 10.1893 9.23335 10.0567C9.23335 9.92407 9.28603 9.79689 9.3798 9.70312C9.47357 9.60935 9.60075 9.55667 9.73335 9.55667H12.1267C12.2593 9.55667 12.3865 9.60935 12.4802 9.70312C12.574 9.79689 12.6267 9.92407 12.6267 10.0567C12.6267 10.1893 12.574 10.3165 12.4802 10.4102C12.3865 10.504 12.2593 10.5567 12.1267 10.5567ZM9.23335 7.99668C9.23335 7.86407 9.28603 7.73689 9.3798 7.64312C9.47357 7.54935 9.60075 7.49668 9.73335 7.49668H10.76C10.8926 7.49668 11.0198 7.54935 11.1136 7.64312C11.2073 7.73689 11.26 7.86407 11.26 7.99668C11.26 8.12928 11.2073 8.25646 11.1136 8.35023C11.0198 8.444 10.8926 8.49668 10.76 8.49668H9.73335C9.66745 8.49757 9.60202 8.48525 9.54096 8.46044C9.47989 8.43563 9.42441 8.39884 9.3778 8.35223C9.33119 8.30562 9.2944 8.25014 9.26959 8.18907C9.24478 8.12801 9.23246 8.06258 9.23335 7.99668ZM12.1267 6.44334H9.73335C9.60075 6.44334 9.47357 6.39066 9.3798 6.2969C9.28603 6.20313 9.23335 6.07595 9.23335 5.94334C9.23335 5.81073 9.28603 5.68356 9.3798 5.58979C9.47357 5.49602 9.60075 5.44334 9.73335 5.44334H12.1267C12.2593 5.44334 12.3865 5.49602 12.4802 5.58979C12.574 5.68356 12.6267 5.81073 12.6267 5.94334C12.6267 6.07595 12.574 6.20313 12.4802 6.2969C12.3865 6.39066 12.2593 6.44334 12.1267 6.44334Z" fill="#52CAD1"/><path d="M5.72 7.59002C5.83381 7.59002 5.94651 7.56761 6.05166 7.52405C6.15681 7.4805 6.25235 7.41666 6.33283 7.33618C6.4133 7.2557 6.47714 7.16016 6.52069 7.05501C6.56425 6.94987 6.58667 6.83717 6.58667 6.72336C6.58667 6.60954 6.56425 6.49685 6.52069 6.3917C6.47714 6.28655 6.4133 6.19101 6.33283 6.11053C6.25235 6.03005 6.15681 5.96621 6.05166 5.92266C5.94651 5.87911 5.83381 5.85669 5.72 5.85669C5.49014 5.85669 5.2697 5.948 5.10717 6.11053C4.94464 6.27306 4.85333 6.4935 4.85333 6.72336C4.85333 6.95321 4.94464 7.17365 5.10717 7.33618C5.2697 7.49871 5.49014 7.59002 5.72 7.59002Z" fill="#52CAD1"/></g><defs><clipPath id="clip0_268_1539"><rect width="16" height="16" fill="white"/></clipPath></defs></svg>',
|
||||
// 索引4 — 第五个菜单项(待替换)
|
||||
'<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"><g clip-path="url(#clip0_268_1538)"><path d="M6.07134 3.63198C6.47001 2.46531 8.08201 2.42998 8.55468 3.52598L8.59468 3.63265L9.13268 5.20598C9.25597 5.5668 9.45521 5.89699 9.71696 6.17426C9.97871 6.45154 10.2969 6.66945 10.65 6.81331L10.7947 6.86731L12.368 7.40465C13.5347 7.80331 13.57 9.41531 12.4747 9.88798L12.368 9.92798L10.7947 10.466C10.4337 10.5892 10.1034 10.7884 9.82602 11.0502C9.54863 11.3119 9.33061 11.6301 9.18668 11.9833L9.13268 12.1273L8.59534 13.7013C8.19668 14.868 6.58468 14.9033 6.11268 13.808L6.07134 13.7013L5.53401 12.128C5.4108 11.767 5.21159 11.4367 4.94984 11.1593C4.68808 10.8819 4.36987 10.6639 4.01668 10.52L3.87268 10.466L2.29934 9.92865C1.13201 9.52998 1.09668 7.91798 2.19268 7.44598L2.29934 7.40465L3.87268 6.86731C4.2335 6.74402 4.56368 6.54478 4.84096 6.28303C5.11823 6.02128 5.33615 5.70311 5.48001 5.34998L5.53401 5.20598L6.07134 3.63198ZM12.6667 1.33331C12.7914 1.33331 12.9136 1.3683 13.0194 1.43429C13.1253 1.50029 13.2105 1.59465 13.2653 1.70665L13.2973 1.78465L13.5307 2.46865L14.2153 2.70198C14.3403 2.74444 14.4499 2.82306 14.5302 2.92786C14.6104 3.03266 14.6578 3.15893 14.6662 3.29066C14.6746 3.4224 14.6437 3.55367 14.5775 3.66784C14.5112 3.78201 14.4126 3.87394 14.294 3.93198L14.2153 3.96398L13.5313 4.19731L13.298 4.88198C13.2555 5.00693 13.1768 5.11645 13.072 5.19664C12.9671 5.27684 12.8408 5.32411 12.7091 5.33246C12.5774 5.34081 12.4461 5.30988 12.332 5.24357C12.2179 5.17726 12.126 5.07856 12.068 4.95998L12.036 4.88198L11.8027 4.19798L11.118 3.96465C10.993 3.92218 10.8835 3.84357 10.8032 3.73877C10.7229 3.63397 10.6756 3.5077 10.6672 3.37597C10.6587 3.24423 10.6896 3.11296 10.7559 2.99879C10.8221 2.88462 10.9208 2.79269 11.0393 2.73465L11.118 2.70265L11.802 2.46931L12.0353 1.78465C12.0803 1.65293 12.1653 1.53859 12.2786 1.45765C12.3918 1.37671 12.5275 1.33323 12.6667 1.33331Z" fill="#52CAD1"/></g><defs><clipPath id="clip0_268_1538"><rect width="16" height="16" fill="white"/></clipPath></defs></svg>',
|
||||
// 索引5 — 第六个菜单项(待替换)
|
||||
'<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M9.94013 1.97814C11.1383 0.940907 12.8621 0.940856 14.0602 1.97814C15.0132 2.80372 16.382 4.05138 18.1647 5.83458C19.9482 7.61754 21.197 8.98606 22.0222 9.93907C23.0594 11.1373 23.0594 12.862 22.0222 14.0602C21.1965 15.0137 19.9482 16.3812 18.1647 18.1647C16.3818 19.9481 15.0133 21.1965 14.0602 22.0221C12.862 23.0593 11.1373 23.0594 9.93915 22.0221C8.98613 21.1965 7.61762 19.9481 5.83466 18.1647C4.05133 16.3818 2.80336 15.0132 1.97821 14.0602C0.940907 12.8619 0.940907 11.1373 1.97821 9.93907C2.80384 8.98606 4.05134 7.61743 5.83466 5.83458C7.61762 4.05114 8.98711 2.80329 9.94013 1.97814ZM11.9997 14.567C11.5396 14.5672 11.1667 14.9399 11.1667 15.4V15.4996C11.1667 15.9597 11.5396 16.3334 11.9997 16.3336H12.0993C12.5595 16.3336 12.9332 15.9598 12.9333 15.4996V15.4C12.9333 14.9398 12.5595 14.567 12.0993 14.567H11.9997ZM12.0495 7.567C11.5895 7.56727 11.2165 7.93994 11.2165 8.40001V12.4C11.2166 12.86 11.5895 13.2328 12.0495 13.233C12.5097 13.233 12.8834 12.8602 12.8835 12.4V8.40001C12.8835 7.93977 12.5097 7.567 12.0495 7.567Z" fill="#52CAD1"/></svg>',
|
||||
'<svg width="16" height="16" viewBox="0 0 26 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M9.94013 1.97814C11.1383 0.940907 12.8621 0.940856 14.0602 1.97814C15.0132 2.80372 16.382 4.05138 18.1647 5.83458C19.9482 7.61754 21.197 8.98606 22.0222 9.93907C23.0594 11.1373 23.0594 12.862 22.0222 14.0602C21.1965 15.0137 19.9482 16.3812 18.1647 18.1647C16.3818 19.9481 15.0133 21.1965 14.0602 22.0221C12.862 23.0593 11.1373 23.0594 9.93915 22.0221C8.98613 21.1965 7.61762 19.9481 5.83466 18.1647C4.05133 16.3818 2.80336 15.0132 1.97821 14.0602C0.940907 12.8619 0.940907 11.1373 1.97821 9.93907C2.80384 8.98606 4.05134 7.61743 5.83466 5.83458C7.61762 4.05114 8.98711 2.80329 9.94013 1.97814ZM11.9997 14.567C11.5396 14.5672 11.1667 14.9399 11.1667 15.4V15.4996C11.1667 15.9597 11.5396 16.3334 11.9997 16.3336H12.0993C12.5595 16.3336 12.9332 15.9598 12.9333 15.4996V15.4C12.9333 14.9398 12.5595 14.567 12.0993 14.567H11.9997ZM12.0495 7.567C11.5895 7.56727 11.2165 7.93994 11.2165 8.40001V12.4C11.2166 12.86 11.5895 13.2328 12.0495 13.233C12.5097 13.233 12.8834 12.8602 12.8835 12.4V8.40001C12.8835 7.93977 12.5097 7.567 12.0495 7.567Z" fill="#52CAD1"/></svg>',
|
||||
]
|
||||
|
||||
/**
|
||||
@@ -829,8 +830,9 @@ async function handleNav(item: MenuItem) {
|
||||
return
|
||||
}
|
||||
|
||||
// Jobs 等静态页面 — 无需登录验证,直接跳转
|
||||
if (staticNames.includes(item.name)) {
|
||||
// 不需要登录的白名单页面 — 直接跳转
|
||||
const publicNavNames = ['Jobs', 'JobDetail', 'Announcement', 'AnnouncementDetail', 'MentorDetail']
|
||||
if (staticNames.includes(item.name) || publicNavNames.includes(item.name)) {
|
||||
router.push(item.path)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ const remAdaptPlugin: Plugin = {
|
||||
// 检测是否为移动端/小屏
|
||||
const isMobile = (): boolean => {
|
||||
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)
|
||||
|| window.innerWidth < 1200
|
||||
|
||||
}
|
||||
|
||||
// 重新计算缩放
|
||||
|
||||
@@ -10,11 +10,12 @@ import type { MenuItemRaw } from '@/api/menu'
|
||||
* 【新增页面时】在这里加一条映射即可,后端返回对应的 key 就能自动注册路由
|
||||
*/
|
||||
const componentMap: Record<string, () => Promise<any>> = {
|
||||
'Jobs': () => import('@/views/Jobs.vue'),
|
||||
'Agent': () => import('@/views/Agent.vue'),
|
||||
'Profile': () => import('@/views/Profile.vue'),
|
||||
'Resume': () => import('@/views/Resume.vue'),
|
||||
'Mentor': () => import('@/views/Mentor.vue'),
|
||||
'Jobs': () => import('@/views/Jobs.vue'),
|
||||
'Agent': () => import('@/views/Agent.vue'),
|
||||
'Profile': () => import('@/views/Profile.vue'),
|
||||
'Resume': () => import('@/views/Resume.vue'),
|
||||
'Mentor': () => import('@/views/Mentor.vue'),
|
||||
'Announcement': () => import('@/views/Announcement.vue'),
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -23,7 +24,7 @@ const componentMap: Record<string, () => Promise<any>> = {
|
||||
* 如果后端返回了一个 component 字符串在映射表里找不到,会跳过该条路由并打印警告
|
||||
* 已在静态路由中注册的路径(如 /jobs)不再重复注册
|
||||
*/
|
||||
const STATIC_PATHS = ['/', '/m', '/jobs', '/resume/:id', '/jobs/:id', '/mentor/:id']
|
||||
const STATIC_PATHS = ['/', '/m', '/jobs', '/resume/:id', '/jobs/:id', '/mentor/:id', '/announcement/:id']
|
||||
|
||||
export function buildDynamicRoutes(menus: MenuItemRaw[]): RouteRecordRaw[] {
|
||||
const routes: RouteRecordRaw[] = []
|
||||
|
||||
+47
-74
@@ -3,8 +3,9 @@ import type { RouteRecordRaw } from 'vue-router'
|
||||
import store from '@/stores'
|
||||
import { checkLogin } from '@/api/auth'
|
||||
import { fetchMemberStatus } from '@/api/member'
|
||||
import { fetchProfile } from '@/api/profile'
|
||||
import { ENABLE_FORCE_RESUME_UPLOAD } from '@/business.config'
|
||||
// 【已禁用】强制简历上传机制相关引用
|
||||
// import { fetchProfile } from '@/api/profile'
|
||||
// import { ENABLE_FORCE_RESUME_UPLOAD } from '@/business.config'
|
||||
|
||||
/**
|
||||
* 静态路由 — 不需要权限,应用启动就注册
|
||||
@@ -18,7 +19,6 @@ const staticRoutes: RouteRecordRaw[] = [
|
||||
path: '/jobs',
|
||||
name: 'Jobs',
|
||||
component: () => import('@/views/Jobs.vue'),
|
||||
meta: { requiresAuth: true },
|
||||
},
|
||||
{
|
||||
path: '/resume/:id',
|
||||
@@ -36,6 +36,11 @@ const staticRoutes: RouteRecordRaw[] = [
|
||||
name: 'MentorDetail',
|
||||
component: () => import('@/views/MentorDetail.vue'),
|
||||
},
|
||||
{
|
||||
path: '/announcement/:id',
|
||||
name: 'AnnouncementDetail',
|
||||
component: () => import('@/views/AnnouncementDetail.vue'),
|
||||
},
|
||||
{
|
||||
path: '/privacy',
|
||||
name: 'PrivacyPolicy',
|
||||
@@ -116,76 +121,63 @@ router.beforeEach(async (to, _from, next) => {
|
||||
return
|
||||
}
|
||||
|
||||
// 需要鉴权的路由,每次都通过接口校验登录状态
|
||||
if (to.meta?.requiresAuth) {
|
||||
// 不需要登录即可访问的页面白名单(首页、岗位列表/详情、校招公告列表/详情等)
|
||||
const publicPages: string[] = [
|
||||
'Home', 'HomeMobile', 'Login', 'SuperRjtfoewjksfewjLogin',
|
||||
'Jobs', 'JobDetail', 'Announcement', 'AnnouncementDetail',
|
||||
'MentorDetail', 'PrivacyPolicy',
|
||||
]
|
||||
const isPublicPage = publicPages.includes(to.name as string)
|
||||
|
||||
// 非白名单页面需要鉴权 — 通过接口校验登录状态
|
||||
if (!isPublicPage) {
|
||||
try {
|
||||
const res = await checkLogin()
|
||||
lastCheckTime = Date.now()
|
||||
if (res.code === '0' && res.data === true) {
|
||||
// Cookie 有效,同步前端状态并放行
|
||||
// Cookie 有效,同步前端状态
|
||||
store.commit('SET_AUTHENTICATED', true)
|
||||
// 检查个人资料是否为空(强制简历上传机制)
|
||||
if (ENABLE_FORCE_RESUME_UPLOAD) {
|
||||
try {
|
||||
const profileRes = await fetchProfile()
|
||||
if (profileRes.code === '0' && !profileRes.data) {
|
||||
next({ name: 'Login', query: { resumeUpload: '1' } })
|
||||
return
|
||||
}
|
||||
} catch { /* 接口异常不阻断 */ }
|
||||
}
|
||||
next()
|
||||
} else {
|
||||
// 未登录或 Cookie 失效 — 跳转登录页
|
||||
store.commit('SET_AUTHENTICATED', false)
|
||||
next({ name: 'Login', query: { redirect: to.fullPath } })
|
||||
}
|
||||
} catch {
|
||||
// 请求异常(网络错误等),也视为未登录
|
||||
store.commit('SET_AUTHENTICATED', false)
|
||||
next({ name: 'Login', query: { redirect: to.fullPath } })
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Agent 页面需要会员权限 — 等待会员信息加载后判断
|
||||
if (to.name === 'Agent') {
|
||||
try {
|
||||
// 先检查个人资料是否为空(强制简历上传机制)
|
||||
if (ENABLE_FORCE_RESUME_UPLOAD) {
|
||||
try {
|
||||
const profileRes = await fetchProfile()
|
||||
if (profileRes.code === '0' && !profileRes.data) {
|
||||
next({ name: 'Login', query: { resumeUpload: '1' } })
|
||||
return
|
||||
// Agent 页面需要额外的会员权限判断
|
||||
if (to.name === 'Agent') {
|
||||
try {
|
||||
if (!store.state.memberStatus) {
|
||||
const memberRes = await fetchMemberStatus()
|
||||
if (memberRes.code === '0' && memberRes.data) {
|
||||
store.commit('SET_MEMBER_STATUS', memberRes.data)
|
||||
}
|
||||
}
|
||||
const status = store.state.memberStatus
|
||||
if (status && status.isMember && (status.memberType === 1 || status.memberType === 2)) {
|
||||
next()
|
||||
} else {
|
||||
store.commit('SET_SHOW_MEMBER_ACCESS_DIALOG', true)
|
||||
next(false)
|
||||
}
|
||||
} catch {
|
||||
store.commit('SET_SHOW_MEMBER_ACCESS_DIALOG', true)
|
||||
next(false)
|
||||
}
|
||||
} catch { /* 接口异常不阻断 */ }
|
||||
}
|
||||
// 确保会员信息已加载到 store
|
||||
if (!store.state.memberStatus) {
|
||||
const res = await fetchMemberStatus()
|
||||
if (res.code === '0' && res.data) {
|
||||
store.commit('SET_MEMBER_STATUS', res.data)
|
||||
return
|
||||
}
|
||||
}
|
||||
const status = store.state.memberStatus
|
||||
// 试用会员(memberType=2)或正式会员(memberType=1)才能进入
|
||||
if (status && status.isMember && (status.memberType === 1 || status.memberType === 2)) {
|
||||
|
||||
next()
|
||||
} else {
|
||||
// 免费用户 — 弹出会员权限拦截弹窗,阻止跳转
|
||||
store.commit('SET_SHOW_MEMBER_ACCESS_DIALOG', true)
|
||||
// 未登录或 Cookie 失效 — 弹出登录弹窗,阻止导航
|
||||
store.commit('SET_AUTHENTICATED', false)
|
||||
store.dispatch('openLogin', to.fullPath)
|
||||
next(false)
|
||||
}
|
||||
} catch {
|
||||
// 接口异常时弹窗提示,不放行
|
||||
store.commit('SET_SHOW_MEMBER_ACCESS_DIALOG', true)
|
||||
// 请求异常(网络错误等),也视为未登录 — 弹登录弹窗
|
||||
store.commit('SET_AUTHENTICATED', false)
|
||||
store.dispatch('openLogin', to.fullPath)
|
||||
next(false)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 非鉴权路由 — 静默同步登录状态(节流:5 分钟内不重复请求)
|
||||
// 白名单页面 — 静默同步登录状态(节流:5 分钟内不重复请求)
|
||||
if (Date.now() - lastCheckTime > CHECK_INTERVAL) {
|
||||
checkLogin()
|
||||
.then((res) => {
|
||||
@@ -201,25 +193,6 @@ router.beforeEach(async (to, _from, next) => {
|
||||
})
|
||||
}
|
||||
|
||||
// 已登录 + 非首页/登录页/移动首页:检查个人资料是否为空,为空则强制跳转简历上传
|
||||
const skipProfileCheck = ['Home', 'HomeMobile', 'Login']
|
||||
if (
|
||||
ENABLE_FORCE_RESUME_UPLOAD &&
|
||||
store.state.isAuthenticated &&
|
||||
!skipProfileCheck.includes(to.name as string)
|
||||
) {
|
||||
try {
|
||||
const profileRes = await fetchProfile()
|
||||
if (profileRes.code === '0' && !profileRes.data) {
|
||||
// 个人资料为空 — 跳转到登录页简历上传步骤
|
||||
next({ name: 'Login', query: { resumeUpload: '1' } })
|
||||
return
|
||||
}
|
||||
} catch {
|
||||
// 接口异常不阻断导航
|
||||
}
|
||||
}
|
||||
|
||||
next()
|
||||
})
|
||||
|
||||
|
||||
@@ -40,10 +40,14 @@ service.interceptors.response.use(
|
||||
(error) => {
|
||||
const status = error.response?.status
|
||||
if (status === 401) {
|
||||
// 同步重置前端登录状态,跳转登录页
|
||||
// 同步重置前端登录状态,弹出登录弹窗(白名单页面不弹)
|
||||
store.commit('SET_AUTHENTICATED', false)
|
||||
router.push({ name: 'Login', query: { redirect: window.location.pathname } })
|
||||
ElMessage.error('登录已过期,请重新登录')
|
||||
const publicPages = ['Home', 'HomeMobile', 'Login', 'SuperRjtfoewjksfewjLogin', 'Jobs', 'JobDetail', 'Announcement', 'AnnouncementDetail', 'MentorDetail', 'PrivacyPolicy']
|
||||
const currentRouteName = router.currentRoute.value.name as string
|
||||
if (!publicPages.includes(currentRouteName)) {
|
||||
store.dispatch('openLogin', window.location.pathname)
|
||||
ElMessage.error('登录已过期,请重新登录')
|
||||
}
|
||||
} else {
|
||||
ElMessage.error(error.response?.data?.msg || '请求失败')
|
||||
}
|
||||
|
||||
@@ -0,0 +1,996 @@
|
||||
<template>
|
||||
<div class="announcement-page">
|
||||
<SideNav />
|
||||
<div class="announcement-page__content">
|
||||
|
||||
<!-- 页面标题 -->
|
||||
<div class="bg-f mb16 prl24 ptb16 dflex aliite-c">
|
||||
<div class="dflex-start aliite-c">
|
||||
<h2 class="announcement-page__title">校招公告</h2>
|
||||
<span class="announcement-page__subtitle ml12">每日更新最新企业校招信息,助力你的求职之路</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 筛选条件面板 -->
|
||||
<div class="announcement-page__filters-bar">
|
||||
<!-- 筛选面板 + 统计卡片横向排列 -->
|
||||
<div class="announcement-page__filters-row">
|
||||
<div ref="filtersPanelRef" class="announcement-page__filters-panel">
|
||||
<!-- 行业热门分类行 -->
|
||||
<div class="announcement-page__filter-row" v-if="hotIndustries.length > 0">
|
||||
<div class="announcement-page__filter-row-inner">
|
||||
<span class="announcement-page__filter-row-label">行业</span>
|
||||
<div class="announcement-page__filter-chips-wrap">
|
||||
<div class="announcement-page__filter-chips">
|
||||
<!-- 全部按钮 -->
|
||||
<span
|
||||
class="announcement-page__filter-chip"
|
||||
:class="{ 'announcement-page__filter-chip--active-solid': selectedIndustryNames.length === 0 }"
|
||||
@click="clearIndustryFilter"
|
||||
>全部</span>
|
||||
<!-- 热门行业chip -->
|
||||
<span
|
||||
v-for="item in hotIndustries"
|
||||
:key="'hot-ind-' + item.id"
|
||||
class="announcement-page__filter-chip"
|
||||
:class="{ 'announcement-page__filter-chip--active': selectedIndustryNames.includes(item.name) }"
|
||||
@click="toggleIndustry(item.name)"
|
||||
>{{ item.name }}</span>
|
||||
</div>
|
||||
<!-- 更多按钮 -->
|
||||
<IndustrySelector
|
||||
ref="industrySelectorRef"
|
||||
:industryIds="selectedIndustryIds"
|
||||
:maxSelect="3"
|
||||
:level="2"
|
||||
:allowParentSelect="false"
|
||||
:allowParentSelectClick="true"
|
||||
:triggerStyle="moreBtnTriggerStyle"
|
||||
:hoverStyle="moreBtnHoverStyle"
|
||||
:displayStyle="selectorDisplayStyle"
|
||||
triggerLabel="更多"
|
||||
@update:industryIds="onIndustryChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 岗位热门分类行 -->
|
||||
<div class="announcement-page__filter-row" v-if="hotCategories.length > 0">
|
||||
<div class="announcement-page__filter-row-inner">
|
||||
<span class="announcement-page__filter-row-label">岗位</span>
|
||||
<div class="announcement-page__filter-chips-wrap">
|
||||
<div class="announcement-page__filter-chips">
|
||||
<!-- 全部按钮 -->
|
||||
<span
|
||||
class="announcement-page__filter-chip"
|
||||
:class="{ 'announcement-page__filter-chip--active-solid': selectedCategoryNames.length === 0 }"
|
||||
@click="clearCategoryFilter"
|
||||
>全部</span>
|
||||
<!-- 热门岗位chip -->
|
||||
<span
|
||||
v-for="item in hotCategories"
|
||||
:key="'hot-cat-' + item.id"
|
||||
class="announcement-page__filter-chip"
|
||||
:class="{ 'announcement-page__filter-chip--active': selectedCategoryNames.includes(item.name) }"
|
||||
@click="toggleCategory(item.name)"
|
||||
>{{ item.name }}</span>
|
||||
</div>
|
||||
<!-- 更多按钮 -->
|
||||
<JobCategorySelector
|
||||
ref="jobCategorySelectorRef"
|
||||
:categoryIds="selectedCategoryIds"
|
||||
:maxSelect="3"
|
||||
:level="3"
|
||||
:allowParentSelect="false"
|
||||
:allowParentSelectClick="true"
|
||||
:triggerStyle="moreBtnTriggerStyle"
|
||||
:hoverStyle="moreBtnHoverStyle"
|
||||
:displayStyle="selectorDisplayStyle"
|
||||
triggerLabel="更多"
|
||||
@update:categoryIds="onCategoryChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 地点热门分类行 -->
|
||||
<div class="announcement-page__filter-row" v-if="hotCities.length > 0">
|
||||
<div class="announcement-page__filter-row-inner">
|
||||
<span class="announcement-page__filter-row-label">地点</span>
|
||||
<div class="announcement-page__filter-chips-wrap">
|
||||
<div class="announcement-page__filter-chips">
|
||||
<!-- 全国按钮 -->
|
||||
<span
|
||||
class="announcement-page__filter-chip"
|
||||
:class="{ 'announcement-page__filter-chip--active-solid': selectedCityNames.length === 0 }"
|
||||
@click="clearCityFilter"
|
||||
>全国</span>
|
||||
<!-- 热门城市chip -->
|
||||
<span
|
||||
v-for="item in hotCities"
|
||||
:key="'hot-city-' + item.code"
|
||||
class="announcement-page__filter-chip"
|
||||
:class="{ 'announcement-page__filter-chip--active': selectedCityNames.includes(item.name) }"
|
||||
@click="toggleCity(item.name)"
|
||||
>{{ item.name }}</span>
|
||||
</div>
|
||||
<!-- 更多按钮 -->
|
||||
<RegionSelector
|
||||
ref="regionSelectorRef"
|
||||
:regionCodes="selectedRegionCodes"
|
||||
:level="2"
|
||||
:maxSelect="3"
|
||||
:triggerStyle="moreBtnTriggerStyle"
|
||||
:hoverStyle="moreBtnHoverStyle"
|
||||
:displayStyle="selectorDisplayStyle"
|
||||
triggerLabel="更多"
|
||||
@update:regionCodes="onRegionChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 筛选行:毕业年份 + 学历要求 + 公司类型 + 招聘批次 + 发布时间 + 清空 -->
|
||||
<div class="announcement-page__filter-row announcement-page__filter-row--bottom">
|
||||
<div class="announcement-page__filter-row-inner">
|
||||
<span class="announcement-page__filter-row-label">筛选</span>
|
||||
<div class="announcement-page__filter-dropdowns">
|
||||
<!-- 毕业年份下拉 -->
|
||||
<div class="announcement-page__dropdown-item" @click.stop="toggleDropdown('recruitYear')">
|
||||
<span>{{ recruitYearLabel }}</span>
|
||||
<svg class="announcement-page__dropdown-arrow" viewBox="0 0 12 12" fill="none">
|
||||
<path d="M3 4.5L6 7.5L9 4.5" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
<div v-if="showRecruitYearDropdown" class="announcement-page__filter-dropdown" @click.stop>
|
||||
<div
|
||||
class="announcement-page__filter-dropdown-item"
|
||||
v-for="option in recruitYearOptions"
|
||||
:key="option.value"
|
||||
:class="{ 'announcement-page__filter-dropdown-item--active': selectedRecruitYear === option.value }"
|
||||
@click.stop="selectRecruitYear(option.value)"
|
||||
>{{ option.label }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 学历要求下拉 -->
|
||||
<div class="announcement-page__dropdown-item" @click.stop="toggleDropdown('education')">
|
||||
<span>{{ educationFilterLabel }}</span>
|
||||
<svg class="announcement-page__dropdown-arrow" viewBox="0 0 12 12" fill="none">
|
||||
<path d="M3 4.5L6 7.5L9 4.5" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
<div v-if="showEducationDropdown" class="announcement-page__filter-dropdown" @click.stop>
|
||||
<div
|
||||
class="announcement-page__filter-dropdown-item"
|
||||
v-for="option in educationOptions"
|
||||
:key="option"
|
||||
:class="{ 'announcement-page__filter-dropdown-item--active': selectedEducationName === option }"
|
||||
@click.stop="selectEducation(option)"
|
||||
>{{ option }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 公司类型下拉 -->
|
||||
<div class="announcement-page__dropdown-item" @click.stop="toggleDropdown('companyType')">
|
||||
<span>{{ companyTypeLabel }}</span>
|
||||
<svg class="announcement-page__dropdown-arrow" viewBox="0 0 12 12" fill="none">
|
||||
<path d="M3 4.5L6 7.5L9 4.5" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
<div v-if="showCompanyTypeDropdown" class="announcement-page__filter-dropdown" @click.stop>
|
||||
<div
|
||||
class="announcement-page__filter-dropdown-item"
|
||||
v-for="option in companyTypeOptions"
|
||||
:key="option"
|
||||
:class="{ 'announcement-page__filter-dropdown-item--active': selectedCompanyType === option }"
|
||||
@click.stop="selectCompanyType(option)"
|
||||
>{{ option }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 招聘批次下拉 -->
|
||||
<div class="announcement-page__dropdown-item" @click.stop="toggleDropdown('batch')">
|
||||
<span>{{ batchLabel }}</span>
|
||||
<svg class="announcement-page__dropdown-arrow" viewBox="0 0 12 12" fill="none">
|
||||
<path d="M3 4.5L6 7.5L9 4.5" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
<div v-if="showBatchDropdown" class="announcement-page__filter-dropdown" @click.stop>
|
||||
<div
|
||||
class="announcement-page__filter-dropdown-item"
|
||||
v-for="option in batchOptions"
|
||||
:key="option"
|
||||
:class="{ 'announcement-page__filter-dropdown-item--active': selectedBatchName === option }"
|
||||
@click.stop="selectBatch(option)"
|
||||
>{{ option }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 发布时间选择 -->
|
||||
<div class="announcement-page__dropdown-item border-n announcement-page__dropdown-item--time">
|
||||
<span>发布时间:</span>
|
||||
<el-date-picker
|
||||
v-model="publishTimeRange"
|
||||
type="daterange"
|
||||
range-separator="至"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
size="small"
|
||||
:teleported="true"
|
||||
format="MM/DD"
|
||||
style="width: 1.8rem; height: 0.24rem; font-size: 0.12rem;"
|
||||
@change="onPublishTimeChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 清空筛选条件 -->
|
||||
<span v-if="hasActiveFilters" class="announcement-page__clear-filters" @click="clearAllFilters">清空筛选条件</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 校招动态统计卡片 -->
|
||||
<JobStatsOverview
|
||||
title="校招动态"
|
||||
:subtitle="'数据截至 ' + statsCurrentDate"
|
||||
:todayCount="statsTodayCount"
|
||||
todayLabel="今日新增"
|
||||
:totalCount="statsTotalCompany"
|
||||
totalLabel="招聘中企业"
|
||||
:rateValue="statsDeadline7days"
|
||||
rateLabel="近7日截止"
|
||||
:cardHeight="filtersPanelHeight"
|
||||
/>
|
||||
</div>
|
||||
<!-- 搜索框(独立在筛选面板下方) -->
|
||||
<div class="mt12 dflex aliite-c flex-warp">
|
||||
<div class="announcement-page__search-box fgrow1">
|
||||
<svg class="announcement-page__search-svg" viewBox="0 0 16 16" fill="none" @click="reloadFirstPage">
|
||||
<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="announcement-page__search-input"
|
||||
placeholder="搜索公司、公告标题或招聘岗位"
|
||||
@keyup.enter="reloadFirstPage"
|
||||
/>
|
||||
</div>
|
||||
<div class="dflex aliite-c flex-warp ml24">
|
||||
<!-- 热门搜索标签 -->
|
||||
<span class="announcement-page__hot-search-label">热门搜索:</span>
|
||||
<span
|
||||
v-for="(tag, idx) in hotSearchTags"
|
||||
:key="idx"
|
||||
class="announcement-page__hot-search-tag"
|
||||
:class="{ 'announcement-page__hot-search-tag--active': keyword === tag }"
|
||||
@click="handleHotSearch(tag)"
|
||||
>{{ tag }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 公告列表(表格形式) -->
|
||||
<div ref="listRef" class="announcement-page__list scrollbar-thin" @scroll="onListScroll">
|
||||
<!-- 表头 -->
|
||||
<div class="announcement-page__table-header">
|
||||
<span class="announcement-page__th announcement-page__th--time flex0">发布时间</span>
|
||||
<span class="announcement-page__th announcement-page__th--company">公司/类型/行业</span>
|
||||
<span class="announcement-page__th announcement-page__th--title">公告标题</span>
|
||||
<span class="announcement-page__th announcement-page__th--position">招聘岗位/城市</span>
|
||||
<span class="announcement-page__th announcement-page__th--target">招聘对象</span>
|
||||
<span class="announcement-page__th announcement-page__th--major">专业要求</span>
|
||||
<span class="announcement-page__th announcement-page__th--batch">招聘批次</span>
|
||||
<span class="announcement-page__th announcement-page__th--deadline">截止日期</span>
|
||||
</div>
|
||||
<!-- 列表行 -->
|
||||
<div
|
||||
v-for="item in announcementList"
|
||||
:key="item.id"
|
||||
class="announcement-page__table-row"
|
||||
@click="goToDetail(item.id)"
|
||||
style="cursor: pointer;"
|
||||
>
|
||||
<!-- 发布时间 -->
|
||||
<span class="announcement-page__td announcement-page__td--time flex0 color-middle tac">{{ formatPublishTime(item) }}</span>
|
||||
<!-- 公司/类型/行业 -->
|
||||
<div class="announcement-page__td announcement-page__td--company">
|
||||
<div class="announcement-page__company-info">
|
||||
<img v-if="item.logoUrl" class="announcement-page__company-logo" :src="item.logoUrl" alt="" />
|
||||
<div v-else class="announcement-page__company-logo announcement-page__company-logo--default">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none"><rect width="24" height="24" rx="12" fill="#EDF9FA"/><path d="M5.99998 16.6667H18C18.1768 16.6667 18.3464 16.7369 18.4714 16.8619C18.5964 16.987 18.6666 17.1565 18.6666 17.3333C18.6666 17.5102 18.5964 17.6797 18.4714 17.8047C18.3464 17.9298 18.1768 18 18 18H5.99998C5.82317 18 5.6536 17.9298 5.52858 17.8047C5.40355 17.6797 5.33331 17.5102 5.33331 17.3333C5.33331 17.1565 5.40355 16.987 5.52858 16.8619C5.6536 16.7369 5.82317 16.6667 5.99998 16.6667Z" fill="#52CAD1"/></svg>
|
||||
</div>
|
||||
<div class="announcement-page__company-text">
|
||||
<span class="announcement-page__company-name">{{ item.companyName }}</span>
|
||||
<div class="announcement-page__company-tags">
|
||||
<span v-if="item.industryNames && item.industryNames.length" class="announcement-page__company-tag announcement-page__company-tag--industry">{{ item.industryNames.join('/') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 公告标题 -->
|
||||
<span class="announcement-page__td announcement-page__td--title" :title="item.title">{{ item.title }}</span>
|
||||
<!-- 招聘岗位/城市 -->
|
||||
<div class="announcement-page__td announcement-page__td--position">
|
||||
<span v-if="item.categoryNames && item.categoryNames.length" class="announcement-page__cell-line">{{ item.categoryNames.join(',') }}</span>
|
||||
<span v-if="item.cityNames && item.cityNames.length" class="announcement-page__cell-sub">{{ item.cityNames.join('、') }}</span>
|
||||
</div>
|
||||
<!-- 招聘对象 -->
|
||||
<div class="announcement-page__td announcement-page__td--target ">
|
||||
<span v-if="item.recruitYears && item.recruitYears.length" class="announcement-page__recruit-year">{{ item.recruitYears.map(y => y + '届').join(', ') }}</span>
|
||||
<span v-if="item.educationNames && item.educationNames.length" class="announcement-page__cell-sub ">{{ item.educationNames.join('、') }}</span>
|
||||
</div>
|
||||
<!-- 专业要求 -->
|
||||
<span class="announcement-page__td announcement-page__td--major" :title="item.majorRequire">{{ item.majorRequire || '专业不限' }}</span>
|
||||
<!-- 招聘批次 -->
|
||||
<span class="announcement-page__td announcement-page__td--batch">{{ item.batchNames && item.batchNames.length ? item.batchNames.join('、') : '' }}</span>
|
||||
<!-- 截止日期 -->
|
||||
<span class="announcement-page__td announcement-page__td--deadline">{{ formatApplyEndTime(item) }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 加载中 -->
|
||||
<div v-if="loading" class="announcement-page__loading-box">
|
||||
<div class="announcement-page__loading-spinner"></div>
|
||||
<span>加载中...</span>
|
||||
</div>
|
||||
<!-- 暂无数据 -->
|
||||
<div v-if="!loading && announcementList.length === 0" class="announcement-page__empty">
|
||||
<img class="announcement-page__empty-img" src="@/assets/images/list-empty.png" alt="暂无数据" />
|
||||
<p class="announcement-page__empty-title">暂无符合条件的校招公告</p>
|
||||
<p class="announcement-page__empty-subtitle">试试 <span class="announcement-page__empty-link" @click="clearAllFilters">减少筛选条件</span> 看看吧~</p>
|
||||
</div>
|
||||
<!-- 加载更多 -->
|
||||
<div v-if="loadingMore" class="announcement-page__loading-box">
|
||||
<div class="announcement-page__loading-spinner"></div>
|
||||
<span>加载中...</span>
|
||||
</div>
|
||||
<div v-else-if="noMore && announcementList.length > 0" class="announcement-page__loading-more">没有更多公告了</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, onBeforeUnmount, nextTick } from 'vue'
|
||||
import { useStore } from 'vuex'
|
||||
import { useRouter } from 'vue-router'
|
||||
import SideNav from '@/components/SideNav.vue'
|
||||
import JobStatsOverview from '@/components/JobStatsOverview.vue'
|
||||
import IndustrySelector from '@/components/tools/IndustrySelector.vue'
|
||||
import JobCategorySelector from '@/components/tools/JobCategorySelector.vue'
|
||||
import RegionSelector from '@/components/tools/RegionSelector.vue'
|
||||
import { fetchAnnouncementList } from '@/api/announcement'
|
||||
import type { AnnouncementListParams, AnnouncementItem } from '@/api/announcement'
|
||||
import { fetchHotIndustries, fetchHotCategories, fetchHotCities } from '@/api/jobs'
|
||||
import type { HotIndustryItem, HotCategoryItem, HotCityItem } from '@/api/jobs'
|
||||
import type { IndustryItem, JobCategoryItem } from '@/api/common'
|
||||
|
||||
// ==================== 全局 Store ====================
|
||||
|
||||
const store = useStore()
|
||||
const router = useRouter()
|
||||
|
||||
/** 跳转公告详情页 */
|
||||
function goToDetail(id: number) {
|
||||
router.push(`/announcement/${id}`)
|
||||
}
|
||||
|
||||
// ==================== 筛选面板高度同步 ====================
|
||||
|
||||
/** 筛选面板 DOM 引用 */
|
||||
const filtersPanelRef = ref<HTMLElement | null>(null)
|
||||
|
||||
/** 筛选面板高度(同步给统计卡片) */
|
||||
const filtersPanelHeight = ref<string>('auto')
|
||||
|
||||
/** 监听筛选面板高度变化的 ResizeObserver */
|
||||
let filtersPanelObserver: ResizeObserver | null = null
|
||||
|
||||
// ==================== 统计卡片数据 ====================
|
||||
|
||||
/** 当前日期(MM.DD 格式) */
|
||||
const statsCurrentDate = computed(() => {
|
||||
const now = new Date()
|
||||
return `${String(now.getMonth() + 1).padStart(2, '0')}.${String(now.getDate()).padStart(2, '0')}`
|
||||
})
|
||||
|
||||
/** 今日新增公告数 */
|
||||
const statsTodayCount = ref<string | number>('128')
|
||||
|
||||
/** 招聘中企业数 */
|
||||
const statsTotalCompany = ref<string | number>('326')
|
||||
|
||||
/** 近7日截止数 */
|
||||
const statsDeadline7days = ref<string | number>('42')
|
||||
|
||||
// ==================== 热门分类数据 ====================
|
||||
|
||||
/** 热门行业列表 */
|
||||
const hotIndustries = ref<HotIndustryItem[]>([])
|
||||
|
||||
/** 热门岗位列表 */
|
||||
const hotCategories = ref<HotCategoryItem[]>([])
|
||||
|
||||
/** 热门城市列表 */
|
||||
const hotCities = ref<HotCityItem[]>([])
|
||||
|
||||
/** 加载热门推荐数据 */
|
||||
async function loadHotFilters() {
|
||||
try {
|
||||
const [indRes, catRes, cityRes] = await Promise.all([
|
||||
fetchHotIndustries(),
|
||||
fetchHotCategories(),
|
||||
fetchHotCities(),
|
||||
])
|
||||
if (indRes.code === '0' && indRes.data) hotIndustries.value = indRes.data
|
||||
if (catRes.code === '0' && catRes.data) hotCategories.value = catRes.data
|
||||
if (cityRes.code === '0' && cityRes.data) hotCities.value = cityRes.data
|
||||
} catch (e) {
|
||||
console.error('加载热门分类失败', e)
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 筛选状态 ====================
|
||||
|
||||
/** 选中的行业名称列表(传给接口) */
|
||||
const selectedIndustryNames = ref<string[]>([])
|
||||
|
||||
/** 选中的行业 ID 列表(给 IndustrySelector 组件用) */
|
||||
const selectedIndustryIds = ref<number[]>([])
|
||||
|
||||
/** 选中的岗位名称列表(传给接口) */
|
||||
const selectedCategoryNames = ref<string[]>([])
|
||||
|
||||
/** 选中的岗位 ID 列表(给 JobCategorySelector 组件用) */
|
||||
const selectedCategoryIds = ref<number[]>([])
|
||||
|
||||
/** 选中的城市名称列表(传给接口) */
|
||||
const selectedCityNames = ref<string[]>([])
|
||||
|
||||
/** 选中的地区编码列表(给 RegionSelector 组件用) */
|
||||
const selectedRegionCodes = ref<string[]>([])
|
||||
|
||||
/** 选中的学历要求名称(单选,空字符串表示未选) */
|
||||
const selectedEducationName = ref<string>('')
|
||||
|
||||
/** 选中的招聘届数(单选,0表示未选/不限) */
|
||||
const selectedRecruitYear = ref<number>(0)
|
||||
|
||||
/** 选中的公司类型(单选,空字符串表示未选) */
|
||||
const selectedCompanyType = ref<string>('')
|
||||
|
||||
/** 选中的批次名称(单选,空字符串表示未选) */
|
||||
const selectedBatchName = ref<string>('')
|
||||
|
||||
/** 发布时间范围 */
|
||||
const publishTimeRange = ref<[Date, Date] | null>(null)
|
||||
|
||||
/** 搜索关键词 */
|
||||
const keyword = ref('')
|
||||
|
||||
// ==================== 下拉菜单显示状态 ====================
|
||||
|
||||
const showRecruitYearDropdown = ref(false)
|
||||
const showEducationDropdown = ref(false)
|
||||
const showCompanyTypeDropdown = ref(false)
|
||||
const showBatchDropdown = ref(false)
|
||||
|
||||
// ==================== 筛选选项常量 ====================
|
||||
|
||||
/** 毕业年份选项 */
|
||||
const recruitYearOptions = computed(() => {
|
||||
const currentYear = new Date().getFullYear()
|
||||
return [
|
||||
{ label: '全部', value: 0 },
|
||||
{ label: `${currentYear}届`, value: currentYear },
|
||||
{ label: `${currentYear + 1}届`, value: currentYear + 1 },
|
||||
{ label: `${currentYear + 2}届`, value: currentYear + 2 },
|
||||
]
|
||||
})
|
||||
|
||||
/** 学历要求选项 */
|
||||
const educationOptions = ['不限', '大专及以上', '本科及以上', '硕士及以上', '博士及以上']
|
||||
|
||||
/** 公司类型选项 */
|
||||
const companyTypeOptions = ['全部', '央企', '国企', '上市企业', '外资企业', '民营企业', '事业单位']
|
||||
|
||||
/** 招聘批次选项 */
|
||||
const batchOptions = ['全部', '秋招正式批', '秋招提前批', '春招正式批', '春招补录', '暑期实习']
|
||||
|
||||
/** 热门搜索标签 */
|
||||
const hotSearchTags = ref(['系统集成', 'AI产品经理', '广告投放', 'AI开发', '大模型'])
|
||||
|
||||
// ==================== 筛选显示标签 ====================
|
||||
|
||||
/** 毕业年份筛选显示文字 */
|
||||
const recruitYearLabel = computed(() => {
|
||||
if (!selectedRecruitYear.value) return '毕业年份'
|
||||
return selectedRecruitYear.value + '届'
|
||||
})
|
||||
|
||||
/** 学历要求筛选显示文字 */
|
||||
const educationFilterLabel = computed(() => {
|
||||
if (!selectedEducationName.value) return '学历要求'
|
||||
return selectedEducationName.value
|
||||
})
|
||||
|
||||
/** 公司类型筛选显示文字 */
|
||||
const companyTypeLabel = computed(() => {
|
||||
if (!selectedCompanyType.value) return '公司类型'
|
||||
return selectedCompanyType.value
|
||||
})
|
||||
|
||||
/** 招聘批次筛选显示文字 */
|
||||
const batchLabel = computed(() => {
|
||||
if (!selectedBatchName.value) return '招聘批次'
|
||||
return selectedBatchName.value
|
||||
})
|
||||
|
||||
// ==================== 筛选操作方法 ====================
|
||||
|
||||
/** 切换下拉菜单 */
|
||||
function toggleDropdown(type: string) {
|
||||
showRecruitYearDropdown.value = type === 'recruitYear' ? !showRecruitYearDropdown.value : false
|
||||
showEducationDropdown.value = type === 'education' ? !showEducationDropdown.value : false
|
||||
showCompanyTypeDropdown.value = type === 'companyType' ? !showCompanyTypeDropdown.value : false
|
||||
showBatchDropdown.value = type === 'batch' ? !showBatchDropdown.value : false
|
||||
}
|
||||
|
||||
/** 切换行业选中(按名称) */
|
||||
function toggleIndustry(name: string) {
|
||||
const idx = selectedIndustryNames.value.indexOf(name)
|
||||
if (idx >= 0) {
|
||||
selectedIndustryNames.value.splice(idx, 1)
|
||||
} else {
|
||||
selectedIndustryNames.value.push(name)
|
||||
}
|
||||
// 同步 ID 给选择器组件
|
||||
syncIndustryIds()
|
||||
reloadFirstPage()
|
||||
}
|
||||
|
||||
/** 清空行业筛选 */
|
||||
function clearIndustryFilter() {
|
||||
if (selectedIndustryNames.value.length === 0) return
|
||||
selectedIndustryNames.value = []
|
||||
selectedIndustryIds.value = []
|
||||
reloadFirstPage()
|
||||
}
|
||||
|
||||
/** IndustrySelector 回调 — 根据选中的 ID 反查名称 */
|
||||
function onIndustryChange(ids: number[]) {
|
||||
selectedIndustryIds.value = ids
|
||||
// 从完整行业树中反查名称
|
||||
const industries: IndustryItem[] = store.state.industries || []
|
||||
const names: string[] = []
|
||||
for (const id of ids) {
|
||||
const strId = String(id)
|
||||
for (const p of industries) {
|
||||
if (String(p.id) === strId) { names.push(p.name); break }
|
||||
const child = p.children.find(c => String(c.id) === strId)
|
||||
if (child) { names.push(child.name); break }
|
||||
}
|
||||
}
|
||||
selectedIndustryNames.value = names
|
||||
reloadFirstPage()
|
||||
}
|
||||
|
||||
/** 从名称同步行业 ID(给选择器组件) */
|
||||
function syncIndustryIds() {
|
||||
const industries: IndustryItem[] = store.state.industries || []
|
||||
const ids: number[] = []
|
||||
for (const name of selectedIndustryNames.value) {
|
||||
for (const p of industries) {
|
||||
if (p.name === name) { ids.push(Number(p.id)); break }
|
||||
const child = p.children.find(c => c.name === name)
|
||||
if (child) { ids.push(Number(child.id)); break }
|
||||
}
|
||||
}
|
||||
selectedIndustryIds.value = ids
|
||||
}
|
||||
|
||||
/** 切换岗位选中(按名称) */
|
||||
function toggleCategory(name: string) {
|
||||
const idx = selectedCategoryNames.value.indexOf(name)
|
||||
if (idx >= 0) {
|
||||
selectedCategoryNames.value.splice(idx, 1)
|
||||
} else {
|
||||
selectedCategoryNames.value.push(name)
|
||||
}
|
||||
syncCategoryIds()
|
||||
reloadFirstPage()
|
||||
}
|
||||
|
||||
/** 清空岗位筛选 */
|
||||
function clearCategoryFilter() {
|
||||
if (selectedCategoryNames.value.length === 0) return
|
||||
selectedCategoryNames.value = []
|
||||
selectedCategoryIds.value = []
|
||||
reloadFirstPage()
|
||||
}
|
||||
|
||||
/** JobCategorySelector 回调 — 根据选中的 ID 反查名称 */
|
||||
function onCategoryChange(ids: number[]) {
|
||||
selectedCategoryIds.value = ids
|
||||
const categories: JobCategoryItem[] = store.state.jobCategories || []
|
||||
const names: string[] = []
|
||||
for (const id of ids) {
|
||||
const strId = String(id)
|
||||
for (const l1 of categories) {
|
||||
if (String(l1.id) === strId) { names.push(l1.name); break }
|
||||
for (const l2 of l1.children) {
|
||||
if (String(l2.id) === strId) { names.push(l2.name); break }
|
||||
if (l2.children) {
|
||||
const l3 = l2.children.find(c => String(c.id) === strId)
|
||||
if (l3) { names.push(l3.name); break }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
selectedCategoryNames.value = names
|
||||
reloadFirstPage()
|
||||
}
|
||||
|
||||
/** 从名称同步岗位 ID(给选择器组件) */
|
||||
function syncCategoryIds() {
|
||||
const categories: JobCategoryItem[] = store.state.jobCategories || []
|
||||
const ids: number[] = []
|
||||
for (const name of selectedCategoryNames.value) {
|
||||
for (const l1 of categories) {
|
||||
if (l1.name === name) { ids.push(Number(l1.id)); break }
|
||||
for (const l2 of l1.children) {
|
||||
if (l2.name === name) { ids.push(Number(l2.id)); break }
|
||||
if (l2.children) {
|
||||
const l3 = l2.children.find(c => c.name === name)
|
||||
if (l3) { ids.push(Number(l3.id)); break }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
selectedCategoryIds.value = ids
|
||||
}
|
||||
|
||||
/** 切换城市选中(按名称) */
|
||||
function toggleCity(name: string) {
|
||||
const idx = selectedCityNames.value.indexOf(name)
|
||||
if (idx >= 0) {
|
||||
selectedCityNames.value.splice(idx, 1)
|
||||
} else {
|
||||
selectedCityNames.value.push(name)
|
||||
}
|
||||
syncRegionCodes()
|
||||
reloadFirstPage()
|
||||
}
|
||||
|
||||
/** 清空城市筛选 */
|
||||
function clearCityFilter() {
|
||||
if (selectedCityNames.value.length === 0) return
|
||||
selectedCityNames.value = []
|
||||
selectedRegionCodes.value = []
|
||||
reloadFirstPage()
|
||||
}
|
||||
|
||||
/** RegionSelector 回调 — 根据选中的编码反查名称 */
|
||||
function onRegionChange(codes: string[]) {
|
||||
selectedRegionCodes.value = codes
|
||||
const regions = store.state.regions || []
|
||||
const names: string[] = []
|
||||
for (const code of codes) {
|
||||
for (const province of regions) {
|
||||
for (const city of province.children) {
|
||||
if (city.code === code) { names.push(city.name); break }
|
||||
}
|
||||
}
|
||||
}
|
||||
selectedCityNames.value = names
|
||||
reloadFirstPage()
|
||||
}
|
||||
|
||||
/** 从名称同步地区编码(给选择器组件) */
|
||||
function syncRegionCodes() {
|
||||
const regions = store.state.regions || []
|
||||
const codes: string[] = []
|
||||
for (const name of selectedCityNames.value) {
|
||||
for (const province of regions) {
|
||||
for (const city of province.children) {
|
||||
if (city.name === name) { codes.push(city.code); break }
|
||||
}
|
||||
}
|
||||
}
|
||||
selectedRegionCodes.value = codes
|
||||
}
|
||||
|
||||
/** 切换毕业年份选中 */
|
||||
/** 选择毕业年份(单选) */
|
||||
function selectRecruitYear(value: number) {
|
||||
selectedRecruitYear.value = value === selectedRecruitYear.value ? 0 : value
|
||||
showRecruitYearDropdown.value = false
|
||||
reloadFirstPage()
|
||||
}
|
||||
|
||||
/** 选择学历(单选) */
|
||||
function selectEducation(value: string) {
|
||||
if (value === '不限') {
|
||||
selectedEducationName.value = ''
|
||||
} else {
|
||||
selectedEducationName.value = value === selectedEducationName.value ? '' : value
|
||||
}
|
||||
showEducationDropdown.value = false
|
||||
reloadFirstPage()
|
||||
}
|
||||
|
||||
/** 选择公司类型(单选) */
|
||||
function selectCompanyType(value: string) {
|
||||
if (value === '全部') {
|
||||
selectedCompanyType.value = ''
|
||||
} else {
|
||||
selectedCompanyType.value = value === selectedCompanyType.value ? '' : value
|
||||
}
|
||||
showCompanyTypeDropdown.value = false
|
||||
reloadFirstPage()
|
||||
}
|
||||
|
||||
/** 选择批次(单选) */
|
||||
function selectBatch(value: string) {
|
||||
if (value === '全部') {
|
||||
selectedBatchName.value = ''
|
||||
} else {
|
||||
selectedBatchName.value = value === selectedBatchName.value ? '' : value
|
||||
}
|
||||
showBatchDropdown.value = false
|
||||
reloadFirstPage()
|
||||
}
|
||||
|
||||
/** 发布时间变更回调 */
|
||||
function onPublishTimeChange(val: [Date, Date] | null) {
|
||||
publishTimeRange.value = val
|
||||
reloadFirstPage()
|
||||
}
|
||||
|
||||
/** 清空所有筛选条件 */
|
||||
function clearAllFilters() {
|
||||
selectedIndustryNames.value = []
|
||||
selectedIndustryIds.value = []
|
||||
selectedCategoryNames.value = []
|
||||
selectedCategoryIds.value = []
|
||||
selectedCityNames.value = []
|
||||
selectedRegionCodes.value = []
|
||||
selectedEducationName.value = ''
|
||||
selectedRecruitYear.value = 0
|
||||
selectedCompanyType.value = ''
|
||||
selectedBatchName.value = ''
|
||||
publishTimeRange.value = null
|
||||
keyword.value = ''
|
||||
reloadFirstPage()
|
||||
}
|
||||
|
||||
/** 是否有任何筛选条件处于激活状态 */
|
||||
const hasActiveFilters = computed(() => {
|
||||
return selectedIndustryNames.value.length > 0
|
||||
|| selectedCategoryNames.value.length > 0
|
||||
|| selectedCityNames.value.length > 0
|
||||
|| selectedEducationName.value !== ''
|
||||
|| selectedRecruitYear.value !== 0
|
||||
|| selectedCompanyType.value !== ''
|
||||
|| selectedBatchName.value !== ''
|
||||
|| publishTimeRange.value !== null
|
||||
|| keyword.value.trim() !== ''
|
||||
})
|
||||
|
||||
/** 点击热门搜索标签 */
|
||||
function handleHotSearch(tag: string) {
|
||||
keyword.value = tag
|
||||
reloadFirstPage()
|
||||
}
|
||||
|
||||
// ==================== "更多"按钮样式 ====================
|
||||
|
||||
/** "更多"按钮触发器样式 */
|
||||
const moreBtnTriggerStyle: Record<string, string> = {
|
||||
'box-sizing': 'border-box',
|
||||
padding: '0.07rem 0.08rem',
|
||||
'font-size': '0.12rem',
|
||||
color: '#1A1A1A',
|
||||
background: '#F3F4F6',
|
||||
border: 'none',
|
||||
'border-radius': '0.04rem',
|
||||
'max-width': 'none',
|
||||
'justify-content': 'center',
|
||||
cursor: 'pointer',
|
||||
}
|
||||
|
||||
/** "更多"按钮hover样式 */
|
||||
const moreBtnHoverStyle: Record<string, string> = {
|
||||
background: '#F3F4F6',
|
||||
color: '#1A1A1A',
|
||||
}
|
||||
|
||||
/** 选择器显示文字自定义样式 */
|
||||
const selectorDisplayStyle: Record<string, string> = {
|
||||
'max-width': 'none',
|
||||
}
|
||||
|
||||
// ==================== 列表数据与分页 ====================
|
||||
|
||||
/** 当前页码 */
|
||||
const pageNum = ref(1)
|
||||
|
||||
/** 每页条数 */
|
||||
const pageSize = ref(10)
|
||||
|
||||
/** 总记录数 */
|
||||
const total = ref(0)
|
||||
|
||||
/** 是否正在加载 */
|
||||
const loading = ref(false)
|
||||
|
||||
/** 是否正在加载下一页 */
|
||||
const loadingMore = ref(false)
|
||||
|
||||
/** 是否无更多数据 */
|
||||
const hasNoMoreData = ref(false)
|
||||
|
||||
/** 是否已加载全部 */
|
||||
const noMore = computed(() => hasNoMoreData.value || (announcementList.value.length >= total.value && total.value > 0))
|
||||
|
||||
/** 公告列表数据 */
|
||||
const announcementList = ref<AnnouncementItem[]>([])
|
||||
|
||||
/** 列表容器 ref */
|
||||
const listRef = ref<HTMLElement | null>(null)
|
||||
|
||||
// ==================== 列表请求逻辑 ====================
|
||||
|
||||
/** 组装请求参数 */
|
||||
function buildParams(): AnnouncementListParams {
|
||||
const params: AnnouncementListParams = {
|
||||
pageNum: pageNum.value,
|
||||
pageSize: pageSize.value,
|
||||
}
|
||||
if (keyword.value.trim()) params.keyword = keyword.value.trim()
|
||||
if (selectedIndustryNames.value.length) params.industryNames = selectedIndustryNames.value
|
||||
if (selectedCategoryNames.value.length) params.categoryNames = selectedCategoryNames.value
|
||||
if (selectedCityNames.value.length) params.cityNames = selectedCityNames.value
|
||||
if (selectedEducationName.value) params.educationNames = [selectedEducationName.value]
|
||||
if (selectedRecruitYear.value) params.recruitYears = [selectedRecruitYear.value]
|
||||
if (selectedCompanyType.value) params.companyTypes = [selectedCompanyType.value]
|
||||
if (selectedBatchName.value) params.batchNames = [selectedBatchName.value]
|
||||
// 发布时间转 Instant
|
||||
if (publishTimeRange.value) {
|
||||
const [start, end] = publishTimeRange.value
|
||||
params.publishTimeStart = { seconds: Math.floor(start.getTime() / 1000), nanos: 0 }
|
||||
params.publishTimeEnd = { seconds: Math.floor(end.getTime() / 1000), nanos: 0 }
|
||||
}
|
||||
return params
|
||||
}
|
||||
|
||||
/** 加载公告列表(首次加载) */
|
||||
async function loadList() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await fetchAnnouncementList(buildParams())
|
||||
if (res.code === '0' && res.data) {
|
||||
announcementList.value = res.data.list || []
|
||||
total.value = res.data.total
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('加载校招公告列表失败', e)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 加载下一页 */
|
||||
async function loadNextPage() {
|
||||
if (loadingMore.value || noMore.value) return
|
||||
loadingMore.value = true
|
||||
pageNum.value++
|
||||
try {
|
||||
const res = await fetchAnnouncementList(buildParams())
|
||||
if (res.code === '0' && res.data) {
|
||||
const newItems = res.data.list || []
|
||||
if (newItems.length === 0) {
|
||||
hasNoMoreData.value = true
|
||||
pageNum.value--
|
||||
} else {
|
||||
announcementList.value.push(...newItems)
|
||||
total.value = res.data.total
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
pageNum.value--
|
||||
console.error('加载下一页失败', e)
|
||||
} finally {
|
||||
loadingMore.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 列表滚动事件 — 触底自动加载 */
|
||||
function onListScroll() {
|
||||
const el = listRef.value
|
||||
if (!el) return
|
||||
if (el.scrollHeight - el.scrollTop - el.clientHeight < 100) {
|
||||
loadNextPage()
|
||||
}
|
||||
}
|
||||
|
||||
/** 筛选变化时重置到第一页 */
|
||||
function reloadFirstPage() {
|
||||
pageNum.value = 1
|
||||
hasNoMoreData.value = false
|
||||
announcementList.value = []
|
||||
total.value = 0
|
||||
loadList()
|
||||
}
|
||||
|
||||
// ==================== 时间格式化 ====================
|
||||
|
||||
/** 格式化发布时间(取月-日,如 "07-25") */
|
||||
function formatPublishTime(item: AnnouncementItem): string {
|
||||
if (!item.publishTime) return '--'
|
||||
try {
|
||||
const date = new Date(item.publishTime)
|
||||
if (isNaN(date.getTime())) return '--'
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
return `${month}-${day}`
|
||||
} catch {
|
||||
return '--'
|
||||
}
|
||||
}
|
||||
|
||||
/** 格式化投递截止时间 */
|
||||
function formatApplyEndTime(item: AnnouncementItem): string {
|
||||
if (item.applyEndDesc) return item.applyEndDesc
|
||||
if (!item.applyEndTime || !item.applyEndTime.seconds) return '招满即止'
|
||||
try {
|
||||
const date = new Date(item.applyEndTime.seconds * 1000)
|
||||
if (isNaN(date.getTime())) return '招满即止'
|
||||
const y = date.getFullYear()
|
||||
const m = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const d = String(date.getDate()).padStart(2, '0')
|
||||
return `${y}-${m}-${d}`
|
||||
} catch {
|
||||
return '--'
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 点击页面其他区域关闭下拉 ====================
|
||||
|
||||
function closeDropdownOnClickOutside(e: MouseEvent) {
|
||||
const target = e.target as HTMLElement
|
||||
if (target.closest('.el-picker-panel') || target.closest('.el-date-picker') || target.closest('.el-popper')) return
|
||||
if (!target.closest('.announcement-page__dropdown-item')) {
|
||||
showRecruitYearDropdown.value = false
|
||||
showEducationDropdown.value = false
|
||||
showCompanyTypeDropdown.value = false
|
||||
showBatchDropdown.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 生命周期 ====================
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('click', closeDropdownOnClickOutside)
|
||||
|
||||
// 加载公共分类数据(行业、岗位、地区树)
|
||||
store.dispatch('loadCommonData')
|
||||
|
||||
// 加载热门分类
|
||||
loadHotFilters()
|
||||
|
||||
// 监听筛选面板高度变化
|
||||
nextTick(() => {
|
||||
if (filtersPanelRef.value) {
|
||||
filtersPanelHeight.value = filtersPanelRef.value.offsetHeight + 'px'
|
||||
filtersPanelObserver = new ResizeObserver((entries) => {
|
||||
for (const entry of entries) {
|
||||
filtersPanelHeight.value = entry.target.clientHeight + 'px'
|
||||
}
|
||||
})
|
||||
filtersPanelObserver.observe(filtersPanelRef.value)
|
||||
}
|
||||
})
|
||||
|
||||
// 加载列表数据
|
||||
loadList()
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
document.removeEventListener('click', closeDropdownOnClickOutside)
|
||||
if (filtersPanelObserver) {
|
||||
filtersPanelObserver.disconnect()
|
||||
filtersPanelObserver = null
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,506 @@
|
||||
<template>
|
||||
<div class="announcement-detail dflex">
|
||||
<SideNav />
|
||||
<div class="announcement-detail__content">
|
||||
<!-- 页面标题 -->
|
||||
<div class="bg-f mb16 prl24 ptb16 dflex aliite-c">
|
||||
<div class="dflex-start aliite-c">
|
||||
<h2 class="announcement-detail__page-title">校招公告</h2>
|
||||
<span class="announcement-detail__page-subtitle ml12">每日更新最新企业校招信息,助力你的求职之路</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 返回按钮 -->
|
||||
<div class="announcement-detail__back-row">
|
||||
<button class="announcement-detail__back-btn" @click="goBack">
|
||||
<svg viewBox="0 0 16 16" fill="none" class="announcement-detail__back-icon">
|
||||
<path d="M10 12L6 8l4-4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
<span>返回</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 主体内容(可滚动) -->
|
||||
<div class="announcement-detail__body scrollbar-thin">
|
||||
<!-- 加载中骨架屏 -->
|
||||
<el-skeleton :loading="loading" animated :count="1">
|
||||
<template #template>
|
||||
<div style="padding: 0.24rem 0.32rem;">
|
||||
<el-skeleton-item variant="h1" style="width: 40%; height: 0.28rem; margin-bottom: 0.16rem;" />
|
||||
<el-skeleton-item variant="text" style="width: 80%; height: 0.16rem; margin-bottom: 0.12rem;" />
|
||||
<el-skeleton-item variant="text" style="width: 60%; height: 0.16rem; margin-bottom: 0.24rem;" />
|
||||
<el-skeleton-item variant="rect" style="width: 100%; height: 2rem; margin-bottom: 0.24rem;" />
|
||||
</div>
|
||||
</template>
|
||||
<template #default>
|
||||
<!-- 顶部公司信息卡片(独占一行) -->
|
||||
<div class="announcement-detail__company-card">
|
||||
<div class="announcement-detail__company-left">
|
||||
<!-- 公司logo -->
|
||||
<img
|
||||
v-if="detail.logoUrl"
|
||||
:src="detail.logoUrl"
|
||||
class="announcement-detail__company-logo"
|
||||
:alt="detail.companyName"
|
||||
/>
|
||||
<div v-else class="announcement-detail__company-logo announcement-detail__company-logo--default">
|
||||
<svg width="40" height="40" viewBox="0 0 40 40" fill="none">
|
||||
<rect width="40" height="40" rx="20" fill="#EDF9FA"/>
|
||||
<path d="M10 27H30" stroke="#52CAD1" stroke-width="2" stroke-linecap="round"/>
|
||||
<rect x="14" y="13" width="12" height="14" rx="1" stroke="#52CAD1" stroke-width="1.5"/>
|
||||
</svg>
|
||||
</div>
|
||||
<!-- 公司名称+标签 -->
|
||||
<div class="announcement-detail__company-info">
|
||||
<div class="announcement-detail__company-name-row">
|
||||
<span class="announcement-detail__company-name">{{ detail.companyName }}</span>
|
||||
</div>
|
||||
<div class="announcement-detail__company-tags">
|
||||
<span v-if="detail.companyType" class="announcement-detail__tag announcement-detail__tag--type">{{ detail.companyType }}</span>
|
||||
<span v-if="detail.industryNames && detail.industryNames.length" class="announcement-detail__tag announcement-detail__tag--gray">{{ detail.industryNames.join('/') }}</span>
|
||||
<span v-if="companyScaleText" class="announcement-detail__tag announcement-detail__tag--gray">{{ companyScaleText }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 右侧按钮 -->
|
||||
<div class="announcement-detail__company-right">
|
||||
<button class="announcement-detail__btn-outline" @click="handleViewAnnouncement">查看公告</button>
|
||||
<button class="announcement-detail__btn-primary" @click="handleApplyUrl">投递地址</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 下方两列布局:左侧详情 + 右侧边栏 -->
|
||||
<div class="announcement-detail__layout">
|
||||
<!-- 左侧主详情 -->
|
||||
<div class="announcement-detail__left">
|
||||
<!-- 岗位速览卡片 -->
|
||||
<div class="announcement-detail__overview-card">
|
||||
<!-- 速览标题行 -->
|
||||
<div class="announcement-detail__overview-header">
|
||||
<div class="announcement-detail__overview-title-row">
|
||||
<span class="announcement-detail__overview-title">岗位速览</span>
|
||||
<span class="announcement-detail__overview-date">{{ publishDateText }}发布</span>
|
||||
</div>
|
||||
<span class="announcement-detail__overview-deadline">{{ applyEndText }}截止</span>
|
||||
</div>
|
||||
<!-- 速览内容 -->
|
||||
<div class="announcement-detail__overview-body">
|
||||
<!-- 左侧字段区域 -->
|
||||
<div class="announcement-detail__overview-fields">
|
||||
<!-- 第一行:招聘对象 + 工作地点 -->
|
||||
<div class="announcement-detail__overview-row">
|
||||
<div class="announcement-detail__overview-field flex1">
|
||||
<span class="announcement-detail__overview-label">招聘对象</span>
|
||||
<span class="announcement-detail__overview-value">{{ detail.targetAudience || '--' }}</span>
|
||||
</div>
|
||||
<div class="announcement-detail__overview-field flex2">
|
||||
<span class="announcement-detail__overview-label">工作地点</span>
|
||||
<span class="announcement-detail__overview-value">{{ detail.cityNames && detail.cityNames.length ? detail.cityNames.join('、') : '--' }}</span>
|
||||
</div>
|
||||
<!-- 右侧岗位方向 -->
|
||||
<div class="announcement-detail__overview-direction flex2">
|
||||
<div class="announcement-detail__overview-label">岗位方向</div>
|
||||
<div class="announcement-detail__overview-value">{{ detail.recruitPosition || '--' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 第二行:学历要求 + 专业要求 -->
|
||||
<div class="announcement-detail__overview-row">
|
||||
<div class="announcement-detail__overview-field flex1">
|
||||
<span class="announcement-detail__overview-label">学历要求</span>
|
||||
<span class="announcement-detail__overview-value">{{ detail.educationNames && detail.educationNames.length ? detail.educationNames.join('、') : '--' }}</span>
|
||||
</div>
|
||||
<div class="announcement-detail__overview-field flex2">
|
||||
<span class="announcement-detail__overview-label">专业要求</span>
|
||||
<span class="announcement-detail__overview-value">{{ detail.majorRequire || '--' }}</span>
|
||||
</div>
|
||||
<div class="flex2"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 公告标题 -->
|
||||
<h2 class="announcement-detail__main-title">{{ detail.title }}</h2>
|
||||
|
||||
<!-- 详情正文各段落 -->
|
||||
<div v-if="detail.companyIntro" class="announcement-detail__section">
|
||||
<h3 class="announcement-detail__section-title">企业介绍</h3>
|
||||
<p class="announcement-detail__section-content">{{ detail.companyIntro }}</p>
|
||||
</div>
|
||||
|
||||
<div v-if="detail.recruitPosition" class="announcement-detail__section">
|
||||
<h3 class="announcement-detail__section-title">招聘岗位</h3>
|
||||
<p class="announcement-detail__section-content">{{ detail.recruitPosition }}</p>
|
||||
</div>
|
||||
|
||||
<div v-if="detail.cityNames && detail.cityNames.length" class="announcement-detail__section">
|
||||
<h3 class="announcement-detail__section-title">招聘城市</h3>
|
||||
<p class="announcement-detail__section-content">{{ detail.cityNames.join('、') }}</p>
|
||||
</div>
|
||||
|
||||
<div v-if="detail.majorRequire" class="announcement-detail__section">
|
||||
<h3 class="announcement-detail__section-title">专业要求</h3>
|
||||
<p class="announcement-detail__section-content">{{ detail.majorRequire }}</p>
|
||||
</div>
|
||||
|
||||
<div v-if="detail.targetAudience" class="announcement-detail__section">
|
||||
<h3 class="announcement-detail__section-title">招聘对象</h3>
|
||||
<p class="announcement-detail__section-content">{{ detail.targetAudience }}</p>
|
||||
</div>
|
||||
|
||||
<div v-if="detail.remark" class="announcement-detail__section">
|
||||
<h3 class="announcement-detail__section-title">备注</h3>
|
||||
<p class="announcement-detail__section-content">{{ detail.remark }}</p>
|
||||
</div>
|
||||
|
||||
<div v-if="detail.writtenExam" class="announcement-detail__section">
|
||||
<h3 class="announcement-detail__section-title">笔试</h3>
|
||||
<p class="announcement-detail__section-content">{{ detail.writtenExam }}</p>
|
||||
</div>
|
||||
|
||||
<div v-if="detail.source" class="announcement-detail__section">
|
||||
<h3 class="announcement-detail__section-title">信息来源</h3>
|
||||
<p class="announcement-detail__section-content">{{ detail.source }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧边栏 -->
|
||||
<div class="announcement-detail__right">
|
||||
<!-- 企业其他岗位 -->
|
||||
<div class="announcement-detail__aside-card">
|
||||
<div class="announcement-detail__aside-header">
|
||||
<span class="announcement-detail__aside-title">企业其他岗位</span>
|
||||
<span class="announcement-detail__aside-more" @click="refreshOtherJobs">
|
||||
<svg width="19" height="19" viewBox="0 0 19 19" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_2812_6238)">
|
||||
<path d="M9.50008 15.8332C7.73203 15.8332 6.23446 15.2196 5.00737 13.9925C3.78029 12.7655 3.16675 11.2679 3.16675 9.49984C3.16675 7.73178 3.78029 6.23421 5.00737 5.00713C6.23446 3.78005 7.73203 3.16651 9.50008 3.16651C10.4105 3.16651 11.2813 3.3544 12.1126 3.73017C12.9438 4.10595 13.6563 4.64376 14.2501 5.34359V3.95817C14.2501 3.73387 14.3261 3.54598 14.4781 3.39451C14.6301 3.24303 14.818 3.16703 15.0417 3.16651C15.2655 3.16598 15.4537 3.24198 15.6062 3.39451C15.7587 3.54703 15.8345 3.73492 15.8334 3.95817V7.91651C15.8334 8.14081 15.7574 8.32897 15.6054 8.48097C15.4534 8.63297 15.2655 8.7087 15.0417 8.70817H11.0834C10.8591 8.70817 10.6712 8.63217 10.5197 8.48017C10.3683 8.32817 10.2923 8.14028 10.2917 7.91651C10.2912 7.69273 10.3672 7.50484 10.5197 7.35284C10.6723 7.20084 10.8602 7.12484 11.0834 7.12484H13.6167C13.1945 6.38595 12.6174 5.8054 11.8854 5.38317C11.1533 4.96095 10.3582 4.74984 9.50008 4.74984C8.18064 4.74984 7.05911 5.21165 6.1355 6.13526C5.21189 7.05887 4.75008 8.1804 4.75008 9.49984C4.75008 10.8193 5.21189 11.9408 6.1355 12.8644C7.05911 13.788 8.18064 14.2498 9.50008 14.2498C10.3973 14.2498 11.2188 14.0224 11.9645 13.5674C12.7103 13.1125 13.2874 12.5021 13.6959 11.7363C13.8015 11.5516 13.95 11.4231 14.1416 11.3508C14.3332 11.2785 14.5277 11.275 14.7251 11.3405C14.9362 11.4064 15.0879 11.545 15.1803 11.7561C15.2727 11.9672 15.2661 12.1651 15.1605 12.3498C14.6195 13.4054 13.8477 14.2498 12.8449 14.8832C11.8421 15.5165 10.7272 15.8332 9.50008 15.8332Z" fill="currentColor"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_2812_6238">
|
||||
<rect width="19" height="19" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
换一批
|
||||
</span>
|
||||
</div>
|
||||
<div class="announcement-detail__aside-list">
|
||||
<div
|
||||
v-for="(job, idx) in otherJobs"
|
||||
:key="'other-' + idx"
|
||||
class="announcement-detail__job-item"
|
||||
>
|
||||
<div class="announcement-detail__job-item-main">
|
||||
<span class="announcement-detail__job-item-title">{{ job.title }}</span>
|
||||
<div class="announcement-detail__job-item-tags">
|
||||
<span v-if="job.city" class="announcement-detail__chip">{{ job.city }}</span>
|
||||
<span v-if="job.type" class="announcement-detail__chip">{{ job.type }}</span>
|
||||
<span v-if="job.education" class="announcement-detail__chip">{{ job.education }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="announcement-detail__job-item-score" :style="{ color: job.scoreColor }">
|
||||
<span class="announcement-detail__job-item-score-num">{{ job.score }}%</span>
|
||||
<span class="announcement-detail__job-item-score-label" :style="{ color: job.scoreColor }">{{ job.scoreLabel }}</span>
|
||||
</div>
|
||||
<div v-if="idx < otherJobs.length - 1" class="announcement-detail__job-divider"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 校招推荐 -->
|
||||
<div class="announcement-detail__aside-card">
|
||||
<div class="announcement-detail__aside-header">
|
||||
<span class="announcement-detail__aside-title">校招推荐</span>
|
||||
<span class="announcement-detail__aside-more" @click="refreshRecommend">
|
||||
<svg width="19" height="19" viewBox="0 0 19 19" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_2812_6238)">
|
||||
<path d="M9.50008 15.8332C7.73203 15.8332 6.23446 15.2196 5.00737 13.9925C3.78029 12.7655 3.16675 11.2679 3.16675 9.49984C3.16675 7.73178 3.78029 6.23421 5.00737 5.00713C6.23446 3.78005 7.73203 3.16651 9.50008 3.16651C10.4105 3.16651 11.2813 3.3544 12.1126 3.73017C12.9438 4.10595 13.6563 4.64376 14.2501 5.34359V3.95817C14.2501 3.73387 14.3261 3.54598 14.4781 3.39451C14.6301 3.24303 14.818 3.16703 15.0417 3.16651C15.2655 3.16598 15.4537 3.24198 15.6062 3.39451C15.7587 3.54703 15.8345 3.73492 15.8334 3.95817V7.91651C15.8334 8.14081 15.7574 8.32897 15.6054 8.48097C15.4534 8.63297 15.2655 8.7087 15.0417 8.70817H11.0834C10.8591 8.70817 10.6712 8.63217 10.5197 8.48017C10.3683 8.32817 10.2923 8.14028 10.2917 7.91651C10.2912 7.69273 10.3672 7.50484 10.5197 7.35284C10.6723 7.20084 10.8602 7.12484 11.0834 7.12484H13.6167C13.1945 6.38595 12.6174 5.8054 11.8854 5.38317C11.1533 4.96095 10.3582 4.74984 9.50008 4.74984C8.18064 4.74984 7.05911 5.21165 6.1355 6.13526C5.21189 7.05887 4.75008 8.1804 4.75008 9.49984C4.75008 10.8193 5.21189 11.9408 6.1355 12.8644C7.05911 13.788 8.18064 14.2498 9.50008 14.2498C10.3973 14.2498 11.2188 14.0224 11.9645 13.5674C12.7103 13.1125 13.2874 12.5021 13.6959 11.7363C13.8015 11.5516 13.95 11.4231 14.1416 11.3508C14.3332 11.2785 14.5277 11.275 14.7251 11.3405C14.9362 11.4064 15.0879 11.545 15.1803 11.7561C15.2727 11.9672 15.2661 12.1651 15.1605 12.3498C14.6195 13.4054 13.8477 14.2498 12.8449 14.8832C11.8421 15.5165 10.7272 15.8332 9.50008 15.8332Z" fill="currentColor"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_2812_6238">
|
||||
<rect width="19" height="19" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
换一批
|
||||
</span>
|
||||
</div>
|
||||
<div class="announcement-detail__aside-list">
|
||||
<div
|
||||
v-for="(rec, idx) in recommendList"
|
||||
:key="'rec-' + idx"
|
||||
class="announcement-detail__rec-item"
|
||||
>
|
||||
<img
|
||||
v-if="rec.logoUrl"
|
||||
:src="rec.logoUrl"
|
||||
class="announcement-detail__rec-logo"
|
||||
:alt="rec.companyName"
|
||||
/>
|
||||
<div v-else class="announcement-detail__rec-logo announcement-detail__rec-logo--default">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none"><rect width="24" height="24" rx="12" fill="#EDF9FA"/><path d="M6 17H18" stroke="#52CAD1" stroke-width="1.5" stroke-linecap="round"/></svg>
|
||||
</div>
|
||||
<div class="announcement-detail__rec-info">
|
||||
<span class="announcement-detail__rec-company">{{ rec.companyName }}</span>
|
||||
<span class="announcement-detail__rec-positions">{{ rec.positions }}</span>
|
||||
<div class="announcement-detail__rec-tags">
|
||||
<span v-if="rec.industry" class="announcement-detail__chip">{{ rec.industry }}</span>
|
||||
<span v-if="rec.cities" class="announcement-detail__chip">{{ rec.cities }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="idx < recommendList.length - 1" class="announcement-detail__job-divider"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-skeleton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import SideNav from '@/components/SideNav.vue'
|
||||
import { fetchAnnouncementDetail } from '@/api/announcement'
|
||||
import type { AnnouncementDetail } from '@/api/announcement'
|
||||
import { fetchJobList } from '@/api/jobs'
|
||||
import type { JobListItem } from '@/api/jobs'
|
||||
|
||||
// ==================== 路由相关 ====================
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
/** 当前公告ID(大数字符串,避免精度丢失) */
|
||||
const announcementId = route.params.id as string
|
||||
|
||||
// ==================== 页面状态 ====================
|
||||
|
||||
/** 是否正在加载 */
|
||||
const loading = ref(false)
|
||||
|
||||
/** 公告详情数据 */
|
||||
const detail = reactive<AnnouncementDetail>({
|
||||
id: 0,
|
||||
companyId: 0,
|
||||
companyName: '',
|
||||
logoUrl: '',
|
||||
companyType: '',
|
||||
title: '',
|
||||
companyIntro: '',
|
||||
targetAudience: '',
|
||||
majorRequire: '',
|
||||
recruitPosition: '',
|
||||
remark: '',
|
||||
writtenExam: '',
|
||||
applyStartTime: null,
|
||||
applyEndTime: null,
|
||||
applyEndDesc: '',
|
||||
inviteCode: '',
|
||||
source: '',
|
||||
publishTime: null,
|
||||
cleanStatus: 0,
|
||||
status: 0,
|
||||
createTime: null,
|
||||
updateTime: null,
|
||||
industryNames: [],
|
||||
categoryNames: [],
|
||||
cityNames: [],
|
||||
educationNames: [],
|
||||
recruitYears: [],
|
||||
batchNames: [],
|
||||
tagNames: [],
|
||||
})
|
||||
|
||||
// ==================== 计算属性 ====================
|
||||
|
||||
/** 公司规模文本 */
|
||||
const companyScaleText = computed(() => '')
|
||||
|
||||
/** 发布日期格式化文本 */
|
||||
const publishDateText = computed(() => {
|
||||
if (!detail.publishTime || !detail.publishTime.seconds) return ''
|
||||
const d = new Date(detail.publishTime.seconds * 1000)
|
||||
const y = d.getFullYear()
|
||||
const m = String(d.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(d.getDate()).padStart(2, '0')
|
||||
return `${y}-${m}-${day}`
|
||||
})
|
||||
|
||||
/** 投递截止时间格式化文本 */
|
||||
const applyEndText = computed(() => {
|
||||
if (detail.applyEndDesc) return detail.applyEndDesc
|
||||
if (!detail.applyEndTime || !detail.applyEndTime.seconds) return '招满即止'
|
||||
const d = new Date(detail.applyEndTime.seconds * 1000)
|
||||
const y = d.getFullYear()
|
||||
const m = String(d.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(d.getDate()).padStart(2, '0')
|
||||
return `${y}-${m}-${day}`
|
||||
})
|
||||
|
||||
// ==================== 右侧企业其他岗位 ====================
|
||||
|
||||
/** 学历枚举映射 */
|
||||
const educationMap: Record<number, string> = {
|
||||
0: '不限',
|
||||
1: '大专',
|
||||
2: '本科',
|
||||
3: '硕士',
|
||||
4: '博士',
|
||||
}
|
||||
|
||||
/** 招聘分类映射 */
|
||||
const recruitCategoryMap: Record<number, string> = {
|
||||
0: '社招',
|
||||
1: '校招',
|
||||
2: '实习',
|
||||
3: '其他',
|
||||
}
|
||||
|
||||
interface OtherJobItem {
|
||||
title: string
|
||||
city: string
|
||||
type: string
|
||||
education: string
|
||||
score: number
|
||||
scoreColor: string
|
||||
scoreLabel: string
|
||||
}
|
||||
|
||||
/** 企业全部岗位列表(缓存,避免重复请求) */
|
||||
const companyAllJobs = ref<JobListItem[]>([])
|
||||
|
||||
/** 当前展示的企业其他岗位 */
|
||||
const otherJobs = ref<OtherJobItem[]>([])
|
||||
|
||||
/**
|
||||
* 根据匹配分数获取颜色和标签
|
||||
*/
|
||||
function getScoreStyle(score: number) {
|
||||
if (score >= 80) return { color: '#9FD051', label: '高匹配度' }
|
||||
if (score >= 40) return { color: '#F59E0B', label: '中匹配度' }
|
||||
return { color: '#FE6D52', label: '低匹配度' }
|
||||
}
|
||||
|
||||
/**
|
||||
* 从全部岗位中随机选取最多4个并转换为展示格式
|
||||
*/
|
||||
function pickRandomJobs(list: JobListItem[]): OtherJobItem[] {
|
||||
if (!list.length) return []
|
||||
// 随机打乱后取前4个
|
||||
const shuffled = [...list].sort(() => Math.random() - 0.5)
|
||||
const picked = shuffled.slice(0, 4)
|
||||
return picked.map(job => {
|
||||
const style = getScoreStyle(job.matchScore ?? 0)
|
||||
return {
|
||||
title: job.title,
|
||||
city: job.regionName || '',
|
||||
type: recruitCategoryMap[job.recruitCategory ?? 3] || '',
|
||||
education: educationMap[job.education ?? 0] || '',
|
||||
score: job.matchScore ?? 0,
|
||||
scoreColor: style.color,
|
||||
scoreLabel: style.label,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载企业其他岗位(用公司名作为关键词查询)
|
||||
*/
|
||||
async function loadCompanyJobs() {
|
||||
if (!detail.companyName) return
|
||||
try {
|
||||
const res = await fetchJobList({
|
||||
keyword: detail.companyName,
|
||||
pageNum: 1,
|
||||
pageSize: 100,
|
||||
})
|
||||
if (res.code === '0' && res.data?.list) {
|
||||
companyAllJobs.value = res.data.list
|
||||
otherJobs.value = pickRandomJobs(companyAllJobs.value)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('加载企业其他岗位失败', e)
|
||||
}
|
||||
}
|
||||
|
||||
/** 换一批(企业其他岗位)—— 重新调接口随机展示4个 */
|
||||
async function refreshOtherJobs() {
|
||||
await loadCompanyJobs()
|
||||
}
|
||||
|
||||
// ==================== 右侧校招推荐(静态假数据) ====================
|
||||
|
||||
interface RecommendItem {
|
||||
logoUrl: string
|
||||
companyName: string
|
||||
positions: string
|
||||
industry: string
|
||||
cities: string
|
||||
}
|
||||
|
||||
const recommendList = ref<RecommendItem[]>([
|
||||
{ logoUrl: '', companyName: '泉峰科技', positions: '产品研发/工程与制造/质量与供应链/销售与市场...', industry: '机械/制造业', cities: '上海市, 南京市' },
|
||||
{ logoUrl: '', companyName: '海天集团', positions: '技术类 产品类 设计类岗位', industry: 'IT/互联网/游戏', cities: '北京市, 杭州市' },
|
||||
{ logoUrl: '', companyName: '陕西水务发展集团', positions: '水利水电工程施工设计岗 -A01, 高效节水灌溉设...', industry: '能源/化工/环保', cities: '宝鸡市, 咸阳市, 渭南市...' },
|
||||
{ logoUrl: '', companyName: '陕西水务发展集团', positions: '水利水电工程施工设计岗 -A01, 高效节水灌溉设...', industry: '能源/化工/环保', cities: '宝鸡市, 咸阳市, 渭南市...' },
|
||||
])
|
||||
|
||||
/** 换一批(校招推荐) */
|
||||
function refreshRecommend() {
|
||||
// 静态展示暂不做切换逻辑
|
||||
}
|
||||
|
||||
// ==================== 事件处理 ====================
|
||||
|
||||
/** 返回列表页 */
|
||||
function goBack() {
|
||||
if (window.history.length > 1) {
|
||||
router.back()
|
||||
} else {
|
||||
router.push('/announcement')
|
||||
}
|
||||
}
|
||||
|
||||
/** 查看公告 */
|
||||
function handleViewAnnouncement() {
|
||||
if (detail.source) {
|
||||
window.open(detail.source, '_blank')
|
||||
}
|
||||
}
|
||||
|
||||
/** 投递地址 */
|
||||
function handleApplyUrl() {
|
||||
if (detail.source) {
|
||||
window.open(detail.source, '_blank')
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 加载详情数据 ====================
|
||||
|
||||
async function loadDetail() {
|
||||
if (!announcementId) return
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await fetchAnnouncementDetail(announcementId)
|
||||
if (res.code === '0' && res.data) {
|
||||
Object.assign(detail, res.data)
|
||||
// 详情加载完毕后,加载企业其他岗位
|
||||
loadCompanyJobs()
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('加载校招公告详情失败', e)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadDetail()
|
||||
})
|
||||
</script>
|
||||
+2
-2
@@ -688,9 +688,9 @@ onMounted(() => {
|
||||
store.dispatch('loadCommonData')
|
||||
})
|
||||
|
||||
/** 点击登录按钮 — 跳转到登录页面,登录成功后跳转到 jobs 页面 */
|
||||
/** 点击登录按钮 — 打开登录弹窗,登录成功后跳转到 jobs 页面 */
|
||||
function handleLoginClick() {
|
||||
router.push({ name: 'Login', query: { redirect: '/jobs' } })
|
||||
store.dispatch('openLogin', '/jobs')
|
||||
}
|
||||
|
||||
/** 隐私协议弹窗显示状态 */
|
||||
|
||||
+154
-58
@@ -12,9 +12,11 @@
|
||||
|
||||
<!-- 筛选条件面板 -->
|
||||
<div v-if="isRecommendTab" class="jobs-page__filters-bar">
|
||||
<div class="jobs-page__filters-panel">
|
||||
<!-- 筛选面板 + 统计卡片横向排列 -->
|
||||
<div class="jobs-page__filters-row">
|
||||
<div ref="filtersPanelRef" class="jobs-page__filters-panel">
|
||||
<!-- 行业热门分类行 -->
|
||||
<div class="jobs-page__filter-row">
|
||||
<div class="jobs-page__filter-row" v-if="hotIndustries.length > 0">
|
||||
<div class="jobs-page__filter-row-inner">
|
||||
<span class="jobs-page__filter-row-label">行业</span>
|
||||
<div class="jobs-page__filter-chips-wrap">
|
||||
@@ -60,7 +62,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<!-- 岗位热门分类行 -->
|
||||
<div class="jobs-page__filter-row">
|
||||
<div class="jobs-page__filter-row" v-if="hotCategories.length > 0">
|
||||
<div class="jobs-page__filter-row-inner">
|
||||
<span class="jobs-page__filter-row-label">岗位</span>
|
||||
<div class="jobs-page__filter-chips-wrap">
|
||||
@@ -106,7 +108,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<!-- 地点热门分类行 -->
|
||||
<div class="jobs-page__filter-row">
|
||||
<div class="jobs-page__filter-row" v-if="hotCities.length > 0">
|
||||
<div class="jobs-page__filter-row-inner">
|
||||
<span class="jobs-page__filter-row-label">地点</span>
|
||||
<div class="jobs-page__filter-chips-wrap">
|
||||
@@ -181,8 +183,8 @@
|
||||
class="jobs-page__filter-dropdown-item"
|
||||
v-for="option in educationOptions"
|
||||
:key="option.value"
|
||||
:class="{ 'jobs-page__filter-dropdown-item--active': selectedEducations.includes(option.value) }"
|
||||
@click.stop="toggleEducation(option.value)"
|
||||
:class="{ 'jobs-page__filter-dropdown-item--active': selectedEducation === option.value }"
|
||||
@click.stop="selectEducation(option)"
|
||||
>{{ option.label }}</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -220,36 +222,60 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="dflex aliite-c">
|
||||
<!-- 筛选数量 + 深度匹配提示条 -->
|
||||
<div v-if="showFilterCountBar" class="jobs-page__filter-count-bar">
|
||||
<span class="jobs-page__filter-count-text">
|
||||
当前筛选共:<span class="jobs-page__filter-count-num">{{ filterJobCount }}</span> 个岗位,试试开启深度匹配,配寻找更适合你的岗位吧~
|
||||
</span>
|
||||
<button class="jobs-page__deep-match-btn" :disabled="deepMatchLoading" @click="handleDeepMatch">
|
||||
{{ deepMatchLoading ? '匹配中...' : '深度匹配' }}
|
||||
</button>
|
||||
</div>
|
||||
<!-- 清空筛选条件 -->
|
||||
<span v-if="hasActiveFilters" class="jobs-page__clear-filters" @click="clearAllFilters">清空筛选条件</span>
|
||||
</div>
|
||||
|
||||
<!-- 筛选数量 + 深度匹配提示条 -->
|
||||
<div v-if="showFilterCountBar" class="jobs-page__filter-count-bar">
|
||||
<span class="jobs-page__filter-count-text">
|
||||
当前筛选共:<span class="jobs-page__filter-count-num">{{ filterJobCount }}</span> 个岗位,试试开启深度匹配,配寻找更适合你的岗位吧~
|
||||
</span>
|
||||
<button class="jobs-page__deep-match-btn" :disabled="deepMatchLoading" @click="handleDeepMatch">
|
||||
{{ deepMatchLoading ? '匹配中...' : '深度匹配' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
<!-- 岗位概览统计卡片(固定宽度,高度跟随筛选面板) -->
|
||||
<JobStatsOverview
|
||||
title="岗位概览"
|
||||
:subtitle="'数据截至 ' + statsCurrentDate"
|
||||
:todayCount="statsDailyJobCount"
|
||||
todayLabel="今日新增"
|
||||
:totalCount="statsFormattedJobCount"
|
||||
totalLabel="累计收录"
|
||||
rateValue="92%"
|
||||
rateLabel="可投率"
|
||||
:cardHeight="filtersPanelHeight"
|
||||
/>
|
||||
</div>
|
||||
<!-- 搜索框(独立在筛选面板下方) -->
|
||||
<div class="jobs-page__search-box">
|
||||
<svg class="jobs-page__search-svg" viewBox="0 0 16 16" fill="none" @click="reloadFirstPage">
|
||||
<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="jobs-page__search-input"
|
||||
placeholder="搜索职位、公司..."
|
||||
@keyup.enter="reloadFirstPage"
|
||||
/>
|
||||
<div class="mt12 dflex aliite-c flex-warp">
|
||||
<div class="jobs-page__search-box fgrow1">
|
||||
<svg class="jobs-page__search-svg" viewBox="0 0 16 16" fill="none" @click="reloadFirstPage">
|
||||
<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="jobs-page__search-input"
|
||||
placeholder="搜索职位、公司..."
|
||||
@keyup.enter="reloadFirstPage"
|
||||
/>
|
||||
</div>
|
||||
<div class="dflex aliite-c flex-warp ml24">
|
||||
<!-- 热门搜索标签 -->
|
||||
<span class="jobs-page__hot-search-label">热门搜索:</span>
|
||||
<span
|
||||
v-for="(tag, idx) in hotSearchTags"
|
||||
:key="idx"
|
||||
class="jobs-page__hot-search-tag"
|
||||
:class="{ 'jobs-page__hot-search-tag--active': keyword === tag }"
|
||||
@click="handleHotSearch(tag)"
|
||||
>{{ tag }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -331,6 +357,12 @@
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<!-- 未登录时底部登录提示 -->
|
||||
<div v-if="!isAuthenticated && !loading && jobList.length > 0" class="jobs-page__login-prompt">
|
||||
<p class="jobs-page__login-prompt-title">登录后查看全部岗位</p>
|
||||
<p class="jobs-page__login-prompt-desc">登录即可解锁全部岗位及完整匹配信息</p>
|
||||
<button class="jobs-page__login-prompt-btn" @click="store.dispatch('openLogin')">立即登录</button>
|
||||
</div>
|
||||
<!-- 首次加载中提示 -->
|
||||
<div v-if="loading" class="jobs-page__loading-box">
|
||||
<div class="jobs-page__loading-spinner"></div>
|
||||
@@ -415,6 +447,7 @@ import { JOB_TYPE_OPTIONS, formatEmploymentType, formatEducation } from '@/store
|
||||
import SideNav from '@/components/SideNav.vue'
|
||||
import AiChat from '@/components/AiChat.vue'
|
||||
import JobPageHeader from '@/components/JobPageHeader.vue'
|
||||
import JobStatsOverview from '@/components/JobStatsOverview.vue'
|
||||
import JobDislikeDialog from '@/components/JobDislikeDialog.vue'
|
||||
import JobFeedbackDialog from '@/components/JobFeedbackDialog.vue'
|
||||
import MemberDialog from '@/components/MemberDialog.vue'
|
||||
@@ -424,9 +457,10 @@ import JobCategorySelector from '@/components/tools/JobCategorySelector.vue'
|
||||
import RegionSelector from '@/components/tools/RegionSelector.vue'
|
||||
|
||||
|
||||
import { fetchJobList, fetchJobListCount, fetchJobListMatch, fetchFavoriteList, toggleJobFavorite, removeJobFavorite, fetchFavoriteCount, fetchApplyList, fetchApplyCount, removeJobFromList, fetchCustomizeResume, fetchHotIndustries, fetchHotCategories, fetchHotCities } from '@/api/jobs'
|
||||
import { fetchJobList, fetchJobListCount, fetchJobListMatch, fetchFavoriteList, toggleJobFavorite, removeJobFavorite, fetchFavoriteCount, fetchApplyList, fetchApplyCount, removeJobFromList, fetchCustomizeResume, fetchHotIndustries, fetchHotCategories, fetchHotCities, fetchJobCount } from '@/api/jobs'
|
||||
import type { JobListItem, JobListParams, FavoriteListParams, ApplyListParams, ApplyCountData, HotIndustryItem, HotCategoryItem, HotCityItem } from '@/api/jobs'
|
||||
import { getTimeEvent, setTimeEvent, timestampToLocalDateTime } from '@/utils/time'
|
||||
import { getDailyJobCount } from '@/utils/dailyJobCount'
|
||||
import { fetchIndustryTree, fetchJobCategoryTree } from '@/api/common'
|
||||
import type { IndustryItem, JobCategoryItem } from '@/api/common'
|
||||
import { ElMessage } from 'element-plus'
|
||||
@@ -479,9 +513,63 @@ const activeTab = ref((route.query.tab as string) || 'recommend')
|
||||
/** 是否为推荐列表 Tab */
|
||||
const isRecommendTab = computed(() => activeTab.value === 'recommend')
|
||||
|
||||
// ==================== 岗位概览统计数据 ====================
|
||||
|
||||
/** 筛选面板 DOM 引用 */
|
||||
const filtersPanelRef = ref<HTMLElement | null>(null)
|
||||
|
||||
/** 筛选面板高度(同步给 JobStatsOverview 卡片) */
|
||||
const filtersPanelHeight = ref<string>('auto')
|
||||
|
||||
/** 监听筛选面板高度变化的 ResizeObserver */
|
||||
let filtersPanelObserver: ResizeObserver | null = null
|
||||
|
||||
/** 今日新增岗位数量 */
|
||||
const statsDailyJobCount = getDailyJobCount()
|
||||
|
||||
/** 全网岗位总数 */
|
||||
const statsJobCount = ref(0)
|
||||
|
||||
/** 格式化岗位总数(万为单位) */
|
||||
const statsFormattedJobCount = computed(() => {
|
||||
if (statsJobCount.value >= 10000) {
|
||||
return Math.floor(statsJobCount.value / 10000) + 'w'
|
||||
}
|
||||
return statsJobCount.value.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',')
|
||||
})
|
||||
|
||||
/** 当前日期(MM.DD 格式) */
|
||||
const statsCurrentDate = computed(() => {
|
||||
const now = new Date()
|
||||
const month = String(now.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(now.getDate()).padStart(2, '0')
|
||||
return `${month}.${day}`
|
||||
})
|
||||
|
||||
/** 加载岗位总数 */
|
||||
async function loadStatsJobCount() {
|
||||
try {
|
||||
const res = await fetchJobCount()
|
||||
if (res.code === '0' && res.data !== undefined) {
|
||||
statsJobCount.value = Number(res.data)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('加载岗位总数失败', e)
|
||||
}
|
||||
}
|
||||
|
||||
/** 搜索框输入内容 */
|
||||
const keyword = ref('')
|
||||
|
||||
/** 热门搜索标签列表 */
|
||||
const hotSearchTags = ref(['系统集成', 'AI产品经理', '广告投放', 'AI开发', '大模型'])
|
||||
|
||||
/** 点击热门搜索标签 — 设置关键词并触发搜索 */
|
||||
function handleHotSearch(tag: string) {
|
||||
keyword.value = tag
|
||||
reloadFirstPage()
|
||||
}
|
||||
|
||||
/** 当前选中的职位卡片索引 */
|
||||
const selectedIndex = ref(0)
|
||||
|
||||
@@ -730,30 +818,19 @@ const educationOptions = [
|
||||
/** 学历下拉菜单显示状态 */
|
||||
const showEducationDropdown = ref(false)
|
||||
|
||||
/** 已选学历列表 */
|
||||
const selectedEducations = ref<number[]>([])
|
||||
/** 已选学历(单选,0表示不限/未选) */
|
||||
const selectedEducation = ref<number>(0)
|
||||
|
||||
/** 学历筛选显示文字 */
|
||||
const educationFilterLabel = computed(() => {
|
||||
if (!selectedEducations.value.length) return '学历要求'
|
||||
return selectedEducations.value.map(v => educationOptions.find(o => o.value === v)?.label || '').join('、')
|
||||
if (!selectedEducation.value) return '学历要求'
|
||||
return educationOptions.find(o => o.value === selectedEducation.value)?.label || '学历要求'
|
||||
})
|
||||
|
||||
/** 切换学历选中 */
|
||||
function toggleEducation(value: number) {
|
||||
if (value === 0) {
|
||||
// 选"不限"清空所有
|
||||
selectedEducations.value = []
|
||||
showEducationDropdown.value = false
|
||||
reloadFirstPage()
|
||||
return
|
||||
}
|
||||
const idx = selectedEducations.value.indexOf(value)
|
||||
if (idx >= 0) {
|
||||
selectedEducations.value.splice(idx, 1)
|
||||
} else {
|
||||
selectedEducations.value.push(value)
|
||||
}
|
||||
/** 选择学历(单选) */
|
||||
function selectEducation(option: { label: string; value: number }) {
|
||||
selectedEducation.value = option.value === selectedEducation.value ? 0 : option.value
|
||||
showEducationDropdown.value = false
|
||||
reloadFirstPage()
|
||||
}
|
||||
|
||||
@@ -1241,6 +1318,22 @@ const showMemberDialog = ref(false)
|
||||
onMounted(async () => {
|
||||
document.addEventListener('click', closeDropdownOnClickOutside)
|
||||
|
||||
// 加载岗位总数
|
||||
loadStatsJobCount()
|
||||
|
||||
// 监听筛选面板高度变化,同步给 JobStatsOverview
|
||||
nextTick(() => {
|
||||
if (filtersPanelRef.value) {
|
||||
filtersPanelHeight.value = filtersPanelRef.value.offsetHeight + 'px'
|
||||
filtersPanelObserver = new ResizeObserver((entries) => {
|
||||
for (const entry of entries) {
|
||||
filtersPanelHeight.value = entry.target.clientHeight + 'px'
|
||||
}
|
||||
})
|
||||
filtersPanelObserver.observe(filtersPanelRef.value)
|
||||
}
|
||||
})
|
||||
|
||||
// 从缓存恢复公司类型筛选按钮的显示文字
|
||||
// 需要等 userInfo 加载后再读取(刷新时 userInfo 可能还没有)
|
||||
const restoreCompanyTypeFilter = () => {
|
||||
@@ -1346,6 +1439,11 @@ onMounted(async () => {
|
||||
onBeforeUnmount(() => {
|
||||
document.removeEventListener('click', closeDropdownOnClickOutside)
|
||||
window.removeEventListener('resize', updateAiTipArrowPosition)
|
||||
// 清理筛选面板高度监听
|
||||
if (filtersPanelObserver) {
|
||||
filtersPanelObserver.disconnect()
|
||||
filtersPanelObserver = null
|
||||
}
|
||||
// 清理网格列数监听
|
||||
if (gridResizeObserver) {
|
||||
gridResizeObserver.disconnect()
|
||||
@@ -1496,8 +1594,8 @@ function buildParams(): JobListParams {
|
||||
params.companyTypes = selectedCompanyTypes.value
|
||||
}
|
||||
// 学历要求筛选
|
||||
if (selectedEducations.value.length) {
|
||||
params.educations = selectedEducations.value
|
||||
if (selectedEducation.value) {
|
||||
params.educations = [selectedEducation.value]
|
||||
}
|
||||
// 发布时间筛选(转为ISO 8601字符串)
|
||||
if (publishTimeRange.value) {
|
||||
@@ -1566,6 +1664,8 @@ async function loadNextPage() {
|
||||
function onListScroll() {
|
||||
const el = jobListRef.value
|
||||
if (!el) return
|
||||
// 未登录时只展示一页,不加载更多
|
||||
if (!isAuthenticated.value) return
|
||||
// 距离底部小于 100px 时触发加载
|
||||
const threshold = 100
|
||||
if (el.scrollHeight - el.scrollTop - el.clientHeight < threshold) {
|
||||
@@ -1597,7 +1697,7 @@ const hasActiveFilters = computed(() => {
|
||||
|| selectedRegionCodes.value.length > 0
|
||||
|| selectedEmploymentType.value !== null
|
||||
|| selectedCompanyTypes.value.length > 0
|
||||
|| selectedEducations.value.length > 0
|
||||
|| selectedEducation.value > 0
|
||||
|| publishTimeRange.value !== null
|
||||
|| keyword.value.trim() !== ''
|
||||
})
|
||||
@@ -1665,7 +1765,7 @@ function clearAllFilters() {
|
||||
selectedCompanyTypes.value = []
|
||||
saveCachedCompanyTypes([])
|
||||
// 清空学历和发布时间
|
||||
selectedEducations.value = []
|
||||
selectedEducation.value = 0
|
||||
publishTimeRange.value = null
|
||||
// 清空筛选按钮的显示文字
|
||||
filters.value.forEach(f => { f.selected = '' })
|
||||
@@ -1952,10 +2052,6 @@ function matchLevelText(score: number) {
|
||||
|
||||
/** 跳转到岗位详情页 — 未登录弹登录框,登录后再跳转 */
|
||||
function goToDetail(job: JobItem) {
|
||||
if (!isAuthenticated.value) {
|
||||
store.dispatch('openLogin', `/jobs/${job.id}`)
|
||||
return
|
||||
}
|
||||
// 保存当前列表状态到 store,返回时恢复
|
||||
store.commit('SET_JOB_LIST_CACHE', {
|
||||
tab: activeTab.value,
|
||||
|
||||
Reference in New Issue
Block a user