fix(rag): 全文索引在容器里根本没持久化 —— 补 /data 卷 + BLEVE_PATH #8
@@ -150,6 +150,31 @@ export interface MyTenant {
|
|||||||
members: number;
|
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 }> {
|
export async function myTenants(): Promise<{ tenants: MyTenant[]; active: string }> {
|
||||||
const res = guard401(await fetch(`${GATEWAY}/api/v1/me/tenants`, { headers: bearer() }));
|
const res = guard401(await fetch(`${GATEWAY}/api/v1/me/tenants`, { headers: bearer() }));
|
||||||
if (!res.ok) return { tenants: [], active: "" };
|
if (!res.ok) return { tenants: [], active: "" };
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { Coins, RefreshCw, Wallet, Cpu, Receipt, Activity } from "lucide-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";
|
import { Card, Panel, EmptyState, cn } from "../ui";
|
||||||
|
|
||||||
// 桌面端「用量」= 用户自己租户的计量观测:积分余额 + 消耗趋势 + 最近消耗。
|
// 桌面端「用量」= 用户自己租户的计量观测:积分余额 + 消耗趋势 + 最近消耗。
|
||||||
@@ -34,11 +35,20 @@ export function UsageView() {
|
|||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [refreshing, setRefreshing] = useState(false);
|
const [refreshing, setRefreshing] = useState(false);
|
||||||
const [err, setErr] = useState("");
|
const [err, setErr] = useState("");
|
||||||
|
const [sub, setSub] = useState<MySubscription | null>(null);
|
||||||
|
const [subPlan, setSubPlan] = useState<MySubPlan | null>(null);
|
||||||
|
|
||||||
const load = async () => {
|
const load = async () => {
|
||||||
setRefreshing(true);
|
setRefreshing(true);
|
||||||
try {
|
try {
|
||||||
setData(await myUsage(days));
|
setData(await myUsage(days));
|
||||||
|
// 订阅只读展示;失败不影响用量页主体(没订阅也是常态)
|
||||||
|
mySubscription()
|
||||||
|
.then((r) => {
|
||||||
|
setSub(r.subscription);
|
||||||
|
setSubPlan(r.plan);
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
setErr("");
|
setErr("");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setErr((e as Error).message);
|
setErr((e as Error).message);
|
||||||
@@ -114,6 +124,10 @@ export function UsageView() {
|
|||||||
<Metric icon={Receipt} label="估算成本" value={money(t.cost_micros)} sub="按定价折算(配置币种)" accent="text-slate-100" />
|
<Metric icon={Receipt} label="估算成本" value={money(t.cost_micros)} sub="按定价折算(配置币种)" accent="text-slate-100" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 订阅状态:到期即失效、无自动续费、无扣款通知 —— 必须在用户干活的地方看得见。
|
||||||
|
购买/续订不在这里做(入口在 Web 面账单页),避免同一功能两个入口。 */}
|
||||||
|
{sub && subPlan && <SubBar sub={sub} plan={subPlan} />}
|
||||||
|
|
||||||
{/* 消耗趋势 */}
|
{/* 消耗趋势 */}
|
||||||
<Panel title="积分消耗趋势" icon={Activity} className="min-h-[200px]">
|
<Panel title="积分消耗趋势" icon={Activity} className="min-h-[200px]">
|
||||||
<TrendBars series={series} />
|
<TrendBars series={series} />
|
||||||
@@ -182,3 +196,31 @@ function TrendBars({ series }: { series: UsageDay[] }) {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 (
|
||||||
|
<Card className={cn("flex flex-wrap items-center justify-between gap-3 p-4", left <= 7 ? "border-warn/40" : "")}>
|
||||||
|
<div>
|
||||||
|
<div className="text-sm font-medium text-slate-200">订阅中 · {plan.name}</div>
|
||||||
|
<div className="mt-0.5 text-[11px] text-slate-500">
|
||||||
|
每 {plan.refill_interval_days} 天发放 {credits(plan.refill_credits_micro)} 积分 · 已发放 {sub.refill_seq} 次 ·
|
||||||
|
到期后不再发放
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<span className={cn("text-xs tabular-nums", tone)}>
|
||||||
|
{new Date(sub.expires_at).toLocaleDateString("zh-CN")} 到期(剩 {left} 天)
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
onClick={() => openExternal(`${GATEWAY}/usage`)}
|
||||||
|
className="rounded-lg border border-line px-3 py-1.5 text-xs text-slate-300 transition-colors hover:border-brand/50 hover:text-brand"
|
||||||
|
>
|
||||||
|
去续订
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user