baoshiwei
2026-01-20 faa25a85c10aa0fa2df824318a4bfa542f6a5a46
feat(services): 添加主流程服务并重构时间处理逻辑

重构数据查询服务和挤出机服务,添加时区偏移处理
新增MainProcessService用于处理主流程数据查询
优化数据清洗服务中的类型检查和时区处理
移除旧版仪表盘代码,新增分页式仪表盘实现
已添加5个文件
已修改4个文件
已删除1个文件
2358 ■■■■■ 文件已修改
app/dashboard/extruder_dashboard.py 161 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/pages/comprehensive_dashboard.py 329 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/pages/extruder_dashboard.py 344 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/pages/main_process_dashboard.py 172 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/pages/sorting_dashboard.py 416 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/services/data_processing_service.py 47 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/services/data_query_service.py 20 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/services/extruder_service.py 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/services/main_process_service.py 88 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
dashboard.py 758 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/dashboard/extruder_dashboard.py
ÎļþÒÑɾ³ý
app/pages/comprehensive_dashboard.py
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,329 @@
import streamlit as st
import plotly.express as px
import plotly.graph_objects as go
import pandas as pd
from datetime import datetime, timedelta
from app.services.data_query_service import DataQueryService
from app.services.extruder_service import ExtruderService
from app.services.main_process_service import MainProcessService
def show_comprehensive_dashboard():
    # åˆå§‹åŒ–服务
    sorting_service = DataQueryService()
    extruder_service = ExtruderService()
    main_process_service = MainProcessService()
    # é¡µé¢æ ‡é¢˜
    st.title("多维综合分析")
    # åˆå§‹åŒ–会话状态用于日期同步
    if 'comp_start_date' not in st.session_state:
        st.session_state['comp_start_date'] = datetime.now().date() - timedelta(days=7)
    if 'comp_end_date' not in st.session_state:
        st.session_state['comp_end_date'] = datetime.now().date()
    if 'comp_quick_select' not in st.session_state:
        st.session_state['comp_quick_select'] = "最近7天"
    # å®šä¹‰å›žè°ƒå‡½æ•°
    def update_dates(qs):
        st.session_state['comp_quick_select'] = qs
        today = datetime.now().date()
        if qs == "今天":
            st.session_state['comp_start_date'] = today
            st.session_state['comp_end_date'] = today
        elif qs == "最近3天":
            st.session_state['comp_start_date'] = today - timedelta(days=3)
            st.session_state['comp_end_date'] = today
        elif qs == "最近7天":
            st.session_state['comp_start_date'] = today - timedelta(days=7)
            st.session_state['comp_end_date'] = today
        elif qs == "最近30天":
            st.session_state['comp_start_date'] = today - timedelta(days=30)
            st.session_state['comp_end_date'] = today
    def on_date_change():
        st.session_state['comp_quick_select'] = "自定义"
    # æŸ¥è¯¢æ¡ä»¶åŒºåŸŸ
    with st.expander("🔍 æŸ¥è¯¢é…ç½®", expanded=True):
        # æ·»åŠ è‡ªå®šä¹‰ CSS å®žçŽ°å“åº”å¼æ¢è¡Œ
        st.markdown("""
            <style>
            /* å¼ºåˆ¶åˆ—容器换行 */
            [data-testid="stExpander"] [data-testid="column"] {
                flex: 1 1 120px !important;
                min-width: 120px !important;
            }
            /* é’ˆå¯¹æ—¥æœŸè¾“入框列稍微加宽一点 */
            @media (min-width: 768px) {
                [data-testid="stExpander"] [data-testid="column"]:nth-child(6),
                [data-testid="stExpander"] [data-testid="column"]:nth-child(7) {
                    flex: 2 1 180px !important;
                    min-width: 180px !important;
                }
            }
            </style>
            """, unsafe_allow_html=True)
        # åˆ›å»ºå¸ƒå±€
        cols = st.columns([1, 1, 1, 1, 1, 1.5, 1.5, 1])
        options = ["今天", "最近3天", "最近7天", "最近30天", "自定义"]
        for i, option in enumerate(options):
            with cols[i]:
                # æ ¹æ®å½“前选择状态决定按钮类型
                button_type = "primary" if st.session_state['comp_quick_select'] == option else "secondary"
                if st.button(option, key=f"btn_comp_{option}", width='stretch', type=button_type):
                    update_dates(option)
                    st.rerun()
        with cols[5]:
            start_date = st.date_input(
                "开始日期",
                label_visibility="collapsed",
                key="comp_start_date",
                on_change=on_date_change
            )
        with cols[6]:
            end_date = st.date_input(
                "结束日期",
                label_visibility="collapsed",
                key="comp_end_date",
                on_change=on_date_change
            )
        with cols[7]:
            query_button = st.button("🚀 æŸ¥è¯¢", key="comp_query", width='stretch')
    # è½¬æ¢ä¸ºdatetime对象
    start_dt = datetime.combine(start_date, datetime.min.time())
    end_dt = datetime.combine(end_date, datetime.max.time())
    # æŸ¥è¯¢å¤„理
    if query_button:
        with st.spinner("正在聚合多源数据..."):
            # 1. èŽ·å–åˆ†æ‹£ç£…ç§¤æ•°æ®
            df_sorting = sorting_service.get_sorting_scale_data(start_dt, end_dt)
            # 2. èŽ·å–æŒ¤å‡ºæœºæ•°æ®
            df_extruder = extruder_service.get_extruder_data(start_dt, end_dt)
            # 3. èŽ·å–ä¸»æµç¨‹æŽ§åˆ¶æ•°æ®
            df_main_speed = main_process_service.get_cutting_setting_data(start_dt, end_dt)
            df_temp = main_process_service.get_temperature_control_data(start_dt, end_dt)
            # æ£€æŸ¥æ˜¯å¦æœ‰æ•°æ®
            has_data = any([
                df_sorting is not None and not df_sorting.empty,
                df_extruder is not None and not df_extruder.empty,
                df_main_speed is not None and not df_main_speed.empty,
                df_temp is not None and not df_temp.empty
            ])
            if not has_data:
                st.warning("所选时间段内未找到任何数据,请尝试调整查询条件。")
                return
            # åˆ›å»ºç»¼åˆåˆ†æžå›¾è¡¨
            fig = go.Figure()
            # æ·»åŠ åˆ†æ‹£ç£…ç§¤é‡é‡ (基图)
            if df_sorting is not None and not df_sorting.empty:
                # æ£€æŸ¥æ˜¯å¦åŒ…含阈值相关字段
                has_thresholds = all(col in df_sorting.columns for col in ['over_difference', 'under_difference'])
                if has_thresholds:
                    # åˆ†ç¦»æ­£å¸¸å’Œå¼‚常数据点
                    # å¤åˆ¶æ•°æ®ä»¥é¿å…ä¿®æ”¹åŽŸå§‹æ•°æ®ï¼Œå¹¶å¤„ç†å¯èƒ½çš„é›¶å€¼/空值
                    plot_df = df_sorting.copy()
                    is_out_of_range = (plot_df['weight'] > plot_df['over_difference']) | (plot_df['weight'] < plot_df['under_difference'])
                    normal_points = plot_df[~is_out_of_range]
                    anomaly_points = plot_df[is_out_of_range]
                    # 1.1 æ­£å¸¸é‡é‡ä»¥è“è‰²æ•°æ®ç‚¹æ˜¾ç¤º
                    if not normal_points.empty:
                        fig.add_trace(go.Scatter(
                            x=normal_points['time'],
                            y=normal_points['weight'],
                            name='分拣重量 (正常)',
                            mode='markers',
                            marker=dict(size=5, color='blue', opacity=0.7)
                        ))
                    # 1.2 å¼‚常重量以红色三角显示
                    if not anomaly_points.empty:
                        fig.add_trace(go.Scatter(
                            x=anomaly_points['time'],
                            y=anomaly_points['weight'],
                            name='分拣重量 (异常)',
                            mode='markers',
                            marker=dict(
                                size=7,
                                color='red',
                                symbol='triangle-up',
                                line=dict(width=1, color='darkred')
                            )
                        ))
                else:
                    # å¦‚果没有阈值,则全部显示为普通蓝色点
                    fig.add_trace(go.Scatter(
                        x=df_sorting['time'],
                        y=df_sorting['weight'],
                        name='分拣重量 (kg)',
                        mode='markers',
                        marker=dict(size=5, color='blue', opacity=0.7)
                    ))
                # 2. æ˜¾ç¤ºä¸Šä¸‹é™å’Œæ ‡å‡†å€¼çš„æ›²çº¿
                if 'baseline_value' in df_sorting.columns:
                     fig.add_trace(go.Scatter(
                         x=df_sorting['time'],
                         y=df_sorting['baseline_value'],
                         name='基准值',
                         mode='lines',
                         line=dict(color='green', width=2),
                         opacity=0.6
                     ))
                if 'over_difference' in df_sorting.columns:
                    fig.add_trace(go.Scatter(
                        x=df_sorting['time'],
                        y=df_sorting['over_difference'],
                        name='上限阈值',
                        mode='lines',
                        line=dict(color='red', width=1.5),
                        opacity=0.5
                    ))
                if 'under_difference' in df_sorting.columns:
                    fig.add_trace(go.Scatter(
                        x=df_sorting['time'],
                        y=df_sorting['under_difference'],
                        name='下限阈值',
                        mode='lines',
                        line=dict(color='orange', width=1.5),
                        opacity=0.5
                    ))
            # æ·»åŠ æŒ¤å‡ºæœºç±³é‡
            if df_extruder is not None and not df_extruder.empty:
                fig.add_trace(go.Scatter(
                    x=df_extruder['time'],
                    y=df_extruder['metered_weight'],
                    name='挤出机米重 (g/m)',
                    mode='lines',
                    line=dict(color='green', width=1.5),
                    yaxis='y2'
                ))
                # æ·»åŠ æŒ¤å‡ºæœºå®žé™…è½¬é€Ÿ
                fig.add_trace(go.Scatter(
                    x=df_extruder['time'],
                    y=df_extruder['screw_speed_actual'],
                    name='挤出机实际转速 (RPM)',
                    mode='lines',
                    line=dict(color='orange', width=1.5),
                    yaxis='y3'
                ))
            # æ·»åŠ æµç¨‹ä¸»é€Ÿ
            if df_main_speed is not None and not df_main_speed.empty:
                fig.add_trace(go.Scatter(
                    x=df_main_speed['time'],
                    y=df_main_speed['process_main_speed'],
                    name='流程主速 (M/Min)',
                    mode='lines',
                    line=dict(color='red', width=1.5),
                    yaxis='y3' # å…±ç”¨é€Ÿåº¦è½´
                ))
            # æ·»åŠ æ¸©åº¦è®¾å®šå€¼
            if df_temp is not None and not df_temp.empty:
                temp_fields = {
                    'nakata_extruder_screw_set_temp': '螺杆设定 (°C)',
                    'nakata_extruder_rear_barrel_set_temp': '后机筒设定 (°C)',
                    'nakata_extruder_front_barrel_set_temp': '前机筒设定 (°C)',
                    'nakata_extruder_head_set_temp': '机头设定 (°C)'
                }
                colors = ['#FF4B4B', '#FF8C00', '#FFD700', '#DA70D6']
                for i, (field, label) in enumerate(temp_fields.items()):
                    fig.add_trace(go.Scatter(
                        x=df_temp['time'],
                        y=df_temp[field],
                        name=label,
                        mode='lines',
                        line=dict(width=1),
                        yaxis='y4'
                    ))
            # è®¾ç½®å¤šåæ ‡è½´å¸ƒå±€
            fig.update_layout(
                title='多维综合趋势分析',
                xaxis=dict(
                    title='时间',
                    rangeslider=dict(visible=True),
                    type='date'
                ),
                yaxis=dict(
                    title='重量 (kg)',
                    title_font=dict(color='blue'),
                    tickfont=dict(color='blue')
                ),
                yaxis2=dict(
                    title='米重 (g/m)',
                    title_font=dict(color='green'),
                    tickfont=dict(color='green'),
                    overlaying='y',
                    side='right'
                ),
                yaxis3=dict(
                    title='速度 (RPM / M/Min)',
                    title_font=dict(color='red'),
                    tickfont=dict(color='red'),
                    overlaying='y',
                    side='right',
                    anchor='free',
                    position=0.85
                ),
                yaxis4=dict(
                    title='温度 (°C)',
                    title_font=dict(color='purple'),
                    tickfont=dict(color='purple'),
                    overlaying='y',
                    side='left',
                    anchor='free',
                    position=0.15
                ),
                legend=dict(
                    orientation="h",
                    yanchor="bottom",
                    y=1.02,
                    xanchor="right",
                    x=1
                ),
                height=700,
                margin=dict(l=100, r=100, t=100, b=100),
                hovermode='x unified'
            )
            # æ˜¾ç¤ºå›¾è¡¨
            st.plotly_chart(fig, width='stretch', config={'scrollZoom': True})
            # æ•°æ®æ‘˜è¦
            st.subheader("📊 æ•°æ®æ‘˜è¦")
            summary_cols = st.columns(4)
            with summary_cols[0]:
                if df_sorting is not None and not df_sorting.empty:
                    st.metric("平均重量", f"{df_sorting['weight'].mean():.2f} kg")
            with summary_cols[1]:
                if df_extruder is not None and not df_extruder.empty:
                    st.metric("平均米重", f"{df_extruder['metered_weight'].mean():.2f} g/m")
            with summary_cols[2]:
                if df_main_speed is not None and not df_main_speed.empty:
                    st.metric("平均主速", f"{df_main_speed['process_main_speed'].mean():.2f} M/Min")
            with summary_cols[3]:
                if df_temp is not None and not df_temp.empty:
                    st.metric("平均螺杆温控", f"{df_temp['nakata_extruder_screw_set_temp'].mean():.1f} Â°C")
