update
This commit is contained in:
parent
01b3849309
commit
f9fa280dee
222
main.py
222
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):
|
||||
<div class="sign-line"><span>评委签名:____________________</span><span>日期:____年____月____日</span></div>
|
||||
<div class="sheet-bottom-space"></div></div>"""
|
||||
html += (
|
||||
'<div class="no-print" style="position:fixed; bottom:20px; right:20px;"><button onclick="window.print()">🖨️ 打印</button></div></body></html>'
|
||||
'<div class="no-print" style="position:fixed; bottom:20px; right:20px;"><button onclick="window.print()">🖨️ 打印本页</button></div></body></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 += `<td><input type="number" class="score-input" id="score_${prog.sn}_${judge.id}" value="${score}" step="0.5" min="0" max="100"></td>`;
|
||||
}
|
||||
html += `<td><button class="btn btn-outline btn-sm" onclick="printProgram('${prog.sn}')">🖨️ 打印</button></td></tr>`;
|
||||
html += `<td><button class="btn btn-outline btn-sm" onclick="printProgram('${prog.sn}')">🖨️ 打印评分</button></td></tr>`;
|
||||
}
|
||||
html += '</tbody></tr>';
|
||||
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 = `<button class="btn btn-outline btn-sm" onclick="printBlank('${item.sn}')">📄 空白评分表</button><button class="btn btn-outline btn-sm" onclick="printProgram('${item.sn}')">🖨️ 打印</button>`;
|
||||
btnCell.innerHTML = `<button class="btn btn-outline btn-sm" onclick="printBlank('${item.sn}')">📄 空白评分表</button><button class="btn btn-outline btn-sm" onclick="printProgram('${item.sn}')">🖨️ 打印评分</button>`;
|
||||
});
|
||||
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 += `<h4 style="margin:15px 0 5px 0;">📁 ${batchName}</h4>`;
|
||||
grouped[batchName].forEach(p => {
|
||||
html += `<div class="program-list-item">
|
||||
<span>${p.sn} | ${p.name} | ${p.unit} | ${p.performer}</span>
|
||||
grouped[batchName].forEach((p, idx, arr) => {
|
||||
html += `<div class="program-list-item" style="display:flex; justify-content:space-between; align-items:center; padding:10px 0; border-bottom:1px solid #eee;">
|
||||
<span style="flex:1;">${idx+1}. ${p.sn} | ${p.name} | ${p.unit} | ${p.performer}</span>
|
||||
<div class="program-buttons">
|
||||
${idx > 0 ? `<button class="btn btn-outline btn-sm" onclick="moveProgramUp('${p.sn}', '${p.batch_id}')">⬆️ 上移</button>` : ''}
|
||||
${idx < arr.length-1 ? `<button class="btn btn-outline btn-sm" onclick="moveProgramDown('${p.sn}', '${p.batch_id}')">⬇️ 下移</button>` : ''}
|
||||
<button class="btn btn-outline btn-sm" onclick="editProgram('${p.sn}')">✏️ 编辑</button>
|
||||
<button class="btn btn-outline btn-sm" onclick="deleteProgram('${p.sn}')">🗑️ 删除</button>
|
||||
</div>
|
||||
@ -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();
|
||||
|
||||
BIN
score_system.db
BIN
score_system.db
Binary file not shown.
Loading…
Reference in New Issue
Block a user