zhangliang před 3 týdny
rodič
revize
38705ecd74
1 změnil soubory, kde provedl 328 přidání a 49 odebrání
  1. 328 49
      dashboard/app.py

+ 328 - 49
dashboard/app.py

@@ -60,6 +60,95 @@ def get_keywords():
         return pd.read_sql(sql, conn)
 
 
+def add_keyword(keyword):
+    """
+    添加关键词:如果存在则激活,不存在则新增
+    """
+    from sqlalchemy import text
+    
+    engine = get_db_connection()
+    with engine.begin() as conn:
+        # 检查是否已存在
+        check_sql = text("SELECT id, is_active FROM wx_trend_keywords WHERE keyword = :keyword")
+        result = conn.execute(check_sql, {"keyword": keyword}).fetchone()
+        
+        if result:
+            # 已存在,激活
+            if result[1] == 0:
+                update_sql = text("UPDATE wx_trend_keywords SET is_active = 1 WHERE id = :id")
+                conn.execute(update_sql, {"id": result[0]})
+                return True, f"关键词 '{keyword}' 已重新激活"
+            else:
+                return False, f"关键词 '{keyword}' 已存在且处于激活状态"
+        else:
+            # 不存在,新增
+            insert_sql = text("INSERT INTO wx_trend_keywords (keyword, is_active, priority) VALUES (:keyword, 1, 0)")
+            conn.execute(insert_sql, {"keyword": keyword})
+            return True, f"关键词 '{keyword}' 已成功添加"
+
+
+def delete_keyword(keyword_id):
+    """
+    删除关键词:将is_active改为0
+    """
+    from sqlalchemy import text
+    
+    engine = get_db_connection()
+    with engine.begin() as conn:
+        sql = text("UPDATE wx_trend_keywords SET is_active = 0 WHERE id = :id")
+        result = conn.execute(sql, {"id": keyword_id})
+        return result.rowcount > 0
+
+
+def fetch_real_time_data(keyword):
+    """
+    实时查询关键词数据
+    """
+    import httpx
+    from datetime import datetime, timedelta
+    from app.core.config import settings
+    
+    end_date = datetime.now()
+    start_date = end_date - timedelta(days=settings.LIMIT_DAY)
+    
+    payload = {
+        "keyword": keyword,
+        "start_ymd": str(start_date.strftime("%Y%m%d")),
+        "end_ymd": str(end_date.strftime("%Y%m%d"))
+    }
+    
+    try:
+        with httpx.Client(timeout=30.0) as client:
+            resp = client.post(settings.API_URL, json=payload)
+            resp.raise_for_status()
+            data = resp.json()
+            
+            if data.get('code') != 0:
+                return False, data.get('msg', 'API请求失败')
+            
+            raw_list = data.get('data', {}).get('data', [])
+            raw_list.sort(key=lambda x: x['ymd'])
+            
+            if not raw_list:
+                return False, '未获取到数据'
+            
+            # 转换为DataFrame
+            df_list = []
+            for item in raw_list:
+                row = {
+                    'keyword': keyword,
+                    'ymd': item['ymd'],
+                    'date': pd.to_datetime(item['ymd'], format='%Y%m%d')
+                }
+                row.update(item['channel_score'])
+                df_list.append(row)
+            
+            return True, pd.DataFrame(df_list)
+            
+    except Exception as e:
+        return False, f"查询失败: {str(e)}"
+
+
 @st.cache_data(ttl=3600)
 def get_trend_data(keyword_ids, start_date, end_date):
     if not keyword_ids:
@@ -102,49 +191,245 @@ def get_trend_data(keyword_ids, start_date, end_date):
 # Sidebar
 # ======================
 
-st.sidebar.header("🔍 筛选条件")
+# 初始化session state
+if 'recent_keywords' not in st.session_state:
+    st.session_state['recent_keywords'] = []
 
-kw_df = get_keywords()
-selected_keywords = st.sidebar.multiselect(
-    "关键词",
-    options=kw_df['keyword'].tolist(),
-    default=kw_df['keyword'].iloc[:3].tolist() if not kw_df.empty else []
-)
+# ======================
+# 实时查询
+# ======================
 
