feat(user-guide): 添加图片点击放大和批量导出PNG功能
演示页面增强: - 引入 html2canvas 库支持导出为 PNG - 添加顶部工具栏和批量导出按钮 - 图片点击放大/缩小交互 - 单页导出按钮在每个卡片头部 - 兼容 file:// 和 HTTP 协议的导出处理 - 本地文件模式下显示协议限制提示
This commit is contained in:
@@ -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>
|
||||
Reference in New Issue
Block a user