Compare commits

...

3 Commits

Author SHA1 Message Date
zhenghu
e3ac2beb7a refactor: 精简 UI 自定义样式,使用 Streamlit 原生组件替代 HTML 卡片
- 将大量自定义 CSS(主题色、卡片、徽章、alert 等)精简为仅保留字体设置
  - sidebar 标题/副标题/分隔线改用 st.header、st.caption、st.divider
  - KPI 指标卡片改用 st.metric
  - 作物推荐排行改用 st.container + st.progress
  - 种植建议的 alert 框改用 st.warning / st.success / st.info
  - 删除自定义 HTML footer,改用 st.caption
2026-04-14 17:35:42 +08:00
zhenghu
6af6a10636 refactor: 整体 UI 主题改版为浅色自然风格,并优化文案与清理代码
- 将页面主题从深色科技风改为浅色自然风(土壤棕/叶绿/麦穗黄配色)
  - 页面标题与文案统一为"种植决策助手",提升可读性
  - 移除未使用的导入(pandas、plotly.express、make_subplots)
  - 更新作物颜色配置以适配新主题
  - 调整 Plotly 图表样式(背景、轴线、网格、标注线颜色)适配浅色模式
  - 新增 Streamlit 按钮的圆角与悬停样式覆盖
  - 删除多余的伪元素装饰代码,精简 CSS
2026-04-14 11:24:08 +08:00
zhenghu
1c97a065a7 chore: 将项目名称统一为 yield-smart-app
- 更新 pyproject.toml 中的包名
  - 同步 uv.lock 中的包名
  - 更新 README.md 中的项目目录和 Docker 镜像名称
  - 更新 justfile 和 main.py 中的引用名称
2026-04-13 17:51:36 +08:00
6 changed files with 90 additions and 317 deletions

View File

