Spaces:
Sleeping
Sleeping
Upload 65 files
Browse files- App.tsx +3 -9
- components/Sidebar.tsx +22 -26
- index.html +6 -2
- metadata.json +1 -1
- package.json +3 -3
- pages/MyClass.tsx +7 -10
- 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(`
|
| 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('
|
| 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 |
-
|
| 24 |
-
|
| 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:
|
| 33 |
-
{ id: 'ai-assistant', label: 'AI 智能助教', icon: Bot, roles:
|
| 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[]>(
|
| 50 |
const [isEditing, setIsEditing] = useState(false);
|
| 51 |
|
| 52 |
-
//
|
| 53 |
useEffect(() => {
|
| 54 |
let ordered: MenuItem[] = [];
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
|
|
|
| 58 |
// Add saved items in order
|
| 59 |
-
|
| 60 |
if (map.has(id)) {
|
| 61 |
ordered.push(map.get(id)!);
|
| 62 |
map.delete(id);
|
| 63 |
}
|
| 64 |
});
|
| 65 |
-
// Append
|
| 66 |
map.forEach(item => ordered.push(item));
|
| 67 |
} else {
|
| 68 |
-
ordered =
|
| 69 |
}
|
| 70 |
setMenuItems(ordered);
|
| 71 |
-
}, [currentUser?.menuOrder
|
| 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 |
-
//
|
| 75 |
-
|
| 76 |
-
setDeskCols(Math.ceil(savedTotalCols /
|
| 77 |
-
setGroupSize(
|
| 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 |
-
{/*
|
| 565 |
-
<div className="w-[60%] h-
|
| 566 |
-
|
| 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: [
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
build: {
|
| 8 |
-
outDir: '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: {
|