app/pages/extruder_dashboard.py
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,344 @@
import streamlit as st
import plotly.express as px
import plotly.graph_objects as go
import pandas as pd
from datetime import datetime, timedelta
from app.services.extruder_service import ExtruderService
from app.services.data_processing_service import DataProcessingService
def show_extruder_dashboard():
    # åˆå§‹åŒ–服务
    extruder_service = ExtruderService()
    processing_service = DataProcessingService()
    # é¡µé¢æ ‡é¢˜
    st.title("挤出机数据分析")
    # åˆå§‹åŒ–会话状态用于日期同步
    if 'extruder_start_date' not in st.session_state:
        st.session_state['extruder_start_date'] = datetime.now().date() - timedelta(days=7)
    if 'extruder_end_date' not in st.session_state:
        st.session_state['extruder_end_date'] = datetime.now().date()
    if 'extruder_quick_select' not in st.session_state:
        st.session_state['extruder_quick_select'] = "最近7天"
    # å®šä¹‰å›žè°ƒå‡½æ•°
    def update_dates(qs):
        st.session_state['extruder_quick_select'] = qs
        today = datetime.now().date()
        if qs == "今天":
            st.session_state['extruder_start_date'] = today
            st.session_state['extruder_end_date'] = today
        elif qs == "最近3天":
            st.session_state['extruder_start_date'] = today - timedelta(days=3)
            st.session_state['extruder_end_date'] = today
        elif qs == "最近7天":
            st.session_state['extruder_start_date'] = today - timedelta(days=7)
            st.session_state['extruder_end_date'] = today
        elif qs == "最近30天":
            st.session_state['extruder_start_date'] = today - timedelta(days=30)
            st.session_state['extruder_end_date'] = today
    def on_date_change():
        st.session_state['extruder_quick_select'] = "自定义"
    # æŸ¥è¯¢æ¡ä»¶åŒºåŸŸ
    with st.expander("🔍 æŸ¥è¯¢é…ç½®", expanded=True):
        # æ·»åŠ è‡ªå®šä¹‰ CSS å®žçŽ°å“åº”å¼æ¢è¡Œ
        st.markdown("""
            <style>
            /* å¼ºåˆ¶åˆ—容器换行 */
            [data-testid="stExpander"] [data-testid="column"] {
                flex: 1 1 120px !important;
                min-width: 120px !important;
            }
            /* é’ˆå¯¹æ—¥æœŸè¾“入框列稍微加宽一点 */
            @media (min-width: 768px) {
                [data-testid="stExpander"] [data-testid="column"]:nth-child(6),
                [data-testid="stExpander"] [data-testid="column"]:nth-child(7) {
                    flex: 2 1 180px !important;
                    min-width: 180px !important;
                }
            }
            </style>
            """, unsafe_allow_html=True)
        # åˆ›å»ºå¸ƒå±€
        cols = st.columns([1, 1, 1, 1, 1, 1.5, 1.5, 1])
        options = ["今天", "最近3天", "最近7天", "最近30天", "自定义"]
        for i, option in enumerate(options):
            with cols[i]:
                # æ ¹æ®å½“前选择状态决定按钮类型
                button_type = "primary" if st.session_state['extruder_quick_select'] == option else "secondary"
                if st.button(option, key=f"btn_extruder_{option}", width='stretch', type=button_type):
                    update_dates(option)
                    st.rerun()
        with cols[5]:
            start_date = st.date_input(
                "开始日期",
                label_visibility="collapsed",
                key="extruder_start_date",
                on_change=on_date_change
            )
        with cols[6]:
            end_date = st.date_input(
                "结束日期",
                label_visibility="collapsed",
                key="extruder_end_date",
                on_change=on_date_change
            )
        with cols[7]:
            query_button = st.button("🚀 æŸ¥è¯¢", key="extruder_query", width='stretch')
    # è½¬æ¢ä¸ºdatetime对象(包含时间)
    start_datetime = datetime.combine(start_date, datetime.min.time())
    end_datetime = datetime.combine(end_date, datetime.max.time())
    # æŸ¥è¯¢æŒ‰é’®å¤„理
    if query_button:
        # éªŒè¯æ—¥æœŸèŒƒå›´
        if start_datetime > end_datetime:
            st.error("开始日期不能晚于结束日期!")
        else:
            # æ˜¾ç¤ºåŠ è½½çŠ¶æ€
            with st.spinner("正在查询数据..."):
                # æŸ¥è¯¢æ•°æ®
                raw_data = extruder_service.get_extruder_data(start_datetime, end_datetime)
                if raw_data is None or raw_data.empty:
                    st.warning("未查询到数据,请检查日期范围或数据库连接!")
                    st.session_state['extruder_results'] = None
                else:
                    # æ¸…洗数据
                    cleaned_data = processing_service.clean_data(raw_data)
                    # æ£€æµ‹æ¢æ‰¹äº‹ä»¶
                    batch_changes = extruder_service.detect_batch_changes(cleaned_data)
                    # ç¼“存结果
                    st.session_state['extruder_results'] = {
                        'cleaned_data': cleaned_data,
                        'batch_changes': batch_changes,
                    }
                    # æ˜¾ç¤ºæ•°æ®æ¦‚览
                    st.subheader("数据概览")
                    col1, col2, col3, col4 = st.columns(4)
                    with col1:
                        st.metric("总记录数", len(cleaned_data))
                    with col2:
                        st.metric("换批次数", len(batch_changes))
                    with col3:
                        st.metric("数据时间范围", f"{cleaned_data['time'].min()} è‡³ {cleaned_data['time'].max()}")
                    # æ˜¾ç¤ºæ¢æ‰¹åˆ†æž
                    st.subheader("换批分析")
                    if not batch_changes.empty:
                        # å‡†å¤‡å±•示数据
                        batch_display = batch_changes[['batch_id', 'compound_code', 'start_time', 'end_time', 'duration_minutes']].copy()
                        # æ ¼å¼åŒ–æ—¶é—´
                        batch_display['start_time'] = batch_display['start_time'].dt.strftime('%Y-%m-%d %H:%M:%S')
                        batch_display['end_time'] = batch_display['end_time'].dt.strftime('%Y-%m-%d %H:%M:%S')
                        # ä¿®æ”¹åˆ—名
                        batch_display.columns = ['批号', '胶料号', '开始时间', '结束时间', '持续时长(分钟)']
                        # æ˜¾ç¤ºæ•°æ®è¡¨æ ¼
                        st.dataframe(batch_display, use_container_width=True)
                    else:
                        st.warning("未检测到换批事件")
                    # æ˜¾ç¤ºæ¢æ–™æ“ä½œå¯è§†åŒ–图表
                    st.subheader("换料操作可视化")
                    if not batch_changes.empty:
                        # åˆ›å»ºæ¢æ–™æ“ä½œå¯è§†åŒ–图表
                        fig = go.Figure()
                        # æ·»åŠ å…³é”®å‚æ•°è¶‹åŠ¿çº¿
                        fig.add_trace(go.Scatter(
                            x=cleaned_data['time'],
                            y=cleaned_data['screw_speed_actual'],
                            name='实际转速',
                            line=dict(color='blue', width=2),
                            opacity=0.8
                        ))
                        fig.add_trace(go.Scatter(
                            x=cleaned_data['time'],
                            y=cleaned_data['head_pressure'],
                            name='机头压力',
                            line=dict(color='red', width=2),
                            opacity=0.8,
                            yaxis='y2'
                        ))
                        fig.add_trace(go.Scatter(
                            x=cleaned_data['time'],
                            y=cleaned_data['extruder_current'],
                            name='挤出机电流',
                            line=dict(color='green', width=2),
                            opacity=0.8,
                            yaxis='y3'
                        ))
                        fig.add_trace(go.Scatter(
                            x=cleaned_data['time'],
                            y=cleaned_data['metered_weight'],
                            name='米重',
                            line=dict(color='orange', width=2),
                            opacity=0.8,
                            yaxis='y4'
                        ))
                        # æ·»åŠ æ¢æ–™äº‹ä»¶æ ‡è®°
                        for i, row in batch_changes.iterrows():
                            # æ·»åŠ åž‚ç›´çº¿
                            fig.add_shape(
                                type="line",
                                x0=row['start_time'],
                                y0=0,
                                x1=row['start_time'],
                                y1=1,
                                yref="paper",
                                line=dict(color="purple", width=2, dash="dash")
                            )
                            # æ·»åŠ æ³¨é‡Š
                            fig.add_annotation(
                                x=row['start_time'],
                                y=1,
                                yref='paper',
                                text=f'换料: {row["compound_code"]}\n批号: {row["batch_id"]}',
                                showarrow=True,
                                arrowhead=1,
                                ax=0,
                                ay=-60
                            )
                        # é…ç½®å›¾è¡¨å¸ƒå±€
                        fig.update_layout(
                            title='换料操作关键参数变化趋势',
                            xaxis_title='时间',
                            xaxis=dict(
                                rangeslider=dict(visible=True),
                                type='date'
                            ),
                            yaxis_title='实际转速 (rpm)',
                            yaxis2=dict(
                                title='机头压力 (MPa)',
                                overlaying='y',
                                side='right',
                                position=0.85
                            ),
                            yaxis3=dict(
                                title='挤出机电流 (A)',
                                overlaying='y',
                                side='right',
                                position=0.92
                            ),
                            yaxis4=dict(
                                title='米重 (kg)',
                                overlaying='y',
                                side='right',
                                position=1
                            ),
                            legend=dict(
                                orientation="h",
                                yanchor="bottom",
                                y=1.02,
                                xanchor="right",
                                x=1
                            ),
                            hovermode='x unified',
                            height=700
                        )
                        # æ˜¾ç¤ºå›¾è¡¨
                        st.plotly_chart(fig, width='stretch', config={'scrollZoom': True})
                        # æ·»åŠ æ•°æ®å¯¼å‡ºåŠŸèƒ½
                        import io
                        # å‡†å¤‡å¯¼å‡ºæ•°æ®
                        export_data = []
                        for i, row in batch_changes.iterrows():
                            # èŽ·å–æ¢æ–™å‰åŽçš„æ•°æ®
                            before_change = cleaned_data[cleaned_data['time'] < row['start_time']].tail(5)
                            after_change = cleaned_data[cleaned_data['time'] >= row['start_time']].head(5)
                            # æ·»åŠ æ¢æ–™äº‹ä»¶è®°å½•
                            export_data.append({
                                'event_type': '换料事件',
                                'batch_id': row['batch_id'],
                                'compound_code': row['compound_code'],
                                'time': row['start_time'],
                                'screw_speed': '',
                                'head_pressure': '',
                                'extruder_current': '',
                                'metered_weight': ''
                            })
                            # æ·»åŠ æ¢æ–™å‰æ•°æ®
                            for _, before_row in before_change.iterrows():
                                export_data.append({
                                    'event_type': '换料前',
                                    'batch_id': row['batch_id'],
                                    'compound_code': row['compound_code'],
                                    'time': before_row['time'],
                                    'screw_speed': before_row['screw_speed_actual'],
                                    'head_pressure': before_row['head_pressure'],
                                    'extruder_current': before_row['extruder_current'],
                                    'metered_weight': before_row['metered_weight']
                                })
                            # æ·»åŠ æ¢æ–™åŽæ•°æ®
                            for _, after_row in after_change.iterrows():
                                export_data.append({
                                    'event_type': '换料后',
                                    'batch_id': row['batch_id'],
                                    'compound_code': row['compound_code'],
                                    'time': after_row['time'],
                                    'screw_speed': after_row['screw_speed_actual'],
                                    'head_pressure': after_row['head_pressure'],
                                    'extruder_current': after_row['extruder_current'],
                                    'metered_weight': after_row['metered_weight']
                                })
                        # è½¬æ¢ä¸ºDataFrame
                        export_df = pd.DataFrame(export_data)
                        # åˆ›å»ºCSV数据
                        csv_buffer = io.StringIO()
                        export_df.to_csv(csv_buffer, index=False, encoding='utf-8-sig')
                        csv_data = csv_buffer.getvalue()
                        # æ·»åŠ ä¸‹è½½æŒ‰é’®
                        st.download_button(
                            label="下载换料操作分析数据",
                            data=csv_data,
                            file_name=f"换料操作分析_{start_date}_{end_date}.csv",
                            mime="text/csv"
                        )
                    else:
                        st.warning("未检测到换批事件,无法生成换料操作图表")
                    # æ˜¾ç¤ºåŽŸå§‹æ•°æ®
                    st.subheader("原始数据")
                    st.dataframe(cleaned_data, use_container_width=True)
    # æ•°æ®åº“连接状态
    st.sidebar.subheader("数据库状态")
    if extruder_service.db.is_connected():
        st.sidebar.success("数据库连接正常")
    else:
        st.sidebar.warning("数据库未连接")