@@ -1,4 +1,4 @@
# YieldSmart 农业智能决策系统
# yield-smart-app 农业智能决策系统
基于多因子 Cobb-Douglas 产量模型的作物种植决策支持应用。
@@ -47,7 +47,7 @@ streamlit run app.py
## 项目结构
```
YieldSmart/
yield-smart-app/
├── app.py # 主应用文件Streamlit
├── main.py # 入口文件
├── pyproject.toml # 项目配置
@@ -78,10 +78,10 @@ just check
```bash
# 构建镜像
docker build -t yieldsmart .
docker build -t yield-smart-app .
# 运行容器
docker run -p 8000:8000 yieldsmart
docker run -p 8000:8000 yield-smart-app
```
## 许可证

391
app.py
View File

@@ -5,183 +5,21 @@
import streamlit as st
import numpy as np
import pandas as pd
import plotly.graph_objects as go
import plotly.express as px
from plotly.subplots import make_subplots
# ─── Page Config ────────────────────────────────────────────────────────────
st.set_page_config(
page_title="农业智能决策系统",
page_title="种植决策助手",
page_icon="🌾",
layout="wide",
initial_sidebar_state="expanded",
)
# ─── Custom CSS ─────────────────────────────────────────────────────────────
# ─── Minimal CSS ─────────────────────────────────────────────────────────────
st.markdown("""
<style>
:root {
--bg-dark: #0a1628;
--bg-card: #0f2040;
--accent-green: #4ade80;
--accent-gold: #f59e0b;
--accent-blue: #38bdf8;
--text-primary: #e2e8f0;
--text-muted: #64748b;
--border: rgba(74, 222, 128, 0.2);
}
html, body, [class*="css"] {
font-family: "PingFang SC", "Microsoft YaHei", "Noto Serif SC", serif;
background-color: var(--bg-dark);
color: var(--text-primary);
}
.stApp {
background: linear-gradient(135deg, #0a1628 0%, #0d1f3c 50%, #091520 100%);
}
/* Sidebar */
[data-testid="stSidebar"] {
background: linear-gradient(180deg, #0f2040 0%, #0a1628 100%);
border-right: 1px solid var(--border);
}
[data-testid="stSidebar"] .stSlider label,
[data-testid="stSidebar"] .stNumberInput label,
[data-testid="stSidebar"] .stSelectbox label {
color: var(--accent-green) !important;
font-size: 0.82rem;
font-family: "JetBrains Mono", "Fira Code", "Source Code Pro", monospace;
letter-spacing: 0.05em;
}
/* Metric cards */
.metric-card {
background: linear-gradient(135deg, #0f2040, #132b55);
border: 1px solid var(--border);
border-radius: 12px;
padding: 20px 24px;
text-align: center;
position: relative;
overflow: hidden;
}
.metric-card::before {
content: '';
position: absolute;
top: 0; left: 0; right: 0;
height: 2px;
background: linear-gradient(90deg, var(--accent-green), var(--accent-blue));
}
.metric-value {
font-family: "JetBrains Mono", "Fira Code", "Source Code Pro", monospace;
font-size: 2.2rem;
font-weight: 700;
color: var(--accent-green);
line-height: 1;
}
.metric-unit {
font-size: 0.85rem;
color: var(--text-muted);
margin-top: 4px;
}
.metric-label {
font-size: 0.9rem;
color: var(--text-primary);
margin-top: 8px;
}
/* Section headers */
.section-header {
font-size: 0.75rem;
font-family: "JetBrains Mono", "Fira Code", "Source Code Pro", monospace;
letter-spacing: 0.15em;
color: var(--accent-gold);
text-transform: uppercase;
border-bottom: 1px solid rgba(245,158,11,0.3);
padding-bottom: 6px;
margin-bottom: 16px;
margin-top: 24px;
}
/* Crop badge */
.crop-badge {
display: inline-block;
background: linear-gradient(135deg, rgba(74,222,128,0.15), rgba(56,189,248,0.15));
border: 1px solid var(--accent-green);
border-radius: 6px;
padding: 4px 12px;
font-family: "JetBrains Mono", "Fira Code", "Source Code Pro", monospace;
font-size: 0.78rem;
color: var(--accent-green);
margin: 3px;
}
/* Recommendation card */
.rec-card {
background: linear-gradient(135deg, rgba(74,222,128,0.08), rgba(56,189,248,0.05));
border: 1px solid rgba(74,222,128,0.3);
border-radius: 12px;
padding: 18px 22px;
margin: 10px 0;
}
.rec-rank {
font-family: "JetBrains Mono", "Fira Code", "Source Code Pro", monospace;
font-size: 1.5rem;
color: var(--accent-gold);
font-weight: 700;
}
.rec-crop {
font-size: 1.1rem;
color: var(--text-primary);
font-weight: 600;
}
.rec-score {
font-family: "JetBrains Mono", "Fira Code", "Source Code Pro", monospace;
font-size: 0.85rem;
color: var(--accent-blue);
}
/* Hero title */
.hero-title {
font-size: 2rem;
font-weight: 700;
background: linear-gradient(135deg, var(--accent-green), var(--accent-blue));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
line-height: 1.2;
}
.hero-sub {
font-family: "JetBrains Mono", "Fira Code", "Source Code Pro", monospace;
font-size: 0.8rem;
color: var(--text-muted);
letter-spacing: 0.1em;
margin-top: 4px;
}
/* Alert boxes */
.alert-good {
background: rgba(74,222,128,0.1);
border-left: 3px solid var(--accent-green);
border-radius: 0 8px 8px 0;
padding: 12px 16px;
margin: 8px 0;
font-size: 0.9rem;
}
.alert-warn {
background: rgba(245,158,11,0.1);
border-left: 3px solid var(--accent-gold);
border-radius: 0 8px 8px 0;
padding: 12px 16px;
margin: 8px 0;
font-size: 0.9rem;
}
/* Override streamlit slider colors */
.stSlider [data-baseweb="slider"] [data-testid="stTickBarMin"],
.stSlider [data-baseweb="slider"] [data-testid="stTickBarMax"] {
color: var(--text-muted);
font-family: "PingFang SC", "Microsoft YaHei", "Noto Sans SC", sans-serif;
}
</style>
""", unsafe_allow_html=True)
@@ -194,42 +32,42 @@ CROPS = {
"optimal": {"ph": (6.0, 7.0), "N": (80, 120), "P": (30, 60), "K": (40, 80),
"rainfall": (150, 250), "temp": (22, 30)},
"base_yield": 7500, # kg/ha
"color": "#4ade80"
"color": "#4a7c59"
},
"小麦": {
"emoji": "🌿",
"optimal": {"ph": (6.0, 7.5), "N": (60, 100), "P": (20, 50), "K": (30, 60),
"rainfall": (60, 120), "temp": (15, 22)},
"base_yield": 6000,
"color": "#f59e0b"
"color": "#c69c5d"
},
"玉米": {
"emoji": "🌽",
"optimal": {"ph": (5.8, 7.0), "N": (100, 150), "P": (40, 70), "K": (60, 100),
"rainfall": (100, 180), "temp": (20, 28)},
"base_yield": 8500,
"color": "#fbbf24"
"color": "#e8a93f"
},
"大豆": {
"emoji": "🫘",
"optimal": {"ph": (6.0, 7.0), "N": (20, 50), "P": (30, 60), "K": (40, 80),
"rainfall": (80, 150), "temp": (18, 26)},
"base_yield": 3500,
"color": "#a78bfa"
"color": "#8b7cb3"
},
"油菜": {
"emoji": "🌻",
"optimal": {"ph": (6.0, 7.5), "N": (80, 130), "P": (30, 60), "K": (50, 90),
"rainfall": (80, 130), "temp": (15, 20)},
"base_yield": 3000,
"color": "#f97316"
"color": "#d97836"
},
"棉花": {
"emoji": "☁️",
"optimal": {"ph": (6.0, 8.0), "N": (60, 100), "P": (20, 45), "K": (40, 70),
"rainfall": (70, 120), "temp": (25, 32)},
"base_yield": 4500,
"color": "#e2e8f0"
"color": "#5a6b7c"
},
}
@@ -294,11 +132,11 @@ def rank_crops(ph, N, P, K, rainfall, temp, pesticide, area):
# ─── Sidebar Inputs ──────────────────────────────────────────────────────────
with st.sidebar:
st.markdown('<div class="hero-title">🌾 农业决策</div>', unsafe_allow_html=True)
st.markdown('<div class="hero-sub">SMART FARMING SYSTEM v2.0</div>', unsafe_allow_html=True)
st.markdown("---")
st.header("🌾 种植决策助手")
st.caption("根据土壤和气候,推荐适宜作物")
st.divider()
st.markdown('<div class="section-header">🧪 土壤参数</div>', unsafe_allow_html=True)
st.subheader("🧪 土壤参数")
col1, col2 = st.columns(2)
with col1:
ph = st.slider("pH 值", 4.0, 9.0, 6.5, 0.1)
@@ -307,18 +145,18 @@ with st.sidebar:
P = st.slider("磷 P (mg/kg)", 0, 100, 45, 5)
K = st.slider("钾 K (mg/kg)", 0, 150, 60, 5)
st.markdown('<div class="section-header">🌦 气象数据</div>', unsafe_allow_html=True)
st.subheader("🌦 气象数据")
col3, col4 = st.columns(2)
with col3:
rainfall = st.slider("降雨量 (mm/月)", 0, 400, 120, 10)
with col4:
temp = st.slider("温度 (°C)", 0, 45, 22, 1)
st.markdown('<div class="section-header">🌱 种植参数</div>', unsafe_allow_html=True)
st.subheader("🌱 种植参数")
area = st.number_input("种植面积 (公顷)", 0.1, 10000.0, 100.0, 10.0)
pesticide = st.slider("农药用量 (kg/ha)", 0, 200, 50, 5)
st.markdown('<div class="section-header">🎯 目标作物</div>', unsafe_allow_html=True)
st.subheader("🎯 目标作物")
selected_crop = st.selectbox(
"选择分析作物",
list(CROPS.keys()),
@@ -333,54 +171,22 @@ best_crop = rankings[0]
# ─── Main Layout ─────────────────────────────────────────────────────────────
st.markdown(f"""
<div style="display:flex; align-items:baseline; gap:16px; margin-bottom:4px;">
<div class="hero-title">农业智能决策系统</div>
</div>
<div class="hero-sub">YIELD = f(SOIL · WEATHER · PESTICIDE) &nbsp;|&nbsp; 基于多因子 Cobb-Douglas 产量模型</div>
""", unsafe_allow_html=True)
st.markdown("<br>", unsafe_allow_html=True)
st.title("🌾 种植决策助手")
st.caption("输入土壤与气象条件,获得作物产量预测与种植建议")
# KPI row
overall = np.mean(list(factors.values()))
k1, k2, k3, k4 = st.columns(4)
with k1:
st.markdown(f"""
<div class="metric-card">
<div class="metric-value">{yph:,.0f}</div>
<div class="metric-unit">kg / 公顷</div>
<div class="metric-label">{CROPS[selected_crop]['emoji']} {selected_crop} 单产</div>
</div>""", unsafe_allow_html=True)
with k2:
st.markdown(f"""
<div class="metric-card">
<div class="metric-value">{ytotal/1000:,.1f}</div>
<div class="metric-unit">吨 / 总产量</div>
<div class="metric-label">📦 {area:.0f} 公顷总产</div>
</div>""", unsafe_allow_html=True)
with k3:
overall = np.mean(list(factors.values()))
st.markdown(f"""
<div class="metric-card">
<div class="metric-value">{overall*100:.1f}%</div>
<div class="metric-unit">综合适宜度</div>
<div class="metric-label">🎯 环境匹配指数</div>
</div>""", unsafe_allow_html=True)
with k4:
st.markdown(f"""
<div class="metric-card">
<div class="metric-value">{best_crop['emoji']}</div>
<div class="metric-unit">{best_crop['crop']} ({best_crop['score']*100:.0f}%)</div>
<div class="metric-label">🏆 最优推荐作物</div>
</div>""", unsafe_allow_html=True)
st.markdown("<br>", unsafe_allow_html=True)
k1.metric(f"{CROPS[selected_crop]['emoji']} {selected_crop} 单产", f"{yph:,.0f} kg/ha")
k2.metric(f"📦 {area:.0f} 公顷总产", f"{ytotal/1000:,.1f}")
k3.metric("🎯 环境匹配指数", f"{overall*100:.1f}%")
k4.metric("🏆 最优推荐作物", f"{best_crop['emoji']} {best_crop['crop']}", f"匹配度 {best_crop['score']*100:.0f}%")
# ─── Charts Row ──────────────────────────────────────────────────────────────
col_left, col_right = st.columns([3, 2])
with col_left:
st.markdown('<div class="section-header">📊 影响因子雷达图</div>', unsafe_allow_html=True)
st.subheader("📊 影响因子雷达图")
factor_names = list(factors.keys())
factor_vals = [round(v * 100, 1) for v in factors.values()]
@@ -392,14 +198,14 @@ with col_left:
r=factor_vals_closed,
theta=factor_names_closed,
fill='toself',
fillcolor='rgba(74,222,128,0.15)',
line=dict(color='#4ade80', width=2),
fillcolor='rgba(74, 124, 89, 0.15)',
line=dict(color='#4a7c59', width=2),
name=selected_crop,
))
fig_radar.add_trace(go.Scatterpolar(
r=[100]*len(factor_names_closed),
theta=factor_names_closed,
line=dict(color='rgba(255,255,255,0.1)', width=1, dash='dot'),
line=dict(color='rgba(0,0,0,0.15)', width=1, dash='dot'),
mode='lines',
name='理想值',
))
@@ -407,14 +213,14 @@ with col_left:
polar=dict(
bgcolor='rgba(0,0,0,0)',
radialaxis=dict(range=[0, 100], showticklabels=True,
tickfont=dict(color='#64748b', size=9),
gridcolor='rgba(255,255,255,0.06)'),
angularaxis=dict(tickfont=dict(color='#e2e8f0', size=11),
gridcolor='rgba(255,255,255,0.08)'),
tickfont=dict(color='#5a5a5a', size=9),
gridcolor='rgba(0,0,0,0.08)'),
angularaxis=dict(tickfont=dict(color='#2c2c2c', size=11),
gridcolor='rgba(0,0,0,0.1)'),
),
paper_bgcolor='rgba(0,0,0,0)',
plot_bgcolor='rgba(0,0,0,0)',
font=dict(color='#e2e8f0'),
font=dict(color='#2c2c2c'),
legend=dict(orientation='h', y=-0.12, font=dict(size=10)),
margin=dict(t=20, b=40, l=40, r=40),
height=320,
@@ -422,30 +228,17 @@ with col_left:
st.plotly_chart(fig_radar, use_container_width=True)
with col_right:
st.markdown('<div class="section-header">🏅 作物推荐排行</div>', unsafe_allow_html=True)
st.subheader("🏅 作物推荐排行")
for i, r in enumerate(rankings[:4]):
rank_icons = ["🥇", "🥈", "🥉", "4"]
bar_width = int(r['score'] * 100)
bar_color = r['color']
st.markdown(f"""
<div class="rec-card" style="margin-bottom:8px;">
<div style="display:flex; justify-content:space-between; align-items:center;">
<div>
<span class="rec-rank">{rank_icons[i]}</span>
<span class="rec-crop" style="margin-left:8px;">{r['emoji']} {r['crop']}</span>
</div>
<span class="rec-score">{r['score']*100:.1f}%</span>
</div>
<div style="background:rgba(255,255,255,0.06); border-radius:4px; height:4px; margin-top:8px; overflow:hidden;">
<div style="width:{bar_width}%; height:100%; background:{bar_color}; border-radius:4px;"></div>
</div>
<div style="font-size:0.78rem; color:#64748b; margin-top:4px; font-family:'JetBrains Mono',monospace;">
{r['yield_ha']:,.0f} kg/ha &nbsp;·&nbsp; 总产 {r['total_yield']/1000:,.1f}
</div>
</div>""", unsafe_allow_html=True)
with st.container(border=True):
c1, c2 = st.columns([3, 1])
c1.markdown(f"**{rank_icons[i]} {r['emoji']} {r['crop']}**")
c2.markdown(f"<div style='text-align:right; color:#4a7c59; font-weight:600;'>{r['score']*100:.1f}%</div>", unsafe_allow_html=True)
st.progress(int(r['score'] * 100), text=f"{r['yield_ha']:,.0f} kg/ha · 总产 {r['total_yield']/1000:,.1f}")
# ─── Sensitivity Analysis ─────────────────────────────────────────────────────
st.markdown('<div class="section-header">📈 产量敏感性分析</div>', unsafe_allow_html=True)
st.subheader("📈 产量敏感性分析")
sa_col1, sa_col2 = st.columns(2)
@@ -456,18 +249,18 @@ with sa_col1:
fig_N = go.Figure()
fig_N.add_trace(go.Scatter(
x=N_range, y=y_N,
mode='lines', line=dict(color='#4ade80', width=2.5),
fill='tozeroy', fillcolor='rgba(74,222,128,0.08)',
mode='lines', line=dict(color='#4a7c59', width=2.5),
fill='tozeroy', fillcolor='rgba(74, 124, 89, 0.08)',
name='产量'
))
fig_N.add_vline(x=N, line=dict(color='#f59e0b', width=1.5, dash='dot'),
annotation_text=f"当前 {N}", annotation_font_color='#f59e0b')
fig_N.add_vline(x=N, line=dict(color='#d4a574', width=1.5, dash='dot'),
annotation_text=f"当前 {N}", annotation_font_color='#7c5e42')
fig_N.update_layout(
title=dict(text="氮肥用量 vs 产量", font=dict(color='#94a3b8', size=12)),
xaxis=dict(title="氮 N (mg/kg)", color='#64748b', gridcolor='rgba(255,255,255,0.05)'),
yaxis=dict(title="产量 (kg/ha)", color='#64748b', gridcolor='rgba(255,255,255,0.05)'),
title=dict(text="氮肥用量 vs 产量", font=dict(color='#5a5a5a', size=12)),
xaxis=dict(title="氮 N (mg/kg)", color='#5a5a5a', gridcolor='rgba(0,0,0,0.06)'),
yaxis=dict(title="产量 (kg/ha)", color='#5a5a5a', gridcolor='rgba(0,0,0,0.06)'),
paper_bgcolor='rgba(0,0,0,0)', plot_bgcolor='rgba(0,0,0,0)',
font=dict(color='#e2e8f0', size=10),
font=dict(color='#2c2c2c', size=10),
margin=dict(t=36, b=36, l=50, r=20), height=220,
showlegend=False,
)
@@ -480,25 +273,25 @@ with sa_col2:
fig_R = go.Figure()
fig_R.add_trace(go.Scatter(
x=rain_range, y=y_rain,
mode='lines', line=dict(color='#38bdf8', width=2.5),
fill='tozeroy', fillcolor='rgba(56,189,248,0.08)',
mode='lines', line=dict(color='#5a8f9e', width=2.5),
fill='tozeroy', fillcolor='rgba(90, 143, 158, 0.08)',
name='产量'
))
fig_R.add_vline(x=rainfall, line=dict(color='#f59e0b', width=1.5, dash='dot'),
annotation_text=f"当前 {rainfall}mm", annotation_font_color='#f59e0b')
fig_R.add_vline(x=rainfall, line=dict(color='#d4a574', width=1.5, dash='dot'),
annotation_text=f"当前 {rainfall}mm", annotation_font_color='#7c5e42')
fig_R.update_layout(
title=dict(text="月降雨量 vs 产量", font=dict(color='#94a3b8', size=12)),
xaxis=dict(title="降雨量 (mm/月)", color='#64748b', gridcolor='rgba(255,255,255,0.05)'),
yaxis=dict(title="产量 (kg/ha)", color='#64748b', gridcolor='rgba(255,255,255,0.05)'),
title=dict(text="月降雨量 vs 产量", font=dict(color='#5a5a5a', size=12)),
xaxis=dict(title="降雨量 (mm/月)", color='#5a5a5a', gridcolor='rgba(0,0,0,0.06)'),
yaxis=dict(title="产量 (kg/ha)", color='#5a5a5a', gridcolor='rgba(0,0,0,0.06)'),
paper_bgcolor='rgba(0,0,0,0)', plot_bgcolor='rgba(0,0,0,0)',
font=dict(color='#e2e8f0', size=10),
font=dict(color='#2c2c2c', size=10),
margin=dict(t=36, b=36, l=50, r=20), height=220,
showlegend=False,
)
st.plotly_chart(fig_R, use_container_width=True)
# ─── All Crops Comparison Bar Chart ───────────────────────────────────────────
st.markdown('<div class="section-header">🌐 全作物产量对比</div>', unsafe_allow_html=True)
st.subheader("🌐 全作物产量对比")
crop_names = [f"{r['emoji']} {r['crop']}" for r in rankings]
crop_yields = [r['yield_ha'] for r in rankings]
@@ -508,78 +301,58 @@ fig_bar = go.Figure()
fig_bar.add_trace(go.Bar(
x=crop_names, y=crop_yields,
marker=dict(color=crop_colors, opacity=0.85,
line=dict(color='rgba(255,255,255,0.2)', width=1)),
line=dict(color='rgba(0,0,0,0.08)', width=1)),
text=[f"{y:,.0f}" for y in crop_yields],
textposition='outside',
textfont=dict(color='#94a3b8', size=10, family='JetBrains Mono'),
textfont=dict(color='#5a5a5a', size=10),
))
fig_bar.update_layout(
xaxis=dict(color='#64748b', gridcolor='rgba(255,255,255,0.04)'),
yaxis=dict(title="预期产量 (kg/ha)", color='#64748b', gridcolor='rgba(255,255,255,0.05)'),
xaxis=dict(color='#5a5a5a', gridcolor='rgba(0,0,0,0.04)'),
yaxis=dict(title="预期产量 (kg/ha)", color='#5a5a5a', gridcolor='rgba(0,0,0,0.06)'),
paper_bgcolor='rgba(0,0,0,0)', plot_bgcolor='rgba(0,0,0,0)',
font=dict(color='#e2e8f0', size=11),
margin=dict(t=20, b=30, l=60, r=20), height=240,
font=dict(color='#2c2c2c', size=11),
margin=dict(t=20, b=30, l=60, r=20), height=260,
showlegend=False,
)
st.plotly_chart(fig_bar, use_container_width=True)
# ─── Advisory Panel ───────────────────────────────────────────────────────────
st.markdown('<div class="section-header">💡 智能建议</div>', unsafe_allow_html=True)
st.subheader("💡 种植建议")
adv1, adv2 = st.columns(2)
with adv1:
crop_opt = CROPS[selected_crop]["optimal"]
advisories = []
if not (crop_opt["ph"][0] <= ph <= crop_opt["ph"][1]):
advisories.append(("warn", f"pH {ph} 偏离 {selected_crop} 适宜范围 {crop_opt['ph']},建议{'施石灰' if ph < crop_opt['ph'][0] else '施硫磺'}调节"))
st.warning(f"pH {ph} 偏离 {selected_crop} 适宜范围 {crop_opt['ph']},建议{'施石灰' if ph < crop_opt['ph'][0] else '施硫磺'}调节")
else:
advisories.append(("good", f"土壤 pH {ph} 处于 {selected_crop} 适宜范围内"))
st.success(f"土壤 pH {ph} 处于 {selected_crop} 适宜范围内")
if N < crop_opt["N"][0]:
advisories.append(("warn", f"氮肥不足({N} vs 建议 {crop_opt['N'][0]}-{crop_opt['N'][1]} mg/kg建议追施尿素"))
st.warning(f"氮肥不足({N} vs 建议 {crop_opt['N'][0]}-{crop_opt['N'][1]} mg/kg建议追施尿素")
elif N > crop_opt["N"][1]:
advisories.append(("warn", f"氮肥过量({N} mg/kg可能造成徒长建议减施"))
st.warning(f"氮肥过量({N} mg/kg可能造成徒长建议减施")
else:
advisories.append(("good", f"氮肥水平 {N} mg/kg 适宜"))
st.success(f"氮肥水平 {N} mg/kg 适宜")
if rainfall < crop_opt["rainfall"][0]:
advisories.append(("warn", f"降雨量不足,建议增加灌溉(缺水 {crop_opt['rainfall'][0]-rainfall} mm"))
st.warning(f"降雨量不足,建议增加灌溉(缺水 {crop_opt['rainfall'][0]-rainfall} mm")
elif rainfall > crop_opt["rainfall"][1]:
advisories.append(("warn", f"降雨量偏多,注意防涝排水"))
st.warning(f"降雨量偏多,注意防涝排水")
else:
advisories.append(("good", f"降雨量 {rainfall}mm 适合 {selected_crop} 生长"))
for typ, msg in advisories:
css_class = "alert-good" if typ == "good" else "alert-warn"
st.markdown(f'<div class="{css_class}">{msg}</div>', unsafe_allow_html=True)
st.success(f"降雨量 {rainfall}mm 适合 {selected_crop} 生长")
with adv2:
st.markdown(f"""
<div class="rec-card">
<div style="font-size:0.82rem; color:#64748b; font-family:'JetBrains Mono',monospace; margin-bottom:12px;">
当前环境参数下适宜种植:
</div>
""", unsafe_allow_html=True)
badges = "".join([
f'<span class="crop-badge">{r["emoji"]} {r["crop"]} {r["score"]*100:.0f}%</span>'
for r in rankings if r['score'] > 0.6
])
st.markdown(f'{badges}</div>', unsafe_allow_html=True)
suitable = [r for r in rankings if r['score'] > 0.6]
if suitable:
st.info("当前环境参数下适宜种植:" + "".join([f"{r['emoji']} {r['crop']} ({r['score']*100:.0f}%)" for r in suitable]))
else:
st.info("当前环境参数下暂无特别适宜的作物")
st.markdown(f"""
<div style="margin-top:16px; font-size:0.85rem; color:#94a3b8; line-height:1.7;">
<b style="color:#4ade80;">最优方案:</b>{best_crop['emoji']} {best_crop['crop']}<br>
预期单产:<span style="font-family:'JetBrains Mono',monospace; color:#38bdf8;">{best_crop['yield_ha']:,.0f} kg/ha</span><br>
{area:.0f}公顷总产:<span style="font-family:'JetBrains Mono',monospace; color:#38bdf8;">{best_crop['total_yield']/1000:,.1f} 吨</span>
</div>
""", unsafe_allow_html=True)
with st.container(border=True):
st.markdown(f"**最优方案:{best_crop['emoji']} {best_crop['crop']}**")
st.markdown(f"- 预期单产:**{best_crop['yield_ha']:,.0f} kg/ha**")
st.markdown(f"- {area:.0f} 公顷总产:**{best_crop['total_yield']/1000:,.1f} 吨**")
# ─── Footer ───────────────────────────────────────────────────────────────────
st.markdown("<br>", unsafe_allow_html=True)
st.markdown("""
<div style="text-align:center; font-family:'JetBrains Mono',monospace; font-size:0.72rem;
color:#334155; padding:16px; border-top:1px solid rgba(74,222,128,0.1);">
YIELD = f(Soil, Weather, Pesticide) &nbsp;|&nbsp; Cobb-Douglas Multi-Factor Model &nbsp;|&nbsp; 农业智能决策系统
</div>
""", unsafe_allow_html=True)
st.divider()
st.caption("种植决策助手 · 基于 Cobb-Douglas 多因子产量模型 · 仅供参考")

View File

@@ -1,4 +1,4 @@
# Justfile for YieldSmart 农业智能决策系统
# Justfile for yield-smart-app 农业智能决策系统
# Use `just <command>` to run tasks
# Default task: show available commands

View File

@@ -1,5 +1,5 @@
def main():
print("Hello from YieldSmart!")
print("Hello from yield-smart-app!")
if __name__ == "__main__":

View File

@@ -1,5 +1,5 @@
[project]
name = "yield-smart"
name = "yield-smart-app"
version = "0.1.0"
description = "农业智能决策系统 - 基于多因子 Cobb-Douglas 产量模型的作物种植决策支持应用"
readme = "README.md"

2
uv.lock generated
View File

@@ -632,7 +632,7 @@ wheels = [
]
[[package]]
name = "yield-smart"
name = "yield-smart-app"
version = "0.1.0"
source = { virtual = "." }
dependencies = [