重新设计日历视图:以用户本地时区为主视角

- CalendarView 组件重写:添加实时用户时钟头部(HH:MM:SS + 完整日期)
- 卡片重新设计:用户本地时间为主要展示,城市窗口时间为次要
- 新增 urgency badge(进行中/即将/稍后/已过)视觉标识
- 优化卡片布局:顶部城市+徽标 → 用户时间 → 倒计时 → 原因 → 底部 DEB+阶段
- CSS 全面重写:用户时钟头部、卡片分层、计时器颜色编码
- 浅色主题同步更新所有新 class 名称
This commit is contained in:
2569718930@qq.com
2026-05-10 15:34:46 +08:00
parent 0e529358c9
commit 5ff9fade5f
3 changed files with 402 additions and 171 deletions
@@ -7,12 +7,27 @@ import {
buildCalendarCoreReason,
buildCalendarMeta,
dedupeCalendarRows,
formatCalendarCardShortDate,
getCalendarActionGroup,
isCalendarActionable,
type CalendarActionItem,
} from "@/components/dashboard/scan-terminal/calendar-action-utils";
function formatUserClock(now: Date) {
const h = String(now.getHours()).padStart(2, "0");
const m = String(now.getMinutes()).padStart(2, "0");
const s = String(now.getSeconds()).padStart(2, "0");
return `${h}:${m}:${s}`;
}
function formatUserFullDate(now: Date, locale: string) {
return now.toLocaleDateString(locale === "en-US" ? "en-US" : "zh-CN", {
weekday: "long",
year: "numeric",
month: "long",
day: "numeric",
});
}
const CalendarActionCard = memo(function CalendarActionCard({
item,
locale,
@@ -27,6 +42,7 @@ const CalendarActionCard = memo(function CalendarActionCard({
const { row, meta, reason } = item;
const tempSymbol = row.temp_symbol || "°C";
const phaseMeta = getWindowPhaseMeta(row, locale);
const isEn = locale === "en-US";
return (
<button
@@ -34,41 +50,60 @@ const CalendarActionCard = memo(function CalendarActionCard({
className={`scan-calendar-card peak-${meta.tone} ${selected ? "selected" : ""}`}
onClick={() => onSelectRow(row)}
>
<div className="scan-calendar-city">
{getLocalizedCityName(
row.city,
row.city_display_name || row.display_name || row.city,
locale,
)}
</div>
<div className="scan-calendar-countdown">
{meta.title}
{meta.localWindowLabel ? (
<small>
{locale === "en-US" ? "Your time: " : "本地时间:"}
{meta.localWindowLabel}
</small>
) : null}
<small>
{locale === "en-US" ? "City window: " : "城市窗口:"}
{meta.cityWindowLabel || meta.detail}
</small>
</div>
<p className="scan-calendar-reason">{reason}</p>
<div className="scan-calendar-action">
<span>{locale === "en-US" ? "DEB high" : "DEB 预测高点"}</span>
<b>
{row.deb_prediction != null
? formatTemperatureValue(row.deb_prediction, tempSymbol)
: "--"}
</b>
</div>
<div className="scan-calendar-meta">
<span>
{formatCalendarCardShortDate(row, locale)} · {row.local_time || "--"}
<div className="scan-calendar-card-top">
<div className="scan-calendar-city">
{getLocalizedCityName(
row.city,
row.city_display_name || row.display_name || row.city,
locale,
)}
</div>
<span className={`scan-calendar-badge tone-${meta.tone}`}>
{meta.tone === "active"
? isEn ? "NOW" : "进行中"
: meta.tone === "upcoming"
? isEn ? "SOON" : "即将"
: meta.tone === "later"
? isEn ? "LATER" : "稍后"
: isEn ? "PAST" : "已过"}
</span>
<span>{phaseMeta.label}</span>
</div>
<div className="scan-calendar-user-time">
<span className="scan-calendar-user-label">
{isEn ? "Your time" : "本地时间"}
</span>
<strong>{meta.localWindowLabel || meta.detail}</strong>
</div>
<div className="scan-calendar-countdown">
<span className={`scan-calendar-countdown-timer tone-${meta.tone}`}>
{meta.title}
</span>
</div>
<p className="scan-calendar-reason">{reason}</p>
<div className="scan-calendar-card-foot">
<div className="scan-calendar-deb">
<span>{isEn ? "DEB high" : "DEB 预测高点"}</span>
<b>
{row.deb_prediction != null
? formatTemperatureValue(row.deb_prediction, tempSymbol)
: "--"}
</b>
</div>
<div className="scan-calendar-phase">
<span>{phaseMeta.label}</span>
</div>
</div>
{meta.cityWindowLabel && meta.cityWindowLabel !== meta.detail ? (
<div className="scan-calendar-city-time">
{isEn ? "City window: " : "城市窗口:"}
{meta.cityWindowLabel}
</div>
) : null}
</button>
);
});
@@ -95,7 +130,7 @@ export const CalendarView = memo(function CalendarView({
useEffect(() => {
const intervalId = window.setInterval(() => {
setNowMs(Date.now());
}, 60_000);
}, 30_000);
return () => window.clearInterval(intervalId);
}, []);
@@ -139,47 +174,68 @@ export const CalendarView = memo(function CalendarView({
}));
}, [locale, nowMs, rows, snapshotMs]);
const now = new Date(nowMs);
const isEn = locale === "en-US";
if (!groups.length) {
return (
<div className="scan-empty-state compact">
<div className="scan-empty-title">
{locale === "en-US"
? "No actionable calendar windows in the next 12 hours"
<div className="scan-calendar-empty">
<div className="scan-calendar-empty-title">
{isEn
? "No actionable windows in the next 12 hours"
: "未来 12 小时内没有可行动日历窗口"}
</div>
<div className="scan-calendar-empty-sub">
{isEn
? "Check back after the next observation cycle"
: "等待下一轮观测后再查看"}
</div>
</div>
);
}
return (
<div className="scan-calendar-view">
{groups.map((group) => (
<section key={group.key} className="scan-calendar-group">
<div className="scan-calendar-group-head">
<div>
<div className="scan-calendar-date">{group.label}</div>
<div className="scan-calendar-subtitle">
{group.subtitle}
<div className="scan-calendar-header">
<div className="scan-calendar-clock">
<span className="scan-calendar-clock-time">
{formatUserClock(now)}
</span>
<span className="scan-calendar-clock-date">
{formatUserFullDate(now, locale)}
</span>
</div>
<div className="scan-calendar-clock-meta">
{isEn ? "Your local time · Auto-refreshes" : "您所在时区 · 自动刷新"}
</div>
</div>
<div className="scan-calendar-groups">
{groups.map((group) => (
<section key={group.key} className="scan-calendar-group">
<div className="scan-calendar-group-head">
<div>
<div className="scan-calendar-group-label">{group.label}</div>
<div className="scan-calendar-group-sub">{group.subtitle}</div>
</div>
<div className="scan-calendar-group-count">
{group.items.length}
</div>
</div>
<div className="scan-calendar-count">
{locale === "en-US" ? `${group.items.length} rows` : `${group.items.length}`}
<div className="scan-calendar-grid">
{group.items.map((item) => (
<CalendarActionCard
key={item.row.id}
item={item}
locale={locale}
selected={selectedRowId === item.row.id}
onSelectRow={onSelectRow}
/>
))}
</div>
</div>
<div className="scan-calendar-grid">
{group.items.map((item) => (
<CalendarActionCard
key={item.row.id}
item={item}
locale={locale}
selected={selectedRowId === item.row.id}
onSelectRow={onSelectRow}
/>
))}
</div>
</section>
))}
</section>
))}
</div>
</div>
);
});