// ==================== 全局配置 ==================== var SUPABASE_URL = 'https://otivsahvviqrblciikfj.supabase.co '; var SUPABASE_ANON_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im90aXZzYWh2dmlxcmJsY2lpa2ZqIiwicm9sZSI6ImFub24iLCJpYXQiOjE3ODY4NTEyMDUsImV4cCI6MjEwMjQyNzIwNX0.0awAjSQyWBErSecD_t-fkcsdIwoJcHBx-wIhFCTfXgQ'; var AI_WORKER_URL = '/api/ai'; var supabase = window.supabase.createClient(SUPABASE_URL, SUPABASE_ANON_KEY); var AppState = { currentUser: null, currentProfile: null, authMode: 'login', allStudents: [], attendanceStudents: [], homeworkList: [], selectedStudents: [], selectedExamId: null, commentList: [], scheduleView: 'class', navCollapsed: {}, selectedStudentId: null, schoolStartDate: '2026-09-01', selectedSemesterId: null, periodTimes: null, aiConfig: { model: '', apiKey: '' } }; // ==================== 工具函数 ==================== function showToast(message, type = 'success') { const container = document.querySelector('.toast-container') || (() => { const div = document.createElement('div'); div.className = 'toast-container'; document.body.appendChild(div); return div; })(); const toast = document.createElement('div'); toast.className = `toast ${type}`; toast.textContent = message; container.appendChild(toast); setTimeout(() => toast.remove(), 3000); } function escapeHtml(text) { if (!text) return ''; const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } function parseCSV(text) { const lines = text.split(/\r?\n/).filter(l => l.trim()); return lines.map(line => { const result = []; let current = ''; let inQuotes = false; for (let i = 0; i < line.length; i++) { const c = line[i]; if (c === '"') inQuotes = !inQuotes; else if (c === ',' && !inQuotes) { result.push(current.trim()); current = ''; } else current += c; } result.push(current.trim()); return result; }); } function compressImage(file, maxWidth, quality) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = (e) => { const img = new Image(); img.onload = () => { const canvas = document.createElement('canvas'); let width = img.width, height = img.height; if (width > maxWidth) { height = (height * maxWidth) / width; width = maxWidth; } canvas.width = width; canvas.height = height; const ctx = canvas.getContext('2d'); ctx.drawImage(img, 0, 0, width, height); canvas.toBlob((blob) => resolve(new File([blob], 'compressed.jpg', { type: 'image/jpeg' })), 'image/jpeg', quality); }; img.onerror = reject; img.src = e.target.result; }; reader.onerror = reject; reader.readAsDataURL(file); }); } async function logActivity(action, details) { if (!AppState.currentProfile) return; try { await supabase.from('activity_logs').insert({ class_id: AppState.currentProfile.class_id, user_id: AppState.currentUser.id, action, details }); } catch (err) { console.error('记录活动失败:', err); } } function getRoleLabel(role) { const map = { 'admin': '系统管理员', 'class_teacher': '班主任', 'subject_teacher': '科任教师' }; return map[role] || role; } function exportStudents(data) { if (!data || data.length === 0) { showToast('没有学生数据可导出', 'warning'); return; } const rows = data.map(s => ({ '学号': s.student_no || '', '姓名': s.name, '性别': s.gender || '', '族别': s.ethnicity || '', '座位': s.seat || '', '职务': s.role || '', '家长姓名': s.parent_name || '', '家长电话': s.parent_phone || '', '住址': s.address || '' })); const ws = XLSX.utils.json_to_sheet(rows); const wb = XLSX.utils.book_new(); XLSX.utils.book_append_sheet(wb, ws, '学生名单'); XLSX.writeFile(wb, `学生名单_${new Date().toISOString().split('T')[0]}.xlsx`); showToast('导出成功', 'success'); } async function callAI(type, payload) { try { const response = await fetch(AI_WORKER_URL, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ type, payload }) }); if (!response.ok) { const err = await response.json().catch(() => ({ error: '请求失败' })); throw new Error(err.error || 'AI 调用失败'); } const result = await response.json(); return result.content; } catch (error) { console.error('AI 调用错误:', error); showToast('AI 服务不可用:' + error.message, 'error'); return null; } } // 统一字段映射(用于所有导入) function autoDetectFieldMap(headerRow) { const fieldMap = []; const keywords = [ { field: 'student_no', keys: ['学号', '编号', '序号'] }, { field: 'name', keys: ['姓名', '名字', '学生'] }, { field: 'gender', keys: ['性别', '男女'] }, { field: 'ethnicity', keys: ['族别', '民族'] }, { field: 'seat', keys: ['座位', '座号'] }, { field: 'role', keys: ['职务', '班干部', '职位'] }, { field: 'parent_name', keys: ['家长姓名', '父亲', '母亲', '监护人'] }, { field: 'parent_phone', keys: ['家长电话', '联系电话', '手机', '电话'] }, { field: 'address', keys: ['住址', '地址', '家庭住址'] }, { field: 'birth_date', keys: ['出生', '生日', '出生日期'] }, { field: 'hobbies', keys: ['兴趣', '爱好'] }, { field: 'specialty', keys: ['特长', '擅长'] }, { field: 'health_status', keys: ['健康', '体质', '病史'] } ]; headerRow.forEach((header, index) => { const text = String(header).toLowerCase(); let mapped = null; for (let kw of keywords) { if (kw.keys.some(key => text.includes(key))) { mapped = kw.field; break; } } fieldMap[index] = mapped; }); return fieldMap; } function createModal(title, bodyHtml, actionsHtml, maxWidth = '600px') { const modal = document.createElement('div'); modal.className = 'modal-overlay'; modal.innerHTML = ` `; document.body.appendChild(modal); return modal; } function refreshModule(moduleName) { const refreshMap = { 'dashboard': loadDashboard, 'students': loadStudents, 'attendance': () => { loadAttendanceData(); loadLeaves(); loadAttendanceStats(); }, 'homework': loadHomeworkData, 'grades': loadExamList, 'schedule': loadScheduleData, 'comments': renderAvatarWall, 'analysis': () => { if (AppState.selectedStudentId) loadAnalysisData(AppState.selectedStudentId); }, 'evaluation': loadEvalList, 'communication': () => { loadCommStats(); loadCommPreviews(); }, 'notices': loadNoticesList, 'moments': loadMomentsList, 'insights': loadInsightsData, 'notifications': loadNotificationsList, 'tasks': loadTasksList, 'users': loadUsers }; if (refreshMap[moduleName]) refreshMap[moduleName](); } // ==================== 认证 ==================== function setAuthMode(mode) { AppState.authMode = mode; const loginTab = document.getElementById('login-tab'); const signupTab = document.getElementById('signup-tab'); const realnameGroup = document.getElementById('realname-group'); const realnameInput = document.getElementById('realname'); if (mode === 'login') { loginTab.classList.add('active'); signupTab.classList.remove('active'); realnameGroup.style.display = 'none'; realnameInput.removeAttribute('required'); document.getElementById('auth-submit-btn').textContent = '登 录'; } else { loginTab.classList.remove('active'); signupTab.classList.add('active'); realnameGroup.style.display = 'block'; realnameInput.setAttribute('required', ''); document.getElementById('auth-submit-btn').textContent = '注 册'; } document.getElementById('auth-message').textContent = ''; } async function handleAuth(e) { e.preventDefault(); const username = document.getElementById('username').value.trim(); const realname = document.getElementById('realname').value.trim(); const password = document.getElementById('password').value; const email = `${username}@workbuddy.local`; try { if (AppState.authMode === 'signup') { const { data, error } = await supabase.auth.signUp({ email, password, options: { data: { username, realname } } }); if (error) throw error; if (data.user) { AppState.currentUser = data.user; const profile = await fetchProfile(); if (profile) { AppState.currentProfile = profile; showMain(); switchPage('dashboard'); if (profile.role === 'admin') loadUsers(); applyBannerBackground(); } } else { showToast('注册成功,但需要邮箱确认', 'warning'); } } else { const { data, error } = await supabase.auth.signInWithPassword({ email, password }); if (error) throw error; AppState.currentUser = data.user; const profile = await fetchProfile(); if (!profile) { showToast('用户资料不存在', 'error'); await supabase.auth.signOut(); return; } if (!profile.is_active) { showToast('账户已被禁用', 'error'); await supabase.auth.signOut(); return; } AppState.currentProfile = profile; showMain(); switchPage('dashboard'); if (profile.role === 'admin') loadUsers(); applyBannerBackground(); } } catch (error) { showToast(error.message || '操作失败', 'error'); } } async function fetchProfile() { if (!AppState.currentUser) return null; const { data, error } = await supabase .from('profiles') .select('*') .eq('id', AppState.currentUser.id) .single(); if (error) return null; return data; } function showAuth() { document.getElementById('auth-container').style.display = 'flex'; document.getElementById('main-container').style.display = 'none'; } function showMain() { document.getElementById('auth-container').style.display = 'none'; document.getElementById('main-container').style.display = 'flex'; const avatarLetter = (AppState.currentProfile.realname || AppState.currentProfile.username).charAt(0).toUpperCase(); document.getElementById('user-avatar-letter').textContent = avatarLetter; document.getElementById('sidebar-username').textContent = AppState.currentProfile.realname || AppState.currentProfile.username; document.getElementById('sidebar-role').textContent = getRoleLabel(AppState.currentProfile.role); } function applyBannerBackground() { const banner = document.getElementById('top-banner'); if (AppState.currentProfile && AppState.currentProfile.header_bg_url) { banner.style.backgroundImage = `url('${AppState.currentProfile.header_bg_url}')`; banner.style.backgroundSize = 'cover'; banner.style.backgroundPosition = 'center'; } else { banner.style.backgroundImage = 'linear-gradient(135deg, #1e1b4b 0%, #4f3df0 50%, #00d4ff 100%)'; } } async function handleBannerUpload(event) { const file = event.target.files[0]; if (!file) return; if (!file.type.startsWith('image/')) { showToast('请选择图片文件', 'error'); return; } try { const compressedFile = await compressImage(file, 1600, 0.8); const fileName = `${AppState.currentUser.id}_${Date.now()}.jpg`; await supabase.storage.from('header-images').upload(fileName, compressedFile, { upsert: true }); const { data: urlData } = supabase.storage.from('header-images').getPublicUrl(fileName); await supabase.from('profiles').update({ header_bg_url: urlData.publicUrl }).eq('id', AppState.currentUser.id); AppState.currentProfile.header_bg_url = urlData.publicUrl; applyBannerBackground(); showToast('背景图上传成功!', 'success'); } catch (error) { showToast('上传失败:' + error.message, 'error'); } finally { event.target.value = ''; } } async function logout() { await supabase.auth.signOut(); AppState.currentUser = null; AppState.currentProfile = null; showAuth(); } // ==================== 导航 ==================== const NAV_GROUPS = [ { title: '总览', items: [{ page: 'dashboard', icon: 'fa-gauge-high', label: '工作台' }] }, { title: '班级管理', items: [ { page: 'students', icon: 'fa-users', label: '学生管理' }, { page: 'seating', icon: 'fa-chair', label: '座次表' }, { page: 'rollcall', icon: 'fa-shuffle', label: '随机点名' } ]}, { title: '教学管理', items: [ { page: 'homework', icon: 'fa-book', label: '作业管理' }, { page: 'grades', icon: 'fa-chart-bar', label: '成绩管理' }, { page: 'schedule', icon: 'fa-calendar-days', label: '课程表' } ]}, { title: '学生发展', items: [ { page: 'attendance', icon: 'fa-calendar-check', label: '考勤管理' }, { page: 'comments', icon: 'fa-comment-dots', label: '期末评语' }, { page: 'analysis', icon: 'fa-user-graduate', label: '综合分析' }, { page: 'evaluation', icon: 'fa-star', label: '班级评价' } ]}, { title: '家校沟通', items: [ { page: 'communication', icon: 'fa-phone', label: '家校沟通' }, { page: 'notices', icon: 'fa-bullhorn', label: '通知公告' } ]}, { title: '协同管理', items: [ { page: 'moments', icon: 'fa-images', label: '班级朋友圈' }, { page: 'insights', icon: 'fa-chart-pie', label: '数据洞察' }, { page: 'notifications', icon: 'fa-bell', label: '通知中心' }, { page: 'tasks', icon: 'fa-list-check', label: '任务协作' }, { page: 'users', icon: 'fa-user-shield', label: '用户管理', adminOnly: true } ]} ]; function renderNavigation() { const nav = document.getElementById('nav-links'); nav.innerHTML = ''; NAV_GROUPS.forEach((group, index) => { if (group.items.every(item => item.adminOnly && (!AppState.currentProfile || AppState.currentProfile.role !== 'admin'))) return; const isCollapsed = AppState.navCollapsed[index] !== undefined ? AppState.navCollapsed[index] : false; const groupDiv = document.createElement('div'); groupDiv.className = `nav-group ${isCollapsed ? 'collapsed' : ''}`; groupDiv.innerHTML = ``; const body = groupDiv.querySelector('.nav-group-body'); group.items.forEach(item => { if (item.adminOnly && (!AppState.currentProfile || AppState.currentProfile.role !== 'admin')) return; const div = document.createElement('div'); div.className = 'nav-item'; div.dataset.page = item.page; div.innerHTML = `${item.label}`; div.addEventListener('click', () => { switchPage(item.page); }); body.appendChild(div); }); groupDiv.querySelector('.nav-group-title').addEventListener('click', (e) => { e.stopPropagation(); const idx = parseInt(e.currentTarget.dataset.groupIndex); AppState.navCollapsed[idx] = !AppState.navCollapsed[idx]; groupDiv.classList.toggle('collapsed'); }); nav.appendChild(groupDiv); }); } function renderPageContainers() { const container = document.getElementById('page-container'); container.innerHTML = ` `; } function switchPage(pageId, studentId = null) { if (studentId) AppState.selectedStudentId = studentId; document.querySelectorAll('.page').forEach(p => p.style.display = 'none'); const target = document.getElementById(`page-${pageId}`); if (target) target.style.display = 'block'; document.querySelectorAll('.nav-item').forEach(item => { if (item.dataset.page === pageId) item.classList.add('active'); else item.classList.remove('active'); }); const loaders = { dashboard: loadDashboard, students: loadStudents, seating: loadSeating, rollcall: loadRollcall, homework: loadHomework, grades: loadGrades, schedule: loadSchedule, attendance: loadAttendance, comments: loadComments, analysis: loadAnalysis, evaluation: loadEvaluation, communication: loadCommunication, notices: loadNotices, moments: loadMoments, insights: loadInsights, notifications: loadNotifications, tasks: loadTasks, users: loadUsers }; if (loaders[pageId]) loaders[pageId](); } window.switchPage = switchPage; window.switchPageWithStudent = function(pageId, studentId) { switchPage(pageId, studentId); }; // ==================== 工作台 ==================== let activitySubscription = null; function loadDashboard() { const container = document.getElementById('page-dashboard'); container.innerHTML = `

0

班级学生数

-

今日出勤率

-

待批作业

-

最近考试均分

快捷操作

考勤打卡
布置作业
随机点名
成绩录入
发动态
数据洞察

班级管理团队

未设置班干部/科代表

最新动态

实时

暂无动态

