From da8434662f1ec46122faba9292b65e1f2fcd40b1 Mon Sep 17 00:00:00 2001 From: NetworkCats Date: Sun, 22 Mar 2026 12:06:56 +0800 Subject: [PATCH] feat: add skeleton loading state for alt-IP section when primary IP is IPv6 Show a shimmer skeleton placeholder while fetching the local IPv4 address via the IPv4-only domain. When the primary IP is already IPv4, no loading state is displayed and the alt section fades in directly. --- static/script.js | 46 +++++++++++++++++++++++++---- static/style.css | 76 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 117 insertions(+), 5 deletions(-) diff --git a/static/script.js b/static/script.js index c28ab68..d0b8967 100644 --- a/static/script.js +++ b/static/script.js @@ -272,10 +272,37 @@ function initCopyIp(wrap){ return html; } - function showAltIp(ip,info){ - section.innerHTML=buildBlock(ip,info); + function buildSkeleton(){ + var html='
'; + html+='
'; + html+='
'; + html+='
'; + for(var i=0;i<8;i++){ + html+='
'; + html+='
'; + html+='
'; + html+='
'; + } + html+='
'; + return html; + } + + function showSkeleton(){ + section.innerHTML=buildSkeleton(); section.hidden=false; - initCopyIp(section.querySelector(".ip-copy-wrap")); + section.classList.add("alt-visible"); + } + + function showAltIp(ip,info){ + section.classList.remove("alt-visible"); + setTimeout(function(){ + section.innerHTML=buildBlock(ip,info); + initCopyIp(section.querySelector(".ip-copy-wrap")); + section.hidden=false; + // Force reflow before adding the visible class for transition + void section.offsetHeight; + section.classList.add("alt-visible"); + },primaryIsIPv6?300:0); } function fetchJson(ip){ @@ -285,8 +312,17 @@ function initCopyIp(wrap){ }); } + function hideSkeleton(){ + section.classList.remove("alt-visible"); + setTimeout(function(){ + section.innerHTML=""; + section.hidden=true; + },300); + } + function detectAltIp(){ if(primaryIsIPv6&&ipv4Domain){ + showSkeleton(); var controller=new AbortController(); var timeout=setTimeout(function(){controller.abort();},5000); fetch("https://"+ipv4Domain+"/",{signal:controller.signal}) @@ -297,12 +333,12 @@ function initCopyIp(wrap){ }) .then(function(text){ var ip=text.trim(); - if(!ip||ip.indexOf(":")!==-1)return; + if(!ip||ip.indexOf(":")!==-1){hideSkeleton();return;} return fetchJson(ip).then(function(info){ showAltIp(ip,info); }); }) - .catch(function(){}); + .catch(function(){hideSkeleton();}); }else if(!primaryIsIPv6){ fetch("/ip") .then(function(r){ diff --git a/static/style.css b/static/style.css index 26b4d2b..8f65d83 100644 --- a/static/style.css +++ b/static/style.css @@ -477,6 +477,82 @@ main { padding: 6px 10px 6px 28px; } +#alt-ip-section { + opacity: 0; + transition: opacity 0.3s ease; +} + +#alt-ip-section.alt-visible { + opacity: 1; +} + +.skeleton-block { + display: flex; + flex-direction: column; + align-items: center; +} + +.skeleton-bone { + background: var(--color-gray5); + border-radius: 6px; + position: relative; + overflow: hidden; +} + +.skeleton-bone::after { + content: ""; + position: absolute; + inset: 0; + background: linear-gradient( + 90deg, + transparent 0%, + var(--color-bg-tertiary) 50%, + transparent 100% + ); + animation: skeleton-shimmer 1.5s ease-in-out infinite; +} + +@keyframes skeleton-shimmer { + 0% { translate: -100% 0; } + 100% { translate: 100% 0; } +} + +.skeleton-ip { + inline-size: 260px; + block-size: 42px; + margin-block: 28px 20px; +} + +.skeleton-table { + inline-size: 100%; +} + +.skeleton-row { + display: flex; + align-items: center; + padding-block: 10px; + border-block-start: 1px solid var(--color-separator); +} + +.skeleton-row:last-child { + border-block-end: 1px solid var(--color-separator); +} + +.skeleton-row-label { + inline-size: 42%; + padding-inline: 35px 32px; +} + +.skeleton-row-label .skeleton-bone { + inline-size: 60px; + block-size: 15px; +} + +.skeleton-row-value .skeleton-bone { + inline-size: 100px; + block-size: 15px; +} + @media (width <= 480px) { main { margin-block-start: 40px;