Ethscriptions commited on
Commit
89e2a2e
·
verified ·
1 Parent(s): 8319975

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +110 -128
app.py CHANGED
@@ -7,12 +7,11 @@ import base64
7
  import matplotlib.gridspec as gridspec
8
  import math
9
  from matplotlib.backends.backend_pdf import PdfPages
10
- # The 'FancyBboxPatch' is no longer needed for the new border style.
11
 
12
  SPLIT_TIME = "17:30"
13
  BUSINESS_START = "09:30"
14
  BUSINESS_END = "01:30"
15
- BORDER_COLOR = 'gray' # Changed to gray for the new style
16
  DATE_COLOR = '#A9A9A9'
17
 
18
  def process_schedule(file):
@@ -20,74 +19,72 @@ def process_schedule(file):
20
  try:
21
  # 读取 Excel,跳过前 8 行
22
  df = pd.read_excel(file, skiprows=8)
23
-
24
  # 提取所需列 (G9, H9, J9)
25
  df = df.iloc[:, [6, 7, 9]] # G, H, J 列
26
  df.columns = ['Hall', 'StartTime', 'EndTime']
27
-
28
  # 清理数据
29
  df = df.dropna(subset=['Hall', 'StartTime', 'EndTime'])
30
-
31
- # 转换影厅格式为 "#" 格式 (移除末尾空格)
32
- df['Hall'] = df['Hall'].str.extract(r'(\d+)号').astype(str)
33
-
34
  # 保存原始时间字符串用于诊断
35
  df['original_end'] = df['EndTime']
36
-
37
  # 转换时间为 datetime 对象
38
  base_date = datetime.today().date()
39
  df['StartTime'] = pd.to_datetime(df['StartTime'])
40
  df['EndTime'] = pd.to_datetime(df['EndTime'])
41
-
42
  # 设置基准时间
43
  business_start = datetime.strptime(f"{base_date} {BUSINESS_START}", "%Y-%m-%d %H:%M")
44
  business_end = datetime.strptime(f"{base_date} {BUSINESS_END}", "%Y-%m-%d %H:%M")
45
-
46
  # 处理跨天情况
47
  if business_end < business_start:
48
  business_end += timedelta(days=1)
49
-
50
  # 标准化所有时间到同一天
51
  for idx, row in df.iterrows():
52
  end_time = row['EndTime']
53
  if end_time.hour < 9:
54
  df.at[idx, 'EndTime'] = end_time + timedelta(days=1)
55
-
56
  if row['StartTime'].hour >= 21 and end_time.hour < 9:
57
  df.at[idx, 'EndTime'] = end_time + timedelta(days=1)
58
-
59
  # 筛选营业时间内的场次
60
  df['time_for_comparison'] = df['EndTime'].apply(
61
  lambda x: datetime.combine(base_date, x.time())
62
  )
63
-
64
  df.loc[df['time_for_comparison'].dt.hour < 9, 'time_for_comparison'] += timedelta(days=1)
65
-
66
  valid_times = (
67
  ((df['time_for_comparison'] >= datetime.combine(base_date, business_start.time())) &
68
  (df['time_for_comparison'] <= datetime.combine(base_date + timedelta(days=1), business_end.time())))
69
  )
70
-
71
  df = df[valid_times]
72
-
73
  # 按散场时间排序
74
  df = df.sort_values('EndTime')
75
-
76
  # 分割数据
77
- split_time_obj = datetime.strptime(SPLIT_TIME, "%H:%M").time()
78
- split_datetime = datetime.combine(base_date, split_time_obj)
79
-
80
- part1 = df[df['time_for_comparison'] <= split_datetime].copy()
81
- part2 = df[df['time_for_comparison'] > split_datetime].copy()
82
-
 
 
83
  # 格式化时间显示
84
  for part in [part1, part2]:
85
- # Use '%-H' for 24-hour format without leading zero on Linux/macOS
86
- # Use '%#H' on Windows. A more cross-platform way is to format and remove later.
87
- # Let's stick to '%H:%M' for universal 24-hour format e.g., "09:30"
88
- part['EndTime'] = part['EndTime'].dt.strftime('%H:%M')
89
-
90
-
91
  # 关键修改:精确读取C6单元格
