Ethscriptions commited on
Commit
79debab
·
verified ·
1 Parent(s): 28f28d0

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +178 -178
app.py CHANGED
@@ -4,66 +4,90 @@ from datetime import datetime, timedelta
4
  import matplotlib.pyplot as plt
5
  import io
6
  import base64
 
 
7
  import math
8
- from matplotlib.backends.backend_pdf import PdfPages
9
 
10
- # --- Constants ---
11
  SPLIT_TIME = "17:30"
12
  BUSINESS_START = "09:30"
13
  BUSINESS_END = "01:30"
14
- BORDER_COLOR = 'grey' # Changed to grey for the new design
15
  DATE_COLOR = '#A9A9A9'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
 
17
  def process_schedule(file):
18
- """
19
- Processes the uploaded Excel file to extract, clean, and sort screening times.
20
- """
21
  try:
22
- # Read Excel, skipping header rows
23
  df = pd.read_excel(file, skiprows=8)
24
 
25
- # Extract required columns (G, H, J)
26
- df = df.iloc[:, [6, 7, 9]]
27
  df.columns = ['Hall', 'StartTime', 'EndTime']
28
 
29
- # Clean data: drop rows with missing values
30
  df = df.dropna(subset=['Hall', 'StartTime', 'EndTime'])
 
31
 
32
- # Format Hall to "# " format
33
- df['Hall'] = df['Hall'].str.extract(r'(\d+)').astype(str) + ' '
34
 
35
- # Convert time columns to datetime objects
36
  base_date = datetime.today().date()
37
- df['StartTime'] = pd.to_datetime(df['StartTime'])
38
- df['EndTime'] = pd.to_datetime(df['EndTime'])
39
 
40
- # --- Handle overnight screenings ---
41
- # If a show ends after midnight (e.g., 1:30 AM), it belongs to the previous day's schedule.
42
- # We handle this by adding a day to its datetime object.
43
- for idx, row in df.iterrows():
44
- if row['EndTime'].hour < 9: # Assuming any end time before 9 AM is part of the previous night
45
- df.at[idx, 'EndTime'] = row['EndTime'] + timedelta(days=1)
46
 
47
- # Create a comparable time column that correctly handles the business day logic
48
- df['time_for_comparison'] = df['EndTime']
 
 
 
 
49
 
50
- # Sort screenings by their end time
51
- df = df.sort_values('EndTime')
52
 
53
- # Split data into day and night shifts
54
- split_datetime = datetime.combine(base_date, datetime.strptime(SPLIT_TIME, "%H:%M").time())
55
- part1 = df[df['time_for_comparison'] <= split_datetime].copy()
56
- part2 = df[df['time_for_comparison'] > split_datetime].copy()
57
 
58
- # Format the time display string (e.g., "5:30")
59
- for part in [part1, part2]:
60
- part['EndTime'] = part['EndTime'].dt.strftime('%-I:%M')
 
 
61
 
62
- # Precisely read the date from cell C6
63
- date_df = pd.read_excel(file, skiprows=5, nrows=1, usecols=[2], header=None)
 
 
 
 
 
 
 
 
 
 
64
  date_cell = date_df.iloc[0, 0]
65
 
66
  try:
 
67
  if isinstance(date_cell, str):
68
  date_str = datetime.strptime(date_cell, '%Y-%m-%d').strftime('%Y-%m-%d')
69
  else:
@@ -71,153 +95,132 @@ def process_schedule(file):
71
  except:
72
  date_str = datetime.today().strftime('%Y-%m-%d')
73
 
74
- return part1[['Hall', 'EndTime']], part2[['Hall', 'EndTime']], date_str
75
 
76
  except Exception as e:
77
- st.error(f"Error processing file: {str(e)}")
78
  return None, None, None
79
 