`; if (!AppState.currentProfile) return; // 加载基础统计 supabase.from('classes').select('name').eq('id', AppState.currentProfile.class_id).single().then(({ data }) => { document.getElementById('banner-class-info').textContent = data ? `班级:${data.name}` : '班级:未设置'; }); document.getElementById('stat-students').textContent = AppState.allStudents.length; const today = new Date().toISOString().split('T')[0]; supabase.from('attendance').select('status').eq('class_id', AppState.currentProfile.class_id).eq('date', today).then(({ data: attData }) => { if (attData && attData.length > 0 && AppState.allStudents.length > 0) { const present = attData.filter(a => a.status === 'present' || a.status === 'late').length; document.getElementById('stat-attendance').textContent = Math.round((present / AppState.allStudents.length) * 100) + '%'; } else { document.getElementById('stat-attendance').textContent = '-'; } }); supabase.from('homework').select('*', { count: 'exact', head: true }).eq('class_id', AppState.currentProfile.class_id).then(({ count }) => { document.getElementById('stat-homework').textContent = count || 0; }); const hour = new Date().getHours(); let greeting = '你好'; if (hour < 6) greeting = '凌晨好'; else if (hour < 12) greeting = '早上好'; else if (hour < 18) greeting = '下午好'; else greeting = '晚上好'; document.getElementById('banner-greeting').textContent = `${greeting},${AppState.currentProfile.realname || AppState.currentProfile.username}老师!`; loadTeam(); loadActivityList(); setupActivitySubscription(); loadBannerInfo(); container.querySelectorAll('.quick-action[data-page]').forEach(el => el.addEventListener('click', () => switchPage(el.dataset.page))); document.getElementById('manage-cadres-btn').addEventListener('click', openCadresModal); document.getElementById('manage-reps-btn').addEventListener('click', openRepsModal); document.getElementById('refresh-activity-btn').addEventListener('click', loadActivityList); } async function loadTeam() { const container = document.getElementById('team-list'); const { data: cadres } = await supabase.from('students').select('*').eq('class_id', AppState.currentProfile.class_id).not('role', 'is', null).not('role', 'eq', ''); const { data: reps } = await supabase.from('subject_reps').select('*, students(name, avatar_color)').eq('class_id', AppState.currentProfile.class_id); let html = ''; if (cadres && cadres.length > 0) { cadres.forEach(s => { const color = s.avatar_color || '#6d5dfc'; html += `
${escapeHtml(s.name.charAt(0))}
${escapeHtml(s.name)} ${escapeHtml(s.role)}
`; }); } if (reps && reps.length > 0) { reps.forEach(r => { const color = r.students?.avatar_color || '#10b981'; html += `
${escapeHtml(r.students?.name?.charAt(0) || '')}
${escapeHtml(r.students?.name || '')} ${escapeHtml(r.subject)}科代表
`; }); } container.innerHTML = html || '

未设置班干部/科代表

'; } async function loadActivityList() { const { data: logs, error } = await supabase.from('activity_logs').select('*').eq('class_id', AppState.currentProfile.class_id).order('created_at', { ascending: false }).limit(20); if (error) return; const container = document.getElementById('activity-list'); if (!logs || logs.length === 0) { container.innerHTML = '

暂无动态

'; return; } container.innerHTML = logs.map(log => `
${escapeHtml(log.action)} ${escapeHtml(log.details || '')} ${new Date(log.created_at).toLocaleString('zh-CN')}
`).join(''); container.querySelectorAll('.delete-activity-btn').forEach(btn => btn.addEventListener('click', async (e) => { e.stopPropagation(); const id = btn.dataset.id; if (confirm('确定删除该动态?')) { await supabase.from('activity_logs').delete().eq('id', id); showToast('动态已删除', 'success'); loadActivityList(); } })); } function setupActivitySubscription() { if (activitySubscription) supabase.removeChannel(activitySubscription); activitySubscription = supabase .channel('activity-changes') .on('postgres_changes', { event: 'INSERT', schema: 'public', table: 'activity_logs', filter: `class_id=eq.${AppState.currentProfile.class_id}` }, () => { loadActivityList(); }) .subscribe(); } function openCadresModal() { const modal = createModal( '班干部管理', `
加载中...
`, ``, '500px' ); document.getElementById('cadre-close').addEventListener('click', () => modal.remove()); loadCadresList(modal.querySelector('#cadres-list')); document.getElementById('add-cadre-btn').addEventListener('click', async () => { const studentId = modal.querySelector('#cadre-student').value; const role = modal.querySelector('#cadre-role').value; if (!studentId || !role) { showToast('请选择学生和职务', 'error'); return; } await supabase.from('students').update({ role }).eq('id', studentId); showToast('班干部已设置', 'success'); loadCadresList(modal.querySelector('#cadres-list')); loadTeam(); }); } async function loadCadresList(container) { const { data: cadres } = await supabase.from('students').select('*').eq('class_id', AppState.currentProfile.class_id).not('role', 'is', null).not('role', 'eq', ''); if (cadres.length === 0) { container.innerHTML = '暂无班干部'; return; } container.innerHTML = cadres.map(s => `
${escapeHtml(s.name)} - ${escapeHtml(s.role)}
`).join(''); container.querySelectorAll('.remove-cadre-btn').forEach(btn => btn.addEventListener('click', async () => { await supabase.from('students').update({ role: null }).eq('id', btn.dataset.id); loadCadresList(container); loadTeam(); })); } function openRepsModal() { const modal = createModal( '科代表管理', `
加载中...
`, ``, '500px' ); document.getElementById('rep-close').addEventListener('click', () => modal.remove()); loadRepsList(modal.querySelector('#reps-list')); document.getElementById('add-rep-btn').addEventListener('click', async () => { const studentId = modal.querySelector('#rep-student').value; const subject = modal.querySelector('#rep-subject').value; if (!studentId || !subject) { showToast('请选择学生和科目', 'error'); return; } await supabase.from('subject_reps').insert({ class_id: AppState.currentProfile.class_id, student_id: studentId, subject }); showToast('科代表已设置', 'success'); loadRepsList(modal.querySelector('#reps-list')); loadTeam(); }); } async function loadRepsList(container) { const { data: reps } = await supabase.from('subject_reps').select('*, students(name)').eq('class_id', AppState.currentProfile.class_id); if (reps.length === 0) { container.innerHTML = '暂无科代表'; return; } container.innerHTML = reps.map(r => `
${escapeHtml(r.students?.name || '')} - ${escapeHtml(r.subject)}科代表
`).join(''); container.querySelectorAll('.remove-rep-btn').forEach(btn => btn.addEventListener('click', async () => { await supabase.from('subject_reps').delete().eq('id', btn.dataset.id); loadRepsList(container); loadTeam(); })); } function loadBannerInfo() { const infoDiv = document.getElementById('banner-date-info'); const today = new Date(); const dateStr = `${today.getFullYear()}年${today.getMonth()+1}月${today.getDate()}日 星期${'日一二三四五六'[today.getDay()]}`; let schoolStart = AppState.schoolStartDate || '2026-09-01'; const startDate = new Date(schoolStart); const diffDays = Math.ceil((startDate - today) / (24*60*60*1000)); let daysText = ''; if (diffDays > 0) daysText = `距开学还有 ${diffDays} 天`; else if (diffDays === 0) daysText = '今天开学!'; else daysText = `已开学 ${Math.abs(diffDays)} 天`; const quotes = [ '教育不是灌满一桶水,而是点燃一把火。 —— 叶芝', '学而不思则罔,思而不学则殆。 —— 孔子', '教育是改变世界最强大的武器。 —— 曼德拉', 'The best way to predict the future is to create it. —— Peter Drucker', '千里之行,始于足下。 —— 老子', '兴趣是最好的老师。 —— 爱因斯坦', '教育的目的在于让人能够继续教育自己。 —— 杜威', '授人以鱼不如授人以渔。 —— 谚语' ]; const quote = quotes[Math.floor(Math.random() * quotes.length)]; infoDiv.innerHTML = `${dateStr}
${daysText}
${quote}`; } // ==================== 学生管理 ==================== function loadStudents() { const container = document.getElementById('page-students'); container.innerHTML = `

0

学生总数

0

男生

0

女生

0

班干部

学生列表

头像学号姓名性别族别座位职务家长姓名家长电话住址操作
`; if (!AppState.currentProfile) return; supabase.from('students').select('*').eq('class_id', AppState.currentProfile.class_id).order('seat', { ascending: true }).then(({ data, error }) => { if (error) { showToast('加载学生失败', 'error'); return; } AppState.allStudents = data; document.getElementById('stu-total').textContent = data.length; document.getElementById('stu-male').textContent = data.filter(s => s.gender === '男').length; document.getElementById('stu-female').textContent = data.filter(s => s.gender === '女').length; document.getElementById('stu-cadre').textContent = data.filter(s => s.role && s.role.trim() !== '').length; const ethnicities = [...new Set(data.map(s => s.ethnicity).filter(Boolean))]; const ethSelect = document.getElementById('filter-ethnicity'); ethSelect.innerHTML = '' + ethnicities.map(e => ``).join(''); applyStudentFilters(); }); const table = document.getElementById('students-table'); table.addEventListener('click', (e) => { const target = e.target.closest('.edit-student-btn, .delete-student-btn, .student-detail-link, .student-avatar-link'); if (!target) return; const id = target.dataset.id; if (target.classList.contains('edit-student-btn')) openStudentModal(id); else if (target.classList.contains('delete-student-btn')) deleteStudent(id); else if (target.classList.contains('student-detail-link') || target.classList.contains('student-avatar-link')) { e.preventDefault(); openStudentDetail(id); } }); document.getElementById('add-student-btn').addEventListener('click', () => openStudentModal()); document.getElementById('import-students-btn').addEventListener('click', openImportModal); document.getElementById('export-students-btn').addEventListener('click', () => exportStudents(AppState.allStudents)); document.getElementById('clear-students-btn').addEventListener('click', clearAllStudents); document.getElementById('filter-gender').addEventListener('change', applyStudentFilters); document.getElementById('filter-ethnicity').addEventListener('change', applyStudentFilters); document.getElementById('filter-role').addEventListener('change', applyStudentFilters); document.getElementById('search-student').addEventListener('input', applyStudentFilters); } function applyStudentFilters() { const gender = document.getElementById('filter-gender')?.value || ''; const ethnicity = document.getElementById('filter-ethnicity')?.value || ''; const role = document.getElementById('filter-role')?.value || ''; const search = document.getElementById('search-student')?.value.toLowerCase() || ''; let filtered = AppState.allStudents.filter(s => { if (gender && s.gender !== gender) return false; if (ethnicity && s.ethnicity !== ethnicity) return false; if (role === '班干部' && (!s.role || s.role.trim() === '')) return false; if (search && !(s.name.toLowerCase().includes(search) || (s.student_no && s.student_no.toLowerCase().includes(search)) || (s.ethnicity && s.ethnicity.toLowerCase().includes(search)))) return false; return true; }); const tbody = document.getElementById('students-tbody'); const empty = document.getElementById('students-empty'); tbody.innerHTML = ''; if (filtered.length === 0) { empty.style.display = 'block'; return; } empty.style.display = 'none'; filtered.forEach(s => { const color = s.avatar_color || '#6d5dfc'; const tr = document.createElement('tr'); tr.innerHTML = `
${escapeHtml(s.name.charAt(0))}
${escapeHtml(s.student_no || '-')} ${escapeHtml(s.name)} ${s.gender === '男' ? ' 男' : s.gender === '女' ? ' 女' : '-'} ${escapeHtml(s.ethnicity || '-')} ${s.seat || '-'} ${s.role ? `${escapeHtml(s.role)}` : '-'} ${escapeHtml(s.parent_name || '-')} ${escapeHtml(s.parent_phone || '-')} ${escapeHtml(s.address || '-')} `; tbody.appendChild(tr); }); } function openStudentModal(studentId = null) { const student = studentId ? AppState.allStudents.find(s => s.id == studentId) : null; const modal = createModal( student ? '编辑学生' : '添加学生', `
`, ``, '700px' ); document.getElementById('modal-cancel').addEventListener('click', () => modal.remove()); document.getElementById('modal-save').addEventListener('click', async () => { const data = { student_no: document.getElementById('modal-student-no').value.trim() || null, name: document.getElementById('modal-student-name').value.trim(), gender: document.getElementById('modal-student-gender').value || null, ethnicity: document.getElementById('modal-student-ethnicity').value.trim() || null, seat: document.getElementById('modal-student-seat').value ? parseInt(document.getElementById('modal-student-seat').value) : null, role: document.getElementById('modal-student-role').value || null, parent_name: document.getElementById('modal-parent-name').value.trim() || null, parent_phone: document.getElementById('modal-student-phone').value.trim() || null, address: document.getElementById('modal-student-address').value.trim() || null, birth_date: document.getElementById('modal-student-birth').value || null, hobbies: document.getElementById('modal-student-hobbies').value.trim() || null, specialty: document.getElementById('modal-student-specialty').value.trim() || null, health_status: document.getElementById('modal-student-health').value.trim() || null, avatar_color: student?.avatar_color || '#' + Math.floor(Math.random()*16777215).toString(16).padStart(6,'0') }; if (!data.name) { showToast('姓名不能为空', 'error'); return; } try { if (studentId) { await supabase.from('students').update(data).eq('id', studentId); showToast('学生信息已更新', 'success'); } else { await supabase.from('students').insert({ ...data, class_id: AppState.currentProfile.class_id }); showToast('学生添加成功', 'success'); } modal.remove(); loadStudents(); loadDashboard(); } catch (err) { showToast('保存失败:' + err.message, 'error'); } }); } async function deleteStudent(id) { if (!confirm('确定删除该学生?')) return; await supabase.from('students').delete().eq('id', id); showToast('学生已删除', 'success'); loadStudents(); } async function clearAllStudents() { const modal = createModal( '清空所有学生', `

⚠️ 此操作将删除本班级所有学生及其关联数据(考勤、作业、成绩等),不可恢复!

请输入您的登录密码以确认:

确认倒计时 5

`, ``, '500px' ); let countdown = 5; const interval = setInterval(() => { countdown--; document.getElementById('clear-countdown').textContent = countdown; if (countdown <= 0) { clearInterval(interval); document.getElementById('clear-confirm').disabled = false; } }, 1000); document.getElementById('clear-cancel').addEventListener('click', () => { clearInterval(interval); modal.remove(); }); document.getElementById('clear-confirm').addEventListener('click', async () => { const password = modal.querySelector('#clear-password').value; if (!password) { showToast('请输入密码', 'error'); return; } const email = `${AppState.currentProfile.username}@workbuddy.local`; const { error } = await supabase.auth.signInWithPassword({ email, password }); if (error) { showToast('密码错误,操作取消', 'error'); return; } const { error: delError } = await supabase.from('students').delete().eq('class_id', AppState.currentProfile.class_id); if (delError) { showToast('清空失败:' + delError.message, 'error'); return; } clearInterval(interval); modal.remove(); showToast('已清空所有学生', 'success'); loadStudents(); loadDashboard(); }); } function openStudentDetail(studentId) { const student = AppState.allStudents.find(s => s.id == studentId); if (!student) return; const color = student.avatar_color || '#6d5dfc'; const modal = createModal( `${escapeHtml(student.name)} 的详情`, `
${escapeHtml(student.name.charAt(0))}

${escapeHtml(student.name)}

${student.gender === '男' ? ' 男' : student.gender === '女' ? ' 女' : ''} ${student.role ? `${escapeHtml(student.role)}` : ''}

学号:${escapeHtml(student.student_no || '-')}

族别:${escapeHtml(student.ethnicity || '-')}

