Make dashboard decision cards feel product-ready

Users needed reassurance that unavailable quotes and long AI evidence are normal states, not broken systems. This adds a v1.5.5 upgrade announcement, softens market-unavailable copy, surfaces a one-line recommendation reason, and makes mobile cards prioritize observed temperature, expected high, peak timing, AI expansion, and a separate market line.

Constraint: Keep existing dashboard data contracts and avoid backend schema changes.
Rejected: Hide unavailable market rows entirely | users still need to know weather evidence remains usable without a quote.
Confidence: high
Scope-risk: moderate
Reversibility: clean
Tested: npm run build
Not-tested: Browser visual QA on physical mobile devices.
This commit is contained in:
2569718930@qq.com
2026-04-28 07:48:47 +08:00
parent 326bfe258e
commit 1543207ced
5 changed files with 452 additions and 87 deletions
+5 -1
View File
@@ -2,8 +2,12 @@
## 1.5.5 - 2026-04-27
- Dashboard 新增 v1.5.5 升级公告,提示所有会员已额外延长 7 天,并集中说明 DeepSeek 机场报文解读、日历行动视图、本地时间峰值窗口和 AI 证据护栏
- 城市决策卡空状态与市场不可用文案产品化:将“未接入/缺失”改为“市场价格暂不可用,天气判断仍可参考”,避免用户误以为系统故障
- 城市决策卡新增“为什么推荐/为什么不推荐”短句,优先解释实测突破、峰值窗口已过、METAR 过旧、市场暂不可用或模型一致等关键原因
- 移动端城市决策卡前置当前温度、预测高点和峰值时间;长 AI 解读在手机端默认折叠,可展开查看,市场价格单独成行展示
- 新增 Qingdao / 青岛城市,结算锚点接入 Wunderground 青岛胶东国际机场 `ZSQD` 历史页,并补齐别名、时区、预热、官方来源和前端地区归类
- 城市决策卡顶部状态标签收口为 2-3 个高优先级信号,优先展示“实测突破 / 峰值窗口已过 / METAR 过旧 / AI 解读中 / 市场价格缺失 / 模型高度一致 / 需要等待下一报文”,让用户第一眼看到重点
- 城市决策卡顶部状态标签收口为 2-3 个高优先级信号,优先展示“实测突破 / 峰值窗口已过 / METAR 过旧 / AI 解读中 / 市场价暂不可用 / 模型高度一致 / 需要等待下一报文”,让用户第一眼看到重点
- 城市决策卡 AI 机场报文区明确拆分“快速判断已完成,AI 正在补充机场报文细节… / AI 机场报文解读已完成 / AI 解读未完整返回,当前使用规则证据”三种状态,减少 fallback 与流式返回造成的误解
- 城市决策卡新增“数据新鲜度”区块,分别展示 METAR/官方观测、模型、市场价格和 AI 状态;过旧观测会标明“仅作背景参考”
- 日历视图升级为行动视图,按“现在可看 / 1-3 小时内 / 今天稍后 / 已过峰值,等待确认”分组,并为每个城市显示一句核心原因
@@ -9384,6 +9384,68 @@
font-variant-numeric: tabular-nums;
}
.root :global(.scan-upgrade-announcement) {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 16px;
align-items: center;
margin: -4px 0 6px;
border: 1px solid rgba(77, 163, 255, 0.2);
border-radius: 18px;
background:
radial-gradient(circle at top left, rgba(77, 163, 255, 0.18), transparent 36%),
linear-gradient(135deg, rgba(18, 33, 58, 0.96), rgba(15, 23, 42, 0.92));
box-shadow: 0 18px 48px rgba(0, 0, 0, 0.2);
padding: 16px 18px;
}
.root :global(.scan-upgrade-announcement-copy) {
min-width: 0;
}
.root :global(.scan-upgrade-announcement-copy span) {
color: #86efac;
font-size: 12px;
font-weight: 900;
letter-spacing: 0.04em;
text-transform: uppercase;
}
.root :global(.scan-upgrade-announcement-copy strong) {
display: block;
margin-top: 5px;
color: #f3f8ff;
font-size: 18px;
line-height: 1.25;
}
.root :global(.scan-upgrade-announcement-copy p) {
margin: 6px 0 0;
color: #b4c8e2;
font-size: 13px;
line-height: 1.55;
}
.root :global(.scan-upgrade-announcement ul) {
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
gap: 8px;
margin: 0;
padding: 0;
list-style: none;
}
.root :global(.scan-upgrade-announcement li) {
border: 1px solid rgba(77, 163, 255, 0.22);
border-radius: 999px;
background: rgba(77, 163, 255, 0.1);
color: #d7e5f7;
font-size: 12px;
font-weight: 850;
padding: 7px 10px;
}
.root :global(.scan-ghost-button),
.root :global(.scan-cta-ghost),
.root :global(.scan-theme-button),
@@ -11314,6 +11376,10 @@
line-height: 1.1;
}
.root :global(.scan-ai-city-mobile-priority) {
display: none;
}
.root :global(.scan-ai-city-pills) {
display: flex;
flex-wrap: wrap;
@@ -11598,6 +11664,17 @@
line-height: 1.55;
}
.root :global(.scan-ai-decision-why) {
display: inline-flex;
max-width: 100%;
border: 1px solid rgba(77, 163, 255, 0.24);
border-radius: 12px;
background: rgba(77, 163, 255, 0.1);
color: #d8e7f8 !important;
font-weight: 900;
padding: 9px 11px;
}
.root :global(.scan-ai-decision-reasons) {
display: grid;
gap: 8px;
@@ -11632,6 +11709,10 @@
font-weight: 800;
}
.root :global(.scan-ai-market-mobile-line) {
display: none;
}
.root :global(.scan-ai-market-decision) {
display: grid;
grid-template-columns: minmax(0, 1fr) auto auto;
@@ -11753,6 +11834,41 @@
margin-bottom: 12px;
}
.root :global(.scan-ai-city-ai-read) {
overflow: hidden;
}
.root :global(.scan-ai-city-ai-read summary) {
cursor: pointer;
list-style: none;
}
.root :global(.scan-ai-city-ai-read summary::-webkit-details-marker) {
display: none;
}
.root :global(.scan-ai-city-ai-read summary::after) {
width: 18px;
height: 18px;
display: inline-grid;
place-items: center;
border-radius: 999px;
background: rgba(77, 163, 255, 0.12);
color: #9ecbff;
content: "⌄";
font-size: 12px;
line-height: 1;
transition: transform 0.18s ease;
}
.root :global(.scan-ai-city-ai-read[open] summary::after) {
transform: rotate(180deg);
}
.root :global(.scan-ai-city-section-body) {
margin-top: 2px;
}
.root :global(.scan-ai-city-section p) {
margin: 0 0 10px;
color: #9fb2c7;
@@ -14055,6 +14171,14 @@
align-items: flex-start;
}
.root :global(.scan-upgrade-announcement) {
grid-template-columns: 1fr;
}
.root :global(.scan-upgrade-announcement ul) {
justify-content: flex-start;
}
.root :global(.scan-table-header),
.root :global(.scan-table-row) {
grid-template-columns: 1fr;
@@ -14101,6 +14225,10 @@
grid-template-columns: 1fr;
}
.root :global(.scan-ai-market-decision) {
grid-template-columns: 1fr;
}
.root :global(.scan-opportunity-card-grid),
.root :global(.scan-opportunity-summary) {
grid-template-columns: 1fr;
@@ -14157,6 +14285,114 @@
flex-wrap: wrap;
}
.root :global(.scan-upgrade-announcement) {
padding: 14px;
}
.root :global(.scan-upgrade-announcement-copy strong) {
font-size: 16px;
}
.root :global(.scan-upgrade-announcement ul) {
gap: 6px;
}
.root :global(.scan-upgrade-announcement li) {
font-size: 11px;
padding: 6px 8px;
}
.root :global(.scan-ai-city-hero) {
padding: 16px;
}
.root :global(.scan-ai-city-hero h3) {
margin-bottom: 10px;
font-size: 24px;
}
.root :global(.scan-ai-city-mobile-priority) {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 8px;
margin: 0 0 12px;
}
.root :global(.scan-ai-city-mobile-priority span) {
display: grid;
gap: 4px;
border: 1px solid rgba(77, 163, 255, 0.18);
border-radius: 12px;
background: rgba(77, 163, 255, 0.09);
padding: 9px;
}
.root :global(.scan-ai-city-mobile-priority small) {
color: #9fb2c7;
font-size: 10px;
font-weight: 900;
}
.root :global(.scan-ai-city-mobile-priority b) {
color: #f3f8ff;
font-size: 13px;
line-height: 1.15;
}
.root :global(.scan-ai-city-pills) {
display: none;
}
.root :global(.scan-ai-city-freshness) {
grid-template-columns: 1fr;
}
.root :global(.scan-ai-decision-band) {
padding: 14px;
}
.root :global(.scan-ai-decision-band strong) {
font-size: 20px;
}
.root :global(.scan-ai-decision-long),
.root :global(.scan-ai-decision-reasons),
.root :global(.scan-ai-decision-risk) {
display: none;
}
.root :global(.scan-ai-market-mobile-line) {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-top: 12px;
border: 1px solid rgba(77, 163, 255, 0.18);
border-radius: 12px;
background: rgba(11, 18, 32, 0.48);
padding: 10px 12px;
}
.root :global(.scan-ai-market-mobile-line span) {
color: #9fb2c7;
font-size: 12px;
font-weight: 900;
}
.root :global(.scan-ai-market-mobile-line b) {
color: #e6edf3;
font-size: 13px;
text-align: right;
}
.root :global(.scan-ai-market-decision) {
margin-top: 10px;
}
.root :global(.scan-ai-city-ai-read:not([open])) {
padding-bottom: 4px;
}
.root :global(.scan-list-header) {
flex-direction: column;
align-items: flex-start;
@@ -14321,3 +14557,40 @@
.root:global(.light) :global(.scan-terminal.light .scan-ai-city-status-tag.red) {
color: #b91c1c !important;
}
.root:global(.light) :global(.scan-terminal.light .scan-upgrade-announcement) {
background:
radial-gradient(circle at top left, rgba(37, 99, 235, 0.12), transparent 36%),
#ffffff !important;
border-color: rgba(37, 99, 235, 0.16) !important;
box-shadow: 0 16px 36px rgba(61, 100, 145, 0.12) !important;
}
.root:global(.light) :global(.scan-terminal.light .scan-upgrade-announcement-copy strong),
.root:global(.light) :global(.scan-terminal.light .scan-ai-city-mobile-priority b),
.root:global(.light) :global(.scan-terminal.light .scan-ai-market-mobile-line b) {
color: #0f172a !important;
}
.root:global(.light) :global(.scan-terminal.light .scan-upgrade-announcement-copy p),
.root:global(.light) :global(.scan-terminal.light .scan-upgrade-announcement li),
.root:global(.light) :global(.scan-terminal.light .scan-ai-city-mobile-priority small),
.root:global(.light) :global(.scan-terminal.light .scan-ai-market-mobile-line span) {
color: #334155 !important;
}
.root:global(.light) :global(.scan-terminal.light .scan-upgrade-announcement-copy span) {
color: #047857 !important;
}
.root:global(.light) :global(.scan-terminal.light .scan-upgrade-announcement li),
.root:global(.light) :global(.scan-terminal.light .scan-ai-city-mobile-priority span),
.root:global(.light) :global(.scan-terminal.light .scan-ai-decision-why),
.root:global(.light) :global(.scan-terminal.light .scan-ai-market-mobile-line) {
background: #eef7ff !important;
border-color: rgba(37, 99, 235, 0.16) !important;
}
.root:global(.light) :global(.scan-terminal.light .scan-ai-decision-why) {
color: #1d4ed8 !important;
}
@@ -701,6 +701,26 @@ function ScanTerminalScreen() {
</div>
</div>
<section className="scan-upgrade-announcement" aria-label={isEn ? "Upgrade announcement" : "升级公告"}>
<div className="scan-upgrade-announcement-copy">
<span>{isEn ? "v1.5.5 upgrade" : "v1.5.5 升级公告"}</span>
<strong>
{isEn ? "PolyWeather is now upgraded to v1.5.5" : "网站已升级到 v1.5.5"}
</strong>
<p>
{isEn
? "All members received an extra 7 days. The decision terminal now explains more evidence without making unavailable quotes look like a system failure."
: "所有会员已额外延长 7 天。新版会把证据讲得更清楚,也不会让暂无报价看起来像系统故障。"}
</p>
</div>
<ul>
<li>{isEn ? "DeepSeek airport bulletin read" : "DeepSeek 机场报文解读"}</li>
<li>{isEn ? "Action calendar view" : "日历行动视图"}</li>
<li>{isEn ? "Local-time peak window" : "本地时间峰值窗口"}</li>
<li>{isEn ? "AI evidence guardrails" : "AI 证据护栏"}</li>
</ul>
</section>
<AiForecastKPIBar
pinnedCount={aiPinnedCities.length}
activeCityName={kpiCityName}
@@ -182,7 +182,7 @@ function buildMarketFreshnessValue({
marketStatus: ReturnType<typeof useCityMarketScan>["marketStatus"];
}) {
if (marketStatus === "loading") return isEn ? "syncing" : "同步中";
if (!marketScan?.available) return isEn ? "missing" : "缺失";
if (!marketScan?.available) return isEn ? "temporarily unavailable" : "暂不可用";
const quoteAgeMs = Number(
marketScan.quote_age_ms ??
marketScan.yes_token?.quote_age_ms ??
@@ -307,6 +307,16 @@ function AiPinnedCityCard({
enabled: Boolean(detail),
});
const isRefreshing = refreshingDetail || aiForecast.status === "loading";
const [isCompactCard, setIsCompactCard] = useState(false);
useEffect(() => {
if (typeof window === "undefined" || !window.matchMedia) return;
const media = window.matchMedia("(max-width: 820px)");
const syncCompactMode = () => setIsCompactCard(media.matches);
syncCompactMode();
media.addEventListener("change", syncCompactMode);
return () => media.removeEventListener("change", syncCompactMode);
}, []);
const aiCityForecast = aiForecast.payload?.city_forecast || null;
const localizedFinalJudgmentRaw =
@@ -376,6 +386,14 @@ function AiPinnedCityCard({
peakWindow,
tempSymbol,
});
const expectedHighText =
decisionExpectedHighNumber != null
? formatTemperatureValue(decisionExpectedHighNumber, tempSymbol, { digits: 1 })
: "--";
const currentTempText =
currentTempNumber != null
? formatTemperatureValue(currentTempNumber, tempSymbol, { digits: 1 })
: "--";
const marketDecisionView = buildMarketDecisionView({
expectedHigh: decisionExpectedHighNumber,
isEn,
@@ -540,7 +558,7 @@ function AiPinnedCityCard({
: null,
marketDecisionView.status === "unavailable"
? {
label: isEn ? "Market missing" : "市场价格缺失",
label: isEn ? "Market unavailable" : "市场价暂不可用",
tone: marketStatusTone,
}
: null,
@@ -574,6 +592,43 @@ function AiPinnedCityCard({
(isEn ? aiForecast.payload?.reason_en : aiForecast.payload?.reason_zh) ||
aiForecast.payload?.reason ||
"";
const decisionWhyText = observedHighBreak
? isEn
? "Worth watching now: observation has broken above the model range."
: "当前值得关注:实测已突破模型上沿。"
: peakHasPassed
? isEn
? "Avoid chasing now: peak window has passed; wait to confirm no new high."
: "当前不宜追高:峰值窗口已过,等待确认是否还有新高。"
: observationStale
? isEn
? "Use as background only: observation is stale and needs the next report."
: "当前仅作背景:观测已过旧,需要下一报文确认。"
: marketDecisionView.status === "unavailable"
? isEn
? "Weather evidence remains usable, but no tradable quote is available yet."
: "当前可参考天气:暂无可交易价格。"
: modelHighlyConsistent
? isEn
? "Worth watching now: models agree; wait for observation confirmation."
: "当前值得关注:模型高度一致,等待实测确认。"
: needsNextBulletin
? isEn
? "Wait for confirmation: the next bulletin should decide direction."
: "当前建议等待:下一报文更适合决定方向。"
: isEn
? "Watch the peak window and compare observations against the expected high."
: "当前重点:盯住峰值窗口,把实测与预计高点对照。";
const marketLineText =
marketDecisionView.status === "ready"
? `${marketDecisionView.bucketLabel} · ${marketDecisionView.priceText}`
: marketDecisionView.status === "loading"
? isEn
? "syncing"
: "同步中"
: isEn
? "temporarily unavailable"
: "暂不可用";
const collapseId = `ai-city-body-${normalizeCityKey(item.cityName) || item.addedAt}`;
@@ -585,6 +640,20 @@ function AiPinnedCityCard({
{isEn ? "Deep analysis" : "城市深度分析"}
</span>
<h3>{displayName}</h3>
<div className="scan-ai-city-mobile-priority" aria-label={isEn ? "Key city decision metrics" : "城市决策重点"}>
<span>
<small>{isEn ? "Observed" : "当前温度"}</small>
<b>{currentTempText}</b>
</span>
<span>
<small>{isEn ? "Expected high" : "预测高点"}</small>
<b>{expectedHighText}</b>
</span>
<span>
<small>{isEn ? "Peak" : "峰值时间"}</small>
<b>{peakWindow}</b>
</span>
</div>
<div className="scan-ai-city-status-tags">
{statusTags.map((tag) => (
<span
@@ -623,9 +692,7 @@ function AiPinnedCityCard({
<div className="scan-ai-city-hero-side">
<span>{isEn ? "Expected high" : "预计最高温"}</span>
<strong>
{decisionExpectedHighNumber != null
? formatTemperatureValue(decisionExpectedHighNumber, tempSymbol, { digits: 1 })
: "--"}
{expectedHighText}
</strong>
<div className="scan-ai-city-actions">
<button
@@ -687,13 +754,18 @@ function AiPinnedCityCard({
<div className="scan-ai-decision-main">
<span>{decisionView.kicker}</span>
<strong>{decisionView.action}</strong>
<p>{localizedFinalJudgment || paceText}</p>
<p className="scan-ai-decision-why">{decisionWhyText}</p>
<p className="scan-ai-decision-long">{localizedFinalJudgment || paceText}</p>
<div className="scan-ai-decision-reasons">
{decisionView.reasons.map((reason, index) => (
<small key={`${reason}-${index}`}>{reason}</small>
))}
</div>
<p className="scan-ai-decision-risk">{decisionView.risk}</p>
<div className={clsx("scan-ai-market-mobile-line", marketDecisionView.tone)}>
<span>{isEn ? "Market price" : "市场价格"}</span>
<b>{marketLineText}</b>
</div>
<div className={clsx("scan-ai-market-decision", marketDecisionView.tone)}>
<div>
<span>
@@ -740,11 +812,7 @@ function AiPinnedCityCard({
</span>
<span>
{isEn ? "Observed" : "实测"}
<b>
{currentTempNumber != null
? formatTemperatureValue(currentTempNumber, tempSymbol, { digits: 1 })
: "--"}
</b>
<b>{currentTempText}</b>
</span>
<span>
{isEn ? "Path delta" : "路径偏差"} <b>{paceView?.deltaText || "--"}</b>
@@ -766,8 +834,8 @@ function AiPinnedCityCard({
<div className="scan-ai-city-analysis-grid">
<AiCityTemperatureChart detail={detail} />
<section className="scan-ai-city-section">
<div className="scan-ai-city-section-title">
<details className="scan-ai-city-section scan-ai-city-ai-read" open={!isCompactCard}>
<summary className="scan-ai-city-section-title">
{isHkoObservation
? isEn
? "Evidence · AI HKO observation read"
@@ -775,78 +843,80 @@ function AiPinnedCityCard({
: isEn
? "Evidence · AI airport read"
: "证据 · AI 机场报文解读"}
</div>
{aiForecast.status === "loading" ? (
<>
<p className="scan-ai-weather-summary">
{aiReadInProgressText}
</p>
{localizedFinalJudgment || aiForecast.streamText ? (
<p className="scan-ai-city-muted">
{localizedFinalJudgment || aiForecast.streamText}
</summary>
<div className="scan-ai-city-section-body">
{aiForecast.status === "loading" ? (
<>
<p className="scan-ai-weather-summary">
{aiReadInProgressText}
</p>
) : null}
<p className="scan-ai-city-muted">
{localizedFinalJudgment || aiForecast.streamText ? (
<p className="scan-ai-city-muted">
{localizedFinalJudgment || aiForecast.streamText}
</p>
) : null}
<p className="scan-ai-city-muted">
{isEn
? isHkoObservation
? "Rule evidence is shown first; the full HKO AI read will merge automatically."
: "Rule evidence is shown first; the full airport AI read will merge automatically."
: isHkoObservation
? "先展示规则证据,完整香港天文台 AI 解读返回后会自动合并。"
: "先展示规则证据,完整机场 AI 解读返回后会自动合并。"}
</p>
</>
) : aiForecast.status === "ready" && aiCityForecast ? (
<>
<p className="scan-ai-weather-summary">
{aiRuleEvidenceMode ? aiRuleEvidenceText : aiReadCompleteText}
</p>
<ul className="scan-ai-weather-bullets">
{[
localizedFinalJudgment,
...aiBullets,
].filter((line) => String(line || "").trim()).map((line, index) => (
<li key={`${line}-${index}`}>{line}</li>
))}
</ul>
<p className="scan-ai-raw-metar">
{rawObservationText}
</p>
</>
) : aiForecast.status === "ready" ? (
<>
<p className="scan-ai-weather-summary">
{aiRuleEvidenceText}
</p>
<ul className="scan-ai-weather-bullets">
{fallbackAiReason ? <li>{fallbackAiReason}</li> : null}
<li>{localModelSupportNote}</li>
<li>{rawObservationText}</li>
</ul>
</>
) : aiForecast.status === "failed" ? (
<>
<p className="scan-ai-weather-summary">
{aiRuleEvidenceText}
</p>
<ul className="scan-ai-weather-bullets">
{aiForecast.error ? <li>{aiForecast.error}</li> : null}
<li>{localModelSupportNote}</li>
<li>{rawObservationText}</li>
</ul>
</>
) : (
<p>
{isEn
? isHkoObservation
? "Rule evidence is shown first; the full HKO AI read will merge automatically."
: "Rule evidence is shown first; the full airport AI read will merge automatically."
? "Waiting for AI to read the latest HKO observation."
: "Waiting for AI to read the latest airport bulletin."
: isHkoObservation
? "先展示规则证据,完整香港天文台 AI 解读返回后会自动合并。"
: "先展示规则证据,完整机场 AI 解读返回后会自动合并。"}
? "等待 AI 解读最新香港天文台观测。"
: "等待 AI 解读最新机场报文。"}
</p>
</>
) : aiForecast.status === "ready" && aiCityForecast ? (
<>
<p className="scan-ai-weather-summary">
{aiRuleEvidenceMode ? aiRuleEvidenceText : aiReadCompleteText}
</p>
<ul className="scan-ai-weather-bullets">
{[
localizedFinalJudgment,
...aiBullets,
].filter((line) => String(line || "").trim()).map((line, index) => (
<li key={`${line}-${index}`}>{line}</li>
))}
</ul>
<p className="scan-ai-raw-metar">
{rawObservationText}
</p>
</>
) : aiForecast.status === "ready" ? (
<>
<p className="scan-ai-weather-summary">
{aiRuleEvidenceText}
</p>
<ul className="scan-ai-weather-bullets">
{fallbackAiReason ? <li>{fallbackAiReason}</li> : null}
<li>{localModelSupportNote}</li>
<li>{rawObservationText}</li>
</ul>
</>
) : aiForecast.status === "failed" ? (
<>
<p className="scan-ai-weather-summary">
{aiRuleEvidenceText}
</p>
<ul className="scan-ai-weather-bullets">
{aiForecast.error ? <li>{aiForecast.error}</li> : null}
<li>{localModelSupportNote}</li>
<li>{rawObservationText}</li>
</ul>
</>
) : (
<p>
{isEn
? isHkoObservation
? "Waiting for AI to read the latest HKO observation."
: "Waiting for AI to read the latest airport bulletin."
: isHkoObservation
? "等待 AI 解读最新香港天文台观测。"
: "等待 AI 解读最新机场报文。"}
</p>
)}
</section>
)}
</div>
</details>
</div>
<section className="scan-ai-city-section models">
@@ -369,13 +369,11 @@ export function buildMarketDecisionView({
impliedText: "--",
modelText: "--",
priceText: "--",
reason:
marketScan?.reason ||
(isEn
? "No active matched Polymarket temperature market is available for this city yet."
: "该城市暂未匹配到可用的 Polymarket 温度市场。"),
reason: isEn
? "No tradable quote is available yet; weather evidence is still shown."
: "暂无可交易价格,仅展示天气证据。天气判断仍可参考。",
status: "unavailable",
title: isEn ? "No market quote" : "暂无市场报价",
title: isEn ? "Market price temporarily unavailable" : "市场价格暂不可用",
tone: "watch",
};
}