Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1e103627f8 | ||
|
|
8b9ebd6ddc | ||
|
|
5abd6d27d6 | ||
|
|
bb7a8c55b2 | ||
|
|
a0f01dd697 | ||
|
|
58a027296a | ||
|
|
010a80be81 | ||
|
|
6c03793daf | ||
|
|
33ecccdcb0 | ||
|
|
d429bae98a | ||
|
|
0b9f2e7333 | ||
|
|
db46766d48 | ||
|
|
27d2592d06 | ||
|
|
c81c9b79dd | ||
|
|
5f4aab8d04 | ||
|
|
15971bffd9 | ||
|
|
bf84da7783 | ||
|
|
0911760e27 | ||
|
|
66026f211c | ||
|
|
7abe158dbb | ||
|
|
aed2535ab2 | ||
|
|
5f723e5573 |
@@ -13,6 +13,7 @@ declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
AgentApplyProgress: typeof import('./src/components/AgentApplyProgress.vue')['default']
|
||||
AgentApplyProgressPanel: typeof import('./src/components/AgentApplyProgressPanel.vue')['default']
|
||||
AgentBrowserInstallGuide: typeof import('./src/components/AgentBrowserInstallGuide.vue')['default']
|
||||
AgentChatJobList: typeof import('./src/components/AgentChatJobList.vue')['default']
|
||||
AgentJobPreviewPanel: typeof import('./src/components/AgentJobPreviewPanel.vue')['default']
|
||||
AgentMatchJobAdd: typeof import('./src/components/AgentMatchJobAdd.vue')['default']
|
||||
@@ -30,17 +31,15 @@ declare module 'vue' {
|
||||
ElDialog: typeof import('element-plus/es')['ElDialog']
|
||||
ElIcon: typeof import('element-plus/es')['ElIcon']
|
||||
ElInput: typeof import('element-plus/es')['ElInput']
|
||||
ElOption: typeof import('element-plus/es')['ElOption']
|
||||
ElRadio: typeof import('element-plus/es')['ElRadio']
|
||||
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
|
||||
ElSelect: typeof import('element-plus/es')['ElSelect']
|
||||
ElSkeleton: typeof import('element-plus/es')['ElSkeleton']
|
||||
ElSkeletonItem: typeof import('element-plus/es')['ElSkeletonItem']
|
||||
ElSwitch: typeof import('element-plus/es')['ElSwitch']
|
||||
ElTooltip: typeof import('element-plus/es')['ElTooltip']
|
||||
FullscreenLoading: typeof import('./src/components/FullscreenLoading.vue')['default']
|
||||
HelloWorld: typeof import('./src/components/HelloWorld.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']
|
||||
JobDislikeDialog: typeof import('./src/components/JobDislikeDialog.vue')['default']
|
||||
JobFeedbackDialog: typeof import('./src/components/JobFeedbackDialog.vue')['default']
|
||||
|
||||
@@ -7,6 +7,16 @@
|
||||
<title>Offer派 - 大学生AI求职平台 | 智能岗位匹配·一键自动网申·AI简历优化</title>
|
||||
<meta name="description" content="Offer派是专为大学生打造的AI求职平台,提供智能岗位匹配、一键自动网申、岗位定制简历、内推人脉直通等功能,让校招求职效率提升80%。" />
|
||||
<meta name="keywords" content="大学生求职,校招,AI求职,智能匹配,自动网申,简历优化,内推,Offer派" />
|
||||
<!-- 友盟统计 -->
|
||||
<script>
|
||||
var _czc = _czc || [];
|
||||
(function () {
|
||||
var um = document.createElement("script");
|
||||
um.src = "https://v1.cnzz.com/z.js?id=1281490571&async=1";
|
||||
var s = document.getElementsByTagName("script")[0];
|
||||
s.parentNode.insertBefore(um, s);
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
"vuex": "^4.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/chrome": "^0.2.2",
|
||||
"@types/markdown-it": "^14.1.2",
|
||||
"@types/node": "^24.10.1",
|
||||
"@vitejs/plugin-vue": "^6.0.2",
|
||||
|
||||
@@ -36,6 +36,9 @@ importers:
|
||||
specifier: ^4.1.0
|
||||
version: 4.1.0(vue@3.5.29(typescript@5.9.3))
|
||||
devDependencies:
|
||||
'@types/chrome':
|
||||
specifier: ^0.2.2
|
||||
version: 0.2.2
|
||||
'@types/markdown-it':
|
||||
specifier: ^14.1.2
|
||||
version: 14.1.2
|
||||
@@ -490,9 +493,21 @@ packages:
|
||||
'@sxzz/popperjs-es@2.11.8':
|
||||
resolution: {integrity: sha512-wOwESXvvED3S8xBmcPWHs2dUuzrE4XiZeFu7e1hROIJkm02a49N120pmOXxY33sBb6hArItm5W5tcg1cBtV+HQ==}
|
||||
|
||||
'@types/chrome@0.2.2':
|
||||
resolution: {integrity: sha512-8rSMZ4cvo2xmaSyQg0sN5yRL7oiDkntLoiHxUhfwQnv1mvnkrdoZ25SlNrKWmYKaeP50WvrfWj1pmc02+U9KKw==}
|
||||
|
||||
'@types/estree@1.0.8':
|
||||
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
|
||||
|
||||
'@types/filesystem@0.0.36':
|
||||
resolution: {integrity: sha512-vPDXOZuannb9FZdxgHnqSwAG/jvdGM8Wq+6N4D/d80z+D4HWH+bItqsZaVRQykAn6WEVeEkLm2oQigyHtgb0RA==}
|
||||
|
||||
'@types/filewriter@0.0.33':
|
||||
resolution: {integrity: sha512-xFU8ZXTw4gd358lb2jw25nxY9QAgqn2+bKKjKOYfNCzN4DKCFetK7sPtrlpg66Ywe3vWY9FNxprZawAh9wfJ3g==}
|
||||
|
||||
'@types/har-format@1.2.16':
|
||||
resolution: {integrity: sha512-fluxdy7ryD3MV6h8pTfTYpy/xQzCFC7m89nOH9y94cNqJ1mDIDPut7MnRHI3F6qRmh/cT2fUjG1MLdCNb4hE9A==}
|
||||
|
||||
'@types/linkify-it@5.0.0':
|
||||
resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==}
|
||||
|
||||
@@ -1365,8 +1380,21 @@ snapshots:
|
||||
|
||||
'@sxzz/popperjs-es@2.11.8': {}
|
||||
|
||||
'@types/chrome@0.2.2':
|
||||
dependencies:
|
||||
'@types/filesystem': 0.0.36
|
||||
'@types/har-format': 1.2.16
|
||||
|
||||
'@types/estree@1.0.8': {}
|
||||
|
||||
'@types/filesystem@0.0.36':
|
||||
dependencies:
|
||||
'@types/filewriter': 0.0.33
|
||||
|
||||
'@types/filewriter@0.0.33': {}
|
||||
|
||||
'@types/har-format@1.2.16': {}
|
||||
|
||||
'@types/linkify-it@5.0.0': {}
|
||||
|
||||
'@types/lodash-es@4.17.12':
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
google-site-verification: google18c40aea647ea1cd.html
|
||||
@@ -301,9 +301,7 @@ export interface OptimizeResumeParams {
|
||||
*/
|
||||
export function optimizeAgentResume(params: OptimizeResumeParams) {
|
||||
return aiService.post('/job-agent/optimize-resume', params, {
|
||||
headers: {
|
||||
Token: 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOjIwMzUyNTM4OTg5MTk4NzA0NjUsInV1SWQiOiI2MmQ5MDE2NTcyNzY0ZmNjODNjZTIyYjRjODA5ZmU5MiJ9.eE-Q5rio5J5kxkS-XPYdmk-1Tgvg6kj6NGoKWMFNU14',
|
||||
},
|
||||
|
||||
}).then(res => res.data)
|
||||
}
|
||||
|
||||
|
||||
@@ -54,6 +54,19 @@ export async function smsLogin(mobileNumber: string, code: string, inviteCode?:
|
||||
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
|
||||
|
||||
@@ -3,6 +3,16 @@ import aiService from '@/utils/aiRequest'
|
||||
import type { ApiResult } from '@/api/auth'
|
||||
import type { AiResult } from '@/utils/aiRequest'
|
||||
|
||||
// ==================== 岗位总数 ====================
|
||||
|
||||
/**
|
||||
* 获取全网实时岗位总数
|
||||
* GET /job/count
|
||||
*/
|
||||
export function fetchJobCount() {
|
||||
return request.get<any, ApiResult<number>>('/job/count')
|
||||
}
|
||||
|
||||
// ==================== 匹配详情 ====================
|
||||
|
||||
/** 匹配度详情 */
|
||||
@@ -91,7 +101,8 @@ export interface JobListParams {
|
||||
recruitCategory?: number
|
||||
/** 排除岗位ID列表(用于推荐时排除已推荐过的) */
|
||||
excludeJobIds?: number[]
|
||||
|
||||
/** 公司类型列表(上市企业、独角兽、国企等,可多选) */
|
||||
companyTypes?: string[]
|
||||
}
|
||||
|
||||
// ==================== 求职意向 ====================
|
||||
|
||||
|
After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 67 KiB |
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 56 KiB |
|
After Width: | Height: | Size: 105 KiB |
|
After Width: | Height: | Size: 1008 KiB |
|
Before Width: | Height: | Size: 7.4 KiB After Width: | Height: | Size: 211 KiB |
|
After Width: | Height: | Size: 67 KiB |
|
Before Width: | Height: | Size: 2.3 MiB |
|
Before Width: | Height: | Size: 53 KiB |
|
Before Width: | Height: | Size: 85 KiB |
|
Before Width: | Height: | Size: 55 KiB |
|
Before Width: | Height: | Size: 462 KiB |
|
Before Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 177 KiB |
|
Before Width: | Height: | Size: 246 KiB |
|
Before Width: | Height: | Size: 114 KiB |
|
Before Width: | Height: | Size: 168 KiB |
|
Before Width: | Height: | Size: 221 KiB |
|
Before Width: | Height: | Size: 485 KiB |
|
Before Width: | Height: | Size: 232 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 127 KiB |
|
Before Width: | Height: | Size: 77 KiB |
|
Before Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 92 KiB |
|
Before Width: | Height: | Size: 390 KiB |
|
Before Width: | Height: | Size: 114 KiB |
|
Before Width: | Height: | Size: 53 KiB |
|
Before Width: | Height: | Size: 408 KiB |
|
Before Width: | Height: | Size: 177 KiB |
|
Before Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 247 KiB |
|
Before Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 83 KiB |
|
Before Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 1.8 MiB |
@@ -531,6 +531,11 @@ body:not(#_) {
|
||||
.color-2{
|
||||
color: #222;
|
||||
}
|
||||
|
||||
.color-2F7DEA{
|
||||
color: #2F7DEA;
|
||||
}
|
||||
|
||||
.color-3{
|
||||
color: #333;
|
||||
}
|
||||
@@ -583,6 +588,10 @@ body:not(#_) {
|
||||
.color-9B9EAB{
|
||||
color:#9B9EAB;
|
||||
}
|
||||
.color-D31414{
|
||||
color: #D31414;
|
||||
}
|
||||
|
||||
.color-accent{
|
||||
color:$accent;
|
||||
}
|
||||
@@ -897,6 +906,9 @@ body:not(#_) {
|
||||
.max-w1200{
|
||||
max-width: 12rem;
|
||||
}
|
||||
.max-w602{
|
||||
max-width: 6.02rem;
|
||||
}
|
||||
.w0{width: 0rem;}
|
||||
.w1{width: 0.01rem;} .w2{width: 0.02rem;} .w3{width: 0.03rem;} .w4{width: 0.04rem;} .w5{width: 0.05rem;} .w6{width: 0.06rem;} .w7{width: 0.07rem;} .w8{width: 0.08rem;} .w9{width: 0.09rem;} .w10{width: 0.1rem;} .w11{width: 0.11rem;} .w12{width: 0.12rem;} .w13{width: 0.13rem;} .w14{width: 0.14rem;} .w15{width: 0.15rem;} .w16{width: 0.16rem;} .w17{width: 0.17rem;} .w18{width: 0.18rem;} .w19{width: 0.19rem;} .w20{width: 0.2rem;} .w21{width: 0.21rem;} .w22{width: 0.22rem;} .w23{width: 0.23rem;} .w24{width: 0.24rem;} .w25{width: 0.25rem;} .w26{width: 0.26rem;} .w27{width: 0.27rem;} .w28{width: 0.28rem;} .w29{width: 0.29rem;} .w30{width: 0.3rem;} .w31{width: 0.31rem;} .w32{width: 0.32rem;} .w33{width: 0.33rem;} .w34{width: 0.34rem;} .w35{width: 0.35rem;} .w36{width: 0.36rem;} .w37{width: 0.37rem;} .w38{width: 0.38rem;} .w39{width: 0.39rem;} .w40{width: 0.4rem;} .w41{width: 0.41rem;} .w42{width: 0.42rem;} .w43{width: 0.43rem;} .w44{width: 0.44rem;} .w45{width: 0.45rem;} .w46{width: 0.46rem;} .w47{width: 0.47rem;} .w48{width: 0.48rem;} .w49{width: 0.49rem;} .w50{width: 0.5rem;} .w51{width: 0.51rem;} .w52{width: 0.52rem;} .w53{width: 0.53rem;} .w54{width: 0.54rem;} .w55{width: 0.55rem;} .w56{width: 0.56rem;} .w57{width: 0.57rem;} .w58{width: 0.58rem;} .w59{width: 0.59rem;} .w60{width: 0.6rem;} .w61{width: 0.61rem;} .w62{width: 0.62rem;} .w63{width: 0.63rem;} .w64{width: 0.64rem;} .w65{width: 0.65rem;} .w66{width: 0.66rem;} .w67{width: 0.67rem;} .w68{width: 0.68rem;} .w69{width: 0.69rem;} .w70{width: 0.7rem;} .w71{width: 0.71rem;} .w72{width: 0.72rem;} .w73{width: 0.73rem;} .w74{width: 0.74rem;} .w75{width: 0.75rem;} .w76{width: 0.76rem;} .w77{width: 0.77rem;} .w78{width: 0.78rem;} .w79{width: 0.79rem;} .w80{width: 0.8rem;} .w81{width: 0.81rem;} .w82{width: 0.82rem;} .w83{width: 0.83rem;} .w84{width: 0.84rem;} .w85{width: 0.85rem;} .w86{width: 0.86rem;} .w87{width: 0.87rem;} .w88{width: 0.88rem;} .w89{width: 0.89rem;} .w90{width: 0.9rem;} .w91{width: 0.91rem;} .w92{width: 0.92rem;} .w93{width: 0.93rem;} .w94{width: 0.94rem;} .w95{width: 0.95rem;} .w96{width: 0.96rem;} .w97{width: 0.97rem;} .w98{width: 0.98rem;} .w99{width: 0.99rem;}
|
||||
.w100{width: 1rem;}
|
||||
@@ -919,12 +931,14 @@ body:not(#_) {
|
||||
.w244{width: 2.44rem;}
|
||||
.w263{width: 2.63rem;}
|
||||
.w444{width: 4.44rem;}
|
||||
.w706{width: 7.06rem;}
|
||||
.w1200{width:12rem}
|
||||
|
||||
/*height*/
|
||||
.h0{height: 0rem;}
|
||||
.h1{height: 0.01rem;} .h2{height: 0.02rem;} .h3{height: 0.03rem;} .h4{height: 0.04rem;} .h5{height: 0.05rem;} .h6{height: 0.06rem;} .h7{height: 0.07rem;} .h8{height: 0.08rem;} .h9{height: 0.09rem;} .h10{height: 0.1rem;} .h11{height: 0.11rem;} .h12{height: 0.12rem;} .h13{height: 0.13rem;} .h14{height: 0.14rem;} .h15{height: 0.15rem;} .h16{height: 0.16rem;} .h17{height: 0.17rem;} .h18{height: 0.18rem;} .h19{height: 0.19rem;} .h20{height: 0.2rem;} .h21{height: 0.21rem;} .h22{height: 0.22rem;} .h23{height: 0.23rem;} .h24{height: 0.24rem;} .h25{height: 0.25rem;} .h26{height: 0.26rem;} .h27{height: 0.27rem;} .h28{height: 0.28rem;} .h29{height: 0.29rem;} .h30{height: 0.3rem;} .h31{height: 0.31rem;} .h32{height: 0.32rem;} .h33{height: 0.33rem;} .h34{height: 0.34rem;} .h35{height: 0.35rem;} .h36{height: 0.36rem;} .h37{height: 0.37rem;} .h38{height: 0.38rem;} .h39{height: 0.39rem;} .h40{height: 0.4rem;} .h41{height: 0.41rem;} .h42{height: 0.42rem;} .h43{height: 0.43rem;} .h44{height: 0.44rem;} .h45{height: 0.45rem;} .h46{height: 0.46rem;} .h47{height: 0.47rem;} .h48{height: 0.48rem;} .h49{height: 0.49rem;} .h50{height: 0.5rem;} .h51{height: 0.51rem;} .h52{height: 0.52rem;} .h53{height: 0.53rem;} .h54{height: 0.54rem;} .h55{height: 0.55rem;} .h56{height: 0.56rem;} .h57{height: 0.57rem;} .h58{height: 0.58rem;} .h59{height: 0.59rem;} .h60{height: 0.6rem;} .h61{height: 0.61rem;} .h62{height: 0.62rem;} .h63{height: 0.63rem;} .h64{height: 0.64rem;} .h65{height: 0.65rem;} .h66{height: 0.66rem;} .h67{height: 0.67rem;} .h68{height: 0.68rem;} .h69{height: 0.69rem;} .h70{height: 0.7rem;} .h71{height: 0.71rem;} .h72{height: 0.72rem;} .h73{height: 0.73rem;} .h74{height: 0.74rem;} .h75{height: 0.75rem;} .h76{height: 0.76rem;} .h77{height: 0.77rem;} .h78{height: 0.78rem;} .h79{height: 0.79rem;} .h80{height: 0.8rem;} .h81{height: 0.81rem;} .h82{height: 0.82rem;} .h83{height: 0.83rem;} .h84{height: 0.84rem;} .h85{height: 0.85rem;} .h86{height: 0.86rem;} .h87{height: 0.87rem;} .h88{height: 0.88rem;} .h89{height: 0.89rem;} .h90{height: 0.9rem;} .h91{height: 0.91rem;} .h92{height: 0.92rem;} .h93{height: 0.93rem;} .h94{height: 0.94rem;} .h95{height: 0.95rem;} .h96{height: 0.96rem;} .h97{height: 0.97rem;} .h98{height: 0.98rem;} .h99{height: 0.99rem;}
|
||||
.h100{height: 1rem;}
|
||||
.h452{height: 4.52rem;}
|
||||
|
||||
//行高
|
||||
.lh0{line-height: 0rem;}
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
|
||||
&__job-title {
|
||||
font-size: 0.18rem;
|
||||
font-weight: 520;
|
||||
font-weight: 620;
|
||||
color: $text-dark;
|
||||
line-height: 0.27rem;
|
||||
}
|
||||
@@ -126,7 +126,7 @@
|
||||
// 步骤名称
|
||||
&__step-name {
|
||||
font-size: 0.14rem;
|
||||
font-weight: 520;
|
||||
font-weight: 620;
|
||||
line-height: 0.19rem;
|
||||
|
||||
&--done {
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
// Agent浏览器插件安装步骤指引组件样式
|
||||
@use '../variables' as *;
|
||||
|
||||
/* 步骤内容区域 */
|
||||
.agent-browser-guide-drawer__body {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 0 0.32rem 0.32rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.16rem;
|
||||
font-size: 0.14rem;
|
||||
}
|
||||
|
||||
/* 步骤卡片 */
|
||||
.agent-browser-guide-drawer__card {
|
||||
border: 1px solid #F0F0F0;
|
||||
border-radius: 0.12rem;
|
||||
padding: 0.16rem 0.2rem;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.agent-browser-guide-drawer__step-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.12rem;
|
||||
margin-bottom: 0.16rem;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
.agent-browser-guide-drawer__step-num {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 0.32rem;
|
||||
height: 0.32rem;
|
||||
background: #EDF9FA;
|
||||
border-radius: 0.08rem;
|
||||
font-size: 0.18rem;
|
||||
font-weight: 620;
|
||||
color: $accent;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.agent-browser-guide-drawer__step-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.agent-browser-guide-drawer__step-title {
|
||||
font-size: 0.18rem;
|
||||
font-weight: 620;
|
||||
color: $text-dark;
|
||||
line-height: 0.24rem;
|
||||
margin: 0 0 0.04rem;
|
||||
}
|
||||
|
||||
.agent-browser-guide-drawer__step-desc {
|
||||
font-size: 0.14rem;
|
||||
font-weight: 330;
|
||||
color: $text-middle;
|
||||
line-height: 0.19rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* 下载按钮区域 */
|
||||
.agent-browser-guide-drawer__download-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.16rem;
|
||||
padding: 0.04rem 0.04rem 0.04rem 0.16rem;
|
||||
background: $bg-main;
|
||||
border-radius: 0.12rem;
|
||||
height: 0.44rem;
|
||||
margin-left: 0.4rem;
|
||||
}
|
||||
|
||||
.agent-browser-guide-drawer__download-name {
|
||||
font-size: 0.14rem;
|
||||
font-weight: 450;
|
||||
color: $text-dark;
|
||||
line-height: 0.19rem;
|
||||
}
|
||||
|
||||
.agent-browser-guide-drawer__download-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.04rem;
|
||||
padding: 0.08rem 0.12rem;
|
||||
background: $accent;
|
||||
border: none;
|
||||
border-radius: 0.08rem;
|
||||
color: $bg-white;
|
||||
font-size: 0.14rem;
|
||||
font-weight: 620;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: $accent-hover;
|
||||
}
|
||||
}
|
||||
|
||||
/* 浏览器链接区域 */
|
||||
.agent-browser-guide-drawer__browser-links {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.24rem;
|
||||
margin-bottom: 0.16rem;
|
||||
margin-left: 0.4rem;
|
||||
}
|
||||
|
||||
.agent-browser-guide-drawer__browser-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.04rem;
|
||||
padding: 0.12rem 0.16rem;
|
||||
background: $bg-white;
|
||||
border: 1px solid #F0F0F0;
|
||||
border-radius: 0.08rem;
|
||||
font-size: 0.14rem;
|
||||
font-weight: 450;
|
||||
color: $text-middle;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.2s;
|
||||
|
||||
&:hover {
|
||||
border-color: $accent;
|
||||
}
|
||||
|
||||
svg {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
span {
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.agent-browser-guide-drawer__copy-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.04rem;
|
||||
color: $accent;
|
||||
font-size: 0.14rem;
|
||||
font-weight: 450;
|
||||
margin-left: 0.12rem;
|
||||
}
|
||||
|
||||
/* 步骤示意图 */
|
||||
.agent-browser-guide-drawer__step-img {
|
||||
width: 100%;
|
||||
max-width: 3.52rem;
|
||||
border-radius: 0.08rem;
|
||||
margin-top: 0.12rem;
|
||||
margin-left: 0.4rem;
|
||||
}
|
||||
@@ -12,7 +12,7 @@
|
||||
// 标题
|
||||
&__header {
|
||||
font-size: 0.22rem;
|
||||
font-weight: 520;
|
||||
font-weight: 620;
|
||||
color: $text-dark;
|
||||
margin-bottom: 0.04rem;
|
||||
line-height: 0.29rem;
|
||||
@@ -127,7 +127,7 @@
|
||||
// 岗位名称
|
||||
&__title {
|
||||
font-size: 0.18rem;
|
||||
font-weight: 520;
|
||||
font-weight: 620;
|
||||
color: $text-dark;
|
||||
line-height: 0.27rem;
|
||||
}
|
||||
@@ -208,7 +208,7 @@
|
||||
background: transparent;
|
||||
border: none;
|
||||
font-size: 0.14rem;
|
||||
font-weight: 520;
|
||||
font-weight: 620;
|
||||
color: $accent;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
@@ -232,7 +232,7 @@
|
||||
border: none;
|
||||
border-radius: 0.08rem;
|
||||
font-size: 0.14rem;
|
||||
font-weight: 520;
|
||||
font-weight: 620;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
transition: all 0.2s ease;
|
||||
@@ -291,7 +291,7 @@
|
||||
border: 1px solid $accent;
|
||||
border-radius: 0.22rem;
|
||||
font-size: 0.14rem;
|
||||
font-weight: 520;
|
||||
font-weight: 620;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
@@ -325,7 +325,7 @@
|
||||
border: none;
|
||||
border-radius: 0.22rem;
|
||||
font-size: 0.14rem;
|
||||
font-weight: 520;
|
||||
font-weight: 620;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
/* 标题文字 */
|
||||
&__title {
|
||||
font-size: 0.18rem;
|
||||
font-weight: 520;
|
||||
font-weight: 620;
|
||||
color: $text-dark;
|
||||
line-height: 0.24rem;
|
||||
}
|
||||
@@ -89,7 +89,7 @@
|
||||
/* 岗位标题 */
|
||||
&__job-title {
|
||||
font-size: 0.24rem;
|
||||
font-weight: 520;
|
||||
font-weight: 620;
|
||||
color: $text-dark;
|
||||
line-height: 0.32rem;
|
||||
margin: 0 0 0.08rem;
|
||||
@@ -221,7 +221,7 @@
|
||||
|
||||
&__company-name {
|
||||
font-size: 0.18rem;
|
||||
font-weight: 520;
|
||||
font-weight: 620;
|
||||
color: $text-dark;
|
||||
line-height: 0.24rem;
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
// 区块标题
|
||||
&__section-title {
|
||||
font-size: 0.16rem;
|
||||
font-weight: 520;
|
||||
font-weight: 620;
|
||||
line-height: 0.21rem;
|
||||
color: #1A1A1A;
|
||||
}
|
||||
@@ -297,7 +297,7 @@
|
||||
|
||||
&__edit-dialog-title {
|
||||
font-size: 0.18rem;
|
||||
font-weight: 520;
|
||||
font-weight: 620;
|
||||
line-height: 0.24rem;
|
||||
color: #1A1A1A;
|
||||
}
|
||||
@@ -451,7 +451,7 @@
|
||||
|
||||
&__title {
|
||||
font-size: 0.18rem;
|
||||
font-weight: 520;
|
||||
font-weight: 620;
|
||||
color: $text-dark;
|
||||
line-height: 0.24rem;
|
||||
}
|
||||
@@ -464,6 +464,7 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.16rem;
|
||||
font-size: 0.14rem;
|
||||
}
|
||||
|
||||
/* 步骤卡片 */
|
||||
@@ -471,6 +472,8 @@
|
||||
border: 1px solid #F0F0F0;
|
||||
border-radius: 0.12rem;
|
||||
padding: 0.16rem 0.2rem;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
&__step-row {
|
||||
@@ -478,6 +481,7 @@
|
||||
align-items: flex-start;
|
||||
gap: 0.12rem;
|
||||
margin-bottom: 0.16rem;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
&__step-num {
|
||||
@@ -489,7 +493,7 @@
|
||||
background: #EDF9FA;
|
||||
border-radius: 0.08rem;
|
||||
font-size: 0.18rem;
|
||||
font-weight: 520;
|
||||
font-weight: 620;
|
||||
color: $accent;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
@@ -500,7 +504,7 @@
|
||||
|
||||
&__step-title {
|
||||
font-size: 0.18rem;
|
||||
font-weight: 520;
|
||||
font-weight: 620;
|
||||
color: $text-dark;
|
||||
line-height: 0.24rem;
|
||||
margin: 0 0 0.04rem;
|
||||
@@ -523,6 +527,7 @@
|
||||
background: $bg-main;
|
||||
border-radius: 0.12rem;
|
||||
height: 0.44rem;
|
||||
margin-left: 0.4rem;
|
||||
}
|
||||
|
||||
&__download-name {
|
||||
@@ -542,7 +547,7 @@
|
||||
border-radius: 0.08rem;
|
||||
color: $bg-white;
|
||||
font-size: 0.14rem;
|
||||
font-weight: 520;
|
||||
font-weight: 620;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
|
||||
@@ -557,6 +562,7 @@
|
||||
align-items: flex-start;
|
||||
gap: 0.24rem;
|
||||
margin-bottom: 0.16rem;
|
||||
margin-left: 0.4rem;
|
||||
}
|
||||
|
||||
&__browser-link {
|
||||
@@ -599,9 +605,10 @@
|
||||
/* 步骤示意图 */
|
||||
&__step-img {
|
||||
width: 100%;
|
||||
max-width: 4rem;
|
||||
max-width: 3.52rem;
|
||||
border-radius: 0.08rem;
|
||||
margin-top: 0.12rem;
|
||||
margin-left: 0.4rem;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
/* 标题文字 */
|
||||
&__title {
|
||||
font-size: 0.18rem;
|
||||
font-weight: 520;
|
||||
font-weight: 620;
|
||||
color: $text-dark;
|
||||
line-height: 0.24rem;
|
||||
}
|
||||
@@ -136,7 +136,7 @@
|
||||
/* 岗位标题 */
|
||||
&__card-title {
|
||||
font-size: 0.18rem;
|
||||
font-weight: 520;
|
||||
font-weight: 620;
|
||||
color: $text-dark;
|
||||
line-height: 0.27rem;
|
||||
overflow: hidden;
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
|
||||
&__title {
|
||||
font-size: 0.18rem;
|
||||
font-weight: 520;
|
||||
font-weight: 620;
|
||||
line-height: 0.24rem;
|
||||
color: #1A1A1A;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,86 @@
|
||||
@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 {
|
||||
|
||||
|
||||
|
||||
@@ -1398,9 +1398,9 @@
|
||||
}
|
||||
}
|
||||
|
||||
// 立即去投递按钮
|
||||
// 下载专属简历按钮
|
||||
&__submit-btn {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
height: 0.48rem;
|
||||
background: $btn-dark;
|
||||
color: $bg-white;
|
||||
@@ -1415,6 +1415,20 @@
|
||||
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%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 右侧滑入动画
|
||||
|
||||
@@ -251,17 +251,15 @@
|
||||
|
||||
&__tag {
|
||||
font-size: 0.12rem;
|
||||
color: $text-dark;
|
||||
background: $bg-main;
|
||||
border: 1px solid $border-color;
|
||||
border-radius: 0.16rem;
|
||||
padding: 0.05rem 0.16rem;
|
||||
color: $accent;
|
||||
background: #fff;
|
||||
border: 1px solid $accent;
|
||||
border-radius: 0.04rem;
|
||||
padding: 0.05rem 0.08rem;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
border-color: $accent;
|
||||
color: $accent;
|
||||
background: $theme-color;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -53,6 +53,7 @@
|
||||
}
|
||||
|
||||
&__item {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.04rem;
|
||||
@@ -120,6 +121,26 @@
|
||||
flex: none;
|
||||
}
|
||||
|
||||
// 公测群二维码弹出层(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 {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -44,6 +44,7 @@
|
||||
@use './components/member-access-dialog.scss';
|
||||
@use './components/step-progress-overlay.scss';
|
||||
@use './components/mentor-aside-panel.scss';
|
||||
@use './components/agent-browser-install-guide.scss';
|
||||
|
||||
// 全局样式(优先级最高)
|
||||
@use './auto.scss';
|
||||
|
||||
@@ -279,8 +279,6 @@
|
||||
border-radius: 0.12rem;
|
||||
max-height: calc(var(--app-height, 100vh) - 1.4rem);
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
scrollbar-width: none; // Firefox
|
||||
-ms-overflow-style: none; // IE/Edge
|
||||
|
||||
@@ -690,7 +688,7 @@
|
||||
|
||||
&__step2-faq-title {
|
||||
font-size: 0.18rem;
|
||||
font-weight: 520;
|
||||
font-weight: 620;
|
||||
color: $accent;
|
||||
margin-bottom: 0.13rem;
|
||||
}
|
||||
@@ -706,7 +704,7 @@
|
||||
|
||||
&__step2-faq-item-title {
|
||||
font-size: 0.14rem;
|
||||
font-weight: 520;
|
||||
font-weight: 620;
|
||||
color: $text-dark;
|
||||
margin-bottom: 0.08rem;
|
||||
}
|
||||
@@ -972,7 +970,6 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.16rem;
|
||||
padding: 0.24rem;
|
||||
}
|
||||
|
||||
// 单个步骤卡片
|
||||
@@ -1014,7 +1011,7 @@
|
||||
background: #EDF9FA;
|
||||
border-radius: 0.08rem;
|
||||
font-size: 0.18rem;
|
||||
font-weight: 520;
|
||||
font-weight: 620;
|
||||
color: $accent;
|
||||
flex-shrink: 0;
|
||||
line-height: 1;
|
||||
@@ -1029,7 +1026,7 @@
|
||||
// 步骤标题
|
||||
&__install-card-title {
|
||||
font-size: 0.18rem;
|
||||
font-weight: 520;
|
||||
font-weight: 620;
|
||||
color: $text-dark;
|
||||
margin: 0 0 0.04rem;
|
||||
line-height: 0.24rem;
|
||||
@@ -1087,7 +1084,7 @@
|
||||
border-radius: 0.08rem;
|
||||
color: $bg-white;
|
||||
font-size: 0.14rem;
|
||||
font-weight: 520;
|
||||
font-weight: 620;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.2s ease;
|
||||
white-space: nowrap;
|
||||
@@ -1156,7 +1153,6 @@
|
||||
|
||||
// 完成页占位图标
|
||||
&__complete-icon {
|
||||
width: 1.2rem;
|
||||
height: 1.2rem;
|
||||
margin-bottom: 0.32rem;
|
||||
|
||||
@@ -1344,7 +1340,7 @@
|
||||
border-radius: 0.08rem;
|
||||
background: $bg-white;
|
||||
font-size: 0.14rem;
|
||||
font-weight: 520;
|
||||
font-weight: 620;
|
||||
color: $text-middle;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
@@ -1373,7 +1369,7 @@
|
||||
border: none;
|
||||
border-radius: 0.08rem;
|
||||
font-size: 0.14rem;
|
||||
font-weight: 520;
|
||||
font-weight: 620;
|
||||
color: $bg-white;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
@@ -1773,7 +1769,7 @@
|
||||
/* 标题文字 */
|
||||
&__title {
|
||||
font-size: 0.18rem;
|
||||
font-weight: 520;
|
||||
font-weight: 620;
|
||||
color: $text-dark;
|
||||
line-height: 0.24rem;
|
||||
}
|
||||
@@ -1868,3 +1864,127 @@
|
||||
transform: translateX(100%);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 插件安装确认弹窗 ====================
|
||||
|
||||
/* 遮罩层 */
|
||||
.agent-page__install-dialog-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 2000;
|
||||
}
|
||||
|
||||
/* 弹窗主体 */
|
||||
.agent-page__install-dialog {
|
||||
position: relative;
|
||||
width: 3.84rem;
|
||||
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.24rem 0.32rem 0.32rem;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 渐变背景 */
|
||||
.agent-page__install-dialog-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;
|
||||
}
|
||||
|
||||
/* 关闭按钮 */
|
||||
.agent-page__install-dialog-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;
|
||||
}
|
||||
}
|
||||
|
||||
/* 标题 */
|
||||
.agent-page__install-dialog-title {
|
||||
position: relative;
|
||||
font-size: 0.18rem;
|
||||
font-weight: 520;
|
||||
color: $text-dark;
|
||||
text-align: center;
|
||||
margin: 0 0 0.2rem 0;
|
||||
line-height: 0.24rem;
|
||||
}
|
||||
|
||||
/* 描述文字 */
|
||||
.agent-page__install-dialog-desc {
|
||||
position: relative;
|
||||
font-size: 0.16rem;
|
||||
font-weight: 330;
|
||||
color: $text-middle;
|
||||
text-align: center;
|
||||
line-height: 0.28rem;
|
||||
margin: 0 0 0.24rem 0;
|
||||
}
|
||||
|
||||
/* 底部按钮容器 */
|
||||
.agent-page__install-dialog-btns {
|
||||
position: relative;
|
||||
display: flex;
|
||||
gap: 0.08rem;
|
||||
}
|
||||
|
||||
/* 按钮通用 */
|
||||
.agent-page__install-dialog-btn {
|
||||
flex: 1;
|
||||
height: 0.37rem;
|
||||
border-radius: 0.08rem;
|
||||
font-size: 0.14rem;
|
||||
font-weight: 450;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s;
|
||||
|
||||
/* 描边按钮 — 还未安装 */
|
||||
&--outline {
|
||||
background: $bg-white;
|
||||
border: 1px solid $accent;
|
||||
color: $accent;
|
||||
|
||||
&:hover {
|
||||
background: $theme-color;
|
||||
}
|
||||
}
|
||||
|
||||
/* 实心按钮 — 已安装,继续 */
|
||||
&--primary {
|
||||
background: $accent;
|
||||
border: none;
|
||||
color: #FFFFFF;
|
||||
|
||||
&:hover {
|
||||
background: $accent-hover;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -344,7 +344,7 @@
|
||||
&__quote-avatar {
|
||||
width: 0.38rem;
|
||||
height: 0.38rem;
|
||||
background: #D9E3EA;
|
||||
//background: #D9E3EA;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@@ -142,18 +142,21 @@
|
||||
|
||||
// 主标题
|
||||
&__title {
|
||||
font-size: 0.54rem;
|
||||
font-size: 0.48rem;
|
||||
font-weight: 600;
|
||||
line-height: 0.71rem;
|
||||
letter-spacing: -0.03rem;
|
||||
line-height: 0.61rem;
|
||||
letter-spacing: 0.03rem;
|
||||
color: #111;
|
||||
margin: 0;
|
||||
>span{
|
||||
width: 1.51rem;
|
||||
}
|
||||
}
|
||||
|
||||
// 副标题描述
|
||||
&__desc {
|
||||
font-size: 0.2rem;
|
||||
line-height: 0.325rem;
|
||||
font-size: 0.16rem;
|
||||
line-height: 0.2rem;
|
||||
color: #9ca3af;
|
||||
margin-top: 0.1rem;
|
||||
}
|
||||
@@ -162,12 +165,11 @@
|
||||
&__cta {
|
||||
margin-top: 0.69rem;
|
||||
width: 1.84rem;
|
||||
height: 0.56rem;
|
||||
//line-height: 0.24rem;
|
||||
height: 0.52rem;
|
||||
border-radius: 0.16rem;
|
||||
background: linear-gradient(90deg, rgba(82, 202, 209, 1) 0%, rgba(83, 217, 200, 1) 100%);
|
||||
background: $accent;
|
||||
color: #fff;
|
||||
font-size: 0.18rem;
|
||||
font-size: 0.15rem;
|
||||
font-weight: 600;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
@@ -178,8 +180,9 @@
|
||||
// 右侧卡片区
|
||||
&__right {
|
||||
flex-shrink: 0;
|
||||
box-shadow: 0px 0px 15.45px rgba(82, 202, 209, 0.1);
|
||||
//box-shadow: 0px 0px 15.45px rgba(82, 202, 209, 0.1);
|
||||
border-radius: 0.16rem;
|
||||
background: #FCFEFE;
|
||||
}
|
||||
|
||||
&__right-top{
|
||||
@@ -365,25 +368,25 @@
|
||||
.stat-card {
|
||||
background: #fff;
|
||||
border-radius: 0.16rem;
|
||||
width: 2.03rem;
|
||||
height: 1.87rem;
|
||||
width: 1.9rem;
|
||||
height: 1.9rem;
|
||||
text-align: center;
|
||||
padding-top:0.38rem;
|
||||
box-shadow: 0 0.06rem 0.2rem rgba(0, 0, 0, 0.05);
|
||||
|
||||
// 大数字
|
||||
&__num {
|
||||
font-size: 0.48rem;
|
||||
font-size: 0.38rem;
|
||||
font-weight: 900;
|
||||
color: #52CAD1;
|
||||
color: #2A8186;
|
||||
line-height: 0.63rem;
|
||||
}
|
||||
|
||||
// 描述文字
|
||||
&__label {
|
||||
font-size: 0.16rem;
|
||||
font-size: 0.13rem;
|
||||
line-height: 0.22rem;
|
||||
color: #9ca3af;
|
||||
color: #8D9399;
|
||||
margin-top: 0.12rem;
|
||||
letter-spacing: 0.0035rem;
|
||||
}
|
||||
@@ -391,7 +394,7 @@
|
||||
|
||||
// ==================== 岗位展示区 ====================
|
||||
.home-jobs-showcase {
|
||||
padding: 1rem 0;
|
||||
padding: 0.85rem 0 0.7rem 0;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background: #fff;
|
||||
@@ -441,20 +444,20 @@
|
||||
|
||||
// 副标题
|
||||
&__sub {
|
||||
font-size: 0.2rem;
|
||||
font-size: 0.15rem;
|
||||
line-height: 0.28rem;
|
||||
color: #999;
|
||||
margin-top: 0.2rem;
|
||||
margin-top: 0.19rem;
|
||||
}
|
||||
|
||||
// 玻璃态展示盒子
|
||||
&__box {
|
||||
margin-top: 0.6rem;
|
||||
margin-top: 0.87rem;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
border: 1px solid rgba(255, 255, 255, 0.9);
|
||||
box-shadow: 0 10px 40px rgba(82, 202, 209, 0.12);
|
||||
backdrop-filter: blur(100px);
|
||||
border-radius: 0.4rem;
|
||||
border-radius: 0.1rem;
|
||||
padding: 0.64rem 0;
|
||||
}
|
||||
|
||||
@@ -472,7 +475,7 @@
|
||||
|
||||
.ticker-track {
|
||||
display: flex;
|
||||
gap: 0.16rem;
|
||||
gap: 0.30rem;
|
||||
width: max-content;
|
||||
animation: ticker-scroll 20s linear infinite;
|
||||
}
|
||||
@@ -483,16 +486,16 @@
|
||||
.showcase-stat {
|
||||
text-align: center;
|
||||
&__num {
|
||||
font-size: 0.48rem;
|
||||
font-size: 0.40rem;
|
||||
line-height: 0.6rem;
|
||||
font-weight: 900;
|
||||
color: #1d1d1d;
|
||||
.accent { color: $accent; }
|
||||
color: #2A8186;
|
||||
.accent { color: #2A8186; }
|
||||
}
|
||||
&__label {
|
||||
font-size: 0.18rem;
|
||||
font-size: 0.14rem;
|
||||
line-height: 0.28rem;
|
||||
color: #64748b;
|
||||
color: #8D9399;
|
||||
margin-top: 0.08rem;
|
||||
}
|
||||
}
|
||||
@@ -500,7 +503,7 @@
|
||||
// 单个岗位滚动卡片
|
||||
.job-ticker {
|
||||
flex-shrink: 0;
|
||||
background: rgba(30, 30, 46, 0.9);
|
||||
background: #12383A;
|
||||
backdrop-filter: blur(36px);
|
||||
border-radius: 0.12rem;
|
||||
padding: 0.2rem 0.24rem;
|
||||
@@ -545,16 +548,17 @@
|
||||
min-width: 0;
|
||||
|
||||
h2 {
|
||||
font-size: 0.48rem;
|
||||
font-size: 0.34rem;
|
||||
font-weight: 600;
|
||||
line-height: 0.63rem;
|
||||
line-height: 0.43rem;
|
||||
letter-spacing: 0.02rem;
|
||||
color: #111;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 0.2rem;
|
||||
line-height: 0.325rem;
|
||||
font-size: 0.14rem;
|
||||
line-height: 0.2rem;
|
||||
color: #999;
|
||||
margin-top: 0.16rem;
|
||||
}
|
||||
@@ -566,12 +570,13 @@
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.12rem;
|
||||
padding: 0.15rem 0.4rem;
|
||||
border-radius: 0.16rem;
|
||||
background: #111111;
|
||||
padding: 0 0.22rem;
|
||||
border-radius: 0.12rem;
|
||||
background: $accent;
|
||||
color: #fff;
|
||||
font-size: 0.16rem;
|
||||
line-height: 0.28rem;
|
||||
font-size: 0.12rem;
|
||||
height: 0.42rem;
|
||||
line-height: 0.42rem;
|
||||
font-weight: 600;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
@@ -582,7 +587,8 @@
|
||||
}
|
||||
svg {
|
||||
flex-shrink: 0;
|
||||
color: $btn-dark;
|
||||
color: #fff;
|
||||
font-size: 0.12rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1069,7 +1075,7 @@
|
||||
margin-bottom: 0.8rem;
|
||||
|
||||
h2 {
|
||||
font-size: 0.48rem;
|
||||
font-size: 0.38rem;
|
||||
line-height: 0.48rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.012rem;
|
||||
@@ -1077,12 +1083,12 @@
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 0.16rem;
|
||||
font-size: 0.15rem;
|
||||
line-height: 0.24rem;
|
||||
color: #9ca3af;
|
||||
letter-spacing: 0.0rem;
|
||||
text-transform: uppercase;
|
||||
margin-top: 0.24rem;
|
||||
margin-top: 0.14rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1612,10 +1618,18 @@
|
||||
margin: 0.6rem auto 0;
|
||||
text-align: center;
|
||||
padding-top: 0.4rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 0.2rem;
|
||||
|
||||
|
||||
p {
|
||||
font-size: 0.12rem;
|
||||
line-height: 0.16rem;
|
||||
color: #9ca3af;
|
||||
}
|
||||
a{
|
||||
font-size: 0.12rem;
|
||||
text-decoration: none;
|
||||
color: #9ca3af;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,14 +167,15 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0.08rem 0.16rem;
|
||||
width: 0.88rem;
|
||||
height: 0.37rem;
|
||||
line-height: 0.37rem;
|
||||
background: $accent;
|
||||
border: 1px solid $accent;
|
||||
border-radius: 0.08rem;
|
||||
color: $bg-white;
|
||||
font-size: 0.14rem;
|
||||
font-weight: 600;
|
||||
line-height: 0.19rem;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
|
||||
|
||||
@@ -168,11 +168,34 @@
|
||||
&__ai-tip-text {
|
||||
font-size: 0.12rem;
|
||||
line-height: 0.16rem;
|
||||
color: $accent;
|
||||
color: $text-light;
|
||||
vertical-align: text-top;
|
||||
margin-left: 0.03rem;
|
||||
}
|
||||
|
||||
// AI 推荐的可点击岗位/行业关键词
|
||||
&__ai-tip-keyword {
|
||||
color: $accent;
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.2s;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.75;
|
||||
}
|
||||
}
|
||||
|
||||
// 关键词之间的顿号(无下划线,text-light 色)
|
||||
&__ai-tip-comma {
|
||||
color: $text-light;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
// ";行业:" 分隔符保持 text-light 色
|
||||
&__ai-tip-separator {
|
||||
color: $text-light;
|
||||
}
|
||||
|
||||
// 筛选条件
|
||||
&__filters {
|
||||
display: flex;
|
||||
@@ -185,6 +208,7 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.08rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
&__filter-item {
|
||||
@@ -465,7 +489,7 @@
|
||||
border-radius: 0.04rem;
|
||||
font-size: 0.12rem;
|
||||
font-weight: 330;
|
||||
color: $accent;
|
||||
color: #fff;
|
||||
line-height: 0.16rem;
|
||||
}
|
||||
|
||||
@@ -548,7 +572,10 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0.08rem 0.16rem;
|
||||
text-align: center;
|
||||
width: 0.88rem;
|
||||
height: 0.37rem;
|
||||
line-height: 0.37rem;
|
||||
border: 1.23px solid $accent;
|
||||
border-radius: 0.08rem;
|
||||
background: none;
|
||||
@@ -557,7 +584,6 @@
|
||||
color: $accent;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
line-height: 0.19rem;
|
||||
white-space: nowrap;
|
||||
|
||||
&:hover {
|
||||
@@ -569,7 +595,9 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0.08rem 0.16rem;
|
||||
width: 0.88rem;
|
||||
height: 0.37rem;
|
||||
line-height: 0.35rem;
|
||||
background: $accent;
|
||||
border: none;
|
||||
border-radius: 0.08rem;
|
||||
@@ -578,7 +606,6 @@
|
||||
color: #FFFFFF;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
line-height: 0.19rem;
|
||||
white-space: nowrap;
|
||||
|
||||
&:hover {
|
||||
@@ -774,7 +801,46 @@
|
||||
// 暂无数据提示
|
||||
&__empty {
|
||||
text-align: center;
|
||||
padding: 1rem 0;
|
||||
font-size: 0.14rem;
|
||||
color: $text-light;
|
||||
}
|
||||
|
||||
// 空状态图片
|
||||
&__empty-img {
|
||||
width: 1.24rem;
|
||||
height: 1.3rem;
|
||||
display: block;
|
||||
margin: 1.6rem 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;
|
||||
}
|
||||
}
|
||||
|
||||
// 列表结果较少时的底部提示
|
||||
&__few-results {
|
||||
text-align: center;
|
||||
padding: 0.12rem 0;
|
||||
font-size: 0.14rem;
|
||||
color: $text-light;
|
||||
}
|
||||
@@ -1157,3 +1223,109 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 简历快照抽屉 ====================
|
||||
|
||||
/* 遮罩层 */
|
||||
.jobs-resume-drawer-mask {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: var(--app-height, 100vh);
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
z-index: 2000;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
/* 抽屉主体 */
|
||||
.jobs-resume-drawer {
|
||||
width: 9.6rem;
|
||||
max-width: 9.6rem;
|
||||
height: var(--app-height, 100vh);
|
||||
background: $bg-white;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-size: 0.14rem;
|
||||
box-shadow: -4px 0 20px rgba(0, 0, 0, 0.08);
|
||||
|
||||
/* 顶部标题栏 */
|
||||
&__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.2rem 0.24rem;
|
||||
height: 0.64rem;
|
||||
box-sizing: border-box;
|
||||
border-bottom: 1px solid $border-color;
|
||||
}
|
||||
|
||||
&__header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.16rem;
|
||||
}
|
||||
|
||||
/* 关闭按钮 */
|
||||
&__close {
|
||||
width: 0.24rem;
|
||||
height: 0.24rem;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
&__close-icon {
|
||||
width: 0.24rem;
|
||||
height: 0.24rem;
|
||||
color: $text-middle;
|
||||
}
|
||||
|
||||
/* 标题文字 */
|
||||
&__title {
|
||||
font-size: 0.18rem;
|
||||
font-weight: 620;
|
||||
color: $text-dark;
|
||||
line-height: 0.24rem;
|
||||
}
|
||||
|
||||
/* 加载状态 */
|
||||
&__loading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 1rem 0;
|
||||
gap: 0.12rem;
|
||||
color: $text-light;
|
||||
font-size: 0.13rem;
|
||||
}
|
||||
|
||||
/* 内容区域(可滚动) */
|
||||
&__body {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 0.24rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* 抽屉动画 */
|
||||
.jobs-resume-drawer-enter-active,
|
||||
.jobs-resume-drawer-leave-active {
|
||||
transition: opacity 0.3s ease;
|
||||
|
||||
.jobs-resume-drawer {
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
}
|
||||
|
||||
.jobs-resume-drawer-enter-from,
|
||||
.jobs-resume-drawer-leave-to {
|
||||
opacity: 0;
|
||||
|
||||
.jobs-resume-drawer {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,7 +177,7 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.12rem;
|
||||
width: 3.41rem;
|
||||
width: 4rem;
|
||||
|
||||
img{
|
||||
width: 1.6rem;
|
||||
@@ -230,12 +230,12 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.24rem;
|
||||
width: 3.41rem;
|
||||
width: 4rem;
|
||||
}
|
||||
|
||||
/* 标题 */
|
||||
.login-view__heading {
|
||||
width: 3.41rem;
|
||||
width: 4rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.05rem;
|
||||
@@ -243,15 +243,16 @@
|
||||
|
||||
.login-view__title {
|
||||
font-weight: 700;
|
||||
font-size: 0.18rem;
|
||||
font-size: 0.24rem;
|
||||
line-height: 0.22rem;
|
||||
color: #18181A;
|
||||
}
|
||||
|
||||
.login-view__subtitle {
|
||||
font-weight: 400;
|
||||
font-size: 0.10rem;
|
||||
line-height: 0.12rem;
|
||||
font-size: 0.14rem;
|
||||
margin-top: 0.08rem;
|
||||
margin-bottom: 0.08rem;
|
||||
color: #616367;
|
||||
|
||||
>span:nth-child(2){
|
||||
@@ -264,12 +265,12 @@
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 3.41rem;
|
||||
height: 0.40rem;
|
||||
width: 4rem;
|
||||
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.08rem;
|
||||
border-radius: 0.12rem;
|
||||
}
|
||||
|
||||
/* 手机图标 */
|
||||
@@ -292,7 +293,7 @@
|
||||
.login-view__country-label {
|
||||
margin-left: 0.06rem;
|
||||
font-weight: 500;
|
||||
font-size: 0.12rem;
|
||||
font-size: 0.14rem;
|
||||
line-height: 0.15rem;
|
||||
color: #202124;
|
||||
white-space: nowrap;
|
||||
@@ -330,7 +331,7 @@
|
||||
border: none;
|
||||
background: transparent;
|
||||
font-weight: 400;
|
||||
font-size: 0.12rem;
|
||||
font-size: 0.14rem;
|
||||
line-height: 0.15rem;
|
||||
color: #202124;
|
||||
outline: none;
|
||||
@@ -342,14 +343,14 @@
|
||||
|
||||
/* ==================== 发送验证码按钮(步骤一) ==================== */
|
||||
.login-view__send-btn {
|
||||
width: 3.41rem;
|
||||
height: 0.40rem;
|
||||
width: 4rem;
|
||||
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.08rem;
|
||||
border-radius: 0.12rem;
|
||||
border: none;
|
||||
font-weight: 600;
|
||||
font-size: 0.12rem;
|
||||
font-size: 0.14rem;
|
||||
line-height: 0.15rem;
|
||||
color: #FFFFFF;
|
||||
cursor: pointer;
|
||||
@@ -391,8 +392,8 @@
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 0.42rem;
|
||||
height: 0.46rem;
|
||||
width: 0.56rem;
|
||||
height: 0.56rem;
|
||||
background: #FAFBFC;
|
||||
border: 1px solid #E2E8F0;
|
||||
border-radius: 0.12rem;
|
||||
@@ -401,7 +402,7 @@
|
||||
|
||||
.login-view__otp-box--active {
|
||||
background: #F3FFFD;
|
||||
border: 2px solid $accent;
|
||||
border: 1px solid $accent;
|
||||
}
|
||||
|
||||
.login-view__otp-box--filled {
|
||||
@@ -444,13 +445,13 @@
|
||||
}
|
||||
|
||||
.login-view__sms-status--error {
|
||||
color: #E85635;
|
||||
color: #EF4444;
|
||||
}
|
||||
|
||||
/* ==================== 状态按钮(步骤二) ==================== */
|
||||
.login-view__status-btn {
|
||||
width: 100%;
|
||||
height: 0.4rem;
|
||||
height: 0.43rem;
|
||||
background: linear-gradient(90deg, $btn-dark 0%, $btn-dark-hover 100%);
|
||||
border-radius: 0.12rem;
|
||||
border: none;
|
||||
@@ -494,7 +495,7 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.06rem;
|
||||
width: 3.41rem;
|
||||
width: 4rem;
|
||||
}
|
||||
|
||||
.login-view__checkbox {
|
||||
@@ -547,7 +548,7 @@
|
||||
|
||||
/* ==================== 简历上传阶段 — 右侧面板 ==================== */
|
||||
.login-view__form-wrap--resume {
|
||||
width: 3.41rem;
|
||||
width: 4rem;
|
||||
}
|
||||
|
||||
/* 上传区域 */
|
||||
@@ -592,15 +593,16 @@
|
||||
|
||||
.login-view__upload-btn {
|
||||
height: 0.4rem;
|
||||
line-height: 0.4rem;
|
||||
line-height: 0.43rem;
|
||||
background: linear-gradient(90deg, $btn-dark 0%, $btn-dark-hover 100%);
|
||||
border: none;
|
||||
border-radius: 0.08rem;
|
||||
font-size: 0.16rem;
|
||||
font-size: 0.14rem;
|
||||
font-weight: 600;
|
||||
color: #FFFFFF;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.2s;
|
||||
margin-bottom: -0.1rem;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.9;
|
||||
@@ -610,7 +612,7 @@
|
||||
/* 解析中状态 — 新版步骤面板 */
|
||||
.login-view__parse-panel {
|
||||
box-sizing: border-box;
|
||||
width: 3.41rem;
|
||||
width: 4rem;
|
||||
padding: 0.24rem;
|
||||
background: #F6FFFF;
|
||||
border: 1px solid #D6F1EF;
|
||||
@@ -767,13 +769,13 @@
|
||||
|
||||
/* 解析中按钮(禁用样式) */
|
||||
.login-view__parse-btn {
|
||||
width: 3.41rem;
|
||||
height: 0.40rem;
|
||||
width: 4rem;
|
||||
height: 0.43rem;
|
||||
background: #A6EAE7;
|
||||
border-radius: 0.08rem;
|
||||
border: none;
|
||||
font-weight: 600;
|
||||
font-size: 0.12rem;
|
||||
font-size: 0.14rem;
|
||||
line-height: 0.15rem;
|
||||
color: #FFFFFF;
|
||||
cursor: not-allowed;
|
||||
@@ -782,7 +784,7 @@
|
||||
/* ==================== 状态4:上传成功面板 ==================== */
|
||||
.login-view__success-panel {
|
||||
box-sizing: border-box;
|
||||
width: 3.41rem;
|
||||
width: 4rem;
|
||||
padding: 0.28rem 0.24rem;
|
||||
background: #F6FFFB;
|
||||
border: 1px solid #BCEED7;
|
||||
@@ -863,13 +865,13 @@
|
||||
|
||||
/* 进入后台按钮 */
|
||||
.login-view__success-btn {
|
||||
width: 3.41rem;
|
||||
height: 0.40rem;
|
||||
width: 4rem;
|
||||
height: 0.43rem;
|
||||
background: #4FC2C9;
|
||||
border-radius: 0.08rem;
|
||||
border: none;
|
||||
font-weight: 600;
|
||||
font-size: 0.12rem;
|
||||
font-size: 0.14rem;
|
||||
line-height: 0.15rem;
|
||||
color: #FFFFFF;
|
||||
cursor: pointer;
|
||||
@@ -883,7 +885,7 @@
|
||||
/* ==================== 状态3:上传失败面板 ==================== */
|
||||
.login-view__failed-panel {
|
||||
box-sizing: border-box;
|
||||
width: 3.41rem;
|
||||
width: 4rem;
|
||||
padding: 0.28rem 0.24rem;
|
||||
background: #FFFBFA;
|
||||
border: 1px solid #FADBD5;
|
||||
@@ -964,14 +966,14 @@
|
||||
|
||||
/* 重新上传按钮 */
|
||||
.login-view__failed-btn {
|
||||
width: 3.41rem;
|
||||
height: 0.40rem;
|
||||
width: 4rem;
|
||||
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.08rem;
|
||||
border: none;
|
||||
font-weight: 600;
|
||||
font-size: 0.12rem;
|
||||
font-size: 0.14rem;
|
||||
line-height: 0.15rem;
|
||||
color: #FFFFFF;
|
||||
cursor: pointer;
|
||||
@@ -990,4 +992,5 @@
|
||||
color: #999;
|
||||
padding-top: 0.3rem;
|
||||
border-top: 1px solid #E7F2F3;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
@@ -92,6 +92,13 @@
|
||||
&:hover {
|
||||
background: #EDF9FA;
|
||||
}
|
||||
|
||||
// 小屏(视口宽度 < 1440px)只显示第一个标签
|
||||
@media (max-width: 1439px) {
|
||||
& ~ & {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索框
|
||||
|
||||
@@ -726,7 +726,7 @@
|
||||
background: $bg-white;
|
||||
border: 1px solid $accent;
|
||||
border-radius: 0.04rem;
|
||||
padding: 0.05rem 0.14rem;
|
||||
padding: 0.05rem 0.08rem;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* 业务配置文件 — 集中管理可开关的业务策略
|
||||
*/
|
||||
|
||||
/** 是否开启"个人资料为空时强制跳转到登录页简历上传"机制 */
|
||||
export const ENABLE_FORCE_RESUME_UPLOAD = true
|
||||
|
||||
/** 浏览器插件目标版本(用于判断用户是否需要更新插件) */
|
||||
export const PLUGIN_TARGET_VERSION = '0.1.1'
|
||||
@@ -155,8 +155,12 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useStore } from 'vuex'
|
||||
import { createChannelBridge } from '@/utils/channelBridge'
|
||||
import type { JobListItem } from '@/api/jobs'
|
||||
|
||||
const store = useStore()
|
||||
|
||||
// ==================== Props ====================
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -286,8 +290,29 @@ function handleContinueApply() {
|
||||
emit('continueApply')
|
||||
}
|
||||
|
||||
/** 前往投递 */
|
||||
function handleGoApply() {
|
||||
/** 前往投递 — 跨域 put 投递链接给插件 */
|
||||
async function handleGoApply() {
|
||||
const link = props.jobInfo?.sourceUrl
|
||||
if (link) {
|
||||
const bridge = createChannelBridge({ domain: store.state.bridgeDomain })
|
||||
try {
|
||||
// 先 get 拿到已有的链接列表
|
||||
const existing = await bridge.get<{ linkList: string[] }>(store.state.bridgeDomain, 'offerpaiDeliveryLinkList', true)
|
||||
const linkList: string[] = existing?.linkList || []
|
||||
|
||||
// 如果链接不在列表中则 push
|
||||
if (!linkList.includes(link)) {
|
||||
linkList.push(link)
|
||||
}
|
||||
|
||||
// 跨域 put 数据给插件
|
||||
await bridge.put<{ linkList: string[] }>(store.state.bridgeDomain, 'offerpaiDeliveryLinkList', { linkList }, true)
|
||||
} catch (e) {
|
||||
console.error('[AgentApplyProgress] 跨域 put 投递链接失败', e)
|
||||
} finally {
|
||||
bridge.destroy()
|
||||
}
|
||||
}
|
||||
emit('goApply')
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
<!-- Agent页面 - 浏览器插件安装步骤指引组件 -->
|
||||
<template>
|
||||
<div class="agent-browser-guide-drawer__body">
|
||||
<!-- 步骤1:下载插件包 -->
|
||||
<div class="agent-browser-guide-drawer__card">
|
||||
<div class="mr100">
|
||||
<div class="agent-browser-guide-drawer__step-row mr0">
|
||||
<span class="agent-browser-guide-drawer__step-num">1</span>
|
||||
<div class="agent-browser-guide-drawer__step-info">
|
||||
<h4 class="agent-browser-guide-drawer__step-title">下载并解压插件压缩包</h4>
|
||||
<p class="agent-browser-guide-drawer__step-desc">下载 Offer派自动投递助手安装包,并将下载好的插件压缩包解压</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="agent-browser-guide-drawer__download-box w263">
|
||||
<span class="agent-browser-guide-drawer__download-name">Offer派智能投递助手.zip</span>
|
||||
<button class="agent-browser-guide-drawer__download-btn" @click="downloadExtension">
|
||||
<svg viewBox="0 0 16 16" fill="none" width="14" height="14"><path d="M8 2v8M8 10L5 7M8 10l3-3M3 13h10" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||
<span>下载</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 步骤1示意图 -->
|
||||
<img src="@/assets/images/agent-setting/1-1.png" alt="打开开发者模式" class="agent-browser-guide-drawer__step-img" />
|
||||
</div>
|
||||
|
||||
<!-- 步骤2:打开扩展管理页面 -->
|
||||
<div class="agent-browser-guide-drawer__card">
|
||||
|
||||
<div>
|
||||
<div class="agent-browser-guide-drawer__step-row">
|
||||
<span class="agent-browser-guide-drawer__step-num">2</span>
|
||||
<div class="agent-browser-guide-drawer__step-info">
|
||||
<h4 class="agent-browser-guide-drawer__step-title">打开扩展管理页面</h4>
|
||||
<p class="agent-browser-guide-drawer__step-desc">在浏览器地址栏输入对应地址并回车,打开扩展管理页面</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 浏览器地址链接 -->
|
||||
<div class="agent-browser-guide-drawer__browser-links">
|
||||
<div class="agent-browser-guide-drawer__browser-link" @click="copyToClipboard('chrome://extensions')">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_guide_chrome)">
|
||||
<path d="M8.00027 12.4507C10.4587 12.4507 12.4516 10.4577 12.4516 7.99929C12.4516 5.54085 10.4587 3.54785 8.00027 3.54785C5.54183 3.54785 3.54883 5.54085 3.54883 7.99929C3.54883 10.4577 5.54177 12.4507 8.0002 12.4507" fill="white"/>
|
||||
<path d="M2.24303 5.81204C1.91094 5.23683 1.52061 4.63314 1.07203 4.00098C0.369757 5.21706 2.38548e-05 6.59661 1.15428e-09 8.0009C-2.38525e-05 9.4052 0.369663 10.7848 1.07189 12.0009C1.77412 13.217 2.78414 14.2268 4.0004 14.9288C5.21666 15.6307 6.59629 16.0001 8.00059 15.9998C8.73688 14.9671 9.2368 14.2225 9.50034 13.766C10.0065 12.8893 10.6612 11.6341 11.4643 10.0004V9.99941C11.1134 10.6077 10.6086 11.113 10.0005 11.4643C9.39241 11.8156 8.70256 12.0006 8.0003 12.0007C7.29804 12.0008 6.60813 11.816 5.99995 11.4649C5.39178 11.1138 4.88677 10.6087 4.53571 10.0005C3.44496 7.96623 2.68073 6.57004 2.24303 5.81204Z" fill="#229342"/>
|
||||
<path d="M8.00046 16.0002C9.05106 16.0004 10.0914 15.7936 11.0621 15.3916C12.0327 14.9896 12.9147 14.4003 13.6575 13.6574C14.4004 12.9145 14.9896 12.0325 15.3915 11.0619C15.7935 10.0912 16.0002 9.05084 16 8.00024C15.9997 6.59593 15.6297 5.21644 14.9273 4.00043C13.4118 3.85105 12.2933 3.77637 11.5719 3.77637C10.7539 3.77637 9.56323 3.85105 7.9999 4.00043L7.99902 4.00105C8.7013 4.00071 9.39128 4.18529 9.99958 4.53623C10.6079 4.88717 11.113 5.39211 11.4643 6.00024C11.8155 6.60838 12.0004 7.29828 12.0004 8.00056C12.0004 8.70284 11.8155 9.39274 11.4642 10.0009L8.00046 16.0002Z" fill="#FBC116"/>
|
||||
<path d="M8.00013 11.1677C9.74913 11.1677 11.1669 9.74992 11.1669 8.00086C11.1669 6.2518 9.74913 4.83398 8.00006 4.83398C6.25113 4.83398 4.83325 6.25186 4.83325 8.00086C4.83325 9.74986 6.25113 11.1677 8.00013 11.1677Z" fill="#1A73E8"/>
|
||||
<path d="M8.00027 4.00027H14.9276C14.2256 2.78399 13.2158 1.77397 11.9997 1.07176C10.7835 0.369547 9.40396 -9.50021e-05 7.99965 1.83149e-08C6.59534 9.50388e-05 5.21581 0.369924 3.99978 1.0723C2.78374 1.77467 1.77406 2.78484 1.07227 4.00121L4.53595 10.0006L4.53689 10.0011C4.18549 9.39306 4.00037 8.70322 4.00017 8.00095C3.99996 7.29868 4.18467 6.60873 4.53571 6.0005C4.88676 5.39226 5.39177 4.88718 5.99995 4.53605C6.60814 4.18491 7.29806 4.0001 8.00033 4.00021L8.00027 4.00027Z" fill="#E33B2E"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_guide_chrome">
|
||||
<rect width="16" height="16" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
<span>Chrome浏览器:chrome://extensions</span>
|
||||
<span class="agent-browser-guide-drawer__copy-btn">
|
||||
<svg viewBox="0 0 14 14" fill="none" width="12" height="12"><rect x="4" y="4" width="8" height="8" rx="1" stroke="currentColor" stroke-width="1"/><path d="M10 4V3a1 1 0 00-1-1H3a1 1 0 00-1 1v6a1 1 0 001 1h1" stroke="currentColor" stroke-width="1"/></svg>
|
||||
复制
|
||||
</span>
|
||||
</div>
|
||||
<div class="agent-browser-guide-drawer__browser-link" @click="copyToClipboard('edge://extensions')">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_guide_edge)">
|
||||
<path d="M14.4375 11.9059C14.225 12.0184 14.0062 12.1184 13.7812 12.1996C13.0625 12.4684 12.3062 12.6059 11.5375 12.6059C8.5812 12.6059 6.0062 10.5746 6.0062 7.96211C6.01245 7.24961 6.4062 6.59336 7.0312 6.25586C4.3562 6.36836 3.6687 9.15586 3.6687 10.7871C3.6687 15.4059 7.92495 15.8746 8.8437 15.8746C9.33745 15.8746 10.0812 15.7309 10.5312 15.5871L10.6125 15.5621C12.3375 14.9684 13.8 13.8059 14.775 12.2621C14.85 12.1434 14.8125 11.9934 14.7 11.9184C14.6187 11.8684 14.5187 11.8621 14.4375 11.9059Z" fill="url(#paint0_linear_guide_edge)"/>
|
||||
<path opacity="0.35" d="M14.4375 11.9059C14.225 12.0184 14.0062 12.1184 13.7812 12.1996C13.0625 12.4684 12.3062 12.6059 11.5375 12.6059C8.5812 12.6059 6.0062 10.5746 6.0062 7.96211C6.01245 7.24961 6.4062 6.59336 7.0312 6.25586C4.3562 6.36836 3.6687 9.15586 3.6687 10.7871C3.6687 15.4059 7.92495 15.8746 8.8437 15.8746C9.33745 15.8746 10.0812 15.7309 10.5312 15.5871L10.6125 15.5621C12.3375 14.9684 13.8 13.8059 14.775 12.2621C14.85 12.1434 14.8125 11.9934 14.7 11.9184C14.6187 11.8684 14.5187 11.8621 14.4375 11.9059Z" fill="url(#paint1_radial_guide_edge)"/>
|
||||
<path d="M6.60629 15.0877C6.05004 14.7439 5.56879 14.2877 5.18754 13.7564C3.54379 11.5064 4.03754 8.3502 6.28754 6.70645C6.52504 6.5377 6.76879 6.38145 7.03129 6.25645C7.22504 6.1627 7.55629 6.0002 8.00004 6.00645C8.63129 6.0127 9.22504 6.3127 9.60629 6.81895C9.85629 7.15645 10 7.5627 10.0063 7.9877C10.0063 7.9752 11.5375 3.0127 5.00629 3.0127C2.26254 3.0127 0.00628588 5.61895 0.00628588 7.9002C-0.00621412 9.10645 0.256286 10.3064 0.762536 11.4002C2.48754 15.0752 6.68754 16.8752 10.5375 15.5939C9.21879 16.0064 7.78129 15.8252 6.60629 15.0877Z" fill="url(#paint2_linear_guide_edge)"/>
|
||||
<path opacity="0.41" d="M6.60629 15.0877C6.05004 14.7439 5.56879 14.2877 5.18754 13.7564C3.54379 11.5064 4.03754 8.3502 6.28754 6.70645C6.52504 6.5377 6.76879 6.38145 7.03129 6.25645C7.22504 6.1627 7.55629 6.0002 8.00004 6.00645C8.63129 6.0127 9.22504 6.3127 9.60629 6.81895C9.85629 7.15645 10 7.5627 10.0063 7.9877C10.0063 7.9752 11.5375 3.0127 5.00629 3.0127C2.26254 3.0127 0.00628588 5.61895 0.00628588 7.9002C-0.00621412 9.10645 0.256286 10.3064 0.762536 11.4002C2.48754 15.0752 6.68754 16.8752 10.5375 15.5939C9.21879 16.0064 7.78129 15.8252 6.60629 15.0877Z" fill="url(#paint3_radial_guide_edge)"/>
|
||||
<path d="M9.51875 9.30625C9.46875 9.36875 9.3125 9.4625 9.3125 9.6625C9.3125 9.825 9.41875 9.98125 9.60625 10.1125C10.5062 10.7375 12.2 10.6562 12.2063 10.6562C12.875 10.6562 13.525 10.475 14.1 10.1375C15.275 9.45 16 8.19375 16 6.83125C16.0187 5.43125 15.5 4.5 15.2937 4.0875C13.9688 1.49375 11.1062 0 8 0C3.625 0 0.0625 3.5125 0 7.8875C0.03125 5.60625 2.3 3.7625 5 3.7625C5.21875 3.7625 6.46875 3.78125 7.625 4.39375C8.64375 4.93125 9.18125 5.575 9.55 6.21875C9.9375 6.8875 10.0063 7.725 10.0063 8.0625C10.0063 8.39375 9.8375 8.89375 9.51875 9.30625Z" fill="url(#paint4_radial_guide_edge)"/>
|
||||
<path d="M9.51875 9.30625C9.46875 9.36875 9.3125 9.4625 9.3125 9.6625C9.3125 9.825 9.41875 9.98125 9.60625 10.1125C10.5062 10.7375 12.2 10.6562 12.2063 10.6562C12.875 10.6562 13.525 10.475 14.1 10.1375C15.275 9.45 16 8.19375 16 6.83125C16.0187 5.43125 15.5 4.5 15.2937 4.0875C13.9688 1.49375 11.1062 0 8 0C3.625 0 0.0625 3.5125 0 7.8875C0.03125 5.60625 2.3 3.7625 5 3.7625C5.21875 3.7625 6.46875 3.78125 7.625 4.39375C8.64375 4.93125 9.18125 5.575 9.55 6.21875C9.9375 6.8875 10.0063 7.725 10.0063 8.0625C10.0063 8.39375 9.8375 8.89375 9.51875 9.30625Z" fill="url(#paint5_radial_guide_edge)"/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_guide_edge" x1="3.66895" y1="11.0634" x2="14.8116" y2="11.0634" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#0C59A4"/>
|
||||
<stop offset="1" stop-color="#114A8B"/>
|
||||
</linearGradient>
|
||||
<radialGradient id="paint1_radial_guide_edge" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(9.82444 11.1519) scale(5.96065 5.662)">
|
||||
<stop offset="0.72" stop-opacity="0"/>
|
||||
<stop offset="0.95" stop-opacity="0.53"/>
|
||||
<stop offset="1"/>
|
||||
</radialGradient>
|
||||
<linearGradient id="paint2_linear_guide_edge" x1="9.54822" y1="6.23432" x2="2.58741" y2="13.8163" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#1B9DE2"/>
|
||||
<stop offset="0.16" stop-color="#1595DF"/>
|
||||
<stop offset="0.67" stop-color="#0680D7"/>
|
||||
<stop offset="1" stop-color="#0078D4"/>
|
||||
</linearGradient>
|
||||
<radialGradient id="paint3_radial_guide_edge" cx="0" cy="0" r="1" gradientTransform="matrix(1.34287 -8.86118 7.162 1.0743 4.41307 12.4383)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.76" stop-opacity="0"/>
|
||||
<stop offset="0.95" stop-opacity="0.5"/>
|
||||
<stop offset="1"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="paint4_radial_guide_edge" cx="0" cy="0" r="1" gradientTransform="matrix(-0.506075 12.6493 -26.9472 -1.0119 1.61573 2.96205)" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#35C1F1"/>
|
||||
<stop offset="0.11" stop-color="#34C1ED"/>
|
||||
<stop offset="0.23" stop-color="#2FC2DF"/>
|
||||
<stop offset="0.31" stop-color="#2BC3D2"/>
|
||||
<stop offset="0.67" stop-color="#36C752"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="paint5_radial_guide_edge" cx="0" cy="0" r="1" gradientTransform="matrix(1.70345 5.83918 -4.74533 1.39926 15.0038 4.83786)" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#66EB6E"/>
|
||||
<stop offset="1" stop-color="#66EB6E" stop-opacity="0"/>
|
||||
</radialGradient>
|
||||
<clipPath id="clip0_guide_edge">
|
||||
<rect width="16" height="16" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
<span>Edge浏览器:edge://extensions</span>
|
||||
<span class="agent-browser-guide-drawer__copy-btn">
|
||||
<svg viewBox="0 0 14 14" fill="none" width="12" height="12"><rect x="4" y="4" width="8" height="8" rx="1" stroke="currentColor" stroke-width="1"/><path d="M10 4V3a1 1 0 00-1-1H3a1 1 0 00-1 1v6a1 1 0 001 1h1" stroke="currentColor" stroke-width="1"/></svg>
|
||||
复制
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<!-- 步骤2示意图 -->
|
||||
<img src="@/assets/images/agent-setting/2-1.png" alt="输入扩展地址" class="agent-browser-guide-drawer__step-img" />
|
||||
</div>
|
||||
|
||||
<!-- 步骤3:打开开发者模式 -->
|
||||
<div class="agent-browser-guide-drawer__card">
|
||||
<div class="agent-browser-guide-drawer__step-row">
|
||||
<span class="agent-browser-guide-drawer__step-num">3</span>
|
||||
<div class="agent-browser-guide-drawer__step-info">
|
||||
<h4 class="agent-browser-guide-drawer__step-title">打开开发者模式</h4>
|
||||
<p class="agent-browser-guide-drawer__step-desc"><span class="color-D31414">Chrome浏览器</span>在扩展管理页面的右上角,找到「开发者模式」开关并打开</p>
|
||||
<p class="agent-browser-guide-drawer__step-desc mt3"><span class="color-2F7DEA">Edge浏览器</span>在扩展管理页面左下角,找到「开发者模式」开关并打开</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 步骤3示意图 -->
|
||||
<img src="@/assets/images/agent-setting/3-1.png" alt="打开开发者模式" class="agent-browser-guide-drawer__step-img max-w602" />
|
||||
</div>
|
||||
|
||||
<!-- 步骤4:将CRX安装包拖入安装 -->
|
||||
<div class="agent-browser-guide-drawer__card">
|
||||
<div class="agent-browser-guide-drawer__step-row">
|
||||
<span class="agent-browser-guide-drawer__step-num">4</span>
|
||||
<div class="agent-browser-guide-drawer__step-info">
|
||||
<h4 class="agent-browser-guide-drawer__step-title">点击加载未打包的扩展程序</h4>
|
||||
<p class="agent-browser-guide-drawer__step-desc">点击"加载未打包的扩展程序",选择解压后的插件文件夹,完成安装</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 步骤4示意图 -->
|
||||
<img src="@/assets/images/agent-setting/4-1.png" alt="拖入安装" class="agent-browser-guide-drawer__step-img" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
/** 插件下载地址 */
|
||||
const extensionDownloadUrl = 'https://offerpie.oss-cn-guangzhou.aliyuncs.com/extension/Offer派智能投递助手.zip'
|
||||
|
||||
/** 下载插件 */
|
||||
function downloadExtension() {
|
||||
window.open(extensionDownloadUrl, '_blank')
|
||||
}
|
||||
|
||||
/** 复制文本到剪贴板 */
|
||||
function copyToClipboard(text: string) {
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
ElMessage.success('已复制到剪贴板')
|
||||
}).catch(() => {
|
||||
ElMessage.error('复制失败,请手动复制')
|
||||
})
|
||||
}
|
||||
|
||||
/** 暴露方法给父组件调用 */
|
||||
defineExpose({ downloadExtension })
|
||||
</script>
|
||||
@@ -250,141 +250,7 @@
|
||||
</div>
|
||||
|
||||
<!-- 步骤内容 -->
|
||||
<div class="agent-browser-guide-drawer__body">
|
||||
<!-- 步骤1:下载插件包 -->
|
||||
<div class="agent-browser-guide-drawer__card">
|
||||
<div class="agent-browser-guide-drawer__step-row">
|
||||
<span class="agent-browser-guide-drawer__step-num">1</span>
|
||||
<div class="agent-browser-guide-drawer__step-info">
|
||||
<h4 class="agent-browser-guide-drawer__step-title">下载插件包</h4>
|
||||
<p class="agent-browser-guide-drawer__step-desc">下载 Offer派自动投递助手安装包,下载完成后由步骤 2 进入浏览器扩展页面</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="agent-browser-guide-drawer__download-box w263">
|
||||
<span class="agent-browser-guide-drawer__download-name">Offer派自动投递助手.crx</span>
|
||||
<button class="agent-browser-guide-drawer__download-btn" @click="downloadExtension">
|
||||
<svg viewBox="0 0 16 16" fill="none" width="14" height="14"><path d="M8 2v8M8 10L5 7M8 10l3-3M3 13h10" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||
<span>下载</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 步骤2:打开扩展管理页面 -->
|
||||
<div class="agent-browser-guide-drawer__card">
|
||||
<div class="agent-browser-guide-drawer__step-row">
|
||||
<span class="agent-browser-guide-drawer__step-num">2</span>
|
||||
<div class="agent-browser-guide-drawer__step-info">
|
||||
<h4 class="agent-browser-guide-drawer__step-title">打开扩展管理页面</h4>
|
||||
<p class="agent-browser-guide-drawer__step-desc">在浏览器地址栏输入对应地址并回车,打开扩展管理页面</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 浏览器地址链接 -->
|
||||
<div class="agent-browser-guide-drawer__browser-links">
|
||||
<div class="agent-browser-guide-drawer__browser-link" @click="copyToClipboard('chrome://extensions')">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_498_5140)">
|
||||
<path d="M8.00027 12.4507C10.4587 12.4507 12.4516 10.4577 12.4516 7.99929C12.4516 5.54085 10.4587 3.54785 8.00027 3.54785C5.54183 3.54785 3.54883 5.54085 3.54883 7.99929C3.54883 10.4577 5.54177 12.4507 8.0002 12.4507" fill="white"/>
|
||||
<path d="M2.24303 5.81204C1.91094 5.23683 1.52061 4.63314 1.07203 4.00098C0.369757 5.21706 2.38548e-05 6.59661 1.15428e-09 8.0009C-2.38525e-05 9.4052 0.369663 10.7848 1.07189 12.0009C1.77412 13.217 2.78414 14.2268 4.0004 14.9288C5.21666 15.6307 6.59629 16.0001 8.00059 15.9998C8.73688 14.9671 9.2368 14.2225 9.50034 13.766C10.0065 12.8893 10.6612 11.6341 11.4643 10.0004V9.99941C11.1134 10.6077 10.6086 11.113 10.0005 11.4643C9.39241 11.8156 8.70256 12.0006 8.0003 12.0007C7.29804 12.0008 6.60813 11.816 5.99995 11.4649C5.39178 11.1138 4.88677 10.6087 4.53571 10.0005C3.44496 7.96623 2.68073 6.57004 2.24303 5.81204Z" fill="#229342"/>
|
||||
<path d="M8.00046 16.0002C9.05106 16.0004 10.0914 15.7936 11.0621 15.3916C12.0327 14.9896 12.9147 14.4003 13.6575 13.6574C14.4004 12.9145 14.9896 12.0325 15.3915 11.0619C15.7935 10.0912 16.0002 9.05084 16 8.00024C15.9997 6.59593 15.6297 5.21644 14.9273 4.00043C13.4118 3.85105 12.2933 3.77637 11.5719 3.77637C10.7539 3.77637 9.56323 3.85105 7.9999 4.00043L7.99902 4.00105C8.7013 4.00071 9.39128 4.18529 9.99958 4.53623C10.6079 4.88717 11.113 5.39211 11.4643 6.00024C11.8155 6.60838 12.0004 7.29828 12.0004 8.00056C12.0004 8.70284 11.8155 9.39274 11.4642 10.0009L8.00046 16.0002Z" fill="#FBC116"/>
|
||||
<path d="M8.00013 11.1677C9.74913 11.1677 11.1669 9.74992 11.1669 8.00086C11.1669 6.2518 9.74913 4.83398 8.00006 4.83398C6.25113 4.83398 4.83325 6.25186 4.83325 8.00086C4.83325 9.74986 6.25113 11.1677 8.00013 11.1677Z" fill="#1A73E8"/>
|
||||
<path d="M8.00027 4.00027H14.9276C14.2256 2.78399 13.2158 1.77397 11.9997 1.07176C10.7835 0.369547 9.40396 -9.50021e-05 7.99965 1.83149e-08C6.59534 9.50388e-05 5.21581 0.369924 3.99978 1.0723C2.78374 1.77467 1.77406 2.78484 1.07227 4.00121L4.53595 10.0006L4.53689 10.0011C4.18549 9.39306 4.00037 8.70322 4.00017 8.00095C3.99996 7.29868 4.18467 6.60873 4.53571 6.0005C4.88676 5.39226 5.39177 4.88718 5.99995 4.53605C6.60814 4.18491 7.29806 4.0001 8.00033 4.00021L8.00027 4.00027Z" fill="#E33B2E"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_498_5140">
|
||||
<rect width="16" height="16" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
<span>Chrome浏览器:chrome://extensions</span>
|
||||
<span class="agent-browser-guide-drawer__copy-btn">
|
||||
<svg viewBox="0 0 14 14" fill="none" width="12" height="12"><rect x="4" y="4" width="8" height="8" rx="1" stroke="currentColor" stroke-width="1"/><path d="M10 4V3a1 1 0 00-1-1H3a1 1 0 00-1 1v6a1 1 0 001 1h1" stroke="currentColor" stroke-width="1"/></svg>
|
||||
复制
|
||||
</span>
|
||||
</div>
|
||||
<div class="agent-browser-guide-drawer__browser-link" @click="copyToClipboard('edge://extensions')">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_498_5157)">
|
||||
<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_498_5157)"/>
|
||||
<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_498_5157)"/>
|
||||
<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_498_5157)"/>
|
||||
<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_498_5157)"/>
|
||||
<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_498_5157)"/>
|
||||
<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_498_5157)"/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_498_5157" 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_498_5157" 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_498_5157" 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_498_5157" 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_498_5157" 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_498_5157" 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_498_5157">
|
||||
<rect width="16" height="16" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
<span>Edge浏览器:edge://extensions</span>
|
||||
<span class="agent-browser-guide-drawer__copy-btn">
|
||||
<svg viewBox="0 0 14 14" fill="none" width="12" height="12"><rect x="4" y="4" width="8" height="8" rx="1" stroke="currentColor" stroke-width="1"/><path d="M10 4V3a1 1 0 00-1-1H3a1 1 0 00-1 1v6a1 1 0 001 1h1" stroke="currentColor" stroke-width="1"/></svg>
|
||||
复制
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 步骤2示意图 -->
|
||||
<img src="@/assets/images/agent-setting/2-1.png" alt="输入扩展地址" class="agent-browser-guide-drawer__step-img" />
|
||||
</div>
|
||||
|
||||
<!-- 步骤3:打开开发者模式 -->
|
||||
<div class="agent-browser-guide-drawer__card">
|
||||
<div class="agent-browser-guide-drawer__step-row">
|
||||
<span class="agent-browser-guide-drawer__step-num">3</span>
|
||||
<div class="agent-browser-guide-drawer__step-info">
|
||||
<h4 class="agent-browser-guide-drawer__step-title">打开开发者模式</h4>
|
||||
<p class="agent-browser-guide-drawer__step-desc">在扩展管理页面的右上角,找到「开发者模式」开关并打开</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 步骤3示意图 -->
|
||||
<img src="@/assets/images/agent-setting/3-1.png" alt="打开开发者模式" class="agent-browser-guide-drawer__step-img" />
|
||||
</div>
|
||||
|
||||
<!-- 步骤4:将CRX安装包拖入安装 -->
|
||||
<div class="agent-browser-guide-drawer__card">
|
||||
<div class="agent-browser-guide-drawer__step-row">
|
||||
<span class="agent-browser-guide-drawer__step-num">4</span>
|
||||
<div class="agent-browser-guide-drawer__step-info">
|
||||
<h4 class="agent-browser-guide-drawer__step-title">将 CRX 安装包拖入安装</h4>
|
||||
<p class="agent-browser-guide-drawer__step-desc">直接将下载好的CRX文件拖拽到扩展管理页面中,浏览器会自动识别并安装插件</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 步骤4示意图 -->
|
||||
<img src="@/assets/images/agent-setting/4-1.png" alt="拖入安装" class="agent-browser-guide-drawer__step-img" />
|
||||
</div>
|
||||
</div>
|
||||
<AgentBrowserInstallGuide />
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
@@ -399,6 +265,7 @@ import { ElMessage } from 'element-plus'
|
||||
import { formatEmploymentType } from '@/stores/index'
|
||||
import JobGoalDialog from '@/components/JobGoalDialog.vue'
|
||||
import RegionSelector from '@/components/tools/RegionSelector.vue'
|
||||
import AgentBrowserInstallGuide from '@/components/AgentBrowserInstallGuide.vue'
|
||||
import { saveProfile, fetchProfile, fetchEducation } from '@/api/profile'
|
||||
import { resolveRegionName } from '@/utils/region'
|
||||
import { resolveIndustryName } from '@/utils/industry'
|
||||
@@ -662,9 +529,6 @@ async function handleSavePreference() {
|
||||
/** 浏览器插件安装教程抽屉是否显示 */
|
||||
const showBrowserGuideDrawer = ref(false)
|
||||
|
||||
/** 插件下载地址 */
|
||||
const extensionDownloadUrl = 'https://offerpie.oss-cn-guangzhou.aliyuncs.com/extension/chrome-mv3-prod.rar'
|
||||
|
||||
/** 打开浏览器安装指引抽屉 */
|
||||
function openBrowserGuide(_browser: string) {
|
||||
showBrowserGuideDrawer.value = true
|
||||
@@ -675,20 +539,6 @@ function closeBrowserGuideDrawer() {
|
||||
showBrowserGuideDrawer.value = false
|
||||
}
|
||||
|
||||
/** 下载插件 */
|
||||
function downloadExtension() {
|
||||
window.open(extensionDownloadUrl, '_blank')
|
||||
}
|
||||
|
||||
/** 复制文本到剪贴板 */
|
||||
function copyToClipboard(text: string) {
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
ElMessage.success('已复制到剪贴板')
|
||||
}).catch(() => {
|
||||
ElMessage.error('复制失败,请手动复制')
|
||||
})
|
||||
}
|
||||
|
||||
// ==================== 初始化 ====================
|
||||
|
||||
onMounted(async () => {
|
||||
|
||||
@@ -48,6 +48,7 @@
|
||||
<p class="agent-page__intro-desc">请仔细对你的资料评估(包括个人信息、教育背景、工作履历及技能),确保资料齐全</p>
|
||||
<!-- 按钮 -->
|
||||
<button class="agent-page__intro-gradient-btn" @click="handleNext">下一步</button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -76,7 +77,8 @@
|
||||
:categoryIds="step2CategoryIds"
|
||||
:maxSelect="3"
|
||||
:level="3"
|
||||
:allowParentSelect="true"
|
||||
:allowParentSelect="false"
|
||||
:allowParentSelectClick="true"
|
||||
:triggerStyle="prefFilterTriggerStyle"
|
||||
:displayStyle="prefFilterDisplayStyle"
|
||||
@update:categoryIds="onStep2CategoryChange"
|
||||
@@ -86,7 +88,8 @@
|
||||
:industryIds="step2IndustryIds"
|
||||
:maxSelect="3"
|
||||
:level="2"
|
||||
:allowParentSelect="true"
|
||||
:allowParentSelect="false"
|
||||
:allowParentSelectClick="true"
|
||||
:triggerStyle="prefFilterTriggerStyle"
|
||||
:displayStyle="prefFilterDisplayStyle"
|
||||
@update:industryIds="onStep2IndustryChange"
|
||||
@@ -161,164 +164,7 @@
|
||||
<!-- 左侧:安装教程内容 -->
|
||||
<div class="agent-page__right">
|
||||
<div class="agent-page__profile-wrapper bg-f">
|
||||
<div class="agent-page__install-guide">
|
||||
|
||||
<!-- 步骤1:下载插件包 -->
|
||||
<div class="agent-page__install-card">
|
||||
<div class="agent-page__install-card-row">
|
||||
<div class="agent-page__install-card-left">
|
||||
<div class="agent-page__install-card-header">
|
||||
<span class="agent-page__install-card-num">1</span>
|
||||
<div class="agent-page__install-card-info">
|
||||
<h4 class="agent-page__install-card-title">下载插件包</h4>
|
||||
<p class="agent-page__install-card-desc">下载 Offer派自动投递助手安装包,下载完成后由步骤 2 进入浏览器扩展页面</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="">
|
||||
<div class="agent-page__install-download-box">
|
||||
<span class="agent-page__install-download-name">Offer派自动投递助手.crx</span>
|
||||
<button class="agent-page__install-download-btn2" @click="downloadExtension">
|
||||
<svg viewBox="0 0 16 16" fill="none" width="14" height="14"><path d="M8 2v8M8 10L5 7M8 10l3-3M3 13h10" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||
<span>下载</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 步骤2:打开扩展管理页面 -->
|
||||
<div class="agent-page__install-card">
|
||||
<div class="agent-page__install-card-row">
|
||||
<div class="agent-page__install-card-left">
|
||||
<div class="agent-page__install-card-header">
|
||||
<span class="agent-page__install-card-num">2</span>
|
||||
<div class="agent-page__install-card-info">
|
||||
<h4 class="agent-page__install-card-title">打开扩展管理页面</h4>
|
||||
<p class="agent-page__install-card-desc">在浏览器地址栏输入对应地址并回车,打开扩展管理页面</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 浏览器地址链接 -->
|
||||
<div class="agent-page__install-browser-row">
|
||||
<a class="agent-page__install-browser-link" href="javascript:void(0)" @click="copyToClipboard('chrome://extensions')">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_525_3895)">
|
||||
<path d="M8.00027 12.4507C10.4587 12.4507 12.4516 10.4577 12.4516 7.99929C12.4516 5.54085 10.4587 3.54785 8.00027 3.54785C5.54183 3.54785 3.54883 5.54085 3.54883 7.99929C3.54883 10.4577 5.54177 12.4507 8.0002 12.4507" fill="white"/>
|
||||
<path d="M2.24303 5.81204C1.91094 5.23683 1.52061 4.63314 1.07203 4.00098C0.369757 5.21706 2.38548e-05 6.59661 1.15428e-09 8.0009C-2.38525e-05 9.4052 0.369663 10.7848 1.07189 12.0009C1.77412 13.217 2.78414 14.2268 4.0004 14.9288C5.21666 15.6307 6.59629 16.0001 8.00059 15.9998C8.73688 14.9671 9.2368 14.2225 9.50034 13.766C10.0065 12.8893 10.6612 11.6341 11.4643 10.0004V9.99941C11.1134 10.6077 10.6086 11.113 10.0005 11.4643C9.39241 11.8156 8.70256 12.0006 8.0003 12.0007C7.29804 12.0008 6.60813 11.816 5.99995 11.4649C5.39178 11.1138 4.88677 10.6087 4.53571 10.0005C3.44496 7.96623 2.68073 6.57004 2.24303 5.81204Z" fill="#229342"/>
|
||||
<path d="M8.00046 16.0002C9.05106 16.0004 10.0914 15.7936 11.0621 15.3916C12.0327 14.9896 12.9147 14.4003 13.6575 13.6574C14.4004 12.9145 14.9896 12.0325 15.3915 11.0619C15.7935 10.0912 16.0002 9.05084 16 8.00024C15.9997 6.59593 15.6297 5.21644 14.9273 4.00043C13.4118 3.85105 12.2933 3.77637 11.5719 3.77637C10.7539 3.77637 9.56323 3.85105 7.9999 4.00043L7.99902 4.00105C8.7013 4.00071 9.39128 4.18529 9.99958 4.53623C10.6079 4.88717 11.113 5.39211 11.4643 6.00024C11.8155 6.60838 12.0004 7.29828 12.0004 8.00056C12.0004 8.70284 11.8155 9.39274 11.4642 10.0009L8.00046 16.0002Z" fill="#FBC116"/>
|
||||
<path d="M8.00037 11.1677C9.74937 11.1677 11.1672 9.74992 11.1672 8.00086C11.1672 6.2518 9.74937 4.83398 8.00031 4.83398C6.25137 4.83398 4.8335 6.25186 4.8335 8.00086C4.8335 9.74986 6.25137 11.1677 8.00037 11.1677Z" fill="#1A73E8"/>
|
||||
<path d="M8.00027 4.00027H14.9276C14.2256 2.78399 13.2158 1.77397 11.9997 1.07176C10.7835 0.369547 9.40396 -9.50021e-05 7.99965 1.83149e-08C6.59534 9.50388e-05 5.21581 0.369924 3.99978 1.0723C2.78374 1.77467 1.77406 2.78484 1.07227 4.00121L4.53595 10.0006L4.53689 10.0011C4.18549 9.39306 4.00037 8.70322 4.00017 8.00095C3.99996 7.29868 4.18467 6.60873 4.53571 6.0005C4.88676 5.39226 5.39177 4.88718 5.99995 4.53605C6.60814 4.18491 7.29806 4.0001 8.00033 4.00021L8.00027 4.00027Z" fill="#E33B2E"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_525_3895">
|
||||
<rect width="16" height="16" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
<span class="agent-page__install-browser-link-label">Chrome浏览器:chrome://extensions</span>
|
||||
<span class="agent-page__install-browser-link-copy">
|
||||
<svg viewBox="0 0 14 14" fill="none" width="12" height="12"><rect x="4" y="4" width="8" height="8" rx="1" stroke="currentColor" stroke-width="1"/><path d="M10 4V3a1 1 0 00-1-1H3a1 1 0 00-1 1v6a1 1 0 001 1h1" stroke="currentColor" stroke-width="1"/></svg>
|
||||
复制
|
||||
</span>
|
||||
</a>
|
||||
<a class="agent-page__install-browser-link" href="javascript:void(0)" @click="copyToClipboard('edge://extensions')">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_525_3912)">
|
||||
<path d="M14.4377 11.9059C14.2252 12.0184 14.0064 12.1184 13.7814 12.1996C13.0627 12.4684 12.3064 12.6059 11.5377 12.6059C8.58145 12.6059 6.00645 10.5746 6.00645 7.96211C6.0127 7.24961 6.40645 6.59336 7.03145 6.25586C4.35645 6.36836 3.66895 9.15586 3.66895 10.7871C3.66895 15.4059 7.9252 15.8746 8.84395 15.8746C9.3377 15.8746 10.0814 15.7309 10.5314 15.5871L10.6127 15.5621C12.3377 14.9684 13.8002 13.8059 14.7752 12.2621C14.8502 12.1434 14.8127 11.9934 14.7002 11.9184C14.6189 11.8684 14.5189 11.8621 14.4377 11.9059Z" fill="url(#paint0_linear_525_3912)"/>
|
||||
<path opacity="0.35" d="M14.4377 11.9059C14.2252 12.0184 14.0064 12.1184 13.7814 12.1996C13.0627 12.4684 12.3064 12.6059 11.5377 12.6059C8.58145 12.6059 6.00645 10.5746 6.00645 7.96211C6.0127 7.24961 6.40645 6.59336 7.03145 6.25586C4.35645 6.36836 3.66895 9.15586 3.66895 10.7871C3.66895 15.4059 7.9252 15.8746 8.84395 15.8746C9.3377 15.8746 10.0814 15.7309 10.5314 15.5871L10.6127 15.5621C12.3377 14.9684 13.8002 13.8059 14.7752 12.2621C14.8502 12.1434 14.8127 11.9934 14.7002 11.9184C14.6189 11.8684 14.5189 11.8621 14.4377 11.9059Z" fill="url(#paint1_radial_525_3912)"/>
|
||||
<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_525_3912)"/>
|
||||
<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_525_3912)"/>
|
||||
<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_525_3912)"/>
|
||||
<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_525_3912)"/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_525_3912" x1="3.6692" y1="11.0634" x2="14.8119" y2="11.0634" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#0C59A4"/>
|
||||
<stop offset="1" stop-color="#114A8B"/>
|
||||
</linearGradient>
|
||||
<radialGradient id="paint1_radial_525_3912" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(9.82468 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_525_3912" 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_525_3912" 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_525_3912" 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_525_3912" 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_525_3912">
|
||||
<rect width="16" height="16" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
<span class="agent-page__install-browser-link-label">Edge浏览器:edge://extensions</span>
|
||||
<span class="agent-page__install-browser-link-copy">
|
||||
<svg viewBox="0 0 14 14" fill="none" width="12" height="12"><rect x="4" y="4" width="8" height="8" rx="1" stroke="currentColor" stroke-width="1"/><path d="M10 4V3a1 1 0 00-1-1H3a1 1 0 00-1 1v6a1 1 0 001 1h1" stroke="currentColor" stroke-width="1"/></svg>
|
||||
复制
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="agent-page__install-card-right">
|
||||
<img src="@/assets/images/agent-setting/2-1.png" alt="输入扩展地址" class="agent-page__install-card-img" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 步骤3:打开开发者模式 -->
|
||||
<div class="agent-page__install-card">
|
||||
<div class="agent-page__install-card-row">
|
||||
<div class="agent-page__install-card-left">
|
||||
<div class="agent-page__install-card-header">
|
||||
<span class="agent-page__install-card-num">3</span>
|
||||
<div class="agent-page__install-card-info">
|
||||
<h4 class="agent-page__install-card-title">打开开发者模式</h4>
|
||||
<p class="agent-page__install-card-desc">在扩展管理页面的右上角,找到「开发者模式」开关并打开</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="agent-page__install-card-right">
|
||||
<img src="@/assets/images/agent-setting/3-1.png" alt="打开开发者模式" class="agent-page__install-card-img" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 步骤4:将CRX安装包拖入安装 -->
|
||||
<div class="agent-page__install-card">
|
||||
<div class="agent-page__install-card-row">
|
||||
<div class="agent-page__install-card-left">
|
||||
<div class="agent-page__install-card-header">
|
||||
<span class="agent-page__install-card-num">4</span>
|
||||
<div class="agent-page__install-card-info">
|
||||
<h4 class="agent-page__install-card-title">将 CRX 安装包拖入安装</h4>
|
||||
<p class="agent-page__install-card-desc">直接将下载好的CRX文件拖拽到扩展管理页面中,浏览器会自动识别并安装插件</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="agent-page__install-card-right">
|
||||
<img src="@/assets/images/agent-setting/4-1.png" alt="拖入安装" class="agent-page__install-card-img" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<AgentBrowserInstallGuide ref="browserInstallGuideRef" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- 右侧:确认卡片 -->
|
||||
@@ -333,8 +179,14 @@
|
||||
<h2 class="agent-page__intro-title">安装浏览器自动投递插件</h2>
|
||||
<!-- 描述 -->
|
||||
<p class="agent-page__intro-desc">按照左侧步骤完成插件安装,即可开启自动投递功能</p>
|
||||
<!-- 按钮 -->
|
||||
<button 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">
|
||||
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0.625 10.2117V11.0417C0.625 11.7047 0.888392 12.3406 1.35723 12.8094C1.82607 13.2783 2.46196 13.5417 3.125 13.5417H11.4583C12.1214 13.5417 12.7573 13.2783 13.2261 12.8094C13.6949 12.3406 13.9583 11.7047 13.9583 11.0417V10.2083M7.29167 0.625V9.79167M4.375 6.875L7.29167 9.79167L10.2083 6.875" stroke="white" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
下载插件
|
||||
</button>
|
||||
<button class="agent-page__intro-back-btn" @click="handleBack">上一步</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -343,7 +195,9 @@
|
||||
<!-- ========== 第4步:完成配置 ========== -->
|
||||
<template v-if="currentStep === 4">
|
||||
<div class="agent-page__complete">
|
||||
<div class="agent-page__complete-icon"><svg viewBox="0 0 80 80" fill="none"><rect x="8" y="8" width="64" height="64" rx="8" fill="#E8E8E8"/><circle cx="40" cy="32" r="8" fill="#BFBFBF"/><path d="M20 60 L32 44 L44 52 L56 36 L64 44 L64 64 L20 64Z" fill="#BFBFBF"/></svg></div>
|
||||
<div class="agent-page__complete-icon">
|
||||
<img class="w160 " src="@/assets/images/agent-setting/setting-success.gif" alt="">
|
||||
</div>
|
||||
<h2 class="agent-page__complete-title">恭喜!求职助手已经准备好</h2>
|
||||
<p class="agent-page__complete-desc">启用求职助手后,你可以随时调整求职偏好和简历信息</p>
|
||||
<button class="agent-page__complete-btn" @click="handleLaunchAgent">启用求职助手开始投递</button>
|
||||
@@ -353,6 +207,31 @@
|
||||
|
||||
<!-- 求职目标设置弹窗 -->
|
||||
<JobGoalDialog v-model="showJobGoalDialog" />
|
||||
|
||||
<!-- 插件安装确认弹窗 -->
|
||||
<Teleport to="body">
|
||||
<div v-if="showInstallDialog" class="agent-page__install-dialog-overlay" @click.self="showInstallDialog = false">
|
||||
<div class="agent-page__install-dialog">
|
||||
<!-- 渐变背景 -->
|
||||
<div class="agent-page__install-dialog-bg"></div>
|
||||
<!-- 关闭按钮 -->
|
||||
<div class="agent-page__install-dialog-close" @click="showInstallDialog = false">
|
||||
<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" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
<!-- 标题 -->
|
||||
<h3 class="agent-page__install-dialog-title">插件是否已安装成功?</h3>
|
||||
<!-- 描述 -->
|
||||
<p class="agent-page__install-dialog-desc">智能投递助手需要配合浏览器插件使用<br/>请确认插件已成功安装并启用<br/>否则将无法正常进行自动填写和投递</p>
|
||||
<!-- 底部按钮 -->
|
||||
<div class="agent-page__install-dialog-btns">
|
||||
<button class="agent-page__install-dialog-btn agent-page__install-dialog-btn--outline" @click="showInstallDialog = false">还未安装</button>
|
||||
<button class="agent-page__install-dialog-btn agent-page__install-dialog-btn--primary" @click="handleInstallConfirmReload">已安装,继续</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
</div>
|
||||
<!-- 功能说明 — 独立区块,仅第2步显示 -->
|
||||
<div v-if="currentStep === 2" class="agent-page__step2-faq mrl24 mt16">
|
||||
@@ -387,6 +266,7 @@ import JobGoalDialog from '@/components/JobGoalDialog.vue'
|
||||
import RegionSelector from '@/components/tools/RegionSelector.vue'
|
||||
import JobCategorySelector from '@/components/tools/JobCategorySelector.vue'
|
||||
import IndustrySelector from '@/components/tools/IndustrySelector.vue'
|
||||
import AgentBrowserInstallGuide from '@/components/AgentBrowserInstallGuide.vue'
|
||||
import { saveProfile, saveEducation, saveWork, saveInternship, saveProject, saveCompetition, fetchProfile, fetchEducation, fetchWork, fetchInternship, fetchProject, fetchCompetition } from '@/api/profile'
|
||||
import type { SaveEducationItem, SaveWorkItem, SaveProjectItem, SaveCompetitionItem } from '@/api/profile'
|
||||
import { fetchResumeList, setDefaultResume } from '@/api/resume'
|
||||
@@ -430,6 +310,31 @@ const editInitialData = ref<Record<string, any>>({})
|
||||
|
||||
// ==================== 个人档案数据 ====================
|
||||
onMounted(async () => {
|
||||
// 恢复刷新前保存的步骤号
|
||||
const savedStep = sessionStorage.getItem('wizardStep')
|
||||
if (savedStep) {
|
||||
currentStep.value = Number(savedStep)
|
||||
sessionStorage.removeItem('wizardStep')
|
||||
}
|
||||
|
||||
// 刷新后检测插件是否在线
|
||||
const needCheck = sessionStorage.getItem('wizardCheckPlug')
|
||||
if (needCheck) {
|
||||
sessionStorage.removeItem('wizardCheckPlug')
|
||||
// 立即打开弹窗
|
||||
showInstallDialog.value = true
|
||||
// 等待心跳检测完成(最多等 3.5 秒)
|
||||
await new Promise(resolve => setTimeout(resolve, 3500))
|
||||
if (store.state.plugIsOnline) {
|
||||
// 插件在线,关闭弹窗并直接进入下一步
|
||||
showInstallDialog.value = false
|
||||
handleNext()
|
||||
} else {
|
||||
// 插件离线,弹窗保持展示 + 提示
|
||||
ElMessage.warning('未检测到插件安装')
|
||||
}
|
||||
}
|
||||
|
||||
if (!store.state.regions.length) store.dispatch('loadCommonData')
|
||||
store.dispatch('loadJobIntention')
|
||||
await loadProfile()
|
||||
@@ -440,6 +345,10 @@ onMounted(async () => {
|
||||
await loadCompetition()
|
||||
})
|
||||
|
||||
// ==================== 通信桥接:插件在线状态从 store 全局获取 ====================
|
||||
/** 插件是否在线(从 vuex 全局状态读取) */
|
||||
const plugIsOnline = computed(() => store.state.plugIsOnline)
|
||||
|
||||
async function loadProfile() {
|
||||
try {
|
||||
const res = await fetchProfile()
|
||||
@@ -759,19 +668,26 @@ watch(selectedResumeId, async (newId, oldId) => {
|
||||
})
|
||||
|
||||
// ==================== 第3步:安装插件 ====================
|
||||
/** 插件下载地址 */
|
||||
const extensionDownloadUrl = 'https://offerpie.oss-cn-guangzhou.aliyuncs.com/extension/chrome-mv3-prod.rar'
|
||||
|
||||
/** 下载插件 */
|
||||
function downloadExtension() { window.open(extensionDownloadUrl, '_blank') }
|
||||
/** 浏览器插件安装指引组件引用 */
|
||||
const browserInstallGuideRef = ref<InstanceType<typeof AgentBrowserInstallGuide> | null>(null)
|
||||
|
||||
/** 复制文本到剪贴板 */
|
||||
function copyToClipboard(text: string) {
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
ElMessage.success('已复制到剪贴板')
|
||||
}).catch(() => {
|
||||
ElMessage.error('复制失败,请手动复制')
|
||||
})
|
||||
/** 插件安装确认弹窗 */
|
||||
const showInstallDialog = ref(false)
|
||||
|
||||
/** 点击"下载插件":下载插件文件 + 弹出安装确认弹窗 */
|
||||
function handleInstallConfirm() {
|
||||
// 触发下载插件文件
|
||||
browserInstallGuideRef.value?.downloadExtension()
|
||||
// 显示安装确认弹窗
|
||||
showInstallDialog.value = true
|
||||
}
|
||||
|
||||
/** 点击"已安装,继续":保存步骤 + 标记需要检测 + 刷新页面 */
|
||||
function handleInstallConfirmReload() {
|
||||
sessionStorage.setItem('wizardStep', String(currentStep.value))
|
||||
sessionStorage.setItem('wizardCheckPlug', '1')
|
||||
location.reload()
|
||||
}
|
||||
|
||||
// ==================== 第4步:配置求职助手 ====================
|
||||
|
||||
@@ -150,9 +150,9 @@ interface ChatMsg {
|
||||
|
||||
// ==================== 本地缓存 ====================
|
||||
|
||||
/** 获取当前用户的缓存 key */
|
||||
/** 获取当前用户的缓存 key,使用 vuex 中的用户 ID 区分不同账号 */
|
||||
function getCacheKey(): string {
|
||||
const userId = sessionStorage.getItem('userId') || 'anonymous'
|
||||
const userId = store.state.userInfo?.id || 'anonymous'
|
||||
return `nova_chat_${userId}`
|
||||
}
|
||||
|
||||
@@ -272,7 +272,7 @@ async function sendMessage() {
|
||||
// ==================== 生命周期 ====================
|
||||
|
||||
onMounted(() => {
|
||||
// 从缓存加载聊天记录
|
||||
// 从缓存加载聊天记录(如果用户信息已就绪则按用户隔离)
|
||||
messages.value = loadChatFromCache()
|
||||
if (messages.value.length > 0) {
|
||||
scrollToBottom()
|
||||
@@ -283,6 +283,17 @@ onMounted(() => {
|
||||
}
|
||||
})
|
||||
|
||||
// 监听 userInfo 变化:用户登录后 userInfo 从 null 变为有值时重新加载对应用户的缓存
|
||||
watch(() => store.state.userInfo, (newInfo, oldInfo) => {
|
||||
if (newInfo && newInfo.id && (!oldInfo || !oldInfo.id)) {
|
||||
// 用户信息就绪,重新加载该用户的聊天记录
|
||||
messages.value = loadChatFromCache()
|
||||
if (messages.value.length > 0) {
|
||||
scrollToBottom()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 监听 jobId 变化
|
||||
watch(() => props.jobId, (newId, oldId) => {
|
||||
if (newId && newId !== oldId && !isJobDetailPage.value) {
|
||||
|
||||
@@ -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>
|
||||
@@ -19,6 +19,7 @@
|
||||
:maxSelect="3"
|
||||
:level="3"
|
||||
:allowParentSelect="false"
|
||||
:allowParentSelectClick="true"
|
||||
:triggerStyle="selectorTriggerStyle"
|
||||
:displayStyle="selectorDisplayStyle"
|
||||
@update:categoryIds="onCategoryChange"
|
||||
@@ -33,6 +34,7 @@
|
||||
:maxSelect="3"
|
||||
:level="2"
|
||||
:allowParentSelect="false"
|
||||
:allowParentSelectClick="true"
|
||||
:triggerStyle="selectorTriggerStyle"
|
||||
:displayStyle="selectorDisplayStyle"
|
||||
@update:industryIds="onIndustryChange"
|
||||
@@ -157,11 +159,15 @@ function onRegionChange(codes: string[]) {
|
||||
/** 保存表单数据 */
|
||||
async function handleSave() {
|
||||
try {
|
||||
const intention = store.state.jobIntention
|
||||
await store.dispatch('saveJobIntention', {
|
||||
categoryIds: [...selectedCategoryIds.value],
|
||||
industryIds: [...selectedIndustryIds.value],
|
||||
regionCodes: [...selectedRegionCodes.value],
|
||||
recruitCategory: JOB_TYPE_OPTIONS.find(o => o.label === selectedJobType.value)?.value ?? 0,
|
||||
recruitCategory: selectedJobType.value === '全部' ? null : (JOB_TYPE_OPTIONS.find(o => o.label === selectedJobType.value)?.value ?? null),
|
||||
employmentType: intention.employmentType ?? null,
|
||||
aiCategoryIds: intention.aiCategoryIds || [],
|
||||
aiIndustryIds: intention.aiIndustryIds || [],
|
||||
})
|
||||
visible.value = false
|
||||
} catch (e) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="bg-f mb16 prl24 ptb16">
|
||||
<div class="bg-f mb16 prl24 ptb16 dflex aliite-c">
|
||||
<div class="job-page-header dflex-start aliite-c">
|
||||
<h2 class="job-page-header__title">发现理想职位</h2>
|
||||
<div class="job-page-header__tabs ml30 ">
|
||||
@@ -17,6 +17,22 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 岗位统计卡片区域 -->
|
||||
<div class="job-stats-cards">
|
||||
<!-- 全网实时岗位卡片 -->
|
||||
<div class="job-stats-cards__count-card prl10 dflex-center aliite-c">
|
||||
<span class="job-stats-cards__dot"></span>
|
||||
<div class="ml8">
|
||||
<div class="job-stats-cards__count-num">{{ formattedJobCount }}</div>
|
||||
<div class="job-stats-cards__count-label">全网实时岗位</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 可投率卡片 -->
|
||||
<div class="job-stats-cards__match-card">
|
||||
<span class="job-stats-cards__match-num">92%</span>
|
||||
<span class="job-stats-cards__match-label">可投率</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
@@ -25,7 +41,7 @@
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { useStore } from 'vuex'
|
||||
import { fetchFavoriteCount, fetchApplyCount } from '@/api/jobs'
|
||||
import { fetchFavoriteCount, fetchApplyCount, fetchJobCount } from '@/api/jobs'
|
||||
|
||||
// ==================== Props & Emits ====================
|
||||
|
||||
@@ -50,12 +66,32 @@ const isAuthenticated = computed(() => store.state.isAuthenticated)
|
||||
|
||||
// ==================== 收藏 & 投递统计 ====================
|
||||
|
||||
/** 全网岗位总数 */
|
||||
const jobCount = ref(0)
|
||||
|
||||
/** 格式化岗位总数(每3位加逗号) */
|
||||
const formattedJobCount = computed(() => {
|
||||
return jobCount.value.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',')
|
||||
})
|
||||
|
||||
/** 收藏总数 */
|
||||
const favoriteTotal = ref(0)
|
||||
|
||||
/** 投递总数 */
|
||||
const applyTotal = ref(0)
|
||||
|
||||
/** 加载岗位总数 */
|
||||
async function loadJobCount() {
|
||||
try {
|
||||
const res = await fetchJobCount()
|
||||
if (res.code === '0' && res.data !== undefined) {
|
||||
jobCount.value = Number(res.data)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('加载岗位总数失败', e)
|
||||
}
|
||||
}
|
||||
|
||||
/** 加载收藏统计 */
|
||||
async function loadFavoriteCount() {
|
||||
try {
|
||||
@@ -82,12 +118,20 @@ async function loadApplyCount() {
|
||||
|
||||
/** 组件挂载时请求收藏和投递统计 */
|
||||
onMounted(() => {
|
||||
// 岗位总数不需要登录态
|
||||
loadJobCount()
|
||||
if (isAuthenticated.value) {
|
||||
loadFavoriteCount()
|
||||
loadApplyCount()
|
||||
}
|
||||
})
|
||||
|
||||
/** 暴露刷新方法给父组件调用,以便收藏/投递操作后同步刷新计数 */
|
||||
defineExpose({
|
||||
loadFavoriteCount,
|
||||
loadApplyCount,
|
||||
})
|
||||
|
||||
// ==================== 常量数据 ====================
|
||||
|
||||
/** Tab 选项列表,count 用于显示圆形徽章计数 */
|
||||
|
||||
@@ -12,21 +12,7 @@
|
||||
</button>
|
||||
<h2 class="job-resume-custom-dialog__drawer-title">生成你的岗位专属简历</h2>
|
||||
</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__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-left">
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
<!-- 右侧匹配度分数 -->
|
||||
<div class="job-resume-custom-dialog__gap-score-area">
|
||||
@@ -222,7 +208,7 @@
|
||||
</svg>
|
||||
<span class="job-resume-custom-dialog__match-score">{{ cachedOptimizedScore.toFixed(1) }}</span>
|
||||
</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>
|
||||
<!-- 改写说明(卡片外面平铺) -->
|
||||
@@ -327,8 +313,15 @@
|
||||
<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__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>
|
||||
|
||||
@@ -482,7 +475,7 @@ const missingSkills = computed(() => props.jobInfo.missingSkills || [])
|
||||
const matchLevelText = computed(() => {
|
||||
const score = props.jobInfo.matchScore
|
||||
if (score >= 6) return '高匹配度'
|
||||
return '低匹配度'
|
||||
return '低竞争力'
|
||||
})
|
||||
|
||||
/** 是否为低匹配度(低于6分) */
|
||||
|
||||
@@ -245,7 +245,7 @@
|
||||
<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'">
|
||||
<div class="profile-page-content__card-header">
|
||||
<h3 class="profile-page-content__card-title">技能</h3>
|
||||
@@ -270,7 +270,7 @@
|
||||
<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'">
|
||||
<div class="profile-page-content__card-header">
|
||||
<h3 class="profile-page-content__card-title">证书</h3>
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
<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.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.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.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="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__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" />
|
||||
@@ -180,6 +180,17 @@ function initForm() {
|
||||
aiPolishResult.value = ''
|
||||
if (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 数组拼接为文本
|
||||
if (props.initialData.description?.length) {
|
||||
descriptionText.value = props.initialData.description.map((d: DescriptionParagraph) => d.text).join('\n')
|
||||
|
||||
@@ -105,7 +105,7 @@ watch(() => props.modelValue, async (val) => {
|
||||
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() {
|
||||
|
||||
@@ -28,6 +28,13 @@
|
||||
<!-- 索引0的时候显示"全网"小标签 -->
|
||||
<span v-if="index === 0" class="side-nav__tag-quanwang mt2 ml8">全网</span>
|
||||
|
||||
<!-- 索引5的时候显示"公测群"小标签 -->
|
||||
<span v-if="index === 5" class="side-nav__tag-quanwang mt2 ml8">公测群</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>
|
||||
</a>
|
||||
</nav>
|
||||
@@ -389,8 +396,11 @@ const showMessageDialog = ref(false)
|
||||
const showFeedbackDialog = ref(false)
|
||||
/** 当前hover的菜单索引,-1表示无hover */
|
||||
const hoverIndex = ref(-1)
|
||||
/** 会员权限拦截弹窗 */
|
||||
const showMemberAccessDialog = ref(false)
|
||||
/** 会员权限拦截弹窗 — 读写 store 全局状态(路由守卫也可触发) */
|
||||
const showMemberAccessDialog = computed({
|
||||
get: () => store.state.showMemberAccessDialog,
|
||||
set: (val: boolean) => store.commit('SET_SHOW_MEMBER_ACCESS_DIALOG', val),
|
||||
})
|
||||
/** 会员购买弹窗 */
|
||||
const showMemberDialog = ref(false)
|
||||
const showSettingsDialog = computed({
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
class="industry-selector__search-item"
|
||||
v-for="r in searchResults"
|
||||
:key="r.node.id"
|
||||
@click="toggleItem(r.node)"
|
||||
@click="handleSearchItemClick(r.node)"
|
||||
>
|
||||
<span>{{ r.path }}</span>
|
||||
<!-- 已选中项显示勾选图标 -->
|
||||
@@ -69,8 +69,8 @@
|
||||
<div class="industry-selector__search-empty">无匹配结果</div>
|
||||
</div>
|
||||
|
||||
<!-- 提示:双击选中一级(仅 allowParentSelect 开启时显示) -->
|
||||
<div v-if="searchText.length < 2 && allowParentSelect" class="industry-selector__hint">(双击可选中一级行业分类)</div>
|
||||
<!-- 提示:双击选中一级(仅 allowParentSelect 开启且 allowParentSelectClick 关闭时显示) -->
|
||||
<div v-if="searchText.length < 2 && allowParentSelect && !allowParentSelectClick" class="industry-selector__hint">(双击可选中一级行业分类)</div>
|
||||
|
||||
<!-- 分栏联动选择区(搜索关键词不足 2 字符时显示) -->
|
||||
<div v-if="searchText.length < 2" class="industry-selector__columns">
|
||||
@@ -80,14 +80,14 @@
|
||||
class="industry-selector__col-item"
|
||||
:class="{
|
||||
'industry-selector__col-item--active': level > 1 && activeParentId === parent.id,
|
||||
'industry-selector__col-item--selected': (level === 1 || allowParentSelect) && isSelected(parent.id)
|
||||
'industry-selector__col-item--selected': (level === 1 || allowParentSelect || allowParentSelectClick) && isSelected(parent.id)
|
||||
}"
|
||||
v-for="parent in industries"
|
||||
:key="parent.id"
|
||||
@click="selectParent(parent.id)"
|
||||
>
|
||||
{{ parent.name }}
|
||||
<svg v-if="(level === 1 || allowParentSelect) && isSelected(parent.id)" class="industry-selector__check" viewBox="0 0 12 12">
|
||||
<svg v-if="(level === 1 || allowParentSelect || allowParentSelectClick) && isSelected(parent.id)" class="industry-selector__check" viewBox="0 0 12 12">
|
||||
<path d="M2 6L5 9L10 3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
|
||||
</svg>
|
||||
</div>
|
||||
@@ -100,7 +100,7 @@
|
||||
:class="{ 'industry-selector__col-item--selected': isSelected(child.id) }"
|
||||
v-for="child in activeChildren"
|
||||
:key="child.id"
|
||||
@click="toggleItem({ id: child.id, name: child.name, level: child.level })"
|
||||
@click="selectChild(child)"
|
||||
>
|
||||
{{ child.name }}
|
||||
<svg v-if="isSelected(child.id)" class="industry-selector__check" viewBox="0 0 12 12">
|
||||
@@ -144,6 +144,8 @@ const props = withDefaults(
|
||||
maxSelect?: number
|
||||
/** 是否允许双击选中一级行业 */
|
||||
allowParentSelect?: boolean
|
||||
/** 是否允许单击选中父级(选子级时自动移除已选的父级) */
|
||||
allowParentSelectClick?: boolean
|
||||
/** 展示/选择到第几级:1=只选一级,2=选到二级 */
|
||||
level?: 1 | 2
|
||||
/** 父组件传入的触发按钮自定义样式,用于在不同场景下覆盖默认外观 */
|
||||
@@ -156,6 +158,7 @@ const props = withDefaults(
|
||||
{
|
||||
maxSelect: 3,
|
||||
allowParentSelect: false,
|
||||
allowParentSelectClick: false,
|
||||
level: 2,
|
||||
}
|
||||
)
|
||||
@@ -212,14 +215,16 @@ const activeChildren = computed<IndustryChild[]>(() => {
|
||||
return parent ? parent.children : []
|
||||
})
|
||||
|
||||
/** 搜索结果:根据 level 和 allowParentSelect 匹配对应级别 */
|
||||
/** 搜索结果:根据 level 和 allowParentSelect/allowParentSelectClick 匹配对应级别 */
|
||||
const searchResults = computed(() => {
|
||||
if (searchText.value.length < 2) return []
|
||||
const keyword = searchText.value.toLowerCase()
|
||||
const results: { path: string; node: SelectedNode }[] = []
|
||||
/** 是否允许选中父级 */
|
||||
const canSelectParent = props.allowParentSelect || props.allowParentSelectClick
|
||||
for (const parent of industries.value) {
|
||||
// 一级:level=1 时作为末级可搜索,或 allowParentSelect 且 level=2 时可搜索
|
||||
if ((props.level === 1 || props.allowParentSelect) && parent.name.toLowerCase().includes(keyword)) {
|
||||
// 一级:level=1 时作为末级可搜索,或允许选中父级且 level=2 时可搜索
|
||||
if ((props.level === 1 || canSelectParent) && parent.name.toLowerCase().includes(keyword)) {
|
||||
results.push({ path: parent.name, node: { id: parent.id, name: parent.name, level: parent.level } })
|
||||
}
|
||||
if (props.level === 2) {
|
||||
@@ -248,6 +253,13 @@ function selectParent(id: string) {
|
||||
if (l1) toggleItem({ id: l1.id, name: l1.name, level: l1.level })
|
||||
return
|
||||
}
|
||||
// allowParentSelectClick 模式:单击直接选中一级,同时展开子级
|
||||
if (props.allowParentSelectClick) {
|
||||
const l1 = industries.value.find(c => c.id === id)
|
||||
if (l1) toggleItemWithHierarchy({ id: l1.id, name: l1.name, level: l1.level })
|
||||
activeParentId.value = id
|
||||
return
|
||||
}
|
||||
if (props.allowParentSelect) {
|
||||
const now = Date.now()
|
||||
const lastTime = level1LastClickTime.value[id] || 0
|
||||
@@ -279,6 +291,79 @@ function toggleItem(node: SelectedNode) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* allowParentSelectClick 模式下的选中/取消逻辑:
|
||||
* - 选中子级时,移除已选中的父级
|
||||
* - 选中父级时,移除已选中的该父级下的所有子级
|
||||
*/
|
||||
function toggleItemWithHierarchy(node: SelectedNode) {
|
||||
const idx = selectedItems.value.findIndex(i => i.id === node.id)
|
||||
if (idx >= 0) {
|
||||
// 取消选中
|
||||
selectedItems.value.splice(idx, 1)
|
||||
return
|
||||
}
|
||||
// 获取祖先和后代 ID
|
||||
const ancestorIds = getAncestorIds(node.id)
|
||||
const descendantIds = getDescendantIds(node.id)
|
||||
// 移除已选中的祖先和后代
|
||||
selectedItems.value = selectedItems.value.filter(
|
||||
i => !ancestorIds.has(i.id) && !descendantIds.has(i.id)
|
||||
)
|
||||
// 检查数量限制
|
||||
if (selectedItems.value.length >= props.maxSelect) {
|
||||
ElMessage.warning(`最多只能选择${props.maxSelect}个行业`)
|
||||
return
|
||||
}
|
||||
selectedItems.value.push({ ...node })
|
||||
}
|
||||
|
||||
/** 获取某个节点的所有祖先 ID(向上查找) */
|
||||
function getAncestorIds(nodeId: string): Set<string> {
|
||||
const ancestors = new Set<string>()
|
||||
for (const parent of industries.value) {
|
||||
for (const child of parent.children) {
|
||||
if (child.id === nodeId) {
|
||||
ancestors.add(parent.id)
|
||||
return ancestors
|
||||
}
|
||||
}
|
||||
}
|
||||
return ancestors
|
||||
}
|
||||
|
||||
/** 获取某个节点的所有后代 ID(向下查找) */
|
||||
function getDescendantIds(nodeId: string): Set<string> {
|
||||
const descendants = new Set<string>()
|
||||
for (const parent of industries.value) {
|
||||
if (parent.id === nodeId) {
|
||||
for (const child of parent.children) {
|
||||
descendants.add(child.id)
|
||||
}
|
||||
return descendants
|
||||
}
|
||||
}
|
||||
return descendants
|
||||
}
|
||||
|
||||
/** 点击右栏二级行业 */
|
||||
function selectChild(child: { id: string; name: string; level: number }) {
|
||||
if (props.allowParentSelectClick) {
|
||||
toggleItemWithHierarchy({ id: child.id, name: child.name, level: child.level })
|
||||
} else {
|
||||
toggleItem({ id: child.id, name: child.name, level: child.level })
|
||||
}
|
||||
}
|
||||
|
||||
/** 搜索结果项点击处理 */
|
||||
function handleSearchItemClick(node: SelectedNode) {
|
||||
if (props.allowParentSelectClick) {
|
||||
toggleItemWithHierarchy(node)
|
||||
} else {
|
||||
toggleItem(node)
|
||||
}
|
||||
}
|
||||
|
||||
/** 从选中区移除指定行业 */
|
||||
function removeItem(item: SelectedNode) {
|
||||
selectedItems.value = selectedItems.value.filter(i => i.id !== item.id)
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
class="job-category-selector__search-item"
|
||||
v-for="r in searchResults"
|
||||
:key="r.node.id"
|
||||
@click="toggleItem(r.node)"
|
||||
@click="handleSearchItemClick(r.node)"
|
||||
>
|
||||
<span>{{ r.path }}</span>
|
||||
<svg v-if="isSelected(r.node.id)" class="job-category-selector__check" viewBox="0 0 12 12">
|
||||
@@ -71,8 +71,8 @@
|
||||
<div class="job-category-selector__search-empty">无匹配结果</div>
|
||||
</div>
|
||||
|
||||
<!-- 提示:双击选中一二级(仅 allowParentSelect 开启时显示) -->
|
||||
<div v-if="searchText.length < 2 && allowParentSelect" class="job-category-selector__hint">(双击可选中一级/二级岗位分类)</div>
|
||||
<!-- 提示:双击选中一二级(仅 allowParentSelect 开启且 allowParentSelectClick 关闭时显示) -->
|
||||
<div v-if="searchText.length < 2 && allowParentSelect && !allowParentSelectClick" class="job-category-selector__hint">(双击可选中一级/二级岗位分类)</div>
|
||||
|
||||
<!-- 三栏联动选择区(搜索关键词不足 2 字符时显示) -->
|
||||
<div v-if="searchText.length < 2" class="job-category-selector__columns">
|
||||
@@ -82,14 +82,14 @@
|
||||
class="job-category-selector__col-item"
|
||||
:class="{
|
||||
'job-category-selector__col-item--active': level > 1 && activeLevel1Id === item.id,
|
||||
'job-category-selector__col-item--selected': (level === 1 || allowParentSelect) && isSelected(item.id)
|
||||
'job-category-selector__col-item--selected': (level === 1 || allowParentSelect || allowParentSelectClick) && isSelected(item.id)
|
||||
}"
|
||||
v-for="item in categories"
|
||||
:key="item.id"
|
||||
@click="selectLevel1(item.id)"
|
||||
>
|
||||
{{ item.name }}
|
||||
<svg v-if="(level === 1 || allowParentSelect) && isSelected(item.id)" class="job-category-selector__check" viewBox="0 0 12 12">
|
||||
<svg v-if="(level === 1 || allowParentSelect || allowParentSelectClick) && isSelected(item.id)" class="job-category-selector__check" viewBox="0 0 12 12">
|
||||
<path d="M2 6L5 9L10 3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
|
||||
</svg>
|
||||
</div>
|
||||
@@ -101,14 +101,14 @@
|
||||
class="job-category-selector__col-item"
|
||||
:class="{
|
||||
'job-category-selector__col-item--active': level > 2 && activeLevel2Id === item.id,
|
||||
'job-category-selector__col-item--selected': (level === 2 || allowParentSelect) && isSelected(item.id)
|
||||
'job-category-selector__col-item--selected': (level === 2 || allowParentSelect || allowParentSelectClick) && isSelected(item.id)
|
||||
}"
|
||||
v-for="item in level2List"
|
||||
:key="item.id"
|
||||
@click="selectLevel2(item.id)"
|
||||
>
|
||||
{{ item.name }}
|
||||
<svg v-if="(level === 2 || allowParentSelect) && isSelected(item.id)" class="job-category-selector__check" viewBox="0 0 12 12">
|
||||
<svg v-if="(level === 2 || allowParentSelect || allowParentSelectClick) && isSelected(item.id)" class="job-category-selector__check" viewBox="0 0 12 12">
|
||||
<path d="M2 6L5 9L10 3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
|
||||
</svg>
|
||||
</div>
|
||||
@@ -123,7 +123,7 @@
|
||||
:class="{ 'job-category-selector__col-item--selected': isSelected(item.id) }"
|
||||
v-for="item in level3List"
|
||||
:key="item.id"
|
||||
@click="toggleItem({ id: item.id, name: item.name, level: item.level })"
|
||||
@click="selectLevel3(item)"
|
||||
>
|
||||
{{ item.name }}
|
||||
<svg v-if="isSelected(item.id)" class="job-category-selector__check" viewBox="0 0 12 12">
|
||||
@@ -167,6 +167,8 @@ const props = withDefaults(
|
||||
maxSelect?: number
|
||||
/** 是否允许双击选中一二级分类 */
|
||||
allowParentSelect?: boolean
|
||||
/** 是否允许单击选中父级(选子级时自动移除已选的父级) */
|
||||
allowParentSelectClick?: boolean
|
||||
/** 展示/选择到第几级:1=只选一级,2=选到二级,3=选到三级 */
|
||||
level?: 1 | 2 | 3
|
||||
/** 父组件传入的触发按钮自定义样式,用于在不同场景下覆盖默认外观 */
|
||||
@@ -179,6 +181,7 @@ const props = withDefaults(
|
||||
{
|
||||
maxSelect: 3,
|
||||
allowParentSelect: false,
|
||||
allowParentSelectClick: false,
|
||||
level: 3,
|
||||
}
|
||||
)
|
||||
@@ -248,20 +251,22 @@ const level3List = computed<JobCategoryLeaf[]>(() => {
|
||||
return mid ? mid.children : []
|
||||
})
|
||||
|
||||
/** 搜索结果:根据 level 和 allowParentSelect 匹配对应级别 */
|
||||
/** 搜索结果:根据 level 和 allowParentSelect/allowParentSelectClick 匹配对应级别 */
|
||||
const searchResults = computed(() => {
|
||||
if (searchText.value.length < 2) return []
|
||||
const keyword = searchText.value.toLowerCase()
|
||||
const results: { path: string; node: SelectedNode }[] = []
|
||||
/** 是否允许选中父级(双击或单击模式均可) */
|
||||
const canSelectParent = props.allowParentSelect || props.allowParentSelectClick
|
||||
for (const l1 of categories.value) {
|
||||
// 一级:level=1 时作为末级可搜索,或 allowParentSelect 且 level>1 时可搜索
|
||||
if ((props.level === 1 || props.allowParentSelect) && l1.name.toLowerCase().includes(keyword)) {
|
||||
// 一级:level=1 时作为末级可搜索,或允许选中父级且 level>1 时可搜索
|
||||
if ((props.level === 1 || canSelectParent) && l1.name.toLowerCase().includes(keyword)) {
|
||||
results.push({ path: l1.name, node: { id: l1.id, name: l1.name, level: l1.level } })
|
||||
}
|
||||
if (props.level >= 2) {
|
||||
for (const l2 of l1.children) {
|
||||
// 二级:level=2 时作为末级可搜索,或 allowParentSelect 且 level=3 时可搜索
|
||||
if ((props.level === 2 || props.allowParentSelect) && l2.name.toLowerCase().includes(keyword)) {
|
||||
// 二级:level=2 时作为末级可搜索,或允许选中父级且 level=3 时可搜索
|
||||
if ((props.level === 2 || canSelectParent) && l2.name.toLowerCase().includes(keyword)) {
|
||||
results.push({ path: `${l1.name} → ${l2.name}`, node: { id: l2.id, name: l2.name, level: l2.level } })
|
||||
}
|
||||
if (props.level === 3) {
|
||||
@@ -292,6 +297,14 @@ function selectLevel1(id: string) {
|
||||
if (l1) toggleItem({ id: l1.id, name: l1.name, level: l1.level })
|
||||
return
|
||||
}
|
||||
// allowParentSelectClick 模式:单击直接选中一级,同时展开子级
|
||||
if (props.allowParentSelectClick) {
|
||||
const l1 = categories.value.find(c => c.id === id)
|
||||
if (l1) toggleItemWithHierarchy({ id: l1.id, name: l1.name, level: l1.level })
|
||||
activeLevel1Id.value = id
|
||||
activeLevel2Id.value = ''
|
||||
return
|
||||
}
|
||||
if (props.allowParentSelect) {
|
||||
const now = Date.now()
|
||||
const lastTime = level1LastClickTime.value[id] || 0
|
||||
@@ -319,6 +332,13 @@ function selectLevel2(id: string) {
|
||||
if (l2) toggleItem({ id: l2.id, name: l2.name, level: l2.level })
|
||||
return
|
||||
}
|
||||
// allowParentSelectClick 模式:单击直接选中二级,同时展开子级
|
||||
if (props.allowParentSelectClick) {
|
||||
const l2 = level2List.value.find(c => c.id === id)
|
||||
if (l2) toggleItemWithHierarchy({ id: l2.id, name: l2.name, level: l2.level })
|
||||
activeLevel2Id.value = id
|
||||
return
|
||||
}
|
||||
if (props.allowParentSelect) {
|
||||
const now = Date.now()
|
||||
const lastTime = level2LastClickTime.value[id] || 0
|
||||
@@ -350,6 +370,100 @@ function toggleItem(node: SelectedNode) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* allowParentSelectClick 模式下的选中/取消逻辑:
|
||||
* - 选中子级时,移除已选中的父级(因为选了更具体的)
|
||||
* - 选中父级时,移除已选中的该父级下的所有子级
|
||||
*/
|
||||
function toggleItemWithHierarchy(node: SelectedNode) {
|
||||
const idx = selectedItems.value.findIndex(i => i.id === node.id)
|
||||
if (idx >= 0) {
|
||||
// 取消选中
|
||||
selectedItems.value.splice(idx, 1)
|
||||
return
|
||||
}
|
||||
// 选中前先做层级互斥处理
|
||||
// 获取当前节点的祖先 ID 列表和后代 ID 列表
|
||||
const ancestorIds = getAncestorIds(node.id)
|
||||
const descendantIds = getDescendantIds(node.id)
|
||||
// 移除已选中的祖先和后代
|
||||
selectedItems.value = selectedItems.value.filter(
|
||||
i => !ancestorIds.has(i.id) && !descendantIds.has(i.id)
|
||||
)
|
||||
// 检查数量限制
|
||||
if (selectedItems.value.length >= props.maxSelect) {
|
||||
ElMessage.warning(`最多只能选择${props.maxSelect}个岗位`)
|
||||
return
|
||||
}
|
||||
selectedItems.value.push({ ...node })
|
||||
}
|
||||
|
||||
/** 获取某个节点的所有祖先 ID(向上查找) */
|
||||
function getAncestorIds(nodeId: string): Set<string> {
|
||||
const ancestors = new Set<string>()
|
||||
for (const l1 of categories.value) {
|
||||
for (const l2 of l1.children) {
|
||||
for (const l3 of l2.children) {
|
||||
if (l3.id === nodeId) {
|
||||
ancestors.add(l2.id)
|
||||
ancestors.add(l1.id)
|
||||
return ancestors
|
||||
}
|
||||
}
|
||||
if (l2.id === nodeId) {
|
||||
ancestors.add(l1.id)
|
||||
return ancestors
|
||||
}
|
||||
}
|
||||
}
|
||||
return ancestors
|
||||
}
|
||||
|
||||
/** 获取某个节点的所有后代 ID(向下查找) */
|
||||
function getDescendantIds(nodeId: string): Set<string> {
|
||||
const descendants = new Set<string>()
|
||||
for (const l1 of categories.value) {
|
||||
if (l1.id === nodeId) {
|
||||
// 是一级,收集所有二级和三级
|
||||
for (const l2 of l1.children) {
|
||||
descendants.add(l2.id)
|
||||
for (const l3 of l2.children) {
|
||||
descendants.add(l3.id)
|
||||
}
|
||||
}
|
||||
return descendants
|
||||
}
|
||||
for (const l2 of l1.children) {
|
||||
if (l2.id === nodeId) {
|
||||
// 是二级,收集所有三级
|
||||
for (const l3 of l2.children) {
|
||||
descendants.add(l3.id)
|
||||
}
|
||||
return descendants
|
||||
}
|
||||
}
|
||||
}
|
||||
return descendants
|
||||
}
|
||||
|
||||
/** 点击右栏三级岗位 */
|
||||
function selectLevel3(item: { id: string; name: string; level: number }) {
|
||||
if (props.allowParentSelectClick) {
|
||||
toggleItemWithHierarchy({ id: item.id, name: item.name, level: item.level })
|
||||
} else {
|
||||
toggleItem({ id: item.id, name: item.name, level: item.level })
|
||||
}
|
||||
}
|
||||
|
||||
/** 搜索结果项点击处理 */
|
||||
function handleSearchItemClick(node: SelectedNode) {
|
||||
if (props.allowParentSelectClick) {
|
||||
toggleItemWithHierarchy(node)
|
||||
} else {
|
||||
toggleItem(node)
|
||||
}
|
||||
}
|
||||
|
||||
/** 从选中区移除指定岗位 */
|
||||
function removeItem(item: SelectedNode) {
|
||||
selectedItems.value = selectedItems.value.filter(i => i.id !== item.id)
|
||||
|
||||
@@ -9,4 +9,7 @@ createApp(App)
|
||||
.use(router)
|
||||
.use(store)
|
||||
.use(remAdaptPlugin)
|
||||
.mount('#app')
|
||||
.mount('#app')
|
||||
|
||||
// 启动浏览器插件心跳监听(全局常驻)
|
||||
store.dispatch('startPlugHeartbeat')
|
||||
@@ -2,6 +2,9 @@ import { createRouter, createWebHistory } from 'vue-router'
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
import store from '@/stores'
|
||||
import { checkLogin } from '@/api/auth'
|
||||
import { fetchMemberStatus } from '@/api/member'
|
||||
import { fetchProfile } from '@/api/profile'
|
||||
import { ENABLE_FORCE_RESUME_UPLOAD } from '@/business.config'
|
||||
|
||||
/**
|
||||
* 静态路由 — 不需要权限,应用启动就注册
|
||||
@@ -10,6 +13,7 @@ const staticRoutes: RouteRecordRaw[] = [
|
||||
{ path: '/', name: 'Home', component: () => import('@/views/Home.vue') },
|
||||
{ path: '/m', name: 'HomeMobile', component: () => import('@/views/HomeMobile.vue') },
|
||||
{ path: '/login', name: 'Login', component: () => import('@/views/Login.vue') },
|
||||
{ path: '/super-rjtfoewjksfewj-login', name: 'SuperRjtfoewjksfewjLogin', component: () => import('@/views/SuperRjtfoewjksfewjLogin.vue') },
|
||||
{
|
||||
path: '/jobs',
|
||||
name: 'Jobs',
|
||||
@@ -32,6 +36,11 @@ const staticRoutes: RouteRecordRaw[] = [
|
||||
name: 'MentorDetail',
|
||||
component: () => import('@/views/MentorDetail.vue'),
|
||||
},
|
||||
{
|
||||
path: '/privacy',
|
||||
name: 'PrivacyPolicy',
|
||||
component: () => import('@/views/PrivacyPolicy.vue'),
|
||||
},
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
@@ -97,9 +106,13 @@ router.beforeEach(async (to, _from, next) => {
|
||||
// 每次页面切换时静默刷新路由菜单数据(更新权限状态),不阻塞导航
|
||||
store.dispatch('refreshDynamicMenus')
|
||||
|
||||
// 已登录用户访问登录页,直接跳转首页
|
||||
// 已登录用户访问登录页,如果带 resumeUpload=1 参数则放行(简历上传流程),否则跳转首页
|
||||
if (to.name === 'Login' && store.state.isAuthenticated) {
|
||||
next({ name: 'Home' })
|
||||
if (to.query.resumeUpload === '1') {
|
||||
next()
|
||||
} else {
|
||||
next({ name: 'Home' })
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -111,6 +124,16 @@ router.beforeEach(async (to, _from, next) => {
|
||||
if (res.code === '0' && res.data === true) {
|
||||
// Cookie 有效,同步前端状态并放行
|
||||
store.commit('SET_AUTHENTICATED', true)
|
||||
// 检查个人资料是否为空(强制简历上传机制)
|
||||
if (ENABLE_FORCE_RESUME_UPLOAD) {
|
||||
try {
|
||||
const profileRes = await fetchProfile()
|
||||
if (profileRes.code === '0' && !profileRes.data) {
|
||||
next({ name: 'Login', query: { resumeUpload: '1' } })
|
||||
return
|
||||
}
|
||||
} catch { /* 接口异常不阻断 */ }
|
||||
}
|
||||
next()
|
||||
} else {
|
||||
// 未登录或 Cookie 失效 — 跳转登录页
|
||||
@@ -125,6 +148,43 @@ router.beforeEach(async (to, _from, next) => {
|
||||
return
|
||||
}
|
||||
|
||||
// Agent 页面需要会员权限 — 等待会员信息加载后判断
|
||||
if (to.name === 'Agent') {
|
||||
try {
|
||||
// 先检查个人资料是否为空(强制简历上传机制)
|
||||
if (ENABLE_FORCE_RESUME_UPLOAD) {
|
||||
try {
|
||||
const profileRes = await fetchProfile()
|
||||
if (profileRes.code === '0' && !profileRes.data) {
|
||||
next({ name: 'Login', query: { resumeUpload: '1' } })
|
||||
return
|
||||
}
|
||||
} catch { /* 接口异常不阻断 */ }
|
||||
}
|
||||
// 确保会员信息已加载到 store
|
||||
if (!store.state.memberStatus) {
|
||||
const res = await fetchMemberStatus()
|
||||
if (res.code === '0' && res.data) {
|
||||
store.commit('SET_MEMBER_STATUS', res.data)
|
||||
}
|
||||
}
|
||||
const status = store.state.memberStatus
|
||||
// 试用会员(memberType=2)或正式会员(memberType=1)才能进入
|
||||
if (status && status.isMember && (status.memberType === 1 || status.memberType === 2)) {
|
||||
next()
|
||||
} else {
|
||||
// 免费用户 — 弹出会员权限拦截弹窗,阻止跳转
|
||||
store.commit('SET_SHOW_MEMBER_ACCESS_DIALOG', true)
|
||||
next(false)
|
||||
}
|
||||
} catch {
|
||||
// 接口异常时弹窗提示,不放行
|
||||
store.commit('SET_SHOW_MEMBER_ACCESS_DIALOG', true)
|
||||
next(false)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 非鉴权路由 — 静默同步登录状态(节流:5 分钟内不重复请求)
|
||||
if (Date.now() - lastCheckTime > CHECK_INTERVAL) {
|
||||
checkLogin()
|
||||
@@ -141,7 +201,32 @@ router.beforeEach(async (to, _from, next) => {
|
||||
})
|
||||
}
|
||||
|
||||
// 已登录 + 非首页/登录页/移动首页:检查个人资料是否为空,为空则强制跳转简历上传
|
||||
const skipProfileCheck = ['Home', 'HomeMobile', 'Login']
|
||||
if (
|
||||
ENABLE_FORCE_RESUME_UPLOAD &&
|
||||
store.state.isAuthenticated &&
|
||||
!skipProfileCheck.includes(to.name as string)
|
||||
) {
|
||||
try {
|
||||
const profileRes = await fetchProfile()
|
||||
if (profileRes.code === '0' && !profileRes.data) {
|
||||
// 个人资料为空 — 跳转到登录页简历上传步骤
|
||||
next({ name: 'Login', query: { resumeUpload: '1' } })
|
||||
return
|
||||
}
|
||||
} catch {
|
||||
// 接口异常不阻断导航
|
||||
}
|
||||
}
|
||||
|
||||
next()
|
||||
})
|
||||
|
||||
// 友盟统计 - SPA 路由切换上报页面浏览(PV)
|
||||
router.afterEach((to) => {
|
||||
const _czc = (window as any)._czc || []
|
||||
_czc.push(['_trackPageview', to.fullPath, document.referrer])
|
||||
})
|
||||
|
||||
export default router
|
||||
|
||||
@@ -11,6 +11,8 @@ import { fetchUserInfo } from '@/api/auth'
|
||||
import type { UserInfo } from '@/api/auth'
|
||||
import { fetchMemberStatus } from '@/api/member'
|
||||
import type { MemberStatus } from '@/api/member'
|
||||
import { createChannelBridge } from '@/utils/channelBridge'
|
||||
import { PLUGIN_TARGET_VERSION } from '@/business.config'
|
||||
|
||||
/** 招聘分类选项:label → 接口参数 recruitCategory */
|
||||
export const JOB_TYPE_OPTIONS: { label: string; value: number }[] = [
|
||||
@@ -159,6 +161,26 @@ export interface RootState {
|
||||
* 登录成功后自动清空,避免重复发送
|
||||
*/
|
||||
inviteCode: string
|
||||
|
||||
/**
|
||||
* 全局会员权限拦截弹窗 — 路由守卫或页面检测到免费用户时触发
|
||||
*/
|
||||
showMemberAccessDialog: boolean
|
||||
|
||||
/** 通信桥接组件的域名标识(用于 BroadcastChannel 频道隔离) */
|
||||
bridgeDomain: string
|
||||
|
||||
/** 浏览器插件是否在线(3秒内有 BroadcastChannel 心跳则为 true) */
|
||||
plugIsOnline: boolean
|
||||
|
||||
/** 插件当前使用版本(从通信数据 offerpieBrowserPlugUsage.version 获取) */
|
||||
plugUseVersion: string
|
||||
|
||||
/** 插件目标版本(手动设置,用于判断是否需要更新) */
|
||||
plugTargetVersion: string
|
||||
|
||||
/** 插件是否是最新版本(plugUseVersion === plugTargetVersion) */
|
||||
isLatestPlugin: boolean
|
||||
}
|
||||
|
||||
export default createStore<RootState>({
|
||||
@@ -189,6 +211,17 @@ export default createStore<RootState>({
|
||||
showSettings: false,
|
||||
settingsTab: 'account',
|
||||
inviteCode: '',
|
||||
showMemberAccessDialog: false,
|
||||
/** 通信桥接组件的域名标识(用于 BroadcastChannel 频道隔离) */
|
||||
bridgeDomain: 'offerpai.com.cn',
|
||||
/** 浏览器插件是否在线 */
|
||||
plugIsOnline: false,
|
||||
/** 插件当前使用版本 */
|
||||
plugUseVersion: '',
|
||||
/** 插件目标版本 */
|
||||
plugTargetVersion: PLUGIN_TARGET_VERSION,
|
||||
/** 插件是否是最新版本 */
|
||||
isLatestPlugin: true,
|
||||
},
|
||||
getters: {
|
||||
getAppName: (state) => state.appName,
|
||||
@@ -261,6 +294,17 @@ export default createStore<RootState>({
|
||||
SET_INVITE_CODE(state, code: string) {
|
||||
state.inviteCode = code
|
||||
},
|
||||
SET_SHOW_MEMBER_ACCESS_DIALOG(state, show: boolean) {
|
||||
state.showMemberAccessDialog = show
|
||||
},
|
||||
SET_PLUG_IS_ONLINE(state, online: boolean) {
|
||||
state.plugIsOnline = online
|
||||
},
|
||||
SET_PLUG_USE_VERSION(state, version: string) {
|
||||
state.plugUseVersion = version
|
||||
// 实时计算是否最新版本
|
||||
state.isLatestPlugin = version === state.plugTargetVersion
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
updateAppName({ commit }, name: string) {
|
||||
@@ -439,6 +483,84 @@ export default createStore<RootState>({
|
||||
return { code: '0', message: 'ok' }
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 初始化浏览器插件心跳监听
|
||||
* 双重检测机制:
|
||||
* 1. BroadcastChannel watch — 实时收心跳(插件与本页面同时在线时最快)
|
||||
* 2. 定时轮询 bridge.get 读 IndexedDB — 每3秒兜底检测(覆盖 BroadcastChannel 不通的场景)
|
||||
* 启动时立即 get 一次快速判断初始在线状态
|
||||
* 返回销毁函数
|
||||
*/
|
||||
startPlugHeartbeat({ commit, state }) {
|
||||
const bridge = createChannelBridge({ domain: state.bridgeDomain })
|
||||
let lastHeartbeat = 0
|
||||
|
||||
/** 解析 updateTime 字符串为时间戳(格式:yyyy-MM-dd HH:mm:ss) */
|
||||
function parseTime(timeStr: string): number {
|
||||
return new Date(timeStr.replace(' ', 'T')).getTime()
|
||||
}
|
||||
|
||||
/** 根据 updateTime 判断是否在线并更新状态,同时提取 version */
|
||||
function checkOnline(updateTime: string, version?: string) {
|
||||
const elapsed = Date.now() - parseTime(updateTime)
|
||||
if (elapsed <= 3000) {
|
||||
lastHeartbeat = Date.now()
|
||||
if (!state.plugIsOnline) {
|
||||
commit('SET_PLUG_IS_ONLINE', true)
|
||||
}
|
||||
// 更新插件版本号
|
||||
if (version && version !== state.plugUseVersion) {
|
||||
commit('SET_PLUG_USE_VERSION', version)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 启动时立即 get 一次,根据 updateTime 判断初始在线状态
|
||||
bridge.get<{ usage: string; version?: string }>(state.bridgeDomain, 'offerpieBrowserPlugUsage').then((data) => {
|
||||
if (data && data.updateTime) {
|
||||
checkOnline(data.updateTime, data.version)
|
||||
}
|
||||
})
|
||||
|
||||
// 方式1:BroadcastChannel watch — 实时收心跳
|
||||
const unwatch = bridge.watch<{ usage: string; version?: string }>(
|
||||
state.bridgeDomain,
|
||||
'offerpieBrowserPlugUsage',
|
||||
(data) => {
|
||||
if (data && data.updateTime) {
|
||||
checkOnline(data.updateTime, data.version)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// 方式2:定时轮询 get IndexedDB — 兜底(覆盖 BroadcastChannel 断连场景)
|
||||
const pollTimer = setInterval(async () => {
|
||||
try {
|
||||
const data = await bridge.get<{ usage: string; version?: string }>(state.bridgeDomain, 'offerpieBrowserPlugUsage')
|
||||
if (data && data.updateTime) {
|
||||
checkOnline(data.updateTime, data.version)
|
||||
}
|
||||
} catch { /* 忽略读取异常 */ }
|
||||
}, 3000)
|
||||
|
||||
// 定时检查超时(3秒无心跳判定离线)
|
||||
const checkTimer = setInterval(() => {
|
||||
if (lastHeartbeat > 0 && Date.now() - lastHeartbeat > 3000) {
|
||||
if (state.plugIsOnline) {
|
||||
commit('SET_PLUG_IS_ONLINE', false)
|
||||
}
|
||||
}
|
||||
}, 1000)
|
||||
|
||||
// 返回销毁函数
|
||||
return () => {
|
||||
unwatch()
|
||||
clearInterval(pollTimer)
|
||||
clearInterval(checkTimer)
|
||||
bridge.destroy()
|
||||
}
|
||||
},
|
||||
},
|
||||
modules: {},
|
||||
})
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import axios from 'axios'
|
||||
|
||||
/** 默认 Token(Nova 对话接口使用) */
|
||||
export const DEFAULT_TOKEN = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOjIwMzUyNTM4OTg5MTk4NzA0NjUsInV1SWQiOiI2MmQ5MDE2NTcyNzY0ZmNjODNjZTIyYjRjODA5ZmU5MiJ9.eE-Q5rio5J5kxkS-XPYdmk-1Tgvg6kj6NGoKWMFNU14'
|
||||
|
||||
/**
|
||||
* AI 端专用 axios 实例
|
||||
* 基础地址指向 AI 服务,超时时间较长(大模型响应慢)
|
||||
@@ -17,7 +14,7 @@ const aiService = axios.create({
|
||||
* 浏览器安全策略不允许 JS 直接设置 Cookie header,改用 Token header 传递
|
||||
*/
|
||||
aiService.interceptors.request.use((config) => {
|
||||
// config.headers['Token'] = DEFAULT_TOKEN
|
||||
|
||||
return config
|
||||
})
|
||||
|
||||
@@ -110,7 +107,7 @@ export interface NovaChatResponse {
|
||||
export function sendNovaChat(params: NovaChatParams) {
|
||||
return aiService.post<AiResult<NovaChatResponse>>('/nova-chat/chat', params, {
|
||||
headers: {
|
||||
Token: DEFAULT_TOKEN,
|
||||
|
||||
},
|
||||
}).then(res => res.data)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,734 @@
|
||||
/**
|
||||
* OfferPie 跨标签页通信桥接组件
|
||||
* 用于浏览器插件与网站页面之间的纯本地通信(不依赖任何后端接口)
|
||||
*
|
||||
* 【使用说明】
|
||||
* 1. 引入并创建实例:
|
||||
* import { createChannelBridge } from './channelBridge'
|
||||
* const bridge = createChannelBridge({ domain: 'offerpai.com', isPlugin: true })
|
||||
*
|
||||
* 2. 发送数据(单次,同时写缓存 + 广播):
|
||||
* bridge.put<MyDataType>('offerpai.com', 'resumeStatus', { ... })
|
||||
*
|
||||
* 3. 高频发送(节流写缓存,每次都广播):
|
||||
* bridge.emit<MyDataType>('offerpai.com', 'cursorPosition', { ... })
|
||||
*
|
||||
* 4. 主动获取一次数据(先广播问对方,60ms超时转IndexedDB):
|
||||
* const result = await bridge.get<MyDataType>('offerpai.com', 'resumeStatus')
|
||||
*
|
||||
* 5. 持续监听数据变化(初始读一次缓存,后续实时接收 put/emit):
|
||||
* const unwatch = bridge.watch<MyDataType>('offerpai.com', 'resumeStatus', (data) => { ... })
|
||||
* // 取消监听
|
||||
* unwatch()
|
||||
*
|
||||
* 【跨域模式 useCsl】
|
||||
* 在 put/emit/get/watch 最后一个参数传 true,数据存储走 chrome.storage.local(通过 Background 中转)
|
||||
* 适用于网页与插件在不同域名标签页之间的通信场景
|
||||
* 示例:bridge.put<MyData>('offerpai.com', 'someData', { ... }, true)
|
||||
* 注意:useCsl 模式依赖插件 Background Service Worker 在线
|
||||
*
|
||||
* 6. 销毁实例(页面卸载时调用):
|
||||
* bridge.destroy()
|
||||
*
|
||||
* 【参数说明】
|
||||
* - domain: 频道域名标识,不同网站项目用不同域名隔离
|
||||
* - isPlugin: 是否为插件环境,默认 false(非插件时会启用 Web Lock 保活)
|
||||
*
|
||||
* 【数据类型要求】
|
||||
* - 传递的数据必须有 TypeScript 类型定义(泛型约束 extends object)
|
||||
* - 组件自动为每条数据添加 createTime 和 updateTime 字段
|
||||
*
|
||||
* ============================================================
|
||||
* 【通信数据名称注册表】
|
||||
* 在此区域记录所有通信数据的名称、类型和用途说明
|
||||
* 新增数据时在此补充,删除时移除对应行
|
||||
* ------------------------------------------------------------
|
||||
* | 数据名(name) | 中文名称 | 说明 |
|
||||
* | ----------------------------- | -------------- | --------------------------------- |
|
||||
* | offerpieBrowserPlugUsage | 插件使用状态 | 标识插件正在运行 { usage: string, version: string } |
|
||||
* | offerpaiDeliveryLinkList | 投递链接列表 | 记录要投递职位的来源链接,目前用于解决投递网站的重定向 { linkList: string[] } |
|
||||
* ============================================================
|
||||
*/
|
||||
|
||||
// ============ 类型定义 ============
|
||||
|
||||
/** 组件自动附加的时间戳字段 */
|
||||
export interface BridgeTimestamp {
|
||||
/** 数据首次创建时间(格式:yyyy-MM-dd HH:mm:ss) */
|
||||
createTime: string
|
||||
/** 数据最近更新时间(格式:yyyy-MM-dd HH:mm:ss) */
|
||||
updateTime: string
|
||||
}
|
||||
|
||||
/** 带时间戳的完整数据包装 */
|
||||
export type BridgeData<T extends object> = T & BridgeTimestamp
|
||||
|
||||
/** IndexedDB 存储记录结构 */
|
||||
interface DBRecord {
|
||||
/** 数据名称(唯一索引) */
|
||||
name: string
|
||||
/** JSON 数据本体 */
|
||||
data: object
|
||||
/** 创建时间 */
|
||||
createTime: string
|
||||
/** 更新时间 */
|
||||
updateTime: string
|
||||
}
|
||||
|
||||
/** BroadcastChannel 消息结构 */
|
||||
interface ChannelMessage {
|
||||
/** 消息类型 */
|
||||
type: 'put' | 'emit' | 'get-request' | 'get-response' | 'csl-put-request' | 'csl-put-response' | 'csl-get-request' | 'csl-get-response'
|
||||
/** 数据名称 */
|
||||
name: string
|
||||
/** 请求唯一ID(用于 get 请求-响应匹配) */
|
||||
id?: string
|
||||
/** 数据本体(put/emit/get-response 携带) */
|
||||
data?: object
|
||||
/** 创建时间 */
|
||||
createTime?: string
|
||||
/** 更新时间 */
|
||||
updateTime?: string
|
||||
/** 目标域名(csl 中转消息使用) */
|
||||
domain?: string
|
||||
}
|
||||
|
||||
/** watch 回调函数类型 */
|
||||
type WatchCallback<T extends object> = (data: BridgeData<T> | null) => void
|
||||
|
||||
/** 创建实例的配置参数 */
|
||||
export interface ChannelBridgeOptions {
|
||||
/** 默认域名(可在方法调用时覆盖) */
|
||||
domain: string
|
||||
/** 是否为插件环境,默认 false。非插件时启用 Web Lock 保活 */
|
||||
isPlugin?: boolean
|
||||
}
|
||||
|
||||
// ============ 常量 ============
|
||||
|
||||
/** IndexedDB 数据库名称(含固定雪花ID保证唯一性) */
|
||||
const DB_NAME = 'offerpie_comm_7394028156183472'
|
||||
/** get 方法 BroadcastChannel 超时时间(毫秒) */
|
||||
const GET_TIMEOUT_MS = 60
|
||||
/** emit 方法节流写库间隔(毫秒) */
|
||||
const EMIT_THROTTLE_MS = 500
|
||||
/** chrome.storage.local 存储总 key */
|
||||
const CSL_STORAGE_KEY = 'offerpie_comm_7394028156183472'
|
||||
/** Background 消息类型前缀(用于 useCsl 模式) */
|
||||
const CSL_MSG_PREFIX = 'OFFERPIE_CSL'
|
||||
|
||||
// ============ 工具函数 ============
|
||||
|
||||
/** 获取当前时间的 localDateTime 格式字符串:yyyy-MM-dd HH:mm:ss */
|
||||
function nowLocalDateTime(): string {
|
||||
const d = new Date()
|
||||
const pad = (n: number) => String(n).padStart(2, '0')
|
||||
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`
|
||||
}
|
||||
|
||||
/** 生成简单唯一ID(用于 get 请求匹配) */
|
||||
function uid(): string {
|
||||
return Date.now().toString(36) + Math.random().toString(36).slice(2, 8)
|
||||
}
|
||||
|
||||
// ============ chrome.storage.local 操作(useCsl 模式) ============
|
||||
|
||||
/** 检测 chrome.runtime.sendMessage 是否可用(插件环境) */
|
||||
function isChromeRuntimeAvailable(): boolean {
|
||||
return typeof chrome !== 'undefined' && !!chrome.runtime && !!chrome.runtime.sendMessage
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过 Background 写入 chrome.storage.local
|
||||
* 插件环境直接发 chrome.runtime.sendMessage
|
||||
* 网页环境自动走 BroadcastChannel 中转给同域 Content Script 代理
|
||||
*/
|
||||
function cslPut(domain: string, name: string, data: object, createTime: string, updateTime: string): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (isChromeRuntimeAvailable()) {
|
||||
// 插件环境:直接发给 Background
|
||||
chrome.runtime.sendMessage(
|
||||
{ type: `${CSL_MSG_PREFIX}_PUT`, domain, name, data, createTime, updateTime },
|
||||
(response) => {
|
||||
if (chrome.runtime.lastError) {
|
||||
reject(new Error(chrome.runtime.lastError.message))
|
||||
} else {
|
||||
resolve()
|
||||
}
|
||||
}
|
||||
)
|
||||
} else {
|
||||
// 网页环境:通过 BroadcastChannel 请求同域 Content Script 代理
|
||||
const requestId = uid()
|
||||
const ch = _getCslProxyChannel(domain)
|
||||
const handler = (event: MessageEvent<ChannelMessage>) => {
|
||||
if (event.data.type === 'csl-put-response' && event.data.id === requestId) {
|
||||
resolve()
|
||||
}
|
||||
}
|
||||
ch.addEventListener('message', handler)
|
||||
ch.postMessage({ type: 'csl-put-request', id: requestId, name, data, createTime, updateTime, domain } as ChannelMessage)
|
||||
// 超时兜底
|
||||
setTimeout(() => {
|
||||
ch.removeEventListener('message', handler)
|
||||
resolve() // 即使超时也不阻塞
|
||||
}, 3000)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过 Background 从 chrome.storage.local 读取数据
|
||||
* 插件环境直接发 chrome.runtime.sendMessage
|
||||
* 网页环境自动走 BroadcastChannel 中转给同域 Content Script 代理
|
||||
*/
|
||||
function cslGet(domain: string, name: string): Promise<DBRecord | null> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (isChromeRuntimeAvailable()) {
|
||||
// 插件环境:直接发给 Background
|
||||
chrome.runtime.sendMessage(
|
||||
{ type: `${CSL_MSG_PREFIX}_GET`, domain, name },
|
||||
(response) => {
|
||||
if (chrome.runtime.lastError) {
|
||||
reject(new Error(chrome.runtime.lastError.message))
|
||||
} else {
|
||||
resolve(response?.record || null)
|
||||
}
|
||||
}
|
||||
)
|
||||
} else {
|
||||
// 网页环境:通过 BroadcastChannel 请求同域 Content Script 代理
|
||||
const requestId = uid()
|
||||
const ch = _getCslProxyChannel(domain)
|
||||
const handler = (event: MessageEvent<ChannelMessage>) => {
|
||||
if (event.data.type === 'csl-get-response' && event.data.id === requestId) {
|
||||
ch.removeEventListener('message', handler)
|
||||
const record = event.data.data && event.data.createTime && event.data.updateTime
|
||||
? { name: event.data.name, data: event.data.data, createTime: event.data.createTime, updateTime: event.data.updateTime }
|
||||
: null
|
||||
resolve(record)
|
||||
}
|
||||
}
|
||||
ch.addEventListener('message', handler)
|
||||
ch.postMessage({ type: 'csl-get-request', id: requestId, name, domain } as ChannelMessage)
|
||||
// 超时兜底
|
||||
setTimeout(() => {
|
||||
ch.removeEventListener('message', handler)
|
||||
resolve(null)
|
||||
}, 3000)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/** CSL 代理频道缓存 */
|
||||
const _cslProxyChannels = new Map<string, BroadcastChannel>()
|
||||
|
||||
/** 获取 CSL 代理用的 BroadcastChannel(与主频道分开,避免消息混淆) */
|
||||
function _getCslProxyChannel(domain: string): BroadcastChannel {
|
||||
if (_cslProxyChannels.has(domain)) return _cslProxyChannels.get(domain)!
|
||||
const ch = new BroadcastChannel(`offerpie-bridge-csl-${domain}`)
|
||||
_cslProxyChannels.set(domain, ch)
|
||||
return ch
|
||||
}
|
||||
|
||||
// ============ IndexedDB 操作 ============
|
||||
|
||||
/** 数据库连接缓存(避免重复打开和版本冲突) */
|
||||
const _dbCache = new Map<string, Promise<IDBDatabase>>()
|
||||
|
||||
/** 打开/创建 IndexedDB 数据库,按 domain 动态创建 ObjectStore,带连接缓存防并发冲突 */
|
||||
function openDB(storeName: string): Promise<IDBDatabase> {
|
||||
// 如果已有进行中或已完成的连接,直接复用
|
||||
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 = () => {
|
||||
const db = request.result
|
||||
if (!db.objectStoreNames.contains(storeName)) {
|
||||
const store = db.createObjectStore(storeName, { keyPath: 'name' })
|
||||
store.createIndex('name', 'name', { unique: true })
|
||||
}
|
||||
}
|
||||
request.onsuccess = () => {
|
||||
const db = request.result
|
||||
if (!db.objectStoreNames.contains(storeName)) {
|
||||
// store 不存在,需要升版本。关闭当前连接再重开
|
||||
db.close()
|
||||
_dbCache.delete(storeName)
|
||||
const version = db.version + 1
|
||||
const req2 = indexedDB.open(DB_NAME, version)
|
||||
req2.onupgradeneeded = () => {
|
||||
const db2 = req2.result
|
||||
if (!db2.objectStoreNames.contains(storeName)) {
|
||||
const store = db2.createObjectStore(storeName, { keyPath: 'name' })
|
||||
store.createIndex('name', 'name', { unique: true })
|
||||
}
|
||||
}
|
||||
req2.onsuccess = () => resolve(req2.result)
|
||||
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 {
|
||||
resolve(db)
|
||||
}
|
||||
}
|
||||
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 */
|
||||
async function dbPut(storeName: string, record: DBRecord): Promise<void> {
|
||||
try {
|
||||
const db = await openDB(storeName)
|
||||
return new Promise((resolve, reject) => {
|
||||
const tx = db.transaction(storeName, 'readwrite')
|
||||
const store = tx.objectStore(storeName)
|
||||
store.put(record)
|
||||
tx.oncomplete = () => resolve()
|
||||
tx.onerror = () => reject(tx.error)
|
||||
})
|
||||
} catch (err) {
|
||||
console.warn('[ChannelBridge] dbPut failed:', err)
|
||||
}
|
||||
}
|
||||
|
||||
/** 从 IndexedDB 读取一条记录 */
|
||||
async function dbGet(storeName: string, name: string): Promise<DBRecord | null> {
|
||||
try {
|
||||
const db = await openDB(storeName)
|
||||
return new Promise((resolve, reject) => {
|
||||
const tx = db.transaction(storeName, 'readonly')
|
||||
const store = tx.objectStore(storeName)
|
||||
const req = store.get(name)
|
||||
req.onsuccess = () => resolve(req.result || null)
|
||||
req.onerror = () => reject(req.error)
|
||||
})
|
||||
} catch (err) {
|
||||
console.warn('[ChannelBridge] dbGet failed:', err)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// ============ 核心:创建通信桥接实例 ============
|
||||
|
||||
export function createChannelBridge(options: ChannelBridgeOptions) {
|
||||
const { domain, isPlugin = false } = options
|
||||
|
||||
/** BroadcastChannel 实例缓存(按 domain 隔离频道) */
|
||||
const channels = new Map<string, BroadcastChannel>()
|
||||
|
||||
/** watch 订阅回调注册表:key = `${domain}::${name}` */
|
||||
const watchers = new Map<string, Set<WatchCallback<any>>>()
|
||||
|
||||
/** emit 节流定时器:key = `${domain}::${name}` */
|
||||
const emitTimers = new Map<string, ReturnType<typeof setTimeout>>()
|
||||
|
||||
/** emit 节流暂存最新数据:key = `${domain}::${name}` */
|
||||
const emitPending = new Map<string, DBRecord>()
|
||||
|
||||
/** get 请求等待队列:key = requestId */
|
||||
const getResolvers = new Map<string, (data: BridgeData<any> | null) => void>()
|
||||
|
||||
// --- Web Lock 保活(非插件环境) ---
|
||||
if (!isPlugin && typeof navigator !== 'undefined' && navigator.locks) {
|
||||
navigator.locks.request('offerpie-keep-alive', () => new Promise(() => {}))
|
||||
}
|
||||
|
||||
/** 获取或创建指定 domain 的 BroadcastChannel */
|
||||
function getChannel(channelDomain: string): BroadcastChannel {
|
||||
if (channels.has(channelDomain)) return channels.get(channelDomain)!
|
||||
const ch = new BroadcastChannel(`offerpie-bridge-${channelDomain}`)
|
||||
ch.onmessage = (event: MessageEvent<ChannelMessage>) => handleMessage(channelDomain, event.data)
|
||||
channels.set(channelDomain, ch)
|
||||
return ch
|
||||
}
|
||||
|
||||
/** 处理收到的 BroadcastChannel 消息 */
|
||||
async function handleMessage(channelDomain: string, msg: ChannelMessage) {
|
||||
const watchKey = `${channelDomain}::${msg.name}`
|
||||
|
||||
if (msg.type === 'put' || msg.type === 'emit') {
|
||||
// 收到对方的数据推送,触发本地 watch 回调
|
||||
const wrapped = msg.data && msg.createTime && msg.updateTime
|
||||
? { ...msg.data, createTime: msg.createTime, updateTime: msg.updateTime } as BridgeData<any>
|
||||
: null
|
||||
const callbacks = watchers.get(watchKey)
|
||||
if (callbacks) {
|
||||
callbacks.forEach(cb => cb(wrapped))
|
||||
}
|
||||
}
|
||||
|
||||
if (msg.type === 'get-request' && msg.id) {
|
||||
// 收到对方的 get 请求,从本地 IndexedDB 读取数据并回复
|
||||
const record = await dbGet(channelDomain, msg.name)
|
||||
const ch = getChannel(channelDomain)
|
||||
const response: ChannelMessage = {
|
||||
type: 'get-response',
|
||||
name: msg.name,
|
||||
id: msg.id,
|
||||
data: record?.data,
|
||||
createTime: record?.createTime,
|
||||
updateTime: record?.updateTime,
|
||||
}
|
||||
ch.postMessage(response)
|
||||
}
|
||||
|
||||
if (msg.type === 'get-response' && msg.id) {
|
||||
// 收到对方对 get 请求的回复
|
||||
const resolver = getResolvers.get(msg.id)
|
||||
if (resolver) {
|
||||
getResolvers.delete(msg.id)
|
||||
if (msg.data && msg.createTime && msg.updateTime) {
|
||||
resolver({ ...msg.data, createTime: msg.createTime, updateTime: msg.updateTime } as BridgeData<any>)
|
||||
} else {
|
||||
resolver(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化默认频道监听
|
||||
getChannel(domain)
|
||||
|
||||
// --- CSL 代理监听(插件环境下,监听网页端的 CSL 中转请求) ---
|
||||
if (isPlugin && isChromeRuntimeAvailable()) {
|
||||
const cslProxyCh = new BroadcastChannel(`offerpie-bridge-csl-${domain}`)
|
||||
cslProxyCh.onmessage = async (event: MessageEvent<ChannelMessage>) => {
|
||||
const msg = event.data
|
||||
|
||||
if (msg.type === 'csl-put-request' && msg.id) {
|
||||
// 代理写入 chrome.storage.local
|
||||
const targetDomain = msg.domain || domain
|
||||
await new Promise<void>((resolve) => {
|
||||
chrome.runtime.sendMessage(
|
||||
{ type: `${CSL_MSG_PREFIX}_PUT`, domain: targetDomain, name: msg.name, data: msg.data, createTime: msg.createTime, updateTime: msg.updateTime },
|
||||
() => resolve()
|
||||
)
|
||||
})
|
||||
cslProxyCh.postMessage({ type: 'csl-put-response', id: msg.id, name: msg.name } as ChannelMessage)
|
||||
}
|
||||
|
||||
if (msg.type === 'csl-get-request' && msg.id) {
|
||||
// 代理读取 chrome.storage.local
|
||||
const targetDomain = msg.domain || domain
|
||||
const response = await new Promise<any>((resolve) => {
|
||||
chrome.runtime.sendMessage(
|
||||
{ type: `${CSL_MSG_PREFIX}_GET`, domain: targetDomain, name: msg.name },
|
||||
(res) => resolve(res)
|
||||
)
|
||||
})
|
||||
const record = response?.record
|
||||
cslProxyCh.postMessage({
|
||||
type: 'csl-get-response',
|
||||
id: msg.id,
|
||||
name: msg.name,
|
||||
data: record?.data,
|
||||
createTime: record?.createTime,
|
||||
updateTime: record?.updateTime,
|
||||
} as ChannelMessage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- 公开方法 ---
|
||||
|
||||
/**
|
||||
* put - 单次发送数据(写入 IndexedDB + BroadcastChannel 广播)
|
||||
* @param targetDomain 目标域名频道
|
||||
* @param name 数据名称
|
||||
* @param data 数据本体(需有 TS 类型定义)
|
||||
* @param useCsl 是否使用 chrome.storage.local 跨域模式(通过 Background 中转)
|
||||
*/
|
||||
async function put<T extends object>(targetDomain: string, name: string, data: T, useCsl?: boolean): Promise<void> {
|
||||
const now = nowLocalDateTime()
|
||||
|
||||
if (useCsl) {
|
||||
// 跨域模式:通过 Background 读写 chrome.storage.local
|
||||
const existing = await cslGet(targetDomain, name).catch(() => null)
|
||||
const createTime = existing?.createTime || now
|
||||
const updateTime = now
|
||||
await cslPut(targetDomain, name, data, createTime, updateTime)
|
||||
// 同时广播(同域标签页也能收到)
|
||||
const ch = getChannel(targetDomain)
|
||||
const msg: ChannelMessage = { type: 'put', name, data, createTime, updateTime }
|
||||
ch.postMessage(msg)
|
||||
return
|
||||
}
|
||||
|
||||
// 默认模式:IndexedDB
|
||||
const existing = await dbGet(targetDomain, name)
|
||||
const createTime = existing?.createTime || now
|
||||
const updateTime = now
|
||||
|
||||
const record: DBRecord = { name, data, createTime, updateTime }
|
||||
// 写入 IndexedDB
|
||||
await dbPut(targetDomain, record)
|
||||
// BroadcastChannel 广播
|
||||
const ch = getChannel(targetDomain)
|
||||
const msg: ChannelMessage = { type: 'put', name, data, createTime, updateTime }
|
||||
ch.postMessage(msg)
|
||||
}
|
||||
|
||||
/**
|
||||
* emit - 高频发送数据(每次都广播,节流 500ms 写一次 IndexedDB)
|
||||
* @param targetDomain 目标域名频道
|
||||
* @param name 数据名称
|
||||
* @param data 数据本体(需有 TS 类型定义)
|
||||
* @param useCsl 是否使用 chrome.storage.local 跨域模式(通过 Background 中转)
|
||||
*/
|
||||
async function emit<T extends object>(targetDomain: string, name: string, data: T, useCsl?: boolean): Promise<void> {
|
||||
const now = nowLocalDateTime()
|
||||
const key = `${targetDomain}::${name}`
|
||||
|
||||
if (useCsl) {
|
||||
// 跨域模式:每次广播,节流写 chrome.storage.local
|
||||
let createTime: string
|
||||
const pending = emitPending.get(key)
|
||||
if (pending) {
|
||||
createTime = pending.createTime
|
||||
} else {
|
||||
const existing = await cslGet(targetDomain, name).catch(() => null)
|
||||
createTime = existing?.createTime || now
|
||||
}
|
||||
const updateTime = now
|
||||
const record: DBRecord = { name, data, createTime, updateTime }
|
||||
emitPending.set(key, record)
|
||||
|
||||
// BroadcastChannel 每次都广播
|
||||
const ch = getChannel(targetDomain)
|
||||
const msg: ChannelMessage = { type: 'emit', name, data, createTime, updateTime }
|
||||
ch.postMessage(msg)
|
||||
|
||||
// 节流写 chrome.storage.local
|
||||
if (!emitTimers.has(key)) {
|
||||
emitTimers.set(key, setTimeout(async () => {
|
||||
emitTimers.delete(key)
|
||||
const latestRecord = emitPending.get(key)
|
||||
if (latestRecord) {
|
||||
emitPending.delete(key)
|
||||
await cslPut(targetDomain, latestRecord.name, latestRecord.data, latestRecord.createTime, latestRecord.updateTime).catch(() => {})
|
||||
}
|
||||
}, EMIT_THROTTLE_MS))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 默认模式:IndexedDB
|
||||
// 先尝试获取 createTime(从暂存或内存缓存)
|
||||
let createTime: string
|
||||
const pending = emitPending.get(key)
|
||||
if (pending) {
|
||||
createTime = pending.createTime
|
||||
} else {
|
||||
// 只在首次查库获取 createTime,失败就用当前时间(不阻塞后续 emit)
|
||||
let existing: DBRecord | null = null
|
||||
try {
|
||||
existing = await dbGet(targetDomain, name)
|
||||
} catch {}
|
||||
createTime = existing?.createTime || now
|
||||
}
|
||||
const updateTime = now
|
||||
const record: DBRecord = { name, data, createTime, updateTime }
|
||||
|
||||
// 暂存最新数据(不再 delete,保证后续 emit 总能命中缓存不走 dbGet)
|
||||
emitPending.set(key, record)
|
||||
|
||||
// BroadcastChannel 每次都广播(保证 watch 实时性)
|
||||
const ch = getChannel(targetDomain)
|
||||
const msg: ChannelMessage = { type: 'emit', name, data, createTime, updateTime }
|
||||
ch.postMessage(msg)
|
||||
|
||||
// 节流写库
|
||||
if (!emitTimers.has(key)) {
|
||||
emitTimers.set(key, setTimeout(async () => {
|
||||
emitTimers.delete(key)
|
||||
const latestRecord = emitPending.get(key)
|
||||
if (latestRecord) {
|
||||
// 注意:不再 delete emitPending,让下次 emit 继续命中缓存
|
||||
await dbPut(targetDomain, latestRecord)
|
||||
}
|
||||
}, EMIT_THROTTLE_MS))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* get - 主动获取一次数据(先 BroadcastChannel 请求,60ms 超时转 IndexedDB)
|
||||
* @param targetDomain 目标域名频道
|
||||
* @param name 数据名称
|
||||
* @param useCsl 是否使用 chrome.storage.local 跨域模式(通过 Background 中转)
|
||||
* @returns 数据本体(含 createTime/updateTime),无数据返回 null
|
||||
*/
|
||||
async function get<T extends object>(targetDomain: string, name: string, useCsl?: boolean): Promise<BridgeData<T> | null> {
|
||||
if (useCsl) {
|
||||
// 跨域模式:先广播请求,超时读 chrome.storage.local
|
||||
const ch = getChannel(targetDomain)
|
||||
const requestId = uid()
|
||||
const msg: ChannelMessage = { type: 'get-request', name, id: requestId }
|
||||
ch.postMessage(msg)
|
||||
|
||||
const result = await new Promise<BridgeData<T> | null>((resolve) => {
|
||||
getResolvers.set(requestId, resolve as any)
|
||||
setTimeout(() => {
|
||||
if (getResolvers.has(requestId)) {
|
||||
getResolvers.delete(requestId)
|
||||
resolve(null)
|
||||
}
|
||||
}, GET_TIMEOUT_MS)
|
||||
})
|
||||
|
||||
if (result) return result
|
||||
|
||||
// 超时降级读 chrome.storage.local
|
||||
const record = await cslGet(targetDomain, name).catch(() => null)
|
||||
if (record) {
|
||||
return { ...record.data, createTime: record.createTime, updateTime: record.updateTime } as BridgeData<T>
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// 默认模式:IndexedDB
|
||||
const ch = getChannel(targetDomain)
|
||||
const requestId = uid()
|
||||
|
||||
// 发起广播请求
|
||||
const msg: ChannelMessage = { type: 'get-request', name, id: requestId }
|
||||
ch.postMessage(msg)
|
||||
|
||||
// 等待响应或超时
|
||||
const result = await new Promise<BridgeData<T> | null>((resolve) => {
|
||||
getResolvers.set(requestId, resolve as any)
|
||||
setTimeout(() => {
|
||||
if (getResolvers.has(requestId)) {
|
||||
getResolvers.delete(requestId)
|
||||
resolve(null) // 超时,标记为未收到响应
|
||||
}
|
||||
}, GET_TIMEOUT_MS)
|
||||
})
|
||||
|
||||
// 如果广播拿到了数据直接返回
|
||||
if (result) return result
|
||||
|
||||
// 超时降级读 IndexedDB
|
||||
const record = await dbGet(targetDomain, name)
|
||||
if (record) {
|
||||
return { ...record.data, createTime: record.createTime, updateTime: record.updateTime } as BridgeData<T>
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* watch - 持续监听数据变化(初始读一次缓存,后续实时接收 put/emit 推送)
|
||||
* @param targetDomain 目标域名频道
|
||||
* @param name 数据名称
|
||||
* @param callback 数据变化回调,参数为最新数据或 null
|
||||
* @param useCsl 是否使用 chrome.storage.local 跨域模式(通过 Background 中转)
|
||||
* @returns 取消监听的函数
|
||||
*/
|
||||
function watch<T extends object>(targetDomain: string, name: string, callback: WatchCallback<T>, useCsl?: boolean): () => void {
|
||||
const key = `${targetDomain}::${name}`
|
||||
// 确保频道已初始化
|
||||
getChannel(targetDomain)
|
||||
|
||||
// 注册回调(BroadcastChannel 广播也能触发,不管 useCsl 与否)
|
||||
if (!watchers.has(key)) watchers.set(key, new Set())
|
||||
watchers.get(key)!.add(callback)
|
||||
|
||||
if (useCsl) {
|
||||
// 跨域模式:初始读 chrome.storage.local
|
||||
cslGet(targetDomain, name).then((record) => {
|
||||
if (record) {
|
||||
callback({ ...record.data, createTime: record.createTime, updateTime: record.updateTime } as BridgeData<T>)
|
||||
} else {
|
||||
callback(null)
|
||||
}
|
||||
}).catch(() => callback(null))
|
||||
|
||||
// 监听 chrome.storage.onChanged 事件
|
||||
const storageListener = (changes: { [key: string]: chrome.storage.StorageChange }, areaName: string) => {
|
||||
if (areaName !== 'local' || !changes[CSL_STORAGE_KEY]) return
|
||||
const newValue = changes[CSL_STORAGE_KEY].newValue as Array<{ domain: string; value: DBRecord }> | undefined
|
||||
if (!newValue) return
|
||||
const item = newValue.find(i => i.domain === targetDomain && i.value?.name === name)
|
||||
if (item && item.value) {
|
||||
callback({ ...item.value.data, createTime: item.value.createTime, updateTime: item.value.updateTime } as BridgeData<T>)
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof chrome !== 'undefined' && chrome.storage && chrome.storage.onChanged) {
|
||||
chrome.storage.onChanged.addListener(storageListener)
|
||||
}
|
||||
|
||||
// 返回取消监听函数
|
||||
return () => {
|
||||
const set = watchers.get(key)
|
||||
if (set) {
|
||||
set.delete(callback)
|
||||
if (set.size === 0) watchers.delete(key)
|
||||
}
|
||||
if (typeof chrome !== 'undefined' && chrome.storage && chrome.storage.onChanged) {
|
||||
chrome.storage.onChanged.removeListener(storageListener)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 默认模式:初始读取 IndexedDB
|
||||
dbGet(targetDomain, name).then((record) => {
|
||||
if (record) {
|
||||
callback({ ...record.data, createTime: record.createTime, updateTime: record.updateTime } as BridgeData<T>)
|
||||
} else {
|
||||
callback(null)
|
||||
}
|
||||
})
|
||||
|
||||
// 返回取消监听函数
|
||||
return () => {
|
||||
const set = watchers.get(key)
|
||||
if (set) {
|
||||
set.delete(callback)
|
||||
if (set.size === 0) watchers.delete(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* destroy - 销毁实例,关闭所有频道,清除定时器
|
||||
*/
|
||||
function destroy(): void {
|
||||
channels.forEach(ch => ch.close())
|
||||
channels.clear()
|
||||
watchers.clear()
|
||||
emitTimers.forEach(timer => clearTimeout(timer))
|
||||
emitTimers.clear()
|
||||
emitPending.clear()
|
||||
getResolvers.clear()
|
||||
}
|
||||
|
||||
return { put, emit, get, watch, destroy }
|
||||
}
|
||||
@@ -102,7 +102,7 @@ export function exportResumeWord(element: HTMLElement, fileName: string) {
|
||||
const blob = new Blob([fullHtml], { type: 'application/msword' })
|
||||
const link = document.createElement('a')
|
||||
link.href = URL.createObjectURL(blob)
|
||||
link.download = `${fileName}.doc`
|
||||
link.download = `${fileName}.docx`
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
|
||||
@@ -14,12 +14,13 @@
|
||||
@launch="handleLaunch"
|
||||
/>
|
||||
<!-- <AgentSetupWizard-->
|
||||
<!-- v-if="true"-->
|
||||
<!-- :initial-config="agentConfig"-->
|
||||
<!-- @complete="handleSetupComplete"-->
|
||||
<!-- @launch="handleLaunch"-->
|
||||
<!-- v-if="true"-->
|
||||
<!-- :initial-config="agentConfig"-->
|
||||
<!-- @complete="handleSetupComplete"-->
|
||||
<!-- @launch="handleLaunch"-->
|
||||
<!-- />-->
|
||||
|
||||
|
||||
<!-- 求职助手正式内容区域 -->
|
||||
<div v-else-if="!pageLoading" class="agent-main">
|
||||
<!-- 左侧主区域 -->
|
||||
@@ -242,6 +243,8 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, nextTick } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useStore } from 'vuex'
|
||||
import SideNav from '@/components/SideNav.vue'
|
||||
import AgentSetupWizard from '@/components/AgentSetupWizard.vue'
|
||||
import AgentChatJobList from '@/components/AgentChatJobList.vue'
|
||||
@@ -256,8 +259,20 @@ import type { AgentConfig, AgentRecommendJob } from '@/api/agent'
|
||||
import { fetchAgentTaskList, fetchJobList, fetchJobIntention, fetchCustomizeResume } from '@/api/jobs'
|
||||
import type { JobListItem, JobListParams } from '@/api/jobs'
|
||||
import { fetchResumeList } from '@/api/resume'
|
||||
import { fetchMemberStatus } from '@/api/member'
|
||||
import JobGoalDialog from '@/components/JobGoalDialog.vue'
|
||||
|
||||
|
||||
// ==================== 路由 & Store ====================
|
||||
const router = useRouter()
|
||||
const store = useStore()
|
||||
|
||||
// 声明友盟统计 _czc 对象
|
||||
const _czc: any[] = (window as any)._czc || [];
|
||||
(window as any)._czc = _czc;
|
||||
// 绑定 siteid,请用您的 siteid 替换下方 "XXXXXXXX" 部分
|
||||
_czc.push(["_setAccount", "1281490571"]);
|
||||
|
||||
// ==================== sessionStorage 缓存 key ====================
|
||||
const STORAGE_KEY = 'agent_apply_progress_list'
|
||||
const COMPLETED_STORAGE_KEY = 'agent_completed_apply_list'
|
||||
@@ -390,9 +405,31 @@ function loadCompletedFromStorage() {
|
||||
|
||||
/** 页面挂载时查询配置 */
|
||||
onMounted(async () => {
|
||||
// 检查会员权限:确保会员信息已加载,免费用户弹窗并跳回
|
||||
if (!store.state.memberStatus) {
|
||||
try {
|
||||
const res = await fetchMemberStatus()
|
||||
if (res.code === '0' && res.data) {
|
||||
store.commit('SET_MEMBER_STATUS', res.data)
|
||||
}
|
||||
} catch { /* 异常时继续检查 */ }
|
||||
}
|
||||
const status = store.state.memberStatus
|
||||
if (!status || !status.isMember) {
|
||||
// 免费用户 — 弹出会员权限拦截弹窗,跳回上一页
|
||||
store.commit('SET_SHOW_MEMBER_ACCESS_DIALOG', true)
|
||||
router.back()
|
||||
return
|
||||
}
|
||||
|
||||
loadProgressFromStorage()
|
||||
loadCompletedFromStorage()
|
||||
await loadAgentConfig()
|
||||
|
||||
// 检查插件版本是否最新
|
||||
if (store.state.plugIsOnline && !store.state.isLatestPlugin) {
|
||||
ElMessage.warning(`当前安装的浏览器插件不是最新${store.state.plugTargetVersion}版,请去智能投递里下载最新版安装`)
|
||||
}
|
||||
})
|
||||
|
||||
/** 查询求职助手配置 */
|
||||
|
||||
@@ -13,8 +13,8 @@
|
||||
<img src="@/assets/images/logo.png" alt="Offer派" class="home-nav__logo-img" />
|
||||
</div>
|
||||
<div class="dflex-end aliite-c">
|
||||
<!-- 未登录时显示登陆和立即加入按钮 -->
|
||||
<div v-if="!store.state.isAuthenticated" class="fs14 color-1 mr40 fw600 cursor-po" @click="handleLoginClick">登陆</div>
|
||||
<!-- 未登录时显示登录和立即加入按钮 -->
|
||||
<div v-if="!store.state.isAuthenticated" class="fs14 color-1 mr40 fw600 cursor-po" @click="handleLoginClick">登录</div>
|
||||
<button v-if="!store.state.isAuthenticated" class="home-nav__btn" @click="router.push('/jobs')">立即加入</button>
|
||||
<!-- 已登录时显示进入平台按钮 -->
|
||||
<button v-else class="home-nav__btn" @click="router.push('/jobs')">进入平台</button>
|
||||
@@ -23,34 +23,38 @@
|
||||
</header>
|
||||
<div class="home-hero__inner">
|
||||
<div class="home-hero__left">
|
||||
<h1 class="home-hero__title">Offer派<br/>收offer就是快!</h1>
|
||||
<h1 class="home-hero__title">Offer派,<br/><span class="dib"></span>收offer就是快!</h1>
|
||||
<p class="home-hero__desc">智能匹配职位、自动填写申请、量身定制简历、推荐内部人脉——<br/>不到1分钟,统统搞定!</p>
|
||||
<button class="home-hero__cta" @click="router.push('/jobs')">免费体验</button>
|
||||
</div>
|
||||
<!-- 右侧视频展示区 -->
|
||||
<div class="home-hero__right">
|
||||
<div class="home-hero__right-top">
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<!-- <div class="home-hero__right-top">-->
|
||||
<!-- <div></div>-->
|
||||
<!-- <div></div>-->
|
||||
<!-- <div></div>-->
|
||||
<!-- </div>-->
|
||||
<!-- <video-->
|
||||
<!-- class="home-hero__video"-->
|
||||
<!-- src="https://jsxq-image-static.oss-cn-shenzhen.aliyuncs.com/aiJob/find/open-intention-video.mp4"-->
|
||||
<!-- autoplay-->
|
||||
<!-- loop-->
|
||||
<!-- muted-->
|
||||
<!-- playsinline-->
|
||||
<!-- ></video>-->
|
||||
<div>
|
||||
<img class="w706 h452" src="@/assets/images/home/home-top.gif" alt="">
|
||||
</div>
|
||||
<video
|
||||
class="home-hero__video"
|
||||
src="https://jsxq-image-static.oss-cn-shenzhen.aliyuncs.com/aiJob/find/open-intention-video.mp4"
|
||||
autoplay
|
||||
loop
|
||||
muted
|
||||
playsinline
|
||||
></video>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
|
||||
<!-- Stats Section -->
|
||||
<section class="home-stats">
|
||||
<div class="home-stats__inner">
|
||||
<div class="home-stats__header">
|
||||
<h2 class="fs48">让每一次求职努力<br/>都成为看得见的成果</h2>
|
||||
<h2 class="fs38">让每一次求职努力<br/>都成为看得见的成果</h2>
|
||||
<div class="home-stats__divider"></div>
|
||||
</div>
|
||||
<div class="home-stats__cards">
|
||||
@@ -77,16 +81,16 @@
|
||||
<!-- 背景色块 — 底部全宽渐变 -->
|
||||
<div class="home-jobs-showcase__orb home-jobs-showcase__orb--bottom"></div>
|
||||
<div class="home-jobs-showcase__inner">
|
||||
<h2>海量优质校招岗位,尽在Offer派!</h2>
|
||||
<h2 class="fs38">海量优质校招岗位,尽在Offer派!</h2>
|
||||
<p class="home-jobs-showcase__sub">实时汇集海量超10000+名校校招职位</p>
|
||||
<div class="home-jobs-showcase__box">
|
||||
<div class="home-jobs-showcase__box prl25">
|
||||
<div class="home-jobs-showcase__stats">
|
||||
<div class="showcase-stat">
|
||||
<div class="showcase-stat__num"><span><span class="accent">40</span><span class="accent">万</span></span><span class="fs32">+</span></div>
|
||||
<div class="showcase-stat__label">岗位总数</div>
|
||||
</div>
|
||||
<div class="showcase-stat">
|
||||
<div class="showcase-stat__num"><span class="accent">{{ dailyJobCount }}</span><span class="fs32">个</span></div>
|
||||
<div class="showcase-stat__num"><span class="accent">{{ dailyJobCount }}</span><span class="">个</span></div>
|
||||
<div class="showcase-stat__label">今日更新</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -114,7 +118,7 @@
|
||||
<h2>个性化智能<br/>岗位匹配</h2>
|
||||
<p>第一时间发现真正适合你的岗位,精准匹配你的真<br/>实技能,杜绝虚假信息。</p>
|
||||
<button class="home-feature__btn" @click="router.push('/jobs')">
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="8" stroke="currentColor" stroke-width="2"/><circle cx="10" cy="10" r="4" stroke="currentColor" stroke-width="2"/><circle cx="10" cy="10" r="1.5" fill="currentColor"/></svg>
|
||||
<svg width="12" height="12" viewBox="0 0 14 23" fill="none"><circle cx="10" cy="10" r="8" stroke="currentColor" stroke-width="2"/><circle cx="10" cy="10" r="4" stroke="currentColor" stroke-width="2"/><circle cx="10" cy="10" r="1.5" fill="currentColor"/></svg>
|
||||
<span>立即匹配</span>
|
||||
</button>
|
||||
</div>
|
||||
@@ -147,7 +151,7 @@
|
||||
<h2>校招一键<br/>自动网申</h2>
|
||||
<p>每日向数百岗位一键投递,覆盖各大企业网申系<br/>统。告别重复填写,节省 82% 校招求职时间。</p>
|
||||
<button class="home-feature__btn" @click="router.push('/jobs')">
|
||||
<img class="w20 h20" src="@/assets/images/home/blue-flash-icon-01.png" alt="">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="13" height="13" viewBox="0 0 20 21" fill="none"><path d="M3.33331 11.6667C3.17561 11.6673 3.021 11.623 2.88743 11.5392C2.75387 11.4554 2.64683 11.3354 2.57876 11.1931C2.51069 11.0509 2.48438 10.8922 2.50289 10.7356C2.52139 10.579 2.58396 10.4309 2.68331 10.3084L10.9333 1.80839C10.9952 1.73696 11.0795 1.68868 11.1725 1.6715C11.2654 1.65431 11.3614 1.66923 11.4447 1.71381C11.5281 1.75838 11.5938 1.82997 11.6311 1.91681C11.6684 2.00365 11.675 2.10059 11.65 2.19172L10.05 7.20839C10.0028 7.33466 9.98695 7.47049 10.0038 7.60423C10.0206 7.73797 10.0697 7.86562 10.1467 7.97624C10.2237 8.08686 10.3265 8.17715 10.446 8.23936C10.5656 8.30156 10.6985 8.33383 10.8333 8.33339L16.6666 8.33339C16.8243 8.33285 16.9789 8.37707 17.1125 8.46091C17.2461 8.54474 17.3531 8.66476 17.4212 8.80701C17.4893 8.94926 17.5156 9.1079 17.4971 9.26451C17.4785 9.42111 17.416 9.56926 17.3166 9.69172L9.06664 18.1917C9.00476 18.2632 8.92042 18.3114 8.82749 18.3286C8.73455 18.3458 8.63854 18.3309 8.5552 18.2863C8.47186 18.2417 8.40615 18.1701 8.36886 18.0833C8.33157 17.9965 8.32491 17.8995 8.34997 17.8084L9.94997 12.7917C9.99715 12.6655 10.013 12.5296 9.99615 12.3959C9.9793 12.2621 9.93026 12.1345 9.85323 12.0239C9.77621 11.9132 9.6735 11.823 9.55391 11.7608C9.43433 11.6985 9.30144 11.6663 9.16664 11.6667L3.33331 11.6667Z" stroke="currentColor" stroke-width="2" stroke-linejoin="round" stroke-linecap="round" style="mix-blend-mode:normal"></path></svg>
|
||||
<span>开启自动网申</span>
|
||||
</button>
|
||||
</div>
|
||||
@@ -190,7 +194,7 @@
|
||||
<h2>岗位定制简历</h2>
|
||||
<p>10秒内生成针对特定岗位优化的专业简历,通过<br/>ATS系统,突出你的核心优势。</p>
|
||||
<button class="home-feature__btn" @click="router.push('/jobs')">
|
||||
<img class="w20 h20" src="@/assets/images/home/blue-file-icon-01.png" alt="">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="14" height="14" viewBox="0 0 20 22" fill="none"><path stroke="currentColor" stroke-width="2" stroke-linejoin="round" stroke-linecap="round" style="mix-blend-mode:normal" d="M10.5492 18.3334L15 18.3334C15.442 18.3334 15.8659 18.1578 16.1785 17.8453C16.4911 17.5327 16.6667 17.1088 16.6667 16.6668L16.6667 6.66675C16.6673 6.40268 16.6157 6.14109 16.5147 5.89708C16.4137 5.65307 16.2654 5.43148 16.0783 5.24509L13.0883 2.25509C12.9019 2.06802 12.6803 1.91972 12.4363 1.81874C12.1923 1.71776 11.9307 1.66611 11.6667 1.66675L4.99999 1.66675C4.55797 1.66675 4.13404 1.84235 3.82148 2.15491C3.50892 2.46747 3.33333 2.89139 3.33333 3.33342L3.33333 11.1168"></path><path stroke="rgba(82, 202, 209, 1)" stroke-width="2" stroke-linejoin="round" stroke-linecap="round" style="mix-blend-mode:normal" d="M11.6667 1.66675L11.6667 5.83341C11.6667 6.05443 11.7545 6.26639 11.9107 6.42267C12.067 6.57895 12.279 6.66675 12.5 6.66675L16.6667 6.66675"></path><path d="M8.64834 10.5184C8.98019 10.1869 9.43015 10.0007 9.89922 10.001C10.3683 10.0012 10.8181 10.1878 11.1496 10.5196C11.4811 10.8515 11.6672 11.3014 11.667 11.7705C11.6668 12.2396 11.4802 12.6894 11.1483 13.0209L6.96667 17.1975C6.76865 17.3957 6.52389 17.5407 6.255 17.6192L3.86584 18.3167C3.79413 18.3376 3.71812 18.3389 3.64576 18.3203C3.5734 18.3018 3.50736 18.2642 3.45454 18.2113C3.40173 18.1585 3.36408 18.0925 3.34554 18.0201C3.327 17.9478 3.32826 17.8718 3.34917 17.8L4.04584 15.4092C4.12448 15.1406 4.2695 14.8962 4.4675 14.6984L8.64834 10.5184Z" stroke="currentColor" stroke-width="2" stroke-linejoin="round" stroke-linecap="round" style="mix-blend-mode:normal"></path></svg>
|
||||
<span>优化我的简历</span>
|
||||
</button>
|
||||
</div>
|
||||
@@ -229,7 +233,7 @@
|
||||
<h2>名企内推<br/>人脉直通</h2>
|
||||
<p>实时获取名企最新内推信息,自动填写网申内推<br/>码,简历更快到达HR。</p>
|
||||
<button class="home-feature__btn" @click="router.push('/jobs')">
|
||||
<img class="w20 h20" src="@/assets/images/home/blue-horn-icon-01.png" alt="">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="13" height="13" viewBox="0 0 20 20" fill="none"><path d="M9.16667 5C11.7011 5.06529 14.1783 4.23959 16.1667 2.66667C16.2905 2.57381 16.4377 2.51727 16.5918 2.50337C16.746 2.48947 16.9009 2.51877 17.0393 2.58798C17.1778 2.65719 17.2942 2.76358 17.3755 2.89522C17.4569 3.02687 17.5 3.17857 17.5 3.33333L17.5 13.3333C17.5 13.4881 17.4569 13.6398 17.3755 13.7714C17.2942 13.9031 17.1778 14.0095 17.0393 14.0787C16.9009 14.1479 16.746 14.1772 16.5918 14.1633C16.4377 14.1494 16.2905 14.0929 16.1667 14C14.1783 12.4271 11.7011 11.6014 9.16667 11.6667L4.16667 11.6667C3.72464 11.6667 3.30072 11.4911 2.98816 11.1785C2.67559 10.866 2.5 10.442 2.5 10L2.5 6.66667C2.5 6.22464 2.67559 5.80072 2.98816 5.48816C3.30072 5.1756 3.72464 5 4.16667 5L9.16667 5Z" stroke="currentColor" stroke-width="2" stroke-linejoin="round" stroke-linecap="round" style="mix-blend-mode:normal"></path><path stroke="currentColor" stroke-width="2" stroke-linejoin="round" stroke-linecap="round" style="mix-blend-mode:normal" d="M5 11.6665C5 13.8302 5.70178 15.9355 7 17.6665C7.26522 18.0201 7.66005 18.2539 8.09763 18.3164C8.53522 18.3789 8.97971 18.2651 9.33333 17.9998C9.68695 17.7346 9.92074 17.3398 9.98325 16.9022C10.0458 16.4646 9.93188 16.0201 9.66667 15.6665C8.80119 14.5125 8.33333 13.109 8.33333 11.6665"></path><path stroke="currentColor" stroke-width="2" stroke-linejoin="round" stroke-linecap="round" style="mix-blend-mode:normal" d="M6.66669 5L6.66669 11.6667"></path></svg>
|
||||
<span>立即投递</span>
|
||||
</button>
|
||||
</div>
|
||||
@@ -254,7 +258,7 @@
|
||||
<h2>24h全天候<br/>AI求职助手</h2>
|
||||
<p>随时提供求职指导,从岗位筛选到面试技巧,你的专属职业规划顾问。</p>
|
||||
<button class="home-feature__btn" @click="router.push('/jobs')">
|
||||
<img class="w20 h20" src="@/assets/images/home/blue-message-icon-01.png" alt="">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="13" height="13" viewBox="0 0 20 20" fill="none"><path d="M18.3333 14.1667C18.3333 14.6087 18.1577 15.0326 17.8452 15.3452C17.5326 15.6577 17.1087 15.8333 16.6667 15.8333L5.69 15.8333C5.24801 15.8334 4.82416 16.0091 4.51167 16.3217L2.67667 18.1567C2.59393 18.2394 2.48851 18.2957 2.37375 18.3186C2.25898 18.3414 2.14003 18.3297 2.03193 18.2849C1.92382 18.2401 1.83142 18.1643 1.76641 18.067C1.7014 17.9697 1.66669 17.8553 1.66667 17.7383L1.66667 4.16667C1.66667 3.72464 1.84227 3.30072 2.15483 2.98816C2.46739 2.67559 2.89131 2.5 3.33334 2.5L16.6667 2.5C17.1087 2.5 17.5326 2.67559 17.8452 2.98816C18.1577 3.30072 18.3333 3.72464 18.3333 4.16667L18.3333 14.1667Z" stroke="currentColor" stroke-width="2" stroke-linejoin="round" stroke-linecap="round" style="mix-blend-mode:normal"></path></svg>
|
||||
<span>立即咨询</span>
|
||||
</button>
|
||||
</div>
|
||||
@@ -405,6 +409,7 @@
|
||||
:level="2"
|
||||
:maxSelect="3"
|
||||
:allowParentSelect="false"
|
||||
:allowParentSelectClick="true"
|
||||
:triggerStyle="filterTriggerStyle"
|
||||
:displayStyle="filterDisplayStyle"
|
||||
@update:industryIds="onHomeIndustryChange"
|
||||
@@ -424,6 +429,7 @@
|
||||
:level="3"
|
||||
:maxSelect="3"
|
||||
:allowParentSelect="false"
|
||||
:allowParentSelectClick="true"
|
||||
:triggerStyle="filterTriggerStyle"
|
||||
:displayStyle="filterDisplayStyle"
|
||||
@update:categoryIds="onHomeCategoryChange"
|
||||
@@ -473,6 +479,7 @@
|
||||
</div>
|
||||
<div class="home-footer__bottom">
|
||||
<p>©2016-2026 - 广州油梨信息科技有限公司 版权所有</p>
|
||||
<a href="https://beian.miit.gov.cn/" target="_blank" class="">粤ICP备16019697号-7</a>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
@@ -500,6 +507,12 @@ const router = useRouter()
|
||||
/** Vuex 状态管理实例 */
|
||||
const store = useStore()
|
||||
|
||||
// 声明友盟统计 _czc 对象
|
||||
const _czc: any[] = (window as any)._czc || [];
|
||||
(window as any)._czc = _czc;
|
||||
// 绑定 siteid,请用您的 siteid 替换下方 "XXXXXXXX" 部分
|
||||
_czc.push(["_setAccount", "1281490571"]);
|
||||
|
||||
/** 页面是否已向下滚动超过 20px — 用于导航栏背景切换 */
|
||||
const isScrolled = ref(false)
|
||||
|
||||
@@ -675,7 +688,7 @@ onMounted(() => {
|
||||
store.dispatch('loadCommonData')
|
||||
})
|
||||
|
||||
/** 点击登陆按钮 — 跳转到登录页面,登录成功后跳转到 jobs 页面 */
|
||||
/** 点击登录按钮 — 跳转到登录页面,登录成功后跳转到 jobs 页面 */
|
||||
function handleLoginClick() {
|
||||
router.push({ name: 'Login', query: { redirect: '/jobs' } })
|
||||
}
|
||||
|
||||
@@ -99,7 +99,7 @@
|
||||
"真没想到校招居然可以这么轻松高效!一周就收到了3个面试邀约。"
|
||||
</p>
|
||||
<div class="home-mobile__quote-author">
|
||||
<div class="home-mobile__quote-avatar"></div>
|
||||
<div class="home-mobile__quote-avatar"><img class="w45 h45 " src="@/assets/images/home/student-avatar-0.png" alt=""></div>
|
||||
<div class="home-mobile__quote-info">
|
||||
<span class="home-mobile__quote-name">李同学</span>
|
||||
<span class="home-mobile__quote-school">西安交通大学</span>
|
||||
@@ -157,7 +157,7 @@
|
||||
</div>
|
||||
<p class="home-mobile__dialog-qr-title">保存二维码,扫码加入求职交流群</p>
|
||||
<div class="home-mobile__dialog-qr">
|
||||
<img src="@/assets/images/home/mobile-qcCode.png" alt="二维码" class="home-mobile__dialog-qr-img" />
|
||||
<img src="https://offerpie.oss-cn-guangzhou.aliyuncs.com/images/mobile-qcCode.png" alt="二维码" class="home-mobile__dialog-qr-img" />
|
||||
</div>
|
||||
<p class="home-mobile__dialog-qr-hint">长按保存二维码获取更多求职资料,公开课</p>
|
||||
<button class="home-mobile__dialog-btn" @click="showDialog = false">知道了</button>
|
||||
@@ -170,9 +170,57 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import { getDailyJobCount } from '@/utils/dailyJobCount'
|
||||
|
||||
// ==================== 移动端缩放重置 ====================
|
||||
/**
|
||||
* 确保 HomeMobile 页面不受全局 transform scale 缩放影响
|
||||
* 部分浏览器中 remAdapt 插件的 recalc 在路由切换后未及时重新执行,
|
||||
* 导致 body 上仍残留 PC 端 scale 样式,这里在组件挂载时主动重置
|
||||
*/
|
||||
function resetMobileScale() {
|
||||
const docEl = document.documentElement
|
||||
const body = document.body
|
||||
const clientWidth = docEl.clientWidth || window.innerWidth
|
||||
// 移动端首页 1rem = 视口宽度 / 3.90(设计稿 390px)
|
||||
const mobileFontSize = clientWidth / 3.90
|
||||
docEl.style.fontSize = mobileFontSize + 'px'
|
||||
// 清除 scale 缩放相关样式
|
||||
body.style.width = ''
|
||||
body.style.height = ''
|
||||
body.style.minHeight = ''
|
||||
body.style.overflowY = ''
|
||||
body.style.transform = ''
|
||||
body.style.transformOrigin = ''
|
||||
docEl.style.height = ''
|
||||
docEl.style.overflow = ''
|
||||
docEl.style.setProperty('--app-height', '100vh')
|
||||
docEl.style.setProperty('--chat-width', '100%')
|
||||
}
|
||||
|
||||
/** 窗口 resize 时重新执行重置 */
|
||||
function onResize() {
|
||||
resetMobileScale()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
resetMobileScale()
|
||||
window.addEventListener('resize', onResize)
|
||||
window.addEventListener('orientationchange', onResize)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', onResize)
|
||||
window.removeEventListener('orientationchange', onResize)
|
||||
})
|
||||
|
||||
// 声明友盟统计 _czc 对象
|
||||
const _czc: any[] = (window as any)._czc || [];
|
||||
(window as any)._czc = _czc;
|
||||
// 绑定 siteid,请用您的 siteid 替换下方 "XXXXXXXX" 部分
|
||||
_czc.push(["_setAccount", "1281490571"]);
|
||||
|
||||
/** 今日更新岗位数量 */
|
||||
const dailyJobCount = getDailyJobCount()
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<SideNav />
|
||||
<div class="job-detail__content">
|
||||
<!-- 页面标题 + Tab 切换 -->
|
||||
<JobPageHeader :activeTab="cachedTab" />
|
||||
<JobPageHeader ref="jobPageHeaderRef" :activeTab="cachedTab" />
|
||||
<!-- 主内容卡片 -->
|
||||
<div class="job-detail__main-card">
|
||||
<!-- 返回按钮 -->
|
||||
@@ -169,6 +169,15 @@
|
||||
<MemberAccessDialog v-model="showMemberAccessDialog" @open-member="showMemberDialog = true" />
|
||||
<!-- 会员购买弹窗 -->
|
||||
<MemberDialog v-model="showMemberDialog" />
|
||||
|
||||
<!-- 选择投递方式弹窗 -->
|
||||
<JobApplyMethodDialog
|
||||
ref="applyMethodDialogRef"
|
||||
v-model="showApplyMethodDialog"
|
||||
:source-url="job.sourceUrl"
|
||||
:job-id="jobId"
|
||||
@open-member="showMemberDialog = true"
|
||||
/>
|
||||
<!-- 全屏加载遮罩 -->
|
||||
<FullscreenLoading :visible="fullLoading" :text="fullLoadingText" title="Nova 正在帮您努力处理~" />
|
||||
|
||||
@@ -187,7 +196,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { ref, reactive, computed, onMounted, nextTick } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { useStore } from 'vuex'
|
||||
import { formatEmploymentType } from '@/stores/index'
|
||||
@@ -199,16 +208,26 @@ import MemberAccessDialog from '@/components/MemberAccessDialog.vue'
|
||||
import FullscreenLoading from '@/components/FullscreenLoading.vue'
|
||||
import MemberDialog from '@/components/MemberDialog.vue'
|
||||
import StepProgressOverlay from '@/components/StepProgressOverlay.vue'
|
||||
import JobApplyMethodDialog from '@/components/JobApplyMethodDialog.vue'
|
||||
import { fetchJobDetail, toggleJobFavorite, removeJobFavorite, fetchSkillGap } from '@/api/jobs'
|
||||
import { fetchMemberStatus } from '@/api/member'
|
||||
|
||||
import type { JobDetailData, SkillGapData } from '@/api/jobs'
|
||||
|
||||
|
||||
|
||||
// ==================== 路由相关 ====================
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const store = useStore()
|
||||
|
||||
|
||||
// 声明友盟统计 _czc 对象
|
||||
const _czc: any[] = (window as any)._czc || [];
|
||||
(window as any)._czc = _czc;
|
||||
// 绑定 siteid,请用您的 siteid 替换下方 "XXXXXXXX" 部分
|
||||
_czc.push(["_setAccount", "1281490571"]);
|
||||
|
||||
// ==================== 全屏加载状态 ====================
|
||||
/** 全屏加载是否显示 */
|
||||
const fullLoading = ref(false)
|
||||
@@ -443,6 +462,9 @@ function goBack() {
|
||||
/** AiChat 组件引用 */
|
||||
const aiChatRef = ref<InstanceType<typeof AiChat> | null>(null)
|
||||
|
||||
/** JobPageHeader 组件引用,用于刷新收藏/投递计数 */
|
||||
const jobPageHeaderRef = ref<InstanceType<typeof JobPageHeader> | null>(null)
|
||||
|
||||
/** 问 Nova — 触发 AI 聊天,推送提示消息 */
|
||||
function handleAskNova() {
|
||||
aiChatRef.value?.triggerAsk()
|
||||
@@ -479,16 +501,20 @@ const resumeCustomJobInfo = computed(() => ({
|
||||
|
||||
/** 生成岗位专属简历 — 先检查会员状态,非会员弹权限拦截弹窗 */
|
||||
async function handleGenerateResume() {
|
||||
// 先检查会员状态
|
||||
try {
|
||||
const statusRes = await fetchMemberStatus()
|
||||
if (statusRes.code === '0' && statusRes.data && !statusRes.data.isMember) {
|
||||
// 非会员 — 弹出会员权限拦截弹窗
|
||||
showMemberAccessDialog.value = true
|
||||
return
|
||||
// 优先从 vuex store 读取会员状态,没有则请求接口
|
||||
let isMember = store.state.memberStatus?.isMember
|
||||
if (isMember === undefined || isMember === null) {
|
||||
try {
|
||||
await store.dispatch('loadMemberStatus')
|
||||
isMember = store.state.memberStatus?.isMember
|
||||
} catch {
|
||||
// 接口异常时不阻断,继续执行
|
||||
}
|
||||
} catch {
|
||||
// 接口异常时不阻断,继续执行
|
||||
}
|
||||
if (!isMember) {
|
||||
// 非会员 — 弹出会员权限拦截弹窗
|
||||
showMemberAccessDialog.value = true
|
||||
return
|
||||
}
|
||||
|
||||
// 显示步骤进度遮罩
|
||||
@@ -556,6 +582,8 @@ async function toggleFavorite() {
|
||||
const wasFavorite = job.isFavorite
|
||||
job.isFavorite = !job.isFavorite
|
||||
ElMessage.success(wasFavorite ? '已取消收藏' : '收藏成功')
|
||||
// 同步刷新 JobPageHeader 组件内的收藏计数
|
||||
jobPageHeaderRef.value?.loadFavoriteCount()
|
||||
// 同步收藏状态到 Jobs 页面缓存,返回时无需刷新列表即可看到最新状态
|
||||
const cache = store.state.jobListCache
|
||||
if (cache) {
|
||||
@@ -568,10 +596,14 @@ async function toggleFavorite() {
|
||||
}
|
||||
}
|
||||
|
||||
/** 去投递 — 跳转到来源链接 */
|
||||
/** 去投递 — 打开选择投递方式弹窗 */
|
||||
const showApplyMethodDialog = ref(false)
|
||||
const applyMethodDialogRef = ref<InstanceType<typeof JobApplyMethodDialog> | null>(null)
|
||||
|
||||
function handleApply() {
|
||||
if (job.sourceUrl) {
|
||||
window.open(job.sourceUrl, '_blank')
|
||||
}
|
||||
showApplyMethodDialog.value = true
|
||||
nextTick(() => {
|
||||
applyMethodDialogRef.value?.open()
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||