import tkinter as tk
from tkinter import ttk, messagebox
import json

SCORE_FILE = 'datan.txt'
STUDENT_FILE = 'student_users.json'
is_saved = True
SUBJECT_LIST = ["语文", "数学", "英语", "物理", "化学", "生物", "政治", "历史", "地理"]

score_data = {}
try:
    with open(SCORE_FILE, "r", encoding="utf-8") as f:
        score_data = json.load(f)
        score_data = {k.strip(): v.strip() for k, v in score_data.items() if k.strip() and v.strip()}
except FileNotFoundError:
    score_data = {}
except json.JSONDecodeError:
    messagebox.showwarning("警告", "成绩文件损坏，已重置为空数据")
    score_data = {}
except PermissionError:
    messagebox.showerror("错误", "无文件读写权限，请检查程序运行目录")

student_users = []
try:
    with open(STUDENT_FILE, "r", encoding="utf-8") as f:
        student_users = json.load(f)
        student_users = list(set([u.strip() for u in student_users if u.strip()]))
except FileNotFoundError:
    student_users = []
except json.JSONDecodeError:
    messagebox.showwarning("警告", "学生账户文件损坏，已重置为空")
    student_users = []
except PermissionError:
    messagebox.showerror("错误", "无文件读写权限，请检查程序运行目录")

def refresh_score_list(listbox):
    listbox.delete(0, tk.END)
    sorted_items = sorted(score_data.items(), key=lambda x: x[0].split("-"))
    for key, score in sorted_items:
        parts = key.split("-")
        if len(parts) == 3:
            exam, sub, name = parts
            listbox.insert(tk.END, f"[{exam}] {sub}-{name}\t\t{score}分")

def save_student_users():
    global student_users
    student_users = list(set([u.strip() for u in student_users if u.strip()]))
    try:
        with open(STUDENT_FILE, "w", encoding="utf-8") as f:
            json.dump(student_users, f, ensure_ascii=False, indent=2)
        return True
    except PermissionError:
        messagebox.showerror("错误", "无文件写入权限，保存失败")
        return False

def save_score_data():
    global is_saved
    try:
        with open(SCORE_FILE, "w", encoding="utf-8") as f:
            json.dump(score_data, f, ensure_ascii=False, indent=2)
        is_saved = True
        messagebox.showinfo("成功", "成绩单已永久保存！")
    except PermissionError:
        messagebox.showerror("错误", "无文件写入权限，保存失败")

def get_exam_history():
    exam_set = set()
    for key in score_data.keys():
        parts = key.split("-")
        if len(parts) == 3:
            exam_set.add(parts[0])
    return sorted(list(exam_set))

def teacher_login():
    def check_teacher():
        account = teachername.get().strip()
        password = teacherpassword.get().strip()
        if account == 'teacher' and password == '123456':
            login_win.destroy()
            open_teacher_window()
        else:
            messagebox.showerror("登录失败", '账户名或密码错误！')
    login_win = tk.Toplevel(main)
    login_win.title("老师登录")
    login_win.geometry("350x220")
    login_win.resizable(False, False)
    login_win.grab_set()
    tk.Label(login_win, text='老师账户登录', font=("黑体", 14)).pack(pady=15)
    tk.Label(login_win, text='账户名').pack()
    teachername = ttk.Entry(login_win, width=30)
    teachername.pack(pady=3)
    tk.Label(login_win, text='密码').pack()
    teacherpassword = ttk.Entry(login_win, width=30, show="*")
    teacherpassword.pack(pady=3)
    tk.Button(login_win, text='登录', font=("黑体", 12), width=15, command=check_teacher).pack(pady=15)

