#!/usr/bin/env node /** * 租赁合同表单页 + 工作台 · 5 主题 × 浅/暗 截图 * * Usage: * node scripts/shot-form-workbench-themes.mjs * node scripts/shot-form-workbench-themes.mjs --base=http://127.0.0.1:51720 */ import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { createRequire } from 'node:module'; import { THEME_SHOTS, buildSkinCss } from './lease-contract-theme-skins.mjs'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const root = path.resolve(__dirname, '..'); const outRoot = path.join(root, 'src/resources/design-system/lease-contract-theme-shots'); const PAGES = [ { key: 'form', title: '租赁合同 · 新增表单', path: '/prototypes/lease-contract-management/', ready: '.vm-page.lc-page', shot: '.vm-page.lc-create-page, .vm-page.lc-page', async prepare(page) { // 已在表单页则跳过 const onCreate = await page.$('.lc-create-page'); if (onCreate) return; // 点工具栏「新增」 const clicked = await page.evaluate(() => { const buttons = Array.from(document.querySelectorAll('button, a, .vm-btn')); const btn = buttons.find((el) => (el.textContent || '').trim() === '新增'); if (btn) { btn.click(); return true; } return false; }); if (!clicked) throw new Error('未找到「新增」按钮'); await page.waitForSelector('.lc-create-page', { timeout: 30000 }); await new Promise((r) => setTimeout(r, 500)); }, }, { key: 'workbench', title: '工作台', path: '/prototypes/oneos-web-workbench-new/', ready: '.vm-page.wb-page', shot: '.vm-page.wb-page', /** 比稿截图不需要版本更新弹层遮挡首屏 */ async beforeGoto(page) { await page.evaluateOnNewDocument(() => { const KEY = 'oneos-wb-release-seen-v1'; const VERSION = '1.1.5'; const operators = [ '王冕', '周凯', '王磊', '刘洋', '黄倩', '孙敏', '陈静', '赵律师', '张明', ]; const map = {}; try { const raw = window.localStorage.getItem(KEY); if (raw) Object.assign(map, JSON.parse(raw) || {}); } catch { /* ignore */ } for (const name of operators) map[name] = VERSION; window.localStorage.setItem(KEY, JSON.stringify(map)); }); }, async prepare(page) { // 若仍弹出(缓存/旧会话),点「知道了」或强制隐藏 await page.evaluate(async () => { const confirm = document.querySelector('.wb-release-confirm'); if (confirm instanceof HTMLElement) confirm.click(); await new Promise((r) => setTimeout(r, 200)); let style = document.getElementById('ds-theme-shot-hide-release'); if (!style) { style = document.createElement('style'); style.id = 'ds-theme-shot-hide-release'; document.head.appendChild(style); } style.textContent = ` [data-annotation-id="wb-release-notes"], .wb-overlay--release, .wb-modal--release { display: none !important; visibility: hidden !important; pointer-events: none !important; } `; }); await new Promise((r) => setTimeout(r, 150)); }, }, ]; function parseArgs(argv) { const args = { base: 'http://127.0.0.1:51720' }; for (const a of argv) { if (a.startsWith('--base=')) args.base = a.slice('--base='.length); } return args; } async function loadPuppeteer() { const require = createRequire(import.meta.url); try { return require('puppeteer'); } catch { return require('puppeteer-core'); } } async function applySkin(page, theme, mode) { const css = buildSkinCss(theme, mode); await page.evaluate((cssText, modeName) => { document.documentElement.dataset.dsMode = modeName; let el = document.getElementById('ds-theme-shot-skin'); if (!el) { el = document.createElement('style'); el.id = 'ds-theme-shot-skin'; document.head.appendChild(el); } el.textContent = cssText; }, css, mode); await new Promise((r) => setTimeout(r, 250)); } async function hideAnnotation(page) { await page.evaluate(() => { let el = document.getElementById('ds-theme-shot-hide-ann'); if (!el) { el = document.createElement('style'); el.id = 'ds-theme-shot-hide-ann'; document.head.appendChild(el); } el.textContent = ` .axhub-annotation-host, [data-axhub-annotation], [class*="Annotation"], [class*="annotation-toolbar"] { display: none !important; visibility: hidden !important; } `; }); } async function main() { const args = parseArgs(process.argv.slice(2)); const base = args.base.replace(/\/$/, ''); const puppeteer = await loadPuppeteer(); const browser = await puppeteer.launch({ headless: true, executablePath: process.env.PUPPETEER_EXECUTABLE_PATH || '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', defaultViewport: { width: 1440, height: 1100, deviceScaleFactor: 2 }, args: ['--no-sandbox', '--disable-setuid-sandbox'], }); const allResults = []; for (const pageCfg of PAGES) { const outDir = path.join(outRoot, pageCfg.key); fs.mkdirSync(outDir, { recursive: true }); const page = await browser.newPage(); page.setDefaultTimeout(60000); const url = `${base}${pageCfg.path}`; console.log(`\n== ${pageCfg.title} ==\n打开 ${url}`); if (typeof pageCfg.beforeGoto === 'function') { await pageCfg.beforeGoto(page); } await page.goto(url, { waitUntil: 'networkidle2' }); await page.waitForSelector(pageCfg.ready, { timeout: 60000 }); await pageCfg.prepare(page); await hideAnnotation(page); for (const theme of THEME_SHOTS) { for (const mode of ['light', 'dark']) { await applySkin(page, theme, mode); const file = `${theme.id}-${mode}.png`; const outPath = path.join(outDir, file); const handle = await page.$(pageCfg.shot); if (handle) { await handle.screenshot({ path: outPath, type: 'png' }); } else { await page.screenshot({ path: outPath, type: 'png' }); } console.log('wrote', pageCfg.key + '/' + file); allResults.push({ page: pageCfg.key, pageTitle: pageCfg.title, file: `${pageCfg.key}/${file}`, theme: theme.name, mode, category: theme.category, }); } } await page.close(); const readme = `# ${pageCfg.title} · 主题比稿截图 生成时间:${new Date().toISOString().slice(0, 10)} | 文件 | 主题 | 模式 | |------|------|------| ${allResults .filter((r) => r.page === pageCfg.key) .map((r) => `| \`${path.basename(r.file)}\` | ${r.theme} | ${r.mode === 'light' ? '浅色' : '暗色'} |`) .join('\n')} `; fs.writeFileSync(path.join(outDir, 'README.md'), readme); } // 更新总 README const indexMd = `# 主题比稿截图总览 生成时间:${new Date().toISOString().slice(0, 10)} ## 页面 | 目录 | 页面 | |------|------| | \`.\`(根目录 png) | 租赁合同 · 列表 | | [\`form/\`](./form/) | 租赁合同 · 新增表单 | | [\`workbench/\`](./workbench/) | 工作台 | ## 五套主题 | 编号 | 主题 | 气质 | |------|------|------| | A | Linear | 冷 · 紫靛 · 精致中后台 | | B | Stripe | 冷 · 品牌紫 · 金融精致 | | C | Airbnb | 暖 · 珊瑚红 · 出行品牌 | | D | Claude | 暖 · 陶土米 · 人文克制 | | E | Intercom | 暖 · 米白黑 · 温和 B2B | 每页均为 5 主题 × 浅/暗 = 10 张。皮肤为 token 叠加预览,定稿后再完整落地。 `; fs.writeFileSync(path.join(outRoot, 'README.md'), indexMd); await browser.close(); console.log('\n完成 →', outRoot); } main().catch((err) => { console.error(err); process.exit(1); });