diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 107ca74e7..ceb48bc10 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -12,7 +12,7 @@ // Dev container is used for development, so the YPPF_DEBUG environment variable should be set "containerEnv": { "YPPF_DEBUG": "true" - }, + }, "customizations": { "vscode": { "settings": { diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml index 8a28b61f7..9f6c38ce2 100644 --- a/.devcontainer/docker-compose.yml +++ b/.devcontainer/docker-compose.yml @@ -6,26 +6,12 @@ services: volumes: - yppf-mysql-data:/var/lib/mysql - yppf-mysql-config:/etc/mysql - command: - [ - "mysqld", - "--character-set-server=utf8mb4", - "--collation-server=utf8mb4_bin" - ] + command: [ "mysqld", "--character-set-server=utf8mb4", "--collation-server=utf8mb4_bin" ] environment: MYSQL_ROOT_PASSWORD: secret MYSQL_DATABASE: yppf healthcheck: - test: - [ - "CMD", - "mysqladmin", - "ping", - "-h", - "localhost", - "-uroot", - "-psecret" - ] + test: [ "CMD", "mysqladmin", "ping", "-h", "localhost", "-uroot", "-psecret" ] interval: 5s timeout: 5s retries: 100 @@ -38,6 +24,7 @@ services: condition: service_healthy working_dir: /workspace environment: + - YPPF_DEBUG=true - DB_HOST=mysql - DB_USER=root - DB_PASSWORD=secret diff --git a/2025 test b/2025 test new file mode 100644 index 000000000..f634730a2 --- /dev/null +++ b/2025 test @@ -0,0 +1,4 @@ +new 2025 + + + diff --git a/Appointment/summary.py b/Appointment/summary.py index 4d6630d16..38311d46d 100644 --- a/Appointment/summary.py +++ b/Appointment/summary.py @@ -5,6 +5,7 @@ from django.http import HttpRequest from django.shortcuts import render, redirect from django.urls import reverse +from django.contrib import auth from Appointment.models import Room from Appointment.utils.identity import identity_check @@ -186,7 +187,8 @@ def summary2023(request: HttpRequest): user_cancel = request.GET.get('cancel') == 'true' infos = {} - infos.update(logged_in=logged_in, user_accept=user_accept, user_cancel=user_cancel) + infos.update(logged_in=logged_in, user_accept=user_accept, + user_cancel=user_cancel) if not user_accept or not logged_in or user_cancel: # 新生/不接受协议/未登录 展示样例 @@ -195,13 +197,14 @@ def summary2023(request: HttpRequest): infos.update(json.load(f)) if logged_in: with open(os.path.join(base_dir, 'summary2023.json'), 'r') as f: - infos.update(home_Sname=json.load(f)[request.user.username].get('Sname', '')) + infos.update(home_Sname=json.load( + f)[request.user.username].get('Sname', '')) else: # 读取年度总结中该用户的个人数据 with open(os.path.join(base_dir, 'summary2023.json'), 'r') as f: infos.update(json.load(f)[request.user.username]) - infos.update(home_Sname = infos['Sname']) + infos.update(home_Sname=infos['Sname']) # 读取年度总结中该用户的排名数据 with open(os.path.join(base_dir, 'rank2023.json'), 'r') as f: @@ -242,25 +245,33 @@ def summary2023(request: HttpRequest): infos['Discuss_appoint_most_room'] = '' # 将导出数据中iosformat的日期转化为只包含年、月、日的文字 - if infos.get('Discuss_appoint_longest_day'): # None or '' - Discuss_appoint_longest_day = datetime.fromisoformat(infos['Discuss_appoint_longest_day']) - infos['Discuss_appoint_longest_day'] = Discuss_appoint_longest_day.strftime("%Y年%m月%d日") + if infos.get('Discuss_appoint_longest_day'): # None or '' + Discuss_appoint_longest_day = datetime.fromisoformat( + infos['Discuss_appoint_longest_day']) + infos['Discuss_appoint_longest_day'] = Discuss_appoint_longest_day.strftime( + "%Y年%m月%d日") if infos.get('Function_appoint_longest_day'): - Function_appoint_longest_day = datetime.fromisoformat(infos['Function_appoint_longest_day']) - infos['Function_appoint_longest_day'] = Function_appoint_longest_day.strftime("%Y年%m月%d日") + Function_appoint_longest_day = datetime.fromisoformat( + infos['Function_appoint_longest_day']) + infos['Function_appoint_longest_day'] = Function_appoint_longest_day.strftime( + "%Y年%m月%d日") # 对最长研讨室/功能室预约的小时数向下取整 if infos.get('Discuss_appoint_longest_duration'): - Discuss_appoint_longest_day_hours = infos['Discuss_appoint_longest_duration'].split('小时')[0] - infos.update(Discuss_appoint_longest_day_hours = Discuss_appoint_longest_day_hours) + Discuss_appoint_longest_day_hours = infos['Discuss_appoint_longest_duration'].split('小时')[ + 0] + infos.update( + Discuss_appoint_longest_day_hours=Discuss_appoint_longest_day_hours) else: - infos.update(Discuss_appoint_longest_day_hours = 0) + infos.update(Discuss_appoint_longest_day_hours=0) if infos.get('Function_appoint_longest_duration'): - Function_appoint_longest_day_hours = infos['Function_appoint_longest_duration'].split('小时')[0] - infos.update(Function_appoint_longest_day_hours = Function_appoint_longest_day_hours) + Function_appoint_longest_day_hours = infos['Function_appoint_longest_duration'].split('小时')[ + 0] + infos.update( + Function_appoint_longest_day_hours=Function_appoint_longest_day_hours) else: - infos.update(Function_appoint_longest_day_hours = 0) + infos.update(Function_appoint_longest_day_hours=0) # 处理导出共同预约关键词数据格式为[co_keyword, appear_num]的情况 if infos.get('co_keyword'): @@ -273,10 +284,12 @@ def summary2023(request: HttpRequest): # 将list格式的top3最热门课程转化为一个字符串 hottest_courses_23_fall_dict = infos['hottest_courses_23_Fall'] - hottest_course_names_23_fall = '\n'.join([list(dic.keys())[0] for dic in hottest_courses_23_fall_dict]) + hottest_course_names_23_fall = '\n'.join( + [list(dic.keys())[0] for dic in hottest_courses_23_fall_dict]) infos.update(hottest_course_names_23_fall=hottest_course_names_23_fall) hottest_courses_23_spring_dict = infos['hottest_courses_23_Spring'] - hottest_course_names_23_spring = '\n'.join([list(dic.keys())[0] for dic in hottest_courses_23_spring_dict]) + hottest_course_names_23_spring = '\n'.join( + [list(dic.keys())[0] for dic in hottest_courses_23_spring_dict]) infos.update(hottest_course_names_23_spring=hottest_course_names_23_spring) # 根据最长连续签到天数授予用户称号 @@ -309,12 +322,13 @@ def summary2023(request: HttpRequest): else: infos.update(admin_org_names_str=','.join(admin_org_names)) else: - infos.update(admin_org_names_str='') + infos.update(admin_org_names_str='') # 将小组活动预约top3关键词由list转为一个string if infos.get('act_top_three_keywords'): act_top_three_keywords = infos['act_top_three_keywords'] - infos.update(act_top_three_keywords_str=','.join(act_top_three_keywords)) + infos.update(act_top_three_keywords_str=','.join( + act_top_three_keywords)) else: infos.update(act_top_three_keywords_str='') @@ -337,7 +351,8 @@ def summary2023(request: HttpRequest): infos.update(most_act_common_hour_name='') # 计算参与的学生小组+书院课程小组数 - infos.update(club_course_num=infos.get('club_num', 0)+infos.get('course_org_num', 0)) + infos.update(club_course_num=infos.get( + 'club_num', 0)+infos.get('course_org_num', 0)) # 根据已选修书院课程种类数授予成就 type_count = infos.get('type_count', 0) @@ -351,9 +366,12 @@ def summary2023(request: HttpRequest): infos.update(type_count_name='你先别急') # 计算2023年两学期平均书院课程预选数和选中数 - avg_preelect_num = (infos['preelect_course_23fall_num'] + infos['preelect_course_23spring_num']) / 2 - avg_elected_num = (infos['elected_course_23fall_num'] + infos['elected_course_23spring_num']) / 2 - infos.update(avg_preelect_num=avg_preelect_num, avg_elected_num=avg_elected_num) + avg_preelect_num = (infos['preelect_course_23fall_num'] + + infos['preelect_course_23spring_num']) / 2 + avg_elected_num = (infos['elected_course_23fall_num'] + + infos['elected_course_23spring_num']) / 2 + infos.update(avg_preelect_num=avg_preelect_num, + avg_elected_num=avg_elected_num) # 根据盲盒中奖率授予成就 mystery_boxes_num = infos['mystery_boxes_num'] @@ -378,7 +396,6 @@ def summary2023(request: HttpRequest): def summary2024(request: HttpRequest): # 2024年度总结 base_dir = 'static/Appointment/assets/summary_data/summary2024' - logged_in = request.user.is_authenticated if logged_in: username = request.session.get("NP", "") @@ -390,22 +407,24 @@ def summary2024(request: HttpRequest): user_cancel = request.GET.get('cancel') == 'true' infos = {} - infos.update(logged_in=logged_in, user_accept=user_accept, user_cancel=user_cancel) + infos.update(logged_in=logged_in, user_accept=user_accept, + user_cancel=user_cancel) if not user_accept or not logged_in or user_cancel: # 新生/不接受协议/未登录 展示样例 example_file = os.path.join(base_dir, 'template.json') - with open(example_file ,encoding='utf-8') as f: + with open(example_file, encoding='utf-8') as f: infos.update(json.load(f)) if logged_in: with open(os.path.join(base_dir, 'summary2024.json'), 'r', encoding='utf-8') as f: - infos.update(home_Sname=json.load(f)[request.user.username].get('Sname', '')) + infos.update(home_Sname=json.load( + f)[request.user.username].get('Sname', '')) else: # 读取年度总结中该用户的个人数据 with open(os.path.join(base_dir, 'summary2024.json'), 'r', encoding='utf-8') as f: infos.update(json.load(f)[request.user.username]) - infos.update(home_Sname = infos['Sname']) + infos.update(home_Sname=infos['Sname']) # 读取年度总结中该用户的排名数据 with open(os.path.join(base_dir, 'rank2024.json'), 'r', encoding='utf-8') as f: @@ -446,25 +465,33 @@ def summary2024(request: HttpRequest): infos['Discuss_appoint_most_room'] = '' # 将导出数据中iosformat的日期转化为只包含年、月、日的文字 - if infos.get('Discuss_appoint_longest_day'): # None or '' - Discuss_appoint_longest_day = datetime.fromisoformat(infos['Discuss_appoint_longest_day']) - infos['Discuss_appoint_longest_day'] = Discuss_appoint_longest_day.strftime("%Y年%m月%d日") + if infos.get('Discuss_appoint_longest_day'): # None or '' + Discuss_appoint_longest_day = datetime.fromisoformat( + infos['Discuss_appoint_longest_day']) + infos['Discuss_appoint_longest_day'] = Discuss_appoint_longest_day.strftime( + "%Y年%m月%d日") if infos.get('Function_appoint_longest_day'): - Function_appoint_longest_day = datetime.fromisoformat(infos['Function_appoint_longest_day']) - infos['Function_appoint_longest_day'] = Function_appoint_longest_day.strftime("%Y年%m月%d日") + Function_appoint_longest_day = datetime.fromisoformat( + infos['Function_appoint_longest_day']) + infos['Function_appoint_longest_day'] = Function_appoint_longest_day.strftime( + "%Y年%m月%d日") # 对最长研讨室/功能室预约的小时数向下取整 if infos.get('Discuss_appoint_longest_duration'): - Discuss_appoint_longest_day_hours = infos['Discuss_appoint_longest_duration'].split('小时')[0] - infos.update(Discuss_appoint_longest_day_hours = Discuss_appoint_longest_day_hours) + Discuss_appoint_longest_day_hours = infos['Discuss_appoint_longest_duration'].split('小时')[ + 0] + infos.update( + Discuss_appoint_longest_day_hours=Discuss_appoint_longest_day_hours) else: - infos.update(Discuss_appoint_longest_day_hours = 0) + infos.update(Discuss_appoint_longest_day_hours=0) if infos.get('Function_appoint_longest_duration'): - Function_appoint_longest_day_hours = infos['Function_appoint_longest_duration'].split('小时')[0] - infos.update(Function_appoint_longest_day_hours = Function_appoint_longest_day_hours) + Function_appoint_longest_day_hours = infos['Function_appoint_longest_duration'].split('小时')[ + 0] + infos.update( + Function_appoint_longest_day_hours=Function_appoint_longest_day_hours) else: - infos.update(Function_appoint_longest_day_hours = 0) + infos.update(Function_appoint_longest_day_hours=0) # 处理导出共同预约关键词数据格式为[co_keyword, appear_num]的情况 if infos.get('co_keyword'): @@ -477,10 +504,12 @@ def summary2024(request: HttpRequest): # 将list格式的top3最热门课程转化为一个字符串 hottest_courses_23_fall_dict = infos['hottest_courses_24_Fall'] - hottest_course_names_23_fall = '\n'.join([list(dic.keys())[0] for dic in hottest_courses_23_fall_dict]) + hottest_course_names_23_fall = '\n'.join( + [list(dic.keys())[0] for dic in hottest_courses_23_fall_dict]) infos.update(hottest_course_names_23_fall=hottest_course_names_23_fall) hottest_courses_23_spring_dict = infos['hottest_courses_24_Spring'] - hottest_course_names_23_spring = '\n'.join([list(dic.keys())[0] for dic in hottest_courses_23_spring_dict]) + hottest_course_names_23_spring = '\n'.join( + [list(dic.keys())[0] for dic in hottest_courses_23_spring_dict]) infos.update(hottest_course_names_23_spring=hottest_course_names_23_spring) # 处理用户创建学生小组过多的情况 @@ -499,22 +528,27 @@ def summary2024(request: HttpRequest): else: infos.update(admin_org_names_str=','.join(admin_org_names)) else: - infos.update(admin_org_names_str='') + infos.update(admin_org_names_str='') # 将小组活动预约top3关键词由list转为一个string if infos.get('act_top_three_keywords'): act_top_three_keywords = infos['act_top_three_keywords'] - infos.update(act_top_three_keywords_str=','.join(act_top_three_keywords)) + infos.update(act_top_three_keywords_str=','.join( + act_top_three_keywords)) else: infos.update(act_top_three_keywords_str='') # 计算参与的学生小组+书院课程小组数 - infos.update(club_course_num=infos.get('club_num', 0)+infos.get('course_org_num', 0)) + infos.update(club_course_num=infos.get( + 'club_num', 0)+infos.get('course_org_num', 0)) # 计算2023年两学期平均书院课程预选数和选中数 - avg_preelect_num = (infos['preelect_course_23fall_num'] + infos['preelect_course_23spring_num']) / 2 - avg_elected_num = (infos['elected_course_23fall_num'] + infos['elected_course_23spring_num']) / 2 - infos.update(avg_preelect_num=avg_preelect_num, avg_elected_num=avg_elected_num) + avg_preelect_num = (infos['preelect_course_23fall_num'] + + infos['preelect_course_23spring_num']) / 2 + avg_elected_num = (infos['elected_course_23fall_num'] + + infos['elected_course_23spring_num']) / 2 + infos.update(avg_preelect_num=avg_preelect_num, + avg_elected_num=avg_elected_num) # 2024 年新特性(MBTI计算等) # 未使用的 2023 特性没有删除 @@ -554,7 +588,7 @@ def summary2024(request: HttpRequest): infos.update(most_act_common_hour_name='主打一个“月亮不睡我不睡”!') else: infos.update(most_act_common_hour_name='') - + # 根据已选修书院课程种类数授予成就(已根据 2024 文案修改) type_count = infos.get('type_count', 0) if type_count >= 3: @@ -566,7 +600,7 @@ def summary2024(request: HttpRequest): else: infos.update(type_count_name='我自有安排') - #根据用户兑换的奖池奖品数量给出相应的评语 + # 根据用户兑换的奖池奖品数量给出相应的评语 # number_of_unique_prizes_comment = '' # number_of_unique_prizes = infos.get('number_of_unique_prizes', 0) # if number_of_unique_prizes > 0: @@ -634,3 +668,341 @@ def summary2024(request: HttpRequest): infos['sharp_appoint_num_rank_inverse'] = sharp_appoint_num_rank_inverse return render(request, 'Appointment/summary2024.html', infos) + + +def summary2025(request: HttpRequest): + # 2025年度总结 + base_dir = 'raw_data/summary2025' + + # 先展示入口页,让用户选择“登录查看”或“访客查看” + view_mode = request.GET.get('view', '') + if view_mode not in ['login', 'guest']: + return render(request, 'Appointment/summary2025_entry.html', { + 'logged_in': request.user.is_authenticated, + 'login_failed': request.GET.get('login_failed') == '1', + }) + + logged_in = request.user.is_authenticated + infos = {} + # 获取用户真实姓名 + real_name = "访客" + if logged_in: + try: + from app.models import NaturalPerson + real_name = NaturalPerson.objects.get(person_id=request.user).name + except: + real_name = request.user.username + + if logged_in: + username = request.session.get("NP", "") + if username: + from app.utils import update_related_account_in_session + update_related_account_in_session(request, username, shift=True) + + user_accept = request.GET.get('accept') == 'true' + user_cancel = request.GET.get('cancel') == 'true' or view_mode == 'guest' + # 已登录且未取消时,视为展示真实数据(前端通过遮罩控制协议同意流程,避免刷新闪烁) + show_real_data = logged_in and not user_cancel + + infos.update(logged_in=logged_in, user_accept=user_accept, + user_cancel=user_cancel) + + if not show_real_data: + # 新生/不接受协议/未登录 展示样例 + example_file = os.path.join(base_dir, 'template.json') + with open(example_file, encoding='utf-8') as f: + template_data = json.load(f) + # template.json 结构是 {"2300000000": {...}},需要提取第一个用户的数据 + if template_data: + first_key = list(template_data.keys())[0] + infos.update(template_data[first_key]) + # 如果是访客模式,不要读取真实用户数据,即使已登录 + if logged_in and view_mode != 'guest': + with open(os.path.join(base_dir, 'summary2025.json'), 'r', encoding='utf-8') as f: + user_data = json.load(f).get(request.user.username, {}) + infos.update(home_Sname=user_data.get('Sname', '')) + else: + # 读取年度总结中该用户的个人数据 + with open(os.path.join(base_dir, 'summary2025.json'), 'r', encoding='utf-8') as f: + user_data = json.load(f).get(request.user.username, {}) + if user_data: + infos.update(user_data) + else: + # 用户不在数据中,使用模板保底 + with open(os.path.join(base_dir, 'template.json'), 'r', encoding='utf-8') as tf: + template_data = json.load(tf) + if template_data: + first_key = list(template_data.keys())[0] + infos.update(template_data[first_key]) + + infos.update(home_Sname=infos.get('Sname', infos.get('name', ''))) + + # 读取该用户的排名数据 + with open(os.path.join(base_dir, 'rank2025.json'), 'r', encoding='utf-8') as f: + rank_data = json.load(f).get(request.user.username, {}) + if rank_data: + infos.update(rank_data) + print(f"[DEBUG] 用户 {request.user.username} 的排名数据加载成功") + print( + f"[DEBUG] personal_most_frequent_co_appoint: {rank_data.get('personal_most_frequent_co_appoint')}") + else: + print( + f"[DEBUG] 用户 {request.user.username} 不在 rank2025.json 中,将使用模板默认值") + + # 处理姓名显示逻辑 + display_name = infos.get('Sname') or infos.get('name') or real_name + if display_name == "虚拟人": + display_name = real_name + + # 如果是访客模式,强制显示为“访客” + if view_mode == 'guest': + display_name = "访客" + + infos.update(home_Sname=display_name) + if not infos.get('name') or infos.get('name') == "虚拟人" or view_mode == 'guest': + infos['name'] = display_name + if not infos.get('Sname') or infos.get('Sname') == "虚拟人" or view_mode == 'guest': + infos['Sname'] = display_name + + # 读取年度总结中所有用户的总体数据 + with open(os.path.join(base_dir, 'summary_overall_2025.json'), 'r', encoding='utf-8') as f: + infos.update(json.load(f)) + + # 将数据中缺少的项利用template中的默认值补齐 + with open(os.path.join(base_dir, 'template.json'), 'r', encoding='utf-8') as f: + template = json.load(f) + # template.json 结构是 {"2300000000": {...}},需要提取第一个用户的数据 + if template: + first_key = list(template.keys())[0] + template = template[first_key] + + # 递归填充缺失或为null的字段 + def fill_null_fields(data_dict, template_dict): + for key, template_value in template_dict.items(): + if key not in data_dict or data_dict[key] is None: + data_dict[key] = template_value + elif isinstance(template_value, dict) and isinstance(data_dict.get(key), dict): + fill_null_fields(data_dict[key], template_value) + + fill_null_fields(infos, template) + + # 字段名兼容:如果没有 personal_most_frequent_co_appoint 但有 organization_most_frequent_co_appoint,则复制 + # 这个逻辑必须在 fill_rank_null_fields 之前执行,否则会被 rank-template 的默认值覆盖 + if 'personal_most_frequent_co_appoint' not in infos or not infos.get('personal_most_frequent_co_appoint'): + if infos.get('organization_most_frequent_co_appoint'): + infos['personal_most_frequent_co_appoint'] = infos['organization_most_frequent_co_appoint'] + print( + f"[DEBUG] 使用 organization_most_frequent_co_appoint 作为 personal_most_frequent_co_appoint: {infos['personal_most_frequent_co_appoint']}") + + # 将 rank 数据中缺少的项利用 rank-template 中的默认值补齐 + with open(os.path.join(base_dir, 'rank-template.json'), 'r', encoding='utf-8') as f: + rank_template = json.load(f) + # rank-template.json 结构也是 {"2300000000": {...}} + if rank_template: + first_key = list(rank_template.keys())[0] + rank_template = rank_template[first_key] + + # 递归填充缺失或为null的字段(但跳过已经存在且有值的字段) + def fill_rank_null_fields(data_dict, template_dict): + for key, template_value in template_dict.items(): + # 跳过 personal_most_frequent_co_appoint,如果没有真实数据就让它保持为空 + if key == 'personal_most_frequent_co_appoint': + continue + if key not in data_dict or data_dict[key] is None: + data_dict[key] = template_value + elif isinstance(template_value, dict) and isinstance(data_dict.get(key), dict): + fill_rank_null_fields(data_dict[key], template_value) + + fill_rank_null_fields(infos, rank_template) + + # 处理空字符串的 usage 字段 + def fix_empty_usage(data_dict, parent_key=''): + """递归将空字符串的 usage 字段替换为合理默认值""" + if isinstance(data_dict, dict): + # 如果当前字典有 usage 字段且值为空字符串,则替换 + if 'usage' in data_dict and data_dict['usage'] == '': + # 根据父键设置合理的默认值 + if 'underground' in parent_key or 'study' in parent_key: + data_dict['usage'] = '自习' + elif 'talk' in parent_key or 'func' in parent_key: + data_dict['usage'] = '小组讨论' + else: + data_dict['usage'] = '预约活动' + print(f"[DEBUG] 修复了 {parent_key}.usage: {data_dict['usage']}") + + # 递归处理嵌套字典 + for key, value in data_dict.items(): + if isinstance(value, dict): + fix_empty_usage( + value, parent_key=f"{parent_key}.{key}" if parent_key else key) + + fix_empty_usage(infos) + + # 判断用户是否为新用户(2025年注册) + date_joined_str = infos.get('date_joined') + is_new_user = False + if date_joined_str: + try: + date_joined_obj = datetime.fromisoformat(date_joined_str) if isinstance( + date_joined_str, str) else date_joined_str + is_new_user = date_joined_obj.year >= 2025 + except: + is_new_user = False + infos['is_new_user'] = is_new_user + + # 计算用户自注册起至今过去的天数(2025 days己计算) + + # 将导出数据中iosformat的日期转化为只包含年、月、日的文字date_joined"# 注册日期(2025update) + longest_usage = infos.get("longest_underground_usage", {}) + start_date = longest_usage.get("longest_continuous_start_date") + end_date = longest_usage.get("longest_continuous_end_date") + if infos.get('date_joined'): # None or '' + date_joined = datetime.fromisoformat(infos['date_joined']) + infos['date_joined'] = date_joined.strftime("%Y年%m月%d日") + if start_date: + start_date = datetime.fromisoformat(start_date) + if "longest_underground_usage" not in infos: + infos["longest_underground_usage"] = {} + infos["longest_underground_usage"]["longest_continuous_start_date"] = start_date.strftime( + "%Y年%m月%d日") + if end_date: + end_date = datetime.fromisoformat(end_date) + if "longest_underground_usage" not in infos: + infos["longest_underground_usage"] = {} + infos["longest_underground_usage"]["longest_continuous_end_date"] = end_date.strftime( + "%Y年%m月%d日") + + # 对最长研讨室/功能室预约的小时数向下取整 + if infos.get('Discuss_appoint_longest_duration'): + Discuss_appoint_longest_day_hours = infos['Discuss_appoint_longest_duration'].split('小时')[ + 0] + infos.update( + Discuss_appoint_longest_day_hours=Discuss_appoint_longest_day_hours) + else: + infos.update(Discuss_appoint_longest_day_hours=0) + + if infos.get('Function_appoint_longest_duration'): + Function_appoint_longest_day_hours = infos['Function_appoint_longest_duration'].split('小时')[ + 0] + infos.update( + Function_appoint_longest_day_hours=Function_appoint_longest_day_hours) + else: + infos.update(Function_appoint_longest_day_hours=0) + + # 2025新特性 + # 根据刷卡/预约记录总天数超越百分比显示文字 + underground_usage_percentile = infos.get('underground_usage_percentile', 0) + if underground_usage_percentile is None: + underground_usage_percentile = 0 + if underground_usage_percentile <= 50: + infos.update(underground_usage_percentile_name='新的一年,期待着和你遇见!') + elif underground_usage_percentile <= 85: + infos.update(underground_usage_percentile_name='新的一年,我依然在这里等你。') + else: + infos.update(underground_usage_percentile_name='我宣布,没有人比你更了解地下室!') + # 如果该用户刷卡/预约记录为0,则下面三部分不保留 + record_is_zero = (infos.get('underground_usage_days', 0) == 0) + infos.update(record_is_zero=record_is_zero) + # 用户在统计周期内刷卡/预约记录的最早日期自习室研讨室类型? + study_room_list = ['B108', 'B112', 'B118', 'B106', 'B119', 'B114'] + room = infos.get("first_underground_record", {}).get("room") + first_room_study = False + last_room_study = False + if room in study_room_list: + first_room_study = True + room = infos.get("last_underground_record", {}).get("room") + if room in study_room_list: + last_room_study = True + infos.update(first_room_study=first_room_study) + infos.update(last_room_study=last_room_study) + # 研讨室/功能室预约时长最长的日期的参与人数分类前端? + # 房间预约-“最期待”中的数字和单位问题 + average_diff = infos.get("appoint_habit", {}).get('average_diff') + max_diff = infos.get("appoint_habit", {}).get('max_diff') + average_diff_time = '小时' + max_diff_time = '小时' + if isinstance(average_diff, (int, float)) and average_diff > 0: + if average_diff > 24: + average_diff_time = '天' + average_diff = average_diff//24 + else: + average_diff = 0 # None/非数值兜底为0 + + if isinstance(max_diff, (int, float)) and max_diff > 0: + if max_diff and max_diff > 24: + max_diff_time = '天' + max_diff = max_diff//24 + else: + max_diff = 0 + infos.update(average_diff_time=average_diff_time, + max_diff_time=max_diff_time) + # 确保 appoint_habit 是字典 + if not isinstance(infos.get("appoint_habit"), dict): + infos['appoint_habit'] = {} + infos['appoint_habit']['average_diff'] = average_diff + infos['appoint_habit']['max_diff'] = max_diff + + # 为talk_and_func_room_longest_record添加participant_num字段 + # 如果原数据没有,则用talk_room_average_participant_num判断 + talk_and_func_record = infos.get('talk_and_func_room_usage', {}).get( + 'talk_and_func_room_longest_record') + if talk_and_func_record and isinstance(talk_and_func_record, dict): + if 'participant_num' not in talk_and_func_record: + # 使用平均参与人数作为判断依据 + avg_participant = infos.get('talk_and_func_room_usage', {}).get( + 'talk_room_average_participant_num', 2) + # 如果平均人数<=1,认为是个人使用;否则认为是多人使用 + talk_and_func_record['participant_num'] = 1 if avg_participant <= 1 else 2 + + # 个人/集体预约分类 + + # 如果担任职务小组数量为0,则该行不保留 + org_reserved = True + org_usage = infos.get("org_usage", {}) + if org_usage and org_usage.get('org_num'): + org_name_list = org_usage.get('org_name_list', []) + if len(org_name_list) == 0: + org_reserved = False + infos.update(org_reserved=org_reserved) + + # 处理用户担任admin职务的小组数过多的情况(2025变量名org_name_list_str) + org_name_list = infos.get("org_usage", {}).get('org_name_list', []) + admin_org_num = len(org_name_list) + if admin_org_num: + if admin_org_num > 3: + org_name_list = org_name_list[:3] + infos.update(org_name_list_str=','.join(org_name_list) + '等') + else: + infos.update(org_name_list_str=','.join(org_name_list)) + else: + infos.update(org_name_list_str='') + + # 计算已选修书院课程种类数 + course_type_str = infos.get('course_usage', {}).get('course_type_str', '') + if course_type_str: + course_type_list = course_type_str.split() + infos['course_type_count'] = len(course_type_list) + infos['course_type_str_formatted'] = '、'.join(course_type_list) + else: + infos['course_type_count'] = 0 + infos['course_type_str_formatted'] = '' + + return render(request, 'Appointment/summary2025.html', infos) + + +def summary2025_login(request: HttpRequest): + if request.method != 'POST': + return redirect(reverse('Appointment:summary2025')) + + username = (request.POST.get('username') or '').strip() + password = request.POST.get('password') or '' + + if not username or not password: + return redirect(f"{reverse('Appointment:summary2025')}?login_failed=1") + + user = auth.authenticate(username=username, password=password) + if user is None: + return redirect(f"{reverse('Appointment:summary2025')}?login_failed=1") + + auth.login(request, user) + return redirect(f"{reverse('Appointment:summary2025')}?view=login") diff --git a/Appointment/urls.py b/Appointment/urls.py index a6b488867..7a5f145a2 100644 --- a/Appointment/urls.py +++ b/Appointment/urls.py @@ -52,5 +52,7 @@ path('summary', summary.summary, name='summary'), path('summary/2021', summary.summary2021, name='summary2021'), path('summary/2023', summary.summary2023, name='summary2023'), - path('summary/2024', summary.summary2024, name='summary2024') + path('summary/2024', summary.summary2024, name='summary2024'), + path('summary/2025/login', summary.summary2025_login, name='summary2025_login'), + path('summary/2025', summary.summary2025, name='summary2025') ] diff --git a/dm/management/commands/dump_summary2025.py b/dm/management/commands/dump_summary2025.py new file mode 100644 index 000000000..82866c472 --- /dev/null +++ b/dm/management/commands/dump_summary2025.py @@ -0,0 +1,1415 @@ +# 2025年度总结数据导出脚本 + +# 引入必要的库 +import json +from collections import defaultdict +from datetime import datetime, time, date, timedelta +from django.core.management.base import BaseCommand +from django.db.models import Avg, Count, Q, Sum +from app.models import * +from Appointment.models import Appoint, CardCheckInfo, Room +from Appointment.utils.identity import get_participant +from utils.models.query import * +from utils.models.semester import Semester +from generic.models import User, YQPointRecord + +# 定义常量 +SUMMARY_YEAR = 2025 +SUMMARY_SEM_START = datetime(SUMMARY_YEAR, 1, 19) +SUMMARY_SEM_END = datetime(SUMMARY_YEAR + 1, 1, 19) + +LAST_YEAR_SUMMARY_START = datetime(SUMMARY_YEAR - 1, 1, 16) +LAST_YEAR_SUMMARY_END = datetime(SUMMARY_YEAR, 1, 18) + +# 硬编码三类房间的列表 Rid,包括自习室、研讨室、功能房 +study_room_list = ['B108', 'B112', 'B118', 'B106', 'B119', 'B114'] +talk_room_list = ['B104', 'B107A', 'B107B', 'B111', 'B206', 'R113', 'R201'] +func_room_list = ['B217', 'B220', 'B221', 'B207', + 'B208', 'B214', 'B206', 'B216', 'B111', 'B104'] + +# 本年度书院课「课程 id -> 选中人数/预选人数 比例」缓存,在 handle 中先调用 cal_select_course_ratio() 写入 +_select_course_ratio_cache = None +# 本年度书院课「课程 id -> {preselect_count, success_count}」缓存 +_course_stats_cache = None + +# 所有用户「用户名 -> 刷卡或预约总天数」缓存,在处理个人数据时收集 +_underground_usage_days_cache = {} + +# 所有用户「用户名 -> 个人刷卡次数最多的自习室」缓存,在处理个人数据时收集 +_study_room_top_cache = {} + +# 所有用户「用户名 -> 最多学时书院课」缓存,在处理个人数据时收集 +_most_hours_course_cache = {} + +# 功能性函数 + + +def datetime_converter(o): + """供 json.dump 序列化 datetime/time/date 使用""" + if isinstance(o, (datetime, time, date)): + return o.isoformat() + raise TypeError( + f'Object of type {type(o).__name__} is not JSON serializable') + + +# 定义子模块数据处理函数(个人信息部分) + +def get_user_register_date_and_days(user: 'User|NaturalPerson'): + """ + 获取用户账号注册日期和注册天数。 + 返回: (注册日期, 注册天数) 或 None + """ + if isinstance(user, User): + person = NaturalPerson.objects.get_by_user(user) + else: + person = user + + if person is None: + return None + + user_obj = person.get_user() + if user_obj is None: + return None + + date_joined = user_obj.date_joined + days = (SUMMARY_SEM_END - date_joined).days + return { + 'date_joined': date_joined.strftime('%Y-%m-%d'), + 'days': days, + } + +# 个人地下室使用总览 + +# 个人有刷卡或预约记录的总天数 + + +def get_person_underground_usage(person: 'NaturalPerson'): + """ + 获取个人有刷卡或预约记录的总天数(去重后的日期数)。 + """ + user = person.get_user() + if user is None: + return 0 + + card_dates = set( + CardCheckInfo.objects.filter( + Cardstudent__Sid=user, + Cardtime__gt=SUMMARY_SEM_START, + Cardtime__lt=SUMMARY_SEM_END + ).values_list('Cardtime__date', flat=True).distinct() + ) + if user is None: + appoint_dates = set() + else: + appoint_dates = set( + Appoint.objects.filter( + students__Sid=user, + Astart__gt=SUMMARY_SEM_START, + Astart__lt=SUMMARY_SEM_END + ).values_list('Astart__date', flat=True).distinct() + ) + return len(card_dates | appoint_dates) + +# 个人本年度首条刷卡/预约记录:日期、房间号、预约关键词(如有) + + +def get_person_first_underground_record(person: 'NaturalPerson'): + """ + 获取个人本年度首条刷卡/预约记录。 + 对比第一条刷卡记录和第一条预约记录,返回时间更早的记录。 + 返回: {'date': 日期, 'room': 房间号, 'usage': 预约关键词} 或 None + """ + user = person.get_user() + if user is None: + card_record = None + else: + card_record = CardCheckInfo.objects.filter( + Cardstudent__Sid=user, + Cardtime__gt=SUMMARY_SEM_START, + Cardtime__lt=SUMMARY_SEM_END + ).select_related('Cardroom').order_by('Cardtime').first() + + user = person.get_user() + if user is None: + appoint_record = None + else: + appoint_record = Appoint.objects.filter( + students__Sid=user, + Astart__gt=SUMMARY_SEM_START, + Astart__lt=SUMMARY_SEM_END + ).select_related('Room').order_by('Astart').first() + + if card_record is None and appoint_record is None: + return None + + if card_record is None: + if appoint_record and appoint_record.Room: + return { + 'date': appoint_record.Astart.strftime('%Y年%m月%d日'), + 'room': appoint_record.Room.Rid, + 'usage': appoint_record.Ausage or '', + } + elif appoint_record: + return { + 'date': appoint_record.Astart.strftime('%Y年%m月%d日'), + 'room': None, + 'usage': appoint_record.Ausage or '', + } + return None + + if appoint_record is None: + if card_record.Cardroom: + return { + 'date': card_record.Cardtime.strftime('%Y年%m月%d日'), + 'room': card_record.Cardroom.Rid, + 'usage': '', + } + else: + return { + 'date': card_record.Cardtime.strftime('%Y年%m月%d日'), + 'room': None, + 'usage': '', + } + + if card_record.Cardtime < appoint_record.Astart: + if card_record.Cardroom: + return { + 'date': card_record.Cardtime.strftime('%Y年%m月%d日'), + 'room': card_record.Cardroom.Rid, + 'usage': '', + } + else: + return { + 'date': card_record.Cardtime.strftime('%Y年%m月%d日'), + 'room': None, + 'usage': '', + } + + if appoint_record.Room: + return { + 'date': appoint_record.Astart.strftime('%Y年%m月%d日'), + 'room': appoint_record.Room.Rid, + 'usage': appoint_record.Ausage or '', + } + else: + return { + 'date': appoint_record.Astart.strftime('%Y年%m月%d日'), + 'room': None, + 'usage': appoint_record.Ausage or '', + } + +# 3.个人本年度末条刷卡/预约记录:日期、房间号、预约关键词(如有) + + +def get_person_last_underground_record(person: 'NaturalPerson'): + """ + 获取个人本年度末条刷卡/预约记录。 + 对比最后一条刷卡记录和最后一条预约记录,返回时间更晚的记录。 + 返回: {'date': 日期, 'room': 房间号, 'usage': 预约关键词} 或 None + """ + user = person.get_user() + if user is None: + card_record = None + else: + card_record = CardCheckInfo.objects.filter( + Cardstudent__Sid=user, + Cardtime__gt=SUMMARY_SEM_START, + Cardtime__lt=SUMMARY_SEM_END + ).select_related('Cardroom').order_by('-Cardtime').first() + + user = person.get_user() + if user is None: + appoint_record = None + else: + appoint_record = Appoint.objects.filter( + students__Sid=user, + Astart__gt=SUMMARY_SEM_START, + Astart__lt=SUMMARY_SEM_END + ).select_related('Room').order_by('-Astart').first() + + if card_record is None and appoint_record is None: + return None + + if card_record is None: + if appoint_record and appoint_record.Room: + return { + 'date': appoint_record.Astart.strftime('%Y年%m月%d日'), + 'room': appoint_record.Room.Rid, + 'usage': appoint_record.Ausage or '', + } + elif appoint_record: + return { + 'date': appoint_record.Astart.strftime('%Y年%m月%d日'), + 'room': None, + 'usage': appoint_record.Ausage or '', + } + return None + + if appoint_record is None: + if card_record.Cardroom: + return { + 'date': card_record.Cardtime.strftime('%Y年%m月%d日'), + 'room': card_record.Cardroom.Rid, + 'usage': '', + } + else: + return { + 'date': card_record.Cardtime.strftime('%Y年%m月%d日'), + 'room': None, + 'usage': '', + } + + if card_record.Cardtime > appoint_record.Astart: + if card_record.Cardroom: + return { + 'date': card_record.Cardtime.strftime('%Y年%m月%d日'), + 'room': card_record.Cardroom.Rid, + 'usage': '', + } + else: + return { + 'date': card_record.Cardtime.strftime('%Y年%m月%d日'), + 'room': None, + 'usage': '', + } + + if appoint_record.Room: + return { + 'date': appoint_record.Astart.strftime('%Y年%m月%d日'), + 'room': appoint_record.Room.Rid, + 'usage': appoint_record.Ausage or '', + } + else: + return { + 'date': appoint_record.Astart.strftime('%Y年%m月%d日'), + 'room': None, + 'usage': appoint_record.Ausage or '', + } + +# 个人最长连续刷卡/预约天数,起止日期 + + +def get_person_longest_underground_usage(person: 'NaturalPerson'): + """ + 获取个人最长连续刷卡/预约天数及起止日期。 + 获取所有记录,按日期遍历,记录连续天数,如果中断则重置。 + """ + # 获取所有记录日期(去重) + user = person.get_user() + if user is None: + card_dates = set() + else: + card_dates = set( + CardCheckInfo.objects.filter( + Cardstudent__Sid=user, + Cardtime__gt=SUMMARY_SEM_START, + Cardtime__lt=SUMMARY_SEM_END + ).values_list('Cardtime__date', flat=True).distinct() + ) + if user is None: + appoint_dates = set() + else: + appoint_dates = set( + Appoint.objects.filter( + students__Sid=user, + Astart__gt=SUMMARY_SEM_START, + Astart__lt=SUMMARY_SEM_END + ).values_list('Astart__date', flat=True).distinct() + ) + all_dates = sorted(card_dates | appoint_dates) + + if not all_dates: + return { + 'longest_continuous_days': 0, + 'longest_continuous_start_date': None, + 'longest_continuous_end_date': None, + } + + longest_continuous_days = 0 + longest_continuous_start_date = None + longest_continuous_end_date = None + current_continuous_days = 0 + current_continuous_start_date = None + previous_date = None + + for current_date in all_dates: + if previous_date is None: + # 第一条记录 + current_continuous_days = 1 + current_continuous_start_date = current_date + elif (current_date - previous_date).days == 1: + # 连续日期 + current_continuous_days += 1 + else: + # 中断,重置 + current_continuous_days = 1 + current_continuous_start_date = current_date + + current_continuous_end_date = current_date + + if current_continuous_days > longest_continuous_days: + longest_continuous_days = current_continuous_days + longest_continuous_start_date = current_continuous_start_date + longest_continuous_end_date = current_continuous_end_date + + previous_date = current_date + + return { + 'longest_continuous_days': longest_continuous_days, + 'longest_continuous_start_date': longest_continuous_start_date.strftime('%Y-%m-%d') if longest_continuous_start_date else None, + 'longest_continuous_end_date': longest_continuous_end_date.strftime('%Y-%m-%d') if longest_continuous_end_date else None, + } + +# 自习室部分统计 + + +def get_person_study_room_usage(person: 'NaturalPerson'): + """ + 统计个人自习室使用情况: + - 本年度自习室刷卡总次数 + - 刷卡次数最多的自习室及次数(次数相同则返回最早的房间号) + - 去年刷卡次数最多的自习室及次数 + - 两者是否相同 + """ + # 获取个人本年度自习室刷卡总次数 + user = person.get_user() + if user is None: + study_room_num = 0 + study_room_dict = {} + else: + study_room_num = CardCheckInfo.objects.filter( + Cardstudent__Sid=user, + Cardroom__Rid__in=study_room_list, + Cardtime__gt=SUMMARY_SEM_START, + Cardtime__lt=SUMMARY_SEM_END + ).count() + + # 分自习室房间号统计,返回字典(房间号: 刷卡次数) + study_room_dict = defaultdict(int) + for record in CardCheckInfo.objects.filter( + Cardstudent__Sid=user, + Cardroom__Rid__in=study_room_list, + Cardtime__gt=SUMMARY_SEM_START, + Cardtime__lt=SUMMARY_SEM_END).select_related('Cardroom'): + if record.Cardroom: + study_room_dict[record.Cardroom.Rid] += 1 + + if study_room_dict: + study_room_top, study_room_top_num = max( + study_room_dict.items(), key=lambda x: (x[1], -ord(x[0][0]) if x[0] else 0)) + else: + study_room_top, study_room_top_num = None, 0 + + # 获取去年刷卡次数最多的自习室 + user = person.get_user() + last_year_study_room_dict = defaultdict(int) + if user is not None: + for record in CardCheckInfo.objects.filter( + Cardstudent__Sid=user, + Cardroom__Rid__in=study_room_list, + Cardtime__gt=LAST_YEAR_SUMMARY_START, + Cardtime__lt=LAST_YEAR_SUMMARY_END).select_related('Cardroom'): + if record.Cardroom: + last_year_study_room_dict[record.Cardroom.Rid] += 1 + + if last_year_study_room_dict: + last_year_study_room_top, last_year_study_room_top_num = max( + last_year_study_room_dict.items(), key=lambda x: (x[1], -ord(x[0][0]) if x[0] else 0)) + else: + last_year_study_room_top, last_year_study_room_top_num = None, 0 + + return { + 'study_room_num': study_room_num, + 'study_room_top': study_room_top, + 'study_room_top_num': study_room_top_num, + 'last_year_study_room_top': last_year_study_room_top, + 'last_year_study_room_top_num': last_year_study_room_top_num, + 'is_same_as_last_year': study_room_top == last_year_study_room_top, + } + +# 统计个人使用研讨室&功能房总次数 + + +def get_person_talk_and_func_room_usage(person: 'NaturalPerson'): + """ + 统计个人研讨室和功能房使用情况: + - 研讨室预约总次数、总时长、平均参与人数 + - 功能房预约总次数、总时长 + - 预约时长最长的一次记录(日期、房间号、时长、预约关键词) + """ + # 1. 获取个人本年度研讨室预约总次数 + user = person.get_user() + if user is None: + talk_room_num = 0 + talk_room_hour = 0.0 + talk_room_average_participant_num = 0 + func_room_num = 0 + func_room_hour = 0.0 + all_room_appoints = Appoint.objects.none() + else: + talk_room_appoints = Appoint.objects.filter( + students__Sid=user, + Room__Rid__in=talk_room_list, + Astart__gt=SUMMARY_SEM_START, + Astart__lt=SUMMARY_SEM_END + ) + talk_room_num = talk_room_appoints.count() + + # 2. 计算个人本年度研讨室预约总时长(单位:小时,保留一位小数) + talk_room_durations = [ + (finish - start).total_seconds() + for start, finish in talk_room_appoints.values_list('Astart', 'Afinish') + ] + talk_room_hour = round(sum(talk_room_durations) / + 3600.0, 1) if talk_room_durations else 0.0 + + # 3. 计算个人预约研讨室的平均参与人数(默认为0) + # 使用 Count 统计每个预约的参与者数量,然后计算平均值 + talk_room_with_count = talk_room_appoints.annotate( + participant_count=Count('students') + ) + participant_counts = [ + a.participant_count for a in talk_room_with_count] + talk_room_average_participant_num = round( + sum(participant_counts) / len(participant_counts), 1) if participant_counts else 0 + + # 4. 个人功能房预约总次数 + func_room_appoints = Appoint.objects.filter( + students__Sid=user, + Room__Rid__in=func_room_list, + Astart__gt=SUMMARY_SEM_START, + Astart__lt=SUMMARY_SEM_END + ) + func_room_num = func_room_appoints.count() + + # 5. 计算个人本年度功能房预约总时长(单位:小时,保留一位小数) + func_room_durations = [ + (finish - start).total_seconds() + for start, finish in func_room_appoints.values_list('Astart', 'Afinish') + ] + func_room_hour = round(sum(func_room_durations) / + 3600.0, 1) if func_room_durations else 0.0 + + # 6. 找出本年度预约中时长最长的一次预约记录 + all_room_appoints = Appoint.objects.filter( + students__Sid=user, + Room__Rid__in=talk_room_list + func_room_list, + Astart__gt=SUMMARY_SEM_START, + Astart__lt=SUMMARY_SEM_END + ).select_related('Room') + + talk_and_func_room_longest_record = None + longest_duration = timedelta(0) + for appoint in all_room_appoints: + duration = appoint.Afinish - appoint.Astart + if duration > longest_duration: + longest_duration = duration + talk_and_func_room_longest_record = appoint + + longest_record_info = None + if talk_and_func_room_longest_record: + longest_record_info = { + 'date': talk_and_func_room_longest_record.Astart.strftime('%Y年%m月%d日'), + 'room': talk_and_func_room_longest_record.Room.Rid if talk_and_func_room_longest_record.Room else None, + 'hour': round(longest_duration.total_seconds() / 3600.0, 1), + 'usage': talk_and_func_room_longest_record.Ausage or '', + } + + return { + 'talk_room_num': talk_room_num, + 'talk_room_hour': talk_room_hour, + 'talk_room_average_participant_num': round(talk_room_average_participant_num, 1) if talk_room_average_participant_num else 0, + 'func_room_num': func_room_num, + 'func_room_hour': func_room_hour, + 'talk_and_func_room_longest_record': longest_record_info, + } + +# 预约习惯统计部分 + + +def get_person_appoint_habit(person: 'NaturalPerson'): + """ + 1. 申请(创建)时间到开始时间的时间差:平均差值(小时)、最大差值、最大差值对应记录(日期、房间号、预约关键词) + 2. 按房间号统计个人预约次数最多的房间号,返回房间号、次数 + 3. 按类别统计当天预约与临时预约次数;当天预约中最小时间差及对应记录(日期、房间号、预约关键词) + """ + user = person.get_user() + if user is None: + appoint_list = [] + n = 0 + else: + appoint_records = Appoint.objects.filter( + students__Sid=user, + Astart__gt=SUMMARY_SEM_START, + Astart__lt=SUMMARY_SEM_END, + ).select_related('Room') + appoint_list = list(appoint_records) + n = len(appoint_list) + + if n == 0: + return { + 'average_diff': None, + 'max_diff': None, + 'max_diff_record': None, + 'room_num_top': None, + 'room_num_top_num': None, + 'day_appoint_num': 0, + 'temporary_appoint_num': 0, + 'day_appoint_min_diff': None, + 'day_appoint_min_diff_record': None, + } + + # 1. 申请到开始的时间差:平均(小时)、最大(小时)、最大差值对应的一条记录 + create_start_list = [(r.Atime, r.Astart) for r in appoint_list] + diffs_seconds = [(start - create).total_seconds() + for create, start in create_start_list] + average_diff = round(sum(diffs_seconds) / 3600.0 / n, 1) + max_diff_seconds = max(diffs_seconds) + max_diff_hours = round(max_diff_seconds / 3600.0, 1) + max_diff_idx = diffs_seconds.index(max_diff_seconds) + max_diff_record_obj = appoint_list[max_diff_idx] + max_diff_record = { + 'date': max_diff_record_obj.Astart.strftime('%Y年%m月%d日'), + 'room': max_diff_record_obj.Room.Rid if max_diff_record_obj.Room else None, + 'usage': max_diff_record_obj.Ausage, + } + + # 2. 按房间号统计,预约次数最多的房间号及次数 + room_num_dict = defaultdict(int) + for r in appoint_list: + if r.Room: + room_num_dict[r.Room.Rid] += 1 + room_num_top, room_num_top_num = max( + room_num_dict.items(), key=lambda x: x[1]) + + # 3. 当天预约 / 临时预约次数;当天预约中最小时间差及对应记录 + day_appoint_num = sum( + 1 for r in appoint_list if r.Atype == Appoint.Type.TODAY) + temporary_appoint_num = sum( + 1 for r in appoint_list if r.Atype == Appoint.Type.TEMPORARY) + day_appoints = [r for r in appoint_list if r.Atype == Appoint.Type.TODAY] + if day_appoints: + day_diffs = [(r.Astart - r.Atime).total_seconds() + for r in day_appoints] + day_appoint_min_diff_seconds = min(day_diffs) + day_appoint_min_diff = round(day_appoint_min_diff_seconds / 3600.0, 1) + day_min_record_obj = day_appoints[day_diffs.index( + day_appoint_min_diff_seconds)] + day_appoint_min_diff_record = { + 'date': day_min_record_obj.Astart.strftime('%Y年%m月%d日'), + 'room': day_min_record_obj.Room.Rid if day_min_record_obj.Room else None, + 'usage': day_min_record_obj.Ausage, + } + else: + day_appoint_min_diff = None + day_appoint_min_diff_record = None + + return { + 'average_diff': average_diff, + 'max_diff': max_diff_hours, + 'max_diff_record': max_diff_record, + 'room_num_top': room_num_top, + 'room_num_top_num': room_num_top_num, + 'day_appoint_num': day_appoint_num, + 'temporary_appoint_num': temporary_appoint_num, + 'day_appoint_min_diff': day_appoint_min_diff, + 'day_appoint_min_diff_record': day_appoint_min_diff_record, + } + + +def get_person_login_days(person: 'NaturalPerson'): + """ + 获取个人年度登录总天数(去重后的日期数)。 + """ + user = person.get_user() + if user is None: + return 0 + + login_dates = YQPointRecord.objects.filter( + user=user, + source_type=YQPointRecord.SourceType.CHECK_IN, + time__gt=SUMMARY_SEM_START, + time__lt=SUMMARY_SEM_END + ).values_list('time__date', flat=True).distinct() + + return len(set(login_dates)) + +# 处理小组相关内容 + + +def get_person_org_usage(person: 'NaturalPerson'): + """ + 统计个人小组和活动参与情况: + - 本年度参与的小组总数和名称列表 + - 参加小组活动的次数和累计时长 + - 参与次数最多的小组及次数 + - 开始时间最早/最晚的活动(仅比较小时和分钟) + - 参与次数最多的时段窗口 + """ + # 获取个人本年度参与的小组总数(2024春、2025秋,包括全年类型) + org_num = Position.objects.filter( + person=person, + year=2024, + semester__in=[Semester.SPRING, Semester.ANNUAL] + ).count() + Position.objects.filter( + person=person, + year=2025, + semester__in=[Semester.FALL, Semester.ANNUAL] + ).count() + + # 获取这些小组的名称 + org_name_list = list( + Position.objects.filter( + person=person, + year=2024, + semester=Semester.SPRING + ).values_list('org__oname', flat=True) + ) + list( + Position.objects.filter( + person=person, + year=2025, + semester__in=[Semester.FALL, Semester.ANNUAL] + ).values_list('org__oname', flat=True) + ) + + # 筛选出时间段内所有的该用户的 Participation 记录(状态必须是已参与) + participation_records = Participation.objects.filter( + person=person, + status=Participation.AttendStatus.ATTENDED, + activity__start__gt=SUMMARY_SEM_START, + activity__start__lt=SUMMARY_SEM_END + ).select_related('activity__organization_id', 'activity') + + # 统计参加小组活动的次数和累计时长 + act_num = participation_records.count() + act_durations = [ + (finish - start).total_seconds() + for start, finish in participation_records.values_list('activity__start', 'activity__end') + ] + act_hour = round(sum(act_durations) / 3600.0, 1) if act_durations else 0.0 + + # 统计每个小组的参与次数 + org_num_dict = defaultdict(int) + for record in participation_records: + if record.activity and record.activity.organization_id: + org_num_dict[record.activity.organization_id.oname] += 1 + + # 找出次数最多的小组(次数相同则返回最早的小组名) + if org_num_dict: + org_top, org_top_num = max( + org_num_dict.items(), key=lambda x: (x[1], x[0])) + else: + org_top, org_top_num = None, 0 + + # 找出开始时间最早的活动(仅比较小时和分钟) + earliest_act_info = None + earliest_act_time = None + for record in participation_records: + if record.activity: + act_time = record.activity.start.time() + if earliest_act_time is None or act_time < earliest_act_time: + earliest_act_time = act_time + earliest_act_info = { + 'date': record.activity.start.strftime('%Y年%m月%d日'), + 'name': record.activity.title or '', + 'time': act_time.strftime('%H:%M'), + } + + # 找出开始时间最晚的活动(仅比较小时和分钟) + latest_act_info = None + latest_act_time = None + for record in participation_records: + if record.activity: + act_time = record.activity.start.time() + if latest_act_time is None or act_time > latest_act_time: + latest_act_time = act_time + latest_act_info = { + 'date': record.activity.start.strftime('%Y年%m月%d日'), + 'name': record.activity.title or '', + 'time': act_time.strftime('%H:%M'), + } + + # 统计每个时段窗口(小时)的参与次数 + window_num_dict = defaultdict(int) + for record in participation_records: + if record.activity: + hour = record.activity.start.hour + if 6 <= hour <= 23: + window_num_dict[hour] += 1 + + # 找出次数最多的窗口(次数相同则返回最早的窗口) + if window_num_dict: + window_top, window_top_num = max( + window_num_dict.items(), key=lambda x: (x[1], x[0])) + else: + window_top, window_top_num = None, 0 + + return { + 'org_num': org_num, + 'org_name_list': org_name_list, + 'act_num': act_num, + 'act_hour': act_hour, + 'org_top': org_top, + 'org_top_num': org_top_num, + 'earliest_act_record': earliest_act_info, + 'latest_act_record': latest_act_info, + 'window_top': window_top, + 'window_top_num': window_top_num, + } + +# 统计用户书院课程参与情况 + + +def get_person_course_usage(person: 'NaturalPerson'): + # 获取用户所有(不止本年度)有学时记录的课程总数 + course_num = CourseRecord.objects.filter( + person=person, + total_hours__gt=0).count() + # 获取用户所有(不止本年度)的总有效学时数 + valid_hours = CourseRecord.objects.filter( + person=person, + total_hours__gt=0).aggregate( + valid_hours=Sum('total_hours'))['valid_hours'] or 0 + + # 然后五种类型分别统计有无(使用Course.CourseType.values中的值作为键,值为bool flag),遍历用户的学时表以更新键值对,最后,flag为True的类型对应的文字加到字符串中,不能重复加,最后返回字符串 + valid_hours_dict = defaultdict(bool) + for record in CourseRecord.objects.filter( + person=person, + total_hours__gt=0).select_related('course'): + if record.course and record.course.type is not None: + valid_hours_dict[record.course.type] = True + course_type_str = '' + for course_type in Course.CourseType: + if valid_hours_dict[course_type.value]: + course_type_str += course_type.label + ' ' + # 从所有有效学时中找到最多的学时数对应的课程,返回课程名称、学时数,若有多个课程学时数相同,则返回最早的课程 + most_hours_course = None + most_hours = 0 + for record in CourseRecord.objects.filter( + person=person, + total_hours__gt=0): + if record.total_hours > most_hours: + most_hours = record.total_hours + most_hours_course = record.course + # 从用户本年度(24春、25秋)所成功选课的书院课中,找出成功人数/总选课人数比例最低的课程;比例相同则返回最后更新的课程(id 最大) + ratio_by_course = _get_select_course_ratio() + course_stats = _get_course_stats() # 获取课程统计信息(包含总选课人数) + person_course_statuses = [ + CourseParticipant.Status.SUCCESS + ] + person_annual_courses = list( + Course.objects.exclude(status=Course.Status.ABORT) + .filter( + Q(year=2024, semester=Semester.SPRING) + | Q(year=2025, semester=Semester.FALL) + ) + .filter( + participant_set__person=person, + participant_set__status__in=person_course_statuses, + ) + .distinct() + .order_by('-id') # 按id倒序,id大的(最后更新的)在前面 + ) + lowest_ratio_course = None + lowest_ratio = float('inf') # 初始化为无穷大,这样任何比例都会更小 + for course in person_annual_courses: + r = ratio_by_course.get(course.id, 0) + # 如果比例更小,则更新(因为已经按id倒序排列,比例相同时第一个就是id最大的) + if r < lowest_ratio: + lowest_ratio = r + lowest_ratio_course = course + highest_ratio_course_info = None + if lowest_ratio_course is not None: + stats = course_stats.get(lowest_ratio_course.id, {}) + total_participants = stats.get('preselect_count', 0) + highest_ratio_course_info = { + 'name': lowest_ratio_course.name, + 'total_participants': total_participants, + } + + return { + 'course_num': course_num, + 'valid_hours': valid_hours, + 'course_type_str': course_type_str.strip(), + 'most_hours_course': most_hours_course.name if most_hours_course else None, + 'most_hours': most_hours, + 'highest_ratio_course': highest_ratio_course_info, + } + +# 统计用户年度元气值收入总额 + + +def get_person_yqpoint_income(person: 'NaturalPerson'): + """ + 获取用户本年度元气值收入总额(签到获得的元气值)。 + """ + user = person.get_user() + if user is None: + return 0 + + result = YQPointRecord.objects.filter( + user=user, + source_type=YQPointRecord.SourceType.CHECK_IN, + time__gt=SUMMARY_SEM_START, + time__lt=SUMMARY_SEM_END + ).aggregate(total_income=Sum('delta')) + + return result['total_income'] or 0 + +# 定义子模块数据处理函数(排名部分)(与其他用户对比的部分都放这个里面) + +# 定义子模块数据处理函数(排名部分) + + +def calculate_underground_usage_percentile(): + """ + 计算每个用户有刷卡或预约记录的总天数超越其他用户的百分比。 + 使用预缓存的 _underground_usage_days_cache 数据。 + 返回: {username: percentile} 字典,percentile 为 0-100 的浮点数,表示超越了多少百分比的其他用户。 + """ + global _underground_usage_days_cache + + if not _underground_usage_days_cache: + return {} + + # 将所有用户按天数排序(从低到高) + sorted_users = sorted( + _underground_usage_days_cache.items(), key=lambda x: x[1]) + total_users = len(sorted_users) + + if total_users == 0: + return {} + + # 计算每个用户的百分比排名 + # percentile = (小于该用户天数的用户数) / 总用户数 * 100 + result = {} + + # 使用字典记录每个天数对应的用户列表(处理相同天数的情况) + days_to_users = defaultdict(list) + for username, days in sorted_users: + days_to_users[days].append(username) + + # 计算每个用户的百分比 + users_below = 0 # 当前天数以下的用户数 + for days in sorted(set(_underground_usage_days_cache.values())): + users_with_this_days = days_to_users[days] + # 对于相同天数的用户,使用相同的百分比(取中位数) + # 百分比 = (users_below + users_with_this_days - 1) / total_users * 100 + percentile = (users_below / total_users) * \ + 100 if total_users > 1 else 0 + + for username in users_with_this_days: + result[username] = round(percentile, 2) + + users_below += len(users_with_this_days) + + return result + +# 计算每个用户的 本年度“个人刷卡天数最多的自习室”记录相同的用户数 + + +def calculate_same_study_room_top_count(): + """ + 计算每个用户的"本年度个人刷卡次数最多的自习室"记录相同的用户数。 + 使用预缓存的 _study_room_top_cache 数据。 + 先按自习室统计用户数(自习室 -> 用户列表),再遍历用户计算相同用户数。 + 返回: {username: count} 字典,count 为选择相同自习室的其他用户数(不包括自己)。 + """ + global _study_room_top_cache + + if not _study_room_top_cache: + return {} + + # 按自习室统计用户数:自习室 -> 用户列表 + room_to_users = defaultdict(list) + for username, room in _study_room_top_cache.items(): + if room is not None: # 只统计有自习室记录的用户 + room_to_users[room].append(username) + + # 计算每个用户有多少其他用户选择了相同的自习室 + result = {} + for username, room in _study_room_top_cache.items(): + if room is None: + # 如果没有自习室记录,相同用户数为0 + result[username] = 0 + else: + # 相同自习室的用户数 - 1(排除自己) + same_room_users = room_to_users[room] + result[username] = len(same_room_users) - 1 + + return result + +# 计算每个用户的 “最多学时书院课” 与自己相同的用户数 + + +def calculate_same_most_hours_course_count(): + """ + 计算每个用户的"最多学时书院课"与自己相同的用户数。 + 使用预缓存的 _most_hours_course_cache 数据。 + 先按课程名统计用户数(课程名 -> 用户列表),再遍历用户计算相同用户数。 + 返回: {username: count} 字典,count 为选择相同课程的其他用户数(不包括自己)。 + """ + global _most_hours_course_cache + + if not _most_hours_course_cache: + return {} + + # 按课程名统计用户数:课程名 -> 用户列表 + course_to_users = defaultdict(list) + for username, course_name in _most_hours_course_cache.items(): + if course_name is not None: # 只统计有课程记录的用户 + course_to_users[course_name].append(username) + + # 计算每个用户有多少其他用户选择了相同的课程 + result = {} + for username, course_name in _most_hours_course_cache.items(): + if course_name is None: + # 如果没有课程记录,相同用户数为0 + result[username] = 0 + else: + # 相同课程的用户数 - 1(排除自己) + same_course_users = course_to_users[course_name] + result[username] = len(same_course_users) - 1 + + return result + +# 计算 a.个人账号预约中,最经常一起预约的人、一起预约次数 +# b.小组账号预约中,最经常一起预约的人、一起预约次数 +# 方法:先取出时间段内所有的预约记录,根据预约申请者的类型判断是个人账户预约还是小组账户预约,然后对于每个参加本次预约的用户,遍历除了自己以外的其他参与者来更新自己的键值对表 +# 每个用户分别维护一系列键值对,记录其他人和自己一起出现在预约中的次数,最后对每个用户的键值对表排序得到结果 + + +def calculate_most_frequent_co_appoint(): + """ + 计算每个用户最经常一起预约的人和一起预约次数。 + 分别统计个人账号预约和小组账号预约两种情况。 + + 方法: + 1. 获取时间段内所有预约记录 + 2. 根据预约申请者(major_student.Sid.utype)判断是个人账户预约还是小组账户预约:utype == Type.ORG 算作小组预约,其他一律算作个人预约 + 3. 对于每个参与者,遍历除了自己以外的其他参与者来更新自己的键值对表 + 4. 每个用户分别维护键值对,记录其他人和自己一起出现在预约中的次数 + 5. 对每个用户的键值对表排序得到结果 + + 返回: { + 'personal': {username: {'co_name': 自然人姓名, 'count': 次数}}, + 'organization': {username: {'co_name': 自然人姓名, 'count': 次数}} + } + """ + # 获取时间段内所有预约记录 + appoints = Appoint.objects.filter( + Astart__gt=SUMMARY_SEM_START, + Astart__lt=SUMMARY_SEM_END + ).select_related('major_student__Sid').prefetch_related('students__Sid') + + # 个人账户预约:用户 -> {其他用户: 一起预约次数} + personal_co_appoint_dict = defaultdict(lambda: defaultdict(int)) + # 小组账户预约:用户 -> {其他用户: 一起预约次数} + org_co_appoint_dict = defaultdict(lambda: defaultdict(int)) + + # 建立username到name的映射缓存 + username_to_name_cache = {} + + for appoint in appoints: + # 判断是个人账户预约还是小组账户预约 + if appoint.major_student is None: + continue + + appointer_user = appoint.major_student.Sid + if appointer_user is None: + continue + + # utype == Type.ORG 算作小组预约,其他一律算作个人预约 + is_personal = appointer_user.utype != User.Type.ORG + + # 获取所有参与者 + participants = appoint.students.all() + participant_usernames = [] + for participant in participants: + if participant.Sid is not None: + username = participant.Sid.username + participant_usernames.append(username) + # 缓存username到name的映射 + if username not in username_to_name_cache: + try: + person = NaturalPerson.objects.get_by_user( + participant.Sid) + username_to_name_cache[username] = person.name + except NaturalPerson.DoesNotExist: + # 如果不是自然人,使用User的name字段 + username_to_name_cache[username] = participant.Sid.name or username + + # 对于每个参与者,遍历除了自己以外的其他参与者 + for username in participant_usernames: + co_appoint_dict = personal_co_appoint_dict if is_personal else org_co_appoint_dict + + for co_username in participant_usernames: + if co_username != username: + co_appoint_dict[username][co_username] += 1 + + # 对每个用户的键值对表排序,得到最经常一起预约的人和次数 + result_personal = {} + result_org = {} + + for username, co_dict in personal_co_appoint_dict.items(): + if co_dict: + # 按次数排序,次数相同则按用户名排序 + sorted_co = sorted(co_dict.items(), key=lambda x: (-x[1], x[0])) + co_username, count = sorted_co[0] + # 获取对应的自然人姓名 + co_name = username_to_name_cache.get(co_username, co_username) + result_personal[username] = { + 'co_name': co_name, + 'count': count, + } + else: + result_personal[username] = { + 'co_name': None, + 'count': 0, + } + + for username, co_dict in org_co_appoint_dict.items(): + if co_dict: + # 按次数排序,次数相同则按用户名排序 + sorted_co = sorted(co_dict.items(), key=lambda x: (-x[1], x[0])) + co_username, count = sorted_co[0] + # 获取对应的自然人姓名 + co_name = username_to_name_cache.get(co_username, co_username) + result_org[username] = { + 'co_name': co_name, + 'count': count, + } + else: + result_org[username] = { + 'co_name': None, + 'count': 0, + } + + # 统计有数据的条目数 + personal_with_data = sum(1 for v in result_personal.values() if v.get('co_name') is not None and v.get('count', 0) > 0) + org_with_data = sum(1 for v in result_org.values() if v.get('co_name') is not None and v.get('count', 0) > 0) + + import sys + output = sys.stdout + output.write(f"\n=== 共同预约统计调试信息 ===\n") + output.write(f"有个人预约共同预约最多次数的条目数: {personal_with_data}\n") + output.write(f"有小组共同预约最多次数的条目数: {org_with_data}\n") + output.write(f"个人预约总条目数: {len(result_personal)}\n") + output.write(f"小组预约总条目数: {len(result_org)}\n") + output.write("=" * 50 + "\n\n") + output.flush() + + return { + 'personal': result_personal, + 'organization': result_org, + } + + +# 定义子模块数据处理函数(总统计数据部分) + + +# 地下室年度使用情况总览(自习室刷卡总次数,研讨室预约总次数,功能房预约总次数) + + +def get_underground_annual_usage(): + + # 获取自习室刷卡总次数 + study_room_num = CardCheckInfo.objects.filter( + Cardroom__Rid__in=study_room_list, Cardtime__gt=SUMMARY_SEM_START, Cardtime__lt=SUMMARY_SEM_END).count() + # 获取研讨室预约总次数 + talk_room_num = Appoint.objects.filter( + Room__Rid__in=talk_room_list, Astart__gt=SUMMARY_SEM_START, Astart__lt=SUMMARY_SEM_END).count() + # 获取功能房预约总次数 + func_room_num = Appoint.objects.filter( + Room__Rid__in=func_room_list, Astart__gt=SUMMARY_SEM_START, Astart__lt=SUMMARY_SEM_END).count() + return { + 'study_room_num': study_room_num, + 'talk_room_num': talk_room_num, + 'func_room_num': func_room_num, + } + +# YPPF 年度使用情况总览(智慧书院现有小组总数,小组年度发起活动总次数,年度开设书院课程总数) + + +def get_yppf_annual_usage(): + # 获取智慧书院现有小组总数 + org_num = Organization.objects.activated().count() + # 获取小组年度发起活动总次数 + act_num = Activity.objects.exclude(status__in=[Activity.Status.REVIEWING, Activity.Status.CANCELED, Activity.Status.ABORT, Activity.Status.REJECT]).filter( + start__gt=SUMMARY_SEM_START, start__lt=SUMMARY_SEM_END).count() + # 获取年度开设书院课程总数 (24学年春季,25学年秋季) + course_num = Course.objects.exclude(status=Course.Status.ABORT).filter(year=2024, semester=Semester.SPRING).count( + ) + Course.objects.exclude(status=Course.Status.ABORT).filter(year=2025, semester=Semester.FALL).count() + + return { + 'org_num': org_num, + 'act_num': act_num, + 'course_num': course_num, + } + + +# 计算本年度所有书院课的选中人数/预选人数 比例,并返回课程-比例键值对 +def cal_select_course_ratio(): + """本年度(24春、25秋)书院课:预选人数=SELECT/SUCCESS/FAILED,选中人数=SUCCESS;比例=选中/预选,预选为0则比例为0。返回 {course_id: ratio}。""" + global _select_course_ratio_cache, _course_stats_cache + preselect_statuses = [ + CourseParticipant.Status.SELECT, + CourseParticipant.Status.SUCCESS, + CourseParticipant.Status.FAILED, + ] + courses = Course.objects.exclude(status=Course.Status.ABORT).filter( + Q(year=2024, semester=Semester.SPRING) | Q( + year=2025, semester=Semester.FALL) + ).annotate( + preselect_count=Count( + 'participant_set', + filter=Q(participant_set__status__in=preselect_statuses), + ), + success_count=Count( + 'participant_set', + filter=Q(participant_set__status=CourseParticipant.Status.SUCCESS), + ), + ) + result = {} + import sys + output = sys.stdout + output.write("\n=== 课程选课比例调试信息 ===\n") + output.write( + f"{'课程ID':<10} {'课程名称':<30} {'成功人数':<10} {'总选课人数':<12} {'比例':<10}\n") + output.write("-" * 80 + "\n") + for c in courses.order_by('id'): + ratio = (c.success_count / c.preselect_count) if c.preselect_count else 0 + result[c.id] = round(ratio, 4) + course_name = c.name[:28] if len(c.name) > 28 else c.name + output.write( + f"{c.id:<10} {course_name:<30} {c.success_count:<10} {c.preselect_count:<12} {ratio:.4f}\n") + output.write(f"\n总计: {len(result)} 门课程\n") + output.write("=" * 80 + "\n\n") + output.flush() + _select_course_ratio_cache = result + # 同时更新统计信息缓存 + _course_stats_cache = {} + for c in courses.order_by('id'): + _course_stats_cache[c.id] = { + 'preselect_count': c.preselect_count, + 'success_count': c.success_count, + } + return result + + +def _get_select_course_ratio(): + """获取课程-比例缓存,未计算则先调用 cal_select_course_ratio()。""" + global _select_course_ratio_cache + if _select_course_ratio_cache is None: + cal_select_course_ratio() + return _select_course_ratio_cache + + +def _get_course_stats(): + """获取课程统计信息缓存(包含总选课人数和成功人数),未计算则先调用 cal_select_course_ratio()。""" + global _course_stats_cache + if _course_stats_cache is None: + # 重新查询以获取统计信息 + preselect_statuses = [ + CourseParticipant.Status.SELECT, + CourseParticipant.Status.SUCCESS, + CourseParticipant.Status.FAILED, + ] + courses = Course.objects.exclude(status=Course.Status.ABORT).filter( + Q(year=2024, semester=Semester.SPRING) | Q( + year=2025, semester=Semester.FALL) + ).annotate( + preselect_count=Count( + 'participant_set', + filter=Q(participant_set__status__in=preselect_statuses), + ), + success_count=Count( + 'participant_set', + filter=Q(participant_set__status=CourseParticipant.Status.SUCCESS), + ), + ) + _course_stats_cache = {} + for c in courses: + _course_stats_cache[c.id] = { + 'preselect_count': c.preselect_count, + 'success_count': c.success_count, + } + return _course_stats_cache + + +# 定义命令处理函数 + + +class Command(BaseCommand): + help = '导出2025年度总结数据' + + def handle(self, *args, **option): + + # 总统计数据部分,写入到 raw_data/summary2025/summary_overall_2025.json 文件中 + + import os + os.makedirs('raw_data/summary2025', exist_ok=True) + + overall_info = get_underground_annual_usage() + overall_info.update(get_yppf_annual_usage()) + with open('raw_data/summary2025/summary_overall_2025.json', 'w', encoding='utf-8') as f: + json.dump(overall_info, f, default=datetime_converter, + ensure_ascii=False, indent=2) + self.stdout.write(self.style.SUCCESS( + "总统计数据已写入: raw_data/summary2025/summary_overall_2025.json")) + # ========== 预计算课程比例 ========== + self.stdout.write("预计算本年度所有书院课的选中/预选比例...") + cal_select_course_ratio() + self.stdout.write(self.style.SUCCESS("课程比例计算完成")) + # 个人信息部分,写入到 raw_data/summary2025/summary2025.json 文件中 + # 获取所有对应着 “自然人”/"老师”/“学生” 的“用户”账号 + # 依次获取每个人的数据,并写入到json文件中 ( raw_data/summary2025/ 目录下 summary2025.json 文件中) + # ========== 个人信息部分 ========== + self.stdout.write("开始导出个人信息数据...") + # 初始化缓存 + global _underground_usage_days_cache, _study_room_top_cache, _most_hours_course_cache + _underground_usage_days_cache = {} + _study_room_top_cache = {} + _most_hours_course_cache = {} + + person_data = {} + user_count = 0 + + users = User.objects.filter(utype__in=[ + User.Type.PERSON, User.Type.TEACHER, User.Type.STUDENT]).order_by('username') + total_users = users.count() + + for user in users: + try: + person = NaturalPerson.objects.get_by_user(user) + except NaturalPerson.DoesNotExist: + continue + + user_count += 1 + if user_count % 100 == 0: + self.stdout.write(f"已处理 {user_count}/{total_users} 个用户...") + + # 组装个人数据 + person_info = {} + + # 用户注册信息 + register_info = get_user_register_date_and_days(person) + if register_info: + person_info.update(register_info) + + # 地下室使用总览 + underground_usage_days = get_person_underground_usage(person) + person_info['underground_usage_days'] = underground_usage_days + # 缓存用户名和天数,用于后续排名计算 + _underground_usage_days_cache[user.username] = underground_usage_days + + person_info['first_underground_record'] = get_person_first_underground_record( + person) + person_info['last_underground_record'] = get_person_last_underground_record( + person) + person_info['longest_underground_usage'] = get_person_longest_underground_usage( + person) + + # 自习室使用情况 + study_room_usage = get_person_study_room_usage(person) + person_info['study_room_usage'] = study_room_usage + # 缓存用户名和最多的自习室,用于后续排名计算 + _study_room_top_cache[user.username] = study_room_usage.get( + 'study_room_top') + + # 研讨室和功能房使用情况 + person_info['talk_and_func_room_usage'] = get_person_talk_and_func_room_usage( + person) + + # 预约习惯 + person_info['appoint_habit'] = get_person_appoint_habit(person) + + # 登录天数 + person_info['login_days'] = get_person_login_days(person) + + # 小组和活动参与情况 + person_info['org_usage'] = get_person_org_usage(person) + + # 书院课程参与情况 + course_usage = get_person_course_usage(person) + person_info['course_usage'] = course_usage + # 缓存用户名和最多学时课程,用于后续排名计算 + _most_hours_course_cache[user.username] = course_usage.get( + 'most_hours_course') + + # 元气值收入 + person_info['yqpoint_income'] = get_person_yqpoint_income(person) + + # 添加自然人姓名 + person_info['name'] = person.name if person.name else '' + + # 使用用户名作为key + person_data[user.username] = person_info + + # 写入个人信息JSON文件(按用户名排序) + person_file = 'raw_data/summary2025/summary2025.json' + # 确保按用户名排序 + sorted_person_data = dict(sorted(person_data.items())) + with open(person_file, 'w', encoding='utf-8') as f: + json.dump(sorted_person_data, f, default=datetime_converter, + ensure_ascii=False, indent=2) + self.stdout.write(self.style.SUCCESS( + f"个人信息数据已写入: {person_file} (共 {user_count} 个用户)")) + + # ========== 排名数据部分 ========== + self.stdout.write("开始计算排名数据...") + rank_data = {} + + # 计算每个用户有刷卡或预约记录的总天数超越其他用户的百分比 + underground_percentile = calculate_underground_usage_percentile() + + # 计算每个用户的"个人刷卡次数最多的自习室"记录相同的用户数 + same_study_room_top_count = calculate_same_study_room_top_count() + + # 计算每个用户的"最多学时书院课"与自己相同的用户数 + same_most_hours_course_count = calculate_same_most_hours_course_count() + + # 计算每个用户最经常一起预约的人和一起预约次数(个人账户和小组账户分别统计) + most_frequent_co_appoint = calculate_most_frequent_co_appoint() + + # 将数据组织为 用户名 -> 数据名 -> 值 的结构 + all_usernames = ( + set(underground_percentile.keys()) | + set(same_study_room_top_count.keys()) | + set(same_most_hours_course_count.keys()) | + set(most_frequent_co_appoint['personal'].keys()) | + set(most_frequent_co_appoint['organization'].keys()) + ) + + for username in all_usernames: + if username not in rank_data: + rank_data[username] = {} + + if username in underground_percentile: + rank_data[username]['underground_usage_percentile'] = underground_percentile[username] + + if username in same_study_room_top_count: + rank_data[username]['same_study_room_top_count'] = same_study_room_top_count[username] + + if username in same_most_hours_course_count: + rank_data[username]['same_most_hours_course_count'] = same_most_hours_course_count[username] + + # 个人账号预约中最经常一起预约的人 + if username in most_frequent_co_appoint['personal']: + if username in most_frequent_co_appoint['personal']: + rank_data[username]['personal_most_frequent_co_appoint'] = most_frequent_co_appoint['personal'][username] + + # 小组账号预约中最经常一起预约的人 + if username in most_frequent_co_appoint['organization']: + if username in most_frequent_co_appoint['organization']: + rank_data[username]['organization_most_frequent_co_appoint'] = most_frequent_co_appoint['organization'][username] + + # 按用户名排序 + sorted_rank_data = dict(sorted(rank_data.items())) + + rank_file = 'raw_data/summary2025/rank2025.json' + with open(rank_file, 'w', encoding='utf-8') as f: + json.dump(sorted_rank_data, f, ensure_ascii=False, indent=2) + self.stdout.write(self.style.SUCCESS(f"排名数据已写入: {rank_file}")) + + self.stdout.write(self.style.SUCCESS("\n所有数据导出完成!")) diff --git "a/static/Appointment/assets/bgm/\344\270\200\345\217\252\345\275\261\345\255\220YZYZ - \347\224\230\351\234\262.mp3" "b/static/Appointment/assets/bgm/\344\270\200\345\217\252\345\275\261\345\255\220YZYZ - \347\224\230\351\234\262.mp3" new file mode 100644 index 000000000..27f6991a9 Binary files /dev/null and "b/static/Appointment/assets/bgm/\344\270\200\345\217\252\345\275\261\345\255\220YZYZ - \347\224\230\351\234\262.mp3" differ diff --git a/static/Appointment/assets/css/summary2025.css b/static/Appointment/assets/css/summary2025.css new file mode 100644 index 000000000..930fd2ffd --- /dev/null +++ b/static/Appointment/assets/css/summary2025.css @@ -0,0 +1,1835 @@ +/* 字体定义 */ +@font-face { + font-family: "Siyuan SongTi regular"; + src: url("../fonts/Siyuan-SongTi-regular.ttf") format("truetype"); +} + +@font-face { + font-family: "GenWanMin2TC-R"; + src: url("../fonts/GenWanMin2TC-R.otf") format("otf"); +} + +/* PREFLIGHT */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +h1, +h2, +h3, +p { + margin: 0; + padding: 0; +} + +/* UTILITIES */ +.mt-35 { + margin-top: 35px; +} + +.mt-50 { + margin-top: 50px; +} + +.mt-35-no-bold { + margin-top: 35px; +} +.fs-36b { + font-size: 36px; + font-weight: bold; +} + +.fc-blue { + color: #004AAD; +} + +.fc-rose { + font-weight: 900; + color: #ff5050; + font-size: 22px; + padding: 0 0.2em; +} + +/* BODY */ +body { + font-family: "Siyuan SongTi regular", "SimSun", "宋体", STSong, serif; + font-size: 23px; + line-height: 1.5; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + -webkit-font-smoothing: subpixel-antialiased; + overflow: hidden; +} + +p { + margin: 10px 0 0 0; + padding: 0; + color: rgb(60, 60, 60); + font-family: "Siyuan SongTi regular", "SimSun", "宋体", STSong, serif; + font-size: 23px; + line-height: 1.8; + text-align: center; +} + +button { + cursor: pointer; + border: none; + background: transparent; +} + +/* 音乐播放器 */ +#music-container { + position: fixed; + top: 20px; + right: 20px; + z-index: 1000; +} + +#play-music { + background: rgba(255, 255, 255, 0.8); + border-radius: 50%; + width: 50px; + height: 50px; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); +} + +#playing { + display: block; +} + +#paused { + display: none; +} + +/* 页面通用样式 */ +.section.page { + position: relative; + width: 100vw; + height: 100vh; + overflow: hidden; +} + +.bg-image { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + object-fit: cover; + z-index: 1; + opacity: 0.8; +} + +/* 启动页样式 */ +.splash-page { + background: transparent; +} + +.splash-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + object-fit: cover; + z-index: 1; +} + +.splash-content { + position: relative; + z-index: 2; + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + justify-content: flex-start; + /* 改为从上排,让中间内容自然下落,footer沉底 */ + align-items: center; + padding: 20px; +} + +/* 移除旧的 splash-decorations 相关 */ + +.splash-logo { + position: absolute; + top: 30px; + right: 20px; + width: 80px; + height: auto; + animation: fade-in 1s ease forwards; + z-index: 10; +} + +.splash-photos { + position: absolute; + top: -20px; + left: -30px; + width: 260px; + /* 左上角装饰大小 */ + height: auto; + transform: rotate(-15deg); + animation: slide-down 1.4s ease forwards; + pointer-events: none; + /* 防止遮挡点击 */ +} + +.splash-centered-info { + width: 100%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + margin-top: 25vh; + /* 距离顶部位置 */ + position: relative; + z-index: 5; +} + +.splash-title { + width: 85%; + max-width: 500px; + height: auto; + animation: fade-in 1.2s ease forwards; + margin: 0; +} + +.splash-user-greeting { + font-family: "GenWanMin2TC-R", "Siyuan SongTi regular", serif; + font-size: 28px; + color: #ffffff; + text-shadow: 0 2px 8px rgba(0, 74, 173, 0.3); + /* 蓝色阴影,增加对比度 */ + letter-spacing: 2px; + opacity: 0; + animation: fade-in-up 1.5s ease forwards 0.5s; + /* 延迟出现 */ + display: flex; + flex-direction: column; + align-items: center; + gap: 10px; +} + +.user-name { + font-size: 32px; + font-weight: bold; + border-bottom: 2px solid rgba(255, 255, 255, 0.6); + padding-bottom: 5px; +} + +.greeting-text { + font-size: 24px; + opacity: 0.9; +} + +.splash-footer { + width: 100%; + max-width: 400px; + display: flex; + flex-direction: column; + align-items: center; + gap: 20px; + margin-top: auto; + /* Push to bottom */ + margin-bottom: 60px; + /* Bottom padding */ +} + +@keyframes slide-down { + from { + opacity: 0; + transform: translate(-20px, -40px) rotate(-20deg); + } + + to { + opacity: 1; + transform: translate(0, 0) rotate(-15deg); + } +} + +@keyframes fade-in-up { + from { + opacity: 0; + transform: translateY(20px); + } + + to { + opacity: 1; + transform: translateY(0); + } +} + +.splash-button-img { + width: 200px; + height: auto; + cursor: pointer; + transition: all 0.3s ease; + opacity: 0.5; + filter: grayscale(100%); +} + +.splash-button-img.active { + opacity: 1; + filter: grayscale(0%); + animation: pulse 1.5s ease-in-out infinite; +} + +.splash-button-img:hover { + transform: scale(1.05); +} + +@keyframes fade-in { + from { + opacity: 0; + transform: translateY(20px); + } + + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes pulse { + + 0%, + 100% { + transform: scale(1); + } + + 50% { + transform: scale(1.05); + } +} + +/* 全局数据样式 - 所有number, name, room, date, yppf, org等强调类 */ +[class*="-number"], +[class*="-name"], +[class*="-room"], +[class*="-date"], +[class*="-yppf"], +[class*="-org"] { + color: #ff5050; + font-size: 20px; + font-weight: 700; + padding: 0 0.1em; +} + +/* 每页首字放大样式 */ +.p1-question::first-letter, +.p2-greeting::first-letter, +.p3-title::first-letter, +.p4-title::first-letter, +.p5-title::first-letter, +.p6-title:not(.mt-35)::first-letter, +.p7-section-title:first-child::first-letter, +.p8-section-title:first-child::first-letter, +.p9-section-title:first-child::first-letter, +.p10-title::first-letter, +.p12-stat:first-of-type::first-letter, +.text-box>p:first-child::first-letter { + font-size: 24px; + font-weight: bold; + line-height: 1.1; + margin-right: 2px; +} + +.content { + position: relative; + z-index: 2; + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 5vh 5vw; + overflow-y: auto; +} + +/* 页面对齐方式设定 */ + +/* 居中对齐页面 (强制完全居中) */ +#page-home .content, +#page-4 .content, +#page-6 .content, +#page-7 .content, +#page-8 .content, +#page-9 .content, +#page-12 .content, +#page-14 .content, +#page-15 .content, +#page-16 .content { + align-items: center !important; + justify-content: center !important; +} + +#page-home .content [class*="-text-box"], +#page-4 .content [class*="-text-box"], +#page-6 .content [class*="-text-box"], +#page-7 .content [class*="-text-box"], +#page-8 .content [class*="-text-box"], +#page-9 .content [class*="-text-box"], +#page-12 .content [class*="-text-box"], +#page-14 .content [class*="-text-box"], +#page-15 .content [class*="-text-box"], +#page-16 .content [class*="-text-box"] { + text-align: center !important; + align-items: center !important; + max-width: 90% !important; + display: flex !important; + flex-direction: column !important; + margin: 0 auto !important; +} + +#page-home .content p, +#page-4 .content p, +#page-6 .content p, +#page-7 .content p, +#page-8 .content p, +#page-9 .content p, +#page-12 .content p, +#page-14 .content p, +#page-15 .content p, +#page-16 .content p { + text-align: center !important; + margin-left: auto !important; + margin-right: auto !important; +} + +/* 左对齐页面 */ +#page-2 .content, +#page-3 .content, +#page-10 .content { + align-items: flex-start !important; + padding-left: 15vw !important; +} + +#page-2 .content p, +#page-3 .content p, +#page-10 .content p { + text-align: left !important; +} + +/* 右对齐页面 */ +#page-5 .content, +#page-11 .content, +#page-13 .content, +#page-17 .content { + align-items: flex-end !important; + padding-right: 15vw !important; +} + +#page-5 .content p, +#page-11 .content p, +#page-13 .content p, +#page-17 .content p { + text-align: right !important; +} + +/* P1: 首页样式 - 诗意文案 */ +#page-home { + background: transparent; +} + +.p1-content { + justify-content: center; + align-items: center; + padding: 60px 40px 80px 40px; +} + +.p1-text-box { + text-align: center; + max-width: 90%; + opacity: 0; + transform: translateY(30px); +} + +.p1-text-box.animate-in { + animation: fade-in-up 0.8s ease forwards; + opacity: 1; + transform: translateY(0); +} + +.p1-question { + font-family: "Siyuan SongTi regular", serif; + font-size: 16px; + font-weight: 700; + color: #000000; + margin: 0; + line-height: 2.2; +} + +.p1-subtitle { + font-size: 18px; + font-weight: 400; +} + +.p1-poem { + font-family: "Siyuan SongTi regular", serif; + font-size: 16px; + color: #000000; + line-height: 2.2; + margin: 0; +} + +.p1-divider { + font-family: "Siyuan SongTi regular", serif; + font-size: 16px; + color: #000000; + margin: 25px 0 15px 0; + line-height: 2.2; +} + +.p1-stat { + font-family: "Siyuan SongTi regular", serif; + font-size: 16px; + color: #000000; + line-height: 2.2; + margin: 0; +} + +.p1-number { + color: #ff5050; + font-weight: 700; + font-size: 20px; + margin: 0 3px; +} + +.p1-year { + font-family: "Siyuan SongTi regular", serif; + font-size: 18px; + color: #000000; + font-weight: 600; + margin-top: 30px; + line-height: 1.8; +} + +/* P2: 欢迎页面样式 */ +#page-2 { + background: transparent; +} + +.p2-content { + justify-content: center; + align-items: center; + padding: 80px 40px; +} + +.p2-text-box { + text-align: left; + max-width: 90%; + opacity: 0; + transform: translateY(30px); +} + +.p2-text-box p { + text-align: left; +} + +.p2-text-box.animate-in { + animation: fade-in-up 0.8s ease forwards; + opacity: 1; + transform: translateY(0); +} + +.p2-greeting { + font-family: "Siyuan SongTi regular", serif; + font-size: 18px; + font-weight: 700; + color: #000000; + line-height: 2.2; + margin: 0; +} + +.p2-name { + color: #ff5050; + font-size: 28px; +} + +.p2-intro { + font-family: "Siyuan SongTi regular", serif; + font-size: 18px; + color: #000000; + line-height: 2.2; + margin: 0; +} + +/* P3: 地下室使用情况样式 */ +#page-3 { + background: transparent; +} + +.p3-content { + justify-content: center; + align-items: center; +} + +.p3-text-box { + text-align: left; + max-width: 90%; + opacity: 0; + transform: translateY(30px); +} + +.p3-text-box p { + text-align: left; +} + +.p3-text-box.animate-in { + animation: fade-in-up 0.8s ease forwards; + opacity: 1; + transform: translateY(0); +} + +.p3-title { + font-family: "Siyuan SongTi regular", serif; + font-size: 16px; + font-weight: 700; + color: #000000; + margin: 0; + line-height: 2.2; +} + +.p3-stat { + font-family: "Siyuan SongTi regular", serif; + font-size: 16px; + color: #000000; + line-height: 2.2; + margin: 0; +} + +.p3-number { + color: #ff5050; + font-weight: 700; + font-size: 28px; + margin: 0 3px; +} + +.p3-msg { + font-family: "Siyuan SongTi regular", serif; + font-size: 23px; + color: #000000; + font-weight: 600; + line-height: 2.2; + margin: 15px 0; +} + +.p3-divider { + font-family: "Siyuan SongTi regular", serif; + font-size: 16px; + color: #000000; + margin: 25px 0 10px 0; + line-height: 2.2; +} + +.p3-date { + font-family: "Siyuan SongTi regular", serif; + font-size: 16px; + color: #ff5050; + font-weight: 700; + line-height: 2.2; + margin: 0; +} + +.p3-detail { + font-family: "Siyuan SongTi regular", serif; + font-size: 16px; + color: #000000; + line-height: 2.2; + margin: 0; +} + +.p3-room { + color: #ff5050; + font-weight: 700; +} + +/* P4: 最长连续使用样式 */ +#page-4 { + background: transparent; +} + +.p4-text-box { + text-align: left; + max-width: 90%; +} + +.p4-text-box p { + text-align: left; +} + +.p4-title { + font-family: "Siyuan SongTi regular", serif; + font-size: 16px; + font-weight: 700; + color: #000000; + line-height: 2.2; + margin: 0; +} + +.p4-date { + font-family: "Siyuan SongTi regular", serif; + font-size: 23px; + color: #ff5050; + font-weight: 700; + line-height: 2.2; + margin: 0; +} + +.p4-detail, +.p4-witness { + font-family: "Siyuan SongTi regular", serif; + font-size: 16px; + color: #000000; + line-height: 2.2; + margin: 0; +} + +.p4-room, +.p4-usage { + font-family: "Siyuan SongTi regular", serif; + font-size: 18px; + color: #ff5050; + font-weight: 700; + line-height: 2.2; + margin: 0; +} + +.p4-stat { + font-family: "Siyuan SongTi regular", serif; + font-size: 16px; + color: #000000; + line-height: 2.2; + margin: 0; +} + +.p4-number { + color: #ff5050; + font-weight: 700; + font-size: 20px; + margin: 0 3px; +} + +.p4-yppf { + color: #ff5050; + font-weight: 700; +} + +.p4-to { + font-family: "Siyuan SongTi regular", serif; + font-size: 16px; + color: #000000; + line-height: 2.2; + margin: 0; +} + +.p4-message { + font-family: "Siyuan SongTi regular", serif; + font-size: 16px; + color: #000000; + line-height: 2.2; + margin: 15px 0 5px 0; +} + +/* P5: 自习室使用样式 */ +#page-5 { + background: transparent; +} + +.p5-content { + justify-content: center; + align-items: center; +} + +.p5-text-box { + text-align: left; + max-width: 90%; +} + +.p5-text-box p { + text-align: left; +} + +.p5-title { + font-family: "Siyuan SongTi regular", serif; + font-size: 16px; + font-weight: 700; + color: #000000; + line-height: 2.2; + margin: 0; +} + +.p5-stat { + font-family: "Siyuan SongTi regular", serif; + font-size: 16px; + color: #000000; + line-height: 2.2; + margin: 0; +} + +.p5-number { + color: #ff5050; + font-weight: 700; + font-size: 20px; + margin: 0 3px; +} + +.p5-poem { + font-family: "Siyuan SongTi regular", serif; + font-size: 16px; + color: #000000; + line-height: 2.2; + margin: 0; +} + +.p5-room { + font-family: "Siyuan SongTi regular", serif; + font-size: 18px; + color: #ff5050; + font-weight: 700; + line-height: 2.2; + margin: 0; +} + +.p5-detail { + font-family: "Siyuan SongTi regular", serif; + font-size: 16px; + color: #000000; + line-height: 2.2; + margin: 0; +} + +.p5-share { + font-family: "Siyuan SongTi regular", serif; + font-size: 16px; + color: #000000; + line-height: 2.2; + margin: 0; +} + +/* P6: 研讨室和功能室样式 */ +#page-6 { + background: transparent; +} + +.p6-content { + justify-content: center; + align-items: center; + padding: 80px 40px; +} + +.p6-text-box { + text-align: left; + max-width: 400px; +} + +.p6-title { + font-family: "Siyuan SongTi regular", serif; + font-size: 16px; + font-weight: 700; + color: #000000; + line-height: 2.2; + margin: 0; +} + +.p6-stat { + font-family: "Siyuan SongTi regular", serif; + font-size: 16px; + color: #000000; + line-height: 2.2; + margin: 0; +} + +.p6-number { + color: #ff5050; + font-weight: 700; + font-size: 20px; + margin: 0 3px; +} + +.p6-poem { + font-family: "Siyuan SongTi regular", serif; + font-size: 16px; + color: #000000; + line-height: 2.2; + margin: 0; +} + +.p6-memory { + font-family: "Siyuan SongTi regular", serif; + font-size: 16px; + color: #000000; + line-height: 2.2; + margin: 0; +} + +.p6-date { + color: #ff5050; + font-weight: 700; +} + +.p6-detail { + font-family: "Siyuan SongTi regular", serif; + font-size: 16px; + color: #000000; + line-height: 2.2; + margin: 0; +} + +.p6-room { + color: #ff5050; + font-weight: 700; +} + +.p6-wish { + font-family: "Siyuan SongTi regular", serif; + font-size: 16px; + color: #000000; + line-height: 2.2; + margin: 0; +} + +.p6-yppf { + font-family: "Siyuan SongTi regular", serif; + font-size: 18px; + color: #ff5050; + font-weight: 700; + line-height: 2.2; + margin: 0; +} + +/* P7: 预约习惯样式 */ +#page-7 { + background: transparent; +} + +.p7-content { + justify-content: center; + align-items: center; + padding: 80px 40px; +} + +.p7-text-box { + text-align: left; + max-width: 400px; +} + +.p7-section-title { + font-family: "Siyuan SongTi regular", serif; + font-size: 21px; + font-weight: 700; + color: #000000; + line-height: 2.2; + margin: 0; +} + +.p7-stat { + font-family: "Siyuan SongTi regular", serif; + font-size: 16px; + color: #000000; + line-height: 2.2; + margin: 0; +} + +.p7-number { + color: #ff5050; + font-weight: 700; + font-size: 20px; + margin: 0 3px; +} + +.p7-detail { + font-family: "Siyuan SongTi regular", serif; + font-size: 16px; + color: #000000; + line-height: 2.2; + margin: 0; +} + +.p7-yppf { + color: #ff5050; + font-weight: 700; +} + +.p7-room { + color: #ff5050; + font-weight: 700; +} + +.p7-message { + font-family: "Siyuan SongTi regular", serif; + font-size: 16px; + color: #000000; + line-height: 2.2; + margin: 10px 0 5px 0; +} + +/* P8: 最极限样式 */ +#page-8 { + background: transparent; +} + +.p8-content { + justify-content: center; + align-items: center; + padding: 80px 40px; +} + +.p8-text-box { + text-align: left; + max-width: 400px; +} + +.p8-section-title { + font-family: "Siyuan SongTi regular", serif; + font-size: 21px; + font-weight: 700; + color: #000000; + line-height: 2.2; + margin: 0; +} + +.p8-stat { + font-family: "Siyuan SongTi regular", serif; + font-size: 16px; + color: #000000; + line-height: 2.2; + margin: 0; +} + +.p8-number { + color: #ff5050; + font-weight: 700; + font-size: 20px; + margin: 0 3px; +} + +.p8-detail { + font-family: "Siyuan SongTi regular", serif; + font-size: 16px; + color: #000000; + line-height: 2.2; + margin: 0; +} + +.p8-date { + font-family: "Siyuan SongTi regular", serif; + font-size: 16px; + color: #ff5050; + font-weight: 700; + line-height: 2.2; + margin: 0; +} + +.p8-room { + font-family: "Siyuan SongTi regular", serif; + font-size: 18px; + color: #ff5050; + font-weight: 700; + line-height: 2.2; + margin: 0; +} + +.p8-witness, +.p8-yppf { + font-family: "Siyuan SongTi regular", serif; + font-size: 16px; + color: #000000; + line-height: 2.2; + margin: 0; +} + +.p8-yppf { + font-weight: 700; + color: #ff5050; +} + +.p8-message { + font-family: "Siyuan SongTi regular", serif; + font-size: 16px; + color: #000000; + font-weight: 600; + line-height: 2.2; + margin: 10px 0 5px 0; +} + +/* P9: 最投缘样式 */ +#page-9 { + background: transparent; +} + +.p9-content { + justify-content: center; + align-items: center; + padding: 80px 40px; +} + +.p9-text-box { + text-align: left; + max-width: 400px; +} + +.p9-section-title { + font-family: "Siyuan SongTi regular", serif; + font-size: 21px; + font-weight: 700; + color: #000000; + line-height: 2.2; + margin: 0; +} + +.p9-stat { + font-family: "Siyuan SongTi regular", serif; + font-size: 16px; + color: #000000; + line-height: 2.2; + margin: 0; +} + +.p9-number { + color: #ff5050; + font-weight: 700; + font-size: 20px; + margin: 0 3px; +} + +.p9-name { + color: #ff5050; + font-weight: 700; +} + +.p9-detail { + font-family: "Siyuan SongTi regular", serif; + font-size: 16px; + color: #000000; + line-height: 2.2; + margin: 0; +} + +/* P10: 书院总体总结样式 */ +#page-10 { + background: transparent; +} + +.p10-content { + justify-content: center; + align-items: center; + padding: 80px 40px; +} + +.p10-text-box { + text-align: left; + max-width: 400px; +} + +.p10-large { + font-size: 32px; + font-weight: 700; +} + +.p10-title { + font-family: "Siyuan SongTi regular", serif; + font-size: 18px; + color: #000000; + line-height: 2.2; + margin: 0; +} + +.p10-poem { + font-family: "Siyuan SongTi regular", serif; + font-size: 16px; + color: #000000; + line-height: 2.2; + margin: 3px 0; +} + +.p10-stat { + font-family: "Siyuan SongTi regular", serif; + font-size: 18px; + color: #000000; + line-height: 2.2; + margin: 0; +} + +.p10-number { + color: #ff5050; + font-weight: 700; + font-size: 22px; + margin-right: 5px; +} + +.p10-witness { + font-family: "Siyuan SongTi regular", serif; + font-size: 16px; + color: #000000; + line-height: 2.2; +} + +.p10-wish { + font-family: "Siyuan SongTi regular", serif; + font-size: 18px; + color: #000000; + font-weight: 600; + line-height: 2.2; +} + +/* P11: 首次相遇样式 */ +#page-11 { + background: transparent; +} + +.p11-content { + justify-content: center; + align-items: center; + padding: 100px 20px; +} + +.p11-text-box { + text-align: center; +} + +.p11-date { + font-family: "Siyuan SongTi regular", serif; + font-size: 18px; + color: #ff5050; + font-weight: 700; + line-height: 2.2; + margin: 0; +} + +.p11-poem { + font-family: "Siyuan SongTi regular", serif; + font-size: 18px; + color: #000000; + line-height: 2.2; + margin: 0; +} + +.p11-number { + color: #ff5050; + font-weight: 700; + font-size: 24px; + margin: 0 5px; +} + +.p11-title { + font-family: "Siyuan SongTi regular", serif; + font-size: 20px; + color: #000000; + font-weight: 600; + line-height: 2.2; +} + +.p11-stat { + font-family: "Siyuan SongTi regular", serif; + font-size: 18px; + color: #000000; + line-height: 2.2; + margin: 0; +} + +.p11-wish { + font-family: "Siyuan SongTi regular", serif; + font-size: 18px; + color: #000000; + line-height: 2.2; + margin-top: 20px; + font-weight: 600; +} + +/* P12: 小组参与详情样式 */ +#page-12 { + background: transparent; +} + +.p12-content { + justify-content: center; + align-items: center; + padding: 80px 20px; +} + +.p12-text-box { + text-align: center; +} + +.p12-stat { + font-family: "Siyuan SongTi regular", serif; + font-size: 18px; + color: #000000; + line-height: 2.2; + margin: 0; +} + +.p12-number { + color: #ff5050; + font-weight: 700; + font-size: 24px; + margin: 0 5px; +} + +.p12-org { + color: #ff5050; + font-weight: 700; + font-size: 20px; +} + +.p12-detail { + font-family: "Siyuan SongTi regular", serif; + font-size: 18px; + color: #000000; + line-height: 2.2; +} + +.p12-poem { + font-family: "Siyuan SongTi regular", serif; + font-size: 18px; + color: #000000; + line-height: 2.2; + margin: 0; +} + +.p12-title { + font-family: "Siyuan SongTi regular", serif; + font-size: 20px; + color: #000000; + font-weight: 600; + line-height: 2.2; +} + +.p12-message { + font-family: "Siyuan SongTi regular", serif; + font-size: 18px; + color: #000000; + font-weight: 600; + line-height: 2.2; + margin-top: 20px; +} + +.p12-fav-line { + margin: 0; +} + +.p12-fav-main, +.p12-fav-suffix { + font-family: "Siyuan SongTi regular", serif; + font-size: 18px !important; + font-weight: 700 !important; + line-height: 2.2; +} + +.p12-fav-main { + color: #ff5050; +} + +.p12-fav-suffix { + color: #000000; +} +/* 底部按钮区域 */ +.home-footer { + width: 100%; + max-width: 400px; + display: flex; + flex-direction: column; + align-items: center; + gap: 20px; + margin-top: auto; +} + +#continue-button { + background: linear-gradient(135deg, #f9d89c 0%, #e8b86d 100%); + border: none; + border-radius: 30px; + padding: 15px 50px; + font-size: 20px; + font-weight: 600; + color: #5a4a3a; + box-shadow: 0 4px 15px rgba(232, 184, 109, 0.4); + transition: all 0.3s ease; + cursor: pointer; +} + +#continue-button:hover { + transform: translateY(-2px); + box-shadow: 0 6px 20px rgba(232, 184, 109, 0.5); +} + +#continue-button:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.agree-rule-label { + display: flex; + align-items: center; + gap: 10px; + font-size: 14px; + color: #000000; + cursor: pointer; +} + +.agree-rule-label input[type="checkbox"] { + width: 18px; + height: 18px; + cursor: pointer; +} + +.agree-rule-label p { + margin: 0; + font-size: 14px; + line-height: 1.5; + color: #000000; +} + +.agree-rule-label a { + color: #5a8a87; + text-decoration: underline; +} + +/* 协议弹窗 */ +#rule { + display: none; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.5); + z-index: 9999; + align-items: center; + justify-content: center; + padding: 20px; +} + +#rule.show { + display: flex; +} + +.rule-wrapper { + background: white; + border-radius: 15px; + padding: 30px; + max-width: 600px; + max-height: 80vh; + overflow-y: auto; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3); +} + +.rule-title { + font-size: 22px; + font-weight: 700; + color: #2c5f5d; + margin: 0; + text-align: center; +} + +.rule-content { + font-size: 14px; + line-height: 1.8; + color: #000000; + margin: 0; +} + +.rule-content ol { + padding-left: 20px; +} + +.rule-content li { + margin-bottom: 12px; +} + +#rule-button { + background: linear-gradient(135deg, #f9d89c 0%, #e8b86d 100%); + border: none; + border-radius: 25px; + padding: 12px 40px; + font-size: 16px; + font-weight: 600; + color: #5a4a3a; + cursor: pointer; + display: block; + margin: 20px auto 0; + box-shadow: 0 4px 10px rgba(232, 184, 109, 0.3); + transition: all 0.3s ease; +} + +#rule-button:hover { + transform: translateY(-2px); + box-shadow: 0 6px 15px rgba(232, 184, 109, 0.4); +} + +/* 通用文本内容页面样式 */ +.text-content { + justify-content: center; + align-items: center; + padding: 60px 40px; +} + +.text-box { + max-width: 90%; + width: 100%; + text-align: right; + display: flex; + flex-direction: column; + align-items: flex-end; + justify-content: center; +} + +.intro-text { + font-family: "Siyuan SongTi regular", serif; + font-size: clamp(16px, 3vw, 22px); + line-height: 2.2; + color: #000000; + margin: 8px 0; + text-align: center; +} + +.highlight { + color: #d4a574; + font-weight: 700; + font-size: 26px; +} + +.highlight-large { + color: #d4a574; + font-weight: 900; + font-size: 42px; + display: inline-block; + margin: 0 8px; +} + +/* P2-P17 页面特定样式 */ +#page-2, +#page-3, +#page-4, +#page-5, +#page-6, +#page-7, +#page-8, +#page-9, +#page-10, +#page-11, +#page-12, +#page-13, +#page-14, +#page-15, +#page-16, +#page-17 { + background: #fff; +} + +/* 动画效果 */ +@keyframes fade-in-up { + from { + opacity: 0; + transform: translateY(30px); + } + + to { + opacity: 1; + transform: translateY(0); + } +} + +/* 为所有内容框添加初始隐藏状态和动画效果 */ +.text-box, +.home-title-container, +.p1-text-box, +.p2-text-box, +.p3-text-box, +.p4-text-box, +.p5-text-box, +.p6-text-box, +.p7-text-box, +.p8-text-box, +.p9-text-box, +.p10-text-box, +.p11-text-box, +.p12-text-box { + opacity: 0; + transform: translateY(30px); +} + +.text-box.animate-in, +.home-title-container.animate-in, +.p1-text-box.animate-in, +.p2-text-box.animate-in, +.p3-text-box.animate-in, +.p4-text-box.animate-in, +.p5-text-box.animate-in, +.p6-text-box.animate-in, +.p7-text-box.animate-in, +.p8-text-box.animate-in, +.p9-text-box.animate-in, +.p10-text-box.animate-in, +.p11-text-box.animate-in, +.p12-text-box.animate-in { + animation: fade-in-up 0.8s ease forwards; + opacity: 1; + transform: translateY(0); +} + +/* ==================== + 统一排版基线(字号/行距/段间距) + ==================== */ + +/* 统一各页文本容器中的正文排版 */ +#app .content [class*="-text-box"]>p:not(.p7-section-title):not(.p8-section-title):not(.p9-section-title), +#app .content .text-box>p:not(.p7-section-title):not(.p8-section-title):not(.p9-section-title) { + font-size: 17px !important; + line-height: 1.8 !important; +} + +/* 第一个段落上边距为0 */ +#app .content [class*="-text-box"]>p:first-of-type, +#app .content .text-box>p:first-of-type { + margin-top: 0 !important; +} + +/* 段与段之间保持稳定间距,形成明显分段 */ +#app .content [class*="-text-box"]>p+p, +#app .content .text-box>p+p { + margin-top: 12px !important; +} + +/* 保留模板中的显式分段间距(避免被统一 margin:0 覆盖) */ +#app .content [class*="-text-box"]>p.mt-35, +#app .content .text-box>p.mt-35 { + margin-top: 35px !important; +} + +#app .content [class*="-text-box"]>p.mt-50, +#app .content .text-box>p.mt-50 { + margin-top: 50px !important; +} + +/* 仅对非第一段的分段起始行(mt-35/mt-50)做首行加粗 */ +#app .content [class*="-text-box"]>p.mt-35:not(:first-of-type)::first-line, +#app .content [class*="-text-box"]>p.mt-50:not(:first-of-type)::first-line, +#app .content .text-box>p.mt-35:not(:first-of-type)::first-line, +#app .content .text-box>p.mt-50:not(:first-of-type)::first-line { + font-weight: 600; +} + +/* 全局兜底:所有页面第一段第一行不加粗 */ +#app .content [class*="-text-box"]>p:not(.p7-section-title):not(.p8-section-title):not(.p9-section-title):not(.p11-date):not(.p12-org):first-of-type::first-line, +#app .content .text-box>p:not(.p7-section-title):not(.p8-section-title):not(.p9-section-title):not(.p11-date):not(.p12-org):first-of-type::first-line { + font-weight: 400 !important; +} + +/* 第一段首字保留放大,但不额外加粗 */ +#app .content [class*="-text-box"]>p:not(.p7-section-title):not(.p8-section-title):not(.p9-section-title):not(.p11-date):not(.p12-org):first-of-type::first-letter, +#app .content .text-box>p:not(.p7-section-title):not(.p8-section-title):not(.p9-section-title):not(.p11-date):not(.p12-org):first-of-type::first-letter { + font-weight: 400 !important; +} + +/* p4-p10:统一用户数据字号并降低偏大观感 */ +#page-4 [class*="-number"], +#page-4 [class*="-name"], +#page-4 [class*="-room"], +#page-4 [class*="-date"], +#page-4 [class*="-yppf"], +#page-4 [class*="-org"], +#page-5 [class*="-number"], +#page-5 [class*="-name"], +#page-5 [class*="-room"], +#page-5 [class*="-date"], +#page-5 [class*="-yppf"], +#page-5 [class*="-org"], +#page-7 [class*="-number"], +#page-7 [class*="-name"], +#page-7 [class*="-room"], +#page-7 [class*="-date"], +#page-7 [class*="-yppf"], +#page-7 [class*="-org"], +#page-8 [class*="-number"], +#page-8 [class*="-name"], +#page-8 [class*="-room"], +#page-8 [class*="-date"], +#page-8 [class*="-yppf"], +#page-8 [class*="-org"], +#page-9 [class*="-number"], +#page-9 [class*="-name"], +#page-9 [class*="-room"], +#page-9 [class*="-date"], +#page-9 [class*="-yppf"], +#page-9 [class*="-org"], +#page-10 [class*="-number"], +#page-10 [class*="-name"], +#page-10 [class*="-room"], +#page-10 [class*="-date"], +#page-10 [class*="-yppf"], +#page-10 [class*="-org"] { + font-size: 20px !important; + line-height: inherit; +} + +/* p5 第一行不加粗, p11 第二段不加粗 */ +#page-5 .p5-title, +#page-11 .p11-title { + font-weight: 400 !important; +} + +/* p7/p8/p9 小标题比正文略大,且不做首字放大 */ +#page-7 .p7-section-title, +#page-8 .p8-section-title, +#page-9 .p9-section-title { + font-size: 21px !important; + font-weight: 600 !important; + line-height: 1.75 !important; +} + +/* 显式保证第一段是标题时也能加粗 (覆盖全局第一段不加粗规则) */ +#page-7 .p7-section-title::first-line, +#page-8 .p8-section-title::first-line, +#page-9 .p9-section-title::first-line { + font-weight: 600 !important; +} + +/* 统一数据高亮字号,避免忽大忽小 */ +[class*="-number"], +[class*="-name"], +[class*="-room"], +[class*="-yppf"], +[class*="-org"], +.fc-rose { + font-size: 20px !important; + line-height: inherit; +} + +/* 日期/年份回归正文字号,避免过于突兀 */ +[class*="-date"], +.p1-year { + font-size: 17px !important; + font-weight: 700 !important; +} + +/* 特殊超大字号适度收敛,保证跨页一致性 */ +.p10-large, +.highlight-large, +.fs-36b { + font-size: 28px !important; +} + +.highlight { + font-size: 20px !important; +} +/* 响应式设计 */ +@media (max-width: 768px) { + .home-main-title { + font-size: 42px; + } + + .home-subtitle { + font-size: 20px; + } + + .home-name { + font-size: 24px; + } + + .intro-text { + font-size: 18px; + line-height: 1.8; + } + + .highlight { + font-size: 22px; + } + + .highlight-large { + font-size: 36px; + } + + .text-content { + padding: 40px 20px; + } +} + +@media (max-width: 480px) { + .home-main-title { + font-size: 36px; + } + + .home-subtitle { + font-size: 18px; + } + + .intro-text { + font-size: 16px; + } + + .highlight { + font-size: 20px; + } + + .highlight-large { + font-size: 30px; + } +} +/* ==================== + 针对部分页面的行距/间距微调 + ==================== */ + +/* p10 统计行间距收紧 */ +#page-10 .p10-stat+.p10-stat { + margin-top: 6px !important; +} + +/* p11 日期与正文行距统一,且不放大首字 */ +#page-11 .p11-date::first-letter { + font-size: inherit !important; + font-weight: inherit !important; + margin-right: 0 !important; +} + +/* p12/p13 以及 intro-text 段落内行距微调 (增加精致感) */ +#page-12 .p12-text-box>p, +#app .content .text-box>p.intro-text { + line-height: 1.75 !important; +} + +/* 确保所有数据高亮在 17px 背景下依然清晰且不过大 */ +[class*="-number"] { + font-size: 20px !important; +} + +/* 统一 fc-rose (课程数据) */ +.fc-rose { + font-size: 20px !important; + font-weight: 700 !important; + line-height: inherit !important; +} + +#page-15 .text-box, +#page-16 .text-box { + text-align: center !important; + align-items: center !important; +} +#page-16 .p16-yq-block { + display: flex; + flex-direction: column; + gap: 18px; +} + +#page-16 .p16-yq-line { + font-family: "Siyuan SongTi regular", serif; + font-size: clamp(16px, 3vw, 22px); + color: #000000; + line-height: 1.45 !important; + margin: 0; + text-align: center; +} + +/* 页面特定例外规则 - 必须放在最后以覆盖通用规则 */ +p.p11-date, +#page-11 p.p11-date { + font-size: 18px !important; + font-weight: 700 !important; +} \ No newline at end of file diff --git a/static/Appointment/assets/css/summary2025.css.backup b/static/Appointment/assets/css/summary2025.css.backup new file mode 100644 index 000000000..770a2500e --- /dev/null +++ b/static/Appointment/assets/css/summary2025.css.backup @@ -0,0 +1,1770 @@ +/* 字体定义 */ +@font-face { + font-family: "Siyuan SongTi regular"; + src: url("../fonts/Siyuan-SongTi-regular.ttf") format("truetype"); +} + +@font-face { + font-family: "GenWanMin2TC-R"; + src: url("../fonts/GenWanMin2TC-R.otf") format("otf"); +} + +/* PREFLIGHT */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +h1, +h2, +h3, +p { + margin: 0; + padding: 0; +} + +/* UTILITIES */ +.mt-35 { + margin-top: 35px; +} + +.mt-50 { + margin-top: 50px; +} + +.mt-35-no-bold { + margin-top: 35px; +} +.fs-36b { + font-size: 36px; + font-weight: bold; +} + +.fc-blue { + color: #004AAD; +} + +.fc-rose { + font-weight: 900; + color: #ff5050; + font-size: 22px; + padding: 0 0.2em; +} + +/* BODY */ +body { + font-family: "Siyuan SongTi regular", "SimSun", "宋体", STSong, serif; + font-size: 23px; + line-height: 1.5; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + -webkit-font-smoothing: subpixel-antialiased; + overflow: hidden; +} + +p { + margin: 10px 0 0 0; + padding: 0; + color: rgb(60, 60, 60); + font-family: "Siyuan SongTi regular", "SimSun", "宋体", STSong, serif; + font-size: 23px; + line-height: 2.2; + text-align: center; +} + +button { + cursor: pointer; + border: none; + background: transparent; +} + +/* 音乐播放器 */ +#music-container { + position: fixed; + top: 20px; + right: 20px; + z-index: 1000; +} + +#play-music { + background: rgba(255, 255, 255, 0.8); + border-radius: 50%; + width: 50px; + height: 50px; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); +} + +#playing { + display: block; +} + +#paused { + display: none; +} + +/* 页面通用样式 */ +.section.page { + position: relative; + width: 100vw; + height: 100vh; + overflow: hidden; +} + +.bg-image { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + object-fit: cover; + z-index: 1; + opacity: 0.8; +} + +/* 启动页样式 */ +.splash-page { + background: transparent; +} + +.splash-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + object-fit: cover; + z-index: 1; +} + +.splash-content { + position: relative; + z-index: 2; + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + justify-content: flex-start; + /* 改为从上排,让中间内容自然下落,footer沉底 */ + align-items: center; + padding: 20px; +} + +/* 移除旧的 splash-decorations 相关 */ + +.splash-logo { + position: absolute; + top: 30px; + right: 20px; + width: 80px; + height: auto; + animation: fade-in 1s ease forwards; + z-index: 10; +} + +.splash-photos { + position: absolute; + top: -20px; + left: -30px; + width: 260px; + /* 左上角装饰大小 */ + height: auto; + transform: rotate(-15deg); + animation: slide-down 1.4s ease forwards; + pointer-events: none; + /* 防止遮挡点击 */ +} + +.splash-centered-info { + width: 100%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + margin-top: 25vh; + /* 距离顶部位置 */ + position: relative; + z-index: 5; +} + +.splash-title { + width: 85%; + max-width: 500px; + height: auto; + animation: fade-in 1.2s ease forwards; + margin: 0; +} + +.splash-user-greeting { + font-family: "GenWanMin2TC-R", "Siyuan SongTi regular", serif; + font-size: 28px; + color: #ffffff; + text-shadow: 0 2px 8px rgba(0, 74, 173, 0.3); + /* 蓝色阴影,增加对比度 */ + letter-spacing: 2px; + opacity: 0; + animation: fade-in-up 1.5s ease forwards 0.5s; + /* 延迟出现 */ + display: flex; + flex-direction: column; + align-items: center; + gap: 10px; +} + +.user-name { + font-size: 32px; + font-weight: bold; + border-bottom: 2px solid rgba(255, 255, 255, 0.6); + padding-bottom: 5px; +} + +.greeting-text { + font-size: 24px; + opacity: 0.9; +} + +.splash-footer { + width: 100%; + max-width: 400px; + display: flex; + flex-direction: column; + align-items: center; + gap: 20px; + margin-top: auto; + /* Push to bottom */ + margin-bottom: 60px; + /* Bottom padding */ +} + +@keyframes slide-down { + from { + opacity: 0; + transform: translate(-20px, -40px) rotate(-20deg); + } + + to { + opacity: 1; + transform: translate(0, 0) rotate(-15deg); + } +} + +@keyframes fade-in-up { + from { + opacity: 0; + transform: translateY(20px); + } + + to { + opacity: 1; + transform: translateY(0); + } +} + +.splash-button-img { + width: 200px; + height: auto; + cursor: pointer; + transition: all 0.3s ease; + opacity: 0.5; + filter: grayscale(100%); +} + +.splash-button-img.active { + opacity: 1; + filter: grayscale(0%); + animation: pulse 1.5s ease-in-out infinite; +} + +.splash-button-img:hover { + transform: scale(1.05); +} + +@keyframes fade-in { + from { + opacity: 0; + transform: translateY(20px); + } + + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes pulse { + + 0%, + 100% { + transform: scale(1); + } + + 50% { + transform: scale(1.05); + } +} + +/* 全局数据样式 - 所有number, name, room, date, yppf, org等强调类 */ +[class*="-number"], +[class*="-name"], +[class*="-room"], +[class*="-date"], +[class*="-yppf"], +[class*="-org"] { + color: #ff5050; + font-size: 20px; + font-weight: 700; + padding: 0 0.1em; +} + +/* 每页首字放大样式 */ +.p1-question::first-letter, +.p2-greeting::first-letter, +.p3-title::first-letter, +.p4-title::first-letter, +.p5-title::first-letter, +.p6-title:not(.mt-35)::first-letter, +.p7-section-title:first-child::first-letter, +.p8-section-title:first-child::first-letter, +.p9-section-title:first-child::first-letter, +.p10-title::first-letter, +.p12-stat:first-of-type::first-letter, +.text-box>p:first-child::first-letter { + font-size: 24px; + font-weight: bold; + line-height: 1.1; + margin-right: 2px; +} + +.content { + position: relative; + z-index: 2; + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 5vh 5vw; + overflow-y: auto; +} + +/* 页面对齐方式设定 */ + +/* 居中对齐页面 (强制完全居中) */ +#page-home .content, +#page-4 .content, +#page-6 .content, +#page-7 .content, +#page-8 .content, +#page-9 .content, +#page-12 .content, +#page-14 .content, +#page-15 .content, +#page-16 .content { + align-items: center !important; + justify-content: center !important; +} + +#page-home .content [class*="-text-box"], +#page-4 .content [class*="-text-box"], +#page-6 .content [class*="-text-box"], +#page-7 .content [class*="-text-box"], +#page-8 .content [class*="-text-box"], +#page-9 .content [class*="-text-box"], +#page-12 .content [class*="-text-box"], +#page-14 .content [class*="-text-box"], +#page-15 .content [class*="-text-box"], +#page-16 .content [class*="-text-box"] { + text-align: center !important; + align-items: center !important; + max-width: 90% !important; + display: flex !important; + flex-direction: column !important; + margin: 0 auto !important; +} + +#page-home .content p, +#page-4 .content p, +#page-6 .content p, +#page-7 .content p, +#page-8 .content p, +#page-9 .content p, +#page-12 .content p, +#page-14 .content p, +#page-15 .content p, +#page-16 .content p { + text-align: center !important; + margin-left: auto !important; + margin-right: auto !important; +} + +/* 左对齐页面 */ +#page-2 .content, +#page-3 .content, +#page-10 .content { + align-items: flex-start !important; + padding-left: 15vw !important; +} + +#page-2 .content p, +#page-3 .content p, +#page-10 .content p { + text-align: left !important; +} + +/* 右对齐页面 */ +#page-5 .content, +#page-11 .content, +#page-13 .content, +#page-17 .content { + align-items: flex-end !important; + padding-right: 15vw !important; +} + +#page-5 .content p, +#page-11 .content p, +#page-13 .content p, +#page-17 .content p { + text-align: right !important; +} + +/* P1: 首页样式 - 诗意文案 */ +#page-home { + background: transparent; +} + +.p1-content { + justify-content: center; + align-items: center; + padding: 60px 40px 80px 40px; +} + +.p1-text-box { + text-align: center; + max-width: 90%; + opacity: 0; + transform: translateY(30px); +} + +.p1-text-box.animate-in { + animation: fade-in-up 0.8s ease forwards; + opacity: 1; + transform: translateY(0); +} + +.p1-question { + font-size: 16px; + font-weight: 700; +} + +.p1-subtitle { + font-size: 18px; + font-weight: 400; +} + +.p1-poem { + font-size: 16px; +} + +.p1-stat { + font-size: 16px; +} + +.p1-number { + color: #ff5050; + font-weight: 700; + font-size: 20px; + padding: 0 3px; +} + +.p1-year { + font-size: 18px; + font-weight: 600; +} + +/* P2: 欢迎页面样式 */ +#page-2 { + background: transparent; +} + +.p2-content { + justify-content: center; + align-items: center; + padding: 80px 40px; +} + +.p2-text-box { + text-align: left; + max-width: 90%; + opacity: 0; + transform: translateY(30px); +} + +.p2-text-box p { + text-align: left; +} + +.p2-text-box.animate-in { + animation: fade-in-up 0.8s ease forwards; + opacity: 1; + transform: translateY(0); +} + +.p2-greeting { + font-family: "Siyuan SongTi regular", serif; + font-size: 18px; + font-weight: 700; + color: #000000; + line-height: 2.2; + margin: 0; +} + +.p2-name { + color: #ff5050; + font-size: 28px; +} + +.p2-intro { + font-family: "Siyuan SongTi regular", serif; + font-size: 18px; + color: #000000; + line-height: 2.2; + margin: 0; +} + +/* P3: 地下室使用情况样式 */ +#page-3 { + background: transparent; +} + +.p3-content { + justify-content: center; + align-items: center; +} + +.p3-text-box { + text-align: left; + max-width: 90%; + opacity: 0; + transform: translateY(30px); +} + +.p3-text-box p { + text-align: left; +} + +.p3-text-box.animate-in { + animation: fade-in-up 0.8s ease forwards; + opacity: 1; + transform: translateY(0); +} + +.p3-title { + font-family: "Siyuan SongTi regular", serif; + font-size: 16px; + font-weight: 700; + color: #000000; + margin: 0; + line-height: 2.2; +} + +.p3-stat { + font-family: "Siyuan SongTi regular", serif; + font-size: 16px; + color: #000000; + line-height: 2.2; + margin: 0; +} + +.p3-number { + color: #ff5050; + font-weight: 700; + font-size: 28px; + margin: 0 3px; +} + +.p3-msg { + font-family: "Siyuan SongTi regular", serif; + font-size: 23px; + color: #000000; + font-weight: 600; + line-height: 2.2; + margin: 15px 0; +} + +.p3-divider { + font-family: "Siyuan SongTi regular", serif; + font-size: 16px; + color: #000000; + margin: 25px 0 10px 0; + line-height: 2.2; +} + +.p3-date { + font-family: "Siyuan SongTi regular", serif; + font-size: 16px; + color: #ff5050; + font-weight: 700; + line-height: 2.2; + margin: 0; +} + +.p3-detail { + font-family: "Siyuan SongTi regular", serif; + font-size: 16px; + color: #000000; + line-height: 2.2; + margin: 0; +} + +.p3-room { + color: #ff5050; + font-weight: 700; +} + +/* P4: 最长连续使用样式 */ +#page-4 { + background: transparent; +} + +.p4-text-box { + text-align: left; + max-width: 90%; +} + +.p4-text-box p { + text-align: left; +} + +.p4-title { + font-family: "Siyuan SongTi regular", serif; + font-size: 16px; + font-weight: 700; + color: #000000; + line-height: 2.2; + margin: 0; +} + +.p4-date { + font-family: "Siyuan SongTi regular", serif; + font-size: 23px; + color: #ff5050; + font-weight: 700; + line-height: 2.2; + margin: 0; +} + +.p4-detail, +.p4-witness { + font-family: "Siyuan SongTi regular", serif; + font-size: 16px; + color: #000000; + line-height: 2.2; + margin: 0; +} + +.p4-room, +.p4-usage { + font-family: "Siyuan SongTi regular", serif; + font-size: 18px; + color: #ff5050; + font-weight: 700; + line-height: 2.2; + margin: 0; +} + +.p4-stat { + font-family: "Siyuan SongTi regular", serif; + font-size: 16px; + color: #000000; + line-height: 2.2; + margin: 0; +} + +.p4-number { + color: #ff5050; + font-weight: 700; + font-size: 20px; + margin: 0 3px; +} + +.p4-yppf { + color: #ff5050; + font-weight: 700; +} + +.p4-to { + font-family: "Siyuan SongTi regular", serif; + font-size: 16px; + color: #000000; + line-height: 2.2; + margin: 0; +} + +.p4-message { + font-family: "Siyuan SongTi regular", serif; + font-size: 16px; + color: #000000; + line-height: 2.2; + margin: 15px 0 5px 0; +} + +/* P5: 自习室使用样式 */ +#page-5 { + background: transparent; +} + +.p5-content { + justify-content: center; + align-items: center; +} + +.p5-text-box { + text-align: left; + max-width: 90%; +} + +.p5-text-box p { + text-align: left; +} + +.p5-title { + font-family: "Siyuan SongTi regular", serif; + font-size: 16px; + font-weight: 700; + color: #000000; + line-height: 2.2; + margin: 0; +} + +.p5-stat { + font-family: "Siyuan SongTi regular", serif; + font-size: 16px; + color: #000000; + line-height: 2.2; + margin: 0; +} + +.p5-number { + color: #ff5050; + font-weight: 700; + font-size: 20px; + margin: 0 3px; +} + +.p5-poem { + font-family: "Siyuan SongTi regular", serif; + font-size: 16px; + color: #000000; + line-height: 2.2; + margin: 0; +} + +.p5-room { + font-family: "Siyuan SongTi regular", serif; + font-size: 18px; + color: #ff5050; + font-weight: 700; + line-height: 2.2; + margin: 0; +} + +.p5-detail { + font-family: "Siyuan SongTi regular", serif; + font-size: 16px; + color: #000000; + line-height: 2.2; + margin: 0; +} + +.p5-share { + font-family: "Siyuan SongTi regular", serif; + font-size: 16px; + color: #000000; + line-height: 2.2; + margin: 0; +} + +/* P6: 研讨室和功能室样式 */ +#page-6 { + background: transparent; +} + +.p6-content { + justify-content: center; + align-items: center; + padding: 80px 40px; +} + +.p6-text-box { + text-align: left; + max-width: 400px; +} + +.p6-title { + font-family: "Siyuan SongTi regular", serif; + font-size: 16px; + font-weight: 700; + color: #000000; + line-height: 2.2; + margin: 0; +} + +.p6-stat { + font-family: "Siyuan SongTi regular", serif; + font-size: 16px; + color: #000000; + line-height: 2.2; + margin: 0; +} + +.p6-number { + color: #ff5050; + font-weight: 700; + font-size: 20px; + margin: 0 3px; +} + +.p6-poem { + font-family: "Siyuan SongTi regular", serif; + font-size: 16px; + color: #000000; + line-height: 2.2; + margin: 0; +} + +.p6-memory { + font-family: "Siyuan SongTi regular", serif; + font-size: 16px; + color: #000000; + line-height: 2.2; + margin: 0; +} + +.p6-date { + color: #ff5050; + font-weight: 700; +} + +.p6-detail { + font-family: "Siyuan SongTi regular", serif; + font-size: 16px; + color: #000000; + line-height: 2.2; + margin: 0; +} + +.p6-room { + color: #ff5050; + font-weight: 700; +} + +.p6-wish { + font-family: "Siyuan SongTi regular", serif; + font-size: 16px; + color: #000000; + line-height: 2.2; + margin: 0; +} + +.p6-yppf { + font-family: "Siyuan SongTi regular", serif; + font-size: 18px; + color: #ff5050; + font-weight: 700; + line-height: 2.2; + margin: 0; +} + +/* P7: 预约习惯样式 */ +#page-7 { + background: transparent; +} + +.p7-content { + justify-content: center; + align-items: center; + padding: 80px 40px; +} + +.p7-text-box { + text-align: left; + max-width: 400px; +} + +.p7-section-title { + font-family: "Siyuan SongTi regular", serif; + font-size: 21px; + font-weight: 700; + color: #000000; + line-height: 2.2; + margin: 0; +} + +.p7-stat { + font-family: "Siyuan SongTi regular", serif; + font-size: 16px; + color: #000000; + line-height: 2.2; + margin: 0; +} + +.p7-number { + color: #ff5050; + font-weight: 700; + font-size: 20px; + margin: 0 3px; +} + +.p7-detail { + font-family: "Siyuan SongTi regular", serif; + font-size: 16px; + color: #000000; + line-height: 2.2; + margin: 0; +} + +.p7-yppf { + color: #ff5050; + font-weight: 700; +} + +.p7-room { + color: #ff5050; + font-weight: 700; +} + +.p7-message { + font-family: "Siyuan SongTi regular", serif; + font-size: 16px; + color: #000000; + line-height: 2.2; + margin: 10px 0 5px 0; +} + +/* P8: 最极限样式 */ +#page-8 { + background: transparent; +} + +.p8-content { + justify-content: center; + align-items: center; + padding: 80px 40px; +} + +.p8-text-box { + text-align: left; + max-width: 400px; +} + +.p8-section-title { + font-family: "Siyuan SongTi regular", serif; + font-size: 21px; + font-weight: 700; + color: #000000; + line-height: 2.2; + margin: 0; +} + +.p8-stat { + font-family: "Siyuan SongTi regular", serif; + font-size: 16px; + color: #000000; + line-height: 2.2; + margin: 0; +} + +.p8-number { + color: #ff5050; + font-weight: 700; + font-size: 20px; + margin: 0 3px; +} + +.p8-detail { + font-family: "Siyuan SongTi regular", serif; + font-size: 16px; + color: #000000; + line-height: 2.2; + margin: 0; +} + +.p8-date { + font-family: "Siyuan SongTi regular", serif; + font-size: 16px; + color: #ff5050; + font-weight: 700; + line-height: 2.2; + margin: 0; +} + +.p8-room { + font-family: "Siyuan SongTi regular", serif; + font-size: 18px; + color: #ff5050; + font-weight: 700; + line-height: 2.2; + margin: 0; +} + +.p8-witness, +.p8-yppf { + font-family: "Siyuan SongTi regular", serif; + font-size: 16px; + color: #000000; + line-height: 2.2; + margin: 0; +} + +.p8-yppf { + font-weight: 700; + color: #ff5050; +} + +.p8-message { + font-family: "Siyuan SongTi regular", serif; + font-size: 16px; + color: #000000; + font-weight: 600; + line-height: 2.2; + margin: 10px 0 5px 0; +} + +/* P9: 最投缘样式 */ +#page-9 { + background: transparent; +} + +.p9-content { + justify-content: center; + align-items: center; + padding: 80px 40px; +} + +.p9-text-box { + text-align: left; + max-width: 400px; +} + +.p9-section-title { + font-family: "Siyuan SongTi regular", serif; + font-size: 21px; + font-weight: 700; + color: #000000; + line-height: 2.2; + margin: 0; +} + +.p9-stat { + font-family: "Siyuan SongTi regular", serif; + font-size: 16px; + color: #000000; + line-height: 2.2; + margin: 0; +} + +.p9-number { + color: #ff5050; + font-weight: 700; + font-size: 20px; + margin: 0 3px; +} + +.p9-name { + color: #ff5050; + font-weight: 700; +} + +.p9-detail { + font-family: "Siyuan SongTi regular", serif; + font-size: 16px; + color: #000000; + line-height: 2.2; + margin: 0; +} + +/* P10: 书院总体总结样式 */ +#page-10 { + background: transparent; +} + +.p10-content { + justify-content: center; + align-items: center; + padding: 80px 40px; +} + +.p10-text-box { + text-align: left; + max-width: 400px; +} + +.p10-large { + font-size: 32px; + font-weight: 700; +} + +.p10-title { + font-family: "Siyuan SongTi regular", serif; + font-size: 18px; + color: #000000; + line-height: 2.2; + margin: 0; +} + +.p10-poem { + font-family: "Siyuan SongTi regular", serif; + font-size: 16px; + color: #000000; + line-height: 2.2; + margin: 3px 0; +} + +.p10-stat { + font-family: "Siyuan SongTi regular", serif; + font-size: 18px; + color: #000000; + line-height: 2.2; + margin: 0; +} + +.p10-number { + color: #ff5050; + font-weight: 700; + font-size: 22px; + margin-right: 5px; +} + +.p10-witness { + font-family: "Siyuan SongTi regular", serif; + font-size: 16px; + color: #000000; + line-height: 2.2; +} + +.p10-wish { + font-family: "Siyuan SongTi regular", serif; + font-size: 18px; + color: #000000; + font-weight: 600; + line-height: 2.2; +} + +/* P11: 首次相遇样式 */ +#page-11 { + background: transparent; +} + +.p11-content { + justify-content: center; + align-items: center; + padding: 100px 20px; +} + +.p11-text-box { + text-align: center; +} + +.p11-date { + font-family: "Siyuan SongTi regular", serif; + font-size: 18px; + color: #ff5050; + font-weight: 700; + line-height: 2.2; + margin: 0; +} + +.p11-poem { + font-family: "Siyuan SongTi regular", serif; + font-size: 18px; + color: #000000; + line-height: 2.2; + margin: 0; +} + +.p11-number { + color: #ff5050; + font-weight: 700; + font-size: 24px; + margin: 0 5px; +} + +.p11-title { + font-family: "Siyuan SongTi regular", serif; + font-size: 20px; + color: #000000; + font-weight: 600; + line-height: 2.2; +} + +.p11-stat { + font-family: "Siyuan SongTi regular", serif; + font-size: 18px; + color: #000000; + line-height: 2.2; + margin: 0; +} + +.p11-wish { + font-family: "Siyuan SongTi regular", serif; + font-size: 18px; + color: #000000; + line-height: 2.2; + margin-top: 20px; + font-weight: 600; +} + +/* P12: 小组参与详情样式 */ +#page-12 { + background: transparent; +} + +.p12-content { + justify-content: center; + align-items: center; + padding: 80px 20px; +} + +.p12-text-box { + text-align: center; +} + +.p12-stat { + font-family: "Siyuan SongTi regular", serif; + font-size: 18px; + color: #000000; + line-height: 2.2; + margin: 0; +} + +.p12-number { + color: #ff5050; + font-weight: 700; + font-size: 24px; + margin: 0 5px; +} + +.p12-org { + color: #ff5050; + font-weight: 700; + font-size: 20px; +} + +.p12-detail { + font-family: "Siyuan SongTi regular", serif; + font-size: 18px; + color: #000000; + line-height: 2.2; +} + +.p12-poem { + font-family: "Siyuan SongTi regular", serif; + font-size: 18px; + color: #000000; + line-height: 2.2; + margin: 0; +} + +.p12-title { + font-family: "Siyuan SongTi regular", serif; + font-size: 20px; + color: #000000; + font-weight: 600; + line-height: 2.2; +} + +.p12-message { + font-family: "Siyuan SongTi regular", serif; + font-size: 18px; + color: #000000; + font-weight: 600; + line-height: 2.2; + margin-top: 20px; +} + +/* 底部按钮区域 */ +.home-footer { + width: 100%; + max-width: 400px; + display: flex; + flex-direction: column; + align-items: center; + gap: 20px; + margin-top: auto; +} + +#continue-button { + background: linear-gradient(135deg, #f9d89c 0%, #e8b86d 100%); + border: none; + border-radius: 30px; + padding: 15px 50px; + font-size: 20px; + font-weight: 600; + color: #5a4a3a; + box-shadow: 0 4px 15px rgba(232, 184, 109, 0.4); + transition: all 0.3s ease; + cursor: pointer; +} + +#continue-button:hover { + transform: translateY(-2px); + box-shadow: 0 6px 20px rgba(232, 184, 109, 0.5); +} + +#continue-button:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.agree-rule-label { + display: flex; + align-items: center; + gap: 10px; + font-size: 14px; + color: #000000; + cursor: pointer; +} + +.agree-rule-label input[type="checkbox"] { + width: 18px; + height: 18px; + cursor: pointer; +} + +.agree-rule-label p { + margin: 0; + font-size: 14px; + line-height: 1.5; + color: #000000; +} + +.agree-rule-label a { + color: #5a8a87; + text-decoration: underline; +} + +/* 协议弹窗 */ +#rule { + display: none; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.5); + z-index: 9999; + align-items: center; + justify-content: center; + padding: 20px; +} + +#rule.show { + display: flex; +} + +.rule-wrapper { + background: white; + border-radius: 15px; + padding: 30px; + max-width: 600px; + max-height: 80vh; + overflow-y: auto; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3); +} + +.rule-title { + font-size: 22px; + font-weight: 700; + color: #2c5f5d; + margin: 0; + text-align: center; +} + +.rule-content { + font-size: 14px; + line-height: 1.8; + color: #000000; + margin: 0; +} + +.rule-content ol { + padding-left: 20px; +} + +.rule-content li { + margin-bottom: 12px; +} + +#rule-button { + background: linear-gradient(135deg, #f9d89c 0%, #e8b86d 100%); + border: none; + border-radius: 25px; + padding: 12px 40px; + font-size: 16px; + font-weight: 600; + color: #5a4a3a; + cursor: pointer; + display: block; + margin: 20px auto 0; + box-shadow: 0 4px 10px rgba(232, 184, 109, 0.3); + transition: all 0.3s ease; +} + +#rule-button:hover { + transform: translateY(-2px); + box-shadow: 0 6px 15px rgba(232, 184, 109, 0.4); +} + +/* 通用文本内容页面样式 */ +.text-content { + justify-content: center; + align-items: center; + padding: 60px 40px; +} + +.text-box { + max-width: 90%; + width: 100%; + text-align: right; + display: flex; + flex-direction: column; + align-items: flex-end; + justify-content: center; +} + +.intro-text { + font-family: "Siyuan SongTi regular", serif; + font-size: clamp(16px, 3vw, 22px); + line-height: 2.2; + color: #000000; + margin: 8px 0; + text-align: center; +} + +.highlight { + color: #d4a574; + font-weight: 700; + font-size: 26px; +} + +.highlight-large { + color: #d4a574; + font-weight: 900; + font-size: 42px; + display: inline-block; + margin: 0 8px; +} + +/* P2-P17 页面特定样式 */ +#page-2, +#page-3, +#page-4, +#page-5, +#page-6, +#page-7, +#page-8, +#page-9, +#page-10, +#page-11, +#page-12, +#page-13, +#page-14, +#page-15, +#page-16, +#page-17 { + background: #fff; +} + +/* 动画效果 */ +@keyframes fade-in-up { + from { + opacity: 0; + transform: translateY(30px); + } + + to { + opacity: 1; + transform: translateY(0); + } +} + +/* 为所有内容框添加初始隐藏状态和动画效果 */ +.text-box, +.home-title-container, +.p1-text-box, +.p2-text-box, +.p3-text-box, +.p4-text-box, +.p5-text-box, +.p6-text-box, +.p7-text-box, +.p8-text-box, +.p9-text-box, +.p10-text-box, +.p11-text-box, +.p12-text-box { + opacity: 0; + transform: translateY(30px); +} + +.text-box.animate-in, +.home-title-container.animate-in, +.p1-text-box.animate-in, +.p2-text-box.animate-in, +.p3-text-box.animate-in, +.p4-text-box.animate-in, +.p5-text-box.animate-in, +.p6-text-box.animate-in, +.p7-text-box.animate-in, +.p8-text-box.animate-in, +.p9-text-box.animate-in, +.p10-text-box.animate-in, +.p11-text-box.animate-in, +.p12-text-box.animate-in { + animation: fade-in-up 0.8s ease forwards; + opacity: 1; + transform: translateY(0); +} + +/* ==================== + 统一排版基线(字号/行距/段间距) + ==================== */ + +/* ==================== + 全局段间距统一控制 + ==================== */ + +/* 第一个段落无上边距 */ +#app .content p:first-child { + margin-top: 0 !important; +} + +/* 相邻段落统一12px间距 */ +#app .content p + p { + margin-top: 12px !important; +} + +/* 明显分段标记:35px间距 */ +#app .content p.mt-35 { + margin-top: 35px !important; +} + +/* 明显分段标记:50px间距 */ +#app .content p.mt-50 { + margin-top: 50px !important; +} + +/* 统一正文字号为17px(P1除外) */ +#app .content p:not([class*="p1-"]) { + font-size: 17px !important; +} + +/* 仅对非第一段的分段起始行(mt-35/mt-50)做首行加粗 */ +#app .content [class*="-text-box"]>p.mt-35:not(:first-of-type)::first-line, +#app .content [class*="-text-box"]>p.mt-50:not(:first-of-type)::first-line, +#app .content .text-box>p.mt-35:not(:first-of-type)::first-line, +#app .content .text-box>p.mt-50:not(:first-of-type)::first-line { + font-weight: 600; +} + +/* 全局兜底:所有页面第一段第一行不加粗 */ +#app .content [class*="-text-box"]>p:not(.p7-section-title):not(.p8-section-title):not(.p9-section-title):first-of-type::first-line, +#app .content .text-box>p:not(.p7-section-title):not(.p8-section-title):not(.p9-section-title):first-of-type::first-line { + font-weight: 400 !important; +} + +/* 第一段首字保留放大,但不额外加粗 */ +#app .content [class*="-text-box"]>p:not(.p7-section-title):not(.p8-section-title):not(.p9-section-title):first-of-type::first-letter, +#app .content .text-box>p:not(.p7-section-title):not(.p8-section-title):not(.p9-section-title):first-of-type::first-letter { + font-weight: 400 !important; +} + +/* p4-p10:统一用户数据字号并降低偏大观感 */ +#page-4 [class*="-number"], +#page-4 [class*="-name"], +#page-4 [class*="-room"], +#page-4 [class*="-date"], +#page-4 [class*="-yppf"], +#page-4 [class*="-org"], +#page-5 [class*="-number"], +#page-5 [class*="-name"], +#page-5 [class*="-room"], +#page-5 [class*="-date"], +#page-5 [class*="-yppf"], +#page-5 [class*="-org"], +#page-7 [class*="-number"], +#page-7 [class*="-name"], +#page-7 [class*="-room"], +#page-7 [class*="-date"], +#page-7 [class*="-yppf"], +#page-7 [class*="-org"], +#page-8 [class*="-number"], +#page-8 [class*="-name"], +#page-8 [class*="-room"], +#page-8 [class*="-date"], +#page-8 [class*="-yppf"], +#page-8 [class*="-org"], +#page-9 [class*="-number"], +#page-9 [class*="-name"], +#page-9 [class*="-room"], +#page-9 [class*="-date"], +#page-9 [class*="-yppf"], +#page-9 [class*="-org"], +#page-10 [class*="-number"], +#page-10 [class*="-name"], +#page-10 [class*="-room"], +#page-10 [class*="-date"], +#page-10 [class*="-yppf"], +#page-10 [class*="-org"] { + font-size: 20px !important; + line-height: inherit; +} + +/* p5 第一行不加粗, p11 第二段不加粗 */ +#page-5 .p5-title, +#page-11 .p11-title { + font-weight: 400 !important; +} + +/* p7/p8/p9 小标题比正文略大,且不做首字放大 */ +#page-7 .p7-section-title, +#page-8 .p8-section-title, +#page-9 .p9-section-title { + font-size: 21px !important; + font-weight: 600 !important; + line-height: 1.75 !important; +} + +/* 显式保证第一段是标题时也能加粗 (覆盖全局第一段不加粗规则) */ +#page-7 .p7-section-title::first-line, +#page-8 .p8-section-title::first-line, +#page-9 .p9-section-title::first-line { + font-weight: 600 !important; +} + +/* 统一数据高亮字号,避免忽大忽小 */ +[class*="-number"], +[class*="-name"], +[class*="-room"], +[class*="-yppf"], +[class*="-org"], +.fc-rose { + font-size: 20px !important; + line-height: inherit; +} + +/* 日期/年份回归正文字号,避免过于突兀 */ +[class*="-date"], +.p1-year { + font-size: 17px !important; + font-weight: 400 !important; +} + +/* 特殊超大字号适度收敛,保证跨页一致性 */ +.p10-large, +.highlight-large, +.fs-36b { + font-size: 28px !important; +} + +.highlight { + font-size: 20px !important; +} +/* 响应式设计 */ +@media (max-width: 768px) { + .home-main-title { + font-size: 42px; + } + + .home-subtitle { + font-size: 20px; + } + + .home-name { + font-size: 24px; + } + + .intro-text { + font-size: 18px; + line-height: 1.8; + } + + .highlight { + font-size: 22px; + } + + .highlight-large { + font-size: 36px; + } + + .text-content { + padding: 40px 20px; + } +} + +@media (max-width: 480px) { + .home-main-title { + font-size: 36px; + } + + .home-subtitle { + font-size: 18px; + } + + .intro-text { + font-size: 16px; + } + + .highlight { + font-size: 20px; + } + + .highlight-large { + font-size: 30px; + } +} +/* ==================== + 针对部分页面的行距/间距微调 + ==================== */ + +/* p10 统计行间距收紧 */ +#page-10 .p10-stat+.p10-stat { + margin-top: 6px !important; +} + +/* p11 日期与正文行距统一,且不放大首字 */ +#page-11 .p11-date::first-letter { + font-size: inherit !important; + font-weight: inherit !important; + margin-right: 0 !important; +} + +/* p12/p13 以及 intro-text 段落内行距微调 (增加精致感) */ +#page-12 .p12-text-box>p, +#app .content .text-box>p.intro-text { + line-height: 1.75 !important; +} + +/* 确保所有数据高亮在 17px 背景下依然清晰且不过大 */ +[class*="-number"] { + font-size: 20px !important; +} + +/* 统一 fc-rose (课程数据) */ +.fc-rose { + font-size: 20px !important; + font-weight: 700 !important; + line-height: inherit !important; +} + +#page-15 .text-box, +#page-16 .text-box { + text-align: center !important; + align-items: center !important; +} \ No newline at end of file diff --git "a/static/Appointment/assets/img/summary2025/0.1_\345\256\214\346\225\264\346\225\210\346\236\234.png" "b/static/Appointment/assets/img/summary2025/0.1_\345\256\214\346\225\264\346\225\210\346\236\234.png" new file mode 100644 index 000000000..8db00a595 Binary files /dev/null and "b/static/Appointment/assets/img/summary2025/0.1_\345\256\214\346\225\264\346\225\210\346\236\234.png" differ diff --git "a/static/Appointment/assets/img/summary2025/0.2_\345\211\215\347\253\257\346\267\273\345\212\240\350\257\264\346\230\216.png" "b/static/Appointment/assets/img/summary2025/0.2_\345\211\215\347\253\257\346\267\273\345\212\240\350\257\264\346\230\216.png" new file mode 100644 index 000000000..8cacc0101 Binary files /dev/null and "b/static/Appointment/assets/img/summary2025/0.2_\345\211\215\347\253\257\346\267\273\345\212\240\350\257\264\346\230\216.png" differ diff --git "a/static/Appointment/assets/img/summary2025/1_\345\272\225\345\233\276.png" "b/static/Appointment/assets/img/summary2025/1_\345\272\225\345\233\276.png" new file mode 100644 index 000000000..ce372bc4d Binary files /dev/null and "b/static/Appointment/assets/img/summary2025/1_\345\272\225\345\233\276.png" differ diff --git "a/static/Appointment/assets/img/summary2025/2_\346\240\207\351\242\230.png" "b/static/Appointment/assets/img/summary2025/2_\346\240\207\351\242\230.png" new file mode 100644 index 000000000..832129f30 Binary files /dev/null and "b/static/Appointment/assets/img/summary2025/2_\346\240\207\351\242\230.png" differ diff --git "a/static/Appointment/assets/img/summary2025/3_\347\202\271\345\207\273\345\274\200\345\220\257\346\214\211\351\222\256.png" "b/static/Appointment/assets/img/summary2025/3_\347\202\271\345\207\273\345\274\200\345\220\257\346\214\211\351\222\256.png" new file mode 100644 index 000000000..8dcf4e777 Binary files /dev/null and "b/static/Appointment/assets/img/summary2025/3_\347\202\271\345\207\273\345\274\200\345\220\257\346\214\211\351\222\256.png" differ diff --git "a/static/Appointment/assets/img/summary2025/4_YPPFlogo\351\200\217\346\230\216\347\211\210.png" "b/static/Appointment/assets/img/summary2025/4_YPPFlogo\351\200\217\346\230\216\347\211\210.png" new file mode 100644 index 000000000..33e44eb01 Binary files /dev/null and "b/static/Appointment/assets/img/summary2025/4_YPPFlogo\351\200\217\346\230\216\347\211\210.png" differ diff --git "a/static/Appointment/assets/img/summary2025/5_\347\233\270\347\211\207\345\205\203\347\264\240.png" "b/static/Appointment/assets/img/summary2025/5_\347\233\270\347\211\207\345\205\203\347\264\240.png" new file mode 100644 index 000000000..552b83607 Binary files /dev/null and "b/static/Appointment/assets/img/summary2025/5_\347\233\270\347\211\207\345\205\203\347\264\240.png" differ diff --git a/static/Appointment/assets/img/summary2025/P1.png b/static/Appointment/assets/img/summary2025/P1.png new file mode 100644 index 000000000..0c65325f9 Binary files /dev/null and b/static/Appointment/assets/img/summary2025/P1.png differ diff --git a/static/Appointment/assets/img/summary2025/P10.png b/static/Appointment/assets/img/summary2025/P10.png new file mode 100644 index 000000000..edd883387 Binary files /dev/null and b/static/Appointment/assets/img/summary2025/P10.png differ diff --git a/static/Appointment/assets/img/summary2025/P10instruct.png b/static/Appointment/assets/img/summary2025/P10instruct.png new file mode 100644 index 000000000..76ebf494f Binary files /dev/null and b/static/Appointment/assets/img/summary2025/P10instruct.png differ diff --git a/static/Appointment/assets/img/summary2025/P11.png b/static/Appointment/assets/img/summary2025/P11.png new file mode 100644 index 000000000..da8c6425e Binary files /dev/null and b/static/Appointment/assets/img/summary2025/P11.png differ diff --git a/static/Appointment/assets/img/summary2025/P11instruct.png b/static/Appointment/assets/img/summary2025/P11instruct.png new file mode 100644 index 000000000..3a4000ded Binary files /dev/null and b/static/Appointment/assets/img/summary2025/P11instruct.png differ diff --git a/static/Appointment/assets/img/summary2025/P12.png b/static/Appointment/assets/img/summary2025/P12.png new file mode 100644 index 000000000..0c65325f9 Binary files /dev/null and b/static/Appointment/assets/img/summary2025/P12.png differ diff --git a/static/Appointment/assets/img/summary2025/P12instruct.png b/static/Appointment/assets/img/summary2025/P12instruct.png new file mode 100644 index 000000000..ce9b3b7bb Binary files /dev/null and b/static/Appointment/assets/img/summary2025/P12instruct.png differ diff --git a/static/Appointment/assets/img/summary2025/P13.png b/static/Appointment/assets/img/summary2025/P13.png new file mode 100644 index 000000000..9b064d3b4 Binary files /dev/null and b/static/Appointment/assets/img/summary2025/P13.png differ diff --git a/static/Appointment/assets/img/summary2025/P13instruct.png b/static/Appointment/assets/img/summary2025/P13instruct.png new file mode 100644 index 000000000..c5f8157b9 Binary files /dev/null and b/static/Appointment/assets/img/summary2025/P13instruct.png differ diff --git a/static/Appointment/assets/img/summary2025/P14.png b/static/Appointment/assets/img/summary2025/P14.png new file mode 100644 index 000000000..92f190363 Binary files /dev/null and b/static/Appointment/assets/img/summary2025/P14.png differ diff --git a/static/Appointment/assets/img/summary2025/P14instruct.png b/static/Appointment/assets/img/summary2025/P14instruct.png new file mode 100644 index 000000000..154a54369 Binary files /dev/null and b/static/Appointment/assets/img/summary2025/P14instruct.png differ diff --git a/static/Appointment/assets/img/summary2025/P15.png b/static/Appointment/assets/img/summary2025/P15.png new file mode 100644 index 000000000..a4cd426cd Binary files /dev/null and b/static/Appointment/assets/img/summary2025/P15.png differ diff --git a/static/Appointment/assets/img/summary2025/P15instruct.png b/static/Appointment/assets/img/summary2025/P15instruct.png new file mode 100644 index 000000000..6c680a81f Binary files /dev/null and b/static/Appointment/assets/img/summary2025/P15instruct.png differ diff --git a/static/Appointment/assets/img/summary2025/P16.png b/static/Appointment/assets/img/summary2025/P16.png new file mode 100644 index 000000000..a7f394dd8 Binary files /dev/null and b/static/Appointment/assets/img/summary2025/P16.png differ diff --git a/static/Appointment/assets/img/summary2025/P16instruct.png b/static/Appointment/assets/img/summary2025/P16instruct.png new file mode 100644 index 000000000..008139ce6 Binary files /dev/null and b/static/Appointment/assets/img/summary2025/P16instruct.png differ diff --git a/static/Appointment/assets/img/summary2025/P17.png b/static/Appointment/assets/img/summary2025/P17.png new file mode 100644 index 000000000..b20ffcc35 Binary files /dev/null and b/static/Appointment/assets/img/summary2025/P17.png differ diff --git a/static/Appointment/assets/img/summary2025/P17instruct.png b/static/Appointment/assets/img/summary2025/P17instruct.png new file mode 100644 index 000000000..78aaa0664 Binary files /dev/null and b/static/Appointment/assets/img/summary2025/P17instruct.png differ diff --git a/static/Appointment/assets/img/summary2025/P1instruct.png b/static/Appointment/assets/img/summary2025/P1instruct.png new file mode 100644 index 000000000..db5e82046 Binary files /dev/null and b/static/Appointment/assets/img/summary2025/P1instruct.png differ diff --git a/static/Appointment/assets/img/summary2025/P2.png b/static/Appointment/assets/img/summary2025/P2.png new file mode 100644 index 000000000..92f190363 Binary files /dev/null and b/static/Appointment/assets/img/summary2025/P2.png differ diff --git a/static/Appointment/assets/img/summary2025/P2instruct.png b/static/Appointment/assets/img/summary2025/P2instruct.png new file mode 100644 index 000000000..67288e4ec Binary files /dev/null and b/static/Appointment/assets/img/summary2025/P2instruct.png differ diff --git a/static/Appointment/assets/img/summary2025/P3.png b/static/Appointment/assets/img/summary2025/P3.png new file mode 100644 index 000000000..a7f394dd8 Binary files /dev/null and b/static/Appointment/assets/img/summary2025/P3.png differ diff --git a/static/Appointment/assets/img/summary2025/P3instruct.png b/static/Appointment/assets/img/summary2025/P3instruct.png new file mode 100644 index 000000000..985325eac Binary files /dev/null and b/static/Appointment/assets/img/summary2025/P3instruct.png differ diff --git a/static/Appointment/assets/img/summary2025/P4.png b/static/Appointment/assets/img/summary2025/P4.png new file mode 100644 index 000000000..158787c25 Binary files /dev/null and b/static/Appointment/assets/img/summary2025/P4.png differ diff --git a/static/Appointment/assets/img/summary2025/P4instruct.png b/static/Appointment/assets/img/summary2025/P4instruct.png new file mode 100644 index 000000000..de092737d Binary files /dev/null and b/static/Appointment/assets/img/summary2025/P4instruct.png differ diff --git a/static/Appointment/assets/img/summary2025/P5.png b/static/Appointment/assets/img/summary2025/P5.png new file mode 100644 index 000000000..5fb177229 Binary files /dev/null and b/static/Appointment/assets/img/summary2025/P5.png differ diff --git a/static/Appointment/assets/img/summary2025/P5instruct.png b/static/Appointment/assets/img/summary2025/P5instruct.png new file mode 100644 index 000000000..959145e2c Binary files /dev/null and b/static/Appointment/assets/img/summary2025/P5instruct.png differ diff --git a/static/Appointment/assets/img/summary2025/P6.png b/static/Appointment/assets/img/summary2025/P6.png new file mode 100644 index 000000000..0a900aa14 Binary files /dev/null and b/static/Appointment/assets/img/summary2025/P6.png differ diff --git a/static/Appointment/assets/img/summary2025/P6instruct.png b/static/Appointment/assets/img/summary2025/P6instruct.png new file mode 100644 index 000000000..e1d359aa3 Binary files /dev/null and b/static/Appointment/assets/img/summary2025/P6instruct.png differ diff --git a/static/Appointment/assets/img/summary2025/P7.png b/static/Appointment/assets/img/summary2025/P7.png new file mode 100644 index 000000000..d38951d3a Binary files /dev/null and b/static/Appointment/assets/img/summary2025/P7.png differ diff --git a/static/Appointment/assets/img/summary2025/P7instruct.png b/static/Appointment/assets/img/summary2025/P7instruct.png new file mode 100644 index 000000000..f3cc4468f Binary files /dev/null and b/static/Appointment/assets/img/summary2025/P7instruct.png differ diff --git a/static/Appointment/assets/img/summary2025/P8.png b/static/Appointment/assets/img/summary2025/P8.png new file mode 100644 index 000000000..fb1760871 Binary files /dev/null and b/static/Appointment/assets/img/summary2025/P8.png differ diff --git a/static/Appointment/assets/img/summary2025/P8instruct.png b/static/Appointment/assets/img/summary2025/P8instruct.png new file mode 100644 index 000000000..dff39691d Binary files /dev/null and b/static/Appointment/assets/img/summary2025/P8instruct.png differ diff --git a/static/Appointment/assets/img/summary2025/P9.png b/static/Appointment/assets/img/summary2025/P9.png new file mode 100644 index 000000000..bf2790cf9 Binary files /dev/null and b/static/Appointment/assets/img/summary2025/P9.png differ diff --git a/static/Appointment/assets/img/summary2025/P9instruct.png b/static/Appointment/assets/img/summary2025/P9instruct.png new file mode 100644 index 000000000..d03d6e926 Binary files /dev/null and b/static/Appointment/assets/img/summary2025/P9instruct.png differ diff --git a/static/Appointment/assets/img/summary2025/instruct.txt b/static/Appointment/assets/img/summary2025/instruct.txt new file mode 100644 index 000000000..e69de29bb diff --git a/static/Appointment/assets/js/summary2025.js b/static/Appointment/assets/js/summary2025.js new file mode 100644 index 000000000..2d7c85e1e --- /dev/null +++ b/static/Appointment/assets/js/summary2025.js @@ -0,0 +1,116 @@ +// 智慧书院 2025 年度总结 JavaScript +console.log('=== Summary2025.js 已加载 v20260205 ==='); + +$(document).ready(function () { + console.log('jQuery ready, 初始化中...'); + // 检查复选框状态 + let isAgreed = false; + + // 初始化 fullPage.js + $('#app').fullpage({ + scrollingSpeed: 700, + autoScrolling: true, + fitToSection: true, + navigation: true, + navigationPosition: 'right', + showActiveTooltip: false, + slidesNavigation: false, + controlArrows: false, + anchors: ['splash', 'home', 'page2', 'page3', 'page4', 'page5', 'page6', 'page7', 'page8', 'page9', 'page10', 'page11', 'page12', 'page13', 'page14', 'page15', 'page16', 'page17'], + afterLoad: function (anchorLink, index) { + // 页面加载后的动画逻辑 + console.log('Loaded section:', 'anchor:', anchorLink, 'index:', index); + + // 为当前页面的所有内容框添加淡入动画 + $('.section.active').find('.text-box, .p1-text-box, .p2-text-box, .p3-text-box, .p4-text-box, .p5-text-box, .p6-text-box, .p7-text-box, .p8-text-box, .p9-text-box, .p10-text-box, .p11-text-box, .p12-text-box, .home-title-container').addClass('animate-in'); + }, + onLeave: function (index, nextIndex, direction) { + console.log('onLeave 触发:', 'from index', index, 'to', nextIndex, 'direction:', direction, 'isAgreed:', isAgreed); + // 如果在启动页(index=1)且未同意协议,禁止离开 + if (index === 1 && !isAgreed) { + console.log('❌ 阻止离开启动页 - 未同意协议'); + return false; + } + + // 离开页面时移除动画类,准备下次进入 + $('.section').eq(index - 1).find('.text-box, .p1-text-box, .p2-text-box, .p3-text-box, .p4-text-box, .p5-text-box, .p6-text-box, .p7-text-box, .p8-text-box, .p9-text-box, .p10-text-box, .p11-text-box, .p12-text-box, .home-title-container').removeClass('animate-in'); + } + }); + + // 初始化音乐播放 + const audio = document.querySelector('audio'); + + // 检查复选框状态,控制按钮是否可用 + const splashButton = document.getElementById('splash-start-btn'); + const agreeCheckbox = document.getElementById('agree-rule'); + const urlParams = new URLSearchParams(window.location.search); + const hasAccepted = urlParams.get('accept') === 'true'; + + if (hasAccepted && agreeCheckbox) { + agreeCheckbox.checked = true; + if (splashButton) { + splashButton.classList.add('active'); + // 自动点击开启旅程 + setTimeout(() => { + splashButton.click(); + }, 500); + } + isAgreed = true; + } + + // 复选框状态改变时更新按钮样式 + agreeCheckbox.addEventListener('change', function () { + if (this.checked) { + splashButton.classList.add('active'); + isAgreed = true; + } else { + splashButton.classList.remove('active'); + isAgreed = false; + } + console.log('复选框状态改变:', 'checked:', this.checked, 'isAgreed:', isAgreed); + }); + + // 点击"开启旅程"按钮 + splashButton.addEventListener('click', function () { + if (agreeCheckbox.checked) { + isAgreed = true; + + if (!hasAccepted) { + const nextParams = new URLSearchParams(window.location.search); + nextParams.set("accept", "true"); + nextParams.delete("cancel"); + const newUrl = window.location.pathname + '?' + nextParams.toString(); + window.history.replaceState({}, '', newUrl); + } + + // 播放音乐 + audio.play(); + document.querySelector('#playing').style.display = 'block'; + document.querySelector('#paused').style.display = 'none'; + + // 跳转到下一页 + $.fn.fullpage.moveSectionDown(); + } + }); + + // 显示协议 + const ruleLink = document.getElementById('rule-link'); + const ruleDiv = document.getElementById('rule'); + const ruleButton = document.getElementById('rule-button'); + + ruleLink.addEventListener('click', function (e) { + e.preventDefault(); + ruleDiv.classList.add('show'); + }); + + ruleButton.addEventListener('click', function () { + ruleDiv.classList.remove('show'); + }); + + // 点击协议外部关闭 + ruleDiv.addEventListener('click', function (e) { + if (e.target === ruleDiv) { + ruleDiv.classList.remove('show'); + } + }); +}); diff --git a/static/Appointment/assets/summary_data/summary2025/.gitignore b/static/Appointment/assets/summary_data/summary2025/.gitignore new file mode 100644 index 000000000..fbc2a9d86 --- /dev/null +++ b/static/Appointment/assets/summary_data/summary2025/.gitignore @@ -0,0 +1,3 @@ +rank2025.json +summary_overall_2025.json +summary2025.json \ No newline at end of file diff --git a/static/Appointment/assets/summary_data/summary2025/rank-template.json b/static/Appointment/assets/summary_data/summary2025/rank-template.json new file mode 100644 index 000000000..bd7dffdee --- /dev/null +++ b/static/Appointment/assets/summary_data/summary2025/rank-template.json @@ -0,0 +1,15 @@ +{ + "2300000000": { + "underground_usage_percentile": 50.0, + "same_study_room_top_count": 10, + "same_most_hours_course_count": 5, + "personal_most_frequent_co_appoint": { + "co_name": "张三", + "count": 25 + }, + "organization_most_frequent_co_appoint": { + "co_name": "李四", + "count": 15 + } + } +} diff --git a/static/Appointment/assets/summary_data/summary2025/rank-white-template.json b/static/Appointment/assets/summary_data/summary2025/rank-white-template.json new file mode 100644 index 000000000..83a1f6af7 --- /dev/null +++ b/static/Appointment/assets/summary_data/summary2025/rank-white-template.json @@ -0,0 +1,7 @@ +{ + "透明人": { + "underground_usage_percentile": 0.0, + "same_study_room_top_count": 0, + "same_most_hours_course_count": 0 + } +} diff --git a/static/Appointment/assets/summary_data/summary2025/template.json b/static/Appointment/assets/summary_data/summary2025/template.json new file mode 100644 index 000000000..b4b544b98 --- /dev/null +++ b/static/Appointment/assets/summary_data/summary2025/template.json @@ -0,0 +1,99 @@ +{ + "2300000000": { + "date_joined": "2020-09-01", + "days": 1593, + "underground_usage_days": 100, + "first_underground_record": { + "date": "2025年01月20日", + "room": "B114", + "usage": "自习" + }, + "last_underground_record": { + "date": "2025年12月31日", + "room": "B104", + "usage": "自习" + }, + "longest_underground_usage": { + "longest_continuous_days": 30, + "longest_continuous_start_date": "2025-05-01", + "longest_continuous_end_date": "2025-05-30" + }, + "study_room_usage": { + "study_room_num": 150, + "study_room_top": "B114", + "study_room_top_num": 50, + "last_year_study_room_top": "B104", + "last_year_study_room_top_num": 45, + "is_same_as_last_year": false + }, + "talk_and_func_room_usage": { + "talk_room_num": 20, + "talk_room_hour": 50.0, + "talk_room_average_participant_num": 3.5, + "func_room_num": 15, + "func_room_hour": 30.0, + "talk_and_func_room_longest_record": { + "date": "2025年06月15日", + "room": "B104", + "hour": 3.0, + "usage": "小组讨论" + } + }, + "appoint_habit": { + "average_diff": 24.5, + "max_diff": 168.0, + "max_diff_record": { + "date": "2025年03月10日", + "room": "B107A", + "usage": "例会" + }, + "room_num_top": "B104", + "room_num_top_num": 30, + "day_appoint_num": 50, + "temporary_appoint_num": 10, + "day_appoint_min_diff": 0.5, + "day_appoint_min_diff_record": { + "date": "2025年04月20日", + "room": "B107B", + "usage": "临时会议" + } + }, + "login_days": 200, + "org_usage": { + "org_num": 2, + "org_name_list": [ + "元培学院", + "内联权益部" + ], + "act_num": 15, + "act_hour": 45.0, + "org_top": "元培学院", + "org_top_num": 10, + "earliest_act_record": { + "date": "2025年02月15日", + "name": "春季开学活动", + "time": "09:00" + }, + "latest_act_record": { + "date": "2025年12月20日", + "name": "新年晚会", + "time": "19:00" + }, + "window_top": 18, + "window_top_num": 5 + }, + "course_usage": { + "course_num": 5, + "valid_hours": 32.0, + "course_type_str": "智 体 美", + "most_hours_course": "健身类课程", + "most_hours": 16.0, + "highest_ratio_course": { + "name": "热门课程", + "total_participants": 50 + } + }, + "yqpoint_income": 100, + "name": "虚拟人" + } +} diff --git a/static/Appointment/assets/summary_data/summary2025/white-template.json b/static/Appointment/assets/summary_data/summary2025/white-template.json new file mode 100644 index 000000000..29eca2682 --- /dev/null +++ b/static/Appointment/assets/summary_data/summary2025/white-template.json @@ -0,0 +1,64 @@ +{ + "2500000000": { + "date_joined": "2020-09-01", + "days": 0, + "underground_usage_days": 0, + "first_underground_record": null, + "last_underground_record": null, + "longest_underground_usage": { + "longest_continuous_days": 0, + "longest_continuous_start_date": null, + "longest_continuous_end_date": null + }, + "study_room_usage": { + "study_room_num": 0, + "study_room_top": null, + "study_room_top_num": 0, + "last_year_study_room_top": null, + "last_year_study_room_top_num": 0, + "is_same_as_last_year": true + }, + "talk_and_func_room_usage": { + "talk_room_num": 0, + "talk_room_hour": 0.0, + "talk_room_average_participant_num": 0, + "func_room_num": 0, + "func_room_hour": 0.0, + "talk_and_func_room_longest_record": null + }, + "appoint_habit": { + "average_diff": null, + "max_diff": null, + "max_diff_record": null, + "room_num_top": null, + "room_num_top_num": null, + "day_appoint_num": 0, + "temporary_appoint_num": 0, + "day_appoint_min_diff": null, + "day_appoint_min_diff_record": null + }, + "login_days": 0, + "org_usage": { + "org_num": 0, + "org_name_list": [], + "act_num": 0, + "act_hour": 0.0, + "org_top": null, + "org_top_num": 0, + "earliest_act_record": null, + "latest_act_record": null, + "window_top": null, + "window_top_num": 0 + }, + "course_usage": { + "course_num": 0, + "valid_hours": 0, + "course_type_str": "", + "most_hours_course": null, + "most_hours": 0, + "highest_ratio_course": null + }, + "yqpoint_income": 0, + "name": "" + } +} diff --git a/templates/Appointment/summary2025.html b/templates/Appointment/summary2025.html new file mode 100644 index 000000000..2391ad8fd --- /dev/null +++ b/templates/Appointment/summary2025.html @@ -0,0 +1,597 @@ +{% load static %} + + + + + 时光流淌 + + + + + + + + + + + + + + + + + + +
+ + +
+
+ +
+ 启动页背景 +
+ + + 相片元素 + + +
+ 时光流淌 2025 +
+ {{ home_Sname }} + 亲启 +
+
+ + +
+
+

