Unify city detail panel access for guests and free users
This commit is contained in:
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import type { ChartConfiguration } from "chart.js";
|
import type { ChartConfiguration } from "chart.js";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
import { useEffect, useMemo, useRef, useState } from "react";
|
import { useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { ForecastTable } from "@/components/dashboard/PanelSections";
|
import { ForecastTable } from "@/components/dashboard/PanelSections";
|
||||||
import { useChart } from "@/hooks/useChart";
|
import { useChart } from "@/hooks/useChart";
|
||||||
@@ -130,6 +131,7 @@ function DetailMiniTemperatureChart({ detail }: { detail: CityDetail }) {
|
|||||||
export function DetailPanel() {
|
export function DetailPanel() {
|
||||||
const store = useDashboardStore();
|
const store = useDashboardStore();
|
||||||
const { locale, t } = useI18n();
|
const { locale, t } = useI18n();
|
||||||
|
const router = useRouter();
|
||||||
const detail = store.selectedDetail;
|
const detail = store.selectedDetail;
|
||||||
const selectedCityItem = useMemo(
|
const selectedCityItem = useMemo(
|
||||||
() =>
|
() =>
|
||||||
@@ -138,11 +140,8 @@ export function DetailPanel() {
|
|||||||
: null,
|
: null,
|
||||||
[store.cities, store.selectedCity],
|
[store.cities, store.selectedCity],
|
||||||
);
|
);
|
||||||
const selectedSummary = store.selectedCity
|
|
||||||
? store.citySummariesByName[store.selectedCity] || null
|
|
||||||
: null;
|
|
||||||
const isBasicGuestPanel = !detail && Boolean(store.selectedCity && selectedCityItem && selectedSummary);
|
|
||||||
const isPro = store.proAccess.subscriptionActive;
|
const isPro = store.proAccess.subscriptionActive;
|
||||||
|
const isAuthenticated = store.proAccess.authenticated;
|
||||||
const panelRef = useRef<HTMLElement | null>(null);
|
const panelRef = useRef<HTMLElement | null>(null);
|
||||||
const [heavyContentReady, setHeavyContentReady] = useState(false);
|
const [heavyContentReady, setHeavyContentReady] = useState(false);
|
||||||
const isOverlayOpen =
|
const isOverlayOpen =
|
||||||
@@ -151,7 +150,7 @@ export function DetailPanel() {
|
|||||||
const isVisible =
|
const isVisible =
|
||||||
store.isPanelOpen &&
|
store.isPanelOpen &&
|
||||||
Boolean(store.selectedCity) &&
|
Boolean(store.selectedCity) &&
|
||||||
(Boolean(detail) || isBasicGuestPanel) &&
|
Boolean(detail) &&
|
||||||
!store.loadingState.cityDetail &&
|
!store.loadingState.cityDetail &&
|
||||||
!isOverlayOpen;
|
!isOverlayOpen;
|
||||||
const panelDisplayName =
|
const panelDisplayName =
|
||||||
@@ -176,6 +175,29 @@ export function DetailPanel() {
|
|||||||
active.blur();
|
active.blur();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
const handleFeatureAccess = (feature: "today" | "history") => {
|
||||||
|
blurActiveElement();
|
||||||
|
|
||||||
|
if (isPro) {
|
||||||
|
if (feature === "today") {
|
||||||
|
void store.openTodayModal();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
void store.openHistory();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isAuthenticated) {
|
||||||
|
router.push("/account");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (feature === "today") {
|
||||||
|
void store.openTodayModal();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
void store.openHistory();
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const panel = panelRef.current;
|
const panel = panelRef.current;
|
||||||
@@ -266,11 +288,8 @@ export function DetailPanel() {
|
|||||||
? t("detail.todayAnalysis")
|
? t("detail.todayAnalysis")
|
||||||
: `${t("detail.todayAnalysis")} (Pro)`
|
: `${t("detail.todayAnalysis")} (Pro)`
|
||||||
}
|
}
|
||||||
onClick={() => {
|
onClick={() => handleFeatureAccess("today")}
|
||||||
blurActiveElement();
|
disabled={!store.selectedCity}
|
||||||
void store.openTodayModal();
|
|
||||||
}}
|
|
||||||
disabled={!detail}
|
|
||||||
>
|
>
|
||||||
{isPro
|
{isPro
|
||||||
? t("detail.todayAnalysis")
|
? t("detail.todayAnalysis")
|
||||||
@@ -282,11 +301,8 @@ export function DetailPanel() {
|
|||||||
title={
|
title={
|
||||||
isPro ? t("detail.history") : `${t("detail.history")} (Pro)`
|
isPro ? t("detail.history") : `${t("detail.history")} (Pro)`
|
||||||
}
|
}
|
||||||
onClick={() => {
|
onClick={() => handleFeatureAccess("history")}
|
||||||
blurActiveElement();
|
disabled={!store.selectedCity}
|
||||||
void store.openHistory();
|
|
||||||
}}
|
|
||||||
disabled={!detail}
|
|
||||||
>
|
>
|
||||||
{isPro ? t("detail.history") : `${t("detail.history")} · Pro`}
|
{isPro ? t("detail.history") : `${t("detail.history")} · Pro`}
|
||||||
</button>
|
</button>
|
||||||
@@ -296,7 +312,7 @@ export function DetailPanel() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="panel-body">
|
<div className="panel-body">
|
||||||
{!detail && !isBasicGuestPanel ? (
|
{!detail ? (
|
||||||
<section>
|
<section>
|
||||||
<div style={{ color: "var(--text-muted)", fontSize: "13px" }}>
|
<div style={{ color: "var(--text-muted)", fontSize: "13px" }}>
|
||||||
{store.loadingState.cityDetail
|
{store.loadingState.cityDetail
|
||||||
@@ -306,66 +322,6 @@ export function DetailPanel() {
|
|||||||
</section>
|
</section>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{isBasicGuestPanel && selectedCityItem && selectedSummary ? (
|
|
||||||
<>
|
|
||||||
<section className="detail-section">
|
|
||||||
<h3>{t("detail.profile")}</h3>
|
|
||||||
<div className="detail-grid">
|
|
||||||
<div className="detail-card">
|
|
||||||
<span className="detail-label">{locale === "en-US" ? "City" : "城市"}</span>
|
|
||||||
<span className="detail-value">{selectedCityItem.display_name}</span>
|
|
||||||
</div>
|
|
||||||
<div className="detail-card">
|
|
||||||
<span className="detail-label">{locale === "en-US" ? "Current" : "当前温度"}</span>
|
|
||||||
<span className="detail-value">
|
|
||||||
{selectedSummary.current?.temp != null
|
|
||||||
? `${selectedSummary.current.temp}${selectedSummary.temp_symbol || "°C"}`
|
|
||||||
: t("common.na")}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="detail-card">
|
|
||||||
<span className="detail-label">{locale === "en-US" ? "Observation" : "观测时间"}</span>
|
|
||||||
<span className="detail-value">{selectedSummary.current?.obs_time || t("common.na")}</span>
|
|
||||||
</div>
|
|
||||||
<div className="detail-card">
|
|
||||||
<span className="detail-label">{locale === "en-US" ? "DEB" : "DEB 预测"}</span>
|
|
||||||
<span className="detail-value">
|
|
||||||
{selectedSummary.deb?.prediction != null
|
|
||||||
? `${selectedSummary.deb.prediction}${selectedSummary.temp_symbol || "°C"}`
|
|
||||||
: t("common.na")}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="detail-card">
|
|
||||||
<span className="detail-label">{locale === "en-US" ? "Settlement" : "结算口径"}</span>
|
|
||||||
<span className="detail-value">
|
|
||||||
{selectedSummary.current?.settlement_source_label ||
|
|
||||||
selectedCityItem.settlement_source_label ||
|
|
||||||
t("common.na")}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="detail-card">
|
|
||||||
<span className="detail-label">{t("section.airport")}</span>
|
|
||||||
<span className="detail-value">{selectedCityItem.airport || t("common.na")}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<section className="detail-section">
|
|
||||||
<div className="detail-card">
|
|
||||||
<span className="detail-label">
|
|
||||||
{locale === "en-US" ? "Pro required for intraday analysis and history" : "今日日内分析与历史对账需开通 Pro"}
|
|
||||||
</span>
|
|
||||||
<span className="detail-source-note" style={{ marginTop: 8 }}>
|
|
||||||
{locale === "en-US"
|
|
||||||
? "Guests can browse the city overview here. Sign in and subscribe to unlock the full intraday model, history reconciliation, and market-linked weather analysis."
|
|
||||||
: "游客可先浏览城市概览。登录并开通 Pro 后,可查看完整的今日日内分析、历史对账和市场联动天气解读。"}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{!isBasicGuestPanel ? (
|
|
||||||
<>
|
|
||||||
<section className="detail-scenery-card">
|
<section className="detail-scenery-card">
|
||||||
{scenery ? (
|
{scenery ? (
|
||||||
<>
|
<>
|
||||||
@@ -454,8 +410,6 @@ export function DetailPanel() {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
{heavyContentReady ? <ForecastTable /> : null}
|
{heavyContentReady ? <ForecastTable /> : null}
|
||||||
</>
|
|
||||||
) : null}
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -491,17 +491,19 @@ export function DashboardStoreProvider({
|
|||||||
await refreshProAccess();
|
await refreshProAccess();
|
||||||
}
|
}
|
||||||
const access = proAccessRef.current;
|
const access = proAccessRef.current;
|
||||||
if (!access.authenticated || !access.subscriptionActive) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setLoadingState((current) => ({ ...current, cityDetail: true }));
|
setLoadingState((current) => ({ ...current, cityDetail: true }));
|
||||||
try {
|
try {
|
||||||
const detail = await ensureCityDetail(cityName);
|
const detail = await ensureCityDetail(cityName);
|
||||||
setSelectedForecastDate(detail.local_date);
|
setSelectedForecastDate(detail.local_date);
|
||||||
// 预热市场数据,不做 await 阻塞,后台静默拉取
|
if (access.authenticated && access.subscriptionActive) {
|
||||||
void ensureCityMarketScan(cityName, false, null, detail.local_date).catch(
|
// 预热市场数据,不做 await 阻塞,后台静默拉取
|
||||||
() => {},
|
void ensureCityMarketScan(
|
||||||
);
|
cityName,
|
||||||
|
false,
|
||||||
|
null,
|
||||||
|
detail.local_date,
|
||||||
|
).catch(() => {});
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setLoadingState((current) => ({ ...current, cityDetail: false }));
|
setLoadingState((current) => ({ ...current, cityDetail: false }));
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user