if __name__ == "__main__":
    show_extruder_dashboard()
app/pages/main_process_dashboard.py
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,172 @@
import streamlit as st
import plotly.express as px
import plotly.graph_objects as go
import pandas as pd
from datetime import datetime, timedelta
from app.services.main_process_service import MainProcessService
def show_main_process_dashboard():
    # åˆå§‹åŒ–服务
    service = MainProcessService()
    # é¡µé¢æ ‡é¢˜
    st.title("主流程控制数据分析")
    # åˆå§‹åŒ–会话状态用于日期同步
    if 'main_process_start_date' not in st.session_state:
        st.session_state['main_process_start_date'] = datetime.now().date() - timedelta(days=7)
    if 'main_process_end_date' not in st.session_state:
        st.session_state['main_process_end_date'] = datetime.now().date()
    if 'main_process_quick_select' not in st.session_state:
        st.session_state['main_process_quick_select'] = "最近7天"
    # å®šä¹‰å›žè°ƒå‡½æ•°
    def update_dates(qs):
        st.session_state['main_process_quick_select'] = qs
        today = datetime.now().date()
        if qs == "今天":
            st.session_state['main_process_start_date'] = today
            st.session_state['main_process_end_date'] = today
        elif qs == "最近3天":
            st.session_state['main_process_start_date'] = today - timedelta(days=3)
            st.session_state['main_process_end_date'] = today
        elif qs == "最近7天":
            st.session_state['main_process_start_date'] = today - timedelta(days=7)
            st.session_state['main_process_end_date'] = today
        elif qs == "最近30天":
            st.session_state['main_process_start_date'] = today - timedelta(days=30)
            st.session_state['main_process_end_date'] = today
    def on_date_change():
        st.session_state['main_process_quick_select'] = "自定义"
    # æŸ¥è¯¢æ¡ä»¶åŒºåŸŸ
    with st.expander("🔍 æŸ¥è¯¢é…ç½®", expanded=True):
        # æ·»åŠ è‡ªå®šä¹‰ CSS å®žçŽ°å“åº”å¼æ¢è¡Œ
        st.markdown("""
            <style>
            /* å¼ºåˆ¶åˆ—容器换行 */
            [data-testid="stExpander"] [data-testid="column"] {
                flex: 1 1 120px !important;
                min-width: 120px !important;
            }
            /* é’ˆå¯¹æ—¥æœŸè¾“入框列稍微加宽一点 */
            @media (min-width: 768px) {
                [data-testid="stExpander"] [data-testid="column"]:nth-child(6),
                [data-testid="stExpander"] [data-testid="column"]:nth-child(7) {
                    flex: 2 1 180px !important;
                    min-width: 180px !important;
                }
            }
            </style>
            """, unsafe_allow_html=True)
        # åˆ›å»ºå¸ƒå±€
        cols = st.columns([1, 1, 1, 1, 1, 1.5, 1.5, 1])
        options = ["今天", "最近3天", "最近7天", "最近30天", "自定义"]
        for i, option in enumerate(options):
            with cols[i]:
                # æ ¹æ®å½“前选择状态决定按钮类型
                button_type = "primary" if st.session_state['main_process_quick_select'] == option else "secondary"
                if st.button(option, key=f"btn_main_{option}", width='stretch', type=button_type):
                    update_dates(option)
                    st.rerun()
        with cols[5]:
            st.date_input(
                "开始日期",
                label_visibility="collapsed",
                key="main_process_start_date",
                on_change=on_date_change
            )
        with cols[6]:
            st.date_input(
                "结束日期",
                label_visibility="collapsed",
                key="main_process_end_date",
                on_change=on_date_change
            )
        with cols[7]:
            query_button = st.button("🚀 æŸ¥è¯¢", key="main_process_query", width='stretch')
    # è½¬æ¢ä¸ºdatetime对象
    start_dt = datetime.combine(st.session_state['main_process_start_date'], datetime.min.time())
    end_dt = datetime.combine(st.session_state['main_process_end_date'], datetime.max.time())
    if query_button:
        with st.spinner("正在获取主流程数据..."):
            # 1. èŽ·å–ä¸»é€Ÿåº¦æ•°æ®
            df_speed = service.get_cutting_setting_data(start_dt, end_dt)
            # 2. èŽ·å–ç”µæœºç›‘æŽ§æ•°æ®
            df_motor = service.get_motor_monitoring_data(start_dt, end_dt)
            # 3. èŽ·å–æ¸©åº¦æŽ§åˆ¶æ•°æ®
            df_temp = service.get_temperature_control_data(start_dt, end_dt)
            # --- è¶‹åŠ¿å›¾ 1: æµç¨‹ä¸»é€Ÿåº¦ ---
            st.subheader("📈 æµç¨‹ä¸»é€Ÿåº¦è¶‹åŠ¿")
            if not df_speed.empty:
                fig_speed = px.line(df_speed, x='time', y='process_main_speed',
                                   title="流程主速度 (M/Min)",
                                   labels={'time': '时间', 'process_main_speed': '主速度 (M/Min)'})
                fig_speed.update_layout(xaxis=dict(rangeslider=dict(visible=True), type='date'))
                st.plotly_chart(fig_speed, width='stretch', config={'scrollZoom': True})
            else:
                st.info("该时间段内无主速度数据")
            # --- è¶‹åŠ¿å›¾ 2: ç”µæœºè¿è¡Œçº¿é€Ÿ ---
            st.subheader("📈 ç”µæœºè¿è¡Œçº¿é€Ÿè¶‹åŠ¿")
            if not df_motor.empty:
                fig_motor = go.Figure()
                fig_motor.add_trace(go.Scatter(x=df_motor['time'], y=df_motor['m1_line_speed'], name='拉出一段线速'))
                fig_motor.add_trace(go.Scatter(x=df_motor['time'], y=df_motor['m2_line_speed'], name='拉出二段线速'))
                fig_motor.update_layout(
                    title="电机线速 (M/Min)",
                    xaxis_title="时间",
                    yaxis_title="线速 (M/Min)",
                    xaxis=dict(rangeslider=dict(visible=True), type='date')
                )
                st.plotly_chart(fig_motor, width='stretch', config={'scrollZoom': True})
            else:
                st.info("该时间段内无电机监控数据")
            # --- è¶‹åŠ¿å›¾ 3: ä¸­ç”°æŒ¤å‡ºæœºæ¸©åº¦æŽ§åˆ¶ ---
            st.subheader("📈 ä¸­ç”°æŒ¤å‡ºæœºæ¸©åº¦è¶‹åŠ¿")
            if not df_temp.empty:
                fig_temp = go.Figure()
                # èžºæ†æ¸©åº¦
                fig_temp.add_trace(go.Scatter(x=df_temp['time'], y=df_temp['nakata_extruder_screw_set_temp'],
                                            name='螺杆设定', line=dict(dash='dash')))
                fig_temp.add_trace(go.Scatter(x=df_temp['time'], y=df_temp['nakata_extruder_screw_display_temp'],
                                            name='螺杆显示'))
                # åŽæœºç­’温度
                fig_temp.add_trace(go.Scatter(x=df_temp['time'], y=df_temp['nakata_extruder_rear_barrel_set_temp'],
                                            name='后机筒设定', line=dict(dash='dash')))
                fig_temp.add_trace(go.Scatter(x=df_temp['time'], y=df_temp['nakata_extruder_rear_barrel_display_temp'],
                                            name='后机筒显示'))
                # å‰æœºç­’温度
                fig_temp.add_trace(go.Scatter(x=df_temp['time'], y=df_temp['nakata_extruder_front_barrel_set_temp'],
                                            name='前机筒设定', line=dict(dash='dash')))
                fig_temp.add_trace(go.Scatter(x=df_temp['time'], y=df_temp['nakata_extruder_front_barrel_display_temp'],
                                            name='前机筒显示'))
                # æœºå¤´æ¸©åº¦
                fig_temp.add_trace(go.Scatter(x=df_temp['time'], y=df_temp['nakata_extruder_head_set_temp'],
                                            name='机头设定', line=dict(dash='dash')))
                fig_temp.add_trace(go.Scatter(x=df_temp['time'], y=df_temp['nakata_extruder_head_display_temp'],
                                            name='机头显示'))
                fig_temp.update_layout(
                    title="中田挤出机温度 (°C)",
                    xaxis_title="时间",
                    yaxis_title="温度 (°C)",
                    xaxis=dict(rangeslider=dict(visible=True), type='date')
                )
                st.plotly_chart(fig_temp, width='stretch', config={'scrollZoom': True})
            else:
                st.info("该时间段内无温度控制数据")
