Release v1.5.1 and improve mobile dashboard layouts
This commit is contained in:
+14
-6
@@ -1,15 +1,23 @@
|
||||
# Changelog
|
||||
|
||||
## Unreleased
|
||||
## 1.5.1 - 2026-03-23
|
||||
|
||||
- 新增 `/ops` 轻量运营后台,支持系统状态、会员列表、用户查询、周榜、手动补分
|
||||
- `/ops` 新增支付异常单,支持按原因筛选并手动标记“已处理”
|
||||
- 会员列表支持按 `user_id` 去重,并回补 Supabase Auth 邮箱/注册时间
|
||||
- `/ops` 页面增加管理员守卫,前后端双层限制管理员访问
|
||||
- `/ops` 支持会员列表、支付异常单、用户查询、周榜和手动补分
|
||||
- `/ops` 支付异常单支持按原因筛选、标记已处理,并补充支付异常审计视图
|
||||
- 会员列表支持按 `user_id` 去重,并优先回补 Supabase Auth 邮箱/注册时间
|
||||
- 新增按邮箱补跑订阅恢复脚本 `scripts/reconcile_subscription_by_email.py`
|
||||
- 支付确认失败(如 `receiver_mismatch`)现在会明确落 `failed`,并写入 SQLite 审计事件
|
||||
- 账户页支付前强制重新拉取 `/api/payments/config`,并对 `tx_payload.to` 做最新地址校验
|
||||
- 支付前强制重新拉取 `/api/payments/config`,并校验最新地址、允许域名和当前支付上下文
|
||||
- 浏览器钱包选择补齐 EIP-6963 发现、稳定去重和绑定后账户状态即时刷新
|
||||
- 城市详情页新增 `官方参考 / Official Sources` 区块,覆盖主要城市的官方机构/机场/METAR 链接
|
||||
- 前端钱包选择补齐 EIP-6963 发现、稳定去重和绑定后账户状态即时刷新
|
||||
- “今日日内分析”结构解读改为后端同源动态短评,并统一网页与 Bot 解释口径
|
||||
- 台北主结算源切换到 `NOAA RCTP`,按最终质控后的最高整度摄氏值展示和说明
|
||||
- 浏览器插件同步台北 `NOAA RCTP` 结算参考标签和说明
|
||||
- `/ops` 手机端收口为卡片化视图,保留桌面表格
|
||||
|
||||
|
||||
## Unreleased
|
||||
|
||||
## 1.5.0 - 2026-03-21
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# 前端交付与重构报告(v1.5.0)
|
||||
# 前端交付与重构报告(v1.5.1)
|
||||
|
||||
最后更新:`2026-03-14`
|
||||
|
||||
|
||||
@@ -162,5 +162,5 @@ docker compose logs -f polyweather | egrep "polymarket wallet activity watcher s
|
||||
|
||||
## Version
|
||||
|
||||
- Version: `v1.5.0`
|
||||
- Version: `v1.5.1`
|
||||
- Last Updated: `2026-03-21`
|
||||
|
||||
+1
-1
@@ -177,5 +177,5 @@ docker compose logs -f polyweather | egrep "polymarket wallet activity watcher s
|
||||
|
||||
## 当前版本
|
||||
|
||||
- 版本:`v1.5.0`
|
||||
- 版本:`v1.5.1`
|
||||
- 文档最后更新:`2026-03-21`
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
# PolyWeather API 文档(v1.5.0)
|
||||
# PolyWeather API 文档(v1.5.1)
|
||||
|
||||
最后更新:`2026-03-21`
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Supabase + 登录 + 支付接入说明(v1.5.0)
|
||||
# Supabase + 登录 + 支付接入说明(v1.5.1)
|
||||
|
||||
最后更新:`2026-03-14`
|
||||
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
# 技术债与工程待办(v1.5.0)
|
||||
# 技术债与工程待办(v1.5.1)
|
||||
|
||||
最后更新:`2026-03-20`
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# 技术债与工程待办(v1.5.0)
|
||||
# 技术债与工程待办(v1.5.1)
|
||||
|
||||
最后更新:`2026-03-20`
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# PolyWeatherCheckout PolygonScan 验证(v1.5.0)
|
||||
# PolyWeatherCheckout PolygonScan 验证(v1.5.1)
|
||||
|
||||
最后更新:`2026-03-20`
|
||||
|
||||
|
||||
@@ -390,6 +390,10 @@
|
||||
.cards {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.metaList dd {
|
||||
max-width: 56%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 560px) {
|
||||
@@ -414,5 +418,74 @@
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.ghostBtn,
|
||||
.primaryBtn {
|
||||
min-height: 40px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.heroCard {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 16px;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.heroMain h2 {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.updatedText {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.metaList > div {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.metaList dd {
|
||||
max-width: 100%;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.commandRow {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.command {
|
||||
flex: 1 1 auto;
|
||||
width: 100%;
|
||||
min-height: 48px;
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.copyBtn {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.noticeRow,
|
||||
.errorRow {
|
||||
width: 100%;
|
||||
align-items: flex-start;
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1392,8 +1392,58 @@
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.root :global(.header) {
|
||||
height: auto;
|
||||
min-height: var(--header-height);
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.root :global(.brand) {
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.root :global(.header-right) {
|
||||
flex: 1 1 auto;
|
||||
justify-content: flex-end;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.root :global(.lang-switch) {
|
||||
order: 1;
|
||||
}
|
||||
|
||||
.root :global(.account-btn),
|
||||
.root :global(.info-btn) {
|
||||
padding: 6px 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.root :global(.live-badge) {
|
||||
order: 4;
|
||||
padding: 4px 10px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.root :global(.refresh-btn) {
|
||||
order: 5;
|
||||
}
|
||||
|
||||
.root :global(.city-list) {
|
||||
display: none;
|
||||
display: flex;
|
||||
top: auto;
|
||||
left: 8px;
|
||||
right: 8px;
|
||||
bottom: 8px;
|
||||
width: auto;
|
||||
max-height: min(36vh, 320px);
|
||||
border-radius: 14px;
|
||||
z-index: 920;
|
||||
}
|
||||
.root :global(.detail-panel) {
|
||||
width: 100%;
|
||||
@@ -1401,15 +1451,97 @@
|
||||
.root {
|
||||
--panel-width: 100%;
|
||||
}
|
||||
|
||||
.root :global(.city-list-header) {
|
||||
padding: 12px 14px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.root :global(.city-list-items) {
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.root :global(.city-group-header) {
|
||||
padding: 8px 9px;
|
||||
}
|
||||
|
||||
.root :global(.city-item) {
|
||||
padding: 8px 9px;
|
||||
}
|
||||
|
||||
.root :global(.city-item .city-name-text),
|
||||
.root :global(.city-item .city-temp) {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.root :global(.city-item-info) {
|
||||
padding-left: 18px;
|
||||
flex-wrap: wrap;
|
||||
row-gap: 4px;
|
||||
}
|
||||
|
||||
.root :global(.panel-header) {
|
||||
padding: 16px 16px 14px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.root :global(.header) {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 8px;
|
||||
padding: 8px 10px;
|
||||
}
|
||||
|
||||
.root :global(.brand) {
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.root :global(.subtitle) {
|
||||
display: none;
|
||||
}
|
||||
.root :global(.brand h1) {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.root :global(.header-right) {
|
||||
width: 100%;
|
||||
justify-content: flex-start;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.root :global(.lang-switch) {
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.root :global(.account-btn),
|
||||
.root :global(.info-btn) {
|
||||
min-height: 34px;
|
||||
padding: 6px 9px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.root :global(.account-btn span),
|
||||
.root :global(.info-btn span) {
|
||||
max-width: 88px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.root :global(.live-badge) {
|
||||
padding: 4px 8px;
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.6px;
|
||||
}
|
||||
|
||||
.root :global(.refresh-btn) {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.root :global(.hero-value) {
|
||||
font-size: 42px;
|
||||
}
|
||||
@@ -1420,6 +1552,47 @@
|
||||
.root :global(.hero-item .value) {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.root :global(.city-list) {
|
||||
left: 6px;
|
||||
right: 6px;
|
||||
bottom: 6px;
|
||||
max-height: min(40vh, 300px);
|
||||
}
|
||||
|
||||
.root :global(.city-list-header) {
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.root :global(.city-group-title) {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.root :global(.city-item-main) {
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.root :global(.city-item-info) {
|
||||
font-size: 9px;
|
||||
}
|
||||
|
||||
.root :global(.panel-header) {
|
||||
padding: 14px 14px 12px;
|
||||
}
|
||||
|
||||
.root :global(.city-marker:hover) {
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.root :global(.marker-bubble) {
|
||||
min-width: 48px;
|
||||
min-height: 32px;
|
||||
padding: 6px 10px;
|
||||
}
|
||||
|
||||
.root :global(.marker-name) {
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── History Modal ── */
|
||||
@@ -1490,6 +1663,7 @@
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
margin: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
.root :global(.future-modal-title-with-actions) {
|
||||
display: flex;
|
||||
@@ -1658,6 +1832,32 @@
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.root :global(.modal-content) {
|
||||
border-radius: 14px;
|
||||
max-height: calc(100vh - 16px);
|
||||
}
|
||||
|
||||
.root :global(.modal-content.history-modal),
|
||||
.root :global(.modal-content.large.future-modal) {
|
||||
width: min(100vw - 12px, 100%);
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.root :global(.modal-header) {
|
||||
padding: 14px 14px 12px;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.root :global(.modal-header h2) {
|
||||
font-size: 15px;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.root :global(.modal-body) {
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.root :global(.history-modal .history-stats) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
@@ -2195,6 +2395,93 @@
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.root :global(.future-modal-body) {
|
||||
gap: 12px;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.root :global(.future-v2-card),
|
||||
.root :global(.future-modal-section) {
|
||||
border-radius: 12px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.root :global(.future-v2-hero-main) {
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.root :global(.future-v2-hero-temp) {
|
||||
font-size: 44px;
|
||||
}
|
||||
|
||||
.root :global(.future-v2-hero-divider) {
|
||||
height: 44px;
|
||||
}
|
||||
|
||||
.root :global(.future-v2-hero-obs) {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.root :global(.future-v2-mini-grid) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.root :global(.future-v2-mini-item strong) {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.root :global(.future-v2-main-chart) {
|
||||
min-height: 280px;
|
||||
}
|
||||
|
||||
.root :global(.future-chart-wrapper) {
|
||||
height: 190px;
|
||||
}
|
||||
|
||||
.root :global(.future-trend-card) {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.root :global(.future-trend-value) {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.root :global(.future-trend-note),
|
||||
.root :global(.future-text-block) {
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.root :global(.future-front-meta) {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.root :global(.future-front-pill) {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.root :global(.detail-grid) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.root :global(.detail-mini-chart) {
|
||||
height: 170px;
|
||||
}
|
||||
|
||||
.root :global(.detail-source-link) {
|
||||
padding: 10px 12px;
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.root :global(.detail-source-label) {
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.root :global(.future-trend-grid) {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
|
||||
@@ -142,6 +142,23 @@ async function readJson<T>(url: string): Promise<T> {
|
||||
return response.json() as Promise<T>;
|
||||
}
|
||||
|
||||
function MobileField({
|
||||
label,
|
||||
value,
|
||||
mono = false,
|
||||
}: {
|
||||
label: string;
|
||||
value: string;
|
||||
mono?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div className="rounded-xl border border-slate-800 bg-slate-950/60 px-3 py-2">
|
||||
<div className="text-[11px] font-bold uppercase tracking-[0.14em] text-slate-500">{label}</div>
|
||||
<div className={`mt-1 break-all text-sm text-slate-200 ${mono ? "font-mono text-xs" : ""}`}>{value}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function OpsDashboard() {
|
||||
const [health, setHealth] = useState<HealthPayload | null>(null);
|
||||
const [status, setStatus] = useState<SystemStatusPayload | null>(null);
|
||||
@@ -328,9 +345,9 @@ export function OpsDashboard() {
|
||||
}, [grantEmail, grantPoints, incidentReasonFilter, loadLeaderboard, loadMemberships, loadPaymentIncidents, loadUsers, searchQuery]);
|
||||
|
||||
return (
|
||||
<main className="min-h-screen bg-slate-950 px-4 py-8 text-slate-100 sm:px-6 lg:px-8">
|
||||
<div className="mx-auto flex max-w-7xl flex-col gap-6">
|
||||
<section className="flex flex-col gap-4 rounded-3xl border border-slate-800 bg-slate-900/80 p-6 shadow-2xl backdrop-blur-xl lg:flex-row lg:items-end lg:justify-between">
|
||||
<main className="min-h-screen bg-slate-950 px-3 py-6 text-slate-100 sm:px-6 sm:py-8 lg:px-8">
|
||||
<div className="mx-auto flex max-w-7xl flex-col gap-5 sm:gap-6">
|
||||
<section className="flex flex-col gap-4 rounded-2xl border border-slate-800 bg-slate-900/80 p-4 shadow-2xl backdrop-blur-xl sm:rounded-3xl sm:p-6 lg:flex-row lg:items-end lg:justify-between">
|
||||
<div className="space-y-3">
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<Badge variant="secondary">Ops</Badge>
|
||||
@@ -344,20 +361,20 @@ export function OpsDashboard() {
|
||||
EMOS {rolloutDecision?.decision || "hold"}
|
||||
</Badge>
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-3xl font-black tracking-tight">PolyWeather Ops</h1>
|
||||
<p className="mt-2 max-w-3xl text-sm text-slate-400">
|
||||
直接挂在现有域名下的轻量运营页。先做只读运维视图,把系统状态、支付运行态和当前登录态聚合起来。
|
||||
</p>
|
||||
<div>
|
||||
<h1 className="text-2xl font-black tracking-tight sm:text-3xl">PolyWeather Ops</h1>
|
||||
<p className="mt-2 max-w-3xl text-sm text-slate-400">
|
||||
直接挂在现有域名下的轻量运营页。先做只读运维视图,把系统状态、支付运行态和当前登录态聚合起来。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col items-stretch gap-3 text-xs text-slate-400 sm:flex-row sm:flex-wrap sm:items-center">
|
||||
<span className="break-all">刷新时间: {formatDateTime(refreshedAt)}</span>
|
||||
<Button onClick={() => void load()} disabled={loading} className="gap-2 sm:w-auto">
|
||||
<RefreshCcw className="h-4 w-4" />
|
||||
{loading ? "加载中" : "刷新"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-3 text-xs text-slate-400">
|
||||
<span>刷新时间: {formatDateTime(refreshedAt)}</span>
|
||||
<Button onClick={() => void load()} disabled={loading} className="gap-2">
|
||||
<RefreshCcw className="h-4 w-4" />
|
||||
{loading ? "加载中" : "刷新"}
|
||||
</Button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{error ? (
|
||||
@@ -507,17 +524,17 @@ export function OpsDashboard() {
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<CardTitle>支付异常单</CardTitle>
|
||||
<CardDescription>只显示已明确标记失败的支付确认事故。</CardDescription>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<select
|
||||
className="rounded-xl border border-slate-700 bg-slate-950 px-3 py-2 text-sm text-slate-200"
|
||||
value={incidentReasonFilter}
|
||||
onChange={(event) => {
|
||||
const next = event.target.value;
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<CardTitle>支付异常单</CardTitle>
|
||||
<CardDescription>只显示已明确标记失败的支付确认事故。</CardDescription>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 sm:flex-row sm:items-center">
|
||||
<select
|
||||
className="w-full rounded-xl border border-slate-700 bg-slate-950 px-3 py-2 text-sm text-slate-200 sm:w-auto"
|
||||
value={incidentReasonFilter}
|
||||
onChange={(event) => {
|
||||
const next = event.target.value;
|
||||
setIncidentReasonFilter(next);
|
||||
void loadPaymentIncidents(next);
|
||||
}}
|
||||
@@ -528,15 +545,46 @@ export function OpsDashboard() {
|
||||
<option value="event_mismatch">event_mismatch</option>
|
||||
<option value="tx_reverted">tx_reverted</option>
|
||||
</select>
|
||||
<Button variant="secondary" onClick={() => void loadPaymentIncidents(incidentReasonFilter)} disabled={incidentsLoading}>
|
||||
{incidentsLoading ? "加载中" : "刷新异常"}
|
||||
</Button>
|
||||
<Button variant="secondary" onClick={() => void loadPaymentIncidents(incidentReasonFilter)} disabled={incidentsLoading} className="w-full sm:w-auto">
|
||||
{incidentsLoading ? "加载中" : "刷新异常"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="overflow-x-auto rounded-2xl border border-slate-800 bg-slate-950/70">
|
||||
<table className="min-w-full divide-y divide-slate-800 text-left text-sm">
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-3 md:hidden">
|
||||
{paymentIncidents.map((item) => (
|
||||
<div key={item.id} className="rounded-2xl border border-slate-800 bg-slate-950/70 p-3">
|
||||
<div className="mb-3 flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<div className="font-semibold text-slate-100">{item.payload?.reason || "-"}</div>
|
||||
<div className="mt-1 text-xs text-slate-500">{formatDateTime(item.created_at)}</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => void resolveIncident(item.id)}
|
||||
disabled={resolvingIncidentId === item.id}
|
||||
>
|
||||
{resolvingIncidentId === item.id ? "处理中" : "已处理"}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<MobileField label="套餐" value={item.payload?.plan_code || "-"} />
|
||||
<MobileField label="Tx" value={maskUrl(item.payload?.tx_hash)} mono />
|
||||
<MobileField label="实际收款" value={maskUrl(item.payload?.receiver_actual)} mono />
|
||||
<MobileField label="期望收款" value={maskUrl(item.payload?.receiver_expected)} mono />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{!paymentIncidents.length ? (
|
||||
<div className="rounded-2xl border border-slate-800 bg-slate-950/70 px-3 py-4 text-sm text-slate-500">
|
||||
暂无支付异常单
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="hidden overflow-x-auto rounded-2xl border border-slate-800 bg-slate-950/70 md:block">
|
||||
<table className="min-w-full divide-y divide-slate-800 text-left text-sm">
|
||||
<thead className="bg-slate-900/80 text-xs uppercase tracking-[0.14em] text-slate-500">
|
||||
<tr>
|
||||
<th className="px-4 py-3">时间</th>
|
||||
@@ -588,15 +636,32 @@ export function OpsDashboard() {
|
||||
<CardDescription>当前有效订阅用户、注册时间和到期时间。</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4 text-sm text-slate-300">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="text-xs text-slate-500">
|
||||
当前有效会员数:{memberships.length}
|
||||
</div>
|
||||
<Button variant="secondary" onClick={() => void loadMemberships()} disabled={membershipsLoading}>
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="text-xs text-slate-500">
|
||||
当前有效会员数:{memberships.length}
|
||||
</div>
|
||||
<Button variant="secondary" onClick={() => void loadMemberships()} disabled={membershipsLoading} className="w-full sm:w-auto">
|
||||
{membershipsLoading ? "加载中" : "刷新会员"}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="overflow-x-auto rounded-2xl border border-slate-800 bg-slate-950/70">
|
||||
<div className="space-y-3 md:hidden">
|
||||
{memberships.map((item) => (
|
||||
<div key={`${item.user_id}-${item.expires_at || ""}`} className="rounded-2xl border border-slate-800 bg-slate-950/70 p-3">
|
||||
<div className="font-semibold text-slate-100">{item.email || "-"}</div>
|
||||
<div className="mt-1 text-xs text-slate-500">{item.username || "-"}</div>
|
||||
<div className="mt-3 grid gap-2">
|
||||
<MobileField label="注册时间" value={formatDateTime(item.registered_at)} />
|
||||
<MobileField label="到期时间" value={formatDateTime(item.expires_at)} />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{!memberships.length ? (
|
||||
<div className="rounded-2xl border border-slate-800 bg-slate-950/70 px-3 py-4 text-sm text-slate-500">
|
||||
暂无有效会员
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="hidden overflow-x-auto rounded-2xl border border-slate-800 bg-slate-950/70 md:block">
|
||||
<table className="min-w-full divide-y divide-slate-800 text-left text-sm">
|
||||
<thead className="bg-slate-900/80 text-xs uppercase tracking-[0.14em] text-slate-500">
|
||||
<tr>
|
||||
@@ -642,7 +707,7 @@ export function OpsDashboard() {
|
||||
placeholder="telegram id / username / email"
|
||||
className="h-10 flex-1 rounded-xl border border-slate-800 bg-slate-950 px-3 text-sm text-slate-100 outline-none ring-0"
|
||||
/>
|
||||
<Button variant="secondary" onClick={() => void loadUsers(searchQuery)} disabled={usersLoading}>
|
||||
<Button variant="secondary" onClick={() => void loadUsers(searchQuery)} disabled={usersLoading} className="w-full sm:w-auto">
|
||||
{usersLoading ? "查询中" : "查询"}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "polyweather-frontend",
|
||||
"version": "1.5.0",
|
||||
"version": "1.5.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
|
||||
Reference in New Issue
Block a user