From 1b5977a42051e5eaa9513bd8ea4226e115a549c0 Mon Sep 17 00:00:00 2001 From: shangzy Date: Thu, 2 Apr 2026 19:30:32 +0800 Subject: [PATCH] feat: add hidden /portal page for SinoCode iframe embedding Full-screen landing page with glassmorphism cards linking to monitoring (8018), docs (8017), and analytics (8019). Sidebar is conditionally hidden on the portal route. Supports dark/light themes and i18n. --- app/globals.css | 363 +++++++++++++++++++++++++++++++++ app/portal/layout.tsx | 14 ++ app/portal/page.tsx | 199 ++++++++++++++++++ components/ClientProviders.tsx | 14 +- lib/i18n.tsx | 17 ++ 5 files changed, 605 insertions(+), 2 deletions(-) create mode 100644 app/portal/layout.tsx create mode 100644 app/portal/page.tsx diff --git a/app/globals.css b/app/globals.css index 4692890..375888b 100644 --- a/app/globals.css +++ b/app/globals.css @@ -186,6 +186,369 @@ body { ::-webkit-scrollbar-thumb { background: var(--surface-border); border-radius: 3px; } ::-webkit-scrollbar-thumb:hover { background: var(--input-border); } +/* ═══════════════════════════════════════════ + PORTAL PAGE + ═══════════════════════════════════════════ */ + +/* Portal accent palette */ +:root, [data-theme="dark"] { + --portal-accent-green: #34d399; + --portal-accent-violet: #8b5cf6; + --portal-accent-cyan: #00e5ff; +} +[data-theme="light"] { + --portal-accent-green: #059669; + --portal-accent-violet: #7c3aed; + --portal-accent-cyan: #0284c7; +} + +/* Root container */ +.portal-page { + position: relative; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + min-height: 100vh; + overflow: hidden; + padding: 2rem; +} + +/* ── Background: animated grid ── */ +.portal-grid { + position: fixed; + inset: 0; + z-index: 0; + background-image: + linear-gradient(var(--grid-color) 1px, transparent 1px), + linear-gradient(90deg, var(--grid-color) 1px, transparent 1px); + background-size: 48px 48px; + mask-image: radial-gradient(ellipse 80% 70% at 50% 50%, black 20%, transparent 85%); + animation: portal-grid-drift 30s linear infinite; +} +@keyframes portal-grid-drift { + from { background-position: 0 0; } + to { background-position: 48px 48px; } +} + +/* ── Background: floating orbs ── */ +.portal-orb { + position: fixed; + border-radius: 50%; + filter: blur(100px); + pointer-events: none; + z-index: 0; + animation: portal-float 12s ease-in-out infinite; +} +.portal-orb--1 { + width: 500px; height: 500px; + top: -15%; left: 15%; + background: radial-gradient(circle, rgba(0,229,255,0.12), transparent 70%); + animation-duration: 14s; +} +.portal-orb--2 { + width: 380px; height: 380px; + bottom: -10%; right: 10%; + background: radial-gradient(circle, rgba(139,92,246,0.1), transparent 70%); + animation-duration: 18s; + animation-delay: -6s; +} +.portal-orb--3 { + width: 300px; height: 300px; + top: 35%; left: 55%; + background: radial-gradient(circle, rgba(52,211,153,0.06), transparent 70%); + animation-duration: 22s; + animation-delay: -10s; +} +[data-theme="light"] .portal-orb--1 { + background: radial-gradient(circle, rgba(2,132,199,0.08), transparent 70%); +} +[data-theme="light"] .portal-orb--2 { + background: radial-gradient(circle, rgba(124,58,237,0.06), transparent 70%); +} +[data-theme="light"] .portal-orb--3 { + background: radial-gradient(circle, rgba(5,150,105,0.05), transparent 70%); +} + +@keyframes portal-float { + 0%, 100% { transform: translate(0, 0) scale(1); } + 33% { transform: translate(25px, -18px) scale(1.04); } + 66% { transform: translate(-18px, 12px) scale(0.96); } +} + +/* ── Floating particles ── */ +.portal-particles { position: fixed; inset: 0; z-index: 0; pointer-events: none; } +.portal-particle { + position: absolute; + width: 3px; height: 3px; + border-radius: 50%; + background: var(--accent); + opacity: 0.25; + animation: portal-particle-drift 10s ease-in-out infinite; +} +@keyframes portal-particle-drift { + 0%, 100% { transform: translate(0, 0); opacity: 0.15; } + 50% { transform: translate(-15px, -30px); opacity: 0.4; } +} + +/* ── Header / Branding ── */ +.portal-header { + position: relative; + z-index: 10; + text-align: center; + margin-bottom: 3.5rem; +} + +.portal-logo-mark { + display: inline-flex; + align-items: center; + justify-content: center; + width: 52px; height: 52px; + border-radius: 14px; + margin-bottom: 1.25rem; + background: var(--btn-active-bg); + border: 1px solid var(--surface-border); + color: var(--accent); + box-shadow: 0 0 30px rgba(0,229,255,0.08); +} +[data-theme="light"] .portal-logo-mark { + box-shadow: 0 4px 20px rgba(2,132,199,0.1); +} + +.portal-title { + font-size: 4rem; + font-weight: 800; + letter-spacing: -0.03em; + line-height: 1; +} +.portal-title-glow { + color: var(--text-primary); +} +.portal-title-accent { + background: linear-gradient(135deg, var(--accent) 0%, var(--accent-purple) 60%, var(--accent-pink) 100%); + -webkit-background-clip: text; + background-clip: text; + -webkit-text-fill-color: transparent; +} +[data-theme="dark"] .portal-title-glow { + text-shadow: 0 0 40px rgba(0,229,255,0.15); +} + +.portal-subtitle { + margin-top: 0.75rem; + font-size: 1.05rem; + font-weight: 300; + letter-spacing: 0.12em; + color: var(--text-muted); +} + +/* Animated divider with scan effect */ +.portal-divider { + position: relative; + width: 240px; + height: 1px; + margin: 1.5rem auto 0; + background: linear-gradient(90deg, transparent 0%, var(--surface-border) 20%, var(--accent) 50%, var(--surface-border) 80%, transparent 100%); + transform-origin: center; + overflow: hidden; +} +.portal-divider-scan { + position: absolute; + top: 0; left: -40%; + width: 40%; height: 100%; + background: linear-gradient(90deg, transparent, var(--accent), transparent); + animation: portal-scan 3.5s ease-in-out infinite; + animation-delay: 1s; +} +@keyframes portal-scan { + 0% { left: -40%; opacity: 0; } + 10% { opacity: 1; } + 90% { opacity: 1; } + 100% { left: 100%; opacity: 0; } +} + +/* ── Service Cards Grid ── */ +.portal-cards { + position: relative; + z-index: 10; + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 1.5rem; + max-width: 960px; + width: 100%; +} +@media (max-width: 768px) { + .portal-cards { + grid-template-columns: 1fr; + max-width: 400px; + } +} + +/* Individual card */ +.portal-card { + position: relative; + display: flex; + flex-direction: column; + align-items: center; + padding: 2.5rem 2rem 2rem; + border-radius: 16px; + background: var(--surface); + backdrop-filter: blur(24px); + border: 1px solid var(--surface-border); + text-decoration: none; + color: var(--text-primary); + cursor: pointer; + overflow: hidden; + transition: border-color 0.35s ease, box-shadow 0.35s ease; +} +.portal-card:hover { + border-color: var(--card-accent); + box-shadow: + 0 0 40px var(--card-glow), + inset 0 0 40px rgba(0,0,0,0.02); +} +[data-theme="light"] .portal-card:hover { + box-shadow: + 0 8px 40px var(--card-glow), + 0 2px 8px rgba(0,0,0,0.04); +} + +/* Corner brackets */ +.portal-corner { + position: absolute; + width: 14px; height: 14px; + border-color: var(--card-accent); + opacity: 0; + transition: opacity 0.35s ease; + pointer-events: none; +} +.portal-card:hover .portal-corner { opacity: 0.7; } +.portal-corner--tl { top: 6px; left: 6px; border-top: 2px solid; border-left: 2px solid; } +.portal-corner--tr { top: 6px; right: 6px; border-top: 2px solid; border-right: 2px solid; } +.portal-corner--bl { bottom: 6px; left: 6px; border-bottom: 2px solid; border-left: 2px solid; } +.portal-corner--br { bottom: 6px; right: 6px; border-bottom: 2px solid; border-right: 2px solid; } + +/* Decorative bg icon (large, faded) */ +.portal-card-decor { + position: absolute; + top: -12px; right: -12px; + width: 80px; height: 80px; + color: var(--card-accent); + opacity: 0.04; + transition: opacity 0.35s; + pointer-events: none; +} +.portal-card:hover .portal-card-decor { opacity: 0.08; } + +/* Icon container */ +.portal-card-icon { + display: flex; + align-items: center; + justify-content: center; + width: 60px; height: 60px; + border-radius: 14px; + margin-bottom: 1.25rem; + background: var(--btn-active-bg); + border: 1px solid var(--surface-border); + color: var(--card-accent); + transition: border-color 0.3s, box-shadow 0.3s, color 0.3s; +} +.portal-card-icon svg { + width: 28px; height: 28px; +} +.portal-card:hover .portal-card-icon { + border-color: var(--card-accent); + box-shadow: 0 0 24px var(--card-glow); +} + +/* Text */ +.portal-card-title { + font-size: 1.15rem; + font-weight: 700; + margin-bottom: 0.5rem; + letter-spacing: -0.01em; + transition: color 0.3s; +} +.portal-card:hover .portal-card-title { + color: var(--card-accent); +} +.portal-card-desc { + font-size: 0.82rem; + line-height: 1.55; + color: var(--text-muted); + text-align: center; + margin-bottom: 1.25rem; +} + +/* Enter action */ +.portal-card-action { + display: flex; + align-items: center; + gap: 0.35rem; + font-size: 0.75rem; + font-weight: 500; + letter-spacing: 0.06em; + text-transform: uppercase; + color: var(--text-muted); + opacity: 0; + transform: translateY(6px); + transition: all 0.35s ease; +} +.portal-card:hover .portal-card-action { + opacity: 1; + transform: translateY(0); + color: var(--card-accent); +} +.portal-card-action-icon { + width: 14px; height: 14px; + transition: transform 0.25s; +} +.portal-card:hover .portal-card-action-icon { + transform: translate(2px, -2px); +} + +/* Bottom highlight bar */ +.portal-card-bar { + position: absolute; + bottom: 0; left: 50%; + transform: translateX(-50%); + width: 0; height: 2px; + border-radius: 2px; + background: var(--card-accent); + transition: width 0.4s cubic-bezier(0.22, 1, 0.36, 1); +} +.portal-card:hover .portal-card-bar { + width: 60%; +} + +/* ── Footer ── */ +.portal-footer { + position: relative; + z-index: 10; + display: flex; + align-items: center; + gap: 0.5rem; + margin-top: 3rem; +} +.portal-status-dot { + width: 6px; height: 6px; + border-radius: 50%; + background: #34d399; + box-shadow: 0 0 8px rgba(52,211,153,0.5); + animation: portal-pulse 2s ease-in-out infinite; +} +@keyframes portal-pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.5; } +} +.portal-status-text { + font-size: 0.7rem; + font-weight: 500; + letter-spacing: 0.1em; + text-transform: uppercase; + color: var(--text-muted); +} + /* Recharts overrides */ .recharts-cartesian-grid-horizontal line, .recharts-cartesian-grid-vertical line { diff --git a/app/portal/layout.tsx b/app/portal/layout.tsx new file mode 100644 index 0000000..c569e4f --- /dev/null +++ b/app/portal/layout.tsx @@ -0,0 +1,14 @@ +import type { Metadata } from "next"; + +export const metadata: Metadata = { + title: "SinoCode — 智能 API 网关平台", + description: "SinoCode intelligent API gateway platform portal", +}; + +export default function PortalLayout({ + children, +}: { + children: React.ReactNode; +}) { + return <>{children}; +} diff --git a/app/portal/page.tsx b/app/portal/page.tsx new file mode 100644 index 0000000..c66f51d --- /dev/null +++ b/app/portal/page.tsx @@ -0,0 +1,199 @@ +"use client"; + +import { motion } from "motion/react"; +import { + Activity, + MonitorCheck, + BookOpen, + BarChart3, + ArrowUpRight, + Hexagon, + Zap, + Shield, + Server, +} from "lucide-react"; +import { useI18n } from "@/lib/i18n"; + +/* ═══════════════════════════════════════════════════════ + Configure service URLs here. + Change these to your actual endpoints. + ═══════════════════════════════════════════════════════ */ +const SERVICES = [ + { + key: "monitoring" as const, + icon: MonitorCheck, + accentVar: "--portal-accent-green", + glowColor: "rgba(52,211,153,0.15)", + decorIcon: Shield, + }, + { + key: "docs" as const, + icon: BookOpen, + accentVar: "--portal-accent-violet", + glowColor: "rgba(139,92,246,0.15)", + decorIcon: Hexagon, + }, + { + key: "analytics" as const, + icon: BarChart3, + accentVar: "--portal-accent-cyan", + glowColor: "rgba(0,229,255,0.15)", + decorIcon: Zap, + }, +]; + +const SERVICE_URLS: Record = { + monitoring: "http://192.168.111.90:8018", + docs: "http://192.168.111.90:8017", + analytics: "http://192.168.111.90:8019", +}; + +export default function PortalPage() { + const { t } = useI18n(); + + const i18nMap: Record = { + monitoring: { title: t("portal.monitoring"), desc: t("portal.monitoringDesc") }, + docs: { title: t("portal.docs"), desc: t("portal.docsDesc") }, + analytics: { title: t("portal.analytics"), desc: t("portal.analyticsDesc") }, + }; + + return ( +
+ {/* ── Background layers ── */} +
+
+
+
+ + {/* Decorative floating particles */} +
+ {Array.from({ length: 6 }).map((_, i) => ( +
+ ))} +
+ + {/* ── Header / Branding ── */} + + {/* Logo mark */} + + + + +

+ Sino + Code +

+ + + {t("portal.subtitle")} + + + {/* Animated scan divider */} + +
+ + + + {/* ── Service Cards ── */} +
+ {SERVICES.map((service, i) => { + const Icon = service.icon; + const DecorIcon = service.decorIcon; + const info = i18nMap[service.key]; + const url = SERVICE_URLS[service.key]; + + return ( + + {/* Corner brackets */} + + + + + + {/* Decorative bg icon */} + + + {/* Icon container */} +
+ +
+ + {/* Text content */} +

{info.title}

+

{info.desc}

+ + {/* Enter indicator */} +
+ {t("portal.enter")} + +
+ + {/* Bottom highlight bar */} +
+ + ); + })} +
+ + {/* ── Footer status ── */} + +
+ + + System Online + + +
+ ); +} diff --git a/components/ClientProviders.tsx b/components/ClientProviders.tsx index 0d270f9..84559d3 100644 --- a/components/ClientProviders.tsx +++ b/components/ClientProviders.tsx @@ -1,16 +1,26 @@ "use client"; import { type ReactNode } from "react"; +import { usePathname } from "next/navigation"; import { I18nProvider } from "@/lib/i18n"; import { ThemeProvider } from "@/lib/theme"; import { Sidebar } from "@/components/Sidebar"; export function ClientProviders({ children }: { children: ReactNode }) { + const pathname = usePathname(); + const isPortal = pathname === "/portal"; + return ( - -
{children}
+ {isPortal ? ( + <>{children} + ) : ( + <> + +
{children}
+ + )}
); diff --git a/lib/i18n.tsx b/lib/i18n.tsx index 50c6093..3f66ba6 100644 --- a/lib/i18n.tsx +++ b/lib/i18n.tsx @@ -80,6 +80,15 @@ const translations = { "theme.light": "浅色", "theme.dark": "深色", "theme.system": "系统", + // portal + "portal.subtitle": "智能 API 网关平台", + "portal.monitoring": "监控站", + "portal.monitoringDesc": "实时系统状态监控与告警通知", + "portal.docs": "文档中心", + "portal.docsDesc": "API 接口文档与集成指南", + "portal.analytics": "使用统计", + "portal.analyticsDesc": "API 调用数据分析与可视化看板", + "portal.enter": "进入", }, en: { "nav.overview": "Overview", @@ -144,6 +153,14 @@ const translations = { "theme.light": "Light", "theme.dark": "Dark", "theme.system": "System", + "portal.subtitle": "Intelligent API Gateway Platform", + "portal.monitoring": "Monitoring", + "portal.monitoringDesc": "Real-time system status monitoring & alerts", + "portal.docs": "Documentation", + "portal.docsDesc": "API reference docs & integration guides", + "portal.analytics": "Usage Analytics", + "portal.analyticsDesc": "API usage data analytics & visualization dashboard", + "portal.enter": "Enter", }, } as const;