图像(03):补充示例
补充示例
<h4>029-image-basic-attributes.html</h4>
<!-- 来源:5-图像.md - 图像基本属性体系 -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>图像基本属性体系</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
line-height: 1.6;
color: #333;
background: #f5f7fa;
padding: 40px 20px;
}
.container {
max-width: 1000px;
margin: 0 auto;
}
h1 {
text-align: center;
color: #222;
margin-bottom: 8px;
font-size: 32px;
}
.subtitle {
text-align: center;
color: #666;
margin-bottom: 36px;
font-size: 15px;
}
/* 属性速查表 */
.attr-table {
width: 100%;
border-collapse: collapse;
background: white;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 2px 12px rgba(0,0,0,0.06);
margin-bottom: 30px;
}
.attr-table th,
.attr-table td {
padding: 14px 18px;
text-align: left;
border-bottom: 1px solid #eee;
font-size: 14px;
}
.attr-table thead {
background: linear-gradient(135deg, #0066cc, #0052a3);
color: white;
}
.attr-table th {
font-weight: 600;
font-size: 13px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.attr-tag {
display: inline-block;
padding: 3px 8px;
background: #e3f2fd;
color: #0066cc;
border-radius: 4px;
font-family: 'Monaco', monospace;
font-size: 13px;
font-weight: 600;
}
.required { color: #dc3545; }
.recommended { color: #28a745; }
.optional { color: #666; }
/* 演示区域 */
.demo-section {
background: white;
border-radius: 12px;
padding: 28px;
margin-bottom: 24px;
box-shadow: 0 2px 12px rgba(0,0,0,0.06);
}
.demo-section h3 {
color: #333;
margin-bottom: 16px;
font-size: 18px;
display: flex;
align-items: center;
gap: 8px;
}
/* 图片展示 */
.image-showcase {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 20px;
margin-top: 20px;
}
.image-card {
background: #f8f9fa;
border-radius: 10px;
overflow: hidden;
transition: transform 0.2s, box-shadow 0.2s;
}
.image-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 24px rgba(0,0,0,0.12);
}
.image-card img {
width: 100%;
height: 180px;
object-fit: cover;
display: block;
}
.image-card-info {
padding: 14px;
}
.image-card-info h4 {
font-size: 14px;
color: #222;
margin-bottom: 4px;
}
.image-card-info p {
font-size: 12px;
color: #888;
line-height: 1.5;
}
/* alt 属性演示 */
.alt-demo {
display: flex;
flex-direction: column;
gap: 16px;
}
.alt-item {
display: flex;
align-items: flex-start;
gap: 16px;
padding: 16px;
background: #f8f9fa;
border-radius: 8px;
border-left: 4px solid transparent;
}
.alt-item.good { border-left-color: #28a745; }
.alt-item.bad { border-left-color: #dc3545; }
.alt-preview {
width: 80px;
height: 60px;
background: #ddd;
border-radius: 6px;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
font-size: 11px;
color: #888;
overflow: hidden;
}
.alt-preview img {
width: 100%;
height: 100%;
object-fit: cover;
}
.alt-text {
flex: 1;
}
.alt-label {
font-size: 12px;
font-weight: 600;
margin-bottom: 4px;
}
.alt-label.good { color: #28a745; }
.alt-label.bad { color: #dc3545; }
.alt-code {
font-family: 'Monaco', monospace;
font-size: 12px;
color: #555;
background: #fff;
padding: 6px 10px;
border-radius: 4px;
display: inline-block;
margin-top: 4px;
}
/* CLS 演示 */
.cls-demo {
position: relative;
background: #f0f0f0;
border-radius: 8px;
padding: 20px;
min-height: 200px;
}
.cls-placeholder {
background: linear-gradient(135deg, #e0e0e0 25%, #f0f0f0 25%, #f0f0f0 50%, #e0e0e0 50%, #e0e0e0 75%, #f0f0f0 75%);
background-size: 20px 20px;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
color: #999;
font-size: 14px;
min-height: 160px;
transition: all 0.3s;
}
.cls-loaded {
animation: fadeIn 0.5s ease-out;
}
@keyframes fadeIn {
from { opacity: 0; transform: scale(0.98); }
to { opacity: 1; transform: scale(1); }
}
.badge-inline {
display: inline-block;
padding: 2px 8px;
border-radius: 4px;
font-size: 11px;
font-weight: 600;
margin-left: 6px;
}
.badge-required { background: #f8d7da; color: #721c24; }
.badge-recommended { background: #d4edda; color: #155724; }
.badge-performance { background: #cce5ff; color: #004085; }
</style>
</head>
<body>
<div class="container">
<h1>🖼️ 图像基本属性体系</h1>
<p class="subtitle">掌握 img 标签完整属性,构建高性能、可访问的图像展示</p>
<!-- 属性速查表 -->
<table class="attr-table">
<thead>
<tr>
<th>属性</th>
<th>作用</th>
<th>常见取值</th>
<th>必要性</th>
</tr>
</thead>
<tbody>
<tr>
<td><span class="attr-tag">src</span></td>
<td>图像源地址</td>
<td>相对路径 / 绝对 URL / CDN 地址</td>
<td><span class="required">⚠️ 必填</span></td>
</tr>
<tr>
<td><span class="attr-tag">alt</span></td>
<td>替代文本(可访问性+SEO)</td>
<td>描述性文本 / 空字符串(装饰图)</td>
<td><span class="required">⚠️ 必填</span></td>
</tr>
<tr>
<td><span class="attr-tag">title</span></td>
<td>鼠标悬停提示文字</td>
<td>补充说明文字</td>
<td><span class="optional">可选</span></td>
</tr>
<tr>
<td><span class="attr-tag">width/height</span></td>
<td>图像尺寸(防布局偏移)</td>
<td>像素值(如 800x600)</td>
<td><span class="recommended">✅ 推荐</span></td>
</tr>
<tr>
<td><span class="attr-tag">loading</span></td>
<td>懒加载控制</td>
<td>"lazy" | "eager"</td>
<td><span class="recommended">✅ 推荐</span></td>
</tr>
<tr>
<td><span class="attr-tag">decoding</span></td>
<td>解码方式</td>
<td>"async" | "sync" | "auto"</td>
<td><span class="performance">⚡ 性能优化</span></td>
</tr>
<tr>
<td><span class="attr-tag">fetchpriority</span></td>
<td>加载优先级</td>
<td>"high" | "low" | "auto"</td>
<td><span class="performance">⚡ 性能优化</span></td>
</tr>
<tr>
<td><span class="attr-tag">crossorigin</span></td>
<td>跨域设置</td>
<td>"anonymous" | "use-credentials"</td>
<td><span class="optional">按需</span></td>
</tr>
</tbody>
</table>
<!-- 演示 1:图片展示(含所有关键属性) -->
<div class="demo-section">
<h3>📸 完整属性图片展示</h3>
<div class="image-showcase">
<div class="image-card">
<img src="https://picsum.photos/400/300?random=1"
alt="宁静的山湖风景,远处有雪山倒映在清澈的湖水中"
title="点击查看大图"
loading="lazy"
decoding="async"
width="400"
height="300" />
<div class="image-card-info">
<h4>信息性图像 ✅</h4>
<p>提供有意义的 alt 描述,包含 width/height 防 CLS</p>
</div>
</div>
<div class="image-card">
<img src="https://picsum.photos/400/300?random=2"
alt=""
aria-hidden="true"
loading="lazy"
decoding="async"
width="400"
height="300" />
<div class="image-card-info">
<h4>装饰性图像 ✅</h4>
<p>使用空 alt + aria-hidden,屏幕阅读器跳过</p>
</div>
</div>
<div class="image-card">
<img src="https://picsum.photos/400/300?random=3"
alt="产品照片:红色运动鞋,侧面视角,白色背景"
loading="lazy"
fetchpriority="low"
width="400"
height="300" />
<div class="image-card-info">
<h4>电商产品图 ✅</h4>
<p>详细描述产品特征,低优先级加载</p>
</div>
</div>
<div class="image-card">
<img src="https://picsum.photos/400/300?random=4"
alt="2024年Q3销售数据柱状图,显示同比增长30%"
loading="lazy"
decoding="async"
width="400"
height="300" />
<div class="image-card-info">
<h4>数据图表 ✅</h4>
<p>简洁描述图表传达的核心信息</p>
</div>
</div>
</div>
</div>
<!-- 演示 2:alt 属性最佳实践 -->
<div class="demo-section">
<h3>📝 alt 属性编写指南</h3>
<div class="alt-demo">
<div class="alt-item good">
<div class="alt-preview">
<img src="https://picsum.photos/80/60?random=10" alt="">
</div>
<div class="alt-text">
<div class="alt-label good">✅ 信息性图像 — 好的写法</div>
<code class="alt-code">alt="红色苹果 iPhone 14 Pro,128GB 存储,正面视角"</code>
<p style="font-size: 12px; color: #666; margin-top: 6px;">
准确描述内容,包含关键视觉信息,便于 SEO 和无障碍访问
</p>
</div>
</div>
<div class="alt-item bad">
<div class="alt-preview" style="background: #fee;">❌</div>
<div class="alt-text">
<div class="alt-label bad">❌ 冗余描述 — 不推荐</div>
<code class="alt-code">alt="一张产品图片"</code>
<p style="font-size: 12px; color: #666; margin-top: 6px;">
过于笼统,没有传递任何有用信息,等同于没有 alt
</p>
</div>
</div>
<div class="alt-item bad">
<div class="alt-preview" style="background: #fee;">❌</div>
<div class="alt-text">
<div class="alt-label bad">❌ 使用文件名 — 不推荐</div>
<code class="alt-code">alt="img_001.jpg"</code>
<p style="font-size: 12px; color: #666; margin-top: 6px;">
文件名对用户毫无意义,且暴露内部文件结构
</p>
</div>
</div>
<div class="alt-item bad">
<div class="alt-preview" style="background: #fee;">❌</div>
<div class="alt-text">
<div class="alt-label bad">❌ 过于冗长 — 不推荐</div>
<code class="alt-code" style="font-size: 11px;">alt="这是一个显示2023年第一季度到第四季度销售额的柱状图..."</code>
<p style="font-size: 12px; color: #666; margin-top: 6px;">
太长!应提炼核心信息,详细内容放在周围文字或 figcaption 中
</p>
</div>
</div>
<div class="alt-item good">
<div class="alt-preview" style="background: #d4edda;">🎨</div>
<div class="alt-text">
<div class="alt-label good">✅ 装饰性图像 — 正确处理</div>
<code class="alt-code">alt="" aria-hidden="true"</code>
<p style="font-size: 12px; color: #666; margin-top: 6px;">
纯装饰性元素使用空 alt + aria-hidden,屏幕阅读器完全忽略
</p>
</div>
</div>
</div>
</div>
<!-- 演示 3:防止布局偏移 (CLS) -->
<div class="demo-section">
<h3>📐 防止累积布局偏移 (CLS)</h3>
<p style="color: #666; font-size: 14px; margin-bottom: 16px;">
设置 <strong>width 和 height</strong> 属性可以让浏览器预留空间,
避免图像加载完成后页面跳动,提升 Core Web Vitals 评分。
</p>
<div class="cls-demo">
<div id="clsPlaceholder" class="cls-placeholder">
⏳ 图像加载中...(此空间由 width/height 预留)
</div>
</div>
<p style="text-align: center; margin-top: 12px;">
<button onclick="simulateImageLoad()" style="padding: 8px 20px; background: #0066cc; color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 14px;">
🔄 模拟图像加载
</button>
</p>
</div>
</div>
<script>
/**
* 模拟图像加载过程,展示 width/height 如何防止布局偏移
*/
function simulateImageLoad() {
const placeholder = document.getElementById('clsPlaceholder')
// 重置状态
placeholder.className = 'cls-placeholder'
placeholder.innerHTML = '⏳ 图像加载中...(此空间由 width/height 预留)'
// 模拟网络延迟后加载完成
setTimeout(() => {
placeholder.innerHTML = `
<img src="https://picsum.photos/600/350?random=20"
alt="模拟加载完成的示例图像"
style="width:100%;height:auto;border-radius:6px;"
class="cls-loaded" />
`
placeholder.classList.add('cls-loaded')
}, 800)
}
</script>
</body>
</html>
<h4>033-image-hotspots.html</h4>
<!-- 来源:5-图像.md - 图像热区链接(usemap/map/area) -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>图像热区链接</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
line-height: 1.6;
color: #333;
background: #f5f7fa;
padding: 40px 20px;
}
.container {
max-width: 1000px;
margin: 0 auto;
}
h1 { text-align: center; color: #222; margin-bottom: 8px; font-size: 32px; }
.subtitle { text-align: center; color: #666; margin-bottom: 36px; font-size: 15px; }
/* 热区演示容器 */
.hotspot-demo {
background: white;
border-radius: 16px;
padding: 30px;
margin-bottom: 24px;
box-shadow: 0 2px 16px rgba(0,0,0,0.08);
}
.hotspot-demo h3 {
font-size: 18px;
color: #333;
margin-bottom: 16px;
display: flex;
align-items: center;
gap: 8px;
}
.image-map-container {
position: relative;
max-width: 700px;
margin: 0 auto;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 4px 20px rgba(0,0,0,0.12);
}
/* 背景图片 */
.map-image {
width: 100%;
height: auto;
display: block;
user-select: none;
}
/* 热区高亮覆盖层(用于可视化) */
.hotspot-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
}
.hotspot-area {
position: absolute;
border: 2px solid rgba(255,255,255,0.9);
background: rgba(0,102,204,0.15);
cursor: pointer;
transition: all 0.3s ease;
pointer-events: auto;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 13px;
font-weight: 600;
text-shadow: 0 1px 3px rgba(0,0,0,0.5);
}
.hotspot-area:hover {
background: rgba(0,102,204,0.35);
border-color: #0066cc;
transform: scale(1.02);
z-index: 10;
}
.hotspot-rect { border-radius: 4px; }
.hotspot-circle { border-radius: 50%; }
/* 热区信息面板 */
.hotspot-info {
background: linear-gradient(135deg, #e3f2fd 0%, #bbdefb 100%);
padding: 16px 20px;
border-radius: 10px;
margin-top: 20px;
min-height: 60px;
display: flex;
align-items: center;
gap: 12px;
transition: all 0.3s;
}
.info-icon {
width: 40px;
height: 40px;
background: #0066cc;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
flex-shrink: 0;
}
.info-text h4 {
color: #0066cc;
font-size: 16px;
margin-bottom: 2px;
}
.info-text p {
color: #555;
font-size: 13px;
}
/* 形状说明表格 */
.shape-table {
width: 100%;
border-collapse: collapse;
background: white;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 2px 12px rgba(0,0,0,0.06);
margin-top: 24px;
}
.shape-table th,
.shape-table td {
padding: 14px 18px;
text-align: left;
border-bottom: 1px solid #eee;
font-size: 14px;
}
.shape-table thead {
background: linear-gradient(135deg, #28a745, #20883d);
color: white;
}
.shape-table th {
font-weight: 600;
font-size: 13px;
text-transform: uppercase;
}
.code-inline {
font-family: 'Monaco', monospace;
background: #f0f0f0;
padding: 2px 8px;
border-radius: 4px;
font-size: 12px;
}
/* 代码展示 */
.code-block {
background: #1e1e1e;
color: #d4d4d4;
padding: 16px 20px;
border-radius: 8px;
font-family: 'Monaco', 'Menlo', monospace;
font-size: 12px;
line-height: 1.7;
overflow-x: auto;
margin-top: 16px;
}
.tag { color: #569cd6; }
.attr { color: #9cdcfe; }
.value { color: #ce9178; }
.comment { color: #6a9955; }
/* 最佳实践提示 */
.best-practices {
background: #fff3cd;
border-left: 4px solid #ffc107;
padding: 18px 22px;
border-radius: 8px;
margin-top: 24px;
}
.best-practices h4 {
color: #856404;
margin-bottom: 10px;
font-size: 15px;
}
.best-practices ul {
list-style: none;
font-size: 13px;
color: #856404;
}
.best-practices li {
padding: 4px 0;
padding-left: 20px;
position: relative;
}
.best-practices li::before {
content: "✓";
position: absolute;
left: 0;
font-weight: bold;
}
</style>
</head>
<body>
<div class="container">
<h1>🗺️ 图像热区链接</h1>
<p class="subtitle">usemap + map + area:将一张图划分为多个可点击区域</p>
<!-- 热区演示 -->
<div class="hotspot-demo">
<h3>🖱️ 交互式热区地图演示</h3>
<p style="color:#666;font-size:14px;margin-bottom:16px;">
将鼠标悬停在下方图片的不同区域上,查看热区高亮效果和提示信息。
点击区域可跳转到对应链接。
</p>
<div class="image-map-container">
<!-- 实际的热区映射图片 -->
<!-- 使用 picsum 占位图,通过 CSS overlay 模拟热区效果 -->
<img
src="https://picsum.photos/700/400?random=map"
usemap="#productMap"
alt="产品分类导航热区地图"
class="map-image"
id="mapImage" />
<!-- 可视化热区覆盖层(纯演示用) -->
<div class="hotspot-overlay">
<!-- 区域 1:电脑数码 (矩形) -->
<div
class="hotspot-area hotspot-rect"
style="top: 10%; left: 5%; width: 28%; height: 38%;"
data-title="电脑数码"
data-desc="笔记本电脑、台式机、显示器等电子设备"
onmouseenter="showHotspotInfo(this)"
onmouseleave="hideHotspotInfo()"
onclick="alert('导航到:电脑数码分类')">
💻 电脑
</div>
<!-- 区域 2:手机通讯 (矩形) -->
<div
class="hotspot-area hotspot-rect"
style="top: 10%; left: 37%; width: 26%; height: 38%;"
data-title="手机通讯"
data-desc="智能手机、平板电脑、智能手表等移动设备"
onmouseenter="showHotspotInfo(this)"
onmouseleave="hideHotspotInfo()"
onclick="alert('导航到:手机通讯分类')">
📱 手机
</div>
<!-- 区域 3:家用电器 (矩形) -->
<div
class="hotspot-area hotspot-rect"
style="top: 10%; left: 67%; width: 28%; height: 38%;"
data-title="家用电器"
data-desc="空调、冰箱、洗衣机、厨房电器等"
onmouseenter="showHotspotInfo(this)"
onmouseleave="hideHotspotInfo()"
onclick="alert('导航到:家用电器分类')">
🏠 家电
</div>
<!-- 区域 4:生鲜食品 (大矩形) -->
<div
class="hotspot-area hotspot-rect"
style="top: 55%; left: 5%; width: 42%; height: 38%;"
data-title="生鲜食品"
data-desc="新鲜水果、蔬菜、肉类、海鲜水产"
onmouseenter="showHotspotInfo(this)"
onmouseleave="hideHotspotInfo()"
onclick="alert('导航到:生鲜食品分类')">
🥬 生鲜
</div>
<!-- 区域 5:日用百货 (圆形) -->
<div
class="hotspot-area hotspot-circle"
style="top: 58%; left: 55%; width: 18%; height: 35%;"
data-title="日用百货"
data-desc="清洁用品、纸品、个人护理、家居用品"
onmouseenter="showHotspotInfo(this)"
onmouseleave="hideHotspotInfo()"
onclick="alert('导航到:日用百货分类')">
🧴 日用
</div>
<!-- 区域 6:服装鞋包 (矩形) -->
<div
class="hotspot-area hotspot-rect"
style="top: 55%; left: 77%; width: 18%; height: 38%;"
data-title="服装鞋包"
data-desc="男装、女装、童装、运动鞋、箱包配饰"
onmouseenter="showHotspotInfo(this)"
onmouseleave="hideHotspotInfo()"
onclick="alert('导航到:服装鞋包分类')">
👕 服饰
</div>
</div>
</div>
<!-- 信息展示面板 -->
<div class="hotspot-info" id="hotspotInfo">
<div class="info-icon">📍</div>
<div class="info-text">
<h4>将鼠标移至上方区域</h4>
<p>悬停在图片的不同区域上查看详细信息</p>
</div>
</div>
<!-- 原生 HTML usemap 代码 -->
<div class="code-block">
<span class="comment"><!-- 使用原生 HTML usemap 属性实现热区 --></span>
<span class="tag"><img</span> <span class="attr">src</span>=<span class="value">"products.jpg"</span>
<span class="attr">usemap</span>=<span class="value">"#productMap"</span>
<span class="attr">alt</span>=<span class="value">"产品分类导航地图"</span> />
<span class="tag"><map</span> <span class="attr">name</span>=<span class="value">"productMap"</span>><br>
<span class="comment"><!-- shape=rect: 矩形区域,coords=左,上,右,下 --></span>
<span class="tag"><area</span> <span class="attr">shape</span>=<span class="value">"rect"</span>
<span class="attr">coords</span>=<span class="value">"45,126,143,203"</span>
<span class="attr">href</span>=<span class="value">"/category/computers"</span>
<span class="attr">alt</span>=<span class="value">"电脑数码"</span>
<span class="attr">title</span>=<span class="value">"点击浏览电脑产品"</span> /><br><br>
<span class="comment"><!-- shape=circle: 圆形区域,coords=圆心x,圆心y,半径 --></span>
<span class="tag"><area</span> <span class="attr">shape</span>=<span class="value">"circle"</span>
<span class="attr">coords</span>=<span class="value">"300,280,80"</span>
<span class="attr">href</span>=<span class="value">"/category/daily"</span>
<span class="attr">alt</span>=<span class="value">"日用百货"</span> /><br><br>
<span class="comment"><!-- shape=poly: 多边形区域,coords=各点x,y坐标 --></span>
<span class="tag"><area</span> <span class="attr">shape</span>=<span class="value">"poly"</span>
<span class="attr">coords</span>=<span class="value">"500,250,600,200,650,300,550,350"</span>
<span class="attr">href</span>=<span class="value">"/category/special"</span>
<span class="attr">alt</span>=<span class="value">"特惠专区"</span> />
<span class="tag"></map></span>
</div>
</div>
<!-- 三种形状说明表 -->
<table class="shape-table">
<thead>
<tr>
<th>形状 (shape)</th>
<th>坐标格式 (coords)</th>
<th>说明</th>
<th>适用场景</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>rect</strong></td>
<td><code class="code-inline">left,top,right,bottom</code></td>
<td>矩形两个对角点的坐标</td>
<td>规则区域(如按钮、卡片)</td>
</tr>
<tr>
<td><strong>circle</strong></td>
<td><code class="code-inline">center-x,center-y,radius</code></td>
<td>圆心坐标 + 半径长度</td>
<td>圆形图标、头像区域</td>
</tr>
<tr>
<td><strong>poly</strong></td>
<td><code class="code-inline">x1,y1,x2,y2,x3,y3...</code></td>
<td>多边形各顶点坐标(顺序连接)</td>
<td>不规则形状、复杂轮廓</td>
</tr>
</tbody>
</table>
<!-- 最佳实践 -->
<div class="best-practices">
<h4>⚠️ 可访问性与最佳实践</h4>
<ul>
<li>每个 <area> 都<strong>必须提供 alt 属性</strong>,描述该区域的用途或目标</li>
<li>使用 <strong>title 属性</strong>提供额外的鼠标悬停提示信息</li>
<li>确保每个热区的<strong>点击面积足够大</strong>(移动端至少 44×44px)</li>
<li>考虑使用 CSS 或 SVG 替代方案以获得更好的响应式支持</li>
<li>热区坐标需要根据实际图片尺寸精确计算,可使用在线工具辅助定位</li>
</ul>
</div>
</div>
<script>
/**
* 热区信息显示控制
*/
const infoPanel = document.getElementById('hotspotInfo')
function showHotspotInfo(element) {
const title = element.dataset.title || '未命名区域'
const desc = element.dataset.desc || '暂无描述'
infoPanel.innerHTML = `
<div class="info-icon" style="background: ${getRandomColor()}">${getEmoji(title)}</div>
<div class="info-text">
<h4>${title}</h4>
<p>${desc}</p>
</div>
`
infoPanel.style.background = `linear-gradient(135deg, ${getLightColor()}, ${getLightColor(true)})`
infoPanel.querySelector('.info-icon').style.background = getRandomColor()
}
function hideHotspotInfo() {
infoPanel.innerHTML = `
<div class="info-icon">📍</div>
<div class="info-text">
<h4>将鼠标移至上方区域</h4>
<p>悬停在图片的不同区域上查看详细信息</p>
</div>
`
infoPanel.style.background = ''
}
// 辅助函数
function getEmoji(title) {
const map = {
'电脑数码': '💻', '手机通讯': '📱', '家用电器': '🏠',
'生鲜食品': '🥬', '日用百货': '🧴', '服装鞋包': '👕'
}
return map[title] || '📍'
}
function getRandomColor() {
const colors = ['#0066cc', '#28a745', '#fd7e14', '#6f42c1', '#dc3545']
return colors[Math.floor(Math.random() * colors.length)]
}
function getLightColor(dark = false) {
if (dark) return '#c3e6cb'
const colors = ['#e3f2fd', '#e8f5e9', '#fff3e0', '#f3e5f5', '#fce4ec']
return colors[Math.floor(Math.random() * colors.length)]
}
</script>
</body>
</html>
<h4>035-image-full-attributes.html</h4>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>【35】img 标签完整属性演示</title>
<!--
来源: HTML5基础知识/5-图像.md
知识点: img 标签完整属性 src/alt/title/loading/decoding/fetchpriority/crossorigin/referrerpolicy
-->
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
padding: 20px; background: #f0f4f8; color: #333;
min-height: 100vh;
}
.container { max-width: 1100px; margin: 0 auto; }
.header {
text-align: center; padding: 24px; background: linear-gradient(135deg, #667eea, #764ba2);
color: white; border-radius: 12px; margin-bottom: 24px;
}
.header h1 { font-size: 22px; margin-bottom: 6px; }
.header p { opacity: 0.9; font-size: 14px; }
.card {
background: white; border-radius: 10px; padding: 24px;
margin-bottom: 20px; box-shadow: 0 2px 10px rgba(0,0,0,0.06);
}
.card-title {
font-size: 16px; font-weight: 600; color: #333;
border-left: 4px solid #667eea; padding-left: 12px; margin-bottom: 16px;
}
/* 属性表格 */
.attr-table { width: 100%; border-collapse: collapse; font-size: 13px; margin-bottom: 20px; }
.attr-table th, .attr-table td {
padding: 10px 14px; text-align: left; border-bottom: 1px solid #eee;
}
.attr-table th { background: #f8f9fa; font-weight: 600; color: #555; width: 140px; }
.attr-table code {
background: #f1f3f5; padding: 2px 6px; border-radius: 4px;
font-size: 12px; color: #e64980;
}
.attr-value { color: #22863a; font-family: monospace; font-size: 12px; }
/* 图片展示区 */
.image-showcase {
display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 20px; margin-top: 16px;
}
.img-demo-card {
border: 2px solid #e9ecef; border-radius: 10px; overflow: hidden;
transition: border-color 0.2s;
}
.img-demo-card:hover { border-color: #667eea; }
.img-demo-header {
background: #f8f9fa; padding: 10px 14px; font-size: 12px; font-weight: 600;
color: #555; display: flex; justify-content: space-between; align-items: center;
}
.img-demo-body { padding: 16px; text-align: center; background: #fafbfc; min-height: 180px; display: flex; align-items: center; justify-content: center; }
.img-demo-body img {
max-width: 100%; max-height: 200px; border-radius: 6px;
object-fit: contain;
}
.placeholder-img {
width: 240px; height: 160px; background: linear-gradient(135deg, #e0e5ec, #d0d5dc);
border-radius: 8px; display: flex; flex-direction: column;
align-items: center; justify-content: center; color: #8898aa;
}
.placeholder-img svg { width: 40px; height: 40px; margin-bottom: 8px; opacity: 0.4; }
.placeholder-img span { font-size: 11px; }
.attr-tags { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 8px; }
.attr-tag {
padding: 2px 8px; border-radius: 10px; font-size: 10px; font-weight: 600;
background: #e8f0fe; color: #1967d2;
}
/* 交互面板 */
.interactive-panel {
background: #f8f9fa; border-radius: 8px; padding: 16px; margin-top: 16px;
}
.control-row { display: flex; gap: 10px; flex-wrap: wrap; align-items: center; margin-bottom: 12px; }
label { font-size: 13px; font-weight: 500; color: #555; }
select, input[type="text"] {
padding: 7px 12px; border: 1px solid #ddd; border-radius: 6px;
font-size: 13px; background: white;
}
select:focus, input:focus { outline: none; border-color: #667eea; }
.btn {
padding: 8px 18px; border: none; border-radius: 6px; cursor: pointer;
font-size: 13px; font-weight: 500; transition: all 0.2s;
}
.btn-primary { background: #667eea; color: white; }
.btn-primary:hover { background: #5a67d8; }
.code-preview {
background: #1e1e1e; border-radius: 8px; padding: 14px;
font-family: monospace; font-size: 12px; color: #d4d4d4;
overflow-x: auto; line-height: 1.6; margin-top: 12px;
}
.code-preview .tag { color: #569cd6; }
.code-preview .attr { color: #9cdcfe; }
.code-preview .val { color: #ce9178; }
.code-preview .comment { color: #6a9955; }
.info-box {
background: #e8f4fd; border-left: 4px solid #007bff;
padding: 12px 16px; border-radius: 4px; font-size: 13px;
line-height: 1.6; color: #1565c0; margin-bottom: 16px;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🖼️ img 标签完整属性演示</h1>
<p>src / alt / title / loading / decoding / fetchpriority / crossorigin / referrerpolicy 全部属性效果</p>
</div>
<!-- 属性速查表 -->
<div class="card">
<div class="card-title">📋 属性速查表</div>
<table class="attr-table">
<thead>
<tr><th>属性</th><th>说明</th><th>常用值</th></tr>
</thead>
<tbody>
<tr><td><code>src</code></td><td>图像源地址(必填)</td><td class="attr-value">URL 路径</td></tr>
<tr><td><code>alt</code></td><td>替代文本(必填,可访问性)</td><td class="attr-value">描述性文字</td></tr>
<tr><td><code>title</code></td><td>鼠标悬停提示文字</td><td class="attr-value">提示文字</td></tr>
<tr><td><code>width/height</code></td><td>尺寸(防止 CLS 布局偏移)</td><td class="attr-value">像素值</td></tr>
<tr><td><code>loading</code></td><td>加载策略</td><td class="attr-value">lazy | eager</td></tr>
<tr><td><code>decoding</code></td><td>解码方式</td><td class="attr-value">async | sync | auto</td></tr>
<tr><td><code>fetchpriority</code></td><td>获取优先级</td><td class="attr-value">high | low | auto</td></tr>
<tr><td><code>crossorigin</code></td><td>CORS 设置</td><td class="attr-value">anonymous | use-credentials</td></tr>
<tr><td><code>referrerpolicy</code></td><td>引荐来源策略</td><td class="attr-value">no-referrer | origin | strict-origin...</td></tr>
</tbody>
</table>
<div class="info-box">
💡 <strong>核心要点:</strong>始终设置 <code>alt</code> 属性以支持可访问性和 SEO;设置 <code>width/height</code>
可预留空间减少布局偏移(CLS);<code>loading="lazy"</code> 实现原生懒加载。
</div>
</div>
<!-- 属性演示 -->
<div class="card">
<div class="card-title">🎨 属性效果展示</div>
<div class="image-showcase">
<!-- 基础属性 -->
<div class="img-demo-card">
<div class="img-demo-header">
<span>基础属性 (src + alt + title)</span>
</div>
<div class="img-demo-body">
<div class="placeholder-img" id="demoBasic">
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z"/></svg>
<span>悬停查看 title 效果</span>
</div>
</div>
<div style="padding:10px 14px;">
<div class="attr-tags">
<span class="attr-tag">src</span>
<span class="attr-tag">alt</span>
<span class="attr-tag">title</span>
<span class="attr-tag">width</span>
<span class="attr-tag">height</span>
</div>
</div>
</div>
<!-- loading=lazy -->
<div class="img-demo-card">
<div class="img-demo-header">
<span>懒加载 loading="lazy"</span>
</div>
<div class="img-demo-body">
<div class="placeholder-img">
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H5V5h14v14z"/></svg>
<span>滚动到可视区域时加载</span>
</div>
</div>
<div style="padding:10px 14px;">
<div class="attr-tags">
<span class="attr-tag">loading="lazy"</span>
</div>
</div>
</div>
<!-- decoding -->
<div class="img-demo-card">
<div class="img-demo-header">
<span>解码方式 decoding</span>
</div>
<div class="img-demo-body">
<div class="placeholder-img">
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M21 3H3c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h18c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H3V5h18v14z"/></svg>
<span>async/sync/auto 解码策略</span>
</div>
</div>
<div style="padding:10px 14px;">
<div class="attr-tags">
<span class="attr-tag">decoding="async"</span>
<span class="attr-tag">decoding="sync"</span>
<span class="attr-tag">decoding="auto"</span>
</div>
</div>
</div>
<!-- fetchpriority -->
<div class="img-demo-card">
<div class="img-demo-header">
<span>获取优先级 fetchpriority</span>
</div>
<div class="img-demo-body">
<div class="placeholder-img">
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2L4.5 20.29l.71.71L12 18l6.79 3 .71-.71z"/></svg>
<span>控制资源加载优先级</span>
</div>
</div>
<div style="padding:10px 14px;">
<div class="attr-tags">
<span class="attr-tag">fetchpriority="high"</span>
<span class="attr-tag">fetchpriority="low"</span>
</div>
</div>
</div>
<!-- crossorigin -->
<div class="img-demo-card">
<div class="img-demo-header">
<span>跨域设置 crossorigin</span>
</div>
<div class="img-demo-body">
<div class="placeholder-img">
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39z"/></svg>
<span>Canvas/CSS 访问跨域图片</span>
</div>
</div>
<div style="padding:10px 14px;">
<div class="attr-tags">
<span class="attr-tag">crossorigin="anonymous"</span>
<span class="attr-tag">crossorigin="use-credentials"</span>
</div>
</div>
</div>
<!-- referrerpolicy -->
<div class="img-demo-card">
<div class="img-demo-header">
<span>引荐来源策略 referrerpolicy</span>
</div>
<div class="img-demo-body">
<div class="placeholder-img">
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 1L3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-4zm0 10.99h7c-.53 4.12-3.28 7.79-7 8.94V12H5V6.3l7-3.11v8.8z"/></svg>
<span>控制 Referer 头发送策略</span>
</div>
</div>
<div style="padding:10px 14px;">
<div class="attr-tags">
<span class="attr-tag">referrerpolicy="no-referrer"</span>
<span class="attr-tag">referrerpolicy="origin"</span>
</div>
</div>
</div>
</div>
</div>
<!-- 交互式代码生成器 -->
<div class="card">
<div class="card-title">🛠️ 交互式 img 标签生成器</div>
<div class="interactive-panel">
<div class="control-row">
<label>图片 URL:</label>
<input type="text" id="genSrc" value="https://picsum.photos/400/300" style="flex:1;min-width:250px;" />
</div>
<div class="control-row">
<label>alt 文本:</label>
<input type="text" id="genAlt" value="示例图片" />
<label>title:</label>
<input type="text" id="genTitle" value="这是一张示例图片" />
</div>
<div class="control-row">
<label>loading:</label>
<select id="genLoading">
<option value="">(默认 eager)</option>
<option value='loading="lazy"' selected>lazy</option>
<option value='loading="eager"'>eager</option>
</select>
<label>decoding:</label>
<select id="genDecoding">
<option value="">(默认 auto)</option>
<option value='decoding="async"' selected>async</option>
<option value='decoding="sync"'>sync</option>
</select>
<label>优先级:</label>
<select id="genPriority">
<option value="">(默认 auto)</option>
<option value='fetchpriority="high"'>high</option>
<option value='fetchpriority="low"'>low</option>
</select>
</div>
<button class="btn btn-primary" onclick="generateCode()">📝 生成代码</button>
<div class="code-preview" id="codeOutput">
<span class="comment"><!-- 点击 "生成代码" 按钮 --></span>
</div>
</div>
</div>
</div>
<script>
function generateCode() {
const src = document.getElementById('genSrc').value || 'image.jpg';
const alt = document.getElementById('genAlt').value || '';
const title = document.getElementById('genTitle').value || '';
const loading = document.getElementById('genLoading').value;
const decoding = document.getElementById('genDecoding').value;
const priority = document.getElementById('genPriority').value;
let attrs = [`src="${src}"`];
if (alt) attrs.push(`alt="${alt}"`);
if (title) attrs.push(`title="${title}"`);
if (loading) attrs.push(loading);
if (decoding) attrs.push(decoding);
if (priority) attrs.push(priority);
// 默认加上宽高
attrs.push('width="400"');
attrs.push('height="300"');
const html = `<span class="tag"><img</span> ${attrs.join('\n ')} <span class="tag">/></span>`;
document.getElementById('codeOutput').innerHTML = html;
// 同时更新基础 demo
const basicEl = document.getElementById('demoBasic');
basicEl.innerHTML = `<img src="${src}" alt="${alt}" title="${title}"
onerror="this.style.display='none';this.nextElementSibling.style.display='flex';"
onload="this.style.display='block';this.nextElementSibling.style.display='none';"
style="max-width:100%;max-height:200px;border-radius:6px;display:none;" />
<div class="placeholder-img" style="display:flex;"><svg viewBox="0 0 24 24" fill="currentColor" style="width:40px;height:40px;margin-bottom:8px;opacity:0.4;"><path d="M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z"/></svg><span>图片加载中或失败</span></div>`;
}
// 初始生成一次
generateCode();
</script>
</body>
</html>
<h4>039-image-error-handling.html</h4>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>【39】图片错误处理与降级方案</title>
<!--
来源: HTML5基础知识/5-图像.md
知识点: onerror 降级方案、placeholder 占位图、多级容错
-->
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
padding: 20px; background: #fafaf9; color: #333;
}
.container { max-width: 1050px; margin: 0 auto; }
.header {
text-align: center; padding: 24px; background: linear-gradient(135deg, #dc2626, #ea580c);
color: white; border-radius: 12px; margin-bottom: 24px;
}
.header h1 { font-size: 22px; margin-bottom: 6px; }
.header p { opacity: 0.9; font-size: 14px; }
.card {
background: white; border-radius: 10px; padding: 24px;
margin-bottom: 20px; box-shadow: 0 2px 10px rgba(0,0,0,0.05);
}
.card-title {
font-size: 16px; font-weight: 600; color: #333;
border-left: 4px solid #dc2626; padding-left: 12px; margin-bottom: 16px;
}
.info-box {
background: #fef2f2; border-left: 4px solid #dc2626;
padding: 12px 16px; border-radius: 4px; font-size: 13px;
line-height: 1.6; color: #991b1b; margin-bottom: 16px;
}
/* 图片展示 */
.demo-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 18px; }
.demo-card {
border: 2px solid #e5e7eb; border-radius: 10px; overflow: hidden;
transition: all 0.3s;
}
.demo-card:hover { box-shadow: 0 4px 16px rgba(0,0,0,0.08); }
.demo-header {
padding: 10px 14px; font-size: 12px; font-weight: 700;
background: #f9fafb; color: #374151;
}
.demo-body {
height: 170px; display: flex; align-items: center; justify-content: center;
background: #f3f4f6; position: relative;
}
.demo-body img {
max-width: 100%; max-height: 160px; object-fit: contain; border-radius: 6px;
}
/* 占位符样式 */
.placeholder {
display: flex; flex-direction: column; align-items: center; justify-content: center;
color: #9ca3af; gap: 6px;
}
.placeholder svg { width: 36px; height: 36px; opacity: 0.35; }
.placeholder span { font-size: 11px; text-align: center; }
.placeholder-skeleton {
width: 180px; height: 120px; background: linear-gradient(90deg,
#e5e7eb 25%, #f3f4f6 50%, #e5e7eb 75%);
background-size: 200% 100%;
animation: skeleton-loading 1.5s infinite;
border-radius: 6px;
}
@keyframes skeleton-loading {
0%{background-position:200% 0}100%{background-position:-200% 0}
}
.placeholder-broken {
display: flex; flex-direction: column; align-items: center; gap: 4px;
color: #d1d5db;
}
.placeholder-broken .icon { font-size: 32px; }
.placeholder-broken .text { font-size: 11px; }
.placeholder-color {
width: 180px; height: 120px; border-radius: 6px;
display: flex; align-items: center; justify-content: center;
font-size: 36px;
}
.demo-footer {
padding: 8px 14px; font-size: 10px; color: #9ca3af;
display: flex; justify-content: space-between; background: #fafafa;
}
.status-tag {
padding: 2px 8px; border-radius: 8px; font-size: 10px; font-weight: 600;
}
.status-ok { background: #d1fae5; color: #065f46; }
.status-fail { background: #fee2e2; color: #991b1b; }
.status-pending { background: #fef3c7; color: #92400e; }
/* 控制区 */
.controls { display: flex; gap: 10px; flex-wrap: wrap; margin-bottom: 20px; }
.btn {
padding: 9px 18px; border: none; border-radius: 6px; cursor: pointer;
font-size: 13px; font-weight: 500; transition: all 0.2s;
}
.btn-red { background: #dc2626; color: white; }
.btn-red:hover { background: #b91c1c; }
.btn-gray { background: #6b7280; color: white; }
.btn-gray:hover { background: #4b5563; }
/* 代码展示 */
.code-block {
background: #1e1e1e; border-radius: 8px; padding: 14px;
font-family: monospace; font-size: 12px; color: #d4d4d4;
overflow-x: auto; line-height: 1.6; margin-top: 12px;
}
.tag { color: #569cd6; }
.attr { color: #9cdcfe; }
.val { color: #ce9178; }
.cm { color: #6a9955; }
.compat-note {
background: #eff6ff; border: 1px solid #bfdbfe; border-radius: 6px;
padding: 10px 14px; font-size: 12px; color: #1e40af; margin-top: 16px;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🛡️ 图片错误处理与降级方案</h1>
<p>onerror 降级 / placeholder 占位图 / 多级容错策略</p>
</div>
<div class="card">
<div class="card-title">📸 降级策略演示</div>
<div class="info-box">
⚠️ <strong>常见场景:</strong>图片 URL 失效、网络中断、CORS 阻止、404 错误等。<br>
好的错误处理应该:<strong>① 显示友好的占位图</strong> → <strong>② 尝试备用 URL</strong> → <strong>③ 提供重试机制</strong>
</div>
<div class="controls">
<button class="btn btn-red" onclick="triggerErrors()">💥 触发错误 (模拟)</button>
<button class="btn btn-gray" onclick="resetAll()">🔄 重置全部</button>
</div>
<div class="demo-grid" id="demoGrid"></div>
</div>
<!-- 代码示例 -->
<div class="card">
<div class="card-title">📝 核心代码模式</div>
<div class="code-block">
<span class="cm"><!-- 方式一:基础 onerror 替换 --></span>
<span class="tag"><img</span> <span class="attr">src</span>=<span class="val">"可能失败的URL"</span>
<span class="attr">alt</span>=<span class="val">"描述"</span>
<span class="attr">onerror</span>=<span class="val">"this.src='fallback.jpg'"</span>
<span class="tag">/></span>
<span class="cm"><!-- 方式二:隐藏图片 + 显示占位 --></span>
<span class="tag"><img</span> <span class="attr">src</span>=<span class="val">"url"</span>
<span class="attr">onerror</span>=<span class="val">"this.style.display='none';document.getElementById('placeholder').style.display='flex';"</span>
<span class="tag">/></span>
<span class="tag"><div</span> <span class="attr">id</span>=<span class="val">"placeholder"</span> <span class="attr">style</span>=<span class="val">"display:none;"</span><span class="tag">></span>占位内容<span class="tag"></div></span>
<span class="cm"><!-- 方式三:多级降级 --></span>
<span class="tag"><img</span> <span class="attr">src</span>=<span class="val">"primary.jpg"</span>
<span class="attr">onerror</span>=<span class="val">"
this.onerror=null; // 防止无限循环
this.src='backup-cdn.jpg';
"</span>
<span class="tag">/></span>
</div>
<div class="compat-note">
ℹ️ <strong>注意:</strong><code>this.onerror=null</code> 是防止备用图片也失败时陷入无限循环的关键技巧。
<code>onerror</code> 在所有浏览器中均完全支持。
</div>
</div>
</div>
<script>
const demos = [
{
title: '❌ 无错误处理',
desc: '直接显示 alt 文本或破碎图标',
url: 'https://invalid-url-that-does-not-exist.example.com/broken.jpg',
strategy: 'none'
},
{
title: '🖼️ onerror 替换为占位图',
desc: '失败时切换到本地占位图',
url: 'https://invalid-url-2.example.com/nope.jpg',
strategy: 'fallback-img'
},
{
title: '🔲 骨架屏占位',
desc: '失败时显示骨架动画',
url: 'https://broken-3.example.com/image.png',
strategy: 'skeleton'
},
{
title: '🎨 彩色占位块',
desc: '根据图片名生成颜色块',
url: 'https://not-found-4.example.com/photo.webp',
strategy: 'color-block'
},
{
title: '🔄 多级降级 (2级)',
desc: '主→备→占位 三级容错',
url: 'https://nonexistent-5.example.com/pic.gif',
strategy: 'multi-level'
},
{
title: '✅ 正常图片 (对比)',
desc: '正常加载的图片作为参照',
url: 'https://picsum.photos/seed/normal-demo/240/160',
strategy: 'normal'
},
{
title: '📦 SVG Data URI 占位',
desc: '内联 SVG 作为最终兜底',
url: 'https://gone-6.example.com/img.tiff',
strategy: 'svg-placeholder'
},
{
title: '♻️ 自动重试机制',
desc: '失败后延迟自动重试 N 次',
url: 'https://timeout-7.example.com/large.jpg',
strategy: 'retry'
},
];
const demoGrid = document.getElementById('demoGrid');
function renderDemos() {
demoGrid.innerHTML = '';
demos.forEach((demo, i) => {
const card = document.createElement('div');
card.className = 'demo-card';
card.id = `card-${i}`;
const placeholderId = `ph-${i}`;
card.innerHTML = `
<div class="demo-header">${demo.title}</div>
<div class="demo-body" id="body-${i}">
<img src="${demo.url}" alt="${demo.desc}"
id="img-${i}"
loading="lazy"
onload="onImgLoad(${i})"
onerror="handleError(${i}, '${demo.strategy}')"
style="display:none;" />
<div class="placeholder" id="${placeholderId}">
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z"/></svg>
<span>加载中...</span>
</div>
</div>
<div class="demo-footer">
<span>${demo.desc}</span>
<span class="status-tag status-pending" id="status-${i}">等待</span>
</div>
`;
demoGrid.appendChild(card);
});
}
function onImgLoad(i) {
const img = document.getElementById(`img-${i}`);
const ph = document.getElementById(`ph-${i}`);
const status = document.getElementById(`status-${i}`);
img.style.display = 'block';
ph.style.display = 'none';
status.textContent = '成功 ✓';
status.className = 'status-tag status-ok';
}
function handleError(i, strategy) {
const img = document.getElementById(`img-${i}`);
const ph = document.getElementById(`ph-${i}`);
const status = document.getElementById(`status-${i}`);
const body = document.getElementById(`body-${i}`);
status.textContent = '失败 ✗';
status.className = 'status-tag status-fail';
switch (strategy) {
case 'none':
// 无处理,浏览器默认行为
ph.innerHTML = `<svg viewBox="0 0 24 24" fill="#ccc" style="width:48px;height:48px;"><path d="M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zm-2-6l-3-3-3 3-3-3-3 3V7h12z"/></svg><span style="font-size:11px;">浏览器默认破损图标</span>`;
break;
case 'fallback-img':
img.onerror = null;
img.src = `https://picsum.photos/seed/fallback-${i}/240/160`;
break;
case 'skeleton':
img.style.display = 'none';
ph.style.display = 'flex';
ph.innerHTML = '<div class="placeholder-skeleton"></div>';
ph.className = 'placeholder';
break;
case 'color-block':
img.style.display = 'none';
ph.style.display = 'flex';
ph.className = 'placeholder';
const hue = (i * 47) % 360;
ph.innerHTML = `<div class="placeholder-color" style="background:hsl(${hue},60%,85%);color:hsl(${hue},60%,45%);">🖼</div>`;
break;
case 'multi-level':
img.onerror = null;
// 第一级降级
img.src = `https://picsum.photos/seed/backup-l1-${i}/240/160`;
// 如果第一级也失败
img.onerror = function() {
this.onerror = null;
this.style.display = 'none';
ph.style.display = 'flex';
ph.innerHTML = `<div class="placeholder-broken"><span class="icon">⚠️</span><span class="text">所有来源均失败<br/>显示最终占位</span></div>`;
};
break;
case 'normal':
// 正常图片不应该出错,但以防万一
ph.innerHTML = '<span style="color:#22c55e;">✅ 正常加载</span>';
break;
case 'svg-placeholder':
img.style.display = 'none';
ph.style.display = 'flex';
ph.innerHTML = `
<div style="text-align:center;">
<svg width="80" height="80" viewBox="0 0 24 24" fill="none" stroke="#9ca3af" stroke-width="1.5">
<rect x="3" y="3" width="18" height="18" rx="2"/>
<circle cx="8.5" cy="8.5" r="1.5"/><path d="M21 15l-5-5L5 21"/>
<line x1="4" y1="4" x2="20" y2="20" stroke-width="2" stroke="#ef4444"/>
</svg>
<span style="font-size:11px;color:#9ca3af;">SVG 占位图</span>
</div>`;
break;
case 'retry':
handleRetry(i, img, ph);
break;
}
}
// 重试机制
function handleRetry(i, img, ph) {
let retries = 0;
const maxRetries = 3;
function attempt() {
retries++;
ph.innerHTML = `<div class="placeholder"><span style="font-size:11px;">重试中... (${retries}/${maxRetries})</span></div>`;
ph.style.display = 'flex';
setTimeout(() => {
const retryUrl = `https://picsum.photos/seed/retry-${i}-${retries}-${Date.now()}/240/160`;
const testImg = new Image();
testImg.onload = function() {
img.src = retryUrl;
img.style.display = 'block';
ph.style.display = 'none';
document.getElementById(`status-${i}`).textContent = `重试${retries}次成功 ✓`;
document.getElementById(`status-${i}`).className = 'status-tag status-ok';
};
testImg.onerror = function() {
if (retries < maxRetries) {
attempt();
} else {
ph.innerHTML = `<div class="placeholder-broken"><span class="icon">😵</span><span class="text">${maxRetries} 次重试均失败</span></div>`;
}
};
testImg.src = retryUrl;
}, 800 * retries);
}
attempt();
}
function triggerErrors() {
// 强制触发所有图片的 error(通过替换 src 为无效 URL)
demos.forEach((_, i) => {
const img = document.getElementById(`img-${i}`);
if (img && demos[i].strategy !== 'normal') {
img.src = `about:blank?error=${Date.now()}`;
}
});
}
function resetAll() {
renderDemos();
}
// 初始化
renderDemos();
</script>
</body>
</html>