基本完成课表生成

This commit is contained in:
2026-04-02 23:16:08 +08:00
parent 868abb2358
commit a003931207
3 changed files with 220 additions and 86 deletions

View File

@@ -5,7 +5,7 @@ import tempfile
import subprocess
from astrbot.api import logger
from schedule_utils import get_period_time, format_weeks
from .schedule_utils import get_period_time, format_weeks
class ScheduleRenderer:
@@ -15,11 +15,14 @@ class ScheduleRenderer:
self.config = config or {}
def generate_html(self, schedule: list[dict], title: str) -> str:
"""生成课程表 HTML"""
"""生成课程表 HTML(支持 rowspan 合并连续节次)"""
weekdays = ['周一', '周二', '周三', '周四', '周五', '周六', '周日']
weekday_short = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
grid = [[[] for _ in range(7)] for _ in range(12)]
# grid 存储 (course_key, html_content) 或 None被合并的单元格
# course_key 用于识别同一门课name|teacher|classroom|weeks_str
grid = [[None for _ in range(7)] for _ in range(12)]
for course in schedule:
name = course.get('name', '未知课程')
@@ -27,34 +30,43 @@ class ScheduleRenderer:
classroom = course.get('classroom') or ''
weeks = course.get('weeks', [])
day = course.get('day', 1)
classes = course.get('classes', [])
classes = sorted(course.get('classes', []))
day_idx = day - 1
if day_idx < 0 or day_idx > 6:
if day_idx < 0 or day_idx > 6 or not classes:
continue
# 生成课程 key 和 HTML 内容
weeks_str = format_weeks(weeks)
course_key = f"{name}|{teacher}|{classroom}|{weeks_str}"
course_text = f"<b>{name}</b>"
if teacher:
course_text += f"<br><small>{teacher}</small>"
if classroom:
course_text += f"<br><small>@{classroom}</small>"
if weeks:
course_text += f"<br><small>第{format_weeks(weeks)}周</small>"
for class_idx in classes:
if 1 <= class_idx <= 12:
grid[class_idx - 1][day_idx].append(course_text)
course_text += f"<br><small>第{weeks_str}周</small>"
# 填充到 grid只填第一个节次
first_period = min(classes)
if 1 <= first_period <= 12:
grid[first_period - 1][day_idx] = (course_key, course_text, len(classes))
# 判断是否隐藏空行空列
hide_void = self.config.get('hide_void_columns_rows', True)
# 计算有课的行和列
# 计算有课的行和列(考虑 rowspan
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)
# rowspan 占据的行也算有课
rowspan = grid[row_idx][col_idx][2]
for r in range(row_idx, min(row_idx + rowspan, 12)):
active_rows.add(r)
active_cols.add(col_idx)
# 如果没有课,默认显示全部
@@ -149,6 +161,8 @@ class ScheduleRenderer:
padding: 15px 10px;
border-radius: 12px;
text-align: center;
height: 190px;
box-sizing: border-box;
}}
td.time-col .period-num {{
@@ -168,21 +182,23 @@ class ScheduleRenderer:
border-radius: 12px;
padding: 10px;
vertical-align: top;
height: 110px;
height: 190px;
box-sizing: border-box;
}}
.course-item {{
background: #00838f;
border-radius: 10px;
padding: 6px;
margin: 2px 0;
margin: 0;
color: #e0f7fa;
line-height: 1.15;
font-size: 17px;
height: 180px;
height: 100%;
text-align: center;
display: table;
width: 100%;
box-sizing: border-box;
}}
.course-item-inner {{
@@ -244,10 +260,14 @@ class ScheduleRenderer:
return html
def _generate_table_body(self, grid: list, active_rows: list, active_cols: list) -> str:
"""生成表格主体 HTML"""
"""生成表格主体 HTML(支持 rowspan"""
rows = []
course_type_counter = 0
type_classes = ['type-a', 'type-b', 'type-c', 'type-d', 'type-e']
# 跟踪哪些单元格被 rowspan 占用了
# occupied[row][col] = True 表示该单元格被上方的 rowspan 占用
occupied = [[False for _ in range(7)] for _ in range(12)]
for period in active_rows:
start_time = get_period_time(period + 1)
@@ -259,17 +279,23 @@ class ScheduleRenderer:
</td>'''
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'<div class="course-item {type_class}"><div class="course-item-inner">{c}</div></div>'
# 检查该单元格是否被上方的 rowspan 占用
if occupied[period][day]:
continue
cell_data = grid[period][day]
if cell_data:
course_key, course_text, rowspan = cell_data
type_class = type_classes[course_type_counter % len(type_classes)]
course_type_counter += 1
# 标记下方被占用的单元格
for r in range(period, min(period + rowspan, 12)):
occupied[r][day] = True
row_html += f'<td class="day-cell" rowspan="{rowspan}"><div class="course-item {type_class}"><div class="course-item-inner">{course_text}</div></div></td>'
else:
cell_content = ''
row_html += f'<td class="day-cell">{cell_content}</td>'
row_html += f'<td class="day-cell"></td>'
row_html += '</tr>'
rows.append(row_html)
@@ -277,31 +303,44 @@ class ScheduleRenderer:
return '\n'.join(rows)
def generate_daily_html(self, schedule: list[dict], title: str, weekday_name: str = "") -> str:
"""生成单日课程表 HTML - 垂直卡片布局"""
"""生成单日课程表 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
rows_html = []
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', [])
classes = sorted(course.get('classes', []))
if not classes:
continue
# 获取第一节课的节次和时间
first_period = min(classes)
last_period = max(classes)
period_count = len(classes)
start_time = get_period_time(first_period)
end_time = get_period_time(max(classes))
end_time = get_period_time(last_period)
# 构建课程信息文本
# 节次显示
if first_period == last_period:
period_display = str(first_period)
else:
period_display = f"{first_period}-{last_period}"
# 计算行高基础120px每多一节+100px
row_height = 120 + (period_count - 1) * 100
type_class = type_classes[course_type_counter % len(type_classes)]
course_type_counter += 1
# 构建课程信息
info_lines = []
if teacher:
info_lines.append(f"<div class='info-line'>👤 {teacher}</div>")
@@ -310,26 +349,29 @@ class ScheduleRenderer:
if weeks:
info_lines.append(f"<div class='info-line'>📅 第{format_weeks(weeks)}周</div>")
type_class = type_classes[course_type_counter % len(type_classes)]
course_type_counter += 1
card_html = f'''
<div class="course-card {type_class}">
<div class="time-badge">
<div class="period-num">{first_period}</div>
<div class="time-range">{start_time}-{end_time}</div>
</div>
<div class="course-content">
<div class="course-name">{name}</div>
{''.join(info_lines)}
</div>
</div>
row_html = f'''
<tr style="height: {row_height}px;">
<td class="time-cell">
<div class="time-inner">
<div class="period-num">{period_display}</div>
<div class="time-range">{start_time}-{end_time}</div>
</div>
</td>
<td class="course-cell {type_class}">
<div class="course-inner">
<div class="course-name">{name}</div>
{''.join(info_lines)}
</div>
</td>
</tr>
'''
cards_html.append(card_html)
rows_html.append(row_html)
# 如果没有课程
if not cards_html:
cards_html.append('<div class="empty-message">今日暂无课程安排 🎉</div>')
if not rows_html:
table_body = '<tr><td colspan="2" class="empty-cell">今日暂无课程安排 🎉</td></tr>'
else:
table_body = '\n'.join(rows_html)
subtitle = f"{weekday_name} 课程安排" if weekday_name else "课程安排"
@@ -352,7 +394,7 @@ class ScheduleRenderer:
}}
.container {{
width: 800px;
width: 600px;
background: #1a1a2e;
border-radius: 20px;
padding: 40px;
@@ -375,51 +417,74 @@ class ScheduleRenderer:
margin-top: 8px;
}}
.cards-wrapper {{
display: flex;
flex-direction: column;
gap: 16px;
table {{
width: 100%;
border-collapse: separate;
border-spacing: 0 12px;
}}
.course-card {{
display: flex;
align-items: center;
background: #00838f;
border-radius: 16px;
padding: 20px;
min-height: 120px;
tr {{
height: 120px;
}}
.course-card.type-a {{ background: linear-gradient(135deg, #0277bd 0%, #01579b 100%); }}
.course-card.type-b {{ background: linear-gradient(135deg, #2e7d32 0%, #1b5e20 100%); }}
.course-card.type-c {{ background: linear-gradient(135deg, #7b1fa2 0%, #4a148c 100%); }}
.course-card.type-d {{ background: linear-gradient(135deg, #c6278e 0%, #880e4f 100%); }}
.course-card.type-e {{ background: linear-gradient(135deg, #ef6c00 0%, #e65100 100%); }}
.time-cell {{
width: 100px;
padding: 0;
vertical-align: middle;
}}
.time-badge {{
flex-shrink: 0;
width: 80px;
.time-inner {{
background: #00796b;
border-radius: 12px;
padding: 15px 10px;
text-align: center;
padding-right: 20px;
border-right: 2px solid rgba(255, 255, 255, 0.3);
margin-right: 20px;
height: 100%;
display: table;
width: 100%;
}}
.time-badge .period-num {{
.time-inner > div {{
display: table-cell;
vertical-align: middle;
}}
.time-cell .period-num {{
font-size: 32px;
font-weight: bold;
color: #ffffff;
line-height: 1;
display: block;
}}
.time-badge .time-range {{
font-size: 12px;
color: rgba(255, 255, 255, 0.8);
.time-cell .time-range {{
font-size: 13px;
color: #b2ebf2;
margin-top: 4px;
display: block;
}}
.course-content {{
flex: 1;
.course-cell {{
padding: 0 0 0 12px;
vertical-align: middle;
}}
.course-inner {{
background: #00838f;
border-radius: 12px;
padding: 20px;
height: 100%;
display: table;
width: 100%;
}}
.course-cell.type-a .course-inner {{ background: linear-gradient(135deg, #0277bd 0%, #01579b 100%); }}
.course-cell.type-b .course-inner {{ background: linear-gradient(135deg, #2e7d32 0%, #1b5e20 100%); }}
.course-cell.type-c .course-inner {{ background: linear-gradient(135deg, #7b1fa2 0%, #4a148c 100%); }}
.course-cell.type-d .course-inner {{ background: linear-gradient(135deg, #c6278e 0%, #880e4f 100%); }}
.course-cell.type-e .course-inner {{ background: linear-gradient(135deg, #ef6c00 0%, #e65100 100%); }}
.course-inner > div {{
display: table-cell;
vertical-align: middle;
}}
.course-name {{
@@ -435,7 +500,7 @@ class ScheduleRenderer:
margin-top: 4px;
}}
.empty-message {{
.empty-cell {{
text-align: center;
font-size: 20px;
color: #80deea;
@@ -458,9 +523,11 @@ class ScheduleRenderer:
<h1>{title}</h1>
<div class="subtitle">{subtitle}</div>
</div>
<div class="cards-wrapper">
{''.join(cards_html)}
</div>
<table>
<tbody>
{table_body}
</tbody>
</table>
<div class="footer">Powered by &lt;芒果酸&gt; suan.mangofanfan.cn</div>
</div>
</body>