app/pages/sorting_dashboard.py
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,416 @@
import streamlit as st
import plotly.express as px
import plotly.graph_objects as go
import pandas as pd
from datetime import datetime, timedelta
from app.services.data_query_service import DataQueryService
from app.services.data_processing_service import DataProcessingService
def show_sorting_dashboard():
    # åˆå§‹åŒ–服务
    query_service = DataQueryService()
    processing_service = DataProcessingService()
    # é¡µé¢æ ‡é¢˜
    st.title("分拣磅秤数据分析")
    # åˆå§‹åŒ–会话状态用于日期同步
    if 'sorting_start_date' not in st.session_state:
        st.session_state['sorting_start_date'] = datetime.now().date() - timedelta(days=7)
    if 'sorting_end_date' not in st.session_state:
        st.session_state['sorting_end_date'] = datetime.now().date()
    if 'sorting_quick_select' not in st.session_state:
        st.session_state['sorting_quick_select'] = "最近7天"
    # å®šä¹‰å›žè°ƒå‡½æ•°
    def update_dates(qs):
        st.session_state['sorting_quick_select'] = qs
        today = datetime.now().date()
        if qs == "今天":
            st.session_state['sorting_start_date'] = today
            st.session_state['sorting_end_date'] = today
        elif qs == "最近3天":
            st.session_state['sorting_start_date'] = today - timedelta(days=3)
            st.session_state['sorting_end_date'] = today
        elif qs == "最近7天":
            st.session_state['sorting_start_date'] = today - timedelta(days=7)
            st.session_state['sorting_end_date'] = today
        elif qs == "最近30天":
            st.session_state['sorting_start_date'] = today - timedelta(days=30)
            st.session_state['sorting_end_date'] = today
    def on_date_change():
        st.session_state['sorting_quick_select'] = "自定义"
    # æŸ¥è¯¢æ¡ä»¶åŒºåŸŸ
    with st.expander("🔍 æŸ¥è¯¢é…ç½®", expanded=True):
        # æ·»åŠ è‡ªå®šä¹‰ CSS å®žçŽ°å“åº”å¼æ¢è¡Œ
        st.markdown("""
            <style>
            /* å¼ºåˆ¶åˆ—容器换行 */
            [data-testid="stExpander"] [data-testid="column"] {
                flex: 1 1 120px !important;
                min-width: 120px !important;
            }
            /* é’ˆå¯¹æ—¥æœŸè¾“入框列稍微加宽一点 */
            @media (min-width: 768px) {
                [data-testid="stExpander"] [data-testid="column"]:nth-child(6),
                [data-testid="stExpander"] [data-testid="column"]:nth-child(7) {
                    flex: 2 1 180px !important;
                    min-width: 180px !important;
                }
            }
            </style>
            """, unsafe_allow_html=True)
        # åˆ›å»ºå¸ƒå±€
        cols = st.columns([1, 1, 1, 1, 1, 1.5, 1.5, 1])
        options = ["今天", "最近3天", "最近7天", "最近30天", "自定义"]
        for i, option in enumerate(options):
            with cols[i]:
                # æ ¹æ®å½“前选择状态决定按钮类型
                button_type = "primary" if st.session_state['sorting_quick_select'] == option else "secondary"
                if st.button(option, key=f"btn_{option}", width='stretch', type=button_type):
                    update_dates(option)
                    st.rerun()
        with cols[5]:
            start_date = st.date_input(
                "开始日期",
                label_visibility="collapsed",
                key="sorting_start_date",
                on_change=on_date_change
            )
        with cols[6]:
            end_date = st.date_input(
                "结束日期",
                label_visibility="collapsed",
                key="sorting_end_date",
                on_change=on_date_change
            )
        with cols[7]:
            query_button = st.button("🚀 æŸ¥è¯¢", key="sorting_query", width='stretch')
    # è½¬æ¢ä¸ºdatetime对象(包含时间)
    start_datetime = datetime.combine(start_date, datetime.min.time())
    end_datetime = datetime.combine(end_date, datetime.max.time())
    # æŸ¥è¯¢æŒ‰é’®å¤„理
    if query_button:
        # éªŒè¯æ—¥æœŸèŒƒå›´
        if start_datetime > end_datetime:
            st.error("开始日期不能晚于结束日期!")
        else:
            # æ˜¾ç¤ºåŠ è½½çŠ¶æ€
            with st.spinner("正在查询数据..."):
                # æŸ¥è¯¢æ•°æ®
                raw_data = query_service.get_sorting_scale_data(start_datetime, end_datetime)
                if raw_data is None or raw_data.empty:
                    st.warning("未查询到数据,请检查日期范围或数据库连接!")
                    st.session_state['query_results'] = None
                else:
                    # æ¸…洗数据
                    cleaned_data = processing_service.clean_data(raw_data)
                    # è®¡ç®—统计信息
                    stats = processing_service.calculate_statistics(cleaned_data)
                    # åˆ†æžæžå€¼ç‚¹
                    extreme_analysis = processing_service.analyze_extreme_points(cleaned_data)
                    extreme_points = extreme_analysis['extreme_points']
                    phase_maxima = extreme_analysis['phase_maxima']
                    overall_pass_rate = extreme_analysis['overall_pass_rate']
                    # ç¼“存结果
                    st.session_state['query_results'] = {
                        'cleaned_data': cleaned_data,
                        'stats': stats,
                        'extreme_points': extreme_points,
                        'phase_maxima': phase_maxima,
                        'overall_pass_rate': overall_pass_rate
                    }
                    # æ˜¾ç¤ºæ•°æ®æ¦‚览
                    st.subheader("数据概览")
                    col1, col2, col3, col4 = st.columns(4)
                    with col1:
                        st.metric("总记录数", stats.get('total_records', 0))
                    with col2:
                        st.metric("平均合格数", round(stats.get('count_in_range', {}).get('mean', 0), 2))
                    with col3:
                        st.metric("数据时间范围", f"{cleaned_data['time'].min()} è‡³ {cleaned_data['time'].max()}")
                    with col4:
                        st.metric("整体合格率", f"{overall_pass_rate}%")
                    # æ˜¾ç¤ºè¶‹åŠ¿å›¾
                    st.subheader("数据趋势图")
                    fig = px.line(
                        cleaned_data,
                        x='time',
                        y=['count_under', 'count_in_range', 'count_over'],
                        labels={
                            'time': '时间',
                            'value': '数量',
                            'variable': '数据类型'
                        },
                        title='分拣磅秤数据趋势',
                        color_discrete_map={
                            'count_under': 'red',
                            'count_in_range': 'green',
                            'count_over': 'blue'
                        }
                    )
                    # è‡ªå®šä¹‰å›¾ä¾‹
                    fig.for_each_trace(lambda t: t.update(name={
                        'count_under': '低于标准',
                        'count_in_range': '在标准范围内',
                        'count_over': '高于标准'
                    }[t.name]))
                    # é…ç½®å›¾è¡¨ç¼©æ”¾åŠŸèƒ½
                    fig.update_layout(
                        xaxis=dict(
                            fixedrange=False,
                            rangeslider=dict(visible=True)
                        ),
                        yaxis=dict(fixedrange=False),
                        dragmode='zoom'
                    )
                    # é…ç½®å›¾è¡¨å‚æ•°
                    config = {'scrollZoom': True}
                    # æ˜¾ç¤ºå›¾è¡¨
                    st.plotly_chart(fig, width='stretch', config=config)
                    # æ˜¾ç¤ºæžå€¼ç‚¹åˆ†æž
                    st.subheader("极值点分析")
                    if not extreme_points.empty:
                        # å‡†å¤‡å±•示数据
                        display_df = extreme_points[['time', 'count_under', 'count_in_range', 'count_over', 'pass_rate']].copy()
                        # æ ¼å¼åŒ–时间戳
                        display_df['time'] = display_df['time'].dt.strftime('%Y-%m-%d %H:%M:%S')
                        # ä¿®æ”¹åˆ—名
                        display_df.columns = ['时间戳', '超下限数值', '范围内数值', '超上限数值', '合格率(%)']
                        # æ˜¾ç¤ºæ•°æ®è¡¨æ ¼
                        st.dataframe(display_df, use_container_width=True)
                        # æ˜¾ç¤ºæžå€¼ç‚¹æ•°é‡
                        st.info(f"共识别到 {len(extreme_points)} ä¸ªæžå€¼ç‚¹")
                        # æ·»åŠ å¯¼å‡ºåŠŸèƒ½
                        import io
                        # åˆ›å»ºCSV数据
                        csv_buffer = io.StringIO()
                        display_df.to_csv(csv_buffer, index=False, encoding='utf-8-sig')
                        csv_data = csv_buffer.getvalue()
                        # æ·»åŠ ä¸‹è½½æŒ‰é’®
                        st.download_button(
                            label="下载极值点分析结果",
                            data=csv_data,
                            file_name=f"极值点分析_{start_date}_{end_date}.csv",
                            mime="text/csv"
                        )
                    else:
                        st.warning("未识别到极值点")
                    # æ˜¾ç¤ºé‡é‡è¶‹åŠ¿å›¾
                    st.subheader("重量趋势图")
                    if 'weight' in cleaned_data.columns:
                        # åˆ›å»ºå›¾è¡¨
                        weight_fig = go.Figure()
                        # æ£€æŸ¥æ˜¯å¦åŒ…含阈值相关字段
                        has_thresholds = all(col in cleaned_data.columns for col in ['baseline_value', 'over_difference', 'under_difference'])
                        # è®¡ç®—动态阈值
                        if has_thresholds:
                            # å¤åˆ¶æ•°æ®ä»¥é¿å…ä¿®æ”¹åŽŸå§‹æ•°æ®
                            threshold_data = cleaned_data.copy()
                            # å¤„理零值
                            for col in ['baseline_value', 'over_difference', 'under_difference']:
                                threshold_data[col] = threshold_data[col].replace(0, pd.NA)
                            # å‘前填充缺失值
                            threshold_data = threshold_data.ffill()
                            # è®¡ç®—上下限阈值
                            threshold_data['upper_threshold'] = threshold_data['over_difference']
                            threshold_data['lower_threshold'] = threshold_data['under_difference']
                            # æ ‡è®°è¶…出阈值的点
                            threshold_data['is_out_of_range'] = (threshold_data['weight'] > threshold_data['upper_threshold']) | (threshold_data['weight'] < threshold_data['lower_threshold'])
                            # æ·»åŠ åŸºå‡†å€¼çº¿ï¼ˆåŠ¨æ€ï¼‰
                            weight_fig.add_trace(go.Scatter(
                                x=threshold_data['time'],
                                y=threshold_data['baseline_value'],
                                name='基准值',
                                line=dict(color='green', width=2),
                                opacity=0.7
                            ))
                            # æ·»åŠ ä¸Šé™é˜ˆå€¼çº¿ï¼ˆåŠ¨æ€ï¼‰
                            weight_fig.add_trace(go.Scatter(
                                x=threshold_data['time'],
                                y=threshold_data['upper_threshold'],
                                name='上限阈值',
                                line=dict(color='red', width=2),
                                opacity=0.7
                            ))
                            # æ·»åŠ ä¸‹é™é˜ˆå€¼çº¿ï¼ˆåŠ¨æ€ï¼‰
                            weight_fig.add_trace(go.Scatter(
                                x=threshold_data['time'],
                                y=threshold_data['lower_threshold'],
                                name='下限阈值',
                                line=dict(color='orange', width=2),
                                opacity=0.7
                            ))
                            # åˆ†ç¦»æ­£å¸¸å’Œå¼‚常数据点
                            normal_data = threshold_data[~threshold_data['is_out_of_range']]
                            out_of_range_data = threshold_data[threshold_data['is_out_of_range']]
                            # æ·»åŠ æ­£å¸¸é‡é‡ç‚¹
                            weight_fig.add_trace(go.Scatter(
                                x=normal_data['time'],
                                y=normal_data['weight'],
                                name='重量 (正常)',
                                opacity=0.8,
                                mode='markers',
                                marker=dict(
                                    size=4,
                                    color='blue',
                                    symbol='circle',
                                    line=dict(width=0, color='blue')
                                )
                            ))
                            # æ·»åŠ å¼‚å¸¸é‡é‡ç‚¹
                            if not out_of_range_data.empty:
                                weight_fig.add_trace(go.Scatter(
                                    x=out_of_range_data['time'],
                                    y=out_of_range_data['weight'],
                                    name='重量 (异常)',
                                    opacity=0.8,
                                    mode='markers',
                                    marker=dict(
                                        size=4,
                                        color='red',
                                        symbol='triangle-up',
                                        line=dict(width=2, color='darkred')
                                    )
                                ))
                        else:
                            # æ²¡æœ‰é˜ˆå€¼æ•°æ®ï¼Œåªæ˜¾ç¤ºé‡é‡è¶‹åŠ¿
                            weight_fig.add_trace(go.Scatter(
                                x=cleaned_data['time'],
                                y=cleaned_data['weight'],
                                name='重量',
                                line=dict(color='blue', width=2),
                                opacity=0.8
                            ))
                            st.warning("数据中不包含阈值相关字段,无法显示阈值线和异常警示!")
                        # é…ç½®å›¾è¡¨å¸ƒå±€
                        weight_fig.update_layout(
                            title='重量随时间变化趋势',
                            xaxis_title='时间',
                            yaxis_title='重量',
                            xaxis=dict(
                                rangeslider=dict(visible=True),
                                type='date',
                                fixedrange=False
                            ),
                            yaxis=dict(fixedrange=False),
                            legend=dict(
                                orientation="h",
                                yanchor="bottom",
                                y=1.02,
                                xanchor="right",
                                x=1
                            ),
                            hovermode='x unified',
                            height=600,
                            dragmode='zoom',
                            updatemenus=[
                                dict(
                                    type="buttons",
                                    direction="left",
                                    buttons=list([
                                        dict(args=["visible", [True, True, True, True, True]], label="显示全部", method="restyle"),
                                        dict(args=["visible", [False, False, False, True, True]], label="仅显示重量", method="restyle"),
                                        dict(args=["visible", [True, True, True, False, False]], label="仅显示阈值", method="restyle"),
                                        dict(args=["visible", [True, True, True, True, False]], label="显示正常重量", method="restyle"),
                                        dict(args=["visible", [True, True, True, False, True]], label="显示异常重量", method="restyle")
                                    ]),
                                    pad={"r": 10, "t": 10},
                                    showactive=True,
                                    x=0.1,
                                    xanchor="left",
                                    y=1.1,
                                    yanchor="top"
                                ),
                            ]
                        )
                        # æ˜¾ç¤ºå›¾è¡¨
                        st.plotly_chart(weight_fig, width='stretch', config={'scrollZoom': True})
                    # æ˜¾ç¤ºæ•°æ®è¡¨æ ¼
                    st.subheader("原始数据")
                    st.dataframe(cleaned_data, use_container_width=True)
                    # æ˜¾ç¤ºè¯¦ç»†ç»Ÿè®¡ä¿¡æ¯
                    if stats:
                        st.subheader("详细统计信息")
                        with st.expander("查看详细统计"):
                            col_stats1, col_stats2, col_stats3 = st.columns(3)
                            with col_stats1:
                                st.write("**低于标准**")
                                st.write(f"平均值: {round(stats['count_under']['mean'], 2)}")
                                st.write(f"总和: {stats['count_under']['sum']}")
                                st.write(f"最大值: {stats['count_under']['max']}")
                                st.write(f"最小值: {stats['count_under']['min']}")
                            with col_stats2:
                                st.write("**在标准范围内**")
                                st.write(f"平均值: {round(stats['count_in_range']['mean'], 2)}")
                                st.write(f"总和: {stats['count_in_range']['sum']}")
                                st.write(f"最大值: {stats['count_in_range']['max']}")
                                st.write(f"最小值: {stats['count_in_range']['min']}")
                            with col_stats3:
                                st.write("**高于标准**")
                                st.write(f"平均值: {round(stats['count_over']['mean'], 2)}")
                                st.write(f"总和: {stats['count_over']['sum']}")
                                st.write(f"最大值: {stats['count_over']['max']}")
                                st.write(f"最小值: {stats['count_over']['min']}")
    # æ•°æ®åº“连接状态
    st.sidebar.subheader("数据库状态")
    if query_service.db.is_connected():
        st.sidebar.success("数据库连接正常")
    else:
        st.sidebar.warning("数据库未连接")
