Skip to content

Commit 5767a42

Browse files
committed
feat(ui): Enhance FAB and modal interactions with animations and state updates
- Added ripple and click animations to the FAB for better visual feedback. - Integrated dynamic scale and transform-origin effects during modal transitions. - Included loading spinner and skeleton screens for smoother content loading. - Improved responsiveness by recalculating positions on window resize. - Optimized accessibility and motion preferences with prefers-reduced-motion media query support.
1 parent 0582e66 commit 5767a42

2 files changed

Lines changed: 214 additions & 4 deletions

File tree

src/App.svelte

Lines changed: 213 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,26 @@
11
<script>
22
import {onMount, tick} from 'svelte';
3+
import {scale, fade} from 'svelte/transition';
34
import {Logger} from './utils/logger.js';
45
import {SETTINGS} from './utils/settings.js';
56
import AppController from './app/app-controller.js';
67
7-
88
// Import logo images for inline packaging
99
import logo40w from '../public/assets/logo-40w.png';
1010
import logo80w from '../public/assets/logo-80w.png';
1111
import logo120w from '../public/assets/logo-120w.png';
12+
import logoFullRes from '../public/assets/logo.png';
1213
1314
let showModal = false;
1415
let isAssistantInitialized = false;
1516
let appInstance = null;
1617
let fab;
1718
let modalContent;
1819
let aiContainerElement;
20+
let isButtonClicked = false;
21+
let rippleActive = false;
22+
let fabPosition = {x: 0, y: 0}; // FAB按钮中心坐标
23+
let modalOrigin = '50% 50%'; // Modal的transform-origin
1924
2025
2126
// FAB 拖拽逻辑
@@ -116,9 +121,41 @@
116121
};
117122
}
118123
124+
function calculateFabCenter() {
125+
if (!fab) return {x: 0, y: 0};
126+
const rect = fab.getBoundingClientRect();
127+
return {
128+
x: rect.left + rect.width / 2,
129+
y: rect.top + rect.height / 2
130+
};
131+
}
132+
133+
function updateModalOrigin() {
134+
const {x, y} = fabPosition;
135+
const vw = window.innerWidth;
136+
const vh = window.innerHeight;
137+
const originX = (x / vw) * 100;
138+
const originY = (y / vh) * 100;
139+
modalOrigin = `${originX}% ${originY}%`;
140+
}
141+
119142
async function handleFabClick() {
120143
if (wasDragged) return;
121144
145+
// 按钮点击动画效果
146+
isButtonClicked = true;
147+
rippleActive = true;
148+
149+
// 300ms后重置按钮状态
150+
setTimeout(() => {
151+
isButtonClicked = false;
152+
rippleActive = false;
153+
}, 300);
154+
155+
// 计算FAB按钮位置
156+
fabPosition = calculateFabCenter();
157+
updateModalOrigin();
158+
122159
showModal = true;
123160
if (!isAssistantInitialized) {
124161
await tick();
@@ -143,6 +180,21 @@
143180
}
144181
}
145182
183+
function dynamicScale(node, {duration = 180, start = 0.8, origin = '50% 50%'}) {
184+
return {
185+
duration,
186+
css: (t) => {
187+
const scale = start + (1 - start) * t;
188+
const opacity = t;
189+
return `
190+
transform: scale(${scale});
191+
transform-origin: ${origin};
192+
opacity: ${opacity};
193+
`;
194+
}
195+
};
196+
}
197+
146198
// Click outside to close logic
147199
function handleClickOutside(event) {
148200
if (fab && fab.contains(event.target)) {
@@ -183,6 +235,22 @@
183235
}
184236
}
185237
238+
// 窗口大小变化时重新计算
239+
$: if (typeof window !== 'undefined') {
240+
const handleResize = () => {
241+
if (showModal && fab) {
242+
fabPosition = calculateFabCenter();
243+
updateModalOrigin();
244+
}
245+
};
246+
247+
if (showModal) {
248+
window.addEventListener('resize', handleResize);
249+
} else {
250+
window.removeEventListener('resize', handleResize);
251+
}
252+
}
253+
186254
onMount(() => {
187255
// 修复:只在需要时添加事件监听器,避免全局污染
188256
return () => {
@@ -195,6 +263,9 @@
195263
<button
196264
bind:this={fab}
197265
id="ai-assistant-fab"
266+
class:fab-clicked={isButtonClicked}
267+
class:fab-ripple={rippleActive}
268+
198269
on:mousedown={handleMouseDown}
199270
on:click={handleFabClick}
200271
>
@@ -206,9 +277,23 @@
206277
</button>
207278

208279
{#if showModal}
209-
<div class="ai-modal-content" bind:this={modalContent} use:draggable>
280+
<div class="ai-modal-content" bind:this={modalContent} use:draggable
281+
transition:dynamicScale={{duration: 180, start: 0.8, origin: modalOrigin}}>
210282
<button class="ai-modal-close-btn" on:click={() => { showModal = false; }}>&times;</button>
211-
<div id="ai-container" bind:this={aiContainerElement}></div>
283+
<div id="ai-container" bind:this={aiContainerElement}>
284+
{#if !isAssistantInitialized}
285+
<div class="simple-loading" transition:fade={{duration: 300}}>
286+
<img src={logoFullRes} alt="Loading" class="loading-logo"/>
287+
</div>
288+
{:else if false}
289+
<div class="skeleton-container">
290+
<div class="skeleton-header"></div>
291+
<div class="skeleton-message"></div>
292+
<div class="skeleton-message short"></div>
293+
<div class="skeleton-input"></div>
294+
</div>
295+
{/if}
296+
</div>
212297
</div>
213298
{/if}
214299

@@ -250,6 +335,36 @@
250335
transform: scale(0.98);
251336
}
252337
338+
/* FAB 动画状态 */
339+
#ai-assistant-fab.fab-clicked {
340+
transform: scale(0.95);
341+
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.3);
342+
transition: all 0.15s ease-out;
343+
}
344+
345+
#ai-assistant-fab.fab-ripple::after {
346+
content: '';
347+
position: absolute;
348+
top: 50%;
349+
left: 50%;
350+
width: 0;
351+
height: 0;
352+
border-radius: 50%;
353+
background: rgba(255, 255, 255, 0.3);
354+
transform: translate(-50%, -50%);
355+
animation: ripple 0.6s ease-out;
356+
}
357+
358+
@keyframes ripple {
359+
to {
360+
width: 120px;
361+
height: 120px;
362+
opacity: 0;
363+
}
364+
}
365+
366+
/* 移除了旋转动画效果 */
367+
253368
/* Modal Styles */
254369
.ai-modal-content {
255370
position: fixed;
@@ -309,6 +424,7 @@
309424
display: flex;
310425
flex-direction: column;
311426
overflow: hidden; /* Important to contain children */
427+
position: relative; /* 为loading-splash提供定位上下文 */
312428
}
313429
314430
.ai-modal-close-btn svg,
@@ -317,5 +433,99 @@
317433
height: 100%;
318434
}
319435
436+
/* 简单加载动画样式 */
437+
.simple-loading {
438+
position: absolute;
439+
top: 0;
440+
left: 0;
441+
width: 100%;
442+
height: 100%;
443+
display: flex;
444+
justify-content: center;
445+
align-items: center;
446+
z-index: 10001;
447+
}
448+
449+
.loading-logo {
450+
width: 80px;
451+
height: 80px;
452+
opacity: 0.7;
453+
animation: breathing 1s ease-in-out infinite;
454+
}
455+
456+
@keyframes breathing {
457+
0%, 100% {
458+
transform: scale(1);
459+
opacity: 0.7;
460+
}
461+
50% {
462+
transform: scale(1.1);
463+
opacity: 1;
464+
}
465+
}
466+
467+
/* 骨架屏样式 */
468+
.skeleton-container {
469+
padding: 20px;
470+
width: 100%;
471+
height: 100%;
472+
}
473+
474+
.skeleton-header,
475+
.skeleton-message,
476+
.skeleton-input {
477+
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
478+
background-size: 200% 100%;
479+
animation: skeletonLoading 1.5s infinite;
480+
border-radius: 4px;
481+
margin-bottom: 16px;
482+
}
483+
484+
.skeleton-header {
485+
height: 24px;
486+
width: 60%;
487+
}
488+
489+
.skeleton-message {
490+
height: 16px;
491+
width: 100%;
492+
}
493+
494+
.skeleton-message.short {
495+
width: 75%;
496+
}
497+
498+
.skeleton-input {
499+
height: 40px;
500+
width: 100%;
501+
margin-top: 20px;
502+
}
503+
504+
@keyframes skeletonLoading {
505+
0% {
506+
background-position: -200% 0;
507+
}
508+
100% {
509+
background-position: 200% 0;
510+
}
511+
}
512+
513+
/* 性能优化 */
514+
#ai-assistant-fab,
515+
.ai-modal-content,
516+
.loading-splash {
517+
will-change: transform;
518+
}
519+
520+
/* 减少动画偏好支持 */
521+
@media (prefers-reduced-motion: reduce) {
522+
#ai-assistant-fab,
523+
.ai-modal-content,
524+
.loading-logo {
525+
animation: none !important;
526+
transition: none !important;
527+
}
528+
}
529+
320530
321531
</style>

src/app/app-controller.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ export default class AppController {
4242

4343
if (!containerElement) {
4444
// fixme)) Cannot initialize
45-
Logger.error(`Initialization failed: Invalid container element provided.`);
45+
Logger.error("Initialization failed: Invalid container element provided.");
4646
return;
4747
}
4848

0 commit comments

Comments
 (0)