| | |
| | | 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'; |
| | | |
| | | /** |
| | |
| | | * 按原型 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)); |
| | | }; |
| | |
| | | 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; |
| | | {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.id}> |
| | | <div className="case-card" key={item.cpwsCaseTextId || item.id || item.caseId}> |
| | | <div |
| | | className={isExpanded ? 'case-header active' : 'case-header'} |
| | | onClick={() => handleToggleCase(item.id)} |
| | | onClick={() => handleToggleCase(item.cpwsCaseTextId || item.id || item.caseId)} |
| | | > |
| | | <div className="case-title-container"> |
| | | <h3 className="case-title"> |
| | | {item.title} |
| | | <span className="similarity-tag"> |
| | | {item.caseName || item.title || item.caseTitle || '未命名案例'} |
| | | {similarityLevel && ( |
| | | <span className={`similarity-tag ${similarityLevel.className}`}> |
| | | <i className="fas fa-chart-line"></i> |
| | | {item.similarity} |
| | | {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.date}</span> |
| | | <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.location}</span> |
| | | <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.type}</span> |
| | | <span>案由类型:{item.caseType}</span> |
| | | </div> |
| | | )} |
| | | </div> |
| | | <div |
| | | className={ |
| | | item.caseType === 'mediation' |
| | | (item.caseType || item.type) === 'mediation' |
| | | ? 'case-type-badge mediation' |
| | | : 'case-type-badge judgment' |
| | | } |
| | | > |
| | | <i className={item.caseType === 'mediation' ? 'fas fa-handshake' : 'fas fa-gavel'}></i> |
| | | {item.caseType === 'mediation' ? '调解案例' : '判决文书'} |
| | | <i className={(item.caseType || item.type) === 'mediation' ? 'fas fa-handshake' : 'fas fa-gavel'}></i> |
| | | {(item.caseType || item.type) === 'mediation' ? '调解案例' : '判决文书'} |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <button className="toggle-btn" onClick={() => handleToggleCase(item.id)}> |
| | | <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> |
| | | |
| | | <div className={isExpanded ? 'case-content expanded' : 'case-content'}> |
| | | <div className="case-detail"> |
| | | {item.overview && ( |
| | | {(item.basicCaseInfo || item.overview || item.caseOverview) && ( |
| | | <div className="detail-section"> |
| | | <h4 className="detail-title">案例概述</h4> |
| | | <div className="detail-content"> |
| | | <p>{item.overview}</p> |
| | | <p>{item.basicCaseInfo || item.overview || item.caseOverview}</p> |
| | | </div> |
| | | </div> |
| | | )} |
| | | |
| | | {item.background && ( |
| | | {(item.background || item.caseBackground) && ( |
| | | <div className="detail-section"> |
| | | <h4 className="detail-title">调解/审理背景</h4> |
| | | <div className="detail-content"> |
| | | <p>{item.background}</p> |
| | | <p>{item.background || item.caseBackground}</p> |
| | | </div> |
| | | </div> |
| | | )} |
| | | |
| | | {item.plaintiffDemand && item.plaintiffDemand.length > 0 && ( |
| | | {(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.map((demand, index) => ( |
| | | {(item.plaintiffDemand || item.demands).map((demand, index) => ( |
| | | <li key={index}>{demand}</li> |
| | | ))} |
| | | </ol> |
| | |
| | | </div> |
| | | )} |
| | | |
| | | {item.courtDecision && ( |
| | | {(item.judgment || item.courtDecision) && ( |
| | | <div className="detail-section court-decision"> |
| | | <h4 className="detail-title">法院审理与判决</h4> |
| | | <div className="detail-content"> |
| | | <p>{item.courtDecision}</p> |
| | | <p>{item.judgment || item.courtDecision}</p> |
| | | </div> |
| | | </div> |
| | | )} |
| | | |
| | | {item.mediationScheme && item.mediationScheme.length > 0 && ( |
| | | {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 && Array.isArray(item.mediationScheme) && ( |
| | | <div className="detail-section mediation-scheme"> |
| | | <h4 className="detail-title">调解方案</h4> |
| | | <div className="detail-content"> |
| | |
| | | </div> |
| | | )} |
| | | |
| | | {item.mediationResult && item.mediationResult.length > 0 && ( |
| | | {item.mediationResult && Array.isArray(item.mediationResult) && ( |
| | | <div className="detail-section mediation-result"> |
| | | <h4 className="detail-title">调解结果</h4> |
| | | <div className="detail-content"> |
| | |
| | | </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> |
| | | |
| | |
| | | </h2> |
| | | |
| | | <div className="laws-count"> |
| | | 与本案相关的法律条文共 <strong>{mockRelatedLaws.length}条</strong> |
| | | 与本案相关的法律条文共 <strong>{displayLaws.length}条</strong> |
| | | </div> |
| | | |
| | | <div className="laws-list"> |
| | | {mockRelatedLaws.map((law) => ( |
| | | {displayLaws.length > 0 ? ( |
| | | displayLaws.map((law) => ( |
| | | <div |
| | | key={law.id} |
| | | className={law.id === activeLawId ? 'law-card active' : 'law-card'} |
| | | onClick={() => handleSelectLaw(law.id)} |
| | | 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.name}</h3> |
| | | <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.status}</span> |
| | | <span>时效性:{law.lawValidityName}</span> |
| | | </div> |
| | | )} |
| | | {law.authorityName && ( |
| | | <div className="law-meta-item"> |
| | | <i className="fas fa-landmark"></i> |
| | | <span>制定机关:{law.authority}</span> |
| | | <span>制定机关:{law.authorityName}</span> |
| | | </div> |
| | | <div className="law-meta-item"> |
| | | <i className="far fa-calendar-alt"></i> |
| | | <span>公布日期:{law.publishDate}</span> |
| | | </div> |
| | | )} |
| | | </div> |
| | | {/* 法条内容区域 */} |
| | | {(law.provisionIndex && law.provisionText) && ( |
| | | <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> |
| | | </div> |
| | | ))} |
| | | <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> |
| | | )} |