if __name__ == "__main__":
    show_sorting_dashboard()
app/services/data_processing_service.py
@@ -18,24 +18,15 @@
            # å¤„理缺失值
            cleaned_df = cleaned_df.fillna(0)
            
            # ç¡®ä¿æ•°æ®ç±»åž‹æ­£ç¡®
            cleaned_df['count_under'] = cleaned_df['count_under'].astype(int)
            cleaned_df['count_in_range'] = cleaned_df['count_in_range'].astype(int)
            cleaned_df['count_over'] = cleaned_df['count_over'].astype(int)
            # ç¡®ä¿æ•°æ®ç±»åž‹æ­£ç¡®ï¼ˆä»…当列存在时)
            for col in ['count_under', 'count_in_range', 'count_over']:
                if col in cleaned_df.columns:
                    cleaned_df[col] = cleaned_df[col].astype(int)
            
            # ç¡®ä¿time是datetime类型并处理时区
            if 'time' in cleaned_df.columns:
                # è½¬æ¢ä¸ºdatetime类型
                cleaned_df['time'] = pd.to_datetime(cleaned_df['time'])
                # å¤„理时区
                # æ£€æŸ¥æ˜¯å¦å·²ç»æœ‰æ—¶åŒºä¿¡æ¯
                if cleaned_df['time'].dt.tz is None:
                    # å¦‚果没有时区信息,假设是UTC时间并添加时区
                    cleaned_df['time'] = cleaned_df['time'].dt.tz_localize('UTC')
                # è½¬æ¢ä¸ºä¸Šæµ·æ—¶åŒºï¼ˆUTC+8)
                cleaned_df['time'] = cleaned_df['time'].dt.tz_convert('Asia/Shanghai')
            
            return cleaned_df
        except Exception as e:
@@ -53,26 +44,18 @@
        
        try:
            stats = {
                'total_records': len(df),
                'count_under': {
                    'mean': df['count_under'].mean(),
                    'sum': df['count_under'].sum(),
                    'max': df['count_under'].max(),
                    'min': df['count_under'].min()
                },
                'count_in_range': {
                    'mean': df['count_in_range'].mean(),
                    'sum': df['count_in_range'].sum(),
                    'max': df['count_in_range'].max(),
                    'min': df['count_in_range'].min()
                },
                'count_over': {
                    'mean': df['count_over'].mean(),
                    'sum': df['count_over'].sum(),
                    'max': df['count_over'].max(),
                    'min': df['count_over'].min()
                }
                'total_records': len(df)
            }
            # ä»…当列存在时计算统计信息
            for col in ['count_under', 'count_in_range', 'count_over']:
                if col in df.columns:
                    stats[col] = {
                        'mean': df[col].mean(),
                        'sum': df[col].sum(),
                        'max': df[col].max(),
                        'min': df[col].min()
                    }
            return stats
        except Exception as e:
            print(f"计算统计信息失败: {e}")
app/services/data_query_service.py
@@ -1,25 +1,31 @@
import pandas as pd
from functools import lru_cache
from datetime import timedelta
from app.database.database import DatabaseConnection
class DataQueryService:
    def __init__(self):
        self.db = DatabaseConnection()
        self.timezone_offset = 8  # é»˜è®¤ä¸œå…«åŒºï¼ˆåŒ—京时间)
    
    def get_sorting_scale_data(self, start_date, end_date):
        """
        æŸ¥è¯¢åˆ†æ‹£ç£…秤数据
        :param start_date: å¼€å§‹æ—¥æœŸ
        :param end_date: ç»“束日期
        :return: åŒ…含count_under, count_in_range, count_over的数据框
        :param start_date: å¼€å§‹æ—¥æœŸ (本地时间)
        :param end_date: ç»“束日期 (本地时间)
        :return: åŒ…含count_under, count_in_range, count_over的数据框 (返回本地时间)
        """
        try:
            # å°†æœ¬åœ°æ—¶é—´è½¬æ¢ä¸ºUTC时间进行查询
            start_date_utc = start_date - timedelta(hours=self.timezone_offset)
            end_date_utc = end_date - timedelta(hours=self.timezone_offset)
            # è¿žæŽ¥æ•°æ®åº“
            if not self.db.is_connected():
                if not self.db.connect():
                    return None
            
            connection = self.db.get_connection()
            # connection = self.db.get_connection()
            
            # SQL查询语句
            query = """
@@ -41,7 +47,11 @@
            """
            
            # æ‰§è¡ŒæŸ¥è¯¢å¹¶è½¬æ¢ä¸ºDataFrame
            df = pd.read_sql(query, connection, params=(start_date, end_date))
            df = pd.read_sql(query, self.db.get_connection(), params=(start_date_utc, end_date_utc))
            # å°†æŸ¥è¯¢ç»“果中的UTC时间转换回本地时间
            if not df.empty and 'time' in df.columns:
                df['time'] = pd.to_datetime(df['time']) + timedelta(hours=self.timezone_offset)
            
            return df
        except Exception as e:
app/services/extruder_service.py
@@ -1,19 +1,25 @@
import pandas as pd
from functools import lru_cache
from datetime import timedelta
from app.database.database import DatabaseConnection
class ExtruderService:
    def __init__(self):
        self.db = DatabaseConnection()
        self.timezone_offset = 8  # é»˜è®¤ä¸œå…«åŒºï¼ˆåŒ—京时间)
    
    def get_extruder_data(self, start_date, end_date):
        """
        æŸ¥è¯¢æŒ¤å‡ºæœºæ•°æ®
        :param start_date: å¼€å§‹æ—¥æœŸ
        :param end_date: ç»“束日期
        :return: åŒ…含挤出机数据的数据框
        :param start_date: å¼€å§‹æ—¥æœŸ (本地时间)
        :param end_date: ç»“束日期 (本地时间)
        :return: åŒ…含挤出机数据的数据框 (返回本地时间)
        """
        try:
            # å°†æœ¬åœ°æ—¶é—´è½¬æ¢ä¸ºUTC时间进行查询
            start_date_utc = start_date - timedelta(hours=self.timezone_offset)
            end_date_utc = end_date - timedelta(hours=self.timezone_offset)
            # è¿žæŽ¥æ•°æ®åº“
            if not self.db.is_connected():
                if not self.db.connect():