80
- def _draw_grid_on_figure(fig, data, title, date_str):
81
- """
82
- Internal helper function to draw the new grid layout onto a Matplotlib figure.
83
- """
84
- plt.rcParams['font.family'] = 'sans-serif'
85
- plt.rcParams['font.sans-serif'] = ['Arial Unicode MS'] # Font that supports Chinese characters
86
-
87
- total_items = len(data)
88
- if total_items == 0:
89
- return
90
-
91
- num_cols = 3
92
- num_rows = math.ceil(total_items / num_cols)
93
-
94
- A5_WIDTH_IN = 5.83
95
- A5_HEIGHT_IN = 8.27
96
-
97
- # 1. Redesign layout based on precise grid calculations
98
- margin_y = (A5_HEIGHT_IN / num_rows) / 4
99
- margin_x = margin_y # Use symmetric margins for a cleaner look
100
-
101
- # Prevent margins from becoming too large on pages with few items
102
- if A5_WIDTH_IN < 2 * margin_x or A5_HEIGHT_IN < 2 * margin_y:
103
- margin_x = A5_WIDTH_IN / 10
104
- margin_y = A5_HEIGHT_IN / 10
105
-
106
- printable_width = A5_WIDTH_IN - 2 * margin_x
107
- printable_height = A5_HEIGHT_IN - 2 * margin_y
108
- cell_width = printable_width / num_cols
109
- cell_height = printable_height / num_rows
110
-
111
- # Prepare data: Sort into column-first (Z-style) order for layout
112
- data_values = data.values.tolist()
113
- while len(data_values) % num_cols != 0:
114
- data_values.append(['', '']) # Pad data for a full grid
115
- rows_per_col_layout = math.ceil(len(data_values) / num_cols)
116
- sorted_data = [['', '']] * len(data_values)
117
- for i, item in enumerate(data_values):
118
- if item[0] and item[1]:
119
- row_in_col = i % rows_per_col_layout
120
- col_idx = i // rows_per_col_layout
121
- new_index = row_in_col * num_cols + col_idx
122
- if new_index < len(sorted_data):
123
- sorted_data[new_index] = item
124
-
125
- # --- Draw each cell onto the figure ---
126
- item_counter = 0
127
- for idx, (hall, end_time) in enumerate(sorted_data):
128
- if not (hall and end_time):
129
- continue
130
-
131
- item_counter += 1
132
- row_grid = idx // num_cols
133
- col_grid = idx % num_cols
134
-
135
- # Calculate position for each cell's axes in Figure coordinates [left, bottom, width, height]
136
- ax_left_in = margin_x + col_grid * cell_width
137
- ax_bottom_in = margin_y + (num_rows - 1 - row_grid) * cell_height # Y-axis from bottom
138
- ax_pos = [
139
- ax_left_in / A5_WIDTH_IN,
140
- ax_bottom_in / A5_HEIGHT_IN,
141
- cell_width / A5_WIDTH_IN,
142
- cell_height / A5_HEIGHT_IN,
143
- ]
144
- ax = fig.add_axes(ax_pos)
145
-
146
- # 2. Change Cell Border to a gray, dotted line
147
- for spine in ax.spines.values():
148
- spine.set_visible(True)
149
- spine.set_linestyle(':') # Dotted line style
150
- spine.set_edgecolor(BORDER_COLOR)
151
- spine.set_linewidth(1)
152
-
153
- # 4. Add Cell Index Number
154
- ax.text(0.07, 0.93, str(item_counter),
155
- transform=ax.transAxes,
156
- fontsize=9,
157
- color='grey',
158
- ha='left',
159
- va='top')
160
-
161
- # 3. Adjust Cell Content
162
- display_text = f"{hall}{end_time}"
163
-
164
- # Dynamically estimate font size to fill 90% of the cell width
165
- # This is a heuristic that provides a good balance of size and spacing.
166
- font_scale_factor = 1.7
167
- estimated_fontsize = (cell_width * 72 / len(display_text)) * font_scale_factor * 0.9
168
- max_fontsize = cell_height * 72 * 0.6 # Cap font size to 60% of cell height
169
- final_fontsize = min(estimated_fontsize, max_fontsize)
170
-
171
- ax.text(0.5, 0.5, display_text,
172
- fontsize=final_fontsize,
173
- fontweight='bold',
174
- ha='center',
175
- va='center',
176
- transform=ax.transAxes)
177
-
178
- ax.set_xticks([])
179
- ax.set_yticks([])
180
-
181
- # Add date and title to the top margin of the figure
182
- title_fontsize = margin_y * 72 * 0.3 # Scale title font size with margin
183
- fig.text(margin_x / A5_WIDTH_IN, 1 - (margin_y * 0.5 / A5_HEIGHT_IN),
184
- f"{date_str} {title}",
185
- fontsize=title_fontsize,
186
- color=DATE_COLOR,
187
- fontweight='bold',
188
- ha='left',
189
- va='center')
190
-
191
 
