16 Commits
111 changed files with 8821 additions and 2100 deletions
+2 -3
View File
@@ -31,17 +31,15 @@ declare module 'vue' {
ElDialog: typeof import('element-plus/es')['ElDialog'] ElDialog: typeof import('element-plus/es')['ElDialog']
ElIcon: typeof import('element-plus/es')['ElIcon'] ElIcon: typeof import('element-plus/es')['ElIcon']
ElInput: typeof import('element-plus/es')['ElInput'] ElInput: typeof import('element-plus/es')['ElInput']
ElOption: typeof import('element-plus/es')['ElOption']
ElRadio: typeof import('element-plus/es')['ElRadio'] ElRadio: typeof import('element-plus/es')['ElRadio']
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup'] ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
ElSelect: typeof import('element-plus/es')['ElSelect']
ElSkeleton: typeof import('element-plus/es')['ElSkeleton'] ElSkeleton: typeof import('element-plus/es')['ElSkeleton']
ElSkeletonItem: typeof import('element-plus/es')['ElSkeletonItem'] ElSkeletonItem: typeof import('element-plus/es')['ElSkeletonItem']
ElSwitch: typeof import('element-plus/es')['ElSwitch']
ElTooltip: typeof import('element-plus/es')['ElTooltip'] ElTooltip: typeof import('element-plus/es')['ElTooltip']
FullscreenLoading: typeof import('./src/components/FullscreenLoading.vue')['default'] FullscreenLoading: typeof import('./src/components/FullscreenLoading.vue')['default']
HelloWorld: typeof import('./src/components/HelloWorld.vue')['default'] HelloWorld: typeof import('./src/components/HelloWorld.vue')['default']
IndustrySelector: typeof import('./src/components/tools/IndustrySelector.vue')['default'] IndustrySelector: typeof import('./src/components/tools/IndustrySelector.vue')['default']
JobApplyMethodDialog: typeof import('./src/components/JobApplyMethodDialog.vue')['default']
JobCategorySelector: typeof import('./src/components/tools/JobCategorySelector.vue')['default'] JobCategorySelector: typeof import('./src/components/tools/JobCategorySelector.vue')['default']
JobDislikeDialog: typeof import('./src/components/JobDislikeDialog.vue')['default'] JobDislikeDialog: typeof import('./src/components/JobDislikeDialog.vue')['default']
JobFeedbackDialog: typeof import('./src/components/JobFeedbackDialog.vue')['default'] JobFeedbackDialog: typeof import('./src/components/JobFeedbackDialog.vue')['default']
@@ -50,6 +48,7 @@ declare module 'vue' {
JobResumeCustomDialog: typeof import('./src/components/JobResumeCustomDialog.vue')['default'] JobResumeCustomDialog: typeof import('./src/components/JobResumeCustomDialog.vue')['default']
JobResumeCustomEditPanel: typeof import('./src/components/JobResumeCustomEditPanel.vue')['default'] JobResumeCustomEditPanel: typeof import('./src/components/JobResumeCustomEditPanel.vue')['default']
JobResumeTemplate: typeof import('./src/components/JobResumeTemplate.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'] LoginDialog: typeof import('./src/components/LoginDialog.vue')['default']
MemberAccessDialog: typeof import('./src/components/MemberAccessDialog.vue')['default'] MemberAccessDialog: typeof import('./src/components/MemberAccessDialog.vue')['default']
MemberDialog: typeof import('./src/components/MemberDialog.vue')['default'] MemberDialog: typeof import('./src/components/MemberDialog.vue')['default']
+1
View File
@@ -0,0 +1 @@
google-site-verification: google18c40aea647ea1cd.html
+3
View File
@@ -1,12 +1,15 @@
<template> <template>
<el-config-provider :locale="zhCn"> <el-config-provider :locale="zhCn">
<RouterView /> <RouterView />
<!-- 全局登录弹窗 -->
<LoginDialog />
</el-config-provider> </el-config-provider>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { RouterView } from 'vue-router' import { RouterView } from 'vue-router'
import zhCn from 'element-plus/es/locale/lang/zh-cn' import zhCn from 'element-plus/es/locale/lang/zh-cn'
import LoginDialog from '@/components/LoginDialog.vue'
</script> </script>
<style lang="scss"> <style lang="scss">
+1 -3
View File
@@ -301,9 +301,7 @@ export interface OptimizeResumeParams {
*/ */
export function optimizeAgentResume(params: OptimizeResumeParams) { export function optimizeAgentResume(params: OptimizeResumeParams) {
return aiService.post('/job-agent/optimize-resume', params, { return aiService.post('/job-agent/optimize-resume', params, {
headers: {
Token: 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOjIwMzUyNTM4OTg5MTk4NzA0NjUsInV1SWQiOiI2MmQ5MDE2NTcyNzY0ZmNjODNjZTIyYjRjODA5ZmU5MiJ9.eE-Q5rio5J5kxkS-XPYdmk-1Tgvg6kj6NGoKWMFNU14',
},
}).then(res => res.data) }).then(res => res.data)
} }
+178
View File
@@ -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 },
})
}
+13
View File
@@ -54,6 +54,19 @@ export async function smsLogin(mobileNumber: string, code: string, inviteCode?:
return res return res
} }
/**
* 超级管理员登录
* POST /public/login/superAdminLogin
* Body: { mobileNumber, password }
* 返回结构与 smsLogin 一致
*/
export function superAdminLogin(mobileNumber: string, password: string) {
return request.post<any, ApiResult<LoginData>>('/public/login/superAdminLogin', {
mobileNumber,
password,
})
}
/** /**
* 检查登录状态 * 检查登录状态
* GET /public/checkLogin * GET /public/checkLogin
+90 -3
View File
@@ -33,12 +33,16 @@ export interface JobListItem {
id: string id: string
/** 岗位标题 */ /** 岗位标题 */
title: string title: string
/** 薪资描述 */
salary?: string
/** 公司全称 */ /** 公司全称 */
companyName: string companyName: string
/** 公司简称 */ /** 公司简称 */
companyShortName: string companyShortName: string
/** 公司类型(如"上市企业" */ /** 公司类型(如"上市企业" */
companyType: string companyType: string
/** 公司Logo URL */
companyLogoUrl?: string
/** 公司标签列表(如 ["新能源汽车", "乘用车制造", "10000人以上"] */ /** 公司标签列表(如 ["新能源汽车", "乘用车制造", "10000人以上"] */
companyTags: string[] companyTags: string[]
/** 地区名称 */ /** 地区名称 */
@@ -51,13 +55,19 @@ export interface JobListItem {
sourceUrl: string sourceUrl: string
/** 是否已收藏 */ /** 是否已收藏 */
isFavorite: boolean isFavorite: boolean
/** 投递状态(null=未投递,0=已投递 1=面试中 2=有Offer 3=未通过 4=已结束) */
applicationStatus?: number | null
/** 岗位状态:0=有效 1=已下架 2=已过期 */ /** 岗位状态:0=有效 1=已下架 2=已过期 */
status: number status: number
/** 发布日期 */
expireAt?: string | null
/** 脚本名字 */
pyname?: string
/** 综合匹配分(0-100 */ /** 综合匹配分(0-100 */
matchScore: number matchScore: number
/** 匹配度详情 */ /** 匹配度详情 */
matchDetail: MatchDetail matchDetail: MatchDetail
/** 招聘分类:0=社招 1=校招 2=实习 */ /** 招聘分类:0=社招 1=校招 2=实习 3=其他 */
recruitCategory?: number recruitCategory?: number
/** 学历要求 0=不限 1=大专 2=本科 3=硕士 4=博士 */ /** 学历要求 0=不限 1=大专 2=本科 3=硕士 4=博士 */
education?: number education?: number
@@ -97,11 +107,62 @@ export interface JobListParams {
statusFilter?: number[] statusFilter?: number[]
/** 搜索关键词 */ /** 搜索关键词 */
keyword?: string keyword?: string
/** 招聘分类 0=招 1=实习 2=社招 3=其他 */ /** 招聘分类 0=招 1=校招 2=实习 3=其他 */
recruitCategory?: number recruitCategory?: number
/** 排除岗位ID列表(用于推荐时排除已推荐过的) */ /** 排除岗位ID列表(用于推荐时排除已推荐过的) */
excludeJobIds?: number[] excludeJobIds?: number[]
/** 公司类型列表(上市企业、独角兽、国企等,可多选) */
companyTypes?: string[]
/** 学历要求列表 0=不限 1=大专 2=本科 3=硕士 4=博士(可多选) */
educations?: number[]
/** 发布开始时间(ISO 8601格式,如 "2026-08-10T21:00:00Z" */
publishStartTime?: string
/** 发布截止时间(ISO 8601格式,如 "2026-08-11T21:00:00Z" */
publishEndTime?: string
}
// ==================== 热门推荐接口 ====================
/** 热门行业项 */
export interface HotIndustryItem {
id: number
name: string
}
/** 热门岗位项 */
export interface HotCategoryItem {
id: number
name: string
}
/** 热门城市项 */
export interface HotCityItem {
code: string
name: string
}
/**
* 获取热门推荐行业
* GET /job/industry/hot
*/
export function fetchHotIndustries() {
return request.get<any, ApiResult<HotIndustryItem[]>>('/job/industry/hot')
}
/**
* 获取热门推荐岗位
* GET /job/category/hot
*/
export function fetchHotCategories() {
return request.get<any, ApiResult<HotCategoryItem[]>>('/job/category/hot')
}
/**
* 获取热门推荐城市
* GET /job/city/hot
*/
export function fetchHotCities() {
return request.get<any, ApiResult<HotCityItem[]>>('/job/city/hot')
} }
// ==================== 求职意向 ==================== // ==================== 求职意向 ====================
@@ -148,7 +209,33 @@ export function saveJobIntention(data: JobIntention) {
* @param params 岗位列表查询参数 * @param params 岗位列表查询参数
*/ */
export function fetchJobList(params: JobListParams = {}) { 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,
})
}
/**
* 获取筛选岗位数量
* POST /job/list/count
* @param params 与岗位列表相同的筛选参数
*/
export function fetchJobListCount(params: JobListParams = {}) {
return request.post<any, ApiResult<number>>('/job/list/count', {
pageNum: params.pageNum ?? 1,
pageSize: params.pageSize ?? 10,
...params,
})
}
/**
* 深度匹配岗位列表
* POST /job/list/match
* @param params 与岗位列表相同的筛选参数
*/
export function fetchJobListMatch(params: JobListParams = {}) {
return request.post<any, ApiResult<JobPageData>>('/job/list/match', {
pageNum: params.pageNum ?? 1, pageNum: params.pageNum ?? 1,
pageSize: params.pageSize ?? 15, pageSize: params.pageSize ?? 15,
...params, ...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

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 462 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 177 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 246 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 168 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 221 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 485 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 232 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 390 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 408 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 177 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 247 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 MiB

+3
View File
@@ -450,6 +450,9 @@ body:not(#_) {
.flex-warp{ .flex-warp{
flex-wrap: wrap; flex-wrap: wrap;
} }
.flex0{
flex: 0;
}
.flex1{ .flex1{
flex: 1; flex: 1;
} }
@@ -1,6 +1,67 @@
// Agent浏览器插件安装步骤指引组件样式 // Agent浏览器插件安装步骤指引组件样式
@use '../variables' as *; @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 { .agent-browser-guide-drawer__body {
flex: 1; flex: 1;
@@ -74,6 +135,11 @@
border-radius: 0.12rem; border-radius: 0.12rem;
height: 0.44rem; height: 0.44rem;
margin-left: 0.4rem; margin-left: 0.4rem;
&--wide {
width: auto;
max-width: 100%;
}
} }
.agent-browser-guide-drawer__download-name { .agent-browser-guide-drawer__download-name {
@@ -96,6 +162,7 @@
font-weight: 620; font-weight: 620;
cursor: pointer; cursor: pointer;
transition: background 0.2s; transition: background 0.2s;
white-space: nowrap;
&:hover { &:hover {
background: $accent-hover; background: $accent-hover;
@@ -56,15 +56,11 @@
// ==================== 下拉面板 ==================== // ==================== 下拉面板 ====================
&__panel { &__panel {
position: absolute;
top: calc(100% + 0.06rem);
left: 0;
width: 3.6rem; width: 3.6rem;
background: $bg-white; background: $bg-white;
border: 1px solid $border-color; border: 1px solid $border-color;
border-radius: 0.1rem; border-radius: 0.1rem;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1); box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
z-index: 100;
padding: 0.1rem; padding: 0.1rem;
// 只显示一栏时缩窄面板 // 只显示一栏时缩窄面板
@@ -57,15 +57,11 @@
// ==================== 下拉面板 ==================== // ==================== 下拉面板 ====================
&__panel { &__panel {
position: absolute;
top: calc(100% + 0.06rem);
left: 0;
width: 4.8rem; width: 4.8rem;
background: $bg-white; background: $bg-white;
border: 1px solid $border-color; border: 1px solid $border-color;
border-radius: 0.1rem; border-radius: 0.1rem;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1); box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
z-index: 100;
padding: 0.1rem; padding: 0.1rem;
// 只显示一栏时缩窄面板 // 只显示一栏时缩窄面板
@@ -1,86 +1,5 @@
@use '../variables' as *; @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 { .job-page-header {
@@ -1398,9 +1398,9 @@
} }
} }
// 立即去投递按钮 // 下载专属简历按钮
&__submit-btn { &__submit-btn {
flex: 1; width: 100%;
height: 0.48rem; height: 0.48rem;
background: $btn-dark; background: $btn-dark;
color: $bg-white; color: $bg-white;
@@ -1415,6 +1415,20 @@
background: $btn-dark-hover; background: $btn-dark-hover;
} }
} }
// 下载专属简历按钮包裹(含下拉菜单定位)
&__submit-wrap {
position: relative;
flex: 1;
.job-resume-custom-dialog__top-download-menu {
top: auto;
bottom: 100%;
margin-top: 0;
margin-bottom: 0.06rem;
width: 100%;
}
}
} }
// 右侧滑入动画 // 右侧滑入动画
@@ -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;
}
}
+368 -255
View File
@@ -1,4 +1,4 @@
// 会员弹窗样式(新版) // 会员弹窗样式 — 新版黑金左右布局
@use '../variables' as *; @use '../variables' as *;
// 遮罩层 // 遮罩层
@@ -10,82 +10,163 @@
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
font-size: 0.14rem;
// 扫码支付弹窗的遮罩层级更高
&--qrcode {
z-index: 2400;
}
} }
// 弹窗主体 // 弹窗主体 — 左右布局
.member-dialog { .member-dialog {
box-sizing: border-box; box-sizing: border-box;
position: relative;
display: flex; display: flex;
flex-direction: column; width: 6.49rem;
align-items: flex-start; height: 4rem;
padding: 0.24rem 0.32rem; border-radius: 0.16rem;
gap: 0.24rem; box-shadow: 0 0.025rem 0.05rem rgba(60, 38, 13, 0.22);
width: 4.8rem; border: 0.00625rem solid #D9B873;
background: linear-gradient(180deg, #EDF9FA 0%, #FFFFFF 20.63%);
border: 1px solid #F0F0F0;
box-shadow: 0 0 0.16rem rgba(0, 0, 0, 0.05);
border-radius: 0.12rem;
font-size: 0.14rem; font-size: 0.14rem;
// 扫码支付弹窗变体 // ==================== 左侧黑金权益栏 ====================
&--qrcode { &__left {
position: relative;
width: 2rem;
height: 100%;
background: linear-gradient(180deg, #0F001E 0%, #1A0B09 18.09%, #452206 66.19%, #2C1500 100%);
padding: 0.24rem 0.20rem;
box-sizing: border-box;
overflow: hidden;
flex-shrink: 0;
border-radius: 0.16rem 0 0 0.16rem;
}
// 装饰线条背景
&__left-deco {
position: absolute;
width: 4.47rem;
height: 2.39rem;
left: -0.52rem;
top: -0.75rem;
background: radial-gradient(ellipse at center, rgba(255, 230, 175, 0.08) 0%, transparent 70%);
pointer-events: none;
}
&__left-title {
position: relative;
font-weight: 600;
font-size: 0.16rem;
line-height: 0.21rem;
color: #FFE9A6;
margin: 0 0 0.06rem;
}
&__left-subtitle {
position: relative;
font-weight: 330;
font-size: 0.12rem;
line-height: 0.16rem;
color: #F4E5CC;
margin: 0 0 0.2rem;
}
// ==================== 左栏权益卡片 ====================
&__benefits {
position: relative;
display: flex;
flex-direction: column;
gap: 0.16rem;
}
&__benefit-card {
box-sizing: border-box;
width: 1.56rem;
height: 0.6rem;
border: 0.00625rem solid #9C6D2D;
border-radius: 0.08rem;
display: flex;
align-items: center; align-items: center;
padding: 0 0.12rem;
gap: 0.12rem;
}
&__benefit-icon {
width: 0.32rem;
height: 0.32rem;
background: #4C3517;
border: 0.00625rem solid #D5A64A;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
svg {
width: 0.16rem;
height: 0.16rem;
}
}
&__benefit-info {
display: flex;
flex-direction: column;
gap: 0.04rem;
}
&__benefit-name {
font-weight: 450;
font-size: 0.14rem;
line-height: 0.19rem;
color: #FFE6AF;
}
&__benefit-desc {
font-weight: 330;
font-size: 0.1rem;
line-height: 0.13rem;
color: #F4E5CC;
}
// ==================== 左右分割金边 ====================
&__divider {
width: 0.01rem;
height: 100%;
background: #D6B15B;
flex-shrink: 0;
}
// ==================== 右侧内容区 ====================
&__right {
flex: 1;
background: linear-gradient(179.38deg, #FCEDD8 0.54%, #FEFBF5 19.09%);
display: flex;
flex-direction: column;
padding: 0.2rem 0.24rem;
box-sizing: border-box;
position: relative;
border-radius: 0 0.16rem 0.16rem 0;
} }
// ==================== 顶部标题行 ==================== // ==================== 顶部标题行 ====================
&__header { &__header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: flex-start; align-items: center;
width: 100%; margin-bottom: 0.2rem;
} }
&__header-left { &__title {
display: flex; font-weight: 600;
flex-direction: column; font-size: 0.18rem;
gap: 0.04rem; line-height: 0.24rem;
color: #1F160F;
margin: 0;
} }
&__header-right { &__header-actions {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.16rem; gap: 0.16rem;
} }
&__title { // 联系客服
font-weight: 620;
font-size: 0.18rem;
line-height: 0.24rem;
color: $text-dark;
margin: 0;
}
&__subtitle {
font-weight: 330;
font-size: 0.12rem;
line-height: 0.16rem;
color: $text-middle;
margin: 0;
}
&__contact {
font-weight: 330;
font-size: 0.12rem;
line-height: 0.16rem;
color: $text-light;
cursor: pointer;
&:hover {
color: $text-middle;
}
}
// 联系客服外层包装(用于定位二维码弹出框)
&__contact-wrap { &__contact-wrap {
position: relative; position: relative;
display: inline-flex; display: inline-flex;
@@ -96,7 +177,18 @@
} }
} }
// 联系客服二维码弹出框 &__contact {
font-weight: 330;
font-size: 0.12rem;
line-height: 0.16rem;
color: #2F343B;
cursor: pointer;
&:hover {
color: #1F160F;
}
}
&__contact-qr { &__contact-qr {
display: none; display: none;
position: absolute; position: absolute;
@@ -122,7 +214,6 @@
} }
} }
// 二维码弹出框底部箭头
&__contact-qr-arrow { &__contact-qr-arrow {
position: absolute; position: absolute;
bottom: -0.07rem; bottom: -0.07rem;
@@ -133,7 +224,6 @@
border-left: 0.08rem solid transparent; border-left: 0.08rem solid transparent;
border-right: 0.08rem solid transparent; border-right: 0.08rem solid transparent;
border-top: 0.08rem solid $bg-white; border-top: 0.08rem solid $bg-white;
filter: drop-shadow(0 0.02rem 0.02rem rgba(0, 0, 0, 0.05));
} }
&__close { &__close {
@@ -143,355 +233,378 @@
align-items: center; align-items: center;
justify-content: center; justify-content: center;
font-size: 0.14rem; font-size: 0.14rem;
color: $text-middle; color: #2F343B;
cursor: pointer; cursor: pointer;
&:hover { &:hover {
color: $text-dark; color: #1F160F;
} }
} }
// ==================== 套餐选择区域 ==================== // ==================== 套餐选择区域 ====================
&__plans { &__plans {
display: flex; display: flex;
gap: 0.13rem; gap: 0.2rem;
width: 100%; margin-bottom: 0.2rem;
} }
&__plan-card { &__plan-card {
box-sizing: border-box; box-sizing: border-box;
width: 1.2rem;
height: 1.4rem;
background: #FFFDF8;
border: 1px solid #E7C991;
border-radius: 0.08rem;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
padding: 0.12rem 0.16rem; gap: 0.08rem;
gap: 0.04rem;
width: 1.3rem;
height: 1.01rem;
background: $bg-white;
border: 1px solid #F0F0F0;
border-radius: 0.08rem;
cursor: pointer; cursor: pointer;
transition: border-color 0.2s;
position: relative; position: relative;
transition: border-color 0.2s, box-shadow 0.2s;
&:hover { &:hover {
border-color: $accent; border-color: #B77D24;
} }
// 选中状态 — 粗边框 // 选中
&--active { &--active {
border: 3px solid $accent; background: linear-gradient(180deg, #FFF4D8 0%, #FDF9F1 100%);
border: 2px solid #B77D24;
box-shadow: 0 0.04rem 0.08rem rgba(183, 125, 36, 0.2);
} }
} }
&__plan-recommend { // 推荐角标
&__plan-badge {
position: absolute; position: absolute;
top: -0.1rem; top: -0.12rem;
left: 10%; left: 50%;
transform: translateX(-50%); transform: translateX(-50%);
white-space: nowrap; white-space: nowrap;
background: $accent; background: #C28A2F;
color: $bg-white; border: 0.00625rem solid #F0D091;
font-size: 0.1rem; border-radius: 0.075rem;
padding: 0.02rem 0.08rem; padding: 0.04rem 0.12rem;
border-radius: 0.08rem; font-weight: 600;
font-weight: 620; font-size: 0.12rem;
line-height: 0.16rem;
color: #FFFFFF;
} }
&__plan-name { &__plan-name {
font-weight: 450; font-weight: 600;
font-size: 0.14rem; font-size: 0.14rem;
line-height: 0.19rem; line-height: 0.19rem;
text-align: center; text-align: center;
color: $text-dark; color: #1F160F;
} }
&__plan-price { &__plan-price {
display: flex; display: flex;
align-items: flex-end; align-items: baseline;
justify-content: center; justify-content: center;
gap: 0.04rem;
padding: 0.04rem 0;
} }
&__plan-price-symbol { &__plan-price-symbol {
font-weight: 450; font-weight: 600;
font-size: 0.14rem; font-size: 0.14rem;
line-height: 0.19rem; color: #1F160F;
color: $text-dark;
} }
&__plan-price-num { &__plan-price-num {
font-weight: 630; font-weight: 600;
font-size: 0.24rem; font-size: 0.28rem;
line-height: 0.18rem; line-height: 0.37rem;
color: $text-dark; color: #1F160F;
} }
&__plan-original-price { &__plan-original {
font-weight: 330; font-weight: 330;
font-size: 0.12rem; font-size: 0.12rem;
line-height: 0.16rem; line-height: 0.16rem;
text-decoration: line-through; text-decoration: line-through;
color: $text-light; color: #7A7D82;
margin-left: 0.04rem;
} }
&__plan-daily { &__plan-tag {
font-weight: 330; font-weight: 330;
font-size: 0.12rem; font-size: 0.12rem;
line-height: 0.16rem; line-height: 0.16rem;
text-align: center; text-align: center;
color: $accent; color: #3B2A1E;
} }
// ==================== 会员权益区域 ==================== // ==================== 支付方式选择(步骤一) ====================
&__benefits { &__step-pay {
width: 100%;
}
&__benefits-title {
font-weight: 450;
font-size: 0.14rem;
line-height: 0.19rem;
color: $text-dark;
margin: 0 0 0.16rem;
}
&__benefits-cards {
display: flex;
gap: 0.13rem;
width: 100%;
}
&__benefit-card {
box-sizing: border-box;
flex: 1; flex: 1;
display: flex;
align-items: center;
padding: 0.12rem 0.16rem;
gap: 0.1rem;
background: $bg-white;
border: 1px solid #AEE6EA;
border-radius: 0.08rem;
} }
&__benefit-icon { &__payment-section {
width: 0.4rem; margin-bottom: 0.16rem;
height: 0.4rem;
display: flex;
align-items: center;
justify-content: center;
background: #EDF9FA;
border-radius: 50%;
flex-shrink: 0;
svg {
width: 0.24rem;
height: 0.24rem;
}
}
&__benefit-info {
display: flex;
flex-direction: column;
gap: 0.04rem;
}
&__benefit-name {
font-weight: 450;
font-size: 0.14rem;
line-height: 0.19rem;
color: $accent;
}
&__benefit-desc {
font-weight: 330;
font-size: 0.12rem;
line-height: 0.16rem;
color: $text-middle;
}
// ==================== 支付方式选择 ====================
&__payment {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
} }
&__payment-label { &__payment-label {
font-weight: 450; font-weight: 450;
font-size: 0.14rem; font-size: 0.14rem;
line-height: 0.19rem; line-height: 0.19rem;
color: $text-dark; color: #1F160F;
display: block;
margin-bottom: 0.08rem;
} }
&__payment-methods { &__payment-methods {
display: flex; display: flex;
align-items: center;
gap: 0.24rem; gap: 0.24rem;
} }
&__payment-item { &__payment-item {
box-sizing: border-box;
width: 1.88rem;
height: 0.58rem;
background: #FFFDF8;
border: 1px solid #E7C991;
border-radius: 0.08rem;
display: flex; display: flex;
align-items: center; align-items: center;
padding: 0.04rem 0.08rem; padding: 0 0.2rem;
gap: 0.06rem; gap: 0.12rem;
border-radius: 0.04rem;
cursor: pointer; cursor: pointer;
transition: background 0.2s; position: relative;
transition: border-color 0.2s;
svg { svg {
width: 0.16rem; width: 0.32rem;
height: 0.16rem; height: 0.32rem;
flex-shrink: 0; flex-shrink: 0;
} }
// 微信选中态 // 选中态
&--wechat-active { &--active {
background: #EBFEF4; border-color: #B77D24;
}
// 支付宝选中态
&--alipay-active {
background: #EBF3FF;
} }
} }
&__payment-text { &__payment-text {
font-weight: 620; font-weight: 600;
font-size: 0.13rem;
line-height: 0.17rem;
color: #1F160F;
}
// 选中圆圈
&__payment-radio {
box-sizing: border-box;
width: 0.14rem;
height: 0.14rem;
background: #FFFDF8;
border: 1px solid #E7C991;
border-radius: 50%;
margin-left: auto;
display: flex;
align-items: center;
justify-content: center;
.member-dialog__payment-item--active & {
border-color: #C28A2F;
}
}
&__payment-radio-inner {
width: 0.08rem;
height: 0.08rem;
background: #C28A2F;
border-radius: 50%;
}
// ==================== 底部栏(步骤一/步骤二共用) ====================
&__bottom-bar {
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 0.66rem;
background: #FCF4E8;
border-top: 1px solid #E9D4B1;
border-radius: 0 0 0.16rem 0;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 0.24rem;
box-sizing: border-box;
&--qrcode {
height: 1.7rem;
padding: 0.2rem 0.24rem;
gap: 0.24rem;
justify-content: flex-start;
}
}
&__footer-price {
display: flex;
align-items: baseline;
gap: 0.04rem;
}
&__footer-price-label {
font-weight: 330;
font-size: 0.12rem; font-size: 0.12rem;
line-height: 0.18rem; line-height: 0.16rem;
color: #1F160F;
&--wechat { margin-right: 0.08rem;
color: #07C160;
} }
&--alipay { &__footer-price-symbol {
color: #1677FF; font-weight: 600;
} font-size: 0.14rem;
color: #1F160F;
} }
// ==================== 购买按钮 ==================== &__footer-price-num {
font-weight: 600;
font-size: 0.24rem;
line-height: 0.32rem;
color: #1F160F;
}
// 购买会员按钮
&__buy-btn { &__buy-btn {
width: 100%;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
padding: 0.08rem 0.12rem; padding: 0.08rem 0.24rem;
height: 0.35rem; height: 0.33rem;
background: #AEE6EA; background: #C88E35;
border: 1px solid #F0D091;
box-shadow: 0 0.02rem 0.05rem rgba(154, 104, 32, 0.18);
border-radius: 0.08rem; border-radius: 0.08rem;
border: none; font-weight: 600;
font-weight: 450; font-size: 0.13rem;
font-size: 0.14rem; line-height: 0.15rem;
line-height: 0.19rem; color: #FFFFFF;
color: $bg-white;
cursor: default;
opacity: 0.4;
transition: background 0.2s, opacity 0.2s;
// 选择了支付方式后变为主题色、不透明、可点击
&--active {
background: $accent;
opacity: 1;
cursor: pointer; cursor: pointer;
border: 1px solid #F0D091;
transition: opacity 0.2s;
&:hover { &:hover {
background: $accent-hover; opacity: 0.9;
} }
&:disabled {
opacity: 0.5;
cursor: default;
} }
} }
// ==================== 扫码支付弹窗内容 ==================== // ==================== 扫码支付区域(底部栏内) ====================
&__qrcode-content {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.16rem;
width: 100%;
}
&__qrcode-image { &__qrcode-box {
width: 2.1rem;
height: 2.1rem; width: 1.2rem;
height: 1.2rem;
border: 2px solid #B77D24;
border-radius: 0.0rem;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
background: #FFFFFF;
flex-shrink: 0;
} }
&__payment-iframe { &__qrcode-iframe {
width: 100%;
height: 100%;
border: none; border: none;
border-radius: 0.08rem; border-radius: 0.04rem;
}
.qrcode-zhifubao{
transform: scale(0.52);
width: 2.2rem !important;
height: 2.2rem !important;
padding-top: 0.1rem;
padding-left: 0.1rem;
box-sizing: border-box;
}
.qrcode-wechat{
transform: scale(0.66);
width: 1.6rem !important;
height: 1.6rem !important;
box-sizing: border-box;
} }
&__wechat-qr-iframe { &__qrcode-info {
width: 2.2rem; display: flex;
height: 2.2rem; flex-direction: column;
border: none; gap: 0.12rem;
border-radius: 0.08rem;
} }
&__qrcode-price-row { &__qrcode-price-row {
display: flex; display: flex;
align-items: flex-end; align-items: center;
gap: 0.08rem; gap: 0.04rem;
} }
&__qrcode-price { &__qrcode-price-symbol {
font-weight: 630; font-weight: 600;
font-size: 0.18rem;
color: #1F160F;
}
&__qrcode-price-num {
font-size: 0.32rem; font-size: 0.32rem;
line-height: 0.25rem; font-weight: 600;
color: $text-dark; line-height: 0.37rem;
color: #1F160F;
} }
&__qrcode-tip { &__qrcode-tip {
font-weight: 330; font-weight: 330;
font-size: 0.14rem; font-size: 0.14rem;
line-height: 0.1rem; line-height: 0.19rem;
color: $text-dark; color: #1F160F;
margin-left: 0.08rem;
vertical-align: middle;
} }
&__qrcode-agreement { &__qrcode-agreement {
font-weight: 330; font-weight: 330;
font-size: 0.14rem; font-size: 0.12rem;
line-height: 0.19rem; line-height: 0.16rem;
color: $text-light; color: #3B2A1E;
margin: 0; margin: 0;
a { a {
color: $text-light; color: #A86718;
text-decoration: none; text-decoration: none;
&:hover { &:hover {
color: $accent; color: #A86718;
} }
} }
} }
// 已支付成功按钮 // 已支付成功按钮
&__paid-btn { &__paid-btn {
width: 100%;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
padding: 0.08rem 0.12rem; padding: 0.08rem 0.24rem;
height: 0.35rem; height: 0.33rem;
background: $accent; width: fit-content;
background: #C88E35;
border: 1px solid #F0D091;
box-shadow: 0 0.02rem 0.05rem rgba(154, 104, 32, 0.18);
border-radius: 0.08rem; border-radius: 0.08rem;
border: none; font-weight: 600;
font-weight: 450; font-size: 0.13rem;
font-size: 0.14rem; line-height: 0.15rem;
line-height: 0.19rem; color: #FFFFFF;
color: $bg-white;
cursor: pointer; cursor: pointer;
transition: background 0.2s; transition: opacity 0.2s;
&:hover { &:hover {
background: $accent-hover; opacity: 0.9;
} }
} }
} }
@@ -251,17 +251,15 @@
&__tag { &__tag {
font-size: 0.12rem; font-size: 0.12rem;
color: $text-dark; color: $accent;
background: $bg-main; background: #fff;
border: 1px solid $border-color; border: 1px solid $accent;
border-radius: 0.16rem; border-radius: 0.04rem;
padding: 0.05rem 0.16rem; padding: 0.05rem 0.08rem;
transition: all 0.2s; transition: all 0.2s;
&:hover { &:hover {
border-color: $accent;
color: $accent;
background: $theme-color;
} }
} }
@@ -351,4 +349,14 @@
color: #64748B; color: #64748B;
flex-shrink: 0; 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;
}
} }
@@ -55,15 +55,11 @@
// ==================== 下拉面板 ==================== // ==================== 下拉面板 ====================
&__panel { &__panel {
position: absolute;
top: calc(100% + 0.06rem);
left: 0;
width: 4.2rem; width: 4.2rem;
background: $bg-white; background: $bg-white;
border: 1px solid $border-color; border: 1px solid $border-color;
border-radius: 0.1rem; border-radius: 0.1rem;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1); box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
z-index: 100;
padding: 0.1rem; padding: 0.1rem;
// level=2 时只有两栏,面板收窄 // level=2 时只有两栏,面板收窄
+216 -45
View File
@@ -12,6 +12,14 @@
position: fixed; position: fixed;
left: 0; left: 0;
top: 0; top: 0;
z-index: 100;
// 窄版模式(window.innerWidth < 1440
&--narrow {
width: 0.8rem;
padding: 0.16rem 0.10rem;
align-items: center;
}
&__header { &__header {
display: flex; display: flex;
@@ -21,6 +29,12 @@
margin-bottom: 0.2rem; margin-bottom: 0.2rem;
} }
// 窄版header居中
&--narrow &__header {
justify-content: center;
padding: 0.1rem 0;
}
&__avatar { &__avatar {
width: 0.36rem; width: 0.36rem;
height: 0.36rem; height: 0.36rem;
@@ -43,6 +57,12 @@
width: 1.32rem; width: 1.32rem;
height: 0.34rem; height: 0.34rem;
display: block; display: block;
// 窄版logo
&--narrow {
width: 0.46rem;
height: auto;
}
} }
&__menu { &__menu {
@@ -52,7 +72,13 @@
flex: 1; flex: 1;
} }
// 窄版菜单宽度撑满
&--narrow &__menu {
width: 100%;
}
&__item { &__item {
position: relative;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.04rem; gap: 0.04rem;
@@ -79,18 +105,45 @@
} }
} }
// 窄版导航项:纵向居中排列
&--narrow &__item {
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0.04rem;
padding: 0.1rem 0.06rem;
}
&__item-icon { &__item-icon {
width: 0.2rem; width: 0.2rem;
height: 0.2rem; height: 0.2rem;
object-fit: contain; object-fit: contain;
font-size: 0.16rem; font-size: 0.16rem;
// 内部SVG/img统一撑满容器
svg, img {
width: 100%;
height: 100%;
display: block;
}
}
// 窄版图标放大,防止scale缩放后图标过小
&--narrow &__item-icon {
width: 0.24rem;
height: 0.24rem;
} }
&__item-label { &__item-label {
} }
// 窄版label字号缩小
&--narrow &__item-label {
font-size: 0.11rem;
white-space: nowrap;
}
&__badge { &__badge {
background: #e85635; background: #e85635;
color: #fff; color: #fff;
@@ -118,6 +171,39 @@
text-align: center; text-align: center;
color: #FFFFFF; color: #FFFFFF;
flex: none; flex: none;
// 窄版角标样式(右上角定位)
&--corner {
position: absolute;
top: -0.07rem;
right: -0.05rem;
min-width: auto;
padding: 0.02rem 0.04rem;
height: 0.17rem;
font-size: 0.10rem;
line-height: 0.12rem;
border-radius: 0.03rem;
}
}
// 公测群二维码弹出层(hover索引5时显示)
&__qrcode-popup {
position: absolute;
top: calc(100% + 0.08rem);
left: 0;
padding: 0.08rem;
background: #fff;
border-radius: 0.08rem;
box-shadow: 0 0.02rem 0.12rem rgba(0, 0, 0, 0.08);
z-index: 100;
}
&__qrcode-img {
display: block;
width: auto;
height: auto;
max-width: 1.6rem;
border-radius: 0.04rem;
} }
&__footer { &__footer {
@@ -126,6 +212,48 @@
gap: 0.04rem; gap: 0.04rem;
padding-top: 0.12rem; padding-top: 0.12rem;
} }
// 窄版footer居中
&--narrow &__footer {
align-items: center;
width: 100%;
}
}
// ==================== 窄版邀请图片卡片 ====================
.side-nav__referral-card-narrow {
display: flex;
justify-content: center;
margin-top: 0.12rem;
}
.side-nav__referral-card-narrow-img {
width: 0.56rem;
height: 0.85rem;
display: block;
border-radius: 0.06rem;
}
// ==================== 窄版用户信息 ====================
.side-nav__user-info-narrow {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.06rem;
cursor: pointer;
padding: 0.08rem 0;
border-radius: 0.08rem;
&:hover {
}
}
.side-nav__user-avatar-narrow {
width: 0.26rem;
height: 0.26rem;
border-radius: 50%;
display: block;
} }
.side-nav__dialog-overlay { .side-nav__dialog-overlay {
@@ -478,84 +606,120 @@
// ==================== 邀请会员卡片 ==================== // ==================== 邀请会员卡片 ====================
.side-nav__referral-card { .side-nav__referral-card {
position: relative;
width: 100%; width: 100%;
background: linear-gradient(128.17deg, #52CAD1 0%, #82D79B 100%); height: 2.21rem;
background: linear-gradient(90deg, #DFF5F6 0%, #FBFDF7 100%);
box-shadow: 0px 0px 0.08rem rgba(0, 0, 0, 0.06);
border-radius: 0.08rem; border-radius: 0.08rem;
padding: 0.16rem;
box-sizing: border-box; box-sizing: border-box;
margin-top: 0.12rem; margin-top: 0.12rem;
overflow: hidden;
}
// 顶部区域:标题在左上,礼物图片定位在右下角
.side-nav__referral-top {
position: relative;
padding: 0.2rem 0.16rem 0.4rem;
}
.side-nav__referral-top-text {
position: relative;
z-index: 1;
} }
.side-nav__referral-title { .side-nav__referral-title {
font-size: 0.16rem; font-size: 0.18rem;
font-weight: 630; font-weight: 630;
color: #FFFFFF; color: #1A1A1A;
line-height: 0.21rem; line-height: 0.24rem;
&--accent {
background: linear-gradient(128.17deg, #52CAD1 0%, #82D79B 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
} }
.side-nav__referral-subtitle { .side-nav__referral-subtitle {
font-size: 0.10rem; font-size: 0.14rem;
font-weight: 330; font-weight: 330;
color: #EAFBFC; color: #6A7282;
line-height: 0.13rem; line-height: 0.19rem;
margin-top: 0.04rem; margin-top: 0.08rem;
} }
// 礼物图片:absolute定位在顶部区域的右下角,底部被毛玻璃面板遮住
.side-nav__referral-gift {
position: absolute;
right: 0.10rem;
bottom: 0.02rem;
width: 0.68rem;
height: 0.68rem;
mix-blend-mode: darken;
}
// 白色毛玻璃面板(覆盖在礼物底部之上)
.side-nav__referral-panel { .side-nav__referral-panel {
background: #FFFFFF; position: absolute;
box-shadow: 0px 0.02rem 0.06rem rgba(26, 26, 26, 0.05); left: 0;
border-radius: 0.06rem; bottom: 0;
padding: 0.10rem 0.08rem; width: 100%;
margin-top: 0.10rem; height: 1.38rem;
background: rgba(255, 255, 255, 0.4);
border: 1px solid #FFFFFF;
border-bottom: none;
backdrop-filter: blur(3px);
border-radius: 0.08rem 0.08rem 0 0;
box-sizing: border-box;
padding: 0.16rem;
} }
.side-nav__referral-row { .side-nav__referral-row {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; gap: 0.08rem;
} }
.side-nav__referral-label { .side-nav__referral-label {
font-size: 0.10rem; font-size: 0.14rem;
font-weight: 330; font-weight: 330;
color: #6B7280; color: #6A7282;
line-height: 0.13rem; line-height: 0.19rem;
} }
.side-nav__referral-value { .side-nav__referral-value {
font-size: 0.10rem; font-size: 0.14rem;
font-weight: 620; font-weight: 450;
color: #52CAD1; line-height: 0.19rem;
line-height: 0.13rem; background: linear-gradient(128.17deg, #52CAD1 0%, #82D79B 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
} }
.side-nav__referral-track { .side-nav__referral-track {
width: 100%; width: 100%;
height: 0.02rem; height: 0.06rem;
background: #D7F1F3; background: #EDF9FA;
border-radius: 9999px; border-radius: 0.06rem;
margin-top: 0.06rem; margin-top: 0.1rem;
overflow: hidden; overflow: hidden;
} }
.side-nav__referral-fill { .side-nav__referral-fill {
height: 100%; height: 100%;
background: #52CAD1; background: #52CAD1;
border-radius: 9999px; border-radius: 0.06rem;
}
.side-nav__referral-divider {
width: 100%;
border-top: 0.01rem solid #8C8C8C;
margin-top: 0.08rem;
} }
.side-nav__referral-tip { .side-nav__referral-tip {
font-size: 0.10rem; font-size: 0.12rem;
font-weight: 330; font-weight: 330;
color: #1A1A1A; color: #6A7282;
line-height: 0.13rem; line-height: 0.16rem;
margin-top: 0.08rem; margin-top: 0.06rem;
strong { strong {
font-weight: 630; font-weight: 630;
@@ -563,16 +727,22 @@
} }
.side-nav__referral-action { .side-nav__referral-action {
font-size: 0.11rem; display: flex;
font-weight: 620; align-items: center;
color: #52CAD1; justify-content: center;
line-height: 0.15rem; width: 100%;
text-align: center; height: 0.40rem;
margin-top: 0.08rem; background: linear-gradient(90deg, #36C1C9 16.99%, #AFE9D0 100%);
box-shadow: inset 0px 0px 0.06rem rgba(255, 255, 255, 0.5);
border-radius: 0.08rem;
font-size: 0.14rem;
font-weight: 600;
color: #FFFFFF;
margin-top: 0.12rem;
cursor: pointer; cursor: pointer;
&:hover { &:hover {
opacity: 0.8; opacity: 0.9;
} }
} }
@@ -680,12 +850,13 @@
bottom: calc(100% + 0.08rem); bottom: calc(100% + 0.08rem);
left: 0; left: 0;
width: 100%; width: 100%;
min-width: 1.65rem;
background: #FFFFFF; background: #FFFFFF;
border: 1px solid #F0F0F0; border: 1px solid #F0F0F0;
box-shadow: 0px 0.02rem 0.08rem rgba(0, 0, 0, 0.04); box-shadow: 0px 0.02rem 0.08rem rgba(0, 0, 0, 0.04);
border-radius: 0.08rem; border-radius: 0.08rem;
overflow: hidden; overflow: hidden;
z-index: 100; z-index: 1000;
} }
.side-nav__account-popup-header { .side-nav__account-popup-header {
+4
View File
@@ -12,12 +12,15 @@
@use './pages/login.scss'; @use './pages/login.scss';
@use './pages/mentor.scss'; @use './pages/mentor.scss';
@use './pages/mentor-detail.scss'; @use './pages/mentor-detail.scss';
@use './pages/announcement.scss';
@use './pages/announcement-detail.scss';
// 组件样式 // 组件样式
@use './components/hello-world.scss'; @use './components/hello-world.scss';
@use './components/side-nav.scss'; @use './components/side-nav.scss';
@use './components/ai-chat.scss'; @use './components/ai-chat.scss';
@use './components/job-page-header.scss'; @use './components/job-page-header.scss';
@use './components/job-stats-overview.scss';
@use './components/job-goal-dialog.scss'; @use './components/job-goal-dialog.scss';
@use './components/settings-dialog.scss'; @use './components/settings-dialog.scss';
@use './components/member-dialog.scss'; @use './components/member-dialog.scss';
@@ -45,6 +48,7 @@
@use './components/step-progress-overlay.scss'; @use './components/step-progress-overlay.scss';
@use './components/mentor-aside-panel.scss'; @use './components/mentor-aside-panel.scss';
@use './components/agent-browser-install-guide.scss'; @use './components/agent-browser-install-guide.scss';
@use './components/login-dialog.scss';
// 全局样式优先级最高 // 全局样式优先级最高
@use './auto.scss'; @use './auto.scss';
+77 -4
View File
@@ -8,7 +8,7 @@
font-size: 0.14rem; font-size: 0.14rem;
// 主内容区域左侧导航栏右边的部分 // 主内容区域左侧导航栏右边的部分
&__content { &__content {
margin-left: 2.2rem; margin-left: var(--nav-width, 2.2rem);
flex: 1; flex: 1;
overflow-y: auto; overflow-y: auto;
height: var(--app-height, 100vh); height: var(--app-height, 100vh);
@@ -677,6 +677,78 @@
margin-bottom: 0.16rem; 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步功能说明区域 全宽 // 第2步功能说明区域 全宽
&__step2-faq { &__step2-faq {
width: 100%; width: 100%;
@@ -744,13 +816,14 @@
padding: 0.12rem 0.16rem; padding: 0.12rem 0.16rem;
min-width: 1.6rem; min-width: 1.6rem;
height: 0.43rem; height: 0.43rem;
background: #F3F4F6; background: #fff;
border-radius: 0.12rem; border-radius: 0.12rem;
cursor: pointer; cursor: pointer;
box-sizing: border-box; box-sizing: border-box;
user-select: none; user-select: none;
transition: border-color 0.2s ease; transition: border-color 0.2s ease;
width: 1.6rem; width: 100%;
border: 1px solid #F0F0F0;
&:hover { border-color: darken($border-color, 10%); } &:hover { border-color: darken($border-color, 10%); }
} }
@@ -1928,7 +2001,7 @@
.agent-page__install-dialog-title { .agent-page__install-dialog-title {
position: relative; position: relative;
font-size: 0.18rem; font-size: 0.18rem;
font-weight: 520; font-weight: 600;
color: $text-dark; color: $text-dark;
text-align: center; text-align: center;
margin: 0 0 0.2rem 0; margin: 0 0 0.2rem 0;
@@ -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;
}
+555
View File
@@ -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;
}
}
+12 -1
View File
@@ -148,6 +148,9 @@
letter-spacing: 0.03rem; letter-spacing: 0.03rem;
color: #111; color: #111;
margin: 0; margin: 0;
>span{
width: 1.51rem;
}
} }
// 副标题描述 // 副标题描述
@@ -1615,10 +1618,18 @@
margin: 0.6rem auto 0; margin: 0.6rem auto 0;
text-align: center; text-align: center;
padding-top: 0.4rem; padding-top: 0.4rem;
display: flex;
justify-content: center;
gap: 0.2rem;
p { p {
font-size: 0.12rem; font-size: 0.12rem;
line-height: 0.16rem; color: #9ca3af;
}
a{
font-size: 0.12rem;
text-decoration: none;
color: #9ca3af; color: #9ca3af;
} }
} }
+4 -3
View File
@@ -3,7 +3,7 @@
// ==================== 岗位详情页 ==================== // ==================== 岗位详情页 ====================
.job-detail { .job-detail {
&__content { &__content {
margin-left: 2.2rem; margin-left: var(--nav-width, 2.2rem);
margin-right: var(--chat-width, 3.6rem); margin-right: var(--chat-width, 3.6rem);
flex: 1; flex: 1;
height: var(--app-height, 100vh); height: var(--app-height, 100vh);
@@ -167,14 +167,15 @@
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
padding: 0.08rem 0.16rem; width: 0.88rem;
height: 0.37rem;
line-height: 0.37rem;
background: $accent; background: $accent;
border: 1px solid $accent; border: 1px solid $accent;
border-radius: 0.08rem; border-radius: 0.08rem;
color: $bg-white; color: $bg-white;
font-size: 0.14rem; font-size: 0.14rem;
font-weight: 600; font-weight: 600;
line-height: 0.19rem;
cursor: pointer; cursor: pointer;
transition: background 0.2s; transition: background 0.2s;
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -14,7 +14,7 @@
flex: 1; flex: 1;
height: 100%; height: 100%;
overflow: hidden; overflow: hidden;
margin-left: 2.2rem; margin-left: var(--nav-width, 2.2rem);
display: flex; display: flex;
} }
+1 -1
View File
@@ -14,7 +14,7 @@
flex: 1; flex: 1;
height: 100%; height: 100%;
overflow: hidden; overflow: hidden;
margin-left: 2.2rem; margin-left: var(--nav-width, 2.2rem);
} }
// 左侧主内容区 // 左侧主内容区
+1 -1
View File
@@ -3,7 +3,7 @@
// ==================== 个人资料页 ==================== // ==================== 个人资料页 ====================
.profile-page { .profile-page {
&__content { &__content {
margin-left: 2.2rem; margin-left: var(--nav-width, 2.2rem);
flex: 1; flex: 1;
padding-bottom: 0.12rem; padding-bottom: 0.12rem;
height: var(--app-height, 100vh); height: var(--app-height, 100vh);
+2 -2
View File
@@ -3,7 +3,7 @@
// ==================== 简历详情页 ==================== // ==================== 简历详情页 ====================
.resume-detail { .resume-detail {
&__content { &__content {
margin-left: 2.2rem; margin-left: var(--nav-width, 2.2rem);
flex: 1; flex: 1;
padding-bottom: 0.24rem; padding-bottom: 0.24rem;
height: var(--app-height, 100vh); height: var(--app-height, 100vh);
@@ -726,7 +726,7 @@
background: $bg-white; background: $bg-white;
border: 1px solid $accent; border: 1px solid $accent;
border-radius: 0.04rem; border-radius: 0.04rem;
padding: 0.05rem 0.14rem; padding: 0.05rem 0.08rem;
transition: all 0.2s; transition: all 0.2s;
&:hover { &:hover {
+1 -1
View File
@@ -3,7 +3,7 @@
// ==================== 简历列表页 ==================== // ==================== 简历列表页 ====================
.resume-page { .resume-page {
&__content { &__content {
margin-left: 2.2rem; margin-left: var(--nav-width, 2.2rem);
flex: 1; flex: 1;
height: var(--app-height, 100vh); height: var(--app-height, 100vh);
box-sizing: border-box; box-sizing: border-box;
+9
View File
@@ -0,0 +1,9 @@
/**
*
*/
/** 是否开启"个人资料为空时强制跳转到登录页简历上传"机制 */
export const ENABLE_FORCE_RESUME_UPLOAD = true
/** 浏览器插件目标版本(用于判断用户是否需要更新插件) */
export const PLUGIN_TARGET_VERSION = '0.1.1'
+132 -65
View File
@@ -1,6 +1,99 @@
<!-- Agent页面 - 浏览器插件安装步骤指引组件 --> <!-- Agent页面 - 浏览器插件安装步骤指引组件双Tab切换 -->
<template> <template>
<div class="agent-browser-guide-drawer__body"> <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>
</div>
<!-- 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>
<!-- 步骤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下载插件包 --> <!-- 步骤1下载插件包 -->
<div class="agent-browser-guide-drawer__card"> <div class="agent-browser-guide-drawer__card">
<div class="mr100"> <div class="mr100">
@@ -8,7 +101,7 @@
<span class="agent-browser-guide-drawer__step-num">1</span> <span class="agent-browser-guide-drawer__step-num">1</span>
<div class="agent-browser-guide-drawer__step-info"> <div class="agent-browser-guide-drawer__step-info">
<h4 class="agent-browser-guide-drawer__step-title">下载并解压插件压缩包</h4> <h4 class="agent-browser-guide-drawer__step-title">下载并解压插件压缩包</h4>
<p class="agent-browser-guide-drawer__step-desc">下载 Offer派自动投递助手安装包并将下载好的插件压缩包解压</p> <p class="agent-browser-guide-drawer__step-desc">下载 Offer派智能投递助手安装包并将下载好的插件压缩包解压</p>
</div> </div>
</div> </div>
<div class="agent-browser-guide-drawer__download-box w263"> <div class="agent-browser-guide-drawer__download-box w263">
@@ -25,7 +118,6 @@
<!-- 步骤2打开扩展管理页面 --> <!-- 步骤2打开扩展管理页面 -->
<div class="agent-browser-guide-drawer__card"> <div class="agent-browser-guide-drawer__card">
<div> <div>
<div class="agent-browser-guide-drawer__step-row"> <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">2</span>
@@ -38,18 +130,14 @@
<div class="agent-browser-guide-drawer__browser-links"> <div class="agent-browser-guide-drawer__browser-links">
<div class="agent-browser-guide-drawer__browser-link" @click="copyToClipboard('chrome://extensions')"> <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"> <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)"> <g clip-path="url(#clip0_guide_chrome2)">
<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="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.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="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.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 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.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 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.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"/> <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> </g>
<defs> <defs><clipPath id="clip0_guide_chrome2"><rect width="16" height="16" fill="white"/></clipPath></defs>
<clipPath id="clip0_guide_chrome">
<rect width="16" height="16" fill="white"/>
</clipPath>
</defs>
</svg> </svg>
<span>Chrome浏览器chrome://extensions</span> <span>Chrome浏览器chrome://extensions</span>
<span class="agent-browser-guide-drawer__copy-btn"> <span class="agent-browser-guide-drawer__copy-btn">
@@ -59,50 +147,11 @@
</div> </div>
<div class="agent-browser-guide-drawer__browser-link" @click="copyToClipboard('edge://extensions')"> <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"> <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)"> <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="url(#paint0_linear_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="#0C59A4"/>
<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="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"/>
<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> </g>
<defs> <defs><clipPath id="clip0_guide_edge2"><rect width="16" height="16" fill="white"/></clipPath></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> </svg>
<span>Edge浏览器edge://extensions</span> <span>Edge浏览器edge://extensions</span>
<span class="agent-browser-guide-drawer__copy-btn"> <span class="agent-browser-guide-drawer__copy-btn">
@@ -111,7 +160,6 @@
</span> </span>
</div> </div>
</div> </div>
</div> </div>
<!-- 步骤2示意图 --> <!-- 步骤2示意图 -->
<img src="@/assets/images/agent-setting/2-1.png" alt="输入扩展地址" class="agent-browser-guide-drawer__step-img" /> <img src="@/assets/images/agent-setting/2-1.png" alt="输入扩展地址" class="agent-browser-guide-drawer__step-img" />
@@ -131,28 +179,47 @@
<img src="@/assets/images/agent-setting/3-1.png" alt="打开开发者模式" class="agent-browser-guide-drawer__step-img max-w602" /> <img src="@/assets/images/agent-setting/3-1.png" alt="打开开发者模式" class="agent-browser-guide-drawer__step-img max-w602" />
</div> </div>
<!-- 步骤4将CRX安装包拖入安装 --> <!-- 步骤4加载未打包的扩展程序 -->
<div class="agent-browser-guide-drawer__card"> <div class="agent-browser-guide-drawer__card">
<div class="agent-browser-guide-drawer__step-row"> <div class="agent-browser-guide-drawer__step-row">
<span class="agent-browser-guide-drawer__step-num">4</span> <span class="agent-browser-guide-drawer__step-num">4</span>
<div class="agent-browser-guide-drawer__step-info"> <div class="agent-browser-guide-drawer__step-info dflex flex-warp">
<div>
<h4 class="agent-browser-guide-drawer__step-title">点击加载未打包的扩展程序</h4> <h4 class="agent-browser-guide-drawer__step-title">点击加载未打包的扩展程序</h4>
<p class="agent-browser-guide-drawer__step-desc">点击"加载未打包的扩展程序"选择解压后的插件文件夹完成安装</p> <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>
</div> </div>
<!-- 步骤4示意图 --> <!-- 步骤4示意图 -->
<img src="@/assets/images/agent-setting/4-1.png" alt="拖入安装" class="agent-browser-guide-drawer__step-img" />
<img src="@/assets/images/agent-setting/4-2.png" alt="加载扩展" class="agent-browser-guide-drawer__step-img" />
</div>
</div> </div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue'
import { ElMessage } from 'element-plus' 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' const extensionDownloadUrl = 'https://offerpie.oss-cn-guangzhou.aliyuncs.com/extension/Offer派智能投递助手.zip'
/** 下载插件 */ /** 跳转Edge商店 */
function openEdgeStore() {
window.open(edgeStoreUrl, '_blank')
}
/** 下载插件zip包 */
function downloadExtension() { function downloadExtension() {
window.open(extensionDownloadUrl, '_blank') window.open(extensionDownloadUrl, '_blank')
} }
+455 -140
View File
@@ -43,9 +43,28 @@
<span class="agent-page__intro-avatar-name">Nova</span> <span class="agent-page__intro-avatar-name">Nova</span>
</div> </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> <button class="agent-page__intro-gradient-btn" @click="handleNext">下一步</button>
@@ -54,7 +73,7 @@
</template> </template>
<!-- ========== 第2步确认目标 ========== --> <!-- ========== 第2步配置投递方案 ========== -->
<template v-if="currentStep === 2"> <template v-if="currentStep === 2">
<!-- 左侧求职偏好 + 简历选择 --> <!-- 左侧求职偏好 + 简历选择 -->
<div class="agent-page__right"> <div class="agent-page__right">
@@ -113,25 +132,28 @@
</div> </div>
</div> </div>
</div> </div>
<!-- 简历选择区域 --> <!-- 投递偏好区域 -->
<div class="agent-page__step2-section"> <div class="agent-page__step2-section">
<h3 class="agent-page__step2-section-title">简历选择</h3> <h3 class="agent-page__step2-section-title">投递偏好</h3>
<div class="agent-page__step2-filters"> <div class="agent-page__step2-pref-switches">
<!-- 自定义简历下拉 --> <!-- 开启简历针对性优化 -->
<div class="agent-page__step2-resume-dropdown" @click.stop="showResumeDropdown = !showResumeDropdown"> <div class="agent-page__step2-switch-item">
<span class="agent-page__step2-resume-dropdown-text">{{ selectedResumeName || '请选择简历' }}</span> <div class="agent-page__step2-switch-text">
<svg class="agent-page__step2-resume-dropdown-arrow" viewBox="0 0 12 12" fill="none"> <span class="agent-page__step2-switch-label">开启简历针对性优化</span>
<path d="M3 4.5L6 7.5L9 4.5" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/> <span class="agent-page__step2-switch-desc">结合岗位要求自动优化简历表达</span>
</svg> </div>
<!-- 下拉菜单 --> <div class="agent-page__step2-toggle" :class="{ 'agent-page__step2-toggle--on': autoOptimizeSwitch }" @click="toggleOptimize">
<div v-if="showResumeDropdown" class="agent-page__step2-resume-menu" @click.stop> <div class="agent-page__step2-toggle-dot"></div>
<div </div>
v-for="r in resumeList" </div>
:key="r.id" <!-- 开启深度人岗匹配分析 -->
class="agent-page__step2-resume-menu-item" <div class="agent-page__step2-switch-item">
:class="{ 'agent-page__step2-resume-menu-item--active': selectedResumeId === r.id }" <div class="agent-page__step2-switch-text">
@click.stop="selectResume(r)" <span class="agent-page__step2-switch-label">开启深度人岗匹配分析</span>
>{{ r.resumeName }}</div> <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> </div>
</div> </div>
@@ -176,9 +198,9 @@
<span class="agent-page__intro-avatar-name">Nova</span> <span class="agent-page__intro-avatar-name">Nova</span>
</div> </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-if="plugIsOnline" class="agent-page__intro-gradient-btn" @click="handleNext">下一步</button>
<button v-else class="agent-page__intro-gradient-btn" @click="handleInstallConfirm"> <button v-else class="agent-page__intro-gradient-btn" @click="handleInstallConfirm">
@@ -234,7 +256,7 @@
</Teleport> </Teleport>
</div> </div>
<!-- 功能说明 独立区块仅第2步显示 --> <!-- 功能说明 独立区块仅第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> <h3 class="agent-page__step2-faq-title">功能说明</h3>
<div class="agent-page__step2-faq-item"> <div class="agent-page__step2-faq-item">
<div class="agent-page__step2-faq-item-title">为什么要选择求职偏好</div> <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 JobCategorySelector from '@/components/tools/JobCategorySelector.vue'
import IndustrySelector from '@/components/tools/IndustrySelector.vue' import IndustrySelector from '@/components/tools/IndustrySelector.vue'
import AgentBrowserInstallGuide from '@/components/AgentBrowserInstallGuide.vue' import AgentBrowserInstallGuide from '@/components/AgentBrowserInstallGuide.vue'
import { saveProfile, saveEducation, saveWork, saveInternship, saveProject, saveCompetition, fetchProfile, fetchEducation, fetchWork, fetchInternship, fetchProject, fetchCompetition } from '@/api/profile' import { saveProfile, fetchProfile } from '@/api/profile'
import type { SaveEducationItem, SaveWorkItem, SaveProjectItem, SaveCompetitionItem } from '@/api/profile' import {
import { fetchResumeList, setDefaultResume } from '@/api/resume' 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 { ResumeListItem } from '@/api/resume'
import type { AgentConfig } from '@/api/agent' import type { AgentConfig } from '@/api/agent'
import { fetchAgentConfig, saveAgentConfig } from '@/api/agent'
const store = useStore() const store = useStore()
@@ -309,6 +339,10 @@ const editModule = ref('info')
const editInitialData = ref<Record<string, any>>({}) const editInitialData = ref<Record<string, any>>({})
// ==================== ==================== // ==================== ====================
/** 默认简历 ID(从简历列表中确定) */
const defaultResumeId = ref('')
onMounted(async () => { onMounted(async () => {
// //
const savedStep = sessionStorage.getItem('wizardStep') const savedStep = sessionStorage.getItem('wizardStep')
@@ -337,92 +371,160 @@ onMounted(async () => {
if (!store.state.regions.length) store.dispatch('loadCommonData') if (!store.state.regions.length) store.dispatch('loadCommonData')
store.dispatch('loadJobIntention') store.dispatch('loadJobIntention')
// /user/profile
await loadProfile() await loadProfile()
await loadEducation() // 5
await loadWork() await loadDefaultResumeData()
await loadInternship() //
await loadProject() loadResumeList()
await loadCompetition() //
loadPreferenceSwitches()
}) })
// ==================== 线 store ==================== // ==================== 线 store ====================
/** 插件是否在线(从 vuex 全局状态读取) */ /** 插件是否在线(从 vuex 全局状态读取) */
const plugIsOnline = computed(() => store.state.plugIsOnline) const plugIsOnline = computed(() => store.state.plugIsOnline)
/** 加载个人资料主表 — 仅取 name/email/mobileNumber/regionCode/wechatNumber/portfolioUrl */
async function loadProfile() { async function loadProfile() {
try { try {
const res = await fetchProfile() const res = await fetchProfile()
if (res.code === '0' && res.data) { if (res.code === '0' && res.data) {
const d = res.data const d = res.data
profile.value.name = d.name || ''; profile.value.phone = d.mobileNumber || '' profile.value.name = d.name || ''
profile.value.email = d.email || ''; profile.value.idNumber = d.idCard || '' profile.value.phone = d.mobileNumber || ''
profile.value.regionCode = d.regionCode || ''; profile.value.wechat = d.wechatNumber || '' profile.value.email = d.email || ''
profile.value.skills = d.skills || []; profile.value.certificates = d.certificates || [] profile.value.regionCode = d.regionCode || ''
profile.value.wechat = d.wechatNumber || ''
profile.value.portfolioUrl = d.portfolioUrl || '' profile.value.portfolioUrl = d.portfolioUrl || ''
} }
} catch { console.error('[AgentSetupWizard] 加载个人资料失败') } } catch { console.error('[AgentSetupWizard] 加载个人资料失败') }
} }
async function loadEducation() {
/**
* 确定默认简历并加载简历主表 + 5大经历
* 规则优先 isDefault===1 的简历否则取 createTime 最新的
*/
async function loadDefaultResumeData() {
try { try {
const res = await fetchEducation() const listRes = await fetchResumeList()
if (res.code === '0' && res.data) { if (listRes.code !== '0' || !listRes.data || listRes.data.length === 0) {
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 || '' })) })) console.warn('[AgentSetupWizard] 无简历数据')
return
} }
} catch { console.error('[AgentSetupWizard] 加载教育经历失败') } const list = listRes.data
} //
async function loadWork() { let target = list.find(r => r.isDefault === 1)
try { if (!target) {
const res = await fetchWork() // createTime
if (res.code === '0' && res.data) { target = [...list].sort((a, b) => (b.createTime || 0) - (a.createTime || 0))[0]
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 || '' })) }))
} }
} catch { console.error('[AgentSetupWizard] 加载工作经历失败') } if (!target || !target.id) return
} defaultResumeId.value = target.id
async function loadInternship() {
try { // + 5
const res = await fetchInternship() const [mainRes, eduRes, workRes, internRes, projRes, compRes] = await Promise.all([
if (res.code === '0' && res.data) { fetchResumeMain(defaultResumeId.value),
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 || '' })) })) 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] 加载实习经历失败') } //
} if (eduRes.code === '0' && eduRes.data) {
async function loadProject() { profile.value.education = eduRes.data.map(item => ({
try { id: item.id || '',
const res = await fetchProject() school: item.school || '', major: item.major || '',
if (res.code === '0' && res.data) { studyType: item.studyType || '',
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 || '' })) })) degree: item.degree || '',
startDate: item.startDate || '', endDate: item.endDate || '',
description: (item.description || []).map(d => ({ id: d.id || '', text: d.text || '' })),
}))
} }
} catch { console.error('[AgentSetupWizard] 加载项目经历失败') } //
} if (workRes.code === '0' && workRes.data) {
async function loadCompetition() { profile.value.works = workRes.data.map(item => ({
try { id: item.id || '',
const res = await fetchCompetition() companyName: item.companyName || '', position: item.position || '',
if (res.code === '0' && res.data) { startDate: item.startDate || '', endDate: item.endDate || '',
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 || '' })) })) 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({ const profile = ref({
name: '', phone: '', email: '', idNumber: '', regionCode: '', portfolioUrl: '', wechat: '', name: '', phone: '', email: '', idNumber: '', regionCode: '', portfolioUrl: '', wechat: '',
skills: [] as string[], certificates: [] as string[], skills: [] as string[], certificates: [] as string[], summary: '',
education: [] as Array<{ school: string; major: string; studyType: number; degree: number; startDate: string; endDate: string; description: Array<{ id: string; text: string }> }>, 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<{ companyName: string; position: string; 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<{ 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<{ projectName: string; companyName: string; role: 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<{ competitionName: string; award: string; awardDate: 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) { function handleEdit(section: string) {
editModule.value = section 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 } } 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 === 'education') {
else if (section === 'work') { editInitialData.value = { works: profile.value.works.map(exp => ({ ...exp, description: exp.description.map(d => ({ ...d })) })) } } // IDdiff
else if (section === 'internship') { editInitialData.value = { internships: (profile.value.internships || []).map(exp => ({ ...exp, description: exp.description.map(d => ({ ...d })) })) } } editSnapshotEducationIds.value = profile.value.education.map(e => e.id).filter(Boolean)
else if (section === 'project') { editInitialData.value = { projects: (profile.value.projects || []).map(proj => ({ ...proj, description: proj.description.map(d => ({ ...d })) })) } } editInitialData.value = { education: profile.value.education.map(edu => ({ ...edu, description: edu.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 === '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 === '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 === 'skills') { editInitialData.value = { skills: [...profile.value.skills] } }
else if (section === 'certificate') { editInitialData.value = { certificates: [...(profile.value.certificates || [])] } } else if (section === 'certificate') { editInitialData.value = { certificates: [...(profile.value.certificates || [])] } }
else { editInitialData.value = {} } else { editInitialData.value = {} }
@@ -432,58 +534,226 @@ function handleEdit(section: string) {
/** 保存中的加载状态 */ /** 保存中的加载状态 */
const saving = ref(false) const saving = ref(false)
/** 保存编辑数据 — 调用接口持久化 */ /** 保存编辑数据 — 个人信息走 profile 接口,其余走简历接口 */
async function handleSaveEdit(data: Record<string, any>) { async function handleSaveEdit(data: Record<string, any>) {
if (editModule.value === 'info') { 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') { } 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') { } 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') { } 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') { } 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') { } 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') { } 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') { } 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') { } 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>) { async function handleInlineSave(section: string, data: Record<string, any>) {
editModule.value = section editModule.value = section
await handleSaveEdit(data) await handleSaveEdit(data)
} }
/** 单条经历更新 — 替换到完整列表后整体保存 */ /** 单条经历更新 — 调用简历单条编辑接口 */
async function handleExpUpdate(moduleType: string, index: number, data: Record<string, any>) { async function handleExpUpdate(moduleType: string, index: number, data: Record<string, any>) {
if (!defaultResumeId.value) { ElMessage.error('未找到默认简历'); return }
try { 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') { if (moduleType === 'education') {
const list = profile.value.education.map((item, i) => i === index ? data : item) as any[] 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 })
await saveEducation(list) profile.value.education[index] = { id: recordId, ...data } as any
profile.value.education = list
} else if (moduleType === 'work') { } else if (moduleType === 'work') {
const list = profile.value.works.map((item, i) => i === index ? data : item) as any[] await updateResumeWork({ id: recordId, companyName: data.companyName, position: data.position, startDate: data.startDate, endDate: data.endDate, description: data.description })
await saveWork(list) profile.value.works[index] = { id: recordId, ...data } as any
profile.value.works = list
} else if (moduleType === 'internship') { } else if (moduleType === 'internship') {
const list = profile.value.internships.map((item, i) => i === index ? data : item) as any[] await updateResumeInternship({ id: recordId, companyName: data.companyName, position: data.position, startDate: data.startDate, endDate: data.endDate, description: data.description })
await saveInternship(list) profile.value.internships[index] = { id: recordId, ...data } as any
profile.value.internships = list
} else if (moduleType === 'project') { } else if (moduleType === 'project') {
const list = profile.value.projects.map((item, i) => i === index ? data : item) as any[] await updateResumeProject({ id: recordId, projectName: data.projectName, companyName: data.companyName, role: data.role, startDate: data.startDate, endDate: data.endDate, description: data.description })
await saveProject(list) profile.value.projects[index] = { id: recordId, ...data } as any
profile.value.projects = list
} else if (moduleType === 'competition') { } else if (moduleType === 'competition') {
const list = profile.value.competitions.map((item, i) => i === index ? data : item) as any[] await updateResumeCompetition({ id: recordId, competitionName: data.competitionName, award: data.award, awardDate: data.awardDate, description: data.description })
await saveCompetition(list) profile.value.competitions[index] = { id: recordId, ...data } as any
profile.value.competitions = list
} }
ElMessage.success('保存成功') ElMessage.success('保存成功')
} catch { } catch {
@@ -491,29 +761,31 @@ async function handleExpUpdate(moduleType: string, index: number, data: Record<s
} }
} }
/** 新增经历 */ /** 新增经历 — 调用简历单条添加接口 */
async function handleExpAdd(moduleType: string, data: Record<string, any>) { async function handleExpAdd(moduleType: string, data: Record<string, any>) {
if (!defaultResumeId.value) { ElMessage.error('未找到默认简历'); return }
try { try {
let newId = ''
if (moduleType === 'education') { if (moduleType === 'education') {
const list = [...profile.value.education, data] as any[] 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 })
await saveEducation(list) newId = res.data || ''
profile.value.education = list profile.value.education.push({ id: newId, ...data } as any)
} else if (moduleType === 'work') { } else if (moduleType === 'work') {
const list = [...profile.value.works, data] as any[] const res = await addResumeWork(defaultResumeId.value, { companyName: data.companyName, position: data.position, startDate: data.startDate, endDate: data.endDate, description: data.description })
await saveWork(list) newId = res.data || ''
profile.value.works = list profile.value.works.push({ id: newId, ...data } as any)
} else if (moduleType === 'internship') { } else if (moduleType === 'internship') {
const list = [...profile.value.internships, data] as any[] const res = await addResumeInternship(defaultResumeId.value, { companyName: data.companyName, position: data.position, startDate: data.startDate, endDate: data.endDate, description: data.description })
await saveInternship(list) newId = res.data || ''
profile.value.internships = list profile.value.internships.push({ id: newId, ...data } as any)
} else if (moduleType === 'project') { } else if (moduleType === 'project') {
const list = [...profile.value.projects, data] as any[] const res = await addResumeProject(defaultResumeId.value, { projectName: data.projectName, companyName: data.companyName, role: data.role, startDate: data.startDate, endDate: data.endDate, description: data.description })
await saveProject(list) newId = res.data || ''
profile.value.projects = list profile.value.projects.push({ id: newId, ...data } as any)
} else if (moduleType === 'competition') { } else if (moduleType === 'competition') {
const list = [...profile.value.competitions, data] as any[] const res = await addResumeCompetition(defaultResumeId.value, { competitionName: data.competitionName, award: data.award, awardDate: data.awardDate, description: data.description })
await saveCompetition(list) newId = res.data || ''
profile.value.competitions = list profile.value.competitions.push({ id: newId, ...data } as any)
} }
ElMessage.success('添加成功') ElMessage.success('添加成功')
} catch { } catch {
@@ -521,7 +793,7 @@ async function handleExpAdd(moduleType: string, data: Record<string, any>) {
} }
} }
/** 删除单条经历 — 确认后从列表移除并全量保存 */ /** 删除单条经历 — 调用简历单条删除接口 */
async function handleExpDelete(moduleType: string, index: number) { async function handleExpDelete(moduleType: string, index: number) {
try { try {
await ElMessageBox.confirm('确定要删除这条经历吗?', '提示', { await ElMessageBox.confirm('确定要删除这条经历吗?', '提示', {
@@ -532,27 +804,28 @@ async function handleExpDelete(moduleType: string, index: number) {
} catch { } catch {
return return
} }
if (!defaultResumeId.value) { ElMessage.error('未找到默认简历'); return }
try { try {
if (moduleType === 'education') { if (moduleType === 'education') {
const list = profile.value.education.filter((_, i) => i !== index) as any[] const id = profile.value.education[index]?.id
await saveEducation(list) if (id) await deleteResumeEducation(id)
profile.value.education = list profile.value.education.splice(index, 1)
} else if (moduleType === 'work') { } else if (moduleType === 'work') {
const list = profile.value.works.filter((_, i) => i !== index) as any[] const id = profile.value.works[index]?.id
await saveWork(list) if (id) await deleteResumeWork(id)
profile.value.works = list profile.value.works.splice(index, 1)
} else if (moduleType === 'internship') { } else if (moduleType === 'internship') {
const list = profile.value.internships.filter((_, i) => i !== index) as any[] const id = profile.value.internships[index]?.id
await saveInternship(list) if (id) await deleteResumeInternship(id)
profile.value.internships = list profile.value.internships.splice(index, 1)
} else if (moduleType === 'project') { } else if (moduleType === 'project') {
const list = profile.value.projects.filter((_, i) => i !== index) as any[] const id = profile.value.projects[index]?.id
await saveProject(list) if (id) await deleteResumeProject(id)
profile.value.projects = list profile.value.projects.splice(index, 1)
} else if (moduleType === 'competition') { } else if (moduleType === 'competition') {
const list = profile.value.competitions.filter((_, i) => i !== index) as any[] const id = profile.value.competitions[index]?.id
await saveCompetition(list) if (id) await deleteResumeCompetition(id)
profile.value.competitions = list profile.value.competitions.splice(index, 1)
} }
ElMessage.success('删除成功') ElMessage.success('删除成功')
} catch { } catch {
@@ -581,6 +854,11 @@ const selectedResumeName = computed(() => {
function selectResume(r: { id?: string; resumeName?: string }) { function selectResume(r: { id?: string; resumeName?: string }) {
selectedResumeId.value = r.id || '' selectedResumeId.value = r.id || ''
showResumeDropdown.value = false showResumeDropdown.value = false
// /user/profile
loadProfile()
// 5
loadDefaultResumeData()
} }
/** 选中招聘分类 */ /** 选中招聘分类 */
@@ -639,13 +917,50 @@ const autoOptimizeSwitch = ref(true)
/** 岗位匹配分析开关 */ /** 岗位匹配分析开关 */
const autoMatchAnalysisSwitch = 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() { async function loadResumeList() {
try { try {
const res = await fetchResumeList() const res = await fetchResumeList()
if (res.code === '0' && res.data) { if (res.code === '0' && res.data) {
resumeList.value = 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) { if (defaultResume && defaultResume.id) {
selectedResumeId.value = defaultResume.id selectedResumeId.value = defaultResume.id
} else if (res.data.length > 0 && res.data[0].id) { } else if (res.data.length > 0 && res.data[0].id) {
@@ -656,7 +971,7 @@ async function loadResumeList() {
} }
/** 进入第2步时加载简历列表 */ /** 进入第2步时加载简历列表 */
watch(currentStep, (val) => { if (val === 2) loadResumeList() }) // watch(currentStep, (val) => { if (val === 1) loadResumeList() })
/** 选择简历后设为默认简历 */ /** 选择简历后设为默认简历 */
watch(selectedResumeId, async (newId, oldId) => { watch(selectedResumeId, async (newId, oldId) => {
+4 -4
View File
@@ -152,11 +152,11 @@ function switchTab(tab: 'pending' | 'completed') {
activeTab.value = tab activeTab.value = tab
} }
/** 加载待投递列表(pageSize=300 */ /** 加载待投递列表(pageSize=200 */
async function loadPendingList() { async function loadPendingList() {
pendingLoading.value = true pendingLoading.value = true
try { try {
const res = await fetchAgentTaskList({ pageNum: 1, pageSize: 300, tab: 1 }) const res = await fetchAgentTaskList({ pageNum: 1, pageSize: 200, tab: 1 })
if (res.code === '0' && res.data) { if (res.code === '0' && res.data) {
pendingList.value = res.data.list || [] pendingList.value = res.data.list || []
} }
@@ -167,11 +167,11 @@ async function loadPendingList() {
} }
} }
/** 加载已完成列表(pageSize=300 */ /** 加载已完成列表(pageSize=200 */
async function loadCompletedList() { async function loadCompletedList() {
completedLoading.value = true completedLoading.value = true
try { 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) { if (res.code === '0' && res.data) {
completedList.value = res.data.list || [] completedList.value = res.data.list || []
} }
+420
View File
@@ -0,0 +1,420 @@
<template>
<!-- 选择投递方式弹窗 -->
<div v-if="visible" class="job-apply-method__overlay" @click.self="close">
<div class="job-apply-method__dialog">
<!-- 渐变背景层 -->
<div class="job-apply-method__bg"></div>
<!-- 标题 -->
<p class="job-apply-method__title">选择投递方式</p>
<!-- 关闭按钮 -->
<span class="job-apply-method__close" @click="close">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none"><path d="M18 6L6 18M6 6l12 12" stroke="#6A7282" stroke-width="1.5" stroke-linecap="round"/></svg>
</span>
<!-- 两个选项卡 -->
<div class="job-apply-method__options">
<!-- 直接前往官网 -->
<div class="job-apply-method__card" :class="{ 'job-apply-method__card--active': choice === 'direct' }" @click="choice = 'direct'">
<p class="job-apply-method__card-title">直接前往官网</p>
<p class="job-apply-method__card-desc">手动填写网申信息<br>自行记录投递进度</p>
</div>
<!-- 智能投递助手 -->
<div class="job-apply-method__card" :class="{ 'job-apply-method__card--active': choice === 'agent' }" @click="choice = 'agent'">
<!-- 左下角圆形装饰 -->
<div class="job-apply-method__card-circle"></div>
<div class="job-apply-method__card-header">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M8 1L10 5.5L15 6.5L11.5 10L12.5 15L8 12.5L3.5 15L4.5 10L1 6.5L6 5.5L8 1Z" fill="#52CAD1"/></svg>
<span class="job-apply-method__card-title job-apply-method__card-title--accent">智能投递助手</span>
</div>
<p class="job-apply-method__card-desc">{{ agentCardDesc }}</p>
</div>
</div>
<!-- 底部操作按钮 -->
<button class="job-apply-method__action" :class="actionClass" :disabled="!choice" @click="handleAction">
<svg v-if="actionIcon" width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_apply_method)">
<path d="M6.0714 3.63204C6.47007 2.46537 8.08207 2.43004 8.55474 3.52604L8.59474 3.63271L9.13274 5.20604C9.25603 5.56686 9.45527 5.89705 9.71702 6.17432C9.97877 6.4516 10.2969 6.66952 10.6501 6.81337L10.7947 6.86737L12.3681 7.40471C13.5347 7.80337 13.5701 9.41537 12.4747 9.88804L12.3681 9.92804L10.7947 10.466C10.4338 10.5893 10.1035 10.7885 9.82608 11.0502C9.54869 11.312 9.33067 11.6302 9.18674 11.9834L9.13274 12.1274L8.5954 13.7014C8.19674 14.868 6.58474 14.9034 6.11274 13.808L6.0714 13.7014L5.53407 12.128C5.41086 11.7671 5.21165 11.4368 4.9499 11.1594C4.68814 10.882 4.36993 10.664 4.01674 10.52L3.87274 10.466L2.2994 9.92871C1.13207 9.53004 1.09674 7.91804 2.19274 7.44604L2.2994 7.40471L3.87274 6.86737C4.23356 6.74408 4.56374 6.54484 4.84102 6.28309C5.11829 6.02134 5.33621 5.70317 5.48007 5.35004L5.53407 5.20604L6.0714 3.63204ZM7.3334 4.06271L6.79607 5.63604C6.60833 6.18624 6.30292 6.68891 5.90109 7.10904C5.49927 7.52917 5.0107 7.85666 4.4694 8.06871L4.30274 8.12937L2.7294 8.66671L4.30274 9.20404C4.85294 9.39178 5.35561 9.6972 5.77573 10.099C6.19586 10.5008 6.52335 10.9894 6.7354 11.5307L6.79607 11.6974L7.3334 13.2707L7.87074 11.6974C8.05848 11.1472 8.36389 10.6445 8.76571 10.2244C9.16753 9.80425 9.65611 9.47676 10.1974 9.26471L10.3641 9.20471L11.9374 8.66671L10.3641 8.12937C9.81387 7.94163 9.3112 7.63622 8.89107 7.2344C8.47094 6.83258 8.14345 6.34401 7.9314 5.80271L7.8714 5.63604L7.3334 4.06271ZM12.6667 1.33337C12.7915 1.33337 12.9137 1.36836 13.0195 1.43436C13.1253 1.50035 13.2105 1.59471 13.2654 1.70671L13.2974 1.78471L13.5307 2.46871L14.2154 2.70204C14.3404 2.7445 14.45 2.82312 14.5302 2.92792C14.6105 3.03272 14.6578 3.15899 14.6663 3.29072C14.6747 3.42246 14.6438 3.55373 14.5775 3.6679C14.5113 3.78207 14.4126 3.874 14.2941 3.93204L14.2154 3.96404L13.5314 4.19737L13.2981 4.88204C13.2555 5.00699 13.1769 5.11651 13.072 5.1967C12.9672 5.2769 12.8409 5.32417 12.7092 5.33252C12.5774 5.34088 12.4462 5.30994 12.3321 5.24363C12.2179 5.17732 12.1261 5.07862 12.0681 4.96004L12.0361 4.88204L11.8027 4.19804L11.1181 3.96471C10.9931 3.92224 10.8835 3.84363 10.8033 3.73883C10.723 3.63403 10.6756 3.50776 10.6672 3.37603C10.6588 3.24429 10.6897 3.11302 10.7559 2.99885C10.8222 2.88468 10.9208 2.79275 11.0394 2.73471L11.1181 2.70271L11.8021 2.46937L12.0354 1.78471C12.0804 1.65299 12.1654 1.53865 12.2786 1.45771C12.3918 1.37677 12.5276 1.33329 12.6667 1.33337Z" fill="white"/>
</g>
<defs>
<clipPath id="clip0_apply_method">
<rect width="16" height="16" fill="white"/>
</clipPath>
</defs>
</svg>
{{ actionText }}
</button>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { useRouter } from 'vue-router'
import { useStore } from 'vuex'
import { createChannelBridge } from '@/utils/channelBridge'
import { fetchAgentConfig, applyJob } from '@/api/agent'
import { ElMessage } from 'element-plus'
// ==================== Props & Emits ====================
const props = defineProps<{
/** 控制弹窗显隐(v-model */
modelValue: boolean
/** 岗位来源链接 */
sourceUrl: string
/** 岗位 ID */
jobId: string
}>()
const emit = defineEmits<{
(e: 'update:modelValue', val: boolean): void
/** 需要打开会员购买弹窗 */
(e: 'openMember'): void
}>()
// ==================== ====================
const router = useRouter()
const store = useStore()
// ==================== ====================
/** 弹窗是否显示 */
const visible = computed(() => props.modelValue)
// ==================== ====================
/** 用户选择的投递方式:null=未选择, 'direct'=直接前往官网, 'agent'=智能投递助手 */
const choice = ref<'direct' | 'agent' | null>(null)
/** AI助手是否已完成配置(status === 1 */
const agentConfigReady = ref(false)
/** 是否正在加载 AI 助手配置 */
const agentConfigLoading = ref(false)
// ==================== Store ====================
/** 用户会员状态 */
const memberStatus = computed(() => store.state.memberStatus)
/** 用户是否是会员(正式或试用) */
const isMember = computed(() => !!memberStatus.value?.isMember)
// ==================== ====================
/** 智能投递助手选项卡内的描述文字 — 根据会员和配置状态动态显示 */
const agentCardDesc = computed(() => {
if (!isMember.value) {
return '自动填写网申信息\n统一管理投递进度'
}
if (!agentConfigReady.value) {
return '完成助手设置后\n可使用智能投递'
}
return '将该岗位加入待投递列表\n在投递助手中统一处理'
})
/** 底部操作按钮文字 */
const actionText = computed(() => {
if (!choice.value) return '请选择投递方式'
if (choice.value === 'direct') return '去官网投递'
if (!isMember.value) return '开通会员使用'
if (!agentConfigReady.value) return '去完成设置'
return '加入待投递列表'
})
/** 底部操作按钮是否显示 icon */
const actionIcon = computed(() => {
if (!choice.value) return false
if (choice.value === 'direct') return false
return true
})
/** 底部操作按钮样式 class */
const actionClass = computed(() => {
if (!choice.value) return 'job-apply-method__action--disabled'
return 'job-apply-method__action--active'
})
// ==================== ====================
/** 加载 AI 助手配置状态 */
async function loadAgentConfigStatus() {
if (agentConfigLoading.value) return
agentConfigLoading.value = true
try {
const res = await fetchAgentConfig()
if (res.code === '0' && res.data && res.data.status === 1) {
agentConfigReady.value = true
} else {
agentConfigReady.value = false
}
} catch {
agentConfigReady.value = false
} finally {
agentConfigLoading.value = false
}
}
/** 打开弹窗时重置状态并加载配置 */
function open() {
choice.value = null
loadAgentConfigStatus()
}
/** 关闭弹窗 */
function close() {
emit('update:modelValue', false)
}
/** 底部操作按钮点击处理 */
async function handleAction() {
if (!choice.value) return
if (choice.value === 'direct') {
//
close()
if (props.sourceUrl) {
window.open(props.sourceUrl, '_blank')
//
const url = props.sourceUrl
const bridge = createChannelBridge({ domain: store.state.bridgeDomain })
bridge.get<{ linkList: string[] }>(store.state.bridgeDomain, 'offerpaiDeliveryLinkList', true)
.then((existing) => {
const linkList: string[] = existing?.linkList || []
if (!linkList.includes(url)) {
linkList.push(url)
}
return bridge.put<{ linkList: string[] }>(store.state.bridgeDomain, 'offerpaiDeliveryLinkList', { linkList }, true)
})
.catch((e) => {
console.error('[JobApplyMethodDialog] 跨域 put 投递链接失败', e)
})
.finally(() => {
bridge.destroy()
})
}
return
}
//
if (!isMember.value) {
//
close()
emit('openMember')
return
}
if (!agentConfigReady.value) {
// AI Agent
close()
router.push('/agent')
return
}
//
try {
const res = await applyJob({ jobId: props.jobId, status: -1 })
if (res.code === '0') {
ElMessage.success('已加入待投递列表')
} else {
ElMessage.error(res.msg || '加入待投递列表失败')
}
} catch {
ElMessage.error('加入待投递列表失败')
}
close()
}
// ==================== ====================
defineExpose({ open })
</script>
<style lang="scss" scoped>
@use '../assets/styles/variables' as *;
/* 组件最外层字号基准 */
.job-apply-method__overlay {
font-size: 0.14rem;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: $overlay-bg;
display: flex;
align-items: center;
justify-content: center;
z-index: 2000;
}
.job-apply-method__dialog {
position: relative;
width: 4.8rem;
background: $bg-white;
border: 1px solid #F0F0F0;
box-shadow: 0 0 0.16rem rgba(0, 0, 0, 0.05);
border-radius: 0.12rem;
padding: 0.28rem 0.32rem 0.32rem;
box-sizing: border-box;
overflow: hidden;
}
.job-apply-method__bg {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(180deg, #EDF9FA 0%, #FFFFFF 20.63%);
border-radius: 0.12rem;
pointer-events: none;
}
.job-apply-method__title {
position: relative;
font-size: 0.18rem;
font-weight: 620;
color: $text-dark;
text-align: center;
margin: 0 0 0.2rem 0;
line-height: 0.24rem;
}
.job-apply-method__close {
position: absolute;
top: 0.24rem;
right: 0.24rem;
width: 0.24rem;
height: 0.24rem;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
z-index: 1;
&:hover {
opacity: 0.7;
}
}
.job-apply-method__options {
position: relative;
display: flex;
gap: 0.16rem;
margin-bottom: 0.24rem;
}
.job-apply-method__card {
position: relative;
flex: 1;
height: 1.06rem;
background: $bg-white;
border: 1px solid #F0F0F0;
border-radius: 0.08rem;
padding: 0.2rem 0;
box-sizing: border-box;
cursor: pointer;
text-align: center;
transition: border-color 0.2s, background 0.2s;
overflow: hidden;
&:hover {
border-color: #D0D0D0;
}
&--active {
border-color: $accent;
background: #EDF9FA;
}
}
.job-apply-method__card-title {
font-size: 0.14rem;
font-weight: 450;
color: #000;
margin: 0;
line-height: 0.19rem;
&--accent {
color: $accent;
}
}
.job-apply-method__card-desc {
font-size: 0.12rem;
font-weight: 330;
color: $text-light;
line-height: 0.2rem;
margin: 0.06rem 0 0 0;
white-space: pre-line;
}
.job-apply-method__card-header {
display: flex;
align-items: center;
justify-content: center;
gap: 0.04rem;
}
.job-apply-method__card-circle {
position: absolute;
width: 0.46rem;
height: 0.46rem;
left: -0.05rem;
bottom: -0.08rem;
background: #AEE6EA;
border-radius: 50%;
opacity: 1;
transition: opacity 0.2s;
&::before,
&::after {
content: '';
position: absolute;
width: 0.04rem;
height: 0.09rem;
background: $bg-white;
border-radius: 0.02rem;
top: 40%;
}
&::before {
left: 42%;
}
&::after {
left: 60%;
}
}
.job-apply-method__action {
position: relative;
width: 100%;
height: 0.35rem;
border: none;
border-radius: 0.08rem;
font-size: 0.14rem;
font-weight: 450;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: background 0.2s, color 0.2s;
&--disabled {
background: #EDF9FA;
color: #AEE6EA;
cursor: not-allowed;
}
&--active {
background: $accent;
color: $bg-white;
&:hover {
background: $accent-hover;
}
}
}
</style>
+2 -39
View File
@@ -17,22 +17,7 @@
</div> </div>
</div> </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> </div>
</template> </template>
@@ -41,7 +26,7 @@
import { ref, computed, onMounted } from 'vue' import { ref, computed, onMounted } from 'vue'
import { useRouter, useRoute } from 'vue-router' import { useRouter, useRoute } from 'vue-router'
import { useStore } from 'vuex' import { useStore } from 'vuex'
import { fetchFavoriteCount, fetchApplyCount, fetchJobCount } from '@/api/jobs' import { fetchFavoriteCount, fetchApplyCount } from '@/api/jobs'
// ==================== Props & Emits ==================== // ==================== 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 favoriteTotal = ref(0)
/** 投递总数 */ /** 投递总数 */
const applyTotal = 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() { async function loadFavoriteCount() {
try { try {
@@ -118,8 +83,6 @@ async function loadApplyCount() {
/** 组件挂载时请求收藏和投递统计 */ /** 组件挂载时请求收藏和投递统计 */
onMounted(() => { onMounted(() => {
//
loadJobCount()
if (isAuthenticated.value) { if (isAuthenticated.value) {
loadFavoriteCount() loadFavoriteCount()
loadApplyCount() loadApplyCount()
+14 -21
View File
@@ -12,21 +12,7 @@
</button> </button>
<h2 class="job-resume-custom-dialog__drawer-title">生成你的岗位专属简历</h2> <h2 class="job-resume-custom-dialog__drawer-title">生成你的岗位专属简历</h2>
</div> </div>
<!-- 下载简历按钮步骤三显示位于步骤指示器上方 -->
<div v-if="currentStep === 4" class="job-resume-custom-dialog__top-download mt10 ml12">
<button class="job-resume-custom-dialog__top-download-btn" @click="toggleDownloadMenu">
<svg viewBox="0 0 16 16" fill="none" class="job-resume-custom-dialog__top-download-icon">
<path d="M8 2v8M8 10l-3-3M8 10l3-3" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M3 12h10" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/>
</svg>
下载简历
</button>
<!-- 下载下拉菜单 -->
<div v-if="showDownloadMenu" class="job-resume-custom-dialog__top-download-menu">
<button class="job-resume-custom-dialog__top-download-option" @click="handleDownload('pdf')">下载PDF</button>
<button class="job-resume-custom-dialog__top-download-option" @click="handleDownload('word')">下载Word</button>
</div>
</div>
<!-- 步骤指示器 --> <!-- 步骤指示器 -->
<div class="job-resume-custom-dialog__steps pt10"> <div class="job-resume-custom-dialog__steps pt10">
<div class="job-resume-custom-dialog__step" :class="{ 'job-resume-custom-dialog__step--active': currentStep === 2 }"> <div class="job-resume-custom-dialog__step" :class="{ 'job-resume-custom-dialog__step--active': currentStep === 2 }">
@@ -48,8 +34,8 @@
<!-- 匹配度标题区域 --> <!-- 匹配度标题区域 -->
<div class="job-resume-custom-dialog__gap-header"> <div class="job-resume-custom-dialog__gap-header">
<div class="job-resume-custom-dialog__gap-left"> <div class="job-resume-custom-dialog__gap-left">
<h3 class="job-resume-custom-dialog__gap-title">你的简历与该岗位<span class="job-resume-custom-dialog__gap-title-highlight">匹配度{{ isLowMatch ? '较' : '较' }}</span></h3> <h3 class="job-resume-custom-dialog__gap-title">你的简历与该岗位任职要求差距<span class="job-resume-custom-dialog__gap-title-highlight">{{ isLowMatch ? '较' : '较' }}</span></h3>
<p class="job-resume-custom-dialog__gap-subtitle">匹配度低于 6.0 分的简历在筛选环节可能会被优先淘汰我们会帮你快速优化提升</p> <p class="job-resume-custom-dialog__gap-subtitle">竞争力低于 6.0 分的简历在筛选环节可能会被优先淘汰我们会帮你快速优化提升</p>
</div> </div>
<!-- 右侧匹配度分数 --> <!-- 右侧匹配度分数 -->
<div class="job-resume-custom-dialog__gap-score-area"> <div class="job-resume-custom-dialog__gap-score-area">
@@ -222,7 +208,7 @@
</svg> </svg>
<span class="job-resume-custom-dialog__match-score">{{ cachedOptimizedScore.toFixed(1) }}</span> <span class="job-resume-custom-dialog__match-score">{{ cachedOptimizedScore.toFixed(1) }}</span>
</div> </div>
<span class="job-resume-custom-dialog__match-label job-resume-custom-dialog__match-label--high">{{ cachedOptimizedScore >= 9 ? '非常匹配' : cachedOptimizedScore >= 6 ? '高匹配度' : '低匹配度' }}</span> <span class="job-resume-custom-dialog__match-label job-resume-custom-dialog__match-label--high">{{ cachedOptimizedScore >= 9 ? '非常匹配' : cachedOptimizedScore >= 6 ? '高匹配度' : '低竞争力' }}</span>
</div> </div>
</div> </div>
<!-- 改写说明卡片外面平铺 --> <!-- 改写说明卡片外面平铺 -->
@@ -327,8 +313,15 @@
<div v-if="currentStep === 4" class="job-resume-custom-dialog__preview-footer mt10 pt30"> <div v-if="currentStep === 4" class="job-resume-custom-dialog__preview-footer mt10 pt30">
<!-- 左侧上一步按钮 --> <!-- 左侧上一步按钮 -->
<button class="job-resume-custom-dialog__footer-prev-btn" @click="goToStep(3)">上一步</button> <button class="job-resume-custom-dialog__footer-prev-btn" @click="goToStep(3)">上一步</button>
<!-- 右侧立即去投递按钮 --> <!-- 右侧下载专属简历按钮 -->
<button class="job-resume-custom-dialog__submit-btn" @click="handleSubmit">立即去投递</button> <div class="job-resume-custom-dialog__submit-wrap">
<button class="job-resume-custom-dialog__submit-btn" @click="toggleDownloadMenu">下载专属简历</button>
<!-- 下载下拉菜单 -->
<div v-if="showDownloadMenu" class="job-resume-custom-dialog__top-download-menu">
<button class="job-resume-custom-dialog__top-download-option" @click="handleDownload('pdf')">下载PDF</button>
<button class="job-resume-custom-dialog__top-download-option" @click="handleDownload('word')">下载Word</button>
</div>
</div>
</div> </div>
</div> </div>
@@ -482,7 +475,7 @@ const missingSkills = computed(() => props.jobInfo.missingSkills || [])
const matchLevelText = computed(() => { const matchLevelText = computed(() => {
const score = props.jobInfo.matchScore const score = props.jobInfo.matchScore
if (score >= 6) return '高匹配度' if (score >= 6) return '高匹配度'
return '低匹配度' return '低竞争力'
}) })
/** 是否为低匹配度(低于6分) */ /** 是否为低匹配度(低于6分) */
+62
View File
@@ -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
View File
@@ -1,152 +1,282 @@
<template> <template>
<!-- <!-- 登录弹窗 使用登录页 step1&2 的样式弹窗形式 -->
该弹窗登录组件已搁置登录功能已改为独立页面 /login (src/views/Login.vue) <Teleport to="body">
如需恢复弹窗登录重新在 App.vue 中引用此组件即可 <div v-if="visible" class="login-dialog-overlay" @click.self="handleClose">
--> <div class="login-dialog-box">
<el-dialog <!-- 关闭按钮 -->
v-model="visible" <div class="login-dialog-box__close" @click="handleClose">
width="480px" <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
:show-close="true" <path d="M5 5L15 15M15 5L5 15" stroke="#999" stroke-width="1.8" stroke-linecap="round"/>
:close-on-click-modal="false" </svg>
: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>
</div> </div>
<el-button <!-- Logo -->
type="primary" <div class="login-dialog-box__logo-row">
size="large" <img src="@/assets/images/logo.png" alt="">
class="login-btn h50 border-ra20" </div>
@click="handleLogin"
>
登录
</el-button>
<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>
</div> </div>
</el-dialog> <!-- 状态提示 -->
<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>
</Teleport>
<!-- 协议预览弹窗 -->
<AgreementPreviewDialog v-model="showAgreementDialog" :code="agreementCode" />
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed } from 'vue' import { ref, computed, nextTick, watch } from 'vue'
import { useStore } from 'vuex' import { useStore } from 'vuex'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { sendSmsCode, smsLogin } from '@/api/auth' import { sendSmsCode, smsLogin } from '@/api/auth'
import AgreementPreviewDialog from '@/components/tools/AgreementPreviewDialog.vue'
const store = useStore() const store = useStore()
const router = useRouter() const router = useRouter()
// ==================== ====================
/** 弹窗可见性,绑定 store.state.showLogin */
const visible = computed(() => store.state.showLogin)
// ==================== ====================
/** 当前步骤:1=输入手机号 2=输入验证码 */
const step = ref(1)
/** 手机号 */
const phone = ref('') const phone = ref('')
const code = ref('') /** OTP 验证码值 */
const otpValue = ref('')
/** 是否已勾选协议 */
const agreedTerms = ref(true)
/** 倒计时秒数 */
const countdown = ref(0) 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 */ /** 发送按钮是否可用(手机号11位 + 勾选协议) */
const countdownText = computed(() => { const canSend = computed(() => phone.value.length === 11 && agreedTerms.value)
const m = Math.floor(countdown.value / 60)
const s = countdown.value % 60
return `${m}:${s.toString().padStart(2, '0')}`
})
const visible = computed({ // ==================== ====================
get: () => store.state.showLogin, watch(visible, (val) => {
set: (val: boolean) => { if (val) {
if (!val) store.dispatch('closeLogin') step.value = 1
}, phone.value = ''
}) otpValue.value = ''
countdown.value = 0
// smsStatusMsg.value = ''
async function sendCode() { smsStatusIsError.value = false
if (!phone.value) { isLoggingIn.value = false
ElMessage.warning('请输入手机号') if (countdownTimer) { clearInterval(countdownTimer); countdownTimer = null }
return
} }
})
// ==================== ====================
/** 关闭弹窗 */
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 { try {
const res = await sendSmsCode(phone.value) const res = await sendSmsCode(phone.value)
if (res.code === '0' && res.data === true) { if (res.code === '0') {
ElMessage.success('验证码发送') smsStatusMsg.value = '验证码发送成功,请输入'
countdown.value = 60 smsStatusIsError.value = false
timer = setInterval(() => { step.value = 2
countdown.value-- startCountdown()
if (countdown.value <= 0 && timer) { nextTick(() => focusOtpInput())
clearInterval(timer) } else if (res.msg && res.msg.includes('请勿重复发送')) {
timer = null smsStatusMsg.value = '验证码还在有效期内,请输入'
} smsStatusIsError.value = false
}, 1000) step.value = 2
startCountdown()
nextTick(() => focusOtpInput())
} else { } else {
ElMessage.error(res.msg || '验证码发送失败') smsStatusMsg.value = res.msg || '验证码发送失败'
smsStatusIsError.value = true
} }
} catch { } catch (err: any) {
// request const msg = err?.response?.data?.msg || '验证码发送失败'
smsStatusMsg.value = msg
smsStatusIsError.value = true
} }
} }
/** /** OTP 输入处理 — 6位自动登录 */
* 登录流程 function handleOtpInput() {
* 1. smsLogin 接口后端成功会 Set-Cookie: Token=xxx otpValue.value = otpValue.value.replace(/\D/g, '').slice(0, 6)
* 2. Cookie 自动保持登录状态不再存 localStorage if (otpValue.value.length === 6) { handleLogin() }
* 3. 关闭弹窗跳转到登录前想去的页面 }
*/
async function handleLogin() { /** 步骤二:再次发送验证码 */
if (!phone.value || !code.value) { async function handleResendCode() {
ElMessage.warning('请输入手机号和验证码') if (isLoggingIn.value || countdown.value > 0) return
return
}
try { 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') { if (res.code !== '0') {
ElMessage.error(res.msg || '登录失败') smsStatusMsg.value = '验证码有误,请重新输入'
smsStatusIsError.value = true
otpValue.value = ''
isLoggingIn.value = false
nextTick(() => focusOtpInput())
return return
} }
// Cookie Token Set-Cookie //
ElMessage.success(`欢迎回来,${res.data?.nick || ''}`) ElMessage.success(`欢迎回来,${res.data?.nick || ''}`)
if (countdownTimer) { clearInterval(countdownTimer); countdownTimer = null }
//
if (timer) {
clearInterval(timer)
timer = null
}
countdown.value = 0 countdown.value = 0
store.commit('SET_AUTHENTICATED', true) store.commit('SET_AUTHENTICATED', true)
//
const redirect = store.state.loginRedirect const redirect = store.state.loginRedirect
store.dispatch('closeLogin') store.dispatch('closeLogin')
if (redirect) { if (redirect) {
router.push(redirect) router.push(redirect)
} }
} catch { } catch {
// request smsStatusMsg.value = '验证码有误,请重新输入'
smsStatusIsError.value = true
otpValue.value = ''
isLoggingIn.value = false
nextTick(() => focusOtpInput())
} }
} }
function handleClose() {
store.dispatch('closeLogin')
}
</script> </script>
+157 -177
View File
@@ -3,17 +3,84 @@
<Teleport to="body"> <Teleport to="body">
<!-- 遮罩层 点击关闭弹窗 --> <!-- 遮罩层 点击关闭弹窗 -->
<div v-if="modelValue" class="member-dialog-overlay" @click="$emit('update:modelValue', false)"> <div v-if="modelValue" class="member-dialog-overlay" @click="$emit('update:modelValue', false)">
<!-- 弹窗主体 阻止点击冒泡 --> <!-- 弹窗主体 左右布局 -->
<div class="member-dialog" @click.stop> <div class="member-dialog" @click.stop>
<!-- 左侧黑金权益栏 -->
<div class="member-dialog__left">
<!-- 装饰线条背景 -->
<div class="member-dialog__left-deco"></div>
<!-- 左栏标题 -->
<h2 class="member-dialog__left-title">Offer派会员专享权益</h2>
<p class="member-dialog__left-subtitle">解锁更多功能让求职更高效</p>
<!-- 权益卡片列表 -->
<div class="member-dialog__benefits">
<div class="member-dialog__benefit-card">
<div class="member-dialog__benefit-icon">
<svg width="14" height="15" viewBox="0 0 13 15" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4.67492 2.29867C5.07359 1.132 6.68559 1.09667 7.15825 2.19267L7.19825 2.29933L7.73625 3.87267C7.85955 4.23349 8.05879 4.56367 8.32054 4.84095C8.58229 5.11822 8.90046 5.33614 9.25359 5.48L9.39825 5.534L10.9716 6.07133C12.1383 6.47 12.1736 8.082 11.0783 8.55467L10.9716 8.59467L9.39825 9.13267C9.03731 9.25588 8.70699 9.45508 8.4296 9.71684C8.1522 9.97859 7.93418 10.2968 7.79025 10.65L7.73625 10.794L7.19892 12.368C6.80025 13.5347 5.18825 13.57 4.71625 12.4747L4.67492 12.368L4.13759 10.7947C4.01437 10.4337 3.81517 10.1034 3.55341 9.82601C3.29166 9.54862 2.97345 9.3306 2.62025 9.18667L2.47625 9.13267L0.902919 8.59533C-0.264415 8.19667 -0.299748 6.58467 0.796252 6.11267L0.902919 6.07133L2.47625 5.534C2.83707 5.4107 3.16726 5.21146 3.44453 4.94971C3.72181 4.68796 3.93973 4.36979 4.08359 4.01667L4.13759 3.87267L4.67492 2.29867ZM11.2703 1.20458e-07C11.395 -1.57315e-07 11.5172 0.0349855 11.623 0.100981C11.7288 0.166977 11.814 0.261335 11.8689 0.373333L11.9009 0.451333L12.1343 1.13533L12.8189 1.36867C12.9439 1.41113 13.0535 1.48974 13.1337 1.59454C13.214 1.69934 13.2613 1.82561 13.2698 1.95735C13.2782 2.08908 13.2473 2.22035 13.1811 2.33453C13.1148 2.4487 13.0161 2.54063 12.8976 2.59867L12.8189 2.63067L12.1349 2.864L11.9016 3.54867C11.859 3.67362 11.7804 3.78313 11.6755 3.86333C11.5707 3.94352 11.4444 3.99079 11.3127 3.99915C11.1809 4.0075 11.0497 3.97656 10.9356 3.91025C10.8214 3.84394 10.7296 3.74524 10.6716 3.62667L10.6396 3.54867L10.4063 2.86467L9.72159 2.63133C9.5966 2.58887 9.48703 2.51026 9.40677 2.40546C9.3265 2.30066 9.27916 2.17439 9.27073 2.04265C9.26231 1.91092 9.29318 1.77965 9.35944 1.66548C9.4257 1.5513 9.52436 1.45937 9.64292 1.40133L9.72159 1.36933L10.4056 1.136L10.6389 0.451333C10.6839 0.319617 10.7689 0.205272 10.8821 0.124333C10.9954 0.0433927 11.1311 -8.35209e-05 11.2703 1.20458e-07Z" fill="#FFE2A1"/>
</svg>
</div>
<div class="member-dialog__benefit-info">
<span class="member-dialog__benefit-name">AI智能投递</span>
<span class="member-dialog__benefit-desc">会员专享</span>
</div>
</div>
<div class="member-dialog__benefit-card">
<div class="member-dialog__benefit-icon">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_2639_2062)">
<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="#FFE2A1"/>
<path d="M10.5 1.88272C10.5 1.76005 10.6287 1.68205 10.724 1.75872C10.8049 1.82405 10.8767 1.90005 10.9393 1.98672L12.948 4.78472C12.9933 4.84872 12.944 4.93139 12.8653 4.93139H10.6667C10.6225 4.93139 10.5801 4.91383 10.5488 4.88257C10.5176 4.85131 10.5 4.80892 10.5 4.76472V1.88272Z" fill="#FFE2A1"/>
</g>
<defs>
<clipPath id="clip0_2639_2062">
<rect width="16" height="16" fill="white"/>
</clipPath>
</defs>
</svg>
</div>
<div class="member-dialog__benefit-info">
<span class="member-dialog__benefit-name">AI优化简历</span>
<span class="member-dialog__benefit-desc">享受无限次数</span>
</div>
</div>
<div class="member-dialog__benefit-card">
<div class="member-dialog__benefit-icon">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.655 1.64404C8.70394 1.5974 8.76182 1.56116 8.82516 1.53753C8.8885 1.5139 8.95597 1.50335 9.0235 1.50654C11.9815 1.64504 14.355 4.01854 14.4935 6.97654C14.4967 7.04411 14.4861 7.11162 14.4624 7.17499C14.4388 7.23835 14.4025 7.29626 14.3558 7.3452C14.3091 7.39414 14.253 7.4331 14.1908 7.45971C14.1286 7.48632 14.0616 7.50004 13.994 7.50004H9C8.86739 7.50004 8.74021 7.44736 8.64645 7.35359C8.55268 7.25982 8.5 7.13264 8.5 7.00004V2.00604C8.49999 1.93835 8.51372 1.87136 8.54036 1.80913C8.567 1.74691 8.60601 1.69074 8.655 1.64404ZM7.345 3.14404C7.39399 3.19074 7.43299 3.24691 7.45964 3.30913C7.48628 3.37136 7.50001 3.43835 7.5 3.50604V7.25004C7.5 7.58156 7.6317 7.8995 7.86612 8.13392C8.10054 8.36834 8.41848 8.50004 8.75 8.50004H12.494C12.5617 8.49996 12.6287 8.51363 12.6909 8.54021C12.7532 8.5668 12.8094 8.60574 12.8561 8.65469C12.9029 8.70364 12.9392 8.76157 12.9629 8.82497C12.9866 8.88837 12.9972 8.95593 12.994 9.02354C12.85 12.0725 10.3335 14.5 7.25 14.5C4.0745 14.5 1.5 11.9255 1.5 8.75004C1.5 5.66604 3.9275 3.14954 6.9765 3.00654C7.04403 3.00335 7.1115 3.0139 7.17484 3.03753C7.23818 3.06116 7.29606 3.0974 7.345 3.14404Z" fill="#FFE2A1"/>
</svg>
</div>
<div class="member-dialog__benefit-info">
<span class="member-dialog__benefit-name">海量岗位数据</span>
<span class="member-dialog__benefit-desc">全网实时更新</span>
</div>
</div>
<div class="member-dialog__benefit-card">
<div class="member-dialog__benefit-icon">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 2C11.4696 2 10.9609 2.21071 10.5858 2.58579C10.2107 2.96086 10 3.46957 10 4C10 4.53043 10.2107 5.03914 10.5858 5.41421C10.9609 5.78929 11.4696 6 12 6C12.5304 6 13.0391 5.78929 13.4142 5.41421C13.7893 5.03914 14 4.53043 14 4C14 3.46957 13.7893 2.96086 13.4142 2.58579C13.0391 2.21071 12.5304 2 12 2Z" fill="#FFE2A1"/>
<path d="M3.33333 2.66663C2.6 2.66663 2 3.26663 2 3.99996V12.6666C2 13.4 2.6 14 3.33333 14H12C12.7333 14 13.3333 13.4 13.3333 12.6666V6.67329C12.9267 6.87329 12.48 6.99996 12 6.99996C10.3467 6.99996 9 5.65329 9 3.99996C9 3.51996 9.12667 3.07329 9.32667 2.66663H3.33333Z" fill="#FFE2A1"/>
</svg>
</div>
<div class="member-dialog__benefit-info">
<span class="member-dialog__benefit-name">校招公告速递</span>
<span class="member-dialog__benefit-desc">名企动态抢先看</span>
</div>
</div>
</div>
</div>
<!-- 左右分割金边 -->
<div class="member-dialog__divider"></div>
<!-- 右侧内容区 -->
<div class="member-dialog__right">
<!-- 顶部标题行 --> <!-- 顶部标题行 -->
<div class="member-dialog__header"> <div class="member-dialog__header">
<div class="member-dialog__header-left">
<h2 class="member-dialog__title">会员中心</h2> <h2 class="member-dialog__title">会员中心</h2>
<p class="member-dialog__subtitle">解锁更多求职功能让投递更高效</p> <div class="member-dialog__header-actions">
</div> <!-- 联系客服 -->
<div class="member-dialog__header-right">
<!-- 联系客服hover显示二维码 -->
<span class="member-dialog__contact-wrap"> <span class="member-dialog__contact-wrap">
<span class="member-dialog__contact">联系客服</span> <span class="member-dialog__contact">联系客服</span>
<div class="member-dialog__contact-qr"> <div class="member-dialog__contact-qr">
@@ -21,6 +88,7 @@
<div class="member-dialog__contact-qr-arrow"></div> <div class="member-dialog__contact-qr-arrow"></div>
</div> </div>
</span> </span>
<!-- 关闭按钮 -->
<span class="member-dialog__close" @click="$emit('update:modelValue', false)"></span> <span class="member-dialog__close" @click="$emit('update:modelValue', false)"></span>
</div> </div>
</div> </div>
@@ -34,150 +102,129 @@
:class="{ 'member-dialog__plan-card--active': selectedPlan === plan.key }" :class="{ 'member-dialog__plan-card--active': selectedPlan === plan.key }"
@click="selectedPlan = plan.key" @click="selectedPlan = plan.key"
> >
<!-- 推荐标 --> <!-- 推荐 -->
<div v-if="plan.recommend" class="member-dialog__plan-recommend"><svg width="10" height="10" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg"> <div v-if="plan.recommend" class="member-dialog__plan-badge">超值推荐</div>
<g clip-path="url(#clip0_180_2594)">
<path d="M0.872709 4.61946C0.867714 4.56192 0.874732 4.50398 0.893317 4.44931C0.911903 4.39464 0.94165 4.34442 0.980672 4.30186C1.01969 4.25929 1.06714 4.2253 1.11999 4.20204C1.17285 4.17878 1.22996 4.16676 1.28771 4.16675H2.08333C2.19384 4.16675 2.29982 4.21065 2.37796 4.28879C2.4561 4.36693 2.5 4.47291 2.5 4.58341V8.54175C2.5 8.65225 2.4561 8.75823 2.37796 8.83637C2.29982 8.91451 2.19384 8.95841 2.08333 8.95841H1.63208C1.5278 8.95844 1.42729 8.91936 1.35042 8.84889C1.27355 8.77842 1.2259 8.68168 1.21688 8.57779L0.872709 4.61946ZM3.75 4.45321C3.75 4.27904 3.85833 4.1232 4.01563 4.04904C4.35917 3.88696 4.94438 3.56133 5.20833 3.12112C5.54854 2.55362 5.61271 1.52841 5.62313 1.29362C5.62458 1.26071 5.62375 1.22779 5.62813 1.19529C5.68458 0.788414 6.46979 1.26362 6.77083 1.76612C6.93438 2.03862 6.95521 2.39675 6.93813 2.67654C6.91958 2.97571 6.83188 3.26466 6.74583 3.55175L6.5625 4.16362H8.82438C8.88875 4.16362 8.95225 4.17853 9.0099 4.20719C9.06754 4.23585 9.11776 4.27748 9.15662 4.32881C9.19547 4.38014 9.22191 4.43977 9.23384 4.50303C9.24578 4.56629 9.2429 4.63145 9.22542 4.69341L8.10667 8.65508C8.08197 8.74245 8.02941 8.81937 7.95698 8.87413C7.88455 8.92888 7.79622 8.95848 7.70542 8.95841H4.16667C4.05616 8.95841 3.95018 8.91451 3.87204 8.83637C3.7939 8.75823 3.75 8.65225 3.75 8.54175V4.45321Z" fill="white" stroke="white" stroke-width="0.833333" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<defs>
<clipPath id="clip0_180_2594">
<rect width="10" height="10" fill="white"/>
</clipPath>
</defs>
</svg>
推荐</div>
<!-- 套餐名称 --> <!-- 套餐名称 -->
<div class="member-dialog__plan-name">{{ plan.name }}</div> <div class="member-dialog__plan-name">{{ plan.name }}</div>
<!-- 价格行 --> <!-- 价格行 -->
<div class="member-dialog__plan-price"> <div class="member-dialog__plan-price">
<span class="member-dialog__plan-price-symbol">¥</span> <span class="member-dialog__plan-price-symbol">¥</span>
<span class="member-dialog__plan-price-num">{{ plan.priceInt }}</span> <span class="member-dialog__plan-price-num">{{ plan.priceInt }}</span>
<span v-if="plan.originalPrice" class="member-dialog__plan-original-price">{{ plan.originalPrice }}</span> <span v-if="plan.originalPrice" class="member-dialog__plan-original">{{ plan.originalPrice }}</span>
</div>
<!-- 标签描述接口tag字段 -->
<div class="member-dialog__plan-daily mt4">{{ plan.tag }}</div>
</div>
</div>
<!-- 会员权益区域 -->
<div class="member-dialog__benefits">
<h3 class="member-dialog__benefits-title">会员权益</h3>
<div class="member-dialog__benefits-cards">
<div class="member-dialog__benefit-card">
<div class="member-dialog__benefit-icon">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
<rect width="24" height="24" rx="12" fill="#EDF9FA"/>
<path d="M7 8h7v2H7V8zm0 3h10v2H7v-2zm0 3h5v2H7v-2z" fill="#52CAD1"/>
</svg>
</div>
<div class="member-dialog__benefit-info">
<span class="member-dialog__benefit-name">AI优化简历</span>
<span class="member-dialog__benefit-desc">享受无限次数</span>
</div>
</div>
<div class="member-dialog__benefit-card">
<div class="member-dialog__benefit-icon">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
<rect width="24" height="24" rx="12" fill="#EDF9FA"/>
<path d="M12 7l1.5 3 3.5.5-2.5 2.5.5 3.5L12 15l-3 1.5.5-3.5L7 10.5l3.5-.5L12 7z" fill="#52CAD1"/>
</svg>
</div>
<div class="member-dialog__benefit-info">
<span class="member-dialog__benefit-name">AI+投递助手</span>
<span class="member-dialog__benefit-desc">会员专享</span>
</div>
</div> </div>
<!-- 标签描述 -->
<div class="member-dialog__plan-tag">{{ plan.tag }}</div>
</div> </div>
</div> </div>
<!-- 步骤一选择支付方式 + 底部应付金额购买按钮未创建订单时显示 -->
<div v-if="!showQrCode" class="member-dialog__step-pay">
<!-- 支付方式选择 --> <!-- 支付方式选择 -->
<div class="member-dialog__payment"> <div class="member-dialog__payment-section">
<span class="member-dialog__payment-label">选择支付方式</span> <span class="member-dialog__payment-label">选择支付方式</span>
<div class="member-dialog__payment-methods"> <div class="member-dialog__payment-methods">
<!-- 微信支付 -->
<div <div
class="member-dialog__payment-item" class="member-dialog__payment-item"
:class="{ 'member-dialog__payment-item--wechat-active': selectedPayment === 'wechat' }" :class="{ 'member-dialog__payment-item--active': selectedPayment === 'wechat' }"
@click="selectedPayment = 'wechat'" @click="selectedPayment = 'wechat'"
> >
<svg width="16" height="16" viewBox="0 0 16 16" fill="none"> <svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14 8c0 3.3-2.7 6-6 6s-6-2.7-6-6 2.7-6 6-6 6 2.7 6 6z" fill="#07C160"/> <g clip-path="url(#clip0_2639_1996)">
<path d="M5.5 6.5c.5-.3 1.2-.1 1.5.4l.5 1 1.5-2c.3-.5 1-.6 1.5-.3.5.3.6 1 .3 1.5l-2.5 3.5c-.2.3-.5.4-.8.4-.3 0-.6-.1-.8-.4L5.2 8c-.3-.5-.2-1.2.3-1.5z" fill="white"/> <path d="M20.8249 10.8533C21.3364 10.8533 21.8348 10.8933 22.32 10.96C21.4282 7 17.3364 4 12.4184 4C6.8446 4 2.32001 7.84 2.32001 12.5733C2.32001 15.3067 3.8282 17.72 6.17574 19.2933L4.85116 22L8.47083 20.4133C9.2446 20.6933 10.0577 20.92 10.9233 21.04C10.8053 20.52 10.7397 19.9867 10.7397 19.4267C10.7266 14.7067 15.2512 10.8533 20.8249 10.8533ZM15.7757 7.85333C15.9411 7.85333 16.1048 7.88644 16.2576 7.95077C16.4103 8.01509 16.5491 8.10938 16.666 8.22824C16.7829 8.3471 16.8757 8.4882 16.9389 8.6435C17.0022 8.7988 17.0348 8.96524 17.0348 9.13333C17.0348 9.30143 17.0022 9.46787 16.9389 9.62317C16.8757 9.77847 16.7829 9.91957 16.666 10.0384C16.5491 10.1573 16.4103 10.2516 16.2576 10.3159C16.1048 10.3802 15.9411 10.4133 15.7757 10.4133C15.4418 10.4133 15.1216 10.2785 14.8855 10.0384C14.6494 9.79838 14.5167 9.47281 14.5167 9.13333C14.5167 8.79386 14.6494 8.46828 14.8855 8.22824C15.1216 7.98819 15.4418 7.85333 15.7757 7.85333ZM9.04788 10.4267C8.71396 10.4267 8.39373 10.2918 8.15762 10.0518C7.92151 9.81172 7.78886 9.48614 7.78886 9.14667C7.78886 8.80719 7.92151 8.48162 8.15762 8.24157C8.39373 8.00152 8.71396 7.86667 9.04788 7.86667C9.38179 7.86667 9.70202 8.00152 9.93814 8.24157C10.1742 8.48162 10.3069 8.80719 10.3069 9.14667C10.3069 9.48614 10.1742 9.81172 9.93814 10.0518C9.70202 10.2918 9.38179 10.4267 9.04788 10.4267Z" fill="#07C160"/>
<path d="M29 19.6638C29 15.9818 25.1942 13 20.5 13C15.8058 13 12 15.9818 12 19.6638C12 23.3457 15.8058 26.3276 20.5 26.3276C21.2691 26.3276 22.0117 26.2239 22.7145 26.0683L27.3027 28L25.7114 24.8885C27.7005 23.6698 29 21.8029 29 19.6638ZM17.9407 19.2489C17.6889 19.2489 17.4428 19.1759 17.2335 19.0392C17.0241 18.9024 16.861 18.708 16.7646 18.4806C16.6683 18.2532 16.643 18.0029 16.6922 17.7615C16.7413 17.5201 16.8625 17.2983 17.0406 17.1243C17.2186 16.9502 17.4454 16.8317 17.6924 16.7836C17.9393 16.7356 18.1953 16.7603 18.4279 16.8545C18.6605 16.9487 18.8593 17.1082 18.9992 17.3129C19.1391 17.5175 19.2137 17.7582 19.2137 18.0043C19.227 18.6914 18.6435 19.2489 17.9407 19.2489ZM23.046 19.2489C22.7084 19.2489 22.3846 19.1178 22.1459 18.8844C21.9071 18.651 21.773 18.3344 21.773 18.0043C21.773 17.6742 21.9071 17.3577 22.1459 17.1243C22.3846 16.8909 22.7084 16.7597 23.046 16.7597C23.3836 16.7597 23.7074 16.8909 23.9462 17.1243C24.1849 17.3577 24.319 17.6742 24.319 18.0043C24.319 18.3344 24.1849 18.651 23.9462 18.8844C23.7074 19.1178 23.3836 19.2489 23.046 19.2489Z" fill="#07C160"/>
</g>
<defs>
<clipPath id="clip0_2639_1996">
<rect width="32" height="32" fill="white"/>
</clipPath>
</defs>
</svg> </svg>
<span class="member-dialog__payment-text member-dialog__payment-text--wechat">微信支付</span> <span class="member-dialog__payment-text">微信支付</span>
<!-- 选中圆圈 -->
<div class="member-dialog__payment-radio">
<div v-if="selectedPayment === 'wechat'" class="member-dialog__payment-radio-inner"></div>
</div> </div>
</div>
<!-- 支付宝支付 -->
<div <div
class="member-dialog__payment-item" class="member-dialog__payment-item"
:class="{ 'member-dialog__payment-item--alipay-active': selectedPayment === 'alipay' }" :class="{ 'member-dialog__payment-item--active': selectedPayment === 'alipay' }"
@click="selectedPayment = 'alipay'" @click="selectedPayment = 'alipay'"
> >
<svg width="16" height="16" viewBox="0 0 16 16" fill="none"> <svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="16" height="16" rx="3" fill="#1677FF"/> <g clip-path="url(#clip0_2639_2002)">
<text x="4" y="12" fill="white" font-size="10" font-weight="bold"></text> <path fill-rule="evenodd" clip-rule="evenodd" d="M17.4127 4.03125C17.5281 4.03125 17.6226 4.12374 17.6226 4.23954V7.82858H25.2414C25.3583 7.82858 25.4488 7.92605 25.4492 8.0431C25.4496 8.79249 25.2601 9.75641 24.8287 9.75641H17.623V12.2974H22.8164C23.0565 12.2974 23.2477 12.5015 23.2303 12.7419L23.2267 12.7891C23.2051 15.6153 22.2698 18.4489 20.7417 20.9244L21.0142 21.0439C23.4297 22.0928 26.0812 22.9505 29.3624 23.0535C29.7298 23.0652 30.0163 23.3761 29.9993 23.7449L29.9929 23.8687C29.8888 25.7353 29.3682 28.0312 27.4979 28.0312C27.2227 28.0312 26.9576 28.014 26.7016 27.9808C24.2628 27.6909 21.8177 26.1933 19.3166 24.526L18.775 24.1633L18.3641 23.8868C17.6939 24.5493 16.9634 25.1524 16.1769 25.6805C15.9834 25.8062 15.779 25.9218 15.5652 26.0275C13.5181 27.2551 11.1213 27.9891 8.45844 27.9895C4.76459 27.9895 2.05466 25.5465 2.00081 22.4783L2 22.3858L2.00081 22.3325C2.05288 19.2583 4.65006 16.8441 8.34497 16.7956L8.45841 16.7949C11.6748 16.7949 14.2916 17.8455 16.9109 19.0864L16.9248 19.0984L16.978 18.9897C17.6426 17.6065 18.1167 16.0147 18.3135 14.2254L8.16359 14.2252C8.10869 14.2252 8.05603 14.2033 8.01716 14.1645C7.97829 14.1256 7.95638 14.0728 7.95622 14.0178C7.95497 13.333 8.14369 12.2974 8.57666 12.2974H13.6419V9.75641H5.99781C5.88084 9.75641 5.79044 9.65934 5.79044 9.54188C5.79 8.83332 5.97956 7.82858 6.41087 7.82858H13.6419V5.79663L13.6428 5.73444C13.6712 4.76532 14.3841 4.03125 17.4127 4.03125ZM8.028 19.3837C5.69156 19.3837 3.76147 20.6349 3.71934 22.3791L3.71875 22.4185L3.72122 22.5201C3.72714 22.6636 3.74369 22.8064 3.77072 22.9475C4.40375 25.9708 10.6506 27.0115 15.1562 21.801L14.9053 21.6531C12.6859 20.3584 10.4044 19.3837 8.028 19.3837Z" fill="#1677FF"/>
</g>
<defs>
<clipPath id="clip0_2639_2002">
<rect width="32" height="32" fill="white"/>
</clipPath>
</defs>
</svg> </svg>
<span class="member-dialog__payment-text member-dialog__payment-text--alipay">支付宝支付</span> <span class="member-dialog__payment-text">支付宝支付</span>
<!-- 选中圆圈 -->
<div class="member-dialog__payment-radio">
<div v-if="selectedPayment === 'alipay'" class="member-dialog__payment-radio-inner"></div>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
<!-- 购买按钮 --> <!-- 底部栏步骤一显示应付金额+购买按钮步骤二显示二维码+已支付 -->
<div class="member-dialog__bottom-bar" :class="{ 'member-dialog__bottom-bar--qrcode': showQrCode }">
<!-- 步骤一应付金额 + 购买按钮 -->
<template v-if="!showQrCode">
<div class="member-dialog__footer-price">
<span class="member-dialog__footer-price-label">应付金额</span>
<span class="member-dialog__footer-price-symbol">¥</span>
<span class="member-dialog__footer-price-num">{{ currentPlan.priceInt }}</span>
</div>
<button <button
class="member-dialog__buy-btn" class="member-dialog__buy-btn"
:class="{ 'member-dialog__buy-btn--active': selectedPayment }" :disabled="!selectedPayment"
@click="handleBuy" @click="handleBuy"
>购买会员</button> >购买会员</button>
</template>
</div> <!-- 步骤二扫码支付区域 -->
</div> <template v-else>
<!-- 二维码 -->
<!-- ==================== 扫码支付弹窗 ==================== --> <div class="member-dialog__qrcode-box">
<div v-if="showQrCode" class="member-dialog-overlay member-dialog-overlay--qrcode" @click="handleCloseQrCode"> <!-- 支付宝iframe 渲染 -->
<div class="member-dialog member-dialog--qrcode" @click.stop>
<!-- 顶部标题行 -->
<div class="member-dialog__header">
<div class="member-dialog__header-left">
<h2 class="member-dialog__title">扫码支付</h2>
</div>
<div class="member-dialog__header-right">
<span class="member-dialog__close" @click="handleCloseQrCode"></span>
</div>
</div>
<!-- 二维码区域 -->
<div class="member-dialog__qrcode-content">
<!-- 支付宝iframe 渲染支付表单 -->
<div v-if="selectedPayment === 'alipay' && paymentFormHtml" class="member-dialog__qrcode-image">
<iframe <iframe
v-if="selectedPayment === 'alipay' && paymentFormHtml"
:srcdoc="paymentFormHtml" :srcdoc="paymentFormHtml"
frameborder="0" frameborder="0"
class="member-dialog__payment-iframe" class="member-dialog__qrcode-iframe qrcode-zhifubao"
scrolling="auto" scrolling="auto"
></iframe> ></iframe>
</div> <!-- 微信qrcode.js 渲染 -->
<!-- 微信使用 qrcode.js 渲染二维码 -->
<div v-if="selectedPayment === 'wechat' && wechatCodeUrl" class="member-dialog__qrcode-image">
<iframe <iframe
v-if="selectedPayment === 'wechat' && wechatCodeUrl"
:srcdoc="wechatQrHtml" :srcdoc="wechatQrHtml"
frameborder="0" frameborder="0"
class="member-dialog__wechat-qr-iframe" class="member-dialog__qrcode-iframe qrcode-wechat"
scrolling="no" scrolling="no"
></iframe> ></iframe>
</div> </div>
<!-- 右侧信息 -->
<!-- 金额和提示 --> <div class="member-dialog__qrcode-info">
<div class="member-dialog__qrcode-price-row"> <div class="member-dialog__qrcode-price-row">
<span class="member-dialog__qrcode-price">¥{{ currentPlan.priceInt }}</span> <span class="member-dialog__qrcode-price-num">¥{{ currentPlan.priceInt }}</span>
<span class="member-dialog__qrcode-tip">扫码完成支付</span> <span class="member-dialog__qrcode-tip">扫码完成支付</span>
</div> </div>
<!-- 协议提示 -->
<p class="member-dialog__qrcode-agreement"> <p class="member-dialog__qrcode-agreement">
开通即代表同意 <a href="javascript:;" @click.prevent="openMemberAgreement">会员服务协议</a> 开通即代表同意 <a href="javascript:;" @click.prevent="openMemberAgreement">会员服务协议</a>
</p> </p>
<!-- 已支付成功按钮 -->
<button class="member-dialog__paid-btn" @click="handleConfirmPaid">我已支付成功</button>
</div>
</template>
</div>
</div> </div>
<!-- 已支付成功按钮 -->
<button class="member-dialog__paid-btn" @click="handleConfirmPaid">已支付成功</button>
</div> </div>
</div> </div>
@@ -203,19 +250,14 @@ const store = useStore()
// ==================== ==================== // ==================== ====================
/** 套餐项类型(包含后端完整数据 + 前端展示字段) */ /** 套餐项类型 */
interface PlanItem { interface PlanItem {
key: string key: string
name: string name: string
/** 整数价格(元),用于显示 */
priceInt: string priceInt: string
/** 划线价(元) */
originalPrice: string originalPrice: string
/** 标签描述(接口tag字段,如"轻量体验"、"低至¥0.56/天" */
tag: string tag: string
/** 是否推荐 */
recommend?: boolean recommend?: boolean
/** 后端原始数据,下单时使用 */
raw: MemberProduct raw: MemberProduct
} }
@@ -224,22 +266,22 @@ interface PlanItem {
/** 当前选中的套餐 key */ /** 当前选中的套餐 key */
const selectedPlan = ref('') const selectedPlan = ref('')
/** 当前选中的支付方式wechat / alipay),默认不选 */ /** 当前选中的支付方式 */
const selectedPayment = ref('') const selectedPayment = ref('')
/** 是否显示扫码支付弹窗 */ /** 是否显示扫码支付区域 */
const showQrCode = ref(false) const showQrCode = ref(false)
/** 支付宝表单 HTML(用于 iframe 渲染) */ /** 支付宝表单 HTML */
const paymentFormHtml = ref('') const paymentFormHtml = ref('')
/** 微信支付 code_url(用于生成二维码) */ /** 微信支付 code_url */
const wechatCodeUrl = ref('') const wechatCodeUrl = ref('')
/** 微信二维码 iframe srcdoc HTML */ /** 微信二维码 iframe srcdoc */
const wechatQrHtml = computed(() => { const wechatQrHtml = computed(() => {
if (!wechatCodeUrl.value) return '' if (!wechatCodeUrl.value) return ''
return `<!DOCTYPE html><html><head><meta charset="UTF-8"><script src="https://cdn.jsdelivr.net/npm/qrcodejs@1.0.0/qrcode.min.js"><\/script><style>body{display:flex;align-items:center;justify-content:center;margin:0;height:100%;}</style></head><body><div id="qrcode"></div><script>new QRCode(document.getElementById("qrcode"),{text:"${wechatCodeUrl.value}",width:210,height:210,colorDark:"#000000",colorLight:"#ffffff",correctLevel:QRCode.CorrectLevel.H});<\/script></body></html>` return `<!DOCTYPE html><html><head><meta charset="UTF-8"><script src="https://cdn.jsdelivr.net/npm/qrcodejs@1.0.0/qrcode.min.js"><\/script><style>body{display:flex;align-items:center;justify-content:center;margin:0;height:100%;}</style></head><body><div id="qrcode"></div><script>new QRCode(document.getElementById("qrcode"),{text:"${wechatCodeUrl.value}",width:160,height:160,colorDark:"#000000",colorLight:"#ffffff",correctLevel:QRCode.CorrectLevel.H});<\/script></body></html>`
}) })
/** 当前订单ID */ /** 当前订单ID */
@@ -248,12 +290,12 @@ const orderId = ref('')
/** 是否正在创建订单 */ /** 是否正在创建订单 */
const creatingOrder = ref(false) const creatingOrder = ref(false)
/** 协议预览弹窗显示状态 */ /** 协议预览弹窗 */
const showAgreementDialog = ref(false) const showAgreementDialog = ref(false)
// ==================== ==================== // ==================== ====================
/** 监听弹窗开关 — 打开时锁定背景滚动,关闭时恢复并重置状态 */ /** 监听弹窗开关 */
watch(() => props.modelValue, (val) => { watch(() => props.modelValue, (val) => {
document.body.style.overflow = val ? 'hidden' : '' document.body.style.overflow = val ? 'hidden' : ''
if (!val) { if (!val) {
@@ -274,18 +316,14 @@ const currentPlan = computed(() => {
// ==================== ==================== // ==================== ====================
/** 套餐列表(从接口获取) */ /** 套餐列表 */
const plans = ref<PlanItem[]>([]) const plans = ref<PlanItem[]>([])
/** 后端商品数据转换为前端套餐项 */ /** 后端商品数据转换 */
function mapProductToPlan(product: MemberProduct): PlanItem { function mapProductToPlan(product: MemberProduct): PlanItem {
// 100
const priceYuan = product.price / 100 const priceYuan = product.price / 100
const originalPriceYuan = product.originalPrice ? product.originalPrice / 100 : 0 const originalPriceYuan = product.originalPrice ? product.originalPrice / 100 : 0
// 使 productName
const name = product.productName const name = product.productName
//
const priceDisplay = Number.isInteger(priceYuan) ? String(priceYuan) : priceYuan.toFixed(2) const priceDisplay = Number.isInteger(priceYuan) ? String(priceYuan) : priceYuan.toFixed(2)
const originalDisplay = originalPriceYuan > priceYuan const originalDisplay = originalPriceYuan > priceYuan
? `¥${Number.isInteger(originalPriceYuan) ? originalPriceYuan : originalPriceYuan.toFixed(2)}` ? `¥${Number.isInteger(originalPriceYuan) ? originalPriceYuan : originalPriceYuan.toFixed(2)}`
@@ -309,7 +347,6 @@ async function loadMemberProducts() {
if (res.data && res.data.length) { if (res.data && res.data.length) {
const sorted = [...res.data].sort((a, b) => a.sortOrder - b.sortOrder) const sorted = [...res.data].sort((a, b) => a.sortOrder - b.sortOrder)
plans.value = sorted.map(mapProductToPlan) plans.value = sorted.map(mapProductToPlan)
//
const featured = plans.value.find(p => p.recommend) const featured = plans.value.find(p => p.recommend)
selectedPlan.value = featured ? featured.key : plans.value[0].key selectedPlan.value = featured ? featured.key : plans.value[0].key
} }
@@ -318,7 +355,6 @@ async function loadMemberProducts() {
} }
} }
/** 组件挂载时加载商品列表 */
onMounted(() => { onMounted(() => {
loadMemberProducts() loadMemberProducts()
}) })
@@ -352,8 +388,6 @@ async function handleBuy() {
wechatCodeUrl.value = '' wechatCodeUrl.value = ''
} }
showQrCode.value = true showQrCode.value = true
//
// confirmPaymentPolling()
} }
} catch (e) { } catch (e) {
console.error('创建订单失败', e) console.error('创建订单失败', e)
@@ -362,67 +396,18 @@ async function handleBuy() {
} }
} }
// /** */ /** 点击"我已支付成功"按钮 */
// let pollTimer: ReturnType<typeof setInterval> | null = null
// /**
// *
// *
// */
// async function confirmPaymentPolling() {
// const startTime = Date.now()
// const timeout = 5 * 60 * 1000 // 5
// const interval = 500
//
// if (pollTimer) {
// clearInterval(pollTimer)
// pollTimer = null
// }
//
// pollTimer = setInterval(async () => {
// try {
// const res = await fetchOrderDetail(orderId.value)
// const status = res.data?.status
//
// if (status === 1) {
// clearInterval(pollTimer!)
// pollTimer = null
// showQrCode.value = false
// ElMessage.success('')
// emit('update:modelValue', false)
// store.dispatch('loadMemberStatus')
// } else if (status === 2 || status === 3) {
// clearInterval(pollTimer!)
// pollTimer = null
// showQrCode.value = false
// ElMessage.error('')
// } else if (Date.now() - startTime >= timeout) {
// clearInterval(pollTimer!)
// pollTimer = null
// showQrCode.value = false
// ElMessage.error('')
// }
// } catch (e) {
// console.error('', e)
// }
// }, interval)
// }
/** 点击"已支付成功"按钮 — 手动查询一次订单状态 */
async function handleConfirmPaid() { async function handleConfirmPaid() {
if (!orderId.value) return if (!orderId.value) return
try { try {
const res = await fetchOrderDetail(orderId.value) const res = await fetchOrderDetail(orderId.value)
const status = res.data?.status const status = res.data?.status
if (status === 1) { if (status === 1) {
//
showQrCode.value = false showQrCode.value = false
ElMessage.success('开通成功') ElMessage.success('开通成功')
emit('update:modelValue', false) emit('update:modelValue', false)
// vuex
store.dispatch('loadMemberStatus') store.dispatch('loadMemberStatus')
} else { } else {
//
ElMessage.warning('未查询到支付订单,请再次确认') ElMessage.warning('未查询到支付订单,请再次确认')
} }
} catch (e) { } catch (e) {
@@ -431,11 +416,6 @@ async function handleConfirmPaid() {
} }
} }
/** 关闭扫码支付弹窗 */
function handleCloseQrCode() {
showQrCode.value = false
}
/** 打开会员服务协议 */ /** 打开会员服务协议 */
function openMemberAgreement() { function openMemberAgreement() {
showAgreementDialog.value = true showAgreementDialog.value = true
+41 -7
View File
@@ -64,6 +64,30 @@
<div class="h1 border-EDF9FA-bottom-2 mrl14 mt14"></div> <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 ref="educationRef" class="profile-page-content__card">
<div class="profile-page-content__card-header profile-page-content__header-hover"> <div class="profile-page-content__card-header profile-page-content__header-hover">
@@ -245,7 +269,7 @@
<div class="h1 border-EDF9FA-bottom-2 mrl14"></div> <div class="h1 border-EDF9FA-bottom-2 mrl14"></div>
<!-- 技能 --> <!-- 技能 -->
<div ref="skillsRef" class="profile-page-content__card" :class="{ 'profile-page-content__card-hover': editingModule !== 'skills' }"> <div ref="skillsRef" class="profile-page-content__card mtb16" :class="{ 'profile-page-content__card-hover': editingModule !== 'skills' }">
<template v-if="editingModule !== 'skills'"> <template v-if="editingModule !== 'skills'">
<div class="profile-page-content__card-header"> <div class="profile-page-content__card-header">
<h3 class="profile-page-content__card-title">技能</h3> <h3 class="profile-page-content__card-title">技能</h3>
@@ -270,7 +294,7 @@
<div class="h1 border-EDF9FA-bottom-2 mrl14"></div> <div class="h1 border-EDF9FA-bottom-2 mrl14"></div>
<!-- 证书 --> <!-- 证书 -->
<div ref="certificateRef" class="profile-page-content__card" :class="{ 'profile-page-content__card-hover': editingModule !== 'certificate' }"> <div ref="certificateRef" class="profile-page-content__card mtb16" :class="{ 'profile-page-content__card-hover': editingModule !== 'certificate' }">
<template v-if="editingModule !== 'certificate'"> <template v-if="editingModule !== 'certificate'">
<div class="profile-page-content__card-header"> <div class="profile-page-content__card-header">
<h3 class="profile-page-content__card-title">证书</h3> <h3 class="profile-page-content__card-title">证书</h3>
@@ -305,6 +329,7 @@ import ResumeExpInlineEdit from '@/components/ResumeExpInlineEdit.vue'
/** Tab 导航列表 */ /** Tab 导航列表 */
const tabList = [ const tabList = [
{ key: 'info', label: '个人信息' }, { key: 'info', label: '个人信息' },
{ key: 'summary', label: '个人概述' },
{ key: 'education', label: '教育经历' }, { key: 'education', label: '教育经历' },
{ key: 'work', label: '工作经历' }, { key: 'work', label: '工作经历' },
{ key: 'internship', label: '实习经历' }, { key: 'internship', label: '实习经历' },
@@ -320,6 +345,7 @@ const activeTab = ref('info')
/** 各模块的 DOM 引用 */ /** 各模块的 DOM 引用 */
const scrollContainerRef = ref<HTMLElement | null>(null) const scrollContainerRef = ref<HTMLElement | null>(null)
const infoRef = ref<HTMLElement | null>(null) const infoRef = ref<HTMLElement | null>(null)
const summaryRef = ref<HTMLElement | null>(null)
const educationRef = ref<HTMLElement | null>(null) const educationRef = ref<HTMLElement | null>(null)
const workRef = ref<HTMLElement | null>(null) const workRef = ref<HTMLElement | null>(null)
const internshipRef = ref<HTMLElement | null>(null) const internshipRef = ref<HTMLElement | null>(null)
@@ -331,6 +357,7 @@ const certificateRef = ref<HTMLElement | null>(null)
/** 模块 key 与 ref 的映射 */ /** 模块 key 与 ref 的映射 */
const sectionRefMap: Record<string, typeof infoRef> = { const sectionRefMap: Record<string, typeof infoRef> = {
info: infoRef, info: infoRef,
summary: summaryRef,
education: educationRef, education: educationRef,
work: workRef, work: workRef,
internship: internshipRef, internship: internshipRef,
@@ -360,7 +387,8 @@ interface ProfileData {
regionCode: string regionCode: string
portfolioUrl: string portfolioUrl: string
wechat?: 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 }> }> 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 }> }> 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 }> }> 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('') const addingModule = ref('')
/** 获取学历文本 */ /** 获取学历文本 — 兼容数字和中文字符串 */
function getDegreeText(studyType: number, degree: number): string { function getDegreeText(studyType: string | number, degree: string | number): string {
const studyTypeText = studyType === 0 ? '全日制' : '非全日制' // 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: '博士' } 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 || [])] } editInitialData.value = { skills: [...(props.profile.skills || [])] }
} else if (module === 'certificate') { } else if (module === 'certificate') {
editInitialData.value = { certificates: [...(props.profile.certificates || [])] } editInitialData.value = { certificates: [...(props.profile.certificates || [])] }
} else if (module === 'summary') {
editInitialData.value = { summary: props.profile.summary || '' }
} }
} }
+13 -2
View File
@@ -7,8 +7,8 @@
<div class="profile-edit-inline__fields-row"> <div class="profile-edit-inline__fields-row">
<div class="profile-edit-inline__field-item"><label class="profile-edit-inline__label">学校</label><input class="profile-edit-inline__input" placeholder="请输入学校名称" v-model="form.school" /></div> <div class="profile-edit-inline__field-item"><label class="profile-edit-inline__label">学校</label><input class="profile-edit-inline__input" placeholder="请输入学校名称" v-model="form.school" /></div>
<div class="profile-edit-inline__field-item"><label class="profile-edit-inline__label">专业</label><input class="profile-edit-inline__input" placeholder="请输入专业名称" v-model="form.major" /></div> <div class="profile-edit-inline__field-item"><label class="profile-edit-inline__label">专业</label><input class="profile-edit-inline__input" placeholder="请输入专业名称" v-model="form.major" /></div>
<div class="profile-edit-inline__field-item"><label class="profile-edit-inline__label">学历</label><select class="profile-edit-inline__input" v-model="form.degree"><option value="大专">大专</option><option value="本科">本科</option><option value="硕士">硕士</option><option value="博士">博士</option></select></div> <div class="profile-edit-inline__field-item"><label class="profile-edit-inline__label">学历</label><select class="profile-edit-inline__input" v-model="form.degree"><option :value="1">大专</option><option :value="2">本科</option><option :value="3">硕士</option><option :value="4">博士</option></select></div>
<div class="profile-edit-inline__field-item"><label class="profile-edit-inline__label">学历类型</label><select class="profile-edit-inline__input" v-model="form.studyType"><option value="全日制">全日制</option><option value="非全日制">非全日制</option></select></div> <div class="profile-edit-inline__field-item"><label class="profile-edit-inline__label">学历类型</label><select class="profile-edit-inline__input" v-model="form.studyType"><option :value="0">全日制</option><option :value="1">非全日制</option></select></div>
<div class="profile-edit-inline__field-item"><label class="profile-edit-inline__label">就读时间</label> <div class="profile-edit-inline__field-item"><label class="profile-edit-inline__label">就读时间</label>
<div class="profile-edit-inline__date-range"> <div class="profile-edit-inline__date-range">
<el-date-picker v-model="form.startDate" type="month" placeholder="开始" format="YYYY-MM" value-format="YYYY-MM" class="profile-edit-inline__date-picker" :teleported="true" popper-class="profile-drawer-date-popper" /> <el-date-picker v-model="form.startDate" type="month" placeholder="开始" format="YYYY-MM" value-format="YYYY-MM" class="profile-edit-inline__date-picker" :teleported="true" popper-class="profile-drawer-date-popper" />
@@ -180,6 +180,17 @@ function initForm() {
aiPolishResult.value = '' aiPolishResult.value = ''
if (props.initialData) { if (props.initialData) {
form.value = { ...props.initialData } form.value = { ...props.initialData }
// degree studyType
if (props.moduleType === 'education') {
const degreeMap: Record<string, number> = { '大专': 1, '本科': 2, '硕士': 3, '博士': 4 }
const studyTypeMap: Record<string, number> = { '全日制': 0, '非全日制': 1 }
if (typeof form.value.degree === 'string') {
form.value.degree = degreeMap[form.value.degree] ?? (Number(form.value.degree) || null)
}
if (typeof form.value.studyType === 'string') {
form.value.studyType = studyTypeMap[form.value.studyType] ?? (Number(form.value.studyType) || null)
}
}
// description // description
if (props.initialData.description?.length) { if (props.initialData.description?.length) {
descriptionText.value = props.initialData.description.map((d: DescriptionParagraph) => d.text).join('\n') descriptionText.value = props.initialData.description.map((d: DescriptionParagraph) => d.text).join('\n')
+1 -1
View File
@@ -105,7 +105,7 @@ watch(() => props.modelValue, async (val) => {
const inviteCode = computed(() => store.state.userInfo?.inviteCode || 'NOVA2026') const inviteCode = computed(() => store.state.userInfo?.inviteCode || 'NOVA2026')
/** 邀请链接地址 */ /** 邀请链接地址 */
const inviteUrl = computed(() => `https://www.offerpai.com.cn/invite?code=${inviteCode.value}`) const inviteUrl = computed(() => `https://www.offerpai.com.cn/?invite_code=${inviteCode.value}`)
/** 复制链接到剪贴板 */ /** 复制链接到剪贴板 */
async function handleCopy() { async function handleCopy() {
+61 -28
View File
@@ -1,10 +1,11 @@
<template> <template>
<div class="side-nav"> <div class="side-nav" :class="{ 'side-nav--narrow': isNarrow }">
<!-- 顶部 Logo --> <!-- 顶部 Logo -->
<div class="side-nav__header"> <div class="side-nav__header">
<!-- 窄版Logologo-min.png -->
<!-- 侧边栏Logo图片 --> <img v-if="isNarrow" src="@/assets/images/logo-min.png" alt="Offer派" class="side-nav__logo-img side-nav__logo-img--narrow" />
<img src="@/assets/images/logo.png" alt="Offer派" class="side-nav__logo-img" /> <!-- 宽版Logo -->
<img v-else src="@/assets/images/logo.png" alt="Offer派" class="side-nav__logo-img" />
</div> </div>
<!-- 主导航 --> <!-- 主导航 -->
@@ -20,13 +21,24 @@
@mouseleave="hoverIndex = -1" @mouseleave="hoverIndex = -1"
> >
<!-- SVG图标hover/active 状态显示选中图标否则显示默认图标 --> <!-- SVG图标hover/active 状态显示选中图标否则显示默认图标 -->
<span <span class="side-nav__item-icon" v-if="index==9">
</span>
<span v-else
class="side-nav__item-icon" class="side-nav__item-icon"
v-html="(isActive(item.name) || hoverIndex === index) && getActiveIcon(index) ? getActiveIcon(index) : item.iconImg" v-html="(isActive(item.name) || hoverIndex === index) && getActiveIcon(index) ? getActiveIcon(index) : item.iconImg"
></span> ></span>
<span class="side-nav__item-label">{{ item.label }}</span> <span class="side-nav__item-label">{{ item.label }}</span>
<!-- 索引0的时候显示"全网"小标签 --> <!-- 索引0的时候显示"全网"小标签 -->
<span v-if="index === 0" class="side-nav__tag-quanwang mt2 ml8">全网</span> <span v-if="index === 0" class="side-nav__tag-quanwang" :class="{ 'side-nav__tag-quanwang--corner': isNarrow, 'mt2 ml8': !isNarrow }">全网</span>
<!-- 索引5的时候显示"公测群"/"加群"小标签 -->
<span v-if="index === 5" class="side-nav__tag-quanwang" :class="{ 'side-nav__tag-quanwang--corner': isNarrow, 'mt2 ml8': !isNarrow }">{{ isNarrow ? '加群' : '公测群' }}</span>
<!-- 索引5 hover时显示公测群二维码 -->
<div v-if="index === 5 && hoverIndex === 5" class="side-nav__qrcode-popup">
<img src="https://offerpie.oss-cn-guangzhou.aliyuncs.com/images/mobile-qcCode.png" alt="公测群二维码" class="side-nav__qrcode-img" />
</div>
<span v-if="item.badge" class="side-nav__badge">{{ item.badge }}</span> <span v-if="item.badge" class="side-nav__badge">{{ item.badge }}</span>
</a> </a>
@@ -35,16 +47,21 @@
<!-- 底部菜单 --> <!-- 底部菜单 -->
<div class="side-nav__footer"> <div class="side-nav__footer">
<!-- 邀请会员卡片 --> <!-- 宽版邀请会员卡片 -->
<div class="side-nav__referral-card cursor-po" @click="showShareDialog = true"> <div v-if="!isNarrow" class="side-nav__referral-card cursor-po" @click="showShareDialog = true">
<!-- 顶部标题区域 --> <!-- 顶部区域标题 + 礼物图片 -->
<div class="side-nav__referral-title">邀请好友送 7 天会员</div> <div class="side-nav__referral-top">
<div class="side-nav__referral-subtitle">好友加入后自动到账</div> <div class="side-nav__referral-top-text">
<!-- 白色面板 --> <div class="side-nav__referral-title">邀请好友<span class="side-nav__referral-title--accent">得会员</span></div>
<div class="side-nav__referral-subtitle">邀1人得7天</div>
</div>
<img src="@/assets/images/nav/gift.png" alt="礼物" class="side-nav__referral-gift" />
</div>
<!-- 白色毛玻璃面板遮住礼物底部 -->
<div class="side-nav__referral-panel"> <div class="side-nav__referral-panel">
<!-- 邀请进度行 --> <!-- 年卡进度行 -->
<div class="side-nav__referral-row"> <div class="side-nav__referral-row">
<span class="side-nav__referral-label">邀请进度</span> <span class="side-nav__referral-label">年卡进度</span>
<span class="side-nav__referral-value">{{ milestoneProgress }}/{{ milestoneTarget }}</span> <span class="side-nav__referral-value">{{ milestoneProgress }}/{{ milestoneTarget }}</span>
</div> </div>
<!-- 进度条 --> <!-- 进度条 -->
@@ -52,13 +69,15 @@
<div class="side-nav__referral-fill" :style="{ width: inviteProgressPercent + '%' }"></div> <div class="side-nav__referral-fill" :style="{ width: inviteProgressPercent + '%' }"></div>
</div> </div>
<!-- 提示文字 --> <!-- 提示文字 -->
<div class="side-nav__referral-tip">再邀 <strong>{{ inviteRemaining }}</strong> 可获得年会员</div> <div class="side-nav__referral-tip">再邀 <strong>{{ inviteRemaining }}</strong> 可获得年</div>
<!-- 分隔线 -->
<div class="side-nav__referral-divider"></div>
<!-- 去邀请按钮 --> <!-- 去邀请按钮 -->
<div class="side-nav__referral-action">去邀请</div> <div class="side-nav__referral-action">去邀请</div>
</div> </div>
</div> </div>
<!-- 窄版邀请图片卡片 -->
<div v-if="isNarrow" class="side-nav__referral-card-narrow cursor-po" @click="showShareDialog = true">
<img src="@/assets/images/nav/referral-card-min.png" alt="邀请有礼" class="side-nav__referral-card-narrow-img" />
</div>
<!-- 用户信息模块 --> <!-- 用户信息模块 -->
<div class="side-nav__user-info-wrapper" v-if="store.state.isAuthenticated && store.state.userInfo"> <div class="side-nav__user-info-wrapper" v-if="store.state.isAuthenticated && store.state.userInfo">
@@ -80,12 +99,15 @@
<div class="side-nav__account-popup-item" @click="handleAccountLogout">退出登录</div> <div class="side-nav__account-popup-item" @click="handleAccountLogout">退出登录</div>
</div> </div>
<!-- 窄版用户信息只显示头像+会员标签 -->
<div v-if="isNarrow" class="side-nav__user-info-narrow" @click.stop="showAccountPopup = !showAccountPopup">
<img src="@/assets/images/nav/avatar-default.png" alt="用户头像" class="side-nav__user-avatar-narrow" />
<!-- 会员标签 -->
<div class="side-nav__user-member-tag" :class="memberTagClass">{{ memberTagText }}</div>
</div>
<!-- 宽版用户信息卡片点击弹出/关闭账户弹窗 -->
<div v-if="!isNarrow" class="side-nav__user-info" :class="{ 'side-nav__user-info--active': showAccountPopup }" @click.stop="showAccountPopup = !showAccountPopup">
<!-- 用户信息卡片点击弹出/关闭账户弹窗 -->
<div class="side-nav__user-info" :class="{ 'side-nav__user-info--active': showAccountPopup }" @click.stop="showAccountPopup = !showAccountPopup">
<div class="side-nav__user-left"> <div class="side-nav__user-left">
<!-- 手机号脱敏显示 --> <!-- 手机号脱敏显示 -->
<div class="side-nav__user-phone">{{ maskedPhone }}</div> <div class="side-nav__user-phone">{{ maskedPhone }}</div>
@@ -269,17 +291,17 @@ import navMentorIcon from '@/assets/images/nav/nav-profile-icon.png'
*/ */
const activeIconSvgs: string[] = [ const activeIconSvgs: string[] = [
// 0 Jobs // 0 Jobs
'<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_1536)"><path d="M7.66664 2.16669C6.78388 2.16674 5.91411 2.37928 5.13081 2.78634C4.34751 3.1934 3.6737 3.78302 3.16631 4.50539C2.65891 5.22775 2.33285 6.06164 2.21567 6.93658C2.09849 7.81152 2.19362 8.70182 2.49305 9.53224C2.79247 10.3627 3.28738 11.1088 3.93596 11.7077C4.58454 12.3065 5.36773 12.7404 6.21936 12.9728C7.07099 13.2051 7.96603 13.229 8.82886 13.0426C9.6917 12.8561 10.497 12.4647 11.1766 11.9014L12.962 13.6867C13.0568 13.775 13.1821 13.8231 13.3117 13.8208C13.4412 13.8185 13.5648 13.766 13.6564 13.6744C13.748 13.5828 13.8005 13.4592 13.8028 13.3297C13.805 13.2002 13.757 13.0748 13.6686 12.98L11.8853 11.196C12.5567 10.3935 12.9853 9.41618 13.1208 8.37866C13.2562 7.34115 13.0929 6.28652 12.65 5.33856C12.2071 4.3906 11.503 3.58865 10.6203 3.02682C9.73759 2.465 8.71296 2.16661 7.66664 2.16669Z" fill="#52CAD1"/></g><defs><clipPath id="clip0_268_1536"><rect width="16" height="16" fill="white"/></clipPath></defs></svg>', '<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 // 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>', '<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 // 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>', '<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 // 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>', '<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 // 5
'<svg width="14" height="13" viewBox="0 0 14 13" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M2.66667 10.6667L1.13333 12.2C0.922223 12.4111 0.680446 12.4584 0.408001 12.342C0.135557 12.2256 -0.000443359 12.0171 1.08578e-06 11.7167V1.33333C1.08578e-06 0.966667 0.130668 0.652889 0.392001 0.392C0.653334 0.131111 0.967112 0.000444444 1.33333 0H12C12.3667 0 12.6807 0.130667 12.942 0.392C13.2033 0.653333 13.3338 0.967111 13.3333 1.33333V9.33333C13.3333 9.7 13.2029 10.014 12.942 10.2753C12.6811 10.5367 12.3671 10.6671 12 10.6667H2.66667ZM6.66667 8.66667C6.85556 8.66667 7.014 8.60267 7.142 8.47467C7.27 8.34667 7.33378 8.18845 7.33333 8C7.33289 7.81156 7.26889 7.65333 7.14133 7.52533C7.01378 7.39733 6.85556 7.33333 6.66667 7.33333C6.47778 7.33333 6.31956 7.39733 6.192 7.52533C6.06445 7.65333 6.00045 7.81156 6 8C5.99956 8.18845 6.06356 8.34689 6.192 8.47533C6.32045 8.60378 6.47867 8.66756 6.66667 8.66667ZM7.142 5.808C7.26956 5.68044 7.33333 5.52222 7.33333 5.33333V2.66667C7.33333 2.47778 7.26933 2.31956 7.14133 2.192C7.01333 2.06444 6.85511 2.00044 6.66667 2C6.47822 1.99956 6.32 2.06356 6.192 2.192C6.064 2.32044 6 2.47867 6 2.66667V5.33333C6 5.52222 6.064 5.68067 6.192 5.80867C6.32 5.93667 6.47822 6.00044 6.66667 6C6.85511 5.99956 7.01356 5.93556 7.142 5.808Z" 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>',
] ]
/** /**
@@ -385,6 +407,14 @@ const mainMenus = computed<MenuItem[]>(() => {
}) })
const showShareDialog = ref(false) const showShareDialog = ref(false)
/** 是否为窄版导航(窗口宽度 < 1440px */
const isNarrow = ref(window.innerWidth < 1440)
/** 监听窗口宽度变化,动态切换窄版/宽版 */
function handleNavResize() {
isNarrow.value = window.innerWidth < 1440
}
const showMessageDialog = ref(false) const showMessageDialog = ref(false)
const showFeedbackDialog = ref(false) const showFeedbackDialog = ref(false)
/** 当前hover的菜单索引,-1表示无hover */ /** 当前hover的菜单索引,-1表示无hover */
@@ -751,10 +781,12 @@ onMounted(() => {
loadUnreadCount() loadUnreadCount()
loadInviteStats() loadInviteStats()
document.addEventListener('click', handleGlobalClick) document.addEventListener('click', handleGlobalClick)
window.addEventListener('resize', handleNavResize)
}) })
onUnmounted(() => { onUnmounted(() => {
document.removeEventListener('click', handleGlobalClick) document.removeEventListener('click', handleGlobalClick)
window.removeEventListener('resize', handleNavResize)
}) })
/** /**
@@ -798,8 +830,9 @@ async function handleNav(item: MenuItem) {
return 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) router.push(item.path)
return return
} }
+37 -3
View File
@@ -16,11 +16,13 @@
</svg> </svg>
</div> </div>
<!-- 下拉面板 --> <!-- 下拉面板Teleport到body避免被父级overflow:hidden裁切 -->
<Teleport to="body">
<div <div
v-if="visible" v-if="visible"
class="industry-selector__panel" class="industry-selector__panel"
:class="{ 'industry-selector__panel--one-col': level === 1 }" :class="{ 'industry-selector__panel--one-col': level === 1 }"
:style="panelStyle"
@click.stop @click.stop
> >
@@ -118,6 +120,7 @@
<button class="industry-selector__btn industry-selector__btn--confirm" @click="handleConfirm">确认</button> <button class="industry-selector__btn industry-selector__btn--confirm" @click="handleConfirm">确认</button>
</div> </div>
</div> </div>
</Teleport>
</div> </div>
</template> </template>
@@ -154,6 +157,8 @@ const props = withDefaults(
hoverStyle?: Record<string, string> hoverStyle?: Record<string, string>
/** 父组件传入的显示文字自定义样式,用于覆盖 max-width 等默认样式 */ /** 父组件传入的显示文字自定义样式,用于覆盖 max-width 等默认样式 */
displayStyle?: Record<string, string> displayStyle?: Record<string, string>
/** 自定义触发按钮默认文字,传入后替换"行业" */
triggerLabel?: string
}>(), }>(),
{ {
maxSelect: 3, maxSelect: 3,
@@ -171,6 +176,27 @@ const store = useStore()
/** 组件根元素引用,用于点击外部关闭判断 */ /** 组件根元素引用,用于点击外部关闭判断 */
const selectorRef = ref<HTMLElement | null>(null) const selectorRef = ref<HTMLElement | null>(null)
/** 面板的固定定位样式(基于trigger按钮位置动态计算) */
const panelStyle = ref<Record<string, string>>({})
/** 计算面板位置(考虑body transform scale */
function updatePanelPosition() {
if (!selectorRef.value) return
const rect = selectorRef.value.getBoundingClientRect()
// bodyscale
const bodyTransform = document.body.style.transform
let scale = 1
const scaleMatch = bodyTransform.match(/scale\(([\d.]+)\)/)
if (scaleMatch) scale = parseFloat(scaleMatch[1]) || 1
// fixedtransformbodyscale
panelStyle.value = {
position: 'fixed',
top: `${(rect.bottom + 6) / scale}px`,
right: `${(window.innerWidth - rect.right) / scale}px`,
'z-index': '2000',
}
}
// ==================== ==================== // ==================== ====================
/** 下拉面板是否可见 */ /** 下拉面板是否可见 */
@@ -202,8 +228,9 @@ const industries = computed<IndustryItem[]>(() => store.state.industries)
/** 已选中行业 ID 集合,用于快速判断某项是否选中 */ /** 已选中行业 ID 集合,用于快速判断某项是否选中 */
const selectedIdSet = computed(() => new Set(selectedItems.value.map(i => i.id))) const selectedIdSet = computed(() => new Set(selectedItems.value.map(i => i.id)))
/** 触发按钮显示文字:无选中显示"行业",有选中则用逗号拼接名称 */ /** 触发按钮显示文字:传了triggerLabel就始终显示它,否则无选中显示默认文字,有选中则用逗号拼接名称 */
const displayText = computed(() => { const displayText = computed(() => {
if (props.triggerLabel) return props.triggerLabel
if (!selectedItems.value.length) return '行业' if (!selectedItems.value.length) return '行业'
return selectedItems.value.map(i => i.name).join('') return selectedItems.value.map(i => i.name).join('')
}) })
@@ -394,12 +421,19 @@ function toggleDropdown() {
if (industries.value.length && !activeParentId.value) { if (industries.value.length && !activeParentId.value) {
activeParentId.value = industries.value[0].id activeParentId.value = industries.value[0].id
} }
//
updatePanelPosition()
} }
} }
/** 点击组件外部时关闭下拉面板 */ /** 点击组件外部时关闭下拉面板 */
function onClickOutside(e: MouseEvent) { function onClickOutside(e: MouseEvent) {
if (selectorRef.value && !selectorRef.value.contains(e.target as Node)) { const target = e.target as Node
// Teleport
if (selectorRef.value && !selectorRef.value.contains(target)) {
// Teleportbody
const panel = document.querySelector('.industry-selector__panel')
if (panel && panel.contains(target)) return
visible.value = false visible.value = false
} }
} }
+33 -3
View File
@@ -16,7 +16,8 @@
</svg> </svg>
</div> </div>
<!-- 下拉面板 --> <!-- 下拉面板Teleport到body避免被父级overflow:hidden裁切 -->
<Teleport to="body">
<div <div
v-if="visible" v-if="visible"
class="job-category-selector__panel" class="job-category-selector__panel"
@@ -24,6 +25,7 @@
'job-category-selector__panel--one-col': level === 1, 'job-category-selector__panel--one-col': level === 1,
'job-category-selector__panel--two-col': level === 2 'job-category-selector__panel--two-col': level === 2
}" }"
:style="panelStyle"
@click.stop @click.stop
> >
@@ -141,6 +143,7 @@
<button class="job-category-selector__btn job-category-selector__btn--confirm" @click="handleConfirm">确认</button> <button class="job-category-selector__btn job-category-selector__btn--confirm" @click="handleConfirm">确认</button>
</div> </div>
</div> </div>
</Teleport>
</div> </div>
</template> </template>
@@ -177,6 +180,8 @@ const props = withDefaults(
hoverStyle?: Record<string, string> hoverStyle?: Record<string, string>
/** 父组件传入的显示文字自定义样式,用于覆盖 max-width 等默认样式 */ /** 父组件传入的显示文字自定义样式,用于覆盖 max-width 等默认样式 */
displayStyle?: Record<string, string> displayStyle?: Record<string, string>
/** 自定义触发按钮默认文字,传入后替换"岗位" */
triggerLabel?: string
}>(), }>(),
{ {
maxSelect: 3, maxSelect: 3,
@@ -194,6 +199,25 @@ const store = useStore()
/** 组件根元素引用,用于点击外部关闭判断 */ /** 组件根元素引用,用于点击外部关闭判断 */
const selectorRef = ref<HTMLElement | null>(null) const selectorRef = ref<HTMLElement | null>(null)
/** 面板的固定定位样式(基于trigger按钮位置动态计算) */
const panelStyle = ref<Record<string, string>>({})
/** 计算面板位置(考虑body transform scale */
function updatePanelPosition() {
if (!selectorRef.value) return
const rect = selectorRef.value.getBoundingClientRect()
const bodyTransform = document.body.style.transform
let scale = 1
const scaleMatch = bodyTransform.match(/scale\(([\d.]+)\)/)
if (scaleMatch) scale = parseFloat(scaleMatch[1]) || 1
panelStyle.value = {
position: 'fixed',
top: `${(rect.bottom + 6) / scale}px`,
right: `${(window.innerWidth - rect.right) / scale}px`,
'z-index': '2000',
}
}
// ==================== ==================== // ==================== ====================
/** 下拉面板是否可见 */ /** 下拉面板是否可见 */
@@ -231,8 +255,9 @@ const categories = computed<JobCategoryItem[]>(() => store.state.jobCategories)
/** 已选中岗位 ID 集合,用于快速判断某项是否选中 */ /** 已选中岗位 ID 集合,用于快速判断某项是否选中 */
const selectedIdSet = computed(() => new Set(selectedItems.value.map(i => i.id))) const selectedIdSet = computed(() => new Set(selectedItems.value.map(i => i.id)))
/** 触发按钮显示文字:无选中显示"岗位",有选中则用逗号拼接名称 */ /** 触发按钮显示文字:传了triggerLabel就始终显示它,否则无选中显示默认文字,有选中则用逗号拼接名称 */
const displayText = computed(() => { const displayText = computed(() => {
if (props.triggerLabel) return props.triggerLabel
if (!selectedItems.value.length) return '岗位' if (!selectedItems.value.length) return '岗位'
return selectedItems.value.map(i => i.name).join('') return selectedItems.value.map(i => i.name).join('')
}) })
@@ -493,12 +518,17 @@ function toggleDropdown() {
if (categories.value.length && !activeLevel1Id.value) { if (categories.value.length && !activeLevel1Id.value) {
activeLevel1Id.value = categories.value[0].id activeLevel1Id.value = categories.value[0].id
} }
//
updatePanelPosition()
} }
} }
/** 点击组件外部时关闭下拉面板 */ /** 点击组件外部时关闭下拉面板 */
function onClickOutside(e: MouseEvent) { function onClickOutside(e: MouseEvent) {
if (selectorRef.value && !selectorRef.value.contains(e.target as Node)) { const target = e.target as Node
if (selectorRef.value && !selectorRef.value.contains(target)) {
const panel = document.querySelector('.job-category-selector__panel')
if (panel && panel.contains(target)) return
visible.value = false visible.value = false
} }
} }
+32 -2
View File
@@ -16,11 +16,13 @@
</svg> </svg>
</div> </div>
<!-- 下拉面板level=2 时两栏较窄level=3 时三栏较宽 --> <!-- 下拉面板Teleport到body避免被父级overflow:hidden裁切 -->
<Teleport to="body">
<div <div
v-if="visible" v-if="visible"
class="region-selector__panel" class="region-selector__panel"
:class="{ 'region-selector__panel--two-col': level === 2 }" :class="{ 'region-selector__panel--two-col': level === 2 }"
:style="panelStyle"
@click.stop @click.stop
> >
@@ -124,6 +126,7 @@
<button class="region-selector__btn region-selector__btn--confirm" @click="handleConfirm">确认</button> <button class="region-selector__btn region-selector__btn--confirm" @click="handleConfirm">确认</button>
</div> </div>
</div> </div>
</Teleport>
</div> </div>
</template> </template>
@@ -162,6 +165,8 @@ const props = withDefaults(
hoverStyle?: Record<string, string> hoverStyle?: Record<string, string>
/** 父组件传入的显示文字自定义样式,用于覆盖 max-width 等默认样式 */ /** 父组件传入的显示文字自定义样式,用于覆盖 max-width 等默认样式 */
displayStyle?: Record<string, string> displayStyle?: Record<string, string>
/** 自定义触发按钮默认文字,传入后替换"城市" */
triggerLabel?: string
}>(), }>(),
{ {
level: 2, level: 2,
@@ -177,6 +182,25 @@ const store = useStore()
/** 组件根元素引用,用于点击外部关闭判断 */ /** 组件根元素引用,用于点击外部关闭判断 */
const selectorRef = ref<HTMLElement | null>(null) const selectorRef = ref<HTMLElement | null>(null)
/** 面板的固定定位样式(基于trigger按钮位置动态计算) */
const panelStyle = ref<Record<string, string>>({})
/** 计算面板位置(考虑body transform scale */
function updatePanelPosition() {
if (!selectorRef.value) return
const rect = selectorRef.value.getBoundingClientRect()
const bodyTransform = document.body.style.transform
let scale = 1
const scaleMatch = bodyTransform.match(/scale\(([\d.]+)\)/)
if (scaleMatch) scale = parseFloat(scaleMatch[1]) || 1
panelStyle.value = {
position: 'fixed',
top: `${(rect.bottom + 6) / scale}px`,
right: `${(window.innerWidth - rect.right) / scale}px`,
'z-index': '2000',
}
}
// ==================== ==================== // ==================== ====================
/** 下拉面板是否可见 */ /** 下拉面板是否可见 */
@@ -207,6 +231,7 @@ const selectedCodeSet = computed(() => new Set(selectedItems.value.map(i => i.co
/** 触发按钮显示文字 */ /** 触发按钮显示文字 */
const displayText = computed(() => { const displayText = computed(() => {
if (props.triggerLabel) return props.triggerLabel
if (!selectedItems.value.length) return '城市' if (!selectedItems.value.length) return '城市'
return selectedItems.value.map(i => i.name).join('') return selectedItems.value.map(i => i.name).join('')
}) })
@@ -345,12 +370,17 @@ function toggleDropdown() {
if (regions.value.length && !activeProvinceCode.value) { if (regions.value.length && !activeProvinceCode.value) {
activeProvinceCode.value = regions.value[0].code activeProvinceCode.value = regions.value[0].code
} }
//
updatePanelPosition()
} }
} }
/** 点击组件外部时关闭下拉面板 */ /** 点击组件外部时关闭下拉面板 */
function onClickOutside(e: MouseEvent) { function onClickOutside(e: MouseEvent) {
if (selectorRef.value && !selectorRef.value.contains(e.target as Node)) { const target = e.target as Node
if (selectorRef.value && !selectorRef.value.contains(target)) {
const panel = document.querySelector('.region-selector__panel')
if (panel && panel.contains(target)) return
visible.value = false visible.value = false
} }
} }
+6 -1
View File
@@ -13,7 +13,7 @@ const remAdaptPlugin: Plugin = {
// 检测是否为移动端/小屏 // 检测是否为移动端/小屏
const isMobile = (): boolean => { const isMobile = (): boolean => {
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)
|| window.innerWidth < 1200
} }
// 重新计算缩放 // 重新计算缩放
@@ -70,6 +70,8 @@ const remAdaptPlugin: Plugin = {
// 用 scale 直接驱动(scale=0.625 时满宽4remscale 越小宽度越小) // 用 scale 直接驱动(scale=0.625 时满宽4remscale 越小宽度越小)
const chatWidth = 1.0 + (4.0 - 1.0) * scale // scale=1→4rem, scale=0.5→2.5rem, scale=0.3→1.9rem const chatWidth = 1.0 + (4.0 - 1.0) * scale // scale=1→4rem, scale=0.5→2.5rem, scale=0.3→1.9rem
docEl.style.setProperty('--chat-width', Math.max(chatWidth, 1.0).toFixed(2) + 'rem') docEl.style.setProperty('--chat-width', Math.max(chatWidth, 1.0).toFixed(2) + 'rem')
// 小屏导航宽度CSS变量
docEl.style.setProperty('--nav-width', '0.8rem')
// body 高度 = 视口高度 / scale,使其缩放后刚好等于视口高度 // body 高度 = 视口高度 / scale,使其缩放后刚好等于视口高度
// overflow-y: auto 让内容在 body 内部滚动 // overflow-y: auto 让内容在 body 内部滚动
@@ -94,6 +96,9 @@ const remAdaptPlugin: Plugin = {
docEl.style.overflow = '' docEl.style.overflow = ''
docEl.style.setProperty('--app-height', '100vh') docEl.style.setProperty('--app-height', '100vh')
docEl.style.setProperty('--chat-width', '3.6rem') docEl.style.setProperty('--chat-width', '3.6rem')
// 根据窗口宽度设置导航栏宽度CSS变量
const navWidth = window.innerWidth < 1440 ? '0.8rem' : '2.2rem'
docEl.style.setProperty('--nav-width', navWidth)
} }
} }
+2 -1
View File
@@ -15,6 +15,7 @@ const componentMap: Record<string, () => Promise<any>> = {
'Profile': () => import('@/views/Profile.vue'), 'Profile': () => import('@/views/Profile.vue'),
'Resume': () => import('@/views/Resume.vue'), 'Resume': () => import('@/views/Resume.vue'),
'Mentor': () => import('@/views/Mentor.vue'), 'Mentor': () => import('@/views/Mentor.vue'),
'Announcement': () => import('@/views/Announcement.vue'),
} }
/** /**
@@ -23,7 +24,7 @@ const componentMap: Record<string, () => Promise<any>> = {
* component * component
* /jobs * /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[] { export function buildDynamicRoutes(menus: MenuItemRaw[]): RouteRecordRaw[] {
const routes: RouteRecordRaw[] = [] const routes: RouteRecordRaw[] = []
+51 -27
View File
@@ -3,6 +3,9 @@ import type { RouteRecordRaw } from 'vue-router'
import store from '@/stores' import store from '@/stores'
import { checkLogin } from '@/api/auth' import { checkLogin } from '@/api/auth'
import { fetchMemberStatus } from '@/api/member' import { fetchMemberStatus } from '@/api/member'
// 【已禁用】强制简历上传机制相关引用
// import { fetchProfile } from '@/api/profile'
// import { ENABLE_FORCE_RESUME_UPLOAD } from '@/business.config'
/** /**
* *
@@ -11,11 +14,11 @@ const staticRoutes: RouteRecordRaw[] = [
{ path: '/', name: 'Home', component: () => import('@/views/Home.vue') }, { path: '/', name: 'Home', component: () => import('@/views/Home.vue') },
{ path: '/m', name: 'HomeMobile', component: () => import('@/views/HomeMobile.vue') }, { path: '/m', name: 'HomeMobile', component: () => import('@/views/HomeMobile.vue') },
{ path: '/login', name: 'Login', component: () => import('@/views/Login.vue') }, { path: '/login', name: 'Login', component: () => import('@/views/Login.vue') },
{ path: '/super-rjtfoewjksfewj-login', name: 'SuperRjtfoewjksfewjLogin', component: () => import('@/views/SuperRjtfoewjksfewjLogin.vue') },
{ {
path: '/jobs', path: '/jobs',
name: 'Jobs', name: 'Jobs',
component: () => import('@/views/Jobs.vue'), component: () => import('@/views/Jobs.vue'),
meta: { requiresAuth: true },
}, },
{ {
path: '/resume/:id', path: '/resume/:id',
@@ -33,6 +36,16 @@ const staticRoutes: RouteRecordRaw[] = [
name: 'MentorDetail', name: 'MentorDetail',
component: () => import('@/views/MentorDetail.vue'), component: () => import('@/views/MentorDetail.vue'),
}, },
{
path: '/announcement/:id',
name: 'AnnouncementDetail',
component: () => import('@/views/AnnouncementDetail.vue'),
},
{
path: '/privacy',
name: 'PrivacyPolicy',
component: () => import('@/views/PrivacyPolicy.vue'),
},
] ]
const router = createRouter({ const router = createRouter({
@@ -98,62 +111,73 @@ router.beforeEach(async (to, _from, next) => {
// 每次页面切换时静默刷新路由菜单数据(更新权限状态),不阻塞导航 // 每次页面切换时静默刷新路由菜单数据(更新权限状态),不阻塞导航
store.dispatch('refreshDynamicMenus') store.dispatch('refreshDynamicMenus')
// 已登录用户访问登录页,直接跳转首页 // 已登录用户访问登录页,如果带 resumeUpload=1 参数则放行(简历上传流程),否则跳转首页
if (to.name === 'Login' && store.state.isAuthenticated) { if (to.name === 'Login' && store.state.isAuthenticated) {
if (to.query.resumeUpload === '1') {
next()
} else {
next({ name: 'Home' }) next({ name: 'Home' })
}
return 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 { try {
const res = await checkLogin() const res = await checkLogin()
lastCheckTime = Date.now() lastCheckTime = Date.now()
if (res.code === '0' && res.data === true) { if (res.code === '0' && res.data === true) {
// Cookie 有效,同步前端状态并放行 // Cookie 有效,同步前端状态
store.commit('SET_AUTHENTICATED', true) store.commit('SET_AUTHENTICATED', true)
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 页面需要会员权限 — 等待会员信息加载后判断 // Agent 页面需要额外的会员权限判断
if (to.name === 'Agent') { if (to.name === 'Agent') {
try { try {
// 确保会员信息已加载到 store
if (!store.state.memberStatus) { if (!store.state.memberStatus) {
const res = await fetchMemberStatus() const memberRes = await fetchMemberStatus()
if (res.code === '0' && res.data) { if (memberRes.code === '0' && memberRes.data) {
store.commit('SET_MEMBER_STATUS', res.data) store.commit('SET_MEMBER_STATUS', memberRes.data)
} }
} }
const status = store.state.memberStatus const status = store.state.memberStatus
// 试用会员(memberType=2)或正式会员(memberType=1)才能进入
if (status && status.isMember && (status.memberType === 1 || status.memberType === 2)) { if (status && status.isMember && (status.memberType === 1 || status.memberType === 2)) {
next() next()
} else { } else {
// 免费用户 — 弹出会员权限拦截弹窗,阻止跳转
store.commit('SET_SHOW_MEMBER_ACCESS_DIALOG', true) store.commit('SET_SHOW_MEMBER_ACCESS_DIALOG', true)
next(false) next(false)
} }
} catch { } catch {
// 接口异常时弹窗提示,不放行
store.commit('SET_SHOW_MEMBER_ACCESS_DIALOG', true) store.commit('SET_SHOW_MEMBER_ACCESS_DIALOG', true)
next(false) next(false)
} }
return return
} }
// 非鉴权路由 — 静默同步登录状态(节流:5 分钟内不重复请求) next()
} else {
// 未登录或 Cookie 失效 — 弹出登录弹窗,阻止导航
store.commit('SET_AUTHENTICATED', false)
store.dispatch('openLogin', to.fullPath)
next(false)
}
} catch {
// 请求异常(网络错误等),也视为未登录 — 弹登录弹窗
store.commit('SET_AUTHENTICATED', false)
store.dispatch('openLogin', to.fullPath)
next(false)
}
return
}
// 白名单页面 — 静默同步登录状态(节流:5 分钟内不重复请求)
if (Date.now() - lastCheckTime > CHECK_INTERVAL) { if (Date.now() - lastCheckTime > CHECK_INTERVAL) {
checkLogin() checkLogin()
.then((res) => { .then((res) => {
+3 -2
View File
@@ -12,6 +12,7 @@ import type { UserInfo } from '@/api/auth'
import { fetchMemberStatus } from '@/api/member' import { fetchMemberStatus } from '@/api/member'
import type { MemberStatus } from '@/api/member' import type { MemberStatus } from '@/api/member'
import { createChannelBridge } from '@/utils/channelBridge' import { createChannelBridge } from '@/utils/channelBridge'
import { PLUGIN_TARGET_VERSION } from '@/business.config'
/** 招聘分类选项:label → 接口参数 recruitCategory */ /** 招聘分类选项:label → 接口参数 recruitCategory */
export const JOB_TYPE_OPTIONS: { label: string; value: number }[] = [ export const JOB_TYPE_OPTIONS: { label: string; value: number }[] = [
@@ -212,13 +213,13 @@ export default createStore<RootState>({
inviteCode: '', inviteCode: '',
showMemberAccessDialog: false, showMemberAccessDialog: false,
/** 通信桥接组件的域名标识(用于 BroadcastChannel 频道隔离) */ /** 通信桥接组件的域名标识(用于 BroadcastChannel 频道隔离) */
bridgeDomain: 'test.offerpai.com.cn', bridgeDomain: 'offerpai.com.cn',
/** 浏览器插件是否在线 */ /** 浏览器插件是否在线 */
plugIsOnline: false, plugIsOnline: false,
/** 插件当前使用版本 */ /** 插件当前使用版本 */
plugUseVersion: '', plugUseVersion: '',
/** 插件目标版本 */ /** 插件目标版本 */
plugTargetVersion: '0.1.0', plugTargetVersion: PLUGIN_TARGET_VERSION,
/** 插件是否是最新版本 */ /** 插件是否是最新版本 */
isLatestPlugin: true, isLatestPlugin: true,
}, },
+2 -5
View File
@@ -1,8 +1,5 @@
import axios from 'axios' import axios from 'axios'
/** 默认 TokenNova 对话接口使用) */
export const DEFAULT_TOKEN = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOjIwMzUyNTM4OTg5MTk4NzA0NjUsInV1SWQiOiI2MmQ5MDE2NTcyNzY0ZmNjODNjZTIyYjRjODA5ZmU5MiJ9.eE-Q5rio5J5kxkS-XPYdmk-1Tgvg6kj6NGoKWMFNU14'
/** /**
* AI axios * AI axios
* AI * AI
@@ -17,7 +14,7 @@ const aiService = axios.create({
* JS Cookie header Token header * JS Cookie header Token header
*/ */
aiService.interceptors.request.use((config) => { aiService.interceptors.request.use((config) => {
// config.headers['Token'] = DEFAULT_TOKEN
return config return config
}) })
@@ -110,7 +107,7 @@ export interface NovaChatResponse {
export function sendNovaChat(params: NovaChatParams) { export function sendNovaChat(params: NovaChatParams) {
return aiService.post<AiResult<NovaChatResponse>>('/nova-chat/chat', params, { return aiService.post<AiResult<NovaChatResponse>>('/nova-chat/chat', params, {
headers: { headers: {
Token: DEFAULT_TOKEN,
}, },
}).then(res => res.data) }).then(res => res.data)
} }
+64 -18
View File
@@ -45,8 +45,8 @@
* ------------------------------------------------------------ * ------------------------------------------------------------
* | (name) | | | * | (name) | | |
* | ----------------------------- | -------------- | --------------------------------- | * | ----------------------------- | -------------- | --------------------------------- |
* * | offerpieBrowserPlugUsage | 使 | { usage: string, version: string } | * | offerpieBrowserPlugUsage | 使 | { usage: string, version: string } |
* * | offerpaiDeliveryLinkList | | , { linkList: string[] },useCsl:true | * | offerpaiDeliveryLinkList | | , { linkList: string[] } |
* ============================================================ * ============================================================
*/ */
@@ -108,8 +108,6 @@ export interface ChannelBridgeOptions {
/** IndexedDB 数据库名称(含固定雪花ID保证唯一性) */ /** IndexedDB 数据库名称(含固定雪花ID保证唯一性) */
const DB_NAME = 'offerpie_comm_7394028156183472' const DB_NAME = 'offerpie_comm_7394028156183472'
/** IndexedDB 版本号 */
const DB_VERSION = 1
/** get 方法 BroadcastChannel 超时时间(毫秒) */ /** get 方法 BroadcastChannel 超时时间(毫秒) */
const GET_TIMEOUT_MS = 60 const GET_TIMEOUT_MS = 60
/** emit 方法节流写库间隔(毫秒) */ /** emit 方法节流写库间隔(毫秒) */
@@ -235,10 +233,30 @@ function _getCslProxyChannel(domain: string): BroadcastChannel {
// ============ IndexedDB 操作 ============ // ============ IndexedDB 操作 ============
/** 打开/创建 IndexedDB 数据库,按 domain 动态创建 ObjectStore */ /** 数据库连接缓存(避免重复打开和版本冲突) */
const _dbCache = new Map<string, Promise<IDBDatabase>>()
/** 打开/创建 IndexedDB 数据库,按 domain 动态创建 ObjectStore,带连接缓存防并发冲突 */
function openDB(storeName: string): Promise<IDBDatabase> { function openDB(storeName: string): Promise<IDBDatabase> {
return new Promise((resolve, reject) => { // 如果已有进行中或已完成的连接,直接复用
const request = indexedDB.open(DB_NAME, DB_VERSION) if (_dbCache.has(storeName)) {
return _dbCache.get(storeName)!.then(db => {
// 检查连接是否还有效(可能被 close 了)
try {
// 尝试创建事务验证连接有效性
if (db.objectStoreNames.contains(storeName)) {
return db
}
} catch {
// 连接已关闭,清除缓存重新打开
_dbCache.delete(storeName)
}
return openDB(storeName)
})
}
const promise = new Promise<IDBDatabase>((resolve, reject) => {
const request = indexedDB.open(DB_NAME)
request.onupgradeneeded = () => { request.onupgradeneeded = () => {
const db = request.result const db = request.result
if (!db.objectStoreNames.contains(storeName)) { if (!db.objectStoreNames.contains(storeName)) {
@@ -248,9 +266,10 @@ function openDB(storeName: string): Promise<IDBDatabase> {
} }
request.onsuccess = () => { request.onsuccess = () => {
const db = request.result const db = request.result
// 如果 store 不存在(版本没升级的情况),关闭后升版本重建
if (!db.objectStoreNames.contains(storeName)) { if (!db.objectStoreNames.contains(storeName)) {
// store 不存在,需要升版本。关闭当前连接再重开
db.close() db.close()
_dbCache.delete(storeName)
const version = db.version + 1 const version = db.version + 1
const req2 = indexedDB.open(DB_NAME, version) const req2 = indexedDB.open(DB_NAME, version)
req2.onupgradeneeded = () => { req2.onupgradeneeded = () => {
@@ -261,37 +280,60 @@ function openDB(storeName: string): Promise<IDBDatabase> {
} }
} }
req2.onsuccess = () => resolve(req2.result) req2.onsuccess = () => resolve(req2.result)
req2.onerror = () => reject(req2.error) req2.onerror = () => { _dbCache.delete(storeName); reject(req2.error) }
req2.onblocked = () => {
// 其他标签页占用数据库导致升级被阻塞,超时后放弃
console.warn('[ChannelBridge] IndexedDB upgrade blocked, retrying...')
_dbCache.delete(storeName)
reject(new Error('IndexedDB upgrade blocked'))
}
} else { } else {
resolve(db) resolve(db)
} }
} }
request.onerror = () => reject(request.error) request.onerror = () => { _dbCache.delete(storeName); reject(request.error) }
request.onblocked = () => {
console.warn('[ChannelBridge] IndexedDB open blocked')
_dbCache.delete(storeName)
reject(new Error('IndexedDB open blocked'))
}
}) })
_dbCache.set(storeName, promise)
return promise
} }
/** 写入或更新一条记录到 IndexedDB */ /** 写入或更新一条记录到 IndexedDB */
async function dbPut(storeName: string, record: DBRecord): Promise<void> { async function dbPut(storeName: string, record: DBRecord): Promise<void> {
try {
const db = await openDB(storeName) const db = await openDB(storeName)
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const tx = db.transaction(storeName, 'readwrite') const tx = db.transaction(storeName, 'readwrite')
const store = tx.objectStore(storeName) const store = tx.objectStore(storeName)
store.put(record) store.put(record)
tx.oncomplete = () => { db.close(); resolve() } tx.oncomplete = () => resolve()
tx.onerror = () => { db.close(); reject(tx.error) } tx.onerror = () => reject(tx.error)
}) })
} catch (err) {
console.warn('[ChannelBridge] dbPut failed:', err)
}
} }
/** 从 IndexedDB 读取一条记录 */ /** 从 IndexedDB 读取一条记录 */
async function dbGet(storeName: string, name: string): Promise<DBRecord | null> { async function dbGet(storeName: string, name: string): Promise<DBRecord | null> {
try {
const db = await openDB(storeName) const db = await openDB(storeName)
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const tx = db.transaction(storeName, 'readonly') const tx = db.transaction(storeName, 'readonly')
const store = tx.objectStore(storeName) const store = tx.objectStore(storeName)
const req = store.get(name) const req = store.get(name)
req.onsuccess = () => { db.close(); resolve(req.result || null) } req.onsuccess = () => resolve(req.result || null)
req.onerror = () => { db.close(); reject(req.error) } req.onerror = () => reject(req.error)
}) })
} catch (err) {
console.warn('[ChannelBridge] dbGet failed:', err)
return null
}
} }
// ============ 核心:创建通信桥接实例 ============ // ============ 核心:创建通信桥接实例 ============
@@ -499,19 +541,23 @@ export function createChannelBridge(options: ChannelBridgeOptions) {
} }
// 默认模式:IndexedDB // 默认模式:IndexedDB
// 先尝试获取 createTime(从暂存或数据库 // 先尝试获取 createTime(从暂存或内存缓存
let createTime: string let createTime: string
const pending = emitPending.get(key) const pending = emitPending.get(key)
if (pending) { if (pending) {
createTime = pending.createTime createTime = pending.createTime
} else { } else {
const existing = await dbGet(targetDomain, name) // 只在首次查库获取 createTime,失败就用当前时间(不阻塞后续 emit)
let existing: DBRecord | null = null
try {
existing = await dbGet(targetDomain, name)
} catch {}
createTime = existing?.createTime || now createTime = existing?.createTime || now
} }
const updateTime = now const updateTime = now
const record: DBRecord = { name, data, createTime, updateTime } const record: DBRecord = { name, data, createTime, updateTime }
// 暂存最新数据 // 暂存最新数据(不再 delete,保证后续 emit 总能命中缓存不走 dbGet)
emitPending.set(key, record) emitPending.set(key, record)
// BroadcastChannel 每次都广播(保证 watch 实时性) // BroadcastChannel 每次都广播(保证 watch 实时性)
@@ -525,7 +571,7 @@ export function createChannelBridge(options: ChannelBridgeOptions) {
emitTimers.delete(key) emitTimers.delete(key)
const latestRecord = emitPending.get(key) const latestRecord = emitPending.get(key)
if (latestRecord) { if (latestRecord) {
emitPending.delete(key) // 注意:不再 delete emitPending,让下次 emit 继续命中缓存
await dbPut(targetDomain, latestRecord) await dbPut(targetDomain, latestRecord)
} }
}, EMIT_THROTTLE_MS)) }, EMIT_THROTTLE_MS))

Some files were not shown because too many files have changed in this diff Show More