通用模式兼容摩卡和飞书,添加通信工具组件,添加未开通会员面板
This commit is contained in:
+279
-101
@@ -6,15 +6,27 @@
|
||||
|
||||
import { useState, useEffect, useRef, useCallback } from "react"
|
||||
import { getCookieValue } from "~utils/cookie"
|
||||
import { get as storageGet, set as storageSet, remove as storageRemove } from "~utils/storage"
|
||||
import { getCustomizeResume } from "~api/aiApi"
|
||||
import { getMemberStatus } from "~api/dataApi"
|
||||
import { handleAutoFillCommon } from "~handlers/handleAutoFillCommon"
|
||||
import { handleAutoFillBeisen } from "~handlers/handleAutoFillBeisen"
|
||||
import { scanPageFields, extractNonResumeFields, printFieldStats } from "~lib/fillStats"
|
||||
import type { TitleStat } from "~lib/fillStats"
|
||||
import type { MatchedFormField, ResumeData, JobInfo } from "~lib/types"
|
||||
import { createChannelBridge } from "~lib/channelBridge"
|
||||
import logoImg from "data-base64:~/../assets/logo-offerpai.png"
|
||||
import { config as appConfig } from "~config"
|
||||
import "./SidebarPanel.scss"
|
||||
|
||||
/** 投递链接列表数据类型(跨域通信用) */
|
||||
interface OfferpaieDeliveryLinkListData {
|
||||
linkList: string[]
|
||||
}
|
||||
|
||||
/** 创建通信桥接实例(用于投递链接列表的跨域读写) */
|
||||
const panelBridge = createChannelBridge({ domain: appConfig.bridgeDomain, isPlugin: true })
|
||||
|
||||
/** 侧边栏面板的 Props */
|
||||
interface SidebarPanelProps {
|
||||
/** 当前标签页的来源链接 */
|
||||
@@ -28,6 +40,8 @@ interface SidebarPanelProps {
|
||||
export function SidebarPanel({ sourceUrl, jobInfo, onClose }: SidebarPanelProps) {
|
||||
/** 是否已登录(有 Token) */
|
||||
const [isLoggedIn, setIsLoggedIn] = useState<boolean | null>(null)
|
||||
/** 是否为会员(null=未检测,true=会员,false=非会员) */
|
||||
const [isMember, setIsMember] = useState<boolean | null>(null)
|
||||
/** 是否正在执行自动填写 */
|
||||
const [filling, setFilling] = useState(false)
|
||||
/** 页面语言类型:中文 / 英文 */
|
||||
@@ -43,7 +57,7 @@ export function SidebarPanel({ sourceUrl, jobInfo, onClose }: SidebarPanelProps)
|
||||
/** 页面字段扫描统计结果(用于面板底部展示已填/待填字段列表) */
|
||||
const [fieldStats, setFieldStats] = useState<TitleStat[]>([])
|
||||
/** 顶部状态文字 */
|
||||
const [headerStatus, setHeaderStatus] = useState<string>("")
|
||||
const [headerStatus, setHeaderStatus] = useState<string>("准备中, 请登录到投递表单页~")
|
||||
|
||||
/** 是否启用字段更新时自动滚动到该字段,模拟跟踪(常量开关,方便调试关闭) */
|
||||
const ENABLE_AUTO_SCROLL_TO_UPDATED = true
|
||||
@@ -139,11 +153,12 @@ export function SidebarPanel({ sourceUrl, jobInfo, onClose }: SidebarPanelProps)
|
||||
return () => { stopScanLoop() }
|
||||
}, [])
|
||||
|
||||
/** 页面加载时检测是否有足够的输入框,显示"准备就绪" */
|
||||
/** 页面加载 & SPA 路由变化时检测是否有足够的输入框,显示"准备就绪" */
|
||||
useEffect(() => {
|
||||
const hostDoc = document.getRootNode() === document ? document : (document.getRootNode() as ShadowRoot).ownerDocument
|
||||
|
||||
/** 检测页面是否有足够的表单输入框 */
|
||||
const checkInputs = () => {
|
||||
// 注意:需要查询宿主页面的 DOM,而不是 Shadow DOM 内部
|
||||
const hostDoc = document.getRootNode() === document ? document : (document.getRootNode() as ShadowRoot).ownerDocument
|
||||
const inputs = hostDoc.querySelectorAll("input:not([type='hidden']):not([type='submit']):not([type='button']):not([type='file']), textarea")
|
||||
if (inputs.length > 2) {
|
||||
setHeaderStatus("准备就绪,请点击开始填写")
|
||||
@@ -151,16 +166,35 @@ export function SidebarPanel({ sourceUrl, jobInfo, onClose }: SidebarPanelProps)
|
||||
}
|
||||
return false
|
||||
}
|
||||
// 立即检测一次
|
||||
if (checkInputs()) return
|
||||
// 页面可能还在加载,延迟重试几次
|
||||
const timer1 = setTimeout(checkInputs, 500)
|
||||
const timer2 = setTimeout(checkInputs, 1500)
|
||||
const timer3 = setTimeout(checkInputs, 3000)
|
||||
|
||||
/** 路由变化后延迟多次重试检测(等待新页面 DOM 渲染) */
|
||||
const timers: ReturnType<typeof setTimeout>[] = []
|
||||
const scheduleCheck = () => {
|
||||
setHeaderStatus("准备中, 请登录到投递表单页~")
|
||||
if (checkInputs()) return
|
||||
timers.push(setTimeout(checkInputs, 500))
|
||||
timers.push(setTimeout(checkInputs, 1500))
|
||||
timers.push(setTimeout(checkInputs, 3000))
|
||||
timers.push(setTimeout(checkInputs, 5000))
|
||||
}
|
||||
|
||||
// 初次挂载时检测
|
||||
scheduleCheck()
|
||||
|
||||
// URL 轮询:每秒检查一次 URL 是否变化(兼容所有 SPA 路由方式)
|
||||
let lastUrl = window.location.href
|
||||
const urlPollInterval = setInterval(() => {
|
||||
const currentUrl = window.location.href
|
||||
if (currentUrl !== lastUrl) {
|
||||
lastUrl = currentUrl
|
||||
console.log("[OfferPie] 检测到 URL 变化:", currentUrl)
|
||||
scheduleCheck()
|
||||
}
|
||||
}, 1000)
|
||||
|
||||
return () => {
|
||||
clearTimeout(timer1)
|
||||
clearTimeout(timer2)
|
||||
clearTimeout(timer3)
|
||||
clearInterval(urlPollInterval)
|
||||
timers.forEach(clearTimeout)
|
||||
}
|
||||
}, [])
|
||||
|
||||
@@ -203,14 +237,32 @@ export function SidebarPanel({ sourceUrl, jobInfo, onClose }: SidebarPanelProps)
|
||||
setLastUpdatedFieldEl(null)
|
||||
}, [lastUpdatedFieldEl])
|
||||
|
||||
/** 页面加载时检查 Token,有岗位信息则查询定制简历 */
|
||||
/** 页面加载时检查 Token,有岗位信息则查询定制简历,并检查会员状态 */
|
||||
useEffect(() => {
|
||||
getCookieValue("Token").then((token) => {
|
||||
console.log("[OfferPie] SidebarPanel 获取到的 Token:", token)
|
||||
setIsLoggedIn(!!token)
|
||||
|
||||
// 有 Token 且有岗位 ID 时,查询定制简历
|
||||
if (token && jobInfo?.id) {
|
||||
if (!token) {
|
||||
setHeaderStatus("请登录后继续使用")
|
||||
return
|
||||
}
|
||||
|
||||
// 查询会员状态
|
||||
getMemberStatus().then((memberData) => {
|
||||
console.log("[OfferPie] 会员状态:", memberData)
|
||||
setIsMember(memberData.isMember)
|
||||
if (!memberData.isMember) {
|
||||
setHeaderStatus("请开通会员后继续使用")
|
||||
}
|
||||
}).catch((err) => {
|
||||
console.warn("[OfferPie] 查询会员状态失败:", err)
|
||||
setIsMember(false)
|
||||
setHeaderStatus("请开通会员后继续使用")
|
||||
})
|
||||
|
||||
// 有岗位 ID 时,查询定制简历
|
||||
if (jobInfo?.id) {
|
||||
getCustomizeResume(jobInfo.id).then((resumeRes: any) => {
|
||||
console.log("[OfferPie] 定制简历数据:", resumeRes)
|
||||
// 接口返回的 resume 字段映射为 main
|
||||
@@ -355,6 +407,26 @@ export function SidebarPanel({ sourceUrl, jobInfo, onClose }: SidebarPanelProps)
|
||||
* 8. matchMainFields 匹配非经历区域字段(姓名/手机/邮箱等)并填充
|
||||
* 9. 收集剩余未匹配的空白字段(unmatchedFields),后续交给 AI 生成答案
|
||||
*
|
||||
/** 重置按钮:恢复到开始填写状态,重新检测页面 */
|
||||
const handleReset = () => {
|
||||
setFillCompleted(false)
|
||||
// 重新检测页面输入框状态
|
||||
setHeaderStatus("准备中, 请登录到投递表单页~")
|
||||
const hostDoc = document.getRootNode() === document ? document : (document.getRootNode() as ShadowRoot).ownerDocument
|
||||
const inputs = hostDoc.querySelectorAll("input:not([type='hidden']):not([type='submit']):not([type='button']):not([type='file']), textarea")
|
||||
if (inputs.length > 2) {
|
||||
setHeaderStatus("准备就绪,请点击开始填写")
|
||||
} else {
|
||||
// 延迟重试
|
||||
setTimeout(() => {
|
||||
const inputs2 = hostDoc.querySelectorAll("input:not([type='hidden']):not([type='submit']):not([type='button']):not([type='file']), textarea")
|
||||
if (inputs2.length > 2) setHeaderStatus("准备就绪,请点击开始填写")
|
||||
}, 1000)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 自动填写主流程
|
||||
* 填写完成后更新组件状态,供后续"保存用户修改"和"AI补填"使用
|
||||
*/
|
||||
const handleAutoFill = async () => {
|
||||
@@ -404,14 +476,12 @@ export function SidebarPanel({ sourceUrl, jobInfo, onClose }: SidebarPanelProps)
|
||||
* 模拟提交:通过 fillStats 扫描页面字段,过滤出 B2 阶段字段数据,更新到缓存
|
||||
* 【注意】弹窗打开时不执行,避免覆盖用户正在编辑的数据
|
||||
*/
|
||||
const handleSaveUserInput = () => {
|
||||
const handleSaveUserInput = async () => {
|
||||
// 弹窗打开时直接跳过,不更新缓存
|
||||
if (showCacheModal) return
|
||||
try {
|
||||
const cached = localStorage.getItem("offerpie_unfilled_form")
|
||||
if (!cached) { alert("未找到缓存数据,请先执行自动填写"); return }
|
||||
|
||||
const cacheData = JSON.parse(cached)
|
||||
const cacheData = await storageGet<any>("offerpie_unfilled_form")
|
||||
if (!cacheData) { return }
|
||||
|
||||
// 检测当前网站模式
|
||||
const currentHost = window.location.hostname
|
||||
@@ -424,81 +494,72 @@ export function SidebarPanel({ sourceUrl, jobInfo, onClose }: SidebarPanelProps)
|
||||
const nonResumeData = extractNonResumeFields(titleStats)
|
||||
|
||||
// 更新缓存中的 unfilledFormData
|
||||
// 策略:新数据中有的字段用新值替换,新数据中没有但旧数据中有的字段保留
|
||||
const oldSections = cacheData.unfilledFormData as any[] || []
|
||||
for (const newSec of nonResumeData) {
|
||||
const oldSec = oldSections.find((s: any) => s.title === newSec.title)
|
||||
if (!oldSec) {
|
||||
const oldSecIdx = oldSections.findIndex((s: any) => s.title === newSec.title)
|
||||
if (oldSecIdx < 0) {
|
||||
oldSections.push(newSec)
|
||||
continue
|
||||
}
|
||||
|
||||
if (newSec.isExperience) {
|
||||
const oldSegments = oldSec.formItems as any[][]
|
||||
const newSegments = newSec.formItems as any[][]
|
||||
for (let sIdx = 0; sIdx < newSegments.length; sIdx++) {
|
||||
if (sIdx >= oldSegments.length) {
|
||||
oldSegments.push(newSegments[sIdx])
|
||||
continue
|
||||
}
|
||||
const oldFields = oldSegments[sIdx]
|
||||
const newFields = newSegments[sIdx]
|
||||
for (const newField of newFields) {
|
||||
const oldField = oldFields.find((f: any) => f.label === newField.label)
|
||||
if (oldField) {
|
||||
if (newField.value) oldField.value = newField.value
|
||||
} else {
|
||||
const oldSec = oldSections[oldSecIdx]
|
||||
if (newSec.isExperience && oldSec.isExperience) {
|
||||
// 经历类型:按段合并,保留旧字段
|
||||
const oldSegments = oldSec.formItems as any[][]
|
||||
const newSegments = newSec.formItems as any[][]
|
||||
for (let sIdx = 0; sIdx < newSegments.length; sIdx++) {
|
||||
if (sIdx >= oldSegments.length) {
|
||||
oldSegments.push(newSegments[sIdx])
|
||||
} else {
|
||||
oldFields.push(newField)
|
||||
// 新字段更新值,旧字段中有但新字段中没有的保留
|
||||
const oldFields = oldSegments[sIdx]
|
||||
const newFields = newSegments[sIdx]
|
||||
for (const nf of newFields) {
|
||||
const of_ = oldFields.find((f: any) => f.label === nf.label)
|
||||
if (of_) {
|
||||
if (nf.value) of_.value = nf.value
|
||||
} else {
|
||||
oldFields.push(nf)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const oldFields = oldSec.formItems as any[]
|
||||
const newFields = newSec.formItems as any[]
|
||||
for (const newField of newFields) {
|
||||
const oldField = oldFields.find((f: any) => f.label === newField.label)
|
||||
if (oldField) {
|
||||
if (newField.value) oldField.value = newField.value
|
||||
} else {
|
||||
oldFields.push(newField)
|
||||
} else if (!newSec.isExperience && !oldSec.isExperience) {
|
||||
// 非经历类型:按字段合并,保留旧字段
|
||||
const oldFields = oldSec.formItems as any[]
|
||||
const newFields = newSec.formItems as any[]
|
||||
for (const nf of newFields) {
|
||||
const of_ = oldFields.find((f: any) => f.label === nf.label)
|
||||
if (of_) {
|
||||
if (nf.value) of_.value = nf.value
|
||||
} else {
|
||||
oldFields.push(nf)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 类型变化,直接替换
|
||||
oldSections[oldSecIdx] = newSec
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cacheData.unfilledFormData = oldSections
|
||||
cacheData.timestamp = Date.now()
|
||||
localStorage.setItem("offerpie_unfilled_form", JSON.stringify(cacheData))
|
||||
await storageSet("offerpie_unfilled_form", cacheData)
|
||||
|
||||
// 打印所有字段数据
|
||||
// console.log("===== OfferPie: handleSaveUserInput 全量字段数据 =====")
|
||||
printFieldStats(titleStats)
|
||||
|
||||
// 打印 B2 字段数据
|
||||
// console.log("===== OfferPie: handleSaveUserInput B2阶段字段数据 =====")
|
||||
// console.log(JSON.stringify(nonResumeData, null, 2))
|
||||
// console.log("===== OfferPie: B2字段数据打印完毕 =====")
|
||||
|
||||
const updatedCount = nonResumeData.reduce((sum, sec) => {
|
||||
if (sec.isExperience) {
|
||||
return sum + (sec.formItems as any[][]).reduce((s, seg) => s + seg.filter((f: any) => f.value).length, 0)
|
||||
}
|
||||
return sum + (sec.formItems as any[]).filter((f: any) => f.value).length
|
||||
}, 0)
|
||||
// console.log(`已保存!B2字段中有值的共 ${updatedCount} 个`)
|
||||
} catch (e) {
|
||||
console.error("[OfferPie] 模拟提交失败:", e)
|
||||
console.log("保存失败,请查看控制台")
|
||||
}
|
||||
}
|
||||
|
||||
/** 打开"助手已记住的表单"弹窗,从 localStorage 加载数据 */
|
||||
const openCacheModal = () => {
|
||||
/** 打开"助手已记住的表单"弹窗,从 chrome.storage 加载数据 */
|
||||
const openCacheModal = async () => {
|
||||
try {
|
||||
const cached = localStorage.getItem("offerpie_unfilled_form")
|
||||
if (!cached) {
|
||||
const cacheData = await storageGet<any>("offerpie_unfilled_form")
|
||||
if (!cacheData) {
|
||||
setCacheModalData([])
|
||||
} else {
|
||||
const cacheData = JSON.parse(cached)
|
||||
setCacheModalData(cacheData.unfilledFormData || [])
|
||||
}
|
||||
} catch {
|
||||
@@ -528,14 +589,13 @@ export function SidebarPanel({ sourceUrl, jobInfo, onClose }: SidebarPanelProps)
|
||||
setCacheModalData(updated)
|
||||
}
|
||||
|
||||
/** 弹窗内保存数据到 localStorage */
|
||||
const saveCacheModalData = () => {
|
||||
/** 弹窗内保存数据到 chrome.storage */
|
||||
const saveCacheModalData = async () => {
|
||||
try {
|
||||
const cached = localStorage.getItem("offerpie_unfilled_form")
|
||||
const cacheData = cached ? JSON.parse(cached) : {}
|
||||
const cacheData = await storageGet<any>("offerpie_unfilled_form") || {}
|
||||
cacheData.unfilledFormData = cacheModalData || []
|
||||
cacheData.timestamp = Date.now()
|
||||
localStorage.setItem("offerpie_unfilled_form", JSON.stringify(cacheData))
|
||||
await storageSet("offerpie_unfilled_form", cacheData)
|
||||
alert("已保存表单数据")
|
||||
} catch (e) {
|
||||
console.error("[OfferPie] 保存缓存数据失败:", e)
|
||||
@@ -544,10 +604,12 @@ export function SidebarPanel({ sourceUrl, jobInfo, onClose }: SidebarPanelProps)
|
||||
}
|
||||
|
||||
/** 弹窗内清空缓存数据(带确认) */
|
||||
const clearCacheFromModal = () => {
|
||||
const clearCacheFromModal = async () => {
|
||||
const confirmed = window.confirm("是否清空已记录的表单数据?清空后下次填简历那些数据需要您一个个手动填。")
|
||||
if (!confirmed) return
|
||||
localStorage.removeItem("offerpie_unfilled_form")
|
||||
// 先停止循环扫描,防止 handleSaveUserInput 又把数据写回去
|
||||
stopScanLoop()
|
||||
await storageRemove("offerpie_unfilled_form")
|
||||
setCacheModalData([])
|
||||
alert("已清空缓存数据")
|
||||
console.log("[OfferPie] 已清空 offerpie_unfilled_form 缓存")
|
||||
@@ -555,11 +617,50 @@ export function SidebarPanel({ sourceUrl, jobInfo, onClose }: SidebarPanelProps)
|
||||
|
||||
// ============ 拖拽逻辑 ============
|
||||
|
||||
/** 鼠标按下:启动长按计时 */
|
||||
/** 即时拖拽:收起状态横条 / 展开状态顶部 header,按住即可拖动(无需长按) */
|
||||
const handleInstantDragDown = useCallback((e: React.MouseEvent) => {
|
||||
// 如果点击的是箭头按钮,不启动拖拽
|
||||
const target = e.target as HTMLElement
|
||||
if (target.closest("button, .op-collapsed-arrow, .op-collapse-btn")) return
|
||||
|
||||
e.preventDefault()
|
||||
const state = dragStateRef.current
|
||||
state.startX = e.clientX
|
||||
state.startY = e.clientY
|
||||
state.startOffsetX = dragOffset.x
|
||||
state.startOffsetY = dragOffset.y
|
||||
state.isDragging = true
|
||||
state.isLongPress = true // 标记为已激活拖拽
|
||||
if (containerRef.current) {
|
||||
containerRef.current.style.cursor = "grabbing"
|
||||
}
|
||||
|
||||
const handleMouseMove = (ev: MouseEvent) => {
|
||||
const dx = ev.clientX - state.startX
|
||||
const dy = ev.clientY - state.startY
|
||||
setDragOffset({ x: state.startOffsetX + dx, y: state.startOffsetY + dy })
|
||||
if (!hasDragged) setHasDragged(true)
|
||||
}
|
||||
|
||||
const handleMouseUp = () => {
|
||||
state.isDragging = false
|
||||
state.isLongPress = false
|
||||
if (containerRef.current) {
|
||||
containerRef.current.style.cursor = ""
|
||||
}
|
||||
document.removeEventListener("mousemove", handleMouseMove)
|
||||
document.removeEventListener("mouseup", handleMouseUp)
|
||||
}
|
||||
|
||||
document.addEventListener("mousemove", handleMouseMove)
|
||||
document.addEventListener("mouseup", handleMouseUp)
|
||||
}, [dragOffset, hasDragged])
|
||||
|
||||
/** 鼠标按下(非 header 区域):启动长按计时,300ms 后才可拖拽 */
|
||||
const handleMouseDown = useCallback((e: React.MouseEvent) => {
|
||||
// 如果点击的是交互元素(按钮、链接、输入框等),不启动拖拽
|
||||
const target = e.target as HTMLElement
|
||||
if (target.closest("button, a, input, textarea, select, [role='button'], .op-credits-link, .op-field-item, .op-cache-modal-mask")) return
|
||||
if (target.closest("button, a, input, textarea, select, [role='button'], .op-credits-link, .op-field-item, .op-cache-modal-mask, .op-header, .op-collapsed-bar")) return
|
||||
|
||||
e.preventDefault()
|
||||
const state = dragStateRef.current
|
||||
@@ -646,50 +747,84 @@ export function SidebarPanel({ sourceUrl, jobInfo, onClose }: SidebarPanelProps)
|
||||
>
|
||||
{/* 拖拽提示气泡 */}
|
||||
{showDragTooltip && (
|
||||
<div className="op-drag-tooltip">长按可以拖动助手位置</div>
|
||||
<div className="op-drag-tooltip">助手位置可以拖动</div>
|
||||
)}
|
||||
|
||||
{/* 收起状态:只显示 logo 圆圈 */}
|
||||
{/* 收起状态:显示 logo + 提示文字 + 向下箭头 */}
|
||||
{collapsed ? (
|
||||
<div className="op-collapsed-logo" onClick={() => setCollapsed(false)}>
|
||||
<img src={logoImg} alt="OfferPie" width={28} height={31} />
|
||||
<div className="op-collapsed-bar" onMouseDown={handleInstantDragDown}>
|
||||
<img src={logoImg} alt="OfferPie" width={28} height={31} className="op-header-logo" />
|
||||
<span className="op-collapsed-text">{headerStatus || "Offer派助手"}</span>
|
||||
<svg className="op-collapsed-arrow" onClick={() => setCollapsed(false)} width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
||||
<polyline points="6 9 12 15 18 9" />
|
||||
</svg>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* 填写中遮罩:覆盖整个面板,阻止用户操作 */}
|
||||
{filling && <div className="op-filling-overlay" />}
|
||||
<div className="op-inner">
|
||||
{/* 新顶部:logo + 提示文字 + 收起箭头 */}
|
||||
<div className="op-header">
|
||||
{/* 新顶部:logo + 提示文字 + 收起箭头(按住即可拖动) */}
|
||||
<div className="op-header" onMouseDown={handleInstantDragDown}>
|
||||
<div className="op-header-left">
|
||||
<img src={logoImg} alt="OfferPie" width={28} height={31} className="op-header-logo" />
|
||||
<span className="op-header-status">{headerStatus}</span>
|
||||
<span className="op-header-status">{headerStatus || "Offer派助手"}</span>
|
||||
</div>
|
||||
<button className="op-collapse-btn" onClick={() => setCollapsed(true)}>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
||||
<polyline points="6 9 12 15 18 9" />
|
||||
<polyline points="9 6 15 12 9 18" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 未登录状态:显示登录提示 */}
|
||||
{!isLoggedIn && isLoggedIn !== null && (
|
||||
<div className="op-login-prompt">
|
||||
<p className="op-login-text">请先前往 Offer派 官网进行登录</p>
|
||||
<div className="op-nonmember">
|
||||
<div className="op-nonmember-divider" />
|
||||
<div className="op-nonmember-title">登录 Offer 派,开启智能投递</div>
|
||||
<div className="op-nonmember-desc">登录后将自动同步你的会员状态、简历和求职信息</div>
|
||||
<button
|
||||
className="op-login-btn"
|
||||
onClick={() => {
|
||||
window.open("http://localhost:5173/jobs", "_blank")
|
||||
}}
|
||||
className="op-nonmember-btn"
|
||||
onClick={() => { window.open("https://test.offerpai.com.cn/jobs", "_blank") }}
|
||||
>
|
||||
前往 Offer派
|
||||
前往登录
|
||||
</button>
|
||||
<div
|
||||
className="op-nonmember-refresh"
|
||||
onClick={() => { window.location.reload() }}
|
||||
>
|
||||
已完成登录?刷新登录状态
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 已登录状态:显示完整功能 */}
|
||||
{isLoggedIn && (
|
||||
<>
|
||||
{/* 非会员状态:显示开通会员提示 */}
|
||||
{isMember === false && (
|
||||
<div className="op-nonmember">
|
||||
<div className="op-nonmember-divider" />
|
||||
<div className="op-nonmember-title">智能投递助手为会员专享功能</div>
|
||||
<div className="op-nonmember-desc">开通会员后,即可自动识别并填写网申表单,减少重复操作</div>
|
||||
<button
|
||||
className="op-nonmember-btn"
|
||||
onClick={() => { window.open("https://test.offerpai.com.cn/jobs", "_blank") }}
|
||||
>
|
||||
开通会员,立即使用
|
||||
</button>
|
||||
<div
|
||||
className="op-nonmember-refresh"
|
||||
onClick={() => { window.location.reload() }}
|
||||
>
|
||||
已开通会员?刷新会员状态
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 会员状态:显示完整功能 */}
|
||||
{isMember === true && (
|
||||
<>
|
||||
{/* 职位信息卡片 */}
|
||||
<div className="op-job-card">
|
||||
|
||||
@@ -718,13 +853,50 @@ export function SidebarPanel({ sourceUrl, jobInfo, onClose }: SidebarPanelProps)
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 自动填写按钮 */}
|
||||
<button className="op-autofill-btn" onClick={handleAutoFill} disabled={filling}>
|
||||
{filling ? "填写中..." : "开始填写"}
|
||||
</button>
|
||||
{/* 自动填写按钮区域 */}
|
||||
{fillCompleted ? (
|
||||
<div className="op-fill-done-row">
|
||||
<button className="op-reset-btn" onClick={handleReset}>
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#52CAD1" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<polyline points="23 4 23 10 17 10" />
|
||||
<polyline points="1 20 1 14 7 14" />
|
||||
<path d="M3.51 9a9 9 0 0114.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0020.49 15" />
|
||||
</svg>
|
||||
</button>
|
||||
<div className="op-fill-done-label">填写完成,请检查</div>
|
||||
</div>
|
||||
) : (
|
||||
<button className={`op-autofill-btn${filling ? " op-autofill-btn--filling" : ""}`} onClick={handleAutoFill} disabled={filling || headerStatus !== "准备就绪,请点击开始填写"}>
|
||||
{filling ? "填写中...." : "开始填写"}
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* 使用次数信息 */}
|
||||
{/* 操作链接 */}
|
||||
<div className="op-credits-row">
|
||||
<span className="op-credits-link" onClick={async () => {
|
||||
const cachedUrl = sessionStorage.getItem("offerpie_source_url") || sourceUrl
|
||||
if (!cachedUrl) return
|
||||
|
||||
try {
|
||||
// 通过跨域 CSL 模式读取投递链接列表
|
||||
const result = await panelBridge.get<OfferpaieDeliveryLinkListData>(appConfig.bridgeDomain, "offerpaiDeliveryLinkList", true)
|
||||
const linkList = result?.linkList || []
|
||||
|
||||
// 检查当前投递链接是否已在列表中
|
||||
if (!linkList.includes(cachedUrl)) {
|
||||
linkList.push(cachedUrl)
|
||||
}
|
||||
|
||||
// 写回跨域通信(让网页端能读到)
|
||||
await panelBridge.put<OfferpaieDeliveryLinkListData>(appConfig.bridgeDomain, "offerpaiDeliveryLinkList", { linkList }, true)
|
||||
console.log("[OfferPie] 已更新投递链接列表:", linkList)
|
||||
} catch (err) {
|
||||
console.warn("[OfferPie] 更新投递链接列表失败:", err)
|
||||
}
|
||||
|
||||
// 跳转到投递链接
|
||||
window.location.href = cachedUrl
|
||||
}}>打开投递链接</span>
|
||||
<span className="op-credits-link" onClick={openCacheModal}>助手已记住的表单</span>
|
||||
</div>
|
||||
|
||||
@@ -864,6 +1036,12 @@ export function SidebarPanel({ sourceUrl, jobInfo, onClose }: SidebarPanelProps)
|
||||
)
|
||||
})()}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{/* 版本号(未登录或非会员时隐藏) */}
|
||||
{isLoggedIn && isMember !== false && (
|
||||
<div className="op-version">ver: {appConfig.pluginVersion}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user