192
  def create_print_layout(data, title, date_str):
193
- """
194
- Creates the final print-ready output in both PNG and PDF formats using the new grid layout.
195
- """
196
  if data.empty:
197
  return None
198
 
199
- # --- Create separate figures for PNG and PDF to ensure no cross-contamination ---
200
- png_fig = plt.figure(figsize=(5.83, 8.27), dpi=300)
201
- pdf_fig = plt.figure(figsize=(5.83, 8.27), dpi=300)
202
-
203
- # --- Draw the layout on both figures ---
204
- _draw_grid_on_figure(png_fig, data, title, date_str)
205
- _draw_grid_on_figure(pdf_fig, data, title, date_str)
206
-
207
- # --- Save PNG to a memory buffer ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
208
  png_buffer = io.BytesIO()
209
- png_fig.savefig(png_buffer, format='png', bbox_inches='tight', pad_inches=0.02)
210
  png_buffer.seek(0)
211
  png_base64 = base64.b64encode(png_buffer.getvalue()).decode()
212
- plt.close(png_fig)
213
-
214
- # --- Save PDF to a memory buffer ---
215
  pdf_buffer = io.BytesIO()
216
- with PdfPages(pdf_buffer) as pdf:
217
- pdf.savefig(pdf_fig, bbox_inches='tight', pad_inches=0.02)
218
  pdf_buffer.seek(0)
219
  pdf_base64 = base64.b64encode(pdf_buffer.getvalue()).decode()
220
- plt.close(pdf_fig)
 
221
 
222
  return {
223
  'png': f'data:image/png;base64,{png_base64}',
@@ -225,32 +228,30 @@ def create_print_layout(data, title, date_str):
225
  }
226
 
227
  def display_pdf(base64_pdf):
228
- """
229
- Generates the HTML to embed and display a PDF in Streamlit.
230
- """
231
  pdf_display = f'<iframe src="data:application/pdf;base64,{base64_pdf}" width="100%" height="800" type="application/pdf"></iframe>'
232
  return pdf_display
233
 
234
- # --- Streamlit User Interface ---
235
  st.set_page_config(page_title="散厅时间快捷打印", layout="wide")
236
  st.title("散厅时间快捷打印")
237
 
238
  uploaded_file = st.file_uploader("上传【放映场次核对表.xls】文件", type=["xls", "xlsx"])
239
 
240
  if uploaded_file:
241
- part1_data, part2_data, date_string = process_schedule(uploaded_file)
 
242
 
243
  if part1_data is not None and part2_data is not None:
244
- # Generate layouts for both day and night shifts
245
- part1_output = create_print_layout(part1_data, "A", date_string)
246
- part2_output = create_print_layout(part2_data, "C", date_string)
247
 
248
  col1, col2 = st.columns(2)
249
 
250
  with col1:
251
- st.subheader("白班散场预览 (时间 ≤ 17:30)")
252
  if part1_output:
253
- # Use tabs for PDF and PNG previews
254
  tab1_1, tab1_2 = st.tabs(["PDF 预览", "PNG 预览"])
255
  with tab1_1:
256
  st.markdown(display_pdf(part1_output['pdf']), unsafe_allow_html=True)
@@ -260,9 +261,8 @@ if uploaded_file:
260
  st.info("白班部分没有数据")
261
 
262
  with col2:
263
- st.subheader("夜班散场预览 (时间 > 17:30)")
264
  if part2_output:
265
- # Use tabs for PDF and PNG previews
266
  tab2_1, tab2_2 = st.tabs(["PDF 预览", "PNG 预览"])
267
  with tab2_1:
268
  st.markdown(display_pdf(part2_output['pdf']), unsafe_allow_html=True)
 
4
  import matplotlib.pyplot as plt
5
  import io
6
  import base64
7
+ import matplotlib.gridspec as gridspec
8
+ import matplotlib.font_manager as fm
9
  import math
 
10
 
11
+ # --- CONSTANTS ---
12
  SPLIT_TIME = "17:30"