座位:${student.seat || '-'}

出生日期:${student.birth_date || '-'}

家长:${escapeHtml(student.parent_name || '-')}

电话:${escapeHtml(student.parent_phone || '-')}

住址:${escapeHtml(student.address || '-')}

兴趣爱好:${escapeHtml(student.hobbies || '-')}

特长:${escapeHtml(student.specialty || '-')}

健康状况:${escapeHtml(student.health_status || '-')}

`, ``, '750px' ); document.getElementById('modal-close').addEventListener('click', () => modal.remove()); document.getElementById('analysis-jump-btn').addEventListener('click', () => { modal.remove(); switchPageWithStudent('analysis', student.id); }); document.getElementById('grades-jump-btn').addEventListener('click', () => { modal.remove(); switchPageWithStudent('grades', student.id); }); document.getElementById('records-toggle-btn').addEventListener('click', () => { const section = document.getElementById('records-section'); section.style.display = section.style.display === 'none' ? 'block' : 'none'; if (section.style.display === 'block') loadProfileRecords(student.id, modal.querySelector('#record-list')); }); modal.querySelector('#add-record-btn').addEventListener('click', async () => { const type = modal.querySelector('#record-type').value; const content = modal.querySelector('#record-content').value.trim(); const date = modal.querySelector('#record-date').value || new Date().toISOString().split('T')[0]; if (!content) { showToast('请输入内容', 'error'); return; } await supabase.from('profile_records').insert({ student_id: student.id, class_id: AppState.currentProfile.class_id, type, content, record_date: date }); modal.querySelector('#record-content').value = ''; loadProfileRecords(student.id, modal.querySelector('#record-list')); showToast('成长记录已添加', 'success'); }); } function loadProfileRecords(studentId, container) { supabase.from('profile_records').select('*').eq('student_id', studentId).order('record_date', { ascending: false }).then(({ data, error }) => { if (error) { container.innerHTML = '加载失败'; return; } if (data.length === 0) { container.innerHTML = '

暂无记录

'; return; } container.innerHTML = data.map(r => `
${escapeHtml(r.content)}${r.record_date} ${r.type}
`).join(''); }); } function openImportModal() { const modal = createModal( '批量导入学生', `

支持 CSV / Excel 文件,表头自动识别,也可粘贴 CSV 数据。

`, ``, '700px' ); document.getElementById('import-cancel').addEventListener('click', () => modal.remove()); document.getElementById('import-submit').addEventListener('click', async () => { const csvText = modal.querySelector('#import-csv').value.trim(); const fileInput = modal.querySelector('#import-file'); let rows = []; if (csvText) { rows = parseCSV(csvText); } else if (fileInput.files.length > 0) { const file = fileInput.files[0]; if (file.name.endsWith('.csv')) { rows = parseCSV(await file.text()); } else { const data = await file.arrayBuffer(); const wb = XLSX.read(data, { type: 'array' }); rows = XLSX.utils.sheet_to_json(wb.Sheets[wb.SheetNames[0]], { header: 1 }); } } else { showToast('请输入数据或选择文件', 'error'); return; } if (rows.length === 0) { showToast('没有数据', 'error'); return; } // 智能表头识别 const firstRow = rows[0]; let fieldMap = []; let hasHeader = false; if (firstRow) { hasHeader = firstRow.some(cell => ['学号','姓名','性别','族别','座位','职务','家长','电话','住址','出生','兴趣','特长','健康'].some(kw => String(cell).includes(kw))); if (hasHeader) { fieldMap = autoDetectFieldMap(firstRow); rows.shift(); } } // 如果没有表头,使用默认顺序 const defaultMap = ['student_no','name','gender','ethnicity','seat','role','parent_name','parent_phone','address','birth_date','hobbies','specialty','health_status']; const studentsToInsert = rows.filter(r => r.length >= 2 && r[1]).map(r => { const obj = {}; if (hasHeader) { fieldMap.forEach((field, index) => { if (field && r[index] !== undefined) { obj[field] = String(r[index]).trim(); } }); } else { defaultMap.forEach((field, index) => { if (r[index] !== undefined) { obj[field] = String(r[index]).trim(); } }); } // 日期校验 if (obj.birth_date && !/^\d{4}[-/]\d{1,2}[-/]\d{1,2}$/.test(obj.birth_date)) { obj.birth_date = null; } // 座位数字校验 obj.seat = obj.seat ? parseInt(obj.seat) : null; if (obj.seat && isNaN(obj.seat)) obj.seat = null; return { student_no: obj.student_no || null, name: obj.name || null, gender: obj.gender || null, ethnicity: obj.ethnicity || null, seat: obj.seat, role: obj.role || null, parent_name: obj.parent_name || null, parent_phone: obj.parent_phone || null, address: obj.address || null, birth_date: obj.birth_date || null, hobbies: obj.hobbies || null, specialty: obj.specialty || null, health_status: obj.health_status || null, class_id: AppState.currentProfile.class_id, avatar_color: '#' + Math.floor(Math.random()*16777215).toString(16).padStart(6,'0') }; }).filter(s => s.name); if (studentsToInsert.length === 0) { showToast('没有有效数据', 'error'); return; } // 导入前预览(简单显示前3条) const preview = modal.querySelector('#import-preview'); preview.innerHTML = `

即将导入 ${studentsToInsert.length} 条数据,前3条预览:

` + studentsToInsert.slice(0,3).map(s => `
${escapeHtml(s.name)} - ${escapeHtml(s.parent_name || '无家长')} - ${escapeHtml(s.address || '无住址')}
`).join(''); // 确认导入 if (!confirm(`确认导入 ${studentsToInsert.length} 条学生数据?`)) return; const { error } = await supabase.from('students').insert(studentsToInsert); if (error) { showToast('导入失败:' + error.message, 'error'); return; } showToast(`成功导入 ${studentsToInsert.length} 名学生`, 'success'); modal.remove(); loadStudents(); }); } // ==================== 座次表 ==================== function loadSeating() { const container = document.getElementById('page-seating'); container.innerHTML = `

座位编排

讲 台

每行8列,1-2列、3-4列、5-6列、7-8列为一组,组间过道。点击学生交换座位。

未就座学生

暂无未就座学生

`; if (!AppState.currentProfile) return; supabase.from('students').select('*').eq('class_id', AppState.currentProfile.class_id).order('seat').then(({ data }) => { AppState.allStudents = data; renderSeatGrid(); renderUnseated(); }); document.getElementById('auto-seat-btn').addEventListener('click', autoSeat); document.getElementById('reset-seat-btn').addEventListener('click', resetSeats); } function renderSeatGrid() { const container = document.getElementById('seat-grid-container'); container.innerHTML = ''; const maxSeat = 64; const seatMap = {}; AppState.allStudents.forEach(s => { if (s.seat && s.seat <= 64) seatMap[s.seat] = s; }); let html = '
'; for (let row = 0; row < 8; row++) { html += '
'; for (let group = 0; group < 4; group++) { html += '
'; for (let col = 0; col < 2; col++) { const seatNum = row * 8 + group * 2 + col + 1; if (seatNum > maxSeat) break; const student = seatMap[seatNum]; const genderClass = student ? (student.gender === '男' ? 'male' : student.gender === '女' ? 'female' : '') : ''; const genderIcon = student ? (student.gender === '男' ? '' : student.gender === '女' ? '' : '') : ''; const roleText = student?.role ? `
${escapeHtml(student.role)}
` : ''; html += `
${student ? `
${escapeHtml(student.name)}
${roleText}
座位${seatNum} ${genderIcon}
` : `
座位${seatNum}
`}
`; } html += '
'; if (group < 3) { html += '
过道
'; } } html += '
'; } html += '
'; container.innerHTML = html; container.querySelectorAll('.seat-item:not(.empty)').forEach(div => div.addEventListener('click', () => handleSeatClick(div))); } function handleSeatClick(div) { const studentId = parseInt(div.dataset.studentId); const student = AppState.allStudents.find(s => s.id === studentId); if (!student) return; if (AppState.selectedStudents.length === 0) { AppState.selectedStudents.push(student); div.classList.add('selected'); } else if (AppState.selectedStudents.length === 1 && AppState.selectedStudents[0].id === student.id) { AppState.selectedStudents = []; div.classList.remove('selected'); } else { const other = AppState.selectedStudents[0]; const tempSeat = other.seat; other.seat = student.seat; student.seat = tempSeat; AppState.selectedStudents = []; updateSeatInDatabase(other); updateSeatInDatabase(student); renderSeatGrid(); renderUnseated(); } } function updateSeatInDatabase(student) { supabase.from('students').update({ seat: student.seat }).eq('id', student.id).then(() => {}); } function renderUnseated() { const unseated = AppState.allStudents.filter(s => !s.seat || s.seat > 64); const container = document.getElementById('unseated-students'); if (unseated.length === 0) { container.innerHTML = '

暂无未就座学生

'; return; } container.innerHTML = unseated.map(s => `${escapeHtml(s.name)}${s.role ? ' ('+escapeHtml(s.role)+')' : ''}`).join(''); } function autoSeat() { const students = [...AppState.allStudents]; for (let i = students.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [students[i], students[j]] = [students[j], students[i]]; } const updates = students.map((s, idx) => ({ id: s.id, seat: idx + 1 })); Promise.all(updates.map(u => supabase.from('students').update({ seat: u.seat }).eq('id', u.id))).then(() => { showToast('自动排座完成', 'success'); loadSeating(); }); } function resetSeats() { if (!confirm('确定清空所有座位?')) return; Promise.all(AppState.allStudents.map(s => supabase.from('students').update({ seat: null }).eq('id', s.id))).then(() => { showToast('座位已清空', 'success'); loadSeating(); }); } // ==================== 随机点名 ==================== let rollcallTimer = null; let isRolling = false; let calledIds = new Set(); function loadRollcall() { const container = document.getElementById('page-rollcall'); container.innerHTML = `
准备就绪
已点名 0 人 / 共 0

点名历史

暂无记录

`; if (!AppState.currentProfile) return; loadRollcallHistory(); document.getElementById('start-rollcall-btn').addEventListener('click', startRollcall); document.getElementById('stop-rollcall-btn').addEventListener('click', stopRollcall); document.getElementById('reset-rollcall-btn').addEventListener('click', resetRollcall); } function startRollcall() { if (isRolling || AppState.allStudents.length === 0) return; const available = AppState.allStudents.filter(s => !calledIds.has(s.id)); if (available.length === 0) { resetRollcall(); return; } isRolling = true; document.getElementById('start-rollcall-btn').style.display = 'none'; document.getElementById('stop-rollcall-btn').style.display = 'inline-flex'; const display = document.getElementById('rollcall-display-box'); display.classList.add('rolling'); rollcallTimer = setInterval(() => { const random = available[Math.floor(Math.random() * available.length)]; display.textContent = random.name; }, 80); } function stopRollcall() { if (!isRolling) return; isRolling = false; clearInterval(rollcallTimer); const display = document.getElementById('rollcall-display-box'); display.classList.remove('rolling'); document.getElementById('start-rollcall-btn').style.display = 'inline-flex'; document.getElementById('stop-rollcall-btn').style.display = 'none'; const selectedName = display.textContent; const selected = AppState.allStudents.find(s => s.name === selectedName); if (selected) { supabase.from('point_records').insert({ class_id: AppState.currentProfile.class_id, student_id: selected.id, point_date: new Date().toISOString().split('T')[0] }).then(() => { loadRollcallHistory(); }); showToast(`点中:${selected.name}`, 'success'); } } function resetRollcall() { if (isRolling) { clearInterval(rollcallTimer); isRolling = false; } document.getElementById('start-rollcall-btn').style.display = 'inline-flex'; document.getElementById('stop-rollcall-btn').style.display = 'none'; document.getElementById('rollcall-display-box').classList.remove('rolling'); document.getElementById('rollcall-display-box').textContent = '准备就绪'; supabase.from('point_records').delete().eq('class_id', AppState.currentProfile.class_id).then(() => { loadRollcallHistory(); }); } function loadRollcallHistory() { supabase.from('point_records').select('*, students(name)').eq('class_id', AppState.currentProfile.class_id).order('created_at', { ascending: false }).limit(30).then(({ data, error }) => { if (error) return; calledIds = new Set(data.map(r => r.student_id)); const container = document.getElementById('rollcall-history-list'); if (data.length === 0) { container.innerHTML = '

暂无记录

'; } else { container.innerHTML = data.map(r => `
${escapeHtml(r.students?.name || '未知')}${new Date(r.created_at).toLocaleTimeString('zh-CN')}
`).join(''); } document.getElementById('rollcall-count').textContent = data.length; document.getElementById('rollcall-total').textContent = AppState.allStudents.length; }); } // ==================== 作业管理 ==================== let currentHomeworkSubject = ''; let currentHomeworkWeek = 'all'; function loadHomework() { const container = document.getElementById('page-homework'); container.innerHTML = `

0

总作业数

0

本周作业

-

平均提交率

0

累计未交人次

学科分类

作业列表

`; if (!AppState.currentProfile) return; document.getElementById('add-homework-btn').addEventListener('click', () => openHomeworkModal()); document.getElementById('week-filter').addEventListener('change', (e) => { currentHomeworkWeek = e.target.value; renderHomeworkList(); }); document.getElementById('hw-total-card').addEventListener('click', () => showHomeworkStats('total')); document.getElementById('hw-week-card').addEventListener('click', () => showHomeworkStats('week')); document.getElementById('hw-submit-rate-card').addEventListener('click', () => showHomeworkStats('rate')); document.getElementById('hw-missing-card').addEventListener('click', () => showHomeworkStats('missing')); loadHomeworkData(); } function loadHomeworkData() { supabase.from('homework').select('*').eq('class_id', AppState.currentProfile.class_id).order('created_at', { ascending: false }).then(({ data, error }) => { if (error) { showToast('加载作业失败', 'error'); return; } AppState.homeworkList = data || []; document.getElementById('hw-total').textContent = AppState.homeworkList.length; const now = new Date(); const weekAgo = new Date(now.getTime() - 7*24*60*60*1000); document.getElementById('hw-week').textContent = AppState.homeworkList.filter(h => new Date(h.assigned_date) >= weekAgo).length; const homeworkIds = AppState.homeworkList.map(h => h.id); if (homeworkIds.length > 0) { supabase.from('homework_submissions').select('status').in('homework_id', homeworkIds).then(({ data: subs }) => { if (subs && subs.length > 0) { const submitted = subs.filter(s => s.status === 'submitted' || s.status === 'late').length; document.getElementById('hw-submit-rate').textContent = Math.round((submitted / subs.length) * 100) + '%'; } document.getElementById('hw-missing').textContent = subs ? subs.filter(s => s.status === 'missing').length : 0; }); } const subjects = [...new Set(AppState.homeworkList.map(h => h.subject))]; const subjectFilter = document.getElementById('subject-filter'); subjectFilter.innerHTML = ``; subjects.forEach(sub => { const count = AppState.homeworkList.filter(h => h.subject === sub).length; subjectFilter.innerHTML += ``; }); subjectFilter.addEventListener('click', (e) => { const el = e.target.closest('.nav-item'); if (!el) return; currentHomeworkSubject = el.dataset.subject; subjectFilter.querySelectorAll('.nav-item').forEach(x => x.classList.remove('active')); el.classList.add('active'); renderHomeworkList(); }); renderHomeworkList(); }); } function renderHomeworkList() { let list = AppState.homeworkList; if (currentHomeworkSubject) list = list.filter(h => h.subject === currentHomeworkSubject); if (currentHomeworkWeek === 'current') { const now = new Date(); const weekStart = new Date(now); weekStart.setDate(now.getDate() - now.getDay() + 1); list = list.filter(h => new Date(h.assigned_date) >= weekStart); } else if (currentHomeworkWeek === 'last') { const now = new Date(); const weekStart = new Date(now); weekStart.setDate(now.getDate() - now.getDay() - 6); const weekEnd = new Date(weekStart); weekEnd.setDate(weekStart.getDate() + 6); list = list.filter(h => new Date(h.assigned_date) >= weekStart && new Date(h.assigned_date) <= weekEnd); } const container = document.getElementById('homework-list-container'); if (list.length === 0) { container.innerHTML = '

暂无作业

'; return; } container.innerHTML = list.map(hw => { const deadline = hw.deadline ? new Date(hw.deadline) : null; const daysLeft = deadline ? Math.ceil((deadline - new Date()) / (24*60*60*1000)) : null; const deadlineStr = hw.deadline ? `${hw.deadline}(星期${'日一二三四五六'[new Date(hw.deadline).getDay()]})` : '无截止'; return `
${escapeHtml(hw.subject)}${daysLeft !== null ? (daysLeft < 0 ? '已截止' : `剩余${daysLeft}天`) : '无截止'}
${escapeHtml(hw.title)}
布置于 ${hw.assigned_date || '-'} | 截止:${deadlineStr}
`; }).join(''); container.querySelectorAll('.homework-card').forEach(card => card.addEventListener('click', () => openHomeworkDetail(card.dataset.id))); } function openHomeworkModal(homeworkId = null) { const hw = homeworkId ? AppState.homeworkList.find(h => h.id == homeworkId) : null; const modal = createModal( hw ? '编辑作业' : '布置作业', `

