File size: 11,063 Bytes
d40b8b6
 
 
 
 
 
 
 
5422259
c924006
d40b8b6
c924006
d40b8b6
 
 
 
 
c924006
d40b8b6
 
 
 
 
 
c924006
d40b8b6
 
 
c924006
d40b8b6
 
c924006
d40b8b6
 
c924006
d40b8b6
 
c924006
d40b8b6
 
c924006
 
 
 
 
d40b8b6
c924006
 
 
 
 
 
 
 
d40b8b6
 
c924006
 
 
d40b8b6
c924006
d40b8b6
c924006
 
 
 
d40b8b6
c924006
 
 
 
d40b8b6
c924006
d40b8b6
 
c924006
d40b8b6
 
c924006
d40b8b6
 
 
c924006
 
 
d40b8b6
 
 
 
c924006
d40b8b6
c924006
 
 
 
 
 
 
d40b8b6
 
 
c924006
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d40b8b6
c924006
d40b8b6
 
 
c924006
d40b8b6
c924006
d40b8b6
 
c924006
 
d40b8b6
 
 
 
 
 
 
 
 
 
 
c924006
d40b8b6
 
 
c924006
d40b8b6
 
c924006
d40b8b6
 
c924006
 
 
 
 
d40b8b6
 
 
 
c924006
d40b8b6
 
 
 
 
 
 
 
 
 
c924006
d40b8b6
 
 
 
 
 
 
c924006
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
import pandas as pd
import streamlit as st
from datetime import datetime, timedelta
import matplotlib.pyplot as plt
import io
import base64
import matplotlib.gridspec as gridspec
import math
from matplotlib.backends.backend_pdf import PdfPages
from matplotlib.patches import Rectangle # Replaced FancyBboxPatch

# --- Constants ---
SPLIT_TIME = "17:30"
BUSINESS_START = "09:30"
BUSINESS_END = "01:30"
BORDER_COLOR = '#A9A9A9'
DATE_COLOR = '#A9A9A9'
SEQ_COLOR = '#A9A9A9' # Color for the new serial number

def process_schedule(file):
    """处理上传的 Excel 文件,生成排序和分组后的打印内容"""
    try:
        # 读取 Excel,跳过前 8 行
        df = pd.read_excel(file, skiprows=8)

        # 提取所需列 (G9, H9, J9)
        df = df.iloc[:, [6, 7, 9]]  # G, H, J 列
        df.columns = ['Hall', 'StartTime', 'EndTime']

        # 清理数据
        df = df.dropna(subset=['Hall', 'StartTime', 'EndTime'])

        # 转换影厅格式为 "#号" 格式
        df['Hall'] = df['Hall'].str.extract(r'(\d+)号').astype(str) + ' '

        # 保存原始时间字符串用于诊断
        df['original_end'] = df['EndTime']

        # 转换时间为 datetime 对象
        base_date = datetime.today().date()
        # Using errors='coerce' will turn unparseable times into NaT (Not a Time)
        df['StartTime'] = pd.to_datetime(df['StartTime'], errors='coerce')
        df['EndTime'] = pd.to_datetime(df['EndTime'], errors='coerce')
        df = df.dropna(subset=['StartTime', 'EndTime']) # Drop rows where time conversion failed

        # 设置基准时间
        business_start_time = datetime.strptime(BUSINESS_START, "%H:%M").time()
        business_end_time = datetime.strptime(BUSINESS_END, "%H:%M").time()

        # 处理跨天情况:结束时间小于开始时间,则结束时间加一天
        # This logic handles cases like 9:30 AM to 1:30 AM (next day)
        df['EndTime_adjusted'] = df.apply(
            lambda row: row['EndTime'] + timedelta(days=1) if row['EndTime'].time() < row['StartTime'].time() else row['EndTime'],
            axis=1
        )
        
        # 按散场时间排序 (using the adjusted time)
        df = df.sort_values('EndTime_adjusted')

        # 分割数据
        split_dt = datetime.strptime(SPLIT_TIME, "%H:%M").time()
        
        part1 = df[df['EndTime_adjusted'].dt.time <= split_dt].copy()
        part2 = df[df['EndTime_adjusted'].dt.time > split_dt].copy()

        # 格式化时间显示 (use original EndTime for display)
        for part in [part1, part2]:
            part['EndTime_formatted'] = part['EndTime'].dt.strftime('%-I:%M')

        # 读取日期单元格 C6
        date_df = pd.read_excel(file, skiprows=5, nrows=1, usecols=[2], header=None)
        date_cell = date_df.iloc[0, 0]

        try:
            if isinstance(date_cell, str):
                # Assuming format like '2023-10-27'
                date_str = datetime.strptime(date_cell, '%Y-%m-%d').strftime('%Y-%m-%d')
            else:
                # Assuming it's a datetime object
                date_str = pd.to_datetime(date_cell).strftime('%Y-%m-%d')
        except:
            date_str = datetime.today().strftime('%Y-%m-%d')

        return part1[['Hall', 'EndTime_formatted']], part2[['Hall', 'EndTime_formatted']], date_str

    except Exception as e:
        st.error(f"处理文件时出错: {str(e)}")
        return None, None, None


