diff --git a/public/js/app.js b/public/js/app.js
index 6d21b40..fdcc891 100644
--- a/public/js/app.js
+++ b/public/js/app.js
@@ -207,19 +207,20 @@
dom.btnChangePassword.addEventListener('click', saveChangePassword);
}
- // Globe expansion (FLIP animation)
+ // Globe expansion (FLIP animation via Web Animations API)
let savedGlobeRect = null;
+ let globeAnimating = false;
function expandGlobe() {
- if (dom.globeCard.classList.contains('expanded')) return;
+ if (dom.globeCard.classList.contains('expanded') || globeAnimating) return;
+ globeAnimating = true;
- // Save original rect for FLIP
+ // FLIP: capture original position
savedGlobeRect = dom.globeCard.getBoundingClientRect();
+ // Apply expanded state
dom.globeCard.classList.add('expanded');
dom.btnExpandGlobe.classList.add('active');
-
- // Update button icon/title
dom.btnExpandGlobe.title = '缩小显示';
dom.btnExpandGlobe.innerHTML = `
`;
+ // Resize ECharts immediately (prevents flash)
if (myMap2D) myMap2D.resize();
+ // FLIP: capture expanded position
const endRect = dom.globeCard.getBoundingClientRect();
const scaleX = savedGlobeRect.width / endRect.width;
const scaleY = savedGlobeRect.height / endRect.height;
-
- // Using top-left for math (transformOrigin: 0 0)
const dx = savedGlobeRect.left - endRect.left;
const dy = savedGlobeRect.top - endRect.top;
- dom.globeCard.style.willChange = 'transform, box-shadow';
- dom.globeCard.style.transformOrigin = '0 0';
- dom.globeCard.style.transition = 'none';
- dom.globeCard.style.transform = `translate(${dx}px, ${dy}px) scale(${scaleX}, ${scaleY})`;
- dom.globeCard.style.boxShadow = '0 0 0 0 transparent, 0 0 0 0 transparent';
+ // Animate using Web Animations API (bypasses all CSS conflicts)
+ const anim = dom.globeCard.animate([
+ {
+ transform: `translate(${dx}px, ${dy}px) scale(${scaleX}, ${scaleY})`,
+ boxShadow: '0 0 0 0 transparent, 0 0 0 0 transparent',
+ offset: 0
+ },
+ {
+ transform: 'translate(0, 0) scale(1)',
+ boxShadow: '0 0 80px rgba(0,0,0,0.8), 0 0 0 100vh rgba(0,0,0,0.65)',
+ offset: 1
+ }
+ ], {
+ duration: 400,
+ easing: 'cubic-bezier(0.16, 1, 0.3, 1)',
+ fill: 'none'
+ });
- dom.globeCard.offsetHeight; // Force reflow
-
- dom.globeCard.style.transition = 'transform 0.4s cubic-bezier(0.16, 1, 0.3, 1), box-shadow 0.4s ease';
- dom.globeCard.style.transform = 'translate(0px, 0px) scale(1)';
- dom.globeCard.style.boxShadow = '';
-
- const onTransitionEnd = (e) => {
- if (e.propertyName !== 'transform') return;
- dom.globeCard.removeEventListener('transitionend', onTransitionEnd);
- dom.globeCard.style.transition = '';
- dom.globeCard.style.willChange = '';
- dom.globeCard.style.transformOrigin = '';
- dom.globeCard.style.transform = '';
+ anim.onfinish = () => {
+ globeAnimating = false;
};
- dom.globeCard.addEventListener('transitionend', onTransitionEnd);
}
function collapseGlobe() {
- if (!dom.globeCard.classList.contains('expanded')) return;
- if (dom.globeCard.classList.contains('globe-collapsing')) return;
+ if (!dom.globeCard.classList.contains('expanded') || globeAnimating) return;
+ globeAnimating = true;
const resetState = () => {
dom.globeCard.classList.remove('expanded', 'globe-collapsing');
dom.btnExpandGlobe.classList.remove('active');
- dom.globeCard.style.transition = '';
- dom.globeCard.style.transform = '';
- dom.globeCard.style.boxShadow = '';
- dom.globeCard.style.willChange = '';
-
- // Restore button icon/title
dom.btnExpandGlobe.title = '放大显示';
dom.btnExpandGlobe.innerHTML = `
`;
-
+ globeAnimating = false;
if (myMap2D) requestAnimationFrame(() => myMap2D.resize());
};
@@ -290,43 +285,35 @@
dom.globeCard.classList.add('globe-collapsing');
+ // FLIP: compute target transform to original position
const expandedRect = dom.globeCard.getBoundingClientRect();
const scaleX = savedGlobeRect.width / expandedRect.width;
const scaleY = savedGlobeRect.height / expandedRect.height;
-
- // Match origin used in expand
const dx = savedGlobeRect.left - expandedRect.left;
const dy = savedGlobeRect.top - expandedRect.top;
- // Force setup the expanded state with origin
- dom.globeCard.style.transformOrigin = '0 0';
- dom.globeCard.style.transform = 'translate(0px, 0px) scale(1)';
- dom.globeCard.style.boxShadow = ''; // Reset to what CSS says
- dom.globeCard.offsetHeight; // Critical sync reflow
+ // Animate from current expanded state to original rect
+ const anim = dom.globeCard.animate([
+ {
+ transform: 'translate(0, 0) scale(1)',
+ boxShadow: '0 0 80px rgba(0,0,0,0.8), 0 0 0 100vh rgba(0,0,0,0.65)',
+ offset: 0
+ },
+ {
+ transform: `translate(${dx}px, ${dy}px) scale(${scaleX}, ${scaleY})`,
+ boxShadow: '0 0 0 0 transparent, 0 0 0 0 transparent',
+ offset: 1
+ }
+ ], {
+ duration: 350,
+ easing: 'cubic-bezier(0.4, 0, 0.2, 1)',
+ fill: 'forwards' // Hold final frame until we remove class
+ });
- dom.globeCard.style.willChange = 'transform, box-shadow';
- dom.globeCard.style.transition = 'transform 0.35s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.35s ease';
-
- // Apply target values immediately after reflow
- dom.globeCard.style.transform = `translate(${dx}px, ${dy}px) scale(${scaleX}, ${scaleY})`;
- dom.globeCard.style.boxShadow = '0 0 0 0 transparent, 0 0 0 0 transparent';
-
- let transitionFinished = false;
- const onTransitionEnd = (e) => {
- if (e.propertyName !== 'transform') return;
- transitionFinished = true;
- dom.globeCard.removeEventListener('transitionend', onTransitionEnd);
+ anim.onfinish = () => {
+ anim.cancel(); // Release the fill-forwards hold
resetState();
};
- dom.globeCard.addEventListener('transitionend', onTransitionEnd);
-
- // Robustness fallback
- setTimeout(() => {
- if (!transitionFinished) {
- dom.globeCard.removeEventListener('transitionend', onTransitionEnd);
- resetState();
- }
- }, 500);
}
if (dom.btnExpandGlobe) {