/** * 表单样式控制模块 * 用于在自动填写过程中,对匹配到的表单字段容器设置背景色高亮反馈 * 颜色含义:green=填写成功(淡绿色)、red=填写失败/必填未填(淡红色)、yellow=非必填跳过(淡黄色) */ /** 高亮颜色类型 */ export type HighlightColor = "green" |"greenTwo" | "red" | "yellow" /** 颜色映射表(淡色系,不影响文字可读性) */ const COLOR_MAP: Record = { 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 { // ---- 策略1:input 自身 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 } } } // 检查子元素(如 *) 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) } }