diff --git a/frontend/components/dashboard/scan-terminal/CalendarView.tsx b/frontend/components/dashboard/scan-terminal/CalendarView.tsx
index d76fee56..af76e189 100644
--- a/frontend/components/dashboard/scan-terminal/CalendarView.tsx
+++ b/frontend/components/dashboard/scan-terminal/CalendarView.tsx
@@ -1,4 +1,4 @@
-import { useEffect, useMemo, useState } from "react";
+import { memo, useEffect, useMemo, useState } from "react";
import { getWindowPhaseMeta } from "@/components/dashboard/OpportunityTable";
import type { ScanOpportunityRow } from "@/lib/dashboard-types";
import { getLocalizedCityName } from "@/lib/dashboard-home-copy";
@@ -26,6 +26,12 @@ type CalendarActionGroup = {
sort: number;
};
+type CalendarActionItem = {
+ row: ScanOpportunityRow;
+ meta: CalendarMeta;
+ reason: string;
+};
+
function normalizeCalendarCityKey(value?: string | null) {
return String(value || "")
.trim()
@@ -291,6 +297,66 @@ function buildCalendarCoreReason(
: "先放入行动面板,等待下一轮观测";
}
+const CalendarActionCard = memo(function CalendarActionCard({
+ item,
+ locale,
+ selected,
+ onSelectRow,
+}: {
+ item: CalendarActionItem;
+ locale: string;
+ selected: boolean;
+ onSelectRow: (row: ScanOpportunityRow) => void;
+}) {
+ const { row, meta, reason } = item;
+ const tempSymbol = row.temp_symbol || "°C";
+ const phaseMeta = getWindowPhaseMeta(row, locale);
+
+ return (
+
+ );
+});
+
export function CalendarView({
rows,
locale,
@@ -324,7 +390,7 @@ export function CalendarView({
label: string;
subtitle: string;
sort: number;
- items: Array<{ row: ScanOpportunityRow; meta: CalendarMeta; reason: string }>;
+ items: CalendarActionItem[];
}
>();
dedupeCalendarRows(rows).forEach((row) => {
@@ -385,57 +451,14 @@ export function CalendarView({
- {group.items.map(({ row, meta, reason }) => (
-
+ {group.items.map((item) => (
+
))}
diff --git a/frontend/components/dashboard/scan-terminal/OpportunityOverview.tsx b/frontend/components/dashboard/scan-terminal/OpportunityOverview.tsx
index 638af1e8..3e5b567f 100644
--- a/frontend/components/dashboard/scan-terminal/OpportunityOverview.tsx
+++ b/frontend/components/dashboard/scan-terminal/OpportunityOverview.tsx
@@ -1,4 +1,4 @@
-import { useMemo } from "react";
+import { memo, useMemo } from "react";
import clsx from "clsx";
import type { ScanOpportunityRow, ScanTerminalResponse } from "@/lib/dashboard-types";
import { formatTemperatureValue } from "@/lib/dashboard-utils";
@@ -13,7 +13,7 @@ import {
pickOpportunitySections,
} from "@/components/dashboard/scan-terminal/decision-utils";
-function OpportunityDecisionCard({
+const OpportunityDecisionCard = memo(function OpportunityDecisionCard({
row,
locale,
selected,
@@ -91,7 +91,7 @@ function OpportunityDecisionCard({
);
-}
+});
export function OpportunityOverview({
rows,
@@ -116,7 +116,10 @@ export function OpportunityOverview({
}) {
const isEn = locale === "en-US";
const sections = useMemo(() => pickOpportunitySections(rows, locale), [locale, rows]);
- const visibleSections = sections.filter((section) => section.rows.length > 0);
+ const visibleSections = useMemo(
+ () => sections.filter((section) => section.rows.length > 0),
+ [sections],
+ );
const summary = terminalData?.summary;
if (loading) {