Ethscriptions commited on
Commit
7704499
·
verified ·
1 Parent(s): c924006

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +216 -388
app.py CHANGED
@@ -1,453 +1,281 @@
1
  import pandas as pd
2
  import streamlit as st
 
3
  import matplotlib.pyplot as plt
4
- import matplotlib.font_manager as font_manager
5
  import io
6
  import base64
7
- import os
8
- from datetime import datetime, timedelta
9
  import math
10
- from pypinyin import lazy_pinyin, Style
11
  from matplotlib.backends.backend_pdf import PdfPages
12
- import matplotlib.gridspec as gridspec
13
- from matplotlib.patches import FancyBboxPatch
14
 
15
- # --- Constants for "Quick Print" (放映场次核对表) ---
16
  SPLIT_TIME = "17:30"
17
  BUSINESS_START = "09:30"
18
  BUSINESS_END = "01:30"
19
  BORDER_COLOR = '#A9A9A9'
20
  DATE_COLOR = '#A9A9A9'
 
21
 
22
- # --- Helper functions for "LED Screen" (放映时间核对表) ---
23
- def get_font(size=14):
24
- """Loads a specific font file, falling back to a default if not found."""
25
- font_path = "simHei.ttc"
26
- if not os.path.exists(font_path):
27
- font_path = "SimHei.ttf" # Fallback font
28
- # Add a final fallback for systems without Chinese fonts
29
- try:
30
- return font_manager.FontProperties(fname=font_path, size=size)
31
- except RuntimeError:
32
- # If the font file is not found, use a default font that should exist.
33
- # This will likely not render Chinese characters correctly but prevents crashing.
34
- return font_manager.FontProperties(family='sans-serif', size=size)
35
-
36
-
37
- def get_pinyin_abbr(text):
38
- """Gets the first letter of the Pinyin for the first two Chinese characters of a text."""
39
- if not text:
40
- return ""
41
- # Extract the first two Chinese characters
42
- chars = [c for c in text if '\u4e00' <= c <= '\u9fff']
43
- chars = chars[:2]
44
- # Get the first letter of the pinyin for each character
45
- pinyin_list = lazy_pinyin(chars, style=Style.FIRST_LETTER)
46
- return ''.join(pinyin_list).upper()
47
-
48
- # --- Processing logic for "LED Screen" (放映时间核对表) ---
49
- def process_schedule_led(file):
50
- """Processes the '放映时间核对表.xls' file."""
51
- try:
52
- # Attempt to read the date from a specific cell
53
- date_df = pd.read_excel(file, header=None, skiprows=7, nrows=1, usecols=[3])
54
- date_str = pd.to_datetime(date_df.iloc[0, 0]).strftime('%Y-%m-%d')
55
- base_date = pd.to_datetime(date_str).date()
56
- except Exception:
57
- # Fallback to the current date if reading fails
58
- date_str = datetime.today().strftime('%Y-%m-%d')
59
- base_date = datetime.today().date()
60
-
61
  try:
62
- df = pd.read_excel(file, header=9, usecols=[1, 2, 4, 5])
63
- df.columns = ['Hall', 'StartTime', 'EndTime', 'Movie']
64
- df['Hall'] = df['Hall'].ffill()
65
- df.dropna(subset=['StartTime', 'EndTime', 'Movie'], inplace=True)
66
- df['Hall'] = df['Hall'].astype(str).str.extract(r'(\d+号)')
67
-
68
- # Convert times to datetime objects, handling overnight screenings
69
- df['StartTime_dt'] = pd.to_datetime(df['StartTime'], format='%H:%M', errors='coerce').apply(
70
- lambda t: t.replace(year=base_date.year, month=base_date.month, day=base_date.day) if pd.notnull(t) else t
71
- )
72
- df['EndTime_dt'] = pd.to_datetime(df['EndTime'], format='%H:%M', errors='coerce').apply(
73
- lambda t: t.replace(year=base_date.year, month=base_date.month, day=base_date.day) if pd.notnull(t) else t
74
- )
75
- df.loc[df['EndTime_dt'] < df['StartTime_dt'], 'EndTime_dt'] += timedelta(days=1)
76
- df = df.sort_values(['Hall', 'StartTime_dt'])
77
-
78
- # Merge consecutive screenings of the same movie
79
- merged_rows = []
80
- for hall, group in df.groupby('Hall'):
81
- group = group.sort_values('StartTime_dt')
82
- current = None
83
- for _, row in group.iterrows():
84
- if current is None:
85
- current = row.copy()
86
- else:
87
- if row['Movie'] == current['Movie']:
88
- current['EndTime_dt'] = row['EndTime_dt']
89
- else:
90
- merged_rows.append(current)
91
- current = row.copy()
92
- if current is not None:
93
- merged_rows.append(current)
94
-
95
- merged_df = pd.DataFrame(merged_rows)
96
-
97
- # Adjust start and end times
98
- merged_df['StartTime_dt'] = merged_df['StartTime_dt'] - timedelta(minutes=10)
99
- merged_df['EndTime_dt'] = merged_df['EndTime_dt'] - timedelta(minutes=5)
100
-
101
- merged_df['StartTime_str'] = merged_df['StartTime_dt'].dt.strftime('%H:%M')
102
- merged_df['EndTime_str'] = merged_df['EndTime_dt'].dt.strftime('%H:%M')
103
-
104
- return merged_df[['Hall', 'Movie', 'StartTime_str', 'EndTime_str']], date_str
105
- except Exception as e:
106
- st.error(f"An error occurred during file processing: {e}")
107
- return None, date_str
108
-
109
- # --- Layout generation for "LED Screen" (放映时间核对表) ---
110
- def create_print_layout_led(data, date_str):
111
- """Generates PNG and PDF layouts for the 'LED Screen' schedule."""
112
- if data is None or data.empty:
113
- return None
114
-
115
- # Create figures for PNG and PDF output with A4 dimensions
116
- png_fig = plt.figure(figsize=(8.27, 11.69), dpi=300)
117
- png_ax = png_fig.add_subplot(111)
118
- png_ax.set_axis_off()
119
- png_fig.subplots_adjust(left=0.02, right=0.98, top=0.98, bottom=0.02)
120
-
121
- pdf_fig = plt.figure(figsize=(8.27, 11.69), dpi=300)
122
- pdf_ax = pdf_fig.add_subplot(111)
123
- pdf_ax.set_axis_off()
124
- pdf_fig.subplots_adjust(left=0.02, right=0.98, top=0.98, bottom=0.02)
125
-
126
- def process_figure(fig, ax):
127
- halls = sorted(data['Hall'].unique(), key=lambda h: int(h.replace('号','')) if h else 0)
128
-
129
- num_separators = len(halls) - 1
130
- total_layout_rows = len(data) + num_separators + 2
131
-
132
- available_height = 0.96
133
- row_height = available_height / total_layout_rows
134
 
135
- fig_height_inches = fig.get_figheight()
136
- row_height_points = row_height * fig_height_inches * 72
137
- font_size = row_height_points * 0.9
138
 
139
- date_font = get_font(font_size * 0.8)
140
- hall_font = get_font(font_size)
141
- movie_font = get_font(font_size)
142
 
143
- col_hall_left = 0.0
144
- col_movie_right = 0.50
145
- col_seq_left = 0.52
146
- col_pinyin_left = 0.62
147
- col_time_left = 0.75
148
 
149
- ax.text(col_hall_left, 0.99, date_str, color='#A9A9A9',
150
- ha='left', va='top', fontproperties=date_font, transform=ax.transAxes)
151
-
152
- y_position = 0.98 - row_height
153
-
154
- for i, hall in enumerate(halls):
155
- hall_data = data[data['Hall'] == hall]
156
-
157
- if i > 0:
158
- ax.axhline(y=y_position + row_height / 2, xmin=col_hall_left, xmax=0.97, color='black', linewidth=0.7)
159
- y_position -= row_height
160
-
161
- movie_count = 1
162
- for _, row in hall_data.iterrows():
163
- if movie_count == 1:
164
- ax.text(col_hall_left, y_position, f"{hall.replace('号', '')}#",
165
- ha='left', va='center', fontweight='bold',
166
- fontproperties=hall_font, transform=ax.transAxes)
167
-
168
- ax.text(col_movie_right, y_position, row['Movie'],
169
- ha='right', va='center', fontproperties=movie_font, transform=ax.transAxes)
170
-
171
- ax.text(col_seq_left, y_position, f"{movie_count}.",
172
- ha='left', va='center', fontproperties=movie_font, transform=ax.transAxes)
173
-
174
- pinyin_abbr = get_pinyin_abbr(row['Movie'])
175
- ax.text(col_pinyin_left, y_position, pinyin_abbr,
176
- ha='left', va='center', fontproperties=movie_font, transform=ax.transAxes)
177
-
178
- ax.text(col_time_left, y_position, f"{row['StartTime_str']}-{row['EndTime_str']}",
179
- ha='left', va='center', fontproperties=movie_font, transform=ax.transAxes)
180
-
181
- y_position -= row_height
182
- movie_count += 1
183
-
184
- process_figure(png_fig, png_ax)
185
- process_figure(pdf_fig, pdf_ax)
186
-
187
- png_buffer = io.BytesIO()
188
- png_fig.savefig(png_buffer, format='png', bbox_inches='tight', pad_inches=0.05)
189
- png_buffer.seek(0)
190
- image_base64 = base64.b64encode(png_buffer.getvalue()).decode()
191
- plt.close(png_fig)
192
-
193
- pdf_buffer = io.BytesIO()
194
- with PdfPages(pdf_buffer) as pdf:
195
- pdf.savefig(pdf_fig, bbox_inches='tight', pad_inches=0.05)
196
- pdf_buffer.seek(0)
197
- pdf_base64 = base64.b64encode(pdf_buffer.getvalue()).decode()
198
- plt.close(pdf_fig)
199
-
200
- return {
201
- 'png': f"data:image/png;base64,{image_base64}",
202
- 'pdf': f"data:application/pdf;base64,{pdf_base64}"
203
- }
204
 