`, ``, '850px' ); document.getElementById('hw-cancel').addEventListener('click', () => modal.remove()); document.getElementById('hw-deadline').addEventListener('change', (e) => { if (e.target.value) { const week = '日一二三四五六'[new Date(e.target.value).getDay()]; document.getElementById('hw-deadline-week').textContent = `选中日期:星期${week}`; } else { document.getElementById('hw-deadline-week').textContent = ''; } }); document.getElementById('hw-save').addEventListener('click', async () => { const subject = modal.querySelector('#hw-subject').value.trim(); const title = modal.querySelector('#hw-title').value.trim(); const description = modal.querySelector('#hw-description').value.trim(); const deadline = modal.querySelector('#hw-deadline').value || null; if (!subject || !title) { showToast('科目和标题不能为空', 'error'); return; } if (hw) { await supabase.from('homework').update({ subject, title, description, deadline }).eq('id', hw.id); } else { await supabase.from('homework').insert({ class_id: AppState.currentProfile.class_id, subject, title, description, deadline }); } modal.remove(); showToast('作业已保存', 'success'); loadHomeworkData(); }); } function openHomeworkDetail(homeworkId) { const hw = AppState.homeworkList.find(h => h.id == homeworkId); if (!hw) return; const deadlineStr = hw.deadline ? `${hw.deadline}(星期${'日一二三四五六'[new Date(hw.deadline).getDay()]})` : '无截止'; const modal = createModal( `${escapeHtml(hw.subject)}:${escapeHtml(hw.title)}`, `

${escapeHtml(hw.description || '无描述')} | 截止:${deadlineStr}

提交管理

加载中...
`, ``, '700px' ); document.getElementById('detail-close').addEventListener('click', () => modal.remove()); document.getElementById('edit-hw-btn').addEventListener('click', () => { modal.remove(); openHomeworkModal(hw.id); }); supabase.from('homework_submissions').select('*, students(name)').eq('homework_id', hw.id).then(({ data }) => { const container = modal.querySelector('#submission-detail'); const statusMap = { submitted: '已交', late: '迟交', missing: '未交' }; const allStudents = AppState.allStudents; // 建立现有提交记录映射 const recordMap = {}; data.forEach(s => { recordMap[s.student_id] = s.status; }); container.innerHTML = `
${allStudents.map(stu => { const status = recordMap[stu.id] || 'submitted'; return ``; }).join('')}
学生状态
${escapeHtml(stu.name)}
`; container.querySelector('#save-submissions-btn').addEventListener('click', async () => { const selects = container.querySelectorAll('.sub-status-select'); const records = Array.from(selects).map(sel => ({ homework_id: hw.id, student_id: parseInt(sel.dataset.studentId), status: sel.value })); await supabase.from('homework_submissions').upsert(records, { onConflict: 'homework_id,student_id' }); showToast('提交状态已保存', 'success'); loadHomeworkData(); }); container.querySelectorAll('.sub-student-link').forEach(a => a.addEventListener('click', (e) => { e.preventDefault(); modal.remove(); switchPageWithStudent('analysis', a.dataset.id); })); }); } function showHomeworkStats(type) { const modal = createModal( '作业统计详情', `
加载中...
`, ``, '750px' ); document.getElementById('stats-close').addEventListener('click', () => modal.remove()); const container = modal.querySelector('#homework-stats-content'); if (type === 'total') { container.innerHTML = `
${AppState.homeworkList.map(h => ``).join('')}
科目标题布置日期截止日期
${escapeHtml(h.subject)}${escapeHtml(h.title)}${h.assigned_date || '-'}${h.deadline || '-'}
`; } else if (type === 'week') { const now = new Date(); const weekStart = new Date(now); weekStart.setDate(now.getDate() - now.getDay() + 1); const weekList = AppState.homeworkList.filter(h => new Date(h.assigned_date) >= weekStart); container.innerHTML = `
${weekList.map(h => ``).join('')}
科目标题布置日期
${escapeHtml(h.subject)}${escapeHtml(h.title)}${h.assigned_date || '-'}
`; } else if (type === 'rate') { container.innerHTML = '

各作业提交率:

'; AppState.homeworkList.forEach(hw => { supabase.from('homework_submissions').select('status').eq('homework_id', hw.id).then(({ data: subs }) => { if (subs && subs.length > 0) { const submitted = subs.filter(s => s.status === 'submitted' || s.status === 'late').length; container.innerHTML += `

${escapeHtml(hw.subject)} - ${escapeHtml(hw.title)}: ${Math.round((submitted/subs.length)*100)}%

`; } }); }); } else if (type === 'missing') { const homeworkIds = AppState.homeworkList.map(h => h.id); if (homeworkIds.length === 0) { container.innerHTML = '暂无数据'; return; } supabase.from('homework_submissions').select('*, students(name), homework(subject, title)').eq('status', 'missing').in('homework_id', homeworkIds).then(({ data }) => { if (!data || data.length === 0) { container.innerHTML = '暂无未交记录'; return; } const studentMissing = {}; data.forEach(s => { const key = s.student_id; if (!studentMissing[key]) studentMissing[key] = { name: s.students?.name, count: 0, subjects: [] }; studentMissing[key].count++; studentMissing[key].subjects.push(`${s.homework?.subject}:${s.homework?.title}`); }); container.innerHTML = `
`; Object.values(studentMissing).forEach(sm => { container.innerHTML += ``; }); container.innerHTML += '
学生科目作业标题未交次数
${escapeHtml(sm.name)}${escapeHtml(sm.subjects.join('、'))}${escapeHtml(sm.subjects.join('、'))}${sm.count}
'; }); } } // ==================== 成绩管理 ==================== function loadGrades() { const container = document.getElementById('page-grades'); container.innerHTML = `

学期管理

考试列表

暂无考试

成绩导入

`; if (!AppState.currentProfile) return; loadSemesters(); document.getElementById('add-semester-btn').addEventListener('click', () => { const modal = createModal( '新增学期', `
`, ``, '500px' ); document.getElementById('semester-cancel').addEventListener('click', () => modal.remove()); document.getElementById('semester-save').addEventListener('click', async () => { const name = modal.querySelector('#semester-name').value.trim(); if (!name) { showToast('请输入学期名称', 'error'); return; } await supabase.from('semesters').insert({ class_id: AppState.currentProfile.class_id, name, start_date: modal.querySelector('#semester-start').value || null, end_date: modal.querySelector('#semester-end').value || null }); modal.remove(); showToast('学期已创建', 'success'); loadSemesters(); }); }); document.getElementById('add-exam-btn').addEventListener('click', openExamModal); document.getElementById('import-grades-btn').addEventListener('click', openGradeImportModal); loadExamList(); } function loadSemesters() { supabase.from('semesters').select('*').eq('class_id', AppState.currentProfile.class_id).order('start_date').then(({ data }) => { const container = document.getElementById('semester-list'); if (!data || data.length === 0) { container.innerHTML = '暂无学期,请先新增'; return; } container.innerHTML = data.map(sem => ``).join(''); container.querySelectorAll('button[data-semester-id]').forEach(btn => btn.addEventListener('click', () => { AppState.selectedSemesterId = btn.dataset.semesterId; container.querySelectorAll('button').forEach(b => b.classList.remove('btn-primary')); btn.classList.add('btn-primary'); loadExamList(); })); }); } function loadExamList() { let query = supabase.from('exams').select('*').eq('class_id', AppState.currentProfile.class_id).order('exam_date', { ascending: false }); if (AppState.selectedSemesterId) query = query.eq('semester_id', AppState.selectedSemesterId); query.then(({ data }) => { const container = document.getElementById('exam-list'); if (!data || data.length === 0) { container.innerHTML = '

暂无考试

'; return; } container.innerHTML = data.map(exam => `
${escapeHtml(exam.exam_name)} ${exam.exam_date || '-'}
`).join(''); container.querySelectorAll('.homework-card').forEach(card => card.addEventListener('click', () => openGradeEntry(card.dataset.examId))); }); } function openExamModal() { const modal = createModal( '新建考试', `
`, ``, '500px' ); supabase.from('semesters').select('*').eq('class_id', AppState.currentProfile.class_id).then(({ data }) => { const select = modal.querySelector('#exam-semester'); select.innerHTML = '' + (data || []).map(s => ``).join(''); }); document.getElementById('exam-cancel').addEventListener('click', () => modal.remove()); document.getElementById('exam-save').addEventListener('click', async () => { const name = modal.querySelector('#exam-name').value.trim(); if (!name) { showToast('请输入名称', 'error'); return; } await supabase.from('exams').insert({ class_id: AppState.currentProfile.class_id, exam_name: name, exam_date: modal.querySelector('#exam-date').value || null, exam_type: modal.querySelector('#exam-type').value, semester_id: modal.querySelector('#exam-semester').value || null }); modal.remove(); showToast('考试已创建', 'success'); loadExamList(); }); } function openGradeEntry(examId) { AppState.selectedExamId = examId; Promise.all([ supabase.from('students').select('*').eq('class_id', AppState.currentProfile.class_id).order('seat'), supabase.from('grades').select('*').eq('exam_id', examId) ]).then(([studentsRes, gradesRes]) => { const students = studentsRes.data || []; const grades = gradesRes.data || []; const gradeMap = {}; grades.forEach(g => gradeMap[`${g.student_id}_${g.subject}`] = g.score); const subjects = ['语文','数学','英语','物理','化学','生物','政治','历史','地理']; const area = createModal( '成绩录入与查看', `
${subjects.map(s => ``).join('')}${students.map(stu => { let total = 0; const cells = subjects.map(sub => { const val = gradeMap[`${stu.id}_${sub}`] ?? ''; if (val !== '') total += parseFloat(val); return ``; }).join(''); return `${cells}`; }).join('')}
姓名${s}总分
${escapeHtml(stu.name)}${total}
`, ``, '900px' ); area.querySelector('#grade-close').addEventListener('click', () => area.remove()); area.querySelector('#grade-save-btn').addEventListener('click', async () => { const inputs = area.querySelectorAll('.grade-input'); const records = []; inputs.forEach(inp => { if (inp.value !== '' && !isNaN(parseFloat(inp.value))) records.push({ class_id: AppState.currentProfile.class_id, exam_id: examId, student_id: parseInt(inp.dataset.student), subject: inp.dataset.subject, score: parseFloat(inp.value) }); }); await supabase.from('grades').delete().eq('exam_id', examId); if (records.length) await supabase.from('grades').insert(records); showToast('成绩保存成功', 'success'); area.remove(); loadExamList(); }); area.querySelector('#view-stats-btn').addEventListener('click', () => showGradeStats(examId, students, gradeMap)); area.querySelectorAll('.grade-student-link').forEach(a => a.addEventListener('click', (e) => { e.preventDefault(); area.remove(); switchPageWithStudent('analysis', a.dataset.id); })); }); } function showGradeStats(examId, students, gradeMap) { const modal = createModal( '成绩统计', `
加载中...
`, ``, '750px' ); document.getElementById('stats-close').addEventListener('click', () => modal.remove()); const container = modal.querySelector('#stats-content'); const subjects = ['语文','数学','英语','物理','化学','生物','政治','历史','地理']; let html = '
'; subjects.forEach(sub => { const scores = students.map(s => parseFloat(gradeMap[`${s.id}_${sub}`] || '0')).filter(v => !isNaN(v) && v > 0); if (scores.length > 0) { const avg = (scores.reduce((a,b) => a+b, 0) / scores.length).toFixed(1); const passRate = Math.round((scores.filter(v => v >= 60).length / scores.length) * 100); const excellentRate = Math.round((scores.filter(v => v >= 85).length / scores.length) * 100); html += ``; } }); html += '
科目平均分及格率优秀率
${sub}${avg}${passRate}%${excellentRate}%
'; container.innerHTML = html; } function deleteExam(examId) { if (!confirm('确定删除该考试?')) return; supabase.from('exams').delete().eq('id', examId).then(() => { showToast('考试已删除', 'success'); loadExamList(); }); } function openGradeImportModal() { const modal = createModal( '导入成绩Excel', `

Excel格式:第一行表头含“姓名”或“学号”,其余列为科目名,成绩按行填入。

`, ``, '600px' ); supabase.from('exams').select('*').eq('class_id', AppState.currentProfile.class_id).then(({ data }) => { const select = modal.querySelector('#import-exam-select'); select.innerHTML = '' + (data || []).map(e => ``).join(''); }); document.getElementById('import-grades-cancel').addEventListener('click', () => modal.remove()); document.getElementById('import-grades-submit').addEventListener('click', async () => { const examId = modal.querySelector('#import-exam-select').value; const file = modal.querySelector('#import-grades-file').files[0]; if (!examId || !file) { showToast('请选择考试和文件', 'error'); return; } const data = await file.arrayBuffer(); const wb = XLSX.read(data, { type: 'array' }); const rows = XLSX.utils.sheet_to_json(wb.Sheets[wb.SheetNames[0]], { header: 1 }); if (rows.length < 2) { showToast('数据不足', 'error'); return; } const header = rows[0]; const nameIdx = header.findIndex(h => String(h).includes('姓名') || String(h).includes('学号')); if (nameIdx === -1) { showToast('未找到姓名列', 'error'); return; } const subjectIndices = []; header.forEach((h, idx) => { if (idx !== nameIdx) subjectIndices.push({ idx, subject: String(h).trim() }); }); const studentsRes = await supabase.from('students').select('*').eq('class_id', AppState.currentProfile.class_id); const studentList = studentsRes.data || []; const records = []; for (let i = 1; i < rows.length; i++) { const row = rows[i]; const studentName = String(row[nameIdx]).trim(); const student = studentList.find(s => s.name === studentName || s.student_no === studentName); if (!student) continue; subjectIndices.forEach(({ idx, subject }) => { const score = parseFloat(row[idx]); if (!isNaN(score)) records.push({ class_id: AppState.currentProfile.class_id, exam_id: examId, student_id: student.id, subject, score }); }); } await supabase.from('grades').delete().eq('exam_id', examId); if (records.length) await supabase.from('grades').insert(records); modal.remove(); showToast(`导入 ${records.length} 条成绩`, 'success'); loadExamList(); }); } // ==================== 课程表 ==================== const SUBJECT_COLORS = { '语文': '#ef4444', '数学': '#3b82f6', '英语': '#10b981', '物理': '#f59e0b', '化学': '#8b5cf6', '生物': '#06b6d4', '政治': '#eab308', '历史': '#a16207', '地理': '#ec4899', '体育': '#22c55e', '音乐': '#f472b6', '美术': '#fb923c', '其他': '#94a3b8' }; function loadSchedule() { const container = document.getElementById('page-schedule'); container.innerHTML = `

