screenshot.js 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
  1. /**
  2. * 自动截图脚本 - 遍历所有角色和页面,生成截图并拼接为横版A4 PDF
  3. * 依赖:puppeteer-core (使用本地 Chrome)
  4. * 用法:node screenshot.js
  5. */
  6. const puppeteer = require('puppeteer-core');
  7. const path = require('path');
  8. const fs = require('fs');
  9. const PROTO_PATH = path.resolve(__dirname, '../../prototype/index.html');
  10. const OUTPUT_DIR = path.resolve(__dirname, '../../prd');
  11. const OUTPUT_PDF = path.join(OUTPUT_DIR, 'nwafu-exam-demoImages-202604.pdf');
  12. const TEMP_DIR = path.join(__dirname, '_temp_screenshots');
  13. // 本地 Chrome 路径
  14. const CHROME_PATH = '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome';
  15. // 角色列表
  16. const ROLES = [
  17. { key: 'newGraduate', label: '研究生新入学' },
  18. { key: 'graduateSenior', label: '研究生在读(研四)' },
  19. { key: 'newUndergraduate', label: '本科生新入学' },
  20. { key: 'undergraduateSenior', label: '本科生在读(大四)' },
  21. { key: 'newStaff', label: '教职工新入职' },
  22. { key: 'existingStaff', label: '教职工存量' }
  23. ];
  24. // 页面列表
  25. const PAGES = [
  26. { key: 'workbench', label: '工作台' },
  27. { key: 'statistics', label: '数据统计' },
  28. { key: 'approval', label: '人员审批' }
  29. ];
  30. (async () => {
  31. // 确保临时目录存在
  32. if (!fs.existsSync(TEMP_DIR)) fs.mkdirSync(TEMP_DIR, { recursive: true });
  33. const browser = await puppeteer.launch({
  34. headless: 'new',
  35. executablePath: CHROME_PATH,
  36. args: ['--no-sandbox', '--disable-setuid-sandbox']
  37. });
  38. const page = await browser.newPage();
  39. // 横版 1920x1080
  40. await page.setViewport({ width: 1920, height: 1080 });
  41. const fileUrl = 'file://' + PROTO_PATH;
  42. await page.goto(fileUrl, { waitUntil: 'networkidle0', timeout: 30000 });
  43. // 等待 Vue 渲染
  44. await page.waitForSelector('#app .layout', { timeout: 10000 });
  45. const screenshots = [];
  46. for (const role of ROLES) {
  47. // 切换角色
  48. await page.evaluate((roleKey) => {
  49. const app = document.querySelector('#app').__vue__;
  50. app.switchRole(roleKey);
  51. }, role.key);
  52. await sleep(300);
  53. // 关闭所有弹窗
  54. await page.evaluate(() => {
  55. const app = document.querySelector('#app').__vue__;
  56. app.showBindDialog = false;
  57. app.showGraduateReport = false;
  58. app.showRejectDialog = false;
  59. });
  60. await sleep(300);
  61. for (const pg of PAGES) {
  62. // 切换页面
  63. await page.evaluate((pageKey) => {
  64. const app = document.querySelector('#app').__vue__;
  65. app.activePage = pageKey;
  66. }, pg.key);
  67. await sleep(800);
  68. // 如果是统计页面,等待图表渲染
  69. if (pg.key === 'statistics') {
  70. await page.evaluate(() => {
  71. const app = document.querySelector('#app').__vue__;
  72. if (app.renderStatsCharts) app.renderStatsCharts();
  73. });
  74. await sleep(1000);
  75. }
  76. const filename = `${role.key}_${pg.key}.png`;
  77. const filepath = path.join(TEMP_DIR, filename);
  78. // 隐藏左侧菜单栏,只截取主内容区
  79. await page.evaluate(() => {
  80. const sidebar = document.querySelector('.sidebar');
  81. if (sidebar) sidebar.style.display = 'none';
  82. const mainWrapper = document.querySelector('.main-wrapper');
  83. if (mainWrapper) mainWrapper.style.marginLeft = '0';
  84. });
  85. await sleep(200);
  86. await page.screenshot({ path: filepath, fullPage: false });
  87. // 恢复侧边栏
  88. await page.evaluate(() => {
  89. const sidebar = document.querySelector('.sidebar');
  90. if (sidebar) sidebar.style.display = '';
  91. const mainWrapper = document.querySelector('.main-wrapper');
  92. if (mainWrapper) mainWrapper.style.marginLeft = '';
  93. });
  94. screenshots.push({ filepath, title: `${role.label} - ${pg.label}` });
  95. console.log(` 截图完成: ${role.label} - ${pg.label}`);
  96. }
  97. }
  98. // 生成横版 A4 PDF (297mm x 210mm)
  99. console.log('\n正在生成PDF...');
  100. const pdfPage = await browser.newPage();
  101. await pdfPage.setViewport({ width: 1920, height: 1080 });
  102. // 构建 HTML 页面,每张截图占一页
  103. const imagesHtml = screenshots.map(s => {
  104. const imgData = fs.readFileSync(s.filepath);
  105. const base64 = imgData.toString('base64');
  106. return `
  107. <div class="page">
  108. <div class="page-title">${s.title}</div>
  109. <img src="data:image/png;base64,${base64}" />
  110. </div>
  111. `;
  112. }).join('');
  113. const html = `<!DOCTYPE html>
  114. <html>
  115. <head>
  116. <style>
  117. * { margin: 0; padding: 0; box-sizing: border-box; }
  118. body { font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif; }
  119. .page {
  120. width: 297mm;
  121. height: 210mm;
  122. padding: 8mm;
  123. display: flex;
  124. flex-direction: column;
  125. align-items: center;
  126. justify-content: center;
  127. page-break-after: always;
  128. overflow: hidden;
  129. }
  130. .page:last-child { page-break-after: auto; }
  131. .page-title {
  132. font-size: 14px;
  133. font-weight: 600;
  134. color: #333;
  135. margin-bottom: 6mm;
  136. text-align: center;
  137. }
  138. .page img {
  139. max-width: 100%;
  140. max-height: 185mm;
  141. object-fit: contain;
  142. border: 1px solid #e0e0e0;
  143. border-radius: 4px;
  144. }
  145. </style>
  146. </head>
  147. <body>${imagesHtml}</body>
  148. </html>`;
  149. await pdfPage.setContent(html, { waitUntil: 'networkidle0' });
  150. await pdfPage.pdf({
  151. path: OUTPUT_PDF,
  152. landscape: true,
  153. format: 'A4',
  154. printBackground: true,
  155. margin: { top: '0', bottom: '0', left: '0', right: '0' }
  156. });
  157. console.log(`PDF已生成: ${OUTPUT_PDF}`);
  158. // 清理临时文件
  159. for (const s of screenshots) {
  160. fs.unlinkSync(s.filepath);
  161. }
  162. fs.rmdirSync(TEMP_DIR);
  163. console.log('临时文件已清理');
  164. await browser.close();
  165. })();
  166. function sleep(ms) {
  167. return new Promise(resolve => setTimeout(resolve, ms));
  168. }