经历部分填写逻辑优化

This commit is contained in:
2026-05-12 21:21:59 +08:00
parent 888d4450f1
commit 84a2a3993a
6 changed files with 1971 additions and 387 deletions
+256 -3
View File
@@ -1,5 +1,5 @@
/**
* 日期选择器填写模块
* 日期选择器填写模块(支持年月日或年月的时间选择)
* 负责:通用日期选择器的日历面板操作
*
* 核心思路:
@@ -785,6 +785,231 @@ async function navigateToYearMonth(
return finalYear === targetYear && finalMonth === targetMonth
}
// ============ 月份面板检测与点击 ============
/**
* 检测当前弹出的是否为月份选择面板,如果是则导航年份并点击目标月份
*
* 月份面板特征:
* - 包含12个格子,文字为 "一月"~"十二月" 或 "1月"~"12月" 或 "Jan"~"Dec"
* - 有年份显示和前后翻页按钮
* - 没有星期行和1~31的日期格子
*
* 【注意】复用 detectNavButtons 的按钮探测逻辑(点击观察年份变化)来找年份加减按钮
*/
async function tryFillMonthPanel(
targetYear: number,
targetMonth: number,
labelText: string,
fillValue: string,
inputElement: HTMLElement
): Promise<boolean> {
// 在页面中查找可见的月份面板
const panelSelectors = [
'[class*="month-panel"]', '[class*="month-picker"]',
'[class*="picker-panel"]', '[class*="calendar-month"]',
'[class*="picker-body"]', '[class*="picker-content"]',
'[class*="popover"]', '[class*="popper"]', '[class*="popup"]',
'[class*="dropdown"]', '[class*="overlay"]',
]
let monthPanel: HTMLElement | null = null
let monthCells: HTMLElement[] = []
for (const sel of panelSelectors) {
const panels = document.querySelectorAll(sel)
for (const panel of Array.from(panels)) {
const htmlPanel = panel as HTMLElement
if (!isVisible(htmlPanel)) continue
const cells = findMonthCells(htmlPanel)
if (cells.length >= 10) {
monthPanel = htmlPanel
monthCells = cells
break
}
}
if (monthPanel) break
}
// 兜底:在 body 直接子元素中找
if (!monthPanel) {
for (const child of Array.from(document.body.children)) {
const htmlChild = child as HTMLElement
if (!isVisible(htmlChild) || htmlChild.offsetHeight < 50) continue
const cells = findMonthCells(htmlChild)
if (cells.length >= 10) {
monthPanel = htmlChild
monthCells = cells
break
}
}
}
if (!monthPanel || monthCells.length === 0) {
console.log(`OfferPie: [datePicker] 未检测到月份面板`)
return false
}
console.log(`OfferPie: [datePicker] ✅ 检测到月份面板,共 ${monthCells.length} 个月份格子`)
// ---- 分析月份面板头部,找到年份显示和候选按钮 ----
// 月份格子区域作为边界(头部在月份格子之上)
const monthGridArea = monthCells[0].closest("table, tbody, [class*='body'], [class*='content'], div") as HTMLElement || monthCells[0].parentElement as HTMLElement
// 用 analyzeHeaderArea 分析头部(复用已有逻辑)
const headerInfo = analyzeHeaderArea(monthPanel, monthGridArea)
const currentYear = headerInfo.currentYear
console.log(`OfferPie: [datePicker] 月份面板当前年份: ${currentYear},目标年份: ${targetYear}`)
// ---- 用按钮探测逻辑找年份加减按钮(点击观察年份变化) ----
if (currentYear !== 0 && currentYear !== targetYear && headerInfo.headerButtons.length > 0) {
// 构造一个临时的 CalendarPanelInfo 用于 detectNavButtons
const tempPanel: CalendarPanelInfo = {
panelRoot: monthPanel,
weekdayRow: monthGridArea, // 用月份格子区域作为边界
dayGridArea: monthGridArea,
yearLabel: headerInfo.yearLabel,
monthLabel: null, // 月份面板里没有独立的月份显示标签
currentYear: currentYear,
currentMonth: 0,
headerButtons: headerInfo.headerButtons,
}
// 探测导航按钮(只关心年份按钮)
const nav = await detectNavButtons(tempPanel)
// 导航到目标年份
if (nav.yearPrev || nav.yearNext) {
const yearDiff = targetYear - (readCurrentYear(tempPanel) || currentYear)
const yearBtn = yearDiff > 0 ? nav.yearNext : nav.yearPrev
if (yearBtn) {
const steps = Math.abs(yearDiff)
console.log(`OfferPie: [datePicker] 月份面板年份导航: ${yearDiff > 0 ? "+" : ""}${yearDiff}`)
for (let i = 0; i < steps && i < 50; i++) {
yearBtn.click()
await delay(150)
const newYear = readCurrentYear(tempPanel)
if (newYear === targetYear) break
}
}
} else if (nav.monthPrev || nav.monthNext) {
// 有些月份面板的按钮被探测为"月份按钮"(因为点击后年份变了12个月=1年)
// 这种情况用月份按钮来导航年份
const yearDiff = targetYear - (readCurrentYear(tempPanel) || currentYear)
const btn = yearDiff > 0 ? nav.monthNext : nav.monthPrev
if (btn) {
const steps = Math.abs(yearDiff)
for (let i = 0; i < steps && i < 50; i++) {
btn.click()
await delay(150)
const newYear = readCurrentYear(tempPanel)
if (newYear === targetYear) break
}
}
}
// 导航后重新获取月份格子(DOM 可能更新了)
monthCells = findMonthCells(monthPanel)
}
// ---- 点击目标月份格子 ----
for (const cell of monthCells) {
const cellMonth = getMonthFromCell(cell)
if (cellMonth === targetMonth) {
const cls = ((cell as HTMLElement).className || "") + " " + ((cell.parentElement as HTMLElement)?.className || "")
if (cls.includes("disabled")) continue
console.log(`OfferPie: [datePicker] 📅 点击月份格子: ${targetMonth}`)
// 找最内层叶子节点点击(兼容不同组件库)
let deepest: HTMLElement = cell
const inner = cell.querySelector("[class*='inner'], [class*='content'], span, a")
if (inner && isVisible(inner)) deepest = inner as HTMLElement
deepest.click()
await delay(100)
if (deepest !== cell) cell.click()
await delay(300)
await closePopup(inputElement)
console.log(`OfferPie: ✅ [月份选择器] 已选择 "${labelText}" = "${fillValue}" (${targetYear}${targetMonth}月)`)
return true
}
}
console.log(`OfferPie: [datePicker] ❌ 月份面板中未找到 ${targetMonth}月 的格子`)
return false
}
/**
* 在容器中查找月份格子元素
* 月份格子特征:文字为 "一月"~"十二月" / "1月"~"12月" / "Jan"~"Dec" / 纯数字1~12
*/
function findMonthCells(container: HTMLElement): HTMLElement[] {
const cells: HTMLElement[] = []
// 常见月份格子选择器
const selectors = [
'[class*="month-panel-cell"]', '[class*="month-cell"]',
'[class*="picker-cell"]', 'td[class*="cell"]',
'[class*="month-table"] td', '[class*="month-body"] td',
'[role="gridcell"]',
]
for (const sel of selectors) {
const candidates = container.querySelectorAll(sel)
const validCells: HTMLElement[] = []
for (const el of Array.from(candidates)) {
if (!isVisible(el)) continue
const month = getMonthFromCell(el as HTMLElement)
if (month !== null) validCells.push(el as HTMLElement)
}
if (validCells.length >= 10) return validCells
}
// 兜底:遍历所有可见的短文本元素
const allEls = container.querySelectorAll("td, div, span, a")
const fallbackCells: HTMLElement[] = []
for (const el of Array.from(allEls)) {
if (!isVisible(el)) continue
const month = getMonthFromCell(el as HTMLElement)
if (month !== null) {
// 确保不是年份数字(排除 2020~2030 等)
const text = getFullText(el)
if (extractYear(text) !== null) continue
fallbackCells.push(el as HTMLElement)
}
}
// 去重(如果父子元素都匹配,只保留最内层)
const filtered = fallbackCells.filter((el) => {
return !fallbackCells.some((other) => other !== el && el.contains(other))
})
if (filtered.length >= 10) return filtered
return []
}
/**
* 从格子元素中提取月份数字
* 支持:一月~十二月、1月~12月、Jan~Dec、纯数字1~12
*/
function getMonthFromCell(el: HTMLElement): number | null {
const text = getFullText(el).trim()
// 中文月份:一月~十二月
for (const [zh, num] of Object.entries(ZH_MONTH_NUMS)) {
if (text === zh + "月" || text === zh) return num
}
// 数字月份:1月~12月 或 01~12
const numMatch = text.match(/^0?(\d{1,2})月?$/)
if (numMatch) {
const n = parseInt(numMatch[1], 10)
if (n >= 1 && n <= 12) return n
}
// 英文月份
const lower = text.toLowerCase()
for (const [key, val] of Object.entries(EN_MONTH_MAP)) {
if (lower === key || lower === key.slice(0, 3)) return val
}
return null
}
// ============ 点击日期格子 ============
/**
@@ -871,12 +1096,24 @@ function parseDateValue(fillValue: string): { year: number; month: number; day:
return { year: parseInt(sepMatch[1], 10), month: parseInt(sepMatch[2], 10), day: parseInt(sepMatch[3], 10) }
}
// YYYY-MM / YYYY.MM / YYYY/MM(只有年月,day=0 表示不需要点日期格子)
const yearMonthMatch = fillValue.match(/^(\d{4})[\/\-.](\d{1,2})$/)
if (yearMonthMatch) {
return { year: parseInt(yearMonthMatch[1], 10), month: parseInt(yearMonthMatch[2], 10), day: 0 }
}
// YYYY年MM月DD日
const zhMatch = fillValue.match(/(\d{4})年(\d{1,2})月(\d{1,2})日?/)
if (zhMatch) {
return { year: parseInt(zhMatch[1], 10), month: parseInt(zhMatch[2], 10), day: parseInt(zhMatch[3], 10) }
}
// YYYY年MM月(只有年月)
const zhYMMatch = fillValue.match(/(\d{4})年(\d{1,2})月/)
if (zhYMMatch) {
return { year: parseInt(zhYMMatch[1], 10), month: parseInt(zhYMMatch[2], 10), day: 0 }
}
// YYYYMMDD
const compactMatch = fillValue.match(/^(\d{4})(\d{2})(\d{2})$/)
if (compactMatch) {
@@ -929,12 +1166,19 @@ export async function fillDatePicker(field: MatchedFormField): Promise<boolean>
}
const { year: targetYear, month: targetMonth, day: targetDay } = dateInfo
console.log(`OfferPie: [datePicker] 目标日期: ${targetYear}${targetMonth}${targetDay}`)
console.log(`OfferPie: [datePicker] 目标日期: ${targetYear}${targetMonth}${targetDay === 0 ? "(仅年月)" : targetDay + "日"}`)
// ---- 步骤2:写入日期字符串(触发面板联动) ----
forceSetValue(inputElement, fillValue)
await delay(500)
// ---- 步骤2.5:如果只有年月(day=0),检测是否为月份面板 ----
if (targetDay === 0) {
const monthPanelResult = await tryFillMonthPanel(targetYear, targetMonth, labelText, fillValue, inputElement as HTMLElement)
if (monthPanelResult) return true
// 如果不是月份面板,继续走日期面板逻辑(day 默认选1号)
}
// ---- 步骤3:分析日历面板结构 ----
const panel = analyzeCalendarPanel()
if (!panel) {
@@ -960,7 +1204,16 @@ export async function fillDatePicker(field: MatchedFormField): Promise<boolean>
console.warn("OfferPie: [datePicker] 导航到目标年月失败,仍尝试点击日期格子")
}
// ---- 步骤6:点击目标日期格子 ----
// ---- 步骤6:点击目标日期格子(如果 day=0 表示只选年月,跳过日期点击) ----
if (targetDay === 0) {
// 只需要年月,不需要点日期格子。尝试点击月份面板中的月份
// 如果导航成功了,可能面板已经关闭(某些组件选完月份自动关闭)
await delay(200)
await closePopup(inputElement as HTMLElement)
console.log(`OfferPie: ✅ [日期] 已导航到 "${labelText}" = "${fillValue}" (${targetYear}${targetMonth}月)`)
return true
}
await delay(200)
const clicked = await clickDayCell(panel, targetDay, labelText, fillValue, inputElement as HTMLElement)
if (clicked) return true