92
  date_df = pd.read_excel(
93
  file,
@@ -97,7 +94,7 @@ def process_schedule(file):
97
  header=None # 无表头
98
  )
99
  date_cell = date_df.iloc[0, 0]
100
-
101
  try:
102
  # 处理不同日期格式
103
  if isinstance(date_cell, str):
@@ -106,152 +103,139 @@ def process_schedule(file):
106
  date_str = pd.to_datetime(date_cell).strftime('%Y-%m-%d')
107
  except:
108
  date_str = datetime.today().strftime('%Y-%m-%d')
109
-
110
  return part1[['Hall', 'EndTime']], part2[['Hall', 'EndTime']], date_str
111
-
112
  except Exception as e:
113
  st.error(f"处理文件时出错: {str(e)}")
114
  return None, None, None
115
 
116
-
117
  def create_print_layout(data, title, date_str):
118
- """创建打印布局 (PNG 和 PDF)"""
 
 
119
  if data.empty:
120
  return None
121
 
122
- # --- A5 Paper Dimensions in inches for precise layout ---
123
- A5_WIDTH_IN = 5.83
124
- A5_HEIGHT_IN = 8.27
125
- NUM_COLS = 3
126
 
127
- # --- Create Figures for PNG and PDF ---
128
- png_fig = plt.figure(figsize=(A5_WIDTH_IN, A5_HEIGHT_IN), dpi=300)
129
- pdf_fig = plt.figure(figsize=(A5_WIDTH_IN, A5_HEIGHT_IN), dpi=300)
130
 
131
- # --- Internal drawing function to apply changes to both figures ---
132
  def process_figure(fig):
 
133
  plt.rcParams['font.family'] = 'sans-serif'
134
  plt.rcParams['font.sans-serif'] = ['Arial Unicode MS']
135
 
136
- total_items = len(data)
137
- if total_items == 0:
138
- plt.close(fig)
139
- return
140
 
141
- # --- 1. Redesign Print Layout ---
142
- # Calculate number of rows needed
143
- num_rows = math.ceil(total_items / NUM_COLS)
 
144
 
145
- # Remove all padding from the figure edges
146
- fig.subplots_adjust(left=0, right=1, top=0.95, bottom=0)
147
 
148
- # Create a grid with no space between cells. A small top row for the date.
149
- gs = gridspec.GridSpec(
150
- num_rows + 1,
151
- NUM_COLS,
152
- hspace=0,
153
- wspace=0,
154
- height_ratios=[0.3] + [1] * num_rows, # Make date row shorter
155
- figure=fig
156
- )
157
 
 
158
  data_values = data.values.tolist()
159
-
160
- # Pad data with empty values to make it a multiple of NUM_COLS
161
- while len(data_values) % NUM_COLS != 0:
162
  data_values.append(['', ''])
163
 
164
- # --- Sort data column-first (Z-pattern) ---
165
- rows_per_col_layout = math.ceil(len(data_values) / NUM_COLS)
166
  sorted_data = [['', '']] * len(data_values)
167
  for i, item in enumerate(data_values):
168
  if item[0] and item[1]:
169
  row_in_col = i % rows_per_col_layout
170
  col_idx = i // rows_per_col_layout
171
- new_index = row_in_col * NUM_COLS + col_idx
172
  if new_index < len(sorted_data):
173
  sorted_data[new_index] = item
174
-
175
- # --- Dynamic Font Size Calculation ---
176
- def get_dynamic_fontsize(text, cell_width_inches):
177
- if not text:
178
- return 1
179
- # This factor is empirical, adjusts font size to fill ~90% of cell width
180
- # A lower factor (e.g., 0.5) results in larger text.
181
- ASPECT_RATIO_FACTOR = 0.55
182
- num_chars = len(text)
183
- # Formula: (target_width_points) / (num_characters * aspect_ratio)
184
- fontsize = (cell_width_inches * 0.9 * 72) / (num_chars * ASPECT_RATIO_FACTOR)
185
- return max(10, fontsize) # Return at least size 10
186
-
187
- cell_width_inches = A5_WIDTH_IN / NUM_COLS
188
-
189
- # --- Draw each data cell ---
 
 
 
 
 
 
 
 
 
190
  for idx, (hall, end_time) in enumerate(sorted_data):
