309 lines
9.7 KiB
JavaScript
309 lines
9.7 KiB
JavaScript
import fs from "node:fs/promises";
|
|
import path from "node:path";
|
|
import { chromium, devices } from "playwright";
|
|
|
|
const targetUrl =
|
|
"https://m.ctrip.com/tangram/OTQ1Mg==?ctm_ref=vactang_page_9452&isHideNavBar=YES&ctm_ref=vactang_page_159930";
|
|
|
|
const root = process.cwd();
|
|
const researchDir = path.join(root, "docs", "research");
|
|
const referenceDir = path.join(root, "docs", "design-references");
|
|
|
|
await fs.mkdir(researchDir, { recursive: true });
|
|
await fs.mkdir(referenceDir, { recursive: true });
|
|
|
|
const browser = await chromium.launch({ headless: true });
|
|
|
|
async function captureViewport(name, contextOptions) {
|
|
const context = await browser.newContext({
|
|
...contextOptions,
|
|
locale: "zh-CN",
|
|
timezoneId: "Asia/Shanghai",
|
|
extraHTTPHeaders: {
|
|
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
|
|
},
|
|
});
|
|
const page = await context.newPage();
|
|
const consoleMessages = [];
|
|
const failedRequests = [];
|
|
|
|
page.on("console", (msg) => {
|
|
const text = msg.text();
|
|
if (consoleMessages.length < 50) consoleMessages.push({ type: msg.type(), text });
|
|
});
|
|
page.on("requestfailed", (request) => {
|
|
if (failedRequests.length < 50) {
|
|
failedRequests.push({
|
|
url: request.url(),
|
|
failure: request.failure()?.errorText,
|
|
});
|
|
}
|
|
});
|
|
|
|
await page.goto(targetUrl, { waitUntil: "domcontentloaded", timeout: 60000 });
|
|
await page.waitForTimeout(8000);
|
|
|
|
const pageInfoBeforeScroll = await page.evaluate(() => ({
|
|
title: document.title,
|
|
url: location.href,
|
|
readyState: document.readyState,
|
|
scrollHeight: document.documentElement.scrollHeight,
|
|
viewportHeight: innerHeight,
|
|
bodyTextLength: document.body?.innerText?.length ?? 0,
|
|
bodyTextStart: document.body?.innerText?.slice(0, 1000) ?? "",
|
|
}));
|
|
|
|
await page.screenshot({
|
|
path: path.join(referenceDir, `source-${name}-top.png`),
|
|
fullPage: false,
|
|
});
|
|
|
|
await page.screenshot({
|
|
path: path.join(referenceDir, `source-${name}-full.png`),
|
|
fullPage: true,
|
|
});
|
|
|
|
const scrollSamples = [];
|
|
let i = 0;
|
|
let y = 0;
|
|
let previousHeight = 0;
|
|
while (i < 24) {
|
|
await page.evaluate((scrollY) => window.scrollTo(0, scrollY), y);
|
|
await page.waitForTimeout(1200);
|
|
await page.screenshot({
|
|
path: path.join(referenceDir, `source-${name}-segment-${String(i).padStart(2, "0")}.png`),
|
|
fullPage: false,
|
|
});
|
|
scrollSamples.push(
|
|
await page.evaluate(() => ({
|
|
scrollY,
|
|
visibleText: document.body.innerText.slice(0, 1600),
|
|
activeElements: [...document.querySelectorAll("button, a, [role='button'], [class*='active'], [class*='selected']")]
|
|
.slice(0, 80)
|
|
.map((el) => ({
|
|
tag: el.tagName.toLowerCase(),
|
|
text: el.textContent?.trim().slice(0, 120) ?? "",
|
|
className: String(el.className ?? "").slice(0, 200),
|
|
href: el instanceof HTMLAnchorElement ? el.href : "",
|
|
})),
|
|
})),
|
|
);
|
|
const metrics = await page.evaluate(() => ({
|
|
scrollHeight: document.documentElement.scrollHeight,
|
|
viewportHeight: innerHeight,
|
|
}));
|
|
const maxScroll = Math.max(0, metrics.scrollHeight - metrics.viewportHeight);
|
|
if (y >= maxScroll - 4 && metrics.scrollHeight === previousHeight) break;
|
|
previousHeight = metrics.scrollHeight;
|
|
y = Math.min(maxScroll, y + 680);
|
|
i += 1;
|
|
}
|
|
|
|
await page.evaluate(() => window.scrollTo(0, 0));
|
|
await page.waitForTimeout(500);
|
|
|
|
await page.screenshot({
|
|
path: path.join(referenceDir, `source-${name}-full-after-scroll.png`),
|
|
fullPage: true,
|
|
});
|
|
|
|
const extract = await page.evaluate(() => {
|
|
const styleProps = [
|
|
"fontSize",
|
|
"fontWeight",
|
|
"fontFamily",
|
|
"lineHeight",
|
|
"letterSpacing",
|
|
"color",
|
|
"backgroundColor",
|
|
"backgroundImage",
|
|
"padding",
|
|
"margin",
|
|
"display",
|
|
"position",
|
|
"top",
|
|
"left",
|
|
"right",
|
|
"bottom",
|
|
"width",
|
|
"height",
|
|
"borderRadius",
|
|
"boxShadow",
|
|
"transform",
|
|
"opacity",
|
|
"zIndex",
|
|
];
|
|
|
|
function compactStyles(el) {
|
|
const cs = getComputedStyle(el);
|
|
const out = {};
|
|
for (const prop of styleProps) {
|
|
const value = cs[prop];
|
|
if (value && value !== "none" && value !== "normal" && value !== "auto" && value !== "0px" && value !== "rgba(0, 0, 0, 0)") {
|
|
out[prop] = value;
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
const visibleElements = [...document.querySelectorAll("body *")]
|
|
.map((el) => {
|
|
const rect = el.getBoundingClientRect();
|
|
const area = Math.round(rect.width * rect.height);
|
|
return { el, rect, area };
|
|
})
|
|
.filter(({ rect, area }) => area > 5000 && rect.width > 40 && rect.height > 20)
|
|
.slice(0, 220)
|
|
.map(({ el, rect, area }) => ({
|
|
tag: el.tagName.toLowerCase(),
|
|
id: el.id,
|
|
className: String(el.className ?? "").slice(0, 240),
|
|
text: el.textContent?.trim().replace(/\s+/g, " ").slice(0, 260) ?? "",
|
|
rect: {
|
|
x: Math.round(rect.x),
|
|
y: Math.round(rect.y + scrollY),
|
|
width: Math.round(rect.width),
|
|
height: Math.round(rect.height),
|
|
},
|
|
area,
|
|
styles: compactStyles(el),
|
|
}));
|
|
|
|
const images = [...document.images].map((img) => ({
|
|
src: img.currentSrc || img.src,
|
|
alt: img.alt,
|
|
naturalWidth: img.naturalWidth,
|
|
naturalHeight: img.naturalHeight,
|
|
className: String(img.className ?? "").slice(0, 160),
|
|
parentClassName: String(img.parentElement?.className ?? "").slice(0, 160),
|
|
rect: (() => {
|
|
const rect = img.getBoundingClientRect();
|
|
return {
|
|
x: Math.round(rect.x),
|
|
y: Math.round(rect.y + scrollY),
|
|
width: Math.round(rect.width),
|
|
height: Math.round(rect.height),
|
|
};
|
|
})(),
|
|
}));
|
|
|
|
const backgroundImages = [...document.querySelectorAll("*")]
|
|
.map((el) => ({
|
|
tag: el.tagName.toLowerCase(),
|
|
className: String(el.className ?? "").slice(0, 180),
|
|
backgroundImage: getComputedStyle(el).backgroundImage,
|
|
}))
|
|
.filter((item) => item.backgroundImage && item.backgroundImage !== "none")
|
|
.slice(0, 260);
|
|
|
|
const videos = [...document.querySelectorAll("video")].map((video) => ({
|
|
src: video.currentSrc || video.src,
|
|
poster: video.poster,
|
|
autoplay: video.autoplay,
|
|
loop: video.loop,
|
|
muted: video.muted,
|
|
}));
|
|
|
|
const links = [...document.querySelectorAll("a")].map((a) => ({
|
|
text: a.textContent?.trim().replace(/\s+/g, " ").slice(0, 160) ?? "",
|
|
href: a.href,
|
|
className: String(a.className ?? "").slice(0, 120),
|
|
}));
|
|
|
|
const buttons = [...document.querySelectorAll("button, [role='button']")].map((button) => ({
|
|
text: button.textContent?.trim().replace(/\s+/g, " ").slice(0, 160) ?? "",
|
|
className: String(button.className ?? "").slice(0, 160),
|
|
styles: compactStyles(button),
|
|
}));
|
|
|
|
const fontFamilies = [...new Set([...document.querySelectorAll("body *")].slice(0, 500).map((el) => getComputedStyle(el).fontFamily))];
|
|
const colors = [...new Set([...document.querySelectorAll("body *")].slice(0, 500).flatMap((el) => {
|
|
const cs = getComputedStyle(el);
|
|
return [cs.color, cs.backgroundColor].filter(Boolean);
|
|
}))].filter((color) => color !== "rgba(0, 0, 0, 0)").slice(0, 100);
|
|
|
|
return {
|
|
title: document.title,
|
|
location: location.href,
|
|
htmlLang: document.documentElement.lang,
|
|
bodyText: document.body.innerText,
|
|
meta: [...document.querySelectorAll("meta")].map((meta) => ({
|
|
name: meta.getAttribute("name"),
|
|
property: meta.getAttribute("property"),
|
|
content: meta.getAttribute("content"),
|
|
})),
|
|
styleSheets: [...document.styleSheets].map((sheet) => sheet.href).filter(Boolean),
|
|
scripts: [...document.scripts].map((script) => script.src).filter(Boolean),
|
|
links: [...document.querySelectorAll("link")].map((link) => ({
|
|
rel: link.rel,
|
|
href: link.href,
|
|
as: link.as,
|
|
})),
|
|
visibleElements,
|
|
images,
|
|
backgroundImages,
|
|
videos,
|
|
anchors: links,
|
|
buttons,
|
|
fontFamilies,
|
|
colors,
|
|
viewport: {
|
|
width: innerWidth,
|
|
height: innerHeight,
|
|
scrollHeight: document.documentElement.scrollHeight,
|
|
},
|
|
};
|
|
});
|
|
|
|
await fs.writeFile(path.join(researchDir, `source-${name}.html`), await page.content());
|
|
await fs.writeFile(
|
|
path.join(researchDir, `source-${name}-extract.json`),
|
|
JSON.stringify({ pageInfoBeforeScroll, extract, scrollSamples, consoleMessages, failedRequests }, null, 2),
|
|
);
|
|
|
|
await context.close();
|
|
return { pageInfoBeforeScroll, extract, scrollSamples, consoleMessages, failedRequests };
|
|
}
|
|
|
|
const iphone = devices["iPhone 13 Pro"];
|
|
const mobile = await captureViewport("mobile", {
|
|
...iphone,
|
|
viewport: { width: 390, height: 844 },
|
|
});
|
|
|
|
const desktop = await captureViewport("desktop", {
|
|
viewport: { width: 1440, height: 1200 },
|
|
userAgent:
|
|
"Mozilla/5.0 (Macintosh; Intel Mac OS X 14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36",
|
|
deviceScaleFactor: 1,
|
|
isMobile: false,
|
|
hasTouch: false,
|
|
});
|
|
|
|
await browser.close();
|
|
|
|
await fs.writeFile(
|
|
path.join(researchDir, "source-summary.json"),
|
|
JSON.stringify(
|
|
{
|
|
targetUrl,
|
|
mobile: mobile.pageInfoBeforeScroll,
|
|
desktop: desktop.pageInfoBeforeScroll,
|
|
mobileAssetCounts: {
|
|
images: mobile.extract.images.length,
|
|
backgrounds: mobile.extract.backgroundImages.length,
|
|
videos: mobile.extract.videos.length,
|
|
},
|
|
desktopAssetCounts: {
|
|
images: desktop.extract.images.length,
|
|
backgrounds: desktop.extract.backgroundImages.length,
|
|
videos: desktop.extract.videos.length,
|
|
},
|
|
},
|
|
null,
|
|
2,
|
|
),
|
|
);
|
|
|
|
console.log("Source capture complete.");
|