智慧书院2025年度回忆授权协议

+

+ 感谢您阅读《智慧书院2025年度回忆授权协议》!在正式使用智慧书院2025年度回忆功能之前,您应仔细阅读并充分理解本协议中的全部内容,并通过本页面点击确认的方式同意使用该功能,如您不同意本协议中的任何条款,请勿点击确认授权。您使用智慧书院2025年度回忆功能的行为将被视为已经仔细阅读、充分理解并毫无保留地接受本协议所有条款。 +

+
    +
  1. + 为了生成您的2025年度回忆,您同意授权智慧校园项目组查询您的姓名、年级等个人基本信息以及在2025自然年度(2025.1.19-2026.1.19)的智慧书院使用数据(包括系统登录记录、地下室使用记录、加入小组记录、书院课程参与记录、活动参与记录、元气值使用记录、学术地图使用记录等)并进行汇总统计分析,以用于年度回忆页面向您进行个性化专属展示。 +
  2. +
  3. 您的2025年度回忆为根据算法自动生成,可能与实际情况存在偏差,敬请理解。
  4. +
  5. 请您确认,您的2025年度回忆包含您的个人信息,您有权自行处理您的个人信息,包括但不限于使用、保存及对外共享给他人等行为,您将承担由您的操作或行为导致的个人信息安全风险。
  6. +
  7. 除以上声明的目的外,智慧校园项目组不会对本次查询的信息以及产生的分析结果作其他任何处理。
  8. +
  9. 如您对本次2025年度回忆有任何疑问,您可通过YPPF反馈中心联系智慧校园项目组。
  10. +
