feat: 完成美团|抖音自动化脚本

This commit is contained in:
duanshuwen committed 2026-03-14 22:47:15 +08:00
1 parent a6f010a1f4
commit 2fc8ad3140
9 files changed
+9089 -10

No files matched your search

File diff suppressed because it is too large. Load diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 245 KiB

+222 -2
View File
@@ -1,4 +1,224 @@
import { chromium } from 'playwright';
import log from 'electron-log';
import tabsPkg from './common/tabs.js';
log.info('dy_trace.mjs placeholder: not implemented');
process.exit(0);
const { preparePage, safeDisconnectBrowser } = tabsPkg;
const parseDateInput = (dateStr) => {
if (!dateStr) return null;
if (typeof dateStr === 'number' && Number.isFinite(dateStr)) return new Date(dateStr);
if (dateStr instanceof Date && !Number.isNaN(dateStr.getTime())) return dateStr;
const raw = String(dateStr).trim();
const ymdMatch = raw.match(/(\d{4}-\d{2}-\d{2})/);
if (ymdMatch) return new Date(`${ymdMatch[1]}T00:00:00`);
const mmddMatch = raw.match(/(\d{2}-\d{2})/);
if (mmddMatch) {
const year = new Date().getFullYear();
return new Date(`${year}-${mmddMatch[1]}T00:00:00`);
}
return null;
};
const formatDate = (d) => {
const yyyy = d.getFullYear();
const mm = String(d.getMonth() + 1).padStart(2, '0');
const dd = String(d.getDate()).padStart(2, '0');
return `${yyyy}-${mm}-${dd}`;
};
const buildDateList = (startDate, endDate) => {
const start = parseDateInput(startDate);
const end = parseDateInput(endDate || startDate);
if (!start || !end) return [];
const from = start <= end ? start : end;
const to = start <= end ? end : start;
const dates = [];
const cur = new Date(from.getTime());
while (cur <= to) {
dates.push(formatDate(cur));
cur.setDate(cur.getDate() + 1);
}
return dates;
};
const toggleRoomByDate = async (page, { roomType, dateStr, operation }) => {
// Find row by room type
const row = page.locator('.lifep-table-row').filter({ hasText: roomType }).first();
if (await row.count() === 0) {
throw new Error(`Room type not found: ${roomType}`);
}
// Find cell by date class (e.g., .date-2026-03-14)
const cell = row.locator(`.lifep-table-cell.date-${dateStr}`);
if (await cell.count() === 0) {
log.warn(`Date cell not found: ${dateStr}`);
return;
}
// Find switch components
// Structure: .byted-switch (label) > input[type=checkbox] + .byted-switch-wrapper (visual)
// The user provided structure shows: .lifep-table-cell... > .room-swtich-container-PxrDYy > .byted-switch > .byted-switch-wrapper
// We can just find .byted-switch inside the cell, it should be unique enough.
const switchLabel = cell.locator('.byted-switch').first();
const switchWrapper = cell.locator('.byted-switch-wrapper').first();
const switchInput = cell.locator('input[type="checkbox"]').first();
if (await switchLabel.count() === 0) {
// If not found, log details and return
log.warn(`Switch not found for ${roomType} on ${dateStr}`);
return;
}
await switchWrapper.scrollIntoViewIfNeeded();
// Check state
// Preferred: check input state or wrapper class
let isOpen = false;
if (await switchInput.count() > 0) {
// In dy.md, when checked, the input has a `checked=""` attribute.
// However, playwright's isChecked() might not always sync perfectly if the framework uses synthetic events.
// So we check both isChecked and the classes.
isOpen = await switchInput.isChecked();
}
if (!isOpen) {
const labelClass = await switchLabel.getAttribute('class') || '';
const wrapperClass = await switchWrapper.getAttribute('class') || '';
// Based on dy.md: `byted-switch-checked` is added to the label
// and `byted-switch-wrapper-checked` is added to the wrapper
isOpen = labelClass.includes('byted-switch-checked') || wrapperClass.includes('byted-switch-wrapper-checked');
}
if (operation === 'open' && !isOpen) {
// Click the wrapper as seen in recording
await switchWrapper.click();
log.info(`Opened room ${roomType} on ${dateStr}`);
await handleConfirmation(page);
} else if (operation === 'close' && isOpen) {
await switchWrapper.click();
log.info(`Closed room ${roomType} on ${dateStr}`);
await handleConfirmation(page);
} else {
log.info(`Room ${roomType} already ${operation === 'open' ? 'open' : 'closed'} on ${dateStr}`);
}
};
const handleConfirmation = async (page) => {
try {
const confirmBtn = page.getByRole('button', { name: '确定' });
// Wait slightly longer for dialog animation
if (await confirmBtn.isVisible({ timeout: 3000 })) {
await confirmBtn.click();
// Wait for dialog to disappear
await confirmBtn.waitFor({ state: 'hidden', timeout: 3000 }).catch(() => {});
}
} catch (e) {
// Ignore if no confirmation dialog appears
}
};
const navigateToRoomStatusManagement = async (page) => {
try {
const currentUrl = page.url();
if (currentUrl.includes('/p/travel-ari/hotel/price_amount_state')) return;
} catch (e) {}
try {
const tableRow = page.locator('.lifep-table-row').first();
if (await tableRow.isVisible({ timeout: 1500 })) return;
} catch (e) {}
const managementEntry = page.getByText('房价房态管理', { exact: true });
try {
if (await managementEntry.isVisible({ timeout: 1500 })) return;
} catch (e) {}
const candidates = [
page.locator('div').filter({ hasText: /^房价房态$/ }).nth(1),
page.locator('div').filter({ hasText: /^房价房态$/ }).first(),
page.getByText('房价房态', { exact: true }),
page.getByRole('menuitem', { name: '房价房态' }).first(),
];
for (const candidate of candidates) {
try {
await candidate.waitFor({ state: 'visible', timeout: 6000 });
await candidate.click({ timeout: 6000, force: true });
if (await managementEntry.isVisible({ timeout: 3000 })) break;
} catch (e) {}
}
await managementEntry.waitFor({ state: 'visible', timeout: 10000 });
await managementEntry.click({ timeout: 10000, force: true });
};
(async () => {
let browser;
try {
const groupId = '1816249020842116';
const homeUrl = `https://life.douyin.com/p/home?groupid=${groupId}`;
const priceAmountStateUrl = `https://life.douyin.com/p/travel-ari/hotel/price_amount_state?groupid=${groupId}`;
const prepared = await preparePage(chromium, { targetUrl: priceAmountStateUrl });
browser = prepared.browser;
const page = prepared.page;
try {
if (!page.url().includes('/p/travel-ari/hotel/price_amount_state')) {
await page.goto(priceAmountStateUrl, { waitUntil: 'domcontentloaded' });
}
} catch (e) {
try {
await page.goto(homeUrl, { waitUntil: 'domcontentloaded' });
} catch (e2) {}
}
// Navigation logic (User provided)
try {
// Try to click the store/login button if visible
const storeBtn = page.getByText('息烽南山天沐温泉我创建的');
if (await storeBtn.isVisible({ timeout: 3000 })) {
await storeBtn.click();
await page.getByRole('button', { name: '确认登录' }).click();
await page.waitForTimeout(2000);
}
// Navigate to Price/Room Status
await navigateToRoomStatusManagement(page);
await page.waitForTimeout(2000);
} catch (e) {
log.warn('Navigation steps skipped or failed, assuming already on page or different flow:', e.message);
}
const roomType = process.env.ROOM_TYPE;
const startDate = process.env.START_DATE;
const endDate = process.env.END_DATE;
const operation = process.env.OPERATION === 'close' ? 'close' : 'open';
if (!roomType || !startDate) {
log.info('ROOM_TYPE/START_DATE not provided, skip room toggle.');
return;
}
// Wait for table rows to appear
await page.locator('.lifep-table-row').first().waitFor({ state: 'visible', timeout: 15000 });
const dateList = buildDateList(startDate, endDate);
for (const dateStr of dateList) {
try {
await toggleRoomByDate(page, { roomType, dateStr, operation });
} catch (e) {
log.error(`Failed to toggle ${roomType} on ${dateStr}: ${e.message}`);
}
await page.waitForTimeout(500 + Math.random() * 500);
}
} catch (error) {
log.error(error);
process.exitCode = 1;
} finally {
await safeDisconnectBrowser(browser);
}
})();
+205 -3
View File
@@ -4,15 +4,217 @@ import tabsPkg from './common/tabs.js';
const { preparePage, safeDisconnectBrowser } = tabsPkg;
const parseDateInput = (dateStr) => {
if (!dateStr) return null;
if (typeof dateStr === 'number' && Number.isFinite(dateStr)) return new Date(dateStr);
if (dateStr instanceof Date && !Number.isNaN(dateStr.getTime())) return dateStr;
const raw = String(dateStr).trim();
const ymdMatch = raw.match(/(\d{4}-\d{2}-\d{2})/);
if (ymdMatch) return new Date(`${ymdMatch[1]}T00:00:00`);
const mmddMatch = raw.match(/(\d{2}-\d{2})/);
if (mmddMatch) {
const year = new Date().getFullYear();
return new Date(`${year}-${mmddMatch[1]}T00:00:00`);
}
return null;
};
const formatDate = (d) => {
const yyyy = d.getFullYear();
const mm = String(d.getMonth() + 1).padStart(2, '0');
const dd = String(d.getDate()).padStart(2, '0');
return `${yyyy}-${mm}-${dd}`;
};
const buildDateList = (startDate, endDate) => {
const start = parseDateInput(startDate);
const end = parseDateInput(endDate || startDate);
if (!start || !end) return [];
const from = start <= end ? start : end;
const to = start <= end ? end : start;
const dates = [];
const cur = new Date(from.getTime());
while (cur <= to) {
dates.push(formatDate(cur));
cur.setDate(cur.getDate() + 1);
}
return dates;
};
const toggleRoomByDate = async (page, { roomType, dateStr, operation }) => {
// Find row by room type
const row = page.locator('.lifep-table-row').filter({ hasText: roomType }).first();
if (await row.count() === 0) {
throw new Error(`Room type not found: ${roomType}`);
}
// Find cell by date class (e.g., .date-2026-03-14)
const cell = row.locator(`.lifep-table-cell.date-${dateStr}`);
if (await cell.count() === 0) {
log.warn(`Date cell not found: ${dateStr}`);
return;
}
// Find switch components
// Structure: .byted-switch (label) > input[type=checkbox] + .byted-switch-wrapper (visual)
// The user provided structure shows: .lifep-table-cell... > .room-swtich-container-PxrDYy > .byted-switch > .byted-switch-wrapper
// We can just find .byted-switch inside the cell, it should be unique enough.
const switchLabel = cell.locator('.byted-switch').first();
const switchWrapper = cell.locator('.byted-switch-wrapper').first();
const switchInput = cell.locator('input[type="checkbox"]').first();
if (await switchLabel.count() === 0) {
// If not found, log details and return
log.warn(`Switch not found for ${roomType} on ${dateStr}`);
return;
}
await switchWrapper.scrollIntoViewIfNeeded();
// Check state
// Preferred: check input state or wrapper class
let isOpen = false;
if (await switchInput.count() > 0) {
// In dy.md, when checked, the input has a `checked=""` attribute.
// However, playwright's isChecked() might not always sync perfectly if the framework uses synthetic events.
// So we check both isChecked and the classes.
isOpen = await switchInput.isChecked();
}
if (!isOpen) {
const labelClass = await switchLabel.getAttribute('class') || '';
const wrapperClass = await switchWrapper.getAttribute('class') || '';
// Based on dy.md: `byted-switch-checked` is added to the label
// and `byted-switch-wrapper-checked` is added to the wrapper
isOpen = labelClass.includes('byted-switch-checked') || wrapperClass.includes('byted-switch-wrapper-checked');
}
if (operation === 'open' && !isOpen) {
// Click the wrapper as seen in recording
await switchWrapper.click();
log.info(`Opened room ${roomType} on ${dateStr}`);
await handleConfirmation(page);
} else if (operation === 'close' && isOpen) {
await switchWrapper.click();
log.info(`Closed room ${roomType} on ${dateStr}`);
await handleConfirmation(page);
} else {
log.info(`Room ${roomType} already ${operation === 'open' ? 'open' : 'closed'} on ${dateStr}`);
}
};
const handleConfirmation = async (page) => {
try {
const confirmBtn = page.getByRole('button', { name: '确定' });
// Wait slightly longer for dialog animation
if (await confirmBtn.isVisible({ timeout: 3000 })) {
await confirmBtn.click();
// Wait for dialog to disappear
await confirmBtn.waitFor({ state: 'hidden', timeout: 3000 }).catch(() => {});
}
} catch (e) {
// Ignore if no confirmation dialog appears
}
};
const navigateToRoomStatusManagement = async (page) => {
try {
const currentUrl = page.url();
if (currentUrl.includes('/p/travel-ari/hotel/price_amount_state')) return;
} catch (e) {}
try {
const tableRow = page.locator('.lifep-table-row').first();
if (await tableRow.isVisible({ timeout: 1500 })) return;
} catch (e) {}
const managementEntry = page.getByText('房价房态管理', { exact: true });
try {
if (await managementEntry.isVisible({ timeout: 1500 })) return;
} catch (e) {}
const candidates = [
page.locator('div').filter({ hasText: /^房价房态$/ }).nth(1),
page.locator('div').filter({ hasText: /^房价房态$/ }).first(),
page.getByText('房价房态', { exact: true }),
page.getByRole('menuitem', { name: '房价房态' }).first(),
];
for (const candidate of candidates) {
try {
await candidate.waitFor({ state: 'visible', timeout: 6000 });
await candidate.click({ timeout: 6000, force: true });
if (await managementEntry.isVisible({ timeout: 3000 })) break;
} catch (e) {}
}
await managementEntry.waitFor({ state: 'visible', timeout: 10000 });
await managementEntry.click({ timeout: 10000, force: true });
};
(async () => {
let browser;
try {
const targetUrl = process.env.TARGET_URL || 'https://life.douyin.com/';
const prepared = await preparePage(chromium, { targetUrl });
const groupId = '1816249020842116';
const homeUrl = `https://life.douyin.com/p/home?groupid=${groupId}`;
const priceAmountStateUrl = `https://life.douyin.com/p/travel-ari/hotel/price_amount_state?groupid=${groupId}`;
const prepared = await preparePage(chromium, { targetUrl: priceAmountStateUrl });
browser = prepared.browser;
const page = prepared.page;
try {
if (!page.url().includes('/p/travel-ari/hotel/price_amount_state')) {
await page.goto(priceAmountStateUrl, { waitUntil: 'domcontentloaded' });
}
} catch (e) {
try {
await page.goto(homeUrl, { waitUntil: 'domcontentloaded' });
} catch (e2) {}
}
// Navigation logic (User provided)
try {
// Try to click the store/login button if visible
const storeBtn = page.getByText('息烽南山天沐温泉我创建的');
if (await storeBtn.isVisible({ timeout: 3000 })) {
await storeBtn.click();
await page.getByRole('button', { name: '确认登录' }).click();
await page.waitForTimeout(2000);
}
// Navigate to Price/Room Status
await navigateToRoomStatusManagement(page);
await page.waitForTimeout(2000);
} catch (e) {
log.warn('Navigation steps skipped or failed, assuming already on page or different flow:', e.message);
}
const roomType = process.env.ROOM_TYPE;
const startDate = process.env.START_DATE;
const endDate = process.env.END_DATE;
const operation = process.env.OPERATION === 'close' ? 'close' : 'open';
if (!roomType || !startDate) {
log.info('ROOM_TYPE/START_DATE not provided, skip room toggle.');
return;
}
// Wait for table rows to appear
await page.locator('.lifep-table-row').first().waitFor({ state: 'visible', timeout: 15000 });
const dateList = buildDateList(startDate, endDate);
for (const dateStr of dateList) {
try {
await toggleRoomByDate(page, { roomType, dateStr, operation });
} catch (e) {
log.error(`Failed to toggle ${roomType} on ${dateStr}: ${e.message}`);
}
await page.waitForTimeout(500 + Math.random() * 500);
}
log.info('dy_hotel_trace.mjs placeholder: not implemented');
} catch (error) {
log.error(error);
process.exitCode = 1;
+41 -4
View File
@@ -42,6 +42,40 @@ const buildDateList = (startDate, endDate) => {
return dates;
};
const clickWithRetry = async (locator, { attempts = 6 } = {}) => {
let lastError;
for (let i = 0; i < attempts; i++) {
try {
await locator.scrollIntoViewIfNeeded().catch(() => {});
await locator.waitFor({ state: 'visible', timeout: 8000 });
await locator.click({ timeout: 8000, force: true });
return;
} catch (e) {
lastError = e;
try {
await locator.page().keyboard.press('Escape');
} catch (e2) {}
await locator.page().waitForTimeout(400 + Math.random() * 600);
}
}
throw lastError;
};
const ensureRoomPriceCalendar = async (page) => {
const container = page.locator('#price-reserve-table-container');
try {
if (await container.isVisible({ timeout: 1500 })) return;
} catch (e) {}
const menuTitle = page.getByRole('menuitem', { name: /房价房量管理/ }).first();
await clickWithRetry(menuTitle);
const calendarEntry = page.getByRole('menuitem', { name: /房价房量日历/ }).first();
await clickWithRetry(calendarEntry);
await container.waitFor({ state: 'visible', timeout: 30000 });
};
const findHeaderDateIndex = async (container, mmdd) => {
for (let attempt = 0; attempt < 10; attempt++) {
const headerCells = container.locator(
@@ -62,7 +96,12 @@ const findHeaderDateIndex = async (container, mmdd) => {
};
const toggleRoomByDateIndex = async (container, { roomType, dateIndex, operation }) => {
const roomAnchor = container.getByText(roomType, { exact: true }).first();
const strictAnchor = container.getByText(roomType, { exact: true }).first();
const looseAnchor = container.getByText(roomType).first();
const roomAnchor = (await strictAnchor.count()) > 0 ? strictAnchor : looseAnchor;
if ((await roomAnchor.count()) === 0) {
throw new Error(`Room type not found in table: ${roomType}`);
}
await roomAnchor.scrollIntoViewIfNeeded();
const row = roomAnchor.locator(
@@ -101,9 +140,7 @@ const toggleRoomByDateIndex = async (container, { roomType, dateIndex, operation
await page.waitForTimeout(4000 + Math.random() * 300);
await page.getByRole('menuitem', { name: '房价房量管理' }).click();
await page.waitForTimeout(4000 + Math.random() * 1000);
await page.getByText('房价房量日历').click();
await ensureRoomPriceCalendar(page);
const roomType = process.env.ROOM_TYPE;
const startDate = process.env.START_DATE;
+305
View File
@@ -0,0 +1,305 @@
```html
<section class="ant-layout ant-layout-has-sider" style="min-height: calc(-100px + 100vh); overflow: hidden;"><div class="menu___1gSsA" style="max-width: 236px;"><aside class="ant-layout-sider ant-layout-sider-light printHidden sider___2CnY3" style="height: 100%; overflow: auto; flex: 0 0 200px; max-width: 200px; min-width: 200px; width: 200px;"><div class="ant-layout-sider-children"><ul class="ant-menu ant-menu-root ant-menu-inline ant-menu-light" role="menu" tabindex="0" data-menu-list="true" style="width: 200px; padding-bottom: 40px;"><li class="ant-menu-item" role="menuitem" tabindex="-1" data-menu-id="rc-menu-uuid-56984-1-0" style="padding-left: 16px;"><img class="menuIcon___3eTy4 ant-menu-item-icon" src="https://img.alicdn.com/imgextra/i3/O1CN01uPdotX2AIwuZM13h6_!!6000000008181-2-tps-32-32.png" style="margin-right: 6px;"><span class="ant-menu-title-content"><span>首页</span></span></li><li class="ant-menu-item" role="menuitem" tabindex="-1" data-menu-id="rc-menu-uuid-56984-1-1" style="padding-left: 16px;"><img class="menuIcon___3eTy4 ant-menu-item-icon" src="https://img.alicdn.com/imgextra/i3/O1CN01Mrv2EJ1zH9zpm2z4O_!!6000000006688-55-tps-18-17.svg" style="margin-right: 6px;"><span class="ant-menu-title-content"><span>AI 助理</span></span></li><li class="ant-menu-submenu ant-menu-submenu-inline" role="none"><div role="menuitem" class="ant-menu-submenu-title" tabindex="-1" data-menu-id="rc-menu-uuid-56984-1-2" aria-expanded="false" aria-haspopup="true" aria-controls="rc-menu-uuid-56984-1-2-popup" style="padding-left: 16px;"><img class="menuIcon___3eTy4 ant-menu-item-icon" src="https://img.alicdn.com/imgextra/i3/O1CN01X5RgMo1bp04vOu1MZ_!!6000000003513-2-tps-32-32.png"><span style="margin-left: 4px;">交易管理 </span><i class="ant-menu-submenu-arrow"></i></div></li><li class="ant-menu-submenu ant-menu-submenu-inline ant-menu-submenu-open ant-menu-submenu-selected" role="none"><div role="menuitem" class="ant-menu-submenu-title" tabindex="-1" data-menu-id="rc-menu-uuid-56984-1-3" aria-expanded="true" aria-haspopup="true" aria-controls="rc-menu-uuid-56984-1-3-popup" style="padding-left: 16px;"><img class="menuIcon___3eTy4 ant-menu-item-icon" src="https://img.alicdn.com/imgextra/i4/O1CN01tn1vPH1N1AM0C6MxT_!!6000000001509-2-tps-32-32.png"><span style="margin-left: 4px;" data-spm-anchor-id="0.0.0.i0.43d517aeiMi1C7">房价房量管理 </span><i class="ant-menu-submenu-arrow"></i></div><ul class="ant-menu ant-menu-sub ant-menu-inline" id="rc-menu-uuid-56984-1-3-popup" data-menu-list="true" style=""><li role="menuitem" tabindex="-1" data-menu-id="rc-menu-uuid-56984-1-3-0" class="ant-menu-item ant-menu-item-selected ant-menu-item-only-child" style="padding-left: 32px;"><span class="ant-menu-title-content"><span class="subMenu___3b-b_" data-spm-anchor-id="0.0.0.i1.43d517aeiMi1C7">房价房量日历 </span></span></li><li role="menuitem" tabindex="-1" data-menu-id="rc-menu-uuid-56984-1-3-1" class="ant-menu-item ant-menu-item-only-child" style="padding-left: 32px;"><span class="ant-menu-title-content"><span class="subMenu___3b-b_">批量修改-房量 </span></span></li><li role="menuitem" tabindex="-1" data-menu-id="rc-menu-uuid-56984-1-3-2" class="ant-menu-item ant-menu-item-only-child" style="padding-left: 32px;"><span class="ant-menu-title-content"><span class="subMenu___3b-b_">批量修改-价格 </span></span></li><li role="menuitem" tabindex="-1" data-menu-id="rc-menu-uuid-56984-1-3-3" class="ant-menu-item ant-menu-item-only-child" style="padding-left: 32px;"><span class="ant-menu-title-content"><span class="subMenu___3b-b_">价库助手 <img class="hotIcon____MFb1" src="//img.alicdn.com/imgextra/i3/O1CN01OnzMIB272VqDvjH7q_!!6000000007739-2-tps-52-52.png" style="height: 26px;"> </span></span></li><li role="menuitem" tabindex="-1" data-menu-id="rc-menu-uuid-56984-1-3-4" class="ant-menu-item ant-menu-item-only-child" style="padding-left: 32px;"><span class="ant-menu-title-content"><span class="subMenu___3b-b_">价格联动 </span></span></li><li role="menuitem" tabindex="-1" data-menu-id="rc-menu-uuid-56984-1-3-5" class="ant-menu-item ant-menu-item-only-child" style="padding-left: 32px;"><span class="ant-menu-title-content"><span class="subMenu___3b-b_">日历套餐管理 </span></span></li><li role="menuitem" tabindex="-1" data-menu-id="rc-menu-uuid-56984-1-3-6" class="ant-menu-item ant-menu-item-only-child" style="padding-left: 32px;"><span class="ant-menu-title-content"><span class="subMenu___3b-b_">协议保留房 </span></span></li></ul></li><li class="ant-menu-submenu ant-menu-submenu-inline" role="none"><div role="menuitem" class="ant-menu-submenu-title" tabindex="-1" data-menu-id="rc-menu-uuid-56984-1-4" aria-expanded="false" aria-haspopup="true" aria-controls="rc-menu-uuid-56984-1-4-popup" style="padding-left: 16px;"><img class="menuIcon___3eTy4 ant-menu-item-icon" src="https://img.alicdn.com/imgextra/i1/O1CN01eh30W61d5mCcaNOAA_!!6000000003685-2-tps-32-32.png"><span style="margin-left: 4px;"Line truncated
</span><i class="ant-menu-submenu-arrow"></i></div></li><li class="ant-menu-item" role="menuitem" tabindex="-1" data-menu-id="rc-menu-uuid-56984-1-15" style="padding-left: 16px;"><img class="menuIcon___3eTy4 ant-menu-item-icon" src="https://img.alicdn.com/imgextra/i3/O1CN01IiGgQj22Mb9nE3jDe_!!6000000007106-2-tps-200-200.png" style="margin-right: 6px;"><span class="ant-menu-title-content"><span>飞猪储蓄罐</span></span></li><li class="ant-menu-item" role="menuitem" tabindex="-1" data-menu-id="rc-menu-uuid-56984-1-16" style="padding-left: 16px;"><img class="menuIcon___3eTy4 ant-menu-item-icon" src="https://img.alicdn.com/imgextra/i2/O1CN010DzOF228QccLGURdp_!!6000000007927-2-tps-38-32.png" style="margin-right: 6px;"><span class="ant-menu-title-content"><span>付费推广-全网推</span></span></li><li class="ant-menu-item" role="menuitem" tabindex="-1" data-menu-id="rc-menu-uuid-56984-1-17" style="padding-left: 16px;"><img class="menuIcon___3eTy4 ant-menu-item-icon" src="https://img.alicdn.com/imgextra/i3/O1CN01snXr9o1V0irZRwip2_!!6000000002591-2-tps-32-32.png" style="margin-right: 6px;"><span class="ant-menu-title-content"><span>付费推广-直通车</span></span></li></ul><div aria-hidden="true" style="display: none;"></div><div class="toggleOpenBtn___1UngR"><span role="img" aria-label="menu-fold" class="anticon anticon-menu-fold" style="width: 16px; margin-left: 154px;"><svg viewBox="64 64 896 896" focusable="false" class="" data-icon="menu-fold" width="1em" height="1em" fill="currentColor" aria-hidden="true"><path d="M408 442h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm-8 204c0 4.4 3.6 8 8 8h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56zm504-486H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 632H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM115.4 518.9L271.7 642c5.8 4.6 14.4.5 14.4-6.9V388.9c0-7.4-8.5-11.5-14.4-6.9L115.4 505.1a8.74 8.74 0 000 13.8z"></path></svg></span></div></div></aside></div><main class="ant-layout-content" style="overflow: hidden; position: relative;"><div id="hotel-portal-main-content" class="hotelPortalMainContent___3kGZE" style="overflow: auto; height: 100%;"><div class="ant-spin-nested-loading"><div class="ant-spin-container"><div id="subAppWrapper" class="subAppWrapper___1OhRg" style="min-height: 80vh; display: flex;"><div class="ant-pro-layout-watermark-wrapper" style="position: relative; width: 100%;"><div id="subApp" class="subApp___-HD6G"><div id="__qiankun_microapp_wrapper_for_ebk_rp__" data-name="/ebk-rp" data-version="2.10.16" data-sandbox-cfg="true"><qiankun-head><style data-rc-order="prepend" rc-util-key="@ant-design-icons">
.anticon {
display: inline-flex;
alignItems: center;
color: inherit;
font-style: normal;
line-height: 0;
text-align: center;
text-transform: none;
vertical-align: -0.125em;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.anticon > * {
line-height: 1;
}
.anticon svg {
display: inline-block;
}
.anticon::before {
display: none;
}
.anticon .anticon-icon {
display: block;
}
.anticon[tabindex] {
cursor: pointer;
}
.anticon-spin::before,
.anticon-spin {
display: inline-block;
-webkit-animation: loadingCircle 1s infinite linear;
animation: loadingCircle 1s infinite linear;
}
@-webkit-keyframes loadingCircle {
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
@keyframes loadingCircle {
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
</style><!--dynamic script https://g.alicdn.com/trip/hotel-ebk-price-reserve/1.16.56/6365.async.js replaced by qiankun--><!--dynamic script https://g.alicdn.com/trip/hotel-ebk-price-reserve/1.16.56/5967.async.js replaced by qiankun--><!--dynamic script https://g.alicdn.com/trip/hotel-ebk-price-reserve/1.16.56/4689.async.js replaced by qiankun--><!--dynamic script https://g.alicdn.com/trip/hotel-ebk-price-reserve/1.16.56/2725.async.js replaced by qiankun--><link rel="stylesheet" type="text/css" href="//g.alicdn.com/trip/hotel-ebk-price-reserve/1.16.56/7317.chunk.css"><!--dynamic script https://g.alicdn.com/trip/hotel-ebk-price-reserve/1.16.56/7317.async.js replaced by qiankun--><link rel="stylesheet" type="text/css" href="//g.alicdn.com/trip/hotel-ebk-price-reserve/1.16.56/layouts__BasicLayout.chunk.css"><!--dynamic script https://g.alicdn.com/trip/hotel-ebk-price-reserve/1.16.56/layouts__BasicLayout.async.js replaced by qiankun--><style type="text/css">.lf-progress {
-webkit-appearance: none;
-moz-apperance: none;
width: 100%;
/* margin: 0 10px; */
height: 4px;
border-radius: 3px;
cursor: pointer;
}
.lf-progress:focus {
outline: none;
border: none;
}
.lf-progress::-moz-range-track {
cursor: pointer;
background: none;
border: none;
outline: none;
}
.lf-progress::-webkit-slider-thumb {
-webkit-appearance: none !important;
height: 13px;
width: 13px;
border: 0;
border-radius: 50%;
background: #0fccce;
cursor: pointer;
}
.lf-progress::-moz-range-thumb {
-moz-appearance: none !important;
height: 13px;
width: 13px;
border: 0;
border-radius: 50%;
background: #0fccce;
cursor: pointer;
}
.lf-progress::-ms-track {
width: 100%;
height: 3px;
cursor: pointer;
background: transparent;
border-color: transparent;
color: transparent;
}
.lf-progress::-ms-fill-lower {
background: #ccc;
border-radius: 3px;
}
.lf-progress::-ms-fill-upper {
background: #ccc;
border-radius: 3px;
}
.lf-progress::-ms-thumb {
border: 0;
height: 15px;
width: 15px;
border-radius: 50%;
background: #0fccce;
cursor: pointer;
}
.lf-progress:focus::-ms-fill-lower {
background: #ccc;
}
.lf-progress:focus::-ms-fill-upper {
background: #ccc;
}
.lf-player-container :focus {
outline: 0;
}
.lf-popover {
position: relative;
}
.lf-popover-content {
display: inline-block;
position: absolute;
opacity: 1;
visibility: visible;
transform: translate(0, -10px);
box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.26);
transition: all 0.3s cubic-bezier(0.75, -0.02, 0.2, 0.97);
}
.lf-popover-content.hidden {
opacity: 0;
visibility: hidden;
transform: translate(0, 0px);
}
.lf-player-btn-container {
display: flex;
align-items: center;
}
.lf-player-btn {
cursor: pointer;
fill: #999;
width: 14px;
}
.lf-player-btn.active {
fill: #555;
}
.lf-popover {
position: relative;
}
.lf-popover-content {
display: inline-block;
position: absolute;
background-color: #ffffff;
opacity: 1;
transform: translate(0, -10px);
box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.26);
transition: all 0.3s cubic-bezier(0.75, -0.02, 0.2, 0.97);
padding: 10px;
}
.lf-popover-content.hidden {
opacity: 0;
visibility: hidden;
transform: translate(0, 0px);
}
.lf-arrow {
position: absolute;
z-index: -1;
content: '';
bottom: -9px;
border-style: solid;
border-width: 10px 10px 0px 10px;
}
.lf-left-align,
.lf-left-align .lfarrow {
left: 0;
right: unset;
}
.lf-right-align,
.lf-right-align .lf-arrow {
right: 0;
left: unset;
}
.lf-text-input {
border: 1px #ccc solid;
border-radius: 5px;
padding: 3px;
width: 60px;
margin: 0;
}
.lf-color-picker {
display: flex;
flex-direction: row;
justify-content: space-between;
height: 90px;
}
.lf-color-selectors {
display: flex;
flex-direction: column;
justify-content: space-between;
}
.lf-color-component {
display: flex;
flex-direction: row;
font-size: 12px;
align-items: center;
justify-content: center;
}
.lf-color-component strong {
width: 40px;
}
.lf-color-component input[type='range'] {
margin: 0 0 0 10px;
}
.lf-color-component input[type='number'] {
width: 50px;
margin: 0 0 0 10px;
}
.lf-color-preview {
font-size: 12px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
padding-left: 5px;
}
.lf-preview {
height: 60px;
width: 60px;
}
.lf-popover-snapshot {
width: 150px;
}
.lf-popover-snapshot h5 {
margin: 5px 0 10px 0;
font-size: 0.75rem;
}
.lf-popover-snapshot a {
display: block;
text-decoration: none;
}
.lf-popover-snapshot a:before {
content: '⥼';
margin-right: 5px;
}
.lf-popover-snapshot .lf-note {
display: block;
margin-top: 10px;
color: #999;
}
.lf-player-controls > div {
margin-right: 5px;
margin-left: 5px;
}
.lf-player-controls > div:first-child {
margin-left: 0px;
}
.lf-player-controls > div:last-child {
margin-right: 0px;
}
</style><!--dynamic script https://g.alicdn.com/trip/hotel-ebk-price-reserve/1.16.56/5957.async.js replaced by qiankun--><link rel="stylesheet" type="text/css" href="//g.alicdn.com/trip/hotel-ebk-price-reserve/1.16.56/8724.chunk.css"><!--dynamic script https://g.alicdn.com/trip/hotel-ebk-price-reserve/1.16.56/8724.async.js replaced by qiankun--><!--dynamic script https://g.alicdn.com/trip/hotel-ebk-price-reserve/1.16.56/3885.async.js replaced by qiankun--><!--dynamic script https://g.alicdn.com/trip/hotel-ebk-price-reserve/1.16.56/49.async.js replaced by qiankun--><link rel="stylesheet" type="text/css" href="//g.alicdn.com/trip/hotel-ebk-price-reserve/1.16.56/5570.chunk.css"><!--dynamic script https://g.alicdn.com/trip/hotel-ebk-price-reserve/1.16.56/5570.async.js replaced by qiankun--><!--dynamic script https://g.alicdn.com/trip/hotel-ebk-price-reserve/1.16.56/8083.async.js replaced by qiankun--><!--dynamic script https://g.alicdn.com/trip/hotel-ebk-price-reserve/1.16.56/7731.async.js replaced by qiankun--><!--dynamic script https://g.alicdn.com/trip/hotel-ebk-price-reserve/1.16.56/8852.async.js replaced by qiankun--><!--dynamic script https://g.alicdn.com/trip/hotel-ebk-price-reserve/1.16.56/4178.async.js replaced by qiankun--><!--dynamic script https://g.alicdn.com/trip/hotel-ebk-price-reserve/1.16.56/794.async.js replaced by qiankun--><!--dynamic script https://g.alicdn.com/trip/hotel-ebk-price-reserve/1.16.56/5380.async.js replaced by qiankun--><!--dynamic script https://g.alicdn.com/trip/hotel-ebk-price-reserve/1.16.56/6850.async.js replaced by qiankun--><!--dynamic script https://g.alicdn.com/trip/hotel-ebk-price-reserve/1.16.56/1361.async.js replaced by qiankun--><!--dynamic script https://g.alicdn.com/trip/hotel-ebk-price-reserve/1.16.56/7211.async.js replaced by qiankun--><!--dynamic script https://g.alicdn.com/trip/hotel-ebk-price-reserve/1.16.56/8756.async.js replaced by qiankun--><!--dynamic script https://g.alicdn.com/trip/hotel-ebk-price-reserve/1.16.56/6160.async.js replaced by qiankun--><!--dynamic script https://g.alicdn.com/trip/hotel-ebk-price-reserve/1.16.56/1727.async.js replaced by qiankun--><!--dynamic script https://g.alicdn.com/trip/hotel-ebk-price-reserve/1.16.56/1905.async.js replaced by qiankun--><!--dynamic script https://g.alicdn.com/trip/hotel-ebk-price-reserve/1.16.56/113.async.js replaced by qiankun--><!--dynamic script https://g.alicdn.com/trip/hotel-ebk-price-reserve/1.16.56/7349.async.js replaced by qiankun--><!--dynamic script https://g.alicdn.com/trip/hotel-ebk-price-reserve/1.16.56/3957.async.js replaced by qiankun--><!--dynamic script https://g.alicdn.com/trip/hotel-ebk-price-reserve/1.16.56/7379.async.js replaced by qiankun--><!--dynamic script https://g.alicdn.com/trip/hotel-ebk-price-reserve/1.16.56/5975.async.js replaced by qiankun--><!--dynamic script https://g.alicdn.com/trip/hotel-ebk-price-reserve/1.16.56/4457.async.js replaced by qiankun--><!--dynamic script https://g.alicdn.com/trip/hotel-ebk-price-reserve/1.16.56/6203.async.js replaced by qiankun--><!--dynamic script https://g.alicdn.com/trip/hotel-ebk-price-reserve/1.16.56/1635.async.js replaced by qiankun--><!--dynamic script https://g.alicdn.com/trip/hotel-ebk-price-reserve/1.16.56/6861.async.js replaced by qiankun--><!--dynamic script https://g.alicdn.com/trip/hotel-ebk-price-reserve/1.16.56/1785.async.js replaced by qiankun--><!--dynamic script https://g.alicdn.com/trip/hotel-ebk-price-reserve/1.16.56/4804.async.js replaced by qiankun--><!--dynamic script https://g.alicdn.com/trip/hotel-ebk-price-reserve/1.16.56/7810.async.js replaced by qiankun--><!--dynamic script https://g.alicdn.com/trip/hotel-ebk-price-reserve/1.16.56/8491.async.js replaced by qiankun--><link rel="stylesheet" type="text/css" href="//g.alicdn.com/trip/hotel-ebk-price-reserve/1.16.56/p__RoomsVsManage__index.chunk.css"><!--dynamic script https://g.alicdn.com/trip/hotel-ebk-price-reserve/1.16.56/p__RoomsVsManage__index.async.js replaced by qiankun--><style type="text/css">.reactour-svg-mask {
position: fixed;
top: 0;
left: 0;
z-index: 99999;
width: 100%;
height: 100%;
color: '#000';
opacity: 0.7;
pointer-events: none;
}
</style></qiankun-head><style>/* //g.alicdn.com/trip/hotel-ebk-price-reserve/1.16.56/umi.css */.visible{visibility:visible}.\!visible{visibility:visible!important}.static{position:static}.fixed{position:fixed}.absolute{position:absolute}.\!absolute{position:absolute!important}.relative{position:relative}.\!relative{position:relative!important}.sticky{position:sticky}.\!left-\[668px\]{left:668px!important}.\!top-\[0px\]{top:0!important}.\!left-\[19px\]{left:19px!important}.\!top-\[16px\]{top:16px!important}.\!right-\[19px\]{right:19px!important}.\!top-\[7px\]{top:7px!important}.top-1{top:4px}.left-0{left:0}.bottom-0{bottom:0}.right-0{right:0}.bottom-\[5px\]{bottom:5px}.right-\[1px\]{right:1px}.top-\[1px\]{top:1px}.bottom-3{bottom:12px}.top-2{top:8px}.bottom-4{bottom:16px}.left-1{left:4px}.top-\[2px\]{top:2px}.m-0{margin:0}.mx-2{margin-left:8px;margin-right:8px}.my-2{margin-top:8px;margin-bottom:8px}.mx-\[6px\]{margin-left:6px;margin-right:6px}.mx-6{margin-left:24px;margin-right:24px}.mx-\[156px\]{margin-left:156px;margin-right:156px}.mx-1{margin-left:4px;margin-right:4px}.mx-\[2px\]{margin-left:2px;margin-right:2px}.\!mb-4{margin-bottom:16px!important}.\!mt-2{margin-top:8px!important}.mb-1{margin-bottom:4px}.ml-2{margin-left:8px}.mb-\[2px\]{margin-bottom:2px}.mr-1{margin-right:4px}.mb-2{margin-bottom:8px}.mt-2{margin-top:8px}.-mt-3{margin-top:-12px}.mb-px{margin-bottom:1px}.mb-0\.5{margin-bottom:2px}.mb-0{margin-bottom:0}.mr-0\.5{margin-right:2px}.mr-0{margin-right:0}.-ml-5{margin-left:-20px}.-mt-14{margin-top:-56px}.ml-4{margin-left:16px}.mb-6{margin-bottom:24px}.mb-4{margin-bottom:16px}.mb-3{margin-bottom:12px}.mr-6{margin-right:24px}.mt-4{margin-top:16px}.mt-6{margin-top:24px}.mt-3{margin-top:12px}.mb-\[72px\]{margin-bottom:72px}.\!mb-\[130px\]{margin-bottom:130px!important}.ml-\[8px\]{margin-left:8px}.mb-20{margin-bottom:80px}.ml-\[6px\]{margin-left:6px}.mr-\[2px\]{margin-right:2px}.ml-3{margin-left:12px}.ml-\[5px\]{margin-left:5px}.mt-\[-12px\]{margin-top:-12px}.ml-\[2px\]{margin-left:2px}.-mt-6{margin-top:-24px}.ml-5{margin-left:20px}.mb-7{margin-bottom:28px}.-mt-4{margin-top:-16px}.-mt-2{margin-top:-8px}.mb-5{margin-bottom:20px}.\!ml-2{margin-left:8px!important}.mr-2{margin-right:8px}.mt-\[6px\]{margin-top:6px}.box-border{box-sizing:border-box}.block{display:block}.inline-block{display:inline-block}.inline{display:inline}.\!flex{display:-webkit-box!important;display:-ms-flexbox!important;display:flex!important}.flex{display:-webkit-box;display:-ms-flexbox;display:flex}.inline-flex{display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex}.table{display:table}.hidden{display:none}.\!h-\[50px\]{height:50px!important}.h-8{height:32px}.h-20{height:80px}.h-\[220px\]{height:220px}.h-10{height:40px}.h-\[17px\]{height:17px}.h-\[13px\]{height:13px}.h-\[14px\]{height:14px}.h-3{height:12px}.h-5{height:20px}.h-full{height:100%}.h-\[400px\]{height:400px}.h-\[300px\]{height:300px}.h-2{height:8px}.max-h-52{max-height:208px}.min-h-\[44px\]{min-height:44px}.\!w-full{width:100%!important}.w-\[240px\]{width:240px}.w-72{width:288px}.w-full{width:100%}.w-20{width:80px}.w-\[186px\]{width:186px}.w-\[66px\]{width:66px}.w-\[64px\]{width:64px}.w-\[30px\]{width:30px}.w-\[2px\]{width:2px}.w-3{width:12px}.w-\[77px\]{width:77px}.w-\[90px\]{width:90px}.w-\[18px\]{width:18px}.w-\[fit-content\]{width:-webkit-fit-content;width:-moz-fit-content;width:fit-content}.w-\[55px\]{width:55px}.w-28{width:112px}.w-\[720px\]{width:720px}.w-0{width:0px}.w-\[500px\]{width:500px}.w-60{width:240px}.w-24{width:96px}.w-\[168px\]{width:168px}.w-\[14px\]{width:14px}.min-w-\[85px\]{min-width:85px}.min-w-\[200px\]{min-width:200px}.max-w-\[fit-content\]{max-width:-webkit-fit-content;max-width:-moz-fit-content;max-width:fit-content}.flex-1{flex:1 1 0%}.origin-left{transform-origin:left}.origin-bottom-right{transform-origin:bottom right}.transform{transform:var(--tw-transform)}.cursor-pointer{cursor:pointer}.resize{resize:both}.\!flex-row-reverse{flex-direction:row-reverse!important}.\!flex-col{flex-direction:column!important}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-start{align-items:flex-start}.\!items-end{align-items:flex-end!important}.items-end{align-items:flex-end}.\!items-center{align-items:center!important}.items-center{align-items:center}.items-stretch{align-items:stretch}.justify-start{justify-content:flex-start}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.\!justify-center{justify-content:center!important}.justify-between{justify-content:space-between}.\!gap-\[8px\]{gap:8px!important}.gap-2{gap:8px}.gap-1{gap:4px}.space-x-1>:not([hidden])~:not([hidden]){--tw-space-x-reverse: 0;margin-right:calc(4px * var(--tw-space-x-reverse));margin-left:calc(4px * calc(1 - var(--tw-space-x-reverse)))}.space-x-2>:not([hidden])~:not([hidden]){--tw-space-x-reverse: 0;margin-right:calc(8px * var(--tw-space-x-reverse));margin-left:calc(8px * calc(1 - var(--tw-space-x-reverse)))}.space-y-1>:not([hLine truncated
/*# sourceMappingURL=umi.css.map*/</style><div id="ebk-rp-root"><div class=""><div class="item___KIZD4"><div class="flexScroll___RB3lm"><div class="ant-pro-page-container" style="min-width: 824px;"><div class="ant-pro-page-container-warp"><div class="ant-page-header"><div class="ant-page-header-heading"><div class="ant-page-header-heading-left"><span class="ant-page-header-heading-title" title="房价房量管理">房价房量管理</span></div><span class="ant-page-header-heading-extra"><div class="ant-space ant-space-horizontal ant-space-align-center roomVsManage-tour-step1" style="gap: 8px;"><div class="ant-space-item" style=""><button type="button" class="ant-btn ant-btn-link ebkc-extraButton"><span>酒店属性及ID</span></button></div><span class="ant-space-item-split" style=""><div class="ant-divider ant-divider-vertical" role="separator"></div></span><div class="ant-space-item" style=""><button type="button" class="ant-btn ant-btn-link ebkc-extraButton"><span>新手教学</span></button></div><span class="ant-space-item-split" style=""><div class="ant-divider ant-divider-vertical" role="separator"></div></span><div class="ant-space-item" style=""><button type="button" class="ant-btn ant-btn-link ebkc-extraButton"><span>操作日志</span></button></div><span class="ant-space-item-split" style=""><div class="ant-divider ant-divider-vertical" role="separator"></div></span><div class="ant-space-item"><button type="button" class="ant-btn ant-btn-link ebkc-extraButton"><span>价库操作日志</span></button></div></div></span></div></div></div><div class="ant-pro-grid-content"><div class="ant-pro-grid-content-children"><div class="ant-pro-page-container-children-content"><div class="columnWrapper___c2wUd"><div class="ant-spin-nested-loading"><div class="ant-spin-container"><div class="ant-pro-card optionArea___oJUtg"><div class="ant-pro-card-body" style="padding: 10px 12px;"><div class="flex justify-between w-full items-start" data-spm-anchor-id="0.0.0.i4.43d517aeiMi1C7"><div class="flex items-center"><div class="ant-space ant-space-horizontal ant-space-align-center roomVsManage-tour-step2" style="gap: 8px;"><div class="ant-space-item" style=""><button type="button" class="ant-btn ant-btn-primary"><span>快速筛选</span></button></div><div class="ant-space-item" style=""><button type="button" class="ant-btn ant-btn-primary ant-btn-background-ghost"><span>批量改房量</span></button></div><div class="ant-space-item"><button type="button" class="ant-btn ant-btn-primary ant-btn-background-ghost"><span>批量改房价</span></button></div><div class="ant-space-item"><button type="button" class="ant-btn ant-btn-primary ant-btn-background-ghost"><div class="flex items-center gap-1">到手价查询<span class="newIcon">new</span></div></button></div></div><button type="button" class="ant-btn ant-dropdown-trigger roomVsManage-tour-step3"><span>更多</span><span role="img" aria-label="down" class="anticon anticon-down"><svg viewBox="64 64 896 896" focusable="false" class="" data-icon="down" width="1em" height="1em" fill="currentColor" aria-hidden="true"><path d="M884 256h-75c-5.1 0-9.9 2.5-12.9 6.6L512 654.2 227.9 262.6c-3-4.1-7.8-6.6-12.9-6.6h-75c-6.5 0-10.3 7.4-6.5 12.7l352.6 486.1c12.8 17.6 39 17.6 51.7 0l352.6-486.1c3.9-5.3.1-12.7-6.4-12.7z"></path></svg></span></button></div><div class="flex items-center flex-wrap justify-end gap-1" style="gap: 4px;"><div class="ant-checkbox-group checkboxGroup___BEi5L roomVsManage-tour-step4"><span style="color: rgb(92, 95, 102); margin-right: 8px;">显示</span><label class="ant-checkbox-wrapper ant-checkbox-wrapper-checked"><span class="ant-checkbox ant-checkbox-checked"><input type="checkbox" class="ant-checkbox-input" value="2" checked=""><span class="ant-checkbox-inner"></span></span><span>房价</span></label><label class="ant-checkbox-wrapper ant-checkbox-wrapper-checked"><span class="ant-checkbox ant-checkbox-checked"><input type="checkbox" class="ant-checkbox-input" value="1" checked=""><span class="ant-checkbox-inner"></span></span><span>房态</span></label><label class="ant-checkbox-wrapper ant-checkbox-wrapper-checked"><span class="ant-checkbox ant-checkbox-checked"><input type="checkbox" class="ant-checkbox-input" value="4" checked=""><span class="ant-checkbox-inner"></span></span><span>直连产品</span></label></div><div><span class="ant-tooltip-disabled-compatible-wrapper" style="display: inline-block; cursor: not-allowed;"><button disabled="" type="button" class="ant-btn" style="box-shadow: none; border-right: none; pointer-events: none;"><span role="img" aria-label="left" class="anticon anticon-left"><svg viewBox="64 64 896 896" focusable="false" class="" data-icon="left" width="1em" height="1em" fill="currentColor" aria-hidden="true"><path d="M724 218.3V141c0-6.7-7.7-10.4-12.9-6.3L260.3 486.8a31.86 31.86 0 000 50.3l450.8 352.1c5.3 4.1 12.9.4 12.9-6.3v-77.3c0-4.9-2.3-9.6-6.1-12.6l-360-281 360-281.1c3.8-3 6.1-7.7 6.1-12.6z"></path></svg></span></Line truncated
```
+1 -1
View File
@@ -1,3 +1,3 @@
```html
<div class="product-container" data-v-2311e292=""><div class="sub-header" data-v-4ad0f163="" data-v-2311e292=""><div class="product-notice-alert" use-custom-pr-freshs="" data-v-bcd285a2="" data-v-4ad0f163=""><div class="lion-push-notice" eventtype="" extrastyle="" usecustomrsfresh="true" usecustomprfresh="false" data-v-1d6a6128="" data-v-bcd285a2="" style="display: none;"><!----></div><div class="supply-notice" eventtype="" extrastyle="" usecustomrsfresh="true" usecustomprfresh="false" data-v-5445a116="" data-v-bcd285a2="" style="display: none;"><!----></div><div class="lz-announcement lz-announcement-warning lz-announcement-closable hotspot-calendar-notice" source="roomstatus" data-v-1d80a1e6="" data-v-bcd285a2=""><i class="lz-announcement-icon lzicon lzicon-warning-circle"></i><div class="lz-announcement-content"><div class="lz-announcement-title"><span class="notice-desc high-light" data-v-1d80a1e6="">第29届安团家博会-贵阳展将至</span><span class="notice-desc" data-v-1d80a1e6="">,请提前调整好</span><span class="notice-desc high-light" data-v-1d80a1e6="">03月20日至03月22日</span><span class="notice-desc" data-v-1d80a1e6="">房价,确保房量和房态准确,</span><span class="notice-desc" data-v-1d80a1e6="">并及时安排好接单人员</span><span data-v-1d80a1e6=""><a href="javascript:;" class="right-link" data-v-1d80a1e6="">查看其他热点</a></span><div class="notice-actions" data-v-1d80a1e6=""><a href="javascript:;" data-v-1d80a1e6="">立即维护</a></div></div><!----><div class="lz-announcement-close"><i class="lzicon lzicon-close-thick"></i></div></div></div></div><!----><!----><!----><!----><div class="eb-header-container" data-v-4ad0f163="" style="margin-bottom: 12px;"><form class="lz-form lz-form-right lz-form-inline" autocomplete="off" style="display: flex; flex-wrap: wrap;"><div class="lz-form-item lz-form-item-right"><!----><div class="lz-form-item-content"><button class="lz-btn lz-btn-primary" type="button"><!---->新增产品(新)</button><!----><!----></div></div><div class="lz-form-item lz-form-item-right"><!----><div class="lz-form-item-content"><button class="lz-btn lz-btn-primary lz-btn-ghost" type="button"><!---->批量修改销售规则</button><!----><!----></div></div><div class="lz-form-item lz-form-item-right"><!----><div class="lz-form-item-content"><button class="lz-btn lz-btn-primary lz-btn-ghost" type="button"><!----><span style="margin-right: 4px;">查看协议与记录</span><i class="lzicon lzicon-down"></i></button><!----><!----></div></div><!----><!----><!----><div class="lz-form-item lz-form-item-right"><!----><div class="lz-form-item-content"><button class="lz-btn lz-btn-primary lz-btn-ghost" type="button"><!---->热点日历</button><!----><!----></div></div></form></div><!----><!----><div class="common-search-area" data-v-4ad0f163=""><div class="merchant-search-area" data-v-df6b8148="" data-v-4ad0f163=""><form class="lz-form lz-form-right lz-form-inline header-form" autocomplete="off" data-v-df6b8148="" style="display: flex; flex-wrap: wrap;"><div class="lz-form-item lz-form-item-right" data-v-df6b8148=""><!----><div class="lz-form-item-content"><button class="lz-btn lz-btn-primary" type="button" data-v-df6b8148=""><!---->快速筛选房型</button><!----><!----></div></div><div class="lz-form-item lz-form-item-right" data-v-df6b8148=""><!----><div class="lz-form-item-content"><div class="calendar-date-picker" data-v-df6b8148=""><div class="lz-date-picker lz-date-picker-selected date-picker popper-identification-symbol" style="width: 175px;"><div class="date-picker lz-date-picker-editor lz-input-wrapper lz-range-picker"><!----><!----><input class="date-picker lz-date-picker-editor lz-input" closable="true" loading="false" show-tag="true" placeholder="" type="text"><span class="range-separator"> ~ </span><input class="date-picker lz-date-picker-editor lz-input" closable="true" loading="false" show-tag="true" placeholder="" type="text"><span class="lz-input-suffix-inner"><i class="lzicon lzicon-calendar-o"></i></span><!----></div></div></div><!----><!----></div></div><div class="lz-form-item lz-form-item-right quick-select-box" data-v-df6b8148=""><!----><div class="lz-form-item-content"><div class="main-container" data-v-20a91b9c="" data-v-df6b8148=""><div class="quick-select-container" data-v-20a91b9c=""><div class="lz-checkbox-group checkbox-wrapper" data-v-20a91b9c=""><label class="lz-checkbox lz-checkbox-checked" data-v-20a91b9c="" style="margin-left: 8px;"><input class="lz-checkbox-input" type="checkbox" style="display: none;"><span class="lz-checkbox-inner"><i class="lzicon lzicon-check-thick"></i></span><span class="lz-checkbox-text">全日房</span></label><label class="lz-checkbox" data-v-20a91b9c="" style="margin-left: 8px;"><input class="lz-checkbox-input" type="checkbox" style="display: none;"><span class="lz-checkbox-inner"><i class="lzicon lzicon-check-thick" style="display: none;"></i></span><span class="lz-checkboLine truncated
<div class="product-container" data-v-2311e292=""><div class="sub-header" data-v-4ad0f163="" data-v-2311e292=""><div class="product-notice-alert" use-custom-pr-freshs="" data-v-bcd285a2="" data-v-4ad0f163=""><div class="lion-push-notice" eventtype="" extrastyle="" usecustomrsfresh="true" usecustomprfresh="false" data-v-1d6a6128="" data-v-bcd285a2="" style="display: none;"><!----></div><div class="supply-notice" eventtype="" extrastyle="" usecustomrsfresh="true" usecustomprfresh="false" data-v-5445a116="" data-v-bcd285a2="" style="display: none;"><!----></div><!----></div><!----><!----><!----><!----><div class="eb-header-container" data-v-4ad0f163="" style="margin-bottom: 12px;"><form class="lz-form lz-form-right lz-form-inline" autocomplete="off" style="display: flex; flex-wrap: wrap;"><div class="lz-form-item lz-form-item-right"><!----><div class="lz-form-item-content"><button class="lz-btn lz-btn-primary" type="button"><!---->新增产品(新)</button><!----><!----></div></div><div class="lz-form-item lz-form-item-right"><!----><div class="lz-form-item-content"><button class="lz-btn lz-btn-primary lz-btn-ghost" type="button"><!---->批量修改销售规则</button><!----><!----></div></div><div class="lz-form-item lz-form-item-right"><!----><div class="lz-form-item-content"><button class="lz-btn lz-btn-primary lz-btn-ghost" type="button"><!----><span style="margin-right: 4px;">查看协议与记录</span><i class="lzicon lzicon-down"></i></button><!----><!----></div></div><!----><div class="lz-form-item lz-form-item-right"><!----><div class="lz-form-item-content"><button class="lz-btn lz-btn-primary lz-btn-ghost" type="button"><!---->闲置房</button><!----><!----></div></div><!----><div class="lz-form-item lz-form-item-right"><!----><div class="lz-form-item-content"><button class="lz-btn lz-btn-primary lz-btn-ghost" type="button"><!---->热点日历</button><!----><!----></div></div></form></div><!----><!----><div class="common-search-area" data-v-4ad0f163=""><div class="merchant-search-area" data-v-df6b8148="" data-v-4ad0f163=""><form class="lz-form lz-form-right lz-form-inline header-form" autocomplete="off" data-v-df6b8148="" style="display: flex; flex-wrap: wrap;"><div class="lz-form-item lz-form-item-right" data-v-df6b8148=""><!----><div class="lz-form-item-content"><button class="lz-btn lz-btn-primary" type="button" data-v-df6b8148=""><!---->快速筛选房型</button><!----><!----></div></div><div class="lz-form-item lz-form-item-right" data-v-df6b8148=""><!----><div class="lz-form-item-content"><div class="calendar-date-picker" data-v-df6b8148=""><div class="lz-date-picker lz-date-picker-selected date-picker popper-identification-symbol" style="width: 175px;"><div class="date-picker lz-date-picker-editor lz-input-wrapper lz-range-picker"><!----><!----><input class="date-picker lz-date-picker-editor lz-input" closable="true" loading="false" show-tag="true" placeholder="" type="text"><span class="range-separator"> ~ </span><input class="date-picker lz-date-picker-editor lz-input" closable="true" loading="false" show-tag="true" placeholder="" type="text"><span class="lz-input-suffix-inner"><i class="lzicon lzicon-calendar-o"></i></span><!----></div></div></div><!----><!----></div></div><div class="lz-form-item lz-form-item-right quick-select-box" data-v-df6b8148=""><!----><div class="lz-form-item-content"><div class="main-container" data-v-20a91b9c="" data-v-df6b8148=""><div class="quick-select-container" data-v-20a91b9c=""><div class="lz-checkbox-group checkbox-wrapper" data-v-20a91b9c=""><label class="lz-checkbox lz-checkbox-checked" data-v-20a91b9c="" style="margin-left: 8px;"><input class="lz-checkbox-input" type="checkbox" style="display: none;"><span class="lz-checkbox-inner"><i class="lzicon lzicon-check-thick"></i></span><span class="lz-checkbox-text">全日房</span></label><label class="lz-checkbox" data-v-20a91b9c="" style="margin-left: 8px;"><input class="lz-checkbox-input" type="checkbox" style="display: none;"><span class="lz-checkbox-inner"><i class="lzicon lzicon-check-thick" style="display: none;"></i></span><span class="lz-checkbox-text">钟点房</span></label></div></div></div><!----><!----><!----></div></div><div class="lz-form-item lz-form-item-right display-box" data-v-df6b8148=""><!----><div class="lz-form-item-content"><div class="display-select" data-v-7a3dc564="" data-v-df6b8148=""><div class="lz-checkbox-group checkbox-wrapper" data-v-7a3dc564=""><label class="lz-checkbox lz-checkbox-checked" data-v-7a3dc564="" style="margin-left: 8px;"><input class="lz-checkbox-input" type="checkbox" style="display: none;"><span class="lz-checkbox-inner"><i class="lzicon lzicon-check-thick"></i></span><span class="lz-checkbox-text">显示房态</span></label><label class="lz-checkbox lz-checkbox-checked" data-v-7a3dc564="" style="margin-left: 8px;"><input class="lz-checkbox-input" type="checkbox" style="display: none;"><span class="lz-checkbox-inner"><i class="lzicon lzicon-check-tLine truncated
```
Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

After

Width:  |  Height:  |  Size: 74 KiB

+216
View File
@@ -1,8 +1,174 @@
import { chromium } from 'playwright';
import log from 'electron-log';
import tabsPkg from './common/tabs.js';
const { preparePage, safeDisconnectBrowser } = tabsPkg;
const parseDateInput = (dateStr) => {
if (!dateStr) return null;
if (typeof dateStr === 'number' && Number.isFinite(dateStr)) return new Date(dateStr);
if (dateStr instanceof Date && !Number.isNaN(dateStr.getTime())) return dateStr;
const raw = String(dateStr).trim();
const ymdMatch = raw.match(/(\d{4}-\d{2}-\d{2})/);
if (ymdMatch) return new Date(`${ymdMatch[1]}T00:00:00`);
const mmddMatch = raw.match(/(\d{2}-\d{2})/);
if (mmddMatch) {
const year = new Date().getFullYear();
return new Date(`${year}-${mmddMatch[1]}T00:00:00`);
}
return null;
};
const formatMMDD = (d) => {
const mm = String(d.getMonth() + 1).padStart(2, '0');
const dd = String(d.getDate()).padStart(2, '0');
return `${mm}.${dd}`; // Meituan uses MM.DD format
};
const buildDateList = (startDate, endDate) => {
const start = parseDateInput(startDate);
const end = parseDateInput(endDate || startDate);
if (!start || !end) return [];
const from = start <= end ? start : end;
const to = start <= end ? end : start;
const dates = [];
const cur = new Date(from.getTime());
while (cur <= to) {
dates.push(formatMMDD(cur));
cur.setDate(cur.getDate() + 1);
}
return dates;
};
// Find the column class (e.g. "col_55") for a specific date
const findDateColumnClass = async (page, mmdd) => {
// Try visible headers first
const headerCells = page.locator('.vxe-header--column');
const checkHeaders = async () => {
const c = await headerCells.count();
for (let i = 0; i < c; i++) {
const cell = headerCells.nth(i);
const text = (await cell.innerText()).replace(/\s+/g, ' ').trim();
if (text.includes(mmdd)) {
// Extract the col_X class
const classList = await cell.getAttribute('class');
const match = classList.match(/col_\d+/);
if (match) return match[0];
}
}
return null;
};
let colClass = await checkHeaders();
if (colClass) return colClass;
// Try scrolling if not found
const scrollContainer = page.locator('.vxe-table--body-wrapper').first();
if (await scrollContainer.isVisible()) {
for (let attempt = 0; attempt < 10; attempt++) {
await scrollContainer.evaluate((el) => {
el.scrollLeft += 600;
});
await page.waitForTimeout(300);
colClass = await checkHeaders();
if (colClass) return colClass;
}
}
return null;
};
// Find the row index for a specific room type
const findRoomRowIndex = async (page, roomType) => {
// We look for the row in ANY part of the table (fixed left or main)
// and assume row indexes align across split tables.
const rows = page.locator('.vxe-body--row');
const count = await rows.count();
for (let i = 0; i < count; i++) {
const text = await rows.nth(i).innerText();
if (text.includes(roomType)) {
// Found the row. Now we need to determine its "logical" index.
// If the table is split into left/main, 'rows' contains ALL rows from both tables mixed or sequential.
// A safer way is to find the specific TR and get its rowIndex or check its parent.
// Optimization: Usually room names are in the fixed-left wrapper.
// Let's try to find the row specifically in the left wrapper first if it exists.
const leftRows = page.locator('.vxe-table--fixed-left-wrapper .vxe-body--row');
if (await leftRows.count() > 0) {
const leftCount = await leftRows.count();
for(let j=0; j<leftCount; j++) {
if ((await leftRows.nth(j).innerText()).includes(roomType)) {
return j;
}
}
}
// Fallback: Just return this index if it's a simple table
return i;
}
}
return -1;
};
const toggleRoom = async (page, { roomType, mmdd, operation }) => {
// 1. Find the column class for the date (e.g. "col_6")
const colClass = await findDateColumnClass(page, mmdd);
if (!colClass) {
throw new Error(`Date column not found for ${mmdd}`);
}
// log.info(`Date ${mmdd} maps to column class: ${colClass}`);
// 2. Find the row index for the room
const rowIndex = await findRoomRowIndex(page, roomType);
if (rowIndex === -1) {
throw new Error(`Room row not found for ${roomType}`);
}
// log.info(`Room ${roomType} maps to row index: ${rowIndex}`);
// 3. Locate the target cell in the MAIN body wrapper (where switches usually are)
// using the row index and column class.
const mainBody = page.locator('.vxe-table--body-wrapper');
const targetRow = mainBody.locator('.vxe-body--row').nth(rowIndex);
const targetCell = targetRow.locator(`.${colClass}`);
// 4. Find and toggle switch
const switchEl = targetCell.locator('.lz-switch').first();
if (await switchEl.count() === 0) {
log.warn(`No switch found at ${roomType} / ${mmdd} (Row ${rowIndex}, Col ${colClass})`);
return;
}
await switchEl.scrollIntoViewIfNeeded();
// Check state
// Inspecting common switch classes: 'lz-switch-active' (Meituan), 'is-checked', etc.
const classList = await switchEl.getAttribute('class');
const isChecked = classList && (
classList.includes('is-checked') ||
classList.includes('checked') ||
classList.includes('lz-switch-checked') ||
classList.includes('lz-switch-active')
);
const isOpen = isChecked;
if (operation === 'open' && !isOpen) {
await switchEl.click();
log.info(`Opened room ${roomType} on ${mmdd}`);
} else if (operation === 'close' && isOpen) {
await switchEl.click();
log.info(`Closed room ${roomType} on ${mmdd}`);
} else {
log.info(`Room ${roomType} already ${operation === 'open' ? 'open' : 'closed'} on ${mmdd}`);
}
};
(async () => {
let browser;
@@ -11,6 +177,56 @@ const { preparePage, safeDisconnectBrowser } = tabsPkg;
const prepared = await preparePage(chromium, { targetUrl });
browser = prepared.browser;
const page = prepared.page;
await page.waitForTimeout(4000 + Math.random() * 300);
// Navigation logic from user snippet
try {
const menu = page.locator('#vina-menu-item-100114001 > .lz-submenu-title');
if (await menu.isVisible()) {
await menu.click();
await page.waitForTimeout(1000);
} else {
// Fallback if the submenu title isn't there
const menuFallback = page.locator('#vina-menu-item-100114001');
if (await menuFallback.isVisible()) {
await menuFallback.click();
await page.waitForTimeout(1000);
}
}
const calendarLink = page.locator('a').filter({ hasText: '房价房量日历' });
if (await calendarLink.isVisible()) {
await calendarLink.click();
await page.waitForTimeout(3000);
}
} catch (e) {
log.warn('Navigation attempt failed, assuming already on page:', e.message);
}
const roomType = process.env.ROOM_TYPE;
const startDate = process.env.START_DATE;
const endDate = process.env.END_DATE;
const operation = process.env.OPERATION === 'close' ? 'close' : 'open';
if (!roomType || !startDate) {
log.info('ROOM_TYPE/START_DATE not provided, skip room toggle.');
return;
}
// Wait for table
await page.locator('.vxe-table--body-wrapper').waitFor({ state: 'visible', timeout: 15000 });
const dateList = buildDateList(startDate, endDate);
for (const mmdd of dateList) {
try {
await toggleRoom(page, { roomType, mmdd, operation });
} catch (e) {
log.error(`Failed to toggle ${roomType} on ${mmdd}: ${e.message}`);
}
await page.waitForTimeout(600 + Math.random() * 600);
}
} catch (error) {
log.error(error);
process.exitCode = 1;