13
  BUSINESS_START = "09:30"
14
  BUSINESS_END = "01:30"
15
+ BORDER_COLOR = '#A9A9A9'
16
  DATE_COLOR = '#A9A9A9'
17
+ A5_WIDTH_IN = 5.83
18
+ A5_HEIGHT_IN = 8.27
19
+ DPI = 300
20
+ NUM_COLS = 3
21
+
22
+ # --- FONT SETUP ---
23
+ # Attempt to load the specified font, with a safe fallback.
24
+ try:
25
+ # IMPORTANT: Place 'SimHei.ttf' in the same directory as the script.
26
+ FONT_PROP = fm.FontProperties(fname='SimHei.ttf')
27
+ except FileNotFoundError:
28
+ st.warning("SimHei.ttf not found. Using a default sans-serif font. Chinese characters may not display correctly.")
29
+ FONT_PROP = fm.FontProperties(family='sans-serif')
30
+
31
 
32
  def process_schedule(file):
33
+ """处理上传的 Excel 文件,生成排序和分组后的打印内容"""
 
 
34
  try:
35
+ # 读取 Excel,跳过前 8
36
  df = pd.read_excel(file, skiprows=8)
37
 
38
+ # 提取所需列 (G9, H9, J9)
39
+ df = df.iloc[:, [6, 7, 9]] # G, H, J 列
40
  df.columns = ['Hall', 'StartTime', 'EndTime']
41
 
42
+ # 清理数据
43
  df = df.dropna(subset=['Hall', 'StartTime', 'EndTime'])
44
+ df = df[df['Hall'].astype(str).str.contains(r'\d', na=False)] # Ensure Hall has a digit
45
 
46
+ # 转换影厅格式为 "#号" 格式
47
+ df['Hall'] = df['Hall'].astype(str).str.extract(r'(\d+)').astype(str) + ' '
48
 
49
+ # 转换时间为 datetime 对象
50
  base_date = datetime.today().date()
51
+ df['StartTime'] = pd.to_datetime(df['StartTime'], errors='coerce').dt.time
52
+ df['EndTime'] = pd.to_datetime(df['EndTime'], errors='coerce').dt.time
53
 
54
+ df = df.dropna(subset=['StartTime', 'EndTime'])
 
 
 
 
 
55
 
56
+ def combine_date_time(t):
57
+ dt = datetime.combine(base_date, t)
58
+ # Handle overnight shows (times past midnight like 1:30 AM are for the next day)
59
+ if t.hour < int(BUSINESS_START.split(':')[0]) - 1:
60
+ return dt + timedelta(days=1)
61
+ return dt
62
 
63
+ df['StartDateTime'] = df['StartTime'].apply(combine_date_time)
64
+ df['EndDateTime'] = df['EndTime'].apply(combine_date_time)
65
 
66
+ # 按散场时间排序
67
+ df = df.sort_values('EndDateTime')
 
 
68
 
69
+ # 分割数据
70
+ split_dt = datetime.combine(base_date, datetime.strptime(SPLIT_TIME, "%H:%M").time())
71
+
72
+ part1 = df[df['EndDateTime'] <= split_dt].copy()
73
+ part2 = df[df['EndDateTime'] > split_dt].copy()
74
 
75
+ # 格式化时间显示
76
+ for part in [part1, part2]:
77
+ part['EndTimeStr'] = part['EndDateTime'].dt.strftime('%-I:%M')
78
+
79
+ # 关键修改:精确读取C6单元格
80
+ date_df = pd.read_excel(
81
+ file,
82
+ skiprows=5, # 跳过前5行(0-4)
83
+ nrows=1, # 只读1行
84
+ usecols=[2], # 第三列(C列)
85
+ header=None # 无表头
86
+ )
87
  date_cell = date_df.iloc[0, 0]
88
 
89
  try:
90
+ # 处理不同日期格式
91
  if isinstance(date_cell, str):
92
  date_str = datetime.strptime(date_cell, '%Y-%m-%d').strftime('%Y-%m-%d')
93
  else:
 
95
  except:
96
  date_str = datetime.today().strftime('%Y-%m-%d')
97
 
98
+ return part1[['Hall', 'EndTimeStr']], part2[['Hall', 'EndTimeStr']], date_str
99
 
