统一控制延时
This commit is contained in:
@@ -101,3 +101,16 @@ src/
|
||||
- 禁止对 isAsyncDropdown 字段走 `tryClickDropdownListItem` 全页面搜(选项数可能为1,全页面搜会被其他同规格元素干扰)
|
||||
- `pickerFill.ts` 中 `groupBySpec` / `findVisibleListGroups` 的同规格标签组阈值为 ≥2(不可改回 ≥3,否则只有2个选项的下拉列表会漏匹配)
|
||||
- `clickBestOptionInDropdown` 是在指定弹出层 DOM 内递归搜所有可见叶子文字节点做模糊匹配的核心方法,**不可简化其递归逻辑**
|
||||
|
||||
|
||||
## 延时调用规范
|
||||
|
||||
- **项目中所有延时调用必须且只能使用 `src/utils/delay.ts` 中导出的 `delay` 函数**
|
||||
- 禁止在任何文件中自行编写 `setTimeout` / `new Promise(resolve => setTimeout(...))` 等延时逻辑
|
||||
- `delay` 函数接收延时等级参数,统一管理延时时长:
|
||||
- `delay("low")` — 低延时(微等待,如点击后极短暂停)
|
||||
- `delay("mid")` — 中延时(等待 DOM 更新、弹出层渲染等)
|
||||
- `delay("high")` — 高延时(等待接口返回、搜索结果、动画完成等)
|
||||
- `delay("max")` — 特殊超长延时(谨慎使用)
|
||||
- 具体毫秒数只在 `src/utils/delay.ts` 中统一设置,调用方只使用等级名称
|
||||
- 如需调整全局延时策略,只修改 `delay.ts` 中的 `DELAY_MS` 常量即可
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "offerpie-extension",
|
||||
"displayName": "Offer派智能投递助手",
|
||||
"version": "0.1.0",
|
||||
"version": "0.1.1",
|
||||
"description": "offer派平台旗下网申投递助手 - 自动化投递简历\n官网:https://offerpai.com.cn",
|
||||
"scripts": {
|
||||
"dev": "plasmo dev",
|
||||
|
||||
+3
-3
@@ -3,7 +3,7 @@
|
||||
*/
|
||||
|
||||
/** 当前环境,手动切换 */
|
||||
const ENV = 'prod'
|
||||
const ENV = 'dev'
|
||||
|
||||
/** 各环境配置 */
|
||||
const envConfigs: Record<string, {
|
||||
@@ -20,14 +20,14 @@ const envConfigs: Record<string, {
|
||||
// aiBaseApi: 'http://192.168.31.133:8000',
|
||||
aiBaseApi: 'http://127.0.0.1:8000',
|
||||
cookieSourceUrl: 'http://127.0.0.1:5173',
|
||||
bridgeDomain: 'test.offerpai.com.cn',
|
||||
bridgeDomain: 'offerpai.com.cn',
|
||||
pluginVersion: '0.1.1',
|
||||
},
|
||||
prod: {
|
||||
dataBaseApi: 'https://www.offerpai.com.cn/api',
|
||||
aiBaseApi: 'https://www.offerpai.com.cn/ai-api',
|
||||
cookieSourceUrl: 'https://www.offerpai.com.cn',
|
||||
bridgeDomain: 'www.offerpai.com.cn',
|
||||
bridgeDomain: 'offerpai.com.cn',
|
||||
pluginVersion: '0.1.1',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
* 不要在此文件中自行编写选择器操作逻辑,必须引用 lib 中已封装的方法。
|
||||
*/
|
||||
|
||||
import { fillMatchedField, delay, forceSetValue, isSearchPickerField, fillSearchPickerField, isTimePeriodField, isTimeSingleField, fillTimePeriodField, fillTimeSingleField } from "~lib/autofill"
|
||||
import { fillMatchedField, forceSetValue, isSearchPickerField, fillSearchPickerField, isTimePeriodField, isTimeSingleField, fillTimePeriodField, fillTimeSingleField } from "~lib/autofill"
|
||||
import { delay } from "~utils/delay"
|
||||
import { extractDomStructure, detectPageLanguage, isJobApplicationForm, buildSelector } from "~lib/dom"
|
||||
import { matchFormFieldsInRange, matchMainFields } from "~lib/formMatcher"
|
||||
import { detectPickerField } from "~lib/pickerDetector"
|
||||
@@ -103,7 +104,7 @@ export async function handleAutoFillBeisen(params: AutoFillBeisenParams): Promis
|
||||
// const resumeUrl = "https://offerpie.oss-cn-guangzhou.aliyuncs.com/%E5%AE%BE%E5%A4%95%E6%B3%95%E5%B0%BC%E4%BA%9A%E5%A4%A7%E5%AD%A6_%E4%B8%81%E5%B1%B9%E6%B6%B5.pdf"
|
||||
// const uploaded = await detectAndUploadResume(resumeUrl)
|
||||
// console.log(`===== OfferPie: 简历上传 ${uploaded ? "成功" : "跳过(未找到上传按钮或失败)"} =====`)
|
||||
// if (uploaded) await delay(1000) // 等待网站解析简历
|
||||
// if (uploaded) await delay("high") // 等待网站解析简历
|
||||
|
||||
// 4.5 定位经历区块并统计已展开段数
|
||||
const sectionResults = locateExperienceSections(document.body, lang)
|
||||
@@ -331,14 +332,14 @@ export async function handleAutoFillBeisen(params: AutoFillBeisenParams): Promis
|
||||
const 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(100)
|
||||
await delay("mid")
|
||||
continue
|
||||
} else if (isBeisenCascadeField(f.labelText) && f.inputElement) {
|
||||
// 北森多级级联选择器字段(地区、籍贯、民族等)
|
||||
const ok = await fillBeisenCascadePicker(f.inputElement, f.labelText, f.fillValue)
|
||||
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(150)
|
||||
await delay("mid")
|
||||
continue
|
||||
} else {
|
||||
await detectPickerField(f, lang)
|
||||
@@ -353,7 +354,7 @@ export async function handleAutoFillBeisen(params: AutoFillBeisenParams): Promis
|
||||
if (lastTextInput && !lastTextInputIsPicker) {
|
||||
;(lastTextInput as HTMLElement).click()
|
||||
lastTextInput.focus()
|
||||
await delay(50)
|
||||
await delay("low")
|
||||
lastTextInput.blur()
|
||||
} else {
|
||||
if (document.activeElement instanceof HTMLElement) {
|
||||
@@ -364,7 +365,7 @@ export async function handleAutoFillBeisen(params: AutoFillBeisenParams): Promis
|
||||
}
|
||||
document.body.click()
|
||||
}
|
||||
await delay(150)
|
||||
await delay("mid")
|
||||
|
||||
if (!f.isPicker && f.inputElement && !isTimePeriodField(f.key) && !isTimeSingleField(f.key)) {
|
||||
lastTextInput = f.inputElement
|
||||
@@ -402,7 +403,7 @@ export async function handleAutoFillBeisen(params: AutoFillBeisenParams): Promis
|
||||
console.log(` [${f.key}] "${f.labelText}" → 北森级联选择器 | fillValue: "${f.fillValue}"`)
|
||||
const ok = await fillBeisenCascadePicker(f.inputElement, f.labelText, f.fillValue)
|
||||
if (ok) { result.success++ } else { result.failed++ }
|
||||
await delay(150)
|
||||
await delay("mid")
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -420,7 +421,7 @@ export async function handleAutoFillBeisen(params: AutoFillBeisenParams): Promis
|
||||
if (lastTextInput) {
|
||||
;(lastTextInput as HTMLElement).click()
|
||||
lastTextInput.focus()
|
||||
await delay(50)
|
||||
await delay("low")
|
||||
lastTextInput.blur()
|
||||
} else {
|
||||
if (document.activeElement instanceof HTMLElement) {
|
||||
@@ -431,7 +432,7 @@ export async function handleAutoFillBeisen(params: AutoFillBeisenParams): Promis
|
||||
}
|
||||
document.body.click()
|
||||
}
|
||||
await delay(150)
|
||||
await delay("mid")
|
||||
|
||||
if (!f.isPicker && f.inputElement) lastTextInput = f.inputElement
|
||||
}
|
||||
@@ -1322,7 +1323,7 @@ async function b2FillDateTimeField(
|
||||
// 和阶段A同样的方式:点击 input 展开日期面板,然后直接调 fillDatePicker
|
||||
inputEl.focus()
|
||||
;(inputEl as HTMLElement).click()
|
||||
await delay(150)
|
||||
await delay("mid")
|
||||
|
||||
const ok = await fillDatePicker(field)
|
||||
if (ok) {
|
||||
@@ -1388,21 +1389,21 @@ async function b2FillBeisenRadioGroup(
|
||||
const childEl = item.firstElementChild as HTMLElement | null
|
||||
if (childEl) {
|
||||
childEl.click()
|
||||
await delay(50)
|
||||
await delay("low")
|
||||
// 子子级也点一下确保触发
|
||||
const grandChildEl = childEl.firstElementChild as HTMLElement | null
|
||||
if (grandChildEl) {
|
||||
grandChildEl.click()
|
||||
await delay(50)
|
||||
await delay("low")
|
||||
}
|
||||
} else {
|
||||
// fallback:直接点击 radioItem 自身
|
||||
;(item as HTMLElement).click()
|
||||
await delay(50)
|
||||
await delay("low")
|
||||
}
|
||||
|
||||
// 4. 验证选中结果:检查子级是否出现 phoenix-radio--checked 类名
|
||||
await delay(100)
|
||||
await delay("mid")
|
||||
const checkedEl = item.querySelector(".phoenix-radio--checked")
|
||||
if (checkedEl) {
|
||||
// 选中成功,radio group 容器变 greenTwo
|
||||
@@ -1499,7 +1500,7 @@ async function handleFillCachedData(params: FillCachedDataParams): Promise<FillC
|
||||
const ok = await b2FillSingleField(inputEl, field.label, field.value, lang)
|
||||
if (ok) { result.success++; usedInputs.add(inputEl); setFieldHighlight(inputEl, "greenTwo") } else { result.failed++ }
|
||||
}
|
||||
await delay(100)
|
||||
await delay("mid")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -1549,7 +1550,7 @@ async function handleFillCachedData(params: FillCachedDataParams): Promise<FillC
|
||||
}).length
|
||||
const clicked = await clickAddButton(addBtn, titleEl, nextTitleEl, beforeCount)
|
||||
if (!clicked) { console.log(` [B2] "${sectionData.title}" 点击添加按钮无效`); break }
|
||||
await delay(150)
|
||||
await delay("mid")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1571,7 +1572,7 @@ async function handleFillCachedData(params: FillCachedDataParams): Promise<FillC
|
||||
const ok = await b2FillSingleField(inputEl, field.label, field.value, lang)
|
||||
if (ok) { result.success++; usedInputs.add(inputEl); setFieldHighlight(inputEl, "greenTwo") } else { result.failed++ }
|
||||
}
|
||||
await delay(100)
|
||||
await delay("mid")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1600,7 +1601,7 @@ async function handleFillCachedData(params: FillCachedDataParams): Promise<FillC
|
||||
const radioOk = await b2FillBeisenRadioGroup(field.label, field.value, titleEl, nextTitleEl)
|
||||
if (radioOk) { result.success++ } else { result.skipped++ }
|
||||
}
|
||||
await delay(100)
|
||||
await delay("mid")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1661,7 +1662,7 @@ export async function fillBeisenCascadePicker(
|
||||
inputEl.scrollIntoView({ block: "center", behavior: "instant" })
|
||||
;(inputEl as HTMLElement).click()
|
||||
inputEl.focus()
|
||||
await delay(400)
|
||||
await delay("high")
|
||||
|
||||
// 2. 在页面中找 common-unmodeled-layer 面板(取最后一个,即最新弹出的)
|
||||
const allPanels = document.querySelectorAll(".common-unmodeled-layer")
|
||||
@@ -1695,7 +1696,7 @@ export async function fillBeisenCascadePicker(
|
||||
const clearBtn = panelEl.querySelector(".clear-select-data") as HTMLElement | null
|
||||
if (clearBtn) {
|
||||
clearBtn.click()
|
||||
await delay(200) // 等待清空操作生效
|
||||
await delay("mid") // 等待清空操作生效
|
||||
console.log(`OfferPie: [北森级联] "${labelText}" 已清空已选内容`)
|
||||
}
|
||||
|
||||
@@ -1704,7 +1705,7 @@ export async function fillBeisenCascadePicker(
|
||||
forceSetValue(searchInput, fillValue)
|
||||
searchInput.dispatchEvent(new Event("input", { bubbles: true }))
|
||||
searchInput.dispatchEvent(new Event("change", { bubbles: true }))
|
||||
await delay(300) // 等待搜索结果刷新(太短会导致选项列表未更新,选到错误选项)
|
||||
await delay("mid") // 等待搜索结果刷新(太短会导致选项列表未更新,选到错误选项)
|
||||
|
||||
// 6. 检测选项集合位置(两种情况)
|
||||
// 在 left-container 下查找
|
||||
@@ -1728,7 +1729,7 @@ export async function fillBeisenCascadePicker(
|
||||
const svgEl = iconSpan.querySelector("svg") as unknown as HTMLElement | null
|
||||
if (svgEl) svgEl.dispatchEvent(new MouseEvent("click", { bubbles: true }))
|
||||
optionClicked = true
|
||||
await delay(200)
|
||||
await delay("mid")
|
||||
console.log(`OfferPie: [北森级联] "${labelText}" 已点击第一个选项的 icon-container + svg`)
|
||||
}
|
||||
}
|
||||
@@ -1754,7 +1755,7 @@ export async function fillBeisenCascadePicker(
|
||||
const svgEl = iconSpan.querySelector("svg") as unknown as HTMLElement | null
|
||||
if (svgEl) svgEl.dispatchEvent(new MouseEvent("click", { bubbles: true }))
|
||||
optionClicked = true
|
||||
await delay(200)
|
||||
await delay("mid")
|
||||
console.log(`OfferPie: [北森级联] "${labelText}" 已点击第一个选项的 icon-container + svg`)
|
||||
}
|
||||
}
|
||||
@@ -1822,7 +1823,7 @@ export async function fillBeisenCascadePicker(
|
||||
}
|
||||
}
|
||||
|
||||
await delay(300)
|
||||
await delay("mid")
|
||||
console.log(`OfferPie: [北森级联] "${labelText}" 已点击确定按钮`)
|
||||
} else {
|
||||
console.log(`OfferPie: [北森级联] "${labelText}" 未找到确定按钮`)
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
* 不要在此文件中自行编写选择器操作逻辑,必须引用 lib 中已封装的方法。
|
||||
*/
|
||||
|
||||
import { fillMatchedField, delay, isSearchPickerField, fillSearchPickerField, isTimePeriodField, isTimeSingleField, fillTimePeriodField, fillTimeSingleField } from "~lib/autofill"
|
||||
import { fillMatchedField, isSearchPickerField, fillSearchPickerField, isTimePeriodField, isTimeSingleField, fillTimePeriodField, fillTimeSingleField } from "~lib/autofill"
|
||||
import { delay } from "~utils/delay"
|
||||
import { extractDomStructure, detectPageLanguage, isJobApplicationForm, buildSelector } from "~lib/dom"
|
||||
import { matchFormFieldsInRange, matchMainFields } from "~lib/formMatcher"
|
||||
import { detectPickerField } from "~lib/pickerDetector"
|
||||
@@ -103,7 +104,7 @@ export async function handleAutoFillCommon(params: AutoFillCommonParams): Promis
|
||||
// const resumeUrl = "https://offerpie.oss-cn-guangzhou.aliyuncs.com/%E5%AE%BE%E5%A4%95%E6%B3%95%E5%B0%BC%E4%BA%9A%E5%A4%A7%E5%AD%A6_%E4%B8%81%E5%B1%B9%E6%B6%B5.pdf"
|
||||
// const uploaded = await detectAndUploadResume(resumeUrl)
|
||||
// console.log(`===== OfferPie: 简历上传 ${uploaded ? "成功" : "跳过(未找到上传按钮或失败)"} =====`)
|
||||
// if (uploaded) await delay(1000) // 等待网站解析简历
|
||||
// if (uploaded) await delay("high") // 等待网站解析简历
|
||||
|
||||
// 4.5 定位经历区块并统计已展开段数
|
||||
const sectionResults = locateExperienceSections(document.body, lang)
|
||||
@@ -331,7 +332,7 @@ export async function handleAutoFillCommon(params: AutoFillCommonParams): Promis
|
||||
const 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(100)
|
||||
await delay("mid")
|
||||
continue
|
||||
} else {
|
||||
await detectPickerField(f, lang)
|
||||
@@ -346,7 +347,7 @@ export async function handleAutoFillCommon(params: AutoFillCommonParams): Promis
|
||||
if (lastTextInput && !lastTextInputIsPicker) {
|
||||
;(lastTextInput as HTMLElement).click()
|
||||
lastTextInput.focus()
|
||||
await delay(50)
|
||||
await delay("low")
|
||||
lastTextInput.blur()
|
||||
} else {
|
||||
if (document.activeElement instanceof HTMLElement) {
|
||||
@@ -357,7 +358,7 @@ export async function handleAutoFillCommon(params: AutoFillCommonParams): Promis
|
||||
}
|
||||
document.body.click()
|
||||
}
|
||||
await delay(150)
|
||||
await delay("mid")
|
||||
|
||||
if (!f.isPicker && f.inputElement && !isTimePeriodField(f.key) && !isTimeSingleField(f.key)) {
|
||||
lastTextInput = f.inputElement
|
||||
@@ -404,7 +405,7 @@ export async function handleAutoFillCommon(params: AutoFillCommonParams): Promis
|
||||
if (lastTextInput) {
|
||||
;(lastTextInput as HTMLElement).click()
|
||||
lastTextInput.focus()
|
||||
await delay(50)
|
||||
await delay("low")
|
||||
lastTextInput.blur()
|
||||
} else {
|
||||
if (document.activeElement instanceof HTMLElement) {
|
||||
@@ -415,7 +416,7 @@ export async function handleAutoFillCommon(params: AutoFillCommonParams): Promis
|
||||
}
|
||||
document.body.click()
|
||||
}
|
||||
await delay(150)
|
||||
await delay("mid")
|
||||
|
||||
if (!f.isPicker && f.inputElement) lastTextInput = f.inputElement
|
||||
}
|
||||
@@ -1257,7 +1258,7 @@ async function b2FillDateTimeField(
|
||||
// 和阶段A同样的方式:点击 input 展开日期面板,然后直接调 fillDatePicker
|
||||
inputEl.focus()
|
||||
;(inputEl as HTMLElement).click()
|
||||
await delay(150)
|
||||
await delay("mid")
|
||||
|
||||
const ok = await fillDatePicker(field)
|
||||
if (ok) {
|
||||
@@ -1339,7 +1340,7 @@ async function handleFillCachedData(params: FillCachedDataParams): Promise<FillC
|
||||
const ok = await b2FillSingleField(inputEl, field.label, field.value, lang)
|
||||
if (ok) { result.success++; usedInputs.add(inputEl); setFieldHighlight(inputEl, "greenTwo") } else { result.failed++ }
|
||||
}
|
||||
await delay(100)
|
||||
await delay("mid")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -1389,7 +1390,7 @@ async function handleFillCachedData(params: FillCachedDataParams): Promise<FillC
|
||||
}).length
|
||||
const clicked = await clickAddButton(addBtn, titleEl, nextTitleEl, beforeCount)
|
||||
if (!clicked) { console.log(` [B2] "${sectionData.title}" 点击添加按钮无效`); break }
|
||||
await delay(150)
|
||||
await delay("mid")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1407,7 +1408,7 @@ async function handleFillCachedData(params: FillCachedDataParams): Promise<FillC
|
||||
const ok = await b2FillSingleField(inputEl, field.label, field.value, lang)
|
||||
if (ok) { result.success++; usedInputs.add(inputEl); setFieldHighlight(inputEl, "greenTwo") } else { result.failed++ }
|
||||
}
|
||||
await delay(100)
|
||||
await delay("mid")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1426,7 +1427,7 @@ async function handleFillCachedData(params: FillCachedDataParams): Promise<FillC
|
||||
const ok = await b2FillSingleField(inputEl, field.label, field.value, lang)
|
||||
if (ok) { result.success++; usedInputs.add(inputEl); setFieldHighlight(inputEl, "greenTwo") } else { result.failed++ }
|
||||
}
|
||||
await delay(100)
|
||||
await delay("mid")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+35
-39
@@ -7,14 +7,10 @@ import type { MatchedFormField } from "./types"
|
||||
import { findAndClickOptionInVisiblePopups, clickBestOptionInDropdown } from "./pickerFill"
|
||||
import { fillDatePicker } from "./datePicker"
|
||||
import { JOB_FORM_LABELS } from "./constants"
|
||||
import { delay } from "~utils/delay"
|
||||
|
||||
// ============ 工具函数 ============
|
||||
|
||||
/** 延时工具函数 */
|
||||
export function delay(ms: number) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms))
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用的值写入方法(适配各种前端框架的受控组件)
|
||||
* 优先用 execCommand 模拟真实输入,失败则 fallback 到原生 setter
|
||||
@@ -60,12 +56,12 @@ export function forceSetValue(el: HTMLInputElement | HTMLTextAreaElement, value:
|
||||
/** 关闭弹出层 */
|
||||
export async function closePopup(inputEl: HTMLElement) {
|
||||
inputEl.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape", code: "Escape", keyCode: 27, bubbles: true }))
|
||||
await delay(100)
|
||||
await delay("mid")
|
||||
inputEl.blur()
|
||||
inputEl.dispatchEvent(new Event("blur", { bubbles: true }))
|
||||
await delay(100)
|
||||
await delay("mid")
|
||||
document.body.click()
|
||||
await delay(200)
|
||||
await delay("mid")
|
||||
}
|
||||
|
||||
// ============ 字段填写 ============
|
||||
@@ -103,7 +99,7 @@ export async function fillMatchedField(field: MatchedFormField): Promise<boolean
|
||||
if (inputElement instanceof HTMLInputElement && inputElement.type === "file") { console.warn(`OfferPie: ⏭ "${labelText}" 是文件上传框,跳过`); return false }
|
||||
if (isPicker) return await fillPickerField(field)
|
||||
inputElement.focus()
|
||||
await delay(50)
|
||||
await delay("low")
|
||||
forceSetValue(inputElement, fillValue)
|
||||
console.log(`OfferPie: ✅ [输入框] 已填写 "${labelText}" = "${fillValue}"`)
|
||||
return true
|
||||
@@ -167,9 +163,9 @@ async function fillPickerField(field: MatchedFormField): Promise<boolean> {
|
||||
// 搜不到时再补充搜索页面上其他可见弹出层(处理嵌套容器),最终都搜不到才关闭后走步骤1-4。
|
||||
if (!isDateValue && field.pickerDropdownElement) {
|
||||
const existingPopupEl = field.pickerDropdownElement
|
||||
await delay(100) // 等待弹出层渲染完成
|
||||
await delay("mid") // 等待弹出层渲染完成
|
||||
if (clickBestOptionInDropdown(existingPopupEl, fillValue)) {
|
||||
await delay(300); await closePopup(inputElement)
|
||||
await delay("mid"); await closePopup(inputElement)
|
||||
console.log(`OfferPie: ✅ [选择器] 已选择 "${labelText}" = "${fillValue}" (已有弹出层)`)
|
||||
return true
|
||||
}
|
||||
@@ -181,7 +177,7 @@ async function fillPickerField(field: MatchedFormField): Promise<boolean> {
|
||||
const htmlEl = el as HTMLElement
|
||||
if (htmlEl.offsetHeight > 0 && htmlEl.offsetWidth > 0 && htmlEl !== existingPopupEl) {
|
||||
if (clickBestOptionInDropdown(htmlEl, fillValue)) {
|
||||
await delay(300); await closePopup(inputElement)
|
||||
await delay("mid"); await closePopup(inputElement)
|
||||
console.log(`OfferPie: ✅ [选择器] 已选择 "${labelText}" = "${fillValue}" (嵌套弹出层)`)
|
||||
return true
|
||||
}
|
||||
@@ -189,7 +185,7 @@ async function fillPickerField(field: MatchedFormField): Promise<boolean> {
|
||||
}
|
||||
// 仍然没找到,关闭弹出层后走步骤1-4 重新展开
|
||||
await closePopup(inputElement)
|
||||
await delay(200)
|
||||
await delay("mid")
|
||||
}
|
||||
|
||||
// ---- 步骤1:记录点击前的 DOM 快照 ----
|
||||
@@ -218,7 +214,7 @@ async function fillPickerField(field: MatchedFormField): Promise<boolean> {
|
||||
clickTargets[i].click()
|
||||
}
|
||||
inputElement.focus()
|
||||
await delay(300)
|
||||
await delay("mid")
|
||||
|
||||
if (isDateValue) return await fillDatePicker(field)
|
||||
|
||||
@@ -266,7 +262,7 @@ async function fillPickerField(field: MatchedFormField): Promise<boolean> {
|
||||
if (dropdownEl) {
|
||||
console.log(`OfferPie: [fillPicker] "${labelText}" 找到弹出层: ${dropdownEl.tagName}.${(dropdownEl.className || "").toString().split(" ")[0]}`)
|
||||
if (clickBestOptionInDropdown(dropdownEl, fillValue)) {
|
||||
await delay(300); await closePopup(inputElement)
|
||||
await delay("mid"); await closePopup(inputElement)
|
||||
console.log(`OfferPie: ✅ [选择器] 已选择 "${labelText}" = "${fillValue}"`)
|
||||
return true
|
||||
}
|
||||
@@ -274,23 +270,23 @@ async function fillPickerField(field: MatchedFormField): Promise<boolean> {
|
||||
|
||||
// ---- 步骤5:fallback - 在弹出层中搜索(有弹出层在弹出层内搜,没有则全页面搜) ----
|
||||
if (findAndClickOptionInVisiblePopups(fillValue, labelText, dropdownEl)) {
|
||||
await delay(300); await closePopup(inputElement)
|
||||
await delay("mid"); await closePopup(inputElement)
|
||||
console.log(`OfferPie: ✅ [选择器] 已选择 "${labelText}" = "${fillValue}"`)
|
||||
return true
|
||||
}
|
||||
|
||||
// ---- 步骤6:写入值触发搜索,再尝试一次 ----
|
||||
forceSetValue(inputElement, fillValue)
|
||||
await delay(500)
|
||||
await delay("high")
|
||||
if (findAndClickOptionInVisiblePopups(fillValue, labelText, dropdownEl)) {
|
||||
await delay(300); await closePopup(inputElement)
|
||||
await delay("mid"); await closePopup(inputElement)
|
||||
console.log(`OfferPie: ✅ [选择器] 已选择 "${labelText}" = "${fillValue}"`)
|
||||
return true
|
||||
}
|
||||
|
||||
// ---- 步骤7:兜底 Enter 确认 ----
|
||||
inputElement.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", code: "Enter", keyCode: 13, bubbles: true }))
|
||||
await delay(300)
|
||||
await delay("mid")
|
||||
await closePopup(inputElement)
|
||||
console.log(`OfferPie: ⚠️ [选择器] 未找到匹配选项,已兜底填写 "${labelText}" = "${fillValue}"`)
|
||||
return true
|
||||
@@ -305,7 +301,7 @@ export async function fillAllFields(fields: MatchedFormField[]): Promise<{ total
|
||||
if (field.inputType !== "radio" && !field.inputElement) { result.skipped++; continue }
|
||||
const ok = await fillMatchedField(field)
|
||||
if (ok) result.success++; else result.failed++
|
||||
await delay(200)
|
||||
await delay("mid")
|
||||
}
|
||||
console.log(`===== OfferPie: 自动填写完毕 总计 ${result.total} | 成功 ${result.success} | 失败 ${result.failed} | 跳过 ${result.skipped} =====`)
|
||||
return result
|
||||
@@ -353,7 +349,7 @@ export async function fillSearchPickerField(field: MatchedFormField): Promise<bo
|
||||
// 2. 点击 input 获取焦点并输入值
|
||||
inputElement.focus()
|
||||
;(inputElement as HTMLElement).click()
|
||||
await delay(200)
|
||||
await delay("mid")
|
||||
forceSetValue(inputElement, fillValue)
|
||||
|
||||
// 3. 等待弹出层出现(异步下拉字段轮询等待,普通字段固定等待)
|
||||
@@ -363,7 +359,7 @@ export async function fillSearchPickerField(field: MatchedFormField): Promise<bo
|
||||
// 接口异步下拉:轮询等待弹出层出现,最多等3秒
|
||||
console.log(`OfferPie: [搜索选择器] "${labelText}" 异步下拉字段,等待接口返回...`)
|
||||
for (let i = 0; i < 10; i++) {
|
||||
await delay(200)
|
||||
await delay("mid")
|
||||
// 检查是否有新弹出层
|
||||
for (const child of Array.from(document.body.children)) {
|
||||
if (!beforeBodyChildren.has(child) && (child as HTMLElement).offsetHeight > 10) {
|
||||
@@ -385,7 +381,7 @@ export async function fillSearchPickerField(field: MatchedFormField): Promise<bo
|
||||
if (hasNewPopup) break
|
||||
}
|
||||
} else {
|
||||
await delay(401)
|
||||
await delay("high")
|
||||
// 检查 body 下新增的子元素
|
||||
for (const child of Array.from(document.body.children)) {
|
||||
if (!beforeBodyChildren.has(child) && (child as HTMLElement).offsetHeight > 10) {
|
||||
@@ -414,19 +410,19 @@ export async function fillSearchPickerField(field: MatchedFormField): Promise<bo
|
||||
console.log(`OfferPie: ✅ [搜索选择器→普通输入] "${labelText}" 无弹出层,已直接填写 "${fillValue}"`)
|
||||
inputElement.dispatchEvent(new Event("blur", { bubbles: true }))
|
||||
// 调试:blur 后再验证
|
||||
await delay(50)
|
||||
await delay("low")
|
||||
console.log(`OfferPie: [搜索选择器→普通输入] "${labelText}" blur后验证: value="${inputElement.value}"`)
|
||||
return true
|
||||
}
|
||||
|
||||
// 5. 有弹出层 → 走搜索选择器逻辑,等待搜索结果
|
||||
await delay(500)
|
||||
await delay("high")
|
||||
|
||||
// 【注意】对异步下拉字段,必须优先在检测到的弹出层元素内直接做文字匹配
|
||||
// 不能走 tryClickDropdownListItem 全页面搜——异步下拉可能只有1-2个选项,全页面搜会被其他同规格元素干扰
|
||||
if (isAsyncDropdown && newPopupEl) {
|
||||
if (clickBestOptionInDropdown(newPopupEl, fillValue)) {
|
||||
await delay(300)
|
||||
await delay("mid")
|
||||
await closePopup(inputElement)
|
||||
console.log(`OfferPie: ✅ [搜索选择器] 已选择 "${labelText}" = "${fillValue}"`)
|
||||
return true
|
||||
@@ -436,16 +432,16 @@ export async function fillSearchPickerField(field: MatchedFormField): Promise<bo
|
||||
// 在弹出层中查找匹配选项
|
||||
const { findAndClickOptionInVisiblePopups } = await import("./pickerFill")
|
||||
if (findAndClickOptionInVisiblePopups(fillValue, labelText, newPopupEl)) {
|
||||
await delay(300)
|
||||
await delay("mid")
|
||||
await closePopup(inputElement)
|
||||
console.log(`OfferPie: ✅ [搜索选择器] 已选择 "${labelText}" = "${fillValue}"`)
|
||||
return true
|
||||
}
|
||||
|
||||
// 6. 再等一会重试(有些网站接口慢)
|
||||
await delay(600)
|
||||
await delay("high")
|
||||
if (findAndClickOptionInVisiblePopups(fillValue, labelText, newPopupEl)) {
|
||||
await delay(300)
|
||||
await delay("mid")
|
||||
await closePopup(inputElement)
|
||||
console.log(`OfferPie: ✅ [搜索选择器] 已选择 "${labelText}" = "${fillValue}"(重试)`)
|
||||
return true
|
||||
@@ -522,7 +518,7 @@ async function fillYearMonthPicker(inputEl: HTMLInputElement | HTMLTextAreaEleme
|
||||
// 点击 input 展开下拉
|
||||
inputEl.focus()
|
||||
;(inputEl as HTMLElement).click()
|
||||
await delay(300)
|
||||
await delay("mid")
|
||||
|
||||
// 检测新出现的弹出层
|
||||
let dropdownEl: HTMLElement | null = null
|
||||
@@ -551,14 +547,14 @@ async function fillYearMonthPicker(inputEl: HTMLInputElement | HTMLTextAreaEleme
|
||||
if (dropdownEl) {
|
||||
// 在弹出层中精确匹配选项并点击
|
||||
if (clickBestOptionInDropdown(dropdownEl, value)) {
|
||||
await delay(200)
|
||||
await delay("mid")
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// 兜底:直接写入值
|
||||
forceSetValue(inputEl, value)
|
||||
await delay(100)
|
||||
await delay("mid")
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -630,10 +626,10 @@ export async function fillTimePeriodField(
|
||||
usedInputs.add(yearMonthInputs[3])
|
||||
}
|
||||
|
||||
await delay(200)
|
||||
await delay("mid")
|
||||
// 点击空白关闭弹窗
|
||||
document.body.click()
|
||||
await delay(100)
|
||||
await delay("mid")
|
||||
|
||||
console.log(`OfferPie: ✅ [时间选择器] "${field.labelText}" 已填写 ${start.year}.${start.month} - ${end.year}.${end.month}`)
|
||||
return true
|
||||
@@ -649,7 +645,7 @@ export async function fillTimePeriodField(
|
||||
usedInputs.add(yearMonthInputs[1])
|
||||
}
|
||||
document.body.click()
|
||||
await delay(100)
|
||||
await delay("mid")
|
||||
console.log(`OfferPie: ✅ [时间选择器] "${field.labelText}" 已填写 ${start.year}.${start.month}`)
|
||||
return true
|
||||
}
|
||||
@@ -671,14 +667,14 @@ export async function fillTimePeriodField(
|
||||
}
|
||||
startInput.focus()
|
||||
;(startInput as HTMLElement).click()
|
||||
await delay(300)
|
||||
await delay("mid")
|
||||
const startOk = await fillDatePicker(startField)
|
||||
if (startOk) {
|
||||
usedInputs.add(startInput)
|
||||
console.log(`OfferPie: [时间选择器] "${field.labelText}" 开始时间填写成功: ${startDate}`)
|
||||
}
|
||||
|
||||
await delay(500)
|
||||
await delay("high")
|
||||
|
||||
// 从开始时间 input 往后找最近的下一个 input 作为结束时间
|
||||
if (endDate) {
|
||||
@@ -703,7 +699,7 @@ export async function fillTimePeriodField(
|
||||
}
|
||||
endInput.focus()
|
||||
;(endInput as HTMLElement).click()
|
||||
await delay(300)
|
||||
await delay("mid")
|
||||
const endOk = await fillDatePicker(endField)
|
||||
if (endOk) {
|
||||
usedInputs.add(endInput)
|
||||
@@ -768,7 +764,7 @@ export async function fillTimeSingleField(
|
||||
usedInputs.add(yearMonthInputs[1])
|
||||
}
|
||||
document.body.click()
|
||||
await delay(100)
|
||||
await delay("mid")
|
||||
console.log(`OfferPie: ✅ [时间选择器] "${field.labelText}" 已填写 ${year}.${month}`)
|
||||
return true
|
||||
}
|
||||
|
||||
+27
-26
@@ -14,7 +14,8 @@
|
||||
*/
|
||||
|
||||
import type { MatchedFormField } from "./types"
|
||||
import { delay, forceSetValue, closePopup } from "./autofill"
|
||||
import { forceSetValue, closePopup } from "./autofill"
|
||||
import { delay } from "~utils/delay"
|
||||
|
||||
// ============ 常量 ============
|
||||
|
||||
@@ -560,7 +561,7 @@ async function detectNavButtons(panel: CalendarPanelInfo): Promise<NavButtons> {
|
||||
|
||||
// 点击按钮
|
||||
btn.click()
|
||||
await delay(150)
|
||||
await delay("mid")
|
||||
|
||||
const afterYear = readCurrentYear(panel)
|
||||
const afterMonth = readCurrentMonth(panel)
|
||||
@@ -626,21 +627,21 @@ async function detectNavButtons(panel: CalendarPanelInfo): Promise<NavButtons> {
|
||||
|
||||
if (restoreBtn) {
|
||||
restoreBtn.click()
|
||||
await delay(150)
|
||||
await delay("mid")
|
||||
} else {
|
||||
// 没有已知反向按钮,尝试用位置对称的按钮恢复
|
||||
// 如果当前按钮在前半部分,对称按钮在后半部分
|
||||
const symmetricIdx = buttons.length - 1 - i
|
||||
if (symmetricIdx !== i && symmetricIdx >= 0 && symmetricIdx < buttons.length) {
|
||||
buttons[symmetricIdx].click()
|
||||
await delay(150)
|
||||
await delay("mid")
|
||||
// 验证是否恢复
|
||||
const checkYear = readCurrentYear(panel)
|
||||
const checkMonth = readCurrentMonth(panel)
|
||||
if (checkYear !== beforeYear || checkMonth !== beforeMonth) {
|
||||
// 没恢复成功,再点一次原按钮的对称位置
|
||||
buttons[symmetricIdx].click()
|
||||
await delay(150)
|
||||
await delay("mid")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -684,7 +685,7 @@ async function restoreToDate(
|
||||
if (yearBtn) {
|
||||
for (let i = 0; i < Math.abs(yearDiff) && i < 50; i++) {
|
||||
yearBtn.click()
|
||||
await delay(80)
|
||||
await delay("low")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -700,7 +701,7 @@ async function restoreToDate(
|
||||
if (monthBtn) {
|
||||
for (let i = 0; i < Math.abs(monthDiff) && i < 24; i++) {
|
||||
monthBtn.click()
|
||||
await delay(80)
|
||||
await delay("low")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -731,7 +732,7 @@ async function navigateToYearMonth(
|
||||
console.log(`OfferPie: [datePicker] 年份需要${yearDiff > 0 ? "增加" : "减少"} ${steps} 年`)
|
||||
for (let i = 0; i < steps && i < 100; i++) {
|
||||
yearBtn.click()
|
||||
await delay(100)
|
||||
await delay("mid")
|
||||
// 验证年份确实变了
|
||||
const newYear = readCurrentYear(panel)
|
||||
if (newYear === targetYear) break
|
||||
@@ -744,7 +745,7 @@ async function navigateToYearMonth(
|
||||
console.log(`OfferPie: [datePicker] 无年份按钮,用月份按钮跨 ${Math.abs(totalMonthDiff)} 个月`)
|
||||
for (let i = 0; i < Math.abs(totalMonthDiff) && i < 200; i++) {
|
||||
monthBtn.click()
|
||||
await delay(60)
|
||||
await delay("low")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -770,7 +771,7 @@ async function navigateToYearMonth(
|
||||
console.log(`OfferPie: [datePicker] 月份需要${monthDiff > 0 ? "增加" : "减少"} ${steps} 月`)
|
||||
for (let i = 0; i < steps && i < 24; i++) {
|
||||
btn.click()
|
||||
await delay(100)
|
||||
await delay("mid")
|
||||
const newMonth = readCurrentMonth(panel)
|
||||
if (newMonth === targetMonth) break
|
||||
}
|
||||
@@ -887,7 +888,7 @@ async function tryFillMonthPanel(
|
||||
console.log(`OfferPie: [datePicker] 月份面板年份导航: ${yearDiff > 0 ? "+" : ""}${yearDiff}年`)
|
||||
for (let i = 0; i < steps && i < 50; i++) {
|
||||
yearBtn.click()
|
||||
await delay(150)
|
||||
await delay("mid")
|
||||
const newYear = readCurrentYear(tempPanel)
|
||||
if (newYear === targetYear) break
|
||||
}
|
||||
@@ -901,7 +902,7 @@ async function tryFillMonthPanel(
|
||||
const steps = Math.abs(yearDiff)
|
||||
for (let i = 0; i < steps && i < 50; i++) {
|
||||
btn.click()
|
||||
await delay(150)
|
||||
await delay("mid")
|
||||
const newYear = readCurrentYear(tempPanel)
|
||||
if (newYear === targetYear) break
|
||||
}
|
||||
@@ -926,9 +927,9 @@ async function tryFillMonthPanel(
|
||||
if (inner && isVisible(inner)) deepest = inner as HTMLElement
|
||||
|
||||
deepest.click()
|
||||
await delay(100)
|
||||
await delay("mid")
|
||||
if (deepest !== cell) cell.click()
|
||||
await delay(300)
|
||||
await delay("mid")
|
||||
await closePopup(inputElement)
|
||||
console.log(`OfferPie: ✅ [月份选择器] 已选择 "${labelText}" = "${fillValue}" (${targetYear}年${targetMonth}月)`)
|
||||
return true
|
||||
@@ -1059,12 +1060,12 @@ async function clickDayCell(
|
||||
|
||||
// 三层点击:叶子 → 父级 → 原始元素
|
||||
deepest.click()
|
||||
await delay(50)
|
||||
await delay("low")
|
||||
if (deepest.parentElement) deepest.parentElement.click()
|
||||
await delay(50)
|
||||
await delay("low")
|
||||
if (cell !== deepest && cell !== deepest.parentElement) (cell as HTMLElement).click()
|
||||
|
||||
await delay(300)
|
||||
await delay("mid")
|
||||
await closePopup(inputElement)
|
||||
console.log(`OfferPie: ✅ [日期] 已选择 "${labelText}" = "${fillValue}" (日期 ${targetDay})`)
|
||||
return true
|
||||
@@ -1158,9 +1159,9 @@ export async function fillDatePicker(field: MatchedFormField): Promise<boolean>
|
||||
console.warn(`OfferPie: [datePicker] 无法解析日期值: "${fillValue}"`)
|
||||
// 兜底:直接写入值 + Enter
|
||||
forceSetValue(inputElement, fillValue)
|
||||
await delay(300)
|
||||
await delay("mid")
|
||||
inputElement.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", code: "Enter", keyCode: 13, bubbles: true }))
|
||||
await delay(300)
|
||||
await delay("mid")
|
||||
await closePopup(inputElement as HTMLElement)
|
||||
return true
|
||||
}
|
||||
@@ -1170,7 +1171,7 @@ export async function fillDatePicker(field: MatchedFormField): Promise<boolean>
|
||||
|
||||
// ---- 步骤2:写入日期字符串(触发面板联动) ----
|
||||
forceSetValue(inputElement, fillValue)
|
||||
await delay(500)
|
||||
await delay("high")
|
||||
|
||||
// ---- 步骤2.5:如果只有年月(day=0),检测是否为月份面板 ----
|
||||
if (targetDay === 0) {
|
||||
@@ -1208,13 +1209,13 @@ export async function fillDatePicker(field: MatchedFormField): Promise<boolean>
|
||||
if (targetDay === 0) {
|
||||
// 只需要年月,不需要点日期格子。尝试点击月份面板中的月份
|
||||
// 如果导航成功了,可能面板已经关闭(某些组件选完月份自动关闭)
|
||||
await delay(200)
|
||||
await delay("mid")
|
||||
await closePopup(inputElement as HTMLElement)
|
||||
console.log(`OfferPie: ✅ [日期] 已导航到 "${labelText}" = "${fillValue}" (${targetYear}年${targetMonth}月)`)
|
||||
return true
|
||||
}
|
||||
|
||||
await delay(200)
|
||||
await delay("mid")
|
||||
const clicked = await clickDayCell(panel, targetDay, labelText, fillValue, inputElement as HTMLElement)
|
||||
if (clicked) return true
|
||||
|
||||
@@ -1250,10 +1251,10 @@ async function fallbackClickDay(
|
||||
break
|
||||
}
|
||||
}
|
||||
deepest.click(); await delay(50)
|
||||
if (deepest.parentElement) deepest.parentElement.click(); await delay(50)
|
||||
deepest.click(); await delay("low")
|
||||
if (deepest.parentElement) deepest.parentElement.click(); await delay("low")
|
||||
if (cell !== deepest && cell !== deepest.parentElement) (cell as HTMLElement).click()
|
||||
await delay(300)
|
||||
await delay("mid")
|
||||
await closePopup(inputElement)
|
||||
console.log(`OfferPie: ✅ [日期-兜底] 已选择 "${labelText}" = "${fillValue}" (日期 ${targetDay})`)
|
||||
return true
|
||||
@@ -1263,7 +1264,7 @@ async function fallbackClickDay(
|
||||
// 最终兜底:Enter 确认
|
||||
console.warn(`OfferPie: ⚠ [日期] "${labelText}" 未找到日历格子,Enter 确认`)
|
||||
inputElement.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", code: "Enter", keyCode: 13, bubbles: true }))
|
||||
await delay(300)
|
||||
await delay("mid")
|
||||
await closePopup(inputElement)
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
*/
|
||||
|
||||
import { EXPERIENCE_SECTION_CONFIGS, JOB_FORM_LABELS } from "./constants"
|
||||
import { delay } from "./autofill"
|
||||
import { delay } from "~utils/delay"
|
||||
import { snapshotElementsInRange, diffSnapshots, findTopLevelNewElements } from "./dom"
|
||||
import type { ExperienceSection, ExperienceSectionConfig, ResumeData } from "./types"
|
||||
|
||||
@@ -766,7 +766,7 @@ export async function clickAddButton(
|
||||
// 逐层点击,每层点击后检测 input 数量是否增加
|
||||
for (const target of clickTargets) {
|
||||
target.click()
|
||||
await delay(400)
|
||||
await delay("high")
|
||||
|
||||
const afterInputs = findInputsInRange(titleEl, nextTitleEl)
|
||||
if (afterInputs.length > beforeInputCount) {
|
||||
@@ -1105,7 +1105,7 @@ export async function expandExperienceSections(
|
||||
|
||||
// 点击添加按钮
|
||||
const clickSuccess = await clickAddButton(addBtn, result.titleElement, nextTitleEl, beforeInputCount)
|
||||
await delay(300)
|
||||
await delay("mid")
|
||||
|
||||
if (clickSuccess) {
|
||||
// 通过 DOM 差异找出新增的顶层容器(即一段经历的完整 DOM)
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
import type { MatchedFormField, UILibPickerConfig } from "./types"
|
||||
import { UI_LIB_PICKER_CONFIGS } from "./constants"
|
||||
import { delay } from "./autofill"
|
||||
import { delay } from "~utils/delay"
|
||||
|
||||
/** 通过提示文字判断是否为选择器 */
|
||||
function hasPickerHintText(field: MatchedFormField, lang: "zh" | "en"): boolean {
|
||||
@@ -109,7 +109,7 @@ async function dismissPopup() {
|
||||
document.activeElement.blur()
|
||||
}
|
||||
document.body.click()
|
||||
await delay(200)
|
||||
await delay("mid")
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -142,7 +142,7 @@ async function detectByClick(field: MatchedFormField): Promise<boolean> {
|
||||
for (const target of clickTargets) {
|
||||
const before = snapshotDOM()
|
||||
target.click()
|
||||
await delay(200)
|
||||
await delay("mid")
|
||||
|
||||
if (hasNewPopup(before)) {
|
||||
console.log(`OfferPie: [${field.key}] 点击 ${target.tagName}.${(target.className || "").toString().split(" ")[0]} 后检测到弹出层`)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* 负责:检测页面上的简历上传按钮,从 OSS 下载简历文件并注入到 file input
|
||||
*/
|
||||
|
||||
import { delay } from "./autofill"
|
||||
import { delay } from "~utils/delay"
|
||||
|
||||
/**
|
||||
* 检测页面上的简历上传标签,并自动上传简历文件
|
||||
@@ -73,7 +73,7 @@ export async function detectAndUploadResume(resumeUrl: string): Promise<boolean>
|
||||
}
|
||||
for (const target of clickTargets) {
|
||||
target.click()
|
||||
await delay(300)
|
||||
await delay("mid")
|
||||
for (const input of Array.from(document.querySelectorAll('input[type="file"]'))) {
|
||||
const id = input.id?.toLowerCase() || ""
|
||||
const name = input.getAttribute("name")?.toLowerCase() || ""
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* 统一延时工具
|
||||
* 所有延时调用必须通过此模块,禁止在其他文件中直接使用 setTimeout 或自定义 delay
|
||||
*/
|
||||
|
||||
/** 延时等级 */
|
||||
export type DelayLevel = "low" | "mid" | "high" | "max"
|
||||
|
||||
/** 各等级对应的毫秒数(统一在此处调整) */
|
||||
const DELAY_MS: Record<DelayLevel, number> = {
|
||||
low: 50, // <100ms 场景:微等待,点击后极短暂停
|
||||
mid: 200, // 100~300ms 场景:等待 DOM 更新、弹出层渲染
|
||||
high: 500, // >300ms 场景:等待接口返回、搜索结果、动画完成
|
||||
max: 2000, // >2000ms 场景:特殊超长延时(谨慎使用)
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一延时函数
|
||||
* @param level 延时等级:low / mid / high / max
|
||||
*/
|
||||
export function delay(level: DelayLevel): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, DELAY_MS[level]))
|
||||
}
|
||||
Reference in New Issue
Block a user