diff --git a/_conf_schema.json b/_conf_schema.json
new file mode 100644
index 0000000..22ee34a
--- /dev/null
+++ b/_conf_schema.json
@@ -0,0 +1,7 @@
+{
+ "hide_void_columns_rows": {
+ "description": "隐藏课程表图片中的空行空列",
+ "type": "bool",
+ "default": true
+ }
+}
diff --git a/main.py b/main.py
index 2c44152..9ac3429 100644
--- a/main.py
+++ b/main.py
@@ -1,18 +1,22 @@
-import os
-import tempfile
-import subprocess
-import base64
+"""AstrBot 课表渲染插件主模块"""
+
from typing import Any, AsyncGenerator
from astrbot.api.event import filter, AstrMessageEvent, MessageEventResult
from astrbot.api.star import Context, Star
from astrbot.api import logger
+from astrbot.core import AstrBotConfig
from astrbot.core.message.message_event_result import MessageEventResult
+from schedule_utils import convert_tuple_schedule_to_dict
+from schedule_renderer import ScheduleRenderer
+
class SuanPlugin(Star):
- def __init__(self, context: Context):
+ def __init__(self, context: Context, config: AstrBotConfig):
super().__init__(context)
+ self.config = config
+ self.renderer = ScheduleRenderer(config)
async def initialize(self):
"""可选择实现异步的插件初始化方法,当实例化该插件类之后会自动调用该方法。"""
@@ -41,478 +45,67 @@ class SuanPlugin(Star):
try:
# 生成 HTML 内容
- html_content = self._generate_schedule_html(schedule_dict, title)
+ html_content = self.renderer.generate_html(schedule_dict, title)
- # 额外保存一份到指定路径,用于样式调试
- with open('/home/fanfan/test.html', 'w', encoding='utf-8') as f:
- f.write(html_content)
-
- # 创建临时文件
- with tempfile.NamedTemporaryFile(mode='w', suffix='.html', delete=False, encoding='utf-8') as f:
- f.write(html_content)
- html_path = f.name
-
- # 生成 PNG 输出路径
- png_path = html_path.replace('.html', '.png')
-
- # 使用 wkhtmltoimage 直接生成图片(比 wkhtmltopdf 再转换更方便)
- # 如果系统只有 wkhtmltopdf,可以先生成 PDF 再转换
- try:
- # 尝试使用 wkhtmltoimage(如果安装了)
- cmd = [
- 'wkhtmltoimage',
- '--width', '1680',
- '--quality', '90',
- '--enable-local-file-access',
- '--transparent',
- html_path,
- png_path
- ]
- result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
-
- if result.returncode != 0 or not os.path.exists(png_path):
- # 回退到 wkhtmltopdf + convert 方案
- png_path = await self._html_to_png_via_pdf(html_path)
-
- except FileNotFoundError:
- # wkhtmltoimage 不存在,使用 pdf 方案
- png_path = await self._html_to_png_via_pdf(html_path)
-
- # 读取图片并返回
- if os.path.exists(png_path):
- with open(png_path, 'rb') as f:
- image_data = f.read()
-
- # 清理临时文件
- self._cleanup_temp_files(html_path, png_path)
-
- # 返回图片消息
- from astrbot.api.message_components import Image
- yield event.chain_result([Image.fromBytes(image_data)])
- else:
- yield event.plain_result("生成课表图片失败,请检查 wkhtmltopdf 是否已安装")
+ # 渲染为 PNG
+ image_data = await self.renderer.render_to_png(
+ html_content,
+ debug_path='/home/fanfan/test.html'
+ )
+
+ # 返回图片消息
+ from astrbot.api.message_components import Image
+ yield event.chain_result([Image.fromBytes(image_data)])
except Exception as e:
logger.error(f"渲染课表失败: {e}")
yield event.plain_result(f"生成课表图片时出错: {str(e)}")
- async def _html_to_png_via_pdf(self, html_path: str) -> str:
- """通过 PDF 中转生成 PNG"""
- pdf_path = html_path.replace('.html', '.pdf')
- png_path = html_path.replace('.html', '.png')
-
- # HTML -> PDF
- cmd = [
- 'wkhtmltopdf',
- '--page-width', '297mm', # A4 横向
- '--page-height', '210mm',
- '--encoding', 'utf-8',
- '--enable-local-file-access',
- '--no-stop-slow-scripts',
- '--javascript-delay', '1000',
- html_path,
- pdf_path
- ]
- result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
-
- if result.returncode != 0:
- raise RuntimeError(f"wkhtmltopdf 失败: {result.stderr}")
-
- # PDF -> PNG (使用 ImageMagick 的 convert)
- cmd = [
- 'convert',
- '-density', '150',
- pdf_path,
- '-quality', '90',
- png_path
- ]
- result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
-
- if result.returncode != 0:
- # 尝试使用 pdftoppm
- cmd = [
- 'pdftoppm',
- '-png',
- '-r', '150',
- '-singlefile',
- pdf_path,
- png_path.replace('.png', '')
- ]
- result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
-
- if result.returncode != 0:
- raise RuntimeError(f"PDF 转 PNG 失败: {result.stderr}")
-
- # pdftoppm 生成的文件名可能不同
- generated_png = png_path.replace('.png', '-1.png')
- if os.path.exists(generated_png):
- os.rename(generated_png, png_path)
-
- # 清理 PDF 临时文件
- if os.path.exists(pdf_path):
- os.unlink(pdf_path)
-
- return png_path
-
- def _cleanup_temp_files(self, html_path: str, png_path: str):
- """清理临时文件"""
- for path in [html_path, png_path]:
- try:
- if os.path.exists(path):
- os.unlink(path)
- except Exception as e:
- logger.warning(f"清理临时文件失败 {path}: {e}")
-
- def _generate_schedule_html(self, schedule: list[dict], title: str) -> str:
- """生成课程表 HTML - 清新薄荷绿风格(静态图片版)"""
-
- weekdays = ['周一', '周二', '周三', '周四', '周五', '周六', '周日']
- weekday_short = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
- grid = [[[] for _ in range(7)] for _ in range(12)]
-
- for course in schedule:
- name = course.get('name', '未知课程')
- teacher = course.get('teacher') or ''
- classroom = course.get('classroom') or ''
- weeks = course.get('weeks', [])
- day = course.get('day', 1)
- classes = course.get('classes', [])
-
- day_idx = day - 1
- if day_idx < 0 or day_idx > 6:
- continue
-
- course_text = f"{name}"
- if teacher:
- course_text += f"
{teacher}"
- if classroom:
- course_text += f"
@{classroom}"
- if weeks:
- course_text += f"
第{self._format_weeks(weeks)}周"
-
- for class_idx in classes:
- if 1 <= class_idx <= 12:
- grid[class_idx - 1][day_idx].append(course_text)
-
- html = f'''
-
-
-
- {title}
-
+ try:
+ # 生成单日 HTML 内容
+ html_content = self.renderer.generate_daily_html(schedule_dict, title, weekday_name)
-
-
-
-
-
-
-
-
-
-
-
-
-
- | TIME |
- {''.join(f'{w}{weekday_short[i]} | ' for i, w in enumerate(weekdays))}
-
-
-
- {self._generate_table_body(grid)}
-
-
-
Powered by <芒果酸> suan.mangofanfan.cn
-
-
-'''
-
- return html
-
- def _generate_table_body(self, grid: list) -> str:
- """生成表格主体 HTML"""
- rows = []
- course_type_counter = 0
- type_classes = ['type-a', 'type-b', 'type-c', 'type-d', 'type-e']
-
- for period in range(12):
- start_time = self._get_period_time(period + 1)
-
- row_html = f'''
- |
- {period + 1}
- {start_time}
- | '''
-
- for day in range(7):
- courses = grid[period][day]
- if courses:
- cell_content = ''
- for c in courses:
- type_class = type_classes[course_type_counter % len(type_classes)]
- course_type_counter += 1
- cell_content += f'{c}
'
- else:
- cell_content = ''
-
- row_html += f'{cell_content} | '
-
- row_html += '
'
- rows.append(row_html)
-
- return '\n'.join(rows)
-
-
- def _get_period_time(self, period: int) -> str:
- """获取节次对应的时间(南邮标准时间)"""
- # 南邮标准作息时间,可根据需要调整
- time_map = {
- 1: '08:00',
- 2: '08:50',
- 3: '09:50',
- 4: '10:40',
- 5: '11:30',
- 6: '13:45',
- 7: '14:35',
- 8: '15:35',
- 9: '16:25',
- 10: '18:30',
- 11: '19:20',
- 12: '20:10'
- }
- return time_map.get(period, '')
-
- def _format_weeks(self, weeks: list[int]) -> str:
- """格式化周数显示,如 [1,2,3,5,6,7] -> "1-3,5-7" """
- if not weeks:
- return ''
-
- weeks = sorted(set(weeks))
- ranges = []
- start = end = weeks[0]
-
- for w in weeks[1:]:
- if w == end + 1:
- end = w
- else:
- if start == end:
- ranges.append(str(start))
- else:
- ranges.append(f"{start}-{end}")
- start = end = w
-
- if start == end:
- ranges.append(str(start))
- else:
- ranges.append(f"{start}-{end}")
-
- return ','.join(ranges)
+ # 渲染为 PNG(使用较小的宽度适合单列卡片)
+ image_data = await self.renderer.render_to_png(
+ html_content,
+ debug_path='/home/fanfan/test_daily.html',
+ width=880
+ )
+
+ # 返回图片消息
+ from astrbot.api.message_components import Image
+ yield event.chain_result([Image.fromBytes(image_data)])
+ except Exception as e:
+ logger.error(f"渲染单日课表失败: {e}")
+ yield event.plain_result(f"生成单日课表图片时出错: {str(e)}")
async def terminate(self):
"""可选择实现异步的插件销毁方法,当插件被卸载/停用时会调用。"""
-
-
-def convert_tuple_schedule_to_dict(schedule: list[tuple]) -> list[dict]:
- """将元组格式的课表转换为字典格式。
-
- Args:
- schedule: list[tuple],每个元组包含 (name, teacher, classroom, weeks, day, classes)
- 其中 weeks 可以是 list[int] 或 str(如 "1-17")
-
- Returns:
- list[dict]: 标准格式的课程数据
- """
- result = []
- for course in schedule:
- # 确保元组长度足够,不足的补 None
- padded = list(course) + [None] * (6 - len(course))
- name, teacher, classroom, weeks, day, classes = padded[:6]
-
- # 处理 weeks:支持 list 或 str(如 "1-17", "1,3,5-10")
- weeks_list = []
- if isinstance(weeks, list):
- weeks_list = weeks
- elif isinstance(weeks, str):
- weeks_list = parse_weeks_string(weeks)
-
- result.append({
- "name": name,
- "teacher": teacher,
- "classroom": classroom,
- "weeks": weeks_list,
- "day": day if isinstance(day, int) else 1,
- "classes": classes if isinstance(classes, list) else []
- })
-
- return result
-
-
-def parse_weeks_string(weeks_str: str) -> list[int]:
- """将周数字符串解析为整数列表。
-
- 支持格式:
- "1-17" -> [1,2,3,...,17]
- "1,3,5" -> [1,3,5]
- "1-5,7,9-11" -> [1,2,3,4,5,7,9,10,11]
-
- Args:
- weeks_str: 周数字符串
-
- Returns:
- list[int]: 周数列表
- """
- if not weeks_str or not isinstance(weeks_str, str):
- return []
-
- result = []
- parts = weeks_str.split(',')
-
- for part in parts:
- part = part.strip()
- if '-' in part:
- # 范围格式:1-17
- try:
- start, end = part.split('-', 1)
- start = int(start.strip())
- end = int(end.strip())
- result.extend(range(start, end + 1))
- except ValueError:
- continue
- else:
- # 单个数字
- try:
- result.append(int(part))
- except ValueError:
- continue
-
- # 去重并排序
- return sorted(set(result))
-
diff --git a/schedule_renderer.py b/schedule_renderer.py
new file mode 100644
index 0000000..0235203
--- /dev/null
+++ b/schedule_renderer.py
@@ -0,0 +1,594 @@
+"""课表渲染模块 - 负责生成 HTML 和转换为图片"""
+
+import os
+import tempfile
+import subprocess
+from astrbot.api import logger
+
+from schedule_utils import get_period_time, format_weeks
+
+
+class ScheduleRenderer:
+ """课表渲染器"""
+
+ def __init__(self, config: dict = None):
+ self.config = config or {}
+
+ def generate_html(self, schedule: list[dict], title: str) -> str:
+ """生成课程表 HTML"""
+
+ weekdays = ['周一', '周二', '周三', '周四', '周五', '周六', '周日']
+ weekday_short = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
+ grid = [[[] for _ in range(7)] for _ in range(12)]
+
+ for course in schedule:
+ name = course.get('name', '未知课程')
+ teacher = course.get('teacher') or ''
+ classroom = course.get('classroom') or ''
+ weeks = course.get('weeks', [])
+ day = course.get('day', 1)
+ classes = course.get('classes', [])
+
+ day_idx = day - 1
+ if day_idx < 0 or day_idx > 6:
+ continue
+
+ course_text = f"{name}"
+ if teacher:
+ course_text += f"
{teacher}"
+ if classroom:
+ course_text += f"
@{classroom}"
+ if weeks:
+ course_text += f"
第{format_weeks(weeks)}周"
+
+ for class_idx in classes:
+ if 1 <= class_idx <= 12:
+ grid[class_idx - 1][day_idx].append(course_text)
+
+ # 判断是否隐藏空行空列
+ hide_void = self.config.get('hide_void_columns_rows', True)
+
+ # 计算有课的行和列
+ active_rows = set()
+ active_cols = set()
+ for row_idx in range(12):
+ for col_idx in range(7):
+ if grid[row_idx][col_idx]:
+ active_rows.add(row_idx)
+ active_cols.add(col_idx)
+
+ # 如果没有课,默认显示全部
+ if not active_rows:
+ active_rows = set(range(12))
+ if not active_cols:
+ active_cols = set(range(7))
+
+ # 转换为有序列表
+ active_rows = sorted(active_rows)
+ active_cols = sorted(active_cols)
+
+ # 如果不隐藏,显示全部
+ if not hide_void:
+ active_rows = list(range(12))
+ active_cols = list(range(7))
+
+ # 计算列宽:时间列90px,剩余宽度均分给各天
+ day_col_width = 14.28 if not hide_void else (100.0 / len(active_cols))
+
+ html = f'''
+
+
+
+ {title}
+
+
+
+
+
+
+
+
+
+ {''.join('' for _ in active_cols)}
+
+
+
+ | TIME |
+ {''.join(f'{weekdays[i]}{weekday_short[i]} | ' for i in active_cols)}
+
+
+
+ {self._generate_table_body(grid, active_rows, active_cols)}
+
+
+
Powered by <芒果酸> suan.mangofanfan.cn
+
+
+'''
+
+ return html
+
+ def _generate_table_body(self, grid: list, active_rows: list, active_cols: list) -> str:
+ """生成表格主体 HTML"""
+ rows = []
+ course_type_counter = 0
+ type_classes = ['type-a', 'type-b', 'type-c', 'type-d', 'type-e']
+
+ for period in active_rows:
+ start_time = get_period_time(period + 1)
+
+ row_html = f'''
+ |
+ {period + 1}
+ {start_time}
+ | '''
+
+ for day in active_cols:
+ courses = grid[period][day]
+ if courses:
+ cell_content = ''
+ for c in courses:
+ type_class = type_classes[course_type_counter % len(type_classes)]
+ course_type_counter += 1
+ cell_content += f''
+ else:
+ cell_content = ''
+
+ row_html += f'{cell_content} | '
+
+ row_html += '
'
+ rows.append(row_html)
+
+ return '\n'.join(rows)
+
+ def generate_daily_html(self, schedule: list[dict], title: str, weekday_name: str = "") -> str:
+ """生成单日课程表 HTML - 垂直卡片布局"""
+
+ # 按节次排序课程
+ sorted_courses = sorted(schedule, key=lambda x: min(x.get('classes', [99])) if x.get('classes') else 99)
+
+ type_classes = ['type-a', 'type-b', 'type-c', 'type-d', 'type-e']
+ cards_html = []
+ course_type_counter = 0
+
+ for course in sorted_courses:
+ name = course.get('name', '未知课程')
+ teacher = course.get('teacher') or ''
+ classroom = course.get('classroom') or ''
+ weeks = course.get('weeks', [])
+ classes = course.get('classes', [])
+
+ if not classes:
+ continue
+
+ # 获取第一节课的节次和时间
+ first_period = min(classes)
+ start_time = get_period_time(first_period)
+ end_time = get_period_time(max(classes))
+
+ # 构建课程信息文本
+ info_lines = []
+ if teacher:
+ info_lines.append(f"👤 {teacher}
")
+ if classroom:
+ info_lines.append(f"📍 {classroom}
")
+ if weeks:
+ info_lines.append(f"📅 第{format_weeks(weeks)}周
")
+
+ type_class = type_classes[course_type_counter % len(type_classes)]
+ course_type_counter += 1
+
+ card_html = f'''
+
+
+
{first_period}
+
{start_time}-{end_time}
+
+
+
{name}
+ {''.join(info_lines)}
+
+
+ '''
+ cards_html.append(card_html)
+
+ # 如果没有课程
+ if not cards_html:
+ cards_html.append('今日暂无课程安排 🎉
')
+
+ subtitle = f"{weekday_name} 课程安排" if weekday_name else "课程安排"
+
+ html = f'''
+
+
+
+ {title}
+
+
+
+
+
+
+ {''.join(cards_html)}
+
+
+
+
+'''
+
+ return html
+
+ async def html_to_png_via_pdf(self, html_path: str) -> str:
+ """通过 PDF 中转生成 PNG"""
+ pdf_path = html_path.replace('.html', '.pdf')
+ png_path = html_path.replace('.html', '.png')
+
+ # HTML -> PDF
+ cmd = [
+ 'wkhtmltopdf',
+ '--page-width', '297mm', # A4 横向
+ '--page-height', '210mm',
+ '--encoding', 'utf-8',
+ '--enable-local-file-access',
+ '--no-stop-slow-scripts',
+ '--javascript-delay', '1000',
+ html_path,
+ pdf_path
+ ]
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
+
+ if result.returncode != 0:
+ raise RuntimeError(f"wkhtmltopdf 失败: {result.stderr}")
+
+ # PDF -> PNG (使用 ImageMagick 的 convert)
+ cmd = [
+ 'convert',
+ '-density', '150',
+ pdf_path,
+ '-quality', '90',
+ png_path
+ ]
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
+
+ if result.returncode != 0:
+ # 尝试使用 pdftoppm
+ cmd = [
+ 'pdftoppm',
+ '-png',
+ '-r', '150',
+ '-singlefile',
+ pdf_path,
+ png_path.replace('.png', '')
+ ]
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
+
+ if result.returncode != 0:
+ raise RuntimeError(f"PDF 转 PNG 失败: {result.stderr}")
+
+ # pdftoppm 生成的文件名可能不同
+ generated_png = png_path.replace('.png', '-1.png')
+ if os.path.exists(generated_png):
+ os.rename(generated_png, png_path)
+
+ # 清理 PDF 临时文件
+ if os.path.exists(pdf_path):
+ os.unlink(pdf_path)
+
+ return png_path
+
+ def cleanup_temp_files(self, html_path: str, png_path: str):
+ """清理临时文件"""
+ for path in [html_path, png_path]:
+ try:
+ if os.path.exists(path):
+ os.unlink(path)
+ except Exception as e:
+ logger.warning(f"清理临时文件失败 {path}: {e}")
+
+ async def render_to_png(self, html_content: str, debug_path: str = None, width: int = 1680) -> bytes:
+ """将 HTML 内容渲染为 PNG 图片字节
+
+ Args:
+ html_content: HTML 内容字符串
+ debug_path: 调试文件保存路径(可选)
+ width: 输出图片宽度,默认 1680
+
+ Returns:
+ PNG 图片字节数据
+ """
+
+ # 额外保存一份到指定路径,用于样式调试
+ if debug_path:
+ try:
+ with open(debug_path, 'w', encoding='utf-8') as f:
+ f.write(html_content)
+ except Exception as e:
+ logger.warning(f"保存调试文件失败: {e}")
+
+ # 创建临时文件
+ with tempfile.NamedTemporaryFile(mode='w', suffix='.html', delete=False, encoding='utf-8') as f:
+ f.write(html_content)
+ html_path = f.name
+
+ # 生成 PNG 输出路径
+ png_path = html_path.replace('.html', '.png')
+
+ # 使用 wkhtmltoimage 直接生成图片
+ try:
+ cmd = [
+ 'wkhtmltoimage',
+ '--width', str(width),
+ '--quality', '90',
+ '--enable-local-file-access',
+ '--transparent',
+ html_path,
+ png_path
+ ]
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
+
+ if result.returncode != 0 or not os.path.exists(png_path):
+ # 回退到 wkhtmltopdf + convert 方案
+ png_path = await self.html_to_png_via_pdf(html_path)
+
+ except FileNotFoundError:
+ # wkhtmltoimage 不存在,使用 pdf 方案
+ png_path = await self.html_to_png_via_pdf(html_path)
+
+ # 读取图片数据
+ with open(png_path, 'rb') as f:
+ image_data = f.read()
+
+ # 清理临时文件
+ self.cleanup_temp_files(html_path, png_path)
+
+ return image_data
diff --git a/schedule_utils.py b/schedule_utils.py
new file mode 100644
index 0000000..ccae31e
--- /dev/null
+++ b/schedule_utils.py
@@ -0,0 +1,124 @@
+"""课表数据处理工具函数"""
+
+
+def convert_tuple_schedule_to_dict(schedule: list[tuple]) -> list[dict]:
+ """将元组格式的课表转换为字典格式。
+
+ Args:
+ schedule: list[tuple],每个元组包含 (name, teacher, classroom, weeks, day, classes)
+ 其中 weeks 可以是 list[int] 或 str(如 "1-17")
+
+ Returns:
+ list[dict]: 标准格式的课程数据
+ """
+ result = []
+ for course in schedule:
+ # 确保元组长度足够,不足的补 None
+ padded = list(course) + [None] * (6 - len(course))
+ name, teacher, classroom, weeks, day, classes = padded[:6]
+
+ # 处理 weeks:支持 list 或 str(如 "1-17", "1,3,5-10")
+ weeks_list = []
+ if isinstance(weeks, list):
+ weeks_list = weeks
+ elif isinstance(weeks, str):
+ weeks_list = parse_weeks_string(weeks)
+
+ result.append({
+ "name": name,
+ "teacher": teacher,
+ "classroom": classroom,
+ "weeks": weeks_list,
+ "day": day if isinstance(day, int) else 1,
+ "classes": classes if isinstance(classes, list) else []
+ })
+
+ return result
+
+
+def parse_weeks_string(weeks_str: str) -> list[int]:
+ """将周数字符串解析为整数列表。
+
+ 支持格式:
+ "1-17" -> [1,2,3,...,17]
+ "1,3,5" -> [1,3,5]
+ "1-5,7,9-11" -> [1,2,3,4,5,7,9,10,11]
+
+ Args:
+ weeks_str: 周数字符串
+
+ Returns:
+ list[int]: 周数列表
+ """
+ if not weeks_str or not isinstance(weeks_str, str):
+ return []
+
+ result = []
+ parts = weeks_str.split(',')
+
+ for part in parts:
+ part = part.strip()
+ if '-' in part:
+ # 范围格式:1-17
+ try:
+ start, end = part.split('-', 1)
+ start = int(start.strip())
+ end = int(end.strip())
+ result.extend(range(start, end + 1))
+ except ValueError:
+ continue
+ else:
+ # 单个数字
+ try:
+ result.append(int(part))
+ except ValueError:
+ continue
+
+ # 去重并排序
+ return sorted(set(result))
+
+
+def format_weeks(weeks: list[int]) -> str:
+ """格式化周数显示,如 [1,2,3,5,6,7] -> "1-3,5-7" """
+ if not weeks:
+ return ''
+
+ weeks = sorted(set(weeks))
+ ranges = []
+ start = end = weeks[0]
+
+ for w in weeks[1:]:
+ if w == end + 1:
+ end = w
+ else:
+ if start == end:
+ ranges.append(str(start))
+ else:
+ ranges.append(f"{start}-{end}")
+ start = end = w
+
+ if start == end:
+ ranges.append(str(start))
+ else:
+ ranges.append(f"{start}-{end}")
+
+ return ','.join(ranges)
+
+
+def get_period_time(period: int) -> str:
+ """获取节次对应的时间(南邮标准时间)"""
+ time_map = {
+ 1: '08:00',
+ 2: '08:50',
+ 3: '09:50',
+ 4: '10:40',
+ 5: '11:30',
+ 6: '13:45',
+ 7: '14:35',
+ 8: '15:35',
+ 9: '16:25',
+ 10: '18:30',
+ 11: '19:20',
+ 12: '20:10'
+ }
+ return time_map.get(period, '')