feat(user-guide): 添加图片点击放大和批量导出PNG功能

演示页面增强:
- 引入 html2canvas 库支持导出为 PNG
- 添加顶部工具栏和批量导出按钮
- 图片点击放大/缩小交互
- 单页导出按钮在每个卡片头部
- 兼容 file:// 和 HTTP 协议的导出处理
- 本地文件模式下显示协议限制提示
This commit is contained in:
2026-04-20 02:28:42 +08:00
parent 846771c948
commit d05a6cd529
+301 -16
View File
@@ -4,6 +4,8 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>阿尼坊小程序 - 用户指南(公众号截图版)</title>
<!-- html2canvas 库 - 用于导出PNG -->
<script src="https://cdn.jsdelivr.net/npm/html2canvas@1.4.1/dist/html2canvas.min.js"></script>
<style>
:root {
--primary-color: #6366f1;
@@ -31,9 +33,40 @@
line-height: 1.7;
}
/* 顶部提示工具栏 - 已隐藏 */
/* 顶部提示工具栏 */
.toolbar {
display: none;
display: flex;
justify-content: space-between;
align-items: center;
width: 393px;
background-color: var(--bg-content);
padding: 12px 20px;
margin-bottom: 20px;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}
.toolbar-text {
color: var(--text-muted);
font-size: 14px;
}
.toolbar-btn {
background-color: var(--primary-color);
color: #fff;
border: none;
padding: 8px 16px;
border-radius: 6px;
font-size: 14px;
cursor: pointer;
transition: all 0.2s;
}
.toolbar-btn:hover {
background-color: #4f46e5;
transform: scale(1.02);
}
.toolbar-btn:disabled {
background-color: #94a3b8;
cursor: not-allowed;
transform: none;
}
/* 核心:iPhone16 竖屏卡片 9:19.5 (393x852) - 自适应高度 */
@@ -100,7 +133,7 @@
.cover-list ol { margin-bottom: 0; }
.cover-list li { margin-bottom: 14px; font-weight: 500; font-size: 16px;}
/* 截图图片控制 */
/* 截图图片控制 - 点击放大功能 */
.img-wrapper { text-align: center; margin: 10px 0; flex-shrink: 0; }
.actual-img {
max-width: 100%;
@@ -108,6 +141,23 @@
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
object-fit: contain;
border: 1px solid #e2e8f0;
cursor: pointer;
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.actual-img:hover {
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
}
.actual-img.enlarged {
transform: scale(1.8);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
z-index: 10;
position: relative;
}
.img-enlarge-hint {
font-size: 12px;
color: #94a3b8;
margin-top: 4px;
opacity: 0.8;
}
/* 表格样式适配移动端 */
@@ -130,16 +180,34 @@
}
code { font-family: ui-monospace, monospace; }
p code, li code { background-color: #f1f5f9; color: #ef4444; padding: 2px 6px; border-radius: 4px; font-size: 14px; }
/* 导出按钮样式 */
.export-btn {
background: transparent;
color: #fff;
border: 1px solid rgba(255,255,255,0.6);
padding: 4px 10px;
border-radius: 4px;
font-size: 12px;
cursor: pointer;
margin-left: 8px;
transition: all 0.2s;
}
.export-btn:hover {
background: rgba(255,255,255,0.2);
border-color: #fff;
}
</style>
</head>
<body>
<div class="toolbar">
💡 页面布局已调整为 iPhone 16 竖屏比例 (393x852) - 共 7 页,请直接框选白色卡片范围进行截图。
<span class="toolbar-text">💡 iPhone 16 竖屏比例 (393x852) - 共 7 页</span>
<button class="toolbar-btn" onclick="exportAllCards()" id="exportAllBtn">📦 批量导出全部</button>
</div>
<!-- Page 1: 封面 -->
<div class="iphone-card">
<div class="card-header"><span>🦊 阿尼坊</span><span>01 / 07</span></div>
<div class="card-header"><span>🦊 阿尼坊</span><span>01 / 07</span><button class="export-btn" onclick="exportCard(this)">导出PNG</button></div>
<div class="card-body cover-body">
<div class="cover-icon">🦊</div>
<h1 class="cover-title">阿尼坊小程序</h1>
@@ -160,7 +228,7 @@
<!-- Page 2: 登录流程 -->
<div class="iphone-card">
<div class="card-header"><span>🦊 阿尼坊</span><span>02 / 07</span></div>
<div class="card-header"><span>🦊 阿尼坊</span><span>02 / 07</span><button class="export-btn" onclick="exportCard(this)">导出PNG</button></div>
<div class="card-body">
<h1>一、登录流程</h1>
<p>首次打开阿尼坊小程序时,系统会引导您完成微信登录。</p>
@@ -171,7 +239,8 @@
<h3>2. 触发登录</h3>
<p>当您点击需要登录的功能(如申摊)时,系统会弹出登录提示框。</p>
<div class="img-wrapper">
<img src="screenshots/2_前往登录弹窗.jpg" alt="登录提示弹窗" class="actual-img" style="max-height: 280px;">
<img src="screenshots/2_前往登录弹窗.jpg" alt="登录提示弹窗" class="actual-img" crossorigin="anonymous">
<div class="img-enlarge-hint">点击图片可放大查看</div>
</div>
<div class="alert warning" style="margin-top: auto;">
@@ -185,20 +254,22 @@
<!-- Page 3: 登录流程 (续) -->
<div class="iphone-card">
<div class="card-header"><span>🦊 阿尼坊</span><span>03 / 07</span></div>
<div class="card-header"><span>🦊 阿尼坊</span><span>03 / 07</span><button class="export-btn" onclick="exportCard(this)">导出PNG</button></div>
<div class="card-body">
<h1>一、登录流程 (续)</h1>
<h3>3. 填写个人信息</h3>
<p>登录表单包含头像选择、昵称填写和用户协议勾选。</p>
<div class="img-wrapper">
<img src="screenshots/3_填写昵称_头像信息.png" alt="登录表单" class="actual-img" style="max-height: 220px;">
<img src="screenshots/3_填写昵称_头像信息.png" alt="登录表单" class="actual-img" crossorigin="anonymous">
<div class="img-enlarge-hint">点击图片可放大查看</div>
</div>
<h3>4. 手机号授权</h3>
<p>系统申请获取微信绑定手机号,用于验证码接收和客服联系。</p>
<div class="img-wrapper">
<img src="screenshots/4_登录后授权手机号弹窗.png" alt="手机号授权" class="actual-img" style="max-height: 220px;">
<img src="screenshots/4_登录后授权手机号弹窗.png" alt="手机号授权" class="actual-img" crossorigin="anonymous">
<div class="img-enlarge-hint">点击图片可放大查看</div>
</div>
<h3>5. 完成登录</h3>
@@ -209,7 +280,7 @@
<!-- Page 4: 实名认证 -->
<div class="iphone-card">
<div class="card-header"><span>🦊 阿尼坊</span><span>04 / 07</span></div>
<div class="card-header"><span>🦊 阿尼坊</span><span>04 / 07</span><button class="export-btn" onclick="exportCard(this)">导出PNG</button></div>
<div class="card-body">
<h1>二、实名认证</h1>
<p>实名认证是使用设施预约和摊位申请功能的前置条件。</p>
@@ -217,7 +288,8 @@
<h3>认证入口</h3>
<p>进入「我的」页面,点击「前往实名认证」按钮。</p>
<div class="img-wrapper">
<img src="screenshots/5_我的界面_前往实名认证.png" alt="实名认证入口" class="actual-img" style="max-height: 220px;">
<img src="screenshots/5_我的界面_前往实名认证.png" alt="实名认证入口" class="actual-img" crossorigin="anonymous">
<div class="img-enlarge-hint">点击图片可放大查看</div>
</div>
<h3>认证信息填写</h3>
@@ -231,7 +303,8 @@
</tbody>
</table>
<div class="img-wrapper">
<img src="screenshots/6_填写实名认证信息.png" alt="实名认证表单" class="actual-img" style="max-height: 180px;">
<img src="screenshots/6_填写实名认证信息.png" alt="实名认证表单" class="actual-img" crossorigin="anonymous">
<div class="img-enlarge-hint">点击图片可放大查看</div>
</div>
<div class="alert warning" style="margin-top: auto;">
@@ -244,7 +317,7 @@
<!-- Page 5: 申摊流程 -->
<div class="iphone-card">
<div class="card-header"><span>🦊 阿尼坊</span><span>05 / 07</span></div>
<div class="card-header"><span>🦊 阿尼坊</span><span>05 / 07</span><button class="export-btn" onclick="exportCard(this)">导出PNG</button></div>
<div class="card-body">
<h1>三、申摊流程</h1>
<p>申请摊位参加阿尼坊动漫市集。</p>
@@ -284,7 +357,7 @@
<!-- Page 6: 申摊流程 (续) -->
<div class="iphone-card">
<div class="card-header"><span>🦊 阿尼坊</span><span>06 / 07</span></div>
<div class="card-header"><span>🦊 阿尼坊</span><span>06 / 07</span><button class="export-btn" onclick="exportCard(this)">导出PNG</button></div>
<div class="card-body">
<h1>三、申摊流程 (续)</h1>
@@ -319,7 +392,7 @@
<!-- Page 7: 预约详情 -->
<div class="iphone-card">
<div class="card-header"><span>🦊 阿尼坊</span><span>07 / 07</span></div>
<div class="card-header"><span>🦊 阿尼坊</span><span>07 / 07</span><button class="export-btn" onclick="exportCard(this)">导出PNG</button></div>
<div class="card-body">
<h1>四、预约详情</h1>
<p>查看摊位申请和设施预约记录。</p>
@@ -355,4 +428,216 @@
</div>
</body>
<script>
// 图片 base64 缓存(解决跨域图片 tainted canvas 问题)
const imageCache = {};
let preloadComplete = false;
let isLocalFile = window.location.protocol === 'file:';
let allowTaintedCanvas = false;
// 显示协议兼容性提示
function showProtocolWarning() {
const toolbar = document.querySelector('.toolbar');
if (toolbar && isLocalFile) {
const warningDiv = document.createElement('div');
warningDiv.style.cssText = 'width:393px;background:#fffbeb;border-left:4px solid #f59e0b;padding:12px 16px;margin-bottom:20px;border-radius:8px;font-size:14px;color:#92400e;';
warningDiv.innerHTML = `
<strong>⚠️ 本地文件模式限制</strong>
<p style="margin:6px 0 0;">当前使用 file:// 协议直接打开,图片导出功能受限。</p>
<p style="margin:4px 0;">推荐方式:启动本地 HTTP 服务器</p>
<code style="background:#fef3c7;padding:2px 6px;border-radius:4px;">npx serve -l 3000</code>
<p style="margin:4px 0;">然后访问 <a href="#" onclick="copyToLocalhost()" style="color:#d97706;">http://localhost:3000/...</a></p>
`;
toolbar.insertAdjacentElement('beforebegin', warningDiv);
allowTaintedCanvas = true;
}
}
// 复制 localhost URL
function copyToLocalhost() {
const localhostUrl = 'http://localhost:3000/' + window.location.pathname.split('/').pop();
navigator.clipboard.writeText(localhostUrl).then(() => {
alert('已复制: ' + localhostUrl + '\n请启动服务器后访问');
});
}
// 预加载图片转为 base64(仅 HTTP/HTTPS 协议有效)
function preloadImages() {
// file:// 协议下 XMLHttpRequest 被 CORS 阻止,跳过预加载
if (isLocalFile) {
console.warn('file:// 协议下无法预加载图片,将使用 allowTaint 模式导出');
preloadComplete = true;
allowTaintedCanvas = true;
return;
}
const images = document.querySelectorAll('.actual-img');
const promises = [];
images.forEach(img => {
const src = img.getAttribute('src');
if (src && !imageCache[src]) {
const promise = new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.onload = function() {
const reader = new FileReader();
reader.onloadend = function() {
imageCache[src] = reader.result;
img.src = reader.result; // 替换为 base64
resolve();
};
reader.readAsDataURL(xhr.response);
};
xhr.onerror = function() {
console.warn('图片预加载失败:', src);
resolve(); // 即使失败也继续,不阻塞导出
};
xhr.open('GET', src);
xhr.responseType = 'blob';
xhr.send();
});
promises.push(promise);
}
});
// 所有图片加载完成后标记
Promise.all(promises).then(() => {
preloadComplete = true;
console.log('图片预加载完成,可以安全导出');
});
}
// 页面加载完成后初始化
window.addEventListener('load', () => {
showProtocolWarning();
preloadImages();
});
// 图片点击放大/缩小切换
document.querySelectorAll('.actual-img').forEach(img => {
img.addEventListener('click', function() {
if (this.classList.contains('enlarged')) {
// 缩小恢复
this.classList.remove('enlarged');
} else {
// 先缩小其他已放大的图片
document.querySelectorAll('.actual-img.enlarged').forEach(other => {
other.classList.remove('enlarged');
});
// 放大当前图片
this.classList.add('enlarged');
}
});
});
// 导出单个卡片为PNG
function exportCard(button) {
// file:// 协议下跳过预加载检查,使用 allowTaint 模式
if (!isLocalFile && !preloadComplete) {
alert('图片正在加载中,请稍后再试导出...\n建议:等待页面完全加载后再点击导出');
return;
}
// file:// 协议下提示用户
if (isLocalFile) {
console.warn('file:// 模式导出:图片可能无法正确渲染,建议使用 HTTP 服务器');
}
const card = button.closest('.iphone-card');
const pageText = card.querySelector('.card-header span:nth-child(2)').textContent.trim();
const pageNum = pageText.split('/')[0].trim().replace('0', '');
const title = getCardTitle(pageNum);
html2canvas(card, {
scale: 2, // 高清导出
useCORS: !isLocalFile, // HTTP 下启用跨域,file:// 下禁用
allowTaint: allowTaintedCanvas, // file:// 下允许污染画布
backgroundColor: '#ffffff',
logging: false
}).then(canvas => {
const link = document.createElement('a');
link.download = `阿尼坊用户指南_${title}.png`;
link.href = canvas.toDataURL('image/png');
link.click();
}).catch(err => {
let errorMsg = err.message;
if (isLocalFile) {
errorMsg += '\n\nfile:// 协议限制:请使用 HTTP 服务器访问';
}
alert('导出失败:' + errorMsg);
console.error('导出错误:', err);
});
}
// 根据页码获取卡片标题
function getCardTitle(pageNum) {
const titles = {
'1': '封面',
'2': '登录流程',
'3': '登录流程续',
'4': '实名认证',
'5': '申摊流程',
'6': '申摊流程续',
'7': '预约详情'
};
return titles[pageNum] || `${pageNum}`;
}
// 批量导出所有卡片
async function exportAllCards() {
// file:// 协议下跳过预加载检查,使用 allowTaint 模式
if (!isLocalFile && !preloadComplete) {
alert('图片正在加载中,请稍后再试批量导出...\n建议:等待页面完全加载后再点击导出');
return;
}
// file:// 协议下提示用户
if (isLocalFile) {
console.warn('file:// 模式批量导出:图片可能无法正确渲染,建议使用 HTTP 服务器');
}
const btn = document.getElementById('exportAllBtn');
const cards = document.querySelectorAll('.iphone-card');
const total = cards.length;
btn.disabled = true;
btn.textContent = `⏳ 正在导出...`;
for (let i = 0; i < total; i++) {
btn.textContent = `⏳ 导出中 (${i+1}/${total})`;
const card = cards[i];
const pageNum = (i + 1).toString();
const title = getCardTitle(pageNum);
try {
const canvas = await html2canvas(card, {
scale: 2,
useCORS: !isLocalFile, // HTTP 下启用跨域,file:// 下禁用
allowTaint: allowTaintedCanvas, // file:// 下允许污染画布
backgroundColor: '#ffffff',
logging: false
});
const link = document.createElement('a');
link.download = `阿尼坊用户指南_${title}.png`;
link.href = canvas.toDataURL('image/png');
link.click();
// 稍作延迟避免浏览器阻塞
await new Promise(r => setTimeout(r, 300));
} catch (err) {
console.error(`${pageNum}页导出失败:`, err);
let errorMsg = err.message;
if (isLocalFile) {
errorMsg += ' (file:// 协议限制)';
}
alert(`${pageNum}页导出失败:${errorMsg}`);
}
}
btn.disabled = false;
btn.textContent = '📦 批量导出全部';
alert('全部导出完成!');
}
</script>
</html>