实现的功能
- 苹果手机框出现
- 手机可随意移动
- 灵动岛模仿
- 图片自动切换
- 拖动图片可手动切换图片
- 自定义图片
- 切换文章组件依在
一 , 苹果手机框出现
首先我们创建一个初始化函数:
然后写两个判断:
1 2
| if(window.innerWidth < 900) return if(document.querySelector("#iphone17-container")) return
|
作用:
手机端不显示
防止 PJAX 页面重复创建
创建手机样式
在 JS 里动态插入 CSS:
1
| const style=document.createElement("style") style.id="iphone-style"
|
然后写入样式:
1
| .iphone17{ width:320px; height:660px; border-radius:70px; padding:12px; background:#111; box-shadow: 0 40px 100px rgba(0,0,0,.8); }
|
效果:
320×660 的手机比例
圆角外壳
阴影制造悬浮感
接着创建屏幕:
1
| .iphone-screen{ width:100%; height:100%; border-radius:48px; overflow:hidden; background:#000; cursor:grab; position:relative; }
|
这样就得到一个 完整的 iPhone 外壳 + 屏幕。
二、让手机可以随意拖动
创建手机容器:
1
| const container=document.createElement("div") container.id="iphone17-container"
|
1
| #iphone17-container{ position:fixed; top:160px; left:80px; width:280px; z-index:10; cursor:move; }
|
使用 position: fixed,手机会悬浮在页面上。
拖动逻辑
1
| container.addEventListener("mousedown",(e)=>{ phoneDrag=true offsetX=e.clientX-container.offsetLeft offsetY=e.clientY-container.offsetTop })
|
记录鼠标位置。
移动时更新位置:
1
| document.addEventListener("mousemove",(e)=>{ if(!phoneDrag) return container.style.left=(e.clientX-offsetX)+"px" container.style.top=(e.clientY-offsetY)+"px" })
|
松开鼠标:
1
| document.addEventListener("mouseup",()=>{ phoneDrag=false snapToEdge() })
|
边缘吸附
当手机靠近边缘时自动贴边:
如果距离小于 80px 就吸附:
1
| if(rect.left<snap){ left=10 }
|
这样手机拖动体验会更像 桌面小组件
三、实现 Dynamic Island 灵动岛
在屏幕顶部添加:
1
| <div class="dynamic-island"> <div class="island-core"></div> </div>
|
样式:
1
| .dynamic-island{ position:absolute; top:12px; left:35%; transform:translateX(-50%); width:100px; height:28px; background:#000; border-radius:20px; }
|
为了让它有 呼吸动画:
1
| animation:islandBreath 10s ease-in-out infinite;
|
为了让它有 呼吸动画:
1
| @keyframes islandBreath{ 0%{ transform:scale(1); } 50%{ transform:scale(1.06); } 100%{ transform:scale(1); } }
|
效果就是:
灵动岛微微变大
再恢复
模拟 iPhone 的 UI 细节。
四、实现图片自动轮播
1
| <div class="photo-track" id="iphoneTrack"> <img src="1.png"> <img src="2.png"> <img src="3.png"> </div>
|
1
| .photo-track{ display:flex; height:100%; will-change:transform; }
|
1
| .photo-track img{ width:100%; height:100%; object-fit:cover; flex-shrink:0; }
|
自动切换
每 3 秒切换:
1
| setInterval(()=>{ if(dragging) return autoIndex++ position=-width*autoIndex },3000)
|
原理:
改变 translateX
让图片轨道向左移动
五、拖动图片手动切换
当鼠标按下时:
1 2
| screen.addEventListener("mousedown",(e)=>{ dragging=true startX=e.clientX })
|
鼠标移动:
1
| let dx=e.clientX-startX position += dx velocity = dx * 0.4
|
这会产生 惯性滑动效果。
动画循环:
1
| function animate(){ position+=velocity velocity*=0.92 track.style.transform="translateX("+position+"px)" requestAnimationFrame(animate) }
|
实现:
丝滑滑动
惯性减速
体验类似手机相册
六、自定义图片
只需要修改这一部分:
1 2 3
| <img src="1.png"> <img src="2.png"> <img src="3.png">
|
换成你自己的图片:
1 2 3
| <img src="/img/photo1.jpg"> <img src="/img/photo2.jpg"> <img src="/img/photo3.jpg">
|
也可以使用 CDN:
1
| <img src="https://cdn.xxx.com/photo1.jpg">
|
甚至可以使用 Base64 图片:
1
| <img src="data:image/png;base64,....">
|
七、切换文章组件依在
1 防止重复创建
在初始化函数最开始有一句:
1
| if(document.querySelector("#iphone17-container")) return
|
意思是:
如果页面已经存在手机
就停止执行
这样即使 PJAX 触发 JS 重新运行,也不会创建第二个手机。
逻辑流程:
第一次加载
↓
创建 iPhone
↓
PJAX 切换文章
↓
再次执行 initIphone()
↓
检测到手机已存在
↓
停止创建
所以页面始终只有 一个手机实例。
2 PJAX 页面监听
2 PJAX 页面监听
代码底部还有这一句:
1
| document.addEventListener("pjax:complete",initIphone)
|
它的作用是:
每次 PJAX 页面加载完成
重新执行 initIphone()
为什么要这样?
因为在 SPA / PJAX 博客中:
DOMContentLoaded
只会触发一次
而页面切换后:
新文章 HTML 已经加载
但 JS 不会重新运行
所以需要监听:
pjax:complete
来重新初始化组件。
完整代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224
| (function(){
function initIphone(){
if(window.innerWidth < 900) return if(document.querySelector("#iphone17-container")) return
if(!document.querySelector("#iphone-style")){
const style=document.createElement("style") style.id="iphone-style"
style.innerHTML=`
#iphone17-container{ position:fixed; top:160px; left:80px; width:280px; z-index:10; cursor:move; }
.iphone17{ width:320px; height:660px; border-radius:70px; padding:12px; background:#111; box-shadow: 0 40px 100px rgba(0,0,0,.8); }
.iphone-screen{ width:100%; height:100%; border-radius:48px; overflow:hidden; background:#000; cursor:grab; position:relative; }
.dynamic-island{ position:absolute; top:12px; left:35%; transform:translateX(-50%); width:100px; height:28px; background:#000; border-radius:20px; z-index:20; animation:islandBreath 10s ease-in-out infinite; }
.island-core{ width:100%; height:100%; background:#000; border-radius:20px; animation:islandBreath 10s ease-in-out infinite; }
.photo-track{ display:flex; height:100%; will-change:transform; }
.photo-track img{ width:100%; height:100%; object-fit:cover; flex-shrink:0; pointer-events:none; user-select:none; }
@keyframes islandBreath{ 0%{ transform:scale(1); } 50%{ transform:scale(1.06); } 100%{ transform:scale(1); } }
.photo-track img{ pointer-events:none; }
@media(max-width:900px){ #iphone17-container{display:none;} }
`
document.head.appendChild(style)
}
const container=document.createElement("div") container.id="iphone17-container" container.innerHTML=`
<div class="iphone17"> <div class="iphone-screen" id="iphoneScreen"> <div class="dynamic-island"> <div class="island-core"></div> </div>
<div class="photo-track" id="iphoneTrack"> <img src="1.png"> <img src="2.png"> <img src="3.png"> </div>
</div> </div>
`
document.body.appendChild(container)
const screen=document.getElementById("iphoneScreen") const track=document.getElementById("iphoneTrack") const imgs=Array.from(track.children) imgs.forEach(img=>{ track.appendChild(img.cloneNode(true)) })
let position=0 let velocity=0 let dragging=false let startX=0
screen.addEventListener("mousedown",(e)=>{ e.preventDefault() dragging=true startX=e.clientX screen.style.cursor="grabbing" })
document.addEventListener("mouseup",()=>{ dragging=false screen.style.cursor="grab" })
document.addEventListener("mousemove",(e)=>{ if(!dragging) return let dx=e.clientX-startX position += dx velocity = dx * 0.4 startX=e.clientX })
function animate(){ position+=velocity velocity*=0.92 const width=screen.offsetWidth if(position<=-width*imgs.length){ position=0 } if(position>0){ position=-width*imgs.length } track.style.transform="translateX("+position+"px)" requestAnimationFrame(animate) }
animate()
let autoIndex=0
setInterval(()=>{ if(dragging) return const width=screen.offsetWidth autoIndex++ position=-width*autoIndex if(autoIndex>=imgs.length){ autoIndex=0; position=0 } },3000)
let phoneDrag=false let offsetX=0 let offsetY=0
container.addEventListener("mousedown",(e)=>{ if(e.target.closest(".iphone-screen")) return phoneDrag=true offsetX=e.clientX-container.offsetLeft offsetY=e.clientY-container.offsetTop })
document.addEventListener("mouseup",()=>{ if(!phoneDrag) return phoneDrag=false snapToEdge() })
document.addEventListener("mousemove",(e)=>{ if(!phoneDrag) return container.style.left=(e.clientX-offsetX)+"px" container.style.top=(e.clientY-offsetY)+"px" })
function snapToEdge(){ const rect=container.getBoundingClientRect() const screenWidth=window.innerWidth const screenHeight=window.innerHeight const snap=80 let left=rect.left let top=rect.top if(rect.left<snap){ left=10 } if(screenWidth-rect.right<snap){ left=screenWidth-rect.width-10 } if(rect.top<snap){ top=10 } if(screenHeight-rect.bottom<snap){ top=screenHeight-rect.height-10 } container.style.left=left+"px" container.style.top=top+"px" }
}
document.addEventListener("DOMContentLoaded",initIphone) document.addEventListener("pjax:complete",initIphone)
})();
|