205
- # --- Processing logic for "Quick Print" (放映场次核对表) ---
206
- def process_schedule_quick(file):
207
- """Processes the '放映场次核对表.xls' file."""
208
- try:
209
- df = pd.read_excel(file, skiprows=8)
210
- df = df.iloc[:, [6, 7, 9]]
211
- df.columns = ['Hall', 'StartTime', 'EndTime']
212
- df = df.dropna(subset=['Hall', 'StartTime', 'EndTime'])
213
- df['Hall'] = df['Hall'].str.extract(r'(\d+)号').astype(str) + ' '
214
-
215
  base_date = datetime.today().date()
216
- df['StartTime'] = pd.to_datetime(df['StartTime'])
217
- df['EndTime'] = pd.to_datetime(df['EndTime'])
218
-
219
- business_start = datetime.strptime(f"{base_date} {BUSINESS_START}", "%Y-%m-%d %H:%M")
220
- business_end = datetime.strptime(f"{base_date} {BUSINESS_END}", "%Y-%m-%d %H:%M")
221
- if business_end < business_start:
222
- business_end += timedelta(days=1)
223
-
224
- for idx, row in df.iterrows():
225
- end_time = row['EndTime']
226
- if end_time.hour < 9:
227
- df.at[idx, 'EndTime'] = end_time + timedelta(days=1)
228
- if row['StartTime'].hour >= 21 and end_time.hour < 9:
229
- df.at[idx, 'EndTime'] = end_time + timedelta(days=1)
230
-
231
- df['time_for_comparison'] = df['EndTime'].apply(lambda x: datetime.combine(base_date, x.time()))
232
- df.loc[df['time_for_comparison'].dt.hour < 9, 'time_for_comparison'] += timedelta(days=1)
233
-
234
- valid_times = (
235
- (df['time_for_comparison'] >= datetime.combine(base_date, business_start.time())) &
236
- (df['time_for_comparison'] <= datetime.combine(base_date + timedelta(days=1), business_end.time()))
237
  )
238
- df = df[valid_times]
239
- df = df.sort_values('EndTime')
240
 
241
- split_time_dt = datetime.strptime(f"{base_date} {SPLIT_TIME}", "%Y-%m-%d %H:%M")
 
 
 
 
242
 
243
- part1 = df[df['time_for_comparison'] <= split_time_dt].copy()
244
- part2 = df[df['time_for_comparison'] > split_time_dt].copy()
245
 
 
246
  for part in [part1, part2]:
247
- part['EndTime'] = part['EndTime'].dt.strftime('%-H:%M')
248
-
 
249
  date_df = pd.read_excel(file, skiprows=5, nrows=1, usecols=[2], header=None)
250
  date_cell = date_df.iloc[0, 0]
251
-
252
  try:
253
  if isinstance(date_cell, str):
 
254
  date_str = datetime.strptime(date_cell, '%Y-%m-%d').strftime('%Y-%m-%d')
255
  else:
 
256
  date_str = pd.to_datetime(date_cell).strftime('%Y-%m-%d')
257
  except:
258
  date_str = datetime.today().strftime('%Y-%m-%d')
259
-
260
- return part1[['Hall', 'EndTime']], part2[['Hall', 'EndTime']], date_str
261
 
262
  except Exception as e:
263
  st.error(f"处理文件时出错: {str(e)}")
264
  return None, None, None
265
 