+ +
+
+
+ +
+ + +
+ 背景图 +
+
+

们该如何理解时间?

+

有人说,光阴像日夜不息的流水

+

有人说,四季是流转轮回的风车

+

在刹那与永恒的交替里

+

自习室的{{ study_room_num }}次推门而入

+

研讨室里{{ talk_room_num }}回思维碰撞

+

功能房中{{ func_room_num }}场热情飞扬

+

都是你我在岁月长河中相伴而行的注脚

+

2025,我们又一次共同走过

+
+
+
+ + +
+ 背景图 +
+
+

你好,{{ home_Sname }}

+

我是35楼地下室

+

欢迎翻开这一本

+

我和你的2025时光相册!

+
+
+
+ + +
+ 背景图 +
+ {% if not record_is_zero %} +
+

这一年里

+

你有 {{ underground_usage_days }} 天走进地下室

+

超过了 {{ underground_usage_percentile }} %的同学

+

{{ underground_usage_percentile_name }}

+ {% if first_underground_record %} +

这段旅程最早开始于

+

{{ first_underground_record.date }}

+

那一天

+ {% if first_room_study %} +

你在 {{ first_underground_record.room }}自习室

+

开启了新一段思想的远征

+ {% else %} +

你在 {{ first_underground_record.room }}

