Skip to content

Commit 1bc36cf

Browse files
committed
feat: supprt pwa
1 parent 228eff8 commit 1bc36cf

9 files changed

Lines changed: 3312 additions & 41 deletions

File tree

web/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,4 @@ dist-ssr
2626

2727
# component examples
2828
*/components/shadcn-studio/**
29+
**/dev-dist/

web/index.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
/>
1616
<link rel="stylesheet" href="./index.css" />
1717
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
18+
<meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)" />
19+
<meta name="theme-color" content="#000000" media="(prefers-color-scheme: dark)" />
1820
<title>ScienceOL</title>
1921
</head>
2022
<body>

web/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@
6868
"tw-animate-css": "^1.4.0",
6969
"typescript": "~5.8.3",
7070
"typescript-eslint": "^8.44.0",
71-
"vite": "^7.1.7"
71+
"vite": "^7.1.7",
72+
"vite-plugin-pwa": "^1.2.0"
7273
}
7374
}

web/public/pwa-icon.png

95.2 KB
Loading

web/src/app/App.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
*/
44

55
import LogoLoading from '@/components/basic/loading';
6+
import MobileOverlay from '@/components/mobile-overlay';
67
import { useAuth } from '@/hooks/useAuth';
78

89
import { useUI } from '@/hooks/useUI';
@@ -34,5 +35,10 @@ export default function App() {
3435
}
3536

3637
// 渲染落地页
37-
return <LandscapePage />;
38+
return (
39+
<>
40+
<MobileOverlay />
41+
<LandscapePage />
42+
</>
43+
);
3844
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { Laptop } from 'lucide-react';
2+
import { useEffect, useState } from 'react';
3+
4+
const MobileOverlay = () => {
5+
const [isMobile, setIsMobile] = useState(false);
6+
7+
useEffect(() => {
8+
const checkDevice = () => {
9+
// 检查屏幕宽度是否小于 768px (常见的平板/移动端断点)
10+
const isSmallScreen = window.innerWidth < 768;
11+
12+
// 检查是否为移动设备用户代理
13+
const isMobileDevice =
14+
/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
15+
navigator.userAgent
16+
);
17+
18+
setIsMobile(isSmallScreen || isMobileDevice);
19+
};
20+
21+
// 初始检查
22+
checkDevice();
23+
24+
// 监听窗口大小变化
25+
window.addEventListener('resize', checkDevice);
26+
27+
return () => {
28+
window.removeEventListener('resize', checkDevice);
29+
};
30+
}, []);
31+
32+
if (!isMobile) return null;
33+
34+
return (
35+
<div className="fixed inset-0 z-[9999] flex flex-col items-center justify-center bg-background/95 backdrop-blur-sm p-6 text-center">
36+
<div className="max-w-md space-y-6">
37+
<div className="flex justify-center">
38+
<div className="relative">
39+
<div className="absolute -inset-1 rounded-full bg-gradient-to-r from-primary to-purple-600 blur opacity-75 animate-pulse"></div>
40+
<div className="relative bg-background rounded-full p-4 border border-border">
41+
<Laptop className="w-12 h-12 text-primary" />
42+
</div>
43+
</div>
44+
</div>
45+
46+
<div className="space-y-2">
47+
<h2 className="text-2xl font-bold tracking-tight text-foreground">
48+
请使用桌面端访问
49+
</h2>
50+
<p className="text-muted-foreground text-sm leading-relaxed">
51+
为了获得最佳的使用体验,当前应用仅支持在平板/桌面端设备(PC/Mac)上运行。
52+
<br />
53+
请切换至大屏设备继续操作。
54+
</p>
55+
</div>
56+
57+
<div className="pt-4">
58+
<p className="text-xs text-muted-foreground/50">
59+
建议分辨率 &gt; 768px
60+
</p>
61+
</div>
62+
</div>
63+
</div>
64+
);
65+
};
66+
67+
export default MobileOverlay;

web/src/router.tsx

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,35 @@
1+
import { Suspense, lazy } from "react";
12
import { BrowserRouter, Route, Routes } from "react-router-dom";
23
import App from "./app/App";
3-
import ChatPage from "./app/chat/page";
4-
import { EnvironmentPage } from "./app/dashboard/environment";
5-
import EnvironmentDetail from "./app/dashboard/environment/EnvironmentDetail";
6-
// import DashboardHome from "./app/dashboard/Home";
7-
import DesktopWindow from "./app/dashboard/Desktop";
8-
import CallbackPage from "./app/login/CallbackPage";
9-
import LoginPage from "./app/login/LoginPage";
10-
import UiTestPage from "./app/ui/page";
114
import ProtectedDashboardLayout from "./components/layout/ProtectedDashboardPage";
125

