Files
offerpai_browser_plug/src/components/SidebarPanel.tsx
T

203 lines
8.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 侧边栏面板组件
* 插件的主操作界面,固定在浏览器右上角
* 包含:职位信息卡片、自动填写按钮、简历管理、填写进度等功能区域
*/
import { useState, useEffect } from "react"
import { getCookieValue } from "~utils/cookie"
import { getCustomizeResume } from "~api/aiApi"
import { handleAutoFillCommon } from "~handlers/handleAutoFillCommon"
import type { MatchedFormField, ResumeData, JobInfo } from "~lib/types"
import "./SidebarPanel.scss"
/** 侧边栏面板的 Props */
interface SidebarPanelProps {
/** 当前标签页的来源链接 */
sourceUrl: string
/** 岗位信息(由 sidebar 层查询后传入) */
jobInfo: JobInfo | null
/** 关闭面板的回调函数 */
onClose: () => void
}
export function SidebarPanel({ sourceUrl, jobInfo, onClose }: SidebarPanelProps) {
/** 是否已登录(有 Token */
const [isLoggedIn, setIsLoggedIn] = useState<boolean | null>(null)
/** 是否正在执行自动填写 */
const [filling, setFilling] = useState(false)
/** 页面语言类型:中文 / 英文 */
const [pageLang, setPageLang] = useState<"zh" | "en">("zh")
/** 是否为职位申请表单页面 */
const [isFormPage, setIsFormPage] = useState(false)
/** 匹配到的表单字段列表 */
const [formFields, setFormFields] = useState<MatchedFormField[]>([])
/** 当前使用的简历数据 */
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)
})
}
})
}, [jobInfo?.id])
/**
* 自动填写按钮点击处理
* 内部根据条件判断走 handlers/ 下具体哪个处理文件:
* - handleAutoFillCommon:通用模式(当前默认)
* - 后续特殊网站会在此处加条件分支(如根据 domain 或 jobInfo 来源判断)
*/
const handleAutoFill = async () => {
setFilling(true)
try {
const fillResult = await handleAutoFillCommon({ resumeData, jobInfo })
setPageLang(fillResult.lang)
setIsFormPage(fillResult.isFormPage)
if (fillResult.resumeData) setResumeData(fillResult.resumeData)
setFormFields(fillResult.formFields)
} catch (e) {
console.error("OfferPie: 自动填写异常", e)
}
setTimeout(() => setFilling(false), 1000)
}
return (
<div className="op-container">
{/* 顶部操作栏:关闭按钮始终显示,反馈和设置仅登录后显示 */}
<div className="op-header">
{isLoggedIn && <span className="op-header-link">反馈</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" />
<path d="M9 12h6M12 9l3 3-3 3" />
</svg>
</button>
</div>
{/* 插件标题 */}
<div className="op-title">大学生求职助手</div>
{/* 未登录状态:显示登录提示 */}
{!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>
)}
{/* 已登录状态:显示完整功能 */}
{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>
{/* 自动填写按钮 */}
<button className="op-autofill-btn" onClick={handleAutoFill} disabled={filling}>
{filling ? "分析中..." : "自动填写"}
</button>
{/* 使用次数信息 */}
<div className="op-credits-row">
<span className="op-credits-text">剩余4</span>
<span className="op-credits-link">立即获得无限次数</span>
</div>
{/* 简历区域 */}
<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>
{/* 针对性优化简历按钮 */}
<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>
)
}