表单颜色标记,用户输入数据提交时保存

This commit is contained in:
2026-06-15 18:24:58 +08:00
parent e3a4ae2381
commit 6be840a416
8 changed files with 3127 additions and 15 deletions
+303
View File
@@ -0,0 +1,303 @@
/**
* 表单样式控制模块
* 用于在自动填写过程中,对匹配到的表单字段容器设置背景色高亮反馈
* 颜色含义:green=填写成功(淡绿色)、red=填写失败/必填未填(淡红色)、yellow=非必填跳过(淡黄色)
*/
/** 高亮颜色类型 */
export type HighlightColor = "green" |"greenTwo" | "red" | "yellow"
/** 颜色映射表(淡色系,不影响文字可读性) */
const COLOR_MAP: Record<HighlightColor, string> = {
green: "#b7ffc5", // 淡绿色
greenTwo: "#b7ffc6", // 淡绿色2
red: "#ffb7b7", // 淡红色
yellow: "#fff", // 白色
}
/** 常见的表单项容器选择器(与 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)
}
}