From 4eb71775167ae903aea17bb410c6201e872daf66 Mon Sep 17 00:00:00 2001
From: tony.cheng <chengmingwei_1984122@126.com>
Date: Thu, 05 Feb 2026 16:35:50 +0800
Subject: [PATCH] feat: 优化类案推荐功能 - 相似度分级、分页加载、详情字段扩展及法条显示优化

---
 web-app/src/components/tools/SimilarCaseContent.jsx |  515 +++++++++++++++++++++++++++++++++++++++++----------------
 1 files changed, 371 insertions(+), 144 deletions(-)

diff --git a/web-app/src/components/tools/SimilarCaseContent.jsx b/web-app/src/components/tools/SimilarCaseContent.jsx
index b156767..638f6ce 100644
--- a/web-app/src/components/tools/SimilarCaseContent.jsx
+++ b/web-app/src/components/tools/SimilarCaseContent.jsx
@@ -1,5 +1,7 @@
-import React, { useState } from 'react';
-import { mockSimilarCases, mockRelatedLaws } from '../../mocks/similarCaseMocks';
+import React, { useState, useEffect, useCallback } from 'react';
+import { useCaseData } from '../../contexts/CaseDataContext';
+import RecommendAPIService from '../../services/RecommendAPIService';
+import { Spin, Alert } from 'antd';
 import './SimilarCaseContent.css';
 
 /**
@@ -7,9 +9,107 @@
  * 按原型 similar_case.html 和 UI 图实现
  */
 const SimilarCaseContent = () => {
+  // 状态管理
+  const [cases, setCases] = useState([]);
+  const [laws, setLaws] = useState([]);
+  const [loading, setLoading] = useState(false);
+  const [error, setError] = useState(null);
   const [expandedId, setExpandedId] = useState(null);
-  const [activeLawId, setActiveLawId] = useState(mockRelatedLaws[0]?.id || null);
-
+  const [activeLawId, setActiveLawId] = useState(null);
+  // 分页加载状态
+  const [loadedCount, setLoadedCount] = useState(3); // 默认加载3条
+  
+  // 获取timeline数据
+  const { caseData: timeline } = useCaseData();
+  
+  // 参数构建函数
+  const buildRequestParams = (timelineData) => {
+    return {
+      caseDes: timelineData.case_des || '',
+      caseClaim: timelineData.case_claim || '',
+      caseId: timelineData.case_id
+    };
+  };
+  
+  // API调用函数
+  const loadRecommendations = useCallback(async () => {
+    setLoading(true);
+    setError(null);
+    
+    try {
+      const params = buildRequestParams(timeline);
+      
+      // 并行调用类案和法条推荐API
+      const [casesResult, lawsResult] = await Promise.all([
+        RecommendAPIService.getSimilarCases({
+          caseDes: params.caseDes,
+          caseClaim: params.caseClaim,
+          caseId: params.caseId
+        }),
+        RecommendAPIService.getSimilarLaws({
+          caseDes: params.caseDes,
+          caseClaim: params.caseClaim,
+          caseId: params.caseId
+        })
+      ]);
+      
+      // 从API返回的data中提取similarCases和provisions/laws数组
+      const casesData = casesResult.data?.similarCases || casesResult.data?.cases || [];
+      const lawsData = lawsResult.data?.provisions || lawsResult.data?.laws || [];
+      
+      console.log('类案推荐数据:', casesData);
+      console.log('法条推荐数据:', lawsData);
+      
+      setCases(casesData);
+      setLaws(lawsData);
+      
+      // 设置默认选中的法条
+      if (lawsData && lawsData.length > 0) {
+        setActiveLawId(lawsData[0].lawProvisionId || lawsData[0].id || lawsData[0].lawId || null);
+      }
+    } catch (err) {
+      setError(err.message);
+      console.error('获取推荐数据失败:', err);
+      // 不使用mock数据降级,直接显示错误
+      setCases([]);
+      setLaws([]);
+      setActiveLawId(null);
+    } finally {
+      setLoading(false);
+    }
+  }, [timeline]);
+  
+  // 自动加载数据
+  useEffect(() => {
+    if (timeline && (timeline.caseDes || timeline.case_claim || timeline.case_id)) {
+      loadRecommendations();
+    }
+  }, [loadRecommendations]);
+  
+  // 使用API数据,如果没有数据则显示空状态,不使用mock数据
+  // 确保displayCases和displayLaws始终为数组类型,避免TypeError
+  const displayCases = Array.isArray(cases) ? cases : [];
+  const displayLaws = Array.isArray(laws) ? laws : [];
+  const activeLaw = displayLaws.length > 0 ? (displayLaws.find((law) => (law.lawProvisionId || law.id) === activeLawId) || displayLaws[0]) : null;
+  
+  // 分页显示的案例(默认3条,每次加载3条)
+  const displayedCases = displayCases.slice(0, loadedCount);
+  const hasMore = loadedCount < displayCases.length;
+  const totalCases = displayCases.length;
+  
+  // 相似度分级函数
+  const getSimilarityLevel = (score) => {
+    const numScore = typeof score === 'string' ? parseFloat(score) : score;
+    if (numScore >= 0.6) {
+      return { text: '极高相似度', className: 'extreme-similarity' };
+    }
+    if (numScore >= 0.5) {
+      return { text: '高相似度', className: 'high-similarity' };
+    }
+    return { text: '一般相似度', className: 'normal-similarity' };
+  };
+  
+  // 事件处理函数
   const handleToggleCase = (id) => {
     setExpandedId((prev) => (prev === id ? null : id));
   };
@@ -17,139 +117,238 @@
   const handleSelectLaw = (id) => {
     setActiveLawId(id);
   };
-
-  const activeLaw = mockRelatedLaws.find((law) => law.id === activeLawId) || mockRelatedLaws[0];
+  
+  // 加载更多案例
+  const handleLoadMore = () => {
+    setLoadedCount((prev) => Math.min(prev + 3, totalCases));
+  };
+  
+  // 渲染Loading状态
+  if (loading) {
+    return (
+      <div className="similar-case-container" style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '600px' }}>
+        <Spin size="large" tip="正在加载推荐数据..." />
+      </div>
+    );
+  }
+  
+  // 渲染错误状态
+  if (error) {
+    return (
+      <div className="similar-case-container">
+        <Alert 
+          message="数据加载失败" 
+          description={error}
+          type="error" 
+          showIcon
+          style={{ marginBottom: '20px' }}
+        />
+        <div style={{ textAlign: 'center', padding: '40px' }}>
+          <p>暂时无法获取推荐数据,请稍后重试。</p>
+        </div>
+      </div>
+    );
+  }
 
   return (
     <div className="similar-case-container">
-      {/* 左侧:相似典型案例TOP3 */}
+      {/* 左侧:相似典型案例TOP{loadedCount} */}
       <section className="cases-section">
         <h2 className="similar-section-title">
           <i className="fas fa-folder-open"></i>
-          相似典型案例TOP3
+          相似典型案例TOP{loadedCount}
         </h2>
 
         <div className="cases-list">
-          {mockSimilarCases.map((item) => {
-            const isExpanded = expandedId === item.id;
-            return (
-              <div className="case-card" key={item.id}>
-                <div
-                  className={isExpanded ? 'case-header active' : 'case-header'}
-                  onClick={() => handleToggleCase(item.id)}
-                >
-                  <div className="case-title-container">
-                    <h3 className="case-title">
-                      {item.title}
-                      <span className="similarity-tag">
-                        <i className="fas fa-chart-line"></i>
-                        {item.similarity}
-                      </span>
-                    </h3>
-                    <div className="case-meta-container">
-                      <div className="case-meta">
-                        <div className="case-meta-item">
-                          <i className="far fa-calendar-alt"></i>
-                          <span>发生时间:{item.date}</span>
+          {displayedCases.length > 0 ? (
+            displayedCases.map((item) => {
+              const isExpanded = expandedId === (item.cpwsCaseTextId || item.id || item.caseId);
+              const similarity = item.similarity || item.score;
+              const similarityLevel = similarity ? getSimilarityLevel(similarity) : null;
+              return (
+                <div className="case-card" key={item.cpwsCaseTextId || item.id || item.caseId}>
+                  <div
+                    className={isExpanded ? 'case-header active' : 'case-header'}
+                    onClick={() => handleToggleCase(item.cpwsCaseTextId || item.id || item.caseId)}
+                  >
+                    <div className="case-title-container">
+                      <h3 className="case-title">
+                        {item.caseName || item.title || item.caseTitle || '未命名案例'}
+                        {similarityLevel && (
+                          <span className={`similarity-tag ${similarityLevel.className}`}>
+                            <i className="fas fa-chart-line"></i>
+                            {similarityLevel.text}
+                          </span>
+                        )}
+                      </h3>
+                      <div className="case-meta-container">
+                        <div className="case-meta">
+                          {item.caseNumber && (
+                            <div className="case-meta-item">
+                              <i className="fas fa-file-alt"></i>
+                              <span>案号:{item.caseNumber}</span>
+                            </div>
+                          )}
+                          {(item.date || item.occurTime || item.judgmentDate) && (
+                            <div className="case-meta-item">
+                              <i className="far fa-calendar-alt"></i>
+                              <span>日期:{item.judgmentDate || item.date || item.occurTime}</span>
+                            </div>
+                          )}
+                          {(item.court || item.location) && (
+                            <div className="case-meta-item">
+                              <i className="fas fa-map-marker-alt"></i>
+                              <span>{item.court ? '法院:' : '地点:'}{item.court || item.location}</span>
+                            </div>
+                          )}
+                          {item.caseType && (
+                            <div className="case-meta-item">
+                              <i className="fas fa-balance-scale"></i>
+                              <span>案由类型:{item.caseType}</span>
+                            </div>
+                          )}
                         </div>
-                        <div className="case-meta-item">
-                          <i className="fas fa-map-marker-alt"></i>
-                          <span>发生地点:{item.location}</span>
+                        <div
+                          className={
+                            (item.caseType || item.type) === 'mediation'
+                              ? 'case-type-badge mediation'
+                              : 'case-type-badge judgment'
+                          }
+                        >
+                          <i className={(item.caseType || item.type) === 'mediation' ? 'fas fa-handshake' : 'fas fa-gavel'}></i>
+                          {(item.caseType || item.type) === 'mediation' ? '调解案例' : '判决文书'}
                         </div>
-                        <div className="case-meta-item">
-                          <i className="fas fa-balance-scale"></i>
-                          <span>纠纷类型:{item.type}</span>
-                        </div>
-                      </div>
-                      <div
-                        className={
-                          item.caseType === 'mediation'
-                            ? 'case-type-badge mediation'
-                            : 'case-type-badge judgment'
-                        }
-                      >
-                        <i className={item.caseType === 'mediation' ? 'fas fa-handshake' : 'fas fa-gavel'}></i>
-                        {item.caseType === 'mediation' ? '调解案例' : '判决文书'}
                       </div>
                     </div>
+                    <button className="toggle-btn" onClick={() => handleToggleCase(item.cpwsCaseTextId || item.id || item.caseId)}>
+                      <i className={isExpanded ? 'fas fa-chevron-up' : 'fas fa-chevron-down'}></i>
+                    </button>
                   </div>
-                  <button className="toggle-btn" onClick={() => handleToggleCase(item.id)}>
-                    <i className={isExpanded ? 'fas fa-chevron-up' : 'fas fa-chevron-down'}></i>
-                  </button>
-                </div>
 
-                <div className={isExpanded ? 'case-content expanded' : 'case-content'}>
-                  <div className="case-detail">
-                    {item.overview && (
-                      <div className="detail-section">
-                        <h4 className="detail-title">案例概述</h4>
-                        <div className="detail-content">
-                          <p>{item.overview}</p>
+                  <div className={isExpanded ? 'case-content expanded' : 'case-content'}>
+                    <div className="case-detail">
+                      {(item.basicCaseInfo || item.overview || item.caseOverview) && (
+                        <div className="detail-section">
+                          <h4 className="detail-title">案例概述</h4>
+                          <div className="detail-content">
+                            <p>{item.basicCaseInfo || item.overview || item.caseOverview}</p>
+                          </div>
                         </div>
-                      </div>
-                    )}
+                      )}
 
-                    {item.background && (
-                      <div className="detail-section">
-                        <h4 className="detail-title">调解/审理背景</h4>
-                        <div className="detail-content">
-                          <p>{item.background}</p>
+                      {(item.background || item.caseBackground) && (
+                        <div className="detail-section">
+                          <h4 className="detail-title">调解/审理背景</h4>
+                          <div className="detail-content">
+                            <p>{item.background || item.caseBackground}</p>
+                          </div>
                         </div>
-                      </div>
-                    )}
+                      )}
 
-                    {item.plaintiffDemand && item.plaintiffDemand.length > 0 && (
-                      <div className="detail-section plaintiff-demand">
-                        <h4 className="detail-title">原告诉讼请求</h4>
-                        <div className="detail-content">
-                          <ol>
-                            {item.plaintiffDemand.map((demand, index) => (
-                              <li key={index}>{demand}</li>
-                            ))}
-                          </ol>
+                      {(item.plaintiffDemand || item.demands) && Array.isArray(item.plaintiffDemand || item.demands) && (
+                        <div className="detail-section plaintiff-demand">
+                          <h4 className="detail-title">原告诉讼请求</h4>
+                          <div className="detail-content">
+                            <ol>
+                              {(item.plaintiffDemand || item.demands).map((demand, index) => (
+                                <li key={index}>{demand}</li>
+                              ))}
+                            </ol>
+                          </div>
                         </div>
-                      </div>
-                    )}
+                      )}
 
-                    {item.courtDecision && (
-                      <div className="detail-section court-decision">
-                        <h4 className="detail-title">法院审理与判决</h4>
-                        <div className="detail-content">
-                          <p>{item.courtDecision}</p>
+                      {(item.judgment || item.courtDecision) && (
+                        <div className="detail-section court-decision">
+                          <h4 className="detail-title">法院审理与判决</h4>
+                          <div className="detail-content">
+                            <p>{item.judgment || item.courtDecision}</p>
+                          </div>
                         </div>
-                      </div>
-                    )}
+                      )}
+                      
+                      {item.legalBasis && (
+                        <div className="detail-section">
+                          <h4 className="detail-title">法律依据</h4>
+                          <div className="detail-content">
+                            <p>{item.legalBasis}</p>
+                          </div>
+                        </div>
+                      )}
+                      
+                      {item.trialFinding && (
+                        <div className="detail-section">
+                          <h4 className="detail-title">审理查明</h4>
+                          <div className="detail-content">
+                            <p>{item.trialFinding}</p>
+                          </div>
+                        </div>
+                      )}
+                      
+                      {item.trialProcess && (
+                        <div className="detail-section">
+                          <h4 className="detail-title">审理经过</h4>
+                          <div className="detail-content">
+                            <p>{item.trialProcess}</p>
+                          </div>
+                        </div>
+                      )}
 
-                    {item.mediationScheme && item.mediationScheme.length > 0 && (
-                      <div className="detail-section mediation-scheme">
-                        <h4 className="detail-title">调解方案</h4>
-                        <div className="detail-content">
-                          <ul>
-                            {item.mediationScheme.map((scheme, index) => (
-                              <li key={index}>{scheme}</li>
-                            ))}
-                          </ul>
+                      {item.mediationScheme && Array.isArray(item.mediationScheme) && (
+                        <div className="detail-section mediation-scheme">
+                          <h4 className="detail-title">调解方案</h4>
+                          <div className="detail-content">
+                            <ul>
+                              {item.mediationScheme.map((scheme, index) => (
+                                <li key={index}>{scheme}</li>
+                              ))}
+                            </ul>
+                          </div>
                         </div>
-                      </div>
-                    )}
+                      )}
 
-                    {item.mediationResult && item.mediationResult.length > 0 && (
-                      <div className="detail-section mediation-result">
-                        <h4 className="detail-title">调解结果</h4>
-                        <div className="detail-content">
-                          <ol>
-                            {item.mediationResult.map((r, index) => (
-                              <li key={index}>{r}</li>
-                            ))}
-                          </ol>
+                      {item.mediationResult && Array.isArray(item.mediationResult) && (
+                        <div className="detail-section mediation-result">
+                          <h4 className="detail-title">调解结果</h4>
+                          <div className="detail-content">
+                            <ol>
+                              {item.mediationResult.map((r, index) => (
+                                <li key={index}>{r}</li>
+                              ))}
+                            </ol>
+                          </div>
                         </div>
-                      </div>
-                    )}
+                      )}
+                    </div>
                   </div>
                 </div>
+              );
+            })
+          ) : (
+            <div className="empty-state">
+              <div className="empty-icon">
+                <i className="fas fa-file-alt"></i>
               </div>
-            );
-          })}
+              <p className="empty-text">暂无类案推荐</p>
+            </div>
+          )}
+          
+          {/* 加载更多按钮 */}
+          {displayedCases.length > 0 && (
+            <div className="load-more-container">
+              {hasMore ? (
+                <button className="load-more-btn" onClick={handleLoadMore}>
+                  <i className="fas fa-angle-down"></i>
+                  加载更多
+                </button>
+              ) : (
+                <div className="no-more-data">
+                  <i className="fas fa-check-circle"></i>
+                  没有更多案例数据
+                </div>
+              )}
+            </div>
+          )}
         </div>
       </section>
 
@@ -161,54 +360,82 @@
         </h2>
 
         <div className="laws-count">
-          与本案相关的法律条文共 <strong>{mockRelatedLaws.length}条</strong>
+          与本案相关的法律条文共 <strong>{displayLaws.length}条</strong>
         </div>
 
         <div className="laws-list">
-          {mockRelatedLaws.map((law) => (
-            <div
-              key={law.id}
-              className={law.id === activeLawId ? 'law-card active' : 'law-card'}
-              onClick={() => handleSelectLaw(law.id)}
-            >
-              <h3 className="law-title">{law.name}</h3>
-              <div className="law-meta">
-                <div className="law-meta-item">
-                  <i className="fas fa-check-circle" style={{ color: 'var(--success-color)' }}></i>
-                  <span>时效性:{law.status}</span>
+          {displayLaws.length > 0 ? (
+            displayLaws.map((law) => (
+              <div
+                key={law.lawProvisionId || law.id || law.lawId}
+                className={(law.lawProvisionId || law.id) === activeLawId ? 'law-card active' : 'law-card'}
+                onClick={() => handleSelectLaw(law.lawProvisionId || law.id || law.lawId)}
+              >
+                <h3 className="law-title">{law.lawTitle || '未命名法条'}</h3>
+                <div className="law-meta">
+                  {law.lawValidityName && (
+                    <div className="law-meta-item">
+                      <i className="fas fa-check-circle" style={{ color: 'var(--success-color)' }}></i>
+                      <span>时效性:{law.lawValidityName}</span>
+                    </div>
+                  )}
+                  {law.authorityName && (
+                    <div className="law-meta-item">
+                      <i className="fas fa-landmark"></i>
+                      <span>制定机关:{law.authorityName}</span>
+                    </div>
+                  )}
                 </div>
-                <div className="law-meta-item">
-                  <i className="fas fa-landmark"></i>
-                  <span>制定机关:{law.authority}</span>
-                </div>
-                <div className="law-meta-item">
-                  <i className="far fa-calendar-alt"></i>
-                  <span>公布日期:{law.publishDate}</span>
-                </div>
-              </div>
-              {/* 法条内容区域 */}
-              <div className="law-content">
-                {law.articles.map((article, index) => (
-                  <div className="law-article" key={index}>
-                    <span className="article-number">{article.number}</span>
-                    <span>{article.content}</span>
+                {/* 法条内容区域 */}
+                {(law.provisionIndex && law.provisionText) && (
+                  <div className="law-content">
+                    <div className="law-article">
+                      <span className="article-number">{law.provisionIndex}</span>
+                      <span>{law.provisionText}</span>
+                    </div>
                   </div>
-                ))}
+                )}
               </div>
+            ))
+          ) : (
+            <div className="empty-state">
+              <div className="empty-icon">
+                <i className="fas fa-gavel"></i>
+              </div>
+              <p className="empty-text">暂无相关专业法条数据</p>
             </div>
-          ))}
+          )}
 
           {/* 当前选中法条详情 - 放在 laws-list 内部实现整体滚动 */}
           {activeLaw && (
             <div className="law-detail-panel">
-              <h3 className="law-detail-title">{activeLaw.name}</h3>
-              <div className="law-detail-content">
-                {activeLaw.articles.map((article, index) => (
-                  <div className="law-article" key={index}>
-                    <span className="article-number">{article.number}</span>
-                    <span>{article.content}</span>
+              <h3 className="law-detail-title">{activeLaw.lawTitle || '未命名法条'}</h3>
+              
+              {/* 添加时效性和制定机关信息 */}
+              <div className="law-meta" style={{ marginBottom: '15px' }}>
+                {activeLaw.lawValidityName && (
+                  <div className="law-meta-item">
+                    <i className="fas fa-check-circle" style={{ color: 'var(--success-color)' }}></i>
+                    <span>时效性:{activeLaw.lawValidityName}</span>
                   </div>
-                ))}
+                )}
+                {activeLaw.authorityName && (
+                  <div className="law-meta-item">
+                    <i className="fas fa-landmark"></i>
+                    <span>制定机关:{activeLaw.authorityName}</span>
+                  </div>
+                )}
+              </div>
+              
+              <div className="law-detail-content">
+                {(activeLaw.provisionIndex && activeLaw.provisionText) ? (
+                  <div className="law-article">
+                    <span className="article-number">{activeLaw.provisionIndex}</span>
+                    <span>{activeLaw.provisionText}</span>
+                  </div>
+                ) : (
+                  <p>暂无详细内容</p>
+                )}
               </div>
             </div>
           )}

--
Gitblit v1.8.0