表单颜色标记,用户输入数据提交时保存
This commit is contained in:
@@ -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 {
|
||||
// ---- 策略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
|
||||
}
|
||||
}
|
||||
}
|
||||
// 检查子元素(如 <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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user