From 487341aba4760e62a47f96ffefc219e3a3b24967 Mon Sep 17 00:00:00 2001 From: SII-123qwe-asd-ux <1120240220@mail.nankai.edu.cn> Date: Thu, 13 Nov 2025 23:59:29 +0800 Subject: [PATCH 1/4] =?UTF-8?q?=E2=9C=A8=20feat:=20Add=20Xyzen=20chat=20la?= =?UTF-8?q?nding=20page=20with=20HeroParallax=20and=20CTA=20button?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ♻️ refactor:Create /chat route with HeroParallax component for product showcase - Add products.ts with Xyzen product data - Implement chat page layout with animated parallax effect - Add CTA button linking to Bohrium Xyzen application - Update Navbar to include Projects menu with 5 ScienceOL products - Add dark mode styling for better visual appearance - Integrate React Router with new /chat route in router.tsx --- web/index.css | 118 +++++++++- web/src/app/chat/page.tsx | 37 +++ web/src/app/chat/products.ts | 95 ++++++++ web/src/app/navbar/Navbar.tsx | 13 +- web/src/app/navbar/Projects.tsx | 285 +++++++++++++++--------- web/src/app/navbar/Tutorials.tsx | 119 +++++----- web/src/components/ui/hero-parallax.tsx | 155 +++++++++++++ web/src/lib/utils.ts | 6 + web/src/router.tsx | 2 + 9 files changed, 672 insertions(+), 158 deletions(-) create mode 100644 web/src/app/chat/page.tsx create mode 100644 web/src/app/chat/products.ts create mode 100644 web/src/components/ui/hero-parallax.tsx create mode 100644 web/src/lib/utils.ts diff --git a/web/index.css b/web/index.css index a64ab83..ca79076 100644 --- a/web/index.css +++ b/web/index.css @@ -1,4 +1,5 @@ @import "tailwindcss"; +@import "tw-animate-css"; @source "node_modules/@sciol/xyzen/dist/xyzen.css"; @custom-variant dark (&:where(.dark, .dark *)); @@ -196,10 +197,125 @@ body { border-radius: 0.5rem; } - @media (prefers-reduced-motion: reduce) { .animate-sheen { animation: none; opacity: .25; } } + +@theme inline { + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + --color-chart-1: var(--chart-1); + --color-chart-2: var(--chart-2); + --color-chart-3: var(--chart-3); + --color-chart-4: var(--chart-4); + --color-chart-5: var(--chart-5); + --color-sidebar: var(--sidebar); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-ring: var(--sidebar-ring); +} + +:root { + --radius: 0.625rem; + --background: oklch(1 0 0); + --foreground: oklch(0.145 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0.145 0 0); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.145 0 0); + --primary: oklch(0.205 0 0); + --primary-foreground: oklch(0.985 0 0); + --secondary: oklch(0.97 0 0); + --secondary-foreground: oklch(0.205 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --accent: oklch(0.97 0 0); + --accent-foreground: oklch(0.205 0 0); + --destructive: oklch(0.577 0.245 27.325); + --border: oklch(0.922 0 0); + --input: oklch(0.922 0 0); + --ring: oklch(0.708 0 0); + --chart-1: oklch(0.646 0.222 41.116); + --chart-2: oklch(0.6 0.118 184.704); + --chart-3: oklch(0.398 0.07 227.392); + --chart-4: oklch(0.828 0.189 84.429); + --chart-5: oklch(0.769 0.188 70.08); + --sidebar: oklch(0.985 0 0); + --sidebar-foreground: oklch(0.145 0 0); + --sidebar-primary: oklch(0.205 0 0); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.97 0 0); + --sidebar-accent-foreground: oklch(0.205 0 0); + --sidebar-border: oklch(0.922 0 0); + --sidebar-ring: oklch(0.708 0 0); +} + +.dark { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.205 0 0); + --popover-foreground: oklch(0.985 0 0); + --primary: oklch(0.922 0 0); + --primary-foreground: oklch(0.205 0 0); + --secondary: oklch(0.269 0 0); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --accent: oklch(0.269 0 0); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.704 0.191 22.216); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: oklch(0.556 0 0); + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --sidebar: oklch(0.205 0 0); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.269 0 0); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(1 0 0 / 10%); + --sidebar-ring: oklch(0.556 0 0); +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + } +} diff --git a/web/src/app/chat/page.tsx b/web/src/app/chat/page.tsx new file mode 100644 index 0000000..452fd45 --- /dev/null +++ b/web/src/app/chat/page.tsx @@ -0,0 +1,37 @@ +'use client'; + +import { HeroParallax } from '@/components/ui/hero-parallax'; +import { products } from './products'; + +export default function ChatPage() { + return ( +
+ + +
+
+
+

+ 准备好开始了吗? +

+

+ 点击下方按钮,立即体验 Xyzen AI Agent +

+
+ + +
+
+
+ ); +} diff --git a/web/src/app/chat/products.ts b/web/src/app/chat/products.ts new file mode 100644 index 0000000..ae9c9d5 --- /dev/null +++ b/web/src/app/chat/products.ts @@ -0,0 +1,95 @@ +export const products = [ + { + title: "chat1", + link: "https://www.bohrium.com/apps/xyzen/job?type=app", + thumbnail: + "https://storage.sciol.ac.cn/library/docs/chat1.png", + }, + { + title: "chat2", + link: "https://www.bohrium.com/apps/xyzen/job?type=app", + thumbnail: + "https://storage.sciol.ac.cn/library/docs/chat2.png", + }, + { + title: "chat3", + link: "https://www.bohrium.com/apps/xyzen/job?type=app", + thumbnail: + "https://storage.sciol.ac.cn/library/docs/chat3.png", + }, + + { + title: "chat4", + link: "https://www.bohrium.com/apps/xyzen/job?type=app", + thumbnail: + "https://storage.sciol.ac.cn/library/docs/chat4.png", + }, + { + title: "chat5", + link: "https://www.bohrium.com/apps/xyzen/job?type=app", + thumbnail: + "https://storage.sciol.ac.cn/library/docs/chat5.png", + }, + { + title: "chat6", + link: "https://www.bohrium.com/apps/xyzen/job?type=app", + thumbnail: + "https://storage.sciol.ac.cn/library/docs/chat6.png", + }, + + { + title: "chat7", + link: "https://www.bohrium.com/apps/xyzen/job?type=app", + thumbnail: + "https://storage.sciol.ac.cn/library/docs/chat7.png", + }, + { + title: "chat8", + link: "https://www.bohrium.com/apps/xyzen/job?type=app", + thumbnail: + "https://storage.sciol.ac.cn/library/docs/chat8.png", + }, + { + title: "chat9", + link: "https://www.bohrium.com/apps/xyzen/job?type=app", + thumbnail: + "https://storage.sciol.ac.cn/library/docs/chat9.png", + }, + { + title: "chat10", + link: "https://www.bohrium.com/apps/xyzen/job?type=app", + thumbnail: + "https://storage.sciol.ac.cn/library/docs/chat10.png", + }, + { + title: "chat11", + link: "https://www.bohrium.com/apps/xyzen/job?type=app", + thumbnail: + "https://storage.sciol.ac.cn/library/docs/chat11.png", + }, + + { + title: "chat12", + link: "https://www.bohrium.com/apps/xyzen/job?type=app", + thumbnail: + "https://storage.sciol.ac.cn/library/docs/chat12.png", + }, + { + title: "chat13", + link: "https://www.bohrium.com/apps/xyzen/job?type=app", + thumbnail: + "https://storage.sciol.ac.cn/library/docs/chat13.png", + }, + { + title: "chat14", + link: "https://www.bohrium.com/apps/xyzen/job?type=app", + thumbnail: + "https://storage.sciol.ac.cn/library/docs/chat14.png", + }, + // { + // title: "E Free Invoice", + // link: "https://efreeinvoice.com", + // thumbnail: + // "https://aceternity.com/images/products/thumbnails/new/efreeinvoice.png", + // }, +]; diff --git a/web/src/app/navbar/Navbar.tsx b/web/src/app/navbar/Navbar.tsx index 1226165..3c50cb2 100644 --- a/web/src/app/navbar/Navbar.tsx +++ b/web/src/app/navbar/Navbar.tsx @@ -1,7 +1,7 @@ 'use client'; import Logo from '@/assets/Logo'; -import { useState } from 'react'; +import React, { useState } from 'react'; import { useTranslation } from 'react-i18next'; import About from './About'; import Community from './Community'; @@ -9,6 +9,14 @@ import Projects from './Projects'; import { RightSideStatus } from './RightSideStatus'; import Tutorial from './Tutorials'; +const TutorialComponent = Tutorial as React.ComponentType<{ + index: number; + activeMenuItem: number | null; + setActiveMenuItem: React.Dispatch>; + open: boolean; + setOpen: React.Dispatch>; +}>; + const NavbarMenu = () => { const { t } = useTranslation(); const [open, setOpen] = useState(false); @@ -29,8 +37,7 @@ const NavbarMenu = () => { open={open} setOpen={setOpen} /> - - +// ) { +// return ( +// +// ); +// } + import { - ABACUSIcon, - AISSquareIcon, - DeePMDIcon, - DPGenIcon, -} from '@/assets/Icons'; -import Logo from '@/assets/Logo'; -import NavbarFullWidthPreview from './NavbarFullWidthPreview'; -import type { NavbarFullWidthPreviewProps } from './types'; + // BookmarkSquareIcon, + EnvelopeIcon, + // PuzzlePieceIcon, + // RectangleGroupIcon, +} from '@heroicons/react/24/outline'; + +import { SiUnrealengine,SiUnity,SiProton,SiX,SiStmicroelectronics } from 'react-icons/si'; +import { GitHubIcon } from '@/assets/SocialIcons'; +import NavbarFullWidth from './NavbarFullWidth'; +import type { NavbarFullWidthProps } from './types'; +// import { color } from 'framer-motion'; -const options = { - 'Molecule Dynamics': [ - { - name: 'DeePMD-kit', - description: 'Learn how to use DeePMD-kit', - href: `/space/DeePMD-kit`, - icon: DeePMDIcon, - }, - { - name: 'DPGen', - description: 'Learn from DPGen publications', - href: `/dp-gen`, - icon: DPGenIcon, - }, - { - name: 'DeePMD-kit', - description: 'Learn how to use DeePMD-kit', - href: `/DeePMD-kit`, - icon: DeePMDIcon, - }, - { - name: 'DPGen', - description: 'Learn from DPGen publications', - href: `/DeePMD-kit`, - icon: DPGenIcon, - }, - { - name: 'DeePMD-kit', - description: 'Learn how to use DeePMD-kit', - href: `/DeePMD-kit`, - icon: DeePMDIcon, - }, - { - name: 'DPGen', - description: 'Learn from DPGen publications', - href: `/DeePMD-kit`, - icon: DPGenIcon, - }, - { - name: 'DeePMD-kit', - description: 'Learn how to use DeePMD-kit', - href: `/DeePMD-kit`, - icon: DeePMDIcon, - }, - { - name: 'DPGen', - description: 'Learn from DPGen publications', - href: `/DeePMD-kit`, - icon: DPGenIcon, - }, - ], - 'Density Functional Theory': [ - { - name: 'ABACUS', - description: 'Learn how to use ABACUS', - href: `/abacus`, - icon: ABACUSIcon, - }, - ], - 'Finite Element Method': [ - { - name: 'Protium', - description: 'Get all of our website information', - href: ``, - icon: Logo, - }, - { - name: 'DeePMD-kit', - description: 'Learn how to use DeePMD-kit', - href: `/DeePMD-kit`, - icon: DeePMDIcon, - }, - ], - 'Community Platform': [ - { - name: 'Protium', - description: 'Get all of our website information', - href: ``, - icon: Logo, - }, - { - name: 'AIS-Square', - description: 'Learn how to use DeePMD-kit', - href: `/DeePMD-kit`, - icon: AISSquareIcon, - }, - ], -}; +const resources = [ + { + name: 'Studio', + description: '所有ScienceOL服务和社区的门户', + href: ``, + icon:SiStmicroelectronics, + color:'text-sky-500', + }, + { + name: 'Xyzen', + description: '面向实验室场景的专用智能体', + href: `/chat`, + icon: SiX, + color:'text-amber-500', + }, + { + name: 'PROTIUM', + description: '为科学计算设计的AI原生工作流引擎', + href: `/deepmd-kit`, + icon: SiProton, + color:'text-indigo-500', + }, + { + name: 'Anti', + description: '用于实验室模拟的3D数字孪生平台', + href: `/deepmd-kit`, + icon: SiUnity, + color:'text-rose-500', + }, + { + name: 'Lab-OS', + description: '用于模块化实验室硬件的开源操作系统', + href: `/deepmd-kit`, + icon: SiUnrealengine, + color:'text-emerald-500', + } +]; const callsToAction = [ - { name: 'See All Projects', href: '/space', icon: RocketLaunchIcon }, { name: 'Follow in Github', href: 'https://github.com/Protium', @@ -112,15 +189,17 @@ const callsToAction = [ export default function Projects( props: Omit< - NavbarFullWidthPreviewProps, - 'callsToAction' | 'buttonName' | 'options' + NavbarFullWidthProps, + 'solutions' | 'callsToAction' | 'buttonName' > ) { return ( - ); diff --git a/web/src/app/navbar/Tutorials.tsx b/web/src/app/navbar/Tutorials.tsx index ce0577f..483fab8 100644 --- a/web/src/app/navbar/Tutorials.tsx +++ b/web/src/app/navbar/Tutorials.tsx @@ -1,58 +1,75 @@ -import { - BookmarkSquareIcon, - EnvelopeIcon, - PuzzlePieceIcon, - RectangleGroupIcon, -} from '@heroicons/react/24/outline'; +// import { +// BookmarkSquareIcon, +// EnvelopeIcon, +// PuzzlePieceIcon, +// RectangleGroupIcon, +// } from '@heroicons/react/24/outline'; -import { GitHubIcon } from '@/assets/SocialIcons'; -import NavbarFullWidth from './NavbarFullWidth'; -import type { NavbarFullWidthProps } from './types'; +// import { GitHubIcon } from '@/assets/SocialIcons'; +// import NavbarFullWidth from './NavbarFullWidth'; +// import type { NavbarFullWidthProps } from './types'; -const resources = [ - { - name: 'Overview', - description: 'Get review of our projects', - href: ``, - icon: BookmarkSquareIcon, - }, - { - name: 'Workflow', - description: 'Learn from DeePMD-kit publications', - href: `/deepmd-kit`, - icon: RectangleGroupIcon, - }, - { - name: 'Flociety', - description: 'Learn from DPGen publications', - href: `/deepmd-kit`, - icon: PuzzlePieceIcon, - }, -]; +// const resources = [ +// { +// name: 'Overview', +// description: 'Get review of our projects', +// href: ``, +// icon: BookmarkSquareIcon, +// }, +// { +// name: 'Workflow', +// description: 'Learn from DeePMD-kit publications', +// href: `/deepmd-kit`, +// icon: RectangleGroupIcon, +// }, +// { +// name: 'Flociety', +// description: 'Learn from DPGen publications', +// href: `/deepmd-kit`, +// icon: PuzzlePieceIcon, +// }, +// ]; -const callsToAction = [ - { - name: 'Follow in Github', - href: 'https://github.com/Protium', - icon: GitHubIcon, - }, - { name: 'Contact us', href: '#', icon: EnvelopeIcon }, -]; +// const callsToAction = [ +// { +// name: 'Follow in Github', +// href: 'https://github.com/Protium', +// icon: GitHubIcon, +// }, +// { name: 'Contact us', href: '#', icon: EnvelopeIcon }, +// ]; -export default function Tutorial( - props: Omit< - NavbarFullWidthProps, - 'solutions' | 'callsToAction' | 'buttonName' - > -) { +// export default function Tutorial( +// props: Omit< +// NavbarFullWidthProps, +// 'solutions' | 'callsToAction' | 'buttonName' +// > +// ) { +// return ( +// +// ); +// } + +'use client'; + +// import Link from 'next/link'; + +export default function Tutorial() { return ( - + + 教程 + ); } diff --git a/web/src/components/ui/hero-parallax.tsx b/web/src/components/ui/hero-parallax.tsx new file mode 100644 index 0000000..75ae479 --- /dev/null +++ b/web/src/components/ui/hero-parallax.tsx @@ -0,0 +1,155 @@ +'use client'; +import { + motion, + MotionValue, + useScroll, + useSpring, + useTransform, +} from 'motion/react'; +import React from 'react'; + +export const HeroParallax = ({ + products, +}: { + products: { + title: string; + link: string; + thumbnail: string; + }[]; +}) => { + const firstRow = products.slice(0, 5); + const secondRow = products.slice(5, 10); + const thirdRow = products.slice(10, 15); + const ref = React.useRef(null); + const { scrollYProgress } = useScroll({ + target: ref, + offset: ['start start', 'end start'], + }); + + const springConfig = { stiffness: 300, damping: 30, bounce: 100 }; + + const translateX = useSpring( + useTransform(scrollYProgress, [0, 1], [0, 1000]), + springConfig + ); + const translateXReverse = useSpring( + useTransform(scrollYProgress, [0, 1], [0, -1000]), + springConfig + ); + const rotateX = useSpring( + useTransform(scrollYProgress, [0, 0.2], [15, 0]), + springConfig + ); + const opacity = useSpring( + useTransform(scrollYProgress, [0, 0.2], [0.2, 1]), + springConfig + ); + const rotateZ = useSpring( + useTransform(scrollYProgress, [0, 0.2], [20, 0]), + springConfig + ); + const translateY = useSpring( + useTransform(scrollYProgress, [0, 0.2], [-700, 500]), + springConfig + ); + return ( +
+
+ + + {firstRow.map((product) => ( + + ))} + + + {secondRow.map((product) => ( + + ))} + + + {thirdRow.map((product) => ( + + ))} + + +
+ ); +}; + +export const Header = () => { + return ( + //white text +
+

+ Xyzen (/ˈsaɪ.zan/)!
AI-driven Agent IDE +

+

+ Xyzen 是一个 Agent, 他是一个会创造 Agent 的 Agent。 Xyzen + 可以为你开启一个通往Agent 与MCP无限可能的大门。 +

+
+ ); +}; + +export const ProductCard = ({ + product, + translate, +}: { + product: { + title: string; + link: string; + thumbnail: string; + }; + translate: MotionValue; +}) => { + return ( + + + {product.title} + +
+

+ {product.title} +

+
+ ); +}; diff --git a/web/src/lib/utils.ts b/web/src/lib/utils.ts new file mode 100644 index 0000000..bd0c391 --- /dev/null +++ b/web/src/lib/utils.ts @@ -0,0 +1,6 @@ +import { clsx, type ClassValue } from "clsx" +import { twMerge } from "tailwind-merge" + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} diff --git a/web/src/router.tsx b/web/src/router.tsx index 493edd4..8c9ac81 100644 --- a/web/src/router.tsx +++ b/web/src/router.tsx @@ -1,5 +1,6 @@ import { BrowserRouter, Route, Routes } from 'react-router-dom'; import App from './app/App'; +import ChatPage from './app/chat/page'; import { EnvironmentPage } from './app/dashboard/environment'; import EnvironmentDetail from './app/dashboard/environment/EnvironmentDetail'; import DashboardHome from './app/dashboard/Home'; @@ -20,6 +21,7 @@ export default function Router() { } /> } /> } /> + } /> {/* 所有需要侧边栏和登录保护的页面 */} }> From f3e8b4b5a382d07386495cf06b8820203fd0c01a Mon Sep 17 00:00:00 2001 From: SII-123qwe-asd-ux <1120240220@mail.nankai.edu.cn> Date: Sun, 16 Nov 2025 17:06:47 +0800 Subject: [PATCH 2/4] =?UTF-8?q?=E2=9C=A8=20feat:=20Add=20Xyzen=20chat=20la?= =?UTF-8?q?nding=20page=20with=20dark/light=20mode=20support?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Create /chat route with HeroParallax component and product carousel - Add Projects menu to navbar with 5 ScienceOL products (Studio, Xyzen, PROTIUM, Anti, Lab-OS) - Implement responsive chat page with animated parallax and CTA button - Add Tutorial menu item linking to documentation - Create custom useTheme hook integrated with existing UiCore system - Support dynamic theme switching (system/dark/light modes) - Merge main branch and resolve conflicts --- web/src/app/chat/page.tsx | 21 ++++++++++++++++--- web/src/app/chat/products.ts | 14 ++++++------- web/src/app/navbar/Navbar.tsx | 26 ++++++++++++++--------- web/src/app/navbar/Tutorials.tsx | 14 ++++++++----- web/src/components/ui/hero-parallax.tsx | 13 +++++++----- web/src/hooks/useTheme.ts | 28 +++++++++++++++++++++++++ 6 files changed, 86 insertions(+), 30 deletions(-) create mode 100644 web/src/hooks/useTheme.ts diff --git a/web/src/app/chat/page.tsx b/web/src/app/chat/page.tsx index 452fd45..46cc297 100644 --- a/web/src/app/chat/page.tsx +++ b/web/src/app/chat/page.tsx @@ -1,20 +1,35 @@ 'use client'; import { HeroParallax } from '@/components/ui/hero-parallax'; +import { useTheme } from '@/hooks/useTheme'; import { products } from './products'; export default function ChatPage() { + const { isDark } = useTheme(); + return (
-
+
-

+

准备好开始了吗?

-

+

点击下方按钮,立即体验 Xyzen AI Agent

diff --git a/web/src/app/chat/products.ts b/web/src/app/chat/products.ts index ae9c9d5..01443b9 100644 --- a/web/src/app/chat/products.ts +++ b/web/src/app/chat/products.ts @@ -9,7 +9,7 @@ export const products = [ title: "chat2", link: "https://www.bohrium.com/apps/xyzen/job?type=app", thumbnail: - "https://storage.sciol.ac.cn/library/docs/chat2.png", + "https://storage.sciol.ac.cn/library/docs/chat2_light.png", }, { title: "chat3", @@ -22,7 +22,7 @@ export const products = [ title: "chat4", link: "https://www.bohrium.com/apps/xyzen/job?type=app", thumbnail: - "https://storage.sciol.ac.cn/library/docs/chat4.png", + "https://storage.sciol.ac.cn/library/docs/chat4_light.png", }, { title: "chat5", @@ -34,7 +34,7 @@ export const products = [ title: "chat6", link: "https://www.bohrium.com/apps/xyzen/job?type=app", thumbnail: - "https://storage.sciol.ac.cn/library/docs/chat6.png", + "https://storage.sciol.ac.cn/library/docs/chat6_light.png", }, { @@ -47,7 +47,7 @@ export const products = [ title: "chat8", link: "https://www.bohrium.com/apps/xyzen/job?type=app", thumbnail: - "https://storage.sciol.ac.cn/library/docs/chat8.png", + "https://storage.sciol.ac.cn/library/docs/chat8_light.png", }, { title: "chat9", @@ -59,7 +59,7 @@ export const products = [ title: "chat10", link: "https://www.bohrium.com/apps/xyzen/job?type=app", thumbnail: - "https://storage.sciol.ac.cn/library/docs/chat10.png", + "https://storage.sciol.ac.cn/library/docs/chat10_light.png", }, { title: "chat11", @@ -72,7 +72,7 @@ export const products = [ title: "chat12", link: "https://www.bohrium.com/apps/xyzen/job?type=app", thumbnail: - "https://storage.sciol.ac.cn/library/docs/chat12.png", + "https://storage.sciol.ac.cn/library/docs/chat12_light.png", }, { title: "chat13", @@ -84,7 +84,7 @@ export const products = [ title: "chat14", link: "https://www.bohrium.com/apps/xyzen/job?type=app", thumbnail: - "https://storage.sciol.ac.cn/library/docs/chat14.png", + "https://storage.sciol.ac.cn/library/docs/chat14_light.png", }, // { // title: "E Free Invoice", diff --git a/web/src/app/navbar/Navbar.tsx b/web/src/app/navbar/Navbar.tsx index 3c50cb2..202e458 100644 --- a/web/src/app/navbar/Navbar.tsx +++ b/web/src/app/navbar/Navbar.tsx @@ -7,15 +7,15 @@ import About from './About'; import Community from './Community'; import Projects from './Projects'; import { RightSideStatus } from './RightSideStatus'; -import Tutorial from './Tutorials'; +// import Tutorial from './Tutorials'; -const TutorialComponent = Tutorial as React.ComponentType<{ - index: number; - activeMenuItem: number | null; - setActiveMenuItem: React.Dispatch>; - open: boolean; - setOpen: React.Dispatch>; -}>; +// const Tutorial = Tutorial as React.ComponentType<{ +// index: number; +// activeMenuItem: number | null; +// setActiveMenuItem: React.Dispatch>; +// open: boolean; +// setOpen: React.Dispatch>; +// }>; const NavbarMenu = () => { const { t } = useTranslation(); @@ -37,13 +37,19 @@ const NavbarMenu = () => { open={open} setOpen={setOpen} /> - + {t('navbar.tutorial')} + + {/* + /> */} - 教程 - + className="inline-flex items-center gap-x-1 text-sm font-semibold leading-6 text-neutral-900 focus:outline-none dark:text-neutral-100 hover:text-indigo-600 dark:hover:text-indigo-500" + > + {t('navbar.tutorial','Tutorial')} + ); } diff --git a/web/src/components/ui/hero-parallax.tsx b/web/src/components/ui/hero-parallax.tsx index 75ae479..a3c5a91 100644 --- a/web/src/components/ui/hero-parallax.tsx +++ b/web/src/components/ui/hero-parallax.tsx @@ -7,6 +7,7 @@ import { useTransform, } from 'motion/react'; import React from 'react'; +import { useTheme } from '@/hooks/useTheme'; export const HeroParallax = ({ products, @@ -17,6 +18,8 @@ export const HeroParallax = ({ thumbnail: string; }[]; }) => { + const { isDark } = useTheme(); + const firstRow = products.slice(0, 5); const secondRow = products.slice(5, 10); const thirdRow = products.slice(10, 15); @@ -56,9 +59,9 @@ export const HeroParallax = ({
-
+
{ +export const Header = ({isDark}:{isDark: boolean}) => { return ( //white text
-

+

Xyzen (/ˈsaɪ.zan/)!
AI-driven Agent IDE

-

+

Xyzen 是一个 Agent, 他是一个会创造 Agent 的 Agent。 Xyzen 可以为你开启一个通往Agent 与MCP无限可能的大门。

diff --git a/web/src/hooks/useTheme.ts b/web/src/hooks/useTheme.ts new file mode 100644 index 0000000..3e2f4b0 --- /dev/null +++ b/web/src/hooks/useTheme.ts @@ -0,0 +1,28 @@ +'use client'; + +import { useUI } from '@/hooks/useUI'; +import { useEffect, useState } from 'react'; + +export function useTheme() { + const { theme } = useUI(); + const [isDark, setIsDark] = useState(false); + + useEffect(() => { + // 根据主题设置 isDark + const updateIsDark = () => { + setIsDark(theme === 'dark' || (theme === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches)); + }; + + updateIsDark(); + + // 监听系统主题变化(当主题设置为 system 时) + const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); + mediaQuery.addEventListener('change', updateIsDark); + + return () => { + mediaQuery.removeEventListener('change', updateIsDark); + }; + }, [theme]); + + return { isDark }; +} From ecea6aea113e92d2bafedf8588dd2422f0313fd1 Mon Sep 17 00:00:00 2001 From: SII-123qwe-asd-ux <1120240220@mail.nankai.edu.cn> Date: Tue, 16 Dec 2025 11:42:00 +0800 Subject: [PATCH 3/4] feat: Add 3D Lab interactive visualization with device components - Create interactive 3D lab scene using React Three Fiber - Implement 12+ device components (liquid handler, microscope, centrifuge, AGV robot, etc.) - Add device detail modal with 3D preview and specifications - Support device animations and interactive highlighting - Include device information database - Update navbar with 3D Lab navigation links - Enhance scrollbar styling for better UX --- web/index.css | 30 + web/src/app/3D_lab/DeviceDetailModal.tsx | 341 +++++++++ web/src/app/3D_lab/InteractiveLabScene.tsx | 805 +++++++++++++++++++++ web/src/app/3D_lab/deviceComponents.tsx | 505 +++++++++++++ web/src/app/3D_lab/deviceInfo.ts | 118 +++ web/src/app/3D_lab/page.tsx | 224 ++++++ web/src/app/3D_lab/styles.css | 104 +++ web/src/app/navbar/Navbar.tsx | 4 +- web/src/app/navbar/NavbarFullWidth.tsx | 27 +- web/src/app/navbar/Projects.tsx | 13 +- web/src/router.tsx | 2 + 11 files changed, 2161 insertions(+), 12 deletions(-) create mode 100644 web/src/app/3D_lab/DeviceDetailModal.tsx create mode 100644 web/src/app/3D_lab/InteractiveLabScene.tsx create mode 100644 web/src/app/3D_lab/deviceComponents.tsx create mode 100644 web/src/app/3D_lab/deviceInfo.ts create mode 100644 web/src/app/3D_lab/page.tsx create mode 100644 web/src/app/3D_lab/styles.css diff --git a/web/index.css b/web/index.css index 0e92213..32af51e 100644 --- a/web/index.css +++ b/web/index.css @@ -110,6 +110,36 @@ scrollbar-color: rgba(156, 163, 175, 0.4) transparent; } +/* Webkit 浏览器滚动条样式补充 */ +.custom-scrollbar::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +.custom-scrollbar::-webkit-scrollbar-track { + background: transparent; + border-radius: 4px; +} + +.custom-scrollbar::-webkit-scrollbar-thumb { + background-color: rgba(156, 163, 175, 0.4); + border-radius: 4px; + border: 2px solid transparent; + background-clip: padding-box; +} + +.custom-scrollbar::-webkit-scrollbar-thumb:hover { + background-color: rgba(156, 163, 175, 0.6); +} + +.dark .custom-scrollbar::-webkit-scrollbar-thumb { + background-color: rgba(156, 163, 175, 0.5); +} + +.dark .custom-scrollbar::-webkit-scrollbar-thumb:hover { + background-color: rgba(156, 163, 175, 0.7); +} + /* 全局背景色设置,防止滚动时出现白色 */ html, body { diff --git a/web/src/app/3D_lab/DeviceDetailModal.tsx b/web/src/app/3D_lab/DeviceDetailModal.tsx new file mode 100644 index 0000000..7e9349f --- /dev/null +++ b/web/src/app/3D_lab/DeviceDetailModal.tsx @@ -0,0 +1,341 @@ +'use client'; + +import LogoLoading from '@/components/basic/loading'; +import { XMarkIcon } from '@heroicons/react/24/outline'; +import { OrbitControls, PerspectiveCamera } from '@react-three/drei'; +import { Canvas } from '@react-three/fiber'; +import { Suspense } from 'react'; +import { getDeviceInfo } from './deviceInfo'; + +// 导入设备组件 +import { + AGVRobot, + Beaker, + Centrifuge, + LiquidHandlerModel, + Microscope, + Monitor, + PetriDishStack, + PipetteRack, + ReagentBottle, + ReagentRack, + SampleRack, + StorageCabinet, +} from './deviceComponents'; + +interface DeviceDetailModalProps { + deviceId: string; + onClose: () => void; + isAnimating?: boolean; + onToggleAnimation?: () => void; +} + +// 设备渲染映射 +function DeviceRenderer({ + deviceId, + isAnimating = false, +}: { + deviceId: string; + isAnimating?: boolean; +}) { + const position: [number, number, number] = [0, 0, 0]; + + switch (deviceId) { + case 'liquid-handler': + return ( + + ); + case 'microscope': + return ; + case 'monitor': + return ; + case 'agv-robot': + return ( + + ); + case 'centrifuge': + return ; + case 'pipette-rack': + return ; + case 'beaker': + return ; + case 'storage-cabinet': + return ; + case 'reagent-rack': + return ; + case 'reagent-bottle': + return ; + case 'petri-dish': + return ; + case 'sample-rack': + return ; + default: + return null; + } +} + +// 根据设备类型调整相机位置 +function getCameraPosition(deviceId: string): [number, number, number] { + const positions: Record = { + 'liquid-handler': [0, 2, 3], + microscope: [0.5, 0.8, 1.2], + monitor: [0, 0.8, 1.5], + 'agv-robot': [2, 2, 3], + centrifuge: [0.4, 0.4, 0.8], + 'pipette-rack': [0.3, 0.3, 0.6], + beaker: [0.3, 0.3, 0.5], + 'storage-cabinet': [0, 2, 3], + 'reagent-rack': [0.5, 0.5, 1], + 'reagent-bottle': [0.3, 0.3, 0.5], + 'petri-dish': [0.2, 0.2, 0.4], + 'sample-rack': [0.5, 0.5, 1], + }; + return positions[deviceId] || [0, 1, 2]; +} + +export default function DeviceDetailModal({ + deviceId, + onClose, + isAnimating = false, + onToggleAnimation, +}: DeviceDetailModalProps) { + const deviceInfo = getDeviceInfo(deviceId); + const cameraPos = getCameraPosition(deviceId); + + // 支持动画的设备列表 + const animatableDevices = [ + 'liquid-handler', + 'microscope', + 'agv-robot', + 'centrifuge', + ]; + const canAnimate = animatableDevices.includes(deviceId); + + if (!deviceInfo) { + return null; + } + + return ( +
+
+ {/* 关闭按钮 */} + + +
+ {/* 左侧:3D 视图 */} +
+ + +
+ } + > + + + + + {/* 光照 */} + + + + + + {/* 设备模型 */} + + + {/* 地面 */} + + + + + + {/* 背景网格 */} + + + + + {/* 操作提示 */} +
+

+ 🖱️ 拖动旋转 | 滚轮缩放 | 右键平移 +

+
+
+ + {/* 右侧:设备信息 */} +
+
+ {/* 标题 */} +
+

+ {deviceInfo.name} +

+

+ {deviceInfo.nameEn} +

+
+ + {/* 分隔线 */} +
+ + {/* 描述 */} +
+

+ 设备简介 +

+

+ {deviceInfo.description} +

+
+ + {/* 规格参数 */} + {deviceInfo.specs && deviceInfo.specs.length > 0 && ( +
+

+ 技术规格 +

+
    + {deviceInfo.specs.map((spec, index) => ( +
  • + + ▹ + + {spec} +
  • + ))} +
+
+ )} + + {/* 使用场景 */} + {deviceInfo.usage && ( +
+

+ 应用场景 +

+

+ {deviceInfo.usage} +

+
+ )} + + {/* 动画控制按钮 */} + {canAnimate && onToggleAnimation && ( +
+ + {isAnimating && ( +

+ 正在演示设备工作流程 +

+ )} +
+ )} + + {/* 装饰性图标 */} +
+
+
+
+
+
+
+
+
+
+
+
+ ); +} diff --git a/web/src/app/3D_lab/InteractiveLabScene.tsx b/web/src/app/3D_lab/InteractiveLabScene.tsx new file mode 100644 index 0000000..7a9b759 --- /dev/null +++ b/web/src/app/3D_lab/InteractiveLabScene.tsx @@ -0,0 +1,805 @@ +'use client'; + +import LogoLoading from '@/components/basic/loading'; +import { + Html, + OrbitControls, + PerspectiveCamera, + useGLTF, +} from '@react-three/drei'; +import { Canvas, useFrame } from '@react-three/fiber'; +import { Suspense, useRef, useState } from 'react'; +import type { Group, Mesh } from 'three'; +// 使用原生动画,无需额外依赖 +import { getDeviceInfo } from './deviceInfo'; +// 导入所有设备组件 +import { + Beaker, + Monitor, + PetriDishStack, + PipetteRack, + ReagentBottle, + ReagentRack, + SampleRack, + StorageCabinet, +} from './deviceComponents'; + +type Position3D = [number, number, number]; +type Rotation3D = [number, number, number]; + +interface PositionProps { + position?: Position3D; +} + +interface PositionRotationProps { + position?: Position3D; + rotation?: Rotation3D; +} + +interface LabBenchProps { + position?: Position3D; + width?: number; + depth?: number; +} + +interface ClickableDeviceProps extends PositionProps { + deviceId: string; + children: React.ReactNode; + onDeviceClick: (deviceId: string) => void; + isHighlighted?: boolean; + isAnimating?: boolean; + disabled?: boolean; +} + +// 可点击设备包装器 +function ClickableDevice({ + deviceId, + position = [0, 0, 0], + children, + onDeviceClick, + isHighlighted = false, + isAnimating = false, + disabled = false, +}: ClickableDeviceProps) { + const [hovered, setHovered] = useState(false); + const groupRef = useRef(null); + const deviceInfo = getDeviceInfo(deviceId); + + // 高亮动画 - 使用 useFrame 实现平滑过渡 + const targetScale = isHighlighted ? 1.05 : hovered ? 1.02 : 1; + const currentScale = useRef(1); + + useFrame(() => { + if (groupRef.current) { + currentScale.current += (targetScale - currentScale.current) * 0.1; + groupRef.current.scale.setScalar(currentScale.current); + } + }); + + // 点击动画 + useFrame((state) => { + if (groupRef.current && isAnimating) { + // 简单的脉冲动画 + const pulse = Math.sin(state.clock.elapsedTime * 2) * 0.05; + groupRef.current.scale.setScalar(1 + pulse); + } + }); + + return ( + { + if (disabled) return; + e.stopPropagation(); + onDeviceClick(deviceId); + }} + onPointerOver={(e) => { + if (disabled) return; + e.stopPropagation(); + setHovered(true); + document.body.style.cursor = 'pointer'; + }} + onPointerOut={() => { + if (disabled) return; + setHovered(false); + document.body.style.cursor = 'default'; + }} + > + {children} + + {/* 高亮光晕效果 */} + {(hovered || isHighlighted) && ( + + + + + )} + + {/* 悬浮提示 - 只在hover时显示,不在highlighted时显示,避免遮挡场景 */} + {hovered && !isHighlighted && deviceInfo && ( + +
+
{deviceInfo.name}
+
点击查看详情
+
+ + )} +
+ ); +} + +// 移液站模型组件 - 支持动画 +function LiquidHandlerModel({ + position = [0, 0.1, 0], + isAnimating = false, +}: PositionProps & { isAnimating?: boolean }) { + const OSS_BASE_URL = + 'https://storage.sciol.ac.cn/library/liquid_transform_xyz/meshes'; + + const baseLink = useGLTF(`${OSS_BASE_URL}/base_link.glb`); + const xLink = useGLTF(`${OSS_BASE_URL}/x_link.glb`); + const yLink = useGLTF(`${OSS_BASE_URL}/y_link.glb`); + const zLink = useGLTF(`${OSS_BASE_URL}/z_link.glb`); + + const xLinkRef = useRef(null); + const yLinkRef = useRef(null); + const zLinkRef = useRef(null); + + // 移液站工作动画:X、Y、Z 轴移动 + useFrame((state) => { + if (isAnimating) { + const time = state.clock.elapsedTime; + if (xLinkRef.current) { + xLinkRef.current.position.x = Math.sin(time * 0.5) * 0.3; + } + if (yLinkRef.current) { + yLinkRef.current.position.y = Math.sin(time * 0.7 + 1) * 0.2; + } + if (zLinkRef.current) { + zLinkRef.current.position.z = Math.sin(time * 0.6 + 2) * 0.25; + } + } + }); + + return ( + + + + + + + + + + + + + ); +} + +// 试剂瓶组 +function ReagentBottles({ position = [0, 0, 0] }: PositionProps) { + const bottles = [ + { pos: [0, 0, 0], color: '#ef4444' }, + { pos: [0.2, 0, 0], color: '#3b82f6' }, + { pos: [0.4, 0, 0], color: '#10b981' }, + { pos: [0, 0, 0.2], color: '#f59e0b' }, + { pos: [0.2, 0, 0.2], color: '#8b5cf6' }, + { pos: [0.4, 0, 0.2], color: '#06b6d4' }, + ]; + + return ( + + {bottles.map((bottle, i) => ( + + + + + + + + + + + + + + + ))} + + ); +} + +// 移液枪架 +function PipetteStand({ position = [0, 0, 0] }: PositionProps) { + return ( + + + + + + + + + + {[0, 0.1, 0.2].map((y, i) => ( + + + + + + + ))} + + ); +} + +// 实验室货架 +function LabShelf({ + position = [0, 0, 0], + rotation = [0, 0, 0], +}: PositionRotationProps) { + return ( + + {[ + [-0.4, 0.75, -0.2], + [0.4, 0.75, -0.2], + [-0.4, 0.75, 0.2], + [0.4, 0.75, 0.2], + ].map((pos, i) => ( + + + + + ))} + {[0.3, 0.7, 1.1, 1.5].map((y, i) => ( + + + + + ))} + + ); +} + +// 实验台组件 +function LabBench({ + position = [0, 0, 0], + width = 5, + depth = 2.5, +}: LabBenchProps) { + return ( + + + + + + {[ + [-width / 2 + 0.15, 0.45, -depth / 2 + 0.15], + [width / 2 - 0.15, 0.45, -depth / 2 + 0.15], + [-width / 2 + 0.15, 0.45, depth / 2 - 0.15], + [width / 2 - 0.15, 0.45, depth / 2 - 0.15], + ].map((pos, i) => ( + + + + + ))} + + + + + + ); +} + +// 显微镜 - 支持动画 +function Microscope({ + position = [0, 0, 0], + isAnimating = false, +}: PositionProps & { isAnimating?: boolean }) { + const lensRef = useRef(null); + + // 显微镜观察动画:镜头上下移动 + useFrame((state) => { + if (lensRef.current && isAnimating) { + const time = state.clock.elapsedTime; + lensRef.current.position.y = 0.5 + Math.sin(time * 1.5) * 0.05; + } + }); + + return ( + + + + + + + + + + + + + + + + + + + ); +} + +// AGV机器人 - 支持动画 +function AGVRobot({ + position = [0, 0, 0], + rotation = [0, 0, 0], + isAnimating = false, +}: PositionRotationProps & { isAnimating?: boolean }) { + const robotRef = useRef(null); + const armRef = useRef(null); + + // AGV移动和机械臂动画 + useFrame((state) => { + if (isAnimating) { + const time = state.clock.elapsedTime; + if (robotRef.current) { + // 前后移动 + robotRef.current.position.z = Math.sin(time * 0.3) * 0.5; + } + if (armRef.current) { + // 机械臂摆动 + armRef.current.rotation.y = Math.sin(time * 0.5) * 0.3; + armRef.current.rotation.z = Math.sin(time * 0.7) * 0.2; + } + } + }); + + return ( + + + + + + {[ + [-0.52, 0.1, 0.38], + [0.52, 0.1, 0.38], + [-0.52, 0.1, -0.38], + [0.52, 0.1, -0.38], + ].map((pos, i) => ( + + + + + ))} + + + + + + + + + + + + + + + + ); +} + +// 离心机 - 支持动画 +function Centrifuge({ + position = [0, 0, 0], + isAnimating = false, +}: PositionProps & { isAnimating?: boolean }) { + const rotorRef = useRef(null); + + // 离心机旋转动画 + + return ( + + + + + + + + + + + + + + + + + + + ); +} + +// 其他设备组件(简化版,从原文件导入) +// 为了简化,这里只实现关键的可点击设备 +// 其他设备可以从 deviceComponents.tsx 导入 + +interface InteractiveLabSceneProps { + onDeviceClick: (deviceId: string) => void; + highlightedDevice?: string | null; + animatingDevice?: string | null; + disabled?: boolean; // 禁用交互(当模态框打开时) +} + +// 交互式实验室场景 +function InteractiveLabScene({ + onDeviceClick, + highlightedDevice, + animatingDevice, + disabled = false, +}: InteractiveLabSceneProps) { + return ( + <> + + + + {/* 环境光照 */} + + + + + {/* 天花板灯光 */} + {[ + [-4, 4.5, -2], + [4, 4.5, -2], + [-4, 4.5, 3], + [4, 4.5, 3], + [0, 4.5, 0], + ].map((pos, i) => ( + + ))} + + {/* 中央工作台 */} + + + + + + + + + + + + + + + + {/* 左侧工作台 */} + + + + + + + + + + + + + + + + + + + + + {/* 右侧工作台 */} + + + + + + + + + + + + + + + + + {/* 后方工作台 */} + + + + + + + + + + + + + + + + + {/* 前方工作台 */} + + + + + + + + {/* AGV 小车 */} + + + + + + + + {/* 储物柜 - 靠墙排列 */} + + + + + + + + {/* 开放式货架 - 摆满试剂 */} + + + + {/* 架子上的物品 - 左侧 */} + + + + + + + + + {/* 架子上的物品 - 右侧 */} + + + + + + + + + {/* 地板 */} + + + + + + + ); +} + +// 加载占位组件 +function LoadingFallback() { + return ( +
+ +
+ ); +} + +// 主组件 +export default function InteractiveLabSceneComponent({ + onDeviceClick, + highlightedDevice, + animatingDevice, +}: InteractiveLabSceneProps) { + return ( +
+ }> + + + + +
+ ); +} diff --git a/web/src/app/3D_lab/deviceComponents.tsx b/web/src/app/3D_lab/deviceComponents.tsx new file mode 100644 index 0000000..849c91d --- /dev/null +++ b/web/src/app/3D_lab/deviceComponents.tsx @@ -0,0 +1,505 @@ +// 从 LabScene3D 导出所有设备组件,供 DeviceDetailModal 使用 +import { useGLTF } from '@react-three/drei'; +import { useFrame } from '@react-three/fiber'; +import type { JSX } from 'react'; +import { useRef } from 'react'; +import type { Group, Mesh } from 'three'; + +type Position3D = [number, number, number]; +type Rotation3D = [number, number, number]; + +interface PositionProps { + position?: Position3D; +} + +interface PositionRotationProps { + position?: Position3D; + rotation?: Rotation3D; +} + +// 移液站模型组件 +export function LiquidHandlerModel({ + position = [0, 0.1, 0], + isAnimating = false, +}: PositionProps & { isAnimating?: boolean }): JSX.Element { + const OSS_BASE_URL = + 'https://storage.sciol.ac.cn/library/liquid_transform_xyz/meshes'; + + const baseLink = useGLTF(`${OSS_BASE_URL}/base_link.glb`); + const xLink = useGLTF(`${OSS_BASE_URL}/x_link.glb`); + const yLink = useGLTF(`${OSS_BASE_URL}/y_link.glb`); + const zLink = useGLTF(`${OSS_BASE_URL}/z_link.glb`); + + const xLinkRef = useRef(null); + const yLinkRef = useRef(null); + const zLinkRef = useRef(null); + + // 移液站工作动画:X、Y、Z 轴移动 + useFrame((state) => { + if (isAnimating) { + const time = state.clock.elapsedTime; + if (xLinkRef.current) { + xLinkRef.current.position.x = Math.sin(time * 0.5) * 0.3; + } + if (yLinkRef.current) { + yLinkRef.current.position.y = Math.sin(time * 0.7 + 1) * 0.2; + } + if (zLinkRef.current) { + zLinkRef.current.position.z = Math.sin(time * 0.6 + 2) * 0.25; + } + } + }); + + return ( + + + + + + + + + + + + + ); +} + +// 显微镜 +export function Microscope({ + position = [0, 0, 0], + isAnimating = false, +}: PositionProps & { isAnimating?: boolean }): JSX.Element { + const lensRef = useRef(null); + + // 显微镜观察动画:镜头上下移动 + useFrame((state) => { + if (lensRef.current && isAnimating) { + const time = state.clock.elapsedTime; + lensRef.current.position.y = 0.5 + Math.sin(time * 1.5) * 0.05; + } + }); + + return ( + + + + + + + + + + + + + + + + + + + ); +} + +// 显示器 +export function Monitor({ + position = [0, 0, 0], + rotation = [0, 0, 0], +}: PositionRotationProps): JSX.Element { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +} + +// AGV机器人(简化版,只显示核心部分) +export function AGVRobot({ + position = [0, 0, 0], + rotation = [0, 0, 0], + isAnimating = false, +}: PositionRotationProps & { isAnimating?: boolean }): JSX.Element { + const robotRef = useRef(null); + const armRef = useRef(null); + + // AGV移动和机械臂动画 + useFrame((state) => { + if (isAnimating) { + const time = state.clock.elapsedTime; + if (robotRef.current) { + // 前后移动 + robotRef.current.position.z = Math.sin(time * 0.3) * 0.5; + } + if (armRef.current) { + // 机械臂摆动 + armRef.current.rotation.y = Math.sin(time * 0.5) * 0.3; + armRef.current.rotation.z = Math.sin(time * 0.7) * 0.2; + } + } + }); + + return ( + + + + + + {[ + [-0.52, 0.1, 0.38], + [0.52, 0.1, 0.38], + [-0.52, 0.1, -0.38], + [0.52, 0.1, -0.38], + ].map((pos, i) => ( + + + + + ))} + + + + + + + + + + + + + + + + ); +} + +// 离心机 +export function Centrifuge({ + position = [0, 0, 0], + isAnimating = false, +}: PositionProps & { isAnimating?: boolean }): JSX.Element { + const rotorRef = useRef(null); + + // 离心机旋转动画 + useFrame(() => { + if (rotorRef.current && isAnimating) { + rotorRef.current.rotation.y += 0.1; + } + }); + + return ( + + + + + + + + + + + + + + + + + + + ); +} + +// 移液枪架 +export function PipetteRack({ + position = [0, 0, 0], +}: PositionProps): JSX.Element { + const colors = ['#ef4444', '#3b82f6', '#10b981', '#f59e0b']; + + return ( + + + + + + {colors.map((color, i) => ( + + + + + + + ))} + + ); +} + +// 烧杯 +export function Beaker({ + position = [0, 0, 0], + color = '#3b82f6', +}: PositionProps & { color?: string }): JSX.Element { + return ( + + + + + + + + + + + ); +} + +// 储物柜 +export function StorageCabinet({ + position = [0, 0, 0], +}: PositionProps): JSX.Element { + return ( + + + + + + {[ + [0.2, 1.35, 0.301], + [-0.2, 1.35, 0.301], + [0.2, 0.45, 0.301], + [-0.2, 0.45, 0.301], + ].map((pos, i) => ( + + + + + ))} + + ); +} + +// 试剂瓶架 +export function ReagentRack({ + position = [0, 0, 0], +}: PositionProps): JSX.Element { + const colors = [ + '#ef4444', + '#f59e0b', + '#10b981', + '#3b82f6', + '#8b5cf6', + '#ec4899', + ]; + const bottles = []; + + for (let row = 0; row < 3; row++) { + for (let col = 0; col < 3; col++) { + bottles.push({ + pos: [col * 0.15 - 0.15, 0, row * 0.15 - 0.15] as Position3D, + color: colors[(row * 3 + col) % colors.length], + }); + } + } + + return ( + + + + + + {bottles.map((bottle, i) => ( + + ))} + + ); +} + +// 试剂瓶 +export function ReagentBottle({ + position = [0, 0, 0], + color = '#3b82f6', + size = 'medium', +}: PositionProps & { + color?: string; + size?: 'small' | 'medium' | 'large'; +}): JSX.Element { + const sizes = { + small: { radius: 0.04, height: 0.12 }, + medium: { radius: 0.06, height: 0.2 }, + large: { radius: 0.08, height: 0.28 }, + }; + const { radius, height } = sizes[size]; + + return ( + + + + + + + + + + + + + + + ); +} + +// 培养皿 +export function PetriDishStack({ + position = [0, 0, 0], +}: PositionProps): JSX.Element { + return ( + + {[0, 0.015, 0.03, 0.045, 0.06].map((y, i) => ( + + + + + ))} + + ); +} + +// 样品架 +export function SampleRack({ + position = [0, 0, 0], +}: PositionProps): JSX.Element { + return ( + + + + + + {Array.from({ length: 48 }).map((_, i) => { + const row = Math.floor(i / 8); + const col = i % 8; + return ( + + + + + ); + })} + + ); +} diff --git a/web/src/app/3D_lab/deviceInfo.ts b/web/src/app/3D_lab/deviceInfo.ts new file mode 100644 index 0000000..c1f00b9 --- /dev/null +++ b/web/src/app/3D_lab/deviceInfo.ts @@ -0,0 +1,118 @@ +// 设备信息配置 +export interface DeviceInfo { + id: string; + name: string; + nameEn: string; + description: string; + specs?: string[]; + usage?: string; +} + +export const DEVICE_INFO: Record = { + 'liquid-handler': { + id: 'liquid-handler', + name: '自动移液工作站', + nameEn: 'Liquid Handler', + description: '高精度自动化液体处理系统,用于样品分配、稀释和混合操作', + specs: ['精度:±1.5%', '工作范围:0.5-1000μL', '96/384孔板兼容'], + usage: '用于高通量样品制备、PCR反应体系配置、细胞培养等实验', + }, + microscope: { + id: 'microscope', + name: '光学显微镜', + nameEn: 'Microscope', + description: '高分辨率光学显微镜,用于细胞观察和微观结构分析', + specs: ['放大倍数:40x-1000x', '分辨率:0.2μm', '数字成像系统'], + usage: '用于细胞形态观察、组织切片分析、微生物检测等', + }, + monitor: { + id: 'monitor', + name: '工作站电脑', + nameEn: 'Workstation', + description: '实验室数据处理和设备控制工作站', + specs: ['24英寸4K显示屏', '高性能处理器', '专业图形显卡'], + usage: '用于实验数据分析、设备程序控制、结果可视化', + }, + 'agv-robot': { + id: 'agv-robot', + name: '智能AGV运输机器人', + nameEn: 'AGV Robot', + description: '配备6轴机械臂的自主移动机器人,实现实验室自动化物流', + specs: ['载重:50kg', '精度:±2mm', '6自由度机械臂', '自主导航'], + usage: '用于样品运输、耗材配送、设备间协作等自动化任务', + }, + centrifuge: { + id: 'centrifuge', + name: '台式离心机', + nameEn: 'Centrifuge', + description: '高速台式离心机,用于样品分离和沉淀', + specs: ['最高转速:15000 rpm', '容量:24×1.5ml', '温度控制:-10~40℃'], + usage: '用于DNA/RNA提取、蛋白质纯化、细胞分离等', + }, + 'pipette-rack': { + id: 'pipette-rack', + name: '移液枪架', + nameEn: 'Pipette Rack', + description: '多通道移液枪存储架,配备不同量程移液枪', + specs: ['容纳数量:4-8支', '量程:0.5-1000μL', '不锈钢材质'], + usage: '用于存放和快速取用各种规格的移液枪', + }, + beaker: { + id: 'beaker', + name: '烧杯', + nameEn: 'Beaker', + description: '标准实验室玻璃烧杯,用于溶液配置和反应', + specs: ['容量:50-1000ml', '材质:硼硅玻璃', '耐温:-70~500℃'], + usage: '用于溶液混合、加热反应、样品储存等', + }, + 'storage-cabinet': { + id: 'storage-cabinet', + name: '实验室储物柜', + nameEn: 'Storage Cabinet', + description: '标准实验室储物柜,用于存放试剂和耗材', + specs: ['尺寸:90×180×60cm', '防腐蚀材质', '多层分隔'], + usage: '用于存放化学试剂、实验耗材、个人防护用品等', + }, + 'reagent-rack': { + id: 'reagent-rack', + name: '试剂瓶架', + nameEn: 'Reagent Rack', + description: '多位试剂瓶存储架,整齐存放各类试剂', + specs: ['容量:9-16瓶', '防腐蚀托盘', '标签系统'], + usage: '用于分类存放和管理各种化学试剂、缓冲液等', + }, + 'reagent-bottle': { + id: 'reagent-bottle', + name: '试剂瓶', + nameEn: 'Reagent Bottle', + description: '标准实验室试剂瓶,密封保存各类试剂', + specs: ['容量:50-1000ml', '材质:玻璃/塑料', '密封瓶盖'], + usage: '用于存放和使用各种化学试剂、溶液', + }, + 'petri-dish': { + id: 'petri-dish', + name: '培养皿', + nameEn: 'Petri Dish', + description: '无菌塑料培养皿,用于微生物和细胞培养', + specs: ['直径:90mm', '材质:聚苯乙烯', '灭菌处理'], + usage: '用于细菌培养、细胞培养、菌落计数等', + }, + 'sample-rack': { + id: 'sample-rack', + name: '微孔板', + nameEn: 'Microplate', + description: '96孔标准微孔板,用于高通量实验', + specs: ['规格:96/384孔', '材质:聚丙烯', '体积:50-300μL/孔'], + usage: '用于ELISA、PCR、细胞培养等高通量实验', + }, +}; + +// 获取设备信息 +export function getDeviceInfo(deviceId: string): DeviceInfo | null { + return DEVICE_INFO[deviceId] || null; +} + +// 获取所有可交互设备列表 +export function getAllDeviceIds(): string[] { + return Object.keys(DEVICE_INFO); +} diff --git a/web/src/app/3D_lab/page.tsx b/web/src/app/3D_lab/page.tsx new file mode 100644 index 0000000..c4c8db4 --- /dev/null +++ b/web/src/app/3D_lab/page.tsx @@ -0,0 +1,224 @@ +'use client'; + +import { useUI } from '@/hooks/useUI'; +import { useEffect, useState } from 'react'; +import DeviceDetailModal from './DeviceDetailModal'; +import { getAllDeviceIds, getDeviceInfo } from './deviceInfo'; +import InteractiveLabScene from './InteractiveLabScene'; +import './styles.css'; + +export default function Lab3DPage() { + // 初始化主题系统,跟随主页设置 + useUI(); + + const [selectedDevice, setSelectedDevice] = useState(null); + const [showDeviceList, setShowDeviceList] = useState(false); + const [highlightedDevice, setHighlightedDevice] = useState( + null + ); + const [animatingDevice, setAnimatingDevice] = useState(null); + + const deviceIds = getAllDeviceIds(); + + // 处理设备点击 + const handleDeviceClick = (deviceId: string) => { + setSelectedDevice(deviceId); + setHighlightedDevice(deviceId); + // 自动开始动画演示 + setAnimatingDevice(deviceId); + }; + + // 当模态框关闭时,停止高亮和动画 + useEffect(() => { + if (!selectedDevice) { + setHighlightedDevice(null); + setAnimatingDevice(null); + } + }, [selectedDevice]); + + // 处理动画控制 + const handleToggleAnimation = (deviceId: string) => { + if (animatingDevice === deviceId) { + setAnimatingDevice(null); + } else { + setAnimatingDevice(deviceId); + } + }; + + return ( +
+ {/* 3D 交互式场景 */} + + + {/* 页面标题 */} +
+
+

+ 3D 智能实验室 +

+

+ Interactive Laboratory Visualization +

+
+
+ + {/* 设备列表按钮 */} +
+ +
+ + {/* 设备列表侧边栏 */} + {showDeviceList && ( +
+
+ {/* 头部 */} +
+

+ 实验室设备 +

+ +
+ + {/* 滚动内容区域 */} +
+
+ {deviceIds.map((deviceId) => { + const info = getDeviceInfo(deviceId); + if (!info) return null; + + return ( + + ); + })} +
+
+
+
+ )} + + {/* 操作指南 */} +
+
+
+
+ 🖱️ + 拖动旋转 +
+
+ ⚙️ + 滚轮缩放 +
+
+ 👆 + 点击设备查看 +
+
+ 📋 + 右上查看列表 +
+
+
+
+ + {/* 快速访问设备卡片(底部) */} +
+
+
+
+ {['liquid-handler', 'microscope', 'agv-robot', 'centrifuge'].map( + (deviceId) => { + const info = getDeviceInfo(deviceId); + if (!info) return null; + + return ( + + ); + } + )} +
+
+
+
+ + {/* 设备详情模态框 */} + {selectedDevice && ( + setSelectedDevice(null)} + isAnimating={animatingDevice === selectedDevice} + onToggleAnimation={() => handleToggleAnimation(selectedDevice)} + /> + )} +
+ ); +} diff --git a/web/src/app/3D_lab/styles.css b/web/src/app/3D_lab/styles.css new file mode 100644 index 0000000..73a0a84 --- /dev/null +++ b/web/src/app/3D_lab/styles.css @@ -0,0 +1,104 @@ +/* 3D Lab 页面样式 */ + +@keyframes fade-in { + from { + opacity: 0; + transform: translateY(-10px); + } + + to { + opacity: 1; + transform: translateY(0); + } +} + +.animate-fade-in { + animation: fade-in 0.3s ease-out; +} + +@keyframes pulse-slow { + + 0%, + 100% { + opacity: 1; + } + + 50% { + opacity: 0.5; + } +} + +.animate-pulse-slow { + animation: pulse-slow 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; +} + +/* 延迟动画 */ +.delay-75 { + animation-delay: 75ms; +} + +.delay-150 { + animation-delay: 150ms; +} + +/* 自定义滚动条样式 - 覆盖 Tailwind utility */ +.custom-scrollbar { + /* Firefox */ + scrollbar-width: thin !important; + scrollbar-color: rgba(156, 163, 175, 0.5) rgba(0, 0, 0, 0.05) !important; + /* 强制显示滚动条 */ + overflow-y: auto !important; + overflow-x: hidden !important; +} + +/* Webkit 浏览器(Chrome, Safari, Edge)的滚动条样式 */ +.custom-scrollbar::-webkit-scrollbar { + width: 10px !important; + height: 10px !important; + -webkit-appearance: none; + display: block !important; +} + +.custom-scrollbar::-webkit-scrollbar-track { + background: rgba(0, 0, 0, 0.05) !important; + border-radius: 5px; + margin: 4px 0; +} + +.dark .custom-scrollbar::-webkit-scrollbar-track { + background: rgba(255, 255, 255, 0.05) !important; +} + +.custom-scrollbar::-webkit-scrollbar-thumb { + background-color: rgba(156, 163, 175, 0.5) !important; + border-radius: 5px !important; + border: 2px solid transparent; + background-clip: padding-box; + -webkit-appearance: none; + min-height: 20px; +} + +.custom-scrollbar::-webkit-scrollbar-thumb:hover { + background-color: rgba(156, 163, 175, 0.6) !important; +} + +.custom-scrollbar::-webkit-scrollbar-thumb:active { + background-color: rgba(156, 163, 175, 0.8) !important; +} + +/* 深色模式下的滚动条 */ +.dark .custom-scrollbar { + scrollbar-color: rgba(156, 163, 175, 0.5) transparent !important; +} + +.dark .custom-scrollbar::-webkit-scrollbar-thumb { + background-color: rgba(156, 163, 175, 0.5) !important; +} + +.dark .custom-scrollbar::-webkit-scrollbar-thumb:hover { + background-color: rgba(156, 163, 175, 0.7) !important; +} + +.dark .custom-scrollbar::-webkit-scrollbar-thumb:active { + background-color: rgba(156, 163, 175, 0.9) !important; +} diff --git a/web/src/app/navbar/Navbar.tsx b/web/src/app/navbar/Navbar.tsx index 202e458..d38cb59 100644 --- a/web/src/app/navbar/Navbar.tsx +++ b/web/src/app/navbar/Navbar.tsx @@ -1,7 +1,7 @@ 'use client'; import Logo from '@/assets/Logo'; -import React, { useState } from 'react'; +import { useState } from 'react'; import { useTranslation } from 'react-i18next'; import About from './About'; import Community from './Community'; @@ -39,7 +39,7 @@ const NavbarMenu = () => { /> {t('navbar.tutorial')} diff --git a/web/src/app/navbar/NavbarFullWidth.tsx b/web/src/app/navbar/NavbarFullWidth.tsx index b08aad4..3347edf 100644 --- a/web/src/app/navbar/NavbarFullWidth.tsx +++ b/web/src/app/navbar/NavbarFullWidth.tsx @@ -4,6 +4,7 @@ import clsx from 'clsx'; import { Fragment } from 'react'; import { useTranslation } from 'react-i18next'; +import { Link } from 'react-router-dom'; import NavbarFullWidthFooter from './NavbarFullWidthFooter'; import type { NavbarFullWidthProps } from './types'; @@ -102,13 +103,25 @@ export default function NavbarFullWidth({ />
- - {item.name} - - + {item.href && item.href.startsWith('http') ? ( + + {item.name} + + + ) : ( + + {item.name} + + + )}

{item.description}

diff --git a/web/src/app/navbar/Projects.tsx b/web/src/app/navbar/Projects.tsx index 6db9cb6..14cdb78 100644 --- a/web/src/app/navbar/Projects.tsx +++ b/web/src/app/navbar/Projects.tsx @@ -134,7 +134,7 @@ import { // RectangleGroupIcon, } from '@heroicons/react/24/outline'; -import { SiUnrealengine,SiUnity,SiProton,SiX,SiStmicroelectronics } from 'react-icons/si'; +import { SiUnrealengine,SiUnity,SiProton,SiX,SiStmicroelectronics,SiBlender } from 'react-icons/si'; import { GitHubIcon } from '@/assets/SocialIcons'; import NavbarFullWidth from './NavbarFullWidth'; import type { NavbarFullWidthProps } from './types'; @@ -165,7 +165,7 @@ const resources = [ { name: 'Anti', description: '用于实验室模拟的3D数字孪生平台', - href: `/deepmd-kit`, + href: `/3D_lab`, icon: SiUnity, color:'text-rose-500', }, @@ -175,7 +175,14 @@ const resources = [ href: `/deepmd-kit`, icon: SiUnrealengine, color:'text-emerald-500', - } + }, + { + name:'3D Lab', + description: '展示3D实验室的实验仪器和场景', + href: `/3D_lab`, + icon: SiBlender, + color:'text-purple-500', + }, ]; const callsToAction = [ diff --git a/web/src/router.tsx b/web/src/router.tsx index 8c9ac81..c0cd42c 100644 --- a/web/src/router.tsx +++ b/web/src/router.tsx @@ -1,4 +1,5 @@ import { BrowserRouter, Route, Routes } from 'react-router-dom'; +import Lab3DPage from './app/3D_lab/page'; import App from './app/App'; import ChatPage from './app/chat/page'; import { EnvironmentPage } from './app/dashboard/environment'; @@ -22,6 +23,7 @@ export default function Router() { } /> } /> } /> + } /> {/* 所有需要侧边栏和登录保护的页面 */} }> From 0b584fad3a65c010c2b0e0cc667ef19cb5f57c7d Mon Sep 17 00:00:00 2001 From: SII-123qwe-asd-ux <1120240220@mail.nankai.edu.cn> Date: Tue, 16 Dec 2025 13:24:57 +0800 Subject: [PATCH 4/4] =?UTF-8?q?feat:=20=E5=AE=9E=E7=8E=B0=E8=B7=AF?= =?UTF-8?q?=E7=94=B1=E6=87=92=E5=8A=A0=E8=BD=BD=EF=BC=8C=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E9=A1=B5=E9=9D=A2=E5=8A=A0=E8=BD=BD=E6=80=A7=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/src/router.tsx | 75 ++++++++++++++++++++++++++++------------------ 1 file changed, 46 insertions(+), 29 deletions(-) diff --git a/web/src/router.tsx b/web/src/router.tsx index c0cd42c..ce1650a 100644 --- a/web/src/router.tsx +++ b/web/src/router.tsx @@ -1,40 +1,57 @@ -import { BrowserRouter, Route, Routes } from 'react-router-dom'; -import Lab3DPage from './app/3D_lab/page'; -import App from './app/App'; -import ChatPage from './app/chat/page'; -import { EnvironmentPage } from './app/dashboard/environment'; -import EnvironmentDetail from './app/dashboard/environment/EnvironmentDetail'; -import DashboardHome from './app/dashboard/Home'; +import { Suspense, lazy } from "react"; +import { BrowserRouter, Route, Routes } from "react-router-dom"; +import App from "./app/App"; +import ProtectedDashboardLayout from "./components/layout/ProtectedDashboardPage"; -import CallbackPage from './app/login/CallbackPage'; -import LoginPage from './app/login/LoginPage'; -import UiTestPage from './app/ui/page'; -import ProtectedDashboardLayout from './components/layout/ProtectedDashboardPage'; +// 路由懒加载 +const ChatPage = lazy(() => import("./app/chat/page")); +const EnvironmentPage = lazy(() => + import("./app/dashboard/environment").then((module) => ({ + default: module.EnvironmentPage, + })) +); +const EnvironmentDetail = lazy( + () => import("./app/dashboard/environment/EnvironmentDetail") +); +const DesktopWindow = lazy(() => import("./app/dashboard/Desktop")); +const CallbackPage = lazy(() => import("./app/login/CallbackPage")); +const LoginPage = lazy(() => import("./app/login/LoginPage")); +const UiTestPage = lazy(() => import("./app/ui/page")); +const Lab3DPage = lazy(() => import("./app/3D_lab/page")); + +const LoadingFallback = () => ( +
+
+
+); export default function Router() { return ( - + }> + {/* 根路径 - App 组件根据登录状态分流 */} - } /> + } /> - {/* 公开路由 */} - } /> - } /> - } /> - } /> - } /> + {/* 公开路由 */} + } /> + } /> + } /> + } /> + } /> - {/* 所有需要侧边栏和登录保护的页面 */} - }> - } /> - } /> - } - /> - - + {/* 所有需要侧边栏和登录保护的页面 */} + }> + {/*} />*/} + } /> + } /> + } + /> + + + ); }