840 lines
32 KiB
JavaScript
840 lines
32 KiB
JavaScript
/**
|
||
* 龙蛟IBP系统主页面对象模型
|
||
*/
|
||
const BasePage = require('../utils/BasePage');
|
||
const FileUtils = require('../utils/FileUtils');
|
||
|
||
class LongiMainPage extends BasePage {
|
||
/**
|
||
* 创建主页面对象
|
||
* @param {import('@playwright/test').Page} page Playwright页面对象
|
||
*/
|
||
constructor(page) {
|
||
super(page);
|
||
|
||
// 页面元素选择器
|
||
this.selectors = {
|
||
// 侧边导航菜单 - 使用更精确的选择器
|
||
sideNav: '.ly-side-nav, .el-menu', // 主菜单
|
||
menuToggle: '.hamburger-container, .fold-btn, button.hamburger, .vab-content .toggle-icon', // 菜单展开/收起按钮,提供多个可能的选择器
|
||
menuContainer: '.el-scrollbar__view > .el-menu', // 菜单容器
|
||
menuItems: '.el-sub-menu__title, .el-menu-item', // 菜单项
|
||
menuItemText: '.titleSpan', // 菜单项文本
|
||
firstLevelIndicator: '.menuIcon', // 一级菜单指示器
|
||
thirdLevelMenu: '.el-popper.is-light.el-popover .menuTitle.canClick', // 三级菜单项
|
||
thirdLevelIndicator: '.el-icon-arrow-right', // 三级菜单指示器(箭头图标)
|
||
subMenuIndicator: '.el-sub-menu__icon-arrow' // 子菜单指示器
|
||
};
|
||
|
||
// 设置超时时间
|
||
this.timeout = 10000;
|
||
}
|
||
|
||
/**
|
||
* 检查菜单是否已展开
|
||
* @returns {Promise<boolean>} 菜单是否已展开
|
||
*/
|
||
async isMenuExpanded() {
|
||
// 获取菜单元素
|
||
const sideNav = this.page.locator(this.selectors.sideNav).first();
|
||
|
||
// 检查菜单的位置
|
||
const leftValue = await sideNav.evaluate(el => {
|
||
const style = window.getComputedStyle(el);
|
||
console.log(`菜单现在的偏移量是:${style.left}`);
|
||
return style.left;
|
||
});
|
||
|
||
// 如果left是0px,说明菜单已展开
|
||
return leftValue === '0px';
|
||
}
|
||
|
||
/**
|
||
* 点击展开菜单
|
||
* @returns {Promise<void>}
|
||
*/
|
||
async clickExpandMenu() {
|
||
try {
|
||
// 尝试查找菜单切换按钮
|
||
const toggleButton = this.page.locator(this.selectors.menuToggle).first();
|
||
|
||
// 检查按钮是否可见
|
||
const isVisible = await toggleButton.isVisible().catch(() => false);
|
||
if (!isVisible) {
|
||
console.log('菜单切换按钮不可见,尝试其他方法');
|
||
// 如果按钮不可见,可以尝试其他方法,比如键盘快捷键或直接修改DOM
|
||
return;
|
||
}
|
||
|
||
// 点击菜单切换按钮
|
||
await toggleButton.click();
|
||
console.log('已点击展开菜单');
|
||
} catch (error) {
|
||
console.error('点击展开菜单时出错:', error);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 检查并展开侧边菜单
|
||
* 如果菜单已收起,则点击展开
|
||
* @returns {Promise<boolean>} 是否执行了展开操作
|
||
*/
|
||
async expandSideMenu() {
|
||
try {
|
||
// 检查菜单是否已展开
|
||
const isExpanded = await this.isMenuExpanded();
|
||
|
||
if (!isExpanded) {
|
||
console.log('菜单未展开,点击展开');
|
||
await this.clickExpandMenu();
|
||
return true;
|
||
} else {
|
||
// console.log('菜单已经处于展开状态');
|
||
return false;
|
||
}
|
||
} catch (error) {
|
||
console.error('展开菜单时出错:', error);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 检查菜单数据文件是否存在并加载数据
|
||
*/
|
||
async checkAndLoadMenuItems() {
|
||
try {
|
||
// 加载JSON文件
|
||
const menuItems = FileUtils.loadFromJsonFile(process.env.MENU_DATA_FILE_PATH);
|
||
|
||
// 检查是否成功加载数据
|
||
if (menuItems && Array.isArray(menuItems) && menuItems.length > 0) {
|
||
console.log(`从文件 ${process.env.BASE_URL} 成功加载了 ${menuItems.length} 个菜单项`);
|
||
return menuItems;
|
||
} else {
|
||
await this.expandSideMenu();
|
||
return await this.findAndSaveMenuItems();
|
||
}
|
||
} catch (error) {
|
||
console.error(`检查并加载菜单项时出错: ${error}`);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 查找菜单项并保存到文件
|
||
*/
|
||
async findAndSaveMenuItems() {
|
||
try {
|
||
// 查找菜单项
|
||
const menuItems = await this.findMenuItems();
|
||
|
||
// 如果没有找到菜单项,则返回空数组
|
||
if (!menuItems || menuItems.length === 0) {
|
||
console.warn('未找到任何菜单项,无法保存到文件');
|
||
return [];
|
||
}
|
||
|
||
// 过滤掉不能序列化的element属性
|
||
const menuItemsForSave = menuItems.map(({element, ...rest}) => rest);
|
||
|
||
// 保存到文件
|
||
FileUtils.saveToJsonFile(menuItemsForSave, process.env.MENU_DATA_FILE_PATH);
|
||
|
||
console.log(`已找到并保存 ${menuItems.length} 个菜单项到文件: ${process.env.MENU_DATA_FILE_PATH}`);
|
||
|
||
return menuItems;
|
||
} catch (error) {
|
||
console.error('查找并保存菜单项时出错:', error);
|
||
return [];
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 查找所有菜单项
|
||
* @returns {Promise<Array>} 菜单项数组
|
||
*/
|
||
async findMenuItems() {
|
||
try {
|
||
console.log('开始查找菜单项...');
|
||
|
||
// 等待菜单加载完成
|
||
await this.page.waitForSelector(this.selectors.sideNav, {
|
||
timeout: parseInt(process.env.MENU_TIME_OUT || '30000', 10)
|
||
});
|
||
|
||
// 获取所有菜单项
|
||
const items = await this.page.locator(this.selectors.menuItems).all();
|
||
console.log(`找到 ${items.length} 个菜单项元素`);
|
||
const menuItems = [];
|
||
// 处理每个菜单项
|
||
for (let i = 0; i < items.length; i++) {
|
||
const item = items[i];
|
||
//过滤一级菜单
|
||
let isTopMenu = (await item.locator(this.selectors.firstLevelIndicator).count()) > 0;
|
||
if (isTopMenu) {
|
||
continue;
|
||
}
|
||
// 获取菜单项文本
|
||
const text = await item.textContent();
|
||
|
||
// 检查是否是可见的菜单项
|
||
const isVisible = await item.isVisible();
|
||
|
||
if (isVisible && text.trim()) {
|
||
// 检查是否有子菜单指示器
|
||
const hasSubMenuIndicator = await item.evaluate(el => {
|
||
return el.querySelector('.el-submenu__icon-arrow') !== null ||
|
||
el.querySelector('.el-icon-arrow-down') !== null;
|
||
});
|
||
|
||
// 检查是否有三级菜单指示器
|
||
const hasThirdLevelIndicator = await item.evaluate(el => {
|
||
// 检查是否有特定的三级菜单指示器
|
||
return el.classList.contains('is-opened') ||
|
||
el.querySelector('.third-level-menu') !== null ||
|
||
el.querySelector('.el-menu--inline') !== null;
|
||
});
|
||
|
||
// 检查是否是子菜单标题
|
||
const isSubMenuTitle = await item.evaluate(el => el.classList.contains('el-sub-menu__title'));
|
||
|
||
// 综合判断是否有三级菜单
|
||
const hasThirdMenu = isSubMenuTitle || hasSubMenuIndicator || hasThirdLevelIndicator;
|
||
|
||
console.log(`菜单项 "${text.trim()}" ${hasThirdMenu ? '有' : '没有'}三级菜单 (通过DOM结构判断)`);
|
||
|
||
// 生成唯一标识符,结合索引和文本
|
||
const uniqueId = `menu_${i}_${text.trim().replace(/\s+/g, '_')}`;
|
||
|
||
// 获取菜单路径
|
||
const menuPath = await this.getMenuPath(item);
|
||
|
||
menuItems.push({
|
||
index: i,
|
||
text: text.trim(),
|
||
element: item,
|
||
hasThirdMenu: hasThirdMenu,
|
||
uniqueId: uniqueId,
|
||
// 添加路径信息,帮助识别菜单层级
|
||
path: menuPath
|
||
});
|
||
}
|
||
}
|
||
|
||
console.log(`🔍 找到 ${menuItems.length} 个可测试的菜单项`);
|
||
return menuItems;
|
||
} catch (error) {
|
||
console.error('查找菜单项时出错:', error);
|
||
return [];
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取菜单项的路径信息
|
||
* @param {Object} menuItem 菜单项元素
|
||
* @returns {Promise<string>} 菜单路径
|
||
*/
|
||
async getMenuPath(menuItem) {
|
||
try {
|
||
// 尝试获取父级菜单的文本
|
||
const parentText = await menuItem.evaluate(el => {
|
||
// 查找最近的父级菜单项
|
||
const parent = el.closest('.el-submenu');
|
||
if (parent) {
|
||
const parentTitle = parent.querySelector('.el-submenu__title');
|
||
if (parentTitle) {
|
||
const titleSpan = parentTitle.querySelector('.titleSpan');
|
||
return titleSpan ? titleSpan.textContent.trim() : parentTitle.textContent.trim();
|
||
}
|
||
}
|
||
return '';
|
||
});
|
||
|
||
const itemText = await menuItem.textContent();
|
||
|
||
if (parentText) {
|
||
return `${parentText} > ${itemText}`;
|
||
}
|
||
|
||
return itemText;
|
||
} catch (error) {
|
||
console.error('获取菜单路径时出错:', error);
|
||
return '';
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 通过索引查找菜单项
|
||
* @param {number} index 菜单项索引
|
||
* @param {Array} menuItems 菜单项数组,如果未提供则会调用findMenuItems获取
|
||
* @returns {Promise<Object|null>} 找到的菜单项或null
|
||
*/
|
||
async findMenuItemByIndex(index, menuItems = null) {
|
||
try {
|
||
// 如果未提供菜单项数组,则获取
|
||
if (!menuItems) {
|
||
menuItems = await this.findMenuItems();
|
||
}
|
||
|
||
// 查找指定索引的菜单项
|
||
const menuItem = menuItems.find(item => item.index === index);
|
||
|
||
if (menuItem) {
|
||
console.log(`通过索引 ${index} 找到菜单项: "${menuItem.text}"`);
|
||
return menuItem;
|
||
} else {
|
||
console.log(`未找到索引为 ${index} 的菜单项`);
|
||
return null;
|
||
}
|
||
} catch (error) {
|
||
console.error(`通过索引查找菜单项时出错: ${error}`);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 通过文本和可选的索引查找菜单项
|
||
* 当有多个同名菜单项时,可以通过指定occurrence来选择第几个匹配项
|
||
* @param {string} text 菜单项文本
|
||
* @param {number} occurrence 第几个匹配项,从0开始计数,默认为0(第一个)
|
||
* @param {Array} menuItems 菜单项数组,如果未提供则会调用findMenuItems获取
|
||
* @returns {Promise<Object|null>} 找到的菜单项或null
|
||
*/
|
||
async findMenuItemByText(text, occurrence = 0, menuItems = null) {
|
||
try {
|
||
// 如果未提供菜单项数组,则获取
|
||
if (!menuItems) {
|
||
menuItems = await this.findMenuItems();
|
||
}
|
||
|
||
// 查找所有匹配文本的菜单项
|
||
const matchingItems = menuItems.filter(item => item.text === text);
|
||
|
||
if (matchingItems.length > 0) {
|
||
if (occurrence < matchingItems.length) {
|
||
const menuItem = matchingItems[occurrence];
|
||
console.log(`通过文本 "${text}" 找到第 ${occurrence + 1} 个菜单项,索引为 ${menuItem.index}`);
|
||
return menuItem;
|
||
} else {
|
||
console.log(`未找到第 ${occurrence + 1} 个文本为 "${text}" 的菜单项,只有 ${matchingItems.length} 个匹配项`);
|
||
return null;
|
||
}
|
||
} else {
|
||
console.log(`未找到文本为 "${text}" 的菜单项`);
|
||
return null;
|
||
}
|
||
} catch (error) {
|
||
console.error(`通过文本查找菜单项时出错: ${error}`);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 通过唯一ID查找菜单项
|
||
* @param {string} uniqueId 菜单项的唯一ID
|
||
* @param {Array} menuItems 菜单项数组,如果未提供则会调用findMenuItems获取
|
||
* @returns {Promise<Object|null>} 找到的菜单项或null
|
||
*/
|
||
async findMenuItemByUniqueId(uniqueId, menuItems = null) {
|
||
try {
|
||
// 如果未提供菜单项数组,则获取
|
||
if (!menuItems) {
|
||
menuItems = await this.findMenuItems();
|
||
}
|
||
|
||
// 查找指定唯一ID的菜单项
|
||
const menuItem = menuItems.find(item => item.uniqueId === uniqueId);
|
||
|
||
if (menuItem) {
|
||
console.log(`通过唯一ID ${uniqueId} 找到菜单项: "${menuItem.text}"`);
|
||
return menuItem;
|
||
} else {
|
||
console.log(`未找到唯一ID为 ${uniqueId} 的菜单项`);
|
||
return null;
|
||
}
|
||
} catch (error) {
|
||
console.error(`通过唯一ID查找菜单项时出错: ${error}`);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 检查是否存在三级菜单并进行相应处理
|
||
* @param {Object} menuItem 菜单项对象,包含索引、文本和元素
|
||
* @param {number} timeout 超时时间(毫秒),默认30秒
|
||
* @returns {Promise<{hasThirdMenu: boolean, thirdMenuItems: Array}>} 是否有三级菜单及三级菜单项数组
|
||
*/
|
||
async checkForThirdLevelMenu(menuItem, timeout = 30000) {
|
||
try {
|
||
console.log(`点击菜单项 "${menuItem.text}" 检查三级菜单...`);
|
||
|
||
// 点击菜单项,触发三级菜单弹出
|
||
await menuItem.element.click();
|
||
|
||
// 等待弹出动画完成,增加等待时间
|
||
await this.page.waitForTimeout(2000);
|
||
|
||
// 检查三级菜单
|
||
const thirdMenuSelector = this.selectors.thirdLevelMenu;
|
||
|
||
// 尝试等待三级菜单出现,但不抛出错误
|
||
const hasThirdMenu = await this.page.locator(thirdMenuSelector).count().then(count => count > 0).catch(() => false);
|
||
|
||
// 如果没有立即找到,再等待一段时间再次检查
|
||
let thirdMenuCount = 0;
|
||
if (hasThirdMenu) {
|
||
thirdMenuCount = await this.page.locator(thirdMenuSelector).count();
|
||
} else {
|
||
// 再等待一段时间,有些菜单可能加载较慢
|
||
await this.page.waitForTimeout(1000);
|
||
thirdMenuCount = await this.page.locator(thirdMenuSelector).count();
|
||
}
|
||
|
||
console.log(`菜单项 "${menuItem.text}" ${thirdMenuCount > 0 ? '有' : '没有'}三级菜单,找到 ${thirdMenuCount} 个三级菜单项`);
|
||
|
||
// 如果没有三级菜单,直接返回
|
||
if (thirdMenuCount === 0) {
|
||
return {hasThirdMenu: false, thirdMenuItems: []};
|
||
}
|
||
|
||
// 收集三级菜单项
|
||
const thirdMenuItems = [];
|
||
for (let i = 0; i < thirdMenuCount; i++) {
|
||
const item = this.page.locator(thirdMenuSelector).nth(i);
|
||
const text = await item.textContent();
|
||
|
||
thirdMenuItems.push({
|
||
index: i,
|
||
text: text.trim(),
|
||
element: item
|
||
});
|
||
}
|
||
|
||
// 输出三级菜单项文本
|
||
const menuTexts = thirdMenuItems.map(item => item.text).join(', ');
|
||
console.log(`三级菜单项: ${menuTexts}`);
|
||
|
||
return {hasThirdMenu: true, thirdMenuItems};
|
||
} catch (error) {
|
||
console.error(`检查三级菜单时出错: ${error}`);
|
||
return {hasThirdMenu: false, thirdMenuItems: []};
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 处理菜单项,包括检查和处理可能的三级菜单
|
||
* @param {Object} menuItem 菜单项对象,包含索引、文本和元素以及hasThirdMenu标志
|
||
* @param {Function} processWithThirdMenu 处理有三级菜单情况的回调函数
|
||
* @param {Function} processWithoutThirdMenu 处理没有三级菜单情况的回调函数
|
||
* @returns {Promise<{hasThirdMenu: boolean, thirdMenuItems: Array}>} 处理结果
|
||
*/
|
||
async processMenuItem(menuItem, processWithThirdMenu = null, processWithoutThirdMenu = null) {
|
||
try {
|
||
console.log(`处理菜单项: "${menuItem.text}"`);
|
||
|
||
// 使用菜单项中的hasThirdMenu字段
|
||
const hasThirdMenu = menuItem.hasThirdMenu;
|
||
|
||
// 点击菜单项
|
||
await menuItem.element.click();
|
||
await this.page.waitForTimeout(1000); // 等待可能的动画完成
|
||
|
||
let thirdMenuItems = [];
|
||
|
||
// 如果有三级菜单,获取三级菜单项
|
||
if (hasThirdMenu) {
|
||
const thirdMenuSelector = this.selectors.thirdLevelMenu;
|
||
const thirdMenuCount = await this.page.locator(thirdMenuSelector).count();
|
||
|
||
console.log(`菜单项 "${menuItem.text}" 有三级菜单,找到 ${thirdMenuCount} 个三级菜单项`);
|
||
|
||
// 收集三级菜单项
|
||
for (let i = 0; i < thirdMenuCount; i++) {
|
||
const item = this.page.locator(thirdMenuSelector).nth(i);
|
||
const text = await item.textContent();
|
||
|
||
thirdMenuItems.push({
|
||
index: i,
|
||
text: text.trim(),
|
||
element: item
|
||
});
|
||
}
|
||
|
||
// 输出三级菜单项文本
|
||
const menuTexts = thirdMenuItems.map(item => item.text).join(', ');
|
||
console.log(`三级菜单项: ${menuTexts}`);
|
||
} else {
|
||
console.log(`菜单项 "${menuItem.text}" 没有三级菜单`);
|
||
}
|
||
|
||
// 根据是否有三级菜单调用相应的处理函数
|
||
if (hasThirdMenu && processWithThirdMenu) {
|
||
await processWithThirdMenu(menuItem, thirdMenuItems);
|
||
} else if (!hasThirdMenu && processWithoutThirdMenu) {
|
||
await processWithoutThirdMenu(menuItem);
|
||
}
|
||
|
||
return {hasThirdMenu, thirdMenuItems};
|
||
} catch (error) {
|
||
console.error(`处理菜单项时出错: ${error}`);
|
||
return {hasThirdMenu: false, thirdMenuItems: []};
|
||
}
|
||
}
|
||
|
||
async handleAllMenus(menuItems) {
|
||
for (let i = 0; i < menuItems.length; i++) {
|
||
await this.handleSingleMenu(menuItems[i]);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 恢复菜单项的element元素
|
||
* @param {Object} menuItem 需要恢复element的菜单项
|
||
* @returns {Promise<Object>} 返回更新后的菜单项
|
||
* @private
|
||
*/
|
||
async restoreMenuElement(menuItem) {
|
||
try {
|
||
// 获取当前页面上的所有菜单元素
|
||
const currentMenuElements = await this.page.locator(this.selectors.menuItems).all();
|
||
let foundElement = null;
|
||
let sameTextElements = [];
|
||
|
||
// 首先找到所有文本匹配的元素
|
||
for (const element of currentMenuElements) {
|
||
const elementText = await element.textContent();
|
||
if (elementText.trim() === menuItem.text) {
|
||
// 检查是否是一级菜单
|
||
const isTopMenu = await element.locator(this.selectors.firstLevelIndicator).count() > 0;
|
||
if (!isTopMenu) {
|
||
sameTextElements.push(element);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 如果找到多个同名菜单,根据是否有三级菜单来区分
|
||
if (sameTextElements.length > 0) {
|
||
for (const element of sameTextElements) {
|
||
// 检查是否有三级菜单指示器
|
||
const hasThirdMenu = await element.evaluate(el => {
|
||
return el.classList.contains('el-sub-menu__title') ||
|
||
el.querySelector('.el-submenu__icon-arrow') !== null;
|
||
});
|
||
|
||
if (hasThirdMenu === menuItem.hasThirdMenu) {
|
||
foundElement = element;
|
||
break;
|
||
}
|
||
}
|
||
|
||
// 如果还没找到,就用第一个匹配的元素
|
||
if (!foundElement && sameTextElements.length > 0) {
|
||
foundElement = sameTextElements[0];
|
||
}
|
||
}
|
||
|
||
if (foundElement) {
|
||
menuItem.element = foundElement;
|
||
console.log(`✅ 成功恢复菜单项 "${menuItem.text}" (${menuItem.hasThirdMenu ? '有' : '无'}三级菜单) 的element元素`);
|
||
} else {
|
||
console.warn(`⚠️ 无法恢复菜单项 "${menuItem.text}" (${menuItem.hasThirdMenu ? '有' : '无'}三级菜单) 的element元素`);
|
||
}
|
||
|
||
return menuItem;
|
||
} catch (error) {
|
||
console.error(`恢复菜单项element时出错 [${menuItem.text}]:`, error.message);
|
||
return menuItem;
|
||
}
|
||
}
|
||
|
||
async handleSingleMenu(menu) {
|
||
await this.expandSideMenu();
|
||
// 在处理菜单前重新获取最新的element
|
||
menu = await this.restoreMenuElement(menu);
|
||
|
||
if (!menu.element) {
|
||
console.error(`无法找到菜单项 "${menu.text}" 的element,跳过处理`);
|
||
return;
|
||
}
|
||
|
||
if (menu.hasThirdMenu) {
|
||
await this.handleThreeLevelMenu(menu);
|
||
} else {
|
||
// 处理二级菜单点击
|
||
await this.handleMenuClick(menu);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 等待页面加载完成,带重试机制
|
||
* @param {Object} menu 菜单对象
|
||
* @param {string} subMenuText 子菜单文本(可选)
|
||
* @returns {Promise<boolean>} 页面是否加载成功
|
||
*/
|
||
async waitForPageLoadWithRetry(menu, subMenuText = '') {
|
||
const pageName = subMenuText ? `${menu.text} > ${subMenuText}` : menu.text;
|
||
const pageStartTime = Date.now();
|
||
console.log(`等待页面 ${pageName} 数据加载...`);
|
||
|
||
const config = {
|
||
maxRetries: 30,
|
||
retryInterval: 500,
|
||
stabilityDelay: 1000
|
||
};
|
||
|
||
let retryCount = 0;
|
||
|
||
try {
|
||
while (retryCount < config.maxRetries) {
|
||
// 检查页面状态
|
||
const selectors = {
|
||
loadingMask: '.el-loading-mask',
|
||
errorBox: '.el-message-box__message',
|
||
errorMessage: '.el-message--error'
|
||
};
|
||
|
||
// 检查错误状态
|
||
for (const [key, selector] of Object.entries(selectors)) {
|
||
const elements = await this.page.locator(selector).all();
|
||
if (elements.length > 0 && key !== 'loadingMask') {
|
||
const errorText = await elements[0].textContent();
|
||
console.error(`页面加载出现错误: ${pageName}, 错误信息: ${errorText}`);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
// 检查加载状态
|
||
const loadingMasks = await this.page.locator(selectors.loadingMask).all();
|
||
if (loadingMasks.length === 0) {
|
||
// 加载完成,等待页面稳定
|
||
await this.page.waitForTimeout(config.stabilityDelay);
|
||
console.log(`✅ 页面 ${pageName} 加载完成`);
|
||
return true;
|
||
}
|
||
|
||
// 继续等待
|
||
retryCount++;
|
||
await this.page.waitForTimeout(config.retryInterval);
|
||
}
|
||
|
||
// 超时处理
|
||
console.error(`页面加载超时: ${pageName}, 重试次数: ${config.maxRetries}`);
|
||
return false;
|
||
|
||
} catch (error) {
|
||
// 任何错误都记录并返回false
|
||
console.error(`页面加载出错: ${pageName}, 错误信息: ${error.message}`);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 关闭当前活动的标签页
|
||
* @param {string} pageName 页面名称,用于日志显示
|
||
* @returns {Promise<boolean>} 是否成功关闭标签页
|
||
*/
|
||
async closeActiveTab(pageName) {
|
||
try {
|
||
console.log(`🗑️ 正在关闭页面 "${pageName}" 的tab...`);
|
||
// 检查是否存在活动的tab和关闭按钮
|
||
const activeTab = this.page.locator('.vab-tabs .el-tabs--card .el-tabs__item.is-active');
|
||
const closeButton = activeTab.locator('.el-icon.is-icon-close');
|
||
|
||
const hasActiveTab = await activeTab.count() > 0;
|
||
const hasCloseButton = await closeButton.count() > 0;
|
||
|
||
if (hasActiveTab && hasCloseButton) {
|
||
// 确保关闭按钮可见并点击
|
||
await closeButton.waitFor({state: 'visible', timeout: 5000});
|
||
await closeButton.click();
|
||
|
||
// 等待关闭动画完成
|
||
await this.page.waitForTimeout(500);
|
||
return true;
|
||
} else {
|
||
console.log(`⚠️ [${pageName}] 没有找到可关闭的tab,继续执行...`);
|
||
return false;
|
||
}
|
||
} catch (error) {
|
||
console.error(`关闭标签页时出错 [${pageName}]:`, error.message);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取三级菜单列表
|
||
* @param {Object} parentMenu 父级菜单对象
|
||
* @returns {Promise<Array>} 三级菜单项数组
|
||
* @private
|
||
*/
|
||
async getThirdLevelMenus(parentMenu) {
|
||
try {
|
||
// 检查三级菜单是否存在
|
||
const elements = await this.page.locator('.el-popper.is-light.el-popover .menuTitle.canClick').all();
|
||
if (elements.length === 0) {
|
||
return [];
|
||
}
|
||
// 收集所有三级菜单项
|
||
const thirdMenus = [];
|
||
for (let i = 0; i < elements.length; i++) {
|
||
const element = elements[i];
|
||
const text = await element.textContent();
|
||
|
||
thirdMenus.push({
|
||
index: i,
|
||
text: text.trim(),
|
||
element: element,
|
||
hasThirdMenu: false, // 三级菜单项不会有下一级
|
||
uniqueId: `menu_${parentMenu.uniqueId}_${i}_${text.trim().replace(/\s+/g, '_')}`,
|
||
path: `${parentMenu.text} > ${text.trim()}`
|
||
});
|
||
}
|
||
|
||
return thirdMenus;
|
||
} catch (error) {
|
||
console.error(`获取三级菜单失败: ${error.message}`);
|
||
return [];
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 处理三级菜单
|
||
* @param {Object} menu 菜单对象
|
||
*/
|
||
async handleThreeLevelMenu(menu) {
|
||
try {
|
||
console.log(`正在处理 ${menu.text} 菜单`);
|
||
await menu.element.click();
|
||
|
||
// 等待一个短暂的时间让弹出层出现
|
||
await this.page.waitForTimeout(500);
|
||
|
||
// 获取三级菜单列表
|
||
const thirdMenus = await this.getThirdLevelMenus(menu);
|
||
|
||
// 这里可以根据需要处理三级菜单项
|
||
for (const thirdMenu of thirdMenus) {
|
||
await this.handleMenuClick(thirdMenu, menu.text);
|
||
|
||
// 如果不是最后一个菜单项,需要重新点击父菜单展开三级菜单
|
||
if (thirdMenu !== thirdMenus[thirdMenus.length - 1]) {
|
||
await menu.element.click();
|
||
await this.page.waitForTimeout(500);
|
||
}
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error(`处理三级菜单时出错 [${menu.text}]:`, error.message);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 通用的菜单点击和页面加载处理方法
|
||
* @param {Object} menuInfo 菜单信息对象,包含text属性
|
||
* @param {string} parentText 父级菜单文本(可选)
|
||
* @returns {Promise<boolean>} 处理是否成功
|
||
*/
|
||
async handleMenuClick(menuInfo, parentText = '') {
|
||
try {
|
||
const menuPath = parentText ? `${parentText} > ${menuInfo.text}` : menuInfo.text;
|
||
console.log(`🔸 点击菜单: ${menuPath}`);
|
||
|
||
await menuInfo.element.click();
|
||
|
||
// 等待页面加载
|
||
const loadResult = await this.waitForPageLoadWithRetry(menuInfo);
|
||
if (!loadResult) {
|
||
console.warn('页面加载失败');
|
||
return false;
|
||
}
|
||
|
||
// 处理所有TAB页
|
||
await this.handleAllTabs(menuInfo);
|
||
|
||
// 关闭标签页
|
||
await this.closeActiveTab(menuPath);
|
||
return true;
|
||
} catch (error) {
|
||
console.error(`处理菜单点击失败 [${menuInfo.text}]:`, error.message);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 处理单个TAB页
|
||
* @param {Object} tabInfo TAB页信息对象,包含text、isActive和element属性
|
||
* @param {Object} parentMenu 父级菜单对象
|
||
* @returns {Promise<boolean>} 处理是否成功
|
||
* @private
|
||
*/
|
||
async handleSingleTab(tabInfo, parentMenu) {
|
||
try {
|
||
const menuPath = parentMenu.path || parentMenu.text;
|
||
console.log(`🔹 处理TAB页: ${menuPath} > ${tabInfo.text}`);
|
||
|
||
// 直接使用传入的element点击
|
||
await tabInfo.element.click();
|
||
|
||
// 等待页面加载
|
||
const loadResult = await this.waitForPageLoadWithRetry(parentMenu, tabInfo.text);
|
||
if (!loadResult) {
|
||
console.warn('页面加载失败');
|
||
return false;
|
||
}
|
||
|
||
return true;
|
||
} catch (error) {
|
||
console.error(`处理TAB页失败 [${parentMenu.text} > ${tabInfo.text}]:`, error.message);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 处理所有TAB页
|
||
* @param {Object} menu 菜单对象
|
||
* @returns {Promise<boolean>} 处理是否成功
|
||
* @private
|
||
*/
|
||
async handleAllTabs(menu) {
|
||
try {
|
||
// 等待TAB容器加载
|
||
await this.page.waitForTimeout(1000);
|
||
|
||
// 使用更精确的选择器获取工作区的TAB页
|
||
const tabs = await this.page.locator('.workSpaceBaseTab .el-tabs__item').all();
|
||
if (tabs.length === 0) {
|
||
console.log(`📝 ${menu.text} 没有TAB页`);
|
||
return true;
|
||
}
|
||
|
||
console.log(`📑 ${menu.text} 找到 ${tabs.length} 个TAB页`);
|
||
|
||
// 获取所有TAB页的完整信息(文本、激活状态和元素引用)
|
||
const tabInfos = await Promise.all(
|
||
tabs.map(async element => ({
|
||
text: (await element.textContent()).trim(),
|
||
isActive: await element.evaluate(el => el.classList.contains('is-active')),
|
||
element: element // 保存元素引用
|
||
}))
|
||
);
|
||
|
||
// 处理每个非激活的TAB页
|
||
for (const tabInfo of tabInfos) {
|
||
// 跳过当前激活的TAB页,因为它已经是默认加载的
|
||
if (!tabInfo.isActive) {
|
||
await this.handleSingleTab(tabInfo, menu);
|
||
} else {
|
||
console.log(`⏭️ 跳过当前激活的TAB页: ${menu.text} > ${tabInfo.text}`);
|
||
}
|
||
}
|
||
|
||
return true;
|
||
} catch (error) {
|
||
console.error(`处理TAB页失败 [${menu.text}]:`, error.message);
|
||
return false;
|
||
}
|
||
}
|
||
}
|
||
|
||
module.exports = LongiMainPage; |