From 69c77dc7ac2ca592b0b86635fbe719389ff4970a Mon Sep 17 00:00:00 2001 From: xuxin <15279969124@163.com> Date: Wed, 5 Aug 2026 17:35:31 +0800 Subject: [PATCH] =?UTF-8?q?=E8=BE=93=E5=85=A5=E6=A1=86=E4=BF=AE=E6=94=B9?= =?UTF-8?q?=E9=A2=9C=E8=89=B2=E5=BE=AE=E8=B0=83=EF=BC=8C=E5=BC=B9=E5=87=BA?= =?UTF-8?q?=E5=B1=82=E4=B8=8B=E6=8B=89=E9=80=89=E9=A1=B9=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E6=94=B6=E9=9B=86=E4=BF=AE=E6=94=B9=E5=85=B3=E9=97=AD=E5=BC=B9?= =?UTF-8?q?=E5=87=BA=E5=B1=82=E7=9A=84=E5=8A=9E=E6=B3=95=EF=BC=8C=E9=A3=9E?= =?UTF-8?q?=E4=B9=A6=E7=BB=8F=E5=8E=86=E6=B7=BB=E5=8A=A0=E6=8C=89=E9=92=AE?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E4=B8=93=E5=B1=9E=E5=85=BC=E5=AE=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/experienceSection.ts | 41 +++++++++++++++++++++++++++++++++++- src/lib/fillStats.ts | 27 ++++++++++++++++++++++-- src/lib/formStyle.ts | 2 +- 3 files changed, 66 insertions(+), 4 deletions(-) diff --git a/src/lib/experienceSection.ts b/src/lib/experienceSection.ts index 6efa7a7..83e1caf 100644 --- a/src/lib/experienceSection.ts +++ b/src/lib/experienceSection.ts @@ -16,6 +16,7 @@ import { EXPERIENCE_SECTION_CONFIGS, JOB_FORM_LABELS } from "./constants" import { delay } from "~utils/delay" +import { simulateClick } from "~utils/domEvent" import { snapshotElementsInRange, diffSnapshots, findTopLevelNewElements } from "./dom" import { findNearestInput } from "./formMatcher" import type { ExperienceSection, ExperienceSectionConfig, ResumeData } from "./types" @@ -732,12 +733,50 @@ function getDirectText(el: Element): string { * * 【注意】验证 fullText 不能包含其他经历类型的标题关键词,防止跨区域误匹配 */ + +/** + * 【飞书专用】在大标题到下一大标题范围内查找添加按钮 + * 飞书招聘平台的添加按钮使用 ud__button ud__button--text 类名的 button 标签 + * 通用 findAddButton 在飞书上第二次定位时会误匹配到包含字段文字的大容器,因此飞书走此独立逻辑 + * + * @param titleEl - 当前经历区块的大标题元素 + * @param nextTitleEl - 下一个大标题元素(范围边界) + * @returns 找到的添加按钮 button 元素,未找到返回 null + */ +function findFeishuAddButton(titleEl: Element, nextTitleEl: Element | null): Element | null { + // 在范围内查找所有 button.ud__button.ud__button--text + const allButtons = document.querySelectorAll("button.ud__button.ud__button--text") + for (const btn of Array.from(allButtons)) { + // 判断是否在 titleEl 之后 + const afterTitle = !!(titleEl.compareDocumentPosition(btn) & Node.DOCUMENT_POSITION_FOLLOWING) + if (!afterTitle) continue + // 判断是否在 nextTitleEl 之前(如果有下一大标题) + if (nextTitleEl) { + const beforeNext = !!(nextTitleEl.compareDocumentPosition(btn) & Node.DOCUMENT_POSITION_PRECEDING) + if (!beforeNext) continue + } + // 确认按钮文字含"添加"关键词(排除删除等其他操作按钮) + const btnText = btn.textContent?.trim() || "" + if (btnText.includes("添加") || btnText.includes("Add")) { + return btn + } + } + return null +} + export function findAddButton( titleEl: Element, nextTitleEl: Element | null, config: ExperienceSectionConfig, lang: "zh" | "en" ): Element | null { + // 【飞书特有逻辑】飞书招聘平台的添加按钮是 button.ud__button.ud__button--text + // 通用逻辑在飞书上第二次查找会误匹配到整个段容器,因此飞书走独立的定位策略 + if (window.location.hostname.includes("feishu.cn")) { + const feishuBtn = findFeishuAddButton(titleEl, nextTitleEl) + if (feishuBtn) return feishuBtn + } + const addKeywords = lang === "zh" ? config.addButtonZh : config.addButtonEn // 当前经历类型的标题关键词(用于验证按钮确实属于本区块) const sectionKeywords = lang === "zh" ? config.zh : config.en @@ -842,7 +881,7 @@ export async function clickAddButton( // 逐层点击,每层点击后检测 input 数量是否增加 for (const target of clickTargets) { - target.click() + simulateClick(target) await delay("high") const afterInputs = findInputsInRange(titleEl, nextTitleEl) diff --git a/src/lib/fillStats.ts b/src/lib/fillStats.ts index 3ed54a0..5fb19fa 100644 --- a/src/lib/fillStats.ts +++ b/src/lib/fillStats.ts @@ -15,6 +15,7 @@ import type { MatchedFormField, UnmatchedFormField } from "./types" import { isRequiredField, setFieldHighlight } from "./formStyle" import { findLabelForInput } from "./labelFinder" import { delay } from "~utils/delay" +import { simulateClick } from "~utils/domEvent" // ==================================================================== // 一、类型定义 @@ -1236,12 +1237,34 @@ export async function collectPickerOptionTexts(fieldStats: TitleStat[]): Promise await delay("midH") } - // 循环结束后,点击 document.documentElement 关闭最后一个可能残留的弹出层 - document.documentElement.dispatchEvent(new MouseEvent("click", { bubbles: true, clientX: 0, clientY: 0 })) + // 循环结束后,点击第一个已识别字段的输入框,通过 simulateClick 完整事件链关闭所有残留弹出层 + const firstInputEl = findFirstVisibleInput(fieldStats) + if (firstInputEl) { + simulateClick(firstInputEl) + await delay("mid") + } return optionTexts } +/** + * 从字段统计列表中找到第一个可见的 input 元素 + * 用于点击关闭残留弹出层(simulateClick 触发完整事件链,兼容所有 UI 组件库) + * + * @param fieldStats - scanPageFields 返回的按大标题分组的字段统计数组 + * @returns 第一个可见的 input HTMLElement,找不到返回 null + */ +function findFirstVisibleInput(fieldStats: TitleStat[]): HTMLElement | null { + for (const ts of fieldStats) { + for (const f of ts.fields) { + if (!f.inputElement) continue + const el = f.inputElement as HTMLElement + if (el.offsetHeight > 0 && el.offsetWidth > 0) return el + } + } + return null +} + /** * 递归收集指定元素内所有可见叶子节点的文字 * 只收集有效文字(长度 2-30,至少含1个汉字或2个英文字母),排除纯数字/纯符号 diff --git a/src/lib/formStyle.ts b/src/lib/formStyle.ts index 055ea4b..504f013 100644 --- a/src/lib/formStyle.ts +++ b/src/lib/formStyle.ts @@ -12,7 +12,7 @@ const COLOR_MAP: Record = { green: "#b7ffc5", // 淡绿色 greenTwo: "#b7ffc6", // 淡绿色2 red: "#ffb7b7", // 淡红色 - yellow: "#fff", // 白色 + yellow: "transparent", // 透明色 } /** 常见的表单项容器选择器(与 formMatcher.ts 保持一致) */