+

进行了"{{ first_underground_record.usage }}"

+ {% endif %} + {% endif %} +
+ {% else %} +
+

这一年里

+

你还没有来地下室看过我

+

新的一年,期待着和你遇见!

+
+ {% endif %} +
+
+ + + {% if not record_is_zero and longest_underground_usage.longest_continuous_days > 0 %} +
+ 背景图 +
+
+

这一年,我和你暂别于

+

{{ last_underground_record.date }}

+

那一天

+ {% if last_room_study %} +

你在 {{ last_underground_record.room }}自习室

+

写下与我的告别

+ {% else %} +

{{ last_underground_record.room }}

+

+

"{{ last_underground_record.usage }}" 的见证

+ {% endif %} +

你最长连续 {{ longest_underground_usage.longest_continuous_days }} 天来到这里

+

{{ longest_underground_usage.longest_continuous_start_date }}

+

+

{{ longest_underground_usage.longest_continuous_end_date }}

+

一定是一段很难忘的时光吧!

+
+
+
+ {% endif %} + + + {% if not record_is_zero %} +
+ 背景图 +
+
+

这一年里

+

你曾经 {{ study_room_usage.study_room_num }} 次走进自习室

+

那些沉思与顿悟的瞬间

+

是专属于你我的回忆

+ {% if study_room_usage.study_room_num > 0 and study_room_usage.study_room_top %} +

