Files
leaudit-platform-frontend/auth_doc/version_management_v3_frontend_guide.md
2025-12-05 00:09:32 +08:00

39 KiB
Raw Permalink Blame History

版本管理 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>
  );
};

📝 总结

核心要点

  1. change_from_previous 是关键指标

    • 负数 = 改进
    • 正数 = 退步
    • 零 = 持平 ➡️
  2. LLM 开关灵活使用

    • 默认关闭(快速、免费)
    • 需要时开启(智能、有成本)
  3. 分步加载优化体验

    • 先显示统计结果
    • 后台加载 LLM 分析
  4. 完善的错误处理

    • 401: 跳转登录
    • 404: 权限提示
    • 500: 重试提示

🔗 相关资源


维护者: Claude Code 联系方式: 通过 GitHub Issues 反馈问题 最后更新: 2025-11-18