def manage_student_account(teacher_name_cb=None):
    def refresh_account_list():
        listbox.delete(0, tk.END)
        for user in sorted(student_users):
            listbox.insert(tk.END, f"学生账户：{user}")
        if teacher_name_cb:
            teacher_name_cb.config(values=sorted(student_users))
            if student_users:
                teacher_name_cb.current(0)
    def add_student():
        user = entry_user.get().strip()
        if not user:
            messagebox.showwarning("提示", "学生账户名不能为空！")
            return
        if user in student_users:
            messagebox.showwarning("提示", "该学生账户已存在！")
            return
        student_users.append(user)
        if save_student_users():
            messagebox.showinfo("成功", "学生账户添加成功！")
            entry_user.delete(0, tk.END)
            refresh_account_list()
    def del_student():
        index = listbox.curselection()
        if not index:
            messagebox.showwarning("提示", "请先选择要删除的账户！")
            return
        select_text = listbox.get(index[0])
        student_name = select_text.replace("学生账户：", "").strip()
        confirm = messagebox.askyesno("确认删除", f"确定要删除学生【{student_name}】吗？\n删除后将同步清除该学生的所有历史成绩！")
        if not confirm:
            return
        global score_data, is_saved
        del_keys = [key for key in score_data.keys() if key.endswith(f"-{student_name}")]
        for key in del_keys:
            del score_data[key]
        if del_keys:
            is_saved = False
        student_users.remove(student_name)
        if save_student_users():
            messagebox.showinfo("成功", f"学生【{student_name}】账户及成绩已删除！")
            refresh_account_list()
    win = tk.Toplevel()
    win.title("学生账户管理")
    win.geometry("480x400")
    win.resizable(False, False)
    win.grab_set()
    tk.Label(win, text="学生账户管理", font=("黑体", 14)).pack(pady=10)
    add_frame = tk.Frame(win)
    add_frame.pack(pady=5)
    tk.Label(add_frame, text="学生姓名：").pack(side=tk.LEFT, padx=5)
    entry_user = ttk.Entry(add_frame, width=20)
    entry_user.pack(side=tk.LEFT, padx=5)
    ttk.Button(add_frame, text="添加账户", command=add_student).pack(side=tk.LEFT, padx=5)
    btn_frame = tk.Frame(win)
    btn_frame.pack(pady=5)
    ttk.Button(btn_frame, text="删除选中账户", command=del_student).pack(side=tk.LEFT, padx=10)
    tk.Label(win, text="已添加学生账户", font=("黑体", 11)).pack(pady=5)
    listbox = tk.Listbox(win, width=55, height=15, font=("黑体", 10))
    listbox.pack(pady=5)
    refresh_account_list()

def draw_score_curve(canvas, name, scores):
    canvas.delete("all")
    if not scores:
        canvas.create_text(275, 150, text="暂无成绩数据，无法生成曲线图", font=("黑体", 14), fill="red")
        return
    exam_subject_list = []
    score_value_list = []
    for item in scores:
        left_part, score = item.rsplit("\t\t", 1)
        exam_subject_list.append(left_part.replace("[", "").replace("]", ""))
        score = score.replace("分", "")
        try:
            score_value_list.append(float(score))
        except:
            continue
    if not score_value_list:
        canvas.create_text(275, 150, text="成绩数据格式异常", font=("黑体", 14), fill="red")
        return
    canvas_width = 550
    canvas_height = 300
    padding_x = 60
    padding_y = 40
    max_data_count = len(exam_subject_list)
    x_step = (canvas_width - 2 * padding_x) // max(max_data_count - 1, 1)
    y_scale = (canvas_height - 2 * padding_y) / 100
    canvas.create_line(padding_x, padding_y, padding_x, canvas_height - padding_y, width=2, arrow=tk.LAST)
    canvas.create_line(padding_x, canvas_height - padding_y, canvas_width - padding_x, canvas_height - padding_y, width=2, arrow=tk.LAST)
    for i in range(0, 101, 20):
        y = canvas_height - padding_y - i * y_scale
        canvas.create_text(padding_x - 20, y, text=f"{i}分", font=("黑体", 8))
        canvas.create_line(padding_x - 5, y, padding_x, y, width=1)
    point_list = []
    for i, (exam_sub, score) in enumerate(zip(exam_subject_list, score_value_list)):
        x = padding_x + i * x_step
        y = canvas_height - padding_y - score * y_scale
        point_list.append((x, y))
        canvas.create_oval(x-4, y-4, x+4, y+4, fill="blue", outline="blue")
        canvas.create_text(x, canvas_height - padding_y + 20, text=exam_sub[:6], font=("黑体", 8))
        canvas.create_text(x, y-15, text=f"{score}", font=("黑体", 10), fill="red")
    if len(point_list) > 1:
        canvas.create_line(point_list, width=2, fill="green", smooth=True)
    canvas.create_text(275, 20, text=f"{name} 历次考试成绩趋势图", font=("黑体", 14))

