去掉多余的类名常量规则,降低radio的操作优先级

This commit is contained in:
2026-08-05 15:54:02 +08:00
parent d5b273ebc0
commit e4e3a2683b
7 changed files with 64 additions and 673 deletions
+2 -96
View File
@@ -483,53 +483,8 @@ export async function handleAutoFillBeisen(params: AutoFillBeisenParams): Promis
}
if (inExcludeRange) continue
// 向上查找最近的标签文字
let labelText = ""
let labelElement: Element | null = null
// 策略1:查找 input 所在表单项容器内的标签
const formItemSelectors = [
".form-item", ".form-group", ".form-field",
".el-form-item", ".ant-form-item", ".ant-row",
".arco-form-item", ".t-form-item", ".n-form-item",
"[class*='form-item']", "[class*='form-group']", "[class*='formItem']",
]
let container: Element | null = null
for (const sel of formItemSelectors) {
container = inp.closest(sel)
if (container) break
}
if (container) {
const labelEls = container.querySelectorAll("label, span, div, td, th, p, legend, dt")
for (const el of Array.from(labelEls)) {
const directText = Array.from(el.childNodes)
.filter((n) => n.nodeType === Node.TEXT_NODE)
.map((n) => n.textContent?.trim())
.filter(Boolean)
.join("")
if (!directText || directText.length > 30) continue
if (EXCLUDE_LABEL_TEXTS.some((ex) => directText === ex)) continue
if (el.compareDocumentPosition(inp) & Node.DOCUMENT_POSITION_FOLLOWING) {
labelText = directText
labelElement = el
break
}
}
}
// 策略2:向前查找兄弟/父级中的标签
if (!labelText) {
let prev: Element | null = inp.previousElementSibling
for (let i = 0; i < 3 && prev; i++) {
const text = prev.textContent?.trim() || ""
if (text && text.length <= 20 && !EXCLUDE_LABEL_TEXTS.some((ex) => text === ex)) {
labelText = text
labelElement = prev
break
}
prev = prev.previousElementSibling
}
}
// 使用统一的 findLabelForInput 查找标签文字(纯 DOM 逆向遍历,不依赖类名)
const { labelText, labelElement } = findLabelForInput(inp)
if (!labelText || !labelElement) continue
@@ -620,12 +575,6 @@ export async function handleAutoFillBeisen(params: AutoFillBeisenParams): Promis
// 常量
const STAT_EXCLUDE_LABEL_TEXTS = ["请输入", "输入", ":", "", "请选择", "选择", "*", "必填"]
const INPUT_SEL_STAT = "input:not([type='hidden']):not([type='submit']):not([type='button']):not([type='checkbox']):not([type='radio']):not([type='file']):not([disabled]), textarea:not([disabled])"
const FORM_ITEM_SELS = [
".form-item", ".form-group", ".form-field",
".el-form-item", ".ant-form-item", ".ant-row",
".arco-form-item", ".t-form-item", ".n-form-item",
"[class*='form-item']", "[class*='form-group']", "[class*='formItem']",
]
/** 5大经历类型的 section 名集合(用于判断是否能从 expandedResults 取段落信息) */
const FIVE_EXP_SECTIONS = new Set(["education", "work", "internship", "project", "competition"])
@@ -696,49 +645,6 @@ export async function handleAutoFillBeisen(params: AutoFillBeisenParams): Promis
return { segmentCount: maxCount, fieldSegMap }
}
/**
* 查找标签文字的辅助函数
*/
function findLabelForInput(inp: Element): { labelText: string; labelElement: Element | null } {
let labelText = ""
let labelElement: Element | null = null
let container: Element | null = null
for (const sel of FORM_ITEM_SELS) {
container = inp.closest(sel)
if (container) break
}
if (container) {
const labelEls = container.querySelectorAll("label, span, div, td, th, p, legend, dt")
for (const el of Array.from(labelEls)) {
const directText = Array.from(el.childNodes)
.filter((n) => n.nodeType === Node.TEXT_NODE)
.map((n) => n.textContent?.trim())
.filter(Boolean)
.join("")
if (!directText || directText.length > 30) continue
if (STAT_EXCLUDE_LABEL_TEXTS.some((ex) => directText === ex)) continue
if (el.compareDocumentPosition(inp) & Node.DOCUMENT_POSITION_FOLLOWING) {
labelText = directText
labelElement = el
break
}
}
}
if (!labelText) {
let prev: Element | null = inp.previousElementSibling
for (let i = 0; i < 3 && prev; i++) {
const text = prev.textContent?.trim() || ""
if (text && text.length <= 20 && !STAT_EXCLUDE_LABEL_TEXTS.some((ex) => text === ex)) {
labelText = text
labelElement = prev
break
}
prev = prev.previousElementSibling
}
}
if (!labelText) labelText = (inp as HTMLInputElement).getAttribute("placeholder") || "(未知字段)"
return { labelText, labelElement }
}
// 按大标题分组统计
for (let tIdx = 0; tIdx < allTitles.length; tIdx++) {
+2 -96
View File
@@ -467,53 +467,8 @@ export async function handleAutoFillCommon(params: AutoFillCommonParams): Promis
}
if (inExcludeRange) continue
// 向上查找最近的标签文字
let labelText = ""
let labelElement: Element | null = null
// 策略1:查找 input 所在表单项容器内的标签
const formItemSelectors = [
".form-item", ".form-group", ".form-field",
".el-form-item", ".ant-form-item", ".ant-row",
".arco-form-item", ".t-form-item", ".n-form-item",
"[class*='form-item']", "[class*='form-group']", "[class*='formItem']",
]
let container: Element | null = null
for (const sel of formItemSelectors) {
container = inp.closest(sel)
if (container) break
}
if (container) {
const labelEls = container.querySelectorAll("label, span, div, td, th, p, legend, dt")
for (const el of Array.from(labelEls)) {
const directText = Array.from(el.childNodes)
.filter((n) => n.nodeType === Node.TEXT_NODE)
.map((n) => n.textContent?.trim())
.filter(Boolean)
.join("")
if (!directText || directText.length > 30) continue
if (EXCLUDE_LABEL_TEXTS.some((ex) => directText === ex)) continue
if (el.compareDocumentPosition(inp) & Node.DOCUMENT_POSITION_FOLLOWING) {
labelText = directText
labelElement = el
break
}
}
}
// 策略2:向前查找兄弟/父级中的标签
if (!labelText) {
let prev: Element | null = inp.previousElementSibling
for (let i = 0; i < 3 && prev; i++) {
const text = prev.textContent?.trim() || ""
if (text && text.length <= 20 && !EXCLUDE_LABEL_TEXTS.some((ex) => text === ex)) {
labelText = text
labelElement = prev
break
}
prev = prev.previousElementSibling
}
}
// 使用统一的 findLabelForInput 查找标签文字(纯 DOM 逆向遍历,不依赖类名)
const { labelText, labelElement } = findLabelForInput(inp)
if (!labelText || !labelElement) continue
@@ -604,12 +559,6 @@ export async function handleAutoFillCommon(params: AutoFillCommonParams): Promis
// 常量
const STAT_EXCLUDE_LABEL_TEXTS = ["请输入", "输入", ":", "", "请选择", "选择", "*", "必填"]
const INPUT_SEL_STAT = "input:not([type='hidden']):not([type='submit']):not([type='button']):not([type='checkbox']):not([type='radio']):not([type='file']):not([disabled]), textarea:not([disabled])"
const FORM_ITEM_SELS = [
".form-item", ".form-group", ".form-field",
".el-form-item", ".ant-form-item", ".ant-row",
".arco-form-item", ".t-form-item", ".n-form-item",
"[class*='form-item']", "[class*='form-group']", "[class*='formItem']",
]
/** 5大经历类型的 section 名集合(用于判断是否能从 expandedResults 取段落信息) */
const FIVE_EXP_SECTIONS = new Set(["education", "work", "internship", "project", "competition"])
@@ -680,49 +629,6 @@ export async function handleAutoFillCommon(params: AutoFillCommonParams): Promis
return { segmentCount: maxCount, fieldSegMap }
}
/**
* 查找标签文字的辅助函数
*/
function findLabelForInput(inp: Element): { labelText: string; labelElement: Element | null } {
let labelText = ""
let labelElement: Element | null = null
let container: Element | null = null
for (const sel of FORM_ITEM_SELS) {
container = inp.closest(sel)
if (container) break
}
if (container) {
const labelEls = container.querySelectorAll("label, span, div, td, th, p, legend, dt")
for (const el of Array.from(labelEls)) {
const directText = Array.from(el.childNodes)
.filter((n) => n.nodeType === Node.TEXT_NODE)
.map((n) => n.textContent?.trim())
.filter(Boolean)
.join("")
if (!directText || directText.length > 30) continue
if (STAT_EXCLUDE_LABEL_TEXTS.some((ex) => directText === ex)) continue
if (el.compareDocumentPosition(inp) & Node.DOCUMENT_POSITION_FOLLOWING) {
labelText = directText
labelElement = el
break
}
}
}
if (!labelText) {
let prev: Element | null = inp.previousElementSibling
for (let i = 0; i < 3 && prev; i++) {
const text = prev.textContent?.trim() || ""
if (text && text.length <= 20 && !STAT_EXCLUDE_LABEL_TEXTS.some((ex) => text === ex)) {
labelText = text
labelElement = prev
break
}
prev = prev.previousElementSibling
}
}
if (!labelText) labelText = (inp as HTMLInputElement).getAttribute("placeholder") || "(未知字段)"
return { labelText, labelElement }
}
// 按大标题分组统计
for (let tIdx = 0; tIdx < allTitles.length; tIdx++) {
+2 -96
View File
@@ -473,53 +473,8 @@ export async function handleAutoFillFeishu(params: AutoFillFeishuParams): Promis
}
if (inExcludeRange) continue
// 向上查找最近的标签文字
let labelText = ""
let labelElement: Element | null = null
// 策略1:查找 input 所在表单项容器内的标签
const formItemSelectors = [
".form-item", ".form-group", ".form-field",
".el-form-item", ".ant-form-item", ".ant-row",
".arco-form-item", ".t-form-item", ".n-form-item",
"[class*='form-item']", "[class*='form-group']", "[class*='formItem']",
]
let container: Element | null = null
for (const sel of formItemSelectors) {
container = inp.closest(sel)
if (container) break
}
if (container) {
const labelEls = container.querySelectorAll("label, span, div, td, th, p, legend, dt")
for (const el of Array.from(labelEls)) {
const directText = Array.from(el.childNodes)
.filter((n) => n.nodeType === Node.TEXT_NODE)
.map((n) => n.textContent?.trim())
.filter(Boolean)
.join("")
if (!directText || directText.length > 30) continue
if (EXCLUDE_LABEL_TEXTS.some((ex) => directText === ex)) continue
if (el.compareDocumentPosition(inp) & Node.DOCUMENT_POSITION_FOLLOWING) {
labelText = directText
labelElement = el
break
}
}
}
// 策略2:向前查找兄弟/父级中的标签
if (!labelText) {
let prev: Element | null = inp.previousElementSibling
for (let i = 0; i < 3 && prev; i++) {
const text = prev.textContent?.trim() || ""
if (text && text.length <= 20 && !EXCLUDE_LABEL_TEXTS.some((ex) => text === ex)) {
labelText = text
labelElement = prev
break
}
prev = prev.previousElementSibling
}
}
// 使用统一的 findLabelForInput 查找标签文字(纯 DOM 逆向遍历,不依赖类名)
const { labelText, labelElement } = findLabelForInput(inp)
if (!labelText || !labelElement) continue
@@ -610,12 +565,6 @@ export async function handleAutoFillFeishu(params: AutoFillFeishuParams): Promis
// 常量
const STAT_EXCLUDE_LABEL_TEXTS = ["请输入", "输入", ":", "", "请选择", "选择", "*", "必填"]
const INPUT_SEL_STAT = "input:not([type='hidden']):not([type='submit']):not([type='button']):not([type='checkbox']):not([type='radio']):not([type='file']):not([disabled]), textarea:not([disabled])"
const FORM_ITEM_SELS = [
".form-item", ".form-group", ".form-field",
".el-form-item", ".ant-form-item", ".ant-row",
".arco-form-item", ".t-form-item", ".n-form-item",
"[class*='form-item']", "[class*='form-group']", "[class*='formItem']",
]
/** 5大经历类型的 section 名集合(用于判断是否能从 expandedResults 取段落信息) */
const FIVE_EXP_SECTIONS = new Set(["education", "work", "internship", "project", "competition"])
@@ -686,49 +635,6 @@ export async function handleAutoFillFeishu(params: AutoFillFeishuParams): Promis
return { segmentCount: maxCount, fieldSegMap }
}
/**
* 查找标签文字的辅助函数
*/
function findLabelForInput(inp: Element): { labelText: string; labelElement: Element | null } {
let labelText = ""
let labelElement: Element | null = null
let container: Element | null = null
for (const sel of FORM_ITEM_SELS) {
container = inp.closest(sel)
if (container) break
}
if (container) {
const labelEls = container.querySelectorAll("label, span, div, td, th, p, legend, dt")
for (const el of Array.from(labelEls)) {
const directText = Array.from(el.childNodes)
.filter((n) => n.nodeType === Node.TEXT_NODE)
.map((n) => n.textContent?.trim())
.filter(Boolean)
.join("")
if (!directText || directText.length > 30) continue
if (STAT_EXCLUDE_LABEL_TEXTS.some((ex) => directText === ex)) continue
if (el.compareDocumentPosition(inp) & Node.DOCUMENT_POSITION_FOLLOWING) {
labelText = directText
labelElement = el
break
}
}
}
if (!labelText) {
let prev: Element | null = inp.previousElementSibling
for (let i = 0; i < 3 && prev; i++) {
const text = prev.textContent?.trim() || ""
if (text && text.length <= 20 && !STAT_EXCLUDE_LABEL_TEXTS.some((ex) => text === ex)) {
labelText = text
labelElement = prev
break
}
prev = prev.previousElementSibling
}
}
if (!labelText) labelText = (inp as HTMLInputElement).getAttribute("placeholder") || "(未知字段)"
return { labelText, labelElement }
}
// 按大标题分组统计
for (let tIdx = 0; tIdx < allTitles.length; tIdx++) {
+2 -96
View File
@@ -473,53 +473,8 @@ export async function handleAutoFillHotjob(params: AutoFillHotjobParams): Promis
}
if (inExcludeRange) continue
// 向上查找最近的标签文字
let labelText = ""
let labelElement: Element | null = null
// 策略1:查找 input 所在表单项容器内的标签
const formItemSelectors = [
".form-item", ".form-group", ".form-field",
".el-form-item", ".ant-form-item", ".ant-row",
".arco-form-item", ".t-form-item", ".n-form-item",
"[class*='form-item']", "[class*='form-group']", "[class*='formItem']",
]
let container: Element | null = null
for (const sel of formItemSelectors) {
container = inp.closest(sel)
if (container) break
}
if (container) {
const labelEls = container.querySelectorAll("label, span, div, td, th, p, legend, dt")
for (const el of Array.from(labelEls)) {
const directText = Array.from(el.childNodes)
.filter((n) => n.nodeType === Node.TEXT_NODE)
.map((n) => n.textContent?.trim())
.filter(Boolean)
.join("")
if (!directText || directText.length > 30) continue
if (EXCLUDE_LABEL_TEXTS.some((ex) => directText === ex)) continue
if (el.compareDocumentPosition(inp) & Node.DOCUMENT_POSITION_FOLLOWING) {
labelText = directText
labelElement = el
break
}
}
}
// 策略2:向前查找兄弟/父级中的标签
if (!labelText) {
let prev: Element | null = inp.previousElementSibling
for (let i = 0; i < 3 && prev; i++) {
const text = prev.textContent?.trim() || ""
if (text && text.length <= 20 && !EXCLUDE_LABEL_TEXTS.some((ex) => text === ex)) {
labelText = text
labelElement = prev
break
}
prev = prev.previousElementSibling
}
}
// 使用统一的 findLabelForInput 查找标签文字(纯 DOM 逆向遍历,不依赖类名)
const { labelText, labelElement } = findLabelForInput(inp)
if (!labelText || !labelElement) continue
@@ -610,12 +565,6 @@ export async function handleAutoFillHotjob(params: AutoFillHotjobParams): Promis
// 常量
const STAT_EXCLUDE_LABEL_TEXTS = ["请输入", "输入", ":", "", "请选择", "选择", "*", "必填"]
const INPUT_SEL_STAT = "input:not([type='hidden']):not([type='submit']):not([type='button']):not([type='checkbox']):not([type='radio']):not([type='file']):not([disabled]), textarea:not([disabled])"
const FORM_ITEM_SELS = [
".form-item", ".form-group", ".form-field",
".el-form-item", ".ant-form-item", ".ant-row",
".arco-form-item", ".t-form-item", ".n-form-item",
"[class*='form-item']", "[class*='form-group']", "[class*='formItem']",
]
/** 5大经历类型的 section 名集合(用于判断是否能从 expandedResults 取段落信息) */
const FIVE_EXP_SECTIONS = new Set(["education", "work", "internship", "project", "competition"])
@@ -686,49 +635,6 @@ export async function handleAutoFillHotjob(params: AutoFillHotjobParams): Promis
return { segmentCount: maxCount, fieldSegMap }
}
/**
* 查找标签文字的辅助函数
*/
function findLabelForInput(inp: Element): { labelText: string; labelElement: Element | null } {
let labelText = ""
let labelElement: Element | null = null
let container: Element | null = null
for (const sel of FORM_ITEM_SELS) {
container = inp.closest(sel)
if (container) break
}
if (container) {
const labelEls = container.querySelectorAll("label, span, div, td, th, p, legend, dt")
for (const el of Array.from(labelEls)) {
const directText = Array.from(el.childNodes)
.filter((n) => n.nodeType === Node.TEXT_NODE)
.map((n) => n.textContent?.trim())
.filter(Boolean)
.join("")
if (!directText || directText.length > 30) continue
if (STAT_EXCLUDE_LABEL_TEXTS.some((ex) => directText === ex)) continue
if (el.compareDocumentPosition(inp) & Node.DOCUMENT_POSITION_FOLLOWING) {
labelText = directText
labelElement = el
break
}
}
}
if (!labelText) {
let prev: Element | null = inp.previousElementSibling
for (let i = 0; i < 3 && prev; i++) {
const text = prev.textContent?.trim() || ""
if (text && text.length <= 20 && !STAT_EXCLUDE_LABEL_TEXTS.some((ex) => text === ex)) {
labelText = text
labelElement = prev
break
}
prev = prev.previousElementSibling
}
}
if (!labelText) labelText = (inp as HTMLInputElement).getAttribute("placeholder") || "(未知字段)"
return { labelText, labelElement }
}
// 按大标题分组统计
for (let tIdx = 0; tIdx < allTitles.length; tIdx++) {
+2 -96
View File
@@ -492,53 +492,8 @@ export async function handleAutoFillMoka(params: AutoFillMokaParams): Promise<Au
}
if (inExcludeRange) continue
// 向上查找最近的标签文字
let labelText = ""
let labelElement: Element | null = null
// 策略1:查找 input 所在表单项容器内的标签
const formItemSelectors = [
".form-item", ".form-group", ".form-field",
".el-form-item", ".ant-form-item", ".ant-row",
".arco-form-item", ".t-form-item", ".n-form-item",
"[class*='form-item']", "[class*='form-group']", "[class*='formItem']",
]
let container: Element | null = null
for (const sel of formItemSelectors) {
container = inp.closest(sel)
if (container) break
}
if (container) {
const labelEls = container.querySelectorAll("label, span, div, td, th, p, legend, dt")
for (const el of Array.from(labelEls)) {
const directText = Array.from(el.childNodes)
.filter((n) => n.nodeType === Node.TEXT_NODE)
.map((n) => n.textContent?.trim())
.filter(Boolean)
.join("")
if (!directText || directText.length > 30) continue
if (EXCLUDE_LABEL_TEXTS.some((ex) => directText === ex)) continue
if (el.compareDocumentPosition(inp) & Node.DOCUMENT_POSITION_FOLLOWING) {
labelText = directText
labelElement = el
break
}
}
}
// 策略2:向前查找兄弟/父级中的标签
if (!labelText) {
let prev: Element | null = inp.previousElementSibling
for (let i = 0; i < 3 && prev; i++) {
const text = prev.textContent?.trim() || ""
if (text && text.length <= 20 && !EXCLUDE_LABEL_TEXTS.some((ex) => text === ex)) {
labelText = text
labelElement = prev
break
}
prev = prev.previousElementSibling
}
}
// 使用统一的 findLabelForInput 查找标签文字(纯 DOM 逆向遍历,不依赖类名)
const { labelText, labelElement } = findLabelForInput(inp)
if (!labelText || !labelElement) continue
@@ -629,12 +584,6 @@ export async function handleAutoFillMoka(params: AutoFillMokaParams): Promise<Au
// 常量
const STAT_EXCLUDE_LABEL_TEXTS = ["请输入", "输入", ":", "", "请选择", "选择", "*", "必填"]
const INPUT_SEL_STAT = "input:not([type='hidden']):not([type='submit']):not([type='button']):not([type='checkbox']):not([type='radio']):not([type='file']):not([disabled]), textarea:not([disabled])"
const FORM_ITEM_SELS = [
".form-item", ".form-group", ".form-field",
".el-form-item", ".ant-form-item", ".ant-row",
".arco-form-item", ".t-form-item", ".n-form-item",
"[class*='form-item']", "[class*='form-group']", "[class*='formItem']",
]
/** 5大经历类型的 section 名集合(用于判断是否能从 expandedResults 取段落信息) */
const FIVE_EXP_SECTIONS = new Set(["education", "work", "internship", "project", "competition"])
@@ -705,49 +654,6 @@ export async function handleAutoFillMoka(params: AutoFillMokaParams): Promise<Au
return { segmentCount: maxCount, fieldSegMap }
}
/**
* 查找标签文字的辅助函数
*/
function findLabelForInput(inp: Element): { labelText: string; labelElement: Element | null } {
let labelText = ""
let labelElement: Element | null = null
let container: Element | null = null
for (const sel of FORM_ITEM_SELS) {
container = inp.closest(sel)
if (container) break
}
if (container) {
const labelEls = container.querySelectorAll("label, span, div, td, th, p, legend, dt")
for (const el of Array.from(labelEls)) {
const directText = Array.from(el.childNodes)
.filter((n) => n.nodeType === Node.TEXT_NODE)
.map((n) => n.textContent?.trim())
.filter(Boolean)
.join("")
if (!directText || directText.length > 30) continue
if (STAT_EXCLUDE_LABEL_TEXTS.some((ex) => directText === ex)) continue
if (el.compareDocumentPosition(inp) & Node.DOCUMENT_POSITION_FOLLOWING) {
labelText = directText
labelElement = el
break
}
}
}
if (!labelText) {
let prev: Element | null = inp.previousElementSibling
for (let i = 0; i < 3 && prev; i++) {
const text = prev.textContent?.trim() || ""
if (text && text.length <= 20 && !STAT_EXCLUDE_LABEL_TEXTS.some((ex) => text === ex)) {
labelText = text
labelElement = prev
break
}
prev = prev.previousElementSibling
}
}
if (!labelText) labelText = (inp as HTMLInputElement).getAttribute("placeholder") || "(未知字段)"
return { labelText, labelElement }
}
// 按大标题分组统计
for (let tIdx = 0; tIdx < allTitles.length; tIdx++) {
+33 -43
View File
@@ -45,33 +45,16 @@ function fixSpecialLabelInput(
// ============ input 查找 ============
/** 常见的表单项容器选择器 */
const FORM_ITEM_SELECTORS = [
".form-item", ".form-group", ".form-field",
".el-form-item", ".ant-form-item", ".ant-row",
".arco-form-item", ".t-form-item", ".n-form-item",
".ivu-form-item", ".v-input", ".MuiFormControl-root",
"[class*='form-item']", "[class*='form-group']", "[class*='formItem']",
]
/** input 选择器(排除隐藏、提交、按钮、复选、单选、文件类型) */
const INPUT_SEL = "input:not([type='hidden']):not([type='submit']):not([type='button']):not([type='checkbox']):not([type='radio']):not([type='file']):not([disabled]), textarea:not([disabled])"
/**
* 在标签元素附近查找最近的、属于同一表单项的 input/textarea 输入框
* 策略:1.表单项容器内查找 → 2.逐级向上查找 → 3.兜底查兄弟节点
* 策略:1.逐级向上查找含单个 input 的父级 → 2.含多个 input 时取 DOM 顺序最近的 → 3.兜底查兄弟节点
* 不依赖任何 form-item 类名,纯 DOM 结构适配所有网站
*/
export function findNearestInput(labelEl: Element): HTMLInputElement | HTMLTextAreaElement | null {
// 策略1:找标签所在的最近表单项容器
for (const sel of FORM_ITEM_SELECTORS) {
const container = labelEl.closest(sel)
if (container) {
const input = container.querySelector(INPUT_SEL) as HTMLInputElement | HTMLTextAreaElement | null
if (input) return input
}
}
// 策略2:逐级向上查找
// 策略1逐级向上查
let parent: Element | null = labelEl.parentElement
for (let i = 0; i < 5 && parent; i++) {
const inputs = parent.querySelectorAll(INPUT_SEL)
@@ -80,7 +63,7 @@ export function findNearestInput(labelEl: Element): HTMLInputElement | HTMLTextA
parent = parent.parentElement
}
// 策略3:兜底查兄弟节点
// 策略2:兜底查兄弟节点
let sibling = labelEl.nextElementSibling
while (sibling) {
if ((sibling.tagName === "INPUT" || sibling.tagName === "TEXTAREA") &&
@@ -175,12 +158,14 @@ export function matchFormFields(
if (matchLabel) {
// 检测单选按钮组
// 检测单选按钮组:从标签元素逐级向上查找含 radio 的容器
let isRadioType = false
let radioContainer: Element | null = null
const closestFormItem = candidateEl.closest(".form-item, .form-group, .el-form-item, .ant-form-item")
if (closestFormItem) {
const radioInItem = closestFormItem.querySelector('input[type="radio"]')
if (radioInItem) { isRadioType = true; radioContainer = closestFormItem }
let radioSearchParent: Element | null = candidateEl.parentElement
for (let rUp = 0; rUp < 4 && radioSearchParent; rUp++) {
const radioInItem = radioSearchParent.querySelector('input[type="radio"]')
if (radioInItem) { isRadioType = true; radioContainer = radioSearchParent; break }
radioSearchParent = radioSearchParent.parentElement
}
// 非 radio 类型,正常找 input
@@ -313,12 +298,14 @@ export function matchFormFieldsInRange(
if (matchLabel) {
// 检测单选按钮组
// 检测单选按钮组:从标签元素逐级向上查找含 radio 的容器
let isRadioType = false
let radioContainer: Element | null = null
const closestFormItem = candidateEl.closest(".form-item, .form-group, .el-form-item, .ant-form-item")
if (closestFormItem) {
const radioInItem = closestFormItem.querySelector('input[type="radio"]')
if (radioInItem) { isRadioType = true; radioContainer = closestFormItem }
let radioSearchParent: Element | null = candidateEl.parentElement
for (let rUp = 0; rUp < 4 && radioSearchParent; rUp++) {
const radioInItem = radioSearchParent.querySelector('input[type="radio"]')
if (radioInItem) { isRadioType = true; radioContainer = radioSearchParent; break }
radioSearchParent = radioSearchParent.parentElement
}
// 非 radio 类型,正常找 input
@@ -420,26 +407,29 @@ export function matchMainFields(
)
if (matchLabel) {
let isRadioType = false
let radioContainer: Element | null = null
const closestFormItem = candidateEl.closest(".form-item, .form-group, .el-form-item, .ant-form-item")
if (closestFormItem) {
const radioInItem = closestFormItem.querySelector('input[type="radio"]')
if (radioInItem) { isRadioType = true; radioContainer = closestFormItem }
}
let inputEl: HTMLInputElement | HTMLTextAreaElement | null = null
let inputType: "text" | "radio" | "picker" | "textarea" = isRadioType ? "radio" : "text"
if (!isRadioType) {
inputEl = findNearestInput(candidateEl)
// 先尝试找 input(优先级高于 radio 检测,避免深层 DOM 结构误判)
let inputEl: HTMLInputElement | HTMLTextAreaElement | null = findNearestInput(candidateEl)
if (inputEl) inputEl = fixSpecialLabelInput(item.key, inputEl, candidateEl, usedInputs)
if (inputEl && usedInputs.has(inputEl)) continue
// 找不到 input 时,再检测单选按钮组:从标签元素逐级向上查找含 radio 的容器
let isRadioType = false
let radioContainer: Element | null = null
if (!inputEl) {
let radioSearchParent: Element | null = candidateEl.parentElement
for (let rUp = 0; rUp < 4 && radioSearchParent; rUp++) {
const radioInItem = radioSearchParent.querySelector('input[type="radio"]')
if (radioInItem) { isRadioType = true; radioContainer = radioSearchParent; break }
radioSearchParent = radioSearchParent.parentElement
}
}
let inputType: "text" | "radio" | "picker" | "textarea" = isRadioType ? "radio" : "text"
if (inputEl) {
usedInputs.add(inputEl)
if (inputEl.tagName === "TEXTAREA") inputType = "textarea"
}
}
const btnEl = inputEl ? findNearbyButton(inputEl) : null
results.push({
+12 -141
View File
@@ -2,9 +2,7 @@
* 表单字段标签定位模块(统一封装)
* 负责:根据 input/textarea 元素在 DOM 中定位其对应的标签文字
*
* 两遍策略:
* 第一遍:通过 form-item 容器选择器定位 → 逐级向上修复 → 在容器内找标签
* 第二遍:如果第一遍失败,沿 DOM 文档流逆向遍历找到有效中文标签
* 核心策略:沿 DOM 文档流逆向遍历,从 input 往前找到第一个有效中文标签
*
* 所有需要"识别输入框前面的标签名字"的地方统一调用此模块
*/
@@ -12,18 +10,10 @@
// ============ 常量 ============
/** 排除的标签文字(不视为有效标签) */
const EXCLUDE_LABEL_TEXTS = ["请输入", "输入", ":", "", "请选择", "选择", "添加", "*", "必填", "确定", "取消", "搜索", "无准确的毕业时间可填写预计毕业时间","暂无选项"]
const EXCLUDE_LABEL_TEXTS = ["请输入", "输入", ":", "", "请选择", "选择", "添加", "*", "必填", "确定", "取消", "搜索", "无准确的毕业时间可填写预计毕业时间", "暂无选项"]
/** 排除的标签文字片段(包含这些片段的文本也不视为有效标签) */
const EXCLUDE_LABEL_INCLUDES = ["请输入","请选择"]
/** 表单项容器选择器集合(用于第一遍策略的 closest 查找) */
const FORM_ITEM_SELS = [
".form-item", ".form-group", ".form-field",
".el-form-item", ".ant-form-item", ".ant-row",
".arco-form-item", ".t-form-item", ".n-form-item",
"[class*='form-item']", "[class*='form-group']", "[class*='formItem']",
]
const EXCLUDE_LABEL_INCLUDES = ["请输入", "请选择"]
// ============ 工具方法 ============
@@ -35,20 +25,17 @@ export function isValidLabelText(text: string): boolean {
const trimmed = text.trim()
if (!trimmed) return false
if (trimmed.length > 30) return false
// 必须包含至少2个汉字
const chineseChars = trimmed.match(/[\u4e00-\u9fff]/g)
if (!chineseChars || chineseChars.length < 2) return false
// 排除已知无意义标签
if (EXCLUDE_LABEL_TEXTS.some((ex) => trimmed === ex)) return false
// 排除包含特定片段的文字
if (EXCLUDE_LABEL_INCLUDES.some((inc) => trimmed.includes(inc))) return false
return true
}
// ============ 第二遍策略:DOM 逆向遍历 ============
// ============ DOM 逆向遍历 ============
/**
* 【第二遍策略】沿 DOM 文档流往前遍历,查找 input 对应的标签文字
* 沿 DOM 文档流往前遍历,查找 input 对应的标签文字
* 从 input 出发逆向走,逐个检查文本节点
*
* 停止条件:
@@ -110,140 +97,24 @@ function walkBackwardForLabel(inp: Element, titleElements?: Set<Element>, exclud
* 查找 input/textarea 元素对应的标签文字
*
* @param inp - 输入框元素
* @param titleElements - 大标题元素集合(用于第二遍策略的停止边界,可选)
* @param titleElements - 大标题元素集合(用于逆向遍历的停止边界,可选)
* @param excludeTexts - 需要排除的文字集合(简历数据/缓存中的已填值,遇到时跳过继续找,可选)
* @returns { labelText, labelElement } 标签文字和标签元素
*
* 策略流程
* 1. closest(FORM_ITEM_SELS) 找容器
* 2. 验证容器内是否同时有标签和输入框,不满足则逐级向上找(最多4层)
* 3. 在容器内 querySelectorAll 找 input 前面的第一个有效文字元素
* 4. fallback: previousElementSibling 查找
* 5. 第二遍:如果以上都未得到含≥2个汉字的标签,启动 walkBackwardForLabel 逆向遍历
* 6. 都找不到则返回 placeholder 或 "(未知字段)"
* 策略:纯 DOM 逆向遍历,从 input 往前走找到第一个有效中文标签文本节点
* 不依赖任何 form-item 类名容器,适配所有网站
*/
export function findLabelForInput(inp: Element, titleElements?: Set<Element>, excludeTexts?: Set<string>): { labelText: string; labelElement: Element | null } {
let labelText = ""
let labelElement: Element | null = null
let container: Element | null = null
for (const sel of FORM_ITEM_SELS) {
container = inp.closest(sel)
if (container) break
}
// 【验证】container 应同时包含"标签文字"和"输入框",否则匹配层级太浅
if (container) {
const hasInput = container.querySelector("input:not([type='hidden']):not([type='submit']):not([type='button']):not([type='checkbox']):not([type='radio']):not([type='file']), textarea")
let hasLabelText = false
if (hasInput) {
const candidates = container.querySelectorAll("label, span, div, td, th, p, legend, dt")
for (const el of Array.from(candidates)) {
if (el === inp || el.contains(inp)) continue
const dt = Array.from(el.childNodes)
.filter((n) => n.nodeType === Node.TEXT_NODE)
.map((n) => n.textContent?.trim())
.filter(Boolean)
.join("")
if (dt && dt.length <= 30 && !EXCLUDE_LABEL_TEXTS.some((ex) => dt === ex)) {
if (EXCLUDE_LABEL_INCLUDES.some((inc) => dt.includes(inc))) continue
if (excludeTexts && excludeTexts.has(dt)) continue
if (el.compareDocumentPosition(inp) & Node.DOCUMENT_POSITION_FOLLOWING) {
hasLabelText = true
break
}
}
}
}
// 逐级向上找,最多 4 层
if (!hasLabelText) {
let parent = container.parentElement
for (let up = 0; up < 4 && parent; up++) {
const parentCandidates = parent.querySelectorAll("label, span, div, td, th, p, legend, dt")
let found = false
for (const el of Array.from(parentCandidates)) {
if (el === inp || el.contains(inp)) continue
const dt = Array.from(el.childNodes)
.filter((n) => n.nodeType === Node.TEXT_NODE)
.map((n) => n.textContent?.trim())
.filter(Boolean)
.join("")
if (dt && dt.length <= 30 && !EXCLUDE_LABEL_TEXTS.some((ex) => dt === ex)) {
if (EXCLUDE_LABEL_INCLUDES.some((inc) => dt.includes(inc))) continue
if (excludeTexts && excludeTexts.has(dt)) continue
if (el.compareDocumentPosition(inp) & Node.DOCUMENT_POSITION_FOLLOWING) {
found = true
break
}
}
}
if (found) {
container = parent
break
}
parent = parent.parentElement
}
}
}
// 在容器内找标签
if (container) {
const labelEls = container.querySelectorAll("label, span, div, td, th, p, legend, dt")
for (const el of Array.from(labelEls)) {
const directText = Array.from(el.childNodes)
.filter((n) => n.nodeType === Node.TEXT_NODE)
.map((n) => n.textContent?.trim())
.filter(Boolean)
.join("")
if (!directText || directText.length > 30) continue
if (EXCLUDE_LABEL_TEXTS.some((ex) => directText === ex)) continue
if (EXCLUDE_LABEL_INCLUDES.some((inc) => directText.includes(inc))) continue
// 跳过已填值(简历数据/缓存数据中的值不应作为标签)
if (excludeTexts && excludeTexts.has(directText)) continue
if (el.compareDocumentPosition(inp) & Node.DOCUMENT_POSITION_FOLLOWING) {
labelText = directText
labelElement = el
break
}
}
}
// fallback: previousElementSibling
if (!labelText) {
let prev: Element | null = inp.previousElementSibling
for (let i = 0; i < 3 && prev; i++) {
const text = prev.textContent?.trim() || ""
if (text && text.length <= 20 && !EXCLUDE_LABEL_TEXTS.some((ex) => text === ex) && !EXCLUDE_LABEL_INCLUDES.some((inc) => text.includes(inc))) {
// 跳过已填值
if (excludeTexts && excludeTexts.has(text)) {
prev = prev.previousElementSibling
continue
}
labelText = text
labelElement = prev
break
}
prev = prev.previousElementSibling
}
}
// 【第二遍策略】如果第一遍未识别出有效标签(空或不含2个汉字),逆向遍历 DOM
if (!labelText || !isValidLabelText(labelText)) {
// 直接走 DOM 逆向遍历,从 input 往前找最近的有效中文标签
const foundLabel = walkBackwardForLabel(inp, titleElements, excludeTexts)
if (foundLabel) {
labelText = foundLabel.text
labelElement = foundLabel.element
}
return { labelText: foundLabel.text, labelElement: foundLabel.element }
}
// 最终 fallback:都找不到才用 placeholder 或返回"(未知字段)"
if (!labelText) {
const placeholder = (inp as HTMLInputElement).getAttribute("placeholder") || ""
// placeholder 如果包含排除片段(如"请输入"、"请选择"),不作为标签
if (placeholder && !EXCLUDE_LABEL_INCLUDES.some((inc) => placeholder.includes(inc)) && !EXCLUDE_LABEL_TEXTS.some((ex) => placeholder === ex)) {
labelText = placeholder
} else {
labelText = "(未知字段)"
return { labelText: placeholder, labelElement: null }
}
}
return { labelText, labelElement }
return { labelText: "(未知字段)", labelElement: null }
}