课程表

`; if (!AppState.currentProfile) return; loadScheduleData(); document.getElementById('schedule-view-type').addEventListener('change', (e) => { AppState.scheduleView = e.target.value; loadScheduleData(); }); document.getElementById('schedule-week-type').addEventListener('change', loadScheduleData); document.getElementById('edit-schedule-btn').addEventListener('click', () => openScheduleEditor()); document.getElementById('import-schedule-btn').addEventListener('click', openScheduleImportModal); document.getElementById('ai-schedule-btn').addEventListener('click', aiScheduleSuggest); document.getElementById('period-time-settings-btn').addEventListener('click', openPeriodTimeSettings); } function getPeriodTime(period) { if (AppState.periodTimes && AppState.periodTimes[period]) return AppState.periodTimes[period]; const times = {1:'8:00-8:45',2:'8:55-9:40',3:'9:50-10:35',4:'10:45-11:30',5:'14:00-14:45',6:'14:55-15:40',7:'15:50-16:35',8:'16:45-17:30'}; return times[period] || ''; } function loadScheduleData() { const weekType = document.getElementById('schedule-week-type')?.value || 'both'; const viewType = AppState.scheduleView; let query = supabase.from('schedule').select('*').eq('class_id', AppState.currentProfile.class_id); if (viewType === 'class') { query = query.or(`week_type.eq.${weekType},week_type.eq.both`); } else { const teacherName = AppState.currentProfile.realname || AppState.currentProfile.username; query = query.eq('teacher_name', teacherName).or(`week_type.eq.${weekType},week_type.eq.both`); } query.then(({ data, error }) => { if (error) { showToast('加载课程表失败', 'error'); return; } const table = document.getElementById('schedule-table'); const days = ['周一','周二','周三','周四','周五','周六','周日']; const periods = 8; let html = '节次/时间'; days.forEach((d, i) => { const weekend = i >= 5; html += `${d}`; }); html += ''; for (let period = 1; period <= periods; period++) { html += `第${period}节
${getPeriodTime(period)}`; for (let day = 1; day <= 7; day++) { const slot = data.find(s => s.day_of_week === day && s.period === period); const color = slot ? SUBJECT_COLORS[slot.subject] || SUBJECT_COLORS['其他'] : ''; const isWeekend = day >= 6; html += ``; if (slot) { html += `
${escapeHtml(slot.subject)}
${escapeHtml(slot.teacher_name || '')}
`; } html += ''; } html += ''; if (period === 4) { html += `午休 12:00 - 14:00`; } } html += ''; table.innerHTML = html; table.querySelectorAll('.schedule-block').forEach(block => block.addEventListener('click', () => openScheduleEditor(block.dataset.slotId))); }); } function openScheduleEditor(slotId = null) { const modal = createModal( '编辑课程', `
`, `${slotId ? '' : ''}`, '500px' ); if (slotId) { supabase.from('schedule').select('*').eq('id', slotId).single().then(({ data }) => { if (data) { modal.querySelector('#edit-day').value = data.day_of_week; modal.querySelector('#edit-period').value = data.period; modal.querySelector('#edit-subject').value = data.subject; modal.querySelector('#edit-teacher').value = data.teacher_name || ''; modal.querySelector('#edit-week-type').value = data.week_type; } }); } document.getElementById('edit-cancel').addEventListener('click', () => modal.remove()); document.getElementById('edit-save').addEventListener('click', async () => { const day = parseInt(modal.querySelector('#edit-day').value); const period = parseInt(modal.querySelector('#edit-period').value); const subject = modal.querySelector('#edit-subject').value.trim(); const teacher = modal.querySelector('#edit-teacher').value.trim(); const weekType = modal.querySelector('#edit-week-type').value; if (!subject || !period) { showToast('请填写完整', 'error'); return; } if (slotId) { await supabase.from('schedule').update({ day_of_week: day, period, subject, teacher_name: teacher || null, week_type: weekType }).eq('id', slotId); } else { await supabase.from('schedule').upsert({ class_id: AppState.currentProfile.class_id, day_of_week: day, period, subject, teacher_name: teacher || null, week_type: weekType }, { onConflict: 'class_id,day_of_week,period,week_type' }); } modal.remove(); showToast('课程已更新', 'success'); loadScheduleData(); }); if (slotId) { document.getElementById('edit-delete').addEventListener('click', async () => { if (confirm('确定删除该课程?')) { await supabase.from('schedule').delete().eq('id', slotId); modal.remove(); showToast('课程已删除', 'success'); loadScheduleData(); } }); } } function openScheduleImportModal() { const modal = createModal( '导入课程表Excel', `

Excel格式:第一行为节次,第一列为星期,交叉单元格填课程名称。可含教师名(如“语文(张老师)”)。

`, ``, '600px' ); document.getElementById('import-schedule-cancel').addEventListener('click', () => modal.remove()); document.getElementById('import-schedule-submit').addEventListener('click', async () => { const file = modal.querySelector('#import-schedule-file').files[0]; if (!file) { showToast('请选择文件', 'error'); return; } const data = await file.arrayBuffer(); const wb = XLSX.read(data, { type: 'array' }); const rows = XLSX.utils.sheet_to_json(wb.Sheets[wb.SheetNames[0]], { header: 1 }); const records = []; const dayMap = { '周一': 1, '星期二': 2, '周三': 3, '周四': 4, '周五': 5, '周六': 6, '周日': 7 }; for (let i = 1; i < rows.length; i++) { const row = rows[i]; if (!row[0]) continue; const dayStr = String(row[0]).trim(); let day = dayMap[dayStr] || parseInt(dayStr); if (!day || day < 1 || day > 7) continue; for (let j = 1; j < row.length; j++) { const cell = String(row[j] || '').trim(); if (!cell) continue; const period = j; let subject = cell; let teacher = null; const match = cell.match(/^(.+?)\((.+?)\)$/); if (match) { subject = match[1].trim(); teacher = match[2].trim(); } records.push({ class_id: AppState.currentProfile.class_id, day_of_week: day, period, subject, teacher_name: teacher, week_type: 'both' }); } } if (records.length === 0) { showToast('未识别到课程', 'error'); return; } await supabase.from('schedule').delete().eq('class_id', AppState.currentProfile.class_id); await supabase.from('schedule').insert(records); modal.remove(); showToast(`导入 ${records.length} 条课程`, 'success'); loadScheduleData(); }); } async function aiScheduleSuggest() { const result = await callAI('schedule_suggest', { prompt: '请根据一般初中或高中课程设置,提供一份合理的课程表排布建议,包含每天8节课,周一至周五。' }); if (result) { const modal = createModal( 'AI 排课建议', `
${escapeHtml(result)}
`, ``, '600px' ); document.getElementById('ai-close').addEventListener('click', () => modal.remove()); } } function openPeriodTimeSettings() { const modal = createModal( '课程时段设置', `
${[1,2,3,4,5,6,7,8].map(i => `
`).join('')}
`, ``, '500px' ); document.getElementById('pt-cancel').addEventListener('click', () => modal.remove()); document.getElementById('pt-save').addEventListener('click', async () => { const times = {}; for (let i = 1; i <= 8; i++) { times[i] = document.getElementById(`period-time-${i}`).value.trim(); } AppState.periodTimes = times; await supabase.from('settings').upsert({ class_id: AppState.currentProfile.class_id, period_times: times }, { onConflict: 'class_id' }); modal.remove(); showToast('时段设置已保存', 'success'); loadScheduleData(); }); } // ==================== 考勤管理 ==================== function loadAttendance() { const container = document.getElementById('page-attendance'); container.innerHTML = `

-

今日出勤率

-

本月出勤率

0

本月缺勤

0

本月请假

每日考勤

姓名状态

请假管理

学生日期状态操作
`; if (!AppState.currentProfile) return; const dateInput = document.getElementById('attendance-date'); if (!dateInput.value) dateInput.value = new Date().toISOString().split('T')[0]; loadAttendanceData(); loadLeaves(); loadAttendanceStats(); document.getElementById('load-attendance-btn').addEventListener('click', loadAttendanceData); document.getElementById('save-attendance-btn').addEventListener('click', saveAttendance); document.getElementById('add-leave-btn').addEventListener('click', openLeaveModal); document.getElementById('all-present-btn').addEventListener('click', () => { document.querySelectorAll('.att-status-select').forEach(sel => sel.value = 'present'); }); document.getElementById('att-today-card').addEventListener('click', () => showAttStats('today')); document.getElementById('att-month-card').addEventListener('click', () => showAttStats('month')); document.getElementById('att-absent-card').addEventListener('click', () => showAttStats('absent')); document.getElementById('att-leave-card').addEventListener('click', () => showAttStats('leave')); } function loadAttendanceData() { const date = document.getElementById('attendance-date').value; if (!date) return; Promise.all([ supabase.from('students').select('*').eq('class_id', AppState.currentProfile.class_id).order('seat'), supabase.from('attendance').select('*').eq('class_id', AppState.currentProfile.class_id).eq('date', date) ]).then(([studentsRes, attRes]) => { AppState.attendanceStudents = studentsRes.data || []; const recordMap = {}; (attRes.data || []).forEach(r => recordMap[r.student_id] = r.status); const tbody = document.getElementById('attendance-tbody'); tbody.innerHTML = ''; AppState.attendanceStudents.forEach(s => { const status = recordMap[s.id] || 'present'; const tr = document.createElement('tr'); tr.innerHTML = `${escapeHtml(s.name)}`; tbody.appendChild(tr); }); tbody.querySelectorAll('.att-student-link').forEach(a => a.addEventListener('click', (e) => { e.preventDefault(); switchPageWithStudent('analysis', a.dataset.id); })); }); } function saveAttendance() { const date = document.getElementById('attendance-date').value; const selects = document.querySelectorAll('.att-status-select'); const records = Array.from(selects).map(sel => ({ class_id: AppState.currentProfile.class_id, student_id: parseInt(sel.dataset.studentId), date, status: sel.value })); supabase.from('attendance').upsert(records, { onConflict: 'class_id,student_id,date' }).then(() => { showToast('考勤保存成功!', 'success'); loadAttendanceStats(); refreshModule('dashboard'); }); } function loadAttendanceStats() { const today = new Date().toISOString().split('T')[0]; const monthStart = new Date(); monthStart.setDate(1); const monthStartStr = monthStart.toISOString().split('T')[0]; Promise.all([ supabase.from('students').select('*', { count: 'exact', head: true }).eq('class_id', AppState.currentProfile.class_id), supabase.from('attendance').select('status').eq('class_id', AppState.currentProfile.class_id).eq('date', today), supabase.from('attendance').select('status').eq('class_id', AppState.currentProfile.class_id).gte('date', monthStartStr) ]).then(([studentRes, todayAttRes, monthAttRes]) => { const totalStudents = studentRes.count || 0; const todayAtt = todayAttRes.data || []; const monthAtt = monthAttRes.data || []; if (totalStudents > 0 && todayAtt.length > 0) { const present = todayAtt.filter(a => a.status === 'present' || a.status === 'late').length; document.getElementById('att-today-rate').textContent = Math.round((present / totalStudents) * 100) + '%'; } else document.getElementById('att-today-rate').textContent = '-'; if (monthAtt.length > 0) { const presentRecords = monthAtt.filter(a => a.status === 'present' || a.status === 'late').length; document.getElementById('att-month-rate').textContent = Math.round((presentRecords / monthAtt.length) * 100) + '%'; document.getElementById('att-absent-count').textContent = monthAtt.filter(a => a.status === 'absent').length; document.getElementById('att-leave-count').textContent = monthAtt.filter(a => a.status === 'leave').length; } }); } function loadLeaves() { supabase.from('leaves').select('*, students(name)').eq('class_id', AppState.currentProfile.class_id).order('date', { ascending: false }).then(({ data }) => { const tbody = document.getElementById('leaves-tbody'); tbody.innerHTML = ''; (data || []).forEach(l => { const tr = document.createElement('tr'); tr.innerHTML = `${escapeHtml(l.students?.name || '未知')}${l.date}${l.status === 'pending' ? '待审批' : l.status === 'approved' ? '已批准' : '已拒绝'}${l.status === 'pending' ? ` ` : '-'}`; tbody.appendChild(tr); }); tbody.querySelectorAll('.approve-leave').forEach(btn => btn.addEventListener('click', () => updateLeaveStatus(btn.dataset.id, 'approved'))); tbody.querySelectorAll('.reject-leave').forEach(btn => btn.addEventListener('click', () => updateLeaveStatus(btn.dataset.id, 'rejected'))); tbody.querySelectorAll('.leave-student-link').forEach(a => a.addEventListener('click', (e) => { e.preventDefault(); switchPageWithStudent('analysis', a.dataset.id); })); }); } function updateLeaveStatus(id, status) { supabase.from('leaves').update({ status }).eq('id', id).then(() => { showToast('请假状态已更新', 'success'); loadLeaves(); loadAttendanceStats(); refreshModule('dashboard'); }); } function openLeaveModal() { const modal = createModal( '新增请假', `
`, ``, '500px' ); document.getElementById('leave-cancel').addEventListener('click', () => modal.remove()); document.getElementById('leave-save').addEventListener('click', async () => { const student_id = modal.querySelector('#leave-student').value; const date = modal.querySelector('#leave-date').value; const reason = modal.querySelector('#leave-reason').value.trim(); if (!student_id || !date) { showToast('请选择学生和日期', 'error'); return; } await supabase.from('leaves').insert({ class_id: AppState.currentProfile.class_id, student_id: parseInt(student_id), date, reason: reason || null }); modal.remove(); showToast('请假记录已添加', 'success'); loadLeaves(); loadAttendanceStats(); refreshModule('dashboard'); }); } function showAttStats(type) { const modal = createModal( '考勤统计详情', `
加载中...
`, ``, '750px' ); document.getElementById('att-stats-close').addEventListener('click', () => modal.remove()); const container = modal.querySelector('#att-stats-content'); if (type === 'today') { const today = new Date().toISOString().split('T')[0]; supabase.from('attendance').select('*, students(name)').eq('class_id', AppState.currentProfile.class_id).eq('date', today).then(({ data }) => { if (!data || data.length === 0) { container.innerHTML = '暂无考勤记录'; return; } const statusMap = { present: '出勤', late: '迟到', absent: '缺勤', leave: '请假' }; container.innerHTML = `
${data.map(a => ``).join('')}
学生状态
${escapeHtml(a.students?.name || '')}${statusMap[a.status]}
`; }); } else if (type === 'month') { const monthStart = new Date(); monthStart.setDate(1); const monthStartStr = monthStart.toISOString().split('T')[0]; supabase.from('attendance').select('date, status').eq('class_id', AppState.currentProfile.class_id).gte('date', monthStartStr).then(({ data }) => { if (!data || data.length === 0) { container.innerHTML = '暂无记录'; return; } const grouped = {}; data.forEach(d => { if (!grouped[d.date]) grouped[d.date] = { present: 0, late: 0, absent: 0, leave: 0, total: 0 }; grouped[d.date][d.status]++; grouped[d.date].total++; }); const dates = Object.keys(grouped).sort(); const ctx = document.createElement('canvas'); container.innerHTML = ''; container.appendChild(ctx); new Chart(ctx, { type: 'line', data: { labels: dates, datasets: [{ label: '出勤率(%)', data: dates.map(d => Math.round((grouped[d].present + grouped[d].late) / grouped[d].total * 100)), borderColor: '#10b981' }] }, options: { responsive: true, scales: { y: { beginAtZero: true, max: 100 } } } }); }); } else if (type === 'absent') { const monthStart = new Date(); monthStart.setDate(1); const monthStartStr = monthStart.toISOString().split('T')[0]; supabase.from('attendance').select('*, students(name)').eq('class_id', AppState.currentProfile.class_id).gte('date', monthStartStr).eq('status', 'absent').then(({ data }) => { if (!data || data.length === 0) { container.innerHTML = '本月无缺勤记录'; return; } const studentMap = {}; data.forEach(a => { if (!studentMap[a.student_id]) studentMap[a.student_id] = { name: a.students?.name, count: 0, dates: [] }; studentMap[a.student_id].count++; studentMap[a.student_id].dates.push(a.date); }); container.innerHTML = `
${Object.values(studentMap).map(s => ``).join('')}
学生缺勤次数日期
${escapeHtml(s.name)}${s.count}${s.dates.join(', ')}
`; }); } else if (type === 'leave') { supabase.from('leaves').select('*, students(name)').eq('class_id', AppState.currentProfile.class_id).then(({ data }) => { if (!data || data.length === 0) { container.innerHTML = '暂无请假记录'; return; } container.innerHTML = `
${data.map(l => ``).join('')}
学生日期原因状态
${escapeHtml(l.students?.name || '')}${l.date}${escapeHtml(l.reason || '-')}${l.status === 'approved' ? '已批准' : l.status === 'rejected' ? '已拒绝' : '待审批'}
`; }); } } // ==================== 期末评语 ==================== function loadComments() { const container = document.getElementById('page-comments'); container.innerHTML = `