6+
// 路由懒加载
7+
const ChatPage = lazy(() => import("./app/chat/page"));
8+
const EnvironmentPage = lazy(() =>
9+
import("./app/dashboard/environment").then((module) => ({
10+
default: module.EnvironmentPage,
11+
}))
12+
);
13+
const EnvironmentDetail = lazy(
14+
() => import("./app/dashboard/environment/EnvironmentDetail")
15+
);
16+
const DesktopWindow = lazy(() => import("./app/dashboard/Desktop"));
17+
const CallbackPage = lazy(() => import("./app/login/CallbackPage"));
18+
const LoginPage = lazy(() => import("./app/login/LoginPage"));
19+
const UiTestPage = lazy(() => import("./app/ui/page"));
20+
21+
const LoadingFallback = () => (
22+
<div className="flex h-screen w-full items-center justify-center bg-gray-50">
23+
<div className="h-8 w-8 animate-spin rounded-full border-4 border-gray-200 border-t-blue-500"></div>
24+
</div>
25+
);
26+
1327
export default function Router() {
1428
return (
1529
<BrowserRouter>
16-
<Routes>
17-
{/* 根路径 - App 组件根据登录状态分流 */}
30+
<Suspense fallback={<LoadingFallback />}>
31+
<Routes>
32+
{/* 根路径 - App 组件根据登录状态分流 */}
1833
<Route path="/" element={<App />} />
1934

2035
{/* 公开路由 */}
@@ -33,7 +48,8 @@ export default function Router() {
3348
element={<EnvironmentDetail />}
3449
/>
3550
</Route>
36-
</Routes>
51+
</Routes>
52+
</Suspense>
3753
</BrowserRouter>
3854
);
3955
}

web/vite.config.ts

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,38 @@ import tailwindcss from '@tailwindcss/vite';
22
import react from '@vitejs/plugin-react-swc';
33
import { resolve } from 'path';
44
import { defineConfig } from 'vite';
5+
import { VitePWA } from 'vite-plugin-pwa';
56

67
// https://vite.dev/config/
78
export default defineConfig(({ command }) => ({
8-
plugins: [react(), tailwindcss()],
9+
plugins: [
10+
react(),
11+
tailwindcss(),
12+
VitePWA({
13+
registerType: 'autoUpdate',
14+
includeAssets: ['favicon.ico', 'apple-touch-icon.png', 'mask-icon.svg'],
15+
manifest: {
16+
name: 'ScienceOL',
17+
short_name: 'ScienceOL',
18+
description: 'ScienceOL Application',
19+
display: 'standalone',
20+
display_override: ['window-controls-overlay'],
21+
icons: [
22+
{
23+
src: 'pwa-icon.png',
24+
sizes: '192x192 512x512',
25+
type: 'image/svg+xml',
26+
},
27+
],
28+
},
29+
devOptions: {
30+
enabled: true,
31+
},
32+
workbox: {
33+
maximumFileSizeToCacheInBytes: 10 * 1024 * 1024, // 10 MiB
34+
},
35+
}),
36+
],
937
resolve: {
1038
alias: {
1139
'@': resolve(__dirname, './src'),
@@ -30,4 +58,21 @@ export default defineConfig(({ command }) => ({
3058
command === 'build'
3159
? { pure: ['console.log', 'console.debug'] }
3260
: undefined,
61+
build: {
62+
rollupOptions: {
63+
output: {
64+
manualChunks: {
65+
'react-vendor': ['react', 'react-dom', 'react-router-dom'],
66+
'three-vendor': ['three', '@react-three/fiber', '@react-three/drei'],
67+
'monaco-vendor': ['monaco-editor', '@monaco-editor/react'],
68+
'ui-vendor': [
69+
'@headlessui/react',
70+
'@radix-ui/react-slot',
71+
'framer-motion',
72+
'motion',
73+
],
74+
},
75+
},
76+
},
77+
},
3378
}));

0 commit comments

Comments
 (0)