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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +102 -102
app.py CHANGED
@@ -8,84 +8,89 @@ 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):
18
  """处理上传的 Excel 文件,生成排序和分组后的打印内容"""
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,
91
  skiprows=5, # 跳过前5行(0-4)
@@ -94,7 +99,7 @@ def process_schedule(file):
94
  header=None # 无表头
95
  )
96
  date_cell = date_df.iloc[0, 0]
97
-
98
  try:
99
  # 处理不同日期格式
100
  if isinstance(date_cell, str):
@@ -103,104 +108,97 @@ def process_schedule(file):
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,
@@ -210,35 +208,37 @@ def create_print_layout(data, title, date_str):
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)
 
242
 
243
  return {
244
  'png': f'data:image/png;base64,{png_base64}',
@@ -251,7 +251,7 @@ def display_pdf(base64_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
 
 
8
  import math
9
  from matplotlib.backends.backend_pdf import PdfPages
10
 
11
+ # Constants
12
  SPLIT_TIME = "17:30"
13
  BUSINESS_START = "09:30"
14
  BUSINESS_END = "01:30"
15
+ BORDER_COLOR = 'grey' # Changed to grey for the new border
16
  DATE_COLOR = '#A9A9A9'
17
+ A5_WIDTH_IN = 5.83
18
+ A5_HEIGHT_IN = 8.27
19
+ NUM_COLS = 3
20
+
21
 
22
  def process_schedule(file):
23
  """处理上传的 Excel 文件,生成排序和分组后的打印内容"""
24
  try:
25
  # 读取 Excel,跳过前 8 行
26
  df = pd.read_excel(file, skiprows=8)
27
+
28
  # 提取所需列 (G9, H9, J9)
29
  df = df.iloc[:, [6, 7, 9]] # G, H, J 列
30
  df.columns = ['Hall', 'StartTime', 'EndTime']
31
+
32
  # 清理数据
33
  df = df.dropna(subset=['Hall', 'StartTime', 'EndTime'])
34
+
35
  # 转换影厅格式为 "#号" 格式
36
  df['Hall'] = df['Hall'].str.extract(r'(\d+)号').astype(str) + ' '
37
+
38
  # 保存原始时间字符串用于诊断
39
  df['original_end'] = df['EndTime']
40
+
41
  # 转换时间为 datetime 对象
42
  base_date = datetime.today().date()
43
  df['StartTime'] = pd.to_datetime(df['StartTime'])
44
  df['EndTime'] = pd.to_datetime(df['EndTime'])
45
+
46
  # 设置基准时间
47
  business_start = datetime.strptime(f"{base_date} {BUSINESS_START}", "%Y-%m-%d %H:%M")
48
  business_end = datetime.strptime(f"{base_date} {BUSINESS_END}", "%Y-%m-%d %H:%M")
49
+
50
  # 处理跨天情况
51
  if business_end < business_start:
52
  business_end += timedelta(days=1)
53
+
54
  # 标准化所有时间到同一天
55
  for idx, row in df.iterrows():
56
  end_time = row['EndTime']
57
  if end_time.hour < 9:
58
  df.at[idx, 'EndTime'] = end_time + timedelta(days=1)
59
+
60
  if row['StartTime'].hour >= 21 and end_time.hour < 9:
61
  df.at[idx, 'EndTime'] = end_time + timedelta(days=1)
62
+
63
  # 筛选营业时间内的场次
64
  df['time_for_comparison'] = df['EndTime'].apply(
65
  lambda x: datetime.combine(base_date, x.time())
66
  )
67
+
68
  df.loc[df['time_for_comparison'].dt.hour < 9, 'time_for_comparison'] += timedelta(days=1)
69
+
70
  valid_times = (
71
  ((df['time_for_comparison'] >= datetime.combine(base_date, business_start.time())) &
72
  (df['time_for_comparison'] <= datetime.combine(base_date + timedelta(days=1), business_end.time())))
73
  )
74
+
75
  df = df[valid_times]
76
+
77
  # 按散场时间排序
78
  df = df.sort_values('EndTime')
79
+
80
  # 分割数据
81
  split_time = datetime.strptime(f"{base_date} {SPLIT_TIME}", "%Y-%m-%d %H:%M")
82
  split_time_for_comparison = df['time_for_comparison'].apply(
83
  lambda x: datetime.combine(base_date, split_time.time())
84
  )
85
+
86
  part1 = df[df['time_for_comparison'] <= split_time_for_comparison].copy()
87
  part2 = df[df['time_for_comparison'] > split_time_for_comparison].copy()
88
+
89
  # 格式化时间显示
90
  for part in [part1, part2]:
91
+ part['EndTime'] = part['EndTime'].dt.strftime('%-H:%M')
92
+
93
+ # 精确读取C6单元格
94
  date_df = pd.read_excel(
95
  file,
96
  skiprows=5, # 跳过前5行(0-4)
 
99
  header=None # 无表头
100
  )
101
  date_cell = date_df.iloc[0, 0]
102
+
103
  try:
104
  # 处理不同日期格式
105
  if isinstance(date_cell, str):
 
108
  date_str = pd.to_datetime(date_cell).strftime('%Y-%m-%d')
109
  except:
110
  date_str = datetime.today().strftime('%Y-%m-%d')
111
+
112
  return part1[['Hall', 'EndTime']], part2[['Hall', 'EndTime']], date_str
113
+
114
  except Exception as e:
115
  st.error(f"处理文件时出错: {str(e)}")
116
  return None, None, None
117
 
118
+
119
  def create_print_layout(data, title, date_str):
120
+ """创建精确的 A5 表格打印布局 (PNG 和 PDF)"""
 
 
121
  if data.empty:
122
  return None
123
 
124
+ # --- 内部绘图函数 ---
125
+ def generate_figure():
126
+ # --- 1. 计算布局和字体大小 ---
127
+ total_items = len(data)
128
+ num_rows = math.ceil(total_items / NUM_COLS) if total_items > 0 else 1
129
 
130
+ # 定义日期标题行的高度(英寸),数据行将填充剩余空间
131
+ date_header_height_in = 0.3
132
+ data_area_height_in = A5_HEIGHT_IN - date_header_height_in
133
 
134
+ # 计算每个数据单元格的尺寸(英寸)
135
+ cell_width_in = A5_WIDTH_IN / NUM_COLS
136
+ cell_height_in = data_area_height_in / num_rows
137
+
138
+ # 将单元格宽度转换为点(1 英寸 = 72 点)
139
+ cell_width_pt = cell_width_in * 72
140
+ cell_height_pt = cell_height_in * 72
141
 
142
+ # --- 动态字体大小计算 ---
143
+ # 目标:文本总宽度为单元格宽度的 90%
144
+ target_text_width_pt = cell_width_pt * 0.9
145
+ # 启发式估算:假设最长文本为 "10 23:59" (8个字符),平均字符宽度约为字体大小的0.6倍
146
+ # FONT_SIZE = target_width / (num_chars * avg_char_width_factor)
147
+ fontsize_from_width = target_text_width_pt / (8 * 0.6)
148
+ # 字体高度不能超过单元格高度(留出20%的垂直边距)
149
+ fontsize_from_height = cell_height_pt * 0.8
150
+ # 选择两者中较小的一个,以确保文本能完全容纳
151
+ base_fontsize = min(fontsize_from_width, fontsize_from_height)
152
+
153
+ # --- 2. 创建图形和网格 ---
154
+ fig = plt.figure(figsize=(A5_WIDTH_IN, A5_HEIGHT_IN), dpi=300)
155
+ # 设置无边距,让网格填满整个图纸
156
  fig.subplots_adjust(left=0, right=1, top=1, bottom=0)
157
 
158
+ # 设置字体
159
+ plt.rcParams['font.family'] = 'sans-serif'
160
+ plt.rcParams['font.sans-serif'] = ['Arial Unicode MS'] # 确保字体可用
161
+
162
+ # 创建网格,顶部为日期行,下方为数据行
163
+ # 使用高度(英寸)作为比率,GridSpec会自动归一化
164
+ gs = gridspec.GridSpec(
165
+ num_rows + 1, NUM_COLS,
166
+ hspace=0, wspace=0, # 无单元格间距
167
+ height_ratios=[date_header_height_in] + [cell_height_in] * num_rows,
168
+ figure=fig
169
+ )
170
+
171
+ # --- 3. 补全和排序数据 ---
 
172
  data_values = data.values.tolist()
173
+ while len(data_values) % NUM_COLS != 0:
174
  data_values.append(['', ''])
175
 
176
+ rows_per_col_layout = math.ceil(len(data_values) / NUM_COLS)
177
  sorted_data = [['', '']] * len(data_values)
178
  for i, item in enumerate(data_values):
179
  if item[0] and item[1]:
180
  row_in_col = i % rows_per_col_layout
181
  col_idx = i // rows_per_col_layout
182
+ new_index = row_in_col * NUM_COLS + col_idx
183
  if new_index < len(sorted_data):
184
  sorted_data[new_index] = item
185
+
186
+ # --- 4. 绘制数据单元格 ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
187
  for idx, (hall, end_time) in enumerate(sorted_data):
188
  if hall and end_time:
189
+ row_grid = idx // NUM_COLS + 1 # +1 因为日期行占了第0行
190
+ col_grid = idx % NUM_COLS
191
+
192
  ax = fig.add_subplot(gs[row_grid, col_grid])
193
 
194
+ # --- 设置点状虚线边框 ---
195
  for spine in ax.spines.values():
196
  spine.set_visible(True)
197
+ spine.set_linestyle((0, (1, 2))) # 点状线: (offset, (on_length, off_length))
198
  spine.set_color(BORDER_COLOR)
199
+ spine.set_linewidth(0.75) # 点状线可能需要稍粗一点才清晰
 
200
 
201
+ # 绘制居中对齐的文本
202
  display_text = f"{hall}{end_time}"
203
  ax.text(0.5, 0.5, display_text,
204
  fontsize=base_fontsize,
 
208
 
209
  ax.set_xticks([])
210
  ax.set_yticks([])
211
+ ax.set_facecolor('none')
212
 
213
+ # --- 5. 绘制日期标题 ---
214
  ax_date = fig.add_subplot(gs[0, :])
215
+ ax_date.text(0.01, 0.5, f"{date_str} {title}",
216
+ fontsize=base_fontsize * 0.5, # 日期字体稍小
217
+ color=DATE_COLOR, fontweight='bold',
 
218
  ha='left', va='center',
219
  transform=ax_date.transAxes)
220
+ ax_date.set_axis_off() # 完全隐藏日期行的边框和刻度
221
+ ax_date.set_facecolor('none')
222
 
223
+ return fig
224
+
225
+ # --- 生成并保存图形 ---
226
+ fig_for_output = generate_figure()
227
 
228
  # 保存为 PNG
229
  png_buffer = io.BytesIO()
230
+ fig_for_output.savefig(png_buffer, format='png') # 无需 bbox_inches='tight'
231
  png_buffer.seek(0)
232
  png_base64 = base64.b64encode(png_buffer.getvalue()).decode()
 
233
 
234
  # 保存为 PDF
235
  pdf_buffer = io.BytesIO()
236
  with PdfPages(pdf_buffer) as pdf:
237
+ pdf.savefig(fig_for_output) # 无需 bbox_inches='tight'
238
  pdf_buffer.seek(0)
239
  pdf_base64 = base64.b64encode(pdf_buffer.getvalue()).decode()
240
+
241
+ plt.close(fig_for_output) # 关闭图形,释放内存
242
 
243
  return {
244
  'png': f'data:image/png;base64,{png_base64}',
 
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