-selected_ids = kw_df[kw_df['keyword'].isin(selected_keywords)]['id'].tolist()
+with st.sidebar.expander("⚡ 实时查询", expanded=True):
+    realtime_keyword = st.text_input("输入要实时查询的关键词", placeholder="例如:区块链")
+    if st.button("实时查询"):
+        if realtime_keyword.strip():
+            with st.spinner(f"正在查询 '{realtime_keyword}' 的实时数据..."):
+                success, result = fetch_real_time_data(realtime_keyword.strip())
+                if success:
+                    # 将实时数据存储到session state
+                    st.session_state['realtime_data'] = result
+                    st.session_state['realtime_keyword'] = realtime_keyword.strip()
+                    # 更新最近使用的关键词
+                    if realtime_keyword.strip() not in st.session_state['recent_keywords']:
+                        st.session_state['recent_keywords'].insert(0, realtime_keyword.strip())
+                        # 只保留最近10个
+                        st.session_state['recent_keywords'] = st.session_state['recent_keywords'][:10]
+                    st.success("实时查询成功")
+                else:
+                    st.error(f"查询失败: {result}")
+        else:
+            st.warning("请输入关键词")
+    
+    # 最近使用的关键词
+    if st.session_state['recent_keywords']:
+        st.markdown("**最近查询**:")
+        for i, kw in enumerate(st.session_state['recent_keywords'][:5]):
+            if st.button(kw, key=f"recent_{i}", help="点击使用此关键词"):
+                # 触发实时查询
+                success, result = fetch_real_time_data(kw)
+                if success:
+                    st.session_state['realtime_data'] = result
+                    st.session_state['realtime_keyword'] = kw
+                    st.success(f"已查询关键词: {kw}")
+                else:
+                    st.error(f"查询失败: {result}")
 
-date_range = st.sidebar.date_input(
-    "日期范围",
-    value=(datetime.now() - timedelta(days=30), datetime.now()),
-    max_value=datetime.now()
-)
+# ======================
+# 筛选条件
+# ======================
 
-selected_metrics = st.sidebar.multiselect(
-    "展示维度",
-    options=list(FIELD_MAPPING.keys()),
-    format_func=lambda x: FIELD_MAPPING[x],
-    default=['total_score']
-)
+with st.sidebar.expander("🔍 筛选条件", expanded=True):
+    kw_df = get_keywords()
+    keyword_options = kw_df['keyword'].tolist()
+    
+    # 关键词搜索
+    search_term = st.text_input("搜索关键词", placeholder="输入关键词搜索")
+    
+    # 过滤关键词
+    filtered_keywords = keyword_options
+    if search_term:
+        filtered_keywords = [kw for kw in keyword_options if search_term.lower() in kw.lower()]
+    
+    # 关键词选择:支持多选
+    selected_keywords = st.multiselect(
+        "选择关键词(可多选)",
+        options=filtered_keywords,
+        default=filtered_keywords[:3] if filtered_keywords else [],
+        placeholder="请选择关键词"
+    )
+    
+    # 自定义关键词输入
+    custom_keyword = ""
+    if st.checkbox("输入自定义关键词", value=False):
+        custom_keyword = st.text_input("自定义关键词", placeholder="例如:春节")
+    
+    # 确定最终的关键词
+    final_keywords = selected_keywords.copy()
+    if custom_keyword:
+        final_keywords.append(custom_keyword)
+    
+    # 获取选中关键词的ID(仅适用于列表中的关键词)
+    selected_ids = []
+    for keyword in selected_keywords:
+        if keyword in keyword_options:
+            selected_ids.extend(kw_df[kw_df['keyword'] == keyword]['id'].tolist())
+    
+    date_range = st.date_input(
+        "日期范围",
+        value=(datetime.now() - timedelta(days=30), datetime.now()),
+        max_value=datetime.now()
+    )
+    
+    selected_metrics = st.multiselect(
+        "展示维度",
+        options=list(FIELD_MAPPING.keys()),
+        format_func=lambda x: FIELD_MAPPING[x],
+        default=['total_score']
+    )
+    
+    # 快捷操作
+    col1, col2 = st.columns(2)
+    with col1:
+        if st.button("清空筛选"):
+            # 清空筛选条件
+            st.session_state['realtime_data'] = None
+            st.session_state['realtime_keyword'] = None
+            st.success("筛选条件已清空")
+    with col2:
+        if st.button("刷新数据"):
+            # 清除缓存
+            get_keywords.clear()
+            st.success("数据已刷新")
 
 # ======================
-# Main
+# 关键词管理
 # ======================
 
-st.title("微信指数趋势洞察")
-
-if not selected_ids:
-    st.warning("请至少选择一个关键词")
-    st.stop()
+with st.sidebar.expander("📝 关键词管理", expanded=False):
+    # 添加关键词
+    new_keyword = st.text_input("输入新关键词", placeholder="例如:人工智能")
+    if st.button("添加关键词"):
+        if new_keyword.strip():
+            success, message = add_keyword(new_keyword.strip())
+            if success:
+                st.success(message)
+                # 清除缓存
+                get_keywords.clear()
+                # 重新加载关键词列表
+                kw_df = get_keywords()
+            else:
+                st.warning(message)
+        else:
+            st.warning("请输入关键词")
+    
+    # 删除关键词
+    if not kw_df.empty:
+        keyword_options = {row['keyword']: row['id'] for _, row in kw_df.iterrows()}
+        selected_del_keyword = st.selectbox(
+            "选择要删除的关键词",
+            options=list(keyword_options.keys()),
+            index=None,
+            placeholder="请选择关键词"
+        )
+        if st.button("删除关键词"):
+            if selected_del_keyword:
+                keyword_id = keyword_options[selected_del_keyword]
+                success = delete_keyword(keyword_id)
+                if success:
+                    st.success(f"关键词 '{selected_del_keyword}' 已删除")
+                    # 清除缓存
+                    get_keywords.clear()
+                    # 重新加载关键词列表
+                    kw_df = get_keywords()
+                else:
+                    st.error("删除失败,请重试")
+            else:
+                st.warning("请选择要删除的关键词")
 
