39 KiB
39 KiB
版本管理 v3 前端对接文档
完整的前端集成指南,包含 TypeScript 类型定义、API 调用示例和 UI 展示建议
版本: v3.0.0 最后更新: 2025-11-18 适用前端框架: React / Vue / Angular 通用
📋 目录
🚀 快速开始
1. 基础配置
// config/api.ts
export const API_BASE_URL = process.env.REACT_APP_API_URL || 'http://localhost:8000';
export const API_VERSION_PREFIX = '/admin/api/v3/versions';
// 获取完整的API路径
export const getVersionApiUrl = (path: string) => {
return `${API_BASE_URL}${API_VERSION_PREFIX}${path}`;
};
2. Axios 配置(推荐)
// utils/request.ts
import axios from 'axios';
const request = axios.create({
baseURL: API_BASE_URL,
timeout: 30000,
});
// 请求拦截器 - 添加 Token
request.interceptors.request.use(
(config) => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => Promise.reject(error)
);
// 响应拦截器 - 错误处理
request.interceptors.response.use(
(response) => response.data,
(error) => {
if (error.response?.status === 401) {
// Token 过期,跳转登录
window.location.href = '/login';
}
return Promise.reject(error);
}
);
export default request;
🔌 API 接口
接口总览
| 接口名称 | 方法 | 路径 | 说明 |
|---|---|---|---|
| 获取版本列表 | GET | /versions/{entityId} |
查询所有历史版本 |
| 版本对比 | POST | /versions/compare |
对比两个版本 |
| 获取失败评查点 | GET | /versions/{entityId}/failed-points |
查询不通过的评查点 |
| 获取统计信息 | GET | /versions/statistics |
查询统计数据 |
通用查询参数
所有接口都支持 table_name 参数(可选):
?table_name=documents // 文档(默认)
?table_name=dossiers // 卷宗
📘 TypeScript 类型定义
完整类型定义文件
// types/version.ts
/**
* 版本信息
*/
export interface VersionInfo {
version_number: number; // 版本号(1, 2, 3...)
entity_id: number; // 实体ID
entity_name: string; // 实体名称
created_at: string; // 创建时间(ISO 8601)
status: string; // 状态
total_evaluation_points: number; // 总评查点数
failed_evaluation_points: number; // 不通过的评查点数
is_current: boolean; // 是否为当前版本
metadata?: { // 扩展元数据
document_number?: string;
type_id?: number;
file_size?: number;
audit_status?: string;
[key: string]: any;
};
}
/**
* 版本列表响应
*/
export interface VersionListResponse {
current_version: VersionInfo; // 当前版本信息
entity_name: string; // 实体名称
total_versions: number; // 总版本数
versions: VersionInfo[]; // 版本列表
}
/**
* 问题详情
*/
export interface IssueDetail {
name: string; // 问题名称
description: string; // 问题描述
current_message?: string; // 当前版本消息
previous_message?: string; // 上个版本消息
}
/**
* 对比统计数据
*/
export interface ComparisonStatistics {
old_total: number; // 上个版本问题总数
new_total: number; // 当前版本问题总数
change_from_previous: number; // ✅ 对比上个版本的变化(负数=减少,正数=增加)
change_type: 'decreased' | 'increased' | 'unchanged'; // 变化类型
change_amount: number; // 变化的绝对值
improvement_rate: number; // 改进率(0-100)
resolved_count: number; // 已解决的问题数
remaining_count: number; // 仍然存在的问题数
new_issues_count: number; // 新增的问题数
}
/**
* 统计分析结果
*/
export interface StatisticalAnalysis {
type: 'statistical'; // 分析类型
summary: string; // 分析总结
conclusion: 'positive' | 'negative' | 'neutral'; // 结论
recommendations: string[]; // 建议列表
change_details: {
resolved: number;
remaining: number;
new: number;
};
}
/**
* 风险评估
*/
export interface RiskAssessment {
overall_risk: 'high' | 'medium' | 'low'; // 整体风险
critical_issues: string[]; // 关键问题列表
}
/**
* 优先级问题
*/
export interface PriorityIssue {
name: string; // 问题名称
priority: 'high' | 'medium' | 'low'; // 优先级
reason: string; // 原因说明
}
/**
* LLM 分析结果
*/
export interface LLMAnalysis {
type: 'llm'; // 分析类型
summary: string; // 分析总结
conclusion: 'positive' | 'negative' | 'neutral'; // 结论
recommendations: string[]; // 建议列表
risk_assessment?: RiskAssessment; // 风险评估
priority_issues?: PriorityIssue[]; // 优先级问题列表
change_details: {
resolved: number;
remaining: number;
new: number;
};
raw_llm_output?: Record<string, any>; // 原始LLM输出
}
/**
* 对比响应
*/
export interface ComparisonResponse {
statistics: ComparisonStatistics; // 统计数据
resolved_issues: IssueDetail[]; // 已解决的问题列表
remaining_issues: IssueDetail[]; // 仍存在的问题列表
new_issues: IssueDetail[]; // 新增的问题列表
analysis: StatisticalAnalysis | LLMAnalysis; // 分析结果(统一格式)
}
/**
* 对比请求
*/
export interface ComparisonRequest {
old_version_id: number; // 旧版本ID
new_version_id: number; // 新版本ID
enable_llm: boolean; // 是否启用LLM深度分析(默认false)
}
/**
* 失败评查点响应
*/
export interface FailedPointsResponse {
entity_id: number; // 实体ID
failed_count: number; // 失败评查点数
failed_points: Array<{
evaluation_result_id: number;
evaluation_point_id: number;
point_name: string;
point_description: string;
evaluated_results: {
result: boolean;
message: string;
[key: string]: any;
};
evaluated_point_results_log?: any;
}>;
}
/**
* 统计响应
*/
export interface StatisticsResponse {
total_documents: number; // 文档总数
total_versions: number; // 版本总数
unique_documents: number; // 唯一文档数
average_versions_per_document: number; // 平均版本数
version_distribution?: Record<string, number>; // 版本分布
}
/**
* 错误响应
*/
export interface ErrorResponse {
detail: string; // 错误详情
}
📡 请求示例
1. 获取版本列表
// api/version.ts
import request from '@/utils/request';
import type { VersionListResponse } from '@/types/version';
/**
* 获取版本列表
*/
export const getVersionList = async (
entityId: number,
tableName: string = 'documents'
): Promise<VersionListResponse> => {
return request.get(`/admin/api/v3/versions/${entityId}`, {
params: { table_name: tableName }
});
};
// 使用示例
const versionList = await getVersionList(123);
console.log('当前版本:', versionList.current_version);
console.log('总版本数:', versionList.total_versions);
2. 版本对比(统计模式)
/**
* 版本对比 - 统计模式(快速)
*/
export const compareVersions = async (
oldVersionId: number,
newVersionId: number,
tableName: string = 'documents'
): Promise<ComparisonResponse> => {
return request.post(
'/admin/api/v3/versions/compare',
{
old_version_id: oldVersionId,
new_version_id: newVersionId,
enable_llm: false // 统计模式
},
{
params: { table_name: tableName }
}
);
};
// 使用示例
const comparison = await compareVersions(122, 123);
console.log('问题变化:', comparison.statistics.change_from_previous);
console.log('改进率:', comparison.statistics.improvement_rate);
3. 版本对比(LLM 模式)
/**
* 版本对比 - LLM 模式(智能)
*/
export const compareVersionsWithLLM = async (
oldVersionId: number,
newVersionId: number,
tableName: string = 'documents'
): Promise<ComparisonResponse> => {
return request.post(
'/admin/api/v3/versions/compare',
{
old_version_id: oldVersionId,
new_version_id: newVersionId,
enable_llm: true // ✅ 启用 LLM
},
{
params: { table_name: tableName }
}
);
};
// 使用示例
const comparisonLLM = await compareVersionsWithLLM(122, 123);
if (comparisonLLM.analysis.type === 'llm') {
console.log('风险评估:', comparisonLLM.analysis.risk_assessment);
console.log('优先级问题:', comparisonLLM.analysis.priority_issues);
}
4. 获取失败评查点
/**
* 获取失败评查点
*/
export const getFailedPoints = async (
entityId: number,
tableName: string = 'documents'
): Promise<FailedPointsResponse> => {
return request.get(`/admin/api/v3/versions/${entityId}/failed-points`, {
params: { table_name: tableName }
});
};
// 使用示例
const failedPoints = await getFailedPoints(123);
console.log('失败评查点数:', failedPoints.failed_count);
5. 获取统计信息
/**
* 获取统计信息
*/
export const getStatistics = async (
tableName: string = 'documents',
dateFrom?: string,
dateTo?: string
): Promise<StatisticsResponse> => {
return request.get('/admin/api/v3/versions/statistics', {
params: {
table_name: tableName,
date_from: dateFrom,
date_to: dateTo
}
});
};
// 使用示例
const stats = await getStatistics('documents', '2025-01-01', '2025-12-31');
console.log('平均版本数:', stats.average_versions_per_document);
🎨 UI 展示建议
1. 版本列表展示
React 示例
// components/VersionList.tsx
import React from 'react';
import { Table, Tag, Badge } from 'antd';
import type { VersionInfo } from '@/types/version';
interface VersionListProps {
versions: VersionInfo[];
onVersionClick?: (version: VersionInfo) => void;
}
export const VersionList: React.FC<VersionListProps> = ({ versions, onVersionClick }) => {
const columns = [
{
title: '版本号',
dataIndex: 'version_number',
key: 'version_number',
width: 100,
render: (num: number, record: VersionInfo) => (
<div>
<strong>版本 {num}</strong>
{record.is_current && <Tag color="blue" style={{ marginLeft: 8 }}>当前</Tag>}
</div>
),
},
{
title: '创建时间',
dataIndex: 'created_at',
key: 'created_at',
render: (date: string) => new Date(date).toLocaleString('zh-CN'),
},
{
title: '评查点统计',
key: 'evaluation',
render: (_: any, record: VersionInfo) => (
<div>
<span>总数: {record.total_evaluation_points}</span>
{record.failed_evaluation_points > 0 ? (
<Badge
count={record.failed_evaluation_points}
style={{ marginLeft: 8, backgroundColor: '#ff4d4f' }}
title="不通过"
/>
) : (
<Tag color="success" style={{ marginLeft: 8 }}>全部通过</Tag>
)}
</div>
),
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
render: (status: string) => {
const statusMap: Record<string, { color: string; text: string }> = {
completed: { color: 'success', text: '已完成' },
processing: { color: 'processing', text: '处理中' },
failed: { color: 'error', text: '失败' },
};
const config = statusMap[status] || { color: 'default', text: status };
return <Tag color={config.color}>{config.text}</Tag>;
},
},
];
return (
<Table
columns={columns}
dataSource={versions}
rowKey="entity_id"
onRow={(record) => ({
onClick: () => onVersionClick?.(record),
style: { cursor: 'pointer' },
})}
/>
);
};
2. 版本对比结果展示
关键指标卡片
// components/ComparisonSummary.tsx
import React from 'react';
import { Card, Statistic, Row, Col, Tag } from 'antd';
import { ArrowDownOutlined, ArrowUpOutlined, MinusOutlined } from '@ant-design/icons';
import type { ComparisonStatistics } from '@/types/version';
interface ComparisonSummaryProps {
statistics: ComparisonStatistics;
}
export const ComparisonSummary: React.FC<ComparisonSummaryProps> = ({ statistics }) => {
// 计算变化趋势的图标和颜色
const getTrendIcon = () => {
if (statistics.change_from_previous < 0) {
return <ArrowDownOutlined style={{ color: '#52c41a' }} />;
} else if (statistics.change_from_previous > 0) {
return <ArrowUpOutlined style={{ color: '#ff4d4f' }} />;
}
return <MinusOutlined style={{ color: '#8c8c8c' }} />;
};
const getTrendColor = () => {
if (statistics.change_from_previous < 0) return '#52c41a';
if (statistics.change_from_previous > 0) return '#ff4d4f';
return '#8c8c8c';
};
const getTrendText = () => {
const abs = Math.abs(statistics.change_from_previous);
if (statistics.change_from_previous < 0) {
return `减少 ${abs} 个问题`;
} else if (statistics.change_from_previous > 0) {
return `增加 ${abs} 个问题`;
}
return '无变化';
};
return (
<Card title="版本对比摘要" bordered={false}>
<Row gutter={16}>
<Col span={6}>
<Statistic
title="旧版本问题数"
value={statistics.old_total}
valueStyle={{ color: '#8c8c8c' }}
/>
</Col>
<Col span={6}>
<Statistic
title="新版本问题数"
value={statistics.new_total}
valueStyle={{ color: statistics.new_total < statistics.old_total ? '#52c41a' : '#ff4d4f' }}
/>
</Col>
<Col span={6}>
<div>
<div style={{ fontSize: 14, color: '#8c8c8c', marginBottom: 8 }}>对比上个版本</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
{getTrendIcon()}
<span style={{ fontSize: 24, fontWeight: 500, color: getTrendColor() }}>
{getTrendText()}
</span>
</div>
</div>
</Col>
<Col span={6}>
<Statistic
title="改进率"
value={statistics.improvement_rate}
suffix="%"
valueStyle={{ color: statistics.improvement_rate > 0 ? '#52c41a' : '#8c8c8c' }}
/>
</Col>
</Row>
<Row gutter={16} style={{ marginTop: 24 }}>
<Col span={8}>
<Card size="small" style={{ backgroundColor: '#f6ffed', borderColor: '#b7eb8f' }}>
<Statistic
title="已解决"
value={statistics.resolved_count}
valueStyle={{ color: '#52c41a', fontSize: 20 }}
prefix="✓"
/>
</Card>
</Col>
<Col span={8}>
<Card size="small" style={{ backgroundColor: '#fff7e6', borderColor: '#ffd591' }}>
<Statistic
title="仍存在"
value={statistics.remaining_count}
valueStyle={{ color: '#fa8c16', fontSize: 20 }}
prefix="!"
/>
</Card>
</Col>
<Col span={8}>
<Card size="small" style={{ backgroundColor: '#fff1f0', borderColor: '#ffccc7' }}>
<Statistic
title="新增"
value={statistics.new_issues_count}
valueStyle={{ color: '#ff4d4f', fontSize: 20 }}
prefix="+"
/>
</Card>
</Col>
</Row>
{/* 变化类型标签 */}
<div style={{ marginTop: 16 }}>
{statistics.change_type === 'decreased' && (
<Tag color="success" icon={<ArrowDownOutlined />}>版本改进</Tag>
)}
{statistics.change_type === 'increased' && (
<Tag color="error" icon={<ArrowUpOutlined />}>版本退步</Tag>
)}
{statistics.change_type === 'unchanged' && (
<Tag color="default" icon={<MinusOutlined />}>版本持平</Tag>
)}
</div>
</Card>
);
};
分析结果展示
// components/AnalysisResult.tsx
import React from 'react';
import { Card, Alert, List, Tag, Descriptions } from 'antd';
import type { StatisticalAnalysis, LLMAnalysis } from '@/types/version';
interface AnalysisResultProps {
analysis: StatisticalAnalysis | LLMAnalysis;
}
export const AnalysisResult: React.FC<AnalysisResultProps> = ({ analysis }) => {
const conclusionConfig = {
positive: { type: 'success' as const, text: '积极' },
negative: { type: 'error' as const, text: '消极' },
neutral: { type: 'info' as const, text: '中性' },
};
const config = conclusionConfig[analysis.conclusion];
return (
<Card
title={
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<span>分析结果</span>
<Tag color={analysis.type === 'llm' ? 'purple' : 'blue'}>
{analysis.type === 'llm' ? '🤖 AI 分析' : '📊 统计分析'}
</Tag>
</div>
}
bordered={false}
>
{/* 总结 */}
<Alert
message="分析总结"
description={analysis.summary}
type={config.type}
showIcon
style={{ marginBottom: 16 }}
/>
{/* 建议列表 */}
<Card size="small" title="💡 改进建议" style={{ marginBottom: 16 }}>
<List
dataSource={analysis.recommendations}
renderItem={(item, index) => (
<List.Item>
<span style={{ marginRight: 8, color: '#1890ff', fontWeight: 'bold' }}>
{index + 1}.
</span>
{item}
</List.Item>
)}
/>
</Card>
{/* LLM 特有分析 */}
{analysis.type === 'llm' && (
<>
{/* 风险评估 */}
{analysis.risk_assessment && (
<Card size="small" title="🎯 风险评估" style={{ marginBottom: 16 }}>
<Descriptions column={1}>
<Descriptions.Item label="整体风险">
<Tag color={
analysis.risk_assessment.overall_risk === 'high' ? 'red' :
analysis.risk_assessment.overall_risk === 'medium' ? 'orange' : 'green'
}>
{analysis.risk_assessment.overall_risk.toUpperCase()}
</Tag>
</Descriptions.Item>
{analysis.risk_assessment.critical_issues?.length > 0 && (
<Descriptions.Item label="关键问题">
<List
size="small"
dataSource={analysis.risk_assessment.critical_issues}
renderItem={(issue) => (
<List.Item>
<Tag color="red">!</Tag> {issue}
</List.Item>
)}
/>
</Descriptions.Item>
)}
</Descriptions>
</Card>
)}
{/* 优先级问题 */}
{analysis.priority_issues && analysis.priority_issues.length > 0 && (
<Card size="small" title="⭐ 优先级问题">
<List
dataSource={analysis.priority_issues}
renderItem={(issue) => (
<List.Item>
<List.Item.Meta
avatar={
<Tag color={
issue.priority === 'high' ? 'red' :
issue.priority === 'medium' ? 'orange' : 'blue'
}>
{issue.priority.toUpperCase()}
</Tag>
}
title={issue.name}
description={issue.reason}
/>
</List.Item>
)}
/>
</Card>
)}
</>
)}
</Card>
);
};
3. 问题列表展示
// components/IssueList.tsx
import React from 'react';
import { Collapse, Badge, Tag, Empty } from 'antd';
import { CheckCircleOutlined, ExclamationCircleOutlined, PlusCircleOutlined } from '@ant-design/icons';
import type { IssueDetail } from '@/types/version';
interface IssueListProps {
resolvedIssues: IssueDetail[];
remainingIssues: IssueDetail[];
newIssues: IssueDetail[];
}
export const IssueList: React.FC<IssueListProps> = ({
resolvedIssues,
remainingIssues,
newIssues,
}) => {
const { Panel } = Collapse;
return (
<Collapse defaultActiveKey={['remaining', 'resolved']} bordered={false}>
{/* 已解决的问题 */}
<Panel
header={
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<CheckCircleOutlined style={{ color: '#52c41a', fontSize: 16 }} />
<span>已解决的问题</span>
<Badge count={resolvedIssues.length} style={{ backgroundColor: '#52c41a' }} />
</div>
}
key="resolved"
>
{resolvedIssues.length === 0 ? (
<Empty description="暂无已解决的问题" />
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
{resolvedIssues.map((issue, index) => (
<div key={index} style={{ padding: 12, backgroundColor: '#f6ffed', borderRadius: 4 }}>
<div style={{ fontWeight: 500, marginBottom: 4 }}>
<Tag color="success">✓</Tag>
{issue.name}
</div>
{issue.description && (
<div style={{ color: '#8c8c8c', fontSize: 12 }}>{issue.description}</div>
)}
{issue.previous_message && (
<div style={{ marginTop: 8, fontSize: 12, color: '#8c8c8c' }}>
原因: {issue.previous_message}
</div>
)}
</div>
))}
</div>
)}
</Panel>
{/* 仍存在的问题 */}
<Panel
header={
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<ExclamationCircleOutlined style={{ color: '#fa8c16', fontSize: 16 }} />
<span>仍存在的问题</span>
<Badge count={remainingIssues.length} style={{ backgroundColor: '#fa8c16' }} />
</div>
}
key="remaining"
>
{remainingIssues.length === 0 ? (
<Empty description="暂无仍存在的问题" />
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
{remainingIssues.map((issue, index) => (
<div key={index} style={{ padding: 12, backgroundColor: '#fff7e6', borderRadius: 4 }}>
<div style={{ fontWeight: 500, marginBottom: 4 }}>
<Tag color="warning">!</Tag>
{issue.name}
</div>
{issue.description && (
<div style={{ color: '#8c8c8c', fontSize: 12, marginBottom: 8 }}>
{issue.description}
</div>
)}
{issue.current_message && (
<div style={{ fontSize: 12 }}>
<span style={{ color: '#8c8c8c' }}>当前: </span>
{issue.current_message}
</div>
)}
{issue.previous_message && issue.previous_message !== issue.current_message && (
<div style={{ fontSize: 12, marginTop: 4 }}>
<span style={{ color: '#8c8c8c' }}>之前: </span>
{issue.previous_message}
</div>
)}
</div>
))}
</div>
)}
</Panel>
{/* 新增的问题 */}
{newIssues.length > 0 && (
<Panel
header={
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<PlusCircleOutlined style={{ color: '#ff4d4f', fontSize: 16 }} />
<span>新增的问题</span>
<Badge count={newIssues.length} style={{ backgroundColor: '#ff4d4f' }} />
</div>
}
key="new"
>
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
{newIssues.map((issue, index) => (
<div key={index} style={{ padding: 12, backgroundColor: '#fff1f0', borderRadius: 4 }}>
<div style={{ fontWeight: 500, marginBottom: 4 }}>
<Tag color="error">+</Tag>
{issue.name}
</div>
{issue.description && (
<div style={{ color: '#8c8c8c', fontSize: 12, marginBottom: 8 }}>
{issue.description}
</div>
)}
{issue.current_message && (
<div style={{ fontSize: 12, color: '#ff4d4f' }}>
{issue.current_message}
</div>
)}
</div>
))}
</div>
</Panel>
)}
</Collapse>
);
};
⚠️ 错误处理
错误处理封装
// utils/errorHandler.ts
import { message } from 'antd';
import type { AxiosError } from 'axios';
import type { ErrorResponse } from '@/types/version';
export const handleApiError = (error: AxiosError<ErrorResponse>) => {
if (error.response) {
const { status, data } = error.response;
switch (status) {
case 401:
message.error('登录已过期,请重新登录');
// 跳转到登录页
window.location.href = '/login';
break;
case 404:
message.error(data.detail || '请求的资源不存在或无权限访问');
break;
case 500:
message.error(data.detail || '服务器内部错误,请稍后重试');
break;
default:
message.error(data.detail || '请求失败,请稍后重试');
}
} else if (error.request) {
message.error('网络连接失败,请检查网络');
} else {
message.error('请求配置错误');
}
return Promise.reject(error);
};
在组件中使用
// pages/VersionComparison.tsx
import React, { useState } from 'react';
import { Button, message, Spin } from 'antd';
import { compareVersions } from '@/api/version';
import { handleApiError } from '@/utils/errorHandler';
export const VersionComparison: React.FC = () => {
const [loading, setLoading] = useState(false);
const handleCompare = async () => {
try {
setLoading(true);
const result = await compareVersions(122, 123);
message.success('对比完成');
// 处理结果...
} catch (error) {
handleApiError(error as any);
} finally {
setLoading(false);
}
};
return (
<Spin spinning={loading}>
<Button type="primary" onClick={handleCompare}>
开始对比
</Button>
</Spin>
);
};
🎯 常见场景
场景 1: 文档版本历史页面
// pages/DocumentVersionHistory.tsx
import React, { useEffect, useState } from 'react';
import { Card, Button, Modal, message } from 'antd';
import { getVersionList, compareVersions } from '@/api/version';
import { VersionList } from '@/components/VersionList';
import { ComparisonSummary } from '@/components/ComparisonSummary';
import { IssueList } from '@/components/IssueList';
import type { VersionInfo, ComparisonResponse } from '@/types/version';
interface Props {
documentId: number;
}
export const DocumentVersionHistory: React.FC<Props> = ({ documentId }) => {
const [versions, setVersions] = useState<VersionInfo[]>([]);
const [loading, setLoading] = useState(false);
const [selectedVersions, setSelectedVersions] = useState<number[]>([]);
const [comparisonResult, setComparisonResult] = useState<ComparisonResponse | null>(null);
const [modalVisible, setModalVisible] = useState(false);
// 加载版本列表
useEffect(() => {
loadVersions();
}, [documentId]);
const loadVersions = async () => {
try {
setLoading(true);
const data = await getVersionList(documentId);
setVersions(data.versions);
} catch (error) {
message.error('加载版本列表失败');
} finally {
setLoading(false);
}
};
// 选择版本进行对比
const handleVersionSelect = (version: VersionInfo) => {
if (selectedVersions.includes(version.entity_id)) {
setSelectedVersions(selectedVersions.filter(id => id !== version.entity_id));
} else if (selectedVersions.length < 2) {
setSelectedVersions([...selectedVersions, version.entity_id]);
} else {
message.warning('最多只能选择两个版本进行对比');
}
};
// 执行对比
const handleCompare = async () => {
if (selectedVersions.length !== 2) {
message.warning('请选择两个版本进行对比');
return;
}
try {
setLoading(true);
const [oldId, newId] = selectedVersions.sort((a, b) => a - b);
const result = await compareVersions(oldId, newId);
setComparisonResult(result);
setModalVisible(true);
} catch (error) {
message.error('版本对比失败');
} finally {
setLoading(false);
}
};
return (
<div>
<Card
title="版本历史"
extra={
<Button
type="primary"
disabled={selectedVersions.length !== 2}
onClick={handleCompare}
>
对比选中的版本
</Button>
}
>
<VersionList
versions={versions}
onVersionClick={handleVersionSelect}
/>
</Card>
{/* 对比结果弹窗 */}
<Modal
title="版本对比结果"
open={modalVisible}
onCancel={() => setModalVisible(false)}
width={1000}
footer={null}
>
{comparisonResult && (
<div>
<ComparisonSummary statistics={comparisonResult.statistics} />
<div style={{ marginTop: 24 }}>
<IssueList
resolvedIssues={comparisonResult.resolved_issues}
remainingIssues={comparisonResult.remaining_issues}
newIssues={comparisonResult.new_issues}
/>
</div>
</div>
)}
</Modal>
</div>
);
};
场景 2: LLM 分析开关
// components/ComparisonWithLLMOption.tsx
import React, { useState } from 'react';
import { Button, Switch, Space, message, Tooltip } from 'antd';
import { compareVersions, compareVersionsWithLLM } from '@/api/version';
interface Props {
oldVersionId: number;
newVersionId: number;
onResult: (result: any) => void;
}
export const ComparisonWithLLMOption: React.FC<Props> = ({
oldVersionId,
newVersionId,
onResult,
}) => {
const [enableLLM, setEnableLLM] = useState(false);
const [loading, setLoading] = useState(false);
const handleCompare = async () => {
try {
setLoading(true);
const result = enableLLM
? await compareVersionsWithLLM(oldVersionId, newVersionId)
: await compareVersions(oldVersionId, newVersionId);
onResult(result);
message.success(enableLLM ? '智能分析完成' : '快速对比完成');
} catch (error) {
message.error('对比失败');
} finally {
setLoading(false);
}
};
return (
<Space>
<Tooltip title={enableLLM ? '使用 AI 进行深度分析(约 2-5秒)' : '使用统计分析(约 100ms)'}>
<Switch
checked={enableLLM}
onChange={setEnableLLM}
checkedChildren="🤖 AI"
unCheckedChildren="📊 快速"
/>
</Tooltip>
<Button type="primary" loading={loading} onClick={handleCompare}>
{enableLLM ? '智能对比分析' : '快速对比'}
</Button>
{enableLLM && (
<span style={{ color: '#8c8c8c', fontSize: 12 }}>
预计耗时: 2-5秒
</span>
)}
</Space>
);
};
场景 3: 实时加载优化
// hooks/useVersionComparison.ts
import { useState, useCallback } from 'react';
import { compareVersions, compareVersionsWithLLM } from '@/api/version';
import type { ComparisonResponse } from '@/types/version';
export const useVersionComparison = () => {
const [loading, setLoading] = useState(false);
const [result, setResult] = useState<ComparisonResponse | null>(null);
const [error, setError] = useState<Error | null>(null);
const compare = useCallback(async (
oldVersionId: number,
newVersionId: number,
enableLLM: boolean = false
) => {
try {
setLoading(true);
setError(null);
// 先快速获取统计结果
const statisticalResult = await compareVersions(oldVersionId, newVersionId);
setResult(statisticalResult);
// 如果启用 LLM,后台更新为 LLM 结果
if (enableLLM) {
const llmResult = await compareVersionsWithLLM(oldVersionId, newVersionId);
setResult(llmResult);
}
} catch (err) {
setError(err as Error);
} finally {
setLoading(false);
}
}, []);
return { loading, result, error, compare };
};
// 使用示例
const MyComponent = () => {
const { loading, result, compare } = useVersionComparison();
useEffect(() => {
compare(122, 123, true); // 先显示统计,后更新为 LLM
}, []);
return <div>{/* 渲染结果 */}</div>;
};
📚 完整示例
完整的版本对比页面
// pages/VersionComparisonPage.tsx
import React, { useState, useEffect } from 'react';
import { Card, Button, Select, Space, message, Spin, Tabs } from 'antd';
import { getVersionList } from '@/api/version';
import { useVersionComparison } from '@/hooks/useVersionComparison';
import { ComparisonSummary } from '@/components/ComparisonSummary';
import { AnalysisResult } from '@/components/AnalysisResult';
import { IssueList } from '@/components/IssueList';
import { ComparisonWithLLMOption } from '@/components/ComparisonWithLLMOption';
import type { VersionInfo } from '@/types/version';
interface Props {
documentId: number;
}
export const VersionComparisonPage: React.FC<Props> = ({ documentId }) => {
const [versions, setVersions] = useState<VersionInfo[]>([]);
const [oldVersionId, setOldVersionId] = useState<number>();
const [newVersionId, setNewVersionId] = useState<number>();
const { loading, result, compare } = useVersionComparison();
useEffect(() => {
loadVersions();
}, [documentId]);
const loadVersions = async () => {
try {
const data = await getVersionList(documentId);
setVersions(data.versions);
// 自动选择最新两个版本
if (data.versions.length >= 2) {
setNewVersionId(data.versions[0].entity_id);
setOldVersionId(data.versions[1].entity_id);
}
} catch (error) {
message.error('加载版本列表失败');
}
};
const handleCompare = (enableLLM: boolean) => {
if (!oldVersionId || !newVersionId) {
message.warning('请选择要对比的版本');
return;
}
compare(oldVersionId, newVersionId, enableLLM);
};
return (
<div style={{ padding: 24 }}>
<Card title="版本对比分析" bordered={false}>
<Space direction="vertical" size="large" style={{ width: '100%' }}>
{/* 版本选择 */}
<Space>
<span>旧版本:</span>
<Select
style={{ width: 200 }}
value={oldVersionId}
onChange={setOldVersionId}
placeholder="选择旧版本"
>
{versions.map((v) => (
<Select.Option key={v.entity_id} value={v.entity_id}>
版本 {v.version_number} ({v.failed_evaluation_points} 个问题)
</Select.Option>
))}
</Select>
<span>新版本:</span>
<Select
style={{ width: 200 }}
value={newVersionId}
onChange={setNewVersionId}
placeholder="选择新版本"
>
{versions.map((v) => (
<Select.Option key={v.entity_id} value={v.entity_id}>
版本 {v.version_number} ({v.failed_evaluation_points} 个问题)
</Select.Option>
))}
</Select>
<ComparisonWithLLMOption
oldVersionId={oldVersionId!}
newVersionId={newVersionId!}
onResult={(res) => compare(oldVersionId!, newVersionId!, false)}
/>
</Space>
{/* 对比结果 */}
<Spin spinning={loading}>
{result && (
<Tabs
items={[
{
key: '1',
label: '📊 统计摘要',
children: <ComparisonSummary statistics={result.statistics} />,
},
{
key: '2',
label: '💡 分析结果',
children: <AnalysisResult analysis={result.analysis} />,
},
{
key: '3',
label: '📋 问题详情',
children: (
<IssueList
resolvedIssues={result.resolved_issues}
remainingIssues={result.remaining_issues}
newIssues={result.new_issues}
/>
),
},
]}
/>
)}
</Spin>
</Space>
</Card>
</div>
);
};
📝 总结
核心要点
-
change_from_previous 是关键指标
- 负数 = 改进 ✅
- 正数 = 退步 ❌
- 零 = 持平 ➡️
-
LLM 开关灵活使用
- 默认关闭(快速、免费)
- 需要时开启(智能、有成本)
-
分步加载优化体验
- 先显示统计结果
- 后台加载 LLM 分析
-
完善的错误处理
- 401: 跳转登录
- 404: 权限提示
- 500: 重试提示
🔗 相关资源
维护者: Claude Code 联系方式: 通过 GitHub Issues 反馈问题 最后更新: 2025-11-18