import streamlit as st
import pyodbc
import pandas as pd
import time
from datetime import datetime, timedelta
import os
import sys
import plotly.graph_objects as go
import plotly.express as px
# 页面配置 - 设置更宽的布局和标题
st.set_page_config(
page_title="KUSO",
layout="wide",
page_icon="📊",
initial_sidebar_state="expanded"
)
# 配置数据库连接参数
server = 'svcntpj02.leoni.local'
database = 'LEP_FAAR_CN'
username = 'sqlfacility'
password = '52s0nszj'
connection_string = f'DRIVER={{SQL Server}};SERVER={server};DATABASE={database};UID={username};PWD={password}'
# Excel文件路径
excel_file_path = r'\\SVCNTFILE01\infos\Public\08 FST\2.Production plan\JIS\JIS Production Plan 2018.xlsx'
# 班次时间常量
SHIFT_START = "08:00:00"
SHIFT_C1 = "20:00:00"
SHIFT_C2 = "08:00:00"
# 黑色主题CSS样式 - 更新表格样式
st.markdown("""
<style>
/* 全局黑色背景 */
.stApp {
background-color: #000000;
color: #ffffff;
}
/* 主标题样式 */
.main-title {
font-size: 3rem;
color: #00b4d8;
text-align: center;
margin-bottom: 30px;
font-weight: 700;
text-shadow: 0 0 10px rgba(0, 180, 216, 0.7);
}
/* KOSU表格样式 - 更新列宽 */
.kosu-table {
width: 100%;
border-collapse: collapse;
margin: 20px 0;
font-size: 1.1rem;
table-layout: fixed;
}
.kosu-table th {
background-color: #1a1a1a;
color: #00b4d8;
padding: 12px 15px;
text-align: center;
border: 1px solid #333;
font-weight: bold;
}
.kosu-table td {
padding: 12px 15px;
text-align: center;
border: 1px solid #333;
background-color: #1a1a1a;
}
.kosu-table tr:hover td {
background-color: #2a2a2a;
}
/* 表头行样式 */
.table-header {
background-color: #004d66 !important;
color: white !important;
font-size: 1.2rem;
font-weight: bold;
}
/* 高完成率样式 */
.high-productivity {
color: #00ff00;
font-weight: bold;
}
/* 低完成率样式 */
.low-productivity {
color: #ff3333;
font-weight: bold;
}
/* 中等完成率样式 */
.medium-productivity {
color: #ffa500;
font-weight: bold;
}
/* 侧边栏样式 */
.css-1d391kg, .css-1d391kg button {
background-color: #121212;
color: #ffffff;
}
/* 按钮样式 */
.stButton button {
background-color: #00b4d8;
color: #000000;
border-radius: 6px;
border: none;
padding: 10px 24px;
font-weight: 500;
transition: all 0.3s;
}
.stButton button:hover {
background-color: #0096c7;
box-shadow: 0 0 10px rgba(0, 180, 216, 0.5);
}
/* 其他样式 */
.stSlider {
color: #00b4d8;
}
.stAlert {
border-radius: 10px;
background-color: #1a1a1a;
border: 1px solid #333;
}
.update-time {
font-size: 0.9rem;
color: #888;
text-align: right;
font-style: italic;
}
.stSpinner > div {
text-align: center;
color: #00b4d8;
}
/* 下拉选择器样式 */
.stSelectbox > div > div {
background-color: #1a1a1a;
color: white;
border: 1px solid #333;
}
.stSelectbox label {
color: #00b4d8;
font-weight: bold;
}
/* 时间显示样式 */
.time-display {
font-size: 1.5rem;
color: #00b4d8;
text-align: center;
margin-bottom: 20px;
font-weight: bold;
}
/* 日期显示样式 - 更新字体和大小 */
.date-cell {
color: #00b4d8;
font-weight: bold;
font-size: 4.5rem; /* 72px 约等于 4.5rem */
font-family: SimHei, "黑体", sans-serif; /* 黑体字体 */
line-height: 1.2;
}
/* 数字和百分比样式 - 更新字体和大小 */
.number-cell {
font-size: 4.5rem; /* 72px 约等于 4.5rem */
font-family: SimSun, "宋体", serif; /* 宋体字体 */
line-height: 1.2;
}
/* 饼图容器样式 */
.pie-chart-container {
background-color: #001f3f;
border-radius: 10px;
padding: 15px;
margin: 10px 0;
border: 1px solid #00b4d8;
}
/* 设置特定列宽 */
.col-line { width: 65.38mm !important; }
.col-order { width: 36.38mm !important; }
.col-trend { width: 34mm !important; }
.col-produced { width: 36mm !important; }
.col-productivity { width: 55mm !important; }
.col-rework { width: 26.5mm !important; }
.col-diff { width: 30.63mm !important; }
</style>
""", unsafe_allow_html=True)
# 应用标题
st.markdown('<div class="main-title">KOSU 生产监控系统</div>', unsafe_allow_html=True)
# 计算完成率函数
def calculate_productivity(completed, planned):
if planned == 0:
return 0.00
return (completed / planned) * 100
# 根据完成率获取CSS类名
def get_productivity_class(percentage):
if percentage >= 80:
return "high-productivity"
elif percentage >= 50:
return "medium-productivity"
else:
return "low-productivity"
# 从Excel文件读取数据的函数 - 扩展读取所有必要数据
@st.cache_data(ttl=300, show_spinner="正在从Excel获取数据...")
def get_excel_data():
try:
# 读取Capacity工作表数据
df_capacity = pd.read_excel(
excel_file_path,
sheet_name='Capacity',
header=None
)
# 读取"请勿修改"工作表数据
df_no_modify = pd.read_excel(
excel_file_path,
sheet_name='请勿修改',
header=None
)
# 获取计划订单数据 (F列)
planned_data = {
'ASS30': int(df_capacity.iloc[4, 5]) if not pd.isna(df_capacity.iloc[4, 5]) else 0, # F5
'ASS31': int(df_capacity.iloc[5, 5]) if not pd.isna(df_capacity.iloc[5, 5]) else 0, # F6
'ASS32': int(df_capacity.iloc[6, 5]) if not pd.isna(df_capacity.iloc[6, 5]) else 0 # F7
}
# 获取工作时间数据 (G列)
work_start_times = {
'ASS30': df_capacity.iloc[4, 6] if not pd.isna(df_capacity.iloc[4, 6]) else "08:00:00", # G5
'ASS31': df_capacity.iloc[5, 6] if not pd.isna(df_capacity.iloc[5, 6]) else "08:00:00", # G6
'ASS32': df_capacity.iloc[6, 6] if not pd.isna(df_capacity.iloc[6, 6]) else "08:00:00" # G7
}
# 获取效率数据 (N列) - 修复了缺少逗号的问题
efficiency_data = {
'ASS30': float(df_capacity.iloc[4, 13]) if not pd.isna(df_capacity.iloc[4, 13]) else 13.47, # N5
'ASS31': float(df_capacity.iloc[5, 13]) if not pd.isna(df_capacity.iloc[5, 13]) else 13.47, # N6
'ASS32': float(df_capacity.iloc[6, 13]) if not pd.isna(df_capacity.iloc[6, 13]) else 13.47 # N7
}
# 获取午餐开始时间 (请勿修改工作表的F3, F4, F5)
lunch_start_times = {
'ASS30': df_no_modify.iloc[2, 5] if not pd.isna(df_no_modify.iloc[2, 5]) else "12:00:00", # F3
'ASS31': df_no_modify.iloc[3, 5] if not pd.isna(df_no_modify.iloc[3, 5]) else "12:00:00", # F4
'ASS32': df_no_modify.iloc[4, 5] if not pd.isna(df_no_modify.iloc[4, 5]) else "12:00:00" # F5
}
return planned_data, work_start_times, efficiency_data, lunch_start_times
except Exception as e:
st.error(f"读取Excel文件失败: {str(e)}")
# 返回默认数据
planned_data = {'ASS30': 0, 'ASS31': 0, 'ASS32': 0}
work_start_times = {'ASS30': "08:00:00", 'ASS31': "08:00:00", 'ASS32': "08:00:00"}
efficiency_data = {'ASS30': 13.47, 'ASS31': 13.47, 'ASS32': 13.47}
lunch_start_times = {'ASS30': "12:00:00", 'ASS31': "12:00:00", 'ASS32': "12:00:00"}
return planned_data, work_start_times, efficiency_data, lunch_start_times
# 计算Trend(应完成)的逻辑函数
def calculate_trend_value(current_time, work_start_time_str, lunch_start_time_str, efficiency):
"""
计算Trend(应完成)值,考虑午餐休息时间
Args:
current_time: 当前时间 (datetime对象)
work_start_time_str: 工作开始时间字符串 (格式: "HH:MM:SS")
lunch_start_time_str: 午餐开始时间字符串 (格式: "HH:MM:SS")
efficiency: 效率 (每小时完成数量)
Returns:
int: 应完成数量
"""
try:
# 获取当前日期
current_date = current_time.date()
# 转换为完整的时间字符串
work_start_full = f"{current_date} {work_start_time_str}"
lunch_start_full = f"{current_date} {lunch_start_time_str}"
# 转换为datetime对象
work_start = datetime.strptime(work_start_full, "%Y-%m-%d %H:%M:%S")
lunch_start = datetime.strptime(lunch_start_full, "%Y-%m-%d %H:%M:%S")
# 计算时间差(分钟)
minutes_to_start = (current_time - work_start).total_seconds() / 60
minutes_to_lunch = (current_time - lunch_start).total_seconds() / 60
minutes_lunch_to_start = (lunch_start - work_start).total_seconds() / 60
# 午餐休息逻辑判断
if minutes_to_start < 0:
# 当前时间在工作开始时间之前
worktime_hours = 0
elif minutes_to_lunch > 0 or minutes_lunch_to_start > 0:
# 条件1: 当前时间在午餐开始时间之前 或 午餐开始时间在工作开始时间之后
worktime_hours = minutes_to_start / 60
elif abs(minutes_to_lunch) < 30:
# 条件2: 当前时间在午餐开始后的30分钟内(午餐休息期间)
worktime_hours = (lunch_start - work_start).total_seconds() / 3600
else:
# 条件3: 午餐休息结束后
worktime_hours = minutes_to_start / 60 - 0.5 # 减去30分钟午餐时间
# 计算应完成数量 = 工作时间(小时) × 效率(每小时数量)
trend_value = int(worktime_hours * efficiency)
return max(0, trend_value) # 确保不为负数
except Exception as e:
print(f"计算Trend值错误: {e}")
return 0
# 处理跨班次的时间逻辑
def get_shift_adjusted_time(current_time, work_start_time, lunch_start_time, efficiency, line_name):
"""
处理跨班次的时间调整逻辑
Args:
current_time: 当前时间
work_start_time: 工作开始时间字符串
lunch_start_time: 午餐开始时间字符串
efficiency: 效率
line_name: 生产线名称
Returns:
int: 调整后的Trend值
"""
# 班次判断逻辑
shift_start_time = datetime.strptime(f"{current_time.date()} {SHIFT_START}", "%Y-%m-%d %H:%M:%S")
shift_c1_time = datetime.strptime(f"{current_time.date()} {SHIFT_C1}", "%Y-%m-%d %H:%M:%S")
shift_c2_time = datetime.strptime(f"{current_time.date()} {SHIFT_C2}", "%Y-%m-%d %H:%M:%S")
if shift_start_time <= current_time <= shift_c1_time:
# 白班 - 直接计算
return calculate_trend_value(current_time, work_start_time, lunch_start_time, efficiency)
elif current_time > shift_c2_time and current_time < shift_start_time:
# 不工作时间段
return 0
elif current_time.hour <= int(SHIFT_C2.split(':')[0]):
# 夜班(跨天情况)- 使用前一天的时间
prev_day = current_time.date() - timedelta(days=1)
prev_work_start = f"{prev_day} {work_start_time}"
prev_lunch_start = f"{prev_day} {lunch_start_time}"
# 计算到夜班结束的时间
night_end = datetime.strptime(f"{current_time.date()} {SHIFT_C2}", "%Y-%m-%d %H:%M:%S")
trend_at_night_end = calculate_trend_value(night_end, work_start_time, lunch_start_time, efficiency)
return trend_at_night_end
else:
# 正常夜班
return calculate_trend_value(current_time, work_start_time, lunch_start_time, efficiency)
# 优化查询函数
def fetch_data():
try:
# 建立数据库连接
conn = pyodbc.connect(connection_string)
st.sidebar.success("数据库连接成功!", icon="✅")
# 获取今天的日期
today = datetime.now().strftime('%Y-%m-%d')
tomorrow = (datetime.now() + timedelta(days=1)).strftime('%Y-%m-%d')
# 修改查询语句以获取更详细的数据,使用今天的日期
query = f"""
SELECT A.[MachineName], B.ResultName, COUNT(DISTINCT a.[Identifier]) AS Ruilin
FROM [LEP_FAAR_CN].[TRK_Item].[_ProcessLog_1] A
INNER JOIN [LEP_FAAR_CN].[TRK_Item].[_ProcessLog_1] B ON A.Identifier = B.Identifier
WHERE A.StepName = 'MA031'
AND A.Completed >= '{today} 00:00:00'
AND A.Completed < '{tomorrow} 00:00:00'
AND B.ResultName IN ('OK', 'Rework')
GROUP BY A.MachineName, B.ResultName
"""
# 使用您提供的批量处理方法
cursor = conn.cursor()
cursor.execute(query)
# 获取列名
columns = [column[0] for column in cursor.description]
# 分批获取数据
data = []
while True:
batch = cursor.fetchmany(5000)
if not batch:
break
data.extend(batch)
# 转为DataFrame
df = pd.DataFrame.from_records(data, columns=columns)
return df
except pyodbc.Error as e:
error_msg = f"数据库连接失败: {str(e)}"
st.sidebar.error(error_msg, icon="🚨")
return pd.DataFrame()
except Exception as e:
st.error(f"查询错误: {str(e)}")
return pd.DataFrame()
# 处理数据库数据,提取各ASS的OK和Rework数量
def process_db_data(df):
# 初始化结果字典
result = {
'ASS30': {'OK': 0, 'Rework': 0},
'ASS31': {'OK': 0, 'Rework': 0},
'ASS32': {'OK': 0, 'Rework': 0}
}
if not df.empty:
for _, row in df.iterrows():
machine = row['MachineName']
result_type = row['ResultName']
ruilin = row['Ruilin']
# 标准化机器名称(去除空格,转换为大写)
machine_clean = machine.strip().upper()
# 标准化结果类型(处理大小写问题)
result_type_clean = result_type.strip().upper()
# 检查是否是有效的机器名
if machine_clean in result:
# 处理OK/Ok大小写问题
if result_type_clean == 'OK':
result[machine_clean]['OK'] = ruilin
elif result_type_clean == 'REWORK':
result[machine_clean]['Rework'] = ruilin
return result
# 创建饼状图函数
def create_pie_chart(ass_name, planned, completed, productivity):
# 计算剩余数量
remaining = max(0, planned - completed)
# 创建饼图数据
labels = ['已完成', '剩余']
values = [completed, remaining]
# 创建饼图
fig = go.Figure()
# 添加环形图
fig.add_trace(go.Pie(
labels=labels,
values=values,
hole=0.6, # 空心饼图
marker=dict(
colors=['#00b4d8', 'rgba(255, 165, 0, 0.3)'], # 蓝色已完成,橙色空心部分
line=dict(color='#ffa500', width=2) # 橙色边框
),
textinfo='none', # 不显示文本标签
hoverinfo='label+percent'
))
# 更新布局
fig.update_layout(
title={
'text': f'{ass_name} - 完成率: {productivity:.1f}%',
'x': 0.5,
'xanchor': 'center',
'font': {'size': 16, 'color': '#00b4d8'}
},
showlegend=True,
legend=dict(
orientation="h",
yanchor="bottom",
y=-0.2,
xanchor="center",
x=0.5,
font=dict(color='white')
),
paper_bgcolor='#001f3f', # 科技感蓝色背景
plot_bgcolor='#001f3f',
font=dict(color='white'),
height=300,
margin=dict(l=20, r=20, t=50, b=50)
)
# 在中心添加文本
fig.add_annotation(
text=f"{productivity:.1f}%",
x=0.5, y=0.5,
xref="paper", yref="paper",
showarrow=False,
font=dict(size=20, color='#00b4d8')
)
return fig
# 侧边栏配置
with st.sidebar:
st.header("控制面板")
# 添加页面选择下拉框
page_option = st.selectbox(
"选择页面",
["四面大屏", "新项目1", "新项目2"],
help="选择要查看的监控页面"
)
# 只在四面大屏页面显示刷新率控制
if page_option == "四面大屏":
refresh_rate = st.slider("刷新频率(秒)", 1, 300, 60, help="设置数据自动刷新的时间间隔")
st.divider()
# 添加日期选择器
selected_date = st.date_input("选择日期", datetime.now())
st.divider()
# 添加导出功能
if st.button('导出当前数据', key="export_btn"):
df = fetch_data()
if not df.empty:
csv = df.to_csv(index=False).encode('utf-8-sig')
st.download_button(
label="下载CSV",
data=csv,
file_name=f"返工数据_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv",
mime='text/csv',
key="download_btn"
)
else:
st.warning("无数据可导出")
# 根据选择的页面显示不同内容
if page_option == "四面大屏":
# 主展示区
placeholder = st.empty()
while True:
# 获取Excel数据
planned_data, work_start_times, efficiency_data, lunch_start_times = get_excel_data()
with placeholder.container():
# 获取当前日期和时间
current_date = datetime.now().strftime("%Y-%m-%d")
current_time = datetime.now().strftime("%H:%M") # 只显示时间,不显示日期
current_datetime = datetime.now() # 完整的当前时间
st.markdown(f'<div class="time-display">{current_time}</div>', unsafe_allow_html=True)
# 获取数据库数据并处理
db_data = process_db_data(fetch_data())
# 计算各生产线的完成率
prod_30 = calculate_productivity(db_data['ASS30']['OK'], planned_data['ASS30'])
prod_31 = calculate_productivity(db_data['ASS31']['OK'], planned_data['ASS31'])
prod_32 = calculate_productivity(db_data['ASS32']['OK'], planned_data['ASS32'])
# 计算Trend(应完成)值 - 应用新的逻辑
trend_30 = get_shift_adjusted_time(
current_datetime,
str(work_start_times['ASS30']),
str(lunch_start_times['ASS30']),
efficiency_data['ASS30'],
'ASS30'
)
trend_31 = get_shift_adjusted_time(
current_datetime,
str(work_start_times['ASS31']),
str(lunch_start_times['ASS31']),
efficiency_data['ASS31'],
'ASS31'
)
trend_32 = get_shift_adjusted_time(
current_datetime,
str(work_start_times['ASS32']),
str(lunch_start_times['ASS32']),
efficiency_data['ASS32'],
'ASS32'
)
# 创建KOSU表格,使用从Excel获取的数据和处理后的数据库数据
html_table = f"""
<table class="kosu-table">
<tr class="table-header">
<th class="col-line">Line</th>
<th class="col-order">Order (计划订单)</th>
<th class="col-trend">Trend (应完成)</th>
<th class="col-produced">Produced (已完成)</th>
<th class="col-productivity">Productivity (完成率)</th>
<th class="col-rework">Rework (返工)</th>
<th class="col-diff">Diff. (差额)</th>
</tr>
<tr>
<td class="date-cell col-line">ASS30</td>
<td class="number-cell col-order">{int(planned_data['ASS30'])}</td>
<td class="number-cell col-trend">{trend_30}</td>
<td class="number-cell col-produced">{db_data['ASS30']['OK']}</td>
<td class="number-cell col-productivity {get_productivity_class(prod_30)}">{prod_30:.2f}%</td>
<td class="number-cell col-rework">{db_data['ASS30']['Rework']}</td>
<td class="number-cell col-diff">{int(planned_data['ASS30'] - db_data['ASS30']['OK'])}</td>
</tr>
<tr>
<td class="date-cell col-line">ASS31</td>
<td class="number-cell col-order">{int(planned_data['ASS31'])}</td>
<td class="number-cell col-trend">{trend_31}</td>
<td class="number-cell col-produced">{db_data['ASS31']['OK']}</td>
<td class="number-cell col-productivity {get_productivity_class(prod_31)}">{prod_31:.2f}%</td>
<td class="number-cell col-rework">{db_data['ASS31']['Rework']}</td>
<td class="number-cell col-diff">{int(planned_data['ASS31'] - db_data['ASS31']['OK'])}</td>
</tr>
<tr>
<td class="date-cell col-line">ASS32</td>
<td class="number-cell col-order">{int(planned_data['ASS32'])}</td>
<td class="number-cell col-trend">{trend_32}</td>
<td class="number-cell col-produced">{db_data['ASS32']['OK']}</td>
<td class="number-cell col-productivity {get_productivity_class(prod_32)}">{prod_32:.2f}%</td>
<td class="number-cell col-rework">{db_data['ASS32']['Rework']}</td>
<td class="number-cell col-diff">{int(planned_data['ASS32'] - db_data['ASS32']['OK'])}</td>
</tr>
</table>
"""
st.markdown(html_table, unsafe_allow_html=True)
# 显示工作时间信息(可选)
with st.expander("生产参数详情", expanded=False):
col1, col2, col3 = st.columns(3)
with col1:
st.subheader("ASS30")
st.write(f"工作开始时间: {work_start_times['ASS30']}")
st.write(f"午餐开始时间: {lunch_start_times['ASS30']}")
st.write(f"效率: {efficiency_data['ASS30']} 个/小时")
with col2:
st.subheader("ASS31")
st.write(f"工作开始时间: {work_start_times['ASS31']}")
st.write(f"午餐开始时间: {lunch_start_times['ASS31']}")
st.write(f"效率: {efficiency_data['ASS31']} 个/小时")
with col3:
st.subheader("ASS32")
st.write(f"工作开始时间: {work_start_times['ASS32']}")
st.write(f"午餐开始时间: {lunch_start_times['ASS32']}")
st.write(f"效率: {efficiency_data['ASS32']} 个/小时")
# 创建三个饼状图
st.subheader("生产完成率饼状图")
col1, col2, col3 = st.columns(3)
with col1:
st.markdown('<div class="pie-chart-container">', unsafe_allow_html=True)
fig30 = create_pie_chart("ASS30", planned_data['ASS30'], db_data['ASS30']['OK'], prod_30)
st.plotly_chart(fig30, use_container_width=True)
st.markdown('</div>', unsafe_allow_html=True)
with col2:
st.markdown('<div class="pie-chart-container">', unsafe_allow_html=True)
fig31 = create_pie_chart("ASS31", planned_data['ASS31'], db_data['ASS31']['OK'], prod_31)
st.plotly_chart(fig31, use_container_width=True)
st.markdown('</div>', unsafe_allow_html=True)
with col3:
st.markdown('<div class="pie-chart-container">', unsafe_allow_html=True)
fig32 = create_pie_chart("ASS32", planned_data['ASS32'], db_data['ASS32']['OK'], prod_32)
st.plotly_chart(fig32, use_container_width=True)
st.markdown('</div>', unsafe_allow_html=True)
# 可折叠的SQL查询部分
with st.expander("执行SQL查询", expanded=False):
if st.button("执行SQL查询", key="sql_query_btn"):
with st.spinner("正在查询数据库..."):
df = fetch_data()
if not df.empty:
st.subheader("SQL查询结果")
st.dataframe(
df.style
.highlight_max(axis=0, color="#C2A516")
.set_properties(**{'background-color': '#1a1a1a', 'color': 'white'}),
use_container_width=True,
height=400
)
# 显示处理后的数据
db_data = process_db_data(df)
st.subheader("处理后的数据")
st.json(db_data)
else:
st.info("数据库查询无结果")
# 更新时间显示
st.markdown(f'<div class="update-time">最后更新: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}</div>', unsafe_allow_html=True)
time.sleep(refresh_rate)
elif page_option == "新项目1":
st.info("新项目1页面尚未更新,敬请期待!")
st.image("https://via.placeholder.com/800x400/1a1a1a/00b4d8?text=新项目1+开发中", use_column_width=True)
elif page_option == "新项目2":
st.info("新项目2页面尚未更新,敬请期待!")
st.image("https://via.placeholder.com/800x400/1a1a1a/00b4d8?text=新项目2+开发中", use_column_width=True) 将python语句按功能拆分成多个模块,并在excel模块中加入验证的功能。我是新手你要注释每一行代码