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