hotjob时间选择器和搜索下拉型输入框组件
This commit is contained in:
@@ -993,7 +993,7 @@ export function SidebarPanel({ sourceUrl, jobInfo, onClose }: SidebarPanelProps)
|
|||||||
<div className="op-nonmember-desc">登录后将自动同步你的会员状态、简历和求职信息</div>
|
<div className="op-nonmember-desc">登录后将自动同步你的会员状态、简历和求职信息</div>
|
||||||
<button
|
<button
|
||||||
className="op-nonmember-btn"
|
className="op-nonmember-btn"
|
||||||
onClick={() => { window.open("https://test.offerpai.com.cn/jobs", "_blank") }}
|
onClick={() => { window.open("https://www.offerpai.com.cn", "_blank") }}
|
||||||
>
|
>
|
||||||
前往登录
|
前往登录
|
||||||
</button>
|
</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"
|
<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}`}
|
strokeDasharray={`${(jobInfo.matchScore) / 100 * 2 * Math.PI * 20} ${2 * Math.PI * 20}`}
|
||||||
strokeLinecap="round" transform="rotate(-90 24 24)" />
|
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}%
|
{jobInfo.matchScore}%
|
||||||
</text>
|
</text>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
@@ -7,8 +7,9 @@
|
|||||||
* 不要在此文件中自行编写选择器操作逻辑,必须引用 lib 中已封装的方法。
|
* 不要在此文件中自行编写选择器操作逻辑,必须引用 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 { delay } from "~utils/delay"
|
||||||
|
import { simulateClick } from "~utils/domEvent"
|
||||||
import { extractDomStructure, detectPageLanguage, isJobApplicationForm, buildSelector } from "~lib/dom"
|
import { extractDomStructure, detectPageLanguage, isJobApplicationForm, buildSelector } from "~lib/dom"
|
||||||
import { matchFormFieldsInRange, matchMainFields } from "~lib/formMatcher"
|
import { matchFormFieldsInRange, matchMainFields } from "~lib/formMatcher"
|
||||||
import { detectPickerField } from "~lib/pickerDetector"
|
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 { get as storageGet, set as storageSet } from "~utils/storage"
|
||||||
import { findLabelForInput } from "~lib/labelFinder"
|
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模式自动填写的参数 */
|
/** Hotjob模式自动填写的参数 */
|
||||||
export interface AutoFillHotjobParams {
|
export interface AutoFillHotjobParams {
|
||||||
/** 简历数据(接口获取的) */
|
/** 简历数据(接口获取的) */
|
||||||
@@ -315,6 +478,7 @@ export async function handleAutoFillHotjob(params: AutoFillHotjobParams): Promis
|
|||||||
|
|
||||||
// 根据字段类型选择对应的填充方法(全部走已封装的统一入口)
|
// 根据字段类型选择对应的填充方法(全部走已封装的统一入口)
|
||||||
if (isTimePeriodField(f.key)) {
|
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 startDateVal = getResumeFieldValue(currentResumeData, f.section, dataIdx, "startDate")
|
||||||
const endDateVal = getResumeFieldValue(currentResumeData, f.section, dataIdx, "endDate")
|
const endDateVal = getResumeFieldValue(currentResumeData, f.section, dataIdx, "endDate")
|
||||||
let ok = await fillTimePeriodField(f, startDateVal, endDateVal, usedInputs)
|
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++ }
|
if (ok) { result.success++ } else { result.failed++ }
|
||||||
} else if (isTimeSingleField(f.key)) {
|
} 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 }
|
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)
|
let ok: boolean
|
||||||
if (!ok) {
|
// Hotjob平台:所有时间字段都走hotjobFillDateField(Ant DatePicker弹出层在DOM末尾)
|
||||||
await detectPickerField(f, lang)
|
console.log(`OfferPie: [Hotjob-Debug] 走 hotjobFillDateField`)
|
||||||
ok = await fillMatchedField(f)
|
ok = await hotjobFillDateField(f)
|
||||||
}
|
|
||||||
if (ok) { result.success++ } else { result.failed++ }
|
if (ok) { result.success++ } else { result.failed++ }
|
||||||
} else if (!f.fillValue) {
|
} else if (!f.fillValue) {
|
||||||
phaseAFields.push({ labelText: f.labelText, inputElement: f.inputElement, filled: false, fillValue: "", section: f.section, segmentIndex: segIdx }); result.skipped++; continue
|
phaseAFields.push({ labelText: f.labelText, inputElement: f.inputElement, filled: false, fillValue: "", section: f.section, segmentIndex: segIdx }); result.skipped++; continue
|
||||||
} else if (isSearchPickerField(f.key)) {
|
} 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++ }
|
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 })
|
phaseAFields.push({ labelText: f.labelText, inputElement: f.inputElement, filled: ok, fillValue: f.fillValue, section: f.section, segmentIndex: segIdx })
|
||||||
await delay("mid")
|
await delay("mid")
|
||||||
@@ -1190,7 +1360,7 @@ async function b2FillDateTimeField(
|
|||||||
labelText: string,
|
labelText: string,
|
||||||
fillValue: string
|
fillValue: string
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
// 构造 MatchedFormField(和阶段A一样,isPicker=true,不设 pickerDropdownElement)
|
// Hotjob平台:所有时间/日期字段都走hotjobFillDateField(Ant DatePicker弹出层在DOM末尾,通用逻辑无法定位)
|
||||||
const field: MatchedFormField = {
|
const field: MatchedFormField = {
|
||||||
key: "", section: "main", resumeField: "",
|
key: "", section: "main", resumeField: "",
|
||||||
sectionIndex: 0, labelText,
|
sectionIndex: 0, labelText,
|
||||||
@@ -1205,17 +1375,11 @@ async function b2FillDateTimeField(
|
|||||||
pickerDropdownElement: null, pickerDropdownSelector: "",
|
pickerDropdownElement: null, pickerDropdownSelector: "",
|
||||||
fillValue,
|
fillValue,
|
||||||
}
|
}
|
||||||
|
const ok = await hotjobFillDateField(field)
|
||||||
// 和阶段A同样的方式:点击 input 展开日期面板,然后直接调 fillDatePicker
|
|
||||||
inputEl.focus()
|
|
||||||
;(inputEl as HTMLElement).click()
|
|
||||||
await delay("mid")
|
|
||||||
|
|
||||||
const ok = await fillDatePicker(field)
|
|
||||||
if (ok) {
|
if (ok) {
|
||||||
console.log(`OfferPie: ✅ [B2-时间字段] "${labelText}" = "${fillValue}" 填写成功`)
|
console.log(`OfferPie: ✅ [B2-时间字段] "${labelText}" = "${fillValue}" 填写成功 (Hotjob模式)`)
|
||||||
} else {
|
} else {
|
||||||
console.log(`OfferPie: ❌ [B2-时间字段] "${labelText}" = "${fillValue}" 填写失败`)
|
console.log(`OfferPie: ❌ [B2-时间字段] "${labelText}" = "${fillValue}" 填写失败 (Hotjob模式)`)
|
||||||
}
|
}
|
||||||
return ok
|
return ok
|
||||||
}
|
}
|
||||||
|
|||||||
+32
-7
@@ -186,9 +186,31 @@ function isDayGridArea(el: HTMLElement): boolean {
|
|||||||
* 2. 从日期格子向上找星期行(日一二三四五六)
|
* 2. 从日期格子向上找星期行(日一二三四五六)
|
||||||
* 3. 从星期行继续向上到面板顶部,识别年月显示和可点击按钮
|
* 3. 从星期行继续向上到面板顶部,识别年月显示和可点击按钮
|
||||||
*/
|
*/
|
||||||
function analyzeCalendarPanel(): CalendarPanelInfo | null {
|
function analyzeCalendarPanel(panelHint?: HTMLElement): CalendarPanelInfo | null {
|
||||||
// ---- 步骤1:找到日期格子区域 ----
|
// ---- 步骤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 = [
|
const panelSelectors = [
|
||||||
'[class*="picker-panel"]', '[class*="date-panel"]', '[class*="calendar"]',
|
'[class*="picker-panel"]', '[class*="date-panel"]', '[class*="calendar"]',
|
||||||
'[class*="datepicker"]', '[class*="date-picker"]',
|
'[class*="datepicker"]', '[class*="date-picker"]',
|
||||||
@@ -197,9 +219,6 @@ function analyzeCalendarPanel(): CalendarPanelInfo | null {
|
|||||||
'[class*="dropdown"]', '[class*="overlay"]',
|
'[class*="dropdown"]', '[class*="overlay"]',
|
||||||
]
|
]
|
||||||
|
|
||||||
let dayGridArea: HTMLElement | null = null
|
|
||||||
let panelRoot: HTMLElement | null = null
|
|
||||||
|
|
||||||
// 先尝试从已知面板选择器中找
|
// 先尝试从已知面板选择器中找
|
||||||
for (const sel of panelSelectors) {
|
for (const sel of panelSelectors) {
|
||||||
const panels = document.querySelectorAll(sel)
|
const panels = document.querySelectorAll(sel)
|
||||||
@@ -247,6 +266,8 @@ function analyzeCalendarPanel(): CalendarPanelInfo | null {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
} // end if (!dayGridArea) — panelHint 未命中时的全 DOM 搜索
|
||||||
|
|
||||||
if (!dayGridArea || !panelRoot) {
|
if (!dayGridArea || !panelRoot) {
|
||||||
console.warn("OfferPie: [datePicker] 未找到日期格子区域")
|
console.warn("OfferPie: [datePicker] 未找到日期格子区域")
|
||||||
return null
|
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
|
const { labelText, inputElement, fillValue } = field
|
||||||
if (!inputElement) return false
|
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) {
|
if (location.hostname.includes("mokahr.com") && (inputElement as HTMLInputElement).readOnly) {
|
||||||
console.log(`OfferPie: [datePicker] Moka适配: 跳过 forceSetValue(readonly input),等待弹出层渲染`)
|
console.log(`OfferPie: [datePicker] Moka适配: 跳过 forceSetValue(readonly input),等待弹出层渲染`)
|
||||||
await delay("high")
|
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 {
|
} else {
|
||||||
forceSetValue(inputElement, fillValue)
|
forceSetValue(inputElement, fillValue)
|
||||||
await delay("high")
|
await delay("high")
|
||||||
@@ -1302,7 +1327,7 @@ export async function fillDatePicker(field: MatchedFormField): Promise<boolean>
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ---- 步骤3:分析日历面板结构 ----
|
// ---- 步骤3:分析日历面板结构 ----
|
||||||
const panel = analyzeCalendarPanel()
|
const panel = analyzeCalendarPanel(panelHint)
|
||||||
if (!panel) {
|
if (!panel) {
|
||||||
console.warn("OfferPie: [datePicker] 无法分析日历面板,尝试直接点击日期格子")
|
console.warn("OfferPie: [datePicker] 无法分析日历面板,尝试直接点击日期格子")
|
||||||
// 兜底:尝试旧逻辑直接找日期格子
|
// 兜底:尝试旧逻辑直接找日期格子
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ export type HighlightColor = "green" |"greenTwo" | "red" | "yellow"
|
|||||||
|
|
||||||
/** 颜色映射表(淡色系,不影响文字可读性) */
|
/** 颜色映射表(淡色系,不影响文字可读性) */
|
||||||
const COLOR_MAP: Record<HighlightColor, string> = {
|
const COLOR_MAP: Record<HighlightColor, string> = {
|
||||||
green: "#b7ffc5", // 淡绿色
|
green: "#b7ffc64d", // 淡绿色
|
||||||
greenTwo: "#b7ffc6", // 淡绿色2
|
greenTwo: "#b7ffc54d", // 淡绿色2
|
||||||
red: "#ffb7b7", // 淡红色
|
red: "#ffb7b7", // 淡红色
|
||||||
yellow: "transparent", // 透明色
|
yellow: "transparent", // 透明色
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type { MatchedFormField } from "./types"
|
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
|
let bestMatch: { el: HTMLElement; text: string; score: number } | null = null
|
||||||
for (const { el, text } of candidates) {
|
for (const { el, text } of candidates) {
|
||||||
if (text.toLowerCase() === fillValue.toLowerCase()) {
|
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}" (精确匹配)`)
|
console.log(`OfferPie: ✅ [弹出层] 点击选项 "${text}" (精确匹配)`)
|
||||||
return true
|
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 (score > 0.2 && (!bestMatch || score > bestMatch.score)) bestMatch = { el, text, score }
|
||||||
}
|
}
|
||||||
if (bestMatch) {
|
if (bestMatch) {
|
||||||
bestMatch.el.click()
|
// 【注意】同上,必须用 simulateClick 触发完整事件链,否则 Ant Design 等组件不响应
|
||||||
|
simulateClick(bestMatch.el)
|
||||||
console.log(`OfferPie: ✅ [弹出层] 点击选项 "${bestMatch.text}" (模糊匹配 score=${bestMatch.score.toFixed(2)})`)
|
console.log(`OfferPie: ✅ [弹出层] 点击选项 "${bestMatch.text}" (模糊匹配 score=${bestMatch.score.toFixed(2)})`)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user