{{ study_room_usage.study_room_top }} 自习室

+

是你最常走进的自习室

+

你曾在那里度过 {{ study_room_usage.study_room_top_num }} 天的精神之旅

+ {% endif %} + {% if same_study_room_top_count %} +

{{ same_study_room_top_count }} 人和你一样

+

格外喜欢这里

+

在一次次独自修习里 他们恰好在场

+ {% endif %} +
+
+
+ {% endif %} + + + {% if talk_and_func_room_usage.talk_room_num > 0 or talk_and_func_room_usage.func_room_num > 0 %} +
+ 背景图 +
+
+ {% if talk_and_func_room_usage.talk_room_num > 0 %} +

这一年里

+

你预约了 {{ talk_and_func_room_usage.talk_room_num }} 次研讨室

+

与伙伴们共同度过 {{ talk_and_func_room_usage.talk_room_hour }} 小时

+ {% if talk_and_func_room_usage.talk_room_average_participant_num %} +

平均每次有 {{ talk_and_func_room_usage.talk_room_average_participant_num }} 人参与

+ {% endif %} + {% endif %} + {% if talk_and_func_room_usage.func_room_num > 0 %} +

这一年里

+

你预约了 {{ talk_and_func_room_usage.func_room_num }} 次功能室

+

生命中 {{ talk_and_func_room_usage.func_room_hour }} 小时的光阴

