refactor(app): 使用 Streamlit 原生组件替换自定义 HTML/CSS
移除大量自定义样式定义,改用 st.header/caption/subheader/badge/info/divider 等原生组件, 简化 UI 代码并提升可维护性。
This commit is contained in:
293
app.py
293
app.py
@@ -25,187 +25,11 @@ st.set_page_config(
|
||||
initial_sidebar_state="expanded",
|
||||
)
|
||||
|
||||
# ─── Custom CSS ──────────────────────────────────────────────────────────────
|
||||
# ─── Minimal CSS ─────────────────────────────────────────────────────────────
|
||||
st.markdown("""
|
||||
<style>
|
||||
:root {
|
||||
--soil: #7c5e42;
|
||||
--leaf: #4a7c59;
|
||||
--leaf-light: #6b9e75;
|
||||
--wheat: #d4a574;
|
||||
--cream: #faf8f3;
|
||||
--paper: #ffffff;
|
||||
--ink: #2c2c2c;
|
||||
--ink-muted: #5a5a5a;
|
||||
--border: #e5e0d5;
|
||||
--shadow: rgba(0,0,0,0.04);
|
||||
--danger: #c45c4a;
|
||||
--danger-light: #f5eae8;
|
||||
}
|
||||
|
||||
html, body, [class*="css"] {
|
||||
font-family: "PingFang SC", "Microsoft YaHei", "Noto Sans SC", sans-serif;
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.stApp {
|
||||
background: var(--cream);
|
||||
}
|
||||
|
||||
/* Sidebar */
|
||||
[data-testid="stSidebar"] {
|
||||
background: #f5f2eb;
|
||||
border-right: 1px solid var(--border);
|
||||
}
|
||||
[data-testid="stSidebar"] .stSlider label,
|
||||
[data-testid="stSidebar"] .stNumberInput label,
|
||||
[data-testid="stSidebar"] .stSelectbox label,
|
||||
[data-testid="stSidebar"] .stTextInput label,
|
||||
[data-testid="stSidebar"] .stFileUploader label {
|
||||
color: var(--soil) !important;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Metric / hero cards */
|
||||
.metric-card {
|
||||
background: var(--paper);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 14px;
|
||||
padding: 20px 18px;
|
||||
text-align: center;
|
||||
box-shadow: 0 2px 10px var(--shadow);
|
||||
}
|
||||
.metric-value {
|
||||
font-size: 1.9rem;
|
||||
font-weight: 700;
|
||||
color: var(--leaf);
|
||||
line-height: 1.1;
|
||||
}
|
||||
.metric-unit {
|
||||
font-size: 0.8rem;
|
||||
color: var(--ink-muted);
|
||||
margin-top: 4px;
|
||||
}
|
||||
.metric-label {
|
||||
font-size: 0.9rem;
|
||||
color: var(--ink);
|
||||
margin-top: 8px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Section headers */
|
||||
.section-header {
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
color: var(--soil);
|
||||
padding-bottom: 8px;
|
||||
margin-bottom: 14px;
|
||||
margin-top: 22px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
/* Result cards */
|
||||
.result-card {
|
||||
background: var(--paper);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 14px;
|
||||
padding: 16px;
|
||||
margin-bottom: 14px;
|
||||
box-shadow: 0 1px 6px var(--shadow);
|
||||
}
|
||||
.result-rank {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 50%;
|
||||
background: var(--wheat);
|
||||
color: #fff;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 700;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 10px;
|
||||
}
|
||||
.result-name {
|
||||
font-size: 1.05rem;
|
||||
font-weight: 600;
|
||||
color: var(--ink);
|
||||
}
|
||||
.result-score {
|
||||
font-size: 0.9rem;
|
||||
color: var(--leaf);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Tags */
|
||||
.tag {
|
||||
display: inline-block;
|
||||
background: #f3f6f3;
|
||||
border: 1px solid var(--leaf-light);
|
||||
border-radius: 999px;
|
||||
padding: 3px 10px;
|
||||
font-size: 0.78rem;
|
||||
color: var(--leaf);
|
||||
margin: 3px 3px 3px 0;
|
||||
}
|
||||
.tag-warn {
|
||||
background: var(--danger-light);
|
||||
border-color: var(--danger);
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
/* Hero */
|
||||
.hero-title {
|
||||
font-size: 1.6rem;
|
||||
font-weight: 700;
|
||||
color: var(--soil);
|
||||
line-height: 1.2;
|
||||
}
|
||||
.hero-sub {
|
||||
font-size: 0.85rem;
|
||||
color: var(--ink-muted);
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
/* Sidebar title */
|
||||
.sidebar-title {
|
||||
font-size: 1.15rem;
|
||||
font-weight: 700;
|
||||
color: var(--soil);
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
.sidebar-sub {
|
||||
font-size: 0.75rem;
|
||||
color: var(--ink-muted);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
/* Info panel */
|
||||
.info-panel {
|
||||
background: var(--paper);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
padding: 14px 16px;
|
||||
font-size: 0.88rem;
|
||||
color: var(--ink-muted);
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
/* Streamlit overrides */
|
||||
.stButton > button {
|
||||
border-radius: 10px !important;
|
||||
background: var(--leaf) !important;
|
||||
border: none !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
.stButton > button:hover {
|
||||
background: var(--leaf-light) !important;
|
||||
}
|
||||
|
||||
/* Radio horizontal */
|
||||
.stRadio [role="radiogroup"] {
|
||||
gap: 8px;
|
||||
}
|
||||
</style>
|
||||
""", unsafe_allow_html=True)
|
||||
@@ -423,11 +247,11 @@ def build_index() -> tuple[list[dict], list[str], list[str]]:
|
||||
|
||||
# ─── Sidebar ─────────────────────────────────────────────────────────────────
|
||||
with st.sidebar:
|
||||
st.markdown('<div class="sidebar-title">🌿 病虫害以图搜图</div>', unsafe_allow_html=True)
|
||||
st.markdown('<div class="sidebar-sub">上传图片,智能识别相似病虫害</div>', unsafe_allow_html=True)
|
||||
st.markdown("<hr style='border:none;border-top:1px solid var(--border);margin:12px 0;'>", unsafe_allow_html=True)
|
||||
st.header("🌿 病虫害以图搜图")
|
||||
st.caption("上传图片,智能识别相似病虫害")
|
||||
st.divider()
|
||||
|
||||
st.markdown('<div class="section-header" style="margin-top:0">🖼️ 输入方式</div>', unsafe_allow_html=True)
|
||||
st.subheader("🖼️ 输入方式")
|
||||
input_mode = st.radio("", ["上传本地图片", "输入图片 URL", "选择示例图片"], label_visibility="collapsed")
|
||||
|
||||
# 初始化 session_state
|
||||
@@ -453,7 +277,7 @@ with st.sidebar:
|
||||
if query_url.strip():
|
||||
query_source = query_url.strip()
|
||||
else:
|
||||
st.markdown('<div style="font-size:0.8rem;color:#7a7a7a;margin-bottom:6px;">点击选择示例</div>', unsafe_allow_html=True)
|
||||
st.caption("点击选择示例")
|
||||
cols = st.columns(2)
|
||||
for idx, (name, url) in enumerate(EXAMPLE_IMAGES):
|
||||
with cols[idx % 2]:
|
||||
@@ -466,42 +290,32 @@ with st.sidebar:
|
||||
query_source = query_url
|
||||
st.image(query_url, use_container_width=True)
|
||||
|
||||
st.markdown('<div class="section-header">⚙️ 搜索设置</div>', unsafe_allow_html=True)
|
||||
st.subheader("⚙️ 搜索设置")
|
||||
top_k = st.slider("返回条数", 1, min(12, len(PEST_KNOWLEDGE)), 5)
|
||||
|
||||
st.markdown("<br>", unsafe_allow_html=True)
|
||||
search_clicked = st.button("开始搜索", type="primary", use_container_width=True)
|
||||
|
||||
st.markdown("<hr style='border:none;border-top:1px solid var(--border);margin:12px 0;'>", unsafe_allow_html=True)
|
||||
st.markdown("""
|
||||
<div class="info-panel">
|
||||
<b>使用说明</b><br>
|
||||
1. 上传病虫害患处图片<br>
|
||||
2. 系统自动提取图像特征<br>
|
||||
3. 与知识库比对返回相似结果<br>
|
||||
4. 参考症状与防治建议
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
st.divider()
|
||||
st.info(
|
||||
"**使用说明**\n\n"
|
||||
"1. 上传病虫害患处图片\n"
|
||||
"2. 系统自动提取图像特征\n"
|
||||
"3. 与知识库比对返回相似结果\n"
|
||||
"4. 参考症状与防治建议"
|
||||
)
|
||||
|
||||
# ─── Build Index ─────────────────────────────────────────────────────────────
|
||||
index_items, succeeded, failed = build_index()
|
||||
|
||||
# ─── Main Layout ─────────────────────────────────────────────────────────────
|
||||
st.markdown("""
|
||||
<div style="display:flex; align-items:baseline; gap:12px; margin-bottom:4px;">
|
||||
<div class="hero-title">病虫害以图搜图</div>
|
||||
</div>
|
||||
<div class="hero-sub">基于 CLIP 视觉模型的病虫害相似度检索与防治建议</div>
|
||||
""", unsafe_allow_html=True)
|
||||
st.title("🌿 病虫害以图搜图")
|
||||
st.caption("基于 CLIP 视觉模型的病虫害相似度检索与防治建议")
|
||||
|
||||
# Status badges
|
||||
badges = []
|
||||
if succeeded:
|
||||
badges.append(f'<span class="tag">📚 知识库 {len(succeeded)} 种</span>')
|
||||
st.badge(f"📚 知识库 {len(succeeded)} 种", color="blue")
|
||||
if failed:
|
||||
badges.append(f'<span class="tag tag-warn">⚠️ 索引失败 {len(failed)} 种</span>')
|
||||
if badges:
|
||||
st.markdown(f"<div style='margin-top:8px;'>{''.join(badges)}</div>", unsafe_allow_html=True)
|
||||
st.badge(f"⚠️ 索引失败 {len(failed)} 种", color="red")
|
||||
|
||||
st.markdown("<br>", unsafe_allow_html=True)
|
||||
|
||||
@@ -516,11 +330,11 @@ if search_clicked:
|
||||
if query_img is not None:
|
||||
col_query, col_preview = st.columns([1, 3])
|
||||
with col_query:
|
||||
st.markdown('<div class="section-header" style="margin-top:0">🔍 查询图片</div>', unsafe_allow_html=True)
|
||||
st.subheader("🔍 查询图片")
|
||||
st.image(query_img, use_container_width=True)
|
||||
|
||||
with col_preview:
|
||||
st.markdown('<div class="section-header" style="margin-top:0">⏳ 正在分析...</div>', unsafe_allow_html=True)
|
||||
st.subheader("⏳ 正在分析...")
|
||||
progress = st.progress(0, text="提取图像特征...")
|
||||
|
||||
embedder = get_embedder()
|
||||
@@ -536,7 +350,7 @@ if search_clicked:
|
||||
progress.progress(100, text="搜索完成")
|
||||
progress.empty()
|
||||
|
||||
st.markdown(f'<div class="section-header" style="margin-top:0">🏆 搜索结果(Top-{len(results)})</div>', unsafe_allow_html=True)
|
||||
st.subheader(f"🏆 搜索结果(Top-{len(results)})")
|
||||
|
||||
# Similarity bar chart
|
||||
names = [f"{r[1]['name']}" for r in results]
|
||||
@@ -566,49 +380,34 @@ if search_clicked:
|
||||
st.plotly_chart(fig_bar, use_container_width=True)
|
||||
|
||||
# Result cards below
|
||||
st.markdown('<div class="section-header">📋 详细结果</div>', unsafe_allow_html=True)
|
||||
st.subheader("📋 详细结果")
|
||||
for rank, (sim, item) in enumerate(results, 1):
|
||||
with st.container():
|
||||
st.markdown(f"""
|
||||
<div class="result-card">
|
||||
<div style="display:flex; gap:14px; align-items:flex-start;">
|
||||
<div style="flex:0 0 140px;">
|
||||
<img src="{item['url']}" style="width:100%; border-radius:10px; border:1px solid var(--border);">
|
||||
</div>
|
||||
<div style="flex:1;">
|
||||
<div style="display:flex; align-items:center; margin-bottom:8px;">
|
||||
<span class="result-rank">{rank}</span>
|
||||
<span class="result-name">{item['name']}</span>
|
||||
<span style="margin-left:auto;" class="result-score">相似度 {sim*100:.1f}%</span>
|
||||
</div>
|
||||
<div style="margin-bottom:8px;">
|
||||
<span class="tag">{item['crop']}</span>
|
||||
<span class="tag{' tag-warn' if item['category'] == '虫害' else ''}">{item['category']}</span>
|
||||
</div>
|
||||
<div style="font-size:0.88rem; color:var(--ink); line-height:1.6;">
|
||||
<b>症状:</b>{item['symptoms']}<br>
|
||||
<b>防治:</b>{item['treatment']}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
with st.container(border=True):
|
||||
c1, c2 = st.columns([1, 4])
|
||||
with c1:
|
||||
st.image(item["url"], use_container_width=True)
|
||||
with c2:
|
||||
header_col, score_col = st.columns([3, 1])
|
||||
header_col.markdown(f"**#{rank} {item['name']}**")
|
||||
score_col.markdown(f"<div style='text-align:right; font-weight:600;'>相似度 {sim*100:.1f}%</div>", unsafe_allow_html=True)
|
||||
|
||||
badge_cols = st.columns([1, 1, 4])
|
||||
badge_cols[0].caption(f"🌾 {item['crop']}")
|
||||
badge_cols[1].caption(f"🐛 {item['category']}" if item["category"] == "虫害" else f"🍃 {item['category']}")
|
||||
|
||||
st.markdown(f"**症状:** {item['symptoms']}")
|
||||
st.markdown(f"**防治:** {item['treatment']}")
|
||||
|
||||
# Advisory summary
|
||||
if results:
|
||||
best = results[0][1]
|
||||
st.markdown('<div class="section-header">💡 初步建议</div>', unsafe_allow_html=True)
|
||||
st.markdown(f"""
|
||||
<div class="info-panel" style="border-left:3px solid var(--leaf-light); border-radius:0 12px 12px 0;">
|
||||
系统判断该图片与 <b>{best['name']}</b>({best['crop']}{best['category']})最为相似,相似度 <b>{results[0][0]*100:.1f}%</b>。<br>
|
||||
建议结合田间实际情况进一步确认,参考防治方案:<b>{best['treatment']}</b>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
st.subheader("💡 初步建议")
|
||||
st.info(
|
||||
f"系统判断该图片与 **{best['name']}**({best['crop']}{best['category']})最为相似,"
|
||||
f"相似度 **{results[0][0]*100:.1f}%**。\n\n"
|
||||
f"建议结合田间实际情况进一步确认,参考防治方案:**{best['treatment']}**"
|
||||
)
|
||||
|
||||
# ─── Footer ───────────────────────────────────────────────────────────────────
|
||||
st.markdown("<br>", unsafe_allow_html=True)
|
||||
st.markdown("""
|
||||
<div style="text-align:center; font-size:0.78rem; color:#aaa; padding:14px; border-top:1px solid #e5e0d5;">
|
||||
病虫害以图搜图 · 基于 CLIP 视觉模型 · 结果仅供参考,请结合田间实际情况判断
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
st.divider()
|
||||
st.caption("病虫害以图搜图 · 基于 CLIP 视觉模型 · 结果仅供参考,请结合田间实际情况判断")
|
||||
|
||||
Reference in New Issue
Block a user