1835 lines
99 KiB
Python
1835 lines
99 KiB
Python
import sqlite3
|
||
from contextlib import contextmanager
|
||
from datetime import datetime
|
||
from io import BytesIO
|
||
from typing import cast
|
||
|
||
import pandas as pd
|
||
from fastapi import FastAPI, File, HTTPException, Query, UploadFile
|
||
from fastapi.middleware.cors import CORSMiddleware
|
||
from fastapi.responses import HTMLResponse, StreamingResponse
|
||
from pydantic import BaseModel
|
||
|
||
app = FastAPI(title='文艺汇演评分系统', description='乌鲁木齐群众文化艺术展评')
|
||
|
||
app.add_middleware(
|
||
CORSMiddleware,
|
||
allow_origins=['*'],
|
||
allow_credentials=True,
|
||
allow_methods=['*'],
|
||
allow_headers=['*'],
|
||
)
|
||
|
||
# ==================== 数据库配置 ====================
|
||
DB_PATH = 'score_system.db'
|
||
|
||
|
||
@contextmanager
|
||
def get_db():
|
||
conn = sqlite3.connect(DB_PATH)
|
||
conn.row_factory = sqlite3.Row
|
||
try:
|
||
yield conn
|
||
conn.commit()
|
||
finally:
|
||
conn.close()
|
||
|
||
|
||
def init_db():
|
||
with get_db() as conn:
|
||
cursor = conn.cursor()
|
||
|
||
cursor.execute(
|
||
"""CREATE TABLE IF NOT EXISTS batches (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, created_at TIMESTAMP)"""
|
||
)
|
||
cursor.execute(
|
||
"""CREATE TABLE IF NOT EXISTS programs (sn TEXT PRIMARY KEY, name TEXT NOT NULL, unit TEXT, performer TEXT, category TEXT, batch_id INTEGER, order_num INTEGER, created_at TIMESTAMP)"""
|
||
)
|
||
cursor.execute(
|
||
"""CREATE TABLE IF NOT EXISTS judges (id TEXT PRIMARY KEY, name TEXT NOT NULL, order_num INTEGER, created_at TIMESTAMP)"""
|
||
)
|
||
cursor.execute(
|
||
"""CREATE TABLE IF NOT EXISTS scores (id INTEGER PRIMARY KEY AUTOINCREMENT, judge_id TEXT NOT NULL, program_sn TEXT NOT NULL, content_score REAL, stage_score REAL, art_score REAL, overall_score REAL, comment TEXT, updated_at TIMESTAMP, UNIQUE(judge_id, program_sn))"""
|
||
)
|
||
cursor.execute(
|
||
"""CREATE TABLE IF NOT EXISTS admins (id TEXT PRIMARY KEY, username TEXT UNIQUE NOT NULL, password TEXT NOT NULL, created_at TIMESTAMP)"""
|
||
)
|
||
cursor.execute(
|
||
"""CREATE TABLE IF NOT EXISTS settings (key TEXT PRIMARY KEY, value TEXT)"""
|
||
)
|
||
|
||
cursor.execute('SELECT COUNT(*) FROM admins')
|
||
if cursor.fetchone()[0] == 0:
|
||
cursor.execute(
|
||
'INSERT INTO admins (id, username, password, created_at) VALUES (?, ?, ?, ?)',
|
||
('admin_001', 'admin', 'admin123', datetime.now()),
|
||
)
|
||
|
||
cursor.execute("SELECT COUNT(*) FROM settings WHERE key = 'award_first'")
|
||
if cursor.fetchone()[0] == 0:
|
||
cursor.execute(
|
||
"INSERT INTO settings (key, value) VALUES ('award_first', '3'), ('award_second', '6'), ('award_third', '10')"
|
||
)
|
||
|
||
cursor.execute("SELECT COUNT(*) FROM settings WHERE key = 'award_first_name'")
|
||
if cursor.fetchone()[0] == 0:
|
||
cursor.execute(
|
||
"INSERT INTO settings (key, value) VALUES ('award_first_name', '一等奖'), ('award_second_name', '二等奖'), ('award_third_name', '三等奖')"
|
||
)
|
||
|
||
cursor.execute("SELECT COUNT(*) FROM settings WHERE key = 'main_title'")
|
||
if cursor.fetchone()[0] == 0:
|
||
cursor.execute(
|
||
"INSERT INTO settings (key, value) VALUES ('main_title', '乌鲁木齐群众文化艺术展评'), ('sub_title', '“建农杯”文艺汇演')"
|
||
)
|
||
|
||
cursor.execute('SELECT COUNT(*) FROM batches')
|
||
if cursor.fetchone()[0] == 0:
|
||
cursor.execute(
|
||
'INSERT INTO batches (name, created_at) VALUES (?, ?)',
|
||
('第一批次', datetime.now()),
|
||
)
|
||
cursor.execute(
|
||
'INSERT INTO batches (name, created_at) VALUES (?, ?)',
|
||
('第二批次', datetime.now()),
|
||
)
|
||
|
||
cursor.execute('SELECT COUNT(*) FROM judges')
|
||
if cursor.fetchone()[0] == 0:
|
||
judges_data = [
|
||
('P.01', '王飞翔', 1, datetime.now()),
|
||
('P.02', '冯建农', 2, datetime.now()),
|
||
('P.03', '张宁', 3, datetime.now()),
|
||
('P.04', '沈澜涛', 4, datetime.now()),
|
||
('P.05', '龚楠', 5, datetime.now()),
|
||
]
|
||
cursor.executemany(
|
||
'INSERT INTO judges (id, name, order_num, created_at) VALUES (?, ?, ?, ?)',
|
||
judges_data,
|
||
)
|
||
|
||
|
||
init_db()
|
||
|
||
|
||
# ==================== Pydantic模型 ====================
|
||
class AdminLogin(BaseModel):
|
||
username: str
|
||
password: str
|
||
|
||
|
||
class JudgeLogin(BaseModel):
|
||
judge_id: str
|
||
judge_name: str
|
||
|
||
|
||
class JudgeCreate(BaseModel):
|
||
id: str
|
||
name: str
|
||
|
||
|
||
class JudgeUpdate(BaseModel):
|
||
name: str
|
||
|
||
|
||
class ProgramCreate(BaseModel):
|
||
sn: str
|
||
name: str
|
||
unit: str
|
||
performer: str
|
||
category: str
|
||
batch_id: int
|
||
|
||
|
||
class ProgramUpdate(BaseModel):
|
||
name: str
|
||
unit: str
|
||
performer: str
|
||
category: str
|
||
batch_id: int
|
||
|
||
|
||
class BatchCreate(BaseModel):
|
||
name: str
|
||
|
||
|
||
class ScoreDetailUpdate(BaseModel):
|
||
program_sn: str
|
||
field: str
|
||
value: float | None = None
|
||
|
||
|
||
class ScoreBatchUpdate(BaseModel):
|
||
program_sn: str
|
||
judge_scores: dict[str, float | None]
|
||
|
||
|
||
class CommentUpdate(BaseModel):
|
||
program_sn: str
|
||
comment: str
|
||
|
||
|
||
class AwardSettings(BaseModel):
|
||
first: int
|
||
second: int
|
||
third: int
|
||
|
||
|
||
class AwardNamesSettings(BaseModel):
|
||
first_name: str
|
||
second_name: str
|
||
third_name: str
|
||
|
||
|
||
class TitleSettings(BaseModel):
|
||
main_title: str
|
||
sub_title: str
|
||
|
||
|
||
# ==================== 根路径 ====================
|
||
@app.get('/')
|
||
async def root():
|
||
return HTMLResponse(content=HTML_TEMPLATE)
|
||
|
||
|
||
# ==================== 管理员登录API ====================
|
||
@app.post('/api/admin/login')
|
||
async def admin_login(login_data: AdminLogin):
|
||
with get_db() as conn:
|
||
cursor = conn.cursor()
|
||
cursor.execute(
|
||
'SELECT id, username FROM admins WHERE username = ? AND password = ?',
|
||
(login_data.username, login_data.password),
|
||
)
|
||
admin = cursor.fetchone()
|
||
if not admin:
|
||
raise HTTPException(status_code=401, detail='管理员用户名或密码错误')
|
||
return {'success': True, 'admin_id': admin['id'], 'username': admin['username']}
|
||
|
||
|
||
@app.post('/api/judge/login')
|
||
async def judge_login(login_data: JudgeLogin):
|
||
with get_db() as conn:
|
||
cursor = conn.cursor()
|
||
cursor.execute(
|
||
'SELECT id, name FROM judges WHERE id = ? AND name = ?',
|
||
(login_data.judge_id, login_data.judge_name),
|
||
)
|
||
judge = cursor.fetchone()
|
||
if not judge:
|
||
raise HTTPException(status_code=401, detail='评委编号或姓名错误')
|
||
return {'success': True, 'judge_id': judge['id'], 'judge_name': judge['name']}
|
||
|
||
|
||
# ==================== 设置API ====================
|
||
@app.get('/api/settings/titles')
|
||
async def get_title_settings():
|
||
with get_db() as conn:
|
||
cursor = conn.cursor()
|
||
cursor.execute(
|
||
"SELECT key, value FROM settings WHERE key IN ('main_title', 'sub_title')"
|
||
)
|
||
titles = {row['key']: row['value'] for row in cursor.fetchall()}
|
||
return {
|
||
'main_title': titles.get('main_title', '乌鲁木齐群众文化艺术展评'),
|
||
'sub_title': titles.get('sub_title', '“建农杯”文艺汇演'),
|
||
}
|
||
|
||
|
||
@app.post('/api/settings/titles')
|
||
async def update_title_settings(titles: TitleSettings):
|
||
with get_db() as conn:
|
||
cursor = conn.cursor()
|
||
cursor.execute(
|
||
"UPDATE settings SET value = ? WHERE key = 'main_title'",
|
||
(titles.main_title,),
|
||
)
|
||
cursor.execute(
|
||
"UPDATE settings SET value = ? WHERE key = 'sub_title'", (titles.sub_title,)
|
||
)
|
||
return {'success': True}
|
||
|
||
|
||
@app.get('/api/settings/awards')
|
||
async def get_award_settings():
|
||
with get_db() as conn:
|
||
cursor = conn.cursor()
|
||
cursor.execute(
|
||
"SELECT key, value FROM settings WHERE key IN ('award_first', 'award_second', 'award_third')"
|
||
)
|
||
counts = {row['key']: int(row['value']) for row in cursor.fetchall()}
|
||
cursor.execute(
|
||
"SELECT key, value FROM settings WHERE key IN ('award_first_name', 'award_second_name', 'award_third_name')"
|
||
)
|
||
names = {row['key']: row['value'] for row in cursor.fetchall()}
|
||
return {
|
||
'first': counts.get('award_first', 3),
|
||
'second': counts.get('award_second', 6),
|
||
'third': counts.get('award_third', 10),
|
||
'first_name': names.get('award_first_name', '一等奖'),
|
||
'second_name': names.get('award_second_name', '二等奖'),
|
||
'third_name': names.get('award_third_name', '三等奖'),
|
||
}
|
||
|
||
|
||
@app.post('/api/settings/awards')
|
||
async def update_award_settings(settings: AwardSettings):
|
||
with get_db() as conn:
|
||
cursor = conn.cursor()
|
||
cursor.execute(
|
||
"UPDATE settings SET value = ? WHERE key = 'award_first'",
|
||
(str(settings.first),),
|
||
)
|
||
cursor.execute(
|
||
"UPDATE settings SET value = ? WHERE key = 'award_second'",
|
||
(str(settings.second),),
|
||
)
|
||
cursor.execute(
|
||
"UPDATE settings SET value = ? WHERE key = 'award_third'",
|
||
(str(settings.third),),
|
||
)
|
||
return {'success': True}
|
||
|
||
|
||
@app.post('/api/settings/award_names')
|
||
async def update_award_names(settings: AwardNamesSettings):
|
||
with get_db() as conn:
|
||
cursor = conn.cursor()
|
||
cursor.execute(
|
||
"UPDATE settings SET value = ? WHERE key = 'award_first_name'",
|
||
(settings.first_name,),
|
||
)
|
||
cursor.execute(
|
||
"UPDATE settings SET value = ? WHERE key = 'award_second_name'",
|
||
(settings.second_name,),
|
||
)
|
||
cursor.execute(
|
||
"UPDATE settings SET value = ? WHERE key = 'award_third_name'",
|
||
(settings.third_name,),
|
||
)
|
||
return {'success': True}
|
||
|
||
|
||
@app.get('/api/criteria')
|
||
async def get_criteria():
|
||
return {
|
||
'content': {
|
||
'label': '内容取材',
|
||
'max': 20,
|
||
'description': '主题鲜明,健康向上,编排新颖,原创性加分',
|
||
},
|
||
'stage': {
|
||
'label': '舞台风度',
|
||
'max': 10,
|
||
'description': '服装统一整洁,精神饱满,出场退场从容得体',
|
||
},
|
||
'art': {
|
||
'label': '表演艺术水平',
|
||
'max': 60,
|
||
'description': '动作流畅协调,节奏感强,技巧娴熟,富有感染力',
|
||
},
|
||
'overall': {
|
||
'label': '综合评价',
|
||
'max': 10,
|
||
'description': '整体效果印象深刻,现场驾驭能力强',
|
||
},
|
||
}
|
||
|
||
|
||
# ==================== 批次管理API ====================
|
||
@app.get('/api/batches')
|
||
async def get_batches():
|
||
with get_db() as conn:
|
||
cursor = conn.cursor()
|
||
cursor.execute('SELECT id, name, created_at FROM batches ORDER BY id')
|
||
batches = [{'id': row['id'], 'name': row['name']} for row in cursor.fetchall()]
|
||
return batches
|
||
|
||
|
||
@app.post('/api/batches')
|
||
async def create_batch(batch: BatchCreate):
|
||
with get_db() as conn:
|
||
cursor = conn.cursor()
|
||
cursor.execute(
|
||
'INSERT INTO batches (name, created_at) VALUES (?, ?)',
|
||
(batch.name, datetime.now()),
|
||
)
|
||
new_id = cursor.lastrowid
|
||
return {'success': True, 'id': new_id, 'name': batch.name}
|
||
|
||
|
||
@app.delete('/api/batches/{batch_id}')
|
||
async def delete_batch(batch_id: int):
|
||
with get_db() as conn:
|
||
cursor = conn.cursor()
|
||
cursor.execute('SELECT id FROM batches WHERE id = ?', (batch_id,))
|
||
if not cursor.fetchone():
|
||
raise HTTPException(status_code=404, detail='批次不存在')
|
||
cursor.execute(
|
||
'UPDATE programs SET batch_id = NULL WHERE batch_id = ?', (batch_id,)
|
||
)
|
||
cursor.execute('DELETE FROM batches WHERE id = ?', (batch_id,))
|
||
return {'success': True}
|
||
|
||
|
||
@app.delete('/api/batches/{batch_id}/programs')
|
||
async def clear_batch_programs(batch_id: int):
|
||
with get_db() as conn:
|
||
cursor = conn.cursor()
|
||
cursor.execute('SELECT id FROM batches WHERE id = ?', (batch_id,))
|
||
if not cursor.fetchone():
|
||
raise HTTPException(status_code=404, detail='批次不存在')
|
||
cursor.execute('SELECT sn FROM programs WHERE batch_id = ?', (batch_id,))
|
||
programs = cursor.fetchall()
|
||
for prog in programs:
|
||
cursor.execute('DELETE FROM scores WHERE program_sn = ?', (prog['sn'],))
|
||
cursor.execute('DELETE FROM programs WHERE batch_id = ?', (batch_id,))
|
||
return {'success': True}
|
||
|
||
|
||
# ==================== 评委管理API ====================
|
||
@app.get('/api/judges')
|
||
async def get_judges():
|
||
with get_db() as conn:
|
||
cursor = conn.cursor()
|
||
cursor.execute('SELECT id, name, order_num FROM judges ORDER BY order_num')
|
||
judges = [
|
||
{'id': row['id'], 'name': row['name'], 'order_num': row['order_num']}
|
||
for row in cursor.fetchall()
|
||
]
|
||
return judges
|
||
|
||
|
||
@app.post('/api/judges')
|
||
async def create_judge(judge: JudgeCreate):
|
||
with get_db() as conn:
|
||
cursor = conn.cursor()
|
||
cursor.execute('SELECT id FROM judges WHERE id = ?', (judge.id,))
|
||
if cursor.fetchone():
|
||
raise HTTPException(status_code=400, detail='评委编号已存在')
|
||
cursor.execute('SELECT MAX(order_num) FROM judges')
|
||
max_order = cursor.fetchone()[0] or 0
|
||
cursor.execute(
|
||
'INSERT INTO judges (id, name, order_num, created_at) VALUES (?, ?, ?, ?)',
|
||
(judge.id, judge.name, max_order + 1, datetime.now()),
|
||
)
|
||
cursor.execute('SELECT sn FROM programs')
|
||
for prog in cursor.fetchall():
|
||
cursor.execute(
|
||
'INSERT OR IGNORE INTO scores (judge_id, program_sn, updated_at) VALUES (?, ?, ?)',
|
||
(judge.id, prog['sn'], datetime.now()),
|
||
)
|
||
return {'success': True}
|
||
|
||
|
||
@app.put('/api/judges/{judge_id}')
|
||
async def update_judge(judge_id: str, judge: JudgeUpdate):
|
||
with get_db() as conn:
|
||
cursor = conn.cursor()
|
||
cursor.execute('SELECT id FROM judges WHERE id = ?', (judge_id,))
|
||
if not cursor.fetchone():
|
||
raise HTTPException(status_code=404, detail='评委不存在')
|
||
cursor.execute(
|
||
'UPDATE judges SET name = ? WHERE id = ?', (judge.name, judge_id)
|
||
)
|
||
return {'success': True}
|
||
|
||
|
||
@app.delete('/api/judges/{judge_id}')
|
||
async def delete_judge(judge_id: str):
|
||
with get_db() as conn:
|
||
cursor = conn.cursor()
|
||
cursor.execute('SELECT id FROM judges WHERE id = ?', (judge_id,))
|
||
if not cursor.fetchone():
|
||
raise HTTPException(status_code=404, detail='评委不存在')
|
||
cursor.execute('DELETE FROM scores WHERE judge_id = ?', (judge_id,))
|
||
cursor.execute('DELETE FROM judges WHERE id = ?', (judge_id,))
|
||
return {'success': True}
|
||
|
||
|
||
# ==================== 节目管理API ====================
|
||
@app.get('/api/programs')
|
||
async def get_programs(batch_id: int | None = Query(None)):
|
||
with get_db() as conn:
|
||
cursor = conn.cursor()
|
||
if batch_id:
|
||
cursor.execute(
|
||
'SELECT sn, name, unit, performer, category, batch_id, order_num FROM programs WHERE batch_id = ? ORDER BY order_num',
|
||
(batch_id,),
|
||
)
|
||
else:
|
||
cursor.execute(
|
||
'SELECT sn, name, unit, performer, category, batch_id, order_num FROM programs ORDER BY batch_id, order_num'
|
||
)
|
||
programs = [
|
||
{
|
||
'sn': row['sn'],
|
||
'name': row['name'],
|
||
'unit': row['unit'],
|
||
'performer': row['performer'],
|
||
'category': row['category'],
|
||
'batch_id': row['batch_id'],
|
||
'order_num': row['order_num'],
|
||
}
|
||
for row in cursor.fetchall()
|
||
]
|
||
return programs
|
||
|
||
|
||
@app.post('/api/programs')
|
||
async def create_program(program: ProgramCreate):
|
||
with get_db() as conn:
|
||
cursor = conn.cursor()
|
||
cursor.execute('SELECT sn FROM programs WHERE sn = ?', (program.sn,))
|
||
if cursor.fetchone():
|
||
raise HTTPException(status_code=400, detail='节目序号已存在')
|
||
cursor.execute(
|
||
'SELECT MAX(order_num) FROM programs WHERE batch_id = ?',
|
||
(program.batch_id,),
|
||
)
|
||
max_order = cursor.fetchone()[0]
|
||
if max_order is None:
|
||
max_order = 0
|
||
cursor.execute(
|
||
'INSERT INTO programs (sn, name, unit, performer, category, batch_id, order_num, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
|
||
(
|
||
program.sn,
|
||
program.name,
|
||
program.unit,
|
||
program.performer,
|
||
program.category,
|
||
program.batch_id,
|
||
max_order + 1,
|
||
datetime.now(),
|
||
),
|
||
)
|
||
cursor.execute('SELECT id FROM judges')
|
||
for judge in cursor.fetchall():
|
||
cursor.execute(
|
||
'INSERT OR IGNORE INTO scores (judge_id, program_sn, updated_at) VALUES (?, ?, ?)',
|
||
(judge['id'], program.sn, datetime.now()),
|
||
)
|
||
return {'success': True}
|
||
|
||
|
||
@app.post('/api/programs/import')
|
||
async def import_programs(batch_id: int = Query(...), file: UploadFile = File(...)):
|
||
try:
|
||
contents = await file.read()
|
||
df = pd.read_excel(BytesIO(contents))
|
||
|
||
required_columns = ['节目序号', '节目名称', '演出单位', '表演者', '节目类别']
|
||
for col in required_columns:
|
||
if col not in df.columns:
|
||
raise HTTPException(status_code=400, detail=f'Excel缺少必要列:{col}')
|
||
|
||
with get_db() as conn:
|
||
cursor = conn.cursor()
|
||
cursor.execute(
|
||
'SELECT MAX(order_num) FROM programs WHERE batch_id = ?', (batch_id,)
|
||
)
|
||
max_order_result = cursor.fetchone()[0]
|
||
start_order = max_order_result if max_order_result is not None else 0
|
||
|
||
for idx, row in df.iterrows():
|
||
sn = str(row['节目序号']).strip()
|
||
name = str(row['节目名称']).strip()
|
||
unit = str(row['演出单位']).strip() if pd.notna(row['演出单位']) else ''
|
||
performer = (
|
||
str(row['表演者']).strip() if pd.notna(row['表演者']) else ''
|
||
)
|
||
category = (
|
||
str(row['节目类别']).strip()
|
||
if pd.notna(row['节目类别'])
|
||
else '舞蹈'
|
||
)
|
||
|
||
if not sn or not name:
|
||
continue
|
||
|
||
idx = cast(int, idx)
|
||
current_order = start_order + idx + 1
|
||
cursor.execute('SELECT sn FROM programs WHERE sn = ?', (sn,))
|
||
if cursor.fetchone():
|
||
cursor.execute(
|
||
'UPDATE programs SET name=?, unit=?, performer=?, category=?, batch_id=?, order_num=? WHERE sn=?',
|
||
(name, unit, performer, category, batch_id, current_order, sn),
|
||
)
|
||
else:
|
||
cursor.execute(
|
||
'INSERT INTO programs (sn, name, unit, performer, category, batch_id, order_num, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
|
||
(
|
||
sn,
|
||
name,
|
||
unit,
|
||
performer,
|
||
category,
|
||
batch_id,
|
||
current_order,
|
||
datetime.now(),
|
||
),
|
||
)
|
||
cursor.execute('SELECT id FROM judges')
|
||
for judge in cursor.fetchall():
|
||
cursor.execute(
|
||
'INSERT OR IGNORE INTO scores (judge_id, program_sn, updated_at) VALUES (?, ?, ?)',
|
||
(judge['id'], sn, datetime.now()),
|
||
)
|
||
|
||
return {'success': True, 'message': f'成功导入/更新 {df.shape[0]} 个节目'}
|
||
except Exception as e:
|
||
raise HTTPException(status_code=400, detail=f'导入失败:{str(e)}') from e
|
||
|
||
|
||
@app.put('/api/programs/{program_sn}')
|
||
async def update_program(program_sn: str, program: ProgramUpdate):
|
||
with get_db() as conn:
|
||
cursor = conn.cursor()
|
||
cursor.execute('SELECT sn FROM programs WHERE sn = ?', (program_sn,))
|
||
if not cursor.fetchone():
|
||
raise HTTPException(status_code=404, detail='节目不存在')
|
||
cursor.execute(
|
||
'UPDATE programs SET name = ?, unit = ?, performer = ?, category = ?, batch_id = ? WHERE sn = ?',
|
||
(
|
||
program.name,
|
||
program.unit,
|
||
program.performer,
|
||
program.category,
|
||
program.batch_id,
|
||
program_sn,
|
||
),
|
||
)
|
||
return {'success': True}
|
||
|
||
|
||
@app.delete('/api/programs/{program_sn}')
|
||
async def delete_program(program_sn: str):
|
||
with get_db() as conn:
|
||
cursor = conn.cursor()
|
||
cursor.execute('SELECT sn FROM programs WHERE sn = ?', (program_sn,))
|
||
if not cursor.fetchone():
|
||
raise HTTPException(status_code=404, detail='节目不存在')
|
||
cursor.execute('DELETE FROM scores WHERE program_sn = ?', (program_sn,))
|
||
cursor.execute('DELETE FROM programs WHERE sn = ?', (program_sn,))
|
||
return {'success': True}
|
||
|
||
|
||
# ==================== 评分API ====================
|
||
@app.get('/api/scores/{judge_id}')
|
||
async def get_judge_scores(judge_id: str):
|
||
with get_db() as conn:
|
||
cursor = conn.cursor()
|
||
cursor.execute(
|
||
'SELECT program_sn, content_score, stage_score, art_score, overall_score, comment FROM scores WHERE judge_id = ?',
|
||
(judge_id,),
|
||
)
|
||
rows = cursor.fetchall()
|
||
scores = {}
|
||
for row in rows:
|
||
scores[row['program_sn']] = {
|
||
'content': row['content_score'],
|
||
'stage': row['stage_score'],
|
||
'art': row['art_score'],
|
||
'overall': row['overall_score'],
|
||
'comment': row['comment'] or '',
|
||
}
|
||
cursor.execute('SELECT sn FROM programs')
|
||
for row in cursor.fetchall():
|
||
if row['sn'] not in scores:
|
||
scores[row['sn']] = {
|
||
'content': None,
|
||
'stage': None,
|
||
'art': None,
|
||
'overall': None,
|
||
'comment': '',
|
||
}
|
||
return scores
|
||
|
||
|
||
@app.post('/api/scores/{judge_id}')
|
||
async def update_score(judge_id: str, score_data: ScoreDetailUpdate):
|
||
max_scores = {'content': 20, 'stage': 10, 'art': 60, 'overall': 10}
|
||
if score_data.field not in max_scores:
|
||
raise HTTPException(status_code=400, detail='无效评分项')
|
||
val = score_data.value
|
||
if val is not None and (val < 0 or val > max_scores[score_data.field]):
|
||
raise HTTPException(
|
||
status_code=400, detail=f'分值必须在0-{max_scores[score_data.field]}之间'
|
||
)
|
||
field_map = {
|
||
'content': 'content_score',
|
||
'stage': 'stage_score',
|
||
'art': 'art_score',
|
||
'overall': 'overall_score',
|
||
}
|
||
with get_db() as conn:
|
||
cursor = conn.cursor()
|
||
cursor.execute(
|
||
f"""
|
||
INSERT INTO scores (judge_id, program_sn, {field_map[score_data.field]}, updated_at)
|
||
VALUES (?, ?, ?, ?)
|
||
ON CONFLICT(judge_id, program_sn) DO UPDATE SET
|
||
{field_map[score_data.field]} = excluded.{field_map[score_data.field]},
|
||
updated_at = excluded.updated_at
|
||
""",
|
||
(judge_id, score_data.program_sn, val, datetime.now()),
|
||
)
|
||
cursor.execute(
|
||
'SELECT content_score, stage_score, art_score, overall_score FROM scores WHERE judge_id = ? AND program_sn = ?',
|
||
(judge_id, score_data.program_sn),
|
||
)
|
||
row = cursor.fetchone()
|
||
total = 0
|
||
if row:
|
||
for v in [
|
||
row['content_score'],
|
||
row['stage_score'],
|
||
row['art_score'],
|
||
row['overall_score'],
|
||
]:
|
||
if v is not None:
|
||
total += v
|
||
return {'success': True, 'total': total}
|
||
|
||
|
||
@app.post('/api/comment/{judge_id}')
|
||
async def update_comment(judge_id: str, comment_data: CommentUpdate):
|
||
with get_db() as conn:
|
||
cursor = conn.cursor()
|
||
cursor.execute(
|
||
'INSERT INTO scores (judge_id, program_sn, comment, updated_at) VALUES (?, ?, ?, ?) ON CONFLICT(judge_id, program_sn) DO UPDATE SET comment = excluded.comment, updated_at = excluded.updated_at',
|
||
(judge_id, comment_data.program_sn, comment_data.comment, datetime.now()),
|
||
)
|
||
return {'success': True}
|
||
|
||
|
||
@app.post('/api/scores/admin/batch')
|
||
async def batch_update_scores(batch_data: ScoreBatchUpdate):
|
||
"""管理员批量更新总分"""
|
||
with get_db() as conn:
|
||
cursor = conn.cursor()
|
||
for judge_id, total_score in batch_data.judge_scores.items():
|
||
if total_score is not None and (total_score < 0 or total_score > 100):
|
||
raise HTTPException(status_code=400, detail='总分必须在0-100之间')
|
||
|
||
if total_score is None:
|
||
cursor.execute(
|
||
"""
|
||
UPDATE scores SET content_score = NULL, stage_score = NULL, art_score = NULL, overall_score = NULL, updated_at = ?
|
||
WHERE judge_id = ? AND program_sn = ?
|
||
""",
|
||
(datetime.now(), judge_id, batch_data.program_sn),
|
||
)
|
||
else:
|
||
cursor.execute(
|
||
"""
|
||
INSERT INTO scores (judge_id, program_sn, content_score, stage_score, art_score, overall_score, comment, updated_at)
|
||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||
ON CONFLICT(judge_id, program_sn) DO UPDATE SET
|
||
content_score = excluded.content_score,
|
||
stage_score = excluded.stage_score,
|
||
art_score = excluded.art_score,
|
||
overall_score = excluded.overall_score,
|
||
comment = excluded.comment,
|
||
updated_at = excluded.updated_at
|
||
""",
|
||
(
|
||
judge_id,
|
||
batch_data.program_sn,
|
||
total_score,
|
||
None,
|
||
None,
|
||
None,
|
||
'',
|
||
datetime.now(),
|
||
),
|
||
)
|
||
return {'success': True}
|
||
|
||
|
||
@app.get('/api/scores/program/{program_sn}')
|
||
async def get_program_scores(program_sn: str):
|
||
with get_db() as conn:
|
||
cursor = conn.cursor()
|
||
cursor.execute(
|
||
"""
|
||
SELECT j.id, j.name,
|
||
s.content_score, s.stage_score, s.art_score, s.overall_score,
|
||
s.comment
|
||
FROM judges j
|
||
LEFT JOIN scores s ON j.id = s.judge_id AND s.program_sn = ?
|
||
ORDER BY j.order_num
|
||
""",
|
||
(program_sn,),
|
||
)
|
||
scores = []
|
||
for row in cursor.fetchall():
|
||
total = None
|
||
if (
|
||
row['content_score'] is not None
|
||
or row['stage_score'] is not None
|
||
or row['art_score'] is not None
|
||
or row['overall_score'] is not None
|
||
):
|
||
total = 0
|
||
if row['content_score'] is not None:
|
||
total += row['content_score']
|
||
if row['stage_score'] is not None:
|
||
total += row['stage_score']
|
||
if row['art_score'] is not None:
|
||
total += row['art_score']
|
||
if row['overall_score'] is not None:
|
||
total += row['overall_score']
|
||
if total == 0 and row['content_score'] is not None:
|
||
total = row['content_score']
|
||
scores.append(
|
||
{'judge_id': row['id'], 'judge_name': row['name'], 'total_score': total}
|
||
)
|
||
return scores
|
||
|
||
|
||
# ==================== 汇总统计API ====================
|
||
@app.get('/api/aggregate')
|
||
async def get_aggregate(batch_id: int | None = Query(None)):
|
||
with get_db() as conn:
|
||
cursor = conn.cursor()
|
||
if batch_id:
|
||
cursor.execute(
|
||
'SELECT sn, name, unit, performer, category FROM programs WHERE batch_id = ? ORDER BY order_num',
|
||
(batch_id,),
|
||
)
|
||
else:
|
||
cursor.execute(
|
||
'SELECT sn, name, unit, performer, category FROM programs ORDER BY batch_id, order_num'
|
||
)
|
||
programs = cursor.fetchall()
|
||
results = []
|
||
for prog in programs:
|
||
cursor.execute(
|
||
"""
|
||
SELECT content_score, stage_score, art_score, overall_score
|
||
FROM scores WHERE program_sn = ?
|
||
""",
|
||
(prog['sn'],),
|
||
)
|
||
scores_list = []
|
||
for row in cursor.fetchall():
|
||
total = 0
|
||
if row['content_score'] is not None:
|
||
total += row['content_score']
|
||
if row['stage_score'] is not None:
|
||
total += row['stage_score']
|
||
if row['art_score'] is not None:
|
||
total += row['art_score']
|
||
if row['overall_score'] is not None:
|
||
total += row['overall_score']
|
||
if total != 0 or (total == 0 and row['content_score'] is not None):
|
||
scores_list.append(total)
|
||
if scores_list:
|
||
avg_score = round(sum(scores_list) / len(scores_list), 3)
|
||
max_score = max(scores_list)
|
||
min_score = min(scores_list)
|
||
else:
|
||
avg_score, max_score, min_score = 0, 0, 0
|
||
results.append(
|
||
{
|
||
'sn': prog['sn'],
|
||
'name': prog['name'],
|
||
'unit': prog['unit'],
|
||
'performer': prog['performer'],
|
||
'category': prog['category'],
|
||
'avg_score': avg_score,
|
||
'max_score': max_score,
|
||
'min_score': min_score,
|
||
'scored_count': len(scores_list),
|
||
}
|
||
)
|
||
results.sort(key=lambda x: x['avg_score'], reverse=True)
|
||
for idx, r in enumerate(results):
|
||
r['rank'] = idx + 1
|
||
return results
|
||
|
||
|
||
@app.get('/api/award_winners')
|
||
async def get_award_winners(batch_id: int | None = Query(None)):
|
||
aggregate = await get_aggregate(batch_id)
|
||
settings = await get_award_settings()
|
||
first_count, second_count, third_count = (
|
||
settings['first'],
|
||
settings['second'],
|
||
settings['third'],
|
||
)
|
||
winners = {'first': [], 'second': [], 'third': []}
|
||
for idx, prog in enumerate(aggregate):
|
||
if idx < first_count:
|
||
winners['first'].append(prog)
|
||
elif idx < first_count + second_count:
|
||
winners['second'].append(prog)
|
||
elif idx < first_count + second_count + third_count:
|
||
winners['third'].append(prog)
|
||
return winners
|
||
|
||
|
||
@app.get('/api/progress')
|
||
async def get_progress():
|
||
with get_db() as conn:
|
||
cursor = conn.cursor()
|
||
total_programs = cursor.execute('SELECT COUNT(*) FROM programs').fetchone()[0]
|
||
total_judges = cursor.execute('SELECT COUNT(*) FROM judges').fetchone()[0]
|
||
total_cells = total_programs * total_judges
|
||
scored_cells = cursor.execute(
|
||
'SELECT COUNT(*) FROM scores WHERE content_score IS NOT NULL'
|
||
).fetchone()[0]
|
||
progress = round(scored_cells / total_cells * 100, 1) if total_cells > 0 else 0
|
||
return {
|
||
'total_programs': total_programs,
|
||
'total_judges': total_judges,
|
||
'scored_programs': scored_cells,
|
||
'total_cells': total_cells,
|
||
'progress': progress,
|
||
}
|
||
|
||
|
||
@app.get('/api/export_excel')
|
||
async def export_excel(batch_id: int | None = Query(None)):
|
||
aggregate = await get_aggregate(batch_id)
|
||
data = [
|
||
{
|
||
'名次': item['rank'],
|
||
'节目序号': item['sn'],
|
||
'节目名称': item['name'],
|
||
'演出单位': item['unit'],
|
||
'表演者': item['performer'],
|
||
'节目类别': item['category'],
|
||
'平均分': item['avg_score'],
|
||
'最高分': item['max_score'],
|
||
'最低分': item['min_score'],
|
||
'评分评委数': item['scored_count'],
|
||
}
|
||
for item in aggregate
|
||
]
|
||
df = pd.DataFrame(data)
|
||
output = BytesIO()
|
||
with pd.ExcelWriter(output, engine='openpyxl') as writer:
|
||
df.to_excel(writer, sheet_name='评分汇总表', index=False)
|
||
output.seek(0)
|
||
filename = f'{"batch_" + str(batch_id) if batch_id else "all"}_scores.xlsx'
|
||
return StreamingResponse(
|
||
output,
|
||
media_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||
headers={'Content-Disposition': f'attachment; filename={filename}'},
|
||
)
|
||
|
||
|
||
# ==================== 打印页面API ====================
|
||
@app.get('/print/program/{program_sn}')
|
||
async def print_program_score(program_sn: str):
|
||
with get_db() as conn:
|
||
prog = conn.execute(
|
||
'SELECT sn, name, unit, performer, category FROM programs WHERE sn = ?',
|
||
(program_sn,),
|
||
).fetchone()
|
||
if not prog:
|
||
raise HTTPException(status_code=404, detail='节目不存在')
|
||
judges = conn.execute(
|
||
'SELECT id, name FROM judges ORDER BY order_num'
|
||
).fetchall()
|
||
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 = ?',
|
||
(judge['id'], program_sn),
|
||
).fetchone()
|
||
total = 0
|
||
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']
|
||
comment = score['comment'] or ''
|
||
if total > 0:
|
||
all_scores.append(total)
|
||
scores_data[judge['name']] = {'total': total, 'comment': comment}
|
||
max_score = max(all_scores) if all_scores else 0
|
||
min_score = min(all_scores) if all_scores else 0
|
||
avg_score = round(sum(all_scores) / len(all_scores), 3) if all_scores else 0
|
||
titles = await get_title_settings()
|
||
return HTMLResponse(
|
||
content=render_print_program_template(
|
||
prog, judges, scores_data, max_score, min_score, avg_score, titles
|
||
)
|
||
)
|
||
|
||
|
||
@app.get('/print/blank/{program_sn}')
|
||
async def print_blank_score(program_sn: str):
|
||
with get_db() as conn:
|
||
prog = conn.execute(
|
||
'SELECT sn, name, unit, performer, category FROM programs WHERE sn = ?',
|
||
(program_sn,),
|
||
).fetchone()
|
||
if not prog:
|
||
raise HTTPException(status_code=404, detail='节目不存在')
|
||
judges = conn.execute(
|
||
'SELECT id, name FROM judges ORDER BY order_num'
|
||
).fetchall()
|
||
criteria = await get_criteria()
|
||
titles = await get_title_settings()
|
||
return HTMLResponse(
|
||
content=render_print_blank_template(prog, judges, criteria, titles)
|
||
)
|
||
|
||
|
||
@app.get('/print/award/{award_type}')
|
||
async def print_award_list(award_type: str, batch_id: int | None = Query(None)):
|
||
winners = await get_award_winners(batch_id)
|
||
if award_type not in winners:
|
||
raise HTTPException(status_code=404, detail='奖项类型不存在')
|
||
award_settings = await get_award_settings()
|
||
award_names = {
|
||
'first': award_settings['first_name'],
|
||
'second': award_settings['second_name'],
|
||
'third': award_settings['third_name'],
|
||
}
|
||
titles = await get_title_settings()
|
||
batch_name = ''
|
||
if batch_id:
|
||
with get_db() as conn:
|
||
batch = conn.execute(
|
||
'SELECT name FROM batches WHERE id = ?', (batch_id,)
|
||
).fetchone()
|
||
if batch:
|
||
batch_name = f'({batch["name"]})'
|
||
return HTMLResponse(
|
||
content=render_print_award_template(
|
||
award_type, award_names[award_type], winners[award_type], titles, batch_name
|
||
)
|
||
)
|
||
|
||
|
||
def render_print_program_template(
|
||
program, judges, scores_data, max_score, min_score, avg_score, titles
|
||
):
|
||
html = f"""<!DOCTYPE html><html><head><meta charset="UTF-8"><title>{program['name']} - 评分详情</title>
|
||
<style>*{{margin:0;padding:0;box-sizing:border-box;}} body{{font-family: SimSun, 'Microsoft YaHei', serif; padding: 20px;}} .container{{max-width: 1000px; margin: 0 auto;}} h1{{text-align: center; margin-bottom: 5px; font-size: 22px;}} .subtitle{{text-align: center; color: #666; margin-bottom: 20px; font-size: 14px;}} .info{{margin-bottom: 20px; padding: 15px; background: #f5f5f5; border-radius: 8px;}} .info p{{margin: 5px 0;}} .stat-box{{display: flex; gap: 20px; justify-content: center; margin-bottom: 20px;}} .stat-item{{text-align: center; padding: 10px 20px; background: #e8f4e8; border-radius: 8px;}} .stat-item span{{font-size: 24px; font-weight: bold;}} table{{width: 100%; border-collapse: collapse; margin-top: 20px;}} th, td{{border: 1px solid #000; padding: 12px; text-align: center;}} th{{background: #f0f0f0;}} .comment-cell{{text-align: left;}} @media print{{.no-print{{display: none;}}}}</style></head>
|
||
<body><div class="container"><h1>{titles['main_title']}</h1><div class="subtitle">{titles['sub_title']} · 评分详情表</div>
|
||
<div class="info"><p><strong>节目序号:</strong> {program['sn']}</p><p><strong>节目名称:</strong> {program['name']}</p><p><strong>演出单位:</strong> {program['unit']}</p><p><strong>表演者:</strong> {program['performer']}</p><p><strong>节目类别:</strong> {program['category']}</p></div>
|
||
<div class="stat-box"><div class="stat-item">📊 平均分<br><span>{avg_score}</span></div><div class="stat-item">🎯 最高分<br><span>{max_score}</span></div><div class="stat-item">⭐ 最低分<br><span>{min_score}</span></div></div>
|
||
<table><thead><tr><th>评委姓名</th><th>总分(100分)</th><th>评委点评</th></tr></thead><tbody>"""
|
||
for judge in judges:
|
||
s = scores_data.get(judge['name'], {})
|
||
html += f"""<tr><td style="font-weight:bold;">{judge['name']}</td><td class="total">{s.get('total') if s.get('total') > 0 else '-'}</td><td class="comment-cell">{s.get('comment', '')}</td></tr>"""
|
||
html += f"""</tbody></table><div style="margin-top: 30px;"><p>评委组签名:____________________</p><p style="text-align: right;">日期:{datetime.now().strftime('%Y年%m月%d日')}</p></div>
|
||
<div class="no-print" style="margin-top: 30px; text-align: center;"><button onclick="window.print()">🖨️ 打印本页</button> <button onclick="window.close()">关闭</button></div></div></body></html>"""
|
||
return html
|
||
|
||
|
||
def render_print_blank_template(program, judges, criteria, titles):
|
||
html = f"""<!DOCTYPE html><html><head><meta charset="UTF-8"><title>{program['name']} - 空白评分表</title>
|
||
<style>*{{margin:0;padding:0;box-sizing:border-box;}} body{{font-family: SimSun, 'Microsoft YaHei', serif;}} .judge-sheet{{width:100%; max-width:800px; margin:0 auto; padding:30px 20px 40px; page-break-after:always; border-bottom:2px dashed #ccc;}} .judge-sheet:last-child{{page-break-after:auto; border-bottom:none;}} .sheet-top-space{{height:25mm;}} .sheet-bottom-space{{height:20mm;}} h1{{text-align:center; margin-bottom:5px; font-size:22px;}} .subtitle{{text-align:center; color:#666; margin-bottom:15px;}} .program-info{{margin-bottom:20px; padding:12px; background:#f5f5f5; border-radius:8px;}} .judge-title{{font-size:16px; font-weight:bold; margin:15px 0 12px; padding:8px 15px; background:#e8f4e8; border-left:4px solid #48bb78;}} table{{width:100%; border-collapse:collapse; margin:15px 0;}} th,td{{border:1px solid #000; padding:10px; text-align:center;}} th{{background:#f0f0f0;}} .score-desc{{text-align:left; font-size:12px;}} .sign-line{{margin-top:25px; display:flex; justify-content:space-between;}} @media print{{.no-print{{display:none;}} .judge-sheet{{padding:20px 15px 30px; page-break-after:always; border-bottom:none;}}}}</style>
|
||
<body>"""
|
||
for _judge in judges:
|
||
html += f"""<div class="judge-sheet"><div class="sheet-top-space"></div>
|
||
<h1>{titles['main_title']}</h1><div class="subtitle">{titles['sub_title']} · 节目评分表</div>
|
||
<div class="program-info"><p><strong>节目序号:</strong> {program['sn']} | <strong>节目名称:</strong> {program['name']}</p>
|
||
<p><strong>演出单位:</strong> {program['unit']} | <strong>表演者:</strong> {program['performer']} | <strong>节目类别:</strong> {program['category']}</p></div>
|
||
<table><thead><tr><th style="width:20%">评分项目</th><th style="width:10%">满分</th><th style="width:15%">得分</th><th style="width:55%">评分要点</th></tr></thead><tbody>"""
|
||
for _key, cri in criteria.items():
|
||
html += f"""<tr><td style="font-weight:bold;">{cri['label']}</td><td style="text-align:center;">{cri['max']}分</td><td class="score-blank">______</td><td class="score-desc">{cri['description']}</td></tr>"""
|
||
html += """<tr style="background:#f9f9f9;"><td style="font-weight:bold;">合计</td><td style="text-align:center;">100分</td><td class="score-blank">______</td><td class="score-desc"></td><tr>
|
||
</tbody></table>
|
||
<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>'
|
||
''
|
||
)
|
||
return html
|
||
|
||
|
||
def render_print_award_template(award_type, award_name, winners, titles, batch_name=''):
|
||
html = f"""<!DOCTYPE html><html><head><meta charset="UTF-8"><title>{award_name}{batch_name}获奖名单</title>
|
||
<style>
|
||
*{{margin:0;padding:0;box-sizing:border-box;}}
|
||
body{{font-family: SimSun, 'Microsoft YaHei', serif; padding: 20px;}}
|
||
.container{{max-width: 800px; margin: 0 auto;}}
|
||
h1{{text-align: center; font-size: 22px; margin-bottom: 5px;}}
|
||
.subtitle{{text-align: center; color: #666; margin-bottom: 20px;}}
|
||
.award-title{{text-align: center; font-size: 36px; font-weight: bold; margin: 20px 0; padding: 15px; background: linear-gradient(135deg, #667eea, #764ba2); color: white; border-radius: 12px;}}
|
||
table{{width: 100%; border-collapse: collapse; margin-top: 20px;}}
|
||
th, td{{border: 1px solid #000; padding: 12px; text-align: center;}}
|
||
th{{background: #f0f0f0;}}
|
||
.sign-line{{margin-top: 40px; display: flex; justify-content: space-between;}}
|
||
@media print{{.no-print{{display: none;}}}}
|
||
</style>
|
||
<body><div class="container"><h1>{titles['main_title']}</h1><div class="subtitle">{titles['sub_title']}{batch_name}</div>
|
||
<div class="award-title">{award_name}</div>
|
||
<table>
|
||
<thead>
|
||
<tr><th>名次</th><th>节目序号</th><th>节目名称</th><th>演出单位</th><th>表演者</th><th>节目类别</th><th>平均分</th></tr>
|
||
</thead>
|
||
<tbody>"""
|
||
for idx, prog in enumerate(winners, 1):
|
||
html += f"""<tr><td style="text-align:center;">{idx}</td><td style="text-align:center;">{prog['sn']}</td><td style="text-align:left;">{prog['name']}</td><td style="text-align:left;">{prog['unit']}</td><td style="text-align:left;">{prog['performer']}</td><td style="text-align:center;">{prog['category']}</td><td style="text-align:center;">{prog['avg_score']}</td></tr>"""
|
||
html += f"""</tbody>
|
||
</table>
|
||
<div class="sign-line"><span>评委组签名:____________________</span><span>日期:{datetime.now().strftime('%Y年%m月%d日')}</span></div>
|
||
<div class="no-print" style="margin-top: 30px; text-align: center;"><button onclick="window.print()">🖨️ 打印本页</button> <button onclick="window.close()">关闭</button></div></div></body></html>"""
|
||
return html
|
||
|
||
|
||
# ==================== HTML模板(前端部分) ====================
|
||
HTML_TEMPLATE = """
|
||
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>文艺汇演评分系统</title>
|
||
<style>
|
||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||
body { font-family: 'Microsoft YaHei', sans-serif; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 100vh; padding: 20px; }
|
||
.container { max-width: 1400px; margin: 0 auto; }
|
||
.clearfix { clear: both; }
|
||
.header, .tabs, .batch-bar, .program-list-sidebar, .score-panel, .summary-panel,
|
||
.admin-panel, .settings-panel, .judge-scoring-container, .admin-scoring-container,
|
||
.programs-sidebar, .scoring-panel { clear: both; }
|
||
|
||
.header { background: rgba(255,255,255,0.95); border-radius: 20px; padding: 20px 30px; margin-bottom: 25px; box-shadow: 0 10px 40px rgba(0,0,0,0.1); }
|
||
.header h1 { font-size: 28px; color: #2d3748; border-left: 5px solid #667eea; padding-left: 20px; }
|
||
.header .subtitle { color: #718096; padding-left: 25px; font-size: 14px; margin-top: 8px; }
|
||
.login-bar { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 15px; margin-top: 15px; padding-top: 15px; border-top: 1px solid #e2e8f0; }
|
||
.login-area, .user-info-area { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
|
||
.btn { padding: 6px 16px; border: none; border-radius: 6px; cursor: pointer; font-weight: 500; transition: all 0.2s; font-size: 13px; line-height: 1.5; }
|
||
.btn-primary { background: #667eea; color: white; }
|
||
.btn-primary:hover { background: #5a67d8; }
|
||
.btn-success { background: #48bb78; color: white; }
|
||
.btn-success:hover { background: #38a169; }
|
||
.btn-warning { background: #ed8936; color: white; }
|
||
.btn-outline { background: white; border: 1px solid #cbd5e1; color: #4a5568; }
|
||
.btn-danger { background: #e53e3e; color: white; }
|
||
.btn-sm { padding: 4px 12px; font-size: 12px; }
|
||
|
||
.judge-select { padding: 6px 12px; border: 1px solid #cbd5e1; border-radius: 6px; font-size: 14px; background: white; min-width: 150px; cursor: pointer; }
|
||
.judge-select:focus { outline: none; border-color: #667eea; box-shadow: 0 0 0 2px rgba(102,126,234,0.2); }
|
||
|
||
.admin-input { padding: 6px 12px; border: 1px solid #cbd5e1; border-radius: 6px; font-size: 14px; }
|
||
.admin-input:focus { outline: none; border-color: #667eea; box-shadow: 0 0 0 2px rgba(102,126,234,0.2); }
|
||
|
||
.user-info { background: #edf2f7; padding: 6px 16px; border-radius: 30px; font-size: 14px; }
|
||
.admin-info { background: #fef5e7; color: #c05621; padding: 6px 16px; border-radius: 30px; font-size: 14px; }
|
||
.progress-bar { background: #e2e8f0; border-radius: 20px; height: 8px; margin: 12px 0 5px; overflow: hidden; }
|
||
.progress-fill { background: linear-gradient(90deg, #48bb78, #38a169); height: 100%; width: 0%; transition: width 0.3s; }
|
||
.tabs { display: flex; gap: 10px; margin-bottom: 20px; flex-wrap: wrap; }
|
||
.tab { padding: 8px 20px; cursor: pointer; border-radius: 20px; background: rgba(255,255,255,0.8); }
|
||
.tab.active { background: #667eea; color: white; }
|
||
.mode-badge { display: inline-block; padding: 4px 12px; border-radius: 20px; font-size: 12px; margin-left: 10px; }
|
||
.mode-judge { background: #48bb78; color: white; }
|
||
.mode-admin { background: #ed8936; color: white; }
|
||
.batch-bar { display: flex; gap: 10px; margin-bottom: 15px; align-items: center; flex-wrap: wrap; }
|
||
.batch-btn { padding: 6px 15px; border: none; border-radius: 20px; cursor: pointer; background: #e2e8f0; font-size: 13px; }
|
||
.batch-btn.active { background: #667eea; color: white; }
|
||
.judge-scoring-container, .admin-scoring-container { background: white; border-radius: 20px; padding: 20px; overflow-x: auto; margin-bottom: 20px; }
|
||
.program-list-sidebar { flex: 1; min-width: 250px; background: #f7fafc; border-radius: 16px; padding: 15px; max-height: 600px; overflow-y: auto; }
|
||
.program-list-sidebar h4 { margin-bottom: 10px; }
|
||
.program-item { padding: 10px; margin: 5px 0; background: white; border-radius: 8px; cursor: pointer; transition: all 0.2s; border-left: 3px solid transparent; }
|
||
.program-item:hover { background: #e0e7ff; transform: translateX(3px); }
|
||
.program-item.active { background: #e0e7ff; border-left-color: #667eea; font-weight: bold; }
|
||
.score-panel { flex: 2; }
|
||
.scoring-table { width: 100%; border-collapse: collapse; }
|
||
.scoring-table th, .scoring-table td { border: 1px solid #e2e8f0; padding: 10px 6px; text-align: center; font-size: 13px; }
|
||
.scoring-table th { background: #f7fafc; font-weight: 600; }
|
||
.score-input { width: 70px; padding: 4px 6px; text-align: center; border: 1px solid #cbd5e1; border-radius: 4px; font-size: 13px; }
|
||
.detail-score-input { width: 70px; padding: 6px; text-align: center; border: 1px solid #cbd5e1; border-radius: 6px; }
|
||
.total-score { font-weight: bold; font-size: 18px; margin: 10px 0; text-align: center; padding: 10px; background: linear-gradient(135deg, #667eea, #764ba2); color: white; border-radius: 12px; }
|
||
.summary-panel { margin-top: 30px; background: white; border-radius: 20px; padding: 20px; overflow-x: auto; }
|
||
.summary-table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
||
.summary-table th, .summary-table td { padding: 10px 8px; text-align: center; border-bottom: 1px solid #e2e8f0; }
|
||
.summary-table th { background: #f7fafc; }
|
||
.rank-1 { background: #fef5e7; font-weight: bold; }
|
||
.award-table-container { max-height: 400px; overflow-y: auto; margin-top: 15px; }
|
||
.award-table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
||
.award-table th, .award-table td { padding: 10px 8px; text-align: center; border: 1px solid #e2e8f0; }
|
||
.award-table th { background: #f7fafc; }
|
||
.admin-panel, .settings-panel { background: white; border-radius: 20px; padding: 20px; margin-bottom: 20px; }
|
||
.print-buttons { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 15px; }
|
||
.modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 1000; justify-content: center; align-items: center; }
|
||
.modal-content { background: white; padding: 25px; border-radius: 20px; width: 90%; max-width: 500px; }
|
||
.modal-content input, .modal-content select { width: 100%; padding: 10px; margin-bottom: 15px; border: 1px solid #cbd5e1; border-radius: 8px; }
|
||
.batch-list-item, .judge-list-item, .program-list-item { display: flex; justify-content: space-between; align-items: center; padding: 10px 0; border-bottom: 1px solid #eee; clear: both; }
|
||
.batch-list-item span, .judge-list-item span, .program-list-item span { font-size: 14px; }
|
||
.batch-buttons, .judge-buttons, .program-buttons { display: flex; gap: 8px; }
|
||
.toast { position: fixed; bottom: 20px; right: 20px; background: #333; color: white; padding: 12px 20px; border-radius: 8px; animation: fadeOut 2s ease; z-index: 1000; }
|
||
.col-operation {
|
||
display: none;
|
||
}
|
||
body.print-mode .col-operation {
|
||
display: table-cell;
|
||
}
|
||
@keyframes fadeOut { 0% { opacity: 1; } 70% { opacity: 1; } 100% { opacity: 0; visibility: hidden; } }
|
||
@media (max-width: 768px) {
|
||
.judge-scoring-container > div {
|
||
flex-direction: column !important;
|
||
}
|
||
.judge-scoring-container .program-list-sidebar {
|
||
width: 100%;
|
||
max-height: 300px;
|
||
overflow-y: auto;
|
||
margin-bottom: 16px;
|
||
}
|
||
.judge-scoring-container .score-panel {
|
||
width: 100%;
|
||
}
|
||
#judgeScorePanel [style*="display: grid"] {
|
||
grid-template-columns: 1fr !important;
|
||
gap: 12px !important;
|
||
}
|
||
.score-input, .detail-score-input, .admin-input, .judge-select {
|
||
width: 100%;
|
||
box-sizing: border-box;
|
||
}
|
||
.scoring-table, .summary-table {
|
||
display: block;
|
||
overflow-x: auto;
|
||
white-space: nowrap;
|
||
}
|
||
.batch-bar {
|
||
flex-wrap: wrap;
|
||
}
|
||
.batch-btn {
|
||
margin-bottom: 8px;
|
||
}
|
||
.header h1 {
|
||
font-size: 20px;
|
||
}
|
||
.login-area {
|
||
flex-wrap: wrap;
|
||
}
|
||
.login-area .admin-input,
|
||
.login-area .judge-select,
|
||
.login-area .btn {
|
||
width: 100%;
|
||
margin-bottom: 8px;
|
||
}
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
<div class="header">
|
||
<h1 id="mainTitle">加载中...</h1>
|
||
<div class="subtitle" id="subTitle"></div>
|
||
<div class="login-bar">
|
||
<div class="login-area" id="loginArea">
|
||
<select id="judgeSelect" class="judge-select"><option value="">选择评委</option></select>
|
||
<button class="btn btn-primary" onclick="loginAsJudge()">评委登录</button>
|
||
<span style="margin:0 5px">或</span>
|
||
<input type="text" id="adminUsername" placeholder="管理员账号" class="admin-input" style="width:100px;">
|
||
<input type="password" id="adminPassword" placeholder="密码" class="admin-input" style="width:100px;">
|
||
<button class="btn btn-warning" onclick="loginAsAdmin()">管理员登录</button>
|
||
</div>
|
||
<div class="user-info-area" id="userInfoArea" style="display: none;">
|
||
<span id="userInfo" class="user-info"></span>
|
||
<button class="btn btn-outline btn-sm" onclick="logout()">退出</button>
|
||
<button id="editTitleBtn" class="btn btn-primary btn-sm" onclick="showTitleModal()" style="display:none;">✏️ 修改标题</button>
|
||
</div>
|
||
</div>
|
||
<div class="progress-bar"><div class="progress-fill" id="progressFill"></div></div>
|
||
<div id="progressText" style="font-size:12px;"></div>
|
||
</div>
|
||
|
||
<div class="tabs">
|
||
<div class="tab active" onclick="switchTab('scoring')">🎯 评分打分</div>
|
||
<div class="tab" id="adminTab" style="display:none;" onclick="switchTab('admin')">⚙️ 数据管理</div>
|
||
<div class="tab" id="awardTab" style="display:none;" onclick="switchTab('award')">🏆 奖项设置</div>
|
||
<div class="tab" onclick="switchTab('print')">🖨️ 打印中心</div>
|
||
</div>
|
||
|
||
<!-- 评委评分页面 -->
|
||
<div id="judgeScoringTab" style="display:block;">
|
||
<div class="batch-bar" id="judgeBatchBar"></div>
|
||
<div class="judge-scoring-container" id="judgeScoringContainer">
|
||
<div style="text-align:center;padding:40px;">请先登录选择节目评分</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 管理员批量评分页面 -->
|
||
<div id="adminScoringTab" style="display:none;">
|
||
<div class="batch-bar" id="adminBatchBar"></div>
|
||
<div class="admin-scoring-container" id="adminScoringContainer"></div>
|
||
</div>
|
||
|
||
<!-- 数据管理页面 -->
|
||
<div id="adminTabContent" style="display:none;">
|
||
<div class="batch-bar" id="configBatchBar"></div>
|
||
<div style="display: flex; gap: 20px; flex-wrap: wrap;">
|
||
<div class="admin-panel" style="flex:1;">
|
||
<h3>👥 评委管理 <button class="btn btn-primary btn-sm" onclick="showAddJudgeModal()" style="float:right">+ 添加评委</button></h3>
|
||
<div id="judgeManageList"></div>
|
||
</div>
|
||
<div class="admin-panel" style="flex:1;">
|
||
<h3>📦 批次管理 <button class="btn btn-primary btn-sm" onclick="showAddBatchModal()" style="float:right">+ 添加批次</button></h3>
|
||
<div id="batchList"></div>
|
||
</div>
|
||
<div class="admin-panel" style="flex:2;">
|
||
<h3>📋 节目管理 <button class="btn btn-primary btn-sm" onclick="showAddProgramModal()" style="float:right">+ 添加节目</button>
|
||
<button class="btn btn-primary btn-sm" onclick="showImportProgramModal()" style="float:right;margin-right:10px;">📎 Excel导入</button>
|
||
</h3>
|
||
<div id="programManageContainer"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 奖项设置页面 -->
|
||
<div id="awardTabContent" style="display:none;">
|
||
<div class="batch-bar" id="awardBatchBar"></div>
|
||
<div class="settings-panel">
|
||
<h3>🏆 奖项数量设置</h3>
|
||
<div style="display: flex; gap: 20px; flex-wrap: wrap; align-items: center;">
|
||
<div><label>一等奖数量:</label><input type="number" id="awardFirst" style="width:80px"></div>
|
||
<div><label>二等奖数量:</label><input type="number" id="awardSecond" style="width:80px"></div>
|
||
<div><label>三等奖数量:</label><input type="number" id="awardThird" style="width:80px"></div>
|
||
<button class="btn btn-primary btn-sm" onclick="saveAwardSettings()">保存数量</button>
|
||
</div>
|
||
</div>
|
||
<div class="settings-panel">
|
||
<h3>🏷️ 奖项名称设置</h3>
|
||
<div style="display: flex; gap: 20px; flex-wrap: wrap; align-items: center;">
|
||
<div><label>一等奖名称:</label><input type="text" id="awardFirstName" style="width:100px"></div>
|
||
<div><label>二等奖名称:</label><input type="text" id="awardSecondName" style="width:100px"></div>
|
||
<div><label>三等奖名称:</label><input type="text" id="awardThirdName" style="width:100px"></div>
|
||
<button class="btn btn-primary btn-sm" onclick="saveAwardNames()">保存名称</button>
|
||
</div>
|
||
</div>
|
||
<div class="settings-panel">
|
||
<h3>📊 获奖名单预览</h3>
|
||
<div id="awardPreview"></div>
|
||
<div class="print-buttons">
|
||
<button class="btn btn-success btn-sm" onclick="printAward('first')">打印一等奖名单</button>
|
||
<button class="btn btn-success btn-sm" onclick="printAward('second')">打印二等奖名单</button>
|
||
<button class="btn btn-success btn-sm" onclick="printAward('third')">打印三等奖名单</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 打印中心页面 -->
|
||
<div id="printTabContent" style="display:none;">
|
||
<div class="batch-bar" id="printBatchBar"></div>
|
||
<div class="admin-panel"><h3>🏆 获奖名单打印</h3>
|
||
<div id="awardPreviewPrint"></div>
|
||
<div class="print-buttons">
|
||
<button class="btn btn-success btn-sm" onclick="printAward('first')">打印一等奖名单</button>
|
||
<button class="btn btn-success btn-sm" onclick="printAward('second')">打印二等奖名单</button>
|
||
<button class="btn btn-success btn-sm" onclick="printAward('third')">打印三等奖名单</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 汇总表 -->
|
||
<div class="summary-panel">
|
||
<h3>📊 评分汇总表 <span id="summaryTitle"></span><button class="btn btn-success btn-sm" onclick="exportExcel()" style="float:right">📎 导出Excel</button></h3>
|
||
<div style="overflow-x: auto; clear: both;"><table class="summary-table" id="summaryTable"><thead><tr><th>名次</th><th>节目序号</th><th>节目名称</th><th>演出单位</th><th>表演者</th><th>类别</th><th>平均分</th><th>最高分</th><th>最低分</th><th>评委数</th><th class="col-operation">操作</th></tr></thead><tbody id="summaryBody"></tbody></table></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 模态框 -->
|
||
<div id="programModal" class="modal"><div class="modal-content"><h3 id="programModalTitle">添加节目</h3><input type="text" id="programSn" placeholder="节目序号"><input type="text" id="programName" placeholder="节目名称"><input type="text" id="programUnit" placeholder="演出单位"><input type="text" id="programPerformer" placeholder="表演者"><select id="programCategory"><option value="舞蹈">舞蹈</option><option value="音乐">音乐</option><option value="语言">语言</option><option value="器乐">器乐</option></select><select id="programBatchId"></select><div class="modal-buttons"><button class="btn btn-outline btn-sm" onclick="closeProgramModal()">取消</button><button class="btn btn-primary btn-sm" onclick="saveProgram()">保存</button></div></div></div>
|
||
<div id="importModal" class="modal"><div class="modal-content"><h3>从Excel导入节目</h3><p style="margin-bottom:10px; font-size:12px;">Excel需要包含列:节目序号、节目名称、演出单位、表演者、节目类别</p><input type="file" id="importFile" accept=".xlsx,.xls"><select id="importBatchId" style="margin-top:10px;"></select><div class="modal-buttons" style="margin-top:15px;"><button class="btn btn-outline btn-sm" onclick="closeImportModal()">取消</button><button class="btn btn-primary btn-sm" onclick="importPrograms()">导入</button></div></div></div>
|
||
<div id="judgeModal" class="modal"><div class="modal-content"><h3 id="judgeModalTitle">添加评委</h3><input type="text" id="judgeId" placeholder="评委编号"><input type="text" id="judgeName" placeholder="评委姓名"><div class="modal-buttons"><button class="btn btn-outline btn-sm" onclick="closeJudgeModal()">取消</button><button class="btn btn-primary btn-sm" onclick="saveJudge()">保存</button></div></div></div>
|
||
<div id="batchModal" class="modal"><div class="modal-content"><h3>添加批次</h3><input type="text" id="batchName" placeholder="批次名称"><div class="modal-buttons"><button class="btn btn-outline btn-sm" onclick="closeBatchModal()">取消</button><button class="btn btn-primary btn-sm" onclick="saveBatch()">保存</button></div></div></div>
|
||
<div id="titleModal" class="modal"><div class="modal-content"><h3>修改标题</h3><input type="text" id="mainTitleInput" placeholder="主标题"><input type="text" id="subTitleInput" placeholder="副标题"><div class="modal-buttons"><button class="btn btn-outline btn-sm" onclick="closeTitleModal()">取消</button><button class="btn btn-primary btn-sm" onclick="saveTitleSettings()">保存</button></div></div></div>
|
||
|
||
<script>
|
||
let currentUser = null, userType = null, judgesData = [], batchesData = [], programsData = [], criteriaData = {};
|
||
let currentBatchId = null, currentJudgeProgram = null;
|
||
let editingProgramSn = null, editingJudgeId = null;
|
||
|
||
async function restoreLogin() {
|
||
const stored = localStorage.getItem('score_user');
|
||
if (!stored) return false;
|
||
try {
|
||
const user = JSON.parse(stored);
|
||
if (user.type === 'judge') {
|
||
// 可选:向后端验证评委是否存在(为了安全,但最小修改可省略直接恢复)
|
||
currentUser = { judge_id: user.id, judge_name: user.name };
|
||
userType = 'judge';
|
||
document.getElementById('loginArea').style.display = 'none';
|
||
document.getElementById('userInfoArea').style.display = 'flex';
|
||
document.getElementById('userInfo').innerHTML = `👨⚖️ 评委:${user.name} <span class="mode-badge mode-judge">评委模式</span>`;
|
||
document.getElementById('editTitleBtn').style.display = 'none';
|
||
document.getElementById('adminTab').style.display = 'none';
|
||
document.getElementById('awardTab').style.display = 'none';
|
||
document.getElementById('judgeScoringTab').style.display = 'block';
|
||
document.getElementById('adminScoringTab').style.display = 'none';
|
||
await loadJudgePrograms();
|
||
return true;
|
||
} else if (user.type === 'admin') {
|
||
currentUser = { id: user.id, username: user.username };
|
||
userType = 'admin';
|
||
document.getElementById('loginArea').style.display = 'none';
|
||
document.getElementById('userInfoArea').style.display = 'flex';
|
||
document.getElementById('userInfo').innerHTML = `👑 管理员:${user.username} <span class="mode-badge mode-admin">管理员模式</span>`;
|
||
document.getElementById('editTitleBtn').style.display = 'inline-block';
|
||
document.getElementById('adminTab').style.display = 'block';
|
||
document.getElementById('awardTab').style.display = 'block';
|
||
document.getElementById('judgeScoringTab').style.display = 'none';
|
||
document.getElementById('adminScoringTab').style.display = 'block';
|
||
await loadAdminScoring();
|
||
return true;
|
||
}
|
||
} catch(e) { console.warn(e); }
|
||
return false;
|
||
}
|
||
|
||
async function init() {
|
||
const loggedIn = await restoreLogin();
|
||
await loadTitles();
|
||
await loadBatches();
|
||
await loadJudges();
|
||
await loadPrograms();
|
||
await loadCriteria();
|
||
await loadAwardNames();
|
||
await loadAggregate();
|
||
await loadProgress();
|
||
setInterval(() => { if(currentUser) loadAggregate(); loadProgress(); }, 5000);
|
||
if (!loggedIn) {
|
||
document.getElementById('judgeScoringTab').style.display = 'block';
|
||
document.getElementById('adminScoringTab').style.display = 'none';
|
||
}
|
||
const adminPasswordInput = document.getElementById('adminPassword');
|
||
const adminUsernameInput = document.getElementById('adminUsername');
|
||
const loginHandler = function(e) { if(e.key === 'Enter') loginAsAdmin(); };
|
||
if(adminPasswordInput) adminPasswordInput.addEventListener('keypress', loginHandler);
|
||
if(adminUsernameInput) adminUsernameInput.addEventListener('keypress', loginHandler);
|
||
}
|
||
|
||
async function loadTitles() { const res = await fetch('/api/settings/titles'); const data = await res.json(); document.getElementById('mainTitle').innerText = data.main_title; document.getElementById('subTitle').innerText = data.sub_title; }
|
||
async function loadCriteria() { const res = await fetch('/api/criteria'); criteriaData = await res.json(); }
|
||
async function loadBatches() { const res = await fetch('/api/batches'); batchesData = await res.json(); renderBatchBars(); renderBatchSelect(); renderBatchList(); }
|
||
async function loadAwardNames() { const res = await fetch('/api/settings/awards'); const data = await res.json(); document.getElementById('awardFirstName').value = data.first_name; document.getElementById('awardSecondName').value = data.second_name; document.getElementById('awardThirdName').value = data.third_name; document.getElementById('awardFirst').value = data.first; document.getElementById('awardSecond').value = data.second; document.getElementById('awardThird').value = data.third; await updateAwardPreview(); }
|
||
|
||
function renderBatchBars() {
|
||
const bars = ['judgeBatchBar', 'adminBatchBar', 'configBatchBar', 'awardBatchBar', 'printBatchBar'];
|
||
bars.forEach(barId => {
|
||
const bar = document.getElementById(barId);
|
||
if(bar){
|
||
bar.innerHTML = '<button class="batch-btn" onclick="selectBatch(null)">📊 全部</button>';
|
||
batchesData.forEach(b => { bar.innerHTML += `<button class="batch-btn ${currentBatchId===b.id?'active':''}" onclick="selectBatch(${b.id})">📁 ${b.name}</button>`; });
|
||
}
|
||
});
|
||
}
|
||
|
||
function renderBatchSelect() { const select = document.getElementById('programBatchId'); if(select){ select.innerHTML = ''; batchesData.forEach(b => { select.innerHTML += `<option value="${b.id}">${b.name}</option>`; }); } }
|
||
|
||
function renderBatchList() {
|
||
const container = document.getElementById('batchList');
|
||
if(container){
|
||
container.innerHTML = '';
|
||
batchesData.forEach(b => {
|
||
container.innerHTML += `<div class="batch-list-item">
|
||
<span>📁 ${b.name}</span>
|
||
<div class="batch-buttons">
|
||
<button class="btn btn-outline btn-sm" onclick="clearBatchPrograms(${b.id})">🗑️ 清空节目</button>
|
||
<button class="btn btn-outline btn-sm" onclick="deleteBatch(${b.id})">删除批次</button>
|
||
</div>
|
||
</div>`;
|
||
});
|
||
}
|
||
}
|
||
|
||
async function clearBatchPrograms(batchId) {
|
||
if(confirm(`确定要清空该批次的所有节目吗?此操作不可恢复!`)){
|
||
const res = await fetch(`/api/batches/${batchId}/programs`, { method:'DELETE' });
|
||
if(res.ok){
|
||
showToast('该批次所有节目已清空');
|
||
await loadPrograms();
|
||
if(userType === 'judge') await loadJudgePrograms();
|
||
if(userType === 'admin') await loadAdminScoring();
|
||
await loadAggregate();
|
||
} else {
|
||
showToast('操作失败');
|
||
}
|
||
}
|
||
}
|
||
|
||
async function selectBatch(batchId) {
|
||
currentBatchId = batchId;
|
||
renderBatchBars();
|
||
if(userType === 'judge'){
|
||
await loadJudgePrograms();
|
||
} else if(userType === 'admin'){
|
||
await loadAdminScoring();
|
||
}
|
||
await loadAggregate();
|
||
}
|
||
|
||
async function loginAsJudge() {
|
||
const select = document.getElementById('judgeSelect');
|
||
const judgeId = select.value;
|
||
if(!judgeId){ showToast('请选择评委'); return; }
|
||
const judgeName = select.options[select.selectedIndex].text.split(' (')[0];
|
||
const res = await fetch('/api/judge/login', { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({judge_id:judgeId, judge_name:judgeName}) });
|
||
if(res.ok){
|
||
const data = await res.json();
|
||
currentUser = data; userType = 'judge';
|
||
localStorage.setItem('score_user', JSON.stringify({
|
||
type: 'judge',
|
||
id: data.judge_id,
|
||
name: data.judge_name
|
||
}));
|
||
document.getElementById('loginArea').style.display = 'none';
|
||
document.getElementById('userInfoArea').style.display = 'flex';
|
||
document.getElementById('userInfo').innerHTML = `👨⚖️ 评委:${data.judge_name} <span class="mode-badge mode-judge">评委模式</span>`;
|
||
document.getElementById('editTitleBtn').style.display = 'none';
|
||
document.getElementById('adminTab').style.display = 'none';
|
||
document.getElementById('awardTab').style.display = 'none';
|
||
document.getElementById('judgeScoringTab').style.display = 'block';
|
||
document.getElementById('adminScoringTab').style.display = 'none';
|
||
await loadJudgePrograms();
|
||
} else { showToast('登录失败'); }
|
||
}
|
||
|
||
async function loginAsAdmin() {
|
||
const username = document.getElementById('adminUsername').value;
|
||
const password = document.getElementById('adminPassword').value;
|
||
if(!username||!password){ showToast('请输入账号密码'); return; }
|
||
const res = await fetch('/api/admin/login', { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({username,password}) });
|
||
if(res.ok){
|
||
const data = await res.json();
|
||
currentUser = data; userType = 'admin';
|
||
localStorage.setItem('score_user', JSON.stringify({
|
||
type: 'admin',
|
||
id: data.admin_id,
|
||
username: data.username
|
||
}));
|
||
document.getElementById('loginArea').style.display = 'none';
|
||
document.getElementById('userInfoArea').style.display = 'flex';
|
||
document.getElementById('userInfo').innerHTML = `👑 管理员:${data.username} <span class="mode-badge mode-admin">管理员模式</span>`;
|
||
document.getElementById('editTitleBtn').style.display = 'inline-block';
|
||
document.getElementById('adminTab').style.display = 'block';
|
||
document.getElementById('awardTab').style.display = 'block';
|
||
document.getElementById('judgeScoringTab').style.display = 'none';
|
||
document.getElementById('adminScoringTab').style.display = 'block';
|
||
await loadAdminScoring();
|
||
} else { showToast('登录失败'); }
|
||
}
|
||
|
||
function logout() {
|
||
localStorage.removeItem('score_user');
|
||
currentUser = null; userType = null;
|
||
document.getElementById('loginArea').style.display = 'flex';
|
||
document.getElementById('userInfoArea').style.display = 'none';
|
||
document.getElementById('adminTab').style.display = 'none';
|
||
document.getElementById('awardTab').style.display = 'none';
|
||
document.getElementById('judgeScoringTab').style.display = 'block';
|
||
document.getElementById('adminScoringTab').style.display = 'none';
|
||
document.getElementById('judgeScoringContainer').innerHTML = '<div style="text-align:center;padding:40px;">请先登录选择节目评分</div>';
|
||
switchTab('scoring');
|
||
}
|
||
|
||
async function loadJudgePrograms() {
|
||
let url = '/api/programs';
|
||
if(currentBatchId) url += `?batch_id=${currentBatchId}`;
|
||
const res = await fetch(url);
|
||
programsData = await res.json();
|
||
renderJudgeProgramList();
|
||
}
|
||
|
||
function renderJudgeProgramList() {
|
||
const container = document.getElementById('judgeScoringContainer');
|
||
if(!container) return;
|
||
let html = '<div style="display:flex; gap:20px; flex-wrap:wrap;">';
|
||
html += '<div class="program-list-sidebar"><h4>📋 节目列表</h4><ul style="list-style:none;">';
|
||
programsData.forEach(prog => {
|
||
const isActive = currentJudgeProgram && currentJudgeProgram.sn === prog.sn;
|
||
html += `<li class="program-item ${isActive ? 'active' : ''}" onclick="selectJudgeProgram('${prog.sn}')">
|
||
<strong>${escapeHtml(prog.name)}</strong><br><span style="font-size:12px; color:#666;">${prog.sn} | ${escapeHtml(prog.performer)}</span>
|
||
</li>`;
|
||
});
|
||
html += '</ul></div><div class="score-panel" id="judgeScorePanel"><div style="text-align:center;padding:40px;">请选择节目进行评分</div></div></div>';
|
||
container.innerHTML = html;
|
||
}
|
||
|
||
async function selectJudgeProgram(programSn) {
|
||
currentJudgeProgram = programsData.find(p=>p.sn===programSn);
|
||
renderJudgeProgramList();
|
||
const res = await fetch(`/api/scores/${currentUser.judge_id}`);
|
||
const scores = await res.json();
|
||
const progScore = scores[programSn] || {content:null, stage:null, art:null, overall:null, comment:''};
|
||
renderJudgeScorePanel(programSn, progScore);
|
||
}
|
||
|
||
function renderJudgeScorePanel(programSn, scores) {
|
||
const prog = programsData.find(p=>p.sn===programSn);
|
||
if(!prog) return;
|
||
const total = (scores.content||0)+(scores.stage||0)+(scores.art||0)+(scores.overall||0);
|
||
const html = `<div style="background:white; border-radius:16px; padding:20px;">
|
||
<h3>${escapeHtml(prog.name)}</h3><p style="color:#666; margin-bottom:20px;">序号:${prog.sn} | 单位:${escapeHtml(prog.unit)} | 表演者:${escapeHtml(prog.performer)} | 类别:${prog.category}</p>
|
||
<div style="display:grid; grid-template-columns:repeat(auto-fit,minmax(200px,1fr)); gap:15px;">
|
||
<div style="background:#f7fafc; padding:15px; border-radius:12px; text-align:center;">
|
||
<label>📖 内容取材</label><div style="font-size:12px; color:#999;">满分20分</div>
|
||
<input type="number" id="score_content" value="${scores.content!==null?scores.content:''}" step="0.5" min="0" max="20" style="width:100%; padding:8px; margin:10px 0; font-size:18px; text-align:center; border:1px solid #ddd; border-radius:8px;">
|
||
<div style="font-size:11px; color:#666;">${criteriaData.content?.description || ''}</div>
|
||
</div>
|
||
<div style="background:#f7fafc; padding:15px; border-radius:12px; text-align:center;">
|
||
<label>💃 舞台风度</label><div style="font-size:12px; color:#999;">满分10分</div>
|
||
<input type="number" id="score_stage" value="${scores.stage!==null?scores.stage:''}" step="0.5" min="0" max="10" style="width:100%; padding:8px; margin:10px 0; font-size:18px; text-align:center; border:1px solid #ddd; border-radius:8px;">
|
||
<div style="font-size:11px; color:#666;">${criteriaData.stage?.description || ''}</div>
|
||
</div>
|
||
<div style="background:#f7fafc; padding:15px; border-radius:12px; text-align:center;">
|
||
<label>🎨 表演艺术水平</label><div style="font-size:12px; color:#999;">满分60分</div>
|
||
<input type="number" id="score_art" value="${scores.art!==null?scores.art:''}" step="0.5" min="0" max="60" style="width:100%; padding:8px; margin:10px 0; font-size:18px; text-align:center; border:1px solid #ddd; border-radius:8px;">
|
||
<div style="font-size:11px; color:#666;">${criteriaData.art?.description || ''}</div>
|
||
</div>
|
||
<div style="background:#f7fafc; padding:15px; border-radius:12px; text-align:center;">
|
||
<label>⭐ 综合评价</label><div style="font-size:12px; color:#999;">满分10分</div>
|
||
<input type="number" id="score_overall" value="${scores.overall!==null?scores.overall:''}" step="0.5" min="0" max="10" style="width:100%; padding:8px; margin:10px 0; font-size:18px; text-align:center; border:1px solid #ddd; border-radius:8px;">
|
||
<div style="font-size:11px; color:#666;">${criteriaData.overall?.description || ''}</div>
|
||
</div>
|
||
</div>
|
||
<div class="total-score">🏆 节目总分:<span id="totalScore">${total}</span> / 100</div>
|
||
<div style="margin:20px 0;"><textarea id="commentInput" rows="3" placeholder="评委点评(可选)" style="width:100%; padding:10px; border:1px solid #ddd; border-radius:8px;">${escapeHtml(scores.comment||'')}</textarea></div>
|
||
<div style="display:flex; gap:10px; justify-content:flex-end;"><button class="btn btn-outline btn-sm" onclick="saveJudgeComment()">💾 保存点评</button></div>
|
||
</div>`;
|
||
document.getElementById('judgeScorePanel').innerHTML = html;
|
||
bindJudgeScoreEvents();
|
||
}
|
||
|
||
function bindJudgeScoreEvents() {
|
||
['content','stage','art','overall'].forEach(field => {
|
||
const input = document.getElementById(`score_${field}`);
|
||
if(input) input.onchange = () => saveJudgeScore(field, parseFloat(input.value)||null);
|
||
});
|
||
}
|
||
|
||
async function saveJudgeScore(field, value) {
|
||
if(!currentUser || !currentJudgeProgram) return;
|
||
const res = await fetch(`/api/scores/${currentUser.judge_id}`, { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({program_sn:currentJudgeProgram.sn, field, value}) });
|
||
if(res.ok){ const data = await res.json(); document.getElementById('totalScore').innerText = data.total; await loadAggregate(); await loadProgress(); }
|
||
else { const err = await res.json(); showToast(err.detail); }
|
||
}
|
||
|
||
async function saveJudgeComment() {
|
||
if(!currentUser || !currentJudgeProgram) return;
|
||
const comment = document.getElementById('commentInput').value;
|
||
await fetch(`/api/comment/${currentUser.judge_id}`, { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({program_sn:currentJudgeProgram.sn, comment}) });
|
||
showToast('点评已保存');
|
||
}
|
||
|
||
async function loadAdminScoring() {
|
||
let url = '/api/programs';
|
||
if(currentBatchId) url += `?batch_id=${currentBatchId}`;
|
||
const res = await fetch(url);
|
||
programsData = await res.json();
|
||
await renderAdminScoringTable();
|
||
}
|
||
|
||
async function renderAdminScoringTable() {
|
||
if(!judgesData.length || !programsData.length){ document.getElementById('adminScoringContainer').innerHTML = '<div style="text-align:center;padding:40px;">加载中...</div>'; return; }
|
||
let scoresMap = {};
|
||
for(let prog of programsData){ const res = await fetch(`/api/scores/program/${prog.sn}`); scoresMap[prog.sn] = await res.json(); }
|
||
let html = '<table class="scoring-table"><thead><tr><th>节目序号</th><th>节目名称</th><th>表演者</th>';
|
||
judgesData.forEach(j => { html += `<th>${j.name}<br><span style="font-size:11px;">(${j.id})</span></th>`; });
|
||
html += '<th>操作</th></tr></thead><tbody>';
|
||
for(let prog of programsData){
|
||
html += `<tr><td style="text-align:center;">${prog.sn}</td><td style="text-align:left;">${prog.name}</td><td style="text-align:left;">${escapeHtml(prog.performer)}`;
|
||
for(let judge of judgesData){
|
||
let score = '';
|
||
if(scoresMap[prog.sn]){ const judgeScore = scoresMap[prog.sn].find(s => s.judge_id === judge.id); if(judgeScore && judgeScore.total_score !== null) score = judgeScore.total_score; }
|
||
html += `<td><input type="number" class="score-input" id="score_${prog.sn}_${judge.id}" value="${score}" step="0.5" min="0" max="100" onchange="saveAdminScore('${prog.sn}','${judge.id}',this.value)"></td>`;
|
||
}
|
||
html += `<td><button class="btn btn-outline btn-sm" onclick="printProgram('${prog.sn}')">🖨️ 打印</button></td></tr>`;
|
||
}
|
||
html += '</tbody></table>';
|
||
document.getElementById('adminScoringContainer').innerHTML = html;
|
||
}
|
||
|
||
async function saveAdminScore(programSn, judgeId, value) {
|
||
if(!currentUser) return;
|
||
let numValue = value === '' ? null : parseFloat(value);
|
||
if(numValue !== null && (numValue < 0 || numValue > 100)){
|
||
showToast('总分必须在0-100之间');
|
||
document.getElementById(`score_${programSn}_${judgeId}`).value = '';
|
||
return;
|
||
}
|
||
let scores = {};
|
||
scores[judgeId] = numValue;
|
||
try {
|
||
const res = await fetch('/api/scores/admin/batch', {
|
||
method:'POST',
|
||
headers:{'Content-Type':'application/json'},
|
||
body:JSON.stringify({program_sn:programSn, judge_scores:scores})
|
||
});
|
||
if(res.ok){
|
||
await loadAggregate();
|
||
await loadProgress();
|
||
document.getElementById(`score_${programSn}_${judgeId}`).value = numValue !== null ? numValue : '';
|
||
showToast('保存成功');
|
||
} else {
|
||
const err = await res.json();
|
||
showToast('保存失败:' + (err.detail || '未知错误'));
|
||
}
|
||
} catch(e) {
|
||
showToast('网络错误:' + e.message);
|
||
}
|
||
}
|
||
|
||
async function loadPrograms() { const res = await fetch('/api/programs'); programsData = await res.json(); renderProgramManageList(); }
|
||
async function loadJudges() { const res = await fetch('/api/judges'); judgesData = await res.json(); renderJudgeManageList(); const select = document.getElementById('judgeSelect'); select.innerHTML = '<option value="">选择评委</option>'; judgesData.forEach(j => { select.innerHTML += `<option value="${j.id}">${j.name} (${j.id})</option>`; }); if(userType === 'admin') await renderAdminScoringTable(); }
|
||
|
||
async function loadAggregate() {
|
||
let url = '/api/aggregate';
|
||
if (currentBatchId) url += `?batch_id=${currentBatchId}`;
|
||
const res = await fetch(url);
|
||
const data = await res.json();
|
||
const tbody = document.getElementById('summaryBody');
|
||
tbody.innerHTML = '';
|
||
data.forEach(item => {
|
||
const row = tbody.insertRow();
|
||
if (item.rank === 1) row.className = 'rank-1';
|
||
row.insertCell(0).innerText = item.rank;
|
||
row.insertCell(1).innerText = item.sn;
|
||
row.insertCell(2).innerText = item.name;
|
||
row.insertCell(3).innerText = item.unit;
|
||
row.insertCell(4).innerText = item.performer;
|
||
row.insertCell(5).innerText = item.category;
|
||
row.insertCell(6).innerHTML = `<strong>${item.avg_score}</strong>`;
|
||
row.insertCell(7).innerText = item.max_score;
|
||
row.insertCell(8).innerText = item.min_score;
|
||
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>`;
|
||
});
|
||
const batch = batchesData.find(b => b.id === currentBatchId);
|
||
document.getElementById('summaryTitle').innerHTML = batch ? ` - ${batch.name}` : '';
|
||
await updateAwardPreview();
|
||
await updateAwardPreview('awardPreviewPrint');
|
||
}
|
||
|
||
async function loadProgress() { const res = await fetch('/api/progress'); const data = await res.json(); document.getElementById('progressFill').style.width = `${data.progress}%`; document.getElementById('progressText').innerHTML = `评分进度:${data.scored_programs}/${data.total_cells} (${data.progress}%)`; }
|
||
|
||
function switchTab(tab) {
|
||
document.querySelectorAll('.tab').forEach(t=>t.classList.remove('active'));
|
||
document.getElementById('judgeScoringTab').style.display='none'; document.getElementById('adminScoringTab').style.display='none';
|
||
document.getElementById('adminTabContent').style.display='none'; document.getElementById('awardTabContent').style.display='none'; document.getElementById('printTabContent').style.display='none';
|
||
if(tab==='scoring'){ document.querySelector('.tab:first-child').classList.add('active'); if(userType === 'judge') document.getElementById('judgeScoringTab').style.display='block'; else if(userType === 'admin') document.getElementById('adminScoringTab').style.display='block'; else document.getElementById('judgeScoringTab').style.display='block'; }
|
||
else if(tab==='admin'){ document.querySelectorAll('.tab')[1].classList.add('active'); document.getElementById('adminTabContent').style.display='block'; loadBatches(); loadJudges(); loadPrograms(); }
|
||
else if(tab==='award'){ document.querySelectorAll('.tab')[2].classList.add('active'); document.getElementById('awardTabContent').style.display='block'; updateAwardPreview(); }
|
||
else if(tab==='print'){ document.querySelectorAll('.tab')[3].classList.add('active'); document.getElementById('printTabContent').style.display='block'; updateAwardPreview('awardPreviewPrint'); }
|
||
if (tab === 'print') {
|
||
document.body.classList.add('print-mode');
|
||
} else {
|
||
document.body.classList.remove('print-mode');
|
||
}
|
||
}
|
||
|
||
async function updateAwardPreview(containerId = 'awardPreview') {
|
||
let url = '/api/aggregate';
|
||
if(currentBatchId) url += `?batch_id=${currentBatchId}`;
|
||
const programs = await fetch(url).then(r=>r.json());
|
||
const settings = await fetch('/api/settings/awards').then(r=>r.json());
|
||
const firstCount=settings.first, secondCount=settings.second, thirdCount=settings.third;
|
||
let html = '<div class="award-table-container"><table class="award-table"><thead><tr><th>名次</th><th>节目序号</th><th>节目名称</th><th>演出单位</th><th>表演者</th><th>类别</th><th>平均分</th><th>奖项</th></tr></thead><tbody>';
|
||
programs.forEach((p,idx)=>{
|
||
let award='';
|
||
if(idx<firstCount) award=settings.first_name;
|
||
else if(idx<firstCount+secondCount) award=settings.second_name;
|
||
else if(idx<firstCount+secondCount+thirdCount) award=settings.third_name;
|
||
else award='-';
|
||
html += `<tr><td style="text-align:center;">${idx+1}</td><td style="text-align:center;">${p.sn}</td><td style="text-align:left;">${p.name}</td><td style="text-align:left;">${p.unit}</td><td style="text-align:left;">${p.performer}</td><td style="text-align:center;">${p.category}</td><td style="text-align:center;">${p.avg_score}</td><td style="text-align:center;">${award}</td><tr>`;
|
||
});
|
||
html += '</tbody></table></div>';
|
||
document.getElementById(containerId).innerHTML = html;
|
||
}
|
||
|
||
function renderProgramManageList() {
|
||
const container = document.getElementById('programManageContainer');
|
||
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); });
|
||
let html = '';
|
||
for(let batchName in grouped){
|
||
html += `<h4 style="margin:15px 0 5px 0;">📁 ${batchName}</h4>`;
|
||
grouped[batchName].forEach(p => {
|
||
html += `<div class="program-list-item">
|
||
<span>${p.sn} | ${escapeHtml(p.name)} | ${escapeHtml(p.unit)} | ${escapeHtml(p.performer)}</span>
|
||
<div class="program-buttons">
|
||
<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>
|
||
</div>`;
|
||
});
|
||
}
|
||
container.innerHTML = html;
|
||
}
|
||
|
||
function renderJudgeManageList() {
|
||
const container = document.getElementById('judgeManageList');
|
||
if(container){
|
||
container.innerHTML = '';
|
||
judgesData.forEach(j => {
|
||
container.innerHTML += `<div class="judge-list-item">
|
||
<span>${j.name} (${j.id})</span>
|
||
<div class="judge-buttons">
|
||
<button class="btn btn-outline btn-sm" onclick="editJudge('${j.id}')">✏️ 编辑</button>
|
||
<button class="btn btn-outline btn-sm" onclick="deleteJudge('${j.id}')">🗑️ 删除</button>
|
||
</div>
|
||
</div>`;
|
||
});
|
||
}
|
||
}
|
||
|
||
function printBlank(sn) { window.open(`/print/blank/${sn}`, '_blank'); }
|
||
function printProgram(sn) { window.open(`/print/program/${sn}`, '_blank'); }
|
||
function printAward(type) {
|
||
let url = `/print/award/${type}`;
|
||
if(currentBatchId) url += `?batch_id=${currentBatchId}`;
|
||
window.open(url, '_blank');
|
||
}
|
||
async function exportExcel() { let url = '/api/export_excel'; if(currentBatchId) url += `?batch_id=${currentBatchId}`; window.open(url, '_blank'); }
|
||
|
||
async function saveAwardSettings() { 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', { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({first,second,third}) }); showToast('保存成功'); await updateAwardPreview(); }
|
||
async function saveAwardNames() { 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', { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({first_name,second_name,third_name}) }); showToast('保存成功'); await updateAwardPreview(); }
|
||
|
||
function showAddBatchModal() { document.getElementById('batchModal').style.display='flex'; }
|
||
async function saveBatch() { const name = document.getElementById('batchName').value.trim(); if(!name){ showToast('请输入批次名称'); return; } await fetch('/api/batches', { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({name}) }); closeBatchModal(); await loadBatches(); }
|
||
async function deleteBatch(id) { if(confirm('确定删除该批次吗?')) { await fetch(`/api/batches/${id}`, { method:'DELETE' }); await loadBatches(); await loadPrograms(); } }
|
||
|
||
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() { const sn = document.getElementById('programSn').value.trim(); const name = document.getElementById('programName').value.trim(); const unit = document.getElementById('programUnit').value.trim(); const performer = document.getElementById('programPerformer').value.trim(); const category = document.getElementById('programCategory').value; const batch_id = parseInt(document.getElementById('programBatchId').value); if(!sn||!name){ showToast('请填写节目序号和名称'); return; } let res; if(editingProgramSn){ res = await fetch(`/api/programs/${editingProgramSn}`, { method:'PUT', headers:{'Content-Type':'application/json'}, body:JSON.stringify({name,unit,performer,category,batch_id}) }); } else { res = await fetch('/api/programs', { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({sn,name,unit,performer,category,batch_id}) }); } if(res.ok){ closeProgramModal(); await loadPrograms(); if(userType==='judge') await loadJudgePrograms(); if(userType==='admin') await loadAdminScoring(); await loadAggregate(); } else { const err=await res.json(); showToast(err.detail); } }
|
||
async function deleteProgram(sn) { if(confirm(`确定删除节目 ${sn} 吗?`)){ await fetch(`/api/programs/${sn}`, { method:'DELETE' }); await loadPrograms(); if(userType==='judge') await loadJudgePrograms(); if(userType==='admin') await loadAdminScoring(); await loadAggregate(); } }
|
||
function closeProgramModal() { document.getElementById('programModal').style.display='none'; document.getElementById('programSn').disabled=false; }
|
||
|
||
function showImportProgramModal() { renderBatchSelect(); document.getElementById('importBatchId').innerHTML = document.getElementById('programBatchId').innerHTML; document.getElementById('importModal').style.display='flex'; }
|
||
async function importPrograms() { const file = document.getElementById('importFile').files[0]; if(!file){ showToast('请选择Excel文件'); return; } const batch_id = document.getElementById('importBatchId').value; const formData = new FormData(); formData.append('file', file); const res = await fetch(`/api/programs/import?batch_id=${batch_id}`, { method:'POST', body:formData }); if(res.ok){ showToast('导入成功'); closeImportModal(); await loadPrograms(); if(userType==='judge') await loadJudgePrograms(); if(userType==='admin') await loadAdminScoring(); await loadAggregate(); } else { const err=await res.json(); showToast('导入失败:'+err.detail); } }
|
||
function closeImportModal() { document.getElementById('importModal').style.display='none'; document.getElementById('importFile').value=''; }
|
||
|
||
function showAddJudgeModal() { editingJudgeId=null; document.getElementById('judgeModalTitle').innerText='添加评委'; document.getElementById('judgeId').value=''; document.getElementById('judgeName').value=''; document.getElementById('judgeModal').style.display='flex'; }
|
||
function editJudge(id) { const judge = judgesData.find(j=>j.id===id); if(!judge) return; editingJudgeId=id; document.getElementById('judgeModalTitle').innerText='编辑评委'; document.getElementById('judgeId').value=judge.id; document.getElementById('judgeId').disabled=true; document.getElementById('judgeName').value=judge.name; document.getElementById('judgeModal').style.display='flex'; }
|
||
async function saveJudge() { const id = document.getElementById('judgeId').value.trim(); const name = document.getElementById('judgeName').value.trim(); if(!id||!name){ showToast('请填写评委编号和姓名'); return; } let res; if(editingJudgeId){ res = await fetch(`/api/judges/${editingJudgeId}`, { method:'PUT', headers:{'Content-Type':'application/json'}, body:JSON.stringify({name}) }); } else { res = await fetch('/api/judges', { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({id,name}) }); } if(res.ok){ closeJudgeModal(); await loadJudges(); if(userType==='admin') await renderAdminScoringTable(); } else { const err=await res.json(); showToast(err.detail); } }
|
||
async function deleteJudge(id) { if(confirm(`确定删除评委 ${id} 吗?`)){ await fetch(`/api/judges/${id}`, { method:'DELETE' }); await loadJudges(); if(userType==='admin') await renderAdminScoringTable(); await loadAggregate(); } }
|
||
function closeJudgeModal() { document.getElementById('judgeModal').style.display='none'; document.getElementById('judgeId').disabled=false; }
|
||
|
||
function closeBatchModal() { document.getElementById('batchModal').style.display='none'; document.getElementById('batchName').value=''; }
|
||
function showTitleModal() { fetch('/api/settings/titles').then(r=>r.json()).then(data=>{ document.getElementById('mainTitleInput').value=data.main_title; document.getElementById('subTitleInput').value=data.sub_title; document.getElementById('titleModal').style.display='flex'; }); }
|
||
async function saveTitleSettings() { const main_title = document.getElementById('mainTitleInput').value; const sub_title = document.getElementById('subTitleInput').value; await fetch('/api/settings/titles', { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({main_title,sub_title}) }); closeTitleModal(); await loadTitles(); }
|
||
function closeTitleModal() { document.getElementById('titleModal').style.display='none'; }
|
||
function escapeHtml(str) { if(!str) return ''; return str.replace(/[&<>]/g, function(m){ if(m==='&') return '&'; if(m==='<') return '<'; if(m==='>') return '>'; return m; }); }
|
||
function showToast(msg) { const toast = document.createElement('div'); toast.className='toast'; toast.innerText=msg; document.body.appendChild(toast); setTimeout(()=>toast.remove(),2000); }
|
||
|
||
init();
|
||
</script>
|
||
</body>
|
||
</html>
|
||
"""
|