191
  if hall and end_time:
192
- row_grid = idx // NUM_COLS + 1 # +1 to skip date row
193
- col_grid = idx % NUM_COLS
194
 
195
  ax = fig.add_subplot(gs[row_grid, col_grid])
196
-
197
- display_text = f"{hall} {end_time}"
198
-
199
- # Calculate optimal font size
200
- fontsize = get_dynamic_fontsize(display_text, cell_width_inches)
201
 
 
 
 
 
 
 
 
 
202
  ax.text(0.5, 0.5, display_text,
203
- fontsize=fontsize,
204
  fontweight='bold',
205
- ha='center',
206
- va='center',
207
  transform=ax.transAxes)
208
 
209
- # --- 2. Change Cell Border ---
210
- # Set a dotted gray border
211
- for spine in ax.spines.values():
212
- spine.set_visible(True)
213
- spine.set_linestyle((0, (1, 2))) # Dotted line: (0, (on, off))
214
- spine.set_edgecolor(BORDER_COLOR)
215
- spine.set_linewidth(1.5)
216
-
217
  ax.set_xticks([])
218
  ax.set_yticks([])
219
- ax.set_facecolor('none')
220
 
221
- # --- Add date and title information to the top row ---
222
  ax_date = fig.add_subplot(gs[0, :])
223
- ax_date.text(0.01, 0.5, f"{date_str} {title}",
224
- fontsize=12,
225
  color=DATE_COLOR,
226
  fontweight='bold',
227
- ha='left',
228
- va='center',
229
  transform=ax_date.transAxes)
230
-
231
- # Hide the border for the date cell
232
- for spine in ax_date.spines.values():
233
- spine.set_visible(False)
234
- ax_date.set_xticks([])
235
- ax_date.set_yticks([])
236
- ax_date.set_facecolor('none')
237
 
238
- # Process both the PNG and PDF figures with the new layout
239
  process_figure(png_fig)
240
  process_figure(pdf_fig)
241
 
242
- # --- Save PNG ---
243
  png_buffer = io.BytesIO()
244
- # Use pad_inches=0 because we handled margins with subplots_adjust
245
- png_fig.savefig(png_buffer, format='png', pad_inches=0)
246
  png_buffer.seek(0)
247
  png_base64 = base64.b64encode(png_buffer.getvalue()).decode()
248
  plt.close(png_fig)
249
 
250
- # --- Save PDF ---
251
  pdf_buffer = io.BytesIO()
252
  with PdfPages(pdf_buffer) as pdf:
253
- # Use pad_inches=0 for PDF as well
254
- pdf.savefig(pdf_fig, pad_inches=0)
255
  pdf_buffer.seek(0)
256
  pdf_base64 = base64.b64encode(pdf_buffer.getvalue()).decode()
257
  plt.close(pdf_fig)
@@ -261,13 +245,13 @@ def create_print_layout(data, title, date_str):
261
  'pdf': f'data:application/pdf;base64,{pdf_base64}'
262
  }
263
 
264
- # --- PDF display function ---
265
  def display_pdf(base64_pdf):
266
- """Embeds PDF in Streamlit for display"""
267
  pdf_display = f'<iframe src="{base64_pdf}" width="100%" height="800" type="application/pdf"></iframe>'
268
  return pdf_display
269
 
270
- # Streamlit UI
271
  st.set_page_config(page_title="散厅时间快捷打印", layout="wide")
272
  st.title("散厅时间快捷打印")
273
 
@@ -277,7 +261,7 @@ if uploaded_file:
277
  part1, part2, date_str = process_schedule(uploaded_file)
278
 
279
  if part1 is not None and part2 is not None:
280
- # Generate outputs containing both PNG and PDF data
281
  part1_output = create_print_layout(part1, "A", date_str)
282
  part2_output = create_print_layout(part2, "C", date_str)
283
 
@@ -286,7 +270,6 @@ if uploaded_file:
286
  with col1:
287
  st.subheader("白班散场预览(时间 ≤ 17:30)")
288
  if part1_output:
289
- # Use tabs to show both PDF and PNG previews
290
  tab1_1, tab1_2 = st.tabs(["PDF 预览", "PNG 预览"])
291
  with tab1_1:
292
  st.markdown(display_pdf(part1_output['pdf']), unsafe_allow_html=True)
@@ -298,7 +281,6 @@ if uploaded_file:
298
  with col2:
299
  st.subheader("夜班散场预览(时间 > 17:30)")
300
  if part2_output:
301
- # Use tabs to show both PDF and PNG previews
302
  tab2_1, tab2_2 = st.tabs(["PDF 预览", "PNG 预览"])
303
  with tab2_1:
304
  st.markdown(display_pdf(part2_output['pdf']), unsafe_allow_html=True)
 
7
  import matplotlib.gridspec as gridspec
8
  import math
9
  from matplotlib.backends.backend_pdf import PdfPages
 
10
 
11
  SPLIT_TIME = "17:30"
12
  BUSINESS_START = "09:30"
13
  BUSINESS_END = "01:30"
14
+ BORDER_COLOR = '#A9A9A9'
15
  DATE_COLOR = '#A9A9A9'
16
 
17
  def process_schedule(file):
 
19
  try:
20
  # 读取 Excel,跳过前 8 行
21
  df = pd.read_excel(file, skiprows=8)
22
+
23
  # 提取所需列 (G9, H9, J9)
24
  df = df.iloc[:, [6, 7, 9]] # G, H, J 列
25
  df.columns = ['Hall', 'StartTime', 'EndTime']
26
+
27
  # 清理数据
28
  df = df.dropna(subset=['Hall', 'StartTime', 'EndTime'])
29
+
30
+ # 转换影厅格式为 "#号" 格式
31
+ df['Hall'] = df['Hall'].str.extract(r'(\d+)号').astype(str) + ' '
32
+
33
  # 保存原始时间字符串用于诊断
34
  df['original_end'] = df['EndTime']
35
+
36
  # 转换时间为 datetime 对象
37
  base_date = datetime.today().date()
38
  df['StartTime'] = pd.to_datetime(df['StartTime'])
39
  df['EndTime'] = pd.to_datetime(df['EndTime'])
40
+
41
  # 设置基准时间
42
  business_start = datetime.strptime(f"{base_date} {BUSINESS_START}", "%Y-%m-%d %H:%M")
43
  business_end = datetime.strptime(f"{base_date} {BUSINESS_END}", "%Y-%m-%d %H:%M")
44
+
45
  # 处理跨天情况
46
  if business_end < business_start:
47
  business_end += timedelta(days=1)
48
+
49
  # 标准化所有时间到同一天
50
  for idx, row in df.iterrows():
51
  end_time = row['EndTime']
52
  if end_time.hour < 9:
53
  df.at[idx, 'EndTime'] = end_time + timedelta(days=1)
54
+
55
  if row['StartTime'].hour >= 21 and end_time.hour < 9:
56
  df.at[idx, 'EndTime'] = end_time + timedelta(days=1)
57
+
58
  # 筛选营业时间内的场次
59
  df['time_for_comparison'] = df['EndTime'].apply(
60
  lambda x: datetime.combine(base_date, x.time())
61
  )
62
+
63
  df.loc[df['time_for_comparison'].dt.hour < 9, 'time_for_comparison'] += timedelta(days=1)
64
+
65
  valid_times = (
66
  ((df['time_for_comparison'] >= datetime.combine(base_date, business_start.time())) &
67
  (df['time_for_comparison'] <= datetime.combine(base_date + timedelta(days=1), business_end.time())))
68
  )
69
+
70
  df = df[valid_times]
71
+
72
  # 按散场时间排序
73
  df = df.sort_values('EndTime')
74
+
75
  # 分割数据
76
+ split_time = datetime.strptime(f"{base_date} {SPLIT_TIME}", "%Y-%m-%d %H:%M")
77
+ split_time_for_comparison = df['time_for_comparison'].apply(
78
+ lambda x: datetime.combine(base_date, split_time.time())
79
+ )
80
+
81
+ part1 = df[df['time_for_comparison'] <= split_time_for_comparison].copy()
82
+ part2 = df[df['time_for_comparison'] > split_time_for_comparison].copy()
83
+
84
  # 格式化时间显示
85
  for part in [part1, part2]:
86
+ part['EndTime'] = part['EndTime'].dt.strftime('%-I:%M')
87
+
 
 
 
 
88
  # 关键修改:精确读取C6单元格
89
  date_df = pd.read_excel(
90
  file,
 
94
  header=None # 无表头
95
  )
96
  date_cell = date_df.iloc[0, 0]
97
+
98
  try:
99
  # 处理不同日期格式
100
  if isinstance(date_cell, str):
 
103
  date_str = pd.to_datetime(date_cell).strftime('%Y-%m-%d')
104
  except:
105
  date_str = datetime.today().strftime('%Y-%m-%d')
106
+
107
  return part1[['Hall', 'EndTime']], part2[['Hall', 'EndTime']], date_str
108
+
109
  except Exception as e:
110
  st.error(f"处理文件时出错: {str(e)}")
111
  return None, None, None
112
 
 
113
  def create_print_layout(data, title, date_str):
114
+ """
115
+ 创建基于精确A5网格的打印布局 (PNG 和 PDF),具有点状虚线边框和动态字体大小。
116
+ """
117
  if data.empty:
118
  return None
119
 
120
+ # A5 纸张尺寸(英寸)和 DPI
121
+ A5_WIDTH_INCHES = 5.83
122
+ A5_HEIGHT_INCHES = 8.27
123
+ DPI = 300
124
 
125
+ # PNG PDF 创建图形
126
+ png_fig = plt.figure(figsize=(A5_WIDTH_INCHES, A5_HEIGHT_INCHES), dpi=DPI)
127
+ pdf_fig = plt.figure(figsize=(A5_WIDTH_INCHES, A5_HEIGHT_INCHES), dpi=DPI)
128
 
 
129
  def process_figure(fig):
130
+ # 设置支持中文的字体
131
  plt.rcParams['font.family'] = 'sans-serif'
132
  plt.rcParams['font.sans-serif'] = ['Arial Unicode MS']
133
 
134
+ # 使网格充满整个画布,无边距
135
+ fig.subplots_adjust(left=0, right=1, top=1, bottom=0)
 
 
136
 
137
+ # 计算网格维度
138
+ total_items = len(data)
139
+ num_cols = 3
140
+ num_rows = math.ceil(total_items / num_cols)
141
 
142
+ if num_rows == 0:
143
+ return # 如果没有数据则退出
144
 
145
+ # 创建网格布局,为日期/标题行分配较小的高度
146
+ height_ratios = [0.4] + [1] * num_rows
147
+ gs = gridspec.GridSpec(num_rows + 1, num_cols,
148
+ height_ratios=height_ratios,
149
+ wspace=0, hspace=0, figure=fig)
 
 
 
 
150
 
151
+ # 准备数据,按Z字形(列优先)排序
152
  data_values = data.values.tolist()
153
+ while len(data_values) % num_cols != 0:
 
 
154
  data_values.append(['', ''])
155
 
156
+ rows_per_col_layout = math.ceil(len(data_values) / num_cols)
 
157
  sorted_data = [['', '']] * len(data_values)
158
  for i, item in enumerate(data_values):
159
  if item[0] and item[1]:
160
  row_in_col = i % rows_per_col_layout
161
  col_idx = i // rows_per_col_layout
162
+ new_index = row_in_col * num_cols + col_idx
163
  if new_index < len(sorted_data):
164
  sorted_data[new_index] = item
165
+
166
+ # --- 动态计算字体大小 ---
167
+ # 1. 找到最长的文本字符串
168
+ longest_string = ""
169
+ for hall, end_time in sorted_data:
170
+ if hall and end_time:
171
+ text = f"{hall}{end_time}"
172
+ if len(text) > len(longest_string):
173
+ longest_string = text
174
+
175
+ base_fontsize = 10 # 默认字体大小
176
+ if longest_string:
177
+ # 2. 计算单元格宽度(以磅为单位,1英寸=72磅)
178
+ fig_width_pt = fig.get_figwidth() * 72
179
+ cell_width_pt = fig_width_pt / num_cols
180
+
181
+ # 3. 目标文本宽度为单元格宽度的80%
182
+ target_text_width_pt = cell_width_pt * 0.8
183
+
184
+ # 4. 根据经验系数估算字体大小
185
+ # (字符宽度约等于字体大小的0.6倍)
186
+ char_width_factor = 0.6
187
+ base_fontsize = (target_text_width_pt / len(longest_string)) / char_width_factor
188
+
189
+ # 绘制数据单元格
190
  for idx, (hall, end_time) in enumerate(sorted_data):
