diff --git a/src/api/aiApi.ts b/src/api/aiApi.ts index 0358aa7..48597f8 100644 --- a/src/api/aiApi.ts +++ b/src/api/aiApi.ts @@ -1,5 +1,6 @@ import { config } from "~config" import { createHttp } from "./request" +import type { ResumeData } from "~lib/types" /** Python AI 后端接口 */ const http = createHttp(config.aiBaseApi) @@ -8,3 +9,8 @@ const http = createHttp(config.aiBaseApi) export function healthCheck() { return http.get('/health/') } + +/** 查询岗位定制简历 */ +export function getCustomizeResume(jobId: number) { + return http.get('/job/customize-resume', { params: { job_id: String(jobId) } }) +} diff --git a/src/api/dataApi.ts b/src/api/dataApi.ts index b30f49d..180d17f 100644 --- a/src/api/dataApi.ts +++ b/src/api/dataApi.ts @@ -1,12 +1,13 @@ import { config } from "~config" import { createHttp } from "./request" +import type { JobInfo } from "~lib/types" /** Java 后端接口 */ const http = createHttp(config.dataBaseApi) /** 根据岗位来源地址查询岗位信息 */ export function findJobBySourceUrl(sourceUrl: string) { - return http.get('/job/findByUrl', { params: { sourceUrl } }) + return http.get('/job/findByUrl', { params: { sourceUrl } }) } /** 校验登录状态 */ diff --git a/src/components/SidebarPanel.scss b/src/components/SidebarPanel.scss index 6a9838f..dc3f750 100644 --- a/src/components/SidebarPanel.scss +++ b/src/components/SidebarPanel.scss @@ -9,6 +9,7 @@ $radius-sm: 12px; top: 10px; right: 10px; width: 384px; + min-height: 480px; max-height: calc(100vh - 20px); background: #fff; border-radius: $radius-lg; @@ -251,3 +252,35 @@ $radius-sm: 12px; transition: width 0.3s; } } + +/* 未登录提示区域 */ +.op-login-prompt { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 60px 20px; +} + +.op-login-text { + font-size: 15px; + color: #555; + margin-bottom: 24px; + text-align: center; +} + +.op-login-btn { + width: 100%; + height: 48px; + background: $primary; + color: #fff; + border: none; + border-radius: $radius-md; + font-size: 16px; + font-weight: 600; + cursor: pointer; + transition: opacity 0.2s; + + &:hover { opacity: 0.85; } +} diff --git a/src/components/SidebarPanel.tsx b/src/components/SidebarPanel.tsx index bc831db..8e6cdbe 100644 --- a/src/components/SidebarPanel.tsx +++ b/src/components/SidebarPanel.tsx @@ -4,25 +4,33 @@ * 包含:职位信息卡片、自动填写按钮、简历管理、填写进度等功能区域 */ -import { useState } from "react" +import { useState, useEffect } from "react" +import { getCookieValue } from "~utils/cookie" +import { getCustomizeResume } from "~api/aiApi" import { fillMatchedField, delay, isSearchPickerField, fillSearchPickerField, isTimePeriodField, isTimeSingleField, fillTimePeriodField, fillTimeSingleField } from "~lib/autofill" import { extractDomStructure, detectPageLanguage, isJobApplicationForm } from "~lib/dom" import { matchFormFields, matchFormFieldsInRange, matchMainFields } from "~lib/formMatcher" import { detectPickerField } from "~lib/pickerDetector" import { detectAndUploadResume } from "~lib/resumeUpload" -import { getMockResumeData } from "~lib/constants" +import { getMockResumeData2 } from "~lib/constants" import { getResumeFieldValue } from "~lib/resumeDataHelper" import { locateExperienceSections, expandExperienceSections, sortExperienceByTime, relocateSegmentContainer } from "~lib/experienceSection" -import type { MatchedFormField, ResumeData, ExperienceSection } from "~lib/types" +import type { MatchedFormField, ResumeData, ExperienceSection, JobInfo } from "~lib/types" import "./SidebarPanel.scss" /** 侧边栏面板的 Props */ interface SidebarPanelProps { + /** 当前标签页的来源链接 */ + sourceUrl: string + /** 岗位信息(由 sidebar 层查询后传入) */ + jobInfo: JobInfo | null /** 关闭面板的回调函数 */ onClose: () => void } -export function SidebarPanel({ onClose }: SidebarPanelProps) { +export function SidebarPanel({ sourceUrl, jobInfo, onClose }: SidebarPanelProps) { + /** 是否已登录(有 Token) */ + const [isLoggedIn, setIsLoggedIn] = useState(null) /** 是否正在执行自动填写 */ const [filling, setFilling] = useState(false) /** 页面语言类型:中文 / 英文 */ @@ -34,6 +42,33 @@ export function SidebarPanel({ onClose }: SidebarPanelProps) { /** 当前使用的简历数据 */ const [resumeData, setResumeData] = useState(null) + /** 页面加载时检查 Token,有岗位信息则查询定制简历 */ + useEffect(() => { + getCookieValue("Token").then((token) => { + console.log("[OfferPie] SidebarPanel 获取到的 Token:", token) + setIsLoggedIn(!!token) + + // 有 Token 且有岗位 ID 时,查询定制简历 + if (token && jobInfo?.id) { + getCustomizeResume(jobInfo.id).then((resumeRes: any) => { + console.log("[OfferPie] 定制简历数据:", resumeRes) + // 接口返回的 resume 字段映射为 main + const mappedData: ResumeData = { + main: resumeRes.resume || {}, + education: resumeRes.education || [], + work: resumeRes.work || [], + internship: resumeRes.internship || [], + project: resumeRes.project || [], + competition: resumeRes.competition || [], + } + setResumeData(mappedData) + }).catch((err) => { + console.warn("[OfferPie] 查询定制简历失败:", err) + }) + } + }) + }, []) + /** * 自动填写按钮点击处理 * 流程:提取 DOM → 检测语言 → 判断是否表单页 → 检测简历上传 → 匹配字段 → 识别选择器 → 填充测试数据 @@ -58,17 +93,16 @@ export function SidebarPanel({ onClose }: SidebarPanelProps) { console.log(`===== OfferPie: 是否为职位申请表单页面 = ${isForm} =====`) if (isForm) { - // 4. 获取简历数据(当前使用 mock 数据,后续替换为接口调用) - // TODO: 替换为真实接口 → const res = await javaApi.get<{resume: ..., education: ..., ...}>("/resume/detail") - const currentResumeData = getMockResumeData() - setResumeData(currentResumeData) + // 4. 获取简历数据(优先使用接口数据,无接口数据时 fallback 到 mock) + const currentResumeData = resumeData || getMockResumeData2() + if (!resumeData) setResumeData(currentResumeData) console.log(`===== OfferPie: 已加载简历数据,教育${currentResumeData.education.length}段 工作${currentResumeData.work.length}段 实习${currentResumeData.internship.length}段 项目${currentResumeData.project.length}段 竞赛${currentResumeData.competition.length}段 =====`) - // // 4.1 检测并上传简历文件 - // const resumeUrl = "https://offerpie.oss-cn-guangzhou.aliyuncs.com/%E4%BA%8E%E5%A4%A7%E6%98%A5.pdf" - // const uploaded = await detectAndUploadResume(resumeUrl) - // console.log(`===== OfferPie: 简历上传 ${uploaded ? "成功" : "跳过(未找到上传按钮或失败)"} =====`) - // if (uploaded) await delay(1000) // 等待网站解析简历 + // 4.1 检测并上传简历文件 + const resumeUrl = "https://offerpie.oss-cn-guangzhou.aliyuncs.com/%E4%BA%8E%E5%A4%A7%E6%98%A5.pdf" + const uploaded = await detectAndUploadResume(resumeUrl) + console.log(`===== OfferPie: 简历上传 ${uploaded ? "成功" : "跳过(未找到上传按钮或失败)"} =====`) + if (uploaded) await delay(1000) // 等待网站解析简历 // 4.5 定位经历区块并统计已展开段数 const sectionResults = locateExperienceSections(document.body, lang) @@ -307,11 +341,10 @@ export function SidebarPanel({ onClose }: SidebarPanelProps) { return (
- {/* 顶部操作栏:反馈、设置、关闭按钮 */} + {/* 顶部操作栏:关闭按钮始终显示,反馈和设置仅登录后显示 */}
- 反馈123456789 - 设置 - {/* 关闭按钮:点击后隐藏侧边栏 */} + {isLoggedIn && 反馈12} + {isLoggedIn && 设置}
- {/* 职位名称和公司信息 */} -
-
数据产品经理
-
北京 · 字节跳动 独角兽
-
- {/* 匹配度环形进度条 */} -
- - {/* 背景圆环 */} - - {/* 进度圆环:60% 匹配度 */} - - {/* 百分比文字 */} - 60% - -
-
+ )} - {/* 自动填写按钮:点击后获取页面结构,后续会调用 AI 接口自动填表 */} - + {/* 已登录状态:显示完整功能 */} + {isLoggedIn && ( + <> + {/* 职位信息卡片 */} +
+
+ {jobInfo?.companyLogoUrl ? ( + logo + ) : ( + + + + + + + )} +
+
+
{jobInfo?.title || "未匹配到岗位信息"}
+
+ {jobInfo + ? [jobInfo.regionName, jobInfo.companyShortName || jobInfo.companyName, jobInfo.companyType] + .filter(Boolean) + .join(" · ") || "—" + : "当前页面暂未关联岗位"} +
+
+ {jobInfo?.matchScore != null && ( +
+ + + + + {jobInfo.matchScore}% + + +
+ )} +
- {/* 使用次数信息 */} -
- 剩余4次 - 立即获得无限次数 -
+ {/* 自动填写按钮 */} + - {/* 简历区域 */} -
简历
- {/* 简历卡片:展示当前选中的简历信息 */} -
- {/* 简历头像 */} -
B
- {/* 简历名称和标签 */} -
- 李华-产品经理 - 默认 - 竞争力分析 -
- {/* 更改简历按钮 */} - 更改 -
+ {/* 使用次数信息 */} +
+ 剩余4次 + 立即获得无限次数 +
- {/* 针对性优化简历按钮 */} - + {/* 简历区域 */} +
简历
+
+
+ {resumeData?.main?.name ? resumeData.main.name.charAt(0) : "—"} +
+
+ + {resumeData?.main?.name || "暂无简历"} + + {resumeData && 默认} +
+ {/* 更改 */} +
- {/* 填写进度区域 */} -
- 填写进度 - 50% -
- {/* 进度条 */} -
-
-
+ {/* 针对性优化简历按钮 */} + + + {/* 填写进度区域 */} +
+ 填写进度 + 50% +
+
+
+
+ + )}
) } diff --git a/src/config.ts b/src/config.ts index b986c9a..050b364 100644 --- a/src/config.ts +++ b/src/config.ts @@ -13,6 +13,7 @@ const envConfigs: Record = { dev: { dataBaseApi: 'http://localhost:8080/api', + // aiBaseApi: 'http://192.168.31.133:8000', aiBaseApi: 'http://localhost:8000', cookieSourceUrl: 'http://192.168.31.135:5173', }, diff --git a/src/contents/sidebar.tsx b/src/contents/sidebar.tsx index 47cee5f..de868db 100644 --- a/src/contents/sidebar.tsx +++ b/src/contents/sidebar.tsx @@ -6,8 +6,11 @@ import styleText from "data-text:~components/SidebarPanel.scss" import type { PlasmoCSConfig, PlasmoGetStyle } from "plasmo" -import { useEffect, useState } from "react" +import { useEffect, useState, useRef } from "react" import { SidebarPanel } from "~components/SidebarPanel" +import { getCookieValue } from "~utils/cookie" +import { findJobBySourceUrl } from "~api/dataApi" +import type { JobInfo } from "~lib/types" /** Content Script 配置:匹配所有网页 */ export const config: PlasmoCSConfig = { @@ -31,8 +34,38 @@ export const getStyle: PlasmoGetStyle = () => { function Sidebar() { /** 侧边栏是否可见 */ const [visible, setVisible] = useState(false) + /** 当前标签页的来源链接 */ + const [sourceUrl, setSourceUrl] = useState("") + /** 岗位信息(在 sidebar 层查询,传给 SidebarPanel) */ + const [jobInfo, setJobInfo] = useState(null) + /** 是否已自动打开过面板(避免重复打开) */ + const autoOpenedRef = useRef(false) useEffect(() => { + // 页面打开时获取 Token 和当前页面 URL 作为 sourceUrl + const currentUrl = window.location.href + setSourceUrl(currentUrl) + console.log("[OfferPie] 当前页面 sourceUrl:", currentUrl) + + getCookieValue("Token").then((token) => { + console.log("[OfferPie] 页面加载时获取到的 Token:", token) + if (!token) return + + // 有 token 时,先查询岗位信息,有岗位才自动打开面板 + findJobBySourceUrl(currentUrl).then((data) => { + console.log("[OfferPie] 岗位信息:", data) + if (data && data.id) { + setJobInfo(data) + if (!autoOpenedRef.current) { + autoOpenedRef.current = true + setVisible(true) + } + } + }).catch((err) => { + console.warn("[OfferPie] 查询岗位信息失败:", err) + }) + }) + /** * 消息监听器:接收来自 background 或 popup 的消息 * 当收到 TOGGLE_SIDEBAR 消息时,切换侧边栏的显示状态 @@ -40,6 +73,9 @@ function Sidebar() { const handler = (message: any) => { if (message.type === "TOGGLE_SIDEBAR") { setVisible((prev) => !prev) + getCookieValue("Token").then((token) => { + console.log("[OfferPie] 点击插件按钮时获取到的 Token:", token) + }) } } chrome.runtime.onMessage.addListener(handler) @@ -50,7 +86,7 @@ function Sidebar() { // 不可见时不渲染任何内容 if (!visible) return null - return setVisible(false)} /> + return setVisible(false)} /> } export default Sidebar diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 3e1ff46..f74e4dc 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -268,10 +268,10 @@ export const EXPERIENCE_SECTION_CONFIGS: ExperienceSectionConfig[] = [ // ============ Mock 简历数据(模拟接口返回) ============ /** - * 模拟后端接口返回的完整简历数据 - * 后续会替换为真实接口调用:javaApi.get("/resume/detail") + * 模拟后端接口返回的完整简历数据(测试用) + * 已被接口替代,保留用于离线调试 */ -export function getMockResumeData(): ResumeData { +export function getMockResumeData2(): ResumeData { return { main: { avatarUrl: "", diff --git a/src/lib/types.ts b/src/lib/types.ts index c75c784..e0369a6 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -3,6 +3,54 @@ * 包含简历数据接口、表单标签接口、匹配字段接口、UI组件库配置接口等所有共享类型 */ +// ============ 岗位信息类型 ============ + +/** 岗位匹配度详情 */ +export interface JobMatchScoreDto { + /** 教育得分(0-100) */ + educationScore?: number + /** 技能得分(0-100) */ + skillScore?: number + /** 经验得分(0-100) */ + experienceScore?: number +} + +/** 岗位信息 */ +export interface JobInfo { + /** 岗位ID */ + id?: number + /** 岗位名称 */ + title?: string + /** 薪资描述 */ + salary?: string + /** 公司名称 */ + companyName?: string + /** 公司简称 */ + companyShortName?: string + /** 公司类型 */ + companyType?: string + /** 公司Logo */ + companyLogoUrl?: string + /** 地区名称 */ + regionName?: string + /** 岗位类型名称 */ + categoryName?: string + /** 岗位标签 */ + tags?: string[] + /** 来源链接 */ + sourceUrl?: string + /** 是否收藏 */ + isFavorite?: boolean + /** 投递状态(null=未投递,0=已投递 1=面试中 2=有Offer 3=未通过 4=已结束) */ + applicationStatus?: number | null + /** 岗位状态(0=有效 1=已下架 2=已过期) */ + status?: number + /** 匹配总分(0-90) */ + matchScore?: number + /** 匹配度详情 */ + matchDetail?: JobMatchScoreDto +} + // ============ 简历数据类型 ============ /** 描述段落(富文本段落) */