feat: 文件夹双策略识别 + 布局选项 + 移除提交率卡片 + localStorage缓存层
- 文件夹识别:mode 含 'folder' 优先,缺省时用 parentId 引用反推; 即使卷王版本无 mode 字段也能正确分组,避免'直接没有文件夹选项' - 布局选项:工具栏新增 #layoutSel(默认 / 成绩明细与缺考名单同行), 用 CSS Grid 双模式渲染,窄屏回落单列;偏好写 localStorage - 移除统计卡片中的提交率卡片(卡片数 9 → 8) - localStorage 缓存层:问卷列表 / 每个项目数据 / 看板数据 / 班级组别 筛选 / 布局 / 上次项目 id 全部持久化;进入页面先渲染缓存, 后台再异步拉新数据并写回缓存;网络失败时仍能用本地缓存查看 - dashboard.php 同步:文件夹识别 / 数据缓存一致更新
This commit is contained in:
+81
-9
@@ -87,6 +87,15 @@ function rateColor(r){
|
||||
return 'var(--danger)';
|
||||
}
|
||||
|
||||
/* 本地存储(缓存问卷列表/看板数据,重开即恢复) */
|
||||
const LS = {
|
||||
get(k, def){ try { const v = localStorage.getItem(k); return v == null ? def : JSON.parse(v); } catch(e){ return def; } },
|
||||
set(k, v){ try { localStorage.setItem(k, JSON.stringify(v)); } catch(e){} },
|
||||
del(k){ try { localStorage.removeItem(k); } catch(e){} }
|
||||
};
|
||||
const LS_PROJECTS = 'ny_projects';
|
||||
const LS_DASH = pid => 'ny_dashboard_' + pid;
|
||||
|
||||
let DASH = null; // 最近一次看板数据
|
||||
let CUR = null; // 当前查看的班级名
|
||||
|
||||
@@ -102,16 +111,60 @@ async function api(action, opts){
|
||||
throw new Error((j && (j.msg || j.error)) || '请求失败');
|
||||
}
|
||||
|
||||
/* ---------- 问卷列表:双策略识别文件夹 + 缓存优先 ---------- */
|
||||
function renderProjects(list){
|
||||
const sel = $('#projectSel');
|
||||
const keep = sel.value;
|
||||
sel.innerHTML = '<option value="">请选择问卷 / 考试</option>';
|
||||
// 双策略识别文件夹:mode 含 folder,或被其他项目作为 parent_id 引用
|
||||
const isFolder = p => (p.mode || '').toLowerCase().includes('folder');
|
||||
const parentIds = new Set();
|
||||
list.forEach(p => { const pid = (p.parent_id || '0'); if (pid && pid !== '0' && pid !== '') parentIds.add(pid); });
|
||||
const folderSet = new Set();
|
||||
list.forEach(p => { if (isFolder(p) || parentIds.has(p.id)) folderSet.add(p.id); });
|
||||
const childOf = fid => list.filter(p => !folderSet.has(p.id) && (p.parent_id || '0') === fid);
|
||||
|
||||
// 根项
|
||||
list.filter(p => !folderSet.has(p.id)).forEach(p => {
|
||||
const o = document.createElement('option');
|
||||
o.value = p.id; o.textContent = (p.mode ? '[' + p.mode + '] ' : '') + p.name + '(' + (p.total || 0) + '份)';
|
||||
sel.appendChild(o);
|
||||
});
|
||||
// 文件夹
|
||||
list.filter(p => folderSet.has(p.id)).forEach(f => {
|
||||
const kids = childOf(f.id);
|
||||
if (!kids.length) return;
|
||||
const og = document.createElement('optgroup');
|
||||
og.label = '📁 ' + f.name;
|
||||
kids.forEach(p => {
|
||||
const o = document.createElement('option');
|
||||
o.value = p.id; o.textContent = (p.mode ? '[' + p.mode + '] ' : '') + p.name + '(' + (p.total || 0) + '份)';
|
||||
og.appendChild(o);
|
||||
});
|
||||
sel.appendChild(og);
|
||||
});
|
||||
if (keep && list.some(p => p.id === keep)) sel.value = keep;
|
||||
else {
|
||||
const last = LS.get('ny_dash_last_pid', '');
|
||||
if (last && list.some(p => p.id === last)) sel.value = last;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadProjects(){
|
||||
// 1) 先读本地缓存
|
||||
const cached = LS.get(LS_PROJECTS, []);
|
||||
if (Array.isArray(cached) && cached.length) {
|
||||
renderProjects(cached);
|
||||
}
|
||||
// 2) 后台异步刷新
|
||||
try {
|
||||
const list = await api('projects');
|
||||
const sel = $('#projectSel');
|
||||
const keep = sel.value;
|
||||
sel.innerHTML = '<option value="">请选择问卷 / 考试</option>' +
|
||||
list.map(p => '<option value="' + esc(p.id) + '">' + esc(p.name) + '(' + esc(p.mode || '') + ')</option>').join('');
|
||||
if (keep) sel.value = keep;
|
||||
LS.set(LS_PROJECTS, list || []);
|
||||
renderProjects(list || []);
|
||||
} catch (e) {
|
||||
$('#classGrid').innerHTML = '<div class="empty">加载问卷列表失败:' + esc(e.message) + '</div>';
|
||||
if (!cached || !cached.length) {
|
||||
$('#classGrid').innerHTML = '<div class="empty">加载问卷列表失败:' + esc(e.message) + '</div>';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -241,20 +294,39 @@ async function loadDashboard(){
|
||||
$('#classGrid').innerHTML = '<div class="empty">请选择问卷 / 考试以查看各班数据</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
// 1) 缓存优先:先渲染上次保存的看板,避免空白
|
||||
const cached = LS.get(LS_DASH(pid), null);
|
||||
if (cached && cached.classes) {
|
||||
DASH = cached;
|
||||
renderSummary(DASH);
|
||||
renderClasses(DASH);
|
||||
$('#lastUpdate').textContent = (cached.updated_at || '—') + '(缓存)';
|
||||
if (CUR) {
|
||||
const still = (DASH.classes || []).some(x => x.class === CUR);
|
||||
if (still) showClassDetail(CUR); else backToOverview();
|
||||
}
|
||||
}
|
||||
|
||||
// 2) 后台拉真实数据
|
||||
try {
|
||||
DASH = await api('dashboard', { params: { projectId: pid } });
|
||||
const fresh = await api('dashboard', { params: { projectId: pid } });
|
||||
DASH = fresh;
|
||||
LS.set(LS_DASH(pid), fresh);
|
||||
LS.set('ny_dash_last_pid', pid);
|
||||
renderSummary(DASH);
|
||||
renderClasses(DASH);
|
||||
$('#lastUpdate').textContent = DASH.updated_at || '-';
|
||||
$('#liveDot').classList.add('on');
|
||||
setTimeout(() => $('#liveDot').classList.remove('on'), 900);
|
||||
// 若正在看某班详情,刷新后同步更新
|
||||
if (CUR) {
|
||||
const still = (DASH.classes || []).some(x => x.class === CUR);
|
||||
if (still) showClassDetail(CUR); else backToOverview();
|
||||
}
|
||||
} catch (e) {
|
||||
$('#classGrid').innerHTML = '<div class="empty">数据加载失败:' + esc(e.message) + '</div>';
|
||||
if (!cached) {
|
||||
$('#classGrid').innerHTML = '<div class="empty">数据加载失败:' + esc(e.message) + '</div>';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user