hotjob时间选择器和搜索下拉型输入框组件
This commit is contained in:
@@ -993,7 +993,7 @@ export function SidebarPanel({ sourceUrl, jobInfo, onClose }: SidebarPanelProps)
|
||||
<div className="op-nonmember-desc">登录后将自动同步你的会员状态、简历和求职信息</div>
|
||||
<button
|
||||
className="op-nonmember-btn"
|
||||
onClick={() => { window.open("https://test.offerpai.com.cn/jobs", "_blank") }}
|
||||
onClick={() => { window.open("https://www.offerpai.com.cn", "_blank") }}
|
||||
>
|
||||
前往登录
|
||||
</button>
|
||||
@@ -1053,7 +1053,7 @@ export function SidebarPanel({ sourceUrl, jobInfo, onClose }: SidebarPanelProps)
|
||||
<circle cx="24" cy="24" r="20" fill="none" stroke="#52CAD1" 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">
|
||||
<text x="24" y="28.6" textAnchor="middle" fontSize="13" fontWeight="700" fill="#000">
|
||||
{jobInfo.matchScore}%
|
||||
</text>
|
||||
</svg>
|
||||
|
||||
@@ -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<boolean> {
|
||||
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<boolean> {
|
||||
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<boolean> {
|
||||
// 构造 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
|
||||
}
|
||||
|
||||
+32
-7
@@ -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<boolean> {
|
||||
export async function fillDatePicker(field: MatchedFormField, panelHint?: HTMLElement): Promise<boolean> {
|
||||
const { labelText, inputElement, fillValue } = field
|
||||
if (!inputElement) return false
|
||||
|
||||
@@ -1289,6 +1310,10 @@ export async function fillDatePicker(field: MatchedFormField): Promise<boolean>
|
||||
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<boolean>
|
||||
}
|
||||
|
||||
// ---- 步骤3:分析日历面板结构 ----
|
||||
const panel = analyzeCalendarPanel()
|
||||
const panel = analyzeCalendarPanel(panelHint)
|
||||
if (!panel) {
|
||||
console.warn("OfferPie: [datePicker] 无法分析日历面板,尝试直接点击日期格子")
|
||||
// 兜底:尝试旧逻辑直接找日期格子
|
||||
|
||||
@@ -9,8 +9,8 @@ export type HighlightColor = "green" |"greenTwo" | "red" | "yellow"
|
||||
|
||||
/** 颜色映射表(淡色系,不影响文字可读性) */
|
||||
const COLOR_MAP: Record<HighlightColor, string> = {
|
||||
green: "#b7ffc5", // 淡绿色
|
||||
greenTwo: "#b7ffc6", // 淡绿色2
|
||||
green: "#b7ffc64d", // 淡绿色
|
||||
greenTwo: "#b7ffc54d", // 淡绿色2
|
||||
red: "#ffb7b7", // 淡红色
|
||||
yellow: "transparent", // 透明色
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user