Files
offerpai_browser_plug/src/lib/formStyle.ts
T

304 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 表单样式控制模块
* 用于在自动填写过程中,对匹配到的表单字段容器设置背景色高亮反馈
* 颜色含义:green=填写成功(淡绿色)、red=填写失败/必填未填(淡红色)、yellow=非必填跳过(淡黄色)
*/
/** 高亮颜色类型 */
export type HighlightColor = "green" |"greenTwo" | "red" | "yellow"
/** 颜色映射表(淡色系,不影响文字可读性) */
const COLOR_MAP: Record<HighlightColor, string> = {
green: "#b7ffc5", // 淡绿色
greenTwo: "#b7ffc6", // 淡绿色2
red: "#ffb7b7", // 淡红色
yellow: "transparent", // 透明色
}
/** 常见的表单项容器选择器(与 formMatcher.ts 保持一致) */
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']",
]
// ============ 必填红*标记检测 ============
/**
* 非必填字段的红*标记样式特征排除配置
* 用于兼容不同网站:某些网站的红*标记不代表必填,通过此配置排除误判
*
* 使用场景:特殊网站中某些 label 带红*但实际非必填,可以把这些特征加进来排除
*
* @property containerClassExcludes - 如果表单项容器包含这些 class 片段则跳过必填判定
* @property labelClassExcludes - 如果标签元素包含这些 class 片段则跳过必填判定
* @property labelTextExcludes - 如果标签文字包含这些关键词则跳过必填判定
*/
export interface RequiredMarkExcludeConfig {
/** 容器 class 排除特征(className 中包含任一则视为非必填) */
containerClassExcludes?: string[]
/** 标签 class 排除特征 */
labelClassExcludes?: string[]
/** 标签文字排除关键词 */
labelTextExcludes?: string[]
}
/**
* 检测表单字段是否有红*必填标记
*
* 检测策略(按优先级):
* 1. input 元素自身的 required 属性 / aria-required="true"
* 2. 表单项容器 class 包含 required / is-required 等关键词
* 3. 标签元素或其子元素中存在纯文字 "*" 的红色节点
* 4. 标签元素或其相邻元素的 ::before / ::after 伪元素 content 包含 "*"
*
* @param labelElement - 标签 DOM 元素(从 MatchedFormField.labelElement 获取)
* @param inputElement - 输入框 DOM 元素(从 MatchedFormField.inputElement 获取)
* @param excludeConfig - 排除配置(预留给特殊网站兼容)
* @returns true=该字段有必填标记,false=未检测到必填标记
*/
export function isRequiredField(
labelElement: Element | null,
inputElement: HTMLInputElement | HTMLTextAreaElement | null,
excludeConfig?: RequiredMarkExcludeConfig
): boolean {
// ---- 策略1input 自身 required 属性 ----
if (inputElement) {
if (inputElement.hasAttribute("required") || inputElement.getAttribute("aria-required") === "true") {
return true
}
}
// ---- 找到表单项容器 ----
const formItemContainer = findFormItemContainer(labelElement, inputElement)
// ---- 排除配置检查 ----
if (excludeConfig && formItemContainer) {
const containerClass = formItemContainer.className || ""
if (excludeConfig.containerClassExcludes?.some((cls) => containerClass.includes(cls))) {
return false
}
}
if (excludeConfig && labelElement) {
const labelClass = (labelElement as HTMLElement).className || ""
if (excludeConfig.labelClassExcludes?.some((cls) => labelClass.includes(cls))) {
return false
}
const labelText = labelElement.textContent?.trim() || ""
if (excludeConfig.labelTextExcludes?.some((kw) => labelText.includes(kw))) {
return false
}
}
// ---- 策略2:容器 class 包含 required 关键词 ----
if (formItemContainer) {
const containerClass = formItemContainer.className || ""
if (/\brequired\b|is-required|isRequired|form-item--required/.test(containerClass)) {
return true
}
}
// ---- 策略3:标签元素内或附近的红色 "*" 文本节点 ----
// 搜索范围:标签元素自身 + 表单项容器内的前几个子元素
const searchElements: Element[] = []
if (labelElement) searchElements.push(labelElement)
if (formItemContainer) {
// 在容器内寻找标签附近的小元素(span/em/i 等可能放红*的标签)
const smallEls = formItemContainer.querySelectorAll("span, em, i, sup, label")
for (const el of Array.from(smallEls)) {
// 只看标签元素前后附近的(DOM 位置接近的)
if (labelElement && isNearLabel(el, labelElement)) {
searchElements.push(el)
}
}
}
for (const el of searchElements) {
if (hasRedAsteriskText(el)) return true
}
// ---- 策略4:伪元素 ::before / ::after 检测 ----
if (labelElement && labelElement instanceof HTMLElement) {
if (hasPseudoElementAsterisk(labelElement)) return true
}
// 也检查容器内的 label 标签(有些框架把伪元素放在 label 上)
if (formItemContainer) {
const labels = formItemContainer.querySelectorAll("label, [class*='label']")
for (const lbl of Array.from(labels)) {
if (lbl instanceof HTMLElement && hasPseudoElementAsterisk(lbl)) return true
}
}
return false
}
/**
* 查找标签/输入框所在的表单项容器
* 与 formMatcher.ts 中 findNearestInput 使用的容器选择器一致
*/
function findFormItemContainer(
labelElement: Element | null,
inputElement: Element | null
): Element | null {
// 优先从 labelElement 向上找
if (labelElement) {
for (const sel of FORM_ITEM_SELECTORS) {
const container = labelElement.closest(sel)
if (container) return container
}
}
// fallback:从 inputElement 向上找
if (inputElement) {
for (const sel of FORM_ITEM_SELECTORS) {
const container = inputElement.closest(sel)
if (container) return container
}
}
// 最后兜底:labelElement 的父级(最多向上3层)
let parent = labelElement?.parentElement || inputElement?.parentElement || null
for (let i = 0; i < 3 && parent; i++) {
if (parent !== document.body) return parent
parent = parent.parentElement
}
return null
}
/**
* 判断元素是否在标签元素附近(DOM 距离近)
*/
function isNearLabel(el: Element, labelElement: Element): boolean {
// 如果是标签的子元素或标签本身
if (labelElement.contains(el) || el.contains(labelElement)) return true
// 如果是标签的前后兄弟
if (el.previousElementSibling === labelElement || el.nextElementSibling === labelElement) return true
// 如果和标签在同一个父元素内且距离不超过3个节点
if (el.parentElement === labelElement.parentElement) return true
return false
}
/**
* 检测元素内是否有红色的 "*" 文本
* 遍历元素的子节点,查找包含 "*" 的文本节点,并检查其颜色是否为红色系
*/
function hasRedAsteriskText(el: Element): boolean {
// 检查元素自身的直接文本子节点
for (const child of Array.from(el.childNodes)) {
if (child.nodeType === Node.TEXT_NODE) {
const text = child.textContent || ""
if (text.includes("*")) {
// 检查该文本所在元素的颜色
const color = getComputedColor(el)
if (isRedColor(color)) return true
}
}
}
// 检查子元素(如 <span>*</span>
for (const child of Array.from(el.children)) {
const text = child.textContent?.trim() || ""
if (text === "*" || text === "* " || text === " *") {
const color = getComputedColor(child)
if (isRedColor(color)) return true
}
}
return false
}
/**
* 检测元素的 ::before 或 ::after 伪元素是否包含 "*"
* 这是最常见的必填标记实现方式(如 Element UI、Ant Design
*/
function hasPseudoElementAsterisk(el: HTMLElement): boolean {
for (const pseudo of ["::before", "::after"] as const) {
const style = window.getComputedStyle(el, pseudo)
const content = style.getPropertyValue("content")
// content 格式形如 '"*"' 或 '"* "' 或 '"\uff0a"'(全角星号)
if (content && (content.includes("*") || content.includes("\\*") || content.includes("\uff0a"))) {
// 进一步确认颜色是红色系(排除装饰性星号)
const color = style.getPropertyValue("color")
if (isRedColor(color)) return true
// 有些情况不设颜色但 content 确实是 "*",也认为是必填
if (content.replace(/['"\\s ]/g, "") === "*") return true
}
}
return false
}
/** 获取元素的计算颜色值 */
function getComputedColor(el: Element): string {
if (!(el instanceof HTMLElement)) return ""
return window.getComputedStyle(el).getPropertyValue("color")
}
/**
* 判断颜色值是否为红色系
* 支持 rgb / rgba / hex 格式
*/
function isRedColor(color: string): boolean {
if (!color) return false
// rgb(r, g, b) 或 rgba(r, g, b, a) 格式
const rgbMatch = color.match(/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/)
if (rgbMatch) {
const r = parseInt(rgbMatch[1])
const g = parseInt(rgbMatch[2])
const b = parseInt(rgbMatch[3])
// 红色系:R 通道较高,G 和 B 通道较低
return r > 150 && g < 100 && b < 100
}
// 常见红色关键词
if (color === "red" || color.includes("#f5222d") || color.includes("#ff4d4f") || color.includes("#e00")) return true
return false
}
// ============ 背景高亮设置 ============
/**
* 设置表单字段容器的背景高亮颜色
* 通过行内样式直接覆盖原网页 class 中的背景色
*
* @param element - 要设置背景色的 HTML 元素(input / textarea / div 等)
* @param color - 高亮颜色:green=成功 red=失败 yellow=跳过
*/
export function setFieldHighlight(element: Element | null, color: HighlightColor): void {
if (!element || !(element instanceof HTMLElement)) return
if (!element.isConnected) return // 元素已脱离 DOM 则跳过
element.style.backgroundColor = COLOR_MAP[color]
}
/**
* 清除表单字段容器的背景高亮颜色(恢复为透明)
*
* @param element - 要清除背景色的 HTML 元素
*/
export function clearFieldHighlight(element: Element | null): void {
if (!element || !(element instanceof HTMLElement)) return
if (!element.isConnected) return
element.style.backgroundColor = ""
}
/**
* 批量设置多个元素的背景高亮颜色
*
* @param elements - 要设置的元素数组
* @param color - 高亮颜色
*/
export function setFieldsHighlight(elements: (Element | null)[], color: HighlightColor): void {
for (const el of elements) {
setFieldHighlight(el, color)
}
}
/**
* 批量清除多个元素的背景高亮颜色
*
* @param elements - 要清除的元素数组
*/
export function clearFieldsHighlight(elements: (Element | null)[]): void {
for (const el of elements) {
clearFieldHighlight(el)
}
}