def create_print_layout(data, title, date_str):
    """
    创建符合新要求的打印布局 (PNG 和 PDF)。
    1. 动态计算边距。
    2. 使用灰色虚线圆点作为单元格边框。
    3. 单元格内容区域为单元格的90%。
    4. 在左上角添加灰色序号。
    """
    if data.empty:
        return None

    # --- Constants ---
    A5_WIDTH_IN = 5.83
    A5_HEIGHT_IN = 8.27
    DPI = 300
    NUM_COLS = 3

    # --- Setup Figure ---
    fig = plt.figure(figsize=(A5_WIDTH_IN, A5_HEIGHT_IN), dpi=DPI)
    
    # --- Font Setup ---
    plt.rcParams['font.family'] = 'sans-serif'
    plt.rcParams['font.sans-serif'] = ['Arial Unicode MS', 'Heiti TC', 'sans-serif']


    # --- Data Preparation ---
    total_items = len(data)
    # Augment data with an original index for numbering
    data_values_with_index = [(i, row) for i, row in enumerate(data.values.tolist())]
    
    # Pad data to be a multiple of NUM_COLS
    padded_total = math.ceil(total_items / NUM_COLS) * NUM_COLS
    while len(data_values_with_index) < padded_total:
        data_values_with_index.append((None, ['', '']))

    num_rows = padded_total // NUM_COLS

    # --- Layout Calculation (Request 1) ---
    if num_rows > 0:
        # "A5 paper height / num_rows / 4 is the padding for all sides"
        padding_in = (A5_HEIGHT_IN / num_rows / 4)
        # Cap padding to prevent it from being excessively large
        padding_in = min(padding_in, 0.5) 
    else:
        padding_in = 0.25 # Default padding if no rows

    # Convert padding to relative figure coordinates for subplots_adjust
    left_margin = padding_in / A5_WIDTH_IN
    right_margin = 1 - left_margin
    bottom_margin = padding_in / A5_HEIGHT_IN
    top_margin = 1 - bottom_margin
    
    # Adjust overall figure margins
    fig.subplots_adjust(left=left_margin, right=right_margin, top=top_margin, bottom=bottom_margin, hspace=0.4, wspace=0.4)

    # --- Grid & Font Size ---
    gs = gridspec.GridSpec(num_rows + 1, NUM_COLS, height_ratios=[0.2] + [1] * num_rows, figure=fig)

    if num_rows > 0:
        content_area_height_in = A5_HEIGHT_IN * (top_margin - bottom_margin)
        cell_height_in = content_area_height_in / num_rows * (1 - fig.subplotpars.hspace)
        base_fontsize = min(40, max(10, cell_height_in * 72 * 0.4)) # 72 pt/inch, 40% of cell height
    else:
        base_fontsize = 20

    # --- Z-Sort (Column-major) Data for Layout ---
    rows_per_col_layout = num_rows
    sorted_data = [(None, ['',''])] * padded_total
    for i, item_tuple in enumerate(data_values_with_index):
        if item_tuple[0] is not None:
            original_data_index = i # Index from the time-sorted list
            row_in_col = original_data_index % rows_per_col_layout
            col_idx = original_data_index // rows_per_col_layout
            new_grid_index = row_in_col * NUM_COLS + col_idx
            if new_grid_index < len(sorted_data):
                sorted_data[new_grid_index] = item_tuple

    # --- Drawing Logic ---
    for grid_idx, item_tuple in enumerate(sorted_data):
        original_index, (hall, end_time) = item_tuple
        
        if original_index is not None:
            row_grid = grid_idx // NUM_COLS + 1 # +1 because date is in row 0
            col_grid = grid_idx % NUM_COLS
            
            ax = fig.add_subplot(gs[row_grid, col_grid])
            ax.set_axis_off()

            # --- Cell Border (Request 2) & Content Area (Request 3) ---
            # Draw a dotted rectangle. Content will be placed inside this.
            # Making the rect slightly smaller creates a visual 90% area.
            cell_border = Rectangle((0.05, 0.05), 0.9, 0.9,
                                    edgecolor=BORDER_COLOR,
                                    facecolor='none',
                                    linestyle=(0, (1, 1.5)), # Dotted line with round caps
                                    linewidth=1,
                                    transform=ax.transAxes,
                                    clip_on=False)
            ax.add_patch(cell_border)

            # --- Cell Content ---
            display_text = f"{hall}{end_time}"
            ax.text(0.5, 0.5, display_text,
                    fontsize=base_fontsize,
                    fontweight='bold',
                    ha='center', va='center',
                    transform=ax.transAxes)

            # --- Cell Numbering (Request 4) ---
            # Serial number is original_index + 1
            ax.text(0.12, 0.82, str(original_index + 1),
                    fontsize=base_fontsize * 0.5,
                    color=SEQ_COLOR,
                    fontweight='normal',
                    ha='center', va='center',
                    transform=ax.transAxes)

    # --- Date Header ---
    ax_date = fig.add_subplot(gs[0, :])
    ax_date.set_axis_off()
    ax_date.text(0, 0.5, f"{date_str} {title}",
                 fontsize=base_fontsize * 0.6,
                 color=DATE_COLOR,
                 fontweight='bold',
                 ha='left', va='center',
                 transform=ax_date.transAxes)

    # --- Save to Buffers ---
    # Save PNG
    png_buffer = io.BytesIO()
    fig.savefig(png_buffer, format='png', bbox_inches='tight', pad_inches=0.02)
    png_buffer.seek(0)
    png_base64 = base64.b64encode(png_buffer.getvalue()).decode()

    # Save PDF
    pdf_buffer = io.BytesIO()
    fig.savefig(pdf_buffer, format='pdf', bbox_inches='tight', pad_inches=0.02)
    pdf_buffer.seek(0)
    pdf_base64 = base64.b64encode(pdf_buffer.getvalue()).decode()
    
    plt.close(fig)

    return {
        'png': f'data:image/png;base64,{png_base64}',
        'pdf': f'data:application/pdf;base64,{pdf_base64}'
    }