def student_login_window():
    def check_student_login():
        select_student = student_cb.get().strip()
        if not select_student:
            messagebox.showwarning("提示", "请选择学生账户！")
            return
        if select_student not in student_users:
            messagebox.showerror("错误", "该学生账户不存在！")
            return
        student_scores = []
        for key, score in score_data.items():
            parts = key.split("-")
            if len(parts) == 3:
                exam, subject, name = parts
                if name == select_student:
                    student_scores.append(f"[{exam}] {subject}\t\t{score}分")
        student_scores.sort()
        login_win.destroy()
        show_student_main_window(select_student, student_scores)
    login_win = tk.Toplevel(main)
    login_win.title("学生登录")
    login_win.geometry("350x200")
    login_win.resizable(False, False)
    login_win.grab_set()
    tk.Label(login_win, text="学生成绩查询登录", font=("黑体", 14)).pack(pady=15)
    tk.Label(login_win, text="请选择你的姓名").pack(pady=5)
    student_cb = ttk.Combobox(login_win, values=sorted(student_users), state="readonly", width=30)
    if student_users:
        student_cb.current(0)
    student_cb.pack(pady=5)
    tk.Button(login_win, text="登录查看成绩", font=("黑体", 12), width=18, command=check_student_login).pack(pady=15)

def show_student_main_window(name, scores):
    stu_win = tk.Toplevel(main)
    stu_win.title(f"学生端 - {name} 个人成绩中心")
    stu_win.geometry("600x550")
    stu_win.resizable(False, False)
    tk.Label(stu_win, text=f"{name} 同学 历次考试成绩", font=("黑体", 14)).pack(pady=10)
    score_listbox = tk.Listbox(stu_win, font=("黑体", 11), width=65, height=8)
    score_listbox.pack(pady=5)
    for score_item in scores:
        score_listbox.insert(tk.END, score_item)
    tk.Label(stu_win, text="成绩趋势曲线图", font=("黑体", 14)).pack(pady=10)
    curve_canvas = tk.Canvas(stu_win, width=550, height=300, bg="white")
    curve_canvas.pack(pady=5)
    draw_score_curve(curve_canvas, name, scores)

