导师相关接口换offer派接口,职位列表和职位详情,Nova聊天侧边栏UI重写

This commit is contained in:
2026-07-03 19:55:53 +08:00
parent a1a27073e4
commit f90c8be2a9
26 changed files with 896 additions and 1129 deletions
+2
View File
@@ -49,6 +49,8 @@ export interface JobListItem {
matchDetail: MatchDetail
/** 招聘分类:0=社招 1=校招 2=实习 */
recruitCategory?: number
/** 学历要求 0=不限 1=大专 2=本科 3=硕士 4=博士 */
education?: number
}
// ==================== 分页结构 ====================
+7 -7
View File
@@ -1,4 +1,4 @@
import jianshiRequest from '@/utils/jianshiRequest'
import request from '@/utils/request'
/**
* 分页查询导师列表请求参数
@@ -81,10 +81,10 @@ export interface MentorPageResult {
/**
* 分页查询导师列表
* POST /public/website/pageSelectMentor
* POST /public/mentor/pageofferpai 后端)
*/
export function fetchMentorList(params: MentorListParams): Promise<MentorPageResult> {
return jianshiRequest.post('/public/website/pageSelectMentor', params)
return request.post('/public/mentor/page', params)
}
// ==================== 导师详情接口 ====================
@@ -189,16 +189,16 @@ export interface MentorTalk {
/**
* 获取导师详情信息
* GET /public/website/selectMentorFullInfo?mentorId=xxx
* GET /public/mentor/info?mentorId=xxxofferpai 后端)
*/
export function fetchMentorFullInfo(mentorId: string): Promise<MentorFullInfo> {
return jianshiRequest.get('/public/website/selectMentorFullInfo', { params: { mentorId } })
return request.get('/public/mentor/info', { params: { mentorId } })
}
/**
* 获取导师话题列表
* GET /public/website/selectTalk?teacherId=xxx
* GET /public/mentor/talk?teacherId=xxxofferpai 后端)
*/
export function fetchMentorTalks(teacherId: string): Promise<MentorTalk[]> {
return jianshiRequest.get('/public/website/selectTalk', { params: { teacherId } })
return request.get('/public/mentor/talk', { params: { teacherId } })
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

+57
View File
@@ -3,10 +3,67 @@ html{
font-size: 100px;
}
// 全局滚动条美化(必须在 body 包裹外,才能作用到 body/html 自身的滚动条)
::-webkit-scrollbar {
width: 4px;
height: 4px;
}
::-webkit-scrollbar-thumb {
background: #D4D4D4;
border-radius: 4px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-button {
display: none;
width: 0;
height: 0;
}
::-webkit-scrollbar-corner {
background: transparent;
}
html, body {
scrollbar-width: thin;
scrollbar-color: #D4D4D4 transparent;
}
// body 滚动条在缩放模式下会被 transform scale 放大变丑,强制隐藏
body {
&::-webkit-scrollbar {
width: 0 !important;
display: none !important;
}
-ms-overflow-style: none;
scrollbar-width: none;
}
// 用 body:not(#_) 包裹,提升选择器权重到 id 级别,确保工具类优先级最高
// :not(#_) 永远为 true,不影响匹配,只提升权重
body:not(#_) {
// 统一细滚动条工具类(4px 宽、圆角、无按钮)
.scrollbar-thin {
&::-webkit-scrollbar {
width: 4px;
height: 4px;
}
&::-webkit-scrollbar-thumb {
background: #D4D4D4;
border-radius: 4px;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-button {
display: none;
width: 0;
height: 0;
}
scrollbar-width: thin;
scrollbar-color: #D4D4D4 transparent;
}
//组件定制样式
.box-shadow-head{
box-shadow: 0px 6px 16px rgba(0, 0, 0, 0.08), 0px 3px 6px -4px rgba(0, 0, 0, 0.12), 0px -3px 6px -4px rgba(0, 0, 0, 0.12);
+216 -120
View File
@@ -1,84 +1,119 @@
// 颜色变量已移至全局 variables.scss
@use '../variables' as *;
.ai-chat {
width: var(--chat-width, 4.0rem);
max-width: 4.0rem;
min-width: 1.5rem;
font-size: 0.14rem;
width: var(--chat-width, 3.6rem);
height: var(--app-height, 100vh);
background: #f3f4f6;
display: flex;
flex-direction: column;
box-sizing: border-box;
background: $bg-white;
position: fixed;
right: 0;
top: 0;
border-left: 1px solid #e5e7eb;
&__banner {
display: flex;
align-items: center;
justify-content: space-between;
background: #0F172B;
color: #fff;
padding: 0.12rem 0.14rem;
font-size: 0.14rem;
font-weight: 600;
cursor: pointer;
border-radius: 0.2rem;
margin: 0.15rem;
height: 0.41rem;
}
&__banner-arrow {
font-size: 0.16rem;
padding-bottom: 0.05rem;
flex-direction: column;
box-sizing: border-box;
}
// ==================== 顶部头部区域 ====================
&__header {
display: flex;
align-items: center;
gap: 0.1rem;
justify-content: space-between;
padding: 0.16rem;
border-bottom: 1px solid #e5e7eb;
height: 0.80rem;
box-sizing: border-box;
}
&__header-left {
display: flex;
align-items: center;
gap: 0.12rem;
}
&__avatar {
width: 0.4rem;
height: 0.4rem;
width: 0.44rem;
height: 0.44rem;
border-radius: 50%;
background: #e5e7eb;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.2rem;
object-fit: cover;
}
&__info {
flex: 1;
display: flex;
flex-direction: column;
}
&__name {
font-size: 0.16rem;
font-weight: 600;
color: #1a1a2e;
font-size: 0.15rem;
font-weight: 520;
line-height: 0.22rem;
color: $text-dark;
}
&__desc {
font-size: 0.12rem;
color: #6b7280;
margin-top: 0.02rem;
font-weight: 330;
line-height: 0.18rem;
color: #99A1AF;
}
// 在线状态标签
&__online-tag {
display: flex;
align-items: center;
justify-content: center;
padding: 0.04rem 0.08rem;
gap: 0.04rem;
background: #52CAD1;
border-radius: 0.12rem;
font-size: 0.12rem;
font-weight: 330;
line-height: 0.16rem;
color: $bg-white;
}
&__online-dot {
width: 0.06rem;
height: 0.06rem;
background: $bg-white;
border-radius: 50%;
}
// ==================== 聊天消息区域 ====================
&__messages {
flex: 1;
overflow-y: auto;
padding: 0.16rem;
padding: 0 0.16rem;
padding-bottom: 0.16rem;
}
// 欢迎气泡
&__welcome-bubble {
display: flex;
flex-direction: column;
align-items: flex-start;
padding: 0.16rem;
gap: 0.08rem;
background: #F4FBFB;
border: 1px solid #AEE6EA;
border-radius: 0.12rem;
margin-bottom: 0.17rem;
}
&__welcome-title {
font-size: 0.14rem;
font-weight: 520;
line-height: 0.16rem;
color: $text-dark;
}
&__welcome-text {
font-size: 0.12rem;
font-weight: 330;
line-height: 0.18rem;
color: #99A1AF;
}
// ==================== 消息气泡 ====================
&__msg {
margin-bottom: 0.12rem;
margin-bottom: 0.17rem;
display: flex;
&--ai {
@@ -91,13 +126,14 @@
}
&__msg-bubble {
border-radius: 0.1rem;
padding: 0.12rem 0.14rem;
font-size: 0.13rem;
line-height: 1.6;
max-width: 85%;
max-width: 2.8rem;
padding: 0.08rem 0.12rem;
font-size: 0.14rem;
font-weight: 330;
line-height: 0.24rem;
box-shadow: 0px 1px 4px rgba(0, 0, 0, 0.04);
// ===== Markdown 渲染样式 =====
// Markdown 渲染样式
p {
margin: 0 0 0.08rem;
&:last-child { margin-bottom: 0; }
@@ -174,123 +210,183 @@
background: #f8fafc;
font-weight: 600;
}
tr:nth-child(even) {
background: #fafbfc;
}
}
hr {
border: none;
border-top: 1px solid #e5e7eb;
margin: 0.1rem 0;
}
}
&__msg--ai &__msg-bubble {
background: #fff;
border: 1px solid #e5e7eb;
color: #374151;
}
// 用户消息气泡 — 品牌色背景,右上圆角小
&__msg--user &__msg-bubble {
background: #fff;
color: #1a1a2e;
background: #52CAD1;
color: $bg-white;
border-radius: 0.12rem 0.04rem 0.12rem 0.12rem;
}
&__msg-title {
font-weight: 600;
font-size: 0.14rem;
margin-bottom: 0.06rem;
color: #1a1a2e;
// AI 消息气泡 — 白色背景绿色边框,左上圆角小
&__msg--ai &__msg-bubble {
background: $bg-white;
color: $text-dark;
border: 1px solid #AEE6EA;
box-shadow: 0px 2px 4px rgba(82, 202, 209, 0.1);
border-radius: 0.04rem 0.12rem 0.12rem 0.12rem;
}
&__msg-text {
color: #6b7280;
font-size: 0.12rem;
// ==================== 三点等待动效 ====================
&__typing-indicator {
display: flex;
align-items: center;
padding: 0.14rem 0.12rem;
gap: 0.06rem;
background: $bg-white;
border: 1px solid #AEE6EA;
box-shadow: 0px 1px 4px rgba(0, 0, 0, 0.04);
border-radius: 0.04rem 0.12rem 0.12rem 0.12rem;
}
&__quick-questions {
&__typing-dot {
width: 0.06rem;
height: 0.06rem;
border-radius: 50%;
background: #AEE6EA;
animation: typingBounce 1.4s infinite ease-in-out both;
&:nth-child(1) {
animation-delay: 0s;
}
&:nth-child(2) {
animation-delay: 0.2s;
background: rgba(174, 230, 234, 0.6);
}
&:nth-child(3) {
animation-delay: 0.4s;
background: rgba(174, 230, 234, 0.4);
}
}
@keyframes typingBounce {
0%, 80%, 100% {
transform: scale(1);
opacity: 0.6;
}
40% {
transform: scale(1.3);
opacity: 1;
}
}
// ==================== 话题推荐模块 ====================
&__topics {
display: flex;
flex-direction: column;
gap: 0.08rem;
margin-top: 0.12rem;
align-items: flex-start;
padding: 0.16rem 0.16rem 0.16rem 0.16rem;
gap: 0.12rem;
background: linear-gradient(180deg, #EDF9FA 0%, #FFFFFF 20.63%);
border: 1px solid #AEE6EA;
border-radius: 0.12rem;
margin: 0 0.16rem;
margin-bottom: 0.12rem;
}
&__quick-item {
background: #fff;
border: 1px solid #e5e7eb;
&__topics-header {
display: flex;
align-items: center;
gap: 0.04rem;
}
&__topics-icon {
width: 0.16rem;
height: 0.16rem;
}
&__topics-title {
font-size: 0.14rem;
font-weight: 520;
line-height: 0.19rem;
color: #52CAD1;
}
&__topic-btn {
display: flex;
align-items: center;
padding: 0.08rem 0.12rem;
background: $bg-white;
border: 1px solid #F0F0F0;
border-radius: 0.08rem;
padding: 0.1rem 0.14rem;
color: $text-middle;
font-size: 0.12rem;
font-weight: 330;
line-height: 0.16rem;
color: $text-middle;
cursor: pointer;
transition: background 0.2s;
max-width: 80%;
&:hover {
background: #f9fafb;
color: #374151;
background: $accent;
color: #fff;
}
/* AI 提问靠左 */
&--ai {
align-self: flex-start;
}
// 第一个按钮用品牌色
&--primary {
background: #52CAD1;
border-color: #52CAD1;
color: $bg-white;
/* 用户提问靠右 */
&--user {
align-self: flex-end;
background: #f0f3f6;
border-color: #e0e3e6;
&:hover {
background: #47b8bf;
}
}
}
// ==================== 底部输入框 ====================
&__input-area {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.08rem 0.12rem;
gap: 0.08rem;
padding: 0.12rem 0.16rem;
border-top: 1px solid #e5e7eb;
background: #fff;
margin: 0 0.16rem;
margin-bottom: 0.16rem;
background: rgba(255, 255, 255, 0.95);
border: 1px solid #AEE6EA;
border-radius: 0.08rem;
backdrop-filter: blur(12px);
box-sizing: border-box;
}
&__input {
flex: 1;
background: #f3f4f6;
border: 1px solid #e5e7eb;
border-radius: 0.06rem;
padding: 0.1rem 0.12rem;
color: #1a1a2e;
font-size: 0.12rem;
border: none;
outline: none;
background: transparent;
font-size: 0.12rem;
font-weight: 400;
line-height: 0.15rem;
color: $text-dark;
&::placeholder {
color: #9ca3af;
}
&:focus {
border-color: #9ca3af;
color: #99A1AF;
}
}
&__send-btn {
width: 0.36rem;
height: 0.36rem;
border-radius: 50%;
background: #1a1a2e;
border: none;
color: #fff;
font-size: 0.16rem;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
width: 0.32rem;
height: 0.32rem;
background: #52CAD1;
border-radius: 0.08rem;
border: none;
cursor: pointer;
flex-shrink: 0;
transition: background 0.2s;
&:hover {
background: #2e3142;
background: #47b8bf;
}
&:disabled {
opacity: 0.6;
cursor: not-allowed;
}
}
}
@@ -1,10 +1,10 @@
@use '../variables' as *;
.job-page-header {
margin-bottom: 0;
&__title {
font-size: 0.24rem;
font-size: 0.18rem;
font-weight: 700;
color: $text-dark;
margin: 0 !important;
@@ -15,8 +15,6 @@
&__subtitle {
font-size: 0.13rem;
color: $text-light;
margin: 0.02rem 0 0 0 !important;
padding: 0;
}
&__tabs {
@@ -24,7 +22,6 @@
justify-content: flex-start;
align-items: center;
gap: 0.04rem;
margin: 0.1rem 0 0.16rem 0;
background: $bg-white;
border-radius: 0.22rem;
padding: 0.04rem;
@@ -51,13 +48,16 @@
&__tab {
font-size: 0.13rem;
color: $text-light;
color: $text-middle;
cursor: pointer;
padding: 0.06rem 0.16rem;
border-radius: 0.18rem;
padding: 0.06rem 0.20rem;
border-radius: 0.08rem;
transition: all 0.25s ease;
user-select: none;
font-weight: 500;
display: flex;
align-items: center;
gap: 0.06rem;
&:hover {
color: $text-dark;
@@ -76,4 +76,20 @@
}
}
}
/* 计数圆形徽章 */
&__tab-badge {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 0.20rem;
height: 0.20rem;
padding: 0 0.05rem;
border-radius: 50%;
background: #F3F4F6;
font-size: 0.11rem;
font-weight: 500;
color: $text-middle;
line-height: 1;
}
}
@@ -224,15 +224,15 @@
&__portfolio-link {
font-size: 0.13rem;
line-height: 1.6;
color: $accent;
color: $text-middle;
word-break: break-all;
text-decoration: none;
text-decoration: underline;
transition: color 0.2s;
display: inline-block;
vertical-align: text-bottom;
&:hover {
color: $accent-hover;
text-decoration: underline;
}
}
+252 -509
View File
@@ -4,9 +4,8 @@
.job-detail {
&__content {
margin-left: 2rem;
margin-right: var(--chat-width, 4.0rem);
margin-right: var(--chat-width, 3.6rem);
flex: 1;
padding: 0.12rem 0.12rem 0.12rem 0.12rem;
height: var(--app-height, 100vh);
box-sizing: border-box;
overflow: hidden;
@@ -14,155 +13,168 @@
display: flex;
flex-direction: column;
font-size: 0.14rem;
}
// 手动包裹的两层 div需要撑满剩余高度并允许内部滚动
> .bg-white {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
overflow: hidden;
> .bg-main {
// ---- 主内容卡片 ----
&__main-card {
flex: 1;
min-height: 0;
margin: 0.16rem 0.24rem;
background: $bg-white;
border: 1px solid #F0F0F0;
box-shadow: 0px 1px 4px rgba(0, 0, 0, 0.04);
border-radius: 0.08rem;
display: flex;
flex-direction: column;
overflow: hidden;
}
}
// ---- 返回按钮 ----
&__back-row {
padding: 0.24rem 0.32rem 0;
}
// ---- 页面标题 ----
&__header {
margin-bottom: 0;
}
&__page-title {
font-size: 0.24rem;
font-weight: 700;
color: $text-dark;
margin: 0;
line-height: 1.1;
}
&__page-subtitle {
font-size: 0.13rem;
color: $text-light;
margin: 0.02rem 0 0 0;
}
// ---- Tab 切换 ----
&__tabs {
display: inline-flex;
&__back-btn {
display: flex;
align-items: center;
gap: 0.04rem;
margin: 0.1rem 0 0.16rem 0;
background: $bg-white;
border-radius: 0.22rem;
padding: 0.04rem;
}
&__tab {
font-size: 0.13rem;
color: $text-light;
background: none;
border: none;
cursor: pointer;
padding: 0.06rem 0.22rem;
border-radius: 0.18rem;
transition: all 0.25s ease;
user-select: none;
font-weight: 500;
color: $text-middle;
font-size: 0.14rem;
line-height: 0.19rem;
padding: 0;
&:hover {
color: $text-dark;
background: $theme-color;
}
}
&__back-icon {
width: 0.16rem;
height: 0.16rem;
}
// ---- 内容区域可滚动 ----
&__body {
flex: 1;
overflow-y: auto;
padding: 0.24rem 0.32rem 0.4rem;
}
// ---- 岗位头部信息区域 ----
&__header-section {
display: flex;
align-items: flex-start;
justify-content: space-between;
margin-bottom: 0.24rem;
}
// ---- 左侧基本信息 ----
&__basic-info {
flex: 1;
min-width: 0;
}
&__title-row {
display: flex;
align-items: center;
gap: 0.12rem;
margin-bottom: 0.08rem;
}
&__job-title {
font-size: 0.24rem;
font-weight: 600;
line-height: 0.32rem;
color: $text-dark;
margin: 0;
}
&__fav-btn {
background: none;
border: none;
cursor: pointer;
padding: 0;
display: flex;
align-items: center;
justify-content: center;
width: 0.2rem;
height: 0.2rem;
flex-shrink: 0;
color: #8C8C8C;
transition: color 0.2s;
&:hover {
color: $danger;
}
&--active {
background: $accent;
color: $bg-white;
color: $danger;
.job-detail__fav-icon path {
fill: currentColor;
}
}
}
&__fav-icon {
width: 0.18rem;
height: 0.18rem;
}
&__meta-row {
font-size: 0.12rem;
line-height: 0.16rem;
color: $text-middle;
margin-bottom: 0.12rem;
}
&__meta-dot {
margin: 0 0.06rem;
color: $text-middle;
}
// ---- 操作按钮 ----
&__action-btns {
display: flex;
align-items: center;
gap: 0.16rem;
}
&__nova-btn {
display: flex;
align-items: center;
justify-content: center;
padding: 0.08rem 0.16rem;
border: 1px solid $accent;
border-radius: 0.08rem;
background: $bg-white;
color: $accent;
font-size: 0.14rem;
font-weight: 600;
box-shadow: 0 0.02rem 0.08rem rgba(79, 194, 201, 0.3);
&:hover {
background: $accent-hover;
color: $bg-white;
}
}
}
// ---- 顶部操作栏 ----
&__toolbar {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 0.14rem;
}
&__close-btn {
width: 0.32rem;
height: 0.32rem;
border-radius: 50%;
border: 1px solid $border-color;
background: $bg-white;
color: $text-dark;
line-height: 0.19rem;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
&:hover {
background: $theme-color;
border-color: $accent;
color: $accent;
}
}
&__close-icon {
width: 0.14rem;
height: 0.14rem;
}
&__toolbar-right {
display: flex;
align-items: center;
gap: 0.1rem;
}
&__tool-btn {
width: 0.32rem;
height: 0.32rem;
border-radius: 50%;
border: 1px solid $border-color;
background: $bg-white;
color: $text-light;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
&:hover {
color: $accent;
border-color: $accent;
background: $theme-color;
}
}
&__tool-icon {
width: 0.14rem;
height: 0.14rem;
}
&__apply-btn {
display: flex;
align-items: center;
justify-content: center;
padding: 0.08rem 0.16rem;
background: $accent;
border: 1px solid $accent;
border-radius: 0.08rem;
color: $bg-white;
border: none;
border-radius: 0.2rem;
padding: 0.07rem 0.22rem;
font-size: 0.13rem;
font-weight: 500;
font-size: 0.14rem;
font-weight: 600;
line-height: 0.19rem;
cursor: pointer;
transition: background 0.2s;
@@ -171,225 +183,59 @@
}
}
// ---- 内容区域 ----
&__body {
flex: 1;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 0.14rem;
padding-bottom: 0.4rem;
padding-right: 0.05rem;
}
// ---- 导航 Tab ----
&__nav-tabs {
display: flex;
align-items: center;
gap: 0.2rem;
margin-bottom: 0.04rem;
}
&__nav-tab {
font-size: 0.16rem;
font-weight: 600;
color: $text-light;
cursor: pointer;
padding-bottom: 0.06rem;
border-bottom: 2px solid transparent;
transition: all 0.2s;
&:hover {
color: $text-dark;
}
&--active {
color: $text-dark;
border-bottom-color: $text-dark;
}
}
&__nav-tab-right {
margin-left: auto;
display: flex;
align-items: center;
gap: 0.16rem;
padding-right: 0.05rem;
}
&__link-btn {
font-size: 0.12rem;
color: $text-light;
cursor: pointer;
transition: color 0.2s;
&:hover {
color: $accent;
}
}
// ---- 卡片 ----
&__card {
background: $bg-white;
border-radius: 0.12rem;
padding: 0.22rem 0.24rem;
}
&__card-top {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 0.3rem;
}
&__card-left {
flex: 1;
min-width: 0;
}
// ---- 公司 & 职位头部 ----
&__company-row {
display: flex;
align-items: center;
gap: 0.1rem;
margin-bottom: 0.1rem;
}
&__company-icon {
width: 0.36rem;
height: 0.36rem;
// ---- 右侧匹配度卡片 ----
&__match-card {
flex-shrink: 0;
width: 2.06rem;
background: #FBFDF7;
border: 1px solid #9FD051;
border-radius: 0.08rem;
background: $theme-color;
padding: 0.16rem 0.24rem;
}
&__match-top {
display: flex;
align-items: center;
justify-content: center;
border: 1px solid $border-color;
flex-shrink: 0;
}
&__company-svg {
width: 0.2rem;
height: 0.2rem;
color: $accent;
}
&__company-name {
font-size: 0.15rem;
font-weight: 600;
color: $text-dark;
}
&__job-title {
font-size: 0.2rem;
font-weight: 700;
color: $text-dark;
margin: 0 0 0.1rem 0;
}
&__job-meta {
display: flex;
align-items: center;
gap: 0.2rem;
flex-wrap: wrap;
}
&__meta-item {
display: flex;
align-items: center;
gap: 0.05rem;
font-size: 0.12rem;
color: $text-light;
}
&__meta-icon {
width: 0.14rem;
height: 0.14rem;
color: $text-light;
flex-shrink: 0;
}
// ---- 匹配度 ----
&__match-area {
display: flex;
flex-direction: column;
align-items: center;
flex-shrink: 0;
}
&__match-ring {
position: relative;
width: 0.72rem;
height: 0.72rem;
display: flex;
align-items: center;
justify-content: center;
}
&__ring-svg {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
align-items: baseline;
gap: 0.08rem;
margin-bottom: 0.2rem;
}
&__match-score {
font-size: 0.18rem;
font-weight: 700;
color: $accent-hover;
position: relative;
z-index: 1;
font-size: 0.3rem;
font-weight: 630;
line-height: 0.4rem;
color: #9FD051;
}
&__match-label {
font-size: 0.1rem;
color: $text-light;
margin-top: 0.04rem;
font-size: 0.13rem;
font-weight: 520;
line-height: 0.17rem;
color: #9FD051;
}
&__match-details {
display: flex;
align-items: center;
gap: 0.12rem;
margin-top: 0.12rem;
}
&__match-item {
&__match-list {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.03rem;
gap: 0.12rem;
}
&__match-mini-ring {
position: relative;
width: 0.4rem;
height: 0.4rem;
&__match-row {
display: flex;
align-items: center;
justify-content: center;
justify-content: space-between;
}
&__mini-ring-svg {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
&__match-row-label {
font-size: 0.12rem;
line-height: 0.16rem;
color: #6B7280;
}
&__mini-score {
font-size: 0.1rem;
font-weight: 600;
&__match-row-score {
font-size: 0.12rem;
line-height: 0.16rem;
color: $text-dark;
position: relative;
z-index: 1;
}
&__match-item-label {
font-size: 0.09rem;
color: $text-light;
white-space: nowrap;
}
// ---- 优化简历提示条 ----
@@ -397,93 +243,70 @@
display: flex;
align-items: center;
justify-content: space-between;
background: linear-gradient(90deg, #94EF9E , #53D9C8 );
color: $bg-white;
border-radius: 0.1rem;
padding: 0.14rem 0.2rem;
font-size: 0.13rem;
background: linear-gradient(270deg, rgba(159, 208, 81, 0.1) 0%, rgba(82, 202, 209, 0.1) 62.02%);
border-radius: 0.08rem;
padding: 0.12rem 0.24rem;
margin-bottom: 0.32rem;
}
&__optimize-text {
font-size: 0.16rem;
font-weight: 600;
line-height: 0.21rem;
color: $text-dark;
}
&__optimize-btn {
background: $bg-white;
color: $text-dark;
display: flex;
align-items: center;
justify-content: center;
padding: 0.08rem 0.16rem;
background: #52CAD1;
border: none;
border-radius: 0.06rem;
padding: 0.07rem 0.18rem;
font-size: 0.12rem;
font-weight: 500;
border-radius: 0.08rem;
color: $bg-white;
font-size: 0.14rem;
font-weight: 600;
line-height: 0.19rem;
cursor: pointer;
transition: all 0.2s;
white-space: nowrap;
transition: background 0.2s;
&:hover {
background: $theme-color;
color: $accent-hover;
background: $btn-dark-hover;
}
}
// ---- 描述文本 ----
&__desc-text {
font-size: 0.13rem;
color: $text-dark;
line-height: 1.8;
margin: 0 0 0.12rem 0;
// ---- 内容区块 ----
&__section {
margin-bottom: 0.32rem;
}
// ---- 标签 ----
&__tag-list {
display: flex;
flex-wrap: wrap;
gap: 0.08rem;
}
&__tag {
font-size: 0.12rem;
color: $text-dark;
background: $bg-main;
border: 1px solid $border-color;
border-radius: 0.04rem;
padding: 0.04rem 0.14rem;
transition: all 0.2s;
&:hover {
border-color: $accent;
color: $accent;
background: $theme-color;
}
}
// ---- 区块标题 ----
&__section-title {
font-size: 0.17rem;
font-weight: 700;
font-size: 0.18rem;
font-weight: 600;
line-height: 0.24rem;
color: $text-dark;
margin: 0 0 0.14rem 0;
margin: 0 0 0.16rem 0;
}
&__section-header {
&__section-list {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 0.14rem;
.job-detail__section-title {
margin-bottom: 0;
}
flex-direction: column;
gap: 0.12rem;
}
&__skill-hint {
display: flex;
align-items: center;
gap: 0.04rem;
font-size: 0.11rem;
color: $text-light;
&__section-item {
font-size: 0.14rem;
line-height: 0.19rem;
color: $text-middle;
}
&__hint-icon {
width: 0.12rem;
height: 0.12rem;
flex-shrink: 0;
&__bonus-item {
font-size: 0.14rem;
line-height: 0.19rem;
color: $text-dark;
margin-top: 0.12rem;
}
// ---- 技能标签 ----
@@ -491,161 +314,81 @@
display: flex;
flex-wrap: wrap;
gap: 0.08rem;
margin-bottom: 0.16rem;
margin-bottom: 0.12rem;
}
&__skill-tag {
display: inline-flex;
display: flex;
align-items: center;
gap: 0.04rem;
justify-content: center;
padding: 0.04rem 0.06rem;
font-size: 0.12rem;
color: $text-dark;
background: $bg-main;
border: 1px solid $border-color;
border-radius: 0.2rem;
padding: 0.05rem 0.14rem;
transition: all 0.2s;
line-height: 0.18rem;
color: $text-middle;
background: #F3F4F6;
border-radius: 0.04rem;
&--matched {
background: $theme-color;
border-color: $accent;
color: $accent-hover;
}
}
&__skill-close {
width: 0.1rem;
height: 0.1rem;
color: $accent;
cursor: pointer;
}
// ---- 列表 ----
&__list {
font-size: 0.13rem;
color: $text-dark;
line-height: 2;
margin: 0;
padding-left: 0.2rem;
li {
padding-left: 0.04rem;
}
}
// ---- 公司信息 ----
&__company-info {
display: flex;
gap: 0.4rem;
}
&__company-info-left {
flex: 1;
min-width: 0;
}
&__company-header {
// ---- 公司概况 ----
&__company-head {
display: flex;
align-items: center;
gap: 0.1rem;
gap: 0.12rem;
margin-bottom: 0.12rem;
}
&__company-logo {
width: 0.36rem;
height: 0.36rem;
border-radius: 50%;
background: $bg-main;
width: 0.48rem;
height: 0.48rem;
border-radius: 0.08rem;
background: #EDF9FA;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid $border-color;
flex-shrink: 0;
}
&__company-info-name {
font-size: 0.16rem;
&__company-logo-img {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 0.08rem;
}
&__company-svg {
width: 0.32rem;
height: 0.32rem;
color: #52CAD1;
}
&__company-name-info {
display: flex;
flex-direction: column;
gap: 0.06rem;
}
&__company-name {
font-size: 0.18rem;
font-weight: 600;
line-height: 0.24rem;
color: $text-dark;
}
&__company-sub {
font-size: 0.14rem;
line-height: 0.19rem;
color: $text-middle;
}
&__company-desc {
font-size: 0.12rem;
color: $text-dark;
line-height: 1.8;
margin: 0;
}
&__company-info-right {
flex-shrink: 0;
display: flex;
flex-direction: column;
gap: 0.08rem;
max-width: 45%;
}
&__company-meta-item {
font-size: 0.12rem;
color: $text-dark;
line-height: 1.6;
}
&__meta-label {
color: $text-light;
}
&__company-link {
color: $accent;
text-decoration: none;
transition: color 0.2s;
&:hover {
color: $accent-hover;
text-decoration: underline;
}
}
// ---- 融资 ----
&__funding-label {
font-weight: 600;
color: $text-dark;
}
// ---- 最新动态 ----
&__news-list {
display: flex;
gap: 0.14rem;
}
&__news-item {
flex: 1;
background: $bg-main;
border: 1px solid $border-color;
border-radius: 0.08rem;
padding: 0.14rem 0.16rem;
transition: all 0.2s;
&:hover {
border-color: $accent;
background: $theme-color;
}
}
&__news-title {
font-size: 0.13rem;
font-weight: 600;
color: $text-dark;
margin: 0 0 0.06rem 0;
}
&__news-desc {
font-size: 0.11rem;
font-size: 0.14rem;
line-height: 0.24rem;
color: $text-middle;
line-height: 1.6;
margin: 0;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
}
+15 -12
View File
@@ -3,11 +3,13 @@
@use '../variables' as *;
.jobs-page {
display: flex;
&__content {
margin-left: 2rem;
margin-right: var(--chat-width, 4.0rem);
margin-right: var(--chat-width, 3.6rem);
flex: 1;
padding: 0.12rem 0.18rem;
padding-bottom: 0.18rem;
height: var(--app-height, 100vh);
box-sizing: border-box;
overflow: hidden;
@@ -79,10 +81,8 @@
// 筛选条件外层白色背景条
&__filters-bar {
background: $bg-white;
border-radius: 0.12rem;
padding: 0.14rem 0.18rem;
margin-bottom: 0.2rem;
margin: 0 0.24rem 0.2rem 0.24rem;
}
// 筛选条件
@@ -103,11 +103,11 @@
display: flex;
align-items: center;
gap: 0.04rem;
background: $bg-main;
background: #fff;
border: 1px solid #E8E8E8;
border-radius: 0.2rem;
padding: 0.05rem 0.14rem;
color: $text-dark;
border-radius: 0.08rem;
padding: 0.15rem 0.12rem;
color: $text-middle;
font-size: 0.12rem;
cursor: pointer;
transition: all 0.2s ease;
@@ -116,9 +116,11 @@
height: 0.234rem;
&:hover {
border-color: $accent;
color: $accent;
background: $theme-color;
color: #fff;
background: $btn-dark;
svg{
color: #fff;
}
}
}
@@ -207,6 +209,7 @@
flex: 1;
overflow-y: auto;
padding-bottom: 0.2rem;
margin: 0 0.24rem;
// 列表中每张卡片间距用 margin-bottom 替代 gap
& > .jobs-page__job-card {
+4
View File
@@ -303,6 +303,10 @@
// 查看详情按钮
&__detail-btn {
display: flex;
justify-content: center;
gap: 0.06rem;
align-items: center;
height: 0.32rem;
line-height: 0.32rem;
width: 0.92rem;
+1 -1
View File
@@ -9,7 +9,7 @@ $theme-color: #F6FCFC;
$selected-color: #F6FCFC;
// 页面主背景色
$bg-main: #F3F4F5;
$bg-main: #F3F4F6;
// 页面中间色景色用于页面主题色背景和白色背景分隔用于某些模块组件的组件外层背景等
$bg-middle: #E6E6E6;
+80 -72
View File
@@ -1,40 +1,32 @@
<template>
<!-- Nova AI 对话面板 -->
<div class="ai-chat">
<!-- 顶部会员横幅 点击打开会员购买弹窗 -->
<div class="ai-chat__banner" @click="showMemberDialog = true">
<span class="dflex-start aliite-c"> <img src="@/assets/images/bule-flash.png" alt="Offer派" class="w16 h19 mr5" /> <span>升级 Pro 开启求职加速</span></span>
<span class="ai-chat__banner-arrow"></span>
</div>
<!-- AI 头像和名称 -->
<!-- 顶部头部区域 -->
<div class="ai-chat__header">
<div class="ai-chat__avatar">👤</div>
<div class="ai-chat__header-left">
<!-- Nova 头像 -->
<img class="ai-chat__avatar" src="@/assets/images/nova-logo.png" alt="Nova" />
<!-- 名称和副标题 -->
<div class="ai-chat__info">
<div class="ai-chat__name">Nova</div>
<div class="ai-chat__desc">你的AI求职助</div>
<div class="ai-chat__desc">你的AI求职助</div>
</div>
</div>
<!-- 在线状态标签 -->
<div class="ai-chat__online-tag">
<span class="ai-chat__online-dot"></span>
<span>在线</span>
</div>
</div>
<!-- 聊天消息区域 -->
<div class="ai-chat__messages" ref="messagesRef">
<!-- AI 欢迎消息 -->
<div class="ai-chat__msg ai-chat__msg--ai">
<div class="ai-chat__msg-bubble">
<div class="ai-chat__msg-title">欢迎回来{{nickName}}!</div>
<div class="ai-chat__msg-text">很高兴再次见到你让我们继续您通往理想工作的旅程吧</div>
</div>
</div>
<div class="ai-chat__messages scrollbar-thin" ref="messagesRef">
<!-- 快捷问题仅岗位详情页显示3个用户提问靠右 -->
<div v-if="userQuestions.length > 0" class="ai-chat__quick-questions">
<div
v-for="(q, i) in userQuestions"
:key="i"
class="ai-chat__quick-item ai-chat__quick-item--user"
@click="sendQuickQuestion(q)"
>
{{ q }}
</div>
<!-- 固定欢迎气泡 -->
<div class="ai-chat__welcome-bubble">
<div class="ai-chat__welcome-title">欢迎使用 Offer派👋</div>
<div class="ai-chat__welcome-text">从找岗位到投递跟进我会陪你一步步拿下理想Offer</div>
</div>
<!-- 动态消息列表 -->
@@ -47,8 +39,33 @@
<div class="ai-chat__msg-bubble" v-html="formatContent(msg.content)"></div>
</div>
<!-- AI 正在思考中加载指示器 -->
<AiThinkingIndicator v-if="aiLoading" text="AI正在思考中" />
<!-- AI 等待中 3 点动效 -->
<div v-if="aiLoading" class="ai-chat__msg ai-chat__msg--ai">
<div class="ai-chat__typing-indicator">
<span class="ai-chat__typing-dot"></span>
<span class="ai-chat__typing-dot"></span>
<span class="ai-chat__typing-dot"></span>
</div>
</div>
</div>
<!-- 话题推荐模块固定在输入框上方 -->
<div class="ai-chat__topics">
<div class="ai-chat__topics-header">
<svg class="ai-chat__topics-icon" width="16" height="16" viewBox="0 0 16 16" fill="none">
<path d="M8 1L10 6L15 6.5L11.5 10L12.5 15L8 12.5L3.5 15L4.5 10L1 6.5L6 6L8 1Z" fill="#52CAD1"/>
</svg>
<span class="ai-chat__topics-title">话题推荐</span>
</div>
<div
v-for="(topic, i) in topicList"
:key="i"
class="ai-chat__topic-btn"
@click="sendQuickQuestion(topic)"
>
{{ topic }}
</div>
</div>
<!-- 底部输入框 -->
@@ -56,17 +73,17 @@
<input
v-model="inputText"
class="ai-chat__input"
placeholder="搜索职位、公司或关键词..."
placeholder="请输入你想问的问题…"
@keyup.enter="sendMessage"
:disabled="aiLoading"
/>
<button class="ai-chat__send-btn" @click="sendMessage" :disabled="aiLoading">
<span></span>
<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
<path d="M4 10L16 4L13 10L16 16L4 10Z" fill="#FFFFFF"/>
</svg>
</button>
</div>
<!-- 会员购买弹窗 -->
<MemberDialog v-model="showMemberDialog" />
</div>
</template>
@@ -75,8 +92,6 @@ import { ref, computed, watch, nextTick, onMounted } from 'vue'
import { useRoute } from 'vue-router'
import { useStore } from 'vuex'
import markdownit from 'markdown-it'
import MemberDialog from '@/components/MemberDialog.vue'
import AiThinkingIndicator from '@/components/tools/AiThinkingIndicator.vue'
import { sendNovaChat } from '@/utils/aiRequest'
import type { NovaChatHistoryItem } from '@/utils/aiRequest'
import { fetchResumeList } from '@/api/resume'
@@ -85,9 +100,9 @@ import { fetchResumeList } from '@/api/resume'
/** markdown-it 实例,用于将 AI 回复的 Markdown 文本渲染为 HTML */
const md = markdownit({
html: false, // HTML XSS
breaks: true, // <br>
linkify: true, //
html: false,
breaks: true,
linkify: true,
})
// ==================== Props ====================
@@ -98,23 +113,16 @@ const props = defineProps<{
jobId?: string
}>()
/** 当前路由(用于判断是否在岗位详情页) */
/** 当前路由 */
const currentRoute = useRoute()
const store = useStore()
/** 用户昵称 — 从全局 store 读取 */
const nickName = computed(() => store.state.userInfo?.nick || '')
// ==================== ====================
/** 会员购买弹窗的显示状态 */
const showMemberDialog = ref(false)
/** 输入框内容 */
const inputText = ref('')
/** 消息列表容器 DOM 引用(用于滚动控制) */
/** 消息列表容器 DOM 引用 */
const messagesRef = ref<HTMLElement>()
/** AI 是否正在请求中 */
@@ -123,13 +131,20 @@ const aiLoading = ref(false)
/** 默认简历 ID */
const defaultResumeId = ref<string>('')
// ==================== ====================
/** 固定话题推荐(始终显示) */
const topicList = [
'我适合投哪些岗位?',
'帮我看看我的简历可以怎么优化?',
'近期有哪些岗位值得投?',
]
// ==================== ====================
/** 聊天消息类型 */
interface ChatMsg {
/** 角色:assistant-AI助手 user-用户 */
role: 'assistant' | 'user'
/** 消息内容 */
content: string
}
@@ -137,8 +152,6 @@ interface ChatMsg {
/** 获取当前用户的缓存 key */
function getCacheKey(): string {
// sessionStorage +
// ID
const userId = sessionStorage.getItem('userId') || 'anonymous'
return `nova_chat_${userId}`
}
@@ -175,19 +188,6 @@ const messages = ref<ChatMsg[]>([])
/** 是否在岗位详情页 */
const isJobDetailPage = computed(() => currentRoute.name === 'JobDetail' || currentRoute.path.startsWith('/jobs/'))
/** 快捷问题列表(仅岗位详情页显示的 3 个用户提问) */
const quickQuestions = computed(() => {
if (!isJobDetailPage.value) return []
return [
'告诉我这个工作为什么适合我?',
'我想修改这个岗位,怎么优化简历?',
'帮我针对这个岗位生成一份面试攻略',
]
})
/** 用户提问列表(等同于 quickQuestions */
const userQuestions = computed(() => quickQuestions.value)
// ==================== ====================
/** 将消息内容通过 markdown-it 渲染为 HTML */
@@ -208,7 +208,7 @@ function scrollToBottom() {
// ==================== ID ====================
/** 加载默认简历 ID(用于 Nova 对话接口) */
/** 加载默认简历 ID */
async function loadDefaultResumeId() {
try {
const res = await fetchResumeList()
@@ -223,13 +223,13 @@ async function loadDefaultResumeId() {
// ==================== ====================
/** 点击快捷问题,填入输入框并发送 */
/** 点击话题推荐,填入输入框并发送 */
function sendQuickQuestion(question: string) {
inputText.value = question
sendMessage()
}
/** 发送消息(回车或点击发送按钮触发) */
/** 发送消息 */
async function sendMessage() {
if (!inputText.value.trim() || aiLoading.value) return
@@ -256,8 +256,8 @@ async function sendMessage() {
jobId: props.jobId || undefined,
history,
})
// AI
const aiReply = res?.data?.message || res?.message || '抱歉,我暂时无法回答这个问题。'
// res.data.message AI { code, msg, data: { message } }
const aiReply = res?.data?.message || '抱歉,我暂时无法回答这个问题。'
messages.value.push({ role: 'assistant', content: aiReply })
} catch (e) {
console.error('Nova 对话请求失败', e)
@@ -269,8 +269,6 @@ async function sendMessage() {
}
}
// ==================== ====================
// ==================== ====================
onMounted(() => {
@@ -285,13 +283,23 @@ onMounted(() => {
}
})
// jobId Jobs "" AI
// jobId
watch(() => props.jobId, (newId, oldId) => {
if (newId && newId !== oldId && !isJobDetailPage.value) {
// Jobs AI
messages.value.push({ role: 'assistant', content: '你想知道关于这个岗位的什么信息?' })
saveChatToCache()
scrollToBottom()
}
})
// ==================== ====================
/** 触发"问Nova"交互:推送提示消息并聚焦输入框 */
function triggerAsk() {
messages.value.push({ role: 'assistant', content: '你想知道关于这个岗位的什么信息?' })
saveChatToCache()
scrollToBottom()
}
defineExpose({ triggerAsk })
</script>
+12 -10
View File
@@ -1,22 +1,24 @@
<template>
<div>
<div class="job-page-header">
<div class="bg-f mb16 prl24 ptb16">
<div class="job-page-header dflex-start aliite-c">
<h2 class="job-page-header__title">发现理想职位</h2>
<p class="job-page-header__subtitle">找到最适合你的工作机会</p>
</div>
<div class="job-page-header__tabs mt20">
<div class="job-page-header__tabs ml30 ">
<div
v-for="(tab,index) in tabs"
:key="tab.key"
class="job-page-header__tab"
:class="{ 'job-page-header__tab--active': activeTab === tab.key }"
:style="index==0?'padding:0.06rem 0.30rem;':''"
:style="index==0?'padding:0.06rem 0.20rem;':''"
@click="handleTabClick(tab.key)"
>
{{ tab.label }}
<!-- 收藏/已投递 计数圆形徽章 -->
<span v-if="tab.count !== undefined" class="job-page-header__tab-badge">{{ tab.count }}</span>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
@@ -46,11 +48,11 @@ const route = useRoute()
// ==================== ====================
/** Tab 选项列表 */
/** Tab 选项列表count 用于显示圆形徽章计数 */
const tabs = computed(() => [
{ key: 'recommend', label: '推荐' },
{ key: 'collected', label: `收藏(${props.favoriteCount ?? 0}` },
{ key: 'applied', label: `投递(${props.applyCount ?? 0}` },
{ key: 'recommend', label: '推荐', count: undefined },
{ key: 'collected', label: '收藏', count: props.favoriteCount ?? 0 },
{ key: 'applied', label: '已投递', count: props.applyCount ?? 0 },
])
// ==================== ====================
+9 -52
View File
@@ -1,49 +1,10 @@
<template>
<!-- 岗位专属简历定制弹窗步骤1居中弹窗步骤2+右侧抽屉 -->
<div v-if="modelValue" class="job-resume-custom-dialog" :class="{ 'job-resume-custom-dialog--drawer': currentStep >= 2 }">
<!-- 岗位专属简历定制弹窗直接显示右侧抽屉 -->
<div v-if="modelValue" class="job-resume-custom-dialog job-resume-custom-dialog--drawer">
<div class="job-resume-custom-dialog__overlay" @click="handleClose"></div>
<!-- ===== 步骤一居中弹窗 ===== -->
<div v-if="currentStep === 1" class="job-resume-custom-dialog__panel">
<div class="job-resume-custom-dialog__header">
<h2 class="job-resume-custom-dialog__title">10s快速定制岗位专属简历</h2>
<button class="job-resume-custom-dialog__close-btn" @click="handleClose" aria-label="关闭">
<svg viewBox="0 0 16 16" fill="none" class="job-resume-custom-dialog__close-icon"><path d="M12 4L4 12M4 4l8 8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
</button>
</div>
<div v-if="isLowMatch" class="job-resume-custom-dialog__tip-bar"><span>你与该岗位的匹配度较低简历可能无法通过机筛</span></div>
<div class="job-resume-custom-dialog__job-card">
<div class="job-resume-custom-dialog__job-left">
<div class="job-resume-custom-dialog__company-icon">
<img v-if="jobInfo.companyLogoUrl" :src="jobInfo.companyLogoUrl" :alt="jobInfo.company" class="job-resume-custom-dialog__company-logo-img" />
<svg v-else viewBox="0 0 24 24" fill="none" class="job-resume-custom-dialog__company-svg"><rect x="3" y="7" width="18" height="14" rx="2" stroke="currentColor" stroke-width="1.5"/><path d="M7 7V5a2 2 0 012-2h6a2 2 0 012 2v2" stroke="currentColor" stroke-width="1.5"/><path d="M3 13h18" stroke="currentColor" stroke-width="1.5"/></svg>
</div>
<div class="job-resume-custom-dialog__job-info">
<span class="job-resume-custom-dialog__job-title">{{ jobInfo.title }}</span>
<span class="job-resume-custom-dialog__job-sub">{{ jobInfo.location }} · {{ jobInfo.company }}</span>
</div>
</div>
<div class="job-resume-custom-dialog__match-area">
<div class="job-resume-custom-dialog__match-ring">
<svg viewBox="0 0 60 60" class="job-resume-custom-dialog__ring-svg"><circle cx="30" cy="30" r="24" stroke-width="4" stroke="#E8E8E8" fill="none" opacity="0.3"/><circle cx="30" cy="30" r="24" stroke-width="4" fill="none" stroke="#4FC2C9" stroke-linecap="round" :stroke-dasharray="2*Math.PI*24" :stroke-dashoffset="2*Math.PI*24*(1-jobInfo.matchScore/10)" transform="rotate(-90 30 30)"/></svg>
<span class="job-resume-custom-dialog__match-score">{{ jobInfo.matchScore }}</span>
</div>
<span class="job-resume-custom-dialog__match-label">{{ matchLevelText }}</span>
</div>
</div>
<div class="job-resume-custom-dialog__skills-section">
<p class="job-resume-custom-dialog__skills-title">缺少{{ missingSkills.length }}项技能</p>
<div class="job-resume-custom-dialog__skills-list">
<span v-for="skill in missingSkills" :key="skill" class="job-resume-custom-dialog__skill-tag">{{ skill }}</span>
</div>
</div>
<div class="job-resume-custom-dialog__footer">
<button class="job-resume-custom-dialog__primary-btn" @click="goToStep(2)">立即定制简历</button>
<span class="job-resume-custom-dialog__skip-link" @click="handleSkip">不优化直接投递</span>
</div>
</div>
<!-- ===== 步骤2+右侧抽屉 ===== -->
<div v-if="currentStep >= 2" class="job-resume-custom-dialog__drawer">
<!-- ===== 右侧抽屉 ===== -->
<div class="job-resume-custom-dialog__drawer">
<!-- 抽屉头部 -->
<div class="job-resume-custom-dialog__drawer-header">
<button class="job-resume-custom-dialog__close-btn" @click="handleClose" aria-label="关闭">
@@ -62,7 +23,7 @@
<span class="job-resume-custom-dialog__step-num">2</span><span>定制简历</span>
</div>
<div class="job-resume-custom-dialog__step" :class="{ 'job-resume-custom-dialog__step--active': currentStep === 4 }">
<span class="job-resume-custom-dialog__step-num">3</span><span>预览</span>
<span class="job-resume-custom-dialog__step-num">3</span><span>简历预览</span>
</div>
</div>
<!-- 抽屉内容区可滚动 -->
@@ -423,8 +384,8 @@ const fullLoading = ref(false)
/** 全屏加载文案 */
const fullLoadingText = ref('')
/** 当前步骤:1-确认入口(居中弹窗) 2-差距分析(右侧抽屉) 3-定制简历 4-预览 */
const currentStep = ref(1)
/** 当前步骤:2-差距分析(右侧抽屉) 3-定制简历 4-预览 */
const currentStep = ref(2)
/** 缺少的技能列表 */
const missingSkills = computed(() => props.jobInfo.missingSkills || [])
@@ -602,7 +563,7 @@ function onEditPanelUpdate(data: CustomizeResumeData) {
/** 关闭弹窗并重置步骤 */
function handleClose() {
currentStep.value = 1
currentStep.value = 2
showResumeDropdown.value = false
showDownloadMenu.value = false
// AI
@@ -613,11 +574,7 @@ function handleClose() {
emit('update:modelValue', false)
}
/** 不优化,直接投递 */
function handleSkip() {
handleClose()
emit('skip')
}
// ==================== ====================
+7 -1
View File
@@ -41,7 +41,13 @@
</span>
<!-- 作品集链接显示在个人信息卡片内 -->
<span v-if="profile.portfolioUrl" class="profile-page-content__info-contacts">
<span class="profile-page-content__contact-item">🔗 <a :href="profile.portfolioUrl" target="_blank" rel="noopener noreferrer" class="profile-page-content__portfolio-link">{{ profile.portfolioUrl }}</a></span>
<span class="profile-page-content__contact-item dib">
<svg xmlns="http://www.w3.org/2000/svg" class="resume-detail__contact-icon mr6" width="0.15rem" height="0.15rem" viewBox="0 0 16 16">
<path d="M0 0h16v16H0z" fill="none" />
<path fill="none" stroke="currentColor" d="M14.5 8A6.5 6.5 0 0 1 8 14.5M14.5 8A6.5 6.5 0 0 0 8 1.5M14.5 8c0 1.657-2.91 3-6.5 3S1.5 9.657 1.5 8m13 0c0-1.657-2.91-3-6.5-3S1.5 6.343 1.5 8M8 14.5A6.5 6.5 0 0 1 1.5 8M8 14.5c1.657 0 3-2.91 3-6.5S9.657 1.5 8 1.5m0 13c-1.657 0-3-2.91-3-6.5s1.343-6.5 3-6.5M1.5 8A6.5 6.5 0 0 1 8 1.5" />
</svg>
<a :href="profile.portfolioUrl?.startsWith('http') ? profile.portfolioUrl : `http://${profile.portfolioUrl}`" target="_blank" rel="noopener noreferrer" class="profile-page-content__portfolio-link">{{ profile.portfolioUrl }}</a>
</span>
</span>
</div>
</template>
+7 -1
View File
@@ -3,11 +3,12 @@
<div class="industry-selector" ref="selectorRef">
<!-- 触发按钮显示已选行业名称或默认文字"行业" -->
<div class="industry-selector__trigger" :style="triggerStyle" @click="toggleDropdown">
<div class="industry-selector__trigger" :style="{ ...triggerStyle, ...(isHovered || visible ? hoverStyle : {}) }" @click="toggleDropdown" @mouseenter="isHovered = true" @mouseleave="isHovered = false">
<span class="industry-selector__display" :style="displayStyle" :title="displayText">{{ displayText }}</span>
<svg
class="industry-selector__arrow"
:class="{ 'industry-selector__arrow--open': visible }"
:style="(isHovered || visible) && hoverStyle?.color ? { color: hoverStyle.color } : {}"
viewBox="0 0 12 12"
fill="none"
>
@@ -147,6 +148,8 @@ const props = withDefaults(
level?: 1 | 2
/** 父组件传入的触发按钮自定义样式,用于在不同场景下覆盖默认外观 */
triggerStyle?: Record<string, string>
/** 父组件传入的触发按钮 hover 时的自定义样式 */
hoverStyle?: Record<string, string>
/** 父组件传入的显示文字自定义样式,用于覆盖 max-width 等默认样式 */
displayStyle?: Record<string, string>
}>(),
@@ -170,6 +173,9 @@ const selectorRef = ref<HTMLElement | null>(null)
/** 下拉面板是否可见 */
const visible = ref(false)
/** 触发按钮是否处于 hover 状态 */
const isHovered = ref(false)
/** 搜索关键词 */
const searchText = ref('')
+7 -1
View File
@@ -3,11 +3,12 @@
<div class="job-category-selector" ref="selectorRef">
<!-- 触发按钮显示已选岗位名称或默认文字"岗位" -->
<div class="job-category-selector__trigger" :style="triggerStyle" @click="toggleDropdown">
<div class="job-category-selector__trigger" :style="{ ...triggerStyle, ...(isHovered || visible ? hoverStyle : {}) }" @click="toggleDropdown" @mouseenter="isHovered = true" @mouseleave="isHovered = false">
<span class="job-category-selector__display" :style="displayStyle" :title="displayText">{{ displayText }}</span>
<svg
class="job-category-selector__arrow"
:class="{ 'job-category-selector__arrow--open': visible }"
:style="(isHovered || visible) && hoverStyle?.color ? { color: hoverStyle.color } : {}"
viewBox="0 0 12 12"
fill="none"
>
@@ -170,6 +171,8 @@ const props = withDefaults(
level?: 1 | 2 | 3
/** 父组件传入的触发按钮自定义样式,用于在不同场景下覆盖默认外观 */
triggerStyle?: Record<string, string>
/** 父组件传入的触发按钮 hover 时的自定义样式 */
hoverStyle?: Record<string, string>
/** 父组件传入的显示文字自定义样式,用于覆盖 max-width 等默认样式 */
displayStyle?: Record<string, string>
}>(),
@@ -193,6 +196,9 @@ const selectorRef = ref<HTMLElement | null>(null)
/** 下拉面板是否可见 */
const visible = ref(false)
/** 触发按钮是否处于 hover 状态 */
const isHovered = ref(false)
/** 搜索关键词 */
const searchText = ref('')
+7 -1
View File
@@ -3,11 +3,12 @@
<div class="region-selector" ref="selectorRef">
<!-- 触发按钮显示已选地区名称或默认文字"城市" -->
<div class="region-selector__trigger" :style="triggerStyle" @click="toggleDropdown">
<div class="region-selector__trigger" :style="{ ...triggerStyle, ...(isHovered || visible ? hoverStyle : {}) }" @click="toggleDropdown" @mouseenter="isHovered = true" @mouseleave="isHovered = false">
<span class="region-selector__display" :style="displayStyle" :title="displayText">{{ displayText }}</span>
<svg
class="region-selector__arrow"
:class="{ 'region-selector__arrow--open': visible }"
:style="(isHovered || visible) && hoverStyle?.color ? { color: hoverStyle.color } : {}"
viewBox="0 0 12 12"
fill="none"
>
@@ -157,6 +158,8 @@ const props = withDefaults(
maxSelect?: number
/** 父组件传入的触发按钮自定义样式,用于在不同场景下覆盖默认外观 */
triggerStyle?: Record<string, string>
/** 父组件传入的触发按钮 hover 时的自定义样式 */
hoverStyle?: Record<string, string>
/** 父组件传入的显示文字自定义样式,用于覆盖 max-width 等默认样式 */
displayStyle?: Record<string, string>
}>(),
@@ -179,6 +182,9 @@ const selectorRef = ref<HTMLElement | null>(null)
/** 下拉面板是否可见 */
const visible = ref(false)
/** 触发按钮是否处于 hover 状态 */
const isHovered = ref(false)
/** 搜索关键词 */
const searchText = ref('')
+1 -1
View File
@@ -74,7 +74,7 @@ const remAdaptPlugin: Plugin = {
docEl.style.height = ''
docEl.style.overflow = ''
docEl.style.setProperty('--app-height', '100vh')
docEl.style.setProperty('--chat-width', '4rem')
docEl.style.setProperty('--chat-width', '3.6rem')
}
}
+15
View File
@@ -29,6 +29,21 @@ export function formatEmploymentType(type: number | undefined | null): string {
return JOB_TYPE_MAP[type] ?? ''
}
/** 学历要求映射:数字 → 中文标签(0=不限 1=大专 2=本科 3=硕士 4=博士) */
export const EDUCATION_MAP: Record<number, string> = {
0: '不限',
1: '大专',
2: '本科',
3: '硕士',
4: '博士',
}
/** 根据 education 值获取中文标签,未匹配返回空字符串 */
export function formatEducation(edu: number | undefined | null): string {
if (edu === undefined || edu === null) return ''
return EDUCATION_MAP[edu] ?? ''
}
/** 职位列表页缓存数据(从详情页返回时恢复用) */
export interface JobListCache {
/** 缓存的职位列表 */
+2 -1
View File
@@ -105,9 +105,10 @@ export interface NovaChatResponse {
* Nova AI
* POST /nova-chat/chat
* @param params
* @returns { code, msg, data: { message } }
*/
export function sendNovaChat(params: NovaChatParams) {
return aiService.post<NovaChatResponse>('/nova-chat/chat', params, {
return aiService.post<AiResult<NovaChatResponse>>('/nova-chat/chat', params, {
headers: {
Token: DEFAULT_TOKEN,
},
+79 -261
View File
@@ -4,275 +4,128 @@
<div class="job-detail__content">
<!-- 页面标题 + Tab 切换 -->
<JobPageHeader :activeTab="''" />
<div class="bg-white p20 border-ra20">
<div class="bg-main p20 border-ra20">
<!-- 顶部操作栏 -->
<div class="job-detail__toolbar">
<button class="job-detail__close-btn" @click="goBack" aria-label="关闭">
<svg viewBox="0 0 16 16" fill="none" class="job-detail__close-icon">
<path d="M12 4L4 12M4 4l8 8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
<!-- 主内容卡片 -->
<div class="job-detail__main-card">
<!-- 返回按钮 -->
<div class="job-detail__back-row">
<button class="job-detail__back-btn" @click="goBack">
<svg viewBox="0 0 16 16" fill="none" class="job-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 class="job-detail__toolbar-right">
<button class="job-detail__tool-btn" aria-label="编辑" @click="openDislikeDialog">
<svg viewBox="0 0 16 16" fill="none" class="job-detail__tool-icon">
<path d="M11.5 2.5l2 2L5 13H3v-2l8.5-8.5z" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/>
</svg>
</button>
<button class="jobs-page__action-icon-btn" :class="{ 'job-detail__tool-btn--liked': job.isFavorite }" aria-label="收藏" @click="toggleFavorite">
<svg viewBox="0 0 16 16" :fill="job.isFavorite ? 'currentColor' : 'none'" class="job-detail__tool-icon">
<path d="M8 13.7l-1.1-1C3.6 9.8 1.5 7.9 1.5 5.7 1.5 3.9 2.9 2.5 4.7 2.5c1 0 2 .5 2.6 1.2h1.4c.6-.7 1.6-1.2 2.6-1.2 1.8 0 3.2 1.4 3.2 3.2 0 2.2-2.1 4.1-5.4 6.9L8 13.7z" stroke="currentColor" stroke-width="1"/>
</div>
<!-- 内容区域可滚动 -->
<div class="job-detail__body scrollbar-thin">
<!-- 岗位基本信息区 -->
<div class="job-detail__header-section">
<!-- 左侧岗位信息 -->
<div class="job-detail__basic-info">
<!-- 岗位标题 + 收藏 -->
<div class="job-detail__title-row">
<h1 class="job-detail__job-title">{{ job.title }}</h1>
<button class="job-detail__fav-btn" :class="{ 'job-detail__fav-btn--active': job.isFavorite }" aria-label="收藏" @click="toggleFavorite">
<svg viewBox="0 0 16 16" class="job-detail__fav-icon">
<path d="M8 13.7l-1.1-1C3.6 9.8 1.5 7.9 1.5 5.7 1.5 3.9 2.9 2.5 4.7 2.5c1 0 2 .5 2.6 1.2h1.4c.6-.7 1.6-1.2 2.6-1.2 1.8 0 3.2 1.4 3.2 3.2 0 2.2-2.1 4.1-5.4 6.9L8 13.7z" stroke="currentColor" stroke-width="1" fill="none"/>
</svg>
</button>
</div>
<!-- 岗位 meta 信息 -->
<div class="job-detail__meta-row">
<span>{{ job.location }}</span>
<span class="job-detail__meta-dot">·</span>
<span>{{ job.experience }}</span>
<span class="job-detail__meta-dot">·</span>
<span>{{ job.education }}</span>
<span class="job-detail__meta-dot">·</span>
<span>{{ job.salary }}</span>
</div>
<!-- 操作按钮 -->
<div class="job-detail__action-btns">
<button class="job-detail__nova-btn" @click="handleAskNova">问Nova</button>
<button class="job-detail__apply-btn" @click="handleApply">去投递</button>
</div>
</div>
<!-- 内容区域可滚动 -->
<div class="job-detail__body">
<!-- 导航 Tab岗位详情 / 公司概况 -->
<div class="job-detail__nav-tabs">
<div
class="job-detail__nav-tab"
:class="{ 'job-detail__nav-tab--active': activeSection === 'job' }"
@click="activeSection = 'job'"
>岗位详情</div>
<div
class="job-detail__nav-tab"
:class="{ 'job-detail__nav-tab--active': activeSection === 'company' }"
@click="scrollToCompany"
>公司概况</div>
<div class="job-detail__nav-tab-right">
<span class="job-detail__link-btn" @click="handleFeedback">问题反馈</span>
<span class="job-detail__link-btn" @click="handleReport">原链接</span>
</div>
</div>
<!-- 岗位详情内容 -->
<template v-if="activeSection === 'job'">
<!-- 公司 & 职位头部 -->
<div class="job-detail__card">
<div class="job-detail__card-top">
<div class="job-detail__card-left">
<div class="job-detail__company-row">
<div class="job-detail__company-icon">
<img v-if="job.companyLogoUrl" :src="job.companyLogoUrl" :alt="job.company" class="job-detail__company-logo-img" />
<svg v-else viewBox="0 0 24 24" fill="none" class="job-detail__company-svg">
<rect x="3" y="7" width="18" height="14" rx="2" stroke="currentColor" stroke-width="1.5"/>
<path d="M7 7V5a2 2 0 012-2h6a2 2 0 012 2v2" stroke="currentColor" stroke-width="1.5"/>
<path d="M3 13h18" stroke="currentColor" stroke-width="1.5"/>
</svg>
</div>
<span class="job-detail__company-name">{{ job.company }}</span>
</div>
<h3 class="job-detail__job-title">{{ job.title }}</h3>
<div class="job-detail__job-meta">
<span class="job-detail__meta-item">
<svg viewBox="0 0 16 16" fill="none" class="job-detail__meta-icon">
<circle cx="8" cy="6.5" r="2.5" stroke="currentColor" stroke-width="1.2"/>
<path d="M8 14s-5-4-5-7.5a5 5 0 0110 0C13 10 8 14 8 14z" stroke="currentColor" stroke-width="1.2"/>
</svg>
{{ job.location }}
</span>
<span class="job-detail__meta-item">
<svg viewBox="0 0 16 16" fill="none" class="job-detail__meta-icon">
<rect x="2" y="3" width="12" height="11" rx="1.5" stroke="currentColor" stroke-width="1.2"/>
<path d="M2 6.5h12" stroke="currentColor" stroke-width="1.2"/>
</svg>
{{ job.experience }}
</span>
<span class="job-detail__meta-item">
<svg viewBox="0 0 16 16" fill="none" class="job-detail__meta-icon">
<rect x="2" y="2" width="12" height="12" rx="2" stroke="currentColor" stroke-width="1.2"/>
<path d="M5 8h6M8 5v6" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/>
</svg>
{{ job.type }}
</span>
<span v-if="job.education" class="job-detail__meta-item">
<svg viewBox="0 0 16 16" fill="none" class="job-detail__meta-icon">
<path d="M8 2L1 6l7 4 7-4-7-4z" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/>
<path d="M3 7.5v4l5 3 5-3v-4" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/>
</svg>
{{ job.education }}
</span>
<span v-if="job.salary" class="job-detail__meta-item">
<svg viewBox="0 0 16 16" fill="none" class="job-detail__meta-icon">
<path d="M8 1v14M4 4h8M3 8h10M5 12h6" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/>
</svg>
{{ job.salary }}
</span>
</div>
</div>
<!-- 匹配度环 -->
<div class="job-detail__match-area">
<div class="job-detail__match-ring">
<svg viewBox="0 0 80 80" class="job-detail__ring-svg">
<circle cx="40" cy="40" r="34" stroke-width="5" stroke="#E8E8E8" fill="none" opacity="0.3"/>
<circle cx="40" cy="40" r="34" stroke-width="5" fill="none"
stroke="#4FC2C9"
stroke-linecap="round"
:stroke-dasharray="2 * Math.PI * 34"
:stroke-dashoffset="2 * Math.PI * 34 * (1 - job.matchScore / 100)"
transform="rotate(-90 40 40)"
/>
</svg>
<div class="job-detail__match-score">{{ job.matchScore }}%</div>
</div>
<div class="job-detail__match-label">岗位匹配值</div>
<div class="job-detail__match-details">
<div class="job-detail__match-item" v-for="m in matchItems" :key="m.label">
<div class="job-detail__match-mini-ring">
<svg viewBox="0 0 40 40" class="job-detail__mini-ring-svg">
<circle cx="20" cy="20" r="16" stroke-width="3" stroke="#E8E8E8" fill="none" opacity="0.3"/>
<circle cx="20" cy="20" r="16" stroke-width="3" fill="none"
stroke="#BFBFBF"
stroke-linecap="round"
:stroke-dasharray="2 * Math.PI * 16"
:stroke-dashoffset="2 * Math.PI * 16 * (1 - m.score / 100)"
transform="rotate(-90 20 20)"
/>
</svg>
<span class="job-detail__mini-score">{{ m.score }}%</span>
</div>
<span class="job-detail__match-item-label">{{ m.label }}</span>
<!-- 右侧匹配度卡片 -->
<div class="job-detail__match-card">
<div class="job-detail__match-top">
<span class="job-detail__match-score">{{ job.matchScore }}%</span>
<span class="job-detail__match-label">高匹配度</span>
</div>
<div class="job-detail__match-list">
<div class="job-detail__match-row" v-for="m in matchItems" :key="m.label">
<span class="job-detail__match-row-label">{{ m.label }}</span>
<span class="job-detail__match-row-score">{{ m.score }}%</span>
</div>
</div>
</div>
</div>
<!-- 优化简历提示 -->
<!-- 优化简历提示 -->
<div class="job-detail__optimize-bar">
<span>优化简历大幅提升面试成功率</span>
<span class="job-detail__optimize-text">优化简历大幅提升面试成功率</span>
<button class="job-detail__optimize-btn" @click="handleGenerateResume">生成岗位专属简历</button>
</div>
<!-- 岗位描述 -->
<div class="job-detail__card">
<p class="job-detail__desc-text">{{ job.companyInfo.summary }}</p>
<div class="job-detail__tag-list">
<span v-for="tag in job.companyInfo.tags" :key="tag" class="job-detail__tag">{{ tag }}</span>
</div>
</div>
<!-- 岗位职责 -->
<div class="job-detail__card">
<div class="job-detail__section">
<h3 class="job-detail__section-title">岗位职责</h3>
<ol class="job-detail__list">
<li v-for="(item, i) in job.responsibilities" :key="i">{{ item }}</li>
</ol>
<div class="job-detail__section-list">
<div class="job-detail__section-item" v-for="(item, i) in job.responsibilities" :key="i">{{ item }}</div>
</div>
</div>
<!-- 任职要求 -->
<div class="job-detail__card">
<div class="job-detail__section-header">
<div class="job-detail__section">
<h3 class="job-detail__section-title">任职要求</h3>
<span class="job-detail__skill-hint">
<!-- 深色为你拥有的技能-->
</span>
</div>
<!-- 技能标签 -->
<div class="job-detail__skill-tags">
<!-- 先不加点击选中 @click="skill.matched = !skill.matched"-->
<span
v-for="skill in job.requiredSkills"
:key="skill.name"
class="job-detail__skill-tag cursor-po"
class="job-detail__skill-tag"
:class="{ 'job-detail__skill-tag--matched': skill.matched }"
>
{{ skill.name }}
<svg v-if="skill.matched" viewBox="0 0 12 12" fill="none" class="job-detail__skill-close">
<path d="M9 3L3 9M3 3l6 6" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/>
</svg>
</span>
>{{ skill.name }}</span>
</div>
<ol class="job-detail__list">
<li v-for="(item, i) in job.requirements" :key="i">{{ item }}</li>
</ol>
<!-- 要求列表 -->
<div class="job-detail__section-list">
<div class="job-detail__section-item" v-for="(item, i) in job.requirements" :key="i">{{ item }}</div>
</div>
<!-- 加分项 -->
<div class="job-detail__card">
<h3 class="job-detail__section-title">加分项</h3>
<p class="job-detail__desc-text">{{ job.bonus }}</p>
<div v-if="job.bonus" class="job-detail__bonus-item">加分项{{ job.bonus }}</div>
</div>
<!-- 公司概况 -->
<div ref="companySectionRef" class="job-detail__card">
<div class="job-detail__section">
<h3 class="job-detail__section-title">公司概况</h3>
<div class="job-detail__company-info">
<div class="job-detail__company-info-left">
<div class="job-detail__company-header">
<div class="job-detail__company-block">
<!-- 公司 logo + 名称 + 标签 -->
<div class="job-detail__company-head">
<div class="job-detail__company-logo">
<img v-if="job.companyLogoUrl" :src="job.companyLogoUrl" :alt="job.company" class="job-detail__company-logo-img" />
<svg v-else viewBox="0 0 24 24" fill="none" class="job-detail__company-svg">
<rect x="3" y="7" width="18" height="14" rx="2" stroke="currentColor" stroke-width="1.5"/>
<path d="M7 7V5a2 2 0 012-2h6a2 2 0 012 2v2" stroke="currentColor" stroke-width="1.5"/>
<svg v-else viewBox="0 0 32 32" fill="none" class="job-detail__company-svg">
<rect x="4" y="8" width="24" height="18" rx="3" stroke="currentColor" stroke-width="1.5"/>
<path d="M10 8V6a3 3 0 013-3h6a3 3 0 013 3v2" stroke="currentColor" stroke-width="1.5"/>
</svg>
</div>
<span class="job-detail__company-info-name">{{ job.company }}</span>
<div class="job-detail__company-name-info">
<span class="job-detail__company-name">{{ job.company }}</span>
<span class="job-detail__company-sub">{{ job.companyInfo.industryName }}{{ job.companyInfo.size ? ' · ' + job.companyInfo.size : '' }}</span>
</div>
</div>
<!-- 公司描述 -->
<p class="job-detail__company-desc">{{ job.companyInfo.description }}</p>
</div>
<div class="job-detail__company-info-right pr50">
<div class="job-detail__company-meta-item">
<span class="job-detail__meta-label">成立时间</span>
<span>{{ job.companyInfo.founded }}</span>
</div>
<div class="job-detail__company-meta-item">
<span class="job-detail__meta-label">公司地址</span>
<span>{{ job.companyInfo.address }}</span>
</div>
<div class="job-detail__company-meta-item">
<span class="job-detail__meta-label">企业规模</span>
<span>{{ job.companyInfo.size }}</span>
</div>
<div class="job-detail__company-meta-item">
<span class="job-detail__meta-label">官网</span>
<a :href="job.companyInfo.website" target="_blank" class="job-detail__company-link">{{ job.companyInfo.website }}</a>
</div>
</div>
</div>
</div>
<!-- 融资 -->
<div class="job-detail__card">
<h3 class="job-detail__section-title">融资</h3>
<p class="job-detail__desc-text">
<span class="job-detail__funding-label">当前融资阶段</span>{{ job.companyInfo.fundingStage }}
<span class="job-detail__funding-label" style="margin-left: 0.3rem;">最新估值</span>{{ job.companyInfo.valuation }}
</p>
</div>
<!-- 最新动态 -->
<div class="job-detail__card" v-if="job.companyInfo.news.length">
<h3 class="job-detail__section-title">最新动态</h3>
<div class="job-detail__news-list">
<div v-for="(news, i) in job.companyInfo.news" :key="i" class="job-detail__news-item">
<p class="job-detail__news-desc">{{ news }}</p>
</div>
</div>
</div>
</template>
<!-- 公司概况 Tab 已移除点击直接滚动到岗位详情中的公司概况部分 -->
</div>
</div>
</div>
</div>
<AiChat :job-id="jobId" />
<!-- 职位不感兴趣反馈弹窗 -->
<JobDislikeDialog ref="dislikeDialogRef" v-model="showDislikeDialog" :job-id="jobId" />
<!-- 职位问题反馈弹窗 -->
<JobFeedbackDialog ref="feedbackDialogRef" v-model="showFeedbackDialog" :job-id="jobId" />
<AiChat ref="aiChatRef" :job-id="jobId" />
<!-- 岗位专属简历定制弹窗 -->
<JobResumeCustomDialog v-model="showResumeCustomDialog" :job-info="resumeCustomJobInfo" :job-id="jobId" @skip="handleSkipToApply" />
@@ -287,15 +140,13 @@
</template>
<script setup lang="ts">
import { ref, reactive, computed, nextTick, onMounted } from 'vue'
import { ref, reactive, computed, onMounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { useStore } from 'vuex'
import { formatEmploymentType } from '@/stores/index'
import SideNav from '@/components/SideNav.vue'
import AiChat from '@/components/AiChat.vue'
import JobPageHeader from '@/components/JobPageHeader.vue'
import JobDislikeDialog from '@/components/JobDislikeDialog.vue'
import JobFeedbackDialog from '@/components/JobFeedbackDialog.vue'
import JobResumeCustomDialog from '@/components/JobResumeCustomDialog.vue'
import MemberAccessDialog from '@/components/MemberAccessDialog.vue'
import FullscreenLoading from '@/components/FullscreenLoading.vue'
@@ -349,23 +200,11 @@ function splitNumberedText(text: string | undefined): string[] {
// ==================== ====================
/** 当前激活的内容区 Tabjob-岗位详情 / company-公司概况 */
const activeSection = ref<'job' | 'company'>('job')
/** 公司概况卡片 ref,用于滚动定位 */
const companySectionRef = ref<HTMLElement | null>(null)
/** 当前激活的内容区 Tab(保留兼容性但不再使用) */
/** 是否正在加载 */
const loading = ref(false)
/** 点击公司概况 tab 时,确保显示岗位详情内容并滚动到公司概况卡片 */
function scrollToCompany() {
activeSection.value = 'job'
nextTick(() => {
companySectionRef.value?.scrollIntoView({ behavior: 'smooth', block: 'start' })
})
}
// ==================== ====================
/** 匹配度维度列表(岗位匹配环下方的子维度) */
@@ -525,24 +364,8 @@ onMounted(() => {
// ==================== ====================
/** 不感兴趣弹窗状态 */
const showDislikeDialog = ref(false)
const dislikeDialogRef = ref<InstanceType<typeof JobDislikeDialog> | null>(null)
/** 问题反馈弹窗状态 */
const showFeedbackDialog = ref(false)
const feedbackDialogRef = ref<InstanceType<typeof JobFeedbackDialog> | null>(null)
/** 打开不感兴趣弹窗 */
function openDislikeDialog() {
dislikeDialogRef.value?.resetForm()
showDislikeDialog.value = true
}
/** 返回职位列表页 */
function goBack() {
// 使 back() 退 Jobs
// 访 push
if (window.history.length > 1) {
router.back()
} else {
@@ -550,17 +373,12 @@ function goBack() {
}
}
/** 问题反馈 */
function handleFeedback() {
feedbackDialogRef.value?.resetForm()
showFeedbackDialog.value = true
}
/** AiChat 组件引用 */
const aiChatRef = ref<InstanceType<typeof AiChat> | null>(null)
/** 跳转到原链接 */
function handleReport() {
if (job.sourceUrl) {
window.open(job.sourceUrl, '_blank')
}
/** 问 Nova — 触发 AI 聊天,推送提示消息 */
function handleAskNova() {
aiChatRef.value?.triggerAsk()
}
// ==================== ====================
+39 -23
View File
@@ -1,5 +1,5 @@
<template>
<div class="jobs-page dflex">
<div class="jobs-page">
<SideNav />
<div class="jobs-page__content">
@@ -16,25 +16,7 @@
<!-- 页面标题 + Tab 切换 -->
<JobPageHeader v-model:activeTab="activeTab" :favoriteCount="favoriteTotal" :applyCount="applyTotal" />
<!-- 收藏统计栏 -->
<div v-if="activeTab === 'collected'" class="jobs-page__filters-bar">
<div class="jobs-page__fav-stats">
<span class="jobs-page__fav-stats-item">有效{{ favoriteValidCount }}</span>
<span class="jobs-page__fav-stats-item">失效{{ favoriteInvalidCount }}</span>
</div>
</div>
<!-- 投递状态筛选栏 -->
<div v-if="activeTab === 'applied'" class="jobs-page__filters-bar">
<div class="jobs-page__fav-stats">
<span
v-for="tab in applyStatusTabs"
:key="tab.status"
class="jobs-page__fav-stats-item"
:class="{ 'jobs-page__fav-stats-item--active': applyStatusFilter === tab.status }"
@click="switchApplyStatus(tab.status)"
>{{ tab.label }}{{ tab.count }}</span>
</div>
</div>
<!-- 筛选条件 -->
<div v-if="isRecommendTab" class="jobs-page__filters-bar">
<div class="jobs-page__filters">
@@ -47,6 +29,9 @@
:regionCodes="selectedRegionCodes"
:level="2"
:maxSelect="3"
:triggerStyle="selectorTriggerStyle"
:hoverStyle="selectorHoverStyle"
:displayStyle="selectorDisplayStyle"
@update:regionCodes="onRegionChange"
/>
<!-- 岗位筛选使用岗位选择组件 -->
@@ -56,6 +41,9 @@
:maxSelect="3"
:level="3"
:allowParentSelect="true"
:triggerStyle="selectorTriggerStyle"
:hoverStyle="selectorHoverStyle"
:displayStyle="selectorDisplayStyle"
@update:categoryIds="onCategoryChange"
/>
<!-- 行业筛选使用行业选择组件 -->
@@ -65,6 +53,9 @@
:maxSelect="3"
:level="2"
:allowParentSelect="true"
:triggerStyle="selectorTriggerStyle"
:hoverStyle="selectorHoverStyle"
:displayStyle="selectorDisplayStyle"
@update:industryIds="onIndustryChange"
/>
<!-- 其他筛选条件 -->
@@ -112,7 +103,7 @@
</div>
<!-- 职位列表 -->
<div ref="jobListRef" class="jobs-page__list pr5" :style="restoring ? { visibility: 'hidden' } : {}" @scroll="onListScroll">
<div ref="jobListRef" class="jobs-page__list pr5 scrollbar-thin" :style="restoring ? { visibility: 'hidden' } : {}" @scroll="onListScroll">
<div
v-for="(job, index) in jobList"
:key="index"
@@ -151,7 +142,7 @@
</svg>
{{ job.regionName }}
</span>
<span class="jobs-page__meta-edu">本科</span>
<span v-if="formatEducation(job.education)" class="jobs-page__meta-edu">{{ formatEducation(job.education) }}</span>
<span v-if="formatEmploymentType(job.recruitCategory)" class="jobs-page__meta-recruit">{{ formatEmploymentType(job.recruitCategory) }}</span>
</div>
</div>
@@ -267,7 +258,7 @@
import { ref, watch, onMounted, onBeforeUnmount, nextTick, computed } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { useStore } from 'vuex'
import { JOB_TYPE_OPTIONS, formatEmploymentType } from '@/stores/index'
import { JOB_TYPE_OPTIONS, formatEmploymentType, formatEducation } from '@/stores/index'
import SideNav from '@/components/SideNav.vue'
import AiChat from '@/components/AiChat.vue'
import JobPageHeader from '@/components/JobPageHeader.vue'
@@ -340,6 +331,31 @@ function askAssistant(job: JobItem) {
currentAskJobId.value = job.id
}
/** 选择器触发按钮的自定义样式,适配弹窗内布局(参考 ProfileEditDrawer 中 regionTriggerStyle */
const selectorTriggerStyle: Record<string, string> = {
width: '100%',
'box-sizing': 'border-box',
padding: '0.08rem 0.12rem',
'font-size': '0.13rem',
color: '#666',
background: '#fff',
border: '1px solid transparent',
'border-radius': '0.08rem',
'max-width': 'none',
'justify-content': 'space-between',
}
/** 选择器显示文字的自定义样式,覆盖默认 max-width: 1.6rem */
const selectorDisplayStyle: Record<string, string> = {
'max-width': 'none',
}
/** 选择器触发按钮 hover 时的样式 */
const selectorHoverStyle: Record<string, string> = {
background: '#12C7BE',
color: '#fff',
}
// ==================== ====================
/** 收藏总数(用于 Tab 标签显示) */
+4 -1
View File
@@ -100,7 +100,10 @@
<button
class="mentor-page__detail-btn"
@click="goToDetail(mentor)"
>查看详情 </button>
><span>查看详情</span> <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.51612 4.07575C7.38279 3.94242 7.31879 3.78686 7.32412 3.60908C7.32945 3.43131 7.39901 3.27575 7.53278 3.14241C7.66612 3.02019 7.82167 2.95619 7.99946 2.95041C8.17723 2.94464 8.33278 3.00864 8.46612 3.14241L12.8661 7.54241C12.9328 7.60908 12.9801 7.6813 13.0081 7.75908C13.0361 7.83686 13.0497 7.92019 13.0488 8.00908C13.0479 8.09797 13.0339 8.18131 13.0068 8.25908C12.9797 8.33686 12.9326 8.40908 12.8654 8.47575L8.46545 12.8757C8.34323 12.998 8.19034 13.0591 8.00679 13.0591C7.82323 13.0591 7.66501 12.998 7.53212 12.8757C7.39879 12.7424 7.33212 12.584 7.33212 12.4004C7.33212 12.2169 7.39879 12.0586 7.53212 11.9257L10.7828 8.67575H3.33279C3.1439 8.67575 2.98546 8.61175 2.85745 8.48375C2.72945 8.35575 2.66567 8.19753 2.66612 8.00908C2.66656 7.82064 2.73056 7.6622 2.85812 7.53375C2.98568 7.40531 3.1439 7.34153 3.33279 7.34242L10.7828 7.34242L7.51612 4.07575Z" fill="#52CAD1"/>
</svg>
</button>
</div>
</div>
+2
View File
@@ -426,12 +426,14 @@ async function handleSaveEdit(data: Record<string, any>) {
mobileNumber: data.phone,
regionCode: data.location,
wechatNumber: data.wechat,
portfolioUrl: data.portfolioUrl,
})
//
profile.value.name = data.name
profile.value.email = data.email
profile.value.phone = data.phone
profile.value.wechat = data.wechat
profile.value.portfolioUrl = data.portfolioUrl || ''
/** 将城市编码存入 regionCode,先直接显示编码,后续联调获取接口再做名称转换 */
profile.value.regionCode = data.location || ''
ElMessage.success('个人信息保存成功')
+4 -4
View File
@@ -27,8 +27,8 @@ export default defineConfig({
host: '0.0.0.0',
proxy: {
'/api': {
target: 'https://test.offerpai.com.cn/',
// target: 'http://127.0.0.1:8080',
// target: 'https://test.offerpai.com.cn/',
target: 'http://127.0.0.1:8080',
changeOrigin: true,
},
'/jianshi-api': {
@@ -37,8 +37,8 @@ export default defineConfig({
rewrite: (path) => path.replace(/^\/jianshi-api/, '/proxy-api'),
},
'/ai-api': {
target: 'https://test.offerpai.com.cn/',
// target: 'http://127.0.0.1:8000',
// target: 'https://test.offerpai.com.cn/',
target: 'http://127.0.0.1:8000',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/ai-api/, ''),
},