Update CLASS: AICODE03课程内容更新,学生谢善诺班级调整

This commit is contained in:
qiuyan
2026-06-09 22:26:25 +08:00
parent cd05b8a5a7
commit 34226cd922
79 changed files with 14293 additions and 7 deletions

View File

@@ -490,6 +490,180 @@
/* 滚动条美化 */
.tag-sidebar::-webkit-scrollbar { width: 4px; }
.tag-sidebar::-webkit-scrollbar-thumb { background: #ddd; border-radius: 2px; }
/* ==================== 出勤统计表 ==================== */
.attendance-section {
background: var(--card);
border-radius: 14px;
padding: 16px;
margin-top: 16px;
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
display: none;
}
.attendance-section.show { display: block; }
.attendance-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
flex-wrap: wrap;
gap: 8px;
}
.attendance-title { font-size: 16px; font-weight: 800; color: var(--primary); }
.attendance-subtitle { font-size: 12px; color: var(--muted); }
.attendance-summary {
display: flex;
gap: 16px;
font-size: 13px;
color: var(--muted);
}
.attendance-summary span { display: flex; align-items: center; gap: 4px; }
.attendance-summary .rate { color: var(--primary); font-weight: 700; font-size: 15px; }
/* 月份标签页 */
.attendance-tabs {
display: flex;
gap: 6px;
margin-bottom: 12px;
flex-wrap: wrap;
border-bottom: 2px solid #f0ece6;
padding-bottom: 8px;
}
.attendance-tab {
padding: 6px 18px;
border-radius: 20px;
font-size: 13px;
font-weight: 600;
border: none;
background: #f5f5f5;
color: var(--muted);
cursor: pointer;
transition: all 0.2s;
font-family: inherit;
}
.attendance-tab:hover { background: #ebf5ff; color: var(--primary); }
.attendance-tab.active { background: var(--primary); color: white; }
.attendance-tab .count {
display: inline-block;
font-size: 10px;
background: rgba(255,255,255,0.2);
border-radius: 10px;
padding: 0 6px;
margin-left: 4px;
vertical-align: middle;
}
.attendance-tab.active .count { background: rgba(255,255,255,0.25); }
.attendance-tab .count.empty-count { background: #eee; color: #bbb; }
/* 表格容器 */
.attendance-table-wrap {
overflow-x: auto;
border-radius: 10px;
border: 1px solid #f0ece6;
}
.attendance-table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
min-width: 600px;
}
.attendance-table th, .attendance-table td {
padding: 8px 10px;
text-align: center;
border-bottom: 1px solid #f0ece6;
white-space: nowrap;
}
.attendance-table thead th {
background: #faf8f5;
font-weight: 700;
color: var(--text);
font-size: 12px;
position: sticky;
top: 0;
z-index: 2;
}
.attendance-table thead th.student-col {
text-align: left;
position: sticky;
left: 0;
z-index: 3;
background: #faf8f5;
min-width: 80px;
}
.attendance-table thead .week-theme {
font-weight: 400;
font-size: 11px;
color: var(--muted);
display: block;
margin-top: 2px;
max-width: 120px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.attendance-table thead .rate-header {
color: var(--primary);
font-size: 13px;
min-width: 60px;
}
.attendance-table tbody td.student-col {
text-align: left;
font-weight: 600;
position: sticky;
left: 0;
background: white;
z-index: 1;
}
.attendance-table tbody tr:hover td { background: #faf8f5; }
.attendance-table tbody tr:hover td.student-col { background: #f5f0ea; }
.attendance-table tbody tr:last-child td { border-bottom: none; }
/* 出勤状态标记 */
.att-cell {
display: inline-flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
border-radius: 8px;
cursor: pointer;
transition: all 0.15s;
font-size: 16px;
user-select: none;
}
.att-cell:hover { transform: scale(1.15); box-shadow: 0 2px 8px rgba(0,0,0,0.12); }
.att-cell.present {
background: #e8f5e9;
cursor: pointer;
}
.att-cell.leave {
background: #fce4ec;
cursor: pointer;
}
.att-cell.unknown {
background: #f5f5f5;
color: #ccc;
cursor: pointer;
}
.att-cell.unknown::after { content: ''; }
/* 出勤率列 */
.rate-cell {
font-weight: 700;
font-size: 14px;
}
.rate-cell.high { color: var(--accent); }
.rate-cell.medium { color: var(--warning); }
.rate-cell.low { color: var(--danger); }
/* 空状态 */
.attendance-empty {
padding: 32px 16px;
text-align: center;
color: var(--muted);
font-size: 14px;
}
.attendance-empty .hint { font-size: 12px; margin-top: 8px; opacity: 0.7; }
</style>
</head>
<body>
@@ -606,6 +780,12 @@
<!-- 学生卡片容器 -->
<div id="cardsContainer"></div>
<!-- 进度条 -->
<div class="progress-wrap">
<div class="progress-track"><div class="progress-fill" id="progressFill"></div></div>
<div class="progress-label"><strong id="filledCount">0</strong>/<strong id="totalCount">0</strong> 人已填写 <span id="pendingHint">⏳ 等待填写</span></div>
</div>
<!-- 速记参考 -->
<div class="shorthand">
<strong>📌 速记符号参考:</strong><br />
@@ -652,6 +832,29 @@
<button class="btn-secondary" onclick="clearAll()">🗑️ 清空</button>
</div>
<!-- ==================== 出勤统计表 ==================== -->
<div class="attendance-section" id="attendanceSection">
<div class="attendance-header">
<div>
<div class="attendance-title">📊 出勤统计表</div>
<div class="attendance-subtitle">点击表格中的标记可切换出勤状态 · 数据自动保存在浏览器中</div>
</div>
<div class="attendance-summary">
<span>📋 总课次: <strong id="attTotal">0</strong></span>
<span>✅ 出勤: <strong id="attPresent">0</strong></span>
<span>❌ 请假: <strong id="attLeave">0</strong></span>
<span>📈 出勤率: <span class="rate" id="attRate">0%</span></span>
</div>
</div>
<div class="attendance-tabs" id="attendanceTabs"></div>
<div class="attendance-table-wrap" id="attendanceTableWrap">
<div class="attendance-empty" id="attendanceEmpty">
<div>📋 暂无出勤记录</div>
<div class="hint">当您在本页面选择学生出勤/请假状态时,系统会自动记录出勤数据</div>
</div>
</div>
</div>
<!-- 输出区域 -->
<div class="output" id="output">
<h3 id="outputTitle">⬇️ 输出结果复制保存或交给Claude生成课评</h3>
@@ -664,6 +867,7 @@
</div>
<script src=".claude/memory/config/class-data.js?v=20260527"></script>
<script src=".claude/memory/config/attendance_data.js?v=20260606"></script>
<script>
// ===============================================
// 状态管理
@@ -766,6 +970,7 @@ function resetDisplay() {
document.getElementById('coursePanel').classList.remove('show');
document.getElementById('cardsContainer').innerHTML='';
document.getElementById('output').classList.remove('show');
document.getElementById('attendanceSection').classList.remove('show');
selectedTags = [];
renderTagLibrary();
}
@@ -789,7 +994,7 @@ function onClassChange() {
document.getElementById('customClassGroup').style.display='none';
document.getElementById('customClassTypeGroup').style.display='none';
currentClass = CONFIG.classes.find(c=>c.id===classId);
if(!currentClass){document.getElementById('coursePanel').classList.remove('show');document.getElementById('cardsContainer').innerHTML='';return;}
if(!currentClass){document.getElementById('coursePanel').classList.remove('show');document.getElementById('cardsContainer').innerHTML='';document.getElementById('attendanceSection').classList.remove('show');return;}
const courseType=currentClass.courseType;
const cd=CONFIG.courses[courseType]?.[currentWeek];
const theme=cd?.theme||'待定';
@@ -804,6 +1009,10 @@ function onClassChange() {
selectedTags=[];
renderStudents();
renderTagLibrary();
// 渲染出勤统计表
if (currentClass && currentClass.students && currentClass.students.length > 0) {
renderAttendanceSection(currentClass);
}
}
function handleCustomClass() {
@@ -827,6 +1036,10 @@ function handleCustomClass() {
selectedTags=[];
renderCustomStudents();
renderTagLibrary();
// 自定义班级延迟渲染出勤表(等学生列表更新后)
setTimeout(() => {
if (currentClass && students.length > 0) renderAttendanceSection(currentClass);
}, 100);
}
// ===============================================
@@ -834,7 +1047,10 @@ function handleCustomClass() {
// ===============================================
function renderStudents() {
students=[...currentClass.students];
statuses=students.map(()=>'present');
statuses=students.map(s=>{
const saved=currentClass?getAttendanceForWeek(currentClass.id,s.name,currentWeek):null;
return saved||'present';
});
tagTargetIndex=0;
const c=document.getElementById('cardsContainer');
c.innerHTML=students.map((s,i)=>`
@@ -876,7 +1092,10 @@ function updateCustomStudents() {
if(!inp)return;
const names=inp.value.trim().split('\n').filter(n=>n.trim());
students=names.map((n,i)=>({name:n.trim(),emoji:i%2===0?'👧':'🧒',color:['#e8f5e9','#e3f2fd','#fff3e0','#fce4ec','#f3e5f5'][i%5],trait:'待观察'}));
statuses=students.map(()=>'present');
statuses=students.map(s=>{
const saved=currentClass?getAttendanceForWeek(currentClass.id,s.name,currentWeek):null;
return saved||'present';
});
tagTargetIndex=0;
const cc=document.getElementById('customStudentCards');
if(cc)cc.innerHTML=students.map((s,i)=>`
@@ -895,6 +1114,7 @@ function updateCustomStudents() {
`).join('');
updateProgress();
renderTagLibrary();
if (currentClass && students.length > 0) renderAttendanceSection(currentClass);
}
function setStatus(i,status) {
@@ -904,6 +1124,12 @@ function setStatus(i,status) {
lea.classList.toggle('active-leave',status==='leave');
card.classList.toggle('absent',status==='leave');
updateProgress();
// 自动保存出勤
if (currentClass && currentWeek && students[i]?.name) {
recordAttendance(currentClass.id, students[i].name, currentWeek, status);
// 刷新出勤统计表
renderAttendanceSection(currentClass);
}
}
function onInput(i) {
@@ -1186,10 +1412,347 @@ function applyTagsToStudent() {
}
// ===============================================
// 表单生成输
// 出勤统计系统
// ===============================================
const ATTENDANCE_DB_KEY = 'keping_attendance_db';
const MONTH_LABELS = { 2: '3月', 3: '4月', 4: '5月', 5: '6月', 6: '7月' };
const MONTH_KEYS = [2, 3, 4, 5, 6];
// 获取本学期某周对应的月份 (0-based)
function getMonthForWeek(weekNum) {
const start = new Date('2026-03-02');
const monday = new Date(start);
monday.setDate(start.getDate() + (weekNum - 1) * 7);
return monday.getMonth(); // 2=3月, 3=4月, 4=5月, 5=6月, 6=7月
}
// 某个月份包含哪些周
function getWeeksForMonth(monthIdx) {
const weeks = [];
for (let w = 1; w <= 21; w++) {
if (getMonthForWeek(w) === monthIdx) weeks.push(w);
}
return weeks;
}
// 获取学期周对应的具体日期 (基于班级的星期几)
function getDateForWeek(weekNum, weekday) {
return getDateForWeekAndWeekday(weekNum, weekday);
}
// ========== 数据库操作 ==========
function getAttendanceDB() {
try { return JSON.parse(localStorage.getItem(ATTENDANCE_DB_KEY) || '{}'); }
catch(e) { return {}; }
}
function saveAttendanceDB(db) {
try { localStorage.setItem(ATTENDANCE_DB_KEY, JSON.stringify(db)); }
catch(e) { console.warn('出勤数据保存失败:', e); }
}
function recordAttendance(classId, studentName, week, status) {
if (!classId || !studentName || !week) return;
const db = getAttendanceDB();
if (!db[classId]) db[classId] = {};
if (!db[classId][studentName]) db[classId][studentName] = {};
db[classId][studentName][String(week)] = status;
saveAttendanceDB(db);
}
function getAttendanceForWeek(classId, studentName, week) {
const db = getAttendanceDB();
return db[classId]?.[studentName]?.[String(week)] || null;
}
// 获取某学生在某班级的所有出勤记录
function getStudentAttendance(classId, studentName) {
const db = getAttendanceDB();
return db[classId]?.[studentName] || {};
}
// 同步当前表单中的出勤状态到数据库
function syncCurrentAttendance() {
if (!currentClass || !currentWeek) return;
const classId = currentClass.id;
students.forEach((s, i) => {
if (s.name) recordAttendance(classId, s.name, currentWeek, statuses[i]);
});
// 也同步补课学生(标记为 present
const makeup = collectMakeupData();
makeup.forEach(m => {
if (m.name) recordAttendance(classId, m.name, currentWeek, 'present');
});
}
// ========== 统计计算 ==========
function calcAttendanceStats(classId, studentName) {
const records = getStudentAttendance(classId, studentName);
const weeks = Object.keys(records);
let present = 0, leave = 0;
weeks.forEach(w => {
if (records[w] === 'present') present++;
else if (records[w] === 'leave') leave++;
});
return { total: weeks.length, present, leave, rate: weeks.length > 0 ? Math.round(present / weeks.length * 100) : 0 };
}
function calcClassStats(classId, studentNames) {
let total = 0, present = 0, leave = 0;
studentNames.forEach(name => {
const records = getStudentAttendance(classId, name);
Object.values(records).forEach(v => {
total++;
if (v === 'present') present++;
else if (v === 'leave') leave++;
});
});
return { total, present, leave, rate: total > 0 ? Math.round(present / total * 100) : 0 };
}
// ========== 单元格点击切换 ==========
function toggleAttendanceCell(classId, studentName, week) {
const current = getAttendanceForWeek(classId, studentName, week);
let nextStatus;
if (!current || current === 'unknown') nextStatus = 'present';
else if (current === 'present') nextStatus = 'leave';
else nextStatus = 'present'; // leave → present
recordAttendance(classId, studentName, week, nextStatus);
// 重新渲染当前月份
const classObj = CONFIG.classes.find(c => c.id === classId);
if (classObj) {
const activeTab = document.querySelector('.attendance-tab.active');
const monthIdx = activeTab ? parseInt(activeTab.dataset.month) : getMonthForWeek(currentWeek);
renderAttendanceForMonth(classObj, monthIdx);
}
}
// ========== 渲染主函数 ==========
function renderAttendanceSection(classObj) {
if (!classObj) return;
const section = document.getElementById('attendanceSection');
if (!section) return;
const students = classObj.students || [];
if (students.length === 0) { section.classList.remove('show'); return; }
section.classList.add('show');
// 更新总览统计
const stats = calcClassStats(classObj.id, students.map(s => s.name));
document.getElementById('attTotal').textContent = stats.total;
document.getElementById('attPresent').textContent = stats.present;
document.getElementById('attLeave').textContent = stats.leave;
document.getElementById('attRate').textContent = stats.rate + '%';
// 渲染月份标签页
const currentMonth = getMonthForWeek(currentWeek || 12);
renderMonthTabs(classObj, currentMonth);
// 渲染当前月份表格
renderAttendanceForMonth(classObj, currentMonth);
}
function renderMonthTabs(classObj, activeMonth) {
const container = document.getElementById('attendanceTabs');
if (!container) return;
let html = '';
MONTH_KEYS.forEach(m => {
const weeks = getWeeksForMonth(m);
// 计算该月有数据的周数
let hasData = false;
const db = getAttendanceDB();
const classDb = db[classObj.id];
if (classDb) {
for (const name of Object.keys(classDb)) {
for (const w of weeks) {
if (classDb[name][String(w)]) { hasData = true; break; }
}
if (hasData) break;
}
}
// 如果是当前月份或者有数据,显示
const isActive = m === activeMonth;
const cls = 'attendance-tab' + (isActive ? ' active' : '');
html += `<button class="${cls}" data-month="${m}" onclick="switchAttendanceMonth('${classObj.id}', ${m})">${MONTH_LABELS[m]} <span class="count">${weeks.length}周</span></button>`;
});
container.innerHTML = html;
}
function switchAttendanceMonth(classId, monthIdx) {
// 更新 tab 高亮
document.querySelectorAll('.attendance-tab').forEach(tab => {
tab.classList.toggle('active', parseInt(tab.dataset.month) === monthIdx);
});
const classObj = CONFIG.classes.find(c => c.id === classId);
if (classObj) renderAttendanceForMonth(classObj, monthIdx);
}
function renderAttendanceForMonth(classObj, monthIdx) {
const wrap = document.getElementById('attendanceTableWrap');
const empty = document.getElementById('attendanceEmpty');
if (!wrap || !empty) return;
const students = classObj.students || [];
const weeks = getWeeksForMonth(monthIdx);
const courseType = classObj.courseType;
const prefix = classObj.coursePrefix || courseType;
// 检查是否有任何数据
let anyData = false;
const db = getAttendanceDB();
const classDb = db[classObj.id];
if (classDb) {
for (const name of Object.keys(classDb)) {
for (const w of weeks) {
if (classDb[name][String(w)]) { anyData = true; break; }
}
if (anyData) break;
}
}
// 计算该月每个学生的出勤率
const studentRates = students.map(s => {
const stats = calcAttendanceStats(classObj.id, s.name);
const weekRecords = {};
weeks.forEach(w => {
weekRecords[w] = getAttendanceForWeek(classObj.id, s.name, w);
});
return { student: s, weekRecords, stats };
});
// 构建表格 - 使用 data attributes 避免引号问题
let html = '<table class="attendance-table">';
// 表头:第一行 - 周数
html += '<thead><tr>';
html += '<th class="student-col">学生姓名</th>';
weeks.forEach(w => {
const course = CONFIG.courses[courseType]?.[w];
const theme = course?.theme || (prefix ? `${prefix}-${String(w).padStart(2,'0')}` : `${w}`);
const weekDate = classObj.weekday ? formatDateShort(getDateForWeek(w, classObj.weekday)) : '';
html += `<th>第${w}周<div class="week-theme" title="${theme} · ${weekDate}">${theme}</div></th>`;
});
html += '<th class="rate-header">出勤率</th>';
html += '</tr></thead>';
// 表体
html += '<tbody>';
studentRates.forEach(({ student, weekRecords, stats }) => {
const rateColor = stats.rate >= 80 ? 'high' : (stats.rate >= 50 ? 'medium' : 'low');
// 用 data-* 属性存储信息,避免引号问题
const safeId = classObj.id.replace(/['"`]/g, '');
const safeName = student.name.replace(/['"`]/g, '');
html += '<tr>';
html += `<td class="student-col">${student.emoji || '👤'} ${student.name}</td>`;
weeks.forEach(w => {
const status = weekRecords[w];
const cellClass = status === 'present' ? 'present' : (status === 'leave' ? 'leave' : 'unknown');
const symbol = status === 'present' ? '✅' : (status === 'leave' ? '❌' : '');
if (status) anyData = true;
html += `<td><span class="att-cell ${cellClass}" data-class="${safeId}" data-student="${safeName}" data-week="${w}" onclick="toggleAttendanceCellFromAttr(this)">${symbol}</span></td>`;
});
html += `<td class="rate-cell ${rateColor}">${stats.rate}%<br><span style="font-size:11px;font-weight:400;color:var(--muted)">${stats.present}/${stats.total}</span></td>`;
html += '</tr>';
});
html += '</tbody></table>';
if (!anyData) {
empty.style.display = 'block';
const oldTable = wrap.querySelector('table');
if (oldTable) oldTable.remove();
} else {
empty.style.display = 'none';
const oldTable = wrap.querySelector('table');
if (oldTable) oldTable.remove();
wrap.insertAdjacentHTML('beforeend', html);
}
}
// 从 data attributes 切换出勤(安全版本,避免引号问题)
function toggleAttendanceCellFromAttr(el) {
const classId = el.dataset.class;
const studentName = el.dataset.student;
const week = parseInt(el.dataset.week);
if (!classId || !studentName || !week) return;
toggleAttendanceCell(classId, studentName, week);
}
// ========== 从文件数据加载到 localStorage ==========
// 将 ATTENDANCE_DATA从 feedback/ 文件扫描生成)合并到 localStorage
function mergeAttendanceData() {
if (typeof ATTENDANCE_DATA === 'undefined') {
console.log('ATTENDANCE_DATA 未加载,跳过合并');
return;
}
const db = getAttendanceDB();
let added = 0;
for (const classId of Object.keys(ATTENDANCE_DATA)) {
if (!db[classId]) db[classId] = {};
const students = ATTENDANCE_DATA[classId];
for (const studentName of Object.keys(students)) {
if (!db[classId][studentName]) db[classId][studentName] = {};
const weeks = students[studentName];
for (const week of Object.keys(weeks)) {
// 仅在 localStorage 没有该记录时写入(文件数据作为初始基准)
if (!db[classId][studentName][week]) {
db[classId][studentName][week] = weeks[week];
added++;
}
}
}
}
// ===== 名字匹配补救:当配置中的学生名与目录名不一致时 =====
// 例如:目录 "胡翰铭" vs 配置 "胡瀚铭"(同音不同字)
// 使用逐字对比:首字相同 + 至少一半字匹配
function nameSimilar(a, b) {
if (a === b) return true;
if (a[0] !== b[0]) return false;
let matches = 0;
const minLen = Math.min(a.length, b.length);
for (let i = 0; i < minLen; i++) {
if (a[i] === b[i]) matches++;
}
return matches >= Math.ceil(minLen / 2);
}
if (typeof CONFIG !== 'undefined' && CONFIG.classes) {
for (const cls of CONFIG.classes) {
const classId = cls.id;
if (!db[classId]) continue;
for (const stu of cls.students) {
const sname = stu.name;
if (!db[classId][sname] || Object.keys(db[classId][sname]).length === 0) {
const dbStudents = Object.keys(db[classId]);
for (const dbName of dbStudents) {
if (sname !== dbName && nameSimilar(sname, dbName)) {
if (!db[classId][sname]) db[classId][sname] = {};
for (const wk of Object.keys(db[classId][dbName])) {
if (!db[classId][sname][wk]) {
db[classId][sname][wk] = db[classId][dbName][wk];
added++;
}
}
}
}
}
}
}
}
saveAttendanceDB(db);
console.log(`已合并 ${added} 条出勤记录到 localStorage`);
if (added > 0) console.log('(含通过名字匹配补录的记录)');
}
function generate() {
if(!currentClass){showToast('请先选择班级','error');return;}
syncCurrentAttendance(); // 生成前同步出勤数据
const date=getDateForWeekAndWeekday(currentWeek,currentWeekday);
const dateStr=formatDate(date);
let theme=document.getElementById('courseTheme').textContent;
@@ -1263,6 +1826,8 @@ async function generateWithAI() {
if(!url){showToast('请先配置 AI API 地址','error');return;}
if(!currentClass){showToast('请先选择班级','error');return;}
syncCurrentAttendance(); // AI生成前同步出勤数据
const btn=document.getElementById('genAiBtn');
const origText=btn.textContent;
btn.disabled=true; btn.textContent='⏳ AI生成中...';
@@ -1377,6 +1942,8 @@ function init() {
fillWeekOptions();
loadAiConfig();
updateAiStatusDot();
// 从 feedback/ 文件加载出勤数据到 localStorage
mergeAttendanceData();
const today=new Date();
const weekMap=['周日','周一','周二','周三','周四','周五','周六'];