+

有音乐、舞蹈、汗水和欢笑的点缀

+ {% endif %} + {% if talk_and_func_room_usage.talk_and_func_room_longest_record %} +

还记得 {{ talk_and_func_room_usage.talk_and_func_room_longest_record.date }} 吗?

+ {% if talk_and_func_room_usage.talk_and_func_room_longest_record.participant_num == 1 %} +

那一天,你在 {{ talk_and_func_room_usage.talk_and_func_room_longest_record.room }}

+

度过了 {{ talk_and_func_room_usage.talk_and_func_room_longest_record.hour }} 小时

+

希望这段关于

+

" {{ talk_and_func_room_usage.talk_and_func_room_longest_record.usage }} " 的记忆

+

能给你带来滋养

+ {% else %} +

那一天

+

你与伙伴们在 {{ talk_and_func_room_usage.talk_and_func_room_longest_record.room }}

+

一同度过了 {{ talk_and_func_room_usage.talk_and_func_room_longest_record.hour }} 小时的时光

+

关于"{{ talk_and_func_room_usage.talk_and_func_room_longest_record.usage }}"的故事

+

是否还历历在目?

+ {% endif %} + {% endif %} +
+
+
+ {% endif %} + + + {% if appoint_habit.max_diff or appoint_habit.room_num_top %} +
+ 背景图 +
+
+ {% if appoint_habit.average_diff or appoint_habit.max_diff %} +

