diff --git a/src/components/SidebarPanel.tsx b/src/components/SidebarPanel.tsx
index 440c48b..2217a26 100644
--- a/src/components/SidebarPanel.tsx
+++ b/src/components/SidebarPanel.tsx
@@ -993,7 +993,7 @@ export function SidebarPanel({ sourceUrl, jobInfo, onClose }: SidebarPanelProps)
登录后将自动同步你的会员状态、简历和求职信息
@@ -1053,7 +1053,7 @@ export function SidebarPanel({ sourceUrl, jobInfo, onClose }: SidebarPanelProps)
-
+
{jobInfo.matchScore}%
diff --git a/src/handlers/handleAutoFillHotjob.ts b/src/handlers/handleAutoFillHotjob.ts
index 642dfb1..c101be9 100644
--- a/src/handlers/handleAutoFillHotjob.ts
+++ b/src/handlers/handleAutoFillHotjob.ts
@@ -7,8 +7,9 @@
* 不要在此文件中自行编写选择器操作逻辑,必须引用 lib 中已封装的方法。
*/
-import { fillMatchedField, isSearchPickerField, fillSearchPickerField, isTimePeriodField, isTimeSingleField, fillTimePeriodField, fillTimeSingleField } from "~lib/autofill"
+import { fillMatchedField, isSearchPickerField, fillSearchPickerField, forceSetValue, isTimePeriodField, isTimeSingleField, fillTimePeriodField, fillTimeSingleField } from "~lib/autofill"
import { delay } from "~utils/delay"
+import { simulateClick } from "~utils/domEvent"
import { extractDomStructure, detectPageLanguage, isJobApplicationForm, buildSelector } from "~lib/dom"
import { matchFormFieldsInRange, matchMainFields } from "~lib/formMatcher"
import { detectPickerField } from "~lib/pickerDetector"
@@ -23,6 +24,168 @@ import { setFieldHighlight, isRequiredField } from "~lib/formStyle"
import { get as storageGet, set as storageSet } from "~utils/storage"
import { findLabelForInput } from "~lib/labelFinder"
+/** Hotjob平台需要走全DOM搜索ant-select选项的字段标签名(这些字段弹出层在页面DOM末尾,通用逻辑定位不到) */
+const HOTJOB_ANT_SELECT_LABELS = ["学校"]
+
+/** Hotjob平台时间字段标签名(Ant DatePicker弹出层在DOM末尾,需要手动定位ant-calendar-picker-container) */
+const HOTJOB_DATE_LABELS = ["开始时间", "结束时间"]
+
+/**
+ * Hotjob平台特殊处理:全DOM搜索 ant-select-dropdown-menu-item 选项并点击
+ * 原因:Hotjob的Ant Select弹出层渲染在页面DOM最末尾,不在input附近,
+ * 通用的弹出层检测逻辑(DOM差异对比)会误匹配到input内部的镜像span元素
+ * @param inputEl - 搜索输入框元素
+ * @param fillValue - 要填写的值(如学校名称)
+ * @returns 是否成功选中选项
+ */
+async function hotjobFillAntSelectField(
+ inputEl: HTMLInputElement | HTMLTextAreaElement,
+ fillValue: string
+): Promise {
+ if (!inputEl || !fillValue) return false
+
+ console.log(`OfferPie: [Hotjob-AntSelect] "${fillValue}" 开始填写`)
+
+ // 1. 点击input获取焦点并写入搜索值
+ inputEl.focus()
+ simulateClick(inputEl as HTMLElement)
+ await delay("mid")
+ forceSetValue(inputEl, fillValue)
+
+ // 2. 等待接口返回数据(Hotjob接口较慢,用max延时)
+ await delay("max")
+
+ // 3. 全DOM搜索 ant-select-dropdown-menu-item,文字匹配目标值
+ const allItems = document.querySelectorAll("li.ant-select-dropdown-menu-item")
+ let targetItem: HTMLElement | null = null
+ for (const item of Array.from(allItems)) {
+ const text = item.textContent?.trim()
+ if (text && text === fillValue) {
+ targetItem = item as HTMLElement
+ break
+ }
+ }
+
+ if (!targetItem) {
+ console.log(`OfferPie: ❌ [Hotjob-AntSelect] 未找到匹配选项 "${fillValue}"`)
+ return false
+ }
+
+ // 4. 用simulateClick点击选项(Ant Design监听mousedown/pointerdown)
+ console.log(`OfferPie: [Hotjob-AntSelect] 找到选项,点击: tag=${targetItem.tagName} | class="${targetItem.className}"`)
+ simulateClick(targetItem)
+ await delay("mid")
+
+ console.log(`OfferPie: ✅ [Hotjob-AntSelect] 已选择 "${fillValue}"`)
+ return true
+}
+
+/**
+ * Hotjob平台特殊处理:时间/日期字段填写
+ * 原因:Hotjob的Ant DatePicker弹出层渲染在页面DOM最末尾(div.ant-calendar-picker-container),
+ * 通用的 analyzeCalendarPanel 全DOM搜索找不到面板(被input附近元素干扰)
+ * 流程:simulateClick展开面板 → 全DOM搜ant-calendar-picker-container定位面板 → 调fillDatePicker(field, panelHint)走集成式逻辑
+ * @param field - 匹配到的表单字段
+ * @returns 是否成功填写
+ */
+async function hotjobFillDateField(field: MatchedFormField): Promise {
+ const { labelText, inputElement, fillValue } = field
+ if (!inputElement || !fillValue) return false
+
+ // 1. 如果fillValue只有年月(如2025-09),补成2025-09-01
+ let adjustedValue = fillValue
+ if (/^\d{4}[.\-\/]\d{1,2}$/.test(fillValue)) {
+ adjustedValue = fillValue.replace(/[.\-\/]/, "-") + "-01"
+ console.log(`OfferPie: [Hotjob-Date] "${labelText}" 只有年月,补日为: "${adjustedValue}"`)
+ }
+ field.fillValue = adjustedValue
+
+ // 2. simulateClick展开日期面板
+ simulateClick(inputElement as HTMLElement)
+ await delay("mid")
+
+ // 3. 全DOM搜索可见的 ant-calendar-picker-container,再往下找 ant-calendar-date-panel 作为精确面板定位
+ const allContainers = document.querySelectorAll("div.ant-calendar-picker-container")
+ let panelHint: HTMLElement | null = null
+ for (const el of Array.from(allContainers)) {
+ const htmlEl = el as HTMLElement
+ if (htmlEl.offsetHeight > 0 && htmlEl.offsetWidth > 0) {
+ // 优先找更精确的 ant-calendar-date-panel(包含头部年月按钮+日期格子)
+ const datePanel = htmlEl.querySelector("div.ant-calendar-date-panel") as HTMLElement | null
+ if (datePanel && datePanel.offsetHeight > 0) {
+ panelHint = datePanel
+ } else {
+ panelHint = htmlEl
+ }
+ break
+ }
+ }
+
+ if (!panelHint) {
+ console.log(`OfferPie: ❌ [Hotjob-Date] "${labelText}" 未找到 ant-calendar-picker-container`)
+ return false
+ }
+
+ console.log(`OfferPie: [Hotjob-Date] "${labelText}" 找到面板容器,调用 fillDatePicker`)
+
+ // 4. 解析目标年月,通过Ant Calendar专属类名导航到目标年月
+ const dateMatch = adjustedValue.match(/^(\d{4})[.\-\/](\d{1,2})/)
+ if (dateMatch) {
+ const targetYear = parseInt(dateMatch[1], 10)
+ const targetMonth = parseInt(dateMatch[2], 10)
+
+ // 读取面板当前显示的年月(从 ant-calendar-year-select 和 ant-calendar-month-select 获取)
+ const readPanelYearMonth = (): { year: number; month: number } => {
+ const yearEl = panelHint!.querySelector("a.ant-calendar-year-select")
+ const monthEl = panelHint!.querySelector("a.ant-calendar-month-select")
+ const yearText = yearEl?.textContent?.trim() || ""
+ const monthText = monthEl?.textContent?.trim() || ""
+ const year = parseInt(yearText, 10) || 0
+ const month = parseInt(monthText, 10) || 0
+ return { year, month }
+ }
+
+ let current = readPanelYearMonth()
+ console.log(`OfferPie: [Hotjob-Date] "${labelText}" 面板当前: ${current.year}年${current.month}月 → 目标: ${targetYear}年${targetMonth}月`)
+
+ // 导航年份
+ const yearDiff = targetYear - current.year
+ if (yearDiff !== 0) {
+ const yearBtnClass = yearDiff > 0 ? "a.ant-calendar-next-year-btn" : "a.ant-calendar-prev-year-btn"
+ const yearBtn = panelHint!.querySelector(yearBtnClass) as HTMLElement | null
+ if (yearBtn) {
+ for (let i = 0; i < Math.abs(yearDiff) && i < 100; i++) {
+ yearBtn.click()
+ await delay("low")
+ }
+ await delay("low")
+ current = readPanelYearMonth()
+ console.log(`OfferPie: [Hotjob-Date] 年份导航完成,当前: ${current.year}年${current.month}月`)
+ }
+ }
+
+ // 导航月份
+ const monthDiff = targetMonth - current.month
+ if (monthDiff !== 0) {
+ const monthBtnClass = monthDiff > 0 ? "a.ant-calendar-next-month-btn" : "a.ant-calendar-prev-month-btn"
+ const monthBtn = panelHint!.querySelector(monthBtnClass) as HTMLElement | null
+ if (monthBtn) {
+ for (let i = 0; i < Math.abs(monthDiff) && i < 12; i++) {
+ monthBtn.click()
+ await delay("low")
+ }
+ await delay("low")
+ current = readPanelYearMonth()
+ console.log(`OfferPie: [Hotjob-Date] 月份导航完成,当前: ${current.year}年${current.month}月`)
+ }
+ }
+ }
+
+ // 5. 调用集成式选择器核心逻辑,传入panelHint让它在正确的容器内点击日期格子
+ const ok = await fillDatePicker(field, panelHint)
+ return ok
+}
+
/** Hotjob模式自动填写的参数 */
export interface AutoFillHotjobParams {
/** 简历数据(接口获取的) */
@@ -315,6 +478,7 @@ export async function handleAutoFillHotjob(params: AutoFillHotjobParams): Promis
// 根据字段类型选择对应的填充方法(全部走已封装的统一入口)
if (isTimePeriodField(f.key)) {
+ console.log(`OfferPie: [Hotjob-Debug] 进入 isTimePeriodField 分支: key="${f.key}" labelText="${f.labelText}"`)
const startDateVal = getResumeFieldValue(currentResumeData, f.section, dataIdx, "startDate")
const endDateVal = getResumeFieldValue(currentResumeData, f.section, dataIdx, "endDate")
let ok = await fillTimePeriodField(f, startDateVal, endDateVal, usedInputs)
@@ -325,17 +489,23 @@ export async function handleAutoFillHotjob(params: AutoFillHotjobParams): Promis
}
if (ok) { result.success++ } else { result.failed++ }
} else if (isTimeSingleField(f.key)) {
+ console.log(`OfferPie: [Hotjob-Debug] 进入 isTimeSingleField 分支: key="${f.key}" labelText="${f.labelText}" fillValue="${f.fillValue}"`)
if (!f.fillValue) { phaseAFields.push({ labelText: f.labelText, inputElement: f.inputElement, filled: false, fillValue: "", section: f.section, segmentIndex: segIdx }); result.skipped++; continue }
- let ok = await fillTimeSingleField(f, f.fillValue, usedInputs)
- if (!ok) {
- await detectPickerField(f, lang)
- ok = await fillMatchedField(f)
- }
+ let ok: boolean
+ // Hotjob平台:所有时间字段都走hotjobFillDateField(Ant DatePicker弹出层在DOM末尾)
+ console.log(`OfferPie: [Hotjob-Debug] 走 hotjobFillDateField`)
+ ok = await hotjobFillDateField(f)
if (ok) { result.success++ } else { result.failed++ }
} else if (!f.fillValue) {
phaseAFields.push({ labelText: f.labelText, inputElement: f.inputElement, filled: false, fillValue: "", section: f.section, segmentIndex: segIdx }); result.skipped++; continue
} else if (isSearchPickerField(f.key)) {
- const ok = await fillSearchPickerField(f)
+ let ok: boolean
+ // Hotjob平台特殊字段:弹出层在DOM末尾,通用搜索逻辑定位不到,走全DOM搜li选项
+ if (HOTJOB_ANT_SELECT_LABELS.includes(f.labelText)) {
+ ok = await hotjobFillAntSelectField(f.inputElement, f.fillValue)
+ } else {
+ ok = await fillSearchPickerField(f)
+ }
if (ok) { result.success++ } else { result.failed++ }
phaseAFields.push({ labelText: f.labelText, inputElement: f.inputElement, filled: ok, fillValue: f.fillValue, section: f.section, segmentIndex: segIdx })
await delay("mid")
@@ -1190,7 +1360,7 @@ async function b2FillDateTimeField(
labelText: string,
fillValue: string
): Promise {
- // 构造 MatchedFormField(和阶段A一样,isPicker=true,不设 pickerDropdownElement)
+ // Hotjob平台:所有时间/日期字段都走hotjobFillDateField(Ant DatePicker弹出层在DOM末尾,通用逻辑无法定位)
const field: MatchedFormField = {
key: "", section: "main", resumeField: "",
sectionIndex: 0, labelText,
@@ -1205,17 +1375,11 @@ async function b2FillDateTimeField(
pickerDropdownElement: null, pickerDropdownSelector: "",
fillValue,
}
-
- // 和阶段A同样的方式:点击 input 展开日期面板,然后直接调 fillDatePicker
- inputEl.focus()
- ;(inputEl as HTMLElement).click()
- await delay("mid")
-
- const ok = await fillDatePicker(field)
+ const ok = await hotjobFillDateField(field)
if (ok) {
- console.log(`OfferPie: ✅ [B2-时间字段] "${labelText}" = "${fillValue}" 填写成功`)
+ console.log(`OfferPie: ✅ [B2-时间字段] "${labelText}" = "${fillValue}" 填写成功 (Hotjob模式)`)
} else {
- console.log(`OfferPie: ❌ [B2-时间字段] "${labelText}" = "${fillValue}" 填写失败`)
+ console.log(`OfferPie: ❌ [B2-时间字段] "${labelText}" = "${fillValue}" 填写失败 (Hotjob模式)`)
}
return ok
}
diff --git a/src/lib/datePicker.ts b/src/lib/datePicker.ts
index 81ea7a8..e33425c 100644
--- a/src/lib/datePicker.ts
+++ b/src/lib/datePicker.ts
@@ -186,9 +186,31 @@ function isDayGridArea(el: HTMLElement): boolean {
* 2. 从日期格子向上找星期行(日一二三四五六)
* 3. 从星期行继续向上到面板顶部,识别年月显示和可点击按钮
*/
-function analyzeCalendarPanel(): CalendarPanelInfo | null {
+function analyzeCalendarPanel(panelHint?: HTMLElement): CalendarPanelInfo | null {
// ---- 步骤1:找到日期格子区域 ----
- // 从常见的日历容器选择器开始搜索
+ let dayGridArea: HTMLElement | null = null
+ let panelRoot: HTMLElement | null = null
+
+ // 如果调用方传入了面板定位提示(panelHint),优先在提示元素内搜索日期格子
+ if (panelHint) {
+ const innerEls = panelHint.querySelectorAll("table, tbody, div")
+ for (const inner of Array.from(innerEls)) {
+ if (isVisible(inner) && isDayGridArea(inner as HTMLElement)) {
+ dayGridArea = inner as HTMLElement
+ panelRoot = panelHint
+ break
+ }
+ }
+ if (dayGridArea) {
+ console.log(`OfferPie: [datePicker] 通过 panelHint 找到日期格子区域: ${dayGridArea.tagName}.${(dayGridArea.className || "").toString().split(" ")[0]}`)
+ } else {
+ console.log(`OfferPie: [datePicker] panelHint 内未找到日期格子,fallback 到全 DOM 搜索`)
+ }
+ }
+
+ // 从常见的日历容器选择器开始搜索(panelHint 未命中时执行)
+ if (!dayGridArea) {
+
const panelSelectors = [
'[class*="picker-panel"]', '[class*="date-panel"]', '[class*="calendar"]',
'[class*="datepicker"]', '[class*="date-picker"]',
@@ -197,9 +219,6 @@ function analyzeCalendarPanel(): CalendarPanelInfo | null {
'[class*="dropdown"]', '[class*="overlay"]',
]
- let dayGridArea: HTMLElement | null = null
- let panelRoot: HTMLElement | null = null
-
// 先尝试从已知面板选择器中找
for (const sel of panelSelectors) {
const panels = document.querySelectorAll(sel)
@@ -247,6 +266,8 @@ function analyzeCalendarPanel(): CalendarPanelInfo | null {
}
}
+ } // end if (!dayGridArea) — panelHint 未命中时的全 DOM 搜索
+
if (!dayGridArea || !panelRoot) {
console.warn("OfferPie: [datePicker] 未找到日期格子区域")
return null
@@ -1264,7 +1285,7 @@ function parseDateValue(fillValue: string): { year: number; month: number; day:
*
* 【注意】按钮探测逻辑是通用适配的核心,不要简化或删除
*/
-export async function fillDatePicker(field: MatchedFormField): Promise {
+export async function fillDatePicker(field: MatchedFormField, panelHint?: HTMLElement): Promise {
const { labelText, inputElement, fillValue } = field
if (!inputElement) return false
@@ -1289,6 +1310,10 @@ export async function fillDatePicker(field: MatchedFormField): Promise
if (location.hostname.includes("mokahr.com") && (inputElement as HTMLInputElement).readOnly) {
console.log(`OfferPie: [datePicker] Moka适配: 跳过 forceSetValue(readonly input),等待弹出层渲染`)
await delay("high")
+ // 【Hotjob适配】Hotjob 的 Ant DatePicker input 为 readonly,写值会触发面板关闭,跳过
+ } else if (location.hostname.includes("hotjob.cn") && (inputElement as HTMLInputElement).readOnly) {
+ console.log(`OfferPie: [datePicker] Hotjob适配: 跳过 forceSetValue(readonly input),等待弹出层渲染`)
+ await delay("mid")
} else {
forceSetValue(inputElement, fillValue)
await delay("high")
@@ -1302,7 +1327,7 @@ export async function fillDatePicker(field: MatchedFormField): Promise
}
// ---- 步骤3:分析日历面板结构 ----
- const panel = analyzeCalendarPanel()
+ const panel = analyzeCalendarPanel(panelHint)
if (!panel) {
console.warn("OfferPie: [datePicker] 无法分析日历面板,尝试直接点击日期格子")
// 兜底:尝试旧逻辑直接找日期格子
diff --git a/src/lib/formStyle.ts b/src/lib/formStyle.ts
index 504f013..6f4aa98 100644
--- a/src/lib/formStyle.ts
+++ b/src/lib/formStyle.ts
@@ -9,8 +9,8 @@ export type HighlightColor = "green" |"greenTwo" | "red" | "yellow"
/** 颜色映射表(淡色系,不影响文字可读性) */
const COLOR_MAP: Record = {
- green: "#b7ffc5", // 淡绿色
- greenTwo: "#b7ffc6", // 淡绿色2
+ green: "#b7ffc64d", // 淡绿色
+ greenTwo: "#b7ffc54d", // 淡绿色2
red: "#ffb7b7", // 淡红色
yellow: "transparent", // 透明色
}
diff --git a/src/lib/pickerFill.ts b/src/lib/pickerFill.ts
index 5b85665..47eb653 100644
--- a/src/lib/pickerFill.ts
+++ b/src/lib/pickerFill.ts
@@ -4,6 +4,7 @@
*/
import type { MatchedFormField } from "./types"
+import { simulateClick } from "~utils/domEvent"
// ============ 模糊匹配 ============
@@ -62,7 +63,10 @@ export function clickBestOptionInDropdown(dropdownEl: HTMLElement, fillValue: st
let bestMatch: { el: HTMLElement; text: string; score: number } | null = null
for (const { el, text } of candidates) {
if (text.toLowerCase() === fillValue.toLowerCase()) {
- el.click()
+ // 【注意】必须用 simulateClick 触发完整事件链(pointerdown→mousedown→pointerup→mouseup→click)
+ // 原生 .click() 只派发 click 事件,Ant Design 等组件库监听的是 mousedown/pointerdown,用 .click() 选项不会生效
+ console.log(`OfferPie: [clickBestOption] 即将点击元素: tag=${el.tagName} | class="${el.className}" | role="${el.getAttribute("role")}"`)
+ simulateClick(el)
console.log(`OfferPie: ✅ [弹出层] 点击选项 "${text}" (精确匹配)`)
return true
}
@@ -70,7 +74,8 @@ export function clickBestOptionInDropdown(dropdownEl: HTMLElement, fillValue: st
if (score > 0.2 && (!bestMatch || score > bestMatch.score)) bestMatch = { el, text, score }
}
if (bestMatch) {
- bestMatch.el.click()
+ // 【注意】同上,必须用 simulateClick 触发完整事件链,否则 Ant Design 等组件不响应
+ simulateClick(bestMatch.el)
console.log(`OfferPie: ✅ [弹出层] 点击选项 "${bestMatch.text}" (模糊匹配 score=${bestMatch.score.toFixed(2)})`)
return true
}