dvc890 commited on
Commit
151e61b
·
verified ·
1 Parent(s): 2451f7b

Upload 67 files

Browse files
Files changed (3) hide show
  1. App.tsx +22 -6
  2. components/Sidebar.tsx +36 -51
  3. utils/documentParser.ts +35 -35
App.tsx CHANGED
@@ -243,8 +243,18 @@ const AppContent: React.FC = () => {
243
 
244
  const showLiveAssistant = currentUser && (currentUser.role === UserRole.ADMIN || currentUser.aiAccess);
245
 
 
 
 
 
 
 
 
 
 
 
246
  return (
247
- // Changed h-screen to fixed inset-0 to prevent body scrolling on mobile
248
  <div className="fixed inset-0 flex bg-gray-50 overflow-hidden">
249
  <Sidebar
250
  currentView={currentView}
@@ -255,13 +265,19 @@ const AppContent: React.FC = () => {
255
  onClose={() => setSidebarOpen(false)}
256
  />
257
 
258
- <div className="flex-1 flex flex-col w-full relative h-full">
259
  <Header user={currentUser!} title={viewTitles[currentView] || '智慧校园'} onMenuClick={() => setSidebarOpen(true)}/>
260
- <main className="flex-1 overflow-x-hidden overflow-y-auto bg-gray-50 p-4 md:p-6 w-full relative">
261
- <div className="max-w-7xl mx-auto w-full min-h-full">
262
- {renderContent()}
263
- </div>
 
 
 
 
 
264
  </main>
 
265
  {showLiveAssistant && <LiveAssistant />}
266
  </div>
267
  </div>
 
243
 
244
  const showLiveAssistant = currentUser && (currentUser.role === UserRole.ADMIN || currentUser.aiAccess);
245
 
246
+ // Layout Logic:
247
+ // For 'ai-assistant' and 'games', we want full height without internal padding from Main,
248
+ // so the component can manage its own scroll/flex layout (sticky headers/footers).
249
+ // For other pages, we use the standard scrolling Main container.
250
+ const isAppLikePage = currentView === 'ai-assistant' || currentView === 'games';
251
+
252
+ const mainClasses = isAppLikePage
253
+ ? "flex-1 overflow-hidden relative bg-slate-50 w-full"
254
+ : "flex-1 overflow-x-hidden overflow-y-auto bg-gray-50 p-4 md:p-6 w-full relative";
255
+
256
  return (
257
+ // Fixed inset-0 prevents body scroll, everything happens inside
258
  <div className="fixed inset-0 flex bg-gray-50 overflow-hidden">
259
  <Sidebar
260
  currentView={currentView}
 
265
  onClose={() => setSidebarOpen(false)}
266
  />
267
 
268
+ <div className="flex-1 flex flex-col w-full relative h-full min-w-0">
269
  <Header user={currentUser!} title={viewTitles[currentView] || '智慧校园'} onMenuClick={() => setSidebarOpen(true)}/>
270
+
271
+ <main className={mainClasses}>
272
+ {isAppLikePage ? (
273
+ renderContent()
274
+ ) : (
275
+ <div className="max-w-7xl mx-auto w-full min-h-full">
276
+ {renderContent()}
277
+ </div>
278
+ )}
279
  </main>
280
+
281
  {showLiveAssistant && <LiveAssistant />}
282
  </div>
283
  </div>
components/Sidebar.tsx CHANGED
@@ -19,11 +19,16 @@ interface MenuItem {
19
  roles: UserRole[];
20
  }
21
 
22
- // Define static items outside component to prevent recreation
23
- const STATIC_MENU_ITEMS: MenuItem[] = [
 
 
 
 
 
24
  { id: 'dashboard', label: '工作台', icon: LayoutDashboard, roles: [UserRole.ADMIN, UserRole.PRINCIPAL, UserRole.TEACHER, UserRole.STUDENT] },
25
- { id: 'my-class', label: '我的班级', icon: UserCheck, roles: [UserRole.TEACHER] },
26
- { id: 'ai-assistant', label: 'AI 智能助教', icon: Bot, roles: [UserRole.ADMIN, UserRole.TEACHER] },
27
  { id: 'attendance', label: '考勤管理', icon: CalendarCheck, roles: [UserRole.TEACHER, UserRole.PRINCIPAL] },
28
  { id: 'games', label: '互动教学', icon: Gamepad2, roles: [UserRole.TEACHER, UserRole.STUDENT] },
29
  { id: 'wishes', label: '心愿与反馈', icon: MessageSquare, roles: [UserRole.ADMIN, UserRole.PRINCIPAL, UserRole.TEACHER, UserRole.STUDENT] },
@@ -37,91 +42,72 @@ const STATIC_MENU_ITEMS: MenuItem[] = [
37
  { id: 'users', label: '用户管理', icon: UserCog, roles: [UserRole.ADMIN, UserRole.PRINCIPAL, UserRole.TEACHER] },
38
  { id: 'profile', label: '个人中心', icon: UserCircle, roles: [UserRole.ADMIN, UserRole.PRINCIPAL, UserRole.TEACHER, UserRole.STUDENT] },
39
  { id: 'settings', label: '系统设置', icon: Settings, roles: [UserRole.ADMIN, UserRole.PRINCIPAL] },
40
- ];
41
-
42
- export const Sidebar: React.FC<SidebarProps> = ({ currentView, onChangeView, userRole, onLogout, isOpen, onClose }) => {
43
- const currentUser = api.auth.getCurrentUser();
44
- const canSeeAI = userRole === UserRole.ADMIN || (userRole === UserRole.TEACHER && currentUser?.aiAccess);
45
- const isHomeroom = userRole === UserRole.TEACHER && !!currentUser?.homeroomClass;
46
 
47
- const [menuItems, setMenuItems] = useState<MenuItem[]>(STATIC_MENU_ITEMS);
48
  const [isEditing, setIsEditing] = useState(false);
49
  const [installPrompt, setInstallPrompt] = useState<any>(null);
50
  const [isStandalone, setIsStandalone] = useState(false);
51
 
52
- // Capture the PWA install prompt event
53
  useEffect(() => {
54
- // 1. Check if already running in standalone mode (installed)
55
- const isStandaloneMode = window.matchMedia('(display-mode: standalone)').matches ||
56
- (window.navigator as any).standalone === true;
57
- setIsStandalone(isStandaloneMode);
58
-
59
- if (isStandaloneMode) return;
 
 
60
 
61
- // 2. Check if the event was already captured globally (in index.html)
62
  if ((window as any).deferredPrompt) {
63
- console.log('⚡ Using globally captured PWA prompt');
64
  setInstallPrompt((window as any).deferredPrompt);
65
  }
66
 
67
- // 3. Setup listener for future events (if not yet captured)
68
  const handler = (e: any) => {
69
  e.preventDefault();
70
- // Update global and local state
71
  (window as any).deferredPrompt = e;
72
  setInstallPrompt(e);
73
- console.log('⚡ PWA prompt event fired in component');
74
  };
75
 
76
  window.addEventListener('beforeinstallprompt', handler);
77
- return () => window.removeEventListener('beforeinstallprompt', handler);
 
 
 
78
  }, []);
79
 
80
  const handleInstallClick = async () => {
81
- // Prefer local state, fallback to global
82
  const promptEvent = installPrompt || (window as any).deferredPrompt;
83
-
84
  if (!promptEvent) {
85
  alert('安装功能当前不可用。请尝试点击浏览器菜单中的“添加到主屏幕”或“安装应用”。');
86
  return;
87
  }
88
-
89
- // Show the install prompt
90
  promptEvent.prompt();
91
-
92
- // Wait for the user to respond to the prompt
93
  const { outcome } = await promptEvent.userChoice;
94
-
95
  if (outcome === 'accepted') {
96
- console.log('User accepted the install prompt');
97
  setInstallPrompt(null);
98
  (window as any).deferredPrompt = null;
99
- } else {
100
- console.log('User dismissed the install prompt');
101
  }
102
  };
103
 
104
- // Memoize logic to avoid recalculation loops
105
  useEffect(() => {
106
- let ordered: MenuItem[] = [];
107
- const userOrder = currentUser?.menuOrder || [];
108
-
109
- if (userOrder.length > 0) {
110
- const map = new Map(STATIC_MENU_ITEMS.map(i => [i.id, i]));
111
  // Add saved items in order
112
- userOrder.forEach(id => {
113
  if (map.has(id)) {
114
  ordered.push(map.get(id)!);
115
  map.delete(id);
116
  }
117
  });
118
- // Append remaining items
119
  map.forEach(item => ordered.push(item));
120
- } else {
121
- ordered = [...STATIC_MENU_ITEMS];
122
  }
123
- setMenuItems(ordered);
124
- }, [currentUser?.menuOrder]); // Only re-run if the order specifically changes
125
 
126
  const handleMove = (index: number, direction: -1 | 1) => {
127
  const newItems = [...menuItems];
@@ -137,13 +123,13 @@ export const Sidebar: React.FC<SidebarProps> = ({ currentView, onChangeView, use
137
  if (currentUser && currentUser._id) {
138
  try {
139
  await api.users.saveMenuOrder(currentUser._id, orderIds);
 
140
  const updatedUser = { ...currentUser, menuOrder: orderIds };
141
  localStorage.setItem('user', JSON.stringify(updatedUser));
142
  } catch(e) { console.error("Failed to save menu order"); }
143
  }
144
  };
145
 
146
- // Added 'flex flex-col h-full' to ensure proper layout on mobile
147
  const sidebarClasses = `
148
  fixed inset-y-0 left-0 z-50 w-64 bg-slate-900 text-white transition-transform duration-300 ease-in-out transform
149
  flex flex-col h-full
@@ -180,7 +166,7 @@ export const Sidebar: React.FC<SidebarProps> = ({ currentView, onChangeView, use
180
  {menuItems.map((item, idx) => {
181
  if (item.roles.length > 0 && !item.roles.includes(userRole)) return null;
182
  if (item.id === 'ai-assistant' && !canSeeAI) return null;
183
- // Specific check for homeroom class feature
184
  if (item.id === 'my-class' && !isHomeroom) return null;
185
 
186
  const Icon = item.icon;
@@ -212,7 +198,7 @@ export const Sidebar: React.FC<SidebarProps> = ({ currentView, onChangeView, use
212
  </nav>
213
  </div>
214
 
215
- <div className="p-4 border-t border-slate-700 space-y-2 shrink-0">
216
  {!isStandalone && installPrompt && (
217
  <button onClick={handleInstallClick} className="w-full flex items-center space-x-3 px-4 py-3 rounded-lg bg-gradient-to-r from-blue-600 to-indigo-600 text-white hover:shadow-lg transition-all duration-200 animate-in fade-in slide-in-from-bottom-2">
218
  <Download size={20} />
@@ -220,7 +206,6 @@ export const Sidebar: React.FC<SidebarProps> = ({ currentView, onChangeView, use
220
  </button>
221
  )}
222
 
223
- {/* Fallback for iOS/Manual if prompt missing but not installed */}
224
  {!isStandalone && !installPrompt && (
225
  <div className="lg:hidden px-4 py-2 bg-slate-800 rounded-lg text-[10px] text-slate-400 border border-slate-700 flex gap-2 items-start">
226
  <Smartphone size={14} className="mt-0.5 shrink-0"/>
@@ -231,7 +216,7 @@ export const Sidebar: React.FC<SidebarProps> = ({ currentView, onChangeView, use
231
  </div>
232
  </div>
233
  )}
234
-
235
  <div className="px-4 py-2 text-xs text-slate-500 flex justify-between">
236
  <span>当前角色:</span>
237
  <span className="text-slate-300 font-bold">
 
19
  roles: UserRole[];
20
  }
21
 
22
+ export const Sidebar: React.FC<SidebarProps> = ({ currentView, onChangeView, userRole, onLogout, isOpen, onClose }) => {
23
+ const currentUser = api.auth.getCurrentUser();
24
+ const canSeeAI = userRole === UserRole.ADMIN || (userRole === UserRole.TEACHER && currentUser?.aiAccess);
25
+ const isHomeroom = userRole === UserRole.TEACHER && !!currentUser?.homeroomClass;
26
+
27
+ // Default Items
28
+ const defaultItems: MenuItem[] = [
29
  { id: 'dashboard', label: '工作台', icon: LayoutDashboard, roles: [UserRole.ADMIN, UserRole.PRINCIPAL, UserRole.TEACHER, UserRole.STUDENT] },
30
+ { id: 'my-class', label: '我的班级', icon: UserCheck, roles: isHomeroom ? [UserRole.TEACHER] : [] },
31
+ { id: 'ai-assistant', label: 'AI 智能助教', icon: Bot, roles: canSeeAI ? [UserRole.ADMIN, UserRole.TEACHER] : [] },
32
  { id: 'attendance', label: '考勤管理', icon: CalendarCheck, roles: [UserRole.TEACHER, UserRole.PRINCIPAL] },
33
  { id: 'games', label: '互动教学', icon: Gamepad2, roles: [UserRole.TEACHER, UserRole.STUDENT] },
34
  { id: 'wishes', label: '心愿与反馈', icon: MessageSquare, roles: [UserRole.ADMIN, UserRole.PRINCIPAL, UserRole.TEACHER, UserRole.STUDENT] },
 
42
  { id: 'users', label: '用户管理', icon: UserCog, roles: [UserRole.ADMIN, UserRole.PRINCIPAL, UserRole.TEACHER] },
43
  { id: 'profile', label: '个人中心', icon: UserCircle, roles: [UserRole.ADMIN, UserRole.PRINCIPAL, UserRole.TEACHER, UserRole.STUDENT] },
44
  { id: 'settings', label: '系统设置', icon: Settings, roles: [UserRole.ADMIN, UserRole.PRINCIPAL] },
45
+ ];
 
 
 
 
 
46
 
47
+ const [menuItems, setMenuItems] = useState<MenuItem[]>(defaultItems);
48
  const [isEditing, setIsEditing] = useState(false);
49
  const [installPrompt, setInstallPrompt] = useState<any>(null);
50
  const [isStandalone, setIsStandalone] = useState(false);
51
 
52
+ // Capture PWA Prompt
53
  useEffect(() => {
54
+ // 1. Check Standalone
55
+ const checkStandalone = () => {
56
+ const isStandaloneMode = window.matchMedia('(display-mode: standalone)').matches ||
57
+ (window.navigator as any).standalone === true;
58
+ setIsStandalone(isStandaloneMode);
59
+ };
60
+ checkStandalone();
61
+ window.matchMedia('(display-mode: standalone)').addEventListener('change', checkStandalone);
62
 
 
63
  if ((window as any).deferredPrompt) {
 
64
  setInstallPrompt((window as any).deferredPrompt);
65
  }
66
 
 
67
  const handler = (e: any) => {
68
  e.preventDefault();
 
69
  (window as any).deferredPrompt = e;
70
  setInstallPrompt(e);
 
71
  };
72
 
73
  window.addEventListener('beforeinstallprompt', handler);
74
+ return () => {
75
+ window.removeEventListener('beforeinstallprompt', handler);
76
+ window.matchMedia('(display-mode: standalone)').removeEventListener('change', checkStandalone);
77
+ };
78
  }, []);
79
 
80
  const handleInstallClick = async () => {
 
81
  const promptEvent = installPrompt || (window as any).deferredPrompt;
 
82
  if (!promptEvent) {
83
  alert('安装功能当前不可用。请尝试点击浏览器菜单中的“添加到主屏幕”或“安装应用”。');
84
  return;
85
  }
 
 
86
  promptEvent.prompt();
 
 
87
  const { outcome } = await promptEvent.userChoice;
 
88
  if (outcome === 'accepted') {
 
89
  setInstallPrompt(null);
90
  (window as any).deferredPrompt = null;
 
 
91
  }
92
  };
93
 
 
94
  useEffect(() => {
95
+ // Load saved order
96
+ if (currentUser?.menuOrder && currentUser.menuOrder.length > 0) {
97
+ const ordered: MenuItem[] = [];
98
+ const map = new Map(defaultItems.map(i => [i.id, i]));
 
99
  // Add saved items in order
100
+ currentUser.menuOrder.forEach(id => {
101
  if (map.has(id)) {
102
  ordered.push(map.get(id)!);
103
  map.delete(id);
104
  }
105
  });
106
+ // Append any new/remaining items
107
  map.forEach(item => ordered.push(item));
108
+ setMenuItems(ordered);
 
109
  }
110
+ }, []);
 
111
 
112
  const handleMove = (index: number, direction: -1 | 1) => {
113
  const newItems = [...menuItems];
 
123
  if (currentUser && currentUser._id) {
124
  try {
125
  await api.users.saveMenuOrder(currentUser._id, orderIds);
126
+ // Update local user object
127
  const updatedUser = { ...currentUser, menuOrder: orderIds };
128
  localStorage.setItem('user', JSON.stringify(updatedUser));
129
  } catch(e) { console.error("Failed to save menu order"); }
130
  }
131
  };
132
 
 
133
  const sidebarClasses = `
134
  fixed inset-y-0 left-0 z-50 w-64 bg-slate-900 text-white transition-transform duration-300 ease-in-out transform
135
  flex flex-col h-full
 
166
  {menuItems.map((item, idx) => {
167
  if (item.roles.length > 0 && !item.roles.includes(userRole)) return null;
168
  if (item.id === 'ai-assistant' && !canSeeAI) return null;
169
+ // Special check for 'my-class': only homeroom teachers
170
  if (item.id === 'my-class' && !isHomeroom) return null;
171
 
172
  const Icon = item.icon;
 
198
  </nav>
199
  </div>
200
 
201
+ <div className="p-4 border-t border-slate-700 shrink-0 space-y-2">
202
  {!isStandalone && installPrompt && (
203
  <button onClick={handleInstallClick} className="w-full flex items-center space-x-3 px-4 py-3 rounded-lg bg-gradient-to-r from-blue-600 to-indigo-600 text-white hover:shadow-lg transition-all duration-200 animate-in fade-in slide-in-from-bottom-2">
204
  <Download size={20} />
 
206
  </button>
207
  )}
208
 
 
209
  {!isStandalone && !installPrompt && (
210
  <div className="lg:hidden px-4 py-2 bg-slate-800 rounded-lg text-[10px] text-slate-400 border border-slate-700 flex gap-2 items-start">
211
  <Smartphone size={14} className="mt-0.5 shrink-0"/>
 
216
  </div>
217
  </div>
218
  )}
219
+
220
  <div className="px-4 py-2 text-xs text-slate-500 flex justify-between">
221
  <span>当前角色:</span>
222
  <span className="text-slate-300 font-bold">
utils/documentParser.ts CHANGED
@@ -4,16 +4,6 @@ import mammoth from 'mammoth';
4
  // @ts-ignore
5
  import * as pdfjsProxy from 'pdfjs-dist';
6
 
7
- // Handle potential default export (ESM vs CommonJS interop issues)
8
- const pdfjsLib = (pdfjsProxy as any).default || pdfjsProxy;
9
-
10
- // Set worker for PDF.js safely. Using the same version from ESM CDN.
11
- if (pdfjsLib && pdfjsLib.GlobalWorkerOptions) {
12
- pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://esm.sh/pdfjs-dist@3.11.174/build/pdf.worker.min.js';
13
- } else {
14
- console.warn("PDF.js GlobalWorkerOptions not found. PDF parsing might fail.");
15
- }
16
-
17
  export interface ParsedDocument {
18
  fileName: string;
19
  content: string;
@@ -58,30 +48,40 @@ const parseDocx = (file: File): Promise<string> => {
58
  };
59
 
60
  const parsePdf = async (file: File): Promise<string> => {
61
- return new Promise((resolve, reject) => {
62
- const reader = new FileReader();
63
- reader.onload = async (e) => {
64
- try {
65
- const typedarray = new Uint8Array(e.target?.result as ArrayBuffer);
66
- // Ensure we call getDocument on the resolved library object
67
- if (!pdfjsLib || !pdfjsLib.getDocument) {
68
- throw new Error('PDF.js library not loaded correctly');
69
- }
70
- const pdf = await pdfjsLib.getDocument(typedarray).promise;
71
- let fullText = '';
72
-
73
- for (let i = 1; i <= pdf.numPages; i++) {
74
- const page = await pdf.getPage(i);
75
- const textContent = await page.getTextContent();
76
- const pageText = textContent.items.map((item: any) => item.str).join(' ');
77
- fullText += pageText + '\n';
 
 
 
 
 
 
 
 
 
 
78
  }
79
- resolve(fullText);
80
- } catch (err) {
81
- reject(err);
82
- }
83
- };
84
- reader.onerror = (e) => reject(e);
85
- reader.readAsArrayBuffer(file);
86
- });
87
  };
 
4
  // @ts-ignore
5
  import * as pdfjsProxy from 'pdfjs-dist';
6
 
 
 
 
 
 
 
 
 
 
 
7
  export interface ParsedDocument {
8
  fileName: string;
9
  content: string;
 
48
  };
49
 
50
  const parsePdf = async (file: File): Promise<string> => {
51
+ // Lazy initialize worker to prevent top-level await/import crashes
52
+ try {
53
+ const pdfjsLib = (pdfjsProxy as any).default || pdfjsProxy;
54
+ if (pdfjsLib && !pdfjsLib.GlobalWorkerOptions.workerSrc) {
55
+ pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://esm.sh/pdfjs-dist@3.11.174/build/pdf.worker.min.js';
56
+ }
57
+
58
+ return new Promise((resolve, reject) => {
59
+ const reader = new FileReader();
60
+ reader.onload = async (e) => {
61
+ try {
62
+ const typedarray = new Uint8Array(e.target?.result as ArrayBuffer);
63
+ if (!pdfjsLib || !pdfjsLib.getDocument) {
64
+ throw new Error('PDF.js library not loaded correctly');
65
+ }
66
+ const pdf = await pdfjsLib.getDocument(typedarray).promise;
67
+ let fullText = '';
68
+
69
+ for (let i = 1; i <= pdf.numPages; i++) {
70
+ const page = await pdf.getPage(i);
71
+ const textContent = await page.getTextContent();
72
+ const pageText = textContent.items.map((item: any) => item.str).join(' ');
73
+ fullText += pageText + '\n';
74
+ }
75
+ resolve(fullText);
76
+ } catch (err) {
77
+ reject(err);
78
  }
79
+ };
80
+ reader.onerror = (e) => reject(e);
81
+ reader.readAsArrayBuffer(file);
82
+ });
83
+ } catch (e) {
84
+ console.error("PDF Init Error", e);
85
+ throw new Error("PDF 解析库初始化失败,请检查网络或使用纯文本/Word。");
86
+ }
87
  };