266
- # --- Layout generation for "Quick Print" (放映场次核对表) ---
267
- def create_print_layout_quick(data, title, date_str):
268
- """Creates print layout for the 'Quick Print' schedule."""
 
 
 
 
 
 
269
  if data.empty:
270
  return None
271
 
272
- png_fig = plt.figure(figsize=(5.83, 8.27), dpi=300) # A5
273
- png_fig.subplots_adjust(left=0.02, right=0.98, top=0.98, bottom=0.02)
274
-
275
- pdf_fig = plt.figure(figsize=(5.83, 8.27), dpi=300) # A5
276
- pdf_fig.subplots_adjust(left=0.02, right=0.98, top=0.98, bottom=0.02)
277
-
278
- def process_figure(fig, is_pdf=False):
279
- plt.rcParams['font.family'] = 'sans-serif'
280
- plt.rcParams['font.sans-serif'] = ['Arial Unicode MS', 'Heiti TC', 'SimHei']
281
-
282
- total_items = len(data)
283
- num_cols = 3
284
- num_rows = math.ceil(total_items / num_cols)
285
-
286
- gs = gridspec.GridSpec(num_rows + 1, num_cols, hspace=0.05, wspace=0.05, height_ratios=[0.1] + [1] * num_rows, figure=fig)
287
-
288
- target_width_px = 1
289
- if total_items > 0:
290
- ax_temp = fig.add_subplot(gs[1, 0])
291
- fig.canvas.draw()
292
- target_width_px = ax_temp.get_window_extent().width * 0.90
293
- ax_temp.remove()
294
-
295
- available_height_per_row = (8.27 * 0.9 * (1 / 1.2)) / num_rows if num_rows > 0 else 1
296
- date_fontsize = min(40, max(10, available_height_per_row * 72 * 0.5))
297
-
298
- data_values = data.values.tolist()
299
- while len(data_values) % num_cols != 0:
300
- data_values.append(['', ''])
301
- rows_per_col_layout = math.ceil(len(data_values) / num_cols)
302
-
303
- sorted_data = [['', '']] * len(data_values)
304
- for i, item in enumerate(data_values):
305
- if item[0] and item[1]:
306
- row_in_col = i % rows_per_col_layout
307
- col_idx = i // rows_per_col_layout
308
- new_index = row_in_col * num_cols + col_idx
309
- if new_index < len(sorted_data):
310
- sorted_data[new_index] = item
311
-
312
- for idx, (hall, end_time) in enumerate(sorted_data):
313
- if hall and end_time:
314
- row_grid = idx // num_cols + 1
315
- col_grid = idx % num_cols
316
-
317
- if row_grid < num_rows + 1:
318
- ax = fig.add_subplot(gs[row_grid, col_grid])
319
- for spine in ax.spines.values():
320
- spine.set_visible(False)
321
-
322
- bbox = FancyBboxPatch(
323
- (0.01, 0.01), 0.98, 0.98,
324
- boxstyle="round,pad=0,rounding_size=0.02",
325
- edgecolor=BORDER_COLOR, facecolor='none',
326
- linewidth=0.5, transform=ax.transAxes, clip_on=False
327
- )
328
- ax.add_patch(bbox)
329
-
330
- display_text = f"{hall}{end_time}"
331
- t = ax.text(0.5, 0.5, display_text,
332
- fontweight='bold', ha='center', va='center',
333
- transform=ax.transAxes)
334
-
335
- current_size = 120
336
- while current_size > 1:
337
- t.set_fontsize(current_size)
338
- text_bbox = t.get_window_extent(renderer=fig.canvas.get_renderer())
339
- if text_bbox.width <= target_width_px:
340
- break
341
- current_size -= 2
342
-
343
- ax.set_xticks([])
344
- ax.set_yticks([])
345
-
346
- ax_date = fig.add_subplot(gs[0, :])
347
- ax_date.text(0.01, 0.5, f"{date_str} {title}",
348
- fontsize=date_fontsize * 0.5,
349
- color=DATE_COLOR, fontweight='bold',
350
- ha='left', va='center', transform=ax_date.transAxes)
351
- for spine in ax_date.spines.values():
352
- spine.set_visible(False)
353
- ax_date.set_xticks([])
354
- ax_date.set_yticks([])
355
- ax_date.set_facecolor('none')
356
-
357
- process_figure(png_fig)
358
- process_figure(pdf_fig, is_pdf=True)
359
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
360
  png_buffer = io.BytesIO()
361
- png_fig.savefig(png_buffer, format='png', bbox_inches='tight', pad_inches=0.02)
362
  png_buffer.seek(0)