[ 最期待 ]

+ {% if appoint_habit.average_diff %} +

你习惯提前 {{ appoint_habit.average_diff }} {{ average_diff_time }}

+

为活动预约房间

+ {% endif %} + {% if appoint_habit.max_diff and appoint_habit.max_diff_record %} +

最早提前了 {{ appoint_habit.max_diff }} {{ max_diff_time }}

+

{{ appoint_habit.max_diff_record.usage }}

+

预约了 {{ appoint_habit.max_diff_record.room }}

+

稳稳的 很安心

+ {% endif %} + {% endif %} + {% if appoint_habit.room_num_top %} +

[ 最喜欢 ]

+

这一年里,你预约次数

+

最多的房间是

+

{{ appoint_habit.room_num_top }}

+

共预约了 {{ appoint_habit.room_num_top_num }}

+

记下了 你最喜欢这里

+ {% endif %} +
+
+
+ {% endif %} + + + {% if appoint_habit.temporary_appoint_num or appoint_habit.day_appoint_min_diff_record %} +
+ 背景图 +
+
+

[ 最极限 ]

+ {% if appoint_habit.temporary_appoint_num %} +

你曾经 {{ appoint_habit.temporary_appoint_num }}

+

在活动当天预约房间

+ {% endif %} + {% if appoint_habit.day_appoint_min_diff_record %} +

