diff --git a/prisma/schema.prisma b/prisma/schema.prisma index eff599c..71ac345 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -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) diff --git a/src/actions/admin/settings.ts b/src/actions/admin/settings.ts index 6459b42..6c031b2 100644 --- a/src/actions/admin/settings.ts +++ b/src/actions/admin/settings.ts @@ -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, 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, diff --git a/src/app/(admin)/admin/settings/page.tsx b/src/app/(admin)/admin/settings/page.tsx index 0232bc3..0723f3c 100644 --- a/src/app/(admin)/admin/settings/page.tsx +++ b/src/app/(admin)/admin/settings/page.tsx @@ -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, diff --git a/src/app/(admin)/admin/settings/settings-form.tsx b/src/app/(admin)/admin/settings/settings-form.tsx index c38bf61..d7dbc0d 100644 --- a/src/app/(admin)/admin/settings/settings-form.tsx +++ b/src/app/(admin)/admin/settings/settings-form.tsx @@ -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:
- + + +

+ 开启后,商城展示电信、联通、移动当前最低延迟推荐;点击推荐会直接打开对应套餐详情。 +

+
+
+

- 开启后,用户侧商城展示三网推荐、节点延迟、趋势和访问路径;关闭后只保留购买所需的线路入口选择。 + 开启后,套餐详情展示节点延迟、趋势和访问路径;关闭后只保留购买所需的线路入口选择。

diff --git a/src/app/(user)/store/page.tsx b/src/app/(user)/store/page.tsx index 9198b28..0491137 100644 --- a/src/app/(user)/store/page.tsx +++ b/src/app/(user)/store/page.tsx @@ -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() { - {networkInsightsEnabled && proxyCards.length > 0 && ( + {networkRecommendationsEnabled && proxyCards.length > 0 && ( )} diff --git a/src/app/(user)/store/proxy-plan-card.tsx b/src/app/(user)/store/proxy-plan-card.tsx index 25062ff..e4ecd1b 100644 --- a/src/app/(user)/store/proxy-plan-card.tsx +++ b/src/app/(user)/store/proxy-plan-card.tsx @@ -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 ( <>
{ const item = itemMap.get(carrier); return ( -
{ + 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({

{item.nodeName}

{item.planName}

- - 查看套餐 - + 查看详情与购买 + ) : (

正在采集这个运营商的延迟数据。

)} - + ); })} diff --git a/src/app/api/agent/latency/route.ts b/src/app/api/agent/latency/route.ts index 8605d7c..f141a9e 100644 --- a/src/app/api/agent/latency/route.ts +++ b/src/app/api/agent/latency/route.ts @@ -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 }); diff --git a/src/app/api/latency/recommendations/route.ts b/src/app/api/latency/recommendations/route.ts index bf870f7..2f19f07 100644 --- a/src/app/api/latency/recommendations/route.ts +++ b/src/app/api/latency/recommendations/route.ts @@ -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(),