1+ <template >
2+ <div class =" delayed-loading-demo" >
3+ <h1 >useDelayedLoading 演示</h1 >
4+
5+ <div class =" demo-section" >
6+ <h2 >基本使用</h2 >
7+ <div class =" demo-card" >
8+ <div class =" demo-controls" >
9+ <button @click =" startLoading" :disabled =" loading" >开始加载 ({{ requestTime }}ms)</button >
10+ <div class =" loading-config" >
11+ <label >
12+ 是否使用 useDelayedLoading 优化 loading 的显示:
13+ <input type =" checkbox" v-model =" isOptimized" />
14+ </label >
15+ <label >
16+ 请求时间:
17+ <input type =" range" v-model =" requestTime" min =" 100" max =" 3000" step =" 100" />
18+ {{ requestTime }}ms
19+ </label >
20+ <label >
21+ 延迟显示:
22+ <input type =" range" v-model =" delay" min =" 0" max =" 1000" step =" 100" />
23+ {{ delay }}ms
24+ </label >
25+ <label >
26+ 最小显示时间:
27+ <input type =" range" v-model =" minDisplayTime" min =" 0" max =" 2000" step =" 100" />
28+ {{ minDisplayTime }}ms
29+ </label >
30+ </div >
31+ </div >
32+
33+ <div class =" result-display" >
34+ <div class =" status-info" >
35+ <div class =" status-item" >
36+ <span >加载状态 (loading):</span >
37+ <span :class =" { 'status-active': loading }" >{{ loading ? '加载中' : '空闲' }}</span >
38+ </div >
39+ <div class =" status-item" >
40+ <span >延迟加载状态 (loadingDelayed):</span >
41+ <span :class =" { 'status-active': loadingDelayed }" >{{ loadingDelayed ? '显示' : '隐藏' }}</span >
42+ </div >
43+ </div >
44+ <div class =" loading-container" >
45+ <div v-if =" isOptimized ? loadingDelayed : loading" class =" loading-spinner" ></div >
46+ <div v-else class =" content" >{{ content }}, 本次 loading 显示时间: {{ useTime }}ms</div >
47+ </div >
48+ </div >
49+ </div >
50+ </div >
51+
52+ <div class =" code-section" >
53+ <h2 >代码示例</h2 >
54+ <pre ><code >
55+ import { ref, onUnmounted } from 'vue'
56+ import { useDelayedLoading } from '@utopia/vueuse'
57+
58+ // 创建加载状态
59+ const loading = ref(false)
60+
61+ // 配置延迟加载
62+ const { loadingDelayed, cleanup } = useDelayedLoading(loading, {
63+ delay: 300, // 延迟300ms显示加载
64+ minDisplayTime: 500 // 最小显示500ms
65+ })
66+
67+ // 模拟异步请求
68+ const fetchData = async () => {
69+ loading.value = true
70+ try {
71+ await new Promise(resolve => setTimeout(resolve, 1000))
72+ // 获取数据...
73+ } finally {
74+ loading.value = false
75+ }
76+ }
77+
78+ // 在组件卸载时清理计时器
79+ onUnmounted(() => {
80+ cleanup()
81+ })
82+ </code ></pre >
83+ </div >
84+
85+ <div class =" explanation" >
86+ <h2 >说明</h2 >
87+ <ul >
88+ <li ><strong >加载延迟 (delay)</strong >: 当异步操作开始时,等待指定时间后再显示加载动画,避免闪烁</li >
89+ <li ><strong >最小显示时间 (minDisplayTime)</strong >: 一旦显示加载动画,确保它至少显示指定的时间,避免过快消失造成的视觉混乱</li >
90+ <li ><strong >清理功能 (cleanup)</strong >: 在组件卸载时调用,防止内存泄漏</li >
91+ </ul >
92+ </div >
93+ </div >
94+ </template >
95+
96+ <script setup lang="ts">
97+ import { ref , onUnmounted , watch } from ' vue'
98+ import { useDelayedLoading } from ' @utopia-utils/vueuse'
99+
100+ // 配置参数
101+ const delay = ref (300 )
102+ const minDisplayTime = ref (500 )
103+ const requestTime = ref (3000 )
104+
105+ const useTime = ref (0 )
106+
107+ // 状态
108+ const loading = ref (false )
109+ const content = ref (' 数据已加载' )
110+ /** 是否优化 loading 的显示 */
111+ const isOptimized = ref (true )
112+
113+ // 使用延迟加载Hook的函数和值
114+ let { loadingDelayed, cleanup } = useDelayedLoading (loading , {
115+ delay: delay ,
116+ minDisplayTime: minDisplayTime
117+ })
118+
119+ let now = Date .now ()
120+ watch (loadingDelayed , (newVal ) => {
121+ if (newVal ) {
122+ now = Date .now ()
123+ } else {
124+ useTime .value = Date .now () - now
125+ }
126+ })
127+
128+ // 模拟异步请求
129+ const startLoading = async () => {
130+ loading .value = true
131+ content .value = ' 加载中...'
132+
133+ try {
134+ // 模拟API请求
135+ await new Promise (resolve => setTimeout (resolve , requestTime .value ))
136+ content .value = ` 数据已更新 (${new Date ().toLocaleTimeString ()}) `
137+ } catch (error ) {
138+ content .value = ' 加载失败'
139+ } finally {
140+ loading .value = false
141+ }
142+ }
143+
144+ // 组件卸载时清理
145+ onUnmounted (cleanup )
146+ </script >
147+
148+ <style scoped>
149+ .delayed-loading-demo {
150+ max-width : 800px ;
151+ margin : 0 auto ;
152+ padding : 20px ;
153+ font-family : -apple-system , BlinkMacSystemFont, ' Segoe UI' , Roboto, ' Helvetica Neue' , Arial , sans-serif ;
154+ }
155+
156+ h1 , h2 {
157+ color : #2c3e50 ;
158+ }
159+
160+ .demo-section , .code-section , .explanation {
161+ margin-bottom : 30px ;
162+ border : 1px solid #eaeaea ;
163+ border-radius : 8px ;
164+ padding : 20px ;
165+ box-shadow : 0 2px 8px rgba (0 , 0 , 0 , 0.05 );
166+ background-color : white ;
167+ }
168+
169+ .demo-card {
170+ display : flex ;
171+ flex-direction : column ;
172+ gap : 20px ;
173+ }
174+
175+ .demo-controls {
176+ display : flex ;
177+ flex-direction : column ;
178+ gap : 15px ;
179+ }
180+
181+ button {
182+ padding : 10px 16px ;
183+ background-color : #3498db ;
184+ color : white ;
185+ border : none ;
186+ border-radius : 4px ;
187+ cursor : pointer ;
188+ font-size : 16px ;
189+ transition : background-color 0.3s ;
190+ }
191+
192+ button :hover:not (:disabled ) {
193+ background-color : #2980b9 ;
194+ }
195+
196+ button :disabled {
197+ background-color : #95a5a6 ;
198+ cursor : not-allowed ;
199+ }
200+
201+ .loading-config {
202+ display : flex ;
203+ flex-direction : column ;
204+ gap : 10px ;
205+ background-color : #f8f9fa ;
206+ padding : 15px ;
207+ border-radius : 4px ;
208+ }
209+
210+ label {
211+ display : flex ;
212+ align-items : center ;
213+ gap : 10px ;
214+ }
215+
216+ input [type = " range" ] {
217+ flex : 1 ;
218+ }
219+
220+ .result-display {
221+ border : 1px dashed #ddd ;
222+ border-radius : 4px ;
223+ padding : 20px ;
224+ min-height : 200px ;
225+ display : flex ;
226+ flex-direction : column ;
227+ gap : 20px ;
228+ }
229+
230+ .status-info {
231+ display : flex ;
232+ justify-content : space-between ;
233+ flex-wrap : wrap ;
234+ gap : 10px ;
235+ border-bottom : 1px solid #eee ;
236+ padding-bottom : 10px ;
237+ }
238+
239+ .status-item {
240+ display : flex ;
241+ align-items : center ;
242+ gap : 8px ;
243+ }
244+
245+ .status-active {
246+ color : #2ecc71 ;
247+ font-weight : bold ;
248+ }
249+
250+ .loading-container {
251+ display : flex ;
252+ justify-content : center ;
253+ align-items : center ;
254+ min-height : 120px ;
255+ }
256+
257+ .loading-spinner {
258+ width : 50px ;
259+ height : 50px ;
260+ border : 5px solid rgba (0 , 0 , 0 , 0.1 );
261+ border-radius : 50% ;
262+ border-top-color : #3498db ;
263+ animation : spin 1s ease-in-out infinite ;
264+ }
265+
266+ @keyframes spin {
267+ to { transform : rotate (360deg ); }
268+ }
269+
270+ .content {
271+ text-align : center ;
272+ font-size : 18px ;
273+ }
274+
275+ pre {
276+ background-color : #f8f9fa ;
277+ padding : 15px ;
278+ border-radius : 4px ;
279+ overflow-x : auto ;
280+ font-family : ' Courier New' , Courier , monospace ;
281+ font-size : 14px ;
282+ line-height : 1.5 ;
283+ }
284+
285+ code {
286+ color : #16a085 ;
287+ }
288+
289+ .explanation ul {
290+ line-height : 1.6 ;
291+ }
292+
293+ .explanation li {
294+ margin-bottom : 10px ;
295+ }
296+ </style >
0 commit comments