Files
PolyWeather/frontend/components/dashboard/scan-terminal/MobileDecisionCard.tsx
T
2569718930@qq.com 55099c3dd8 恢复城市决策卡温度曲线图
- AiPinnedCityCard: 重新引入 AiCityTemperatureChart 渲染
- MobileDecisionCard: 恢复温度走势图折叠区
2026-05-17 18:19:31 +08:00

202 lines
6.5 KiB
TypeScript

"use client";
import { RefreshCw, X } from "lucide-react";
import type { MouseEvent } from "react";
import { useState } from "react";
import { AiCityTemperatureChart } from "@/components/dashboard/scan-terminal/AiCityTemperatureChart";
import { AiEvidencePanel } from "@/components/dashboard/scan-terminal/AiEvidencePanel";
import {
CityStatusTags,
type CityStatusTag,
type StatusTone,
} from "@/components/dashboard/scan-terminal/CityStatusTags";
import { LoadingSignal } from "@/components/dashboard/scan-terminal/LoadingSignal";
import { MarketDecisionLine } from "@/components/dashboard/scan-terminal/MarketDecisionLine";
import { ModelEvidencePanel } from "@/components/dashboard/scan-terminal/ModelEvidencePanel";
import type { MarketDecisionView } from "@/components/dashboard/scan-terminal/city-card-decision-utils";
import type { CityDecisionState } from "@/components/dashboard/scan-terminal/city-decision-state";
import {
getCityLoadingCopy,
getMobileDecisionCopy,
} from "@/components/dashboard/scan-terminal/decision-copy";
import type {
AiCityForecastPayload,
AiCityForecastState,
} from "@/components/dashboard/scan-terminal/types";
import type { CityDetail } from "@/lib/dashboard-types";
export function MobileDecisionCard({
aiBullets,
aiCityForecast,
aiConfidence,
aiForecast,
aiPredictedMax,
aiRangeHigh,
aiRangeLow,
aiReadCompleteText,
aiReadInProgressText,
aiRuleEvidenceMode,
aiRuleEvidenceText,
debPrediction,
decisionState,
detail,
displayName,
expectedHighText,
fallbackAiReason,
isEn,
isHkoObservation,
isRefreshing,
localModelSupportNote,
localizedFinalJudgment,
marketDecisionView,
marketLineText,
onRefresh,
onRemove,
peakWindow,
removing,
tempSymbol,
}: {
aiBullets: string[];
aiCityForecast: AiCityForecastPayload["city_forecast"] | null;
aiConfidence: string | null;
aiForecast: AiCityForecastState;
aiPredictedMax: number | null;
aiRangeHigh: number | null;
aiRangeLow: number | null;
aiReadCompleteText: string;
aiReadInProgressText: string;
aiRuleEvidenceMode: boolean;
aiRuleEvidenceText: string;
debPrediction: number | null;
decisionState: CityDecisionState;
detail: CityDetail | null;
displayName: string;
expectedHighText: string;
fallbackAiReason: string;
isEn: boolean;
isHkoObservation: boolean;
isRefreshing: boolean;
localModelSupportNote: string;
localizedFinalJudgment: string;
marketDecisionView: MarketDecisionView;
marketLineText: string;
onRefresh: (event: MouseEvent<HTMLButtonElement>) => void;
onRemove: (event: MouseEvent<HTMLButtonElement>) => void;
peakWindow: string;
removing?: boolean;
tempSymbol: string;
}) {
const copy = getMobileDecisionCopy(isEn);
const loadingCopy = getCityLoadingCopy({ isEn, isHkoObservation });
const [modelOpen, setModelOpen] = useState(false);
const [chartOpen, setChartOpen] = useState(false);
const statusTags: CityStatusTag[] = decisionState.badges.length
? decisionState.badges
: [{ label: decisionState.aiStatusLabel, tone: decisionState.aiStatusTone as StatusTone }];
return (
<>
<header className="scan-mobile-decision-head">
<div>
<span className="scan-ai-city-kicker">
{isEn ? "Mobile action card" : "移动端行动卡"}
</span>
<h3>{displayName}</h3>
</div>
<div className="scan-ai-city-actions">
<button
type="button"
className="scan-ai-city-icon-button"
onClick={onRefresh}
aria-label={`${copy.refresh} ${displayName}`}
title={copy.refresh}
disabled={isRefreshing}
>
<RefreshCw size={15} className={isRefreshing ? "spin" : undefined} />
</button>
<button
type="button"
className="scan-ai-city-icon-button danger"
onClick={onRemove}
aria-label={`${copy.remove} ${displayName}`}
title={copy.remove}
disabled={removing}
>
<X size={15} />
</button>
</div>
</header>
<div className="scan-mobile-decision-metrics">
<span className="primary">
<small>{copy.expectedHigh}</small>
<b>{expectedHighText}</b>
</span>
<span>
<small>{copy.peakWindow}</small>
<b>{peakWindow}</b>
</span>
</div>
<p className="scan-mobile-decision-reason">{decisionState.primaryReason}</p>
<CityStatusTags tags={statusTags} />
<MarketDecisionLine
isEn={isEn}
marketDecisionView={marketDecisionView}
marketLineText={marketLineText}
/>
{!detail ? (
<div className="scan-ai-city-loading">
<LoadingSignal
title={loadingCopy.title}
description={loadingCopy.description}
compact
/>
</div>
) : (
<div className="scan-mobile-decision-folds">
<AiEvidencePanel
aiBullets={aiBullets}
aiCityForecast={aiCityForecast}
aiConfidence={aiConfidence}
aiForecast={aiForecast}
aiPredictedMax={aiPredictedMax}
aiRangeHigh={aiRangeHigh}
aiRangeLow={aiRangeLow}
aiReadCompleteText={aiReadCompleteText}
aiReadInProgressText={aiReadInProgressText}
aiRuleEvidenceMode={aiRuleEvidenceMode}
aiRuleEvidenceText={aiRuleEvidenceText}
debPrediction={debPrediction}
fallbackAiReason={fallbackAiReason}
isEn={isEn}
isHkoObservation={isHkoObservation}
localModelSupportNote={localModelSupportNote}
localizedFinalJudgment={localizedFinalJudgment}
tempSymbol={tempSymbol}
/>
<details
className="scan-ai-city-section scan-mobile-fold"
open={modelOpen}
onToggle={(event) => setModelOpen(event.currentTarget.open)}
>
<summary className="scan-ai-city-section-title">{copy.modelEvidence}</summary>
{modelOpen ? <ModelEvidencePanel detail={detail} isEn={isEn} /> : null}
</details>
<details
className="scan-ai-city-section scan-mobile-fold"
open={chartOpen}
onToggle={(event) => setChartOpen(event.currentTarget.open)}
>
<summary className="scan-ai-city-section-title">{copy.chart}</summary>
{chartOpen ? <AiCityTemperatureChart detail={detail} /> : null}
</details>
</div>
)}
</>
);
}