dvc890 commited on
Commit
ce143cd
·
verified ·
1 Parent(s): cdef26f

Upload 65 files

Browse files
Files changed (7) hide show
  1. App.tsx +3 -9
  2. components/Sidebar.tsx +22 -26
  3. index.html +6 -2
  4. metadata.json +1 -1
  5. package.json +3 -3
  6. pages/MyClass.tsx +7 -10
  7. vite.config.ts +56 -2
App.tsx CHANGED
@@ -1,4 +1,3 @@
1
-
2
  import React, { useState, useEffect, useRef } from 'react';
3
  import { Sidebar } from './components/Sidebar';
4
  import { Header } from './components/Header';
@@ -28,14 +27,10 @@ const lazyLoad = (importFn: () => Promise<any>, name: string) => {
28
 
29
  useEffect(() => {
30
  mountedRef.current = true;
31
- // If already loaded in a previous mount (browser cache), this resolves quickly
32
- // We log to debug
33
- console.log(`[App] 🟢 Fetching: ${name}`);
34
 
35
  importFn()
36
  .then(module => {
37
  if (!mountedRef.current) return;
38
- console.log(`[App] ✅ Resolved: ${name}`);
39
  // Support both named export (priority) and default export
40
  const Comp = module[name] || module.default;
41
  if (Comp) {
@@ -47,11 +42,11 @@ const lazyLoad = (importFn: () => Promise<any>, name: string) => {
47
  })
48
  .catch(err => {
49
  if (!mountedRef.current) return;
50
- console.error(`[App] ❌ Error loading ${name}:`, err);
51
  setError(err);
52
  // Retry logic for chunks could go here, but usually reload is best
53
  if (err.message && (err.message.includes('fetch') || err.message.includes('chunk'))) {
54
- console.warn('[App] Chunk load error, suggesting reload.');
55
  }
56
  });
57
 
@@ -225,7 +220,6 @@ const AppContent: React.FC = () => {
225
  <Header user={currentUser!} title={viewTitles[currentView] || '智慧校园'} onMenuClick={() => setSidebarOpen(true)}/>
226
  <main className="flex-1 overflow-x-hidden overflow-y-auto bg-gray-50 p-4 md:p-6 w-full">
227
  <div className="max-w-7xl mx-auto w-full h-full">
228
- {/* Suspense is removed because lazyLoad now handles loading state internally */}
229
  {renderContent()}
230
  </div>
231
  </main>
@@ -243,4 +237,4 @@ const App: React.FC = () => {
243
  );
244
  };
245
 
246
- export default App;
 
 
1
  import React, { useState, useEffect, useRef } from 'react';
2
  import { Sidebar } from './components/Sidebar';
3
  import { Header } from './components/Header';
 
27
 
28
  useEffect(() => {
29
  mountedRef.current = true;
 
 
 
30
 
31
  importFn()
32
  .then(module => {
33
  if (!mountedRef.current) return;
 
34
  // Support both named export (priority) and default export
35
  const Comp = module[name] || module.default;
36
  if (Comp) {
 
42
  })
43
  .catch(err => {
44
  if (!mountedRef.current) return;
45
+ console.error(`Error loading page ${name}:`, err);
46
  setError(err);
47
  // Retry logic for chunks could go here, but usually reload is best
48
  if (err.message && (err.message.includes('fetch') || err.message.includes('chunk'))) {
49
+ console.warn('Chunk load error, suggesting reload.');
50
  }
51
  });
52
 
 
220
  <Header user={currentUser!} title={viewTitles[currentView] || '智慧校园'} onMenuClick={() => setSidebarOpen(true)}/>
221
  <main className="flex-1 overflow-x-hidden overflow-y-auto bg-gray-50 p-4 md:p-6 w-full">
222
  <div className="max-w-7xl mx-auto w-full h-full">
 
223
  {renderContent()}
224
  </div>
225
  </main>
 
237
  );
238
  };
239
 
240
+ export default App;
components/Sidebar.tsx CHANGED
@@ -1,5 +1,5 @@
1
 
2
- import React, { useState, useEffect } from 'react';
3
  import { LayoutDashboard, Users, BookOpen, GraduationCap, Settings, LogOut, FileText, School, UserCog, Palette, X, Building, Gamepad2, CalendarCheck, UserCircle, MessageSquare, Bot, ArrowUp, ArrowDown, Save, UserCheck } from 'lucide-react';
4
  import { UserRole } from '../types';
5
  import { api } from '../services/api';
@@ -20,17 +20,11 @@ interface MenuItem {
20
  roles: UserRole[];
21
  }
22
 
23
- export const Sidebar: React.FC<SidebarProps> = ({ currentView, onChangeView, userRole, onLogout, isOpen, onClose }) => {
24
- const currentUser = api.auth.getCurrentUser();
25
- const canSeeAI = userRole === UserRole.ADMIN || (userRole === UserRole.TEACHER && currentUser?.aiAccess);
26
- const isHomeroom = userRole === UserRole.TEACHER && !!currentUser?.homeroomClass;
27
-
28
- // Default Items Definition
29
- // We define this inside render or memoize it, but it needs to react to isHomeroom
30
- const defaultItems: MenuItem[] = [
31
  { id: 'dashboard', label: '工作台', icon: LayoutDashboard, roles: [UserRole.ADMIN, UserRole.PRINCIPAL, UserRole.TEACHER, UserRole.STUDENT] },
32
- { id: 'my-class', label: '我的班级', icon: UserCheck, roles: isHomeroom ? [UserRole.TEACHER] : [] },
33
- { id: 'ai-assistant', label: 'AI 智能助教', icon: Bot, roles: canSeeAI ? [UserRole.ADMIN, UserRole.TEACHER] : [] },
34
  { id: 'attendance', label: '考勤管理', icon: CalendarCheck, roles: [UserRole.TEACHER, UserRole.PRINCIPAL] },
35
  { id: 'games', label: '互动教学', icon: Gamepad2, roles: [UserRole.TEACHER, UserRole.STUDENT] },
36
  { id: 'wishes', label: '心愿与反馈', icon: MessageSquare, roles: [UserRole.ADMIN, UserRole.PRINCIPAL, UserRole.TEACHER, UserRole.STUDENT] },
@@ -44,31 +38,37 @@ export const Sidebar: React.FC<SidebarProps> = ({ currentView, onChangeView, use
44
  { id: 'users', label: '用户管理', icon: UserCog, roles: [UserRole.ADMIN, UserRole.PRINCIPAL, UserRole.TEACHER] },
45
  { id: 'profile', label: '个人中心', icon: UserCircle, roles: [UserRole.ADMIN, UserRole.PRINCIPAL, UserRole.TEACHER, UserRole.STUDENT] },
46
  { id: 'settings', label: '系统设置', icon: Settings, roles: [UserRole.ADMIN, UserRole.PRINCIPAL] },
47
- ];
 
 
 
 
 
48
 
49
- const [menuItems, setMenuItems] = useState<MenuItem[]>(defaultItems);
50
  const [isEditing, setIsEditing] = useState(false);
51
 
52
- // Sync menu items when currentUser (and thus isHomeroom/permissions) changes
53
  useEffect(() => {
54
  let ordered: MenuItem[] = [];
55
-
56
- if (currentUser?.menuOrder && currentUser.menuOrder.length > 0) {
57
- const map = new Map(defaultItems.map(i => [i.id, i]));
 
58
  // Add saved items in order
59
- currentUser.menuOrder.forEach(id => {
60
  if (map.has(id)) {
61
  ordered.push(map.get(id)!);
62
  map.delete(id);
63
  }
64
  });
65
- // Append any new/remaining items (crucial for new features like 'my-class')
66
  map.forEach(item => ordered.push(item));
67
  } else {
68
- ordered = defaultItems;
69
  }
70
  setMenuItems(ordered);
71
- }, [currentUser?.menuOrder, isHomeroom, canSeeAI]); // Re-run if order, homeroom status, or AI access changes
72
 
73
  const handleMove = (index: number, direction: -1 | 1) => {
74
  const newItems = [...menuItems];
@@ -84,10 +84,8 @@ export const Sidebar: React.FC<SidebarProps> = ({ currentView, onChangeView, use
84
  if (currentUser && currentUser._id) {
85
  try {
86
  await api.users.saveMenuOrder(currentUser._id, orderIds);
87
- // Update local user object
88
  const updatedUser = { ...currentUser, menuOrder: orderIds };
89
  localStorage.setItem('user', JSON.stringify(updatedUser));
90
- // Force update logic (though App.tsx usually handles user state, this helps local consistency)
91
  } catch(e) { console.error("Failed to save menu order"); }
92
  }
93
  };
@@ -125,11 +123,9 @@ export const Sidebar: React.FC<SidebarProps> = ({ currentView, onChangeView, use
125
  </div>
126
  <nav className="space-y-1 px-2">
127
  {menuItems.map((item, idx) => {
128
- // Filtering Logic
129
  if (item.roles.length > 0 && !item.roles.includes(userRole)) return null;
130
-
131
- // Special feature toggles
132
  if (item.id === 'ai-assistant' && !canSeeAI) return null;
 
133
  if (item.id === 'my-class' && !isHomeroom) return null;
134
 
135
  const Icon = item.icon;
 
1
 
2
+ import React, { useState, useEffect, useMemo } from 'react';
3
  import { LayoutDashboard, Users, BookOpen, GraduationCap, Settings, LogOut, FileText, School, UserCog, Palette, X, Building, Gamepad2, CalendarCheck, UserCircle, MessageSquare, Bot, ArrowUp, ArrowDown, Save, UserCheck } from 'lucide-react';
4
  import { UserRole } from '../types';
5
  import { api } from '../services/api';
 
20
  roles: UserRole[];
21
  }
22
 
23
+ // Define static items outside component to prevent recreation
24
+ const STATIC_MENU_ITEMS: MenuItem[] = [
 
 
 
 
 
 
25
  { id: 'dashboard', label: '工作台', icon: LayoutDashboard, roles: [UserRole.ADMIN, UserRole.PRINCIPAL, UserRole.TEACHER, UserRole.STUDENT] },
26
+ { id: 'my-class', label: '我的班级', icon: UserCheck, roles: [UserRole.TEACHER] },
27
+ { id: 'ai-assistant', label: 'AI 智能助教', icon: Bot, roles: [UserRole.ADMIN, UserRole.TEACHER] },
28
  { id: 'attendance', label: '考勤管理', icon: CalendarCheck, roles: [UserRole.TEACHER, UserRole.PRINCIPAL] },
29
  { id: 'games', label: '互动教学', icon: Gamepad2, roles: [UserRole.TEACHER, UserRole.STUDENT] },
30
  { id: 'wishes', label: '心愿与反馈', icon: MessageSquare, roles: [UserRole.ADMIN, UserRole.PRINCIPAL, UserRole.TEACHER, UserRole.STUDENT] },
 
38
  { id: 'users', label: '用户管理', icon: UserCog, roles: [UserRole.ADMIN, UserRole.PRINCIPAL, UserRole.TEACHER] },
39
  { id: 'profile', label: '个人中心', icon: UserCircle, roles: [UserRole.ADMIN, UserRole.PRINCIPAL, UserRole.TEACHER, UserRole.STUDENT] },
40
  { id: 'settings', label: '系统设置', icon: Settings, roles: [UserRole.ADMIN, UserRole.PRINCIPAL] },
41
+ ];
42
+
43
+ export const Sidebar: React.FC<SidebarProps> = ({ currentView, onChangeView, userRole, onLogout, isOpen, onClose }) => {
44
+ const currentUser = api.auth.getCurrentUser();
45
+ const canSeeAI = userRole === UserRole.ADMIN || (userRole === UserRole.TEACHER && currentUser?.aiAccess);
46
+ const isHomeroom = userRole === UserRole.TEACHER && !!currentUser?.homeroomClass;
47
 
48
+ const [menuItems, setMenuItems] = useState<MenuItem[]>(STATIC_MENU_ITEMS);
49
  const [isEditing, setIsEditing] = useState(false);
50
 
51
+ // Memoize logic to avoid recalculation loops
52
  useEffect(() => {
53
  let ordered: MenuItem[] = [];
54
+ const userOrder = currentUser?.menuOrder || [];
55
+
56
+ if (userOrder.length > 0) {
57
+ const map = new Map(STATIC_MENU_ITEMS.map(i => [i.id, i]));
58
  // Add saved items in order
59
+ userOrder.forEach(id => {
60
  if (map.has(id)) {
61
  ordered.push(map.get(id)!);
62
  map.delete(id);
63
  }
64
  });
65
+ // Append remaining items
66
  map.forEach(item => ordered.push(item));
67
  } else {
68
+ ordered = [...STATIC_MENU_ITEMS];
69
  }
70
  setMenuItems(ordered);
71
+ }, [currentUser?.menuOrder]); // Only re-run if the order specifically changes
72
 
73
  const handleMove = (index: number, direction: -1 | 1) => {
74
  const newItems = [...menuItems];
 
84
  if (currentUser && currentUser._id) {
85
  try {
86
  await api.users.saveMenuOrder(currentUser._id, orderIds);
 
87
  const updatedUser = { ...currentUser, menuOrder: orderIds };
88
  localStorage.setItem('user', JSON.stringify(updatedUser));
 
89
  } catch(e) { console.error("Failed to save menu order"); }
90
  }
91
  };
 
123
  </div>
124
  <nav className="space-y-1 px-2">
125
  {menuItems.map((item, idx) => {
 
126
  if (item.roles.length > 0 && !item.roles.includes(userRole)) return null;
 
 
127
  if (item.id === 'ai-assistant' && !canSeeAI) return null;
128
+ // Specific check for homeroom class feature
129
  if (item.id === 'my-class' && !isHomeroom) return null;
130
 
131
  const Icon = item.icon;
index.html CHANGED
@@ -2,7 +2,10 @@
2
  <html lang="zh-CN">
3
  <head>
4
  <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width, initial-scale=1" />
 
 
 
6
  <title>智慧校园管理系统</title>
7
  <style>
8
  /* Critical CSS for immediate loading state */
@@ -34,7 +37,8 @@
34
  "xlsx": "https://aistudiocdn.com/xlsx@^0.18.5",
35
  "@google/genai": "https://esm.sh/@google/genai@^1.33.0",
36
  "react-markdown": "https://esm.sh/react-markdown@^10.1.0",
37
- "remark-gfm": "https://esm.sh/remark-gfm@^4.0.1"
 
38
  }
39
  }
40
  </script>
 
2
  <html lang="zh-CN">
3
  <head>
4
  <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" />
6
+ <meta name="theme-color" content="#2563eb" />
7
+ <meta name="apple-mobile-web-app-capable" content="yes">
8
+ <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
9
  <title>智慧校园管理系统</title>
10
  <style>
11
  /* Critical CSS for immediate loading state */
 
37
  "xlsx": "https://aistudiocdn.com/xlsx@^0.18.5",
38
  "@google/genai": "https://esm.sh/@google/genai@^1.33.0",
39
  "react-markdown": "https://esm.sh/react-markdown@^10.1.0",
40
+ "remark-gfm": "https://esm.sh/remark-gfm@^4.0.1",
41
+ "vite-plugin-pwa": "https://esm.sh/vite-plugin-pwa@^1.2.0"
42
  }
43
  }
44
  </script>
metadata.json CHANGED
@@ -1,5 +1,5 @@
1
  {
2
- "name": "(AI)智慧校园管理系统",
3
  "description": "一个综合性的学生管理系统仪表板,具有基于角色的访问控制、学生档案、课程管理和绩效分析功能。",
4
  "requestFramePermissions": []
5
  }
 
1
  {
2
+ "name": "(PWA)(AI)智慧校园管理系统",
3
  "description": "一个综合性的学生管理系统仪表板,具有基于角色的访问控制、学生档案、课程管理和绩效分析功能。",
4
  "requestFramePermissions": []
5
  }
package.json CHANGED
@@ -1,4 +1,3 @@
1
-
2
  {
3
  "name": "smart-school-system",
4
  "version": "1.0.0",
@@ -36,6 +35,7 @@
36
  "postcss": "^8.4.32",
37
  "tailwindcss": "^3.4.0",
38
  "typescript": "^5.2.2",
39
- "vite": "^5.0.8"
 
40
  }
41
- }
 
 
1
  {
2
  "name": "smart-school-system",
3
  "version": "1.0.0",
 
35
  "postcss": "^8.4.32",
36
  "tailwindcss": "^3.4.0",
37
  "typescript": "^5.2.2",
38
+ "vite": "^5.0.8",
39
+ "vite-plugin-pwa": "^0.21.1"
40
  }
41
+ }
pages/MyClass.tsx CHANGED
@@ -67,14 +67,11 @@ export const MyClass: React.FC = () => {
67
  setMyClassInfo(cls);
68
  if (cls.seatingConfig) {
69
  setRows(cls.seatingConfig.rows || 6);
70
- // Determine visual cols based on data. Default assumption 2 per desk if not saved metadata
71
- // Since backend only stores total cols, we try to infer or default.
72
- // To keep it simple, we default deskCols based on total cols / 2, but let user adjust.
73
  const savedTotalCols = cls.seatingConfig.cols || 8;
74
- // Logic: If saved config exists, we try to approximate.
75
- // Better approach: Just default to sensible values if we can't persist extra metadata yet.
76
- setDeskCols(Math.ceil(savedTotalCols / 2));
77
- setGroupSize(2);
78
  }
79
  }
80
  } catch (e) { console.error(e); }
@@ -561,9 +558,9 @@ export const MyClass: React.FC = () => {
561
 
562
  {/* Center: Grid */}
563
  <div className="flex-1 p-8 overflow-auto flex flex-col items-center bg-gray-100/50">
564
- {/* Classic Blackboard */}
565
- <div className="w-[60%] h-12 bg-[#2d3748] rounded mb-12 flex items-center justify-center shadow-md border-b-4 border-[#1a202c] shrink-0 select-none">
566
- <span className="text-white/80 font-bold tracking-[0.5em] text-sm">讲台 (TEACHER)</span>
567
  </div>
568
 
569
  <div
 
67
  setMyClassInfo(cls);
68
  if (cls.seatingConfig) {
69
  setRows(cls.seatingConfig.rows || 6);
 
 
 
70
  const savedTotalCols = cls.seatingConfig.cols || 8;
71
+ // Try to infer config from saved total columns if we don't have detailed metadata
72
+ const inferredGroupSize = 2; // Default reasonable guess
73
+ setDeskCols(Math.ceil(savedTotalCols / inferredGroupSize));
74
+ setGroupSize(inferredGroupSize);
75
  }
76
  }
77
  } catch (e) { console.error(e); }
 
558
 
559
  {/* Center: Grid */}
560
  <div className="flex-1 p-8 overflow-auto flex flex-col items-center bg-gray-100/50">
561
+ {/* Blackboard / Lectern Area */}
562
+ <div className="w-[60%] h-16 bg-gray-700 rounded-b-xl mb-12 flex items-center justify-center shadow-lg text-gray-300 font-bold tracking-[1em] text-lg border-b-4 border-gray-600 shrink-0 select-none">
563
+ 讲台 ( T E A C H E R )
564
  </div>
565
 
566
  <div
vite.config.ts CHANGED
@@ -1,11 +1,65 @@
1
  import { defineConfig } from 'vite';
2
  import react from '@vitejs/plugin-react';
 
3
 
4
  // https://vitejs.dev/config/
5
  export default defineConfig({
6
- plugins: [react()],
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
  build: {
8
- outDir: 'dist', // 确保编译输出到 dist 目录
9
  emptyOutDir: true,
10
  chunkSizeWarningLimit: 1000,
11
  rollupOptions: {
 
1
  import { defineConfig } from 'vite';
2
  import react from '@vitejs/plugin-react';
3
+ import { VitePWA } from 'vite-plugin-pwa';
4
 
5
  // https://vitejs.dev/config/
6
  export default defineConfig({
7
+ plugins: [
8
+ react(),
9
+ VitePWA({
10
+ registerType: 'autoUpdate', // 核心设置:检测到新内容自动更新 Service Worker
11
+ includeAssets: ['favicon.ico', 'apple-touch-icon.png', 'masked-icon.svg'],
12
+ manifest: {
13
+ name: '智慧校园管理系统',
14
+ short_name: '智慧校园',
15
+ description: '一个综合性的学生管理系统仪表板,具有基于角色的访问控制、学生档案、课程管理和绩效分析功能。',
16
+ theme_color: '#2563eb', // 对应 bg-blue-600
17
+ background_color: '#f9fafb',
18
+ display: 'standalone', // 关键设置:像原生 App 一样全屏显示,无浏览器地址栏
19
+ orientation: 'portrait',
20
+ icons: [
21
+ {
22
+ src: 'https://cdn-icons-png.flaticon.com/512/3135/3135810.png', // 临时使用通用教育图标,实际项目中请替换为本地 public/pwa-192x192.png
23
+ sizes: '192x192',
24
+ type: 'image/png'
25
+ },
26
+ {
27
+ src: 'https://cdn-icons-png.flaticon.com/512/3135/3135810.png', // 临时使用通用教育图标,实际项目中请替换为本地 public/pwa-512x512.png
28
+ sizes: '512x512',
29
+ type: 'image/png'
30
+ },
31
+ {
32
+ src: 'https://cdn-icons-png.flaticon.com/512/3135/3135810.png',
33
+ sizes: '512x512',
34
+ type: 'image/png',
35
+ purpose: 'any maskable'
36
+ }
37
+ ]
38
+ },
39
+ workbox: {
40
+ // 缓存策略配置,确保 API 请求不被过度缓存,但静态资源被缓存
41
+ globPatterns: ['**/*.{js,css,html,ico,png,svg}'],
42
+ runtimeCaching: [
43
+ {
44
+ urlPattern: /^https:\/\/api\.dicebear\.com\/.*/i,
45
+ handler: 'CacheFirst',
46
+ options: {
47
+ cacheName: 'avatar-cache',
48
+ expiration: {
49
+ maxEntries: 100,
50
+ maxAgeSeconds: 60 * 60 * 24 * 30 // 30 Days
51
+ },
52
+ cacheableResponse: {
53
+ statuses: [0, 200]
54
+ }
55
+ }
56
+ }
57
+ ]
58
+ }
59
+ })
60
+ ],
61
  build: {
62
+ outDir: 'dist',
63
  emptyOutDir: true,
64
  chunkSizeWarningLimit: 1000,
65
  rollupOptions: {