diff --git a/sundynix-desktop/frontend/src/lib/api.ts b/sundynix-desktop/frontend/src/lib/api.ts index 836c318..27d4e72 100644 --- a/sundynix-desktop/frontend/src/lib/api.ts +++ b/sundynix-desktop/frontend/src/lib/api.ts @@ -150,6 +150,31 @@ export interface MyTenant { members: number; } +// ---- 订阅(只读)---- +// 桌面端**不做**购买流程:购买入口在 Web 面(一个功能一个入口)。这里只读状态, +// 因为"还剩几天到期"必须在用户干活的地方看得见——到期即失效且没有任何续费通知。 +export interface MySubscription { + id: string; + plan_id: string; + status: string; + expires_at: string; + refill_seq: number; +} + +export interface MySubPlan { + id: string; + name: string; + refill_credits_micro: number; + refill_interval_days: number; +} + +export async function mySubscription(): Promise<{ subscription: MySubscription | null; plan: MySubPlan | null }> { + const res = await fetch(`${GATEWAY}/api/v1/billing/subscription`, { headers: bearer() }); + if (!res.ok) return { subscription: null, plan: null }; + const d = (await res.json()) as { subscription?: MySubscription | null; plan?: MySubPlan | null }; + return { subscription: d.subscription ?? null, plan: d.plan ?? null }; +} + export async function myTenants(): Promise<{ tenants: MyTenant[]; active: string }> { const res = guard401(await fetch(`${GATEWAY}/api/v1/me/tenants`, { headers: bearer() })); if (!res.ok) return { tenants: [], active: "" }; diff --git a/sundynix-desktop/frontend/src/views/UsageView.tsx b/sundynix-desktop/frontend/src/views/UsageView.tsx index 0278ac0..38dea6a 100644 --- a/sundynix-desktop/frontend/src/views/UsageView.tsx +++ b/sundynix-desktop/frontend/src/views/UsageView.tsx @@ -1,6 +1,7 @@ import { useEffect, useMemo, useState } from "react"; import { Coins, RefreshCw, Wallet, Cpu, Receipt, Activity } from "lucide-react"; -import { myUsage, type MyUsage, type UsageDay } from "../lib/api"; +import { myUsage, GATEWAY, type MyUsage, type UsageDay, mySubscription, type MySubscription, type MySubPlan } from "../lib/api"; +import { openExternal } from "../lib/version"; import { Card, Panel, EmptyState, cn } from "../ui"; // 桌面端「用量」= 用户自己租户的计量观测:积分余额 + 消耗趋势 + 最近消耗。 @@ -34,11 +35,20 @@ export function UsageView() { const [loading, setLoading] = useState(true); const [refreshing, setRefreshing] = useState(false); const [err, setErr] = useState(""); + const [sub, setSub] = useState(null); + const [subPlan, setSubPlan] = useState(null); const load = async () => { setRefreshing(true); try { setData(await myUsage(days)); + // 订阅只读展示;失败不影响用量页主体(没订阅也是常态) + mySubscription() + .then((r) => { + setSub(r.subscription); + setSubPlan(r.plan); + }) + .catch(() => {}); setErr(""); } catch (e) { setErr((e as Error).message); @@ -114,6 +124,10 @@ export function UsageView() { + {/* 订阅状态:到期即失效、无自动续费、无扣款通知 —— 必须在用户干活的地方看得见。 + 购买/续订不在这里做(入口在 Web 面账单页),避免同一功能两个入口。 */} + {sub && subPlan && } + {/* 消耗趋势 */} @@ -182,3 +196,31 @@ function TrendBars({ series }: { series: UsageDay[] }) { ); } + +// SubBar 订阅状态条。只读:桌面端不做购买流程,续订跳 Web 面账单页。 +function SubBar({ sub, plan }: { sub: MySubscription; plan: MySubPlan }) { + const left = Math.ceil((new Date(sub.expires_at).getTime() - Date.now()) / 86400000); + const tone = left <= 3 ? "text-danger" : left <= 7 ? "text-warn" : "text-slate-400"; + return ( + +
+
订阅中 · {plan.name}
+
+ 每 {plan.refill_interval_days} 天发放 {credits(plan.refill_credits_micro)} 积分 · 已发放 {sub.refill_seq} 次 · + 到期后不再发放 +
+
+
+ + {new Date(sub.expires_at).toLocaleDateString("zh-CN")} 到期(剩 {left} 天) + + +
+
+ ); +}