@@ -47,7 +53,11 @@
            """
            
            # æ‰§è¡ŒæŸ¥è¯¢å¹¶è½¬æ¢ä¸ºDataFrame
            df = pd.read_sql(query, connection, params=(start_date, end_date))
            df = pd.read_sql(query, connection, params=(start_date_utc, end_date_utc))
            # å°†æŸ¥è¯¢ç»“果中的UTC时间转换回本地时间
            if not df.empty and 'time' in df.columns:
                df['time'] = pd.to_datetime(df['time']) + timedelta(hours=self.timezone_offset)
            
            return df
        except Exception as e:
@@ -75,9 +85,8 @@
            batch_df['compound_code_shift'] = batch_df['compound_code'].shift(1)
            # è‹¥å½“前行 compound_code ä¸Žå‰ä¸€è¡Œä¸åŒï¼Œåˆ™æ ‡è®°ä¸ºæ¢æ‰¹ï¼ˆ1),否则为 0 ï¼Œç¬¬ä¸€è¡Œç‰¹æ®Šå¤„理为 0
            batch_df['is_batch_change'] = (batch_df['compound_code'] != batch_df['compound_code_shift']).astype(int)
            batch_df['is_batch_change'].iloc[0] = 0
            # æ‰“印batch_df
            print(batch_df)
            batch_df.loc[batch_df.index[0], 'is_batch_change'] = 0
            # æå–所有换批事件的索引
            change_indices = batch_df[batch_df['is_batch_change'] == 1].index.tolist()
            
app/services/main_process_service.py
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,88 @@
import pandas as pd
from datetime import timedelta
from app.database.database import DatabaseConnection
class MainProcessService:
    def __init__(self):
        self.db = DatabaseConnection()
        self.timezone_offset = 8  # é»˜è®¤ä¸œå…«åŒºï¼ˆåŒ—京时间)
    def _to_utc(self, dt):
        return dt - timedelta(hours=self.timezone_offset)
    def _to_local(self, df):
        if not df.empty and 'time' in df.columns:
            df['time'] = pd.to_datetime(df['time']) + timedelta(hours=self.timezone_offset)
        return df
    def get_cutting_setting_data(self, start_date, end_date):
        """获取流程主速度数据"""
        try:
            start_utc = self._to_utc(start_date)
            end_utc = self._to_utc(end_date)
            if not self.db.is_connected():
                self.db.connect()
            query = """
            SELECT time, process_main_speed
            FROM public.aics_main_process_cutting_setting
            WHERE time BETWEEN %s AND %s
            ORDER BY time ASC
            """
            df = pd.read_sql(query, self.db.get_connection(), params=(start_utc, end_utc))
            return self._to_local(df)
        except Exception as e:
            print(f"获取主速度数据失败: {e}")
            return pd.DataFrame()
    def get_motor_monitoring_data(self, start_date, end_date):
        """获取电机监控线速数据"""
        try:
            start_utc = self._to_utc(start_date)
            end_utc = self._to_utc(end_date)
            if not self.db.is_connected():
                self.db.connect()
            query = """
            SELECT time, m1_line_speed, m2_line_speed
            FROM public.aics_main_process_motor_monitoring
            WHERE time BETWEEN %s AND %s
            ORDER BY time ASC
            """
            df = pd.read_sql(query, self.db.get_connection(), params=(start_utc, end_utc))
            return self._to_local(df)
        except Exception as e:
            print(f"获取电机监控数据失败: {e}")
            return pd.DataFrame()
    def get_temperature_control_data(self, start_date, end_date):
        """获取中田挤出机温度设定与显示数据"""
        try:
            start_utc = self._to_utc(start_date)
            end_utc = self._to_utc(end_date)
            if not self.db.is_connected():
                self.db.connect()
            query = """
            SELECT
                time,
                nakata_extruder_screw_set_temp,
                nakata_extruder_screw_display_temp,
                nakata_extruder_rear_barrel_set_temp,
                nakata_extruder_rear_barrel_display_temp,
                nakata_extruder_front_barrel_set_temp,
                nakata_extruder_front_barrel_display_temp,
                nakata_extruder_head_set_temp,
                nakata_extruder_head_display_temp
            FROM public.aics_main_process_temperature_control_setting
            WHERE time BETWEEN %s AND %s
            ORDER BY time ASC
            """
            df = pd.read_sql(query, self.db.get_connection(), params=(start_utc, end_utc))
            return self._to_local(df)
        except Exception as e:
            print(f"获取温度控制数据失败: {e}")
            return pd.DataFrame()
dashboard.py
@@ -1,10 +1,8 @@
import streamlit as st
import plotly.express as px
import pandas as pd
from datetime import datetime, timedelta
from app.services.data_query_service import DataQueryService
from app.services.data_processing_service import DataProcessingService
from app.services.extruder_service import ExtruderService
from app.pages.sorting_dashboard import show_sorting_dashboard
from app.pages.extruder_dashboard import show_extruder_dashboard
from app.pages.main_process_dashboard import show_main_process_dashboard
from app.pages.comprehensive_dashboard import show_comprehensive_dashboard
# è®¾ç½®é¡µé¢é…ç½®
st.set_page_config(
@@ -13,722 +11,48 @@
    layout="wide"
)
# å·¦ä¾§èœå•导航
st.sidebar.title("系统导航")
menu = st.sidebar.radio(
    "选择分析模块",
    ["分拣磅秤", "挤出机"],
    index=0,
    key="main_menu"
# å®šä¹‰é¡µé¢
sorting_page = st.Page(
    show_sorting_dashboard,
    title="分拣磅秤",
    icon="⚖️",
    url_path="sorting"
)
# å³ä¾§å†…容区域
if menu == "分拣磅秤":
    # åˆå§‹åŒ–服务
    query_service = DataQueryService()
    processing_service = DataProcessingService()
extruder_page = st.Page(
    show_extruder_dashboard,
    title="挤出机",
    icon="🏭",
    url_path="extruder"
)
    # é¡µé¢æ ‡é¢˜
    st.title("分拣磅秤数据分析")
main_process_page = st.Page(
    show_main_process_dashboard,
    title="主流程控制",
    icon="⚙️",
    url_path="main_process"
)
    # æŸ¥è¯¢æ¡ä»¶åŒºåŸŸï¼ˆè¿ç§»åˆ°å³ä¾§é¡¶éƒ¨ï¼‰
    st.subheader("查询配置")
    col1, col2, col3 = st.columns([2, 2, 1])
    with col1:
        start_date = st.date_input("开始日期", datetime.now() - timedelta(days=7), key="sorting_start_date")
    with col2:
        end_date = st.date_input("结束日期", datetime.now(), key="sorting_end_date")
    with col3:
        st.write("")  # å ä½
        query_button = st.button("查询数据", key="sorting_query")
comprehensive_page = st.Page(
    show_comprehensive_dashboard,
    title="综合分析",
    icon="🌐",
    url_path="comprehensive"
)
    # è½¬æ¢ä¸ºdatetime对象(包含时间)
    start_datetime = datetime.combine(start_date, datetime.min.time())
    end_datetime = datetime.combine(end_date, datetime.max.time())
# ä¾§è¾¹æ é¡µè„šä¿¡æ¯
def show_footer():
    st.sidebar.markdown("---")
    st.sidebar.markdown("© 2026 æ•°æ®åˆ†æžç³»ç»Ÿ")
    # æŸ¥è¯¢æŒ‰é’®å¤„理
    if query_button:
                # éªŒè¯æ—¥æœŸèŒƒå›´
                if start_datetime > end_datetime:
                    st.error("开始日期不能晚于结束日期!")
                else:
                    # æ˜¾ç¤ºåŠ è½½çŠ¶æ€
                    with st.spinner("正在查询数据..."):
                        # æŸ¥è¯¢æ•°æ®
                        raw_data = query_service.get_sorting_scale_data(start_datetime, end_datetime)
                        if raw_data is None or raw_data.empty:
                            st.warning("未查询到数据,请检查日期范围或数据库连接!")
                            st.session_state['query_results'] = None
                        else:
                            # æ¸…洗数据
                            cleaned_data = processing_service.clean_data(raw_data)
                            # è®¡ç®—统计信息
                            stats = processing_service.calculate_statistics(cleaned_data)
                            # åˆ†æžæžå€¼ç‚¹
                            extreme_analysis = processing_service.analyze_extreme_points(cleaned_data)
                            extreme_points = extreme_analysis['extreme_points']
                            phase_maxima = extreme_analysis['phase_maxima']
                            overall_pass_rate = extreme_analysis['overall_pass_rate']
                            # ç¼“存结果
                            st.session_state['query_results'] = {
                                'cleaned_data': cleaned_data,
                                'stats': stats,
                                'extreme_points': extreme_points,
                                'phase_maxima': phase_maxima,
                                'overall_pass_rate': overall_pass_rate
                            }
                            # æ˜¾ç¤ºæ•°æ®æ¦‚览
                            st.subheader("数据概览")
                            col1, col2, col3, col4 = st.columns(4)
                            with col1:
                                st.metric("总记录数", stats.get('total_records', 0))
                            with col2:
                                st.metric("平均合格数", round(stats.get('count_in_range', {}).get('mean', 0), 2))
                            with col3:
                                st.metric("数据时间范围", f"{cleaned_data['time'].min()} è‡³ {cleaned_data['time'].max()}")
                            with col4:
                                st.metric("整体合格率", f"{overall_pass_rate}%")
                            # æ˜¾ç¤ºè¶‹åŠ¿å›¾
                            st.subheader("数据趋势图")
                            fig = px.line(
                                cleaned_data,
                                x='time',
                                y=['count_under', 'count_in_range', 'count_over'],
                                labels={
                                    'time': '时间',
                                    'value': '数量',
                                    'variable': '数据类型'
                                },
                                title='分拣磅秤数据趋势',
                                color_discrete_map={
                                    'count_under': 'red',
                                    'count_in_range': 'green',
                                    'count_over': 'blue'
                                }
                            )
                            # è‡ªå®šä¹‰å›¾ä¾‹
                            fig.for_each_trace(lambda t: t.update(name={
                                'count_under': '低于标准',
                                'count_in_range': '在标准范围内',
                                'count_over': '高于标准'
                            }[t.name]))
                            # é…ç½®å›¾è¡¨ç¼©æ”¾åŠŸèƒ½
                            fig.update_layout(
                                xaxis=dict(
                                    fixedrange=False
                                ),
                                yaxis=dict(
                                    fixedrange=False
                                ),
                                dragmode='zoom'
                            )
                            # é…ç½®å›¾è¡¨å‚æ•°
                            config = {
                                'scrollZoom': True
                            }
                            # æ˜¾ç¤ºå›¾è¡¨
                            st.plotly_chart(fig, use_container_width=True, config=config)
                            # æ˜¾ç¤ºæžå€¼ç‚¹åˆ†æž
                            st.subheader("极值点分析")
                            if not extreme_points.empty:
                                # å‡†å¤‡å±•示数据
                                display_df = extreme_points[['time', 'count_under', 'count_in_range', 'count_over', 'pass_rate']].copy()
                                # æ ¼å¼åŒ–时间戳
                                display_df['time'] = display_df['time'].dt.strftime('%Y-%m-%d %H:%M:%S')
                                # ä¿®æ”¹åˆ—名
                                display_df.columns = ['时间戳', '超下限数值', '范围内数值', '超上限数值', '合格率(%)']
                                # æ˜¾ç¤ºæ•°æ®è¡¨æ ¼
                                st.dataframe(display_df, use_container_width=True)
                                # æ˜¾ç¤ºæžå€¼ç‚¹æ•°é‡
                                st.info(f"共识别到 {len(extreme_points)} ä¸ªæžå€¼ç‚¹")
                                # æ·»åŠ å¯¼å‡ºåŠŸèƒ½
                                import csv
                                import io
                                # åˆ›å»ºCSV数据
                                csv_buffer = io.StringIO()
                                display_df.to_csv(csv_buffer, index=False, encoding='utf-8-sig')
                                csv_data = csv_buffer.getvalue()
                                # æ·»åŠ ä¸‹è½½æŒ‰é’®
                                st.download_button(
                                    label="下载极值点分析结果",
                                    data=csv_data,
                                    file_name=f"极值点分析_{start_date}_{end_date}.csv",
                                    mime="text/csv"
                                )
                            else:
                                st.warning("未识别到极值点")
                            # æ˜¾ç¤ºé‡é‡è¶‹åŠ¿å›¾
                            st.subheader("重量趋势图")
                            if 'weight' in cleaned_data.columns:
                                # åˆ›å»ºé‡é‡è¶‹åŠ¿å›¾
                                import plotly.graph_objects as go
                                # åˆ›å»ºå›¾è¡¨
                                weight_fig = go.Figure()
                                # æ£€æŸ¥æ˜¯å¦åŒ…含阈值相关字段
                                has_thresholds = all(col in cleaned_data.columns for col in ['baseline_value', 'over_difference', 'under_difference'])
                                # è®¡ç®—动态阈值
                                if has_thresholds:
                                    # å¤åˆ¶æ•°æ®ä»¥é¿å…ä¿®æ”¹åŽŸå§‹æ•°æ®
                                    threshold_data = cleaned_data.copy()
                                    # å¤„理零值
                                    for col in ['baseline_value', 'over_difference', 'under_difference']:
                                        threshold_data[col] = threshold_data[col].replace(0, pd.NA)
                                    # å‘前填充缺失值
                                    threshold_data = threshold_data.ffill()
                                    # è®¡ç®—上下限阈值
                                    threshold_data['upper_threshold'] = threshold_data['over_difference']
                                    threshold_data['lower_threshold'] = threshold_data['under_difference']
                                    # æ ‡è®°è¶…出阈值的点
                                    threshold_data['is_out_of_range'] = (threshold_data['weight'] > threshold_data['upper_threshold']) | (threshold_data['weight'] < threshold_data['lower_threshold'])
                                    # æ·»åŠ åŸºå‡†å€¼çº¿ï¼ˆåŠ¨æ€ï¼‰
                                    weight_fig.add_trace(go.Scatter(
                                        x=threshold_data['time'],
                                        y=threshold_data['baseline_value'],
                                        name='基准值',
                                        line=dict(color='green', width=2),
                                        opacity=0.7
                                    ))
                                    # æ·»åŠ ä¸Šé™é˜ˆå€¼çº¿ï¼ˆåŠ¨æ€ï¼‰
                                    weight_fig.add_trace(go.Scatter(
                                        x=threshold_data['time'],
                                        y=threshold_data['upper_threshold'],
                                        name='上限阈值',
                                        line=dict(color='red', width=2),
                                        opacity=0.7
                                    ))
                                    # æ·»åŠ ä¸‹é™é˜ˆå€¼çº¿ï¼ˆåŠ¨æ€ï¼‰
                                    weight_fig.add_trace(go.Scatter(
                                        x=threshold_data['time'],
                                        y=threshold_data['lower_threshold'],
                                        name='下限阈值',
                                        line=dict(color='orange', width=2),
                                        opacity=0.7
                                    ))
                                    # åˆ†ç¦»æ­£å¸¸å’Œå¼‚常数据点
                                    normal_data = threshold_data[~threshold_data['is_out_of_range']]
                                    out_of_range_data = threshold_data[threshold_data['is_out_of_range']]
                                    # æ·»åŠ æ­£å¸¸é‡é‡ç‚¹
                                    weight_fig.add_trace(go.Scatter(
                                        x=normal_data['time'],
                                        y=normal_data['weight'],
                                        name='重量 (正常)',
                                        opacity=0.8,
                                        mode='markers',
                                        marker=dict(
                                            size=4,
                                            color='blue',
                                            symbol='circle',
                                            line=dict(
                                                width=0,
                                                color='blue'
                                            )
                                        )
                                    ))
                                    # æ·»åŠ å¼‚å¸¸é‡é‡ç‚¹
                                    if not out_of_range_data.empty:
                                        weight_fig.add_trace(go.Scatter(
                                            x=out_of_range_data['time'],
                                            y=out_of_range_data['weight'],
                                            name='重量 (异常)',
                                            opacity=0.8,
                                            mode='markers',
                                            marker=dict(
                                                size=4,
                                                color='red',
                                                symbol='triangle-up',
                                                line=dict(
                                                    width=2,
                                                    color='darkred'
                                                )
                                            )
                                        ))
                                else:
                                    # æ²¡æœ‰é˜ˆå€¼æ•°æ®ï¼Œåªæ˜¾ç¤ºé‡é‡è¶‹åŠ¿
                                    weight_fig.add_trace(go.Scatter(
                                        x=cleaned_data['time'],
                                        y=cleaned_data['weight'],
                                        name='重量',
                                        line=dict(color='blue', width=2),
                                        opacity=0.8
                                    ))
                                    st.warning("数据中不包含阈值相关字段,无法显示阈值线和异常警示!")
                                # é…ç½®å›¾è¡¨å¸ƒå±€
                                weight_fig.update_layout(
                                    title='重量随时间变化趋势',
                                    xaxis_title='时间',
                                    yaxis_title='重量',
                                    xaxis=dict(
                                        rangeslider=dict(
                                            visible=True
                                        ),
                                        type='date',
                                        fixedrange=False
                                    ),
                                    yaxis=dict(
                                        fixedrange=False
                                    ),
                                    legend=dict(
                                        orientation="h",
                                        yanchor="bottom",
                                        y=1.02,
                                        xanchor="right",
                                        x=1
                                    ),
                                    hovermode='x unified',
                                    height=600,
                                    dragmode='zoom',
                                    # æ·»åŠ è‡ªå®šä¹‰å·¥å…·æ æŒ‰é’®
                                    updatemenus=[
                                        dict(
                                            type="buttons",
                                            direction="left",
                                            buttons=list([
                                                dict(
                                                    args=["visible", [True, True, True, True, True]],
                                                    label="显示全部",
                                                    method="restyle"
                                                ),
                                                dict(
                                                    args=["visible", [False, False, False, True, True]],
                                                    label="仅显示重量",
                                                    method="restyle"
                                                ),
                                                dict(
                                                    args=["visible", [True, True, True, False, False]],
                                                    label="仅显示阈值",
                                                    method="restyle"
                                                ),
                                                 dict(
                                                    args=["visible", [True, True, True, True, False]],
                                                    label="显示正常重量",
                                                    method="restyle"
                                                ),
                                                 dict(
                                                    args=["visible", [True, True, True, False, True]],
                                                    label="显示异常重量",
                                                    method="restyle"
                                                )
                                            ]),
                                            pad={"r": 10, "t": 10},
                                            showactive=True,
                                            x=0.1,
                                            xanchor="left",
                                            y=1.1,
                                            yanchor="top"
                                        ),
                                    ]
                                )
                                # é…ç½®å›¾è¡¨å‚æ•°
                                config = {
                                    'scrollZoom': True,
                                    'toImageButtonOptions': {
                                        'format': 'png',
                                        'filename': '重量趋势图',
                                        'height': 600,
                                        'width': 1000,
                                        'scale': 1
                                    }
                                }
                                # æ˜¾ç¤ºå›¾è¡¨
                                st.plotly_chart(weight_fig, use_container_width=True, config=config)
                            # æ˜¾ç¤ºæ•°æ®è¡¨æ ¼
                            st.subheader("原始数据")
                            st.dataframe(cleaned_data, use_container_width=True)
                            # æ˜¾ç¤ºè¯¦ç»†ç»Ÿè®¡ä¿¡æ¯
                            if stats:
                                st.subheader("详细统计信息")
                                with st.expander("查看详细统计"):
                                    col_stats1, col_stats2, col_stats3 = st.columns(3)
                                    with col_stats1:
                                        st.write("**低于标准**")
                                        st.write(f"平均值: {round(stats['count_under']['mean'], 2)}")
                                        st.write(f"总和: {stats['count_under']['sum']}")
                                        st.write(f"最大值: {stats['count_under']['max']}")
                                        st.write(f"最小值: {stats['count_under']['min']}")
                                    with col_stats2:
                                        st.write("**在标准范围内**")
                                        st.write(f"平均值: {round(stats['count_in_range']['mean'], 2)}")
                                        st.write(f"总和: {stats['count_in_range']['sum']}")
                                        st.write(f"最大值: {stats['count_in_range']['max']}")
                                        st.write(f"最小值: {stats['count_in_range']['min']}")
                                    with col_stats3:
                                        st.write("**高于标准**")
                                        st.write(f"平均值: {round(stats['count_over']['mean'], 2)}")
                                        st.write(f"总和: {stats['count_over']['sum']}")
                                        st.write(f"最大值: {stats['count_over']['max']}")
                                        st.write(f"最小值: {stats['count_over']['min']}")
# å¯¼èˆªé…ç½®
pg = st.navigation({
    "综合分析": [comprehensive_page],
    "分项分析": [sorting_page, extruder_page, main_process_page]
})
    # æ•°æ®åº“连接状态
    st.sidebar.subheader("数据库状态")
    if 'query_service' in locals() and query_service.db.is_connected():
        st.sidebar.success("数据库连接正常")
    else:
        st.sidebar.warning("数据库未连接")
# è¿è¡Œå¯¼èˆª
pg.run()
elif menu == "挤出机":
    # åˆå§‹åŒ–服务
    extruder_service = ExtruderService()
    processing_service = DataProcessingService()
    # é¡µé¢æ ‡é¢˜
    st.title("挤出机数据分析")
    # æŸ¥è¯¢æ¡ä»¶åŒºåŸŸ
    st.subheader("查询配置")
    col1, col2, col3 = st.columns([2, 2, 1])
    with col1:
        start_date = st.date_input("开始日期", datetime.now() - timedelta(days=7), key="extruder_start_date")
    with col2:
        end_date = st.date_input("结束日期", datetime.now(), key="extruder_end_date")
    with col3:
        st.write("")  # å ä½
        query_button = st.button("查询数据", key="extruder_query")
    # è½¬æ¢ä¸ºdatetime对象(包含时间)
    start_datetime = datetime.combine(start_date, datetime.min.time())
    end_datetime = datetime.combine(end_date, datetime.max.time())
    # æŸ¥è¯¢æŒ‰é’®å¤„理
    if query_button:
                # éªŒè¯æ—¥æœŸèŒƒå›´
                if start_datetime > end_datetime:
                    st.error("开始日期不能晚于结束日期!")
                else:
                    # æ˜¾ç¤ºåŠ è½½çŠ¶æ€
                    with st.spinner("正在查询数据..."):
                        # æŸ¥è¯¢æ•°æ®
                        raw_data = extruder_service.get_extruder_data(start_datetime, end_datetime)
                        if raw_data is None or raw_data.empty:
                            st.warning("未查询到数据,请检查日期范围或数据库连接!")
                            st.session_state['extruder_results'] = None
                        else:
                            # æ¸…洗数据
                            cleaned_data = processing_service.clean_data(raw_data)
                            # æ£€æµ‹æ¢æ‰¹äº‹ä»¶
                            batch_changes = extruder_service.detect_batch_changes(cleaned_data)
                            # åˆ†æžå‚数趋势
                          #  trends = extruder_service.analyze_parameter_trends(cleaned_data)
                            # ç¼“存结果
                            st.session_state['extruder_results'] = {
                                'cleaned_data': cleaned_data,
                                'batch_changes': batch_changes,
                               # 'trends': trends,
                            }
                            # æ˜¾ç¤ºæ•°æ®æ¦‚览
                            st.subheader("数据概览")
                            col1, col2, col3, col4 = st.columns(4)
                            with col1:
                                st.metric("总记录数", len(cleaned_data))
                            with col2:
                                st.metric("换批次数", len(batch_changes))
                            with col3:
                                st.metric("数据时间范围", f"{cleaned_data['time'].min()} è‡³ {cleaned_data['time'].max()}")
                                                        # æ˜¾ç¤ºæ¢æ‰¹åˆ†æž
                            st.subheader("换批分析")
                            if not batch_changes.empty:
                                # å‡†å¤‡å±•示数据
                                batch_display = batch_changes[['batch_id', 'compound_code', 'start_time', 'end_time', 'duration_minutes']].copy()
                                # æ ¼å¼åŒ–æ—¶é—´
                                batch_display['start_time'] = batch_display['start_time'].dt.strftime('%Y-%m-%d %H:%M:%S')
                                batch_display['end_time'] = batch_display['end_time'].dt.strftime('%Y-%m-%d %H:%M:%S')
                                # ä¿®æ”¹åˆ—名
                                batch_display.columns = ['批号', '胶料号', '开始时间', '结束时间', '持续时长(分钟)']
                                # æ˜¾ç¤ºæ•°æ®è¡¨æ ¼
                                st.dataframe(batch_display, use_container_width=True)
                            else:
                                st.warning("未检测到换批事件")
                            # æ˜¾ç¤ºæ¢æ–™æ“ä½œå¯è§†åŒ–图表
                            st.subheader("换料操作可视化")
                            if not batch_changes.empty:
                                # åˆ›å»ºæ¢æ–™æ“ä½œå¯è§†åŒ–图表
                                import plotly.graph_objects as go
                                # å‡†å¤‡å›¾è¡¨æ•°æ®
                                fig = go.Figure()
                                # æ·»åŠ å…³é”®å‚æ•°è¶‹åŠ¿çº¿
                                fig.add_trace(go.Scatter(
                                    x=cleaned_data['time'],
                                    y=cleaned_data['screw_speed_actual'],
                                    name='实际转速',
                                    line=dict(color='blue', width=2),
                                    opacity=0.8
                                ))
                                fig.add_trace(go.Scatter(
                                    x=cleaned_data['time'],
                                    y=cleaned_data['head_pressure'],
                                    name='机头压力',
                                    line=dict(color='red', width=2),
                                    opacity=0.8,
                                    yaxis='y2'
                                ))
                                fig.add_trace(go.Scatter(
                                    x=cleaned_data['time'],
                                    y=cleaned_data['extruder_current'],
                                    name='挤出机电流',
                                    line=dict(color='green', width=2),
                                    opacity=0.8,
                                    yaxis='y3'
                                ))
                                fig.add_trace(go.Scatter(
                                    x=cleaned_data['time'],
                                    y=cleaned_data['metered_weight'],
                                    name='米重',
                                    line=dict(color='orange', width=2),
                                    opacity=0.8,
                                    yaxis='y4'
                                ))
                                # æ·»åŠ æ¢æ–™äº‹ä»¶æ ‡è®°
                                for i, row in batch_changes.iterrows():
                                    # æ·»åŠ åž‚ç›´çº¿
                                    fig.add_shape(
                                        type="line",
                                        x0=row['start_time'],
                                        y0=0,
                                        x1=row['start_time'],
                                        y1=1,
                                        yref="paper",
                                        line=dict(
                                            color="purple",
                                            width=2,
                                            dash="dash"
                                        )
                                    )
                                    # æ·»åŠ æ³¨é‡Š
                                    fig.add_annotation(
                                        x=row['start_time'],
                                        y=1,
                                        yref='paper',
                                        text=f'换料: {row["compound_code"]}\n批号: {row["batch_id"]}',
                                        showarrow=True,
                                        arrowhead=1,
                                        ax=0,
                                        ay=-60
                                    )
                                # é…ç½®å›¾è¡¨å¸ƒå±€
                                fig.update_layout(
                                    title='换料操作关键参数变化趋势',
                                    xaxis_title='时间',
                                    xaxis=dict(
                                        rangeslider=dict(
                                            visible=True
                                        ),
                                        type='date'
                                    ),
                                    yaxis_title='实际转速 (rpm)',
                                    yaxis2=dict(
                                        title='机头压力 (MPa)',
                                        overlaying='y',
                                        side='right',
                                        position=0.85
                                    ),
                                    yaxis3=dict(
                                        title='挤出机电流 (A)',
                                        overlaying='y',
                                        side='right',
                                        position=0.92
                                    ),
                                    yaxis4=dict(
                                        title='米重 (kg)',
                                        overlaying='y',
                                        side='right',
                                        position=1
                                    ),
                                    legend=dict(
                                        orientation="h",
                                        yanchor="bottom",
                                        y=1.02,
                                        xanchor="right",
                                        x=1
                                    ),
                                    hovermode='x unified',
                                    height=700
                                )
                                # æ˜¾ç¤ºå›¾è¡¨
                                st.plotly_chart(fig, use_container_width=True)
                                # æ·»åŠ æ•°æ®å¯¼å‡ºåŠŸèƒ½
                                import csv
                                import io
                                import pandas as pd
                                # å‡†å¤‡å¯¼å‡ºæ•°æ®
                                export_data = []
                                for i, row in batch_changes.iterrows():
                                    # èŽ·å–æ¢æ–™å‰åŽçš„æ•°æ®
                                    before_change = cleaned_data[cleaned_data['time'] < row['start_time']].tail(5)
                                    after_change = cleaned_data[cleaned_data['time'] >= row['start_time']].head(5)
                                    # æ·»åŠ æ¢æ–™äº‹ä»¶è®°å½•
                                    export_data.append({
                                        'event_type': '换料事件',
                                        'batch_id': row['batch_id'],
                                        'compound_code': row['compound_code'],
                                        'time': row['start_time'],
                                        'screw_speed': '',
                                        'head_pressure': '',
                                        'extruder_current': '',
                                        'metered_weight': ''
                                    })
                                    # æ·»åŠ æ¢æ–™å‰æ•°æ®
                                    for _, before_row in before_change.iterrows():
                                        export_data.append({
                                            'event_type': '换料前',
                                            'batch_id': row['batch_id'],
                                            'compound_code': row['compound_code'],
                                            'time': before_row['time'],
                                            'screw_speed': before_row['screw_speed_actual'],
                                            'head_pressure': before_row['head_pressure'],
                                            'extruder_current': before_row['extruder_current'],
                                            'metered_weight': before_row['metered_weight']
                                        })
                                    # æ·»åŠ æ¢æ–™åŽæ•°æ®
                                    for _, after_row in after_change.iterrows():
                                        export_data.append({
                                            'event_type': '换料后',
                                            'batch_id': row['batch_id'],
                                            'compound_code': row['compound_code'],
                                            'time': after_row['time'],
                                            'screw_speed': after_row['screw_speed_actual'],
                                            'head_pressure': after_row['head_pressure'],
                                            'extruder_current': after_row['extruder_current'],
                                            'metered_weight': after_row['metered_weight']
                                        })
                                # è½¬æ¢ä¸ºDataFrame
                                export_df = pd.DataFrame(export_data)
                                # åˆ›å»ºCSV数据
                                csv_buffer = io.StringIO()
                                export_df.to_csv(csv_buffer, index=False, encoding='utf-8-sig')
                                csv_data = csv_buffer.getvalue()
                                # æ·»åŠ ä¸‹è½½æŒ‰é’®
                                st.download_button(
                                    label="下载换料操作分析数据",
                                    data=csv_data,
                                    file_name=f"换料操作分析_{start_date}_{end_date}.csv",
                                    mime="text/csv"
                                )
                            else:
                                st.warning("未检测到换批事件,无法生成换料操作图表")
                            # æ˜¾ç¤ºåŽŸå§‹æ•°æ®
                            st.subheader("原始数据")
                            st.dataframe(cleaned_data, use_container_width=True)
    # æ•°æ®åº“连接状态
    st.sidebar.subheader("数据库状态")
    if 'extruder_service' in locals() and extruder_service.db.is_connected():
        st.sidebar.success("数据库连接正常")
    else:
        st.sidebar.warning("数据库未连接")
# é¡µè„š
st.sidebar.markdown("---")
st.sidebar.markdown("© 2026 æ•°æ®åˆ†æžç³»ç»Ÿ")
# ç¼“存清理机制
def clear_cache():
    """清理会话缓存,当切换菜单或更新查询条件时调用"""
    if 'query_results' in st.session_state:
        del st.session_state['query_results']
    if 'extruder_results' in st.session_state:
        del st.session_state['extruder_results']
# åˆå§‹åŒ–会话状态
if 'query_results' not in st.session_state:
    st.session_state['query_results'] = None
if 'extruder_results' not in st.session_state:
    st.session_state['extruder_results'] = None
# ç›‘听菜单切换,清理缓存
if 'previous_menu' not in st.session_state:
    st.session_state['previous_menu'] = menu
elif st.session_state['previous_menu'] != menu:
    clear_cache()
    st.session_state['previous_menu'] = menu
# å…³é—­æ•°æ®åº“连接(当应用结束时)
def on_app_close():
    if 'query_service' in locals():
        query_service.close_connection()
    if 'extruder_service' in locals():
        extruder_service.close_connection()
# æ³¨å†Œåº”用关闭回调
st.session_state['app_close'] = on_app_close
# æ˜¾ç¤ºé¡µè„š
show_footer()