Update CLASS: AICODE03课程内容更新,学生谢善诺班级调整
This commit is contained in:
575
课评系统.html
575
课评系统.html
@@ -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=['周日','周一','周二','周三','周四','周五','周六'];
|
||||
|
||||
Reference in New Issue
Block a user