mirror of
https://github.com/JetSprow/J-Board-Lite.git
synced 2026-05-01 01:14:10 +05:30
feat: split network recommendation toggles
This commit is contained in:
@@ -786,6 +786,7 @@ model AppConfig {
|
||||
reminderDispatchIntervalMinutes Int @default(60)
|
||||
trafficSyncEnabled Boolean @default(true)
|
||||
trafficSyncIntervalSeconds Int @default(60)
|
||||
networkRecommendationsEnabled Boolean @default(false)
|
||||
networkInsightsEnabled Boolean @default(false)
|
||||
subscriptionRiskEnabled Boolean @default(true)
|
||||
subscriptionRiskAutoSuspend Boolean @default(true)
|
||||
|
||||
@@ -27,6 +27,7 @@ const settingsSchema = z.object({
|
||||
reminderDispatchIntervalMinutes: z.coerce.number().int().positive().optional(),
|
||||
trafficSyncEnabled: z.string().optional(),
|
||||
trafficSyncIntervalSeconds: z.coerce.number().int().min(10).optional(),
|
||||
networkRecommendationsEnabled: z.string().optional(),
|
||||
networkInsightsEnabled: z.string().optional(),
|
||||
subscriptionRiskEnabled: z.string().optional(),
|
||||
subscriptionRiskAutoSuspend: z.string().optional(),
|
||||
@@ -138,6 +139,10 @@ function buildSettingsUpdate(parsed: z.infer<typeof settingsSchema>, current: Aw
|
||||
trafficSyncEnabled: optionalBoolean(parsed.trafficSyncEnabled, current.trafficSyncEnabled),
|
||||
trafficSyncIntervalSeconds:
|
||||
parsed.trafficSyncIntervalSeconds ?? current.trafficSyncIntervalSeconds,
|
||||
networkRecommendationsEnabled: optionalBoolean(
|
||||
parsed.networkRecommendationsEnabled,
|
||||
current.networkRecommendationsEnabled,
|
||||
),
|
||||
networkInsightsEnabled: optionalBoolean(
|
||||
parsed.networkInsightsEnabled,
|
||||
current.networkInsightsEnabled,
|
||||
|
||||
@@ -37,6 +37,7 @@ export default async function AdminSettingsPage() {
|
||||
reminderDispatchIntervalMinutes: config.reminderDispatchIntervalMinutes,
|
||||
trafficSyncEnabled: config.trafficSyncEnabled,
|
||||
trafficSyncIntervalSeconds: config.trafficSyncIntervalSeconds,
|
||||
networkRecommendationsEnabled: config.networkRecommendationsEnabled,
|
||||
networkInsightsEnabled: config.networkInsightsEnabled,
|
||||
subscriptionRiskEnabled: config.subscriptionRiskEnabled,
|
||||
subscriptionRiskAutoSuspend: config.subscriptionRiskAutoSuspend,
|
||||
|
||||
@@ -26,6 +26,7 @@ interface AppConfig {
|
||||
reminderDispatchIntervalMinutes: number;
|
||||
trafficSyncEnabled: boolean;
|
||||
trafficSyncIntervalSeconds: number;
|
||||
networkRecommendationsEnabled: boolean;
|
||||
networkInsightsEnabled: boolean;
|
||||
subscriptionRiskEnabled: boolean;
|
||||
subscriptionRiskAutoSuspend: boolean;
|
||||
@@ -252,7 +253,22 @@ export function SettingsForm({ config, coupons }: { config: AppConfig; coupons:
|
||||
</div>
|
||||
<div className="grid gap-5 md:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="networkInsightsEnabled">三网推荐与线路体验</Label>
|
||||
<Label htmlFor="networkRecommendationsEnabled">三网推荐</Label>
|
||||
<select
|
||||
id="networkRecommendationsEnabled"
|
||||
name="networkRecommendationsEnabled"
|
||||
defaultValue={String(config.networkRecommendationsEnabled)}
|
||||
className={selectClassName}
|
||||
>
|
||||
<option value="false">关闭</option>
|
||||
<option value="true">开启</option>
|
||||
</select>
|
||||
<p className="text-xs leading-5 text-muted-foreground">
|
||||
开启后,商城展示电信、联通、移动当前最低延迟推荐;点击推荐会直接打开对应套餐详情。
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="networkInsightsEnabled">线路体验</Label>
|
||||
<select
|
||||
id="networkInsightsEnabled"
|
||||
name="networkInsightsEnabled"
|
||||
@@ -263,7 +279,7 @@ export function SettingsForm({ config, coupons }: { config: AppConfig; coupons:
|
||||
<option value="true">开启</option>
|
||||
</select>
|
||||
<p className="text-xs leading-5 text-muted-foreground">
|
||||
开启后,用户侧商城展示三网推荐、节点延迟、趋势和访问路径;关闭后只保留购买所需的线路入口选择。
|
||||
开启后,套餐详情展示节点延迟、趋势和访问路径;关闭后只保留购买所需的线路入口选择。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -30,7 +30,14 @@ export const metadata: Metadata = {
|
||||
|
||||
export default async function StorePage() {
|
||||
const session = await getActiveSession();
|
||||
const { plans, availabilityMap, pendingOrder, networkInsightsEnabled, latencyRecommendations } = await getStorePageData(session?.user.id);
|
||||
const {
|
||||
plans,
|
||||
availabilityMap,
|
||||
pendingOrder,
|
||||
networkRecommendationsEnabled,
|
||||
networkInsightsEnabled,
|
||||
latencyRecommendations,
|
||||
} = await getStorePageData(session?.user.id);
|
||||
const proxyPlans = getProxyPlans(plans);
|
||||
const streamingPlans = getStreamingPlans(plans);
|
||||
const proxyCards = sortPlansForDisplay(proxyPlans.map((plan) => toProxyPlanCard(plan, availabilityMap.get(plan.id))));
|
||||
@@ -77,7 +84,7 @@ export default async function StorePage() {
|
||||
|
||||
<PendingOrderBanner order={pendingOrder} />
|
||||
|
||||
{networkInsightsEnabled && proxyCards.length > 0 && (
|
||||
{networkRecommendationsEnabled && proxyCards.length > 0 && (
|
||||
<StoreLatencyRecommendations initialItems={latencyRecommendations} />
|
||||
)}
|
||||
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Gauge, Network, Search, Sparkles } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { StorePlanHeader } from "./plan-card-parts";
|
||||
import { PlanAvailabilityBadges } from "./plan-availability-badges";
|
||||
import { ProxyDetailDialog } from "./proxy-detail-dialog";
|
||||
import { OPEN_PROXY_PLAN_EVENT } from "./store-latency-recommendations";
|
||||
import type { ProxyPlan } from "./proxy-plan-types";
|
||||
|
||||
interface Props {
|
||||
@@ -19,6 +20,18 @@ export function ProxyPlanCard({ plan, networkInsightsEnabled }: Props) {
|
||||
const isFixedPackage = plan.pricingMode === "FIXED_PACKAGE";
|
||||
const displayPrice = isFixedPackage ? (plan.fixedPrice ?? 0) : plan.pricePerGb;
|
||||
|
||||
useEffect(() => {
|
||||
function handleOpen(event: Event) {
|
||||
if (!(event instanceof CustomEvent)) return;
|
||||
if (event.detail?.planId === plan.id) {
|
||||
setDialogOpen(true);
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener(OPEN_PROXY_PLAN_EVENT, handleOpen);
|
||||
return () => window.removeEventListener(OPEN_PROXY_PLAN_EVENT, handleOpen);
|
||||
}, [plan.id]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<article
|
||||
|
||||
@@ -28,7 +28,7 @@ export async function getStorePageData(userId?: string) {
|
||||
})
|
||||
: null,
|
||||
]);
|
||||
const latencyRecommendations = config.networkInsightsEnabled
|
||||
const latencyRecommendations = config.networkRecommendationsEnabled
|
||||
? await getLatencyRecommendations()
|
||||
: [];
|
||||
|
||||
@@ -43,6 +43,7 @@ export async function getStorePageData(userId?: string) {
|
||||
return {
|
||||
plans,
|
||||
availabilityMap,
|
||||
networkRecommendationsEnabled: config.networkRecommendationsEnabled,
|
||||
networkInsightsEnabled: config.networkInsightsEnabled,
|
||||
latencyRecommendations,
|
||||
pendingOrder: pendingOrder
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Activity, Clock3, RadioTower, RefreshCw, Sparkles } from "lucide-react";
|
||||
import { fetchJson } from "@/lib/fetch-json";
|
||||
@@ -18,6 +17,7 @@ interface RecommendationPayload {
|
||||
}
|
||||
|
||||
const REFRESH_INTERVAL_MS = 5 * 60 * 1000;
|
||||
export const OPEN_PROXY_PLAN_EVENT = "jboard:open-proxy-plan";
|
||||
|
||||
function formatTime(value: string | null) {
|
||||
if (!value) return "等待刷新";
|
||||
@@ -34,6 +34,14 @@ function getLatencyTone(latencyMs?: number) {
|
||||
return "border-amber-500/20 bg-amber-500/10 text-amber-700 dark:text-amber-300";
|
||||
}
|
||||
|
||||
function openRecommendedPlan(planId: string) {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent(OPEN_PROXY_PLAN_EVENT, {
|
||||
detail: { planId },
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
export function StoreLatencyRecommendations({
|
||||
initialItems,
|
||||
}: {
|
||||
@@ -96,10 +104,16 @@ export function StoreLatencyRecommendations({
|
||||
{RECOMMENDATION_CARRIERS.map((carrier) => {
|
||||
const item = itemMap.get(carrier);
|
||||
return (
|
||||
<div
|
||||
<button
|
||||
key={carrier}
|
||||
type="button"
|
||||
disabled={!item}
|
||||
onClick={() => {
|
||||
if (item) openRecommendedPlan(item.planId);
|
||||
}}
|
||||
className={cn(
|
||||
"rounded-xl border p-4 transition-colors duration-200",
|
||||
"rounded-xl border p-4 text-left transition-colors duration-200 focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-ring/20 disabled:cursor-default",
|
||||
item && "hover:border-primary/25 hover:bg-primary/7",
|
||||
getLatencyTone(item?.latencyMs),
|
||||
)}
|
||||
>
|
||||
@@ -120,17 +134,16 @@ export function StoreLatencyRecommendations({
|
||||
<p className="text-lg font-semibold tracking-[-0.04em] text-foreground">{item.nodeName}</p>
|
||||
<p className="mt-1 truncate text-xs text-muted-foreground">{item.planName}</p>
|
||||
</div>
|
||||
<Link
|
||||
href={`#plan-${item.planId}`}
|
||||
<span
|
||||
className="inline-flex items-center gap-1.5 text-xs font-semibold text-primary hover:underline"
|
||||
>
|
||||
<Activity className="size-3.5" /> 查看套餐
|
||||
</Link>
|
||||
<Activity className="size-3.5" /> 查看详情与购买
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<p className="mt-4 text-sm leading-6 text-muted-foreground">正在采集这个运营商的延迟数据。</p>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
@@ -14,7 +14,7 @@ export async function POST(req: Request) {
|
||||
const { nodeId } = auth;
|
||||
const config = await getAppConfig();
|
||||
|
||||
if (!config.networkInsightsEnabled) {
|
||||
if (!config.networkRecommendationsEnabled && !config.networkInsightsEnabled) {
|
||||
return NextResponse.json({ ok: true, skipped: true });
|
||||
}
|
||||
|
||||
@@ -54,9 +54,11 @@ export async function POST(req: Request) {
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.nodeLatencyLog.create({
|
||||
data: { nodeId, carrier: entry.carrier, latencyMs },
|
||||
});
|
||||
if (config.networkInsightsEnabled) {
|
||||
await prisma.nodeLatencyLog.create({
|
||||
data: { nodeId, carrier: entry.carrier, latencyMs },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({ ok: true });
|
||||
|
||||
@@ -4,7 +4,7 @@ import { getAppConfig } from "@/services/app-config";
|
||||
|
||||
export async function GET() {
|
||||
const config = await getAppConfig();
|
||||
if (!config.networkInsightsEnabled) {
|
||||
if (!config.networkRecommendationsEnabled) {
|
||||
return NextResponse.json({
|
||||
items: [],
|
||||
updatedAt: new Date().toISOString(),
|
||||
|
||||
Reference in New Issue
Block a user