重新设计日历视图:以用户本地时区为主视角
- CalendarView 组件重写:添加实时用户时钟头部(HH:MM:SS + 完整日期) - 卡片重新设计:用户本地时间为主要展示,城市窗口时间为次要 - 新增 urgency badge(进行中/即将/稍后/已过)视觉标识 - 优化卡片布局:顶部城市+徽标 → 用户时间 → 倒计时 → 原因 → 底部 DEB+阶段 - CSS 全面重写:用户时钟头部、卡片分层、计时器颜色编码 - 浅色主题同步更新所有新 class 名称
This commit is contained in:
@@ -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>
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user