生成评语

学生评语墙

评语列表

暂无评语,请先生成

`; if (!AppState.currentProfile) return; document.getElementById('generate-comments-btn').addEventListener('click', generateComments); document.getElementById('copy-comments-btn').addEventListener('click', copyComments); renderAvatarWall(); } function renderAvatarWall() { const container = document.getElementById('comments-avatar-wall'); if (AppState.allStudents.length === 0) { container.innerHTML = '

暂无学生

'; return; } const colors = ['#6d5dfc','#10b981','#f59e0b','#ef4444','#3b82f6','#ec4899','#8b5cf6','#06b6d4']; container.innerHTML = AppState.allStudents.map((s, idx) => { const color = s.avatar_color || colors[idx % colors.length]; return `
${escapeHtml(s.name.charAt(0))}
${escapeHtml(s.name)}
`; }).join(''); container.querySelectorAll('.avatar-item').forEach(item => item.addEventListener('click', () => showStudentComment(item.dataset.studentId))); } function showStudentComment(studentId) { const student = AppState.allStudents.find(s => s.id == studentId); if (!student) return; const modal = createModal( `${escapeHtml(student.name)} 的评语`, `
加载中...
`, ``, '500px' ); document.getElementById('comment-close').addEventListener('click', () => modal.remove()); supabase.from('ai_reports').select('*').eq('student_id', studentId).eq('type', 'comment').maybeSingle().then(({ data }) => { const container = modal.querySelector('#comment-detail'); if (data) { container.innerHTML = `

${escapeHtml(data.content)}

`; } else { container.innerHTML = '

暂无评语,请先生成

'; } }); } async function generateComments() { const mode = document.getElementById('comment-mode').value; const style = document.getElementById('comment-style').value; if (AppState.allStudents.length === 0) { showToast('没有学生', 'error'); return; } const commentsList = document.getElementById('comments-list'); if (mode === 'local') { const templates = { encouraging: ['学习认真,成绩稳定。继续保持!','乐于助人,团结同学,是班级的榜样。','课堂表现积极,思维活跃,值得表扬。'], suggestive: ['建议加强语文阅读,提升理解能力。','数学基础不错,但需要多练习应用题。','英语听说能力有待提高,建议每天坚持朗读。'], humorous: ['你是班级的开心果,但学习也要跟上哦。','脑袋很聪明,就是偶尔偷懒,加油!','作业总是“明天交”,明天已经堆积如山啦。'] }; const tplList = templates[style] || templates.encouraging; AppState.commentList = AppState.allStudents.map(s => ({ student: s.name, comment: `${s.name}同学,${tplList[Math.floor(Math.random()*tplList.length)]}` })); commentsList.innerHTML = AppState.commentList.map(c => `
${escapeHtml(c.student)}:${escapeHtml(c.comment)}
`).join(''); showToast('已生成本地评语', 'success'); renderAvatarWall(); } else { showToast('正在调用AI生成,请稍候...', 'warning'); const results = await Promise.all(AppState.allStudents.map(async (s) => { const payload = { name: s.name, gender: s.gender, role: s.role, style }; const content = await callAI('comment', payload); if (content) { await supabase.from('ai_reports').upsert({ class_id: AppState.currentProfile.class_id, student_id: s.id, type: 'comment', content }); } return { student: s.name, comment: content || '暂无' }; })); AppState.commentList = results; commentsList.innerHTML = results.map(c => `
${escapeHtml(c.student)}:${escapeHtml(c.comment)}
`).join(''); showToast('AI评语生成完成', 'success'); renderAvatarWall(); } } function copyComments() { if (AppState.commentList.length === 0) { showToast('暂无评语', 'warning'); return; } const text = AppState.commentList.map(c => `${c.student}:${c.comment}`).join('\n'); navigator.clipboard.writeText(text).then(() => showToast('已复制', 'success')); } // ==================== 综合分析 ==================== function loadAnalysis() { const container = document.getElementById('page-analysis'); container.innerHTML = `

请选择学生

`; if (AppState.selectedStudentId) { document.getElementById('analysis-student-select').value = AppState.selectedStudentId; loadAnalysisData(AppState.selectedStudentId); } document.getElementById('analysis-student-select').addEventListener('change', (e) => { AppState.selectedStudentId = e.target.value; if (e.target.value) loadAnalysisData(e.target.value); else document.getElementById('analysis-content').innerHTML = '

请选择学生

'; }); document.getElementById('ai-analysis-btn').addEventListener('click', () => { if (AppState.selectedStudentId) generateAIAnalysis(AppState.selectedStudentId); else showToast('请先选择学生', 'error'); }); document.getElementById('view-grades-btn').addEventListener('click', () => { if (AppState.selectedStudentId) switchPageWithStudent('grades', AppState.selectedStudentId); else showToast('请先选择学生', 'error'); }); } function loadAnalysisData(studentId) { const student = AppState.allStudents.find(s => s.id == studentId); if (!student) return; const container = document.getElementById('analysis-content'); container.innerHTML = '加载中...'; Promise.all([ supabase.from('grades').select('*').eq('student_id', studentId), supabase.from('profile_records').select('*').eq('student_id', studentId).order('record_date', { ascending: false }), supabase.from('attendance').select('*').eq('student_id', studentId), supabase.from('ai_reports').select('*').eq('student_id', studentId).eq('type', 'analysis').maybeSingle() ]).then(([gradesRes, recordsRes, attendanceRes, aiReportRes]) => { const grades = gradesRes.data || []; const records = recordsRes.data || []; const aiReport = aiReportRes.data; let html = ''; const colors = ['#6d5dfc','#10b981','#f59e0b','#ef4444','#3b82f6','#ec4899','#8b5cf6','#06b6d4']; const color = student.avatar_color || colors[Math.floor(Math.random()*colors.length)]; html += `
${escapeHtml(student.name.charAt(0))}

${escapeHtml(student.name)}

学号: ${escapeHtml(student.student_no||'-')} | 性别: ${escapeHtml(student.gender||'-')} | 族别: ${escapeHtml(student.ethnicity||'-')} | 座位: ${student.seat||'-'} | 职务: ${escapeHtml(student.role||'-')}

综合等级: 待评估
`; if (grades.length > 0) { const subjects = ['语文','数学','英语','物理','化学','生物','政治','历史','地理']; const studentScores = subjects.map(sub => { const latest = grades.filter(g => g.subject === sub).sort((a,b) => b.id - a.id)[0]; return latest ? parseFloat(latest.score) : 0; }); html += `

能力雷达图

`; setTimeout(() => { const ctx = document.getElementById('analysis-radar'); if (ctx) { new Chart(ctx, { type: 'radar', data: { labels: subjects, datasets: [{ label: '学生得分', data: studentScores, borderColor: '#6d5dfc', backgroundColor: 'rgba(109,93,252,0.2)' }] }, options: { responsive: true, scales: { r: { beginAtZero: true, max: 100 } } } }); } }, 100); } if (records.length > 0) { html += '

成长记录时间轴

'; records.slice(0, 10).forEach(r => { const typeColor = r.type === 'praise' ? '#10b981' : r.type === 'discipline' ? '#ef4444' : '#3b82f6'; html += `
${escapeHtml(r.content)}${r.record_date}
`; }); html += '
'; } if (aiReport) { html += `

AI 分析报告

${escapeHtml(aiReport.content)}

`; } container.innerHTML = html; }); } window.showAnalysisChart = function(type, studentId) { const student = AppState.allStudents.find(s => s.id == studentId); if (!student) return; const modal = createModal( `${escapeHtml(student.name)} - 详细图表`, ``, ``, '700px' ); document.getElementById('detail-close').addEventListener('click', () => modal.remove()); if (type === 'radar') { supabase.from('grades').select('*').eq('student_id', studentId).then(({ data: grades }) => { const subjects = ['语文','数学','英语','物理','化学','生物','政治','历史','地理']; const scores = subjects.map(sub => { const latest = grades.filter(g => g.subject === sub).sort((a,b) => b.id - a.id)[0]; return latest ? parseFloat(latest.score) : 0; }); const ctx = modal.querySelector('#detail-chart').getContext('2d'); new Chart(ctx, { type: 'radar', data: { labels: subjects, datasets: [{ label: '得分', data: scores, borderColor: '#6d5dfc', backgroundColor: 'rgba(109,93,252,0.3)' }] }, options: { responsive: true, scales: { r: { beginAtZero: true, max: 100 } } } }); }); } }; async function generateAIAnalysis(studentId) { const student = AppState.allStudents.find(s => s.id == studentId); if (!student) return; showToast('正在生成AI分析,请稍候...', 'warning'); const payload = { name: student.name, gender: student.gender, role: student.role }; const content = await callAI('analysis', payload); if (content) { await supabase.from('ai_reports').upsert({ class_id: AppState.currentProfile.class_id, student_id: studentId, type: 'analysis', content }); loadAnalysisData(studentId); showToast('AI分析完成', 'success'); } } // ==================== 班级评价 ==================== function loadEvaluation() { const container = document.getElementById('page-evaluation'); container.innerHTML = `

暂无评价

`; document.getElementById('add-eval-btn').addEventListener('click', async () => { const student_id = document.getElementById('eval-student').value; const content = document.getElementById('eval-content').value.trim(); if (!student_id || !content) { showToast('请填写完整', 'error'); return; } await supabase.from('eval_records').insert({ class_id: AppState.currentProfile.class_id, student_id: parseInt(student_id), dimension: document.getElementById('eval-dimension').value, content, eval_date: new Date().toISOString().split('T')[0] }); document.getElementById('eval-content').value = ''; showToast('评价已添加', 'success'); loadEvalList(); }); loadEvalList(); } function loadEvalList() { supabase.from('eval_records').select('*, students(name)').eq('class_id', AppState.currentProfile.class_id).order('created_at', { ascending: false }).limit(20).then(({ data }) => { const container = document.getElementById('eval-list'); if (!data || data.length === 0) return; container.innerHTML = data.map(r => `
${escapeHtml(r.students?.name || '')}:${escapeHtml(r.content)}${r.eval_date || ''}
`).join(''); container.querySelectorAll('.eval-student-link').forEach(a => a.addEventListener('click', (e) => { e.preventDefault(); switchPageWithStudent('analysis', a.dataset.id); })); }); } // ==================== 家校沟通 ==================== function loadCommunication() { const container = document.getElementById('page-communication'); container.innerHTML = `

-

台账完整度

0

家访次数

0

家长会

0

群通知

家长台账

加载中...

家访记录

加载中...

家长会记录

加载中...

群通知

加载中...

