优化UI模块和资源推荐功能

Made-with: Cursor
This commit is contained in:
刘国栋
2026-04-30 23:35:06 +08:00
parent fb309299bf
commit 86abeeb5cc
14 changed files with 737 additions and 513 deletions
+20 -24
View File
@@ -168,9 +168,12 @@ def load_default_cfg():
for key in ["brand", "advantages", "competitors", "temperature"]: for key in ["brand", "advantages", "competitors", "temperature"]:
if key in app_config and app_config[key]: if key in app_config and app_config[key]:
base_cfg[key] = app_config[key] base_cfg[key] = app_config[key]
except Exception: except FileNotFoundError:
# secrets.toml 不存在时静默忽略,用户可通过侧边栏配置 # secrets.toml 不存在时静默忽略,用户可通过侧边栏配置
pass pass
except Exception as e:
import logging
logging.warning(f"读取 secrets.toml 失败: {e}")
return base_cfg return base_cfg
@@ -230,28 +233,11 @@ ss_init("image_descriptions", []) # 图片描述列表
ss_init("detail_tab_active", "🎨 增强工具") # 保存当前激活的详情Tab ss_init("detail_tab_active", "🎨 增强工具") # 保存当前激活的详情Tab
# ------------------- 工具函数 ------------------- # ------------------- 工具函数 -------------------
from modules.ui.components import INVALID_FS_CHARS
def sanitize_filename(name: str, max_len: int = 80) -> str:
from modules.ui.components import sanitize_filename as _sanitize_filename
return _sanitize_filename(name, max_len)
def safe_decode_uploaded(uploaded) -> str:
from modules.ui.components import safe_decode_uploaded as _safe_decode_uploaded
return _safe_decode_uploaded(uploaded)
def extract_json_array(text: str):
"""从模型输出中抽取 JSON 数组(JsonOutputParser 失败时兜底)。"""
from modules.ui.components import extract_json_array as _extract_json_array
return _extract_json_array(text)
def validate_cfg(cfg: dict): def validate_cfg(cfg: dict):
"""保留你原本的"必须填写所有 API Key"约束,但不 st.stop:改为禁用按钮 + 提示""" """验证配置完整性,返回 (是否有效, 错误列表)"""
errors = [] errors = []
warnings = []
if not cfg.get("gen_api_key", "").strip(): if not cfg.get("gen_api_key", "").strip():
errors.append("生成&优化 LLM 的 API Key 未填写") errors.append("生成&优化 LLM 的 API Key 未填写")
@@ -264,7 +250,12 @@ def validate_cfg(cfg: dict):
if not verify_keys.get(vp, "").strip(): if not verify_keys.get(vp, "").strip():
errors.append(f"验证模型 {vp} 的 API Key 未填写") errors.append(f"验证模型 {vp} 的 API Key 未填写")
return (len(errors) == 0), errors if not cfg.get("brand", "").strip():
warnings.append("品牌名称未填写(部分功能需要)")
if not cfg.get("advantages", "").strip():
warnings.append("核心优势未填写(部分功能需要)")
return (len(errors) == 0), errors + warnings
def model_defaults(provider: str) -> str: def model_defaults(provider: str) -> str:
@@ -408,9 +399,14 @@ with st.sidebar:
st.session_state.cfg_applied = False st.session_state.cfg_applied = False
if not st.session_state.cfg_valid: if not st.session_state.cfg_valid:
st.warning("配置未满足运行条件:\n- " + "\n- ".join(st.session_state.cfg_errors)) with st.container(border=True):
st.markdown("**⚠️ 完成配置后即可使用全部功能**")
for err in st.session_state.cfg_errors:
st.markdown(f"{err}")
else: else:
st.success("配置已就绪,可运行全部模块。") with st.container(border=True):
st.markdown("**✅ 配置已就绪**")
st.caption("所有功能已解锁,可以开始使用")
st.markdown("---") st.markdown("---")
if st.button("重置全部结果(不删除配置)", use_container_width=True, key="sb_reset_all"): if st.button("重置全部结果(不删除配置)", use_container_width=True, key="sb_reset_all"):
+178 -141
View File
@@ -10,31 +10,47 @@ class ResourceRecommender:
"""GEO 资源推荐器""" """GEO 资源推荐器"""
def __init__(self): def __init__(self):
# GEO 代理列表 # GEO 代理/服务列表
self.agents = [ self.agents = [
{ {
"name": "KrillinAI", "name": "Perplexity AI",
"description": "专业的 GEO 代理服务,提供高质量的内容生成和优化", "description": "AI 搜索引擎,可用于验证 GEO 效果",
"url": "https://krillin.ai", "url": "https://www.perplexity.ai",
"category": "代理服务", "category": "AI 搜索",
"rating": "⭐⭐⭐⭐⭐", "rating": "⭐⭐⭐⭐⭐",
"features": ["内容生成", "SEO 优化", "平台支持"] "features": ["实时搜索", "引用来源", "模型支持"]
}, },
{ {
"name": "AutoGPT", "name": "ChatGPT Search",
"description": "自动化 AI 代理,支持 GEO 内容创作", "description": "OpenAI 的搜索功能,验证品牌在 AI 搜索中的表现",
"url": "https://autogpt.net", "url": "https://chat.openai.com",
"category": "代理服务", "category": "AI 搜索",
"rating": "⭐⭐⭐⭐", "rating": "⭐⭐⭐⭐",
"features": ["自动化", "多任务", "API 集成"] "features": ["GPT-4", "实时联网", "引用分析"]
}, },
{ {
"name": "AgentGPT", "name": "Google SGE",
"description": "基于 GPT 的智能代理,支持 GEO 策略执行", "description": "Google 搜索生成体验,了解 AI 搜索趋势",
"url": "https://agentgpt.reworkd.ai", "url": "https://search.google",
"category": "代理服务", "category": "AI 搜索",
"rating": "⭐⭐⭐⭐", "rating": "⭐⭐⭐⭐",
"features": ["策略规划", "任务执行", "结果分析"] "features": ["AI 摘要", "来源引用", "搜索结果"]
},
{
"name": "Jasper AI",
"description": "AI 内容创作平台,支持 SEO 优化内容生成",
"url": "https://www.jasper.ai",
"category": "内容生成",
"rating": "⭐⭐⭐⭐",
"features": ["模板丰富", "品牌声音", "SEO 优化"]
},
{
"name": "Surfer SEO",
"description": "SEO 内容优化工具,支持 SERP 分析",
"url": "https://surferseo.com",
"category": "SEO 工具",
"rating": "⭐⭐⭐⭐",
"features": ["内容评分", "关键词分析", "SERP 分析"]
} }
] ]
@@ -42,9 +58,9 @@ class ResourceRecommender:
self.tools = [ self.tools = [
{ {
"name": "Google Search Console", "name": "Google Search Console",
"description": "监控网站搜索表现,优化 GEO 效果", "description": "监控网站在 Google 搜索中的表现",
"url": "https://search.google.com/search-console", "url": "https://search.google.com/search-console",
"category": "SEO 工具", "category": "搜索引擎工具",
"rating": "⭐⭐⭐⭐⭐", "rating": "⭐⭐⭐⭐⭐",
"features": ["搜索分析", "索引监控", "性能报告"] "features": ["搜索分析", "索引监控", "性能报告"]
}, },
@@ -52,44 +68,84 @@ class ResourceRecommender:
"name": "Bing Webmaster Tools", "name": "Bing Webmaster Tools",
"description": "Bing 搜索引擎的网站管理工具", "description": "Bing 搜索引擎的网站管理工具",
"url": "https://www.bing.com/webmasters", "url": "https://www.bing.com/webmasters",
"category": "SEO 工具", "category": "搜索引擎工具",
"rating": "⭐⭐⭐⭐", "rating": "⭐⭐⭐⭐",
"features": ["索引提交", "搜索分析", "URL 检查"] "features": ["索引提交", "搜索分析", "URL 检查"]
}, },
{ {
"name": "Schema.org Validator", "name": "Schema.org Validator",
"description": "验证 JSON-LD Schema 标记", "description": "验证 JSON-LD Schema 标记是否正确",
"url": "https://validator.schema.org", "url": "https://validator.schema.org",
"category": "技术工具", "category": "结构化数据",
"rating": "⭐⭐⭐⭐⭐", "rating": "⭐⭐⭐⭐⭐",
"features": ["Schema 验证", "结构化数据测试", "错误检测"] "features": ["Schema 验证", "结构化数据测试", "错误检测"]
}, },
{ {
"name": "Rich Results Test", "name": "Google Rich Results Test",
"description": "Google 富媒体结果测试工具", "description": "测试网页是否支持 Google 富媒体搜索结果",
"url": "https://search.google.com/test/rich-results", "url": "https://search.google.com/test/rich-results",
"category": "技术工具", "category": "结构化数据",
"rating": "⭐⭐⭐⭐⭐", "rating": "⭐⭐⭐⭐⭐",
"features": ["富媒体测试", "预览效果", "错误诊断"] "features": ["富媒体测试", "预览效果", "错误诊断"]
}, },
{ {
"name": "PageSpeed Insights", "name": "PageSpeed Insights",
"description": "网站性能分析工具,影响 GEO 排名", "description": "分析网页性能,Core Web Vitals 指标",
"url": "https://pagespeed.web.dev", "url": "https://pagespeed.web.dev",
"category": "性能工具", "category": "性能工具",
"rating": "⭐⭐⭐⭐⭐", "rating": "⭐⭐⭐⭐⭐",
"features": ["性能分析", "优化建议", "移动端测试"] "features": ["性能分析", "优化建议", "移动端测试"]
},
{
"name": "Ahrefs",
"description": "SEO 工具套件,关键词研究和竞品分析",
"url": "https://ahrefs.com",
"category": "SEO 工具",
"rating": "⭐⭐⭐⭐⭐",
"features": ["关键词研究", "反向链接分析", "竞品分析"]
},
{
"name": "SEMrush",
"description": "数字营销工具,SEO 和内容营销分析",
"url": "https://www.semrush.com",
"category": "SEO 工具",
"rating": "⭐⭐⭐⭐⭐",
"features": ["关键词研究", "站点审计", "内容优化"]
},
{
"name": "Clearscope",
"description": "AI 内容优化工具,提升内容相关性",
"url": "https://www.clearscope.io",
"category": "内容优化",
"rating": "⭐⭐⭐⭐",
"features": ["内容评分", "关键词建议", "竞品分析"]
} }
] ]
# 论文/指南链接 # 论文/指南链接
self.papers = [ self.papers = [
{
"title": "GEO: Generative Engine Optimization (arXiv)",
"description": "GEO 原始研究论文,定义了生成式引擎优化的概念和方法",
"url": "https://arxiv.org/abs/2311.09735",
"category": "学术论文",
"date": "2023",
"importance": ""
},
{ {
"title": "Google E-E-A-T Guidelines", "title": "Google E-E-A-T Guidelines",
"description": "Google 官方 E-E-A-T 指南,GEO 核心原则", "description": "Google 官方 E-E-A-T 指南,GEO 核心原则",
"url": "https://developers.google.com/search/docs/fundamentals/creating-helpful-content", "url": "https://developers.google.com/search/docs/fundamentals/creating-helpful-content",
"category": "官方指南", "category": "官方指南",
"date": "2023", "date": "2024",
"importance": ""
},
{
"title": "Google Search Quality Rater Guidelines",
"description": "Google 搜索质量评估指南,详细的 E-E-A-T 标准",
"url": "static.googleusercontent.com/media/guidelines.raterhub.com/en//searchqualityevaluatorguidelines.pdf",
"category": "官方指南",
"date": "2024",
"importance": "" "importance": ""
}, },
{ {
@@ -101,88 +157,92 @@ class ResourceRecommender:
"importance": "" "importance": ""
}, },
{ {
"title": "GEO Strategy Guide", "title": "Google Structured Data Guidelines",
"description": "GEOGenerative Engine Optimization)策略指南", "description": "Google 结构化数据指南和最佳实践",
"url": "https://github.com/mprimi/portable-seed", "url": "https://developers.google.com/search/docs/appearance/structured-data",
"category": "策略指南", "category": "技术文档",
"date": "2024", "date": "2024",
"importance": "" "importance": ""
}, },
{ {
"title": "AI Search Optimization", "title": "AI Search Optimization Guide",
"description": "AI 搜索引擎优化最佳实践", "description": "AI 搜索引擎优化最佳实践指南",
"url": "https://www.searchenginejournal.com/ai-search-optimization", "url": "https://www.searchenginejournal.com/ai-search-optimization",
"category": "最佳实践", "category": "最佳实践",
"date": "2024", "date": "2024",
"importance": "" "importance": ""
}, },
{ {
"title": "LLM Prompt Engineering", "title": "LLM Prompt Engineering Guide",
"description": "大语言模型提示工程指南", "description": "大语言模型提示工程完整指南",
"url": "https://www.promptingguide.ai", "url": "https://www.promptingguide.ai",
"category": "技术指南", "category": "技术指南",
"date": "持续更新", "date": "持续更新",
"importance": "" "importance": ""
},
{
"title": "Content Quality Guidelines",
"description": "高质量内容创作指南",
"url": "https://developers.google.com/search/docs/fundamentals/creating-helpful-content",
"category": "内容指南",
"date": "2024",
"importance": ""
} }
] ]
# 社区资源 # 社区资源
self.communities = [ self.communities = [
{ {
"name": "GEO Reddit Community", "name": "r/SEO (Reddit)",
"description": "GEO 相关讨论和经验分享", "description": "Reddit SEO 社区,讨论 SEO 和 GEO 策略",
"url": "https://www.reddit.com/r/SEO", "url": "https://www.reddit.com/r/SEO",
"category": "社区论坛", "category": "论坛社区",
"rating": "⭐⭐⭐⭐⭐"
},
{
"name": "r/ChatGPT (Reddit)",
"description": "ChatGPT 社区,讨论 AI 搜索和 GEO 应用",
"url": "https://www.reddit.com/r/ChatGPT",
"category": "论坛社区",
"rating": "⭐⭐⭐⭐" "rating": "⭐⭐⭐⭐"
}, },
{ {
"name": "AI SEO Discord", "name": "SEO Twitter/X Community",
"description": "AI SEO 和 GEO 技术交流社区", "description": "SEO 和 GEO 从业者 Twitter 社区",
"url": "https://discord.gg/ai-seo", "url": "https://twitter.com/search?q=SEO%20GEO",
"category": "区论坛", "category": "交媒体",
"rating": "⭐⭐⭐⭐"
},
{
"name": "Google Search Central Community",
"description": "Google 官方搜索社区",
"url": "https://support.google.com/webmasters/community",
"category": "官方社区",
"rating": "⭐⭐⭐⭐⭐"
},
{
"name": "Moz Community",
"description": "Moz SEO 社区,丰富的 SEO 资源",
"url": "https://moz.com/community",
"category": "论坛社区",
"rating": "⭐⭐⭐⭐" "rating": "⭐⭐⭐⭐"
} }
] ]
def get_agents(self, category: Optional[str] = None) -> List[Dict]: def get_agents(self, category: Optional[str] = None) -> List[Dict]:
""" """获取 GEO 代理列表"""
获取 GEO 代理列表
Args:
category: 分类筛选(可选)
Returns:
代理列表
"""
if category: if category:
return [agent for agent in self.agents if agent.get("category") == category] return [agent for agent in self.agents if agent.get("category") == category]
return self.agents return self.agents
def get_tools(self, category: Optional[str] = None) -> List[Dict]: def get_tools(self, category: Optional[str] = None) -> List[Dict]:
""" """获取工具推荐列表"""
获取工具推荐列表
Args:
category: 分类筛选(可选)
Returns:
工具列表
"""
if category: if category:
return [tool for tool in self.tools if tool.get("category") == category] return [tool for tool in self.tools if tool.get("category") == category]
return self.tools return self.tools
def get_papers(self, category: Optional[str] = None, importance: Optional[str] = None) -> List[Dict]: def get_papers(self, category: Optional[str] = None, importance: Optional[str] = None) -> List[Dict]:
""" """获取论文/指南列表"""
获取论文/指南列表
Args:
category: 分类筛选(可选)
importance: 重要性筛选(可选:高、中、低)
Returns:
论文/指南列表
"""
result = self.papers result = self.papers
if category: if category:
result = [p for p in result if p.get("category") == category] result = [p for p in result if p.get("category") == category]
@@ -191,81 +251,58 @@ class ResourceRecommender:
return result return result
def get_communities(self) -> List[Dict]: def get_communities(self) -> List[Dict]:
""" """获取社区资源列表"""
获取社区资源列表
Returns:
社区列表
"""
return self.communities return self.communities
def search_resources(self, query: str, resource_type: Optional[str] = None) -> List[Dict]: def get_resource_summary(self) -> Dict:
""" """获取资源统计摘要"""
搜索资源(简单文本匹配)
Args:
query: 搜索关键词
resource_type: 资源类型(agents, tools, papers, communities
Returns:
匹配的资源列表
"""
query_lower = query.lower()
results = []
if resource_type is None or resource_type == "agents":
for agent in self.agents:
if (query_lower in agent["name"].lower() or
query_lower in agent["description"].lower() or
any(query_lower in f.lower() for f in agent.get("features", []))):
results.append({**agent, "type": "agent"})
if resource_type is None or resource_type == "tools":
for tool in self.tools:
if (query_lower in tool["name"].lower() or
query_lower in tool["description"].lower() or
any(query_lower in f.lower() for f in tool.get("features", []))):
results.append({**tool, "type": "tool"})
if resource_type is None or resource_type == "papers":
for paper in self.papers:
if (query_lower in paper["title"].lower() or
query_lower in paper["description"].lower()):
results.append({**paper, "type": "paper"})
if resource_type is None or resource_type == "communities":
for community in self.communities:
if (query_lower in community["name"].lower() or
query_lower in community["description"].lower()):
results.append({**community, "type": "community"})
return results
def get_categories(self) -> Dict[str, List[str]]:
"""
获取所有分类
Returns:
分类字典
"""
return {
"agents": list(set(agent["category"] for agent in self.agents)),
"tools": list(set(tool["category"] for tool in self.tools)),
"papers": list(set(paper["category"] for paper in self.papers)),
"communities": list(set(community["category"] for community in self.communities))
}
def get_resource_summary(self) -> Dict[str, int]:
"""
获取资源统计摘要
Returns:
统计字典
"""
return { return {
"total": len(self.agents) + len(self.tools) + len(self.papers) + len(self.communities),
"agents": len(self.agents), "agents": len(self.agents),
"tools": len(self.tools), "tools": len(self.tools),
"papers": len(self.papers), "papers": len(self.papers),
"communities": len(self.communities), "communities": len(self.communities)
"total": len(self.agents) + len(self.tools) + len(self.papers) + len(self.communities)
} }
def search_resources(self, query: str, resource_type: Optional[str] = None) -> List[Dict]:
"""搜索资源"""
query_lower = query.lower()
results = []
# 搜索所有资源
all_resources = []
if resource_type is None or resource_type == "agents":
for agent in self.agents:
agent["type"] = "agent"
all_resources.append(agent)
if resource_type is None or resource_type == "tools":
for tool in self.tools:
tool["type"] = "tool"
all_resources.append(tool)
if resource_type is None or resource_type == "papers":
for paper in self.papers:
paper["type"] = "paper"
all_resources.append(paper)
if resource_type is None or resource_type == "communities":
for community in self.communities:
community["type"] = "community"
all_resources.append(community)
# 搜索匹配
for resource in all_resources:
name = resource.get("name", resource.get("title", "")).lower()
description = resource.get("description", "").lower()
category = resource.get("category", "").lower()
features = " ".join(resource.get("features", [])).lower()
if (query_lower in name or
query_lower in description or
query_lower in category or
query_lower in features):
results.append(resource)
return results
+1 -1
View File
@@ -28,7 +28,7 @@ def init_session_state():
# 关键词模块 # 关键词模块
ss_init("keywords", []) ss_init("keywords", [])
ss_init("kw_last_num", 40) ss_init("kw_last_num", 20)
ss_init("kw_generation_mode", "AI生成") ss_init("kw_generation_mode", "AI生成")
ss_init("wordbanks", None) ss_init("wordbanks", None)
+2 -2
View File
@@ -92,7 +92,7 @@ def render_tab_autowrite(
keywords_to_generate = [] keywords_to_generate = []
if mode == "单篇生成": if mode == "单篇生成":
col1, col2 = st.columns([2, 1]) col1, col2 = st.columns(2)
with col1: with col1:
selected_keyword = st.selectbox( selected_keyword = st.selectbox(
"选择关键词", "选择关键词",
@@ -110,7 +110,7 @@ def render_tab_autowrite(
if selected_keyword: if selected_keyword:
keywords_to_generate = [(selected_keyword, platform)] keywords_to_generate = [(selected_keyword, platform)]
else: else:
col1, col2 = st.columns([3, 1]) col1, col2 = st.columns(2)
with col1: with col1:
selected_keywords = st.multiselect( selected_keywords = st.multiselect(
"选择关键词(可多选)", "选择关键词(可多选)",
+2 -2
View File
@@ -64,13 +64,13 @@ def render_tab_config_optimizer(
st.markdown(f"**竞品列表**{', '.join(competitor_list[:5])}{'...' if len(competitor_list) > 5 else ''}") st.markdown(f"**竞品列表**{', '.join(competitor_list[:5])}{'...' if len(competitor_list) > 5 else ''}")
# 分析按钮 # 分析按钮
col1, col2 = st.columns([1, 3]) col1, col2 = st.columns([1, 1])
with col1: with col1:
analyze_btn = st.button("🔍 分析配置优化", type="primary", use_container_width=True, key="tab10_optimize_config") analyze_btn = st.button("🔍 分析配置优化", type="primary", use_container_width=True, key="tab10_optimize_config")
with col2: with col2:
if st.session_state.config_optimization_result: if st.session_state.config_optimization_result:
st.success("✅ 已有优化结果,可直接查看下方建议") st.success("✅ 已有优化结果")
# 执行分析 # 执行分析
if analyze_btn: if analyze_btn:
+1 -8
View File
@@ -140,7 +140,6 @@ def render_tab_keywords(storage, ss_init, gen_llm, brand: str, advantages: str)
st.rerun() st.rerun()
# 统一更新所有词库按钮 # 统一更新所有词库按钮
st.markdown("---")
if st.button( if st.button(
"💾 更新所有词库", "💾 更新所有词库",
use_container_width=True, use_container_width=True,
@@ -179,8 +178,6 @@ def render_tab_keywords(storage, ss_init, gen_llm, brand: str, advantages: str)
key="kw_export_json", key="kw_export_json",
) )
st.markdown("---")
# 导入 # 导入
uploaded_wordbanks = st.file_uploader( uploaded_wordbanks = st.file_uploader(
"导入词库(JSON", "导入词库(JSON",
@@ -199,8 +196,6 @@ def render_tab_keywords(storage, ss_init, gen_llm, brand: str, advantages: str)
except Exception as e: except Exception as e:
st.error(f"导入失败:{e}") st.error(f"导入失败:{e}")
st.markdown("---")
# 重置为默认词库 # 重置为默认词库
if st.button( if st.button(
"重置为默认词库", "重置为默认词库",
@@ -213,8 +208,6 @@ def render_tab_keywords(storage, ss_init, gen_llm, brand: str, advantages: str)
st.success("已重置为默认词库") st.success("已重置为默认词库")
st.rerun() st.rerun()
st.markdown("---")
# ========== 区域 3:生成控制 ========== # ========== 区域 3:生成控制 ==========
with st.container(border=True): with st.container(border=True):
st.markdown("**⚙️ 生成控制**") st.markdown("**⚙️ 生成控制**")
@@ -1251,7 +1244,7 @@ def render_tab_keywords(storage, ss_init, gen_llm, brand: str, advantages: str)
search_col, filter_col = st.columns([3, 1]) search_col, filter_col = st.columns([3, 1])
with search_col: with search_col:
search_term = st.text_input( search_term = st.text_input(
"🔍 搜索关键词", key="kw_search", placeholder="输入关键词搜索..." "搜索关键词", key="kw_search", placeholder="🔍 输入关键词搜索...", label_visibility="collapsed"
) )
with filter_col: with filter_col:
show_original = st.checkbox( show_original = st.checkbox(
+13 -2
View File
@@ -79,7 +79,18 @@ def _render_upload_section(kb: KnowledgeBase):
) )
if uploaded_file: if uploaded_file:
content = uploaded_file.read().decode("utf-8") # 安全解码文件内容
b = uploaded_file.read()
content = None
for enc in ("utf-8-sig", "utf-8", "gb18030", "gbk"):
try:
content = b.decode(enc)
break
except Exception:
pass
if content is None:
content = b.decode("utf-8", errors="replace")
st.text_area("文件预览", content[:1000] + "..." if len(content) > 1000 else content, st.text_area("文件预览", content[:1000] + "..." if len(content) > 1000 else content,
height=150, disabled=True) height=150, disabled=True)
@@ -169,7 +180,7 @@ def _render_search_test(kb: KnowledgeBase):
st.markdown("#### 搜索测试") st.markdown("#### 搜索测试")
st.caption("测试知识库检索效果,验证文档是否被正确索引") st.caption("测试知识库检索效果,验证文档是否被正确索引")
query = st.text_input("输入测试查询", placeholder="例如:产品有什么优势?") query = st.text_input("输入测试查询", placeholder="🔍 例如:产品有什么优势?", label_visibility="collapsed")
col1, col2 = st.columns(2) col1, col2 = st.columns(2)
with col1: with col1:
-1
View File
@@ -38,7 +38,6 @@ def render_tab_optimize(
) )
# === 文章优化功能(主流程) === # === 文章优化功能(主流程) ===
st.markdown("---")
st.markdown("**✏️ 文章内容优化**") st.markdown("**✏️ 文章内容优化**")
with st.container(border=True): with st.container(border=True):
+1 -1
View File
@@ -49,7 +49,7 @@ def render_tab_platform_sync(storage, brand: str) -> None:
key="github_repo_name" key="github_repo_name"
) )
col1, col2 = st.columns([1, 4]) col1, col2 = st.columns([1, 1])
with col1: with col1:
if st.button("💾 保存配置", type="primary", use_container_width=True): if st.button("💾 保存配置", type="primary", use_container_width=True):
if github_api_key and github_repo_owner and github_repo_name: if github_api_key and github_repo_owner and github_repo_name:
-11
View File
@@ -148,7 +148,6 @@ def render_tab_reports(
st.info("📊 暂无验证数据。请先运行自动验证任务或手动验证。") st.info("📊 暂无验证数据。请先运行自动验证任务或手动验证。")
else: else:
# 数据概览 # 数据概览
st.markdown("---")
st.markdown("#### 📈 数据概览") st.markdown("#### 📈 数据概览")
col1, col2, col3, col4 = st.columns(4) col1, col2, col3, col4 = st.columns(4)
@@ -173,7 +172,6 @@ def render_tab_reports(
# 1. 提及率趋势图 # 1. 提及率趋势图
if "验证时间" in verify_df.columns and len(verify_df) > 0: if "验证时间" in verify_df.columns and len(verify_df) > 0:
st.markdown("---")
st.markdown("#### 📊 提及率趋势图") st.markdown("#### 📊 提及率趋势图")
# 按日期聚合数据 # 按日期聚合数据
@@ -196,7 +194,6 @@ def render_tab_reports(
st.plotly_chart(fig_trend, use_container_width=True) st.plotly_chart(fig_trend, use_container_width=True)
# 2. 平台贡献度分析(基于文章平台) # 2. 平台贡献度分析(基于文章平台)
st.markdown("---")
st.markdown("#### 🌐 平台贡献度分析") st.markdown("#### 🌐 平台贡献度分析")
articles = storage.get_articles(brand=brand) articles = storage.get_articles(brand=brand)
@@ -223,7 +220,6 @@ def render_tab_reports(
st.info("暂无文章数据。") st.info("暂无文章数据。")
# 话题集群分析模块 # 话题集群分析模块
st.markdown("---")
st.markdown("#### 🎯 话题集群分析") st.markdown("#### 🎯 话题集群分析")
st.caption("基于历史关键词生成话题集群,分析内容覆盖情况,发现内容盲区") st.caption("基于历史关键词生成话题集群,分析内容覆盖情况,发现内容盲区")
@@ -390,10 +386,8 @@ def render_tab_reports(
ideas = suggestion.get('content_ideas', []) ideas = suggestion.get('content_ideas', [])
if ideas: if ideas:
st.markdown(f"- **内容创意**{', '.join(ideas[:3])}") st.markdown(f"- **内容创意**{', '.join(ideas[:3])}")
st.markdown("---")
# ROI 分析与成本优化模块 # ROI 分析与成本优化模块
st.markdown("---")
st.markdown("#### 💰 ROI 分析与成本优化") st.markdown("#### 💰 ROI 分析与成本优化")
st.caption("量化 GEO 投入产出比,优化成本结构,数据驱动决策") st.caption("量化 GEO 投入产出比,优化成本结构,数据驱动决策")
@@ -621,7 +615,6 @@ def render_tab_reports(
) )
# 3. 内容质量指标分析 # 3. 内容质量指标分析
st.markdown("---")
st.markdown("#### 📈 内容质量指标分析") st.markdown("#### 📈 内容质量指标分析")
st.caption("分析内容的信任度、权威性、参与度等关键指标,量化内容质量") st.caption("分析内容的信任度、权威性、参与度等关键指标,量化内容质量")
@@ -797,7 +790,6 @@ def render_tab_reports(
st.error(f"获取内容质量指标失败:{e}") st.error(f"获取内容质量指标失败:{e}")
# 4. 关键词效果排名 # 4. 关键词效果排名
st.markdown("---")
st.markdown("#### 🎯 关键词效果排名") st.markdown("#### 🎯 关键词效果排名")
brand_verify = verify_df[verify_df["品牌"] == brand].copy() brand_verify = verify_df[verify_df["品牌"] == brand].copy()
@@ -828,7 +820,6 @@ def render_tab_reports(
st.info("暂无品牌验证数据。") st.info("暂无品牌验证数据。")
# 4. 竞品对比分析 # 4. 竞品对比分析
st.markdown("---")
st.markdown("#### ⚔️ 竞品对比分析") st.markdown("#### ⚔️ 竞品对比分析")
if len(competitor_list) > 0: if len(competitor_list) > 0:
@@ -870,7 +861,6 @@ def render_tab_reports(
st.info("💡 提示:在侧边栏配置竞品品牌后,可查看竞品对比分析。") st.info("💡 提示:在侧边栏配置竞品品牌后,可查看竞品对比分析。")
# 5. 负面防护监控报告 # 5. 负面防护监控报告
st.markdown("---")
st.markdown("#### 🛡️ 负面防护监控报告") st.markdown("#### 🛡️ 负面防护监控报告")
st.caption("分析负面查询中的品牌提及情况,提供风险预警和优化建议") st.caption("分析负面查询中的品牌提及情况,提供风险预警和优化建议")
@@ -981,7 +971,6 @@ def render_tab_reports(
st.error(f"生成负面监控报告失败:{e}") st.error(f"生成负面监控报告失败:{e}")
# 6. 数据导出 # 6. 数据导出
st.markdown("---")
st.markdown("#### 💾 数据导出") st.markdown("#### 💾 数据导出")
col1, col2 = st.columns(2) col1, col2 = st.columns(2)
+63 -137
View File
@@ -3,93 +3,86 @@
import streamlit as st import streamlit as st
from modules.resource_recommender import ResourceRecommender from modules.resource_recommender import ResourceRecommender
from modules.ui.components import render_tab_top_with_clear
def render_tab_resources(storage, brand: str) -> None: def render_tab_resources(storage, brand: str) -> None:
"""渲染 Tab8:GEO 资源库。由主入口在 with tab8 内调用。""" """渲染 Tab8:GEO 资源库。由主入口在 with tab8 内调用。"""
st.markdown("### 📚 GEO 资源库") render_tab_top_with_clear(
st.caption("发现 GEO 相关工具、代理、论文和社区资源,增强工具生态") title="📚 GEO 资源库",
caption="发现 GEO 相关工具、代理、论文和社区资源,增强工具生态",
clear_key="resources_clear",
on_clear=lambda: None, # 资源库无需清空
)
resource_recommender = ResourceRecommender() resource_recommender = ResourceRecommender()
# 资源统计概览 # 资源统计概览
summary = resource_recommender.get_resource_summary() summary = resource_recommender.get_resource_summary()
stat_col1, stat_col2, stat_col3, stat_col4, stat_col5 = st.columns(5) stat_cols = st.columns(5)
with stat_col1: stats = [
st.metric("总资源数", summary['total']) ("总资源数", summary['total']),
with stat_col2: ("AI 搜索", summary['agents']),
st.metric("代理服务", summary['agents']) ("工具推荐", summary['tools']),
with stat_col3: ("论文指南", summary['papers']),
st.metric("工具推荐", summary['tools']) ("社区资源", summary['communities']),
with stat_col4: ]
st.metric("论文/指南", summary['papers']) for col, (label, value) in zip(stat_cols, stats):
with stat_col5: with col:
st.metric("社区资源", summary['communities']) st.metric(label, value)
st.markdown("---")
# 搜索功能 # 搜索功能
search_col1, search_col2 = st.columns([3, 1]) search_col1, search_col2 = st.columns([4, 1])
with search_col1: with search_col1:
search_query = st.text_input( search_query = st.text_input(
"🔍 搜索资源", "搜索资源",
key="resource_search", key="resource_search",
placeholder="输入关键词搜索代理、工具、论文、社区...", placeholder="🔍 输入关键词搜索资源名称、描述、功能特性...",
help="支持搜索资源名称、描述、功能特性等" label_visibility="collapsed"
) )
with search_col2: with search_col2:
clear_search = st.button("清除搜索", use_container_width=True, key="clear_resource_search") if st.button("清除", use_container_width=True, key="clear_resource_search"):
if clear_search:
st.session_state.resource_search = "" st.session_state.resource_search = ""
st.rerun() st.rerun()
# 资源分类标签 # 资源分类标签
resource_tab1, resource_tab2, resource_tab3, resource_tab4 = st.tabs(["🤖 GEO 代理", "🛠️ 工具推荐", "📄 论文/指南", "👥 社区资源"]) resource_tab1, resource_tab2, resource_tab3, resource_tab4 = st.tabs([
"🤖 AI 搜索", "🛠️ 工具推荐", "📄 论文指南", "👥 社区资源"
])
# GEO 代理 # AI 搜索/代理
with resource_tab1: with resource_tab1:
st.markdown("#### 🤖 GEO 代理服务") st.caption("AI 搜索引擎和内容生成服务,用于验证和优化 GEO 效果")
st.caption("专业的 GEO 代理服务,提供高质量的内容生成和优化")
if search_query: if search_query:
agents = resource_recommender.search_resources(search_query, "agents") agents = resource_recommender.search_resources(search_query, "agents")
if agents:
st.info(f"🔍 找到 {len(agents)} 个匹配的代理服务")
else: else:
agents = resource_recommender.get_agents() agents = resource_recommender.get_agents()
if agents: if agents:
for i, agent in enumerate(agents, 1): for agent in agents:
with st.container(border=True): with st.container(border=True):
col1, col2 = st.columns([3, 1]) col1, col2 = st.columns([3, 1])
with col1: with col1:
st.markdown(f"##### {i}. {agent['name']} {agent.get('rating', '')}") st.markdown(f"**{agent['name']}** {agent.get('rating', '')}")
with col2: with col2:
if agent.get('url'): if agent.get('url'):
st.markdown(f"[🔗 访问]({agent['url']})") st.link_button("访问", agent['url'], use_container_width=True)
st.markdown(f"**{agent['description']}**") st.caption(agent['description'])
st.markdown(f"**分类**{agent.get('category', 'N/A')}")
if agent.get('features'): if agent.get('features'):
st.markdown("**功能特性**") features_text = " · ".join([f"{f}" for f in agent['features']])
features_text = " | ".join([f"{f}" for f in agent['features']]) st.markdown(f"<small>{features_text}</small>", unsafe_allow_html=True)
st.markdown(features_text)
if agent.get('url'):
st.markdown(f"**链接**{agent['url']}")
else: else:
st.info("💡 暂无匹配的代理资源尝试使用其他关键词搜索。") st.info("💡 暂无匹配的资源尝试其他关键词搜索。")
# 工具推荐 # 工具推荐
with resource_tab2: with resource_tab2:
st.markdown("#### 🛠️ 工具推荐")
st.caption("GEO 相关的工具和服务,帮助优化内容效果") st.caption("GEO 相关的工具和服务,帮助优化内容效果")
if search_query: if search_query:
tools = resource_recommender.search_resources(search_query, "tools") tools = resource_recommender.search_resources(search_query, "tools")
if tools:
st.info(f"🔍 找到 {len(tools)} 个匹配的工具")
else: else:
tools = resource_recommender.get_tools() tools = resource_recommender.get_tools()
@@ -103,144 +96,77 @@ def render_tab_resources(storage, brand: str) -> None:
categories[cat].append(tool) categories[cat].append(tool)
for category, category_tools in categories.items(): for category, category_tools in categories.items():
st.markdown(f"##### 📁 {category}") st.markdown(f"**{category}**")
for i, tool in enumerate(category_tools, 1): for tool in category_tools:
with st.container(border=True): with st.container(border=True):
col1, col2 = st.columns([3, 1]) col1, col2 = st.columns([3, 1])
with col1: with col1:
st.markdown(f"**{tool['name']}** {tool.get('rating', '')}") st.markdown(f"**{tool['name']}** {tool.get('rating', '')}")
with col2: with col2:
if tool.get('url'): if tool.get('url'):
st.markdown(f"[🔗 访问]({tool['url']})") st.link_button("访问", tool['url'], use_container_width=True)
st.markdown(f"*{tool['description']}*") st.caption(tool['description'])
if tool.get('features'): if tool.get('features'):
st.markdown("**功能**") features_text = " · ".join([f"{f}" for f in tool['features']])
features_text = " | ".join([f"{f}" for f in tool['features']]) st.markdown(f"<small>{features_text}</small>", unsafe_allow_html=True)
st.markdown(features_text)
if tool.get('url'):
st.markdown(f"**链接**{tool['url']}")
else: else:
st.info("💡 暂无匹配的工具资源。尝试使用其他关键词搜索。") st.info("💡 暂无匹配的工具,尝试其他关键词搜索。")
# 论文/指南 # 论文/指南
with resource_tab3: with resource_tab3:
st.markdown("#### 📄 论文/指南")
st.caption("GEO 相关的论文、指南、文档,深入学习 GEO 策略") st.caption("GEO 相关的论文、指南、文档,深入学习 GEO 策略")
if search_query: if search_query:
papers = resource_recommender.search_resources(search_query, "papers") papers = resource_recommender.search_resources(search_query, "papers")
if papers:
st.info(f"🔍 找到 {len(papers)} 个匹配的论文/指南")
else: else:
papers = resource_recommender.get_papers() papers = resource_recommender.get_papers()
if papers: if papers:
# 按重要性排序 # 按重要性分组显示
importance_order = {"": 3, "": 2, "": 1} importance_order = {"": 3, "": 2, "": 1}
papers_sorted = sorted(papers, key=lambda x: importance_order.get(x.get('importance', ''), 1), reverse=True) papers_sorted = sorted(papers, key=lambda x: importance_order.get(x.get('importance', ''), 1), reverse=True)
# 按重要性分组显示 importance_groups = {
high_importance = [p for p in papers_sorted if p.get('importance') == ''] "🔥 必读": [p for p in papers_sorted if p.get('importance') == ''],
medium_importance = [p for p in papers_sorted if p.get('importance') == ''] "⭐ 推荐": [p for p in papers_sorted if p.get('importance') == ''],
low_importance = [p for p in papers_sorted if p.get('importance') == ''] }
if high_importance: for group_name, group_papers in importance_groups.items():
st.markdown("##### 🔥 高重要性(必读)") if group_papers:
for paper in high_importance: st.markdown(f"**{group_name}**")
for paper in group_papers:
with st.container(border=True): with st.container(border=True):
st.markdown(f"**🔥 {paper['title']}**") st.markdown(f"**{paper['title']}**")
st.markdown(f"*{paper['description']}*") st.caption(paper['description'])
st.markdown(f"**分类**{paper.get('category', 'N/A')} | **日期**{paper.get('date', 'N/A')}") meta = f"分类{paper.get('category', 'N/A')} · 日期{paper.get('date', 'N/A')}"
st.markdown(f"<small>{meta}</small>", unsafe_allow_html=True)
if paper.get('url'): if paper.get('url'):
st.markdown(f"🔗 [{paper['url']}]({paper['url']})") st.link_button("查看", paper['url'], use_container_width=True)
if medium_importance:
st.markdown("##### ⭐ 中重要性(推荐阅读)")
for paper in medium_importance:
with st.container(border=True):
st.markdown(f"**⭐ {paper['title']}**")
st.markdown(f"*{paper['description']}*")
st.markdown(f"**分类**{paper.get('category', 'N/A')} | **日期**{paper.get('date', 'N/A')}")
if paper.get('url'):
st.markdown(f"🔗 [{paper['url']}]({paper['url']})")
if low_importance:
st.markdown("##### 📌 低重要性(参考阅读)")
for paper in low_importance:
with st.container(border=True):
st.markdown(f"**📌 {paper['title']}**")
st.markdown(f"*{paper['description']}*")
st.markdown(f"**分类**{paper.get('category', 'N/A')} | **日期**{paper.get('date', 'N/A')}")
if paper.get('url'):
st.markdown(f"🔗 [{paper['url']}]({paper['url']})")
else: else:
st.info("💡 暂无匹配的论文/指南资源。尝试使用其他关键词搜索。") st.info("💡 暂无匹配的论文/指南,尝试其他关键词搜索。")
# 社区资源 # 社区资源
with resource_tab4: with resource_tab4:
st.markdown("#### 👥 社区资源")
st.caption("GEO 相关的社区和论坛,与其他用户交流经验") st.caption("GEO 相关的社区和论坛,与其他用户交流经验")
if search_query: if search_query:
communities = resource_recommender.search_resources(search_query, "communities") communities = resource_recommender.search_resources(search_query, "communities")
if communities:
st.info(f"🔍 找到 {len(communities)} 个匹配的社区")
else: else:
communities = resource_recommender.get_communities() communities = resource_recommender.get_communities()
if communities: if communities:
for i, community in enumerate(communities, 1): for community in communities:
with st.container(border=True): with st.container(border=True):
col1, col2 = st.columns([3, 1]) col1, col2 = st.columns([3, 1])
with col1: with col1:
st.markdown(f"##### {i}. {community['name']} {community.get('rating', '')}") st.markdown(f"**{community['name']}** {community.get('rating', '')}")
with col2: with col2:
if community.get('url'): if community.get('url'):
st.markdown(f"[🔗 访问]({community['url']})") st.link_button("访问", community['url'], use_container_width=True)
st.markdown(f"*{community['description']}*") st.caption(community['description'])
st.markdown(f"**分类**{community.get('category', 'N/A')}") st.markdown(f"<small>分类{community.get('category', 'N/A')}</small>", unsafe_allow_html=True)
if community.get('url'):
st.markdown(f"**链接**{community['url']}")
else: else:
st.info("💡 暂无匹配的社区资源尝试使用其他关键词搜索。") st.info("💡 暂无匹配的社区资源尝试其他关键词搜索。")
# 搜索结果显示(跨分类)
if search_query:
all_results = resource_recommender.search_resources(search_query)
if all_results:
st.markdown("---")
st.markdown("#### 🔍 搜索结果汇总")
st.info(f"共找到 {len(all_results)} 个匹配资源(跨所有分类)")
# 按类型分组显示
results_by_type = {}
for result in all_results:
res_type = result.get('type', 'unknown')
if res_type not in results_by_type:
results_by_type[res_type] = []
results_by_type[res_type].append(result)
type_names = {
'agent': '🤖 代理服务',
'tool': '🛠️ 工具',
'paper': '📄 论文/指南',
'community': '👥 社区'
}
for res_type, results in results_by_type.items():
if results:
st.markdown(f"##### {type_names.get(res_type, res_type)} ({len(results)} 个)")
for result in results:
with st.container(border=True):
name_key = 'name' if 'name' in result else 'title'
st.markdown(f"**{result.get(name_key, 'N/A')}**")
st.caption(result.get('description', ''))
if result.get('url'):
st.markdown(f"🔗 [{result['url']}]({result['url']})")
# =======================
+1 -3
View File
@@ -91,8 +91,6 @@ def render_tab_validation(
st.success("✅ 负面查询已添加到验证查询中") st.success("✅ 负面查询已添加到验证查询中")
st.rerun() st.rerun()
st.markdown("---")
with st.container(border=True): with st.container(border=True):
with st.form("verify_form", clear_on_submit=False): with st.form("verify_form", clear_on_submit=False):
test_queries = st.text_area( test_queries = st.text_area(
@@ -104,7 +102,7 @@ def render_tab_validation(
st.session_state.verify_last_queries = test_queries st.session_state.verify_last_queries = test_queries
run_verify_disabled = (not st.session_state.cfg_valid) or (not verify_llms) or (not test_queries.strip()) run_verify_disabled = (not st.session_state.cfg_valid) or (not verify_llms) or (not test_queries.strip())
run_verify = st.form_submit_button("开始验证", use_container_width=True, disabled=run_verify_disabled) run_verify = st.form_submit_button("🔍 开始验证", use_container_width=True, disabled=run_verify_disabled)
# 获取负面监控开关状态 # 获取负面监控开关状态
negative_monitor_enabled = st.session_state.get("negative_monitor_enabled", False) negative_monitor_enabled = st.session_state.get("negative_monitor_enabled", False)
+2 -1
View File
@@ -58,7 +58,8 @@ def render_tab_workflow(
st.rerun() st.rerun()
with col3: with col3:
if st.button("▶️ 执行", key=f"run_{workflow['id']}", use_container_width=True): if st.button("▶️ 执行", key=f"run_{workflow['id']}", use_container_width=True,
disabled=gen_llm is None):
# 创建回调函数 # 创建回调函数
def generate_keywords_callback(num_keywords, generation_mode, brand, advantages): def generate_keywords_callback(num_keywords, generation_mode, brand, advantages):
"""关键词生成回调函数""" """关键词生成回调函数"""
+448 -174
View File
@@ -2,250 +2,524 @@ import streamlit as st
def inject_global_theme(): def inject_global_theme():
"""注入全局 CSS 主题,极简克制的样式优化""" """注入全局 CSS 主题 - 紧凑、高信息密度的 C 端产品 UI"""
st.markdown( st.markdown(
""" """
<style> <style>
/* 使用 Google Fonts */ /* ========== 字体导入 ========== */
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=Noto+Sans+SC:wght@400;500;600&display=swap'); @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Noto+Sans+SC:wght@300;400;500;600;700&display=swap');
/* CSS 变量定义 */ /* ========== CSS 变量 ========== */
:root { :root {
--primary-color: #2563EB; --primary: #4F46E5;
--primary-hover: #1D4ED8; --primary-light: #818CF8;
--background-color: #FFFFFF; --primary-dark: #3730A3;
--secondary-bg: #F7FAFC; --primary-bg: #EEF2FF;
--text-color: #1A202C; --success: #059669;
--text-secondary: #718096; --success-bg: #ECFDF5;
--border-color: #E2E8F0; --warning: #D97706;
--border-radius: 10px; --warning-bg: #FFFBEB;
--shadow-sm: 0 1px 2px rgba(0,0,0,0.05); --error: #DC2626;
--shadow-md: 0 4px 6px rgba(0,0,0,0.07); --error-bg: #FEF2F2;
--bg-white: #FFFFFF;
--bg-gray: #F9FAFB;
--bg-light: #F3F4F6;
--border: #E5E7EB;
--border-light: #F3F4F6;
--text-primary: #111827;
--text-secondary: #6B7280;
--text-muted: #9CA3AF;
--shadow-xs: 0 1px 2px rgba(0,0,0,0.05);
--shadow-sm: 0 1px 3px rgba(0,0,0,0.1), 0 1px 2px rgba(0,0,0,0.06);
--shadow-md: 0 4px 6px -1px rgba(0,0,0,0.1), 0 2px 4px -1px rgba(0,0,0,0.06);
--shadow-lg: 0 10px 15px -3px rgba(0,0,0,0.1), 0 4px 6px -2px rgba(0,0,0,0.05);
--radius-sm: 6px;
--radius-md: 8px;
--radius-lg: 12px;
--radius-xl: 16px;
/* 间距变量 - 紧凑风格 */
--space-xs: 0.25rem;
--space-sm: 0.5rem;
--space-md: 0.75rem;
--space-lg: 1rem;
--space-xl: 1.5rem;
} }
/* 全局字体 */ /* ========== 全局基础 ========== */
html, body, .stApp { html, body, .stApp {
font-family: "Inter", "Noto Sans SC", system-ui, sans-serif; font-family: "Inter", "Noto Sans SC", -apple-system, BlinkMacSystemFont, sans-serif;
color: var(--text-primary);
background: var(--bg-gray);
} }
/* ========== 侧边栏样式 ========== */ .main .block-container {
max-width: 1200px;
padding: 1.5rem 1.5rem 2rem;
}
/* ========== 页面标题 ========== */
h1 {
font-weight: 700 !important;
font-size: 1.5rem !important;
color: var(--text-primary) !important;
letter-spacing: -0.025em;
margin-bottom: 0.5rem !important;
}
/* 副标题 */
.main .block-container > div:first-child p {
margin-bottom: 0.5rem;
}
/* ========== 侧边栏 ========== */
section[data-testid="stSidebar"] { section[data-testid="stSidebar"] {
background: var(--secondary-bg); background: var(--bg-white);
border-right: 1px solid var(--border-color); border-right: 1px solid var(--border);
} }
/* 侧边栏 expander 样式 */ section[data-testid="stSidebar"] .block-container {
padding: 1rem 0.75rem;
}
section[data-testid="stSidebar"] h1,
section[data-testid="stSidebar"] h2,
section[data-testid="stSidebar"] h3 {
font-weight: 600 !important;
color: var(--text-primary) !important;
font-size: 0.875rem !important;
margin-bottom: 0.5rem !important;
}
/* 侧边栏 expander */
section[data-testid="stSidebar"] .streamlit-expanderHeader { section[data-testid="stSidebar"] .streamlit-expanderHeader {
background: var(--background-color); background: var(--bg-gray);
border-radius: 8px; border-radius: var(--radius-md);
border: 1px solid var(--border-color); border: 1px solid var(--border);
font-weight: 600; font-weight: 600;
padding: 0.75rem 1rem; font-size: 0.8125rem;
padding: 0.5rem 0.75rem;
margin-bottom: 0.25rem;
} }
section[data-testid="stSidebar"] .streamlit-expanderContent { section[data-testid="stSidebar"] .streamlit-expanderContent {
background: var(--background-color); background: var(--bg-white);
border-radius: 0 0 8px 8px; border: none;
border: 1px solid var(--border-color); padding: 0.5rem 0;
border-top: none;
padding: 1rem;
} }
/* ========== 按钮样式 ========== */ /* ========== KPI 卡片 - 统一大小 ========== */
button { [data-testid="stHorizontalBlock"] {
border-radius: var(--border-radius) !important; display: flex !important;
font-weight: 500; align-items: stretch !important;
transition: all 0.2s ease; gap: 0.75rem !important;
margin-bottom: 0.75rem !important;
} }
button[kind="primary"] { [data-testid="stHorizontalBlock"] > div {
background-color: var(--primary-color) !important; flex: 1 1 0% !important;
color: white !important; min-width: 0 !important;
border: none !important;
} }
button[kind="primary"]:hover {
background-color: var(--primary-hover) !important;
box-shadow: var(--shadow-md);
transform: translateY(-1px);
}
/* ========== 输入框样式 ========== */
.stTextInput input,
.stTextArea textarea,
.stNumberInput input {
border-radius: var(--border-radius) !important;
border: 1px solid var(--border-color) !important;
padding: 0.75rem;
transition: all 0.2s ease;
}
.stTextInput input:focus,
.stTextArea textarea:focus {
border-color: var(--primary-color) !important;
box-shadow: 0 0 0 3px rgba(37,99,235,0.1);
outline: none;
}
/* ========== 选择框样式 ========== */
.stSelectbox [data-baseweb="select"] > div {
border-radius: var(--border-radius) !important;
border: 1px solid var(--border-color) !important;
min-height: 2.5rem;
}
.stSelectbox [data-baseweb="select"]:hover > div {
border-color: #CBD5E0 !important;
}
.stSelectbox [data-baseweb="select"]:focus-within > div {
border-color: var(--primary-color) !important;
box-shadow: 0 0 0 3px rgba(37,99,235,0.1) !important;
}
/* ========== 多选框样式 ========== */
.stMultiSelect [data-baseweb="select"] > div {
border-radius: var(--border-radius) !important;
border: 1px solid var(--border-color) !important;
}
.stMultiSelect [data-baseweb="tag"] {
border-radius: 6px !important;
background: var(--primary-color) !important;
color: white !important;
border: none !important;
}
/* ========== Tabs 样式 ========== */
.stTabs [data-baseweb="tab-list"] {
gap: 0px;
border-bottom: 2px solid var(--border-color);
overflow-x: auto;
flex-wrap: nowrap;
}
.stTabs [data-baseweb="tab"] {
padding: 0.75rem 1.25rem;
border-radius: 0 !important;
background: transparent;
color: var(--text-secondary);
font-weight: 500;
transition: all 0.2s ease;
white-space: nowrap;
}
.stTabs [data-baseweb="tab"]:hover {
background: rgba(37,99,235,0.04);
color: var(--text-color);
}
.stTabs [aria-selected="true"] {
background: transparent !important;
color: var(--primary-color) !important;
font-weight: 600 !important;
border-bottom: 2px solid var(--primary-color) !important;
margin-bottom: -2px;
}
/* ========== Expander 样式 ========== */
.streamlit-expanderHeader {
border-radius: var(--border-radius);
background: var(--secondary-bg);
border: 1px solid var(--border-color);
font-weight: 500;
}
/* ========== 容器边框 ========== */
div[data-testid="stVerticalBlockBorderWrapper"] {
border-radius: var(--border-radius);
border: 1px solid var(--border-color);
box-shadow: var(--shadow-sm);
}
/* ========== Metric 卡片样式 ========== */
[data-testid="stMetric"] { [data-testid="stMetric"] {
background: var(--background-color); background: var(--bg-white);
border-radius: var(--border-radius); border-radius: var(--radius-lg);
padding: 1rem; padding: 1rem 1.25rem;
border: 1px solid var(--border-color); border: 1px solid var(--border);
box-shadow: var(--shadow-sm); box-shadow: var(--shadow-sm);
transition: all 0.2s ease;
height: 90px !important;
display: flex !important;
flex-direction: column !important;
justify-content: center !important;
overflow: hidden;
} }
[data-testid="stMetric"]:hover { [data-testid="stMetric"]:hover {
box-shadow: var(--shadow-md); box-shadow: var(--shadow-md);
transform: translateY(-2px); transform: translateY(-2px);
transition: all 0.2s ease; border-color: var(--primary-light);
} }
/* ========== 响应式设计 ========== */ [data-testid="stMetricLabel"] {
@media (max-width: 768px) { font-size: 0.6875rem !important;
/* KPI 卡片改为 2 列 */ font-weight: 600 !important;
[data-testid="stHorizontalBlock"] { color: var(--text-secondary) !important;
flex-wrap: wrap; text-transform: uppercase;
} letter-spacing: 0.05em;
[data-testid="stHorizontalBlock"] > div { white-space: nowrap;
flex: 1 1 45%; overflow: hidden;
margin-bottom: 0.5rem; text-overflow: ellipsis;
margin-bottom: 0.25rem !important;
}
[data-testid="stMetricValue"] {
font-size: 1.375rem !important;
font-weight: 700 !important;
color: var(--text-primary) !important;
line-height: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
[data-testid="stMetricDelta"] {
font-size: 0.6875rem !important;
font-weight: 500 !important;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
[data-testid="stMetric"] > div {
padding: 0 !important;
margin: 0 !important;
}
/* ========== Tabs 导航 - 紧凑胶囊 ========== */
.stTabs {
margin-top: 0 !important;
} }
/* Tab 栏可滚动 */
.stTabs [data-baseweb="tab-list"] { .stTabs [data-baseweb="tab-list"] {
gap: 0.25rem;
background: var(--bg-white);
border-radius: var(--radius-lg);
border: 1px solid var(--border);
padding: 0.25rem;
box-shadow: var(--shadow-xs);
overflow-x: auto; overflow-x: auto;
-webkit-overflow-scrolling: touch; flex-wrap: nowrap;
margin-bottom: 0 !important;
} }
.stTabs [data-baseweb="tab"] { .stTabs [data-baseweb="tab"] {
padding: 0.5rem 0.875rem;
border-radius: var(--radius-md);
background: transparent;
color: var(--text-secondary);
font-weight: 500;
font-size: 0.8125rem;
transition: all 0.15s ease;
white-space: nowrap;
border: none;
min-height: unset;
}
.stTabs [data-baseweb="tab"]:hover {
background: var(--bg-gray);
color: var(--text-primary);
}
.stTabs [aria-selected="true"] {
background: var(--primary) !important;
color: white !important;
font-weight: 600 !important;
box-shadow: var(--shadow-sm);
}
.stTabs [data-baseweb="tab-border"],
.stTabs [data-baseweb="tab-highlight"] {
display: none;
}
/* Tab 内容区域 - 紧凑 */
.stTabs [data-baseweb="tab-panel"] {
padding: 0.75rem 0;
border: none;
}
/* ========== 按钮 ========== */
button {
border-radius: var(--radius-md) !important;
font-weight: 500;
font-size: 0.8125rem;
transition: all 0.15s ease;
border: 1px solid transparent;
padding: 0.375rem 0.75rem;
}
button[kind="primary"] {
background: linear-gradient(135deg, var(--primary), var(--primary-dark)) !important;
color: white !important;
border: none !important;
box-shadow: var(--shadow-sm);
}
button[kind="primary"]:hover {
background: linear-gradient(135deg, var(--primary-dark), var(--primary)) !important;
box-shadow: var(--shadow-md);
transform: translateY(-1px);
}
button[kind="secondary"] {
background: var(--bg-white);
color: var(--text-primary);
border: 1px solid var(--border) !important;
}
button[kind="secondary"]:hover {
background: var(--bg-gray);
border-color: var(--primary-light) !important;
}
/* ========== 输入框 - 紧凑 ========== */
.stTextInput input,
.stTextArea textarea,
.stNumberInput input {
border-radius: var(--radius-md) !important;
border: 1px solid var(--border) !important;
padding: 0.5rem 0.75rem; padding: 0.5rem 0.75rem;
font-size: 0.8125rem;
transition: all 0.15s ease;
background: var(--bg-white);
}
.stTextInput input:focus,
.stTextArea textarea:focus {
border-color: var(--primary) !important;
box-shadow: 0 0 0 2px var(--primary-bg);
outline: none;
}
.stTextInput label,
.stTextArea label,
.stSelectbox label,
.stMultiSelect label,
.stRadio label,
.stCheckbox label,
.stSlider label {
font-size: 0.75rem !important;
font-weight: 500 !important;
color: var(--text-secondary) !important;
margin-bottom: 0.25rem !important;
}
/* ========== 选择框 ========== */
.stSelectbox [data-baseweb="select"] > div {
border-radius: var(--radius-md) !important;
border: 1px solid var(--border) !important;
min-height: 2.25rem;
font-size: 0.8125rem;
}
.stSelectbox [data-baseweb="select"]:hover > div {
border-color: var(--primary-light) !important;
}
.stSelectbox [data-baseweb="select"]:focus-within > div {
border-color: var(--primary) !important;
box-shadow: 0 0 0 2px var(--primary-bg) !important;
}
/* 多选框 */
.stMultiSelect [data-baseweb="select"] > div {
border-radius: var(--radius-md) !important;
border: 1px solid var(--border) !important;
}
.stMultiSelect [data-baseweb="tag"] {
border-radius: var(--radius-sm) !important;
background: var(--primary-bg) !important;
color: var(--primary) !important;
border: 1px solid var(--primary-light) !important;
font-size: 0.6875rem;
font-weight: 500;
padding: 0.125rem 0.375rem;
}
/* ========== Radio & Checkbox ========== */
.stRadio [data-baseweb="radio"] {
font-size: 0.8125rem;
}
.stRadio [data-baseweb="radio-control"] {
border-color: var(--border);
}
.stRadio [data-baseweb="radio-control"]:checked {
background: var(--primary);
border-color: var(--primary);
}
/* ========== Slider ========== */
.stSlider [data-baseweb="slider"] {
height: 4px;
}
.stSlider [data-baseweb="thumb"] {
background: var(--primary);
border: 2px solid white;
box-shadow: var(--shadow-sm);
}
/* ========== 容器 - 紧凑 ========== */
div[data-testid="stVerticalBlockBorderWrapper"] {
border-radius: var(--radius-lg);
border: 1px solid var(--border);
box-shadow: var(--shadow-xs);
background: var(--bg-white);
padding: 1rem !important;
margin-bottom: 0.5rem !important;
}
/* ========== Expander - 紧凑 ========== */
.streamlit-expanderHeader {
border-radius: var(--radius-md);
background: var(--bg-white);
border: 1px solid var(--border);
font-weight: 500;
font-size: 0.8125rem;
padding: 0.5rem 0.75rem;
}
.streamlit-expanderHeader:hover {
background: var(--bg-gray);
border-color: var(--primary-light);
}
.streamlit-expanderContent {
padding: 0.5rem 0 !important;
}
/* ========== 表格 ========== */
.stDataFrame {
border-radius: var(--radius-lg);
overflow: hidden;
border: 1px solid var(--border);
font-size: 0.8125rem;
}
/* ========== 提示框 - 紧凑 ========== */
.stAlert {
border-radius: var(--radius-md);
font-size: 0.8125rem;
border: none;
padding: 0.5rem 0.75rem;
}
div[data-baseweb="notification"] {
border-radius: var(--radius-md);
font-size: 0.8125rem;
padding: 0.5rem 0.75rem;
}
/* ========== 分割线 - 紧凑 ========== */
hr {
border: none;
border-top: 1px solid var(--border);
margin: 0.75rem 0;
}
/* ========== Markdown 内容 - 紧凑 ========== */
.stMarkdown {
font-size: 0.8125rem;
line-height: 1.5;
}
.stMarkdown p {
margin-bottom: 0.375rem;
}
.stMarkdown h1,
.stMarkdown h2,
.stMarkdown h3,
.stMarkdown h4 {
font-weight: 600;
color: var(--text-primary);
margin-top: 0.75rem;
margin-bottom: 0.375rem;
font-size: 0.875rem; font-size: 0.875rem;
} }
/* 侧边栏全宽 */ /* 表单 */
.stForm {
border: none !important;
padding: 0 !important;
}
/* ========== 响应式 ========== */
@media (max-width: 768px) {
.main .block-container {
padding: 1rem;
}
[data-testid="stMetric"] {
padding: 0.75rem;
height: 80px !important;
}
[data-testid="stMetricValue"] {
font-size: 1.125rem !important;
}
.stTabs [data-baseweb="tab"] {
padding: 0.375rem 0.625rem;
font-size: 0.75rem;
}
section[data-testid="stSidebar"] { section[data-testid="stSidebar"] {
width: 100% !important; width: 100% !important;
min-width: unset !important; min-width: unset !important;
} }
/* 主内容区 padding */ div[data-testid="stVerticalBlockBorderWrapper"] {
.main .block-container { padding: 0.75rem !important;
padding: 1rem;
} }
} }
@media (max-width: 480px) { @media (max-width: 480px) {
/* KPI 卡片单列 */
[data-testid="stHorizontalBlock"] > div {
flex: 1 1 100%;
}
/* 标题缩小 */
h1 { h1 {
font-size: 1.5rem !important; font-size: 1.25rem !important;
} }
} }
/* ========== 滚动条美化 ========== */ /* ========== 滚动条 ========== */
::-webkit-scrollbar { ::-webkit-scrollbar {
width: 6px; width: 4px;
height: 6px; height: 4px;
} }
::-webkit-scrollbar-track { ::-webkit-scrollbar-track {
background: var(--secondary-bg); background: transparent;
} }
::-webkit-scrollbar-thumb { ::-webkit-scrollbar-thumb {
background: var(--border-color); background: var(--border);
border-radius: 3px; border-radius: 2px;
} }
::-webkit-scrollbar-thumb:hover { ::-webkit-scrollbar-thumb:hover {
background: #A0AEC0; background: var(--text-muted);
} }
/* ========== 选中文本颜色 ========== */ /* ========== 选中文本 ========== */
::selection { ::selection {
background: rgba(37,99,235,0.2); background: var(--primary-bg);
color: var(--text-color); color: var(--primary-dark);
}
/* ========== 动画 ========== */
@keyframes fadeIn {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}
.stApp > .main {
animation: fadeIn 0.2s ease;
}
/* ========== 移除多余间距 ========== */
.element-container {
margin-bottom: 0 !important;
}
div[data-testid="stVerticalBlock"] > div {
margin-bottom: 0 !important;
}
/* 紧凑型 columns 间距 */
[data-testid="stHorizontalBlock"] {
gap: 0.5rem !important;
} }
</style> </style>
""", """,