def edit_score_window(listbox):
    def save_edit_result():
        global score_data, is_saved
        new_score_data = {}
        for key_entry, score_entry in entry_list:
            key = key_entry.get().strip()
            score = score_entry.get().strip()
            if key and score:
                parts = key.split("-")
                if len(parts) != 3:
                    messagebox.showwarning("格式错误", f"条目【{key}】格式错误，需为「考试-科目-姓名」，已跳过")
                    continue
                try:
                    float(score)
                except:
                    messagebox.showwarning("格式错误", f"条目【{key}】的成绩不是有效数字，已跳过")
                    continue
                new_score_data[key] = score
        score_data = new_score_data
        is_saved = False
        refresh_score_list(listbox)
        edit_win.destroy()
        messagebox.showinfo("成功", "成绩编辑已生效，记得点击保存按钮永久保存！")
    def add_new_row():
        row_frame = tk.Frame(scroll_inner)
        row_frame.pack(pady=3, fill=tk.X)
        key_entry = ttk.Entry(row_frame, width=30)
        key_entry.pack(side=tk.LEFT, padx=2)
        key_entry.insert(0, "考试-科目-姓名")
        score_entry = ttk.Entry(row_frame, width=15)
        score_entry.pack(side=tk.LEFT, padx=2)
        score_entry.insert(0, "0")
        entry_list.append((key_entry, score_entry))
        scroll_inner.update_idletasks()
        canvas.config(scrollregion=canvas.bbox("all"))
    edit_win = tk.Toplevel()
    edit_win.title("批量编辑成绩")
    edit_win.geometry("500x500")
    edit_win.grab_set()
    tk.Label(edit_win, text="成绩批量编辑（格式：考试-科目-姓名）", font=("黑体", 12)).pack(pady=10)
    btn_frame = tk.Frame(edit_win)
    btn_frame.pack(pady=5)
    ttk.Button(btn_frame, text="新增一行", command=add_new_row).pack(side=tk.LEFT, padx=10)
    ttk.Button(btn_frame, text="保存编辑结果", command=save_edit_result).pack(side=tk.LEFT, padx=10)
    canvas = tk.Canvas(edit_win)
    scrollbar = ttk.Scrollbar(edit_win, orient="vertical", command=canvas.yview)
    scroll_inner = tk.Frame(canvas)
    scroll_inner.bind("<Configure>", lambda e: canvas.config(scrollregion=canvas.bbox("all")))
    canvas.create_window((0, 0), window=scroll_inner, anchor="nw")
    canvas.configure(yscrollcommand=scrollbar.set)
    canvas.pack(side="left", fill="both", expand=True)
    scrollbar.pack(side="right", fill="y")
    entry_list = []
    for key, score in score_data.items():
        row_frame = tk.Frame(scroll_inner)
        row_frame.pack(pady=3, fill=tk.X)
        key_entry = ttk.Entry(row_frame, width=30)
        key_entry.pack(side=tk.LEFT, padx=2)
        key_entry.insert(0, key)
        score_entry = ttk.Entry(row_frame, width=15)
        score_entry.pack(side=tk.LEFT, padx=2)
        score_entry.insert(0, score)
        entry_list.append((key_entry, score_entry))

