中小学生心理健康筛查评估系统

查看 7|回复 0
作者:hfol85   












[Python] 纯文本查看 复制代码import tkinter as tk
from tkinter import ttk, messagebox, filedialog
import random
from openai import OpenAI
import os
import json
import markdown # Import markdown library
import webbrowser # Import webbrowser for opening html files
import configparser # Import configparser
class MentalHealthApp:
    def __init__(self, root):
        self.root = root
        self.root.title("中小学生心理健康筛查系统")
        self.root.geometry("900x700")
        self.root.configure(bg='#f0f8ff')
        self.root.state('zoomed') # Maximize window on startup
        
        # 定义Markdown渲染所需的字体和标签
        self.setup_markdown_styles()
        # 配置文件路径
        self.config_file = "config.ini"
        
        # DeepSeek API Key (将从配置文件加载或通过对话框输入)
        self.deepseek_api_key = None
        self.client = None
        
        # 尝试加载API Key或提示用户输入
        self.load_or_prompt_api_key()
        
        if not self.deepseek_api_key:
            messagebox.showerror("启动错误", "未能获取DeepSeek API Key,程序将退出。")
            self.root.destroy()
            return
        else:
            self.client = OpenAI(api_key=self.deepseek_api_key, base_url="https://api.deepseek.com")
        # 创建菜单栏
        self.create_menubar()
        # 应用标题
        self.title_frame = tk.Frame(root, bg='#4b86b4')
        self.title_frame.pack(fill='x', pady=(0, 20))
        
        self.title = tk.Label(
            self.title_frame,
            text="中小学生心理健康筛查系统",
            font=('微软雅黑', 24, 'bold'),
            fg='white',
            bg='#4b86b4',
            pady=20
        )
        self.title.pack()
        
        # 创建问题框架
        self.main_frame = tk.Frame(root, bg='#f0f8ff')
        self.main_frame.pack(fill='both', expand=True, padx=40, pady=(0, 20))
        
        # 初始化变量
        self.questions = []
        self.current_question = 0
        self.answers = []
        self.user_info = {}
        
        # 创建问题容器
        self.question_container = tk.Frame(self.main_frame, bg='#f0f8ff')
        self.question_container.pack(fill='both', expand=True)
        
        # 创建用户信息页面
        self.show_user_info_page()
   
    def show_user_info_page(self):
        """显示用户信息输入页面"""
        for widget in self.question_container.winfo_children():
            widget.destroy()
        
        # 用户信息表单
        info_frame = tk.Frame(self.question_container, bg='#e6f2ff', padx=20, pady=20, bd=2, relief='groove')
        info_frame.pack(fill='both', expand=True, padx=20, pady=20)
        
        tk.Label(
            info_frame,
            text="个人信息",
            font=('微软雅黑', 18, 'bold'),
            bg='#e6f2ff',
            fg='#2a4d69'
        ).pack(pady=(0, 20))
        
        # 姓名
        name_frame = tk.Frame(info_frame, bg='#e6f2ff')
        name_frame.pack(fill='x', pady=5)
        tk.Label(name_frame, text="姓名:", font=('微软雅黑', 12), bg='#e6f2ff').pack(side='left', padx=(0, 10))
        self.name_entry = tk.Entry(name_frame, font=('微软雅黑', 12), width=30)
        self.name_entry.pack(side='left', fill='x', expand=True)
        
        # 年龄
        age_frame = tk.Frame(info_frame, bg='#e6f2ff')
        age_frame.pack(fill='x', pady=5)
        tk.Label(age_frame, text="年龄:", font=('微软雅黑', 12), bg='#e6f2ff').pack(side='left', padx=(0, 10))
        self.age_entry = tk.Entry(age_frame, font=('微软雅黑', 12), width=30)
        self.age_entry.pack(side='left', fill='x', expand=True)
        
        # 性别
        gender_frame = tk.Frame(info_frame, bg='#e6f2ff')
        gender_frame.pack(fill='x', pady=5)
        tk.Label(gender_frame, text="性别:", font=('微软雅黑', 12), bg='#e6f2ff').pack(side='left', padx=(0, 10))
        self.gender_var = tk.StringVar(value="男")
        tk.Radiobutton(
            gender_frame, text="男", variable=self.gender_var, value="男",
            font=('微软雅黑', 12), bg='#e6f2ff'
        ).pack(side='left', padx=(0, 20))
        tk.Radiobutton(
            gender_frame, text="女", variable=self.gender_var, value="女",
            font=('微软雅黑', 12), bg='#e6f2ff'
        ).pack(side='left')
        
        # 年级
        grade_frame = tk.Frame(info_frame, bg='#e6f2ff')
        grade_frame.pack(fill='x', pady=5)
        tk.Label(grade_frame, text="年级:", font=('微软雅黑', 12), bg='#e6f2ff').pack(side='left', padx=(0, 10))
        self.grade_var = tk.StringVar()
        grades = ["小学一年级", "小学二年级", "小学三年级", "小学四年级", "小学五年级", "小学六年级",
                 "初中一年级", "初中二年级", "初中三年级",
                 "高中一年级", "高中二年级", "高中三年级"]
        self.grade_combo = ttk.Combobox(grade_frame, textvariable=self.grade_var, values=grades,
                                       font=('微软雅黑', 12), width=27, state="readonly")
        self.grade_combo.current(0)
        self.grade_combo.pack(side='left')
        
        # 开始测试按钮
        start_button = tk.Button(
            info_frame,
            text="开始测试",
            font=('微软雅黑', 14, 'bold'),
            bg='#63a4ff',
            fg='white',
            relief='flat',
            command=self.start_test,
            padx=20,
            pady=10
        )
        start_button.pack(pady=20)
        start_button.bind("", lambda e: start_button.config(bg='#4a86e8'))
        start_button.bind("", lambda e: start_button.config(bg='#63a4ff'))
   
    def start_test(self):
        """开始测试"""
        # 保存用户信息
        self.user_info = {
            "name": self.name_entry.get().strip() or "匿名",
            "age": self.age_entry.get().strip() or "未填写",
            "gender": self.gender_var.get(),
            "grade": self.grade_var.get()
        }
        
        if not self.user_info["name"]:
            messagebox.showwarning("输入错误", "请输入姓名")
            return
        
        # 调用DeepSeek生成题库
        self.generate_questions_with_deepseek()
        
        if not self.questions: # 如果题库生成失败,则不开始测试
            messagebox.showerror("题库生成失败", "未能生成心理健康题库,请稍后重试。")
            return
            
        self.show_question()
   
    def generate_questions_with_deepseek(self):
        """根据用户信息调用DeepSeek API生成心理健康题库"""
        # 显示提示信息窗口
        tip_window = tk.Toplevel(self.root)
        tip_window.title("正在生成题目...")
        tip_window.geometry("400x200")
        tip_window.transient(self.root) # Set to be on top of the main window
        tip_window.grab_set() # Make it modal
        
        tk.Label(tip_window, text="请稍候,DeepSeek AI正在为您生成专属心理健康测试题...",
                 font=('微软雅黑', 12, 'bold'), wraplength=350, justify='center').pack(pady=20)
        tk.Label(tip_window, text="小贴士:保持积极心态,关注情绪变化,及时寻求帮助是心理健康的关键!",
                 font=('微软雅黑', 10), wraplength=350, justify='center', fg='blue').pack(pady=10)
        
        progress_bar = ttk.Progressbar(tip_window, orient='horizontal', length=300, mode='indeterminate')
        progress_bar.pack(pady=10)
        progress_bar.start()
        self.root.update_idletasks() # Update the GUI to show the window
        if not self.deepseek_api_key or self.deepseek_api_key == "YOUR_DEEPSEEK_API_KEY":
            messagebox.showerror("API Key 错误", "请在代码中设置您的DeepSeek API Key 或设置环境变量 DEEPSEEK_API_KEY。")
            self.questions = []
            tip_window.destroy() # Close tip window on error
            return
        user_prompt = f"""
你是一名专业的心理健康评估专家,请根据用户提供的个人信息,为中小学生设计一套心理健康筛查问卷。问卷的特点如下:
1.  **问题侧面提问**:问题应避免直接询问学生的感受,而是通过行为、习惯、反应等侧面反映其心理状态。例如,不直接问"你是否感到焦虑?",而是问"在面对考试或重要任务时,你是否会感到心跳加速、手心出汗?"
2.  **随机性**:每次生成的题库应有所不同,以增加测试的随机性。
3.  **针对用户特点**:结合学生的年龄、性别、年级,生成更贴切、更具代入感的问题。例如,对于小学低年级学生,问题可以更生活化、具体化;对于初高中生,可以涉及学习压力、社交关系等。
4.  **结构化输出**:请以JSON数组格式输出,每个对象包含 'question' (问题内容), 'options' (**务必严格遵循以下两种选项格式之一**), 'category' (问题所属的心理健康维度,例如 '抑郁', '焦虑', '饮食', '自尊', '注意力', '风险', '情绪')。
    *   **普通问题选项**: ["几乎没有", "有时(几天)", "经常(超过一周)", "几乎每天"]
    *   **风险问题选项**: ["从来没有", "偶尔想过", "经常想到", "几乎每天都有这种想法"]
   
    **示例 (普通问题):**
    ```json
    {{"question": "你是否发现自己对以前很喜欢的游戏或活动提不起兴趣?", "options": ["几乎没有", "有时(几天)", "经常(超过一周)", "几乎每天"], "category": "抑郁"}}
    ```
    **示例 (风险问题):**
    ```json
    {{"question": "当遇到非常大的挫折时,你是否会有活得没意思的想法?", "options": ["从来没有", "偶尔想过", "经常想到", "几乎每天都有这种想法"], "category": "风险"}}
    ```
5.  **数量**:生成15个问题。
用户个人信息:
姓名:{self.user_info.get("name", "匿名")}
年龄:{self.user_info.get("age", "未填写")}
性别:{self.user_info.get("gender", "未填写")}
        年级:{self.user_info.get("grade", "未填写")}
        """
        
        try:
            response = self.client.chat.completions.create(
                model="deepseek-chat",
                messages=[
                    {"role": "system", "content": "你是一名专业的心理健康评估专家,请根据学生提供的个人信息设计心理健康筛查问卷,注意问题要侧面提问,并且务必严格遵循规定的JSON数组格式和选项格式。"},
                    {"role": "user", "content": user_prompt}
                ],
                stream=False,
                response_format={"type": "json_object"} # Request JSON output
            )
            
            # 解析JSON响应
            generated_content = response.choices[0].message.content
            # import json # Already imported at the top
            temp_questions = json.loads(generated_content).get("questions", []) # Expecting a key like "questions" in the JSON
            if not isinstance(temp_questions, list):
                raise ValueError("DeepSeek did not return a list of questions.")
            # 校验生成的问题选项格式
            expected_options_normal = ["几乎没有", "有时(几天)", "经常(超过一周)", "几乎每天"]
            expected_options_risk = ["从来没有", "偶尔想过", "经常想到", "几乎每天都有这种想法"]
            
            self.questions = []
            for i, q in enumerate(temp_questions):
                if "question" in q and "options" in q and "category" in q:
                    if q["options"] == expected_options_normal or q["options"] == expected_options_risk:
                        self.questions.append(q)
                    else:
                        print(f"警告:第 {i+1} 个问题选项格式不匹配,已跳过。问题: {q.get("question", "未知")}, 选项: {q.get("options", "未知")}")
                else:
                    print(f"警告:第 {i+1} 个问题缺少必要字段,已跳过。问题内容: {q}")
            # 验证生成的问题数量
            if len(self.questions)  0:
            prev_button = tk.Button(
                nav_frame,
                text="上一题",
                font=('微软雅黑', 12),
                bg='#a3c1ad',
                fg='white',
                command=self.prev_question,
                padx=15
            )
            prev_button.pack(side='left', padx=10)
        
        if self.current_question
    心理健康筛查评估报告
   
   
        body {{ font-family: 'Microsoft YaHei', sans-serif; line-height: 1.6; color: #333; max-width: 800px; margin: 0 auto; padding: 20px; }}
        h1, h2, h3 {{ color: #2c3e50; }}
        h1 {{ text-align: center; border-bottom: 2px solid #eee; padding-bottom: 10px; margin-bottom: 30px; }}
        h2 {{ border-bottom: 1px solid #eee; padding-bottom: 5px; margin-top: 25px; margin-bottom: 15px; }}
        ul {{ list-style-type: disc; margin-left: 20px; }}
        ol {{ list-style-type: decimal; margin-left: 20px; }}
        table {{ width: 100%; border-collapse: collapse; margin-top: 15px; }}
        th, td {{ border: 1px solid #ddd; padding: 8px; text-align: left; }}
        th {{ background-color: #f2f2f2; }}
        pre {{ background-color: #f8f8f8; border: 1px solid #ddd; padding: 10px; overflow-x: auto; }}
        .disclaimer {{ font-size: 0.9em; color: #777; margin-top: 30px; border-top: 1px solid #eee; padding-top: 10px; text-align: center; }}
   

{html_content}

"""
        try:
            # Save to a temporary HTML file
            import tempfile
            temp_file = tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.html', encoding='utf-8')
            temp_file.write(full_html)
            temp_file.close()
            webbrowser.open(f'file://{temp_file.name}')
        except Exception as e:
            messagebox.showerror("打开报告失败", f"无法在浏览器中打开报告: {e}")
    def save_report_as_html(self, markdown_content):
        """将Markdown内容转换为HTML并允许用户保存"""
        print("Debug: Attempting to save report as HTML...") # Debug print
        html_content = markdown.markdown(markdown_content)
        
        # Create a full HTML page (same as open_report_in_browser)
        full_html = f"""
    心理健康筛查评估报告
   
   
        body {{ font-family: 'Microsoft YaHei', sans-serif; line-height: 1.6; color: #333; max-width: 800px; margin: 0 auto; padding: 20px; }}
        h1, h2, h3 {{ color: #2c3e50; }}
        h1 {{ text-align: center; border-bottom: 2px solid #eee; padding-bottom: 10px; margin-bottom: 30px; }}
        h2 {{ border-bottom: 1px solid #eee; padding-bottom: 5px; margin-top: 25px; margin-bottom: 15px; }}
        ul {{ list-style-type: disc; margin-left: 20px; }}
        ol {{ list-style-type: decimal; margin-left: 20px; }}
        table {{ width: 100%; border-collapse: collapse; margin-top: 15px; }}
        th, td {{ border: 1px solid #ddd; padding: 8px; text-align: left; }}
        th {{ background-color: #f2f2f2; }}
        pre {{ background-color: #f8f8f8; border: 1px solid #ddd; padding: 10px; overflow-x: auto; }}
        .disclaimer {{ font-size: 0.9em; color: #777; margin-top: 30px; border-top: 1px solid #eee; padding-top: 10px; text-align: center; }}
   

{html_content}

"""
        file_path = filedialog.asksaveasfilename(
            defaultextension=".html",
            filetypes=[("HTML files", "*.html"), ("All files", "*.*")],
            title="保存评估报告"
        )
        if file_path:
            try:
                with open(file_path, "w", encoding="utf-8") as f:
                    f.write(full_html)
                messagebox.showinfo("保存成功", f"评估报告已保存到:\n{file_path}")
            except Exception as e:
                messagebox.showerror("保存失败", f"保存评估报告失败: {e}")
    def setup_markdown_styles(self):
        """设置Text组件的Markdown渲染样式"""
        # 定义字体
        self.h1_font = ('微软雅黑', 20, 'bold')
        self.h2_font = ('微软雅黑', 16, 'bold')
        self.bold_font = ('微软雅黑', 12, 'bold')
        self.normal_font = ('微软雅黑', 12)
        self.code_font = ('Courier New', 10)
        # 配置Text组件的标签
        # 这些标签需要在Text组件创建后应用
        pass # Actual tag application happens in render_markdown_to_text_widget
    def render_markdown_to_text_widget(self, text_widget, markdown_content):
        """将Markdown内容渲染到Tkinter Text组件中"""
        text_widget.config(state='normal') # Enable editing temporarily
        text_widget.delete('1.0', tk.END)
        # Configure tags (must be done after text_widget is created)
        text_widget.tag_configure('h1', font=self.h1_font, foreground='#2c3e50', justify='center', spacing3=15)
        text_widget.tag_configure('h2', font=self.h2_font, foreground='#2c3e50', spacing3=10)
        text_widget.tag_configure('bold', font=self.bold_font)
        text_widget.tag_configure('normal', font=self.normal_font)
        text_widget.tag_configure('list', lmargin1=20, lmargin2=40)
        text_widget.tag_configure('code', font=self.code_font, background='#f0f0f0', relief='groove', borderwidth=1, lmargin1=20, lmargin2=20)
        text_widget.tag_configure('table_header', font=self.bold_font, background='#f2f2f2', justify='center')
        text_widget.tag_configure('table_cell', font=self.normal_font, justify='left')
        text_widget.tag_configure('disclaimer', font=('微软雅黑', 10, 'italic'), foreground='#777', justify='center', spacing1=10, spacing3=10)
        
        lines = markdown_content.split('\n')
        in_code_block = False
        in_table = False
        table_header_parsed = False
        for line in lines:
            stripped_line = line.strip()
            
            if stripped_line.startswith("```"): # Basic code block detection
                in_code_block = not in_code_block
                if not in_code_block: # End of code block
                    text_widget.insert(tk.END, "\n", 'normal')
                continue
            if in_code_block:
                text_widget.insert(tk.END, line + "\n", 'code')
                continue
            if stripped_line.startswith('# '):
                text_widget.insert(tk.END, stripped_line[2:] + "\n\n", 'h1')
                in_table = False # Reset table state
            elif stripped_line.startswith('## '):
                text_widget.insert(tk.END, stripped_line[3:] + "\n\n", 'h2')
                in_table = False # Reset table state
            elif stripped_line.startswith('* ') or stripped_line.startswith('- '):
                text_widget.insert(tk.END, line + "\n", 'list')
                in_table = False # Reset table state
            elif stripped_line.startswith('| ') and not in_table:
                # Likely a table header, check for separator line next
                text_widget.insert(tk.END, stripped_line + "\n", 'table_header')
                in_table = True
                table_header_parsed = False # Expecting the separator line next
            elif stripped_line.startswith('| :---'): # Table separator line
                # Don't display the separator line, just mark header parsed
                table_header_parsed = True
                text_widget.insert(tk.END, "\n") # Add a newline after table header
            elif stripped_line.startswith('|') and in_table and table_header_parsed:
                # Table row
                cells = [cell.strip() for cell in stripped_line.split('|') if cell.strip()]
                for cell in cells:
                    text_widget.insert(tk.END, f" {cell:

微软, 心理健康

您需要登录后才可以回帖 登录 | 立即注册

返回顶部