From f9fa280deef1d6dbbd01272c0632960824bfa34a Mon Sep 17 00:00:00 2001 From: darkclip Date: Sat, 30 May 2026 23:27:52 +0800 Subject: [PATCH] update --- main.py | 222 +++++++++++++++++++++++++++++++++++------------- score_system.db | Bin 61440 -> 0 bytes 2 files changed, 162 insertions(+), 60 deletions(-) delete mode 100644 score_system.db diff --git a/main.py b/main.py index c935ce3..3ef92a0 100644 --- a/main.py +++ b/main.py @@ -567,6 +567,11 @@ async def reorder_programs(data: dict): raise HTTPException(status_code=400, detail='缺少参数') with get_db(write=True) as conn: cursor = conn.cursor() + # 1. 先将该批次所有节目的 order_num 清空,避免唯一约束冲突 + cursor.execute( + 'UPDATE programs SET order_num = NULL WHERE batch_id = ?', (batch_id,) + ) + # 2. 按新顺序重新赋值 for idx, sn in enumerate(sn_list, start=1): cursor.execute( 'UPDATE programs SET order_num = ? WHERE sn = ? AND batch_id = ?', @@ -901,10 +906,10 @@ async def get_aggregate(batch_id: int | None = Query(None)): f""" SELECT t.program_sn, - AVG(CASE WHEN t.final_total > 0 THEN t.final_total ELSE NULL END) AS avg_score, - MAX(CASE WHEN t.final_total > 0 THEN t.final_total ELSE NULL END) AS max_score, - MIN(CASE WHEN t.final_total > 0 THEN t.final_total ELSE NULL END) AS min_score, - COUNT(CASE WHEN t.final_total > 0 THEN 1 ELSE NULL END) AS scored_count + AVG(CASE WHEN t.has_score THEN t.final_total ELSE NULL END) AS avg_score, + MAX(CASE WHEN t.has_score THEN t.final_total ELSE NULL END) AS max_score, + MIN(CASE WHEN t.has_score THEN t.final_total ELSE NULL END) AS min_score, + COUNT(CASE WHEN t.has_score THEN 1 ELSE NULL END) AS scored_count FROM ( SELECT program_sn, @@ -912,7 +917,10 @@ async def get_aggregate(batch_id: int | None = Query(None)): COALESCE(admin_total_score, COALESCE(content_score,0) + COALESCE(stage_score,0) + COALESCE(art_score,0) + COALESCE(overall_score,0) - ) AS final_total + ) AS final_total, + (admin_total_score IS NOT NULL OR + content_score IS NOT NULL OR stage_score IS NOT NULL OR + art_score IS NOT NULL OR overall_score IS NOT NULL) AS has_score FROM scores WHERE program_sn IN ({placeholders}) ) t @@ -921,13 +929,10 @@ async def get_aggregate(batch_id: int | None = Query(None)): sn_list, ) - # 将结果转换为字典,key为program_sn,值为字典 stats = {} for row in cursor.fetchall(): stats[row['program_sn']] = { - 'avg_score': row['avg_score'] - if row['avg_score'] is not None and row['scored_count'] > 0 - else None, + 'avg_score': row['avg_score'] if row['avg_score'] is not None else None, 'max_score': row['max_score'] or 0, 'min_score': row['min_score'] or 0, 'scored_count': row['scored_count'] or 0, @@ -937,7 +942,7 @@ async def get_aggregate(batch_id: int | None = Query(None)): for prog in programs: stat = stats.get( prog['sn'], - {'avg_score': 0, 'max_score': 0, 'min_score': 0, 'scored_count': 0}, + {'avg_score': None, 'max_score': 0, 'min_score': 0, 'scored_count': 0}, ) results.append( { @@ -1029,25 +1034,36 @@ async def print_program_score(program_sn: str): scores_data, all_scores = {}, [] for judge in judges: score = conn.execute( - 'SELECT content_score, stage_score, art_score, overall_score, comment FROM scores WHERE judge_id = ? AND program_sn = ?', + """SELECT content_score, stage_score, art_score, overall_score, + admin_total_score, comment + FROM scores WHERE judge_id = ? AND program_sn = ?""", (judge['id'], program_sn), ).fetchone() total = 0 + has_score = False comment = '' if score: - total = 0 - if score['content_score'] is not None: - total += score['content_score'] - if score['stage_score'] is not None: - total += score['stage_score'] - if score['art_score'] is not None: - total += score['art_score'] - if score['overall_score'] is not None: - total += score['overall_score'] - if total == 0 and score['content_score'] is not None: - total = score['content_score'] + # 优先使用管理员总分 + if score['admin_total_score'] is not None: + total = score['admin_total_score'] + has_score = True + else: + # 计算明细分之和 + if score['content_score'] is not None: + total += score['content_score'] + has_score = True + if score['stage_score'] is not None: + total += score['stage_score'] + has_score = True + if score['art_score'] is not None: + total += score['art_score'] + has_score = True + if score['overall_score'] is not None: + total += score['overall_score'] + has_score = True comment = score['comment'] or '' - if total > 0: + # 只有已评分的评委才计入统计(包括0分) + if has_score: all_scores.append(total) scores_data[judge['name']] = {'total': total, 'comment': comment} max_score = max(all_scores) if all_scores else 0 @@ -1160,7 +1176,7 @@ def render_print_blank_template(program, judges, criteria, titles):
评委签名:____________________日期:____年____月____日
""" html += ( - '
' + '
' '' ) return html @@ -1440,6 +1456,46 @@ HTML_TEMPLATE = """ let refreshTimer = null; let userToken = null; + // ========== 统一认证请求封装 ========== + async function authFetch(url, options = {}) { + // 如果有 token,添加到请求头 + if (userToken) { + options.headers = options.headers || {}; + options.headers['Authorization'] = 'Bearer ' + userToken; + } + try { + const response = await fetch(url, options); + if (response.status === 401 || response.status === 403) { + // token 无效或过期,清除本地登录状态 + localStorage.removeItem('score_user'); + userToken = null; + currentUser = null; + userType = null; + // 显示提示 + showToast('登录已过期,请重新登录'); + // 重置界面到未登录状态 + document.getElementById('loginArea').style.display = 'flex'; + document.getElementById('userInfoArea').style.display = 'none'; + document.getElementById('tab-admin').style.display = 'none'; + document.getElementById('tab-award').style.display = 'none'; + document.getElementById('judgeScoringTab').style.display = 'block'; + document.getElementById('adminScoringTab').style.display = 'none'; + switchTab('scoring'); + throw new Error('登录已过期'); + } + return response; + } catch (err) { + if (err.message === '登录已过期') { + // 重新抛出,让调用方不再继续处理 + throw err; + } + // 其他网络错误 + console.error('请求失败:', err); + showToast('网络错误,请稍后重试'); + throw err; + } + } + async function restoreLogin() { const stored = localStorage.getItem('score_user'); if (!stored) return false; @@ -1562,9 +1618,8 @@ HTML_TEMPLATE = """ async function clearBatchPrograms(batchId) { if(confirm(`确定要清空该批次的所有节目吗?此操作不可恢复!`)){ - const res = await fetch(`/api/batches/${batchId}/programs`, { + const res = await authFetch(`/api/batches/${batchId}/programs`, { method:'DELETE', - headers:{'Authorization': 'Bearer ' + userToken}, }); if(res.ok){ showToast('该批次所有节目已清空'); @@ -1816,7 +1871,7 @@ HTML_TEMPLATE = """ score = progScores[judge.id]; html += ``; } - html += ``; + html += ``; } html += ''; document.getElementById('adminScoringContainer').innerHTML = html; @@ -1858,9 +1913,9 @@ HTML_TEMPLATE = """ async function saveAdminScore(programSn, scores) { try { - const res = await fetch('/api/scores/admin/batch', { + const res = await authFetch('/api/scores/admin/batch', { method:'POST', - headers:{'Content-Type':'application/json', 'Authorization': 'Bearer ' + userToken}, + headers:{'Content-Type':'application/json'}, body:JSON.stringify({program_sn:programSn, judge_scores:scores}) }); if(res.ok){ @@ -1905,7 +1960,7 @@ HTML_TEMPLATE = """ row.insertCell(9).innerText = item.scored_count; const btnCell = row.insertCell(10); btnCell.className = 'col-operation'; - btnCell.innerHTML = ``; + btnCell.innerHTML = ``; }); const batch = batchesData.find(b => b.id === currentBatchId); document.getElementById('summaryTitle').innerHTML = batch ? ` - ${batch.name}` : ''; @@ -1988,16 +2043,26 @@ HTML_TEMPLATE = """ function renderProgramManageList() { const container = document.getElementById('programManageContainer'); - if(!container) return; + if (!container) return; let grouped = {}; - programsData.forEach(p => { let batch = batchesData.find(b=>b.id===p.batch_id); let batchName = batch ? batch.name : '未分组'; if(!grouped[batchName]) grouped[batchName] = []; grouped[batchName].push(p); }); + // 按批次分组 + programsData.forEach(p => { + let batch = batchesData.find(b => b.id === p.batch_id); + let batchName = batch ? batch.name : '未分组'; + if (!grouped[batchName]) grouped[batchName] = []; + grouped[batchName].push(p); + }); let html = ''; - for(let batchName in grouped){ + for (let batchName in grouped) { + // 组内按 order_num 排序(确保后端顺序) + grouped[batchName].sort((a, b) => (a.order_num || 0) - (b.order_num || 0)); html += `