def open_teacher_window():
    global is_saved
    teacher_win = tk.Toplevel(main)
    teacher_win.title('成绩管理系统 - 老师端')
    teacher_win.geometry('600x700')
    teacher_win.resizable(False, False)
    tk.Label(teacher_win, text='考试名称（如：第一次月考/期中考试）', font=("黑体", 11)).pack(pady=5)
    exam_frame = tk.Frame(teacher_win)
    exam_frame.pack(pady=3)
    exam_entry = ttk.Combobox(exam_frame, values=get_exam_history(), width=40)
    exam_entry.pack(side=tk.LEFT)
    if get_exam_history():
        exam_entry.current(0)
    tk.Label(teacher_win, text='选择科目', font=("黑体", 11)).pack(pady=5)
    subject_cb = ttk.Combobox(teacher_win, values=SUBJECT_LIST, state="readonly", width=40)
    subject_cb.current(0)
    subject_cb.pack(pady=3)
    tk.Label(teacher_win, text='选择学生', font=("黑体", 11)).pack(pady=5)
    name_cb = ttk.Combobox(teacher_win, values=sorted(student_users), state="readonly", width=40)
    if student_users:
        name_cb.current(0)
    name_cb.pack(pady=3)
    tk.Label(teacher_win, text='学生成绩（0-100分，支持小数）', font=("黑体", 11)).pack(pady=5)
    score_entry = ttk.Entry(teacher_win, width=40)
    score_entry.pack(pady=3)
    tk.Label(teacher_win, text="已录入成绩列表", font=("黑体", 12)).pack(pady=10)
    score_listbox = tk.Listbox(teacher_win, font=("黑体", 10), fg="#006699", width=70, height=15)
    score_listbox.pack(pady=5)
    refresh_score_list(score_listbox)
    def add_new_score():
        nonlocal exam_entry, subject_cb, name_cb, score_entry
        global is_saved
        exam_name = exam_entry.get().strip()
        subject = subject_cb.get().strip()
        student_name = name_cb.get().strip()
        score_str = score_entry.get().strip()
        if not exam_name:
            messagebox.showerror('输入错误', '考试名称不能为空！')
            return
        if not student_name:
            messagebox.showerror('输入错误', '请选择学生！')
            return
        if not score_str:
            messagebox.showerror('输入错误', '成绩不能为空！')
            return
        try:
            score = float(score_str)
        except ValueError:
            messagebox.showerror('格式错误', '成绩必须是有效数字！')
            return
        if not 0 <= score <= 100:
            messagebox.showerror('范围错误', '成绩必须在 0 ~ 100 分之间！')
            return
        score_key = f"{exam_name}-{subject}-{student_name}"
        if score_key in score_data:
            confirm = messagebox.askyesno("确认覆盖", f"【{exam_name}】{subject}-{student_name} 已有成绩：{score_data[score_key]}分\n是否覆盖为新成绩：{score_str}分？")
            if not confirm:
                return
        score_data[score_key] = score_str
        is_saved = False
        refresh_score_list(score_listbox)
        score_entry.delete(0, tk.END)
        exam_entry.config(values=get_exam_history())
        messagebox.showinfo("成功", "成绩录入成功！")
    btn_frame1 = tk.Frame(teacher_win)
    btn_frame1.pack(pady=5)
    ttk.Button(btn_frame1, text='添加成绩', width=18, command=add_new_score).pack(side=tk.LEFT, padx=5)
    ttk.Button(btn_frame1, text='保存全部成绩', width=18, command=save_score_data).pack(side=tk.LEFT, padx=5)
    btn_frame2 = tk.Frame(teacher_win)
    btn_frame2.pack(pady=5)
    ttk.Button(btn_frame2, text='刷新成绩列表', width=18, command=lambda: refresh_score_list(score_listbox)).pack(side=tk.LEFT, padx=5)
    ttk.Button(btn_frame2, text='批量编辑成绩', width=18, command=lambda: edit_score_window(score_listbox)).pack(side=tk.LEFT, padx=5)
    ttk.Button(teacher_win, text='管理学生账户', width=40, command=lambda: manage_student_account(name_cb)).pack(pady=10)
    def on_teacher_window_close():
        if not is_saved:
            confirm = messagebox.askyesno("未保存提醒", "你有修改后的成绩未保存，关闭窗口会丢失数据！\n是否确认关闭？")
            if confirm:
                teacher_win.destroy()
        else:
            teacher_win.destroy()
    teacher_win.protocol("WM_DELETE_WINDOW", on_teacher_window_close)

main = tk.Tk()
main.title('学生成绩管理系统')
main.geometry('500x400')
main.resizable(False, False)
tk.Label(main, text='学生成绩管理系统', font=("黑体", 20)).pack(pady=60)
tk.Button(main, text="老师管理端", command=teacher_login, font=("黑体", 16), width=20).pack(pady=20)
tk.Button(main, text="学生查询端", command=student_login_window, font=("黑体", 16), width=20).pack(pady=10)
def on_main_window_close():
    if not is_saved:
        confirm = messagebox.askyesno("未保存提醒", "有未保存的成绩数据，关闭程序会丢失！\n是否确认退出？")
        if confirm:
            pass
    else:
        main.destroy()
main.protocol("WM_DELETE_WINDOW", on_main_window_close)
if __name__ == "__main__":
    main.mainloop()