Add loading indicators for city detail sync
This commit is contained in:
@@ -606,6 +606,30 @@
|
|||||||
margin-bottom: 6px;
|
margin-bottom: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.root :global(.panel-loading-hint) {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: 1px solid rgba(34, 211, 238, 0.18);
|
||||||
|
background: rgba(12, 24, 42, 0.78);
|
||||||
|
color: var(--accent-cyan);
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root :global(.panel-loading-spinner) {
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: 2px solid rgba(34, 211, 238, 0.22);
|
||||||
|
border-top-color: rgba(34, 211, 238, 0.92);
|
||||||
|
animation: loading-spin 0.8s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
.root :global(.panel-meta) {
|
.root :global(.panel-meta) {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -613,6 +637,46 @@
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.root :global(.city-loading-toast) {
|
||||||
|
position: fixed;
|
||||||
|
top: 78px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
z-index: 1200;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 10px 14px;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: 1px solid rgba(34, 211, 238, 0.18);
|
||||||
|
background: linear-gradient(
|
||||||
|
180deg,
|
||||||
|
rgba(10, 18, 34, 0.94),
|
||||||
|
rgba(10, 18, 34, 0.82)
|
||||||
|
);
|
||||||
|
box-shadow:
|
||||||
|
0 18px 40px rgba(2, 6, 23, 0.38),
|
||||||
|
0 0 0 1px rgba(34, 211, 238, 0.04) inset;
|
||||||
|
backdrop-filter: blur(18px);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root :global(.city-loading-dot) {
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: var(--accent-cyan);
|
||||||
|
box-shadow: 0 0 0 0 rgba(34, 211, 238, 0.5);
|
||||||
|
animation: city-loading-pulse 1.35s ease-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root :global(.city-loading-copy) {
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.25px;
|
||||||
|
}
|
||||||
|
|
||||||
.root :global(.pro-locked) {
|
.root :global(.pro-locked) {
|
||||||
filter: grayscale(0.8) opacity(0.7);
|
filter: grayscale(0.8) opacity(0.7);
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -1596,6 +1660,21 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes city-loading-pulse {
|
||||||
|
0% {
|
||||||
|
transform: scale(0.92);
|
||||||
|
box-shadow: 0 0 0 0 rgba(34, 211, 238, 0.44);
|
||||||
|
}
|
||||||
|
70% {
|
||||||
|
transform: scale(1);
|
||||||
|
box-shadow: 0 0 0 10px rgba(34, 211, 238, 0);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scale(0.92);
|
||||||
|
box-shadow: 0 0 0 0 rgba(34, 211, 238, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes radar-sweep {
|
@keyframes radar-sweep {
|
||||||
from {
|
from {
|
||||||
transform: rotate(0deg);
|
transform: rotate(0deg);
|
||||||
|
|||||||
@@ -317,6 +317,16 @@ export function DetailPanel() {
|
|||||||
</button>
|
</button>
|
||||||
<div className="panel-title-area">
|
<div className="panel-title-area">
|
||||||
<h2>{panelDisplayName.toUpperCase()}</h2>
|
<h2>{panelDisplayName.toUpperCase()}</h2>
|
||||||
|
{store.loadingState.cityDetail && (
|
||||||
|
<div className="panel-loading-hint" role="status" aria-live="polite">
|
||||||
|
<span className="panel-loading-spinner" aria-hidden="true" />
|
||||||
|
<span>
|
||||||
|
{locale === "en-US"
|
||||||
|
? `Syncing ${panelDisplayName}...`
|
||||||
|
: `正在同步 ${panelDisplayName}...`}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="panel-meta">
|
<div className="panel-meta">
|
||||||
<span className={clsx("risk-badge", panelRiskLevel)}>
|
<span className={clsx("risk-badge", panelRiskLevel)}>
|
||||||
{getRiskBadgeLabel(panelRiskLevel, locale)}
|
{getRiskBadgeLabel(panelRiskLevel, locale)}
|
||||||
|
|||||||
@@ -45,6 +45,11 @@ const FutureForecastModal = dynamic(
|
|||||||
function DashboardScreen() {
|
function DashboardScreen() {
|
||||||
const store = useDashboardStore();
|
const store = useDashboardStore();
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
const activeCityName =
|
||||||
|
store.selectedDetail?.display_name ||
|
||||||
|
store.cities.find((city) => city.name === store.selectedCity)?.display_name ||
|
||||||
|
store.selectedCity ||
|
||||||
|
"";
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const onKeyDown = (event: KeyboardEvent) => {
|
const onKeyDown = (event: KeyboardEvent) => {
|
||||||
@@ -79,6 +84,14 @@ function DashboardScreen() {
|
|||||||
<HeaderBar />
|
<HeaderBar />
|
||||||
<CitySidebar />
|
<CitySidebar />
|
||||||
<DetailPanel />
|
<DetailPanel />
|
||||||
|
{store.loadingState.cityDetail && activeCityName ? (
|
||||||
|
<div className="city-loading-toast" role="status" aria-live="polite">
|
||||||
|
<span className="city-loading-dot" aria-hidden="true" />
|
||||||
|
<span className="city-loading-copy">
|
||||||
|
{t("dashboard.loading")} {activeCityName}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
{store.historyState.isOpen && <HistoryModal />}
|
{store.historyState.isOpen && <HistoryModal />}
|
||||||
{store.futureModalDate && <FutureForecastModal />}
|
{store.futureModalDate && <FutureForecastModal />}
|
||||||
{showLoading && (
|
{showLoading && (
|
||||||
|
|||||||
Reference in New Issue
Block a user