363
  png_base64 = base64.b64encode(png_buffer.getvalue()).decode()
364
- plt.close(png_fig)
365
 
 
366
  pdf_buffer = io.BytesIO()
367
- with PdfPages(pdf_buffer) as pdf:
368
- pdf.savefig(pdf_fig, bbox_inches='tight', pad_inches=0.02)
369
  pdf_buffer.seek(0)
370
  pdf_base64 = base64.b64encode(pdf_buffer.getvalue()).decode()
371
- plt.close(pdf_fig)
 
372
 
373
  return {
374
  'png': f'data:image/png;base64,{png_base64}',
375
  'pdf': f'data:application/pdf;base64,{pdf_base64}'
376
  }
377
 
378
- # --- Generic Helper to Display PDF ---
379
  def display_pdf(base64_pdf):
380
- """Generates the HTML to embed and display a PDF in Streamlit."""
381
- pdf_display = f"""
382
- <iframe src="{base64_pdf}" width="100%" height="800" type="application/pdf"></iframe>
383
- """
384
  return pdf_display
385
 
386
- # --- Main Streamlit App ---
387
- st.set_page_config(page_title="影院排期打印工具", layout="wide")
388
- st.title("影院排期打印工具")
389
 
390
- uploaded_file = st.file_uploader(
391
- "选择【放映时间核对表.xls】或【放映场次核对表.xls】文件",
392
- accept_multiple_files=False,
393
- type=["xls"]
394
- )
395
 
396
  if uploaded_file:
397
- with st.spinner("文件正在处理中,请稍候..."):
398
- # --- Route to the correct processor based on filename ---
399
-
400
- # 1. Logic for "LED 屏幕时间表打印"
401
- if "放映时间核对表" in uploaded_file.name:
402
- st.subheader("LED 屏幕时间表")
403
- schedule, date_str = process_schedule_led(uploaded_file)
404
- if schedule is not None:
405
- output = create_print_layout_led(schedule, date_str)
406
- if output:
407
- tab1, tab2 = st.tabs(["PDF 预览", "PNG 预览"])
408
- with tab1:
409
- st.markdown(display_pdf(output['pdf']), unsafe_allow_html=True)
410
- with tab2:
411
- st.image(output['png'], use_container_width=True)
412
- else:
413
- st.info("没有可显示的数据。")
 
 
414
  else:
415
- st.error("无法处理文件,请检查文件格式或内容是否正确。")
416
-
417
- # 2. Logic for "散厅时间快捷打印"
418
- elif "放映场次核对表" in uploaded_file.name:
419
- part1_data, part2_data, date_str = process_schedule_quick(uploaded_file)
420
-
421
- if part1_data is not None and part2_data is not None:
422
- part1_output = create_print_layout_quick(part1_data, "A", date_str)
423
- part2_output = create_print_layout_quick(part2_data, "C", date_str)
424
-
425
- col1, col2 = st.columns(2)
426
-
427
- with col1:
428
- st.subheader("白班散场预览(时间 ≤ 17:30)")
429
- if part1_output:
430
- tab1_1, tab1_2 = st.tabs(["PDF 预览 ", "PNG 预览 "]) # Added space to make keys unique
431
- with tab1_1:
432
- st.markdown(display_pdf(part1_output['pdf']), unsafe_allow_html=True)
433
- with tab1_2:
434
- st.image(part1_output['png'])
435
- else:
436
- st.info("白班部分没有数据")
437
-
438
- with col2:
439
- st.subheader("夜班散场预览(时间 > 17:30)")
440
- if part2_output:
441
- tab2_1, tab2_2 = st.tabs(["PDF 预览 ", "PNG 预览 "]) # Added spaces to make keys unique
442
- with tab2_1:
443
- st.markdown(display_pdf(part2_output['pdf']), unsafe_allow_html=True)
444
- with tab2_2:
445
- st.image(part2_output['png'])
446
- else:
447
- st.info("夜班部分没有数据")
448
  else:
449
- st.error("无法处理文件,请检查文件格式或内容是否正确。")
450
-
451
- # 3. Fallback for incorrect file
452
- else:
453
- st.warning("文件名不匹配。请上传名为【放映时间核对表.xls】或【放映场次核对表.xls】的文件。")
 
1
  import pandas as pd
2
  import streamlit as st
3
+ from datetime import datetime, timedelta
4
  import matplotlib.pyplot as plt
 
5
  import io
6
  import base64
7
+ import matplotlib.gridspec as gridspec
 
8
  import math
 
9
  from matplotlib.backends.backend_pdf import PdfPages
