优化UI模块和资源推荐功能
Made-with: Cursor
This commit is contained in:
+20
-24
@@ -168,9 +168,12 @@ def load_default_cfg():
|
||||
for key in ["brand", "advantages", "competitors", "temperature"]:
|
||||
if key in app_config and app_config[key]:
|
||||
base_cfg[key] = app_config[key]
|
||||
except Exception:
|
||||
except FileNotFoundError:
|
||||
# secrets.toml 不存在时静默忽略,用户可通过侧边栏配置
|
||||
pass
|
||||
except Exception as e:
|
||||
import logging
|
||||
logging.warning(f"读取 secrets.toml 失败: {e}")
|
||||
|
||||
return base_cfg
|
||||
|
||||
@@ -230,28 +233,11 @@ ss_init("image_descriptions", []) # 图片描述列表
|
||||
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):
|
||||
"""保留你原本的"必须填写所有 API Key"约束,但不 st.stop:改为禁用按钮 + 提示。"""
|
||||
"""验证配置完整性,返回 (是否有效, 错误列表)。"""
|
||||
errors = []
|
||||
warnings = []
|
||||
|
||||
if not cfg.get("gen_api_key", "").strip():
|
||||
errors.append("生成&优化 LLM 的 API Key 未填写")
|
||||
|
||||
@@ -264,7 +250,12 @@ def validate_cfg(cfg: dict):
|
||||
if not verify_keys.get(vp, "").strip():
|
||||
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:
|
||||
@@ -408,9 +399,14 @@ with st.sidebar:
|
||||
st.session_state.cfg_applied = False
|
||||
|
||||
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:
|
||||
st.success("配置已就绪,可运行全部模块。")
|
||||
with st.container(border=True):
|
||||
st.markdown("**✅ 配置已就绪**")
|
||||
st.caption("所有功能已解锁,可以开始使用")
|
||||
|
||||
st.markdown("---")
|
||||
if st.button("重置全部结果(不删除配置)", use_container_width=True, key="sb_reset_all"):
|
||||
|
||||
+178
-141
@@ -10,31 +10,47 @@ class ResourceRecommender:
|
||||
"""GEO 资源推荐器"""
|
||||
|
||||
def __init__(self):
|
||||
# GEO 代理列表
|
||||
# GEO 代理/服务列表
|
||||
self.agents = [
|
||||
{
|
||||
"name": "KrillinAI",
|
||||
"description": "专业的 GEO 代理服务,提供高质量的内容生成和优化",
|
||||
"url": "https://krillin.ai",
|
||||
"category": "代理服务",
|
||||
"name": "Perplexity AI",
|
||||
"description": "AI 搜索引擎,可用于验证 GEO 效果",
|
||||
"url": "https://www.perplexity.ai",
|
||||
"category": "AI 搜索",
|
||||
"rating": "⭐⭐⭐⭐⭐",
|
||||
"features": ["内容生成", "SEO 优化", "多平台支持"]
|
||||
"features": ["实时搜索", "引用来源", "多模型支持"]
|
||||
},
|
||||
{
|
||||
"name": "AutoGPT",
|
||||
"description": "自动化 AI 代理,支持 GEO 内容创作",
|
||||
"url": "https://autogpt.net",
|
||||
"category": "代理服务",
|
||||
"rating": "⭐⭐⭐⭐",
|
||||
"features": ["自动化", "多任务", "API 集成"]
|
||||
"name": "ChatGPT Search",
|
||||
"description": "OpenAI 的搜索功能,验证品牌在 AI 搜索中的表现",
|
||||
"url": "https://chat.openai.com",
|
||||
"category": "AI 搜索",
|
||||
"rating": "⭐⭐⭐⭐⭐",
|
||||
"features": ["GPT-4", "实时联网", "引用分析"]
|
||||
},
|
||||
{
|
||||
"name": "AgentGPT",
|
||||
"description": "基于 GPT 的智能代理,支持 GEO 策略执行",
|
||||
"url": "https://agentgpt.reworkd.ai",
|
||||
"category": "代理服务",
|
||||
"name": "Google SGE",
|
||||
"description": "Google 搜索生成体验,了解 AI 搜索趋势",
|
||||
"url": "https://search.google",
|
||||
"category": "AI 搜索",
|
||||
"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 = [
|
||||
{
|
||||
"name": "Google Search Console",
|
||||
"description": "监控网站搜索表现,优化 GEO 效果",
|
||||
"description": "监控网站在 Google 搜索中的表现",
|
||||
"url": "https://search.google.com/search-console",
|
||||
"category": "SEO 工具",
|
||||
"category": "搜索引擎工具",
|
||||
"rating": "⭐⭐⭐⭐⭐",
|
||||
"features": ["搜索分析", "索引监控", "性能报告"]
|
||||
},
|
||||
@@ -52,44 +68,84 @@ class ResourceRecommender:
|
||||
"name": "Bing Webmaster Tools",
|
||||
"description": "Bing 搜索引擎的网站管理工具",
|
||||
"url": "https://www.bing.com/webmasters",
|
||||
"category": "SEO 工具",
|
||||
"category": "搜索引擎工具",
|
||||
"rating": "⭐⭐⭐⭐",
|
||||
"features": ["索引提交", "搜索分析", "URL 检查"]
|
||||
},
|
||||
{
|
||||
"name": "Schema.org Validator",
|
||||
"description": "验证 JSON-LD Schema 标记",
|
||||
"description": "验证 JSON-LD Schema 标记是否正确",
|
||||
"url": "https://validator.schema.org",
|
||||
"category": "技术工具",
|
||||
"category": "结构化数据",
|
||||
"rating": "⭐⭐⭐⭐⭐",
|
||||
"features": ["Schema 验证", "结构化数据测试", "错误检测"]
|
||||
},
|
||||
{
|
||||
"name": "Rich Results Test",
|
||||
"description": "Google 富媒体结果测试工具",
|
||||
"name": "Google Rich Results Test",
|
||||
"description": "测试网页是否支持 Google 富媒体搜索结果",
|
||||
"url": "https://search.google.com/test/rich-results",
|
||||
"category": "技术工具",
|
||||
"category": "结构化数据",
|
||||
"rating": "⭐⭐⭐⭐⭐",
|
||||
"features": ["富媒体测试", "预览效果", "错误诊断"]
|
||||
},
|
||||
{
|
||||
"name": "PageSpeed Insights",
|
||||
"description": "网站性能分析工具,影响 GEO 排名",
|
||||
"description": "分析网页性能,Core Web Vitals 指标",
|
||||
"url": "https://pagespeed.web.dev",
|
||||
"category": "性能工具",
|
||||
"rating": "⭐⭐⭐⭐⭐",
|
||||
"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 = [
|
||||
{
|
||||
"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",
|
||||
"description": "Google 官方 E-E-A-T 指南,GEO 核心原则",
|
||||
"url": "https://developers.google.com/search/docs/fundamentals/creating-helpful-content",
|
||||
"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": "高"
|
||||
},
|
||||
{
|
||||
@@ -101,88 +157,92 @@ class ResourceRecommender:
|
||||
"importance": "高"
|
||||
},
|
||||
{
|
||||
"title": "GEO Strategy Guide",
|
||||
"description": "GEO(Generative Engine Optimization)策略指南",
|
||||
"url": "https://github.com/mprimi/portable-seed",
|
||||
"category": "策略指南",
|
||||
"title": "Google Structured Data Guidelines",
|
||||
"description": "Google 结构化数据指南和最佳实践",
|
||||
"url": "https://developers.google.com/search/docs/appearance/structured-data",
|
||||
"category": "技术文档",
|
||||
"date": "2024",
|
||||
"importance": "高"
|
||||
},
|
||||
{
|
||||
"title": "AI Search Optimization",
|
||||
"description": "AI 搜索引擎优化最佳实践",
|
||||
"title": "AI Search Optimization Guide",
|
||||
"description": "AI 搜索引擎优化最佳实践指南",
|
||||
"url": "https://www.searchenginejournal.com/ai-search-optimization",
|
||||
"category": "最佳实践",
|
||||
"date": "2024",
|
||||
"importance": "中"
|
||||
},
|
||||
{
|
||||
"title": "LLM Prompt Engineering",
|
||||
"description": "大语言模型提示工程指南",
|
||||
"title": "LLM Prompt Engineering Guide",
|
||||
"description": "大语言模型提示工程完整指南",
|
||||
"url": "https://www.promptingguide.ai",
|
||||
"category": "技术指南",
|
||||
"date": "持续更新",
|
||||
"importance": "中"
|
||||
},
|
||||
{
|
||||
"title": "Content Quality Guidelines",
|
||||
"description": "高质量内容创作指南",
|
||||
"url": "https://developers.google.com/search/docs/fundamentals/creating-helpful-content",
|
||||
"category": "内容指南",
|
||||
"date": "2024",
|
||||
"importance": "中"
|
||||
}
|
||||
]
|
||||
|
||||
# 社区资源
|
||||
self.communities = [
|
||||
{
|
||||
"name": "GEO Reddit Community",
|
||||
"description": "GEO 相关讨论和经验分享",
|
||||
"name": "r/SEO (Reddit)",
|
||||
"description": "Reddit SEO 社区,讨论 SEO 和 GEO 策略",
|
||||
"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": "⭐⭐⭐⭐"
|
||||
},
|
||||
{
|
||||
"name": "AI SEO Discord",
|
||||
"description": "AI SEO 和 GEO 技术交流社区",
|
||||
"url": "https://discord.gg/ai-seo",
|
||||
"category": "社区论坛",
|
||||
"name": "SEO Twitter/X Community",
|
||||
"description": "SEO 和 GEO 从业者 Twitter 社区",
|
||||
"url": "https://twitter.com/search?q=SEO%20GEO",
|
||||
"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": "⭐⭐⭐⭐"
|
||||
}
|
||||
]
|
||||
|
||||
def get_agents(self, category: Optional[str] = None) -> List[Dict]:
|
||||
"""
|
||||
获取 GEO 代理列表
|
||||
|
||||
Args:
|
||||
category: 分类筛选(可选)
|
||||
|
||||
Returns:
|
||||
代理列表
|
||||
"""
|
||||
"""获取 GEO 代理列表"""
|
||||
if category:
|
||||
return [agent for agent in self.agents if agent.get("category") == category]
|
||||
return self.agents
|
||||
|
||||
def get_tools(self, category: Optional[str] = None) -> List[Dict]:
|
||||
"""
|
||||
获取工具推荐列表
|
||||
|
||||
Args:
|
||||
category: 分类筛选(可选)
|
||||
|
||||
Returns:
|
||||
工具列表
|
||||
"""
|
||||
"""获取工具推荐列表"""
|
||||
if category:
|
||||
return [tool for tool in self.tools if tool.get("category") == category]
|
||||
return self.tools
|
||||
|
||||
def get_papers(self, category: Optional[str] = None, importance: Optional[str] = None) -> List[Dict]:
|
||||
"""
|
||||
获取论文/指南列表
|
||||
|
||||
Args:
|
||||
category: 分类筛选(可选)
|
||||
importance: 重要性筛选(可选:高、中、低)
|
||||
|
||||
Returns:
|
||||
论文/指南列表
|
||||
"""
|
||||
"""获取论文/指南列表"""
|
||||
result = self.papers
|
||||
if category:
|
||||
result = [p for p in result if p.get("category") == category]
|
||||
@@ -191,81 +251,58 @@ class ResourceRecommender:
|
||||
return result
|
||||
|
||||
def get_communities(self) -> List[Dict]:
|
||||
"""
|
||||
获取社区资源列表
|
||||
|
||||
Returns:
|
||||
社区列表
|
||||
"""
|
||||
"""获取社区资源列表"""
|
||||
return self.communities
|
||||
|
||||
def search_resources(self, query: str, resource_type: Optional[str] = None) -> List[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:
|
||||
统计字典
|
||||
"""
|
||||
def get_resource_summary(self) -> Dict:
|
||||
"""获取资源统计摘要"""
|
||||
return {
|
||||
"total": len(self.agents) + len(self.tools) + len(self.papers) + len(self.communities),
|
||||
"agents": len(self.agents),
|
||||
"tools": len(self.tools),
|
||||
"papers": len(self.papers),
|
||||
"communities": len(self.communities),
|
||||
"total": len(self.agents) + len(self.tools) + len(self.papers) + len(self.communities)
|
||||
"communities": 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
@@ -28,7 +28,7 @@ def init_session_state():
|
||||
|
||||
# 关键词模块
|
||||
ss_init("keywords", [])
|
||||
ss_init("kw_last_num", 40)
|
||||
ss_init("kw_last_num", 20)
|
||||
ss_init("kw_generation_mode", "AI生成")
|
||||
ss_init("wordbanks", None)
|
||||
|
||||
|
||||
@@ -92,7 +92,7 @@ def render_tab_autowrite(
|
||||
|
||||
keywords_to_generate = []
|
||||
if mode == "单篇生成":
|
||||
col1, col2 = st.columns([2, 1])
|
||||
col1, col2 = st.columns(2)
|
||||
with col1:
|
||||
selected_keyword = st.selectbox(
|
||||
"选择关键词",
|
||||
@@ -110,7 +110,7 @@ def render_tab_autowrite(
|
||||
if selected_keyword:
|
||||
keywords_to_generate = [(selected_keyword, platform)]
|
||||
else:
|
||||
col1, col2 = st.columns([3, 1])
|
||||
col1, col2 = st.columns(2)
|
||||
with col1:
|
||||
selected_keywords = st.multiselect(
|
||||
"选择关键词(可多选)",
|
||||
|
||||
@@ -64,13 +64,13 @@ def render_tab_config_optimizer(
|
||||
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:
|
||||
analyze_btn = st.button("🔍 分析配置优化", type="primary", use_container_width=True, key="tab10_optimize_config")
|
||||
|
||||
with col2:
|
||||
if st.session_state.config_optimization_result:
|
||||
st.success("✅ 已有优化结果,可直接查看下方建议")
|
||||
st.success("✅ 已有优化结果")
|
||||
|
||||
# 执行分析
|
||||
if analyze_btn:
|
||||
|
||||
@@ -140,7 +140,6 @@ def render_tab_keywords(storage, ss_init, gen_llm, brand: str, advantages: str)
|
||||
st.rerun()
|
||||
|
||||
# 统一更新所有词库按钮
|
||||
st.markdown("---")
|
||||
if st.button(
|
||||
"💾 更新所有词库",
|
||||
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",
|
||||
)
|
||||
|
||||
st.markdown("---")
|
||||
|
||||
# 导入
|
||||
uploaded_wordbanks = st.file_uploader(
|
||||
"导入词库(JSON)",
|
||||
@@ -199,8 +196,6 @@ def render_tab_keywords(storage, ss_init, gen_llm, brand: str, advantages: str)
|
||||
except Exception as e:
|
||||
st.error(f"导入失败:{e}")
|
||||
|
||||
st.markdown("---")
|
||||
|
||||
# 重置为默认词库
|
||||
if st.button(
|
||||
"重置为默认词库",
|
||||
@@ -213,8 +208,6 @@ def render_tab_keywords(storage, ss_init, gen_llm, brand: str, advantages: str)
|
||||
st.success("已重置为默认词库")
|
||||
st.rerun()
|
||||
|
||||
st.markdown("---")
|
||||
|
||||
# ========== 区域 3:生成控制 ==========
|
||||
with st.container(border=True):
|
||||
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])
|
||||
with search_col:
|
||||
search_term = st.text_input(
|
||||
"🔍 搜索关键词", key="kw_search", placeholder="输入关键词搜索..."
|
||||
"搜索关键词", key="kw_search", placeholder="🔍 输入关键词搜索...", label_visibility="collapsed"
|
||||
)
|
||||
with filter_col:
|
||||
show_original = st.checkbox(
|
||||
|
||||
@@ -79,7 +79,18 @@ def _render_upload_section(kb: KnowledgeBase):
|
||||
)
|
||||
|
||||
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,
|
||||
height=150, disabled=True)
|
||||
|
||||
@@ -169,7 +180,7 @@ def _render_search_test(kb: KnowledgeBase):
|
||||
st.markdown("#### 搜索测试")
|
||||
st.caption("测试知识库检索效果,验证文档是否被正确索引")
|
||||
|
||||
query = st.text_input("输入测试查询", placeholder="例如:产品有什么优势?")
|
||||
query = st.text_input("输入测试查询", placeholder="🔍 例如:产品有什么优势?", label_visibility="collapsed")
|
||||
|
||||
col1, col2 = st.columns(2)
|
||||
with col1:
|
||||
|
||||
@@ -38,7 +38,6 @@ def render_tab_optimize(
|
||||
)
|
||||
|
||||
# === 文章优化功能(主流程) ===
|
||||
st.markdown("---")
|
||||
st.markdown("**✏️ 文章内容优化**")
|
||||
|
||||
with st.container(border=True):
|
||||
|
||||
@@ -49,7 +49,7 @@ def render_tab_platform_sync(storage, brand: str) -> None:
|
||||
key="github_repo_name"
|
||||
)
|
||||
|
||||
col1, col2 = st.columns([1, 4])
|
||||
col1, col2 = st.columns([1, 1])
|
||||
with col1:
|
||||
if st.button("💾 保存配置", type="primary", use_container_width=True):
|
||||
if github_api_key and github_repo_owner and github_repo_name:
|
||||
|
||||
@@ -148,7 +148,6 @@ def render_tab_reports(
|
||||
st.info("📊 暂无验证数据。请先运行自动验证任务或手动验证。")
|
||||
else:
|
||||
# 数据概览
|
||||
st.markdown("---")
|
||||
st.markdown("#### 📈 数据概览")
|
||||
|
||||
col1, col2, col3, col4 = st.columns(4)
|
||||
@@ -173,7 +172,6 @@ def render_tab_reports(
|
||||
|
||||
# 1. 提及率趋势图
|
||||
if "验证时间" in verify_df.columns and len(verify_df) > 0:
|
||||
st.markdown("---")
|
||||
st.markdown("#### 📊 提及率趋势图")
|
||||
|
||||
# 按日期聚合数据
|
||||
@@ -196,7 +194,6 @@ def render_tab_reports(
|
||||
st.plotly_chart(fig_trend, use_container_width=True)
|
||||
|
||||
# 2. 平台贡献度分析(基于文章平台)
|
||||
st.markdown("---")
|
||||
st.markdown("#### 🌐 平台贡献度分析")
|
||||
|
||||
articles = storage.get_articles(brand=brand)
|
||||
@@ -223,7 +220,6 @@ def render_tab_reports(
|
||||
st.info("暂无文章数据。")
|
||||
|
||||
# 话题集群分析模块
|
||||
st.markdown("---")
|
||||
st.markdown("#### 🎯 话题集群分析")
|
||||
st.caption("基于历史关键词生成话题集群,分析内容覆盖情况,发现内容盲区")
|
||||
|
||||
@@ -390,10 +386,8 @@ def render_tab_reports(
|
||||
ideas = suggestion.get('content_ideas', [])
|
||||
if ideas:
|
||||
st.markdown(f"- **内容创意**:{', '.join(ideas[:3])}")
|
||||
st.markdown("---")
|
||||
|
||||
# ROI 分析与成本优化模块
|
||||
st.markdown("---")
|
||||
st.markdown("#### 💰 ROI 分析与成本优化")
|
||||
st.caption("量化 GEO 投入产出比,优化成本结构,数据驱动决策")
|
||||
|
||||
@@ -621,7 +615,6 @@ def render_tab_reports(
|
||||
)
|
||||
|
||||
# 3. 内容质量指标分析
|
||||
st.markdown("---")
|
||||
st.markdown("#### 📈 内容质量指标分析")
|
||||
st.caption("分析内容的信任度、权威性、参与度等关键指标,量化内容质量")
|
||||
|
||||
@@ -797,7 +790,6 @@ def render_tab_reports(
|
||||
st.error(f"获取内容质量指标失败:{e}")
|
||||
|
||||
# 4. 关键词效果排名
|
||||
st.markdown("---")
|
||||
st.markdown("#### 🎯 关键词效果排名")
|
||||
|
||||
brand_verify = verify_df[verify_df["品牌"] == brand].copy()
|
||||
@@ -828,7 +820,6 @@ def render_tab_reports(
|
||||
st.info("暂无品牌验证数据。")
|
||||
|
||||
# 4. 竞品对比分析
|
||||
st.markdown("---")
|
||||
st.markdown("#### ⚔️ 竞品对比分析")
|
||||
|
||||
if len(competitor_list) > 0:
|
||||
@@ -870,7 +861,6 @@ def render_tab_reports(
|
||||
st.info("💡 提示:在侧边栏配置竞品品牌后,可查看竞品对比分析。")
|
||||
|
||||
# 5. 负面防护监控报告
|
||||
st.markdown("---")
|
||||
st.markdown("#### 🛡️ 负面防护监控报告")
|
||||
st.caption("分析负面查询中的品牌提及情况,提供风险预警和优化建议")
|
||||
|
||||
@@ -981,7 +971,6 @@ def render_tab_reports(
|
||||
st.error(f"生成负面监控报告失败:{e}")
|
||||
|
||||
# 6. 数据导出
|
||||
st.markdown("---")
|
||||
st.markdown("#### 💾 数据导出")
|
||||
|
||||
col1, col2 = st.columns(2)
|
||||
|
||||
+63
-137
@@ -3,93 +3,86 @@
|
||||
import streamlit as st
|
||||
|
||||
from modules.resource_recommender import ResourceRecommender
|
||||
from modules.ui.components import render_tab_top_with_clear
|
||||
|
||||
|
||||
def render_tab_resources(storage, brand: str) -> None:
|
||||
"""渲染 Tab8:GEO 资源库。由主入口在 with tab8 内调用。"""
|
||||
st.markdown("### 📚 GEO 资源库")
|
||||
st.caption("发现 GEO 相关工具、代理、论文和社区资源,增强工具生态")
|
||||
render_tab_top_with_clear(
|
||||
title="📚 GEO 资源库",
|
||||
caption="发现 GEO 相关工具、代理、论文和社区资源,增强工具生态",
|
||||
clear_key="resources_clear",
|
||||
on_clear=lambda: None, # 资源库无需清空
|
||||
)
|
||||
|
||||
resource_recommender = ResourceRecommender()
|
||||
|
||||
# 资源统计概览
|
||||
summary = resource_recommender.get_resource_summary()
|
||||
stat_col1, stat_col2, stat_col3, stat_col4, stat_col5 = st.columns(5)
|
||||
with stat_col1:
|
||||
st.metric("总资源数", summary['total'])
|
||||
with stat_col2:
|
||||
st.metric("代理服务", summary['agents'])
|
||||
with stat_col3:
|
||||
st.metric("工具推荐", summary['tools'])
|
||||
with stat_col4:
|
||||
st.metric("论文/指南", summary['papers'])
|
||||
with stat_col5:
|
||||
st.metric("社区资源", summary['communities'])
|
||||
|
||||
st.markdown("---")
|
||||
stat_cols = st.columns(5)
|
||||
stats = [
|
||||
("总资源数", summary['total']),
|
||||
("AI 搜索", summary['agents']),
|
||||
("工具推荐", summary['tools']),
|
||||
("论文指南", summary['papers']),
|
||||
("社区资源", summary['communities']),
|
||||
]
|
||||
for col, (label, value) in zip(stat_cols, stats):
|
||||
with col:
|
||||
st.metric(label, value)
|
||||
|
||||
# 搜索功能
|
||||
search_col1, search_col2 = st.columns([3, 1])
|
||||
search_col1, search_col2 = st.columns([4, 1])
|
||||
with search_col1:
|
||||
search_query = st.text_input(
|
||||
"🔍 搜索资源",
|
||||
"搜索资源",
|
||||
key="resource_search",
|
||||
placeholder="输入关键词搜索代理、工具、论文、社区...",
|
||||
help="支持搜索资源名称、描述、功能特性等"
|
||||
placeholder="🔍 输入关键词搜索资源名称、描述、功能特性...",
|
||||
label_visibility="collapsed"
|
||||
)
|
||||
with search_col2:
|
||||
clear_search = st.button("清除搜索", use_container_width=True, key="clear_resource_search")
|
||||
if clear_search:
|
||||
if st.button("清除", use_container_width=True, key="clear_resource_search"):
|
||||
st.session_state.resource_search = ""
|
||||
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:
|
||||
st.markdown("#### 🤖 GEO 代理服务")
|
||||
st.caption("专业的 GEO 代理服务,提供高质量的内容生成和优化")
|
||||
st.caption("AI 搜索引擎和内容生成服务,用于验证和优化 GEO 效果")
|
||||
|
||||
if search_query:
|
||||
agents = resource_recommender.search_resources(search_query, "agents")
|
||||
if agents:
|
||||
st.info(f"🔍 找到 {len(agents)} 个匹配的代理服务")
|
||||
else:
|
||||
agents = resource_recommender.get_agents()
|
||||
|
||||
if agents:
|
||||
for i, agent in enumerate(agents, 1):
|
||||
for agent in agents:
|
||||
with st.container(border=True):
|
||||
col1, col2 = st.columns([3, 1])
|
||||
with col1:
|
||||
st.markdown(f"##### {i}. {agent['name']} {agent.get('rating', '')}")
|
||||
st.markdown(f"**{agent['name']}** {agent.get('rating', '')}")
|
||||
with col2:
|
||||
if agent.get('url'):
|
||||
st.markdown(f"[🔗 访问]({agent['url']})")
|
||||
st.link_button("访问", agent['url'], use_container_width=True)
|
||||
|
||||
st.markdown(f"**{agent['description']}**")
|
||||
st.markdown(f"**分类**:{agent.get('category', 'N/A')}")
|
||||
st.caption(agent['description'])
|
||||
|
||||
if agent.get('features'):
|
||||
st.markdown("**功能特性**:")
|
||||
features_text = " | ".join([f"✓ {f}" for f in agent['features']])
|
||||
st.markdown(features_text)
|
||||
|
||||
if agent.get('url'):
|
||||
st.markdown(f"**链接**:{agent['url']}")
|
||||
features_text = " · ".join([f"✓ {f}" for f in agent['features']])
|
||||
st.markdown(f"<small>{features_text}</small>", unsafe_allow_html=True)
|
||||
else:
|
||||
st.info("💡 暂无匹配的代理资源。尝试使用其他关键词搜索。")
|
||||
st.info("💡 暂无匹配的资源,尝试其他关键词搜索。")
|
||||
|
||||
# 工具推荐
|
||||
with resource_tab2:
|
||||
st.markdown("#### 🛠️ 工具推荐")
|
||||
st.caption("GEO 相关的工具和服务,帮助优化内容效果")
|
||||
|
||||
if search_query:
|
||||
tools = resource_recommender.search_resources(search_query, "tools")
|
||||
if tools:
|
||||
st.info(f"🔍 找到 {len(tools)} 个匹配的工具")
|
||||
else:
|
||||
tools = resource_recommender.get_tools()
|
||||
|
||||
@@ -103,144 +96,77 @@ def render_tab_resources(storage, brand: str) -> None:
|
||||
categories[cat].append(tool)
|
||||
|
||||
for category, category_tools in categories.items():
|
||||
st.markdown(f"##### 📁 {category}")
|
||||
for i, tool in enumerate(category_tools, 1):
|
||||
st.markdown(f"**{category}**")
|
||||
for tool in category_tools:
|
||||
with st.container(border=True):
|
||||
col1, col2 = st.columns([3, 1])
|
||||
with col1:
|
||||
st.markdown(f"**{tool['name']}** {tool.get('rating', '')}")
|
||||
with col2:
|
||||
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'):
|
||||
st.markdown("**功能**:")
|
||||
features_text = " | ".join([f"✓ {f}" for f in tool['features']])
|
||||
st.markdown(features_text)
|
||||
|
||||
if tool.get('url'):
|
||||
st.markdown(f"**链接**:{tool['url']}")
|
||||
features_text = " · ".join([f"✓ {f}" for f in tool['features']])
|
||||
st.markdown(f"<small>{features_text}</small>", unsafe_allow_html=True)
|
||||
else:
|
||||
st.info("💡 暂无匹配的工具资源。尝试使用其他关键词搜索。")
|
||||
st.info("💡 暂无匹配的工具,尝试其他关键词搜索。")
|
||||
|
||||
# 论文/指南
|
||||
with resource_tab3:
|
||||
st.markdown("#### 📄 论文/指南")
|
||||
st.caption("GEO 相关的论文、指南、文档,深入学习 GEO 策略")
|
||||
|
||||
if search_query:
|
||||
papers = resource_recommender.search_resources(search_query, "papers")
|
||||
if papers:
|
||||
st.info(f"🔍 找到 {len(papers)} 个匹配的论文/指南")
|
||||
else:
|
||||
papers = resource_recommender.get_papers()
|
||||
|
||||
if papers:
|
||||
# 按重要性排序
|
||||
# 按重要性分组显示
|
||||
importance_order = {"高": 3, "中": 2, "低": 1}
|
||||
papers_sorted = sorted(papers, key=lambda x: importance_order.get(x.get('importance', '低'), 1), reverse=True)
|
||||
|
||||
# 按重要性分组显示
|
||||
high_importance = [p for p in papers_sorted if p.get('importance') == '高']
|
||||
medium_importance = [p for p in papers_sorted if p.get('importance') == '中']
|
||||
low_importance = [p for p in papers_sorted if p.get('importance') == '低']
|
||||
importance_groups = {
|
||||
"🔥 必读": [p for p in papers_sorted if p.get('importance') == '高'],
|
||||
"⭐ 推荐": [p for p in papers_sorted if p.get('importance') == '中'],
|
||||
}
|
||||
|
||||
if high_importance:
|
||||
st.markdown("##### 🔥 高重要性(必读)")
|
||||
for paper in high_importance:
|
||||
for group_name, group_papers in importance_groups.items():
|
||||
if group_papers:
|
||||
st.markdown(f"**{group_name}**")
|
||||
for paper in group_papers:
|
||||
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')}")
|
||||
st.markdown(f"**{paper['title']}**")
|
||||
st.caption(paper['description'])
|
||||
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'):
|
||||
st.markdown(f"🔗 [{paper['url']}]({paper['url']})")
|
||||
|
||||
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']})")
|
||||
st.link_button("查看", paper['url'], use_container_width=True)
|
||||
else:
|
||||
st.info("💡 暂无匹配的论文/指南资源。尝试使用其他关键词搜索。")
|
||||
st.info("💡 暂无匹配的论文/指南,尝试其他关键词搜索。")
|
||||
|
||||
# 社区资源
|
||||
with resource_tab4:
|
||||
st.markdown("#### 👥 社区资源")
|
||||
st.caption("GEO 相关的社区和论坛,与其他用户交流经验")
|
||||
|
||||
if search_query:
|
||||
communities = resource_recommender.search_resources(search_query, "communities")
|
||||
if communities:
|
||||
st.info(f"🔍 找到 {len(communities)} 个匹配的社区")
|
||||
else:
|
||||
communities = resource_recommender.get_communities()
|
||||
|
||||
if communities:
|
||||
for i, community in enumerate(communities, 1):
|
||||
for community in communities:
|
||||
with st.container(border=True):
|
||||
col1, col2 = st.columns([3, 1])
|
||||
with col1:
|
||||
st.markdown(f"##### {i}. {community['name']} {community.get('rating', '')}")
|
||||
st.markdown(f"**{community['name']}** {community.get('rating', '')}")
|
||||
with col2:
|
||||
if community.get('url'):
|
||||
st.markdown(f"[🔗 访问]({community['url']})")
|
||||
st.link_button("访问", community['url'], use_container_width=True)
|
||||
|
||||
st.markdown(f"*{community['description']}*")
|
||||
st.markdown(f"**分类**:{community.get('category', 'N/A')}")
|
||||
|
||||
if community.get('url'):
|
||||
st.markdown(f"**链接**:{community['url']}")
|
||||
st.caption(community['description'])
|
||||
st.markdown(f"<small>分类:{community.get('category', 'N/A')}</small>", unsafe_allow_html=True)
|
||||
else:
|
||||
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']})")
|
||||
|
||||
# =======================
|
||||
st.info("💡 暂无匹配的社区资源,尝试其他关键词搜索。")
|
||||
|
||||
@@ -91,8 +91,6 @@ def render_tab_validation(
|
||||
st.success("✅ 负面查询已添加到验证查询中")
|
||||
st.rerun()
|
||||
|
||||
st.markdown("---")
|
||||
|
||||
with st.container(border=True):
|
||||
with st.form("verify_form", clear_on_submit=False):
|
||||
test_queries = st.text_area(
|
||||
@@ -104,7 +102,7 @@ def render_tab_validation(
|
||||
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 = 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)
|
||||
|
||||
@@ -58,7 +58,8 @@ def render_tab_workflow(
|
||||
st.rerun()
|
||||
|
||||
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):
|
||||
"""关键词生成回调函数"""
|
||||
|
||||
+448
-174
@@ -2,250 +2,524 @@ import streamlit as st
|
||||
|
||||
|
||||
def inject_global_theme():
|
||||
"""注入全局 CSS 主题,极简克制的样式优化"""
|
||||
"""注入全局 CSS 主题 - 紧凑、高信息密度的 C 端产品 UI"""
|
||||
st.markdown(
|
||||
"""
|
||||
<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 {
|
||||
--primary-color: #2563EB;
|
||||
--primary-hover: #1D4ED8;
|
||||
--background-color: #FFFFFF;
|
||||
--secondary-bg: #F7FAFC;
|
||||
--text-color: #1A202C;
|
||||
--text-secondary: #718096;
|
||||
--border-color: #E2E8F0;
|
||||
--border-radius: 10px;
|
||||
--shadow-sm: 0 1px 2px rgba(0,0,0,0.05);
|
||||
--shadow-md: 0 4px 6px rgba(0,0,0,0.07);
|
||||
--primary: #4F46E5;
|
||||
--primary-light: #818CF8;
|
||||
--primary-dark: #3730A3;
|
||||
--primary-bg: #EEF2FF;
|
||||
--success: #059669;
|
||||
--success-bg: #ECFDF5;
|
||||
--warning: #D97706;
|
||||
--warning-bg: #FFFBEB;
|
||||
--error: #DC2626;
|
||||
--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 {
|
||||
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"] {
|
||||
background: var(--secondary-bg);
|
||||
border-right: 1px solid var(--border-color);
|
||||
background: var(--bg-white);
|
||||
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 {
|
||||
background: var(--background-color);
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border-color);
|
||||
background: var(--bg-gray);
|
||||
border-radius: var(--radius-md);
|
||||
border: 1px solid var(--border);
|
||||
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 {
|
||||
background: var(--background-color);
|
||||
border-radius: 0 0 8px 8px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-top: none;
|
||||
padding: 1rem;
|
||||
background: var(--bg-white);
|
||||
border: none;
|
||||
padding: 0.5rem 0;
|
||||
}
|
||||
|
||||
/* ========== 按钮样式 ========== */
|
||||
button {
|
||||
border-radius: var(--border-radius) !important;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s ease;
|
||||
/* ========== KPI 卡片 - 统一大小 ========== */
|
||||
[data-testid="stHorizontalBlock"] {
|
||||
display: flex !important;
|
||||
align-items: stretch !important;
|
||||
gap: 0.75rem !important;
|
||||
margin-bottom: 0.75rem !important;
|
||||
}
|
||||
|
||||
button[kind="primary"] {
|
||||
background-color: var(--primary-color) !important;
|
||||
color: white !important;
|
||||
border: none !important;
|
||||
[data-testid="stHorizontalBlock"] > div {
|
||||
flex: 1 1 0% !important;
|
||||
min-width: 0 !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"] {
|
||||
background: var(--background-color);
|
||||
border-radius: var(--border-radius);
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--border-color);
|
||||
background: var(--bg-white);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 1rem 1.25rem;
|
||||
border: 1px solid var(--border);
|
||||
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 {
|
||||
box-shadow: var(--shadow-md);
|
||||
transform: translateY(-2px);
|
||||
transition: all 0.2s ease;
|
||||
border-color: var(--primary-light);
|
||||
}
|
||||
|
||||
/* ========== 响应式设计 ========== */
|
||||
@media (max-width: 768px) {
|
||||
/* KPI 卡片改为 2 列 */
|
||||
[data-testid="stHorizontalBlock"] {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
[data-testid="stHorizontalBlock"] > div {
|
||||
flex: 1 1 45%;
|
||||
margin-bottom: 0.5rem;
|
||||
[data-testid="stMetricLabel"] {
|
||||
font-size: 0.6875rem !important;
|
||||
font-weight: 600 !important;
|
||||
color: var(--text-secondary) !important;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
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"] {
|
||||
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;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
flex-wrap: nowrap;
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
.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;
|
||||
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;
|
||||
}
|
||||
|
||||
/* 侧边栏全宽 */
|
||||
/* 表单 */
|
||||
.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"] {
|
||||
width: 100% !important;
|
||||
min-width: unset !important;
|
||||
}
|
||||
|
||||
/* 主内容区 padding */
|
||||
.main .block-container {
|
||||
padding: 1rem;
|
||||
div[data-testid="stVerticalBlockBorderWrapper"] {
|
||||
padding: 0.75rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
/* KPI 卡片单列 */
|
||||
[data-testid="stHorizontalBlock"] > div {
|
||||
flex: 1 1 100%;
|
||||
}
|
||||
|
||||
/* 标题缩小 */
|
||||
h1 {
|
||||
font-size: 1.5rem !important;
|
||||
font-size: 1.25rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* ========== 滚动条美化 ========== */
|
||||
/* ========== 滚动条 ========== */
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: var(--secondary-bg);
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--border-color);
|
||||
border-radius: 3px;
|
||||
background: var(--border);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #A0AEC0;
|
||||
background: var(--text-muted);
|
||||
}
|
||||
|
||||
/* ========== 选中文本颜色 ========== */
|
||||
/* ========== 选中文本 ========== */
|
||||
::selection {
|
||||
background: rgba(37,99,235,0.2);
|
||||
color: var(--text-color);
|
||||
background: var(--primary-bg);
|
||||
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>
|
||||
""",
|
||||
|
||||
Reference in New Issue
Block a user