最极限的一次是在

+

{{ appoint_habit.day_appoint_min_diff_record.date }}

+

你在活动开始前

+

{{ appoint_habit.day_appoint_min_diff }} 小时预约了

+

{{ appoint_habit.day_appoint_min_diff_record.room }}

+

关键词为

+

{{ appoint_habit.day_appoint_min_diff_record.usage }}

+

主打一个随性和刺激!

+ {% endif %} +
+
+
+ {% endif %} + + + {% if personal_most_frequent_co_appoint.co_name or organization_most_frequent_co_appoint.co_name %} +
+ 背景图 +
+
+

[ 最投缘 ]

+ {% if personal_most_frequent_co_appoint.co_name %} +

你最常和 {{ personal_most_frequent_co_appoint.co_name }} 作伴

+

这一年

+

你们共同预约过 {{ personal_most_frequent_co_appoint.count }}

+

多幸运 有个我们

+ {% endif %} + + {% if organization_most_frequent_co_appoint.co_name %} +

在集体活动中

+

{{ organization_most_frequent_co_appoint.co_name }} 是最常和你同行的人

+

冥冥之中 你我的轨道也会偶有交叠

+ {% endif %} +
+
+
+ {% endif %} + + +
+ 背景图 +
+
+

流动不居的时间里

+

记忆的河道难免会迎来孤独的洪流

+

别担心

+

总有一些共同生活的点滴

+

我会为你珍藏——

+ +

{{ org_num }} 个小组

+

{{ act_num }} 次活动

+

以及 {{ course_num }} 门书院课程

+ +

是 “化孤独为共同” 的无言见证

+

你好,我是智慧书院!

+
+
+
+ + +
+ 背景图 +
+
+

{{ date_joined }}

+

这是我和你的第一次相遇

+

时间过得真快

+

我们已经走过 {{ days }} 天了

+ +

这一年里

+

你曾经 {{ login_days }} 次回到智慧书院系统

+

感谢你的一路相陪!

+
+
+
+ + + {% if org_usage.org_num > 0 or org_usage.act_num > 0 %} +
+ 背景图 +
+
+

你加入了 {{ org_usage.org_num }} 个小组

+ {% if org_reserved and org_name_list_str %} +

{{ org_name_list_str }} 的中坚力量

+ {% endif %} +

感谢有你

+

让 “一路相随” 有了鲜活的诠释

+ +

这一年

+

你参与了 {{ org_usage.act_num }} 场小组活动

+

累计活动时长 {{ org_usage.act_hour }} 小时

+ + {% if org_usage.org_top %} +

{{ org_usage.org_top }} 似乎是你的最爱

+

一年里

+

{{ org_usage.org_top_num }} 次参加了

+

这个小组的活动

+

有没有什么趣事值得分享?

+ {% endif %} +
+
+
+ {% endif %} + + + {% if org_usage.earliest_act_record or org_usage.latest_act_record %} +
+ 背景图 +
+
+ {% if org_usage.earliest_act_record %} +

{{ org_usage.earliest_act_record.date }}

+

你在 {{ org_usage.earliest_act_record.time }}

+

就参与了 {{ org_usage.earliest_act_record.name }}

+ {% endif %} + + {% if org_usage.latest_act_record %} +

{{ org_usage.latest_act_record.date }}

+

你在 {{ org_usage.latest_act_record.time }}

+

才从 {{ org_usage.latest_act_record.name }} 离开

+ {% endif %} + + {% if org_usage.window_top %} +

你最常在 {{ org_usage.window_top }}点 参加活动

+

这些时点 你还记得吗?

+ {% endif %} +
+
+
+ {% endif %} + + + {% if course_usage.course_num > 0 %} +
+ 背景图 +
+
+

截 至 2025 年底

+

你已经选修了 {{ course_usage.course_num }} 门书院课程

+

累计学时 {{ course_usage.valid_hours }}

+

在所有类别的课程中

+

你已经选修了

+

{{ course_usage.course_type_str }} 类课程

+
+
+
+ {% endif %} + + + {% if course_usage.most_hours_course or course_usage.highest_ratio_course %} +
+ 背景图 +
+
+ {% if course_usage.most_hours_course %} +

在这些课程里

+

你在 {{ course_usage.most_hours_course }} 中投入了很多心力

+

获得了 {{ course_usage.most_hours }} 个学时

+

还有 {{ same_most_hours_course_count }} 位同学和你一样

+

对这门书院课情有独钟

+

愿它装点你的世界

+ {% endif %} + + {% if course_usage.highest_ratio_course %} +

这一年里

+

你从 {{ course_usage.highest_ratio_course.total_participants }} 人中被幸运之神选中

+

成功选上 {{ course_usage.highest_ratio_course.name }}

+

新的一年 好运也要常相伴!

+ {% endif %} +
+
+
+ {% endif %} + + + {% if yqpoint_income > 0 %} +
+ 背景图 +
+
+
+

这一年里

+

你总共获得了 {{ yqpoint_income }} 点元气值

+

在共同生活的日子里

+

总有人为我们注入元气

+
+
+
+
+ {% endif %} + + +
+ 背景图 +
+
+

一眨眼

+

2025年的时光相册已经翻到了尾页

+

又到了说再见的时候了

+ +

喜怒哀乐 酸甜苦辣

+

我们的生活如何变化

+

似乎都逃不开这八个字

+ +

但岁月里定格的每一帧画面

+

都记录着独一无二的场景

+

35楼与你的每一次重逢

+

都是难以复制的瞬间

+ +

新的一年

+

期待着与你共同迎接新的生活百态!

+
+
+
+ +
+ + + + + + diff --git a/templates/Appointment/summary2025_entry.html b/templates/Appointment/summary2025_entry.html new file mode 100644 index 000000000..73f60ca31 --- /dev/null +++ b/templates/Appointment/summary2025_entry.html @@ -0,0 +1,239 @@ +{% load static %} + + + + + + 2025年度回忆 - 进入方式 + + + + 背景 +
+ + 相片元素 + YPPF logo + +
+
+ 时光流淌 2025 +
+ +
+ + +
+ +
+
+
+ + + +