// ==================== 全局配置 ====================
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 = `
${title}
${bodyHtml}
${actionsHtml}
`;
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 = `${group.title}
`;
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 = `
考勤打卡
布置作业
随机点名
成绩录入
发动态
数据洞察
`;
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(
'班干部管理',
`
加载中...
选择学生 ${AppState.allStudents.map(s => `${escapeHtml(s.name)} `).join('')}
选择职务 ${['班长','副班长','学习委员','纪律委员','劳动委员','体育委员','文艺委员','生活委员','宣传委员','组织委员'].map(r => `${r} `).join('')}
添加
`,
`关闭 `,
'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(
'科代表管理',
`
加载中...
选择学生 ${AppState.allStudents.map(s => `${escapeHtml(s.name)} `).join('')}
选择科目 ${['语文','数学','英语','物理','化学','生物','政治','历史','地理'].map(s => `${s} `).join('')}
添加
`,
`关闭 `,
'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 = `
头像 学号 姓名 性别 族别 座位 职务 家长姓名 家长电话 住址 操作
`;
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 => `${escapeHtml(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 = `
`;
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 = `全部 (${AppState.homeworkList.length})
`;
subjects.forEach(sub => {
const count = AppState.homeworkList.filter(h => h.subject === sub).length;
subjectFilter.innerHTML += `${escapeHtml(sub)} (${count})
`;
});
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 `${escapeHtml(stu.name)}
已交
迟交
未交
`;
}).join('')}
保存提交状态
`;
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 => `${escapeHtml(h.subject)} ${escapeHtml(h.title)} ${h.assigned_date || '-'} ${h.deadline || '-'} `).join('')}
`;
} 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 => `${escapeHtml(h.subject)} ${escapeHtml(h.title)} ${h.assigned_date || '-'} `).join('')}
`;
} 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 += `${escapeHtml(sm.name)} ${escapeHtml(sm.subjects.join('、'))} ${escapeHtml(sm.subjects.join('、'))} ${sm.count} `;
});
container.innerHTML += '
';
});
}
}
// ==================== 成绩管理 ====================
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 => `${escapeHtml(sem.name)} `).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 => `${escapeHtml(s.name)} `).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(
'成绩录入与查看',
`
统计图表
保存成绩
`,
`关闭 `,
'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 += `${sub} ${avg} ${passRate}% ${excellentRate}% `;
}
});
html += '
';
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 => `${escapeHtml(e.exam_name)} `).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(
'课程时段设置',
``,
`取消 保存 `,
'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 = `
`;
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(
'新增请假',
`
选择学生 ${AppState.attendanceStudents.map(s => `${escapeHtml(s.name)} `).join('')}
`,
`取消 保存 `,
'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 => `${escapeHtml(a.students?.name || '')} ${statusMap[a.status]} `).join('')}
`;
});
} 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 => `${escapeHtml(s.name)} ${s.count} ${s.dates.join(', ')} `).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 => `${escapeHtml(l.students?.name || '')} ${l.date} ${escapeHtml(l.reason || '-')} ${l.status === 'approved' ? '已批准' : l.status === 'rejected' ? '已拒绝' : '待审批'} `).join('')}
`;
});
}
}
// ==================== 期末评语 ====================
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 = `
请选择学生 ${AppState.allStudents.map(s => `${escapeHtml(s.name)} `).join('')}
AI深度分析
查看成绩
`;
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 = `
选择学生 ${AppState.allStudents.map(s => `${escapeHtml(s.name)} `).join('')}
课堂表现 作业情况 行为习惯 活动参与
添加
`;
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 => ``).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 = `
`;
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 = ` 添加 `;
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 = ` 新增 `;
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 => `${escapeHtml(m.theme)} ${m.meeting_date || '-'} ${m.attendance_rate ? m.attendance_rate + '%' : '-'} ${escapeHtml(m.notes || '-')} `).join('')}
`;
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 => `${escapeHtml(n.content)} ${n.notice_date || '-'} ${n.is_important ? '重要 ' : ''} `).join('')}
`;
container.querySelector('#add-group-notice-btn').addEventListener('click', () => openGroupNoticeModal(() => loadGroupNoticesDetail(container)));
});
}
function openContactModal() {
const modal = createModal(
'添加家长联系人',
`
选择学生 ${AppState.allStudents.map(s => `${escapeHtml(s.name)} `).join('')}
`,
`取消 保存 `,
'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(
'新增家访',
`
选择学生 ${AppState.allStudents.map(s => `${escapeHtml(s.name)} `).join('')}
待家访 已完成
`,
`取消 保存 `,
'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 = `
导入学生
导入成绩
导入通讯录
导入课表
导出学生花名册
导出全部数据
导入备份
清空所有数据
`;
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 => `${escapeHtml(u.realname || u.username)} `).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 ? '启用' : '禁用'}
管理员 班主任 科任教师 ${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'; }
}
暂无评语,请先生成