Ethscriptions commited on
Commit
d40b8b6
·
verified ·
1 Parent(s): 7123393

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +282 -0
app.py ADDED
@@ -0,0 +1,282 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+
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)
92
+ nrows=1, # 只读1行
93
+ usecols=[2], # 第三列(C列)
94
+ header=None # 无表头
95
+ )
96
+ date_cell = date_df.iloc[0, 0]
97
+
98
+ try:
99
+ # 处理不同日期格式
100
+ if isinstance(date_cell, str):
101
+ date_str = datetime.strptime(date_cell, '%Y-%m-%d').strftime('%Y-%m-%d')
102
+ else:
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
+ """创建打印布局 (PNG 和 PDF)"""
115
+ if data.empty:
116
+ return None
117
+
118
+ # --- 创建 PNG 图形 ---
119
+ png_fig = plt.figure(figsize=(5.83, 8.27), dpi=300) # A5 竖向
120
+ png_ax_container = png_fig.add_subplot(111) # 创建一个容器轴,用于隐藏外部边框
121
+ png_ax_container.set_axis_off()
122
+ png_fig.subplots_adjust(left=0.05, right=0.95, top=0.95, bottom=0.05)
123
+
124
+ # --- 创建 PDF 图形 ---
125
+ pdf_fig = plt.figure(figsize=(5.83, 8.27), dpi=300) # A5 竖向
126
+ pdf_ax_container = pdf_fig.add_subplot(111)
127
+ pdf_ax_container.set_axis_off()
128
+ pdf_fig.subplots_adjust(left=0.05, right=0.95, top=0.95, bottom=0.05)
129
+
130
+ # --- 内部绘图函数 ---
131
+ def process_figure(fig, is_pdf=False):
132
+ # 设置字体
133
+ plt.rcParams['font.family'] = 'sans-serif'
134
+ plt.rcParams['font.sans-serif'] = ['Arial Unicode MS'] # 确保字体可用
135
+
136
+ # 计算行数和总数
137
+ total_items = len(data)
138
+ num_cols = 3
139
+ num_rows = math.ceil(total_items / num_cols)
140
+
141
+ # 创建网格 (在 figure 内部创建)
142
+ gs = gridspec.GridSpec(num_rows + 1, num_cols, hspace=0.1, wspace=0.1, height_ratios=[0.2] + [1] * num_rows, figure=fig) # 将日期行放在顶部
143
+
144
+ # 调整基础字体大小,避免过大或过小
145
+ # A5 宽度大约 1749 像素 @ 300dpi, 高度 2481
146
+ # 每列宽度约 1749 * 0.9 / 3 = 525 像素
147
+ # 每行高度约 (2481 * 0.9 * (1 / (1.2))) / num_rows
148
+ # 字体大小与单元格大小相关,这里用经验值调整
149
+ available_height_per_row = (8.27 * 0.9 * (1 / 1.2)) / num_rows if num_rows > 0 else 1
150
+ base_fontsize = min(40, max(10, available_height_per_row * 72 * 0.5)) # 72 points per inch, 估算系数
151
+
152
+ data_values = data.values.tolist()
153
+
154
+ # 补全空位,确保是3的倍数
155
+ while len(data_values) % num_cols != 0:
156
+ data_values.append(['', ''])
157
+
158
+ rows_per_col_layout = math.ceil(len(data_values) / num_cols) # 按列优先排列的行数
159
+
160
+ # 按列优先排序数据 (Z字形)
161
+ sorted_data = [['', '']] * len(data_values)
162
+ for i, item in enumerate(data_values):
163
+ if item[0] and item[1]:
164
+ row_in_col = i % rows_per_col_layout
165
+ col_idx = i // rows_per_col_layout
166
+ new_index = row_in_col * num_cols + col_idx
167
+ if new_index < len(sorted_data):
168
+ sorted_data[new_index] = item
169
+
170
+ # 绘制数据单元格
171
+ for idx, (hall, end_time) in enumerate(sorted_data):
172
+ if hall and end_time:
173
+ row_grid = idx // num_cols + 1 # +1 因为日期占了第0行
174
+ col_grid = idx % num_cols
175
+
176
+ if row_grid < num_rows + 1: # 确保索引在网格内
177
+ ax = fig.add_subplot(gs[row_grid, col_grid]) # 使用 fig.add_subplot
178
+
179
+ for spine in ax.spines.values():
180
+ spine.set_color(BORDER_COLOR)
181
+ spine.set_linewidth(0.5)
182
+
183
+ display_text = f"{hall}{end_time}"
184
+ ax.text(0.5, 0.5, display_text,
185
+ fontsize=base_fontsize,
186
+ fontweight='bold',
187
+ ha='center',
188
+ va='center',
189
+ transform=ax.transAxes) # 使用相对坐标
190
+
191
+ ax.set_xticks([])
192
+ ax.set_yticks([])
193
+ else:
194
+ print(f"Warning: Index out of bounds - idx={idx}, row_grid={row_grid}, col_grid={col_grid}")
195
+
196
+
197
+ # 添加日期信息到第一个子图的顶部
198
+ ax_date = fig.add_subplot(gs[0, :]) # 跨越第一行的所有列
199
+ ax_date.text(0.01, 0.5, f"{date_str} {title}", # 调整位置和对齐
200
+ fontsize=base_fontsize * 0.5, # 调整日期字体大小
201
+ color=DATE_COLOR,
202
+ fontweight='bold',
203
+ ha='left',
204
+ va='center',
205
+ transform=ax_date.transAxes)
206
+
207
+ for spine in ax_date.spines.values():
208
+ spine.set_visible(False)
209
+ ax_date.set_xticks([])
210
+ ax_date.set_yticks([])
211
+ ax_date.set_facecolor('none') # 使背景透明
212
+
213
+
214
+ # --- 处理图形 ---
215
+ process_figure(png_fig)
216
+ process_figure(pdf_fig, is_pdf=True)
217
+
218
+ # --- 保存 PNG ---
219
+ png_buffer = io.BytesIO()
220
+ png_fig.savefig(png_buffer, format='png', bbox_inches='tight', pad_inches=0.05)
221
+ png_buffer.seek(0)
222
+ png_base64 = base64.b64encode(png_buffer.getvalue()).decode()
223
+ plt.close(png_fig)
224
+
225
+ # --- 保存 PDF ---
226
+ pdf_buffer = io.BytesIO()
227
+ with PdfPages(pdf_buffer) as pdf:
228
+ pdf.savefig(pdf_fig, bbox_inches='tight', pad_inches=0.05)
229
+ pdf_buffer.seek(0)
230
+ pdf_base64 = base64.b64encode(pdf_buffer.getvalue()).decode()
231
+ plt.close(pdf_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
+ # --- 新增 PDF 显示函数 ---
239
+ def display_pdf(base64_pdf):
240
+ """在Streamlit中嵌入显示PDF"""
241
+ pdf_display = f'<iframe src="{base64_pdf}" width="100%" height="800" type="application/pdf"></iframe>'
242
+ return pdf_display
243
+
244
+ # Streamlit 界面
245
+ st.set_page_config(page_title="散厅时间快捷打印", layout="wide")
246
+ st.title("散厅时间快捷打印")
247
+
248
+ uploaded_file = st.file_uploader("上传【放映场次核对表.xls】文件", type=["xls"])
249
+
250
+ if uploaded_file:
251
+ part1, part2, date_str = process_schedule(uploaded_file)
252
+
253
+ if part1 is not None and part2 is not None:
254
+ # 生成包含 PNG 和 PDF 的字典
255
+ part1_output = create_print_layout(part1, "A", date_str)
256
+ part2_output = create_print_layout(part2, "C", date_str)
257
+
258
+ col1, col2 = st.columns(2)
259
+
260
+ with col1:
261
+ st.subheader("白班散场预览(时间 ≤ 17:30)")
262
+ if part1_output:
263
+ tab1_1, tab1_2 = st.tabs(["PDF 预览", "PNG 预览"])
264
+ with tab1_1:
265
+ st.markdown(display_pdf(part1_output['pdf']), unsafe_allow_html=True)
266
+ with tab1_2:
267
+ st.image(part1_output['png'])
268
+ else:
269
+ st.info("白班部分没有数据")
270
+
271
+ with col2:
272
+ st.subheader("夜班散场预览(时间 > 17:30)")
273
+ if part2_output:
274
+ tab2_1, tab2_2 = st.tabs(["PDF 预览", "PNG 预览"])
275
+ with tab2_1:
276
+ st.markdown(display_pdf(part2_output['pdf']), unsafe_allow_html=True)
277
+ with tab2_2:
278
+ st.image(part2_output['png'])
279
+ else:
280
+ st.info("夜班部分没有数据")
281
+
282
+