100
  except Exception as e:
101
+ st.error(f"处理文件时出错: {str(e)}")
102
  return None, None, None
103
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
104
 
105
  def create_print_layout(data, title, date_str):
106
+ """创建符合新要求的打印布局 (PNG 和 PDF)"""
 
 
107
  if data.empty:
108
  return None
109
 
110
+ # --- 1. 动态字体大小计算 ---
111
+ # 目标:让最长的文本占单元格宽度的90%
112
+ longest_text = ""
113
+ if not data.empty:
114
+ data['combined_text'] = data['Hall'] + data['EndTimeStr']
115
+ if not data['combined_text'].empty:
116
+ longest_text = data.loc[data['combined_text'].str.len().idxmax(), 'combined_text']
117
+
118
+ # 估算单元格宽度 (A5纸张,减去边距和间距)
119
+ fig_width_in = A5_WIDTH_IN
120
+ margin_frac = 0.04 # 4% margin on each side
121
+ wspace_frac = 0.05 # 5% space between columns
122
+
123
+ # 可用绘图宽度
124
+ drawable_width_in = fig_width_in * (1 - 2 * margin_frac)
125
+ # 宽度被3个子图和2个间隙瓜分
126
+ # 3*cell_w + 2*(cell_w*wspace) = drawable_width
127
+ cell_width_in = drawable_width_in / (NUM_COLS + (NUM_COLS - 1) * wspace_frac)
128
+
129
+ main_fontsize = 30 # Default start size
130
+ if longest_text:
131
+ # 估算文本宽度 (这是一个经验法则,0.7是中英混合字体的估算因子)
132
+ # 文本宽度(点) ≈ 字符数 * 字体大小(点) * 因子
133
+ estimated_text_width_pt = len(longest_text) * main_fontsize * 0.7
134
+ target_width_pt = (cell_width_in * 0.9) * 72 # 目标宽度 (英寸*0.9 -> 点)
135
+
136
+ if estimated_text_width_pt > 0:
137
+ ratio = target_width_pt / estimated_text_width_pt
138
+ main_fontsize *= ratio
139
+
140
+ main_fontsize = max(10, min(main_fontsize, 50)) # 限制字体大小在合理范围
141
+ index_fontsize = main_fontsize * 0.45 # 序号字体大小
142
+
143
+ # --- 2. 准备绘图 ---
144
+ def generate_figure():
145
+ fig = plt.figure(figsize=(A5_WIDTH_IN, A5_HEIGHT_IN), dpi=DPI)
146
+ fig.subplots_adjust(left=margin_frac, right=1-margin_frac, top=0.95, bottom=margin_frac, hspace=0.2, wspace=wspace_frac)
147
+
148
+ total_items = len(data)
149
+ num_rows = math.ceil(total_items / NUM_COLS)
150
+
151
+ # 创建网格
152
+ gs = gridspec.GridSpec(num_rows + 1, NUM_COLS, height_ratios=[0.2] + [1] * num_rows, figure=fig)
153
+
154
+ # 绘制日期标题
155
+ ax_date = fig.add_subplot(gs[0, :])
156
+ ax_date.text(0, 0.5, f"{date_str} {title}", fontproperties=FONT_PROP,
157
+ fontsize=main_fontsize * 0.5, color=DATE_COLOR, fontweight='bold',
158
+ ha='left', va='center', transform=ax_date.transAxes)
159
+ ax_date.set_axis_off()
160
+
161
+ # 准备Z字形排列的数据
162
+ data_values = data[['Hall', 'EndTimeStr']].values.tolist()
163
+ while len(data_values) % NUM_COLS != 0:
164
+ data_values.append(['', ''])
165
+
166
+ rows_per_col_layout = math.ceil(len(data_values) / NUM_COLS)
167
+ sorted_data = [['', '']] * len(data_values)
168
+ for i, item in enumerate(data_values):
169
+ if item[0] and item[1]:
170
+ row_in_col = i % rows_per_col_layout
171
+ col_idx = i // rows_per_col_layout
172
+ new_index = row_in_col * NUM_COLS + col_idx
173
+ if new_index < len(sorted_data):
174
+ sorted_data[new_index] = item
175
+
176
+ item_counter = 0
177
+ for idx, (hall, end_time) in enumerate(sorted_data):
178
+ if hall and end_time:
179
+ item_counter += 1
180
+ row_grid = idx // NUM_COLS + 1
181
+ col_grid = idx % NUM_COLS
182
+
183
+ if row_grid < num_rows + 1:
184
+ ax = fig.add_subplot(gs[row_grid, col_grid])
185
+
186
+ # --- 3. 绘制单元格 ---
187
+ # a. 设置点状虚线边框
188
+ for spine in ax.spines.values():
189
+ spine.set_linestyle((0, (1, 2))) # (offset, (on, off)) tuple for dotted
190
+ spine.set_edgecolor(BORDER_COLOR)
191
+ spine.set_linewidth(1)
192
+
193
+ # b. 居中绘制主要文本
194
+ display_text = f"{hall}{end_time}"
195
+ ax.text(0.5, 0.5, display_text, fontproperties=FONT_PROP,
196
+ fontsize=main_fontsize, fontweight='bold',
197
+ ha='center', va='center', transform=ax.transAxes)
198
+
199
+ # c. 在左上角添加序号
200
+ ax.text(0.05, 0.95, str(item_counter), fontproperties=FONT_PROP,
201
+ fontsize=index_fontsize, color='grey',
202
+ ha='left', va='top', transform=ax.transAxes)
203
+
204
+ ax.set_xticks([])
205
+ ax.set_yticks([])
206
+ return fig
207
+
208
+ # --- 4. 保存为 PNG 和 PDF ---
209
+ fig_for_output = generate_figure()
210
+
211
+ # 保存 PNG
212
  png_buffer = io.BytesIO()