`; if (!AppState.currentProfile) return; loadCommStats(); loadCommPreviews(); } function loadCommStats() { Promise.all([ supabase.from('parent_contacts').select('*', { count: 'exact', head: true }).eq('class_id', AppState.currentProfile.class_id), supabase.from('students').select('*', { count: 'exact', head: true }).eq('class_id', AppState.currentProfile.class_id), supabase.from('home_visits').select('*', { count: 'exact', head: true }).eq('class_id', AppState.currentProfile.class_id), supabase.from('parent_meetings').select('*', { count: 'exact', head: true }).eq('class_id', AppState.currentProfile.class_id), supabase.from('group_notices').select('*', { count: 'exact', head: true }).eq('class_id', AppState.currentProfile.class_id) ]).then(([contactRes, studentRes, visitRes, meetingRes, noticeRes]) => { const studentCount = studentRes.count || 0; const contactCount = contactRes.count || 0; document.getElementById('comm-contact-rate').textContent = studentCount > 0 ? Math.round((contactCount / studentCount) * 100) + '%' : '-'; document.getElementById('comm-visit-count').textContent = visitRes.count || 0; document.getElementById('comm-meeting-count').textContent = meetingRes.count || 0; document.getElementById('comm-notice-count').textContent = noticeRes.count || 0; }); } function loadCommPreviews() { supabase.from('parent_contacts').select('*, students(name)').eq('class_id', AppState.currentProfile.class_id).limit(3).then(({ data }) => { document.getElementById('comm-contacts-preview').textContent = data && data.length > 0 ? data.map(c => c.students?.name).join('、') + ' 等' : '暂无记录'; }); supabase.from('home_visits').select('*, students(name)').eq('class_id', AppState.currentProfile.class_id).limit(3).then(({ data }) => { document.getElementById('comm-visits-preview').textContent = data && data.length > 0 ? data.map(v => v.students?.name).join('、') + ' 等' : '暂无记录'; }); supabase.from('parent_meetings').select('*').eq('class_id', AppState.currentProfile.class_id).limit(3).then(({ data }) => { document.getElementById('comm-meetings-preview').textContent = data && data.length > 0 ? data.map(m => m.theme).join('、') : '暂无记录'; }); supabase.from('group_notices').select('*').eq('class_id', AppState.currentProfile.class_id).limit(3).then(({ data }) => { document.getElementById('comm-group-notices-preview').textContent = data && data.length > 0 ? data.map(n => n.content).join('、') : '暂无记录'; }); } window.openCommDetail = function(type) { const modal = createModal( type === 'contacts' ? '家长台账' : type === 'visits' ? '家访记录' : type === 'meetings' ? '家长会记录' : '群通知', `
加载中...
`, ``, '750px' ); document.getElementById('comm-close').addEventListener('click', () => modal.remove()); const content = modal.querySelector('#comm-detail-content'); if (type === 'contacts') loadContactsDetail(content); else if (type === 'visits') loadVisitsDetail(content); else if (type === 'meetings') loadMeetingsDetail(content); else if (type === 'notices') loadGroupNoticesDetail(content); }; function loadContactsDetail(container) { supabase.from('parent_contacts').select('*, students(name)').eq('class_id', AppState.currentProfile.class_id).then(({ data }) => { container.innerHTML = `
${data.map(c => ``).join('')}
学生家长关系电话操作
${escapeHtml(c.students?.name || '')}${escapeHtml(c.parent_name || '-')}${escapeHtml(c.relation || '-')}${escapeHtml(c.phone || '-')}
`; container.querySelector('#add-contact-btn').addEventListener('click', openContactModal); container.querySelectorAll('.delete-contact').forEach(btn => btn.addEventListener('click', async () => { await supabase.from('parent_contacts').delete().eq('id', btn.dataset.id); loadContactsDetail(container); })); container.querySelectorAll('.contact-student-link').forEach(a => a.addEventListener('click', (e) => { e.preventDefault(); switchPageWithStudent('analysis', a.dataset.id); })); }); } function loadVisitsDetail(container) { supabase.from('home_visits').select('*, students(name)').eq('class_id', AppState.currentProfile.class_id).order('visit_date', { ascending: false }).then(({ data }) => { container.innerHTML = `
${data.map(v => ``).join('')}
学生日期内容状态
${escapeHtml(v.students?.name || '')}${v.visit_date || '-'}${escapeHtml(v.content || '-')}${v.status === 'completed' ? '已完成' : '待家访'}
`; container.querySelector('#add-visit-btn').addEventListener('click', () => openVisitModal(() => loadVisitsDetail(container))); container.querySelectorAll('.visit-student-link').forEach(a => a.addEventListener('click', (e) => { e.preventDefault(); switchPageWithStudent('analysis', a.dataset.id); })); }); } function loadMeetingsDetail(container) { supabase.from('parent_meetings').select('*').eq('class_id', AppState.currentProfile.class_id).then(({ data }) => { container.innerHTML = `
${data.map(m => ``).join('')}
主题日期到会率备注
${escapeHtml(m.theme)}${m.meeting_date || '-'}${m.attendance_rate ? m.attendance_rate + '%' : '-'}${escapeHtml(m.notes || '-')}
`; container.querySelector('#add-meeting-btn').addEventListener('click', () => openMeetingModal(() => loadMeetingsDetail(container))); }); } function loadGroupNoticesDetail(container) { supabase.from('group_notices').select('*').eq('class_id', AppState.currentProfile.class_id).order('notice_date', { ascending: false }).then(({ data }) => { container.innerHTML = `
${data.map(n => ``).join('')}
内容日期重要
${escapeHtml(n.content)}${n.notice_date || '-'}${n.is_important ? '重要' : ''}
`; container.querySelector('#add-group-notice-btn').addEventListener('click', () => openGroupNoticeModal(() => loadGroupNoticesDetail(container))); }); } function openContactModal() { const modal = createModal( '添加家长联系人', `
`, ``, '500px' ); document.getElementById('contact-cancel').addEventListener('click', () => modal.remove()); document.getElementById('contact-save').addEventListener('click', async () => { const student_id = modal.querySelector('#contact-student').value; if (!student_id) { showToast('请选择学生', 'error'); return; } await supabase.from('parent_contacts').insert({ class_id: AppState.currentProfile.class_id, student_id: parseInt(student_id), parent_name: modal.querySelector('#contact-name').value.trim() || null, relation: modal.querySelector('#contact-relation').value.trim() || null, phone: modal.querySelector('#contact-phone').value.trim() || null }); modal.remove(); showToast('联系人已添加', 'success'); }); } function openVisitModal(callback) { const modal = createModal( '新增家访', `
`, ``, '500px' ); document.getElementById('visit-cancel').addEventListener('click', () => modal.remove()); document.getElementById('visit-save').addEventListener('click', async () => { const student_id = modal.querySelector('#visit-student').value; if (!student_id) { showToast('请选择学生', 'error'); return; } await supabase.from('home_visits').insert({ class_id: AppState.currentProfile.class_id, student_id: parseInt(student_id), visit_date: modal.querySelector('#visit-date').value || null, content: modal.querySelector('#visit-content').value.trim() || null, status: modal.querySelector('#visit-status').value }); modal.remove(); showToast('家访记录已添加', 'success'); if (callback) callback(); }); } function openMeetingModal(callback) { const modal = createModal( '新增家长会', `
`, ``, '500px' ); document.getElementById('meeting-cancel').addEventListener('click', () => modal.remove()); document.getElementById('meeting-save').addEventListener('click', async () => { const theme = modal.querySelector('#meeting-theme').value.trim(); if (!theme) { showToast('请输入主题', 'error'); return; } await supabase.from('parent_meetings').insert({ class_id: AppState.currentProfile.class_id, theme, meeting_date: modal.querySelector('#meeting-date').value || null, attendance_rate: modal.querySelector('#meeting-rate').value ? parseFloat(modal.querySelector('#meeting-rate').value) : null, notes: modal.querySelector('#meeting-notes').value.trim() || null }); modal.remove(); showToast('家长会记录已添加', 'success'); if (callback) callback(); }); } function openGroupNoticeModal(callback) { const modal = createModal( '发布群通知', `
`, ``, '500px' ); document.getElementById('notice-cancel').addEventListener('click', () => modal.remove()); document.getElementById('notice-save').addEventListener('click', async () => { const content = modal.querySelector('#group-notice-content').value.trim(); if (!content) { showToast('请输入内容', 'error'); return; } await supabase.from('group_notices').insert({ class_id: AppState.currentProfile.class_id, content, notice_date: modal.querySelector('#group-notice-date').value || null, is_important: modal.querySelector('#group-notice-important').checked }); modal.remove(); showToast('群通知已发布', 'success'); if (callback) callback(); }); } // ==================== 通知公告 ==================== function loadNotices() { const container = document.getElementById('page-notices'); container.innerHTML = `

暂无通知

`; document.getElementById('add-notice-btn').addEventListener('click', async () => { const title = document.getElementById('notice-title').value.trim(); const content = document.getElementById('notice-content').value.trim(); if (!title) { showToast('请输入标题', 'error'); return; } await supabase.from('notices').insert({ class_id: AppState.currentProfile.class_id, title, content: content || null, is_pinned: document.getElementById('notice-pinned').checked }); // 同步到通知中心 await supabase.from('notifications').insert({ class_id: AppState.currentProfile.class_id, user_id: AppState.currentUser.id, title, content: content || '', type: 'info' }); document.getElementById('notice-title').value = ''; document.getElementById('notice-content').value = ''; document.getElementById('notice-pinned').checked = false; showToast('通知已发布', 'success'); loadNoticesList(); }); loadNoticesList(); } function loadNoticesList() { supabase.from('notices').select('*').eq('class_id', AppState.currentProfile.class_id).order('is_pinned', { ascending: false }).order('created_at', { ascending: false }).then(({ data }) => { const container = document.getElementById('notices-list'); if (!data || data.length === 0) return; container.innerHTML = data.map(n => `
${escapeHtml(n.title)}${n.is_pinned ? ' 置顶' : ''}

${escapeHtml(n.content || '')}

${new Date(n.created_at).toLocaleString('zh-CN')}
`).join(''); container.querySelectorAll('.notice-item').forEach(item => item.addEventListener('click', () => openNoticeDetail(item.dataset.id))); }); } function openNoticeDetail(noticeId) { supabase.from('notices').select('*').eq('id', noticeId).single().then(({ data }) => { if (!data) return; const modal = createModal( escapeHtml(data.title), `

${escapeHtml(data.content || '无内容')}

发布时间:${new Date(data.created_at).toLocaleString('zh-CN')} ${data.is_pinned ? '| 置顶' : ''}

`, ``, '600px' ); document.getElementById('notice-close').addEventListener('click', () => modal.remove()); document.getElementById('notice-delete').addEventListener('click', async () => { await supabase.from('notices').delete().eq('id', noticeId); // 同步删除通知中心对应记录 await supabase.from('notifications').delete().eq('title', data.title); modal.remove(); showToast('公告已删除', 'success'); loadNoticesList(); }); }); } // ==================== 班级朋友圈(卡片化+删除) ==================== let currentPostCategory = ''; function loadMoments() { const container = document.getElementById('page-moments'); container.innerHTML = `
`; if (!AppState.currentProfile) return; document.getElementById('publish-post-btn').addEventListener('click', publishPost); document.querySelectorAll('#post-category-filter button').forEach(btn => btn.addEventListener('click', () => { currentPostCategory = btn.dataset.category; loadMomentsList(); })); loadMomentsList(); } function loadMomentsList() { let query = supabase.from('posts').select('*').eq('class_id', AppState.currentProfile.class_id).order('created_at', { ascending: false }).limit(50); if (currentPostCategory) query = query.eq('category', currentPostCategory); query.then(async ({ data: posts }) => { const container = document.getElementById('moments-list'); if (!posts || posts.length === 0) { container.innerHTML = '

暂无动态

'; return; } let html = ''; for (const post of posts) { const { data: media } = await supabase.from('post_media').select('*').eq('post_id', post.id); const firstImage = media && media.length > 0 ? media[0].media_url : ''; const imageCount = media ? media.length : 0; html += `
${firstImage ? `图片` : ''}
${escapeHtml(post.content || '')}
${new Date(post.created_at).toLocaleString('zh-CN')}${imageCount > 0 ? ` | 📷${imageCount}` : ''}
`; } container.innerHTML = html; container.querySelectorAll('.moment-card').forEach(card => card.addEventListener('click', () => openPostDetail(card.dataset.postId))); }); } function openPostDetail(postId) { Promise.all([ supabase.from('posts').select('*').eq('id', postId).single(), supabase.from('post_media').select('*').eq('post_id', postId) ]).then(([postRes, mediaRes]) => { const post = postRes.data; if (!post) return; const media = mediaRes.data || []; const modal = createModal( '动态详情', `

${escapeHtml(post.content || '')}

${media.map(m => ``).join('')}

${new Date(post.created_at).toLocaleString('zh-CN')} | 分类:${post.category || '-'}

`, ``, '700px' ); document.getElementById('post-close').addEventListener('click', () => modal.remove()); document.getElementById('post-delete').addEventListener('click', async () => { if (confirm('确定删除该动态?')) { await supabase.from('posts').delete().eq('id', postId); modal.remove(); showToast('动态已删除', 'success'); loadMomentsList(); } }); modal.querySelectorAll('.post-media-grid img').forEach(img => img.addEventListener('click', () => window.open(img.src))); }); } function publishPost() { const content = document.getElementById('post-content').value.trim(); const category = document.getElementById('post-category').value; const imagesInput = document.getElementById('post-images-input'); if (!content && imagesInput.files.length === 0) { showToast('请输入内容或选择图片', 'error'); return; } supabase.from('posts').insert({ class_id: AppState.currentProfile.class_id, teacher_id: AppState.currentUser.id, content: content || null, category }).select().single().then(async ({ data: post, error }) => { if (error) { showToast('发布失败', 'error'); return; } if (imagesInput.files.length > 0) { for (const file of imagesInput.files) { const compressed = await compressImage(file, 1200, 0.7); const fileName = `${post.id}_${Date.now()}_${Math.random().toString(36).substring(2,6)}.jpg`; await supabase.storage.from('post-images').upload(fileName, compressed); const { data: urlData } = supabase.storage.from('post-images').getPublicUrl(fileName); await supabase.from('post_media').insert({ post_id: post.id, media_url: urlData.publicUrl }); } } document.getElementById('post-content').value = ''; imagesInput.value = ''; showToast('发布成功', 'success'); loadMomentsList(); }); } // ==================== 数据洞察(数据中心) ==================== function loadInsights() { const container = document.getElementById('page-insights'); container.innerHTML = `

0

学生总数

-

最近均分

-

本月出勤率

0

本月请假人次

数据管理

导入学生
导入成绩
导入通讯录
导入课表
导出学生花名册
导出全部数据
导入备份
清空所有数据

AI 大模型配置

账户管理

`; if (!AppState.currentProfile) return; loadInsightsData(); // 加载AI配置 loadAIConfig(); // 数据管理入口事件 container.querySelectorAll('.quick-action[data-action]').forEach(el => { el.addEventListener('click', () => { const action = el.dataset.action; if (action === 'import-students') switchPage('students'); if (action === 'import-grades') switchPage('grades'); if (action === 'import-contacts') showToast('通讯录导入功能即将上线', 'warning'); if (action === 'import-schedule') switchPage('schedule'); if (action === 'export-students') exportStudents(AppState.allStudents); if (action === 'export-all') exportAllData(); if (action === 'import-backup') importBackup(); if (action === 'clear-all') clearAllData(); }); }); // AI配置事件 document.getElementById('save-ai-config-btn').addEventListener('click', saveAIConfig); document.getElementById('load-ai-config-btn').addEventListener('click', loadAIConfig); document.getElementById('test-ai-btn').addEventListener('click', testAIConnection); // 账户注销事件 document.getElementById('delete-account-btn').addEventListener('click', deleteAccount); } function loadInsightsData() { supabase.from('students').select('*', { count: 'exact', head: true }).eq('class_id', AppState.currentProfile.class_id).then(({ count }) => { document.getElementById('insight-total-students').textContent = count || 0; }); supabase.from('exams').select('*').eq('class_id', AppState.currentProfile.class_id).order('exam_date', { ascending: true }).then(async ({ data: exams }) => { if (exams && exams.length > 0) { const labels = exams.map(e => e.exam_name); const avgScores = []; for (const exam of exams) { const { data: grades } = await supabase.from('grades').select('score').eq('exam_id', exam.id); avgScores.push(grades.length > 0 ? grades.reduce((sum, g) => sum + parseFloat(g.score), 0) / grades.length : null); } const latestAvg = avgScores[avgScores.length - 1]; if (latestAvg) document.getElementById('insight-avg-score').textContent = latestAvg.toFixed(1); } }); const monthStart = new Date(); monthStart.setDate(1); supabase.from('attendance').select('status').eq('class_id', AppState.currentProfile.class_id).gte('date', monthStart.toISOString().split('T')[0]).then(({ data }) => { if (data && data.length > 0) { const present = data.filter(a => a.status === 'present' || a.status === 'late').length; document.getElementById('insight-attendance-rate').textContent = Math.round((present / data.length) * 100) + '%'; } }); supabase.from('leaves').select('*', { count: 'exact', head: true }).eq('class_id', AppState.currentProfile.class_id).gte('date', monthStart.toISOString().split('T')[0]).then(({ count }) => { document.getElementById('insight-leave-total').textContent = count || 0; }); } async function loadAIConfig() { const { data, error } = await supabase.from('settings').select('ai_model, ai_api_key').eq('class_id', AppState.currentProfile.class_id).maybeSingle(); if (error) { console.error('加载AI配置失败:', error); return; } if (data) { AppState.aiConfig.model = data.ai_model || ''; AppState.aiConfig.apiKey = data.ai_api_key || ''; document.getElementById('ai-model-input').value = AppState.aiConfig.model; document.getElementById('ai-api-key-input').value = AppState.aiConfig.apiKey; } } async function saveAIConfig() { const model = document.getElementById('ai-model-input').value.trim(); const apiKey = document.getElementById('ai-api-key-input').value.trim(); if (!model || !apiKey) { showToast('请填写模型名称和API Key', 'error'); return; } await supabase.from('settings').upsert({ class_id: AppState.currentProfile.class_id, ai_model: model, ai_api_key: apiKey }, { onConflict: 'class_id' }); AppState.aiConfig.model = model; AppState.aiConfig.apiKey = apiKey; showToast('AI配置已保存', 'success'); } async function testAIConnection() { // 使用保存的配置测试(通过本地配置直接调用) const model = AppState.aiConfig.model; const apiKey = AppState.aiConfig.apiKey; if (!model || !apiKey) { showToast('请先保存AI配置', 'error'); return; } showToast('正在测试连接...', 'warning'); // 模拟调用(实际项目中应通过后端转发) const result = await callAI('daily_quote', {}); if (result) { showToast('AI连接成功!', 'success'); } else { showToast('AI连接失败,请检查模型名称和API Key', 'error'); } } function exportAllData() { // 导出所有表数据为 JSON const tables = ['classes','profiles','students','attendance','homework','homework_submissions','profile_records','leaves','activity_logs','point_records','semesters','exams','grades','schedule','parent_contacts','home_visits','parent_meetings','group_notices','notices','eval_records','posts','post_media','notifications','tasks','subject_reps','ai_reports','settings']; Promise.all(tables.map(t => supabase.from(t).select('*').eq('class_id', AppState.currentProfile.class_id))).then(results => { const exportData = {}; tables.forEach((t, i) => { exportData[t] = results[i].data || []; }); const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `workbuddy_backup_${new Date().toISOString().split('T')[0]}.json`; a.click(); URL.revokeObjectURL(url); showToast('备份导出成功', 'success'); }); } function importBackup() { const input = document.createElement('input'); input.type = 'file'; input.accept = '.json'; input.addEventListener('change', async (e) => { const file = e.target.files[0]; if (!file) return; const text = await file.text(); const data = JSON.parse(text); // 恢复数据(简化处理) for (const table in data) { if (data[table].length > 0) { await supabase.from(table).upsert(data[table], { onConflict: 'id' }); } } showToast('备份导入成功', 'success'); refreshModule('dashboard'); }); input.click(); } function clearAllData() { const modal = createModal( '清空所有数据', `