191
  if hall and end_time:
192
+ row_grid = idx // num_cols + 1 # +1 是因为日期行占了第0行
193
+ col_grid = idx % num_cols
194
 
195
  ax = fig.add_subplot(gs[row_grid, col_grid])
 
 
 
 
 
196
 
197
+ # 设置灰色点状虚线边框
198
+ for spine in ax.spines.values():
199
+ spine.set_visible(True)
200
+ spine.set_color(BORDER_COLOR)
201
+ spine.set_linestyle(':') # 点状线
202
+ spine.set_linewidth(1)
203
+
204
+ display_text = f"{hall}{end_time}"
205
  ax.text(0.5, 0.5, display_text,
206
+ fontsize=base_fontsize,
207
  fontweight='bold',
208
+ ha='center', va='center',
 
209
  transform=ax.transAxes)
210
 
 
 
 
 
 
 
 
 
211
  ax.set_xticks([])
212
  ax.set_yticks([])
 
213
 
214
+ # 在顶部添加日期/标题信息
215
  ax_date = fig.add_subplot(gs[0, :])
216
+ ax_date.text(0.02, 0.5, f"{date_str} {title}",
217
+ fontsize=base_fontsize * 0.6, # 日期字体稍小
218
  color=DATE_COLOR,
219
  fontweight='bold',
220
+ ha='left', va='center',
 
221
  transform=ax_date.transAxes)
222
+ ax_date.set_axis_off()
 
 
 
 
 
 
223
 
224
+ # --- 处理并保存图形 ---
225
  process_figure(png_fig)
226
  process_figure(pdf_fig)
227
 
228
+ # 保存为 PNG
229
  png_buffer = io.BytesIO()
230
+ png_fig.savefig(png_buffer, format='png') # 移除 bbox_inches pad_inches
 
231
  png_buffer.seek(0)
232
  png_base64 = base64.b64encode(png_buffer.getvalue()).decode()
233
  plt.close(png_fig)
234
 
235
+ # 保存为 PDF
236
  pdf_buffer = io.BytesIO()
237
  with PdfPages(pdf_buffer) as pdf:
238
+ pdf.savefig(pdf_fig) # 移除 bbox_inches pad_inches
 
239
  pdf_buffer.seek(0)
240
  pdf_base64 = base64.b64encode(pdf_buffer.getvalue()).decode()
241
  plt.close(pdf_fig)
 
245
  'pdf': f'data:application/pdf;base64,{pdf_base64}'
246
  }
247
 
248
+
249
  def display_pdf(base64_pdf):
250
+ """在Streamlit中嵌入显示PDF"""
251
  pdf_display = f'<iframe src="{base64_pdf}" width="100%" height="800" type="application/pdf"></iframe>'
252
  return pdf_display
253
 
254
+ # Streamlit 界面
255
  st.set_page_config(page_title="散厅时间快捷打印", layout="wide")
256
  st.title("散厅时间快捷打印")
257
 
 
261
  part1, part2, date_str = process_schedule(uploaded_file)
262
 
263
  if part1 is not None and part2 is not None:
264
+ # 生成包含 PNG PDF 的字典
265
  part1_output = create_print_layout(part1, "A", date_str)
266
  part2_output = create_print_layout(part2, "C", date_str)
267
 
 
270
  with col1:
271
  st.subheader("白班散场预览(时间 ≤ 17:30)")
272
  if part1_output:
 
273
  tab1_1, tab1_2 = st.tabs(["PDF 预览", "PNG 预览"])
274
  with tab1_1:
275
  st.markdown(display_pdf(part1_output['pdf']), unsafe_allow_html=True)
 
281
  with col2:
282
  st.subheader("夜班散场预览(时间 > 17:30)")
283
  if part2_output:
 
284
  tab2_1, tab2_2 = st.tabs(["PDF 预览", "PNG 预览"])
285
  with tab2_1:
286
  st.markdown(display_pdf(part2_output['pdf']), unsafe_allow_html=True)