203 lines
8.0 KiB
TypeScript
203 lines
8.0 KiB
TypeScript
/**
|
||
* 侧边栏面板组件
|
||
* 插件的主操作界面,固定在浏览器右上角
|
||
* 包含:职位信息卡片、自动填写按钮、简历管理、填写进度等功能区域
|
||
*/
|
||
|
||
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>
|
||
)
|
||
}
|
||
|