📁 ${batchName}

`; - grouped[batchName].forEach(p => { - html += `
- ${p.sn} | ${p.name} | ${p.unit} | ${p.performer} + grouped[batchName].forEach((p, idx, arr) => { + html += `
+ ${idx+1}. ${p.sn} | ${p.name} | ${p.unit} | ${p.performer}
+ ${idx > 0 ? `` : ''} + ${idx < arr.length-1 ? `` : ''}
@@ -2036,9 +2101,9 @@ HTML_TEMPLATE = """ const first = parseInt(document.getElementById('awardFirst').value) || 0; const second = parseInt(document.getElementById('awardSecond').value) || 0; const third = parseInt(document.getElementById('awardThird').value) || 0; - await fetch('/api/settings/awards', { + await authFetch('/api/settings/awards', { method: 'POST', - headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + userToken }, + headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ first, second, third }) }); cachedAwardSettings = null; @@ -2050,9 +2115,9 @@ HTML_TEMPLATE = """ const first_name = document.getElementById('awardFirstName').value; const second_name = document.getElementById('awardSecondName').value; const third_name = document.getElementById('awardThirdName').value; - await fetch('/api/settings/award_names', { + await authFetch('/api/settings/award_names', { method: 'POST', - headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + userToken }, + headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ first_name, second_name, third_name }) }); cachedAwardSettings = null; @@ -2064,9 +2129,9 @@ HTML_TEMPLATE = """ async function saveBatch() { const name = document.getElementById('batchName').value.trim(); if(!name){ showToast('请输入批次名称'); return; } - await fetch('/api/batches', { + await authFetch('/api/batches', { method:'POST', - headers:{'Content-Type':'application/json', 'Authorization': 'Bearer ' + userToken}, + headers:{'Content-Type':'application/json'}, body:JSON.stringify({name}) }); closeBatchModal(); @@ -2074,9 +2139,8 @@ HTML_TEMPLATE = """ } async function deleteBatch(id) { if(confirm('确定删除该批次吗?')) { - const res = await fetch(`/api/batches/${id}`, { + const res = await authFetch(`/api/batches/${id}`, { method:'DELETE', - headers:{'Authorization': 'Bearer ' + userToken}, }); if (res.ok) { currentBatchId = null; @@ -2093,6 +2157,47 @@ HTML_TEMPLATE = """ } } + function getProgramsOfBatch(batchId) { + const targetId = parseInt(batchId); + return programsData.filter(p => parseInt(p.batch_id) === targetId) + .sort((a, b) => (a.order_num || 0) - (b.order_num || 0)); + } + async function swapAndSave(batchId, snA, snB) { + let list = getProgramsOfBatch(batchId); + const indexA = list.findIndex(p => p.sn === snA); + const indexB = list.findIndex(p => p.sn === snB); + if (indexA === -1 || indexB === -1) return; + [list[indexA], list[indexB]] = [list[indexB], list[indexA]]; + const snList = list.map(p => p.sn); + const res = await authFetch('/api/programs/reorder', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ batch_id: batchId, sn_list: snList }) + }); + if (res.ok) { + showToast('排序已保存'); + await loadPrograms(); // 重新加载节目数据 + if (userType === 'judge') await loadJudgePrograms(); + if (userType === 'admin') await loadAdminScoring(); + await loadAggregate(); + renderProgramManageList(); // 刷新节目管理列表 + } else { + const err = await res.json(); + showToast('保存失败:' + (err.detail || '未知错误')); + } + } + function moveProgramUp(sn, batchId) { + let list = getProgramsOfBatch(batchId); + let idx = list.findIndex(p => p.sn === sn); + if (idx <= 0) return; + swapAndSave(batchId, sn, list[idx-1].sn); + } + function moveProgramDown(sn, batchId) { + let list = getProgramsOfBatch(batchId); + let idx = list.findIndex(p => p.sn === sn); + if (idx === -1 || idx >= list.length-1) return; + swapAndSave(batchId, sn, list[idx+1].sn); + } function showAddProgramModal() { editingProgramSn=null; document.getElementById('programModalTitle').innerText='添加节目'; document.getElementById('programSn').disabled=false; document.getElementById('programSn').value=''; document.getElementById('programName').value=''; document.getElementById('programUnit').value=''; document.getElementById('programPerformer').value=''; document.getElementById('programCategory').value='舞蹈'; renderBatchSelect(); document.getElementById('programModal').style.display='flex'; } function editProgram(sn) { const prog = programsData.find(p=>p.sn===sn); if(!prog) return; editingProgramSn=sn; document.getElementById('programModalTitle').innerText='编辑节目'; document.getElementById('programSn').value=prog.sn; document.getElementById('programSn').disabled=true; document.getElementById('programName').value=prog.name; document.getElementById('programUnit').value=prog.unit; document.getElementById('programPerformer').value=prog.performer; document.getElementById('programCategory').value=prog.category; renderBatchSelect(); document.getElementById('programBatchId').value=prog.batch_id; document.getElementById('programModal').style.display='flex'; } async function saveProgram() { @@ -2106,15 +2211,15 @@ HTML_TEMPLATE = """ if(!batch_id){ showToast('请先添加批次'); return; } let res; if(editingProgramSn){ - res = await fetch(`/api/programs/${editingProgramSn}`, { + res = await authFetch(`/api/programs/${editingProgramSn}`, { method:'PUT', - headers:{'Content-Type':'application/json', 'Authorization': 'Bearer ' + userToken}, + headers:{'Content-Type':'application/json'}, body:JSON.stringify({name,unit,performer,category,batch_id}) }); } else { - res = await fetch('/api/programs', { + res = await authFetch('/api/programs', { method:'POST', - headers:{'Content-Type':'application/json', 'Authorization': 'Bearer ' + userToken}, + headers:{'Content-Type':'application/json'}, body:JSON.stringify({sn,name,unit,performer,category,batch_id}) }); } @@ -2131,9 +2236,8 @@ HTML_TEMPLATE = """ } async function deleteProgram(sn) { if(confirm(`确定删除节目 ${sn} 吗?`)){ - await fetch(`/api/programs/${sn}`, { + await authFetch(`/api/programs/${sn}`, { method:'DELETE', - headers:{'Authorization': 'Bearer ' + userToken}, }); await loadPrograms(); if(userType==='judge') await loadJudgePrograms(); @@ -2151,9 +2255,8 @@ HTML_TEMPLATE = """ if(!batch_id){ showToast('请先添加批次'); return; } const formData = new FormData(); formData.append('file', file); - const res = await fetch(`/api/programs/import?batch_id=${batch_id}`, { + const res = await authFetch(`/api/programs/import?batch_id=${batch_id}`, { method:'POST', - headers:{'Authorization': 'Bearer ' + userToken}, body:formData }); if(res.ok){ @@ -2178,15 +2281,15 @@ HTML_TEMPLATE = """ if(!id||!name){ showToast('请填写评委编号和姓名'); return; } let res; if(editingJudgeId){ - res = await fetch(`/api/judges/${editingJudgeId}`, { + res = await authFetch(`/api/judges/${editingJudgeId}`, { method:'PUT', - headers:{'Content-Type':'application/json', 'Authorization': 'Bearer ' + userToken}, + headers:{'Content-Type':'application/json'}, body:JSON.stringify({name}) }); } else { - res = await fetch('/api/judges', { + res = await authFetch('/api/judges', { method:'POST', - headers:{'Content-Type':'application/json', 'Authorization': 'Bearer ' + userToken}, + headers:{'Content-Type':'application/json'}, body:JSON.stringify({id,name}) }); } @@ -2201,9 +2304,8 @@ HTML_TEMPLATE = """ } async function deleteJudge(id) { if(confirm(`确定删除评委 ${id} 吗?`)){ - await fetch(`/api/judges/${id}`, { + await authFetch(`/api/judges/${id}`, { method:'DELETE', - headers:{'Authorization': 'Bearer ' + userToken}, }); await loadJudges(); if(userType==='admin') await renderAdminScoringTable(); @@ -2217,9 +2319,9 @@ HTML_TEMPLATE = """ async function saveTitleSettings() { const main_title = document.getElementById('mainTitleInput').value; const sub_title = document.getElementById('subTitleInput').value; - await fetch('/api/settings/titles', { + await authFetch('/api/settings/titles', { method:'POST', - headers:{'Content-Type':'application/json', 'Authorization': 'Bearer ' + userToken}, + headers:{'Content-Type':'application/json'}, body:JSON.stringify({main_title,sub_title}) }); closeTitleModal(); diff --git a/score_system.db b/score_system.db deleted file mode 100644 index 5e9b6551f74d909129fca8d073d8c8a9cdf20ddf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 61440 zcmeI(Pi)&%9KdlWZu6&W>LLw=X{-9CYEq-7Ym@GewhM&Ln=0D0Yg3J#EI0AGK$CQ~ zvoWCw3KL^!2M(|k;(&w%2Q(o#OcNJ`IB*B9T(&W;Ml?R4qvY}Gczg@#$vZ80ScO3|qFk`R(4MV%kT`3a|(v(gj3ah8#`^_^Cu(%}#G zrj!wBAh{wb*Oal;^?}C&-}nETI@kA0|Ch->`o8FUGr5wyyHf?k5kLR|1Q0*~f&ZUC zDHTf(4Mncm`pTMN8Ma-muUg@9V0uw4lvGhFygaLlFe*l0H?E43`f5omEY8do7SD@U z)bm+!Szl`!UOY1~5KB)FMWkxIV!UBpT5~?cX?oLcc<;5aA}trLDH}>(JiTvUWaFyW z5WP~X)-8X&r=t;mOpI15+q9r*8D?Ft`Hc|E#hJ6qswmEvL~(g`HY?V3%evAqgOaUy z*)(+9sA$ed%+k!9x>PF6Eo8>}W9b+7?bHRo8Cu?7rpET{V&|N{bN{<%6JB563((6A z)37XWp6uwWAM^S;Q!J@x)WvpiQCKd`&lH^!b84}a6|XfbtA-Z3w0gI0Hdal&rdf4g zthJ@wsM|)}*1Y2gXQ&+`%hsJ*+9SHzxkux&Vd`sZo%^`srP&Qz?~IolwVHDT@4#8n zT(A7iIU5YqXxMybm^K@n$%@nSi|WjoqMKo2v~7#i+|%l!TAWsw!UEQ)RnLg|qBy0_ zs!rBU7nY_Ar&Kqxyl~3R;~=u7*6_%g_By?w<)qoJYRrr!pOygrKsxE}4RYUGF+bPO zd%5l($)CqN#@~7*SOIB>3+wrHh)<3M4^OuQtPReoj6mk8G z*W6$XtZ>=W(c~~HoUz#E7j(NuH|y0fLDmh^z0@aLToeOHHF*HnzN#JBDn_#!+{SvQEj&h7^gX_wSEvobv`g=z6e-cGM9B zcYCVZE)BL#)Rfz^L3hT400IagfB*srAb)AbVnP4`1Q0*~0R#|0009ILKmdXNy1>y$ z?{IpkpjT?ux;8nP^H#iBEsTZR1@FAA2m009ILKmY**5I_I{1Q0;rUkdD%$0Kc5_}=w@;(2)@2)5lbh_`la-71JF i@<@C6-a+|LN70_v4qhoaB@ah7?+!#$^1)>5j=*2W^3u!z