登陆信息同步和接口联调

This commit is contained in:
2026-05-14 10:47:51 +08:00
parent 84a2a3993a
commit c86c570896
8 changed files with 272 additions and 87 deletions
+141 -81
View File
@@ -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<boolean | null>(null)
/** 是否正在执行自动填写 */
const [filling, setFilling] = useState(false)
/** 页面语言类型:中文 / 英文 */
@@ -34,6 +42,33 @@ export function SidebarPanel({ onClose }: SidebarPanelProps) {
/** 当前使用的简历数据 */
const [resumeData, setResumeData] = useState<ResumeData | null>(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 (
<div className="op-container">
{/* 顶部操作栏:反馈、设置、关闭按钮 */}
{/* 顶部操作栏:关闭按钮始终显示,反馈和设置仅登录后显示 */}
<div className="op-header">
<span className="op-header-link">123456789</span>
<span className="op-header-link"></span>
{/* 关闭按钮:点击后隐藏侧边栏 */}
{isLoggedIn && <span className="op-header-link">12</span>}
{isLoggedIn && <span className="op-header-link"></span>}
<button className="op-close-btn" onClick={onClose}>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round">
<circle cx="12" cy="12" r="10" />
@@ -323,76 +356,103 @@ export function SidebarPanel({ onClose }: SidebarPanelProps) {
{/* 插件标题 */}
<div className="op-title"></div>
{/* 职位信息卡片:展示当前页面识别到的职位信息和匹配度 */}
<div className="op-job-card">
{/* 职位图标 */}
<div className="op-job-icon">
<svg width="20" height="20" viewBox="0 0 24 24" fill="#666">
<rect x="3" y="3" width="7" height="7" rx="1" />
<rect x="14" y="3" width="7" height="7" rx="1" />
<rect x="3" y="14" width="7" height="7" rx="1" />
<rect x="14" y="14" width="7" height="7" rx="1" />
</svg>
{/* 未登录状态:显示登录提示 */}
{!isLoggedIn && isLoggedIn !== null && (
<div className="op-login-prompt">
<p className="op-login-text"> Offer派 </p>
<button
className="op-login-btn"
onClick={() => {
onClose()
window.open("http://localhost:5173/jobs", "_blank")
}}
>
Offer派
</button>
</div>
{/* 职位名称和公司信息 */}
<div className="op-job-info">
<div className="op-job-title"></div>
<div className="op-job-meta"> ·  </div>
</div>
{/* 匹配度环形进度条 */}
<div className="op-match-score">
<svg width="48" height="48" viewBox="0 0 48 48">
{/* 背景圆环 */}
<circle cx="24" cy="24" r="20" fill="none" stroke="#f0f0f0" strokeWidth="3" />
{/* 进度圆环:60% 匹配度 */}
<circle cx="24" cy="24" r="20" fill="none" stroke="#000" strokeWidth="3"
strokeDasharray={`${0.6 * 2 * Math.PI * 20} ${2 * Math.PI * 20}`}
strokeLinecap="round" transform="rotate(-90 24 24)" />
{/* 百分比文字 */}
<text x="24" y="26" textAnchor="middle" fontSize="13" fontWeight="700" fill="#000">60%</text>
</svg>
</div>
</div>
)}
{/* 自动填写按钮:点击后获取页面结构,后续会调用 AI 接口自动填表 */}
<button className="op-autofill-btn" onClick={handleAutoFill} disabled={filling}>
{filling ? "分析中..." : "自动填写"}
</button>
{/* 已登录状态:显示完整功能 */}
{isLoggedIn && (
<>
{/* 职位信息卡片 */}
<div className="op-job-card">
<div className="op-job-icon">
{jobInfo?.companyLogoUrl ? (
<img src={jobInfo.companyLogoUrl} alt="logo" style={{ width: 40, height: 40, borderRadius: 10, objectFit: "cover" }} />
) : (
<svg width="20" height="20" viewBox="0 0 24 24" fill="#666">
<rect x="3" y="3" width="7" height="7" rx="1" />
<rect x="14" y="3" width="7" height="7" rx="1" />
<rect x="3" y="14" width="7" height="7" rx="1" />
<rect x="14" y="14" width="7" height="7" rx="1" />
</svg>
)}
</div>
<div className="op-job-info">
<div className="op-job-title">{jobInfo?.title || "未匹配到岗位信息"}</div>
<div className="op-job-meta">
{jobInfo
? [jobInfo.regionName, jobInfo.companyShortName || jobInfo.companyName, jobInfo.companyType]
.filter(Boolean)
.join(" · ") || "—"
: "当前页面暂未关联岗位"}
</div>
</div>
{jobInfo?.matchScore != null && (
<div className="op-match-score">
<svg width="48" height="48" viewBox="0 0 48 48">
<circle cx="24" cy="24" r="20" fill="none" stroke="#f0f0f0" strokeWidth="3" />
<circle cx="24" cy="24" r="20" fill="none" stroke="#000" strokeWidth="3"
strokeDasharray={`${(jobInfo.matchScore) / 100 * 2 * Math.PI * 20} ${2 * Math.PI * 20}`}
strokeLinecap="round" transform="rotate(-90 24 24)" />
<text x="24" y="26" textAnchor="middle" fontSize="13" fontWeight="700" fill="#000">
{jobInfo.matchScore}%
</text>
</svg>
</div>
)}
</div>
{/* 使用次数信息 */}
<div className="op-credits-row">
<span className="op-credits-text">4</span>
<span className="op-credits-link"></span>
</div>
{/* 自动填写按钮 */}
<button className="op-autofill-btn" onClick={handleAutoFill} disabled={filling}>
{filling ? "分析中..." : "自动填写"}
</button>
{/* 简历区域 */}
<div className="op-section-label"></div>
{/* 简历卡片:展示当前选中的简历信息 */}
<div className="op-resume-card">
{/* 简历头像 */}
<div className="op-resume-avatar">B</div>
{/* 简历名称和标签 */}
<div className="op-resume-info">
<span className="op-resume-name">-</span>
<span className="op-resume-tag"></span>
<span className="op-resume-tag"></span>
</div>
{/* 更改简历按钮 */}
<span className="op-resume-change"></span>
</div>
{/* 使用次数信息 */}
<div className="op-credits-row">
<span className="op-credits-text">4</span>
<span className="op-credits-link"></span>
</div>
{/* 针对性优化简历按钮 */}
<button className="op-optimize-btn"></button>
{/* 简历区域 */}
<div className="op-section-label"></div>
<div className="op-resume-card">
<div className="op-resume-avatar">
{resumeData?.main?.name ? resumeData.main.name.charAt(0) : "—"}
</div>
<div className="op-resume-info">
<span className="op-resume-name">
{resumeData?.main?.name || "暂无简历"}
</span>
{resumeData && <span className="op-resume-tag"></span>}
</div>
{/* <span className="op-resume-change">更改</span> */}
</div>
{/* 填写进度区域 */}
<div className="op-progress-row">
<span className="op-progress-label"></span>
<span className="op-progress-value">50%</span>
</div>
{/* 进度条 */}
<div className="op-progress-bar-bg">
<div className="op-progress-bar-fill" />
</div>
{/* 针对性优化简历按钮 */}
<button className="op-optimize-btn"></button>
{/* 填写进度区域 */}
<div className="op-progress-row">
<span className="op-progress-label"></span>
<span className="op-progress-value">50%</span>
</div>
<div className="op-progress-bar-bg">
<div className="op-progress-bar-fill" />
</div>
</>
)}
</div>
)
}