diff --git a/components.d.ts b/components.d.ts index 551163f..48180a1 100644 --- a/components.d.ts +++ b/components.d.ts @@ -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'] diff --git a/src/assets/images/agent-setting/1-1.png b/src/assets/images/agent-setting/1-1.png new file mode 100644 index 0000000..a7c6682 Binary files /dev/null and b/src/assets/images/agent-setting/1-1.png differ diff --git a/src/assets/images/agent-setting/2-2.png b/src/assets/images/agent-setting/2-2.png deleted file mode 100644 index 2ddca1e..0000000 Binary files a/src/assets/images/agent-setting/2-2.png and /dev/null differ diff --git a/src/assets/images/agent-setting/2-3.png b/src/assets/images/agent-setting/2-3.png deleted file mode 100644 index cca9e7b..0000000 Binary files a/src/assets/images/agent-setting/2-3.png and /dev/null differ diff --git a/src/assets/images/agent-setting/3-1.png b/src/assets/images/agent-setting/3-1.png index 019d22e..d3509e6 100644 Binary files a/src/assets/images/agent-setting/3-1.png and b/src/assets/images/agent-setting/3-1.png differ diff --git a/src/assets/images/agent-setting/4-1.png b/src/assets/images/agent-setting/4-1.png index e6f59fa..c95f7c1 100644 Binary files a/src/assets/images/agent-setting/4-1.png and b/src/assets/images/agent-setting/4-1.png differ diff --git a/src/assets/styles/auto.scss b/src/assets/styles/auto.scss index 5961061..a9dd1b4 100644 --- a/src/assets/styles/auto.scss +++ b/src/assets/styles/auto.scss @@ -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;} diff --git a/src/assets/styles/components/agent-apply-progress.scss b/src/assets/styles/components/agent-apply-progress.scss index bbaf109..c6c75f1 100644 --- a/src/assets/styles/components/agent-apply-progress.scss +++ b/src/assets/styles/components/agent-apply-progress.scss @@ -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 { diff --git a/src/assets/styles/components/agent-browser-install-guide.scss b/src/assets/styles/components/agent-browser-install-guide.scss new file mode 100644 index 0000000..647adae --- /dev/null +++ b/src/assets/styles/components/agent-browser-install-guide.scss @@ -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; +} diff --git a/src/assets/styles/components/agent-chat-job-list.scss b/src/assets/styles/components/agent-chat-job-list.scss index 28be504..97fc7dc 100644 --- a/src/assets/styles/components/agent-chat-job-list.scss +++ b/src/assets/styles/components/agent-chat-job-list.scss @@ -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; diff --git a/src/assets/styles/components/agent-job-preview-panel.scss b/src/assets/styles/components/agent-job-preview-panel.scss index 6cefa8b..1064b36 100644 --- a/src/assets/styles/components/agent-job-preview-panel.scss +++ b/src/assets/styles/components/agent-job-preview-panel.scss @@ -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; } diff --git a/src/assets/styles/components/agent-setting-panel.scss b/src/assets/styles/components/agent-setting-panel.scss index 48a9ef6..afed445 100644 --- a/src/assets/styles/components/agent-setting-panel.scss +++ b/src/assets/styles/components/agent-setting-panel.scss @@ -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; } } diff --git a/src/assets/styles/components/agent-task-list-dropdown.scss b/src/assets/styles/components/agent-task-list-dropdown.scss index db2ec02..da1450c 100644 --- a/src/assets/styles/components/agent-task-list-dropdown.scss +++ b/src/assets/styles/components/agent-task-list-dropdown.scss @@ -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; diff --git a/src/assets/styles/components/job-goal-dialog.scss b/src/assets/styles/components/job-goal-dialog.scss index 86b8f4e..110b3b1 100644 --- a/src/assets/styles/components/job-goal-dialog.scss +++ b/src/assets/styles/components/job-goal-dialog.scss @@ -42,7 +42,7 @@ &__title { font-size: 0.18rem; - font-weight: 520; + font-weight: 620; line-height: 0.24rem; color: #1A1A1A; } diff --git a/src/assets/styles/components/job-page-header.scss b/src/assets/styles/components/job-page-header.scss index e55c0d9..f0622c5 100644 --- a/src/assets/styles/components/job-page-header.scss +++ b/src/assets/styles/components/job-page-header.scss @@ -74,7 +74,7 @@ left: 0.08rem; top: 0.245rem; font-family: 'MiSans'; - font-weight: 520; + font-weight: 620; font-size: 0.10rem; line-height: 0.13rem; color: #8FB7BE; diff --git a/src/assets/styles/index.scss b/src/assets/styles/index.scss index 1704c51..824a146 100644 --- a/src/assets/styles/index.scss +++ b/src/assets/styles/index.scss @@ -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'; diff --git a/src/assets/styles/pages/agent.scss b/src/assets/styles/pages/agent.scss index 284c37a..fda81b5 100644 --- a/src/assets/styles/pages/agent.scss +++ b/src/assets/styles/pages/agent.scss @@ -279,8 +279,7 @@ border-radius: 0.12rem; max-height: calc(var(--app-height, 100vh) - 1.4rem); overflow-y: auto; - display: flex; - flex-direction: column; + padding: 0.24rem; scrollbar-width: none; // Firefox -ms-overflow-style: none; // IE/Edge @@ -690,7 +689,7 @@ &__step2-faq-title { font-size: 0.18rem; - font-weight: 520; + font-weight: 620; color: $accent; margin-bottom: 0.13rem; } @@ -706,7 +705,7 @@ &__step2-faq-item-title { font-size: 0.14rem; - font-weight: 520; + font-weight: 620; color: $text-dark; margin-bottom: 0.08rem; } @@ -972,7 +971,6 @@ display: flex; flex-direction: column; gap: 0.16rem; - padding: 0.24rem; } // 单个步骤卡片 @@ -1014,7 +1012,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 +1027,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 +1085,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; @@ -1343,7 +1341,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; @@ -1372,7 +1370,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; @@ -1772,7 +1770,7 @@ /* 标题文字 */ &__title { font-size: 0.18rem; - font-weight: 520; + font-weight: 620; color: $text-dark; line-height: 0.24rem; } @@ -1867,3 +1865,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; + } + } +} diff --git a/src/assets/styles/pages/jobs.scss b/src/assets/styles/pages/jobs.scss index 530a346..3a2c087 100644 --- a/src/assets/styles/pages/jobs.scss +++ b/src/assets/styles/pages/jobs.scss @@ -465,7 +465,7 @@ border-radius: 0.04rem; font-size: 0.12rem; font-weight: 330; - color: $accent; + color: #fff; line-height: 0.16rem; } @@ -1220,7 +1220,7 @@ /* 标题文字 */ &__title { font-size: 0.18rem; - font-weight: 520; + font-weight: 620; color: $text-dark; line-height: 0.24rem; } diff --git a/src/components/AgentBrowserInstallGuide.vue b/src/components/AgentBrowserInstallGuide.vue new file mode 100644 index 0000000..7b4243a --- /dev/null +++ b/src/components/AgentBrowserInstallGuide.vue @@ -0,0 +1,171 @@ + + + + diff --git a/src/components/AgentSettingPanel.vue b/src/components/AgentSettingPanel.vue index 3200626..7ea5f0e 100644 --- a/src/components/AgentSettingPanel.vue +++ b/src/components/AgentSettingPanel.vue @@ -250,141 +250,7 @@ -
- -
-
- 1 -
-