10
+ from matplotlib.patches import Rectangle # Replaced FancyBboxPatch
 
11
 
12
+ # --- Constants ---
13
  SPLIT_TIME = "17:30"
14
  BUSINESS_START = "09:30"
15
  BUSINESS_END = "01:30"
16
  BORDER_COLOR = '#A9A9A9'
17
  DATE_COLOR = '#A9A9A9'
18
+ SEQ_COLOR = '#A9A9A9' # Color for the new serial number
19
 
20
+ def process_schedule(file):
21
+ """处理上传的 Excel 文件,生成排序和分组后的打印内容"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
  try:
23
+ # 读取 Excel,跳过前 8
24
+ df = pd.read_excel(file, skiprows=8)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
 
26
+ # 提取所需列 (G9, H9, J9)
27
+ df = df.iloc[:, [6, 7, 9]] # G, H, J 列
28
+ df.columns = ['Hall', 'StartTime', 'EndTime']
29
 
30
+ # 清理数据
31
+ df = df.dropna(subset=['Hall', 'StartTime', 'EndTime'])
 
32
 
33
+ # 转换影厅格式为 "#号" 格式
34
+ df['Hall'] = df['Hall'].str.extract(r'(\d+)号').astype(str) + ' '
 
 
 
35
 
36
+ # 保存原始时间字符串用于诊断
37
+ df['original_end'] = df['EndTime']
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
 
39
+ # 转换时间为 datetime 对象
 
 
 
 
 
 
 
 
 
40
  base_date = datetime.today().date()
41
+ # Using errors='coerce' will turn unparseable times into NaT (Not a Time)
42
+ df['StartTime'] = pd.to_datetime(df['StartTime'], errors='coerce')
43
+ df['EndTime'] = pd.to_datetime(df['EndTime'], errors='coerce')
44
+ df = df.dropna(subset=['StartTime', 'EndTime']) # Drop rows where time conversion failed
45
+
46
+ # 设置基准时间
47
+ business_start_time = datetime.strptime(BUSINESS_START, "%H:%M").time()
48
+ business_end_time = datetime.strptime(BUSINESS_END, "%H:%M").time()
49
+
50
+ # 处理跨天情况:结束时间小于开始时间,则结束时间加一天
51
+ # This logic handles cases like 9:30 AM to 1:30 AM (next day)
52
+ df['EndTime_adjusted'] = df.apply(
53
+ lambda row: row['EndTime'] + timedelta(days=1) if row['EndTime'].time() < row['StartTime'].time() else row['EndTime'],
54
+ axis=1
 
 
 
 
 
 
 
55
  )
 
 
56
 
57
+ # 按散场时间排序 (using the adjusted time)
58
+ df = df.sort_values('EndTime_adjusted')
59
+
60
+ # 分割数据
61
+ split_dt = datetime.strptime(SPLIT_TIME, "%H:%M").time()
62
 
63
+ part1 = df[df['EndTime_adjusted'].dt.time <= split_dt].copy()
64
+ part2 = df[df['EndTime_adjusted'].dt.time > split_dt].copy()
65
 
66
+ # 格式化时间显示 (use original EndTime for display)
67
  for part in [part1, part2]:
68
+ part['EndTime_formatted'] = part['EndTime'].dt.strftime('%-I:%M')
69
+
70
+ # 读取日期单元格 C6
71
  date_df = pd.read_excel(file, skiprows=5, nrows=1, usecols=[2], header=None)
72
  date_cell = date_df.iloc[0, 0]
73
+
74
  try:
75
  if isinstance(date_cell, str):
76
+ # Assuming format like '2023-10-27'
77
  date_str = datetime.strptime(date_cell, '%Y-%m-%d').strftime('%Y-%m-%d')
78
  else:
79
+ # Assuming it's a datetime object
80
  date_str = pd.to_datetime(date_cell).strftime('%Y-%m-%d')
81
  except:
82
  date_str = datetime.today().strftime('%Y-%m-%d')
83
+
84
+ return part1[['Hall', 'EndTime_formatted']], part2[['Hall', 'EndTime_formatted']], date_str
85
 
86
  except Exception as e:
87
  st.error(f"处理文件时出错: {str(e)}")
88
  return None, None, None
89
 
90
+
91
+ def create_print_layout(data, title, date_str):
92
+ """
93
+ 创建符合新要求的打印布局 (PNG 和 PDF)。
94
+ 1. 动态计算边距。
95
+ 2. 使用灰色虚线圆点作为单元格边框。
96
+ 3. 单元格内容区域为单元格的90%。
97
+ 4. 在左上角添加灰色序号。
98
+ """
99
  if data.empty:
100
  return None
101
 
102
+ # --- Constants ---
103
+ A5_WIDTH_IN = 5.83
104
+ A5_HEIGHT_IN = 8.27
105
+ DPI = 300
106
+ NUM_COLS = 3
107
+
108
+ # --- Setup Figure ---
109
+ fig = plt.figure(figsize=(A5_WIDTH_IN, A5_HEIGHT_IN), dpi=DPI)
110
+
111
+ # --- Font Setup ---
112
+ plt.rcParams['font.family'] = 'sans-serif'
113
+ plt.rcParams['font.sans-serif'] = ['Arial Unicode MS', 'Heiti TC', 'sans-serif']
114
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
115
 
116
+ # --- Data Preparation ---
117
+ total_items = len(data)
118
+ # Augment data with an original index for numbering
119
+ data_values_with_index = [(i, row) for i, row in enumerate(data.values.tolist())]
120
+
121
+ # Pad data to be a multiple of NUM_COLS
122
+ padded_total = math.ceil(total_items / NUM_COLS) * NUM_COLS
123
+ while len(data_values_with_index) < padded_total:
124
+ data_values_with_index.append((None, ['', '']))
125
+
126
+ num_rows = padded_total // NUM_COLS
127
+
128
+ # --- Layout Calculation (Request 1) ---
129
+ if num_rows > 0:
130
+ # "A5 paper height / num_rows / 4 is the padding for all sides"
131
+ padding_in = (A5_HEIGHT_IN / num_rows / 4)
132
+ # Cap padding to prevent it from being excessively large
133
+ padding_in = min(padding_in, 0.5)
134
+ else:
135
+ padding_in = 0.25 # Default padding if no rows
136
+
137
+ # Convert padding to relative figure coordinates for subplots_adjust
138
+ left_margin = padding_in / A5_WIDTH_IN
139
+ right_margin = 1 - left_margin
140
+ bottom_margin = padding_in / A5_HEIGHT_IN
141
+ top_margin = 1 - bottom_margin
142
+
143
+ # Adjust overall figure margins
144
+ fig.subplots_adjust(left=left_margin, right=right_margin, top=top_margin, bottom=bottom_margin, hspace=0.4, wspace=0.4)
145
+
146
+ # --- Grid & Font Size ---
147
+ gs = gridspec.GridSpec(num_rows + 1, NUM_COLS, height_ratios=[0.2] + [1] * num_rows, figure=fig)
148
+
149
+ if num_rows > 0:
150
+ content_area_height_in = A5_HEIGHT_IN * (top_margin - bottom_margin)
151
+ cell_height_in = content_area_height_in / num_rows * (1 - fig.subplotpars.hspace)
152
+ base_fontsize = min(40, max(10, cell_height_in * 72 * 0.4)) # 72 pt/inch, 40% of cell height
153
+ else:
154
+ base_fontsize = 20
155
+
156
+ # --- Z-Sort (Column-major) Data for Layout ---
157
+ rows_per_col_layout = num_rows
158
+ sorted_data = [(None, ['',''])] * padded_total
159
+ for i, item_tuple in enumerate(data_values_with_index):
160
+ if item_tuple[0] is not None:
161
+ original_data_index = i # Index from the time-sorted list
162
+ row_in_col = original_data_index % rows_per_col_layout
163
+ col_idx = original_data_index // rows_per_col_layout
164
+ new_grid_index = row_in_col * NUM_COLS + col_idx
165
+ if new_grid_index < len(sorted_data):
166
+ sorted_data[new_grid_index] = item_tuple
167
+
168
+ # --- Drawing Logic ---
169
+ for grid_idx, item_tuple in enumerate(sorted_data):
170
+ original_index, (hall, end_time) = item_tuple
171
+
172
+ if original_index is not None:
173
+ row_grid = grid_idx // NUM_COLS + 1 # +1 because date is in row 0
174
+ col_grid = grid_idx % NUM_COLS
175
+
176
+ ax = fig.add_subplot(gs[row_grid, col_grid])
177
+ ax.set_axis_off()
178
+
179
+ # --- Cell Border (Request 2) & Content Area (Request 3) ---
180
+ # Draw a dotted rectangle. Content will be placed inside this.
181
+ # Making the rect slightly smaller creates a visual 90% area.
182
+ cell_border = Rectangle((0.05, 0.05), 0.9, 0.9,
183
+ edgecolor=BORDER_COLOR,
184
+ facecolor='none',
185
+ linestyle=(0, (1, 1.5)), # Dotted line with round caps
186
+ linewidth=1,
187
+ transform=ax.transAxes,
188
+ clip_on=False)
189
+ ax.add_patch(cell_border)
190
+
191
+ # --- Cell Content ---
192
+ display_text = f"{hall}{end_time}"
193
+ ax.text(0.5, 0.5, display_text,
194
+ fontsize=base_fontsize,
195
+ fontweight='bold',
196
+ ha='center', va='center',
197
+ transform=ax.transAxes)
198
+
199
+ # --- Cell Numbering (Request 4) ---
200
+ # Serial number is original_index + 1
201
+ ax.text(0.12, 0.82, str(original_index + 1),
202
+ fontsize=base_fontsize * 0.5,
203
+ color=SEQ_COLOR,
204
+ fontweight='normal',
205
+ ha='center', va='center',
206
+ transform=ax.transAxes)
207
+
208
+ # --- Date Header ---
209
+ ax_date = fig.add_subplot(gs[0, :])
210
+ ax_date.set_axis_off()
211
+ ax_date.text(0, 0.5, f"{date_str} {title}",
212
+ fontsize=base_fontsize * 0.6,
213
+ color=DATE_COLOR,
214
+ fontweight='bold',
215
+ ha='left', va='center',
216
+ transform=ax_date.transAxes)
217
+
218
+ # --- Save to Buffers ---
219
+ # Save PNG
220
  png_buffer = io.BytesIO()
221
+ fig.savefig(png_buffer, format='png', bbox_inches='tight', pad_inches=0.02)
222
  png_buffer.seek(0)
223
  png_base64 = base64.b64encode(png_buffer.getvalue()).decode()
 
224
 
225
+ # Save PDF
226
  pdf_buffer = io.BytesIO()
227
+ fig.savefig(pdf_buffer, format='pdf', bbox_inches='tight', pad_inches=0.02)
 
228
  pdf_buffer.seek(0)
229
  pdf_base64 = base64.b64encode(pdf_buffer.getvalue()).decode()
230
+
231
+ plt.close(fig)
232
 
233
  return {
234
  'png': f'data:image/png;base64,{png_base64}',
235
  'pdf': f'data:application/pdf;base64,{pdf_base64}'
236
  }
237
 
 
238
  def display_pdf(base64_pdf):
239
+ """在Streamlit中嵌入显示PDF"""
240
+ pdf_display = f'<iframe src="{base64_pdf}" width="100%" height="800" type="application/pdf"></iframe>'
 
 
241
  return pdf_display
242
 
243
+ # --- Streamlit UI ---
244
+ st.set_page_config(page_title="散厅时间快捷打印", layout="wide")
245
+ st.title("散厅时间快捷打印")
246
 
247
+ uploaded_file = st.file_uploader("上传【放映场次核对表.xls】文件", type=["xls", "xlsx"])
 
 
 
 
248
 
249
  if uploaded_file:
250
+ # Use new column name 'EndTime_formatted' for display
251
+ part1, part2, date_str = process_schedule(uploaded_file)
252
+ if part1 is not None and part2 is not None:
253
+ part1_data_for_layout = part1[['Hall', 'EndTime_formatted']]
254
+ part2_data_for_layout = part2[['Hall', 'EndTime_formatted']]
255
+
256
+ part1_output = create_print_layout(part1_data_for_layout, "A", date_str)
257
+ part2_output = create_print_layout(part2_data_for_layout, "C", date_str)
258
+
259
+ col1, col2 = st.columns(2)
260
+
261
+ with col1:
262
+ st.subheader("白班散场预览(散场时间 ≤ 17:30)")
263
+ if part1_output:
264
+ tab1_1, tab1_2 = st.tabs(["PDF 预览", "PNG 预览"])
265
+ with tab1_1:
266
+ st.markdown(display_pdf(part1_output['pdf']), unsafe_allow_html=True)
267
+ with tab1_2:
268
+ st.image(part1_output['png'])
269
  else:
270
+ st.info("白班部分没有数据")
271
+
272
+ with col2:
273
+ st.subheader("夜班散场预览(散场时间 > 17:30)")
274
+ if part2_output:
275
+ tab2_1, tab2_2 = st.tabs(["PDF 预览", "PNG 预览"])
276
+ with tab2_1:
277
+ st.markdown(display_pdf(part2_output['pdf']), unsafe_allow_html=True)
278
+ with tab2_2:
279
+ st.image(part2_output['png'])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
280
  else:
281
+ st.info("夜班部分没有数据")