去掉多余的类名常量规则,降低radio的操作优先级
This commit is contained in:
@@ -483,53 +483,8 @@ export async function handleAutoFillBeisen(params: AutoFillBeisenParams): Promis
|
|||||||
}
|
}
|
||||||
if (inExcludeRange) continue
|
if (inExcludeRange) continue
|
||||||
|
|
||||||
// 向上查找最近的标签文字
|
// 使用统一的 findLabelForInput 查找标签文字(纯 DOM 逆向遍历,不依赖类名)
|
||||||
let labelText = ""
|
const { labelText, labelElement } = findLabelForInput(inp)
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!labelText || !labelElement) continue
|
if (!labelText || !labelElement) continue
|
||||||
|
|
||||||
@@ -620,12 +575,6 @@ export async function handleAutoFillBeisen(params: AutoFillBeisenParams): Promis
|
|||||||
// 常量
|
// 常量
|
||||||
const STAT_EXCLUDE_LABEL_TEXTS = ["请输入", "输入", ":", ":", "请选择", "选择", "*", "必填"]
|
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 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 取段落信息) */
|
/** 5大经历类型的 section 名集合(用于判断是否能从 expandedResults 取段落信息) */
|
||||||
const FIVE_EXP_SECTIONS = new Set(["education", "work", "internship", "project", "competition"])
|
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 }
|
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++) {
|
for (let tIdx = 0; tIdx < allTitles.length; tIdx++) {
|
||||||
|
|||||||
@@ -467,53 +467,8 @@ export async function handleAutoFillCommon(params: AutoFillCommonParams): Promis
|
|||||||
}
|
}
|
||||||
if (inExcludeRange) continue
|
if (inExcludeRange) continue
|
||||||
|
|
||||||
// 向上查找最近的标签文字
|
// 使用统一的 findLabelForInput 查找标签文字(纯 DOM 逆向遍历,不依赖类名)
|
||||||
let labelText = ""
|
const { labelText, labelElement } = findLabelForInput(inp)
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!labelText || !labelElement) continue
|
if (!labelText || !labelElement) continue
|
||||||
|
|
||||||
@@ -604,12 +559,6 @@ export async function handleAutoFillCommon(params: AutoFillCommonParams): Promis
|
|||||||
// 常量
|
// 常量
|
||||||
const STAT_EXCLUDE_LABEL_TEXTS = ["请输入", "输入", ":", ":", "请选择", "选择", "*", "必填"]
|
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 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 取段落信息) */
|
/** 5大经历类型的 section 名集合(用于判断是否能从 expandedResults 取段落信息) */
|
||||||
const FIVE_EXP_SECTIONS = new Set(["education", "work", "internship", "project", "competition"])
|
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 }
|
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++) {
|
for (let tIdx = 0; tIdx < allTitles.length; tIdx++) {
|
||||||
|
|||||||
@@ -473,53 +473,8 @@ export async function handleAutoFillFeishu(params: AutoFillFeishuParams): Promis
|
|||||||
}
|
}
|
||||||
if (inExcludeRange) continue
|
if (inExcludeRange) continue
|
||||||
|
|
||||||
// 向上查找最近的标签文字
|
// 使用统一的 findLabelForInput 查找标签文字(纯 DOM 逆向遍历,不依赖类名)
|
||||||
let labelText = ""
|
const { labelText, labelElement } = findLabelForInput(inp)
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!labelText || !labelElement) continue
|
if (!labelText || !labelElement) continue
|
||||||
|
|
||||||
@@ -610,12 +565,6 @@ export async function handleAutoFillFeishu(params: AutoFillFeishuParams): Promis
|
|||||||
// 常量
|
// 常量
|
||||||
const STAT_EXCLUDE_LABEL_TEXTS = ["请输入", "输入", ":", ":", "请选择", "选择", "*", "必填"]
|
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 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 取段落信息) */
|
/** 5大经历类型的 section 名集合(用于判断是否能从 expandedResults 取段落信息) */
|
||||||
const FIVE_EXP_SECTIONS = new Set(["education", "work", "internship", "project", "competition"])
|
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 }
|
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++) {
|
for (let tIdx = 0; tIdx < allTitles.length; tIdx++) {
|
||||||
|
|||||||
@@ -473,53 +473,8 @@ export async function handleAutoFillHotjob(params: AutoFillHotjobParams): Promis
|
|||||||
}
|
}
|
||||||
if (inExcludeRange) continue
|
if (inExcludeRange) continue
|
||||||
|
|
||||||
// 向上查找最近的标签文字
|
// 使用统一的 findLabelForInput 查找标签文字(纯 DOM 逆向遍历,不依赖类名)
|
||||||
let labelText = ""
|
const { labelText, labelElement } = findLabelForInput(inp)
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!labelText || !labelElement) continue
|
if (!labelText || !labelElement) continue
|
||||||
|
|
||||||
@@ -610,12 +565,6 @@ export async function handleAutoFillHotjob(params: AutoFillHotjobParams): Promis
|
|||||||
// 常量
|
// 常量
|
||||||
const STAT_EXCLUDE_LABEL_TEXTS = ["请输入", "输入", ":", ":", "请选择", "选择", "*", "必填"]
|
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 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 取段落信息) */
|
/** 5大经历类型的 section 名集合(用于判断是否能从 expandedResults 取段落信息) */
|
||||||
const FIVE_EXP_SECTIONS = new Set(["education", "work", "internship", "project", "competition"])
|
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 }
|
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++) {
|
for (let tIdx = 0; tIdx < allTitles.length; tIdx++) {
|
||||||
|
|||||||
@@ -492,53 +492,8 @@ export async function handleAutoFillMoka(params: AutoFillMokaParams): Promise<Au
|
|||||||
}
|
}
|
||||||
if (inExcludeRange) continue
|
if (inExcludeRange) continue
|
||||||
|
|
||||||
// 向上查找最近的标签文字
|
// 使用统一的 findLabelForInput 查找标签文字(纯 DOM 逆向遍历,不依赖类名)
|
||||||
let labelText = ""
|
const { labelText, labelElement } = findLabelForInput(inp)
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!labelText || !labelElement) continue
|
if (!labelText || !labelElement) continue
|
||||||
|
|
||||||
@@ -629,12 +584,6 @@ export async function handleAutoFillMoka(params: AutoFillMokaParams): Promise<Au
|
|||||||
// 常量
|
// 常量
|
||||||
const STAT_EXCLUDE_LABEL_TEXTS = ["请输入", "输入", ":", ":", "请选择", "选择", "*", "必填"]
|
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 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 取段落信息) */
|
/** 5大经历类型的 section 名集合(用于判断是否能从 expandedResults 取段落信息) */
|
||||||
const FIVE_EXP_SECTIONS = new Set(["education", "work", "internship", "project", "competition"])
|
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 }
|
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++) {
|
for (let tIdx = 0; tIdx < allTitles.length; tIdx++) {
|
||||||
|
|||||||
+32
-42
@@ -45,33 +45,16 @@ function fixSpecialLabelInput(
|
|||||||
|
|
||||||
// ============ input 查找 ============
|
// ============ 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 选择器(排除隐藏、提交、按钮、复选、单选、文件类型) */
|
/** 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])"
|
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 输入框
|
* 在标签元素附近查找最近的、属于同一表单项的 input/textarea 输入框
|
||||||
* 策略:1.表单项容器内查找 → 2.逐级向上查找 → 3.兜底查兄弟节点
|
* 策略:1.逐级向上查找含单个 input 的父级 → 2.含多个 input 时取 DOM 顺序最近的 → 3.兜底查兄弟节点
|
||||||
|
* 不依赖任何 form-item 类名,纯 DOM 结构适配所有网站
|
||||||
*/
|
*/
|
||||||
export function findNearestInput(labelEl: Element): HTMLInputElement | HTMLTextAreaElement | null {
|
export function findNearestInput(labelEl: Element): HTMLInputElement | HTMLTextAreaElement | null {
|
||||||
// 策略1:找标签所在的最近表单项容器
|
// 策略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:逐级向上查找
|
|
||||||
let parent: Element | null = labelEl.parentElement
|
let parent: Element | null = labelEl.parentElement
|
||||||
for (let i = 0; i < 5 && parent; i++) {
|
for (let i = 0; i < 5 && parent; i++) {
|
||||||
const inputs = parent.querySelectorAll(INPUT_SEL)
|
const inputs = parent.querySelectorAll(INPUT_SEL)
|
||||||
@@ -80,7 +63,7 @@ export function findNearestInput(labelEl: Element): HTMLInputElement | HTMLTextA
|
|||||||
parent = parent.parentElement
|
parent = parent.parentElement
|
||||||
}
|
}
|
||||||
|
|
||||||
// 策略3:兜底查兄弟节点
|
// 策略2:兜底查兄弟节点
|
||||||
let sibling = labelEl.nextElementSibling
|
let sibling = labelEl.nextElementSibling
|
||||||
while (sibling) {
|
while (sibling) {
|
||||||
if ((sibling.tagName === "INPUT" || sibling.tagName === "TEXTAREA") &&
|
if ((sibling.tagName === "INPUT" || sibling.tagName === "TEXTAREA") &&
|
||||||
@@ -175,12 +158,14 @@ export function matchFormFields(
|
|||||||
|
|
||||||
if (matchLabel) {
|
if (matchLabel) {
|
||||||
// 检测单选按钮组
|
// 检测单选按钮组
|
||||||
|
// 检测单选按钮组:从标签元素逐级向上查找含 radio 的容器
|
||||||
let isRadioType = false
|
let isRadioType = false
|
||||||
let radioContainer: Element | null = null
|
let radioContainer: Element | null = null
|
||||||
const closestFormItem = candidateEl.closest(".form-item, .form-group, .el-form-item, .ant-form-item")
|
let radioSearchParent: Element | null = candidateEl.parentElement
|
||||||
if (closestFormItem) {
|
for (let rUp = 0; rUp < 4 && radioSearchParent; rUp++) {
|
||||||
const radioInItem = closestFormItem.querySelector('input[type="radio"]')
|
const radioInItem = radioSearchParent.querySelector('input[type="radio"]')
|
||||||
if (radioInItem) { isRadioType = true; radioContainer = closestFormItem }
|
if (radioInItem) { isRadioType = true; radioContainer = radioSearchParent; break }
|
||||||
|
radioSearchParent = radioSearchParent.parentElement
|
||||||
}
|
}
|
||||||
|
|
||||||
// 非 radio 类型,正常找 input
|
// 非 radio 类型,正常找 input
|
||||||
@@ -313,12 +298,14 @@ export function matchFormFieldsInRange(
|
|||||||
|
|
||||||
if (matchLabel) {
|
if (matchLabel) {
|
||||||
// 检测单选按钮组
|
// 检测单选按钮组
|
||||||
|
// 检测单选按钮组:从标签元素逐级向上查找含 radio 的容器
|
||||||
let isRadioType = false
|
let isRadioType = false
|
||||||
let radioContainer: Element | null = null
|
let radioContainer: Element | null = null
|
||||||
const closestFormItem = candidateEl.closest(".form-item, .form-group, .el-form-item, .ant-form-item")
|
let radioSearchParent: Element | null = candidateEl.parentElement
|
||||||
if (closestFormItem) {
|
for (let rUp = 0; rUp < 4 && radioSearchParent; rUp++) {
|
||||||
const radioInItem = closestFormItem.querySelector('input[type="radio"]')
|
const radioInItem = radioSearchParent.querySelector('input[type="radio"]')
|
||||||
if (radioInItem) { isRadioType = true; radioContainer = closestFormItem }
|
if (radioInItem) { isRadioType = true; radioContainer = radioSearchParent; break }
|
||||||
|
radioSearchParent = radioSearchParent.parentElement
|
||||||
}
|
}
|
||||||
|
|
||||||
// 非 radio 类型,正常找 input
|
// 非 radio 类型,正常找 input
|
||||||
@@ -420,25 +407,28 @@ export function matchMainFields(
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (matchLabel) {
|
if (matchLabel) {
|
||||||
|
// 先尝试找 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 isRadioType = false
|
||||||
let radioContainer: Element | null = null
|
let radioContainer: Element | null = null
|
||||||
const closestFormItem = candidateEl.closest(".form-item, .form-group, .el-form-item, .ant-form-item")
|
if (!inputEl) {
|
||||||
if (closestFormItem) {
|
let radioSearchParent: Element | null = candidateEl.parentElement
|
||||||
const radioInItem = closestFormItem.querySelector('input[type="radio"]')
|
for (let rUp = 0; rUp < 4 && radioSearchParent; rUp++) {
|
||||||
if (radioInItem) { isRadioType = true; radioContainer = closestFormItem }
|
const radioInItem = radioSearchParent.querySelector('input[type="radio"]')
|
||||||
|
if (radioInItem) { isRadioType = true; radioContainer = radioSearchParent; break }
|
||||||
|
radioSearchParent = radioSearchParent.parentElement
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let inputEl: HTMLInputElement | HTMLTextAreaElement | null = null
|
|
||||||
let inputType: "text" | "radio" | "picker" | "textarea" = isRadioType ? "radio" : "text"
|
let inputType: "text" | "radio" | "picker" | "textarea" = isRadioType ? "radio" : "text"
|
||||||
|
|
||||||
if (!isRadioType) {
|
if (inputEl) {
|
||||||
inputEl = findNearestInput(candidateEl)
|
usedInputs.add(inputEl)
|
||||||
if (inputEl) inputEl = fixSpecialLabelInput(item.key, inputEl, candidateEl, usedInputs)
|
if (inputEl.tagName === "TEXTAREA") inputType = "textarea"
|
||||||
if (inputEl && usedInputs.has(inputEl)) continue
|
|
||||||
if (inputEl) {
|
|
||||||
usedInputs.add(inputEl)
|
|
||||||
if (inputEl.tagName === "TEXTAREA") inputType = "textarea"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const btnEl = inputEl ? findNearbyButton(inputEl) : null
|
const btnEl = inputEl ? findNearbyButton(inputEl) : null
|
||||||
|
|||||||
+22
-151
@@ -1,29 +1,19 @@
|
|||||||
/**
|
/**
|
||||||
* 表单字段标签定位模块(统一封装)
|
* 表单字段标签定位模块(统一封装)
|
||||||
* 负责:根据 input/textarea 元素在 DOM 中定位其对应的标签文字
|
* 负责:根据 input/textarea 元素在 DOM 中定位其对应的标签文字
|
||||||
*
|
*
|
||||||
* 两遍策略:
|
* 核心策略:沿 DOM 文档流逆向遍历,从 input 往前找到第一个有效中文标签
|
||||||
* 第一遍:通过 form-item 容器选择器定位 → 逐级向上修复 → 在容器内找标签
|
*
|
||||||
* 第二遍:如果第一遍失败,沿 DOM 文档流逆向遍历找到有效中文标签
|
|
||||||
*
|
|
||||||
* 所有需要"识别输入框前面的标签名字"的地方统一调用此模块
|
* 所有需要"识别输入框前面的标签名字"的地方统一调用此模块
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// ============ 常量 ============
|
// ============ 常量 ============
|
||||||
|
|
||||||
/** 排除的标签文字(不视为有效标签) */
|
/** 排除的标签文字(不视为有效标签) */
|
||||||
const EXCLUDE_LABEL_TEXTS = ["请输入", "输入", ":", ":", "请选择", "选择", "添加", "*", "必填", "确定", "取消", "搜索", "无准确的毕业时间可填写预计毕业时间","暂无选项"]
|
const EXCLUDE_LABEL_TEXTS = ["请输入", "输入", ":", ":", "请选择", "选择", "添加", "*", "必填", "确定", "取消", "搜索", "无准确的毕业时间可填写预计毕业时间", "暂无选项"]
|
||||||
|
|
||||||
/** 排除的标签文字片段(包含这些片段的文本也不视为有效标签) */
|
/** 排除的标签文字片段(包含这些片段的文本也不视为有效标签) */
|
||||||
const EXCLUDE_LABEL_INCLUDES = ["请输入","请选择"]
|
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']",
|
|
||||||
]
|
|
||||||
|
|
||||||
// ============ 工具方法 ============
|
// ============ 工具方法 ============
|
||||||
|
|
||||||
@@ -35,28 +25,25 @@ export function isValidLabelText(text: string): boolean {
|
|||||||
const trimmed = text.trim()
|
const trimmed = text.trim()
|
||||||
if (!trimmed) return false
|
if (!trimmed) return false
|
||||||
if (trimmed.length > 30) return false
|
if (trimmed.length > 30) return false
|
||||||
// 必须包含至少2个汉字
|
|
||||||
const chineseChars = trimmed.match(/[\u4e00-\u9fff]/g)
|
const chineseChars = trimmed.match(/[\u4e00-\u9fff]/g)
|
||||||
if (!chineseChars || chineseChars.length < 2) return false
|
if (!chineseChars || chineseChars.length < 2) return false
|
||||||
// 排除已知无意义标签
|
|
||||||
if (EXCLUDE_LABEL_TEXTS.some((ex) => trimmed === ex)) return false
|
if (EXCLUDE_LABEL_TEXTS.some((ex) => trimmed === ex)) return false
|
||||||
// 排除包含特定片段的文字
|
|
||||||
if (EXCLUDE_LABEL_INCLUDES.some((inc) => trimmed.includes(inc))) return false
|
if (EXCLUDE_LABEL_INCLUDES.some((inc) => trimmed.includes(inc))) return false
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============ 第二遍策略:DOM 逆向遍历 ============
|
// ============ DOM 逆向遍历 ============
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 【第二遍策略】沿 DOM 文档流往前遍历,查找 input 对应的标签文字
|
* 沿 DOM 文档流往前遍历,查找 input 对应的标签文字
|
||||||
* 从 input 出发逆向走,逐个检查文本节点
|
* 从 input 出发逆向走,逐个检查文本节点
|
||||||
*
|
*
|
||||||
* 停止条件:
|
* 停止条件:
|
||||||
* - 碰到另一个 input/textarea/select 元素
|
* - 碰到另一个 input/textarea/select 元素
|
||||||
* - 碰到大标题元素(titleElements 集合中的元素)
|
* - 碰到大标题元素(titleElements 集合中的元素)
|
||||||
* - 碰到包含 input 的非祖先元素(前一个字段区域)
|
* - 碰到包含 input 的非祖先元素(前一个字段区域)
|
||||||
* - 最多走 80 个节点
|
* - 最多走 80 个节点
|
||||||
*
|
*
|
||||||
* 过滤条件:跳过不含至少2个汉字的文本,跳过 excludeTexts 中的文本
|
* 过滤条件:跳过不含至少2个汉字的文本,跳过 excludeTexts 中的文本
|
||||||
*/
|
*/
|
||||||
function walkBackwardForLabel(inp: Element, titleElements?: Set<Element>, excludeTexts?: Set<string>): { text: string; element: Element } | null {
|
function walkBackwardForLabel(inp: Element, titleElements?: Set<Element>, excludeTexts?: Set<string>): { text: string; element: Element } | null {
|
||||||
@@ -108,142 +95,26 @@ function walkBackwardForLabel(inp: Element, titleElements?: Set<Element>, exclud
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 查找 input/textarea 元素对应的标签文字
|
* 查找 input/textarea 元素对应的标签文字
|
||||||
*
|
*
|
||||||
* @param inp - 输入框元素
|
* @param inp - 输入框元素
|
||||||
* @param titleElements - 大标题元素集合(用于第二遍策略的停止边界,可选)
|
* @param titleElements - 大标题元素集合(用于逆向遍历的停止边界,可选)
|
||||||
* @param excludeTexts - 需要排除的文字集合(简历数据/缓存中的已填值,遇到时跳过继续找,可选)
|
* @param excludeTexts - 需要排除的文字集合(简历数据/缓存中的已填值,遇到时跳过继续找,可选)
|
||||||
* @returns { labelText, labelElement } 标签文字和标签元素
|
* @returns { labelText, labelElement } 标签文字和标签元素
|
||||||
*
|
*
|
||||||
* 策略流程:
|
* 策略:纯 DOM 逆向遍历,从 input 往前走找到第一个有效中文标签文本节点
|
||||||
* 1. closest(FORM_ITEM_SELS) 找容器
|
* 不依赖任何 form-item 类名容器,适配所有网站
|
||||||
* 2. 验证容器内是否同时有标签和输入框,不满足则逐级向上找(最多4层)
|
|
||||||
* 3. 在容器内 querySelectorAll 找 input 前面的第一个有效文字元素
|
|
||||||
* 4. fallback: previousElementSibling 查找
|
|
||||||
* 5. 第二遍:如果以上都未得到含≥2个汉字的标签,启动 walkBackwardForLabel 逆向遍历
|
|
||||||
* 6. 都找不到则返回 placeholder 或 "(未知字段)"
|
|
||||||
*/
|
*/
|
||||||
export function findLabelForInput(inp: Element, titleElements?: Set<Element>, excludeTexts?: Set<string>): { labelText: string; labelElement: Element | null } {
|
export function findLabelForInput(inp: Element, titleElements?: Set<Element>, excludeTexts?: Set<string>): { labelText: string; labelElement: Element | null } {
|
||||||
let labelText = ""
|
// 直接走 DOM 逆向遍历,从 input 往前找最近的有效中文标签
|
||||||
let labelElement: Element | null = null
|
const foundLabel = walkBackwardForLabel(inp, titleElements, excludeTexts)
|
||||||
let container: Element | null = null
|
if (foundLabel) {
|
||||||
for (const sel of FORM_ITEM_SELS) {
|
return { labelText: foundLabel.text, labelElement: foundLabel.element }
|
||||||
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)) {
|
|
||||||
const foundLabel = walkBackwardForLabel(inp, titleElements, excludeTexts)
|
|
||||||
if (foundLabel) {
|
|
||||||
labelText = foundLabel.text
|
|
||||||
labelElement = foundLabel.element
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 最终 fallback:都找不到才用 placeholder 或返回"(未知字段)"
|
// 最终 fallback:都找不到才用 placeholder 或返回"(未知字段)"
|
||||||
if (!labelText) {
|
const placeholder = (inp as HTMLInputElement).getAttribute("placeholder") || ""
|
||||||
const placeholder = (inp as HTMLInputElement).getAttribute("placeholder") || ""
|
if (placeholder && !EXCLUDE_LABEL_INCLUDES.some((inc) => placeholder.includes(inc)) && !EXCLUDE_LABEL_TEXTS.some((ex) => placeholder === ex)) {
|
||||||
// placeholder 如果包含排除片段(如"请输入"、"请选择"),不作为标签
|
return { labelText: placeholder, labelElement: null }
|
||||||
if (placeholder && !EXCLUDE_LABEL_INCLUDES.some((inc) => placeholder.includes(inc)) && !EXCLUDE_LABEL_TEXTS.some((ex) => placeholder === ex)) {
|
|
||||||
labelText = placeholder
|
|
||||||
} else {
|
|
||||||
labelText = "(未知字段)"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return { labelText, labelElement }
|
return { labelText: "(未知字段)", labelElement: null }
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user