feat: 迁移 screenshot-modals.js 和 md-viewer.html 至 _templates/ 目录
- md-viewer.html 添加 <base href="../"> 修复子目录相对路径 - screenshot-modals.js 新增 fullPage 长图截取支持,action 改为可选 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,185 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<base href="../">
|
||||
<title>文档预览</title>
|
||||
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
||||
<style>
|
||||
:root {
|
||||
--primary: #1677FF;
|
||||
--primary-light: #E6F7FF;
|
||||
--text-primary: #1a1a2e;
|
||||
--text-secondary: #5a607f;
|
||||
--text-placeholder: #999;
|
||||
--border: #e8ecf1;
|
||||
--bg: #ffffff;
|
||||
}
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Helvetica Neue', 'Segoe UI', sans-serif; background: #f5f7fa; color: var(--text-primary); }
|
||||
|
||||
/* 顶部导航栏 */
|
||||
.topbar { position: sticky; top: 0; z-index: 100; background: var(--bg); border-bottom: 1px solid var(--border); padding: 12px 24px; display: flex; align-items: center; gap: 12px; box-shadow: 0 1px 4px rgba(0,0,0,0.06); }
|
||||
.topbar__back { display: inline-flex; align-items: center; gap: 4px; font-size: 13px; color: var(--primary); cursor: pointer; text-decoration: none; padding: 4px 10px; border-radius: 6px; transition: background .15s; border: none; background: none; }
|
||||
.topbar__back:hover { background: var(--primary-light); }
|
||||
.topbar__title { font-size: 15px; font-weight: 600; color: var(--text-primary); }
|
||||
.topbar__path { font-size: 12px; color: var(--text-placeholder); margin-left: auto; }
|
||||
|
||||
/* 内容区 */
|
||||
.content { max-width: 900px; margin: 0 auto; padding: 32px 40px 60px; background: var(--bg); min-height: calc(100vh - 50px); font-size: 14px; line-height: 1.8; }
|
||||
.loading { display: flex; align-items: center; justify-content: center; height: 300px; color: var(--text-placeholder); font-size: 14px; }
|
||||
.error { color: #ff4d4f; }
|
||||
|
||||
/* Markdown 渲染样式 */
|
||||
.content h1 { font-size: 24px; font-weight: 800; margin: 0 0 16px; padding-bottom: 10px; border-bottom: 2px solid var(--border); }
|
||||
.content h2 { font-size: 20px; font-weight: 700; margin: 32px 0 12px; padding-bottom: 8px; border-bottom: 1px solid var(--border); }
|
||||
.content h3 { font-size: 17px; font-weight: 600; margin: 24px 0 8px; }
|
||||
.content h4 { font-size: 15px; font-weight: 600; margin: 20px 0 6px; }
|
||||
.content p { font-size: 14px; line-height: 1.8; margin: 8px 0; }
|
||||
.content ul, .content ol { padding-left: 20px; margin: 8px 0; font-size: 14px; line-height: 1.8; }
|
||||
.content table { width: 100%; border-collapse: collapse; margin: 12px 0; font-size: 13px; }
|
||||
.content th { background: #fafafa; font-weight: 600; text-align: left; padding: 8px 12px; border: 1px solid var(--border); }
|
||||
.content td { padding: 8px 12px; border: 1px solid var(--border); }
|
||||
.content code { background: #f5f5f5; padding: 2px 6px; border-radius: 4px; font-size: 13px; font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', monospace; }
|
||||
.content pre { background: #f6f8fa; border-radius: 8px; padding: 16px; overflow-x: auto; margin: 12px 0; }
|
||||
.content pre code { background: none; padding: 0; font-size: 13px; line-height: 1.6; }
|
||||
.content blockquote { border-left: 4px solid var(--primary); padding: 8px 16px; margin: 12px 0; background: var(--primary-light); border-radius: 0 4px 4px 0; }
|
||||
.content blockquote p { color: var(--text-secondary); margin: 0; }
|
||||
.content strong { color: var(--text-primary); }
|
||||
.content hr { border: none; border-top: 1px solid var(--border); margin: 24px 0; }
|
||||
.content img { max-width: 100%; height: auto; border-radius: 8px; border: 1px solid var(--border); margin: 8px 0; cursor: pointer; transition: opacity .15s; }
|
||||
.content img:hover { opacity: .85; }
|
||||
|
||||
/* Lightbox 图片放大遮罩 */
|
||||
.lightbox { display: none; position: fixed; inset: 0; z-index: 9999; background: rgba(0,0,0,.75); align-items: center; justify-content: center; cursor: zoom-out; }
|
||||
.lightbox.active { display: flex; }
|
||||
.lightbox img { max-width: 92vw; max-height: 92vh; border-radius: 8px; box-shadow: 0 8px 32px rgba(0,0,0,.4); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="topbar">
|
||||
<button class="topbar__back" onclick="window.close();history.back()">← 返回</button>
|
||||
<span class="topbar__title" id="doc-title">文档预览</span>
|
||||
<span class="topbar__path" id="doc-path"></span>
|
||||
</div>
|
||||
<div class="content" id="content">
|
||||
<div class="loading">加载中...</div>
|
||||
</div>
|
||||
<!-- Lightbox 图片放大遮罩 -->
|
||||
<div class="lightbox" id="lightbox" onclick="this.classList.remove('active')">
|
||||
<img id="lightbox-img" src="" alt="放大预览">
|
||||
</div>
|
||||
<script>
|
||||
const params = new URLSearchParams(location.search);
|
||||
const filePath = params.get('file');
|
||||
const title = params.get('title');
|
||||
|
||||
if (title) {
|
||||
document.getElementById('doc-title').textContent = title;
|
||||
document.title = title + ' - 文档预览';
|
||||
}
|
||||
if (filePath) {
|
||||
document.getElementById('doc-path').textContent = filePath;
|
||||
fetch(filePath)
|
||||
.then(res => {
|
||||
if (!res.ok) throw new Error('文件未找到 (' + res.status + ')');
|
||||
return res.text();
|
||||
})
|
||||
.then(md => {
|
||||
/* 检测是否为自带 HTML 结构的文件(如共性需求说明.md) */
|
||||
const isRawHtml = /^\s*<(?:style|div|!doctype|html)/i.test(md);
|
||||
if (isRawHtml) {
|
||||
/* 为标题生成锚点 id(marked 新版不再自动生成) */
|
||||
function addHeadingIds(html) {
|
||||
return html.replace(/<h([1-6])>([\s\S]*?)<\/h\1>/g, function(_, lvl, inner) {
|
||||
var plain = inner.replace(/<[^>]*>/g, '');
|
||||
var id = plain.toLowerCase().replace(/[^\p{L}\p{N}\s-]/gu, '').trim().replace(/\s+/g, '-');
|
||||
return '<h' + lvl + ' id="' + id + '">' + inner + '</h' + lvl + '>';
|
||||
});
|
||||
}
|
||||
let processed = md;
|
||||
if (typeof marked !== 'undefined') {
|
||||
processed = md.replace(/(^[\s\S]*?<div id="main-content">)([\s\S]*?)(<\/div>\s*$)/i, function(_, before, mdBody, after) {
|
||||
return before + addHeadingIds(marked.parse(mdBody)) + after;
|
||||
});
|
||||
processed = processed.replace(/(<div id="toc-container">)([\s\S]*?)(<\/div>)/, function(_, before, tocBody, after) {
|
||||
return before + marked.parse(tocBody) + after;
|
||||
});
|
||||
}
|
||||
/* iframe 内注入:表格框线 + 排版样式 */
|
||||
const extraCss = '<style>'
|
||||
+ 'html{scroll-behavior:smooth}'
|
||||
+ 'body{font-family:-apple-system,BlinkMacSystemFont,"PingFang SC","Helvetica Neue","Segoe UI",sans-serif}'
|
||||
+ '#main-content table{width:100%;border-collapse:collapse;margin:12px 0;font-size:14px}'
|
||||
+ '#main-content th{background:#fafafa;font-weight:600;text-align:left;padding:8px 12px;border:1px solid #e8ecf1}'
|
||||
+ '#main-content td{padding:8px 12px;border:1px solid #e8ecf1}'
|
||||
+ '#main-content h1{font-size:24px;font-weight:800;margin:0 0 16px;padding-bottom:10px;border-bottom:2px solid #e8ecf1}'
|
||||
+ '#main-content h2{font-size:20px;font-weight:700;margin:32px 0 12px;padding-bottom:8px;border-bottom:1px solid #e8ecf1}'
|
||||
+ '#main-content h3{font-size:17px;font-weight:600;margin:24px 0 8px}'
|
||||
+ '#main-content h4{font-size:15px;font-weight:600;margin:20px 0 6px}'
|
||||
+ '#main-content p{font-size:14px;line-height:1.8;margin:8px 0}'
|
||||
+ '#main-content ul,#main-content ol{padding-left:20px;margin:8px 0;font-size:14px;line-height:1.8}'
|
||||
+ '#main-content code{background:#f5f5f5;padding:2px 6px;border-radius:4px;font-size:13px}'
|
||||
+ '#main-content blockquote{border-left:4px solid #1677FF;padding:8px 16px;margin:12px 0;background:#E6F7FF;border-radius:0 4px 4px 0}'
|
||||
+ '#toc-container a.active{color:#1677FF;font-weight:600}'
|
||||
+ '</style>';
|
||||
/* iframe 内注入:滚动时高亮当前目录项 */
|
||||
const tocScript = '<script>'
|
||||
+ 'document.addEventListener("DOMContentLoaded",function(){'
|
||||
+ 'var links=document.querySelectorAll("#toc-container a[href^=\\"#\\"]");'
|
||||
+ 'var items=[];'
|
||||
+ 'links.forEach(function(a){var id=decodeURIComponent(a.getAttribute("href").slice(1));var el=document.getElementById(id);if(el)items.push({el:el,link:a})});'
|
||||
+ 'if(!items.length)return;'
|
||||
+ 'function update(){var sy=window.scrollY||document.documentElement.scrollTop;var cur=items[0];'
|
||||
+ 'for(var i=0;i<items.length;i++){if(items[i].el.offsetTop<=sy+80)cur=items[i]}'
|
||||
+ 'links.forEach(function(a){a.classList.remove("active")});if(cur)cur.link.classList.add("active")}'
|
||||
+ 'window.addEventListener("scroll",update);update()});'
|
||||
+ '<\/script>';
|
||||
/* 用 iframe 隔离渲染 */
|
||||
document.querySelector('.topbar').style.display = 'none';
|
||||
const wrap = document.getElementById('content');
|
||||
wrap.style.cssText = 'max-width:none;margin:0;padding:0;min-height:100vh';
|
||||
const iframe = document.createElement('iframe');
|
||||
iframe.style.cssText = 'width:100%;height:100vh;border:none';
|
||||
iframe.srcdoc = '<!DOCTYPE html><html><head><meta charset="UTF-8">' + extraCss + '</head><body>' + processed + tocScript + '</body></html>';
|
||||
wrap.innerHTML = '';
|
||||
wrap.appendChild(iframe);
|
||||
} else {
|
||||
/* 计算 Markdown 文件所在目录,用于重写图片相对路径 */
|
||||
const mdBase = filePath.substring(0, filePath.lastIndexOf('/') + 1);
|
||||
const content = document.getElementById('content');
|
||||
if (typeof marked !== 'undefined') {
|
||||
content.innerHTML = marked.parse(md);
|
||||
/* 将图片相对路径补全为基于 Markdown 文件目录的路径 */
|
||||
content.querySelectorAll('img').forEach(img => {
|
||||
const src = img.getAttribute('src');
|
||||
if (src && !src.startsWith('http') && !src.startsWith('/') && !src.startsWith('data:')) {
|
||||
img.src = mdBase + src;
|
||||
}
|
||||
});
|
||||
/* 绑定图片点击放大 */
|
||||
content.querySelectorAll('img').forEach(img => {
|
||||
img.addEventListener('click', () => {
|
||||
document.getElementById('lightbox-img').src = img.src;
|
||||
document.getElementById('lightbox').classList.add('active');
|
||||
});
|
||||
});
|
||||
} else {
|
||||
content.innerHTML = '<pre style="white-space:pre-wrap;font-size:14px;line-height:1.8">' + md.replace(/</g,'<').replace(/>/g,'>') + '</pre>';
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
document.getElementById('content').innerHTML = '<div class="loading error">加载失败:' + err.message + '<br><small style="color:var(--text-placeholder);margin-top:8px;display:block">请确认文件路径:' + filePath + '</small></div>';
|
||||
});
|
||||
} else {
|
||||
document.getElementById('content').innerHTML = '<div class="loading">未指定文件路径</div>';
|
||||
}
|
||||
/* ESC 键关闭 lightbox */
|
||||
document.addEventListener('keydown', e => {
|
||||
if (e.key === 'Escape') document.getElementById('lightbox').classList.remove('active');
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,105 @@
|
||||
/**
|
||||
* 通用弹窗/状态截图脚本
|
||||
*
|
||||
* 使用方式:
|
||||
* node screenshot-modals.js <config.json>
|
||||
*
|
||||
* config.json 格式:
|
||||
* {
|
||||
* "baseDir": "employee-app/emergency-medical",
|
||||
* "tasks": [
|
||||
* { "page": "resource-map.html", "output": "resource-map-simulate.jpg",
|
||||
* "action": "() => { document.querySelector('.simulate-panel').style.display = 'block'; }" },
|
||||
* { "page": "detail.html", "output": "detail-full.jpg", "fullPage": true }
|
||||
* ]
|
||||
* }
|
||||
*
|
||||
* task 字段说明:
|
||||
* page - 页面 HTML 文件路径(相对于 baseDir)
|
||||
* output - 输出截图文件名
|
||||
* action - 可选,截图前执行的 JS(触发弹窗/状态)
|
||||
* fullPage - 可选,true 时去掉 phone-shell 高度限制,截取完整滚动内容(长图)
|
||||
*
|
||||
* 如果不传 config.json,则使用内置的应急就医模块配置作为示例。
|
||||
*/
|
||||
const puppeteer = require('puppeteer');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
const delay = ms => new Promise(r => setTimeout(r, ms));
|
||||
|
||||
// 只截取 .phone-shell 元素,避免外壳背景
|
||||
// fullPage=true 时去掉高度限制和溢出裁剪,截取完整内容(长图)
|
||||
async function screenshotPhoneShell(page, filepath, fullPage) {
|
||||
if (fullPage) {
|
||||
await page.evaluate(() => {
|
||||
const shell = document.querySelector('.phone-shell');
|
||||
if (shell) { shell.style.height = 'auto'; shell.style.borderRadius = '0'; }
|
||||
const sc = document.querySelector('.scroll-content');
|
||||
if (sc) { sc.style.overflow = 'visible'; sc.style.flex = 'none'; }
|
||||
});
|
||||
await new Promise(r => setTimeout(r, 100));
|
||||
}
|
||||
const shell = await page.$('.phone-shell');
|
||||
if (shell) {
|
||||
await shell.screenshot({ path: filepath, type: 'jpeg', quality: 85 });
|
||||
} else {
|
||||
await page.screenshot({ path: filepath, type: 'jpeg', quality: 85 });
|
||||
}
|
||||
}
|
||||
|
||||
// 默认配置(应急就医模块示例)
|
||||
const DEFAULT_CONFIG = {
|
||||
baseDir: 'employee-app/emergency-medical',
|
||||
tasks: [
|
||||
{ page: 'resource-map.html', output: 'resource-map-simulate.jpg',
|
||||
action: `() => { const p = document.querySelector('.simulate-panel'); if(p) p.style.display = 'block'; }` },
|
||||
{ page: 'emergency-chat.html', output: 'emergency-chat-modal.jpg',
|
||||
action: `() => { const m = document.querySelector('.modal-overlay'); if(m){m.style.display='flex';m.classList.add('show');} }` },
|
||||
{ page: 'emergency-chat.html', output: 'emergency-chat-voice.jpg',
|
||||
action: `() => { const btns = document.querySelectorAll('[class*="voice"]'); btns.forEach(b => b.click()); }` },
|
||||
{ page: 'emergency-chat.html', output: 'emergency-chat-plus.jpg',
|
||||
action: `() => { const p = document.querySelector('.plus-panel'); if(p){p.style.display='grid';p.classList.add('show');} }` },
|
||||
{ page: 'appointment-detail-pending.html', output: 'appointment-detail-pending-modal.jpg',
|
||||
action: `() => { const m = document.querySelector('.modal-mask'); if(m){m.style.display='flex';m.classList.add('modal-mask--show');} }` },
|
||||
{ page: 'appointment-detail-confirmed.html', output: 'appointment-detail-confirmed-modal.jpg',
|
||||
action: `() => { const m = document.querySelector('.modal-mask'); if(m){m.style.display='flex';m.classList.add('modal-mask--show');} }` }
|
||||
]
|
||||
};
|
||||
|
||||
(async () => {
|
||||
// 读取配置
|
||||
let config = DEFAULT_CONFIG;
|
||||
const configArg = process.argv[2];
|
||||
if (configArg && fs.existsSync(configArg)) {
|
||||
config = JSON.parse(fs.readFileSync(configArg, 'utf-8'));
|
||||
console.log(`Using config: ${configArg}`);
|
||||
} else {
|
||||
console.log('Using default config (emergency-medical)');
|
||||
}
|
||||
|
||||
const baseDir = path.resolve(config.baseDir);
|
||||
const baseUrl = 'file:///' + baseDir.replace(/\\/g, '/');
|
||||
const outDir = path.join(baseDir, 'doc', 'screenshots');
|
||||
|
||||
// 确保输出目录存在
|
||||
if (!fs.existsSync(outDir)) fs.mkdirSync(outDir, { recursive: true });
|
||||
|
||||
const browser = await puppeteer.launch({ headless: true, args: ['--no-sandbox'] });
|
||||
const page = await browser.newPage();
|
||||
await page.setViewport({ width: 500, height: 900, deviceScaleFactor: 1 });
|
||||
|
||||
for (const task of config.tasks) {
|
||||
await page.goto(`${baseUrl}/${task.page}`, { waitUntil: 'networkidle0' });
|
||||
// 执行 action 触发弹窗/状态(action 可选)
|
||||
if (task.action) {
|
||||
await page.evaluate(new Function('return (' + task.action + ')()'));
|
||||
await delay(300);
|
||||
}
|
||||
await screenshotPhoneShell(page, path.join(outDir, task.output), task.fullPage);
|
||||
console.log(`Done: ${task.output}`);
|
||||
}
|
||||
|
||||
await browser.close();
|
||||
console.log(`\nAll ${config.tasks.length} screenshots saved to: ${outDir}`);
|
||||
})();
|
||||
Reference in New Issue
Block a user