Spaces:
Running
Running
Upload 67 files
Browse files- App.tsx +22 -6
- components/Sidebar.tsx +36 -51
- 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 |
-
//
|
| 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 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 23 |
-
const
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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[]>(
|
| 48 |
const [isEditing, setIsEditing] = useState(false);
|
| 49 |
const [installPrompt, setInstallPrompt] = useState<any>(null);
|
| 50 |
const [isStandalone, setIsStandalone] = useState(false);
|
| 51 |
|
| 52 |
-
// Capture
|
| 53 |
useEffect(() => {
|
| 54 |
-
// 1. Check
|
| 55 |
-
const
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
|
|
|
|
|
|
| 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 () =>
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
const map = new Map(STATIC_MENU_ITEMS.map(i => [i.id, i]));
|
| 111 |
// Add saved items in order
|
| 112 |
-
|
| 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 |
-
|
| 121 |
-
ordered = [...STATIC_MENU_ITEMS];
|
| 122 |
}
|
| 123 |
-
|
| 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 |
-
//
|
| 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
|
| 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 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
const
|
| 77 |
-
fullText
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
}
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 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 |
};
|