-if len(date_range) != 2:
-    st.warning("请选择完整的日期范围")
-    st.stop()
+# ======================
+# 数据导出
+# ======================
 
-with st.spinner("正在加载数据..."):
-    df = get_trend_data(selected_ids, date_range[0], date_range[1])
+# with st.sidebar.expander("💾 数据导出", expanded=False):
+#     st.markdown("**导出当前数据**:")
+#     if st.button("导出为CSV"):
+#         if 'df' in locals():
+#             import io
+#             csv = df.to_csv(index=False)
+#             st.download_button(
+#                 label="下载CSV文件",
+#                 data=csv,
+#                 file_name=f"微信指数_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv",
+#                 mime="text/csv"
+#             )
+#         else:
+#             st.warning("请先查询数据")
+    
+#     if st.button("导出为Excel"):
+#         if 'df' in locals():
+#             try:
+#                 import openpyxl
+#                 import io
+#                 output = io.BytesIO()
+#                 with pd.ExcelWriter(output, engine='openpyxl') as writer:
+#                     df.to_excel(writer, index=False, sheet_name='微信指数')
+#                 output.seek(0)
+#                 st.download_button(
+#                     label="下载Excel文件",
+#                     data=output,
+#                     file_name=f"微信指数_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx",
+#                     mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
+#                 )
+#             except ImportError:
+#                 st.warning("请先安装openpyxl库以支持Excel导出")
+#         else:
+#             st.warning("请先查询数据")
 
-if df.empty:
-    st.warning("所选时间段内暂无数据")
+# ======================
+# Main
+# =====================
+
+st.title("微信指数趋势")
+
+# 检查是否有实时查询数据
+realtime_data = st.session_state.get('realtime_data')
+realtime_keyword = st.session_state.get('realtime_keyword')
+
+# 处理查询逻辑
+if final_keywords:
+    # 检查是否有自定义关键词需要实时查询
+    if custom_keyword:
+        # 执行实时查询
+        with st.spinner(f"正在实时查询 '{custom_keyword}' 的数据..."):
+            success, result = fetch_real_time_data(custom_keyword)
+            if success:
+                st.subheader(f"⚡ 实时查询结果:{custom_keyword}")
+                df = result
+            else:
+                st.error(f"查询失败: {result}")
+                st.stop()
+    elif realtime_data is not None:
+        # 显示实时查询结果
+        st.subheader(f"⚡ 实时查询结果:{realtime_keyword}")
+        df = realtime_data
+    else:
+        # 显示常规查询结果(支持多个关键词)
+        if not selected_ids:
+            st.warning("请至少选择一个关键词")
+            st.stop()
+
+        if len(date_range) != 2:
+            st.warning("请选择完整的日期范围")
+            st.stop()
+
+        with st.spinner("正在加载数据..."):
+            df = get_trend_data(selected_ids, date_range[0], date_range[1])
+
+        if df.empty:
+            st.warning("所选时间段内暂无数据")
+            st.stop()
+else:
+    st.warning("请选择或输入关键词")
     st.stop()
 
 # ======================
@@ -153,9 +438,11 @@ if df.empty:
 
 st.markdown("### 💡 最新指数概览")
 
-cols = st.columns(min(len(selected_keywords), 4))
+# 获取唯一的关键词列表
+unique_keywords = df['keyword'].unique().tolist()
+cols = st.columns(min(len(unique_keywords), 4))
 
-for i, kw in enumerate(selected_keywords):
+for i, kw in enumerate(unique_keywords):
     kw_df = df[df['keyword'] == kw].sort_values("date")
     if kw_df.empty:
         continue
@@ -202,12 +489,11 @@ fig.update_traces(
     mode="lines",
     line=dict(width=3),
     hovertemplate=(
-        "日期:%{x|%Y.%m.%d}<br>"
-        "关键词:%{text}<br>"
+        # "日期:%{x|%Y.%m.%d}<br>"
+        "关键词:%{data.name}<br>"
         "指数:%{y:,.0f}"
         "<extra></extra>"
-    ),
-    text=df['keyword']  # 使用 text 参数传递关键词
+    )
 )
 
 fig.update_layout(
@@ -236,18 +522,11 @@ fig.update_layout(
         gridcolor="rgba(255,255,255,0.06)",
         tickfont=dict(size=12),
         tickformat=",d"
-    )
-)
-fig.update_traces(
-    mode="lines",
-    line=dict(width=3),
-    hovertemplate=(
-        # "日期:%{x|%Y.%m.%d}<br>"
-        "关键词:%{text}<br>"
-        "指数:%{y:,.0f}"
-        "<extra></extra>"
     ),
-    text=df['keyword']
+    hoverlabel=dict(
+        font_size=12,
+        font_family="Arial"
+    )
 )
 
 st.plotly_chart(fig, width='stretch')