下载插件包

-

下载 Offer派自动投递助手安装包,下载完成后由步骤 2 进入浏览器扩展页面

-
-
-
- Offer派自动投递助手.crx - -
-
- - -
-
- 2 -
-

打开扩展管理页面

-

在浏览器地址栏输入对应地址并回车,打开扩展管理页面

-
-
- - - - 输入扩展地址 -
- - -
-
- 3 -
-

打开开发者模式

-

在扩展管理页面的右上角,找到「开发者模式」开关并打开

-
-
- - 打开开发者模式 -
- - -
-
- 4 -
-

将 CRX 安装包拖入安装

-

直接将下载好的CRX文件拖拽到扩展管理页面中,浏览器会自动识别并安装插件

-
-
- - 拖入安装 -
-
+ @@ -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 () => { diff --git a/src/components/AgentSetupWizard.vue b/src/components/AgentSetupWizard.vue index d614f69..c883ec0 100644 --- a/src/components/AgentSetupWizard.vue +++ b/src/components/AgentSetupWizard.vue @@ -48,6 +48,7 @@

请仔细对你的资料评估(包括个人信息、教育背景、工作履历及技能),确保资料齐全

+ @@ -163,164 +164,7 @@
-
- - -
-
-
-
- 1 -
-

下载插件包

-

下载 Offer派自动投递助手安装包,下载完成后由步骤 2 进入浏览器扩展页面

-
-
-
-
-
- Offer派自动投递助手.crx - -
-
-
-
- - - - - -
-
-
-
- 3 -
-

打开开发者模式

-

在扩展管理页面的右上角,找到「开发者模式」开关并打开

-
-
-
-
- 打开开发者模式 -
-
-
- - -
-
-
-
- 4 -
-

将 CRX 安装包拖入安装

-

直接将下载好的CRX文件拖拽到扩展管理页面中,浏览器会自动识别并安装插件

-
-
-
-
- 拖入安装 -
-
-
- -
+
@@ -335,8 +179,14 @@

安装浏览器自动投递插件

按照左侧步骤完成插件安装,即可开启自动投递功能

- - + + + @@ -357,6 +207,31 @@ + + + +
+
+ +
+ +
+ + + +
+ +

插件是否已安装成功?

+ +

智能投递助手需要配合浏览器插件使用
请确认插件已成功安装并启用
否则将无法正常进行自动填写和投递

+ +
+ + +
+
+
+
@@ -391,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' @@ -434,6 +310,13 @@ const editInitialData = ref>({}) // ==================== 个人档案数据 ==================== onMounted(async () => { + // 恢复刷新前保存的步骤号 + const savedStep = sessionStorage.getItem('wizardStep') + if (savedStep) { + currentStep.value = Number(savedStep) + sessionStorage.removeItem('wizardStep') + } + if (!store.state.regions.length) store.dispatch('loadCommonData') store.dispatch('loadJobIntention') await loadProfile() @@ -444,6 +327,10 @@ onMounted(async () => { await loadCompetition() }) +// ==================== 通信桥接:插件在线状态从 store 全局获取 ==================== +/** 插件是否在线(从 vuex 全局状态读取) */ +const plugIsOnline = computed(() => store.state.plugIsOnline) + async function loadProfile() { try { const res = await fetchProfile() @@ -763,19 +650,25 @@ 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 | 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)) + location.reload() } // ==================== 第4步:配置求职助手 ==================== diff --git a/src/main.ts b/src/main.ts index 9fc5c1b..d0dbb0d 100644 --- a/src/main.ts +++ b/src/main.ts @@ -9,4 +9,7 @@ createApp(App) .use(router) .use(store) .use(remAdaptPlugin) - .mount('#app') \ No newline at end of file + .mount('#app') + +// 启动浏览器插件心跳监听(全局常驻) +store.dispatch('startPlugHeartbeat') \ No newline at end of file diff --git a/src/stores/index.ts b/src/stores/index.ts index 1e01e16..4baa110 100644 --- a/src/stores/index.ts +++ b/src/stores/index.ts @@ -11,6 +11,7 @@ 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' /** 招聘分类选项:label → 接口参数 recruitCategory */ export const JOB_TYPE_OPTIONS: { label: string; value: number }[] = [ @@ -164,6 +165,12 @@ export interface RootState { * 全局会员权限拦截弹窗 — 路由守卫或页面检测到免费用户时触发 */ showMemberAccessDialog: boolean + + /** 通信桥接组件的域名标识(用于 BroadcastChannel 频道隔离) */ + bridgeDomain: string + + /** 浏览器插件是否在线(3秒内有 BroadcastChannel 心跳则为 true) */ + plugIsOnline: boolean } export default createStore({ @@ -195,6 +202,10 @@ export default createStore({ settingsTab: 'account', inviteCode: '', showMemberAccessDialog: false, + /** 通信桥接组件的域名标识(用于 BroadcastChannel 频道隔离) */ + bridgeDomain: 'test.offerpai.com.cn', + /** 浏览器插件是否在线 */ + plugIsOnline: false, }, getters: { getAppName: (state) => state.appName, @@ -270,6 +281,9 @@ export default createStore({ SET_SHOW_MEMBER_ACCESS_DIALOG(state, show: boolean) { state.showMemberAccessDialog = show }, + SET_PLUG_IS_ONLINE(state, online: boolean) { + state.plugIsOnline = online + }, }, actions: { updateAppName({ commit }, name: string) { @@ -448,6 +462,81 @@ export default createStore({ 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 判断是否在线并更新状态 */ + function checkOnline(updateTime: string) { + const elapsed = Date.now() - parseTime(updateTime) + if (elapsed <= 3000) { + lastHeartbeat = Date.now() + if (!state.plugIsOnline) { + commit('SET_PLUG_IS_ONLINE', true) + } + } + } + + // 启动时立即 get 一次,根据 updateTime 判断初始在线状态 + bridge.get<{ usage: string }>(state.bridgeDomain, 'offerpieBrowserPlugUsage').then((data) => { + if (data && data.updateTime) { + checkOnline(data.updateTime) + } + }) + + // 方式1:BroadcastChannel watch — 实时收心跳 + const unwatch = bridge.watch<{ usage: string }>( + state.bridgeDomain, + 'offerpieBrowserPlugUsage', + (data) => { + if (data && data.updateTime) { + checkOnline(data.updateTime) + } + } + ) + + // 方式2:定时轮询 get IndexedDB — 兜底(覆盖 BroadcastChannel 断连场景) + const pollTimer = setInterval(async () => { + try { + const data = await bridge.get<{ usage: string }>(state.bridgeDomain, 'offerpieBrowserPlugUsage') + // console.log('[plugHeartbeat] poll get:', data) + if (data && data.updateTime) { + checkOnline(data.updateTime) + } + } 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: {}, }) diff --git a/src/utils/channelBridge.ts b/src/utils/channelBridge.ts new file mode 100644 index 0000000..0bdf63b --- /dev/null +++ b/src/utils/channelBridge.ts @@ -0,0 +1,417 @@ +/** + * OfferPie 跨标签页通信桥接组件 + * 用于浏览器插件与网站页面之间的纯本地通信(不依赖任何后端接口) + * + * 【使用说明】 + * 1. 引入并创建实例: + * import { createChannelBridge } from './channelBridge' + * const bridge = createChannelBridge({ domain: 'offerpai.com', isPlugin: true }) + * + * 2. 发送数据(单次,同时写缓存 + 广播): + * bridge.put('offerpai.com', 'resumeStatus', { ... }) + * + * 3. 高频发送(节流写缓存,每次都广播): + * bridge.emit('offerpai.com', 'cursorPosition', { ... }) + * + * 4. 主动获取一次数据(先广播问对方,60ms超时转IndexedDB): + * const result = await bridge.get('offerpai.com', 'resumeStatus') + * + * 5. 持续监听数据变化(初始读一次缓存,后续实时接收 put/emit): + * const unwatch = bridge.watch('offerpai.com', 'resumeStatus', (data) => { ... }) + * // 取消监听 + * unwatch() + * + * 6. 销毁实例(页面卸载时调用): + * bridge.destroy() + * + * 【参数说明】 + * - domain: 频道域名标识,不同网站项目用不同域名隔离 + * - isPlugin: 是否为插件环境,默认 false(非插件时会启用 Web Lock 保活) + * + * 【数据类型要求】 + * - 传递的数据必须有 TypeScript 类型定义(泛型约束 extends object) + * - 组件自动为每条数据添加 createTime 和 updateTime 字段 + * + * ============================================================ + * 【通信数据名称注册表】 + * 在此区域记录所有通信数据的名称、类型和用途说明 + * 新增数据时在此补充,删除时移除对应行 + * ------------------------------------------------------------ + * | 数据名(name) | 中文名称 | 说明 | + * | ----------------------------- | -------------- | --------------------------------- | + * | offerpieBrowserPlugUsage | 插件使用状态 | 标识插件正在运行 { usage: string } | + * ============================================================ + */ + +// ============ 类型定义 ============ + +/** 组件自动附加的时间戳字段 */ +export interface BridgeTimestamp { + /** 数据首次创建时间(格式:yyyy-MM-dd HH:mm:ss) */ + createTime: string + /** 数据最近更新时间(格式:yyyy-MM-dd HH:mm:ss) */ + updateTime: string +} + +/** 带时间戳的完整数据包装 */ +export type BridgeData = T & BridgeTimestamp + +/** IndexedDB 存储记录结构 */ +interface DBRecord { + /** 数据名称(唯一索引) */ + name: string + /** JSON 数据本体 */ + data: object + /** 创建时间 */ + createTime: string + /** 更新时间 */ + updateTime: string +} + +/** BroadcastChannel 消息结构 */ +interface ChannelMessage { + /** 消息类型 */ + type: 'put' | 'emit' | 'get-request' | 'get-response' + /** 数据名称 */ + name: string + /** 请求唯一ID(用于 get 请求-响应匹配) */ + id?: string + /** 数据本体(put/emit/get-response 携带) */ + data?: object + /** 创建时间 */ + createTime?: string + /** 更新时间 */ + updateTime?: string +} + +/** watch 回调函数类型 */ +type WatchCallback = (data: BridgeData | null) => void + +/** 创建实例的配置参数 */ +export interface ChannelBridgeOptions { + /** 默认域名(可在方法调用时覆盖) */ + domain: string + /** 是否为插件环境,默认 false。非插件时启用 Web Lock 保活 */ + isPlugin?: boolean +} + +// ============ 常量 ============ + +/** IndexedDB 数据库名称(含固定雪花ID保证唯一性) */ +const DB_NAME = 'offerpie_comm_7394028156183472' +/** IndexedDB 版本号 */ +const DB_VERSION = 1 +/** get 方法 BroadcastChannel 超时时间(毫秒) */ +const GET_TIMEOUT_MS = 60 +/** emit 方法节流写库间隔(毫秒) */ +const EMIT_THROTTLE_MS = 500 + +// ============ 工具函数 ============ + +/** 获取当前时间的 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) +} + +// ============ IndexedDB 操作 ============ + +/** 打开/创建 IndexedDB 数据库,按 domain 动态创建 ObjectStore */ +function openDB(storeName: string): Promise { + return new Promise((resolve, reject) => { + const request = indexedDB.open(DB_NAME, DB_VERSION) + 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 + // 如果 store 不存在(版本没升级的情况),关闭后升版本重建 + if (!db.objectStoreNames.contains(storeName)) { + db.close() + 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 = () => reject(req2.error) + } else { + resolve(db) + } + } + request.onerror = () => reject(request.error) + }) +} + +/** 写入或更新一条记录到 IndexedDB */ +async function dbPut(storeName: string, record: DBRecord): Promise { + 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 = () => { db.close(); resolve() } + tx.onerror = () => { db.close(); reject(tx.error) } + }) +} + +/** 从 IndexedDB 读取一条记录 */ +async function dbGet(storeName: string, name: string): Promise { + 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 = () => { db.close(); resolve(req.result || null) } + req.onerror = () => { db.close(); reject(req.error) } + }) +} + +// ============ 核心:创建通信桥接实例 ============ + +export function createChannelBridge(options: ChannelBridgeOptions) { + const { domain, isPlugin = false } = options + + /** BroadcastChannel 实例缓存(按 domain 隔离频道) */ + const channels = new Map() + + /** watch 订阅回调注册表:key = `${domain}::${name}` */ + const watchers = new Map>>() + + /** emit 节流定时器:key = `${domain}::${name}` */ + const emitTimers = new Map>() + + /** emit 节流暂存最新数据:key = `${domain}::${name}` */ + const emitPending = new Map() + + /** get 请求等待队列:key = requestId */ + const getResolvers = new Map | 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) => 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 + : 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) + } else { + resolver(null) + } + } + } + } + + // 初始化默认频道监听 + getChannel(domain) + + // --- 公开方法 --- + + /** + * put - 单次发送数据(写入 IndexedDB + BroadcastChannel 广播) + * @param targetDomain 目标域名频道 + * @param name 数据名称 + * @param data 数据本体(需有 TS 类型定义) + */ + async function put(targetDomain: string, name: string, data: T): Promise { + const now = nowLocalDateTime() + // 读取已有记录判断是否为新建 + 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 类型定义) + */ + async function emit(targetDomain: string, name: string, data: T): Promise { + const now = nowLocalDateTime() + const key = `${targetDomain}::${name}` + + // 先尝试获取 createTime(从暂存或数据库) + let createTime: string + const pending = emitPending.get(key) + if (pending) { + createTime = pending.createTime + } else { + const existing = await dbGet(targetDomain, name) + createTime = existing?.createTime || now + } + const updateTime = now + const record: DBRecord = { name, data, createTime, updateTime } + + // 暂存最新数据 + 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) { + emitPending.delete(key) + await dbPut(targetDomain, latestRecord) + } + }, EMIT_THROTTLE_MS)) + } + } + + /** + * get - 主动获取一次数据(先 BroadcastChannel 请求,60ms 超时转 IndexedDB) + * @param targetDomain 目标域名频道 + * @param name 数据名称 + * @returns 数据本体(含 createTime/updateTime),无数据返回 null + */ + async function get(targetDomain: string, name: string): Promise | null> { + const ch = getChannel(targetDomain) + const requestId = uid() + + // 发起广播请求 + const msg: ChannelMessage = { type: 'get-request', name, id: requestId } + ch.postMessage(msg) + + // 等待响应或超时 + const result = await new Promise | 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 + } + return null + } + + /** + * watch - 持续监听数据变化(初始读一次缓存,后续实时接收 put/emit 推送) + * @param targetDomain 目标域名频道 + * @param name 数据名称 + * @param callback 数据变化回调,参数为最新数据或 null + * @returns 取消监听的函数 + */ + function watch(targetDomain: string, name: string, callback: WatchCallback): () => void { + const key = `${targetDomain}::${name}` + // 确保频道已初始化 + getChannel(targetDomain) + + // 注册回调 + if (!watchers.has(key)) watchers.set(key, new Set()) + watchers.get(key)!.add(callback) + + // 初始读取 IndexedDB + dbGet(targetDomain, name).then((record) => { + if (record) { + callback({ ...record.data, createTime: record.createTime, updateTime: record.updateTime } as BridgeData) + } 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 } +} diff --git a/src/views/Agent.vue b/src/views/Agent.vue index a940c70..5b6c38b 100644 --- a/src/views/Agent.vue +++ b/src/views/Agent.vue @@ -7,18 +7,19 @@
- --> + - - - - - - +
diff --git a/src/views/Jobs.vue b/src/views/Jobs.vue index 09058d7..735cd9d 100644 --- a/src/views/Jobs.vue +++ b/src/views/Jobs.vue @@ -175,7 +175,7 @@ {{ job.regionName }} {{ formatEducation(job.education) }} - {{ formatEmploymentType(job.recruitCategory) }} + {{ formatEmploymentType(job.recruitCategory) }}
@@ -404,6 +404,14 @@ const router = useRouter() const route = useRoute() const store = useStore() +/** 招聘分类标签背景色:校招=#3AC6E5,社招=#B95DDD,实习=#50D3B5 */ +function recruitCategoryColor(category: number | undefined | null): string { + if (category === 1) return '#3AC6E5' // 校招 + if (category === 0) return '#B95DDD' // 社招 + if (category === 2) return '#50D3B5' // 实习 + return '#EDF9FA' +} + /** 登录状态 — 从 store 读取 */ const isAuthenticated = computed(() => store.state.isAuthenticated)