213
+ fig_for_output.savefig(png_buffer, format='png', dpi=DPI)
214
  png_buffer.seek(0)
215
  png_base64 = base64.b64encode(png_buffer.getvalue()).decode()
216
+
217
+ # 保存 PDF
 
218
  pdf_buffer = io.BytesIO()
219
+ fig_for_output.savefig(pdf_buffer, format='pdf', dpi=DPI)
 
220
  pdf_buffer.seek(0)
221
  pdf_base64 = base64.b64encode(pdf_buffer.getvalue()).decode()
222
+
223
+ plt.close(fig_for_output)
224
 
225
  return {
226
  'png': f'data:image/png;base64,{png_base64}',
 
228
  }
229
 
230
  def display_pdf(base64_pdf):
231
+ """在Streamlit中嵌入显示PDF"""
 
 
232
  pdf_display = f'<iframe src="data:application/pdf;base64,{base64_pdf}" width="100%" height="800" type="application/pdf"></iframe>'
233
  return pdf_display
234
 
235
+ # --- Streamlit UI ---
236
  st.set_page_config(page_title="散厅时间快捷打印", layout="wide")
237
  st.title("散厅时间快捷打印")
238
 
239
  uploaded_file = st.file_uploader("上传【放映场次核对表.xls】文件", type=["xls", "xlsx"])
240
 
241
  if uploaded_file:
242
+ # Rename columns in process_schedule call to match new names
243
+ part1_data, part2_data, date_str = process_schedule(uploaded_file)
244
 
245
  if part1_data is not None and part2_data is not None:
246
+ # Pass the dataframes with renamed 'EndTimeStr' column
247
+ part1_output = create_print_layout(part1_data, "A", date_str)
248
+ part2_output = create_print_layout(part2_data, "C", date_str)
249
 
250
  col1, col2 = st.columns(2)
251
 
252
  with col1:
253
+ st.subheader("白班散场预览(结束时间 ≤ 17:30")
254
  if part1_output:
 
255
  tab1_1, tab1_2 = st.tabs(["PDF 预览", "PNG 预览"])
256
  with tab1_1:
257
  st.markdown(display_pdf(part1_output['pdf']), unsafe_allow_html=True)
 
261
  st.info("白班部分没有数据")
262
 
263
  with col2:
264
+ st.subheader("夜班散场预览(结束时间 > 17:30")
265
  if part2_output:
 
266
  tab2_1, tab2_2 = st.tabs(["PDF 预览", "PNG 预览"])
267
  with tab2_1:
268
  st.markdown(display_pdf(part2_output['pdf']), unsafe_allow_html=True)