实现的功能

  1. 苹果手机框出现
  2. 手机可随意移动
  3. 灵动岛模仿
  4. 图片自动切换
  5. 拖动图片可手动切换图片
  6. 自定义图片
  7. 切换文章组件依在

一 , 苹果手机框出现

首先我们创建一个初始化函数:

1
function initIphone(){

然后写两个判断:

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() })

边缘吸附

当手机靠近边缘时自动贴边:

1
const snap=80

如果距离小于 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)

})();