经历部分填写逻辑优化
This commit is contained in:
+256
-3
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user