def display_pdf(base64_pdf):
    """在Streamlit中嵌入显示PDF"""
    pdf_display = f'<iframe src="{base64_pdf}" width="100%" height="800" type="application/pdf"></iframe>'
    return pdf_display

# --- Streamlit UI ---
st.set_page_config(page_title="散厅时间快捷打印", layout="wide")
st.title("散厅时间快捷打印")

uploaded_file = st.file_uploader("上传【放映场次核对表.xls】文件", type=["xls", "xlsx"])

if uploaded_file:
    # Use new column name 'EndTime_formatted' for display
    part1, part2, date_str = process_schedule(uploaded_file)
    if part1 is not None and part2 is not None:
        part1_data_for_layout = part1[['Hall', 'EndTime_formatted']]
        part2_data_for_layout = part2[['Hall', 'EndTime_formatted']]
        
        part1_output = create_print_layout(part1_data_for_layout, "A", date_str)
        part2_output = create_print_layout(part2_data_for_layout, "C", date_str)

        col1, col2 = st.columns(2)

        with col1:
            st.subheader("白班散场预览(散场时间 ≤ 17:30)")
            if part1_output:
                tab1_1, tab1_2 = st.tabs(["PDF 预览", "PNG 预览"])
                with tab1_1:
                    st.markdown(display_pdf(part1_output['pdf']), unsafe_allow_html=True)
                with tab1_2:
                    st.image(part1_output['png'])
            else:
                st.info("白班部分没有数据")

        with col2:
            st.subheader("夜班散场预览(散场时间 > 17:30)")
            if part2_output:
                tab2_1, tab2_2 = st.tabs(["PDF 预览", "PNG 预览"])
                with tab2_1:
                    st.markdown(display_pdf(part2_output['pdf']), unsafe_allow_html=True)
                with tab2_2:
                    st.image(part2_output['png'])
            else:
                st.info("夜班部分没有数据")