⚠️ 此操作将删除本班级所有数据,包括学生、考勤、作业、成绩等,不可恢复!

请输入您的登录密码以确认:

确认倒计时 5

`, ``, '500px' ); let countdown = 5; const interval = setInterval(() => { countdown--; document.getElementById('clear-all-countdown').textContent = countdown; if (countdown <= 0) { clearInterval(interval); document.getElementById('clear-all-confirm').disabled = false; } }, 1000); document.getElementById('clear-all-cancel').addEventListener('click', () => { clearInterval(interval); modal.remove(); }); document.getElementById('clear-all-confirm').addEventListener('click', async () => { const password = modal.querySelector('#clear-all-password').value; if (!password) { showToast('请输入密码', 'error'); return; } const email = `${AppState.currentProfile.username}@workbuddy.local`; const { error } = await supabase.auth.signInWithPassword({ email, password }); if (error) { showToast('密码错误,操作取消', 'error'); return; } // 删除所有班级相关数据 const tables = ['students','attendance','homework','homework_submissions','profile_records','leaves','activity_logs','point_records','semesters','exams','grades','schedule','parent_contacts','home_visits','parent_meetings','group_notices','notices','eval_records','posts','post_media','notifications','tasks','subject_reps','ai_reports']; for (const t of tables) { await supabase.from(t).delete().eq('class_id', AppState.currentProfile.class_id); } clearInterval(interval); modal.remove(); showToast('已清空所有数据', 'success'); refreshModule('dashboard'); }); } function deleteAccount() { const modal = createModal( '注销当前账户', `

⚠️ 注销后您的账户将被删除,所有数据将无法恢复!

请输入您的登录密码以确认:

`, ``, '500px' ); document.getElementById('delete-cancel').addEventListener('click', () => modal.remove()); document.getElementById('delete-confirm').addEventListener('click', async () => { const password = modal.querySelector('#delete-account-password').value; if (!password) { showToast('请输入密码', 'error'); return; } const email = `${AppState.currentProfile.username}@workbuddy.local`; const { error } = await supabase.auth.signInWithPassword({ email, password }); if (error) { showToast('密码错误,操作取消', 'error'); return; } // 通过 Cloudflare Function 删除用户(需要后端支持) try { const response = await fetch('/api/delete-user', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ userId: AppState.currentUser.id }) }); if (!response.ok) { const err = await response.json(); throw new Error(err.error || '删除失败'); } showToast('账户已注销', 'success'); await supabase.auth.signOut(); AppState.currentUser = null; AppState.currentProfile = null; showAuth(); } catch (err) { showToast('注销失败:' + err.message, 'error'); } }); } // ==================== 通知中心 ==================== function loadNotifications() { const container = document.getElementById('page-notifications'); container.innerHTML = `

暂无通知

`; document.getElementById('mark-all-read-btn').addEventListener('click', async () => { await supabase.from('notifications').update({ is_read: true }).eq('class_id', AppState.currentProfile.class_id); showToast('已全部标记为已读', 'success'); loadNotificationsList(); }); loadNotificationsList(); } function loadNotificationsList() { supabase.from('notifications').select('*').eq('class_id', AppState.currentProfile.class_id).order('created_at', { ascending: false }).limit(20).then(({ data }) => { const container = document.getElementById('notifications-list'); if (!data || data.length === 0) return; container.innerHTML = data.map(n => `
${escapeHtml(n.title)}

${escapeHtml(n.content || '')}

${new Date(n.created_at).toLocaleString('zh-CN')}
`).join(''); }); } // ==================== 任务协作 ==================== function loadTasks() { const container = document.getElementById('page-tasks'); container.innerHTML = `

暂无任务

`; supabase.from('profiles').select('id, realname, username').eq('class_id', AppState.currentProfile.class_id).then(({ data: users }) => { const select = document.getElementById('task-assignees'); select.innerHTML = (users || []).map(u => ``).join(''); }); document.getElementById('add-task-btn').addEventListener('click', async () => { const title = document.getElementById('task-title').value.trim(); const description = document.getElementById('task-description').value.trim(); const deadline = document.getElementById('task-deadline').value; const assigneeIds = Array.from(document.getElementById('task-assignees').selectedOptions).map(o => o.value); if (!title || assigneeIds.length === 0) { showToast('请输入标题并至少选择一名负责人', 'error'); return; } // 创建任务 const { data: task, error } = await supabase.from('tasks').insert({ class_id: AppState.currentProfile.class_id, assigner_id: AppState.currentUser.id, title, description, deadline: deadline || null }).select().single(); if (error) { showToast('创建失败:' + error.message, 'error'); return; } // 插入关联表 for (const uid of assigneeIds) { await supabase.from('task_assignees').insert({ task_id: task.id, assignee_id: uid }); } document.getElementById('task-title').value = ''; document.getElementById('task-description').value = ''; document.getElementById('task-deadline').value = ''; showToast('任务已创建', 'success'); loadTasksList(); }); loadTasksList(); } function loadTasksList() { supabase.from('tasks').select('*, task_assignees(assignee_id)').eq('class_id', AppState.currentProfile.class_id).order('created_at', { ascending: false }).then(async ({ data: tasks }) => { const container = document.getElementById('tasks-list'); if (!tasks || tasks.length === 0) return; let html = ''; for (const t of tasks) { // 获取负责人姓名 const assigneeIds = t.task_assignees ? t.task_assignees.map(ta => ta.assignee_id) : []; let assigneeNames = ''; if (assigneeIds.length > 0) { const { data: users } = await supabase.from('profiles').select('realname, username').in('id', assigneeIds); assigneeNames = users ? users.map(u => u.realname || u.username).join('、') : ''; } const statusLabel = t.status === 'pending' ? '待处理' : t.status === 'in_progress' ? '进行中' : '已完成'; html += `
${escapeHtml(t.title)} ${statusLabel}

负责人: ${assigneeNames || '-'} | 截止: ${t.deadline || '-'}

${t.description ? `

${escapeHtml(t.description)}

` : ''}
${t.status !== 'completed' ? `` : ''}
`; } container.innerHTML = html; container.querySelectorAll('.complete-task').forEach(btn => btn.addEventListener('click', async () => { await supabase.from('tasks').update({ status: 'completed' }).eq('id', btn.dataset.id); loadTasksList(); })); container.querySelectorAll('.delete-task').forEach(btn => btn.addEventListener('click', async () => { if (confirm('确定删除该任务?')) { await supabase.from('tasks').delete().eq('id', btn.dataset.id); loadTasksList(); } })); }); } // ==================== 用户管理 ==================== function loadUsers() { if (!AppState.currentProfile || AppState.currentProfile.role !== 'admin') return; const container = document.getElementById('page-users'); container.innerHTML = `

加载中...

`; supabase.from('profiles').select('*').order('created_at', { ascending: true }).then(({ data }) => { const list = document.getElementById('users-list'); if (!data || data.length === 0) { list.innerHTML = '

暂无用户

'; return; } list.innerHTML = data.map(u => `
${escapeHtml(u.username)} ${escapeHtml(u.realname || '')} ${u.is_active ? '启用' : '禁用'}
`).join(''); list.querySelectorAll('.role-select').forEach(sel => sel.addEventListener('change', async (e) => { await supabase.from('profiles').update({ role: e.target.value }).eq('id', e.target.dataset.id); showToast('角色已更新', 'success'); })); list.querySelectorAll('.toggle-user').forEach(btn => btn.addEventListener('click', async (e) => { if (e.currentTarget.dataset.id === AppState.currentUser.id) { showToast('不能禁用自己', 'error'); return; } await supabase.from('profiles').update({ is_active: !(e.currentTarget.dataset.active === 'true') }).eq('id', e.currentTarget.dataset.id); showToast('状态已更新', 'success'); loadUsers(); })); }); } // ==================== 个人中心 ==================== function openUserProfile() { const modal = createModal( '个人中心', `
${(AppState.currentProfile.realname || AppState.currentProfile.username).charAt(0).toUpperCase()}

${escapeHtml(AppState.currentProfile.realname || AppState.currentProfile.username)}

${getRoleLabel(AppState.currentProfile.role)} | 注册于 ${new Date(AppState.currentProfile.created_at).toLocaleDateString('zh-CN')}

修改密码

更换头像

更换背景图

`, ``, '600px' ); document.getElementById('profile-close').addEventListener('click', () => modal.remove()); document.getElementById('profile-save').addEventListener('click', async () => { const realname = modal.querySelector('#profile-realname').value.trim(); await supabase.from('profiles').update({ realname }).eq('id', AppState.currentUser.id); AppState.currentProfile.realname = realname; showToast('资料已更新', 'success'); showMain(); }); document.getElementById('change-password-btn').addEventListener('click', async () => { const oldPwd = modal.querySelector('#old-password').value; const newPwd = modal.querySelector('#new-password').value; const confirmPwd = modal.querySelector('#confirm-new-password').value; if (!oldPwd || !newPwd || !confirmPwd) { showToast('请填写完整', 'error'); return; } if (newPwd !== confirmPwd) { showToast('两次新密码不一致', 'error'); return; } const email = `${AppState.currentProfile.username}@workbuddy.local`; const { error } = await supabase.auth.signInWithPassword({ email, password: oldPwd }); if (error) { showToast('旧密码错误', 'error'); return; } const { error: updateError } = await supabase.auth.updateUser({ password: newPwd }); if (updateError) { showToast('修改失败:' + updateError.message, 'error'); return; } showToast('密码修改成功', 'success'); }); document.getElementById('upload-avatar-btn').addEventListener('click', async () => { const file = modal.querySelector('#avatar-file-input').files[0]; if (!file) { showToast('请选择图片', 'error'); return; } const compressed = await compressImage(file, 200, 0.8); const fileName = `${AppState.currentUser.id}_avatar.jpg`; await supabase.storage.from('header-images').upload(fileName, compressed, { upsert: true }); const { data: urlData } = supabase.storage.from('header-images').getPublicUrl(fileName); await supabase.from('profiles').update({ avatar_url: urlData.publicUrl }).eq('id', AppState.currentUser.id); AppState.currentProfile.avatar_url = urlData.publicUrl; showToast('头像已更新', 'success'); }); document.getElementById('upload-bg-btn').addEventListener('click', async () => { const file = modal.querySelector('#bg-file-input').files[0]; if (!file) { showToast('请选择图片', 'error'); return; } const compressed = await compressImage(file, 1600, 0.8); const fileName = `${AppState.currentUser.id}_bg.jpg`; await supabase.storage.from('header-images').upload(fileName, compressed, { upsert: true }); const { data: urlData } = supabase.storage.from('header-images').getPublicUrl(fileName); await supabase.from('profiles').update({ header_bg_url: urlData.publicUrl }).eq('id', AppState.currentUser.id); AppState.currentProfile.header_bg_url = urlData.publicUrl; applyBannerBackground(); showToast('背景图已更新', 'success'); }); } // ==================== 初始化 ==================== document.addEventListener('DOMContentLoaded', async () => { const { data: { user } } = await supabase.auth.getUser(); if (user) { AppState.currentUser = user; const profile = await fetchProfile(); if (profile && profile.is_active) { AppState.currentProfile = profile; renderNavigation(); renderPageContainers(); showMain(); switchPage('dashboard'); if (profile.role === 'admin') loadUsers(); applyBannerBackground(); loadNotificationBadge(); supabase.from('students').select('*').eq('class_id', AppState.currentProfile.class_id).then(({ data }) => { AppState.allStudents = data || []; }); supabase.from('settings').select('*').eq('class_id', AppState.currentProfile.class_id).maybeSingle().then(({ data }) => { if (data) { AppState.schoolStartDate = data.school_start_date || '2026-09-01'; AppState.periodTimes = data.period_times; AppState.aiConfig.model = data.ai_model || ''; AppState.aiConfig.apiKey = data.ai_api_key || ''; } }); } else { await supabase.auth.signOut(); showAuth(); } } else { showAuth(); } document.getElementById('login-tab').addEventListener('click', () => setAuthMode('login')); document.getElementById('signup-tab').addEventListener('click', () => setAuthMode('signup')); document.getElementById('auth-form').addEventListener('submit', handleAuth); document.getElementById('logout-btn').addEventListener('click', logout); document.getElementById('banner-upload-btn').addEventListener('click', () => document.getElementById('bg-upload-input').click()); document.getElementById('bg-upload-input').addEventListener('change', handleBannerUpload); document.getElementById('logo-home').addEventListener('click', () => switchPage('dashboard')); document.getElementById('user-profile-link').addEventListener('click', openUserProfile); }); async function loadNotificationBadge() { const { data } = await supabase.from('notifications').select('id').eq('class_id', AppState.currentProfile.class_id).eq('is_read', false); const unread = data ? data.length : 0; const badge = document.getElementById('notification-badge'); if (unread > 0) { badge.style.display = 'flex'; badge.textContent = unread; } else { badge.style.display = 'none'; } }